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.
- package/README.md +22 -2
- package/lib/client.js +2604 -767
- 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 +210 -10
- package/lib/host/session-sync.js.map +1 -1
- package/lib/host/store.js +9 -2
- package/lib/host/store.js.map +1 -1
- package/lib/index.js +100 -1
- package/lib/index.js.map +1 -1
- package/lib/shared/api.js.map +1 -1
- package/lib/shared/protocol.js +19 -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 +66 -26
- 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 +13 -0
- package/src/host/routes.ts +49 -1
- package/src/host/session-sync.ts +334 -14
- package/src/host/store.ts +15 -1
- package/src/index.ts +115 -1
- package/src/shared/api.ts +47 -0
- package/src/shared/protocol.ts +41 -0
- package/src/shared/version.ts +1 -1
package/src/client/index.ts
CHANGED
|
@@ -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
|
|
74
|
-
//
|
|
75
|
-
//
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
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
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
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
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
if (
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
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', '
|
|
72
|
-
entry.innerHTML = `<span class="dsh-atb-entry-icon">${ICON}</span><span class="dsh-atb-entry-label"
|
|
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 =
|
|
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
|
}
|
package/src/client/styles.ts
CHANGED
|
@@ -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). */
|
package/src/host/execution.ts
CHANGED
|
@@ -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).
|
package/src/host/routes.ts
CHANGED
|
@@ -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) {
|