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

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 (128) hide show
  1. package/README.i18n.yaml +2 -2
  2. package/README.md +56 -18
  3. package/README.zh.md +56 -18
  4. package/lib/client.js +6242 -958
  5. package/lib/client.js.map +1 -1
  6. package/lib/index.js +3475 -234
  7. package/lib/types/client/PluginSettingsCard.d.ts +7 -0
  8. package/lib/types/client/TaskBoardSettingsCard.d.ts +19 -3
  9. package/lib/types/client/board/ClampedMarkdown.d.ts +3 -0
  10. package/lib/types/client/board/ConfirmDialog.d.ts +4 -1
  11. package/lib/types/client/board/EditTaskModal.d.ts +5 -2
  12. package/lib/types/client/board/LinkSubtaskModal.d.ts +5 -2
  13. package/lib/types/client/board/NewTaskModal.d.ts +4 -1
  14. package/lib/types/client/board/TagManagerModal.d.ts +26 -0
  15. package/lib/types/client/board/TaskBoard.d.ts +10 -1
  16. package/lib/types/client/board/TaskCard.d.ts +14 -1
  17. package/lib/types/client/board/TaskDetail.d.ts +3 -1
  18. package/lib/types/client/board/TaskForm.d.ts +23 -2
  19. package/lib/types/client/board/VerificationReport.d.ts +23 -0
  20. package/lib/types/client/board/card-view.d.ts +113 -0
  21. package/lib/types/client/board/icons.d.ts +32 -0
  22. package/lib/types/client/board/overlay.d.ts +69 -0
  23. package/lib/types/client/board/preset-label.d.ts +22 -0
  24. package/lib/types/client/board/status-key.d.ts +15 -0
  25. package/lib/types/client/board/task-markdown.d.ts +19 -0
  26. package/lib/types/client/board-card-seat.d.ts +34 -0
  27. package/lib/types/client/host-api.d.ts +24 -1
  28. package/lib/types/client/index.d.ts +19 -0
  29. package/lib/types/client/locales.d.ts +141 -3
  30. package/lib/types/client/native-panel.d.ts +6 -20
  31. package/lib/types/client/schedule-zone.d.ts +47 -0
  32. package/lib/types/client/seats.d.ts +18 -0
  33. package/lib/types/client/service.d.ts +32 -0
  34. package/lib/types/client/settings-form.d.ts +16 -0
  35. package/lib/types/core/controller.d.ts +38 -1
  36. package/lib/types/core/degraded-reason.d.ts +46 -0
  37. package/lib/types/core/extension.d.ts +283 -0
  38. package/lib/types/core/handover.d.ts +13 -1
  39. package/lib/types/core/issue-form.d.ts +16 -0
  40. package/lib/types/core/package-name.d.ts +9 -0
  41. package/lib/types/core/schedule.d.ts +95 -20
  42. package/lib/types/core/subtask.d.ts +30 -11
  43. package/lib/types/core/tasks.d.ts +110 -5
  44. package/lib/types/core/use-cases/task-create.d.ts +2 -1
  45. package/lib/types/core/use-cases/task-schedule.d.ts +8 -4
  46. package/lib/types/core/use-cases/task-tag.d.ts +48 -0
  47. package/lib/types/core/use-cases/task-update.d.ts +5 -3
  48. package/lib/types/core/verification.d.ts +386 -0
  49. package/lib/types/core/workspace-target.d.ts +41 -0
  50. package/lib/types/host/agent-tools.d.ts +1 -1
  51. package/lib/types/host/extension-registry.d.ts +111 -0
  52. package/lib/types/host/llm-dispatch.d.ts +63 -0
  53. package/lib/types/host/verification-gate.d.ts +71 -0
  54. package/lib/types/host/verification-runner.d.ts +108 -0
  55. package/lib/types/host/workspace-evidence.d.ts +53 -0
  56. package/lib/types/host-ledger.d.ts +107 -11
  57. package/lib/types/host-runner.d.ts +99 -4
  58. package/lib/types/host-service.d.ts +114 -2
  59. package/lib/types/index.d.ts +80 -26
  60. package/lib/types/protocol.d.ts +53 -3
  61. package/lib/types/tool-surface.d.ts +71 -0
  62. package/package.json +27 -26
  63. package/src/client/PluginSettingsCard.tsx +9 -0
  64. package/src/client/TaskBoardSettingsCard.tsx +304 -65
  65. package/src/client/board/ClampedMarkdown.tsx +69 -0
  66. package/src/client/board/ConfirmDialog.tsx +7 -3
  67. package/src/client/board/EditTaskModal.tsx +5 -2
  68. package/src/client/board/LinkSubtaskModal.tsx +30 -15
  69. package/src/client/board/NewTaskModal.tsx +288 -151
  70. package/src/client/board/TagManagerModal.tsx +235 -0
  71. package/src/client/board/TaskBoard.tsx +259 -103
  72. package/src/client/board/TaskCard.tsx +238 -86
  73. package/src/client/board/TaskDetail.tsx +154 -59
  74. package/src/client/board/TaskForm.tsx +61 -6
  75. package/src/client/board/VerificationReport.tsx +203 -0
  76. package/src/client/board/card-view.ts +220 -0
  77. package/src/client/board/icons.tsx +89 -0
  78. package/src/client/board/overlay.tsx +189 -0
  79. package/src/client/board/preset-label.ts +44 -0
  80. package/src/client/board/status-key.ts +27 -0
  81. package/src/client/board/task-markdown.module.css +137 -0
  82. package/src/client/board/task-markdown.tsx +106 -0
  83. package/src/client/board-card-seat.ts +102 -0
  84. package/src/client/board-settings.module.css +66 -0
  85. package/src/client/board.module.css +602 -68
  86. package/src/client/host-api.ts +109 -6
  87. package/src/client/index.ts +48 -6
  88. package/src/client/locales.ts +290 -12
  89. package/src/client/native-panel.tsx +18 -2
  90. package/src/client/schedule-zone.ts +135 -0
  91. package/src/client/seats.tsx +58 -0
  92. package/src/client/service.ts +94 -0
  93. package/src/client/settings-card.module.css +31 -5
  94. package/src/client/settings-form.ts +27 -1
  95. package/src/core/controller.ts +101 -5
  96. package/src/core/degraded-reason.ts +81 -0
  97. package/src/core/extension.ts +376 -0
  98. package/src/core/freeze-snapshot.ts +2 -2
  99. package/src/core/handover.ts +16 -1
  100. package/src/core/issue-form.ts +19 -0
  101. package/src/core/package-name.ts +9 -0
  102. package/src/core/schedule.ts +211 -55
  103. package/src/core/store.ts +26 -1
  104. package/src/core/subtask.ts +77 -22
  105. package/src/core/tasks.ts +138 -10
  106. package/src/core/use-cases/task-create.ts +14 -2
  107. package/src/core/use-cases/task-parent.ts +3 -2
  108. package/src/core/use-cases/task-schedule.ts +17 -6
  109. package/src/core/use-cases/task-tag.ts +110 -0
  110. package/src/core/use-cases/task-update.ts +13 -4
  111. package/src/core/verification.ts +826 -0
  112. package/src/core/workspace-target.ts +59 -0
  113. package/src/host/agent-tools.ts +58 -24
  114. package/src/host/extension-registry.ts +509 -0
  115. package/src/host/llm-dispatch.ts +87 -0
  116. package/src/host/verification-gate.ts +331 -0
  117. package/src/host/verification-runner.ts +535 -0
  118. package/src/host/workspace-evidence.ts +176 -0
  119. package/src/host-ai.ts +15 -9
  120. package/src/host-ledger.ts +342 -51
  121. package/src/host-routes.ts +15 -2
  122. package/src/host-runner.ts +219 -20
  123. package/src/host-service.ts +346 -26
  124. package/src/index.ts +204 -16
  125. package/src/protocol.ts +122 -9
  126. package/src/tool-surface.ts +95 -0
  127. package/lib/types/host/run-guarded.d.ts +0 -39
  128. package/src/host/run-guarded.ts +0 -76
