@promptctl/cc-candybar 1.3.0 → 1.5.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.
@@ -39,7 +39,11 @@ import { buildDebugSnapshot } from "./debug";
39
39
  import { DEBUG_WHATS, isDebugWhat } from "./debug-types";
40
40
  import { expandHome } from "../config/dsl-loader.js";
41
41
  import { renderDsl } from "../dsl/render.js";
42
- import { effectiveThemeName, resolverForThemeName } from "../themes/index.js";
42
+ import {
43
+ effectiveStripStyle,
44
+ effectiveThemeName,
45
+ resolverForThemeName,
46
+ } from "../themes/index.js";
43
47
  import {
44
48
  renderStripCells,
45
49
  DEFAULT_TERMINAL_WIDTH,
@@ -753,6 +757,16 @@ async function handleRequest(req: Request): Promise<HandledRequest> {
753
757
  entry.state.config.globals.palette,
754
758
  ),
755
759
  );
760
+ // [LAW:one-type-per-behavior][LAW:dataflow-not-control-flow] The powerline
761
+ // cap/separator SHAPE, resolved per render the exact way the theme is: the
762
+ // session's clicked style (SessionState) over the config default over the
763
+ // "powerline" floor — so a style click reshapes the whole bar on the next
764
+ // render. The base style on renderOpts is only the floor; this is the live
765
+ // override fed to the one joiner dispatch in renderStripCells.
766
+ renderOpts.style = effectiveStripStyle(
767
+ sessionState.get(req.hookData.session_id, "style"),
768
+ entry.state.config.globals.style,
769
+ );
756
770
  // [LAW:single-enforcer] renderDsl internally calls
757
771
  // `registry.applyInput(payload)` as its first step (see step 1 in
758
772
  // src/dsl/render.ts). The daemon must not pre-apply — doing so
@@ -24,7 +24,7 @@
24
24
  // numeric stepper with int-range bounds), this same shape extends — the
25
25
  // validator becomes the parsing boundary, the verb body the dataflow.
26
26
 
27
- import { listResolvablePaletteNames, STYLE_ORDER } from "../../themes/policy";
27
+ import { listResolvablePaletteNames, STRIP_STYLES } from "../../themes/policy";
28
28
  import type { ActionDecl, OptionSource } from "../../config/action";
29
29
  import type { DslConfig } from "../../config/dsl-types";
30
30
 
@@ -88,13 +88,13 @@ export type DerivedValidatorSpec =
88
88
  //
89
89
  // [LAW:single-enforcer] Each validator's accepted-set is one constant
90
90
  // lookup structure — a Set for O(1) `has` (matching the BOOLEAN_*
91
- // validators below). The theme registry (rich-js THEMES) and STYLE_ORDER
91
+ // validators below). The theme registry (rich-js THEMES) and STRIP_STYLES
92
92
  // are module-init-static, so caching at module load is correct by
93
93
  // construction; the (list, set) pair is built from the same source so
94
94
  // the error-message ordering and the lookup membership cannot drift.
95
95
  const RESOLVABLE_THEMES_LIST: readonly string[] = listResolvablePaletteNames();
96
96
  const RESOLVABLE_THEMES: ReadonlySet<string> = new Set(RESOLVABLE_THEMES_LIST);
97
- const RESOLVABLE_STYLES: ReadonlySet<string> = new Set(STYLE_ORDER);
97
+ const RESOLVABLE_STYLES: ReadonlySet<string> = new Set(STRIP_STYLES);
98
98
 
