@promptctl/cc-candybar 1.11.0 → 1.12.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.11.0",
3
+ "version": "1.12.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.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"
94
+ "@promptctl/cc-candybar-darwin-arm64": "1.12.0",
95
+ "@promptctl/cc-candybar-darwin-x64": "1.12.0",
96
+ "@promptctl/cc-candybar-linux-x64": "1.12.0",
97
+ "@promptctl/cc-candybar-linux-arm64": "1.12.0"
98
98
  },
99
99
  "pnpm": {
100
100
  "supportedArchitectures": {
@@ -46,6 +46,14 @@
46
46
  "unicode",
47
47
  "ascii"
48
48
  ]
49
+ },
50
+ "colorCompatibility": {
51
+ "enum": [
52
+ "truecolor",
53
+ "256",
54
+ "ansi",
55
+ "none"
56
+ ]
49
57
  }
50
58
  },
51
59
  "additionalProperties": false
@@ -14,7 +14,11 @@
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 { Charset, StripStyle } from "../themes/policy.js";
17
+ import type {
18
+ Charset,
19
+ ColorCompatibility,
20
+ StripStyle,
21
+ } from "../themes/policy.js";
18
22
 
19
23
  // [LAW:types-are-the-program] Three stages, three names.
20
24
  //
@@ -213,6 +217,15 @@ export interface Globals {
213
217
  // [config-only] The daemon resolves `globals.charset ?? "unicode"` into
214
218
  // renderOpts.charset; no SessionState/click half.
215
219
  readonly charset?: Charset;
220
+
221
+ // The legacy display.colorCompatibility knob: the color depth rich-js
222
+ // downsamples output to. Default "truecolor" (current behavior — NOT the
223
+ // legacy "auto" default, which would change rendering for existing users).
224
+ // The type excludes "auto" entirely: the daemon is detached, so env
225
+ // detection would read the wrong terminal — see COLOR_COMPATIBILITIES.
226
+ // [config-only] The daemon resolves `globals.colorCompatibility ??
227
+ // "truecolor"` into renderOpts.colorCompatibility; no SessionState/click half.
228
+ readonly colorCompatibility?: ColorCompatibility;
216
229
  }
217
230
 
218
231
  // [LAW:one-type-per-behavior] One discriminated union covers every source
@@ -4,19 +4,54 @@
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 { CHARSETS, STRIP_STYLES } from "../../themes/policy.js";
7
+ import {
8
+ CHARSETS,
9
+ COLOR_COMPATIBILITIES,
10
+ STRIP_STYLES,
11
+ type ColorCompatibility,
12
+ } from "../../themes/policy.js";
8
13
  import {
9
14
  optionalBooleanSpec,
15
+ optionalEnum,
10
16
  optionalEnumSpec,
11
17
  optionalIntSpec,
12
18
  optionalStringSpec,
13
19
  paletteSpec,
14
20
  record,
15
21
  recordJson,
22
+ type FieldSpec,
16
23
  type JsonNode,
17
24
  type RecordSchema,
18
25
  type ValidateCtx,
19
26
  } from "./validate-core.js";
27
+ import { findKeyLine } from "./diagnostics.js";
28
+
29
+ // [LAW:types-are-the-program] Closed enum like `charset`, plus one
30
+ // migration-pointing rejection: "auto" was the LEGACY default, so migrating
31
+ // configs will carry it — but the daemon runs detached, so rich-js env
32
+ // detection would downsample against the daemon's terminal, not the client's.
33
+ // Rather than ship that silent lie [LAW:no-silent-failure], "auto" is outside
34
+ // the ColorCompatibility domain and gets an error that says why (same species
35
+ // as the removed-layout migration errors in layout.ts). The json emit and the
36
+ // membership check derive from the same COLOR_COMPATIBILITIES literal.
37
+ const colorCompatibilitySpec: FieldSpec<ColorCompatibility> = {
38
+ required: false,
39
+ json: { enum: [...COLOR_COMPATIBILITIES] },
40
+ parse: (ctx, path, field, raw) => {
41
+ if (raw[field] === "auto") {
42
+ ctx.issues.push({
43
+ path: `${path}.${field}`,
44
+ message:
45
+ `${path}.${field}: "auto" is not supported — the render daemon runs detached, ` +
46
+ `so terminal detection would read the daemon's environment, not your terminal's. ` +
47
+ `Pick an explicit depth: ${COLOR_COMPATIBILITIES.join(", ")}`,
48
+ line: findKeyLine(ctx.source, [...path.split("."), field]),
49
+ });
50
+ return undefined;
51
+ }
52
+ return optionalEnum(ctx, path, raw, field, COLOR_COMPATIBILITIES);
53
+ },
54
+ };
20
55
 
