@promptctl/cc-candybar 1.35.0 → 1.37.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.
@@ -0,0 +1,373 @@
1
+ // [LAW:one-source-of-truth] candybar-settings-ui-aok.1 — THE global settings
2
+ // menu: one disclosure that every rendered bar carries, whatever the config
3
+ // says. It exists because `root` replaces wholesale: a user who writes `root:`
4
+ // (the ordinary reason to write a config at all) silently deletes every
5
+ // interactive surface the bundled default placed there — presets, edit mode,
6
+ // the value controls. A door the user can delete by accident is not a door.
7
+ //
8
+ // [LAW:dataflow-not-control-flow] Placement is a POSITION, never a mode. The
9
+ // synthesis runs the same two total functions on every preset root, in the same
10
+ // order, every load: `withAnchor` yields a tree that CONTAINS the anchor — the
11
+ // author's own placement untouched, or the default position appended — and
12
+ // `expandAnchor` replaces that one leaf with the lowered disclosure subtree.
13
+ // "The author placed it" and "the author did not" differ only in the VALUE
14
+ // handed to one splice; there is no second code path to keep in agreement.
15
+ //
16
+ // [LAW:one-type-per-behavior] Nothing here is a new render or interaction
17
+ // concept. The menu is the disclosure primitive's fourth instance, alongside
18
+ // group sugar, `{{ menu }}`, and edit mode's toggle: it calls the SAME
19
+ // `disclosureStateVar`/`disclosureCycleAction`/`menuStateKey` functions those
20
+ // three call, so a synthesized global menu and a hand-authored group are
21
+ // indistinguishable to the render walk.
22
+ //
23
+ // WHY THIS RUNS FROM validateConfig, BEFORE synthesizeEditChrome — the two
24
+ // passes both rewrite every preset root, so their order is a real decision:
25
+ // • It cannot run at parse time (loader/*.ts) like group/menu synthesis,
26
+ // because the tree it must splice into only exists after merge: the user's
27
+ // `root` replaces the bundled default's, and it is the MERGED root the menu
28
+ // has to be present in.
29
+ // • It runs BEFORE edit chrome so edit chrome walks the final content tree.
30
+ // Every name minted here lives under the reserved `settings.` namespace,
31
+ // which `isChromeExempt` excludes, so the menu never acquires a `+`/`-`
32
+ // affordance and can never be edited out of the bar it is the entry point
33
+ // to. Running after would splice the menu into an already-chromed tree,
34
+ // landing it between a segment and the `-` that removes it.
35
+ // • It also GUARANTEES `edit.toggle` (see ensureEditToggle below), which is
36
+ // precisely what edit chrome's own demand gate reads — so the ordering is
37
+ // load-bearing in that direction too, not merely tidy.
38
+
39
+ import type { ActionDecl } from "./action.js";
40
+ import type {
41
+ DslConfig,
42
+ LayoutNode,
43
+ PresetDecl,
44
+ SegmentDecl,
45
+ VariableDecl,
46
+ } from "./dsl-types.js";
47
+ import {
48
+ DISCLOSURE_CLOSED,
49
+ DISCLOSURE_GLYPH_CLOSED,
50
+ DISCLOSURE_GLYPH_OPEN,
51
+ disclosureCycleAction,
52
+ disclosureStateVar,
53
+ } from "./disclosure.js";
54
+ import {
55
+ EDIT_MODE_KEY,
56
+ EDIT_MODE_OPEN,
57
+ EDIT_TOGGLE_ACTION,
58
+ } from "./loader/edit-mode.js";
59
+ import {
60
+ menuActionName,
61
+ menuMember,
62
+ menuPageKey,
63
+ menuStateKey,
64
+ } from "./menu-keys.js";
65
+ import { presetByName, presetNames, presetRoot } from "./presets.js";
66
+
67
+ // [LAW:one-source-of-truth] The reserved namespace every artifact this pass
68
+ // mints lives under, mirroring `groups.`/`menus.`/`edit.`. Reserved at parse
69
+ // time (reservedNamespaceCollisions, from dsl-loader's validateTopLevel) so a
70
+ // user name under it is a loud load error rather than a silent shadowing of
71
+ // the one surface they cannot afford to lose.
72
+ export const SETTINGS_NS = "settings.";
73
+
74
+ // [LAW:one-source-of-truth] THE anchor: one string that is simultaneously the
75
+ // segment name an author places in `root` to choose the menu's position, the
76
+ // name of the toggle segment the synthesis puts there, the disclosure's state
77
+ // variable, and its cycle action. Group sugar already spans those four with one
78
+ // `groups.<name>` string for the same reason — one name means the toggle's
79
+ // click and the body's `when` cannot address different keys.
80
+ export const SETTINGS_ANCHOR = `${SETTINGS_NS}menu`;
81
+
82
+ // The disclosure's open member. Same spelling edit mode uses for its own binary
83
+ // toggle — a binary disclosure holds the CLOSED sentinel or this.
84
+ const SETTINGS_OPEN = EDIT_MODE_OPEN;
85
+
86
+ // The body's two content segments and the preset picker's apply action. `.1`
87
+ // scopes the body to what its acceptance names — switch presets, enter edit
88
+ // mode. The remaining display controls arrive with the config menu (`.3`),
89
+ // which is the child that owns them.
90
+ const PRESETS_SEG = `${SETTINGS_NS}presets`;
91
+ const EDIT_SEG = `${SETTINGS_NS}edit`;
92
+ const APPLY_PRESET_ACTION = `${SETTINGS_NS}applyPreset`;
93
+
94
+ // [LAW:one-source-of-truth] The predicate the body container gates on, derived
95
+ // from the same anchor string the toggle's cycle writes — spelled once here,
96
+ // exactly as lowerGroup derives a group body's `when` from the group's own
97
+ // reference name.
98
+ const SETTINGS_OPEN_GATE = `{{ eq .${SETTINGS_ANCHOR} "${SETTINGS_OPEN}" }}`;
99
+
100
+ // [LAW:single-enforcer] The one answer to "is this segment reference the global
101
+ // menu's anchor". cross-ref.ts asks it to accept an authored placement of a name
102
+ // no config declares (this pass provides it, unconditionally, immediately after
103
+ // cross-ref passes), and to reject a SECOND placement — one key holds one open
104
+ // state, so two anchors would be two toggles writing one disclosure.
105
+ export function isSettingsAnchor(segmentName: string): boolean {
106
+ return segmentName === SETTINGS_ANCHOR;
107
+ }
108
+
109
+ // ─── The anchored-root stamp ────────────────────────────────────────────────
110
+
111
+ declare const anchored: unique symbol;
112
+
113
+ // [LAW:parse-dont-validate] A tree that is KNOWN to contain the anchor. The
114
+ // stamp is the proof, so `expandAnchor` has no "anchor missing" arm to guard
115
+ // and no answer-shaped void to return: the only way to obtain this type is to
116
+ // go through `withAnchor`, which establishes the fact by construction.
117
+ //
118
+ // The theorem includes the anchor inheriting no gate the DEFAULT placement
119
+ // descended into — a weaker stamp ("contains an anchor" alone) is what let a
120
+ // `when`-gated first row silently swallow the menu. Two gates are exempt
121
+ // because they are explicit authorial statements rather than accidents: the
122
+ // author's own placement of the anchor (they chose that position, gate and
123
+ // all) and a `when` on the root itself (there is no bar at all under that
124
+ // condition, so there is nothing to host a menu on).
125
+ type AnchoredRoot = LayoutNode & { readonly [anchored]: true };
126
+
127
+ // [LAW:dataflow-not-control-flow] The default position, as structural recursion
128
+ // over the LayoutNode union rather than a placement mode: descend to the bar's
129
+ // FIRST horizontal row and append there — where the bundled default's own
130
+ // settings affordance already sits, and the place a one-row user config puts
131
+ // everything. Total over every tree shape, including the degenerate ones: a
132
+ // bare-segment root (the A-grammar collapses a lone top-level ref) grows a
133
+ // horizontal wrapper, and an empty container simply becomes the row.
134
+ function appendAnchor(node: LayoutNode): LayoutNode {
135
+ const anchorRef: LayoutNode = { kind: "segment", name: SETTINGS_ANCHOR };
136
+ if (node.kind === "segment") {
137
+ // [LAW:no-silent-failure] A bare-segment root may carry its OWN `when` — an
138
+ // author gating their whole bar behind a condition. This wrapper is a brand
139
+ // new node, so without carrying that gate up, everything spliced beside the
140
+ // segment (this menu, and the reset banner edit chrome later prepends by
141
+ // reading `splicedRoot.when`) would render past a gate the author wrote.
142
+ // The identical carry-up spliceEditChromeForPreset performs, one pass over.
143
+ return {
144
+ kind: "container",
145
+ direction: "horizontal",
146
+ children: [node, anchorRef],
147
+ ...(node.when !== undefined && { when: node.when }),
148
+ };
149
+ }
150
+ const [first, ...rest] = node.children;
151
+ // [LAW:no-silent-failure] Descend only into an UNGATED child. A gate on an
152
+ // inner row is a statement about that row's content, not about the bar — an
153
+ // author writing an ordinary conditional first row (a git row shown only
154
+ // inside a repo) has no idea the default placement attaches the menu there,
155
+ // and inheriting that gate would silently delete the one surface this pass
156
+ // exists to make undeletable, under exactly their condition. When the first
157
+ // row is gated the anchor becomes its own ungated row on this container
158
+ // instead, which is a position the author can still override by placing the
159
+ // anchor themselves.
160
+ //
161
+ // The ROOT's own `when` is deliberately NOT lifted out of, here or in the
162
+ // segment arm above: gating the whole tree is an explicit statement that
163
+ // there is no bar under this condition, and there is no bar to host a menu
164
+ // on. That is the same "the author's explicit choice is the answer" rule
165
+ // that honors an author-placed anchor inside a gated row — and it is what
166
+ // keeps edit chrome's reset banner gated with the content it describes.
167
+ if (
168
+ node.direction === "vertical" &&
169
+ first !== undefined &&
170
+ first.when === undefined
171
+ ) {
172
+ return { ...node, children: [appendAnchor(first), ...rest] };
173
+ }
174
+ return { ...node, children: [...node.children, anchorRef] };
175
+ }
176
+
177
+ // [LAW:parse-dont-validate] The checkpoint: in, a tree that may or may not name
178
+ // the anchor; out, a tree that provably does. The author's placement passes
179
+ // through byte-identical — the position they chose IS the answer — and its
180
+ // absence is answered with the default position. One value, two sources.
181
+ function withAnchor(node: LayoutNode): AnchoredRoot {
182
+ const placed = countAnchors(node) > 0 ? node : appendAnchor(node);
183
+ return placed as AnchoredRoot;
184
+ }
185
+
186
+ // [LAW:single-enforcer] THE anchor census, read by both consumers of the count:
187
+ // `withAnchor` (is there a placement to honor?) and the loader's duplicate check
188
+ // (is there more than one?). One traversal definition, so "placed" cannot mean
189
+ // different things to the two.
190
+ export function countAnchors(node: LayoutNode): number {
191
+ if (node.kind === "segment") return isSettingsAnchor(node.name) ? 1 : 0;
192
+ return node.children.reduce((n, child) => n + countAnchors(child), 0);
193
+ }
194
+
195
+ // [LAW:one-type-per-behavior] The lowering, identical in shape to lowerGroup's:
196
+ // a vertical pair of the toggle segment and a `when`-gated body. Replaces the
197
+ // anchor leaf wherever it sits, so the author's chosen position is the menu's
198
+ // position with nothing else moved.
199
+ function expandAnchor(node: AnchoredRoot | LayoutNode): LayoutNode {
200
+ if (node.kind === "segment") {
201
+ return isSettingsAnchor(node.name)
202
+ ? {
203
+ kind: "container",
204
+ direction: "vertical",
205
+ children: [
206
+ node,
207
+ {
208
+ kind: "container",
209
+ direction: "horizontal",
210
+ children: [
211
+ { kind: "segment", name: PRESETS_SEG },
212
+ { kind: "segment", name: EDIT_SEG },
213
+ ],
214
+ when: SETTINGS_OPEN_GATE,
215
+ },
216
+ ],
217
+ }
218
+ : node;
219
+ }
220
+ return { ...node, children: node.children.map(expandAnchor) };
221
+ }
222
+
223
+ // ─── The artifacts ──────────────────────────────────────────────────────────
224
+
225
+ interface MenuArtifacts {
226
+ readonly variables: Record<string, VariableDecl>;
227
+ readonly actions: Record<string, ActionDecl>;
228
+ readonly segments: Record<string, SegmentDecl>;
229
+ }
230
+
231
+ // [LAW:single-enforcer] The `{{ menu }}` disclosure a body segment hosts,
232
+ // synthesized by calling the SAME pure functions menu-synth.ts's parse-time pass
233
+ // calls — the identical move edit-chrome.ts's insertChrome makes, and for the
234
+ // identical reason: this pass runs too late to piggyback on that one, so parity
235
+ // comes from sharing the derivation, never from restating it.
236
+ function declareHostedMenu(
237
+ segName: string,
238
+ applyName: string,
239
+ artifacts: MenuArtifacts,
240
+ ): void {
241
+ const member = menuMember(applyName);
242
+ const stateKey = menuStateKey(segName, applyName, undefined);
243
+ const pageKey = menuPageKey(stateKey);
244
+ artifacts.variables[stateKey] = disclosureStateVar(
245
+ stateKey,
246
+ DISCLOSURE_CLOSED,
247
+ );
248
+ artifacts.variables[pageKey] = { kind: "state", key: pageKey, default: "0" };
249
+ artifacts.actions[menuActionName(stateKey, member)] = disclosureCycleAction(
250
+ stateKey,
251
+ member,
252
+ );
253
+ artifacts.actions[pageKey] = { set: pageKey, int: true };
254
+ }
255
+
256
+ // [LAW:one-source-of-truth] Everything the menu is, minted ONCE per config and
257
+ // merely REFERENCED from each preset root. This is what makes the pass
258
+ // idempotent across N presets for free: a preset root carries a segment
259
+ // reference, and a second reference to one declaration is a reuse, not the
260
+ // self-collision a second `kind: "group"` node would be (see the settingsDrawer
261
+ // comment in default-dsl-config.ts for that hazard in its original form).
262
+ function settingsArtifacts(): MenuArtifacts {
263
+ const artifacts: MenuArtifacts = {
264
+ variables: {
265
+ [SETTINGS_ANCHOR]: disclosureStateVar(SETTINGS_ANCHOR, DISCLOSURE_CLOSED),
266
+ },
267
+ actions: {
268
+ [SETTINGS_ANCHOR]: disclosureCycleAction(SETTINGS_ANCHOR, SETTINGS_OPEN),
269
+ // [LAW:single-enforcer] The picker's apply effect, gated by derivation
270
+ // like every other `from`-sourced set: `presets` is a per-config domain
271
+ // both deriveActionValidators and the rendered options resolve through
272
+ // one `resolveOptionDomain`, so this adds a control, never a gate.
273
+ [APPLY_PRESET_ACTION]: { set: "preset", from: "presets" },
274
+ },
275
+ segments: {
276
+ // [LAW:representation] The glyph trails the label it gates, per the
277
+ // disclosure vocabulary every other toggle in the bar reads by.
278
+ [SETTINGS_ANCHOR]: {
279
+ template: `{{ action "${SETTINGS_ANCHOR}" "☰ ${DISCLOSURE_GLYPH_CLOSED}" "☰ ${DISCLOSURE_GLYPH_OPEN}" }}`,
280
+ bg: "surface",
281
+ fg: "foreground",
282
+ },
283
+ [PRESETS_SEG]: {
284
+ template: `▦ {{ menu "${APPLY_PRESET_ACTION}" (dict "closeOnPick" true) }}`,
285
+ bg: "surface",
286
+ fg: "foreground",
287
+ },
288
+ // The entry point edit mode never had: `edit.toggle` is a reserved action
289
+ // whose only bundled reference lives in the `toolbar` segment, which a
290
+ // user config's `root` drops like everything else. Here it is reachable
291
+ // from a segment no config can drop.
292
+ [EDIT_SEG]: {
293
+ template: `{{ action "${EDIT_TOGGLE_ACTION}" "✎ edit" "✎ done" }}`,
294
+ bg: "surface",
295
+ fg: "foreground",
296
+ },
297
+ },
298
+ };
299
+ declareHostedMenu(PRESETS_SEG, APPLY_PRESET_ACTION, artifacts);
300
+ return artifacts;
301
+ }
302
+
303
+ // [LAW:one-source-of-truth] Edit mode's toggle, ensured rather than duplicated:
304
+ // both this pass and synthesizeEditModeToggle produce it by calling the same two
305
+ // disclosure functions on the same two exported constants, so the two mints are
306
+ // the same value by construction and whichever lands first is the only one.
307
+ // Ensuring it here is not an optional courtesy — the EDIT_SEG segment above
308
+ // references `edit.toggle`, and that pass is demand-driven off a scan of the
309
+ // segments a FILE declared, which cannot see a segment this pass mints later.
310
+ function ensureEditToggle(artifacts: MenuArtifacts): void {
311
+ artifacts.variables[EDIT_MODE_KEY] = disclosureStateVar(
312
+ EDIT_MODE_KEY,
313
+ DISCLOSURE_CLOSED,
314
+ );
315
+ artifacts.actions[EDIT_TOGGLE_ACTION] = disclosureCycleAction(
316
+ EDIT_MODE_KEY,
317
+ EDIT_MODE_OPEN,
318
+ );
319
+ }
320
+
321
+ // [LAW:one-source-of-truth] The variable whose presence IS the precondition,
322
+ // named once so the predicate below and the load error cross-ref.ts raises when
323
+ // it fails cannot describe different variables.
324
+ export const SESSION_ID_VAR = "session.id";
325
+
326
+ // [LAW:types-are-the-program] The menu's one structural prerequisite, read as a
327
+ // value: a global `session.id`. It is not a demand gate and not a preference —
328
+ // the menu is a CLICK surface, every click composes a URL whose first segment is
329
+ // `session.id` read from the store, and cross-ref.ts already rejects an AUTHORED
330
+ // state read or `set` write in a config that declares no such variable. A config
331
+ // without it describes a static, non-interactive bar, and there is no menu to
332
+ // place on one. Every config the daemon renders merges the bundled default,
333
+ // which declares `session.id`, so in production this is universally true; what
334
+ // it excludes is the hand-built static config, not a user.
335
+ //
336
+ // [LAW:one-source-of-truth] Exported because this is THE fact "will the anchor
337
+ // resolve to a segment?" — asked here to decide whether to mint the menu, and
338
+ // asked by cross-ref.ts to decide whether an authored placement of the anchor is
339
+ // a reference this pass is about to satisfy or a dangling one. Two readers, one
340
+ // predicate: when they were two predicates, cross-ref accepted an anchor this
341
+ // pass then declined to provide, and the un-lowered reference reached the render
342
+ // walk to throw at `lookupSegment`.
343
+ export function canHostSessionState(config: DslConfig): boolean {
344
+ return Object.prototype.hasOwnProperty.call(config.variables, SESSION_ID_VAR);
345
+ }
346
+
347
+ // [LAW:single-enforcer] THE synthesis entry point, called once from
348
+ // validateConfig after cross-ref/cycle checks pass and before edit chrome.
349
+ // Every declared preset — the floor `default` included — gets an explicit
350
+ // `presets[name].root` carrying its anchored, expanded tree; `config.root`
351
+ // itself is left untouched, exactly as synthesizeEditChrome leaves it, because
352
+ // presetRoot falls back to it only for a preset declaring no root of its own
353
+ // and every name now declares one.
354
+ export function synthesizeSettingsMenu(config: DslConfig): DslConfig {
355
+ if (!canHostSessionState(config)) return config;
356
+ const artifacts = settingsArtifacts();
357
+ ensureEditToggle(artifacts);
358
+ const presets: Record<string, PresetDecl> = { ...config.presets };
359
+ for (const name of presetNames(config.presets)) {
360
+ const { node } = presetRoot(config, name);
361
+ presets[name] = {
362
+ ...presetByName(config.presets, name),
363
+ root: expandAnchor(withAnchor(node)),
364
+ };
365
+ }
366
+ return {
367
+ ...config,
368
+ variables: { ...config.variables, ...artifacts.variables },
369
+ actions: { ...config.actions, ...artifacts.actions },
370
+ segments: { ...config.segments, ...artifacts.segments },
371
+ presets,
372
+ };
373
+ }
@@ -48,11 +48,13 @@ import type {
48
48
  // BuildLineOptions), so the value a trigger label displays and the value
49
49
  // that actually shaped the render can never disagree — the same reasoning
50
50
  // theme/look already followed, generalized to every globals field a menu or
51
- // stepper can persist. `theme`/`look`/`style` compose SessionState over the
52
- // config default (a session pick can diverge from the persisted default for
53
- // its own session); `charset`/`colorCompatibility`/`autoWrap`/`padding` have
54
- // no SessionState half today, so their "effective" value is just the
55
- // resolved config global over its floor constant.
51
+ // stepper can persist. `theme`/`look`/`style`/`autoWrap`/`padding` compose
52
+ // SessionState over the config default (a session pick can diverge from the
53
+ // persisted default for its own session); `charset` and `colorCompatibility`
54
+ // have no SessionState half they describe the terminal (glyph coverage,
55
+ // colour depth) rather than a per-session taste, so the resolved config global
56
+ // over its floor constant is their whole resolution. See CHARSETS in
57
+ // themes/policy.ts for why that is a decision rather than a gap.
56
58
  export interface EffectiveGlobals {
57
59
  readonly theme: string;
58
60
  readonly look: string;
@@ -65,6 +65,8 @@ import { expandHome } from "../config/dsl-loader.js";
65
65
  import { renderDsl } from "../dsl/render.js";
66
66
  import {
67
67
  effectiveStripStyle,
68
+ effectiveAutoWrap,
69
+ effectivePadding,
68
70
  effectiveThemeName,
69
71
  effectiveLookName,
70
72
  lookKeyByName,
@@ -867,11 +869,12 @@ async function handleRequest(req: Request): Promise<HandledRequest> {
867
869
  //
868
870
  // [LAW:one-source-of-truth] Every globals field a menu/stepper can
869
871
  // persist (candybar-config-engine-71o.3), resolved ONCE into one
870
- // struct: theme/look/style compose the session's click (SessionState)
871
- // over the config default over a floor — a click recolors/reshapes the
872
- // whole bar on the next render; charset/colorCompatibility/autoWrap/
873
- // padding have no SessionState half today, so their "effective" value
874
- // is just the config global over its floor constant. This struct feeds
872
+ // struct: theme/look/style/autoWrap/padding compose the session's click
873
+ // (SessionState) over the config default over a floor — a click
874
+ // recolors/reshapes the whole bar on the next render; charset and
875
+ // colorCompatibility describe the TERMINAL rather than a taste and so
876
+ // have no SessionState half at all, making the config global over its
877
+ // floor constant their whole resolution. This struct feeds
875
878
  // BOTH the payload's `*.effective` fields (trigger labels) AND
876
879
  // renderOpts below (the actual render) — one resolution, two readers,
877
880
  // so a label can never disagree with what was rendered.
@@ -918,8 +921,18 @@ async function handleRequest(req: Request): Promise<HandledRequest> {
918
921
  sessionState.get(req.hookData.session_id, "style"),
919
922
  globals.style,
920
923
  ),
921
- autoWrap: globals.autoWrap ?? DEFAULT_WRAP,
922
- padding: globals.padding ?? DEFAULT_PADDING,
924
+ autoWrap: effectiveAutoWrap(
925
+ sessionState.get(req.hookData.session_id, "autoWrap"),
926
+ globals.autoWrap,
927
+ ),
928
+ padding: effectivePadding(
929
+ sessionState.get(req.hookData.session_id, "padding"),
930
+ globals.padding,
931
+ ),
932
+ // charset and colorCompatibility have no session half by design —
933
+ // they describe the terminal, not a taste (see CHARSETS in
934
+ // themes/policy.ts) — so the config global over its floor is their
935
+ // whole resolution.
923
936
  charset: globals.charset ?? DEFAULT_CHARSET,
924
937
  colorCompatibility:
925
938
  globals.colorCompatibility ?? DEFAULT_COLOR_COMPATIBILITY,
package/src/demo/dsl.ts CHANGED
@@ -33,6 +33,8 @@ import { listResolvablePaletteNames } from "../themes/policy.js";
33
33
  import {
34
34
  effectiveThemeName,
35
35
  effectiveLookName,
36
+ effectiveAutoWrap,
37
+ effectivePadding,
36
38
  lookKeyByName,
37
39
  paletteForThemeName,
38
40
  } from "../themes/index.js";
@@ -41,9 +43,7 @@ import { registerDslConfig, renderDsl } from "../dsl/render.js";
41
43
  import {
42
44
  DEFAULT_CHARSET,
43
45
  DEFAULT_COLOR_COMPATIBILITY,
44
- DEFAULT_PADDING,
45
46
  DEFAULT_TERMINAL_WIDTH,
46
- DEFAULT_WRAP,
47
47
  } from "../render/strip.js";
48
48
  import { applyClaudeCodeReserve } from "../utils/terminal-width.js";
49
49
 
@@ -139,10 +139,11 @@ try {
139
139
  width: applyClaudeCodeReserve(
140
140
  process.stdout.columns ?? DEFAULT_TERMINAL_WIDTH,
141
141
  ),
142
- // Same resolution the daemon applies: the config global over the
143
- // default-on floor.
144
- wrap: globals.autoWrap ?? DEFAULT_WRAP,
145
- padding: globals.padding ?? DEFAULT_PADDING,
142
+ // Same resolvers the daemon applies, with a null session pick — the
143
+ // demo has no SessionState, so both land on the config default over
144
+ // their floor.
145
+ wrap: effectiveAutoWrap(null, globals.autoWrap),
146
+ padding: effectivePadding(null, globals.padding),
146
147
  charset: globals.charset ?? DEFAULT_CHARSET,
147
148
  },
148
149
  undefined,
@@ -37,16 +37,12 @@ export type { Charset, ColorCompatibility, StripStyle };
37
37
  // applyClaudeCodeReserve from src/utils/terminal-width).
38
38
  export const DEFAULT_TERMINAL_WIDTH = 120;
39
39
 
40
- // [LAW:one-source-of-truth] The one statement of the globals.autoWrap
41
- // default (on current behavior). Every resolver of the config global
42
- // (`globals.autoWrap ?? DEFAULT_WRAP`) derives from this constant.
43
- export const DEFAULT_WRAP = true;
44
-
45
- // [LAW:one-source-of-truth] The one statement of the globals.padding
46
- // default (one space per side inside each segment cell — current behavior,
47
- // matching the legacy display.padding). Every resolver of the config global
48
- // (`globals.padding ?? DEFAULT_PADDING`) derives from this constant.
49
- export const DEFAULT_PADDING = 1;
40
+ // [LAW:one-source-of-truth] autoWrap's and padding's defaults live in
41
+ // themes/policy.ts beside their resolvers and padding's range, because the
42
+ // config loader needs them too and config must not import render
43
+ // [LAW:one-way-deps]. Re-exported here on the same terms as the types above, so
44
+ // render-layer consumers keep importing them from the strip module.
45
+ export { DEFAULT_WRAP, DEFAULT_PADDING } from "../themes/policy.js";
50
46
 
51
47
  // [LAW:one-source-of-truth] The one statement of the globals.charset default
52
48
  // (powerline unicode glyphs — current behavior, matching the legacy
@@ -9,6 +9,8 @@ export {
9
9
  effectiveLookName,
10
10
  lookKeyByName,
11
11
  effectiveStripStyle,
12
+ effectiveAutoWrap,
13
+ effectivePadding,
12
14
  isStripStyle,
13
15
  listResolvablePaletteNames,
14
16
  STRIP_STYLES,