dsh-taskboard 0.5.5 → 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 +22 -2
  2. package/lib/client.js +2604 -767
  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 +210 -10
  8. package/lib/host/session-sync.js.map +1 -1
  9. package/lib/host/store.js +9 -2
  10. package/lib/host/store.js.map +1 -1
  11. package/lib/index.js +100 -1
  12. package/lib/index.js.map +1 -1
  13. package/lib/shared/api.js.map +1 -1
  14. package/lib/shared/protocol.js +19 -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 +66 -26
  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 +13 -0
  36. package/src/host/routes.ts +49 -1
  37. package/src/host/session-sync.ts +334 -14
  38. package/src/host/store.ts +15 -1
  39. package/src/index.ts +115 -1
  40. package/src/shared/api.ts +47 -0
  41. package/src/shared/protocol.ts +41 -0
  42. package/src/shared/version.ts +1 -1
@@ -13,6 +13,7 @@
13
13
  */
14
14
  import { createClient } from './api.ts'
15
15
  import { BoardController } from './controller.ts'
16
+ import { disposeI18n, initI18n } from './i18n/runtime.ts'
16
17
  import { injectStyles } from './styles.ts'
17
18
  import { mountSidebarEntry } from './sidebar-entry.ts'
18
19
  import { mountBoard } from './board-mount.tsx'
