@promptctl/cc-candybar 1.18.0 → 1.19.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.18.0",
3
+ "version": "1.19.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",
@@ -27,7 +27,7 @@
27
27
  "preloadtest": "pnpm build",
28
28
  "loadtest": "node --import tsx scripts/daemon-load-harness.ts",
29
29
  "preloadtest:gate": "pnpm build",
30
- "loadtest:gate": "node --import tsx scripts/daemon-load-harness.ts --sessions 25 --interval 300 --duration 20 --churn --transcript-lines 5000",
30
+ "loadtest:gate": "node --import tsx scripts/daemon-load-harness.ts --sessions 25 --interval 300 --duration 20 --churn --git-churn --transcript-lines 5000",
31
31
  "check:protocol": "node scripts/check-protocol.mjs",
32
32
  "gen:schema": "tsx scripts/gen-schema.ts",
33
33
  "check:schema": "tsx scripts/check-schema.ts",
@@ -89,16 +89,16 @@
89
89
  "typescript": "^5.0.0"
90
90
  },
91
91
  "dependencies": {
92
- "@promptctl/go-template-js": "^0.6.0",
92
+ "@promptctl/go-template-js": "^0.7.0",
93
93
  "@promptctl/rich-js": "^0.6.0",
94
94
  "json5": "^2.2.3",
95
95
  "mobx": "^6.15.0"
96
96
  },
97
97
  "optionalDependencies": {
98
- "@promptctl/cc-candybar-darwin-arm64": "1.18.0",
99
- "@promptctl/cc-candybar-darwin-x64": "1.18.0",
100
- "@promptctl/cc-candybar-linux-x64": "1.18.0",
101
- "@promptctl/cc-candybar-linux-arm64": "1.18.0"
98
+ "@promptctl/cc-candybar-darwin-arm64": "1.19.0",
99
+ "@promptctl/cc-candybar-darwin-x64": "1.19.0",
100
+ "@promptctl/cc-candybar-linux-x64": "1.19.0",
101
+ "@promptctl/cc-candybar-linux-arm64": "1.19.0"
102
102
  },