99
99
  const validateTheme: KeyValidator = (raw) => {
100
100
  if (!raw) return { ok: false, reason: "theme name is required" };
@@ -112,7 +112,7 @@ const validateStyle: KeyValidator = (raw) => {
112
112
  if (!RESOLVABLE_STYLES.has(raw)) {
113
113
  return {
114
114
  ok: false,
115
- reason: `unknown style "${raw}" (have: ${STYLE_ORDER.join(", ")})`,
115
+ reason: `unknown style "${raw}" (have: ${STRIP_STYLES.join(", ")})`,
116
116
  };
117
117
  }
118
118
  return { ok: true, value: raw };
@@ -446,7 +446,7 @@ export function makeRangeValidator(
446
446
  // style validators consult — the rendered options and the derived gate cannot
447
447
  // diverge because there is no second enumeration.
448
448
  function optionValuesFor(src: OptionSource): readonly string[] {
449
- return src === "themes" ? RESOLVABLE_THEMES_LIST : STYLE_ORDER;
449
+ return src === "themes" ? RESOLVABLE_THEMES_LIST : STRIP_STYLES;
450
450
  }
451
451
 
452
452
  // [LAW:types-are-the-program] Collapse one key's spec contributions into the
package/src/demo/dsl.ts CHANGED
@@ -27,6 +27,7 @@ import {
27
27
  } from "../config/dsl-loader.js";
28
28
  import { VariableStore } from "../var-system/store.js";
29
29
  import { SourceRegistry } from "../var-system/sources.js";
30
+ import { SessionState } from "../daemon/session-state.js";
30
31
  import { listResolvablePaletteNames } from "../themes/policy.js";
31
32
  import { effectiveThemeName, resolverForThemeName } from "../themes/index.js";
32
33
  import { registerDslConfig, renderDsl } from "../dsl/render.js";
@@ -75,7 +76,12 @@ const basePalette = resolverForThemeName(
75
76
  // registry, so dispose() must run even if registration or rendering throws —
76
77
  // otherwise those handles keep the process alive. try/finally guarantees it.
77
78
  const store = new VariableStore();
78
- const registry = new SourceRegistry(store);
79
+ // [LAW:no-silent-failure] An EMPTY SessionState — `kind: "state"` variables
80
+ // (the default config's style picker, any interactive config) require one at
81
+ // registration; without it declareState fails and the segment renders an error
82
+ // cell. The demo never clicks, so an empty store is correct: every state var
83
+ // resolves to its declared default (closed pickers, "(default)" labels).
84
+ const registry = new SourceRegistry(store, "", undefined, new SessionState());
79
85
  try {
80
86
  const compiled = registerDslConfig(config, registry, {
81
87
  cwd: process.cwd(),
@@ -26,7 +26,7 @@ import type { VariableStore } from "../var-system/store.js";
26
26
  import { toString as varToString } from "../var-system/types.js";
27
27
  import { buildScope } from "../template-engine/scope.js";
28
28
  import type { ActionDecl, OptionSource } from "../config/action.js";
29
- import { listResolvablePaletteNames, STYLE_ORDER } from "../themes/policy.js";
29
+ import { listResolvablePaletteNames, STRIP_STYLES } from "../themes/policy.js";
30
30
  import {
31
31
  effectsUrl,
32
32
  VERB_COPY,
@@ -107,7 +107,7 @@ export type CompiledActions = ReadonlyMap<string, CompiledActionDecl>;
107
107
  // options and the gate cannot diverge. The render-side resolver (the daemon's
108
108
  // validator-derivation has its own that must agree, both reading themes/policy).
109
109
  export function optionDomain(src: OptionSource): readonly string[] {
110
- return src === "themes" ? listResolvablePaletteNames() : STYLE_ORDER;
110
+ return src === "themes" ? listResolvablePaletteNames() : STRIP_STYLES;
111
111
  }
112
112
 
113
113
  // [LAW:locality-or-seam] The runtime holder the `action` template function closes
@@ -10,6 +10,7 @@ import {
10
10
  type Joiner,
11
11
  type ColorSystemSpec,
12
12
  } from "@promptctl/rich-js";
13
+ import type { StripStyle } from "../themes/policy.js";
13
14
 
14
15
  export interface RenderedSegmentLike {
15
16
  type: string;
@@ -18,7 +19,11 @@ export interface RenderedSegmentLike {
18
19
  fgHex?: string;
19
20
  }
20
21
 
21
- export type StripStyle = "powerline" | "capsule" | "plain";
22
+ // [LAW:one-source-of-truth] `StripStyle` and its value list live in
23
+ // themes/policy.ts (the style-identifier policy module, importable by the
24
+ // option-source machinery without a render→template-engine cycle). Re-exported
25
+ // here so render-layer consumers can keep importing it from the strip module.
26
+ export type { StripStyle };
22
27
 
23
28
  // [LAW:one-source-of-truth] Raw terminal cols we assume when the wire
24
29
  // didn't give us one (older client, env-stripped spawn). RAW — not
@@ -38,13 +43,22 @@ export interface BuildLineOptions {
38
43
  }
39
44
 
40
45
  function pickJoiner(style: StripStyle, separator?: string): Joiner {
41
- // [LAW:dataflow-not-control-flow] joiner choice is data-driven; one branch
42
- // per shape, no nested conditionals.
43
- if (style === "capsule") return new CapsuleJoiner();
44
- if (style === "plain") {
45
- return new PlainJoiner(separator !== undefined ? { separator } : {});
46
+ // [LAW:dataflow-not-control-flow] joiner choice is data-driven; one arm per
47
+ // shape. [LAW:types-are-the-program] Total over StripStyle — the `never`
48
+ // default makes adding a STRIP_STYLES member a compile error here until it
49
+ // gets a joiner, so the picker's domain can never offer an unrenderable shape.
50
+ switch (style) {
51
+ case "capsule":
52
+ return new CapsuleJoiner();
53
+ case "plain":
54
+ return new PlainJoiner(separator !== undefined ? { separator } : {});
55
+ case "powerline":
56
+ return new PowerlineJoiner();
57
+ default: {
58
+ const _exhaustive: never = style;
59
+ return _exhaustive;
60
+ }
46
61
  }
47
- return new PowerlineJoiner();
48
62
  }
49
63
 
50
64
  function toCell(seg: RenderedSegmentLike): RichText {
@@ -16,22 +16,22 @@ import {
16
16
  formatModelName,
17
17
  shortenModelName,
18
18
  } from "../utils/formatters.js";
19
- import { listResolvablePaletteNames, STYLE_ORDER } from "../themes/policy.js";
19
+ import { listResolvablePaletteNames, STRIP_STYLES } from "../themes/policy.js";
20
20
 
21
21
  // [LAW:one-source-of-truth] The DSL `themes()` and `styles()` bindings
22
22
  // project the SAME canonical sources the set-state validator consults
23
- // (listResolvablePaletteNames / STYLE_ORDER). A picker (or a config that
23
+ // (listResolvablePaletteNames / STRIP_STYLES). A picker (or a config that
24
24
  // `range`s over themes() to emit OSC-8 cells) iterates the allow-list the
25
25
  // validator will enforce on the resulting click — the list and the gate cannot
26
26
  // diverge because there is no second list.
27
27
  //
28
28
  // Module-init caching is correct by construction: rich-js THEMES is a
29
29
  // static import (no dynamic palette registration at runtime) and
30
- // STYLE_ORDER is a const array. The "reactivity" requirement from the
30
+ // STRIP_STYLES is a const array. The "reactivity" requirement from the
31
31
  // ticket is satisfied vacuously — the lists never change during a
32
32
  // daemon lifetime, so a cached snapshot IS the current truth.
33
33
  const THEMES_LIST: readonly string[] = listResolvablePaletteNames();
34
- const STYLES_LIST: readonly string[] = [...STYLE_ORDER];
34
+ const STYLES_LIST: readonly string[] = [...STRIP_STYLES];
35
35
 
36
36
  // Normalize an engine-supplied numeric argument. The "number" argType admits
37
37
  // both number and bigint (per @promptctl/go-template-js); the underlying
@@ -6,12 +6,16 @@
6
6
  export {
7
7
  resolvePaletteName,
8
8
  effectiveThemeName,
9
+ effectiveStripStyle,
10
+ isStripStyle,
9
11
  listResolvablePaletteNames,
10
12
  listAvailableThemes,
11
13
  pickRandomTheme,
14
+ STRIP_STYLES,
12
15
  STYLE_ORDER,
13
16
  DISPLAY_STYLES,
14
17
  } from "./policy.js";
18
+ export type { StripStyle } from "./policy.js";
15
19
 
16
20
  export {
17
21
  resolverForThemeName,
@@ -55,8 +55,43 @@ export function listAvailableThemes(): string[] {
55
55
  return [...allNames].sort();
56
56
  }
57
57
 
58
- // --- Style identifiers ---
58
+ // --- Powerline strip-style identifiers ---
59
59
 
60
+ // [LAW:one-source-of-truth][LAW:types-are-the-program] The single canonical set
61
+ // of powerline cap/separator shapes a render can take. The `StripStyle` type is
62
+ // DERIVED from this const, so the picker's option domain, the SessionState
63
+ // validator, the `styles()` template binding, and `pickJoiner`'s dispatch all
64
+ // trace to one literal — adding a shape here forces a new `pickJoiner` arm at
65
+ // compile time (the joiner switch is total over `StripStyle`). This is where the
66
+ // drift between "what you can pick" and "what actually renders" is closed.
67
+ export const STRIP_STYLES = ["powerline", "capsule", "plain"] as const;
68
+ export type StripStyle = (typeof STRIP_STYLES)[number];
69
+
70
+ // [LAW:types-are-the-program] The trust-boundary narrowing from a raw
71
+ // SessionState string (or a config default) to the closed `StripStyle` union.
72
+ export function isStripStyle(value: string): value is StripStyle {
73
+ return (STRIP_STYLES as readonly string[]).includes(value);
74
+ }
75
+
76
+ // The strip style a render should use, as data. [LAW:dataflow-not-control-flow]
77
+ // [LAW:one-type-per-behavior] The exact shape of `effectiveThemeName`, one
78
+ // dimension over: session choice over config default over the "powerline" floor,
79
+ // no "if the session has a style" branch. A value outside the domain (a stale
80
+ // SessionState entry from a prior option vocabulary) collapses to the floor —
81
+ // `pickJoiner` would render it as powerline anyway, so the floor keeps the
82
+ // returned type honest rather than silently widening.
83
+ export function effectiveStripStyle(
84
+ sessionStyle: string | null,
85
+ globalsStyle: StripStyle | undefined,
86
+ ): StripStyle {
87
+ const chosen = sessionStyle ?? globalsStyle ?? "powerline";
88
+ return isStripStyle(chosen) ? chosen : "powerline";
89
+ }
90
+
91
+ // --- Legacy palette-preset identifiers (per-session random feature only) ---
92
+ // [LAW:no-silent-failure] NOT the picker's style domain — these were the
93
+ // pre-DSL bg/fg derivation presets, surviving only as the random pool for
94
+ // session-random.ts. The interactive style picker resolves over STRIP_STYLES.
60
95
  export const STYLE_ORDER: readonly string[] = [
61
96
  "surface",
62
97
  "muted",