1
0
Эх сурвалжийг харах

refactor(tool-ui): route file navigation and remove ToolDetails

imccyu 1 долоо хоног өмнө
parent
commit
108a7478c9

+ 1 - 1
.agents/notes/implemented/architecture/2026-08-23-client-derived-tool-presentation.md

@@ -248,7 +248,7 @@ Host presenter APIs describe top-level calls and results. Code Dispatch subcalls
 
 Code Dispatch start and result events already carry `parentCallId`. Conversation preserves that existing fact on each child `ToolCallBlock`; root Session calls omit it. The diff, read, search, and web models accept only blocks without `parentCallId`; the terminal model and existing renderers that intentionally support nested calls accept child blocks.
 
-The Details panel delegates the selected block unchanged. Shared card models apply the same terminal eligibility and nonterminal child restrictions in rows and Details, so the Details slot needs no placement field.
+Shared card models apply the same terminal eligibility and nonterminal child restrictions wherever a block renders, so no second presentation surface needs a placement field; the details panel that once delegated a selected block was removed with the right-hand details column ([decision](../feature/2026-09-04-right-sidebar-docking-infrastructure.md)).
 
 The keyed slot continues dispatching every subcall by its real tool name. `parentCallId` restricts only the diff, read, search, and web structured models covered by this decision. Existing specialized renderers such as Skill and Cordis, which already read raw blocks, remain unchanged.
 

+ 1 - 1
.agents/notes/implemented/architecture/2026-08-23-client-derived-tool-presentation.zh.md

@@ -248,7 +248,7 @@ Host presenter API 描述顶层 call/result。本决定覆盖的 diff、read、s
 
 Code Dispatch start 与 result event 已经携带 `parentCallId`。Conversation 在每个 child `ToolCallBlock` 上保留这项现有事实,root Session call 则不携带它。diff、read、search 和 web model 只接受没有 `parentCallId` 的 block;terminal model 与原本有意支持嵌套调用的 renderer 接受 child block。
 
-Details panel 原样委托选中的 block。共享 card model 在行与 Details 中应用相同的 terminal 适用规则和非 terminal 子调用限制,因此 Details slot 不需要 placement 字段
+共享的 card model 在 block 渲染到哪里都施加同样的终端资格与非终端子调用限制,因此不需要第二个展示面带 placement 字段;曾经原样委托选中 block 的详情面板已随右侧详情列一并删除([决策](../feature/2026-09-04-right-sidebar-docking-infrastructure.zh.md))
 
 keyed slot 仍按每个子调用的真实 tool name 分发;`parentCallId` 只限制本决定覆盖的 diff/read/search/web 结构化模型。Skill、Cordis 等已经直接读取 raw block 的专用 renderer 保持现状。
 

+ 3 - 3
packages/client/ui-tool/README.md

@@ -57,12 +57,12 @@ The package realizes one dispatch rule: atomic Tool views are keyed by wire Tool
 
 ### Rendering contract
 
-`ToolCallTree` receives one root `ToolCallBlock` that already contains recursive `subCalls`, selection state, the session `cwd`, and Host callbacks for opening files and inspecting calls. It recursively walks the standard call blocks and sends the root and children at every depth through the same atomic dispatch path, without subscribing to a separate parent-to-children map. Each root and child wrapper preserves the `data-chat-anchor-key="call:<id>"` and `data-chat-call-id` DOM contract used for paging and selection.
+`ToolCallTree` receives one root `ToolCallBlock` that already contains recursive `subCalls`, the session `cwd`, and the owner's callbacks for opening files and inspecting calls. It recursively walks the standard call blocks and sends the root and children at every depth through the same atomic dispatch path, without subscribing to a separate parent-to-children map. Each root and child wrapper preserves the `data-chat-anchor-key="call:<id>"` and `data-chat-call-id` DOM contract used for paging and selection.
 
-### Details and cards
+### Cards
 
 
-The package fills `conversation.details.tool` with `ToolDetails`. Row and Details renderers share one pure card model for each terminal, read, diff, search, and web card; the image card is row-only because its gallery renders through the tool-owned `tool.call.images` slot the details panel does not declare. These models validate raw call arguments, result content, failure state, persisted metadata, and Session path facts. Diff, read, search, and web models also reject Code Dispatch children; terminal eligibility does not depend on `parentCallId`. Unsupported or malformed inputs use flattened Tool result text. 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.
+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.
 
 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.
 </details>

