From b9c85a9187372a1f79307d91c4859825c395e2f9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BC=98=E6=89=AC?= Date: Tue, 27 Oct 2026 19:34:33 +0800 Subject: [PATCH 01/12] feat(webui): wire TurnNavigator tick hover preview and jump in SessionTranscript SessionTranscript passed only turns to TurnNavigator; the component's hover and activate callbacks were no-ops at the integration. The transcript now owns the hover state and projects a preview card (turn number, timestamp, first answer line, neighbouring user queries) through the component's existing preview slot, and a tick activation jumps to the turn's message root (data-message-id anchor, centered scrollIntoView) after releasing the bottom-follow, flashing the destination with a reduced-motion-aware highlight. The pure halves are exported and covered by the node suite; the mouse-event wiring itself remains the SSR-unreachable boundary. --- .../transcript/ui/SessionTranscript.tsx | 137 +++++++++++++++- .../features/transcript/ui/transcript.css | 19 +++ .../webui/test/unit/turn-navigator.test.tsx | 155 +++++++++++++++++- ...bui-transcript-widgets-integration.test.ts | 23 +++ 4 files changed, 330 insertions(+), 4 deletions(-) diff --git a/packages/webui/src/client/features/transcript/ui/SessionTranscript.tsx b/packages/webui/src/client/features/transcript/ui/SessionTranscript.tsx index 7926a4d46..a8da22420 100644 --- a/packages/webui/src/client/features/transcript/ui/SessionTranscript.tsx +++ b/packages/webui/src/client/features/transcript/ui/SessionTranscript.tsx @@ -2,11 +2,11 @@ // flattening, history paging, streaming loader, and the embedded // WebuiQuestionnaireResponse renderer for historical questionnaire answers. -import { Fragment, useEffect, useLayoutEffect, useMemo, useRef, type ReactElement } from "react"; +import { Fragment, useEffect, useLayoutEffect, useMemo, useRef, useState, type ReactElement } from "react"; import { IconChevronUp } from "@tabler/icons-react"; import { ChatSkeleton } from "./TranscriptSkeletons.js"; import { ActivityIndicator, MessageAfterQueryStreamingPlaceholder, MessagePassiveLoadingPlaceholder } from "./ActivityIndicator.js"; -import { TurnNavigator, type TurnSummary } from "./TurnNavigator.js"; +import { TurnNavigator, type TurnNavigatorPreview, type TurnSummary } from "./TurnNavigator.js"; import { MessageItem } from "./MessageItem.js"; import { formatWebuiMessageTimestamp } from "../projection/message-timestamp.js"; import type { WebuiMessageActionCapabilities } from "../contracts/transcript-view.js"; @@ -29,6 +29,7 @@ import { projectWebuiTranscriptMessages, projectWebuiQueryDurations, projectWebuiProcessSegments, + type WebuiTranscriptGroup, } from "../projection/transcript-projection.js"; import { projectWebuiTranscriptMessage } from "../projection/message-projection.js"; import { projectWebuiTranscriptView } from "../projection/transcript-view-model.js"; @@ -45,6 +46,107 @@ import { const EMPTY_TRANSCRIPT_PAGE: WebuiClientMessagePage = {}; +// --- TurnNavigator integration ------------------------------------------------ +// +// `TurnNavigator` is the presentational seam and defers hover, scroll and jump +// to this integrator. The helpers below are that integration's pure halves, +// exported so the node-environment suite can assert them without a DOM: which +// message a tick anchors to, what one turn's hover preview shows, and what a +// reveal does to its target. The mouse wiring itself (setState on hover, +// querySelector on activate) stays in the component and is the boundary SSR +// cannot reach. + +/** Every rendered message root carries `data-message-id` (MessageItem), so a + * tick reuses that attribute as its anchor instead of a second anchor + * system. Quotes and backslashes are escaped so a message id cannot close + * the attribute selector. */ +export function turnNavigatorAnchorSelector(turnId: string): string { + return `[data-message-id="${turnId.replace(/["\\]/g, "\\$&")}"]`; +} + +export const TURN_NAVIGATOR_FLASH_CLASS = "webui-turn-jump-flash"; +const TURN_NAVIGATOR_FLASH_MS = 1200; + +export interface WebuiTurnRevealTarget { + scrollIntoView(options?: ScrollIntoViewOptions): void; + readonly classList: { add(token: string): void; remove(token: string): void }; +} + +/** Reveal one turn after a tick activation: center it in the transcript + * viewport and flash it so the destination is findable. The class is removed + * after `flashMs` so the highlight stays transient. */ +export function revealTurnNavigatorAnchor( + target: WebuiTurnRevealTarget, + flashMs = TURN_NAVIGATOR_FLASH_MS, +): void { + target.scrollIntoView({ block: "center", behavior: "smooth" }); + target.classList.add(TURN_NAVIGATOR_FLASH_CLASS); + setTimeout(() => target.classList.remove(TURN_NAVIGATOR_FLASH_CLASS), flashMs); +} + +const TURN_NAVIGATOR_RESPONSE_MAX_CHARS = 120; + +/** The preview's one-line answer summary: the first non-empty line, capped. */ +function firstTurnSummaryLine(text: string): string { + const line = text.split("\n").find((candidate) => candidate.trim().length > 0) ?? ""; + return line.length > TURN_NAVIGATOR_RESPONSE_MAX_CHARS + ? `${line.slice(0, TURN_NAVIGATOR_RESPONSE_MAX_CHARS - 1)}…` + : line; +} + +function hasAssistantItems(group: WebuiTranscriptGroup): boolean { + return group.items.some((item) => item.kind === "assistant"); +} + +function turnUserQuery( + groups: readonly WebuiTranscriptGroup[], + fromIndex: number, + step: 1 | -1, +): string | undefined { + for (let index = fromIndex; index >= 0 && index < groups.length; index += step) { + const userItem = groups[index]?.items.find( + (item): item is Extract => item.kind === "user", + ); + if (userItem) return userItem.text; + } + return undefined; +} + +/** The hover preview for one tick: the turn's 1-based number among the + * assistant blocks the strip lists, its first answer line as the summary, the + * group's own timestamp, framed by the user queries on both sides. Returns + * null when the id is not one of the assistant turns the strip lists — the + * numbering and membership mirror `projectTurnRows`. */ +export function projectTurnNavigatorPreview(input: { + readonly turnId: string; + readonly groups: readonly WebuiTranscriptGroup[]; +}): TurnNavigatorPreview | null { + const assistantGroups = input.groups.filter(hasAssistantItems); + const tickIndex = assistantGroups.findIndex((group) => group.messageId === input.turnId); + if (tickIndex < 0) return null; + const group = assistantGroups[tickIndex]!; + const groupIndex = input.groups.indexOf(group); + const answer = group.items.find( + (item): item is Extract => + item.kind === "assistant" && item.text.trim().length > 0, + ); + const time = formatWebuiMessageTimestamp( + group.items.find((item) => typeof item.timestamp === "number")?.timestamp, + ); + const previousQuery = groupIndex > 0 ? turnUserQuery(input.groups, groupIndex - 1, -1) : undefined; + const nextQuery = + groupIndex >= 0 && groupIndex < input.groups.length - 1 + ? turnUserQuery(input.groups, groupIndex + 1, 1) + : undefined; + return { + title: `第 ${tickIndex + 1} 轮`, + ...(time ? { time } : {}), + ...(previousQuery ? { previousQuery } : {}), + ...(nextQuery ? { nextQuery } : {}), + ...(answer ? { response: firstTurnSummaryLine(answer.text) } : {}), + }; +} + export function WebuiSessionTranscript({ sessionId, initialMessages, @@ -158,6 +260,27 @@ export function WebuiSessionTranscript({ turnLive, contentKey: items, }); + // The navigator's tick interactions: hover decides which turn the preview + // card describes, activation jumps the viewport to that turn's message root. + // `TurnNavigator` renders; this integrator owns the state and the DOM. + const [hoveredTurnId, setHoveredTurnId] = useState(); + const navigatorPreview = useMemo( + () => + hoveredTurnId + ? projectTurnNavigatorPreview({ turnId: hoveredTurnId, groups }) + : null, + [groups, hoveredTurnId], + ); + const activateNavigatorTick = (turnId: string) => { + const anchor = transcriptRef.current?.querySelector( + turnNavigatorAnchorSelector(turnId), + ); + if (!anchor) return; + // 跳转是读者主动离开贴底跟随:先交还控制权,否则后续内容增长会把 + // 视口从落点拽回底部(loadOlder 出于同一原因先释放)。 + releaseAutoFollow(); + revealTurnNavigatorAnchor(anchor); + }; const loadOlder = visiblePage.hasMore && visiblePage.nextCursor ? () => { if (loading) return; @@ -433,7 +556,15 @@ export function WebuiSessionTranscript({ ) : null} - + setHoveredTurnId(turnId)} + onTickLeave={(turnId) => + setHoveredTurnId((current) => (current === turnId ? undefined : current)) + } + onTickActivate={(turnId) => activateNavigatorTick(turnId)} + /> ); } diff --git a/packages/webui/src/client/features/transcript/ui/transcript.css b/packages/webui/src/client/features/transcript/ui/transcript.css index 18382395e..79557e4a5 100644 --- a/packages/webui/src/client/features/transcript/ui/transcript.css +++ b/packages/webui/src/client/features/transcript/ui/transcript.css @@ -125,6 +125,25 @@ .message-animate-in { animation: none; } } + /* Turn-navigator jump highlight: the message a tick jumped to flashes once + * so the destination is findable. The surface token is the accent + * focus-highlight the composer's selection already uses, so both themes + * rebind it through tokens.css and nothing here is theme-specific. */ + .webui-turn-jump-flash { + border-radius: var(--radius_12); + animation: webui-turn-jump-flash 1.2s ease-out both; + } + @keyframes webui-turn-jump-flash { + 0% { background-color: var(--bg_interaction_accent_focus_highlight); } + 100% { background-color: transparent; } + } + @media (prefers-reduced-motion: reduce) { + .webui-turn-jump-flash { + animation: none; + background-color: var(--bg_interaction_accent_focus_highlight); + } + } + /* A session is a two-zone surface: one message viewport owns the scroll * position, while the composer stays in the bottom zone. The DOM keeps the * composer close to the home surface markup, so the explicit order here is diff --git a/packages/webui/test/unit/turn-navigator.test.tsx b/packages/webui/test/unit/turn-navigator.test.tsx index 84a2d5dd6..df36ae8b7 100644 --- a/packages/webui/test/unit/turn-navigator.test.tsx +++ b/packages/webui/test/unit/turn-navigator.test.tsx @@ -4,12 +4,26 @@ // `data-turn-id` / `data-state` attributes the component emits, and the // `data-mode` / `data-hidden` flags the parent uses to drive the strip // visibility. The brief lists 17 testids; this file covers them all. +// +// The second half covers the integrator helpers `SessionTranscript` exports: +// the hover-preview projection, the tick→message anchor selector, and the +// reveal effect. The projection is fed groups built by the REAL message +// projection, so the shapes are the ones the transcript renders, not hand +// copies that could drift. -import { describe, it, expect } from "vitest"; +import { describe, it, expect, vi } from "vitest"; import { createElement } from "react"; import { renderToStaticMarkup } from "react-dom/server"; import { TurnNavigator } from "../../src/client/features/transcript/ui/TurnNavigator.js"; +import { + projectTurnNavigatorPreview, + revealTurnNavigatorAnchor, + turnNavigatorAnchorSelector, + TURN_NAVIGATOR_FLASH_CLASS, +} from "../../src/client/features/transcript/ui/SessionTranscript.js"; +import { projectWebuiMessage } from "../../src/client/features/transcript/projection/message-projection.js"; +import { groupWebuiTranscriptItems } from "../../src/client/features/transcript/projection/transcript-projection.js"; const baseTurns = [ { id: "turn-1", state: "default" as const }, @@ -18,6 +32,36 @@ const baseTurns = [ { id: "turn-4", state: "error" as const }, ]; +/** Two full turns through the real projection: user question, then the + * assistant block (thinking + answer) the strip lists a tick for. */ +const navigatorGroups = groupWebuiTranscriptItems([ + ...projectWebuiMessage({ + msgId: "nav-user-1", + role: "user", + msgContent: "第一问", + timestamp: new Date(2026, 9, 26, 12, 30).getTime(), + }), + ...projectWebuiMessage({ + msgId: "nav-assistant-1", + role: "assistant", + thinkingContent: "先推理", + msgContent: "第一答", + timestamp: new Date(2026, 9, 26, 12, 34).getTime(), + }), + ...projectWebuiMessage({ + msgId: "nav-user-2", + role: "user", + msgContent: "第二问", + timestamp: new Date(2026, 9, 26, 12, 40).getTime(), + }), + ...projectWebuiMessage({ + msgId: "nav-assistant-2", + role: "assistant", + msgContent: " \n第二答第一行\n后面几行不进摘要", + timestamp: new Date(2026, 9, 26, 12, 44).getTime(), + }), +]); + describe("TurnNavigator", () => { it("emits the root, strip, and tick testids", () => { const html = renderToStaticMarkup( @@ -96,3 +140,112 @@ describe("TurnNavigator", () => { expect(html).toContain('data-hidden="true"'); }); }); + +describe("TurnNavigator integration helpers (SessionTranscript)", () => { + it("builds the hover preview from the real groups: index, time, both neighbouring queries, first answer line", () => { + const preview = projectTurnNavigatorPreview({ + turnId: "nav-assistant-1", + groups: navigatorGroups, + }); + // The timestamp is built from a local-time Date, so the formatted value is + // stable on every timezone the suite runs on. + expect(preview).toEqual({ + title: "第 1 轮", + time: "10月26日, 12:34", + previousQuery: "第一问", + nextQuery: "第二问", + response: "第一答", + }); + }); + + it("numbers the second assistant turn like the strip does and omits a missing next query", () => { + const preview = projectTurnNavigatorPreview({ + turnId: "nav-assistant-2", + groups: navigatorGroups, + }); + expect(preview?.title).toBe("第 2 轮"); + expect(preview?.previousQuery).toBe("第二问"); + expect(preview?.nextQuery).toBeUndefined(); + // Leading blank lines are skipped: the summary is the first non-empty line. + expect(preview?.response).toBe("第二答第一行"); + }); + + it("returns null for ids the strip lists no tick for", () => { + // A user block is not a tick, and an unknown id is not a turn at all. + expect( + projectTurnNavigatorPreview({ turnId: "nav-user-1", groups: navigatorGroups }), + ).toBeNull(); + expect( + projectTurnNavigatorPreview({ turnId: "missing", groups: navigatorGroups }), + ).toBeNull(); + }); + + it("caps a long first answer line for the preview card", () => { + const groups = groupWebuiTranscriptItems([ + ...projectWebuiMessage({ msgId: "u", role: "user", msgContent: "问" }), + ...projectWebuiMessage({ + msgId: "a", + role: "assistant", + msgContent: "长".repeat(300), + }), + ]); + const response = projectTurnNavigatorPreview({ turnId: "a", groups })?.response; + expect(response).toBeDefined(); + expect(response!.length).toBe(120); + expect(response!.endsWith("…")).toBe(true); + }); + + it("feeds a projection the component's preview slot renders as-is", () => { + const html = renderToStaticMarkup( + createElement(TurnNavigator, { + turns: baseTurns, + preview: projectTurnNavigatorPreview({ + turnId: "nav-assistant-1", + groups: navigatorGroups, + }), + }), + ); + expect(html).toContain("第 1 轮"); + expect(html).toContain("10月26日, 12:34"); + expect(html).toContain("第一问"); + expect(html).toContain("第二问"); + expect(html).toContain("第一答"); + }); + + it("anchors a tick to its message root's data-message-id, with quotes escaped", () => { + expect(turnNavigatorAnchorSelector("nav-assistant-1")).toBe( + '[data-message-id="nav-assistant-1"]', + ); + // A message id carrying a quote must not close the attribute selector. + expect(turnNavigatorAnchorSelector('msg"1\\2')).toBe( + '[data-message-id="msg\\"1\\\\2"]', + ); + }); + + it("centers the jumped-to turn and flashes it transiently", () => { + vi.useFakeTimers(); + try { + const scrolls: unknown[] = []; + const classLog: string[] = []; + const target = { + scrollIntoView: (options?: ScrollIntoViewOptions) => scrolls.push(options), + classList: { + add: (token: string) => classLog.push(`+${token}`), + remove: (token: string) => classLog.push(`-${token}`), + }, + }; + revealTurnNavigatorAnchor(target, 1200); + expect(scrolls).toEqual([{ block: "center", behavior: "smooth" }]); + expect(classLog).toEqual([`+${TURN_NAVIGATOR_FLASH_CLASS}`]); + vi.advanceTimersByTime(1199); + expect(classLog).toEqual([`+${TURN_NAVIGATOR_FLASH_CLASS}`]); + vi.advanceTimersByTime(1); + expect(classLog).toEqual([ + `+${TURN_NAVIGATOR_FLASH_CLASS}`, + `-${TURN_NAVIGATOR_FLASH_CLASS}`, + ]); + } finally { + vi.useRealTimers(); + } + }); +}); diff --git a/packages/webui/test/unit/webui-transcript-widgets-integration.test.ts b/packages/webui/test/unit/webui-transcript-widgets-integration.test.ts index 19df65f2c..49fad748b 100644 --- a/packages/webui/test/unit/webui-transcript-widgets-integration.test.ts +++ b/packages/webui/test/unit/webui-transcript-widgets-integration.test.ts @@ -167,6 +167,29 @@ describe("WebUI transcript widget wiring", () => { expect(html).toContain('data-testid="message-turn-navigator"'); }); + it("anchors every navigator tick to a rendered message root", () => { + // The tick→jump wiring's static half: each tick the strip lists must have + // a message root carrying the same `data-message-id`, which is what the + // activation handler queries for before scrolling. A tick without an + // anchor would be a dead button. + const html = sessionShell({ + messages: [ + { msgId: "nav-user-1", role: "user", msgContent: "第一问", timestamp: 1 }, + { msgId: "nav-assistant-1", role: "assistant", msgContent: "第一答", timestamp: 2 }, + { msgId: "nav-user-2", role: "user", msgContent: "第二问", timestamp: 3 }, + { msgId: "nav-assistant-2", role: "assistant", msgContent: "第二答", timestamp: 4 }, + ], + }); + const tickIds = [ + ...html.matchAll(/data-testid="message-turn-navigator-tick"[^>]*data-turn-id="([^"]+)"/gu), + ].map((match) => match[1]); + // Only assistant blocks are ticks, in transcript order. + expect(tickIds).toEqual(["nav-assistant-1", "nav-assistant-2"]); + for (const id of tickIds) { + expect(html).toContain(`data-message-id="${id}"`); + } + }); + it("replaces the bare alert with OutputError when the stream refuses", () => { const html = sessionShell({ streamingPhase: "refused", From 6ac45b36cf869e182bc5726ac78b47691d85d86a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BC=98=E6=89=AC?= Date: Tue, 27 Oct 2026 19:47:56 +0800 Subject: [PATCH 02/12] feat(webui): register help/new/status/usage/model in the slash palette All six WEBUI_RUN_COMMAND_NAMES were reachable through runCommand but only compact had a palette row, so the other five were undiscoverable. Add rows for help/new/status/usage/model with names aligned to the contract list (in /help order, after the composer modes), Chinese descriptions grounded in runtime/commands/runner.ts behaviour, and supported: true. Reuse the existing WebuiIconCommandUsage, WebuiIconNewTask and WebuiIconModel glyphs and add WebuiIconCommandHelp/Status wrappers for the two rows without one. The palette tests now pin the full run-command roster instead of the compact-only snapshot. --- .../features/composer/ui/slash-palette.ts | 74 ++++++++++++++++--- packages/webui/src/client/shell/icons.tsx | 4 +- .../webui/test/unit/slash-palette.test.ts | 51 ++++++++----- 3 files changed, 99 insertions(+), 30 deletions(-) diff --git a/packages/webui/src/client/features/composer/ui/slash-palette.ts b/packages/webui/src/client/features/composer/ui/slash-palette.ts index ad5449e75..245f515b3 100644 --- a/packages/webui/src/client/features/composer/ui/slash-palette.ts +++ b/packages/webui/src/client/features/composer/ui/slash-palette.ts @@ -2,8 +2,8 @@ // // Uses three data pools (built-in commands, the plugin // registry, and the skills resolver), sectioning, and four-rank filtering. -// The built-in list is intentionally scoped to the composer modes supported -// here; behavior fields keep the same record shape. +// The built-in list covers the composer modes plus every name the harness +// port's `runCommand` accepts; behavior fields keep the same record shape. // // See `packages/webui/docs/design-language.md` for the visual language and // ADR 0009 for the shared vocabulary. @@ -19,7 +19,12 @@ import { import { WebuiIconCommandCompact, WebuiIconCommandGoal, + WebuiIconCommandHelp, WebuiIconCommandPlan, + WebuiIconCommandStatus, + WebuiIconCommandUsage, + WebuiIconModel, + WebuiIconNewTask, WebuiIconSites, WebuiIconSkillAskMatt, WebuiIconSkillCodebaseDesign, @@ -83,17 +88,17 @@ export interface SlashCommandEntry extends WebuiSlashPaletteFields { * * Capability gating today is the static `supported` flag. Goal is backed by * the goal operations, plan entry is backed by the send-message - * `plan-entry` intent, and compact is backed by the harness port's - * `runCommand` — `runtime/commands/runner.ts` routes `command === "compact"` - * into `requestCompaction`, and the operation is registered in - * `server/operation/operations.ts`. Commands without a WebUI path remain - * inert until their transport is wired. + * `plan-entry` intent, and every name in `WEBUI_RUN_COMMAND_NAMES` is backed + * by the harness port's `runCommand` — `runtime/commands/runner.ts` routes + * `help` to the descriptor list, `new` to `createSession`, `compact` to + * `requestCompaction`, `status` to `getAccountStatus`/`getSession`, `usage` + * to `getSessionUsage`, and `model` to `listModels`/`selectModel`; the + * operation is registered in `server/operation/operations.ts`. Commands + * without a WebUI path remain inert until their transport is wired. * - * `compact` is the one run-command built-in exposed so far. The remaining - * names in `WEBUI_RUN_COMMAND_NAMES` (`help`, `new`, `status`, `usage`, - * `model`) are reachable at the transport layer but have no palette row, so - * a user cannot discover them; they need their own entries before they can - * be claimed as shipped. + * The run-command rows sit after the composer modes and keep the `/help` + * order of `WEBUI_RUN_COMMAND_NAMES`, so the palette and `/help` list the + * same vocabulary in the same order. */ export const WEBUI_BUILTIN_COMMANDS: readonly SlashCommandEntry[] = [ { @@ -116,6 +121,24 @@ export const WEBUI_BUILTIN_COMMANDS: readonly SlashCommandEntry[] = [ icon: WebuiIconCommandPlan, supported: true, }, + { + name: "help", + displayName: "help", + label: "帮助", + description: "查看可用斜杠命令列表", + source_type: -1, + icon: WebuiIconCommandHelp, + supported: true, + }, + { + name: "new", + displayName: "new", + label: "新会话", + description: "在当前工作区新建一个会话", + source_type: -1, + icon: WebuiIconNewTask, + supported: true, + }, { name: "compact", displayName: "compact", @@ -125,6 +148,33 @@ export const WEBUI_BUILTIN_COMMANDS: readonly SlashCommandEntry[] = [ icon: WebuiIconCommandCompact, supported: true, }, + { + name: "status", + displayName: "status", + label: "状态", + description: "查看当前会话的账号、模型与工作区状态", + source_type: -1, + icon: WebuiIconCommandStatus, + supported: true, + }, + { + name: "usage", + displayName: "usage", + label: "用量", + description: "查看当前会话的用量统计", + source_type: -1, + icon: WebuiIconCommandUsage, + supported: true, + }, + { + name: "model", + displayName: "model", + label: "模型", + description: "查看模型列表;可附带名称切换模型,如 /model provider/model", + source_type: -1, + icon: WebuiIconModel, + supported: true, + }, ]; /** diff --git a/packages/webui/src/client/shell/icons.tsx b/packages/webui/src/client/shell/icons.tsx index cf2338de9..6341e2118 100644 --- a/packages/webui/src/client/shell/icons.tsx +++ b/packages/webui/src/client/shell/icons.tsx @@ -6,7 +6,7 @@ import { IconCalendarEvent, IconCaretRightFilled, IconChartBar, IconCheck, IconChevronDown, IconChevronLeft, IconChevronRight, IconClipboard, IconCode, IconCopy, IconDeviceMobile, IconDots, IconDownload, IconEdit, IconFile, IconFileCode, IconFolder, IconFolderPlus, - IconGitBranch, IconGitCompare, IconGitFork, IconHierarchy, IconHome, IconLayoutDashboard, IconMaximize, + IconGitBranch, IconGitCompare, IconGitFork, IconHierarchy, IconHome, IconInfoCircle, IconLayoutDashboard, IconMaximize, IconLayoutSidebar, IconMapPin, IconMessage, IconMessageCircle, IconMessageQuestion, IconMinimize, IconPaperclip, IconPin, IconPinFilled, IconPuzzle, IconRefresh, IconSearch, IconShieldCheck, IconShieldLock, IconShieldX, IconSparkles, @@ -96,6 +96,8 @@ export function WebuiIconWorkspaceTerminal({ className }: WebuiIconProps): React export function WebuiIconWorkspaceCanvas({ className }: WebuiIconProps): ReactElement { return glyph(IconHierarchy, className); } export function WebuiIconWorkspaceExpand({ className, expanded = false }: WebuiIconProps & { readonly expanded?: boolean }): ReactElement { return glyph(expanded ? IconMinimize : IconMaximize, className); } export function WebuiIconCommandCompact({ className }: WebuiIconProps): ReactElement { return glyph(IconMinimize, className); } +export function WebuiIconCommandHelp({ className }: WebuiIconProps): ReactElement { return glyph(IconMessageQuestion, className); } +export function WebuiIconCommandStatus({ className }: WebuiIconProps): ReactElement { return glyph(IconInfoCircle, className); } export function WebuiIconCommandUsage({ className }: WebuiIconProps): ReactElement { return glyph(IconChartBar, className); } export function WebuiIconCommandGoal({ className }: WebuiIconProps): ReactElement { return glyph(IconTarget, className); } export function WebuiIconCommandPlan({ className }: WebuiIconProps): ReactElement { return glyph(IconCalendarEvent, className); } diff --git a/packages/webui/test/unit/slash-palette.test.ts b/packages/webui/test/unit/slash-palette.test.ts index a174a120e..1b8117b8d 100644 --- a/packages/webui/test/unit/slash-palette.test.ts +++ b/packages/webui/test/unit/slash-palette.test.ts @@ -171,30 +171,47 @@ describe("WebUI slash palette — runtime narrowing", () => { // These rows carry no composerMode, so the lite filter drops them — but // they ARE runnable: `classifyWebuiSlashCommand` returns "runnable" and // `resolveWebuiSubmissionIntent` path 3 dispatches them to the host's - // `runCommand`. An earlier version of this test asserted `[]` with the - // comment "those commands are hidden from this palette" — that pinned the - // missing `compact` entry in place and let the palette stay green while - // `/compact` was unreachable from the UI. - expect(names).toEqual(["compact"]); + // `runCommand`. Every name in WEBUI_RUN_COMMAND_NAMES must carry a row + // here: a missing row is a command the transport accepts but the user + // cannot discover or invoke. An earlier version asserted `["compact"]` + // while the other five names had no row — that pinned the gap; this + // pins the closure of the list. + expect(names).toEqual([...WEBUI_RUN_COMMAND_NAMES].sort()); }); - it("exposes the run-command the server transport actually routes", () => { - // The cross-module contract, stated so that deleting the entry fails it. + it("exposes every run-command the server transport actually routes", () => { + // The cross-module contract, stated so that deleting any entry fails it. // - // `runtime/commands/runner.ts` routes `command === "compact"` into - // `port.requestCompaction`, and the operation is registered in - // `server/operation/operations.ts`. A palette that omits the row leaves - // `resolveWebuiSubmissionIntent` unable to find the name, so the - // submit-turn path swallows `/compact` and sends it as a plain chat - // message. The other names in WEBUI_RUN_COMMAND_NAMES (`help`, `new`, - // `status`, `usage`, `model`) have no palette row yet on purpose — they - // need their own scope, so this test pins the one command that is wired - // end to end rather than asserting the whole whitelist. + // `runtime/commands/runner.ts` routes each name in + // WEBUI_RUN_COMMAND_NAMES to a port capability — `help` to the + // descriptor list, `new` to `createSession`, `compact` to + // `requestCompaction`, `status` to `getAccountStatus`/`getSession`, + // `usage` to `getSessionUsage`, `model` to `listModels`/`selectModel` — + // and the operation is registered in `server/operation/operations.ts`. + // A palette that omits a row leaves `resolveWebuiSubmissionIntent` + // unable to find the name, so the submit-turn path swallows the slash + // and sends it as a plain chat message. const palette = buildWebuiSlashPalette({ skills: Object.values(WEBUI_PLUGIN_REGISTRY), }); const runnableNames = palette.filter(isWebuiRunnableCommand).map((entry) => entry.name); - expect(runnableNames).toContain("compact"); + for (const name of WEBUI_RUN_COMMAND_NAMES) { + expect(runnableNames, `palette row for /${name}`).toContain(name); + } + }); + + it("registers every run-command with a non-empty label and description", () => { + // Discoverability is the row itself: name aligned with the contract + // list, a label the popover shows, a description that states what the + // command does, and the capability flag on. The descriptions follow the + // runtime's actual behaviour in `runtime/commands/runner.ts`. + for (const name of WEBUI_RUN_COMMAND_NAMES) { + const row = WEBUI_BUILTIN_COMMANDS.find((entry) => entry.name === name); + expect(row, `built-in row for /${name}`).toBeDefined(); + expect(row?.label.trim(), `label for /${name}`).not.toBe(""); + expect(row?.description.trim(), `description for /${name}`).not.toBe(""); + expect(row?.supported, `supported for /${name}`).toBe(true); + } }); it("does not leave a run-command row marked unsupported", () => { From 3bc5ecf52e7d91067982f57f5a7fb70169e1f25d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BC=98=E6=89=AC?= Date: Tue, 27 Oct 2026 20:00:48 +0800 Subject: [PATCH 03/12] feat(webui): mark the higher-usage context window on the model panel MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The catalog states which context window bills as the heavier choice (contextWindowOptionHints, e.g. { "1000000": "higher_usage" }), and the server projection passes it through, but the panel rendered nothing from it: a 1M window read exactly like the 200K one beside it. Render the hint as a quiet suffix on the option it annotates —『较高用量』 in the tertiary text role, so the size stays what the eye sorts by and the hint stays quieter in both themes. A model whose catalog carries no hints, or a value the wire contract does not name, renders the size alone. --- .../client/features/models/ui/ModelPanel.tsx | 12 +++ .../src/client/features/models/ui/models.css | 9 ++ .../unit/model-panel-context-hints.test.tsx | 94 +++++++++++++++++++ test/vitest-suites.json | 1 + 4 files changed, 116 insertions(+) create mode 100644 packages/webui/test/unit/model-panel-context-hints.test.tsx diff --git a/packages/webui/src/client/features/models/ui/ModelPanel.tsx b/packages/webui/src/client/features/models/ui/ModelPanel.tsx index 22b58d617..e4abda7b5 100644 --- a/packages/webui/src/client/features/models/ui/ModelPanel.tsx +++ b/packages/webui/src/client/features/models/ui/ModelPanel.tsx @@ -121,6 +121,13 @@ export function WebuiModelPanel({ const active = typeof recordedContext === "number" && recordedContext === value; + // The catalog's word for a window that bills as the heavier + // choice. The hint is keyed by the option's own value, so it can + // only ever annotate a size the row already shows — and a model + // whose catalog states none renders the size alone. + const higherUsage = + model?.contextWindowOptionHints?.[String(value)] === + "higher_usage"; return ( ); })} diff --git a/packages/webui/src/client/features/models/ui/models.css b/packages/webui/src/client/features/models/ui/models.css index c71f0c4bc..b0167cd87 100644 --- a/packages/webui/src/client/features/models/ui/models.css +++ b/packages/webui/src/client/features/models/ui/models.css @@ -306,6 +306,7 @@ display: inline-flex; height: var(--spacing_24); align-items: center; + gap: var(--spacing_4); border: 1px solid var(--border_default); border-radius: var(--radius_8); background: transparent; @@ -322,6 +323,14 @@ border-color: var(--border_accent); color: var(--text_default_primary); } + /* The catalog's word for the window that bills as the heavier choice. It is + * metadata about the size, not the size itself, so it is quieter than the + * number it rides with — and quieter in both themes, being a semantic role + * rather than a color. */ + .webui-model-panel-context-option-hint { + color: var(--text_default_tertiary); + font-variant-numeric: normal; + } /* ---- the thinking scale ---- * * A blue rail up to the thumb, a white thumb ON it, and the stops as small diff --git a/packages/webui/test/unit/model-panel-context-hints.test.tsx b/packages/webui/test/unit/model-panel-context-hints.test.tsx new file mode 100644 index 000000000..bec384aea --- /dev/null +++ b/packages/webui/test/unit/model-panel-context-hints.test.tsx @@ -0,0 +1,94 @@ +/** + * `WebuiModelPanel` — the context window row's per-option usage hint. + * + * The catalog states, for a model whose windows are not billed equally, which + * window is the heavier choice (`contextWindowOptionHints: { "1000000": + * "higher_usage" }`), and the server projection passes the fact through. The + * panel is where the fact becomes a word the person choosing can read: without + * it, a 1M window that bills differently reads exactly like a 200K one. + * + * No DOM framework is allowed here, so the branches are asserted through + * `renderToStaticMarkup` — the hint is pure markup, so SSR sees all of it. + */ +import { describe, expect, it } from "vitest"; +import { renderToStaticMarkup } from "react-dom/server"; +import { createElement } from "react"; + +import { WebuiModelPanel } from "../../src/client/features/models/ui/ModelPanel.js"; +import type { WebuiModelPickerEntry } from "../../src/client/features/models/contracts/model-view.js"; + +const HINT = "较高用量"; + +function renderPanel(model: WebuiModelPickerEntry | undefined): string { + return renderToStaticMarkup( + createElement(WebuiModelPanel, { + model, + draft: {}, + onOpenList: () => undefined, + onEffortChange: () => undefined, + onResetEffort: () => undefined, + onContextLimitChange: () => undefined, + }), + ); +} + +/** The one context-option button for `value`, as markup. */ +function option(html: string, value: number): string { + const match = html.match( + new RegExp( + `]*data-webui-model-context-option="${value}"[\\s\\S]*?`, + ), + ); + // Not a manufactured testid read: the failure message names the option that + // did not render, which is the fact under test when it is missing. + expect(match, `context option ${value} rendered`).toBeTruthy(); + return match![0]; +} + +const WINDOWS: WebuiModelPickerEntry = { + providerId: "minimax_api", + modelId: "MiniMax-M3", + contextWindowOptions: [128_000, 1_000_000], +}; + +describe("model panel — the context window's usage hint", () => { + it("marks the window the catalog calls the heavier choice, and only that one", () => { + const html = renderPanel({ + ...WINDOWS, + contextWindowOptionHints: { "1000000": "higher_usage" }, + }); + + // The hint rides inside its option, as the class that styles it. + expect(option(html, 1_000_000)).toContain( + '较高用量', + ); + // The ordinary window keeps stating only its size. + expect(option(html, 128_000)).not.toContain(HINT); + // Exactly one hint in the whole panel, whatever the row holds. + expect(html.match(new RegExp(HINT, "gu"))).toHaveLength(1); + }); + + it("renders no hint for a model whose catalog carries none", () => { + const html = renderPanel(WINDOWS); + + expect(html).toContain('data-webui-model-context-option="128000"'); + expect(html).toContain('data-webui-model-context-option="1000000"'); + expect(html).not.toContain(HINT); + }); + + it("ignores hint values other than the one the wire contract names", () => { + // The server projection keeps only `higher_usage` entries; the view type + // is looser than that, so the panel answers the same filter itself rather + // than printing a value it has no wording for. + const html = renderPanel({ + ...WINDOWS, + contextWindowOptionHints: { + "128000": "lower_usage", + "1000000": "higher_usage", + }, + }); + + expect(option(html, 128_000)).not.toContain(HINT); + expect(option(html, 1_000_000)).toContain(HINT); + }); +}); diff --git a/test/vitest-suites.json b/test/vitest-suites.json index 96e088acf..439906ad0 100644 --- a/test/vitest-suites.json +++ b/test/vitest-suites.json @@ -330,6 +330,7 @@ "packages/webui/test/unit/model-picker-effort-options.test.ts", "packages/webui/test/unit/model-effort-scale.test.ts", "packages/webui/test/unit/model-picker-groups.test.tsx", + "packages/webui/test/unit/model-panel-context-hints.test.tsx", "packages/webui/test/unit/model-provider-projection.test.ts", "packages/webui/test/unit/models-workflows.test.ts", "packages/webui/test/unit/output-error.test.tsx", From 405c79370b8a90098108d52b7413452ce8f42fb3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BC=98=E6=89=AC?= Date: Tue, 27 Oct 2026 20:10:13 +0800 Subject: [PATCH 04/12] feat(webui): summarize todo completion in the progress section header MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The roadmap's completion-percentage affordance was never rendered: the Todo section listed items but its header showed only the title. The progress section header now carries a quiet tertiary-text summary (`3/7 已完成 · 43%`) computed client-side from the todo list — completed count over all todos, rounded to a whole percent. It renders only when todos exist, rides in the header button so it survives collapsing, and the empty state stays as plain copy. The live browser spec's Todo heading selector moves from the exact accessible name to a title-anchored pattern because the summary is now part of the button's name; the stream test also pins that the summary tracks a live todo update. --- .../workspace/ui/WorkspaceProgressPanel.tsx | 13 ++++++-- .../browser/workspace-progress-live.spec.mjs | 9 +++++- packages/webui/test/unit/webui-shell.test.ts | 31 +++++++++++++++++++ 3 files changed, 50 insertions(+), 3 deletions(-) diff --git a/packages/webui/src/client/features/workspace/ui/WorkspaceProgressPanel.tsx b/packages/webui/src/client/features/workspace/ui/WorkspaceProgressPanel.tsx index 54b528ce9..57ebf602f 100644 --- a/packages/webui/src/client/features/workspace/ui/WorkspaceProgressPanel.tsx +++ b/packages/webui/src/client/features/workspace/ui/WorkspaceProgressPanel.tsx @@ -20,10 +20,12 @@ const PROGRESS_COPY = { close: "关闭", } as const; -function WorkspaceProgressSection({ kind, className, title, collapsed, onToggle, children, busy }: { +function WorkspaceProgressSection({ kind, className, title, titleMeta, collapsed, onToggle, children, busy }: { readonly kind: "environment" | "progress" | "subagents"; readonly className: string; readonly title: string; + /** Quiet metadata shown in the header next to the title (completion summary). */ + readonly titleMeta?: ReactElement; readonly collapsed: boolean; readonly onToggle?: () => void; readonly children: ReactElement | null; @@ -32,6 +34,7 @@ function WorkspaceProgressSection({ kind, className, title, collapsed, onToggle, return
element?.toggleAttribute("inert", collapsed)}> @@ -48,7 +51,13 @@ export function WebuiProgressPanel({ todos, showProgress = true, showEmptyState readonly onToggle?: () => void; }): ReactElement | null { if (!showProgress || (todos.length === 0 && !showEmptyState)) return null; - return + // Completion summary for the section header: completed count over the whole + // list. An empty list renders no summary (the empty state explains itself). + const completedCount = todos.filter((todo) => todo.status === "completed").length; + const completionSummary = todos.length > 0 + ? `${completedCount}/${todos.length} 已完成 · ${Math.round((completedCount / todos.length) * 100)}%` + : null; + return {completionSummary}} collapsed={collapsed} onToggle={onToggle}> {todos.length === 0 ?

{PROGRESS_COPY.progressEmpty}

:
{todos.map((todo, index) =>
{todo.status === "completed" ? : index + 1} diff --git a/packages/webui/test/browser/workspace-progress-live.spec.mjs b/packages/webui/test/browser/workspace-progress-live.spec.mjs index b6bb22586..5b788e785 100644 --- a/packages/webui/test/browser/workspace-progress-live.spec.mjs +++ b/packages/webui/test/browser/workspace-progress-live.spec.mjs @@ -218,6 +218,9 @@ test("a todo update inside a stream message reaches the progress panel live", as }), }); await expect.poll(() => panelText(page)).toContain("第一步:调研"); + // One in-progress todo, nothing completed yet: the header summary tracks the + // live list, not the snapshot from session open. + await expect.poll(() => panelText(page)).toContain("0/1 已完成 · 0%"); // A `todowrite` call in the same live turn carries the full list too, in the // raw wire shape where the name and arguments sit under `function`. @@ -258,7 +261,11 @@ test("collapsing Todo releases the overview space and moves Subagents upward", a }), }); - const todoHeading = page.getByRole("button", { name: "进度", exact: true }); + // The Todo section's accessible name carries the completion summary + // ("进度 0/1 已完成 · 0%"), so match on the title prefix rather than the + // exact name. The panel toggle control is labelled "环境信息与进度" and does + // not start with "进度", so the anchored pattern stays unambiguous. + const todoHeading = page.getByRole("button", { name: /^进度/ }); await expect(todoHeading).toHaveAttribute("aria-expanded", "true"); await expect(page.getByRole("button", { name: "layout-worker running" })).toBeVisible(); const expanded = await page.evaluate(() => { diff --git a/packages/webui/test/unit/webui-shell.test.ts b/packages/webui/test/unit/webui-shell.test.ts index 024094c7b..cc2c7120f 100644 --- a/packages/webui/test/unit/webui-shell.test.ts +++ b/packages/webui/test/unit/webui-shell.test.ts @@ -395,6 +395,37 @@ describe("WebUI shell", () => { expect(collapsed).toContain('aria-expanded="false"'); expect(collapsed).toContain('aria-hidden="true"'); }); + + it("summarizes todo completion in the section header and hides the summary for an empty list", () => { + // 3 completed of 7 total; `cancelled` still counts towards the total the + // roadmap's completion percentage is computed over. + const todos = [ + { content: "已完成步骤 1", status: "completed" as const }, + { content: "已完成步骤 2", status: "completed" as const }, + { content: "已完成步骤 3", status: "completed" as const }, + { content: "已取消步骤", status: "cancelled" as const }, + { content: "待处理步骤", status: "pending" as const }, + { content: "当前步骤", status: "in_progress" as const }, + { content: "另一待处理", status: "pending" as const }, + ]; + const markup = renderToStaticMarkup(createElement(WebuiProgressPanel, { todos })); + expect(markup).toContain("3/7 已完成 · 43%"); + // The summary rides in the header button, so collapsing the section hides + // the rows while the summary stays readable. + const collapsed = renderToStaticMarkup(createElement(WebuiProgressPanel, { todos, collapsed: true })); + expect(collapsed).toContain("3/7 已完成 · 43%"); + expect(collapsed).toContain("webui-progress-section webui-progress-panel is-collapsed"); + // The 0% and 100% edges stay whole numbers, not fractions or NaN. + expect(renderToStaticMarkup(createElement(WebuiProgressPanel, { todos: [{ content: "未开始", status: "pending" }] }))).toContain("0/1 已完成 · 0%"); + expect(renderToStaticMarkup(createElement(WebuiProgressPanel, { todos: [{ content: "收尾", status: "completed" }] }))).toContain("1/1 已完成 · 100%"); + // An empty list renders the plain empty state: no summary line appears. + const empty = renderToStaticMarkup(createElement(WebuiProgressPanel, { todos: [], showEmptyState: true })); + expect(empty).toContain("跟踪较长任务的进度"); + expect(empty).not.toContain("已完成 ·"); + // The overview panel carries the same summary through its progress section. + const overview = renderToStaticMarkup(createElement(WebuiProgressOverviewPanel, { todos, open: true })); + expect(overview).toContain("3/7 已完成 · 43%"); + }); it("preserves an explicit empty thinking variant in model selection requests", () => { expect( buildWebuiModelSelectionRequest( From 6592974b55456026c71984797d77dff7373b225f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BC=98=E6=89=AC?= Date: Tue, 27 Oct 2026 22:22:00 +0800 Subject: [PATCH 05/12] fix(local-runtime): release the process-cached V1 sqlite connection on host close MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The V1 stores share one cached better-sqlite3 connection per data directory (withLocalRuntimeDb), and nothing owned its lifetime: after apiHost.close() resolved, runtime-state.sqlite and its -shm stayed locked by a handle. POSIX unlinks open files silently, so only Windows surfaced it — every isolated-data-dir consumer's cleanup rm hung on EBUSY and the integration hooks timed out. closeResources now closes that cached connection last, keyed by the construction-time data directory rather than the live configGetter, which a second host booted in the same process has already moved. --- packages/local-runtime/src/api/host.ts | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/packages/local-runtime/src/api/host.ts b/packages/local-runtime/src/api/host.ts index fdcb31692..8527e09e5 100644 --- a/packages/local-runtime/src/api/host.ts +++ b/packages/local-runtime/src/api/host.ts @@ -96,6 +96,7 @@ import type { LocalTokenUsageStore, LocalTurnDiffStore, } from "../persistence/ports.js"; +import { closeLocalRuntimeDb } from "../persistence/db.js"; import { SqliteLocalCommunicationMessageStore, SqliteLocalTokenUsageStore, @@ -436,6 +437,14 @@ export class LocalRuntimeApiHost { private readonly metricsReporter: LocalRuntimeApiHostOptions["metricsReporter"]; private readonly mavisCronAdapterProvider: LocalRuntimeApiHostOptions["mavisCronAdapterProvider"]; private readonly deleteAgentCronTasksOverride: LocalRuntimeApiHostOptions["deleteAgentCronTasks"]; + /** + * The data directory this host's stores opened against, captured at + * construction. `configGetter()` stays live — a process may boot a second + * host (and move `MINIMAX_DATA_DIR`) before the first one closes — but the + * cached V1 SQLite connection this host owns is keyed by the construction + * time path, so that is the path close must release. + */ + private readonly persistenceDataDir: string; private readonly fileGitChangesCoordinator = createLocalFileApiGitChangesCoordinator(); constructor(options: LocalRuntimeApiHostOptions = {}) { @@ -507,6 +516,7 @@ export class LocalRuntimeApiHost { ); } const runtimeDataDir = this.configGetter().dataDir; + this.persistenceDataDir = runtimeDataDir; try { ensureTrashScript(runtimeDataDir); // Seeded alongside the trash script: the shim is the execution-layer @@ -1477,6 +1487,16 @@ export class LocalRuntimeApiHost { () => this.globalEvents.close(), () => drainBackgroundTasks(this), () => this.errorReporter?.close(), + // The V1 stores share one process-wide cached connection per data + // directory (`withLocalRuntimeDb`), and nothing else owns its lifetime. + // Closing it last — after the background tasks that may still write + // through it have drained — releases the SQLite file handles; on + // Windows an open handle keeps the data directory undeletable + // (`EBUSY`), which every isolated-data-dir consumer inherits. The + // construction-time path is the key: `configGetter()` reads the live + // environment, which a second host in the same process has already + // moved. + () => closeLocalRuntimeDb(this.persistenceDataDir), ]; for (const close of cleanups) { try { From 3ef4c0269d1d2b1bf3ea4e8efbc06649ff67e155 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BC=98=E6=89=AC?= Date: Tue, 27 Oct 2026 22:22:24 +0800 Subject: [PATCH 06/12] fix(webui): make the profile-root guard separator-agnostic MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit isInsideDataDir compared resolve()d paths with a `${root}/` prefix, which never matches on Windows where resolve keeps the platform separator: every in-profile path was refused as outside. Compare through path.relative instead — separator-agnostic, and sibling roots such as -other still climb out with a leading '..' and are rejected. --- packages/webui/src/runtime/profile-files.ts | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/packages/webui/src/runtime/profile-files.ts b/packages/webui/src/runtime/profile-files.ts index 31e165c4f..2b1a565cc 100644 --- a/packages/webui/src/runtime/profile-files.ts +++ b/packages/webui/src/runtime/profile-files.ts @@ -51,7 +51,7 @@ import { MEMORY_TAIL_INJECTION_CAP_CHARS } from "@mavis/shared/memory-limits"; import { randomBytes } from "node:crypto"; import { mkdir, open, readFile, rename, stat, unlink } from "node:fs/promises"; -import { dirname, join, resolve } from "node:path"; +import { dirname, isAbsolute, join, relative, resolve } from "node:path"; import type { WebuiAgentMemoryView, WebuiGlobalInstructionsView, WebuiUserProfileFields, WebuiUserProfileView } from "../shared/contracts/personalization.js"; @@ -513,9 +513,12 @@ export async function writeUserProfile( /** Guards against a path resolving outside the profile root. */ export function isInsideDataDir(dataDir: string, filePath: string): boolean { - const root = resolve(dataDir); - const target = resolve(filePath); - return target === root || target.startsWith(`${root}/`); + // `resolve` keeps the platform separator, so a `${root}/` prefix only works + // on POSIX. Compare through `relative` instead: it is separator-agnostic and + // still rejects sibling roots such as `-other` (their relative path + // climbs out with a leading `..`). + const rel = relative(resolve(dataDir), resolve(filePath)); + return rel === "" || (!rel.startsWith("..") && !isAbsolute(rel)); } /** From 6820a5d97558f7eed230060856d5e4df6eb9a5fc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BC=98=E6=89=AC?= Date: Tue, 27 Oct 2026 22:22:24 +0800 Subject: [PATCH 07/12] test(webui): carry the suite's Windows dialect through the frozen contracts - Speak the '/' path dialect where assertions meet path helpers or TypeScript's normalized file names (boundary-check, dependency-check fixtures, plugin-action-inventory, design-tokens, host-event-generation): the host-event-generation mutation hook in particular compared a backslash path by strict equality against TS's normalized names, so the rewrite never landed and the case asserted against an unmutated program. - Build the archive fixtures without assuming Info-ZIP or a colon-free archive path: adapter-conversions writes the same single-entry zip byte by byte when the CLI is absent, and the archive suite passes --force-local on Windows so MSYS tar does not read the drive-letter colon as a remote host spec. - webui-service takes openClient's up-front closed promise so the post-shutdown case cannot miss the client's close event while awaiting service.close(); agent-transcript polls the rewound row's removal (the socket frame and the React commit are separate steps), follows the Todo heading to the summary-carrying accessible name the 405c7937 header introduced, and reads the attachment path out of the JSON-stringified prompt without excluding Windows separators. - Register webui-turn-jump-flash in the W0 animation inventories the b9c85a91 TurnNavigator feature added to transcript.css. --- .../test/gates/webui-boundary-check.test.ts | 6 +- .../test/gates/webui-dependency-check.test.ts | 11 +++- .../integration/adapter-conversions.test.ts | 56 ++++++++++++++++- .../test/integration/agent-transcript.test.ts | 62 ++++++++++++------- .../test/unit/plugin-action-inventory.test.ts | 10 ++- .../test/unit/webui-design-tokens.test.ts | 6 +- .../unit/webui-host-event-generation.test.ts | 8 ++- .../webui/test/unit/webui-service.test.ts | 8 ++- .../test/unit/webui-w0-css-structure.test.ts | 10 ++- .../test/unit/webui-workspace-archive.test.ts | 11 +++- 10 files changed, 149 insertions(+), 39 deletions(-) diff --git a/packages/webui/test/gates/webui-boundary-check.test.ts b/packages/webui/test/gates/webui-boundary-check.test.ts index c1cf3d7f5..4185fe62a 100644 --- a/packages/webui/test/gates/webui-boundary-check.test.ts +++ b/packages/webui/test/gates/webui-boundary-check.test.ts @@ -231,7 +231,11 @@ describe("webui boundary rules", () => { }); it("script lives at the repo-root scripts directory", () => { - expect(path.relative(repoRoot, here)).toMatch(/^packages\/webui\/test/); + // `path.relative` speaks the platform separator; the repo's path dialect + // is `/`, the same one `rewriteInputs` normalizes to a few cases below. + expect(path.relative(repoRoot, here).split(path.sep).join("/")).toMatch( + /^packages\/webui\/test/, + ); }); }); diff --git a/packages/webui/test/gates/webui-dependency-check.test.ts b/packages/webui/test/gates/webui-dependency-check.test.ts index d28e7b81c..6fae23932 100644 --- a/packages/webui/test/gates/webui-dependency-check.test.ts +++ b/packages/webui/test/gates/webui-dependency-check.test.ts @@ -75,7 +75,7 @@ describe("V42 frozen pre-refactor metadata and permissions", () => { for (const entry of readdirSync(directory, { withFileTypes: true })) { const filename = path.join(directory, entry.name); if (entry.isDirectory()) walk(filename); - else if (/\.(?:ts|tsx|css)$/u.test(filename)) paths.push(path.relative(sourceRoot, filename)); + else if (/\.(?:ts|tsx|css)$/u.test(filename)) paths.push(path.relative(sourceRoot, filename).split(path.sep).join("/")); } }; walk(sourceRoot); @@ -570,7 +570,10 @@ describe("direction fixtures", () => { }); it("keeps a React host dependency when TypeScript resolves it outside the WebUI source tree", () => { - const reactTypes = path.join(repoRoot, "node_modules/@types/react/index.d.ts"); + // TypeScript resolves path-mapped targets in the `/` dialect (the graph + // records `resolvedFileName` verbatim), so the expectation speaks it too + // rather than the host separator `path.join` produced. + const reactTypes = path.join(repoRoot, "node_modules/@types/react/index.d.ts").split(path.sep).join("/"); expect(readFileSync(reactTypes, "utf8")).toContain("export = React"); const { graph, result } = evaluateFixture({ "client/application/app.ts": 'import { useState } from "react";\nexport const use = useState;\n', @@ -939,7 +942,9 @@ describe("the ambient-member rule", () => { // one, even when the specifier it was authored with says nothing of the sort. describe("the resolved-host rule", () => { it("rejects a React dependency that arrives through a path alias", () => { - const reactTypes = path.join(repoRoot, "node_modules/@types/react/index.d.ts"); + // Same dialect note as the direction fixture above: `resolvedFileName` + // for a path-mapped target arrives with `/` separators. + const reactTypes = path.join(repoRoot, "node_modules/@types/react/index.d.ts").split(path.sep).join("/"); const { graph, result } = evaluateFixture( { "client/application/app.ts": diff --git a/packages/webui/test/integration/adapter-conversions.test.ts b/packages/webui/test/integration/adapter-conversions.test.ts index 58dd8b56a..b42c7fe9b 100644 --- a/packages/webui/test/integration/adapter-conversions.test.ts +++ b/packages/webui/test/integration/adapter-conversions.test.ts @@ -45,6 +45,55 @@ import type { } from "../../src/shared/contracts/goal.js"; import { removeRealHostDataDir } from "./remove-real-host-data-dir.js"; +/** Whether the Info-ZIP CLI is on PATH (`test/unit/webui-workspace-archive.test.ts` asks the same). */ +const HAS_ZIP = (() => { + try { + execFileSync("zip", ["--help"], { stdio: "ignore" }); + return true; + } catch { + return false; + } +})(); + +/** + * A one-entry stored zip, byte by byte. + * + * The `zip` CLI is a fixture builder, not the thing under test — the port's + * archive reader reads whatever bytes are in the file — and Windows hosts ship + * without Info-ZIP. Rather than lose the fallback assertion on those hosts, + * the same archive is written by hand, exactly the way the archive unit suite + * substitutes a crafted zip when it cannot call the CLI. + */ +function craftSingleEntryZip(name: string, content: string): Buffer { + const nameBytes = Buffer.from(name, "utf8"); + const data = Buffer.from(content, "utf8"); + const local = Buffer.alloc(30); + local.writeUInt32LE(0x04034b50, 0); + local.writeUInt16LE(20, 4); + local.writeUInt16LE(0x800, 6); // UTF-8 names + local.writeUInt16LE(0, 8); // stored + local.writeUInt32LE(data.length, 18); + local.writeUInt32LE(data.length, 22); + local.writeUInt16LE(nameBytes.length, 26); + const central = Buffer.alloc(46); + central.writeUInt32LE(0x02014b50, 0); + central.writeUInt16LE((3 << 8) | 20, 4); // unix host, spec 2.0 + central.writeUInt16LE(20, 6); + central.writeUInt16LE(0x800, 8); + central.writeUInt16LE(0, 10); // stored + central.writeUInt32LE(data.length, 20); + central.writeUInt32LE(data.length, 24); + central.writeUInt16LE(nameBytes.length, 28); + central.writeUInt32LE((0o100644 & 0xffff) * 0x10000, 38); + const end = Buffer.alloc(22); + end.writeUInt32LE(0x06054b50, 0); + end.writeUInt16LE(1, 8); + end.writeUInt16LE(1, 10); + end.writeUInt32LE(central.length, 12); + end.writeUInt32LE(local.length + nameBytes.length + data.length, 16); + return Buffer.concat([local, nameBytes, data, central, nameBytes, end]); +} + interface Booted { readonly dataDir: string; readonly workspaceDir: string; @@ -225,7 +274,12 @@ describe("adapter translations against the real host", () => { // The v2 host exposes no archive reader, so this exercises the fallback to // `runtime/workspace-archive.ts` through the real port. const archivePath = join(workspaceDir, "bundle.zip"); - execFileSync("zip", ["-q", "-r", archivePath, "tracked.txt"], { cwd: workspaceDir }); + if (HAS_ZIP) { + execFileSync("zip", ["-q", "-r", archivePath, "tracked.txt"], { cwd: workspaceDir }); + } else { + // No Info-ZIP on this host: the same archive, written byte by byte. + await writeFile(archivePath, craftSingleEntryZip("tracked.txt", "tracked\n")); + } const listing = await port.readWorkspaceArchive({ workspaceDir, path: "bundle.zip" }); expect(listing.archivePath).toBe("bundle.zip"); expect(listing.entries.map((entry) => entry.name)).toContain("tracked.txt"); diff --git a/packages/webui/test/integration/agent-transcript.test.ts b/packages/webui/test/integration/agent-transcript.test.ts index af76b009e..1976b2740 100644 --- a/packages/webui/test/integration/agent-transcript.test.ts +++ b/packages/webui/test/integration/agent-transcript.test.ts @@ -603,25 +603,35 @@ it("a real agent turn resumes after refresh and drains a queued message in order messages?: { msgId?: string; role?: string; msgContent?: string }[]; } | undefined)?.messages; expect(refreshMessages?.some((message) => message.msgId === persistedSourceUserMessageId)).toBe(false); - expect(await persistedSourceUserMessage.count(), JSON.stringify({ - refreshMessagesRequest: refreshMessagesRequest && { - requestId: refreshMessagesRequest.requestId, - operation: refreshMessagesRequest.operation, - body: refreshMessagesRequest.body, - }, - refreshMessages: refreshMessages?.map((message) => ({ - id: message.msgId, - role: message.role, - content: message.msgContent, - })), - visibleMessageIds: await page.locator("[data-webui-message-role]").evaluateAll((nodes) => - nodes.map((node) => ({ - role: node.getAttribute("data-webui-message-role"), - id: node.getAttribute("data-message-id"), - text: node.textContent, + // The refreshed history response arriving on the socket (observed above + // through `received`) and React committing the row's removal are two + // separate steps; poll for the DOM catch-up rather than asserting the + // count in the same tick the frame landed, and keep the diagnostic dump + // for the case where it never does. + try { + await browserExpect.poll(() => persistedSourceUserMessage.count(), { timeout: 10_000 }) + .toBe(0); + } catch (error) { + throw new Error(`${error instanceof Error ? error.message : String(error)}\n${JSON.stringify({ + refreshMessagesRequest: refreshMessagesRequest && { + requestId: refreshMessagesRequest.requestId, + operation: refreshMessagesRequest.operation, + body: refreshMessagesRequest.body, + }, + refreshMessages: refreshMessages?.map((message) => ({ + id: message.msgId, + role: message.role, + content: message.msgContent, })), - ), - })).toBe(0); + visibleMessageIds: await page.locator("[data-webui-message-role]").evaluateAll((nodes) => + nodes.map((node) => ({ + role: node.getAttribute("data-webui-message-role"), + id: node.getAttribute("data-message-id"), + text: node.textContent, + })), + ), + })}`); + } const mountedTranscript = page.locator(`[data-webui-transcript="${sessionId}"]`); await browserExpect(mountedTranscript.getByText("Message actions before refresh.", { exact: true })) .toBeVisible(); @@ -833,7 +843,12 @@ it("a real agent turn resumes after refresh and drains a queued message in order await composer.press("Enter"); await browserExpect(page.getByTestId("progress-overview-card").getByText("Visible task from runtime tool")) .toBeVisible({ timeout: 30_000 }); - const todoPanel = page.getByRole("button", { name: "进度", exact: true }); + // The Todo section's accessible name carries the completion summary + // ("进度 0/1 已完成 · 0%"), so match on the title prefix rather than the + // exact name — the same anchored pattern the live browser spec uses. The + // panel toggle control is labelled "环境信息与进度" and does not start + // with "进度", so the pattern stays unambiguous. + const todoPanel = page.getByRole("button", { name: /^进度/ }); await browserExpect(todoPanel).toBeVisible(); await browserExpect(page.locator("span.webui-tool-label").filter({ hasText: "todowrite" })) .toHaveText("todowrite"); @@ -993,8 +1008,13 @@ it("a real agent turn resumes after refresh and drains a queued message in order const referencePrompt = (referencePayload.messages ?? []) .map((message) => typeof message.content === "string" ? message.content : JSON.stringify(message.content)) .join("\n"); - const runtimeAttachmentPath = /]* path=\\?"([^"\\]+)\\?"/u - .exec(referencePrompt)?.[1]; + // The attachment path is read out of the prompt the model stub received. + // Non-string message content is JSON.stringify'd above, so on Windows the + // path arrives escaped (`path=\"C:\\Users\\…\"`): the capture may not + // exclude the backslash, and the doubled separators fold back to the real + // path (a no-op for POSIX separators). + const runtimeAttachmentPath = /]* path=\\?"([^"]+)"/u + .exec(referencePrompt)?.[1]?.replaceAll("\\\\", "\\"); expect(runtimeAttachmentPath).toBeDefined(); expect(await readFile(runtimeAttachmentPath!, "utf8")).toBe("Synthetic attachment body."); await expect.poll(async () => { diff --git a/packages/webui/test/unit/plugin-action-inventory.test.ts b/packages/webui/test/unit/plugin-action-inventory.test.ts index c22d2ceb8..9311499c4 100644 --- a/packages/webui/test/unit/plugin-action-inventory.test.ts +++ b/packages/webui/test/unit/plugin-action-inventory.test.ts @@ -10,7 +10,7 @@ // to any carrier without the document, or a name changed in one place, fails here. import { readFileSync, readdirSync, statSync } from "node:fs"; -import { join } from "node:path"; +import { join, relative, sep } from "node:path"; import { fileURLToPath } from "node:url"; import { describe, expect, it } from "vitest"; @@ -118,9 +118,13 @@ describe("each action's business is the one the inventory records", () => { return listFiles(dir).filter((file) => /pluginManagement/u.test(stripComments(readSafe(file)))); }); expect(reach.length).toBeGreaterThan(0); - const base = fileURLToPath(CLIENT).replace(/\/$/u, "") + "/"; + // `fileURLToPath` ends in the platform separator, and `join`-built paths + // carry it throughout on Windows, so slicing off a `/`-anchored base both + // clips a character and leaves `\` separators that match no allowance. + // Compare through `relative` and speak the allowance's `/` dialect. + const base = fileURLToPath(CLIENT).replace(/[\\/]+$/u, ""); for (const file of reach) { - const rel = file.slice(base.length); + const rel = relative(base, file).split(sep).join("/"); expect(allowed, `${rel} reaches the plugin port`).toContain(rel); } }); diff --git a/packages/webui/test/unit/webui-design-tokens.test.ts b/packages/webui/test/unit/webui-design-tokens.test.ts index 98735ef55..15f710d63 100644 --- a/packages/webui/test/unit/webui-design-tokens.test.ts +++ b/packages/webui/test/unit/webui-design-tokens.test.ts @@ -114,8 +114,10 @@ function semanticNames(): string[] { describe("WebUI design tokens", () => { it("lives at packages/webui/src/client/styles/tokens.css", () => { - // Sanity check that the file is being read from the right path. - expect(tokensPath.endsWith("src/client/styles/tokens.css")).toBe(true); + // Sanity check that the file is being read from the right path. Spoken in + // the `/` dialect because `path` helpers keep the platform separator. + const rel = path.relative(packageDir, tokensPath).split(path.sep).join("/"); + expect(rel).toBe("src/client/styles/tokens.css"); }); it("carries the 211 semantic tokens that packages/webui/docs/design-language.md names", () => { diff --git a/packages/webui/test/unit/webui-host-event-generation.test.ts b/packages/webui/test/unit/webui-host-event-generation.test.ts index 6968383af..f211df688 100644 --- a/packages/webui/test/unit/webui-host-event-generation.test.ts +++ b/packages/webui/test/unit/webui-host-event-generation.test.ts @@ -93,8 +93,12 @@ describe("host-derived event types", () => { const configPath = path.join(root, "packages/webui/tsconfig.client.json"); const config = ts.readConfigFile(configPath, ts.sys.readFile); const parsed = ts.parseJsonConfigFileContent(config.config, ts.sys, path.dirname(configPath)); - const generated = path.join(root, "packages/webui/src/shared/contracts/host-event-types.generated.ts"); - const reader = path.join(root, "packages/webui/src/client/features/turn/application/session-fact.ts"); + // TypeScript addresses program files in the `/` dialect even on Windows + // (`getSourceFiles()` names prove it), so the mutation hook below — which + // compares by strict equality — must speak it too, or the rewrite never + // lands and the case asserts against an unmutated program. + const generated = path.join(root, "packages/webui/src/shared/contracts/host-event-types.generated.ts").split(path.sep).join("/"); + const reader = path.join(root, "packages/webui/src/client/features/turn/application/session-fact.ts").split(path.sep).join("/"); function diagnostics(mutate: boolean) { const host = ts.createCompilerHost(parsed.options); const read = host.readFile.bind(host); diff --git a/packages/webui/test/unit/webui-service.test.ts b/packages/webui/test/unit/webui-service.test.ts index 2cd8ae6b2..cafa0e3dd 100644 --- a/packages/webui/test/unit/webui-service.test.ts +++ b/packages/webui/test/unit/webui-service.test.ts @@ -2679,11 +2679,15 @@ describe("WebUI service", () => { it("does not accept requests after shutdown has been requested", async () => { const { url } = await bootService(); - const { ws, upgrade } = openClient(url); + // `closed` is attached up-front by `openClient`: `service.close()` may + // finish the socket teardown while it is being awaited, and a listener + // attached afterwards would miss the client's `close` event and hang — + // the same race the helper's own comment warns about. + const { ws, upgrade, closed } = openClient(url); await upgrade; // Drive the shutdown and assert the in-flight connection is closed. await service.close(); - const closeEvent = await awaitClose(ws); + const closeEvent = await closed; expect(closeEvent.code).toBeGreaterThanOrEqual(1000); expect(port.closed).toBe(true); }); diff --git a/packages/webui/test/unit/webui-w0-css-structure.test.ts b/packages/webui/test/unit/webui-w0-css-structure.test.ts index 337540211..44444e36f 100644 --- a/packages/webui/test/unit/webui-w0-css-structure.test.ts +++ b/packages/webui/test/unit/webui-w0-css-structure.test.ts @@ -863,12 +863,12 @@ describe("W0 · at-rules and animations W5/W6 must not remove", () => { rule.context.some((entry) => entry.includes("prefers-reduced-motion")), ); - // Sixteen rules across the stylesheets the shell imports. Deleting one of + // Seventeen rules across the stylesheets the shell imports. Deleting one of // these is the failure mode this assertion exists for: the base rule would // keep animating for a user who asked for reduced motion. The exact list // is the point -- adding an animated rule without registering its override // here is the same defect, in the other direction. - expect(reducedMotion).toHaveLength(16); + expect(reducedMotion).toHaveLength(17); for (const rule of reducedMotion) expect(rule.body).toMatch(/(?:animation|transition):\s*none/u); @@ -915,6 +915,9 @@ describe("W0 · at-rules and animations W5/W6 must not remove", () => { // Renamed from `.webui-settings-toggle span` when the toggle became the // shared `.webui-toggle-switch` control. ".webui-toggle-switch, .webui-toggle-switch > span", + // Added with the TurnNavigator jump: the destination message flashes + // once, so reduced motion replaces the flash with a steady highlight. + ".webui-turn-jump-flash", // Added with the account menu's daily check-in fly-out, which now enters // with the shared surface's motion. It is rendered through a portal, so // the shell-wide override in `index.css` — which is scoped to @@ -1002,6 +1005,9 @@ describe("W0 · at-rules and animations W5/W6 must not remove", () => { "webui-progress-skeleton", "webui-settings-content-in", "webui-signin-claim-spin", + // The TurnNavigator jump flash: the message a tick jumped to flashes + // once so the destination is findable without following the scroll. + "webui-turn-jump-flash", "webui-user-menu-usage-pulse", // The workspace panel's own reveal: it grows from the right edge, and // the panel that covers it slides the other way. diff --git a/packages/webui/test/unit/webui-workspace-archive.test.ts b/packages/webui/test/unit/webui-workspace-archive.test.ts index 9538cf239..b7d8c3f3a 100644 --- a/packages/webui/test/unit/webui-workspace-archive.test.ts +++ b/packages/webui/test/unit/webui-workspace-archive.test.ts @@ -144,6 +144,13 @@ function canRun(command: string): boolean { const HAS_ZIP = canRun("zip"); const HAS_TAR = canRun("tar"); +/** + * Windows tar builds (MSYS GNU tar) read the drive-letter colon in an absolute + * archive path as a `host:file` remote spec and fail with "Cannot connect to + * C:". GNU tar's `--force-local` keeps the path local; bsdtar (macOS) needs + * no such flag and does not accept it, so it is added on Windows only. + */ +const TAR_LOCAL_FLAGS = process.platform === "win32" ? ["--force-local"] : []; function writeTree( name: string, @@ -188,7 +195,7 @@ function realTar( const args = name.endsWith(".tgz") ? ["czf", artifactPath(name)] : ["cf", artifactPath(name)]; - execFileSync("tar", [...args, "."], { cwd: source, env: FIXTURE_ENV }); + execFileSync("tar", [...args, ...TAR_LOCAL_FLAGS, "."], { cwd: source, env: FIXTURE_ENV }); return artifactPath(name); } @@ -766,7 +773,7 @@ describe("workspace archive extraction", () => { /** A tar containing a symlink, via the CLI (`tar` records typeflag `2`). */ function realTarLink(name: string, source: string, member: string): string { - execFileSync("tar", ["cf", artifactPath(name), member], { + execFileSync("tar", ["cf", artifactPath(name), ...TAR_LOCAL_FLAGS, member], { cwd: source, env: FIXTURE_ENV, }); From 5f3a4454626eac387cdd3a32a871fc1829c8a1fb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BC=98=E6=89=AC?= Date: Tue, 27 Oct 2026 22:44:27 +0800 Subject: [PATCH 08/12] test(webui): write the archive fixtures through sibling-relative tar paths The tar fixtures passed the archive as an absolute path and reached for --force-local on win32, which covers MSYS GNU tar but not the bsdtar that ships with Windows and answers "Option --force-local is not supported". Write the archive as a sibling-relative path (../ from the source directory) instead: no drive-letter colon for GNU tar to mistake for a host spec, and no flag for bsdtar to reject, on any platform. --- .../test/unit/webui-workspace-archive.test.ts | 26 ++++++++++--------- 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/packages/webui/test/unit/webui-workspace-archive.test.ts b/packages/webui/test/unit/webui-workspace-archive.test.ts index b7d8c3f3a..9a0f1c153 100644 --- a/packages/webui/test/unit/webui-workspace-archive.test.ts +++ b/packages/webui/test/unit/webui-workspace-archive.test.ts @@ -144,13 +144,6 @@ function canRun(command: string): boolean { const HAS_ZIP = canRun("zip"); const HAS_TAR = canRun("tar"); -/** - * Windows tar builds (MSYS GNU tar) read the drive-letter colon in an absolute - * archive path as a `host:file` remote spec and fail with "Cannot connect to - * C:". GNU tar's `--force-local` keeps the path local; bsdtar (macOS) needs - * no such flag and does not accept it, so it is added on Windows only. - */ -const TAR_LOCAL_FLAGS = process.platform === "win32" ? ["--force-local"] : []; function writeTree( name: string, @@ -186,16 +179,23 @@ function realZip( return artifactPath(name); } -/** A real tar, or a gzipped one when the name ends in `.tgz`. */ +/** A real tar, or a gzipped one when the name ends in `.tgz`. + * + * The archive argument is sibling-relative (`../` from the source + * directory), not absolute: GNU tar reads the drive-letter colon in an + * absolute Windows path as a `host:file` remote spec, and Windows's bsdtar + * does not accept the `--force-local` escape hatch. A relative path carries + * no colon, so every tar flavour (GNU and bsdtar, all three platforms) takes + * it as a plain local file. */ function realTar( name: string, members: Readonly>, ): string { const source = writeTree(`${name}-src`, members); const args = name.endsWith(".tgz") - ? ["czf", artifactPath(name)] - : ["cf", artifactPath(name)]; - execFileSync("tar", [...args, ...TAR_LOCAL_FLAGS, "."], { cwd: source, env: FIXTURE_ENV }); + ? ["czf", `../${name}`] + : ["cf", `../${name}`]; + execFileSync("tar", [...args, "."], { cwd: source, env: FIXTURE_ENV }); return artifactPath(name); } @@ -773,7 +773,9 @@ describe("workspace archive extraction", () => { /** A tar containing a symlink, via the CLI (`tar` records typeflag `2`). */ function realTarLink(name: string, source: string, member: string): string { - execFileSync("tar", ["cf", artifactPath(name), ...TAR_LOCAL_FLAGS, member], { + // Sibling-relative for the same reason as `realTar`: no drive-letter colon + // for GNU tar to mistake for a host, no flag for bsdtar to reject. + execFileSync("tar", ["cf", `../${name}`, member], { cwd: source, env: FIXTURE_ENV, }); From f1b6d32ef08ee425d34b5c7e449bf4b429813edc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BC=98=E6=89=AC?= Date: Tue, 27 Oct 2026 23:41:07 +0800 Subject: [PATCH 09/12] chore(source): register the context-hints test in the public source inventory --- release/public-source.json | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/release/public-source.json b/release/public-source.json index 1de27d55d..935acc700 100644 --- a/release/public-source.json +++ b/release/public-source.json @@ -115,6 +115,32 @@ ".plan/workspace-browser-tab/complete/two-page-spike.json", ".plan/workspace-browser-tab/complete/workspace-browser-tab.acceptance.json", ".plan/workspace-browser-tab/workspace-browser-tab-plan.md", + ".zcode/gate-install.log", + ".zcode/gate-tc-client-r1.log", + ".zcode/gate-tc-client-r2.log", + ".zcode/gate-tc-server-r1.log", + ".zcode/gate-tc-server-r2.log", + ".zcode/gate-tc-test.log", + ".zcode/gate-test-r1.log", + ".zcode/gate-test-r2.log", + ".zcode/gate-test-r3.log", + ".zcode/probe.log", + ".zcode/webui-retest-report-e2de9a1a.md", + ".zcode/workflow-drafts/.gitignore", + ".zcode/workflow-drafts/Q区错误可见性修复心跳与降级.dwf.ts", + ".zcode/workflow-drafts/四细项实现与PR.dwf.ts", + ".zcode/workflow-drafts/满足度重测源码层.dwf.ts", + ".zcode/workflow-runs/.gitignore", + ".zcode/workflow-runs/dwfeval-6eb8d80a-f32c-4f3d-9c2c-c49f7a1bf91e.mjs", + ".zcode/workflow-runs/dwfeval-b1f44290-a8a9-46ad-bfe8-f67378890ae6.mjs", + ".zcode/workflow-runs/dwfrun-19aaadca-1145-4cb6-8797-aed89ce742d3.mjs", + ".zcode/workflow-runs/dwfrun-2822aff0-4ddf-4927-bb0d-3043233674fb.mjs", + ".zcode/workflow-runs/dwfrun-3808a8e4-4a6c-4a2c-a842-6daf441fb9b8.mjs", + ".zcode/workflow-runs/dwfrun-3edf106e-2334-4e62-b1ad-13f8425c13cf.mjs", + ".zcode/workflow-runs/dwfrun-429c4fe9-3230-406b-a705-af91b7881db7.mjs", + ".zcode/workflow-runs/dwfrun-5b9c655a-dc9e-4a71-a39b-03ab7c347008.mjs", + ".zcode/workflow-runs/dwfrun-a1c119d8-4003-44a1-8581-38251c480f59.mjs", + ".zcode/workflows/webui-satisfaction-retest.dwf.ts", "AGENTS.md", "CONTEXT.md", "CONTRIBUTING.md", @@ -4432,6 +4458,7 @@ "packages/webui/test/unit/miniapp-standalone-route.test.ts", "packages/webui/test/unit/miniapp-surfaces.test.tsx", "packages/webui/test/unit/model-effort-scale.test.ts", + "packages/webui/test/unit/model-panel-context-hints.test.tsx", "packages/webui/test/unit/model-picker-effort-options.test.ts", "packages/webui/test/unit/model-picker-groups.test.tsx", "packages/webui/test/unit/model-provider-projection.test.ts", From 595dbc6059adb6206d943d93151a04de65b6193f Mon Sep 17 00:00:00 2001 From: Hong Liu <88263959+Hylouis233@users.noreply.github.com> Date: Wed, 28 Oct 2026 00:56:18 +0800 Subject: [PATCH 10/12] chore(source): drop local .zcode artifacts from the public source inventory --- release/public-source.json | 26 -------------------------- 1 file changed, 26 deletions(-) diff --git a/release/public-source.json b/release/public-source.json index 935acc700..205499071 100644 --- a/release/public-source.json +++ b/release/public-source.json @@ -115,32 +115,6 @@ ".plan/workspace-browser-tab/complete/two-page-spike.json", ".plan/workspace-browser-tab/complete/workspace-browser-tab.acceptance.json", ".plan/workspace-browser-tab/workspace-browser-tab-plan.md", - ".zcode/gate-install.log", - ".zcode/gate-tc-client-r1.log", - ".zcode/gate-tc-client-r2.log", - ".zcode/gate-tc-server-r1.log", - ".zcode/gate-tc-server-r2.log", - ".zcode/gate-tc-test.log", - ".zcode/gate-test-r1.log", - ".zcode/gate-test-r2.log", - ".zcode/gate-test-r3.log", - ".zcode/probe.log", - ".zcode/webui-retest-report-e2de9a1a.md", - ".zcode/workflow-drafts/.gitignore", - ".zcode/workflow-drafts/Q区错误可见性修复心跳与降级.dwf.ts", - ".zcode/workflow-drafts/四细项实现与PR.dwf.ts", - ".zcode/workflow-drafts/满足度重测源码层.dwf.ts", - ".zcode/workflow-runs/.gitignore", - ".zcode/workflow-runs/dwfeval-6eb8d80a-f32c-4f3d-9c2c-c49f7a1bf91e.mjs", - ".zcode/workflow-runs/dwfeval-b1f44290-a8a9-46ad-bfe8-f67378890ae6.mjs", - ".zcode/workflow-runs/dwfrun-19aaadca-1145-4cb6-8797-aed89ce742d3.mjs", - ".zcode/workflow-runs/dwfrun-2822aff0-4ddf-4927-bb0d-3043233674fb.mjs", - ".zcode/workflow-runs/dwfrun-3808a8e4-4a6c-4a2c-a842-6daf441fb9b8.mjs", - ".zcode/workflow-runs/dwfrun-3edf106e-2334-4e62-b1ad-13f8425c13cf.mjs", - ".zcode/workflow-runs/dwfrun-429c4fe9-3230-406b-a705-af91b7881db7.mjs", - ".zcode/workflow-runs/dwfrun-5b9c655a-dc9e-4a71-a39b-03ab7c347008.mjs", - ".zcode/workflow-runs/dwfrun-a1c119d8-4003-44a1-8581-38251c480f59.mjs", - ".zcode/workflows/webui-satisfaction-retest.dwf.ts", "AGENTS.md", "CONTEXT.md", "CONTRIBUTING.md", From d6108adfae51826bb23e1bea2530588940820ef8 Mon Sep 17 00:00:00 2001 From: Hong Liu <88263959+Hylouis233@users.noreply.github.com> Date: Wed, 28 Oct 2026 00:56:18 +0800 Subject: [PATCH 11/12] test(webui): stop the attachment path capture at the escaped closing quote on POSIX --- .../webui/test/integration/agent-transcript.test.ts | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/packages/webui/test/integration/agent-transcript.test.ts b/packages/webui/test/integration/agent-transcript.test.ts index 1976b2740..68b60b0ac 100644 --- a/packages/webui/test/integration/agent-transcript.test.ts +++ b/packages/webui/test/integration/agent-transcript.test.ts @@ -1010,11 +1010,13 @@ it("a real agent turn resumes after refresh and drains a queued message in order .join("\n"); // The attachment path is read out of the prompt the model stub received. // Non-string message content is JSON.stringify'd above, so on Windows the - // path arrives escaped (`path=\"C:\\Users\\…\"`): the capture may not - // exclude the backslash, and the doubled separators fold back to the real - // path (a no-op for POSIX separators). - const runtimeAttachmentPath = /]* path=\\?"([^"]+)"/u - .exec(referencePrompt)?.[1]?.replaceAll("\\\\", "\\"); + // path arrives escaped (`path=\"C:\\Users\\…\"`). The opening quote's + // escape (if any) is captured and must close the value too, so the JSON + // escape before the closing quote never lands in the path on any platform; + // the doubled separators then fold back to the real path (a no-op for + // POSIX separators and for unescaped string content). + const runtimeAttachmentPath = /]* path=(\\?)"(.+?)\1"/u + .exec(referencePrompt)?.[2]?.replaceAll("\\\\", "\\"); expect(runtimeAttachmentPath).toBeDefined(); expect(await readFile(runtimeAttachmentPath!, "utf8")).toBe("Synthetic attachment body."); await expect.poll(async () => { From cdd542514f073d65e864185a0dba56451652f542 Mon Sep 17 00:00:00 2001 From: Hong Liu <88263959+Hylouis233@users.noreply.github.com> Date: Wed, 28 Oct 2026 00:56:18 +0800 Subject: [PATCH 12/12] test(webui): refresh progress panel and command overlay visual baselines --- .../composer-command-overlay-linux.png | Bin 34976 -> 37379 bytes .../progress-panel-linux.png | Bin 7204 -> 8163 bytes 2 files changed, 0 insertions(+), 0 deletions(-) diff --git a/packages/webui/test/browser/visual-baselines.spec.mjs-snapshots/composer-command-overlay-linux.png b/packages/webui/test/browser/visual-baselines.spec.mjs-snapshots/composer-command-overlay-linux.png index cca587e9d75a733cc92261e315fed39d58e4db11..44e734a051294fefc77a765852b5c8f019102b05 100644 GIT binary patch literal 37379 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 A-T(jq diff --git a/packages/webui/test/browser/visual-baselines.spec.mjs-snapshots/progress-panel-linux.png b/packages/webui/test/browser/visual-baselines.spec.mjs-snapshots/progress-panel-linux.png index 710aa43609759eaf00ce7e598d0c7c73b1d77ba4..a95e7634d1f216e0220149c169e9ed7847fd7c96 100644 GIT binary patch literal 8163 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 Q56)nyKY6BHrU(oE4@r1`rvLx| literal 7204 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