@promptctl/cc-candybar 1.42.1 → 1.43.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.
Files changed (126) hide show
  1. package/dist/index.mjs +72 -71
  2. package/package.json +5 -6
  3. package/src/check.ts +0 -478
  4. package/src/cli-flags.ts +0 -8
  5. package/src/click/wire.ts +0 -158
  6. package/src/config/action.ts +0 -329
  7. package/src/config/cli.ts +0 -71
  8. package/src/config/default-dsl-config.ts +0 -1645
  9. package/src/config/disclosure.ts +0 -170
  10. package/src/config/dsl-loader.ts +0 -339
  11. package/src/config/dsl-types.ts +0 -581
  12. package/src/config/edit-chrome.ts +0 -559
  13. package/src/config/help.ts +0 -151
  14. package/src/config/ident.ts +0 -22
  15. package/src/config/layout-ops.ts +0 -177
  16. package/src/config/loader/actions.ts +0 -972
  17. package/src/config/loader/cache.ts +0 -206
  18. package/src/config/loader/cross-ref.ts +0 -714
  19. package/src/config/loader/cycles.ts +0 -148
  20. package/src/config/loader/diagnostics.ts +0 -99
  21. package/src/config/loader/discovery.ts +0 -182
  22. package/src/config/loader/edit-mode.ts +0 -137
  23. package/src/config/loader/emit-schema.ts +0 -68
  24. package/src/config/loader/globals.ts +0 -269
  25. package/src/config/loader/helpers.ts +0 -48
  26. package/src/config/loader/layout.ts +0 -693
  27. package/src/config/loader/looks.ts +0 -96
  28. package/src/config/loader/menu-synth.ts +0 -435
  29. package/src/config/loader/merge.ts +0 -115
  30. package/src/config/loader/persist-target.ts +0 -67
  31. package/src/config/loader/presets.ts +0 -119
  32. package/src/config/loader/refs.ts +0 -100
  33. package/src/config/loader/reserved-namespace.ts +0 -38
  34. package/src/config/loader/segments.ts +0 -120
  35. package/src/config/loader/validate-core.ts +0 -737
  36. package/src/config/loader/variables.ts +0 -260
  37. package/src/config/menu-keys.ts +0 -139
  38. package/src/config/option-domain.ts +0 -164
  39. package/src/config/presets.ts +0 -326
  40. package/src/config/settings-menu.ts +0 -775
  41. package/src/daemon/acquire.ts +0 -684
  42. package/src/daemon/cache/git.ts +0 -649
  43. package/src/daemon/cache/render.ts +0 -623
  44. package/src/daemon/cache/session-usage-store.ts +0 -720
  45. package/src/daemon/cache/watchers.ts +0 -249
  46. package/src/daemon/client-debug.ts +0 -120
  47. package/src/daemon/client-stats.ts +0 -130
  48. package/src/daemon/client-transport.ts +0 -273
  49. package/src/daemon/client.ts +0 -78
  50. package/src/daemon/config-overrides-store.ts +0 -663
  51. package/src/daemon/debug-types.ts +0 -91
  52. package/src/daemon/debug.ts +0 -264
  53. package/src/daemon/fork-bomb-breaker.ts +0 -351
  54. package/src/daemon/limits.ts +0 -211
  55. package/src/daemon/log.ts +0 -81
  56. package/src/daemon/parent-watchdog.ts +0 -87
  57. package/src/daemon/paths.ts +0 -211
  58. package/src/daemon/process-fingerprint.ts +0 -146
  59. package/src/daemon/protocol.ts +0 -292
  60. package/src/daemon/render-payload.ts +0 -1256
  61. package/src/daemon/server.ts +0 -1330
  62. package/src/daemon/session-state-file.ts +0 -108
  63. package/src/daemon/session-state.ts +0 -237
  64. package/src/daemon/socket-lease.ts +0 -209
  65. package/src/daemon/socket-ownership.ts +0 -209
  66. package/src/daemon/stats.ts +0 -235
  67. package/src/daemon/verbs/config-validators.ts +0 -250
  68. package/src/daemon/verbs/index.ts +0 -706
  69. package/src/daemon/verbs/state-validators.ts +0 -249
  70. package/src/daemon/verbs/validator-registry.ts +0 -457
  71. package/src/demo/dsl.ts +0 -143
  72. package/src/demo/mock-data.ts +0 -67
  73. package/src/demo/statusline.json5 +0 -94
  74. package/src/dsl/node-registry.ts +0 -374
  75. package/src/dsl/render.ts +0 -803
  76. package/src/help-text.ts +0 -90
  77. package/src/index.ts +0 -210
  78. package/src/install/currency.ts +0 -197
  79. package/src/install/index.ts +0 -557
  80. package/src/proc/launch.ts +0 -459
  81. package/src/proc/stats-handle.ts +0 -13
  82. package/src/render/action.ts +0 -883
  83. package/src/render/active-segment.ts +0 -78
  84. package/src/render/diagnostic-style.ts +0 -23
  85. package/src/render/diagnostic-text.ts +0 -77
  86. package/src/render/error-glyph.ts +0 -53
  87. package/src/render/menu.ts +0 -257
  88. package/src/render/outcome-plan.ts +0 -45
  89. package/src/render/picker.ts +0 -372
  90. package/src/render/segment-color.ts +0 -74
  91. package/src/render/split-lines.ts +0 -51
  92. package/src/render/strip.ts +0 -228
  93. package/src/segments/cache.ts +0 -131
  94. package/src/segments/context.ts +0 -190
  95. package/src/segments/git.ts +0 -1084
  96. package/src/segments/metrics.ts +0 -187
  97. package/src/segments/pricing.ts +0 -452
  98. package/src/segments/session.ts +0 -23
  99. package/src/segments/tmux.ts +0 -74
  100. package/src/template-engine/cells.ts +0 -90
  101. package/src/template-engine/colors.ts +0 -124
  102. package/src/template-engine/engine.ts +0 -108
  103. package/src/template-engine/funcs.ts +0 -232
  104. package/src/template-engine/index.ts +0 -11
  105. package/src/template-engine/layout.ts +0 -133
  106. package/src/template-engine/scope.ts +0 -62
  107. package/src/template-engine/sparkline.ts +0 -79
  108. package/src/themes/index.ts +0 -20
  109. package/src/themes/palette-resolvers.ts +0 -84
  110. package/src/themes/policy.ts +0 -393
  111. package/src/utils/cache.ts +0 -206
  112. package/src/utils/claude.ts +0 -683
  113. package/src/utils/color-support.ts +0 -118
  114. package/src/utils/formatters.ts +0 -99
  115. package/src/utils/logger.ts +0 -5
  116. package/src/utils/outcome.ts +0 -33
  117. package/src/utils/schema-validator.ts +0 -126
  118. package/src/utils/single-flight.ts +0 -57
  119. package/src/utils/terminal-width.ts +0 -51
  120. package/src/utils/terminal.ts +0 -11
  121. package/src/utils/transcript-fs.ts +0 -279
  122. package/src/var-system/index.ts +0 -24
  123. package/src/var-system/sources.ts +0 -1047
  124. package/src/var-system/store.ts +0 -223
  125. package/src/var-system/types.ts +0 -57
  126. package/src/version.ts +0 -17
