dsh-taskboard 0.5.4 → 0.6.0
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 +27 -160
- package/lib/client.js +2564 -678
- package/lib/host/execution.js +3 -0
- package/lib/host/execution.js.map +1 -1
- package/lib/host/routes.js +31 -1
- package/lib/host/routes.js.map +1 -1
- package/lib/host/session-sync.js +449 -0
- package/lib/host/session-sync.js.map +1 -0
- package/lib/host/store.js +9 -2
- package/lib/host/store.js.map +1 -1
- package/lib/index.js +109 -2
- package/lib/index.js.map +1 -1
- package/lib/shared/api.js.map +1 -1
- package/lib/shared/protocol.js +27 -1
- package/lib/shared/protocol.js.map +1 -1
- package/package.json +75 -75
- package/src/client/api.ts +8 -0
- package/src/client/board/AlertModal.tsx +3 -1
- package/src/client/board/ImportModal.tsx +26 -24
- package/src/client/board/SettingsModal.tsx +98 -22
- package/src/client/board/SlashPromptInput.tsx +272 -0
- package/src/client/board/TaskBoard.tsx +53 -49
- package/src/client/board/TaskCard.tsx +33 -21
- package/src/client/board/TaskDetail.tsx +169 -104
- package/src/client/board/TaskFormModal.tsx +254 -202
- package/src/client/board/TemplateManager.tsx +32 -29
- package/src/client/board/labels.ts +36 -27
- package/src/client/controller.ts +62 -2
- package/src/client/i18n/en.ts +455 -0
- package/src/client/i18n/runtime.ts +155 -0
- package/src/client/i18n/zh.ts +460 -0
- package/src/client/index.ts +182 -42
- package/src/client/sidebar-entry.ts +13 -3
- package/src/client/styles.ts +131 -0
- package/src/host/execution.ts +14 -1
- package/src/host/routes.ts +49 -1
- package/src/host/session-sync.ts +650 -0
- package/src/host/store.ts +15 -1
- package/src/index.ts +125 -1
- package/src/shared/api.ts +49 -0
- package/src/shared/protocol.ts +54 -0
- package/src/shared/version.ts +1 -1
package/src/index.ts
CHANGED
|
@@ -27,6 +27,7 @@ import { SchedulerService } from './host/scheduler.ts'
|
|
|
27
27
|
import { dshHomePath } from './host/sdk.ts'
|
|
28
28
|
import { TaskStore } from './host/store.ts'
|
|
29
29
|
import { TemplateStore } from './host/templates.ts'
|
|
30
|
+
import { ExternalSessionSyncService } from './host/session-sync.ts'
|
|
30
31
|
import { registerTaskboardTools, workspaceFace } from './host/tools.ts'
|
|
31
32
|
|
|
32
33
|
/** Ledger file name under the DSH home. */
|
|
@@ -93,15 +94,41 @@ export function apply(ctx: Context): void {
|
|
|
93
94
|
// Settlement listener over the session event bus.
|
|
94
95
|
const events: EventsFace = {
|
|
95
96
|
onSessionEvent: (listener) => wsCtx.on('session/event', (session, event) => {
|
|
96
|
-
listener(session.id, event as { type: string; data?: unknown })
|
|
97
|
+
listener(session.id, event as { type: string; data?: unknown }, session as never)
|
|
97
98
|
}),
|
|
98
99
|
}
|
|
99
100
|
|
|
101
|
+
let agentSessions: { get?: (id: string) => unknown; list?: () => unknown[] } | undefined
|
|
102
|
+
|
|
103
|
+
// External workspace sessions sync service (0.5.4).
|
|
104
|
+
const sessionSync = new ExternalSessionSyncService({
|
|
105
|
+
store,
|
|
106
|
+
workspaces: workspaceFace(wsCtx.workspaceRegistry),
|
|
107
|
+
events,
|
|
108
|
+
sessions: {
|
|
109
|
+
get: id => {
|
|
110
|
+
try {
|
|
111
|
+
const registry = (agentSessions ?? wsCtx.get('sessions') ?? wsCtx.get('sessionRegistry') ?? wsCtx.root?.get('sessions')) as { get?: (id: string) => unknown } | undefined
|
|
112
|
+
return registry?.get?.(id)
|
|
113
|
+
} catch { return undefined }
|
|
114
|
+
},
|
|
115
|
+
list: () => {
|
|
116
|
+
try {
|
|
117
|
+
const registry = (agentSessions ?? wsCtx.get('sessions') ?? wsCtx.get('sessionRegistry') ?? wsCtx.root?.get('sessions')) as { list?: () => unknown[] } | undefined
|
|
118
|
+
return registry?.list?.() ?? []
|
|
119
|
+
} catch { return [] }
|
|
120
|
+
},
|
|
121
|
+
},
|
|
122
|
+
now,
|
|
123
|
+
})
|
|
124
|
+
disposers.push(() => sessionSync.dispose())
|
|
125
|
+
|
|
100
126
|
// The narrow git face shared by execution (worktree isolation) and the
|
|
101
127
|
// routes (merge / remove / workspace detection).
|
|
102
128
|
const git = createGitFace()
|
|
103
129
|
|
|
104
130
|
wsCtx.inject(['agents'], (agentCtx: Context) => {
|
|
131
|
+
agentSessions = agentCtx.get('sessions') as { get?: (id: string) => unknown; list?: () => unknown[] } | undefined
|
|
105
132
|
const execution = new ExecutionService({
|
|
106
133
|
store,
|
|
107
134
|
agents: {
|
|
@@ -150,6 +177,16 @@ export function apply(ctx: Context): void {
|
|
|
150
177
|
return read === undefined ? undefined : read.call(selection)
|
|
151
178
|
} catch { return undefined }
|
|
152
179
|
},
|
|
180
|
+
setPermission: (sessionId, permission) => {
|
|
181
|
+
try {
|
|
182
|
+
const permService = agentCtx.get('permissionPresets') as { set(session: unknown, name: string): void } | undefined
|
|
183
|
+
const sessions = agentCtx.get('sessions') as { get(id: string): unknown } | undefined
|
|
184
|
+
const session = sessions?.get(sessionId)
|
|
185
|
+
if (session !== undefined && permService !== undefined) {
|
|
186
|
+
permService.set(session, permission)
|
|
187
|
+
}
|
|
188
|
+
} catch { /* cosmetic */ }
|
|
189
|
+
},
|
|
153
190
|
maxConcurrent,
|
|
154
191
|
})
|
|
155
192
|
|
|
@@ -165,6 +202,93 @@ export function apply(ctx: Context): void {
|
|
|
165
202
|
modelProviders,
|
|
166
203
|
git,
|
|
167
204
|
templates,
|
|
205
|
+
promptCompletions: async () => {
|
|
206
|
+
try {
|
|
207
|
+
const skillsService = agentCtx.get('skills') as { list?(options?: unknown): Promise<Array<{ name: string; description?: string }>> } | undefined
|
|
208
|
+
const commandsService = agentCtx.get('commands') as { list?(): Array<{ name: string; description?: string; input?: { hint?: string } }> } | undefined
|
|
209
|
+
const rawSkills = skillsService?.list ? await skillsService.list().catch(() => []) : []
|
|
210
|
+
const rawCommands = commandsService?.list ? commandsService.list() : []
|
|
211
|
+
return {
|
|
212
|
+
skills: Array.isArray(rawSkills) ? rawSkills.map(s => ({ name: s.name, description: s.description })) : [],
|
|
213
|
+
commands: Array.isArray(rawCommands) ? rawCommands.map(c => ({ name: c.name, description: c.description, hint: c.input?.hint })) : [],
|
|
214
|
+
}
|
|
215
|
+
} catch {
|
|
216
|
+
return { skills: [], commands: [] }
|
|
217
|
+
}
|
|
218
|
+
},
|
|
219
|
+
modelCatalog: async () => {
|
|
220
|
+
try {
|
|
221
|
+
type ModelItem = {
|
|
222
|
+
provider: string
|
|
223
|
+
model: string
|
|
224
|
+
name?: string
|
|
225
|
+
description?: string
|
|
226
|
+
reasoning?: {
|
|
227
|
+
efforts: Array<{ id: string; name: string; description?: string }>
|
|
228
|
+
defaultEffort?: string
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
const models: ModelItem[] = []
|
|
232
|
+
|
|
233
|
+
const llm = (agentCtx.get('llm') ?? wsCtx.get('llm')) as {
|
|
234
|
+
listProviders?(): Array<{ id: string; name?: string }>
|
|
235
|
+
listModels?(provider: string): Promise<Array<{ id: string; name?: string; description?: string }>>
|
|
236
|
+
resolveModelInfo?(provider: string, model: string): Promise<{ reasoning?: { efforts: Array<{ id: string; name: string; description?: string }>; defaultEffort?: string } }>
|
|
237
|
+
resolveModel?(provider: string, model: string): Promise<{ reasoning?: { efforts: Array<{ id: string; name: string; description?: string }>; defaultEffort?: string } }>
|
|
238
|
+
} | undefined
|
|
239
|
+
|
|
240
|
+
if (llm?.listProviders !== undefined && llm.listModels !== undefined) {
|
|
241
|
+
const providers = llm.listProviders()
|
|
242
|
+
for (const p of providers) {
|
|
243
|
+
try {
|
|
244
|
+
const list = await llm.listModels(p.id)
|
|
245
|
+
for (const m of list) {
|
|
246
|
+
let reasoning: { efforts: Array<{ id: string; name: string; description?: string }>; defaultEffort?: string } | undefined
|
|
247
|
+
try {
|
|
248
|
+
const meta = llm.resolveModelInfo !== undefined
|
|
249
|
+
? await llm.resolveModelInfo(p.id, m.id)
|
|
250
|
+
: llm.resolveModel !== undefined ? await llm.resolveModel(p.id, m.id) : undefined
|
|
251
|
+
if (meta?.reasoning !== undefined) {
|
|
252
|
+
reasoning = meta.reasoning
|
|
253
|
+
}
|
|
254
|
+
} catch { /* ignore */ }
|
|
255
|
+
|
|
256
|
+
models.push({
|
|
257
|
+
provider: p.id,
|
|
258
|
+
model: m.id,
|
|
259
|
+
name: m.name,
|
|
260
|
+
...(m.description ? { description: m.description } : {}),
|
|
261
|
+
...(reasoning !== undefined ? { reasoning } : {}),
|
|
262
|
+
})
|
|
263
|
+
}
|
|
264
|
+
} catch { /* continue */ }
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
const presetsService = agentCtx.get('agentPresets') as {
|
|
269
|
+
list?(): Promise<{ ok: boolean; value?: { presets: Array<{ id: string; name?: string; isDefault?: boolean }> } } | Array<{ id: string; name?: string; isDefault?: boolean }>>
|
|
270
|
+
} | undefined
|
|
271
|
+
const presets: Array<{ id: string; name?: string }> = []
|
|
272
|
+
let defaultPresetId: string | undefined
|
|
273
|
+
|
|
274
|
+
if (presetsService?.list !== undefined) {
|
|
275
|
+
try {
|
|
276
|
+
const raw = await presetsService.list()
|
|
277
|
+
const list = (raw as { ok?: boolean; value?: { presets?: unknown[] } }).ok === true
|
|
278
|
+
? (raw as { value: { presets: Array<{ id: string; name?: string; isDefault?: boolean }> } }).value.presets
|
|
279
|
+
: Array.isArray(raw) ? raw : []
|
|
280
|
+
for (const p of list) {
|
|
281
|
+
presets.push({ id: p.id, name: p.name })
|
|
282
|
+
if (p.isDefault) defaultPresetId = p.id
|
|
283
|
+
}
|
|
284
|
+
} catch { /* continue */ }
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
return { models, presets, ...(defaultPresetId !== undefined ? { defaultPresetId } : {}) }
|
|
288
|
+
} catch {
|
|
289
|
+
return { models: [], presets: [] }
|
|
290
|
+
}
|
|
291
|
+
},
|
|
168
292
|
})
|
|
169
293
|
return () => disposeRoutes?.()
|
|
170
294
|
})
|
package/src/shared/api.ts
CHANGED
|
@@ -56,6 +56,8 @@ export type CreateTaskBody = {
|
|
|
56
56
|
isolation?: string
|
|
57
57
|
/** Agent preset for execution sessions; omitted = deployment default. */
|
|
58
58
|
presetId?: string
|
|
59
|
+
/** Execution permission preset ('workspace-write' | 'read-only' | 'danger-full-access'); omitted = default. */
|
|
60
|
+
permission?: string
|
|
59
61
|
/** Acceptance checklist item texts (host mints ids, all unchecked). */
|
|
60
62
|
checklist?: string[]
|
|
61
63
|
}
|
|
@@ -76,6 +78,8 @@ export type UpdateTaskBody = {
|
|
|
76
78
|
isolation?: string
|
|
77
79
|
/** Change the execution preset (takes effect on the next run). */
|
|
78
80
|
presetId?: string | null
|
|
81
|
+
/** Change the execution permission (0.5.5; 'workspace-write' | 'read-only' | 'danger-full-access'). */
|
|
82
|
+
permission?: string | null
|
|
79
83
|
/** Replace the whole checklist (GUI owner surface); null clears it. */
|
|
80
84
|
checklist?: unknown
|
|
81
85
|
}
|
|
@@ -133,6 +137,8 @@ export type TaskTemplateSpec = {
|
|
|
133
137
|
model?: TaskModel
|
|
134
138
|
isolation?: string
|
|
135
139
|
presetId?: string
|
|
140
|
+
/** Execution permission preset (0.5.5). */
|
|
141
|
+
permission?: string
|
|
136
142
|
/** Checklist item texts (host mints ids at create time). */
|
|
137
143
|
checklist?: string[]
|
|
138
144
|
}
|
|
@@ -158,6 +164,49 @@ export type SettingsResponse = BoardSettings
|
|
|
158
164
|
export type UpdateSettingsBody = {
|
|
159
165
|
/** Default code isolation for NEW tasks ('worktree' | 'none'). */
|
|
160
166
|
defaultIsolation?: string
|
|
167
|
+
/** Automatically capture external workspace sessions into the taskboard. */
|
|
168
|
+
syncExternalSessions?: boolean
|
|
169
|
+
/** Default permission preset for NEW tasks ('workspace-write' | 'read-only' | 'danger-full-access'). */
|
|
170
|
+
defaultPermission?: string
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/** Prompt completion item for skills and slash commands (0.5.5). */
|
|
174
|
+
export type PromptCompletionItem = {
|
|
175
|
+
name: string
|
|
176
|
+
kind: 'skill' | 'command'
|
|
177
|
+
description?: string
|
|
178
|
+
hint?: string
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/** Prompt completions response (0.5.5). */
|
|
182
|
+
export type PromptCompletionsResponse = {
|
|
183
|
+
commands: PromptCompletionItem[]
|
|
184
|
+
skills: PromptCompletionItem[]
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/** Model item in catalog (0.5.5). */
|
|
188
|
+
export type CatalogModelItem = {
|
|
189
|
+
provider: string
|
|
190
|
+
model: string
|
|
191
|
+
name?: string
|
|
192
|
+
description?: string
|
|
193
|
+
reasoning?: {
|
|
194
|
+
efforts: Array<{ id: string; name: string; description?: string }>
|
|
195
|
+
defaultEffort?: string
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/** Preset item in catalog (0.5.5). */
|
|
200
|
+
export type CatalogPresetItem = {
|
|
201
|
+
id: string
|
|
202
|
+
name?: string
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/** Model and preset catalog response (0.5.5). */
|
|
206
|
+
export type ModelCatalogResponse = {
|
|
207
|
+
models: CatalogModelItem[]
|
|
208
|
+
presets: CatalogPresetItem[]
|
|
209
|
+
defaultPresetId?: string
|
|
161
210
|
}
|
|
162
211
|
|
|
163
212
|
/** Import dry-run response (0.4.0): every task classified, nothing written. */
|
package/src/shared/protocol.ts
CHANGED
|
@@ -134,6 +134,30 @@ export function effectiveIsolation(task: Pick<TaskRecord, 'isolation'>): Isolati
|
|
|
134
134
|
return task.isolation === undefined ? DEFAULT_ISOLATION : task.isolation
|
|
135
135
|
}
|
|
136
136
|
|
|
137
|
+
/**
|
|
138
|
+
* Execution permission preset (0.5.5). Matches DSH permission presets:
|
|
139
|
+
* - 'workspace-write': 可写入工作区 (factory default)
|
|
140
|
+
* - 'read-only': 仅可查看
|
|
141
|
+
* - 'danger-full-access': 完全权限
|
|
142
|
+
*/
|
|
143
|
+
export type PermissionMode = 'workspace-write' | 'read-only' | 'danger-full-access'
|
|
144
|
+
|
|
145
|
+
/** Factory default permission preset (0.5.5). */
|
|
146
|
+
export const DEFAULT_PERMISSION: PermissionMode = 'workspace-write'
|
|
147
|
+
|
|
148
|
+
/** Canonical list of supported permission modes. */
|
|
149
|
+
export const ALL_PERMISSIONS: readonly PermissionMode[] = ['workspace-write', 'read-only', 'danger-full-access']
|
|
150
|
+
|
|
151
|
+
/** Validate and normalize a permission string into a valid {@link PermissionMode}. */
|
|
152
|
+
export function asPermission(raw: unknown): PermissionMode {
|
|
153
|
+
if (typeof raw !== 'string') return DEFAULT_PERMISSION
|
|
154
|
+
const normalized = raw.trim()
|
|
155
|
+
if (normalized === 'workspace-write' || normalized === 'workspaceWrite') return 'workspace-write'
|
|
156
|
+
if (normalized === 'read-only' || normalized === 'readOnly') return 'read-only'
|
|
157
|
+
if (normalized === 'danger-full-access' || normalized === 'fullAccess') return 'danger-full-access'
|
|
158
|
+
throw new Error("permission must be 'workspace-write', 'read-only', or 'danger-full-access'")
|
|
159
|
+
}
|
|
160
|
+
|
|
137
161
|
/**
|
|
138
162
|
* Board-level settings persisted with the ledger (0.5.0). Only fields the
|
|
139
163
|
* user explicitly set are present; absent fields follow factory defaults.
|
|
@@ -141,6 +165,10 @@ export function effectiveIsolation(task: Pick<TaskRecord, 'isolation'>): Isolati
|
|
|
141
165
|
export type BoardSettings = {
|
|
142
166
|
/** Default code isolation applied when a NEW task is created without an explicit choice. */
|
|
143
167
|
defaultIsolation?: IsolationMode
|
|
168
|
+
/** Automatically capture external workspace sessions into the taskboard (default: false). */
|
|
169
|
+
syncExternalSessions?: boolean
|
|
170
|
+
/** Default permission preset applied when a NEW task is created without an explicit choice (0.5.5, default: 'workspace-write'). */
|
|
171
|
+
defaultPermission?: PermissionMode
|
|
144
172
|
}
|
|
145
173
|
|
|
146
174
|
/** Validate raw input into sanitized {@link BoardSettings} (unknown fields dropped). */
|
|
@@ -156,6 +184,15 @@ export function asBoardSettings(raw: unknown): BoardSettings {
|
|
|
156
184
|
}
|
|
157
185
|
out.defaultIsolation = asIsolation(e.defaultIsolation)
|
|
158
186
|
}
|
|
187
|
+
if (e.syncExternalSessions !== undefined) {
|
|
188
|
+
if (typeof e.syncExternalSessions !== 'boolean') {
|
|
189
|
+
throw new Error('syncExternalSessions must be a boolean')
|
|
190
|
+
}
|
|
191
|
+
out.syncExternalSessions = e.syncExternalSessions
|
|
192
|
+
}
|
|
193
|
+
if (e.defaultPermission !== undefined) {
|
|
194
|
+
out.defaultPermission = asPermission(e.defaultPermission)
|
|
195
|
+
}
|
|
159
196
|
return out
|
|
160
197
|
}
|
|
161
198
|
|
|
@@ -164,6 +201,16 @@ export function defaultIsolationOf(settings?: BoardSettings): IsolationMode {
|
|
|
164
201
|
return settings?.defaultIsolation ?? DEFAULT_ISOLATION
|
|
165
202
|
}
|
|
166
203
|
|
|
204
|
+
/** The effective external session sync switch (board setting → factory default false). */
|
|
205
|
+
export function defaultSyncExternalSessionsOf(settings?: BoardSettings): boolean {
|
|
206
|
+
return settings?.syncExternalSessions ?? false
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/** The effective default permission preset for NEW tasks (board setting → factory default 'workspace-write'). */
|
|
210
|
+
export function defaultPermissionOf(settings?: BoardSettings): PermissionMode {
|
|
211
|
+
return settings?.defaultPermission ?? DEFAULT_PERMISSION
|
|
212
|
+
}
|
|
213
|
+
|
|
167
214
|
/** How a task may run. */
|
|
168
215
|
export type ExecutionMode = 'claim' | 'scheduled'
|
|
169
216
|
|
|
@@ -406,6 +453,10 @@ export type TaskRecord = {
|
|
|
406
453
|
* tool set. Editable any time (each run composes fresh).
|
|
407
454
|
*/
|
|
408
455
|
presetId?: string
|
|
456
|
+
/**
|
|
457
|
+
* Execution permission preset (0.5.5; see {@link PermissionMode}).
|
|
458
|
+
*/
|
|
459
|
+
permission?: PermissionMode
|
|
409
460
|
/**
|
|
410
461
|
* Definition-of-Done acceptance checklist (0.4.0). Agents may append items
|
|
411
462
|
* and check/uncheck them (with evidence); the GUI may edit the whole list.
|
|
@@ -906,6 +957,7 @@ export function validateImportedTask(raw: unknown, now: number): { ok: true; tas
|
|
|
906
957
|
...(typeof e.model === 'object' && e.model !== null ? { model: normalizeModel(e.model) } : {}),
|
|
907
958
|
...(typeof e.isolation === 'string' && (e.isolation === 'worktree' || e.isolation === 'none') ? { isolation: e.isolation } : {}),
|
|
908
959
|
...(typeof e.presetId === 'string' && e.presetId.trim().length > 0 ? { presetId: e.presetId.trim() } : {}),
|
|
960
|
+
...(typeof e.permission === 'string' ? { permission: asPermission(e.permission) } : {}),
|
|
909
961
|
...(Array.isArray(e.checklist) ? { checklist: normalizeChecklist(e.checklist) } : {}),
|
|
910
962
|
...(typeof e.branch === 'string' ? { branch: e.branch } : {}),
|
|
911
963
|
...(status === 'in_progress' && typeof e.claimedBy === 'string' ? { claimedBy: e.claimedBy } : {}),
|
|
@@ -997,6 +1049,7 @@ export type TaskSummary = {
|
|
|
997
1049
|
executionMode: ExecutionMode
|
|
998
1050
|
nextRunAt?: number
|
|
999
1051
|
model?: TaskModel
|
|
1052
|
+
permission?: PermissionMode
|
|
1000
1053
|
version: number
|
|
1001
1054
|
claimOwner?: string
|
|
1002
1055
|
commentCount: number
|
|
@@ -1023,6 +1076,7 @@ export function summarize(task: TaskRecord): TaskSummary {
|
|
|
1023
1076
|
executionMode: task.execution.mode,
|
|
1024
1077
|
nextRunAt: task.execution.nextRunAt,
|
|
1025
1078
|
model: task.model,
|
|
1079
|
+
permission: task.permission,
|
|
1026
1080
|
version: task.version,
|
|
1027
1081
|
claimOwner: isClaimedBy(task),
|
|
1028
1082
|
commentCount: task.comments.length,
|
package/src/shared/version.ts
CHANGED