@promptctl/cc-candybar 1.29.0 → 1.30.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.
@@ -25,7 +25,10 @@ import {
25
25
  } from "../verbs/config-validators.js";
26
26
  import { loadOverrides } from "../config-overrides-store.js";
27
27
  import { configOverridesPath } from "../paths.js";
28
- import { sanitizePersistedPresetOverride } from "../../config/presets.js";
28
+ import {
29
+ applyPresetRootOpsOverrides,
30
+ sanitizePersistedPresetOverride,
31
+ } from "../../config/presets.js";
29
32
  import { VariableStore } from "../../var-system/store.js";
30
33
  import { SourceRegistry } from "../../var-system/sources.js";
31
34
  import type { GitDataProvider } from "./git.js";
@@ -324,8 +327,20 @@ export class RenderCache {
324
327
  withGlobalsOverrides,
325
328
  overrides.segmentPalette,
326
329
  );
327
- const config = validateConfig(
330
+ // [LAW:one-source-of-truth] brandon-layout-edit-2gc.1's replay step —
331
+ // the SAME "patch an already-merged config" cascade as the segment-
332
+ // palette overlay above, one field over (a preset's `root` instead of a
333
+ // segment's `palette`). Runs last so validateConfig's cross-ref walk
334
+ // proves the OPS-PATCHED tree, not the pre-edit one — a structural edit
335
+ // that referenced a segment removed by a later config change is caught
336
+ // exactly like a hand-authored preset root naming the same segment would
337
+ // be.
338
+ const withPresetRootOps = applyPresetRootOpsOverrides(
328
339
  withOverrides,
340
+ overrides.presetRootOps,
341
+ );
342
+ const config = validateConfig(
343
+ withPresetRootOps,
329
344
  resolvedPath ?? "<default>",
330
345
  source,
331
346
  );
@@ -100,7 +100,17 @@ export function coercePersistValue(
100
100
  `coercePersistValue: "${key}" is not a valid persist target`,
101
101
  );
102
102
  }
103
- if (target.scope === "segment-palette") return raw;
103
+ // [LAW:one-type-per-behavior] Both non-globals scopes are always a NAME/
104
+ // TOKEN string — segment-palette's value is a palette name, preset-root-ops'
105
+ // is one op token appended by the daemon's apply-layout-op verb handler
106
+ // (never a bare `persist` write — see verbs/index.ts). Neither has a
107
+ // GLOBALS_FIELD_KIND row because neither is a Globals field.
108
+ if (
109
+ target.scope === "segment-palette" ||
110
+ target.scope === "preset-root-ops"
111
+ ) {
112
+ return raw;
113
+ }
104
114
  const kind = GLOBALS_FIELD_KIND[target.field];
105
115
  if (kind === "string") return raw;