@@ -1,883 +0,0 @@
1
- // [LAW:locality-or-seam] The runtime half of the actions seam. A `{{ action
2
- // "name" display [boundValue] }}` call binds one clickable region (an OSC-8 span)
3
- // to a statically-declared, named action. This module compiles the action table
4
- // (pre-parsing copy/open templates once) and realizes a named action against the
5
- // live state into ONE RichText whose span carries the click URL.
6
- //
7
- // [LAW:one-source-of-truth] The action NAME is the seam: the template supplies
8
- // the REPRESENTATION (the display text), the action declaration supplies the
9
- // BEHAVIOR (what value is written / copied / opened). The same declaration that
10
- // realizes this click derives the wire gate (deriveActionValidators), so the
11
- // rendered click and the gate cannot diverge.
12
- //
13
- // [LAW:dataflow-not-control-flow] `{{ action … }}` is ONE template expression, so
14
- // it emits ONE value — a RichText carrying one OSC-8 span. The realization is a
15
- // single total fold over the compiled-action union: each arm projects (effect,
16
- // active) as DATA, never a branch that skips work.
17
- //
18
- // [LAW:one-way-deps] This is the action feature's runtime. It lives in render/
19
- // (which depends on template-engine/), reads template-engine/scope, and is
20
- // injected into the engine by the caller (registerDslConfig hands the action
21
- // FuncMap in as data). The generic engine never imports this module.
22
-
23
- import { RichText, Style } from "@promptctl/rich-js";
24
- import type { FuncMap, Template } from "@promptctl/go-template-js";
25
- import type { VariableStore } from "../var-system/store.js";
26
- import { toString as varToString } from "../var-system/types.js";
27
- import { buildScope } from "../template-engine/scope.js";
28
- import {
29
- actionDestinations,
30
- actionIsDual,
31
- PERSIST_WHEN,
32
- type ActionDecl,
33
- } from "../config/action.js";
34
- import { resolveOptionDomain } from "../config/option-domain.js";
35
- import { pickCycleDisplay } from "../config/disclosure.js";
36
- import { encodeLayoutOp, type LayoutOp } from "../config/layout-ops.js";
37
- import { parseSessionBoolean, type StripStyle } from "../themes/policy.js";
38
- import {
39
- effectsUrl,
40
- VERB_APPLY_LAYOUT_OP,
41
- VERB_COPY,
42
- VERB_OPEN_VSCODE,
43
- VERB_REDO,
44
- VERB_RESET_CONFIG,
45
- VERB_SET_CONFIG,
46
- VERB_SET_STATE,
47
- VERB_STEP_CONFIG,
48
- VERB_STEP_STATE,
49
- VERB_UNDO,
50
- type Effect,
51
- } from "../click/wire.js";
52
-
53
- // ─── Compiled shapes ───────────────────────────────────────────────────────────
54
-
55
- // [LAW:types-are-the-program] A compiled action mirrors ActionDecl, discriminated
56
- // by `kind`. Each `set` arm carries the SessionState `key` it writes plus the
57
- // `stateVar` that reads it back (resolved from the key, so the displayed/active
58
- // value and the written value are one source — the same resolution a stepper
59
- // widget uses). A literal carries its fixed `value`; an option binds the value
60
- // from the template at render; a bounded carries its [min,max]/by navigation.
61
- // copy/open carry a pre-parsed template evaluated against the live scope.
62
- export type CompiledActionDecl =
63
- | {
64
- readonly kind: "set-literal";
65
- readonly key: string;
66
- readonly value: string;
67
- readonly stateVar: string;
68
- }
69
- | {
70
- readonly kind: "set-option";
71
- readonly key: string;
72
- readonly stateVar: string;
73
- // The resolved option domain. Stored at compile so a picker can iterate it
74
- // without re-resolving the source list, and so the set-option IS
75
- // self-describing (it knows its own domain), not just a key.
76
- readonly options: readonly string[];
77
- }
78
- | {
79
- // [LAW:types-are-the-program] A stepper affordance. It carries ONLY the
80
- // render-invariant click intent: the state `key` and the signed delta `by`.
81
- // It deliberately holds NO stateVar/min/max and reads NO current value at
82
- // render — the absolute target is computed at APPLY time from live state
83
- // (the daemon's step-state handler), so the emitted link is byte-identical
84
- // across renders and N rapid clicks each re-read-and-step. [LAW:one-source-
85
- // of-truth] the bounds live once in the range validator the handler reads.
86
- readonly kind: "set-bounded";
87
- readonly key: string;
88
- readonly by: number;
89
- }
90
- | {
91
- // [LAW:types-are-the-program] An int cursor: it writes whatever integer the
92
- // render binds (the picker's page nav supplies -1/p±1; a bare `{{ action }}`
93
- // supplies its display/boundValue). The gate is an unbounded int — the
94
- // renderer owns clamping to valid pages, exactly as set-bounded owns wrap.
95
- readonly kind: "set-int";
96
- readonly key: string;
97
- readonly stateVar: string;
98
- }
99
- | {
100
- // [LAW:types-are-the-program] An enumerated-domain stepper: the click
101
- // writes the SUCCESSOR of the current value in `members` (wrapping; a
102
- // current value outside the domain counts as the first member). Unlike
103
- // set-bounded — which emits a RELATIVE nudge so rapid clicks accumulate —
104
- // a cycle emits the ABSOLUTE successor computed at render: the rendered
105
- // display names the current state, so the click's meaning is "go to the
106
- // successor of what I showed you". A stale link then lands on the state
107
- // the user saw promised, not an extra flip past it — for toggles the
108
- // absolute write IS the correct intent.
109
- readonly kind: "set-cycle";
110
- readonly key: string;
111
- readonly stateVar: string;
112
- readonly members: readonly string[];
113
- }
114
- | { readonly kind: "copy"; readonly text: Template<RichText> }
115
- | { readonly kind: "open"; readonly target: Template<RichText> }
116
- // [LAW:one-source-of-truth] `persist`'s twin of the set-* kinds above,
117
- // MINUS set-int (a page cursor is never persisted — see action.ts). Carries
118
- // the SAME shapes for the SAME reason: a persistent write is gated and
119
- // realized exactly like a session write, only the wire verb (VERB_SET_CONFIG/
120
- // VERB_STEP_CONFIG) and the write's durability differ.
121
- | {
122
- readonly kind: "persist-literal";
123
- readonly key: string;
124
- readonly value: string;
125
- readonly stateVar: string;
126
- }
127
- | {
128
- readonly kind: "persist-option";
129
- readonly key: string;
130
- readonly stateVar: string;
131
- readonly options: readonly string[];
132
- }
133
- | {
134
- readonly kind: "persist-bounded";
135
- readonly key: string;
136
- readonly by: number;
137
- }
138
- | {
139
- readonly kind: "persist-cycle";
140
- readonly key: string;
141
- readonly stateVar: string;
142
- readonly members: readonly string[];
143
- }
144
- // [LAW:one-source-of-truth] The gated undo for a persistent write: clears
145
- // one config-overrides key. Carries only the key — there is no value to
146
- // realize, so it shares copy/open's "no gate" shape at compile time (the
147
- // GATE is the key-membership check the reset-config verb handler applies).
148
- | { readonly kind: "reset"; readonly key: string }
149
- // [LAW:one-source-of-truth] brandon-layout-edit-2gc.1's structural-edit
150
- // arms. Fully literal at compile time (the op IS the declaration — no
151
- // template-bound option, unlike persist-option), so `op` is precomputed
152
- // here rather than reconstructed from raw fields at every realize() call.
153
- | { readonly kind: "layout-op"; readonly key: string; readonly op: LayoutOp }
154
- // [LAW:one-source-of-truth] brandon-layout-edit-2gc.3's domain-sourced
155
- // sibling of layout-op: `anchor`/`relation` are fixed at compile time (the
156
- // POSITION is author-time data) but the segment name comes from the
157
- // template's bound option — the option-picking shape `persist-option`
158
- // already has, minus the value being written VERBATIM. `requireOptionKind`
159
- // (render/picker.ts) admits this kind alongside set-option/persist-option
160
- // so a `{{ menu }}`/`{{ picker }}` can drive it with zero picker changes;
161
- // only the WRITE (realize(), below) differs — it encodes the picked option
162
- // into a LayoutOp instead of persisting it as-is.
163
- | {
164
- readonly kind: "layout-op-option";
165
- readonly key: string;
166
- readonly anchor: string;
167
- readonly relation: "before" | "after";
168
- readonly options: readonly string[];
169
- }
170
- // [LAW:one-source-of-truth] brandon-layout-edit-2gc.2's global history
171
- // step over the overrides layer — `reset`'s fine-grained sibling. No key:
172
- // there is nothing to carry, since the history stack (not this action) is
173
- // what decides which entry moves.
174
- | { readonly kind: "undo" }
175
- | { readonly kind: "redo" }
176
- // [LAW:dataflow-not-control-flow] candybar-settings-ui-aok.3's ONE control
177
- // per setting. Both destinations are compiled here as the ordinary
178
- // single-destination shapes they are, and `selector` names the session key
179
- // whose boolean value picks between them at click time. The destination is
180
- // therefore a VALUE flowing through `activeDestination` — every consumer
181
- // (realize, the picker, selectDisplay) resolves it once at the top and then
182
- // runs the code it has always run, so nothing downstream branches on
183
- // "is this dual".
184
- | {
185
- readonly kind: "dual";
186
- readonly selector: string;
187
- readonly session: CompiledActionDecl;
188
- readonly durable: CompiledActionDecl;
189
- // The SessionState key the session half writes, carried so a durable
190
- // click can clear it in the same dispatch (see realize's dual arm).
191
- readonly sessionKey: string;
192
- };
193
-
194
- export type CompiledActions = ReadonlyMap<string, CompiledActionDecl>;
195
-
196
- // [LAW:one-source-of-truth] Globals fields whose CURRENT resolved value is
197
- // exposed to templates under a different var name than the field itself (the
198
- // daemon publishes this resolution once per render — e.g. `theme.effective`
199
- // for `palette`, src/daemon/render-payload.ts). A `persist` action with no
200
- // entry here reads back through its own key name as an input var (mirrors
201
- // compileActions' stateKeyToVar fallback), so every persistable globals field
202
- // needs an entry unless its `.effective` projection happens to be named
203
- // exactly the bare field (none are — every projection carries the
204
- // `.effective` suffix). Every field with a projection is listed
205
- // (candybar-config-engine-71o.3 added style/charset/colorCompatibility/
206
- // autoWrap/padding to palette/look's original two); a field with no entry
207
- // here still writes correctly on `persist` — only its "current selection"
208
- // highlight is inert (readVar falls back to "" since no such var exists).
209
- const CONFIG_KEY_TO_EFFECTIVE_VAR: ReadonlyMap<string, string> = new Map([
210
- ["palette", "theme.effective"],
211
- // [LAW:one-source-of-truth] `preset` earns its entry here the moment a DUAL
212
- // control writes it: compileDual makes BOTH halves read back through this
213
- // map, so a field missing from it loses its current-selection mark on the
214
- // session side too — and the preset picker sits on the settings menu's
215
- // always-visible first row, where "which arrangement am I in" is the whole
216
- // question the control answers.
217
- ["preset", "preset.effective"],
218
- ["look", "look.effective"],
219
- ["style", "style.effective"],
220
- ["charset", "charset.effective"],
221
- ["colorCompatibility", "colorCompatibility.effective"],
222
- ["autoWrap", "autoWrap.effective"],
223
- ["padding", "padding.effective"],
224
- ]);
225
-
226
- // [LAW:locality-or-seam] The runtime holder the `action` template function closes
227
- // over. Populated after the engine is constructed (the func references the
228
- // engine, the compiled actions reference the engine — the holder breaks the
229
- // cycle). `store` is the live VariableStore the renderer reads, so the action
230
- // reads session.id and the current value from the same source the rest of the
231
- // render does.
232
- export interface ActionRuntime {
233
- // [LAW:types-are-the-program] Always present — registerDslConfig sources it
234
- // from the registry it is handed (registry.variableStore), so "no store" is
235
- // structurally unrepresentable. The action reads session.id and current
236
- // values from the same store the renderer reads.
237
- store: VariableStore;
238
- compiled: CompiledActions;
239
- // [LAW:locality-or-seam] The current render's strip style, published per render
240
- // by renderDsl. The picker reads it to reserve the joiner's end-cap chrome at
241
- // its pagination seam — the one place that needs strip geometry, kept off the
242
- // shared `term.cols` budget. Defaulted at registration; renders are sequential
243
- // and synchronous, so the per-render write never leaks across renders.
244
- // [LAW:no-ambient-temporal-coupling]
245
- stripStyle: StripStyle;
246
- // [LAW:locality-or-seam] The current render's intra-cell padding (resolved
247
- // globals.padding), published per render by renderDsl exactly like
248
- // stripStyle. The picker reserves 2×padding at its pagination seam — the
249
- // segment layout pads every line it emits, so a page packed to the full
250
- // budget would otherwise be pushed past the width by the pad spaces.
251
- padding: number;
252
- }
253
-
254
- // ─── Compilation ───────────────────────────────────────────────────────────────
255
-
256
- // Pre-parse the copy/open templates for every action once, at config
257
- // registration; set actions stay literal. [LAW:one-source-of-truth] parse-once,
258
- // evaluate-many — renderAction only evaluates. `stateKeyToVar` maps a
259
- // SessionState key → the variable that reads it (same map widgets use), so a
260
- // set action reads its current/active value from the SAME value the templates
261
- // read, regardless of whether the config named the variable after the key.
262
- // [LAW:single-enforcer] `parse` is the config's ONE helper-aware parse closure
263
- // (registerDslConfig owns it), not a bare engine — action copy/open templates
264
- // resolve the same shared `{{ template "name" }}` helpers every segment does,
265
- // through one boundary. compileActions needs only the ability to parse a source.
266
- export function compileActions(
267
- parse: (src: string) => Template<RichText>,
268
- actions: Readonly<Record<string, ActionDecl>>,
269
- stateKeyToVar: ReadonlyMap<string, string>,
270
- // This config's per-config option domains (currently just "looks" — the
271
- // config's merged look names) — resolveOptionDomain checks these before
272
- // falling back to the global registry (themes/styles).
273
- perConfigDomains: ReadonlyMap<string, readonly string[]>,
274
- ): CompiledActions {
275
- const out = new Map<string, CompiledActionDecl>();
276
- for (const [name, action] of Object.entries(actions)) {
277
- out.set(
278
- name,
279
- compileAction(parse, name, action, stateKeyToVar, perConfigDomains),
280
- );
281
- }
282
- return out;
283
- }
284
-
285
- // [LAW:dataflow-not-control-flow] One total fold maps each ActionDecl to its
286
- // compiled shape — the discriminator is which key is present (set's value SOURCE
287
- // for the three set arms; copy/open otherwise). Every arm reads only its own
288
- // fields; a new arm is one new branch.
289
- function compileAction(
290
- parse: (src: string) => Template<RichText>,
291
- name: string,
292
- action: ActionDecl,
293
- stateKeyToVar: ReadonlyMap<string, string>,
294
- perConfigDomains: ReadonlyMap<string, readonly string[]>,
295
- ): CompiledActionDecl {
296
- // [LAW:one-source-of-truth] A dual compiles as its own two destinations —
297
- // the SAME explosion the validator derivations fold over
298
- // (actionDestinations), so the click a dual realizes and the gate it derives
299
- // come from one statement of what the two halves are. It is matched BEFORE
300
- // the `set` arm because a dual carries `set` too.
301
- if (actionIsDual(action)) {
302
- const [session, durable] = actionDestinations(action);
303
- return compileDual(
304
- stateKeyToVar.get(action[PERSIST_WHEN]) ?? action[PERSIST_WHEN],
305
- action.set,
306
- compileAction(parse, name, session!, stateKeyToVar, perConfigDomains),
307
- compileAction(parse, name, durable!, stateKeyToVar, perConfigDomains),
308
- );
309
- }
310
- if ("set" in action) {
311
- const stateVar = stateKeyToVar.get(action.set) ?? action.set;
312
- if ("to" in action) {
313
- return {
314
- kind: "set-literal",
315
- key: action.set,
316
- value: action.to,
317
- stateVar,
318
- };
319
- }
320
- if ("from" in action) {
321
- return {
322
- kind: "set-option",
323
- key: action.set,
324
- stateVar,
325
- options: [...resolveOptionDomain(action.from, perConfigDomains)],
326
- };
327
- }
328
- if ("int" in action) {
329
- return { kind: "set-int", key: action.set, stateVar };
330
- }
331
- if ("cycle" in action) {
332
- return {
333
- kind: "set-cycle",
334
- key: action.set,
335
- stateVar,
336
- members: action.cycle,
337
- };
338
- }
339
- return {
340
- kind: "set-bounded",
341
- key: action.set,
342
- by: action.by,
343
- };
344
- }
345
- if ("persist" in action) {
346
- const stateVar =
347
- CONFIG_KEY_TO_EFFECTIVE_VAR.get(action.persist) ?? action.persist;
348
- if ("to" in action) {
349
- return {
350
- kind: "persist-literal",
351
- key: action.persist,
352
- value: action.to,
353
- stateVar,
354
- };
355
- }
356
- if ("from" in action) {
357
- return {
358
- kind: "persist-option",
359
- key: action.persist,
360
- stateVar,
361
- options: [...resolveOptionDomain(action.from, perConfigDomains)],
362
- };
363
- }
364
- if ("cycle" in action) {
365
- return {
366
- kind: "persist-cycle",
367
- key: action.persist,
368
- stateVar,
369
- members: action.cycle,
370
- };
371
- }
372
- if ("removeSegment" in action) {
373
- return {
374
- kind: "layout-op",
375
- key: action.persist,
376
- op: { op: "remove", target: action.removeSegment },
377
- };
378
- }
379
- if ("insertSegment" in action) {
380
- return {
381
- kind: "layout-op",
382
- key: action.persist,
383
- op: {
384
- op: "insert",
385
- segment: action.insertSegment,
386
- anchor: action.anchor,
387
- relation: action.relation,
388
- },
389
- };
390
- }
391
- if ("insertSegmentFrom" in action) {
392
- return {
393
- kind: "layout-op-option",
394
- key: action.persist,
395
- anchor: action.anchor,
396
- relation: action.relation,
397
- options: [
398
- ...resolveOptionDomain(action.insertSegmentFrom, perConfigDomains),
399
- ],
400
- };
401
- }
402
- return {
403
- kind: "persist-bounded",
404
- key: action.persist,
405
- by: action.by,
406
- };
407
- }
408
- if ("copy" in action) {
409
- return {
410
- kind: "copy",
411
- text: parseActionTemplate(parse, action.copy, name),
412
- };
413
- }
414
- if ("open" in action) {
415
- return {
416
- kind: "open",
417
- target: parseActionTemplate(parse, action.open, name),
418
- };
419
- }
420
- if ("reset" in action) {
421
- return { kind: "reset", key: action.reset };
422
- }
423
- return "undo" in action ? { kind: "undo" } : { kind: "redo" };
424
- }
425
-
426
- // [LAW:one-source-of-truth] A dual control shows ONE current value and writes
427
- // relative to the value it showed — so both destinations read back through the
428
- // DURABLE half's variable, which is the `.effective` projection the daemon
429
- // resolved for this render (CONFIG_KEY_TO_EFFECTIVE_VAR above): the value the
430
- // bar is actually rendering with, whatever chain produced it. Reading the
431
- // session key instead would let a cycle's glyph name the effective state while
432
- // its click stepped from an unwritten session key — the toggle would render
433
- // "wrap: off" and write "false", a click that visibly does nothing. Arms that
434
- // carry no `stateVar` (the bounded steppers) read nothing at render by design:
435
- // their step is relative and resolved daemon-side.
436
- function compileDual(
437
- selectorVar: string,
438
- sessionKey: string,
439
- session: CompiledActionDecl,
440
- durable: CompiledActionDecl,
441
- ): CompiledActionDecl {
442
- const readBack =
443
- "stateVar" in session && "stateVar" in durable
444
- ? { ...session, stateVar: durable.stateVar }
445
- : session;
446
- return {
447
- kind: "dual",
448
- selector: selectorVar,
449
- session: readBack,
450
- durable,
451
- sessionKey,
452
- };
453
- }
454
-
455
- // [LAW:dataflow-not-control-flow] THE destination fold: which store a dual
456
- // action writes is the boolean value of its selector key, read from the same
457
- // live store the rest of the render reads. Total over every compiled action —
458
- // a single-destination action IS its own destination — so callers resolve
459
- // through it unconditionally and never test for the dual kind.
460
- //
461
- // [LAW:one-source-of-truth] `parseSessionBoolean` is the one spelling of a
462
- // boolean in SessionState (themes/policy.ts), the same parse `autoWrap`'s own
463
- // session half goes through: an unwritten, malformed, or "false" selector all
464
- // mean the session destination, and only a canonical "true" means durable.
465
- export function activeDestination(
466
- c: CompiledActionDecl,
467
- store: VariableStore,
468
- ): CompiledActionDecl {
469
- if (c.kind !== "dual") return c;
470
- return parseSessionBoolean(readVar(store, c.selector)) === true
471
- ? c.durable
472
- : c.session;
473
- }
474
-
475
- function parseActionTemplate(
476
- parse: (src: string) => Template<RichText>,
477
- src: string,
478
- name: string,
479
- ): Template<RichText> {
480
- try {
481
- return parse(src);
482
- } catch (e) {
483
- throw new Error(
484
- `Template parse error in actions.${name}: ${(e as Error).message}`,
485
- { cause: e },
486
- );
487
- }
488
- }
489
-
490
- // ─── Rendering ───────────────────────────────────────────────────────────────
491
-
492
- // [LAW:one-source-of-truth] Exported so the picker reads SessionState through the
493
- // SAME boundary (has() discriminates "never written" → "").
494
- export function readVar(store: VariableStore, name: string): string {
495
- // [LAW:no-defensive-null-guards] "current value may not exist" is a legitimate
496
- // state (the key was never written) — guard the store lookup, not a downstream
497
- // operation. has() is the discriminator; absence yields "".
498
- return store.has(name) ? varToString(store.read(name)) : "";
499
- }
500
-
501
- function evalTemplate(tpl: Template<RichText>, scope: object): string {
502
- return tpl
503
- .evaluate(scope)
504
- .map((f) => f.plain)
505
- .join("");
506
- }
507
-
508
- // [LAW:single-enforcer] One link-span constructor for both action and picker
509
- // cells — a Style carrying the OSC-8 url, `active` riding as bold.
510
- export function linkFragment(
511
- text: string,
512
- url: string,
513
- active: boolean,
514
- ): RichText {
515
- // [LAW:one-source-of-truth] Build the link span exactly as rich-js's `link`
516
- // does: a Style carrying the OSC-8 url. `active` rides as bold so the
517
- // currently-selected value reads as current — a value on the span, not a
518
- // branch in the walk.
519
- const rt = new RichText(text, {
520
- style: new Style({ link: url, bold: active }),
521
- });
522
- rt.noWrap = true;
523
- rt.end = "";
524
- return rt;
525
- }
526
-
527
- // [LAW:one-source-of-truth] THE "unknown current counts as the first member"
528
- // rule — the one resolution both the display selection and the successor write
529
- // fold over. Members are ordered default-state-first, so an unset/foreign value
530
- // renders the first display and clicks to the second member (an accordion
531
- // sibling's path "counts as closed", a never-written toggle "counts as off").
532
- function cycleIndex(
533
- c: Extract<CompiledActionDecl, { kind: "set-cycle" | "persist-cycle" }>,
534
- store: VariableStore,
535
- ): number {
536
- return Math.max(c.members.indexOf(readVar(store, c.stateVar)), 0);
537
- }
538
-
539
- // [LAW:dataflow-not-control-flow] The single total projection of a compiled action
540
- // onto (effect, active) — the click's wire effect plus whether this region is the
541
- // current selection. The template supplies `display` (the clickable text) and an
542
- // optional `boundValue` (an option picker binds each option's value); the action
543
- // declaration supplies everything else. Consumers never re-switch on the action
544
- // kind: this fold is the one place the union is matched.
545
- // • set-literal: writes its fixed value; active when the key already holds it.
546
- // • set-option: writes boundValue ?? display (the bound option); active when
547
- // the key already holds it (the picker's current-mark).
548
- // • set-bounded: emits a RELATIVE step-state nudge (key + signed by); never
549
- // reads current and never "active". The wrap + bounds + the
550
- // unset seed are applied at APPLY time by the daemon handler
551
- // reading live state, not snapshotted into the link here.
552
- // • copy/open: one copy/open effect of the evaluated template; never active.
553
- // [LAW:dataflow-not-control-flow] The template scope is an input only the copy/
554
- // open arms consume, so it is built WHERE consumed (buildScope snapshots
555
- // store.names() into a Set per call — paying it for a set-* region, e.g. every
556
- // cell of an option picker, is pure waste). set-* arms read individual vars
557
- // directly. This is data locality, not a control-flow guard: the scope simply
558
- // flows into the arms that need it.
559
- export function realize(
560
- c: CompiledActionDecl,
561
- display: string,
562
- boundValue: string | undefined,
563
- store: VariableStore,
564
- sessionId: string,
565
- ): { effects: readonly Effect[]; active: boolean } {
566
- switch (c.kind) {
567
- case "set-literal": {
568
- const current = readVar(store, c.stateVar);
569
- return {
570
- effects: [{ verb: VERB_SET_STATE, args: [sessionId, c.key, c.value] }],
571
- active: current === c.value,
572
- };
573
- }
574
- case "set-option": {
575
- const value = boundValue ?? display;
576
- const current = readVar(store, c.stateVar);
577
- return {
578
- effects: [{ verb: VERB_SET_STATE, args: [sessionId, c.key, value] }],
579
- active: current === value,
580
- };
581
- }
582
- case "set-int": {
583
- // The render binds the integer to write (a picker's page nav passes the
584
- // target page as boundValue; a bare `{{ action }}` passes its display).
585
- // [LAW:no-silent-failure] A bare `{{ action }}` on a set-int MUST render a
586
- // NUMERIC display (the manual "open at page 0" pattern: `{{ action "openMenu"
587
- // "0" }}`) — the display IS the value written, and the int gate
588
- // (makeIntValidator) rejects a non-integer at click with a loud "must be an
589
- // integer" BAD_REQUEST. There is no load-time check because the display is a
590
- // template evaluated at render (it may be dynamic), so the shape is enforced
591
- // at the wire, not silently coerced. active when the key already holds it.
592
- const value = boundValue ?? display;
593
- const current = readVar(store, c.stateVar);
594
- return {
595
- effects: [{ verb: VERB_SET_STATE, args: [sessionId, c.key, value] }],
596
- active: current === value,
597
- };
598
- }
599
- case "set-cycle": {
600
- // [LAW:one-source-of-truth] The same current-index resolution that picked
601
- // the rendered display picks the write target — display and write derive
602
- // from one read, so the click delivers exactly the transition the glyph
603
- // promised.
604
- const next = c.members[(cycleIndex(c, store) + 1) % c.members.length]!;
605
- return {
606
- effects: [{ verb: VERB_SET_STATE, args: [sessionId, c.key, next] }],
607
- active: false,
608
- };
609
- }
610
- case "set-bounded": {
611
- // [LAW:one-source-of-truth] Emit a RELATIVE nudge — the irreducible intent
612
- // (key + signed delta), never an absolute target derived from a render-time
613
- // snapshot of `current`. The daemon's step-state handler reads live state,
614
- // applies the wrap against the registry's bounds, and writes through the
615
- // single range gate. So the link is byte-identical across renders and N
616
- // rapid clicks each accumulate (the idempotent absolute-write bug is gone).
617
- return {
618
- effects: [
619
- {
620
- verb: VERB_STEP_STATE,
621
- args: [sessionId, c.key, String(c.by)],
622
- },
623
- ],
624
- active: false,
625
- };
626
- }
627
- case "copy":
628
- return {
629
- effects: [
630
- {
631
- verb: VERB_COPY,
632
- args: [evalTemplate(c.text, buildScope(store))],
633
- },
634
- ],
635
- active: false,
636
- };
637
- case "open":
638
- return {
639
- effects: [
640
- {
641
- verb: VERB_OPEN_VSCODE,
642
- args: [evalTemplate(c.target, buildScope(store))],
643
- },
644
- ],
645
- active: false,
646
- };
647
- // [LAW:one-source-of-truth] The persist-* arms mirror set-*'s realization
648
- // verbatim (same literal/option/cycle/bounded semantics), only the wire
649
- // verb differs (VERB_SET_CONFIG/VERB_STEP_CONFIG instead of
650
- // VERB_SET_STATE/VERB_STEP_STATE) — the daemon-side handler is what makes
651
- // the write durable, not the click itself.
652
- case "persist-literal": {
653
- const current = readVar(store, c.stateVar);
654
- return {
655
- effects: [{ verb: VERB_SET_CONFIG, args: [sessionId, c.key, c.value] }],
656
- active: current === c.value,
657
- };
658
- }
659
- case "persist-option": {
660
- const value = boundValue ?? display;
661
- const current = readVar(store, c.stateVar);
662
- return {
663
- effects: [{ verb: VERB_SET_CONFIG, args: [sessionId, c.key, value] }],
664
- active: current === value,
665
- };
666
- }
667
- case "persist-cycle": {
668
- const next = c.members[(cycleIndex(c, store) + 1) % c.members.length]!;
669
- return {
670
- effects: [{ verb: VERB_SET_CONFIG, args: [sessionId, c.key, next] }],
671
- active: false,
672
- };
673
- }
674
- case "persist-bounded": {
675
- return {
676
- effects: [
677
- {
678
- verb: VERB_STEP_CONFIG,
679
- args: [sessionId, c.key, String(c.by)],
680
- },
681
- ],
682
- active: false,
683
- };
684
- }
685
- case "reset":
686
- return {
687
- effects: [{ verb: VERB_RESET_CONFIG, args: [sessionId, c.key] }],
688
- active: false,
689
- };
690
- // [LAW:one-source-of-truth] No key to carry — the click just says "step
691
- // the history", and which entry moves is entirely server-side state
692
- // (never wire input, so there is nothing here to gate). Never "active":
693
- // a history step is a one-shot trigger, not a current-selection toggle.
694
- case "undo":
695
- return {
696
- effects: [{ verb: VERB_UNDO, args: [sessionId] }],
697
- active: false,
698
- };
699
- case "redo":
700
- return {
701
- effects: [{ verb: VERB_REDO, args: [sessionId] }],
702
- active: false,
703
- };
704
- // [LAW:one-source-of-truth] The op is fixed at compile time (see
705
- // compileAction) — the click just delivers it. `apply-layout-op`'s
706
- // handler does read-current-append-write (see verbs/index.ts), unlike
707
- // persist-literal's plain overwrite, so it is its own verb rather than
708
- // VERB_SET_CONFIG. Never "active": a structural edit is a one-shot
709
- // trigger, not a current-selection toggle.
710
- case "layout-op":
711
- return {
712
- effects: [
713
- {
714
- verb: VERB_APPLY_LAYOUT_OP,
715
- args: [sessionId, c.key, encodeLayoutOp(c.op)],
716
- },
717
- ],
718
- active: false,
719
- };
720
- // [LAW:one-source-of-truth] The picked option (boundValue ?? display — the
721
- // SAME resolution persist-option uses) becomes the op's `segment`; anchor/
722
- // relation are the compiled literals. Same wire shape a literal layout-op
723
- // emits, so the daemon's apply-layout-op handler and undo/redo need no
724
- // knowledge of where the segment name came from. Never "active": a
725
- // structural edit is a one-shot trigger, not a current-selection toggle.
726
- // [LAW:dataflow-not-control-flow] The destination is resolved to a value
727
- // and the SAME fold runs on it — a dual's realization is its chosen
728
- // half's realization, with nothing about persistence duplicated here.
729
- // Depth is structurally one: a dual's halves are the single-destination
730
- // decls actionDestinations built, which can never be dual themselves.
731
- //
732
- // [LAW:no-silent-failure] A DURABLE click carries the session key to
733
- // RELEASE as a trailing arg on its own write, so the daemon drops it only
734
- // after that write succeeded. Without the release the write would be
735
- // invisible to the session that made it — every settable global resolves
736
- // session pick OVER durable default, so the workflow this menu invites
737
- // ("try it here, then tick persist? to commit it") would set a default the
738
- // user cannot see and leave the control dead for the rest of the session.
739
- // Riding the write rather than sitting beside it is what makes the pair
740
- // unsplittable: a click runs every effect it carries, so a rejected write
741
- // must not be able to drop the pick on its own.
742
- case "dual": {
743
- const chosen = activeDestination(c, store);
744
- const { effects, active } = realize(
745
- chosen,
746
- display,
747
- boundValue,
748
- store,
749
- sessionId,
750
- );
751
- // The durable write carries the session key to RELEASE as one more
752
- // segment on itself, so the daemon clears it only after its own write
753
- // succeeded. A second effect beside it would not do: `dispatch` runs
754
- // every effect in a click by design, so a rejected write would still
755
- // wipe the session pick and leave nothing durable in its place.
756
- return chosen === c.durable
757
- ? {
758
- effects: effects.map((e) => ({
759
- ...e,
760
- args: [...e.args, c.sessionKey],
761
- })),
762
- active,
763
- }
764
- : { effects, active };
765
- }
766
- case "layout-op-option": {
767
- const segment = boundValue ?? display;
768
- const op: LayoutOp = {
769
- op: "insert",
770
- segment,
771
- anchor: c.anchor,
772
- relation: c.relation,
773
- };
774
- return {
775
- effects: [
776
- {
777
- verb: VERB_APPLY_LAYOUT_OP,
778
- args: [sessionId, c.key, encodeLayoutOp(op)],
779
- },
780
- ],
781
- active: false,
782
- };
783
- }
784
- }
785
- }
786
-
787
- // [LAW:dataflow-not-control-flow] Which text a region shows is a pure function
788
- // of (action kind, bound displays, current state). A cycle binds one display per
789
- // member positionally (the toggle/N-state-cycler form: `{{ action "t" "▸" "▾"
790
- // }}`) or one static display for all states; every other kind binds one display
791
- // plus an optional boundValue (the option-picker form). Wrong arity is an author
792
- // error surfaced loudly at render (composeWithDiagnostics shows it), never a
793
- // silently dropped argument.
794
- function selectDisplay(
795
- name: string,
796
- action: CompiledActionDecl,
797
- displays: readonly string[],
798
- store: VariableStore,
799
- ): { display: string; boundValue: string | undefined } {
800
- if (displays.length === 0) {
801
- throw new Error(`action "${name}" needs a display (the clickable text)`);
802
- }
803
- if (action.kind === "set-cycle" || action.kind === "persist-cycle") {
804
- // [LAW:single-enforcer] The arity rule and the pick are the disclosure
805
- // primitive's, not this file's — `{{ menu }}` resolves its own trigger
806
- // through the same function over its `[closed, member]` cycle, so the two
807
- // disclosure kinds cannot disagree about what a display binding means.
808
- const display = pickCycleDisplay(
809
- `action "${name}"`,
810
- displays,
811
- action.members.length,
812
- cycleIndex(action, store),
813
- );
814
- return { display, boundValue: undefined };
815
- }
816
- if (displays.length > 2) {
817
- throw new Error(
818
- `action "${name}" takes a display and an optional bound value, got ${displays.length} arguments (per-state displays are a cycle action's form)`,
819
- );
820
- }
821
- return { display: displays[0]!, boundValue: displays[1] };
822
- }
823
-
824
- // Realize a named action against the live state into ONE clickable RichText. The
825
- // `action` template function delegates here.
826
- export function renderAction(
827
- name: string,
828
- displays: readonly string[],
829
- runtime: ActionRuntime,
830
- ): RichText {
831
- const declared = runtime.compiled.get(name);
832
- // [LAW:no-defensive-null-guards] The loader validates every `{{ action "x" }}`
833
- // reference resolves to a declared action, and compileActions compiled every
834
- // declared action for THIS config's engine. A miss is a caller/wiring bug.
835
- if (!declared) {
836
- throw new Error(`action "${name}" is not declared in this config`);
837
- }
838
- const store = runtime.store;
839
- // [LAW:dataflow-not-control-flow] DISPLAY selection reads the resolved half
840
- // (a cycle's glyph is the current member's, whichever store it will write),
841
- // while REALIZATION is handed the declaration itself — a dual realizes as
842
- // its chosen half PLUS the session clear that keeps a durable write visible,
843
- // and that pairing belongs to the one fold that owns the union.
844
- const { display, boundValue } = selectDisplay(
845
- name,
846
- activeDestination(declared, store),
847
- displays,
848
- store,
849
- );
850
- const sessionId = readVar(store, "session.id");
851
- const { effects, active } = realize(
852
- declared,
853
- display,
854
- boundValue,
855
- store,
856
- sessionId,
857
- );
858
- return linkFragment(display, effectsUrl(effects), active);
859
- }
860
-
861
- // ─── FuncMap entry ─────────────────────────────────────────────────────────────
862
-
863
- // [LAW:dataflow-not-control-flow] One func; the action NAME selects which declared
864
- // effect fires, the trailing strings are the bound displays. For most kinds that
865
- // is the clickable text plus an optional boundValue (absent ⇒ the option IS the
866
- // display, the common picker form `{{ action "applyTheme" . }}`); for a cycle it
867
- // is one display per member (the current member's display renders) or one static
868
- // display. Returns T (RichText), the single fragment go-template-js emits for
869
- // `{{ action … }}`.
870
- //
871
- // [LAW:one-way-deps] The caller injects this FuncMap into createCcCandybarEngine
872
- // (capabilities-over-context) so the generic engine never imports the action
873
- // feature.
874
- export function actionFuncs(runtime: ActionRuntime): FuncMap {
875
- return {
876
- action: {
877
- fn: (name: string, ...displays: string[]) =>
878
- renderAction(name, displays, runtime),
879
- argTypes: ["string", "string"],
880
- returnType: "T",
881
- },
882
- };
883
- }