dsh-taskboard 0.4.4 → 0.5.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 +14 -1
- package/lib/client.js +217 -38
- package/lib/host/routes.js +30 -3
- package/lib/host/routes.js.map +1 -1
- package/lib/host/store.js.map +1 -1
- package/lib/host/tools.js +4 -4
- package/lib/host/tools.js.map +1 -1
- package/lib/shared/api.js.map +1 -1
- package/lib/shared/protocol.js +26 -4
- package/lib/shared/protocol.js.map +1 -1
- package/package.json +7 -7
- package/src/client/api.ts +8 -0
- package/src/client/board/SettingsModal.tsx +84 -0
- package/src/client/board/TaskBoard.tsx +40 -2
- package/src/client/board/TaskFormModal.tsx +9 -8
- package/src/client/controller.ts +26 -21
- package/src/client/index.ts +0 -6
- package/src/client/styles.ts +4 -0
- package/src/host/routes.ts +36 -2
- package/src/host/store.ts +1 -1
- package/src/host/tools.ts +7 -3
- package/src/shared/api.ts +10 -1
- package/src/shared/protocol.ts +48 -5
- package/src/shared/version.ts +1 -1
package/src/client/styles.ts
CHANGED
|
@@ -689,6 +689,10 @@ html[data-dsh-atb-active] .dsh-atb-view { display: flex; flex-direction: column;
|
|
|
689
689
|
.dsh-atb-imp-row-status { font-size: 10.5px; color: var(--dsw-alias-label-tertiary, gray); flex-shrink: 0; }
|
|
690
690
|
.dsh-atb-imp-result { font-size: 12px; color: var(--dsw-alias-state-success-primary, #30a46c); margin-top: 10px; }
|
|
691
691
|
.dsh-atb-badge[data-kind="checklist"] { color: var(--dsw-alias-label-secondary, inherit); }
|
|
692
|
+
/* ---------- 0.5.0 board settings ---------- */
|
|
693
|
+
.dsh-atb-set { max-width: 460px; width: min(460px, 92vw); }
|
|
694
|
+
.dsh-atb-set .dsh-atb-mode-picker { margin-top: 8px; }
|
|
695
|
+
.dsh-atb-set .dsh-atb-isolation-note { margin-top: 10px; }
|
|
692
696
|
`
|
|
693
697
|
|
|
694
698
|
/** Style element id (stable since 0.1.x: hook for tests and debugging). */
|
package/src/host/routes.ts
CHANGED
|
@@ -16,11 +16,13 @@ import type { Context } from '@deepseek-ai/cordis'
|
|
|
16
16
|
// Type-only: pulls the webServer Context merge (ctx.webServer).
|
|
17
17
|
import type {} from '@deepseek-ai/dsh-host-webserver'
|
|
18
18
|
import {
|
|
19
|
+
asBoardSettings,
|
|
19
20
|
asIsolation,
|
|
20
21
|
asStatus,
|
|
21
22
|
asUrgency,
|
|
22
23
|
canTransition,
|
|
23
24
|
checklistFromTexts,
|
|
25
|
+
defaultIsolationOf,
|
|
24
26
|
newCommentId,
|
|
25
27
|
newTaskId,
|
|
26
28
|
normalizeBody,
|
|
@@ -355,6 +357,13 @@ export function registerTaskboardRoutes(ctx: Context, options: TaskboardRoutesOp
|
|
|
355
357
|
return
|
|
356
358
|
}
|
|
357
359
|
|
|
360
|
+
// Board settings (0.5.0): absent fields follow factory defaults.
|
|
361
|
+
if (pathname === `${ROUTE_PREFIX}/settings`) {
|
|
362
|
+
await store.load()
|
|
363
|
+
json(res, { ok: true, value: store.snapshot().settings ?? {} })
|
|
364
|
+
return
|
|
365
|
+
}
|
|
366
|
+
|
|
358
367
|
const taskMatch = pathname.match(new RegExp(`^${ROUTE_PREFIX}/tasks/([^/]+)$`))
|
|
359
368
|
if (taskMatch !== null) {
|
|
360
369
|
const task = store.get(taskMatch[1]!)
|
|
@@ -397,7 +406,10 @@ export function registerTaskboardRoutes(ctx: Context, options: TaskboardRoutesOp
|
|
|
397
406
|
const execution = normalizeExecution((body.execution as { mode?: string; cron?: string } | undefined) ?? {}, options.now())
|
|
398
407
|
const model = body.model === undefined ? undefined : checkModel(body.model, options.modelProviders)
|
|
399
408
|
const isolationRaw = str(body, 'isolation')
|
|
400
|
-
|
|
409
|
+
// 0.5.0: an omitted isolation is MATERIALIZED from the board
|
|
410
|
+
// setting (看板设置) at creation, so later setting changes never
|
|
411
|
+
// rewrite existing tasks.
|
|
412
|
+
const isolation = isolationRaw === null ? defaultIsolationOf(store.snapshot().settings) : asIsolation(isolationRaw)
|
|
401
413
|
const presetId = normalizePresetId(str(body, 'presetId'))
|
|
402
414
|
let checklist: TaskRecord['checklist'] = undefined
|
|
403
415
|
if (body.checklist !== undefined) {
|
|
@@ -419,7 +431,7 @@ export function registerTaskboardRoutes(ctx: Context, options: TaskboardRoutesOp
|
|
|
419
431
|
blocked: false,
|
|
420
432
|
execution,
|
|
421
433
|
model,
|
|
422
|
-
|
|
434
|
+
isolation,
|
|
423
435
|
...(presetId !== undefined ? { presetId } : {}),
|
|
424
436
|
...(checklist !== undefined ? { checklist } : {}),
|
|
425
437
|
version: 1,
|
|
@@ -818,6 +830,10 @@ export function registerTaskboardRoutes(ctx: Context, options: TaskboardRoutesOp
|
|
|
818
830
|
if (mode === 'replace') {
|
|
819
831
|
replacedTotal = ledger.tasks.length
|
|
820
832
|
ledger.tasks = structuredClone(imported)
|
|
833
|
+
// Replace is a whole-ledger swap (0.5.0): board settings ride
|
|
834
|
+
// along when the file carries them; merge keeps the live ones.
|
|
835
|
+
if (plan.settings !== undefined) ledger.settings = structuredClone(plan.settings)
|
|
836
|
+
else delete ledger.settings
|
|
821
837
|
return ledger.tasks
|
|
822
838
|
}
|
|
823
839
|
const byId = new Map(ledger.tasks.map(t => [t.id, t]))
|
|
@@ -872,6 +888,24 @@ export function registerTaskboardRoutes(ctx: Context, options: TaskboardRoutesOp
|
|
|
872
888
|
return
|
|
873
889
|
}
|
|
874
890
|
|
|
891
|
+
// ------------------------------------------ POST /settings/update
|
|
892
|
+
// (0.5.0) Whole-object replace semantics: omitted fields fall back to
|
|
893
|
+
// their factory defaults. Affects only tasks created AFTER the change.
|
|
894
|
+
if (pathname === `${ROUTE_PREFIX}/settings/update`) {
|
|
895
|
+
try {
|
|
896
|
+
const next = asBoardSettings(body)
|
|
897
|
+
await store.mutate('settings-updated', ledger => {
|
|
898
|
+
ledger.settings = next
|
|
899
|
+
return []
|
|
900
|
+
})
|
|
901
|
+
json(res, { ok: true, value: next })
|
|
902
|
+
} catch (error) {
|
|
903
|
+
const f = toFail(error)
|
|
904
|
+
json(res, f.res, f.status)
|
|
905
|
+
}
|
|
906
|
+
return
|
|
907
|
+
}
|
|
908
|
+
|
|
875
909
|
res.writeHead(404)
|
|
876
910
|
res.end()
|
|
877
911
|
} catch (error) {
|
package/src/host/store.ts
CHANGED
|
@@ -23,7 +23,7 @@ export interface LedgerChange {
|
|
|
23
23
|
/** The mutated tasks, if any (a comment purge may touch none). */
|
|
24
24
|
tasks: readonly TaskRecord[]
|
|
25
25
|
/** What kind of mutation this was (for SSE event naming later). */
|
|
26
|
-
kind: 'task-created' | 'task-updated' | 'task-moved' | 'task-deleted' | 'comment-added' | 'execution-recorded'
|
|
26
|
+
kind: 'task-created' | 'task-updated' | 'task-moved' | 'task-deleted' | 'comment-added' | 'execution-recorded' | 'settings-updated'
|
|
27
27
|
}
|
|
28
28
|
|
|
29
29
|
/** Options for {@link TaskStore}. */
|
package/src/host/tools.ts
CHANGED
|
@@ -29,6 +29,7 @@ import {
|
|
|
29
29
|
asUrgency,
|
|
30
30
|
canTransition,
|
|
31
31
|
checklistFromTexts,
|
|
32
|
+
defaultIsolationOf,
|
|
32
33
|
effectivePrompt,
|
|
33
34
|
isClaim,
|
|
34
35
|
isClaimedBy,
|
|
@@ -373,7 +374,7 @@ export function registerTaskboardTools(ctx: ToolContextFace, deps: ToolDeps): Ar
|
|
|
373
374
|
},
|
|
374
375
|
isolation: {
|
|
375
376
|
type: 'string',
|
|
376
|
-
description: 'Code isolation for executions: "worktree" (
|
|
377
|
+
description: 'Code isolation for executions: "worktree" (each run gets a fresh git worktree on branch task/<标题>+<taskId>) or "none" (run in the project directory, zero git interaction). Omitted → the board default (看板设置 → 默认执行隔离; factory default "none").',
|
|
377
378
|
},
|
|
378
379
|
presetId: {
|
|
379
380
|
type: 'string',
|
|
@@ -419,7 +420,10 @@ export function registerTaskboardTools(ctx: ToolContextFace, deps: ToolDeps): Ar
|
|
|
419
420
|
}
|
|
420
421
|
const execution = normalizeExecution(args.execution ?? {}, deps.now())
|
|
421
422
|
const model = args.model !== undefined ? checkModel(deps, args.model) : undefined
|
|
422
|
-
|
|
423
|
+
// 0.5.0: an omitted isolation is MATERIALIZED from the board setting
|
|
424
|
+
// (看板设置) at creation, so later setting changes never rewrite
|
|
425
|
+
// existing tasks.
|
|
426
|
+
const isolation = args.isolation === undefined ? defaultIsolationOf(store.snapshot().settings) : asIsolation(args.isolation)
|
|
423
427
|
const presetId = args.presetId?.trim() || undefined
|
|
424
428
|
const checklist = args.checklist !== undefined ? checklistFromTexts(args.checklist) : undefined
|
|
425
429
|
const now = deps.now()
|
|
@@ -434,7 +438,7 @@ export function registerTaskboardTools(ctx: ToolContextFace, deps: ToolDeps): Ar
|
|
|
434
438
|
blocked: false,
|
|
435
439
|
execution,
|
|
436
440
|
model,
|
|
437
|
-
|
|
441
|
+
isolation,
|
|
438
442
|
...(presetId !== undefined ? { presetId } : {}),
|
|
439
443
|
...(checklist !== undefined ? { checklist } : {}),
|
|
440
444
|
version: 1,
|
package/src/shared/api.ts
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
*
|
|
6
6
|
* @module dsh-taskboard/shared/api
|
|
7
7
|
*/
|
|
8
|
-
import type { TaskLedger, TaskRecord, TaskSummary } from './protocol.ts'
|
|
8
|
+
import type { BoardSettings, TaskLedger, TaskRecord, TaskSummary } from './protocol.ts'
|
|
9
9
|
|
|
10
10
|
export type { TaskRecord }
|
|
11
11
|
|
|
@@ -151,6 +151,15 @@ export type TaskTemplate = {
|
|
|
151
151
|
/** Templates listing response. */
|
|
152
152
|
export type TemplatesResponse = { templates: TaskTemplate[] }
|
|
153
153
|
|
|
154
|
+
/** Board-settings response (0.5.0; absent fields follow factory defaults). */
|
|
155
|
+
export type SettingsResponse = BoardSettings
|
|
156
|
+
|
|
157
|
+
/** Update-board-settings request body (0.5.0; whole-object replace semantics). */
|
|
158
|
+
export type UpdateSettingsBody = {
|
|
159
|
+
/** Default code isolation for NEW tasks ('worktree' | 'none'). */
|
|
160
|
+
defaultIsolation?: string
|
|
161
|
+
}
|
|
162
|
+
|
|
154
163
|
/** Import dry-run response (0.4.0): every task classified, nothing written. */
|
|
155
164
|
export type ImportPreviewResponse = {
|
|
156
165
|
plan: {
|
package/src/shared/protocol.ts
CHANGED
|
@@ -107,11 +107,20 @@ export const URGENCY_COLOR: Readonly<Record<Urgency, string>> = {
|
|
|
107
107
|
* - `worktree`: each execution runs in a fresh `git worktree` on a dedicated
|
|
108
108
|
* task branch (`task/<标题>+<taskId>`) under `<workspace>/.dsh-worktrees/`.
|
|
109
109
|
* - `none`: run in the workspace directory as before, zero git interaction.
|
|
110
|
-
* Omitted =
|
|
111
|
-
*
|
|
110
|
+
* Omitted = {@link DEFAULT_ISOLATION}; since 0.5.0 creation materializes the
|
|
111
|
+
* board default onto the record (看板设置 → 执行隔离), and non-git projects
|
|
112
|
+
* still auto-degrade at run time (the execution record carries an
|
|
113
|
+
* `isolationNote` explaining why).
|
|
112
114
|
*/
|
|
113
115
|
export type IsolationMode = 'worktree' | 'none'
|
|
114
116
|
|
|
117
|
+
/**
|
|
118
|
+
* Factory-default isolation (0.5.0): 原目录执行. Applies when neither the
|
|
119
|
+
* task record nor the board setting (`BoardSettings.defaultIsolation`)
|
|
120
|
+
* says otherwise. Before 0.5.0 the implicit default was 'worktree'.
|
|
121
|
+
*/
|
|
122
|
+
export const DEFAULT_ISOLATION: IsolationMode = 'none'
|
|
123
|
+
|
|
115
124
|
/** Validate an isolation value. */
|
|
116
125
|
export function asIsolation(raw: string): IsolationMode {
|
|
117
126
|
if (raw !== 'worktree' && raw !== 'none') {
|
|
@@ -120,9 +129,39 @@ export function asIsolation(raw: string): IsolationMode {
|
|
|
120
129
|
return raw
|
|
121
130
|
}
|
|
122
131
|
|
|
123
|
-
/** Resolve a task's effective isolation (omitted → the
|
|
132
|
+
/** Resolve a task's effective isolation (omitted → the factory default). */
|
|
124
133
|
export function effectiveIsolation(task: Pick<TaskRecord, 'isolation'>): IsolationMode {
|
|
125
|
-
return task.isolation === undefined ?
|
|
134
|
+
return task.isolation === undefined ? DEFAULT_ISOLATION : task.isolation
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Board-level settings persisted with the ledger (0.5.0). Only fields the
|
|
139
|
+
* user explicitly set are present; absent fields follow factory defaults.
|
|
140
|
+
*/
|
|
141
|
+
export type BoardSettings = {
|
|
142
|
+
/** Default code isolation applied when a NEW task is created without an explicit choice. */
|
|
143
|
+
defaultIsolation?: IsolationMode
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/** Validate raw input into sanitized {@link BoardSettings} (unknown fields dropped). */
|
|
147
|
+
export function asBoardSettings(raw: unknown): BoardSettings {
|
|
148
|
+
if (typeof raw !== 'object' || raw === null) {
|
|
149
|
+
throw new Error('board settings must be an object')
|
|
150
|
+
}
|
|
151
|
+
const e = raw as Record<string, unknown>
|
|
152
|
+
const out: BoardSettings = {}
|
|
153
|
+
if (e.defaultIsolation !== undefined) {
|
|
154
|
+
if (typeof e.defaultIsolation !== 'string') {
|
|
155
|
+
throw new Error("defaultIsolation must be 'worktree' or 'none'")
|
|
156
|
+
}
|
|
157
|
+
out.defaultIsolation = asIsolation(e.defaultIsolation)
|
|
158
|
+
}
|
|
159
|
+
return out
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/** The effective default isolation for NEW tasks (board setting → factory default). */
|
|
163
|
+
export function defaultIsolationOf(settings?: BoardSettings): IsolationMode {
|
|
164
|
+
return settings?.defaultIsolation ?? DEFAULT_ISOLATION
|
|
126
165
|
}
|
|
127
166
|
|
|
128
167
|
/** How a task may run. */
|
|
@@ -419,6 +458,8 @@ export type TaskLedger = {
|
|
|
419
458
|
/** Global monotonic revision; every mutation bumps it. */
|
|
420
459
|
revision: number
|
|
421
460
|
tasks: TaskRecord[]
|
|
461
|
+
/** Board-level settings (0.5.0); absent on ledgers never touched by 设置. */
|
|
462
|
+
settings?: BoardSettings
|
|
422
463
|
}
|
|
423
464
|
|
|
424
465
|
/** Current ledger format version. */
|
|
@@ -740,6 +781,8 @@ export type ImportPlan = {
|
|
|
740
781
|
overwrite: TaskRecord[]
|
|
741
782
|
/** Invalid entries with a human-readable reason (never imported). */
|
|
742
783
|
invalid: Array<{ id?: string; reason: string }>
|
|
784
|
+
/** The file's board settings (0.5.0); replace-mode swaps them, merge keeps the live ones. */
|
|
785
|
+
settings?: BoardSettings
|
|
743
786
|
}
|
|
744
787
|
|
|
745
788
|
/** One unknown-value read helper: string fields with defaults. */
|
|
@@ -882,7 +925,7 @@ export function validateLedgerImport(raw: unknown, knownIds: ReadonlySet<string>
|
|
|
882
925
|
throw new Error(`不支持的 schemaVersion ${String(e.schemaVersion)}(当前支持 ${LEDGER_SCHEMA_VERSION})`)
|
|
883
926
|
}
|
|
884
927
|
if (!Array.isArray(e.tasks)) throw new Error('导入文件的 tasks 不是数组')
|
|
885
|
-
const plan: ImportPlan = { create: [], overwrite: [], invalid: [] }
|
|
928
|
+
const plan: ImportPlan = { create: [], overwrite: [], invalid: [], ...(e.settings !== undefined ? { settings: asBoardSettings(e.settings) } : {}) }
|
|
886
929
|
const seen = new Set<string>()
|
|
887
930
|
for (const entry of e.tasks) {
|
|
888
931
|
const id = typeof (entry as { id?: unknown })?.id === 'string' ? (entry as { id: string }).id : undefined
|
package/src/shared/version.ts
CHANGED