@promptctl/cc-candybar 1.39.0 → 1.40.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 +78 -78
- package/package.json +5 -5
- package/schema/cc-candybar.schema.json +61 -1
- package/src/check.ts +14 -31
- package/src/config/default-dsl-config.ts +18 -0
- package/src/config/dsl-loader.ts +8 -1
- package/src/config/dsl-types.ts +23 -0
- package/src/config/loader/emit-schema.ts +2 -1
- package/src/config/loader/globals.ts +66 -32
- package/src/config/loader/merge.ts +6 -0
- package/src/config/presets.ts +20 -1
- package/src/daemon/render-payload.ts +98 -1
- package/src/daemon/server.ts +29 -88
- package/src/demo/dsl.ts +21 -37
- package/src/themes/policy.ts +44 -4
package/src/daemon/server.ts
CHANGED
|
@@ -63,20 +63,8 @@ import { buildDebugSnapshot } from "./debug";
|
|
|
63
63
|
import { DEBUG_WHATS, isDebugWhat } from "./debug-types";
|
|
64
64
|
import { expandHome } from "../config/dsl-loader.js";
|
|
65
65
|
import { renderDsl } from "../dsl/render.js";
|
|
66
|
-
import {
|
|
67
|
-
|
|
68
|
-
effectiveAutoWrap,
|
|
69
|
-
effectivePadding,
|
|
70
|
-
effectiveThemeName,
|
|
71
|
-
effectiveLookName,
|
|
72
|
-
lookKeyByName,
|
|
73
|
-
paletteForThemeName,
|
|
74
|
-
} from "../themes/index.js";
|
|
75
|
-
import {
|
|
76
|
-
effectivePresetName,
|
|
77
|
-
presetGlobals,
|
|
78
|
-
presetIsCustomized,
|
|
79
|
-
} from "../config/presets.js";
|
|
66
|
+
import { lookKeyByName, paletteForThemeName } from "../themes/index.js";
|
|
67
|
+
import { presetIsCustomized } from "../config/presets.js";
|
|
80
68
|
import {
|
|
81
69
|
renderStripCells,
|
|
82
70
|
DEFAULT_CHARSET,
|
|
@@ -90,7 +78,11 @@ import {
|
|
|
90
78
|
} from "../render/strip.js";
|
|
91
79
|
import { applyClaudeCodeReserve } from "../utils/terminal-width.js";
|
|
92
80
|
import type { RichText } from "@promptctl/rich-js";
|
|
93
|
-
import {
|
|
81
|
+
import {
|
|
82
|
+
buildRenderPayload,
|
|
83
|
+
resolveEffectiveGlobals,
|
|
84
|
+
type EffectiveGlobals,
|
|
85
|
+
} from "./render-payload.js";
|
|
94
86
|
import { ContextProvider } from "../segments/context.js";
|
|
95
87
|
import { MetricsProvider } from "../segments/metrics.js";
|
|
96
88
|
import { TmuxService } from "../segments/tmux.js";
|
|
@@ -860,83 +852,28 @@ async function handleRequest(req: Request): Promise<HandledRequest> {
|
|
|
860
852
|
// No special-case branches — same composition every render.
|
|
861
853
|
let body = "";
|
|
862
854
|
if (entry.state !== null) {
|
|
863
|
-
// [LAW:one-source-of-truth]
|
|
864
|
-
//
|
|
865
|
-
//
|
|
866
|
-
//
|
|
867
|
-
//
|
|
868
|
-
//
|
|
869
|
-
//
|
|
870
|
-
//
|
|
871
|
-
//
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
// have no SessionState half at all, making the config global over its
|
|
877
|
-
// floor constant their whole resolution. This struct feeds
|
|
878
|
-
// BOTH the payload's `*.effective` fields (trigger labels) AND
|
|
879
|
-
// renderOpts below (the actual render) — one resolution, two readers,
|
|
880
|
-
// so a label can never disagree with what was rendered.
|
|
881
|
-
//
|
|
882
|
-
// [LAW:one-source-of-truth] The PRESET resolves FIRST, because every
|
|
883
|
-
// other field below reads globals — and which globals is exactly what
|
|
884
|
-
// the preset decides. `presetGlobals` is the config's globals with the
|
|
885
|
-
// active fragment's shallow-merged over them, so the preset sits at its
|
|
886
|
-
// one documented place in the precedence chain (bundled default < user
|
|
887
|
-
// file < persisted overrides < ACTIVE PRESET < session pick — see
|
|
888
|
-
// src/config/presets.ts and docs/interaction-authoring.md): later than
|
|
889
|
-
// everything read per cache ENTRY, earlier than every session click,
|
|
890
|
-
// which is the order the `??` chains below already enforce by reading
|
|
891
|
-
// SessionState first. There is no "does a preset apply?" branch — the
|
|
892
|
-
// floor preset's empty fragment merges as a no-op
|
|
893
|
-
// [LAW:dataflow-not-control-flow].
|
|
894
|
-
const preset = effectivePresetName(
|
|
895
|
-
sessionState.get(req.hookData.session_id, "preset"),
|
|
896
|
-
entry.state.config.globals.preset,
|
|
897
|
-
entry.state.config.presets,
|
|
898
|
-
);
|
|
899
|
-
const globals = presetGlobals(entry.state.config, preset);
|
|
900
|
-
const effective: EffectiveGlobals = {
|
|
901
|
-
preset,
|
|
855
|
+
// [LAW:one-source-of-truth] Every globals field resolved ONCE per
|
|
856
|
+
// render, here — before the payload build, so the same struct feeds
|
|
857
|
+
// BOTH the payload's `*.effective` fields (what a trigger label says)
|
|
858
|
+
// AND renderOpts below (what actually renders). One resolution, two
|
|
859
|
+
// readers, so a label can never disagree with the bar. The precedence
|
|
860
|
+
// the resolver applies, and why each rung sits where it does, lives
|
|
861
|
+
// with the chain (resolveEffectiveGlobals, and src/config/presets.ts).
|
|
862
|
+
// Read alongside the config, from the same entry, in one statement —
|
|
863
|
+
// which is exactly what the closure below claims about it.
|
|
864
|
+
const presetRootOps = entry.state.presetRootOps;
|
|
865
|
+
const effective: EffectiveGlobals = resolveEffectiveGlobals(
|
|
866
|
+
entry.state.config,
|
|
867
|
+
(key: string) => sessionState.get(req.hookData.session_id, key),
|
|
902
868
|
// [LAW:one-source-of-truth] brandon-layout-edit-2gc.5 — read from
|
|
903
869
|
// THIS entry's own presetRootOps (the record that fed the SAME
|
|
904
870
|
// reload that produced entry.state.config), never a fresh
|
|
905
871
|
// loadOverrides() here — a second read could race a concurrent
|
|
906
|
-
// write and disagree with the tree that actually rendered.
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
theme: effectiveThemeName(
|
|
912
|
-
sessionState.get(req.hookData.session_id, "theme"),
|
|
913
|
-
globals.palette,
|
|
914
|
-
),
|
|
915
|
-
look: effectiveLookName(
|
|
916
|
-
sessionState.get(req.hookData.session_id, "look"),
|
|
917
|
-
globals.look,
|
|
918
|
-
entry.state.config.looks,
|
|
919
|
-
),
|
|
920
|
-
style: effectiveStripStyle(
|
|
921
|
-
sessionState.get(req.hookData.session_id, "style"),
|
|
922
|
-
globals.style,
|
|
923
|
-
),
|
|
924
|
-
autoWrap: effectiveAutoWrap(
|
|
925
|
-
sessionState.get(req.hookData.session_id, "autoWrap"),
|
|
926
|
-
globals.autoWrap,
|
|
927
|
-
),
|
|
928
|
-
padding: effectivePadding(
|
|
929
|
-
sessionState.get(req.hookData.session_id, "padding"),
|
|
930
|
-
globals.padding,
|
|
931
|
-
),
|
|
932
|
-
// charset and colorCompatibility have no session half by design —
|
|
933
|
-
// they describe the terminal, not a taste (see CHARSETS in
|
|
934
|
-
// themes/policy.ts) — so the config global over its floor is their
|
|
935
|
-
// whole resolution.
|
|
936
|
-
charset: globals.charset ?? DEFAULT_CHARSET,
|
|
937
|
-
colorCompatibility:
|
|
938
|
-
globals.colorCompatibility ?? DEFAULT_COLOR_COMPATIBILITY,
|
|
939
|
-
};
|
|
872
|
+
// write and disagree with the tree that actually rendered. That is
|
|
873
|
+
// why it arrives as a closure over this entry rather than being
|
|
874
|
+
// looked up inside the resolver.
|
|
875
|
+
(preset: string) => presetIsCustomized(presetRootOps, preset),
|
|
876
|
+
);
|
|
940
877
|
const payload = await buildRenderPayload(
|
|
941
878
|
req.hookData,
|
|
942
879
|
payloadDeps,
|
|
@@ -956,6 +893,10 @@ async function handleRequest(req: Request): Promise<HandledRequest> {
|
|
|
956
893
|
// SAME `effective` struct the payload was just built from — no second
|
|
957
894
|
// `?? DEFAULT_X` computation to drift from it.
|
|
958
895
|
renderOpts.style = effective.style;
|
|
896
|
+
// The `plain` joiner's cell separator. Assigned unconditionally like
|
|
897
|
+
// every field around it: `undefined` is a value pickJoiner already
|
|
898
|
+
// reads as "PlainJoiner's own default", not an absence to branch on.
|
|
899
|
+
renderOpts.separator = effective.separator;
|
|
959
900
|
renderOpts.wrap = effective.autoWrap;
|
|
960
901
|
renderOpts.padding = effective.padding;
|
|
961
902
|
renderOpts.charset = effective.charset;
|
package/src/demo/dsl.ts
CHANGED
|
@@ -30,21 +30,10 @@ import { VariableStore } from "../var-system/store.js";
|
|
|
30
30
|
import { SourceRegistry } from "../var-system/sources.js";
|
|
31
31
|
import { SessionState } from "../daemon/session-state.js";
|
|
32
32
|
import { listResolvablePaletteNames } from "../themes/policy.js";
|
|
33
|
-
import {
|
|
34
|
-
|
|
35
|
-
effectiveLookName,
|
|
36
|
-
effectiveAutoWrap,
|
|
37
|
-
effectivePadding,
|
|
38
|
-
lookKeyByName,
|
|
39
|
-
paletteForThemeName,
|
|
40
|
-
} from "../themes/index.js";
|
|
41
|
-
import { effectivePresetName, presetGlobals } from "../config/presets.js";
|
|
33
|
+
import { lookKeyByName, paletteForThemeName } from "../themes/index.js";
|
|
34
|
+
import { resolveEffectiveGlobals } from "../daemon/render-payload.js";
|
|
42
35
|
import { registerDslConfig, renderDsl } from "../dsl/render.js";
|
|
43
|
-
import {
|
|
44
|
-
DEFAULT_CHARSET,
|
|
45
|
-
DEFAULT_COLOR_COMPATIBILITY,
|
|
46
|
-
DEFAULT_TERMINAL_WIDTH,
|
|
47
|
-
} from "../render/strip.js";
|
|
36
|
+
import { DEFAULT_TERMINAL_WIDTH } from "../render/strip.js";
|
|
48
37
|
import { applyClaudeCodeReserve } from "../utils/terminal-width.js";
|
|
49
38
|
|
|
50
39
|
const FRAMES = 4;
|
|
@@ -83,18 +72,18 @@ const payload = {
|
|
|
83
72
|
// globals every other option reads — the same preset-first order server.ts and
|
|
84
73
|
// check.ts resolve in, so the demo prints the arrangement a fresh session opens
|
|
85
74
|
// in.
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
config.looks,
|
|
96
|
-
effectiveLookName(null, globals.look, config.looks),
|
|
75
|
+
// [LAW:one-source-of-truth] THE daemon's resolver, not a mirror of it — a
|
|
76
|
+
// fresh-session pick reader (null for every key) and no overrides log to be
|
|
77
|
+
// customized by. The demo previously restated this chain field by field and had
|
|
78
|
+
// already drifted: it hardcoded `style: "powerline"` below and so ignored a
|
|
79
|
+
// config's own `globals.style`.
|
|
80
|
+
const effective = resolveEffectiveGlobals(
|
|
81
|
+
config,
|
|
82
|
+
() => null,
|
|
83
|
+
() => false,
|
|
97
84
|
);
|
|
85
|
+
const basePalette = paletteForThemeName(effective.theme);
|
|
86
|
+
const lookKey = lookKeyByName(config.looks, effective.look);
|
|
98
87
|
|
|
99
88
|
// A fresh store + registry for this run. (A hot-reloading daemon would
|
|
100
89
|
// dispose() the old pair and build new ones — see registerDslConfig's docs.)
|
|
@@ -128,26 +117,21 @@ try {
|
|
|
128
117
|
payload,
|
|
129
118
|
basePalette,
|
|
130
119
|
{
|
|
131
|
-
style:
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
colorCompatibility:
|
|
135
|
-
globals.colorCompatibility ?? DEFAULT_COLOR_COMPATIBILITY,
|
|
120
|
+
style: effective.style,
|
|
121
|
+
separator: effective.separator,
|
|
122
|
+
colorCompatibility: effective.colorCompatibility,
|
|
136
123
|
// [LAW:one-source-of-truth] Demo applies the same Claude-Code-UI
|
|
137
124
|
// reserve the daemon does so demo output matches the bytes a real
|
|
138
125
|
// statusline would emit at the same terminal width.
|
|
139
126
|
width: applyClaudeCodeReserve(
|
|
140
127
|
process.stdout.columns ?? DEFAULT_TERMINAL_WIDTH,
|
|
141
128
|
),
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
wrap: effectiveAutoWrap(null, globals.autoWrap),
|
|
146
|
-
padding: effectivePadding(null, globals.padding),
|
|
147
|
-
charset: globals.charset ?? DEFAULT_CHARSET,
|
|
129
|
+
wrap: effective.autoWrap,
|
|
130
|
+
padding: effective.padding,
|
|
131
|
+
charset: effective.charset,
|
|
148
132
|
},
|
|
149
133
|
undefined,
|
|
150
|
-
{ look: lookKey, preset },
|
|
134
|
+
{ look: lookKey, preset: effective.preset },
|
|
151
135
|
);
|
|
152
136
|
process.stdout.write(` ${line}\n`);
|
|
153
137
|
if (frame < FRAMES - 1) await sleep(FRAME_INTERVAL_MS);
|
package/src/themes/policy.ts
CHANGED
|
@@ -52,14 +52,30 @@ export function resolvePaletteName(name: string): string {
|
|
|
52
52
|
// default rather than throw or render something the label disagrees with
|
|
53
53
|
// [LAW:no-silent-failure] — the caller publishes what this returns as
|
|
54
54
|
// `<field>.effective`, so bar and label always trace to one value.
|
|
55
|
+
//
|
|
56
|
+
// [LAW:one-source-of-truth] `staged` is the RIGHTMOST rung of the precedence
|
|
57
|
+
// chain documented in src/config/presets.ts — a fragment some transient MODE of
|
|
58
|
+
// the bar puts on top while it is on (today: edit mode's `editGlobals`). It
|
|
59
|
+
// outranks even the session pick because it is decided LATER: a user picks a
|
|
60
|
+
// style, then afterwards enters edit mode. That is the same lifetime rule that
|
|
61
|
+
// forced the preset's own position, applied one rung further along, which is
|
|
62
|
+
// why it is a parameter of THIS function rather than a check at any call site —
|
|
63
|
+
// a chain with a rung missing from one field is exactly the drift a single
|
|
64
|
+
// resolver exists to prevent.
|
|
65
|
+
//
|
|
66
|
+
// [LAW:dataflow-not-control-flow] Absent ⇒ `undefined`, which the `??` chain
|
|
67
|
+
// skips by the same code path a session that never clicked skips its own rung.
|
|
68
|
+
// There is no "is edit mode on" branch anywhere below this line; the mode is
|
|
69
|
+
// carried entirely by whether this argument has a value.
|
|
55
70
|
export function effectiveGlobal<T>(
|
|
71
|
+
staged: T | null | undefined,
|
|
56
72
|
sessionPick: string | null,
|
|
57
73
|
configDefault: T | null | undefined,
|
|
58
74
|
floor: T,
|
|
59
75
|
parseSession: (raw: string) => T | null,
|
|
60
76
|
): T {
|
|
61
77
|
const picked = sessionPick === null ? null : parseSession(sessionPick);
|
|
62
|
-
return picked ?? configDefault ?? floor;
|
|
78
|
+
return staged ?? picked ?? configDefault ?? floor;
|
|
63
79
|
}
|
|
64
80
|
|
|
65
81
|
// The theme name a render should use, as data.
|
|
@@ -70,10 +86,12 @@ export function effectiveGlobal<T>(
|
|
|
70
86
|
// parse is identity: there is no membership to check here, and pretending
|
|
71
87
|
// otherwise would collapse names `paletteForThemeName` handles fine.
|
|
72
88
|
export function effectiveThemeName(
|
|
89
|
+
stagedPalette: string | undefined,
|
|
73
90
|
sessionTheme: string | null,
|
|
74
91
|
globalsPalette: string | undefined,
|
|
75
92
|
): string {
|
|
76
93
|
return effectiveGlobal(
|
|
94
|
+
stagedPalette,
|
|
77
95
|
sessionTheme,
|
|
78
96
|
globalsPalette,
|
|
79
97
|
"textual-dark",
|
|
@@ -110,7 +128,12 @@ export function listResolvablePaletteNames(): readonly string[] {
|
|
|
110
128
|
//
|
|
111
129
|
// The floor's membership is a load-time guarantee, not a runtime hope: the
|
|
112
130
|
// bundled stdlib ships it and merge-by-name cannot remove it.
|
|
131
|
+
//
|
|
132
|
+
// The staged rung runs through the SAME membership parse the other two do: a
|
|
133
|
+
// fragment naming a member the config does not declare is no more a pick than a
|
|
134
|
+
// stale session entry is, and collapses one rung onward rather than throwing.
|
|
113
135
|
export function effectiveMemberName(
|
|
136
|
+
stagedName: string | undefined,
|
|
114
137
|
sessionPick: string | null,
|
|
115
138
|
configDefault: string | undefined,
|
|
116
139
|
floor: string,
|
|
@@ -119,6 +142,7 @@ export function effectiveMemberName(
|
|
|
119
142
|
const member = (raw: string): string | null =>
|
|
120
143
|
Object.prototype.hasOwnProperty.call(declared, raw) ? raw : null;
|
|
121
144
|
return effectiveGlobal(
|
|
145
|
+
stagedName === undefined ? null : member(stagedName),
|
|
122
146
|
sessionPick,
|
|
123
147
|
member(configDefault ?? floor),
|
|
124
148
|
floor,
|
|
@@ -133,11 +157,18 @@ export function effectiveMemberName(
|
|
|
133
157
|
// carries. A named wrapper (not a bare call at each site) so the floor is
|
|
134
158
|
// spelled once and the three call sites cannot disagree about it.
|
|
135
159
|
export function effectiveLookName(
|
|
160
|
+
stagedLook: string | undefined,
|
|
136
161
|
sessionLook: string | null,
|
|
137
162
|
globalsLook: string | undefined,
|
|
138
163
|
declaredLooks: Readonly<Record<string, ThemeKey>>,
|
|
139
164
|
): string {
|
|
140
|
-
return effectiveMemberName(
|
|
165
|
+
return effectiveMemberName(
|
|
166
|
+
stagedLook,
|
|
167
|
+
sessionLook,
|
|
168
|
+
globalsLook,
|
|
169
|
+
"none",
|
|
170
|
+
declaredLooks,
|
|
171
|
+
);
|
|
141
172
|
}
|
|
142
173
|
|
|
143
174
|
// [LAW:single-enforcer] The one place an effective look NAME becomes the
|
|
@@ -196,11 +227,16 @@ export function isStripStyle(value: string): value is StripStyle {
|
|
|
196
227
|
// [LAW:one-source-of-truth]; test/session-globals.test.ts pins it with a stale
|
|
197
228
|
// pick over a valid non-floor default, the case the old tests never exercised.
|
|
198
229
|
export function effectiveStripStyle(
|
|
230
|
+
stagedStyle: StripStyle | undefined,
|
|
199
231
|
sessionStyle: string | null,
|
|
200
232
|
globalsStyle: StripStyle | undefined,
|
|
201
233
|
): StripStyle {
|
|
202
|
-
return effectiveGlobal(
|
|
203
|
-
|
|
234
|
+
return effectiveGlobal(
|
|
235
|
+
stagedStyle,
|
|
236
|
+
sessionStyle,
|
|
237
|
+
globalsStyle,
|
|
238
|
+
"powerline",
|
|
239
|
+
(raw) => (isStripStyle(raw) ? raw : null),
|
|
204
240
|
);
|
|
205
241
|
}
|
|
206
242
|
|
|
@@ -323,10 +359,12 @@ function parsePadding(raw: string): number | null {
|
|
|
323
359
|
// Whether a render wraps over-wide rows, as data. The session's pick over the
|
|
324
360
|
// config default over the on floor — `effectiveGlobal` with a boolean domain.
|
|
325
361
|
export function effectiveAutoWrap(
|
|
362
|
+
stagedAutoWrap: boolean | undefined,
|
|
326
363
|
sessionAutoWrap: string | null,
|
|
327
364
|
globalsAutoWrap: boolean | undefined,
|
|
328
365
|
): boolean {
|
|
329
366
|
return effectiveGlobal(
|
|
367
|
+
stagedAutoWrap,
|
|
330
368
|
sessionAutoWrap,
|
|
331
369
|
globalsAutoWrap,
|
|
332
370
|
DEFAULT_WRAP,
|
|
@@ -341,10 +379,12 @@ export function effectiveAutoWrap(
|
|
|
341
379
|
// own default is the honest answer rather than a render at a width nobody
|
|
342
380
|
// chose.
|
|
343
381
|
export function effectivePadding(
|
|
382
|
+
stagedPadding: number | undefined,
|
|
344
383
|
sessionPadding: string | null,
|
|
345
384
|
globalsPadding: number | undefined,
|
|
346
385
|
): number {
|
|
347
386
|
return effectiveGlobal(
|
|
387
|
+
stagedPadding,
|
|
348
388
|
sessionPadding,
|
|
349
389
|
globalsPadding,
|
|
350
390
|
DEFAULT_PADDING,
|