@linxin666/dsh-client-ui-task-board 0.4.0 → 0.4.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/README.i18n.yaml +2 -2
  2. package/README.md +24 -0
  3. package/README.zh.md +24 -0
  4. package/lib/client.js +917 -81
  5. package/lib/client.js.map +1 -1
  6. package/lib/index.js +1774 -1420
  7. package/lib/types/client/TaskBoardSettingsCard.d.ts +4 -0
  8. package/lib/types/client/board/LinkSubtaskModal.d.ts +11 -0
  9. package/lib/types/client/board/NewTaskModal.d.ts +7 -1
  10. package/lib/types/client/board/TaskCard.d.ts +11 -1
  11. package/lib/types/client/locales.d.ts +33 -0
  12. package/lib/types/core/controller.d.ts +30 -1
  13. package/lib/types/core/subtask.d.ts +122 -0
  14. package/lib/types/core/tasks.d.ts +46 -3
  15. package/lib/types/core/use-cases/task-archive.d.ts +10 -13
  16. package/lib/types/core/use-cases/task-create.d.ts +13 -4
  17. package/lib/types/core/use-cases/task-delete.d.ts +7 -1
  18. package/lib/types/core/use-cases/task-parent.d.ts +21 -0
  19. package/lib/types/core/use-cases/task-update.d.ts +1 -1
  20. package/lib/types/host/agent-tools.d.ts +27 -0
  21. package/lib/types/host-ledger.d.ts +88 -4
  22. package/lib/types/host-runner.d.ts +39 -1
  23. package/lib/types/host-service.d.ts +47 -0
  24. package/lib/types/index.d.ts +83 -5
  25. package/lib/types/protocol.d.ts +14 -0
  26. package/package.json +23 -21
  27. package/src/client/TaskBoardSettingsCard.tsx +48 -3
  28. package/src/client/board/LinkSubtaskModal.tsx +82 -0
  29. package/src/client/board/NewTaskModal.tsx +40 -6
  30. package/src/client/board/TaskBoard.tsx +88 -8
  31. package/src/client/board/TaskCard.tsx +52 -2
  32. package/src/client/board/TaskDetail.tsx +124 -1
  33. package/src/client/board.module.css +96 -0
  34. package/src/client/host-api.ts +14 -4
  35. package/src/client/index.ts +11 -1
  36. package/src/client/locales.ts +70 -2
  37. package/src/core/controller.ts +69 -8
  38. package/src/core/store.ts +15 -0
  39. package/src/core/subtask.ts +286 -0
  40. package/src/core/tasks.ts +50 -2
  41. package/src/core/use-cases/task-archive.ts +46 -12
  42. package/src/core/use-cases/task-create.ts +42 -7
  43. package/src/core/use-cases/task-delete.ts +11 -1
  44. package/src/core/use-cases/task-parent.ts +64 -0
  45. package/src/core/use-cases/task-update.ts +3 -1
  46. package/src/host/agent-tools.ts +643 -0
  47. package/src/host-ledger.ts +323 -45
  48. package/src/host-runner.ts +141 -12
  49. package/src/host-service.ts +127 -10
  50. package/src/index.ts +181 -9
  51. package/src/protocol.ts +26 -2
@@ -1,4 +1,4 @@
1
- import { type ExecutionRecord, type TaskRecord } from './core/tasks.ts';
1
+ import { type ExecutionOutcome, type ExecutionRecord, type TaskRecord } from './core/tasks.ts';
2
2
  import { type TaskBoardAction, type TaskBoardSchedulerSnapshot } from './protocol.ts';
3
3
  import { type TaskPermission } from './core/handover.ts';
