@linxin666/dsh-client-ui-task-board 0.4.1 → 0.4.3

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 (69) hide show
  1. package/README.i18n.yaml +2 -2
  2. package/README.md +28 -3
  3. package/README.zh.md +28 -3
  4. package/lib/client.js +1142 -467
  5. package/lib/client.js.map +1 -1
  6. package/lib/index.js +2066 -1475
  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/index.d.ts +15 -1
  12. package/lib/types/client/locales.d.ts +33 -0
  13. package/lib/types/client/native-panel.d.ts +62 -0
  14. package/lib/types/client/settings-entry-form.d.ts +49 -0
  15. package/lib/types/core/controller.d.ts +71 -3
  16. package/lib/types/core/subtask.d.ts +122 -0
  17. package/lib/types/core/tasks.d.ts +46 -3
  18. package/lib/types/core/use-cases/task-archive.d.ts +10 -13
  19. package/lib/types/core/use-cases/task-create.d.ts +13 -4
  20. package/lib/types/core/use-cases/task-delete.d.ts +7 -1
  21. package/lib/types/core/use-cases/task-parent.d.ts +21 -0
  22. package/lib/types/core/use-cases/task-update.d.ts +1 -1
  23. package/lib/types/host/agent-tools.d.ts +27 -0
  24. package/lib/types/host-ledger.d.ts +97 -4
  25. package/lib/types/host-runner.d.ts +39 -1
  26. package/lib/types/host-service.d.ts +98 -5
  27. package/lib/types/index.d.ts +93 -5
  28. package/lib/types/mount-once.d.ts +38 -7
  29. package/lib/types/protocol.d.ts +14 -0
  30. package/package.json +26 -21
  31. package/src/client/TaskBoardSettingsCard.tsx +48 -3
  32. package/src/client/board/LinkSubtaskModal.tsx +82 -0
  33. package/src/client/board/NewTaskModal.tsx +40 -6
  34. package/src/client/board/TaskBoard.tsx +88 -8
  35. package/src/client/board/TaskCard.tsx +52 -2
  36. package/src/client/board/TaskDetail.tsx +124 -1
  37. package/src/client/board.module.css +112 -131
  38. package/src/client/host-api.ts +14 -4
  39. package/src/client/index.ts +56 -18
  40. package/src/client/locales.ts +70 -2
  41. package/src/client/native-panel.tsx +124 -0
  42. package/src/client/settings-entry-form.ts +142 -0
  43. package/src/core/controller.ts +128 -10
  44. package/src/core/store.ts +15 -0
  45. package/src/core/subtask.ts +286 -0
  46. package/src/core/tasks.ts +50 -2
  47. package/src/core/use-cases/task-archive.ts +46 -12
  48. package/src/core/use-cases/task-create.ts +42 -7
  49. package/src/core/use-cases/task-delete.ts +11 -1
  50. package/src/core/use-cases/task-parent.ts +64 -0
  51. package/src/core/use-cases/task-update.ts +3 -1
  52. package/src/host/agent-tools.ts +643 -0
  53. package/src/host-ledger.ts +343 -45
  54. package/src/host-routes.ts +37 -1
  55. package/src/host-runner.ts +141 -12
  56. package/src/host-service.ts +265 -32
  57. package/src/index.ts +203 -9
  58. package/src/mount-once.ts +107 -13
  59. package/src/protocol.ts +26 -2
  60. package/lib/types/client/board-mount.d.ts +0 -23
  61. package/lib/types/client/body-mutations.d.ts +0 -45
  62. package/lib/types/client/panel-mount-core.d.ts +0 -56
  63. package/lib/types/client/sidebar-entry-core.d.ts +0 -55
  64. package/lib/types/client/sidebar-entry.d.ts +0 -29
  65. package/src/client/board-mount.tsx +0 -43
  66. package/src/client/body-mutations.ts +0 -133
  67. package/src/client/panel-mount-core.ts +0 -155
  68. package/src/client/sidebar-entry-core.ts +0 -237
  69. package/src/client/sidebar-entry.ts +0 -53
@@ -15,6 +15,8 @@ export interface TaskBoardSettings {
15
15
  announceToAgent?: boolean;
16
16
  /** Prevent host idle sleep while sessions run or schedules are armed. */
17
17
  preventIdleSleep?: boolean;
18
+ /** Subtask depth limit (1..3); 1 means a single level of subtasks. */
19
+ maxSubtaskDepth?: number;
18
20
  }
19
21
  /** What the task-board card renders. */
