@fonlan/dsh-task-kanban 0.2.5 → 0.2.7

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.
@@ -1,27 +1,28 @@
1
1
  /**
2
2
  * User-facing plugin settings (global parallel worker count, per-session-type
3
- * model/effort/preset defaults), persisted through the settings service
4
- * namespace `task-kanban`. The DSH settings service requires a lowercase
5
- * kebab-case namespace (/^[a-z][a-z0-9-]*$/), so the scoped package name
6
- * cannot be used here.
3
+ * model/effort/preset defaults), carried by this bundle's entry config: dsh
4
+ * >= 0.1.7 owns plugin settings as entry config (the plugin's `Config`
5
+ * schema), so the face reads the config it was applied with and persists
6
+ * updates through `settings.update`.
7
7
  */
8
8
  import type { Context } from '@deepseek-ai/cordis'
9
9
  import z from '@deepseek-ai/schemastery'
10
10
  import type { KanbanSessionKind, KanbanSettingsShape } from '../shared/card.js'
11
- import type {} from '@deepseek-ai/dsh-settings'
12
11
 
13
12
  export const KANBAN_SETTINGS_NS = 'task-kanban'
14
13
 
15
- export const KanbanSettingsSchema = z.object({
16
- maxParallelWorkers: z.natural().min(1).default(1),
17
- refinementModel: z.string().default(''),
18
- refinementProvider: z.string().default(''),
19
- refinementReasoningEffort: z.string().default(''),
20
- refinementPreset: z.string().default(''),
21
- phaseModel: z.string().default(''),
22
- phaseProvider: z.string().default(''),
23
- phaseReasoningEffort: z.string().default(''),
24
- phasePreset: z.string().default(''),
14
+ export const KanbanSettingsSchema: z<KanbanSettingsShape> = z.object({
15
+ // Volatile: the settings plane (settings page / settings.update) only
16
+ // writes volatile-marked fields on dsh >= 0.1.7.
17
+ maxParallelWorkers: z.natural().min(1).default(1).volatile(),
18
+ refinementModel: z.string().default('').volatile(),
19
+ refinementProvider: z.string().default('').volatile(),
20
+ refinementReasoningEffort: z.string().default('').volatile(),
21
+ refinementPreset: z.string().default('').volatile(),
22
+ phaseModel: z.string().default('').volatile(),
23
+ phaseProvider: z.string().default('').volatile(),
24
+ phaseReasoningEffort: z.string().default('').volatile(),
25
+ phasePreset: z.string().default('').volatile(),
25
26
  })
26
27
 
27
28
  /** The per-session-type default slice a session kind resolves. */
@@ -47,38 +48,59 @@ export interface KanbanSettingsFace {
47
48
 
48
49
  /** Structural settings-service subset; the real service resolves the schema. */
49
50
  interface SettingsServiceLike {
50
- register(ns: unknown, schema: unknown): unknown
51
51
  update(ns: unknown, patch: Record<string, unknown>, expectedRevision?: number): Promise<unknown>
52
52
  }
53
53
 
54
- const EMPTY_DEFAULTS: KanbanSessionDefaults = { model: '', provider: '', reasoningEffort: '', preset: '' }
54
+ /** One config field as the loader hands it over: a stable volatile reference. */
55
+ interface VolatileField<T> {
56
+ get(): T | undefined
57
+ }
55
58
 
56
- export function registerSettings(ctx: Context): KanbanSettingsFace {
57
- let current: KanbanSettingsShape = {
58
- maxParallelWorkers: 1,
59
- refinementModel: '',
60
- refinementProvider: '',
61
- refinementReasoningEffort: '',
62
- refinementPreset: '',
63
- phaseModel: '',
64
- phaseProvider: '',
65
- phaseReasoningEffort: '',
66
- phasePreset: '',
59
+ /**
60
+ * Dereference one config field. `Config` fields marked `.volatile()` are handed
61
+ * to the plugin as references (`{ get() }`) instead of plain values, so reading
62
+ * them raw yields the reference object — which serializes to `{}` and makes
63
+ * every setting look empty. Non-volatile (plain) configs keep working as-is.
64
+ */
65
+ function fieldValue<T>(value: unknown, fallback: T): T {
66
+ if (value !== null && typeof value === 'object' && typeof (value as VolatileField<T>).get === 'function') {
67
+ return (value as VolatileField<T>).get() ?? fallback
67
68
  }
69
+ return (value as T | undefined) ?? fallback
70
+ }
71
+
72
+ const EMPTY_DEFAULTS: KanbanSessionDefaults = { model: '', provider: '', reasoningEffort: '', preset: '' }
73
+
74
+ export function registerSettings(ctx: Context, config: KanbanSettingsShape): KanbanSettingsFace {
75
+ const fields = (config ?? {}) as unknown as Record<string, unknown>
76
+ // Read the references on EVERY access: the settings plane updates them in
77
+ // place (a volatile-only config change does not restart this entry).
78
+ const snapshot = (): KanbanSettingsShape => ({
79
+ maxParallelWorkers: fieldValue(fields.maxParallelWorkers, 1),
80
+ refinementModel: fieldValue(fields.refinementModel, ''),
81
+ refinementProvider: fieldValue(fields.refinementProvider, ''),
82
+ refinementReasoningEffort: fieldValue(fields.refinementReasoningEffort, ''),
83
+ refinementPreset: fieldValue(fields.refinementPreset, ''),
84
+ phaseModel: fieldValue(fields.phaseModel, ''),
85
+ phaseProvider: fieldValue(fields.phaseProvider, ''),
86
+ phaseReasoningEffort: fieldValue(fields.phaseReasoningEffort, ''),
87
+ phasePreset: fieldValue(fields.phasePreset, ''),
88
+ })
89
+ // Optimistic overlay: `settings.update` persists through the profile document
90
+ // and refreshes the references asynchronously, so the write's own caller must
91
+ // still observe the patch it just sent.
92
+ let pending: Partial<KanbanSettingsShape> = {}
93
+ const current = (): KanbanSettingsShape => ({ ...snapshot(), ...pending })
68
94
  let service: SettingsServiceLike | undefined
95
+ // Optional settings service: without one the settings still resolve from
96
+ // the entry config; edits just cannot persist.
69
97
  ctx.inject(['settings'], (sctx) => {
70
- service = sctx.settings as unknown as SettingsServiceLike
71
- const scope = service.register(KANBAN_SETTINGS_NS, KanbanSettingsSchema) as {
72
- get(): KanbanSettingsShape
73
- watch(callback: (next: KanbanSettingsShape) => void): () => void
74
- }
75
- current = scope.get()
76
- scope.watch((next) => { current = next })
98
+ service = (sctx as unknown as { settings?: SettingsServiceLike }).settings
77
99
  })
78
100
  return {
79
- get: () => current,
101
+ get: () => current(),
80
102
  sessionDefaults: (kind) => {
81
- const s = current
103
+ const s = current()
82
104
  switch (kind) {
83
105
  case 'refine':
84
106
  return { model: s.refinementModel, provider: s.refinementProvider, reasoningEffort: s.refinementReasoningEffort, preset: s.refinementPreset }
@@ -108,7 +130,10 @@ export function registerSettings(ctx: Context): KanbanSettingsFace {
108
130
  if (service === undefined) {
109
131
  throw new Error('@fonlan/dsh-task-kanban: settings service is not available in this profile')
110
132
  }
133
+ // The settings plane keys plugin settings by PROFILE ENTRY id (the id in
134
+ // cordis.patch.yml), which is what this plugin ships.
111
135
  await service.update(KANBAN_SETTINGS_NS, patch as Record<string, unknown>)
136
+ pending = { ...pending, ...patch }
112
137
  },
113
138
  }
114
139
  }