21
56
  const GLOBALS_SCHEMA: RecordSchema<Globals> = {
22
57
  noun: "globals key",
@@ -41,6 +76,8 @@ const GLOBALS_SCHEMA: RecordSchema<Globals> = {
41
76
  // vocabularies pickJoiner can render — validates by membership, emits a
42
77
  // JSON-Schema `enum` from the same CHARSETS literal.
43
78
  charset: optionalEnumSpec(CHARSETS),
79
+ // Closed enum with a bespoke "auto" rejection — see colorCompatibilitySpec.
80
+ colorCompatibility: colorCompatibilitySpec,
44
81
  },
45
82
  };
46
83
 
@@ -47,11 +47,13 @@ import {
47
47
  import {
48
48
  renderStripCells,
49
49
  DEFAULT_CHARSET,
50
+ DEFAULT_COLOR_COMPATIBILITY,
50
51
  DEFAULT_PADDING,
51
52
  DEFAULT_TERMINAL_WIDTH,
52
53
  DEFAULT_WRAP,
53
54
  type BuildLineOptions,
54
55
  type Charset,
56
+ type ColorCompatibility,
55
57
  } from "../render/strip.js";
56
58
  import { applyClaudeCodeReserve } from "../utils/terminal-width.js";
57
59
  import type { RichText } from "@promptctl/rich-js";
@@ -788,6 +790,13 @@ async function handleRequest(req: Request): Promise<HandledRequest> {
788
790
  // [LAW:one-source-of-truth]
789
791
  renderOpts.charset =
790
792
  entry.state.config.globals.charset ?? DEFAULT_CHARSET;
793
+ // [config-only] globals.colorCompatibility, same shape as charset:
794
+ // the config global over the base floor is the whole resolution —
795
+ // the ONE home of the color-depth choice rich-js downsamples to.
796
+ // [LAW:one-source-of-truth]
797
+ renderOpts.colorCompatibility =
798
+ entry.state.config.globals.colorCompatibility ??
799
+ DEFAULT_COLOR_COMPATIBILITY;
791
800
  // [LAW:single-enforcer] renderDsl internally calls
792
801
  // `registry.applyInput(payload)` as its first step (see step 1 in
793
802
  // src/dsl/render.ts). The daemon must not pre-apply — doing so
@@ -895,6 +904,8 @@ async function handleRequest(req: Request): Promise<HandledRequest> {
895
904
  ? serializeSegmentCells(
896
905
  dbgEntry.lastRenderCellsBySegment,
897
906
  dbgEntry.config.globals.charset ?? DEFAULT_CHARSET,
907
+ dbgEntry.config.globals.colorCompatibility ??
908
+ DEFAULT_COLOR_COMPATIBILITY,
898
909
  )
899
910
  : EMPTY_RENDER_MAP,
900
911
  };
@@ -1080,7 +1091,7 @@ const verbCtx = { sessionState, dlog };
1080
1091
  // is rendered standalone (wrap doesn't apply to a one-segment projection).
1081
1092
  const RENDER_OPTS_BASE = {
1082
1093
  style: "powerline" as const,
1083
- colorCompatibility: "truecolor" as const,
1094
+ colorCompatibility: DEFAULT_COLOR_COMPATIBILITY,
1084
1095
  wrap: DEFAULT_WRAP,
1085
1096
  padding: DEFAULT_PADDING,
1086
1097
  charset: DEFAULT_CHARSET,
@@ -1095,21 +1106,26 @@ const DEBUG_RENDER_OPTS: BuildLineOptions = {
1095
1106
  // the DaemonDslState type requires the field.
1096
1107
  const EMPTY_RENDER_MAP = new Map<string, string>();
1097
1108
 
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.
1109
+ // [LAW:one-source-of-truth] The joiner glyph vocabulary and the color depth
1110
+ // are serialization-time choices, and both are config-only (no SessionState
1111
+ // half) so the faithful values are fully derivable from the sampled entry's
1112
+ // config, unlike style, whose live session-over-config resolution needs a
1113
+ // session a debug request doesn't carry. The caller threads the
1114
+ // entry-resolved values; this serializer never re-defaults them.
1104
1115
  function serializeSegmentCells(
1105
1116
  cells: ReadonlyMap<string, readonly RichText[]>,
1106
1117
  charset: Charset,
1118
+ colorCompatibility: ColorCompatibility,
1107
1119
  ): Map<string, string> {
1108
1120
  const out = new Map<string, string>();
1109
1121
  for (const [name, segCells] of cells) {
1110
1122
  out.set(
1111
1123
  name,
1112
- renderStripCells(segCells, { ...DEBUG_RENDER_OPTS, charset }),
1124
+ renderStripCells(segCells, {
1125
+ ...DEBUG_RENDER_OPTS,
1126
+ charset,
1127
+ colorCompatibility,
1128
+ }),
1113
1129
  );
1114
1130
  }
1115
1131
  return out;
package/src/demo/dsl.ts CHANGED
@@ -33,6 +33,7 @@ import { effectiveThemeName, resolverForThemeName } from "../themes/index.js";
33
33
  import { registerDslConfig, renderDsl } from "../dsl/render.js";
34
34
  import {
35
35
  DEFAULT_CHARSET,
36
+ DEFAULT_COLOR_COMPATIBILITY,
36
37
  DEFAULT_PADDING,
37
38
  DEFAULT_TERMINAL_WIDTH,
38
39
  DEFAULT_WRAP,
@@ -108,7 +109,10 @@ try {
108
109
  basePalette,
109
110
  {
110
111
  style: "powerline",
111
- colorCompatibility: "truecolor",
112
+ // Same resolution the daemon applies: the config global over the
113
+ // truecolor default floor.
114
+ colorCompatibility:
115
+ config.globals.colorCompatibility ?? DEFAULT_COLOR_COMPATIBILITY,
112
116
  // [LAW:one-source-of-truth] Demo applies the same Claude-Code-UI
113
117
  // reserve the daemon does so demo output matches the bytes a real
114
118
  // statusline would emit at the same terminal width.
@@ -8,11 +8,14 @@ import {
8
8
  FlexStrip,
9
9
  renderToString,
10
10
  type Joiner,
11
- type ColorSystemSpec,
12
11
  type PowerlineJoinerOptions,
13
12
  type CapsuleJoinerOptions,
14
13
  } from "@promptctl/rich-js";
15
- import type { Charset, StripStyle } from "../themes/policy.js";
14
+ import type {
15
+ Charset,
16
+ ColorCompatibility,
17
+ StripStyle,
18
+ } from "../themes/policy.js";
16
19
 
17
20
  export interface RenderedSegmentLike {
18
21
  type: string;
@@ -25,7 +28,7 @@ export interface RenderedSegmentLike {
25
28
  // in themes/policy.ts (the render-identifier policy module, importable by the
26
29
  // option-source machinery without a render→template-engine cycle). Re-exported
27
30
  // here so render-layer consumers can keep importing them from the strip module.
28
- export type { Charset, StripStyle };
31
+ export type { Charset, ColorCompatibility, StripStyle };
29
32
 
30
33
  // [LAW:one-source-of-truth] Raw terminal cols we assume when the wire
31
34
  // didn't give us one (older client, env-stripped spawn). RAW — not
@@ -51,9 +54,21 @@ export const DEFAULT_PADDING = 1;
51
54
  // (`globals.charset ?? DEFAULT_CHARSET`) derives from this constant.
52
55
  export const DEFAULT_CHARSET: Charset = "unicode";
53
56
 
57
+ // [LAW:one-source-of-truth] The one statement of the globals.colorCompatibility
58
+ // default (truecolor — the CURRENT pinned value, deliberately NOT the legacy
59
+ // "auto" default, which would change rendering for existing users). Every
60
+ // resolver of the config global (`globals.colorCompatibility ??
61
+ // DEFAULT_COLOR_COMPATIBILITY`) derives from this constant.
62
+ export const DEFAULT_COLOR_COMPATIBILITY: ColorCompatibility = "truecolor";
63
+
54
64
  export interface BuildLineOptions {
55
65
  style: StripStyle;
56
- colorCompatibility: ColorSystemSpec;
66
+ // [LAW:types-are-the-program] Narrower than rich-js ColorSystemSpec on
67
+ // purpose: the four explicit depths only. "auto"/null never reach a render —
68
+ // the daemon is detached, so env detection would read the wrong terminal;
69
+ // the loader rejects "auto" at the trust boundary (see COLOR_COMPATIBILITIES
70
+ // in themes/policy.ts), and every construction site states a resolved depth.
71
+ colorCompatibility: ColorCompatibility;
57
72
  separator?: string;
58
73
  // [LAW:types-are-the-program] Every render carries a width. Required (not
59
74
  // optional) so callers cannot silently drop the wire's value.
@@ -5,7 +5,7 @@
5
5
  // PaletteResolver, no ColorRgba, no hex. The semantic/anchor knowledge
6
6
  // (which tokens keep their hue) stays in rich-js (ANCHORED_ROOTS), not here.
7
7
 
8
- import { listThemePalettes } from "@promptctl/rich-js";
8
+ import { listThemePalettes, type ColorSystemSpec } from "@promptctl/rich-js";
9
9
 
10
10
  // --- Theme name aliasing ---
11
11
 
@@ -96,3 +96,28 @@ export function effectiveStripStyle(
96
96
  // is the whole resolution.
97
97
  export const CHARSETS = ["unicode", "ascii"] as const;
98
98
  export type Charset = (typeof CHARSETS)[number];
99
+
100
+ // --- Color-depth identifiers ---
101
+
102
+ // [LAW:one-source-of-truth][LAW:types-are-the-program] The single canonical set
103
+ // of color depths a config can pin (the legacy display.colorCompatibility).
104
+ // Same species as CHARSETS: a closed render-vocabulary enum hosted in this leaf
105
+ // policy module so the config loader (validation + JSON-schema emit) and the
106
+ // render layer both derive from one literal without a config↔render cycle
107
+ // [LAW:one-way-deps]. `satisfies` ties every member to rich-js's
108
+ // ColorSystemSpec at compile time WITHOUT widening the derived union — if
109
+ // rich-js renames a depth, this literal fails to compile rather than drifting.
110
+ //
111
+ // Deliberately NARROWER than ColorSystemSpec: "auto" (and null) are excluded.
112
+ // The daemon is long-lived and detached, so its process env is NOT the client
113
+ // terminal's — rich-js env detection would silently downsample against the
114
+ // wrong terminal [LAW:no-silent-failure]. Honoring "auto" needs a client
115
+ // capability hint over the wire (the termCols pattern); until that lands, the
116
+ // loader rejects "auto" with a pointer instead of shipping a lie.
117
+ export const COLOR_COMPATIBILITIES = [
118
+ "truecolor",
119
+ "256",
120
+ "ansi",
121
+ "none",
122
+ ] as const satisfies readonly ColorSystemSpec[];
123
+ export type ColorCompatibility = (typeof COLOR_COMPATIBILITIES)[number];