@linxin666/dsh-client-ui-task-board 0.4.5 → 0.4.7

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 (83) hide show
  1. package/README.i18n.yaml +2 -2
  2. package/README.md +34 -18
  3. package/README.zh.md +34 -18
  4. package/lib/client.js +2804 -1024
  5. package/lib/client.js.map +1 -1
  6. package/lib/index.js +2576 -416
  7. package/lib/types/client/CardDefaultPresetField.d.ts +35 -0
  8. package/lib/types/client/TaskBoardSettingsCard.d.ts +61 -0
  9. package/lib/types/client/board/ReasoningEffortField.d.ts +25 -0
  10. package/lib/types/client/board/model-options.d.ts +67 -0
  11. package/lib/types/client/board/preset-label.d.ts +21 -0
  12. package/lib/types/client/board/preset-roster.d.ts +45 -0
  13. package/lib/types/client/board/team-run-promotion.d.ts +26 -0
  14. package/lib/types/client/index.d.ts +4 -5
  15. package/lib/types/client/locales.d.ts +81 -0
  16. package/lib/types/client/plugin-card-seat.d.ts +5 -0
  17. package/lib/types/client/schedule-zone.d.ts +50 -0
  18. package/lib/types/core/card-defaults.d.ts +78 -0
  19. package/lib/types/core/controller.d.ts +41 -17
  20. package/lib/types/core/launch-width.d.ts +31 -0
  21. package/lib/types/core/poll-cadence.d.ts +31 -0
  22. package/lib/types/core/reasoning-effort.d.ts +57 -0
  23. package/lib/types/core/subtask.d.ts +30 -8
  24. package/lib/types/core/tasks.d.ts +163 -14
  25. package/lib/types/core/use-cases/task-create.d.ts +10 -5
  26. package/lib/types/core/use-cases/task-schedule.d.ts +68 -10
  27. package/lib/types/core/use-cases/task-update.d.ts +1 -1
  28. package/lib/types/core/verification-budget.d.ts +52 -0
  29. package/lib/types/core/verification.d.ts +376 -9
  30. package/lib/types/host/launch-admission.d.ts +72 -0
  31. package/lib/types/host/verification-gate.d.ts +24 -0
  32. package/lib/types/host/verification-runner.d.ts +38 -7
  33. package/lib/types/host-ledger.d.ts +91 -11
  34. package/lib/types/host-runner.d.ts +111 -13
  35. package/lib/types/host-service.d.ts +199 -8
  36. package/lib/types/index.d.ts +141 -0
  37. package/lib/types/protocol.d.ts +63 -12
  38. package/package.json +29 -33
  39. package/src/client/CardDefaultPresetField.tsx +101 -0
  40. package/src/client/TaskBoardSettingsCard.tsx +288 -17
  41. package/src/client/board/LinkSubtaskModal.tsx +8 -0
  42. package/src/client/board/NewTaskModal.tsx +237 -60
  43. package/src/client/board/ReasoningEffortField.tsx +50 -0
  44. package/src/client/board/TaskCard.tsx +3 -3
  45. package/src/client/board/TaskDetail.tsx +261 -24
  46. package/src/client/board/VerificationReport.tsx +132 -13
  47. package/src/client/board/model-options.ts +178 -0
  48. package/src/client/board/preset-label.ts +32 -0
  49. package/src/client/board/preset-roster.ts +58 -0
  50. package/src/client/board/status-key.ts +3 -0
  51. package/src/client/board/team-run-promotion.ts +39 -0
  52. package/src/client/index.ts +105 -108
  53. package/src/client/locales.ts +171 -6
  54. package/src/client/plugin-card-seat.ts +7 -0
  55. package/src/client/schedule-zone.ts +100 -2
  56. package/src/client/settings-entry-form.ts +0 -1
  57. package/src/core/card-defaults.ts +132 -0
  58. package/src/core/controller.ts +55 -16
  59. package/src/core/launch-width.ts +38 -0
  60. package/src/core/poll-cadence.ts +41 -0
  61. package/src/core/reasoning-effort.ts +92 -0
  62. package/src/core/store.ts +69 -15
  63. package/src/core/subtask.ts +48 -13
  64. package/src/core/tasks.ts +213 -39
  65. package/src/core/use-cases/task-create.ts +57 -25
  66. package/src/core/use-cases/task-schedule.ts +190 -25
  67. package/src/core/use-cases/task-update.ts +9 -4
  68. package/src/core/verification-budget.ts +115 -0
  69. package/src/core/verification.ts +785 -17
  70. package/src/host/agent-tools.ts +127 -29
  71. package/src/host/launch-admission.ts +140 -0
  72. package/src/host/verification-gate.ts +172 -16
  73. package/src/host/verification-runner.ts +207 -40
  74. package/src/host-ledger.ts +346 -48
  75. package/src/host-runner.ts +234 -35
  76. package/src/host-service.ts +539 -52
  77. package/src/index.ts +250 -7
  78. package/src/protocol.ts +160 -32
  79. package/lib/invariant.js +0 -6
  80. package/lib/types/client/board-card-seat.d.ts +0 -34
  81. package/lib/types/invariant.d.ts +0 -3
  82. package/src/client/board-card-seat.ts +0 -102
  83. package/src/invariant.ts +0 -4
