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.
Files changed (42) hide show
  1. package/README.md +27 -160
  2. package/lib/client.js +2564 -678
  3. package/lib/host/execution.js +3 -0
  4. package/lib/host/execution.js.map +1 -1
  5. package/lib/host/routes.js +31 -1
  6. package/lib/host/routes.js.map +1 -1
  7. package/lib/host/session-sync.js +449 -0
  8. package/lib/host/session-sync.js.map +1 -0
  9. package/lib/host/store.js +9 -2
  10. package/lib/host/store.js.map +1 -1
  11. package/lib/index.js +109 -2
  12. package/lib/index.js.map +1 -1
  13. package/lib/shared/api.js.map +1 -1
  14. package/lib/shared/protocol.js +27 -1
  15. package/lib/shared/protocol.js.map +1 -1
  16. package/package.json +75 -75
  17. package/src/client/api.ts +8 -0
  18. package/src/client/board/AlertModal.tsx +3 -1
  19. package/src/client/board/ImportModal.tsx +26 -24
  20. package/src/client/board/SettingsModal.tsx +98 -22
  21. package/src/client/board/SlashPromptInput.tsx +272 -0
  22. package/src/client/board/TaskBoard.tsx +53 -49
  23. package/src/client/board/TaskCard.tsx +33 -21
  24. package/src/client/board/TaskDetail.tsx +169 -104
  25. package/src/client/board/TaskFormModal.tsx +254 -202
  26. package/src/client/board/TemplateManager.tsx +32 -29
  27. package/src/client/board/labels.ts +36 -27
  28. package/src/client/controller.ts +62 -2
  29. package/src/client/i18n/en.ts +455 -0
  30. package/src/client/i18n/runtime.ts +155 -0
  31. package/src/client/i18n/zh.ts +460 -0
  32. package/src/client/index.ts +182 -42
  33. package/src/client/sidebar-entry.ts +13 -3
  34. package/src/client/styles.ts +131 -0
  35. package/src/host/execution.ts +14 -1
  36. package/src/host/routes.ts +49 -1
  37. package/src/host/session-sync.ts +650 -0
  38. package/src/host/store.ts +15 -1
  39. package/src/index.ts +125 -1
  40. package/src/shared/api.ts +49 -0
  41. package/src/shared/protocol.ts +54 -0
  42. 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. */
@@ -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,
@@ -6,4 +6,4 @@
6
6
  */
7
7
 
8
8
  /** The package version (must equal package.json "version"). */
9
- export const PLUGIN_VERSION = '0.5.4'
9
+ export const PLUGIN_VERSION = '0.6.0'