/** * The workflow-graph mode of the Tasks page: layered agent / workflow nodes * over bezier edges on a dotted grid, drag-pan from the BACKGROUND only * (never from a node — capturing pointers on the container retargets the * derived click and silently swallows node clicks), wheel zoom to the cursor, * dashed phase frames, fold aggregate nodes, and the bottom-right horizontal * control cluster (view toggle / fold / zoom out / level / zoom in / fit) * that stays reachable in BOTH modes. * * Fitting: the canvas auto-fits while the reader has not touched the view, * re-running on container resize and layout growth (the sidebar mounts * hidden at zero size, so a one-shot fit on mount is not enough), centering * on both axes and scaling UP to {@link FIT_MAX_SCALE} so a small tree fills * the narrow panel instead of hugging the top-left corner. * * Cards: the node card recipe of tasks-graph.module.css with a TWO-line * clamped title (the card reserve in tasks-graph-layout.ts is sized for that * second line, plus one row per live / task line the card renders) and the * full name in the `title` attribute. While the root catalog hydrates, an * empty canvas shows the same hint as the tree (`loading`). */ import { useCallback, useEffect, useMemo, useRef, useState, type PointerEvent as ReactPointerEvent, type ReactNode, } from 'react' import clsx from 'clsx' import { Button, IconChecklistOutline14, IconFullscreenOutline16, IconLoadingOutline16, 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_FIT_MIN_SCALE, GRAPH_NODE_W, layoutTasksGraphForWidth, type GraphBox, } from './tasks-graph-layout.ts' 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' import canvasCss from './tasks-canvas.module.css' /** The zoom bounds of the canvas. */ const ZOOM_MIN = 0.3 const ZOOM_MAX = 1.5 /** Upper bound of the auto-fit scale (never balloon a two-node tree). */ const FIT_MAX_SCALE = 1.15 /* The readability floor the fit honours is `GRAPH_FIT_MIN_SCALE` (imported): * the layout budgets its columns from the same number, so scaling and wrapping * can never disagree. */ /** Drag-vs-click separation: pointer travel below this stays a click. */ const CLICK_TOLERANCE_PX = 4 /** * Card line 1 of an agent / run node: the canvas node title with the * truncation swapped for a TWO-line clamp — a single ellipsized line left too * few CJK characters to tell two siblings apart, and `nodeHeight` in * tasks-graph-layout.ts reserves the card height for that second line (plus * one row per live / task line below it). The untruncated name stays in the * `title` attribute. */ const NODE_TITLE = canvasCss.nodeTitle /** The fold aggregate's title: the same clamp on the receding plain ink. */ const NODE_TITLE_PLAIN = clsx(NODE_TITLE, canvasCss.nodeTitlePlain) /** One phase frame of a run node (the dashed box behind its members). */ interface PhaseFrame { key: string title: string | undefined x: number y: number w: number h: number } export interface TasksGraphProps { nodes: readonly TasksNode[] folded: boolean /** Re-fit trigger: the topology root id (a reroot re-centers). */ rootId: string | undefined onNodeInfo(node: TasksAgentNode, anchor: HTMLElement): void onWorkflowInfo(node: TasksWorkflowNode, anchor: HTMLElement): void /** Open the shared task window for one task id. */ onOpenTask(taskId: string, anchor: HTMLElement): void onToggleFold(): void mode: 'graph' | 'tree' onModeChange(mode: 'graph' | 'tree'): void /** Fallback hint while the root catalog hydrates (the graph twin of * TasksTree's loading row: the same copy key, the same "nothing to show * yet" condition). */ loading?: boolean } /** The shared view-mode toggle (glyph + label) both modes render in their * bottom-right control cluster. */ export function ViewModeToggle(props: { mode: 'graph' | 'tree' onModeChange(mode: 'graph' | 'tree'): void }): ReactNode { const { mode, onModeChange } = props const toTree = mode === 'graph' const label = toTree ? t('tasksViewTree') : t('tasksViewGraph') return ( ) } /** The fold toggle (expand / re-collapse the settled aggregates). */ export function FoldToggleButton(props: { folded: boolean; onToggleFold(): void }): ReactNode { const { folded, onToggleFold } = props return (