@linxin666/dsh-client-ui-task-board 0.4.3 → 0.4.4
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.
- package/README.i18n.yaml +2 -2
- package/README.md +17 -10
- package/README.zh.md +17 -10
- package/lib/client.js +1350 -552
- package/lib/client.js.map +1 -1
- package/lib/index.js +655 -134
- package/lib/types/client/board/TaskForm.d.ts +19 -1
- package/lib/types/client/board/preset-label.d.ts +22 -0
- package/lib/types/client/host-api.d.ts +24 -1
- package/lib/types/client/locales.d.ts +38 -2
- package/lib/types/client/schedule-zone.d.ts +47 -0
- package/lib/types/core/controller.d.ts +1 -0
- package/lib/types/core/degraded-reason.d.ts +46 -0
- package/lib/types/core/package-name.d.ts +9 -0
- package/lib/types/core/schedule.d.ts +95 -20
- package/lib/types/core/subtask.d.ts +30 -11
- package/lib/types/core/tasks.d.ts +26 -2
- package/lib/types/core/use-cases/task-create.d.ts +2 -1
- package/lib/types/core/use-cases/task-schedule.d.ts +8 -4
- package/lib/types/core/use-cases/task-update.d.ts +1 -1
- package/lib/types/host-ledger.d.ts +56 -4
- package/lib/types/host-runner.d.ts +76 -3
- package/lib/types/host-service.d.ts +26 -0
- package/lib/types/protocol.d.ts +9 -3
- package/package.json +25 -25
- package/src/client/board/LinkSubtaskModal.tsx +21 -11
- package/src/client/board/NewTaskModal.tsx +271 -150
- package/src/client/board/TaskCard.tsx +24 -5
- package/src/client/board/TaskDetail.tsx +83 -33
- package/src/client/board/TaskForm.tsx +48 -3
- package/src/client/board/preset-label.ts +44 -0
- package/src/client/board.module.css +114 -5
- package/src/client/host-api.ts +109 -6
- package/src/client/index.ts +5 -1
- package/src/client/locales.ts +82 -8
- package/src/client/schedule-zone.ts +135 -0
- package/src/core/controller.ts +7 -2
- package/src/core/degraded-reason.ts +81 -0
- package/src/core/package-name.ts +9 -0
- package/src/core/schedule.ts +211 -55
- package/src/core/store.ts +10 -1
- package/src/core/subtask.ts +77 -22
- package/src/core/tasks.ts +38 -4
- package/src/core/use-cases/task-create.ts +14 -2
- package/src/core/use-cases/task-schedule.ts +17 -6
- package/src/core/use-cases/task-update.ts +4 -1
- package/src/host/agent-tools.ts +40 -8
- package/src/host-ledger.ts +203 -23
- package/src/host-runner.ts +164 -14
- package/src/host-service.ts +94 -17
- package/src/protocol.ts +27 -8
- package/lib/types/host/run-guarded.d.ts +0 -39
- package/src/host/run-guarded.ts +0 -76
|
@@ -23,11 +23,20 @@ export interface OpenExecutionReference {
|
|
|
23
23
|
readonly executionId: string;
|
|
24
24
|
readonly sessionId: string | undefined;
|
|
25
25
|
readonly startedAt: number;
|
|
26
|
+
/**
|
|
27
|
+
* True for a member of a team-mode run other than its Lead. A teammate is a
|
|
28
|
+
* durable member of the Team: it stays alive after its turn ends, so the
|
|
29
|
+
* roster may keep reporting its session as running, and the turn it completed
|
|
30
|
+
* is the only verdict it exposes.
|
|
31
|
+
*/
|
|
32
|
+
readonly teamMember: boolean;
|
|
26
33
|
}
|
|
27
34
|
/** Minimal value copy used by the Host scheduler. */
|
|
28
35
|
export interface DueScheduleReference {
|
|
29
36
|
readonly taskId: string;
|
|
30
37
|
readonly cron: string;
|
|
38
|
+
/** Zone this rule's wall clock is read in. */
|
|
39
|
+
readonly timeZone: string;
|
|
31
40
|
readonly nextRunAt: number;
|
|
32
41
|
}
|
|
33
42
|
/** Derived runtime data for one session-poll pass. */
|
|
@@ -191,13 +200,56 @@ export declare class HostTaskLedger {
|
|
|
191
200
|
* (failure dominates, then cancellation).
|
|
192
201
|
*/
|
|
193
202
|
private settleCascade;
|
|
203
|
+
/**
|
|
204
|
+
* Close every run that is already decided, and fold every lineage whose
|
|
205
|
+
* members are all settled. The Host poll calls this on every tick, so a run
|
|
206
|
+
* can no longer be left in the running column by a settle nobody observed:
|
|
207
|
+
*
|
|
208
|
+
* - A team run is governed by its Lead. Once the Lead's own outcome is
|
|
209
|
+
* recorded, its still-open members are settled with that verdict. A
|
|
210
|
+
* teammate is a durable member of the Team and may never report a turn of
|
|
211
|
+
* its own, and without this the whole lineage waits on it forever.
|
|
212
|
+
* - A cascade parent whose members are all settled is finalized here even
|
|
213
|
+
* when the settle that completed the tree happened in another process, or
|
|
214
|
+
* was interrupted between its child write and its parent write.
|
|
215
|
+
*
|
|
216
|
+
* Idempotent: a run that is already folded is left untouched, so a caller may
|
|
217
|
+
* invoke it on every tick.
|
|
218
|
+
* @param persist - false to leave the document for the caller to commit.
|
|
219
|
+
* @returns whether the document changed.
|
|
220
|
+
*/
|
|
221
|
+
finalizeReadyRuns(persist?: boolean): boolean;
|
|
222
|
+
/**
|
|
223
|
+
* The verdict a team run's Lead has already recorded: its own turn outcome
|
|
224
|
+
* once that turn ended, or the folded outcome once its card settled.
|
|
225
|
+
*/
|
|
226
|
+
private leadVerdict;
|
|
227
|
+
/**
|
|
228
|
+
* Settle every still-open member of a team run with the Lead's verdict.
|
|
229
|
+
* A teammate that already reported its own outcome keeps it: the ordinary
|
|
230
|
+
* fold still lets a failure dominate.
|
|
231
|
+
* @param leadId - the Lead card's task id (never settled here).
|
|
232
|
+
* @param groupId - the team run's group.
|
|
233
|
+
* @param verdict - the Lead's outcome, inherited by members that never reported.
|
|
234
|
+
* @param error - the Lead's failure text, inherited with a failed verdict.
|
|
235
|
+
* @param now - clock instant (ms epoch).
|
|
236
|
+
* @returns whether the document changed.
|
|
237
|
+
*/
|
|
238
|
+
private closeTeamMembers;
|
|
194
239
|
private repairSchedules;
|
|
195
240
|
private reconcileInterruptedStarts;
|
|
196
241
|
/**
|
|
197
|
-
* Field-preserving
|
|
198
|
-
*
|
|
199
|
-
*
|
|
200
|
-
*
|
|
242
|
+
* Field-preserving migration of a pre-v4 document. It first proves every task
|
|
243
|
+
* row is structurally valid, so a document that would silently drop or coerce
|
|
244
|
+
* rows fails loudly instead (no quarantined-empty restart), and then reuses
|
|
245
|
+
* the current normalization.
|
|
246
|
+
*
|
|
247
|
+
* v4 adds `ScheduleRule.timeZone`. A rule written before v4 was evaluated in
|
|
248
|
+
* whatever zone the Host process happened to report, so the migration stamps
|
|
249
|
+
* that zone onto every enabled rule: the trigger instant is preserved exactly
|
|
250
|
+
* (the stored `nextRunAt` already encodes the old zone), but the rule stops
|
|
251
|
+
* following a later `TZ` change, which is what made an existing schedule
|
|
252
|
+
* silently move when the Host's zone changed.
|
|
201
253
|
*/
|
|
202
254
|
private migrateLegacyDocument;
|
|
203
255
|
private load;
|
|
@@ -23,8 +23,18 @@ type ExecutionSessionId = SessionSummary['sessionId'];
|
|
|
23
23
|
export interface SessionCommandDispatcher {
|
|
24
24
|
execute(sessionId: ExecutionSessionId, line: string, signal: AbortSignal): Promise<CommandResult | undefined>;
|
|
25
25
|
}
|
|
26
|
-
export type ExecutionInspection =
|
|
26
|
+
export type ExecutionInspection =
|
|
27
|
+
/**
|
|
28
|
+
* No verdict yet. `unreadable` separates "the work may still be in progress"
|
|
29
|
+
* from "this session's history could not be read at all": a reader failure is
|
|
30
|
+
* not progress, and the caller decides how long to keep waiting before it
|
|
31
|
+
* reports the execution as undeterminable instead of holding a card open
|
|
32
|
+
* forever.
|
|
33
|
+
*/
|
|
34
|
+
{
|
|
27
35
|
outcome: 'pending';
|
|
36
|
+
unreadable?: true;
|
|
37
|
+
reason?: string;
|
|
28
38
|
} | {
|
|
29
39
|
outcome: 'succeeded';
|
|
30
40
|
} | {
|
|
@@ -64,6 +74,17 @@ export interface PromptContext {
|
|
|
64
74
|
peers?: readonly PromptPeer[];
|
|
65
75
|
/** True when those members run as teammates inside this session (Team Lead). */
|
|
66
76
|
team?: boolean;
|
|
77
|
+
/**
|
|
78
|
+
* Scheduled-run provenance: the instant the cron rule actually fired and the
|
|
79
|
+
* IANA zone its wall clock was read in. Present only for a cron-triggered
|
|
80
|
+
* run, so the model can resolve an unqualified date in the run's own terms
|
|
81
|
+
* instead of guessing from the Host clock (issue #1722).
|
|
82
|
+
*/
|
|
83
|
+
schedule?: {
|
|
84
|
+
triggeredAt: number;
|
|
85
|
+
timeZone: string;
|
|
86
|
+
cron: string;
|
|
87
|
+
};
|
|
67
88
|
}
|
|
68
89
|
/**
|
|
69
90
|
* Build the execution prompt for one task.
|
|
@@ -72,6 +93,16 @@ export interface PromptContext {
|
|
|
72
93
|
* @returns the prompt text.
|
|
73
94
|
*/
|
|
74
95
|
export declare function promptText(task: TaskRecord, context?: PromptContext): string;
|
|
96
|
+
/**
|
|
97
|
+
* Build the objective the built-in `/goal` command receives. A prompt whose
|
|
98
|
+
* text would be read as a goal operation is prefixed with a neutral label: the
|
|
99
|
+
* instruction stays intact, and the command arms an objective instead of
|
|
100
|
+
* clearing, pausing, or editing an unrelated goal (or refusing for a missing
|
|
101
|
+
* one).
|
|
102
|
+
* @param prompt - the composed execution prompt.
|
|
103
|
+
* @returns the objective text for `/goal <objective>`.
|
|
104
|
+
*/
|
|
105
|
+
export declare function goalObjective(prompt: string): string;
|
|
75
106
|
export declare class HostExecutionRunner {
|
|
76
107
|
private readonly gateway;
|
|
77
108
|
private readonly commands?;
|
|
@@ -122,6 +153,37 @@ export declare class HostExecutionRunner {
|
|
|
122
153
|
* the same permission/model pins before the prompt.
|
|
123
154
|
*/
|
|
124
155
|
private pinAndPrompt;
|
|
156
|
+
/**
|
|
157
|
+
* Arm dsh's built-in goal on the session. The task prompt is queued FIRST, so
|
|
158
|
+
* the session's first turn carries the instruction verbatim; `/goal
|
|
159
|
+
* <objective>` then turns the same text into a persistent objective, and
|
|
160
|
+
* dsh's goal-round driver keeps starting continuation rounds until the agent
|
|
161
|
+
* marks it complete (see {@link goalVerdict} for how the run settles).
|
|
162
|
+
*
|
|
163
|
+
* Opt-out: a task whose `goalRun` is an explicit false runs one plain turn.
|
|
164
|
+
* A refusal (no command dispatcher, no `/goal` command in this cohort, an
|
|
165
|
+
* objective the command rejects) is reported and the run continues as that
|
|
166
|
+
* plain turn: the task was asked to run, and a missing goal mode must not
|
|
167
|
+
* lose the work.
|
|
168
|
+
*/
|
|
169
|
+
private armGoal;
|
|
170
|
+
/**
|
|
171
|
+
* The verdict an armed dsh goal imposes on an otherwise completed turn.
|
|
172
|
+
*
|
|
173
|
+
* A goal run is one session working many automatic continuation rounds:
|
|
174
|
+
* dsh's goal-round driver queues the next round whenever the agent goes
|
|
175
|
+
* idle, so the first completed `turn/end` is not the execution's result.
|
|
176
|
+
* Settling there would leave the card in `done` while the session keeps
|
|
177
|
+
* working, and a scheduled task would fire a second, concurrent session.
|
|
178
|
+
* The session projection is the authority: an `active` goal keeps the
|
|
179
|
+
* execution pending, a `blocked` goal fails it with the recorded reason, and
|
|
180
|
+
* any other phase (complete, paused, or no goal at all) leaves the turn
|
|
181
|
+
* verdict in place.
|
|
182
|
+
*
|
|
183
|
+
* An unreadable projection falls back to that turn verdict: a cohort whose
|
|
184
|
+
* gateway withdrew `session/projections` must not hang every execution.
|
|
185
|
+
*/
|
|
186
|
+
private goalVerdict;
|
|
125
187
|
listRunning(): Promise<{
|
|
126
188
|
known: true;
|
|
127
189
|
count: number;
|
|
@@ -129,6 +191,17 @@ export declare class HostExecutionRunner {
|
|
|
129
191
|
} | {
|
|
130
192
|
known: false;
|
|
131
193
|
}>;
|
|
132
|
-
/**
|
|
133
|
-
|
|
194
|
+
/**
|
|
195
|
+
* Resolve an execution outcome from the session list and bounded history pages.
|
|
196
|
+
* @param sessionId - the session the execution runs in.
|
|
197
|
+
* @param startedAt - instant the execution opened; earlier events are not its own.
|
|
198
|
+
* @param sessions - roster from the caller's own poll, when it already has one.
|
|
199
|
+
* @param options.whileRunning - read the first completed turn even while the
|
|
200
|
+
* roster reports the session as running. A durable Agent Teams teammate
|
|
201
|
+
* stays alive after its turn ends, so `running` never clears for it and the
|
|
202
|
+
* turn it completed is the only verdict it will ever expose.
|
|
203
|
+
*/
|
|
204
|
+
inspect(sessionId: string, startedAt?: number, sessions?: readonly SessionSummary[], options?: {
|
|
205
|
+
whileRunning?: boolean;
|
|
206
|
+
}): Promise<ExecutionInspection>;
|
|
134
207
|
}
|
|
@@ -31,6 +31,16 @@ export interface TeamSpawnResult {
|
|
|
31
31
|
export interface TaskBoardTeamDispatcher {
|
|
32
32
|
spawn(input: TeamSpawnInput): Promise<TeamSpawnResult>;
|
|
33
33
|
}
|
|
34
|
+
/**
|
|
35
|
+
* Provenance of one cron-triggered cascade: when the rule fired and the zone
|
|
36
|
+
* its wall clock was read in. Passed into every launched prompt of that run so
|
|
37
|
+
* a scheduled job knows its own clock rather than inferring one.
|
|
38
|
+
*/
|
|
39
|
+
export interface ScheduledRunContext {
|
|
40
|
+
triggeredAt: number;
|
|
41
|
+
timeZone: string;
|
|
42
|
+
cron: string;
|
|
43
|
+
}
|
|
34
44
|
/**
|
|
35
45
|
* The native timer face the Host arms through. The cordis `timer` service
|
|
36
46
|
* (dsh-base's own `cordis-plugin-timer` row) provides it: its handles are
|
|
@@ -63,6 +73,12 @@ export declare class TaskBoardHostService {
|
|
|
63
73
|
* conversation instead of prompting into a session it cannot see.
|
|
64
74
|
*/
|
|
65
75
|
private idleSessionIds;
|
|
76
|
+
/**
|
|
77
|
+
* Consecutive unreadable-history polls per open execution (see
|
|
78
|
+
* {@link noteUnreadableInspection}). Cleared as soon as an inspection
|
|
79
|
+
* resolves, so a transient reader failure never fails a card.
|
|
80
|
+
*/
|
|
81
|
+
private readonly unreadablePolls;
|
|
66
82
|
private preventIdleSleep;
|
|
67
83
|
private readonly team;
|
|
68
84
|
private readonly timers;
|
|
@@ -100,6 +116,16 @@ export declare class TaskBoardHostService {
|
|
|
100
116
|
private pollSessions;
|
|
101
117
|
/** Reuse the session list this poll already fetched: one list RPC per tick, not 1 + E. */
|
|
102
118
|
private reconcileExecutions;
|
|
119
|
+
/**
|
|
120
|
+
* Count one poll whose session history could not be read. A reader failure is
|
|
121
|
+
* not progress: the session is not running (the runner only reads history for
|
|
122
|
+
* one that is idle), so nothing will ever change that verdict. After
|
|
123
|
+
* {@link UNREADABLE_SETTLE_POLLS} consecutive polls the execution is reported
|
|
124
|
+
* failed with the recorded reason, instead of holding its card — and every
|
|
125
|
+
* ancestor of it — in the running column with no way out. The first poll of
|
|
126
|
+
* each streak is logged, so the Host log names the session.
|
|
127
|
+
*/
|
|
128
|
+
private noteUnreadableInspection;
|
|
103
129
|
/** Drop the armed schedule timer and forget its target. */
|
|
104
130
|
private clearScheduleTimer;
|
|
105
131
|
/**
|
package/lib/types/protocol.d.ts
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import type { TaskUpdatePatch } from './core/use-cases/task-update.ts';
|
|
2
2
|
import { type NewTaskInput, type TaskPermission, type TaskRecord, type TaskStatus } from './core/tasks.ts';
|
|
3
|
-
export declare const TASK_BOARD_SCHEMA_VERSION:
|
|
4
|
-
/** Ledger documents written before
|
|
5
|
-
export declare const TASK_BOARD_LEGACY_SCHEMA_VERSION:
|
|
3
|
+
export declare const TASK_BOARD_SCHEMA_VERSION: 4;
|
|
4
|
+
/** Ledger documents written before v4; loaded once and migrated on startup. */
|
|
5
|
+
export declare const TASK_BOARD_LEGACY_SCHEMA_VERSION: 3;
|
|
6
|
+
/** Ledger documents written before v3; migrated through the v3 normalization too. */
|
|
7
|
+
export declare const TASK_BOARD_OLDER_SCHEMA_VERSION: 2;
|
|
6
8
|
export declare const TASK_BOARD_API_PREFIX = "/api/task-board";
|
|
7
9
|
export type PowerPhase = 'disabled' | 'idle' | 'acquiring' | 'active' | 'error' | 'unsupported';
|
|
8
10
|
export interface TaskBoardPowerSnapshot {
|
|
@@ -90,12 +92,16 @@ export type TaskBoardAction = {
|
|
|
90
92
|
} | {
|
|
91
93
|
kind: 'restore';
|
|
92
94
|
taskId: string;
|
|
95
|
+
} | {
|
|
96
|
+
kind: 'settle';
|
|
97
|
+
taskId: string;
|
|
93
98
|
} | {
|
|
94
99
|
kind: 'set-schedule';
|
|
95
100
|
taskId: string;
|
|
96
101
|
patch: {
|
|
97
102
|
enabled?: boolean;
|
|
98
103
|
cron?: string;
|
|
104
|
+
timeZone?: string | null;
|
|
99
105
|
};
|
|
100
106
|
} | {
|
|
101
107
|
kind: 'run';
|
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.
|
|
4
|
+
"version": "0.4.4",
|
|
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.
|
|
27
|
+
"dsh": ">=0.2.0-rc.1"
|
|
28
28
|
},
|
|
29
29
|
"bundle": {
|
|
30
30
|
"patch": "./cordis.patch.yml"
|
|
@@ -44,7 +44,7 @@
|
|
|
44
44
|
}
|
|
45
45
|
},
|
|
46
46
|
"peerDependencies": {
|
|
47
|
-
"@deepseek-ai/dsh": ">=0.
|
|
47
|
+
"@deepseek-ai/dsh": ">=0.2.0-rc.1",
|
|
48
48
|
"react": "^18.2.0"
|
|
49
49
|
},
|
|
50
50
|
"dependencies": {
|
|
@@ -52,28 +52,28 @@
|
|
|
52
52
|
},
|
|
53
53
|
"devDependencies": {
|
|
54
54
|
"@deepseek-ai/cordis": "^4.0.4",
|
|
55
|
-
"@deepseek-ai/dsh-agent": "^0.
|
|
56
|
-
"@deepseek-ai/dsh-agent-preset-registry": "^0.
|
|
57
|
-
"@deepseek-ai/dsh-api-gateway": "^0.
|
|
58
|
-
"@deepseek-ai/dsh-api-remotes": "^0.
|
|
59
|
-
"@deepseek-ai/dsh-api-session-controller": "^0.
|
|
60
|
-
"@deepseek-ai/dsh-api-workspace-controller": "^0.
|
|
61
|
-
"@deepseek-ai/dsh-client-connection": "^0.
|
|
62
|
-
"@deepseek-ai/dsh-client-locale": "^0.
|
|
63
|
-
"@deepseek-ai/dsh-client-store": "^0.
|
|
64
|
-
"@deepseek-ai/dsh-client-ui-layout": "^0.
|
|
65
|
-
"@deepseek-ai/dsh-client-ui-renderer": "^0.
|
|
66
|
-
"@deepseek-ai/dsh-client-ui-settings": "^0.
|
|
67
|
-
"@deepseek-ai/dsh-client-ui-sidebar": "^0.
|
|
68
|
-
"@deepseek-ai/dsh-client-ui-slots": "^0.
|
|
69
|
-
"@deepseek-ai/dsh-client-ui-workspace": "^0.
|
|
70
|
-
"@deepseek-ai/dsh-commands": "^0.
|
|
71
|
-
"@deepseek-ai/dsh-host-webserver": "^0.
|
|
72
|
-
"@deepseek-ai/dsh-llm": "^0.
|
|
73
|
-
"@deepseek-ai/dsh-settings": "^0.
|
|
74
|
-
"@deepseek-ai/dsh-system-prompt": "^0.
|
|
75
|
-
"@deepseek-ai/dsh-tools": "^0.
|
|
76
|
-
"@deepseek-ai/dsh-workspace": "^0.
|
|
55
|
+
"@deepseek-ai/dsh-agent": "^0.2.0-rc.1",
|
|
56
|
+
"@deepseek-ai/dsh-agent-preset-registry": "^0.2.0-rc.1",
|
|
57
|
+
"@deepseek-ai/dsh-api-gateway": "^0.2.0-rc.1",
|
|
58
|
+
"@deepseek-ai/dsh-api-remotes": "^0.2.0-rc.1",
|
|
59
|
+
"@deepseek-ai/dsh-api-session-controller": "^0.2.0-rc.1",
|
|
60
|
+
"@deepseek-ai/dsh-api-workspace-controller": "^0.2.0-rc.1",
|
|
61
|
+
"@deepseek-ai/dsh-client-connection": "^0.2.0-rc.1",
|
|
62
|
+
"@deepseek-ai/dsh-client-locale": "^0.2.0-rc.1",
|
|
63
|
+
"@deepseek-ai/dsh-client-store": "^0.2.0-rc.1",
|
|
64
|
+
"@deepseek-ai/dsh-client-ui-layout": "^0.2.0-rc.1",
|
|
65
|
+
"@deepseek-ai/dsh-client-ui-renderer": "^0.2.0-rc.1",
|
|
66
|
+
"@deepseek-ai/dsh-client-ui-settings": "^0.2.0-rc.1",
|
|
67
|
+
"@deepseek-ai/dsh-client-ui-sidebar": "^0.2.0-rc.1",
|
|
68
|
+
"@deepseek-ai/dsh-client-ui-slots": "^0.2.0-rc.1",
|
|
69
|
+
"@deepseek-ai/dsh-client-ui-workspace": "^0.2.0-rc.1",
|
|
70
|
+
"@deepseek-ai/dsh-commands": "^0.2.0-rc.1",
|
|
71
|
+
"@deepseek-ai/dsh-host-webserver": "^0.2.0-rc.1",
|
|
72
|
+
"@deepseek-ai/dsh-llm": "^0.2.0-rc.1",
|
|
73
|
+
"@deepseek-ai/dsh-settings": "^0.2.0-rc.1",
|
|
74
|
+
"@deepseek-ai/dsh-system-prompt": "^0.2.0-rc.1",
|
|
75
|
+
"@deepseek-ai/dsh-tools": "^0.2.0-rc.1",
|
|
76
|
+
"@deepseek-ai/dsh-workspace": "^0.2.0-rc.1",
|
|
77
77
|
"@types/node": "^22.20.0",
|
|
78
78
|
"@types/react": "~18.3.1",
|
|
79
79
|
"@types/react-dom": "^18.3.5",
|
|
@@ -4,9 +4,9 @@
|
|
|
4
4
|
* a link the Host would refuse is not offered in the first place; the Host
|
|
5
5
|
* still re-checks the action at submit time.
|
|
6
6
|
*/
|
|
7
|
-
import { useState } from 'react'
|
|
7
|
+
import { useMemo, useState } from 'react'
|
|
8
8
|
import type { BoardController } from '../../core/controller.ts'
|
|
9
|
-
import { DEFAULT_SUBTASK_DEPTH, checkParentLink } from '../../core/subtask.ts'
|
|
9
|
+
import { DEFAULT_SUBTASK_DEPTH, buildLineageIndex, checkParentLink } from '../../core/subtask.ts'
|
|
10
10
|
import type { TaskRecord } from '../../core/tasks.ts'
|
|
11
11
|
import { t } from '../locales.ts'
|
|
12
12
|
import css from '../board.module.css'
|
|
@@ -28,15 +28,25 @@ export function LinkSubtaskModal({ controller, parent, onClose }: LinkSubtaskMod
|
|
|
28
28
|
const limit = snapshot.host?.maxSubtaskDepth ?? DEFAULT_SUBTASK_DEPTH
|
|
29
29
|
// Only root, on-board tasks can move under another parent; the lineage gate
|
|
30
30
|
// decides the rest (depth, cycle, archived parent).
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
31
|
+
//
|
|
32
|
+
// The gate is run once per candidate task, so it is given ONE lineage index
|
|
33
|
+
// for the whole pass: without it every call rebuilds the id/child maps and
|
|
34
|
+
// rescans the ledger, turning a render into quadratic work. The derivation
|
|
35
|
+
// also runs on every controller notification (the SSE heartbeat included),
|
|
36
|
+
// so it is memoized on the inputs it actually reads.
|
|
37
|
+
const tasks = snapshot.tasks
|
|
38
|
+
const candidates = useMemo(() => {
|
|
39
|
+
const lineage = buildLineageIndex(tasks)
|
|
40
|
+
return tasks.filter(task =>
|
|
41
|
+
task.archivedAt === undefined
|
|
42
|
+
// A running task may already be a participant of another run group; the
|
|
43
|
+
// Host refuses the move, so it is not offered here.
|
|
44
|
+
&& task.status !== 'running'
|
|
45
|
+
&& task.parentId === undefined
|
|
46
|
+
&& task.id !== parent.id
|
|
47
|
+
&& checkParentLink(tasks, task.id, parent.id, limit, lineage).ok,
|
|
48
|
+
)
|
|
49
|
+
}, [tasks, parent.id, limit])
|
|
40
50
|
const link = async (taskId: string): Promise<void> => {
|
|
41
51
|
setPending(true)
|
|
42
52
|
setError(undefined)
|