@@ -1,3 +1,5 @@
1
+ import { degradedRowOf, ledgerOwnerPid } from '../core/degraded-reason.ts'
2
+ import { TASK_BOARD_PACKAGE } from '../core/package-name.ts'
1
3
  import type { TaskRecord } from '../core/tasks.ts'
2
4
  import { t } from './locales.ts'
3
5
  import {
@@ -37,7 +39,20 @@ const TASK_PARSE_TIMEOUT_SECONDS = 45
37
39
  * "Unexpected token 'o', \"not found\" is not valid JSON" is what a missing
38
40
  * Host half looks like today.
39
41
  */
40
- export type HostApiFailure = 'not-mounted' | 'unauthorized' | 'locked' | 'rejected' | 'timeout' | 'unreachable' | 'unexpected'
42
+ export type HostApiFailure =
43
+ /** The board's own routes are not registered and nothing explains why. */
44
+ | 'not-mounted'
45
+ /** Another live DSH process owns the ledger, so this instance registered no board routes (#1730). */
46
+ | 'ledger-locked'
47
+ /** This row degraded during start for some other reason the family health route reports (#1730). */
48
+ | 'degraded'
49
+ | 'unauthorized'
50
+ | 'forbidden'
51
+ | 'locked'
52
+ | 'rejected'
53
+ | 'timeout'
54
+ | 'unreachable'
55
+ | 'unexpected'
41
56
 
42
57
  /** Transport failure carrying a stable class next to its user-facing message. */
43
58
  export class HostApiError extends Error {
@@ -47,6 +62,86 @@ export class HostApiError extends Error {
47
62
  }
48
63
  }
49
64
 
65
+ /**
66
+ * Same-origin family health route served by the dsh-web-all shell: the
67
+ * degraded ledger, in which a family row that failed during apply appears WITH
68
+ * its reason. DOCUMENT-RELATIVE for the same reason as the board's own routes
69
+ * (issue #1707). A 404 here means the family shell is absent or older than the
70
+ * route, which is not itself a failure of the board.
71
+ */
72
+ const FAMILY_DEGRADED_ROUTE = 'api/dsh-web-all/degraded'
73
+
74
+ /** Health-route ceiling: an unresponsive family shell must not hold the board's first paint. */
75
+ const DEGRADED_LOOKUP_TIMEOUT_MS = 2_000
76
+
77
+ /**
78
+ * The failure text for a row this browser knows failed during start.
79
+ *
80
+ * A ledger lock is the one class with an owner the user can act on, so its
81
+ * reason is unpacked into the pid plus what to do about it; every other
82
+ * degraded row is reported with the shell's own reason verbatim. The raw
83
+ * reason rides along either way, because it is the only text that names the
84
+ * lock file or the recovery step when the pid is not the whole story.
85
+ * @param reason - the shell's one-line failure reason (never empty).
86
+ */
87
+ function degradedFailureMessage(reason: string): { failure: 'ledger-locked' | 'degraded'; message: string } {
88
+ const pid = ledgerOwnerPid(reason)
89
+ if (pid === undefined) return { failure: 'degraded', message: t('board.hostError.degraded', { detail: reason }) }
90
+ return { failure: 'ledger-locked', message: t('board.hostError.ledgerLocked', { pid: String(pid), detail: reason }) }
91
+ }
92
+
93
+ /**
94
+ * The degraded record the family shell holds for THIS board, or undefined when
95
+ * the row is healthy, the shell is absent, or the answer is unusable. The
96
+ * lookup is best-effort by contract: any uncertainty keeps the caller's own
97
+ * failure text instead of replacing it with a weaker one.
98
+ * @param fetchImpl - fetch implementation (injected for tests).
99
+ * @returns the ledger's reason for this row, or undefined when it names none.
100
+ */
101
+ async function fetchDegradedReason(fetchImpl: typeof fetch): Promise<string | undefined> {
102
+ const controller = new AbortController()
103
+ const timer = globalThis.setTimeout(() => { controller.abort() }, DEGRADED_LOOKUP_TIMEOUT_MS)
104
+ try {
105
+ const response = await fetchImpl(FAMILY_DEGRADED_ROUTE, { signal: controller.signal, headers: { accept: 'application/json' }, cache: 'no-store' })
106
+ if (!response.ok) return undefined
107
+ return degradedRowOf(await response.json(), TASK_BOARD_PACKAGE)?.reason
108
+ } catch {
109
+ return undefined
110
+ } finally {
111
+ globalThis.clearTimeout(timer)
112
+ }
113
+ }
114
+
115
+ /**
116
+ * Name the real reason this board's routes are missing (issue #1730).
117
+ *
118
+ * The core webserver answers an unmounted route with a bare 404, which cannot
119
+ * distinguish "this deployment never loaded the plugin" from "this row
120
+ * degraded during start" — a second DSH process holding `ledger-v2.lock` is
121
+ * the case that matters, because the panel's advice ("restart the service")
122
+ * is then false: the lock belongs to the other process, and restarting cannot
123
+ * win it. The family health route knows the difference, so the 404 is
124
+ * disambiguated there before anything is shown.
125
+ * @param fallback - copy for the plain "routes are not there" failure, which
126
+ * is route-specific (the board's state route and the AI-parse route phrase
127
+ * their own absence); it is also what is shown when the ledger names nothing.
128
+ * @param fetchImpl - fetch implementation (injected for tests).
129
+ * @returns the HostApiError to throw for a missing board route.
130
+ */
131
+ export async function missingRouteFailure(
132
+ fallback: string = t('board.hostError.notMounted'),
133
+ fetchImpl: typeof fetch = fetch,
134
+ ): Promise<HostApiError> {
135
+ const absent = (): HostApiError => new HostApiError('not-mounted', fallback, 404)
136
+ const reason = await fetchDegradedReason(fetchImpl)
137
+ // A shell that predates the reason field (or recorded none) tells us the row
138
+ // is degraded but not why; the route's own wording is more honest than an
139
+ // empty "because".
140
+ if (reason === undefined || reason === '') return absent()
141
+ const described = degradedFailureMessage(reason)
142
+ return new HostApiError(described.failure, described.message, 404)
143
+ }
144
+
50
145
  function uuid(): string {
51
146
  return globalThis.crypto?.randomUUID?.() ?? `browser-${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`
52
147
  }
@@ -79,14 +174,18 @@ async function readJson<T>(response: Response): Promise<T> {
79
174
  // The Host answered with a reason of its own. A ledger lock is the one
80
175
  // class worth naming in the panel's words; the authentication fence and
81
176
  // every other action rejection are reported as they arrive.
82
- if (hostError === 'forbidden') throw new HostApiError('unauthorized', t('board.hostError.unauthorized'), response.status)
177
+ if (hostError === 'forbidden') throw new HostApiError('forbidden', t('board.hostError.forbidden'), response.status)
83
178
  if (response.status === 503 || /lock/i.test(hostError)) {
84
179
  throw new HostApiError('locked', t('board.hostError.locked', { detail: hostError }), response.status)
85
180
  }
86
181
  throw new HostApiError('rejected', hostError, response.status)
87
182
  }
88
- if (response.status === 404) throw new HostApiError('not-mounted', t('board.hostError.notMounted'), 404)
89
- if (response.status === 401 || response.status === 403) throw new HostApiError('unauthorized', t('board.hostError.unauthorized'), response.status)
183
+ // A bare 404 is the ONLY signal that this board's routes are absent; the
184
+ // family health route is what turns it into the real reason when this row
185
+ // degraded during start (issue #1730).
186
+ if (response.status === 404) throw await missingRouteFailure()
187
+ if (response.status === 403) throw new HostApiError('forbidden', t('board.hostError.forbidden'), 403)
188
+ if (response.status === 401) throw new HostApiError('unauthorized', t('board.hostError.unauthorized'), 401)
90
189
  throw new HostApiError('unexpected', t('board.hostError.unexpected', { status: String(response.status) }), response.status)
91
190
  }
