feat(tasks-ui): tasks on their owner nodes, host-primitive controls, icon glyphs, draggable output
Round-two review feedback, all three points: - TASKS ON NODES: the model maps each shared task (ownerName -> member name -> member session id) onto its owner's node; graph cards and tree rows render a fourth line (icon + subject + status + '+N'), and the node detail popover lists every owned task. - TEAM BOARD: member Pills double as the owner filter; each task row is a status dot + subject + owner + status Tag + ONE overflow Menu (complete / reopen / edit / reassign submenu / delete with a two-step arm). Creating and editing moved into a real Modal with labeled Inputs and owner Pills — the narrow strip is no longer where you type. - HOST PRIMITIVES ONLY: the board and the jobs drawer now use Menu / Modal / Input / Button / Pill / Tag / Switch / StateDot; no native select or input remains (guarded by a spec). - ICONS, NOT HAND-DRAWN GLYPHS: every text glyph (circle/diamond/triangle/ check/chevrons/i/house) is replaced by a host icon — agent preset, user (teammate), branch (workflow run), checklist (fold + tasks), tree corner, fullscreen (fit), ellipsis (node details), copy/stop (jobs). The live line now shows the tool's own icon + name + args without the drawn box. - JOBS UX: the output popover is DRAGGABLE (offset clamped to the viewport, double click re-anchors, controls do not start a drag), defaults to 380px, and gains copy-output, a follow-latest switch, kind/status Tags and a drag hint; the drawer highlights the open row. - 19 new copy keys across zh/en/ja and the 18 third-language dictionaries. Tests: 4 new specs (task rendered on its node, no native select/input + menu-driven board, create-through-dialog posts the CAS-free create, job popover drag moves the card). Full suite 1404 passed / 133 files.
This commit is contained in:
+49
-39
@@ -23,11 +23,17 @@ import {
|
||||
type ReactNode,
|
||||
} from 'react'
|
||||
import clsx from 'clsx'
|
||||
import { StateDot } from '@deepseek-ai/dsh-client-ui-primitives'
|
||||
import {
|
||||
Button, IconChecklistOutline14, IconEllipsisOutline16, IconFullscreenOutline16, IconTreeCorner8x10,
|
||||
StateDot,
|
||||
} from '@deepseek-ai/dsh-client-ui-primitives'
|
||||
import type { TasksAgentNode, TasksFoldNode, TasksNode, TasksWorkflowNode } from './tasks-model.ts'
|
||||
import { tasksEdges } from './tasks-model.ts'
|
||||
import { GRAPH_NODE_W, layoutTasksGraphForWidth, type GraphBox } from './tasks-graph-layout.ts'
|
||||
import { agentMeta, foldPreviews, LiveLine, nodeDotState, workflowMeta } from './tasks-shared.tsx'
|
||||
import {
|
||||
AgentGlyph, agentMeta, FoldGlyph, foldPreviews, LiveLine, nodeDotState, TaskLine,
|
||||
WorkflowGlyph, workflowMeta,
|
||||
} from './tasks-shared.tsx'
|
||||
import { t } from './locales.ts'
|
||||
import css from './tasks-graph.module.css'
|
||||
|
||||
@@ -72,17 +78,19 @@ export function ViewModeToggle(props: {
|
||||
}): ReactNode {
|
||||
const { mode, onModeChange } = props
|
||||
const toTree = mode === 'graph'
|
||||
const label = toTree ? `▤ ${t('tasksViewTree')}` : `⌗ ${t('tasksViewGraph')}`
|
||||
const label = toTree ? t('tasksViewTree') : t('tasksViewGraph')
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
className={clsx(css.controlBtn, css.controlMode)}
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className={css.controlMode}
|
||||
icon={toTree ? <IconTreeCorner8x10 /> : <WorkflowGlyph size={13} />}
|
||||
aria-label={t(toTree ? 'tasksViewSwitchToTree' : 'tasksViewSwitchToGraph')}
|
||||
title={t(toTree ? 'tasksViewSwitchToTree' : 'tasksViewSwitchToGraph')}
|
||||
onClick={() => { onModeChange(toTree ? 'tree' : 'graph') }}
|
||||
>
|
||||
{label}
|
||||
</button>
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -90,16 +98,16 @@ export function ViewModeToggle(props: {
|
||||
export function FoldToggleButton(props: { folded: boolean; onToggleFold(): void }): ReactNode {
|
||||
const { folded, onToggleFold } = props
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
className={css.controlBtn}
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className={clsx(css.controlBtn, folded && css.controlBtnActive)}
|
||||
icon={<IconChecklistOutline14 size={13} />}
|
||||
aria-pressed={folded}
|
||||
aria-label={t(folded ? 'tasksFoldExpand' : 'tasksFoldCollapse')}
|
||||
title={t(folded ? 'tasksFoldExpand' : 'tasksFoldCollapse')}
|
||||
onClick={onToggleFold}
|
||||
>
|
||||
{folded ? '⇪' : '⇩'}
|
||||
</button>
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -333,34 +341,36 @@ export function TasksGraph(props: TasksGraphProps): ReactNode {
|
||||
<div className={css.controls} data-graph-controls>
|
||||
<ViewModeToggle mode={mode} onModeChange={onModeChange} />
|
||||
<FoldToggleButton folded={folded} onToggleFold={onToggleFold} />
|
||||
<button
|
||||
type="button"
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className={css.controlBtn}
|
||||
aria-label={t('tasksZoomOut')}
|
||||
title={t('tasksZoomOut')}
|
||||
onClick={() => { zoomBy(1 / 1.2) }}
|
||||
>
|
||||
−
|
||||
</button>
|
||||
</Button>
|
||||
<span className={css.controlZoomLevel} aria-hidden="true">{Math.round(tf.k * 100)}%</span>
|
||||
<button
|
||||
type="button"
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className={css.controlBtn}
|
||||
aria-label={t('tasksZoomIn')}
|
||||
title={t('tasksZoomIn')}
|
||||
onClick={() => { zoomBy(1.2) }}
|
||||
>
|
||||
+
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className={css.controlBtn}
|
||||
icon={<IconFullscreenOutline16 size={13} />}
|
||||
aria-label={t('tasksZoomFit')}
|
||||
title={t('tasksZoomFit')}
|
||||
onClick={refit}
|
||||
>
|
||||
⌂
|
||||
</button>
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -399,25 +409,24 @@ function renderAgentNode(
|
||||
>
|
||||
<span className={css.nodeHeader}>
|
||||
<StateDot state={nodeDotState(node.state)} size={6} className={css.nodeDot} />
|
||||
<span className={css.nodeTitle} title={node.label}>
|
||||
{node.team?.role === 'teammate' ? `◇ ${node.label}` : node.label}
|
||||
</span>
|
||||
<span className={css.nodeGlyph} aria-hidden="true"><AgentGlyph node={node} /></span>
|
||||
<span className={css.nodeTitle} title={node.label}>{node.label}</span>
|
||||
</span>
|
||||
<span className={css.nodeMeta} title={agentMeta(node)}>{agentMeta(node)}</span>
|
||||
{node.state === 'running' && <LiveLine live={node.live} />}
|
||||
<button
|
||||
type="button"
|
||||
className={css.treeInfo}
|
||||
style={{ position: 'absolute', top: 3, right: 3 }}
|
||||
aria-label={t('tasksNodeState')}
|
||||
title={t('tasksNodeState')}
|
||||
<TaskLine tasks={node.tasks} />
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className={css.nodeInfo}
|
||||
icon={<IconEllipsisOutline16 size={12} />}
|
||||
aria-label={t('tasksNodeDetail')}
|
||||
title={t('tasksNodeDetail')}
|
||||
onClick={(event) => {
|
||||
event.stopPropagation()
|
||||
onNodeInfo(node, event.currentTarget)
|
||||
}}
|
||||
>
|
||||
i
|
||||
</button>
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -445,7 +454,8 @@ function renderWorkflowNode(
|
||||
>
|
||||
<span className={css.nodeHeader}>
|
||||
<StateDot state={node.run.status === 'running' ? 'ongoing' : 'done'} size={6} className={css.nodeDot} />
|
||||
<span className={css.nodeTitle} title={node.run.name}>{`▶ ${node.run.name}`}</span>
|
||||
<span className={css.nodeGlyph} aria-hidden="true"><WorkflowGlyph /></span>
|
||||
<span className={css.nodeTitle} title={node.run.name}>{node.run.name}</span>
|
||||
</span>
|
||||
<span className={css.nodeMeta}>{workflowMeta(node)}</span>
|
||||
</div>
|
||||
@@ -474,13 +484,13 @@ function renderFoldNode(
|
||||
}}
|
||||
>
|
||||
<span className={css.nodeHeader}>
|
||||
<StateDot state="done" size={6} className={css.nodeDot} />
|
||||
<span className={css.nodeGlyph} aria-hidden="true"><FoldGlyph /></span>
|
||||
<span className={clsx(css.nodeTitle, css.nodeTitlePlain)}>
|
||||
{t('tasksFoldCompleted', { count: node.count })}
|
||||
</span>
|
||||
</span>
|
||||
<span className={css.nodeMeta} title={foldPreviews(node.previews)}>
|
||||
{`⇪ ${t('tasksFoldExpand')} · ${foldPreviews(node.previews)}`}
|
||||
{`${t('tasksFoldExpand')} · ${foldPreviews(node.previews)}`}
|
||||
</span>
|
||||
</div>
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user