/**
* The two-segment node card of the Tasks graph: a TOP segment that says WHAT
* the node is (kind badge, name, mono meta, optional task line) and a BOTTOM
* bar that says WHAT IT IS DOING (state dot + state word + the merged process
* activity, plus the fold chevron once the node has settled).
*
* Both modes share the model but NOT the card: the TREE keeps its compact
* indent row (its own disclosure, its own keyboard recipe), so this module is
* graph-only. Its two pieces are separate components because every node kind
* (agent / workflow run / fold aggregate) composes them differently while the
* geometry stays identical — the layout reserves the height of exactly these
* rows (`tasks-graph-layout.ts`).
*
* Colour: the group colour of a node is its tree DEPTH, from the plugin's own
* token-only ramp (`.depth0…3` in tasks-graph.module.css). The host ships no
* chart palette and the skin contract forbids literal colours, so the ramp is
* mixed from `--dsw-alias-state-business-primary`; a workflow member's PHASE
* badge uses the ink family instead, so the two groupings never share a hue.
*/
import type { ReactNode } from 'react'
import clsx from 'clsx'
import { IconChevronUpOutlineRegular, StateDot } from '@deepseek-ai/dsh-client-ui-primitives'
import type { TasksNodeState } from './tasks-model.ts'
import { nodeDotState, stateLabel } from './tasks-shared.tsx'
import { t } from './locales.ts'
import css from './tasks-graph.module.css'
/** The five kinds of card the graph draws. */
export type CardKind = 'main' | 'subagent' | 'teammate' | 'workflow' | 'fold'
/**
* The kind badge's word: the SAME vocabulary the rest of the page uses (the
* header already calls the root "主代理"), so no sixth word is invented for it.
*
* The aggregate's badge says 已完成 only when something WAS completed: an
* aggregate that sweeps idle teammates (or only failed rows) and still claimed
* "✓ N 已完成" would be lying about what is inside it, so `foldBadgeIdle`
* switches it to the idle word for exactly that case.
* @param kind - card kind.
* @param count - folded member count (the fold aggregate's badge only).
* @param foldBadgeIdle - the aggregate holds no completed row (see above).
*/
export function cardKindLabel(kind: CardKind, count = 0, foldBadgeIdle = false): string {
switch (kind) {
case 'main': return t('subagentMainAgent')
case 'subagent': return t('tasksKindSubagent')
case 'teammate': return t('tasksKindTeammate')
case 'workflow': return t('tasksKindWorkflow')
case 'fold': return foldBadgeIdle
? t('tasksFoldIdle', { count })
: t('tasksFoldCompleted', { count })
}
}
/**
* The depth class of a node's group colour (levels past 3 share the last one).
* Typed `string | undefined` because the CSS-module map is an open record: a
* typo'd class name would otherwise pass `tsc` as a `string`.
*/
export function depthClass(depth: number): string | undefined {
if (depth <= 0) return css.depth0
if (depth === 1) return css.depth1
if (depth === 2) return css.depth2
return css.depth3
}
/** The phase class of a workflow member's badge (phases past 3 share the last one). */
export function phaseClass(index: number): string | undefined {
if (index <= 0) return css.phase0
if (index === 1) return css.phase1
if (index === 2) return css.phase2
return css.phase3
}
/** The top segment: the node's identity. */
export function CardTop(props: {
kind: CardKind
/** Folded member count, for the aggregate's badge. */
count?: number
/** Whether the aggregate holds no completed row (its badge switches word). */
badgeIdle?: boolean
/** Tree depth → the group colour of the badge and the card's left edge. */
depth: number
/** Workflow phase (a member's badge). */
phase?: { index: number; title?: string }
/** The node's name (already resolved by the model). */
name: string
/** The mono meta line under the name. */
meta?: string
/** Extra class for the name (the aggregate recedes one rung). */
extraClass?: string
/** Extra rows the caller appends (the team task line). */
children?: ReactNode
}): ReactNode {
const { kind, count, badgeIdle, depth, phase } = props
return (
{cardKindLabel(kind, count ?? 0, badgeIdle === true)}
{phase !== undefined && (
{phase.title ?? t('workflowPhaseUnnamed')}
)}
{props.name}
{props.meta !== undefined && props.meta !== '' && (
{props.meta}
)}
{props.children}
)
}
/** The bottom bar: the node's state, its merged activity, and its fold chevron. */
export function CardBar(props: {
state: TasksNodeState
/** The merged process activity line (running nodes and settled summaries). */
activity?: string
/** Whether the node is executing (drives the sweep). */
running?: boolean
/** Fold the node into its parent's completed aggregate (settled nodes only). */
onFold?: (event: { stopPropagation(): void }) => void
/** The chevron's accessible name (defaults to the per-node fold word). */
foldLabel?: string
/** Render the state WORD next to the dot (off for the aggregate's action bar). */
stateWord?: boolean
/** Replace the whole bar's content (the aggregate's action row). */
children?: ReactNode
}): ReactNode {
const { state, activity, running } = props
return (
{props.children ?? (
<>
{props.stateWord !== false && {stateLabel(state)}}
{activity !== undefined && activity !== '' && (
{activity}
)}
{props.onFold !== undefined && (
)}
>
)}
)
}