4
4
  export interface LedgerState {
@@ -9,6 +9,13 @@ export interface LedgerState {
9
9
  export interface OpenedRun {
10
10
  task: TaskRecord;
11
11
  execution: ExecutionRecord;
12
+ /**
13
+ * How the Host obtains this run's session. Absent or `session` launches a
14
+ * fresh (or reused) execution session; `teammate` means the run belongs to a
15
+ * team-mode cascade, where the Host asks the root's Lead session to spawn a
16
+ * teammate instead of launching a session of its own.
17
+ */
18
+ dispatch?: 'session' | 'teammate';
12
19
  }
13
20
  /** Minimal value copy used by the Host session monitor. */
14
21
  export interface OpenExecutionReference {
@@ -52,6 +59,16 @@ export type PowerShellProbe = (script: string, timeoutMs: number) => string | un
52
59
  * probe is injectable so the fallback chain is testable off Windows.
53
60
  */
54
61
  export declare function win32StartTimeMs(pid: number, probe?: PowerShellProbe): number | undefined;
62
+ /**
63
+ * Repair parent links after a load or an import: a link whose parent row is
64
+ * missing, cyclic, or names an archived parent while the child is on board is
65
+ * dropped (the task becomes a root) instead of dropping the task. A chain
66
+ * deeper than the current `maxSubtaskDepth` is deliberately KEPT: the limit
67
+ * gates new links, it never deletes a stored one, and every cascade walk is
68
+ * bounded by the limit anyway. The Host is the only writer of these links, so
69
+ * this is a last-resort guard against a hand-edited or imported document.
70
+ */
71
+ export declare function repairParentLinks(tasks: readonly TaskRecord[]): TaskRecord[];
55
72
  export declare class HostTaskLedger {
56
73
  private readonly now;
57
74
  private document;
@@ -65,9 +82,24 @@ export declare class HostTaskLedger {
65
82
  readonly schedulerFile: string;
66
83
  /** Session-default permission the confirmation gate compares against. */
67
84
  readonly sessionDefaultPermission: TaskPermission;
85
+ /**
86
+ * Deployment subtask depth limit (1..3): the lineage gate every write obeys.
87
+ * The settings card edits it live, so {@link setMaxSubtaskDepth} mutates it
88
+ * instead of remounting the row.
89
+ */
90
+ private depthLimit;
68
91
  constructor(dir?: string, now?: () => number, options?: {
69
92
  sessionDefaultPermission?: TaskPermission;
93
+ maxSubtaskDepth?: number;
70
94
  });
95
+ /** Current subtask depth limit (1..3). */
96
+ get maxSubtaskDepth(): number;
97
+ /**
98
+ * Apply a live settings edit of the subtask depth limit. A no-op when the
99
+ * normalized limit is unchanged, so a coarse volatile invalidation that
100
+ * changed nothing emits nothing.
101
+ */
102
+ setMaxSubtaskDepth(depth: number): void;
71
103
  /** Remove leftover *.tmp-* files from previous crashes or interrupted writes. */
72
104
  private cleanStaleTemporaryFiles;
73
105
  /** Revision + scheduler without any task cloning; feeds the SSE event frame. */
@@ -90,14 +122,66 @@ export declare class HostTaskLedger {
90
122
  dispose(): void;
91
123
  applyRequest(requestId: string, action: TaskBoardAction, initiator?: string): {
92
124
  state: LedgerState;
93
- run?: OpenedRun;
125
+ runs?: OpenedRun[];
94
126
  };
95
- openScheduled(taskId: string, nextRunAt: number | undefined, triggeredAt: number): OpenedRun | undefined;
127
+ /**
128
+ * Open the cascade one due schedule triggers: an empty array means nothing
129
+ * ran (already running, or a participant whose elevated permission is still
130
+ * unconfirmed), and the rule rolls to its next occurrence either way.
131
+ */
132
+ openScheduled(taskId: string, nextRunAt: number | undefined, triggeredAt: number): OpenedRun[];
96
133
  skipMissed(now: number): void;
97
134
  setScheduler(patch: Partial<TaskBoardSchedulerSnapshot>): void;
135
+ /**
136
+ * Settle one execution. A cascade participant first records its OWN outcome
137
+ * and only finalizes once every child execution in its run group has
138
+ * settled, so a parent card leaves 'running' with the whole tree's verdict
139
+ * rather than its own turn alone.
140
+ */
141
+ settle(taskId: string, executionId: string, outcome: ExecutionOutcome, error?: string): void;
98
142
  attachSession(taskId: string, executionId: string, sessionId: string): void;
99
- settle(taskId: string, executionId: string, outcome: 'succeeded' | 'failed' | 'cancelled', error?: string): void;
100
143
  private apply;
144
+ /**
145
+ * The binding that makes a run illegal, if any.
146
+ *
147
+ * A plain cascade launches one session per participant, so every participant
148
+ * carries its own resolved binding and each one gates the run. A team run
149
+ * launches only the Lead session: the Lead's binding gates it, while a
150
+ * subtask's OWN above-default pin cannot be applied to a teammate and is
151
+ * refused instead of being silently dropped (an inherited binding is the
152
+ * Lead's own and stays allowed once the Lead is confirmed).
153
+ * @param root - the task being run.
154
+ * @returns the first refusal, or undefined when the run may start.
155
+ */
156
+ private bindingRefusal;
157
+ /**
158
+ * The tasks a cascade from `rootId` would actually open executions for, with
159
+ * their effective execution targets resolved. Archived members never run, and
160
+ * a member that already has an open execution is skipped (a task cannot run
161
+ * twice), so the result is the participant set the launch will use.
162
+ */
163
+ private cascadeParticipants;
164
+ /** Whether a task or any of its subtasks still has an open execution. */
165
+ private subtreeHasOpenExecution;
166
+ /**
167
+ * Open one execution per cascade participant (the requested task and its
168
+ * on-board descendants, within the depth limit) under a single run group, and
169
+ * commit the ledger once. The returned runs are ordered root-first, so the
170
+ * Host launches the parent before its subtasks.
171
+ * @param root - the task the user ran.
172
+ * @param now - clock instant (ms epoch).
173
+ * @param initiator - the DSH session that asked for the run (audit only).
174
+ * @param rerun - true to reset the root to 'todo' before starting it.
175
+ * @param nextRunAt - when set, the root's schedule also rolls forward.
176
+ */
177
+ private startCascade;
178
+ /**
179
+ * Finalize every cascade parent that is ready, walking from `taskId` up the
180
+ * lineage: a member settles once its own outcome is recorded and none of its
181
+ * group children is still open, and its verdict folds those children in
182
+ * (failure dominates, then cancellation).
183
+ */
184
+ private settleCascade;
101
185
  private repairSchedules;
102
186
  private reconcileInterruptedStarts;
103
187
  /**
@@ -49,7 +49,29 @@ export declare class SessionLaunchError extends Error {
49
49
  * first, the provenance wrap then encloses the instruction. Plain tasks (no
50
50
  * freeze) keep the bare handover preamble + prompt.
51
51
  */
52
- export declare function promptText(task: TaskRecord): string;
52
+ /** One other execution member of a run, as named in the launched task's prompt. */
53
+ export interface PromptPeer {
54
+ /** Task id of the member. */
55
+ id: string;
56
+ /** Member title. */
57
+ title: string;
58
+ /** Teammate name when this member runs as a teammate. */
59
+ name?: string;
60
+ }
61
+ /** Execution-shape context appended to the launched task's prompt. */
62
+ export interface PromptContext {
63
+ /** The other members this run opens, excluding the launched task itself. */
64
+ peers?: readonly PromptPeer[];
65
+ /** True when those members run as teammates inside this session (Team Lead). */
66
+ team?: boolean;
67
+ }
68
+ /**
69
+ * Build the execution prompt for one task.
70
+ * @param task - the task being launched.
71
+ * @param context - the run's other members, for the execution-shape section.
72
+ * @returns the prompt text.
73
+ */
74
+ export declare function promptText(task: TaskRecord, context?: PromptContext): string;
53
75
  export declare class HostExecutionRunner {
54
76
  private readonly gateway;
55
77
  private readonly commands?;
@@ -77,7 +99,23 @@ export declare class HostExecutionRunner {
77
99
  */
78
100
  launch(task: TaskRecord, options?: {
79
101
  reuseSessionId?: string;
102
+ promptContext?: PromptContext;
80
103
  }): Promise<string>;
104
+ /**
105
+ * Fail closed when a reused session records a different Agent preset than
106
+ * the one this task pins.
107
+ *
108
+ * The fresh branch asserts the pin by passing `agentPreset` to
109
+ * `session/create`; the reuse branch must not call that method (it would
110
+ * create or re-adopt the very session the board only meant to continue), so
111
+ * the recorded value is read through `session/projections` — the documented
112
+ * read that resolves no Agent — and compared here (issue #1708). A session
113
+ * whose preset cannot be read is not treated as a match: silently continuing
114
+ * under an unknown composition is the failure this guards.
115
+ * @param sessionId - the session this execution continues in.
116
+ * @param pinned - the preset id the task pins, when it pins one.
117
+ */
118
+ private assertReusedPreset;
81
119
  /**
82
120
  * Re-assert the pinned execution contract on a session and queue the task
83
121
  * prompt. Shared by the fresh-session and reuse paths so both apply exactly
@@ -4,6 +4,33 @@ import { HostExecutionRunner, type SessionCommandDispatcher, type TaskBoardWorks
4
4
  import { PowerInhibitor } from './power-inhibitor.ts';
5
5
  import { type TaskBoardAction, type TaskBoardEventPayload, type TaskBoardSnapshot } from './protocol.ts';
6
6
  import type { TaskPermission } from './core/handover.ts';
7
+ /** One teammate the Host asks the Agent Teams service to spawn for a team run. */
8
+ export interface TeamSpawnInput {
9
+ /** Session id of the run's Team Lead (the root task's execution session). */
10
+ leadSessionId: string;
11
+ /** Immutable lower-kebab-case teammate name, unique inside the Team. */
12
+ name: string;
13
+ /** Short description of the delegated responsibility. */
14
+ description: string;
15
+ /** The subtask's execution prompt. */
16
+ prompt: string;
17
+ }
18
+ /** Result of one teammate spawn attempt. */
19
+ export interface TeamSpawnResult {
20
+ /** The teammate's session id when it reached the active edge. */
21
+ sessionId?: string;
22
+ /** Why the spawn failed: a thrown call, or a member that settled as failed. */
23
+ error?: string;
24
+ }
25
+ /**
26
+ * The Agent Teams capability the Host needs for team-mode runs. The plugin
27
+ * builds it from the optional `agentTeams` service; it is absent when this
28
+ * deployment does not serve that service, in which case a team run is refused
29
+ * instead of silently degrading into a plain cascade.
30
+ */
31
+ export interface TaskBoardTeamDispatcher {
32
+ spawn(input: TeamSpawnInput): Promise<TeamSpawnResult>;
33
+ }
7
34
  export declare class TaskBoardHostService {
8
35
  readonly ledger: HostTaskLedger;
9
36
  readonly runner: HostExecutionRunner;
@@ -23,6 +50,7 @@ export declare class TaskBoardHostService {
23
50
  */
24
51
  private idleSessionIds;
25
52
  private preventIdleSleep;
53
+ private readonly team;
26
54
  private lastPowerJson;
27
55
  private readonly now;
28
56
  constructor(gateway: TypertGateway, options?: {
@@ -32,6 +60,8 @@ export declare class TaskBoardHostService {
32
60
  commandDispatcher?: SessionCommandDispatcher;
33
61
  workspaceRegistry?: TaskBoardWorkspaceRegistry;
34
62
  sessionDefaultPermission?: TaskPermission;
63
+ maxSubtaskDepth?: number;
64
+ team?: TaskBoardTeamDispatcher;
35
65
  });
36
66
  start(): void;
37
67
  setConfiguration(active: boolean, preventIdleSleep: boolean): void;
@@ -42,11 +72,28 @@ export declare class TaskBoardHostService {
42
72
  apply(requestId: string, action: TaskBoardAction, initiator?: string): TaskBoardSnapshot;
43
73
  dispose(): void;
44
74
  private launch;
75
+ private scheduleTeammate;
76
+ /**
77
+ * Spawn one teammate inside the Lead session and attach the teammate's
78
+ * session to the subtask execution, so the existing session monitor settles
79
+ * it from the teammate's own turn like any other execution. A spawn that
80
+ * fails settles the subtask as failed immediately, which then folds into the
81
+ * Lead's cascade verdict.
82
+ */
83
+ private spawnTeammate;
45
84
  private pollSessions;
46
85
  /** Reuse the session list this poll already fetched: one list RPC per tick, not 1 + E. */
47
86
  private reconcileExecutions;
48
87
  private tickSchedule;
49
88
  private armedSchedules;
89
+ /**
90
+ * Launch one run set. The root goes first and receives the run shape in its
91
+ * prompt (which members this run opens, and how they run); every plain-cascade
92
+ * member then launches on its own so one refused participant cannot hold the
93
+ * others back. A team run's members are spawned inside the root's Lead
94
+ * session instead, once that session exists.
95
+ */
96
+ private dispatchRuns;
50
97
  private scheduleLaunch;
51
98
  private schedulePoll;
52
99
  private scheduleTick;
@@ -7,21 +7,25 @@
7
7
  */
8
8
  import type { Context, Volatile } from '@deepseek-ai/cordis';
9
9
  import z from '@deepseek-ai/schemastery';
10
+ import type { ToolDefinition } from '@deepseek-ai/dsh-tools';
10
11
  import type { LlmRuntime } from '@deepseek-ai/dsh-llm';
12
+ import { type TaskBoardTeamDispatcher } from './host-service.ts';
11
13
  import { type TaskPermission } from './core/tasks.ts';
12
14
  /** Default environment variable holding the authenticated proxy token. */
13
15
  export declare const DEFAULT_PROXY_TOKEN_ENV = "DSH_TASK_BOARD_PROXY_TOKEN";
14
16
  export declare const inject: string[];
15
17
  /** Model-facing announcement: plugin presence, capabilities, and limits. */
16
- export declare const TASK_BOARD_GUIDANCE = "\u672C\u673A\u5DF2\u5B89\u88C5 dsh-task-board \u63D2\u4EF6\uFF08DSH Web GUI \u7684\u4EFB\u52A1\u770B\u677F\uFF09\uFF1A\u4FA7\u8FB9\u680F\u300C\u4EFB\u52A1\u770B\u677F\u300D\u5165\u53E3\uFF1B\u5728 dsh-web \u63D2\u4EF6\u5168\u5BB6\u6876\u4ED3\u5E93\uFF08packages/dsh-task-board\uFF09\u7EDF\u4E00\u7EF4\u62A4\uFF0C\u7ECF\u805A\u5408\u5305 web-ui-all \u4E00\u952E\u5B89\u88C5\u3002\u80FD\u529B\uFF1A\u591A\u5217\u770B\u677F\u7BA1\u7406\u4EFB\u52A1\uFF1BHost \u6743\u5A01\u8D26\u672C\uFF1B\u5173\u95ED\u6D4F\u89C8\u5668\u540E\u4ECD\u7531 Host \u6267\u884C\u548C\u7ED3\u7B97\uFF1B\u4EFB\u52A1\u53EF\u9489\u4F4F\u5DE5\u4F5C\u533A\u3001agent \u9884\u8BBE\u548C\u6743\u9650\uFF1B\u652F\u6301 Host \u672C\u5730\u65F6\u533A\u7684 5 \u6BB5 cron\uFF0C\u9519\u8FC7\u7684\u89E6\u53D1\u70B9\u4E0D\u8865\u8DD1\uFF1B\u53EF\u9009\u4E14\u9ED8\u8BA4\u5173\u95ED\u7684\u7A7A\u95F2\u7CFB\u7EDF\u7761\u7720\u4FDD\u62A4\u5141\u8BB8\u5C4F\u5E55\u7184\u706D\uFF0C\u4F46\u4E0D\u627F\u8BFA\u62E6\u622A\u5408\u76D6\u3001\u624B\u52A8\u7761\u7720\u3001\u4F11\u7720\u3001\u5173\u673A\u6216\u5524\u9192\u5DF2\u7761\u7720\u673A\u5668\u3002\u6267\u884C\u6D88\u8017 API \u989D\u5EA6\u3002\u7528\u6237\u63D0\u5230\u300C\u4EFB\u52A1\u770B\u677F / \u770B\u677F / \u5B9A\u65F6\u4EFB\u52A1\u300D\u65F6\u5373\u6307\u672C\u63D2\u4EF6\uFF0C\u8BF7\u636E\u6B64\u534F\u4F5C\u3002\u82E5\u4F60\u540C\u65F6\u7528 todo_write \u7EF4\u62A4\u4F1A\u8BDD\u9876\u90E8\u7684\u53EF\u89C1\u8BA1\u5212\u5217\u8868\uFF0C\u6700\u7EC8\u56DE\u590D\u524D\u5FC5\u987B\u518D\u6B21\u8C03\u7528 todo_write \u6536\u5C3E\uFF1A\u6CA1\u6709\u5269\u4F59\u5DE5\u4F5C\u65F6\u4E0D\u8981\u4FDD\u7559 in_progress\uFF0C\u5DF2\u5B8C\u6210\u7684\u6700\u540E\u4E00\u6B65\u8981\u6807\u4E3A completed\u3002";
18
+ export declare const TASK_BOARD_GUIDANCE = "\u672C\u673A\u5DF2\u5B89\u88C5 dsh-task-board \u63D2\u4EF6\uFF08DSH Web GUI \u7684\u4EFB\u52A1\u770B\u677F\uFF09\uFF1A\u4FA7\u8FB9\u680F\u300C\u4EFB\u52A1\u770B\u677F\u300D\u5165\u53E3\uFF1B\u5728 dsh-web \u63D2\u4EF6\u5168\u5BB6\u6876\u4ED3\u5E93\uFF08packages/dsh-task-board\uFF09\u7EDF\u4E00\u7EF4\u62A4\uFF0C\u7ECF\u805A\u5408\u5305 web-ui-all \u4E00\u952E\u5B89\u88C5\u3002\u80FD\u529B\uFF1A\u591A\u5217\u770B\u677F\u7BA1\u7406\u4EFB\u52A1\uFF1BHost \u6743\u5A01\u8D26\u672C\uFF1B\u5173\u95ED\u6D4F\u89C8\u5668\u540E\u4ECD\u7531 Host \u6267\u884C\u548C\u7ED3\u7B97\uFF1B\u4EFB\u52A1\u53EF\u9489\u4F4F\u5DE5\u4F5C\u533A\u3001agent \u9884\u8BBE\u548C\u6743\u9650\uFF1B\u4EFB\u52A1\u53EF\u5EFA\u5B50\u4EFB\u52A1\uFF08\u6DF1\u5EA6\u4E0A\u9650\u53EF\u914D 1..3\uFF0C\u9ED8\u8BA4 1 \u5C42\uFF0C\u5B50\u4EFB\u52A1\u4E0D\u80FD\u518D\u5E26\u5B50\u4EFB\u52A1\uFF09\uFF0C\u6267\u884C\u7236\u4EFB\u52A1\u4F1A\u5E76\u53D1\u6267\u884C\u5176\u5B50\u4EFB\u52A1\u6811\uFF0C\u5B50\u4EFB\u52A1\u53EF\u5355\u72EC\u8986\u76D6\u6743\u9650\u4E0E\u6A21\u578B\uFF1B\u53E6\u6CE8\u518C task_board_* agent \u5DE5\u5177\uFF08list/get/create/update/set_parent/run/manage/schedule\uFF09\uFF0C\u4EFB\u4F55\u4F1A\u8BDD\u90FD\u53EF\u76F4\u63A5\u8BFB\u5199\u770B\u677F\u3001\u5B50\u4EFB\u52A1\u4E0E\u5B9A\u65F6\u8BA1\u5212\uFF0C\u4F46\u8FD0\u884C\u4EFB\u52A1\u4F1A\u771F\u5B9E\u6267\u884C\u5E76\u6D88\u8017\u989D\u5EA6\uFF0C\u9AD8\u4E8E\u4F1A\u8BDD\u9ED8\u8BA4\u6743\u9650\u7684\u7ED1\u5B9A\u4ECD\u5FC5\u987B\u7531\u7528\u6237\u5728 GUI \u4EBA\u5DE5\u786E\u8BA4\uFF08\u5DE5\u5177\u523B\u610F\u4E0D\u63D0\u4F9B\u786E\u8BA4\u80FD\u529B\uFF09\uFF1B\u652F\u6301 Host \u672C\u5730\u65F6\u533A\u7684 5 \u6BB5 cron\uFF0C\u9519\u8FC7\u7684\u89E6\u53D1\u70B9\u4E0D\u8865\u8DD1\uFF1B\u53EF\u9009\u4E14\u9ED8\u8BA4\u5173\u95ED\u7684\u7A7A\u95F2\u7CFB\u7EDF\u7761\u7720\u4FDD\u62A4\u5141\u8BB8\u5C4F\u5E55\u7184\u706D\uFF0C\u4F46\u4E0D\u627F\u8BFA\u62E6\u622A\u5408\u76D6\u3001\u624B\u52A8\u7761\u7720\u3001\u4F11\u7720\u3001\u5173\u673A\u6216\u5524\u9192\u5DF2\u7761\u7720\u673A\u5668\u3002\u6267\u884C\u6D88\u8017 API \u989D\u5EA6\u3002\u7528\u6237\u63D0\u5230\u300C\u4EFB\u52A1\u770B\u677F / \u770B\u677F / \u5B9A\u65F6\u4EFB\u52A1\u300D\u65F6\u5373\u6307\u672C\u63D2\u4EF6\uFF0C\u8BF7\u636E\u6B64\u534F\u4F5C\u3002\u82E5\u4F60\u540C\u65F6\u7528 todo_write \u7EF4\u62A4\u4F1A\u8BDD\u9876\u90E8\u7684\u53EF\u89C1\u8BA1\u5212\u5217\u8868\uFF0C\u6700\u7EC8\u56DE\u590D\u524D\u5FC5\u987B\u518D\u6B21\u8C03\u7528 todo_write \u6536\u5C3E\uFF1A\u6CA1\u6709\u5269\u4F59\u5DE5\u4F5C\u65F6\u4E0D\u8981\u4FDD\u7559 in_progress\uFF0C\u5DF2\u5B8C\u6210\u7684\u6700\u540E\u4E00\u6B65\u8981\u6807\u4E3A completed\u3002";
17
19
  /**
18
20
  * Plugin config, validated by the same-named schemastery schema.
19
21
  *
20
22
  * This schema IS the board's settings page: the 0.1.7 Host serves one
21
23
  * configuration form per profile entry from the entry's own Config, so the
22
- * three fields the browser card edits are marked volatile — the Loader commits
24
+ * fields the browser card edits are marked volatile — the Loader commits
23
25
  * an edit into the running fiber's references without remounting the row (no
24
- * settings document of the plugin's own exists any more).
26
+ * settings document of the plugin's own exists any more). A field without the
27
+ * marker is refused by the settings write path, which fences writes on
28
+ * volatility.
25
29
  */
26
30
  export interface Config {
27
31
  /**
@@ -48,7 +52,25 @@ export interface Config {
48
52
  * confirmation before it may run; cron refuses unconfirmed cards.
49
53
  */
50
54
  sessionDefaultPermission?: TaskPermission;
55
+ /**
56
+ * Subtask depth limit, 1..3 (default 1). At 1 a task may carry one level of
57
+ * subtasks and a subtask cannot be given subtasks of its own; raising it
58
+ * allows deeper trees. Every extra level multiplies the sessions one run
59
+ * of the root task opens, so the ceiling is deliberately small.
60
+ */
61
+ maxSubtaskDepth?: Volatile<number>;
62
+ /**
63
+ * Continuable-subagent provider the Agent Teams service uses to compose a
64
+ * teammate. Matches the Agent Teams tool plugin's `freshProvider` default;
65
+ * only team-mode runs use it.
66
+ */
67
+ teamProvider?: string;
51
68
  }
69
+ /**
70
+ * Continuable-subagent provider for fresh teammates, matching the Agent Teams
71
+ * tool plugin's own `freshProvider` default.
72
+ */
73
+ export declare const DEFAULT_TEAM_PROVIDER = "spawn";
52
74
  /**
53
75
  * The schema is left to inference rather than annotated with `z<Config>`: a
54
76
  * volatile field's parsed output is a `Volatile` reference while its accepted
@@ -62,6 +84,8 @@ export declare const Config: z<Schemastery.ObjectS<NoInfer<{
62
84
  trustedProxyHosts: z<string[], string[], "defined">;
63
85
  proxyTokenEnv: z<string, string, "defined">;
64
86
  sessionDefaultPermission: z<"read-only" | "workspace-write" | "danger-full-access", "read-only" | "workspace-write" | "danger-full-access", "defined">;
87
+ maxSubtaskDepth: z<number, number, "volatile-defined">;
88
+ teamProvider: z<string, string, "defined">;
65
89
  }>>, Schemastery.ObjectT<NoInfer<{
66
90
  announceToAgent: z<boolean, boolean, "volatile-defined">;
67
91
  enabled: z<boolean, boolean, "volatile-defined">;
@@ -69,6 +93,8 @@ export declare const Config: z<Schemastery.ObjectS<NoInfer<{
69
93
  trustedProxyHosts: z<string[], string[], "defined">;
70
94
  proxyTokenEnv: z<string, string, "defined">;
71
95
  sessionDefaultPermission: z<"read-only" | "workspace-write" | "danger-full-access", "read-only" | "workspace-write" | "danger-full-access", "defined">;
96
+ maxSubtaskDepth: z<number, number, "volatile-defined">;
97
+ teamProvider: z<string, string, "defined">;
72
98
  }>>, "plain">;
73
99
  declare module '@deepseek-ai/cordis' {
74
100
  interface Events {
@@ -100,6 +126,58 @@ export declare function resolveProxyAccess(config: Config | undefined, env?: Nod
100
126
  trustedProxyHosts: string[];
101
127
  proxyToken?: string;
102
128
  };
129
+ /** The registry face the agent tools register into. */
130
+ interface AgentToolRegistry {
131
+ register(definition: ToolDefinition): () => void;
132
+ }
133
+ /**
134
+ * Resolve the optional agent-tool registry. The board deliberately does not
135
+ * inject it: a deployment whose runtime serves no registry must still mount the
136
+ * whole board and lose only the agent-tool surface, the same tolerance the
137
+ * optional llm service gets.
138
+ * @param ctx - the plugin context.
139
+ * @returns the registry, or undefined when this deployment serves none.
140
+ */
141
+ export declare function resolveToolRegistry(ctx: Context): AgentToolRegistry | undefined;
142
+ /** The subset of the Agent Teams service this plugin drives. */
143
+ interface AgentTeamsFace {
144
+ spawnTeammate(caller: unknown, request: {
145
+ name: string;
146
+ description: string;
147
+ prompt: Array<{
148
+ type: 'text';
149
+ text: string;
150
+ }>;
151
+ context: 'fresh' | 'fork';
152
+ provider: string;
153
+ signal: AbortSignal;
154
+ }): Promise<{
155
+ member?: {
156
+ id?: string;
157
+ phase?: string;
158
+ error?: string;
159
+ };
160
+ }>;
161
+ }
162
+ /**
163
+ * Resolve the optional Agent Teams service. Like the tool registry and the llm
164
+ * runtime, it is deliberately not injected: a deployment without Agent Teams
165
+ * must still mount the whole board and only lose team-mode runs.
166
+ * @param ctx - the plugin context.
167
+ * @returns the service, or undefined when this deployment serves none.
168
+ */
169
+ export declare function resolveAgentTeams(ctx: Context): AgentTeamsFace | undefined;
170
+ /**
171
+ * Build the team dispatcher: the run's Lead session spawns one teammate per
172
+ * subtask through the Agent Teams service, and the teammate's session id is
173
+ * attached to that subtask's execution so the ordinary session monitor settles
174
+ * it. Returns undefined when the deployment serves no Agent Teams service or
175
+ * no live-agent registry, which makes a team run fail closed.
176
+ * @param ctx - the plugin context.
177
+ * @param provider - the continuable-subagent provider for fresh teammates.
178
+ * @returns the dispatcher, or undefined when team runs cannot be served.
179
+ */
180
+ export declare function buildTeamDispatcher(ctx: Context, provider: string): TaskBoardTeamDispatcher | undefined;
103
181
  /**
104
182
  * Read the optional `llm` service. The board deliberately does not inject it:
105
183
  * a deployment without a model must still mount the board, and the parse route
@@ -112,8 +190,8 @@ export declare function resolveLlmRuntime(ctx: Context): LlmRuntime | undefined;
112
190
  * Activate the board's host half: ledger, routes, cron scheduler, power
113
191
  * inhibitor, and the model-facing announcement.
114
192
  *
115
- * The effective settings are the config the Host hands this row. The three
116
- * fields the browser card edits are schema-volatile, so the Loader commits an
193
+ * The effective settings are the config the Host hands this row. The fields
194
+ * the browser card edits are schema-volatile, so the Loader commits an
117
195
  * edit into the running fiber's references instead of remounting the row —
118
196
  * `sync` therefore reads them at use time and follows
119
197
  * `loader/volatile-update`, the event the Loader emits once it has committed
@@ -29,6 +29,16 @@ export interface TaskBoardSnapshot {
29
29
  power: TaskBoardPowerSnapshot;
30
30
  /** Session-default permission the confirmation gate compares against. */
31
31
  sessionDefaultPermission?: TaskPermission;
32
+ /**
33
+ * Deployment subtask depth limit (1..3). The browser mirrors it to enable or
34
+ * disable the subtask controls; the Host ledger is the authority.
35
+ */
36
+ maxSubtaskDepth?: number;
37
+ /**
38
+ * Whether this deployment serves the Agent Teams service, so a task may opt
39
+ * into team execution (Team Lead session plus one teammate per subtask).
40
+ */
41
+ teamRunAvailable?: boolean;
32
42
  }
33
43
  /** SSE event frame: revision/scheduler/power only, never the task list. */
34
44
  export interface TaskBoardEventPayload {
@@ -96,6 +106,10 @@ export type TaskBoardAction = {
96
106
  } | {
97
107
  kind: 'confirm-permission';
98
108
  taskId: string;
109
+ } | {
110
+ kind: 'set-parent';
111
+ taskId: string;
112
+ parentId: string | null;
99
113
  };
100
114
  export interface TaskBoardActionEnvelope {
101
115
  requestId: string;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@linxin666/dsh-client-ui-task-board",
3
3
  "description": "Host-authoritative task board for the DSH Web GUI with real session execution, Host cron scheduling, and optional cross-platform idle-sleep protection; mounted without DSH source changes.",
4
- "version": "0.4.0",
4
+ "version": "0.4.2",
5
5
  "icon": "icon.svg",
6
6
  "type": "module",
7
7
  "main": "lib/index.js",
@@ -24,7 +24,7 @@
24
24
  },
25
25
  "dsh": {
26
26
  "engines": {
27
- "dsh": ">=0.1.7-rc.1"
27
+ "dsh": ">=0.1.7-rc.2"
28
28
  },
29
29
  "bundle": {
30
30
  "patch": "./cordis.patch.yml"
@@ -43,7 +43,7 @@
43
43
  }
44
44
  },
45
45
  "peerDependencies": {
46
- "@deepseek-ai/dsh": ">=0.1.7-rc.1",
46
+ "@deepseek-ai/dsh": ">=0.1.7-rc.2",
47
47
  "react": "^18.2.0"
48
48
  },
49
49
  "dependencies": {
@@ -51,24 +51,26 @@
51
51
  },
52
52
  "devDependencies": {
53
53
  "@deepseek-ai/cordis": "^4.0.4",
54
- "@deepseek-ai/dsh-agent": "^0.1.7-rc.1",
55
- "@deepseek-ai/dsh-agent-preset-registry": "^0.1.7-rc.1",
56
- "@deepseek-ai/dsh-api-gateway": "^0.1.7-rc.1",
57
- "@deepseek-ai/dsh-api-remotes": "^0.1.7-rc.1",
58
- "@deepseek-ai/dsh-api-session-controller": "^0.1.7-rc.1",
59
- "@deepseek-ai/dsh-api-workspace-controller": "^0.1.7-rc.1",
60
- "@deepseek-ai/dsh-client-connection": "^0.1.7-rc.1",
61
- "@deepseek-ai/dsh-client-locale": "^0.1.7-rc.1",
62
- "@deepseek-ai/dsh-client-store": "^0.1.7-rc.1",
63
- "@deepseek-ai/dsh-client-ui-renderer": "^0.1.7-rc.1",
64
- "@deepseek-ai/dsh-client-ui-settings": "^0.1.7-rc.1",
65
- "@deepseek-ai/dsh-client-ui-slots": "^0.1.7-rc.1",
66
- "@deepseek-ai/dsh-client-ui-workspace": "^0.1.7-rc.1",
67
- "@deepseek-ai/dsh-commands": "^0.1.7-rc.1",
68
- "@deepseek-ai/dsh-host-webserver": "^0.1.7-rc.1",
69
- "@deepseek-ai/dsh-settings": "^0.1.7-rc.1",
70
- "@deepseek-ai/dsh-system-prompt": "^0.1.7-rc.1",
71
- "@deepseek-ai/dsh-workspace": "^0.1.7-rc.1",
54
+ "@deepseek-ai/dsh-agent": "^0.1.7-rc.2",
55
+ "@deepseek-ai/dsh-agent-preset-registry": "^0.1.7-rc.2",
56
+ "@deepseek-ai/dsh-api-gateway": "^0.1.7-rc.2",
57
+ "@deepseek-ai/dsh-api-remotes": "^0.1.7-rc.2",
58
+ "@deepseek-ai/dsh-api-session-controller": "^0.1.7-rc.2",
59
+ "@deepseek-ai/dsh-api-workspace-controller": "^0.1.7-rc.2",
60
+ "@deepseek-ai/dsh-client-connection": "^0.1.7-rc.2",
61
+ "@deepseek-ai/dsh-client-locale": "^0.1.7-rc.2",
62
+ "@deepseek-ai/dsh-client-store": "^0.1.7-rc.2",
63
+ "@deepseek-ai/dsh-client-ui-renderer": "^0.1.7-rc.2",
64
+ "@deepseek-ai/dsh-client-ui-settings": "^0.1.7-rc.2",
65
+ "@deepseek-ai/dsh-client-ui-slots": "^0.1.7-rc.2",
66
+ "@deepseek-ai/dsh-client-ui-workspace": "^0.1.7-rc.2",
67
+ "@deepseek-ai/dsh-commands": "^0.1.7-rc.2",
68
+ "@deepseek-ai/dsh-host-webserver": "^0.1.7-rc.2",
69
+ "@deepseek-ai/dsh-llm": "^0.1.7-rc.2",
70
+ "@deepseek-ai/dsh-settings": "^0.1.7-rc.2",
71
+ "@deepseek-ai/dsh-system-prompt": "^0.1.7-rc.2",
72
+ "@deepseek-ai/dsh-tools": "^0.1.7-rc.2",
73
+ "@deepseek-ai/dsh-workspace": "^0.1.7-rc.2",
72
74
  "@types/node": "^22.20.0",
73
75
  "@types/react": "~18.3.1",
74
76
  "@types/react-dom": "^18.3.5",
@@ -9,8 +9,33 @@ import type { SnapshotStore } from '@deepseek-ai/dsh-client-store'
9
9
  import type { ConfigForm } from '@deepseek-ai/dsh-client-ui-settings/client'
10
10
  import { useEffect, useState } from 'react'
11
11
  import type { TaskBoardPowerSnapshot } from '../protocol.ts'
12
- import { PluginSettingsCard, BooleanField } from './PluginSettingsCard.tsx'
13
- import { CardForm, booleanField, type CardActions, type CardShell, type FieldState as CardFieldState } from './settings-form.ts'
12
+ import { PluginSettingsCard, BooleanField, ChoiceField } from './PluginSettingsCard.tsx'
13
+ import { SUBTASK_DEPTH_MAX, SUBTASK_DEPTH_MIN } from '../core/subtask.ts'
14
+ import { CardForm, booleanField, type CardActions, type CardShell, type FieldSpec, type FieldState as CardFieldState } from './settings-form.ts'
15
+
16
+ /** The depth choices the card offers, derived from the supported range. */
17
+ const SUBTASK_DEPTH_CHOICES: readonly string[] = Array.from(
18
+ { length: SUBTASK_DEPTH_MAX - SUBTASK_DEPTH_MIN + 1 },
19
+ (_, index) => String(SUBTASK_DEPTH_MIN + index),
20
+ )
21
+
22
+ /**
23
+ * The depth field: a choice among the supported levels whose draft text is a
24
+ * number, because the Host schema (`maxSubtaskDepth`) is numeric. A draft
25
+ * outside the range blocks the save instead of staging a value the Host
26
+ * refuses.
27
+ */
28
+ function subtaskDepthField(): FieldSpec {
29
+ return {
30
+ field: 'maxSubtaskDepth',
31
+ format: value => typeof value === 'number' && Number.isInteger(value) ? String(value) : '',
32
+ parse: (text) => {
33
+ const trimmed = text.trim()
34
+ if (trimmed === '') return { kind: 'clear' }
35
+ return SUBTASK_DEPTH_CHOICES.includes(trimmed) ? { kind: 'set', value: Number(trimmed) } : undefined
36
+ },
37
+ }
38
+ }
14
39
 
15
40
  /** The task-board fields this card edits (the namespace's full schema). */
16
41
  export interface TaskBoardSettings {
@@ -20,6 +45,8 @@ export interface TaskBoardSettings {
20
45
  announceToAgent?: boolean
21
46
  /** Prevent host idle sleep while sessions run or schedules are armed. */
22
47
  preventIdleSleep?: boolean
48
+ /** Subtask depth limit (1..3); 1 means a single level of subtasks. */
49
+ maxSubtaskDepth?: number
23
50
  }
24
51
 
25
52
  /** What the task-board card renders. */
@@ -30,6 +57,8 @@ export interface TaskBoardSettingsCardState extends CardShell {
30
57
  announceToAgent: CardFieldState
31
58
  /** Idle-system-sleep protection flag. */
32
59
  preventIdleSleep: CardFieldState
60
+ /** Subtask depth limit field. */
61
+ maxSubtaskDepth: CardFieldState
33
62
  }
34
63
 
35
64
  /** The registration-side face the card's slot entry injects. */
@@ -51,6 +80,7 @@ export class TaskBoardSettingsCardController {
51
80
  booleanField('enabled'),
52
81
  booleanField('announceToAgent'),
53
82
  booleanField('preventIdleSleep'),
83
+ subtaskDepthField(),
54
84
  ])
55
85
  this.store = this.form.bind(() => this.projection())
56
86
  }
@@ -61,6 +91,7 @@ export class TaskBoardSettingsCardController {
61
91
  enabled: this.form.field('enabled'),
62
92
  announceToAgent: this.form.field('announceToAgent'),
63
93
  preventIdleSleep: this.form.field('preventIdleSleep'),
94
+ maxSubtaskDepth: this.form.field('maxSubtaskDepth'),
64
95
  }
65
96
  }
66
97
 
@@ -102,7 +133,7 @@ export function TaskBoardSettingsCard(props: TaskBoardSettingsCardProps) {
102
133
  // one frame on subscribe; polling the full /state snapshot every 5 s
103
134
  // re-cloned and re-serialized the whole ledger server-side for one field.
104
135
  let live = true
105
- const events = new EventSource('/api/task-board/events')
136
+ const events = new EventSource('api/task-board/events')
106
137
  events.onmessage = (message: MessageEvent<string>): void => {
107
138
  try {
108
139
  const frame = JSON.parse(message.data) as { power?: TaskBoardPowerSnapshot }
@@ -167,6 +198,20 @@ export function TaskBoardSettingsCard(props: TaskBoardSettingsCardProps) {
167
198
  onEdit={(text) => { props.edit('preventIdleSleep', text) }}
168
199
  onReset={() => { props.resetField('preventIdleSleep') }}
169
200
  />
201
+ <ChoiceField
202
+ id="settings-task-board-subtask-depth"
203
+ label={t('settings.maxSubtaskDepth')}
204
+ hint={t('settings.maxSubtaskDepthHint')}
205
+ inheritLabel={t('settings.inherit')}
206
+ choices={SUBTASK_DEPTH_CHOICES.map(value => ({
207
+ value,
208
+ label: t('settings.maxSubtaskDepthOption', { depth: value }),
209
+ }))}
210
+ {...fieldProps}
211
+ {...state.maxSubtaskDepth}
212
+ onEdit={(text) => { props.edit('maxSubtaskDepth', text) }}
213
+ onReset={() => { props.resetField('maxSubtaskDepth') }}
214
+ />
170
215
  <p>
171
216
  {t('settings.powerStatus', {
172
217
  platform: power?.platform ?? t('settings.powerUnknown'),