@linxin666/dsh-client-ui-task-board 0.4.4 → 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 (108) hide show
  1. package/README.i18n.yaml +2 -2
  2. package/README.md +42 -11
  3. package/README.zh.md +42 -11
  4. package/lib/client.js +4958 -472
  5. package/lib/client.js.map +1 -1
  6. package/lib/index.js +2841 -121
  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 +4 -1
  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/status-key.d.ts +15 -0
  24. package/lib/types/client/board/task-markdown.d.ts +19 -0
  25. package/lib/types/client/board-card-seat.d.ts +34 -0
  26. package/lib/types/client/index.d.ts +19 -0
  27. package/lib/types/client/locales.d.ts +103 -1
  28. package/lib/types/client/native-panel.d.ts +6 -20
  29. package/lib/types/client/seats.d.ts +18 -0
  30. package/lib/types/client/service.d.ts +32 -0
  31. package/lib/types/client/settings-form.d.ts +16 -0
  32. package/lib/types/core/controller.d.ts +37 -1
  33. package/lib/types/core/extension.d.ts +283 -0
  34. package/lib/types/core/handover.d.ts +13 -1
  35. package/lib/types/core/issue-form.d.ts +16 -0
  36. package/lib/types/core/tasks.d.ts +84 -3
  37. package/lib/types/core/use-cases/task-tag.d.ts +48 -0
  38. package/lib/types/core/use-cases/task-update.d.ts +5 -3
  39. package/lib/types/core/verification.d.ts +386 -0
  40. package/lib/types/core/workspace-target.d.ts +41 -0
  41. package/lib/types/host/agent-tools.d.ts +1 -1
  42. package/lib/types/host/extension-registry.d.ts +111 -0
  43. package/lib/types/host/llm-dispatch.d.ts +63 -0
  44. package/lib/types/host/verification-gate.d.ts +71 -0
  45. package/lib/types/host/verification-runner.d.ts +108 -0
  46. package/lib/types/host/workspace-evidence.d.ts +53 -0
  47. package/lib/types/host-ledger.d.ts +52 -8
  48. package/lib/types/host-runner.d.ts +23 -1
  49. package/lib/types/host-service.d.ts +88 -2
  50. package/lib/types/index.d.ts +80 -26
  51. package/lib/types/protocol.d.ts +49 -5
  52. package/lib/types/tool-surface.d.ts +71 -0
  53. package/package.json +27 -26
  54. package/src/client/PluginSettingsCard.tsx +9 -0
  55. package/src/client/TaskBoardSettingsCard.tsx +304 -65
  56. package/src/client/board/ClampedMarkdown.tsx +69 -0
  57. package/src/client/board/ConfirmDialog.tsx +7 -3
  58. package/src/client/board/EditTaskModal.tsx +5 -2
  59. package/src/client/board/LinkSubtaskModal.tsx +12 -7
  60. package/src/client/board/NewTaskModal.tsx +17 -1
  61. package/src/client/board/TagManagerModal.tsx +235 -0
  62. package/src/client/board/TaskBoard.tsx +259 -103
  63. package/src/client/board/TaskCard.tsx +214 -81
  64. package/src/client/board/TaskDetail.tsx +71 -26
  65. package/src/client/board/TaskForm.tsx +13 -3
  66. package/src/client/board/VerificationReport.tsx +203 -0
  67. package/src/client/board/card-view.ts +220 -0
  68. package/src/client/board/icons.tsx +89 -0
  69. package/src/client/board/overlay.tsx +189 -0
  70. package/src/client/board/status-key.ts +27 -0
  71. package/src/client/board/task-markdown.module.css +137 -0
  72. package/src/client/board/task-markdown.tsx +106 -0
  73. package/src/client/board-card-seat.ts +102 -0
  74. package/src/client/board-settings.module.css +66 -0
  75. package/src/client/board.module.css +488 -63
  76. package/src/client/index.ts +43 -5
  77. package/src/client/locales.ts +208 -4
  78. package/src/client/native-panel.tsx +18 -2
  79. package/src/client/seats.tsx +58 -0
  80. package/src/client/service.ts +94 -0
  81. package/src/client/settings-card.module.css +31 -5
  82. package/src/client/settings-form.ts +27 -1
  83. package/src/core/controller.ts +94 -3
  84. package/src/core/extension.ts +376 -0
  85. package/src/core/freeze-snapshot.ts +2 -2
  86. package/src/core/handover.ts +16 -1
  87. package/src/core/issue-form.ts +19 -0
  88. package/src/core/store.ts +16 -0
  89. package/src/core/tasks.ts +100 -6
  90. package/src/core/use-cases/task-parent.ts +3 -2
  91. package/src/core/use-cases/task-tag.ts +110 -0
  92. package/src/core/use-cases/task-update.ts +10 -4
  93. package/src/core/verification.ts +826 -0
  94. package/src/core/workspace-target.ts +59 -0
  95. package/src/host/agent-tools.ts +20 -18
  96. package/src/host/extension-registry.ts +509 -0
  97. package/src/host/llm-dispatch.ts +87 -0
  98. package/src/host/verification-gate.ts +331 -0
  99. package/src/host/verification-runner.ts +535 -0
  100. package/src/host/workspace-evidence.ts +176 -0
  101. package/src/host-ai.ts +15 -9
  102. package/src/host-ledger.ts +143 -32
  103. package/src/host-routes.ts +15 -2
  104. package/src/host-runner.ts +60 -11
  105. package/src/host-service.ts +253 -10
  106. package/src/index.ts +204 -16
  107. package/src/protocol.ts +102 -8
  108. package/src/tool-surface.ts +95 -0
