From 10029b995fc0236c8880e836c2853094ad36ce79 Mon Sep 17 00:00:00 2001 From: _Kerman Date: Thu, 10 Sep 2026 17:18:38 +0800 Subject: [PATCH 01/43] feat(web): add compact details to recorded tool rows --- ...client-derived-tool-presentation.i18n.yaml | 4 +- ...-08-23-client-derived-tool-presentation.md | 4 +- ...-23-client-derived-tool-presentation.zh.md | 4 +- .../2026-09-10-compact-tool-details.i18n.yaml | 6 + .../2026-09-10-compact-tool-details.md | 27 + .../2026-09-10-compact-tool-details.zh.md | 27 + apps/web/tests/tool-details.e2e.ts | 83 ++ apps/web/tsconfig.json | 1 + .../ui-conversation/src/client/locales.ts | 372 +++++++++ packages/client/ui-tool/README.i18n.yaml | 4 +- packages/client/ui-tool/README.md | 2 + packages/client/ui-tool/README.zh.md | 2 + packages/client/ui-tool/src/client/apply.ts | 2 + .../tool/components/ToolDetails.module.css | 257 ++++++ .../client/tool/components/ToolDetails.tsx | 134 +++ .../client/tool/components/ToolRow.module.css | 11 + .../src/client/tool/components/ToolRow.tsx | 71 +- .../tool/models/control-details-model.ts | 182 ++++ .../client/tool/models/detail-model-shared.ts | 151 ++++ .../client/tool/models/details-card-model.ts | 145 ++++ .../tool/models/inspection-details-model.ts | 173 ++++ .../src/client/tool/models/todo-diff-model.ts | 76 ++ .../src/client/tool/models/todo-history.ts | 89 ++ .../src/client/tool/toolviews/details-row.tsx | 117 +++ .../src/client/tool/toolviews/todo-row.tsx | 28 +- .../tests/assembly-surfaces.client.spec.tsx | 2 +- .../ui-tool/tests/details-row.client.spec.tsx | 98 +++ .../ui-tool/tests/todo-history.client.spec.ts | 76 ++ .../ui-tool/tests/todo-row.client.spec.tsx | 18 +- .../ui-trajectory/tests/views.client.spec.tsx | 1 + .../src/client/slot-catalog.ts | 1 + snapshots/web/tool-details/session.v3.jsonl | 98 +++ snapshots/web/tool-details/snapshot.yml | 7 + snapshots/web/tool-details/ui.expected.md | 780 ++++++++++++++++++ tsconfig.host.json | 1 + 35 files changed, 3001 insertions(+), 53 deletions(-) create mode 100644 .agents/notes/implemented/architecture/2026-09-10-compact-tool-details.i18n.yaml create mode 100644 .agents/notes/implemented/architecture/2026-09-10-compact-tool-details.md create mode 100644 .agents/notes/implemented/architecture/2026-09-10-compact-tool-details.zh.md create mode 100644 apps/web/tests/tool-details.e2e.ts create mode 100644 packages/client/ui-tool/src/client/tool/components/ToolDetails.module.css create mode 100644 packages/client/ui-tool/src/client/tool/components/ToolDetails.tsx create mode 100644 packages/client/ui-tool/src/client/tool/models/control-details-model.ts create mode 100644 packages/client/ui-tool/src/client/tool/models/detail-model-shared.ts create mode 100644 packages/client/ui-tool/src/client/tool/models/details-card-model.ts create mode 100644 packages/client/ui-tool/src/client/tool/models/inspection-details-model.ts create mode 100644 packages/client/ui-tool/src/client/tool/models/todo-diff-model.ts create mode 100644 packages/client/ui-tool/src/client/tool/models/todo-history.ts create mode 100644 packages/client/ui-tool/src/client/tool/toolviews/details-row.tsx create mode 100644 packages/client/ui-tool/tests/details-row.client.spec.tsx create mode 100644 packages/client/ui-tool/tests/todo-history.client.spec.ts create mode 100644 snapshots/web/tool-details/session.v3.jsonl create mode 100644 snapshots/web/tool-details/snapshot.yml create mode 100644 snapshots/web/tool-details/ui.expected.md diff --git a/.agents/notes/implemented/architecture/2026-08-23-client-derived-tool-presentation.i18n.yaml b/.agents/notes/implemented/architecture/2026-08-23-client-derived-tool-presentation.i18n.yaml index 6f20df4af6..a586d48851 100644 --- a/.agents/notes/implemented/architecture/2026-08-23-client-derived-tool-presentation.i18n.yaml +++ b/.agents/notes/implemented/architecture/2026-08-23-client-derived-tool-presentation.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-23-client-derived-tool-presentation.md -2026-08-23-client-derived-tool-presentation.md: 6b19dc881d572bfece345cbbd5eca688b2e05aab -2026-08-23-client-derived-tool-presentation.zh.md: 98ada31627398f317e4c41336af56e8c6cf0837e +2026-08-23-client-derived-tool-presentation.md: d41ec5ea8ad3707f98eab3803e0d172c05f58041 +2026-08-23-client-derived-tool-presentation.zh.md: 5dd53cdefda4a5e0efcc047bc81edbbd5f199509 diff --git a/.agents/notes/implemented/architecture/2026-08-23-client-derived-tool-presentation.md b/.agents/notes/implemented/architecture/2026-08-23-client-derived-tool-presentation.md index 6b19dc881d..d41ec5ea8a 100644 --- a/.agents/notes/implemented/architecture/2026-08-23-client-derived-tool-presentation.md +++ b/.agents/notes/implemented/architecture/2026-08-23-client-derived-tool-presentation.md @@ -22,7 +22,7 @@ The required result is one raw Session journal and one Client presentation owner ## Decision -The visual-equivalence requirements below exclude the separately approved [nested terminal-card fix](../bug-fix/2026-09-05-nested-terminal-cards.md); all other presentation and ownership constraints remain. +The visual-equivalence requirements below exclude the separately approved [nested terminal-card fix](../bug-fix/2026-09-05-nested-terminal-cards.md) and [compact tool details](2026-09-10-compact-tool-details.md); all other presentation and ownership constraints remain. The Session Remote journal sends only raw, validated, persistable Session events. `session.page` and `session.follow` do not parse tool arguments, query the Tools registry, restore a presenter scope, execute `presentCall` or `presentResult`, or construct or clone any tool view. @@ -51,7 +51,7 @@ The Host `ToolDefinition.presentCall`, `ToolDefinition.presentResult`, `ToolCall | Retained | the Session log format, Remote journal lifecycle, and Conversation identity/topology | | Retained | the existing keyed slot, Generic fallback, and Chat, Details, and Trajectory structure | | Forbidden | a new Client presenter service, parallel registry, or wire renderer id | -| Forbidden | new cards, visual redesign, interaction redesign, or Code Dispatch rich-card enhancements except the [nested terminal-card exception](../bug-fix/2026-09-05-nested-terminal-cards.md) | +| Forbidden | new cards, visual redesign, interaction redesign, or Code Dispatch rich-card enhancements except the [nested terminal-card exception](../bug-fix/2026-09-05-nested-terminal-cards.md) and [compact tool details](2026-09-10-compact-tool-details.md) | | Forbidden | compatibility dual-writing, version negotiation, or retention of the old `view` field | ## Terminology diff --git a/.agents/notes/implemented/architecture/2026-08-23-client-derived-tool-presentation.zh.md b/.agents/notes/implemented/architecture/2026-08-23-client-derived-tool-presentation.zh.md index 98ada31627..5dd53cdefd 100644 --- a/.agents/notes/implemented/architecture/2026-08-23-client-derived-tool-presentation.zh.md +++ b/.agents/notes/implemented/architecture/2026-08-23-client-derived-tool-presentation.zh.md @@ -22,7 +22,7 @@ Host presenter 与 Client keyed renderer 分担展示会形成对同一事件的 ## Decision -下述展示对等要求不包含已独立批准的[嵌套 terminal 卡片修复](../bug-fix/2026-09-05-nested-terminal-cards.zh.md);其他展示与所有权约束全部保留。 +下述展示对等要求不包含已独立批准的[嵌套 terminal 卡片修复](../bug-fix/2026-09-05-nested-terminal-cards.zh.md)和[紧凑工具详情](2026-09-10-compact-tool-details.zh.md);其他展示与所有权约束全部保留。 Session Remote journal 只下发原始、已验证、可持久化的 Session event。`session.page` 和 `session.follow` 不解析工具参数,不查询 Tools registry,不恢复 presenter scope,不执行 `presentCall`/`presentResult`,也不构造或克隆任何 tool view。 @@ -51,7 +51,7 @@ Host 的 `ToolDefinition.presentCall`、`ToolDefinition.presentResult`、`ToolCa | 保留 | Session 日志格式、Remote journal 生命周期与 Conversation identity/topology | | 保留 | 现有 keyed slot、Generic fallback、Chat、Details 与 Trajectory 结构 | | 禁止 | 新 Client presenter service、平行 registry 或 wire renderer id | -| 禁止 | 新卡片、视觉改版、交互改版或 Code Dispatch rich-card 增强,[嵌套 terminal 卡片例外](../bug-fix/2026-09-05-nested-terminal-cards.zh.md)除外 | +| 禁止 | 新卡片、视觉改版、交互改版或 Code Dispatch rich-card 增强,[嵌套 terminal 卡片例外](../bug-fix/2026-09-05-nested-terminal-cards.zh.md)和[紧凑工具详情](2026-09-10-compact-tool-details.zh.md)除外 | | 禁止 | 为兼容保留双写、版本协商或旧 `view` 字段 | ## 术语 diff --git a/.agents/notes/implemented/architecture/2026-09-10-compact-tool-details.i18n.yaml b/.agents/notes/implemented/architecture/2026-09-10-compact-tool-details.i18n.yaml new file mode 100644 index 0000000000..74691e5932 --- /dev/null +++ b/.agents/notes/implemented/architecture/2026-09-10-compact-tool-details.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/architecture/2026-09-10-compact-tool-details.md +2026-09-10-compact-tool-details.md: ca8f4721f4a5f121c98265c304ba6a30bb42596f +2026-09-10-compact-tool-details.zh.md: e62ed43f66af7d06898808733f556b9b42abb0e8 diff --git a/.agents/notes/implemented/architecture/2026-09-10-compact-tool-details.md b/.agents/notes/implemented/architecture/2026-09-10-compact-tool-details.md new file mode 100644 index 0000000000..ca8f4721f4 --- /dev/null +++ b/.agents/notes/implemented/architecture/2026-09-10-compact-tool-details.md @@ -0,0 +1,27 @@ +# Agent Note: Compact Tool Details from Recorded Results + +Status: implemented + +English | [中文](2026-09-10-compact-tool-details.zh.md) + +## Problem + +Goal, todo, and schedule results contain a small set of user-facing facts that take more effort to read as JSON. Dedicated expanded bodies need to fit the existing conversation rows without adding a second presentation registry or live state to historical calls. + +## Decision + +The `ui-tool` package registers goal and schedule toolviews through the existing keyed slot and shares one package-internal fields/list component with the todo view. Successful, recognized results display recorded values; incomplete calls, errors, malformed values, and unsupported result formats retain generic input/output. Schedule domain errors, including uncertain persistence and an unknown deletion target, retain their original result. + +Tool rows keep their disclosure and Inspect interaction. Checklist marks are static and have accessible status labels. Schedule times use absolute dates with the viewer's time zone; states come from the recorded result. Goal details distinguish an active goal awaiting continuation from an armed goal. These display values do not subscribe to the current goal, todo, or schedule projection. + +This scoped visual change is an exception to the visual-equivalence requirement in [Client-derived tool presentation](2026-08-23-client-derived-tool-presentation.md). Its raw-event ownership, generic fallback, and single keyed registry remain authoritative. + +## Alternatives considered + +**A shared presenter framework:** the existing keyed slot already dispatches tools. Reusing only the detail body keeps the abstraction local until another package needs it. + +**A dashboard inside each card:** interactive controls, progress graphics, and live states add visual weight and can misrepresent what a historical call returned. Compact read-only fields and lists preserve the conversation's hierarchy. + +## Consequences + +Seven tool names gain readable details without changing Host tools, Session events, or public Client exports. The adapters own recognition of their recorded result formats; unsupported data remains inspectable as raw text. The authored tool-details Session replay covers their assembled rendering. diff --git a/.agents/notes/implemented/architecture/2026-09-10-compact-tool-details.zh.md b/.agents/notes/implemented/architecture/2026-09-10-compact-tool-details.zh.md new file mode 100644 index 0000000000..e62ed43f66 --- /dev/null +++ b/.agents/notes/implemented/architecture/2026-09-10-compact-tool-details.zh.md @@ -0,0 +1,27 @@ +# Agent Note: Compact Tool Details from Recorded Results + +Status: implemented + +[English](2026-09-10-compact-tool-details.md) | 中文 + +## 问题 + +目标、任务清单和定时任务的结果包含少量面向用户的事实,以 JSON 阅读时较费力。专用展开内容需要融入现有对话行,不能为历史调用新增一套呈现注册表或实时状态。 + +## 决策 + +`ui-tool` 包通过现有按键分发的插槽注册目标和定时任务视图,并与任务清单视图共用一个包内字段/列表组件。成功且可识别的结果显示记录值;未完成调用、错误、格式异常和不支持的结果格式保留通用输入/输出。定时任务领域错误(包括持久化不确定和删除目标不存在)保留原始结果。 + +工具行保留展开和 Inspect 交互。任务清单标记是静态的,并带有可访问的状态标签。定时任务使用查看者时区的绝对日期,状态取自记录的结果。目标详情区分等待继续的活动目标和已启动的目标。这些显示值不订阅当前目标、任务清单或定时任务投影。 + +这项有限的视觉调整是[客户端派生工具呈现](2026-08-23-client-derived-tool-presentation.zh.md)中视觉等价要求的例外。该决策中的原始事件所有权、通用回退和唯一按键注册表仍然有效。 + +## 考虑过的替代方案 + +**共享呈现框架:**现有按键插槽已负责工具分发。仅复用详情内容,可以在其他包出现需求前将抽象限制在包内。 + +**在每张卡片中嵌入仪表盘:**交互控件、进度图形和实时状态会增加视觉负担,也可能歪曲历史调用返回的事实。紧凑的只读字段和列表保留了对话的层级。 + +## 影响 + +七个工具名称获得可读详情,无需修改 Host 工具、Session 事件或公共 Client 导出。适配器负责识别各自记录的结果格式;不支持的数据仍可作为原始文本查看。手工编写的 tool-details Session 回放覆盖它们在完整页面中的呈现。 diff --git a/apps/web/tests/tool-details.e2e.ts b/apps/web/tests/tool-details.e2e.ts new file mode 100644 index 0000000000..a5919daeed --- /dev/null +++ b/apps/web/tests/tool-details.e2e.ts @@ -0,0 +1,83 @@ +/** Authored Session rendering of compact recorded Tool results. */ +import { readFile } from 'node:fs/promises' +import { fileURLToPath } from 'node:url' +import type { Browser, Page } from 'playwright' +import { chromium } from 'playwright' +import { afterAll, beforeAll, describe, expect, it, onTestFailed } from 'vitest' +import { + assertFixtureInventory, captureStableAria, compareOrRefreshGolden, fixtureUserPrompts, + launchWebScaffold, seedSession, watchConsole, webSnapshotMode, type WebScaffold, +} from './scaffold.ts' +import { expandOwningTurnProcess, newEnglishPage, saveFailureShot } from './support.ts' + +const SNAPSHOT_DIR = fileURLToPath(new URL('../../../snapshots/web/tool-details', import.meta.url)) +const FIXTURE = fileURLToPath(new URL('../../../snapshots/web/tool-details/session.v3.jsonl', import.meta.url)) +const UI_EXPECTED = fileURLToPath(new URL('../../../snapshots/web/tool-details/ui.expected.md', import.meta.url)) +const MODE = webSnapshotMode() +const SEED_ID = 'tool-details-web-e2e' +const PROMPT = 'Show a goal, a to-do list, and reminders.' + +describe.skipIf(MODE === 'record')('web e2e: compact Tool details', () => { + let scaffold: WebScaffold + let browser: Browser + let page: Page + let tripwire: ReturnType + + beforeAll(async () => { + const fixture = await readFile(FIXTURE, 'utf8') + expect(fixtureUserPrompts(fixture)).toEqual([PROMPT]) + scaffold = await launchWebScaffold({}) + await seedSession(scaffold, fixture, SEED_ID) + browser = await chromium.launch() + page = await newEnglishPage(browser) + tripwire = watchConsole(page) + await page.goto(scaffold.authenticatedUrl, { waitUntil: 'load' }) + await page.waitForSelector('[class*="frame"]', { timeout: 30_000 }) + + const groupRow = page.locator('[role="treeitem"]').first() + await groupRow.waitFor({ timeout: 15_000 }) + await groupRow.click() + const sessionRow = page.locator('[role="treeitem"]').nth(1) + await sessionRow.waitFor({ timeout: 10_000 }) + await sessionRow.click() + const skillRow = page.locator('[data-tool="create_goal"]') + await expandOwningTurnProcess(page, skillRow) + await skillRow.waitFor({ timeout: 15_000 }) + }, 120_000) + + afterAll(async () => { + try { await browser?.close() } finally { await scaffold?.close() } + }) + + it('expands every recorded tool through its keyed renderer', async () => { + onTestFailed(() => saveFailureShot(page, 'web-e2e-tool-details')) + const rows = page.locator('[data-tool]') + expect(await rows.count()).toBe(44) + for (const row of await rows.all()) { + await row.getByRole('button', { expanded: false }).first().click() + expect(await row.getByRole('listitem').count()).toBeGreaterThan(0) + expect(await row.getByText('Output', { exact: true }).count()).toBe(0) + expect(await row.getByRole('button', { name: 'Inspect', exact: true }).count()).toBe(1) + } + const changedTodos = page.locator('[data-tool="todo_write"]').nth(1) + await changedTodos.getByText('1 added · 1 updated · 1 removed', { exact: true }).waitFor() + for (const summary of await page.locator('[data-tool] details > summary').all()) await summary.click() + await page.locator('[data-tool="create_goal"]').getByText('Awaiting continuation', { exact: true }).waitFor() + const schema = page.locator('[data-tool="cordis_inspect_list"] pre').first() + await expect.poll(() => schema.textContent()).toContain('"properties"') + const snapshots = await Promise.all(Array.from({ length: 44 }, (_, index) => + captureStableAria(page, `[data-chat-call-id="details-call-${index + 1}"] [data-tool]`, scaffold.workspaceCwd))) + const snapshot = snapshots.join('\n') + await compareOrRefreshGolden(UI_EXPECTED, snapshot, MODE) + await page.setViewportSize({ width: 360, height: 800 }) + const card = page.locator('[data-tool="schedule_create"]') + expect(await card.evaluate(element => element.scrollWidth <= element.clientWidth)).toBe(true) + expect(tripwire.pageErrors).toEqual([]) + expect(tripwire.warnings).toEqual([]) + }, 60_000) + + it('keeps the snapshot inventory closed', async () => { + await assertFixtureInventory(SNAPSHOT_DIR, ['session.v3.jsonl', 'ui.expected.md']) + }) + +}) diff --git a/apps/web/tsconfig.json b/apps/web/tsconfig.json index 3b01e3990c..0ed6e455fb 100644 --- a/apps/web/tsconfig.json +++ b/apps/web/tsconfig.json @@ -106,6 +106,7 @@ "tests/background-job-list.e2e.ts", "tests/bash-abort-row.e2e.ts", "tests/skill-tool-row.e2e.ts", + "tests/tool-details.e2e.ts", "tests/turn-tail-actions.e2e.ts", "tests/goal-multi-turn-actions.e2e.ts", "tests/chat-scroll-fixture.ts", diff --git a/packages/client/ui-conversation/src/client/locales.ts b/packages/client/ui-conversation/src/client/locales.ts index 56da231c15..5deb21f04b 100644 --- a/packages/client/ui-conversation/src/client/locales.ts +++ b/packages/client/ui-conversation/src/client/locales.ts @@ -85,6 +85,192 @@ export const zh = { 'todo.progress.active': '{active} 进行中', 'todo.progress.pending': '{pending} 待处理', 'todo.rowTitle': '更新任务清单', + 'tool.title.createGoal': '创建目标', + 'tool.title.getGoal': '查看目标', + 'tool.title.updateGoal': '更新目标', + 'tool.title.createSchedule': '创建定时任务', + 'tool.title.listSchedules': '查看定时任务', + 'tool.title.deleteSchedule': '删除定时任务', + 'detail.state': '状态', + 'detail.todo.completed': '已完成', + 'detail.todo.in_progress': '进行中', + 'detail.todo.pending': '待处理', + 'detail.todo.empty': '任务清单为空', + 'todo.diff.initial': '首次记录', + 'todo.diff.compare': '与上次清单相比', + 'todo.diff.unavailable': '旧清单不可用', + 'todo.diff.noChanges': '清单没有变化', + 'todo.diff.added': '新增 {count}', + 'todo.diff.updated': '更新 {count}', + 'todo.diff.removed': '移除 {count}', + 'todo.diff.unchanged': '{count} 项未变化', + 'todo.diff.addedItem': '新增', + 'todo.diff.updatedItem': '状态变化', + 'todo.diff.movedItem': '顺序调整', + 'todo.diff.removedItem': '移除', + 'detail.goal.empty': '没有目标', + 'detail.goal.active': '进行中', + 'detail.goal.disarmed': '等待继续', + 'detail.goal.paused': '已暂停', + 'detail.goal.blocked': '受阻', + 'detail.goal.complete': '已完成', + 'detail.goal.rounds': '执行轮次', + 'detail.goal.reason': '受阻原因', + 'detail.days': '{count} 天', + 'detail.hours': '{count} 小时', + 'detail.minutes': '{count} 分钟', + 'detail.seconds': '{count} 秒', + 'detail.schedule.once': '单次', + 'detail.schedule.every': '每 {interval}', + 'detail.schedule.when': '计划时间', + 'detail.schedule.frequency': '重复', + 'detail.schedule.scheduled': '等待触发', + 'detail.schedule.overdue': '已到期,等待会话恢复', + 'detail.schedule.empty': '没有定时任务', + 'detail.schedule.deleted': '已删除', + 'detail.schedule.count': '{count} 个定时任务', + 'tool.title.inspectProviders': '检查提供方', + 'tool.title.queryRuntime': '查询运行时', + 'tool.title.inspectPlugins': '检查动态插件', + 'tool.title.workflow': '运行工作流', + 'tool.title.ralph': '运行 Ralph', + 'tool.title.readEvent': '读取事件', + 'tool.title.searchEvents': '搜索事件', + 'tool.title.traceEvent': '追踪事件', + 'tool.title.searchSessions': '搜索会话', + 'tool.title.traceSession': '追踪会话', + 'tool.title.listModels': '查看可用模型', + 'tool.title.subagent': '委派任务', + 'tool.title.listAgents': '查看代理', + 'tool.title.sendMessage': '发送消息', + 'tool.title.interruptAgent': '中断代理', + 'tool.title.listJobs': '查看后台任务', + 'tool.title.readJob': '读取任务输出', + 'tool.title.killJob': '取消后台任务', + 'tool.title.openTerminal': '创建终端', + 'tool.title.readTerminal': '读取终端', + 'tool.title.listTerminals': '查看终端', + 'tool.title.signalTerminal': '发送终端信号', + 'tool.title.closeTerminal': '关闭终端', + 'tool.title.lsp': '查询代码符号', + 'tool.title.findDefinition': '查找定义', + 'tool.title.findReferences': '查找引用', + 'tool.title.findImplementation': '查找实现', + 'tool.title.hoverSymbol': '查看符号信息', + 'tool.title.spawnTeammate': '创建队友', + 'tool.title.createTeamTask': '创建团队任务', + 'tool.title.getTeamTask': '读取团队任务', + 'tool.title.updateTeamTask': '更新团队任务', + 'tool.title.listTeamTasks': '查看团队任务', + 'tool.title.waitAgent': '等待队友', + 'detail.recordedResult': '调用结果', + 'detail.empty': '暂无结果', + 'detail.none': '无', + 'detail.yes': '是', + 'detail.no': '否', + 'detail.moreInInspect': '另有 {count} 项,可在「查看」中读取', + 'detail.status.running': '运行中', + 'detail.status.idle': '空闲', + 'detail.status.ready': '就绪', + 'detail.status.inactive': '未运行', + 'detail.status.provisioning': '准备中', + 'detail.status.failed': '失败', + 'detail.status.completed': '已完成', + 'detail.status.deleted': '已删除', + 'detail.status.killed': '已取消', + 'detail.status.accepted': '已接收', + 'detail.status.queued': '已入队', + 'detail.status.exited': '已退出', + 'detail.field.id': 'ID', + 'detail.field.revision': '版本', + 'detail.field.platform': '平台', + 'detail.field.provider': '提供方', + 'detail.field.model': '模型', + 'detail.field.role': '角色', + 'detail.field.context': '上下文', + 'detail.field.owner': '负责人', + 'detail.field.ready': '可开始', + 'detail.field.dependencies': '前置任务', + 'detail.field.writeScopes': '文件范围', + 'detail.field.warnings': '提示', + 'detail.field.diagnostics': '诊断', + 'detail.field.methods': '方法', + 'detail.field.inputSchema': '输入 Schema', + 'detail.field.outputSchema': '输出 Schema', + 'detail.field.currentPackage': '当前包', + 'detail.field.nextPackage': '待运行的包', + 'detail.field.latestRun': '最近运行', + 'detail.field.packages': '版本包', + 'detail.field.registrations': '注册项', + 'detail.field.props': '属性', + 'detail.field.data': '数据', + 'detail.field.source': '来源', + 'detail.field.content': '内容', + 'detail.field.message': '消息', + 'detail.field.messageId': '消息 ID', + 'detail.field.root': '根节点', + 'detail.field.pid': '进程 ID', + 'detail.field.type': '类型', + 'detail.field.time': '时间', + 'detail.field.seq': '事件序号', + 'detail.field.turn': '轮次', + 'detail.field.step': '步骤', + 'detail.field.callId': '调用 ID', + 'detail.field.agents': '启动代理数', + 'detail.field.result': '结果', + 'detail.field.parent': '父级', + 'detail.field.depth': '层级', + 'detail.field.exitCode': '退出码', + 'detail.field.signal': '信号', + 'detail.field.previousStatus': '中断前状态', + 'detail.field.agent': '代理 ID', + 'detail.field.job': '任务 ID', + 'detail.field.task': '任务内容', + 'detail.field.processGroup': '进程组', + 'detail.field.availability': '可用状态', + 'detail.field.bestMatch': '最相关事件', + 'detail.field.target': '目标事件', + 'detail.field.surface': '记录状态', + 'detail.agents.count': '{count} 个代理', + 'detail.jobs.count': '{count} 个后台任务', + 'detail.terminals.count': '{count} 个终端', + 'detail.tasks.count': '{count} 个团队任务', + 'detail.tasks.nextPage': '后面还有任务;下一页位置为 {cursor}', + 'detail.locations.count': '{count} 个位置', + 'detail.location': '第 {line} 行,第 {column} 列', + 'detail.receipt.delivered': '消息已送达', + 'detail.receipt.interrupt': '已请求中断', + 'detail.receipt.started': '已启动', + 'detail.receipt.cancel': '已请求取消', + 'detail.receipt.alreadyFinished': '任务已结束', + 'detail.receipt.signal': '信号已发送', + 'detail.receipt.closed': '已关闭', + 'detail.receipt.closing': '关闭中', + 'detail.wait.noProgress': '没有正在运行的队友', + 'detail.wait.title': '队友状态', + 'detail.wait.timeout': '等待超时', + 'detail.wait.changed': '检测到变化', + 'detail.agent.reply': '代理回复', + 'detail.models.title': '可用模型', + 'detail.output.lines': '第 {begin}–{end} 行,共 {total} 行', + 'detail.output.truncated': '输出已截断', + 'detail.providers.count': '{count} 个检查提供方', + 'detail.plugins.count': '{count} 个动态插件', + 'detail.workflow.script': '工作流脚本', + 'detail.ralph.reportedComplete': '代理报告完成', + 'detail.ralph.reportedBlocker': '代理报告受阻', + 'detail.ralph.limit': '已达到轮次上限', + 'detail.report.nextSteps': '待完成事项', + 'detail.trace.replacedBy': '被替换为', + 'detail.trace.replacementChain': '替换链', + 'detail.trace.replaces': '替换的事件', + 'detail.trace.sources': '引用来源', + 'detail.trace.derived': '派生事件', + 'detail.trace.ancestors': '祖先会话', + 'detail.trace.descendants': '后代会话', + 'detail.matches.count': '{count} 条匹配', + 'detail.matches.capped': '已达到结果上限,可缩小搜索范围', + 'detail.event.neighbors': '前后事件', 'todo.completed': '{done}/{total} 已完成', 'command.attachmentsUnsupported': '/{command} 不接受附件,请先移除附件', 'ask.rowTitle': '提问', @@ -251,6 +437,192 @@ export const en = { 'todo.progress.active': '{active} in progress', 'todo.progress.pending': '{pending} pending', 'todo.rowTitle': 'Update to-do list', + 'tool.title.createGoal': 'Create goal', + 'tool.title.getGoal': 'View goal', + 'tool.title.updateGoal': 'Update goal', + 'tool.title.createSchedule': 'Create reminder', + 'tool.title.listSchedules': 'List reminders', + 'tool.title.deleteSchedule': 'Delete reminder', + 'detail.state': 'Status', + 'detail.todo.completed': 'Completed', + 'detail.todo.in_progress': 'In progress', + 'detail.todo.pending': 'Pending', + 'detail.todo.empty': 'The to-do list is empty', + 'todo.diff.initial': 'Initial list', + 'todo.diff.compare': 'Changes since the previous list', + 'todo.diff.unavailable': 'Previous list unavailable', + 'todo.diff.noChanges': 'No changes to the list', + 'todo.diff.added': '{count} added', + 'todo.diff.updated': '{count} updated', + 'todo.diff.removed': '{count} removed', + 'todo.diff.unchanged': '{count} unchanged', + 'todo.diff.addedItem': 'Added', + 'todo.diff.updatedItem': 'Status changed', + 'todo.diff.movedItem': 'Reordered', + 'todo.diff.removedItem': 'Removed', + 'detail.goal.empty': 'No goal', + 'detail.goal.active': 'Active', + 'detail.goal.disarmed': 'Awaiting continuation', + 'detail.goal.paused': 'Paused', + 'detail.goal.blocked': 'Blocked', + 'detail.goal.complete': 'Completed', + 'detail.goal.rounds': 'Rounds', + 'detail.goal.reason': 'Blocker', + 'detail.days': '{count} d', + 'detail.hours': '{count} h', + 'detail.minutes': '{count} min', + 'detail.seconds': '{count} s', + 'detail.schedule.once': 'Once', + 'detail.schedule.every': 'Every {interval}', + 'detail.schedule.when': 'Scheduled for', + 'detail.schedule.frequency': 'Repeat', + 'detail.schedule.scheduled': 'Scheduled', + 'detail.schedule.overdue': 'Overdue, awaiting session resume', + 'detail.schedule.empty': 'No reminders', + 'detail.schedule.deleted': 'Deleted', + 'detail.schedule.count': '{count} reminders', + 'tool.title.inspectProviders': 'Inspect providers', + 'tool.title.queryRuntime': 'Query runtime', + 'tool.title.inspectPlugins': 'Inspect plugins', + 'tool.title.workflow': 'Run workflow', + 'tool.title.ralph': 'Run Ralph', + 'tool.title.readEvent': 'Read event', + 'tool.title.searchEvents': 'Search events', + 'tool.title.traceEvent': 'Trace event', + 'tool.title.searchSessions': 'Search sessions', + 'tool.title.traceSession': 'Trace session', + 'tool.title.listModels': 'List models', + 'tool.title.subagent': 'Delegate task', + 'tool.title.listAgents': 'List agents', + 'tool.title.sendMessage': 'Send message', + 'tool.title.interruptAgent': 'Interrupt agent', + 'tool.title.listJobs': 'List background jobs', + 'tool.title.readJob': 'Read job output', + 'tool.title.killJob': 'Cancel background job', + 'tool.title.openTerminal': 'Open terminal', + 'tool.title.readTerminal': 'Read terminal', + 'tool.title.listTerminals': 'List terminals', + 'tool.title.signalTerminal': 'Signal terminal', + 'tool.title.closeTerminal': 'Close terminal', + 'tool.title.lsp': 'Query code symbols', + 'tool.title.findDefinition': 'Find definition', + 'tool.title.findReferences': 'Find references', + 'tool.title.findImplementation': 'Find implementation', + 'tool.title.hoverSymbol': 'Inspect symbol', + 'tool.title.spawnTeammate': 'Create teammate', + 'tool.title.createTeamTask': 'Create team task', + 'tool.title.getTeamTask': 'Read team task', + 'tool.title.updateTeamTask': 'Update team task', + 'tool.title.listTeamTasks': 'List team tasks', + 'tool.title.waitAgent': 'Wait for teammates', + 'detail.recordedResult': 'Recorded result', + 'detail.empty': 'No results', + 'detail.none': 'None', + 'detail.yes': 'Yes', + 'detail.no': 'No', + 'detail.moreInInspect': '{count} more items available in Inspect', + 'detail.status.running': 'Running', + 'detail.status.idle': 'Idle', + 'detail.status.ready': 'Ready', + 'detail.status.inactive': 'Inactive', + 'detail.status.provisioning': 'Provisioning', + 'detail.status.failed': 'Failed', + 'detail.status.completed': 'Completed', + 'detail.status.deleted': 'Deleted', + 'detail.status.killed': 'Cancelled', + 'detail.status.accepted': 'Accepted', + 'detail.status.queued': 'Queued', + 'detail.status.exited': 'Exited', + 'detail.field.id': 'ID', + 'detail.field.revision': 'Revision', + 'detail.field.platform': 'Platform', + 'detail.field.provider': 'Provider', + 'detail.field.model': 'Model', + 'detail.field.role': 'Role', + 'detail.field.context': 'Context', + 'detail.field.owner': 'Owner', + 'detail.field.ready': 'Ready', + 'detail.field.dependencies': 'Dependencies', + 'detail.field.writeScopes': 'Write scopes', + 'detail.field.warnings': 'Warnings', + 'detail.field.diagnostics': 'Diagnostics', + 'detail.field.methods': 'Methods', + 'detail.field.inputSchema': 'Input schema', + 'detail.field.outputSchema': 'Output schema', + 'detail.field.currentPackage': 'Current package', + 'detail.field.nextPackage': 'Next package', + 'detail.field.latestRun': 'Latest run', + 'detail.field.packages': 'Packages', + 'detail.field.registrations': 'Registrations', + 'detail.field.props': 'Props', + 'detail.field.data': 'Data', + 'detail.field.source': 'Source', + 'detail.field.content': 'Content', + 'detail.field.message': 'Message', + 'detail.field.messageId': 'Message ID', + 'detail.field.root': 'Root', + 'detail.field.pid': 'Process ID', + 'detail.field.type': 'Type', + 'detail.field.time': 'Time', + 'detail.field.seq': 'Event sequence', + 'detail.field.turn': 'Turn', + 'detail.field.step': 'Step', + 'detail.field.callId': 'Call ID', + 'detail.field.agents': 'Agents started', + 'detail.field.result': 'Result', + 'detail.field.parent': 'Parent', + 'detail.field.depth': 'Depth', + 'detail.field.exitCode': 'Exit code', + 'detail.field.signal': 'Signal', + 'detail.field.previousStatus': 'Previous status', + 'detail.field.agent': 'Agent ID', + 'detail.field.job': 'Job ID', + 'detail.field.task': 'Task', + 'detail.field.processGroup': 'Process group', + 'detail.field.availability': 'Availability', + 'detail.field.bestMatch': 'Best match', + 'detail.field.target': 'Target event', + 'detail.field.surface': 'Record status', + 'detail.agents.count': '{count} agents', + 'detail.jobs.count': '{count} background jobs', + 'detail.terminals.count': '{count} terminals', + 'detail.tasks.count': '{count} team tasks', + 'detail.tasks.nextPage': 'More tasks available; next cursor is {cursor}', + 'detail.locations.count': '{count} locations', + 'detail.location': 'Line {line}, column {column}', + 'detail.receipt.delivered': 'Message delivered', + 'detail.receipt.interrupt': 'Interrupt requested', + 'detail.receipt.started': 'Started', + 'detail.receipt.cancel': 'Cancellation requested', + 'detail.receipt.alreadyFinished': 'Already finished', + 'detail.receipt.signal': 'Signal delivered', + 'detail.receipt.closed': 'Closed', + 'detail.receipt.closing': 'Closing', + 'detail.wait.noProgress': 'No active teammates', + 'detail.wait.title': 'Teammate activity', + 'detail.wait.timeout': 'Wait timed out', + 'detail.wait.changed': 'Change detected', + 'detail.agent.reply': 'Agent response', + 'detail.models.title': 'Available models', + 'detail.output.lines': 'Lines {begin}–{end} of {total}', + 'detail.output.truncated': 'Output truncated', + 'detail.providers.count': '{count} inspect providers', + 'detail.plugins.count': '{count} dynamic plugins', + 'detail.workflow.script': 'Workflow script', + 'detail.ralph.reportedComplete': 'Worker reported completion', + 'detail.ralph.reportedBlocker': 'Worker reported a blocker', + 'detail.ralph.limit': 'Round limit reached', + 'detail.report.nextSteps': 'Remaining work', + 'detail.trace.replacedBy': 'Replaced by', + 'detail.trace.replacementChain': 'Replacement chain', + 'detail.trace.replaces': 'Replaced events', + 'detail.trace.sources': 'Source events', + 'detail.trace.derived': 'Derived events', + 'detail.trace.ancestors': 'Ancestor sessions', + 'detail.trace.descendants': 'Descendant sessions', + 'detail.matches.count': '{count} matches', + 'detail.matches.capped': 'Result limit reached; narrow the search for more', + 'detail.event.neighbors': 'Surrounding events', 'todo.completed': '{done}/{total} completed', 'command.attachmentsUnsupported': '/{command} does not accept attachments; remove them first', 'ask.rowTitle': 'Ask question', diff --git a/packages/client/ui-tool/README.i18n.yaml b/packages/client/ui-tool/README.i18n.yaml index ad0a426ba6..af11843422 100644 --- a/packages/client/ui-tool/README.i18n.yaml +++ b/packages/client/ui-tool/README.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 packages/client/ui-tool/README.md -README.md: b8dd2a5e3c6743e496d585a2de238714f00e9f6b -README.zh.md: 0f777981ff6ccc6945e516d3c9853a236cde82d7 +README.md: 011935cefc41542917eeb434e2bfed5ce00b0a44 +README.zh.md: a8270ed214b209162faabb3d9f16da1e7281d139 diff --git a/packages/client/ui-tool/README.md b/packages/client/ui-tool/README.md index b8dd2a5e3c..011935cefc 100644 --- a/packages/client/ui-tool/README.md +++ b/packages/client/ui-tool/README.md @@ -64,6 +64,8 @@ The package realizes one dispatch rule: atomic Tool views are keyed by wire Tool Every card is read in place in the call tree; there is no second, full-height presentation of a selected call. Row renderers share one pure card model for each terminal, read, diff, search, and web card, and the image card's gallery renders through the tool-owned `tool.call.images` slot. These models validate raw call arguments, result content, failure state, persisted metadata, Code Dispatch `parentCallId`, and Session path facts. Unsupported or malformed inputs use flattened Tool result text. A file-path summary opens the file through the owner's `openFile`, which the chat view routes to the right Sidebar's text preview; `inspect` opens the trajectory view. Card-specific limits and fallback rules for the terminal, diff, read, search, and web cards remain in [the ui-primitives README](../ui-primitives/README.md); the image card's model in this package carries its own fallback rules. +Goal and schedule tools use compact fields and lists; `todo_write` uses a static checklist. These expanded bodies read successful logged results, preserve generic input/output for failures or unsupported data, and keep Inspect available. Dates include the viewer's time zone, and statuses reflect the call result rather than current session state. [Compact tool details](../../../.agents/notes/implemented/architecture/2026-09-10-compact-tool-details.md) records the scope and presentation trade-offs. + The terminal model uses `hasSpillNotice` from the browser-safe `@deepseek-ai/dsh-spill-policy/notice` entry, not an independent UI pattern. The [spill-policy README](../../spill/spill-policy/README.md#shared-notice-ownership) owns notice formatting and recognition. This check conservatively selects generic output; matching text cannot authenticate its source, and replay leaves recorded result bytes untouched. diff --git a/packages/client/ui-tool/README.zh.md b/packages/client/ui-tool/README.zh.md index 0f777981ff..a8270ed214 100644 --- a/packages/client/ui-tool/README.zh.md +++ b/packages/client/ui-tool/README.zh.md @@ -64,6 +64,8 @@ owner 载荷为 `ToolCallOwnerProps`:`callId`、`toolName`、冻结的 `block` 每张卡片都直接在调用树中查看;选中调用后不会再显示第二个全高视图。行 renderer 为 terminal、read、diff、search 和 web 卡片各复用同一个纯 card model,image 卡片的图库经由工具自有 `tool.call.images` slot 渲染。这些 model 校验原始调用参数、结果内容、失败状态、持久 metadata、Code Dispatch 的 `parentCallId` 与会话路径信息。不受支持或格式错误的输入使用压平的工具结果文本。文件路径摘要经属主的 `openFile` 打开文件,chat 视图把它路由到右侧 Sidebar 的文本预览;`inspect` 打开轨迹视图。terminal、diff、read、search 与 web 卡片的上限与 fallback 规则仍由 [ui-primitives README](../ui-primitives/README.zh.md) 负责;image 卡片的 fallback 规则由本包内的 card model 自行承载。 +目标与定时任务工具使用紧凑的字段和列表;`todo_write` 使用静态任务清单。展开内容读取成功的记录结果,为失败或不支持的数据保留通用输入/输出,并保留 Inspect。日期包含查看者的时区,状态反映调用结果而非当前会话状态。[紧凑工具详情](../../../.agents/notes/implemented/architecture/2026-09-10-compact-tool-details.zh.md)记录了范围与呈现取舍。 + terminal model 使用浏览器安全入口 `@deepseek-ai/dsh-spill-policy/notice` 的 `hasSpillNotice`,而非独立的 UI 匹配规则。[spill-policy README](../../spill/spill-policy/README.zh.md#shared-notice-ownership) 负责提示文本的格式化与识别。该检查保守地选择通用输出;匹配的文本无法证明其来源,回放也不改变已记录的结果字节。 diff --git a/packages/client/ui-tool/src/client/apply.ts b/packages/client/ui-tool/src/client/apply.ts index 28bc6e51fe..e6a866d1b2 100644 --- a/packages/client/ui-tool/src/client/apply.ts +++ b/packages/client/ui-tool/src/client/apply.ts @@ -14,6 +14,7 @@ import { fileMutationToolview } from './tool/toolviews/file-mutation-row.tsx' import { readToolview } from './tool/toolviews/read-row.tsx' import { readImageToolview } from './tool/toolviews/read-image-row.tsx' import { searchToolview } from './tool/toolviews/search-row.tsx' +import { detailsToolview } from './tool/toolviews/details-row.tsx' import { todoToolview } from './tool/toolviews/todo-row.tsx' import { webToolview } from './tool/toolviews/web-row.tsx' @@ -47,5 +48,6 @@ export function apply(ctx: ClientContext): void { ctx.plugin(searchToolview) ctx.plugin(webToolview) ctx.plugin(todoToolview) + ctx.plugin(detailsToolview) ctx.plugin(askQuestionToolview) } diff --git a/packages/client/ui-tool/src/client/tool/components/ToolDetails.module.css b/packages/client/ui-tool/src/client/tool/components/ToolDetails.module.css new file mode 100644 index 0000000000..e600b8a36c --- /dev/null +++ b/packages/client/ui-tool/src/client/tool/components/ToolDetails.module.css @@ -0,0 +1,257 @@ +.root { + margin: 4px 0 4px 4px; + border: 0.5px solid var(--dsw-alias-border-l1); + border-radius: 12px; + background: var(--dsw-alias-markdown-code-block); + color: var(--dsw-alias-label-secondary); + font: var(--dsw-font-xs-13); + overflow: hidden; +} + +.list { + list-style: none; + margin: 0; + padding: 0; + max-height: 320px; + overflow-y: auto; +} + +.item, +.empty { + margin: 0; + padding: 10px 14px; +} + +.item + .item { + border-top: 0.5px solid var(--dsw-alias-border-l2); +} + +.root[data-inspect]:not([data-caption]) > .list > .item:first-child, +.root[data-inspect]:not([data-caption]) .empty, +.root[data-inspect] .caption { + padding-inline-end: 88px; +} + +.caption { + padding: 10px 14px 6px; + color: var(--dsw-alias-label-caption); + font-size: 12px; +} + +.heading { + display: flex; + align-items: baseline; + gap: 8px; + flex-wrap: wrap; +} + +.text { + flex: 1; + min-width: 0; + white-space: pre-wrap; + overflow-wrap: anywhere; +} + +.status { + flex: none; + align-self: flex-start; + display: inline-flex; + align-items: center; + justify-content: center; + width: 14px; + height: 18px; + font-size: 16px; + line-height: 18px; +} + +.pending { + box-sizing: border-box; + width: 10px; + height: 10px; + border: 1px solid var(--dsw-alias-label-tertiary); + border-radius: 2px; +} + +.heading:has(.statusText) .text { + flex-basis: 12em; +} + +.statusText { + margin-inline-start: auto; + color: var(--dsw-alias-label-caption); + font-size: 12px; + white-space: nowrap; +} + +.previous, +.unchanged { + color: var(--dsw-alias-label-tertiary); +} + +.item[data-change='added'] .status { + color: var(--dsw-alias-state-success-primary); +} + +.item[data-change='removed'] .status { + color: var(--dsw-alias-state-error-primary); +} + +.item[data-change='removed'] .text { + color: var(--dsw-alias-label-tertiary); + text-decoration: line-through; +} + +.unchanged { + border-top: 0.5px solid var(--dsw-alias-border-l2); +} + +.unchanged summary { + display: flex; + align-items: center; + gap: 6px; + padding: 9px 14px; + list-style: none; + cursor: pointer; +} + +.unchanged summary::-webkit-details-marker { + display: none; +} + +.unchanged[open] summary svg { + transform: rotate(90deg); +} + +.fields { + margin: 0; +} + +.heading + .fields { + margin-top: 6px; +} + +.field { + display: grid; + grid-template-columns: 5.5em minmax(0, 1fr); + gap: 12px; +} + +.field + .field { + margin-top: 4px; +} + +.field dt { + color: var(--dsw-alias-label-caption); +} + +.field dd { + margin: 0; + white-space: pre-wrap; + overflow-wrap: anywhere; +} + +.badge { + flex: none; + display: inline-flex; + align-items: center; + gap: 5px; + color: var(--dsw-alias-label-caption); + font-size: 12px; +} + +.badge::before { + content: ''; + width: 5px; + height: 5px; + border-radius: 50%; + corner-shape: round; + background: currentColor; +} + +.badge[data-tone='success'] { color: var(--dsw-alias-state-success-primary); } +.badge[data-tone='warning'] { color: var(--dsw-alias-state-warn-primary); } +.badge[data-tone='error'] { color: var(--dsw-alias-state-error-primary); } + +.subtitle { + margin-top: 3px; + color: var(--dsw-alias-label-tertiary); + font-size: 12px; + overflow-wrap: anywhere; +} + +.description { + margin: 6px 0; + color: var(--dsw-alias-label-caption); + white-space: pre-wrap; + overflow-wrap: anywhere; +} + +.lines { + margin: 6px 0 0; + padding-inline-start: 18px; + white-space: pre-wrap; + overflow-wrap: anywhere; +} + +.lines li + li { margin-top: 4px; } + +.group { + margin-top: 8px; + border-top: 0.5px solid var(--dsw-alias-border-l2); +} + +.group > summary { + display: flex; + align-items: center; + gap: 6px; + padding: 8px 0 0; + color: var(--dsw-alias-label-caption); + list-style: none; + cursor: pointer; + overflow-wrap: anywhere; +} + +.group > summary::-webkit-details-marker { display: none; } +.group[open] > summary svg { transform: rotate(90deg); } +.group > summary svg { flex: none; } +.group .list { max-height: none; overflow: visible; } +.group .item { padding: 9px 0 2px 20px; } + +.prose, +.code { + max-width: 100%; + font-size: 13px; +} + +.prose { margin-top: 8px; } + +.root .item > .code { + margin: 8px 0 0; + max-height: 240px; + overflow: auto; +} + +.root .group .item:has(> .code:only-child) { + padding: 4px 0 0 20px; +} + +.root .group .item > .code:only-child { margin: 0; } + +.root .code [data-code-block-banner] { padding: 4px 10px; } +.root .code pre { padding: 6px 10px 8px; } + +.path { + flex: 1; + min-width: 0; + padding: 0; + border: none; + background: none; + color: inherit; + font: inherit; + text-align: start; + overflow-wrap: anywhere; + cursor: pointer; +} + +.path:hover { text-decoration: underline; } +.path:focus-visible { outline: 2px solid var(--dsw-alias-state-business-primary); outline-offset: 3px; } diff --git a/packages/client/ui-tool/src/client/tool/components/ToolDetails.tsx b/packages/client/ui-tool/src/client/tool/components/ToolDetails.tsx new file mode 100644 index 0000000000..3612d85a98 --- /dev/null +++ b/packages/client/ui-tool/src/client/tool/components/ToolDetails.tsx @@ -0,0 +1,134 @@ +/** Compact, read-only fields and lists for recorded Tool results. */ +import { CodeBlock, MarkdownText, IconCheckOutline14, IconChevronRightOutline14, IconPlayOutline16 } from '@deepseek-ai/dsh-client-ui-primitives' +import type { TranslateNS } from '@deepseek-ai/dsh-client-ui-slots' +import type { OpenFileOptions } from '@deepseek-ai/dsh-client-ui-chat/client' +import { markdownLabels } from '../models/primitive-labels.ts' +import css from './ToolDetails.module.css' + +/** One recorded entity, receipt, or collapsible group of related values. */ +export interface ToolDetailItem { + title?: string + subtitle?: string + description?: string + badge?: { label: string; tone: 'neutral' | 'success' | 'warning' | 'error' } + status?: { value: 'completed' | 'in_progress' | 'pending'; label: string } + previousStatus?: string + change?: { value: 'added' | 'removed' | 'updated'; label: string } + fields: readonly { label: string; value: string }[] + lines?: readonly string[] + markdown?: string + code?: { text: string; language?: string } + location?: { path: string; line?: number } + groups?: readonly { label: string; items: readonly ToolDetailItem[] }[] +} + +/** Localized display data shared by the built-in detail cards. */ +export interface ToolDetailsModel { + items: readonly ToolDetailItem[] + summary?: string + empty?: string + caption?: string + unchanged?: { label: string; items: ToolDetailsModel['items'] } +} + +interface DetailContentProps { + t: TranslateNS<'conversation'> + onOpenFile?: ((path: string, options?: OpenFileOptions) => void) | undefined +} + +function DetailItem({ item, t, onOpenFile }: { item: ToolDetailItem } & DetailContentProps) { + const status = item.status + return ( +
  • + {item.title !== undefined && ( +
    + {status !== undefined && ( + + {item.change?.value === 'added' ? '+' + : item.change?.value === 'removed' ? '−' + : status.value === 'completed' ? + : status.value === 'in_progress' ? + : } + + )} + {item.location !== undefined && onOpenFile !== undefined ? ( + + ) : {item.title}} + {item.badge !== undefined && {item.badge.label}} + {status !== undefined && ( + + {item.previousStatus !== undefined && ( + <>{item.previousStatus} → + )} + {status.label} + {item.change?.value === 'updated' && item.previousStatus === undefined && · {item.change.label}} + + )} +
    + )} + {item.subtitle !== undefined &&
    {item.subtitle}
    } + {item.description !== undefined &&

    {item.description}

    } + {item.fields.length > 0 && ( +
    + {item.fields.map(field => ( +
    +
    {field.label}
    +
    {field.value}
    +
    + ))} +
    + )} + {item.lines !== undefined &&
      {item.lines.map((line, index) =>
    • {line}
    • )}
    } + {item.markdown !== undefined &&
    } + {item.code !== undefined && ( + + )} + {item.groups?.map((group, index) => ( +
    + {group.label} +
      + {group.items.map((child, childIndex) => )} +
    +
    + ))} +
  • + ) +} + +/** + * Render recorded values with local disclosures, copy controls, and file navigation. + * @param props.model - Localized fields or list items; an empty list uses its empty label. + * @param props.hasInspect - Reserve space for the row's upper-right Inspect button. + * @param props.t - Conversation dictionary for shared Markdown and code controls. + * @param props.onOpenFile - Open a recorded file location in the session workspace. + * @returns The expanded detail body. + */ +export function ToolDetails({ model, hasInspect = false, t, onOpenFile }: { + model: ToolDetailsModel + hasInspect?: boolean +} & DetailContentProps) { + return ( +
    + {model.caption !== undefined &&
    {model.caption}
    } + {model.items.length === 0 ?

    {model.empty}

    : ( +
      + {model.items.map((item, index) => )} +
    + )} + {model.unchanged !== undefined && ( +
    + {model.unchanged.label} +
      + {model.unchanged.items.map((item, index) => )} +
    +
    + )} +
    + ) +} diff --git a/packages/client/ui-tool/src/client/tool/components/ToolRow.module.css b/packages/client/ui-tool/src/client/tool/components/ToolRow.module.css index f1933dc068..ef1a9f6bbf 100644 --- a/packages/client/ui-tool/src/client/tool/components/ToolRow.module.css +++ b/packages/client/ui-tool/src/client/tool/components/ToolRow.module.css @@ -191,6 +191,17 @@ opacity: 1; } +.detailsBodyWrap { + position: relative; +} + +.detailsBodyWrap .inspectButton { + position: absolute; + top: 12px; + right: 12px; + margin: 0; +} + /* Solid hover fill (a translucent token would let content bleed through). */ .inspectButton:hover { background: var(--dsw-alias-interactive-bg-hover-solid); diff --git a/packages/client/ui-tool/src/client/tool/components/ToolRow.tsx b/packages/client/ui-tool/src/client/tool/components/ToolRow.tsx index ca5becec3b..9a67b36ff6 100644 --- a/packages/client/ui-tool/src/client/tool/components/ToolRow.tsx +++ b/packages/client/ui-tool/src/client/tool/components/ToolRow.tsx @@ -23,6 +23,7 @@ import { } from '../models/tool-call-model.ts' import type { WebCardModelProps } from '../models/web-card-model.ts' import { AskQuestionCard } from './AskQuestionCard.tsx' +import { ToolDetails, type ToolDetailsModel } from './ToolDetails.tsx' import css from './ToolRow.module.css' export interface ToolRowProps { @@ -70,6 +71,8 @@ export interface ToolRowProps { loadImage?: MessageImageLoader | undefined search?: SearchCardModel | null | undefined web?: WebCardModelProps | null | undefined + /** Read-only fields/list card derived from a successful recorded result. */ + details?: ToolDetailsModel | null | undefined state: ToolRowState /** * Filesystem path from tool args; when set with onOpenFile, the summary @@ -128,6 +131,7 @@ export function ToolRow({ loadImage, search, web, + details, state, filePath, filePathLine, @@ -151,8 +155,9 @@ export function ToolRow({ const searchBody = search ?? null const webBody = web ?? null const askQuestionBody = askQuestion ?? null + const detailsBody = details ?? null const outputText = output ?? null - const card = askQuestionBody ?? terminalBody ?? diffBody ?? readBody ?? imageBody ?? searchBody ?? webBody + const card = askQuestionBody ?? terminalBody ?? diffBody ?? readBody ?? imageBody ?? searchBody ?? webBody ?? detailsBody const expandable = bodyRaw != null || outputText !== null || card !== null const open = expanded && expandable const bodyText = useMemo( @@ -234,7 +239,7 @@ export function ToolRow({ )} > -
    +
    {askQuestionBody !== null ? : terminalBody !== null @@ -288,36 +293,38 @@ export function ToolRow({ ) : webBody !== null ? - : ( - <> - {variant === 'code' && bodyText !== null && ( -
    - -
    - )} - {(cardBody !== null || outputText !== null) && ( -
    - {cardBody !== null && ( -
    - {t('row.input')} - {cardBody} -
    - )} - {cardBody !== null && outputText !== null && ( - - )} - {outputText !== null && ( -
    - {t('row.output')} - - {outputText} - -
    - )} -
    - )} - - )} + : detailsBody !== null + ? + : ( + <> + {variant === 'code' && bodyText !== null && ( +
    + +
    + )} + {(cardBody !== null || outputText !== null) && ( +
    + {cardBody !== null && ( +
    + {t('row.input')} + {cardBody} +
    + )} + {cardBody !== null && outputText !== null && ( + + )} + {outputText !== null && ( +
    + {t('row.output')} + + {outputText} + +
    + )} +
    + )} + + )} {inspect !== undefined && (
    + ) })}
    {preview !== undefined && previewPosition !== undefined && ( -