/** * Shared presentation bits of the Tasks page's two modes (graph + tree): * the state-dot mapping, the secondary card line, the iconified live * activity row ("glyph + tool + args" — the user-approved format replacing * the old `>read[file]` text), and the fold aggregate caption. Kept in one * module so graph and tree can never drift apart visually. */ import type { ReactNode } from 'react' import { StateDot, type StateDotState } from '@deepseek-ai/dsh-client-ui-primitives' import type { LastActivity } from '../subagent-activity.ts' import type { TasksAgentNode, TasksNodeState } from './tasks-model.ts' import { toolGlyph } from './tool-icons.tsx' import { t } from './locales.ts' import css from './tasks-graph.module.css' /** Preview cap of one tool-call argument line. */ const ARGS_PREVIEW = 60 /** The host StateDot semantic of a node display state. */ export function nodeDotState(state: TasksNodeState): StateDotState { switch (state) { case 'running': return 'ongoing' case 'idle': return 'idle' case 'done': return 'done' case 'error': return 'error' } } /** First `limit` characters with an ellipsis when truncated. */ export function preview(text: string, limit: number = ARGS_PREVIEW): string { return text.length > limit ? `${text.slice(0, limit)}…` : text } /** Collapse whitespace for single-line previews. */ export function flatten(text: string): string { return text.replace(/\s+/g, ' ').trim() } /** * The secondary line of an agent node: display title (when it differs from * the durable label) · mode · activity · teammate model. Empty parts drop * out. Synthesized workflow members show their origin instead of a mode. */ export function agentSecondary(node: TasksAgentNode): string { const parts: string[] = [] if (node.title !== undefined && node.title !== node.label) parts.push(node.title) if (node.mode !== undefined) { parts.push(node.mode === 'one-shot' ? t('subagentModeOneShot') : t('subagentModeContinuable')) } else if (node.synthesized === true) { parts.push(t('tasksWorkflowMember')) } parts.push(node.activity === 'running' ? t('subagentRunning') : t('subagentInactive')) if (node.team?.model !== undefined) parts.push(node.team.model) return parts.join(' · ') } /** * The live activity row of a RUNNING agent node: the tool line renders as * "glyph + tool + args" (the approved live-line format), plus the flattened * last text line underneath. A running node with neither reads "thinking…". */ export function LiveLine(props: { live: LastActivity | undefined }): ReactNode { const { live } = props if (live?.text === undefined && live?.tool === undefined) { return {t('subagentThinking')} } return ( <> {live.tool !== undefined && ( {live.tool.name} {live.tool.args !== '' && ( {preview(live.tool.args)} )} )} {live.text !== undefined && ( {flatten(live.text)} )} ) } /** The fold aggregate's subtitle: up to two label previews joined by ·. */ export function foldPreviews(previews: readonly string[]): string { return previews.join(' · ') }