fix(tasks-ui): postmodern chrome, working clicks, adaptive layout, lean cards
Review feedback from the live 3384 install, fixed at the root: - COLORS: the new stylesheet used var(--dsw-alias-accent) in 17 places — a token the DSH theme does NOT define, so every declaration was dropped (older files only survived because they carry fallbacks); container edges used border-l1 (4% black, invisible). Accent now rides --dsw-alias-state-business-primary, container edges --dsw-alias-border-l4. - CLICKS: the canvas called setPointerCapture on pointerdown, which retargets the derived click to the container and killed every node (and ⓘ) interaction; jsdom cannot see this, a real browser can. Capture is gone and panning only starts from the background. - LAYOUT: fit gave up silently at zero container size and never retried, centered only horizontally, and capped at 100% while a five-child level scaled to 35%. Now: ResizeObserver + first non-zero fit, both axes centered, a 0.78 readability floor, and layoutTasksGraphForWidth picks the flattest sibling arrangement that still meets that floor. - CARDS: title (one line) + mono meta + live line only; display title, team role, full model id and latest text moved into the ⓘ popover. - PANEL: metrics follow the approved 360x660 mockup (132x46 cards, 112 row stride), explicit line-heights, user-select:none, horizontal control cluster with a labelled mode button, always-visible team board strip. - Tokens for two new popover headings across all 20 dictionaries. Tests: 3 new page-interaction specs (click after background pointerdown, node gestures never pan, team board visible without a click) and a rewritten 9-case layout suite (band wrap, row reservation, width solver, reserved live row). Full suite 1400 passed / 133 files.
This commit is contained in:
+72
-31
@@ -1,20 +1,29 @@
|
||||
/**
|
||||
* 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.
|
||||
* Shared presentation bits of the Tasks page's two modes (graph + tree): the
|
||||
* state-dot mapping, the SHORT mono meta line (one or two tokens — the card
|
||||
* is 132px wide in the narrow native sidebar, so anything longer moves into
|
||||
* the ⓘ popover), the iconified live activity row ("glyph + tool + args",
|
||||
* the approved format), and the fold aggregate caption.
|
||||
*
|
||||
* Card content contract (the page's answer to "everything is ellipsized"):
|
||||
* line 1 dot + name (11px, semibold, one line)
|
||||
* line 2 mode/model · activity (9px mono, one line)
|
||||
* line 3 live line (running nodes only)
|
||||
* Everything else — display title, full model id, team role, phase names,
|
||||
* latest output text, jump action — lives in the anchored popover.
|
||||
*/
|
||||
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 type { TasksAgentNode, TasksNodeState, TasksWorkflowNode } from './tasks-model.ts'
|
||||
import { toolGlyph } from './tool-icons.tsx'
|
||||
import { t } from './locales.ts'
|
||||
import { t, type CopyKey } from './locales.ts'
|
||||
import css from './tasks-graph.module.css'
|
||||
|
||||
/** Preview cap of one tool-call argument line. */
|
||||
const ARGS_PREVIEW = 60
|
||||
const ARGS_PREVIEW = 42
|
||||
/** Preview cap of one text line. */
|
||||
const TEXT_PREVIEW = 72
|
||||
|
||||
/** The host StateDot semantic of a node display state. */
|
||||
export function nodeDotState(state: TasksNodeState): StateDotState {
|
||||
@@ -26,6 +35,16 @@ export function nodeDotState(state: TasksNodeState): StateDotState {
|
||||
}
|
||||
}
|
||||
|
||||
/** The short state word of a node (the meta line's second token). */
|
||||
export function stateLabel(state: TasksNodeState): string {
|
||||
const key: CopyKey = state === 'running'
|
||||
? 'tasksStateRunning'
|
||||
: state === 'idle'
|
||||
? 'subagentInactive'
|
||||
: state === 'error' ? 'tasksStateError' : 'tasksStateDone'
|
||||
return t(key)
|
||||
}
|
||||
|
||||
/** 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
|
||||
@@ -37,46 +56,68 @@ export function flatten(text: string): string {
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
* The mono meta line of an agent card — at most two short tokens:
|
||||
* - the topology root: 主代理 · 状态
|
||||
* - a teammate with a known model: 模型 · 状态
|
||||
* - any other agent: 模式 · 状态
|
||||
*/
|
||||
export function agentSecondary(node: TasksAgentNode): string {
|
||||
const parts: string[] = []
|
||||
if (node.title !== undefined && node.title !== node.label) parts.push(node.title)
|
||||
export function agentMeta(node: TasksAgentNode): string {
|
||||
const state = stateLabel(node.state)
|
||||
if (node.parentId === undefined) return `${t('subagentMainAgent')} · ${state}`
|
||||
if (node.team?.role === 'teammate' && node.team.model !== undefined && node.team.model !== '') {
|
||||
return `${node.team.model} · ${state}`
|
||||
}
|
||||
if (node.mode !== undefined) {
|
||||
parts.push(node.mode === 'one-shot' ? t('subagentModeOneShot') : t('subagentModeContinuable'))
|
||||
} else if (node.synthesized === true) {
|
||||
parts.push(t('tasksWorkflowMember'))
|
||||
return `${node.mode === 'one-shot' ? t('subagentModeOneShot') : t('subagentModeContinuable')} · ${state}`
|
||||
}
|
||||
return state
|
||||
}
|
||||
|
||||
/** The mono meta line of a workflow run card: status · member tally. */
|
||||
export function workflowMeta(node: TasksWorkflowNode): string {
|
||||
const { run } = node
|
||||
let done = 0
|
||||
let total = 0
|
||||
for (const phase of run.phases) {
|
||||
for (const member of phase.members) {
|
||||
total += 1
|
||||
if (member.outcome !== undefined) done += 1
|
||||
}
|
||||
}
|
||||
return `${t(workflowStatusKey(run.status))} · ${t('workflowMembers', { done, total })}`
|
||||
}
|
||||
|
||||
/** The workflow run status label key. */
|
||||
export function workflowStatusKey(status: TasksWorkflowNode['run']['status']): CopyKey {
|
||||
switch (status) {
|
||||
case 'running': return 'workflowRunning'
|
||||
case 'completed': return 'workflowCompleted'
|
||||
case 'cancelled': return 'workflowCancelled'
|
||||
case 'failed': return 'workflowFailed'
|
||||
}
|
||||
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…".
|
||||
* The live activity row of a RUNNING agent: "glyph + tool + args" (the
|
||||
* approved format), plus the flattened last text line underneath. A running
|
||||
* node with neither reads as thinking.
|
||||
*/
|
||||
export function LiveLine(props: { live: LastActivity | undefined }): ReactNode {
|
||||
const { live } = props
|
||||
if (live?.text === undefined && live?.tool === undefined) {
|
||||
return <span className={css.liveLine}>{t('subagentThinking')}</span>
|
||||
return <span className={css.nodeMeta}>{t('subagentThinking')}</span>
|
||||
}
|
||||
return (
|
||||
<>
|
||||
{live.tool !== undefined && (
|
||||
<span className={css.liveLine}>
|
||||
<span className={css.liveGlyph} aria-hidden="true">{toolGlyph(live.tool.name)(10)}</span>
|
||||
<span className={css.live}>
|
||||
<span className={css.liveGlyph} aria-hidden="true">{toolGlyph(live.tool.name)(8)}</span>
|
||||
<span className={css.liveTool}>{live.tool.name}</span>
|
||||
{live.tool.args !== '' && (
|
||||
<span className={css.liveArgs}>{preview(live.tool.args)}</span>
|
||||
)}
|
||||
{live.tool.args !== '' && <span className={css.liveArgs}>{preview(live.tool.args)}</span>}
|
||||
</span>
|
||||
)}
|
||||
{live.text !== undefined && (
|
||||
<span className={css.liveText}>{flatten(live.text)}</span>
|
||||
<span className={css.liveText}>{preview(flatten(live.text), TEXT_PREVIEW)}</span>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
@@ -84,5 +125,5 @@ export function LiveLine(props: { live: LastActivity | undefined }): ReactNode {
|
||||
|
||||
/** The fold aggregate's subtitle: up to two label previews joined by ·. */
|
||||
export function foldPreviews(previews: readonly string[]): string {
|
||||
return previews.join(' · ')
|
||||
return previews.join(' / ')
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user