@@ -24,7 +24,9 @@ import type {} from '@deepseek-ai/dsh-client-ui-settings/client'
24
24
  // Type-only: pulls the workspace plugin's Context merge (ctx.uiWorkspace), the
25
25
  // multi-instance navigation face that replaced ISessions.open().
26
26
  import type {} from '@deepseek-ai/dsh-client-ui-workspace/client'
27
- import { BoardController, TASK_BOARD_PANEL_ID } from '../core/controller.ts'
27
+ import { BoardController, TASK_BOARD_PANEL_ID, type ExecutionModelOption, type ExecutionPresetOption } from '../core/controller.ts'
28
+ import { modelOptionsFromCatalog, modelOptionsFromDiscoveredModels } from './board/model-options.ts'
29
+ import { presetOptionsFromRoster, readPresetRoster, type PresetRemote } from './board/preset-roster.ts'
28
30
  import { mainViewSessionId } from './main-session.ts'
29
31
  import { LocalStorageTaskStore } from '../core/store.ts'
30
32
  import { claimTaskboardApply, releaseTaskboardApply } from './apply-guard.ts'
@@ -33,7 +35,7 @@ import { TaskBoardSettingsCard, TaskBoardSettingsCardController, type TaskBoardS
33
35
  import { en, zh, setRuntimeTranslate, type TaskBoardKey } from './locales.ts'
34
36
  import { HttpTaskBoardHostTransport } from './host-api.ts'
35
37
  import { reportDailyHeartbeat } from './telemetry.ts'
36
- import { installBoardCard } from './board-card-seat.ts'
38
+ import { installPluginCard } from './plugin-card-seat.ts'
37
39
  import { createServedEntryForm } from './settings-entry-form.ts'
38
40
  import { TaskBoardClientService } from './service.ts'
39
41
  import {
@@ -133,62 +135,13 @@ declare module '@deepseek-ai/cordis' {
133
135
 
134
136
  /**
135
137
  * Required services (fiber inject waiting — the runtime must be up first).
136
- * The generated remote faces are probed at use time instead of injected:
137
- * `remote.agentPresets` only registers on 0.1.2-alpha.2 hosts (the
138
- * api-remotes contribution), so a hard wait would pend the entry forever
139
- * on hosts below that cohort, which serve the same roster through the
140
- * connection RPC face.
138
+ * The generated remote namespaces are not all hard injects: `remote.session`
139
+ * is one, while `remote.agentPresets` is reached through a scoped inject at
140
+ * use time (see mountUi), because the namespace mounts with the official
141
+ * api-remotes assembly applied beside this entry rather than before it.
141
142
  */
142
143
  export const inject = ['slots', 'sessions', 'workspaces', 'connection', 'configForms', 'locale', 'remote', 'remote.session', 'uiWorkspace', 'layout']
143
144
 
144
- /** One agent-preset row the mode picker consumes (either face's wire shape). */
145
- interface PresetRosterRow {
146
- id: string
147
- name?: string
148
- description?: string
149
- /** Why this preset cannot compose a session; absent when it can. */
150
- broken?: string
151
- isDefault: boolean
152
- }
153
-
154
- /**
155
- * Read the agent-preset roster through whichever face the running host
156
- * serves: the generated api-remotes face (`remote.agentPresets`,
157
- * 0.1.2-alpha.2) or the connection RPC face
158
- * (`connection.api.agentPresets`, hosts below that cohort). Answers
159
- * undefined when the host serves neither, so the caller leaves the picker
160
- * options untouched instead of erroring.
161
- */
162
- async function readPresetRoster(
163
- ctx: ClientContext,
164
- remote: ClientRemote,
165
- ): Promise<{ ok: boolean; presets: readonly PresetRosterRow[] } | undefined> {
166
- // The cordis `remote` proxy throws on a property that was never injected
167
- // ("cannot get property X without inject") rather than returning undefined,
168
- // so the probe must guard the access — a hard read would abort mounting on
169
- // hosts below the 0.1.2-alpha.2 cohort instead of degrading to the legacy
170
- // connection RPC face below.
171
- let remotes: ClientRemote['agentPresets'] | undefined
172
- try {
173
- remotes = (remote as Partial<ClientRemote>).agentPresets
174
- } catch {
175
- remotes = undefined
176
- }
177
- if (remotes !== undefined) {
178
- const response = await remotes.list()
179
- if (!response.ok) return { ok: false, presets: [] }
180
- return { ok: true, presets: response.value.presets }
181
- }
182
- const connection = ctx.get('connection') as unknown as {
183
- api?: { agentPresets?: { list(request: Record<string, never>): Promise<{ result: { ok: boolean; value?: { presets?: readonly PresetRosterRow[] } } }> } }
184
- }
185
- const legacy = connection.api?.agentPresets
186
- if (legacy === undefined) return undefined
187
- const response = await legacy.list({})
188
- if (!response.result.ok || response.result.value === undefined) return { ok: false, presets: [] }
189
- return { ok: true, presets: response.result.value.presets ?? [] }
190
- }
191
-
192
145
  /**
193
146
  * Mount the task board.
194
147
  * @param ctx - client root context (services: sessions, workspaces).
@@ -238,7 +191,7 @@ export function apply(ctx: ClientContext): void {
238
191
  ctx.effect(() => () => { clientService.detach() }, 'task-board: extension service')
239
192
 
240
193
  const settingsCard = new TaskBoardSettingsCardController(settingsForm, request => clientService.dispatch(request))
241
- installBoardCard(ctx, {
194
+ installPluginCard(ctx, {
242
195
  bundle: '@linxin666/dsh-client-ui-task-board',
243
196
  id: 'task-board',
244
197
  order: 110,
@@ -251,6 +204,48 @@ export function apply(ctx: ClientContext): void {
251
204
  })
252
205
  ctx.effect(() => () => { settingsCard.dispose() }, 'task-board: settings card')
253
206
 
207
+ // The agent-preset roster is a runtime fact of the deployment, not ledger
208
+ // state, and TWO surfaces read it: the board's mode pickers and the settings
209
+ // card's new-card-defaults topic. It is wired here, outside the board view,
210
+ // because the settings card stays reachable while the board itself is
211
+ // disabled — that is where it is switched back on — and its default-preset
212
+ // field must not lose the roster with the view. The namespace mounts with the
213
+ // official api-remotes assembly, which the page applies beside this entry: at
214
+ // mount time it is usually still absent, and reading it through
215
+ // `remote.agentPresets` before it lands throws ("cannot get property ...
216
+ // without inject"), so a one-shot probe would leave both pickers with nothing
217
+ // but "inherit" for the page lifetime. A hard inject is no better — it would
218
+ // pend the whole plugin on a deployment that never mounts the namespace. The
219
+ // scoped inject runs the feed exactly when the namespace exists and re-runs it
220
+ // if the namespace is replaced; the reconnect read inside covers a deployment
221
+ // change behind the same namespace.
222
+ let presetRoster: readonly ExecutionPresetOption[] | undefined
223
+ let liveController: BoardController | undefined
224
+ const pushPresetOptions = async (presetRemote: PresetRemote): Promise<void> => {
225
+ try {
226
+ const roster = await readPresetRoster(presetRemote)
227
+ if (!roster.ok) return
228
+ const presets = presetOptionsFromRoster(roster.presets)
229
+ presetRoster = presets
230
+ settingsCard.setPresetRoster(presets)
231
+ liveController?.setExecutionOptions({ presets })
232
+ } catch (error) {
233
+ // A failed roster read leaves the previous options in place: the pickers
234
+ // stay usable, the default-preset field keeps the inherit choice alone,
235
+ // and the next reconnect retries the read.
236
+ console.error('[dsh-task-board] agent preset roster read failed', error)
237
+ }
238
+ }
239
+ const presetRosterFeed = ctx.inject(['remote.agentPresets'], (inner) => {
240
+ const presetRemote = inner.get('remote.agentPresets') as PresetRemote | undefined
241
+ if (presetRemote === undefined) return
242
+ void pushPresetOptions(presetRemote)
243
+ // Subscribed inside the injected scope, so a namespace that goes away takes
244
+ // its listener with it instead of leaving one per remount.
245
+ return inner.on('connection/reset', () => { void pushPresetOptions(presetRemote) })
246
+ })
247
+ ctx.effect(() => () => { void presetRosterFeed.dispose() }, 'task-board: agent preset roster')
248
+
254
249
  // The sidebar entry and board view mount once the settings form settles;
255
250
  // while the form is still loading, the composition default is unknown, so
256
251
  // nothing mounts yet. Only an unavailable form (no settings surface served)
@@ -297,12 +292,35 @@ export function apply(ctx: ClientContext): void {
297
292
  clientService.attach(controller)
298
293
 
299
294
  const disposers: Array<() => void> = []
295
+ // The board follows the live controller for the roster feed wired above,
296
+ // which serves the settings card whether or not the view is mounted.
297
+ liveController = controller
298
+ if (presetRoster !== undefined) controller.setExecutionOptions({ presets: presetRoster })
299
+
300
+ // The new-card defaults are written through the very settings form this
301
+ // plugin binds (that is the card's save), and the Host republishes its
302
+ // snapshot on that write. Its SSE frame carries the value as of BEFORE the
303
+ // write, though, so a mirror that only followed frames would hand the create
304
+ // dialog the previous defaults for the rest of the page session — the
305
+ // "configure a default, create a card, get nothing" report. Follow the form
306
+ // instead: when the four defaults it holds change, re-read the authoritative
307
+ // snapshot. The signature keeps one write to one GET (the form announces a
308
+ // settled document more than once) and keeps an unrelated settings write, or
309
+ // any board action, from pulling the ledger again.
310
+ let followedDefaults = cardDefaultSignature(settingsForm.getSnapshot().value)
311
+ disposers.push(settingsForm.subscribe(() => {
312
+ const next = cardDefaultSignature(settingsForm.getSnapshot().value)
313
+ if (next === followedDefaults) return
314
+ followedDefaults = next
315
+ controller.refreshHostState()
316
+ }))
300
317
 
301
318
  // Execution-target option feeds: the workspace list drives the workspace
302
319
  // picker, and the agent-preset roster drives the mode picker. Both are
303
320
  // runtime facts (not ledger state), so the wiring pushes them into the
304
- // controller on change; the preset roster is re-read after reconnects
305
- // because a reconnect may serve a different deployment.
321
+ // controller on change; the preset roster feed itself lives beside the
322
+ // settings card (see apply) because that card's new-card-defaults topic
323
+ // reads the same roster while this view may not be mounted at all.
306
324
  const pushWorkspaceOptions = (): void => {
307
325
  const snapshot = workspaces.list.getSnapshot()
308
326
  controller.setExecutionOptions({
@@ -321,28 +339,9 @@ export function apply(ctx: ClientContext): void {
321
339
  const created = await workspaces.create({ path })
322
340
  return { workspaceId: created.workspaceId }
323
341
  })
324
- const pushPresetOptions = async (): Promise<void> => {
325
- try {
326
- const roster = await readPresetRoster(ctx, remote)
327
- if (roster === undefined || !roster.ok) return
328
- controller.setExecutionOptions({
329
- presets: roster.presets.map(preset => ({
330
- id: preset.id,
331
- name: preset.name,
332
- description: preset.description,
333
- broken: preset.broken,
334
- isDefault: preset.isDefault,
335
- })),
336
- })
337
- } catch (error) {
338
- // A failed roster read leaves the previous options in place; the
339
- // picker stays usable and the next reconnect retries the read.
340
- console.error('[dsh-task-board] agent preset roster read failed', error)
341
- }
342
- }
343
342
  const pushModelOptions = async (): Promise<void> => {
344
343
  try {
345
- let models: Array<{ id: string; name?: string; provider?: string }> = []
344
+ let models: ExecutionModelOption[] = []
346
345
  let sessionRemote: ClientRemote['session'] | undefined
347
346
  try {
348
347
  sessionRemote = (remote as Partial<ClientRemote>).session
@@ -351,15 +350,7 @@ export function apply(ctx: ClientContext): void {
351
350
  }
352
351
  if (typeof sessionRemote?.modelCatalog === 'function') {
353
352
  const res = await sessionRemote.modelCatalog()
354
- if (res.ok && Array.isArray(res.value?.groups)) {
355
- for (const g of res.value.groups) {
356
- const provider = (g as { id?: string; provider?: string }).id ?? (g as { id?: string; provider?: string }).provider
357
- for (const m of g.models ?? []) {
358
- const qualifiedId = provider ? `${provider}/${m.id}` : m.id
359
- models.push({ id: qualifiedId, name: m.name ?? m.id, provider })
360
- }
361
- }
362
- }
353
+ if (res.ok) models = modelOptionsFromCatalog(res.value)
363
354
  }
364
355
  if (models.length === 0) {
365
356
  const conn = ctx.get('connection') as {
@@ -371,11 +362,8 @@ export function apply(ctx: ClientContext): void {
371
362
  } | undefined
372
363
  if (conn?.api) {
373
364
  if (typeof conn.api.llm?.discoverModels === 'function') {
374
- const res = await conn.api.llm.discoverModels() as { result?: { value?: { models?: Array<{ id: string; name?: string }> } } }
375
- const list = res?.result?.value?.models
376
- if (Array.isArray(list)) {
377
- models = list.map(m => ({ id: m.id, name: m.name }))
378
- }
365
+ const res = await conn.api.llm.discoverModels() as { result?: { value?: { models?: unknown } } }
366
+ models = modelOptionsFromDiscoveredModels(res?.result?.value?.models)
379
367
  }
380
368
  const catalogFn = typeof conn.api.session?.modelCatalog === 'function'
381
369
  ? conn.api.session.modelCatalog
@@ -383,17 +371,8 @@ export function apply(ctx: ClientContext): void {
383
371
  ? conn.api.sessions.modelCatalog
384
372
  : undefined
385
373
  if (models.length === 0 && catalogFn !== undefined) {
386
- const res = await catalogFn() as { result?: { value?: { groups?: Array<{ id?: string; provider?: string; models?: Array<{ id: string; name?: string }> }> } } }
387
- const groups = res?.result?.value?.groups
388
- if (Array.isArray(groups)) {
389
- for (const g of groups) {
390
- const provider = g.id ?? g.provider
391
- for (const m of g.models ?? []) {
392
- const qualifiedId = provider ? `${provider}/${m.id}` : m.id
393
- models.push({ id: qualifiedId, name: m.name ?? m.id, provider })
394
- }
395
- }
396
- }
374
+ const res = await catalogFn() as { result?: { value?: unknown } }
375
+ models = modelOptionsFromCatalog(res?.result?.value)
397
376
  }
398
377
  }
399
378
  }
@@ -404,12 +383,8 @@ export function apply(ctx: ClientContext): void {
404
383
  console.error('[dsh-task-board] model options read failed', error)
405
384
  }
406
385
  }
407
- void pushPresetOptions()
408
386
  void pushModelOptions()
409
- disposers.push(ctx.on('connection/reset', () => {
410
- void pushPresetOptions()
411
- void pushModelOptions()
412
- }))
387
+ disposers.push(ctx.on('connection/reset', () => { void pushModelOptions() }))
413
388
  // Native panel surfaces: one sidebar row and one center-column page, both
414
389
  // through the official slot seats the shell itself renders. The layout's
415
390
  // panel selection is reconciled back into the controller so the board's own
@@ -434,6 +409,7 @@ export function apply(ctx: ClientContext): void {
434
409
  for (const dispose of disposers.splice(0)) dispose()
435
410
  clientService.detach()
436
411
  controller.dispose()
412
+ liveController = undefined
437
413
  uiDisposer = undefined
438
414
  }
439
415
  }
@@ -487,6 +463,27 @@ export function bindSettingsForm(ctx: ClientContext): ConfigForm<TaskBoardSettin
487
463
  })
488
464
  }
489
465
 
466
+ /**
467
+ * The four new-card defaults one settings document holds, as a change
468
+ * signature.
469
+ *
470
+ * Deliberately nothing but those four keys: the settings form also carries the
471
+ * board's own switches and the acceptance configuration, and an edit to one of
472
+ * those says nothing about the defaults the create dialog prefills from. Not a
473
+ * second normalization of Host state — the value never leaves this comparison.
474
+ * @param value - the settings form's effective section, if it has one.
475
+ * @returns a stable string that changes exactly when the defaults do.
476
+ */
477
+ function cardDefaultSignature(value: unknown): string {
478
+ const section = (value ?? {}) as Record<string, unknown>
479
+ return JSON.stringify([
480
+ section.cardDefaultMode,
481
+ section.cardDefaultPermission,
482
+ section.cardDefaultModel,
483
+ section.cardDefaultReasoningEffort,
484
+ ])
485
+ }
486
+
490
487
  /**
491
488
  * The profile entry id this package's own row carries, for a page that serves
492
489
  * no family binder.