dsh-taskboard 0.5.5 → 0.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/README.md +26 -2
  2. package/lib/client.js +2663 -767
  3. package/lib/host/execution.js +3 -0
  4. package/lib/host/execution.js.map +1 -1
  5. package/lib/host/routes.js +31 -1
  6. package/lib/host/routes.js.map +1 -1
  7. package/lib/host/session-sync.js +210 -10
  8. package/lib/host/session-sync.js.map +1 -1
  9. package/lib/host/store.js +9 -2
  10. package/lib/host/store.js.map +1 -1
  11. package/lib/index.js +100 -1
  12. package/lib/index.js.map +1 -1
  13. package/lib/shared/api.js.map +1 -1
  14. package/lib/shared/protocol.js +19 -1
  15. package/lib/shared/protocol.js.map +1 -1
  16. package/package.json +75 -75
  17. package/src/client/api.ts +8 -0
  18. package/src/client/board/AlertModal.tsx +3 -1
  19. package/src/client/board/ImportModal.tsx +26 -24
  20. package/src/client/board/SettingsModal.tsx +66 -26
  21. package/src/client/board/SlashPromptInput.tsx +340 -0
  22. package/src/client/board/TaskBoard.tsx +53 -49
  23. package/src/client/board/TaskCard.tsx +33 -21
  24. package/src/client/board/TaskDetail.tsx +169 -104
  25. package/src/client/board/TaskFormModal.tsx +254 -202
  26. package/src/client/board/TemplateManager.tsx +32 -29
  27. package/src/client/board/labels.ts +36 -27
  28. package/src/client/controller.ts +62 -2
  29. package/src/client/i18n/en.ts +455 -0
  30. package/src/client/i18n/runtime.ts +155 -0
  31. package/src/client/i18n/zh.ts +460 -0
  32. package/src/client/index.ts +182 -42
  33. package/src/client/sidebar-entry.ts +13 -3
  34. package/src/client/styles.ts +133 -0
  35. package/src/host/execution.ts +13 -0
  36. package/src/host/routes.ts +49 -1
  37. package/src/host/session-sync.ts +334 -14
  38. package/src/host/store.ts +15 -1
  39. package/src/index.ts +115 -1
  40. package/src/shared/api.ts +47 -0
  41. package/src/shared/protocol.ts +41 -0
  42. package/src/shared/version.ts +1 -1
@@ -8,12 +8,14 @@
8
8
  import { useState } from 'react'
9
9
  import type { BoardController } from '../controller.ts'
10
10
  import { useAlert } from './AlertModal.tsx'
11
+ import { useT } from '../i18n/runtime.ts'
11
12
 
12
13
  /**
13
14
  * The template manager modal.
14
15
  * @param controller - the controller.
15
16
  */
16
17
  export function TemplateManager({ controller }: { controller: BoardController }) {
18
+ const t = useT()
17
19
  const state = controller.getSnapshot()
18
20
  const [edits, setEdits] = useState<Record<string, string>>({})
19
21
  const [confirmId, setConfirmId] = useState<string | undefined>(undefined)
@@ -30,78 +32,79 @@ export function TemplateManager({ controller }: { controller: BoardController })
30
32
  void controller.upsertTemplate({ id, name, task: template.task }).then(ok => {
31
33
  if (ok) {
32
34
  setEdits(prev => { const next = { ...prev }; delete next[id]; return next })
33
- showAlert('模板已改名')
35
+ showAlert(t('tpl.renamed'))
34
36
  }
35
37
  })
36
38
  }
37
39
 
