@promptctl/cc-candybar 1.9.0 → 1.11.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 +37 -37
- package/package.json +5 -5
- package/schema/cc-candybar.schema.json +11 -0
- package/src/config/default-dsl-config.ts +43 -31
- package/src/config/dsl-types.ts +18 -1
- package/src/config/loader/globals.ts +11 -1
- package/src/config/loader/validate-core.ts +28 -0
- package/src/daemon/server.ts +32 -2
- package/src/demo/dsl.ts +8 -1
- package/src/dsl/node-registry.ts +9 -0
- package/src/dsl/render.ts +9 -1
- package/src/render/action.ts +6 -0
- package/src/render/picker.ts +5 -1
- package/src/render/strip.ts +77 -20
- package/src/template-engine/layout.ts +23 -2
- package/src/themes/policy.ts +17 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@promptctl/cc-candybar",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.11.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.
|
|
95
|
-
"@promptctl/cc-candybar-darwin-x64": "1.
|
|
96
|
-
"@promptctl/cc-candybar-linux-x64": "1.
|
|
97
|
-
"@promptctl/cc-candybar-linux-arm64": "1.
|
|
94
|
+
"@promptctl/cc-candybar-darwin-arm64": "1.11.0",
|
|
95
|
+
"@promptctl/cc-candybar-darwin-x64": "1.11.0",
|
|
96
|
+
"@promptctl/cc-candybar-linux-x64": "1.11.0",
|
|
97
|
+
"@promptctl/cc-candybar-linux-arm64": "1.11.0"
|
|
98
98
|
},
|
|
99
99
|
"pnpm": {
|
|
100
100
|
"supportedArchitectures": {
|
|
@@ -52,11 +52,11 @@ import type { DslConfig } from "./dsl-types.js";
|
|
|
52
52
|
// full absolute path. Same logic handles equal home & current_dir → just "~".
|
|
53
53
|
const DIR_REL = 'trimPrefix "/" (trimPrefix .project_dir .current_dir)';
|
|
54
54
|
const DIR_TEMPLATE =
|
|
55
|
-
'
|
|
55
|
+
'{{ if and (ne .home "") (or (eq .home .current_dir) (hasPrefix (printf "%s/" .home) .current_dir)) }}~{{ trimPrefix .home .current_dir }}' +
|
|
56
56
|
"{{ else }}" +
|
|
57
57
|
'{{ if or (eq .project_dir .current_dir) (hasPrefix (printf "%s/" .project_dir) .current_dir) }}' +
|
|
58
58
|
`{{ ternary (${DIR_REL}) (basename .project_dir) (ne (${DIR_REL}) "") }}` +
|
|
59
|
-
"{{ else }}{{ .current_dir }}{{ end }}{{ end }}
|
|
59
|
+
"{{ else }}{{ .current_dir }}{{ end }}{{ end }}";
|
|
60
60
|
|
|
61
61
|
// Git working-tree counts — leading-space-then-trim idiom: each present count
|
|
62
62
|
// contributes " +N", trim drops the leading space, survivors single-spaced.
|
|
@@ -74,7 +74,7 @@ const GIT_STATUS =
|
|
|
74
74
|
'{{ if eq .git.status "dirty" }}●{{ else }}✓{{ end }}{{ end }}';
|
|
75
75
|
|
|
76
76
|
const GIT_TEMPLATE =
|
|
77
|
-
'
|
|
77
|
+
'{{ if ne .git.repoName "" }}{{ .git.repoName }} {{ end }}⎇ {{ .git.branch }}' +
|
|
78
78
|
"{{ if .git.sha }} ♯ {{ .git.sha }}{{ end }}" +
|
|
79
79
|
"{{ if or (gt .git.ahead 0) (gt .git.behind 0) }}" +
|
|
80
80
|
" {{ if gt .git.ahead 0 }}↑{{ .git.ahead }}{{ end }}" +
|
|
@@ -83,8 +83,7 @@ const GIT_TEMPLATE =
|
|
|
83
83
|
"{{ if .git.upstream }} →{{ .git.upstream }}{{ end }}" +
|
|
84
84
|
"{{ if gt .git.stash 0 }} ⧇ {{ .git.stash }}{{ end }}" +
|
|
85
85
|
" " +
|
|
86
|
-
GIT_STATUS
|
|
87
|
-
" ";
|
|
86
|
+
GIT_STATUS;
|
|
88
87
|
|
|
89
88
|
// [LAW:dataflow-not-control-flow] block and weekly share the same threshold
|
|
90
89
|
// cascade (≥warningThreshold → error, ≥50 → warning, else panel) on a numeric
|
|
@@ -509,6 +508,12 @@ export const DEFAULT_DSL_CONFIG = {
|
|
|
509
508
|
// palette spec names resolved against the active theme. `when` predicates
|
|
510
509
|
// hide a segment when its primary signal is absent (no git repo, no version
|
|
511
510
|
// field, no env var, no tmux, no rate-limit window).
|
|
511
|
+
//
|
|
512
|
+
// [LAW:one-source-of-truth] Templates author CONTENT only — the intra-cell
|
|
513
|
+
// padding (the space each side of a cell) is render chrome synthesized
|
|
514
|
+
// structurally from the one resolved globals.padding (default 1), never
|
|
515
|
+
// authored here. A template with leading/trailing spaces would render them
|
|
516
|
+
// IN ADDITION to the structural padding.
|
|
512
517
|
segments: {
|
|
513
518
|
directory: {
|
|
514
519
|
template: DIR_TEMPLATE,
|
|
@@ -516,25 +521,25 @@ export const DEFAULT_DSL_CONFIG = {
|
|
|
516
521
|
fg: "foreground",
|
|
517
522
|
},
|
|
518
523
|
model: {
|
|
519
|
-
template: "
|
|
524
|
+
template: "✱ {{ formatModelName .model.display_name }}",
|
|
520
525
|
bg: "panel",
|
|
521
526
|
fg: "foreground",
|
|
522
527
|
when: '{{ ne .model.display_name "" }}',
|
|
523
528
|
},
|
|
524
529
|
sessionId: {
|
|
525
|
-
template: "
|
|
530
|
+
template: "⌗{{ trunc 8 .session.id }}",
|
|
526
531
|
bg: "surface",
|
|
527
532
|
fg: "foreground",
|
|
528
533
|
when: '{{ ne .session.id "" }}',
|
|
529
534
|
},
|
|
530
535
|
version: {
|
|
531
|
-
template: "
|
|
536
|
+
template: "◈ v{{ .version }}",
|
|
532
537
|
bg: "surface",
|
|
533
538
|
fg: "foreground",
|
|
534
539
|
when: '{{ ne .version "" }}',
|
|
535
540
|
},
|
|
536
541
|
tmux: {
|
|
537
|
-
template: '
|
|
542
|
+
template: 'tmux:{{ .tmux.session | default "none" }}',
|
|
538
543
|
bg: "surface-active",
|
|
539
544
|
fg: "foreground",
|
|
540
545
|
when: '{{ ne .tmux.session "" }}',
|
|
@@ -547,7 +552,7 @@ export const DEFAULT_DSL_CONFIG = {
|
|
|
547
552
|
},
|
|
548
553
|
gitaculous: {
|
|
549
554
|
template:
|
|
550
|
-
"
|
|
555
|
+
"(git)" +
|
|
551
556
|
'{{ if ne .git.repoName "" }} {{ .git.repoName }}{{ end }}' +
|
|
552
557
|
'{{ if ne .git.operation "" }} [{{ .git.operation }}]{{ end }}' +
|
|
553
558
|
'{{ if ne .git.sha "" }} {{ .git.sha }}{{ end }}' +
|
|
@@ -564,8 +569,7 @@ export const DEFAULT_DSL_CONFIG = {
|
|
|
564
569
|
'{{ if gt .git.behind 0 }}{{ red (printf "-%v" .git.behind) }}{{ end }}' +
|
|
565
570
|
"{{ end }}]{{ end }}" +
|
|
566
571
|
"{{ if gt .git.stash 0 }} ({{ .git.stash }} stashed){{ end }}" +
|
|
567
|
-
'{{ if gt .git.timeSinceCommit 0 }} ◷ {{ template "formatTimeSince" .git.timeSinceCommit }}{{ end }}'
|
|
568
|
-
" ",
|
|
572
|
+
'{{ if gt .git.timeSinceCommit 0 }} ◷ {{ template "formatTimeSince" .git.timeSinceCommit }}{{ end }}',
|
|
569
573
|
bg: "surface-active",
|
|
570
574
|
fg: "foreground",
|
|
571
575
|
when: '{{ ne .git.branch "" }}',
|
|
@@ -580,10 +584,12 @@ export const DEFAULT_DSL_CONFIG = {
|
|
|
580
584
|
// renders a distinct ⚠ marker so an outage is not mistaken for "no PR";
|
|
581
585
|
// no PR (both empty) leaves the `when` gate false and the segment absent.
|
|
582
586
|
gitPr: {
|
|
587
|
+
// The pad spaces are structural chrome now, OUTSIDE the OSC-8 link
|
|
588
|
+
// region — the clickable area is the glyph text itself.
|
|
583
589
|
template:
|
|
584
590
|
'{{ if ne .git.prUrl "" }}' +
|
|
585
|
-
'{{ link .git.prUrl (printf "
|
|
586
|
-
"{{ else }}
|
|
591
|
+
'{{ link .git.prUrl (printf "⇆ #%v" .git.prNumber) }}' +
|
|
592
|
+
"{{ else }}⚠ PR{{ end }}",
|
|
587
593
|
bg: "surface-active",
|
|
588
594
|
fg: "foreground",
|
|
589
595
|
when: '{{ or (ne .git.prUrl "") (ne .git.prError "") }}',
|
|
@@ -596,28 +602,28 @@ export const DEFAULT_DSL_CONFIG = {
|
|
|
596
602
|
// one OSC-8 clickable region whose URL the wire codec owns end-to-end.
|
|
597
603
|
toolbar: {
|
|
598
604
|
template:
|
|
599
|
-
'
|
|
600
|
-
' {{ action "openProject" "↗ proj" }} {{ action "openTranscript" "↗ log" }}
|
|
605
|
+
'{{ action "copySession" "⎘ id" }} {{ action "copyDir" "⎘ cwd" }}' +
|
|
606
|
+
' {{ action "openProject" "↗ proj" }} {{ action "openTranscript" "↗ log" }}',
|
|
601
607
|
bg: "surface",
|
|
602
608
|
fg: "foreground",
|
|
603
609
|
},
|
|
604
610
|
session: {
|
|
605
611
|
template:
|
|
606
|
-
'
|
|
612
|
+
'§ {{ template "formatCost" .session.cost }} ({{ template "formatTokens" .session.tokens }})',
|
|
607
613
|
bg: "surface",
|
|
608
614
|
fg: "foreground",
|
|
609
615
|
},
|
|
610
616
|
today: {
|
|
611
617
|
template:
|
|
612
|
-
'
|
|
613
|
-
'{{ template "budgetStatus" (dict "cost" .today.cost "budget" .today.budget.amount "warn" .today.budget.warningThreshold) }}
|
|
618
|
+
'☉ {{ template "formatCost" .today.cost }} ({{ template "formatTokens" .today.tokens }})' +
|
|
619
|
+
'{{ template "budgetStatus" (dict "cost" .today.cost "budget" .today.budget.amount "warn" .today.budget.warningThreshold) }}',
|
|
614
620
|
bg: "surface",
|
|
615
621
|
fg: "foreground",
|
|
616
622
|
},
|
|
617
623
|
block: {
|
|
618
624
|
template:
|
|
619
|
-
"
|
|
620
|
-
'({{ template "formatLongTimeRemaining" (minutesUntilReset .block.resetsAt) }})
|
|
625
|
+
"◱ {{ round .block.nativeUtilization }}% " +
|
|
626
|
+
'({{ template "formatLongTimeRemaining" (minutesUntilReset .block.resetsAt) }})',
|
|
621
627
|
bg: blockLikeBg(
|
|
622
628
|
".block.nativeUtilization",
|
|
623
629
|
".block.budget.warningThreshold",
|
|
@@ -628,8 +634,8 @@ export const DEFAULT_DSL_CONFIG = {
|
|
|
628
634
|
},
|
|
629
635
|
weekly: {
|
|
630
636
|
template:
|
|
631
|
-
"
|
|
632
|
-
'({{ template "formatLongTimeRemaining" (minutesUntilReset .weekly.resetsAt) }})
|
|
637
|
+
"◑ {{ round .weekly.percentage }}% " +
|
|
638
|
+
'({{ template "formatLongTimeRemaining" (minutesUntilReset .weekly.resetsAt) }})',
|
|
633
639
|
bg: blockLikeBg(".weekly.percentage", ".weekly.budget.warningThreshold"),
|
|
634
640
|
fg: blockLikeFg(".weekly.percentage"),
|
|
635
641
|
when: "{{ gt .weekly.resetsAt 0 }}",
|
|
@@ -641,9 +647,9 @@ export const DEFAULT_DSL_CONFIG = {
|
|
|
641
647
|
// rate-limit window is active — the same signal block/weekly gate on.
|
|
642
648
|
burnrate: {
|
|
643
649
|
template:
|
|
644
|
-
'
|
|
650
|
+
'⚡ {{ template "formatRate" .burn.costPerHour }} · ' +
|
|
645
651
|
'{{ template "formatEta" .block.etaMinutes }} to 5h · ' +
|
|
646
|
-
'{{ template "formatEta" .weekly.etaMinutes }} to wk
|
|
652
|
+
'{{ template "formatEta" .weekly.etaMinutes }} to wk',
|
|
647
653
|
bg: etaHeatBg(
|
|
648
654
|
".block.etaMinutes",
|
|
649
655
|
".burn.eta.warnMinutes",
|
|
@@ -662,9 +668,9 @@ export const DEFAULT_DSL_CONFIG = {
|
|
|
662
668
|
// turn start, `total` is their sum.
|
|
663
669
|
speed: {
|
|
664
670
|
template:
|
|
665
|
-
'
|
|
671
|
+
'⇅ out {{ template "formatSpeed" .speed.output }} · ' +
|
|
666
672
|
'in {{ template "formatSpeed" .speed.input }} · ' +
|
|
667
|
-
'tot {{ template "formatSpeed" .speed.total }}
|
|
673
|
+
'tot {{ template "formatSpeed" .speed.total }}',
|
|
668
674
|
bg: "panel",
|
|
669
675
|
fg: "foreground",
|
|
670
676
|
when: "{{ gt .session.tokens 0 }}",
|
|
@@ -679,7 +685,7 @@ export const DEFAULT_DSL_CONFIG = {
|
|
|
679
685
|
// two samples before its first bar). [LAW:effects-at-boundaries] — all the
|
|
680
686
|
// history lives in the daemon ring, the template only draws.
|
|
681
687
|
tokenSparkline: {
|
|
682
|
-
template: "
|
|
688
|
+
template: "⚡ {{ sparkline .speed.history 24 }}",
|
|
683
689
|
bg: "panel",
|
|
684
690
|
fg: "foreground",
|
|
685
691
|
when: '{{ ne .speed.history "" }}',
|
|
@@ -693,8 +699,8 @@ export const DEFAULT_DSL_CONFIG = {
|
|
|
693
699
|
// (warm = normal, ≤20m = warning, ≤8m/cold = error).
|
|
694
700
|
cacheTimer: {
|
|
695
701
|
template:
|
|
696
|
-
"
|
|
697
|
-
"{{ else }}{{ minutesUntilReset .cache.expiresAt }}m{{ end }}
|
|
702
|
+
"◴ {{ if le (minutesUntilReset .cache.expiresAt) 0 }}cold" +
|
|
703
|
+
"{{ else }}{{ minutesUntilReset .cache.expiresAt }}m{{ end }}",
|
|
698
704
|
bg: "surface",
|
|
699
705
|
fg:
|
|
700
706
|
"{{ if le (minutesUntilReset .cache.expiresAt) 8 }}error" +
|
|
@@ -704,7 +710,7 @@ export const DEFAULT_DSL_CONFIG = {
|
|
|
704
710
|
},
|
|
705
711
|
context: {
|
|
706
712
|
template:
|
|
707
|
-
"
|
|
713
|
+
"◔ {{ formatInteger .context.totalTokens }} ({{ .context.contextLeft }}%)",
|
|
708
714
|
bg:
|
|
709
715
|
"{{ if le .context.contextLeft 20 }}error" +
|
|
710
716
|
"{{ else }}{{ if le .context.contextLeft 40 }}warning" +
|
|
@@ -724,6 +730,12 @@ export const DEFAULT_DSL_CONFIG = {
|
|
|
724
730
|
// level `when` survives as a weak any-present check so a payload
|
|
725
731
|
// with zero metrics data renders no cell at all (an empty template
|
|
726
732
|
// would otherwise produce a single-space bg-styled cell).
|
|
733
|
+
//
|
|
734
|
+
// [LAW:one-source-of-truth] exception: each arm's leading space is the
|
|
735
|
+
// SEPARATOR between present parts (only data can decide which part is
|
|
736
|
+
// first, so no static strip can remove just the first one), and the
|
|
737
|
+
// trailing space mirrors it for symmetry. At the default padding this
|
|
738
|
+
// cell therefore reads one space wider per side than its siblings.
|
|
727
739
|
template:
|
|
728
740
|
'{{ if .metrics.lastResponseTime }} Δ {{ template "formatResponseTime" .metrics.lastResponseTime }}{{ end }}' +
|
|
729
741
|
'{{ if .metrics.responseTime }} ⧖ {{ template "formatResponseTime" .metrics.responseTime }}{{ end }}' +
|
package/src/config/dsl-types.ts
CHANGED
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
// references it here. The dependency is one-way (this file → action.ts), never
|
|
15
15
|
// the reverse, so that shape can be lifted out without a cycle.
|
|
16
16
|
import type { ActionDecl } from "./action.js";
|
|
17
|
-
import type { StripStyle } from "../themes/policy.js";
|
|
17
|
+
import type { Charset, StripStyle } from "../themes/policy.js";
|
|
18
18
|
|
|
19
19
|
// [LAW:types-are-the-program] Three stages, three names.
|
|
20
20
|
//
|
|
@@ -196,6 +196,23 @@ export interface Globals {
|
|
|
196
196
|
// [config-only] Unlike palette/style there is no SessionState/click half —
|
|
197
197
|
// the daemon resolves `globals.autoWrap ?? true` into renderOpts.wrap.
|
|
198
198
|
readonly autoWrap?: boolean;
|
|
199
|
+
|
|
200
|
+
// The legacy display.padding knob: spaces synthesized INSIDE each segment
|
|
201
|
+
// cell per side (intra-cell, within the bg fill — not rich-js FlexStrip's
|
|
202
|
+
// inter-item gap). Default 1 (current behavior). Templates author content;
|
|
203
|
+
// this chrome is applied structurally at the cell-formation seam.
|
|
204
|
+
// [config-only] The daemon resolves `globals.padding ?? 1` into
|
|
205
|
+
// renderOpts.padding; no SessionState/click half.
|
|
206
|
+
readonly padding?: number;
|
|
207
|
+
|
|
208
|
+
// The legacy display.charset knob: which glyph vocabulary the strip joiners
|
|
209
|
+
// render with. Default "unicode" (current behavior — rich-js's powerline
|
|
210
|
+
// caps, U+E0Bx). "ascii" swaps the caps for single-column ASCII glyphs so
|
|
211
|
+
// terminals/fonts without powerline glyphs render cleanly instead of tofu.
|
|
212
|
+
// Orthogonal to `style`: style picks the joiner shape, charset the glyphs.
|
|
213
|
+
// [config-only] The daemon resolves `globals.charset ?? "unicode"` into
|
|
214
|
+
// renderOpts.charset; no SessionState/click half.
|
|
215
|
+
readonly charset?: Charset;
|
|
199
216
|
}
|
|
200
217
|
|
|
201
218
|
// [LAW:one-type-per-behavior] One discriminated union covers every source
|
|
@@ -4,10 +4,11 @@
|
|
|
4
4
|
// add a key to GLOBALS_SCHEMA and Globals; the engine does the rest.
|
|
5
5
|
|
|
6
6
|
import { type Globals } from "../dsl-types.js";
|
|
7
|
-
import { STRIP_STYLES } from "../../themes/policy.js";
|
|
7
|
+
import { CHARSETS, STRIP_STYLES } from "../../themes/policy.js";
|
|
8
8
|
import {
|
|
9
9
|
optionalBooleanSpec,
|
|
10
10
|
optionalEnumSpec,
|
|
11
|
+
optionalIntSpec,
|
|
11
12
|
optionalStringSpec,
|
|
12
13
|
paletteSpec,
|
|
13
14
|
record,
|
|
@@ -31,6 +32,15 @@ const GLOBALS_SCHEMA: RecordSchema<Globals> = {
|
|
|
31
32
|
// validates by membership and emits a JSON-Schema `enum`.
|
|
32
33
|
style: optionalEnumSpec(STRIP_STYLES),
|
|
33
34
|
autoWrap: optionalBooleanSpec(),
|
|
35
|
+
// Intra-cell spaces per side. Bounded above so a config value can never
|
|
36
|
+
// drive an unbounded `" ".repeat` allocation in the daemon
|
|
37
|
+
// [LAW:no-silent-failure] — an absurd value is a loud load error, not a
|
|
38
|
+
// silently-huge render.
|
|
39
|
+
padding: optionalIntSpec({ min: 0, max: 16 }),
|
|
40
|
+
// [LAW:types-are-the-program] Closed enum like `style`: the joiner glyph
|
|
41
|
+
// vocabularies pickJoiner can render — validates by membership, emits a
|
|
42
|
+
// JSON-Schema `enum` from the same CHARSETS literal.
|
|
43
|
+
charset: optionalEnumSpec(CHARSETS),
|
|
34
44
|
},
|
|
35
45
|
};
|
|
36
46
|
|
|
@@ -554,6 +554,34 @@ export function optionalBooleanSpec(): FieldSpec<boolean> {
|
|
|
554
554
|
};
|
|
555
555
|
}
|
|
556
556
|
|
|
557
|
+
// [LAW:dataflow-not-control-flow] An optional bounded-integer field: the bounds
|
|
558
|
+
// are DATA feeding both interpreters — `parse` checks them and interpolates them
|
|
559
|
+
// into the one message, `json` emits them as minimum/maximum — so the validator
|
|
560
|
+
// and the editor-facing schema cannot describe different ranges.
|
|
561
|
+
export function optionalIntSpec(bounds: {
|
|
562
|
+
readonly min: number;
|
|
563
|
+
readonly max: number;
|
|
564
|
+
}): FieldSpec<number> {
|
|
565
|
+
const { min, max } = bounds;
|
|
566
|
+
return {
|
|
567
|
+
required: false,
|
|
568
|
+
json: { type: "integer", minimum: min, maximum: max },
|
|
569
|
+
parse: (ctx, path, field, raw) => {
|
|
570
|
+
const v = raw[field];
|
|
571
|
+
if (v === undefined) return undefined;
|
|
572
|
+
if (typeof v !== "number" || !Number.isInteger(v) || v < min || v > max) {
|
|
573
|
+
ctx.issues.push({
|
|
574
|
+
path: `${path}.${field}`,
|
|
575
|
+
message: `${field} must be an integer between ${min} and ${max}, got ${describeValue(v)}`,
|
|
576
|
+
line: findKeyLine(ctx.source, [field]),
|
|
577
|
+
});
|
|
578
|
+
return undefined;
|
|
579
|
+
}
|
|
580
|
+
return v;
|
|
581
|
+
},
|
|
582
|
+
};
|
|
583
|
+
}
|
|
584
|
+
|
|
557
585
|
// [LAW:single-enforcer] The palette field defers to the one palette-name
|
|
558
586
|
// authority; the field key is conventionally "palette", which validatePaletteName
|
|
559
587
|
// reads directly.
|
package/src/daemon/server.ts
CHANGED
|
@@ -46,9 +46,12 @@ import {
|
|
|
46
46
|
} from "../themes/index.js";
|
|
47
47
|
import {
|
|
48
48
|
renderStripCells,
|
|
49
|
+
DEFAULT_CHARSET,
|
|
50
|
+
DEFAULT_PADDING,
|
|
49
51
|
DEFAULT_TERMINAL_WIDTH,
|
|
50
52
|
DEFAULT_WRAP,
|
|
51
53
|
type BuildLineOptions,
|
|
54
|
+
type Charset,
|
|
52
55
|
} from "../render/strip.js";
|
|
53
56
|
import { applyClaudeCodeReserve } from "../utils/terminal-width.js";
|
|
54
57
|
import type { RichText } from "@promptctl/rich-js";
|
|
@@ -773,6 +776,18 @@ async function handleRequest(req: Request): Promise<HandledRequest> {
|
|
|
773
776
|
// resolution. Width stays finite regardless: it also feeds the
|
|
774
777
|
// picker's pagination (term.cols), which no-wrap must not break.
|
|
775
778
|
renderOpts.wrap = entry.state.config.globals.autoWrap ?? DEFAULT_WRAP;
|
|
779
|
+
// [config-only] globals.padding, same shape as autoWrap: the config
|
|
780
|
+
// global over the base floor is the whole resolution — the ONE home
|
|
781
|
+
// of the value every cell builder (and the picker's pagination
|
|
782
|
+
// reserve) derives from. [LAW:one-source-of-truth]
|
|
783
|
+
renderOpts.padding =
|
|
784
|
+
entry.state.config.globals.padding ?? DEFAULT_PADDING;
|
|
785
|
+
// [config-only] globals.charset, same shape as autoWrap/padding: the
|
|
786
|
+
// config global over the base floor is the whole resolution — the ONE
|
|
787
|
+
// home of the glyph-vocabulary choice pickJoiner derives from.
|
|
788
|
+
// [LAW:one-source-of-truth]
|
|
789
|
+
renderOpts.charset =
|
|
790
|
+
entry.state.config.globals.charset ?? DEFAULT_CHARSET;
|
|
776
791
|
// [LAW:single-enforcer] renderDsl internally calls
|
|
777
792
|
// `registry.applyInput(payload)` as its first step (see step 1 in
|
|
778
793
|
// src/dsl/render.ts). The daemon must not pre-apply — doing so
|
|
@@ -877,7 +892,10 @@ async function handleRequest(req: Request): Promise<HandledRequest> {
|
|
|
877
892
|
compiled: dbgEntry.compiled,
|
|
878
893
|
lastRenderBySegment:
|
|
879
894
|
req.what === "segments"
|
|
880
|
-
? serializeSegmentCells(
|
|
895
|
+
? serializeSegmentCells(
|
|
896
|
+
dbgEntry.lastRenderCellsBySegment,
|
|
897
|
+
dbgEntry.config.globals.charset ?? DEFAULT_CHARSET,
|
|
898
|
+
)
|
|
881
899
|
: EMPTY_RENDER_MAP,
|
|
882
900
|
};
|
|
883
901
|
return stay({ ok: true, debug: buildDebugSnapshot(req.what, dbgState) });
|
|
@@ -1064,6 +1082,8 @@ const RENDER_OPTS_BASE = {
|
|
|
1064
1082
|
style: "powerline" as const,
|
|
1065
1083
|
colorCompatibility: "truecolor" as const,
|
|
1066
1084
|
wrap: DEFAULT_WRAP,
|
|
1085
|
+
padding: DEFAULT_PADDING,
|
|
1086
|
+
charset: DEFAULT_CHARSET,
|
|
1067
1087
|
};
|
|
1068
1088
|
const DEBUG_RENDER_OPTS: BuildLineOptions = {
|
|
1069
1089
|
...RENDER_OPTS_BASE,
|
|
@@ -1075,12 +1095,22 @@ const DEBUG_RENDER_OPTS: BuildLineOptions = {
|
|
|
1075
1095
|
// the DaemonDslState type requires the field.
|
|
1076
1096
|
const EMPTY_RENDER_MAP = new Map<string, string>();
|
|
1077
1097
|
|
|
1098
|
+
// [LAW:one-source-of-truth] The joiner glyph vocabulary is a serialization-time
|
|
1099
|
+
// choice, and charset is config-only (no SessionState half) — so the faithful
|
|
1100
|
+
// value is fully derivable from the sampled entry's config, unlike style, whose
|
|
1101
|
+
// live session-over-config resolution needs a session a debug request doesn't
|
|
1102
|
+
// carry. The caller threads the entry-resolved charset; this serializer never
|
|
1103
|
+
// re-defaults it.
|
|
1078
1104
|
function serializeSegmentCells(
|
|
1079
1105
|
cells: ReadonlyMap<string, readonly RichText[]>,
|
|
1106
|
+
charset: Charset,
|
|
1080
1107
|
): Map<string, string> {
|
|
1081
1108
|
const out = new Map<string, string>();
|
|
1082
1109
|
for (const [name, segCells] of cells) {
|
|
1083
|
-
out.set(
|
|
1110
|
+
out.set(
|
|
1111
|
+
name,
|
|
1112
|
+
renderStripCells(segCells, { ...DEBUG_RENDER_OPTS, charset }),
|
|
1113
|
+
);
|
|
1084
1114
|
}
|
|
1085
1115
|
return out;
|
|
1086
1116
|
}
|
package/src/demo/dsl.ts
CHANGED
|
@@ -31,7 +31,12 @@ import { SessionState } from "../daemon/session-state.js";
|
|
|
31
31
|
import { listResolvablePaletteNames } from "../themes/policy.js";
|
|
32
32
|
import { effectiveThemeName, resolverForThemeName } from "../themes/index.js";
|
|
33
33
|
import { registerDslConfig, renderDsl } from "../dsl/render.js";
|
|
34
|
-
import {
|
|
34
|
+
import {
|
|
35
|
+
DEFAULT_CHARSET,
|
|
36
|
+
DEFAULT_PADDING,
|
|
37
|
+
DEFAULT_TERMINAL_WIDTH,
|
|
38
|
+
DEFAULT_WRAP,
|
|
39
|
+
} from "../render/strip.js";
|
|
35
40
|
import { applyClaudeCodeReserve } from "../utils/terminal-width.js";
|
|
36
41
|
|
|
37
42
|
const FRAMES = 4;
|
|
@@ -113,6 +118,8 @@ try {
|
|
|
113
118
|
// Same resolution the daemon applies: the config global over the
|
|
114
119
|
// default-on floor.
|
|
115
120
|
wrap: config.globals.autoWrap ?? DEFAULT_WRAP,
|
|
121
|
+
padding: config.globals.padding ?? DEFAULT_PADDING,
|
|
122
|
+
charset: config.globals.charset ?? DEFAULT_CHARSET,
|
|
116
123
|
},
|
|
117
124
|
);
|
|
118
125
|
process.stdout.write(` ${line}\n`);
|
package/src/dsl/node-registry.ts
CHANGED
|
@@ -101,6 +101,10 @@ export interface NodeRenderCtx {
|
|
|
101
101
|
readonly scope: object;
|
|
102
102
|
readonly basePalette: PaletteResolver;
|
|
103
103
|
readonly visible: boolean;
|
|
104
|
+
// [LAW:one-source-of-truth] The render-wide intra-cell padding (resolved
|
|
105
|
+
// globals.padding), threaded by the driver from BuildLineOptions into every
|
|
106
|
+
// segment's layout — one value per render, never re-defaulted per node.
|
|
107
|
+
readonly padding: number;
|
|
104
108
|
// Advance the walk-owned hue cursor by one unit and return that unit's shift.
|
|
105
109
|
nextHueShift(): number;
|
|
106
110
|
readonly perSegmentSink?: Map<string, readonly RichText[]>;
|
|
@@ -279,6 +283,11 @@ const segmentType: NodeType<"segment"> = {
|
|
|
279
283
|
justify: seg.justify ?? "left",
|
|
280
284
|
truncate: seg.truncate ?? "right",
|
|
281
285
|
baseStyle,
|
|
286
|
+
// [LAW:dataflow-not-control-flow] Padding is uniform across every line
|
|
287
|
+
// a segment contributes — inline rows AND dropped menu bands — one
|
|
288
|
+
// value, no per-line-kind branch. The picker reserves 2×padding at its
|
|
289
|
+
// pagination seam so a padded band still fits the width budget.
|
|
290
|
+
padding: ctx.padding,
|
|
282
291
|
} as const;
|
|
283
292
|
|
|
284
293
|
// [LAW:single-enforcer] Partition the segment's authored "\n" into visual
|
package/src/dsl/render.ts
CHANGED
|
@@ -29,7 +29,7 @@ import {
|
|
|
29
29
|
type GitField,
|
|
30
30
|
} from "../var-system/sources.js";
|
|
31
31
|
import type { BuildLineOptions } from "../render/strip.js";
|
|
32
|
-
import { renderStripCells } from "../render/strip.js";
|
|
32
|
+
import { DEFAULT_PADDING, renderStripCells } from "../render/strip.js";
|
|
33
33
|
import { resolverForThemeName } from "../themes/index.js";
|
|
34
34
|
import { buildScope } from "../template-engine/scope.js";
|
|
35
35
|
import {
|
|
@@ -286,6 +286,9 @@ export function registerDslConfig(
|
|
|
286
286
|
// style each render; "powerline" is the registration-time default so a
|
|
287
287
|
// compile-only path (no render) still has a valid value.
|
|
288
288
|
stripStyle: "powerline",
|
|
289
|
+
// Same contract as stripStyle: renderDsl republishes the live resolved
|
|
290
|
+
// globals.padding each render; the constant is only the compile-only floor.
|
|
291
|
+
padding: DEFAULT_PADDING,
|
|
289
292
|
};
|
|
290
293
|
// [LAW:one-way-deps] Inject action + picker feature funcs as data — the engine
|
|
291
294
|
// stays generic. The picker shares the ACTION runtime (it resolves its
|
|
@@ -537,6 +540,10 @@ export function renderDsl(
|
|
|
537
540
|
// once per render here — the same one-owner, per-render-mutation idiom as the
|
|
538
541
|
// menu placement cursor below. [LAW:no-ambient-temporal-coupling]
|
|
539
542
|
compiled.menuRuntime.action.stripStyle = opts.style;
|
|
543
|
+
// [LAW:one-source-of-truth] Publish the render's intra-cell padding beside the
|
|
544
|
+
// style: the picker reserves 2×padding at its pagination seam, the same seam
|
|
545
|
+
// that reserves the joiner chrome — one resolved value, read where needed.
|
|
546
|
+
compiled.menuRuntime.action.padding = opts.padding;
|
|
540
547
|
|
|
541
548
|
const scope = buildScope(store);
|
|
542
549
|
// [LAW:one-source-of-truth] hueStep is a value in the store like every other
|
|
@@ -610,6 +617,7 @@ export function renderDsl(
|
|
|
610
617
|
scope,
|
|
611
618
|
basePalette,
|
|
612
619
|
visible,
|
|
620
|
+
padding: opts.padding,
|
|
613
621
|
nextHueShift,
|
|
614
622
|
perSegmentSink,
|
|
615
623
|
beginSegment,
|
package/src/render/action.ts
CHANGED
|
@@ -134,6 +134,12 @@ export interface ActionRuntime {
|
|
|
134
134
|
// and synchronous, so the per-render write never leaks across renders.
|
|
135
135
|
// [LAW:no-ambient-temporal-coupling]
|
|
136
136
|
stripStyle: StripStyle;
|
|
137
|
+
// [LAW:locality-or-seam] The current render's intra-cell padding (resolved
|
|
138
|
+
// globals.padding), published per render by renderDsl exactly like
|
|
139
|
+
// stripStyle. The picker reserves 2×padding at its pagination seam — the
|
|
140
|
+
// segment layout pads every line it emits, so a page packed to the full
|
|
141
|
+
// budget would otherwise be pushed past the width by the pad spaces.
|
|
142
|
+
padding: number;
|
|
137
143
|
}
|
|
138
144
|
|
|
139
145
|
// ─── Compilation ───────────────────────────────────────────────────────────────
|
package/src/render/picker.ts
CHANGED
|
@@ -168,11 +168,15 @@ export function renderPicker(
|
|
|
168
168
|
// pagination seam, rather than shrinking the shared term.cols every template
|
|
169
169
|
// reads. stripChromeCols owns the per-style geometry; Infinity − chrome stays
|
|
170
170
|
// Infinity, so wrap mode is unaffected.
|
|
171
|
+
// The pad spaces the segment layout synthesizes around the picker's line
|
|
172
|
+
// (2 × the render's intra-cell padding) consume the same width budget the
|
|
173
|
+
// joiner chrome does — reserve both here, at the pagination seam.
|
|
171
174
|
const available = paged
|
|
172
175
|
? Math.max(
|
|
173
176
|
1,
|
|
174
177
|
toNumber(store.read(TERM_COLS_VAR)) -
|
|
175
|
-
stripChromeCols(runtime.stripStyle)
|
|
178
|
+
stripChromeCols(runtime.stripStyle) -
|
|
179
|
+
2 * runtime.padding,
|
|
176
180
|
)
|
|
177
181
|
: Infinity;
|
|
178
182
|
const closeReserve = cellWidth(PICKER_CLOSE) + 1;
|