@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.
- package/dist/index.mjs +72 -71
- package/package.json +5 -6
- package/src/check.ts +0 -478
- package/src/cli-flags.ts +0 -8
- package/src/click/wire.ts +0 -158
- package/src/config/action.ts +0 -329
- package/src/config/cli.ts +0 -71
- package/src/config/default-dsl-config.ts +0 -1645
- package/src/config/disclosure.ts +0 -170
- package/src/config/dsl-loader.ts +0 -339
- package/src/config/dsl-types.ts +0 -581
- package/src/config/edit-chrome.ts +0 -559
- package/src/config/help.ts +0 -151
- package/src/config/ident.ts +0 -22
- package/src/config/layout-ops.ts +0 -177
- package/src/config/loader/actions.ts +0 -972
- package/src/config/loader/cache.ts +0 -206
- package/src/config/loader/cross-ref.ts +0 -714
- package/src/config/loader/cycles.ts +0 -148
- package/src/config/loader/diagnostics.ts +0 -99
- package/src/config/loader/discovery.ts +0 -182
- package/src/config/loader/edit-mode.ts +0 -137
- package/src/config/loader/emit-schema.ts +0 -68
- package/src/config/loader/globals.ts +0 -269
- package/src/config/loader/helpers.ts +0 -48
- package/src/config/loader/layout.ts +0 -693
- package/src/config/loader/looks.ts +0 -96
- package/src/config/loader/menu-synth.ts +0 -435
- package/src/config/loader/merge.ts +0 -115
- package/src/config/loader/persist-target.ts +0 -67
- package/src/config/loader/presets.ts +0 -119
- package/src/config/loader/refs.ts +0 -100
- package/src/config/loader/reserved-namespace.ts +0 -38
- package/src/config/loader/segments.ts +0 -120
- package/src/config/loader/validate-core.ts +0 -737
- package/src/config/loader/variables.ts +0 -260
- package/src/config/menu-keys.ts +0 -139
- package/src/config/option-domain.ts +0 -164
- package/src/config/presets.ts +0 -326
- package/src/config/settings-menu.ts +0 -775
- package/src/daemon/acquire.ts +0 -684
- package/src/daemon/cache/git.ts +0 -649
- package/src/daemon/cache/render.ts +0 -623
- package/src/daemon/cache/session-usage-store.ts +0 -720
- package/src/daemon/cache/watchers.ts +0 -249
- package/src/daemon/client-debug.ts +0 -120
- package/src/daemon/client-stats.ts +0 -130
- package/src/daemon/client-transport.ts +0 -273
- package/src/daemon/client.ts +0 -78
- package/src/daemon/config-overrides-store.ts +0 -663
- package/src/daemon/debug-types.ts +0 -91
- package/src/daemon/debug.ts +0 -264
- package/src/daemon/fork-bomb-breaker.ts +0 -351
- package/src/daemon/limits.ts +0 -211
- package/src/daemon/log.ts +0 -81
- package/src/daemon/parent-watchdog.ts +0 -87
- package/src/daemon/paths.ts +0 -211
- package/src/daemon/process-fingerprint.ts +0 -146
- package/src/daemon/protocol.ts +0 -292
- package/src/daemon/render-payload.ts +0 -1256
- package/src/daemon/server.ts +0 -1330
- package/src/daemon/session-state-file.ts +0 -108
- package/src/daemon/session-state.ts +0 -237
- package/src/daemon/socket-lease.ts +0 -209
- package/src/daemon/socket-ownership.ts +0 -209
- package/src/daemon/stats.ts +0 -235
- package/src/daemon/verbs/config-validators.ts +0 -250
- package/src/daemon/verbs/index.ts +0 -706
- package/src/daemon/verbs/state-validators.ts +0 -249
- package/src/daemon/verbs/validator-registry.ts +0 -457
- package/src/demo/dsl.ts +0 -143
- package/src/demo/mock-data.ts +0 -67
- package/src/demo/statusline.json5 +0 -94
- package/src/dsl/node-registry.ts +0 -374
- package/src/dsl/render.ts +0 -803
- package/src/help-text.ts +0 -90
- package/src/index.ts +0 -210
- package/src/install/currency.ts +0 -197
- package/src/install/index.ts +0 -557
- package/src/proc/launch.ts +0 -459
- package/src/proc/stats-handle.ts +0 -13
- package/src/render/action.ts +0 -883
- package/src/render/active-segment.ts +0 -78
- package/src/render/diagnostic-style.ts +0 -23
- package/src/render/diagnostic-text.ts +0 -77
- package/src/render/error-glyph.ts +0 -53
- package/src/render/menu.ts +0 -257
- package/src/render/outcome-plan.ts +0 -45
- package/src/render/picker.ts +0 -372
- package/src/render/segment-color.ts +0 -74
- package/src/render/split-lines.ts +0 -51
- package/src/render/strip.ts +0 -228
- package/src/segments/cache.ts +0 -131
- package/src/segments/context.ts +0 -190
- package/src/segments/git.ts +0 -1084
- package/src/segments/metrics.ts +0 -187
- package/src/segments/pricing.ts +0 -452
- package/src/segments/session.ts +0 -23
- package/src/segments/tmux.ts +0 -74
- package/src/template-engine/cells.ts +0 -90
- package/src/template-engine/colors.ts +0 -124
- package/src/template-engine/engine.ts +0 -108
- package/src/template-engine/funcs.ts +0 -232
- package/src/template-engine/index.ts +0 -11
- package/src/template-engine/layout.ts +0 -133
- package/src/template-engine/scope.ts +0 -62
- package/src/template-engine/sparkline.ts +0 -79
- package/src/themes/index.ts +0 -20
- package/src/themes/palette-resolvers.ts +0 -84
- package/src/themes/policy.ts +0 -393
- package/src/utils/cache.ts +0 -206
- package/src/utils/claude.ts +0 -683
- package/src/utils/color-support.ts +0 -118
- package/src/utils/formatters.ts +0 -99
- package/src/utils/logger.ts +0 -5
- package/src/utils/outcome.ts +0 -33
- package/src/utils/schema-validator.ts +0 -126
- package/src/utils/single-flight.ts +0 -57
- package/src/utils/terminal-width.ts +0 -51
- package/src/utils/terminal.ts +0 -11
- package/src/utils/transcript-fs.ts +0 -279
- package/src/var-system/index.ts +0 -24
- package/src/var-system/sources.ts +0 -1047
- package/src/var-system/store.ts +0 -223
- package/src/var-system/types.ts +0 -57
- package/src/version.ts +0 -17
package/src/render/picker.ts
DELETED
|
@@ -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
|
-
}
|