@@ -67,54 +68,192 @@ interface ClientContextFace {
67
68
  export function apply(ctx: ClientContextFace): void {
68
69
  try {
69
70
  injectStyles()
71
+ // Locale source (设置 → 通用设置 → 语言): soft-attached — absent on
72
+ // compositions without the DSH locale plugin, where the fallback
73
+ // (<html lang> / navigator) takes over. Never a hard inject.
74
+ initI18n(ctx.get?.('locale'))
70
75
  const client = createClient()
71
76
  const controller = new BoardController(client)
72
77
 
73
- // Model catalog for the composer: llm.models over the connection RPC —
74
- // installed through the controller's formal installer (T13: no more
75
- // monkeypatched instance properties).
76
- const connection = ctx.get?.('connection') as ConnectionFace | undefined
77
- if (connection !== undefined) {
78
- type CatalogRow = {
79
- provider: string
80
- model: string
81
- name?: string
82
- reasoning?: {
83
- efforts: Array<{ id: string; name: string; description?: string }>
84
- defaultEffort?: string
85
- }
78
+ // Model catalog for the composer (0.5.5): multi-tier discovery
79
+ // 1. DSH ui-model-selection service: ctx.get('modelDirectories')?.catalog?.load()
80
+ // 2. DSH Remote RPC: ctx.get('remote')?.session?.modelCatalog()
81
+ // 3. Legacy connection.api face if present
82
+ // 4. Taskboard host endpoint: /dsh-taskboard/model-catalog
83
+ type CatalogRow = {
84
+ provider: string
85
+ model: string
86
+ name?: string
87
+ description?: string
88
+ reasoning?: {
89
+ efforts: Array<{ id: string; name: string; description?: string }>
90
+ defaultEffort?: string
86
91
  }
87
- controller.installModelCatalog(async (): Promise<CatalogRow[]> => {
88
- const response = await connection.api.llm.models({})
89
- if (!response.result.ok) return []
90
- const out: CatalogRow[] = []
91
- for (const group of response.result.value.groups) {
92
- for (const model of group.models) {
93
- out.push({
94
- provider: group.id,
95
- model: model.id,
96
- name: model.name,
97
- ...(model.reasoning !== undefined ? { reasoning: model.reasoning } : {}),
98
- })
92
+ }
93
+ controller.installModelCatalog(async (): Promise<CatalogRow[]> => {
94
+ // 1. DSH ui-model-selection service
95
+ try {
96
+ const modelDirs = (ctx.get?.('modelDirectories') ?? (ctx as Record<string, unknown>).modelDirectories) as {
97
+ catalog?: { load: () => Promise<{ groups: Array<{ id: string; name: string; models: Array<{ id: string; name?: string; description?: string; reasoning?: { efforts: Array<{ id: string; name: string; description?: string }>; defaultEffort?: string } }> }> }> }
98
+ } | undefined
99
+ if (modelDirs?.catalog?.load !== undefined) {
100
+ const res = await modelDirs.catalog.load()
101
+ if (res?.groups !== undefined && res.groups.length > 0) {
102
+ const out: CatalogRow[] = []
103
+ for (const group of res.groups) {
104
+ for (const model of group.models) {
105
+ out.push({
106
+ provider: group.id,
107
+ model: model.id,
108
+ name: model.name,
109
+ ...(model.description !== undefined ? { description: model.description } : {}),
110
+ ...(model.reasoning !== undefined ? { reasoning: model.reasoning } : {}),
111
+ })
112
+ }
113
+ }
114
+ if (out.length > 0) return out
99
115
  }
100
116
  }
101
- return out
102
- })
103
-
104
- // Preset roster for the composer (0.3.3): agentPreset.list over the
105
- // connection RPC [{id, name}] plus which one is the deployment
106
- // default (the form pre-selects it on create).
107
- type PresetRow = { id: string; name?: string }
108
- controller.installPresetRoster(async (): Promise<{ presets: PresetRow[]; defaultId?: string }> => {
109
- const list = connection.api.agentPresets
110
- if (list === undefined) return { presets: [] }
111
- const response = await list.list({})
112
- if (!response.result.ok) return { presets: [] }
113
- const presets = response.result.value.presets.map((p: { id: string; name?: string }) => ({ id: p.id, name: p.name }))
114
- const def = response.result.value.presets.find((p: { id: string; isDefault: boolean }) => p.isDefault)
115
- return { presets, ...(def !== undefined ? { defaultId: def.id } : {}) }
116
- })
117
- }
117
+ } catch { /* try next */ }
118
+
119
+ // 2. DSH Remote RPC
120
+ try {
121
+ const remote = (ctx.get?.('remote') ?? (ctx as Record<string, unknown>).remote) as {
122
+ session?: { modelCatalog: () => Promise<{ ok: boolean; value?: { groups: Array<{ id: string; name: string; models: Array<{ id: string; name?: string; description?: string; reasoning?: { efforts: Array<{ id: string; name: string; description?: string }>; defaultEffort?: string } }> }> } }> }
123
+ llm?: { models: (payload: Record<string, never>) => Promise<{ result: { ok: true; value: { groups: Array<{ id: string; name: string; models: Array<{ id: string; name?: string; description?: string; reasoning?: { efforts: Array<{ id: string; name: string; description?: string }>; defaultEffort?: string } }> }> } } | { ok: false } }> }
124
+ } | undefined
125
+
126
+ if (remote?.session?.modelCatalog !== undefined) {
127
+ const res = await remote.session.modelCatalog()
128
+ if (res.ok && res.value?.groups !== undefined && res.value.groups.length > 0) {
129
+ const out: CatalogRow[] = []
130
+ for (const group of res.value.groups) {
131
+ for (const model of group.models) {
132
+ out.push({
133
+ provider: group.id,
134
+ model: model.id,
135
+ name: model.name,
136
+ ...(model.description !== undefined ? { description: model.description } : {}),
137
+ ...(model.reasoning !== undefined ? { reasoning: model.reasoning } : {}),
138
+ })
139
+ }
140
+ }
141
+ if (out.length > 0) return out
142
+ }
143
+ }
144
+
145
+ if (remote?.llm?.models !== undefined) {
146
+ const res = await remote.llm.models({})
147
+ if (res.result.ok && res.result.value?.groups !== undefined && res.result.value.groups.length > 0) {
148
+ const out: CatalogRow[] = []
149
+ for (const group of res.result.value.groups) {
150
+ for (const model of group.models) {
151
+ out.push({
152
+ provider: group.id,
153
+ model: model.id,
154
+ name: model.name,
155
+ ...(model.description !== undefined ? { description: model.description } : {}),
156
+ ...(model.reasoning !== undefined ? { reasoning: model.reasoning } : {}),
157
+ })
158
+ }
159
+ }
160
+ if (out.length > 0) return out
161
+ }
162
+ }
163
+ } catch { /* try next */ }
164
+
165
+ // 3. Legacy connection.api
166
+ try {
167
+ const connection = (ctx.get?.('connection') ?? (ctx as Record<string, unknown>).connection) as {
168
+ api?: {
169
+ llm?: {
170
+ models: (payload: Record<string, never>) => Promise<{ result: { ok: true; value: { groups: Array<{ id: string; name: string; models: Array<{ id: string; name?: string; description?: string; reasoning?: { efforts: Array<{ id: string; name: string; description?: string }>; defaultEffort?: string } }> }> } } | { ok: false } }>
171
+ }
172
+ }
173
+ } | undefined
174
+ if (connection?.api?.llm?.models !== undefined) {
175
+ const res = await connection.api.llm.models({})
176
+ if (res.result.ok && res.result.value?.groups !== undefined && res.result.value.groups.length > 0) {
177
+ const out: CatalogRow[] = []
178
+ for (const group of res.result.value.groups) {
179
+ for (const model of group.models) {
180
+ out.push({
181
+ provider: group.id,
182
+ model: model.id,
183
+ name: model.name,
184
+ ...(model.description !== undefined ? { description: model.description } : {}),
185
+ ...(model.reasoning !== undefined ? { reasoning: model.reasoning } : {}),
186
+ })
187
+ }
188
+ }
189
+ if (out.length > 0) return out
190
+ }
191
+ }
192
+ } catch { /* try next */ }
193
+
194
+ // 4. Taskboard host endpoint: /dsh-taskboard/model-catalog
195
+ try {
196
+ const res = await client.modelCatalog()
197
+ if (res.models !== undefined && res.models.length > 0) {
198
+ return res.models
199
+ }
200
+ } catch { /* none */ }
201
+
202
+ return []
203
+ })
204
+
205
+ // Preset roster for the composer (0.3.3 / 0.5.5)
206
+ type PresetRow = { id: string; name?: string }
207
+ controller.installPresetRoster(async (): Promise<{ presets: PresetRow[]; defaultId?: string }> => {
208
+ // 1. DSH Remote RPC: ctx.get('remote')?.agentPresets?.list()
209
+ try {
210
+ const remote = (ctx.get?.('remote') ?? (ctx as Record<string, unknown>).remote) as {
211
+ agentPresets?: { list: () => Promise<{ ok: boolean; value?: { presets: Array<{ id: string; name?: string; isDefault?: boolean }> } } | { result: { ok: true; value: { presets: Array<{ id: string; name?: string; isDefault?: boolean }> } } }> }
212
+ } | undefined
213
+ if (remote?.agentPresets?.list !== undefined) {
214
+ const res = await remote.agentPresets.list()
215
+ const rawPresets = (res as { ok?: boolean; value?: { presets?: Array<{ id: string; name?: string; isDefault?: boolean }> } }).ok === true
216
+ ? (res as { value: { presets: Array<{ id: string; name?: string; isDefault?: boolean }> } }).value.presets
217
+ : (res as { result?: { ok?: boolean; value?: { presets?: Array<{ id: string; name?: string; isDefault?: boolean }> } } }).result?.ok === true
218
+ ? (res as { result: { value: { presets: Array<{ id: string; name?: string; isDefault?: boolean }> } } }).result.value.presets
219
+ : undefined
220
+ if (rawPresets !== undefined && rawPresets.length > 0) {
221
+ const presets = rawPresets.map(p => ({ id: p.id, name: p.name }))
222
+ const def = rawPresets.find(p => p.isDefault)
223
+ return { presets, ...(def !== undefined ? { defaultId: def.id } : {}) }
224
+ }
225
+ }
226
+ } catch { /* try next */ }
227
+
228
+ // 2. Legacy connection.api
229
+ try {
230
+ const connection = (ctx.get?.('connection') ?? (ctx as Record<string, unknown>).connection) as {
231
+ api?: {
232
+ agentPresets?: {
233
+ list: (payload: Record<string, never>) => Promise<{ result: { ok: true; value: { presets: Array<{ id: string; name?: string; isDefault: boolean }> } } | { ok: false } }>
234
+ }
235
+ }
236
+ } | undefined
237
+ if (connection?.api?.agentPresets?.list !== undefined) {
238
+ const res = await connection.api.agentPresets.list({})
239
+ if (res.result.ok && res.result.value?.presets !== undefined && res.result.value.presets.length > 0) {
240
+ const presets = res.result.value.presets.map(p => ({ id: p.id, name: p.name }))
241
+ const def = res.result.value.presets.find(p => p.isDefault)
242
+ return { presets, ...(def !== undefined ? { defaultId: def.id } : {}) }
243
+ }
244
+ }
245
+ } catch { /* try next */ }
246
+
247
+ // 3. Taskboard host endpoint: /dsh-taskboard/model-catalog
248
+ try {
249
+ const res = await client.modelCatalog()
250
+ if (res.presets !== undefined && res.presets.length > 0) {
251
+ return { presets: res.presets, ...(res.defaultPresetId !== undefined ? { defaultId: res.defaultPresetId } : {}) }
252
+ }
253
+ } catch { /* none */ }
254
+
255
+ return { presets: [] }
256
+ })
118
257
 
119
258
  // Session navigation for execution rows: resolved LAZILY on every jump —
120
259
  // apply may run before the runtime provides the services, and a captured
@@ -144,6 +283,7 @@ export function apply(ctx: ClientContextFace): void {
144
283
  ctx.effect?.(() => () => {
145
284
  for (const d of disposers.splice(0)) d()
146
285
  controller.dispose()
286
+ disposeI18n()
147
287
  }, 'dsh-taskboard: client mount')
148
288
  } catch (error) {
149
289
  console.error('[dsh-taskboard] client half failed to start:', error)
@@ -15,6 +15,7 @@
15
15
  * @module dsh-taskboard/client/sidebar-entry
16
16
  */
17
17
  import type { BoardController } from './controller.ts'
18
+ import { localeStore, translate } from './i18n/runtime.ts'
18
19
 
19
20
  /** Stable data attribute identifying this entry row. */
20
21
  export const ENTRY_SELECTOR = '[data-dsh-atb-entry]'
@@ -68,8 +69,8 @@ function createEntry(controller: BoardController): HTMLButtonElement {
68
69
  entry.type = 'button'
69
70
  entry.dataset.dshAtbEntry = ''
70
71
  entry.className = 'dsh-atb-entry'
71
- entry.setAttribute('aria-label', 'Agent 任务看板')
72
- entry.innerHTML = `<span class="dsh-atb-entry-icon">${ICON}</span><span class="dsh-atb-entry-label">任务看板</span><span class="dsh-atb-entry-stats"></span>`
72
+ entry.setAttribute('aria-label', translate('shared.entry.aria'))
73
+ entry.innerHTML = `<span class="dsh-atb-entry-icon">${ICON}</span><span class="dsh-atb-entry-label">${translate('shared.entry.label')}</span><span class="dsh-atb-entry-stats"></span>`
73
74
  entry.addEventListener('click', () => { controller.toggleBoard() })
74
75
  return entry
75
76
  }
@@ -164,7 +165,7 @@ function wireStats(entry: HTMLButtonElement, controller: BoardController): () =>
164
165
  setRollValue(slots[0]!, todo)
165
166
  setRollValue(slots[1]!, inProgress)
166
167
  setRollValue(slots[2]!, inReview)
167
- stats.title = `待办 ${todo} 进行中 ${inProgress} 待验收 ${inReview}(待办|进行中|待验收)`
168
+ stats.title = translate('shared.stats.title', { todo, doing: inProgress, review: inReview })
168
169
  }
169
170
  return update
170
171
  }
@@ -262,6 +263,14 @@ export function mountSidebarEntry(controller: BoardController): () => void {
262
263
  syncStats()
263
264
  }
264
265
  const unsubscribe = controller.subscribe(syncActive)
266
+ // Locale switches re-render the static DOM text (aria-label, label, tooltip);
267
+ // the rolling number slots carry digits only, nothing to redo for them.
268
+ const unsubscribeLocale = localeStore.subscribe(() => {
269
+ entry.setAttribute('aria-label', translate('shared.entry.aria'))
270
+ const label = entry.querySelector<HTMLElement>('.dsh-atb-entry-label')
271
+ if (label !== null) label.textContent = translate('shared.entry.label')
272
+ syncStats()
273
+ })
265
274
  syncActive()
266
275
 
267
276
  tryPlace()
@@ -270,6 +279,7 @@ export function mountSidebarEntry(controller: BoardController): () => void {
270
279
  clearInterval(retry)
271
280
  waitObserver.disconnect()
272
281
  rootObserver.disconnect()
282
+ unsubscribeLocale()
273
283
  unsubscribe()
274
284
  entry.remove()
275
285
  }
@@ -441,6 +441,10 @@ button.dsh-atb-chip2.dsh-atb-chip-btn:hover {
441
441
  box-shadow: var(--dsw-shadow-lv3, 0 12px 32px rgba(0,0,0,.18));
442
442
  animation: dsh-atb-pop .16s ease;
443
443
  }
444
+ .dsh-atb-taskform-modal {
445
+ width: min(960px, calc(100vw - 40px));
446
+ max-height: calc(100vh - 50px);
447
+ }
444
448
  @keyframes dsh-atb-pop { from { opacity: 0; transform: translateY(8px) scale(.98); } }
445
449
  .dsh-atb-modal-head {
446
450
  display: flex; align-items: center; gap: 10px;
@@ -464,6 +468,59 @@ button.dsh-atb-chip2.dsh-atb-chip-btn:hover {
464
468
  padding: 13px 16px; overflow-y: auto;
465
469
  display: grid; grid-template-columns: 1fr 1fr; gap: 11px 10px;
466
470
  }
471
+ .dsh-atb-taskform-body {
472
+ padding: 14px 18px;
473
+ display: grid;
474
+ grid-template-columns: 1.05fr 1.15fr;
475
+ gap: 18px;
476
+ }
477
+ .dsh-atb-form-col {
478
+ display: flex;
479
+ flex-direction: column;
480
+ gap: 11px;
481
+ min-width: 0;
482
+ }
483
+ .dsh-atb-form-left {
484
+ border-right: 1px solid var(--dsw-alias-border-l2, rgba(128,128,128,.14));
485
+ padding-right: 18px;
486
+ }
487
+ .dsh-atb-form-right {
488
+ display: flex;
489
+ flex-direction: column;
490
+ gap: 14px;
491
+ }
492
+ .dsh-atb-form-right .dsh-atb-field {
493
+ flex: 1;
494
+ display: flex;
495
+ flex-direction: column;
496
+ }
497
+ .dsh-atb-form-right .dsh-atb-prompt-wrap {
498
+ flex: 1;
499
+ display: flex;
500
+ flex-direction: column;
501
+ }
502
+ .dsh-atb-form-right .dsh-atb-prompt-inner {
503
+ flex: 1;
504
+ display: flex;
505
+ flex-direction: column;
506
+ }
507
+ .dsh-atb-form-right .dsh-atb-prompt-input {
508
+ flex: 1;
509
+ min-height: 130px;
510
+ resize: vertical;
511
+ }
512
+ .dsh-atb-form-subgrid {
513
+ display: grid;
514
+ grid-template-columns: 1fr 1fr;
515
+ gap: 10px;
516
+ }
517
+
518
+ @media (max-width: 768px) {
519
+ .dsh-atb-taskform-modal { width: calc(100vw - 20px); }
520
+ .dsh-atb-taskform-body { grid-template-columns: 1fr; gap: 14px; padding: 12px 14px; }
521
+ .dsh-atb-form-left { border-right: none; padding-right: 0; }
522
+ .dsh-atb-form-right .dsh-atb-prompt-input { min-height: 90px; }
523
+ }
467
524
  .dsh-atb-field { display: flex; flex-direction: column; gap: 5px; min-width: 0; }
468
525
  .dsh-atb-field[data-span="full"] { grid-column: 1 / -1; }
469
526
  .dsh-atb-field-label {
@@ -773,6 +830,80 @@ button.dsh-atb-chip2.dsh-atb-chip-btn:hover {
773
830
  .dsh-atb-set { max-width: 460px; width: min(460px, 92vw); }
774
831
  .dsh-atb-set .dsh-atb-mode-picker { margin-top: 8px; }
775
832
  .dsh-atb-set .dsh-atb-isolation-note { margin-top: 10px; }
833
+
834
+ /* ---------- 0.5.5 SlashPromptInput & Permission Picker ---------- */
835
+ .dsh-atb-perm-picker { display: grid; grid-template-columns: repeat(3, 1fr); gap: 7px; margin-top: 4px; }
836
+ .dsh-atb-perm-opt {
837
+ display: flex; flex-direction: column; align-items: flex-start; gap: 3px;
838
+ padding: 8px 10px; border-radius: 9px; cursor: pointer; text-align: left;
839
+ border: 1px solid var(--dsw-alias-border-l2, rgba(128,128,128,.35));
840
+ background: transparent; color: inherit;
841
+ transition: border-color .12s ease, background .12s ease;
842
+ }
843
+ .dsh-atb-perm-name { display: flex; align-items: center; gap: 5px; font-size: 12px; font-weight: 600; }
844
+ .dsh-atb-perm-hint { font-size: 10.5px; color: var(--dsw-alias-label-tertiary, gray); line-height: 1.35; }
845
+ .dsh-atb-perm-opt:hover { border-color: var(--dsw-alias-label-tertiary, rgba(128,128,128,.6)); }
846
+ .dsh-atb-perm-opt[data-on="true"] {
847
+ border-color: var(--dsw-alias-brand-primary, #1f2328);
848
+ background: color-mix(in srgb, var(--dsw-alias-brand-primary, #1f2328) 9%, transparent);
849
+ }
850
+
851
+ .dsh-atb-prompt-wrap {
852
+ display: flex; flex-direction: column; gap: 6px; position: relative; width: 100%;
853
+ border-radius: 9px; transition: border-color .12s ease;
854
+ }
855
+ .dsh-atb-prompt-wrap[data-drag-over="true"] {
856
+ outline: 2px dashed var(--dsw-alias-brand-primary, #1f2328);
857
+ background: color-mix(in srgb, var(--dsw-alias-brand-primary, #1f2328) 6%, transparent);
858
+ }
859
+ .dsh-atb-prompt-inner { position: relative; width: 100%; }
860
+ .dsh-atb-prompt-input {
861
+ width: 100%; box-sizing: border-box; font: inherit; font-size: 13px; line-height: 1.5;
862
+ padding: 7px 10px; border-radius: 8px; resize: vertical; min-height: 68px;
863
+ border: 1px solid var(--dsw-alias-border-l2, rgba(128,128,128,.35));
864
+ background: var(--dsw-specific-input-major, transparent); color: var(--dsw-alias-label-primary, inherit);
865
+ }
866
+ .dsh-atb-prompt-input:focus {
867
+ outline: none; border-color: var(--dsw-alias-brand-primary, #1f2328);
868
+ box-shadow: 0 0 0 3px color-mix(in srgb, var(--dsw-alias-brand-primary, #1f2328) 18%, transparent);
869
+ }
870
+
871
+ /* Slash Autocomplete Popup */
872
+ .dsh-atb-slash-popup {
873
+ position: absolute; left: 0; bottom: calc(100% + 6px); width: 100%; max-height: 240px; z-index: 100;
874
+ display: flex; flex-direction: column; overflow: hidden; border-radius: 10px;
875
+ background: var(--dsw-alias-bg-overlay, #fff); color: var(--dsw-alias-label-primary, inherit);
876
+ border: 1px solid var(--dsw-alias-border-l2, rgba(128,128,128,.28));
877
+ box-shadow: var(--dsw-shadow-lv3, 0 10px 28px rgba(0,0,0,.22));
878
+ }
879
+ .dsh-atb-slash-head {
880
+ display: flex; align-items: center; justify-content: space-between;
881
+ padding: 6px 10px; border-bottom: 1px solid var(--dsw-alias-border-l2, rgba(128,128,128,.15));
882
+ background: var(--dsw-alias-bg-elevated, rgba(128,128,128,.06));
883
+ }
884
+ .dsh-atb-slash-title { font-size: 11px; font-weight: 600; color: var(--dsw-alias-label-secondary, gray); }
885
+ .dsh-atb-slash-hint { font-size: 10px; color: var(--dsw-alias-label-tertiary, gray); }
886
+ .dsh-atb-slash-list { overflow-y: auto; max-height: 200px; display: flex; flex-direction: column; padding: 4px; }
887
+ .dsh-atb-slash-item {
888
+ display: flex; align-items: center; gap: 8px; padding: 6px 8px; border-radius: 6px;
889
+ cursor: pointer; font-size: 12px; transition: background .1s ease;
890
+ }
891
+ .dsh-atb-slash-item[data-active="true"], .dsh-atb-slash-item:hover {
892
+ background: var(--dsw-alias-interactive-bg-hover, rgba(128,128,128,.14));
893
+ }
894
+ .dsh-atb-slash-badge {
895
+ font-size: 10px; font-weight: 600; padding: 1px 5px; border-radius: 4px; flex-shrink: 0;
896
+ }
897
+ .dsh-atb-slash-badge[data-kind="command"] { background: rgba(217,130,43,.15); color: #d9822b; }
898
+ .dsh-atb-slash-badge[data-kind="skill"] { background: rgba(142,78,198,.15); color: #a06ce0; }
899
+ .dsh-atb-slash-name { font-weight: 600; font-family: monospace; font-size: 12.5px; }
900
+ .dsh-atb-slash-param { font-size: 11px; color: var(--dsw-alias-label-tertiary, gray); font-family: monospace; }
901
+ .dsh-atb-slash-desc { font-size: 11px; color: var(--dsw-alias-label-secondary, gray); margin-left: auto; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 45%; }
902
+
903
+ /* Prompt Foot Toolbar */
904
+ .dsh-atb-prompt-foot { display: flex; align-items: center; justify-content: space-between; gap: 8px; }
905
+ .dsh-atb-prompt-tip { font-size: 11px; color: var(--dsw-alias-label-tertiary, gray); }
906
+ .dsh-atb-prompt-tip code { font-size: 10.5px; padding: 1px 4px; border-radius: 4px; background: rgba(128,128,128,.14); }
776
907
  `
777
908
 
778
909
  /** Style element id (stable since 0.1.x: hook for tests and debugging). */
@@ -11,6 +11,7 @@
11
11
  * @module dsh-taskboard/host/execution
12
12
  */
13
13
  import {
14
+ DEFAULT_PERMISSION,
14
15
  effectiveIsolation,
15
16
  effectivePrompt,
16
17
  newCommentId,
@@ -18,6 +19,7 @@ import {
18
19
  normalizeBody,
19
20
  type ExecutionRecord,
20
21
  type IsolationMode,
22
+ type PermissionMode,
21
23
  type TaskModel,
22
24
  type TaskRecord,
23
25
  } from '../shared/protocol.ts'
@@ -100,6 +102,10 @@ export interface ExecutionDeps {
100
102
  * session (same rollback semantics as apiproxy).
101
103
  */
102
104
  composeAgent?: (presetId?: string) => Promise<AgentComposition | undefined>
105
+ /**
106
+ * Set execution session permission (0.5.5; 'workspace-write' | 'read-only' | 'danger-full-access').
107
+ */
108
+ setPermission?: (sessionId: string, permission: PermissionMode) => void
103
109
  }
104
110
 
105
111
  /** Outcome of a run request (immediate; the run settles asynchronously). */
@@ -446,6 +452,13 @@ export class ExecutionService {
446
452
  // 3. Attach the session to the workspace (GUI project session list).
447
453
  await this.deps.workspaces.attach(task.workspaceId, sessionId).catch(() => { /* cosmetic */ })
448
454
 
455
+ // 3a. Apply execution session permission (0.5.5; 'workspace-write' | 'read-only' | 'danger-full-access').
456
+ if (this.deps.setPermission !== undefined) {
457
+ try {
458
+ this.deps.setPermission(sessionId, task.permission ?? DEFAULT_PERMISSION)
459
+ } catch { /* best effort */ }
460
+ }
461
+
449
462
  // 3b. Best-effort rename: pin the session title to the task title so the
450
463
  // session list shows the task name (a user-sourced title also stops
451
464
  // automatic first-prompt retitling).
@@ -18,11 +18,13 @@ import type {} from '@deepseek-ai/dsh-host-webserver'
18
18
  import {
19
19
  asBoardSettings,
20
20
  asIsolation,
21
+ asPermission,
21
22
  asStatus,
22
23
  asUrgency,
23
24
  canTransition,
24
25
  checklistFromTexts,
25
26
  defaultIsolationOf,
27
+ defaultPermissionOf,
26
28
  newCommentId,
27
29
  newTaskId,
28
30
  normalizeBody,
@@ -39,7 +41,7 @@ import {
39
41
  type TaskRecord,
40
42
  } from '../shared/protocol.ts'
41
43
  import { WORKTREE_DIR, worktreePathOf, type GitFace } from './git.ts'
42
- import type { TaskTemplate } from '../shared/api.ts'
44
+ import type { CatalogModelItem, CatalogPresetItem, TaskTemplate } from '../shared/api.ts'
43
45
  import type { TemplateStore } from './templates.ts'
44
46
  import { ROUTE_PREFIX, SSE_PATH, type ApiFail, type ApiResult } from '../shared/api.ts'
45
47
  import type { TaskStore } from './store.ts'
@@ -81,6 +83,17 @@ export interface TaskboardRoutesOptions {
81
83
  git?: GitFace
82
84
  /** Task-template store (0.4.0); absent → 501 on template actions. */
83
85
  templates?: TemplateStore
86
+ /** Prompt completions face (0.5.5; dynamically discovers skills & commands). */
87
+ promptCompletions?: () => Promise<{
88
+ skills?: Array<{ name: string; description?: string }>
89
+ commands?: Array<{ name: string; description?: string; hint?: string }>
90
+ }>
91
+ /** Model and preset catalog face (0.5.5; dynamically discovers models & presets). */
92
+ modelCatalog?: () => Promise<{
93
+ models?: CatalogModelItem[]
94
+ presets?: CatalogPresetItem[]
95
+ defaultPresetId?: string
96
+ }>
84
97
  }
85
98
 
86
99
  /** Validate a template's task spec (routes-side, unknown → invalid_input). */
@@ -100,12 +113,14 @@ function normalizeTemplateSpec(raw: unknown, now: number): TaskTemplate['task']
100
113
  const urgency = str('urgency')
101
114
  const isolation = str('isolation')
102
115
  const presetId = str('presetId')
116
+ const permission = str('permission')
103
117
  if (title !== undefined) spec.title = normalizeTitle(title)
104
118
  if (description !== undefined) spec.description = description
105
119
  if (prompt !== undefined) spec.prompt = normalizePrompt(prompt)
106
120
  if (urgency !== undefined) spec.urgency = asUrgency(urgency)
107
121
  if (isolation !== undefined) spec.isolation = asIsolation(isolation)
108
122
  if (presetId !== undefined && presetId.trim().length > 0) spec.presetId = presetId.trim()
123
+ if (permission !== undefined && permission.trim().length > 0) spec.permission = asPermission(permission)
109
124
  if (e.execution !== undefined) {
110
125
  spec.execution = normalizeExecution(e.execution as { mode?: string; cron?: string }, now)
111
126
  }
@@ -406,6 +421,33 @@ export function registerTaskboardRoutes(ctx: Context, options: TaskboardRoutesOp
406
421
  return
407
422
  }
408
423
 
424
+ // Prompt completions (0.5.5; dynamically discovers skills & commands).
425
+ if (pathname === `${ROUTE_PREFIX}/prompt-completions`) {
426
+ const completions = await options.promptCompletions?.().catch(() => undefined)
427
+ json(res, {
428
+ ok: true,
429
+ value: {
430
+ commands: completions?.commands ?? [],
431
+ skills: completions?.skills ?? [],
432
+ },
433
+ })
434
+ return
435
+ }
436
+
437
+ // Model catalog (0.5.5; dynamically discovers models & presets from runtime).
438
+ if (pathname === `${ROUTE_PREFIX}/model-catalog`) {
439
+ const catalog = await options.modelCatalog?.().catch(() => undefined)
440
+ json(res, {
441
+ ok: true,
442
+ value: {
443
+ models: catalog?.models ?? [],
444
+ presets: catalog?.presets ?? [],
445
+ ...(catalog?.defaultPresetId !== undefined ? { defaultPresetId: catalog.defaultPresetId } : {}),
446
+ },
447
+ })
448
+ return
449
+ }
450
+
409
451
  const taskMatch = pathname.match(TASK_RE)
410
452
  if (taskMatch !== null) {
411
453
  const task = store.get(taskMatch[1]!)
@@ -463,6 +505,8 @@ export function registerTaskboardRoutes(ctx: Context, options: TaskboardRoutesOp
463
505
  // rewrite existing tasks.
464
506
  const isolation = isolationRaw === null ? defaultIsolationOf(store.snapshot().settings) : asIsolation(isolationRaw)
465
507
  const presetId = normalizePresetId(str(body, 'presetId'))
508
+ const permissionRaw = str(body, 'permission')
509
+ const permission = permissionRaw === null ? defaultPermissionOf(store.snapshot().settings) : asPermission(permissionRaw)
466
510
  let checklist: TaskRecord['checklist'] = undefined
467
511
  if (body.checklist !== undefined) {
468
512
  if (!Array.isArray(body.checklist) || body.checklist.some(c => typeof c !== 'string')) {
@@ -485,6 +529,7 @@ export function registerTaskboardRoutes(ctx: Context, options: TaskboardRoutesOp
485
529
  model,
486
530
  isolation,
487
531
  ...(presetId !== undefined ? { presetId } : {}),
532
+ permission,
488
533
  ...(checklist !== undefined ? { checklist } : {}),
489
534
  version: 1,
490
535
  createdAt: now,
@@ -557,6 +602,9 @@ export function registerTaskboardRoutes(ctx: Context, options: TaskboardRoutesOp
557
602
  // Preset may change any time: each run composes fresh.
558
603
  if (body.presetId === null) delete next.presetId
559
604
  else if (body.presetId !== undefined) next.presetId = normalizePresetId(str(body, 'presetId'))!
605
+ // Permission (0.5.5): 'workspace-write' | 'read-only' | 'danger-full-access'
606
+ if (body.permission === null) delete next.permission
607
+ else if (body.permission !== undefined) next.permission = asPermission(body.permission)
560
608
  // Checklist (0.4.0): the GUI replaces the whole list; null clears.
561
609
  if (body.checklist === null) delete next.checklist
562
610
  else if (body.checklist !== undefined) {