@fonlan/dsh-task-kanban 0.2.6 → 0.2.8

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,36 @@
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
 
14
+ /**
15
+ * The entry config schema. Deliberately UNANNOTATED: schemastery >= 3.18.3
16
+ * added a third `Mode` type parameter, so `.default().volatile()` fields have
17
+ * output type `Volatile<T>` and an explicit `z<KanbanSettingsShape>` no longer
18
+ * compiles (the schema's `meta.default` then carries `Volatile<number>`, which
19
+ * is not a `number`). The plain `KanbanSettingsShape` interface stays the
20
+ * plugin-facing shape: `registerSettings` unwraps the live cells below.
21
+ */
15
22
  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(''),
23
+ // Volatile: the settings plane (settings page / settings.update) only
24
+ // writes volatile-marked fields on dsh >= 0.1.7.
25
+ maxParallelWorkers: z.natural().min(1).default(1).volatile(),
26
+ refinementModel: z.string().default('').volatile(),
27
+ refinementProvider: z.string().default('').volatile(),
28
+ refinementReasoningEffort: z.string().default('').volatile(),
29
+ refinementPreset: z.string().default('').volatile(),
30
+ phaseModel: z.string().default('').volatile(),
31
+ phaseProvider: z.string().default('').volatile(),
32
+ phaseReasoningEffort: z.string().default('').volatile(),
33
+ phasePreset: z.string().default('').volatile(),
25
34
  })
26
35
 
27
36
  /** The per-session-type default slice a session kind resolves. */
@@ -47,38 +56,59 @@ export interface KanbanSettingsFace {
47
56
 
48
57
  /** Structural settings-service subset; the real service resolves the schema. */
49
58
  interface SettingsServiceLike {
50
- register(ns: unknown, schema: unknown): unknown
51
59
  update(ns: unknown, patch: Record<string, unknown>, expectedRevision?: number): Promise<unknown>
52
60
  }
53
61
 
54
- const EMPTY_DEFAULTS: KanbanSessionDefaults = { model: '', provider: '', reasoningEffort: '', preset: '' }
62
+ /** One config field as the loader hands it over: a stable volatile reference. */
63
+ interface VolatileField<T> {
64
+ get(): T | undefined
65
+ }
55
66
 
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: '',
67
+ /**
68
+ * Dereference one config field. `Config` fields marked `.volatile()` are handed
69
+ * to the plugin as references (`{ get() }`) instead of plain values, so reading
70
+ * them raw yields the reference object — which serializes to `{}` and makes
71
+ * every setting look empty. Non-volatile (plain) configs keep working as-is.
72
+ */
73
+ function fieldValue<T>(value: unknown, fallback: T): T {
74
+ if (value !== null && typeof value === 'object' && typeof (value as VolatileField<T>).get === 'function') {
75
+ return (value as VolatileField<T>).get() ?? fallback
67
76
  }
77
+ return (value as T | undefined) ?? fallback
78
+ }
79
+
80
+ const EMPTY_DEFAULTS: KanbanSessionDefaults = { model: '', provider: '', reasoningEffort: '', preset: '' }
81
+
82
+ export function registerSettings(ctx: Context, config: KanbanSettingsShape): KanbanSettingsFace {
83
+ const fields = (config ?? {}) as unknown as Record<string, unknown>
84
+ // Read the references on EVERY access: the settings plane updates them in
85
+ // place (a volatile-only config change does not restart this entry).
86
+ const snapshot = (): KanbanSettingsShape => ({
87
+ maxParallelWorkers: fieldValue(fields.maxParallelWorkers, 1),
88
+ refinementModel: fieldValue(fields.refinementModel, ''),
89
+ refinementProvider: fieldValue(fields.refinementProvider, ''),
90
+ refinementReasoningEffort: fieldValue(fields.refinementReasoningEffort, ''),
91
+ refinementPreset: fieldValue(fields.refinementPreset, ''),
92
+ phaseModel: fieldValue(fields.phaseModel, ''),
93
+ phaseProvider: fieldValue(fields.phaseProvider, ''),
94
+ phaseReasoningEffort: fieldValue(fields.phaseReasoningEffort, ''),
95
+ phasePreset: fieldValue(fields.phasePreset, ''),
96
+ })
97
+ // Optimistic overlay: `settings.update` persists through the profile document
98
+ // and refreshes the references asynchronously, so the write's own caller must
99
+ // still observe the patch it just sent.
100
+ let pending: Partial<KanbanSettingsShape> = {}
101
+ const current = (): KanbanSettingsShape => ({ ...snapshot(), ...pending })
68
102
  let service: SettingsServiceLike | undefined
103
+ // Optional settings service: without one the settings still resolve from
104
+ // the entry config; edits just cannot persist.
69
105
  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 })
106
+ service = (sctx as unknown as { settings?: SettingsServiceLike }).settings
77
107
  })
78
108
  return {
79
- get: () => current,
109
+ get: () => current(),
80
110
  sessionDefaults: (kind) => {
81
- const s = current
111
+ const s = current()
82
112
  switch (kind) {
83
113
  case 'refine':
84
114
  return { model: s.refinementModel, provider: s.refinementProvider, reasoningEffort: s.refinementReasoningEffort, preset: s.refinementPreset }
@@ -108,7 +138,10 @@ export function registerSettings(ctx: Context): KanbanSettingsFace {
108
138
  if (service === undefined) {
109
139
  throw new Error('@fonlan/dsh-task-kanban: settings service is not available in this profile')
110
140
  }
141
+ // The settings plane keys plugin settings by PROFILE ENTRY id (the id in
142
+ // cordis.patch.yml), which is what this plugin ships.
111
143
  await service.update(KANBAN_SETTINGS_NS, patch as Record<string, unknown>)
144
+ pending = { ...pending, ...patch }
112
145
  },
113
146
  }
114
147
  }