20
22
  export interface TaskBoardSettingsCardState extends CardShell {
@@ -24,6 +26,8 @@ export interface TaskBoardSettingsCardState extends CardShell {
24
26
  announceToAgent: CardFieldState;
25
27
  /** Idle-system-sleep protection flag. */
26
28
  preventIdleSleep: CardFieldState;
29
+ /** Subtask depth limit field. */
30
+ maxSubtaskDepth: CardFieldState;
27
31
  }
28
32
  /** The registration-side face the card's slot entry injects. */
29
33
  export interface TaskBoardSettingsCardFace extends CardActions {
@@ -0,0 +1,11 @@
1
+ import type { BoardController } from '../../core/controller.ts';
2
+ import type { TaskRecord } from '../../core/tasks.ts';
3
+ /** Link-subtask overlay props. */
4
+ export interface LinkSubtaskModalProps {
5
+ controller: BoardController;
6
+ /** The task the picked card becomes a subtask of. */
7
+ parent: TaskRecord;
8
+ onClose: () => void;
9
+ }
10
+ /** Link-subtask overlay. */
11
+ export declare function LinkSubtaskModal({ controller, parent, onClose }: LinkSubtaskModalProps): import("react").JSX.Element;
@@ -12,6 +12,12 @@ export interface NewTaskModalProps {
12
12
  defaultWorkspaceId?: string;
13
13
  /** Optional callback after successful duplication (e.g. to archive source). */
14
14
  onDuplicateSuccess?: (sourceTaskId: string) => Promise<void>;
15
+ /**
16
+ * Task the new card becomes a subtask of ("add subtask" from its detail
17
+ * view). The execution targets start from the parent's and the create action
18
+ * carries the parent link; unset targets inherit again at run time.
19
+ */
20
+ parentTask?: TaskRecord;
15
21
  }
16
22
  /** New-task form overlay. */
17
- export declare function NewTaskModal({ controller, onClose, initialTask, defaultWorkspaceId, onDuplicateSuccess }: NewTaskModalProps): import("react").JSX.Element;
23
+ export declare function NewTaskModal({ controller, onClose, initialTask, defaultWorkspaceId, onDuplicateSuccess, parentTask }: NewTaskModalProps): import("react").JSX.Element;
@@ -2,11 +2,21 @@ import type { TaskRecord } from '../../core/tasks.ts';
2
2
  /** Compact relative/absolute time label. */
3
3
  export declare function formatHostTimestamp(ms: number, timeZone?: string): string;
4
4
  export declare function formatTime(ms: number, timeZone?: string): string;
5
- declare function TaskCardInner({ task, pending, timeZone, onClick }: {
5
+ declare function TaskCardInner({ task, pending, timeZone, onClick, subtaskCount, isSubtask, subtasksDone, subtasksRunning, subtasksFailed, }: {
6
6
  task: TaskRecord;
7
7
  pending: boolean;
8
8
  timeZone?: string;
9
9
  onClick: () => void;
10
+ /** Direct subtasks of this card, shown as a badge (0 hides it). */
11
+ subtaskCount?: number;
12
+ /** Whether this card is itself a subtask. */
13
+ isSubtask?: boolean;
14
+ /** Direct subtasks already settled as done, for the roll-up badge. */
15
+ subtasksDone?: number;
16
+ /** Direct subtasks currently running, for the roll-up badge. */
17
+ subtasksRunning?: number;
18
+ /** Direct subtasks that failed, for the roll-up badge. */
19
+ subtasksFailed?: number;
10
20
  }): import("react").JSX.Element;
11
21
  /** Memoized card: re-renders only when the card's own task record changes. */
12
22
  export declare const TaskCard: import("react").MemoExoticComponent<typeof TaskCardInner>;
@@ -9,7 +9,7 @@
9
9
  * plugin must not take the GUI down.
10
10
  */
11
11
  import type { Context as ClientContext } from '@deepseek-ai/cordis';
12
- import type { ConfigForm } from '@deepseek-ai/dsh-client-ui-settings/client';
12
+ import type { ConfigForm, ConfigForms } from '@deepseek-ai/dsh-client-ui-settings/client';
13
13
  import { type TaskBoardSettings } from './TaskBoardSettingsCard.tsx';
14
14
  import { type TaskBoardKey } from './locales.ts';
15
15
  /** Domain-owned description of one settings namespace a family card binds. */
@@ -94,3 +94,17 @@ export declare function apply(ctx: ClientContext): void;
94
94
  * @returns the form the settings card reads and writes.
95
95
  */
96
96
  export declare function bindSettingsForm(ctx: ClientContext): ConfigForm<TaskBoardSettings>;
97
+ /**
98
+ * The profile entry id this package's own row carries, for a page that serves
99
+ * no family binder.
100
+ *
101
+ * The shared describe mirror is the only local evidence of which row id this
102
+ * profile actually serves, but it answers asynchronously: at plugin activation
103
+ * it usually holds nothing yet. An unanswered mirror therefore binds the
104
+ * aggregate row id rather than guessing among the candidates, and the binding
105
+ * is re-resolved once the mirror answers — see
106
+ * {@link createServedEntryForm}, which owns that rebinding.
107
+ * @param forms - the shared configuration forms service.
108
+ * @returns the entry id to bind before the mirror answers.
109
+ */
110
+ export declare function servedEntryId(forms: ConfigForms): string;
@@ -156,12 +156,16 @@ export declare const zh: {
156
156
  'exec.mode.brokenSuffix': string;
157
157
  'exec.mode.removed': string;
158
158
  'exec.permission.default': string;
159
+ 'exec.permission.inheritParent': string;
159
160
  'exec.permission.read-only': string;
160
161
  'exec.permission.workspace-write': string;
161
162
  'exec.permission.danger-full-access': string;
162
163
  'exec.model.default': string;
163
164
  'exec.model.unknown': string;
164
165
  'exec.reuseSession': string;
166
+ 'exec.teamRun': string;
167
+ 'exec.teamRunHint': string;
168
+ 'exec.teamRunUnavailable': string;
165
169
  'exec.reuseSessionHint': string;
166
170
  'detail.executionSettings': string;
167
171
  'exec.hint': string;
@@ -192,6 +196,35 @@ export declare const zh: {
192
196
  'settings.unsaved': string;
193
197
  'settings.saveFailed': string;
194
198
  'settings.invalidNumber': string;
199
+ 'settings.maxSubtaskDepth': string;
200
+ 'settings.maxSubtaskDepthHint': string;
201
+ 'settings.maxSubtaskDepthOption': string;
202
+ 'detail.parent': string;
203
+ 'detail.parent.open': string;
204
+ 'detail.subtasks': string;
205
+ 'detail.subtasks.empty': string;
206
+ 'detail.subtasks.add': string;
207
+ 'detail.subtasks.link': string;
208
+ 'detail.subtasks.detach': string;
209
+ 'detail.subtasks.runningLock': string;
210
+ 'detail.subtasks.runHint': string;
211
+ 'detail.subtasks.waiting': string;
212
+ 'detail.subtasks.depthLimit': string;
213
+ 'detail.subtasks.hostOnly': string;
214
+ 'new.subtaskTitle': string;
215
+ 'new.subtaskInherit': string;
216
+ 'link.subtask.title': string;
217
+ 'link.subtask.hint': string;
218
+ 'link.subtask.empty': string;
219
+ 'link.subtask.confirm': string;
220
+ 'card.subtask': string;
221
+ 'card.subtasks': string;
222
+ 'card.subtasksFailed': string;
223
+ 'card.subtasksRunning': string;
224
+ 'card.subtasksBreakdown': string;
225
+ 'board.hideSubtasks': string;
226
+ 'board.showSubtasks': string;
227
+ 'board.subtaskFilterHint': string;
195
228
  };
196
229
  /** en dictionary, complete against the zh key set. */
197
230
  export declare const en: Record<keyof typeof zh, string>;
@@ -0,0 +1,62 @@
1
+ /**
2
+ * Native panel registration for the task board.
3
+ *
4
+ * The board is an official-style center-column panel, not a DOM takeover: it
5
+ * contributes a row into the sidebar shell's own global panel list
6
+ * (`sidebar.panellist`) and its page into the layout's keyed `main` slot. The
7
+ * shell then owns the row box, the label, the active highlight, the collapsed
8
+ * rail, the panel switch and the window-chrome interplay exactly as it does for
9
+ * the shipped Plugins and Schedule pages, so the board renders through the same
10
+ * container the desktop application's Plugins page uses.
11
+ *
12
+ * Both registrations go through `ctx.slots.inject`, which fires only once the
13
+ * owning shell entry has declared the seat: load order between this plugin and
14
+ * ui-layout / ui-sidebar therefore does not matter, and neither does a shell
15
+ * that cannot serve the seats (the callback simply never runs).
16
+ *
17
+ * @module @linxin666/dsh-client-ui-task-board/client
18
+ */
19
+ import type { Context as ClientContext } from '@deepseek-ai/cordis';
20
+ import { TASK_BOARD_PANEL_ID, type BoardController } from '../core/controller.ts';
21
+ /** The panel id shared by the sidebar row and the main-slot occupant. */
22
+ export { TASK_BOARD_PANEL_ID };
23
+ /**
24
+ * The sidebar row glyph the shell asks for at its own size and active state.
25
+ * The shell owns the button, label, tooltip and rail geometry; this component
26
+ * draws only the glyph, like every other panel row.
27
+ *
28
+ * The glyph carries `data-dsh-panel-entry` because it is the only DOM the
29
+ * panel's own code owns inside that shell-owned row: the L2 contract (skins)
30
+ * resolves which row belongs to which plugin through it, since the shell
31
+ * stamps no per-entry hook of its own (see contracts/semantic-attrs-v1.md).
32
+ * @param props - the shell's icon share: square edge and selection state.
33
+ * @returns the decorative board glyph.
34
+ */
35
+ export declare function TaskBoardPanelIcon({ size }: {
36
+ size: number;
37
+ active: boolean;
38
+ }): React.ReactElement;
39
+ /**
40
+ * The main-slot page. The layout mounts it only while the board is the selected
41
+ * panel, so the conversation keeps the center column untouched the rest of the
42
+ * time; the wrapper carries the pinned `data-dsh-taskboard-view` semantic
43
+ * anchor (L2 contract, skins) and the container query context the board's
44
+ * responsive rules read.
45
+ * @param props - the framework main-slot share plus this entry's injected face.
46
+ * @returns the board page.
47
+ */
48
+ export declare function TaskBoardPanel({ controller }: {
49
+ controller: BoardController;
50
+ }): React.ReactElement;
51
+ /**
52
+ * Register the board's sidebar row and center-column page.
53
+ *
54
+ * Both seats are declared by shell plugins this package does not depend on at
55
+ * runtime, so each registration is wrapped in `ctx.slots.inject`: the callback
56
+ * runs only after the owning entry declares the seat, and a shell that never
57
+ * declares it leaves the board simply absent instead of failing boot.
58
+ * @param ctx - client root context (services: slots).
59
+ * @param controller - the board controller the page and the row drive.
60
+ * @returns disposer releasing both registrations.
61
+ */
62
+ export declare function registerTaskBoardPanel(ctx: ClientContext, controller: BoardController): () => void;
@@ -0,0 +1,49 @@
1
+ /**
2
+ * Family settings form bound to the profile entry id a Host actually serves.
3
+ *
4
+ * Under the 0.1.7 settings model a form is addressed by PROFILE ENTRY ID, while
5
+ * a family card knows only its own settings namespace. The family binder
6
+ * (`ctx.get('webUiSettings')`, published by dsh-web-settings) resolves one onto
7
+ * the other and is the preferred transport; this module is the fallback for a
8
+ * page that serves no binder — the group plugin is not installed, or its client
9
+ * half has not applied yet (the aggregate mounts its children in order, and a
10
+ * child still waiting on its own injects publishes nothing).
11
+ *
12
+ * The shared describe mirror is the only local evidence of which row id this
13
+ * profile serves, but it answers asynchronously: at plugin activation it usually
14
+ * holds nothing yet. A one-shot guess therefore cannot be right for every
15
+ * deployment, and the form is bound once per session:
16
+ *
17
+ * - binding the bare namespace made every save answer `No configurable plugin
18
+ * entry "<namespace>"` on an aggregate install (the namespace is not an entry
19
+ * id there), which the card reported as "the deployment did not accept these
20
+ * values";
21
+ * - binding the aggregate row id would break a standalone install, whose row id
22
+ * is the namespace or the package's own `ui-*` row.
23
+ *
24
+ * So the fallback starts on the most likely row and REBINDS the moment the
25
+ * mirror names one of this package's rows. Until the mirror answers, the form
26
+ * reports the binding it holds (usually `loading`), which the card renders as an
27
+ * unavailable form rather than a doomed one.
28
+ */
29
+ import type { ConfigForm, ConfigForms } from '@deepseek-ai/dsh-client-ui-settings/client';
30
+ /** Options of {@link createServedEntryForm}. */
31
+ export interface ServedEntryFormOptions<T> {
32
+ /** The shared configuration forms service (`ctx.configForms`). */
33
+ forms: ConfigForms;
34
+ /**
35
+ * Profile entry ids this package's rows carry, most likely first: the
36
+ * aggregate's generated row, the package's own standalone row, then the bare
37
+ * namespace. The first is the fallback while the mirror has no answer; the
38
+ * last is the answer when the mirror serves none of them.
39
+ */
40
+ entryIds: readonly string[];
41
+ }
42
+ /**
43
+ * Create a settings form bound to the profile entry id this Host serves, and
44
+ * rebound whenever the shared describe mirror names a different one of this
45
+ * package's rows.
46
+ * @param options - the shared forms service and this package's candidate row ids.
47
+ * @returns a form delegating to the currently resolved entry's own form.
48
+ */
49
+ export declare function createServedEntryForm<T>(options: ServedEntryFormOptions<T>): ConfigForm<T>;
@@ -43,6 +43,19 @@ export interface SessionsControllerFace {
43
43
  /** Subscribe to catalog changes; selection ownership rides the same snapshot. */
44
44
  subscribe(fn: () => void): () => void;
45
45
  }
46
+ /** Stable id shared by the board's sidebar panel row and its main-slot page. */
47
+ export declare const TASK_BOARD_PANEL_ID = "task-board";
48
+ /**
49
+ * The layout's panel-navigation face. The board no longer owns the center
50
+ * column at the DOM level: selecting the panel is the layout's business, and
51
+ * this is the one call that asks for it (null returns to the conversation).
52
+ * Optional, so a composition without the layout service keeps the board's
53
+ * pure state transitions testable.
54
+ */
55
+ export interface PanelNavigationFace {
56
+ /** Select the board panel, or null to hand the column back to the conversation. */
57
+ select(panelId: string | null): void;
58
+ }
46
59
  /** Controller dependencies (all swappable in tests). */
47
60
  export interface ControllerDeps {
48
61
  store: TaskStore;
@@ -53,6 +66,8 @@ export interface ControllerDeps {
53
66
  uuid?: () => string;
54
67
  /** Host-authoritative transport; absent keeps the legacy in-memory test path. */
55
68
  transport?: TaskBoardTransport;
69
+ /** Layout panel selection; absent keeps the board state-only (tests, shell-less hosts). */
70
+ panel?: PanelNavigationFace;
56
71
  }
57
72
  /**
58
73
  * Register one host directory as a DSH project (workspace). Wired by the
@@ -90,6 +105,12 @@ export interface ExecutionOptionsSnapshot {
90
105
  presets: readonly ExecutionPresetOption[];
91
106
  models?: readonly ExecutionModelOption[];
92
107
  }
108
+ /**
109
+ * Host-owned state the browser mirrors. SSE frames carry the volatile subset
110
+ * (revision/scheduler/power); a full snapshot also carries the deployment
111
+ * constants the UI reads (session default permission, subtask depth).
112
+ */
113
+ export type HostMirror = Pick<TaskBoardSnapshot, 'revision' | 'scheduler' | 'power' | 'sessionDefaultPermission' | 'maxSubtaskDepth' | 'teamRunAvailable'>;
93
114
  /** Immutable controller snapshot for UI subscriptions. */
94
115
  export interface ControllerSnapshot {
95
116
  tasks: readonly TaskRecord[];
@@ -105,7 +126,7 @@ export interface ControllerSnapshot {
105
126
  /** Whether this deployment can parse pasted text into task fields (issue #1540). */
106
127
  canParseTask?: boolean;
107
128
  transportError?: string;
108
- host?: Pick<TaskBoardSnapshot, 'revision' | 'scheduler' | 'power' | 'sessionDefaultPermission'>;
129
+ host?: HostMirror;
109
130
  }
110
131
  /** The selected task (resolved from the ledger), or undefined. */
111
132
  export declare function selectedTaskOf(snapshot: ControllerSnapshot): TaskRecord | undefined;
@@ -143,9 +164,33 @@ export declare class BoardController {
143
164
  isHostBacked(): boolean;
144
165
  /** Retry initial migration/state synchronization after an explicit Host error. */
145
166
  retryHostSync(): Promise<boolean>;
167
+ /**
168
+ * Show the board. The layout owns which panel the center column renders, so
169
+ * the state flip and the panel selection travel together here; a composition
170
+ * with no layout face (tests, a shell-less host) still flips the state.
171
+ *
172
+ * The selection is requested AFTER the snapshot flips so a subscriber
173
+ * rendering against `boardOpen` never observes "open" while the shell still
174
+ * shows the conversation.
175
+ */
146
176
  openBoard(): void;
177
+ /**
178
+ * Return the center column to the conversation. The layout's own selection is
179
+ * the source of truth for what the column renders, so this asks for the
180
+ * conversation explicitly rather than only clearing local state.
181
+ */
147
182
  closeBoard(): void;
148
183
  toggleBoard(): void;
184
+ /**
185
+ * Reflect a panel selection that came from OUTSIDE this controller (the user
186
+ * clicked another sidebar row, or the layout dropped the panel id). Keeps
187
+ * `boardOpen` aligned with what the column actually shows without asking the
188
+ * layout to select anything back.
189
+ * @param panelId - the layout's current panel id, or null for the conversation.
190
+ */
191
+ syncPanelSelection(panelId: string | null): void;
192
+ /** Ask the layout to select a panel; a shell that serves no layout face is a no-op. */
193
+ private selectPanel;
149
194
  /**
150
195
  * Switch between the kanban columns and the archive view. Leaving the
151
196
  * archive view with an archived task still selected closes the selection —
@@ -195,6 +240,14 @@ export declare class BoardController {
195
240
  * @returns true when applied.
196
241
  */
197
242
  archiveTask(id: string): boolean;
243
+ /**
244
+ * Attach an existing task under a parent (or detach it with a null parent)
245
+ * through the Host. The lineage gate — parent exists and is on-board, no
246
+ * cycle, depth within the deployment limit — belongs to the Host; a refusal
247
+ * surfaces through the transport error like every other rejected action.
248
+ * @returns true when the link was accepted by the authority.
249
+ */
250
+ setParent(id: string, parentId: string | null): Promise<boolean>;
198
251
  /** Restore an archived task back onto the board (same status column). */
199
252
  restoreTask(id: string): boolean;
200
253
  /**
@@ -245,8 +298,8 @@ export declare class BoardController {
245
298
  * (background navigation, the Host runner creating and selecting a fresh
246
299
  * execution session, settlement, other plugins), so closing on `current`
247
300
  * changes would evict the board without the user asking. The board closes
248
- * only on explicit user navigation: a sidebar session/workspace row click
249
- * (board-mount onClickSidebarRow) or the board's own actions
301
+ * only on explicit user navigation: selecting another panel (the layout owns
302
+ * selection, and `syncPanelSelection` follows it) or the board's own actions
250
303
  * (openSession / close). Keeping the hook preserves the subscription
251
304
  * contract for future listeners.
252
305
  */
@@ -263,6 +316,21 @@ export declare class BoardController {
263
316
  * re-serialize the whole ledger per tab even while nothing changes.
264
317
  */
265
318
  private onRemoteEvent;
319
+ /**
320
+ * Deployment subtask depth limit the browser mirrors from the last Host
321
+ * snapshot; the fallback is the deployment default for the legacy path (no
322
+ * transport) and for the window before the first snapshot arrives.
323
+ */
324
+ private mirrorDepth;
325
+ /**
326
+ * Project a Host snapshot onto the browser's mirror. SSE frames and partial
327
+ * snapshots carry only the volatile subset (revision/scheduler/power), so the
328
+ * deployment constants the UI reads — the session-default permission the
329
+ * confirmation banner compares against and the subtask depth limit — are
330
+ * carried over from the last full snapshot instead of being dropped by a
331
+ * heartbeat frame.
332
+ */
333
+ private mirrorOf;
266
334
  private refreshRemote;
267
335
  private acceptRemote;
268
336
  private notify;
@@ -0,0 +1,122 @@
1
+ /**
2
+ * Subtask lineage: the depth gate, the ancestry/descendant queries, the
3
+ * cascade participant set one run request opens, the deferred settlement of a
4
+ * cascade parent, and the execution-target inheritance a subtask resolves at
5
+ * run time.
6
+ *
7
+ * Framework-free (no cordis, no runtime imports) so every rule is
8
+ * unit-testable in isolation. The Host ledger is the only authority; the
9
+ * browser mirrors these predicates to enable or disable controls, never to
10
+ * decide them.
11
+ */
12
+ import type { ExecutionOutcome, ExecutionRecord, TaskRecord } from './tasks.ts';
13
+ /** Lowest configurable subtask depth. */
14
+ export declare const SUBTASK_DEPTH_MIN = 1;
15
+ /** Highest configurable subtask depth. */
16
+ export declare const SUBTASK_DEPTH_MAX = 3;
17
+ /** Deployment default: one level of subtasks, and no subtask of a subtask. */
18
+ export declare const DEFAULT_SUBTASK_DEPTH = 1;
19
+ /**
20
+ * Clamp a configured depth into the supported range. Anything unusable
21
+ * (absent, non-numeric, not finite) falls back to the default, so a bad
22
+ * configuration narrows the tree instead of lifting the guard.
23
+ */
24
+ export declare function normalizeSubtaskDepth(value: unknown): number;
25
+ /** Whether an unknown value is a settled execution outcome. */
26
+ export declare function isExecutionOutcome(value: unknown): value is ExecutionOutcome;
27
+ /**
28
+ * Nearest-first ancestor chain of a task. The walk is cycle-safe and bounded
29
+ * by the ledger size rather than by the deployment maximum: a chain stored
30
+ * under a larger limit survives that limit being lowered, and restore and
31
+ * inheritance must still see all of it.
32
+ */
33
+ export declare function ancestorChain(tasks: readonly TaskRecord[], task: TaskRecord): TaskRecord[];
34
+ /** Root-to-task depth: a root task is 0, its subtask 1, and so on. */
35
+ export declare function taskDepth(tasks: readonly TaskRecord[], id: string): number;
36
+ /** Direct subtasks of a task, in ledger order. */
37
+ export declare function directSubtasks(tasks: readonly TaskRecord[], id: string): TaskRecord[];
38
+ /**
39
+ * Descendants of a task in breadth-first order, bounded by maxSubtaskDepth
40
+ * total depth. The visited set keeps a malformed ledger from looping.
41
+ */
42
+ export declare function descendantTasks(tasks: readonly TaskRecord[], id: string, maxSubtaskDepth: number): TaskRecord[];
43
+ /**
44
+ * Height of a task's own subtree (0 for a leaf), bounded by the hard maximum.
45
+ * The visited set makes the walk terminate on a cyclic hand-edited ledger
46
+ * instead of recursing until the stack overflows.
47
+ */
48
+ export declare function subtreeHeight(tasks: readonly TaskRecord[], id: string, visited?: ReadonlySet<string>): number;
49
+ /** Why a parent link was refused; also the source of the Host error text. */
50
+ export type ParentLinkRejection = 'unknown-task' | 'unknown-parent' | 'archived-parent' | 'self-parent' | 'cycle' | 'depth-exceeded';
51
+ export interface ParentLinkCheck {
52
+ /** True when the link (or the detachment) may be stored. */
53
+ ok: boolean;
54
+ /** Why it was refused; absent when ok. */
55
+ reason?: ParentLinkRejection;
56
+ }
57
+ /**
58
+ * Whether childId may be attached under parentId. A null parent detaches and
59
+ * is always allowed for a known task. Otherwise the link must name an
60
+ * on-board task that is neither the child itself nor one of its descendants,
61
+ * and the resulting depth (parent depth + 1 + the child own subtree height)
62
+ * must stay within maxSubtaskDepth.
63
+ */
64
+ export declare function checkParentLink(tasks: readonly TaskRecord[], childId: string, parentId: string | null, maxSubtaskDepth: number): ParentLinkCheck;
65
+ /**
66
+ * The tasks one run request opens executions for: the requested task first,
67
+ * then its on-board descendants within the configured depth. Archived tasks
68
+ * never run; a participant that already has an open execution is skipped by
69
+ * the caller, which cannot start it twice.
70
+ */
71
+ export declare function cascadeTargets(tasks: readonly TaskRecord[], rootId: string, maxSubtaskDepth: number): TaskRecord[];
72
+ /** One still-open execution belonging to a cascade run group. */
73
+ export interface OpenCascadeChild {
74
+ taskId: string;
75
+ execution: ExecutionRecord;
76
+ }
77
+ /** The open execution of a task inside a run group, if it still exists. */
78
+ export declare function openGroupExecution(task: TaskRecord, runGroupId: string): ExecutionRecord | undefined;
79
+ /**
80
+ * The direct subtasks of parentTaskId whose execution in this run group is
81
+ * still open: the parent remaining waiters. The parent may settle once this
82
+ * list is empty and its own outcome is known.
83
+ */
84
+ export declare function pendingCascadeChildren(tasks: readonly TaskRecord[], parentTaskId: string, runGroupId: string): OpenCascadeChild[];
85
+ /**
86
+ * Fold a cascade parent own outcome and its children outcomes into one
87
+ * verdict: failure dominates, then cancellation, then success. The error text
88
+ * is the first failure, falling back to the first cancellation.
89
+ */
90
+ export declare function combineCascadeOutcome(entries: readonly {
91
+ result: ExecutionOutcome;
92
+ error?: string;
93
+ }[]): {
94
+ result: ExecutionOutcome;
95
+ error: string | undefined;
96
+ };
97
+ /**
98
+ * The permission a task would actually run under: its handover bundle's pin
99
+ * wins over the plain pin field, exactly as the runner resolves it.
100
+ */
101
+ export declare function effectiveTaskPermission(task: TaskRecord): TaskRecord['permission'];
102
+ /**
103
+ * Resolve the execution targets a (sub)task actually runs with: its own value
104
+ * wins, and an unset target inherits the nearest ancestor up to the root.
105
+ * Workspace, agent preset and model inherit the ancestor's PIN only — a
106
+ * handover bundle is a per-card continuation artefact and is never inherited.
107
+ * The permission inherits the ancestor's EFFECTIVE binding, and its human
108
+ * confirmation travels with it, so a subtask of an already-confirmed elevated
109
+ * card is runnable while a subtask's own unconfirmed binding stays gated.
110
+ */
111
+ /**
112
+ * The immutable teammate name for one team-mode member. Teammate names are
113
+ * lower-kebab-case and must stay unique for the lifetime of the Team, so the
114
+ * run-group token is appended: two runs of the same tree never collide. A title
115
+ * with no ASCII word characters (a CJK title, for example) keeps a stable
116
+ * generic prefix and is still distinguished by the token.
117
+ * @param title - the member task's title.
118
+ * @param token - the run group id (the task id when no group is recorded).
119
+ * @returns the teammate name handed to the Agent Teams service.
120
+ */
121
+ export declare function teammateName(title: string, token: string): string;
122
+ export declare function resolveExecutionTargets(task: TaskRecord, tasks: readonly TaskRecord[], maxSubtaskDepth?: number): TaskRecord;
@@ -8,6 +8,8 @@ import type { FreezeSnapshot } from './freeze-snapshot.ts';
8
8
  import type { TaskHandover, TaskHandoverInput } from './handover.ts';
9
9
  /** Task lifecycle status, one per kanban column. */
10
10
  export type TaskStatus = 'backlog' | 'todo' | 'running' | 'done' | 'failed';
11
+ /** Settled outcome of one execution attempt. */
12
+ export type ExecutionOutcome = 'succeeded' | 'failed' | 'cancelled';
11
13
  /**
12
14
  * One real execution attempt: the run's own id, the dsh session that ran it
13
15
  * (filled once the session is created), and the settled outcome once the
@@ -23,7 +25,7 @@ export interface ExecutionRecord {
23
25
  /** When the run settled; absent while still running. */
24
26
  endedAt: number | undefined;
25
27
  /** Outcome once settled. */
26
- result: 'succeeded' | 'failed' | 'cancelled' | undefined;
28
+ result: ExecutionOutcome | undefined;
27
29
  /** Human failure text when the run failed (prompt rejection or agent error). */
28
30
  error: string | undefined;
29
31
  /**
@@ -36,6 +38,20 @@ export interface ExecutionRecord {
36
38
  frozenAt?: number;
37
39
  /** Freeze source session captured from the card snapshot when the run opened. */
38
40
  frozenBy?: string;
41
+ /**
42
+ * Cascade run group shared by the parent execution and every descendant
43
+ * execution one run request opened. Absent on a plain single-task run.
44
+ */
45
+ runGroupId?: string;
46
+ /**
47
+ * A cascade parent's own session outcome, recorded as soon as its own turn
48
+ * settles and held while its subtask executions finish. The execution stays
49
+ * open (no `endedAt`) until the last child settles; the Host monitor skips
50
+ * an execution whose own outcome is already recorded.
51
+ */
52
+ ownResult?: ExecutionOutcome;
53
+ /** Human failure text that arrived with {@link ownResult}. */
54
+ ownError?: string;
39
55
  }
40
56
  /**
41
57
  * Maximum number of execution records retained per task. Older settled runs
@@ -140,6 +156,14 @@ export interface TaskRecord {
140
156
  description: string;
141
157
  /** The prompt sent to dsh when this task is executed. */
142
158
  prompt: string;
159
+ /**
160
+ * Parent task id: present only on a subtask. The Host owns the lineage gate
161
+ * (see `core/subtask.ts`), which refuses a cycle, an archived parent, and
162
+ * any link that would exceed `maxSubtaskDepth` — so every chain the Host
163
+ * wrote under the current setting is at most that many links long. Lowering
164
+ * the limit later neither rewrites nor deletes an already stored link.
165
+ */
166
+ parentId?: string;
143
167
  /** Current column. */
144
168
  status: TaskStatus;
145
169
  /** Creation instant (ms epoch). */
@@ -181,6 +205,15 @@ export interface TaskRecord {
181
205
  * {@link reusableSessionId}).
182
206
  */
183
207
  reuseSession?: boolean;
208
+ /**
209
+ * Run this task's subtree as an Agent Team instead of one Host-launched
210
+ * session per participant: the root becomes the Team Lead session and the
211
+ * Host spawns one teammate for each direct subtask inside it. Absent or false
212
+ * keeps the plain concurrent cascade. A deployment that does not serve the
213
+ * Agent Teams service refuses a team run instead of silently falling back,
214
+ * and a subtask's own permission pin cannot be honored in this mode.
215
+ */
216
+ teamRun?: boolean;
184
217
  /**
185
218
  * Frozen context snapshot for a continuation card; absent on plain tasks.
186
219
  * Sanitized before it enters the ledger (redaction, slash-command taint,
@@ -231,6 +264,12 @@ export interface NewTaskInput {
231
264
  title: string;
232
265
  description: string;
233
266
  prompt: string;
267
+ /**
268
+ * Parent task id, turning this creation into a subtask. Absent keeps a root
269
+ * task; the create use case validates the link (existence, depth) and
270
+ * inherits the parent's unset execution targets.
271
+ */
272
+ parentId?: string;
234
273
  /** Workspace the execution must run in; empty/absent = the recent workspace. */
235
274
  workspaceId?: string;
236
275
  /** Agent preset the execution session must be composed from; empty/absent = deployment default. */
@@ -241,6 +280,8 @@ export interface NewTaskInput {
241
280
  model?: string;
242
281
  /** Reuse the previous execution's session for later runs (issue #1419). */
243
282
  reuseSession?: boolean;
283
+ /** Run the subtree as an Agent Team (Team Lead session plus one teammate per direct subtask). */
284
+ teamRun?: boolean;
244
285
  /**
245
286
  * Optional scheduled-run rule requested at creation time (the new-task
246
287
  * dialog): an enable flag plus a 5-field cron expression. The create use
@@ -308,8 +349,10 @@ export declare function withSchedule(task: TaskRecord, patch: Partial<ScheduleRu
308
349
  /**
309
350
  * Open a fresh execution on a task: move it to 'running' and append a
310
351
  * running execution record. Returns the new task and the new execution.
352
+ * @param runGroupId - cascade run group, when this run also opened executions
353
+ * for the task's subtasks (see `core/subtask.ts`).
311
354
  */
312
- export declare function startExecution(task: TaskRecord, now: number, executionId: string, initiatedBy?: string): {
355
+ export declare function startExecution(task: TaskRecord, now: number, executionId: string, initiatedBy?: string, runGroupId?: string): {
313
356
  task: TaskRecord;
314
357
  execution: ExecutionRecord;
315
358
  };
@@ -318,6 +361,6 @@ export declare function startExecution(task: TaskRecord, now: number, executionI
318
361
  * matching column. No-op (returns the input task) when the execution is not
319
362
  * the task's latest or is already settled.
320
363
  */
321
- export declare function settleExecution(task: TaskRecord, executionId: string, outcome: 'succeeded' | 'failed' | 'cancelled', now: number, error: string | undefined): TaskRecord;
364
+ export declare function settleExecution(task: TaskRecord, executionId: string, outcome: ExecutionOutcome, now: number, error: string | undefined): TaskRecord;
322
365
  /** A settled-execution summary string for the detail view. */
323
366
  export declare function executionLabel(execution: ExecutionRecord): string;
@@ -1,9 +1,3 @@
1
- /**
2
- * Archive/restore task use case: move a task off the main board and back,
3
- * whatever its status but `running`. The task keeps its status, execution history, and
4
- * transcript references, while archiving disarms any schedule so it cannot
5
- * create more execution records until the user restores and re-enables it.
6
- */
7
1
  import type { TaskRecord } from '../tasks.ts';
8
2
  /** Result of an archive transition. */
9
3
  export interface ArchiveTaskResult {
@@ -13,11 +7,14 @@ export interface ArchiveTaskResult {
13
7
  archived: boolean;
14
8
  }
15
9
  /**
16
- * Archive one task: only a `running` task stays on the board (its runner
17
- * still owns its lifecycle until the execution settles); every other status
18
- * can be archived. Archiving disarms a schedule; already-archived tasks are
19
- * a no-op.
10
+ * Archive one task and its subtasks: only a running member keeps the group on
11
+ * the board (its runner still owns its lifecycle until the execution settles);
12
+ * any other status can be archived. Archiving disarms schedules; an
13
+ * already-archived task is a no-op.
14
+ */
15
+ export declare function applyArchiveTask(tasks: readonly TaskRecord[], id: string, now: number, maxSubtaskDepth?: number): ArchiveTaskResult;
16
+ /**
17
+ * Restore one task back onto the main board, together with its ancestors (an
18
+ * on-board subtask must not point at an archived parent) and its subtasks.
20
19
  */
21
- export declare function applyArchiveTask(tasks: readonly TaskRecord[], id: string, now: number): ArchiveTaskResult;
22
- /** Restore one task back onto the main board (clears the archive marker). */
23
- export declare function applyRestoreTask(tasks: readonly TaskRecord[], id: string, now: number): ArchiveTaskResult;
20
+ export declare function applyRestoreTask(tasks: readonly TaskRecord[], id: string, now: number, maxSubtaskDepth?: number): ArchiveTaskResult;