refactor(todos): update TodoRow styling and logic, add IconChecklistOutline16, and enhance AssistantMarkdown rendering
This commit is contained in:
@@ -158,7 +158,7 @@ it('renders the todo_write turn: dedicated tool row + the dock plan strip', asyn
|
|||||||
"text": "○浏览器验收",
|
"text": "○浏览器验收",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
"row": "☰更新任务清单1/3 已完成 · 实现 fixture 样本",
|
"row": "更新任务清单1/3 已完成 · 实现 fixture 样本",
|
||||||
"rowState": "ok",
|
"rowState": "ok",
|
||||||
}
|
}
|
||||||
`)
|
`)
|
||||||
|
|||||||
@@ -40,13 +40,19 @@ function ThinkRow({ text, running }: { text: string; running: boolean }) {
|
|||||||
|
|
||||||
export const AssistantMarkdown = memo(function AssistantMarkdown({ blocks, streaming, interrupted }: AssistantMarkdownProps) {
|
export const AssistantMarkdown = memo(function AssistantMarkdown({ blocks, streaming, interrupted }: AssistantMarkdownProps) {
|
||||||
const last = blocks.length - 1
|
const last = blocks.length - 1
|
||||||
|
// Tool-call heads render as tool rows in the chat view's grouping pass, so
|
||||||
|
// a node that is only those heads (or empty) would paint an empty root
|
||||||
|
// between tool groups — skip the shell unless something visible remains.
|
||||||
|
const hasVisible = streaming === true
|
||||||
|
|| interrupted === true
|
||||||
|
|| blocks.some((block) => block.kind !== 'tool-call')
|
||||||
|
if (!hasVisible) return null
|
||||||
return (
|
return (
|
||||||
<div className={css.root} data-streaming={streaming || undefined}>
|
<div className={css.root} data-streaming={streaming || undefined}>
|
||||||
{blocks.map((block, i) => {
|
{blocks.map((block, i) => {
|
||||||
switch (block.kind) {
|
switch (block.kind) {
|
||||||
case 'text': return <MarkdownText key={i} text={block.text} streaming={streaming} />
|
case 'text': return <MarkdownText key={i} text={block.text} streaming={streaming} />
|
||||||
case 'reasoning': return <ThinkRow key={i} text={block.text} running={streaming && i === last} />
|
case 'reasoning': return <ThinkRow key={i} text={block.text} running={streaming && i === last} />
|
||||||
// Tool-call heads render as tool rows in the chat view's grouping pass.
|
|
||||||
case 'tool-call': return null
|
case 'tool-call': return null
|
||||||
default: return <JsonBlock key={i} label="未知内容块" payload={block.block} />
|
default: return <JsonBlock key={i} label="未知内容块" payload={block.block} />
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,29 +1,44 @@
|
|||||||
/* todo_write plan-update row: title + progress summary on one line. */
|
/* todo_write plan-update row: ToolRow chrome (figma 780:53675) —
|
||||||
|
[16 checklist] gap6 [title 14/24] gap8 [2x2 dot] gap8 [summary FILL truncate]. */
|
||||||
|
|
||||||
.row {
|
.row {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 8px;
|
|
||||||
height: 24px;
|
height: 24px;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
font-size: 13px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.row:hover {
|
.row:hover {
|
||||||
background: var(--dsw-alias-interactive-bg-hover);
|
background: var(--dsw-alias-interactive-bg-hover);
|
||||||
}
|
}
|
||||||
|
|
||||||
.badge {
|
.leading {
|
||||||
flex: none;
|
flex: none;
|
||||||
color: var(--dsw-alias-state-business-primary);
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
margin-right: 6px;
|
||||||
|
color: var(--dsw-alias-label-tertiary);
|
||||||
}
|
}
|
||||||
|
|
||||||
.title {
|
.title {
|
||||||
flex: none;
|
flex: none;
|
||||||
font-weight: 510;
|
font-size: 14px;
|
||||||
color: var(--dsw-alias-label-primary);
|
line-height: 24px;
|
||||||
|
color: var(--dsw-alias-label-primary-dimmed);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sep {
|
||||||
|
flex: none;
|
||||||
|
width: 2px;
|
||||||
|
height: 2px;
|
||||||
|
border-radius: 1px;
|
||||||
|
margin: 0 8px;
|
||||||
|
background: var(--dsw-alias-label-caption);
|
||||||
}
|
}
|
||||||
|
|
||||||
.summary {
|
.summary {
|
||||||
@@ -32,11 +47,15 @@
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
color: var(--dsw-alias-label-secondary);
|
font-size: 14px;
|
||||||
|
line-height: 24px;
|
||||||
|
color: var(--dsw-alias-label-tertiary);
|
||||||
}
|
}
|
||||||
|
|
||||||
.err {
|
.err {
|
||||||
flex: none;
|
flex: none;
|
||||||
|
margin-left: 8px;
|
||||||
color: var(--dsw-alias-state-error-primary);
|
color: var(--dsw-alias-state-error-primary);
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
|
line-height: 16px;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,13 +3,13 @@
|
|||||||
// hole like the bash sample (a product registration, not a sample). The row
|
// hole like the bash sample (a product registration, not a sample). The row
|
||||||
// summarizes the written list (counts + active item) from the call args; the
|
// summarizes the written list (counts + active item) from the call args; the
|
||||||
// durable list itself renders in the TodoPanel above the composer, so the
|
// durable list itself renders in the TodoPanel above the composer, so the
|
||||||
// row stays one line.
|
// row stays one line. Chrome matches ToolRow (figma 780:53675).
|
||||||
|
|
||||||
import type { KeyboardEvent } from 'react'
|
import type { KeyboardEvent } from 'react'
|
||||||
import type { Context } from 'cordis'
|
import type { Context } from 'cordis'
|
||||||
import { StateDot } from '@deepseek-ai/dsh-client-ui-primitives'
|
import { IconChecklistOutline16, StateDot } from '@deepseek-ai/dsh-client-ui-primitives'
|
||||||
import type { ToolRowProps } from '../contract/slots.ts'
|
import type { ToolRowProps } from '../contract/slots.ts'
|
||||||
import { toolRowModel } from '../contract/tool-call-model.ts'
|
import { toolRowModel, type ToolRowState } from '../contract/tool-call-model.ts'
|
||||||
import css from './todo-row.module.css'
|
import css from './todo-row.module.css'
|
||||||
|
|
||||||
/** One parsed args item, shape-checked (model JSON: any field may be missing or mistyped). */
|
/** One parsed args item, shape-checked (model JSON: any field may be missing or mistyped). */
|
||||||
@@ -40,6 +40,17 @@ function summarize(argsRaw: string): string | null {
|
|||||||
: head
|
: head
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Leading-slot state substitution matches ToolRow / bash: icon yields to the
|
||||||
|
* state semantic while running or failed; ok keeps the checklist glyph. */
|
||||||
|
function leadingFor(state: ToolRowState) {
|
||||||
|
switch (state) {
|
||||||
|
case 'running': return <StateDot state="ongoing" />
|
||||||
|
case 'error': return <StateDot state="error" />
|
||||||
|
case 'stopped': return <StateDot state="warning" />
|
||||||
|
default: return <IconChecklistOutline16 />
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/** One-line plan update row (click opens the raw args in details). Non-ok
|
/** One-line plan update row (click opens the raw args in details). Non-ok
|
||||||
* execution states keep the generic row's dot semantics — a cancelled call
|
* execution states keep the generic row's dot semantics — a cancelled call
|
||||||
* wrote no todo/write, so it must not read as a completed update. */
|
* wrote no todo/write, so it must not read as a completed update. */
|
||||||
@@ -64,10 +75,9 @@ export function TodoRow({ toolName, block, openDetails }: ToolRowProps) {
|
|||||||
onClick={openDetails}
|
onClick={openDetails}
|
||||||
onKeyDown={openFromKeyboard}
|
onKeyDown={openFromKeyboard}
|
||||||
>
|
>
|
||||||
{model.state === 'ok'
|
<span className={css.leading} aria-hidden>{leadingFor(model.state)}</span>
|
||||||
? <span className={css.badge} aria-hidden>☰</span>
|
|
||||||
: <StateDot state={model.state === 'running' ? 'ongoing' : model.state === 'stopped' ? 'warning' : 'error'} />}
|
|
||||||
<span className={css.title}>更新任务清单</span>
|
<span className={css.title}>更新任务清单</span>
|
||||||
|
<span className={css.sep} aria-hidden />
|
||||||
<span className={css.summary}>{summary}</span>
|
<span className={css.summary}>{summary}</span>
|
||||||
{model.state === 'error' && <span className={css.err}>failed</span>}
|
{model.state === 'error' && <span className={css.err}>failed</span>}
|
||||||
{model.state === 'stopped' && <span className={css.err}>已中断</span>}
|
{model.state === 'stopped' && <span className={css.err}>已中断</span>}
|
||||||
|
|||||||
@@ -60,6 +60,20 @@ describe('tails', () => {
|
|||||||
expect(stopped.getByText('已停止')).toBeTruthy()
|
expect(stopped.getByText('已停止')).toBeTruthy()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('AssistantMarkdown skips the root shell when only tool-call heads remain', () => {
|
||||||
|
// Tool heads are drawn by ChatView's tool groups; an empty root between
|
||||||
|
// groups is layout noise (no text, no pulse, no interrupted marker).
|
||||||
|
const empty = render(
|
||||||
|
<AssistantMarkdown
|
||||||
|
blocks={[{ kind: 'tool-call', callId: 'c', name: 'todo_write', argsRaw: '{}' }]}
|
||||||
|
streaming={false}
|
||||||
|
/>,
|
||||||
|
)
|
||||||
|
expect(empty.container.firstChild).toBeNull()
|
||||||
|
const blank = render(<AssistantMarkdown blocks={[]} streaming={false} />)
|
||||||
|
expect(blank.container.firstChild).toBeNull()
|
||||||
|
})
|
||||||
|
|
||||||
it('a settled others-variant row renders the sparkle icon in the leading slot', () => {
|
it('a settled others-variant row renders the sparkle icon in the leading slot', () => {
|
||||||
const settled: ToolResultNode = {
|
const settled: ToolResultNode = {
|
||||||
kind: 'tool-result', seq: 2, time: 2_000, callId: 'c5',
|
kind: 'tool-result', seq: 2, time: 2_000, callId: 'c5',
|
||||||
|
|||||||
@@ -653,6 +653,16 @@ export const IconDataOutline16 = ({ size = 16, className }: IconProps) => (
|
|||||||
</svg>
|
</svg>
|
||||||
)
|
)
|
||||||
|
|
||||||
|
/** ic_checklist_outline_16 (figma extract): two rings + two list bars. */
|
||||||
|
export const IconChecklistOutline16 = ({ size = 16, className }: IconProps) => (
|
||||||
|
<svg width={size} height={size} className={className} viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path transform="translate(1.736 2.0752)" d="M12.5279 8.64648V9.92617H6.48105V8.64648H12.5279Z" fill="currentColor" />
|
||||||
|
<path transform="translate(1.736 2.0752)" d="M12.5279 1.92275V3.20244H6.48105V1.92275H12.5279Z" fill="currentColor" />
|
||||||
|
<path transform="translate(1.736 2.0752)" d="M3.84531 9.28623C3.84525 8.57774 3.271 8.00342 2.5625 8.00342C1.85405 8.00348 1.27975 8.57778 1.27969 9.28623C1.27969 9.99474 1.85401 10.569 2.5625 10.569C3.27105 10.569 3.84531 9.99478 3.84531 9.28623ZM5.12578 9.28623C5.12578 10.7017 3.97797 11.8495 2.5625 11.8495C1.14709 11.8494 0 10.7017 0 9.28623C6.59755e-05 7.87086 1.14713 6.7238 2.5625 6.72373C3.97793 6.72373 5.12572 7.87082 5.12578 9.28623Z" fill="currentColor" />
|
||||||
|
<path transform="translate(1.736 2.0752)" d="M3.84551 2.5625C3.84549 1.85402 3.27118 1.27969 2.5627 1.27969C1.85422 1.2797 1.2799 1.85403 1.27988 2.5625C1.27988 3.27098 1.85422 3.8453 2.5627 3.84531C3.27119 3.84531 3.84551 3.27099 3.84551 2.5625ZM5.1252 2.5625C5.1252 3.97792 3.97811 5.125 2.5627 5.125C1.14729 5.12499 0.000195313 3.97791 0.000195313 2.5625C0.000208508 1.1471 1.1473 1.31957e-05 2.5627 0C3.9781 0 5.12518 1.1471 5.1252 2.5625Z" fill="currentColor" />
|
||||||
|
</svg>
|
||||||
|
)
|
||||||
|
|
||||||
/** ic_ds_List_Pen_outline_16 */
|
/** ic_ds_List_Pen_outline_16 */
|
||||||
export const IconListPenOutline16 = ({ size = 16, className }: IconProps) => (
|
export const IconListPenOutline16 = ({ size = 16, className }: IconProps) => (
|
||||||
<svg width={size} height={size} className={className} viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
<svg width={size} height={size} className={className} viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
|||||||
@@ -14,8 +14,8 @@ const icons = Object.fromEntries(
|
|||||||
const iconNames = Object.keys(icons)
|
const iconNames = Object.keys(icons)
|
||||||
|
|
||||||
describe('ic_ds_ icon set', () => {
|
describe('ic_ds_ icon set', () => {
|
||||||
it('exports the full P-I set (43 deepsuite + 12 figma extracts)', () => {
|
it('exports the full P-I set (43 deepsuite + 13 figma extracts)', () => {
|
||||||
expect(iconNames.length).toBe(55)
|
expect(iconNames.length).toBe(56)
|
||||||
})
|
})
|
||||||
|
|
||||||
it.each(iconNames)('%s renders an svg with currentColor fills and no hardcoded palette', name => {
|
it.each(iconNames)('%s renders an svg with currentColor fills and no hardcoded palette', name => {
|
||||||
|
|||||||
Reference in New Issue
Block a user