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.
@@ -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). */
@@ -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
- const isolation = isolationRaw === null ? undefined : asIsolation(isolationRaw)
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
- ...(isolation !== undefined ? { isolation } : {}),
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" (default — each run gets a fresh git worktree on branch task/<标题>+<taskId>) or "none" (run in the project directory, zero git interaction).',
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
- const isolation = args.isolation === undefined ? undefined : asIsolation(args.isolation)
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
- ...(isolation !== undefined ? { isolation } : {}),
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: {
@@ -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 = the default `worktree`; non-git projects auto-degrade at run
111
- * time (the execution record carries an `isolationNote` explaining why).
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 worktree default). */
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 ? 'worktree' : task.isolation
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
@@ -6,4 +6,4 @@
6
6
  */
7
7
 
8
8
  /** The package version (must equal package.json "version"). */
9
- export const PLUGIN_VERSION = '0.4.4'
9
+ export const PLUGIN_VERSION = '0.5.0'