@@ -7,9 +7,10 @@
7
7
  import { useMemo, useState } from 'react'
8
8
  import type { BoardController } from '../../core/controller.ts'
9
9
  import { DEFAULT_SUBTASK_DEPTH, buildLineageIndex, checkParentLink } from '../../core/subtask.ts'
10
- import type { TaskRecord } from '../../core/tasks.ts'
10
+ import { hasOpenExecution, type TaskRecord } from '../../core/tasks.ts'
11
11
  import { t } from '../locales.ts'
12
12
  import css from '../board.module.css'
13
+ import { useDialog, type OverlayPhase } from './overlay.tsx'
13
14
  import { STATUS_KEY } from './status-key.ts'
14
15
 
15
16
  /** Link-subtask overlay props. */
@@ -18,10 +19,13 @@ export interface LinkSubtaskModalProps {
18
19
  /** The task the picked card becomes a subtask of. */
19
20
  parent: TaskRecord
20
21
  onClose: () => void
22
+ /** Which leg of the enter/exit motion pair the surface is on. */
23
+ phase?: OverlayPhase
21
24
  }
22
25
 
23
26
  /** Link-subtask overlay. */
24
- export function LinkSubtaskModal({ controller, parent, onClose }: LinkSubtaskModalProps) {
27
+ export function LinkSubtaskModal({ controller, parent, onClose, phase = 'open' }: LinkSubtaskModalProps) {
28
+ const dialog = useDialog<HTMLDivElement>(onClose, phase)
25
29
  const [pending, setPending] = useState(false)
26
30
  const [error, setError] = useState<string | undefined>(undefined)
27
31
  const snapshot = controller.getSnapshot()
@@ -39,9 +43,10 @@ export function LinkSubtaskModal({ controller, parent, onClose }: LinkSubtaskMod
39
43
  const lineage = buildLineageIndex(tasks)
40
44
  return tasks.filter(task =>
41
45
  task.archivedAt === undefined
42
- // A running task may already be a participant of another run group; the
43
- // Host refuses the move, so it is not offered here.
44
- && task.status !== 'running'
46
+ // An executing task may already be a participant of another run group;
47
+ // the Host refuses the move, so it is not offered here. A card parked in
48
+ // the running column by hand carries no execution and stays linkable.
49
+ && !hasOpenExecution(task)
45
50
  && task.parentId === undefined
46
51
  && task.id !== parent.id
47
52
  && checkParentLink(tasks, task.id, parent.id, limit, lineage).ok,
@@ -58,8 +63,8 @@ export function LinkSubtaskModal({ controller, parent, onClose }: LinkSubtaskMod
58
63
  setError(controller.getSnapshot().transportError ?? t('new.required'))
59
64
  }
60
65
  return (
61
- <div className={css.modalBackdrop} onMouseDown={event => { if (event.target === event.currentTarget) onClose() }}>
62
- <div className={css.modal} role="dialog" aria-label={t('link.subtask.title')}>
66
+ <div className={css.modalBackdrop} data-state={phase} onMouseDown={dialog.onMouseDown}>
67
+ <div ref={dialog.attach} className={css.modal} role="dialog" aria-modal="true" aria-label={t('link.subtask.title')} tabIndex={-1}>
63
68
  <h2 className={css.modalTitle}>{t('link.subtask.title')}</h2>
64
69
  <p className={css.fieldHint}>{t('link.subtask.hint')}</p>
65
70
  {candidates.length === 0
@@ -12,6 +12,7 @@ import { t, type TaskBoardKey } from '../locales.ts'
12
12
  import { SCHEDULE_PRESETS } from '../schedule-presets.ts'
13
13
  import { nextRunLabel, zoneChoices } from '../schedule-zone.ts'
14
14
  import { CollapsibleSection, ModalShell, TaskContentFields, TaskTagFields, cleanTags } from './TaskForm.tsx'
15
+ import type { OverlayPhase } from './overlay.tsx'
15
16
  import { formatHostTimestamp } from './TaskCard.tsx'
16
17
  import { readParseModelPreference, writeParseModelPreference } from './parse-model-pref.ts'
17
18
  import { inheritPresetLabel, isBuiltinPreset, presetLabel } from './preset-label.ts'
@@ -35,10 +36,12 @@ export interface NewTaskModalProps {
35
36
  * carries the parent link; unset targets inherit again at run time.
36
37
  */
37
38
  parentTask?: TaskRecord
39
+ /** Which leg of the enter/exit motion pair the surface is on. */
40
+ phase?: OverlayPhase
38
41
  }
39
42
 
40
43
  /** New-task form overlay. */
41
- export function NewTaskModal({ controller, onClose, initialTask, defaultWorkspaceId, onDuplicateSuccess, parentTask }: NewTaskModalProps) {
44
+ export function NewTaskModal({ controller, onClose, initialTask, defaultWorkspaceId, onDuplicateSuccess, parentTask, phase }: NewTaskModalProps) {
42
45
  const isDuplicate = initialTask !== undefined
43
46
  const [title, setTitle] = useState(initialTask?.title ?? '')
44
47
  const [description, setDescription] = useState(initialTask?.description ?? '')
@@ -57,6 +60,7 @@ export function NewTaskModal({ controller, onClose, initialTask, defaultWorkspac
57
60
  // Checked by default: a new task starts its runs with dsh's built-in /goal
58
61
  // unless the user opts out here. A duplicate keeps the original card's choice.
59
62
  const [goalRun, setGoalRun] = useState(initialTask?.goalRun ?? true)
63
+ const [skipVerification, setSkipVerification] = useState(initialTask?.skipVerification ?? false)
60
64
  const [scheduleEnabled, setScheduleEnabled] = useState(initialTask?.schedule?.enabled ?? false)
61
65
  const [scheduleCron, setScheduleCron] = useState(initialTask?.schedule?.cron ?? '')
62
66
  // '' means "follow the Host zone" (store no zone), which is the default a
@@ -185,6 +189,7 @@ export function NewTaskModal({ controller, onClose, initialTask, defaultWorkspac
185
189
  model: model === '' ? undefined : model,
186
190
  ...(reuseSession ? { reuseSession: true } : {}),
187
191
  ...(goalRun ? {} : { goalRun: false }),
192
+ ...(skipVerification ? { skipVerification: true } : {}),
188
193
  ...(tagList.length > 0 ? { tags: tagList } : {}),
189
194
  schedule: scheduleEnabled
190
195
  ? { enabled: true, cron: scheduleCron.trim(), ...(scheduleZone === '' ? {} : { timeZone: scheduleZone }) }
@@ -277,6 +282,7 @@ export function NewTaskModal({ controller, onClose, initialTask, defaultWorkspac
277
282
  submitLabel={t('new.submit')}
278
283
  onSubmit={() => { void submit(false) }}
279
284
  onClose={onClose}
285
+ phase={phase}
280
286
  secondaryAction={{ label: t('new.createAndRun'), onSubmit: () => { void submit(true) } }}
281
287
  >
282
288
  {canParse && (
@@ -466,6 +472,16 @@ export function NewTaskModal({ controller, onClose, initialTask, defaultWorkspac
466
472
  <span>{t('exec.goalRun')}</span>
467
473
  </label>
468
474
  <p className={css.detailText}>{t('exec.goalRunHint')}</p>
475
+
476
+ <label className={css.scheduleToggle}>
477
+ <input
478
+ type="checkbox"
479
+ checked={skipVerification}
480
+ onChange={event => { setSkipVerification(event.target.checked) }}
481
+ />
482
+ <span>{t('exec.skipVerification')}</span>
483
+ </label>
484
+ <p className={css.detailText}>{t('exec.skipVerificationHint')}</p>
469
485
  </CollapsibleSection>
470
486
 
471
487
  <CollapsibleSection
@@ -0,0 +1,235 @@
1
+ /**
2
+ * Label manager: the board's label row can only shrink through here.
3
+ *
4
+ * The filter row is the union of every label in use across the ledger, so a
5
+ * label created once — by an experiment, a pasted task, an import — stayed on
6
+ * the board forever: there was no way to rename a typo, merge two labels that
7
+ * mean the same thing, or remove one nobody uses. This dialog is that way out.
8
+ * It lists the labels with their usage counts and offers the two ledger-wide
9
+ * transitions, both of which go through the controller so the Host ledger stays
10
+ * the authority (and a shared board stays consistent for everyone watching it).
11
+ *
12
+ * Renaming onto a name already in use merges the two labels; the dialog says so
13
+ * before the edit is submitted. Deleting is confirmed first, and the
14
+ * confirmation names how many tasks the label is removed from.
15
+ */
16
+ import { useMemo, useState } from 'react'
17
+ import type { BoardController } from '../../core/controller.ts'
18
+ import { TAG_NAME_MAX_LENGTH, collectKnownTags, type TaskRecord } from '../../core/tasks.ts'
19
+ import { t } from '../locales.ts'
20
+ import css from '../board.module.css'
21
+ import { ConfirmDialog } from './ConfirmDialog.tsx'
22
+ import { IconClose } from './icons.tsx'
23
+ import { useDialog, usePresence, type OverlayPhase } from './overlay.tsx'
24
+
25
+ /** One label as the manager lists it: its definition plus how many tasks carry it. */
26
+ export interface TagUsage {
27
+ name: string
28
+ promptPrefix?: string
29
+ /** Tasks carrying the label right now. */
30
+ count: number
31
+ }
32
+
33
+ /**
34
+ * Labels in use, in board order (first occurrence wins, exactly like the
35
+ * filter row), each with its usage count.
36
+ * @param tasks - the whole ledger (board and archive).
37
+ * @returns one row per label.
38
+ */
39
+ export function tagUsage(tasks: readonly TaskRecord[]): TagUsage[] {
40
+ const counts = new Map<string, number>()
41
+ for (const task of tasks) {
42
+ for (const tag of task.tags ?? []) counts.set(tag.name, (counts.get(tag.name) ?? 0) + 1)
43
+ }
44
+ return collectKnownTags(tasks).map(tag => ({
45
+ name: tag.name,
46
+ ...(tag.promptPrefix === undefined ? {} : { promptPrefix: tag.promptPrefix }),
47
+ count: counts.get(tag.name) ?? 0,
48
+ }))
49
+ }
50
+
51
+ /** Label-manager overlay props. */
52
+ export interface TagManagerModalProps {
53
+ controller: BoardController
54
+ onClose: () => void
55
+ /** Which leg of the enter/exit motion pair the surface is on. */
56
+ phase?: OverlayPhase
57
+ }
58
+
59
+ /** Label-manager overlay. */
60
+ export function TagManagerModal({ controller, onClose, phase = 'open' }: TagManagerModalProps) {
61
+ /** The label whose row is being renamed, and the draft name. */
62
+ const [editing, setEditing] = useState<string | undefined>(undefined)
63
+ const [draft, setDraft] = useState('')
64
+ const [busy, setBusy] = useState(false)
65
+ const [error, setError] = useState<string | undefined>(undefined)
66
+ const [confirmDelete, setConfirmDelete] = useState<string | undefined>(undefined)
67
+ const confirmPresence = usePresence(confirmDelete !== undefined)
68
+ // Escape and a backdrop press peel one layer at a time: an open row editor
69
+ // first, then the dialog — so a half-typed rename is never lost to the same
70
+ // key that closes the surface under it.
71
+ const dismiss = (): void => {
72
+ if (editing !== undefined) {
73
+ setEditing(undefined)
74
+ setError(undefined)
75
+ return
76
+ }
77
+ onClose()
78
+ }
79
+ const dialog = useDialog<HTMLDivElement>(dismiss, phase)
80
+ const snapshot = controller.getSnapshot()
81
+ const rows = useMemo(() => tagUsage(snapshot.tasks), [snapshot.tasks])
82
+ const deleting = rows.find(row => row.name === confirmDelete)
83
+ /** The label the typed name would merge into, when it is already in use. */
84
+ const mergesInto = editing === undefined ? undefined : rows.find(row => row.name === draft.trim() && row.name !== editing)
85
+
86
+ const startRename = (row: TagUsage): void => {
87
+ setEditing(row.name)
88
+ setDraft(row.name)
89
+ setError(undefined)
90
+ }
91
+
92
+ const submitRename = async (from: string): Promise<void> => {
93
+ const to = draft.trim()
94
+ if (to === '' || to === from) {
95
+ setEditing(undefined)
96
+ return
97
+ }
98
+ setBusy(true)
99
+ setError(undefined)
100
+ const accepted = await controller.renameTag(from, to)
101
+ setBusy(false)
102
+ if (accepted) {
103
+ setEditing(undefined)
104
+ return
105
+ }
106
+ setError(controller.getSnapshot().transportError ?? t('tags.renameFailed'))
107
+ }
108
+
109
+ const remove = async (name: string): Promise<void> => {
110
+ setBusy(true)
111
+ setError(undefined)
112
+ const accepted = await controller.deleteTag(name)
113
+ setBusy(false)
114
+ if (!accepted) setError(controller.getSnapshot().transportError ?? t('tags.deleteFailed'))
115
+ }
116
+
117
+ return (
118
+ <>
119
+ <div className={css.modalBackdrop} data-state={phase} onMouseDown={dialog.onMouseDown}>
120
+ <div
121
+ ref={dialog.attach}
122
+ className={css.modal}
123
+ role="dialog"
124
+ aria-modal="true"
125
+ aria-label={t('tags.title')}
126
+ tabIndex={-1}
127
+ >
128
+ <header className={css.modalHeader}>
129
+ <h2 className={css.modalTitle}>{t('tags.title')}</h2>
130
+ <button
131
+ type="button"
132
+ className={css.iconButton}
133
+ aria-label={t('detail.close')}
134
+ title={t('detail.close')}
135
+ onClick={onClose}
136
+ >
137
+ <IconClose size={16} />
138
+ </button>
139
+ </header>
140
+
141
+ <div className={css.modalBody}>
142
+ <p className={css.fieldHint}>{t('tags.hint')}</p>
143
+ {rows.length === 0
144
+ ? <p className={css.detailText}>{t('tags.empty')}</p>
145
+ : (
146
+ <ul className={css.tagManageList}>
147
+ {rows.map(row => (
148
+ <li key={row.name} className={css.tagManageRow}>
149
+ {editing === row.name
150
+ ? (
151
+ <>
152
+ <input
153
+ className={css.input + ' ' + css.tagManageInput}
154
+ value={draft}
155
+ maxLength={TAG_NAME_MAX_LENGTH}
156
+ spellCheck={false}
157
+ aria-label={t('tags.renameLabel', { name: row.name })}
158
+ onChange={event => { setDraft(event.target.value); setError(undefined) }}
159
+ onKeyDown={event => {
160
+ if (event.key === 'Enter') void submitRename(row.name)
161
+ }}
162
+ />
163
+ <button
164
+ type="button"
165
+ className={css.primaryButton}
166
+ disabled={busy || draft.trim() === ''}
167
+ onClick={() => { void submitRename(row.name) }}
168
+ >
169
+ {t('tags.save')}
170
+ </button>
171
+ <button
172
+ type="button"
173
+ className={css.ghostButton}
174
+ disabled={busy}
175
+ onClick={() => { setEditing(undefined); setError(undefined) }}
176
+ >
177
+ {t('new.cancel')}
178
+ </button>
179
+ </>
180
+ )
181
+ : (
182
+ <>
183
+ <span className={css.tagManageName}>{row.name}</span>
184
+ <span className={css.tagManageCount}>{t('tags.usage', { count: String(row.count) })}</span>
185
+ <button
186
+ type="button"
187
+ className={css.ghostButton}
188
+ disabled={busy}
189
+ onClick={() => { startRename(row) }}
190
+ >
191
+ {t('tags.rename')}
192
+ </button>
193
+ <button
194
+ type="button"
195
+ className={css.ghostButton}
196
+ disabled={busy}
197
+ onClick={() => { setConfirmDelete(row.name) }}
198
+ >
199
+ {t('tags.delete')}
200
+ </button>
201
+ </>
202
+ )}
203
+ {editing === row.name && mergesInto !== undefined && (
204
+ <span className={css.tagManageMerge}>{t('tags.mergeHint', { name: mergesInto.name })}</span>
205
+ )}
206
+ </li>
207
+ ))}
208
+ </ul>
209
+ )}
210
+ {error !== undefined && <p className={css.formError}>{error}</p>}
211
+ </div>
212
+
213
+ <footer className={css.modalFooter}>
214
+ <button type="button" className={css.ghostButton} onClick={onClose}>{t('tags.done')}</button>
215
+ </footer>
216
+ </div>
217
+ </div>
218
+
219
+ {confirmPresence.mounted && deleting !== undefined && (
220
+ <ConfirmDialog
221
+ title={t('tags.deleteTitle')}
222
+ message={t('tags.deleteConfirm', { name: deleting.name, count: String(deleting.count) })}
223
+ confirmLabel={t('tags.deleteOk')}
224
+ danger
225
+ phase={confirmPresence.phase}
226
+ onCancel={() => { setConfirmDelete(undefined) }}
227
+ onConfirm={() => {
228
+ setConfirmDelete(undefined)
229
+ void remove(deleting.name)
230
+ }}
231
+ />
232
+ )}
233
+ </>
234
+ )
235
+ }