@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.
- package/dist/index.mjs +26 -26
- package/package.json +5 -5
- package/src/check.ts +99 -30
- package/src/config/default-dsl-config.ts +38 -2
- package/src/config/edit-chrome.ts +93 -10
- package/src/config/ident.ts +22 -0
- package/src/config/loader/cross-ref.ts +51 -0
- package/src/config/loader/persist-target.ts +20 -1
- package/src/config/loader/presets.ts +14 -2
- package/src/config/menu-keys.ts +2 -8
- package/src/config/presets.ts +72 -0
- package/src/daemon/cache/render.ts +31 -1
- package/src/daemon/render-payload.ts +20 -2
- package/src/daemon/server.ts +14 -1
- package/src/daemon/verbs/config-validators.ts +47 -3
package/src/config/presets.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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: {
|
|
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 },
|
package/src/daemon/server.ts
CHANGED
|
@@ -70,7 +70,11 @@ import {
|
|
|
70
70
|
lookKeyByName,
|
|
71
71
|
paletteForThemeName,
|
|
72
72
|
} from "../themes/index.js";
|
|
73
|
-
import {
|
|
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
|
|
184
|
-
|
|
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
|