@linxin666/dsh-client-ui-task-board 0.4.4 → 0.4.5
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 +42 -11
- package/README.zh.md +42 -11
- package/lib/client.js +4958 -472
- package/lib/client.js.map +1 -1
- package/lib/index.js +2841 -121
- package/lib/types/client/PluginSettingsCard.d.ts +7 -0
- package/lib/types/client/TaskBoardSettingsCard.d.ts +19 -3
- package/lib/types/client/board/ClampedMarkdown.d.ts +3 -0
- package/lib/types/client/board/ConfirmDialog.d.ts +4 -1
- package/lib/types/client/board/EditTaskModal.d.ts +5 -2
- package/lib/types/client/board/LinkSubtaskModal.d.ts +5 -2
- package/lib/types/client/board/NewTaskModal.d.ts +4 -1
- package/lib/types/client/board/TagManagerModal.d.ts +26 -0
- package/lib/types/client/board/TaskBoard.d.ts +10 -1
- package/lib/types/client/board/TaskCard.d.ts +14 -1
- package/lib/types/client/board/TaskDetail.d.ts +3 -1
- package/lib/types/client/board/TaskForm.d.ts +4 -1
- package/lib/types/client/board/VerificationReport.d.ts +23 -0
- package/lib/types/client/board/card-view.d.ts +113 -0
- package/lib/types/client/board/icons.d.ts +32 -0
- package/lib/types/client/board/overlay.d.ts +69 -0
- package/lib/types/client/board/status-key.d.ts +15 -0
- package/lib/types/client/board/task-markdown.d.ts +19 -0
- package/lib/types/client/board-card-seat.d.ts +34 -0
- package/lib/types/client/index.d.ts +19 -0
- package/lib/types/client/locales.d.ts +103 -1
- package/lib/types/client/native-panel.d.ts +6 -20
- package/lib/types/client/seats.d.ts +18 -0
- package/lib/types/client/service.d.ts +32 -0
- package/lib/types/client/settings-form.d.ts +16 -0
- package/lib/types/core/controller.d.ts +37 -1
- package/lib/types/core/extension.d.ts +283 -0
- package/lib/types/core/handover.d.ts +13 -1
- package/lib/types/core/issue-form.d.ts +16 -0
- package/lib/types/core/tasks.d.ts +84 -3
- package/lib/types/core/use-cases/task-tag.d.ts +48 -0
- package/lib/types/core/use-cases/task-update.d.ts +5 -3
- package/lib/types/core/verification.d.ts +386 -0
- package/lib/types/core/workspace-target.d.ts +41 -0
- package/lib/types/host/agent-tools.d.ts +1 -1
- package/lib/types/host/extension-registry.d.ts +111 -0
- package/lib/types/host/llm-dispatch.d.ts +63 -0
- package/lib/types/host/verification-gate.d.ts +71 -0
- package/lib/types/host/verification-runner.d.ts +108 -0
- package/lib/types/host/workspace-evidence.d.ts +53 -0
- package/lib/types/host-ledger.d.ts +52 -8
- package/lib/types/host-runner.d.ts +23 -1
- package/lib/types/host-service.d.ts +88 -2
- package/lib/types/index.d.ts +80 -26
- package/lib/types/protocol.d.ts +49 -5
- package/lib/types/tool-surface.d.ts +71 -0
- package/package.json +27 -26
- package/src/client/PluginSettingsCard.tsx +9 -0
- package/src/client/TaskBoardSettingsCard.tsx +304 -65
- package/src/client/board/ClampedMarkdown.tsx +69 -0
- package/src/client/board/ConfirmDialog.tsx +7 -3
- package/src/client/board/EditTaskModal.tsx +5 -2
- package/src/client/board/LinkSubtaskModal.tsx +12 -7
- package/src/client/board/NewTaskModal.tsx +17 -1
- package/src/client/board/TagManagerModal.tsx +235 -0
- package/src/client/board/TaskBoard.tsx +259 -103
- package/src/client/board/TaskCard.tsx +214 -81
- package/src/client/board/TaskDetail.tsx +71 -26
- package/src/client/board/TaskForm.tsx +13 -3
- package/src/client/board/VerificationReport.tsx +203 -0
- package/src/client/board/card-view.ts +220 -0
- package/src/client/board/icons.tsx +89 -0
- package/src/client/board/overlay.tsx +189 -0
- package/src/client/board/status-key.ts +27 -0
- package/src/client/board/task-markdown.module.css +137 -0
- package/src/client/board/task-markdown.tsx +106 -0
- package/src/client/board-card-seat.ts +102 -0
- package/src/client/board-settings.module.css +66 -0
- package/src/client/board.module.css +488 -63
- package/src/client/index.ts +43 -5
- package/src/client/locales.ts +208 -4
- package/src/client/native-panel.tsx +18 -2
- package/src/client/seats.tsx +58 -0
- package/src/client/service.ts +94 -0
- package/src/client/settings-card.module.css +31 -5
- package/src/client/settings-form.ts +27 -1
- package/src/core/controller.ts +94 -3
- package/src/core/extension.ts +376 -0
- package/src/core/freeze-snapshot.ts +2 -2
- package/src/core/handover.ts +16 -1
- package/src/core/issue-form.ts +19 -0
- package/src/core/store.ts +16 -0
- package/src/core/tasks.ts +100 -6
- package/src/core/use-cases/task-parent.ts +3 -2
- package/src/core/use-cases/task-tag.ts +110 -0
- package/src/core/use-cases/task-update.ts +10 -4
- package/src/core/verification.ts +826 -0
- package/src/core/workspace-target.ts +59 -0
- package/src/host/agent-tools.ts +20 -18
- package/src/host/extension-registry.ts +509 -0
- package/src/host/llm-dispatch.ts +87 -0
- package/src/host/verification-gate.ts +331 -0
- package/src/host/verification-runner.ts +535 -0
- package/src/host/workspace-evidence.ts +176 -0
- package/src/host-ai.ts +15 -9
- package/src/host-ledger.ts +143 -32
- package/src/host-routes.ts +15 -2
- package/src/host-runner.ts +60 -11
- package/src/host-service.ts +253 -10
- package/src/index.ts +204 -16
- package/src/protocol.ts +102 -8
- package/src/tool-surface.ts +95 -0
|
@@ -36,8 +36,8 @@
|
|
|
36
36
|
border: 0;
|
|
37
37
|
border-radius: 12px;
|
|
38
38
|
align-items: center;
|
|
39
|
-
gap:
|
|
40
|
-
padding:
|
|
39
|
+
gap: 13px;
|
|
40
|
+
padding: 16px;
|
|
41
41
|
display: flex;
|
|
42
42
|
}
|
|
43
43
|
|
|
@@ -51,8 +51,8 @@
|
|
|
51
51
|
box-sizing: border-box;
|
|
52
52
|
border-radius: 12px;
|
|
53
53
|
align-items: center;
|
|
54
|
-
gap:
|
|
55
|
-
padding:
|
|
54
|
+
gap: 13px;
|
|
55
|
+
padding: 16px;
|
|
56
56
|
display: flex;
|
|
57
57
|
}
|
|
58
58
|
|
|
@@ -64,6 +64,25 @@
|
|
|
64
64
|
display: flex;
|
|
65
65
|
}
|
|
66
66
|
|
|
67
|
+
/* Optional leading plugin mark: a quiet tile that takes the ink color on
|
|
68
|
+
hover and while the card is open, so the whole row reads as one control. */
|
|
69
|
+
.mark {
|
|
70
|
+
flex: none;
|
|
71
|
+
display: grid;
|
|
72
|
+
place-items: center;
|
|
73
|
+
width: 34px;
|
|
74
|
+
height: 34px;
|
|
75
|
+
border-radius: 10px;
|
|
76
|
+
background: var(--dsw-alias-bg-module-platform);
|
|
77
|
+
color: var(--dsw-alias-label-secondary);
|
|
78
|
+
transition: color 0.16s;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
.header:hover .mark,
|
|
82
|
+
.cardOpen .mark {
|
|
83
|
+
color: var(--dsw-alias-label-primary);
|
|
84
|
+
}
|
|
85
|
+
|
|
67
86
|
.name {
|
|
68
87
|
color: var(--dsw-alias-label-primary);
|
|
69
88
|
font-size: 15px;
|
|
@@ -95,7 +114,13 @@
|
|
|
95
114
|
.chevron {
|
|
96
115
|
color: var(--dsw-alias-label-tertiary);
|
|
97
116
|
flex: none;
|
|
98
|
-
|
|
117
|
+
width: 16px;
|
|
118
|
+
height: 16px;
|
|
119
|
+
transition: transform 0.16s, color 0.16s;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
.header:hover .chevron {
|
|
123
|
+
color: var(--dsw-alias-label-secondary);
|
|
99
124
|
}
|
|
100
125
|
|
|
101
126
|
.chevronOpen {
|
|
@@ -406,6 +431,7 @@
|
|
|
406
431
|
@media (prefers-reduced-motion: reduce) {
|
|
407
432
|
.card,
|
|
408
433
|
.header,
|
|
434
|
+
.mark,
|
|
409
435
|
.chevron,
|
|
410
436
|
.chevronOpen,
|
|
411
437
|
.discard,
|
|
@@ -215,6 +215,8 @@ export class CardForm<T> {
|
|
|
215
215
|
private readonly disposeForm: () => void
|
|
216
216
|
private disposed = false
|
|
217
217
|
private saving = false
|
|
218
|
+
/** A save asked for while one was in flight; it runs once the first settles (#1754). */
|
|
219
|
+
private saveQueued = false
|
|
218
220
|
private failed = false
|
|
219
221
|
private failedReason: string | undefined
|
|
220
222
|
|
|
@@ -283,7 +285,7 @@ export class CardForm<T> {
|
|
|
283
285
|
resetField: (field) => {
|
|
284
286
|
this.stage(field, { text: this.specOf(field).format(this.baseValue(field)), clear: true })
|
|
285
287
|
},
|
|
286
|
-
save: () => { void this.
|
|
288
|
+
save: () => { void this.requestSave() },
|
|
287
289
|
discard: () => {
|
|
288
290
|
if (this.staged.size === 0 && !this.failed) return
|
|
289
291
|
this.staged.clear()
|
|
@@ -310,6 +312,30 @@ export class CardForm<T> {
|
|
|
310
312
|
* the user can correct them instead of retyping.
|
|
311
313
|
* @returns settlement after the mutation and the read-back.
|
|
312
314
|
*/
|
|
315
|
+
/**
|
|
316
|
+
* Run one save, and re-run it once if another was asked for while this one
|
|
317
|
+
* was still in flight.
|
|
318
|
+
*
|
|
319
|
+
* A save is a Host round trip that also drives the profile reconcile, and
|
|
320
|
+
* the Host runs that write inside one exclusive transaction. Answering a
|
|
321
|
+
* press that arrives mid-flight by returning immediately dropped the edit
|
|
322
|
+
* with no explanation, which is the "the save button stops working after a
|
|
323
|
+
* few rounds" report (#1754). Serializing instead means a save pressed while
|
|
324
|
+
* another is still settling runs against the settled state - which is what
|
|
325
|
+
* the operator meant by pressing it again.
|
|
326
|
+
* @returns settlement after the mutation and the read-back.
|
|
327
|
+
*/
|
|
328
|
+
async requestSave(): Promise<void> {
|
|
329
|
+
if (this.saving) {
|
|
330
|
+
this.saveQueued = true
|
|
331
|
+
return
|
|
332
|
+
}
|
|
333
|
+
await this.save()
|
|
334
|
+
if (!this.saveQueued) return
|
|
335
|
+
this.saveQueued = false
|
|
336
|
+
await this.requestSave()
|
|
337
|
+
}
|
|
338
|
+
|
|
313
339
|
async save(): Promise<void> {
|
|
314
340
|
const plan = this.plan()
|
|
315
341
|
const valid = plan.filter((item): item is PlannedWrite & { judge: () => boolean } => item.judge !== undefined)
|
package/src/core/controller.ts
CHANGED
|
@@ -12,13 +12,14 @@
|
|
|
12
12
|
* owns only the orchestration seam (state, persistence, notify, navigation).
|
|
13
13
|
*/
|
|
14
14
|
import type { TaskStore } from './store.ts'
|
|
15
|
-
import { withStatus, type NewTaskInput, type TaskRecord, type TaskStatus } from './tasks.ts'
|
|
15
|
+
import { hasOpenExecution, withStatus, type NewTaskInput, type TaskRecord, type TaskStatus } from './tasks.ts'
|
|
16
16
|
import { applyArchiveTask, applyRestoreTask } from './use-cases/task-archive.ts'
|
|
17
17
|
import { applyCreateTask } from './use-cases/task-create.ts'
|
|
18
18
|
import { applyDeleteTask } from './use-cases/task-delete.ts'
|
|
19
19
|
import { applyScheduleNextRun as applyScheduleRollForward, applySetSchedule } from './use-cases/task-schedule.ts'
|
|
20
20
|
import { resolveHostTimeZone } from './schedule.ts'
|
|
21
21
|
import { applySetParent } from './use-cases/task-parent.ts'
|
|
22
|
+
import { applyDeleteTag, applyRenameTag } from './use-cases/task-tag.ts'
|
|
22
23
|
import { applyUpdateTask, type TaskUpdatePatch } from './use-cases/task-update.ts'
|
|
23
24
|
import { DEFAULT_SUBTASK_DEPTH } from './subtask.ts'
|
|
24
25
|
import type {
|
|
@@ -28,6 +29,10 @@ import type {
|
|
|
28
29
|
TaskBoardParseRequest,
|
|
29
30
|
TaskBoardSnapshot,
|
|
30
31
|
} from '../protocol.ts'
|
|
32
|
+
import type {
|
|
33
|
+
TaskBoardExtensionActionRequest,
|
|
34
|
+
TaskBoardVisibilityPredicate,
|
|
35
|
+
} from './extension.ts'
|
|
31
36
|
|
|
32
37
|
export interface TaskBoardTransport {
|
|
33
38
|
bootstrap(legacy: readonly TaskRecord[]): Promise<TaskBoardSnapshot>
|
|
@@ -136,7 +141,7 @@ export interface ExecutionOptionsSnapshot {
|
|
|
136
141
|
* (revision/scheduler/power); a full snapshot also carries the deployment
|
|
137
142
|
* constants the UI reads (session default permission, subtask depth).
|
|
138
143
|
*/
|
|
139
|
-
export type HostMirror = Pick<TaskBoardSnapshot, 'revision' | 'scheduler' | 'power' | 'sessionDefaultPermission' | 'maxSubtaskDepth' | 'teamRunAvailable'>
|
|
144
|
+
export type HostMirror = Pick<TaskBoardSnapshot, 'revision' | 'scheduler' | 'power' | 'sessionDefaultPermission' | 'maxSubtaskDepth' | 'teamRunAvailable' | 'extensions'>
|
|
140
145
|
|
|
141
146
|
/** Immutable controller snapshot for UI subscriptions. */
|
|
142
147
|
export interface ControllerSnapshot {
|
|
@@ -154,6 +159,15 @@ export interface ControllerSnapshot {
|
|
|
154
159
|
canParseTask?: boolean
|
|
155
160
|
transportError?: string
|
|
156
161
|
host?: HostMirror
|
|
162
|
+
/**
|
|
163
|
+
* Provider card-visibility predicates, in registration order. A task shows
|
|
164
|
+
* only while every predicate accepts it; providers hide their own off-board
|
|
165
|
+
* cards here instead of the board hard-coding a provider rule. Optional so a
|
|
166
|
+
* hand-built snapshot (tests, a shell-less composition) defaults to visible.
|
|
167
|
+
*/
|
|
168
|
+
visibility?: readonly TaskBoardVisibilityPredicate[]
|
|
169
|
+
/** Read-only summaries published by running extensions, keyed by extension id. */
|
|
170
|
+
extensions?: Readonly<Record<string, unknown>>
|
|
157
171
|
}
|
|
158
172
|
|
|
159
173
|
/** The selected task (resolved from the ledger), or undefined. */
|
|
@@ -201,6 +215,8 @@ export class BoardController {
|
|
|
201
215
|
private readonly taskQueues = new Map<string, Promise<void>>()
|
|
202
216
|
private transportError: string | undefined
|
|
203
217
|
private hostState: HostMirror | undefined
|
|
218
|
+
/** Provider visibility predicates (immutable array; replaced on change). */
|
|
219
|
+
private visibility: readonly TaskBoardVisibilityPredicate[] = []
|
|
204
220
|
private remoteSubscribed = false
|
|
205
221
|
private remoteInitialization: Promise<boolean> | undefined
|
|
206
222
|
|
|
@@ -251,9 +267,42 @@ export class BoardController {
|
|
|
251
267
|
...(typeof this.deps.transport?.parseDraft === 'function' ? { canParseTask: true } : {}),
|
|
252
268
|
...(this.transportError === undefined ? {} : { transportError: this.transportError }),
|
|
253
269
|
...(this.hostState === undefined ? {} : { host: this.hostState }),
|
|
270
|
+
visibility: this.visibility,
|
|
271
|
+
...(this.hostState?.extensions === undefined ? {} : { extensions: this.hostState.extensions }),
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* Register one card-visibility predicate. The returned disposer removes it;
|
|
277
|
+
* both changes notify subscribers so the board re-renders.
|
|
278
|
+
*/
|
|
279
|
+
registerVisibility(predicate: TaskBoardVisibilityPredicate): () => void {
|
|
280
|
+
this.visibility = [...this.visibility, predicate]
|
|
281
|
+
this.notify()
|
|
282
|
+
return () => {
|
|
283
|
+
const next = this.visibility.filter(candidate => candidate !== predicate)
|
|
284
|
+
if (next.length === this.visibility.length) return
|
|
285
|
+
this.visibility = next
|
|
286
|
+
this.notify()
|
|
254
287
|
}
|
|
255
288
|
}
|
|
256
289
|
|
|
290
|
+
/**
|
|
291
|
+
* Deliver one provider action through the board's same-origin channel. A
|
|
292
|
+
* board without a Host transport (the legacy in-memory path) refuses it.
|
|
293
|
+
*/
|
|
294
|
+
async dispatchExtension(request: TaskBoardExtensionActionRequest): Promise<boolean> {
|
|
295
|
+
if (this.deps.transport === undefined) return false
|
|
296
|
+
const action: TaskBoardAction = {
|
|
297
|
+
kind: 'extension-action',
|
|
298
|
+
extensionId: request.extensionId,
|
|
299
|
+
action: request.action,
|
|
300
|
+
...(request.taskId === undefined ? {} : { taskId: request.taskId }),
|
|
301
|
+
...(request.payload === undefined ? {} : { payload: request.payload }),
|
|
302
|
+
}
|
|
303
|
+
return await this.commitRemote(action, request.taskId)
|
|
304
|
+
}
|
|
305
|
+
|
|
257
306
|
subscribe(fn: () => void): () => void {
|
|
258
307
|
this.listeners.add(fn)
|
|
259
308
|
return () => { this.listeners.delete(fn) }
|
|
@@ -507,6 +556,44 @@ export class BoardController {
|
|
|
507
556
|
return true
|
|
508
557
|
}
|
|
509
558
|
|
|
559
|
+
/**
|
|
560
|
+
* Rename one label across the whole ledger; a name already in use merges the
|
|
561
|
+
* two labels. Host-backed: the ledger owns the transition and confirms it, so
|
|
562
|
+
* the board's label row reflects whether the Host accepted the edit.
|
|
563
|
+
* @returns true when the edit was accepted by the authority.
|
|
564
|
+
*/
|
|
565
|
+
async renameTag(from: string, to: string): Promise<boolean> {
|
|
566
|
+
if (this.deps.transport !== undefined) {
|
|
567
|
+
return await this.commitRemote({ kind: 'rename-tag', from, to })
|
|
568
|
+
}
|
|
569
|
+
const result = applyRenameTag(this.tasks, from, to, this.now())
|
|
570
|
+
if (result.error !== undefined) return false
|
|
571
|
+
if (result.changed) {
|
|
572
|
+
this.tasks = [...result.tasks]
|
|
573
|
+
this.persistAndNotify()
|
|
574
|
+
}
|
|
575
|
+
return true
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
/**
|
|
579
|
+
* Remove one label from every task that carries it, board and archive alike —
|
|
580
|
+
* the board's label row is drawn from the whole ledger, so a label left on an
|
|
581
|
+
* archived card would come straight back.
|
|
582
|
+
* @returns true when the edit was accepted by the authority.
|
|
583
|
+
*/
|
|
584
|
+
async deleteTag(name: string): Promise<boolean> {
|
|
585
|
+
if (this.deps.transport !== undefined) {
|
|
586
|
+
return await this.commitRemote({ kind: 'delete-tag', name })
|
|
587
|
+
}
|
|
588
|
+
const result = applyDeleteTag(this.tasks, name, this.now())
|
|
589
|
+
if (result.error !== undefined) return false
|
|
590
|
+
if (result.changed) {
|
|
591
|
+
this.tasks = [...result.tasks]
|
|
592
|
+
this.persistAndNotify()
|
|
593
|
+
}
|
|
594
|
+
return true
|
|
595
|
+
}
|
|
596
|
+
|
|
510
597
|
// --- scheduling ---------------------------------------------------------------
|
|
511
598
|
|
|
512
599
|
/**
|
|
@@ -572,7 +659,9 @@ export class BoardController {
|
|
|
572
659
|
*/
|
|
573
660
|
async runTask(id: string): Promise<boolean> {
|
|
574
661
|
const task = this.tasks.find(candidate => candidate.id === id)
|
|
575
|
-
|
|
662
|
+
// Only an open execution blocks a second launch; the 'running' column is a
|
|
663
|
+
// board statement and a card parked there by hand still runs on request.
|
|
664
|
+
if (task === undefined || task.archivedAt !== undefined || hasOpenExecution(task)) return false
|
|
576
665
|
if (this.deps.transport === undefined) return false
|
|
577
666
|
return await this.commitRemote({ kind: 'run', taskId: id }, id, currentOf(this.deps.sessions))
|
|
578
667
|
}
|
|
@@ -724,6 +813,7 @@ export class BoardController {
|
|
|
724
813
|
const sessionDefaultPermission = snapshot.sessionDefaultPermission ?? this.hostState?.sessionDefaultPermission
|
|
725
814
|
const maxSubtaskDepth = snapshot.maxSubtaskDepth ?? this.hostState?.maxSubtaskDepth
|
|
726
815
|
const teamRunAvailable = snapshot.teamRunAvailable ?? this.hostState?.teamRunAvailable
|
|
816
|
+
const extensions = snapshot.extensions ?? this.hostState?.extensions
|
|
727
817
|
return {
|
|
728
818
|
revision: snapshot.revision,
|
|
729
819
|
scheduler: snapshot.scheduler,
|
|
@@ -731,6 +821,7 @@ export class BoardController {
|
|
|
731
821
|
...(sessionDefaultPermission === undefined ? {} : { sessionDefaultPermission }),
|
|
732
822
|
...(maxSubtaskDepth === undefined ? {} : { maxSubtaskDepth }),
|
|
733
823
|
...(teamRunAvailable === undefined ? {} : { teamRunAvailable }),
|
|
824
|
+
...(extensions === undefined ? {} : { extensions }),
|
|
734
825
|
}
|
|
735
826
|
}
|
|
736
827
|
|
|
@@ -0,0 +1,376 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The task board's external provider extension contract.
|
|
3
|
+
*
|
|
4
|
+
* This module is the single source of truth for how another plugin adds a
|
|
5
|
+
* provider surface to the task board: the cordis service name both halves
|
|
6
|
+
* publish, the API version they negotiate, the child seats a provider renders
|
|
7
|
+
* into, the host capability face a provider implements against, and the client
|
|
8
|
+
* capability face the board's browser half provides. It is framework-free and
|
|
9
|
+
* shared by the host and browser programs; the only imported SDK type is
|
|
10
|
+
* type-only, so the browser bundle never pulls a value out of an SDK package.
|
|
11
|
+
*
|
|
12
|
+
* @module dsh-task-board/core/extension
|
|
13
|
+
*/
|
|
14
|
+
import type { ToolDefinition } from '@deepseek-ai/dsh-tools'
|
|
15
|
+
import type { ExecutionOutcome, TaskRecord, TaskStatus } from './tasks.ts'
|
|
16
|
+
|
|
17
|
+
/** Cordis service name the host and browser halves each publish. */
|
|
18
|
+
export const TASK_BOARD_SERVICE_NAME = 'taskBoard'
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Contract version of {@link TASK_BOARD_SERVICE_NAME}. A provider declares the
|
|
22
|
+
* version it was built against; the board refuses (visibly) any other version
|
|
23
|
+
* instead of guessing at a stale shape.
|
|
24
|
+
*/
|
|
25
|
+
export const TASK_BOARD_API_VERSION = 1
|
|
26
|
+
|
|
27
|
+
/** Child seat a provider registers its task-detail section into. */
|
|
28
|
+
export const TASK_BOARD_DETAIL_SECTION = 'task-board.detail.section'
|
|
29
|
+
/** Child seat a provider registers its settings section into. */
|
|
30
|
+
export const TASK_BOARD_SETTINGS_SECTION = 'task-board.settings.section'
|
|
31
|
+
/** Child seat a provider registers its card decoration into. */
|
|
32
|
+
export const TASK_BOARD_CARD_DECORATION = 'task-board.card.decoration'
|
|
33
|
+
|
|
34
|
+
/** Every child seat the board declares for extensions, in render order. */
|
|
35
|
+
export const TASK_BOARD_EXTENSION_SEATS: readonly string[] = [
|
|
36
|
+
TASK_BOARD_DETAIL_SECTION,
|
|
37
|
+
TASK_BOARD_SETTINGS_SECTION,
|
|
38
|
+
TASK_BOARD_CARD_DECORATION,
|
|
39
|
+
]
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Largest accepted extension payload, in bytes of its JSON serialization. One
|
|
43
|
+
* integration entry — and one action payload — may carry schema the board does
|
|
44
|
+
* not interpret, but never enough of it to bloat the ledger or the action
|
|
45
|
+
* channel.
|
|
46
|
+
*/
|
|
47
|
+
export const TASK_BOARD_PAYLOAD_LIMIT_BYTES = 64 * 1024
|
|
48
|
+
|
|
49
|
+
/** Depth beyond which a payload is refused instead of walked. */
|
|
50
|
+
const PAYLOAD_DEPTH_LIMIT = 64
|
|
51
|
+
|
|
52
|
+
/** One opaque provider payload the board stores and forwards without interpreting. */
|
|
53
|
+
export type TaskBoardExtensionPayload = Record<string, unknown>
|
|
54
|
+
|
|
55
|
+
/** Whether a value is a plain JSON value (no undefined, function, symbol, bigint or cycle). */
|
|
56
|
+
function isJsonValue(value: unknown, depth: number): boolean {
|
|
57
|
+
if (depth > PAYLOAD_DEPTH_LIMIT) return false
|
|
58
|
+
if (value === null) return true
|
|
59
|
+
switch (typeof value) {
|
|
60
|
+
case 'string':
|
|
61
|
+
case 'boolean':
|
|
62
|
+
return true
|
|
63
|
+
case 'number':
|
|
64
|
+
return Number.isFinite(value)
|
|
65
|
+
// A provider's in-memory metadata legitimately clears a field by setting it
|
|
66
|
+
// to undefined; JSON serialization drops such a key, so the persisted
|
|
67
|
+
// payload is still pure JSON.
|
|
68
|
+
case 'undefined':
|
|
69
|
+
return true
|
|
70
|
+
case 'object':
|
|
71
|
+
break
|
|
72
|
+
default:
|
|
73
|
+
return false
|
|
74
|
+
}
|
|
75
|
+
if (Array.isArray(value)) return value.every(item => isJsonValue(item, depth + 1))
|
|
76
|
+
const prototype = Object.getPrototypeOf(value)
|
|
77
|
+
if (prototype !== Object.prototype && prototype !== null) return false
|
|
78
|
+
return Object.values(value as Record<string, unknown>).every(item => isJsonValue(item, depth + 1))
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Validate one extension payload: a pure JSON object whose serialization fits
|
|
83
|
+
* {@link TASK_BOARD_PAYLOAD_LIMIT_BYTES}. Arrays, primitives and non-plain
|
|
84
|
+
* objects are refused; the board never interprets the keys.
|
|
85
|
+
* @param value - candidate payload.
|
|
86
|
+
* @returns whether the value is an acceptable opaque payload.
|
|
87
|
+
*/
|
|
88
|
+
export function isTaskBoardExtensionPayload(value: unknown): value is TaskBoardExtensionPayload {
|
|
89
|
+
if (typeof value !== 'object' || value === null || Array.isArray(value)) return false
|
|
90
|
+
if (!isJsonValue(value, 0)) return false
|
|
91
|
+
try {
|
|
92
|
+
return new TextEncoder().encode(JSON.stringify(value)).length <= TASK_BOARD_PAYLOAD_LIMIT_BYTES
|
|
93
|
+
} catch {
|
|
94
|
+
return false
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** Byte length of a value's JSON serialization, or undefined when it does not serialize. */
|
|
99
|
+
export function taskBoardPayloadBytes(value: unknown): number | undefined {
|
|
100
|
+
try {
|
|
101
|
+
return new TextEncoder().encode(JSON.stringify(value)).length
|
|
102
|
+
} catch {
|
|
103
|
+
return undefined
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Normalize a task's whole integrations container: a plain JSON object whose
|
|
109
|
+
* values are each an opaque provider payload within the size limit. A malformed
|
|
110
|
+
* container normalizes to undefined (the field is dropped), and one malformed
|
|
111
|
+
* entry is dropped alone so a single bad provider can never take a card — or
|
|
112
|
+
* another provider's data — with it.
|
|
113
|
+
* @param value - candidate container from the wire, the ledger, or a create input.
|
|
114
|
+
* @returns the cleaned container, or undefined when nothing usable remains.
|
|
115
|
+
*/
|
|
116
|
+
export function normalizeTaskIntegrations(value: unknown): Record<string, TaskBoardExtensionPayload> | undefined {
|
|
117
|
+
if (typeof value !== 'object' || value === null || Array.isArray(value)) return undefined
|
|
118
|
+
const prototype = Object.getPrototypeOf(value)
|
|
119
|
+
if (prototype !== Object.prototype && prototype !== null) return undefined
|
|
120
|
+
const result: Record<string, TaskBoardExtensionPayload> = {}
|
|
121
|
+
for (const [key, entry] of Object.entries(value as Record<string, unknown>)) {
|
|
122
|
+
if (key === '') continue
|
|
123
|
+
if (!isTaskBoardExtensionPayload(entry)) continue
|
|
124
|
+
result[key] = entry
|
|
125
|
+
}
|
|
126
|
+
return Object.keys(result).length === 0 ? undefined : result
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** One action a provider dispatches over the board's same-origin channel. */
|
|
130
|
+
export interface TaskBoardExtensionActionRequest {
|
|
131
|
+
/** Id of the extension the action belongs to. */
|
|
132
|
+
extensionId: string
|
|
133
|
+
/** Provider-owned action name, routed back to that extension's handleAction. */
|
|
134
|
+
action: string
|
|
135
|
+
/** Task the action targets, when it targets one. */
|
|
136
|
+
taskId?: string
|
|
137
|
+
/** Opaque provider payload, bounded like every other extension payload. */
|
|
138
|
+
payload?: TaskBoardExtensionPayload
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Deliver one extension action through the board's same-origin action channel.
|
|
143
|
+
* The extension never opens an HTTP surface of its own.
|
|
144
|
+
*/
|
|
145
|
+
export type TaskBoardExtensionDispatch = (request: TaskBoardExtensionActionRequest) => Promise<boolean>
|
|
146
|
+
|
|
147
|
+
/** Draft one provider creates a task from. */
|
|
148
|
+
export interface TaskBoardTaskDraft {
|
|
149
|
+
title: string
|
|
150
|
+
description: string
|
|
151
|
+
prompt: string
|
|
152
|
+
/** Column the created card opens in; absent means the board default (todo). */
|
|
153
|
+
status?: TaskStatus
|
|
154
|
+
/** Parent card id, when the provider materializes a subtask. */
|
|
155
|
+
parentId?: string
|
|
156
|
+
/**
|
|
157
|
+
* Workspace the execution must run in. Absent leaves the board's own
|
|
158
|
+
* inheritance rules in charge (the creating session's workspace for a root
|
|
159
|
+
* card, else the recent workspace at execution time), so a provider that has
|
|
160
|
+
* no reason to know better pins nothing.
|
|
161
|
+
*/
|
|
162
|
+
workspaceId?: string
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/** Options of {@link TaskBoardTasksFace.create}. */
|
|
166
|
+
export interface TaskBoardCreateOptions {
|
|
167
|
+
/** Opaque payload stored under the creating extension's id. */
|
|
168
|
+
payload?: TaskBoardExtensionPayload
|
|
169
|
+
/** Create the card off-board: it exists in the ledger but is never shown on the board. */
|
|
170
|
+
hidden?: boolean
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/** One task linked to an extension, with that extension's own payload. */
|
|
174
|
+
export interface TaskBoardLinkedTask {
|
|
175
|
+
task: TaskRecord
|
|
176
|
+
payload: TaskBoardExtensionPayload
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/** Task reads and writes the board grants an extension. */
|
|
180
|
+
export interface TaskBoardTasksFace {
|
|
181
|
+
/** Every task in the ledger, most recent mutation last. */
|
|
182
|
+
list(): readonly TaskRecord[]
|
|
183
|
+
/** One task by id, or undefined. */
|
|
184
|
+
get(taskId: string): TaskRecord | undefined
|
|
185
|
+
/**
|
|
186
|
+
* Create a task through the board's own creation gate; the payload lands
|
|
187
|
+
* under the creating extension's id. Throws on a refused draft.
|
|
188
|
+
*/
|
|
189
|
+
create(draft: TaskBoardTaskDraft, options?: TaskBoardCreateOptions): TaskRecord
|
|
190
|
+
/**
|
|
191
|
+
* Patch a task's content. Reuses the board's own content gate: a task that
|
|
192
|
+
* has started executing keeps its recorded title/description/prompt.
|
|
193
|
+
* Throws when the card is frozen.
|
|
194
|
+
*/
|
|
195
|
+
patchContent(taskId: string, patch: { title?: string; description?: string; prompt?: string }): void
|
|
196
|
+
/**
|
|
197
|
+
* Move a task through the board's existing status gates (running lock,
|
|
198
|
+
* permission confirmation gate). Throws on refusal.
|
|
199
|
+
*/
|
|
200
|
+
setStatus(taskId: string, status: TaskStatus, initiator?: string): void
|
|
201
|
+
/** Every task carrying this extension's payload. */
|
|
202
|
+
linked(): readonly TaskBoardLinkedTask[]
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/** Storage face for one extension's own payload on a task. */
|
|
206
|
+
export interface TaskBoardIntegrationFace {
|
|
207
|
+
/** Read this extension's payload, or undefined when the task carries none. */
|
|
208
|
+
read(taskId: string): TaskBoardExtensionPayload | undefined
|
|
209
|
+
/**
|
|
210
|
+
* Shallow-merge this extension's payload into the task: the given keys
|
|
211
|
+
* replace the stored ones, and a key explicitly set to undefined clears it.
|
|
212
|
+
* The board stores the result opaquely; a non-JSON or oversized payload is
|
|
213
|
+
* refused.
|
|
214
|
+
*/
|
|
215
|
+
write(taskId: string, payload: TaskBoardExtensionPayload): void
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/** Status-change notification. */
|
|
219
|
+
export interface TaskBoardStatusChangedEvent {
|
|
220
|
+
taskId: string
|
|
221
|
+
status: TaskStatus
|
|
222
|
+
previous: TaskStatus
|
|
223
|
+
initiator?: string
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/** Execution-settlement notification. */
|
|
227
|
+
export interface TaskBoardExecutionSettledEvent {
|
|
228
|
+
taskId: string
|
|
229
|
+
executionId: string
|
|
230
|
+
outcome: ExecutionOutcome
|
|
231
|
+
error?: string
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/** Task-deletion notification. */
|
|
235
|
+
export interface TaskBoardTaskDeletedEvent {
|
|
236
|
+
taskId: string
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Event subscriptions an extension registers for. Every callback runs isolated:
|
|
241
|
+
* a throw is logged and never interrupts the board's own execution path.
|
|
242
|
+
*/
|
|
243
|
+
export interface TaskBoardEventFace {
|
|
244
|
+
onStatusChanged(callback: (event: TaskBoardStatusChangedEvent) => void): () => void
|
|
245
|
+
onExecutionSettled(callback: (event: TaskBoardExecutionSettledEvent) => void): () => void
|
|
246
|
+
onTaskDeleted(callback: (event: TaskBoardTaskDeletedEvent) => void): () => void
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Host capability face handed to one extension while it is started. The face
|
|
251
|
+
* lives for the extension's enabled lifetime; the board revokes it by calling
|
|
252
|
+
* the extension's stop().
|
|
253
|
+
*/
|
|
254
|
+
export interface TaskBoardExtensionHost {
|
|
255
|
+
tasks: TaskBoardTasksFace
|
|
256
|
+
integration: TaskBoardIntegrationFace
|
|
257
|
+
events: TaskBoardEventFace
|
|
258
|
+
/**
|
|
259
|
+
* Publish this extension's read-only summary; it lands in the snapshot's
|
|
260
|
+
* extensions[id] for the extension's browser half to render.
|
|
261
|
+
*/
|
|
262
|
+
publish(summary: TaskBoardExtensionPayload): void
|
|
263
|
+
/** Register one model-visible tool; the returned disposer unregisters it. */
|
|
264
|
+
registerTool(definition: ToolDefinition): () => void
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/** One external provider contributed to the task board. */
|
|
268
|
+
export interface TaskBoardExtension {
|
|
269
|
+
/** Stable extension id; also the key of this extension's integration payload. */
|
|
270
|
+
readonly id: string
|
|
271
|
+
/** Contract version this extension was built against. */
|
|
272
|
+
readonly apiVersion: number
|
|
273
|
+
/** Volatile per-extension switch; absent means enabled. Read at use time. */
|
|
274
|
+
enabled?(): boolean
|
|
275
|
+
/** Acquire the capability face and start any provider-side work. */
|
|
276
|
+
start?(host: TaskBoardExtensionHost): void
|
|
277
|
+
/** Release provider-side work. Never clears stored data. */
|
|
278
|
+
stop?(): void
|
|
279
|
+
/** Route one dispatched action; the returned value goes back to the dispatcher. */
|
|
280
|
+
handleAction?(request: TaskBoardExtensionActionRequest): unknown | Promise<unknown>
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/** Board mirror an extension's browser half observes. */
|
|
284
|
+
export interface TaskBoardClientMirror {
|
|
285
|
+
/** Whether the board's master switch currently runs the extension faces. */
|
|
286
|
+
enabled: boolean
|
|
287
|
+
/** Tasks the board currently holds. */
|
|
288
|
+
tasks: readonly TaskRecord[]
|
|
289
|
+
/** Published extension summaries, keyed by extension id (opaque to the board). */
|
|
290
|
+
extensions: Readonly<Record<string, unknown>>
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/** Whether a task should be shown on the board; false hides it. */
|
|
294
|
+
export type TaskBoardVisibilityPredicate = (task: TaskRecord) => boolean
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* Client capability face the board's browser half provides. A provider's
|
|
298
|
+
* browser half resolves it from the shared cordis service name; it never
|
|
299
|
+
* value-imports the board package.
|
|
300
|
+
*/
|
|
301
|
+
export interface TaskBoardClientFace {
|
|
302
|
+
/** Deliver one action through the board's same-origin action channel. */
|
|
303
|
+
dispatch(request: TaskBoardExtensionActionRequest): Promise<boolean>
|
|
304
|
+
/** Register one card visibility predicate; the returned disposer removes it. */
|
|
305
|
+
registerVisibility(predicate: TaskBoardVisibilityPredicate): () => void
|
|
306
|
+
/** Subscribe to the board mirror. */
|
|
307
|
+
subscribe(callback: (mirror: TaskBoardClientMirror) => void): () => void
|
|
308
|
+
/** Read the current board mirror. */
|
|
309
|
+
snapshot(): TaskBoardClientMirror
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/** Owner props of the task-detail seat. */
|
|
313
|
+
export interface TaskBoardDetailSectionProps {
|
|
314
|
+
task: TaskRecord
|
|
315
|
+
dispatch: TaskBoardExtensionDispatch
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/** Owner props of the settings seat. */
|
|
319
|
+
export interface TaskBoardSettingsSectionProps {
|
|
320
|
+
dispatch: TaskBoardExtensionDispatch
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/** Owner props of the card-decoration seat. */
|
|
324
|
+
export interface TaskBoardCardDecorationProps {
|
|
325
|
+
task: TaskRecord
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/**
|
|
329
|
+
* Host-half service face published under {@link TASK_BOARD_SERVICE_NAME}. A
|
|
330
|
+
* provider package's host half resolves this from the shared cordis service
|
|
331
|
+
* and registers its extension; the board's ledger never crosses this boundary.
|
|
332
|
+
*/
|
|
333
|
+
export interface TaskBoardHostFace {
|
|
334
|
+
/**
|
|
335
|
+
* Admit one provider extension. Idempotent by extension id; the returned
|
|
336
|
+
* disposer releases it without clearing stored data.
|
|
337
|
+
*/
|
|
338
|
+
registerExtension(extension: TaskBoardExtension): () => void
|
|
339
|
+
/** Whether the board currently runs a given extension (board x extension). */
|
|
340
|
+
isExtensionEnabled(extensionId: string): boolean
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
declare module '@deepseek-ai/cordis' {
|
|
344
|
+
interface Context {
|
|
345
|
+
/**
|
|
346
|
+
* The board's extension service. The host half publishes
|
|
347
|
+
* {@link TaskBoardHostFace}; the browser half publishes
|
|
348
|
+
* {@link TaskBoardClientFace} under the same name. The two halves never run
|
|
349
|
+
* in one process, so the browser half resolves its face through
|
|
350
|
+
* {@link resolveTaskBoardClientFace} and this shared declaration names the
|
|
351
|
+
* host face that registration-side consumers type against.
|
|
352
|
+
*/
|
|
353
|
+
taskBoard: TaskBoardHostFace
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
/** The narrow context seat {@link resolveTaskBoardClientFace} needs. */
|
|
358
|
+
export interface TaskBoardClientContextSeat {
|
|
359
|
+
get(name: string): unknown
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
/**
|
|
363
|
+
* Resolve the browser half's client capability face by service name. Returns
|
|
364
|
+
* undefined when the board is not loaded or the service is not served, so a
|
|
365
|
+
* provider's browser half can degrade instead of throwing.
|
|
366
|
+
* @param ctx - client context.
|
|
367
|
+
* @returns the client face, or undefined.
|
|
368
|
+
*/
|
|
369
|
+
export function resolveTaskBoardClientFace(ctx: TaskBoardClientContextSeat): TaskBoardClientFace | undefined {
|
|
370
|
+
try {
|
|
371
|
+
const face = ctx.get(TASK_BOARD_SERVICE_NAME) as TaskBoardClientFace | undefined
|
|
372
|
+
return face !== undefined && typeof (face as { dispatch?: unknown }).dispatch === 'function' ? face : undefined
|
|
373
|
+
} catch {
|
|
374
|
+
return undefined
|
|
375
|
+
}
|
|
376
|
+
}
|
|
@@ -48,8 +48,8 @@ const SECTION_NAMES: Record<SectionKey, string> = {
|
|
|
48
48
|
/**
|
|
49
49
|
* Sensitive patterns, each matched globally over every field body:
|
|
50
50
|
* PEM private key blocks (whole block collapses to one marker), Bearer
|
|
51
|
-
* credentials, OpenAI sk-,
|
|
52
|
-
* and AWS access key ids.
|
|
51
|
+
* credentials, OpenAI sk-, `ghp_`-shaped provider tokens, GitLab glpat-,
|
|
52
|
+
* Slack xox* tokens, and AWS access key ids.
|
|
53
53
|
*/
|
|
54
54
|
const SENSITIVE_PATTERNS: RegExp[] = [
|
|
55
55
|
/-----BEGIN [A-Z ]*PRIVATE KEY-----[\s\S]*?-----END [A-Z ]*PRIVATE KEY-----/g,
|