92
191
 
@@ -193,8 +292,12 @@ export class HttpTaskBoardHostTransport implements TaskBoardHostTransport {
193
292
  if (record !== undefined && isTaskParseDraft(record.draft)) return record.draft
194
293
  throw new HostApiError('unexpected', t('new.aiParseFailed', { error: t('board.hostError.unexpected', { status: String(response.status) }) }), response.status)
195
294
  }
196
- if (response.status === 404) throw new HostApiError('not-mounted', t('new.aiParseUnavailable'), 404)
197
- if (response.status === 401 || response.status === 403) throw new HostApiError('unauthorized', t('board.hostError.unauthorized'), response.status)
295
+ // The parse route lives beside the board's own routes, so a 404 means the
296
+ // same thing here: absent routes, with the same health-ledger detail and
297
+ // this route's own wording as the fallback.
298
+ if (response.status === 404) throw await missingRouteFailure(t('new.aiParseUnavailable'))
299
+ if (response.status === 403) throw new HostApiError('forbidden', t('board.hostError.forbidden'), 403)
300
+ if (response.status === 401) throw new HostApiError('unauthorized', t('board.hostError.unauthorized'), 401)
198
301
  const code = typeof record?.code === 'string' ? record.code : undefined
199
302
  if (code === 'no-model') throw new HostApiError('rejected', t('new.aiParseNoModel'), response.status)
