From 5813c14deece17dd632ccb4db6fe6c9db6a08d82 Mon Sep 17 00:00:00 2001 From: Yif <877193178@qq.com> Date: Fri, 4 Sep 2026 20:19:42 +0800 Subject: [PATCH 01/10] fix(client): portal composer menus over the sidebar and float toasts near the top ModelSelect renders its dropdown through a body portal with viewport-clamped fixed placement, so the panel keeps its full width instead of being cut by the sidebar edge. The preset seat passes Menu an anchor class with an icon-only min-width floor and moves its label ellipsis onto a dedicated span, keeping the chip's overflow degradation breakpoint-free. Toasts move to the viewport top and shrink to fit their text. --- .../src/client/AgentPresetSeat.module.css | 24 ++++++- .../src/client/AgentPresetSeat.tsx | 3 +- .../client/ui-model-selection/package.json | 2 + .../src/client/ModelSelect.module.css | 43 +++++++++++-- .../src/client/ModelSelect.tsx | 63 ++++++++++++++++--- .../tests/model-select.client.spec.tsx | 37 +++++++++++ packages/client/ui-primitives/src/Menu.tsx | 2 +- .../client/ui-primitives/src/Toast.module.css | 9 ++- pnpm-lock.yaml | 6 ++ 9 files changed, 170 insertions(+), 19 deletions(-) diff --git a/packages/client/ui-agent-preset/src/client/AgentPresetSeat.module.css b/packages/client/ui-agent-preset/src/client/AgentPresetSeat.module.css index 55fe22e81b..982603c94c 100644 --- a/packages/client/ui-agent-preset/src/client/AgentPresetSeat.module.css +++ b/packages/client/ui-agent-preset/src/client/AgentPresetSeat.module.css @@ -1,10 +1,25 @@ /* Agent-preset chip on the new-session screen, beside the workspace picker. Geometry mirrors HeroShell's .workspace so the two read as one row. */ +/* Overflow-driven degradation (mirrors HeroShell's .workspace), no + breakpoint: the label shrinks with an ellipsis down to nothing, so the + preset icon and chevron always sit snug against whatever text remains — + full text, ellipsized text, or none. */ +/* The Menu anchor wrapper between the row and this chip: its flex + auto-minimum would hold the full label width and the row could never + compress the chip. The explicit floor is the icon-only end state — 8px + paddings + 16px icon + two 4px gaps + 14px chevron — so the glyphs never + crush once the label has ellipsized away to nothing. */ +.menuAnchor { + min-width: 54px; + max-width: 100%; +} + .seat { display: inline-flex; align-items: center; gap: 4px; + min-width: 0; max-width: min(100%, 240px); min-height: 28px; padding: 0 8px; @@ -16,8 +31,6 @@ line-height: 20px; font-weight: 500; white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; cursor: pointer; } @@ -36,6 +49,13 @@ color: var(--dsw-alias-label-primary); } +.seatLabel { + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + /* Introduce cue: the icon eases in on an overshoot-free expo curve (duration matches INTRO_TEXT_DELAY_MS, so the characters start the moment it lands), then the name's characters fade up on a stagger (delays set inline per diff --git a/packages/client/ui-agent-preset/src/client/AgentPresetSeat.tsx b/packages/client/ui-agent-preset/src/client/AgentPresetSeat.tsx index d0501ed0cd..f16d1c83d9 100644 --- a/packages/client/ui-agent-preset/src/client/AgentPresetSeat.tsx +++ b/packages/client/ui-agent-preset/src/client/AgentPresetSeat.tsx @@ -181,6 +181,7 @@ export function AgentPresetSeat({ load, select, introduced, useAgentPresetSeat, }} align="start" portal + className={css.menuAnchor} anchor={( )} diff --git a/packages/client/ui-model-selection/package.json b/packages/client/ui-model-selection/package.json index 2d22a25414..534e126c21 100644 --- a/packages/client/ui-model-selection/package.json +++ b/packages/client/ui-model-selection/package.json @@ -58,6 +58,8 @@ "@types/react": "~18.3.1", "@deepseek-ai/cordis": "workspace:^", "react": "^18.2.0", + "react-dom": "^18.2.0", + "@types/react-dom": "~18.3.0", "@deepseek-ai/dsh-client-store": "workspace:^", "@deepseek-ai/dsh-session": "workspace:^", "@deepseek-ai/dsh-client-ui-renderer": "workspace:^", diff --git a/packages/client/ui-model-selection/src/client/ModelSelect.module.css b/packages/client/ui-model-selection/src/client/ModelSelect.module.css index 85b67ba496..b2cb715287 100644 --- a/packages/client/ui-model-selection/src/client/ModelSelect.module.css +++ b/packages/client/ui-model-selection/src/client/ModelSelect.module.css @@ -49,12 +49,42 @@ white-space: nowrap; } -/* Effort value beside the model name (mock's 'High': same 13/20/500, caption tone). */ +/* Effort value beside the model name (mock's 'High': same 13/20/500, caption + tone). Adaptive priority: the huge shrink factor makes the effort absorb + all deficit first — it ellipsizes away to nothing before the model name + loses a single pixel, and the chevron stays snug throughout. */ .triggerEffort { - flex: 0 0 auto; + flex-shrink: 1000; + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; color: var(--dsw-alias-label-caption); } +/* Stand-in for the whole text below the icon-only cut: the settings page's + Models glyph (IconDataOutline16), hidden while any text fits. */ +.triggerIcon { + display: none; + flex: 0 0 auto; +} + +/* Icon-only floor: past continuous shrink (effort ellipsizes away first, then + the name), a 360px composer row leaves the 45cqw cap too little for a + legible name, so the text yields to the Models glyph. Same anonymous + container as PermissionSelect's 460px cut (InputBar .row); the full name + stays reachable through the trigger's title and aria-label. */ +@container (max-width: 360px) { + .triggerIcon { + display: block; + } + + .triggerLabel, + .triggerEffort { + display: none; + } +} + .chevron { flex: 0 0 auto; color: var(--dsw-alias-label-caption); @@ -65,11 +95,12 @@ transform: rotate(180deg); } +/* Portaled to body and placed from the trigger rect by the component (the + Menu primitive's portal rules), so the sidebar and the columns' overflow + clips cannot crop it; z matches the primitive's .portal layer. */ .menu { - position: absolute; - right: 0; - bottom: calc(100% + 8px); - z-index: 20; + position: fixed; + z-index: 1100; display: flex; flex-direction: column; /* Let the visible pane's rows set the card width, but keep pathological ids diff --git a/packages/client/ui-model-selection/src/client/ModelSelect.tsx b/packages/client/ui-model-selection/src/client/ModelSelect.tsx index e910651e86..f375ade667 100644 --- a/packages/client/ui-model-selection/src/client/ModelSelect.tsx +++ b/packages/client/ui-model-selection/src/client/ModelSelect.tsx @@ -12,14 +12,15 @@ * card; the in-menu strip with Retry remains the catalog-load surface. */ import { - useEffect, useId, useMemo, useRef, useState, useSyncExternalStore, - type KeyboardEvent, type FocusEvent, + useEffect, useId, useLayoutEffect, useMemo, useRef, useState, useSyncExternalStore, + type CSSProperties, type KeyboardEvent, type FocusEvent, } from 'react' +import { createPortal } from 'react-dom' import clsx from 'clsx' import type { ModelReasoningEffort, ModelSelection } from '@deepseek-ai/dsh-api-remotes/client' import { IconCheckOutline16, IconChevronDownOutline14, IconChevronRightOutline14, - IconWarningOutline16, Toast, + IconDataOutline16, IconWarningOutline16, Toast, } from '@deepseek-ai/dsh-client-ui-primitives' import type { PropsLocale } from '@deepseek-ai/dsh-client-ui-slots' import type { ModelSelectInjected } from './slots.ts' @@ -35,6 +36,9 @@ interface EffortChoice { label: string } +/** Unplaced portal card: hidden but laid out at a fixed origin so offsetWidth/offsetHeight are real (Menu primitive's measure pass). */ +const MEASURE_STYLE: CSSProperties = { visibility: 'hidden', left: 0, top: 0 } + /** * Render the composer model seat. * @param props - owner share (locked) + injected face (shared directory @@ -60,6 +64,8 @@ export function ModelSelect( const toastSeq = useRef(0) const rootRef = useRef(null) const triggerRef = useRef(null) + const menuRef = useRef(null) + const [menuPos, setMenuPos] = useState(null) const itemRefs = useRef<(HTMLButtonElement | null)[]>([]) const id = useId() @@ -108,12 +114,45 @@ export function ModelSelect( useEffect(() => { if (!open) return const closeOutside = (event: MouseEvent): void => { - if (!rootRef.current?.contains(event.target as Node)) setOpen(false) + // The portaled card is outside the trigger subtree; check both. + if (rootRef.current?.contains(event.target as Node) === true) return + if (menuRef.current?.contains(event.target as Node) === true) return + setOpen(false) } document.addEventListener('mousedown', closeOutside) return () => { document.removeEventListener('mousedown', closeOutside) } }, [open]) + // Portaled placement (the Menu primitive's portal rules: fixed from the + // anchor rect, measured before paint, clamped inside the viewport): above + // the trigger, right edges aligned. Depends on pane and directory state + // because pane switches and async catalog loads resize the card. + useLayoutEffect(() => { + if (!open) { setMenuPos(null); return } + const place = (): void => { + /* v8 ignore next 2 -- the trigger ref is attached whenever the menu is open. */ + const rect = triggerRef.current?.getBoundingClientRect() + if (rect === undefined) return + const MARGIN = 12 + const lw = menuRef.current?.offsetWidth ?? 0 + const lh = menuRef.current?.offsetHeight ?? 0 + let x = rect.right - lw + let y = rect.top - 8 - lh + if (lw > 0) x = Math.min(Math.max(x, MARGIN), window.innerWidth - lw - MARGIN) + if (lh > 0) y = Math.min(Math.max(y, MARGIN), window.innerHeight - lh - MARGIN) + setMenuPos({ left: x, top: y }) + } + // First run measures the hidden pre-render (same commit as `open`), so + // the card lands placed before anything paints. + place() + window.addEventListener('scroll', place, true) + window.addEventListener('resize', place) + return () => { + window.removeEventListener('scroll', place, true) + window.removeEventListener('resize', place) + } + }, [open, pane, state]) + if (!available) return null const show = (): void => { @@ -152,7 +191,10 @@ export function ModelSelect( } const onBlur = (event: FocusEvent): void => { - if (event.relatedTarget instanceof Node && rootRef.current?.contains(event.relatedTarget)) return + if (event.relatedTarget instanceof Node && ( + rootRef.current?.contains(event.relatedTarget) === true + || menuRef.current?.contains(event.relatedTarget) === true + )) return close() } @@ -232,15 +274,21 @@ export function ModelSelect( } }} > + {modelLabel} {effortLabel !== undefined && {effortLabel}} - {open && ( + {/* Portaled to body (Menu primitive's portal mode) so the sidebar and + column overflow clips cannot crop the card; synthetic events still + bubble through this React subtree, keeping onKeyDown/onBlur live. */} + {open && createPortal( + , + document.body, )} {toast !== null && ( { expect(screen.queryByRole('button', { name: '重试' })).toBeNull() }) + it('portals the placed menu card to body and closes only on truly-outside mousedown', () => { + const offsetWidth = Object.getOwnPropertyDescriptor(HTMLElement.prototype, 'offsetWidth')! + const offsetHeight = Object.getOwnPropertyDescriptor(HTMLElement.prototype, 'offsetHeight')! + Object.defineProperty(HTMLElement.prototype, 'offsetWidth', { configurable: true, get: () => 200 }) + Object.defineProperty(HTMLElement.prototype, 'offsetHeight', { configurable: true, get: () => 300 }) + try { + const { container } = render() + const trigger = screen.getByRole('button', { name: /选择模型/ }) + fireEvent.click(trigger) + const menu = screen.getByRole('menu') + // Outside the composer subtree — column overflow clips cannot crop it. + expect(container.contains(menu)).toBe(false) + expect(menu.parentElement).toBe(document.body) + // jsdom anchor rects are all zero, so the measured 200x300 card clamps + // to the 12px viewport margin on both axes. + expect(menu.style.left).toBe('12px') + expect(menu.style.top).toBe('12px') + // Interactions inside the trigger subtree or the portaled card stay open. + fireEvent.mouseDown(menu) + fireEvent.mouseDown(trigger) + fireEvent.blur(trigger, { relatedTarget: menu }) + expect(screen.getByRole('menu')).toBeTruthy() + fireEvent.mouseDown(document.body) + expect(screen.queryByRole('menu')).toBeNull() + } finally { + Object.defineProperty(HTMLElement.prototype, 'offsetWidth', offsetWidth) + Object.defineProperty(HTMLElement.prototype, 'offsetHeight', offsetHeight) + } + }) + it('renders no Agent-bound control for an addressed subagent session', () => { const load = vi.fn() render( DOMRect | null - className?: string + className?: string | undefined }) { const rootRef = useRef(null) const listRef = useRef(null) diff --git a/packages/client/ui-primitives/src/Toast.module.css b/packages/client/ui-primitives/src/Toast.module.css index 0af6ec2151..793cbe23a1 100644 --- a/packages/client/ui-primitives/src/Toast.module.css +++ b/packages/client/ui-primitives/src/Toast.module.css @@ -5,7 +5,7 @@ .toast { position: fixed; - top: 120px; + top: 40px; left: 50%; /* Above the 1000 the image lightbox backdrop uses: a failure reported while a preview is open must stay readable. */ @@ -15,7 +15,12 @@ display: flex; align-items: center; gap: 10px; - max-width: min(560px, calc(100vw - 48px)); + /* Fixed boxes with `left` set shrink-to-fit against the space RIGHT of + `left` (the -50% translate happens after sizing), so an anchored banner + near the window edge would wrap early. max-content sizes the box from + its text alone, capped by the max-width below. */ + width: max-content; + max-width: min(640px, calc(100vw - 48px)); padding: 12px 16px; border-radius: 14px; background: var(--dsw-alias-button-contrast-fill); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7a305fb42a..b84daf02d6 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -2692,9 +2692,15 @@ importers: '@types/react': specifier: ~18.3.1 version: 18.3.31 + '@types/react-dom': + specifier: ~18.3.0 + version: 18.3.7(@types/react@18.3.31) react: specifier: ^18.2.0 version: 18.3.1 + react-dom: + specifier: ^18.2.0 + version: 18.3.1(react@18.3.1) packages/client/ui-permission-presets: devDependencies: From a3208573a6d9aaeb1c7ff08bb0c6ac9a10522084 Mon Sep 17 00:00:00 2001 From: Yif <877193178@qq.com> Date: Fri, 4 Sep 2026 20:21:16 +0800 Subject: [PATCH 02/10] fix(client): polish composer and hero spacing Tighten the composer card's top pad and draft-text pads, pull the attachment rail closer to the draft, settle the toolbar gaps on 12, give the hero workspace row its right clearance, and let the wrapped hero badge center under the title with more air between the wrapped fish and the title line. --- .../src/client/ComposerAttachments.module.css | 5 ++- .../skeleton/ConversationRoot.module.css | 7 ++-- .../src/client/skeleton/EmptyHero.tsx | 4 +- .../src/client/skeleton/HeroShell.module.css | 40 +++++++++++++------ .../src/client/skeleton/InputBar.module.css | 27 +++++++------ 5 files changed, 52 insertions(+), 31 deletions(-) diff --git a/packages/client/ui-attachment/src/client/ComposerAttachments.module.css b/packages/client/ui-attachment/src/client/ComposerAttachments.module.css index 770a64cef5..85185ca9a7 100644 --- a/packages/client/ui-attachment/src/client/ComposerAttachments.module.css +++ b/packages/client/ui-attachment/src/client/ComposerAttachments.module.css @@ -1,4 +1,7 @@ .rail { min-width: 0; - padding: 4px 12px 0; + padding: 2px 10px 0; + /* The card's flex gap (12px) separates every row pair; the thumbnails sit + closer to the draft text than the other rows, so pull back 6 here. */ + margin-bottom: -6px; } diff --git a/packages/client/ui-conversation/src/client/skeleton/ConversationRoot.module.css b/packages/client/ui-conversation/src/client/skeleton/ConversationRoot.module.css index 3e98b17b42..5f89b11e7d 100644 --- a/packages/client/ui-conversation/src/client/skeleton/ConversationRoot.module.css +++ b/packages/client/ui-conversation/src/client/skeleton/ConversationRoot.module.css @@ -433,9 +433,10 @@ gap: 2px; min-width: 0; margin-top: 4px; - /* figma drew px 8; nudged +12 so the chip's folder glyph lines up closer to - the card's inner controls below. */ - padding-left: 20px; + /* figma drew px 8; left nudged +12 so the chip's folder glyph lines up + closer to the card's inner controls below; right keeps the trailing + preset seat off the card edge. */ + padding: 0 16px 0 20px; } /* Hero: the composer sits inside the session scroll body; center there so diff --git a/packages/client/ui-conversation/src/client/skeleton/EmptyHero.tsx b/packages/client/ui-conversation/src/client/skeleton/EmptyHero.tsx index 6c4820ed4f..e0a7bcb924 100644 --- a/packages/client/ui-conversation/src/client/skeleton/EmptyHero.tsx +++ b/packages/client/ui-conversation/src/client/skeleton/EmptyHero.tsx @@ -149,10 +149,10 @@ export function HeroShell({ t, renderSlot, children }: HeroShellProps) { fallback: , })} - + {t('hero.headline')} + {t('hero.preview')} - {t('hero.preview')}
{/* The composer remains mounted outside this component. */} diff --git a/packages/client/ui-conversation/src/client/skeleton/HeroShell.module.css b/packages/client/ui-conversation/src/client/skeleton/HeroShell.module.css index 6c5053e9c6..d47e0bfe2f 100644 --- a/packages/client/ui-conversation/src/client/skeleton/HeroShell.module.css +++ b/packages/client/ui-conversation/src/client/skeleton/HeroShell.module.css @@ -27,9 +27,15 @@ badge is a product addition outside that source: a mono superscript pill riding the title's top-right. */ .headline { - display: grid; - grid-template-columns: 34px auto auto; + display: flex; + /* Overflow-driven: the fish and the title group are two wrap units sharing + one centered line while they fit; squeezed, the fish takes its own line + above the group. No breakpoint. */ + flex-wrap: wrap; column-gap: 10px; + /* Wrapped, the fish sits on its own line; more air than the 10px side gap + keeps it reading as a mark above the title, not a squeezed inline glyph. */ + row-gap: 12px; align-items: center; justify-content: center; font-size: 26px; @@ -38,17 +44,24 @@ color: var(--dsw-alias-label-primary); } -.headlineText { - grid-row: 1; - grid-column: 2; +/* Title text + badge as one wrap unit so the title never breaks mid-text with + the badge stranded beside a fragment. When the badge no longer fits beside + the title it wraps to its own centered line below; while both fit, the group + is content-sized and centering is a no-op, keeping the superscript snug. */ +.titleGroup { + display: flex; + flex-wrap: wrap; + justify-content: center; + align-items: center; + /* Superscript sits 7 from the title. */ + column-gap: 7px; + row-gap: 4px; + min-width: 0; } .previewBadge { - grid-row: 1; - grid-column: 3; - align-self: start; + align-self: flex-start; margin-top: 2px; - margin-left: -3px; padding: 1px 7px 0; border: 0.5px solid var(--dsw-alias-interactive-bg-hover); border-radius: 24px; @@ -64,9 +77,8 @@ /* Keep hover detection on a stationary box while the mark moves within it; hover rules target the hitbox so a slot-occupant mark swims too. */ .fishHitbox { - grid-row: 1; - grid-column: 1; display: inline-flex; + flex: none; align-items: center; justify-content: center; } @@ -132,7 +144,10 @@ padding-left: 8px; } -/* Folder + label + chevron — transparent at rest; fill only on hover / open. */ +/* Folder + label + chevron — transparent at rest; fill only on hover / open. + Overflow-driven degradation, no breakpoint: the label shrinks with an + ellipsis down to nothing, so folder and chevron always sit snug against + whatever text remains — full text, ellipsized text, or none. */ .workspace { display: inline-flex; align-items: center; @@ -167,6 +182,7 @@ } .workspaceLabel { + min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; diff --git a/packages/client/ui-conversation/src/client/skeleton/InputBar.module.css b/packages/client/ui-conversation/src/client/skeleton/InputBar.module.css index b7561bbfd8..1749d469d9 100644 --- a/packages/client/ui-conversation/src/client/skeleton/InputBar.module.css +++ b/packages/client/ui-conversation/src/client/skeleton/InputBar.module.css @@ -34,12 +34,12 @@ position: relative; /* overlay anchor positioning context */ display: flex; flex-direction: column; - /* figma Input 75:8208: 12px between the text area and the button row; 10px - top pad on the card before .InputText. */ + /* figma Input 75:8208: 12px between the text area and the button row; the + card's top pad before .InputText tightened from the drawn 10 to 8. */ gap: 12px; width: 100%; max-width: var(--dsh-composer-card-max-width); - padding-top: 10px; + padding-top: 8px; /* Input stroke: the light neutral (l2, one step above the menus' l1), drawn as the elevation hairline so it costs no layout; the shadow is the soft elevation tier. Fill: the input surface token (elevated in dark). */ @@ -123,8 +123,8 @@ margin-right: 4px; } -/* The card's 22px corner arc still overlaps the scrollport's top (10px top - pad reaches only y=10): start the thumb's travel below the arc so it never +/* The card's 22px corner arc still overlaps the scrollport's top (8px top + pad reaches only y=8): start the thumb's travel below the arc so it never pokes outside the capsule. WebKit-path only; Firefox's thin bar has no track margin, an accepted remainder. */ .scroll::-webkit-scrollbar-track { @@ -153,11 +153,12 @@ } /* The contenteditable draft surface (grows with its content; .scroll caps - and scrolls it). figma .InputText 34:10434: pl 16 / pr 12 / pt 4 — the - right pad is 8 here plus .scroll's 4px bar inset. */ + and scrolls it). figma .InputText 34:10434 drew pl 16 / pr 12 / pt 4; the + left pad tightened to 14, and the right pad is 8 here plus .scroll's 4px + bar inset. */ .input { box-sizing: border-box; - padding: 4px 8px 0 16px; + padding: 2px 8px 0 14px; font-family: var(--dsw-font-family); font-size: inherit; line-height: inherit; @@ -185,10 +186,10 @@ } /* figma 34:10434: #ADB2B8 light / #81858C dark — the caption pair exactly. - Right inset mirrors .input's 8px pad (the other 4px sits on .scroll). */ + Insets mirror .input's pads (the other right 4px sits on .scroll). */ .placeholder { position: absolute; - inset: 4px 8px auto 16px; + inset: 2px 8px auto 14px; color: var(--dsw-alias-label-caption); pointer-events: none; user-select: none; @@ -238,10 +239,10 @@ } /* figma 75:8208 drew 16 between + and the mode chips and 4 between Plan / - Read-only; the chip gap widened to 12 so the pill chips read as separate - controls. */ + Read-only; both gaps settled on 12 so the pill chips read as separate + controls without drifting from the + control. */ .tools { - gap: 16px; + gap: 12px; } .modes { From a49376a2ca4082c19a1c5670e1ed1d803e196bee Mon Sep 17 00:00:00 2001 From: Yif <877193178@qq.com> Date: Fri, 4 Sep 2026 20:21:46 +0800 Subject: [PATCH 03/10] fix(client): reword composer placeholders around the / and @ triggers The default and hero placeholders drop the ellipsis and name the / commands and @ references as comma-separated actions; goldens and e2e assertions refresh with the copy. --- .../tests/command-image-envelope.expected.e2e.ts | 2 +- .../conversation-expanded.expected.md | 2 +- .../github-ready-review/conversation.expected.md | 2 +- .../goal-command-presentation/ui.expected.md | 2 +- .../expected/markdown-cjk-strong/ui.expected.md | 2 +- .../tests/expected/markdown-images/ui.expected.md | 2 +- .../markdown-inline-code-links/ui.expected.md | 2 +- .../tests/expected/math-rendering/ui.expected.md | 2 +- .../expected/reference-composer/order.expected.md | 2 +- .../skill-user-invoke/ui-expanded.expected.md | 2 +- .../expected/skill-user-invoke/ui.expected.md | 2 +- .../expected/stats-paged-history/ui.expected.md | 2 +- .../tests/expected/steer-all/mid-steer.expected.md | 2 +- .../steer-all/settled-expanded.expected.md | 2 +- .../tests/expected/steer-all/settled.expected.md | 2 +- apps/web/tests/goal-bar.e2e.ts | 2 +- apps/web/tests/image-display.expected.e2e.ts | 6 +++--- apps/web/tests/preview-boot.e2e.ts | 2 +- .../mid-stream.expected.md | 2 +- apps/web/tests/startup-auto-selection.e2e.ts | 2 +- apps/web/tests/subagent-conversation.e2e.ts | 6 +++--- apps/web/tests/subagent-interrupt-ui.e2e.ts | 2 +- apps/web/tests/support.ts | 4 ++-- .../client/ui-conversation/src/client/locales.ts | 8 ++++---- .../tests/input-bar.client.spec.tsx | 14 +++++++------- snapshots/web/bash-abort-row/ui.expected.md | 2 +- snapshots/web/cordis-tool-round/ui.expected.md | 2 +- .../web/feedback-command/ack-expanded.expected.md | 2 +- snapshots/web/feedback-command/ack.expected.md | 2 +- .../web/feedback-release/ack-expanded.expected.md | 2 +- snapshots/web/feedback-release/ack.expected.md | 2 +- .../web/fresh-round-trip/ui-expanded.expected.md | 2 +- snapshots/web/fresh-round-trip/ui.expected.md | 2 +- .../ui-expanded.expected.md | 2 +- .../web/goal-multi-turn-actions/ui.expected.md | 2 +- snapshots/web/lifecycle-chrome/hero.expected.md | 2 +- .../web/lifecycle-chrome/plan-active.expected.md | 2 +- .../lifecycle-chrome/reloaded-expanded.expected.md | 2 +- .../web/lifecycle-chrome/reloaded.expected.md | 2 +- .../live-interactions/cancel-expanded.expected.md | 2 +- snapshots/web/live-interactions/cancel.expected.md | 2 +- .../web/live-interactions/error-auth.expected.md | 2 +- .../web/live-interactions/loading.expected.md | 2 +- .../live-interactions/retry-exhausted.expected.md | 2 +- .../live-interactions/retry-expanded.expected.md | 2 +- snapshots/web/live-interactions/retry.expected.md | 2 +- .../live-interactions/running-draft.expected.md | 2 +- snapshots/web/message-actions/ui.expected.md | 2 +- snapshots/web/minimal-preset/ui.expected.md | 2 +- .../web/plan-review/approved-expanded.expected.md | 2 +- snapshots/web/plan-review/approved.expected.md | 2 +- snapshots/web/ptc-round/ui.expected.md | 2 +- .../answered-expanded.expected.md | 2 +- .../web/question-composer/answered.expected.md | 2 +- .../web/question-composer/cancelled.expected.md | 2 +- .../queue-actions/preserved-expanded.expected.md | 2 +- snapshots/web/queue-actions/preserved.expected.md | 2 +- snapshots/web/queued-image/delivered.expected.md | 2 +- .../web/seeded-history/command-row.expected.md | 2 +- .../web/seeded-history/feedback-row.expected.md | 2 +- .../web/seeded-history/ui-expanded.expected.md | 2 +- snapshots/web/seeded-history/ui.expected.md | 2 +- snapshots/web/skill-tool-row/ui.expected.md | 2 +- .../web/steering/settled-expanded.expected.md | 2 +- snapshots/web/steering/settled.expected.md | 2 +- .../subagent-conversation/ui-expanded.expected.md | 2 +- snapshots/web/subagent-conversation/ui.expected.md | 2 +- .../web/turn-tail-actions/completed.expected.md | 2 +- .../web/turn-tail-actions/focused.expected.md | 2 +- .../web/turn-tail-actions/running.expected.md | 2 +- .../web/turn-tail-actions/settled.expected.md | 2 +- .../turn-tail-actions/usage-expanded.expected.md | 2 +- snapshots/web/web-search-round/ui.expected.md | 2 +- 73 files changed, 87 insertions(+), 87 deletions(-) diff --git a/apps/web/tests/command-image-envelope.expected.e2e.ts b/apps/web/tests/command-image-envelope.expected.e2e.ts index 47fb91069b..48d5f8f407 100644 --- a/apps/web/tests/command-image-envelope.expected.e2e.ts +++ b/apps/web/tests/command-image-envelope.expected.e2e.ts @@ -21,7 +21,7 @@ async function freshComposer(): Promise { fireEvent.click(start) return await waitFor(() => { const surface = document.querySelector( - '[data-composer-input][data-placeholder="Describe what you want to build... / commands, @ files or sessions"]', + '[data-composer-input][data-placeholder="Describe what you want to build, run / commands, @ files or sessions"]', ) if (surface === null) throw new Error('composer surface missing') return surface diff --git a/apps/web/tests/expected/github-ready-review/conversation-expanded.expected.md b/apps/web/tests/expected/github-ready-review/conversation-expanded.expected.md index 3a29a14b51..9f350d3f40 100644 --- a/apps/web/tests/expected/github-ready-review/conversation-expanded.expected.md +++ b/apps/web/tests/expected/github-ready-review/conversation-expanded.expected.md @@ -48,7 +48,7 @@ - img - text: Ran for {{duration}} - text: {{clock}} -- textbox "Message or run a task... / commands, @ files or sessions" +- textbox "Message or run a task, run / commands, @ files or sessions" - button "Commands": - img - 'button "Access mode, current: Read Only"': Read Only diff --git a/apps/web/tests/expected/github-ready-review/conversation.expected.md b/apps/web/tests/expected/github-ready-review/conversation.expected.md index 0a5fe67b90..b57f39a3c4 100644 --- a/apps/web/tests/expected/github-ready-review/conversation.expected.md +++ b/apps/web/tests/expected/github-ready-review/conversation.expected.md @@ -40,7 +40,7 @@ - img - text: Ran for {{duration}} - text: {{clock}} -- textbox "Message or run a task... / commands, @ files or sessions" +- textbox "Message or run a task, run / commands, @ files or sessions" - button "Commands": - img - 'button "Access mode, current: Read Only"': Read Only diff --git a/apps/web/tests/expected/goal-command-presentation/ui.expected.md b/apps/web/tests/expected/goal-command-presentation/ui.expected.md index c10eef6aac..7567b6422f 100644 --- a/apps/web/tests/expected/goal-command-presentation/ui.expected.md +++ b/apps/web/tests/expected/goal-command-presentation/ui.expected.md @@ -14,7 +14,7 @@ - img - img - text: "goal No goal is currently set. Usage: /goal [|clear|edit |pause|resume]" -- textbox "Message or run a task... / commands, @ files or sessions" +- textbox "Message or run a task, run / commands, @ files or sessions" - button "Commands": - img - 'button "Access mode, current: Workspace Write"': Workspace Write diff --git a/apps/web/tests/expected/markdown-cjk-strong/ui.expected.md b/apps/web/tests/expected/markdown-cjk-strong/ui.expected.md index 2fba524c5d..0cf58cabfa 100644 --- a/apps/web/tests/expected/markdown-cjk-strong/ui.expected.md +++ b/apps/web/tests/expected/markdown-cjk-strong/ui.expected.md @@ -48,7 +48,7 @@ - img - text: Ran for {{duration}} - text: {{clock}} -- textbox "Message or run a task... / commands, @ files or sessions" +- textbox "Message or run a task, run / commands, @ files or sessions" - button "Commands": - img - 'button "Access mode, current: Workspace Write"': Workspace Write diff --git a/apps/web/tests/expected/markdown-images/ui.expected.md b/apps/web/tests/expected/markdown-images/ui.expected.md index 3843143d1f..c4fa04a381 100644 --- a/apps/web/tests/expected/markdown-images/ui.expected.md +++ b/apps/web/tests/expected/markdown-images/ui.expected.md @@ -27,7 +27,7 @@ - img - text: Ran for {{duration}} - text: {{clock}} -- textbox "Message or run a task... / commands, @ files or sessions" +- textbox "Message or run a task, run / commands, @ files or sessions" - button "Commands": - img - 'button "Access mode, current: Workspace Write"': Workspace Write diff --git a/apps/web/tests/expected/markdown-inline-code-links/ui.expected.md b/apps/web/tests/expected/markdown-inline-code-links/ui.expected.md index 2d2127cbb2..63013648ea 100644 --- a/apps/web/tests/expected/markdown-inline-code-links/ui.expected.md +++ b/apps/web/tests/expected/markdown-inline-code-links/ui.expected.md @@ -39,7 +39,7 @@ - img - text: Ran for {{duration}} - text: {{clock}} -- textbox "Message or run a task... / commands, @ files or sessions" +- textbox "Message or run a task, run / commands, @ files or sessions" - button "Commands": - img - 'button "Access mode, current: Workspace Write"': Workspace Write diff --git a/apps/web/tests/expected/math-rendering/ui.expected.md b/apps/web/tests/expected/math-rendering/ui.expected.md index fba05ffe71..bf0ed86acf 100644 --- a/apps/web/tests/expected/math-rendering/ui.expected.md +++ b/apps/web/tests/expected/math-rendering/ui.expected.md @@ -43,7 +43,7 @@ - img - text: Ran for {{duration}} - text: {{clock}} -- textbox "Message or run a task... / commands, @ files or sessions" +- textbox "Message or run a task, run / commands, @ files or sessions" - button "Commands": - img - 'button "Access mode, current: Workspace Write"': Workspace Write diff --git a/apps/web/tests/expected/reference-composer/order.expected.md b/apps/web/tests/expected/reference-composer/order.expected.md index 7cec2eaf08..6b17de40cb 100644 --- a/apps/web/tests/expected/reference-composer/order.expected.md +++ b/apps/web/tests/expected/reference-composer/order.expected.md @@ -13,7 +13,7 @@ - button "Session recall Research notes": - img - text: Session recall Research notes -- textbox "Message or run a task... / commands, @ files or sessions" +- textbox "Message or run a task, run / commands, @ files or sessions" - button "Commands": - img - 'button "Access mode, current: Workspace Write"': Workspace Write diff --git a/apps/web/tests/expected/skill-user-invoke/ui-expanded.expected.md b/apps/web/tests/expected/skill-user-invoke/ui-expanded.expected.md index 07be7102ff..6d615be5d1 100644 --- a/apps/web/tests/expected/skill-user-invoke/ui-expanded.expected.md +++ b/apps/web/tests/expected/skill-user-invoke/ui-expanded.expected.md @@ -40,7 +40,7 @@ - img - text: Ran for {{duration}} - text: {{clock}} -- textbox "Message or run a task... / commands, @ files or sessions" +- textbox "Message or run a task, run / commands, @ files or sessions" - button "Commands": - img - 'button "Access mode, current: Workspace Write"': Workspace Write diff --git a/apps/web/tests/expected/skill-user-invoke/ui.expected.md b/apps/web/tests/expected/skill-user-invoke/ui.expected.md index 7ddf071159..6285f1f4a9 100644 --- a/apps/web/tests/expected/skill-user-invoke/ui.expected.md +++ b/apps/web/tests/expected/skill-user-invoke/ui.expected.md @@ -32,7 +32,7 @@ - img - text: Ran for {{duration}} - text: {{clock}} -- textbox "Message or run a task... / commands, @ files or sessions" +- textbox "Message or run a task, run / commands, @ files or sessions" - button "Commands": - img - 'button "Access mode, current: Workspace Write"': Workspace Write diff --git a/apps/web/tests/expected/stats-paged-history/ui.expected.md b/apps/web/tests/expected/stats-paged-history/ui.expected.md index fc8039e282..b0aeaeb11c 100644 --- a/apps/web/tests/expected/stats-paged-history/ui.expected.md +++ b/apps/web/tests/expected/stats-paged-history/ui.expected.md @@ -459,7 +459,7 @@ - text: 7/25 {{clock}} - button "Back to bottom": - img -- textbox "Message or run a task... / commands, @ files or sessions" +- textbox "Message or run a task, run / commands, @ files or sessions" - button "Commands": - img - 'button "Access mode, current: Workspace Write"': Workspace Write diff --git a/apps/web/tests/expected/steer-all/mid-steer.expected.md b/apps/web/tests/expected/steer-all/mid-steer.expected.md index c3847df394..fd3d079115 100644 --- a/apps/web/tests/expected/steer-all/mid-steer.expected.md +++ b/apps/web/tests/expected/steer-all/mid-steer.expected.md @@ -31,7 +31,7 @@ - text: "Interjection: include the word ORANGE in your final reply." - button "Copy": - img -- textbox "Message or run a task... / commands, @ files or sessions" +- textbox "Message or run a task, run / commands, @ files or sessions" - button "Commands": - img - 'button "Access mode, current: Workspace Write"': Workspace Write diff --git a/apps/web/tests/expected/steer-all/settled-expanded.expected.md b/apps/web/tests/expected/steer-all/settled-expanded.expected.md index 73e987dfa2..326c57de80 100644 --- a/apps/web/tests/expected/steer-all/settled-expanded.expected.md +++ b/apps/web/tests/expected/steer-all/settled-expanded.expected.md @@ -50,7 +50,7 @@ - img - text: Ran for {{duration}} - text: {{clock}} -- textbox "Message or run a task... / commands, @ files or sessions" +- textbox "Message or run a task, run / commands, @ files or sessions" - button "Commands": - img - 'button "Access mode, current: Workspace Write"': Workspace Write diff --git a/apps/web/tests/expected/steer-all/settled.expected.md b/apps/web/tests/expected/steer-all/settled.expected.md index cd48c29075..88f6522fd1 100644 --- a/apps/web/tests/expected/steer-all/settled.expected.md +++ b/apps/web/tests/expected/steer-all/settled.expected.md @@ -38,7 +38,7 @@ - img - text: Ran for {{duration}} - text: {{clock}} -- textbox "Message or run a task... / commands, @ files or sessions" +- textbox "Message or run a task, run / commands, @ files or sessions" - button "Commands": - img - 'button "Access mode, current: Workspace Write"': Workspace Write diff --git a/apps/web/tests/goal-bar.e2e.ts b/apps/web/tests/goal-bar.e2e.ts index f9cd46b60c..5b077016e5 100644 --- a/apps/web/tests/goal-bar.e2e.ts +++ b/apps/web/tests/goal-bar.e2e.ts @@ -45,7 +45,7 @@ describe('web e2e: goal bar clear convergence', () => { onTestFailed(() => saveFailureShot(page, 'web-e2e-goal-bar-clear')) // Startup reuses the fixture workspace's blank session, keeping this // command independent of alpha's running replay and pending question. - const input = page.locator('[data-composer-input][data-placeholder="Describe what you want to build... / commands, @ files or sessions"]') + const input = page.locator('[data-composer-input][data-placeholder="Describe what you want to build, run / commands, @ files or sessions"]') await input.waitFor({ timeout: 10_000 }) await input.fill('/goal guard rapid clear clicks') await input.press('Enter') diff --git a/apps/web/tests/image-display.expected.e2e.ts b/apps/web/tests/image-display.expected.e2e.ts index 3b5f8d00f2..c2a5e129f0 100644 --- a/apps/web/tests/image-display.expected.e2e.ts +++ b/apps/web/tests/image-display.expected.e2e.ts @@ -90,7 +90,7 @@ it('accepts pasted images into the composer rail in order and removes them', asy // this assembled lane pins the intake chain over the built graph. const textarea = await waitFor(() => { const surface = document.querySelector( - '[data-composer-input][data-placeholder="Describe what you want to build... / commands, @ files or sessions"]', + '[data-composer-input][data-placeholder="Describe what you want to build, run / commands, @ files or sessions"]', ) if (surface === null) throw new Error('composer surface missing') return surface @@ -165,7 +165,7 @@ it('accepts a whole-page drop under the limits-labeled overlay and refuses an ov fireEvent.click(start) const textarea = await waitFor(() => { const surface = document.querySelector( - '[data-composer-input][data-placeholder="Describe what you want to build... / commands, @ files or sessions"]', + '[data-composer-input][data-placeholder="Describe what you want to build, run / commands, @ files or sessions"]', ) if (surface === null) throw new Error('composer surface missing') return surface @@ -220,7 +220,7 @@ it('renders a host dimension rejection with the projected 2000px limit', async ( const textarea = await waitFor(() => { const surface = document.querySelector( - '[data-composer-input][data-placeholder="Describe what you want to build... / commands, @ files or sessions"]', + '[data-composer-input][data-placeholder="Describe what you want to build, run / commands, @ files or sessions"]', ) if (surface === null) throw new Error('composer surface missing') return surface diff --git a/apps/web/tests/preview-boot.e2e.ts b/apps/web/tests/preview-boot.e2e.ts index 656285cbf9..7b5f49c1c0 100644 --- a/apps/web/tests/preview-boot.e2e.ts +++ b/apps/web/tests/preview-boot.e2e.ts @@ -314,7 +314,7 @@ async function bootPreview(origin: string, browser: Browser): Promise { const configureLater = page.getByRole('button', { name: 'Configure later' }) await configureLater.waitFor({ timeout: 30_000 }) await configureLater.click() - await page.locator('[data-composer-input][data-placeholder="Describe what you want to build... / commands, @ files or sessions"]') + await page.locator('[data-composer-input][data-placeholder="Describe what you want to build, run / commands, @ files or sessions"]') .waitFor({ timeout: 30_000 }) const exercised = await page.evaluate(async () => { diff --git a/apps/web/tests/snapshots/streaming-fence-highlight/mid-stream.expected.md b/apps/web/tests/snapshots/streaming-fence-highlight/mid-stream.expected.md index cd696c5411..f7ad0b36b5 100644 --- a/apps/web/tests/snapshots/streaming-fence-highlight/mid-stream.expected.md +++ b/apps/web/tests/snapshots/streaming-fence-highlight/mid-stream.expected.md @@ -24,7 +24,7 @@ - button "Copy" - code: "const first: number = 1 const second = \"two\" let tail" - status: Deep diving... -- textbox "Message or run a task... / commands, @ files or sessions" +- textbox "Message or run a task, run / commands, @ files or sessions" - button "Commands": - img - 'button "Access mode, current: Workspace Write"': Workspace Write diff --git a/apps/web/tests/startup-auto-selection.e2e.ts b/apps/web/tests/startup-auto-selection.e2e.ts index 1404402b4f..7e7065e6b2 100644 --- a/apps/web/tests/startup-auto-selection.e2e.ts +++ b/apps/web/tests/startup-auto-selection.e2e.ts @@ -130,7 +130,7 @@ describe('web e2e: startup auto-selection', () => { expect(await page.locator('[data-composer-input]').first().isVisible()).toBe(true) releaseOpening() - await page.locator('[data-composer-input][contenteditable="true"][data-placeholder="Describe what you want to build... / commands, @ files or sessions"]') + await page.locator('[data-composer-input][contenteditable="true"][data-placeholder="Describe what you want to build, run / commands, @ files or sessions"]') .waitFor({ timeout: 15_000 }) acknowledgeReloadConnectionLoss(tripwire, warningsBefore) diff --git a/apps/web/tests/subagent-conversation.e2e.ts b/apps/web/tests/subagent-conversation.e2e.ts index d9b66749de..af7407778a 100644 --- a/apps/web/tests/subagent-conversation.e2e.ts +++ b/apps/web/tests/subagent-conversation.e2e.ts @@ -423,7 +423,7 @@ describe('web e2e: persisted subagent conversation and human continuation', () = expect(await page.locator('[data-composer-seat]').evaluate(element => getComputedStyle(element).visibility)).toBe('hidden') releaseCatalog() - const input = page.getByRole('textbox', { name: 'Message or run a task... / commands, @ files or sessions' }) + const input = page.getByRole('textbox', { name: 'Message or run a task, run / commands, @ files or sessions' }) await input.waitFor({ timeout: 15_000 }) await expect.poll(() => input.isEnabled(), { timeout: 15_000 }).toBe(true) acknowledgeReloadConnectionLoss(tripwire, warningStart) @@ -447,7 +447,7 @@ describe('web e2e: persisted subagent conversation and human continuation', () = resolveEnded() }) }) - const input = page.getByRole('textbox', { name: 'Message or run a task... / commands, @ files or sessions' }) + const input = page.getByRole('textbox', { name: 'Message or run a task, run / commands, @ files or sessions' }) await input.fill(FOLLOWUP) await input.press('Enter') await expect.poll( @@ -537,7 +537,7 @@ describe('web e2e: persisted subagent conversation and human continuation', () = .click() await page.getByRole('button', { name: '3 subagents' }).hover() await page.getByRole('treeitem', { name: new RegExp(LABEL) }).click() - await page.getByRole('textbox', { name: 'Message or run a task... / commands, @ files or sessions' }).waitFor() + await page.getByRole('textbox', { name: 'Message or run a task, run / commands, @ files or sessions' }).waitFor() const forkResponse = page.waitForResponse(response => new URL(response.url()).pathname === '/api/session/fork') await page.getByRole('button', { name: 'Branch into a new conversation' }).last().click() diff --git a/apps/web/tests/subagent-interrupt-ui.e2e.ts b/apps/web/tests/subagent-interrupt-ui.e2e.ts index 5cf3efa820..bf770c3874 100644 --- a/apps/web/tests/subagent-interrupt-ui.e2e.ts +++ b/apps/web/tests/subagent-interrupt-ui.e2e.ts @@ -263,7 +263,7 @@ describe.skipIf(MODE === 'record')('web e2e: composer interrupt for a running co .getByRole('button').first().click() await page.getByRole('button', { name: /1 subagent/ }).click() await page.getByRole('treeitem', { name: new RegExp(LABEL) }).click() - const input = page.getByRole('textbox', { name: 'Message or run a task... / commands, @ files or sessions' }) + const input = page.getByRole('textbox', { name: 'Message or run a task, run / commands, @ files or sessions' }) await input.waitFor({ timeout: 15_000 }) expect(await input.isDisabled()).toBe(false) diff --git a/apps/web/tests/support.ts b/apps/web/tests/support.ts index 63cf8c3ff8..252ef70ee7 100644 --- a/apps/web/tests/support.ts +++ b/apps/web/tests/support.ts @@ -112,7 +112,7 @@ export async function connectFreshWorkspace(page: Page, root: string, name = 'wo await dialog.getByRole('button', { name: 'Open', exact: true }).click() // The pick connected the workspace: the blank session's live composer // replaces the locked placeholder and enables. - await page.locator('[data-composer-input][contenteditable="true"][data-placeholder="Describe what you want to build... / commands, @ files or sessions"]') + await page.locator('[data-composer-input][contenteditable="true"][data-placeholder="Describe what you want to build, run / commands, @ files or sessions"]') .waitFor({ timeout: 15_000 }) } @@ -135,7 +135,7 @@ export async function connectFreshWorkspaceZh(page: Page, root: string, name = ' await pathInput.fill(join(root, name)) await pathInput.press('Enter') await dialog.getByRole('button', { name: '打开', exact: true }).click() - await page.locator('[data-composer-input][contenteditable="true"][data-placeholder="描述你想要构建的内容… / 调用指令 @ 文件或对话"]') + await page.locator('[data-composer-input][contenteditable="true"][data-placeholder="描述你想要构建的内容, / 调用指令, @ 文件或对话"]') .waitFor({ timeout: 15_000 }) } diff --git a/packages/client/ui-conversation/src/client/locales.ts b/packages/client/ui-conversation/src/client/locales.ts index 3faaf8f009..0fa83f27eb 100644 --- a/packages/client/ui-conversation/src/client/locales.ts +++ b/packages/client/ui-conversation/src/client/locales.ts @@ -13,10 +13,10 @@ export const zh = { 'hint.goal': '输入目标,智能体将持续执行', 'hint.goal.active': '当前目标进行中。可输入 edit 修改 / pause 暂停 / resume 继续 / clear 清除', 'placeholder.plan': PLAN_NEXT_ACTION_ZH, - 'placeholder.default': '发消息或做任务… / 调用指令 @ 文件或对话', + 'placeholder.default': '发消息或创建任务, / 调用指令, @ 文件或对话', 'placeholder.unavailable': '会话不可用', 'placeholder.parentOffline': '父会话已离线,无法继续发送;仍可停止当前运行', - 'placeholder.hero': '描述你想要构建的内容… / 调用指令 @ 文件或对话', + 'placeholder.hero': '描述你想要构建的内容, / 调用指令, @ 文件或对话', 'placeholder.workspace': '选择一个工作区开始', 'placeholder.steerQueue': 'Cmd/Ctrl+Enter 插话发送全部排队消息', 'input.commands': '指令', @@ -164,10 +164,10 @@ export const en = { 'hint.goal': 'describe the objective for a long-running task', 'hint.goal.active': 'goal active — edit / pause / resume / clear', 'placeholder.plan': PLAN_NEXT_ACTION_EN, - 'placeholder.default': 'Message or run a task... / commands, @ files or sessions', + 'placeholder.default': 'Message or run a task, run / commands, @ files or sessions', 'placeholder.unavailable': 'Session unavailable', 'placeholder.parentOffline': 'Parent session offline; sending is unavailable but you can still stop the run', - 'placeholder.hero': 'Describe what you want to build... / commands, @ files or sessions', + 'placeholder.hero': 'Describe what you want to build, run / commands, @ files or sessions', 'placeholder.workspace': 'Choose a workspace to start', 'placeholder.steerQueue': 'Cmd/Ctrl+Enter steers all queued messages', 'input.commands': 'Commands', diff --git a/packages/client/ui-conversation/tests/input-bar.client.spec.tsx b/packages/client/ui-conversation/tests/input-bar.client.spec.tsx index 0edc8b9ef6..6b9f4e0f81 100644 --- a/packages/client/ui-conversation/tests/input-bar.client.spec.tsx +++ b/packages/client/ui-conversation/tests/input-bar.client.spec.tsx @@ -459,9 +459,9 @@ describe('Enter semantics', () => { }) it('keeps the owning placeholder or ordinary guidance when whole-queue steering is unavailable', () => { - expect(bench({ running: true }).placeholder).toBe('发消息或做任务… / 调用指令 @ 文件或对话') - expect(bench({ queue: [row('q-1')] }).placeholder).toBe('发消息或做任务… / 调用指令 @ 文件或对话') - expect(bench({ running: true, queue: [row('q-1')], draft: '消息' }).placeholder).toBe('发消息或做任务… / 调用指令 @ 文件或对话') + expect(bench({ running: true }).placeholder).toBe('发消息或创建任务, / 调用指令, @ 文件或对话') + expect(bench({ queue: [row('q-1')] }).placeholder).toBe('发消息或创建任务, / 调用指令, @ 文件或对话') + expect(bench({ running: true, queue: [row('q-1')], draft: '消息' }).placeholder).toBe('发消息或创建任务, / 调用指令, @ 文件或对话') expect(bench({ running: true, queue: [row('q-1')], @@ -469,7 +469,7 @@ describe('Enter semantics', () => { address: { parentSessionId: 'parent' as SessionId, childSessionId: SID, mode: 'continuable' }, parentAvailable: true, }, - }).placeholder).toBe('发消息或做任务… / 调用指令 @ 文件或对话') + }).placeholder).toBe('发消息或创建任务, / 调用指令, @ 文件或对话') expect(bench({ running: true, queue: [row('q-1')], @@ -481,7 +481,7 @@ describe('Enter semantics', () => { running: true, queue: [row('q-1')], commandMenuOpen: true, - }).placeholder).toBe('发消息或做任务… / 调用指令 @ 文件或对话') + }).placeholder).toBe('发消息或创建任务, / 调用指令, @ 文件或对话') // The steer hint intentionally outranks the plan placeholder: while it // shows, the whole-queue gesture is genuinely available in plan mode. expect(bench({ @@ -977,7 +977,7 @@ describe('running and lock semantics', () => { it('disabled state shows the unavailable placeholder; custom placeholder wins', () => { expect(bench({ disabled: true }).placeholder).toBe('会话不可用') const live = bench() - expect(live.placeholder).toBe('发消息或做任务… / 调用指令 @ 文件或对话') + expect(live.placeholder).toBe('发消息或创建任务, / 调用指令, @ 文件或对话') const custom = bench({ placeholder: 'Custom placeholder' }) expect(custom.placeholder).toBe('Custom placeholder') }) @@ -1024,7 +1024,7 @@ describe('running and lock semantics', () => { expect(entering.placeholder).toBe('描述你的任务以生成计划') // Pending exit: target is default again. const leaving = bench({ plan: { active: true, pending: true } }) - expect(leaving.placeholder).toBe('发消息或做任务… / 调用指令 @ 文件或对话') + expect(leaving.placeholder).toBe('发消息或创建任务, / 调用指令, @ 文件或对话') // Owner placeholder outranks the plan swap. const custom = bench({ plan: { active: true, pending: false }, placeholder: 'Custom placeholder' }) expect(custom.placeholder).toBe('Custom placeholder') diff --git a/snapshots/web/bash-abort-row/ui.expected.md b/snapshots/web/bash-abort-row/ui.expected.md index e633f1ca1c..ed88971715 100644 --- a/snapshots/web/bash-abort-row/ui.expected.md +++ b/snapshots/web/bash-abort-row/ui.expected.md @@ -26,7 +26,7 @@ - 'button "Failed Bash Error: tool call aborted before dispatch"': - img - text: "Failed Bash Error: tool call aborted before dispatch" -- textbox "Message or run a task... / commands, @ files or sessions" +- textbox "Message or run a task, run / commands, @ files or sessions" - button "Commands": - img - 'button "Access mode, current: Full access"': Full access diff --git a/snapshots/web/cordis-tool-round/ui.expected.md b/snapshots/web/cordis-tool-round/ui.expected.md index 78605cc160..264eebfaa3 100644 --- a/snapshots/web/cordis-tool-round/ui.expected.md +++ b/snapshots/web/cordis-tool-round/ui.expected.md @@ -114,7 +114,7 @@ - img - text: Ran for {{duration}} - text: {{clock}} -- textbox "Message or run a task... / commands, @ files or sessions" +- textbox "Message or run a task, run / commands, @ files or sessions" - button "Commands": - img - 'button "Access mode, current: Workspace Write"': Workspace Write diff --git a/snapshots/web/feedback-command/ack-expanded.expected.md b/snapshots/web/feedback-command/ack-expanded.expected.md index 5b20a1e993..01883c3735 100644 --- a/snapshots/web/feedback-command/ack-expanded.expected.md +++ b/snapshots/web/feedback-command/ack-expanded.expected.md @@ -44,7 +44,7 @@ - img - img - text: "feedback Feedback recorded for session session-{{uuid}} Anonymous user: {{uuid}}. Session sharing is enabled." -- textbox "Message or run a task... / commands, @ files or sessions" +- textbox "Message or run a task, run / commands, @ files or sessions" - button "Commands": - img - 'button "Access mode, current: Workspace Write"': Workspace Write diff --git a/snapshots/web/feedback-command/ack.expected.md b/snapshots/web/feedback-command/ack.expected.md index 89f5b6f42d..efb2bc2188 100644 --- a/snapshots/web/feedback-command/ack.expected.md +++ b/snapshots/web/feedback-command/ack.expected.md @@ -36,7 +36,7 @@ - img - img - text: "feedback Feedback recorded for session session-{{uuid}} Anonymous user: {{uuid}}. Session sharing is enabled." -- textbox "Message or run a task... / commands, @ files or sessions" +- textbox "Message or run a task, run / commands, @ files or sessions" - button "Commands": - img - 'button "Access mode, current: Workspace Write"': Workspace Write diff --git a/snapshots/web/feedback-release/ack-expanded.expected.md b/snapshots/web/feedback-release/ack-expanded.expected.md index 5b753960a5..4ff4b16da8 100644 --- a/snapshots/web/feedback-release/ack-expanded.expected.md +++ b/snapshots/web/feedback-release/ack-expanded.expected.md @@ -44,7 +44,7 @@ - img - img - text: "feedback Feedback recorded for session session-{{uuid}} Anonymous user: {{uuid}}. Session sharing is feedback-gated; recording feedback uploads the session records not yet shared." -- textbox "Message or run a task... / commands, @ files or sessions" +- textbox "Message or run a task, run / commands, @ files or sessions" - button "Commands": - img - 'button "Access mode, current: Workspace Write"': Workspace Write diff --git a/snapshots/web/feedback-release/ack.expected.md b/snapshots/web/feedback-release/ack.expected.md index 057d13cd9c..139650c4b7 100644 --- a/snapshots/web/feedback-release/ack.expected.md +++ b/snapshots/web/feedback-release/ack.expected.md @@ -36,7 +36,7 @@ - img - img - text: "feedback Feedback recorded for session session-{{uuid}} Anonymous user: {{uuid}}. Session sharing is feedback-gated; recording feedback uploads the session records not yet shared." -- textbox "Message or run a task... / commands, @ files or sessions" +- textbox "Message or run a task, run / commands, @ files or sessions" - button "Commands": - img - 'button "Access mode, current: Workspace Write"': Workspace Write diff --git a/snapshots/web/fresh-round-trip/ui-expanded.expected.md b/snapshots/web/fresh-round-trip/ui-expanded.expected.md index 108ee4050c..efdf765b06 100644 --- a/snapshots/web/fresh-round-trip/ui-expanded.expected.md +++ b/snapshots/web/fresh-round-trip/ui-expanded.expected.md @@ -48,7 +48,7 @@ - img - text: Ran for {{duration}} - text: {{clock}} -- textbox "Message or run a task... / commands, @ files or sessions" +- textbox "Message or run a task, run / commands, @ files or sessions" - button "Commands": - img - 'button "Access mode, current: Workspace Write"': Workspace Write diff --git a/snapshots/web/fresh-round-trip/ui.expected.md b/snapshots/web/fresh-round-trip/ui.expected.md index 9a83925d23..155f94151c 100644 --- a/snapshots/web/fresh-round-trip/ui.expected.md +++ b/snapshots/web/fresh-round-trip/ui.expected.md @@ -32,7 +32,7 @@ - img - text: Ran for {{duration}} - text: {{clock}} -- textbox "Message or run a task... / commands, @ files or sessions" +- textbox "Message or run a task, run / commands, @ files or sessions" - button "Commands": - img - 'button "Access mode, current: Workspace Write"': Workspace Write diff --git a/snapshots/web/goal-multi-turn-actions/ui-expanded.expected.md b/snapshots/web/goal-multi-turn-actions/ui-expanded.expected.md index 700f499256..8f434cbd5b 100644 --- a/snapshots/web/goal-multi-turn-actions/ui-expanded.expected.md +++ b/snapshots/web/goal-multi-turn-actions/ui-expanded.expected.md @@ -226,7 +226,7 @@ - text: {{clock}} - button "Back to bottom": - img -- textbox "Message or run a task... / commands, @ files or sessions" +- textbox "Message or run a task, run / commands, @ files or sessions" - button "Commands": - img - 'button "Access mode, current: Workspace Write"': Workspace Write diff --git a/snapshots/web/goal-multi-turn-actions/ui.expected.md b/snapshots/web/goal-multi-turn-actions/ui.expected.md index fd874dc327..6458d97ec7 100644 --- a/snapshots/web/goal-multi-turn-actions/ui.expected.md +++ b/snapshots/web/goal-multi-turn-actions/ui.expected.md @@ -114,7 +114,7 @@ - img - text: Ran for {{duration}} - text: {{clock}} -- textbox "Message or run a task... / commands, @ files or sessions" +- textbox "Message or run a task, run / commands, @ files or sessions" - button "Commands": - img - 'button "Access mode, current: Workspace Write"': Workspace Write diff --git a/snapshots/web/lifecycle-chrome/hero.expected.md b/snapshots/web/lifecycle-chrome/hero.expected.md index 2d9f315f43..8008769d64 100644 --- a/snapshots/web/lifecycle-chrome/hero.expected.md +++ b/snapshots/web/lifecycle-chrome/hero.expected.md @@ -29,7 +29,7 @@ - img - text: Standard mode - img -- textbox "Describe what you want to build... / commands, @ files or sessions": +- textbox "Describe what you want to build, run / commands, @ files or sessions": - paragraph - button "Commands": - img diff --git a/snapshots/web/lifecycle-chrome/plan-active.expected.md b/snapshots/web/lifecycle-chrome/plan-active.expected.md index 5f53d73cfd..7eb9ecc43d 100644 --- a/snapshots/web/lifecycle-chrome/plan-active.expected.md +++ b/snapshots/web/lifecycle-chrome/plan-active.expected.md @@ -29,7 +29,7 @@ - img - text: Standard mode - img -- textbox "Describe what you want to build... / commands, @ files or sessions" +- textbox "Describe what you want to build, run / commands, @ files or sessions" - button "Commands": - img - 'button "Access mode, current: Workspace Write"': Workspace Write diff --git a/snapshots/web/lifecycle-chrome/reloaded-expanded.expected.md b/snapshots/web/lifecycle-chrome/reloaded-expanded.expected.md index 00d271cff3..d5e2baa73d 100644 --- a/snapshots/web/lifecycle-chrome/reloaded-expanded.expected.md +++ b/snapshots/web/lifecycle-chrome/reloaded-expanded.expected.md @@ -40,7 +40,7 @@ - img - text: Ran for {{duration}} - text: {{clock}} -- textbox "Message or run a task... / commands, @ files or sessions" +- textbox "Message or run a task, run / commands, @ files or sessions" - button "Commands": - img - 'button "Access mode, current: Workspace Write"': Workspace Write diff --git a/snapshots/web/lifecycle-chrome/reloaded.expected.md b/snapshots/web/lifecycle-chrome/reloaded.expected.md index bc5558ab03..02668c7ba4 100644 --- a/snapshots/web/lifecycle-chrome/reloaded.expected.md +++ b/snapshots/web/lifecycle-chrome/reloaded.expected.md @@ -32,7 +32,7 @@ - img - text: Ran for {{duration}} - text: {{clock}} -- textbox "Message or run a task... / commands, @ files or sessions" +- textbox "Message or run a task, run / commands, @ files or sessions" - button "Commands": - img - 'button "Access mode, current: Workspace Write"': Workspace Write diff --git a/snapshots/web/live-interactions/cancel-expanded.expected.md b/snapshots/web/live-interactions/cancel-expanded.expected.md index b61cd9e51a..9e53fe0ffe 100644 --- a/snapshots/web/live-interactions/cancel-expanded.expected.md +++ b/snapshots/web/live-interactions/cancel-expanded.expected.md @@ -37,7 +37,7 @@ - img - text: Ran for {{duration}} - text: {{clock}} -- textbox "Message or run a task... / commands, @ files or sessions" +- textbox "Message or run a task, run / commands, @ files or sessions" - button "Commands": - img - 'button "Access mode, current: Workspace Write"': Workspace Write diff --git a/snapshots/web/live-interactions/cancel.expected.md b/snapshots/web/live-interactions/cancel.expected.md index 4dbfb09b4a..300d0545a5 100644 --- a/snapshots/web/live-interactions/cancel.expected.md +++ b/snapshots/web/live-interactions/cancel.expected.md @@ -33,7 +33,7 @@ - img - text: Ran for {{duration}} - text: {{clock}} -- textbox "Message or run a task... / commands, @ files or sessions" +- textbox "Message or run a task, run / commands, @ files or sessions" - button "Commands": - img - 'button "Access mode, current: Workspace Write"': Workspace Write diff --git a/snapshots/web/live-interactions/error-auth.expected.md b/snapshots/web/live-interactions/error-auth.expected.md index ae854c557c..b4f2894bbd 100644 --- a/snapshots/web/live-interactions/error-auth.expected.md +++ b/snapshots/web/live-interactions/error-auth.expected.md @@ -23,7 +23,7 @@ - status: - text: This turn failedAPI key is invalid - code: AUTH -- textbox "Message or run a task... / commands, @ files or sessions" +- textbox "Message or run a task, run / commands, @ files or sessions" - button "Commands": - img - 'button "Access mode, current: Workspace Write"': Workspace Write diff --git a/snapshots/web/live-interactions/loading.expected.md b/snapshots/web/live-interactions/loading.expected.md index 4307a62de2..ec8e57263a 100644 --- a/snapshots/web/live-interactions/loading.expected.md +++ b/snapshots/web/live-interactions/loading.expected.md @@ -22,7 +22,7 @@ - text: Context injection @deepseek-ai/dsh-system-prompt - paragraph: partial - status: Deep diving... -- textbox "Message or run a task... / commands, @ files or sessions" +- textbox "Message or run a task, run / commands, @ files or sessions" - button "Commands": - img - 'button "Access mode, current: Workspace Write"': Workspace Write diff --git a/snapshots/web/live-interactions/retry-exhausted.expected.md b/snapshots/web/live-interactions/retry-exhausted.expected.md index 7dc6c9f613..74f3dcd3b3 100644 --- a/snapshots/web/live-interactions/retry-exhausted.expected.md +++ b/snapshots/web/live-interactions/retry-exhausted.expected.md @@ -25,7 +25,7 @@ - status: - text: This turn failedupstream 503 - code: SERVER -- textbox "Message or run a task... / commands, @ files or sessions" +- textbox "Message or run a task, run / commands, @ files or sessions" - button "Commands": - img - 'button "Access mode, current: Workspace Write"': Workspace Write diff --git a/snapshots/web/live-interactions/retry-expanded.expected.md b/snapshots/web/live-interactions/retry-expanded.expected.md index 1c6a87f6d1..16d9761a90 100644 --- a/snapshots/web/live-interactions/retry-expanded.expected.md +++ b/snapshots/web/live-interactions/retry-expanded.expected.md @@ -42,7 +42,7 @@ - img - text: Ran for {{duration}} - text: {{clock}} -- textbox "Message or run a task... / commands, @ files or sessions" +- textbox "Message or run a task, run / commands, @ files or sessions" - button "Commands": - img - 'button "Access mode, current: Workspace Write"': Workspace Write diff --git a/snapshots/web/live-interactions/retry.expected.md b/snapshots/web/live-interactions/retry.expected.md index 93107fe00b..e729aa627f 100644 --- a/snapshots/web/live-interactions/retry.expected.md +++ b/snapshots/web/live-interactions/retry.expected.md @@ -32,7 +32,7 @@ - img - text: Ran for {{duration}} - text: {{clock}} -- textbox "Message or run a task... / commands, @ files or sessions" +- textbox "Message or run a task, run / commands, @ files or sessions" - button "Commands": - img - 'button "Access mode, current: Workspace Write"': Workspace Write diff --git a/snapshots/web/live-interactions/running-draft.expected.md b/snapshots/web/live-interactions/running-draft.expected.md index 6f8f2cd629..909214d76c 100644 --- a/snapshots/web/live-interactions/running-draft.expected.md +++ b/snapshots/web/live-interactions/running-draft.expected.md @@ -22,7 +22,7 @@ - text: Context injection @deepseek-ai/dsh-system-prompt - paragraph: partial - status: Deep diving... -- textbox "Message or run a task... / commands, @ files or sessions": +- textbox "Message or run a task, run / commands, @ files or sessions": - paragraph: Queue this follow-up while the current turn is running. - button "Commands": - img diff --git a/snapshots/web/message-actions/ui.expected.md b/snapshots/web/message-actions/ui.expected.md index b8ea35966b..20a6119243 100644 --- a/snapshots/web/message-actions/ui.expected.md +++ b/snapshots/web/message-actions/ui.expected.md @@ -66,7 +66,7 @@ - img - text: Ran for {{duration}} - text: 7/25 {{clock}} -- textbox "Message or run a task... / commands, @ files or sessions" +- textbox "Message or run a task, run / commands, @ files or sessions" - button "Commands": - img - 'button "Access mode, current: Workspace Write"': Workspace Write diff --git a/snapshots/web/minimal-preset/ui.expected.md b/snapshots/web/minimal-preset/ui.expected.md index e26d5cdb11..90e013e31f 100644 --- a/snapshots/web/minimal-preset/ui.expected.md +++ b/snapshots/web/minimal-preset/ui.expected.md @@ -37,7 +37,7 @@ - img - text: Ran for {{duration}} - text: {{clock}} -- textbox "Message or run a task... / commands, @ files or sessions" +- textbox "Message or run a task, run / commands, @ files or sessions" - button "Commands": - img - 'button "Access mode, current: Workspace Write"': Workspace Write diff --git a/snapshots/web/plan-review/approved-expanded.expected.md b/snapshots/web/plan-review/approved-expanded.expected.md index 0233cfa370..73ee4e10ae 100644 --- a/snapshots/web/plan-review/approved-expanded.expected.md +++ b/snapshots/web/plan-review/approved-expanded.expected.md @@ -58,7 +58,7 @@ - img - text: Ran for {{duration}} - text: {{clock}} -- textbox "Message or run a task... / commands, @ files or sessions" +- textbox "Message or run a task, run / commands, @ files or sessions" - button "Commands": - img - 'button "Access mode, current: Workspace Write"': Workspace Write diff --git a/snapshots/web/plan-review/approved.expected.md b/snapshots/web/plan-review/approved.expected.md index 14ba8a416e..981b606fb1 100644 --- a/snapshots/web/plan-review/approved.expected.md +++ b/snapshots/web/plan-review/approved.expected.md @@ -38,7 +38,7 @@ - img - text: Ran for {{duration}} - text: {{clock}} -- textbox "Message or run a task... / commands, @ files or sessions" +- textbox "Message or run a task, run / commands, @ files or sessions" - button "Commands": - img - 'button "Access mode, current: Workspace Write"': Workspace Write diff --git a/snapshots/web/ptc-round/ui.expected.md b/snapshots/web/ptc-round/ui.expected.md index 80b8866505..6cbb8be04b 100644 --- a/snapshots/web/ptc-round/ui.expected.md +++ b/snapshots/web/ptc-round/ui.expected.md @@ -53,7 +53,7 @@ - img - text: Ran for {{duration}} - text: {{clock}} -- textbox "Message or run a task... / commands, @ files or sessions" +- textbox "Message or run a task, run / commands, @ files or sessions" - button "Commands": - img - 'button "Access mode, current: Workspace Write"': Workspace Write diff --git a/snapshots/web/question-composer/answered-expanded.expected.md b/snapshots/web/question-composer/answered-expanded.expected.md index b2f7ba6294..90e2fd340e 100644 --- a/snapshots/web/question-composer/answered-expanded.expected.md +++ b/snapshots/web/question-composer/answered-expanded.expected.md @@ -50,7 +50,7 @@ - img - text: Ran for {{duration}} - text: {{clock}} -- textbox "Message or run a task... / commands, @ files or sessions" +- textbox "Message or run a task, run / commands, @ files or sessions" - button "Commands": - img - 'button "Access mode, current: Workspace Write"': Workspace Write diff --git a/snapshots/web/question-composer/answered.expected.md b/snapshots/web/question-composer/answered.expected.md index a6dc5b28fd..5c01a40d89 100644 --- a/snapshots/web/question-composer/answered.expected.md +++ b/snapshots/web/question-composer/answered.expected.md @@ -32,7 +32,7 @@ - img - text: Ran for {{duration}} - text: {{clock}} -- textbox "Message or run a task... / commands, @ files or sessions" +- textbox "Message or run a task, run / commands, @ files or sessions" - button "Commands": - img - 'button "Access mode, current: Workspace Write"': Workspace Write diff --git a/snapshots/web/question-composer/cancelled.expected.md b/snapshots/web/question-composer/cancelled.expected.md index 0c18353ee1..147ebabd2f 100644 --- a/snapshots/web/question-composer/cancelled.expected.md +++ b/snapshots/web/question-composer/cancelled.expected.md @@ -29,7 +29,7 @@ - list: - listitem: Which color do you prefer? - button "Inspect" -- textbox "Message or run a task... / commands, @ files or sessions" +- textbox "Message or run a task, run / commands, @ files or sessions" - button "Commands": - img - 'button "Access mode, current: Workspace Write"': Workspace Write diff --git a/snapshots/web/queue-actions/preserved-expanded.expected.md b/snapshots/web/queue-actions/preserved-expanded.expected.md index 5a5143fc2c..58159d8fa8 100644 --- a/snapshots/web/queue-actions/preserved-expanded.expected.md +++ b/snapshots/web/queue-actions/preserved-expanded.expected.md @@ -56,7 +56,7 @@ - img - button "Steer queued message" [disabled]: - img -- textbox "Message or run a task... / commands, @ files or sessions" +- textbox "Message or run a task, run / commands, @ files or sessions" - button "Commands": - img - 'button "Access mode, current: Workspace Write"': Workspace Write diff --git a/snapshots/web/queue-actions/preserved.expected.md b/snapshots/web/queue-actions/preserved.expected.md index 9798e5da4f..acccb8ba21 100644 --- a/snapshots/web/queue-actions/preserved.expected.md +++ b/snapshots/web/queue-actions/preserved.expected.md @@ -52,7 +52,7 @@ - img - button "Steer queued message" [disabled]: - img -- textbox "Message or run a task... / commands, @ files or sessions" +- textbox "Message or run a task, run / commands, @ files or sessions" - button "Commands": - img - 'button "Access mode, current: Workspace Write"': Workspace Write diff --git a/snapshots/web/queued-image/delivered.expected.md b/snapshots/web/queued-image/delivered.expected.md index b5dc457a4e..4abc5d808d 100644 --- a/snapshots/web/queued-image/delivered.expected.md +++ b/snapshots/web/queued-image/delivered.expected.md @@ -76,7 +76,7 @@ - img - text: Ran for {{duration}} - text: {{clock}} -- textbox "Message or run a task... / commands, @ files or sessions" +- textbox "Message or run a task, run / commands, @ files or sessions" - button "Commands": - img - 'button "Access mode, current: Workspace Write"': Workspace Write diff --git a/snapshots/web/seeded-history/command-row.expected.md b/snapshots/web/seeded-history/command-row.expected.md index 3f4ed25eb3..7f52c24bbc 100644 --- a/snapshots/web/seeded-history/command-row.expected.md +++ b/snapshots/web/seeded-history/command-row.expected.md @@ -58,7 +58,7 @@ - text: Context injection AGENTS.md - img - text: permission preset read-only -- textbox "Message or run a task... / commands, @ files or sessions" +- textbox "Message or run a task, run / commands, @ files or sessions" - button "Commands": - img - 'button "Access mode, current: Read Only"': Read Only diff --git a/snapshots/web/seeded-history/feedback-row.expected.md b/snapshots/web/seeded-history/feedback-row.expected.md index 1265a3e50e..1724e6fa96 100644 --- a/snapshots/web/seeded-history/feedback-row.expected.md +++ b/snapshots/web/seeded-history/feedback-row.expected.md @@ -62,7 +62,7 @@ - img - text: "feedback Feedback recorded for session {{seededId}} Anonymous user: {{uuid}}. Session sharing is not configured." - text: "Feedback recorded for session {{seededId}} Anonymous user: {{uuid}}. Session sharing is not configured." -- textbox "Message or run a task... / commands, @ files or sessions" +- textbox "Message or run a task, run / commands, @ files or sessions" - button "Commands": - img - 'button "Access mode, current: Read Only"': Read Only diff --git a/snapshots/web/seeded-history/ui-expanded.expected.md b/snapshots/web/seeded-history/ui-expanded.expected.md index 2d18d8ec62..2f67dbd50d 100644 --- a/snapshots/web/seeded-history/ui-expanded.expected.md +++ b/snapshots/web/seeded-history/ui-expanded.expected.md @@ -56,7 +56,7 @@ - img - img - text: Context injection AGENTS.md -- textbox "Message or run a task... / commands, @ files or sessions" +- textbox "Message or run a task, run / commands, @ files or sessions" - button "Commands": - img - 'button "Access mode, current: Workspace Write"': Workspace Write diff --git a/snapshots/web/seeded-history/ui.expected.md b/snapshots/web/seeded-history/ui.expected.md index 4711ff7304..6778257a55 100644 --- a/snapshots/web/seeded-history/ui.expected.md +++ b/snapshots/web/seeded-history/ui.expected.md @@ -38,7 +38,7 @@ - img - img - text: Context injection AGENTS.md -- textbox "Message or run a task... / commands, @ files or sessions" +- textbox "Message or run a task, run / commands, @ files or sessions" - button "Commands": - img - 'button "Access mode, current: Workspace Write"': Workspace Write diff --git a/snapshots/web/skill-tool-row/ui.expected.md b/snapshots/web/skill-tool-row/ui.expected.md index cff07a3aae..48b96721cc 100644 --- a/snapshots/web/skill-tool-row/ui.expected.md +++ b/snapshots/web/skill-tool-row/ui.expected.md @@ -51,7 +51,7 @@ - img - text: Ran for {{duration}} - text: {{date}} {{clock}} -- textbox "Message or run a task... / commands, @ files or sessions" +- textbox "Message or run a task, run / commands, @ files or sessions" - button "Commands": - img - 'button "Access mode, current: Full access"': Full access diff --git a/snapshots/web/steering/settled-expanded.expected.md b/snapshots/web/steering/settled-expanded.expected.md index bfd48e7ed9..16c7f00fd7 100644 --- a/snapshots/web/steering/settled-expanded.expected.md +++ b/snapshots/web/steering/settled-expanded.expected.md @@ -43,7 +43,7 @@ - img - text: Ran for {{duration}} - text: {{clock}} -- textbox "Message or run a task... / commands, @ files or sessions" +- textbox "Message or run a task, run / commands, @ files or sessions" - button "Commands": - img - 'button "Access mode, current: Workspace Write"': Workspace Write diff --git a/snapshots/web/steering/settled.expected.md b/snapshots/web/steering/settled.expected.md index 0d190d89e9..e62b8ab4d4 100644 --- a/snapshots/web/steering/settled.expected.md +++ b/snapshots/web/steering/settled.expected.md @@ -35,7 +35,7 @@ - img - text: Ran for {{duration}} - text: {{clock}} -- textbox "Message or run a task... / commands, @ files or sessions" +- textbox "Message or run a task, run / commands, @ files or sessions" - button "Commands": - img - 'button "Access mode, current: Workspace Write"': Workspace Write diff --git a/snapshots/web/subagent-conversation/ui-expanded.expected.md b/snapshots/web/subagent-conversation/ui-expanded.expected.md index 2cbe5b1fa8..2a0549a1d7 100644 --- a/snapshots/web/subagent-conversation/ui-expanded.expected.md +++ b/snapshots/web/subagent-conversation/ui-expanded.expected.md @@ -75,7 +75,7 @@ - img - text: Ran for {{duration}} - text: {{clock}} -- textbox "Message or run a task... / commands, @ files or sessions" +- textbox "Message or run a task, run / commands, @ files or sessions" - button "Commands": - img - 'button "Access mode, current: Custom"': Custom diff --git a/snapshots/web/subagent-conversation/ui.expected.md b/snapshots/web/subagent-conversation/ui.expected.md index 38b587e257..17e68fb4ed 100644 --- a/snapshots/web/subagent-conversation/ui.expected.md +++ b/snapshots/web/subagent-conversation/ui.expected.md @@ -63,7 +63,7 @@ - img - text: Ran for {{duration}} - text: {{clock}} -- textbox "Message or run a task... / commands, @ files or sessions" +- textbox "Message or run a task, run / commands, @ files or sessions" - button "Commands": - img - 'button "Access mode, current: Custom"': Custom diff --git a/snapshots/web/turn-tail-actions/completed.expected.md b/snapshots/web/turn-tail-actions/completed.expected.md index 637055a274..95f6f440f5 100644 --- a/snapshots/web/turn-tail-actions/completed.expected.md +++ b/snapshots/web/turn-tail-actions/completed.expected.md @@ -35,7 +35,7 @@ - img - text: Ran for {{duration}} - text: {{clock}} -- textbox "Message or run a task... / commands, @ files or sessions" +- textbox "Message or run a task, run / commands, @ files or sessions" - button "Commands": - img - 'button "Access mode, current: Workspace Write"': Workspace Write diff --git a/snapshots/web/turn-tail-actions/focused.expected.md b/snapshots/web/turn-tail-actions/focused.expected.md index 94870d19c8..2ddeaa1c4d 100644 --- a/snapshots/web/turn-tail-actions/focused.expected.md +++ b/snapshots/web/turn-tail-actions/focused.expected.md @@ -48,7 +48,7 @@ - img - text: Ran for {{duration}} - text: {{clock}} -- textbox "Message or run a task... / commands, @ files or sessions" +- textbox "Message or run a task, run / commands, @ files or sessions" - button "Commands": - img - 'button "Access mode, current: Workspace Write"': Workspace Write diff --git a/snapshots/web/turn-tail-actions/running.expected.md b/snapshots/web/turn-tail-actions/running.expected.md index d79602b47c..90a45f8028 100644 --- a/snapshots/web/turn-tail-actions/running.expected.md +++ b/snapshots/web/turn-tail-actions/running.expected.md @@ -32,7 +32,7 @@ - text: Bash Print alpha to stdout - paragraph: partial - status: Deep diving... -- textbox "Message or run a task... / commands, @ files or sessions" +- textbox "Message or run a task, run / commands, @ files or sessions" - button "Commands": - img - 'button "Access mode, current: Workspace Write"': Workspace Write diff --git a/snapshots/web/turn-tail-actions/settled.expected.md b/snapshots/web/turn-tail-actions/settled.expected.md index 26bd7a6d83..bfed5ecb1a 100644 --- a/snapshots/web/turn-tail-actions/settled.expected.md +++ b/snapshots/web/turn-tail-actions/settled.expected.md @@ -34,7 +34,7 @@ - img - text: Ran for {{duration}} - text: {{clock}} -- textbox "Message or run a task... / commands, @ files or sessions" +- textbox "Message or run a task, run / commands, @ files or sessions" - button "Commands": - img - 'button "Access mode, current: Workspace Write"': Workspace Write diff --git a/snapshots/web/turn-tail-actions/usage-expanded.expected.md b/snapshots/web/turn-tail-actions/usage-expanded.expected.md index 43d984fc53..4c8f84e2bd 100644 --- a/snapshots/web/turn-tail-actions/usage-expanded.expected.md +++ b/snapshots/web/turn-tail-actions/usage-expanded.expected.md @@ -35,7 +35,7 @@ - img - text: Ran for {{duration}} - text: {{clock}} -- textbox "Message or run a task... / commands, @ files or sessions" +- textbox "Message or run a task, run / commands, @ files or sessions" - button "Commands": - img - 'button "Access mode, current: Workspace Write"': Workspace Write diff --git a/snapshots/web/web-search-round/ui.expected.md b/snapshots/web/web-search-round/ui.expected.md index 0968ee2b75..8b2f000f9a 100644 --- a/snapshots/web/web-search-round/ui.expected.md +++ b/snapshots/web/web-search-round/ui.expected.md @@ -40,7 +40,7 @@ - img - text: Ran for {{duration}} - text: {{clock}} -- textbox "Message or run a task... / commands, @ files or sessions" +- textbox "Message or run a task, run / commands, @ files or sessions" - button "Commands": - img - 'button "Access mode, current: Workspace Write"': Workspace Write From eac164392d1b35de75107826dd610c3eeac6da4d Mon Sep 17 00:00:00 2001 From: Yif <877193178@qq.com> Date: Fri, 4 Sep 2026 20:22:11 +0800 Subject: [PATCH 04/10] fix(client): name the context meter's tools row precisely and drop the header approximation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The heuristic tools segment counts tool schemas only, so the row reads 工具定义 / Tool definitions; the header figures lose the ~ because they are anchored to provider-reported usage, unlike the heuristic rows that keep it. --- packages/client/ui-conversation/src/client/locales.ts | 4 ++-- .../src/client/skeleton/ContextMeter.tsx | 4 +++- .../tests/context-meter.client.spec.tsx | 10 +++++----- 3 files changed, 10 insertions(+), 8 deletions(-) diff --git a/packages/client/ui-conversation/src/client/locales.ts b/packages/client/ui-conversation/src/client/locales.ts index 0fa83f27eb..ed93b09b4b 100644 --- a/packages/client/ui-conversation/src/client/locales.ts +++ b/packages/client/ui-conversation/src/client/locales.ts @@ -49,7 +49,7 @@ export const zh = { 'context.aria': '上下文已用 {percent}', 'context.used': '上下文已用', 'context.system': '系统提示词', - 'context.tools': '工具', + 'context.tools': '工具定义', 'context.messages': '对话消息', 'settings.enter.title': '繁忙时 Enter 键行为', 'settings.enter.description': '仅在智能体运行时生效;Cmd/Ctrl+Enter 使用另一行为', @@ -200,7 +200,7 @@ export const en = { 'context.aria': '{percent} of context used', 'context.used': 'of context used', 'context.system': 'System prompt', - 'context.tools': 'Tools', + 'context.tools': 'Tool definitions', 'context.messages': 'Messages', 'settings.enter.title': 'Enter behavior while busy', 'settings.enter.description': 'Busy only; Cmd/Ctrl+Enter uses the other behavior', diff --git a/packages/client/ui-conversation/src/client/skeleton/ContextMeter.tsx b/packages/client/ui-conversation/src/client/skeleton/ContextMeter.tsx index 0caaf28cb8..396d72b408 100644 --- a/packages/client/ui-conversation/src/client/skeleton/ContextMeter.tsx +++ b/packages/client/ui-conversation/src/client/skeleton/ContextMeter.tsx @@ -135,8 +135,10 @@ export function ContextMeter({ useProjection, t }: ContextMeterProps) { {headBefore} {reading} {headAfter} + {/* No `~`: the headline figure is anchored to provider-reported + usage, unlike the heuristic composition rows below. */} - {`~${formatTokens(context.usedTokens, t)} / ${formatTokens(context.contextWindow, t)}`} + {`${formatTokens(context.usedTokens, t)} / ${formatTokens(context.contextWindow, t)}`}
diff --git a/packages/client/ui-conversation/tests/context-meter.client.spec.tsx b/packages/client/ui-conversation/tests/context-meter.client.spec.tsx index 75b9a59d0c..acf1156ca1 100644 --- a/packages/client/ui-conversation/tests/context-meter.client.spec.tsx +++ b/packages/client/ui-conversation/tests/context-meter.client.spec.tsx @@ -54,11 +54,11 @@ describe('ContextMeter', () => { expect(view.container.querySelector('[role="dialog"]')).toBeNull() fireEvent.click(trigger) const panel = view.container.querySelector('[role="dialog"]')! - expect(panel.textContent).toContain('~32K / 128K') + expect(panel.textContent).toContain('32K / 128K') expect(panel.textContent).toContain('25%') expect(panel.textContent).toContain('上下文已用') expect(panel.textContent).toContain('系统提示词~120') - expect(panel.textContent).toContain('工具~21.5K') + expect(panel.textContent).toContain('工具定义~21.5K') expect(panel.textContent).toContain('对话消息~477K') // The occupancy bar splits into one colored segment per composition row. expect(panel.getElementsByClassName(segmentClass)).toHaveLength(3) @@ -94,7 +94,7 @@ describe('ContextMeter', () => { // `.segment` carries a min-width, so a zero-width part would still paint a // filled sliver over an empty context. expect(panel.getElementsByClassName(segmentClass)).toHaveLength(0) - expect(panel.textContent).toContain('~0 / 128K') + expect(panel.textContent).toContain('0 / 128K') }) it('reads the ring from the projected figure so a compaction shows at once', () => { @@ -106,14 +106,14 @@ describe('ContextMeter', () => { }) const trigger = view.getByRole('button', { name: '上下文已用 2%' }) fireEvent.click(trigger) - expect(view.container.querySelector('[role="dialog"]')!.textContent).toContain('~3K / 128K') + expect(view.container.querySelector('[role="dialog"]')!.textContent).toContain('3K / 128K') }) it('omits the composition rows while the contextBreakdown projection is absent', () => { const view = meter({ contextPressure: { pressureTokens: 32_000, contextWindow: 128_000 } }) fireEvent.click(view.getByRole('button', { name: '上下文已用 25%' })) const panel = view.container.querySelector('[role="dialog"]')! - expect(panel.textContent).toContain('~32K / 128K') + expect(panel.textContent).toContain('32K / 128K') expect(panel.textContent).not.toContain('系统提示词') expect(panel.textContent).not.toContain('对话消息') // Without composition shares, the bar falls back to one plain segment. From fd1e51b097ed962b0a634cd37dc497fed9831920 Mon Sep 17 00:00:00 2001 From: Yif <877193178@qq.com> Date: Fri, 4 Sep 2026 20:25:04 +0800 Subject: [PATCH 05/10] test(web): refresh master-added goldens for the reworded placeholder file-upload-round and the clickable-links gallery were recorded on master with the pre-rewording composer placeholder. --- apps/web/tests/expected/clickable-links-gallery/ui.expected.md | 2 +- snapshots/web/file-upload-round/ui.expected.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/web/tests/expected/clickable-links-gallery/ui.expected.md b/apps/web/tests/expected/clickable-links-gallery/ui.expected.md index 4babf591db..b0ff0e6411 100644 --- a/apps/web/tests/expected/clickable-links-gallery/ui.expected.md +++ b/apps/web/tests/expected/clickable-links-gallery/ui.expected.md @@ -191,7 +191,7 @@ - text: {{clock}} - button "Back to bottom": - img -- textbox "Message or run a task... / commands, @ files or sessions" +- textbox "Message or run a task, run / commands, @ files or sessions" - button "Commands": - img - button "Add attachment": diff --git a/snapshots/web/file-upload-round/ui.expected.md b/snapshots/web/file-upload-round/ui.expected.md index 2541ec1c4a..7d34ba8037 100644 --- a/snapshots/web/file-upload-round/ui.expected.md +++ b/snapshots/web/file-upload-round/ui.expected.md @@ -38,7 +38,7 @@ - img - text: Ran for {{duration}} - text: {{clock}} -- textbox "Message or run a task... / commands, @ files or sessions" +- textbox "Message or run a task, run / commands, @ files or sessions" - button "Commands": - img - button "Add attachment": From 3997f36999a186e9a24944f162f3fe0882ccd8e4 Mon Sep 17 00:00:00 2001 From: Yif <877193178@qq.com> Date: Mon, 7 Sep 2026 20:59:19 +0800 Subject: [PATCH 06/10] feat(client): replace the stats strip with two icon pills and click-open stat dialogs The A/B winner over the single-line StatsLine: a gauge pill (counts + TPS, opening the session-stats dialog) and a database pill (billed total + cache-hit share, opening the exact token-usage dialog), sharing a new stat-dialog module. StatsLine and its dead locale keys are deleted, the gauge icon is optically centered, and the composer keeps its 4px clearance through the data-composer-stats contract. Agent Note added; stale StatsLine references across implemented notes updated in place. --- ...-token-usage-and-request-context.i18n.yaml | 4 +- ...ojected-token-usage-and-request-context.md | 4 +- ...cted-token-usage-and-request-context.zh.md | 4 +- ...lient-conversation-node-assembly.i18n.yaml | 4 +- ...08-09-client-conversation-node-assembly.md | 4 +- ...09-client-conversation-node-assembly.zh.md | 4 +- ...12-full-session-turn-step-counts.i18n.yaml | 4 +- ...026-08-12-full-session-turn-step-counts.md | 2 +- ...-08-12-full-session-turn-step-counts.zh.md | 2 +- ...versation-adaptive-content-width.i18n.yaml | 4 +- ...-18-conversation-adaptive-content-width.md | 2 +- ...-conversation-adaptive-content-width.zh.md | 2 +- ...08-18-settings-font-size-control.i18n.yaml | 4 +- .../2026-08-18-settings-font-size-control.md | 2 +- ...026-08-18-settings-font-size-control.zh.md | 2 +- ...-07-composer-session-stats-pills.i18n.yaml | 6 + ...2026-09-07-composer-session-stats-pills.md | 32 ++ ...6-09-07-composer-session-stats-pills.zh.md | 32 ++ .../command-image-envelope.expected.e2e.ts | 2 +- .../clickable-links-gallery/ui.expected.md | 6 +- .../conversation-expanded.expected.md | 6 +- .../conversation.expected.md | 6 +- .../goal-command-presentation/ui.expected.md | 2 +- .../markdown-cjk-strong/ui.expected.md | 6 +- .../expected/markdown-images/ui.expected.md | 6 +- .../markdown-inline-code-links/ui.expected.md | 6 +- .../expected/math-rendering/ui.expected.md | 6 +- .../reference-composer/order.expected.md | 2 +- .../skill-user-invoke/ui-expanded.expected.md | 9 +- .../expected/skill-user-invoke/ui.expected.md | 9 +- .../stats-paged-history/ui.expected.md | 6 +- .../expected/steer-all/mid-steer.expected.md | 2 +- .../steer-all/settled-expanded.expected.md | 9 +- .../expected/steer-all/settled.expected.md | 9 +- apps/web/tests/goal-bar.e2e.ts | 2 +- apps/web/tests/image-display.expected.e2e.ts | 6 +- apps/web/tests/lifecycle-chrome.e2e.ts | 3 +- apps/web/tests/math-rendering.e2e.ts | 2 +- apps/web/tests/preview-boot.e2e.ts | 2 +- .../mid-stream.expected.md | 2 +- apps/web/tests/startup-auto-selection.e2e.ts | 2 +- apps/web/tests/stats-paged-history.e2e.ts | 2 +- apps/web/tests/subagent-conversation.e2e.ts | 6 +- apps/web/tests/subagent-interrupt-ui.e2e.ts | 2 +- apps/web/tests/support.ts | 2 +- .../ui-attachment/src/FileCard.module.css | 12 + .../client/ui-attachment/src/FileCard.tsx | 6 +- packages/client/ui-chat/src/client/apply.ts | 4 +- .../src/client/chat/StatsLine.module.css | 26 -- .../ui-chat/src/client/chat/StatsLine.tsx | 209 ------------ .../src/client/chat/StatsPills.module.css | 63 ++++ .../ui-chat/src/client/chat/StatsPills.tsx | 299 ++++++++++++++++++ .../src/client/chat/TurnUsagePanel.module.css | 95 +----- .../src/client/chat/TurnUsagePanel.tsx | 85 +---- .../src/client/chat/stat-dialog.module.css | 94 ++++++ .../ui-chat/src/client/chat/stat-dialog.ts | 62 ++++ .../ui-chat/src/client/contract/snapshot.ts | 2 +- .../src/client/contract/turn-metrics.ts | 2 +- .../chat-snapshot-builder.ts | 4 +- packages/client/ui-chat/src/client/locale.ts | 26 +- .../tests/chat-branch-tails.client.spec.tsx | 23 +- .../ui-chat/tests/chat-stats.client.spec.tsx | 286 +++++++++-------- .../tests/gate-branch-tails.client.spec.tsx | 19 +- .../ui-conversation/src/client/locales.ts | 4 +- .../src/client/skeleton/InputBar.module.css | 18 +- .../src/client/ModelSelect.tsx | 4 + .../client/ui-primitives/src/icons/index.tsx | 11 + .../src/client/slot-catalog.ts | 2 +- snapshots/web/bash-abort-row/ui.expected.md | 9 +- .../web/cordis-tool-round/ui.expected.md | 9 +- .../feedback-command/ack-expanded.expected.md | 9 +- .../web/feedback-command/ack.expected.md | 9 +- .../feedback-release/ack-expanded.expected.md | 9 +- .../web/feedback-release/ack.expected.md | 9 +- .../web/file-upload-round/ui.expected.md | 9 +- .../fresh-round-trip/ui-expanded.expected.md | 9 +- snapshots/web/fresh-round-trip/ui.expected.md | 9 +- .../ui-expanded.expected.md | 9 +- .../goal-multi-turn-actions/ui.expected.md | 9 +- .../web/lifecycle-chrome/hero.expected.md | 2 +- .../lifecycle-chrome/plan-active.expected.md | 2 +- .../reloaded-expanded.expected.md | 9 +- .../web/lifecycle-chrome/reloaded.expected.md | 9 +- .../cancel-expanded.expected.md | 6 +- .../web/live-interactions/cancel.expected.md | 6 +- .../live-interactions/error-auth.expected.md | 6 +- .../web/live-interactions/loading.expected.md | 2 +- .../retry-exhausted.expected.md | 6 +- .../retry-expanded.expected.md | 9 +- .../web/live-interactions/retry.expected.md | 9 +- .../running-draft.expected.md | 2 +- snapshots/web/message-actions/ui.expected.md | 9 +- snapshots/web/minimal-preset/ui.expected.md | 9 +- .../plan-review/approved-expanded.expected.md | 9 +- .../web/plan-review/approved.expected.md | 10 +- snapshots/web/ptc-round/ui.expected.md | 9 +- .../answered-expanded.expected.md | 9 +- .../question-composer/answered.expected.md | 9 +- .../question-composer/cancelled.expected.md | 9 +- .../preserved-expanded.expected.md | 6 +- .../web/queue-actions/preserved.expected.md | 6 +- .../web/queued-image/delivered.expected.md | 9 +- .../seeded-history/command-row.expected.md | 9 +- .../seeded-history/feedback-row.expected.md | 9 +- .../seeded-history/ui-expanded.expected.md | 9 +- snapshots/web/seeded-history/ui.expected.md | 9 +- snapshots/web/skill-tool-row/ui.expected.md | 9 +- .../web/steering/settled-expanded.expected.md | 9 +- snapshots/web/steering/settled.expected.md | 9 +- .../ui-expanded.expected.md | 9 +- .../web/subagent-conversation/ui.expected.md | 9 +- .../turn-tail-actions/completed.expected.md | 9 +- .../web/turn-tail-actions/focused.expected.md | 9 +- .../web/turn-tail-actions/running.expected.md | 9 +- .../web/turn-tail-actions/settled.expected.md | 9 +- .../usage-expanded.expected.md | 9 +- snapshots/web/web-search-round/ui.expected.md | 9 +- 117 files changed, 1256 insertions(+), 737 deletions(-) create mode 100644 .agents/notes/implemented/feature/2026-09-07-composer-session-stats-pills.i18n.yaml create mode 100644 .agents/notes/implemented/feature/2026-09-07-composer-session-stats-pills.md create mode 100644 .agents/notes/implemented/feature/2026-09-07-composer-session-stats-pills.zh.md delete mode 100644 packages/client/ui-chat/src/client/chat/StatsLine.module.css delete mode 100644 packages/client/ui-chat/src/client/chat/StatsLine.tsx create mode 100644 packages/client/ui-chat/src/client/chat/StatsPills.module.css create mode 100644 packages/client/ui-chat/src/client/chat/StatsPills.tsx create mode 100644 packages/client/ui-chat/src/client/chat/stat-dialog.module.css create mode 100644 packages/client/ui-chat/src/client/chat/stat-dialog.ts diff --git a/.agents/notes/implemented/architecture/2026-07-29-projected-token-usage-and-request-context.i18n.yaml b/.agents/notes/implemented/architecture/2026-07-29-projected-token-usage-and-request-context.i18n.yaml index aaba9b1db2..892e3a8fc0 100644 --- a/.agents/notes/implemented/architecture/2026-07-29-projected-token-usage-and-request-context.i18n.yaml +++ b/.agents/notes/implemented/architecture/2026-07-29-projected-token-usage-and-request-context.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-07-29-projected-token-usage-and-request-context.md -2026-07-29-projected-token-usage-and-request-context.md: d62f7dccd544a342da64fc35c24d32d53bf56231 -2026-07-29-projected-token-usage-and-request-context.zh.md: 6d2bb624ac11dbcdac30695913d3c16513bfdf9e +2026-07-29-projected-token-usage-and-request-context.md: f96257243bef91ff6a73418231de5e777d8edb2e +2026-07-29-projected-token-usage-and-request-context.zh.md: 7365d5d816f9f9b324f3e3d3b4db0d3346851bdf diff --git a/.agents/notes/implemented/architecture/2026-07-29-projected-token-usage-and-request-context.md b/.agents/notes/implemented/architecture/2026-07-29-projected-token-usage-and-request-context.md index d62f7dccd5..f96257243b 100644 --- a/.agents/notes/implemented/architecture/2026-07-29-projected-token-usage-and-request-context.md +++ b/.agents/notes/implemented/architecture/2026-07-29-projected-token-usage-and-request-context.md @@ -26,7 +26,7 @@ Capacity deliberately stays out of `EpochHeader`. That type is the reconstructio Both units ride the standard projection lifecycle: history tail baselines, `session/projection` live frames, higher-seq-wins client storage, JSON checkpoints, cache recovery, and unit unload. There is no token-specific history field, mux frame, projector, revision counter, or client fence. -The Web `StatsLine` reads both through the standard `useProjection` seat. Window nodes still supply turn and step counts plus LLM and tool wall times — those answer "what is on screen" and are correctly window-scoped. Durable token and context groups remain when compaction leaves no visible assistant step. Cache writes count in billed input and in the cache-hit denominator. A deployment without token-meter drops the token groups; occupancy stays hidden until both pressure and capacity are known. The exact-overflow tooltip mounts its measuring child only for a non-empty line and retains one `ResizeObserver` while values change; text changes perform one direct measurement without replacing the observer. +The Web [`StatsPills`](../feature/2026-09-07-composer-session-stats-pills.md) reads both through the standard `useProjection` seat. Window nodes still supply turn and step counts plus LLM and tool wall times as the no-projection fallback — those answer "what is on screen" and are correctly window-scoped. The durable usage pill remains when compaction leaves no visible assistant step. Cache writes count in billed input and in the cache-hit denominator. A deployment without token-meter drops the usage pill; context occupancy lives on the composer's ContextMeter ring. Exact token figures show in the usage pill's click-open dialog rather than a hover tooltip. ## Context occupancy is approximate, and that is the decision @@ -48,7 +48,7 @@ That cost bought a worse display: occupancy went blank after every reconnect and **Resolve capacity inside token-meter.** The package documents itself as independent of model routing and is otherwise a pure reader that never appends to the log. AgentLoop already holds the resolved metadata where the header is written. -**Extend the `session.models` RPC with capacity.** The handler already resolves and discards it, so the field is nearly free — but `StatsLine` lives in `ui-conversation` while the model directory lives in `ui-model-selection`, and `ui-conversation` cannot depend on `ui-model-selection`. Delivering it would have required either a second dock entry splitting one text row across two plugins, or a cross-plugin store write. +**Extend the `session.models` RPC with capacity.** The handler already resolves and discards it, so the field is nearly free — but the stats display (now `StatsPills`, ui-chat) and the model directory live in separate plugins with no dependency between them. Delivering it would have required either a second dock entry splitting one surface across two plugins, or a cross-plugin store write. **Add a context circle beside the model selector.** That placement suggests selected-model state. The stats line carries the figure without a duplicate UI or data path. diff --git a/.agents/notes/implemented/architecture/2026-07-29-projected-token-usage-and-request-context.zh.md b/.agents/notes/implemented/architecture/2026-07-29-projected-token-usage-and-request-context.zh.md index 6d2bb624ac..7365d5d816 100644 --- a/.agents/notes/implemented/architecture/2026-07-29-projected-token-usage-and-request-context.zh.md +++ b/.agents/notes/implemented/architecture/2026-07-29-projected-token-usage-and-request-context.zh.md @@ -26,7 +26,7 @@ token-meter 还拥有在持久事件上运行的共享纯 attempt/Turn fold。 两个单元都沿用标准投影生命周期:历史尾页基线、`session/projection` 实时帧、seq 高者胜的客户端存储、JSON 检查点、缓存恢复和单元卸载。系统没有任何 token 专用的历史字段、mux 帧、投影器、修订计数器或客户端栅栏。 -Web `StatsLine` 通过标准 `useProjection` 席位读取两者。窗口内节点仍提供轮次和步骤计数,以及 LLM(大语言模型)与工具的墙钟时间:它们回答的是「屏幕上有什么」,按窗口作用域正是正确的。压缩使可见 assistant 步骤归零后,持久 token 与上下文分组仍会保留。缓存写入会计入计费输入和缓存命中率分母。未部署 token-meter 时会去掉 token 分组;只有压力与容量都已知时才显示占用率。精确 overflow tooltip 只在统计行非空时挂载测量子组件,并在值变化期间保留同一个 `ResizeObserver`;文本变化只直接测量一次,不替换 observer。 +Web [`StatsPills`](../feature/2026-09-07-composer-session-stats-pills.zh.md) 通过标准 `useProjection` 席位读取两者。窗口内节点仍作为无投影回退提供轮次和步骤计数,以及 LLM(大语言模型)与工具的墙钟时间:它们回答的是「屏幕上有什么」,按窗口作用域正是正确的。压缩使可见 assistant 步骤归零后,持久用量 pill 仍会保留。缓存写入会计入计费输入和缓存命中率分母。未部署 token-meter 时会去掉用量 pill;上下文占用率由输入框旁的 ContextMeter 圆环承载。精确 token 数字显示在用量 pill 点击展开的弹层里,而非悬停提示。 ## 上下文占用率是近似值,而这正是决策本身 @@ -48,7 +48,7 @@ Web `StatsLine` 通过标准 `useProjection` 席位读取两者。窗口内节 **在 token-meter 内部解析容量。** 该包自述与模型路由无关,且在其他方面是一个从不向日志追加内容的纯读取方。AgentLoop 在写入请求头的位置已经持有已解析的元数据。 -**为 `session.models` RPC 增加容量字段。** 其处理器已经解析出容量又将其丢弃,因此这个字段几乎是免费的;但 `StatsLine` 位于 `ui-conversation`,模型目录位于 `ui-model-selection`,而 `ui-conversation` 不能依赖 `ui-model-selection`。要送达它,就得增加第二个 dock 条目、把一行文本拆到两个插件里,或者做一次跨插件的 store 写入。 +**为 `session.models` RPC 增加容量字段。** 其处理器已经解析出容量又将其丢弃,因此这个字段几乎是免费的;但统计展示(现为 `StatsPills`,ui-chat)与模型目录位于两个互不依赖的插件。要送达它,就得增加第二个 dock 条目把一个表面拆到两个插件里,或者做一次跨插件的 store 写入。 **在模型选择器旁增加上下文圆环。** 该位置会让人以为这是所选模型的状态。统计行可以承载该数字,无需引入重复的 UI 或数据路径。 diff --git a/.agents/notes/implemented/architecture/2026-08-09-client-conversation-node-assembly.i18n.yaml b/.agents/notes/implemented/architecture/2026-08-09-client-conversation-node-assembly.i18n.yaml index eee0bed2ef..e919d1145b 100644 --- a/.agents/notes/implemented/architecture/2026-08-09-client-conversation-node-assembly.i18n.yaml +++ b/.agents/notes/implemented/architecture/2026-08-09-client-conversation-node-assembly.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-08-09-client-conversation-node-assembly.md -2026-08-09-client-conversation-node-assembly.md: 50e11b04d6461e99cdddb8e5e2d94bf864228841 -2026-08-09-client-conversation-node-assembly.zh.md: 7be1cd1b7f356caf51b8003821369771b0a6e269 +2026-08-09-client-conversation-node-assembly.md: 8fcd0350db6ea8e30817995781c75f73db562033 +2026-08-09-client-conversation-node-assembly.zh.md: 32af13badb7e38a5a62cfbf80f3c7489decdc0aa diff --git a/.agents/notes/implemented/architecture/2026-08-09-client-conversation-node-assembly.md b/.agents/notes/implemented/architecture/2026-08-09-client-conversation-node-assembly.md index 50e11b04d6..8fcd0350db 100644 --- a/.agents/notes/implemented/architecture/2026-08-09-client-conversation-node-assembly.md +++ b/.agents/notes/implemented/architecture/2026-08-09-client-conversation-node-assembly.md @@ -315,7 +315,7 @@ The shell synchronously resolves the persisted selection when a Session binding Ordinary prepend and append flushes call `apply({ upserts, timeline })` only for active targets. Complete window replacement and Registry rebuild call `replace()` only for active targets. Unsubscription does not remove a target, so returning to an opened View does not rebuild it. -[`ChatSnapshotBuilder`](../../../../packages/client/ui-chat/src/client/conversation-nodes/chat-snapshot-builder.ts) maintains `order`, a keyed `nodes` store with identity-stable Node and Turn-process sources, the turn/step `locations` index, `timeline`, and the `legacy` slice used by StatsLine and mirrored into top-level public compatibility fields. +[`ChatSnapshotBuilder`](../../../../packages/client/ui-chat/src/client/conversation-nodes/chat-snapshot-builder.ts) maintains `order`, a keyed `nodes` store with identity-stable Node and Turn-process sources, the turn/step `locations` index, `timeline`, and the `legacy` slice used by StatsPills and mirrored into top-level public compatibility fields. Only a new key or a change to `anchorSeq`, visibility, or Location identity makes a Chat update structural. An ordinary content change does not rebuild `order`; the keyed Node store replaces that key's value and publishes only its source. The Turn-process projector recalculates cross-Node presentation only for a Turn whose structure, specification, or status changed, then publishes only that Turn's process sources. @@ -424,4 +424,4 @@ Inbox Context retention grows with splice count and claimed message count rather The cost is new Runtime contracts for Registry, Assembler, Location data, dependency replay, and per-target Builders, plus parent-owned common inject and per-occurrence `hookContext` in UI Slots. Definitions that consume Assistant deltas also maintain equivalent scalar and packed update branches. Definition authors must understand stable IDs, unique scalar starts, forward replay, Step→Turn publication order, read-only Reader access, and the prohibition on Node withdrawal. -`useTurnData()` does not revoke the standard `useSession` capability from session-scoped renderers, so this boundary relies on API guidance and tests rather than capability isolation. Registry changes remain low-frequency full rebuilds; the Chat Builder still maintains a legacy slice for StatsLine and the top-level public fields, while Trajectory owns target-specific Definitions and a Builder over the shared Session window. Built-in Definitions remain in their respective UI packages, and these compatibility boundaries do not return business interpretation to Session. +`useTurnData()` does not revoke the standard `useSession` capability from session-scoped renderers, so this boundary relies on API guidance and tests rather than capability isolation. Registry changes remain low-frequency full rebuilds; the Chat Builder still maintains a legacy slice for StatsPills and the top-level public fields, while Trajectory owns target-specific Definitions and a Builder over the shared Session window. Built-in Definitions remain in their respective UI packages, and these compatibility boundaries do not return business interpretation to Session. diff --git a/.agents/notes/implemented/architecture/2026-08-09-client-conversation-node-assembly.zh.md b/.agents/notes/implemented/architecture/2026-08-09-client-conversation-node-assembly.zh.md index 7be1cd1b7f..32af13badb 100644 --- a/.agents/notes/implemented/architecture/2026-08-09-client-conversation-node-assembly.zh.md +++ b/.agents/notes/implemented/architecture/2026-08-09-client-conversation-node-assembly.zh.md @@ -315,7 +315,7 @@ Session binding 可用、缓存的 binding 成为 current 或 View roster 变化 普通 prepend 与 append flush 只对 active target 调用 `apply({ upserts, timeline })`。完整 window replace 与 Registry rebuild 只对 active target 调用 `replace()`。取消订阅不会移除 target,因此返回已打开的 View 不会重建。 -[`ChatSnapshotBuilder`](../../../../packages/client/ui-chat/src/client/conversation-nodes/chat-snapshot-builder.ts) 维护 `order`、带身份稳定 Node 与 Turn-process source 的 keyed `nodes` store、turn/step `locations` index、`timeline`,以及由 StatsLine 使用并镜像到顶层公共兼容字段的 `legacy` slice。 +[`ChatSnapshotBuilder`](../../../../packages/client/ui-chat/src/client/conversation-nodes/chat-snapshot-builder.ts) 维护 `order`、带身份稳定 Node 与 Turn-process source 的 keyed `nodes` store、turn/step `locations` index、`timeline`,以及由 StatsPills 使用并镜像到顶层公共兼容字段的 `legacy` slice。 Chat 结构变化只由新 key、`anchorSeq`、visibility 或 Location identity 变化触发。普通内容变化不重建 `order`;keyed Node store 只替换该 key 的 value 并发布其 source。Turn-process projector 仅为结构、规格或状态发生变化的 Turn 重算跨 Node 呈现,再只发布该 Turn 的 process source。 @@ -424,4 +424,4 @@ Inbox Context 的保留量随 splice 数和已 claim 消息数增长,不再随 代价是 Runtime 新增 Registry、Assembler、Location data、依赖重放和 per-target Builder 契约,UI Slots 也新增 parent-owned common inject 与 per-occurrence `hookContext`。消费 Assistant delta 的 Definition 还需要维护等价的 scalar 与 packed update 分支。Definition 作者必须理解稳定 ID、唯一 scalar start、正序 replay、Step→Turn 发布顺序、只读 Reader 和 Node 不撤回规则。 -`useTurnData()` 不撤销 session-scoped renderer 的标准 `useSession`,因此该边界依靠 API 引导和测试,而不是能力隔离。Registry 变化仍是低频完整 rebuild;Chat Builder 继续为 StatsLine 和顶层公共字段维护 legacy slice,Trajectory 则在共享 Session 窗口上拥有 target 专属 Definition 与 Builder。内建 Definition 分别留在所属 UI package;这些兼容边界不把业务解释权交还给 Session。 +`useTurnData()` 不撤销 session-scoped renderer 的标准 `useSession`,因此该边界依靠 API 引导和测试,而不是能力隔离。Registry 变化仍是低频完整 rebuild;Chat Builder 继续为 StatsPills 和顶层公共字段维护 legacy slice,Trajectory 则在共享 Session 窗口上拥有 target 专属 Definition 与 Builder。内建 Definition 分别留在所属 UI package;这些兼容边界不把业务解释权交还给 Session。 diff --git a/.agents/notes/implemented/bug-fix/2026-08-12-full-session-turn-step-counts.i18n.yaml b/.agents/notes/implemented/bug-fix/2026-08-12-full-session-turn-step-counts.i18n.yaml index 18ed791f68..cc61d6bc28 100644 --- a/.agents/notes/implemented/bug-fix/2026-08-12-full-session-turn-step-counts.i18n.yaml +++ b/.agents/notes/implemented/bug-fix/2026-08-12-full-session-turn-step-counts.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write .agents/notes/implemented/bug-fix/2026-08-12-full-session-turn-step-counts.md -2026-08-12-full-session-turn-step-counts.md: ecfa00dc3e24101953a9d5a724dba17682d839bd -2026-08-12-full-session-turn-step-counts.zh.md: 85e2a26b5af51296e20f29af49e909c6e182ea05 +2026-08-12-full-session-turn-step-counts.md: a87fb48bd17992b19c7c5ff89ba5ac4e6df89556 +2026-08-12-full-session-turn-step-counts.zh.md: 21e90a0cbb370e3bc566d265d54c5d64cb6189dd diff --git a/.agents/notes/implemented/bug-fix/2026-08-12-full-session-turn-step-counts.md b/.agents/notes/implemented/bug-fix/2026-08-12-full-session-turn-step-counts.md index ecfa00dc3e..a87fb48bd1 100644 --- a/.agents/notes/implemented/bug-fix/2026-08-12-full-session-turn-step-counts.md +++ b/.agents/notes/implemented/bug-fix/2026-08-12-full-session-turn-step-counts.md @@ -10,7 +10,7 @@ The web chat stats strip folded `StatsLine`'s loaded conversation window (`deriv ## Decision -A new function plugin `@deepseek-ai/dsh-session-stats` registers a `sessionStats` projection unit on `ctx.sessionProjections`, mounted as a web-app bundle row. The value carries the strip's whole non-token figure set — `{ turns, steps, llmMs, toolMs, ttftMs, ttftSteps, decodeMs, decodeTokens }`, field names mirroring the window fold so the two swap wholesale. `steps` counts `step/end` events and `turns` counts distinct turns carrying at least one (turn numbers are monotonic, so one `lastTurn` slot suffices); `llmMs` sums `step/start` → `assistant/message`; TTFT records the first non-empty delta chunk per step (surviving in-step `llm/retry`, the window `resetForRetry` parity); decode spans first token → assembled message on usage-reporting steps; `toolMs` pairs `tool/call` → `tool/result` by callId with unresolved calls dropped at `turn/end`. The first-token predicate `isTokenDelta` moved to `@deepseek-ai/dsh-llm/message` (beside the `StreamChunk` type it discriminates) so the host fold and the client timing index share one implementation; client-runtime re-exports it. Delivery is entirely the existing projection seam — history tail-page block, `session/projection` push frames, list rows — with zero changes to apiproxy, wire schemas, or the client runtime. `StatsLine` reads `useProjection('sessionStats')` and falls back to the window fold when the key is undefined (an assembly without the unit). The client connection fixture mirrors the fold as `sessionStatsOf` under its existing every-composed-key discipline. +A new function plugin `@deepseek-ai/dsh-session-stats` registers a `sessionStats` projection unit on `ctx.sessionProjections`, mounted as a web-app bundle row. The value carries the strip's whole non-token figure set — `{ turns, steps, llmMs, toolMs, ttftMs, ttftSteps, decodeMs, decodeTokens }`, field names mirroring the window fold so the two swap wholesale. `steps` counts `step/end` events and `turns` counts distinct turns carrying at least one (turn numbers are monotonic, so one `lastTurn` slot suffices); `llmMs` sums `step/start` → `assistant/message`; TTFT records the first non-empty delta chunk per step (surviving in-step `llm/retry`, the window `resetForRetry` parity); decode spans first token → assembled message on usage-reporting steps; `toolMs` pairs `tool/call` → `tool/result` by callId with unresolved calls dropped at `turn/end`. The first-token predicate `isTokenDelta` moved to `@deepseek-ai/dsh-llm/message` (beside the `StreamChunk` type it discriminates) so the host fold and the client timing index share one implementation; client-runtime re-exports it. Delivery is entirely the existing projection seam — history tail-page block, `session/projection` push frames, list rows — with zero changes to apiproxy, wire schemas, or the client runtime. `StatsPills` reads `useProjection('sessionStats')` and falls back to the window fold when the key is undefined (an assembly without the unit). The client connection fixture mirrors the fold as `sessionStatsOf` under its existing every-composed-key discipline. `step/end` — not `assistant/message` — is the counted event, for two correctness reasons found while reviewing the obvious message-counting design: diff --git a/.agents/notes/implemented/bug-fix/2026-08-12-full-session-turn-step-counts.zh.md b/.agents/notes/implemented/bug-fix/2026-08-12-full-session-turn-step-counts.zh.md index 85e2a26b5a..21e90a0cbb 100644 --- a/.agents/notes/implemented/bug-fix/2026-08-12-full-session-turn-step-counts.zh.md +++ b/.agents/notes/implemented/bug-fix/2026-08-12-full-session-turn-step-counts.zh.md @@ -10,7 +10,7 @@ Web 聊天统计条的每个非 token 数字都折算自 `StatsLine` 已加载 ## 决定 -新的函数插件 `@deepseek-ai/dsh-session-stats` 在 `ctx.sessionProjections` 上注册 `sessionStats` 投影单元,作为 web-app bundle 行挂载。值携带统计条完整的非 token 数字集——`{ turns, steps, llmMs, toolMs, ttftMs, ttftSteps, decodeMs, decodeTokens }`,字段名与窗口折叠一一对应以便整体互换。`steps` 统计 `step/end` 事件,`turns` 统计含至少一条该事件的不同 turn(turn 号单调递增,一个 `lastTurn` 槽即可);`llmMs` 累加 `step/start` → `assistant/message`;TTFT 记录每步首个非空 delta chunk(在步内 `llm/retry` 后保留,与窗口 `resetForRetry` 对齐);解码时长覆盖首 token → 已组装消息、仅统计上报 usage 的步;`toolMs` 按 callId 配对 `tool/call` → `tool/result`,未解决的调用在 `turn/end` 时丢弃。首 token 谓词 `isTokenDelta` 移入 `@deepseek-ai/dsh-llm/message`(与其判别的 `StreamChunk` 类型同处),Host 折叠与客户端计时索引共用同一实现;client-runtime 转发导出。投递完全复用现有投影缝——history 尾页块、`session/projection` 推送帧、列表行——apiproxy、wire schema 与客户端运行时零改动。`StatsLine` 读取 `useProjection('sessionStats')`,键为 undefined(未组合该单元的装配)时整体回退到窗口折叠。客户端 connection fixture 按其「镜像每个已组合键」的既有纪律以 `sessionStatsOf` 平行实现该折叠。 +新的函数插件 `@deepseek-ai/dsh-session-stats` 在 `ctx.sessionProjections` 上注册 `sessionStats` 投影单元,作为 web-app bundle 行挂载。值携带统计条完整的非 token 数字集——`{ turns, steps, llmMs, toolMs, ttftMs, ttftSteps, decodeMs, decodeTokens }`,字段名与窗口折叠一一对应以便整体互换。`steps` 统计 `step/end` 事件,`turns` 统计含至少一条该事件的不同 turn(turn 号单调递增,一个 `lastTurn` 槽即可);`llmMs` 累加 `step/start` → `assistant/message`;TTFT 记录每步首个非空 delta chunk(在步内 `llm/retry` 后保留,与窗口 `resetForRetry` 对齐);解码时长覆盖首 token → 已组装消息、仅统计上报 usage 的步;`toolMs` 按 callId 配对 `tool/call` → `tool/result`,未解决的调用在 `turn/end` 时丢弃。首 token 谓词 `isTokenDelta` 移入 `@deepseek-ai/dsh-llm/message`(与其判别的 `StreamChunk` 类型同处),Host 折叠与客户端计时索引共用同一实现;client-runtime 转发导出。投递完全复用现有投影缝——history 尾页块、`session/projection` 推送帧、列表行——apiproxy、wire schema 与客户端运行时零改动。`StatsPills` 读取 `useProjection('sessionStats')`,键为 undefined(未组合该单元的装配)时整体回退到窗口折叠。客户端 connection fixture 按其「镜像每个已组合键」的既有纪律以 `sessionStatsOf` 平行实现该折叠。 计数事件选 `step/end` 而非 `assistant/message`,源于评审直觉方案(按消息计数)时发现的两个正确性问题: diff --git a/.agents/notes/implemented/feature/2026-08-18-conversation-adaptive-content-width.i18n.yaml b/.agents/notes/implemented/feature/2026-08-18-conversation-adaptive-content-width.i18n.yaml index 3235d96b54..7546cce6a7 100644 --- a/.agents/notes/implemented/feature/2026-08-18-conversation-adaptive-content-width.i18n.yaml +++ b/.agents/notes/implemented/feature/2026-08-18-conversation-adaptive-content-width.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-08-18-conversation-adaptive-content-width.md -2026-08-18-conversation-adaptive-content-width.md: 9b0e7afbded296b60b7c283030a537321b36684e -2026-08-18-conversation-adaptive-content-width.zh.md: f1f424cf9d4ac087518e9f79a365d0052069b999 +2026-08-18-conversation-adaptive-content-width.md: 50de28464181e97d434db9601053e690b36e5746 +2026-08-18-conversation-adaptive-content-width.zh.md: 6d9627060d9037c4bb5e3587f80ac62620fe2da9 diff --git a/.agents/notes/implemented/feature/2026-08-18-conversation-adaptive-content-width.md b/.agents/notes/implemented/feature/2026-08-18-conversation-adaptive-content-width.md index 9b0e7afbde..50de284641 100644 --- a/.agents/notes/implemented/feature/2026-08-18-conversation-adaptive-content-width.md +++ b/.agents/notes/implemented/feature/2026-08-18-conversation-adaptive-content-width.md @@ -6,7 +6,7 @@ English | [中文](2026-08-18-conversation-adaptive-content-width.zh.md) ## Problem -The conversation column's shared width axis (`--dsh-chat-content-width`) was the fixed figma constant 748px. On wide monitors (a 4000px display leaves a ~3500px column) the transcript occupied under a quarter of the column with dead margins on both sides. Every derived surface — the input card (W + 32px), dock cards, takeover panels, StatsLine, the back-to-bottom padding formula — rides this one variable, so any change had to keep the whole column's alignment relations intact. Alongside the adaptive default, users asked for direct control: hover the transcript's side margins to get a col-resize cursor and drag either edge, with both edges moving symmetrically. +The conversation column's shared width axis (`--dsh-chat-content-width`) was the fixed figma constant 748px. On wide monitors (a 4000px display leaves a ~3500px column) the transcript occupied under a quarter of the column with dead margins on both sides. Every derived surface — the input card (W + 32px), dock cards, takeover panels, the stats strip, the back-to-bottom padding formula — rides this one variable, so any change had to keep the whole column's alignment relations intact. Alongside the adaptive default, users asked for direct control: hover the transcript's side margins to get a col-resize cursor and drag either edge, with both edges moving symmetrically. ## Decision diff --git a/.agents/notes/implemented/feature/2026-08-18-conversation-adaptive-content-width.zh.md b/.agents/notes/implemented/feature/2026-08-18-conversation-adaptive-content-width.zh.md index f1f424cf9d..6d9627060d 100644 --- a/.agents/notes/implemented/feature/2026-08-18-conversation-adaptive-content-width.zh.md +++ b/.agents/notes/implemented/feature/2026-08-18-conversation-adaptive-content-width.zh.md @@ -6,7 +6,7 @@ Status: implemented ## 问题 -会话列的共享宽度轴(`--dsh-chat-content-width`)是 figma 定值 748px。在宽显示器上(4000px 屏幕的会话列约 3500px)正文只占列宽不到四分之一,两侧是大片空白边距。所有派生表面——输入卡(W + 32px)、dock 卡片、takeover 面板、StatsLine、回底按钮的 padding 公式——都由这一个变量推导,任何改动都必须保持整列的对齐关系。在自适应默认值之外,用户还要求直接控制:hover 正文两侧边距出现 col-resize 光标,拖任一侧、两侧对称联动。 +会话列的共享宽度轴(`--dsh-chat-content-width`)是 figma 定值 748px。在宽显示器上(4000px 屏幕的会话列约 3500px)正文只占列宽不到四分之一,两侧是大片空白边距。所有派生表面——输入卡(W + 32px)、dock 卡片、takeover 面板、统计条、回底按钮的 padding 公式——都由这一个变量推导,任何改动都必须保持整列的对齐关系。在自适应默认值之外,用户还要求直接控制:hover 正文两侧边距出现 col-resize 光标,拖任一侧、两侧对称联动。 ## 决策 diff --git a/.agents/notes/implemented/feature/2026-08-18-settings-font-size-control.i18n.yaml b/.agents/notes/implemented/feature/2026-08-18-settings-font-size-control.i18n.yaml index f0999e1aa3..04f90225a6 100644 --- a/.agents/notes/implemented/feature/2026-08-18-settings-font-size-control.i18n.yaml +++ b/.agents/notes/implemented/feature/2026-08-18-settings-font-size-control.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-08-18-settings-font-size-control.md -2026-08-18-settings-font-size-control.md: 778893a2c0943f1d3af8905a27efb526071e073b -2026-08-18-settings-font-size-control.zh.md: 71ff3b556622e7091891f8e18f5eefcbda2b0851 +2026-08-18-settings-font-size-control.md: 22eb3d42405a696610795ac1d863258c58512164 +2026-08-18-settings-font-size-control.zh.md: c90b46c10b7fcaa95859c3ec40d52dedadcf8351 diff --git a/.agents/notes/implemented/feature/2026-08-18-settings-font-size-control.md b/.agents/notes/implemented/feature/2026-08-18-settings-font-size-control.md index 778893a2c0..22eb3d4240 100644 --- a/.agents/notes/implemented/feature/2026-08-18-settings-font-size-control.md +++ b/.agents/notes/implemented/feature/2026-08-18-settings-font-size-control.md @@ -14,7 +14,7 @@ The conversation's body text size was fixed (14px after the 0.875 markdown-ladde **Presentation rides the existing snapshot pipeline.** The service never touches the DOM: ui-layout's `ThemePresenter` writes `--dsh-content-font-size` on `body` from each snapshot (and retracts it on dispose), and the Host boot script embeds the durable value in the index response so first paint uses the chosen size — the same pre-plugin path the dark-mode attribute takes, avoiding a font-size flash. -**One CSS delta variable moves the ladder, plus a derived secondary tier.** `gradient-shadow-text.css` derives `--dsh-content-font-delta: calc(var(--dsh-content-font-size, 14px) - 14px)` and shifts the markdown h1–h4 and base variants (size and line height) by that same px increment, preserving the heading hierarchy and each variant's leading. Secondary-tier text — one step under the body — reads `--dsh-content-font-size-secondary: min(setting − 1px, max(13px, setting − 2px))`: setting −1 at ≤14, setting −2 above (12→11, 13→12, 14→13, 15→13, 16→14, 17→15), with `--dsh-content-font-delta-secondary` (secondary − 13px) moving its line heights. The secondary tier covers the markdown table variants, the shared DisclosureRow title (tool calls, think, commands), ToolRow/bash-row summaries and file links, think text, compaction/context/retry/error rows, StatsLine, the chat hint and open-error strips, the workflow-run panel headers/counts/statuses, the turn-status clock, reference summaries, and the note-trigger label. Small and code variants stay fixed — as does the interrupted-turn `.stopped` tag (11px): they are dense secondary text whose defaults would fall below legibility when stepped down. Consumers outside the token ladder at the body size read `var(--dsh-content-font-size, 14px)` and `calc( + var(--dsh-content-font-delta, 0px))` directly: the assistant narration root, the user bubble (inline reference glyphs included), the composer card (whose textarea/mirror/backdrop stack inherits font metrics from the card by design), the compaction/DisclosureRow geometry (row height, leading box, expanded bodies' `22px + delta` indent), the message clock and icon actions (slot-injected message-feedback actions match through the same variables), and the turn status line. Flow icons scale through each leading box's CSS edge (`svg` width/height overriding the glyph attributes); StateDot is exempt via its `data-state` attribute — a status mark, not text furniture. The fallbacks (14px body, 13px secondary) keep every surface pixel-identical when the variables are absent (tests, storybook-like mounts, remote compositions before adoption). +**One CSS delta variable moves the ladder, plus a derived secondary tier.** `gradient-shadow-text.css` derives `--dsh-content-font-delta: calc(var(--dsh-content-font-size, 14px) - 14px)` and shifts the markdown h1–h4 and base variants (size and line height) by that same px increment, preserving the heading hierarchy and each variant's leading. Secondary-tier text — one step under the body — reads `--dsh-content-font-size-secondary: min(setting − 1px, max(13px, setting − 2px))`: setting −1 at ≤14, setting −2 above (12→11, 13→12, 14→13, 15→13, 16→14, 17→15), with `--dsh-content-font-delta-secondary` (secondary − 13px) moving its line heights. The secondary tier covers the markdown table variants, the shared DisclosureRow title (tool calls, think, commands), ToolRow/bash-row summaries and file links, think text, compaction/context/retry/error rows, StatsPills, the chat hint and open-error strips, the workflow-run panel headers/counts/statuses, the turn-status clock, reference summaries, and the note-trigger label. Small and code variants stay fixed — as does the interrupted-turn `.stopped` tag (11px): they are dense secondary text whose defaults would fall below legibility when stepped down. Consumers outside the token ladder at the body size read `var(--dsh-content-font-size, 14px)` and `calc( + var(--dsh-content-font-delta, 0px))` directly: the assistant narration root, the user bubble (inline reference glyphs included), the composer card (whose textarea/mirror/backdrop stack inherits font metrics from the card by design), the compaction/DisclosureRow geometry (row height, leading box, expanded bodies' `22px + delta` indent), the message clock and icon actions (slot-injected message-feedback actions match through the same variables), and the turn status line. Flow icons scale through each leading box's CSS edge (`svg` width/height overriding the glyph attributes); StateDot is exempt via its `data-state` attribute — a status mark, not text furniture. The fallbacks (14px body, 13px secondary) keep every surface pixel-identical when the variables are absent (tests, storybook-like mounts, remote compositions before adoption). **The stepper is a pill, not a menu.** The row reuses the selector-pill geometry (h36 r18 module fill) with the value centered in the pill, the up/down arrow column revealed on hover/focus-within and absolutely anchored to the pill's right edge (so revealing never moves the value), and a `px` unit label after the pill. A tertiary description line under the title states the scope — the size only affects conversation content, not the application chrome. Arrows disable at the bounds; the display follows the store mirror, never the click echo — the same store/face pattern as the Appearance row. diff --git a/.agents/notes/implemented/feature/2026-08-18-settings-font-size-control.zh.md b/.agents/notes/implemented/feature/2026-08-18-settings-font-size-control.zh.md index 71ff3b5566..c90b46c10b 100644 --- a/.agents/notes/implemented/feature/2026-08-18-settings-font-size-control.zh.md +++ b/.agents/notes/implemented/feature/2026-08-18-settings-font-size-control.zh.md @@ -14,7 +14,7 @@ **呈现走既有快照管线。**服务绝不接触 DOM:ui-layout 的 `ThemePresenter` 依据每份快照在 `body` 上写 `--dsh-content-font-size`(dispose 时收回),Host 引导脚本把持久化值嵌入 index 响应,让首帧就使用所选字号——与暗色属性同一条插件前路径,避免字号闪变。 -**一个 CSS 增量变量平移阶梯,另派生一个低一档层级。**`gradient-shadow-text.css` 派生 `--dsh-content-font-delta: calc(var(--dsh-content-font-size, 14px) - 14px)`,把 markdown h1–h4 与 base 各变体(字号与行高)按同一像素增量平移,保持标题层级与各变体的行距。低一档文本——比正文低一档——读 `--dsh-content-font-size-secondary: min(设置值 − 1px, max(13px, 设置值 − 2px))`:设置 ≤14 时为设置值 −1,>14 时为设置值 −2(12→11、13→12、14→13、15→13、16→14、17→15),其行高经 `--dsh-content-font-delta-secondary`(低一档值 − 13px)随动。低一档覆盖:markdown 表格变体、共享 DisclosureRow 标题(工具调用、think、命令)、ToolRow/bash 行的 summary 与文件链接、think 正文、compaction/context/retry/错误行、StatsLine、chat 提示与打开失败条、workflow-run 面板的头部/计数/状态、回合状态时钟、引用摘要与笔记触发标签。small 与 code 变体保持固定——中断回合的 `.stopped` 标签(11px)同样固定:它们是密集次级文本,其默认值在字号下调时会低于可读下限。token 阶梯之外按正文档读取的消费方直接使用 `var(--dsh-content-font-size, 14px)` 与 `calc(<默认行高> + var(--dsh-content-font-delta, 0px))`:助手正文根节点、用户气泡(含行内引用字形)、composer 卡片(其 textarea/mirror/backdrop 三层按设计从卡片继承字体度量)、compaction/DisclosureRow 的行几何(行高、leading 盒、展开内容的 `22px + delta` 缩进)、消息时钟与图标操作(slot 注入的消息反馈操作经同一对变量同步缩放),以及回合状态行。流内图标经由各 leading 盒的 CSS 边长缩放(`svg` width/height 覆盖字形自身属性);StateDot 通过其 `data-state` 属性豁免——它是状态标记,不是文字组件。回退值(正文 14px、低一档 13px)让变量缺席时(测试、独立挂载、采纳前的远程组合)所有表面逐像素不变。 +**一个 CSS 增量变量平移阶梯,另派生一个低一档层级。**`gradient-shadow-text.css` 派生 `--dsh-content-font-delta: calc(var(--dsh-content-font-size, 14px) - 14px)`,把 markdown h1–h4 与 base 各变体(字号与行高)按同一像素增量平移,保持标题层级与各变体的行距。低一档文本——比正文低一档——读 `--dsh-content-font-size-secondary: min(设置值 − 1px, max(13px, 设置值 − 2px))`:设置 ≤14 时为设置值 −1,>14 时为设置值 −2(12→11、13→12、14→13、15→13、16→14、17→15),其行高经 `--dsh-content-font-delta-secondary`(低一档值 − 13px)随动。低一档覆盖:markdown 表格变体、共享 DisclosureRow 标题(工具调用、think、命令)、ToolRow/bash 行的 summary 与文件链接、think 正文、compaction/context/retry/错误行、StatsPills、chat 提示与打开失败条、workflow-run 面板的头部/计数/状态、回合状态时钟、引用摘要与笔记触发标签。small 与 code 变体保持固定——中断回合的 `.stopped` 标签(11px)同样固定:它们是密集次级文本,其默认值在字号下调时会低于可读下限。token 阶梯之外按正文档读取的消费方直接使用 `var(--dsh-content-font-size, 14px)` 与 `calc(<默认行高> + var(--dsh-content-font-delta, 0px))`:助手正文根节点、用户气泡(含行内引用字形)、composer 卡片(其 textarea/mirror/backdrop 三层按设计从卡片继承字体度量)、compaction/DisclosureRow 的行几何(行高、leading 盒、展开内容的 `22px + delta` 缩进)、消息时钟与图标操作(slot 注入的消息反馈操作经同一对变量同步缩放),以及回合状态行。流内图标经由各 leading 盒的 CSS 边长缩放(`svg` width/height 覆盖字形自身属性);StateDot 通过其 `data-state` 属性豁免——它是状态标记,不是文字组件。回退值(正文 14px、低一档 13px)让变量缺席时(测试、独立挂载、采纳前的远程组合)所有表面逐像素不变。 **步进器是药丸控件,不是菜单。**该行复用选择器药丸几何(h36 r18 模块填充),数值在药丸内居中,上下箭头列在 hover/focus-within 时显示并绝对定位在药丸右缘(显示时数值不移动),药丸后带 `px` 单位标签。标题下方的三级说明行标明作用范围——字号仅影响会话内容,不影响应用外框。到达边界时对应箭头禁用;显示跟随 store 镜像,绝不跟随点击回声——与外观行相同的 store/face 模式。 diff --git a/.agents/notes/implemented/feature/2026-09-07-composer-session-stats-pills.i18n.yaml b/.agents/notes/implemented/feature/2026-09-07-composer-session-stats-pills.i18n.yaml new file mode 100644 index 0000000000..01ce7964cd --- /dev/null +++ b/.agents/notes/implemented/feature/2026-09-07-composer-session-stats-pills.i18n.yaml @@ -0,0 +1,6 @@ +# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each +# side as of the last confirmed-consistent state. Both languages carry equal authority; +# after editing either side, bring the other along and re-record with: +# pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-09-07-composer-session-stats-pills.md +2026-09-07-composer-session-stats-pills.md: 577da02cc53e0df8577909d20e12be240cdd4eb5 +2026-09-07-composer-session-stats-pills.zh.md: 0c14060c458ffedd62333625553c63fadc7f417f diff --git a/.agents/notes/implemented/feature/2026-09-07-composer-session-stats-pills.md b/.agents/notes/implemented/feature/2026-09-07-composer-session-stats-pills.md new file mode 100644 index 0000000000..577da02cc5 --- /dev/null +++ b/.agents/notes/implemented/feature/2026-09-07-composer-session-stats-pills.md @@ -0,0 +1,32 @@ +# Agent Note: Composer session stats — two icon pills with click-open stat dialogs + +Status: implemented + +English | [中文](2026-09-07-composer-session-stats-pills.zh.md) + +## Problem + +The session stats strip under the composer (`StatsLine`, ui-chat, mounted on `conversation.composer.dock`) rendered every figure as one resident text line: turn/step counts, LLM and tool wall times, TTFT/TPS averages, token totals with cache-hit share, and context occupancy. The line crowded as figures accumulated, exact token values hid behind a hover tooltip with its own `ResizeObserver` overflow measurement, and the flat text gave no grouping — time figures and billing figures read as one undifferentiated row. An in-page A/B against a two-pill variant settled the direction: the pills won on scannability and on giving each figure family a home. + +## Decision + +`StatsPills` (packages/client/ui-chat/src/client/chat/StatsPills.tsx) replaces `StatsLine` on the same `conversation.composer.dock` slot; the losing variant is deleted, its shared helpers (`deriveStats`, `formatDuration`, `cacheHitPercent`, `billedInputTokens`) absorbed into the new module, and the dead `stats.elapsed` / `stats.tps` locale keys removed. + +- **Two icon pills, two dialogs.** A gauge pill (new `IconGaugeOutline16`, dial center optically dropped to y=8.75 because the bottom-open arc reads high) shows `{turns} 轮 {steps} 步` plus output TPS and click-opens the 会话统计 dialog (LLM time, tool time, average TTFT, TPS). A database pill (`IconDatabaseOutline16`) shows the compact billed total plus cache-hit share and click-opens the Token 用量 dialog (cache hit, uncached input, cache read, cache write, output — exact counts). Both dialogs wear the shared `stat-dialog` module (portal panel, anchored placement, outside-dismiss) extracted for exactly this two-consumer split. +- **Data sourcing is unchanged in architecture.** Counts and times prefer the durable `sessionStats` projection with the window fold as the assembly-without-the-unit fallback ([whole-session counts](../bug-fix/2026-08-12-full-session-turn-step-counts.md)); token figures ride `tokenUsage` only, so an absent projection drops the usage pill rather than showing window-derived billing. Cache writes stay in the billed total and the cache-hit denominator ([projection decision](../architecture/2026-07-29-projected-token-usage-and-request-context.md)). Context occupancy left the strip entirely — the composer's ContextMeter ring owns it. +- **Render discipline.** The row folds settled nodes only (`chat.legacy.nodes` identity), so streaming chunk frames cause zero rerenders — pinned by a render-count unit test. A session with no closed step and no billed tokens renders nothing. +- **`data-composer-stats` is a cross-package attribute contract.** The pills' root carries it; ui-conversation's `InputBar.module.css` `:has([data-composer-stats])` rule tightens the composer's bottom clearance to 4px when the row is mounted. The producer side pins the attribute in unit tests, following the `data-trigger-menu` precedent. + +## Alternatives considered + +- **The single-line variant (StatsLine, the A/B loser).** All figures resident in one text row with a hover tooltip for exact totals. Lost on crowding and discoverability: the tooltip hid exact values behind an unadvertised hover, and one row gave time and billing figures no visual grouping. +- **Three resident groups with one shared dialog.** An intermediate iteration kept counts, time, and tokens as three inline groups. Two pills won because the time/usage split matches the two underlying projections one-to-one and each pill's icon telegraphs its dialog. +- **Keeping context occupancy in the strip.** Rejected: the ContextMeter ring beside the composer already shows occupancy continuously; repeating it in the strip spent width on a duplicate. +- **Extracting the dl bucket rows shared with `TurnUsagePanel`.** The session-total dialog and the per-turn panel render the same skin but different contracts (all buckets always present vs optional per-turn fields plus model routes); a shared component would be conditionals around nine lines. The mirror is marked `jscpd:ignore` with the reason inline. + +## Consequences + +- `ChatSnapshotBuilder`'s legacy slice now serves StatsPills; the [node-assembly note](../architecture/2026-08-09-client-conversation-node-assembly.md) tracks that consumer rename. +- Exact figures cost a click instead of a hover; the strip itself carries only the two headline readings. +- Web e2e strip assertions match substring text inside the time pill; the paged-history scenario's aria golden pins the two-pill structure. +- The `conversation.composer.dock` occupant in the generated slot catalog is `client-ui-chat StatsPills id 'stats'`. diff --git a/.agents/notes/implemented/feature/2026-09-07-composer-session-stats-pills.zh.md b/.agents/notes/implemented/feature/2026-09-07-composer-session-stats-pills.zh.md new file mode 100644 index 0000000000..0c14060c45 --- /dev/null +++ b/.agents/notes/implemented/feature/2026-09-07-composer-session-stats-pills.zh.md @@ -0,0 +1,32 @@ +# Agent Note: 输入框下的会话统计 —— 双图标 pill 与点击展开的统计弹层 + +Status: implemented + +[English](2026-09-07-composer-session-stats-pills.md) | 中文 + +## 问题 + +输入框下方的会话统计条(`StatsLine`,ui-chat,挂载于 `conversation.composer.dock`)把所有数字渲染成一整行常驻文本:轮/步计数、模型与工具用时、TTFT/TPS 均值、token 总量与缓存命中率、上下文占用。数字越多行越拥挤,精确 token 值藏在带独立 `ResizeObserver` 溢出测量的悬停提示里,纯文本也没有分组——时间类数字和计费类数字读起来混作一行。一次页面内 A/B 对比双 pill 变体后方向定案:pill 方案在可扫读性和"每类数字有归属"上胜出。 + +## 决定 + +`StatsPills`(packages/client/ui-chat/src/client/chat/StatsPills.tsx)在同一 `conversation.composer.dock` 插槽上取代 `StatsLine`;落选变体已删除,其共享工具函数(`deriveStats`、`formatDuration`、`cacheHitPercent`、`billedInputTokens`)并入新模块,废弃的 `stats.elapsed` / `stats.tps` 文案键一并移除。 + +- **两个图标 pill、两个弹层。** 仪表盘 pill(新增 `IconGaugeOutline16`,因下开口圆弧视觉偏高而把表盘中心光学下移到 y=8.75)展示 `{turns} 轮 {steps} 步` 加输出 TPS,点击打开「会话统计」弹层(模型用时、工具调用用时、首 token 平均、输出速度)。数据库 pill(`IconDatabaseOutline16`)展示紧凑计费总量加缓存命中率,点击打开「Token 用量」弹层(缓存命中、未缓存输入、缓存读取、缓存写入、输出——精确计数)。两个弹层共用为这两处消费者抽出的 `stat-dialog` 模块(portal 面板、锚定定位、点击外部关闭)。 +- **数据来源架构不变。** 计数与用时优先读取持久的 `sessionStats` 投影,窗口折叠仅作无该单元装配时的回退([全会话计数](../bug-fix/2026-08-12-full-session-turn-step-counts.zh.md));token 数字只走 `tokenUsage`,投影缺席时直接不渲染用量 pill,而非展示窗口推算的计费。缓存写入仍计入计费总量与缓存命中分母([投影决定](../architecture/2026-07-29-projected-token-usage-and-request-context.zh.md))。上下文占用彻底离开统计条——由输入框旁的 ContextMeter 圆环独占。 +- **渲染纪律。** 该行只折叠已定稿节点(`chat.legacy.nodes` 身份),流式 chunk 帧零重渲染——由渲染计数单测钉住。无已完成步且无计费 token 的会话什么都不渲染。 +- **`data-composer-stats` 是跨包属性契约。** pill 行根元素携带它;ui-conversation 的 `InputBar.module.css` 用 `:has([data-composer-stats])` 在该行挂载时把输入框底部留白收紧到 4px。生产方在单测里钉住该属性,沿用 `data-trigger-menu` 先例。 + +## 备选方案 + +- **单行变体(StatsLine,A/B 落选方)。** 全部数字常驻一行文本,精确总量靠悬停提示。败在拥挤与可发现性:悬停入口无提示可寻,单行也无法给时间与计费数字分组。 +- **三个常驻分组共享一个弹层。** 中间迭代曾把计数、时间、token 保持为三个内联分组。双 pill 胜出是因为时间/用量的切分与两个底层投影一一对应,且每个 pill 的图标直接预告其弹层内容。 +- **在统计条里保留上下文占用。** 否决:输入框旁的 ContextMeter 圆环已持续展示占用,统计条重复它是把宽度花在冗余上。 +- **抽出与 `TurnUsagePanel` 共享的 dl 分桶行。** 会话总量弹层与逐轮面板皮肤相同但契约不同(此处所有分桶恒在;逐轮字段可选且含模型路由);共享组件只是给九行代码包一层条件。该镜像以 `jscpd:ignore` 标注并内联说明理由。 + +## 后果 + +- `ChatSnapshotBuilder` 的 legacy 切片现在服务于 StatsPills;[节点装配 note](../architecture/2026-08-09-client-conversation-node-assembly.zh.md) 已跟进该消费者更名。 +- 精确数字从悬停改为一次点击;统计条本身只承载两个头条读数。 +- Web e2e 对统计条的断言匹配时间 pill 内的子串文本;分页历史场景的 aria golden 钉住双 pill 结构。 +- 生成的插槽目录中 `conversation.composer.dock` 占用者为 `client-ui-chat StatsPills id 'stats'`。 diff --git a/apps/web/tests/command-image-envelope.expected.e2e.ts b/apps/web/tests/command-image-envelope.expected.e2e.ts index 36c25232fd..565eaa3524 100644 --- a/apps/web/tests/command-image-envelope.expected.e2e.ts +++ b/apps/web/tests/command-image-envelope.expected.e2e.ts @@ -21,7 +21,7 @@ async function freshComposer(): Promise { fireEvent.click(start) return await waitFor(() => { const surface = document.querySelector( - '[data-composer-input][data-placeholder="Describe what you want to build, run / commands, @ files or sessions"]', + '[data-composer-input][data-placeholder="Describe what you want to build, / commands, @ files or sessions"]', ) if (surface === null) throw new Error('composer surface missing') return surface diff --git a/apps/web/tests/expected/clickable-links-gallery/ui.expected.md b/apps/web/tests/expected/clickable-links-gallery/ui.expected.md index b0ff0e6411..da6d1f3794 100644 --- a/apps/web/tests/expected/clickable-links-gallery/ui.expected.md +++ b/apps/web/tests/expected/clickable-links-gallery/ui.expected.md @@ -191,7 +191,7 @@ - text: {{clock}} - button "Back to bottom": - img -- textbox "Message or run a task, run / commands, @ files or sessions" +- textbox "Message or run a task, / commands, @ files or sessions" - button "Commands": - img - button "Add attachment": @@ -201,4 +201,6 @@ - text: DeepSeek-V4-Flash - img - button "Send message" [disabled] -- text: 1 turns · 1 steps LLM {{duration}} · Tool call {{duration}} +- button "1 turns 1 steps": + - img + - text: 1 turns 1 steps diff --git a/apps/web/tests/expected/github-ready-review/conversation-expanded.expected.md b/apps/web/tests/expected/github-ready-review/conversation-expanded.expected.md index 521443e706..cd24ce7478 100644 --- a/apps/web/tests/expected/github-ready-review/conversation-expanded.expected.md +++ b/apps/web/tests/expected/github-ready-review/conversation-expanded.expected.md @@ -48,7 +48,7 @@ - img - text: Ran for {{duration}} - text: {{clock}} -- textbox "Message or run a task, run / commands, @ files or sessions" +- textbox "Message or run a task, / commands, @ files or sessions" - button "Commands": - img - button "Add attachment": @@ -58,4 +58,6 @@ - text: github-webhook-review-test/reply - img - button "Send message" [disabled] -- text: 1 turns · 1 steps LLM {{duration}} +- button "1 turns 1 steps": + - img + - text: 1 turns 1 steps diff --git a/apps/web/tests/expected/github-ready-review/conversation.expected.md b/apps/web/tests/expected/github-ready-review/conversation.expected.md index 089e6206b4..707362508e 100644 --- a/apps/web/tests/expected/github-ready-review/conversation.expected.md +++ b/apps/web/tests/expected/github-ready-review/conversation.expected.md @@ -40,7 +40,7 @@ - img - text: Ran for {{duration}} - text: {{clock}} -- textbox "Message or run a task, run / commands, @ files or sessions" +- textbox "Message or run a task, / commands, @ files or sessions" - button "Commands": - img - button "Add attachment": @@ -50,4 +50,6 @@ - text: github-webhook-review-test/reply - img - button "Send message" [disabled] -- text: 1 turns · 1 steps LLM {{duration}} +- button "1 turns 1 steps": + - img + - text: 1 turns 1 steps diff --git a/apps/web/tests/expected/goal-command-presentation/ui.expected.md b/apps/web/tests/expected/goal-command-presentation/ui.expected.md index 5a991063cd..92624aee4d 100644 --- a/apps/web/tests/expected/goal-command-presentation/ui.expected.md +++ b/apps/web/tests/expected/goal-command-presentation/ui.expected.md @@ -14,7 +14,7 @@ - img - img - text: "goal No goal is currently set. Usage: /goal [|clear|edit |pause|resume]" -- textbox "Message or run a task, run / commands, @ files or sessions" +- textbox "Message or run a task, / commands, @ files or sessions" - button "Commands": - img - button "Add attachment": diff --git a/apps/web/tests/expected/markdown-cjk-strong/ui.expected.md b/apps/web/tests/expected/markdown-cjk-strong/ui.expected.md index bff877489a..69c7062a46 100644 --- a/apps/web/tests/expected/markdown-cjk-strong/ui.expected.md +++ b/apps/web/tests/expected/markdown-cjk-strong/ui.expected.md @@ -48,7 +48,7 @@ - img - text: Ran for {{duration}} - text: {{clock}} -- textbox "Message or run a task, run / commands, @ files or sessions" +- textbox "Message or run a task, / commands, @ files or sessions" - button "Commands": - img - button "Add attachment": @@ -58,4 +58,6 @@ - text: DeepSeek-V4-Flash - img - button "Send message" [disabled] -- text: 1 turns · 1 steps LLM {{duration}} +- button "1 turns 1 steps": + - img + - text: 1 turns 1 steps diff --git a/apps/web/tests/expected/markdown-images/ui.expected.md b/apps/web/tests/expected/markdown-images/ui.expected.md index 307ea19de3..637700cf64 100644 --- a/apps/web/tests/expected/markdown-images/ui.expected.md +++ b/apps/web/tests/expected/markdown-images/ui.expected.md @@ -27,7 +27,7 @@ - img - text: Ran for {{duration}} - text: {{clock}} -- textbox "Message or run a task, run / commands, @ files or sessions" +- textbox "Message or run a task, / commands, @ files or sessions" - button "Commands": - img - button "Add attachment": @@ -37,4 +37,6 @@ - text: DeepSeek-V4-Flash - img - button "Send message" [disabled] -- text: 1 turns · 1 steps LLM {{duration}} +- button "1 turns 1 steps": + - img + - text: 1 turns 1 steps diff --git a/apps/web/tests/expected/markdown-inline-code-links/ui.expected.md b/apps/web/tests/expected/markdown-inline-code-links/ui.expected.md index 4f0d02556c..c6425f205a 100644 --- a/apps/web/tests/expected/markdown-inline-code-links/ui.expected.md +++ b/apps/web/tests/expected/markdown-inline-code-links/ui.expected.md @@ -39,7 +39,7 @@ - img - text: Ran for {{duration}} - text: {{clock}} -- textbox "Message or run a task, run / commands, @ files or sessions" +- textbox "Message or run a task, / commands, @ files or sessions" - button "Commands": - img - button "Add attachment": @@ -49,4 +49,6 @@ - text: DeepSeek-V4-Flash - img - button "Send message" [disabled] -- text: 1 turns · 1 steps LLM {{duration}} +- button "1 turns 1 steps": + - img + - text: 1 turns 1 steps diff --git a/apps/web/tests/expected/math-rendering/ui.expected.md b/apps/web/tests/expected/math-rendering/ui.expected.md index 3898fca4b0..d4d5cbc87c 100644 --- a/apps/web/tests/expected/math-rendering/ui.expected.md +++ b/apps/web/tests/expected/math-rendering/ui.expected.md @@ -43,7 +43,7 @@ - img - text: Ran for {{duration}} - text: {{clock}} -- textbox "Message or run a task, run / commands, @ files or sessions" +- textbox "Message or run a task, / commands, @ files or sessions" - button "Commands": - img - button "Add attachment": @@ -53,4 +53,6 @@ - text: DeepSeek-V4-Flash - img - button "Send message" [disabled] -- text: 1 turns · 1 steps LLM {{duration}} +- button "1 turns 1 steps": + - img + - text: 1 turns 1 steps diff --git a/apps/web/tests/expected/reference-composer/order.expected.md b/apps/web/tests/expected/reference-composer/order.expected.md index 192e5157d0..9f9f643e70 100644 --- a/apps/web/tests/expected/reference-composer/order.expected.md +++ b/apps/web/tests/expected/reference-composer/order.expected.md @@ -13,7 +13,7 @@ - button "Session recall Research notes": - img - text: Session recall Research notes -- textbox "Message or run a task, run / commands, @ files or sessions" +- textbox "Message or run a task, / commands, @ files or sessions" - button "Commands": - img - button "Add attachment": diff --git a/apps/web/tests/expected/skill-user-invoke/ui-expanded.expected.md b/apps/web/tests/expected/skill-user-invoke/ui-expanded.expected.md index a99a0b3fa3..33c4a66a1c 100644 --- a/apps/web/tests/expected/skill-user-invoke/ui-expanded.expected.md +++ b/apps/web/tests/expected/skill-user-invoke/ui-expanded.expected.md @@ -40,7 +40,7 @@ - img - text: Ran for {{duration}} - text: {{clock}} -- textbox "Message or run a task, run / commands, @ files or sessions" +- textbox "Message or run a task, / commands, @ files or sessions" - button "Commands": - img - button "Add attachment": @@ -51,4 +51,9 @@ - img - button "0% of context used" - button "Send message" [disabled] -- text: 1 turns · 1 steps LLM {{duration}} TTFT avg {{duration}} · {{throughput}} tok/s Cache hit 0% Input 256 tok · Output 16 tok +- button "1 turns 1 steps{{throughput}} tok/s": + - img + - text: 1 turns 1 steps{{throughput}} tok/s +- button "272 tokCache hit 0%": + - img + - text: 272 tokCache hit 0% diff --git a/apps/web/tests/expected/skill-user-invoke/ui.expected.md b/apps/web/tests/expected/skill-user-invoke/ui.expected.md index 604c97db5f..47bcd5d694 100644 --- a/apps/web/tests/expected/skill-user-invoke/ui.expected.md +++ b/apps/web/tests/expected/skill-user-invoke/ui.expected.md @@ -32,7 +32,7 @@ - img - text: Ran for {{duration}} - text: {{clock}} -- textbox "Message or run a task, run / commands, @ files or sessions" +- textbox "Message or run a task, / commands, @ files or sessions" - button "Commands": - img - button "Add attachment": @@ -43,4 +43,9 @@ - img - button "0% of context used" - button "Send message" [disabled] -- text: 1 turns · 1 steps LLM {{duration}} TTFT avg {{duration}} · {{throughput}} tok/s Cache hit 0% Input 256 tok · Output 16 tok +- button "1 turns 1 steps{{throughput}} tok/s": + - img + - text: 1 turns 1 steps{{throughput}} tok/s +- button "272 tokCache hit 0%": + - img + - text: 272 tokCache hit 0% diff --git a/apps/web/tests/expected/stats-paged-history/ui.expected.md b/apps/web/tests/expected/stats-paged-history/ui.expected.md index 4341818041..ce18156e22 100644 --- a/apps/web/tests/expected/stats-paged-history/ui.expected.md +++ b/apps/web/tests/expected/stats-paged-history/ui.expected.md @@ -459,7 +459,7 @@ - text: 7/25 {{clock}} - button "Back to bottom": - img -- textbox "Message or run a task, run / commands, @ files or sessions" +- textbox "Message or run a task, / commands, @ files or sessions" - button "Commands": - img - button "Add attachment": @@ -469,4 +469,6 @@ - text: DeepSeek-V4-Flash - img - button "Send message" [disabled] -- text: 28 turns · 28 steps LLM {{duration}} +- button "28 turns 28 steps": + - img + - text: 28 turns 28 steps diff --git a/apps/web/tests/expected/steer-all/mid-steer.expected.md b/apps/web/tests/expected/steer-all/mid-steer.expected.md index fe5e529df5..781f5be768 100644 --- a/apps/web/tests/expected/steer-all/mid-steer.expected.md +++ b/apps/web/tests/expected/steer-all/mid-steer.expected.md @@ -31,7 +31,7 @@ - text: "Interjection: include the word ORANGE in your final reply." - button "Copy": - img -- textbox "Message or run a task, run / commands, @ files or sessions" +- textbox "Message or run a task, / commands, @ files or sessions" - button "Commands": - img - button "Add attachment": diff --git a/apps/web/tests/expected/steer-all/settled-expanded.expected.md b/apps/web/tests/expected/steer-all/settled-expanded.expected.md index 6939a3c0ba..bb60fc9ee1 100644 --- a/apps/web/tests/expected/steer-all/settled-expanded.expected.md +++ b/apps/web/tests/expected/steer-all/settled-expanded.expected.md @@ -50,7 +50,7 @@ - img - text: Ran for {{duration}} - text: {{clock}} -- textbox "Message or run a task, run / commands, @ files or sessions" +- textbox "Message or run a task, / commands, @ files or sessions" - button "Commands": - img - button "Add attachment": @@ -61,4 +61,9 @@ - img - button "0% of context used" - button "Send message" [disabled] -- text: 1 turns · 2 steps LLM {{duration}} · Tool call {{duration}} TTFT avg {{duration}} · {{throughput}} tok/s Cache hit 0% Input 20 tok · Output 20 tok +- button "1 turns 2 steps{{throughput}} tok/s": + - img + - text: 1 turns 2 steps{{throughput}} tok/s +- button "40 tokCache hit 0%": + - img + - text: 40 tokCache hit 0% diff --git a/apps/web/tests/expected/steer-all/settled.expected.md b/apps/web/tests/expected/steer-all/settled.expected.md index 39547a7fa7..66145a7073 100644 --- a/apps/web/tests/expected/steer-all/settled.expected.md +++ b/apps/web/tests/expected/steer-all/settled.expected.md @@ -38,7 +38,7 @@ - img - text: Ran for {{duration}} - text: {{clock}} -- textbox "Message or run a task, run / commands, @ files or sessions" +- textbox "Message or run a task, / commands, @ files or sessions" - button "Commands": - img - button "Add attachment": @@ -49,4 +49,9 @@ - img - button "0% of context used" - button "Send message" [disabled] -- text: 1 turns · 2 steps LLM {{duration}} · Tool call {{duration}} TTFT avg {{duration}} · {{throughput}} tok/s Cache hit 0% Input 20 tok · Output 20 tok +- button "1 turns 2 steps{{throughput}} tok/s": + - img + - text: 1 turns 2 steps{{throughput}} tok/s +- button "40 tokCache hit 0%": + - img + - text: 40 tokCache hit 0% diff --git a/apps/web/tests/goal-bar.e2e.ts b/apps/web/tests/goal-bar.e2e.ts index 5b077016e5..9d811e5c8b 100644 --- a/apps/web/tests/goal-bar.e2e.ts +++ b/apps/web/tests/goal-bar.e2e.ts @@ -45,7 +45,7 @@ describe('web e2e: goal bar clear convergence', () => { onTestFailed(() => saveFailureShot(page, 'web-e2e-goal-bar-clear')) // Startup reuses the fixture workspace's blank session, keeping this // command independent of alpha's running replay and pending question. - const input = page.locator('[data-composer-input][data-placeholder="Describe what you want to build, run / commands, @ files or sessions"]') + const input = page.locator('[data-composer-input][data-placeholder="Describe what you want to build, / commands, @ files or sessions"]') await input.waitFor({ timeout: 10_000 }) await input.fill('/goal guard rapid clear clicks') await input.press('Enter') diff --git a/apps/web/tests/image-display.expected.e2e.ts b/apps/web/tests/image-display.expected.e2e.ts index 34481c4a17..e35486abfe 100644 --- a/apps/web/tests/image-display.expected.e2e.ts +++ b/apps/web/tests/image-display.expected.e2e.ts @@ -90,7 +90,7 @@ it('accepts pasted images into the composer rail in order and removes them', asy // this assembled lane pins the intake chain over the built graph. const textarea = await waitFor(() => { const surface = document.querySelector( - '[data-composer-input][data-placeholder="Describe what you want to build, run / commands, @ files or sessions"]', + '[data-composer-input][data-placeholder="Describe what you want to build, / commands, @ files or sessions"]', ) if (surface === null) throw new Error('composer surface missing') return surface @@ -161,7 +161,7 @@ it('accepts a whole-page drop under the limits-labeled overlay and refuses an ov fireEvent.click(start) const textarea = await waitFor(() => { const surface = document.querySelector( - '[data-composer-input][data-placeholder="Describe what you want to build, run / commands, @ files or sessions"]', + '[data-composer-input][data-placeholder="Describe what you want to build, / commands, @ files or sessions"]', ) if (surface === null) throw new Error('composer surface missing') return surface @@ -216,7 +216,7 @@ it('renders a host dimension rejection with the projected 2000px limit', async ( const textarea = await waitFor(() => { const surface = document.querySelector( - '[data-composer-input][data-placeholder="Describe what you want to build, run / commands, @ files or sessions"]', + '[data-composer-input][data-placeholder="Describe what you want to build, / commands, @ files or sessions"]', ) if (surface === null) throw new Error('composer surface missing') return surface diff --git a/apps/web/tests/lifecycle-chrome.e2e.ts b/apps/web/tests/lifecycle-chrome.e2e.ts index 3abee08a82..d83a9c8758 100644 --- a/apps/web/tests/lifecycle-chrome.e2e.ts +++ b/apps/web/tests/lifecycle-chrome.e2e.ts @@ -222,7 +222,8 @@ describe('web e2e: lifecycle & chrome (workspace flow / reload / dark mode)', () ).toBeGreaterThanOrEqual(1) await expect.poll(() => page.locator('[role="treeitem"][aria-selected="true"]').count(), { timeout: 10_000 }).toBe(1) await expect.poll(() => page.getByText('LIGHTHOUSE', { exact: true }).count(), { timeout: 15_000 }).toBeGreaterThanOrEqual(1) - await expect.poll(() => page.getByText('Cache hit 99.5%', { exact: true }).count(), { timeout: 15_000 }).toBe(1) + // The usage pill's one label span concatenates the billed total and the cache-hit share. + await expect.poll(() => page.getByRole('button', { name: /Cache hit 99\.5%/ }).count(), { timeout: 15_000 }).toBe(1) // Host: the session's durable header cwd is the folder the workspace // flow created and adopted (/workspace) — the proof the // send went through workspace materialization rather than a bare diff --git a/apps/web/tests/math-rendering.e2e.ts b/apps/web/tests/math-rendering.e2e.ts index 5f5c89ead4..c826d5aff2 100644 --- a/apps/web/tests/math-rendering.e2e.ts +++ b/apps/web/tests/math-rendering.e2e.ts @@ -123,7 +123,7 @@ describe('web e2e: settled Markdown math rendering', () => { await expect.poll(() => page.locator('.katex-display').count(), { timeout: 10_000 }).toBe(2) expect(await page.locator('.katex-error').count()).toBe(0) await expect.poll( - () => page.getByText('1 turns · 1 steps', { exact: false }).count(), + () => page.getByText('1 turns 1 steps', { exact: false }).count(), { timeout: 10_000 }, ).toBe(1) diff --git a/apps/web/tests/preview-boot.e2e.ts b/apps/web/tests/preview-boot.e2e.ts index 7b5f49c1c0..285e3aaa66 100644 --- a/apps/web/tests/preview-boot.e2e.ts +++ b/apps/web/tests/preview-boot.e2e.ts @@ -314,7 +314,7 @@ async function bootPreview(origin: string, browser: Browser): Promise { const configureLater = page.getByRole('button', { name: 'Configure later' }) await configureLater.waitFor({ timeout: 30_000 }) await configureLater.click() - await page.locator('[data-composer-input][data-placeholder="Describe what you want to build, run / commands, @ files or sessions"]') + await page.locator('[data-composer-input][data-placeholder="Describe what you want to build, / commands, @ files or sessions"]') .waitFor({ timeout: 30_000 }) const exercised = await page.evaluate(async () => { diff --git a/apps/web/tests/snapshots/streaming-fence-highlight/mid-stream.expected.md b/apps/web/tests/snapshots/streaming-fence-highlight/mid-stream.expected.md index 461c34a9d1..c5d7728978 100644 --- a/apps/web/tests/snapshots/streaming-fence-highlight/mid-stream.expected.md +++ b/apps/web/tests/snapshots/streaming-fence-highlight/mid-stream.expected.md @@ -24,7 +24,7 @@ - button "Copy" - code: "const first: number = 1 const second = \"two\" let tail" - status: Deep diving... -- textbox "Message or run a task, run / commands, @ files or sessions" +- textbox "Message or run a task, / commands, @ files or sessions" - button "Commands": - img - button "Add attachment": diff --git a/apps/web/tests/startup-auto-selection.e2e.ts b/apps/web/tests/startup-auto-selection.e2e.ts index 7e7065e6b2..882b99b420 100644 --- a/apps/web/tests/startup-auto-selection.e2e.ts +++ b/apps/web/tests/startup-auto-selection.e2e.ts @@ -130,7 +130,7 @@ describe('web e2e: startup auto-selection', () => { expect(await page.locator('[data-composer-input]').first().isVisible()).toBe(true) releaseOpening() - await page.locator('[data-composer-input][contenteditable="true"][data-placeholder="Describe what you want to build, run / commands, @ files or sessions"]') + await page.locator('[data-composer-input][contenteditable="true"][data-placeholder="Describe what you want to build, / commands, @ files or sessions"]') .waitFor({ timeout: 15_000 }) acknowledgeReloadConnectionLoss(tripwire, warningsBefore) diff --git a/apps/web/tests/stats-paged-history.e2e.ts b/apps/web/tests/stats-paged-history.e2e.ts index 6e6174232c..45e5d3de2d 100644 --- a/apps/web/tests/stats-paged-history.e2e.ts +++ b/apps/web/tests/stats-paged-history.e2e.ts @@ -23,7 +23,7 @@ const SEED_ID = 'stats-paged-history-web-e2e' /** Turn count: 2 surface messages per turn, so 28 turns overflow one 50-message page. */ const TURNS = 28 -const FULL_COUNTS = `${TURNS} turns · ${TURNS} steps` +const FULL_COUNTS = `${TURNS} turns ${TURNS} steps` /** * Generate the seed: TURNS closed single-step turns of one short user prompt diff --git a/apps/web/tests/subagent-conversation.e2e.ts b/apps/web/tests/subagent-conversation.e2e.ts index af7407778a..54e9960836 100644 --- a/apps/web/tests/subagent-conversation.e2e.ts +++ b/apps/web/tests/subagent-conversation.e2e.ts @@ -423,7 +423,7 @@ describe('web e2e: persisted subagent conversation and human continuation', () = expect(await page.locator('[data-composer-seat]').evaluate(element => getComputedStyle(element).visibility)).toBe('hidden') releaseCatalog() - const input = page.getByRole('textbox', { name: 'Message or run a task, run / commands, @ files or sessions' }) + const input = page.getByRole('textbox', { name: 'Message or run a task, / commands, @ files or sessions' }) await input.waitFor({ timeout: 15_000 }) await expect.poll(() => input.isEnabled(), { timeout: 15_000 }).toBe(true) acknowledgeReloadConnectionLoss(tripwire, warningStart) @@ -447,7 +447,7 @@ describe('web e2e: persisted subagent conversation and human continuation', () = resolveEnded() }) }) - const input = page.getByRole('textbox', { name: 'Message or run a task, run / commands, @ files or sessions' }) + const input = page.getByRole('textbox', { name: 'Message or run a task, / commands, @ files or sessions' }) await input.fill(FOLLOWUP) await input.press('Enter') await expect.poll( @@ -537,7 +537,7 @@ describe('web e2e: persisted subagent conversation and human continuation', () = .click() await page.getByRole('button', { name: '3 subagents' }).hover() await page.getByRole('treeitem', { name: new RegExp(LABEL) }).click() - await page.getByRole('textbox', { name: 'Message or run a task, run / commands, @ files or sessions' }).waitFor() + await page.getByRole('textbox', { name: 'Message or run a task, / commands, @ files or sessions' }).waitFor() const forkResponse = page.waitForResponse(response => new URL(response.url()).pathname === '/api/session/fork') await page.getByRole('button', { name: 'Branch into a new conversation' }).last().click() diff --git a/apps/web/tests/subagent-interrupt-ui.e2e.ts b/apps/web/tests/subagent-interrupt-ui.e2e.ts index bf770c3874..cd155bda39 100644 --- a/apps/web/tests/subagent-interrupt-ui.e2e.ts +++ b/apps/web/tests/subagent-interrupt-ui.e2e.ts @@ -263,7 +263,7 @@ describe.skipIf(MODE === 'record')('web e2e: composer interrupt for a running co .getByRole('button').first().click() await page.getByRole('button', { name: /1 subagent/ }).click() await page.getByRole('treeitem', { name: new RegExp(LABEL) }).click() - const input = page.getByRole('textbox', { name: 'Message or run a task, run / commands, @ files or sessions' }) + const input = page.getByRole('textbox', { name: 'Message or run a task, / commands, @ files or sessions' }) await input.waitFor({ timeout: 15_000 }) expect(await input.isDisabled()).toBe(false) diff --git a/apps/web/tests/support.ts b/apps/web/tests/support.ts index 252ef70ee7..c52e60c791 100644 --- a/apps/web/tests/support.ts +++ b/apps/web/tests/support.ts @@ -112,7 +112,7 @@ export async function connectFreshWorkspace(page: Page, root: string, name = 'wo await dialog.getByRole('button', { name: 'Open', exact: true }).click() // The pick connected the workspace: the blank session's live composer // replaces the locked placeholder and enables. - await page.locator('[data-composer-input][contenteditable="true"][data-placeholder="Describe what you want to build, run / commands, @ files or sessions"]') + await page.locator('[data-composer-input][contenteditable="true"][data-placeholder="Describe what you want to build, / commands, @ files or sessions"]') .waitFor({ timeout: 15_000 }) } diff --git a/packages/client/ui-attachment/src/FileCard.module.css b/packages/client/ui-attachment/src/FileCard.module.css index 71de9ec4bf..ac4de3905e 100644 --- a/packages/client/ui-attachment/src/FileCard.module.css +++ b/packages/client/ui-attachment/src/FileCard.module.css @@ -106,6 +106,18 @@ opacity: 1; } +@media (pointer: coarse) { + .remove { + opacity: 1; + } +} + +@media (prefers-reduced-motion: reduce) { + .remove { + transition: none; + } +} + .card:hover .name, .card:focus-within .name { padding-right: 18px; diff --git a/packages/client/ui-attachment/src/FileCard.tsx b/packages/client/ui-attachment/src/FileCard.tsx index d5634b7d9a..e10c91993c 100644 --- a/packages/client/ui-attachment/src/FileCard.tsx +++ b/packages/client/ui-attachment/src/FileCard.tsx @@ -1,4 +1,4 @@ -import { DocumentFileIcon, fileSizeText } from '@deepseek-ai/dsh-client-ui-primitives' +import { DocumentFileIcon, fileSizeText, IconCloseFill14 } from '@deepseek-ai/dsh-client-ui-primitives' import css from './FileCard.module.css' /** Localized strings consumed by one pending-file card. */ @@ -72,9 +72,7 @@ export function FileCard({ aria-label={labels.remove} onClick={onRemove} > - - - + {state === 'uploading' && ( diff --git a/packages/client/ui-chat/src/client/apply.ts b/packages/client/ui-chat/src/client/apply.ts index 0c7d87f344..31cfd00696 100644 --- a/packages/client/ui-chat/src/client/apply.ts +++ b/packages/client/ui-chat/src/client/apply.ts @@ -23,7 +23,7 @@ import { EMPTY_CHAT_SNAPSHOT } from './contract/snapshot.ts' import { ApprovalCommand } from './chat/ApprovalCommand.tsx' import { ChatView } from './chat/ChatView.tsx' import { registerChatNodeRenderers } from './chat/register-node-renderers.ts' -import { StatsLine } from './chat/StatsLine.tsx' +import { StatsPills } from './chat/StatsPills.tsx' import { registerConversationNodes } from './conversation-nodes/register.ts' import { DetailsPanel } from './details/DetailsPanel.tsx' import { en, NS, zh } from './locale.ts' @@ -155,7 +155,7 @@ export function apply(ctx: Context): void { ctx.slots.inject('conversation.composer.dock', () => ctx.slots.register({ name: 'conversation.composer.dock', id: 'stats', order: 0, locale: NS, - }, StatsLine)) + }, StatsPills)) ctx.slots.inject('conversation.approval.detail', () => ctx.slots.register({ name: 'conversation.approval.detail' }, ApprovalCommand)) diff --git a/packages/client/ui-chat/src/client/chat/StatsLine.module.css b/packages/client/ui-chat/src/client/chat/StatsLine.module.css deleted file mode 100644 index 29c0bc3aab..0000000000 --- a/packages/client/ui-chat/src/client/chat/StatsLine.module.css +++ /dev/null @@ -1,26 +0,0 @@ -/* Session stats row: 13/20 tertiary text (the secondary content tier) under - the flow, aligned to the shared message column axis - (--dsh-chat-content-width). */ - -.root { - /* Block, not flex: text-overflow only elides a block's inline content, so - an overlong line ends in … instead of a mid-glyph clip. */ - display: block; - text-align: center; - max-width: var(--dsh-chat-content-width); - width: 100%; - margin: 0 auto; - box-sizing: border-box; - padding: 4px calc(var(--dsh-composer-side-clearance) + 16px) 0px; - font-size: var(--dsh-content-font-size-secondary, 13px); - line-height: calc(20px + var(--dsh-content-font-delta-secondary, 0px)); - color: var(--dsw-alias-label-tertiary); - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; -} - -.sep { - color: var(--dsw-alias-separator-primary); - margin: 0 10px; /* carries the former flex gap */ -} diff --git a/packages/client/ui-chat/src/client/chat/StatsLine.tsx b/packages/client/ui-chat/src/client/chat/StatsLine.tsx deleted file mode 100644 index 170ffec956..0000000000 --- a/packages/client/ui-chat/src/client/chat/StatsLine.tsx +++ /dev/null @@ -1,209 +0,0 @@ -// Settled-node identity prevents stream-delta updates from rerendering this row. -// Mounted on 'conversation.composer.dock' so it sticks with the composer in the -// active conversation scrollport (see ConversationRoot data-conversation-scroll). - -import { Fragment, memo, useCallback, useLayoutEffect, useMemo, useRef, useState } from 'react' -import { Tooltip } from '@deepseek-ai/dsh-client-ui-primitives' -import type { UseProjection } from '@deepseek-ai/dsh-api-session-controller/client' -import type { SnapshotSelectorHook } from '@deepseek-ai/dsh-client-ui-slots' -// Type-only: merges the sessionStats key into SessionProjectionMap for useProjection. -import type {} from '@deepseek-ai/dsh-session-stats/client' -import type { TokenUsageProjection } from '@deepseek-ai/dsh-token-meter/client' -import type { ChatViewSlotProps } from '../contract/slots.ts' -import type { ChatSnapshot } from '../contract/snapshot.ts' -import { formatTokensPerSecond } from './message-chrome.ts' -import { assistantStepReading } from '../contract/turn-metrics.ts' -import { formatCacheHitPercent, formatTokens } from './token-format.ts' -import css from './StatsLine.module.css' - -interface WindowStats { - turns: number - steps: number - /** Summed request wall time (step/start → assistant/message); 0 when no node carries timing. */ - llmMs: number - /** Summed tool wall time (tool/call → tool/result); 0 when no pair is in-window. */ - toolMs: number - /** Summed first-token latency over `ttftSteps`; 0 when no step records it. */ - ttftMs: number - /** Steps carrying a recorded TTFT. */ - ttftSteps: number - /** Summed decode wall time over steps that also report output tokens. */ - decodeMs: number - /** Summed output tokens over the same decode-timed steps. */ - decodeTokens: number -} - -/** - * Fold assistant and tool-result nodes into window-scoped display totals — - * the FALLBACK for assemblies without the `sessionStats` projection. - * - * Every displayed figure rides that durable whole-log projection (and token - * accounting rides `tokenUsage`) because the window is paged and compaction - * rewrites it; this fold answers "what is on screen" only when no projection - * value is served. Its field names deliberately mirror the projection's so - * the two swap wholesale. - * @param nodes - snapshot nodes. - * @returns fallback counts and summed wall times. - */ -export function deriveStats(nodes: ChatSnapshot['legacy']['nodes']): WindowStats { - const turns = new Set() - let steps = 0 - let llmMs = 0 - let toolMs = 0 - let ttftMs = 0 - let ttftSteps = 0 - let decodeMs = 0 - let decodeTokens = 0 - for (const node of nodes) { - if (node.kind === 'tool-result') { - if (node.callTime !== null) toolMs += Math.max(0, node.time - node.callTime) - continue - } - if (node.kind !== 'assistant') continue - turns.add(node.turn) - steps += 1 - if (node.timing !== undefined && node.timing.stepStartTime !== null) { - llmMs += Math.max(0, node.timing.completedTime - node.timing.stepStartTime) - } - const reading = assistantStepReading(node) - if (reading.ttftMs !== null) { - ttftMs += reading.ttftMs - ttftSteps += 1 - } - if (reading.decodeMs !== null && reading.outputTokens !== null) { - decodeMs += reading.decodeMs - decodeTokens += reading.outputTokens - } - } - return { turns: turns.size, steps, llmMs, toolMs, ttftMs, ttftSteps, decodeMs, decodeTokens } -} - -/** - * Compact duration: 45.2s under a minute, 2m42s from there on. - * @param ms - duration in milliseconds. - * @returns display string. - */ -export function formatDuration(ms: number, t: ChatViewSlotProps['t']): string { - const s = ms / 1_000 - if (s < 60) return t('duration.compactSeconds', { seconds: Math.round(s * 10) / 10 }) - const whole = Math.round(s) - return t('duration.compactMinutes', { - minutes: Math.floor(whole / 60), - seconds: whole % 60, - }) -} - -/** - * Display-ready cache-hit share of prompt-side input over the whole durable log. - * @param usage - the session's token-usage projection value. - * @returns integer text when integer rounding stays below 100, otherwise the - * minimum decimal precision that still rounds below 100; a full hit returns - * 100, and no billed input returns null. - */ -export function cacheHitPercent(usage: TokenUsageProjection): string | null { - const denominator = billedInputTokens(usage) - return formatCacheHitPercent(usage.cacheReadTokens, denominator) -} - -/** - * Sum the three disjoint prompt-side billing buckets. - * @param usage - the session's token-usage projection value. - * @returns billed input tokens. - */ -export function billedInputTokens(usage: TokenUsageProjection): number { - return usage.uncachedInputTokens + usage.cacheReadTokens + usage.cacheWriteTokens -} - -/** Props: the conversation-snapshot selector plus the projection read seat. */ -export interface StatsLineProps { - useChat: SnapshotSelectorHook - useProjection: UseProjection - /** The owning dock's locale seat. */ - t: ChatViewSlotProps['t'] -} - -/** Render and measure one non-empty statistics line. */ -const StatsLineContent = memo(function StatsLineContent({ - groups, - line, -}: { - readonly groups: readonly string[] - readonly line: string -}) { - const rootRef = useRef(null) - const [truncated, setTruncated] = useState(false) - const measure = useCallback(() => { - const el = rootRef.current - if (el === null) return - const next = el.scrollWidth > el.clientWidth - setTruncated(current => current === next ? current : next) - }, []) - useLayoutEffect(() => { - const el = rootRef.current - if (el === null || typeof ResizeObserver === 'undefined') return - const observer = new ResizeObserver(measure) - observer.observe(el) - return () => { observer.disconnect() } - }, [measure]) - useLayoutEffect(measure, [line, measure]) - return ( - -
- {groups.map((group, i) => ( - - {i > 0 && <>|{' '}} - {group} - - ))} -
-
- ) -}) - -export const StatsLine = memo(function StatsLine({ useChat, useProjection, t }: StatsLineProps) { - const settledNodes = useChat(s => s.legacy.nodes) - const usage = useProjection('tokenUsage') - // Every figure rides the durable sessionStats projection, so paging and - // compaction cannot change any of them; an assembly without the unit falls - // back to the window-scoped fold wholesale (same field names), paid only - // while no projection value is served. - const projected = useProjection('sessionStats') - const stats = useMemo(() => projected ?? deriveStats(settledNodes), [projected, settledNodes]) - // Pipe-separated groups (figma stats strip); a group with no data drops out whole. - const groups: string[] = [] - if (stats.steps > 0) { - groups.push(t('stats.counts', { turns: stats.turns, steps: stats.steps })) - const durations: string[] = [] - if (stats.llmMs > 0) durations.push(t('stats.llm', { duration: formatDuration(stats.llmMs, t) })) - if (stats.toolMs > 0) durations.push(t('stats.toolCall', { duration: formatDuration(stats.toolMs, t) })) - if (durations.length > 0) groups.push(durations.join(' · ')) - const speeds: string[] = [] - if (stats.ttftSteps > 0) { - speeds.push(t('stats.ttftAverage', { duration: formatDuration(stats.ttftMs / stats.ttftSteps, t) })) - } - if (stats.decodeMs > 0) { - speeds.push(t('stats.tokensPerSecond', { - throughput: formatTokensPerSecond(stats.decodeTokens / (stats.decodeMs / 1_000)), - })) - } - if (speeds.length > 0) groups.push(speeds.join(' · ')) - } - // Context occupancy deliberately lives on the composer's ContextMeter ring, - // not here — one home per fact. - // Billing rides the durable projection, so these survive paging and - // compaction. Gated on actual token activity: a session whose steps all - // settled without billing (e.g. every request failed) shows its counts - // without a zero-token group. - if (usage !== undefined - && (billedInputTokens(usage) > 0 || usage.outputTokens > 0)) { - const cacheHit = cacheHitPercent(usage) - if (cacheHit !== null) groups.push(t('stats.cacheHit', { percent: cacheHit })) - groups.push(t('stats.tokens', { - input: formatTokens(billedInputTokens(usage), t), - output: formatTokens(usage.outputTokens, t), - })) - } - const line = groups.join(' | ') - if (groups.length === 0) return null - return -}) diff --git a/packages/client/ui-chat/src/client/chat/StatsPills.module.css b/packages/client/ui-chat/src/client/chat/StatsPills.module.css new file mode 100644 index 0000000000..a3e9d5a901 --- /dev/null +++ b/packages/client/ui-chat/src/client/chat/StatsPills.module.css @@ -0,0 +1,63 @@ +/* Two icon pills side by side under the composer, 13/20 tertiary text tier + with a hover-pill affordance; each pill's click-open panel wears the shared + stat-dialog.module.css skin. */ + +.root { + display: flex; + justify-content: center; + gap: 12px; + max-width: var(--dsh-chat-content-width); + width: 100%; + margin: 0 auto; + box-sizing: border-box; + padding: 4px calc(var(--dsh-composer-side-clearance) + 16px) 0px; + font-size: var(--dsh-content-font-size-secondary, 13px); + line-height: calc(20px + var(--dsh-content-font-delta-secondary, 0px)); +} + +/* Dialog anchor: shrink-wraps its pill so the panel clamp measures the pill. */ +.anchor { + display: inline-flex; + min-width: 0; +} + +.pill { + display: inline-flex; + align-items: center; + gap: 6px; + box-sizing: border-box; + max-width: 100%; + padding: 1px 8px; + border: none; + border-radius: 24px; + background: transparent; + color: var(--dsw-alias-label-tertiary); + font: inherit; + font-variant-numeric: tabular-nums; + line-height: inherit; + white-space: nowrap; + cursor: pointer; +} + +.pill svg { + width: 14px; + height: 14px; + flex: none; +} + +.pill:hover, +.pill[aria-expanded='true'] { + background: var(--dsw-alias-interactive-bg-hover); + color: var(--dsw-alias-label-secondary); +} + +.label { + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; +} + +.sep { + color: var(--dsw-alias-separator-primary); + margin: 0 6px; +} diff --git a/packages/client/ui-chat/src/client/chat/StatsPills.tsx b/packages/client/ui-chat/src/client/chat/StatsPills.tsx new file mode 100644 index 0000000000..14f4f8623c --- /dev/null +++ b/packages/client/ui-chat/src/client/chat/StatsPills.tsx @@ -0,0 +1,299 @@ +// Session stats under the composer, split into two icon pills: a gauge pill +// (turn/step counts + output speed) opening the time-and-speed dialog, and a +// database pill (total tokens + cache hit) opening the token-usage dialog. +// Settled-node identity prevents stream-delta updates from rerendering the row. +// Mounted on 'conversation.composer.dock' so it sticks with the composer in the +// active conversation scrollport (see ConversationRoot data-conversation-scroll). + +import { memo, useMemo } from 'react' +import { createPortal } from 'react-dom' +import { IconDatabaseOutline16, IconGaugeOutline16 } from '@deepseek-ai/dsh-client-ui-primitives' +import type { UseProjection } from '@deepseek-ai/dsh-api-session-controller/client' +import type { SnapshotSelectorHook } from '@deepseek-ai/dsh-client-ui-slots' +// Type-only: merges the sessionStats key into SessionProjectionMap for useProjection. +import type {} from '@deepseek-ai/dsh-session-stats/client' +import type { TokenUsageProjection } from '@deepseek-ai/dsh-token-meter/client' +import type { ChatViewSlotProps } from '../contract/slots.ts' +import type { ChatSnapshot } from '../contract/snapshot.ts' +import { formatTokensPerSecond } from './message-chrome.ts' +import { assistantStepReading } from '../contract/turn-metrics.ts' +import { formatCacheHitPercent, formatExactTokens, formatTokens } from './token-format.ts' +import { MEASURE_STYLE, useStatDialog } from './stat-dialog.ts' +import css from './StatsPills.module.css' +import dialogCss from './stat-dialog.module.css' + +interface WindowStats { + turns: number + steps: number + /** Summed request wall time (step/start → assistant/message); 0 when no node carries timing. */ + llmMs: number + /** Summed tool wall time (tool/call → tool/result); 0 when no pair is in-window. */ + toolMs: number + /** Summed first-token latency over `ttftSteps`; 0 when no step records it. */ + ttftMs: number + /** Steps carrying a recorded TTFT. */ + ttftSteps: number + /** Summed decode wall time over steps that also report output tokens. */ + decodeMs: number + /** Summed output tokens over the same decode-timed steps. */ + decodeTokens: number +} + +/** + * Fold assistant and tool-result nodes into window-scoped display totals — + * the FALLBACK for assemblies without the `sessionStats` projection. + * + * Every displayed figure rides that durable whole-log projection (and token + * accounting rides `tokenUsage`) because the window is paged and compaction + * rewrites it; this fold answers "what is on screen" only when no projection + * value is served. Its field names deliberately mirror the projection's so + * the two swap wholesale. + * @param nodes - snapshot nodes. + * @returns fallback counts and summed wall times. + */ +export function deriveStats(nodes: ChatSnapshot['legacy']['nodes']): WindowStats { + const turns = new Set() + let steps = 0 + let llmMs = 0 + let toolMs = 0 + let ttftMs = 0 + let ttftSteps = 0 + let decodeMs = 0 + let decodeTokens = 0 + for (const node of nodes) { + if (node.kind === 'tool-result') { + if (node.callTime !== null) toolMs += Math.max(0, node.time - node.callTime) + continue + } + if (node.kind !== 'assistant') continue + turns.add(node.turn) + steps += 1 + if (node.timing !== undefined && node.timing.stepStartTime !== null) { + llmMs += Math.max(0, node.timing.completedTime - node.timing.stepStartTime) + } + const reading = assistantStepReading(node) + if (reading.ttftMs !== null) { + ttftMs += reading.ttftMs + ttftSteps += 1 + } + if (reading.decodeMs !== null && reading.outputTokens !== null) { + decodeMs += reading.decodeMs + decodeTokens += reading.outputTokens + } + } + return { turns: turns.size, steps, llmMs, toolMs, ttftMs, ttftSteps, decodeMs, decodeTokens } +} + +/** + * Compact duration: 45.2s under a minute, 2m42s from there on. + * @param ms - duration in milliseconds. + * @returns display string. + */ +export function formatDuration(ms: number, t: ChatViewSlotProps['t']): string { + const s = ms / 1_000 + if (s < 60) return t('duration.compactSeconds', { seconds: Math.round(s * 10) / 10 }) + const whole = Math.round(s) + return t('duration.compactMinutes', { + minutes: Math.floor(whole / 60), + seconds: whole % 60, + }) +} + +/** + * Display-ready cache-hit share of prompt-side input over the whole durable log. + * @param usage - the session's token-usage projection value. + * @returns integer text when integer rounding stays below 100, otherwise the + * minimum decimal precision that still rounds below 100; a full hit returns + * 100, and no billed input returns null. + */ +export function cacheHitPercent(usage: TokenUsageProjection): string | null { + const denominator = billedInputTokens(usage) + return formatCacheHitPercent(usage.cacheReadTokens, denominator) +} + +/** + * Sum the three disjoint prompt-side billing buckets. + * @param usage - the session's token-usage projection value. + * @returns billed input tokens. + */ +export function billedInputTokens(usage: TokenUsageProjection): number { + return usage.uncachedInputTokens + usage.cacheReadTokens + usage.cacheWriteTokens +} + +/** Props: the conversation-snapshot selector plus the projection read seat. */ +export interface StatsPillsProps { + useChat: SnapshotSelectorHook + useProjection: UseProjection + /** The owning dock's locale seat. */ + t: ChatViewSlotProps['t'] +} + +function exactCount(value: number, t: ChatViewSlotProps['t']): string { + return t('message.turnUsage.count', { count: formatExactTokens(value, t) }) +} + +function TimePill({ stats, t }: { stats: WindowStats; t: ChatViewSlotProps['t'] }) { + const { open, setOpen, rootRef, panelRef, pos } = useStatDialog() + return ( + + + {open && createPortal( +
+
+ + + {t('stats.dialog.title')} + +
+
+
+ {stats.llmMs > 0 && ( + <> +
{t('stats.dialog.llmTime')}
+
{formatDuration(stats.llmMs, t)}
+ + )} + {stats.toolMs > 0 && ( + <> +
{t('stats.dialog.toolTime')}
+
{formatDuration(stats.toolMs, t)}
+ + )} + {stats.ttftSteps > 0 && ( + <> +
{t('stats.dialog.ttft')}
+
{formatDuration(stats.ttftMs / stats.ttftSteps, t)}
+ + )} + {stats.decodeMs > 0 && ( + <> +
{t('stats.dialog.speed')}
+
{t('message.tokensPerSecond', { + tps: formatTokensPerSecond(stats.decodeTokens / (stats.decodeMs / 1_000)), + })}
+ + )} +
+
, + document.body, + )} + + ) +} + +function UsagePill({ usage, t }: { usage: TokenUsageProjection; t: ChatViewSlotProps['t'] }) { + const { open, setOpen, rootRef, panelRef, pos } = useStatDialog() + // Same aggregate as the Turn pill's totalTokens: every prompt-side billing bucket plus output. + const total = billedInputTokens(usage) + usage.outputTokens + const cacheHit = cacheHitPercent(usage) + return ( + + + {open && createPortal( +
+
+ + + {t('stats.dialog.usageTitle')} + + {exactCount(total, t)} +
+
+ {/* jscpd:ignore-start -- the session-total bucket rows deliberately mirror + TurnUsagePanel's per-turn dl: same skin, different data contract (all + buckets always present here; per-turn fields are optional). */} +
+ {cacheHit !== null && ( + <> +
{t('message.turnUsage.cacheHit')}
+
{`${cacheHit}%`}
+ + )} +
{t('message.turnUsage.input')}
+
{exactCount(usage.uncachedInputTokens, t)}
+
{t('message.turnUsage.cacheRead')}
+
{exactCount(usage.cacheReadTokens, t)}
+
{t('message.turnUsage.cacheWrite')}
+
{exactCount(usage.cacheWriteTokens, t)}
+
{t('message.turnUsage.output')}
+
{exactCount(usage.outputTokens, t)}
+
+ {/* jscpd:ignore-end */} +
, + document.body, + )} + + ) +} + +export const StatsPills = memo(function StatsPills({ useChat, useProjection, t }: StatsPillsProps) { + const settledNodes = useChat(s => s.legacy.nodes) + const usage = useProjection('tokenUsage') + // Every figure rides the durable sessionStats projection, so paging and + // compaction cannot change any of them; an assembly without the unit falls + // back to the window-scoped fold wholesale (same field names), paid only + // while no projection value is served. + const projected = useProjection('sessionStats') + const stats = useMemo(() => projected ?? deriveStats(settledNodes), [projected, settledNodes]) + // Gated on actual token activity: a session whose steps all settled without + // billing (e.g. every request failed) shows its counts without a usage pill. + const hasTokens = usage !== undefined + && (billedInputTokens(usage) > 0 || usage.outputTokens > 0) + if (stats.steps === 0 && !hasTokens) return null + // data-composer-stats: InputBar's `.root:has([data-composer-stats])` rule + // tightens the composer's bottom clearance only while this row renders. + return ( +
+ {stats.steps > 0 && } + {hasTokens && } +
+ ) +}) diff --git a/packages/client/ui-chat/src/client/chat/TurnUsagePanel.module.css b/packages/client/ui-chat/src/client/chat/TurnUsagePanel.module.css index 7cc7c639eb..52058f8be7 100644 --- a/packages/client/ui-chat/src/client/chat/TurnUsagePanel.module.css +++ b/packages/client/ui-chat/src/client/chat/TurnUsagePanel.module.css @@ -1,6 +1,5 @@ -/* Icon-row Turn-usage pill (data icon + turn total) plus its click-open - Turn-details dialog (menu surface: r12, elevation-prominent — - ContextMeter's panel skin). */ +/* Icon-row Turn-usage pill (data icon + turn total); its click-open dialog + wears the shared stat-dialog.module.css skin. */ .root { display: inline-flex; @@ -74,93 +73,3 @@ margin-left: 0; } } - -/* Portal surface: fixed in the viewport, left/top supplied inline from the - anchored-position clamp so the panel keeps its 12px viewport margin instead - of hanging off the trigger and clipping at the window edge. Portaled panels - layer above modal overlays (z 1000). */ -.panel { - position: fixed; - z-index: 1100; - box-sizing: border-box; - /* Size to the widest row so provider/model stays on one line, within a cap. - Both bounds yield to a viewport narrower than themselves (12px margins), - so a fixed floor cannot push the panel past the placement clamp. */ - width: max-content; - min-width: min(300px, calc(100vw - 24px)); - max-width: min(440px, calc(100vw - 24px)); - padding: 16px; - border: 0; - border-radius: 12px; - background: var(--dsw-specific-menu); - --dsw-elevation-stroke-color: var(--dsw-alias-border-l1); - box-shadow: var(--dsw-elevation-prominent); - font-size: 12px; - line-height: 18px; - color: var(--dsw-alias-label-secondary); - cursor: default; -} - -/* Dialog heading row: the section name left, the section's headline value - (the usage total) right, both at the primary weight. */ -.title { - display: flex; - justify-content: space-between; - gap: 16px; - margin-bottom: 8px; - color: var(--dsw-alias-label-primary); - font-weight: 500; -} - -/* Rule under each section heading, above its rows. */ -.titleRule { - margin-bottom: 10px; - border-top: 0.5px solid var(--dsw-alias-border-l2); -} - -.titleValue { - font-variant-numeric: tabular-nums; -} - -/* Section-heading glyph seated left of the section name. */ -.titleLabel { - display: inline-flex; - align-items: center; - gap: 6px; - min-width: 0; -} - -.titleLabel svg { - width: 14px; - height: 14px; - flex: none; -} - -.details { - display: grid; - grid-template-columns: minmax(76px, auto) minmax(0, 1fr); - gap: 6px 16px; - margin: 0; - color: var(--dsw-alias-label-tertiary); -} - -.details dt, -.details dd { - min-width: 0; - margin: 0; -} - -.details dd { - color: var(--dsw-alias-label-secondary); - font-variant-numeric: tabular-nums; - text-align: right; -} - -.details .route { - overflow-wrap: anywhere; -} - -.reasoning { - color: var(--dsw-alias-label-tertiary); - white-space: nowrap; -} diff --git a/packages/client/ui-chat/src/client/chat/TurnUsagePanel.tsx b/packages/client/ui-chat/src/client/chat/TurnUsagePanel.tsx index b3cd2c12a7..0d64ff9e08 100644 --- a/packages/client/ui-chat/src/client/chat/TurnUsagePanel.tsx +++ b/packages/client/ui-chat/src/client/chat/TurnUsagePanel.tsx @@ -3,16 +3,15 @@ // turn wall time click-opens the Turn-time dialog. Both sit right of the // branch action in the tail's IconActions row, ahead of the plain clock text. -import { useEffect, useRef, useState, type CSSProperties, type MutableRefObject } from 'react' import { createPortal } from 'react-dom' -import { - IconClockOutline16, IconDatabaseOutline16, useAnchoredPosition, useDismissOnOutsidePointer, -} from '@deepseek-ai/dsh-client-ui-primitives' +import { IconClockOutline16, IconDatabaseOutline16 } from '@deepseek-ai/dsh-client-ui-primitives' import type { TurnTokenUsage } from '../contract/chat-nodes.ts' import type { ChatViewSlotProps } from '../contract/slots.ts' import { formatLatencySeconds, formatRunDuration, formatTokensPerSecond } from './message-chrome.ts' import { formatCacheHitPercent, formatExactTokens, formatTokens } from './token-format.ts' +import { MEASURE_STYLE, useStatDialog } from './stat-dialog.ts' import css from './TurnUsagePanel.module.css' +import dialogCss from './stat-dialog.module.css' export interface TurnUsagePanelProps { usage: TurnTokenUsage @@ -39,58 +38,6 @@ function formatExactCount(value: number, t: ChatViewSlotProps['t']): string { return t('message.turnUsage.count', { count: formatExactTokens(value, t) }) } -/** Viewport margin the placement clamp keeps (the Menu portal margin). */ -const PANEL_MARGIN = 12 - -/** Distance between the trigger's top edge and the panel's bottom. */ -const PANEL_GAP = 8 - -/** - * Unplaced portal panel: hidden but laid out so the clamp measures real - * dimensions (the `useAnchoredPosition` measure pass). - */ -const MEASURE_STYLE: CSSProperties = { visibility: 'hidden', left: 0, top: 0 } - -interface StatDialogSeat { - open: boolean - setOpen: (open: boolean) => void - rootRef: MutableRefObject - panelRef: MutableRefObject - pos: CSSProperties | null -} - -/** One trigger-anchored dialog seat: open state, viewport-clamped placement, outside-close. */ -function useStatDialog(): StatDialogSeat { - const [open, setOpen] = useState(false) - const rootRef = useRef(null) - const panelRef = useRef(null) - - // Portal placement: the dialog is fixed above the trigger and clamped inside - // the viewport, so a trigger near the window edge cannot push it off-screen. - const pos = useAnchoredPosition({ - open, - anchorRef: rootRef, - panelRef, - side: 'top', - gap: PANEL_GAP, - margin: PANEL_MARGIN, - }) - - // Outside pointerdown closes through the shared primitive; the portaled - // panel counts as inside. Escape close stays local, one listener while open. - useDismissOnOutsidePointer(rootRef, open, setOpen, panelRef) - useEffect(() => { - if (!open) return - const onKeyDown = (e: KeyboardEvent): void => { - if (e.key === 'Escape') setOpen(false) - } - document.addEventListener('keydown', onKeyDown) - return () => { document.removeEventListener('keydown', onKeyDown) } - }, [open]) - - return { open, setOpen, rootRef, panelRef, pos } -} - /** * Turn-usage IconActions pill with a click-open Turn-usage details dialog. * @param props - Turn usage buckets and locale seat. @@ -120,24 +67,24 @@ export function TurnUsagePanel({ usage, t }: TurnUsagePanelProps) { {open && createPortal(
-
- +
+ {t('message.turnUsage.title')} - {formatExactCount(usage.totalTokens, t)} + {formatExactCount(usage.totalTokens, t)}
-
-
+
+
{routes !== '' && ( <>
{t('message.turnUsage.model')}
-
{routes}
+
{routes}
)} {cacheHit !== null && ( @@ -164,7 +111,7 @@ export function TurnUsagePanel({ usage, t }: TurnUsagePanelProps) {
{formatExactCount(usage.outputTokens, t)} {usage.reasoningTokens !== undefined && ( - + {t('message.turnUsage.reasoning', { tokens: formatExactCount(usage.reasoningTokens, t) })} )} @@ -199,19 +146,19 @@ export function TurnTimePanel({ runMs, tokensPerSecond, ttftMs, t }: TurnTimePan {open && createPortal(
-
- +
+ {t('message.turnTime.title')}
-
-
+
+
{t('message.turnTime.duration')}
{formatRunDuration(runMs, t)}
{tokensPerSecond !== undefined && ( diff --git a/packages/client/ui-chat/src/client/chat/stat-dialog.module.css b/packages/client/ui-chat/src/client/chat/stat-dialog.module.css new file mode 100644 index 0000000000..cc30711a68 --- /dev/null +++ b/packages/client/ui-chat/src/client/chat/stat-dialog.module.css @@ -0,0 +1,94 @@ +/* Stat-dialog surface skin shared by the Turn-stat pills (TurnUsagePanel) and + the session stats pills (StatsPills): menu surface, r12, elevation-prominent + — ContextMeter's panel skin. Placement comes from useStatDialog + (stat-dialog.ts). */ + +/* Portal surface: fixed in the viewport, left/top supplied inline from the + anchored-position clamp so the panel keeps its 12px viewport margin instead + of hanging off the trigger and clipping at the window edge. Portaled panels + layer above modal overlays (z 1000). */ +.panel { + position: fixed; + z-index: 1100; + box-sizing: border-box; + /* Size to the widest row so provider/model stays on one line, within a cap. + Both bounds yield to a viewport narrower than themselves (12px margins), + so a fixed floor cannot push the panel past the placement clamp. */ + width: max-content; + min-width: min(300px, calc(100vw - 24px)); + max-width: min(440px, calc(100vw - 24px)); + padding: 16px; + border: 0; + border-radius: 12px; + background: var(--dsw-specific-menu); + --dsw-elevation-stroke-color: var(--dsw-alias-border-l1); + box-shadow: var(--dsw-elevation-prominent); + font-size: 12px; + line-height: 18px; + color: var(--dsw-alias-label-secondary); + cursor: default; +} + +/* Dialog heading row: the section name left, the section's headline value + right, both at the primary weight. */ +.title { + display: flex; + justify-content: space-between; + gap: 16px; + margin-bottom: 8px; + color: var(--dsw-alias-label-primary); + font-weight: 500; +} + +/* Rule under each section heading, above its rows. */ +.titleRule { + margin-bottom: 10px; + border-top: 0.5px solid var(--dsw-alias-border-l2); +} + +.titleValue { + font-variant-numeric: tabular-nums; +} + +/* Section-heading glyph seated left of the section name. */ +.titleLabel { + display: inline-flex; + align-items: center; + gap: 6px; + min-width: 0; +} + +.titleLabel svg { + width: 14px; + height: 14px; + flex: none; +} + +.details { + display: grid; + grid-template-columns: minmax(76px, auto) minmax(0, 1fr); + gap: 6px 16px; + margin: 0; + color: var(--dsw-alias-label-tertiary); +} + +.details dt, +.details dd { + min-width: 0; + margin: 0; +} + +.details dd { + color: var(--dsw-alias-label-secondary); + font-variant-numeric: tabular-nums; + text-align: right; +} + +.details .route { + overflow-wrap: anywhere; +} + +.reasoning { + color: var(--dsw-alias-label-tertiary); + white-space: nowrap; +} diff --git a/packages/client/ui-chat/src/client/chat/stat-dialog.ts b/packages/client/ui-chat/src/client/chat/stat-dialog.ts new file mode 100644 index 0000000000..0d4d910bfa --- /dev/null +++ b/packages/client/ui-chat/src/client/chat/stat-dialog.ts @@ -0,0 +1,62 @@ +// One trigger-anchored stat dialog seat shared by the Turn-stat pills +// (TurnUsagePanel) and the composer-dock session stats pills (StatsPills); +// the matching dialog surface skin lives in stat-dialog.module.css. + +import { useEffect, useRef, useState, type CSSProperties, type MutableRefObject } from 'react' +import { useAnchoredPosition, useDismissOnOutsidePointer } from '@deepseek-ai/dsh-client-ui-primitives' + +/** Viewport margin the placement clamp keeps (the Menu portal margin). */ +const PANEL_MARGIN = 12 + +/** Distance between the trigger's top edge and the panel's bottom. */ +const PANEL_GAP = 8 + +/** + * Unplaced portal panel: hidden but laid out so the clamp measures real + * dimensions (the `useAnchoredPosition` measure pass). + */ +export const MEASURE_STYLE: CSSProperties = { visibility: 'hidden', left: 0, top: 0 } + +/** Open state, refs, and clamped placement for one stat dialog. */ +export interface StatDialogSeat { + open: boolean + setOpen: (open: boolean) => void + rootRef: MutableRefObject + panelRef: MutableRefObject + pos: CSSProperties | null +} + +/** + * One trigger-anchored dialog seat: open state, viewport-clamped placement, outside-close. + * @returns the seat; spread `pos ?? MEASURE_STYLE` onto the portaled panel. + */ +export function useStatDialog(): StatDialogSeat { + const [open, setOpen] = useState(false) + const rootRef = useRef(null) + const panelRef = useRef(null) + + // Portal placement: the dialog is fixed above the trigger and clamped inside + // the viewport, so a trigger near the window edge cannot push it off-screen. + const pos = useAnchoredPosition({ + open, + anchorRef: rootRef, + panelRef, + side: 'top', + gap: PANEL_GAP, + margin: PANEL_MARGIN, + }) + + // Outside pointerdown closes through the shared primitive; the portaled + // panel counts as inside. Escape close stays local, one listener while open. + useDismissOnOutsidePointer(rootRef, open, setOpen, panelRef) + useEffect(() => { + if (!open) return + const onKeyDown = (e: KeyboardEvent): void => { + if (e.key === 'Escape') setOpen(false) + } + document.addEventListener('keydown', onKeyDown) + return () => { document.removeEventListener('keydown', onKeyDown) } + }, [open]) + + return { open, setOpen, rootRef, panelRef, pos } +} diff --git a/packages/client/ui-chat/src/client/contract/snapshot.ts b/packages/client/ui-chat/src/client/contract/snapshot.ts index c7544d4a16..0d02198811 100644 --- a/packages/client/ui-chat/src/client/contract/snapshot.ts +++ b/packages/client/ui-chat/src/client/contract/snapshot.ts @@ -79,7 +79,7 @@ export interface ChatTurnProcessPresentation { readonly compactAnswer: boolean } -/** Compatibility projection backing StatsLine and the legacy top-level snapshot fields. */ +/** Compatibility projection backing StatsPills and the legacy top-level snapshot fields. */ export interface LegacyConversationSlice { readonly nodes: readonly ConversationNode[] readonly turnTimings: ReadonlyMap diff --git a/packages/client/ui-chat/src/client/contract/turn-metrics.ts b/packages/client/ui-chat/src/client/contract/turn-metrics.ts index 5880a92af5..a9f4dbc7c0 100644 --- a/packages/client/ui-chat/src/client/contract/turn-metrics.ts +++ b/packages/client/ui-chat/src/client/contract/turn-metrics.ts @@ -1,4 +1,4 @@ -// Latency/throughput folds shared by the settled turn footer and StatsLine. +// Latency/throughput folds shared by the settled turn footer and StatsPills. import type { AssistantMessageNode, ConversationNode, diff --git a/packages/client/ui-chat/src/client/conversation-nodes/chat-snapshot-builder.ts b/packages/client/ui-chat/src/client/conversation-nodes/chat-snapshot-builder.ts index 04cb38bc94..fb91233786 100644 --- a/packages/client/ui-chat/src/client/conversation-nodes/chat-snapshot-builder.ts +++ b/packages/client/ui-chat/src/client/conversation-nodes/chat-snapshot-builder.ts @@ -730,7 +730,7 @@ const EMPTY_CONTRIBUTION: LegacyContribution = { function legacyContribution(raw: ChatConversationViewNode): LegacyContribution { const node = raw as ChatNode // Content-free settled Assistants remain in the finalized compatibility - // stream so StatsLine preserves its pre-assembly step counts; hidden running + // stream so StatsPills preserves its pre-assembly step counts; hidden running // attempts have no final Node to contribute. if (raw.visibility !== 'visible' && node.kind !== 'assistant-step') return EMPTY_CONTRIBUTION switch (node.kind) { @@ -802,7 +802,7 @@ function sameContribution(left: LegacyContribution | undefined, right: LegacyCon && sameReferences(left.nodes, right.nodes) } -/** Incremental compatibility projection for StatsLine and legacy top-level snapshot fields. */ +/** Incremental compatibility projection for StatsPills and legacy top-level snapshot fields. */ class LegacySliceBuilder { private readonly contributions = new Map() private readonly finalizedContributions = new Map() diff --git a/packages/client/ui-chat/src/client/locale.ts b/packages/client/ui-chat/src/client/locale.ts index f10a9ced20..72a0b9c74e 100644 --- a/packages/client/ui-chat/src/client/locale.ts +++ b/packages/client/ui-chat/src/client/locale.ts @@ -10,13 +10,14 @@ export const zh = { 'duration.compactSeconds': '{seconds}秒', 'duration.compactMinutes': '{minutes}分{seconds}秒', 'duration.milliseconds': '{milliseconds}毫秒', - 'stats.counts': '{turns} 轮 · {steps} 步', - 'stats.llm': 'LLM {duration}', - 'stats.toolCall': '工具调用 {duration}', - 'stats.ttftAverage': '首 token 平均 {duration}', - 'stats.tokensPerSecond': '{throughput} tok/s', + 'stats.counts': '{turns} 轮 {steps} 步', 'stats.cacheHit': '缓存命中 {percent}%', - 'stats.tokens': '输入 {input} tok · 输出 {output} tok', + 'stats.dialog.title': '会话统计', + 'stats.dialog.usageTitle': 'Token 用量', + 'stats.dialog.llmTime': '模型用时', + 'stats.dialog.toolTime': '工具调用用时', + 'stats.dialog.ttft': '首 token 平均(TTFT)', + 'stats.dialog.speed': '输出速度(TPS)', 'details.title': '详情', 'details.close': '关闭详情', 'details.empty': '点击消息流中的工具行查看详情', @@ -127,13 +128,14 @@ export const en = { 'duration.compactSeconds': '{seconds}s', 'duration.compactMinutes': '{minutes}m{seconds}s', 'duration.milliseconds': '{milliseconds}ms', - 'stats.counts': '{turns} turns · {steps} steps', - 'stats.llm': 'LLM {duration}', - 'stats.toolCall': 'Tool call {duration}', - 'stats.ttftAverage': 'TTFT avg {duration}', - 'stats.tokensPerSecond': '{throughput} tok/s', + 'stats.counts': '{turns} turns {steps} steps', 'stats.cacheHit': 'Cache hit {percent}%', - 'stats.tokens': 'Input {input} tok · Output {output} tok', + 'stats.dialog.title': 'Session statistics', + 'stats.dialog.usageTitle': 'Token usage', + 'stats.dialog.llmTime': 'LLM time', + 'stats.dialog.toolTime': 'Tool time', + 'stats.dialog.ttft': 'Avg time to first token (TTFT)', + 'stats.dialog.speed': 'Tokens per second (TPS)', 'details.title': 'Details', 'details.close': 'Close details', 'details.empty': 'Click a tool row in the message flow to view its details', diff --git a/packages/client/ui-chat/tests/chat-branch-tails.client.spec.tsx b/packages/client/ui-chat/tests/chat-branch-tails.client.spec.tsx index d9a77dd63b..8f80f2dc84 100644 --- a/packages/client/ui-chat/tests/chat-branch-tails.client.spec.tsx +++ b/packages/client/ui-chat/tests/chat-branch-tails.client.spec.tsx @@ -16,18 +16,10 @@ import { UserMessageNodeView, } from '../src/client/chat/MessageItem.tsx' import { AssistantMarkdown, type AssistantMarkdownProps } from '../src/client/chat/AssistantMarkdown.tsx' -import { StatsLine } from '../src/client/chat/StatsLine.tsx' +import { StatsPills } from '../src/client/chat/StatsPills.tsx' import { zh } from '../src/client/locale.ts' import { chatSnapshotFixture } from './chat-snapshot-fixture.client.ts' -/** jsdom has no ResizeObserver; StatsLine watches its row for ellipsis truncation through one. */ -class ResizeObserverStub { - observe(): void {} - unobserve(): void {} - disconnect(): void {} -} - -beforeEach(() => { vi.stubGlobal('ResizeObserver', ResizeObserverStub) }) afterEach(() => { cleanup() vi.useRealTimers() @@ -1042,7 +1034,7 @@ describe('small branch tails', () => { expect(view.getByText('one-liner')).toBeTruthy() }) - it('StatsLine omits the cache-hit segment when no input accounting exists at all', () => { + it('StatsPills omits the cache-hit segment when no input accounting exists at all', () => { // Cache hit is null only when all three prompt buckets are zero (pure // output accounting) — any billed input makes it a real 0%. const nodes = [{ @@ -1051,7 +1043,7 @@ describe('small branch tails', () => { const snap = chatSnapshotFixture({ nodes }) const source = { getSnapshot: () => snap, subscribe: () => () => {} } const view = render( - key === 'tokenUsage' @@ -1059,7 +1051,14 @@ describe('small branch tails', () => { : undefined} />, ) - expect(view.container.textContent).toBe('1 轮 · 1 步| 输入 0 tok · 输出 10 tok') + const [timePill, usagePill] = [...view.getAllByRole('button')] as [HTMLElement, HTMLElement] + expect(timePill.textContent).toBe('1 轮 1 步') + expect(usagePill.textContent).toBe('10 tok') + // Pure output accounting still reaches the usage pill's click-open dialog rows. + fireEvent.click(usagePill) + const dialog = view.getByRole('dialog') + expect(dialog.textContent).toContain('输出10 tok') + expect(dialog.textContent).not.toContain('缓存命中') }) }) diff --git a/packages/client/ui-chat/tests/chat-stats.client.spec.tsx b/packages/client/ui-chat/tests/chat-stats.client.spec.tsx index e35aa297dc..dc4a05984e 100644 --- a/packages/client/ui-chat/tests/chat-stats.client.spec.tsx +++ b/packages/client/ui-chat/tests/chat-stats.client.spec.tsx @@ -1,6 +1,6 @@ // @vitest-environment jsdom -import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' +import { afterEach, describe, expect, it, vi } from 'vitest' import { act, cleanup, fireEvent, render } from '@testing-library/react' import type { AssistantMessageNode, ChatSnapshot, LegacyConversationSlice, ToolResultNode, @@ -8,27 +8,17 @@ import type { import { bindSnapshotSelector, makeTranslate } from '@deepseek-ai/dsh-client-test-runtime' import { en as commonEn } from '@deepseek-ai/dsh-client-locale/src/locales/en.ts' import { zh as commonZh } from '@deepseek-ai/dsh-client-locale/src/locales/zh.ts' -import { StatsLine, deriveStats, formatDuration, type StatsLineProps } from '../src/client/chat/StatsLine.tsx' +import { StatsPills, deriveStats, formatDuration, type StatsPillsProps } from '../src/client/chat/StatsPills.tsx' import { formatTokens } from '../src/client/chat/token-format.ts' import { en, zh } from '../src/client/locale.ts' import { chatSnapshotFixture } from './chat-snapshot-fixture.client.ts' -const t: StatsLineProps['t'] = makeTranslate(zh, commonZh) -const tEn: StatsLineProps['t'] = makeTranslate(en, commonEn) +const t: StatsPillsProps['t'] = makeTranslate(zh, commonZh) +const tEn: StatsPillsProps['t'] = makeTranslate(en, commonEn) -/** jsdom has no ResizeObserver; StatsLine watches its row for ellipsis truncation through one. */ -class ResizeObserverStub { - observe(): void {} - unobserve(): void {} - disconnect(): void {} -} - -beforeEach(() => { vi.stubGlobal('ResizeObserver', ResizeObserverStub) }) afterEach(() => { cleanup() - vi.unstubAllGlobals() vi.restoreAllMocks() - vi.useRealTimers() }) const assistant = (seq: number, turn: number, usage?: unknown): AssistantMessageNode => ({ @@ -135,7 +125,7 @@ describe('formatters', () => { }) }) -describe('StatsLine', () => { +describe('StatsPills', () => { const USAGE = { uncachedInputTokens: 10, outputTokens: 5, cacheReadTokens: 90, cacheWriteTokens: 0 } /** A whole-log sessionStats value: zeros plus overrides. */ @@ -147,14 +137,14 @@ describe('StatsLine', () => { } /** Stub the projection seat: a key-addressed table of whole values. */ - function projections(values: Record): StatsLineProps['useProjection'] { + function projections(values: Record): StatsPillsProps['useProjection'] { return (key: string) => values[key] } function props( source: { getSnapshot(): ChatSnapshot; subscribe(fn: () => void): () => void }, values: Record = { tokenUsage: USAGE }, - ): StatsLineProps { + ): StatsPillsProps { return { useChat: bindSnapshotSelector(source), useProjection: projections(values), t: tEn } } @@ -162,37 +152,31 @@ describe('StatsLine', () => { return { uncachedInputTokens, outputTokens: 1, cacheReadTokens, cacheWriteTokens: 0 } } - it('mounts one size observer only while a non-empty line exists', () => { - let observers = 0 - class CountingResizeObserver { - constructor(_callback: ResizeObserverCallback) { observers++ } - observe(): void {} - disconnect(): void {} - } - vi.stubGlobal('ResizeObserver', CountingResizeObserver) - const { set, source } = makeSource() - render() - expect(observers).toBe(0) - - act(() => { set({ nodes: [assistant(1, 1)] }) }) - expect(observers).toBe(1) - - act(() => { set({ nodes: [assistant(1, 1), assistant(2, 1)] }) }) - expect(observers).toBe(1) + /** A step whose timing yields 3.8s LLM, 0.8s TTFT, and 20 tok/s over 60 tokens. */ + const timedStep = (): AssistantMessageNode => ({ + ...assistant(1, 1, { outputTokens: 60 }), + timing: { stepStartTime: 1_000, firstTokenTime: 1_800, completedTime: 4_800 }, }) - it('renders the grouped stats row and hides a brand-new empty session', () => { + it('renders the counts and usage pills and hides a brand-new empty session', () => { const { source } = makeSource({ nodes: [assistant(1, 1)] }) - const view = render() - // No timing on the fixture: the duration group drops out whole. Tokens come - // from the projection, so paging the window cannot change them. - expect(view.container.textContent).toBe('1 turns · 1 steps| Cache hit 90%| Input 100 tok · Output 5 tok') + const view = render() + // InputBar's `.root:has([data-composer-stats])` bottom-clearance rule keys + // off this attribute: present exactly while the row renders. + expect(view.container.querySelector('[data-composer-stats]')).toBeTruthy() + // No timing on the fixture: the speed segment drops out. Cache hit comes + // from the projection, so paging the window cannot change it; the usage + // pill leads with the whole-log token total. + expect(view.getAllByRole('button').map(pill => pill.textContent)).toEqual( + ['1 turns 1 steps', '105 tok·Cache hit 90%'], + ) const empty = makeSource() - const emptyView = render() expect(emptyView.container.textContent).toBe('') + expect(emptyView.container.querySelector('[data-composer-stats]')).toBeNull() }) it.each([ @@ -214,153 +198,206 @@ describe('StatsLine', () => { { actual: '100%', tokenUsageValue: tokenUsage(10_000, 0), expected: 'Cache hit 100%' }, ])('formats an actual $actual cache-hit ratio as $expected', ({ tokenUsageValue, expected }) => { const { source } = makeSource({ nodes: [assistant(1, 1)] }) - const view = render() - expect(view.container.textContent).toContain(expected) + const view = render() + expect(view.getAllByRole('button')[1]!.textContent).toContain(expected) }) - it('reveals the full line in a delayed hover tooltip only while the row is clipped', () => { - vi.useFakeTimers() - // jsdom lays nothing out; fake a row narrower than its content. - vi.spyOn(Element.prototype, 'scrollWidth', 'get').mockReturnValue(800) - vi.spyOn(Element.prototype, 'clientWidth', 'get').mockReturnValue(400) + it('exposes output speed on the counts pill when decode timing exists', () => { + const { source } = makeSource({ nodes: [timedStep()] }) + const view = render() + expect(view.getAllByRole('button')[0]!.textContent).toBe('1 turns 1 steps·20 tok/s') + }) + + it('click-opens the time-and-speed dialog carrying the time split and speeds', () => { + const { source } = makeSource({ nodes: [timedStep()] }) + const view = render() + + const timePill = view.getAllByRole('button')[0]! + expect(timePill.getAttribute('aria-haspopup')).toBe('dialog') + expect(timePill.getAttribute('aria-expanded')).toBe('false') + expect(view.queryByRole('dialog')).toBeNull() + + fireEvent.click(timePill) + expect(timePill.getAttribute('aria-expanded')).toBe('true') + const dialog = view.getByRole('dialog') + expect(dialog.getAttribute('aria-label')).toBe('Session statistics') + // Portaled out of the composer dock. + expect(dialog.parentElement).toBe(document.body) + expect(dialog.firstChild?.textContent).toBe('Session statistics') + const details = dialog.querySelector('[data-session-stats-details]') as HTMLElement + expect(details).toBeTruthy() + expect(details.textContent).toContain('LLM time3.8s') + // No tool call in this session: the row is absent, not zeroed. + expect(details.textContent).not.toContain('Tool time') + expect(details.textContent).toContain('Avg time to first token (TTFT)0.8s') + expect(details.textContent).toContain('Tokens per second (TPS)20 tok/s') + // Token accounting lives on the usage pill's own dialog, not here. + expect(dialog.textContent).not.toContain('Token usage') + }) + + it('click-opens the token-usage dialog carrying the headline total and exact buckets', () => { + const { source } = makeSource({ nodes: [timedStep()] }) + const view = render() + + const usagePill = view.getAllByRole('button')[1]! + fireEvent.click(usagePill) + expect(usagePill.getAttribute('aria-expanded')).toBe('true') + const dialog = view.getByRole('dialog') + expect(dialog.getAttribute('aria-label')).toBe('Token usage') + expect(dialog.parentElement).toBe(document.body) + // Headline total: all prompt-side buckets (10 + 90 + 0) plus output (5). + expect(dialog.firstChild?.textContent).toBe('Token usage105 tok') + const tokens = dialog.querySelector('[data-session-stats-usage]') as HTMLElement + expect(tokens).toBeTruthy() + expect(tokens.textContent).toContain('Cache hit90%') + expect(tokens.textContent).toContain('Uncached input10 tok') + expect(tokens.textContent).toContain('Cached input90 tok') + expect(tokens.textContent).toContain('Cache write0 tok') + expect(tokens.textContent).toContain('Output5 tok') + // The time split lives on the counts pill's own dialog, not here. + expect(dialog.textContent).not.toContain('LLM time') + }) + + it('closes the dialog on Escape or outside pointerdown', () => { const { source } = makeSource({ nodes: [assistant(1, 1)] }) - const view = render() - expect(view.container.textContent).toContain('Cache hit 99.95%') - fireEvent.mouseEnter(view.container.firstElementChild!) - act(() => { vi.advanceTimersByTime(499) }) - expect(view.container.querySelector('[role="tooltip"]')).toBeNull() - act(() => { vi.advanceTimersByTime(1) }) - expect(view.container.querySelector('[role="tooltip"]')?.textContent) - .toBe('1 turns · 1 steps | Cache hit 99.95% | Input 10K tok · Output 1 tok') + const view = render() + const timePill = view.getAllByRole('button')[0]! + + fireEvent.click(timePill) + expect(view.queryByRole('dialog')).toBeTruthy() + fireEvent.keyDown(document, { key: 'Escape' }) + expect(view.queryByRole('dialog')).toBeNull() + expect(timePill.getAttribute('aria-expanded')).toBe('false') + + fireEvent.click(timePill) + // A pointerdown inside the panel keeps it open; one outside closes it. + fireEvent.pointerDown(view.getByRole('dialog')) + expect(view.queryByRole('dialog')).toBeTruthy() + fireEvent.pointerDown(document.body) + expect(view.queryByRole('dialog')).toBeNull() }) - it('suppresses the tooltip while the row fits without truncation', () => { - vi.useFakeTimers() - const { source } = makeSource({ nodes: [assistant(1, 1)] }) - const view = render() - fireEvent.mouseEnter(view.container.firstElementChild!) - act(() => { vi.advanceTimersByTime(500) }) - expect(view.container.querySelector('[role="tooltip"]')).toBeNull() + it('takes every pill and dialog label from the active locale', () => { + const { source } = makeSource({ nodes: [timedStep()] }) + const view = render() + const [timePill, usagePill] = [...view.getAllByRole('button')] as [HTMLElement, HTMLElement] + expect(timePill.textContent).toBe('1 轮 1 步·20 tok/s') + // Whole-log total 9995 + 5 + 1 compacts to 10K. + expect(usagePill.textContent).toBe('10K tok·缓存命中 99.95%') + fireEvent.click(timePill) + const timeDialog = view.getByRole('dialog') + expect(timeDialog.getAttribute('aria-label')).toBe('会话统计') + expect(timeDialog.textContent).toContain('模型用时3.8秒') + expect(timeDialog.textContent).toContain('首 token 平均(TTFT)0.8秒') + expect(timeDialog.textContent).toContain('输出速度(TPS)20 tok/s') + fireEvent.keyDown(document, { key: 'Escape' }) + fireEvent.click(usagePill) + const usageDialog = view.getByRole('dialog') + expect(usageDialog.getAttribute('aria-label')).toBe('Token 用量') + expect(usageDialog.textContent).toContain('未缓存输入5 tok') }) - it('renders window latency and throughput beside the wall-time group', () => { - const timed: AssistantMessageNode = { - ...assistant(1, 1, { outputTokens: 60 }), - timing: { stepStartTime: 1_000, firstTokenTime: 1_800, completedTime: 4_800 }, - } - const { source } = makeSource({ nodes: [timed] }) - const view = render() - expect(view.container.textContent).toContain('LLM 3.8s| TTFT avg 0.8s · 20 tok/s') - }) - - it('takes every stats label from the active locale', () => { - const timed: AssistantMessageNode = { - ...assistant(1, 1, { outputTokens: 60 }), - timing: { stepStartTime: 1_000, firstTokenTime: 1_800, completedTime: 4_800 }, - } - const { source } = makeSource({ nodes: [timed] }) - const view = render() - expect(view.container.textContent) - .toBe('1 轮 · 1 步| LLM 3.8秒| 首 token 平均 0.8秒 · 20 tok/s| 缓存命中 99.95%| 输入 10K tok · 输出 1 tok') - }) - - it('renders without ResizeObserver support', () => { - vi.unstubAllGlobals() - const { source } = makeSource({ nodes: [assistant(1, 1)] }) - expect(() => render()).not.toThrow() - }) - - it('keeps durable token groups after the visible step window is empty', () => { + it('keeps the durable usage pill after the visible step window is empty', () => { const { source } = makeSource() - const view = render() // Context occupancy lives on the composer's ContextMeter ring, not here. - expect(view.container.textContent) - .toBe('Cache hit 90%| Input 100 tok · Output 5 tok') + const pills = view.getAllByRole('button') + expect(pills).toHaveLength(1) + expect(pills[0]!.textContent).toBe('105 tok·Cache hit 90%') }) - it('drops every token group when no projection is composed', () => { + it('drops the usage pill when no projection is composed', () => { const { source } = makeSource({ nodes: [assistant(1, 1)] }) - const view = render() - expect(view.container.textContent).toBe('1 turns · 1 steps') + const view = render() + expect(view.container.textContent).toBe('1 turns 1 steps') + expect(view.getAllByRole('button')).toHaveLength(1) }) it('renders whole-session counts from the sessionStats projection over the paged window', () => { // The bug's acceptance at unit level: one loaded page must not scope the // counter — the durable projection's totals win over the window fold. const { source } = makeSource({ nodes: [assistant(1, 1)] }) - const view = render() - expect(view.container.textContent) - .toBe('10 turns · 89 steps| Cache hit 90%| Input 100 tok · Output 5 tok') + expect(view.getAllByRole('button')[0]!.textContent).toBe('10 turns 89 steps') }) it('treats a defined zero-count projection as empty, not as fallback', () => { // A composed unit always serves the key; all-zero genuinely means no // closed step in the whole log, so nothing renders on a brand-new session. const empty = makeSource() - const view = render() expect(view.container.textContent).toBe('') }) - it('hides the zero-token group when steps closed without any billed activity', () => { + it('hides the usage pill when steps closed without any billed activity', () => { // A session whose only turn failed before billing (e.g. an auth error): - // the counts group renders alone, not an uninformative zero-token group. + // the counts pill renders alone, not an uninformative zero-token pill. const { source } = makeSource() - const view = render() - expect(view.container.textContent).toBe('1 turns · 1 steps') + expect(view.container.textContent).toBe('1 turns 1 steps') + expect(view.getAllByRole('button')).toHaveLength(1) }) - it('keeps the counts group over an empty visible window when the projection carries totals', () => { + it('keeps the counts pill over an empty visible window when the projection carries totals', () => { // Extends the durable-groups guarantee: full-session counts survive a // window that compaction (or paging) left without assistant nodes. const { source } = makeSource() - const view = render() - expect(view.container.textContent) - .toBe('7 turns · 44 steps| Cache hit 90%| Input 100 tok · Output 5 tok') + expect(view.getAllByRole('button')[0]!.textContent).toBe('7 turns 44 steps') }) - it('renders whole-log wall times and speeds from the projection, not the loaded window', () => { - // The 加载更早 hazard beyond counts: LLM/tool durations and the TTFT and - // throughput figures must not grow per loaded page either. An untimed - // 1-node window renders the projection's whole-log figures verbatim. + it('renders whole-log speed and dialog figures from the projection, not the loaded window', () => { + // The 加载更早 hazard beyond counts: the pill's speed segment and the + // dialog's time split, TTFT, and throughput must not grow per loaded page + // either. An untimed 1-node window renders the projection's whole-log figures. const { source } = makeSource({ nodes: [assistant(1, 1)] }) - const view = render() - expect(view.container.textContent).toBe( - '200 turns · 200 steps| LLM 1m40s · Tool call 1m2s| TTFT avg 0.8s · 20 tok/s| Cache hit 90%| Input 100 tok · Output 5 tok', - ) + const timePill = view.getAllByRole('button')[0]! + expect(timePill.textContent).toBe('200 turns 200 steps·20 tok/s') + fireEvent.click(timePill) + const dialog = view.getByRole('dialog') + expect(dialog.textContent).toContain('LLM time1m40s') + expect(dialog.textContent).toContain('Tool time1m2s') + expect(dialog.textContent).toContain('Avg time to first token (TTFT)0.8s') + expect(dialog.textContent).toContain('Tokens per second (TPS)20 tok/s') }) - it('omits cache hit when nothing was billed on the input side', () => { + it('omits the cache-hit segment when nothing was billed on the input side', () => { const { source } = makeSource({ nodes: [assistant(1, 1)] }) - const view = render() - expect(view.container.textContent).toBe('1 turns · 1 steps| Input 0 tok · Output 7 tok') + const usagePill = view.getAllByRole('button')[1]! + expect(usagePill.textContent).toBe('7 tok') + // Output-only activity still fills the dialog's token rows. + fireEvent.click(usagePill) + expect(view.getByRole('dialog').textContent).toContain('Output7 tok') }) - it('includes cache writes in billed input and the cache-hit denominator', () => { + it('includes cache writes in the total and the cache-hit denominator', () => { const { source } = makeSource({ nodes: [assistant(1, 1)] }) - const view = render( { cacheWriteTokens: 100, }, })} />) - expect(view.container.textContent) - .toBe('1 turns · 1 steps| Cache hit 45%| Input 200 tok · Output 7 tok') + expect(view.getAllByRole('button')[1]!.textContent).toBe('207 tok·Cache hit 45%') }) it('renders ZERO times during streaming chunk frames (RFC hard acceptance)', () => { const { set, source } = makeSource({ nodes: [assistant(1, 1)] }) let renders = 0 - function Counting(p: StatsLineProps) { + function Counting(p: StatsPillsProps) { renders += 1 - return + return } render() const before = renders diff --git a/packages/client/ui-chat/tests/gate-branch-tails.client.spec.tsx b/packages/client/ui-chat/tests/gate-branch-tails.client.spec.tsx index 9f7d3dd062..dbe280b840 100644 --- a/packages/client/ui-chat/tests/gate-branch-tails.client.spec.tsx +++ b/packages/client/ui-chat/tests/gate-branch-tails.client.spec.tsx @@ -1,6 +1,6 @@ // @vitest-environment jsdom -import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' +import { afterEach, describe, expect, it, vi } from 'vitest' import { cleanup, render } from '@testing-library/react' import { bindSnapshotSelector, makeTranslate } from '@deepseek-ai/dsh-client-test-runtime' import { createSnapshotStore } from '@deepseek-ai/dsh-client-store' @@ -18,7 +18,7 @@ import type { import { zh as commonZh } from '@deepseek-ai/dsh-client-locale/src/locales/zh.ts' import { createChatStore } from '../src/client/stores.ts' import { AssistantMarkdown, type AssistantMarkdownProps } from '../src/client/chat/AssistantMarkdown.tsx' -import { StatsLine } from '../src/client/chat/StatsLine.tsx' +import { StatsPills } from '../src/client/chat/StatsPills.tsx' import { DetailsPanel } from '../src/client/details/DetailsPanel.tsx' import { zh } from '../src/client/locale.ts' import { chatSnapshotFixture } from './chat-snapshot-fixture.client.ts' @@ -26,14 +26,6 @@ import { chatSnapshotFixture } from './chat-snapshot-fixture.client.ts' const t: AssistantMarkdownProps['t'] = makeTranslate(zh, commonZh) const renderMessageImages: AssistantMarkdownProps['renderMessageImages'] = () => null -/** jsdom has no ResizeObserver; StatsLine watches its row for ellipsis truncation through one. */ -class ResizeObserverStub { - observe(): void {} - unobserve(): void {} - disconnect(): void {} -} - -beforeEach(() => { vi.stubGlobal('ResizeObserver', ResizeObserverStub) }) afterEach(() => { cleanup() vi.unstubAllGlobals() @@ -92,7 +84,7 @@ describe('render branch tails', () => { expect(view.container.querySelector('[data-state="ok"]')).not.toBeNull() }) - it('StatsLine falls back to window-node counts and drops every token group without projections', () => { + it('StatsPills falls back to window-node counts and drops the usage pill without projections', () => { // No sessionStats key → the window fold supplies the counts (the // assembly-without-the-unit fallback). Node `usage` is deliberately // ignored: billing rides the durable tokenUsage projection, so an absent @@ -105,13 +97,14 @@ describe('render branch tails', () => { const snap = chatSnapshotFixture({ nodes }) const source = { getSnapshot: () => snap, subscribe: () => () => {} } const view = render( - undefined} />, ) - expect(view.container.textContent).toBe('2 轮 · 3 步') + expect(view.container.textContent).toBe('2 轮 3 步') + expect(view.getAllByRole('button')).toHaveLength(1) }) it('AssistantMarkdown reasoning as the streaming tail renders the running ring', () => { diff --git a/packages/client/ui-conversation/src/client/locales.ts b/packages/client/ui-conversation/src/client/locales.ts index 6f94a41dd5..5d01c9d345 100644 --- a/packages/client/ui-conversation/src/client/locales.ts +++ b/packages/client/ui-conversation/src/client/locales.ts @@ -176,10 +176,10 @@ export const en = { 'hint.goal': 'describe the objective for a long-running task', 'hint.goal.active': 'goal active — edit / pause / resume / clear', 'placeholder.plan': PLAN_NEXT_ACTION_EN, - 'placeholder.default': 'Message or run a task, run / commands, @ files or sessions', + 'placeholder.default': 'Message or run a task, / commands, @ files or sessions', 'placeholder.unavailable': 'Session unavailable', 'placeholder.parentOffline': 'Parent session offline; sending is unavailable but you can still stop the run', - 'placeholder.hero': 'Describe what you want to build, run / commands, @ files or sessions', + 'placeholder.hero': 'Describe what you want to build, / commands, @ files or sessions', 'placeholder.workspace': 'Choose a workspace to start', 'placeholder.steerQueue': 'Cmd/Ctrl+Enter steers all queued messages', 'input.commands': 'Commands', diff --git a/packages/client/ui-conversation/src/client/skeleton/InputBar.module.css b/packages/client/ui-conversation/src/client/skeleton/InputBar.module.css index 1749d469d9..1287118622 100644 --- a/packages/client/ui-conversation/src/client/skeleton/InputBar.module.css +++ b/packages/client/ui-conversation/src/client/skeleton/InputBar.module.css @@ -10,6 +10,13 @@ padding: 0 var(--dsh-composer-side-clearance) 8px; } +/* A mounted stats row in the composer dock (data-composer-stats, ui-chat + StatsPills) brings its own 4px top pad, so the root's bottom clearance + tightens to keep the drawn B8 rhythm. */ +.root:has([data-composer-stats]) { + padding-bottom: 4px; +} + .hero { /* No bottom pad in the centered hero, but the side clearance must survive: the hero wrapper is full-width on narrow viewports, so this padding is @@ -158,7 +165,10 @@ bar inset. */ .input { box-sizing: border-box; - padding: 2px 8px 0 14px; + /* Docked floor: one 24px line + 4px top pad = 28 content height; 36 leaves + an 8px breath below the line (the hero keeps its own 52px floor below). */ + min-height: 36px; + padding: 4px 8px 0 14px; font-family: var(--dsw-font-family); font-size: inherit; line-height: inherit; @@ -189,7 +199,7 @@ Insets mirror .input's pads (the other right 4px sits on .scroll). */ .placeholder { position: absolute; - inset: 2px 8px auto 14px; + inset: 4px 8px auto 14px; color: var(--dsw-alias-label-caption); pointer-events: none; user-select: none; @@ -219,8 +229,8 @@ justify-content: space-between; gap: 12px; /* 2px moved from the bottom pad to the top: the whole control row sits 2px - lower in the card (it read too high against the textarea) while the card - height and the controls' own centering stay untouched. */ + lower in the card (it read too high against the textarea) while the + controls' own centering stays untouched. */ padding: 2px 8px 6px; min-width: 0; /* Size container so the chips inside can collapse to icon-only when the diff --git a/packages/client/ui-model-selection/src/client/ModelSelect.tsx b/packages/client/ui-model-selection/src/client/ModelSelect.tsx index f375ade667..b7b212b609 100644 --- a/packages/client/ui-model-selection/src/client/ModelSelect.tsx +++ b/packages/client/ui-model-selection/src/client/ModelSelect.tsx @@ -127,6 +127,9 @@ export function ModelSelect( // anchor rect, measured before paint, clamped inside the viewport): above // the trigger, right edges aligned. Depends on pane and directory state // because pane switches and async catalog loads resize the card. + /* jscpd:ignore-start -- deliberate mirror of ui-primitives Menu's clamped fixed + placement; this select keeps its own pane/reload lifecycle instead of adopting + Menu's item model, so only the measure-and-clamp plumbing repeats. */ useLayoutEffect(() => { if (!open) { setMenuPos(null); return } const place = (): void => { @@ -152,6 +155,7 @@ export function ModelSelect( window.removeEventListener('resize', place) } }, [open, pane, state]) + /* jscpd:ignore-end */ if (!available) return null diff --git a/packages/client/ui-primitives/src/icons/index.tsx b/packages/client/ui-primitives/src/icons/index.tsx index b2d52182cf..4afc31089f 100644 --- a/packages/client/ui-primitives/src/icons/index.tsx +++ b/packages/client/ui-primitives/src/icons/index.tsx @@ -778,6 +778,17 @@ export const IconClockOutline16 = ({ size = 16, className }: IconProps) => ( ) +/** Thin-stroke gauge: dial arc open at the bottom, filled hub, square-cut needle to the upper right. + * The dial center sits at y=8.75, not 8: the bottom opening leaves the glyph top-heavy, and the + * 0.75 drop optically centers the drawn extent in the 16 box. */ +export const IconGaugeOutline16 = ({ size = 16, className }: IconProps) => ( + + + + + +) + /** ic_send_outline_14 (figma extract): thin-stroke upward send arrow. */ export const IconSendOutline14 = ({ size = 14, className }: IconProps) => ( diff --git a/packages/extensions/cordis-client-runner/src/client/slot-catalog.ts b/packages/extensions/cordis-client-runner/src/client/slot-catalog.ts index 78b9f15ce0..7e1637b372 100644 --- a/packages/extensions/cordis-client-runner/src/client/slot-catalog.ts +++ b/packages/extensions/cordis-client-runner/src/client/slot-catalog.ts @@ -493,7 +493,7 @@ export const CLIENT_SLOT_API: readonly ClientSlotEntry[] = [ slotInject: '', declaredBy: 'an entry in \'conversation.composer.bar\' (client-ui-conversation), so it exists while that entry is mounted', occupants: [ - 'client-ui-chat StatsLine id \'stats\'', + 'client-ui-chat StatsPills id \'stats\'', ], replaceRisk: 'none', example: 'return {\n inject: [\'slots\'],\n apply(ctx) {\n ctx.slots.inject(\'conversation.composer.dock\', () => ctx.slots.register(\n { name: \'conversation.composer.dock\', id: \'my-entry\', order: 100, label: \'My entry\' },\n () => React.createElement(\'div\', null, \'hello\'),\n ))\n },\n}', diff --git a/snapshots/web/bash-abort-row/ui.expected.md b/snapshots/web/bash-abort-row/ui.expected.md index f32b712343..e9b2ecb716 100644 --- a/snapshots/web/bash-abort-row/ui.expected.md +++ b/snapshots/web/bash-abort-row/ui.expected.md @@ -26,7 +26,7 @@ - 'button "Failed Bash Error: tool call aborted before dispatch"': - img - text: "Failed Bash Error: tool call aborted before dispatch" -- textbox "Message or run a task, run / commands, @ files or sessions" +- textbox "Message or run a task, / commands, @ files or sessions" - button "Commands": - img - button "Add attachment": @@ -36,4 +36,9 @@ - text: DeepSeek-V4-Flash - img - button "Send message" [disabled] -- text: 1 turns · 1 steps LLM {{duration}} · Tool call {{duration}} TTFT avg {{duration}} · {{throughput}} tok/s Cache hit 0% Input 10 tok · Output 10 tok +- button "1 turns 1 steps{{throughput}} tok/s": + - img + - text: 1 turns 1 steps{{throughput}} tok/s +- button "20 tokCache hit 0%": + - img + - text: 20 tokCache hit 0% diff --git a/snapshots/web/cordis-tool-round/ui.expected.md b/snapshots/web/cordis-tool-round/ui.expected.md index ccb3a4aab1..329f877fd8 100644 --- a/snapshots/web/cordis-tool-round/ui.expected.md +++ b/snapshots/web/cordis-tool-round/ui.expected.md @@ -114,7 +114,7 @@ - img - text: Ran for {{duration}} - text: {{clock}} -- textbox "Message or run a task, run / commands, @ files or sessions" +- textbox "Message or run a task, / commands, @ files or sessions" - button "Commands": - img - button "Add attachment": @@ -125,4 +125,9 @@ - img - button "0% of context used" - button "Send message" [disabled] -- text: 3 turns · 7 steps LLM {{duration}} · Tool call {{duration}} Cache hit 77% Input 66.5K tok · Output 321 tok +- button "3 turns 7 steps": + - img + - text: 3 turns 7 steps +- button "66.8K tokCache hit 77%": + - img + - text: 66.8K tokCache hit 77% diff --git a/snapshots/web/feedback-command/ack-expanded.expected.md b/snapshots/web/feedback-command/ack-expanded.expected.md index d432999e64..dc0af0b123 100644 --- a/snapshots/web/feedback-command/ack-expanded.expected.md +++ b/snapshots/web/feedback-command/ack-expanded.expected.md @@ -44,7 +44,7 @@ - img - img - text: "feedback Feedback recorded for session session-{{uuid}} Anonymous user: {{uuid}}. Session sharing is enabled." -- textbox "Message or run a task, run / commands, @ files or sessions" +- textbox "Message or run a task, / commands, @ files or sessions" - button "Commands": - img - button "Add attachment": @@ -55,4 +55,9 @@ - img - button "6% of context used" - button "Send message" [disabled] -- text: 1 turns · 1 steps LLM {{duration}} TTFT avg {{duration}} · {{throughput}} tok/s Cache hit 99% Input 7.8K tok · Output 21 tok +- button "1 turns 1 steps{{throughput}} tok/s": + - img + - text: 1 turns 1 steps{{throughput}} tok/s +- button "7.8K tokCache hit 99%": + - img + - text: 7.8K tokCache hit 99% diff --git a/snapshots/web/feedback-command/ack.expected.md b/snapshots/web/feedback-command/ack.expected.md index 6639f443b0..a789da0924 100644 --- a/snapshots/web/feedback-command/ack.expected.md +++ b/snapshots/web/feedback-command/ack.expected.md @@ -36,7 +36,7 @@ - img - img - text: "feedback Feedback recorded for session session-{{uuid}} Anonymous user: {{uuid}}. Session sharing is enabled." -- textbox "Message or run a task, run / commands, @ files or sessions" +- textbox "Message or run a task, / commands, @ files or sessions" - button "Commands": - img - button "Add attachment": @@ -47,4 +47,9 @@ - img - button "6% of context used" - button "Send message" [disabled] -- text: 1 turns · 1 steps LLM {{duration}} TTFT avg {{duration}} · {{throughput}} tok/s Cache hit 99% Input 7.8K tok · Output 21 tok +- button "1 turns 1 steps{{throughput}} tok/s": + - img + - text: 1 turns 1 steps{{throughput}} tok/s +- button "7.8K tokCache hit 99%": + - img + - text: 7.8K tokCache hit 99% diff --git a/snapshots/web/feedback-release/ack-expanded.expected.md b/snapshots/web/feedback-release/ack-expanded.expected.md index 64fe91e663..2010a9db77 100644 --- a/snapshots/web/feedback-release/ack-expanded.expected.md +++ b/snapshots/web/feedback-release/ack-expanded.expected.md @@ -44,7 +44,7 @@ - img - img - text: "feedback Feedback recorded for session session-{{uuid}} Anonymous user: {{uuid}}. Session sharing is feedback-gated; recording feedback uploads the session records not yet shared." -- textbox "Message or run a task, run / commands, @ files or sessions" +- textbox "Message or run a task, / commands, @ files or sessions" - button "Commands": - img - button "Add attachment": @@ -55,4 +55,9 @@ - img - button "6% of context used" - button "Send message" [disabled] -- text: 1 turns · 1 steps LLM {{duration}} TTFT avg {{duration}} · {{throughput}} tok/s Cache hit 99% Input 7.8K tok · Output 21 tok +- button "1 turns 1 steps{{throughput}} tok/s": + - img + - text: 1 turns 1 steps{{throughput}} tok/s +- button "7.8K tokCache hit 99%": + - img + - text: 7.8K tokCache hit 99% diff --git a/snapshots/web/feedback-release/ack.expected.md b/snapshots/web/feedback-release/ack.expected.md index 5431636953..201f0bed11 100644 --- a/snapshots/web/feedback-release/ack.expected.md +++ b/snapshots/web/feedback-release/ack.expected.md @@ -36,7 +36,7 @@ - img - img - text: "feedback Feedback recorded for session session-{{uuid}} Anonymous user: {{uuid}}. Session sharing is feedback-gated; recording feedback uploads the session records not yet shared." -- textbox "Message or run a task, run / commands, @ files or sessions" +- textbox "Message or run a task, / commands, @ files or sessions" - button "Commands": - img - button "Add attachment": @@ -47,4 +47,9 @@ - img - button "6% of context used" - button "Send message" [disabled] -- text: 1 turns · 1 steps LLM {{duration}} TTFT avg {{duration}} · {{throughput}} tok/s Cache hit 99% Input 7.8K tok · Output 21 tok +- button "1 turns 1 steps{{throughput}} tok/s": + - img + - text: 1 turns 1 steps{{throughput}} tok/s +- button "7.8K tokCache hit 99%": + - img + - text: 7.8K tokCache hit 99% diff --git a/snapshots/web/file-upload-round/ui.expected.md b/snapshots/web/file-upload-round/ui.expected.md index 7d34ba8037..1422236acc 100644 --- a/snapshots/web/file-upload-round/ui.expected.md +++ b/snapshots/web/file-upload-round/ui.expected.md @@ -38,7 +38,7 @@ - img - text: Ran for {{duration}} - text: {{clock}} -- textbox "Message or run a task, run / commands, @ files or sessions" +- textbox "Message or run a task, / commands, @ files or sessions" - button "Commands": - img - button "Add attachment": @@ -49,4 +49,9 @@ - img - button "1% of context used" - button "Send message" [disabled] -- text: 1 turns · 2 steps LLM {{duration}} · Tool call {{duration}} TTFT avg {{duration}} · {{throughput}} tok/s Cache hit 51% Input 17.2K tok · Output 155 tok +- button "1 turns 2 steps{{throughput}} tok/s": + - img + - text: 1 turns 2 steps{{throughput}} tok/s +- button "17.3K tokCache hit 51%": + - img + - text: 17.3K tokCache hit 51% diff --git a/snapshots/web/fresh-round-trip/ui-expanded.expected.md b/snapshots/web/fresh-round-trip/ui-expanded.expected.md index d20f524749..aa560b0678 100644 --- a/snapshots/web/fresh-round-trip/ui-expanded.expected.md +++ b/snapshots/web/fresh-round-trip/ui-expanded.expected.md @@ -48,7 +48,7 @@ - img - text: Ran for {{duration}} - text: {{clock}} -- textbox "Message or run a task, run / commands, @ files or sessions" +- textbox "Message or run a task, / commands, @ files or sessions" - button "Commands": - img - button "Add attachment": @@ -59,4 +59,9 @@ - img - button "6% of context used" - button "Send message" [disabled] -- text: 1 turns · 2 steps LLM {{duration}} · Tool call {{duration}} TTFT avg {{duration}} · {{throughput}} tok/s Cache hit 99% Input 15.7K tok · Output 111 tok +- button "1 turns 2 steps{{throughput}} tok/s": + - img + - text: 1 turns 2 steps{{throughput}} tok/s +- button "15.8K tokCache hit 99%": + - img + - text: 15.8K tokCache hit 99% diff --git a/snapshots/web/fresh-round-trip/ui.expected.md b/snapshots/web/fresh-round-trip/ui.expected.md index 33eb60503c..d4ab10e3b4 100644 --- a/snapshots/web/fresh-round-trip/ui.expected.md +++ b/snapshots/web/fresh-round-trip/ui.expected.md @@ -32,7 +32,7 @@ - img - text: Ran for {{duration}} - text: {{clock}} -- textbox "Message or run a task, run / commands, @ files or sessions" +- textbox "Message or run a task, / commands, @ files or sessions" - button "Commands": - img - button "Add attachment": @@ -43,4 +43,9 @@ - img - button "6% of context used" - button "Send message" [disabled] -- text: 1 turns · 2 steps LLM {{duration}} · Tool call {{duration}} TTFT avg {{duration}} · {{throughput}} tok/s Cache hit 99% Input 15.7K tok · Output 111 tok +- button "1 turns 2 steps{{throughput}} tok/s": + - img + - text: 1 turns 2 steps{{throughput}} tok/s +- button "15.8K tokCache hit 99%": + - img + - text: 15.8K tokCache hit 99% diff --git a/snapshots/web/goal-multi-turn-actions/ui-expanded.expected.md b/snapshots/web/goal-multi-turn-actions/ui-expanded.expected.md index a36083e5fc..e749d9c8ae 100644 --- a/snapshots/web/goal-multi-turn-actions/ui-expanded.expected.md +++ b/snapshots/web/goal-multi-turn-actions/ui-expanded.expected.md @@ -226,7 +226,7 @@ - text: {{clock}} - button "Back to bottom": - img -- textbox "Message or run a task, run / commands, @ files or sessions" +- textbox "Message or run a task, / commands, @ files or sessions" - button "Commands": - img - button "Add attachment": @@ -237,4 +237,9 @@ - img - button "9% of context used" - button "Send message" [disabled] -- text: 2 turns · 12 steps LLM {{duration}} · Tool call {{duration}} TTFT avg {{duration}} · {{throughput}} tok/s Cache hit 91% Input 113K tok · Output 2.4K tok +- button "2 turns 12 steps{{throughput}} tok/s": + - img + - text: 2 turns 12 steps{{throughput}} tok/s +- button "115K tokCache hit 91%": + - img + - text: 115K tokCache hit 91% diff --git a/snapshots/web/goal-multi-turn-actions/ui.expected.md b/snapshots/web/goal-multi-turn-actions/ui.expected.md index 2cfec96b5d..645c2195d4 100644 --- a/snapshots/web/goal-multi-turn-actions/ui.expected.md +++ b/snapshots/web/goal-multi-turn-actions/ui.expected.md @@ -114,7 +114,7 @@ - img - text: Ran for {{duration}} - text: {{clock}} -- textbox "Message or run a task, run / commands, @ files or sessions" +- textbox "Message or run a task, / commands, @ files or sessions" - button "Commands": - img - button "Add attachment": @@ -125,4 +125,9 @@ - img - button "9% of context used" - button "Send message" [disabled] -- text: 2 turns · 12 steps LLM {{duration}} · Tool call {{duration}} TTFT avg {{duration}} · {{throughput}} tok/s Cache hit 91% Input 113K tok · Output 2.4K tok +- button "2 turns 12 steps{{throughput}} tok/s": + - img + - text: 2 turns 12 steps{{throughput}} tok/s +- button "115K tokCache hit 91%": + - img + - text: 115K tokCache hit 91% diff --git a/snapshots/web/lifecycle-chrome/hero.expected.md b/snapshots/web/lifecycle-chrome/hero.expected.md index 77dab589b9..ae982a8aa4 100644 --- a/snapshots/web/lifecycle-chrome/hero.expected.md +++ b/snapshots/web/lifecycle-chrome/hero.expected.md @@ -29,7 +29,7 @@ - img - text: Standard mode - img -- textbox "Describe what you want to build, run / commands, @ files or sessions": +- textbox "Describe what you want to build, / commands, @ files or sessions": - paragraph - button "Commands": - img diff --git a/snapshots/web/lifecycle-chrome/plan-active.expected.md b/snapshots/web/lifecycle-chrome/plan-active.expected.md index c991125908..f7347fcded 100644 --- a/snapshots/web/lifecycle-chrome/plan-active.expected.md +++ b/snapshots/web/lifecycle-chrome/plan-active.expected.md @@ -29,7 +29,7 @@ - img - text: Standard mode - img -- textbox "Describe what you want to build, run / commands, @ files or sessions" +- textbox "Describe what you want to build, / commands, @ files or sessions" - button "Commands": - img - button "Add attachment": diff --git a/snapshots/web/lifecycle-chrome/reloaded-expanded.expected.md b/snapshots/web/lifecycle-chrome/reloaded-expanded.expected.md index 0a46f8db2d..3ac2826294 100644 --- a/snapshots/web/lifecycle-chrome/reloaded-expanded.expected.md +++ b/snapshots/web/lifecycle-chrome/reloaded-expanded.expected.md @@ -40,7 +40,7 @@ - img - text: Ran for {{duration}} - text: {{clock}} -- textbox "Message or run a task, run / commands, @ files or sessions" +- textbox "Message or run a task, / commands, @ files or sessions" - button "Commands": - img - button "Add attachment": @@ -51,4 +51,9 @@ - img - button "8% of context used" - button "Send message" [disabled] -- text: 1 turns · 1 steps LLM {{duration}} TTFT avg {{duration}} · {{throughput}} tok/s Cache hit 99.5% Input 10K tok · Output 21 tok +- button "1 turns 1 steps{{throughput}} tok/s": + - img + - text: 1 turns 1 steps{{throughput}} tok/s +- button "10K tokCache hit 99.5%": + - img + - text: 10K tokCache hit 99.5% diff --git a/snapshots/web/lifecycle-chrome/reloaded.expected.md b/snapshots/web/lifecycle-chrome/reloaded.expected.md index afa563c994..4d26c1355f 100644 --- a/snapshots/web/lifecycle-chrome/reloaded.expected.md +++ b/snapshots/web/lifecycle-chrome/reloaded.expected.md @@ -32,7 +32,7 @@ - img - text: Ran for {{duration}} - text: {{clock}} -- textbox "Message or run a task, run / commands, @ files or sessions" +- textbox "Message or run a task, / commands, @ files or sessions" - button "Commands": - img - button "Add attachment": @@ -43,4 +43,9 @@ - img - button "8% of context used" - button "Send message" [disabled] -- text: 1 turns · 1 steps LLM {{duration}} TTFT avg {{duration}} · {{throughput}} tok/s Cache hit 99.5% Input 10K tok · Output 21 tok +- button "1 turns 1 steps{{throughput}} tok/s": + - img + - text: 1 turns 1 steps{{throughput}} tok/s +- button "10K tokCache hit 99.5%": + - img + - text: 10K tokCache hit 99.5% diff --git a/snapshots/web/live-interactions/cancel-expanded.expected.md b/snapshots/web/live-interactions/cancel-expanded.expected.md index a0310ed0eb..d50bce7a51 100644 --- a/snapshots/web/live-interactions/cancel-expanded.expected.md +++ b/snapshots/web/live-interactions/cancel-expanded.expected.md @@ -37,7 +37,7 @@ - img - text: Ran for {{duration}} - text: {{clock}} -- textbox "Message or run a task, run / commands, @ files or sessions" +- textbox "Message or run a task, / commands, @ files or sessions" - button "Commands": - img - button "Add attachment": @@ -47,4 +47,6 @@ - text: DeepSeek-V4-Flash - img - button "Send message" [disabled] -- text: 1 turns · 1 steps LLM {{duration}} TTFT avg {{duration}} +- button "1 turns 1 steps": + - img + - text: 1 turns 1 steps diff --git a/snapshots/web/live-interactions/cancel.expected.md b/snapshots/web/live-interactions/cancel.expected.md index 1ba13db840..003a065b81 100644 --- a/snapshots/web/live-interactions/cancel.expected.md +++ b/snapshots/web/live-interactions/cancel.expected.md @@ -33,7 +33,7 @@ - img - text: Ran for {{duration}} - text: {{clock}} -- textbox "Message or run a task, run / commands, @ files or sessions" +- textbox "Message or run a task, / commands, @ files or sessions" - button "Commands": - img - button "Add attachment": @@ -43,4 +43,6 @@ - text: DeepSeek-V4-Flash - img - button "Send message" [disabled] -- text: 1 turns · 1 steps LLM {{duration}} TTFT avg {{duration}} +- button "1 turns 1 steps": + - img + - text: 1 turns 1 steps diff --git a/snapshots/web/live-interactions/error-auth.expected.md b/snapshots/web/live-interactions/error-auth.expected.md index 6519010337..2bdb5e39fd 100644 --- a/snapshots/web/live-interactions/error-auth.expected.md +++ b/snapshots/web/live-interactions/error-auth.expected.md @@ -23,7 +23,7 @@ - status: - text: This turn failedAPI key is invalid - code: AUTH -- textbox "Message or run a task, run / commands, @ files or sessions" +- textbox "Message or run a task, / commands, @ files or sessions" - button "Commands": - img - button "Add attachment": @@ -33,4 +33,6 @@ - text: DeepSeek-V4-Flash - img - button "Send message" [disabled] -- text: 1 turns · 1 steps +- button "1 turns 1 steps": + - img + - text: 1 turns 1 steps diff --git a/snapshots/web/live-interactions/loading.expected.md b/snapshots/web/live-interactions/loading.expected.md index b9dcae1a7a..900433c41b 100644 --- a/snapshots/web/live-interactions/loading.expected.md +++ b/snapshots/web/live-interactions/loading.expected.md @@ -22,7 +22,7 @@ - text: Context injection @deepseek-ai/dsh-system-prompt - paragraph: partial - status: Deep diving... -- textbox "Message or run a task, run / commands, @ files or sessions" +- textbox "Message or run a task, / commands, @ files or sessions" - button "Commands": - img - button "Add attachment": diff --git a/snapshots/web/live-interactions/retry-exhausted.expected.md b/snapshots/web/live-interactions/retry-exhausted.expected.md index 9043e61bfb..08941fd929 100644 --- a/snapshots/web/live-interactions/retry-exhausted.expected.md +++ b/snapshots/web/live-interactions/retry-exhausted.expected.md @@ -25,7 +25,7 @@ - status: - text: This turn failedupstream 503 - code: SERVER -- textbox "Message or run a task, run / commands, @ files or sessions" +- textbox "Message or run a task, / commands, @ files or sessions" - button "Commands": - img - button "Add attachment": @@ -35,4 +35,6 @@ - text: DeepSeek-V4-Flash - img - button "Send message" [disabled] -- text: 1 turns · 1 steps +- button "1 turns 1 steps": + - img + - text: 1 turns 1 steps diff --git a/snapshots/web/live-interactions/retry-expanded.expected.md b/snapshots/web/live-interactions/retry-expanded.expected.md index e858359ac4..65064c68b0 100644 --- a/snapshots/web/live-interactions/retry-expanded.expected.md +++ b/snapshots/web/live-interactions/retry-expanded.expected.md @@ -42,7 +42,7 @@ - img - text: Ran for {{duration}} - text: {{clock}} -- textbox "Message or run a task, run / commands, @ files or sessions" +- textbox "Message or run a task, / commands, @ files or sessions" - button "Commands": - img - button "Add attachment": @@ -53,4 +53,9 @@ - img - button "6% of context used" - button "Send message" [disabled] -- text: 1 turns · 1 steps LLM {{duration}} TTFT avg {{duration}} · {{throughput}} tok/s Cache hit 99% Input 7.8K tok · Output 79 tok +- button "1 turns 1 steps{{throughput}} tok/s": + - img + - text: 1 turns 1 steps{{throughput}} tok/s +- button "7.9K tokCache hit 99%": + - img + - text: 7.9K tokCache hit 99% diff --git a/snapshots/web/live-interactions/retry.expected.md b/snapshots/web/live-interactions/retry.expected.md index 44b7ce4879..7d8520fccf 100644 --- a/snapshots/web/live-interactions/retry.expected.md +++ b/snapshots/web/live-interactions/retry.expected.md @@ -32,7 +32,7 @@ - img - text: Ran for {{duration}} - text: {{clock}} -- textbox "Message or run a task, run / commands, @ files or sessions" +- textbox "Message or run a task, / commands, @ files or sessions" - button "Commands": - img - button "Add attachment": @@ -43,4 +43,9 @@ - img - button "6% of context used" - button "Send message" [disabled] -- text: 1 turns · 1 steps LLM {{duration}} TTFT avg {{duration}} · {{throughput}} tok/s Cache hit 99% Input 7.8K tok · Output 79 tok +- button "1 turns 1 steps{{throughput}} tok/s": + - img + - text: 1 turns 1 steps{{throughput}} tok/s +- button "7.9K tokCache hit 99%": + - img + - text: 7.9K tokCache hit 99% diff --git a/snapshots/web/live-interactions/running-draft.expected.md b/snapshots/web/live-interactions/running-draft.expected.md index a3b47457ca..831e0a22dd 100644 --- a/snapshots/web/live-interactions/running-draft.expected.md +++ b/snapshots/web/live-interactions/running-draft.expected.md @@ -22,7 +22,7 @@ - text: Context injection @deepseek-ai/dsh-system-prompt - paragraph: partial - status: Deep diving... -- textbox "Message or run a task, run / commands, @ files or sessions": +- textbox "Message or run a task, / commands, @ files or sessions": - paragraph: Queue this follow-up while the current turn is running. - button "Commands": - img diff --git a/snapshots/web/message-actions/ui.expected.md b/snapshots/web/message-actions/ui.expected.md index 7fd215050d..9120ef468a 100644 --- a/snapshots/web/message-actions/ui.expected.md +++ b/snapshots/web/message-actions/ui.expected.md @@ -66,7 +66,7 @@ - img - text: Ran for {{duration}} - text: 7/25 {{clock}} -- textbox "Message or run a task, run / commands, @ files or sessions" +- textbox "Message or run a task, / commands, @ files or sessions" - button "Commands": - img - button "Add attachment": @@ -76,4 +76,9 @@ - text: DeepSeek-V4-Flash - img - button "Send message" [disabled] -- text: 2 turns · 3 steps LLM {{duration}} · Tool call {{duration}} TTFT avg {{duration}} · {{throughput}} tok/s Cache hit 98% Input 7.8K tok · Output 103 tok +- button "2 turns 3 steps{{throughput}} tok/s": + - img + - text: 2 turns 3 steps{{throughput}} tok/s +- button "7.9K tokCache hit 98%": + - img + - text: 7.9K tokCache hit 98% diff --git a/snapshots/web/minimal-preset/ui.expected.md b/snapshots/web/minimal-preset/ui.expected.md index 3d08bbcfee..21fde05e19 100644 --- a/snapshots/web/minimal-preset/ui.expected.md +++ b/snapshots/web/minimal-preset/ui.expected.md @@ -37,7 +37,7 @@ - img - text: Ran for {{duration}} - text: {{clock}} -- textbox "Message or run a task, run / commands, @ files or sessions" +- textbox "Message or run a task, / commands, @ files or sessions" - button "Commands": - img - button "Add attachment": @@ -48,4 +48,9 @@ - img - button "0% of context used" - button "Send message" [disabled] -- text: 1 turns · 2 steps LLM {{duration}} · Tool call {{duration}} TTFT avg {{duration}} · {{throughput}} tok/s Cache hit 0% Input 20 tok · Output 8 tok +- button "1 turns 2 steps{{throughput}} tok/s": + - img + - text: 1 turns 2 steps{{throughput}} tok/s +- button "28 tokCache hit 0%": + - img + - text: 28 tokCache hit 0% diff --git a/snapshots/web/plan-review/approved-expanded.expected.md b/snapshots/web/plan-review/approved-expanded.expected.md index a6caedf876..4d70ae9cb9 100644 --- a/snapshots/web/plan-review/approved-expanded.expected.md +++ b/snapshots/web/plan-review/approved-expanded.expected.md @@ -58,7 +58,7 @@ - img - text: Ran for {{duration}} - text: {{clock}} -- textbox "Message or run a task, run / commands, @ files or sessions" +- textbox "Message or run a task, / commands, @ files or sessions" - button "Commands": - img - button "Add attachment": @@ -69,4 +69,9 @@ - img - button "4% of context used" - button "Send message" [disabled] -- text: 1 turns · 2 steps LLM {{duration}} · Tool call {{duration}} TTFT avg {{duration}} · {{throughput}} tok/s Cache hit 51% Input 10.2K tok · Output 346 tok +- button "1 turns 2 steps{{throughput}} tok/s": + - img + - text: 1 turns 2 steps{{throughput}} tok/s +- button "10.6K tokCache hit 51%": + - img + - text: 10.6K tokCache hit 51% diff --git a/snapshots/web/plan-review/approved.expected.md b/snapshots/web/plan-review/approved.expected.md index 94a14902f3..30936753c8 100644 --- a/snapshots/web/plan-review/approved.expected.md +++ b/snapshots/web/plan-review/approved.expected.md @@ -38,7 +38,7 @@ - img - text: Ran for {{duration}} - text: {{clock}} -- textbox "Message or run a task, run / commands, @ files or sessions" +- textbox "Message or run a task, / commands, @ files or sessions" - button "Commands": - img - button "Add attachment": @@ -48,5 +48,11 @@ - text: DeepSeek-V4-Flash - img - button "4% of context used" +- tooltip "4% of context used" - button "Send message" [disabled] -- text: 1 turns · 2 steps LLM {{duration}} · Tool call {{duration}} TTFT avg {{duration}} · {{throughput}} tok/s Cache hit 51% Input 10.2K tok · Output 346 tok +- button "1 turns 2 steps{{throughput}} tok/s": + - img + - text: 1 turns 2 steps{{throughput}} tok/s +- button "10.6K tokCache hit 51%": + - img + - text: 10.6K tokCache hit 51% diff --git a/snapshots/web/ptc-round/ui.expected.md b/snapshots/web/ptc-round/ui.expected.md index a8c59b89c1..d397445895 100644 --- a/snapshots/web/ptc-round/ui.expected.md +++ b/snapshots/web/ptc-round/ui.expected.md @@ -53,7 +53,7 @@ - img - text: Ran for {{duration}} - text: {{clock}} -- textbox "Message or run a task, run / commands, @ files or sessions" +- textbox "Message or run a task, / commands, @ files or sessions" - button "Commands": - img - button "Add attachment": @@ -64,4 +64,9 @@ - img - button "7% of context used" - button "Send message" [disabled] -- text: 1 turns · 2 steps LLM {{duration}} · Tool call {{duration}} TTFT avg {{duration}} · {{throughput}} tok/s Cache hit 52% Input 17.2K tok · Output 252 tok +- button "1 turns 2 steps{{throughput}} tok/s": + - img + - text: 1 turns 2 steps{{throughput}} tok/s +- button "17.5K tokCache hit 52%": + - img + - text: 17.5K tokCache hit 52% diff --git a/snapshots/web/question-composer/answered-expanded.expected.md b/snapshots/web/question-composer/answered-expanded.expected.md index 6618d36d68..bd0145230c 100644 --- a/snapshots/web/question-composer/answered-expanded.expected.md +++ b/snapshots/web/question-composer/answered-expanded.expected.md @@ -50,7 +50,7 @@ - img - text: Ran for {{duration}} - text: {{clock}} -- textbox "Message or run a task, run / commands, @ files or sessions" +- textbox "Message or run a task, / commands, @ files or sessions" - button "Commands": - img - button "Add attachment": @@ -61,4 +61,9 @@ - img - button "3% of context used" - button "Send message" [disabled] -- text: 1 turns · 2 steps LLM {{duration}} · Tool call {{duration}} TTFT avg {{duration}} · {{throughput}} tok/s Cache hit 95% Input 8.6K tok · Output 180 tok +- button "1 turns 2 steps{{throughput}} tok/s": + - img + - text: 1 turns 2 steps{{throughput}} tok/s +- button "8.8K tokCache hit 95%": + - img + - text: 8.8K tokCache hit 95% diff --git a/snapshots/web/question-composer/answered.expected.md b/snapshots/web/question-composer/answered.expected.md index 3a5306b5f6..d9c84ba575 100644 --- a/snapshots/web/question-composer/answered.expected.md +++ b/snapshots/web/question-composer/answered.expected.md @@ -32,7 +32,7 @@ - img - text: Ran for {{duration}} - text: {{clock}} -- textbox "Message or run a task, run / commands, @ files or sessions" +- textbox "Message or run a task, / commands, @ files or sessions" - button "Commands": - img - button "Add attachment": @@ -43,4 +43,9 @@ - img - button "3% of context used" - button "Send message" [disabled] -- text: 1 turns · 2 steps LLM {{duration}} · Tool call {{duration}} TTFT avg {{duration}} · {{throughput}} tok/s Cache hit 95% Input 8.6K tok · Output 180 tok +- button "1 turns 2 steps{{throughput}} tok/s": + - img + - text: 1 turns 2 steps{{throughput}} tok/s +- button "8.8K tokCache hit 95%": + - img + - text: 8.8K tokCache hit 95% diff --git a/snapshots/web/question-composer/cancelled.expected.md b/snapshots/web/question-composer/cancelled.expected.md index a2ede1b5ae..bb933ec66b 100644 --- a/snapshots/web/question-composer/cancelled.expected.md +++ b/snapshots/web/question-composer/cancelled.expected.md @@ -29,7 +29,7 @@ - list: - listitem: Which color do you prefer? - button "Inspect" -- textbox "Message or run a task, run / commands, @ files or sessions" +- textbox "Message or run a task, / commands, @ files or sessions" - button "Commands": - img - button "Add attachment": @@ -40,4 +40,9 @@ - img - button "3% of context used" - button "Send message" [disabled] -- text: 1 turns · 1 steps LLM {{duration}} · Tool call {{duration}} TTFT avg {{duration}} · {{throughput}} tok/s Cache hit 97% Input 4.2K tok · Output 158 tok +- button "1 turns 1 steps{{throughput}} tok/s": + - img + - text: 1 turns 1 steps{{throughput}} tok/s +- button "4.4K tokCache hit 97%": + - img + - text: 4.4K tokCache hit 97% diff --git a/snapshots/web/queue-actions/preserved-expanded.expected.md b/snapshots/web/queue-actions/preserved-expanded.expected.md index d266a7b506..7bda4268d7 100644 --- a/snapshots/web/queue-actions/preserved-expanded.expected.md +++ b/snapshots/web/queue-actions/preserved-expanded.expected.md @@ -56,7 +56,7 @@ - img - button "Steer queued message" [disabled]: - img -- textbox "Message or run a task, run / commands, @ files or sessions" +- textbox "Message or run a task, / commands, @ files or sessions" - button "Commands": - img - button "Add attachment": @@ -66,4 +66,6 @@ - text: DeepSeek-V4-Flash - img - button "Send message" [disabled] -- text: 1 turns · 1 steps LLM {{duration}} TTFT avg {{duration}} +- button "1 turns 1 steps": + - img + - text: 1 turns 1 steps diff --git a/snapshots/web/queue-actions/preserved.expected.md b/snapshots/web/queue-actions/preserved.expected.md index 1e031dda80..250e8d4f1d 100644 --- a/snapshots/web/queue-actions/preserved.expected.md +++ b/snapshots/web/queue-actions/preserved.expected.md @@ -52,7 +52,7 @@ - img - button "Steer queued message" [disabled]: - img -- textbox "Message or run a task, run / commands, @ files or sessions" +- textbox "Message or run a task, / commands, @ files or sessions" - button "Commands": - img - button "Add attachment": @@ -62,4 +62,6 @@ - text: DeepSeek-V4-Flash - img - button "Send message" [disabled] -- text: 1 turns · 1 steps LLM {{duration}} TTFT avg {{duration}} +- button "1 turns 1 steps": + - img + - text: 1 turns 1 steps diff --git a/snapshots/web/queued-image/delivered.expected.md b/snapshots/web/queued-image/delivered.expected.md index d4596fdbde..557eb295db 100644 --- a/snapshots/web/queued-image/delivered.expected.md +++ b/snapshots/web/queued-image/delivered.expected.md @@ -76,7 +76,7 @@ - img - text: Ran for {{duration}} - text: {{clock}} -- textbox "Message or run a task, run / commands, @ files or sessions" +- textbox "Message or run a task, / commands, @ files or sessions" - button "Commands": - img - button "Add attachment": @@ -87,4 +87,9 @@ - img - button "6% of context used" - button "Send message" [disabled] -- text: 3 turns · 3 steps LLM {{duration}} TTFT avg {{duration}} · {{throughput}} tok/s Cache hit 99% Input 15.6K tok · Output 158 tok +- button "3 turns 3 steps{{throughput}} tok/s": + - img + - text: 3 turns 3 steps{{throughput}} tok/s +- button "15.7K tokCache hit 99%": + - img + - text: 15.7K tokCache hit 99% diff --git a/snapshots/web/seeded-history/command-row.expected.md b/snapshots/web/seeded-history/command-row.expected.md index f4aa43adab..11e462169d 100644 --- a/snapshots/web/seeded-history/command-row.expected.md +++ b/snapshots/web/seeded-history/command-row.expected.md @@ -58,7 +58,7 @@ - text: Context injection AGENTS.md - img - text: permission preset read-only -- textbox "Message or run a task, run / commands, @ files or sessions" +- textbox "Message or run a task, / commands, @ files or sessions" - button "Commands": - img - button "Add attachment": @@ -68,4 +68,9 @@ - text: DeepSeek-V4-Flash - img - button "Send message" [disabled] -- text: 1 turns · 2 steps LLM {{duration}} · Tool call {{duration}} TTFT avg {{duration}} · {{throughput}} tok/s Cache hit 98% Input 15.8K tok · Output 135 tok +- button "1 turns 2 steps{{throughput}} tok/s": + - img + - text: 1 turns 2 steps{{throughput}} tok/s +- button "16K tokCache hit 98%": + - img + - text: 16K tokCache hit 98% diff --git a/snapshots/web/seeded-history/feedback-row.expected.md b/snapshots/web/seeded-history/feedback-row.expected.md index 5f5e3424e1..5b100734d0 100644 --- a/snapshots/web/seeded-history/feedback-row.expected.md +++ b/snapshots/web/seeded-history/feedback-row.expected.md @@ -62,7 +62,7 @@ - img - text: "feedback Feedback recorded for session {{seededId}} Anonymous user: {{uuid}}. Session sharing is not configured." - text: "Feedback recorded for session {{seededId}} Anonymous user: {{uuid}}. Session sharing is not configured." -- textbox "Message or run a task, run / commands, @ files or sessions" +- textbox "Message or run a task, / commands, @ files or sessions" - button "Commands": - img - button "Add attachment": @@ -72,4 +72,9 @@ - text: DeepSeek-V4-Flash - img - button "Send message" [disabled] -- text: 1 turns · 2 steps LLM {{duration}} · Tool call {{duration}} TTFT avg {{duration}} · {{throughput}} tok/s Cache hit 98% Input 15.8K tok · Output 135 tok +- button "1 turns 2 steps{{throughput}} tok/s": + - img + - text: 1 turns 2 steps{{throughput}} tok/s +- button "16K tokCache hit 98%": + - img + - text: 16K tokCache hit 98% diff --git a/snapshots/web/seeded-history/ui-expanded.expected.md b/snapshots/web/seeded-history/ui-expanded.expected.md index ed96e5de1d..95a6a763b0 100644 --- a/snapshots/web/seeded-history/ui-expanded.expected.md +++ b/snapshots/web/seeded-history/ui-expanded.expected.md @@ -56,7 +56,7 @@ - img - img - text: Context injection AGENTS.md -- textbox "Message or run a task, run / commands, @ files or sessions" +- textbox "Message or run a task, / commands, @ files or sessions" - button "Commands": - img - button "Add attachment": @@ -66,4 +66,9 @@ - text: DeepSeek-V4-Flash - img - button "Send message" [disabled] -- text: 1 turns · 2 steps LLM {{duration}} · Tool call {{duration}} TTFT avg {{duration}} · {{throughput}} tok/s Cache hit 98% Input 15.8K tok · Output 135 tok +- button "1 turns 2 steps{{throughput}} tok/s": + - img + - text: 1 turns 2 steps{{throughput}} tok/s +- button "16K tokCache hit 98%": + - img + - text: 16K tokCache hit 98% diff --git a/snapshots/web/seeded-history/ui.expected.md b/snapshots/web/seeded-history/ui.expected.md index cda9e9baa9..31ae7ca03b 100644 --- a/snapshots/web/seeded-history/ui.expected.md +++ b/snapshots/web/seeded-history/ui.expected.md @@ -38,7 +38,7 @@ - img - img - text: Context injection AGENTS.md -- textbox "Message or run a task, run / commands, @ files or sessions" +- textbox "Message or run a task, / commands, @ files or sessions" - button "Commands": - img - button "Add attachment": @@ -48,4 +48,9 @@ - text: DeepSeek-V4-Flash - img - button "Send message" [disabled] -- text: 1 turns · 2 steps LLM {{duration}} · Tool call {{duration}} TTFT avg {{duration}} · {{throughput}} tok/s Cache hit 98% Input 15.8K tok · Output 135 tok +- button "1 turns 2 steps{{throughput}} tok/s": + - img + - text: 1 turns 2 steps{{throughput}} tok/s +- button "16K tokCache hit 98%": + - img + - text: 16K tokCache hit 98% diff --git a/snapshots/web/skill-tool-row/ui.expected.md b/snapshots/web/skill-tool-row/ui.expected.md index 1a1a06acaa..17bdc399d7 100644 --- a/snapshots/web/skill-tool-row/ui.expected.md +++ b/snapshots/web/skill-tool-row/ui.expected.md @@ -51,7 +51,7 @@ - img - text: Ran for {{duration}} - text: {{date}} {{clock}} -- textbox "Message or run a task, run / commands, @ files or sessions" +- textbox "Message or run a task, / commands, @ files or sessions" - button "Commands": - img - button "Add attachment": @@ -61,4 +61,9 @@ - text: DeepSeek-V4-Flash - img - button "Send message" [disabled] -- text: 1 turns · 2 steps LLM {{duration}} · Tool call {{duration}} TTFT avg {{duration}} · {{throughput}} tok/s Cache hit 0% Input 280 tok · Output 30 tok +- button "1 turns 2 steps{{throughput}} tok/s": + - img + - text: 1 turns 2 steps{{throughput}} tok/s +- button "310 tokCache hit 0%": + - img + - text: 310 tokCache hit 0% diff --git a/snapshots/web/steering/settled-expanded.expected.md b/snapshots/web/steering/settled-expanded.expected.md index 87b3b151b2..47b25cdcf1 100644 --- a/snapshots/web/steering/settled-expanded.expected.md +++ b/snapshots/web/steering/settled-expanded.expected.md @@ -43,7 +43,7 @@ - img - text: Ran for {{duration}} - text: {{clock}} -- textbox "Message or run a task, run / commands, @ files or sessions" +- textbox "Message or run a task, / commands, @ files or sessions" - button "Commands": - img - button "Add attachment": @@ -54,4 +54,9 @@ - img - button "0% of context used" - button "Send message" [disabled] -- text: 1 turns · 2 steps LLM {{duration}} · Tool call {{duration}} TTFT avg {{duration}} · {{throughput}} tok/s Cache hit 0% Input 20 tok · Output 10 tok +- button "1 turns 2 steps{{throughput}} tok/s": + - img + - text: 1 turns 2 steps{{throughput}} tok/s +- button "30 tokCache hit 0%": + - img + - text: 30 tokCache hit 0% diff --git a/snapshots/web/steering/settled.expected.md b/snapshots/web/steering/settled.expected.md index 18502b9d35..a24abb3594 100644 --- a/snapshots/web/steering/settled.expected.md +++ b/snapshots/web/steering/settled.expected.md @@ -35,7 +35,7 @@ - img - text: Ran for {{duration}} - text: {{clock}} -- textbox "Message or run a task, run / commands, @ files or sessions" +- textbox "Message or run a task, / commands, @ files or sessions" - button "Commands": - img - button "Add attachment": @@ -46,4 +46,9 @@ - img - button "0% of context used" - button "Send message" [disabled] -- text: 1 turns · 2 steps LLM {{duration}} · Tool call {{duration}} TTFT avg {{duration}} · {{throughput}} tok/s Cache hit 0% Input 20 tok · Output 10 tok +- button "1 turns 2 steps{{throughput}} tok/s": + - img + - text: 1 turns 2 steps{{throughput}} tok/s +- button "30 tokCache hit 0%": + - img + - text: 30 tokCache hit 0% diff --git a/snapshots/web/subagent-conversation/ui-expanded.expected.md b/snapshots/web/subagent-conversation/ui-expanded.expected.md index 4e238d019d..9aacbd6bcd 100644 --- a/snapshots/web/subagent-conversation/ui-expanded.expected.md +++ b/snapshots/web/subagent-conversation/ui-expanded.expected.md @@ -70,7 +70,7 @@ - img - text: Ran for {{duration}} - text: {{clock}} -- textbox "Message or run a task, run / commands, @ files or sessions" +- textbox "Message or run a task, / commands, @ files or sessions" - button "Commands": - img - button "Add attachment" [disabled]: @@ -78,4 +78,9 @@ - 'button "Access mode, current: Custom"': Custom - button "6% of context used" - button "Send message" [disabled] -- text: 2 turns · 2 steps LLM {{duration}} TTFT avg {{duration}} · {{throughput}} tok/s Cache hit 99% Input 15.6K tok · Output 158 tok +- button "2 turns 2 steps{{throughput}} tok/s": + - img + - text: 2 turns 2 steps{{throughput}} tok/s +- button "15.7K tokCache hit 99%": + - img + - text: 15.7K tokCache hit 99% diff --git a/snapshots/web/subagent-conversation/ui.expected.md b/snapshots/web/subagent-conversation/ui.expected.md index f1dca1801c..1a57bb2bb3 100644 --- a/snapshots/web/subagent-conversation/ui.expected.md +++ b/snapshots/web/subagent-conversation/ui.expected.md @@ -58,7 +58,7 @@ - img - text: Ran for {{duration}} - text: {{clock}} -- textbox "Message or run a task, run / commands, @ files or sessions" +- textbox "Message or run a task, / commands, @ files or sessions" - button "Commands": - img - button "Add attachment" [disabled]: @@ -66,4 +66,9 @@ - 'button "Access mode, current: Custom"': Custom - button "6% of context used" - button "Send message" [disabled] -- text: 2 turns · 2 steps LLM {{duration}} TTFT avg {{duration}} · {{throughput}} tok/s Cache hit 99% Input 15.6K tok · Output 158 tok +- button "2 turns 2 steps{{throughput}} tok/s": + - img + - text: 2 turns 2 steps{{throughput}} tok/s +- button "15.7K tokCache hit 99%": + - img + - text: 15.7K tokCache hit 99% diff --git a/snapshots/web/turn-tail-actions/completed.expected.md b/snapshots/web/turn-tail-actions/completed.expected.md index b95e74379a..38fe11e9ed 100644 --- a/snapshots/web/turn-tail-actions/completed.expected.md +++ b/snapshots/web/turn-tail-actions/completed.expected.md @@ -35,7 +35,7 @@ - img - text: Ran for {{duration}} - text: {{clock}} -- textbox "Message or run a task, run / commands, @ files or sessions" +- textbox "Message or run a task, / commands, @ files or sessions" - button "Commands": - img - button "Add attachment": @@ -46,4 +46,9 @@ - img - button "6% of context used" - button "Send message" [disabled] -- text: 1 turns · 2 steps LLM {{duration}} · Tool call {{duration}} TTFT avg {{duration}} · {{throughput}} tok/s Cache hit 50% Input 15.7K tok · Output 112 tok +- button "1 turns 2 steps{{throughput}} tok/s": + - img + - text: 1 turns 2 steps{{throughput}} tok/s +- button "15.8K tokCache hit 50%": + - img + - text: 15.8K tokCache hit 50% diff --git a/snapshots/web/turn-tail-actions/focused.expected.md b/snapshots/web/turn-tail-actions/focused.expected.md index 0504850e5c..a1b9392d93 100644 --- a/snapshots/web/turn-tail-actions/focused.expected.md +++ b/snapshots/web/turn-tail-actions/focused.expected.md @@ -48,7 +48,7 @@ - img - text: Ran for {{duration}} - text: {{clock}} -- textbox "Message or run a task, run / commands, @ files or sessions" +- textbox "Message or run a task, / commands, @ files or sessions" - button "Commands": - img - button "Add attachment": @@ -59,4 +59,9 @@ - img - button "6% of context used" - button "Send message" [disabled] -- text: 1 turns · 2 steps LLM {{duration}} · Tool call {{duration}} TTFT avg {{duration}} · {{throughput}} tok/s Cache hit 50% Input 15.7K tok · Output 112 tok +- button "1 turns 2 steps{{throughput}} tok/s": + - img + - text: 1 turns 2 steps{{throughput}} tok/s +- button "15.8K tokCache hit 50%": + - img + - text: 15.8K tokCache hit 50% diff --git a/snapshots/web/turn-tail-actions/running.expected.md b/snapshots/web/turn-tail-actions/running.expected.md index ccaf11a4c4..df7a83034b 100644 --- a/snapshots/web/turn-tail-actions/running.expected.md +++ b/snapshots/web/turn-tail-actions/running.expected.md @@ -32,7 +32,7 @@ - text: Bash Print alpha to stdout - paragraph: partial - status: Deep diving... -- textbox "Message or run a task, run / commands, @ files or sessions" +- textbox "Message or run a task, / commands, @ files or sessions" - button "Commands": - img - button "Add attachment": @@ -43,4 +43,9 @@ - img - button "6% of context used" - button "Stop generating" -- text: 1 turns · 1 steps LLM {{duration}} · Tool call {{duration}} TTFT avg {{duration}} · {{throughput}} tok/s Cache hit 0% Input 7.8K tok · Output 109 tok +- button "1 turns 1 steps{{throughput}} tok/s": + - img + - text: 1 turns 1 steps{{throughput}} tok/s +- button "7.9K tokCache hit 0%": + - img + - text: 7.9K tokCache hit 0% diff --git a/snapshots/web/turn-tail-actions/settled.expected.md b/snapshots/web/turn-tail-actions/settled.expected.md index e668975ca6..7fcc6d9108 100644 --- a/snapshots/web/turn-tail-actions/settled.expected.md +++ b/snapshots/web/turn-tail-actions/settled.expected.md @@ -34,7 +34,7 @@ - img - text: Ran for {{duration}} - text: {{clock}} -- textbox "Message or run a task, run / commands, @ files or sessions" +- textbox "Message or run a task, / commands, @ files or sessions" - button "Commands": - img - button "Add attachment": @@ -45,4 +45,9 @@ - img - button "6% of context used" - button "Send message" [disabled] -- text: 1 turns · 2 steps LLM {{duration}} · Tool call {{duration}} TTFT avg {{duration}} · {{throughput}} tok/s Cache hit 0% Input 7.8K tok · Output 109 tok +- button "1 turns 2 steps{{throughput}} tok/s": + - img + - text: 1 turns 2 steps{{throughput}} tok/s +- button "7.9K tokCache hit 0%": + - img + - text: 7.9K tokCache hit 0% diff --git a/snapshots/web/turn-tail-actions/usage-expanded.expected.md b/snapshots/web/turn-tail-actions/usage-expanded.expected.md index 831043f7f3..dd0e4b20e0 100644 --- a/snapshots/web/turn-tail-actions/usage-expanded.expected.md +++ b/snapshots/web/turn-tail-actions/usage-expanded.expected.md @@ -35,7 +35,7 @@ - img - text: Ran for {{duration}} - text: {{clock}} -- textbox "Message or run a task, run / commands, @ files or sessions" +- textbox "Message or run a task, / commands, @ files or sessions" - button "Commands": - img - button "Add attachment": @@ -46,4 +46,9 @@ - img - button "6% of context used" - button "Send message" [disabled] -- text: 1 turns · 2 steps LLM {{duration}} · Tool call {{duration}} TTFT avg {{duration}} · {{throughput}} tok/s Cache hit 50% Input 15.7K tok · Output 112 tok +- button "1 turns 2 steps{{throughput}} tok/s": + - img + - text: 1 turns 2 steps{{throughput}} tok/s +- button "15.8K tokCache hit 50%": + - img + - text: 15.8K tokCache hit 50% diff --git a/snapshots/web/web-search-round/ui.expected.md b/snapshots/web/web-search-round/ui.expected.md index 550cf32295..a08c9826c1 100644 --- a/snapshots/web/web-search-round/ui.expected.md +++ b/snapshots/web/web-search-round/ui.expected.md @@ -40,7 +40,7 @@ - img - text: Ran for {{duration}} - text: {{clock}} -- textbox "Message or run a task, run / commands, @ files or sessions" +- textbox "Message or run a task, / commands, @ files or sessions" - button "Commands": - img - button "Add attachment": @@ -51,4 +51,9 @@ - img - button "0% of context used" - button "Send message" [disabled] -- text: 1 turns · 2 steps LLM {{duration}} · Tool call {{duration}} TTFT avg {{duration}} · {{throughput}} tok/s Cache hit 0% Input 22 tok · Output 7 tok +- button "1 turns 2 steps{{throughput}} tok/s": + - img + - text: 1 turns 2 steps{{throughput}} tok/s +- button "29 tokCache hit 0%": + - img + - text: 29 tokCache hit 0% From ad38d636265ff442abe50b4922e35c6b4e75284c Mon Sep 17 00:00:00 2001 From: Yif <877193178@qq.com> Date: Mon, 7 Sep 2026 21:18:22 +0800 Subject: [PATCH 07/10] =?UTF-8?q?fix(client):=20ellipsize=20the=20composer?= =?UTF-8?q?=20placeholder=20=E2=80=94=20one=20docked=20line,=20two=20hero?= =?UTF-8?q?=20lines?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A long placeholder wrapped and clipped against the docked composer's 36px floor; the hero's 52px floor fits exactly two lines. --- .../src/client/skeleton/InputBar.module.css | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/packages/client/ui-conversation/src/client/skeleton/InputBar.module.css b/packages/client/ui-conversation/src/client/skeleton/InputBar.module.css index 1287118622..abd5911c90 100644 --- a/packages/client/ui-conversation/src/client/skeleton/InputBar.module.css +++ b/packages/client/ui-conversation/src/client/skeleton/InputBar.module.css @@ -196,11 +196,16 @@ } /* figma 34:10434: #ADB2B8 light / #81858C dark — the caption pair exactly. - Insets mirror .input's pads (the other right 4px sits on .scroll). */ + Insets mirror .input's pads (the other right 4px sits on .scroll). One + ellipsized line: a wrapped second line would clip against the docked + 36px floor. */ .placeholder { position: absolute; inset: 4px 8px auto 14px; color: var(--dsw-alias-label-caption); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; pointer-events: none; user-select: none; } @@ -220,6 +225,16 @@ min-height: 52px; } +/* The hero's 52px floor fits two lines, so its placeholder clamps at two + instead of the docked single line. */ +.hero .placeholder { + white-space: normal; + display: -webkit-box; + -webkit-box-orient: vertical; + -webkit-line-clamp: 2; + line-clamp: 2; +} + /* Toolbar: attach + Plan + Read-only on the left; model + send on the right (figma Input_Bottom chrome). */ .row { From 2c45dfe71e39e2b874ba514ccc79414b6275b2bd Mon Sep 17 00:00:00 2001 From: Yif <877193178@qq.com> Date: Tue, 8 Sep 2026 00:35:04 +0800 Subject: [PATCH 08/10] =?UTF-8?q?fix(client):=20address=20review=20?= =?UTF-8?q?=E2=80=94=20pill=20accessible=20names,=20static=20untimed=20pil?= =?UTF-8?q?l,=20exclusive=20stat=20dialogs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit StatsPills buttons carry explicit aria-labels separating segments with ' · '; a log with no timed figure renders the counts pill as a static reading instead of a button opening an empty dialog; the pills row owns one exclusive open slot so sibling dialogs never stack. ContextMeter restores the '~' approximation prefix (usedTokens prefers heuristic projectedTokens). ModelSelect's jscpd:ignore now names the real gap (useAnchoredPosition places from the left edge only). The pills Agent Note's stale facts (locale keys, context occupancy, golden ownership) are rewritten in place, en+zh. plan-review.e2e.ts parks the pointer after Approve so the ContextMeter tooltip cannot leak into the aria captures; affected goldens refreshed. --- ...-07-composer-session-stats-pills.i18n.yaml | 4 +- ...2026-09-07-composer-session-stats-pills.md | 15 ++- ...6-09-07-composer-session-stats-pills.zh.md | 15 ++- .../skill-user-invoke/ui-expanded.expected.md | 4 +- .../expected/skill-user-invoke/ui.expected.md | 4 +- .../steer-all/settled-expanded.expected.md | 4 +- .../expected/steer-all/settled.expected.md | 4 +- apps/web/tests/plan-review.e2e.ts | 4 + .../src/client/chat/StatsPills.module.css | 11 ++- .../ui-chat/src/client/chat/StatsPills.tsx | 96 +++++++++++++++---- .../ui-chat/src/client/chat/stat-dialog.ts | 10 +- .../tests/chat-branch-tails.client.spec.tsx | 5 +- .../ui-chat/tests/chat-stats.client.spec.tsx | 54 +++++++---- .../tests/gate-branch-tails.client.spec.tsx | 3 +- .../src/client/skeleton/ContextMeter.tsx | 6 +- .../tests/context-meter.client.spec.tsx | 8 +- .../src/client/ModelSelect.tsx | 6 +- snapshots/web/bash-abort-row/ui.expected.md | 4 +- .../web/cordis-tool-round/ui.expected.md | 2 +- .../feedback-command/ack-expanded.expected.md | 4 +- .../web/feedback-command/ack.expected.md | 4 +- .../feedback-release/ack-expanded.expected.md | 4 +- .../web/feedback-release/ack.expected.md | 4 +- .../web/file-upload-round/ui.expected.md | 4 +- .../fresh-round-trip/ui-expanded.expected.md | 4 +- snapshots/web/fresh-round-trip/ui.expected.md | 4 +- .../ui-expanded.expected.md | 4 +- .../goal-multi-turn-actions/ui.expected.md | 4 +- .../reloaded-expanded.expected.md | 4 +- .../web/lifecycle-chrome/reloaded.expected.md | 4 +- .../live-interactions/error-auth.expected.md | 5 +- .../retry-exhausted.expected.md | 5 +- .../retry-expanded.expected.md | 4 +- .../web/live-interactions/retry.expected.md | 4 +- snapshots/web/message-actions/ui.expected.md | 4 +- snapshots/web/minimal-preset/ui.expected.md | 4 +- .../plan-review/approved-expanded.expected.md | 4 +- .../web/plan-review/approved.expected.md | 5 +- snapshots/web/ptc-round/ui.expected.md | 4 +- .../answered-expanded.expected.md | 4 +- .../question-composer/answered.expected.md | 4 +- .../question-composer/cancelled.expected.md | 4 +- .../web/queued-image/delivered.expected.md | 4 +- .../seeded-history/command-row.expected.md | 4 +- .../seeded-history/feedback-row.expected.md | 4 +- .../seeded-history/ui-expanded.expected.md | 4 +- snapshots/web/seeded-history/ui.expected.md | 4 +- snapshots/web/skill-tool-row/ui.expected.md | 4 +- .../web/steering/settled-expanded.expected.md | 4 +- snapshots/web/steering/settled.expected.md | 4 +- .../ui-expanded.expected.md | 4 +- .../web/subagent-conversation/ui.expected.md | 4 +- .../turn-tail-actions/completed.expected.md | 4 +- .../web/turn-tail-actions/focused.expected.md | 4 +- .../web/turn-tail-actions/running.expected.md | 4 +- .../web/turn-tail-actions/settled.expected.md | 4 +- .../usage-expanded.expected.md | 4 +- snapshots/web/web-search-round/ui.expected.md | 4 +- 58 files changed, 252 insertions(+), 166 deletions(-) diff --git a/.agents/notes/implemented/feature/2026-09-07-composer-session-stats-pills.i18n.yaml b/.agents/notes/implemented/feature/2026-09-07-composer-session-stats-pills.i18n.yaml index 1a85d351fe..3fae385422 100644 --- a/.agents/notes/implemented/feature/2026-09-07-composer-session-stats-pills.i18n.yaml +++ b/.agents/notes/implemented/feature/2026-09-07-composer-session-stats-pills.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-09-07-composer-session-stats-pills.md -2026-09-07-composer-session-stats-pills.md: 323a90067b11dd9419469e47e9b3bcdbf0078e5c -2026-09-07-composer-session-stats-pills.zh.md: 00a63e2800f0cf2441ad2a233c3d5f206ca7545c +2026-09-07-composer-session-stats-pills.md: 818c3c156f72816eb6540d505782be8e03dc6aa0 +2026-09-07-composer-session-stats-pills.zh.md: 8a74cddd3176026d799fb5b4223a51371ee8e253 diff --git a/.agents/notes/implemented/feature/2026-09-07-composer-session-stats-pills.md b/.agents/notes/implemented/feature/2026-09-07-composer-session-stats-pills.md index 323a90067b..818c3c156f 100644 --- a/.agents/notes/implemented/feature/2026-09-07-composer-session-stats-pills.md +++ b/.agents/notes/implemented/feature/2026-09-07-composer-session-stats-pills.md @@ -6,27 +6,26 @@ English | [中文](2026-09-07-composer-session-stats-pills.zh.md) ## Problem -The session stats strip under the composer (`StatsLine`, ui-chat, mounted on `conversation.composer.dock`) rendered every figure as one resident text line: turn/step counts, LLM and tool wall times, TTFT/TPS averages, token totals with cache-hit share, and context occupancy. The line crowded as figures accumulated, exact token values hid behind a hover tooltip with its own `ResizeObserver` overflow measurement, and the flat text gave no grouping — time figures and billing figures read as one undifferentiated row. An in-page A/B against a two-pill variant settled the direction: the pills won on scannability and on giving each figure family a home. +The session stats strip under the composer (`StatsLine`, ui-chat, mounted on `conversation.composer.dock`) rendered every figure as one resident text line: turn/step counts, LLM and tool wall times, TTFT/TPS averages, and compact token totals with cache-hit share. The line crowded as figures accumulated, exact token counts appeared nowhere (the `ResizeObserver`-measured hover tooltip only restated the same compact line when it truncated), and the flat text gave no grouping — time figures and billing figures read as one undifferentiated row. An in-page A/B against a two-pill variant settled the direction: the pills won on scannability and on giving each figure family a home. ## Decision -`StatsPills` (packages/client/ui-chat/src/client/chat/StatsPills.tsx) replaces `StatsLine` on the same `conversation.composer.dock` slot; the losing variant is deleted, its shared helpers (`deriveStats`, `formatDuration`, `cacheHitPercent`, `billedInputTokens`) absorbed into the new module, and the dead `stats.elapsed` / `stats.tps` locale keys removed. +`StatsPills` (packages/client/ui-chat/src/client/chat/StatsPills.tsx) replaces `StatsLine` on the same `conversation.composer.dock` slot; the losing variant is deleted, its shared helpers (`deriveStats`, `formatDuration`, `cacheHitPercent`, `billedInputTokens`) absorbed into the new module, and the dead `stats.llm`, `stats.toolCall`, `stats.ttftAverage`, `stats.tokensPerSecond`, and `stats.tokens` locale keys removed. -- **Two icon pills, two dialogs.** A gauge pill (new `IconGaugeOutline16`, dial center optically dropped to y=8.75 because the bottom-open arc reads high) shows `{turns} 轮 {steps} 步` plus output TPS and click-opens the 会话统计 dialog (LLM time, tool time, average TTFT, TPS). A database pill (`IconDatabaseOutline16`) shows the compact billed total plus cache-hit share and click-opens the Token 用量 dialog (cache hit, uncached input, cache read, cache write, output — exact counts). Both dialogs wear the shared `stat-dialog` module (portal panel, anchored placement, outside-dismiss) extracted for exactly this two-consumer split. -- **Data sourcing is unchanged in architecture.** Counts and times prefer the durable `sessionStats` projection with the window fold as the assembly-without-the-unit fallback ([whole-session counts](../../archived/bug-fix/2026-08-12-full-session-turn-step-counts.md)); token figures ride `tokenUsage` only, so an absent projection drops the usage pill rather than showing window-derived billing. Cache writes stay in the billed total and the cache-hit denominator ([projection decision](../architecture/2026-07-29-projected-token-usage-and-request-context.md)). Context occupancy left the strip entirely — the composer's ContextMeter ring owns it. +- **Two icon pills, two dialogs.** A gauge pill (new `IconGaugeOutline16`, dial center optically dropped to y=8.75 because the bottom-open arc reads high) shows `{turns} 轮 {steps} 步` plus output TPS and click-opens the 会话统计 dialog (LLM time, tool time, average TTFT, TPS); a log with no timed figure would open an empty dialog, so that pill renders as a static reading instead of a button. A database pill (`IconDatabaseOutline16`) shows the compact billed total plus cache-hit share and click-opens the Token 用量 dialog (cache hit, uncached input, cache read, cache write, output — exact counts). Both dialogs wear the shared `stat-dialog` module (portal panel, anchored placement, outside-dismiss, optionally externally owned open state) extracted for exactly this two-consumer split; the pills row owns one exclusive open slot, so opening either dialog closes the other, and each button carries an explicit `aria-label` that separates with ` · ` the segments the aria-hidden sep glyph joins visually. +- **Data sourcing is unchanged in architecture.** Counts and times prefer the durable `sessionStats` projection with the window fold as the assembly-without-the-unit fallback ([whole-session counts](../../archived/bug-fix/2026-08-12-full-session-turn-step-counts.md)); token figures ride `tokenUsage` only, so an absent projection drops the usage pill rather than showing window-derived billing. Cache writes stay in the billed total and the cache-hit denominator ([projection decision](../architecture/2026-07-29-projected-token-usage-and-request-context.md)). Context occupancy stays on the composer's ContextMeter ring, where it already lived beside `StatsLine` — the strip never carried it. - **Render discipline.** The row folds settled nodes only (`chat.legacy.nodes` identity), so streaming chunk frames cause zero rerenders — pinned by a render-count unit test. A session with no closed step and no billed tokens renders nothing. - **`data-composer-stats` is a cross-package attribute contract.** The pills' root carries it; ui-conversation's `InputBar.module.css` `:has([data-composer-stats])` rule tightens the composer's bottom clearance to 4px when the row is mounted. The producer side pins the attribute in unit tests, following the `data-trigger-menu` precedent. ## Alternatives considered -- **The single-line variant (StatsLine, the A/B loser).** All figures resident in one text row with a hover tooltip for exact totals. Lost on crowding and discoverability: the tooltip hid exact values behind an unadvertised hover, and one row gave time and billing figures no visual grouping. +- **The single-line variant (StatsLine, the A/B loser).** All figures resident in one text row, with a hover tooltip restating the full line when it truncated. Lost on crowding and reach: exact token counts appeared nowhere (the line and its tooltip both carried compact totals only), and one row gave time and billing figures no visual grouping. - **Three resident groups with one shared dialog.** An intermediate iteration kept counts, time, and tokens as three inline groups. Two pills won because the time/usage split matches the two underlying projections one-to-one and each pill's icon telegraphs its dialog. -- **Keeping context occupancy in the strip.** Rejected: the ContextMeter ring beside the composer already shows occupancy continuously; repeating it in the strip spent width on a duplicate. - **Extracting the dl bucket rows shared with `TurnUsagePanel`.** The session-total dialog and the per-turn panel render the same skin but different contracts (all buckets always present vs optional per-turn fields plus model routes); a shared component would be conditionals around nine lines. The mirror is marked `jscpd:ignore` with the reason inline. ## Consequences - `ChatSnapshotBuilder`'s legacy slice now serves StatsPills; the [node-assembly note](../architecture/2026-08-09-client-conversation-node-assembly.md) tracks that consumer rename. -- Exact figures cost a click instead of a hover; the strip itself carries only the two headline readings. -- Web e2e strip assertions match substring text inside the time pill; the paged-history scenario's aria golden pins the two-pill structure. +- Exact token counts become reachable at all — one click — where `StatsLine` showed only compact totals; the strip itself carries only the two headline readings. +- Web e2e strip assertions match substring text inside the time pill; the fresh-round-trip aria goldens pin the two-pill structure, and stats-paged-history pins the counts reading alone over a log with no billed tokens. - The `conversation.composer.dock` occupant in the generated slot catalog is `client-ui-chat StatsPills id 'stats'`. diff --git a/.agents/notes/implemented/feature/2026-09-07-composer-session-stats-pills.zh.md b/.agents/notes/implemented/feature/2026-09-07-composer-session-stats-pills.zh.md index 00a63e2800..8a74cddd31 100644 --- a/.agents/notes/implemented/feature/2026-09-07-composer-session-stats-pills.zh.md +++ b/.agents/notes/implemented/feature/2026-09-07-composer-session-stats-pills.zh.md @@ -6,27 +6,26 @@ Status: implemented ## 问题 -输入框下方的会话统计条(`StatsLine`,ui-chat,挂载于 `conversation.composer.dock`)把所有数字渲染成一整行常驻文本:轮/步计数、模型与工具用时、TTFT/TPS 均值、token 总量与缓存命中率、上下文占用。数字越多行越拥挤,精确 token 值藏在带独立 `ResizeObserver` 溢出测量的悬停提示里,纯文本也没有分组——时间类数字和计费类数字读起来混作一行。一次页面内 A/B 对比双 pill 变体后方向定案:pill 方案在可扫读性和"每类数字有归属"上胜出。 +输入框下方的会话统计条(`StatsLine`,ui-chat,挂载于 `conversation.composer.dock`)把所有数字渲染成一整行常驻文本:轮/步计数、模型与工具用时、TTFT/TPS 均值、紧凑 token 总量与缓存命中率。数字越多行越拥挤,精确 token 计数无处可看(带 `ResizeObserver` 溢出测量的悬停提示只在截断时复述同一行紧凑文本),纯文本也没有分组——时间类数字和计费类数字读起来混作一行。一次页面内 A/B 对比双 pill 变体后方向定案:pill 方案在可扫读性和"每类数字有归属"上胜出。 ## 决定 -`StatsPills`(packages/client/ui-chat/src/client/chat/StatsPills.tsx)在同一 `conversation.composer.dock` 插槽上取代 `StatsLine`;落选变体已删除,其共享工具函数(`deriveStats`、`formatDuration`、`cacheHitPercent`、`billedInputTokens`)并入新模块,废弃的 `stats.elapsed` / `stats.tps` 文案键一并移除。 +`StatsPills`(packages/client/ui-chat/src/client/chat/StatsPills.tsx)在同一 `conversation.composer.dock` 插槽上取代 `StatsLine`;落选变体已删除,其共享工具函数(`deriveStats`、`formatDuration`、`cacheHitPercent`、`billedInputTokens`)并入新模块,废弃的 `stats.llm`、`stats.toolCall`、`stats.ttftAverage`、`stats.tokensPerSecond`、`stats.tokens` 文案键一并移除。 -- **两个图标 pill、两个弹层。** 仪表盘 pill(新增 `IconGaugeOutline16`,因下开口圆弧视觉偏高而把表盘中心光学下移到 y=8.75)展示 `{turns} 轮 {steps} 步` 加输出 TPS,点击打开「会话统计」弹层(模型用时、工具调用用时、首 token 平均、输出速度)。数据库 pill(`IconDatabaseOutline16`)展示紧凑计费总量加缓存命中率,点击打开「Token 用量」弹层(缓存命中、未缓存输入、缓存读取、缓存写入、输出——精确计数)。两个弹层共用为这两处消费者抽出的 `stat-dialog` 模块(portal 面板、锚定定位、点击外部关闭)。 -- **数据来源架构不变。** 计数与用时优先读取持久的 `sessionStats` 投影,窗口折叠仅作无该单元装配时的回退([全会话计数](../../archived/bug-fix/2026-08-12-full-session-turn-step-counts.md));token 数字只走 `tokenUsage`,投影缺席时直接不渲染用量 pill,而非展示窗口推算的计费。缓存写入仍计入计费总量与缓存命中分母([投影决定](../architecture/2026-07-29-projected-token-usage-and-request-context.zh.md))。上下文占用彻底离开统计条——由输入框旁的 ContextMeter 圆环独占。 +- **两个图标 pill、两个弹层。** 仪表盘 pill(新增 `IconGaugeOutline16`,因下开口圆弧视觉偏高而把表盘中心光学下移到 y=8.75)展示 `{turns} 轮 {steps} 步` 加输出 TPS,点击打开「会话统计」弹层(模型用时、工具调用用时、首 token 平均、输出速度);日志里没有任何计时数字时弹层会是空的,此时该 pill 渲染为静态读数而非按钮。数据库 pill(`IconDatabaseOutline16`)展示紧凑计费总量加缓存命中率,点击打开「Token 用量」弹层(缓存命中、未缓存输入、缓存读取、缓存写入、输出——精确计数)。两个弹层共用为这两处消费者抽出的 `stat-dialog` 模块(portal 面板、锚定定位、点击外部关闭、可选的外部持有开合状态);pill 行持有唯一的互斥开合槽位,打开任一弹层即关闭另一个,且每个按钮携带显式 `aria-label`,用 ` · ` 分隔 aria-hidden 分隔符在视觉上连接的两段文本。 +- **数据来源架构不变。** 计数与用时优先读取持久的 `sessionStats` 投影,窗口折叠仅作无该单元装配时的回退([全会话计数](../../archived/bug-fix/2026-08-12-full-session-turn-step-counts.md));token 数字只走 `tokenUsage`,投影缺席时直接不渲染用量 pill,而非展示窗口推算的计费。缓存写入仍计入计费总量与缓存命中分母([投影决定](../architecture/2026-07-29-projected-token-usage-and-request-context.zh.md))。上下文占用仍在输入框旁的 ContextMeter 圆环上,`StatsLine` 时代它就在那里——统计条从未承载过它。 - **渲染纪律。** 该行只折叠已定稿节点(`chat.legacy.nodes` 身份),流式 chunk 帧零重渲染——由渲染计数单测钉住。无已完成步且无计费 token 的会话什么都不渲染。 - **`data-composer-stats` 是跨包属性契约。** pill 行根元素携带它;ui-conversation 的 `InputBar.module.css` 用 `:has([data-composer-stats])` 在该行挂载时把输入框底部留白收紧到 4px。生产方在单测里钉住该属性,沿用 `data-trigger-menu` 先例。 ## 备选方案 -- **单行变体(StatsLine,A/B 落选方)。** 全部数字常驻一行文本,精确总量靠悬停提示。败在拥挤与可发现性:悬停入口无提示可寻,单行也无法给时间与计费数字分组。 +- **单行变体(StatsLine,A/B 落选方)。** 全部数字常驻一行文本,悬停提示只在截断时复述整行。败在拥挤与可达性:精确 token 计数无处可看(行内和提示里都只有紧凑总量),单行也无法给时间与计费数字分组。 - **三个常驻分组共享一个弹层。** 中间迭代曾把计数、时间、token 保持为三个内联分组。双 pill 胜出是因为时间/用量的切分与两个底层投影一一对应,且每个 pill 的图标直接预告其弹层内容。 -- **在统计条里保留上下文占用。** 否决:输入框旁的 ContextMeter 圆环已持续展示占用,统计条重复它是把宽度花在冗余上。 - **抽出与 `TurnUsagePanel` 共享的 dl 分桶行。** 会话总量弹层与逐轮面板皮肤相同但契约不同(此处所有分桶恒在;逐轮字段可选且含模型路由);共享组件只是给九行代码包一层条件。该镜像以 `jscpd:ignore` 标注并内联说明理由。 ## 后果 - `ChatSnapshotBuilder` 的 legacy 切片现在服务于 StatsPills;[节点装配 note](../architecture/2026-08-09-client-conversation-node-assembly.zh.md) 已跟进该消费者更名。 -- 精确数字从悬停改为一次点击;统计条本身只承载两个头条读数。 -- Web e2e 对统计条的断言匹配时间 pill 内的子串文本;分页历史场景的 aria golden 钉住双 pill 结构。 +- 精确 token 计数第一次变得可达——一次点击即可;`StatsLine` 只展示过紧凑总量。统计条本身只承载两个头条读数。 +- Web e2e 对统计条的断言匹配时间 pill 内的子串文本;fresh-round-trip 的 aria golden 钉住双 pill 结构,stats-paged-history 则在无计费 token 的日志上钉住单独的计数读数。 - 生成的插槽目录中 `conversation.composer.dock` 占用者为 `client-ui-chat StatsPills id 'stats'`。 diff --git a/apps/web/tests/expected/skill-user-invoke/ui-expanded.expected.md b/apps/web/tests/expected/skill-user-invoke/ui-expanded.expected.md index 33c4a66a1c..c23c8653d0 100644 --- a/apps/web/tests/expected/skill-user-invoke/ui-expanded.expected.md +++ b/apps/web/tests/expected/skill-user-invoke/ui-expanded.expected.md @@ -51,9 +51,9 @@ - img - button "0% of context used" - button "Send message" [disabled] -- button "1 turns 1 steps{{throughput}} tok/s": +- button "1 turns 1 steps · {{throughput}} tok/s": - img - text: 1 turns 1 steps{{throughput}} tok/s -- button "272 tokCache hit 0%": +- button "272 tok · Cache hit 0%": - img - text: 272 tokCache hit 0% diff --git a/apps/web/tests/expected/skill-user-invoke/ui.expected.md b/apps/web/tests/expected/skill-user-invoke/ui.expected.md index 47bcd5d694..3fb23aad23 100644 --- a/apps/web/tests/expected/skill-user-invoke/ui.expected.md +++ b/apps/web/tests/expected/skill-user-invoke/ui.expected.md @@ -43,9 +43,9 @@ - img - button "0% of context used" - button "Send message" [disabled] -- button "1 turns 1 steps{{throughput}} tok/s": +- button "1 turns 1 steps · {{throughput}} tok/s": - img - text: 1 turns 1 steps{{throughput}} tok/s -- button "272 tokCache hit 0%": +- button "272 tok · Cache hit 0%": - img - text: 272 tokCache hit 0% diff --git a/apps/web/tests/expected/steer-all/settled-expanded.expected.md b/apps/web/tests/expected/steer-all/settled-expanded.expected.md index bb60fc9ee1..90f2455125 100644 --- a/apps/web/tests/expected/steer-all/settled-expanded.expected.md +++ b/apps/web/tests/expected/steer-all/settled-expanded.expected.md @@ -61,9 +61,9 @@ - img - button "0% of context used" - button "Send message" [disabled] -- button "1 turns 2 steps{{throughput}} tok/s": +- button "1 turns 2 steps · {{throughput}} tok/s": - img - text: 1 turns 2 steps{{throughput}} tok/s -- button "40 tokCache hit 0%": +- button "40 tok · Cache hit 0%": - img - text: 40 tokCache hit 0% diff --git a/apps/web/tests/expected/steer-all/settled.expected.md b/apps/web/tests/expected/steer-all/settled.expected.md index 66145a7073..592c56d935 100644 --- a/apps/web/tests/expected/steer-all/settled.expected.md +++ b/apps/web/tests/expected/steer-all/settled.expected.md @@ -49,9 +49,9 @@ - img - button "0% of context used" - button "Send message" [disabled] -- button "1 turns 2 steps{{throughput}} tok/s": +- button "1 turns 2 steps · {{throughput}} tok/s": - img - text: 1 turns 2 steps{{throughput}} tok/s -- button "40 tokCache hit 0%": +- button "40 tok · Cache hit 0%": - img - text: 40 tokCache hit 0% diff --git a/apps/web/tests/plan-review.e2e.ts b/apps/web/tests/plan-review.e2e.ts index ff9e3af4ee..b9ae32cec0 100644 --- a/apps/web/tests/plan-review.e2e.ts +++ b/apps/web/tests/plan-review.e2e.ts @@ -97,6 +97,10 @@ describe('web e2e: plan review takeover round trip', () => { } await card.getByRole('button', { name: 'Approve' }).click() + // Park the pointer: the card unmounts and the ContextMeter ring lands + // under the click position, whose 200ms hover delay would arm a tooltip + // into the aria captures below. + await page.mouse.move(0, 0) const sessionId = await settled if (MODE === 'record') { diff --git a/packages/client/ui-chat/src/client/chat/StatsPills.module.css b/packages/client/ui-chat/src/client/chat/StatsPills.module.css index a3e9d5a901..22f39a1f26 100644 --- a/packages/client/ui-chat/src/client/chat/StatsPills.module.css +++ b/packages/client/ui-chat/src/client/chat/StatsPills.module.css @@ -36,7 +36,6 @@ font-variant-numeric: tabular-nums; line-height: inherit; white-space: nowrap; - cursor: pointer; } .pill svg { @@ -45,8 +44,14 @@ flex: none; } -.pill:hover, -.pill[aria-expanded='true'] { +/* Only the button form (a pill with dialog rows to open) invites interaction; + the static span form keeps the resting tertiary reading. */ +button.pill { + cursor: pointer; +} + +button.pill:hover, +button.pill[aria-expanded='true'] { background: var(--dsw-alias-interactive-bg-hover); color: var(--dsw-alias-label-secondary); } diff --git a/packages/client/ui-chat/src/client/chat/StatsPills.tsx b/packages/client/ui-chat/src/client/chat/StatsPills.tsx index 14f4f8623c..091352dac1 100644 --- a/packages/client/ui-chat/src/client/chat/StatsPills.tsx +++ b/packages/client/ui-chat/src/client/chat/StatsPills.tsx @@ -5,7 +5,7 @@ // Mounted on 'conversation.composer.dock' so it sticks with the composer in the // active conversation scrollport (see ConversationRoot data-conversation-scroll). -import { memo, useMemo } from 'react' +import { memo, useMemo, useState } from 'react' import { createPortal } from 'react-dom' import { IconDatabaseOutline16, IconGaugeOutline16 } from '@deepseek-ai/dsh-client-ui-primitives' import type { UseProjection } from '@deepseek-ai/dsh-api-session-controller/client' @@ -132,8 +132,44 @@ function exactCount(value: number, t: ChatViewSlotProps['t']): string { return t('message.turnUsage.count', { count: formatExactTokens(value, t) }) } -function TimePill({ stats, t }: { stats: WindowStats; t: ChatViewSlotProps['t'] }) { - const { open, setOpen, rootRef, panelRef, pos } = useStatDialog() +/** External open state one pill's dialog reads and writes (the row's exclusive slot). */ +type PillDialog = Pick, 'open' | 'setOpen'> + +function TimePill({ stats, t, dialog }: { + stats: WindowStats + t: ChatViewSlotProps['t'] + dialog: PillDialog +}) { + const { open, setOpen, rootRef, panelRef, pos } = useStatDialog(dialog) + const counts = t('stats.counts', { turns: stats.turns, steps: stats.steps }) + const tps = stats.decodeMs > 0 + ? t('message.tokensPerSecond', { + tps: formatTokensPerSecond(stats.decodeTokens / (stats.decodeMs / 1_000)), + }) + : null + const label = ( + + {counts} + {tps !== null && ( + <> + · + {tps} + + )} + + ) + // A window without one timed figure has no dialog rows to show, so the pill + // stays a plain reading instead of a button opening an empty dialog. + if (stats.llmMs <= 0 && stats.toolMs <= 0 && stats.ttftSteps <= 0 && stats.decodeMs <= 0) { + return ( + + + + {label} + + + ) + } return ( {open && createPortal(
diff --git a/packages/client/ui-conversation/tests/context-meter.client.spec.tsx b/packages/client/ui-conversation/tests/context-meter.client.spec.tsx index acf1156ca1..90d69e64cc 100644 --- a/packages/client/ui-conversation/tests/context-meter.client.spec.tsx +++ b/packages/client/ui-conversation/tests/context-meter.client.spec.tsx @@ -54,7 +54,7 @@ describe('ContextMeter', () => { expect(view.container.querySelector('[role="dialog"]')).toBeNull() fireEvent.click(trigger) const panel = view.container.querySelector('[role="dialog"]')! - expect(panel.textContent).toContain('32K / 128K') + expect(panel.textContent).toContain('~32K / 128K') expect(panel.textContent).toContain('25%') expect(panel.textContent).toContain('上下文已用') expect(panel.textContent).toContain('系统提示词~120') @@ -94,7 +94,7 @@ describe('ContextMeter', () => { // `.segment` carries a min-width, so a zero-width part would still paint a // filled sliver over an empty context. expect(panel.getElementsByClassName(segmentClass)).toHaveLength(0) - expect(panel.textContent).toContain('0 / 128K') + expect(panel.textContent).toContain('~0 / 128K') }) it('reads the ring from the projected figure so a compaction shows at once', () => { @@ -106,14 +106,14 @@ describe('ContextMeter', () => { }) const trigger = view.getByRole('button', { name: '上下文已用 2%' }) fireEvent.click(trigger) - expect(view.container.querySelector('[role="dialog"]')!.textContent).toContain('3K / 128K') + expect(view.container.querySelector('[role="dialog"]')!.textContent).toContain('~3K / 128K') }) it('omits the composition rows while the contextBreakdown projection is absent', () => { const view = meter({ contextPressure: { pressureTokens: 32_000, contextWindow: 128_000 } }) fireEvent.click(view.getByRole('button', { name: '上下文已用 25%' })) const panel = view.container.querySelector('[role="dialog"]')! - expect(panel.textContent).toContain('32K / 128K') + expect(panel.textContent).toContain('~32K / 128K') expect(panel.textContent).not.toContain('系统提示词') expect(panel.textContent).not.toContain('对话消息') // Without composition shares, the bar falls back to one plain segment. diff --git a/packages/client/ui-model-selection/src/client/ModelSelect.tsx b/packages/client/ui-model-selection/src/client/ModelSelect.tsx index b7b212b609..e9fc0195ef 100644 --- a/packages/client/ui-model-selection/src/client/ModelSelect.tsx +++ b/packages/client/ui-model-selection/src/client/ModelSelect.tsx @@ -127,9 +127,9 @@ export function ModelSelect( // anchor rect, measured before paint, clamped inside the viewport): above // the trigger, right edges aligned. Depends on pane and directory state // because pane switches and async catalog loads resize the card. - /* jscpd:ignore-start -- deliberate mirror of ui-primitives Menu's clamped fixed - placement; this select keeps its own pane/reload lifecycle instead of adopting - Menu's item model, so only the measure-and-clamp plumbing repeats. */ + /* jscpd:ignore-start -- deliberate mirror of ui-primitives useAnchoredPosition: + that hook only places from the anchor's LEFT edge, while this card aligns + right edges (x = rect.right - width), so the measure-and-clamp plumbing repeats. */ useLayoutEffect(() => { if (!open) { setMenuPos(null); return } const place = (): void => { diff --git a/snapshots/web/bash-abort-row/ui.expected.md b/snapshots/web/bash-abort-row/ui.expected.md index e9b2ecb716..2548db42e3 100644 --- a/snapshots/web/bash-abort-row/ui.expected.md +++ b/snapshots/web/bash-abort-row/ui.expected.md @@ -36,9 +36,9 @@ - text: DeepSeek-V4-Flash - img - button "Send message" [disabled] -- button "1 turns 1 steps{{throughput}} tok/s": +- button "1 turns 1 steps · {{throughput}} tok/s": - img - text: 1 turns 1 steps{{throughput}} tok/s -- button "20 tokCache hit 0%": +- button "20 tok · Cache hit 0%": - img - text: 20 tokCache hit 0% diff --git a/snapshots/web/cordis-tool-round/ui.expected.md b/snapshots/web/cordis-tool-round/ui.expected.md index 329f877fd8..587d47410f 100644 --- a/snapshots/web/cordis-tool-round/ui.expected.md +++ b/snapshots/web/cordis-tool-round/ui.expected.md @@ -128,6 +128,6 @@ - button "3 turns 7 steps": - img - text: 3 turns 7 steps -- button "66.8K tokCache hit 77%": +- button "66.8K tok · Cache hit 77%": - img - text: 66.8K tokCache hit 77% diff --git a/snapshots/web/feedback-command/ack-expanded.expected.md b/snapshots/web/feedback-command/ack-expanded.expected.md index e66ccb4aac..28193b78d5 100644 --- a/snapshots/web/feedback-command/ack-expanded.expected.md +++ b/snapshots/web/feedback-command/ack-expanded.expected.md @@ -55,9 +55,9 @@ - img - button "6% of context used" - button "Send message" [disabled] -- button "1 turns 1 steps{{throughput}} tok/s": +- button "1 turns 1 steps · {{throughput}} tok/s": - img - text: 1 turns 1 steps{{throughput}} tok/s -- button "7.8K tokCache hit 99%": +- button "7.8K tok · Cache hit 99%": - img - text: 7.8K tokCache hit 99% diff --git a/snapshots/web/feedback-command/ack.expected.md b/snapshots/web/feedback-command/ack.expected.md index afd5e2c16b..9b5ec817a4 100644 --- a/snapshots/web/feedback-command/ack.expected.md +++ b/snapshots/web/feedback-command/ack.expected.md @@ -47,9 +47,9 @@ - img - button "6% of context used" - button "Send message" [disabled] -- button "1 turns 1 steps{{throughput}} tok/s": +- button "1 turns 1 steps · {{throughput}} tok/s": - img - text: 1 turns 1 steps{{throughput}} tok/s -- button "7.8K tokCache hit 99%": +- button "7.8K tok · Cache hit 99%": - img - text: 7.8K tokCache hit 99% diff --git a/snapshots/web/feedback-release/ack-expanded.expected.md b/snapshots/web/feedback-release/ack-expanded.expected.md index e66ccb4aac..28193b78d5 100644 --- a/snapshots/web/feedback-release/ack-expanded.expected.md +++ b/snapshots/web/feedback-release/ack-expanded.expected.md @@ -55,9 +55,9 @@ - img - button "6% of context used" - button "Send message" [disabled] -- button "1 turns 1 steps{{throughput}} tok/s": +- button "1 turns 1 steps · {{throughput}} tok/s": - img - text: 1 turns 1 steps{{throughput}} tok/s -- button "7.8K tokCache hit 99%": +- button "7.8K tok · Cache hit 99%": - img - text: 7.8K tokCache hit 99% diff --git a/snapshots/web/feedback-release/ack.expected.md b/snapshots/web/feedback-release/ack.expected.md index afd5e2c16b..9b5ec817a4 100644 --- a/snapshots/web/feedback-release/ack.expected.md +++ b/snapshots/web/feedback-release/ack.expected.md @@ -47,9 +47,9 @@ - img - button "6% of context used" - button "Send message" [disabled] -- button "1 turns 1 steps{{throughput}} tok/s": +- button "1 turns 1 steps · {{throughput}} tok/s": - img - text: 1 turns 1 steps{{throughput}} tok/s -- button "7.8K tokCache hit 99%": +- button "7.8K tok · Cache hit 99%": - img - text: 7.8K tokCache hit 99% diff --git a/snapshots/web/file-upload-round/ui.expected.md b/snapshots/web/file-upload-round/ui.expected.md index 1422236acc..d2f9dc2e6d 100644 --- a/snapshots/web/file-upload-round/ui.expected.md +++ b/snapshots/web/file-upload-round/ui.expected.md @@ -49,9 +49,9 @@ - img - button "1% of context used" - button "Send message" [disabled] -- button "1 turns 2 steps{{throughput}} tok/s": +- button "1 turns 2 steps · {{throughput}} tok/s": - img - text: 1 turns 2 steps{{throughput}} tok/s -- button "17.3K tokCache hit 51%": +- button "17.3K tok · Cache hit 51%": - img - text: 17.3K tokCache hit 51% diff --git a/snapshots/web/fresh-round-trip/ui-expanded.expected.md b/snapshots/web/fresh-round-trip/ui-expanded.expected.md index aa560b0678..06b489df09 100644 --- a/snapshots/web/fresh-round-trip/ui-expanded.expected.md +++ b/snapshots/web/fresh-round-trip/ui-expanded.expected.md @@ -59,9 +59,9 @@ - img - button "6% of context used" - button "Send message" [disabled] -- button "1 turns 2 steps{{throughput}} tok/s": +- button "1 turns 2 steps · {{throughput}} tok/s": - img - text: 1 turns 2 steps{{throughput}} tok/s -- button "15.8K tokCache hit 99%": +- button "15.8K tok · Cache hit 99%": - img - text: 15.8K tokCache hit 99% diff --git a/snapshots/web/fresh-round-trip/ui.expected.md b/snapshots/web/fresh-round-trip/ui.expected.md index d4ab10e3b4..c9c3b15ba2 100644 --- a/snapshots/web/fresh-round-trip/ui.expected.md +++ b/snapshots/web/fresh-round-trip/ui.expected.md @@ -43,9 +43,9 @@ - img - button "6% of context used" - button "Send message" [disabled] -- button "1 turns 2 steps{{throughput}} tok/s": +- button "1 turns 2 steps · {{throughput}} tok/s": - img - text: 1 turns 2 steps{{throughput}} tok/s -- button "15.8K tokCache hit 99%": +- button "15.8K tok · Cache hit 99%": - img - text: 15.8K tokCache hit 99% diff --git a/snapshots/web/goal-multi-turn-actions/ui-expanded.expected.md b/snapshots/web/goal-multi-turn-actions/ui-expanded.expected.md index e749d9c8ae..2de8968039 100644 --- a/snapshots/web/goal-multi-turn-actions/ui-expanded.expected.md +++ b/snapshots/web/goal-multi-turn-actions/ui-expanded.expected.md @@ -237,9 +237,9 @@ - img - button "9% of context used" - button "Send message" [disabled] -- button "2 turns 12 steps{{throughput}} tok/s": +- button "2 turns 12 steps · {{throughput}} tok/s": - img - text: 2 turns 12 steps{{throughput}} tok/s -- button "115K tokCache hit 91%": +- button "115K tok · Cache hit 91%": - img - text: 115K tokCache hit 91% diff --git a/snapshots/web/goal-multi-turn-actions/ui.expected.md b/snapshots/web/goal-multi-turn-actions/ui.expected.md index 645c2195d4..ef11e9265e 100644 --- a/snapshots/web/goal-multi-turn-actions/ui.expected.md +++ b/snapshots/web/goal-multi-turn-actions/ui.expected.md @@ -125,9 +125,9 @@ - img - button "9% of context used" - button "Send message" [disabled] -- button "2 turns 12 steps{{throughput}} tok/s": +- button "2 turns 12 steps · {{throughput}} tok/s": - img - text: 2 turns 12 steps{{throughput}} tok/s -- button "115K tokCache hit 91%": +- button "115K tok · Cache hit 91%": - img - text: 115K tokCache hit 91% diff --git a/snapshots/web/lifecycle-chrome/reloaded-expanded.expected.md b/snapshots/web/lifecycle-chrome/reloaded-expanded.expected.md index 3ac2826294..e0dd5984d9 100644 --- a/snapshots/web/lifecycle-chrome/reloaded-expanded.expected.md +++ b/snapshots/web/lifecycle-chrome/reloaded-expanded.expected.md @@ -51,9 +51,9 @@ - img - button "8% of context used" - button "Send message" [disabled] -- button "1 turns 1 steps{{throughput}} tok/s": +- button "1 turns 1 steps · {{throughput}} tok/s": - img - text: 1 turns 1 steps{{throughput}} tok/s -- button "10K tokCache hit 99.5%": +- button "10K tok · Cache hit 99.5%": - img - text: 10K tokCache hit 99.5% diff --git a/snapshots/web/lifecycle-chrome/reloaded.expected.md b/snapshots/web/lifecycle-chrome/reloaded.expected.md index 4d26c1355f..289e2559f8 100644 --- a/snapshots/web/lifecycle-chrome/reloaded.expected.md +++ b/snapshots/web/lifecycle-chrome/reloaded.expected.md @@ -43,9 +43,9 @@ - img - button "8% of context used" - button "Send message" [disabled] -- button "1 turns 1 steps{{throughput}} tok/s": +- button "1 turns 1 steps · {{throughput}} tok/s": - img - text: 1 turns 1 steps{{throughput}} tok/s -- button "10K tokCache hit 99.5%": +- button "10K tok · Cache hit 99.5%": - img - text: 10K tokCache hit 99.5% diff --git a/snapshots/web/live-interactions/error-auth.expected.md b/snapshots/web/live-interactions/error-auth.expected.md index 2bdb5e39fd..67b7a9a42c 100644 --- a/snapshots/web/live-interactions/error-auth.expected.md +++ b/snapshots/web/live-interactions/error-auth.expected.md @@ -33,6 +33,5 @@ - text: DeepSeek-V4-Flash - img - button "Send message" [disabled] -- button "1 turns 1 steps": - - img - - text: 1 turns 1 steps +- img +- text: 1 turns 1 steps diff --git a/snapshots/web/live-interactions/retry-exhausted.expected.md b/snapshots/web/live-interactions/retry-exhausted.expected.md index 08941fd929..c9cc4e92fd 100644 --- a/snapshots/web/live-interactions/retry-exhausted.expected.md +++ b/snapshots/web/live-interactions/retry-exhausted.expected.md @@ -35,6 +35,5 @@ - text: DeepSeek-V4-Flash - img - button "Send message" [disabled] -- button "1 turns 1 steps": - - img - - text: 1 turns 1 steps +- img +- text: 1 turns 1 steps diff --git a/snapshots/web/live-interactions/retry-expanded.expected.md b/snapshots/web/live-interactions/retry-expanded.expected.md index 65064c68b0..26f6602e64 100644 --- a/snapshots/web/live-interactions/retry-expanded.expected.md +++ b/snapshots/web/live-interactions/retry-expanded.expected.md @@ -53,9 +53,9 @@ - img - button "6% of context used" - button "Send message" [disabled] -- button "1 turns 1 steps{{throughput}} tok/s": +- button "1 turns 1 steps · {{throughput}} tok/s": - img - text: 1 turns 1 steps{{throughput}} tok/s -- button "7.9K tokCache hit 99%": +- button "7.9K tok · Cache hit 99%": - img - text: 7.9K tokCache hit 99% diff --git a/snapshots/web/live-interactions/retry.expected.md b/snapshots/web/live-interactions/retry.expected.md index 7d8520fccf..d006bb1f57 100644 --- a/snapshots/web/live-interactions/retry.expected.md +++ b/snapshots/web/live-interactions/retry.expected.md @@ -43,9 +43,9 @@ - img - button "6% of context used" - button "Send message" [disabled] -- button "1 turns 1 steps{{throughput}} tok/s": +- button "1 turns 1 steps · {{throughput}} tok/s": - img - text: 1 turns 1 steps{{throughput}} tok/s -- button "7.9K tokCache hit 99%": +- button "7.9K tok · Cache hit 99%": - img - text: 7.9K tokCache hit 99% diff --git a/snapshots/web/message-actions/ui.expected.md b/snapshots/web/message-actions/ui.expected.md index ab79b2d21b..8902497958 100644 --- a/snapshots/web/message-actions/ui.expected.md +++ b/snapshots/web/message-actions/ui.expected.md @@ -72,9 +72,9 @@ - text: DeepSeek-V4-Flash - img - button "Send message" [disabled] -- button "2 turns 3 steps{{throughput}} tok/s": +- button "2 turns 3 steps · {{throughput}} tok/s": - img - text: 2 turns 3 steps{{throughput}} tok/s -- button "7.9K tokCache hit 98%": +- button "7.9K tok · Cache hit 98%": - img - text: 7.9K tokCache hit 98% diff --git a/snapshots/web/minimal-preset/ui.expected.md b/snapshots/web/minimal-preset/ui.expected.md index 21fde05e19..3d399d9cf3 100644 --- a/snapshots/web/minimal-preset/ui.expected.md +++ b/snapshots/web/minimal-preset/ui.expected.md @@ -48,9 +48,9 @@ - img - button "0% of context used" - button "Send message" [disabled] -- button "1 turns 2 steps{{throughput}} tok/s": +- button "1 turns 2 steps · {{throughput}} tok/s": - img - text: 1 turns 2 steps{{throughput}} tok/s -- button "28 tokCache hit 0%": +- button "28 tok · Cache hit 0%": - img - text: 28 tokCache hit 0% diff --git a/snapshots/web/plan-review/approved-expanded.expected.md b/snapshots/web/plan-review/approved-expanded.expected.md index 4d70ae9cb9..c4aa7e6870 100644 --- a/snapshots/web/plan-review/approved-expanded.expected.md +++ b/snapshots/web/plan-review/approved-expanded.expected.md @@ -69,9 +69,9 @@ - img - button "4% of context used" - button "Send message" [disabled] -- button "1 turns 2 steps{{throughput}} tok/s": +- button "1 turns 2 steps · {{throughput}} tok/s": - img - text: 1 turns 2 steps{{throughput}} tok/s -- button "10.6K tokCache hit 51%": +- button "10.6K tok · Cache hit 51%": - img - text: 10.6K tokCache hit 51% diff --git a/snapshots/web/plan-review/approved.expected.md b/snapshots/web/plan-review/approved.expected.md index 30936753c8..6d27f3191a 100644 --- a/snapshots/web/plan-review/approved.expected.md +++ b/snapshots/web/plan-review/approved.expected.md @@ -48,11 +48,10 @@ - text: DeepSeek-V4-Flash - img - button "4% of context used" -- tooltip "4% of context used" - button "Send message" [disabled] -- button "1 turns 2 steps{{throughput}} tok/s": +- button "1 turns 2 steps · {{throughput}} tok/s": - img - text: 1 turns 2 steps{{throughput}} tok/s -- button "10.6K tokCache hit 51%": +- button "10.6K tok · Cache hit 51%": - img - text: 10.6K tokCache hit 51% diff --git a/snapshots/web/ptc-round/ui.expected.md b/snapshots/web/ptc-round/ui.expected.md index 7ab68fd36d..e609af1f2e 100644 --- a/snapshots/web/ptc-round/ui.expected.md +++ b/snapshots/web/ptc-round/ui.expected.md @@ -70,9 +70,9 @@ - img - button "7% of context used" - button "Send message" [disabled] -- button "1 turns 2 steps{{throughput}} tok/s": +- button "1 turns 2 steps · {{throughput}} tok/s": - img - text: 1 turns 2 steps{{throughput}} tok/s -- button "17.5K tokCache hit 52%": +- button "17.5K tok · Cache hit 52%": - img - text: 17.5K tokCache hit 52% diff --git a/snapshots/web/question-composer/answered-expanded.expected.md b/snapshots/web/question-composer/answered-expanded.expected.md index bd0145230c..d81b0ca0af 100644 --- a/snapshots/web/question-composer/answered-expanded.expected.md +++ b/snapshots/web/question-composer/answered-expanded.expected.md @@ -61,9 +61,9 @@ - img - button "3% of context used" - button "Send message" [disabled] -- button "1 turns 2 steps{{throughput}} tok/s": +- button "1 turns 2 steps · {{throughput}} tok/s": - img - text: 1 turns 2 steps{{throughput}} tok/s -- button "8.8K tokCache hit 95%": +- button "8.8K tok · Cache hit 95%": - img - text: 8.8K tokCache hit 95% diff --git a/snapshots/web/question-composer/answered.expected.md b/snapshots/web/question-composer/answered.expected.md index d9c84ba575..b6a940d29f 100644 --- a/snapshots/web/question-composer/answered.expected.md +++ b/snapshots/web/question-composer/answered.expected.md @@ -43,9 +43,9 @@ - img - button "3% of context used" - button "Send message" [disabled] -- button "1 turns 2 steps{{throughput}} tok/s": +- button "1 turns 2 steps · {{throughput}} tok/s": - img - text: 1 turns 2 steps{{throughput}} tok/s -- button "8.8K tokCache hit 95%": +- button "8.8K tok · Cache hit 95%": - img - text: 8.8K tokCache hit 95% diff --git a/snapshots/web/question-composer/cancelled.expected.md b/snapshots/web/question-composer/cancelled.expected.md index bb933ec66b..717feee682 100644 --- a/snapshots/web/question-composer/cancelled.expected.md +++ b/snapshots/web/question-composer/cancelled.expected.md @@ -40,9 +40,9 @@ - img - button "3% of context used" - button "Send message" [disabled] -- button "1 turns 1 steps{{throughput}} tok/s": +- button "1 turns 1 steps · {{throughput}} tok/s": - img - text: 1 turns 1 steps{{throughput}} tok/s -- button "4.4K tokCache hit 97%": +- button "4.4K tok · Cache hit 97%": - img - text: 4.4K tokCache hit 97% diff --git a/snapshots/web/queued-image/delivered.expected.md b/snapshots/web/queued-image/delivered.expected.md index 557eb295db..b1982b86f8 100644 --- a/snapshots/web/queued-image/delivered.expected.md +++ b/snapshots/web/queued-image/delivered.expected.md @@ -87,9 +87,9 @@ - img - button "6% of context used" - button "Send message" [disabled] -- button "3 turns 3 steps{{throughput}} tok/s": +- button "3 turns 3 steps · {{throughput}} tok/s": - img - text: 3 turns 3 steps{{throughput}} tok/s -- button "15.7K tokCache hit 99%": +- button "15.7K tok · Cache hit 99%": - img - text: 15.7K tokCache hit 99% diff --git a/snapshots/web/seeded-history/command-row.expected.md b/snapshots/web/seeded-history/command-row.expected.md index 11e462169d..e3fc7ba75e 100644 --- a/snapshots/web/seeded-history/command-row.expected.md +++ b/snapshots/web/seeded-history/command-row.expected.md @@ -68,9 +68,9 @@ - text: DeepSeek-V4-Flash - img - button "Send message" [disabled] -- button "1 turns 2 steps{{throughput}} tok/s": +- button "1 turns 2 steps · {{throughput}} tok/s": - img - text: 1 turns 2 steps{{throughput}} tok/s -- button "16K tokCache hit 98%": +- button "16K tok · Cache hit 98%": - img - text: 16K tokCache hit 98% diff --git a/snapshots/web/seeded-history/feedback-row.expected.md b/snapshots/web/seeded-history/feedback-row.expected.md index 3550bf5dc2..150ae70174 100644 --- a/snapshots/web/seeded-history/feedback-row.expected.md +++ b/snapshots/web/seeded-history/feedback-row.expected.md @@ -72,9 +72,9 @@ - text: DeepSeek-V4-Flash - img - button "Send message" [disabled] -- button "1 turns 2 steps{{throughput}} tok/s": +- button "1 turns 2 steps · {{throughput}} tok/s": - img - text: 1 turns 2 steps{{throughput}} tok/s -- button "16K tokCache hit 98%": +- button "16K tok · Cache hit 98%": - img - text: 16K tokCache hit 98% diff --git a/snapshots/web/seeded-history/ui-expanded.expected.md b/snapshots/web/seeded-history/ui-expanded.expected.md index 95a6a763b0..607afdc319 100644 --- a/snapshots/web/seeded-history/ui-expanded.expected.md +++ b/snapshots/web/seeded-history/ui-expanded.expected.md @@ -66,9 +66,9 @@ - text: DeepSeek-V4-Flash - img - button "Send message" [disabled] -- button "1 turns 2 steps{{throughput}} tok/s": +- button "1 turns 2 steps · {{throughput}} tok/s": - img - text: 1 turns 2 steps{{throughput}} tok/s -- button "16K tokCache hit 98%": +- button "16K tok · Cache hit 98%": - img - text: 16K tokCache hit 98% diff --git a/snapshots/web/seeded-history/ui.expected.md b/snapshots/web/seeded-history/ui.expected.md index 31ae7ca03b..94f7e17f4e 100644 --- a/snapshots/web/seeded-history/ui.expected.md +++ b/snapshots/web/seeded-history/ui.expected.md @@ -48,9 +48,9 @@ - text: DeepSeek-V4-Flash - img - button "Send message" [disabled] -- button "1 turns 2 steps{{throughput}} tok/s": +- button "1 turns 2 steps · {{throughput}} tok/s": - img - text: 1 turns 2 steps{{throughput}} tok/s -- button "16K tokCache hit 98%": +- button "16K tok · Cache hit 98%": - img - text: 16K tokCache hit 98% diff --git a/snapshots/web/skill-tool-row/ui.expected.md b/snapshots/web/skill-tool-row/ui.expected.md index 17bdc399d7..e44aef8b5d 100644 --- a/snapshots/web/skill-tool-row/ui.expected.md +++ b/snapshots/web/skill-tool-row/ui.expected.md @@ -61,9 +61,9 @@ - text: DeepSeek-V4-Flash - img - button "Send message" [disabled] -- button "1 turns 2 steps{{throughput}} tok/s": +- button "1 turns 2 steps · {{throughput}} tok/s": - img - text: 1 turns 2 steps{{throughput}} tok/s -- button "310 tokCache hit 0%": +- button "310 tok · Cache hit 0%": - img - text: 310 tokCache hit 0% diff --git a/snapshots/web/steering/settled-expanded.expected.md b/snapshots/web/steering/settled-expanded.expected.md index 47b25cdcf1..d2b0b535b2 100644 --- a/snapshots/web/steering/settled-expanded.expected.md +++ b/snapshots/web/steering/settled-expanded.expected.md @@ -54,9 +54,9 @@ - img - button "0% of context used" - button "Send message" [disabled] -- button "1 turns 2 steps{{throughput}} tok/s": +- button "1 turns 2 steps · {{throughput}} tok/s": - img - text: 1 turns 2 steps{{throughput}} tok/s -- button "30 tokCache hit 0%": +- button "30 tok · Cache hit 0%": - img - text: 30 tokCache hit 0% diff --git a/snapshots/web/steering/settled.expected.md b/snapshots/web/steering/settled.expected.md index a24abb3594..b293d72bdb 100644 --- a/snapshots/web/steering/settled.expected.md +++ b/snapshots/web/steering/settled.expected.md @@ -46,9 +46,9 @@ - img - button "0% of context used" - button "Send message" [disabled] -- button "1 turns 2 steps{{throughput}} tok/s": +- button "1 turns 2 steps · {{throughput}} tok/s": - img - text: 1 turns 2 steps{{throughput}} tok/s -- button "30 tokCache hit 0%": +- button "30 tok · Cache hit 0%": - img - text: 30 tokCache hit 0% diff --git a/snapshots/web/subagent-conversation/ui-expanded.expected.md b/snapshots/web/subagent-conversation/ui-expanded.expected.md index 9aacbd6bcd..54857f0127 100644 --- a/snapshots/web/subagent-conversation/ui-expanded.expected.md +++ b/snapshots/web/subagent-conversation/ui-expanded.expected.md @@ -78,9 +78,9 @@ - 'button "Access mode, current: Custom"': Custom - button "6% of context used" - button "Send message" [disabled] -- button "2 turns 2 steps{{throughput}} tok/s": +- button "2 turns 2 steps · {{throughput}} tok/s": - img - text: 2 turns 2 steps{{throughput}} tok/s -- button "15.7K tokCache hit 99%": +- button "15.7K tok · Cache hit 99%": - img - text: 15.7K tokCache hit 99% diff --git a/snapshots/web/subagent-conversation/ui.expected.md b/snapshots/web/subagent-conversation/ui.expected.md index 1a57bb2bb3..fa9bbc75a6 100644 --- a/snapshots/web/subagent-conversation/ui.expected.md +++ b/snapshots/web/subagent-conversation/ui.expected.md @@ -66,9 +66,9 @@ - 'button "Access mode, current: Custom"': Custom - button "6% of context used" - button "Send message" [disabled] -- button "2 turns 2 steps{{throughput}} tok/s": +- button "2 turns 2 steps · {{throughput}} tok/s": - img - text: 2 turns 2 steps{{throughput}} tok/s -- button "15.7K tokCache hit 99%": +- button "15.7K tok · Cache hit 99%": - img - text: 15.7K tokCache hit 99% diff --git a/snapshots/web/turn-tail-actions/completed.expected.md b/snapshots/web/turn-tail-actions/completed.expected.md index 38fe11e9ed..3e7bb754d6 100644 --- a/snapshots/web/turn-tail-actions/completed.expected.md +++ b/snapshots/web/turn-tail-actions/completed.expected.md @@ -46,9 +46,9 @@ - img - button "6% of context used" - button "Send message" [disabled] -- button "1 turns 2 steps{{throughput}} tok/s": +- button "1 turns 2 steps · {{throughput}} tok/s": - img - text: 1 turns 2 steps{{throughput}} tok/s -- button "15.8K tokCache hit 50%": +- button "15.8K tok · Cache hit 50%": - img - text: 15.8K tokCache hit 50% diff --git a/snapshots/web/turn-tail-actions/focused.expected.md b/snapshots/web/turn-tail-actions/focused.expected.md index a1b9392d93..8891f44238 100644 --- a/snapshots/web/turn-tail-actions/focused.expected.md +++ b/snapshots/web/turn-tail-actions/focused.expected.md @@ -59,9 +59,9 @@ - img - button "6% of context used" - button "Send message" [disabled] -- button "1 turns 2 steps{{throughput}} tok/s": +- button "1 turns 2 steps · {{throughput}} tok/s": - img - text: 1 turns 2 steps{{throughput}} tok/s -- button "15.8K tokCache hit 50%": +- button "15.8K tok · Cache hit 50%": - img - text: 15.8K tokCache hit 50% diff --git a/snapshots/web/turn-tail-actions/running.expected.md b/snapshots/web/turn-tail-actions/running.expected.md index df7a83034b..71e6a98258 100644 --- a/snapshots/web/turn-tail-actions/running.expected.md +++ b/snapshots/web/turn-tail-actions/running.expected.md @@ -43,9 +43,9 @@ - img - button "6% of context used" - button "Stop generating" -- button "1 turns 1 steps{{throughput}} tok/s": +- button "1 turns 1 steps · {{throughput}} tok/s": - img - text: 1 turns 1 steps{{throughput}} tok/s -- button "7.9K tokCache hit 0%": +- button "7.9K tok · Cache hit 0%": - img - text: 7.9K tokCache hit 0% diff --git a/snapshots/web/turn-tail-actions/settled.expected.md b/snapshots/web/turn-tail-actions/settled.expected.md index 7fcc6d9108..fea858ced8 100644 --- a/snapshots/web/turn-tail-actions/settled.expected.md +++ b/snapshots/web/turn-tail-actions/settled.expected.md @@ -45,9 +45,9 @@ - img - button "6% of context used" - button "Send message" [disabled] -- button "1 turns 2 steps{{throughput}} tok/s": +- button "1 turns 2 steps · {{throughput}} tok/s": - img - text: 1 turns 2 steps{{throughput}} tok/s -- button "7.9K tokCache hit 0%": +- button "7.9K tok · Cache hit 0%": - img - text: 7.9K tokCache hit 0% diff --git a/snapshots/web/turn-tail-actions/usage-expanded.expected.md b/snapshots/web/turn-tail-actions/usage-expanded.expected.md index dd0e4b20e0..b7874f156d 100644 --- a/snapshots/web/turn-tail-actions/usage-expanded.expected.md +++ b/snapshots/web/turn-tail-actions/usage-expanded.expected.md @@ -46,9 +46,9 @@ - img - button "6% of context used" - button "Send message" [disabled] -- button "1 turns 2 steps{{throughput}} tok/s": +- button "1 turns 2 steps · {{throughput}} tok/s": - img - text: 1 turns 2 steps{{throughput}} tok/s -- button "15.8K tokCache hit 50%": +- button "15.8K tok · Cache hit 50%": - img - text: 15.8K tokCache hit 50% diff --git a/snapshots/web/web-search-round/ui.expected.md b/snapshots/web/web-search-round/ui.expected.md index a08c9826c1..bf0d5a5930 100644 --- a/snapshots/web/web-search-round/ui.expected.md +++ b/snapshots/web/web-search-round/ui.expected.md @@ -51,9 +51,9 @@ - img - button "0% of context used" - button "Send message" [disabled] -- button "1 turns 2 steps{{throughput}} tok/s": +- button "1 turns 2 steps · {{throughput}} tok/s": - img - text: 1 turns 2 steps{{throughput}} tok/s -- button "29 tokCache hit 0%": +- button "29 tok · Cache hit 0%": - img - text: 29 tokCache hit 0% From bbd899d47824f2a94e46ec1691c2f6fa71bb0130 Mon Sep 17 00:00:00 2001 From: Yif <877193178@qq.com> Date: Tue, 8 Sep 2026 01:16:56 +0800 Subject: [PATCH 09/10] fix(client): count IconGaugeOutline16 in the icon-set pin; keep the hero headline text addressable MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The icon-set spec still pinned 74 exports after IconGaugeOutline16 landed (a hand-drawn product glyph — the set is now eight of those). Moving the preview badge inside the hero title group had merged the headline into one text node with the badge, which broke exact text lookup (hmr-live e2e); the headline gets its own span again. --- .../client/ui-conversation/src/client/skeleton/EmptyHero.tsx | 3 ++- packages/client/ui-primitives/tests/icons.client.spec.tsx | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/packages/client/ui-conversation/src/client/skeleton/EmptyHero.tsx b/packages/client/ui-conversation/src/client/skeleton/EmptyHero.tsx index e0a7bcb924..9ac7ae573b 100644 --- a/packages/client/ui-conversation/src/client/skeleton/EmptyHero.tsx +++ b/packages/client/ui-conversation/src/client/skeleton/EmptyHero.tsx @@ -150,7 +150,8 @@ export function HeroShell({ t, renderSlot, children }: HeroShellProps) { })} - {t('hero.headline')} + {/* Own element: keeps the headline text addressable apart from the badge. */} + {t('hero.headline')} {t('hero.preview')}
diff --git a/packages/client/ui-primitives/tests/icons.client.spec.tsx b/packages/client/ui-primitives/tests/icons.client.spec.tsx index e29bb2ce14..1b9a9c7773 100644 --- a/packages/client/ui-primitives/tests/icons.client.spec.tsx +++ b/packages/client/ui-primitives/tests/icons.client.spec.tsx @@ -17,8 +17,8 @@ const icons = Object.fromEntries( const iconNames = Object.keys(icons) describe('ic_ds_ icon set', () => { - it('exports the full icon set (46 deepsuite + 21 figma extracts + seven product glyphs outside those sets)', () => { - expect(iconNames.length).toBe(74) + it('exports the full icon set (46 deepsuite + 21 figma extracts + eight product glyphs outside those sets)', () => { + expect(iconNames.length).toBe(75) }) it.each(iconNames)('%s renders an svg with currentColor fills and no hardcoded palette', (name) => { From 0ffbd658431dbda5b042a708aec6bd5ea64bffb4 Mon Sep 17 00:00:00 2001 From: Yif <877193178@qq.com> Date: Tue, 8 Sep 2026 01:42:35 +0800 Subject: [PATCH 10/10] test(web): follow hero headline nesting and Tool definitions copy in e2e --- apps/web/tests/built-boot.expected.e2e.ts | 2 +- apps/web/tests/startup-auto-selection.e2e.ts | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/apps/web/tests/built-boot.expected.e2e.ts b/apps/web/tests/built-boot.expected.e2e.ts index 275cda21d5..d7292382df 100644 --- a/apps/web/tests/built-boot.expected.e2e.ts +++ b/apps/web/tests/built-boot.expected.e2e.ts @@ -112,7 +112,7 @@ it('boots the built plugin graph and renders a fixture session end to end', asyn fireEvent.click(contextTrigger) const contextPanel = await screen.findByRole('dialog', { name: 'of context used' }) within(contextPanel).getByText('System prompt') - within(contextPanel).getByText('Tools') + within(contextPanel).getByText('Tool definitions') within(contextPanel).getByText('Messages') // The write/edit turns render a real diff card through the assembled graph diff --git a/apps/web/tests/startup-auto-selection.e2e.ts b/apps/web/tests/startup-auto-selection.e2e.ts index 882b99b420..613eb4a7d0 100644 --- a/apps/web/tests/startup-auto-selection.e2e.ts +++ b/apps/web/tests/startup-auto-selection.e2e.ts @@ -42,7 +42,9 @@ describe('web e2e: startup auto-selection', () => { onTestFailed(() => saveFailureShot(page, 'web-e2e-first-workspace-stable-tree')) await page.locator(`${ROOT_PHASE}[data-phase="hero"]`).waitFor({ timeout: 15_000 }) const headline = page.getByText('Into the Unknown', { exact: true }) - const fishHitbox = headline.locator('xpath=preceding-sibling::span[1]') + // The headline text sits in its own span inside the title group; the fish + // hitbox precedes the group, not the text span. + const fishHitbox = headline.locator('xpath=../preceding-sibling::span[1]') const fish = fishHitbox.locator('svg') expect(await fish.evaluate(node => getComputedStyle(node).color)) .toBe(await headline.evaluate(node => getComputedStyle(node).color))