@promptctl/cc-candybar 1.15.0 → 1.17.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@promptctl/cc-candybar",
3
- "version": "1.15.0",
3
+ "version": "1.17.0",
4
4
  "description": "Statusline renderer for Claude Code — a JSON5-configurable DSL with daemon-cached data sources, byte-clean palette-aware composition, and OSC8 click verbs.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.mjs",
@@ -91,10 +91,10 @@
91
91
  "mobx": "^6.15.0"
92
92
  },
93
93
  "optionalDependencies": {
94
- "@promptctl/cc-candybar-darwin-arm64": "1.15.0",
95
- "@promptctl/cc-candybar-darwin-x64": "1.15.0",
96
- "@promptctl/cc-candybar-linux-x64": "1.15.0",
97
- "@promptctl/cc-candybar-linux-arm64": "1.15.0"
94
+ "@promptctl/cc-candybar-darwin-arm64": "1.17.0",
95
+ "@promptctl/cc-candybar-darwin-x64": "1.17.0",
96
+ "@promptctl/cc-candybar-linux-x64": "1.17.0",
97
+ "@promptctl/cc-candybar-linux-arm64": "1.17.0"
98
98
  },
99
99
  "pnpm": {
100
100
  "supportedArchitectures": {
@@ -185,6 +185,18 @@ export const DEFAULT_DSL_CONFIG = {
185
185
  },
186
186
  "session.id": { kind: "input", path: "session_id", default: "" },
187
187
  version: { kind: "input", path: "version", default: "" },
188
+ // [LAW:one-source-of-truth] The daemon-resolved effective theme name —
189
+ // effectiveThemeName(sessionState.theme, globals.palette), the SAME name the
190
+ // rendered basePalette is built from. A theme-picker config's trigger reads
191
+ // `{{ .theme.effective }}` to show the active theme, so the label and the
192
+ // colors trace to one resolution and cannot drift — no per-config restating
193
+ // of the initial theme (which JSON5, being inert data, cannot derive). The
194
+ // daemon always provides it; the "" default is the unreachable-absence floor.
195
+ "theme.effective": {
196
+ kind: "input",
197
+ path: "theme.effective",
198
+ default: "",
199
+ },
188
200
 
189
201
  // [LAW:one-source-of-truth] The usable terminal width for THIS render —
190
202
  // the exact post-reserve cell count FlexStrip wraps to. renderDsl injects
@@ -33,7 +33,6 @@ import type { ContextProvider } from "../segments/context.js";
33
33
  import type { MetricsProvider } from "../segments/metrics.js";
34
34
  import type { TmuxService } from "../segments/tmux.js";
35
35
  import type { GitDataProvider } from "./cache/git.js";
36
- import type { SessionStateRW } from "./session-state.js";
37
36
 
38
37
  // ─── Augmented payload shape ─────────────────────────────────────────────────
39
38
 
@@ -54,7 +53,16 @@ export interface RenderPayload extends ClaudeHookData {
54
53
 
55
54
  readonly git?: GitPayload;
56
55
  readonly tmux?: { readonly session: string };
57
- readonly theme?: string;
56
+ // [LAW:one-source-of-truth] The daemon-resolved effective theme name —
57
+ // effectiveThemeName(sessionState.theme, globals.palette). The SAME value the
58
+ // rendered basePalette is built from, surfaced so a trigger label can display
59
+ // the active theme WITHOUT the config restating it (the label and the colors
60
+ // trace to one resolution and cannot drift).
61
+ // [LAW:types-are-the-program] REQUIRED, not optional: the daemon resolves it
62
+ // every render and buildRenderPayload includes it unconditionally, so the
63
+ // domain truth is "always present". A `?` here would let a callsite believe it
64
+ // could be undefined and guard defensively against an impossibility.
65
+ readonly theme: { readonly effective: string };
58
66
 
59
67
  // Usage-family. Each provider returns null when it has no data (no
60
68
  // transcript yet, no rate-limit window active, etc.); we drop the field
@@ -206,7 +214,6 @@ export interface RenderPayloadDeps {
206
214
  readonly contextProvider: ContextProvider;
207
215
  readonly metricsProvider: MetricsProvider;
208
216
  readonly tmuxService: TmuxService;
209
- readonly sessionState: SessionStateRW;
210
217
  // [LAW:single-enforcer] The log capability for every provider lane:
211
218
  // buildRenderPayload is the ONE place lane failures are logged, so the
212
219
  // providers' interiors never log and never double-log.
@@ -568,6 +575,13 @@ export async function buildRenderPayload(
568
575
  // registration; passing it in (rather than recomputing per render) keeps
569
576
  // the hot path free of the BFS + extractTemplateRefs cost.
570
577
  neededInputPaths: ReadonlySet<string>,
578
+ // [LAW:one-source-of-truth] The effective theme name, resolved ONCE by the
579
+ // daemon (effectiveThemeName(sessionState.theme, globals.palette)) and used
580
+ // for BOTH the rendered basePalette and this payload field — so a trigger
581
+ // label reading `.theme.effective` can never disagree with the colors. Passed
582
+ // in (not re-resolved here) because the daemon already computes it for the
583
+ // palette; this is that same value, threaded to the sole payload assembler.
584
+ effectiveTheme: string,
571
585
  ): Promise<RenderPayload> {
572
586
  const wants = (prefix: string): boolean =>
573
587
  anyPathStartsWith(neededInputPaths, prefix);
@@ -718,15 +732,6 @@ export async function buildRenderPayload(
718
732
  }
719
733
  : undefined;
720
734
 
721
- // [LAW:one-source-of-truth] The theme variable surfaces the session's
722
- // resolved theme so the toolbar/tray DSL templates can encode it into
723
- // cc-candybar:// URLs without re-resolving. SessionState owns the value;
724
- // we mirror it onto the payload, not redeclare it.
725
- const themeRaw = wants("theme")
726
- ? deps.sessionState.get(hookData.session_id, "theme")
727
- : undefined;
728
- const theme = typeof themeRaw === "string" ? themeRaw : undefined;
729
-
730
735
  // home is always available — it's a single env-var read, no I/O cost.
731
736
  // Letting the gate skip it would add a branch with no win.
732
737
  // [LAW:single-enforcer] All path-shaped payload fields are normalized
@@ -778,7 +783,11 @@ export async function buildRenderPayload(
778
783
  ...(home !== undefined && { home }),
779
784
  ...(gitProjection.git !== undefined && { git: gitProjection.git }),
780
785
  ...(tmuxValue !== undefined && { tmux: { session: tmuxValue } }),
781
- ...(theme !== undefined && { theme }),
786
+ // [LAW:one-source-of-truth] Always present the daemon resolves the
787
+ // effective theme every render (for basePalette), and this is that value.
788
+ // No `wants` gate: it costs nothing (a string already in hand) and a
789
+ // config that reads `.theme.effective` must always find it.
790
+ theme: { effective: effectiveTheme },
782
791
  ...(sessionPayload !== undefined && { session: sessionPayload }),
783
792
  ...(todayPayload !== undefined && { today: todayPayload }),
784
793
  ...(costPerHour !== undefined && { burn: { costPerHour } }),
@@ -745,24 +745,30 @@ async function handleRequest(req: Request): Promise<HandledRequest> {
745
745
  // No special-case branches — same composition every render.
746
746
  let body = "";
747
747
  if (entry.state !== null) {
748
+ // [LAW:one-source-of-truth] Resolve the effective theme ONCE — the
749
+ // session's chosen theme (SessionState) over the config default. This
750
+ // single name drives BOTH the payload's `theme.effective` field (the
751
+ // trigger label reads it) AND the rendered basePalette below, so a label
752
+ // and the colors can never disagree. Resolved here, before the payload
753
+ // build, so it can be threaded into the sole payload assembler.
754
+ const effectiveTheme = effectiveThemeName(
755
+ sessionState.get(req.hookData.session_id, "theme"),
756
+ entry.state.config.globals.palette,
757
+ );
748
758
  const payload = await buildRenderPayload(
749
759
  req.hookData,
750
760
  payloadDeps,
751
761
  req.cwd,
752
762
  entry.state.neededInputPaths,
763
+ effectiveTheme,
753
764
  );
754
765
  // [LAW:one-source-of-truth][LAW:dataflow-not-control-flow] basePalette
755
- // is derived per render from the effective theme — the session's chosen
756
- // theme (SessionState) over the config default so a theme click
757
- // recolors the whole bar on the next render. Not frozen on the cache
758
- // entry (one entry serves many sessions). resolverForThemeName memoizes,
759
- // so the per-render cost is one Map lookup once the theme is warm.
760
- const basePalette = resolverForThemeName(
761
- effectiveThemeName(
762
- sessionState.get(req.hookData.session_id, "theme"),
763
- entry.state.config.globals.palette,
764
- ),
765
- );
766
+ // is derived from the same effective theme resolved above so a theme
767
+ // click recolors the whole bar on the next render. Not frozen on the
768
+ // cache entry (one entry serves many sessions). resolverForThemeName
769
+ // memoizes, so the per-render cost is one Map lookup once the theme is
770
+ // warm.
771
+ const basePalette = resolverForThemeName(effectiveTheme);
766
772
  // [LAW:one-type-per-behavior][LAW:dataflow-not-control-flow] The powerline
767
773
  // cap/separator SHAPE, resolved per render the exact way the theme is: the
768
774
  // session's clicked style (SessionState) over the config default over the
@@ -1139,7 +1145,6 @@ const payloadDeps = {
1139
1145
  contextProvider,
1140
1146
  metricsProvider,
1141
1147
  tmuxService,
1142
- sessionState,
1143
1148
  // [LAW:single-enforcer] buildRenderPayload is the one log site for the
1144
1149
  // outcome-carrying provider lanes (git, cache).
1145
1150
  log: dlog,