From 7cbfc766a15b16173b25cafacd8d16babd4b15a7 Mon Sep 17 00:00:00 2001 From: Menghuan1918 Date: Wed, 16 Sep 2026 17:44:33 +0800 Subject: [PATCH] feat(tasks-ui): one task window everywhere, cards are their own detail entry MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Round-three review feedback: - NO PER-CARD DETAIL BUTTON: the ellipsis control is gone from graph cards and tree rows — the card IS the affordance, and clicking it opens the detail window (the transcript jump moved inside as the primary button). Fold aggregates keep click-to-expand. - ONE TASK WINDOW: every task surface (board row, agent node task line, the node detail list, and creating a task) now opens the same non-fullscreen DRAGGABLE window (TaskWindow.tsx + its TaskPopover shell): markdown description by default, 编辑 switches the same card to multi-line editing, plus owner reassignment, complete/reopen and a two-step delete. The board lost its overflow menu and its Modal entirely; create uses the same window in create mode. - REUSE: TaskPopover (4 call sites), MultilineField (the plugin's own multi-line input — the host primitive set has none), OwnerPicker, TaskCreateButton, and the shared glyph/TaskLine helpers across both modes. - 3 new copy keys across zh/en/ja and the 18 third-language dictionaries. Tests: the node-click spec now asserts the detail window (and that the jump button inside it navigates), a new spec pins 'no nested control inside a card', and two more cover the shared window: markdown-first view then multi-line edit posting an 'edit' with the current revision, and immediate owner reassignment. Full suite 1406 passed / 133 files. --- ...09-14-tasks-graph-workflow-teams-design.md | 20 + src/client/AnchoredPopover.tsx | 2 +- src/client/SubagentView.tsx | 75 ++- src/client/TaskWindow.tsx | 434 ++++++++++++++++++ src/client/TasksGraph.tsx | 30 +- src/client/TasksPopovers.tsx | 14 +- src/client/TasksTree.tsx | 27 +- src/client/TeamBoard.tsx | 282 +----------- src/client/locales-ar.ts | 3 + src/client/locales-de.ts | 3 + src/client/locales-fr.ts | 3 + src/client/locales-hi.ts | 3 + src/client/locales-id.ts | 3 + src/client/locales-it.ts | 3 + src/client/locales-ja.ts | 3 + src/client/locales-ko.ts | 3 + src/client/locales-nl.ts | 3 + src/client/locales-pl.ts | 3 + src/client/locales-pt.ts | 3 + src/client/locales-ru.ts | 3 + src/client/locales-sv.ts | 3 + src/client/locales-th.ts | 3 + src/client/locales-tr.ts | 3 + src/client/locales-vi.ts | 3 + src/client/locales-zh-HK.ts | 3 + src/client/locales-zh-MO.ts | 3 + src/client/locales-zh-TW.ts | 3 + src/client/locales.ts | 6 + src/client/tasks-graph.module.css | 49 ++ src/client/tasks-shared.tsx | 17 +- tests/tasks-page.spec.tsx | 133 +++++- 31 files changed, 799 insertions(+), 347 deletions(-) create mode 100644 src/client/TaskWindow.tsx diff --git a/docs/plans/2026-09-14-tasks-graph-workflow-teams-design.md b/docs/plans/2026-09-14-tasks-graph-workflow-teams-design.md index 7600fd2..c80c94a 100644 --- a/docs/plans/2026-09-14-tasks-graph-workflow-teams-design.md +++ b/docs/plans/2026-09-14-tasks-graph-workflow-teams-design.md @@ -115,6 +115,26 @@ client 半(src/client/) i18n:本轮新增 19 条文案,zh/en/ja + 18 份第三语言词典同步(`tests/locales.spec.ts` 16/16)。 +## 第三轮返工(2026-09-14 深夜,交互统一) + +用户第三轮反馈两点,都指向「减少控件、统一入口」: + +| 反馈 | 做法 | +|---|---| +| 图卡片去掉详情按钮,「统一改为弹出详细」 | 删除每张卡片右上角的 `…` 按钮(树行同理):**卡片/行本身就是详情入口**,点击即弹详情窗口;跳转转录改为详情窗口内的主按钮。折叠聚合节点仍保留「点击=展开/再折叠」的语义 | +| 任务所有入口统一为「点击弹出非全屏可拖动编辑(兼查看)窗口」,含 编辑owner + 编辑 + 重开 + 删除;默认多行 markdown 预览,点编辑才进入多行编辑 | 新增单一组件 `TaskWindow.tsx`:`TaskWindow`(内容)+ `TaskPopover`(可拖动外壳,宽 430)+ `MultilineField`(自建多行输入)+ `OwnerPicker`(Pill 即点即改派)+ `TaskCreateButton`。三处入口全部复用它——任务板行、节点任务行、节点详情里的任务清单;「新建」也走同一个窗口(create 模式直接进入编辑态)。任务板由此**删掉了溢出菜单与 Modal**,只留筛选 Pills、任务行与新建按钮 | + +新文案 3 条(任务详情 / 暂无描述 / 被阻塞),zh/en/ja + 18 份第三语言同步。 + +组件复用清单(本轮): + +| 组件 | 复用点 | +|---|---| +| `TaskPopover` | 任务板行、节点任务行、节点详情任务清单、新建按钮(4 处) | +| `MultilineField` | 任务描述编辑(宿主 primitives 无多行输入,故自建) | +| `TaskCreateButton` | 任务板 / 其它入口共用同一外观 | +| `AgentGlyph` / `WorkflowGlyph` / `FoldGlyph` / `TaskLine` | 图与树两种模式共用 | + ## 验证结果(2026-09-14) PR:[#680](https://github.com/omdsh-dev/DSH-better-sidebar/pull/680)(分支 `feat/tasks-graph-workflow-teams`)。 diff --git a/src/client/AnchoredPopover.tsx b/src/client/AnchoredPopover.tsx index be9e09a..1092e5a 100644 --- a/src/client/AnchoredPopover.tsx +++ b/src/client/AnchoredPopover.tsx @@ -86,7 +86,7 @@ export function AnchoredPopover(props: AnchoredPopoverProps): ReactNode { if (!draggable || event.button !== 0) return const target = event.target as HTMLElement // A control inside the card (copy, kill, follow…) must not start a drag. - if (target.closest('button, input, select, a, [data-popover-no-drag]') !== null) return + if (target.closest('button, input, textarea, select, a, [data-popover-no-drag]') !== null) return event.preventDefault() const startX = event.clientX const startY = event.clientY diff --git a/src/client/SubagentView.tsx b/src/client/SubagentView.tsx index c86cd9f..e28d6ff 100644 --- a/src/client/SubagentView.tsx +++ b/src/client/SubagentView.tsx @@ -49,6 +49,7 @@ import { JobsDrawer, JobOutputPopoverContent } from './JobsDrawer.tsx' import { AnchoredPopover } from './AnchoredPopover.tsx' import { AgentNodePopover, WorkflowNodePopover } from './TasksPopovers.tsx' import { TeamBoard } from './TeamBoard.tsx' +import { TaskPopover } from './TaskWindow.tsx' import type { SidebarStore } from './state.ts' import type { WorkflowRunView } from '../workflow-runs.ts' import legacy from './SubagentView.module.css' @@ -133,6 +134,8 @@ type PagePopover = | { kind: 'node'; nodeId: string; anchor: HTMLElement } | { kind: 'workflow'; nodeId: string; anchor: HTMLElement } | { kind: 'job'; jobId: string; anchor: HTMLElement } + /** The shared task window; taskId undefined = create mode. */ + | { kind: 'task'; taskId: string | undefined; anchor: HTMLElement } /** * The sidebar's Tasks page. @@ -274,9 +277,8 @@ export function SubagentView(props: { void sessions.refreshSubagents?.(parentSessionId) }, [sessions]) - /** Node click = jump to the transcript (synthesized members without a - * session address open their detail popover instead). */ - const activateNode = useCallback((node: TasksAgentNode): void => { + /** Jump from the detail window into the node's transcript. */ + const jumpToNode = useCallback((node: TasksAgentNode): void => { if (node.childAddress !== undefined) { openChild(node.childAddress) setPopover(null) @@ -286,10 +288,21 @@ export function SubagentView(props: { openMain() setPopover(null) } - // Synthesized workflow members without a childId have no transcript to - // jump to: their ⓘ popover is the only detail surface. }, [openChild, openMain]) + /** Open the shared task window from a board row (undefined = create). */ + const openTaskFromBoard = useCallback(( + task: { id: string } | undefined, + anchor: HTMLElement, + ): void => { + setPopover({ kind: 'task', taskId: task?.id, anchor }) + }, []) + + /** Open the shared task window by id (node task lines, detail lists). */ + const openTaskById = useCallback((taskId: string, anchor: HTMLElement): void => { + setPopover({ kind: 'task', taskId, anchor }) + }, []) + const totals = useMemo( () => rootId === undefined ? { count: 0, runningCount: 0 } @@ -351,6 +364,25 @@ export function SubagentView(props: { /** The current popover's resolved content (subjects re-resolve live). */ const popoverContent = ((): ReactNode => { if (popover === null) return null + if (popover.kind === 'task') { + if (teamView?.available !== true || teamView.team === null || rootId === undefined) return null + const task = popover.taskId === undefined + ? undefined + : teamView.team.tasks.find(candidate => candidate.id === popover.taskId) + // A task that vanished (deleted elsewhere) closes the window instead of + // showing a stale card. + if (popover.taskId !== undefined && task === undefined) return null + return ( + + ) + } if (popover.kind === 'job') { const row = jobRows.find(candidate => candidate.job.id === popover.jobId) if (row === undefined) return null @@ -370,11 +402,8 @@ export function SubagentView(props: { return ( { - if (target.childAddress !== undefined) openChild(target.childAddress) - else openMain() - setPopover(null) - }} + onJump={jumpToNode} + onOpenTask={openTaskById} /> ) } @@ -410,7 +439,7 @@ export function SubagentView(props: { rootId={rootId} members={teamView.team.members} tasks={teamView.team.tasks} - onChanged={team.refresh} + onOpenTask={openTaskFromBoard} collapsed={teamBoardCollapsed} onToggleCollapsed={() => { setTeamBoardCollapsed(current => !current) }} /> @@ -441,9 +470,9 @@ export function SubagentView(props: { nodes={model} folded={folded} rootId={rootId} - onActivate={activateNode} onNodeInfo={(node, anchor) => { setPopover({ kind: 'node', nodeId: node.id, anchor }) }} onWorkflowInfo={(node, anchor) => { setPopover({ kind: 'workflow', nodeId: node.id, anchor }) }} + onOpenTask={openTaskById} onToggleFold={() => { setFolded(current => !current) }} mode={mode} onModeChange={setModeOverride} @@ -454,9 +483,9 @@ export function SubagentView(props: { nodes={model} folded={folded} loading={summaryBackedLoading} - onActivate={activateNode} onNodeInfo={(node, anchor) => { setPopover({ kind: 'node', nodeId: node.id, anchor }) }} onWorkflowInfo={(node, anchor) => { setPopover({ kind: 'workflow', nodeId: node.id, anchor }) }} + onOpenTask={openTaskById} onToggleFold={() => { setFolded(current => !current) }} mode={mode} onModeChange={setModeOverride} @@ -473,14 +502,18 @@ export function SubagentView(props: { : { kind: 'job', jobId: row.job.id, anchor }) }} /> - - {popoverContent} - + {popover?.kind === 'task' + ? popoverContent + : ( + + {popoverContent} + + )} ) } diff --git a/src/client/TaskWindow.tsx b/src/client/TaskWindow.tsx new file mode 100644 index 0000000..51414f8 --- /dev/null +++ b/src/client/TaskWindow.tsx @@ -0,0 +1,434 @@ +/** + * The ONE task window: view, edit and create all live here, and every task + * surface (the board's rows, an agent node's task line, the node detail + * popover) opens THIS component — pointer targets stay thin, the window owns + * the behaviour. + * + * Shape: a non-fullscreen, DRAGGABLE card (never a modal dialog). Details + * render as multi-line MARKDOWN by default; 编辑 switches the same card into + * multi-line editing, so reading and writing never jump between surfaces. + * Actions: owner reassignment (Pills, CAS-immediate), edit/save, reopen or + * complete, and a two-step delete. + * + * Host primitives everywhere (Button / Input / Pill / Tag / StateDot) plus + * the plugin's own `MultilineField` (the host set ships no multi-line input) + * and the shared `MarkdownText` renderer with the plugin's copy labels. + */ +import { useEffect, useRef, useState, type ReactNode } from 'react' +import { + Button, IconCheckOutline14, IconCloseOutline16, IconEditOutline16, IconPlusOutline16, + IconRefreshOutline14, IconTrashOutline16, Input, MarkdownText, Pill, StateDot, Tag, +} from '@deepseek-ai/dsh-client-ui-primitives' +import type { SidebarTeamMemberView, SidebarTeamTaskView } from '../context-types.ts' +import { api } from './api.ts' +import { markdownTextProps } from './markdown-labels.tsx' +import { AnchoredPopover } from './AnchoredPopover.tsx' +import { t, type CopyKey } from './locales.ts' +import css from './tasks-graph.module.css' + +/** One mutation outcome (the host route's own union). */ +type MutationResult = { ok: true } | { ok: false; error: { code: string; message: string } } + +/** The task status label key. */ +function taskStatusKey(status: SidebarTeamTaskView['status']): CopyKey { + switch (status) { + case 'pending': return 'teamTaskPending' + case 'in_progress': return 'teamTaskInProgress' + case 'completed': return 'teamTaskCompleted' + case 'deleted': return 'teamTaskDeleted' + } +} + +/** + * The plugin's own multi-line input: the host primitive set ships no + * textarea, so this is OUR component (token-styled, drag-exempt) and every + * multi-line edit in the page goes through it. + */ +export function MultilineField(props: { + value: string + label: string + placeholder?: string + rows?: number + onChange?(next: string): void +}): ReactNode { + return ( +