dsh-taskboard 0.6.6 → 0.7.0
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 +305 -288
- package/lib/client.js +985 -931
- package/lib/host/archive-sessions.js +30 -0
- package/lib/host/archive-sessions.js.map +1 -0
- package/lib/host/assets.js +139 -0
- package/lib/host/assets.js.map +1 -0
- package/lib/host/execution.js +3 -0
- package/lib/host/execution.js.map +1 -1
- package/lib/host/locale.js +17 -0
- package/lib/host/locale.js.map +1 -0
- package/lib/host/routes.js +132 -6
- package/lib/host/routes.js.map +1 -1
- package/lib/host/scheduler.js +2 -0
- package/lib/host/scheduler.js.map +1 -1
- package/lib/host/session-sync.js +4 -1
- package/lib/host/session-sync.js.map +1 -1
- package/lib/host/storage-queue.js +14 -0
- package/lib/host/storage-queue.js.map +1 -0
- package/lib/host/storage.js +249 -0
- package/lib/host/storage.js.map +1 -0
- package/lib/host/store.js +34 -7
- package/lib/host/store.js.map +1 -1
- package/lib/host/templates.js +73 -113
- package/lib/host/templates.js.map +1 -1
- package/lib/host/tools.js +23 -8
- package/lib/host/tools.js.map +1 -1
- package/lib/index.js +83 -27
- package/lib/index.js.map +1 -1
- package/lib/shared/api.js.map +1 -1
- package/lib/shared/builtin-templates.js +155 -0
- package/lib/shared/builtin-templates.js.map +1 -0
- package/lib/shared/protocol.js +39 -2
- package/lib/shared/protocol.js.map +1 -1
- package/package.json +90 -89
- package/src/client/api.ts +35 -1
- package/src/client/board/SettingsModal.tsx +67 -4
- package/src/client/board/SlashPromptInput.tsx +80 -1
- package/src/client/board/TaskBoard.tsx +16 -11
- package/src/client/board/TaskDetail.tsx +186 -17
- package/src/client/board/TaskFormModal.tsx +8 -5
- package/src/client/board/TemplateManager.tsx +22 -10
- package/src/client/controller.ts +67 -5
- package/src/client/i18n/en.ts +35 -3
- package/src/client/i18n/templates.ts +25 -0
- package/src/client/i18n/zh.ts +35 -3
- package/src/client/image-insert.ts +29 -0
- package/src/client/styles.ts +35 -2
- package/src/host/archive-sessions.ts +18 -0
- package/src/host/assets.ts +120 -0
- package/src/host/execution.ts +4 -1
- package/src/host/locale.ts +44 -0
- package/src/host/routes.ts +132 -7
- package/src/host/scheduler.ts +2 -0
- package/src/host/session-sync.ts +4 -1
- package/src/host/storage-queue.ts +10 -0
- package/src/host/storage.ts +212 -0
- package/src/host/store.ts +40 -12
- package/src/host/templates.ts +38 -66
- package/src/host/tools.ts +28 -11
- package/src/index.ts +88 -33
- package/src/shared/api.ts +32 -3
- package/src/shared/builtin-templates.ts +153 -0
- package/src/shared/protocol.ts +64 -0
- package/src/shared/version.ts +9 -9
|
@@ -212,6 +212,7 @@ export function TaskFormModal({ controller, task }: { controller: BoardControlle
|
|
|
212
212
|
// create/update/run round-trip is pending — a double click used to fire
|
|
213
213
|
// duplicate creates (and runs) before the first one returned (review P0).
|
|
214
214
|
const [busy, setBusy] = useState(false)
|
|
215
|
+
const [imageUploading, setImageUploading] = useState(false)
|
|
215
216
|
|
|
216
217
|
// Focus the title and close on Esc while the dialog is open.
|
|
217
218
|
useEffect(() => {
|
|
@@ -294,7 +295,7 @@ export function TaskFormModal({ controller, task }: { controller: BoardControlle
|
|
|
294
295
|
}
|
|
295
296
|
|
|
296
297
|
const submit = (): void => {
|
|
297
|
-
if (!valid || busy) return
|
|
298
|
+
if (!valid || busy || imageUploading) return
|
|
298
299
|
const picked = buildPickedModel()
|
|
299
300
|
if (!editing) saveLastModel(picked)
|
|
300
301
|
const isolationOut = isolationPayload()
|
|
@@ -335,7 +336,7 @@ export function TaskFormModal({ controller, task }: { controller: BoardControlle
|
|
|
335
336
|
|
|
336
337
|
/** Save the form, then immediately trigger a manual run of the task. */
|
|
337
338
|
const submitAndRun = (): void => {
|
|
338
|
-
if (!valid || runBlocked || busy) return
|
|
339
|
+
if (!valid || runBlocked || busy || imageUploading) return
|
|
339
340
|
const picked = buildPickedModel()
|
|
340
341
|
if (!editing) saveLastModel(picked)
|
|
341
342
|
const isolationOut = isolationPayload()
|
|
@@ -602,6 +603,8 @@ export function TaskFormModal({ controller, task }: { controller: BoardControlle
|
|
|
602
603
|
controller={controller}
|
|
603
604
|
rows={7}
|
|
604
605
|
placeholder={t('form.desc.placeholder')}
|
|
606
|
+
allowImages
|
|
607
|
+
onUploadingChange={setImageUploading}
|
|
605
608
|
/>
|
|
606
609
|
</Field>
|
|
607
610
|
|
|
@@ -624,13 +627,13 @@ export function TaskFormModal({ controller, task }: { controller: BoardControlle
|
|
|
624
627
|
<button
|
|
625
628
|
type="button"
|
|
626
629
|
className="dsh-atb-btn"
|
|
627
|
-
disabled={!valid || runBlocked || busy}
|
|
628
|
-
title={runBlocked ? t('form.action.runBlockedTitle') : busy ? t('form.action.runBusyTitle') : t('form.action.runTitle')}
|
|
630
|
+
disabled={!valid || runBlocked || busy || imageUploading}
|
|
631
|
+
title={runBlocked ? t('form.action.runBlockedTitle') : busy || imageUploading ? t('form.action.runBusyTitle') : t('form.action.runTitle')}
|
|
629
632
|
onClick={submitAndRun}
|
|
630
633
|
>
|
|
631
634
|
{t('form.action.run')}
|
|
632
635
|
</button>
|
|
633
|
-
<button type="button" className="dsh-atb-btn" data-primary="true" disabled={!valid || busy} onClick={submit}>
|
|
636
|
+
<button type="button" className="dsh-atb-btn" data-primary="true" disabled={!valid || busy || imageUploading} onClick={submit}>
|
|
634
637
|
{editing ? t('form.action.save') : t('form.action.create')}
|
|
635
638
|
</button>
|
|
636
639
|
</span>
|
|
@@ -7,8 +7,12 @@
|
|
|
7
7
|
*/
|
|
8
8
|
import { useState } from 'react'
|
|
9
9
|
import type { BoardController } from '../controller.ts'
|
|
10
|
+
import type { TaskTemplate } from '../../shared/api.ts'
|
|
11
|
+
import type { Urgency } from '../../shared/protocol.ts'
|
|
10
12
|
import { useAlert } from './AlertModal.tsx'
|
|
13
|
+
import { URGENCY_KEYS } from './labels.ts'
|
|
11
14
|
import { useT } from '../i18n/runtime.ts'
|
|
15
|
+
import { localizeBuiltinName, localizeBuiltinTask } from '../i18n/templates.ts'
|
|
12
16
|
|
|
13
17
|
/**
|
|
14
18
|
* The template manager modal.
|
|
@@ -23,12 +27,20 @@ export function TemplateManager({ controller }: { controller: BoardController })
|
|
|
23
27
|
|
|
24
28
|
const close = (): void => controller.closeTemplateManager()
|
|
25
29
|
|
|
26
|
-
|
|
30
|
+
/** The name a row currently shows: an in-flight rename edit, else the localized name. */
|
|
31
|
+
const nameOf = (tpl: TaskTemplate): string => edits[tpl.id] ?? localizeBuiltinName(tpl)
|
|
32
|
+
|
|
33
|
+
/** Localized urgency label for the meta line ('' when the template pins none). */
|
|
34
|
+
const urgencyLabel = (urgency: string | undefined): string => {
|
|
35
|
+
if (urgency === undefined) return ''
|
|
36
|
+
const key = URGENCY_KEYS[urgency as Urgency]
|
|
37
|
+
return key !== undefined ? ` · ${t(key)}` : ` · ${urgency}`
|
|
38
|
+
}
|
|
27
39
|
|
|
28
40
|
/** Save one template's rename. */
|
|
29
41
|
const save = (id: string, name: string): void => {
|
|
30
42
|
const template = state.templates.find(t => t.id === id)
|
|
31
|
-
if (template === undefined || name === template
|
|
43
|
+
if (template === undefined || name === localizeBuiltinName(template)) return
|
|
32
44
|
void controller.upsertTemplate({ id, name, task: template.task }).then(ok => {
|
|
33
45
|
if (ok) {
|
|
34
46
|
setEdits(prev => { const next = { ...prev }; delete next[id]; return next })
|
|
@@ -57,31 +69,31 @@ export function TemplateManager({ controller }: { controller: BoardController })
|
|
|
57
69
|
<div key={tpl.id} className="dsh-atb-tplm-row">
|
|
58
70
|
<input
|
|
59
71
|
className="dsh-atb-tplm-name"
|
|
60
|
-
value={nameOf(tpl
|
|
72
|
+
value={nameOf(tpl)}
|
|
61
73
|
maxLength={60}
|
|
62
74
|
spellCheck={false}
|
|
63
|
-
aria-label={t('tpl.name.aria', { name: tpl
|
|
75
|
+
aria-label={t('tpl.name.aria', { name: localizeBuiltinName(tpl) })}
|
|
64
76
|
onChange={e => setEdits(prev => ({ ...prev, [tpl.id]: e.target.value }))}
|
|
65
77
|
onKeyDown={e => {
|
|
66
|
-
if (e.key === 'Enter') save(tpl.id, nameOf(tpl
|
|
78
|
+
if (e.key === 'Enter') save(tpl.id, nameOf(tpl))
|
|
67
79
|
}}
|
|
68
80
|
/>
|
|
69
81
|
<span
|
|
70
82
|
className="dsh-atb-tplm-meta"
|
|
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
|
|
83
|
+
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 }) : ''}${urgencyLabel(tpl.task.urgency)}${tpl.task.permission !== undefined ? ` · ${t('shared.permission')}: ${tpl.task.permission}` : ''}`}
|
|
72
84
|
>
|
|
73
85
|
{tpl.builtin === true ? t('tpl.builtin') : t('tpl.custom')}
|
|
74
86
|
{tpl.task.checklist !== undefined && tpl.task.checklist.length > 0 ? t('tpl.meta.checklist', { n: tpl.task.checklist.length }) : ''}
|
|
75
|
-
{tpl.task.urgency
|
|
87
|
+
{urgencyLabel(tpl.task.urgency)}
|
|
76
88
|
{tpl.task.permission !== undefined && tpl.task.permission !== 'workspace-write' ? ` · ${tpl.task.permission === 'read-only' ? t('tpl.meta.permReadOnly') : t('tpl.meta.permFull')}` : ''}
|
|
77
89
|
</span>
|
|
78
90
|
<span className="dsh-atb-tplm-btns">
|
|
79
91
|
<button
|
|
80
92
|
type="button"
|
|
81
93
|
className="dsh-atb-btn"
|
|
82
|
-
disabled={nameOf(tpl
|
|
94
|
+
disabled={nameOf(tpl) === localizeBuiltinName(tpl) || nameOf(tpl).trim().length === 0}
|
|
83
95
|
title={t('tpl.rename.title')}
|
|
84
|
-
onClick={() => save(tpl.id, nameOf(tpl
|
|
96
|
+
onClick={() => save(tpl.id, nameOf(tpl))}
|
|
85
97
|
>
|
|
86
98
|
{t('tpl.rename.button')}
|
|
87
99
|
</button>
|
|
@@ -91,7 +103,7 @@ export function TemplateManager({ controller }: { controller: BoardController })
|
|
|
91
103
|
title={t('tpl.use.title')}
|
|
92
104
|
onClick={() => {
|
|
93
105
|
close()
|
|
94
|
-
controller.newFromTemplate(tpl
|
|
106
|
+
controller.newFromTemplate(localizeBuiltinTask(tpl))
|
|
95
107
|
}}
|
|
96
108
|
>
|
|
97
109
|
{t('tpl.use.button')}
|
package/src/client/controller.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { SessionArchiveResult } from '../shared/api.ts'
|
|
1
2
|
/**
|
|
2
3
|
* The board controller: framework-free state holder the React views render
|
|
3
4
|
* from. Owns the ledger snapshot, workspace listing, view state (open,
|
|
@@ -7,7 +8,7 @@
|
|
|
7
8
|
*
|
|
8
9
|
* @module dsh-taskboard/client/controller
|
|
9
10
|
*/
|
|
10
|
-
import type { ChangeEvent, DiagnosticsResponse, DiffResponse, ImportCommitResponse, ImportPreviewResponse, MergeRepoResult, PromptCompletionsResponse, TaskTemplate, TaskTemplateSpec, UpdateTaskBody, WorkspaceView } from '../shared/api.ts'
|
|
11
|
+
import type { AttachmentUpload, ChangeEvent, DiagnosticsResponse, DiffResponse, ImportCommitResponse, ImportPreviewResponse, MergeRepoResult, PromptCompletionsResponse, StorageStatus, TaskTemplate, TaskTemplateSpec, UpdateTaskBody, WorkspaceView } from '../shared/api.ts'
|
|
11
12
|
import type { ChecklistItem, TaskLedger, TaskRecord, Urgency } from '../shared/protocol.ts'
|
|
12
13
|
import { emptyLedger } from '../shared/protocol.ts'
|
|
13
14
|
import type { TaskboardClient } from './api.ts'
|
|
@@ -47,6 +48,8 @@ function loadView(): { workspaceId?: string; urgencies: Urgency[]; sortBy: SortB
|
|
|
47
48
|
|
|
48
49
|
/** Controller snapshot the views render. */
|
|
49
50
|
export interface ControllerState {
|
|
51
|
+
archiveSessionsSupported?: boolean
|
|
52
|
+
sessionArchive?: { taskId: string; result: SessionArchiveResult }
|
|
50
53
|
boardOpen: boolean
|
|
51
54
|
ledger: TaskLedger
|
|
52
55
|
workspaces: WorkspaceView[]
|
|
@@ -75,6 +78,8 @@ export interface ControllerState {
|
|
|
75
78
|
importOpen: boolean
|
|
76
79
|
/** Board-settings modal visible (0.5.0). */
|
|
77
80
|
settingsOpen: boolean
|
|
81
|
+
/** Current durable-data directory, loaded when settings opens. */
|
|
82
|
+
storage?: StorageStatus
|
|
78
83
|
/** Fields a chosen template prefills into the create form (consumed on open). */
|
|
79
84
|
templatePrefill?: TaskTemplateSpec
|
|
80
85
|
/** Transient error surface (action failures); cleared on next success. */
|
|
@@ -185,7 +190,7 @@ export class BoardController {
|
|
|
185
190
|
if (this.state.selectedId !== undefined) {
|
|
186
191
|
selected = ledger.tasks.find(t => t.id === this.state.selectedId)
|
|
187
192
|
}
|
|
188
|
-
this.setState({ ledger, workspaces, error: undefined, selectedId: selected === undefined ? undefined : this.state.selectedId })
|
|
193
|
+
this.setState({ archiveSessionsSupported: ledger.capabilities?.archiveSessions === true, ledger, workspaces, error: undefined, selectedId: selected === undefined ? undefined : this.state.selectedId })
|
|
189
194
|
if (this.seenRevision === undefined || ledger.revision >= this.seenRevision) break
|
|
190
195
|
}
|
|
191
196
|
} catch (error) {
|
|
@@ -422,10 +427,27 @@ export class BoardController {
|
|
|
422
427
|
}
|
|
423
428
|
}
|
|
424
429
|
|
|
430
|
+
/** Retry session archiving without repeating the task's terminal transition. */
|
|
431
|
+
async retryArchiveSessions(id: string): Promise<void> {
|
|
432
|
+
try {
|
|
433
|
+
if (this.client.archiveSessions === undefined) return
|
|
434
|
+
const result = await this.client.archiveSessions(id)
|
|
435
|
+
this.setState({ sessionArchive: { taskId: id, result } })
|
|
436
|
+
await this.refresh()
|
|
437
|
+
} catch (error) {
|
|
438
|
+
this.setState({ error: error instanceof Error ? error.message : String(error) })
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
|
|
425
442
|
/** Move a task (user surface: done allowed). */
|
|
426
|
-
async move(id: string, ifVersion: number, status: string): Promise<void> {
|
|
443
|
+
async move(id: string, ifVersion: number, status: string, options?: { archiveSessions?: boolean }): Promise<void> {
|
|
427
444
|
try {
|
|
428
|
-
await this.client.move(id, {
|
|
445
|
+
const result = await this.client.move(id, {
|
|
446
|
+
ifVersion,
|
|
447
|
+
status,
|
|
448
|
+
...(options?.archiveSessions !== undefined ? { archiveSessions: options.archiveSessions } : {}),
|
|
449
|
+
})
|
|
450
|
+
if (result.sessionArchive !== undefined) this.setState({ sessionArchive: { taskId: id, result: result.sessionArchive } })
|
|
429
451
|
await this.refresh()
|
|
430
452
|
} catch (error) {
|
|
431
453
|
this.setState({ error: error instanceof Error ? error.message : String(error) })
|
|
@@ -504,6 +526,16 @@ export class BoardController {
|
|
|
504
526
|
}
|
|
505
527
|
}
|
|
506
528
|
|
|
529
|
+
/** Upload an image without putting its bytes in the ledger or agent context. */
|
|
530
|
+
async uploadImage(file: Blob): Promise<AttachmentUpload | undefined> {
|
|
531
|
+
try {
|
|
532
|
+
return await this.client.uploadImage(file)
|
|
533
|
+
} catch (error) {
|
|
534
|
+
this.setState({ error: error instanceof Error ? error.message : String(error) })
|
|
535
|
+
return undefined
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
|
|
507
539
|
/** Trigger a manual run (fresh in-project session, pinned model); `reuse` = 续跑. */
|
|
508
540
|
async run(id: string, reuse = false): Promise<void> {
|
|
509
541
|
try {
|
|
@@ -567,7 +599,12 @@ export class BoardController {
|
|
|
567
599
|
closeDiagnostics(): void { this.setState({ diagOpen: false }) }
|
|
568
600
|
|
|
569
601
|
/** Open the board-settings modal (0.5.0). */
|
|
570
|
-
openSettings(): void {
|
|
602
|
+
openSettings(): void {
|
|
603
|
+
this.setState({ settingsOpen: true })
|
|
604
|
+
void this.client.storage()
|
|
605
|
+
.then(storage => this.setState({ storage, error: undefined }))
|
|
606
|
+
.catch(error => this.setState({ error: error instanceof Error ? error.message : String(error) }))
|
|
607
|
+
}
|
|
571
608
|
|
|
572
609
|
/** Close the board-settings modal. */
|
|
573
610
|
closeSettings(): void { this.setState({ settingsOpen: false }) }
|
|
@@ -588,6 +625,31 @@ export class BoardController {
|
|
|
588
625
|
}
|
|
589
626
|
}
|
|
590
627
|
|
|
628
|
+
/** Validate a candidate host directory without changing the active store. */
|
|
629
|
+
async checkStorage(directory: string): Promise<boolean> {
|
|
630
|
+
try {
|
|
631
|
+
const storage = await this.client.checkStorage(directory)
|
|
632
|
+
this.setState({ storage, error: undefined })
|
|
633
|
+
return true
|
|
634
|
+
} catch (error) {
|
|
635
|
+
this.setState({ error: error instanceof Error ? error.message : String(error) })
|
|
636
|
+
return false
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
/** Atomically migrate all three stores and refresh the displayed location. */
|
|
641
|
+
async migrateStorage(directory: string): Promise<boolean> {
|
|
642
|
+
try {
|
|
643
|
+
const storage = await this.client.migrateStorage(directory)
|
|
644
|
+
this.setState({ storage, error: storage.warnings.length === 0 ? undefined : storage.warnings.join('\n') })
|
|
645
|
+
await this.refresh()
|
|
646
|
+
return true
|
|
647
|
+
} catch (error) {
|
|
648
|
+
this.setState({ error: error instanceof Error ? error.message : String(error) })
|
|
649
|
+
return false
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
|
|
591
653
|
/** Clean one orphan worktree (⚙ panel); refreshes the diagnostics payload. */
|
|
592
654
|
async cleanupOrphan(workspaceId: string, taskId: string): Promise<void> {
|
|
593
655
|
try {
|
package/src/client/i18n/en.ts
CHANGED
|
@@ -213,6 +213,14 @@ export const en: TaskboardDict = {
|
|
|
213
213
|
'detail.move.to': 'Move to → {status}',
|
|
214
214
|
'detail.move.confirmDoneUnchecked': '{n} checklist items are still unchecked — confirm done?',
|
|
215
215
|
'detail.move.confirmDone': 'Confirm done?',
|
|
216
|
+
'detail.move.confirmArchive': 'Confirm archive this task?',
|
|
217
|
+
'detail.move.confirmArchiveSessionWithId': 'Archive execution session {id} together?',
|
|
218
|
+
'detail.move.confirmArchiveSessionCount': 'Archive {n} execution sessions together?',
|
|
219
|
+
'detail.move.archiveWithSession': 'With session',
|
|
220
|
+
'detail.move.archiveUnsupported': 'Session archiving is unavailable on this host',
|
|
221
|
+
'detail.move.archiveResult': 'Card archived; {n} execution sessions archived. Failed sessions, if any:',
|
|
222
|
+
'detail.move.archiveRetry': 'Archive / retry execution sessions',
|
|
223
|
+
'detail.move.archiveCardOnly': 'Card only',
|
|
216
224
|
'detail.move.confirm': 'Confirm',
|
|
217
225
|
'detail.blocked.unmark': '✓ Unblock',
|
|
218
226
|
'detail.blocked.mark': '⛔ Mark blocked',
|
|
@@ -331,14 +339,14 @@ export const en: TaskboardDict = {
|
|
|
331
339
|
'tpl.use.button': 'Use',
|
|
332
340
|
'tpl.delete.title': 'Delete this template',
|
|
333
341
|
'tpl.renamed': 'Template renamed',
|
|
334
|
-
'tpl.foot.hint': 'Templates are stored with the ledger in the
|
|
342
|
+
'tpl.foot.hint': 'Templates are stored with the ledger in the active data directory and survive upgrades',
|
|
335
343
|
|
|
336
344
|
// ── import modal (ImportModal) ────────────────────────────────────
|
|
337
345
|
'imp.aria': 'Import ledger',
|
|
338
346
|
'imp.title': 'Import ledger',
|
|
339
347
|
'imp.subtitle': 'Pick an exported JSON backup: preview first, then merge or replace everything',
|
|
340
348
|
'imp.parseError': 'The file is not valid JSON',
|
|
341
|
-
'imp.note': 'The ⬇ JSON export
|
|
349
|
+
'imp.note': 'The ⬇ JSON export restores the ledger. Back up the dsh-taskboard-assets folder in the data directory shown in Settings as well. The file\u2019s schemaVersion must match the current version.',
|
|
342
350
|
'imp.previewing': 'Previewing…',
|
|
343
351
|
'imp.stat.create': 'New',
|
|
344
352
|
'imp.stat.overwrite': 'Overwrite (same id)',
|
|
@@ -362,7 +370,7 @@ export const en: TaskboardDict = {
|
|
|
362
370
|
// ── board settings modal (SettingsModal) ──────────────────────────
|
|
363
371
|
'set.aria': 'Board settings',
|
|
364
372
|
'set.title': 'Board settings',
|
|
365
|
-
'set.subtitle': '
|
|
373
|
+
'set.subtitle': 'New-task defaults, session sync, and local data storage',
|
|
366
374
|
'set.iso.heading': 'Default execution isolation',
|
|
367
375
|
'set.iso.noneHint': 'No git; works directly in the project directory (factory default)',
|
|
368
376
|
'set.iso.worktreeHint': 'Each execution runs on its own worktree branch (task/title+ID), isolated from the others; multi-repo workspaces are mirrored whole (one branch per repo)',
|
|
@@ -379,6 +387,16 @@ export const en: TaskboardDict = {
|
|
|
379
387
|
'set.foot.dirty': 'Unsaved changes',
|
|
380
388
|
'set.foot.clean': 'Matches the current board settings',
|
|
381
389
|
'set.action.save': 'Save settings',
|
|
390
|
+
'set.storage.heading': 'Data storage location',
|
|
391
|
+
'set.storage.hint': 'The task ledger, templates, and image attachments live together in this directory. Changing it copies and verifies everything before switching.',
|
|
392
|
+
'set.storage.loading': 'Loading the current path…',
|
|
393
|
+
'set.storage.current': 'Current path: {path}',
|
|
394
|
+
'set.storage.assets': 'Image attachments: {count}, {size} MiB',
|
|
395
|
+
'set.storage.default': 'Restore default path',
|
|
396
|
+
'set.storage.check': 'Check path',
|
|
397
|
+
'set.storage.migrate': 'Migrate data',
|
|
398
|
+
'set.storage.migrating': 'Migrating…',
|
|
399
|
+
'set.storage.confirm': 'Migrate the complete task ledger, templates, and image attachments?\n\nFrom: {from}\nTo: {to}\n\nWrites queue briefly during migration.',
|
|
382
400
|
|
|
383
401
|
// ── execution permission (PR #14) ─────────────────────────────────
|
|
384
402
|
'form.field.permission': 'Execution permission',
|
|
@@ -412,6 +430,10 @@ export const en: TaskboardDict = {
|
|
|
412
430
|
'md.imageTitle': 'Click to view full size ({alt})',
|
|
413
431
|
'md.lightboxAlt': 'Full-size preview',
|
|
414
432
|
'md.closePreview': 'Close preview',
|
|
433
|
+
'image.add': 'Insert image',
|
|
434
|
+
'image.uploading': 'Uploading…',
|
|
435
|
+
'image.hint': 'Choose, paste, or drop PNG/JPEG/GIF/WebP images up to 5 MB each',
|
|
436
|
+
'image.defaultAlt': 'Image',
|
|
415
437
|
|
|
416
438
|
// ── slash completion popup (PR #14) ───────────────────────────────
|
|
417
439
|
'slash.aria': 'Quick commands and skills',
|
|
@@ -456,4 +478,14 @@ export const en: TaskboardDict = {
|
|
|
456
478
|
'slash.skill.source-driven-development': 'Design from authoritative docs and source code',
|
|
457
479
|
'slash.skill.spec-driven-development': 'Write clear technical specs before coding',
|
|
458
480
|
'slash.skill.using-agent-skills': 'Discover and invoke agent skills dynamically',
|
|
481
|
+
|
|
482
|
+
// ── host-generated system comments (0.6.4; localized at render) ────
|
|
483
|
+
'sys.execFailed': '[System] Execution failed: {error}; the task was returned to todo.',
|
|
484
|
+
'sys.endedWithComment': '[System] The execution session ended with comments but was not moved to in review; the system moved it to in review automatically.',
|
|
485
|
+
'sys.endedNoHandoff': '[System] The execution session ended without a protocol handoff (no comments, not moved to in review); the system moved it to in review automatically — review it, then send back or accept.',
|
|
486
|
+
'sys.sessionError': '[System] Session execution error: {error}; the task was returned to todo.',
|
|
487
|
+
'sys.sessionDone': '[System] The session finished; automatically moved to in review.',
|
|
488
|
+
'sys.cronDead': '[System] The cron expression {cron} has no trigger time within 4 years; scheduling disabled — fix the cron and re-enable.',
|
|
489
|
+
'sys.mergeSingle': '[System] Branch {branch} merged into the main worktree (--no-ff).',
|
|
490
|
+
'sys.mergeMulti': '[System] Branches merged per repo (--no-ff): {summary}',
|
|
459
491
|
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Client-side localization of built-in task templates (0.6.4). Built-in
|
|
3
|
+
* templates are seeded host-side with zh fallback text; this module resolves
|
|
4
|
+
* the ACTIVE locale's content (name + task spec) so the new-task dropdown,
|
|
5
|
+
* the template manager, and the create-form prefill follow the GUI language
|
|
6
|
+
* live. Custom templates (no `builtin` flag) and unknown built-in ids pass
|
|
7
|
+
* through untouched.
|
|
8
|
+
*
|
|
9
|
+
* @module dsh-taskboard/client/i18n/templates
|
|
10
|
+
*/
|
|
11
|
+
import type { TaskTemplate, TaskTemplateSpec } from '../../shared/api.ts'
|
|
12
|
+
import { builtinTemplateContent } from '../../shared/builtin-templates.ts'
|
|
13
|
+
import { localeStore } from './runtime.ts'
|
|
14
|
+
|
|
15
|
+
/** The localized display name for a template (stored name for custom ones). */
|
|
16
|
+
export function localizeBuiltinName(tpl: Pick<TaskTemplate, 'id' | 'name' | 'builtin'>): string {
|
|
17
|
+
if (tpl.builtin !== true) return tpl.name
|
|
18
|
+
return builtinTemplateContent(tpl.id, localeStore.getSnapshot().active)?.name ?? tpl.name
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** The localized task spec for a template (stored spec for custom ones). */
|
|
22
|
+
export function localizeBuiltinTask(tpl: Pick<TaskTemplate, 'id' | 'task' | 'builtin'>): TaskTemplateSpec {
|
|
23
|
+
if (tpl.builtin !== true) return tpl.task
|
|
24
|
+
return builtinTemplateContent(tpl.id, localeStore.getSnapshot().active)?.task ?? tpl.task
|
|
25
|
+
}
|
package/src/client/i18n/zh.ts
CHANGED
|
@@ -215,6 +215,14 @@ export const zh = {
|
|
|
215
215
|
'detail.move.to': '移至→{status}',
|
|
216
216
|
'detail.move.confirmDoneUnchecked': '仍有 {n} 项清单未勾选,确认完成?',
|
|
217
217
|
'detail.move.confirmDone': '确认完成?',
|
|
218
|
+
'detail.move.confirmArchive': '确认归档该任务?',
|
|
219
|
+
'detail.move.confirmArchiveSessionWithId': '是否连同执行会话 {id} 一同归档?',
|
|
220
|
+
'detail.move.confirmArchiveSessionCount': '是否连同 {n} 个执行会话一同归档?',
|
|
221
|
+
'detail.move.archiveWithSession': '连同会话归档',
|
|
222
|
+
'detail.move.archiveUnsupported': '当前宿主不支持会话归档',
|
|
223
|
+
'detail.move.archiveResult': '卡片已归档;执行会话归档成功 {n} 个。失败会话如下(如有):',
|
|
224
|
+
'detail.move.archiveRetry': '归档 / 重试归档执行会话',
|
|
225
|
+
'detail.move.archiveCardOnly': '仅归档卡片',
|
|
218
226
|
'detail.move.confirm': '确认',
|
|
219
227
|
'detail.blocked.unmark': '✓ 解除受阻',
|
|
220
228
|
'detail.blocked.mark': '⛔ 标记受阻',
|
|
@@ -333,14 +341,14 @@ export const zh = {
|
|
|
333
341
|
'tpl.use.button': '用此新建',
|
|
334
342
|
'tpl.delete.title': '删除该模板',
|
|
335
343
|
'tpl.renamed': '模板已改名',
|
|
336
|
-
'tpl.foot.hint': '
|
|
344
|
+
'tpl.foot.hint': '模板随台账一同保存在当前数据目录,升级不丢',
|
|
337
345
|
|
|
338
346
|
// ── import modal (ImportModal) ────────────────────────────────────
|
|
339
347
|
'imp.aria': '导入台账',
|
|
340
348
|
'imp.title': '导入台账',
|
|
341
349
|
'imp.subtitle': '选择导出的 JSON 备份文件:先预览、再合并或整册替换',
|
|
342
350
|
'imp.parseError': '文件不是合法 JSON',
|
|
343
|
-
'imp.note': '⬇ JSON
|
|
351
|
+
'imp.note': '⬇ JSON 导出可恢复台账;图片附件需同时备份设置页所示数据目录下的 dsh-taskboard-assets 文件夹。导入文件的 schemaVersion 必须与当前版本一致。',
|
|
344
352
|
'imp.previewing': '预览中…',
|
|
345
353
|
'imp.stat.create': '新增',
|
|
346
354
|
'imp.stat.overwrite': '覆盖(同 id)',
|
|
@@ -364,7 +372,7 @@ export const zh = {
|
|
|
364
372
|
// ── board settings modal (SettingsModal) ──────────────────────────
|
|
365
373
|
'set.aria': '看板设置',
|
|
366
374
|
'set.title': '看板设置',
|
|
367
|
-
'set.subtitle': '
|
|
375
|
+
'set.subtitle': '新建任务、会话同步与本地数据存储',
|
|
368
376
|
'set.iso.heading': '默认执行隔离',
|
|
369
377
|
'set.iso.noneHint': '不使用 git,直接在项目目录工作(出厂默认)',
|
|
370
378
|
'set.iso.worktreeHint': '每次执行在独立 worktree 分支上进行(task/标题+ID),互不污染;多仓库工作区自动整区镜像(每仓库独立分支)',
|
|
@@ -381,6 +389,16 @@ export const zh = {
|
|
|
381
389
|
'set.foot.dirty': '有未保存的修改',
|
|
382
390
|
'set.foot.clean': '与看板当前设置一致',
|
|
383
391
|
'set.action.save': '保存设置',
|
|
392
|
+
'set.storage.heading': '数据存储位置',
|
|
393
|
+
'set.storage.hint': '任务台账、模板和图片附件统一存放在此目录。修改路径会先完整复制并校验,再切换到新位置。',
|
|
394
|
+
'set.storage.loading': '正在读取当前路径…',
|
|
395
|
+
'set.storage.current': '当前路径:{path}',
|
|
396
|
+
'set.storage.assets': '图片附件:{count} 个,{size} MiB',
|
|
397
|
+
'set.storage.default': '恢复默认路径',
|
|
398
|
+
'set.storage.check': '检查路径',
|
|
399
|
+
'set.storage.migrate': '迁移数据',
|
|
400
|
+
'set.storage.migrating': '迁移中…',
|
|
401
|
+
'set.storage.confirm': '确认迁移全部任务台账、模板和图片附件?\n\n原路径:{from}\n新路径:{to}\n\n迁移期间写操作会短暂排队。',
|
|
384
402
|
|
|
385
403
|
// ── execution permission (PR #14) ─────────────────────────────────
|
|
386
404
|
'form.field.permission': '执行权限',
|
|
@@ -414,6 +432,10 @@ export const zh = {
|
|
|
414
432
|
'md.imageTitle': '点击查看大图 ({alt})',
|
|
415
433
|
'md.lightboxAlt': '大图预览',
|
|
416
434
|
'md.closePreview': '关闭预览',
|
|
435
|
+
'image.add': '插入图片',
|
|
436
|
+
'image.uploading': '正在上传…',
|
|
437
|
+
'image.hint': '支持选择、粘贴或拖入 PNG/JPEG/GIF/WebP,单张不超过 5 MB',
|
|
438
|
+
'image.defaultAlt': '图片',
|
|
417
439
|
|
|
418
440
|
// ── slash completion popup (PR #14) ───────────────────────────────
|
|
419
441
|
'slash.aria': '快捷命令与技能',
|
|
@@ -458,6 +480,16 @@ export const zh = {
|
|
|
458
480
|
'slash.skill.source-driven-development': '基于权威官方文档与源码进行设计实现',
|
|
459
481
|
'slash.skill.spec-driven-development': '在编码前制定清晰的技术规范',
|
|
460
482
|
'slash.skill.using-agent-skills': '发现并动态调用智能体各项专业技能',
|
|
483
|
+
|
|
484
|
+
// ── host-generated system comments (0.6.4; localized at render) ────
|
|
485
|
+
'sys.execFailed': '[系统] 执行失败:{error};任务已退回待办。',
|
|
486
|
+
'sys.endedWithComment': '[系统] 执行会话已结束并留有评论,但未移至待验收;系统自动移入待验收。',
|
|
487
|
+
'sys.endedNoHandoff': '[系统] 执行会话已结束,但未按协议交接(无评论、未移至待验收);系统自动移入待验收,请审查后退回或验收。',
|
|
488
|
+
'sys.sessionError': '[系统] 会话执行异常:{error};任务已退回待办。',
|
|
489
|
+
'sys.sessionDone': '[系统] 会话执行完毕,已自动进入待验收。',
|
|
490
|
+
'sys.cronDead': '[系统] 定时表达式 {cron} 在 4 年内没有可触发时间,已停用定时;请修正 cron 后重新开启。',
|
|
491
|
+
'sys.mergeSingle': '[系统] 分支 {branch} 已合并到主工作区(--no-ff)。',
|
|
492
|
+
'sys.mergeMulti': '[系统] 分支已按仓库合并(--no-ff):{summary}',
|
|
461
493
|
} as const
|
|
462
494
|
|
|
463
495
|
/** The dictionary shape en.ts must match key-for-key. */
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { AttachmentUpload } from '../shared/api.ts'
|
|
2
|
+
|
|
3
|
+
export const IMAGE_ACCEPT = 'image/png,image/jpeg,image/gif,image/webp'
|
|
4
|
+
const TYPES = new Set(IMAGE_ACCEPT.split(','))
|
|
5
|
+
|
|
6
|
+
/** Keep Markdown alt text single-line and unable to close its own bracket. */
|
|
7
|
+
export function imageAlt(fileName: string, fallback: string): string {
|
|
8
|
+
const withoutExtension = fileName.replace(/\.(?:png|jpe?g|gif|webp)$/i, '')
|
|
9
|
+
const clean = withoutExtension.replace(/[\[\]\r\n]/g, ' ').replace(/\s+/g, ' ').trim()
|
|
10
|
+
return clean.length > 0 ? clean.slice(0, 120) : fallback
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function imageMarkdown(asset: AttachmentUpload, alt: string): string {
|
|
14
|
+
return ``
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** Insert a block at the current selection, preserving readable line boundaries. */
|
|
18
|
+
export function insertImageMarkdown(value: string, start: number, end: number, markdown: string): { value: string; cursor: number } {
|
|
19
|
+
const before = value.slice(0, start)
|
|
20
|
+
const after = value.slice(end)
|
|
21
|
+
const prefix = before.length > 0 && !before.endsWith('\n') ? '\n' : ''
|
|
22
|
+
const suffix = after.length > 0 && !after.startsWith('\n') ? '\n' : ''
|
|
23
|
+
const inserted = `${prefix}${markdown}${suffix}`
|
|
24
|
+
return { value: before + inserted + after, cursor: before.length + inserted.length }
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function acceptedImageFiles(files: Iterable<File>): File[] {
|
|
28
|
+
return Array.from(files).filter(file => TYPES.has(file.type)).slice(0, 10)
|
|
29
|
+
}
|
package/src/client/styles.ts
CHANGED
|
@@ -365,7 +365,7 @@ button.dsh-atb-chip2.dsh-atb-chip-btn:hover {
|
|
|
365
365
|
.dsh-atb-movebtn[data-to="canceled"], .dsh-atb-movebtn[data-to="archived"] { opacity: .75; }
|
|
366
366
|
.dsh-atb-movebtn[data-to="blocked"] { border-color: rgba(229,72,77,.45); }
|
|
367
367
|
.dsh-atb-movebtn[data-to="blocked"]:hover { background: rgba(229,72,77,.1); }
|
|
368
|
-
.dsh-atb-confirm { display: inline-flex; align-items: center; gap: 6px; }
|
|
368
|
+
.dsh-atb-confirm { display: inline-flex; align-items: center; gap: 6px; flex-wrap: wrap; }
|
|
369
369
|
.dsh-atb-confirm-label { font-size: 11.5px; color: var(--dsw-text-secondary, gray); }
|
|
370
370
|
|
|
371
371
|
.dsh-atb-section { font-size: 13px; display: flex; flex-direction: column; gap: 7px; }
|
|
@@ -398,6 +398,27 @@ button.dsh-atb-chip2.dsh-atb-chip-btn:hover {
|
|
|
398
398
|
.dsh-atb-bubble-meta span { font-size: 10.5px; color: var(--dsw-text-secondary, gray); }
|
|
399
399
|
.dsh-atb-bubble-body { font-size: 12.5px; line-height: 1.55; white-space: pre-wrap; word-break: break-word; }
|
|
400
400
|
|
|
401
|
+
.dsh-atb-markdown-body { white-space: pre-wrap; word-break: break-word; }
|
|
402
|
+
.dsh-atb-detail-img-wrap {
|
|
403
|
+
display: inline-flex; flex-direction: column; gap: 4px; max-width: min(100%, 520px); margin: 6px 8px 6px 0;
|
|
404
|
+
vertical-align: top;
|
|
405
|
+
}
|
|
406
|
+
.dsh-atb-detail-img {
|
|
407
|
+
display: block; max-width: 100%; max-height: 320px; object-fit: contain; border-radius: 8px; cursor: zoom-in;
|
|
408
|
+
border: 1px solid var(--dsw-border, rgba(128,128,128,.22)); background: var(--dsw-bg-inset, rgba(128,128,128,.08));
|
|
409
|
+
}
|
|
410
|
+
.dsh-atb-detail-img-caption { font-size: 10.5px; color: var(--dsw-text-secondary, gray); overflow-wrap: anywhere; }
|
|
411
|
+
.dsh-atb-lightbox-backdrop {
|
|
412
|
+
position: fixed; inset: 0; z-index: 120; display: grid; place-items: center; padding: 28px;
|
|
413
|
+
background: rgba(0,0,0,.76); backdrop-filter: blur(3px);
|
|
414
|
+
}
|
|
415
|
+
.dsh-atb-lightbox-content { position: relative; max-width: 96vw; max-height: 92vh; }
|
|
416
|
+
.dsh-atb-lightbox-img { display: block; max-width: 96vw; max-height: 92vh; object-fit: contain; border-radius: 10px; }
|
|
417
|
+
.dsh-atb-lightbox-close {
|
|
418
|
+
position: absolute; top: -14px; right: -14px; width: 30px; height: 30px; border-radius: 999px; cursor: pointer;
|
|
419
|
+
border: 1px solid rgba(255,255,255,.38); background: rgba(20,20,20,.9); color: #fff;
|
|
420
|
+
}
|
|
421
|
+
|
|
401
422
|
.dsh-atb-composer { display: flex; gap: 7px; align-items: flex-end; margin-top: 2px; }
|
|
402
423
|
.dsh-atb-composer-input {
|
|
403
424
|
flex: 1; font: inherit; font-size: 12.5px; line-height: 1.5; padding: 7px 10px; border-radius: 9px;
|
|
@@ -410,6 +431,14 @@ button.dsh-atb-chip2.dsh-atb-chip-btn:hover {
|
|
|
410
431
|
border: 1px solid transparent; background: var(--dsw-alias-button-primary-fill, var(--dsw-alias-brand-primary, #1f2328)); color: var(--dsw-alias-label-primary-foreground, #fff);
|
|
411
432
|
}
|
|
412
433
|
.dsh-atb-composer-send:disabled { opacity: .4; cursor: default; }
|
|
434
|
+
.dsh-atb-image-actions { display: flex; align-items: center; gap: 8px; padding: 5px 1px 0; }
|
|
435
|
+
.dsh-atb-image-add {
|
|
436
|
+
flex: none; font: inherit; font-size: 11.5px; line-height: 1.4; padding: 4px 8px; border-radius: 7px; cursor: pointer;
|
|
437
|
+
border: 1px solid var(--dsw-border, rgba(128,128,128,.3)); background: var(--dsw-bg-elevated, rgba(128,128,128,.08)); color: inherit;
|
|
438
|
+
}
|
|
439
|
+
.dsh-atb-image-add:hover:not(:disabled) { background: var(--dsw-bg-hover, rgba(128,128,128,.14)); }
|
|
440
|
+
.dsh-atb-image-add:disabled { opacity: .45; cursor: default; }
|
|
441
|
+
.dsh-atb-image-hint { font-size: 10.5px; color: var(--dsw-text-secondary, gray); }
|
|
413
442
|
|
|
414
443
|
.dsh-atb-execlist { display: flex; flex-direction: column; gap: 5px; }
|
|
415
444
|
.dsh-atb-exec-row {
|
|
@@ -854,9 +883,13 @@ color: var(--dsw-alias-state-business-primary, #3e63dd);
|
|
|
854
883
|
.dsh-atb-imp-result { font-size: 12px; color: var(--dsw-alias-state-success-primary, #30a46c); margin-top: 10px; }
|
|
855
884
|
.dsh-atb-badge[data-kind="checklist"] { color: var(--dsw-alias-label-secondary, inherit); }
|
|
856
885
|
/* ---------- 0.5.0 board settings ---------- */
|
|
857
|
-
.dsh-atb-set { max-width:
|
|
886
|
+
.dsh-atb-set { max-width: 620px; width: min(620px, 92vw); }
|
|
858
887
|
.dsh-atb-set .dsh-atb-mode-picker { margin-top: 8px; }
|
|
859
888
|
.dsh-atb-set .dsh-atb-isolation-note { margin-top: 10px; }
|
|
889
|
+
.dsh-atb-storage-path { width: 100%; margin-top: 10px; font-family: ui-monospace, SFMono-Regular, Consolas, monospace; }
|
|
890
|
+
.dsh-atb-storage-meta { display: grid; gap: 4px; margin-top: 8px; color: var(--dsh-atb-muted); font-size: 12px; overflow-wrap: anywhere; }
|
|
891
|
+
.dsh-atb-storage-error { color: var(--dsh-atb-danger); }
|
|
892
|
+
.dsh-atb-storage-actions { display: flex; flex-wrap: wrap; justify-content: flex-end; gap: 8px; margin-top: 10px; }
|
|
860
893
|
|
|
861
894
|
/* ---------- 0.5.5 SlashPromptInput & Permission Picker ---------- */
|
|
862
895
|
.dsh-atb-perm-picker { display: grid; grid-template-columns: repeat(3, 1fr); gap: 7px; margin-top: 4px; }
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { SessionArchiveResult } from '../shared/api.ts'
|
|
2
|
+
import { taskAssociatedSessionIds, type TaskRecord } from '../shared/protocol.ts'
|
|
3
|
+
|
|
4
|
+
/** Idempotent best-effort archiving with explicit per-session outcomes. */
|
|
5
|
+
export async function archiveTaskSessions(task: TaskRecord, archive?: (id: string) => Promise<void>): Promise<SessionArchiveResult> {
|
|
6
|
+
const sessionIds = taskAssociatedSessionIds(task)
|
|
7
|
+
if (archive === undefined) return { archived: [], failed: [], unsupported: sessionIds }
|
|
8
|
+
const result: SessionArchiveResult = { archived: [], failed: [], unsupported: [] }
|
|
9
|
+
for (const sessionId of sessionIds) {
|
|
10
|
+
try {
|
|
11
|
+
await archive(sessionId)
|
|
12
|
+
result.archived.push(sessionId)
|
|
13
|
+
} catch (error) {
|
|
14
|
+
result.failed.push({ sessionId, error: error instanceof Error ? error.message : String(error) })
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
return result
|
|
18
|
+
}
|