@promptctl/cc-candybar 1.35.0 → 1.37.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 +58 -58
- package/package.json +5 -5
- package/src/check.ts +4 -4
- package/src/config/default-dsl-config.ts +23 -8
- package/src/config/dsl-loader.ts +20 -1
- package/src/config/edit-chrome.ts +62 -22
- package/src/config/loader/cross-ref.ts +44 -0
- package/src/config/loader/edit-mode.ts +14 -0
- package/src/config/loader/globals.ts +6 -1
- package/src/config/presets.ts +8 -3
- package/src/config/settings-menu.ts +373 -0
- package/src/daemon/render-payload.ts +7 -5
- package/src/daemon/server.ts +20 -7
- package/src/demo/dsl.ts +7 -6
- package/src/render/strip.ts +6 -10
- package/src/themes/index.ts +2 -0
- package/src/themes/policy.ts +182 -40
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@promptctl/cc-candybar",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.37.0",
|
|
4
4
|
"description": "Statusline renderer for Claude Code — a JSON5-configurable DSL with daemon-cached data sources, byte-clean palette-aware composition, and OSC8 click verbs.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.mjs",
|
|
@@ -91,9 +91,9 @@
|
|
|
91
91
|
"mobx": "^6.15.0"
|
|
92
92
|
},
|
|
93
93
|
"optionalDependencies": {
|
|
94
|
-
"@promptctl/cc-candybar-darwin-arm64": "1.
|
|
95
|
-
"@promptctl/cc-candybar-darwin-x64": "1.
|
|
96
|
-
"@promptctl/cc-candybar-linux-x64": "1.
|
|
97
|
-
"@promptctl/cc-candybar-linux-arm64": "1.
|
|
94
|
+
"@promptctl/cc-candybar-darwin-arm64": "1.37.0",
|
|
95
|
+
"@promptctl/cc-candybar-darwin-x64": "1.37.0",
|
|
96
|
+
"@promptctl/cc-candybar-linux-x64": "1.37.0",
|
|
97
|
+
"@promptctl/cc-candybar-linux-arm64": "1.37.0"
|
|
98
98
|
}
|
|
99
99
|
}
|
package/src/check.ts
CHANGED
|
@@ -36,14 +36,14 @@ import {
|
|
|
36
36
|
effectiveLookName,
|
|
37
37
|
lookKeyByName,
|
|
38
38
|
effectiveStripStyle,
|
|
39
|
+
effectiveAutoWrap,
|
|
40
|
+
effectivePadding,
|
|
39
41
|
} from "./themes/policy.js";
|
|
40
42
|
import { paletteForThemeName } from "./themes/palette-resolvers.js";
|
|
41
43
|
import { effectivePresetName, presetGlobals } from "./config/presets.js";
|
|
42
44
|
import {
|
|
43
45
|
DEFAULT_CHARSET,
|
|
44
46
|
DEFAULT_COLOR_COMPATIBILITY,
|
|
45
|
-
DEFAULT_PADDING,
|
|
46
|
-
DEFAULT_WRAP,
|
|
47
47
|
} from "./render/strip.js";
|
|
48
48
|
import type { EffectiveGlobals } from "./daemon/render-payload.js";
|
|
49
49
|
|
|
@@ -321,8 +321,8 @@ function loadRegisterRender(
|
|
|
321
321
|
theme: effectiveThemeName(null, globals.palette),
|
|
322
322
|
look: effectiveLookName(null, globals.look, config.looks),
|
|
323
323
|
style: effectiveStripStyle(null, globals.style),
|
|
324
|
-
autoWrap: globals.autoWrap
|
|
325
|
-
padding: globals.padding
|
|
324
|
+
autoWrap: effectiveAutoWrap(null, globals.autoWrap),
|
|
325
|
+
padding: effectivePadding(null, globals.padding),
|
|
326
326
|
charset: globals.charset ?? DEFAULT_CHARSET,
|
|
327
327
|
colorCompatibility:
|
|
328
328
|
globals.colorCompatibility ?? DEFAULT_COLOR_COMPATIBILITY,
|
|
@@ -28,6 +28,7 @@
|
|
|
28
28
|
|
|
29
29
|
import type { DslConfig, LayoutNode, SegmentDecl } from "./dsl-types.js";
|
|
30
30
|
import { parseDslConfig } from "./dsl-loader.js";
|
|
31
|
+
import { BOOLEAN_MEMBERS, PADDING_RANGE } from "../themes/policy.js";
|
|
31
32
|
import { mergeWithDefault } from "./loader/merge.js";
|
|
32
33
|
|
|
33
34
|
// ─── Shared template fragments ───────────────────────────────────────────────
|
|
@@ -250,7 +251,11 @@ function etaHeatFg(etaRef: string, warnRef: string): string {
|
|
|
250
251
|
// theme/style/look/preset (session `set` for a per-conversation preview, PLUS
|
|
251
252
|
// a persist-forever twin — candybar-config-engine-71o.5, brandon-presets-0yk.3
|
|
252
253
|
// — for pinning the choice as everyone's default), the four .3 globals
|
|
253
|
-
// steppers (persist-only
|
|
254
|
+
// steppers (persist-only HERE — `autoWrap`/`padding` have gained a session
|
|
255
|
+
// half since, but the drawer's own controls stay durable-by-default until
|
|
256
|
+
// candybar-settings-ui-aok.3 gives the panel one `persist?` toggle instead of
|
|
257
|
+
// a spelling per row; `charset`/`colorCompatibility` have no session half at
|
|
258
|
+
// all, by design), and one .6
|
|
254
259
|
// segment-scoped persist control (directoryPaletteControl, persist-only like
|
|
255
260
|
// the four steppers — a per-segment palette pin, not a whole-bar default).
|
|
256
261
|
// Placed as a sibling in row 1's horizontal container, toggled from beside the
|
|
@@ -1222,8 +1227,11 @@ export const RAW_DEFAULT_DSL_CONFIG = {
|
|
|
1222
1227
|
// ── The four .3 globals steppers, folded into the settingsDrawer group
|
|
1223
1228
|
// (candybar-config-engine-71o.4) alongside theme/style/look above. Each
|
|
1224
1229
|
// pairs a `persist` control with a `↺` reset (docs' persist/reset
|
|
1225
|
-
// convention) —
|
|
1226
|
-
// is their only seam,
|
|
1230
|
+
// convention) — `charset`/`colorCompatibility` have no SessionState half
|
|
1231
|
+
// at all, so persist is genuinely their only seam, while
|
|
1232
|
+
// `autoWrap`/`padding` do have one and are simply spelled durable here
|
|
1233
|
+
// (candybar-settings-ui-aok.3 owns whether the panel offers the choice).
|
|
1234
|
+
// Labels
|
|
1227
1235
|
// read `.field.effective` (the daemon-resolved value BuildLineOptions
|
|
1228
1236
|
// actually rendered with), never a restated literal.
|
|
1229
1237
|
charsetControl: {
|
|
@@ -1421,8 +1429,11 @@ export const RAW_DEFAULT_DSL_CONFIG = {
|
|
|
1421
1429
|
// (candybar-config-engine-71o.4), decoupled by NAME from
|
|
1422
1430
|
// charsetControl/colorCompatControl/wrapToggleControl/paddingControl
|
|
1423
1431
|
// below. Unlike theme/style/look (a per-session experiment via `set`),
|
|
1424
|
-
// these four
|
|
1425
|
-
//
|
|
1432
|
+
// these four are spelled durable: `charset`/`colorCompatibility` have no
|
|
1433
|
+
// SessionState half at all, and `autoWrap`/`padding` have one
|
|
1434
|
+
// (candybar-settings-ui-aok.2) but keep persist as the DRAWER's spelling
|
|
1435
|
+
// until .3 collapses the twins behind one `persist?`. So every one of
|
|
1436
|
+
// these writes the
|
|
1426
1437
|
// config-file DEFAULT through the daemon-owned overrides layer (never the
|
|
1427
1438
|
// hand-authored file itself), gated by the SAME deriveActionValidators
|
|
1428
1439
|
// pass as a `set` (persist mirrors set's value-source shapes one for
|
|
@@ -1435,10 +1446,14 @@ export const RAW_DEFAULT_DSL_CONFIG = {
|
|
|
1435
1446
|
from: "colorCompatibilities",
|
|
1436
1447
|
},
|
|
1437
1448
|
resetColorCompat: { reset: "colorCompatibility" },
|
|
1438
|
-
|
|
1449
|
+
// [LAW:one-source-of-truth] The toggle's members and the stepper's bounds
|
|
1450
|
+
// are the SAME literals the resolvers parse a session pick with
|
|
1451
|
+
// (BOOLEAN_MEMBERS / PADDING_RANGE in themes/policy.ts) — a click cannot
|
|
1452
|
+
// write a value the render's own resolution would then refuse.
|
|
1453
|
+
toggleWrapForever: { persist: "autoWrap", cycle: [...BOOLEAN_MEMBERS] },
|
|
1439
1454
|
resetAutoWrap: { reset: "autoWrap" },
|
|
1440
|
-
paddingDownForever: { persist: "padding",
|
|
1441
|
-
paddingUpForever: { persist: "padding",
|
|
1455
|
+
paddingDownForever: { persist: "padding", ...PADDING_RANGE, by: -1 },
|
|
1456
|
+
paddingUpForever: { persist: "padding", ...PADDING_RANGE, by: 1 },
|
|
1442
1457
|
resetPadding: { reset: "padding" },
|
|
1443
1458
|
|
|
1444
1459
|
// [LAW:locality-or-seam] The segment-palette control's behavior
|
package/src/config/dsl-loader.ts
CHANGED
|
@@ -50,6 +50,8 @@ import { synthesizeGroupDecls, validateRoot } from "./loader/layout.js";
|
|
|
50
50
|
import { synthesizeMenuDecls } from "./loader/menu-synth.js";
|
|
51
51
|
import { synthesizeEditModeToggle } from "./loader/edit-mode.js";
|
|
52
52
|
import { synthesizeEditChrome } from "./edit-chrome.js";
|
|
53
|
+
import { SETTINGS_NS, synthesizeSettingsMenu } from "./settings-menu.js";
|
|
54
|
+
import { reservedNamespaceCollisions } from "./loader/reserved-namespace.js";
|
|
53
55
|
import { validateActions } from "./loader/actions.js";
|
|
54
56
|
import { validateLooks } from "./loader/looks.js";
|
|
55
57
|
import { validatePresets } from "./loader/presets.js";
|
|
@@ -152,7 +154,12 @@ export function validateConfig(
|
|
|
152
154
|
// into freshly-synthesized segments, actions into freshly-synthesized
|
|
153
155
|
// actions) is correct by construction and does not re-enter cross-ref/
|
|
154
156
|
// cycle checking, exactly as group/menu synthesis's output doesn't either.
|
|
155
|
-
|
|
157
|
+
// [LAW:dataflow-not-control-flow] candybar-settings-ui-aok.1's global settings
|
|
158
|
+
// menu, spliced BEFORE edit chrome so edit chrome walks the final content tree
|
|
159
|
+
// and treats the menu's reserved `settings.` names as chrome-exempt — the
|
|
160
|
+
// full ordering argument lives in settings-menu.ts's header, beside the pass
|
|
161
|
+
// it governs.
|
|
162
|
+
const withChrome = synthesizeEditChrome(synthesizeSettingsMenu(config));
|
|
156
163
|
return withChrome as ValidatedConfig;
|
|
157
164
|
}
|
|
158
165
|
|
|
@@ -295,6 +302,18 @@ function validateTopLevel(
|
|
|
295
302
|
// affordances) runs later, in validateConfig, once the merged/preset-
|
|
296
303
|
// resolved/rootOps-replayed tree exists to derive it from.
|
|
297
304
|
synthesizeEditModeToggle(ctx, out);
|
|
305
|
+
// [LAW:one-source-of-truth] The global settings menu reserves its namespace
|
|
306
|
+
// here and synthesizes NOTHING here: the tree it must be present in only
|
|
307
|
+
// exists after merge (a user `root` replaces the default's), so the artifacts
|
|
308
|
+
// are minted in validateConfig. The reservation is unconditional all the same,
|
|
309
|
+
// mirroring every other namespace above — "you never author settings.*" is a
|
|
310
|
+
// stable contract, not a rule that switches on when the pass happens to fire.
|
|
311
|
+
reservedNamespaceCollisions(
|
|
312
|
+
ctx,
|
|
313
|
+
out,
|
|
314
|
+
SETTINGS_NS,
|
|
315
|
+
"the global settings menu",
|
|
316
|
+
);
|
|
298
317
|
return out;
|
|
299
318
|
}
|
|
300
319
|
|
|
@@ -40,6 +40,7 @@ import {
|
|
|
40
40
|
EDIT_TOGGLE_ACTION,
|
|
41
41
|
} from "./loader/edit-mode.js";
|
|
42
42
|
import { GROUP_NS } from "./loader/layout.js";
|
|
43
|
+
import { SETTINGS_NS } from "./settings-menu.js";
|
|
43
44
|
import {
|
|
44
45
|
menuActionName,
|
|
45
46
|
menuMember,
|
|
@@ -61,7 +62,11 @@ import {
|
|
|
61
62
|
// knowledge, so the banner below is spliced UNCONDITIONALLY for every
|
|
62
63
|
// preset — same shape, every reload — and this predicate is what decides
|
|
63
64
|
// whether it's visible, never a branch in this synthesis pass.
|
|
64
|
-
|
|
65
|
+
// [LAW:one-source-of-truth] Exported: test/helpers/ambient-chrome.ts filters this
|
|
66
|
+
// ensured name out of "what did the AUTHOR declare" assertions and must read the
|
|
67
|
+
// same string, never a second copy that a rename here would leave behind.
|
|
68
|
+
export const PRESET_CUSTOMIZED_VAR = "preset.customized";
|
|
69
|
+
const PRESET_CUSTOMIZED_GATE = `{{ .${PRESET_CUSTOMIZED_VAR} }}`;
|
|
65
70
|
|
|
66
71
|
// [LAW:one-source-of-truth] group/menu-synthesized segments (`groups.`/
|
|
67
72
|
// `menus.`) and edit mode's own trigger/chrome (`edit.`) are structural —
|
|
@@ -73,7 +78,12 @@ function isChromeExempt(name: string): boolean {
|
|
|
73
78
|
return (
|
|
74
79
|
name.startsWith(EDIT_NS) ||
|
|
75
80
|
name.startsWith(MENU_NS) ||
|
|
76
|
-
name.startsWith(GROUP_NS)
|
|
81
|
+
name.startsWith(GROUP_NS) ||
|
|
82
|
+
// The global settings menu is the entry point edit mode is REACHED from
|
|
83
|
+
// (candybar-settings-ui-aok.1) — offering a `-` beside it would let one
|
|
84
|
+
// click delete the door back in, the self-lockout the `toolbar` trigger's
|
|
85
|
+
// placement was chosen to avoid. Structural, like the three above it.
|
|
86
|
+
name.startsWith(SETTINGS_NS)
|
|
77
87
|
);
|
|
78
88
|
}
|
|
79
89
|
|
|
@@ -93,6 +103,11 @@ function escapeTemplateLiteral(s: string): string {
|
|
|
93
103
|
// so cross-preset names (disambiguated by `presetIdent`) can never collide.
|
|
94
104
|
interface ChromeArtifacts {
|
|
95
105
|
readonly variables: Record<string, VariableDecl>;
|
|
106
|
+
// [LAW:no-silent-failure] Declarations this synthesis DEPENDS on rather than
|
|
107
|
+
// OWNS: merged UNDER the config so a user's own declaration of the same name
|
|
108
|
+
// wins, unlike `variables` above, which lives in a reserved namespace no user
|
|
109
|
+
// may write and therefore merges over.
|
|
110
|
+
readonly ensured: Record<string, VariableDecl>;
|
|
96
111
|
readonly actions: Record<string, ActionDeclType>;
|
|
97
112
|
readonly segments: Record<string, SegmentDecl>;
|
|
98
113
|
}
|
|
@@ -219,7 +234,18 @@ function spliceContainer(
|
|
|
219
234
|
posCounter: { n: number },
|
|
220
235
|
): ContainerNode {
|
|
221
236
|
const children: LayoutNode[] = [];
|
|
222
|
-
|
|
237
|
+
// [LAW:one-source-of-truth] The trailing `+`'s position is "after the last
|
|
238
|
+
// CONTENT segment", not "after the last child". Those coincided until a
|
|
239
|
+
// synthesis started appending exempt chrome (the global settings menu,
|
|
240
|
+
// candybar-settings-ui-aok.1) to a row's end, at which point reading the last
|
|
241
|
+
// child silently dropped the row's final insert point — N segments offering
|
|
242
|
+
// only N insert points instead of N+1.
|
|
243
|
+
const lastContent = node.children.reduce(
|
|
244
|
+
(idx, child, i) =>
|
|
245
|
+
child.kind === "segment" && !isChromeExempt(child.name) ? i : idx,
|
|
246
|
+
-1,
|
|
247
|
+
);
|
|
248
|
+
for (const [i, child] of node.children.entries()) {
|
|
223
249
|
if (child.kind === "container") {
|
|
224
250
|
children.push(
|
|
225
251
|
spliceContainer(
|
|
@@ -250,24 +276,19 @@ function spliceContainer(
|
|
|
250
276
|
);
|
|
251
277
|
children.push(child);
|
|
252
278
|
children.push(removeChrome(presetIdent, rootOpsKey, child.name, artifacts));
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
last.name,
|
|
267
|
-
"after",
|
|
268
|
-
artifacts,
|
|
269
|
-
),
|
|
270
|
-
);
|
|
279
|
+
if (i === lastContent) {
|
|
280
|
+
children.push(
|
|
281
|
+
insertChrome(
|
|
282
|
+
presetIdent,
|
|
283
|
+
rootOpsKey,
|
|
284
|
+
String(posCounter.n++),
|
|
285
|
+
domainName,
|
|
286
|
+
child.name,
|
|
287
|
+
"after",
|
|
288
|
+
artifacts,
|
|
289
|
+
),
|
|
290
|
+
);
|
|
291
|
+
}
|
|
271
292
|
}
|
|
272
293
|
return { ...node, children };
|
|
273
294
|
}
|
|
@@ -301,6 +322,20 @@ function prependCustomizedBanner(
|
|
|
301
322
|
// edited down to zero non-exempt segments (no removeChrome/insertChrome
|
|
302
323
|
// persist actions left to register it) doesn't orphan this exact click.
|
|
303
324
|
artifacts.actions[actionName] = { reset: rootOpsKey };
|
|
325
|
+
// [LAW:one-source-of-truth] The banner reads `.preset.customized`, so THIS
|
|
326
|
+
// pass is what requires that variable — not whichever config happens to
|
|
327
|
+
// declare it. The bundled default does, which is why the dependency stayed
|
|
328
|
+
// invisible until the global settings menu made edit mode reachable from
|
|
329
|
+
// configs that never declared it, and the missing field surfaced as a ⚠ on
|
|
330
|
+
// the bar. Ensured, never overridden: a user declaration of the same name
|
|
331
|
+
// wins (see the merge in synthesizeEditChrome), so this only supplies the
|
|
332
|
+
// floor the synthesis itself depends on.
|
|
333
|
+
artifacts.ensured[PRESET_CUSTOMIZED_VAR] = {
|
|
334
|
+
kind: "input",
|
|
335
|
+
path: PRESET_CUSTOMIZED_VAR,
|
|
336
|
+
type: "boolean",
|
|
337
|
+
default: false,
|
|
338
|
+
};
|
|
304
339
|
const label = escapeTemplateLiteral(presetName);
|
|
305
340
|
artifacts.segments[chromeSegName] = {
|
|
306
341
|
template: `{{ action "${actionName}" "↺ ${label} customized" }}`,
|
|
@@ -391,6 +426,7 @@ export function synthesizeEditChrome(config: DslConfig): DslConfig {
|
|
|
391
426
|
if (!(EDIT_TOGGLE_ACTION in config.actions)) return config;
|
|
392
427
|
const artifacts: ChromeArtifacts = {
|
|
393
428
|
variables: {},
|
|
429
|
+
ensured: {},
|
|
394
430
|
actions: {},
|
|
395
431
|
segments: {},
|
|
396
432
|
};
|
|
@@ -404,7 +440,11 @@ export function synthesizeEditChrome(config: DslConfig): DslConfig {
|
|
|
404
440
|
}
|
|
405
441
|
return {
|
|
406
442
|
...config,
|
|
407
|
-
variables: {
|
|
443
|
+
variables: {
|
|
444
|
+
...artifacts.ensured,
|
|
445
|
+
...config.variables,
|
|
446
|
+
...artifacts.variables,
|
|
447
|
+
},
|
|
408
448
|
actions: { ...config.actions, ...artifacts.actions },
|
|
409
449
|
segments: { ...config.segments, ...artifacts.segments },
|
|
410
450
|
presets,
|
|
@@ -29,6 +29,13 @@ import {
|
|
|
29
29
|
import { listGlobalsFieldNames } from "./globals.js";
|
|
30
30
|
import { parsePersistTarget } from "./persist-target.js";
|
|
31
31
|
import { presetNames } from "../presets.js";
|
|
32
|
+
import {
|
|
33
|
+
canHostSessionState,
|
|
34
|
+
countAnchors,
|
|
35
|
+
isSettingsAnchor,
|
|
36
|
+
SESSION_ID_VAR,
|
|
37
|
+
SETTINGS_ANCHOR,
|
|
38
|
+
} from "../settings-menu.js";
|
|
32
39
|
import { ident } from "../ident.js";
|
|
33
40
|
import { findKeyLine } from "./diagnostics.js";
|
|
34
41
|
import { isPlainObject, type ValidateCtx } from "./validate-core.js";
|
|
@@ -248,11 +255,30 @@ export function validateCrossReferences(
|
|
|
248
255
|
// never a second traversal that could learn a different idea of what a valid
|
|
249
256
|
// layout is. This is what makes `cc-candybar check` catch a preset staging a
|
|
250
257
|
// segment nobody declared.
|
|
258
|
+
//
|
|
259
|
+
// [LAW:single-enforcer] The menu precondition is asked once, of the same
|
|
260
|
+
// predicate synthesizeSettingsMenu gates on, and read by every tree walked.
|
|
261
|
+
const menuWillSynthesize = canHostSessionState(cfg);
|
|
251
262
|
const checkLayoutTree = (
|
|
252
263
|
root: LayoutNode,
|
|
253
264
|
layoutKey: string,
|
|
254
265
|
layoutLine: number | undefined,
|
|
255
266
|
): void => {
|
|
267
|
+
// [LAW:one-source-of-truth] The global settings menu's anchor is a POSITION
|
|
268
|
+
// an author may place and this walk must therefore accept, even though no
|
|
269
|
+
// config declares a segment by that name — synthesizeSettingsMenu provides
|
|
270
|
+
// it unconditionally, immediately after these checks pass. Two placements is
|
|
271
|
+
// the real error: one state key holds one open state, so a second anchor
|
|
272
|
+
// would be a second toggle writing one disclosure, with two bodies claiming
|
|
273
|
+
// to be it. Counted over the SAME census the synthesis reads, so "placed"
|
|
274
|
+
// means one thing [LAW:single-enforcer].
|
|
275
|
+
if (countAnchors(root) > 1) {
|
|
276
|
+
ctx.issues.push({
|
|
277
|
+
path: layoutKey,
|
|
278
|
+
message: `${layoutKey} places the global settings menu anchor "${SETTINGS_ANCHOR}" ${countAnchors(root)} times — it may appear at most once per layout (it is one disclosure, and one state key holds one open state). Remove all but the placement you want; removing every placement puts the menu at its default position.`,
|
|
279
|
+
line: layoutLine,
|
|
280
|
+
});
|
|
281
|
+
}
|
|
256
282
|
for (const node of walkNodes(root)) {
|
|
257
283
|
// [LAW:locality-or-seam] A node's `when` reads the global scope (bare
|
|
258
284
|
// globals + namespaced segment vars) — the same existence-check shape as a
|
|
@@ -263,6 +289,24 @@ export function validateCrossReferences(
|
|
|
263
289
|
});
|
|
264
290
|
}
|
|
265
291
|
if (node.kind !== "segment") continue;
|
|
292
|
+
if (isSettingsAnchor(node.name)) {
|
|
293
|
+
// [LAW:one-source-of-truth] Accepting the anchor asserts that
|
|
294
|
+
// synthesizeSettingsMenu WILL declare a segment by this name, so the
|
|
295
|
+
// acceptance reads the very predicate that pass decides by rather than
|
|
296
|
+
// assuming its answer. When it is false the reference is genuinely
|
|
297
|
+
// dangling, and the error names the unmet precondition: the author
|
|
298
|
+
// placed a documented anchor, they did not typo a segment name, and the
|
|
299
|
+
// generic "does not match any declared segment" would teach the wrong
|
|
300
|
+
// lesson [LAW:no-silent-failure].
|
|
301
|
+
if (!menuWillSynthesize) {
|
|
302
|
+
ctx.issues.push({
|
|
303
|
+
path: layoutKey,
|
|
304
|
+
message: `${layoutKey} places the global settings menu anchor "${SETTINGS_ANCHOR}", but this config declares no "${SESSION_ID_VAR}" variable — the menu is a click surface and every click composes a URL from "${SESSION_ID_VAR}", so it is not synthesized for a config without it. Declare a "${SESSION_ID_VAR}" variable (any config merged onto the bundled default inherits one) or remove the anchor placement.`,
|
|
305
|
+
line: layoutLine,
|
|
306
|
+
});
|
|
307
|
+
}
|
|
308
|
+
continue;
|
|
309
|
+
}
|
|
266
310
|
if (!Object.prototype.hasOwnProperty.call(cfg.segments, node.name)) {
|
|
267
311
|
const renamed = RENAMED_SEGMENTS[node.name];
|
|
268
312
|
const hint =
|
|
@@ -29,6 +29,20 @@
|
|
|
29
29
|
// of this pass caused. The reserved namespace stays reserved unconditionally
|
|
30
30
|
// (mirroring reservedNamespaceCollisions' own contract); only the SYNTHESIS is
|
|
31
31
|
// conditional.
|
|
32
|
+
//
|
|
33
|
+
// WHAT candybar-settings-ui-aok.1 CHANGED, and what it did not: the gate above
|
|
34
|
+
// is intact and still the only way edit mode is reached — but it now has a
|
|
35
|
+
// PERMANENT DEMANDER. synthesizeSettingsMenu mints a `settings.edit` segment
|
|
36
|
+
// referencing `edit.toggle` into every config it can host, so in practice the
|
|
37
|
+
// demand is satisfied for essentially every config a user writes, and reading
|
|
38
|
+
// this section as "most bars carry no edit mode" is no longer true. The
|
|
39
|
+
// separation the gate protects still holds exactly where it always mattered:
|
|
40
|
+
// the menu declines to synthesize for a config with no `session.id`
|
|
41
|
+
// (canHostSessionState in settings-menu.ts), which is precisely the static,
|
|
42
|
+
// non-interactive bar this comment was written to keep clean. Production blast
|
|
43
|
+
// radius was nil either way — the bundled default's `toolbar` segment already
|
|
44
|
+
// referenced `edit.toggle`, so every config merging it already demanded edit
|
|
45
|
+
// mode before the menu existed.
|
|
32
46
|
|
|
33
47
|
import { createEngine } from "@promptctl/go-template-js";
|
|
34
48
|
import type { Mutable, ValidateCtx } from "./validate-core.js";
|
|
@@ -7,6 +7,7 @@ import { type Globals } from "../dsl-types.js";
|
|
|
7
7
|
import {
|
|
8
8
|
CHARSETS,
|
|
9
9
|
COLOR_COMPATIBILITIES,
|
|
10
|
+
PADDING_RANGE,
|
|
10
11
|
STRIP_STYLES,
|
|
11
12
|
type ColorCompatibility,
|
|
12
13
|
} from "../../themes/policy.js";
|
|
@@ -84,7 +85,11 @@ const GLOBALS_FIELDS: FieldSpecMap<Globals> = {
|
|
|
84
85
|
// drive an unbounded `" ".repeat` allocation in the daemon
|
|
85
86
|
// [LAW:no-silent-failure] — an absurd value is a loud load error, not a
|
|
86
87
|
// silently-huge render.
|
|
87
|
-
|
|
88
|
+
// [LAW:one-source-of-truth] The bound comes from PADDING_RANGE, the same
|
|
89
|
+
// literal the bundled default's stepper actions bound clicks by and the
|
|
90
|
+
// session-half parse admits values from — so the file, the click, and the
|
|
91
|
+
// session pick cannot end up honouring three different ranges.
|
|
92
|
+
padding: optionalIntSpec(PADDING_RANGE),
|
|
88
93
|
// [LAW:types-are-the-program] Closed enum like `style`: the joiner glyph
|
|
89
94
|
// vocabularies pickJoiner can render — validates by membership, emits a
|
|
90
95
|
// JSON-Schema `enum` from the same CHARSETS literal.
|
package/src/config/presets.ts
CHANGED
|
@@ -168,9 +168,14 @@ export function presetGlobals(config: DslConfig, name: string): Globals {
|
|
|
168
168
|
// that is a typo in a file the author can see and fix — but a value that
|
|
169
169
|
// only exists because the overrides layer supplied it gets the SAME
|
|
170
170
|
// treatment a stale SessionState pick already gets from effectivePresetName:
|
|
171
|
-
//
|
|
172
|
-
//
|
|
173
|
-
//
|
|
171
|
+
// an undeclared name is not a pick at all, so the resolution moves on to the
|
|
172
|
+
// next rung of the precedence chain — visibly, never a throw. (For a stale
|
|
173
|
+
// SESSION pick that next rung is the config's own `globals.preset`, and only
|
|
174
|
+
// then the floor; for the field dropped here it is whatever the config file
|
|
175
|
+
// itself declares. Same rule, different starting rung —
|
|
176
|
+
// see effectiveGlobal in themes/policy.ts.) Dropping the field here (so it
|
|
177
|
+
// never reaches validateConfig) is what applies that rule to this layer too,
|
|
178
|
+
// instead of a fatal error replacing the whole bar.
|
|
174
179
|
export function sanitizePersistedPresetOverride(
|
|
175
180
|
globalsOverride: Partial<Globals>,
|
|
176
181
|
declaredPresets: Readonly<Record<string, PresetDecl>>,
|