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.
- package/README.md +26 -2
- package/lib/client.js +2663 -767
- package/lib/host/execution.js +3 -0
- package/lib/host/execution.js.map +1 -1
- package/lib/host/routes.js +31 -1
- package/lib/host/routes.js.map +1 -1
- package/lib/host/session-sync.js +210 -10
- package/lib/host/session-sync.js.map +1 -1
- package/lib/host/store.js +9 -2
- package/lib/host/store.js.map +1 -1
- package/lib/index.js +100 -1
- package/lib/index.js.map +1 -1
- package/lib/shared/api.js.map +1 -1
- package/lib/shared/protocol.js +19 -1
- package/lib/shared/protocol.js.map +1 -1
- package/package.json +75 -75
- package/src/client/api.ts +8 -0
- package/src/client/board/AlertModal.tsx +3 -1
- package/src/client/board/ImportModal.tsx +26 -24
- package/src/client/board/SettingsModal.tsx +66 -26
- package/src/client/board/SlashPromptInput.tsx +340 -0
- package/src/client/board/TaskBoard.tsx +53 -49
- package/src/client/board/TaskCard.tsx +33 -21
- package/src/client/board/TaskDetail.tsx +169 -104
- package/src/client/board/TaskFormModal.tsx +254 -202
- package/src/client/board/TemplateManager.tsx +32 -29
- package/src/client/board/labels.ts +36 -27
- package/src/client/controller.ts +62 -2
- package/src/client/i18n/en.ts +455 -0
- package/src/client/i18n/runtime.ts +155 -0
- package/src/client/i18n/zh.ts +460 -0
- package/src/client/index.ts +182 -42
- package/src/client/sidebar-entry.ts +13 -3
- package/src/client/styles.ts +133 -0
- package/src/host/execution.ts +13 -0
- package/src/host/routes.ts +49 -1
- package/src/host/session-sync.ts +334 -14
- package/src/host/store.ts +15 -1
- package/src/index.ts +115 -1
- package/src/shared/api.ts +47 -0
- package/src/shared/protocol.ts +41 -0
- 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
|
-
"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}
|
|
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('
|
|
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
|
-
?
|
|
77
|
-
:
|
|
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
|
|
90
|
-
<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=
|
|
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"
|
|
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"
|
|
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
|
|
113
|
-
<div className="dsh-atb-imp-stat" data-tone="warn"><b>{plan.overwrite.length}</b><span
|
|
114
|
-
<div className="dsh-atb-imp-stat" data-tone={plan.invalid.length > 0 ? 'bad' : undefined}><b>{plan.invalid.length}</b><span
|
|
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
|
|
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
|
|
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
|
|
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 ?? '
|
|
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"
|
|
146
|
-
<span className="dsh-atb-mode-hint"
|
|
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"
|
|
150
|
-
<span className="dsh-atb-mode-hint"
|
|
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 ? '
|
|
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
|
|
16
|
-
{ value: 'none', name: '
|
|
17
|
-
{ value: 'worktree', name: '
|
|
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
|
|
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
|
|
49
|
-
<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=
|
|
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
|
|
61
|
+
<h4>{t('set.iso.heading')}</h4>
|
|
57
62
|
<div className="dsh-atb-mode-picker">
|
|
58
|
-
{
|
|
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
|
-
|
|
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
|
|
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"
|
|
89
|
-
<span className="dsh-atb-mode-hint"
|
|
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"
|
|
99
|
-
<span className="dsh-atb-mode-hint"
|
|
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 ? '
|
|
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()}
|
|
114
|
-
<button type="button" className="dsh-atb-btn" data-primary="true" disabled={!dirty} onClick={save}
|
|
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>
|