Merge remote-tracking branch 'origin/master' into feat/web-terminal-card
# Conflicts: # packages/client/ui-conversation/README.i18n.yaml
This commit is contained in:
@@ -38,7 +38,7 @@ export function GenericToolCard({ toolName, block, cwd, openFile }: ToolRowOwner
|
||||
summary={model.summary}
|
||||
// Single-file tools never expose an args body — the path link is the only action.
|
||||
body={singleFile ? null : model.body}
|
||||
terminal={terminalCardModel(block)}
|
||||
terminal={terminalCardModel(block, cwd)}
|
||||
state={model.state}
|
||||
filePath={model.filePath}
|
||||
onOpenFile={singleFile ? openFile : undefined}
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
* @module
|
||||
*/
|
||||
import type { TerminalBlockProps } from '@deepseek-ai/dsh-client-ui-primitives'
|
||||
import type { ToolCallBlock } from './tool-call-model.ts'
|
||||
import { resolveToolPath, type ToolCallBlock } from './tool-call-model.ts'
|
||||
|
||||
/**
|
||||
* Output lines the chat row's expanded terminal body shows before collapsing
|
||||
@@ -33,6 +33,24 @@ export type TerminalCardModel = Pick<
|
||||
'command' | 'cwd' | 'output' | 'exitCode' | 'signal' | 'running'
|
||||
>
|
||||
|
||||
/**
|
||||
* Resolve a terminal view's working directory the way the render-intent
|
||||
* contract assigns to the UI bridge: an absolute path is used as-is, a relative
|
||||
* one joins under the session workspace, and an omitted one IS the session
|
||||
* workspace. A pure presenter cannot see the session cwd, which is why this
|
||||
* resolution belongs here rather than in the tool. Without a session cwd there
|
||||
* is nothing to resolve against, so a relative path stays as authored and an
|
||||
* omitted one stays absent (the prompt row then draws a bare `$`).
|
||||
* @param viewCwd - the cwd the terminal call view carries, if any.
|
||||
* @param sessionCwd - the session workspace root, if the caller knows it.
|
||||
* @returns the working directory for the prompt label, or undefined.
|
||||
*/
|
||||
function resolveTerminalCwd(viewCwd: string | undefined, sessionCwd: string | undefined): string | undefined {
|
||||
if (viewCwd === undefined || viewCwd === '') return sessionCwd
|
||||
if (sessionCwd === undefined || sessionCwd === '') return viewCwd
|
||||
return resolveToolPath(sessionCwd, viewCwd)
|
||||
}
|
||||
|
||||
/**
|
||||
* Derive the terminal-card props for a tool call, or null when this call is
|
||||
* not a terminal card and belongs on the generic path.
|
||||
@@ -55,15 +73,17 @@ export type TerminalCardModel = Pick<
|
||||
* result view's replacement title, then to an empty command (the prompt line
|
||||
* draws bare), and the prompt shows no cwd.
|
||||
* @param block - RunningToolCall or ToolResultNode off the snapshot caches.
|
||||
* @param sessionCwd - the session workspace root, which resolves an omitted or
|
||||
* relative view cwd (see {@link resolveTerminalCwd}); absent leaves both unresolved.
|
||||
* @returns the terminal-card props, or null for the generic path.
|
||||
*/
|
||||
export function terminalCardModel(block: ToolCallBlock): TerminalCardModel | null {
|
||||
export function terminalCardModel(block: ToolCallBlock, sessionCwd?: string): TerminalCardModel | null {
|
||||
const call = block.callView?.card === 'terminal' ? block.callView : null
|
||||
if (!('kind' in block)) {
|
||||
// Running: the call view exists, the result view does not yet.
|
||||
return call === null ? null : {
|
||||
command: call.title,
|
||||
cwd: call.cwd,
|
||||
cwd: resolveTerminalCwd(call.cwd, sessionCwd),
|
||||
output: undefined,
|
||||
exitCode: undefined,
|
||||
signal: undefined,
|
||||
@@ -73,8 +93,11 @@ export function terminalCardModel(block: ToolCallBlock): TerminalCardModel | nul
|
||||
const result = block.resultView?.card === 'terminal' ? block.resultView : null
|
||||
if (result === null) return null
|
||||
return {
|
||||
command: call?.title ?? result.title ?? '',
|
||||
cwd: call?.cwd,
|
||||
// The result's title REPLACES the pending one when the tool supplies it
|
||||
// (the presentation contract's replacement-title rule); the call title is
|
||||
// what a result without one keeps.
|
||||
command: result.title ?? call?.title ?? '',
|
||||
cwd: resolveTerminalCwd(call?.cwd, sessionCwd),
|
||||
output: result.output,
|
||||
exitCode: result.exitCode,
|
||||
signal: result.signal,
|
||||
|
||||
@@ -68,8 +68,11 @@ function pretty(raw: string): string {
|
||||
}
|
||||
}
|
||||
|
||||
export function DetailsPanel({ useSession, useStore, closeDetails }: DetailsPanelProps) {
|
||||
export function DetailsPanel({ useSession, useSessions, sessionId, useStore, closeDetails }: DetailsPanelProps) {
|
||||
const selection = useStore(s => s.selection)
|
||||
// Session workspace root: an omitted or relative terminal cwd resolves
|
||||
// against it, which the pure presenter cannot see.
|
||||
const sessionCwd = useSessions(list => list.byId[sessionId]?.cwd)
|
||||
const callId = selection?.callId
|
||||
// materialFor builds a fresh wrapper; shallowEqual short-circuits on its
|
||||
// stable members (result node reference rides the snapshot's structural sharing).
|
||||
@@ -107,7 +110,11 @@ export function DetailsPanel({ useSession, useStore, closeDetails }: DetailsPane
|
||||
)}
|
||||
<section className={css.section}>
|
||||
<div className={css.sectionLabel}>Output</div>
|
||||
<OutputBody material={material} />
|
||||
{/* Keyed by the selected call: the body owns per-call view
|
||||
state (the terminal card's expand and copy), which React
|
||||
would otherwise carry into the next selection because the
|
||||
panel does not unmount between calls. */}
|
||||
<OutputBody key={callId} material={material} cwd={sessionCwd} />
|
||||
</section>
|
||||
</>
|
||||
)}
|
||||
@@ -123,10 +130,11 @@ export function DetailsPanel({ useSession, useStore, closeDetails }: DetailsPane
|
||||
* its alignment and scrolls sideways instead of folding. Every other call, and
|
||||
* a running call with no terminal card yet, keeps the flattened text form.
|
||||
* @param props.material - the selected call's material from {@link materialFor}.
|
||||
* @param props.cwd - the session workspace root, resolving the terminal view's cwd.
|
||||
* @returns the Output section's body element.
|
||||
*/
|
||||
function OutputBody({ material }: { material: CallMaterial }) {
|
||||
const terminal = terminalCardModel(material.block)
|
||||
function OutputBody({ material, cwd }: { material: CallMaterial; cwd: string | undefined }) {
|
||||
const terminal = terminalCardModel(material.block, cwd)
|
||||
if (terminal !== null) return <TerminalBlock {...terminal} className={css.terminal} />
|
||||
// A settled call always carries the result node the flattened form needs;
|
||||
// the running shape has no result to flatten.
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
// TodoPanel: persistent plan strip above the composer (the web counterpart
|
||||
// of the TUI plan panel). Renders the latest todo/write whole-list snapshot —
|
||||
// no data of its own, hidden while the list is empty. Mounted through the
|
||||
// 'conversation.input.dock' slot (QueueDock posture): the dock adapter does
|
||||
// the selecting, so the panel takes the plain list and stays framework-free.
|
||||
// Visual: figma 772:51905 (states) / 772:52972 (collapsed) / 772:53419 (expanded).
|
||||
// TodoPanel: plan strip above the composer (the web counterpart of the TUI
|
||||
// plan panel). Renders the standing todo/write whole-list snapshot (cleared on
|
||||
// the next turn/start) — no data of its own, hidden while the list is empty.
|
||||
// Mounted through the 'conversation.input.dock' slot (QueueDock posture): the
|
||||
// dock adapter does the selecting, so the panel takes the plain list and stays
|
||||
// framework-free. Visual: figma 772:51905 / 772:52972 / 772:53419.
|
||||
|
||||
import { useId, useState } from 'react'
|
||||
import type { Context } from 'cordis'
|
||||
|
||||
@@ -45,7 +45,10 @@ function stateStatus(state: ToolRowState): string | null {
|
||||
*/
|
||||
export function BashRow({ toolName, block, sessionId, useSessions }: ToolRowProps) {
|
||||
const model = toolRowModel(toolName, block)
|
||||
const terminal = terminalCardModel(block)
|
||||
// Session workspace root: the terminal view's cwd resolves against it (an
|
||||
// omitted workdir IS the workspace), which the pure presenter cannot do.
|
||||
const cwd = useSessions(list => list.byId[sessionId]?.cwd)
|
||||
const terminal = terminalCardModel(block, cwd)
|
||||
const isChild = useSessions(list => list.byId[sessionId]?.parentId !== undefined)
|
||||
const status = stateStatus(model.state)
|
||||
return (
|
||||
|
||||
Reference in New Issue
Block a user