dsh-taskboard 0.2.2 → 0.3.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.
- package/README.md +44 -4
- package/lib/client.js +635 -23
- package/lib/host/execution.js +194 -54
- package/lib/host/execution.js.map +1 -1
- package/lib/host/git.js +234 -0
- package/lib/host/git.js.map +1 -0
- package/lib/host/routes.js +252 -4
- package/lib/host/routes.js.map +1 -1
- package/lib/host/tools.js +16 -2
- package/lib/host/tools.js.map +1 -1
- package/lib/index.js +17 -2
- package/lib/index.js.map +1 -1
- package/lib/shared/api.js.map +1 -1
- package/lib/shared/protocol.js +10 -1
- package/lib/shared/protocol.js.map +1 -1
- package/package.json +74 -74
- package/src/client/api.ts +19 -3
- package/src/client/board/TaskBoard.tsx +73 -0
- package/src/client/board/TaskDetail.tsx +173 -1
- package/src/client/board/TaskFormModal.tsx +100 -1
- package/src/client/controller.ts +89 -6
- package/src/client/index.ts +18 -1
- package/src/client/styles.ts +45 -0
- package/src/host/execution.ts +291 -65
- package/src/host/git.ts +293 -0
- package/src/host/routes.ts +268 -5
- package/src/host/tools.ts +17 -0
- package/src/index.ts +24 -1
- package/src/shared/api.ts +35 -3
- package/src/shared/protocol.ts +64 -0
- package/src/shared/version.ts +1 -1
package/src/host/tools.ts
CHANGED
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
import type { WorkspaceRegistry } from '@deepseek-ai/dsh-workspace'
|
|
22
22
|
import { defineTool } from './sdk.ts'
|
|
23
23
|
import {
|
|
24
|
+
asIsolation,
|
|
24
25
|
asStatus,
|
|
25
26
|
asUrgency,
|
|
26
27
|
canTransition,
|
|
@@ -74,11 +75,13 @@ function taskDetail(t: TaskRecord & { effectivePrompt?: string }): string {
|
|
|
74
75
|
`任务 ${t.id} 「${t.title}」`,
|
|
75
76
|
`状态: ${t.status} (v${t.version}) · 紧急度: ${t.urgency} · 项目: ${t.workspaceId}${t.blocked ? ' · 受阻' : ''}`,
|
|
76
77
|
`执行方式: ${t.execution.mode}${t.execution.cron !== undefined ? ` cron=${t.execution.cron}` : ''}`,
|
|
78
|
+
`隔离: ${t.isolation === 'none' ? '关闭(原目录执行)' : 'Git Worktree'}${t.branch !== undefined ? `(分支 ${t.branch})` : ''}`,
|
|
77
79
|
]
|
|
78
80
|
const holder = isClaimedBy(t)
|
|
79
81
|
if (holder !== undefined) lines.push(`认领: agent ${String(holder).slice(0, 24)}(持有期间其他会话不可移动)`)
|
|
80
82
|
if (t.execution.nextRunAt !== undefined) lines.push(`下次触发: ${new Date(t.execution.nextRunAt).toISOString()}`)
|
|
81
83
|
if (t.model !== undefined) lines.push(`固定模型: ${t.model.provider}/${t.model.model}`)
|
|
84
|
+
if (t.presetId !== undefined) lines.push(`执行模式: ${t.presetId}(未指定时为部署默认 preset)`)
|
|
82
85
|
lines.push(`描述: ${t.description.length > 0 ? t.description : '(无)'}`)
|
|
83
86
|
lines.push(`执行 Prompt: ${t.effectivePrompt ?? effectivePrompt(t)}`)
|
|
84
87
|
if (t.comments.length > 0) {
|
|
@@ -348,6 +351,14 @@ export function registerTaskboardTools(ctx: ToolContextFace, deps: ToolDeps): Ar
|
|
|
348
351
|
model: { type: 'string', description: 'Provider-owned model id.' },
|
|
349
352
|
},
|
|
350
353
|
},
|
|
354
|
+
isolation: {
|
|
355
|
+
type: 'string',
|
|
356
|
+
description: 'Code isolation for executions: "worktree" (default — each run gets a fresh git worktree on branch task/<标题>+<taskId>) or "none" (run in the project directory, zero git interaction).',
|
|
357
|
+
},
|
|
358
|
+
presetId: {
|
|
359
|
+
type: 'string',
|
|
360
|
+
description: 'Agent preset the execution session is composed from (its tool set / persona); default = the deployment default preset. Optional.',
|
|
361
|
+
},
|
|
351
362
|
},
|
|
352
363
|
output: {
|
|
353
364
|
schema: JSON_OUT,
|
|
@@ -366,6 +377,8 @@ export function registerTaskboardTools(ctx: ToolContextFace, deps: ToolDeps): Ar
|
|
|
366
377
|
prompt?: string
|
|
367
378
|
execution?: { mode?: string; cron?: string }
|
|
368
379
|
model?: { provider?: string; model?: string }
|
|
380
|
+
isolation?: string
|
|
381
|
+
presetId?: string
|
|
369
382
|
}, exec: unknown) {
|
|
370
383
|
try {
|
|
371
384
|
const { actor } = caller(exec as ToolRunContext)
|
|
@@ -380,6 +393,8 @@ export function registerTaskboardTools(ctx: ToolContextFace, deps: ToolDeps): Ar
|
|
|
380
393
|
}
|
|
381
394
|
const execution = normalizeExecution(args.execution ?? {}, deps.now())
|
|
382
395
|
const model = args.model !== undefined ? checkModel(deps, args.model) : undefined
|
|
396
|
+
const isolation = args.isolation === undefined ? undefined : asIsolation(args.isolation)
|
|
397
|
+
const presetId = args.presetId?.trim() || undefined
|
|
383
398
|
const now = deps.now()
|
|
384
399
|
const task: TaskRecord = {
|
|
385
400
|
id: newTaskId(),
|
|
@@ -392,6 +407,8 @@ export function registerTaskboardTools(ctx: ToolContextFace, deps: ToolDeps): Ar
|
|
|
392
407
|
blocked: false,
|
|
393
408
|
execution,
|
|
394
409
|
model,
|
|
410
|
+
...(isolation !== undefined ? { isolation } : {}),
|
|
411
|
+
...(presetId !== undefined ? { presetId } : {}),
|
|
395
412
|
version: 1,
|
|
396
413
|
createdAt: now,
|
|
397
414
|
updatedAt: now,
|
package/src/index.ts
CHANGED
|
@@ -21,6 +21,7 @@ import type {} from '@deepseek-ai/dsh-system-prompt'
|
|
|
21
21
|
import type {} from '@deepseek-ai/dsh-agent'
|
|
22
22
|
import { PROTOCOL_SECTION_NAME, PROTOCOL_SECTION_ORDER, TASKBOARD_PROTOCOL } from './host/protocol-text.ts'
|
|
23
23
|
import { DEFAULT_MAX_CONCURRENT, ExecutionService, type EventsFace } from './host/execution.ts'
|
|
24
|
+
import { createGitFace } from './host/git.ts'
|
|
24
25
|
import { registerTaskboardRoutes } from './host/routes.ts'
|
|
25
26
|
import { SchedulerService } from './host/scheduler.ts'
|
|
26
27
|
import { dshHomePath } from './host/sdk.ts'
|
|
@@ -85,6 +86,10 @@ export function apply(ctx: Context): void {
|
|
|
85
86
|
}),
|
|
86
87
|
}
|
|
87
88
|
|
|
89
|
+
// The narrow git face shared by execution (worktree isolation) and the
|
|
90
|
+
// routes (merge / remove / workspace detection).
|
|
91
|
+
const git = createGitFace()
|
|
92
|
+
|
|
88
93
|
wsCtx.inject(['agents'], (agentCtx: Context) => {
|
|
89
94
|
const execution = new ExecutionService({
|
|
90
95
|
store,
|
|
@@ -100,6 +105,23 @@ export function apply(ctx: Context): void {
|
|
|
100
105
|
},
|
|
101
106
|
events,
|
|
102
107
|
now,
|
|
108
|
+
git,
|
|
109
|
+
// Preset composition (0.3.3): mirror apiproxy's composeAgent — resolve
|
|
110
|
+
// the id BEFORE creation (the session header snapshots meta), mount
|
|
111
|
+
// inside the factory's setup callback. No roster service → undefined
|
|
112
|
+
// (bare host composition, the pre-preset behavior).
|
|
113
|
+
composeAgent: async (presetId) => {
|
|
114
|
+
const presets = agentCtx.get('agentPresets') as {
|
|
115
|
+
resolve(id?: string): Promise<{ id: string }>
|
|
116
|
+
mount(agentCtx: unknown, id?: string): Promise<unknown>
|
|
117
|
+
} | undefined
|
|
118
|
+
if (presets === undefined) return undefined
|
|
119
|
+
const resolved = await presets.resolve(presetId)
|
|
120
|
+
return {
|
|
121
|
+
agentPreset: resolved.id,
|
|
122
|
+
setup: async (ctx: unknown) => { await presets.mount(ctx, resolved.id) },
|
|
123
|
+
}
|
|
124
|
+
},
|
|
103
125
|
renameSession: (sessionId, title) => {
|
|
104
126
|
// Best-effort: pin the execution session's title to the task title
|
|
105
127
|
// through the log-backed session-title service (user-sourced rename).
|
|
@@ -127,9 +149,10 @@ export function apply(ctx: Context): void {
|
|
|
127
149
|
store,
|
|
128
150
|
workspaces: workspaceFace(wsCtx.workspaceRegistry),
|
|
129
151
|
now,
|
|
130
|
-
run: (taskId: string) => execution.run(taskId, 'manual'),
|
|
152
|
+
run: (taskId: string, runOptions?: { reuseWorktree?: boolean }) => execution.run(taskId, 'manual', runOptions),
|
|
131
153
|
cancel: (taskId: string) => execution.cancel(taskId),
|
|
132
154
|
modelProviders,
|
|
155
|
+
git,
|
|
133
156
|
})
|
|
134
157
|
return () => disposeRoutes?.()
|
|
135
158
|
})
|
package/src/shared/api.ts
CHANGED
|
@@ -41,7 +41,7 @@ export type ApiResult<T> = ApiOk<T> | ApiFail
|
|
|
41
41
|
export type StateResponse = TaskLedger
|
|
42
42
|
|
|
43
43
|
/** Workspace listing for the UI pickers. */
|
|
44
|
-
export type WorkspaceView = { id: string; path: string; title: string; sessionCount: number }
|
|
44
|
+
export type WorkspaceView = { id: string; path: string; title: string; sessionCount: number; gitAvailable?: boolean }
|
|
45
45
|
|
|
46
46
|
/** Create-task request body (actor is always the GUI user). */
|
|
47
47
|
export type CreateTaskBody = {
|
|
@@ -52,6 +52,10 @@ export type CreateTaskBody = {
|
|
|
52
52
|
prompt?: string
|
|
53
53
|
execution?: { mode?: string; cron?: string }
|
|
54
54
|
model?: { provider: string; model: string }
|
|
55
|
+
/** Code isolation for executions ('worktree' | 'none'); omitted = default. */
|
|
56
|
+
isolation?: string
|
|
57
|
+
/** Agent preset for execution sessions; omitted = deployment default. */
|
|
58
|
+
presetId?: string
|
|
55
59
|
}
|
|
56
60
|
|
|
57
61
|
/** Update-task request body (ifVersion mandatory). */
|
|
@@ -66,6 +70,10 @@ export type UpdateTaskBody = {
|
|
|
66
70
|
workspaceId?: string
|
|
67
71
|
execution?: { mode?: string; cron?: string }
|
|
68
72
|
model?: { provider: string; model: string } | null
|
|
73
|
+
/** Change isolation; locked once the task has execution history. */
|
|
74
|
+
isolation?: string
|
|
75
|
+
/** Change the execution preset (takes effect on the next run). */
|
|
76
|
+
presetId?: string | null
|
|
69
77
|
}
|
|
70
78
|
|
|
71
79
|
/** Move-task request body (ifVersion mandatory; the user MAY move to done). */
|
|
@@ -84,8 +92,32 @@ export type CommentBody = { body: string }
|
|
|
84
92
|
/** Delete request body (purge=true physically removes a trashed task). */
|
|
85
93
|
export type DeleteTaskBody = { ifVersion?: number; purge?: boolean }
|
|
86
94
|
|
|
87
|
-
/** Run request body (
|
|
88
|
-
export type RunTaskBody =
|
|
95
|
+
/** Run request body; `reuse: true` = 续跑 (keep a live worktree as-is). */
|
|
96
|
+
export type RunTaskBody = { reuse?: boolean }
|
|
97
|
+
|
|
98
|
+
/** Merge outcome: `noop: true` = the branch had no commits over HEAD (nothing merged). */
|
|
99
|
+
export type MergeBranchResponse = { merged: boolean; noop?: boolean; branch: string }
|
|
100
|
+
|
|
101
|
+
/** Remove a task's worktree; optionally delete its branch too. */
|
|
102
|
+
export type WorktreeRemoveBody = { deleteBranch?: boolean }
|
|
103
|
+
|
|
104
|
+
/** One orphan worktree directory (exists on disk, owned by no live task). */
|
|
105
|
+
export type OrphanWorktree = { workspaceId: string; workspacePath: string; taskId: string; path: string }
|
|
106
|
+
|
|
107
|
+
/** A git-enabled workspace whose .gitignore does not cover the worktree dir. */
|
|
108
|
+
export type GitignoreSuggestion = { workspaceId: string; workspacePath: string }
|
|
109
|
+
|
|
110
|
+
/** Health-diagnostics response (⚙ panel). */
|
|
111
|
+
export type DiagnosticsResponse = {
|
|
112
|
+
revision: number
|
|
113
|
+
tasks: number
|
|
114
|
+
/** Executions currently marked `running`. */
|
|
115
|
+
staleRunning: number
|
|
116
|
+
/** Worktree directories whose task no longer exists in the ledger. */
|
|
117
|
+
orphanWorktrees: OrphanWorktree[]
|
|
118
|
+
/** Git workspaces whose .gitignore does not ignore the worktree dir. */
|
|
119
|
+
gitIgnoreSuggestions: GitignoreSuggestion[]
|
|
120
|
+
}
|
|
89
121
|
|
|
90
122
|
/** One task (full record) response. */
|
|
91
123
|
export type TaskResponse = TaskRecord
|
package/src/shared/protocol.ts
CHANGED
|
@@ -102,6 +102,29 @@ export const URGENCY_COLOR: Readonly<Record<Urgency, string>> = {
|
|
|
102
102
|
// Execution
|
|
103
103
|
// ---------------------------------------------------------------------------
|
|
104
104
|
|
|
105
|
+
/**
|
|
106
|
+
* Per-task code isolation mode (0.3.0).
|
|
107
|
+
* - `worktree`: each execution runs in a fresh `git worktree` on a dedicated
|
|
108
|
+
* task branch (`task/<标题>+<taskId>`) under `<workspace>/.dsh-worktrees/`.
|
|
109
|
+
* - `none`: run in the workspace directory as before, zero git interaction.
|
|
110
|
+
* Omitted = the default `worktree`; non-git projects auto-degrade at run
|
|
111
|
+
* time (the execution record carries an `isolationNote` explaining why).
|
|
112
|
+
*/
|
|
113
|
+
export type IsolationMode = 'worktree' | 'none'
|
|
114
|
+
|
|
115
|
+
/** Validate an isolation value. */
|
|
116
|
+
export function asIsolation(raw: string): IsolationMode {
|
|
117
|
+
if (raw !== 'worktree' && raw !== 'none') {
|
|
118
|
+
throw new Error("isolation must be 'worktree' or 'none'")
|
|
119
|
+
}
|
|
120
|
+
return raw
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** Resolve a task's effective isolation (omitted → the worktree default). */
|
|
124
|
+
export function effectiveIsolation(task: Pick<TaskRecord, 'isolation'>): IsolationMode {
|
|
125
|
+
return task.isolation === undefined ? 'worktree' : task.isolation
|
|
126
|
+
}
|
|
127
|
+
|
|
105
128
|
/** How a task may run. */
|
|
106
129
|
export type ExecutionMode = 'claim' | 'scheduled'
|
|
107
130
|
|
|
@@ -237,6 +260,9 @@ export type CommentRecord = {
|
|
|
237
260
|
threadId?: string
|
|
238
261
|
}
|
|
239
262
|
|
|
263
|
+
/** One commit produced by an isolated execution (hash + subject). */
|
|
264
|
+
export type CommitInfo = { hash: string; subject: string }
|
|
265
|
+
|
|
240
266
|
/** One execution attempt of a task. */
|
|
241
267
|
export type ExecutionRecord = {
|
|
242
268
|
id: string
|
|
@@ -248,6 +274,30 @@ export type ExecutionRecord = {
|
|
|
248
274
|
endedAt?: number
|
|
249
275
|
outcome: 'running' | 'succeeded' | 'failed' | 'cancelled'
|
|
250
276
|
error?: string
|
|
277
|
+
/** Code isolation actually used (`none` also covers degraded worktree runs). */
|
|
278
|
+
isolation?: IsolationMode
|
|
279
|
+
/** Why worktree isolation degraded to running in the original directory. */
|
|
280
|
+
isolationNote?: string
|
|
281
|
+
/** The task branch this execution worked on (worktree runs only). */
|
|
282
|
+
branch?: string
|
|
283
|
+
/** Absolute path of the dedicated worktree (worktree runs only). */
|
|
284
|
+
worktreePath?: string
|
|
285
|
+
/** HEAD of the task branch before the execution started. */
|
|
286
|
+
baseCommit?: string
|
|
287
|
+
/** HEAD at settlement. */
|
|
288
|
+
headCommit?: string
|
|
289
|
+
/** Commits between baseCommit and headCommit (hash + subject; capped at 50, newest first). */
|
|
290
|
+
commits?: CommitInfo[]
|
|
291
|
+
/** Total commits before the evidence cap (equals commits.length when under it). */
|
|
292
|
+
commitsTotal?: number
|
|
293
|
+
/** Uncommitted changes present at settlement (`git status --porcelain` lines; capped at 100). */
|
|
294
|
+
dirtyFiles?: string[]
|
|
295
|
+
/** Total uncommitted lines before the evidence cap. */
|
|
296
|
+
dirtyFilesTotal?: number
|
|
297
|
+
/** Aggregate diff stat between baseCommit and headCommit. */
|
|
298
|
+
diffStat?: string
|
|
299
|
+
/** How many files differ between baseCommit and headCommit. */
|
|
300
|
+
changedFiles?: number
|
|
251
301
|
}
|
|
252
302
|
|
|
253
303
|
/** The per-model override a task may carry; absent = session default model. */
|
|
@@ -271,6 +321,20 @@ export type TaskRecord = {
|
|
|
271
321
|
blocked: boolean
|
|
272
322
|
execution: ExecutionConfig
|
|
273
323
|
model?: TaskModel
|
|
324
|
+
/** Code isolation for executions (omitted = the worktree default; see {@link IsolationMode}). */
|
|
325
|
+
isolation?: IsolationMode
|
|
326
|
+
/**
|
|
327
|
+
* The agent preset execution sessions are composed from (omitted = the
|
|
328
|
+
* deployment default preset). Recorded on the session header and mounted
|
|
329
|
+
* via the presets service at creation — this is what hands the session its
|
|
330
|
+
* tool set. Editable any time (each run composes fresh).
|
|
331
|
+
*/
|
|
332
|
+
presetId?: string
|
|
333
|
+
/**
|
|
334
|
+
* The task branch fixed at the FIRST worktree creation (`task/<标题>+<taskId>`).
|
|
335
|
+
* Renaming the task afterwards never changes it (history preservation).
|
|
336
|
+
*/
|
|
337
|
+
branch?: string
|
|
274
338
|
/**
|
|
275
339
|
* The session currently holding the in-progress claim (explicit claim or a
|
|
276
340
|
* live execution). Present only while `status === 'in_progress'`: any move
|
package/src/shared/version.ts
CHANGED