@linxin666/dsh-client-ui-task-board 0.4.1 → 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.
- package/README.i18n.yaml +2 -2
- package/README.md +24 -0
- package/README.zh.md +24 -0
- package/lib/client.js +917 -81
- package/lib/client.js.map +1 -1
- package/lib/index.js +1774 -1420
- package/lib/types/client/TaskBoardSettingsCard.d.ts +4 -0
- package/lib/types/client/board/LinkSubtaskModal.d.ts +11 -0
- package/lib/types/client/board/NewTaskModal.d.ts +7 -1
- package/lib/types/client/board/TaskCard.d.ts +11 -1
- package/lib/types/client/locales.d.ts +33 -0
- package/lib/types/core/controller.d.ts +30 -1
- package/lib/types/core/subtask.d.ts +122 -0
- package/lib/types/core/tasks.d.ts +46 -3
- package/lib/types/core/use-cases/task-archive.d.ts +10 -13
- package/lib/types/core/use-cases/task-create.d.ts +13 -4
- package/lib/types/core/use-cases/task-delete.d.ts +7 -1
- package/lib/types/core/use-cases/task-parent.d.ts +21 -0
- package/lib/types/core/use-cases/task-update.d.ts +1 -1
- package/lib/types/host/agent-tools.d.ts +27 -0
- package/lib/types/host-ledger.d.ts +88 -4
- package/lib/types/host-runner.d.ts +39 -1
- package/lib/types/host-service.d.ts +47 -0
- package/lib/types/index.d.ts +83 -5
- package/lib/types/protocol.d.ts +14 -0
- package/package.json +23 -21
- package/src/client/TaskBoardSettingsCard.tsx +48 -3
- package/src/client/board/LinkSubtaskModal.tsx +82 -0
- package/src/client/board/NewTaskModal.tsx +40 -6
- package/src/client/board/TaskBoard.tsx +88 -8
- package/src/client/board/TaskCard.tsx +52 -2
- package/src/client/board/TaskDetail.tsx +124 -1
- package/src/client/board.module.css +96 -0
- package/src/client/host-api.ts +14 -4
- package/src/client/index.ts +11 -1
- package/src/client/locales.ts +70 -2
- package/src/core/controller.ts +69 -8
- package/src/core/store.ts +15 -0
- package/src/core/subtask.ts +286 -0
- package/src/core/tasks.ts +50 -2
- package/src/core/use-cases/task-archive.ts +46 -12
- package/src/core/use-cases/task-create.ts +42 -7
- package/src/core/use-cases/task-delete.ts +11 -1
- package/src/core/use-cases/task-parent.ts +64 -0
- package/src/core/use-cases/task-update.ts +3 -1
- package/src/host/agent-tools.ts +643 -0
- package/src/host-ledger.ts +323 -45
- package/src/host-runner.ts +141 -12
- package/src/host-service.ts +127 -10
- package/src/index.ts +181 -9
- package/src/protocol.ts +26 -2
package/src/index.ts
CHANGED
|
@@ -14,11 +14,15 @@ import type {} from '@deepseek-ai/dsh-system-prompt'
|
|
|
14
14
|
import type {} from '@deepseek-ai/dsh-api-gateway'
|
|
15
15
|
import type {} from '@deepseek-ai/dsh-workspace'
|
|
16
16
|
import type {} from '@deepseek-ai/dsh-host-webserver'
|
|
17
|
+
import type {} from '@deepseek-ai/dsh-tools'
|
|
18
|
+
import type { ToolDefinition } from '@deepseek-ai/dsh-tools'
|
|
17
19
|
import type { LlmRuntime } from '@deepseek-ai/dsh-llm'
|
|
18
|
-
import { TaskBoardHostService } from './host-service.ts'
|
|
20
|
+
import { TaskBoardHostService, type TaskBoardTeamDispatcher } from './host-service.ts'
|
|
19
21
|
import { parseTaskDraft, TaskParseError } from './host-ai.ts'
|
|
20
22
|
import { TASK_PERMISSIONS, type TaskPermission } from './core/tasks.ts'
|
|
23
|
+
import { DEFAULT_SUBTASK_DEPTH, SUBTASK_DEPTH_MAX, SUBTASK_DEPTH_MIN } from './core/subtask.ts'
|
|
21
24
|
import { DEFAULT_SESSION_PERMISSION } from './core/handover.ts'
|
|
25
|
+
import { buildTaskBoardTools } from './host/agent-tools.ts'
|
|
22
26
|
import { makeTaskBoardRoutes } from './host-routes.ts'
|
|
23
27
|
import { mountOnce } from './mount-once.ts'
|
|
24
28
|
|
|
@@ -31,16 +35,18 @@ export const DEFAULT_PROXY_TOKEN_ENV = 'DSH_TASK_BOARD_PROXY_TOKEN'
|
|
|
31
35
|
export const inject = ['systemPrompt', 'typertGateway', 'workspaceRegistry', 'webServer', 'agents', 'commands']
|
|
32
36
|
|
|
33
37
|
/** Model-facing announcement: plugin presence, capabilities, and limits. */
|
|
34
|
-
export const TASK_BOARD_GUIDANCE = '本机已安装 dsh-task-board 插件(DSH Web GUI 的任务看板):侧边栏「任务看板」入口;在 dsh-web 插件全家桶仓库(packages/dsh-task-board)统一维护,经聚合包 web-ui-all 一键安装。能力:多列看板管理任务;Host 权威账本;关闭浏览器后仍由 Host 执行和结算;任务可钉住工作区、agent
|
|
38
|
+
export const TASK_BOARD_GUIDANCE = '本机已安装 dsh-task-board 插件(DSH Web GUI 的任务看板):侧边栏「任务看板」入口;在 dsh-web 插件全家桶仓库(packages/dsh-task-board)统一维护,经聚合包 web-ui-all 一键安装。能力:多列看板管理任务;Host 权威账本;关闭浏览器后仍由 Host 执行和结算;任务可钉住工作区、agent 预设和权限;任务可建子任务(深度上限可配 1..3,默认 1 层,子任务不能再带子任务),执行父任务会并发执行其子任务树,子任务可单独覆盖权限与模型;另注册 task_board_* agent 工具(list/get/create/update/set_parent/run/manage/schedule),任何会话都可直接读写看板、子任务与定时计划,但运行任务会真实执行并消耗额度,高于会话默认权限的绑定仍必须由用户在 GUI 人工确认(工具刻意不提供确认能力);支持 Host 本地时区的 5 段 cron,错过的触发点不补跑;可选且默认关闭的空闲系统睡眠保护允许屏幕熄灭,但不承诺拦截合盖、手动睡眠、休眠、关机或唤醒已睡眠机器。执行消耗 API 额度。用户提到「任务看板 / 看板 / 定时任务」时即指本插件,请据此协作。若你同时用 todo_write 维护会话顶部的可见计划列表,最终回复前必须再次调用 todo_write 收尾:没有剩余工作时不要保留 in_progress,已完成的最后一步要标为 completed。'
|
|
35
39
|
|
|
36
40
|
/**
|
|
37
41
|
* Plugin config, validated by the same-named schemastery schema.
|
|
38
42
|
*
|
|
39
43
|
* This schema IS the board's settings page: the 0.1.7 Host serves one
|
|
40
44
|
* configuration form per profile entry from the entry's own Config, so the
|
|
41
|
-
*
|
|
45
|
+
* fields the browser card edits are marked volatile — the Loader commits
|
|
42
46
|
* an edit into the running fiber's references without remounting the row (no
|
|
43
|
-
* settings document of the plugin's own exists any more).
|
|
47
|
+
* settings document of the plugin's own exists any more). A field without the
|
|
48
|
+
* marker is refused by the settings write path, which fences writes on
|
|
49
|
+
* volatility.
|
|
44
50
|
*/
|
|
45
51
|
export interface Config {
|
|
46
52
|
/**
|
|
@@ -67,8 +73,27 @@ export interface Config {
|
|
|
67
73
|
* confirmation before it may run; cron refuses unconfirmed cards.
|
|
68
74
|
*/
|
|
69
75
|
sessionDefaultPermission?: TaskPermission
|
|
76
|
+
/**
|
|
77
|
+
* Subtask depth limit, 1..3 (default 1). At 1 a task may carry one level of
|
|
78
|
+
* subtasks and a subtask cannot be given subtasks of its own; raising it
|
|
79
|
+
* allows deeper trees. Every extra level multiplies the sessions one run
|
|
80
|
+
* of the root task opens, so the ceiling is deliberately small.
|
|
81
|
+
*/
|
|
82
|
+
maxSubtaskDepth?: Volatile<number>
|
|
83
|
+
/**
|
|
84
|
+
* Continuable-subagent provider the Agent Teams service uses to compose a
|
|
85
|
+
* teammate. Matches the Agent Teams tool plugin's `freshProvider` default;
|
|
86
|
+
* only team-mode runs use it.
|
|
87
|
+
*/
|
|
88
|
+
teamProvider?: string
|
|
70
89
|
}
|
|
71
90
|
|
|
91
|
+
/**
|
|
92
|
+
* Continuable-subagent provider for fresh teammates, matching the Agent Teams
|
|
93
|
+
* tool plugin's own `freshProvider` default.
|
|
94
|
+
*/
|
|
95
|
+
export const DEFAULT_TEAM_PROVIDER = 'spawn'
|
|
96
|
+
|
|
72
97
|
/**
|
|
73
98
|
* The schema is left to inference rather than annotated with `z<Config>`: a
|
|
74
99
|
* volatile field's parsed output is a `Volatile` reference while its accepted
|
|
@@ -82,6 +107,8 @@ export const Config = z.object({
|
|
|
82
107
|
trustedProxyHosts: z.array(z.string()).default([]),
|
|
83
108
|
proxyTokenEnv: z.string().min(1).default(DEFAULT_PROXY_TOKEN_ENV),
|
|
84
109
|
sessionDefaultPermission: z.union(TASK_PERMISSIONS).default(DEFAULT_SESSION_PERMISSION),
|
|
110
|
+
maxSubtaskDepth: z.number().min(SUBTASK_DEPTH_MIN).max(SUBTASK_DEPTH_MAX).default(DEFAULT_SUBTASK_DEPTH).volatile(),
|
|
111
|
+
teamProvider: z.string().min(1).default(DEFAULT_TEAM_PROVIDER),
|
|
85
112
|
})
|
|
86
113
|
|
|
87
114
|
declare module '@deepseek-ai/cordis' {
|
|
@@ -130,9 +157,105 @@ export function resolveProxyAccess(config: Config | undefined, env: NodeJS.Proce
|
|
|
130
157
|
return { trustedProxyHosts, proxyToken }
|
|
131
158
|
}
|
|
132
159
|
|
|
160
|
+
/** The registry face the agent tools register into. */
|
|
161
|
+
interface AgentToolRegistry {
|
|
162
|
+
register(definition: ToolDefinition): () => void
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Resolve the optional agent-tool registry. The board deliberately does not
|
|
167
|
+
* inject it: a deployment whose runtime serves no registry must still mount the
|
|
168
|
+
* whole board and lose only the agent-tool surface, the same tolerance the
|
|
169
|
+
* optional llm service gets.
|
|
170
|
+
* @param ctx - the plugin context.
|
|
171
|
+
* @returns the registry, or undefined when this deployment serves none.
|
|
172
|
+
*/
|
|
173
|
+
export function resolveToolRegistry(ctx: Context): AgentToolRegistry | undefined {
|
|
174
|
+
try {
|
|
175
|
+
const tools = ctx.get('tools') as AgentToolRegistry | undefined
|
|
176
|
+
return tools !== undefined && typeof tools.register === 'function' ? tools : undefined
|
|
177
|
+
} catch {
|
|
178
|
+
return undefined
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
133
182
|
/** Schema default, re-read for hand-built test contexts (the loader applies them normally). */
|
|
134
183
|
const DEFAULT_ANNOUNCE = false
|
|
135
184
|
|
|
185
|
+
/**
|
|
186
|
+
* How long one teammate may take to reach its durable active or failed edge.
|
|
187
|
+
* Agent Teams provisioning normally resolves in seconds; the bound exists so a
|
|
188
|
+
* hung spawn settles the subtask instead of holding the cascade open forever.
|
|
189
|
+
*/
|
|
190
|
+
const TEAMMATE_PROVISION_TIMEOUT_MS = 600_000
|
|
191
|
+
|
|
192
|
+
/** The subset of the Agent Teams service this plugin drives. */
|
|
193
|
+
interface AgentTeamsFace {
|
|
194
|
+
spawnTeammate(caller: unknown, request: {
|
|
195
|
+
name: string
|
|
196
|
+
description: string
|
|
197
|
+
prompt: Array<{ type: 'text'; text: string }>
|
|
198
|
+
context: 'fresh' | 'fork'
|
|
199
|
+
provider: string
|
|
200
|
+
signal: AbortSignal
|
|
201
|
+
}): Promise<{ member?: { id?: string; phase?: string; error?: string } }>
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Resolve the optional Agent Teams service. Like the tool registry and the llm
|
|
206
|
+
* runtime, it is deliberately not injected: a deployment without Agent Teams
|
|
207
|
+
* must still mount the whole board and only lose team-mode runs.
|
|
208
|
+
* @param ctx - the plugin context.
|
|
209
|
+
* @returns the service, or undefined when this deployment serves none.
|
|
210
|
+
*/
|
|
211
|
+
export function resolveAgentTeams(ctx: Context): AgentTeamsFace | undefined {
|
|
212
|
+
try {
|
|
213
|
+
const teams = ctx.get('agentTeams') as AgentTeamsFace | undefined
|
|
214
|
+
return teams !== undefined && typeof teams.spawnTeammate === 'function' ? teams : undefined
|
|
215
|
+
} catch {
|
|
216
|
+
return undefined
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Build the team dispatcher: the run's Lead session spawns one teammate per
|
|
222
|
+
* subtask through the Agent Teams service, and the teammate's session id is
|
|
223
|
+
* attached to that subtask's execution so the ordinary session monitor settles
|
|
224
|
+
* it. Returns undefined when the deployment serves no Agent Teams service or
|
|
225
|
+
* no live-agent registry, which makes a team run fail closed.
|
|
226
|
+
* @param ctx - the plugin context.
|
|
227
|
+
* @param provider - the continuable-subagent provider for fresh teammates.
|
|
228
|
+
* @returns the dispatcher, or undefined when team runs cannot be served.
|
|
229
|
+
*/
|
|
230
|
+
export function buildTeamDispatcher(ctx: Context, provider: string): TaskBoardTeamDispatcher | undefined {
|
|
231
|
+
const teams = resolveAgentTeams(ctx)
|
|
232
|
+
if (teams === undefined) return undefined
|
|
233
|
+
if (typeof (ctx.agents as { get?: unknown } | undefined)?.get !== 'function') return undefined
|
|
234
|
+
return {
|
|
235
|
+
async spawn(input) {
|
|
236
|
+
const lead = ctx.agents.get(input.leadSessionId as Parameters<typeof ctx.agents.get>[0])
|
|
237
|
+
if (lead === undefined) return { error: `lead session ${input.leadSessionId} is not live` }
|
|
238
|
+
try {
|
|
239
|
+
const result = await teams.spawnTeammate(lead, {
|
|
240
|
+
name: input.name,
|
|
241
|
+
description: input.description,
|
|
242
|
+
prompt: [{ type: 'text', text: input.prompt }],
|
|
243
|
+
context: 'fresh',
|
|
244
|
+
provider,
|
|
245
|
+
signal: AbortSignal.timeout(TEAMMATE_PROVISION_TIMEOUT_MS),
|
|
246
|
+
})
|
|
247
|
+
const member = result?.member
|
|
248
|
+
if (member === undefined) return { error: 'Agent Teams returned no teammate' }
|
|
249
|
+
if (member.phase === 'failed') return { error: member.error ?? 'teammate provisioning failed' }
|
|
250
|
+
if (typeof member.id !== 'string' || member.id === '') return { error: 'Agent Teams returned no teammate session' }
|
|
251
|
+
return { sessionId: member.id }
|
|
252
|
+
} catch (error) {
|
|
253
|
+
return { error: `teammate provisioning failed: ${error instanceof Error ? error.message : String(error)}` }
|
|
254
|
+
}
|
|
255
|
+
},
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
136
259
|
/**
|
|
137
260
|
* Read the optional `llm` service. The board deliberately does not inject it:
|
|
138
261
|
* a deployment without a model must still mount the board, and the parse route
|
|
@@ -153,8 +276,8 @@ export function resolveLlmRuntime(ctx: Context): LlmRuntime | undefined {
|
|
|
153
276
|
* Activate the board's host half: ledger, routes, cron scheduler, power
|
|
154
277
|
* inhibitor, and the model-facing announcement.
|
|
155
278
|
*
|
|
156
|
-
* The effective settings are the config the Host hands this row. The
|
|
157
|
-
*
|
|
279
|
+
* The effective settings are the config the Host hands this row. The fields
|
|
280
|
+
* the browser card edits are schema-volatile, so the Loader commits an
|
|
158
281
|
* edit into the running fiber's references instead of remounting the row —
|
|
159
282
|
* `sync` therefore reads them at use time and follows
|
|
160
283
|
* `loader/volatile-update`, the event the Loader emits once it has committed
|
|
@@ -171,10 +294,14 @@ function applyImpl(ctx: Context, config?: Config): void {
|
|
|
171
294
|
const announceToAgent = (): boolean => readConfigField(config?.announceToAgent, DEFAULT_ANNOUNCE)
|
|
172
295
|
/** Current idle-sleep protection flag. */
|
|
173
296
|
const preventIdleSleep = (): boolean => readConfigField(config?.preventIdleSleep, false)
|
|
297
|
+
/** Current subtask depth limit (read live: the settings card edits it in place). */
|
|
298
|
+
const maxSubtaskDepth = (): number => readConfigField(config?.maxSubtaskDepth, DEFAULT_SUBTASK_DEPTH)
|
|
174
299
|
|
|
175
300
|
const host = new TaskBoardHostService(ctx.typertGateway, {
|
|
176
301
|
workspaceRegistry: ctx.workspaceRegistry,
|
|
177
302
|
sessionDefaultPermission: config?.sessionDefaultPermission ?? DEFAULT_SESSION_PERMISSION,
|
|
303
|
+
maxSubtaskDepth: maxSubtaskDepth(),
|
|
304
|
+
team: buildTeamDispatcher(ctx, config?.teamProvider ?? DEFAULT_TEAM_PROVIDER),
|
|
178
305
|
commandDispatcher: {
|
|
179
306
|
async execute(sessionId, line, signal) {
|
|
180
307
|
const agent = ctx.agents.get(sessionId)
|
|
@@ -187,6 +314,45 @@ function applyImpl(ctx: Context, config?: Config): void {
|
|
|
187
314
|
// scheduler tick, which would roll schedules the board is not running.
|
|
188
315
|
host.setConfiguration(enabled(), preventIdleSleep())
|
|
189
316
|
host.start()
|
|
317
|
+
|
|
318
|
+
// Agent tools: the same Host ledger the browser drives, so any session can
|
|
319
|
+
// list, create, link, run and settle board work. Registration follows the
|
|
320
|
+
// master switch (a disabled board answers no tool call), and the mount
|
|
321
|
+
// effect below owns the disposers.
|
|
322
|
+
let disposeTools: (() => void) | undefined
|
|
323
|
+
const setToolsEnabled = (active: boolean): void => {
|
|
324
|
+
if (!active) {
|
|
325
|
+
disposeTools?.()
|
|
326
|
+
disposeTools = undefined
|
|
327
|
+
return
|
|
328
|
+
}
|
|
329
|
+
if (disposeTools !== undefined) return
|
|
330
|
+
const registry = resolveToolRegistry(ctx)
|
|
331
|
+
if (registry === undefined) return
|
|
332
|
+
const disposers = buildTaskBoardTools(host).map(tool => registry.register(tool))
|
|
333
|
+
disposeTools = () => {
|
|
334
|
+
for (const dispose of disposers.splice(0)) dispose()
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
setToolsEnabled(enabled())
|
|
338
|
+
// A registry that activates after this row (or a row activated before it) is
|
|
339
|
+
// followed through scoped injection where the runtime serves one; capture-only
|
|
340
|
+
// contexts without it register through the direct resolution above.
|
|
341
|
+
const scopedInject = (ctx as { inject?: (names: readonly string[], callback: (scoped: Context) => unknown) => unknown }).inject
|
|
342
|
+
if (typeof scopedInject === 'function') {
|
|
343
|
+
scopedInject.call(ctx, ['tools'], () => {
|
|
344
|
+
setToolsEnabled(enabled())
|
|
345
|
+
// Cordis unloads and re-runs this callback when the injected service's
|
|
346
|
+
// provider fiber changes, and the old registry dies with its provider.
|
|
347
|
+
// Releasing the guard here is what lets the callback register into the
|
|
348
|
+
// NEW registry instead of short-circuiting on the stale disposer.
|
|
349
|
+
return () => {
|
|
350
|
+
disposeTools?.()
|
|
351
|
+
disposeTools = undefined
|
|
352
|
+
}
|
|
353
|
+
})
|
|
354
|
+
}
|
|
355
|
+
|
|
190
356
|
ctx.effect(() => {
|
|
191
357
|
const disposers: Array<() => void> = []
|
|
192
358
|
try {
|
|
@@ -199,18 +365,22 @@ function applyImpl(ctx: Context, config?: Config): void {
|
|
|
199
365
|
})
|
|
200
366
|
for (const route of routes) disposers.push(ctx.webServer.register(route))
|
|
201
367
|
} catch (error) {
|
|
368
|
+
// A failed mount must not leave the agent tools bound to a disposed
|
|
369
|
+
// host: the effect cleanup never runs when this body throws.
|
|
370
|
+
setToolsEnabled(false)
|
|
202
371
|
for (const dispose of disposers) dispose()
|
|
203
372
|
host.dispose()
|
|
204
373
|
throw error
|
|
205
374
|
}
|
|
206
375
|
return () => {
|
|
376
|
+
setToolsEnabled(false)
|
|
207
377
|
for (const dispose of disposers) dispose()
|
|
208
378
|
host.dispose()
|
|
209
379
|
}
|
|
210
380
|
}, 'task-board: host ledger, scheduler, and routes')
|
|
211
381
|
|
|
212
382
|
let disposeSection: (() => void) | undefined
|
|
213
|
-
let applied: { enabled: boolean; announceToAgent: boolean; preventIdleSleep: boolean } | undefined
|
|
383
|
+
let applied: { enabled: boolean; announceToAgent: boolean; preventIdleSleep: boolean; maxSubtaskDepth: number } | undefined
|
|
214
384
|
|
|
215
385
|
// Apply the current values to the host service and the announcement. A
|
|
216
386
|
// commit that changes nothing visible is a no-op, so following a coarse
|
|
@@ -218,12 +388,14 @@ function applyImpl(ctx: Context, config?: Config): void {
|
|
|
218
388
|
// under one disposer: re-registering first tears the old one down so a
|
|
219
389
|
// duplicate-name registration never throws.
|
|
220
390
|
const sync = (): void => {
|
|
221
|
-
const next = { enabled: enabled(), announceToAgent: announceToAgent(), preventIdleSleep: preventIdleSleep() }
|
|
222
|
-
if (applied !== undefined && applied.enabled === next.enabled && applied.announceToAgent === next.announceToAgent && applied.preventIdleSleep === next.preventIdleSleep) {
|
|
391
|
+
const next = { enabled: enabled(), announceToAgent: announceToAgent(), preventIdleSleep: preventIdleSleep(), maxSubtaskDepth: maxSubtaskDepth() }
|
|
392
|
+
if (applied !== undefined && applied.enabled === next.enabled && applied.announceToAgent === next.announceToAgent && applied.preventIdleSleep === next.preventIdleSleep && applied.maxSubtaskDepth === next.maxSubtaskDepth) {
|
|
223
393
|
return
|
|
224
394
|
}
|
|
225
395
|
applied = next
|
|
396
|
+
host.ledger.setMaxSubtaskDepth(next.maxSubtaskDepth)
|
|
226
397
|
host.setConfiguration(next.enabled, next.preventIdleSleep)
|
|
398
|
+
setToolsEnabled(next.enabled)
|
|
227
399
|
if (disposeSection !== undefined) {
|
|
228
400
|
disposeSection()
|
|
229
401
|
disposeSection = undefined
|
package/src/protocol.ts
CHANGED
|
@@ -40,6 +40,16 @@ export interface TaskBoardSnapshot {
|
|
|
40
40
|
power: TaskBoardPowerSnapshot
|
|
41
41
|
/** Session-default permission the confirmation gate compares against. */
|
|
42
42
|
sessionDefaultPermission?: TaskPermission
|
|
43
|
+
/**
|
|
44
|
+
* Deployment subtask depth limit (1..3). The browser mirrors it to enable or
|
|
45
|
+
* disable the subtask controls; the Host ledger is the authority.
|
|
46
|
+
*/
|
|
47
|
+
maxSubtaskDepth?: number
|
|
48
|
+
/**
|
|
49
|
+
* Whether this deployment serves the Agent Teams service, so a task may opt
|
|
50
|
+
* into team execution (Team Lead session plus one teammate per subtask).
|
|
51
|
+
*/
|
|
52
|
+
teamRunAvailable?: boolean
|
|
43
53
|
}
|
|
44
54
|
|
|
45
55
|
/** SSE event frame: revision/scheduler/power only, never the task list. */
|
|
@@ -95,6 +105,7 @@ export type TaskBoardAction =
|
|
|
95
105
|
| { kind: 'run'; taskId: string }
|
|
96
106
|
| { kind: 'rerun'; taskId: string }
|
|
97
107
|
| { kind: 'confirm-permission'; taskId: string }
|
|
108
|
+
| { kind: 'set-parent'; taskId: string; parentId: string | null }
|
|
98
109
|
|
|
99
110
|
export interface TaskBoardActionEnvelope {
|
|
100
111
|
requestId: string
|
|
@@ -192,6 +203,9 @@ function importedTask(value: unknown): TaskRecord | undefined {
|
|
|
192
203
|
lastTriggeredAt: task.schedule.lastTriggeredAt,
|
|
193
204
|
},
|
|
194
205
|
}),
|
|
206
|
+
// The lineage link rides the import; the Host repairs a link whose parent
|
|
207
|
+
// is missing from the merged ledger instead of trusting the export.
|
|
208
|
+
...(task.parentId === undefined ? {} : { parentId: task.parentId }),
|
|
195
209
|
...(task.workspaceId === undefined ? {} : { workspaceId: task.workspaceId }),
|
|
196
210
|
...(task.mode === undefined ? {} : { mode: task.mode }),
|
|
197
211
|
...(task.permission === undefined ? {} : { permission: task.permission }),
|
|
@@ -233,10 +247,12 @@ function handoverPayload(value: unknown): TaskHandoverInput | undefined {
|
|
|
233
247
|
|
|
234
248
|
function createInput(value: unknown): value is NewTaskInput {
|
|
235
249
|
const input = record(value)
|
|
236
|
-
if (input === undefined || !exactKeys(input, ['title', 'description', 'prompt', 'workspaceId', 'mode', 'permission', 'schedule', 'freeze', 'handover', 'model', 'reuseSession', 'tags'])) return false
|
|
250
|
+
if (input === undefined || !exactKeys(input, ['title', 'description', 'prompt', 'parentId', 'workspaceId', 'mode', 'permission', 'schedule', 'freeze', 'handover', 'model', 'reuseSession', 'teamRun', 'tags'])) return false
|
|
251
|
+
if (input.parentId !== undefined && (typeof input.parentId !== 'string' || input.parentId.trim() === '')) return false
|
|
237
252
|
if (typeof input.title !== 'string' || typeof input.description !== 'string' || typeof input.prompt !== 'string') return false
|
|
238
253
|
if (!optionalString(input.workspaceId) || !optionalString(input.mode) || !optionalString(input.model)) return false
|
|
239
254
|
if (input.reuseSession !== undefined && typeof input.reuseSession !== 'boolean') return false
|
|
255
|
+
if (input.teamRun !== undefined && typeof input.teamRun !== 'boolean') return false
|
|
240
256
|
if (input.permission !== undefined && !isTaskPermission(input.permission)) return false
|
|
241
257
|
if (input.tags !== undefined && !isTaskTagList(input.tags)) return false
|
|
242
258
|
if (input.freeze !== undefined && freezePayload(input.freeze) === undefined) return false
|
|
@@ -251,9 +267,10 @@ function createInput(value: unknown): value is NewTaskInput {
|
|
|
251
267
|
|
|
252
268
|
function updatePatch(value: unknown): boolean {
|
|
253
269
|
const patch = record(value)
|
|
254
|
-
if (patch === undefined || !exactKeys(patch, ['title', 'description', 'prompt', 'workspaceId', 'mode', 'permission', 'freeze', 'handover', 'model', 'reuseSession', 'tags'])) return false
|
|
270
|
+
if (patch === undefined || !exactKeys(patch, ['title', 'description', 'prompt', 'workspaceId', 'mode', 'permission', 'freeze', 'handover', 'model', 'reuseSession', 'teamRun', 'tags'])) return false
|
|
255
271
|
// null (or false) clears the reuse opt-in; only a real boolean is accepted.
|
|
256
272
|
if (patch.reuseSession !== undefined && patch.reuseSession !== null && typeof patch.reuseSession !== 'boolean') return false
|
|
273
|
+
if (patch.teamRun !== undefined && patch.teamRun !== null && typeof patch.teamRun !== 'boolean') return false
|
|
257
274
|
for (const key of ['title', 'description', 'prompt', 'workspaceId', 'mode', 'model'] as const) {
|
|
258
275
|
if (!optionalString(patch[key])) return false
|
|
259
276
|
}
|
|
@@ -326,6 +343,13 @@ function parseEnvelopeAction(value: unknown): TaskBoardActionEnvelope | undefine
|
|
|
326
343
|
: patch
|
|
327
344
|
return { requestId: envelope.requestId, action: { kind: 'update', taskId, patch: sanitized } }
|
|
328
345
|
}
|
|
346
|
+
case 'set-parent': {
|
|
347
|
+
if (!exactKeys(action, ['kind', 'taskId', 'parentId'])) return undefined
|
|
348
|
+
if (taskId === undefined) return undefined
|
|
349
|
+
const parentId = action.parentId
|
|
350
|
+
if (parentId !== null && (typeof parentId !== 'string' || parentId.trim() === '')) return undefined
|
|
351
|
+
return { requestId: envelope.requestId, action: { kind: 'set-parent', taskId, parentId } }
|
|
352
|
+
}
|
|
329
353
|
case 'set-schedule':
|
|
330
354
|
if (!exactKeys(action, ['kind', 'taskId', 'patch'])) return undefined
|
|
331
355
|
return taskId !== undefined && schedulePatch(action.patch)
|