@promptctl/cc-candybar 1.33.0 → 1.34.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.
@@ -28,6 +28,7 @@ import { configOverridesPath } from "../paths.js";
28
28
  import {
29
29
  applyPresetRootOpsOverrides,
30
30
  sanitizePersistedPresetOverride,
31
+ sanitizePersistedPresetRootOps,
31
32
  } from "../../config/presets.js";
32
33
  import { VariableStore } from "../../var-system/store.js";
33
34
  import { SourceRegistry } from "../../var-system/sources.js";
@@ -85,6 +86,21 @@ export interface DslRenderState {
85
86
  readonly compiled: CompiledConfig;
86
87
  readonly neededInputPaths: ReadonlySet<string>;
87
88
  readonly lastRenderCellsBySegment: Map<string, readonly RichText[]>;
89
+ // [LAW:one-source-of-truth] The accumulated-ops record this reload read
90
+ // from the overrides file, sanitized against THIS config's declared
91
+ // presets (sanitizePersistedPresetRootOps — never the raw file content: a
92
+ // stale entry naming a preset from a different project must not surface
93
+ // as "customized" here either) — the exact input
94
+ // applyPresetRootOpsOverrides replayed into `config.presets[name].root`
95
+ // above. Carried alongside the replayed config because the replay
96
+ // CONSUMES the op count: by the time a preset's tree is spliced/
97
+ // validated, nothing about it says how many ops (if any) produced it.
98
+ // brandon-layout-edit-2gc.5 reads this per render (keyed by the active
99
+ // preset name) to answer "does the bar's current arrangement differ from
100
+ // what's literally in the user's file" without a second overrides read
101
+ // (this entry rebuilds on the SAME watcher that rebuilds `config`, so the
102
+ // two never drift).
103
+ readonly presetRootOps: Readonly<Record<string, readonly string[]>>;
88
104
  // [LAW:single-enforcer] Disposers for the SessionState validators this config
89
105
  // installed (derived from its action table). Disposed on swap/eviction in the
90
106
  // same dispose-before-swap transaction as the SourceRegistry, so a reload
@@ -327,6 +343,19 @@ export class RenderCache {
327
343
  withGlobalsOverrides,
328
344
  overrides.segmentPalette,
329
345
  );
346
+ // [LAW:no-silent-failure] Drop any entry naming a preset THIS config
347
+ // never declared before replay ever sees it — the shared overrides file
348
+ // outlives any one project's preset names (brandon-layout-edit-2gc.5 PR
349
+ // review: without this, a stale entry from a DIFFERENT project reaches
350
+ // applyPresetRootOpsOverrides -> presetRoot -> presetByName, which
351
+ // throws for an undeclared name, failing this unrelated project's
352
+ // ENTIRE render). Sanitized against `merged.presets` — the SAME config
353
+ // sanitizePersistedPresetOverride checks `globals.preset` against, two
354
+ // lines up, for the identical reason.
355
+ const sanitizedPresetRootOps = sanitizePersistedPresetRootOps(
356
+ overrides.presetRootOps,
357
+ merged.presets,
358
+ );
330
359
  // [LAW:one-source-of-truth] brandon-layout-edit-2gc.1's replay step —
331
360
  // the SAME "patch an already-merged config" cascade as the segment-
332
361
  // palette overlay above, one field over (a preset's `root` instead of a
@@ -337,7 +366,7 @@ export class RenderCache {
337
366
  // be.
338
367
  const withPresetRootOps = applyPresetRootOpsOverrides(
339
368
  withOverrides,
340
- overrides.presetRootOps,
369
+ sanitizedPresetRootOps,
341
370
  );
342
371
  const config = validateConfig(
343
372
  withPresetRootOps,
@@ -414,6 +443,7 @@ export class RenderCache {
414
443
  neededInputPaths: buildNeededPrefixes(config),
415
444
  lastRenderCellsBySegment: new Map<string, readonly RichText[]>(),
416
445
  validatorDisposers,
446
+ presetRootOps: sanitizedPresetRootOps,
417
447
  };
418
448
  }
419
449
 
@@ -61,6 +61,15 @@ export interface EffectiveGlobals {
61
61
  // carried alongside so a menu label states the arrangement that actually
62
62
  // rendered [LAW:one-source-of-truth].
63
63
  readonly preset: string;
64
+ // [LAW:one-source-of-truth] brandon-layout-edit-2gc.5 — the SAME "does the
65
+ // active preset have accumulated rootOps right now" fact
66
+ // presetIsCustomized derives, resolved alongside `preset` (not a second
67
+ // lookup later) because both come from the SAME entry.state read: the
68
+ // resolved name and the resolved op-log presence must trace to one
69
+ // rebuild, or a diagnostic could name the wrong preset after a reload
70
+ // lands mid-render. Not itself a display value either — see `preset`'s
71
+ // own comment.
72
+ readonly presetCustomized: boolean;
64
73
  readonly style: StripStyle;
65
74
  readonly charset: Charset;
66
75
  readonly colorCompatibility: ColorCompatibility;
@@ -108,7 +117,13 @@ export interface RenderPayload extends ClaudeHookData {
108
117
  // and look's twin one level up: the SAME name that selected the layout this
109
118
  // render walked and the globals it rendered with, surfaced so a preset
110
119
  // trigger's label can never claim an arrangement the bar is not in.
111
- readonly preset: { readonly effective: string };
120
+ // [LAW:one-source-of-truth] brandon-layout-edit-2gc.5 `customized` rides
121
+ // alongside `effective` rather than as a sibling top-level field, mirroring
122
+ // the shape a `when`-gated status segment reads (`.preset.customized`)
123
+ // beside the trigger's own `.preset.effective` label. Required for the
124
+ // same reason as `effective`: resolved unconditionally per render from
125
+ // presetIsCustomized, never absent.
126
+ readonly preset: { readonly effective: string; readonly customized: boolean };
112
127
  // [LAW:one-type-per-behavior] style/charset/colorCompatibility/autoWrap/
113
128
  // padding are theme/look's twins over the remaining persistable globals
114
129
  // (candybar-config-engine-71o.3) — each REQUIRED and unconditionally
@@ -858,7 +873,10 @@ export async function buildRenderPayload(
858
873
  // hand) and a config reading e.g. `.padding.effective` must always find it.
859
874
  theme: { effective: effective.theme },
860
875
  look: { effective: effective.look },
861
- preset: { effective: effective.preset },
876
+ preset: {
877
+ effective: effective.preset,
878
+ customized: effective.presetCustomized,
879
+ },
862
880
  style: { effective: effective.style },
863
881
  charset: { effective: effective.charset },
864
882
  colorCompatibility: { effective: effective.colorCompatibility },
@@ -70,7 +70,11 @@ import {
70
70
  lookKeyByName,
71
71
  paletteForThemeName,
72
72
  } from "../themes/index.js";
73
- import { effectivePresetName, presetGlobals } from "../config/presets.js";
73
+ import {
74
+ effectivePresetName,
75
+ presetGlobals,
76
+ presetIsCustomized,
77
+ } from "../config/presets.js";
74
78
  import {
75
79
  renderStripCells,
76
80
  DEFAULT_CHARSET,
@@ -888,6 +892,15 @@ async function handleRequest(req: Request): Promise<HandledRequest> {
888
892
  const globals = presetGlobals(entry.state.config, preset);
889
893
  const effective: EffectiveGlobals = {
890
894
  preset,
895
+ // [LAW:one-source-of-truth] brandon-layout-edit-2gc.5 — read from
896
+ // THIS entry's own presetRootOps (the record that fed the SAME
897
+ // reload that produced entry.state.config), never a fresh
898
+ // loadOverrides() here — a second read could race a concurrent
899
+ // write and disagree with the tree that actually rendered.
900
+ presetCustomized: presetIsCustomized(
901
+ entry.state.presetRootOps,
902
+ preset,
903
+ ),
891
904
  theme: effectiveThemeName(
892
905
  sessionState.get(req.hookData.session_id, "theme"),
893
906
  globals.palette,
@@ -19,6 +19,10 @@ import { addableSegmentDomains } from "../../config/edit-chrome";
19
19
  import type { DslConfig } from "../../config/dsl-types";
20
20
  import { isGlobalsField } from "../config-overrides-store";
21
21
  import { encodeLayoutOp } from "../../config/layout-ops";
22
+ import {
23
+ parsePersistTarget,
24
+ presetRootOpsKey,
25
+ } from "../../config/loader/persist-target";
22
26
  import {
23
27
  clampSeed,
24
28
  createValidatorRegistry,
@@ -168,6 +172,43 @@ function configKeySeeds(config: DslConfig): ReadonlyMap<string, number> {
168
172
  return seeds;
169
173
  }
170
174
 
175
+ // [LAW:one-source-of-truth] Every preset a config's action table ALREADY
176
+ // targets via a `presets.<name>.rootOps` key (persist OR reset — the
177
+ // contribution is keyed off intent to use structural editing for that
178
+ // preset, not off "this config has any presets block") stays a
179
+ // registered key EVEN when that preset's CURRENT tree has no
180
+ // addable/removable segment for removeChrome/insertChrome to contribute
181
+ // from. Without this, a preset edited down to zero non-exempt segments
182
+ // (spliceContainer then contributes nothing for it at all) would orphan
183
+ // its OWN reset action: the one affordance meant to undo a fully-emptied
184
+ // preset would throw "unknown config key" at the exact moment it's needed
185
+ // most (brandon-layout-edit-2gc.5 PR review). An EMPTY allow-list
186
+ // registers the key (so `reset-config`'s membership check —
187
+ // src/daemon/verbs/index.ts's resetConfig — passes) without granting any
188
+ // illegitimate WRITE: a real persist write still needs a real
189
+ // removeSegment/insertSegment/insertSegmentFrom action elsewhere;
190
+ // mergeContributions unions an empty array with whatever those contribute.
191
+ //
192
+ // [LAW:no-mode-explosion] Deliberately narrower than "every declared
193
+ // preset" — a config with a `presets` block but ZERO persist/reset actions
194
+ // over it has no structural-editing surface at all, so it registers
195
+ // nothing here, preserving this module's own "zero baseline keys" floor
196
+ // (a globals field, and now a preset's rootOps, is writable only because
197
+ // SOME action names it).
198
+ function presetRootOpsContributions(config: DslConfig): KeySpecContribution[] {
199
+ const presets = new Set<string>();
200
+ for (const a of Object.values(config.actions)) {
201
+ const key = "persist" in a ? a.persist : "reset" in a ? a.reset : null;
202
+ if (key === null) continue;
203
+ const target = parsePersistTarget(key);
204
+ if (target?.scope === "preset-root-ops") presets.add(target.preset);
205
+ }
206
+ return [...presets].map((name) => ({
207
+ key: presetRootOpsKey(name),
208
+ spec: { kind: "allow-list", allowed: [] },
209
+ }));
210
+ }
211
+
171
212
  function actionContributions(config: DslConfig): KeySpecContribution[] {
172
213
  const seeds = configKeySeeds(config);
173
214
  // [LAW:one-source-of-truth] The "addable segment" domains
@@ -180,9 +221,12 @@ function actionContributions(config: DslConfig): KeySpecContribution[] {
180
221
  ...perConfigDomainsFor(config),
181
222
  ...addableSegmentDomains(config),
182
223
  ]);
183
- return Object.values(config.actions).flatMap((a) =>
184
- actionKeySpecs(a, seeds, perConfigDomains),
185
- );
224
+ return [
225
+ ...presetRootOpsContributions(config),
226
+ ...Object.values(config.actions).flatMap((a) =>
227
+ actionKeySpecs(a, seeds, perConfigDomains),
228
+ ),
229
+ ];
186
230
  }
187
231
 
188
232
  // [LAW:single-enforcer] The SOLE install-site derivation: a config's