103
103
  "pnpm": {
104
104
  "supportedArchitectures": {
@@ -528,12 +528,9 @@ export const DEFAULT_DSL_CONFIG = {
528
528
  // and the render's read are one value. Empty default ⇒ the daemon's
529
529
  // "powerline" floor is in effect and styleControl shows "(default)".
530
530
  activeStyle: { kind: "state", key: "style", default: "" },
531
- // The style picker's page index. Open/closed is the synthesized menus.* key
532
- // the styleControl's {{ menu }} owns NOT this var, which is purely the page
533
- // the picker paginates by (default page 0; the disclosure resets it to 0 on
534
- // every toggle, so a reopened menu never lands on a stale page). The stylePage
535
- // action below declares the int gate this var reads back.
536
- stylePage: { kind: "state", key: "style-page", default: "0" },
531
+ // No page-cursor var: the styleControl {{ menu }} synthesizes its own page
532
+ // cursor (state var + int action, named by menuPageKey) under the reserved
533
+ // menus.* namespace, alongside its open-state.
537
534
  },
538
535
 
539
536
  // ─── Segments ──────────────────────────────────────────────────────────────
@@ -794,15 +791,16 @@ export const DEFAULT_DSL_CONFIG = {
794
791
  // onto the line below this row when open. [LAW:one-type-per-behavior] "a menu
795
792
  // that opens and closes" is one behavior the substrate already expresses — no
796
793
  // bespoke open-action + page-cursor-as-open-state + when-gated reveal row.
797
- // paged omitted false one full-width page (the 3 powerline shapes are a
798
- // small domain; FlexStrip soft-wraps if needed). closeOnPick omitted false
799
- // stay-open, so shapes can be tried in a row; the collapses.
794
+ // The apply name is the whole declaration: the page cursor (state var + int
795
+ // gate) is synthesized from the menu's identity, and the defaults are the
796
+ // canonical path paged (the 3 powerline shapes fit one page, so no arrows
797
+ // render) and stay-open, so shapes can be tried in a row; ▾/✕ collapse.
800
798
  // [LAW:dataflow-not-control-flow] No display state from the provider — the
801
799
  // label is the one "style" value the click writes and the render reads.
802
800
  styleControl: {
803
801
  template:
804
802
  "✦ {{ if .activeStyle }}{{ .activeStyle }}{{ else }}(default){{ end }} " +
805
- '{{ menu "applyStyle" "stylePage" }}',
803
+ '{{ menu "applyStyle" }}',
806
804
  bg: "surface",
807
805
  fg: "foreground",
808
806
  },
@@ -876,18 +874,17 @@ export const DEFAULT_DSL_CONFIG = {
876
874
  openProject: { open: "{{ .project_dir }}" },
877
875
  openTranscript: { open: "{{ .transcript_path }}" },
878
876
 
879
- // [LAW:locality-or-seam] The style menu's behaviors, decoupled by NAME from the
880
- // styleControl {{ menu }} above. The disclosure's open-state toggle + its
881
- // backing state var are SYNTHESIZED by the menu pass (under the reserved
882
- // menus.* namespace) no hand-authored open/close action. These two are the
883
- // picker body's effects, both gated by derivation (deriveActionValidators):
884
- // applyStyle writes the chosen shape, gated to the STRIP_STYLES allow-list
885
- // because its value source is `from: "styles"`; stylePage is the page cursor's
886
- // unbounded int gate (←/→ navigation + the disclosure's page-0 reset). The
877
+ // [LAW:locality-or-seam] The style menu's behavior, decoupled by NAME from the
878
+ // styleControl {{ menu }} above. The disclosure's open-state toggle, its
879
+ // backing state var, AND the picker body's page cursor (state var + int
880
+ // action) are all SYNTHESIZED by the menu pass (under the reserved menus.*
881
+ // namespace) no hand-authored open/close or page plumbing. This one action
882
+ // is the picker body's apply effect, gated by derivation
883
+ // (deriveActionValidators): it writes the chosen shape, gated to the
884
+ // STRIP_STYLES allow-list because its value source is `from: "styles"`. The
887
885
  // rendered click and the wire gate share that one source — a template cannot
888
886
  // smuggle an un-gated style write.
889
887
  applyStyle: { set: "style", from: "styles" },
890
- stylePage: { set: "style-page", int: true },
891
888
  },
892
889
 
893
890
  // [LAW:single-enforcer] / [LAW:one-source-of-truth] Display-formatting policy
@@ -0,0 +1,55 @@
1
+ // [LAW:one-source-of-truth] THE disclosure primitive: the one toggle machinery
2
+ // that both group sugar (`kind: "group"`, src/config/loader/layout.ts) and the
3
+ // `{{ menu }}` helper (src/config/loader/menu-synth.ts + src/render/menu.ts) are
4
+ // built on. A disclosure is a binary toggle over a SessionState key: the key
5
+ // holds either the CLOSED sentinel or a single MEMBER name; a `cycle` action
6
+ // flips between the two; a ▸/▾ glyph shows which state it is in; sibling
7
+ // disclosures sharing one key become mutually exclusive (an accordion) because
8
+ // one key holds one open member. Group and menu differ ONLY in their BODY (group
9
+ // reveals an arbitrary layout container gated by a `when`; menu drops a picker
10
+ // grid below the row) and in where the trigger lives (group synthesizes a toggle
11
+ // segment; the menu helper IS the trigger). The toggle itself — sentinel, glyphs,
12
+ // the `state` var, the `cycle` action — is single-sourced HERE so the two
13
+ // body-kinds cannot drift [LAW:one-type-per-behavior][LAW:decomposition].
14
+ //
15
+ // [LAW:one-way-deps] This module is intentionally PURE — it imports only decl
16
+ // TYPES (erased at build) and holds no loader (`ValidateCtx`, diagnostics) nor
17
+ // render (rich-js) dependency, so both the loader synthesis passes and the render
18
+ // helper can share it without dragging one layer into the other. The loader-side
19
+ // reserved-namespace collision check — the other half of the shared machinery,
20
+ // which needs the validation context — lives in
21
+ // `src/config/loader/reserved-namespace.ts`.
22
+
23
+ import type { ActionDecl } from "./action.js";
24
+ import type { VariableDecl } from "./dsl-types.js";
25
+
26
+ // The "nothing open" sentinel a disclosure's key starts from and returns to on
27
+ // close. A disclosure's MEMBER (a group name / a menu apply-action name) may
28
+ // never equal this — an equal member would make the cycle `[closed, "closed"]`
29
+ // (two identical members, never openable), which both synthesis passes reject.
30
+ export const DISCLOSURE_CLOSED = "closed";
31
+
32
+ // [LAW:representation] The disclosure glyph vocabulary — one pair for the whole
33
+ // bar so every disclosure reads the same (trailing the label/content it gates,
34
+ // per pdu.8): collapsed ▸, expanded ▾.
35
+ export const DISCLOSURE_GLYPH_CLOSED = "▸";
36
+ export const DISCLOSURE_GLYPH_OPEN = "▾";
37
+
38
+ // [LAW:single-enforcer] THE backing `state` variable a disclosure key implies:
39
+ // it holds the open member's name and defaults to `def` (the CLOSED sentinel for
40
+ // an independent disclosure, or an initially-open member for a group's
41
+ // `open: true`). One shape, so a group var and a menu var declared on one key
42
+ // cannot disagree on kind or key.
43
+ export function disclosureStateVar(key: string, def: string): VariableDecl {
44
+ return { kind: "state", key, default: def };
45
+ }
46
+
47
+ // [LAW:single-enforcer] THE toggle action a disclosure realizes: a binary `cycle`
48
+ // between the CLOSED sentinel and the member (ordered closed-first, so an unset or
49
+ // sibling-held key counts as the first member — the toggle renders ▸ and a click
50
+ // writes the member, opening it and auto-closing any accordion sibling). The
51
+ // derived click gate (`deriveActionValidators`) reads this like every other
52
+ // `set`, so a disclosure toggle needs no parallel verb [LAW:single-enforcer].
53
+ export function disclosureCycleAction(key: string, member: string): ActionDecl {
54
+ return { set: key, cycle: [DISCLOSURE_CLOSED, member] };
55
+ }
@@ -30,7 +30,15 @@ import {
30
30
  type VariableDecl,
31
31
  } from "../dsl-types.js";
32
32
  import type { ActionDecl } from "../action.js";
33
+ import {
34
+ DISCLOSURE_CLOSED,
35
+ DISCLOSURE_GLYPH_CLOSED,
36
+ DISCLOSURE_GLYPH_OPEN,
37
+ disclosureCycleAction,
38
+ disclosureStateVar,
39
+ } from "../disclosure.js";
33
40
  import { findKeyLine } from "./diagnostics.js";
41
+ import { reservedNamespaceCollisions } from "./reserved-namespace.js";
34
42
  import {
35
43
  describeType,
36
44
  describeValue,
@@ -385,9 +393,11 @@ export const validateRoot = (
385
393
  // prefix is rejected so synthesis can never silently collide.
386
394
  export const GROUP_NS = "groups.";
387
395
 
388
- // The "no group open" sentinel a group's cycle starts from. Group names are
389
- // forbidden from equaling it, so a cycle's two members are always distinct.
390
- const GROUP_CLOSED = "closed";
396
+ // [LAW:one-source-of-truth] The closed sentinel and ▸/▾ glyphs are the shared
397
+ // disclosure primitive (src/config/disclosure.ts) a group is one of its two
398
+ // body-kinds, so it reuses DISCLOSURE_CLOSED / DISCLOSURE_GLYPH_* rather than
399
+ // keeping a second copy that could drift from the menu's. Group names are
400
+ // forbidden from equaling the sentinel, so a cycle's two members are distinct.
391
401
 
392
402
  // [LAW:types-are-the-program] A group name must be template-addressable — it is
393
403
  // spliced into the synthesized `when` predicate and toggle template as
@@ -396,9 +406,6 @@ const GROUP_CLOSED = "closed";
396
406
  // dots, so a name needs no escaping anywhere it is spliced.
397
407
  const GROUP_NAME_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
398
408
 
399
- const GROUP_GLYPH_CLOSED = "▸";
400
- const GROUP_GLYPH_OPEN = "▾";
401
-
402
409
  function groupNameSpec(): FieldSpec<string> {
403
410
  return {
404
411
  required: true,
@@ -408,11 +415,11 @@ function groupNameSpec(): FieldSpec<string> {
408
415
  if (
409
416
  typeof v !== "string" ||
410
417
  !GROUP_NAME_RE.test(v) ||
411
- v === GROUP_CLOSED
418
+ v === DISCLOSURE_CLOSED
412
419
  ) {
413
420
  ctx.issues.push({
414
421
  path: `${path}.${field}`,
415
- message: `a group "name" must be an identifier (letters, digits, _; not starting with a digit) and not the reserved "${GROUP_CLOSED}", got ${describeValue(v)}`,
422
+ message: `a group "name" must be an identifier (letters, digits, _; not starting with a digit) and not the reserved "${DISCLOSURE_CLOSED}", got ${describeValue(v)}`,
416
423
  line: findKeyLine(ctx.source, ["root"]),
417
424
  });
418
425
  return undefined;
@@ -571,17 +578,10 @@ export function synthesizeGroupDecls(
571
578
  const groups = ctx.groups;
572
579
  if (groups.length === 0) return;
573
580
 
574
- for (const section of ["variables", "actions", "segments"] as const) {
575
- for (const name of Object.keys(out[section] ?? {})) {
576
- if (name.startsWith(GROUP_NS)) {
577
- groupIssue(
578
- ctx,
579
- `${section}.${name}`,
580
- `"${name}" is in the reserved "${GROUP_NS}" namespace (synthesized by group nodes) — rename it`,
581
- );
582
- }
583
- }
584
- }
581
+ // [LAW:single-enforcer] The disclosure primitive's shared reserved-namespace
582
+ // enforcer (mirroring {{ menu }}'s `menus.`) — a user name under `groups.`
583
+ // would silently shadow a synthesized artifact.
584
+ reservedNamespaceCollisions(ctx, out, GROUP_NS, "group nodes");
585
585
 
586
586
  const seen = new Set<string>();
587
587
  for (const g of groups) {
@@ -617,10 +617,10 @@ export function synthesizeGroupDecls(
617
617
  const defaultByKey = new Map<string, string>();
618
618
  for (const g of groups) {
619
619
  const key = groupStateKey(g);
620
- if (!defaultByKey.has(key)) defaultByKey.set(key, GROUP_CLOSED);
620
+ if (!defaultByKey.has(key)) defaultByKey.set(key, DISCLOSURE_CLOSED);
621
621
  if (g.open === true) {
622
622
  const prior = defaultByKey.get(key)!;
623
- if (prior !== GROUP_CLOSED) {
623
+ if (prior !== DISCLOSURE_CLOSED) {
624
624
  groupIssue(
625
625
  ctx,
626
626
  g.path,
@@ -645,22 +645,20 @@ export function synthesizeGroupDecls(
645
645
  (other) => other !== g && g.path.startsWith(other.path + "."),
646
646
  ).length;
647
647
  const indent = " ".repeat(depth);
648
- variables[name] = {
649
- kind: "state",
650
- key,
651
- default: defaultByKey.get(key)!,
652
- };
653
- // Members are ordered default-state-first ("closed" first): an unset or
654
- // sibling-held key counts as the first member, so the toggle renders ▸ and
655
- // clicks to its own name — expand, auto-closing the sibling on a shared key.
656
- actions[name] = { set: key, cycle: [GROUP_CLOSED, g.name] };
648
+ // [LAW:one-source-of-truth] The shared disclosure toggle: one state var + one
649
+ // binary cycle action, both from the primitive. Members are ordered default-
650
+ // state-first (closed first): an unset or sibling-held key counts as the first
651
+ // member, so the toggle renders ▸ and clicks to its own name — expand, auto-
652
+ // closing the sibling on a shared key.
653
+ variables[name] = disclosureStateVar(key, defaultByKey.get(key)!);
654
+ actions[name] = disclosureCycleAction(key, g.name);
657
655
  // [LAW:representation] The disclosure glyph trails the label it gates, so an
658
656
  // arrow reads as belonging to the text on its LEFT — adjacent toggles
659
657
  // ("details ▸" "links ▸") stay unambiguous even when abutted. `indent` is a
660
658
  // structural left-margin (nesting depth) and stays leading; the glyph is a
661
659
  // trailing affordance on the label, never a prefix.
662
660
  segments[name] = {
663
- template: `{{ action "${name}" "${indent}${label} ${GROUP_GLYPH_CLOSED}" "${indent}${label} ${GROUP_GLYPH_OPEN}" }}`,
661
+ template: `{{ action "${name}" "${indent}${label} ${DISCLOSURE_GLYPH_CLOSED}" "${indent}${label} ${DISCLOSURE_GLYPH_OPEN}" }}`,
664
662
  ...(g.bg !== undefined && { bg: g.bg }),
665
663
  ...(g.fg !== undefined && { fg: g.fg }),
666
664
  };