@promptctl/cc-candybar 1.32.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.
@@ -188,6 +188,36 @@ export function sanitizePersistedPresetOverride(
188
188
  return rest;
189
189
  }
190
190
 
191
+ // [LAW:no-silent-failure] sanitizePersistedPresetOverride's twin for the
192
+ // SAME machine-global overrides file, one field over: `presets.<name>.
193
+ // rootOps` entries persist by NAME, and that name only has meaning against
194
+ // the config it was clicked against. Loading a DIFFERENT project's config
195
+ // — one that never declared "compact", say — would otherwise reach
196
+ // applyPresetRootOpsOverrides -> presetRoot -> presetByName, which THROWS
197
+ // for an undeclared name (correctly so for a hand-AUTHORED preset root: a
198
+ // typo in a file the author can see). A stale overrides entry is not that
199
+ // — it is the expected shape of a file that outlives any one project's
200
+ // preset names — so it gets the SAME treatment sanitizePersistedPresetOverride
201
+ // already gives `globals.preset`: dropped before replay ever sees it,
202
+ // never a fatal error replacing an unrelated project's entire bar
203
+ // (brandon-layout-edit-2gc.5 PR review).
204
+ export function sanitizePersistedPresetRootOps(
205
+ presetRootOps: Readonly<Record<string, readonly string[]>>,
206
+ declaredPresets: Readonly<Record<string, PresetDecl>>,
207
+ ): Readonly<Record<string, readonly string[]>> {
208
+ const known = new Set(presetNames(declaredPresets));
209
+ const entries = Object.entries(presetRootOps).filter(([name]) =>
210
+ known.has(name),
211
+ );
212
+ // [LAW:carrying-cost] Identity return when nothing was dropped — the
213
+ // common case (this daemon serves one project, or every persisted name
214
+ // happens to be declared) costs one Set build and no new allocation.
215
+ if (entries.length === Object.keys(presetRootOps).length) {
216
+ return presetRootOps;
217
+ }
218
+ return Object.fromEntries(entries);
219
+ }
220
+
191
221
  // [LAW:one-source-of-truth] brandon-layout-edit-2gc.1's replay step — the
192
222
  // SAME "patch an already-merged config" shape applySegmentPaletteOverrides
193
223
  // (src/config/loader/merge.ts) uses one field over, run at the SAME point in
@@ -208,6 +238,48 @@ export function sanitizePersistedPresetOverride(
208
238
  // already removed it. A malformed individual token (decodeLayoutOp -> null;
209
239
  // can only arise from hand-edited or previous-version state, never from this
210
240
  // process's own encodeLayoutOp) is filtered the same way, never applied.
241
+ // [LAW:one-source-of-truth] brandon-layout-edit-2gc.5's diagnostic seam — the
242
+ // ONE definition of "does this preset's rendered layout currently differ
243
+ // from what's literally declared in the config file", read from the SAME
244
+ // raw op-log record applyPresetRootOpsOverrides replays (never re-derived by
245
+ // diffing the replayed tree against a fresh presetRoot() call, which would
246
+ // be a second, weaker definition — a structural coincidence between two
247
+ // unrelated edits could equal zero net ops and still disagree with the raw
248
+ // log's own length). A name absent from the record (never edited) and a
249
+ // name present with an empty token list (edited down to nothing, e.g. by
250
+ // undo) are the SAME "not customized" — presence in the record only ever
251
+ // means "this reload's overrides file had an entry", not "and it's non-
252
+ // empty" [LAW:no-defensive-null-guards].
253
+ //
254
+ // Decodes each token the SAME way applyPresetRootOpsOverrides does, rather
255
+ // than trusting the raw token COUNT — a token list where every token
256
+ // decodes to null (malformed, or written by a previous protocol version)
257
+ // is exactly the case applyPresetRootOpsOverrides itself treats as "no ops
258
+ // to replay" (`ops.length === 0 → continue`, leaving the root untouched).
259
+ // Counting raw tokens would disagree with that: "customized" would read
260
+ // true over a tree that is, in fact, byte-identical to the literal
261
+ // declared root — the inverted form of the drift this diagnostic exists to
262
+ // catch.
263
+ //
264
+ // [LAW:carrying-cost] Verifies DECODE only, not whether the decoded op's
265
+ // target/anchor still resolves against the CURRENT tree — that check would
266
+ // need this to duplicate applyLayoutOps' own stale-target resolution
267
+ // (a real walk of the tree) just to COUNT, on every render, a fact only a
268
+ // LATER hand-authored config edit can produce (removing a segment a stored
269
+ // op still names — see the stale-op comment on applyPresetRootOpsOverrides
270
+ // above). A rare, later-edit-triggered false "customized" is the honest,
271
+ // documented cost of keeping this an O(tokens) check rather than an O(tree)
272
+ // one; the doc at docs/interaction-authoring.md's "Knowing when a preset's
273
+ // layout has been edited" section states this same limit.
274
+ export function presetIsCustomized(
275
+ presetRootOps: Readonly<Record<string, readonly string[]>>,
276
+ name: string,
277
+ ): boolean {
278
+ const tokens = presetRootOps[name];
279
+ if (tokens === undefined) return false;
280
+ return tokens.some((t) => decodeLayoutOp(t) !== null);
281
+ }
282
+
211
283
  export function applyPresetRootOpsOverrides(
212
284
  config: DslConfig,
213
285
  presetRootOps: Readonly<Record<string, readonly string[]>>,
@@ -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