38
40
  return (
39
41
  <div className="dsh-atb-modal-backdrop" onClick={e => { if (e.target === e.currentTarget) close() }}>
40
- <div className="dsh-atb-modal dsh-atb-tplm" role="dialog" aria-modal="true" aria-label="管理模板">
42
+ <div className="dsh-atb-modal dsh-atb-tplm" role="dialog" aria-modal="true" aria-label={t('tpl.aria')}>
41
43
  <div className="dsh-atb-modal-head">
42
44
  <span className="dsh-atb-modal-headicon">⌗</span>
43
45
  <div className="dsh-atb-modal-headtext">
44
- <h3>任务模板</h3>
45
- <p>新建任务 ▼ 下拉的模板:改名 / 删除 / 直接使用;任务详情页「存为模板」可新增</p>
46
+ <h3>{t('tpl.title')}</h3>
47
+ <p>{t('tpl.subtitle')}</p>
46
48
  </div>
47
- <button type="button" className="dsh-atb-modal-close" aria-label="关闭" onClick={close}>✕</button>
49
+ <button type="button" className="dsh-atb-modal-close" aria-label={t('shared.close')} onClick={close}>✕</button>
48
50
  </div>
49
51
  <div className="dsh-atb-modal-body">
50
52
  {state.templates.length === 0
51
- ? <div className="dsh-atb-empty2">暂无模板 — 在任务详情页点「存为模板」把常用配置沉淀下来</div>
53
+ ? <div className="dsh-atb-empty2">{t('tpl.empty')}</div>
52
54
  : (
53
55
  <div className="dsh-atb-tplm-list">
54
- {state.templates.map(t => (
55
- <div key={t.id} className="dsh-atb-tplm-row">
56
+ {state.templates.map(tpl => (
57
+ <div key={tpl.id} className="dsh-atb-tplm-row">
56
58
  <input
57
59
  className="dsh-atb-tplm-name"
58
- value={nameOf(t.id, t.name)}
60
+ value={nameOf(tpl.id, tpl.name)}
59
61
  maxLength={60}
60
62
  spellCheck={false}
61
- aria-label={`模板名 ${t.name}`}
62
- onChange={e => setEdits(prev => ({ ...prev, [t.id]: e.target.value }))}
63
+ aria-label={t('tpl.name.aria', { name: tpl.name })}
64
+ onChange={e => setEdits(prev => ({ ...prev, [tpl.id]: e.target.value }))}
63
65
  onKeyDown={e => {
64
- if (e.key === 'Enter') save(t.id, nameOf(t.id, t.name))
66
+ if (e.key === 'Enter') save(tpl.id, nameOf(tpl.id, tpl.name))
65
67
  }}
66
68
  />
67
69
  <span
68
70
  className="dsh-atb-tplm-meta"
69
- title={`${t.builtin === true ? '内置' : '自建'}${t.task.checklist !== undefined && t.task.checklist.length > 0 ? ` · 清单 ${t.task.checklist.length} 项` : ''}${t.task.urgency !== undefined ? ` · ${t.task.urgency}` : ''}`}
71
+ title={`${tpl.builtin === true ? t('tpl.builtin') : t('tpl.custom')}${tpl.task.checklist !== undefined && tpl.task.checklist.length > 0 ? t('tpl.meta.checklist', { n: tpl.task.checklist.length }) : ''}${tpl.task.urgency !== undefined ? ` · ${tpl.task.urgency}` : ''}${tpl.task.permission !== undefined ? ` · ${t('shared.permission')}: ${tpl.task.permission}` : ''}`}
70
72
  >
71
- {t.builtin === true ? '内置' : '自建'}
72
- {t.task.checklist !== undefined && t.task.checklist.length > 0 ? ` · 清单 ${t.task.checklist.length} 项` : ''}
73
- {t.task.urgency !== undefined ? ` · ${t.task.urgency}` : ''}
73
+ {tpl.builtin === true ? t('tpl.builtin') : t('tpl.custom')}
74
+ {tpl.task.checklist !== undefined && tpl.task.checklist.length > 0 ? t('tpl.meta.checklist', { n: tpl.task.checklist.length }) : ''}
75
+ {tpl.task.urgency !== undefined ? ` · ${tpl.task.urgency}` : ''}
76
+ {tpl.task.permission !== undefined && tpl.task.permission !== 'workspace-write' ? ` · ${tpl.task.permission === 'read-only' ? t('tpl.meta.permReadOnly') : t('tpl.meta.permFull')}` : ''}
74
77
  </span>
75
78
  <span className="dsh-atb-tplm-btns">
76
79
  <button
77
80
  type="button"
78
81
  className="dsh-atb-btn"
79
- disabled={nameOf(t.id, t.name) === t.name || nameOf(t.id, t.name).trim().length === 0}
80
- title="保存改名"
81
- onClick={() => save(t.id, nameOf(t.id, t.name))}
82
+ disabled={nameOf(tpl.id, tpl.name) === tpl.name || nameOf(tpl.id, tpl.name).trim().length === 0}
83
+ title={t('tpl.rename.title')}
84
+ onClick={() => save(tpl.id, nameOf(tpl.id, tpl.name))}
82
85
  >
83
- 改名
86
+ {t('tpl.rename.button')}
84
87
  </button>
85
88
  <button
86
89
  type="button"
87
90
  className="dsh-atb-btn"
88
- title="用此模板打开新建表单"
91
+ title={t('tpl.use.title')}
89
92
  onClick={() => {
90
93
  close()
91
- controller.newFromTemplate(t.task)
94
+ controller.newFromTemplate(tpl.task)
92
95
  }}
93
96
  >
94
- 用此新建
97
+ {t('tpl.use.button')}
95
98
  </button>
96
- {confirmId === t.id
99
+ {confirmId === tpl.id
97
100
  ? (
98
101
  <>
99
- <button type="button" className="dsh-atb-btn" data-danger="true" onClick={() => { void controller.deleteTemplate(t.id); setConfirmId(undefined) }}>确认删除</button>
100
- <button type="button" className="dsh-atb-btn" onClick={() => setConfirmId(undefined)}>取消</button>
102
+ <button type="button" className="dsh-atb-btn" data-danger="true" onClick={() => { void controller.deleteTemplate(tpl.id); setConfirmId(undefined) }}>{t('shared.confirmDelete')}</button>
103
+ <button type="button" className="dsh-atb-btn" onClick={() => setConfirmId(undefined)}>{t('shared.cancel')}</button>
101
104
  </>
102
105
  )
103
106
  : (
104
- <button type="button" className="dsh-atb-btn" data-danger="true" title="删除该模板" onClick={() => setConfirmId(t.id)}>
107
+ <button type="button" className="dsh-atb-btn" data-danger="true" title={t('tpl.delete.title')} onClick={() => setConfirmId(tpl.id)}>
105
108
  🗑
106
109
  </button>
107
110
  )}
@@ -112,9 +115,9 @@ export function TemplateManager({ controller }: { controller: BoardController })
112
115
  )}
113
116
  </div>
114
117
  <div className="dsh-atb-modal-foot">
115
- <span className="dsh-atb-modal-hint">模板随台账一同保存在 DSH 主目录,升级不丢</span>
118
+ <span className="dsh-atb-modal-hint">{t('tpl.foot.hint')}</span>
116
119
  <span className="dsh-atb-modal-footbtns">
117
- <button type="button" className="dsh-atb-btn" onClick={close}>关闭</button>
120
+ <button type="button" className="dsh-atb-btn" onClick={close}>{t('shared.close')}</button>
118
121
  </span>
119
122
  </div>
120
123
  </div>
@@ -1,44 +1,53 @@
1
1
  /**
2
- * Centralized zh-CN display labels for the board UI.
2
+ * Key maps for the board's enumerated display text (status / urgency /
3
+ * outcome). The values are i18n dictionary keys (see src/client/i18n/zh.ts
4
+ * for the canon and en.ts for the English side); components resolve them
5
+ * with useT()/translate() at render time, so the text follows the GUI
6
+ * language live.
3
7
  *
4
- * Review P2: the same status/urgency/outcome text used to live in three
5
- * components (TaskBoard / TaskCard / TaskDetail) and drifted — adding a
6
- * status meant three edits, missing one leaked the raw English key.
8
+ * Review P2 history: the same status/urgency/outcome text used to live in
9
+ * three components (TaskBoard / TaskCard / TaskDetail) and drifted — adding
10
+ * a status meant three edits, missing one leaked the raw English key. The
11
+ * three-way split below is deliberate and survived the i18n migration:
12
+ *
13
+ * - COLUMN_KEYS — column headers on the five-column main board (+ secondary tab)
14
+ * - STATUS_KEYS — status pill text (detail pane); terminal states read short
15
+ * - MOVE_KEYS — move-button verbs (shorter than the pill text)
7
16
  *
8
17
  * @module dsh-taskboard/client/board/labels
9
18
  */
10
19
  import type { TaskStatus, Urgency } from '../../shared/protocol.ts'
11
20
 
12
- /** Column headers on the five-column main board (+ secondary tab). */
13
- export const COLUMN_LABELS: Readonly<Record<TaskStatus, string>> = {
14
- backlog: '待规划',
15
- todo: '待办',
16
- in_progress: '进行中',
17
- in_review: '待验收',
18
- done: '已完成',
19
- canceled: '已取消',
20
- archived: '已归档',
21
+ /** Column header keys on the five-column main board (+ secondary tab). */
22
+ export const COLUMN_KEYS: Readonly<Record<TaskStatus, string>> = {
23
+ backlog: 'status.column.backlog',
24
+ todo: 'status.column.todo',
25
+ in_progress: 'status.column.in_progress',
26
+ in_review: 'status.column.in_review',
27
+ done: 'status.column.done',
28
+ canceled: 'status.column.canceled',
29
+ archived: 'status.column.archived',
21
30
  }
22
31
 
23
- /** Status pill text (detail pane) — historical wording kept verbatim:
32
+ /** Status pill keys (detail pane) — historical wording kept verbatim:
24
33
  * terminal states read short here, the column headers carry the full forms. */
25
- export const STATUS_LABEL: Readonly<Record<TaskStatus, string>> = {
26
- backlog: '待规划', todo: '待办', in_progress: '进行中', in_review: '待验收',
27
- done: '完成', canceled: '取消', archived: '归档',
34
+ export const STATUS_KEYS: Readonly<Record<TaskStatus, string>> = {
35
+ backlog: 'status.pill.backlog', todo: 'status.pill.todo', in_progress: 'status.pill.in_progress', in_review: 'status.pill.in_review',
36
+ done: 'status.pill.done', canceled: 'status.pill.canceled', archived: 'status.pill.archived',
28
37
  }
29
38
 
30
- /** Move-button verbs (shorter than the pill text). */
31
- export const MOVE_LABEL: Readonly<Record<TaskStatus, string>> = {
32
- backlog: '待规划', todo: '待办', in_progress: '进行中', in_review: '待验收',
33
- done: '完成', canceled: '取消', archived: '归档',
39
+ /** Move-button verb keys (shorter than the pill text). */
40
+ export const MOVE_KEYS: Readonly<Record<TaskStatus, string>> = {
41
+ backlog: 'status.move.backlog', todo: 'status.move.todo', in_progress: 'status.move.in_progress', in_review: 'status.move.in_review',
42
+ done: 'status.move.done', canceled: 'status.move.canceled', archived: 'status.move.archived',
34
43
  }
35
44
 
36
- /** Urgency chip labels. */
37
- export const URGENCY_LABEL: Readonly<Record<Urgency, string>> = {
38
- urgent: '紧急', normal: '一般', relaxed: '不急',
45
+ /** Urgency chip keys. */
46
+ export const URGENCY_KEYS: Readonly<Record<Urgency, string>> = {
47
+ urgent: 'urgency.urgent', normal: 'urgency.normal', relaxed: 'urgency.relaxed',
39
48
  }
40
49
 
41
- /** Execution outcome labels. */
42
- export const OUTCOME_LABEL: Readonly<Record<string, string>> = {
43
- running: '执行中', succeeded: '成功', failed: '失败', cancelled: '已取消',
50
+ /** Execution outcome keys. */
51
+ export const OUTCOME_KEYS: Readonly<Record<string, string>> = {
52
+ running: 'outcome.running', succeeded: 'outcome.succeeded', failed: 'outcome.failed', cancelled: 'outcome.cancelled',
44
53
  }
@@ -7,11 +7,12 @@
7
7
  *
8
8
  * @module dsh-taskboard/client/controller
9
9
  */
10
- import type { ChangeEvent, DiagnosticsResponse, DiffResponse, ImportCommitResponse, ImportPreviewResponse, TaskTemplate, TaskTemplateSpec, UpdateTaskBody, WorkspaceView } from '../shared/api.ts'
10
+ import type { ChangeEvent, DiagnosticsResponse, DiffResponse, ImportCommitResponse, ImportPreviewResponse, PromptCompletionsResponse, TaskTemplate, TaskTemplateSpec, UpdateTaskBody, WorkspaceView } from '../shared/api.ts'
11
11
  import type { ChecklistItem, TaskLedger, TaskRecord, Urgency } from '../shared/protocol.ts'
12
12
  import { emptyLedger } from '../shared/protocol.ts'
13
13
  import type { TaskboardClient } from './api.ts'
14
14
  import type { SessionJumpResult } from './session-jump.ts'
15
+ import { translate } from './i18n/runtime.ts'
15
16
 
16
17
  /** View filters over the ledger. */
17
18
  export interface BoardFilters {
@@ -321,6 +322,54 @@ export class BoardController {
321
322
  return this.catalogFaces.presets
322
323
  }
323
324
 
325
+ /**
326
+ * Fetch model catalog: prefers installed runtime face, falls back to Taskboard client API (0.5.5).
327
+ */
328
+ async fetchModelCatalog(): Promise<Array<{
329
+ provider: string
330
+ model: string
331
+ name?: string
332
+ description?: string
333
+ reasoning?: {
334
+ efforts: Array<{ id: string; name: string; description?: string }>
335
+ defaultEffort?: string
336
+ }
337
+ }>> {
338
+ if (this.catalogFaces.models !== undefined) {
339
+ try {
340
+ const list = await this.catalogFaces.models()
341
+ if (list !== undefined && list.length > 0) return list
342
+ } catch { /* fallback to client */ }
343
+ }
344
+ try {
345
+ const res = await this.client.modelCatalog()
346
+ return res.models ?? []
347
+ } catch {
348
+ return []
349
+ }
350
+ }
351
+
352
+ /**
353
+ * Fetch preset roster: prefers installed runtime face, falls back to Taskboard client API (0.5.5).
354
+ */
355
+ async fetchPresetCatalog(): Promise<{ presets: Array<{ id: string; name?: string }>; defaultId?: string }> {
356
+ if (this.catalogFaces.presets !== undefined) {
357
+ try {
358
+ const roster = await this.catalogFaces.presets()
359
+ if (roster !== undefined && roster.presets !== undefined && roster.presets.length > 0) return roster
360
+ } catch { /* fallback to client */ }
361
+ }
362
+ try {
363
+ const res = await this.client.modelCatalog()
364
+ return {
365
+ presets: res.presets ?? [],
366
+ ...(res.defaultPresetId !== undefined ? { defaultId: res.defaultPresetId } : {}),
367
+ }
368
+ } catch {
369
+ return { presets: [] }
370
+ }
371
+ }
372
+
324
373
  /**
325
374
  * Jump to an execution's session (open it in the GUI). On success the board
326
375
  * closes so the conversation shows; a deleted-or-archived session reports
@@ -557,7 +606,7 @@ export class BoardController {
557
606
  try {
558
607
  await this.client.create({
559
608
  // Keep the suffix under the host's 200-char title cap (review P1).
560
- title: `${task.title.slice(0, 196)}(副本)`,
609
+ title: task.title.slice(0, 196) + translate('shared.duplicate.suffix'),
561
610
  workspaceId: task.workspaceId,
562
611
  urgency: task.urgency,
563
612
  description: task.description.length > 0 ? task.description : undefined,
@@ -568,6 +617,7 @@ export class BoardController {
568
617
  model: task.model,
569
618
  isolation: task.isolation,
570
619
  ...(task.presetId !== undefined ? { presetId: task.presetId } : {}),
620
+ ...(task.permission !== undefined ? { permission: task.permission } : {}),
571
621
  ...(task.checklist !== undefined && task.checklist.length > 0 ? { checklist: task.checklist.map(i => i.text) } : {}),
572
622
  })
573
623
  await this.refresh()
@@ -640,11 +690,21 @@ export class BoardController {
640
690
  model: task.model,
641
691
  isolation: task.isolation,
642
692
  ...(task.presetId !== undefined ? { presetId: task.presetId } : {}),
693
+ ...(task.permission !== undefined ? { permission: task.permission } : {}),
643
694
  ...(task.checklist !== undefined && task.checklist.length > 0 ? { checklist: task.checklist.map(i => i.text) } : {}),
644
695
  },
645
696
  })
646
697
  }
647
698
 
699
+ /** Load prompt completions (skills + slash commands) from host (0.5.5). */
700
+ async fetchPromptCompletions(): Promise<PromptCompletionsResponse | undefined> {
701
+ try {
702
+ return await this.client.promptCompletions()
703
+ } catch {
704
+ return undefined
705
+ }
706
+ }
707
+
648
708
  // ------------------------------------------------ import (0.4.0)
649
709
  /** Open the import modal. */
650
710
  openImport(): void { this.setState({ importOpen: true }) }