@promptctl/cc-candybar 1.26.0 → 1.28.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 +86 -85
- package/package.json +6 -6
- package/schema/cc-candybar.schema.json +193 -4
- package/src/check.ts +49 -27
- package/src/click/wire.ts +16 -0
- package/src/config/action.ts +57 -22
- package/src/config/default-dsl-config.ts +424 -55
- package/src/config/dsl-loader.ts +14 -2
- package/src/config/dsl-types.ts +59 -0
- package/src/config/loader/actions.ts +283 -109
- package/src/config/loader/cross-ref.ts +148 -28
- package/src/config/loader/emit-schema.ts +2 -0
- package/src/config/loader/globals.ts +118 -31
- package/src/config/loader/merge.ts +58 -1
- package/src/config/loader/persist-target.ts +32 -0
- package/src/config/loader/presets.ts +107 -0
- package/src/config/option-domain.ts +164 -0
- package/src/config/presets.ts +188 -0
- package/src/daemon/cache/git.ts +1 -1
- package/src/daemon/cache/render.ts +72 -7
- package/src/daemon/config-overrides-store.ts +322 -0
- package/src/daemon/paths.ts +10 -0
- package/src/daemon/render-payload.ts +84 -19
- package/src/daemon/server.ts +68 -55
- package/src/daemon/verbs/config-validators.ts +127 -0
- package/src/daemon/verbs/index.ts +129 -2
- package/src/daemon/verbs/state-validators.ts +98 -586
- package/src/daemon/verbs/validator-registry.ts +457 -0
- package/src/demo/dsl.ts +17 -10
- package/src/dsl/node-registry.ts +54 -39
- package/src/dsl/render.ts +158 -46
- package/src/help-text.ts +3 -3
- package/src/install/index.ts +2 -2
- package/src/render/action.ts +155 -33
- package/src/render/active-segment.ts +78 -0
- package/src/render/menu.ts +16 -11
- package/src/render/picker.ts +51 -13
- package/src/render/segment-color.ts +74 -0
- package/src/segments/git.ts +389 -48
- package/src/template-engine/colors.ts +67 -45
- package/src/template-engine/engine.ts +11 -12
- package/src/themes/index.ts +1 -4
- package/src/themes/palette-resolvers.ts +22 -30
- package/src/themes/policy.ts +37 -16
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
// The one record describing the segment a template is being evaluated for.
|
|
2
|
+
//
|
|
3
|
+
// [LAW:one-source-of-truth] Several template features need to know something
|
|
4
|
+
// about the enclosing segment — `{{ menu }}` derives its identity from the
|
|
5
|
+
// segment's name, `{{ color }}` must read the segment's own (transposed)
|
|
6
|
+
// palette, `{{ bgOf }}` must read the segment's own resolved background. Each
|
|
7
|
+
// of those could have carried its own published "current segment" pointer, and
|
|
8
|
+
// then two features could disagree about which segment is current. One record,
|
|
9
|
+
// one publisher, one clock.
|
|
10
|
+
//
|
|
11
|
+
// [LAW:no-ambient-temporal-coupling] This is *published state*, not ambient
|
|
12
|
+
// context: the render walk sets it before evaluating a segment's templates and
|
|
13
|
+
// clears it after, and nothing else writes it. The phase structure within a
|
|
14
|
+
// segment is likewise state rather than luck — `bg` is genuinely undefined
|
|
15
|
+
// while the `bg:` template is itself being evaluated, because at that moment
|
|
16
|
+
// the background is the thing being computed. Readers get a message naming the
|
|
17
|
+
// phase instead of a plausible-looking wrong color.
|
|
18
|
+
|
|
19
|
+
import type { ColorRgba, Palette } from "@promptctl/rich-js";
|
|
20
|
+
|
|
21
|
+
export interface ActiveSegment {
|
|
22
|
+
/** The segment's declared name — `{{ menu }}` derives its identity from it. */
|
|
23
|
+
readonly segName: string;
|
|
24
|
+
/**
|
|
25
|
+
* The palette this segment's colors resolve from: the base theme (session
|
|
26
|
+
* choice over config default, or an explicit per-segment `palette:` pin)
|
|
27
|
+
* after the render's look and this segment's hue shift.
|
|
28
|
+
*
|
|
29
|
+
* Template bodies read colors through THIS, not through a palette captured
|
|
30
|
+
* when the config was loaded — otherwise `{{ color "primary" }}` inside a
|
|
31
|
+
* segment paints from a different palette than the cell it sits in.
|
|
32
|
+
*/
|
|
33
|
+
readonly palette: Palette;
|
|
34
|
+
/**
|
|
35
|
+
* The segment's resolved background, once known.
|
|
36
|
+
*
|
|
37
|
+
* Undefined during evaluation of the segment's own `bg:` template — the
|
|
38
|
+
* ordering is bg, then fg, then body, and a background cannot be an input to
|
|
39
|
+
* computing itself.
|
|
40
|
+
*/
|
|
41
|
+
bg: ColorRgba | undefined;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** The published pointer. Null between segments. */
|
|
45
|
+
export interface ActiveSegmentRef {
|
|
46
|
+
current: ActiveSegment | null;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function createActiveSegmentRef(): ActiveSegmentRef {
|
|
50
|
+
return { current: null };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Read the active segment, or fail with a message that says *why* nothing is
|
|
55
|
+
* active rather than what is missing.
|
|
56
|
+
*
|
|
57
|
+
* [LAW:no-defensive-null-guards] Null here is never a state to route around —
|
|
58
|
+
* it means a segment-scoped template function fired outside a segment render,
|
|
59
|
+
* which is either a wiring bug or an author using the function somewhere it
|
|
60
|
+
* cannot mean anything (a variable template, a node `when`). Both need to be
|
|
61
|
+
* seen, and in cc-candybar a thrown template error surfaces as a visible ⚠
|
|
62
|
+
* cell that `cc-candybar check` fails on. [LAW:no-silent-failure]
|
|
63
|
+
*/
|
|
64
|
+
export function requireActiveSegment(
|
|
65
|
+
ref: ActiveSegmentRef,
|
|
66
|
+
func: string,
|
|
67
|
+
): ActiveSegment {
|
|
68
|
+
const active = ref.current;
|
|
69
|
+
if (active === null) {
|
|
70
|
+
throw new Error(
|
|
71
|
+
`{{ ${func} }} is only available inside a segment's templates — ` +
|
|
72
|
+
`there is no active segment here. Segment-scoped functions cannot be ` +
|
|
73
|
+
`used in variable declarations or layout-node "when" predicates, ` +
|
|
74
|
+
`which are evaluated outside any segment.`,
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
return active;
|
|
78
|
+
}
|
package/src/render/menu.ts
CHANGED
|
@@ -50,25 +50,30 @@ import {
|
|
|
50
50
|
import { effectsUrl, VERB_SET_STATE } from "../click/wire.js";
|
|
51
51
|
import { linkFragment, readVar, type ActionRuntime } from "./action.js";
|
|
52
52
|
import { renderPicker } from "./picker.js";
|
|
53
|
+
import type { ActiveSegmentRef } from "./active-segment.js";
|
|
53
54
|
|
|
54
|
-
// [LAW:
|
|
55
|
-
//
|
|
56
|
-
//
|
|
57
|
-
//
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
}
|
|
55
|
+
// [LAW:one-type-per-behavior] A `{{ menu }}` needs one structural fact it cannot
|
|
56
|
+
// see about itself — the name of the segment it renders inside. That used to be
|
|
57
|
+
// its own `MenuPlacement` type; it is now a field on the ONE active-segment
|
|
58
|
+
// record the walk publishes (see render/active-segment.ts), because "which
|
|
59
|
+
// segment is rendering" is a single fact and a per-feature copy of it is a
|
|
60
|
+
// second clock. The menu reads `segName` and ignores the rest.
|
|
61
61
|
|
|
62
62
|
// [LAW:locality-or-seam] The runtime the `menu` func closes over. It shares the
|
|
63
63
|
// ACTION runtime (the menu's glyph and body resolve their actions/state from the
|
|
64
64
|
// same compiled table + store as every other helper) and READS the walk-published
|
|
65
|
-
//
|
|
66
|
-
// mutated only by the single owner (the render walk,
|
|
65
|
+
// active segment — both inputs, never written by the helper. The record is
|
|
66
|
+
// mutated only by the single owner (the render walk, around each segment eval) —
|
|
67
67
|
// the spatial cousin of the hue cursor, one mutator, never ambient.
|
|
68
68
|
// [LAW:no-ambient-temporal-coupling]
|
|
69
69
|
export interface MenuRuntime {
|
|
70
70
|
readonly action: ActionRuntime;
|
|
71
|
-
|
|
71
|
+
// [LAW:one-source-of-truth] The menu does not publish its own "which segment
|
|
72
|
+
// is current" pointer — it reads the ONE record the render walk publishes for
|
|
73
|
+
// every segment-scoped feature (the palette `{{ color }}` resolves against and
|
|
74
|
+
// the background `{{ bgOf }}` returns ride the same record). A second pointer
|
|
75
|
+
// would be a second clock for the same fact.
|
|
76
|
+
readonly activeSegment: ActiveSegmentRef;
|
|
72
77
|
}
|
|
73
78
|
|
|
74
79
|
// [LAW:effects-at-boundaries] The body a `{{ menu }}` drops below its row rides as
|
|
@@ -95,7 +100,7 @@ function renderMenu(
|
|
|
95
100
|
options: MenuOptions,
|
|
96
101
|
runtime: MenuRuntime,
|
|
97
102
|
): RichText {
|
|
98
|
-
const placement = runtime.current;
|
|
103
|
+
const placement = runtime.activeSegment.current;
|
|
99
104
|
// [LAW:no-defensive-null-guards] The walk publishes a placement before every
|
|
100
105
|
// segment template evaluates; a `{{ menu }}` only renders inside a segment. A
|
|
101
106
|
// null here is a wiring bug (the func fired with no current segment), surfaced
|
package/src/render/picker.ts
CHANGED
|
@@ -28,7 +28,7 @@ import type { FuncMap } from "@promptctl/go-template-js";
|
|
|
28
28
|
import { toNumber } from "../var-system/types.js";
|
|
29
29
|
import { stripChromeCols } from "./strip.js";
|
|
30
30
|
import { TERM_COLS_VAR } from "../config/dsl-types.js";
|
|
31
|
-
import { effectsUrl, VERB_SET_STATE } from "../click/wire.js";
|
|
31
|
+
import { effectsUrl, VERB_SET_CONFIG, VERB_SET_STATE } from "../click/wire.js";
|
|
32
32
|
import {
|
|
33
33
|
linkFragment,
|
|
34
34
|
readVar,
|
|
@@ -136,6 +136,31 @@ function requireKind<K extends CompiledActionDecl["kind"]>(
|
|
|
136
136
|
return action as Extract<CompiledActionDecl, { kind: K }>;
|
|
137
137
|
}
|
|
138
138
|
|
|
139
|
+
// [LAW:one-source-of-truth] The apply action a picker grid binds to is EITHER
|
|
140
|
+
// of set-option's two durability twins (src/render/action.ts's persist-*
|
|
141
|
+
// mirrors set-*'s shapes one for one) — a picker over `persist("charset",
|
|
142
|
+
// from:"charsets")` is exactly as legal as one over `set("theme",
|
|
143
|
+
// from:"themes")`, differing only in which wire verb the option click emits
|
|
144
|
+
// (VERB_SET_CONFIG vs VERB_SET_STATE), never in shape (both carry key,
|
|
145
|
+
// stateVar, options). Rejecting persist-option here would be an artificial
|
|
146
|
+
// gap: the same option-domain gate (deriveActionValidators) covers both kinds
|
|
147
|
+
// identically, so there is nothing about "picker" that's set-only.
|
|
148
|
+
function requireOptionKind(
|
|
149
|
+
runtime: ActionRuntime,
|
|
150
|
+
name: string,
|
|
151
|
+
): Extract<CompiledActionDecl, { kind: "set-option" | "persist-option" }> {
|
|
152
|
+
const action = runtime.compiled.get(name);
|
|
153
|
+
if (
|
|
154
|
+
!action ||
|
|
155
|
+
(action.kind !== "set-option" && action.kind !== "persist-option")
|
|
156
|
+
) {
|
|
157
|
+
throw new Error(
|
|
158
|
+
`picker references action "${name}" which must be a set-option or persist-option action ({ set, from } or { persist, from }), got ${action ? `a ${action.kind} action` : "no such action"}`,
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
return action;
|
|
162
|
+
}
|
|
163
|
+
|
|
139
164
|
// [LAW:dataflow-not-control-flow] The page value (and the live width) select
|
|
140
165
|
// which option cells render and which boundary arrows exist — a boundary arrow is
|
|
141
166
|
// an ABSENT fragment, never a skipped branch. ←/→ navigate the page key
|
|
@@ -155,12 +180,7 @@ export function renderPicker(
|
|
|
155
180
|
paged: boolean,
|
|
156
181
|
runtime: ActionRuntime,
|
|
157
182
|
): RichText {
|
|
158
|
-
const apply =
|
|
159
|
-
runtime,
|
|
160
|
-
applyName,
|
|
161
|
-
"set-option",
|
|
162
|
-
"a set-option action ({ set, from })",
|
|
163
|
-
);
|
|
183
|
+
const apply = requireOptionKind(runtime, applyName);
|
|
164
184
|
const store = runtime.store;
|
|
165
185
|
const sessionId = readVar(store, "session.id");
|
|
166
186
|
const current = readVar(store, apply.stateVar);
|
|
@@ -222,13 +242,31 @@ export function renderPicker(
|
|
|
222
242
|
const closeUrl = effectsUrl([
|
|
223
243
|
{ verb: VERB_SET_STATE, args: [sessionId, ...closeFlat] },
|
|
224
244
|
]);
|
|
245
|
+
// [LAW:one-source-of-truth] A set-option apply folds its closeOnPick pairs
|
|
246
|
+
// into ONE set-state batch (setState is variadic — see daemon/verbs). A
|
|
247
|
+
// persist-option apply cannot: setConfig takes exactly one (key, value), so
|
|
248
|
+
// its close pairs (always SessionState — open/page live there regardless of
|
|
249
|
+
// the apply's durability) ride as a SECOND effect in the same dispatch,
|
|
250
|
+
// still one atomic click via effectsUrl's array.
|
|
225
251
|
const optionUrl = (option: string): string =>
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
252
|
+
apply.kind === "persist-option"
|
|
253
|
+
? effectsUrl([
|
|
254
|
+
{ verb: VERB_SET_CONFIG, args: [sessionId, apply.key, option] },
|
|
255
|
+
...(closeOnPick
|
|
256
|
+
? [{ verb: VERB_SET_STATE, args: [sessionId, ...closeFlat] }]
|
|
257
|
+
: []),
|
|
258
|
+
])
|
|
259
|
+
: effectsUrl([
|
|
260
|
+
{
|
|
261
|
+
verb: VERB_SET_STATE,
|
|
262
|
+
args: [
|
|
263
|
+
sessionId,
|
|
264
|
+
apply.key,
|
|
265
|
+
option,
|
|
266
|
+
...(closeOnPick ? closeFlat : []),
|
|
267
|
+
],
|
|
268
|
+
},
|
|
269
|
+
]);
|
|
232
270
|
|
|
233
271
|
const frags: RichText[] = [linkFragment(PICKER_CLOSE, closeUrl, false)];
|
|
234
272
|
if (pageIdx > 0) {
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
// Segment-scoped color functions: the seam between rich-js's palette-free
|
|
2
|
+
// color vocabulary and cc-candybar's notion of a segment.
|
|
3
|
+
//
|
|
4
|
+
// rich-js owns every color operation and knows nothing about segments;
|
|
5
|
+
// cc-candybar owns segments and performs no color arithmetic of its own
|
|
6
|
+
// [LAW:rich-js-owns-color-math]. This module is exactly the join: it supplies
|
|
7
|
+
// rich-js's `color` with *which* palette, and adds the one function whose
|
|
8
|
+
// meaning is candybar-specific — `bgOf`, the background of the segment
|
|
9
|
+
// currently rendering. [LAW:one-way-deps]
|
|
10
|
+
|
|
11
|
+
import type { FuncMap, TemplateFunc } from "@promptctl/go-template-js";
|
|
12
|
+
import { paletteFuncs } from "@promptctl/rich-js/template-bindings";
|
|
13
|
+
import {
|
|
14
|
+
requireActiveSegment,
|
|
15
|
+
type ActiveSegmentRef,
|
|
16
|
+
} from "./active-segment.js";
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Bind `color` and `bgOf` to the segment the walk has published.
|
|
20
|
+
*
|
|
21
|
+
* **Why `color` reads a live palette.** A segment's rendered palette is not a
|
|
22
|
+
* property of the loaded config — it is the base theme (session choice over
|
|
23
|
+
* config default) adapted by the render's look and the segment's hue shift,
|
|
24
|
+
* all resolved per render, per segment. Binding `color` to a palette captured
|
|
25
|
+
* when the config loaded put the *body* of a template on a different palette
|
|
26
|
+
* than the `bg:`/`fg:` of the very same segment, so `{{ color "primary" }}`
|
|
27
|
+
* and `bg: "primary"` could name one thing and paint two.
|
|
28
|
+
* [LAW:one-source-of-truth]
|
|
29
|
+
*
|
|
30
|
+
* That divergence was not exotic. Any session theme click moved the segment's
|
|
31
|
+
* background while leaving every in-body semantic color where it was; a look
|
|
32
|
+
* or a per-segment hue rotation did the same. Reading the live palette makes
|
|
33
|
+
* the two agree by construction rather than by coincidence.
|
|
34
|
+
*
|
|
35
|
+
* **Why `bgOf` exists.** De-emphasis — drawing labels, punctuation and ids
|
|
36
|
+
* quieter than the facts they frame — is "move this color toward the
|
|
37
|
+
* background." A palette's own `foreground-muted` blends toward the *theme's*
|
|
38
|
+
* background, which is the wrong target for any segment not painted in it: a
|
|
39
|
+
* segment on `surface-active` needs its muted text blended toward
|
|
40
|
+
* `surface-active`. Only the segment knows its own background, so only the
|
|
41
|
+
* segment can supply it:
|
|
42
|
+
*
|
|
43
|
+
* ```
|
|
44
|
+
* {{ $muted := mix (color "foreground") (bgOf) 65 }}
|
|
45
|
+
* {{ fg $muted .git.repoName }} {{ fg (color "primary") .git.branch }}
|
|
46
|
+
* ```
|
|
47
|
+
*
|
|
48
|
+
* This is also what makes contrast reachable: `{{ fg (contrastOn (bgOf)) … }}`
|
|
49
|
+
* asks a question about a real background, where the old spec-grammar `"auto"`
|
|
50
|
+
* could only ever be handed a hardcoded literal.
|
|
51
|
+
*/
|
|
52
|
+
export function segmentColorFuncs(ref: ActiveSegmentRef): FuncMap {
|
|
53
|
+
const bgOf: TemplateFunc = {
|
|
54
|
+
fn: (() => {
|
|
55
|
+
const active = requireActiveSegment(ref, "bgOf");
|
|
56
|
+
if (active.bg === undefined) {
|
|
57
|
+
throw new Error(
|
|
58
|
+
`{{ bgOf }} is not available while segments.${active.segName}'s own ` +
|
|
59
|
+
`"bg:" is being evaluated — the background is what that template ` +
|
|
60
|
+
`computes. Reach for a palette color there instead, e.g. ` +
|
|
61
|
+
`bg: '{{ darken (color "surface") 1 }}'.`,
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
return active.bg.hex;
|
|
65
|
+
}) as TemplateFunc["fn"],
|
|
66
|
+
argTypes: [],
|
|
67
|
+
returnType: "string",
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
return {
|
|
71
|
+
...paletteFuncs(() => requireActiveSegment(ref, "color").palette),
|
|
72
|
+
bgOf,
|
|
73
|
+
};
|
|
74
|
+
}
|