@promptctl/cc-candybar 1.42.1 → 1.43.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.
Files changed (126) hide show
  1. package/dist/index.mjs +72 -71
  2. package/package.json +5 -6
  3. package/src/check.ts +0 -478
  4. package/src/cli-flags.ts +0 -8
  5. package/src/click/wire.ts +0 -158
  6. package/src/config/action.ts +0 -329
  7. package/src/config/cli.ts +0 -71
  8. package/src/config/default-dsl-config.ts +0 -1645
  9. package/src/config/disclosure.ts +0 -170
  10. package/src/config/dsl-loader.ts +0 -339
  11. package/src/config/dsl-types.ts +0 -581
  12. package/src/config/edit-chrome.ts +0 -559
  13. package/src/config/help.ts +0 -151
  14. package/src/config/ident.ts +0 -22
  15. package/src/config/layout-ops.ts +0 -177
  16. package/src/config/loader/actions.ts +0 -972
  17. package/src/config/loader/cache.ts +0 -206
  18. package/src/config/loader/cross-ref.ts +0 -714
  19. package/src/config/loader/cycles.ts +0 -148
  20. package/src/config/loader/diagnostics.ts +0 -99
  21. package/src/config/loader/discovery.ts +0 -182
  22. package/src/config/loader/edit-mode.ts +0 -137
  23. package/src/config/loader/emit-schema.ts +0 -68
  24. package/src/config/loader/globals.ts +0 -269
  25. package/src/config/loader/helpers.ts +0 -48
  26. package/src/config/loader/layout.ts +0 -693
  27. package/src/config/loader/looks.ts +0 -96
  28. package/src/config/loader/menu-synth.ts +0 -435
  29. package/src/config/loader/merge.ts +0 -115
  30. package/src/config/loader/persist-target.ts +0 -67
  31. package/src/config/loader/presets.ts +0 -119
  32. package/src/config/loader/refs.ts +0 -100
  33. package/src/config/loader/reserved-namespace.ts +0 -38
  34. package/src/config/loader/segments.ts +0 -120
  35. package/src/config/loader/validate-core.ts +0 -737
  36. package/src/config/loader/variables.ts +0 -260
  37. package/src/config/menu-keys.ts +0 -139
  38. package/src/config/option-domain.ts +0 -164
  39. package/src/config/presets.ts +0 -326
  40. package/src/config/settings-menu.ts +0 -775
  41. package/src/daemon/acquire.ts +0 -684
  42. package/src/daemon/cache/git.ts +0 -649
  43. package/src/daemon/cache/render.ts +0 -623
  44. package/src/daemon/cache/session-usage-store.ts +0 -720
  45. package/src/daemon/cache/watchers.ts +0 -249
  46. package/src/daemon/client-debug.ts +0 -120
  47. package/src/daemon/client-stats.ts +0 -130
  48. package/src/daemon/client-transport.ts +0 -273
  49. package/src/daemon/client.ts +0 -78
  50. package/src/daemon/config-overrides-store.ts +0 -663
  51. package/src/daemon/debug-types.ts +0 -91
  52. package/src/daemon/debug.ts +0 -264
  53. package/src/daemon/fork-bomb-breaker.ts +0 -351
  54. package/src/daemon/limits.ts +0 -211
  55. package/src/daemon/log.ts +0 -81
  56. package/src/daemon/parent-watchdog.ts +0 -87
  57. package/src/daemon/paths.ts +0 -211
  58. package/src/daemon/process-fingerprint.ts +0 -146
  59. package/src/daemon/protocol.ts +0 -292
  60. package/src/daemon/render-payload.ts +0 -1256
  61. package/src/daemon/server.ts +0 -1330
  62. package/src/daemon/session-state-file.ts +0 -108
  63. package/src/daemon/session-state.ts +0 -237
  64. package/src/daemon/socket-lease.ts +0 -209
  65. package/src/daemon/socket-ownership.ts +0 -209
  66. package/src/daemon/stats.ts +0 -235
  67. package/src/daemon/verbs/config-validators.ts +0 -250
  68. package/src/daemon/verbs/index.ts +0 -706
  69. package/src/daemon/verbs/state-validators.ts +0 -249
  70. package/src/daemon/verbs/validator-registry.ts +0 -457
  71. package/src/demo/dsl.ts +0 -143
  72. package/src/demo/mock-data.ts +0 -67
  73. package/src/demo/statusline.json5 +0 -94
  74. package/src/dsl/node-registry.ts +0 -374
  75. package/src/dsl/render.ts +0 -803
  76. package/src/help-text.ts +0 -90
  77. package/src/index.ts +0 -210
  78. package/src/install/currency.ts +0 -197
  79. package/src/install/index.ts +0 -557
  80. package/src/proc/launch.ts +0 -459
  81. package/src/proc/stats-handle.ts +0 -13
  82. package/src/render/action.ts +0 -883
  83. package/src/render/active-segment.ts +0 -78
  84. package/src/render/diagnostic-style.ts +0 -23
  85. package/src/render/diagnostic-text.ts +0 -77
  86. package/src/render/error-glyph.ts +0 -53
  87. package/src/render/menu.ts +0 -257
  88. package/src/render/outcome-plan.ts +0 -45
  89. package/src/render/picker.ts +0 -372
  90. package/src/render/segment-color.ts +0 -74
  91. package/src/render/split-lines.ts +0 -51
  92. package/src/render/strip.ts +0 -228
  93. package/src/segments/cache.ts +0 -131
  94. package/src/segments/context.ts +0 -190
  95. package/src/segments/git.ts +0 -1084
  96. package/src/segments/metrics.ts +0 -187
  97. package/src/segments/pricing.ts +0 -452
  98. package/src/segments/session.ts +0 -23
  99. package/src/segments/tmux.ts +0 -74
  100. package/src/template-engine/cells.ts +0 -90
  101. package/src/template-engine/colors.ts +0 -124
  102. package/src/template-engine/engine.ts +0 -108
  103. package/src/template-engine/funcs.ts +0 -232
  104. package/src/template-engine/index.ts +0 -11
  105. package/src/template-engine/layout.ts +0 -133
  106. package/src/template-engine/scope.ts +0 -62
  107. package/src/template-engine/sparkline.ts +0 -79
  108. package/src/themes/index.ts +0 -20
  109. package/src/themes/palette-resolvers.ts +0 -84
  110. package/src/themes/policy.ts +0 -393
  111. package/src/utils/cache.ts +0 -206
  112. package/src/utils/claude.ts +0 -683
  113. package/src/utils/color-support.ts +0 -118
  114. package/src/utils/formatters.ts +0 -99
  115. package/src/utils/logger.ts +0 -5
  116. package/src/utils/outcome.ts +0 -33
  117. package/src/utils/schema-validator.ts +0 -126
  118. package/src/utils/single-flight.ts +0 -57
  119. package/src/utils/terminal-width.ts +0 -51
  120. package/src/utils/terminal.ts +0 -11
  121. package/src/utils/transcript-fs.ts +0 -279
  122. package/src/var-system/index.ts +0 -24
  123. package/src/var-system/sources.ts +0 -1047
  124. package/src/var-system/store.ts +0 -223
  125. package/src/var-system/types.ts +0 -57
  126. package/src/version.ts +0 -17
