From 67c753dcb79ad6a0896b2b0748f24b7fde6de265 Mon Sep 17 00:00:00 2001 From: imccyu <276526105+imccyu@users.noreply.github.com> Date: Sat, 8 Aug 2026 15:29:26 +0800 Subject: [PATCH] fix(client): restore built Tool presentation snapshots --- apps/web/tests/assembled-boot.ts | 3 +- apps/web/tests/built-boot.snapshot.ts | 2 +- .../search-card/grep-card.expected.txt | 24 ++-- packages/client/runtime/src/client/index.ts | 1 + .../src/client/workspaces/path.ts} | 10 +- .../client/ui-conversation/README.i18n.yaml | 4 +- packages/client/ui-conversation/README.md | 2 +- packages/client/ui-conversation/README.zh.md | 2 +- .../ui-conversation/src/client/apply.ts | 5 +- .../src/client/chat/ContextInjectionRow.tsx | 3 +- .../src/client/chat/DisclosureRow.module.css | 69 ------------ .../src/client/chat/GenericCommandCard.tsx | 3 +- .../src/client/chat/ReasoningRow.tsx | 3 +- .../client/ui-primitives/README.i18n.yaml | 4 +- packages/client/ui-primitives/README.md | 2 +- packages/client/ui-primitives/README.zh.md | 2 +- .../src}/DisclosureRow.module.css | 2 +- .../src}/DisclosureRow.tsx | 4 +- packages/client/ui-primitives/src/index.ts | 2 + .../client/tool/components/DisclosureRow.tsx | 104 ------------------ .../src/client/tool/components/ToolRow.tsx | 3 +- .../client/tool/models/terminal-card-model.ts | 5 +- .../src/client/tool/models/tool-call-model.ts | 15 --- .../client/ui-tool/tests/tool-row.spec.tsx | 13 ++- 24 files changed, 49 insertions(+), 238 deletions(-) rename packages/client/{ui-conversation/src/client/contract/tool-path.ts => runtime/src/client/workspaces/path.ts} (50%) delete mode 100644 packages/client/ui-conversation/src/client/chat/DisclosureRow.module.css rename packages/client/{ui-tool/src/client/tool/components => ui-primitives/src}/DisclosureRow.module.css (92%) rename packages/client/{ui-conversation/src/client/chat => ui-primitives/src}/DisclosureRow.tsx (95%) delete mode 100644 packages/client/ui-tool/src/client/tool/components/DisclosureRow.tsx diff --git a/apps/web/tests/assembled-boot.ts b/apps/web/tests/assembled-boot.ts index 53f976af09..631196c652 100644 --- a/apps/web/tests/assembled-boot.ts +++ b/apps/web/tests/assembled-boot.ts @@ -26,6 +26,7 @@ const PLUGINS: readonly (WebBootEntry & { bundlePath: string })[] = [ { id: '@deepseek-ai/dsh-client-ui-layout', bundlePath: 'packages/client/ui-layout/lib/client.js', url: '/plugins/ui-layout.js', rev: 'fx', inject: ['@deepseek-ai/dsh-client-runtime'] }, { id: '@deepseek-ai/dsh-client-ui-sidebar', bundlePath: 'packages/client/ui-sidebar/lib/client.js', url: '/plugins/ui-sidebar.js', rev: 'fx', inject: ['@deepseek-ai/dsh-client-ui-layout'] }, { id: '@deepseek-ai/dsh-client-ui-conversation', bundlePath: 'packages/client/ui-conversation/lib/client.js', url: '/plugins/ui-conversation.js', rev: 'fx', inject: ['@deepseek-ai/dsh-client-ui-layout'] }, + { id: '@deepseek-ai/dsh-client-ui-tool', bundlePath: 'packages/client/ui-tool/lib/client.js', url: '/plugins/ui-tool.js', rev: 'fx', inject: ['@deepseek-ai/dsh-client-runtime', '@deepseek-ai/dsh-client-locale', '@deepseek-ai/dsh-client-ui-conversation'] }, { id: '@deepseek-ai/dsh-client-ui-workspace', bundlePath: 'packages/client/ui-workspace/lib/client.js', @@ -118,7 +119,7 @@ export function mountAssembledApp(): void { * Match a CSS-module class by its logical name. * Module class names carry a per-build hash in one of two schemes — * ui-primitives emits `__` (name bounded by underscores), - * ui-conversation emits `_` (name at the end) — and a longer name + * feature bundles emit `_` (name at the end) — and a longer name * containing this one must not match (`line` must not hit `lineNumber`). * @param el - element whose class list is inspected. * @param name - logical (unhashed) module class name. diff --git a/apps/web/tests/built-boot.snapshot.ts b/apps/web/tests/built-boot.snapshot.ts index 3d1536e923..80661a0b13 100644 --- a/apps/web/tests/built-boot.snapshot.ts +++ b/apps/web/tests/built-boot.snapshot.ts @@ -95,7 +95,7 @@ it('boots the built plugin graph and renders a fixture session end to end', asyn // Every bundle injected its plugin-owned style tag (the loader's CSS path). const styleOwners = [...document.head.querySelectorAll('style[data-plugin]')] .map(style => style.getAttribute('data-plugin')) - for (const plugin of ['@deepseek-ai/dsh-client-ui-layout', '@deepseek-ai/dsh-client-ui-sidebar', '@deepseek-ai/dsh-client-ui-conversation']) { + for (const plugin of ['@deepseek-ai/dsh-client-ui-layout', '@deepseek-ai/dsh-client-ui-sidebar', '@deepseek-ai/dsh-client-ui-conversation', '@deepseek-ai/dsh-client-ui-tool']) { expect(styleOwners).toContain(plugin) } }) diff --git a/apps/web/tests/snapshots/search-card/grep-card.expected.txt b/apps/web/tests/snapshots/search-card/grep-card.expected.txt index fbb20677df..160251e62e 100644 --- a/apps/web/tests/snapshots/search-card/grep-card.expected.txt +++ b/apps/web/tests/snapshots/search-card/grep-card.expected.txt @@ -1,13 +1,13 @@ kind=matches summary=显示 9 / 共 42 处匹配 · 3 个文件 file=packages/client/ui-primitives/src/SearchBlock.tsx3 -file=packages/client/ui-conversation/src/client/toolviews/search-row.tsx4 +file=packages/client/ui-tool/src/client/tool/toolviews/search-row.tsx4 line=16: export const DEFAULT_SEARCH_MAX_LINES = 16 line=138: export function SearchBlock(props: SearchBlockProps) { line=141: const [collapsed, setCollapsed] = useState>(() => new Set()) -line=35: const search = searchCardModel(block) -line=52: search={search} -line=78: yield ctx.slots.register({ name: 'conversation.chat.toolview', key: 'grep', locale: NS }, SearchRow) +line=36: const search = searchCardModel(block) +line=56: search={search} +line=78: yield ctx.slots.register({ name: 'tool.call.toolview', key: 'grep', locale: NS }, SearchRow) expand=… 其余 4 行 recovery=Found 9 of 42 matches @@ -15,13 +15,13 @@ packages/client/ui-primitives/src/SearchBlock.tsx Line 16: export const DEFAULT_SEARCH_MAX_LINES = 16 Line 138: export function SearchBlock(props: SearchBlockProps) { Line 141: const [collapsed, setCollapsed] = useState>(() => new Set()) -packages/client/ui-conversation/src/client/contract/search-card-model.ts -Line 24: export const CHAT_SEARCH_MAX_LINES = 8 -Line 60: export function searchCardModel(block: ToolCallBlock): SearchCardModel | null { -packages/client/ui-conversation/src/client/toolviews/search-row.tsx -Line 33: export function SearchRow({ toolName, block, inspect, t }: SearchRowProps) { -Line 35: const search = searchCardModel(block) -Line 52: search={search} -Line 78: yield ctx.slots.register({ name: 'conversation.chat.toolview', key: 'grep', locale: NS }, SearchRow) +packages/client/ui-tool/src/client/tool/models/search-card-model.ts +Line 45: export const CHAT_SEARCH_MAX_LINES = 8 +Line 130: export function searchCardModel(block: ToolCallBlock): SearchCardModel | null { +packages/client/ui-tool/src/client/tool/toolviews/search-row.tsx +Line 34: export function SearchRow({ toolName, block, inspect, t }: SearchRowProps) { +Line 36: const search = searchCardModel(block) +Line 56: search={search} +Line 78: yield ctx.slots.register({ name: 'tool.call.toolview', key: 'grep', locale: NS }, SearchRow) (Full grep result stored at: fixture://spill/grep-66. Read it to see every match.) \ No newline at end of file diff --git a/packages/client/runtime/src/client/index.ts b/packages/client/runtime/src/client/index.ts index 67e6c0e61c..3c4e8e0bf4 100644 --- a/packages/client/runtime/src/client/index.ts +++ b/packages/client/runtime/src/client/index.ts @@ -24,6 +24,7 @@ export type { SessionProvideChannelHost } from './sessions/provide.ts' export { createScope } from './agents/scope.ts' export type { AgentScopeHandle } from './agents/scope.ts' export { DirectoryBrowseError, WorkspaceCreateError, WorkspacesService } from './workspaces/service.ts' +export { resolveWorkspacePath } from './workspaces/path.ts' export type { Session } from './sessions/session.ts' export type { ISession, ProjectionsFace, SessionFace } from './contract/session.ts' export type { diff --git a/packages/client/ui-conversation/src/client/contract/tool-path.ts b/packages/client/runtime/src/client/workspaces/path.ts similarity index 50% rename from packages/client/ui-conversation/src/client/contract/tool-path.ts rename to packages/client/runtime/src/client/workspaces/path.ts index 38a55937d3..d1512177ad 100644 --- a/packages/client/ui-conversation/src/client/contract/tool-path.ts +++ b/packages/client/runtime/src/client/workspaces/path.ts @@ -1,12 +1,10 @@ -/** Resolve a Tool argument path against the session workspace. */ - /** - * Resolve a Tool argument path for the Host opener. + * Resolve a workspace-relative path into the Host-facing spelling used by openPath. * @param cwd - session workspace root, when known. - * @param path - path carried by the Tool arguments. - * @returns an absolute-or-workspace-relative Host path. + * @param path - absolute or workspace-relative path. + * @returns an absolute path when a workspace root is available, otherwise the original path. */ -export function resolveToolPath(cwd: string | undefined, path: string): string { +export function resolveWorkspacePath(cwd: string | undefined, path: string): string { if (path.startsWith('/') || /^[A-Za-z]:[/\\]/.test(path) || path.startsWith('\\\\')) return path if (cwd === undefined || cwd === '') return path const base = cwd.replace(/[/\\]+$/, '') diff --git a/packages/client/ui-conversation/README.i18n.yaml b/packages/client/ui-conversation/README.i18n.yaml index 126729ad9d..2a396a8404 100644 --- a/packages/client/ui-conversation/README.i18n.yaml +++ b/packages/client/ui-conversation/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-conversation/README.md -README.md: 3aade8409e74c38cb86a66076cca50d51a1ccbf8 -README.zh.md: 19d08bbc072f1f242417b91ae4dd771f970244b1 +README.md: c4c7a0363c0a2760cf478744073eab96d837b719 +README.zh.md: a34f99f7e4e50750b2ecf024ed9e898c8de09529 diff --git a/packages/client/ui-conversation/README.md b/packages/client/ui-conversation/README.md index fa0c4b7b9b..837edaa097 100644 --- a/packages/client/ui-conversation/README.md +++ b/packages/client/ui-conversation/README.md @@ -16,7 +16,7 @@ Approvals take over the composer through the chain this package declares: `Appro The session header declares and renders the session-scoped `'conversation.session.header.actions'` list beside the title, allowing feature plugins to contribute controls without entering the skeleton. The composer chain currency includes the current conversation `session`; ui-subagent selects one-shot or parent-unavailable addressed sessions for reason-specific read-only copy, while the ordinary InputBar keeps every addressed child Send-only because the continuation service exposes no public per-Activation cancellation operation and `session.cancel` would bypass its ownership. -Logged non-user messages render as a default-collapsed disclosure whose header names the role the runtime projected for the message — `上下文注入` for an injection, `跨会话召回` for a recalled session — followed by the producer name that projection read out of the durable source, so a reader distinguishes a skill catalog from a workspace instruction file or a recalled session without expanding. A source that names no producer shows the role alone. The package-internal `DisclosureRow` gives this context surface the same compact geometry as other flow rows while retaining context semantics: the expanded body follows its content height up to a 141px scrolling cap and synthesizes no tool state or summary ([historical disclosure decision](../../../.agents/notes/archived/feature/2026-07-30-web-context-injection-disclosure.md), [provenance decision](../../../.agents/notes/implemented/feature/2026-08-04-web-context-source-and-steer-marks.md)). That body follows the form the producer declared on its durable source: `instructions` names the reconciled files above their text, `catalog` lists the entries the source recorded instead of the model-facing prose, and every other value — absent, unknown to this version, or carrying no usable fields — renders the opaque body, which shows the model-facing text with its real line breaks and the remaining provenance as fields. The opaque body is the documented default, not a leftover: a resumed, forked, or foreign log must render whether or not its producer is mounted here. A durable or pending steering bubble carries an `插话` / `Interjection` caption above it, the only thing distinguishing a mid-turn interjection from the turn-opening prompt that shares its bubble. +Logged non-user messages render as a default-collapsed disclosure whose header names the role the runtime projected for the message — `上下文注入` for an injection, `跨会话召回` for a recalled session — followed by the producer name that projection read out of the durable source, so a reader distinguishes a skill catalog from a workspace instruction file or a recalled session without expanding. A source that names no producer shows the role alone. The shared `DisclosureRow` primitive gives this context surface the same compact geometry as other flow rows while retaining context semantics: the expanded body follows its content height up to a 141px scrolling cap and synthesizes no tool state or summary ([historical disclosure decision](../../../.agents/notes/archived/feature/2026-07-30-web-context-injection-disclosure.md), [provenance decision](../../../.agents/notes/implemented/feature/2026-08-04-web-context-source-and-steer-marks.md)). That body follows the form the producer declared on its durable source: `instructions` names the reconciled files above their text, `catalog` lists the entries the source recorded instead of the model-facing prose, and every other value — absent, unknown to this version, or carrying no usable fields — renders the opaque body, which shows the model-facing text with its real line breaks and the remaining provenance as fields. The opaque body is the documented default, not a leftover: a resumed, forked, or foreign log must render whether or not its producer is mounted here. A durable or pending steering bubble carries an `插话` / `Interjection` caption above it, the only thing distinguishing a mid-turn interjection from the turn-opening prompt that shares its bubble. A Think row stays collapsed by default and exposes live reasoning throughput without expanding the chain of thought: while its reasoning block is the streaming tail, the summary switches from the settled first line to the latest non-blank line and its one-line scrollport follows each delta to the inline end. Expanding the row removes the moving summary and leaves the full reasoning in ordinary page flow, so page reading never fights an internal follower; settlement restores the stable first-line summary at the left edge ([decision](../../../.agents/notes/implemented/feature/2026-08-02-web-thinking-tail-scroll.md)). diff --git a/packages/client/ui-conversation/README.zh.md b/packages/client/ui-conversation/README.zh.md index 6dc803ee72..419799666d 100644 --- a/packages/client/ui-conversation/README.zh.md +++ b/packages/client/ui-conversation/README.zh.md @@ -14,7 +14,7 @@ 会话页头会在标题旁声明并渲染 Session scope 的 `'conversation.session.header.actions'` 列表,使功能插件无需进入骨架即可贡献控件。编辑器链的 currency 包含当前对话 `session`;ui-subagent 会选取 one-shot 或 parent 不可用的已寻址会话,并按原因显示只读文案,而普通 InputBar 会让所有已寻址 child 仅保留 Send,因为继续执行服务不公开逐 Activation 取消操作,`session.cancel` 也会绕过其所有权。 -已记录的非用户消息渲染为默认折叠的展开项,标题栏先给出运行时为该消息投影出的角色——注入为 `上下文注入`,召回为 `跨会话召回`——其后是该投影从持久来源读出的生产者名称,因此读者无需展开即可区分 skill(技能)目录、工作区指令文件与被召回的会话。来源未提供生产者名称时只显示角色。包内部的 `DisclosureRow` 让该上下文界面与消息流中的其他紧凑行保持相同几何,同时保留上下文语义:展开内容区的高度会随内容自适应,最大为 141px,超出后滚动,且不会合成工具状态或摘要([历史展开项决策](../../../.agents/notes/archived/feature/2026-07-30-web-context-injection-disclosure.md)、[来源决策](../../../.agents/notes/implemented/feature/2026-08-04-web-context-source-and-steer-marks.md))。该内容区按生产方在持久来源上声明的形态渲染:`instructions` 在正文之上列出它对账过的文件,`catalog` 列出来源记录的条目而非面向模型的散文,其余取值——未声明、本版本不认识、或字段不可用——一律渲染 opaque 内容区,即按真实换行展示面向模型的文本,并把剩余来源信息列成字段。opaque 不是兜底剩余物而是有文档的默认:恢复的、fork 的、外部写入的日志,无论其生产方是否挂载在此处,都必须渲染得出来。持久或待处理的 steering(中途引导)气泡上方带有 `插话` / `Interjection` 标注,这是把中途插话与共用同一气泡的开轮提示区分开的唯一标识。 +已记录的非用户消息渲染为默认折叠的展开项,标题栏先给出运行时为该消息投影出的角色——注入为 `上下文注入`,召回为 `跨会话召回`——其后是该投影从持久来源读出的生产者名称,因此读者无需展开即可区分 skill(技能)目录、工作区指令文件与被召回的会话。来源未提供生产者名称时只显示角色。共享的 `DisclosureRow` 原子组件让该上下文界面与消息流中的其他紧凑行保持相同几何,同时保留上下文语义:展开内容区的高度会随内容自适应,最大为 141px,超出后滚动,且不会合成工具状态或摘要([历史展开项决策](../../../.agents/notes/archived/feature/2026-07-30-web-context-injection-disclosure.md)、[来源决策](../../../.agents/notes/implemented/feature/2026-08-04-web-context-source-and-steer-marks.md))。该内容区按生产方在持久来源上声明的形态渲染:`instructions` 在正文之上列出它对账过的文件,`catalog` 列出来源记录的条目而非面向模型的散文,其余取值——未声明、本版本不认识、或字段不可用——一律渲染 opaque 内容区,即按真实换行展示面向模型的文本,并把剩余来源信息列成字段。opaque 不是兜底剩余物而是有文档的默认:恢复的、fork 的、外部写入的日志,无论其生产方是否挂载在此处,都必须渲染得出来。持久或待处理的 steering(中途引导)气泡上方带有 `插话` / `Interjection` 标注,这是把中途插话与共用同一气泡的开轮提示区分开的唯一标识。 Think 行默认保持折叠,并在不展开思维链的情况下暴露实时推理(reasoning)吞吐:当推理块是流式输出尾部时,摘要从结算后的首行切换到最新的非空行,其单行滚动区会随每个 delta 追到行内末端。展开该行会移除移动摘要,让完整推理进入普通页面流,因此页面阅读不会与内部跟随器争夺滚动;结算后恢复左对齐的稳定首行摘要([决策](../../../.agents/notes/implemented/feature/2026-08-02-web-thinking-tail-scroll.md))。 diff --git a/packages/client/ui-conversation/src/client/apply.ts b/packages/client/ui-conversation/src/client/apply.ts index 74533b5e2d..20056757e6 100644 --- a/packages/client/ui-conversation/src/client/apply.ts +++ b/packages/client/ui-conversation/src/client/apply.ts @@ -1,7 +1,7 @@ /** Registers the conversation components, shared store, and service callbacks. */ import type { Context } from 'cordis' import { resolveSlotLabel, type BoundActions } from '@deepseek-ai/dsh-client-ui-slots' -import type { ISessions, SessionId } from '@deepseek-ai/dsh-client-runtime/client' +import { resolveWorkspacePath, type ISessions, type SessionId } from '@deepseek-ai/dsh-client-runtime/client' import type {} from '@deepseek-ai/dsh-client-ui-layout/client' // Type-only: pulls the locale plugin's Context merge (ctx.locale). import type {} from '@deepseek-ai/dsh-client-locale/client' @@ -11,7 +11,6 @@ import type { ConversationSessionHeaderInjected, ConversationSessionInjected, DetailsInjected, } from './contract/slots.ts' import type { InputNotice } from './input/contract.ts' -import { resolveToolPath } from './contract/tool-path.ts' import { createChatStore } from './stores.ts' import { ConversationService } from './service.ts' import type { IConversation } from './service.ts' @@ -320,7 +319,7 @@ export function apply(ctx: Context): void { }, openFile: (path) => { const cwd = sessions.list.getSnapshot().byId[sessionId]?.cwd - void workspaces.openPath(resolveToolPath(cwd, path)).catch(() => { + void workspaces.openPath(resolveWorkspacePath(cwd, path)).catch(() => { // Host/OS open failures stay silent in the chat row; the native // app surfaces its own error dialog when the path is unusable. }) diff --git a/packages/client/ui-conversation/src/client/chat/ContextInjectionRow.tsx b/packages/client/ui-conversation/src/client/chat/ContextInjectionRow.tsx index 6cfc1eebfb..1afdad37f1 100644 --- a/packages/client/ui-conversation/src/client/chat/ContextInjectionRow.tsx +++ b/packages/client/ui-conversation/src/client/chat/ContextInjectionRow.tsx @@ -1,8 +1,7 @@ import { useState } from 'react' import type { ContextMessageNode } from '@deepseek-ai/dsh-client-runtime/client' import type { ChatViewSlotProps } from '../contract/slots.ts' -import { IconBrowseOutline16 } from '@deepseek-ai/dsh-client-ui-primitives' -import { DisclosureRow } from './DisclosureRow.tsx' +import { DisclosureRow, IconBrowseOutline16 } from '@deepseek-ai/dsh-client-ui-primitives' import { contextBody } from './ContextBody.tsx' import css from './ContextInjectionRow.module.css' diff --git a/packages/client/ui-conversation/src/client/chat/DisclosureRow.module.css b/packages/client/ui-conversation/src/client/chat/DisclosureRow.module.css deleted file mode 100644 index 51937ab462..0000000000 --- a/packages/client/ui-conversation/src/client/chat/DisclosureRow.module.css +++ /dev/null @@ -1,69 +0,0 @@ -/* Shared conversation disclosure header: [16px leading] gap 6 [title 14/24]. */ - -.root { - display: flex; - flex-direction: column; - width: 100%; - min-width: 0; -} - -.row { - position: relative; - overflow: hidden; - display: flex; - align-items: center; - height: 24px; - min-width: 0; -} - -.row[data-expandable] { - cursor: pointer; -} - -.leading { - position: relative; - flex: none; - width: 16px; - height: 16px; - display: inline-flex; - align-items: center; - justify-content: center; - margin-right: 6px; - padding: 0; - border: none; - background: none; - color: var(--dsw-alias-label-tertiary); -} - -button.leading { - cursor: pointer; -} - -.iconIdle { - display: inline-flex; - opacity: 1; - transition: opacity 100ms ease; -} - -.chevronHover { - position: absolute; - inset: 0; - margin: auto; - opacity: 0; - transition: opacity 100ms ease; -} - -.row:hover .iconIdle { - opacity: 0; -} - -.row:hover .chevronHover { - opacity: 1; -} - -.title { - flex: none; - font-size: 14px; - line-height: 24px; - color: var(--dsw-alias-label-secondary); -} diff --git a/packages/client/ui-conversation/src/client/chat/GenericCommandCard.tsx b/packages/client/ui-conversation/src/client/chat/GenericCommandCard.tsx index 7bf7d74d2f..b16d838e77 100644 --- a/packages/client/ui-conversation/src/client/chat/GenericCommandCard.tsx +++ b/packages/client/ui-conversation/src/client/chat/GenericCommandCard.tsx @@ -6,8 +6,7 @@ import { useState, type ReactNode } from 'react' import type { ChatViewSlotProps, CommandRowOwnerProps } from '../contract/slots.ts' -import { IconApiOutline14, StateDot } from '@deepseek-ai/dsh-client-ui-primitives' -import { DisclosureRow } from './DisclosureRow.tsx' +import { DisclosureRow, IconApiOutline14, StateDot } from '@deepseek-ai/dsh-client-ui-primitives' import css from './GenericCommandCard.module.css' type CommandRowState = 'running' | 'ok' | 'error' diff --git a/packages/client/ui-conversation/src/client/chat/ReasoningRow.tsx b/packages/client/ui-conversation/src/client/chat/ReasoningRow.tsx index d02bd855b4..d888451ff7 100644 --- a/packages/client/ui-conversation/src/client/chat/ReasoningRow.tsx +++ b/packages/client/ui-conversation/src/client/chat/ReasoningRow.tsx @@ -1,7 +1,6 @@ /** Assistant reasoning disclosure, independent of Tool-call presentation. */ import { useEffect, useRef, useState } from 'react' -import { IconThinkOutline14 } from '@deepseek-ai/dsh-client-ui-primitives' -import { DisclosureRow } from './DisclosureRow.tsx' +import { DisclosureRow, IconThinkOutline14 } from '@deepseek-ai/dsh-client-ui-primitives' import { useThrottledVisualUpdate } from './use-throttled-visual-update.ts' import css from './ReasoningRow.module.css' diff --git a/packages/client/ui-primitives/README.i18n.yaml b/packages/client/ui-primitives/README.i18n.yaml index 3829879b9d..29ddfa1014 100644 --- a/packages/client/ui-primitives/README.i18n.yaml +++ b/packages/client/ui-primitives/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-primitives/README.md -README.md: c54759f98a944565959ef21ce538eb9b12fccdf1 -README.zh.md: 32275c19bca9d6e8aa510e982d535a72eb1a06a7 +README.md: fae49d5764d4001f1852cb43aab730064febf2d2 +README.zh.md: 37984b9020df08b8804306111ca13ec0e92e5ce7 diff --git a/packages/client/ui-primitives/README.md b/packages/client/ui-primitives/README.md index c54759f98a..fae49d5764 100644 --- a/packages/client/ui-primitives/README.md +++ b/packages/client/ui-primitives/README.md @@ -2,7 +2,7 @@ English | [中文](README.zh.md) -Pure React atoms (zero cordis): StateDot, ic_ds_* icons, Button/Pill/Menu/Modal/Input, the OnboardingSurface first-run takeover (body-portaled mask + opaque stage that holds `#root` inert for exactly its own lifetime), the markdown family (MessageText/MarkdownText/JsonBlock), the read-only JsonTree inspector, the `useAnchoredMaxHeight` hook that clamps a bottom-anchored overlay to the viewport space above its anchor (re-measured on resize, scroll, and a caller-supplied dependency), TerminalBlock, DiffBlock, ReadBlock, SearchBlock, and WebBlock. Contract: api-contracts v3 §8. +Pure React atoms (zero cordis): StateDot, DisclosureRow, ic_ds_* icons, Button/Pill/Menu/Modal/Input, the OnboardingSurface first-run takeover (body-portaled mask + opaque stage that holds `#root` inert for exactly its own lifetime), the markdown family (MessageText/MarkdownText/JsonBlock), the read-only JsonTree inspector, the `useAnchoredMaxHeight` hook that clamps a bottom-anchored overlay to the viewport space above its anchor (re-measured on resize, scroll, and a caller-supplied dependency), TerminalBlock, DiffBlock, ReadBlock, SearchBlock, and WebBlock. Contract: api-contracts v3 §8. ## Hover cards diff --git a/packages/client/ui-primitives/README.zh.md b/packages/client/ui-primitives/README.zh.md index 32275c19bc..37984b9020 100644 --- a/packages/client/ui-primitives/README.zh.md +++ b/packages/client/ui-primitives/README.zh.md @@ -2,7 +2,7 @@ [English](README.md) | 中文 -纯 React 原子组件(零 cordis):StateDot、ic_ds_* 图标、Button/Pill/Menu/Modal/Input、OnboardingSurface 首次使用接管层(portal 到 body 的遮罩加不透明展示层,在自身生命周期内保持 `#root` 为 `inert`)、markdown 家族(MessageText/MarkdownText/JsonBlock)、只读 JsonTree 检查器、`useAnchoredMaxHeight` hook(把底部锚定的浮层高度收敛到锚点上方的视口空间,并在 resize、scroll 与调用方提供的依赖变化时重新测量)、TerminalBlock、DiffBlock、ReadBlock、SearchBlock,以及 WebBlock。契约:api-contracts v3 §8。 +纯 React 原子组件(零 cordis):StateDot、DisclosureRow、ic_ds_* 图标、Button/Pill/Menu/Modal/Input、OnboardingSurface 首次使用接管层(portal 到 body 的遮罩加不透明展示层,在自身生命周期内保持 `#root` 为 `inert`)、markdown 家族(MessageText/MarkdownText/JsonBlock)、只读 JsonTree 检查器、`useAnchoredMaxHeight` hook(把底部锚定的浮层高度收敛到锚点上方的视口空间,并在 resize、scroll 与调用方提供的依赖变化时重新测量)、TerminalBlock、DiffBlock、ReadBlock、SearchBlock,以及 WebBlock。契约:api-contracts v3 §8。 ## 悬浮卡片 diff --git a/packages/client/ui-tool/src/client/tool/components/DisclosureRow.module.css b/packages/client/ui-primitives/src/DisclosureRow.module.css similarity index 92% rename from packages/client/ui-tool/src/client/tool/components/DisclosureRow.module.css rename to packages/client/ui-primitives/src/DisclosureRow.module.css index 04f2d18d0a..1aa932826f 100644 --- a/packages/client/ui-tool/src/client/tool/components/DisclosureRow.module.css +++ b/packages/client/ui-primitives/src/DisclosureRow.module.css @@ -1,4 +1,4 @@ -/* Shared Tool calls disclosure header: [16px leading] gap 6 [title 14/24]. */ +/* Shared disclosure header: [16px leading] gap 6 [title 14/24]. */ .root { display: flex; diff --git a/packages/client/ui-conversation/src/client/chat/DisclosureRow.tsx b/packages/client/ui-primitives/src/DisclosureRow.tsx similarity index 95% rename from packages/client/ui-conversation/src/client/chat/DisclosureRow.tsx rename to packages/client/ui-primitives/src/DisclosureRow.tsx index a95114ff2f..f04ad8986a 100644 --- a/packages/client/ui-conversation/src/client/chat/DisclosureRow.tsx +++ b/packages/client/ui-primitives/src/DisclosureRow.tsx @@ -1,9 +1,9 @@ import { type KeyboardEvent, type MouseEvent, type ReactNode } from 'react' import clsx from 'clsx' -import { IconChevronDownOutline14 } from '@deepseek-ai/dsh-client-ui-primitives' +import { IconChevronDownOutline14 } from './icons/index.tsx' import css from './DisclosureRow.module.css' -/** Shared 24px disclosure chrome for conversation flow rows. */ +/** Shared 24px disclosure chrome for compact flow rows. */ export interface DisclosureRowProps { icon: ReactNode title: string diff --git a/packages/client/ui-primitives/src/index.ts b/packages/client/ui-primitives/src/index.ts index fe0235c496..9aac5da45f 100644 --- a/packages/client/ui-primitives/src/index.ts +++ b/packages/client/ui-primitives/src/index.ts @@ -4,6 +4,8 @@ export { StateDot } from './StateDot.tsx' export type { StateDotState } from './StateDot.tsx' +export { DisclosureRow } from './DisclosureRow.tsx' +export type { DisclosureRowProps } from './DisclosureRow.tsx' export { Button } from './Button.tsx' export type { ButtonVariant } from './Button.tsx' export { Pill } from './Pill.tsx' diff --git a/packages/client/ui-tool/src/client/tool/components/DisclosureRow.tsx b/packages/client/ui-tool/src/client/tool/components/DisclosureRow.tsx deleted file mode 100644 index 361fb24517..0000000000 --- a/packages/client/ui-tool/src/client/tool/components/DisclosureRow.tsx +++ /dev/null @@ -1,104 +0,0 @@ -import { type KeyboardEvent, type MouseEvent, type ReactNode } from 'react' -import clsx from 'clsx' -import { IconChevronDownOutline14 } from '@deepseek-ai/dsh-client-ui-primitives' -import css from './DisclosureRow.module.css' - -/** Shared 24px disclosure chrome for conversation flow rows. */ -export interface DisclosureRowProps { - icon: ReactNode - title: string - open: boolean - expandable: boolean - onToggle: () => void - /** Makes the complete title row the disclosure target. */ - expandOnRowClick?: boolean | undefined - /** Replaces the collapsed icon with a chevron while the row is hovered. */ - previewChevron?: boolean | undefined - /** Keeps `collapsedContent` inline while open (ToolRow's summary stays readable next to the expanded card). */ - keepContentWhenOpen?: boolean | undefined - collapsedContent?: ReactNode - children?: ReactNode - className?: string | undefined - rowClassName?: string | undefined - leadingClassName?: string | undefined - chevronClassName?: string | undefined - titleClassName?: string | undefined -} - -/** - * Render one disclosure header and its controlled expanded content. - * @param props - Visual content, controlled state, and interaction policy. - * @returns The disclosure row. - */ -export function DisclosureRow({ - icon, - title, - open, - expandable, - onToggle, - expandOnRowClick = false, - previewChevron = expandable, - keepContentWhenOpen = false, - collapsedContent, - children, - className, - rowClassName, - leadingClassName, - chevronClassName, - titleClassName, -}: DisclosureRowProps) { - const rowExpands = expandable && expandOnRowClick - const toggleFromLeading = (event: MouseEvent) => { - event.stopPropagation() - onToggle() - } - const toggleFromKeyboard = (event: KeyboardEvent) => { - if (!rowExpands || (event.key !== 'Enter' && event.key !== ' ')) return - event.preventDefault() - onToggle() - } - const collapsedLeading = previewChevron - ? ( - <> - {icon} - - - ) - : icon - const leading = open - ? - : collapsedLeading - - return ( -
-
- {expandable && !rowExpands ? ( - - ) : ( - - {leading} - - )} - {title} - {(keepContentWhenOpen || !open) && collapsedContent} -
- {open && children} -
- ) -} 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 61c677ea98..bd4add4234 100644 --- a/packages/client/ui-tool/src/client/tool/components/ToolRow.tsx +++ b/packages/client/ui-tool/src/client/tool/components/ToolRow.tsx @@ -20,7 +20,7 @@ import { useState, type KeyboardEvent, type MouseEvent, type ReactNode } from 'react' import clsx from 'clsx' import { - CodeBlock, DiffBlock, IconInspectOutline12, ReadBlock, SearchBlock, StateDot, TerminalBlock, WebBlock, + CodeBlock, DiffBlock, DisclosureRow, IconInspectOutline12, ReadBlock, SearchBlock, StateDot, TerminalBlock, WebBlock, } from '@deepseek-ai/dsh-client-ui-primitives' import type { WebBlockProps } from '@deepseek-ai/dsh-client-ui-primitives' import type { TranslateNS } from '@deepseek-ai/dsh-client-ui-slots' @@ -29,7 +29,6 @@ import { CHAT_READ_MAX_LINES, type ReadCardModel } from '../models/read-card-mod import { CHAT_SEARCH_MAX_LINES, type SearchCardModel } from '../models/search-card-model.ts' import { terminalBlockLabels, type TerminalCardModel } from '../models/terminal-card-model.ts' import type { ToolRowState, ToolRowVariant } from '../models/tool-call-model.ts' -import { DisclosureRow } from './DisclosureRow.tsx' import css from './ToolRow.module.css' export interface ToolRowProps { diff --git a/packages/client/ui-tool/src/client/tool/models/terminal-card-model.ts b/packages/client/ui-tool/src/client/tool/models/terminal-card-model.ts index 8a0c887990..e0609191b7 100644 --- a/packages/client/ui-tool/src/client/tool/models/terminal-card-model.ts +++ b/packages/client/ui-tool/src/client/tool/models/terminal-card-model.ts @@ -8,9 +8,10 @@ * are derived once. * @module */ +import { resolveWorkspacePath } from '@deepseek-ai/dsh-client-runtime/client' import type { TerminalBlockLabels, TerminalBlockProps } from '@deepseek-ai/dsh-client-ui-primitives' import type { TranslateNS } from '@deepseek-ai/dsh-client-ui-slots' -import { resolveToolPath, type ToolCallBlock } from './tool-call-model.ts' +import type { ToolCallBlock } from './tool-call-model.ts' /** * Build the TerminalBlock display copy from the conversation locale seat — @@ -88,7 +89,7 @@ export function terminalFailed(model: TerminalCardModel): boolean { function resolveTerminalCwd(viewCwd: string | undefined, sessionCwd: string | undefined): string | undefined { if (viewCwd === undefined || viewCwd === '') return sessionCwd if (sessionCwd === undefined || sessionCwd === '') return normalizeSegments(viewCwd) - return normalizeSegments(resolveToolPath(sessionCwd, viewCwd)) + return normalizeSegments(resolveWorkspacePath(sessionCwd, viewCwd)) } /** diff --git a/packages/client/ui-tool/src/client/tool/models/tool-call-model.ts b/packages/client/ui-tool/src/client/tool/models/tool-call-model.ts index 151ef3b45f..f201f4aac4 100644 --- a/packages/client/ui-tool/src/client/tool/models/tool-call-model.ts +++ b/packages/client/ui-tool/src/client/tool/models/tool-call-model.ts @@ -172,21 +172,6 @@ function deriveFilePath(variant: ToolRowVariant, argsRaw: string): string | unde return picked === undefined ? undefined : firstLine(picked) } -/** - * Resolve a tool-arg path against the session cwd for host.openPath. - * Absolute POSIX/Windows paths pass through; relative paths join under cwd. - * @param cwd - session working directory (may be absent for ungrouped sessions). - * @param path - path as carried in tool args. - * @returns a host-facing path string. - */ -export function resolveToolPath(cwd: string | undefined, path: string): string { - if (path.startsWith('/') || /^[A-Za-z]:[/\\]/.test(path) || path.startsWith('\\\\')) return path - if (cwd === undefined || cwd === '') return path - const base = cwd.replace(/[/\\]+$/, '') - const rel = path.replace(/^[/\\]+/, '') - return `${base}/${rel}` -} - function deriveBody(variant: ToolRowVariant, argsRaw: string): string | null { if (argsRaw === '') return null const parsed = parseArgs(argsRaw) diff --git a/packages/client/ui-tool/tests/tool-row.spec.tsx b/packages/client/ui-tool/tests/tool-row.spec.tsx index ab7e1c80f7..f53bcb11f7 100644 --- a/packages/client/ui-tool/tests/tool-row.spec.tsx +++ b/packages/client/ui-tool/tests/tool-row.spec.tsx @@ -5,7 +5,8 @@ import { cleanup, fireEvent, render } from '@testing-library/react' import type { RunningToolCall, ToolResultNode } from '@deepseek-ai/dsh-client-runtime/client' import { makeTranslate } from '@deepseek-ai/dsh-client-test-runtime' import { zh as commonZh } from '@deepseek-ai/dsh-client-locale/src/locales/zh.ts' -import { classifyTool, resolveToolPath, resultText, toolRowModel } from '../src/client/tool/models/tool-call-model.ts' +import { resolveWorkspacePath } from '@deepseek-ai/dsh-client-runtime/client' +import { classifyTool, resultText, toolRowModel } from '../src/client/tool/models/tool-call-model.ts' import { ToolRow } from '../src/client/tool/components/ToolRow.tsx' import { GenericToolCard, type GenericToolCardProps } from '../src/client/tool/toolviews/GenericToolCard.tsx' import { zh } from '../../ui-conversation/src/client/locales.ts' @@ -87,11 +88,11 @@ describe('tool-call-model', () => { expect(toolRowModel('bash', running()).filePath).toBeUndefined() }) - it('resolveToolPath joins relative paths under cwd and passes absolute through', () => { - expect(resolveToolPath('/w', 'src/a.ts')).toBe('/w/src/a.ts') - expect(resolveToolPath('/w/', '/abs/a.ts')).toBe('/abs/a.ts') - expect(resolveToolPath(undefined, 'src/a.ts')).toBe('src/a.ts') - expect(resolveToolPath('/w', 'C:\\x\\a.ts')).toBe('C:\\x\\a.ts') + it('resolveWorkspacePath joins relative paths under cwd and passes absolute through', () => { + expect(resolveWorkspacePath('/w', 'src/a.ts')).toBe('/w/src/a.ts') + expect(resolveWorkspacePath('/w/', '/abs/a.ts')).toBe('/abs/a.ts') + expect(resolveWorkspacePath(undefined, 'src/a.ts')).toBe('src/a.ts') + expect(resolveWorkspacePath('/w', 'C:\\x\\a.ts')).toBe('C:\\x\\a.ts') }) it('displays workspace-rooted paths relative to the session cwd', () => {