+ 3 - 3
packages/client/ui-tool/README.zh.md

@@ -57,12 +57,12 @@ owner 载荷为 `ToolCallOwnerProps`:`callId`、`toolName`、冻结的 `block`
 
 ### 渲染约定
 
-`ToolCallTree` 接收一个已经包含递归 `subCalls` 的 root `ToolCallBlock`、selection 状态、会话 `cwd`,以及用于打开文件和检查调用的 Host 回调。它递归遍历标准调用块,让 root 与任意深度的 child 经过同一条原子分发路径,不订阅独立的 parent-to-children map。每个 root 和 child 包装层都保留 `data-chat-anchor-key="call:<id>"` 与 `data-chat-call-id` DOM 约定,供分页和 selection 使用。
+`ToolCallTree` 接收一个已经包含递归 `subCalls` 的 root `ToolCallBlock`、会话 `cwd`,以及属主用于打开文件和检查调用的回调。它递归遍历标准调用块,让 root 与任意深度的 child 经过同一条原子分发路径,不订阅独立的 parent-to-children map。每个 root 和 child 包装层都保留 `data-chat-anchor-key="call:<id>"` 与 `data-chat-call-id` DOM 约定,供分页和 selection 使用。
 
-### 详情与卡片
+### 卡片
 
 
-本包通过 `ToolDetails` 填充 `conversation.details.tool`。行 renderer 与 Details renderer 分别为 terminal、read、diff、search 和 web 卡片复用同一个纯 card model;image 卡片仅属于行,因为其图库经由工具自有 `tool.call.images` 槽位渲染,而 details 面板不声明该槽位。这些 model 校验原始调用参数、结果内容、失败状态、持久 metadata 与 Session 路径事实。diff、read、search 和 web model 还会拒绝 Code Dispatch 子调用;terminal 适用性不依赖 `parentCallId`。不受支持或格式错误的输入使用压平的工具结果文本。terminal、diff、read、search 与 web 卡片的上限与 fallback 规则仍由 [ui-primitives README](../ui-primitives/README.zh.md) 负责;image 卡片的 fallback 规则由本包内的 card model 自行承载。
+每张卡片都在调用树里就地阅读;不存在选中调用的第二个全高展示面。行 renderer 为 terminal、read、diff、search 和 web 卡片各复用同一个纯 card model,image 卡片的图库经由工具自有 `tool.call.images` 槽位渲染。这些 model 校验原始调用参数、结果内容、失败状态、持久 metadata、Code Dispatch 的 `parentCallId` 与 Session 路径事实。不受支持或格式错误的输入使用压平的工具结果文本。文件路径摘要经属主的 `openFile` 打开文件,chat 视图把它路由到右侧 Sidebar 的文本预览;`inspect` 打开轨迹视图。terminal、diff、read、search 与 web 卡片的上限与 fallback 规则仍由 [ui-primitives README](../ui-primitives/README.zh.md) 负责;image 卡片的 fallback 规则由本包内的 card model 自行承载。
 
 terminal model 使用浏览器安全入口 `@deepseek-ai/dsh-spill-policy/notice` 的 `hasSpillNotice`,而非独立的 UI 匹配规则。[spill-policy README](../../spill/spill-policy/README.zh.md#shared-notice-ownership) 负责通知的格式化与识别。该检查保守地选择通用输出;匹配文本不能认证其来源,回放也不改变已记录的结果字节。
 </details>

+ 0 - 7
packages/client/ui-tool/src/client/apply.ts

@@ -7,7 +7,6 @@ import type {} from '@deepseek-ai/dsh-client-ui-conversation/client'
 import type {} from '@deepseek-ai/dsh-client-ui-renderer/client'
 import type {} from '@deepseek-ai/dsh-client-ui-session/client'
 import { ToolCallTree } from './tool/ToolCallTree.tsx'
-import { ToolDetails } from './tool/ToolDetails.tsx'
 import { CONVERSATION_NS as NS } from './locale.ts'
 import { askQuestionToolview } from './tool/toolviews/ask-question-row.tsx'
 import { bashToolviewSample } from './tool/toolviews/bash-sample.tsx'
@@ -41,12 +40,6 @@ export function apply(ctx: ClientContext): void {
     inject: toolInject,
   }, ToolCallTree))
 