200
303
  if (code === 'timeout') throw new HostApiError('timeout', t('new.aiParseTimeout', { seconds: String(TASK_PARSE_TIMEOUT_SECONDS) }), response.status)
@@ -33,8 +33,16 @@ import { TaskBoardSettingsCard, TaskBoardSettingsCardController, type TaskBoardS
33
33
  import { en, zh, setRuntimeTranslate, type TaskBoardKey } from './locales.ts'
34
34
  import { HttpTaskBoardHostTransport } from './host-api.ts'
35
35
  import { reportDailyHeartbeat } from './telemetry.ts'
36
- import { installPluginCard } from './plugin-card-seat.ts'
36
+ import { installBoardCard } from './board-card-seat.ts'
37
37
  import { createServedEntryForm } from './settings-entry-form.ts'
38
+ import { TaskBoardClientService } from './service.ts'
39
+ import {
40
+ TASK_BOARD_SERVICE_NAME,
41
+ type TaskBoardCardDecorationProps,
42
+ type TaskBoardDetailSectionProps,
43
+ type TaskBoardHostFace,
44
+ type TaskBoardSettingsSectionProps,
45
+ } from '../core/extension.ts'
38
46
 
39
47
  /** Locale namespace this plugin owns. */
40
48
  const NS = 'task-board'
@@ -95,6 +103,13 @@ declare module '@deepseek-ai/dsh-client-ui-slots' {
95
103
  * package can register without depending on the sibling UI package.
96
104
  */
97
105
  'web-ui.plugin.item': { kind: 'list'; scope: 'root'; owner: SettingsPluginItemOwnerProps }
106
+
107
+ /** Provider task-detail section; the board declares and renders it. */
108
+ 'task-board.detail.section': { kind: 'list'; scope: 'root'; owner: TaskBoardDetailSectionProps }
109
+ /** Provider settings section; the board declares and renders it. */
110
+ 'task-board.settings.section': { kind: 'list'; scope: 'root'; owner: TaskBoardSettingsSectionProps }
111
+ /** Provider card decoration; the board declares and renders it. */
112
+ 'task-board.card.decoration': { kind: 'list'; scope: 'root'; owner: TaskBoardCardDecorationProps }
98
113
  }
99
114
  }
