@promptctl/cc-candybar 1.37.0 → 1.39.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.
@@ -28,21 +28,17 @@ import type { FuncMap } from "@promptctl/go-template-js";
28
28
  import { toNumber } from "../var-system/types.js";
29
29
  import { stripChromeCols } from "./strip.js";
30
30
  import { TERM_COLS_VAR } from "../config/dsl-types.js";
31
+ import { effectsUrl, VERB_SET_STATE } from "../click/wire.js";
31
32
  import {
32
- effectsUrl,
33
- VERB_APPLY_LAYOUT_OP,
34
- VERB_SET_CONFIG,
35
- VERB_SET_STATE,
36
- } from "../click/wire.js";
37
- import { encodeLayoutOp } from "../config/layout-ops.js";
38
- import {
33
+ activeDestination,
39
34
  linkFragment,
35
+ realize,
40
36
  readVar,
41
37
  type ActionRuntime,
42
38
  type CompiledActionDecl,
43
39
  } from "./action.js";
40
+ import { DISCLOSURE_GLYPH_CLOSE } from "../config/disclosure.js";
44
41
 
45
- const PICKER_CLOSE = "✕";
46
42
  const PICKER_PREV = "←";
47
43
  const PICKER_NEXT = "→";
48
44
 
@@ -160,7 +156,15 @@ function requireOptionKind(
160
156
  CompiledActionDecl,
161
157
  { kind: "set-option" | "persist-option" | "layout-op-option" }
162
158
  > {
163
- const action = runtime.compiled.get(name);
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;
164
168
  if (
165
169
  !action ||
166
170
  (action.kind !== "set-option" &&
@@ -194,6 +198,13 @@ export function renderPicker(
194
198
  runtime: ActionRuntime,
195
199
  ): RichText {
196
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)!;
197
208
  const store = runtime.store;
198
209
  const sessionId = readVar(store, "session.id");
199
210
  // [LAW:no-defensive-null-guards] layout-op-option carries no `stateVar` —
@@ -231,7 +242,7 @@ export function renderPicker(
231
242
  2 * runtime.padding,
232
243
  )
233
244
  : Infinity;
234
- const closeReserve = cellWidth(PICKER_CLOSE) + 1;
245
+ const closeReserve = cellWidth(DISCLOSURE_GLYPH_CLOSE) + 1;
235
246
  const arrowReserve = cellWidth(PICKER_PREV) + 1 + cellWidth(PICKER_NEXT) + 1;
236
247
  const firstPass = paginate(widths, available, closeReserve);
237
248
  const pages =
@@ -275,33 +286,26 @@ export function renderPicker(
275
286
  ? [{ verb: VERB_SET_STATE, args: [sessionId, ...closeFlat] }]
276
287
  : [];
277
288
  const optionUrl = (option: string): string => {
278
- if (apply.kind === "persist-option") {
279
- return effectsUrl([
280
- { verb: VERB_SET_CONFIG, args: [sessionId, apply.key, option] },
281
- ...closeEffect,
282
- ]);
283
- }
284
- if (apply.kind === "layout-op-option") {
285
- const op = encodeLayoutOp({
286
- op: "insert",
287
- segment: option,
288
- anchor: apply.anchor,
289
- relation: apply.relation,
290
- });
291
- return effectsUrl([
292
- { verb: VERB_APPLY_LAYOUT_OP, args: [sessionId, apply.key, op] },
293
- ...closeEffect,
294
- ]);
295
- }
296
- return effectsUrl([
297
- {
298
- verb: VERB_SET_STATE,
299
- args: [sessionId, apply.key, option, ...(closeOnPick ? closeFlat : [])],
300
- },
301
- ]);
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]);
302
304
  };
303
305
 
304
- const frags: RichText[] = [linkFragment(PICKER_CLOSE, closeUrl, false)];
306
+ const frags: RichText[] = [
307
+ linkFragment(DISCLOSURE_GLYPH_CLOSE, closeUrl, false),
308
+ ];
305
309
  if (pageIdx > 0) {
306
310
  frags.push(linkFragment(PICKER_PREV, pageUrl(pageIdx - 1), false));
307
311
  }
@@ -270,7 +270,16 @@ export const DEFAULT_WRAP = true;
270
270
  // boolean globals field — the same two members the bundled default's
271
271
  // `cycle: [...]` toggle writes and the parse below reads. Spelled once so a
272
272
  // toggle cannot write a member the resolver refuses to parse.
273
- export const BOOLEAN_MEMBERS = ["true", "false"] as const;
273
+ // [LAW:one-source-of-truth] The two members, named, because their ORDER is
274
+ // meaningful and differs per control: a cycle's members are ordered
275
+ // default-state-first (an unwritten key counts as the first member and clicks
276
+ // to the second), so `autoWrap` — on by default — cycles ["true","false"]
277
+ // while `persist?` — off by default — cycles [BOOLEAN_FALSE, BOOLEAN_TRUE].
278
+ // Spelling the members rather than reversing the pair keeps each declaration's
279
+ // default state readable at its own site.
280
+ export const BOOLEAN_TRUE = "true";
281
+ export const BOOLEAN_FALSE = "false";
282
+ export const BOOLEAN_MEMBERS = [BOOLEAN_TRUE, BOOLEAN_FALSE] as const;
274
283
 
275
284
  // [LAW:one-source-of-truth] The one statement of the globals.padding default
276
285
  // (one space per side inside each segment cell — current behavior, matching the
@@ -287,8 +296,16 @@ export const PADDING_RANGE = { min: 0, max: 16 } as const;
287
296
  // [LAW:parse-dont-validate] A SessionState string to a boolean, or null for
288
297
  // anything else. `??` in effectiveGlobal (never `||`) is what keeps a parsed
289
298
  // `false` a real answer rather than falling through to the default.
290
- function parseBoolean(raw: string): boolean | null {
291
- return raw === "true" ? true : raw === "false" ? false : null;
299
+ //
300
+ // [LAW:one-source-of-truth] Exported because SessionState holds strings and
301
+ // BOOLEAN_MEMBERS above is the one spelling of a boolean in that store — so
302
+ // every reader of a boolean session key parses it HERE, not with its own
303
+ // truthiness rule. The second reader is the dual-destination action's
304
+ // `persistWhen` selector (src/render/action.ts): "is persist? checked" is the
305
+ // same question `autoWrap`'s toggle asks of its own key, and a bespoke
306
+ // `raw !== ""` there would accept values this parse rejects.
307
+ export function parseSessionBoolean(raw: string): boolean | null {
308
+ return raw === BOOLEAN_TRUE ? true : raw === BOOLEAN_FALSE ? false : null;
292
309
  }
293
310
 
294
311
  // [LAW:parse-dont-validate] A SessionState string to a padding value inside the
@@ -313,7 +330,7 @@ export function effectiveAutoWrap(
313
330
  sessionAutoWrap,
314
331
  globalsAutoWrap,
315
332
  DEFAULT_WRAP,
316
- parseBoolean,
333
+ parseSessionBoolean,
317
334
  );
318
335
  }
319
336