@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/action.ts
DELETED
|
@@ -1,883 +0,0 @@
|
|
|
1
|
-
// [LAW:locality-or-seam] The runtime half of the actions seam. A `{{ action
|
|
2
|
-
// "name" display [boundValue] }}` call binds one clickable region (an OSC-8 span)
|
|
3
|
-
// to a statically-declared, named action. This module compiles the action table
|
|
4
|
-
// (pre-parsing copy/open templates once) and realizes a named action against the
|
|
5
|
-
// live state into ONE RichText whose span carries the click URL.
|
|
6
|
-
//
|
|
7
|
-
// [LAW:one-source-of-truth] The action NAME is the seam: the template supplies
|
|
8
|
-
// the REPRESENTATION (the display text), the action declaration supplies the
|
|
9
|
-
// BEHAVIOR (what value is written / copied / opened). The same declaration that
|
|
10
|
-
// realizes this click derives the wire gate (deriveActionValidators), so the
|
|
11
|
-
// rendered click and the gate cannot diverge.
|
|
12
|
-
//
|
|
13
|
-
// [LAW:dataflow-not-control-flow] `{{ action … }}` is ONE template expression, so
|
|
14
|
-
// it emits ONE value — a RichText carrying one OSC-8 span. The realization is a
|
|
15
|
-
// single total fold over the compiled-action union: each arm projects (effect,
|
|
16
|
-
// active) as DATA, never a branch that skips work.
|
|
17
|
-
//
|
|
18
|
-
// [LAW:one-way-deps] This is the action feature's runtime. It lives in render/
|
|
19
|
-
// (which depends on template-engine/), reads template-engine/scope, and is
|
|
20
|
-
// injected into the engine by the caller (registerDslConfig hands the action
|
|
21
|
-
// FuncMap in as data). The generic engine never imports this module.
|
|
22
|
-
|
|
23
|
-
import { RichText, Style } from "@promptctl/rich-js";
|
|
24
|
-
import type { FuncMap, Template } from "@promptctl/go-template-js";
|
|
25
|
-
import type { VariableStore } from "../var-system/store.js";
|
|
26
|
-
import { toString as varToString } from "../var-system/types.js";
|
|
27
|
-
import { buildScope } from "../template-engine/scope.js";
|
|
28
|
-
import {
|
|
29
|
-
actionDestinations,
|
|
30
|
-
actionIsDual,
|
|
31
|
-
PERSIST_WHEN,
|
|
32
|
-
type ActionDecl,
|
|
33
|
-
} from "../config/action.js";
|
|
34
|
-
import { resolveOptionDomain } from "../config/option-domain.js";
|
|
35
|
-
import { pickCycleDisplay } from "../config/disclosure.js";
|
|
36
|
-
import { encodeLayoutOp, type LayoutOp } from "../config/layout-ops.js";
|
|
37
|
-
import { parseSessionBoolean, type StripStyle } from "../themes/policy.js";
|
|
38
|
-
import {
|
|
39
|
-
effectsUrl,
|
|
40
|
-
VERB_APPLY_LAYOUT_OP,
|
|
41
|
-
VERB_COPY,
|
|
42
|
-
VERB_OPEN_VSCODE,
|
|
43
|
-
VERB_REDO,
|
|
44
|
-
VERB_RESET_CONFIG,
|
|
45
|
-
VERB_SET_CONFIG,
|
|
46
|
-
VERB_SET_STATE,
|
|
47
|
-
VERB_STEP_CONFIG,
|
|
48
|
-
VERB_STEP_STATE,
|
|
49
|
-
VERB_UNDO,
|
|
50
|
-
type Effect,
|
|
51
|
-
} from "../click/wire.js";
|
|
52
|
-
|
|
53
|
-
// ─── Compiled shapes ───────────────────────────────────────────────────────────
|
|
54
|
-
|
|
55
|
-
// [LAW:types-are-the-program] A compiled action mirrors ActionDecl, discriminated
|
|
56
|
-
// by `kind`. Each `set` arm carries the SessionState `key` it writes plus the
|
|
57
|
-
// `stateVar` that reads it back (resolved from the key, so the displayed/active
|
|
58
|
-
// value and the written value are one source — the same resolution a stepper
|
|
59
|
-
// widget uses). A literal carries its fixed `value`; an option binds the value
|
|
60
|
-
// from the template at render; a bounded carries its [min,max]/by navigation.
|
|
61
|
-
// copy/open carry a pre-parsed template evaluated against the live scope.
|
|
62
|
-
export type CompiledActionDecl =
|
|
63
|
-
| {
|
|
64
|
-
readonly kind: "set-literal";
|
|
65
|
-
readonly key: string;
|
|
66
|
-
readonly value: string;
|
|
67
|
-
readonly stateVar: string;
|
|
68
|
-
}
|
|
69
|
-
| {
|
|
70
|
-
readonly kind: "set-option";
|
|
71
|
-
readonly key: string;
|
|
72
|
-
readonly stateVar: string;
|
|
73
|
-
// The resolved option domain. Stored at compile so a picker can iterate it
|
|
74
|
-
// without re-resolving the source list, and so the set-option IS
|
|
75
|
-
// self-describing (it knows its own domain), not just a key.
|
|
76
|
-
readonly options: readonly string[];
|
|
77
|
-
}
|
|
78
|
-
| {
|
|
79
|
-
// [LAW:types-are-the-program] A stepper affordance. It carries ONLY the
|
|
80
|
-
// render-invariant click intent: the state `key` and the signed delta `by`.
|
|
81
|
-
// It deliberately holds NO stateVar/min/max and reads NO current value at
|
|
82
|
-
// render — the absolute target is computed at APPLY time from live state
|
|
83
|
-
// (the daemon's step-state handler), so the emitted link is byte-identical
|
|
84
|
-
// across renders and N rapid clicks each re-read-and-step. [LAW:one-source-
|
|
85
|
-
// of-truth] the bounds live once in the range validator the handler reads.
|
|
86
|
-
readonly kind: "set-bounded";
|
|
87
|
-
readonly key: string;
|
|
88
|
-
readonly by: number;
|
|
89
|
-
}
|
|
90
|
-
| {
|
|
91
|
-
// [LAW:types-are-the-program] An int cursor: it writes whatever integer the
|
|
92
|
-
// render binds (the picker's page nav supplies -1/p±1; a bare `{{ action }}`
|
|
93
|
-
// supplies its display/boundValue). The gate is an unbounded int — the
|
|
94
|
-
// renderer owns clamping to valid pages, exactly as set-bounded owns wrap.
|
|
95
|
-
readonly kind: "set-int";
|
|
96
|
-
readonly key: string;
|
|
97
|
-
readonly stateVar: string;
|
|
98
|
-
}
|
|
99
|
-
| {
|
|
100
|
-
// [LAW:types-are-the-program] An enumerated-domain stepper: the click
|
|
101
|
-
// writes the SUCCESSOR of the current value in `members` (wrapping; a
|
|
102
|
-
// current value outside the domain counts as the first member). Unlike
|
|
103
|
-
// set-bounded — which emits a RELATIVE nudge so rapid clicks accumulate —
|
|
104
|
-
// a cycle emits the ABSOLUTE successor computed at render: the rendered
|
|
105
|
-
// display names the current state, so the click's meaning is "go to the
|
|
106
|
-
// successor of what I showed you". A stale link then lands on the state
|
|
107
|
-
// the user saw promised, not an extra flip past it — for toggles the
|
|
108
|
-
// absolute write IS the correct intent.
|
|
109
|
-
readonly kind: "set-cycle";
|
|
110
|
-
readonly key: string;
|
|
111
|
-
readonly stateVar: string;
|
|
112
|
-
readonly members: readonly string[];
|
|
113
|
-
}
|
|
114
|
-
| { readonly kind: "copy"; readonly text: Template<RichText> }
|
|
115
|
-
| { readonly kind: "open"; readonly target: Template<RichText> }
|
|
116
|
-
// [LAW:one-source-of-truth] `persist`'s twin of the set-* kinds above,
|
|
117
|
-
// MINUS set-int (a page cursor is never persisted — see action.ts). Carries
|
|
118
|
-
// the SAME shapes for the SAME reason: a persistent write is gated and
|
|
119
|
-
// realized exactly like a session write, only the wire verb (VERB_SET_CONFIG/
|
|
120
|
-
// VERB_STEP_CONFIG) and the write's durability differ.
|
|
121
|
-
| {
|
|
122
|
-
readonly kind: "persist-literal";
|
|
123
|
-
readonly key: string;
|
|
124
|
-
readonly value: string;
|
|
125
|
-
readonly stateVar: string;
|
|
126
|
-
}
|
|
127
|
-
| {
|
|
128
|
-
readonly kind: "persist-option";
|
|
129
|
-
readonly key: string;
|
|
130
|
-
readonly stateVar: string;
|
|
131
|
-
readonly options: readonly string[];
|
|
132
|
-
}
|
|
133
|
-
| {
|
|
134
|
-
readonly kind: "persist-bounded";
|
|
135
|
-
readonly key: string;
|
|
136
|
-
readonly by: number;
|
|
137
|
-
}
|
|
138
|
-
| {
|
|
139
|
-
readonly kind: "persist-cycle";
|
|
140
|
-
readonly key: string;
|
|
141
|
-
readonly stateVar: string;
|
|
142
|
-
readonly members: readonly string[];
|
|
143
|
-
}
|
|
144
|
-
// [LAW:one-source-of-truth] The gated undo for a persistent write: clears
|
|
145
|
-
// one config-overrides key. Carries only the key — there is no value to
|
|
146
|
-
// realize, so it shares copy/open's "no gate" shape at compile time (the
|
|
147
|
-
// GATE is the key-membership check the reset-config verb handler applies).
|
|
148
|
-
| { readonly kind: "reset"; readonly key: string }
|
|
149
|
-
// [LAW:one-source-of-truth] brandon-layout-edit-2gc.1's structural-edit
|
|
150
|
-
// arms. Fully literal at compile time (the op IS the declaration — no
|
|
151
|
-
// template-bound option, unlike persist-option), so `op` is precomputed
|
|
152
|
-
// here rather than reconstructed from raw fields at every realize() call.
|
|
153
|
-
| { readonly kind: "layout-op"; readonly key: string; readonly op: LayoutOp }
|
|
154
|
-
// [LAW:one-source-of-truth] brandon-layout-edit-2gc.3's domain-sourced
|
|
155
|
-
// sibling of layout-op: `anchor`/`relation` are fixed at compile time (the
|
|
156
|
-
// POSITION is author-time data) but the segment name comes from the
|
|
157
|
-
// template's bound option — the option-picking shape `persist-option`
|
|
158
|
-
// already has, minus the value being written VERBATIM. `requireOptionKind`
|
|
159
|
-
// (render/picker.ts) admits this kind alongside set-option/persist-option
|
|
160
|
-
// so a `{{ menu }}`/`{{ picker }}` can drive it with zero picker changes;
|
|
161
|
-
// only the WRITE (realize(), below) differs — it encodes the picked option
|
|
162
|
-
// into a LayoutOp instead of persisting it as-is.
|
|
163
|
-
| {
|
|
164
|
-
readonly kind: "layout-op-option";
|
|
165
|
-
readonly key: string;
|
|
166
|
-
readonly anchor: string;
|
|
167
|
-
readonly relation: "before" | "after";
|
|
168
|
-
readonly options: readonly string[];
|
|
169
|
-
}
|
|
170
|
-
// [LAW:one-source-of-truth] brandon-layout-edit-2gc.2's global history
|
|
171
|
-
// step over the overrides layer — `reset`'s fine-grained sibling. No key:
|
|
172
|
-
// there is nothing to carry, since the history stack (not this action) is
|
|
173
|
-
// what decides which entry moves.
|
|
174
|
-
| { readonly kind: "undo" }
|
|
175
|
-
| { readonly kind: "redo" }
|
|
176
|
-
// [LAW:dataflow-not-control-flow] candybar-settings-ui-aok.3's ONE control
|
|
177
|
-
// per setting. Both destinations are compiled here as the ordinary
|
|
178
|
-
// single-destination shapes they are, and `selector` names the session key
|
|
179
|
-
// whose boolean value picks between them at click time. The destination is
|
|
180
|
-
// therefore a VALUE flowing through `activeDestination` — every consumer
|
|
181
|
-
// (realize, the picker, selectDisplay) resolves it once at the top and then
|
|
182
|
-
// runs the code it has always run, so nothing downstream branches on
|
|
183
|
-
// "is this dual".
|
|
184
|
-
| {
|
|
185
|
-
readonly kind: "dual";
|
|
186
|
-
readonly selector: string;
|
|
187
|
-
readonly session: CompiledActionDecl;
|
|
188
|
-
readonly durable: CompiledActionDecl;
|
|
189
|
-
// The SessionState key the session half writes, carried so a durable
|
|
190
|
-
// click can clear it in the same dispatch (see realize's dual arm).
|
|
191
|
-
readonly sessionKey: string;
|
|
192
|
-
};
|
|
193
|
-
|
|
194
|
-
export type CompiledActions = ReadonlyMap<string, CompiledActionDecl>;
|
|
195
|
-
|
|
196
|
-
// [LAW:one-source-of-truth] Globals fields whose CURRENT resolved value is
|
|
197
|
-
// exposed to templates under a different var name than the field itself (the
|
|
198
|
-
// daemon publishes this resolution once per render — e.g. `theme.effective`
|
|
199
|
-
// for `palette`, src/daemon/render-payload.ts). A `persist` action with no
|
|
200
|
-
// entry here reads back through its own key name as an input var (mirrors
|
|
201
|
-
// compileActions' stateKeyToVar fallback), so every persistable globals field
|
|
202
|
-
// needs an entry unless its `.effective` projection happens to be named
|
|
203
|
-
// exactly the bare field (none are — every projection carries the
|
|
204
|
-
// `.effective` suffix). Every field with a projection is listed
|
|
205
|
-
// (candybar-config-engine-71o.3 added style/charset/colorCompatibility/
|
|
206
|
-
// autoWrap/padding to palette/look's original two); a field with no entry
|
|
207
|
-
// here still writes correctly on `persist` — only its "current selection"
|
|
208
|
-
// highlight is inert (readVar falls back to "" since no such var exists).
|
|
209
|
-
const CONFIG_KEY_TO_EFFECTIVE_VAR: ReadonlyMap<string, string> = new Map([
|
|
210
|
-
["palette", "theme.effective"],
|
|
211
|
-
// [LAW:one-source-of-truth] `preset` earns its entry here the moment a DUAL
|
|
212
|
-
// control writes it: compileDual makes BOTH halves read back through this
|
|
213
|
-
// map, so a field missing from it loses its current-selection mark on the
|
|
214
|
-
// session side too — and the preset picker sits on the settings menu's
|
|
215
|
-
// always-visible first row, where "which arrangement am I in" is the whole
|
|
216
|
-
// question the control answers.
|
|
217
|
-
["preset", "preset.effective"],
|
|
218
|
-
["look", "look.effective"],
|
|
219
|
-
["style", "style.effective"],
|
|
220
|
-
["charset", "charset.effective"],
|
|
221
|
-
["colorCompatibility", "colorCompatibility.effective"],
|
|
222
|
-
["autoWrap", "autoWrap.effective"],
|
|
223
|
-
["padding", "padding.effective"],
|
|
224
|
-
]);
|
|
225
|
-
|
|
226
|
-
// [LAW:locality-or-seam] The runtime holder the `action` template function closes
|
|
227
|
-
// over. Populated after the engine is constructed (the func references the
|
|
228
|
-
// engine, the compiled actions reference the engine — the holder breaks the
|
|
229
|
-
// cycle). `store` is the live VariableStore the renderer reads, so the action
|
|
230
|
-
// reads session.id and the current value from the same source the rest of the
|
|
231
|
-
// render does.
|
|
232
|
-
export interface ActionRuntime {
|
|
233
|
-
// [LAW:types-are-the-program] Always present — registerDslConfig sources it
|
|
234
|
-
// from the registry it is handed (registry.variableStore), so "no store" is
|
|
235
|
-
// structurally unrepresentable. The action reads session.id and current
|
|
236
|
-
// values from the same store the renderer reads.
|
|
237
|
-
store: VariableStore;
|
|
238
|
-
compiled: CompiledActions;
|
|
239
|
-
// [LAW:locality-or-seam] The current render's strip style, published per render
|
|
240
|
-
// by renderDsl. The picker reads it to reserve the joiner's end-cap chrome at
|
|
241
|
-
// its pagination seam — the one place that needs strip geometry, kept off the
|
|
242
|
-
// shared `term.cols` budget. Defaulted at registration; renders are sequential
|
|
243
|
-
// and synchronous, so the per-render write never leaks across renders.
|
|
244
|
-
// [LAW:no-ambient-temporal-coupling]
|
|
245
|
-
stripStyle: StripStyle;
|
|
246
|
-
// [LAW:locality-or-seam] The current render's intra-cell padding (resolved
|
|
247
|
-
// globals.padding), published per render by renderDsl exactly like
|
|
248
|
-
// stripStyle. The picker reserves 2×padding at its pagination seam — the
|
|
249
|
-
// segment layout pads every line it emits, so a page packed to the full
|
|
250
|
-
// budget would otherwise be pushed past the width by the pad spaces.
|
|
251
|
-
padding: number;
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
// ─── Compilation ───────────────────────────────────────────────────────────────
|
|
255
|
-
|
|
256
|
-
// Pre-parse the copy/open templates for every action once, at config
|
|
257
|
-
// registration; set actions stay literal. [LAW:one-source-of-truth] parse-once,
|
|
258
|
-
// evaluate-many — renderAction only evaluates. `stateKeyToVar` maps a
|
|
259
|
-
// SessionState key → the variable that reads it (same map widgets use), so a
|
|
260
|
-
// set action reads its current/active value from the SAME value the templates
|
|
261
|
-
// read, regardless of whether the config named the variable after the key.
|
|
262
|
-
// [LAW:single-enforcer] `parse` is the config's ONE helper-aware parse closure
|
|
263
|
-
// (registerDslConfig owns it), not a bare engine — action copy/open templates
|
|
264
|
-
// resolve the same shared `{{ template "name" }}` helpers every segment does,
|
|
265
|
-
// through one boundary. compileActions needs only the ability to parse a source.
|
|
266
|
-
export function compileActions(
|
|
267
|
-
parse: (src: string) => Template<RichText>,
|
|
268
|
-
actions: Readonly<Record<string, ActionDecl>>,
|
|
269
|
-
stateKeyToVar: ReadonlyMap<string, string>,
|
|
270
|
-
// This config's per-config option domains (currently just "looks" — the
|
|
271
|
-
// config's merged look names) — resolveOptionDomain checks these before
|
|
272
|
-
// falling back to the global registry (themes/styles).
|
|
273
|
-
perConfigDomains: ReadonlyMap<string, readonly string[]>,
|
|
274
|
-
): CompiledActions {
|
|
275
|
-
const out = new Map<string, CompiledActionDecl>();
|
|
276
|
-
for (const [name, action] of Object.entries(actions)) {
|
|
277
|
-
out.set(
|
|
278
|
-
name,
|
|
279
|
-
compileAction(parse, name, action, stateKeyToVar, perConfigDomains),
|
|
280
|
-
);
|
|
281
|
-
}
|
|
282
|
-
return out;
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
// [LAW:dataflow-not-control-flow] One total fold maps each ActionDecl to its
|
|
286
|
-
// compiled shape — the discriminator is which key is present (set's value SOURCE
|
|
287
|
-
// for the three set arms; copy/open otherwise). Every arm reads only its own
|
|
288
|
-
// fields; a new arm is one new branch.
|
|
289
|
-
function compileAction(
|
|
290
|
-
parse: (src: string) => Template<RichText>,
|
|
291
|
-
name: string,
|
|
292
|
-
action: ActionDecl,
|
|
293
|
-
stateKeyToVar: ReadonlyMap<string, string>,
|
|
294
|
-
perConfigDomains: ReadonlyMap<string, readonly string[]>,
|
|
295
|
-
): CompiledActionDecl {
|
|
296
|
-
// [LAW:one-source-of-truth] A dual compiles as its own two destinations —
|
|
297
|
-
// the SAME explosion the validator derivations fold over
|
|
298
|
-
// (actionDestinations), so the click a dual realizes and the gate it derives
|
|
299
|
-
// come from one statement of what the two halves are. It is matched BEFORE
|
|
300
|
-
// the `set` arm because a dual carries `set` too.
|
|
301
|
-
if (actionIsDual(action)) {
|
|
302
|
-
const [session, durable] = actionDestinations(action);
|
|
303
|
-
return compileDual(
|
|
304
|
-
stateKeyToVar.get(action[PERSIST_WHEN]) ?? action[PERSIST_WHEN],
|
|
305
|
-
action.set,
|
|
306
|
-
compileAction(parse, name, session!, stateKeyToVar, perConfigDomains),
|
|
307
|
-
compileAction(parse, name, durable!, stateKeyToVar, perConfigDomains),
|
|
308
|
-
);
|
|
309
|
-
}
|
|
310
|
-
if ("set" in action) {
|
|
311
|
-
const stateVar = stateKeyToVar.get(action.set) ?? action.set;
|
|
312
|
-
if ("to" in action) {
|
|
313
|
-
return {
|
|
314
|
-
kind: "set-literal",
|
|
315
|
-
key: action.set,
|
|
316
|
-
value: action.to,
|
|
317
|
-
stateVar,
|
|
318
|
-
};
|
|
319
|
-
}
|
|
320
|
-
if ("from" in action) {
|
|
321
|
-
return {
|
|
322
|
-
kind: "set-option",
|
|
323
|
-
key: action.set,
|
|
324
|
-
stateVar,
|
|
325
|
-
options: [...resolveOptionDomain(action.from, perConfigDomains)],
|
|
326
|
-
};
|
|
327
|
-
}
|
|
328
|
-
if ("int" in action) {
|
|
329
|
-
return { kind: "set-int", key: action.set, stateVar };
|
|
330
|
-
}
|
|
331
|
-
if ("cycle" in action) {
|
|
332
|
-
return {
|
|
333
|
-
kind: "set-cycle",
|
|
334
|
-
key: action.set,
|
|
335
|
-
stateVar,
|
|
336
|
-
members: action.cycle,
|
|
337
|
-
};
|
|
338
|
-
}
|
|
339
|
-
return {
|
|
340
|
-
kind: "set-bounded",
|
|
341
|
-
key: action.set,
|
|
342
|
-
by: action.by,
|
|
343
|
-
};
|
|
344
|
-
}
|
|
345
|
-
if ("persist" in action) {
|
|
346
|
-
const stateVar =
|
|
347
|
-
CONFIG_KEY_TO_EFFECTIVE_VAR.get(action.persist) ?? action.persist;
|
|
348
|
-
if ("to" in action) {
|
|
349
|
-
return {
|
|
350
|
-
kind: "persist-literal",
|
|
351
|
-
key: action.persist,
|
|
352
|
-
value: action.to,
|
|
353
|
-
stateVar,
|
|
354
|
-
};
|
|
355
|
-
}
|
|
356
|
-
if ("from" in action) {
|
|
357
|
-
return {
|
|
358
|
-
kind: "persist-option",
|
|
359
|
-
key: action.persist,
|
|
360
|
-
stateVar,
|
|
361
|
-
options: [...resolveOptionDomain(action.from, perConfigDomains)],
|
|
362
|
-
};
|
|
363
|
-
}
|
|
364
|
-
if ("cycle" in action) {
|
|
365
|
-
return {
|
|
366
|
-
kind: "persist-cycle",
|
|
367
|
-
key: action.persist,
|
|
368
|
-
stateVar,
|
|
369
|
-
members: action.cycle,
|
|
370
|
-
};
|
|
371
|
-
}
|
|
372
|
-
if ("removeSegment" in action) {
|
|
373
|
-
return {
|
|
374
|
-
kind: "layout-op",
|
|
375
|
-
key: action.persist,
|
|
376
|
-
op: { op: "remove", target: action.removeSegment },
|
|
377
|
-
};
|
|
378
|
-
}
|
|
379
|
-
if ("insertSegment" in action) {
|
|
380
|
-
return {
|
|
381
|
-
kind: "layout-op",
|
|
382
|
-
key: action.persist,
|
|
383
|
-
op: {
|
|
384
|
-
op: "insert",
|
|
385
|
-
segment: action.insertSegment,
|
|
386
|
-
anchor: action.anchor,
|
|
387
|
-
relation: action.relation,
|
|
388
|
-
},
|
|
389
|
-
};
|
|
390
|
-
}
|
|
391
|
-
if ("insertSegmentFrom" in action) {
|
|
392
|
-
return {
|
|
393
|
-
kind: "layout-op-option",
|
|
394
|
-
key: action.persist,
|
|
395
|
-
anchor: action.anchor,
|
|
396
|
-
relation: action.relation,
|
|
397
|
-
options: [
|
|
398
|
-
...resolveOptionDomain(action.insertSegmentFrom, perConfigDomains),
|
|
399
|
-
],
|
|
400
|
-
};
|
|
401
|
-
}
|
|
402
|
-
return {
|
|
403
|
-
kind: "persist-bounded",
|
|
404
|
-
key: action.persist,
|
|
405
|
-
by: action.by,
|
|
406
|
-
};
|
|
407
|
-
}
|
|
408
|
-
if ("copy" in action) {
|
|
409
|
-
return {
|
|
410
|
-
kind: "copy",
|
|
411
|
-
text: parseActionTemplate(parse, action.copy, name),
|
|
412
|
-
};
|
|
413
|
-
}
|
|
414
|
-
if ("open" in action) {
|
|
415
|
-
return {
|
|
416
|
-
kind: "open",
|
|
417
|
-
target: parseActionTemplate(parse, action.open, name),
|
|
418
|
-
};
|
|
419
|
-
}
|
|
420
|
-
if ("reset" in action) {
|
|
421
|
-
return { kind: "reset", key: action.reset };
|
|
422
|
-
}
|
|
423
|
-
return "undo" in action ? { kind: "undo" } : { kind: "redo" };
|
|
424
|
-
}
|
|
425
|
-
|
|
426
|
-
// [LAW:one-source-of-truth] A dual control shows ONE current value and writes
|
|
427
|
-
// relative to the value it showed — so both destinations read back through the
|
|
428
|
-
// DURABLE half's variable, which is the `.effective` projection the daemon
|
|
429
|
-
// resolved for this render (CONFIG_KEY_TO_EFFECTIVE_VAR above): the value the
|
|
430
|
-
// bar is actually rendering with, whatever chain produced it. Reading the
|
|
431
|
-
// session key instead would let a cycle's glyph name the effective state while
|
|
432
|
-
// its click stepped from an unwritten session key — the toggle would render
|
|
433
|
-
// "wrap: off" and write "false", a click that visibly does nothing. Arms that
|
|
434
|
-
// carry no `stateVar` (the bounded steppers) read nothing at render by design:
|
|
435
|
-
// their step is relative and resolved daemon-side.
|
|
436
|
-
function compileDual(
|
|
437
|
-
selectorVar: string,
|
|
438
|
-
sessionKey: string,
|
|
439
|
-
session: CompiledActionDecl,
|
|
440
|
-
durable: CompiledActionDecl,
|
|
441
|
-
): CompiledActionDecl {
|
|
442
|
-
const readBack =
|
|
443
|
-
"stateVar" in session && "stateVar" in durable
|
|
444
|
-
? { ...session, stateVar: durable.stateVar }
|
|
445
|
-
: session;
|
|
446
|
-
return {
|
|
447
|
-
kind: "dual",
|
|
448
|
-
selector: selectorVar,
|
|
449
|
-
session: readBack,
|
|
450
|
-
durable,
|
|
451
|
-
sessionKey,
|
|
452
|
-
};
|
|
453
|
-
}
|
|
454
|
-
|
|
455
|
-
// [LAW:dataflow-not-control-flow] THE destination fold: which store a dual
|
|
456
|
-
// action writes is the boolean value of its selector key, read from the same
|
|
457
|
-
// live store the rest of the render reads. Total over every compiled action —
|
|
458
|
-
// a single-destination action IS its own destination — so callers resolve
|
|
459
|
-
// through it unconditionally and never test for the dual kind.
|
|
460
|
-
//
|
|
461
|
-
// [LAW:one-source-of-truth] `parseSessionBoolean` is the one spelling of a
|
|
462
|
-
// boolean in SessionState (themes/policy.ts), the same parse `autoWrap`'s own
|
|
463
|
-
// session half goes through: an unwritten, malformed, or "false" selector all
|
|
464
|
-
// mean the session destination, and only a canonical "true" means durable.
|
|
465
|
-
export function activeDestination(
|
|
466
|
-
c: CompiledActionDecl,
|
|
467
|
-
store: VariableStore,
|
|
468
|
-
): CompiledActionDecl {
|
|
469
|
-
if (c.kind !== "dual") return c;
|
|
470
|
-
return parseSessionBoolean(readVar(store, c.selector)) === true
|
|
471
|
-
? c.durable
|
|
472
|
-
: c.session;
|
|
473
|
-
}
|
|
474
|
-
|
|
475
|
-
function parseActionTemplate(
|
|
476
|
-
parse: (src: string) => Template<RichText>,
|
|
477
|
-
src: string,
|
|
478
|
-
name: string,
|
|
479
|
-
): Template<RichText> {
|
|
480
|
-
try {
|
|
481
|
-
return parse(src);
|
|
482
|
-
} catch (e) {
|
|
483
|
-
throw new Error(
|
|
484
|
-
`Template parse error in actions.${name}: ${(e as Error).message}`,
|
|
485
|
-
{ cause: e },
|
|
486
|
-
);
|
|
487
|
-
}
|
|
488
|
-
}
|
|
489
|
-
|
|
490
|
-
// ─── Rendering ───────────────────────────────────────────────────────────────
|
|
491
|
-
|
|
492
|
-
// [LAW:one-source-of-truth] Exported so the picker reads SessionState through the
|
|
493
|
-
// SAME boundary (has() discriminates "never written" → "").
|
|
494
|
-
export function readVar(store: VariableStore, name: string): string {
|
|
495
|
-
// [LAW:no-defensive-null-guards] "current value may not exist" is a legitimate
|
|
496
|
-
// state (the key was never written) — guard the store lookup, not a downstream
|
|
497
|
-
// operation. has() is the discriminator; absence yields "".
|
|
498
|
-
return store.has(name) ? varToString(store.read(name)) : "";
|
|
499
|
-
}
|
|
500
|
-
|
|
501
|
-
function evalTemplate(tpl: Template<RichText>, scope: object): string {
|
|
502
|
-
return tpl
|
|
503
|
-
.evaluate(scope)
|
|
504
|
-
.map((f) => f.plain)
|
|
505
|
-
.join("");
|
|
506
|
-
}
|
|
507
|
-
|
|
508
|
-
// [LAW:single-enforcer] One link-span constructor for both action and picker
|
|
509
|
-
// cells — a Style carrying the OSC-8 url, `active` riding as bold.
|
|
510
|
-
export function linkFragment(
|
|
511
|
-
text: string,
|
|
512
|
-
url: string,
|
|
513
|
-
active: boolean,
|
|
514
|
-
): RichText {
|
|
515
|
-
// [LAW:one-source-of-truth] Build the link span exactly as rich-js's `link`
|
|
516
|
-
// does: a Style carrying the OSC-8 url. `active` rides as bold so the
|
|
517
|
-
// currently-selected value reads as current — a value on the span, not a
|
|
518
|
-
// branch in the walk.
|
|
519
|
-
const rt = new RichText(text, {
|
|
520
|
-
style: new Style({ link: url, bold: active }),
|
|
521
|
-
});
|
|
522
|
-
rt.noWrap = true;
|
|
523
|
-
rt.end = "";
|
|
524
|
-
return rt;
|
|
525
|
-
}
|
|
526
|
-
|
|
527
|
-
// [LAW:one-source-of-truth] THE "unknown current counts as the first member"
|
|
528
|
-
// rule — the one resolution both the display selection and the successor write
|
|
529
|
-
// fold over. Members are ordered default-state-first, so an unset/foreign value
|
|
530
|
-
// renders the first display and clicks to the second member (an accordion
|
|
531
|
-
// sibling's path "counts as closed", a never-written toggle "counts as off").
|
|
532
|
-
function cycleIndex(
|
|
533
|
-
c: Extract<CompiledActionDecl, { kind: "set-cycle" | "persist-cycle" }>,
|
|
534
|
-
store: VariableStore,
|
|
535
|
-
): number {
|
|
536
|
-
return Math.max(c.members.indexOf(readVar(store, c.stateVar)), 0);
|
|
537
|
-
}
|
|
538
|
-
|
|
539
|
-
// [LAW:dataflow-not-control-flow] The single total projection of a compiled action
|
|
540
|
-
// onto (effect, active) — the click's wire effect plus whether this region is the
|
|
541
|
-
// current selection. The template supplies `display` (the clickable text) and an
|
|
542
|
-
// optional `boundValue` (an option picker binds each option's value); the action
|
|
543
|
-
// declaration supplies everything else. Consumers never re-switch on the action
|
|
544
|
-
// kind: this fold is the one place the union is matched.
|
|
545
|
-
// • set-literal: writes its fixed value; active when the key already holds it.
|
|
546
|
-
// • set-option: writes boundValue ?? display (the bound option); active when
|
|
547
|
-
// the key already holds it (the picker's current-mark).
|
|
548
|
-
// • set-bounded: emits a RELATIVE step-state nudge (key + signed by); never
|
|
549
|
-
// reads current and never "active". The wrap + bounds + the
|
|
550
|
-
// unset seed are applied at APPLY time by the daemon handler
|
|
551
|
-
// reading live state, not snapshotted into the link here.
|
|
552
|
-
// • copy/open: one copy/open effect of the evaluated template; never active.
|
|
553
|
-
// [LAW:dataflow-not-control-flow] The template scope is an input only the copy/
|
|
554
|
-
// open arms consume, so it is built WHERE consumed (buildScope snapshots
|
|
555
|
-
// store.names() into a Set per call — paying it for a set-* region, e.g. every
|
|
556
|
-
// cell of an option picker, is pure waste). set-* arms read individual vars
|
|
557
|
-
// directly. This is data locality, not a control-flow guard: the scope simply
|
|
558
|
-
// flows into the arms that need it.
|
|
559
|
-
export function realize(
|
|
560
|
-
c: CompiledActionDecl,
|
|
561
|
-
display: string,
|
|
562
|
-
boundValue: string | undefined,
|
|
563
|
-
store: VariableStore,
|
|
564
|
-
sessionId: string,
|
|
565
|
-
): { effects: readonly Effect[]; active: boolean } {
|
|
566
|
-
switch (c.kind) {
|
|
567
|
-
case "set-literal": {
|
|
568
|
-
const current = readVar(store, c.stateVar);
|
|
569
|
-
return {
|
|
570
|
-
effects: [{ verb: VERB_SET_STATE, args: [sessionId, c.key, c.value] }],
|
|
571
|
-
active: current === c.value,
|
|
572
|
-
};
|
|
573
|
-
}
|
|
574
|
-
case "set-option": {
|
|
575
|
-
const value = boundValue ?? display;
|
|
576
|
-
const current = readVar(store, c.stateVar);
|
|
577
|
-
return {
|
|
578
|
-
effects: [{ verb: VERB_SET_STATE, args: [sessionId, c.key, value] }],
|
|
579
|
-
active: current === value,
|
|
580
|
-
};
|
|
581
|
-
}
|
|
582
|
-
case "set-int": {
|
|
583
|
-
// The render binds the integer to write (a picker's page nav passes the
|
|
584
|
-
// target page as boundValue; a bare `{{ action }}` passes its display).
|
|
585
|
-
// [LAW:no-silent-failure] A bare `{{ action }}` on a set-int MUST render a
|
|
586
|
-
// NUMERIC display (the manual "open at page 0" pattern: `{{ action "openMenu"
|
|
587
|
-
// "0" }}`) — the display IS the value written, and the int gate
|
|
588
|
-
// (makeIntValidator) rejects a non-integer at click with a loud "must be an
|
|
589
|
-
// integer" BAD_REQUEST. There is no load-time check because the display is a
|
|
590
|
-
// template evaluated at render (it may be dynamic), so the shape is enforced
|
|
591
|
-
// at the wire, not silently coerced. active when the key already holds it.
|
|
592
|
-
const value = boundValue ?? display;
|
|
593
|
-
const current = readVar(store, c.stateVar);
|
|
594
|
-
return {
|
|
595
|
-
effects: [{ verb: VERB_SET_STATE, args: [sessionId, c.key, value] }],
|
|
596
|
-
active: current === value,
|
|
597
|
-
};
|
|
598
|
-
}
|
|
599
|
-
case "set-cycle": {
|
|
600
|
-
// [LAW:one-source-of-truth] The same current-index resolution that picked
|
|
601
|
-
// the rendered display picks the write target — display and write derive
|
|
602
|
-
// from one read, so the click delivers exactly the transition the glyph
|
|
603
|
-
// promised.
|
|
604
|
-
const next = c.members[(cycleIndex(c, store) + 1) % c.members.length]!;
|
|
605
|
-
return {
|
|
606
|
-
effects: [{ verb: VERB_SET_STATE, args: [sessionId, c.key, next] }],
|
|
607
|
-
active: false,
|
|
608
|
-
};
|
|
609
|
-
}
|
|
610
|
-
case "set-bounded": {
|
|
611
|
-
// [LAW:one-source-of-truth] Emit a RELATIVE nudge — the irreducible intent
|
|
612
|
-
// (key + signed delta), never an absolute target derived from a render-time
|
|
613
|
-
// snapshot of `current`. The daemon's step-state handler reads live state,
|
|
614
|
-
// applies the wrap against the registry's bounds, and writes through the
|
|
615
|
-
// single range gate. So the link is byte-identical across renders and N
|
|
616
|
-
// rapid clicks each accumulate (the idempotent absolute-write bug is gone).
|
|
617
|
-
return {
|
|
618
|
-
effects: [
|
|
619
|
-
{
|
|
620
|
-
verb: VERB_STEP_STATE,
|
|
621
|
-
args: [sessionId, c.key, String(c.by)],
|
|
622
|
-
},
|
|
623
|
-
],
|
|
624
|
-
active: false,
|
|
625
|
-
};
|
|
626
|
-
}
|
|
627
|
-
case "copy":
|
|
628
|
-
return {
|
|
629
|
-
effects: [
|
|
630
|
-
{
|
|
631
|
-
verb: VERB_COPY,
|
|
632
|
-
args: [evalTemplate(c.text, buildScope(store))],
|
|
633
|
-
},
|
|
634
|
-
],
|
|
635
|
-
active: false,
|
|
636
|
-
};
|
|
637
|
-
case "open":
|
|
638
|
-
return {
|
|
639
|
-
effects: [
|
|
640
|
-
{
|
|
641
|
-
verb: VERB_OPEN_VSCODE,
|
|
642
|
-
args: [evalTemplate(c.target, buildScope(store))],
|
|
643
|
-
},
|
|
644
|
-
],
|
|
645
|
-
active: false,
|
|
646
|
-
};
|
|
647
|
-
// [LAW:one-source-of-truth] The persist-* arms mirror set-*'s realization
|
|
648
|
-
// verbatim (same literal/option/cycle/bounded semantics), only the wire
|
|
649
|
-
// verb differs (VERB_SET_CONFIG/VERB_STEP_CONFIG instead of
|
|
650
|
-
// VERB_SET_STATE/VERB_STEP_STATE) — the daemon-side handler is what makes
|
|
651
|
-
// the write durable, not the click itself.
|
|
652
|
-
case "persist-literal": {
|
|
653
|
-
const current = readVar(store, c.stateVar);
|
|
654
|
-
return {
|
|
655
|
-
effects: [{ verb: VERB_SET_CONFIG, args: [sessionId, c.key, c.value] }],
|
|
656
|
-
active: current === c.value,
|
|
657
|
-
};
|
|
658
|
-
}
|
|
659
|
-
case "persist-option": {
|
|
660
|
-
const value = boundValue ?? display;
|
|
661
|
-
const current = readVar(store, c.stateVar);
|
|
662
|
-
return {
|
|
663
|
-
effects: [{ verb: VERB_SET_CONFIG, args: [sessionId, c.key, value] }],
|
|
664
|
-
active: current === value,
|
|
665
|
-
};
|
|
666
|
-
}
|
|
667
|
-
case "persist-cycle": {
|
|
668
|
-
const next = c.members[(cycleIndex(c, store) + 1) % c.members.length]!;
|
|
669
|
-
return {
|
|
670
|
-
effects: [{ verb: VERB_SET_CONFIG, args: [sessionId, c.key, next] }],
|
|
671
|
-
active: false,
|
|
672
|
-
};
|
|
673
|
-
}
|
|
674
|
-
case "persist-bounded": {
|
|
675
|
-
return {
|
|
676
|
-
effects: [
|
|
677
|
-
{
|
|
678
|
-
verb: VERB_STEP_CONFIG,
|
|
679
|
-
args: [sessionId, c.key, String(c.by)],
|
|
680
|
-
},
|
|
681
|
-
],
|
|
682
|
-
active: false,
|
|
683
|
-
};
|
|
684
|
-
}
|
|
685
|
-
case "reset":
|
|
686
|
-
return {
|
|
687
|
-
effects: [{ verb: VERB_RESET_CONFIG, args: [sessionId, c.key] }],
|
|
688
|
-
active: false,
|
|
689
|
-
};
|
|
690
|
-
// [LAW:one-source-of-truth] No key to carry — the click just says "step
|
|
691
|
-
// the history", and which entry moves is entirely server-side state
|
|
692
|
-
// (never wire input, so there is nothing here to gate). Never "active":
|
|
693
|
-
// a history step is a one-shot trigger, not a current-selection toggle.
|
|
694
|
-
case "undo":
|
|
695
|
-
return {
|
|
696
|
-
effects: [{ verb: VERB_UNDO, args: [sessionId] }],
|
|
697
|
-
active: false,
|
|
698
|
-
};
|
|
699
|
-
case "redo":
|
|
700
|
-
return {
|
|
701
|
-
effects: [{ verb: VERB_REDO, args: [sessionId] }],
|
|
702
|
-
active: false,
|
|
703
|
-
};
|
|
704
|
-
// [LAW:one-source-of-truth] The op is fixed at compile time (see
|
|
705
|
-
// compileAction) — the click just delivers it. `apply-layout-op`'s
|
|
706
|
-
// handler does read-current-append-write (see verbs/index.ts), unlike
|
|
707
|
-
// persist-literal's plain overwrite, so it is its own verb rather than
|
|
708
|
-
// VERB_SET_CONFIG. Never "active": a structural edit is a one-shot
|
|
709
|
-
// trigger, not a current-selection toggle.
|
|
710
|
-
case "layout-op":
|
|
711
|
-
return {
|
|
712
|
-
effects: [
|
|
713
|
-
{
|
|
714
|
-
verb: VERB_APPLY_LAYOUT_OP,
|
|
715
|
-
args: [sessionId, c.key, encodeLayoutOp(c.op)],
|
|
716
|
-
},
|
|
717
|
-
],
|
|
718
|
-
active: false,
|
|
719
|
-
};
|
|
720
|
-
// [LAW:one-source-of-truth] The picked option (boundValue ?? display — the
|
|
721
|
-
// SAME resolution persist-option uses) becomes the op's `segment`; anchor/
|
|
722
|
-
// relation are the compiled literals. Same wire shape a literal layout-op
|
|
723
|
-
// emits, so the daemon's apply-layout-op handler and undo/redo need no
|
|
724
|
-
// knowledge of where the segment name came from. Never "active": a
|
|
725
|
-
// structural edit is a one-shot trigger, not a current-selection toggle.
|
|
726
|
-
// [LAW:dataflow-not-control-flow] The destination is resolved to a value
|
|
727
|
-
// and the SAME fold runs on it — a dual's realization is its chosen
|
|
728
|
-
// half's realization, with nothing about persistence duplicated here.
|
|
729
|
-
// Depth is structurally one: a dual's halves are the single-destination
|
|
730
|
-
// decls actionDestinations built, which can never be dual themselves.
|
|
731
|
-
//
|
|
732
|
-
// [LAW:no-silent-failure] A DURABLE click carries the session key to
|
|
733
|
-
// RELEASE as a trailing arg on its own write, so the daemon drops it only
|
|
734
|
-
// after that write succeeded. Without the release the write would be
|
|
735
|
-
// invisible to the session that made it — every settable global resolves
|
|
736
|
-
// session pick OVER durable default, so the workflow this menu invites
|
|
737
|
-
// ("try it here, then tick persist? to commit it") would set a default the
|
|
738
|
-
// user cannot see and leave the control dead for the rest of the session.
|
|
739
|
-
// Riding the write rather than sitting beside it is what makes the pair
|
|
740
|
-
// unsplittable: a click runs every effect it carries, so a rejected write
|
|
741
|
-
// must not be able to drop the pick on its own.
|
|
742
|
-
case "dual": {
|
|
743
|
-
const chosen = activeDestination(c, store);
|
|
744
|
-
const { effects, active } = realize(
|
|
745
|
-
chosen,
|
|
746
|
-
display,
|
|
747
|
-
boundValue,
|
|
748
|
-
store,
|
|
749
|
-
sessionId,
|
|
750
|
-
);
|
|
751
|
-
// The durable write carries the session key to RELEASE as one more
|
|
752
|
-
// segment on itself, so the daemon clears it only after its own write
|
|
753
|
-
// succeeded. A second effect beside it would not do: `dispatch` runs
|
|
754
|
-
// every effect in a click by design, so a rejected write would still
|
|
755
|
-
// wipe the session pick and leave nothing durable in its place.
|
|
756
|
-
return chosen === c.durable
|
|
757
|
-
? {
|
|
758
|
-
effects: effects.map((e) => ({
|
|
759
|
-
...e,
|
|
760
|
-
args: [...e.args, c.sessionKey],
|
|
761
|
-
})),
|
|
762
|
-
active,
|
|
763
|
-
}
|
|
764
|
-
: { effects, active };
|
|
765
|
-
}
|
|
766
|
-
case "layout-op-option": {
|
|
767
|
-
const segment = boundValue ?? display;
|
|
768
|
-
const op: LayoutOp = {
|
|
769
|
-
op: "insert",
|
|
770
|
-
segment,
|
|
771
|
-
anchor: c.anchor,
|
|
772
|
-
relation: c.relation,
|
|
773
|
-
};
|
|
774
|
-
return {
|
|
775
|
-
effects: [
|
|
776
|
-
{
|
|
777
|
-
verb: VERB_APPLY_LAYOUT_OP,
|
|
778
|
-
args: [sessionId, c.key, encodeLayoutOp(op)],
|
|
779
|
-
},
|
|
780
|
-
],
|
|
781
|
-
active: false,
|
|
782
|
-
};
|
|
783
|
-
}
|
|
784
|
-
}
|
|
785
|
-
}
|
|
786
|
-
|
|
787
|
-
// [LAW:dataflow-not-control-flow] Which text a region shows is a pure function
|
|
788
|
-
// of (action kind, bound displays, current state). A cycle binds one display per
|
|
789
|
-
// member positionally (the toggle/N-state-cycler form: `{{ action "t" "▸" "▾"
|
|
790
|
-
// }}`) or one static display for all states; every other kind binds one display
|
|
791
|
-
// plus an optional boundValue (the option-picker form). Wrong arity is an author
|
|
792
|
-
// error surfaced loudly at render (composeWithDiagnostics shows it), never a
|
|
793
|
-
// silently dropped argument.
|
|
794
|
-
function selectDisplay(
|
|
795
|
-
name: string,
|
|
796
|
-
action: CompiledActionDecl,
|
|
797
|
-
displays: readonly string[],
|
|
798
|
-
store: VariableStore,
|
|
799
|
-
): { display: string; boundValue: string | undefined } {
|
|
800
|
-
if (displays.length === 0) {
|
|
801
|
-
throw new Error(`action "${name}" needs a display (the clickable text)`);
|
|
802
|
-
}
|
|
803
|
-
if (action.kind === "set-cycle" || action.kind === "persist-cycle") {
|
|
804
|
-
// [LAW:single-enforcer] The arity rule and the pick are the disclosure
|
|
805
|
-
// primitive's, not this file's — `{{ menu }}` resolves its own trigger
|
|
806
|
-
// through the same function over its `[closed, member]` cycle, so the two
|
|
807
|
-
// disclosure kinds cannot disagree about what a display binding means.
|
|
808
|
-
const display = pickCycleDisplay(
|
|
809
|
-
`action "${name}"`,
|
|
810
|
-
displays,
|
|
811
|
-
action.members.length,
|
|
812
|
-
cycleIndex(action, store),
|
|
813
|
-
);
|
|
814
|
-
return { display, boundValue: undefined };
|
|
815
|
-
}
|
|
816
|
-
if (displays.length > 2) {
|
|
817
|
-
throw new Error(
|
|
818
|
-
`action "${name}" takes a display and an optional bound value, got ${displays.length} arguments (per-state displays are a cycle action's form)`,
|
|
819
|
-
);
|
|
820
|
-
}
|
|
821
|
-
return { display: displays[0]!, boundValue: displays[1] };
|
|
822
|
-
}
|
|
823
|
-
|
|
824
|
-
// Realize a named action against the live state into ONE clickable RichText. The
|
|
825
|
-
// `action` template function delegates here.
|
|
826
|
-
export function renderAction(
|
|
827
|
-
name: string,
|
|
828
|
-
displays: readonly string[],
|
|
829
|
-
runtime: ActionRuntime,
|
|
830
|
-
): RichText {
|
|
831
|
-
const declared = runtime.compiled.get(name);
|
|
832
|
-
// [LAW:no-defensive-null-guards] The loader validates every `{{ action "x" }}`
|
|
833
|
-
// reference resolves to a declared action, and compileActions compiled every
|
|
834
|
-
// declared action for THIS config's engine. A miss is a caller/wiring bug.
|
|
835
|
-
if (!declared) {
|
|
836
|
-
throw new Error(`action "${name}" is not declared in this config`);
|
|
837
|
-
}
|
|
838
|
-
const store = runtime.store;
|
|
839
|
-
// [LAW:dataflow-not-control-flow] DISPLAY selection reads the resolved half
|
|
840
|
-
// (a cycle's glyph is the current member's, whichever store it will write),
|
|
841
|
-
// while REALIZATION is handed the declaration itself — a dual realizes as
|
|
842
|
-
// its chosen half PLUS the session clear that keeps a durable write visible,
|
|
843
|
-
// and that pairing belongs to the one fold that owns the union.
|
|
844
|
-
const { display, boundValue } = selectDisplay(
|
|
845
|
-
name,
|
|
846
|
-
activeDestination(declared, store),
|
|
847
|
-
displays,
|
|
848
|
-
store,
|
|
849
|
-
);
|
|
850
|
-
const sessionId = readVar(store, "session.id");
|
|
851
|
-
const { effects, active } = realize(
|
|
852
|
-
declared,
|
|
853
|
-
display,
|
|
854
|
-
boundValue,
|
|
855
|
-
store,
|
|
856
|
-
sessionId,
|
|
857
|
-
);
|
|
858
|
-
return linkFragment(display, effectsUrl(effects), active);
|
|
859
|
-
}
|
|
860
|
-
|
|
861
|
-
// ─── FuncMap entry ─────────────────────────────────────────────────────────────
|
|
862
|
-
|
|
863
|
-
// [LAW:dataflow-not-control-flow] One func; the action NAME selects which declared
|
|
864
|
-
// effect fires, the trailing strings are the bound displays. For most kinds that
|
|
865
|
-
// is the clickable text plus an optional boundValue (absent ⇒ the option IS the
|
|
866
|
-
// display, the common picker form `{{ action "applyTheme" . }}`); for a cycle it
|
|
867
|
-
// is one display per member (the current member's display renders) or one static
|
|
868
|
-
// display. Returns T (RichText), the single fragment go-template-js emits for
|
|
869
|
-
// `{{ action … }}`.
|
|
870
|
-
//
|
|
871
|
-
// [LAW:one-way-deps] The caller injects this FuncMap into createCcCandybarEngine
|
|
872
|
-
// (capabilities-over-context) so the generic engine never imports the action
|
|
873
|
-
// feature.
|
|
874
|
-
export function actionFuncs(runtime: ActionRuntime): FuncMap {
|
|
875
|
-
return {
|
|
876
|
-
action: {
|
|
877
|
-
fn: (name: string, ...displays: string[]) =>
|
|
878
|
-
renderAction(name, displays, runtime),
|
|
879
|
-
argTypes: ["string", "string"],
|
|
880
|
-
returnType: "T",
|
|
881
|
-
},
|
|
882
|
-
};
|
|
883
|
-
}
|