100
115
 
@@ -211,12 +226,26 @@ export function apply(ctx: ClientContext): void {
211
226
  // namespace, contributed to whichever plugin-card seat this host declares
212
227
  // (the family group's list seat, or the official bundle-configuration seat).
213
228
  const settingsForm = bindSettingsForm(ctx)
214
- const settingsCard = new TaskBoardSettingsCardController(settingsForm)
215
- installPluginCard(ctx, {
229
+ // The board's browser-half extension service. It is provided before any
230
+ // controller exists (the UI mounts only once the settings form settles), so
231
+ // providers can subscribe and register visibility immediately.
232
+ const clientService = new TaskBoardClientService()
233
+ // A capture-only test context implements no service registry; the board's
234
+ // own surfaces still mount.
235
+ if (typeof (ctx as { provide?: unknown }).provide === 'function') {
236
+ ;(ctx.provide as unknown as (name: string, value: unknown) => void)(TASK_BOARD_SERVICE_NAME, clientService as unknown as TaskBoardHostFace)
237
+ }
238
+ ctx.effect(() => () => { clientService.detach() }, 'task-board: extension service')
239
+
240
+ const settingsCard = new TaskBoardSettingsCardController(settingsForm, request => clientService.dispatch(request))
241
+ installBoardCard(ctx, {
216
242
  bundle: '@linxin666/dsh-client-ui-task-board',
217
243
  id: 'task-board',
218
244
  order: 110,
219
245
  locale: NS,
246
+ // The board's own settings card declares the provider settings seat; the
247
+ // provider registers into it and this card renders it.
248
+ children: { 'task-board.settings.section': { kind: 'list', scope: 'root' } },
220
249
  inject: () => settingsCard.inject(),
221
250
  component: TaskBoardSettingsCard,
222
251
  })
@@ -263,6 +292,9 @@ export function apply(ctx: ClientContext): void {
263
292
  },
264
293
  })
265
294
  controller.start()
295
+ // The extension service follows the live controller: providers observe the
296
+ // board mirror and dispatch through it while the board runs.
297
+ clientService.attach(controller)
266
298
 
267
299
  const disposers: Array<() => void> = []
268
300
 
@@ -372,8 +404,12 @@ export function apply(ctx: ClientContext): void {
372
404
  console.error('[dsh-task-board] model options read failed', error)
373
405
  }
374
406
  }