@@ -1,372 +0,0 @@
1
- // [LAW:locality-or-seam] A `{{ picker "applyAction" "pageAction" closeOnPick
2
- // paged }}` call renders a width-fit grid of option cells over the action table:
3
- // each option cell APPLIES the named option action (and, when closeOnPick, also
4
- // resets the named page action's key in the SAME atomic write); ✕/←/→ navigate
5
- // the page cursor. The picker is a pure RENDER helper — it owns no state and no
6
- // new gate. It references two ALREADY-declared, ALREADY-gated actions by name
7
- // (the apply set-option → its option domain + the theme key's allow-list gate;
8
- // the page set-int → the page key's int gate), so the rendered clicks and the
9
- // wire gate share one source: the action table.
10
- //
11
- // [LAW:one-type-per-behavior] There is no `menu`/`picker` TYPE — a picker is
12
- // content a segment template pulls in, exactly like `action`. This is the
13
- // successor to the deleted menu/buttons widget runtime: the same `paginate` fold
14
- // and ←/→/✕ projection, re-expressed over named actions instead of a widget union.
15
- //
16
- // [LAW:dataflow-not-control-flow] Paged vs wrap is ONE value, not a mode: the
17
- // `paged` flag selects the available width passed to `paginate` (term.cols vs
18
- // Infinity). Infinite width ⇒ one page ⇒ the long line wraps via FlexStrip; finite
19
- // ⇒ a sliced page with ←/→. The same fold, same emit pipeline; the width value
20
- // (and the matching noWrap) select the shape.
21
- //
22
- // [LAW:one-way-deps] Lives in render/ (depends on template-engine/ + ./action.js),
23
- // injected into the engine by the caller (registerDslConfig hands pickerFuncs in
24
- // as data). The generic engine never imports this module.
25
-
26
- import { RichText } from "@promptctl/rich-js";
27
- import type { FuncMap } from "@promptctl/go-template-js";
28
- import { toNumber } from "../var-system/types.js";
29
- import { stripChromeCols } from "./strip.js";
30
- import { TERM_COLS_VAR } from "../config/dsl-types.js";
31
- import { effectsUrl, VERB_SET_STATE } from "../click/wire.js";
32
- import {
33
- activeDestination,
34
- linkFragment,
35
- realize,
36
- readVar,
37
- type ActionRuntime,
38
- type CompiledActionDecl,
39
- } from "./action.js";
40
- import { DISCLOSURE_GLYPH_CLOSE } from "../config/disclosure.js";
41
-
42
- const PICKER_PREV = "←";
43
- const PICKER_NEXT = "→";
44
-
45
- // [LAW:single-enforcer] One display-width measure — rich-js's cellLength, the
46
- // same algebra FlexStrip wraps by — so pagination fits the line the strip
47
- // produces. No second width function.
48
- function cellWidth(text: string): number {
49
- return new RichText(text).cellLength;
50
- }
51
-
52
- // [LAW:dataflow-not-control-flow] A pure function of (item widths, available
53
- // width, reserved width): greedy fill into pages, each reserving room for the
54
- // ←/→/✕ affordances. The `page` value selects the slice; an oversized lone item
55
- // gets its own page (it can't be split). Infinite width = one page (the wrap
56
- // case — everything on one line, FlexStrip breaks it). Exported for unit testing.
57
- export function paginate(
58
- widths: readonly number[],
59
- available: number,
60
- reserve: number,
61
- ): number[][] {
62
- if (!Number.isFinite(available)) {
63
- return widths.length > 0 ? [widths.map((_, i) => i)] : [];
64
- }
65
- const usable = Math.max(1, available - reserve);
66
- const pages: number[][] = [];
67
- let cur: number[] = [];
68
- let curW = 0;
69
- for (let i = 0; i < widths.length; i++) {
70
- const w = widths[i]!;
71
- if (cur.length === 0) {
72
- cur = [i];
73
- curW = w;
74
- } else if (curW + 1 + w <= usable) {
75
- cur.push(i);
76
- curW += 1 + w;
77
- } else {
78
- pages.push(cur);
79
- cur = [i];
80
- curW = w;
81
- }
82
- }
83
- if (cur.length > 0) pages.push(cur);
84
- return pages;
85
- }
86
-
87
- // [LAW:dataflow-not-control-flow] Join link-bearing spans with single-space
88
- // separators into ONE RichText (a picker is one `{{ picker }}` expression, so it
89
- // must emit one value; the option/affordance cells ride as spans on it). `noWrap`
90
- // is the `paged` value: a paged page is one line that must not wrap; a wrap-mode
91
- // run is the long line FlexStrip is ALLOWED to break across lines.
92
- function assemble(frags: readonly RichText[], paged: boolean): RichText {
93
- const spaced: RichText[] = [];
94
- for (const frag of frags) {
95
- if (spaced.length > 0) spaced.push(new RichText(" "));
96
- spaced.push(frag);
97
- }
98
- const assembled = RichText.fromFragments(spaced);
99
- assembled.noWrap = paged;
100
- assembled.end = "";
101
- return assembled;
102
- }
103
-
104
- // [LAW:types-are-the-program] The page cursor a picker paginates by: the
105
- // SessionState `key` its ←/→/✕ clicks write and the `stateVar` that reads the
106
- // live page back. The standalone `{{ picker }}` resolves it from its named
107
- // set-int page action; a `{{ menu }}` derives it from the menu's identity
108
- // (menuPageKey) — one value shape, two provenances, one renderer.
109
- export interface PickerPage {
110
- readonly key: string;
111
- readonly stateVar: string;
112
- }
113
-
114
- // [LAW:dataflow-not-control-flow] What a "close" WRITES, as data — the (key,
115
- // value) pairs folded into one atomic set-state by both the ✕ affordance and a
116
- // closeOnPick option click. The standalone picker closes by paging to -1 (the
117
- // when-gate idiom); a menu closes by writing its disclosure key back to the
118
- // closed sentinel and resetting its page cursor. The picker itself never
119
- // branches on which world it is in — the writes flow in.
120
- export type CloseWrites = ReadonlyArray<readonly [key: string, value: string]>;
121
-
122
- // [LAW:no-defensive-null-guards] The loader proves both picker arg names resolve
123
- // to declared actions; this asserts the KIND each must be (apply ⇒ set-option,
124
- // page ⇒ set-int) — a wrong kind is an author error surfaced loudly at render
125
- // (composeWithDiagnostics shows it), not a silent empty picker.
126
- function requireKind<K extends CompiledActionDecl["kind"]>(
127
- runtime: ActionRuntime,
128
- name: string,
129
- kind: K,
130
- shape: string,
131
- ): Extract<CompiledActionDecl, { kind: K }> {
132
- const action = runtime.compiled.get(name);
133
- if (!action || action.kind !== kind) {
134
- throw new Error(
135
- `picker references action "${name}" which must be ${shape}, got ${action ? `a ${action.kind} action` : "no such action"}`,
136
- );
137
- }
138
- return action as Extract<CompiledActionDecl, { kind: K }>;
139
- }
140
-
141
- // [LAW:one-source-of-truth] The apply action a picker grid binds to is one of
142
- // THREE option-domain-driven kinds (src/render/action.ts): set-option/
143
- // persist-option (a picked value is WRITTEN VERBATIM — set-option's two
144
- // durability twins, differing only in wire verb VERB_SET_CONFIG vs
145
- // VERB_SET_STATE) or layout-op-option (a picked value is ENCODED into a
146
- // structural LayoutOp before writing — brandon-layout-edit-2gc.3's
147
- // `insertSegmentFrom`). All three share the same option-domain gate
148
- // (deriveActionValidators/deriveConfigActionValidators) and the same "pick a
149
- // cell, apply it" shape; only WHAT the click writes differs, which is
150
- // realize()'s job, not the picker's. Rejecting any of the three here would be
151
- // an artificial gap — there is nothing about "picker" that excludes one kind.
152
- function requireOptionKind(
153
- runtime: ActionRuntime,
154
- name: string,
155
- ): Extract<
156
- CompiledActionDecl,
157
- { kind: "set-option" | "persist-option" | "layout-op-option" }
158
- > {
159
- // [LAW:dataflow-not-control-flow] A dual-destination action resolves to the
160
- // half its selector names BEFORE the kind check, so a picker over a dual is
161
- // a picker over whichever option kind is live — the grid, its current-mark,
162
- // and its close folding are the code they already were. The check below then
163
- // still names a real, single-destination kind in its error.
164
- const declared = runtime.compiled.get(name);
165
- const action = declared
166
- ? activeDestination(declared, runtime.store)
167
- : declared;
168
- if (
169
- !action ||
170
- (action.kind !== "set-option" &&
171
- action.kind !== "persist-option" &&
172
- action.kind !== "layout-op-option")
173
- ) {
174
- throw new Error(
175
- `picker references action "${name}" which must be a set-option, persist-option, or layout-op-option action ({ set, from }, { persist, from }, or { persist, insertSegmentFrom, anchor, relation }), got ${action ? `a ${action.kind} action` : "no such action"}`,
176
- );
177
- }
178
- return action;
179
- }
180
-
181
- // [LAW:dataflow-not-control-flow] The page value (and the live width) select
182
- // which option cells render and which boundary arrows exist — a boundary arrow is
183
- // an ABSENT fragment, never a skipped branch. ←/→ navigate the page key
184
- // (render-computed p±1); ✕ performs the caller-supplied close writes; each
185
- // option click APPLIES its option AND (when closeOnPick) folds the same close
186
- // writes into one atomic set-state — the caller owns what closing means, so the
187
- // author never re-states a key.
188
- // [LAW:single-enforcer] Exported so the `{{ menu }}` helper renders its body
189
- // through the SAME picker renderer — a menu body IS a picker grid; there is no
190
- // second grid implementation to drift. The menu adds only the disclosure
191
- // wrapper, never a parallel picker.
192
- export function renderPicker(
193
- applyName: string,
194
- page: PickerPage,
195
- close: CloseWrites,
196
- closeOnPick: boolean,
197
- paged: boolean,
198
- runtime: ActionRuntime,
199
- ): RichText {
200
- const apply = requireOptionKind(runtime, applyName);
201
- // [LAW:one-source-of-truth] The GRID reads the resolved half above (its
202
- // options, its current-mark); the CLICK is realized from the declaration
203
- // itself, through the same fold `{{ action }}` uses. That is what carries a
204
- // dual's session clear into a picked option — the picker never learns what a
205
- // dual is, and there is no second projection of "what does this option
206
- // write" to drift from realize's.
207
- const declared = runtime.compiled.get(applyName)!;
208
- const store = runtime.store;
209
- const sessionId = readVar(store, "session.id");
210
- // [LAW:no-defensive-null-guards] layout-op-option carries no `stateVar` —
211
- // a structural insert is a one-shot trigger, not a persisted single value,
212
- // so there is no "current selection" to mark. `undefined` here (never a
213
- // magic sentinel string) makes every option's `option === current` compare
214
- // false below, structurally rather than by accident.
215
- const current =
216
- "stateVar" in apply ? readVar(store, apply.stateVar) : undefined;
217
- const widths = apply.options.map(cellWidth);
218
-
219
- // ✕ is always present; ←/→ appear only on a multi-page menu. Reserve arrow
220
- // space only after a first pass proves it overflows — reserving it
221
- // unconditionally is self-fulfilling (a run that fits with just ✕ could be
222
- // forced to split, making arrows appear unnecessarily). In wrap mode
223
- // (available = Infinity) paginate yields one page, so neither pass splits.
224
- //
225
- // [LAW:locality-or-seam] term.cols is the raw usable width the strip wraps to;
226
- // the picker's row is itself a styled strip segment, so the joiner brackets it
227
- // with end-caps (powerline's trailing separator, capsule's two caps) painted
228
- // OUTSIDE that width. A page packed to the full term.cols is pushed past it by
229
- // the caps — the maximally-packed middle pages overflowed and the terminal ate
230
- // the trailing → (page 0 fit, page N did not). Reserve the chrome HERE, at the
231
- // pagination seam, rather than shrinking the shared term.cols every template
232
- // reads. stripChromeCols owns the per-style geometry; Infinity − chrome stays
233
- // Infinity, so wrap mode is unaffected.
234
- // The pad spaces the segment layout synthesizes around the picker's line
235
- // (2 × the render's intra-cell padding) consume the same width budget the
236
- // joiner chrome does — reserve both here, at the pagination seam.
237
- const available = paged
238
- ? Math.max(
239
- 1,
240
- toNumber(store.read(TERM_COLS_VAR)) -
241
- stripChromeCols(runtime.stripStyle) -
242
- 2 * runtime.padding,
243
- )
244
- : Infinity;
245
- const closeReserve = cellWidth(DISCLOSURE_GLYPH_CLOSE) + 1;
246
- const arrowReserve = cellWidth(PICKER_PREV) + 1 + cellWidth(PICKER_NEXT) + 1;
247
- const firstPass = paginate(widths, available, closeReserve);
248
- const pages =
249
- firstPass.length > 1
250
- ? paginate(widths, available, closeReserve + arrowReserve)
251
- : firstPass;
252
-
253
- // [LAW:no-defensive-null-guards] The page value genuinely may be absent/empty
254
- // (the key was never written) — parse it at this trust boundary; an out-of-range
255
- // or unset value clamps into the existing page set, so the picker never indexes
256
- // a non-existent page. The segment's `when` gates visibility on page >= 0.
257
- const rawPage = parseInt(readVar(store, page.stateVar), 10);
258
- const pageIdx = Number.isInteger(rawPage)
259
- ? Math.max(0, Math.min(rawPage, pages.length - 1))
260
- : 0;
261
- const pageCells = pages[pageIdx] ?? [];
262
-
263
- const pageUrl = (value: number): string =>
264
- effectsUrl([
265
- { verb: VERB_SET_STATE, args: [sessionId, page.key, String(value)] },
266
- ]);
267
- // [LAW:dataflow-not-control-flow] The close writes arrive as data (see
268
- // CloseWrites); ✕ performs exactly them, and a closeOnPick option click folds
269
- // the same pairs into its apply write — one atomic set-state either way, so
270
- // "what closing means" cannot diverge between the two affordances.
271
- const closeFlat = close.flatMap(([k, v]) => [k, v]);
272
- const closeUrl = effectsUrl([
273
- { verb: VERB_SET_STATE, args: [sessionId, ...closeFlat] },
274
- ]);
275
- // [LAW:one-source-of-truth] A set-option apply folds its closeOnPick pairs
276
- // into ONE set-state batch (setState is variadic — see daemon/verbs).
277
- // persist-option and layout-op-option cannot: setConfig/apply-layout-op
278
- // each take exactly one (key, value) pair, so their close pairs (always
279
- // SessionState — open/page live there regardless of the apply's
280
- // durability) ride as a SECOND effect in the same dispatch, still one
281
- // atomic click via effectsUrl's array. layout-op-option's "value" is the
282
- // ENCODED op (segment=option, anchor/relation from the compiled action),
283
- // not the option verbatim — the one place this kind's write differs from
284
- // persist-option's.
285
- const closeEffect = closeOnPick
286
- ? [{ verb: VERB_SET_STATE, args: [sessionId, ...closeFlat] }]
287
- : [];
288
- const optionUrl = (option: string): string => {
289
- const { effects } = realize(declared, option, option, store, sessionId);
290
- // [LAW:one-source-of-truth] A plain session pick folds its close pairs into
291
- // the SAME set-state (setState is variadic), so closing and applying are
292
- // one write. Every other shape — a durable write, a structural op, a dual
293
- // carrying its session clear — takes more than one effect already, so its
294
- // close rides as its own effect in the same atomic dispatch.
295
- const solo = effects.length === 1 ? effects[0]! : undefined;
296
- return solo?.verb === VERB_SET_STATE
297
- ? effectsUrl([
298
- {
299
- verb: VERB_SET_STATE,
300
- args: [...solo.args, ...(closeOnPick ? closeFlat : [])],
301
- },
302
- ])
303
- : effectsUrl([...effects, ...closeEffect]);
304
- };
305
-
306
- const frags: RichText[] = [
307
- linkFragment(DISCLOSURE_GLYPH_CLOSE, closeUrl, false),
308
- ];
309
- if (pageIdx > 0) {
310
- frags.push(linkFragment(PICKER_PREV, pageUrl(pageIdx - 1), false));
311
- }
312
- for (const i of pageCells) {
313
- const option = apply.options[i]!;
314
- frags.push(linkFragment(option, optionUrl(option), option === current));
315
- }
316
- if (pageIdx < pages.length - 1) {
317
- frags.push(linkFragment(PICKER_NEXT, pageUrl(pageIdx + 1), false));
318
- }
319
- return assemble(frags, paged);
320
- }
321
-
322
- // [LAW:dataflow-not-control-flow] One func; the two action NAMES select which
323
- // declared effects fire, the two bools are the bounded author choices
324
- // (closeOnPick, paged). Returns T (RichText), the single fragment go-template-js
325
- // emits for `{{ picker … }}`.
326
- //
327
- // [LAW:no-mode-explosion] Both bools are OPTIONAL trailing values with a
328
- // documented default of `false`: `closeOnPick=false` is stay-open (a pick
329
- // recolors live and LEAVES THE MENU OPEN so themes can be tried in a row — the
330
- // baseline UX; the ✕ affordance closes), `closeOnPick=true` is the opt-in where a
331
- // pick ALSO writes the page key closed; `paged=false` is one wrapping page,
332
- // `paged=true` slices into ←/→ pages at the live width. `enforceArgTypes`
333
- // validates only the values actually passed (it loops over arity), so an omitted
334
- // trailing bool arrives `undefined` and resolves to the default here — no arity
335
- // error, and order is preserved so existing callers (which pass both) are
336
- // untouched. Authoring stay-open + paged is `{{ picker "a" "p" false true }}`.
337
- //
338
- // [LAW:one-way-deps] The caller injects this FuncMap into createCcCandybarEngine
339
- // (capabilities-over-context) so the generic engine never imports the picker.
340
- export function pickerFuncs(runtime: ActionRuntime): FuncMap {
341
- return {
342
- picker: {
343
- fn: (
344
- applyName: string,
345
- pageName: string,
346
- closeOnPick?: boolean,
347
- paged?: boolean,
348
- ) => {
349
- // [LAW:one-source-of-truth] The standalone picker's page cursor comes
350
- // from its NAMED set-int action (the documented desugaring surface);
351
- // closing means paging to -1, the when-gate idiom its host row reads
352
- // (`{{ ge (int .page) 0 }}`).
353
- const page = requireKind(
354
- runtime,
355
- pageName,
356
- "set-int",
357
- "an int action ({ set, int: true })",
358
- );
359
- return renderPicker(
360
- applyName,
361
- { key: page.key, stateVar: page.stateVar },
362
- [[page.key, "-1"]],
363
- closeOnPick === true,
364
- paged === true,
365
- runtime,
366
- );
367
- },
368
- argTypes: ["string", "string", "bool", "bool"],
369
- returnType: "T",
370
- },
371
- };
372
- }
@@ -1,74 +0,0 @@
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
- }
@@ -1,51 +0,0 @@
1
- import type { RichText } from "@promptctl/rich-js";
2
-
3
- // [LAW:single-enforcer] THE one place a horizontal cell stream is partitioned
4
- // into vertical lines. Vertical composition lives on the CELL stream — resolved
5
- // here, before any strip measures a cell — never inside a strip. A "\n" that
6
- // reaches a horizontal strip is a zero-width lie: the strip measures a cell by
7
- // its rendered cell-width, for which an embedded "\n" corrupts wrap math,
8
- // powerline caps, and background fill. Splitting first means each emitted line
9
- // is a clean newline-free cell run the strip can measure honestly.
10
- //
11
- // [LAW:one-source-of-truth] The line boundary is the literal "\n" carried in a
12
- // cell's text — the SOLE vertical sentinel. Horizontal cell identity is already
13
- // carried by segment boundaries and link spans, so only the vertical axis lacks
14
- // a carrier and only it needs a sentinel.
15
-
16
- /**
17
- * Partition a cell stream into per-line cell groups on the "\n" sentinel.
18
- *
19
- * A newline-free cell passes through BY REFERENCE — the common case is
20
- * byte-identical to handing the original stream straight to a strip. A cell
21
- * carrying "\n" is split via `RichText.split` (span- and OSC-8-preserving, and
22
- * it consumes the separator): the piece before the first "\n" closes the
23
- * current line, each interior piece is a whole line of its own, and the final
24
- * piece begins the next line. The "\n" is consumed as the partition point,
25
- * never emitted into a line and never measured.
26
- *
27
- * An empty input yields `[[]]` — one empty line group, not zero — so a visible
28
- * row whose segments all rendered nothing still produces exactly one (empty)
29
- * line, matching the pre-substrate behavior.
30
- */
31
- export function splitCellsIntoLines(cells: readonly RichText[]): RichText[][] {
32
- const lines: RichText[][] = [];
33
- let current: RichText[] = [];
34
- for (const cell of cells) {
35
- // [LAW:dataflow-not-control-flow] The discriminator is whether the cell
36
- // carries the sentinel — a value, read once. The newline-free path keeps
37
- // the original reference so the dominant case allocates nothing new.
38
- if (!cell.contains("\n")) {
39
- current.push(cell);
40
- continue;
41
- }
42
- const pieces = cell.split("\n");
43
- current.push(pieces[0]!);
44
- for (let i = 1; i < pieces.length; i++) {
45
- lines.push(current);
46
- current = [pieces[i]!];
47
- }
48
- }
49
- lines.push(current);
50
- return lines;
51
- }