@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/dist/index.mjs +61 -61
- package/package.json +7 -7
- package/src/config/default-dsl-config.ts +16 -19
- package/src/config/disclosure.ts +55 -0
- package/src/config/loader/layout.ts +29 -31
- package/src/config/loader/menu-synth.ts +163 -108
- package/src/config/loader/refs.ts +11 -10
- package/src/config/loader/reserved-namespace.ts +38 -0
- package/src/config/menu-keys.ts +78 -11
- package/src/daemon/cache/watchers.ts +5 -1
- package/src/daemon/verbs/state-validators.ts +41 -44
- package/src/render/action.ts +7 -1
- package/src/render/menu.ts +49 -56
- package/src/render/picker.ts +51 -23
- package/src/segments/git.ts +126 -172
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@promptctl/cc-candybar",
|
|
3
|
-
"version": "1.
|
|
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.
|
|
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.
|
|
99
|
-
"@promptctl/cc-candybar-darwin-x64": "1.
|
|
100
|
-
"@promptctl/cc-candybar-linux-x64": "1.
|
|
101
|
-
"@promptctl/cc-candybar-linux-arm64": "1.
|
|
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
|
-
//
|
|
532
|
-
//
|
|
533
|
-
//
|
|
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
|
-
//
|
|
798
|
-
//
|
|
799
|
-
//
|
|
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"
|
|
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
|
|
880
|
-
// styleControl {{ menu }} above. The disclosure's open-state toggle
|
|
881
|
-
// backing state var
|
|
882
|
-
//
|
|
883
|
-
//
|
|
884
|
-
//
|
|
885
|
-
//
|
|
886
|
-
//
|
|
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
|
|
389
|
-
//
|
|
390
|
-
|
|
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 ===
|
|
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 "${
|
|
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
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
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,
|
|
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 !==
|
|
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
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
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} ${
|
|
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
|
};
|