106
116
  if (kind === "number") {
@@ -136,9 +146,7 @@ function isValidOverrides(
136
146
  const target = parsePersistTarget(key);
137
147
  if (target === null) return false;
138
148
  const kind =
139
- target.scope === "segment-palette"
140
- ? "string"
141
- : GLOBALS_FIELD_KIND[target.field];
149
+ target.scope === "globals" ? GLOBALS_FIELD_KIND[target.field] : "string";
142
150
  if (kind === "number" && typeof v !== "number") return false;
143
151
  if (kind === "boolean" && typeof v !== "boolean") return false;
144
152
  if (kind === "string" && typeof v !== "string") return false;
@@ -237,6 +245,46 @@ function projectSegmentPaletteOverrides(
237
245
  return out;
238
246
  }
239
247
 
248
+ // [LAW:one-source-of-truth] The preset-root-ops-scoped VIEW of the SAME raw
249
+ // dict — preset name -> the accumulated op-token LIST (brandon-layout-edit-
250
+ // 2gc.1's structural-edit log; see src/config/layout-ops.ts). This is a
251
+ // SHAPE check only (well-formed JSON array of strings) — decoding each
252
+ // token into a typed LayoutOp, and applying the ops to a tree, is presets.ts's
253
+ // job, not this storage-layer module's [LAW:decomposition]. A stored value
254
+ // that isn't a JSON array of strings drops for THAT preset only (a warn log,
255
+ // never a crash of the whole overrides file) — the identical "the world
256
+ // moved on since this was written" recovery projectSegmentPaletteOverrides
257
+ // already gets, one level narrower.
258
+ function projectPresetRootOpsOverrides(
259
+ raw: Readonly<Record<string, string | number | boolean>>,
260
+ logger: DaemonLogger,
261
+ ): Readonly<Record<string, readonly string[]>> {
262
+ const out: Record<string, readonly string[]> = Object.create(null) as Record<
263
+ string,
264
+ readonly string[]
265
+ >;
266
+ for (const [key, value] of Object.entries(raw)) {
267
+ const target = parsePersistTarget(key);
268
+ if (target?.scope !== "preset-root-ops" || typeof value !== "string") {
269
+ continue;
270
+ }
271
+ try {
272
+ const parsed: unknown = JSON.parse(value);
273
+ if (Array.isArray(parsed) && parsed.every((t) => typeof t === "string")) {
274
+ out[target.preset] = parsed;
275
+ continue;
276
+ }
277
+ } catch {
278
+ // fall through to the warn below
279
+ }
280
+ logger(
281
+ "warn",
282
+ `config-overrides: "${key}" is not a valid op-token list, dropping`,
283
+ );
284
+ }
285
+ return out;
286
+ }
287
+
240
288
  export function loadConfigOverrides(
241
289
  filePath: string,
242
290
  logger: DaemonLogger = quietLogger,
@@ -251,14 +299,15 @@ export function loadSegmentPaletteOverrides(
251
299
  return projectSegmentPaletteOverrides(loadRawOverrides(filePath, logger));
252
300
  }
253
301
 
254
- // [LAW:carrying-cost] RenderCache wants BOTH views on every reload
255
- // (buildState merges globals overrides, then overlays segment-palette
256
- // overrides) — calling loadConfigOverrides + loadSegmentPaletteOverrides
257
- // back to back would read, parse, and shape-validate the same tiny file
258
- // twice per reload for no reason. One read, two projections.
302
+ // [LAW:carrying-cost] RenderCache wants ALL THREE views on every reload
303
+ // (buildState merges globals overrides, overlays segment-palette overrides,
304
+ // then replays preset-root-ops overrides) — calling the scoped loaders back
305
+ // to back would read, parse, and shape-validate the same tiny file three
306
+ // times per reload for no reason. One read, three projections.
259
307
  export interface Overrides {
260
308
  readonly globals: Partial<Globals>;
261
309
  readonly segmentPalette: Readonly<Record<string, string>>;
310
+ readonly presetRootOps: Readonly<Record<string, readonly string[]>>;
262
311
  }
263
312
 
264
313
  export function loadOverrides(
@@ -269,6 +318,7 @@ export function loadOverrides(
269
318
  return {
270
319
  globals: projectGlobalsOverrides(raw),
271
320
  segmentPalette: projectSegmentPaletteOverrides(raw),
321
+ presetRootOps: projectPresetRootOpsOverrides(raw, logger),
272
322
  };
273
323
  }
274
324
 
@@ -17,6 +17,7 @@ import {
17
17
  } from "../../config/option-domain";
18
18
  import type { DslConfig } from "../../config/dsl-types";
19
19
  import { isGlobalsField } from "../config-overrides-store";
20
+ import { encodeLayoutOp } from "../../config/layout-ops";
20
21
  import {
21
22
  clampSeed,
22
23
  createValidatorRegistry,
@@ -73,6 +74,43 @@ function actionKeySpecs(
73
74
  if ("cycle" in a) {
74
75
  return [{ key: a.persist, spec: { kind: "allow-list", allowed: a.cycle } }];
75
76
  }
77
+ // [LAW:single-enforcer] brandon-layout-edit-2gc.1's structural-edit arms:
78
+ // the op is fully literal at config-author time (removeSegment's target,
79
+ // insertSegment's segment/anchor/relation), so — exactly like a literal
80
+ // `to` — there is exactly ONE legal value this declared action can ever
81
+ // request: its own encoded op token. Multiple layout actions targeting the
82
+ // same "presets.<name>.rootOps" key each contribute one allow-list member,
83
+ // unioned by mergeContributions below, same as multiple `to` actions on
84
+ // one key already do.
85
+ if ("removeSegment" in a) {
86
+ return [
87
+ {
88
+ key: a.persist,
89
+ spec: {
90
+ kind: "allow-list",
91
+ allowed: [encodeLayoutOp({ op: "remove", target: a.removeSegment })],
92
+ },
93
+ },
94
+ ];
95
+ }
96
+ if ("insertSegment" in a) {
97
+ return [
98
+ {
99
+ key: a.persist,
100
+ spec: {
101
+ kind: "allow-list",
102
+ allowed: [
103
+ encodeLayoutOp({
104
+ op: "insert",
105
+ segment: a.insertSegment,
106
+ anchor: a.anchor,
107
+ relation: a.relation,
108
+ }),
109
+ ],
110
+ },
111
+ },
112
+ ];
113
+ }
76
114
  return [
77
115
  {
78
116
  key: a.persist,
@@ -36,12 +36,15 @@ import {
36
36
  coercePersistValue,
37
37
  isGlobalsField,
38
38
  loadConfigOverrides,
39
+ loadOverrides,
39
40
  writeConfigOverride,
40
41
  } from "../config-overrides-store";
41
42
  import { configOverridesPath } from "../paths";
43
+ import { parsePersistTarget } from "../../config/loader/persist-target";
42
44
  import {
43
45
  decodeSegments,
44
46
  parseEffects,
47
+ VERB_APPLY_LAYOUT_OP,
45
48
  VERB_COPY,
46
49
  VERB_DISPATCH,
47
50
  VERB_OPEN_VSCODE,
@@ -444,6 +447,48 @@ const resetConfig: VerbHandler = (value, ctx) => {
444
447
  ctx.dlog("info", `reset-config: ${key} (session=${sid})`);
445
448
  };
446
449
 
450
+ // [LAW:one-source-of-truth] brandon-layout-edit-2gc.1's structural-edit
451
+ // write: a THIRD config-overrides write shape beside setConfig's overwrite
452
+ // and stepConfig's numeric read-modify-write — read the current op-token
453
+ // list at `key`, append the validated op, write the whole list back. Gated
454
+ // by the SAME allow-list machinery setConfig uses (validateConfigWrite,
455
+ // derived from a config's declared removeSegment/insertSegment actions) —
456
+ // an op token no action declares is a loud BAD_REQUEST, never silently
457
+ // appended. `key` must resolve to the preset-root-ops scope specifically
458
+ // (never a globals/segment-palette key smuggled in through this verb) —
459
+ // checked here rather than trusted from the gate, since the gate only
460
+ // proves the VALUE is allowed for that key, not that the key's SCOPE
461
+ // matches this verb's read-modify-write shape.
462
+ const applyLayoutOp: VerbHandler = (rawValue, ctx) => {
463
+ const [sessionId = "", key = "", opToken = ""] = decodeWire(() =>
464
+ decodeSegments(rawValue),
465
+ );
466
+ const sid = requireSessionId(sessionId);
467
+ if (!key) {
468
+ throw new BadVerbArgs(
469
+ `apply-layout-op: <key>/<op> is required (have: ${listConfigKeys().join(", ")})`,
470
+ );
471
+ }
472
+ const result = validateConfigWrite(key, opToken);
473
+ if (!result.ok) throw new BadVerbArgs(`apply-layout-op: ${result.reason}`);
474
+ const target = parsePersistTarget(key);
475
+ if (target === null || target.scope !== "preset-root-ops") {
476
+ throw new BadVerbArgs(
477
+ `apply-layout-op: "${key}" is not a "presets.<name>.rootOps" target`,
478
+ );
479
+ }
480
+ const existing =
481
+ loadOverrides(configOverridesPath(), ctx.dlog).presetRootOps[
482
+ target.preset
483
+ ] ?? [];
484
+ const next = JSON.stringify([...existing, result.value]);
485
+ writeConfigOverride(configOverridesPath(), key, next, ctx.dlog);
486
+ ctx.dlog(
487
+ "info",
488
+ `apply-layout-op: ${key} += ${result.value} (session=${sid})`,
489
+ );
490
+ };
491
+
447
492
  // ─── Registry ───────────────────────────────────────────────────────────────
448
493
 
449
494
  // [LAW:one-source-of-truth] The LEAF verbs — every click effect that does real
@@ -505,6 +550,7 @@ const LEAF_VERBS = new Map<string, VerbHandler>([
505
550
  [VERB_SET_CONFIG, setConfig],
506
551
  [VERB_STEP_CONFIG, stepConfig],
507
552
  [VERB_RESET_CONFIG, resetConfig],
553
+ [VERB_APPLY_LAYOUT_OP, applyLayoutOp],
508
554
  [VERB_SHOW_CONFIG_ERROR, showConfigError],
509
555
  [VERB_SHOW_CONFIG_WARNING, showConfigWarning],
510
556
  [VERB_TOOLBAR_TOGGLE, toolbarToggle],
@@ -536,9 +582,9 @@ const dispatch: VerbHandler = (rawValue, ctx) => {
536
582
  let sessionId: string | null = null;
537
583
  for (const { verb, value } of parseEffects(rawValue)) {
538
584
  // Extract session ID from the first session-bearing effect for error display.
539
- // set-state, step-state, set-config, step-config, reset-config, and
540
- // toolbar-toggle all carry the session id as their first segment, so a
541
- // failing step surfaces in the bar like any other.
585
+ // set-state, step-state, set-config, step-config, reset-config,
586
+ // apply-layout-op, and toolbar-toggle all carry the session id as their
587
+ // first segment, so a failing step surfaces in the bar like any other.
542
588
  if (
543
589
  !sessionId &&
544
590
  (verb === VERB_SET_STATE ||
@@ -546,6 +592,7 @@ const dispatch: VerbHandler = (rawValue, ctx) => {
546
592
  verb === VERB_SET_CONFIG ||
547
593
  verb === VERB_STEP_CONFIG ||
548
594
  verb === VERB_RESET_CONFIG ||
595
+ verb === VERB_APPLY_LAYOUT_OP ||
549
596
  verb === VERB_TOOLBAR_TOGGLE)
550
597
  ) {
551
598
  const parts = decodeSegments(value);
@@ -27,9 +27,11 @@ import { toString as varToString } from "../var-system/types.js";
27
27
  import { buildScope } from "../template-engine/scope.js";
28
28
  import type { ActionDecl } from "../config/action.js";
29
29
  import { resolveOptionDomain } from "../config/option-domain.js";
30
+ import { encodeLayoutOp, type LayoutOp } from "../config/layout-ops.js";
30
31
  import type { StripStyle } from "../themes/policy.js";
31
32
  import {
32
33
  effectsUrl,
34
+ VERB_APPLY_LAYOUT_OP,
33
35
  VERB_COPY,
34
36
  VERB_OPEN_VSCODE,
35
37
  VERB_RESET_CONFIG,
@@ -135,7 +137,12 @@ export type CompiledActionDecl =
135
137
  // one config-overrides key. Carries only the key — there is no value to
136
138
  // realize, so it shares copy/open's "no gate" shape at compile time (the
137
139
  // GATE is the key-membership check the reset-config verb handler applies).
138
- | { readonly kind: "reset"; readonly key: string };
140
+ | { readonly kind: "reset"; readonly key: string }
141
+ // [LAW:one-source-of-truth] brandon-layout-edit-2gc.1's structural-edit
142
+ // arms. Fully literal at compile time (the op IS the declaration — no
143
+ // template-bound option, unlike persist-option), so `op` is precomputed
144
+ // here rather than reconstructed from raw fields at every realize() call.
145
+ | { readonly kind: "layout-op"; readonly key: string; readonly op: LayoutOp };
139
146
 
140
147
  export type CompiledActions = ReadonlyMap<string, CompiledActionDecl>;
141
148
 
@@ -294,6 +301,25 @@ function compileAction(
294
301
  members: action.cycle,
295
302
  };
296
303
  }
304
+ if ("removeSegment" in action) {
305
+ return {
306
+ kind: "layout-op",
307
+ key: action.persist,
308
+ op: { op: "remove", target: action.removeSegment },
309
+ };
310
+ }
311
+ if ("insertSegment" in action) {
312
+ return {
313
+ kind: "layout-op",
314
+ key: action.persist,
315
+ op: {
316
+ op: "insert",
317
+ segment: action.insertSegment,
318
+ anchor: action.anchor,
319
+ relation: action.relation,
320
+ },
321
+ };
322
+ }
297
323
  return {
298
324
  kind: "persist-bounded",
299
325
  key: action.persist,
@@ -522,6 +548,20 @@ function realize(
522
548
  effect: { verb: VERB_RESET_CONFIG, args: [sessionId, c.key] },
523
549
  active: false,
524
550
  };
551
+ // [LAW:one-source-of-truth] The op is fixed at compile time (see
552
+ // compileAction) — the click just delivers it. `apply-layout-op`'s
553
+ // handler does read-current-append-write (see verbs/index.ts), unlike
554
+ // persist-literal's plain overwrite, so it is its own verb rather than
555
+ // VERB_SET_CONFIG. Never "active": a structural edit is a one-shot
556
+ // trigger, not a current-selection toggle.
557
+ case "layout-op":
558
+ return {
559
+ effect: {
560
+ verb: VERB_APPLY_LAYOUT_OP,
561
+ args: [sessionId, c.key, encodeLayoutOp(c.op)],
562
+ },
563
+ active: false,
564
+ };
525
565
  }
526
566
  }
527
567