-  ctx.slots.inject('conversation.details.tool', () => ctx.slots.register({
-    name: 'conversation.details.tool',
-    locale: NS,
-    inject: toolInject,
-  }, ToolDetails))
-
   ctx.plugin(bashToolviewSample)
   ctx.plugin(readToolview)
   ctx.plugin(readImageToolview)

+ 6 - 8
packages/client/ui-tool/src/client/contract/slots.ts

@@ -3,7 +3,7 @@ import type {
   HostObservable, InjectFace, PropsLocale, PropsRenderSlots, PropsRuntime,
 } from '@deepseek-ai/dsh-client-ui-slots'
 import type { RemoteHostFacts } from '@deepseek-ai/dsh-api-remotes/client'
-import type { ToolCallBlock } from '@deepseek-ai/dsh-client-ui-chat/client'
+import type { OpenFileOptions, ToolCallBlock } from '@deepseek-ai/dsh-client-ui-chat/client'
 import type { MessageImageLoader, MessageImageSource } from '@deepseek-ai/dsh-client-ui-conversation/client'
 import type {} from '@deepseek-ai/dsh-client-locale/client'
 
@@ -63,8 +63,11 @@ export interface ToolCallOwnerProps {
   cwd?: string | undefined
   /** Host account home; POSIX home-rooted summaries display as `~`. */
   home?: string | undefined
-  /** Open a Tool argument path through the Host. */
-  openFile: (path: string) => void
+  /**
+   * Open a Tool argument path. A view that knows which line the call was about
+   * passes it, and the opened surface lands there.
+   */
+  openFile: (path: string, options?: OpenFileOptions) => void
   /**
    * Session-authorized image loader for the `tool.call.images` slot, supplied
    * by the chat node that owns this call. A composed chat node always
@@ -98,8 +101,3 @@ export type ToolTreeProps = PropsRuntime<'conversation.chat.node', 'tool-call'>
   & PropsRenderSlots<'tool.call.toolview'>
   & PropsLocale<'conversation'>
   & InjectFace<ToolHostInfoInjected>
-
-/** Full props of the selected Tool output renderer in the details panel. */
-export type ToolDetailsProps = PropsRuntime<'conversation.details.tool'>
-  & PropsLocale<'conversation'>
-  & InjectFace<ToolHostInfoInjected>

+ 1 - 1
packages/client/ui-tool/src/client/index.ts

@@ -1,5 +1,5 @@
 /** Browser Tool plugin: whole-call composition and keyed atomic Tool views. */
 export { apply, inject } from './apply.ts'
 export type {
-  ToolCallOwnerProps, ToolCallViewProps, ToolDetailsProps, ToolHostInfoInjected, ToolTreeProps,
+  ToolCallOwnerProps, ToolCallViewProps, ToolHostInfoInjected, ToolTreeProps,
 } from './contract/slots.ts'

+ 4 - 9
packages/client/ui-tool/src/client/tool/ToolCallTree.tsx

@@ -12,12 +12,11 @@ function callName(node: ToolCallBlock): string {
 
 /** One atomic call dispatched through the Tool-owned keyed slot. */
 const ToolCall = memo(function ToolCall({
-  renderSlot, callId, toolName, block, openFile, selected, cwd, home, inspectCall, loadImage, t, children,
+  renderSlot, callId, toolName, block, openFile, cwd, home, inspectCall, loadImage, t, children,
 }: Pick<ToolTreeProps, 'renderSlot' | 'openFile' | 'cwd' | 'inspectCall' | 'loadImage' | 't'> & {
   callId: string
   toolName: string
   block: ToolCallBlock
-  selected: boolean
   home?: string | undefined
   children?: ReactNode
 }) {
@@ -36,7 +35,6 @@ const ToolCall = memo(function ToolCall({
       className={css.callRow}
       data-chat-anchor-key={`call:${callId}`}
       data-chat-call-id={callId}
-      data-selected={selected || undefined}
     >
       {renderSlot('tool.call.toolview', owner, {
         entryKey: toolName,
@@ -48,8 +46,8 @@ const ToolCall = memo(function ToolCall({
 })
 
 const ToolCallBranch = memo(function ToolCallBranch({
-  renderSlot, block, selectedCallId, cwd, home, openFile, inspectCall, loadImage, t,
-}: Pick<ToolTreeProps, 'renderSlot' | 'selectedCallId' | 'cwd' | 'openFile' | 'inspectCall' | 'loadImage' | 't'> & {
+  renderSlot, block, cwd, home, openFile, inspectCall, loadImage, t,
+}: Pick<ToolTreeProps, 'renderSlot' | 'cwd' | 'openFile' | 'inspectCall' | 'loadImage' | 't'> & {
   block: ToolCallBlock
   home?: string | undefined
 }) {
@@ -60,7 +58,6 @@ const ToolCallBranch = memo(function ToolCallBranch({
       toolName={callName(block)}
       block={block}
       openFile={openFile}
-      selected={block.callId === selectedCallId}
       cwd={cwd}
       home={home}
       inspectCall={inspectCall}
@@ -74,7 +71,6 @@ const ToolCallBranch = memo(function ToolCallBranch({
               key={child.callId}
               renderSlot={renderSlot}
               block={child}
-              selectedCallId={selectedCallId}
               cwd={cwd}
               home={home}
               openFile={openFile}
@@ -96,7 +92,7 @@ const ToolCallBranch = memo(function ToolCallBranch({
  * @returns the Tool call tree.
  */
 export function ToolCallTree({
-  renderSlot, node, selectedCallId, cwd, openFile, inspectCall, loadImage, useHostInfo, t,
+  renderSlot, node, cwd, openFile, inspectCall, loadImage, useHostInfo, t,
 }: ToolTreeProps) {
   const home = useHostInfo(info => info.home)
   const block = node.data.root
@@ -104,7 +100,6 @@ export function ToolCallTree({
     <ToolCallBranch
       renderSlot={renderSlot}
       block={block}
-      selectedCallId={selectedCallId}
       cwd={cwd}
       home={home}
       openFile={openFile}

+ 0 - 46
packages/client/ui-tool/src/client/tool/ToolDetails.module.css

@@ -1,46 +0,0 @@
-.description {
-  margin: 0 0 6px;
-  color: var(--dsw-alias-label-secondary);
-  font: var(--dsw-font-xs-13);
-}
-
-.cardBody {
-  margin: 0;
-}
-
-.recovery {
-  margin: 6px 0 0;
-  white-space: pre-wrap;
-  overflow-wrap: anywhere;
-  color: var(--dsw-alias-label-tertiary);
-  font: var(--dsw-font-xs-13);
-}
-
-.code {
-  margin: 0;
-  padding: 16px;
-  border-radius: 12px;
-  background: var(--dsw-alias-markdown-code-block);
-  font-family: var(--ds-font-family-code);
-  font-size: 13px;
-  line-height: 22px;
-  color: var(--dsw-alias-label-primary);
-  white-space: pre-wrap;
-  word-break: break-word;
-}
-
-.code[data-error] {
-  color: var(--dsw-alias-state-error-primary);
-}
-
-.read,
-.web {
-  margin: 0;
-}
-
-.empty {
-  padding: 8px 0;
-  font-size: 13px;
-  line-height: 20px;
-  color: var(--dsw-alias-label-tertiary);
-}

+ 0 - 68
packages/client/ui-tool/src/client/tool/ToolDetails.tsx

@@ -1,68 +0,0 @@
-/** Card-aware output body for the selected Tool call in details. */
-import { DiffBlock, ReadBlock, SearchBlock, TerminalBlock, WebBlock } from '@deepseek-ai/dsh-client-ui-primitives'
-import type { ToolDetailsProps } from '../contract/slots.ts'
-import { diffCardModel } from './models/diff-card-model.ts'
-import { readCardModel } from './models/read-card-model.ts'
-import { searchCardModel } from './models/search-card-model.ts'
-import {
-  localizeTerminalCardModel, terminalBlockLabels, terminalCardModel,
-} from './models/terminal-card-model.ts'
-import {
-  diffBlockLabels, readBlockLabels, searchBlockLabels, webBlockLabels,
-} from './models/primitive-labels.ts'
-import { resultText } from './models/tool-call-model.ts'
-import { webCardModel } from './models/web-card-model.ts'
-import css from './ToolDetails.module.css'
-
-/**
- * Render the selected Tool call's structured output when its raw fields form a
- * supported card, otherwise preserve the flattened result text.
- * @param props - selected call slice, workspace root, host home, and locale seat.
- * @returns the details output body.
- */
-export function ToolDetails({
-  block, cwd, useHostInfo, t,
-}: Pick<ToolDetailsProps, 'block' | 'cwd' | 'useHostInfo' | 't'>) {
-  const home = useHostInfo(info => info.home)
-  const terminalModel = terminalCardModel(block, cwd)
-  if (terminalModel !== null) {
-    const terminal = localizeTerminalCardModel(terminalModel, t)
-    return (
-      <>
-        {terminal.description !== undefined ? (
-          <div className={css.description}>{terminal.description}</div>
-        ) : null}
-        <TerminalBlock {...terminal.card} labels={terminalBlockLabels(t)} className={css.cardBody} />
-      </>
-    )
-  }
-  const read = readCardModel(block, cwd, home)
-  if (read !== null) return <ReadBlock {...read} labels={readBlockLabels(t)} className={css.read} />
-  const diff = diffCardModel(block)
-  if (diff !== null) return <DiffBlock {...diff.card} labels={diffBlockLabels(t)} className={css.cardBody} />
-  const search = searchCardModel(block)
-  if (search !== null) {
-    return (
-      <>
-        <SearchBlock {...search.card} labels={searchBlockLabels(t)} className={css.cardBody} />
-        {search.recovery !== undefined ? <div className={css.recovery}>{search.recovery}</div> : null}
-      </>
-    )
-  }
-  const web = webCardModel(block)
-  if (web !== null) {
-    const body = 'kind' in block ? resultText(block) : ''
-    return (
-      <>
-        <WebBlock {...web} labels={webBlockLabels(t)} className={css.web} />
-        {body !== '' ? <pre className={css.code}>{body}</pre> : null}
-      </>
-    )
-  }
-  if (!('kind' in block)) return <div className={css.empty}>{t('details.running')}</div>
-  return (
-    <pre className={css.code} data-error={block.isError || undefined}>
-      {resultText(block)}
-    </pre>
-  )
-}

+ 9 - 3
packages/client/ui-tool/src/client/tool/components/ToolRow.tsx

@@ -5,6 +5,7 @@ import {
   diffTotals,
 } from '@deepseek-ai/dsh-client-ui-primitives'
 import type { PropsRenderSlots, TranslateNS } from '@deepseek-ai/dsh-client-ui-slots'
+import type { OpenFileOptions } from '@deepseek-ai/dsh-client-ui-chat/client'
 import type { MessageImageLoader } from '@deepseek-ai/dsh-client-ui-conversation/client'
 import { CHAT_DIFF_MAX_LINES, type DiffCardModel } from '../models/diff-card-model.ts'
 import { CHAT_READ_MAX_LINES, type ReadCardModel } from '../models/read-card-model.ts'
@@ -75,8 +76,10 @@ export interface ToolRowProps {
    * renders as a hover-underline link that opens the host default app.
    */
   filePath?: string | undefined
-  /** Open the path with the host OS default application (already cwd-resolved). */
-  onOpenFile?: ((path: string) => void) | undefined
+  /** 1-based line the call was about; absent = open the file at its beginning. */
+  filePathLine?: number | undefined
+  /** Open the path (already cwd-resolved), landing on `filePathLine` when given. */
+  onOpenFile?: ((path: string, options?: OpenFileOptions) => void) | undefined
   /**
    * Jump to this call in the trajectory view: a hover-revealed Inspect pill
    * over the expanded body. Absent = no affordance.
@@ -127,6 +130,7 @@ export function ToolRow({
   web,
   state,
   filePath,
+  filePathLine,
   onOpenFile,
   inspect,
 }: ToolRowProps) {
@@ -174,7 +178,9 @@ export function ToolRow({
   }
   const openFile = (event: MouseEvent<HTMLButtonElement>) => {
     event.stopPropagation()
-    if (filePath !== undefined) onOpenFile?.(filePath)
+    if (filePath === undefined || onOpenFile === undefined) return
+    if (filePathLine === undefined) onOpenFile(filePath)
+    else onOpenFile(filePath, { line: filePathLine })
   }
   // Keep Enter/Space on the focused path link from bubbling to the row's
   // keydown handler, which would preventDefault() the key and toggle expand

+ 24 - 2
packages/client/ui-tool/src/client/tool/models/read-card-model.ts

@@ -31,13 +31,18 @@ interface ReadMeta {
   lang?: string
 }
 
+/** Whether a model-supplied argument is a 1-based line position or count: an integer of at least 1. */
+function positiveInteger(value: unknown): value is number {
+  return typeof value === 'number' && Number.isInteger(value) && value >= 1
+}
+
 function validReadCall(block: ToolCallBlock): boolean {
   const call = parsedToolCall(block)
   if (call?.name !== 'read') return false
   const { file_path: path, offset, limit } = call.args
   if (typeof path !== 'string' || path.trim() === '') return false
-  if (offset !== undefined && (typeof offset !== 'number' || !Number.isInteger(offset) || offset < 1)) return false
-  if (limit !== undefined && (typeof limit !== 'number' || !Number.isInteger(limit) || limit < 1)) return false
+  if (offset !== undefined && !positiveInteger(offset)) return false
+  if (limit !== undefined && !positiveInteger(limit)) return false
   return true
 }
 
@@ -66,6 +71,23 @@ function readMeta(meta: unknown): ReadMeta | null {
   }
 }
 
+/**
+ * The line one `read` call was about, from its arguments.
+ *
+ * `offset` is the read tool's own 1-based start line, so opening the path can
+ * land where the model looked. Available while the call is still running,
+ * unlike the persisted metadata, because the arguments carry it. The arguments
+ * are model-produced JSON: only an integer of at least 1 is a line, and a call
+ * whose `offset` is anything else names none.
+ * @param block - running or settled Tool block.
+ * @returns the 1-based line, or undefined when the call named none.
+ */
+export function readCallLine(block: ToolCallBlock): number | undefined {
+  if (!validReadCall(block)) return undefined
+  const { offset } = parsedToolCall(block)?.args ?? {}
+  return positiveInteger(offset) ? offset : undefined
+}
+
 /**
  * Derive a settled root read card after validating its persisted metadata and
  * model-facing read envelope.

+ 4 - 3
packages/client/ui-tool/src/client/tool/toolviews/read-family-row.tsx

@@ -21,10 +21,11 @@ export type ReadImageRowProps = ReadFamilyRowProps & PropsRenderSlots<'tool.call
 
 /**
  * The card material one read-family row contributes: exactly the ToolRow card
- * props that row owns. `read` supplies `read`; `read_image` supplies `image`
- * together with the slot dispatcher and loader that draw it.
+ * props that row owns. `read` supplies `read` and the line its call named;
+ * `read_image` supplies `image` together with the slot dispatcher and loader
+ * that draw it.
  */
-export type ReadFamilyCard = Pick<ToolRowProps, 'read' | 'image' | 'renderSlot' | 'loadImage'>
+export type ReadFamilyCard = Pick<ToolRowProps, 'read' | 'image' | 'renderSlot' | 'loadImage' | 'filePathLine'>
 
 /**
  * Compose a read-family row: the shared chrome and model-derived fields, plus the

+ 7 - 3
packages/client/ui-tool/src/client/tool/toolviews/read-row.tsx

@@ -1,18 +1,22 @@
 import type { Context } from '@deepseek-ai/cordis'
 import type { PropsLocale } from '@deepseek-ai/dsh-client-ui-slots'
 import type { ToolCallViewProps } from '../../contract/slots.ts'
-import { readCardModel } from '../models/read-card-model.ts'
+import { readCallLine, readCardModel } from '../models/read-card-model.ts'
 import { readFamilyRow } from './read-family-row.tsx'
 import { CONVERSATION_NS as NS } from '../../locale.ts'
 
 type ReadRowProps = ToolCallViewProps & PropsLocale<'conversation'>
 
 /**
- * Lets users expand a completed read result and open its reported path.
+ * Lets users expand a completed read result and open its reported path at the
+ * line the call started from.
  */
 export function ReadRow(props: ReadRowProps) {
   const { block, cwd, home } = props
-  return readFamilyRow(props, { read: readCardModel(block, cwd, home) })
+  return readFamilyRow(props, {
+    read: readCardModel(block, cwd, home),
+    filePathLine: readCallLine(block),
+  })
 }
 
 /** Registers the read tool's conversation row. */