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
package/package.json CHANGED
@@ -1,75 +1,75 @@
1
- {
2
- "name": "dsh-taskboard",
3
- "description": "Agent-first task board for the DSH web GUI: host-authoritative task ledger with taskboard_* agent tools, project (= workspace) claim boundaries, per-task model execution in fresh sessions, optional per-task git-worktree isolation (dedicated task branches, commit evidence, one-click merge), host-side cron scheduling, and a live SSE kanban view. Mounts via the official dsh plugin system — no DSH source changes.",
4
- "version": "0.5.5",
5
- "type": "module",
6
- "main": "lib/index.js",
7
- "exports": {
8
- ".": "./lib/index.js",
9
- "./invariant": "./lib/invariant.js",
10
- "./client": "./lib/client.js",
11
- "./package.json": "./package.json"
12
- },
13
- "dsh": {
14
- "bundle": {
15
- "patch": "./cordis.patch.yml"
16
- },
17
- "client": {
18
- "inject": [],
19
- "platform": "web"
20
- }
21
- },
22
- "files": [
23
- "lib/**/*.js",
24
- "lib/**/*.js.map",
25
- "src",
26
- "cordis.patch.yml",
27
- "LICENSE"
28
- ],
29
- "license": "Apache-2.0",
30
- "author": "cloader",
31
- "repository": {
32
- "type": "git",
33
- "url": "git+https://github.com/cloader/dsh-taskboard.git"
34
- },
35
- "bugs": {
36
- "url": "https://github.com/cloader/dsh-taskboard/issues"
37
- },
38
- "homepage": "https://github.com/cloader/dsh-taskboard#readme",
39
- "keywords": [
40
- "dsh",
41
- "deepseek-harness",
42
- "dsh-plugin",
43
- "taskboard",
44
- "kanban",
45
- "agent"
46
- ],
47
- "scripts": {
48
- "build": "node scripts/clean-lib.mjs && npm run build:host && npm run build:client",
49
- "build:host": "tsdown -c tsdown.host.config.ts",
50
- "build:client": "tsdown -c tsdown.client.config.ts && node scripts/wrap-client.mjs",
51
- "watch": "tsdown -c tsdown.host.config.ts --watch",
52
- "typecheck": "tsc --noEmit",
53
- "test": "vitest run"
54
- },
55
- "devDependencies": {
56
- "@deepseek-ai/cordis": "^4.0.1",
57
- "@deepseek-ai/dsh-agent": "^0.1.1-rc.2",
58
- "@deepseek-ai/dsh-home-paths": "^0.1.1-rc.2",
59
- "@deepseek-ai/dsh-host-webserver": "^0.1.1-rc.2",
60
- "@deepseek-ai/dsh-system-prompt": "^0.1.1-rc.2",
61
- "@deepseek-ai/dsh-tools": "^0.1.1-rc.2",
62
- "@deepseek-ai/dsh-workspace": "^0.1.1-rc.2",
63
- "@deepseek-ai/schemastery": "^3.18.1",
64
- "@types/node": "^22.20.1",
65
- "@types/react": "~18.3.1",
66
- "@types/react-dom": "^18.3.7",
67
- "jsdom": "^25.0.1",
68
- "puppeteer-core": "^25.8.0",
69
- "react": "^18.3.1",
70
- "react-dom": "^18.3.1",
71
- "tsdown": "0.22.2",
72
- "typescript": "~5.7.2",
73
- "vitest": "^3.0.0"
74
- }
75
- }
1
+ {
2
+ "name": "dsh-taskboard",
3
+ "description": "Agent-first task board for the DSH web GUI: host-authoritative task ledger with taskboard_* agent tools, project (= workspace) claim boundaries, per-task model execution in fresh sessions, optional per-task git-worktree isolation (dedicated task branches, commit evidence, one-click merge), host-side cron scheduling, and a live SSE kanban view. Mounts via the official dsh plugin system — no DSH source changes.",
4
+ "version": "0.6.1",
5
+ "type": "module",
6
+ "main": "lib/index.js",
7
+ "exports": {
8
+ ".": "./lib/index.js",
9
+ "./invariant": "./lib/invariant.js",
10
+ "./client": "./lib/client.js",
11
+ "./package.json": "./package.json"
12
+ },
13
+ "dsh": {
14
+ "bundle": {
15
+ "patch": "./cordis.patch.yml"
16
+ },
17
+ "client": {
18
+ "inject": [],
19
+ "platform": "web"
20
+ }
21
+ },
22
+ "files": [
23
+ "lib/**/*.js",
24
+ "lib/**/*.js.map",
25
+ "src",
26
+ "cordis.patch.yml",
27
+ "LICENSE"
28
+ ],
29
+ "license": "Apache-2.0",
30
+ "author": "cloader",
31
+ "repository": {
32
+ "type": "git",
33
+ "url": "git+https://github.com/cloader/dsh-taskboard.git"
34
+ },
35
+ "bugs": {
36
+ "url": "https://github.com/cloader/dsh-taskboard/issues"
37
+ },
38
+ "homepage": "https://github.com/cloader/dsh-taskboard#readme",
39
+ "keywords": [
40
+ "dsh",
41
+ "deepseek-harness",
42
+ "dsh-plugin",
43
+ "taskboard",
44
+ "kanban",
45
+ "agent"
46
+ ],
47
+ "scripts": {
48
+ "build": "node scripts/clean-lib.mjs && npm run build:host && npm run build:client",
49
+ "build:host": "tsdown -c tsdown.host.config.ts",
50
+ "build:client": "tsdown -c tsdown.client.config.ts && node scripts/wrap-client.mjs",
51
+ "watch": "tsdown -c tsdown.host.config.ts --watch",
52
+ "typecheck": "tsc --noEmit",
53
+ "test": "vitest run"
54
+ },
55
+ "devDependencies": {
56
+ "@deepseek-ai/cordis": "^4.0.1",
57
+ "@deepseek-ai/dsh-agent": "^0.1.1-rc.2",
58
+ "@deepseek-ai/dsh-home-paths": "^0.1.1-rc.2",
59
+ "@deepseek-ai/dsh-host-webserver": "^0.1.1-rc.2",
60
+ "@deepseek-ai/dsh-system-prompt": "^0.1.1-rc.2",
61
+ "@deepseek-ai/dsh-tools": "^0.1.1-rc.2",
62
+ "@deepseek-ai/dsh-workspace": "^0.1.1-rc.2",
63
+ "@deepseek-ai/schemastery": "^3.18.1",
64
+ "@types/node": "^22.20.1",
65
+ "@types/react": "~18.3.1",
66
+ "@types/react-dom": "^18.3.7",
67
+ "jsdom": "^25.0.1",
68
+ "puppeteer-core": "^25.8.0",
69
+ "react": "^18.3.1",
70
+ "react-dom": "^18.3.1",
71
+ "tsdown": "0.22.2",
72
+ "typescript": "~5.7.2",
73
+ "vitest": "^3.0.0"
74
+ }
75
+ }
package/src/client/api.ts CHANGED
@@ -15,7 +15,9 @@ import type {
15
15
  ImportCommitResponse,
16
16
  ImportPreviewResponse,
17
17
  MergeBranchResponse,
18
+ ModelCatalogResponse,
18
19
  MoveTaskBody,
20
+ PromptCompletionsResponse,
19
21
  RejectTaskBody,
20
22
  RunTaskBody,
21
23
  SettingsResponse,
@@ -96,6 +98,10 @@ export interface TaskboardClient {
96
98
  settings(): Promise<SettingsResponse>
97
99
  /** Replace board settings (whole-object semantics; affects new tasks only). */
98
100
  updateSettings(body: UpdateSettingsBody): Promise<SettingsResponse>
101
+ /** Prompt completions for skills and slash commands (0.5.5). */
102
+ promptCompletions(): Promise<PromptCompletionsResponse>
103
+ /** Model catalog and agent preset roster (0.5.5). */
104
+ modelCatalog(): Promise<ModelCatalogResponse>
99
105
  /** Subscribe to change frames; the disposer stops the stream. */
100
106
  stream(onChange: (event: ChangeEvent) => void, onGap: () => void): () => void
101
107
  }
@@ -131,6 +137,8 @@ export function createClient(): TaskboardClient {
131
137
  templateDelete: id => post('/dsh-taskboard/templates/delete', { id }),
132
138
  settings: () => get<SettingsResponse>('/dsh-taskboard/settings'),
133
139
  updateSettings: body => post('/dsh-taskboard/settings/update', body),
140
+ promptCompletions: () => get<PromptCompletionsResponse>('/dsh-taskboard/prompt-completions'),
141
+ modelCatalog: () => get<ModelCatalogResponse>('/dsh-taskboard/model-catalog'),
134
142
  stream(onChange, onGap) {
135
143
  const es = new EventSource('/dsh-taskboard/events')
136
144
  let revision: number | undefined
@@ -5,6 +5,7 @@
5
5
  * @module dsh-taskboard/client/board/AlertModal
6
6
  */
7
7
  import { useState, useEffect } from 'react'
8
+ import { useT } from '../i18n/runtime.ts'
8
9
 
9
10
  /** Show a non-blocking alert modal. Returns true when opened. */
10
11
  export function useAlert(): { alert: (msg: string) => void; el: React.ReactNode } {
@@ -18,6 +19,7 @@ export function useAlert(): { alert: (msg: string) => void; el: React.ReactNode
18
19
  }
19
20
 
20
21
  function AlertModal({ message, onClose }: { message: string; onClose: () => void }) {
22
+ const t = useT()
21
23
  useEffect(() => {
22
24
  const handler = (e: KeyboardEvent) => { if (e.key === 'Escape') onClose() }
23
25
  window.addEventListener('keydown', handler)
@@ -29,7 +31,7 @@ function AlertModal({ message, onClose }: { message: string; onClose: () => void
29
31
  <div className="dsh-atb-alert" onClick={e => e.stopPropagation()}>
30
32
  <div className="dsh-atb-alert-icon">⛔</div>
31
33
  <div className="dsh-atb-alert-msg">{message}</div>
32
- <button type="button" className="dsh-atb-btn" data-primary="true" onClick={onClose}>知道了</button>
34
+ <button type="button" className="dsh-atb-btn" data-primary="true" onClick={onClose}>{t('alert.ok')}</button>
33
35
  </div>
34
36
  </div>
35
37
  )
@@ -10,6 +10,7 @@ import { useRef, useState } from 'react'
10
10
  import type { BoardController } from '../controller.ts'
11
11
  import type { ImportPreviewResponse } from '../../shared/api.ts'
12
12
  import { useAlert } from './AlertModal.tsx'
13
+ import { useT } from '../i18n/runtime.ts'
13
14
 
14
15
  /** One classified row (create / overwrite). */
15
16
  function PlanRow({ row }: { row: { id: string; title: string; status: string } }) {
@@ -26,6 +27,7 @@ function PlanRow({ row }: { row: { id: string; title: string; status: string } }
26
27
  * @param controller - the controller.
27
28
  */
28
29
  export function ImportModal({ controller }: { controller: BoardController }) {
30
+ const t = useT()
29
31
  const [fileName, setFileName] = useState('')
30
32
  const [parsed, setParsed] = useState<unknown>(null)
31
33
  const [parseError, setParseError] = useState<string | undefined>(undefined)
@@ -55,7 +57,7 @@ export function ImportModal({ controller }: { controller: BoardController }) {
55
57
  if (p !== undefined) setPlan(p)
56
58
  })
57
59
  } catch {
58
- setParseError('文件不是合法 JSON')
60
+ setParseError(t('imp.parseError'))
59
61
  }
60
62
  })
61
63
  }
@@ -73,8 +75,8 @@ export function ImportModal({ controller }: { controller: BoardController }) {
73
75
  setConfirmReplace(false)
74
76
  if (r === undefined) return
75
77
  setResult(r.mode === 'replace'
76
- ? `整册替换完成:导入 ${r.created + r.overwritten} 张(原 ${r.replacedTotal} 张已整册备份)`
77
- : `合并完成:新增 ${r.created} 张、覆盖 ${r.overwritten} 张`)
78
+ ? t('imp.result.replace', { n: r.created + r.overwritten, total: r.replacedTotal ?? 0 })
79
+ : t('imp.result.merge', { n: r.created, m: r.overwritten }))
78
80
  })
79
81
  }
80
82
 
@@ -82,14 +84,14 @@ export function ImportModal({ controller }: { controller: BoardController }) {
82
84
 
83
85
  return (
84
86
  <div className="dsh-atb-modal-backdrop" onClick={e => { if (e.target === e.currentTarget) close() }}>
85
- <div className="dsh-atb-modal dsh-atb-imp" role="dialog" aria-modal="true" aria-label="导入台账">
87
+ <div className="dsh-atb-modal dsh-atb-imp" role="dialog" aria-modal="true" aria-label={t('imp.aria')}>
86
88
  <div className="dsh-atb-modal-head">
87
89
  <span className="dsh-atb-modal-headicon">⬆</span>
88
90
  <div className="dsh-atb-modal-headtext">
89
- <h3>导入台账</h3>
90
- <p>选择导出的 JSON 备份文件:先预览、再合并或整册替换</p>
91
+ <h3>{t('imp.title')}</h3>
92
+ <p>{t('imp.subtitle')}</p>
91
93
  </div>
92
- <button type="button" className="dsh-atb-modal-close" aria-label="关闭" onClick={close}>✕</button>
94
+ <button type="button" className="dsh-atb-modal-close" aria-label={t('shared.close')} onClick={close}>✕</button>
93
95
  </div>
94
96
  <div className="dsh-atb-modal-body">
95
97
  <div className="dsh-atb-imp-picker">
@@ -101,38 +103,38 @@ export function ImportModal({ controller }: { controller: BoardController }) {
101
103
  />
102
104
  {fileName.length > 0 && <span className="dsh-atb-imp-filename">{fileName}</span>}
103
105
  </div>
104
- <div className="dsh-atb-imp-note">⬇ JSON 导出的文件即为同格式备份,可直接导入恢复;导入文件的 schemaVersion 必须与当前版本一致。</div>
106
+ <div className="dsh-atb-imp-note">{t('imp.note')}</div>
105
107
 
106
108
  {parseError !== undefined && <div className="dsh-atb-imp-error">{parseError}</div>}
107
- {plan === undefined && parseError === undefined && fileName.length > 0 && <div className="dsh-atb-empty2">预览中…</div>}
109
+ {plan === undefined && parseError === undefined && fileName.length > 0 && <div className="dsh-atb-empty2">{t('imp.previewing')}</div>}
108
110
 
109
111
  {plan !== undefined && (
110
112
  <>
111
113
  <div className="dsh-atb-imp-stats">
112
- <div className="dsh-atb-imp-stat" data-tone="ok"><b>{plan.create.length}</b><span>新增</span></div>
113
- <div className="dsh-atb-imp-stat" data-tone="warn"><b>{plan.overwrite.length}</b><span>覆盖(同 id)</span></div>
114
- <div className="dsh-atb-imp-stat" data-tone={plan.invalid.length > 0 ? 'bad' : undefined}><b>{plan.invalid.length}</b><span>无效(跳过)</span></div>
114
+ <div className="dsh-atb-imp-stat" data-tone="ok"><b>{plan.create.length}</b><span>{t('imp.stat.create')}</span></div>
115
+ <div className="dsh-atb-imp-stat" data-tone="warn"><b>{plan.overwrite.length}</b><span>{t('imp.stat.overwrite')}</span></div>
116
+ <div className="dsh-atb-imp-stat" data-tone={plan.invalid.length > 0 ? 'bad' : undefined}><b>{plan.invalid.length}</b><span>{t('imp.stat.invalid')}</span></div>
115
117
  </div>
116
118
 
117
119
  {plan.create.length > 0 && (
118
120
  <div className="dsh-atb-imp-sec">
119
- <h4>新增任务</h4>
121
+ <h4>{t('imp.sec.create')}</h4>
120
122
  <div className="dsh-atb-imp-list">{plan.create.map(r => <PlanRow key={r.id} row={r} />)}</div>
121
123
  </div>
122
124
  )}
123
125
  {plan.overwrite.length > 0 && (
124
126
  <div className="dsh-atb-imp-sec">
125
- <h4>覆盖任务(整卡替换,含执行历史与评论)</h4>
127
+ <h4>{t('imp.sec.overwrite')}</h4>
126
128
  <div className="dsh-atb-imp-list">{plan.overwrite.map(r => <PlanRow key={r.id} row={r} />)}</div>
127
129
  </div>
128
130
  )}
129
131
  {plan.invalid.length > 0 && (
130
132
  <div className="dsh-atb-imp-sec">
131
- <h4>无效条目(不会导入)</h4>
133
+ <h4>{t('imp.sec.invalid')}</h4>
132
134
  <div className="dsh-atb-imp-list">
133
135
  {plan.invalid.map((r, i) => (
134
136
  <div key={r.id ?? `invalid-${i}`} className="dsh-atb-imp-row" data-tone="bad" title={r.id ?? ''}>
135
- <span className="dsh-atb-imp-row-title">{r.id ?? '(无 id)'}</span>
137
+ <span className="dsh-atb-imp-row-title">{r.id ?? t('imp.noId')}</span>
136
138
  <span className="dsh-atb-imp-row-status">{r.reason}</span>
137
139
  </div>
138
140
  ))}
@@ -142,12 +144,12 @@ export function ImportModal({ controller }: { controller: BoardController }) {
142
144
 
143
145
  <div className="dsh-atb-mode-picker">
144
146
  <button type="button" className="dsh-atb-mode-opt" data-on={mode === 'merge'} onClick={() => { setMode('merge'); setConfirmReplace(false) }}>
145
- <span className="dsh-atb-mode-name">⊕ 合并</span>
146
- <span className="dsh-atb-mode-hint">新增 + 按 id 覆盖,其余不动</span>
147
+ <span className="dsh-atb-mode-name">{t('imp.mode.merge')}</span>
148
+ <span className="dsh-atb-mode-hint">{t('imp.mode.mergeHint')}</span>
147
149
  </button>
148
150
  <button type="button" className="dsh-atb-mode-opt" data-on={mode === 'replace'} onClick={() => setMode('replace')}>
149
- <span className="dsh-atb-mode-name">💣 整册替换</span>
150
- <span className="dsh-atb-mode-hint">清空当前台账,以导入文件为准(先自动备份)</span>
151
+ <span className="dsh-atb-mode-name">{t('imp.mode.replace')}</span>
152
+ <span className="dsh-atb-mode-hint">{t('imp.mode.replaceHint')}</span>
151
153
  </button>
152
154
  </div>
153
155
 
@@ -158,11 +160,11 @@ export function ImportModal({ controller }: { controller: BoardController }) {
158
160
  <div className="dsh-atb-modal-foot">
159
161
  <span className="dsh-atb-modal-hint">
160
162
  {mode === 'replace'
161
- ? confirmReplace ? '⚠ 再次点击确认执行整册替换(不可撤销,已自动备份)' : '整册替换需要二次确认'
162
- : '合并只写入预览中列出的任务'}
163
+ ? confirmReplace ? t('imp.foot.replaceConfirm') : t('imp.foot.replaceNeedConfirm')
164
+ : t('imp.foot.mergeHint')}
163
165
  </span>
164
166
  <span className="dsh-atb-modal-footbtns">
165
- <button type="button" className="dsh-atb-btn" onClick={close}>{result !== undefined ? '关闭' : '取消'}</button>
167
+ <button type="button" className="dsh-atb-btn" onClick={close}>{result !== undefined ? t('shared.close') : t('shared.cancel')}</button>
166
168
  <button
167
169
  type="button"
168
170
  className="dsh-atb-btn"
@@ -171,7 +173,7 @@ export function ImportModal({ controller }: { controller: BoardController }) {
171
173
  disabled={plan === undefined || busy}
172
174
  onClick={commit}
173
175
  >
174
- {mode === 'replace' && confirmReplace ? '确认整册替换' : '执行导入'}
176
+ {mode === 'replace' && confirmReplace ? t('imp.action.confirmReplace') : t('imp.action.run')}
175
177
  </button>
176
178
  </span>
177
179
  </div>
@@ -9,12 +9,13 @@
9
9
  */
10
10
  import { useState } from 'react'
11
11
  import type { BoardController } from '../controller.ts'
12
- import { DEFAULT_ISOLATION, defaultSyncExternalSessionsOf, type IsolationMode } from '../../shared/protocol.ts'
12
+ import { DEFAULT_ISOLATION, defaultPermissionOf, defaultSyncExternalSessionsOf, type IsolationMode, type PermissionMode } from '../../shared/protocol.ts'
13
+ import { useT, type Translate } from '../i18n/runtime.ts'
13
14
 
14
- /** The isolation options with one-line hints (mirrors the task form). */
15
- const ISOLATION_OPTIONS: ReadonlyArray<{ value: IsolationMode; name: string; hint: string }> = [
16
- { value: 'none', name: '📁 原目录执行', hint: '不使用 git,直接在项目目录工作(出厂默认)' },
17
- { value: 'worktree', name: '🌿 Worktree 隔离', hint: '每次执行在独立 worktree 分支上进行(task/标题+ID),互不污染' },
15
+ /** The isolation options with one-line hints (mirrors the task form; translated per render). */
16
+ const isolationOptions = (t: Translate): ReadonlyArray<{ value: IsolationMode; name: string; hint: string }> => [
17
+ { value: 'none', name: t('form.iso.none'), hint: t('set.iso.noneHint') },
18
+ { value: 'worktree', name: t('form.iso.worktree'), hint: t('set.iso.worktreeHint') },
18
19
  ]
19
20
 
20
21
  /**
@@ -23,17 +24,21 @@ const ISOLATION_OPTIONS: ReadonlyArray<{ value: IsolationMode; name: string; hin
23
24
  * @param controller - the board controller.
24
25
  */
25
26
  export function SettingsModal({ controller }: { controller: BoardController }) {
27
+ const t = useT()
26
28
  const state = controller.getSnapshot()
27
29
  const currentIso = state.ledger.settings?.defaultIsolation ?? DEFAULT_ISOLATION
28
30
  const currentSync = defaultSyncExternalSessionsOf(state.ledger.settings)
31
+ const currentPerm = defaultPermissionOf(state.ledger.settings)
29
32
  const [draftIso, setDraftIso] = useState<IsolationMode>(currentIso)
30
33
  const [draftSync, setDraftSync] = useState<boolean>(currentSync)
31
- const dirty = draftIso !== currentIso || draftSync !== currentSync
34
+ const [draftPerm, setDraftPerm] = useState<PermissionMode>(currentPerm)
35
+ const dirty = draftIso !== currentIso || draftSync !== currentSync || draftPerm !== currentPerm
32
36
 
33
37
  const save = (): void => {
34
38
  void controller.updateSettings({
35
39
  defaultIsolation: draftIso,
36
40
  syncExternalSessions: draftSync,
41
+ defaultPermission: draftPerm,
37
42
  }).then(ok => {
38
43
  if (ok) controller.closeSettings()
39
44
  })
@@ -41,21 +46,21 @@ export function SettingsModal({ controller }: { controller: BoardController }) {
41
46
 
42
47
  return (
43
48
  <div className="dsh-atb-modal-backdrop" onClick={e => { if (e.target === e.currentTarget) controller.closeSettings() }}>
44
- <div className="dsh-atb-modal dsh-atb-set" role="dialog" aria-modal="true" aria-label="看板设置">
49
+ <div className="dsh-atb-modal dsh-atb-set" role="dialog" aria-modal="true" aria-label={t('set.aria')}>
45
50
  <div className="dsh-atb-modal-head">
46
51
  <span className="dsh-atb-modal-headicon">🛠</span>
47
52
  <div className="dsh-atb-modal-headtext">
48
- <h3>看板设置</h3>
49
- <p>新建任务与会话同步的全局默认值</p>
53
+ <h3>{t('set.title')}</h3>
54
+ <p>{t('set.subtitle')}</p>
50
55
  </div>
51
- <button type="button" className="dsh-atb-modal-close" aria-label="关闭" onClick={() => controller.closeSettings()}>✕</button>
56
+ <button type="button" className="dsh-atb-modal-close" aria-label={t('shared.close')} onClick={() => controller.closeSettings()}>✕</button>
52
57
  </div>
53
58
 
54
59
  <div className="dsh-atb-modal-body">
55
60
  <section className="dsh-atb-diag-sec">
56
- <h4>默认执行隔离</h4>
61
+ <h4>{t('set.iso.heading')}</h4>
57
62
  <div className="dsh-atb-mode-picker">
58
- {ISOLATION_OPTIONS.map(o => (
63
+ {isolationOptions(t).map(o => (
59
64
  <button
60
65
  key={o.value}
61
66
  type="button"
@@ -70,48 +75,83 @@ export function SettingsModal({ controller }: { controller: BoardController }) {
70
75
  ))}
71
76
  </div>
72
77
  <span className="dsh-atb-isolation-note">
73
- 当前保存的默认:{currentIso === 'worktree' ? '🌿 Worktree 隔离' : '📁 原目录执行'}
74
- 仅影响之后新建的任务;已有任务保持创建时的选择,非 git 项目运行时仍自动降级原目录。
78
+ {t('set.iso.current', { current: currentIso === 'worktree' ? t('form.iso.worktree') : t('form.iso.none') })}
75
79
  </span>
76
80
  </section>
77
81
 
78
82
  <section className="dsh-atb-diag-sec">
79
- <h4>自动同步工作区会话</h4>
83
+ <h4>{t('set.sync.heading')}</h4>
80
84
  <div className="dsh-atb-mode-picker">
81
85
  <button
82
86
  type="button"
83
87
  className="dsh-atb-mode-opt"
84
88
  data-on={!draftSync}
85
- title="仅管理在任务看板中创建和触发的任务"
89
+ title={t('set.sync.off.title')}
86
90
  onClick={() => setDraftSync(false)}
87
91
  >
88
- <span className="dsh-atb-mode-name">🚫 关闭同步</span>
89
- <span className="dsh-atb-mode-hint">仅管理看板任务(出厂默认)</span>
92
+ <span className="dsh-atb-mode-name">{t('set.sync.off.name')}</span>
93
+ <span className="dsh-atb-mode-hint">{t('set.sync.off.hint')}</span>
90
94
  </button>
91
95
  <button
92
96
  type="button"
93
97
  className="dsh-atb-mode-opt"
94
98
  data-on={draftSync}
95
- title="各工作区直接新建的会话也会在看板展示,运行中进入「进行中」,完成后自动进入「待验收」"
99
+ title={t('set.sync.on.title')}
96
100
  onClick={() => setDraftSync(true)}
97
101
  >
98
- <span className="dsh-atb-mode-name">🔄 自动纳入会话</span>
99
- <span className="dsh-atb-mode-hint">跟踪运行并在完成后进入待验收</span>
102
+ <span className="dsh-atb-mode-name">{t('set.sync.on.name')}</span>
103
+ <span className="dsh-atb-mode-hint">{t('set.sync.on.hint')}</span>
100
104
  </button>
101
105
  </div>
102
106
  <span className="dsh-atb-isolation-note">
103
107
  {currentSync
104
- ? '已开启:工作区直接新建并执行的会话将自动在看板生成任务卡片,并在完成后流转至「待验收」列。'
105
- : '已关闭:仅在看板内部创建与触发执行的任务会出现在看板上。'}
108
+ ? t('set.sync.stateOn')
109
+ : t('set.sync.stateOff')}
110
+ </span>
111
+ </section>
112
+
113
+ <section className="dsh-atb-diag-sec">
114
+ <h4>{t('set.perm.heading')}</h4>
115
+ <div className="dsh-atb-perm-picker">
116
+ <button
117
+ type="button"
118
+ className="dsh-atb-perm-opt"
119
+ data-on={draftPerm === 'workspace-write'}
120
+ onClick={() => setDraftPerm('workspace-write')}
121
+ >
122
+ <span className="dsh-atb-perm-name">{t('set.perm.writeName')}</span>
123
+ <span className="dsh-atb-perm-hint">{t('set.perm.writeHint')}</span>
124
+ </button>
125
+ <button
126
+ type="button"
127
+ className="dsh-atb-perm-opt"
128
+ data-on={draftPerm === 'read-only'}
129
+ onClick={() => setDraftPerm('read-only')}
130
+ >
131
+ <span className="dsh-atb-perm-name">{t('set.perm.readOnlyName')}</span>
132
+ <span className="dsh-atb-perm-hint">{t('set.perm.readOnlyHint')}</span>
133
+ </button>
134
+ <button
135
+ type="button"
136
+ className="dsh-atb-perm-opt"
137
+ data-on={draftPerm === 'danger-full-access'}
138
+ onClick={() => setDraftPerm('danger-full-access')}
139
+ >
140
+ <span className="dsh-atb-perm-name">{t('set.perm.fullName')}</span>
141
+ <span className="dsh-atb-perm-hint">{t('set.perm.fullHint')}</span>
142
+ </button>
143
+ </div>
144
+ <span className="dsh-atb-isolation-note">
145
+ {t('set.perm.current', { current: currentPerm === 'read-only' ? t('set.perm.readOnlyName') : currentPerm === 'danger-full-access' ? t('set.perm.fullName') : t('set.perm.writeName') })}
106
146
  </span>
107
147
  </section>
108
148
  </div>
109
149
 
110
150
  <div className="dsh-atb-modal-foot">
111
- <span className="dsh-atb-modal-hint">{dirty ? '有未保存的修改' : '与看板当前设置一致'}</span>
151
+ <span className="dsh-atb-modal-hint">{dirty ? t('set.foot.dirty') : t('set.foot.clean')}</span>
112
152
  <span className="dsh-atb-modal-footbtns">
113
- <button type="button" className="dsh-atb-btn" onClick={() => controller.closeSettings()}>取消</button>
114
- <button type="button" className="dsh-atb-btn" data-primary="true" disabled={!dirty} onClick={save}>保存设置</button>
153
+ <button type="button" className="dsh-atb-btn" onClick={() => controller.closeSettings()}>{t('shared.cancel')}</button>
154
+ <button type="button" className="dsh-atb-btn" data-primary="true" disabled={!dirty} onClick={save}>{t('set.action.save')}</button>
115
155
  </span>
116
156
  </div>
117
157
  </div>