407
+ void pushPresetOptions()
375
408
  void pushModelOptions()
376
- disposers.push(ctx.on('connection/reset', () => { void pushModelOptions() }))
409
+ disposers.push(ctx.on('connection/reset', () => {
410
+ void pushPresetOptions()
411
+ void pushModelOptions()
412
+ }))
377
413
  // Native panel surfaces: one sidebar row and one center-column page, both
378
414
  // through the official slot seats the shell itself renders. The layout's
379
415
  // panel selection is reconciled back into the controller so the board's own
@@ -396,6 +432,7 @@ export function apply(ctx: ClientContext): void {
396
432
 
397
433
  uiDisposer = () => {
398
434
  for (const dispose of disposers.splice(0)) dispose()
435
+ clientService.detach()
399
436
  controller.dispose()
400
437
  uiDisposer = undefined
401
438
  }
@@ -405,8 +442,13 @@ export function apply(ctx: ClientContext): void {
405
442
  const enabled = snapshot.status === 'ready'
406
443
  ? snapshot.value?.enabled ?? true
407
444
  : snapshot.status === 'unavailable'
408
- if (enabled) mountUi()
409
- else uiDisposer?.()
445
+ if (enabled) {
446
+ mountUi()
447
+ clientService.setEnabled(true)
448
+ } else {
449
+ clientService.setEnabled(false)
450
+ uiDisposer?.()
451
+ }
410
452
  }
411
453
  const unsubscribeSettings = settingsForm.subscribe(syncEnabled)
412
454
  // The mounted surfaces and the settings subscription belong to this fiber.