@groeponline/pi-wishcraft 1.1.0 → 1.3.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.
Files changed (66) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/README.md +16 -7
  3. package/ROADMAP.md +144 -425
  4. package/docs/commands.md +6 -2
  5. package/docs/configuration.md +21 -0
  6. package/docs/design/accessibility.md +71 -0
  7. package/docs/design/deck-layout.md +69 -0
  8. package/docs/design/motion-gallery.md +80 -0
  9. package/docs/design/motion-system.md +119 -0
  10. package/docs/design/presets.md +196 -0
  11. package/docs/design/regression-testing.md +54 -0
  12. package/docs/design/responsive.md +53 -0
  13. package/docs/design/signal.md +71 -0
  14. package/docs/design/theme-contract.md +132 -0
  15. package/docs/design/vnext-overview.md +55 -0
  16. package/docs/design/vnext-release-plan.md +212 -0
  17. package/docs/index.md +19 -1
  18. package/docs/segments.md +1 -1
  19. package/package.json +2 -1
  20. package/src/config/appearance.ts +229 -0
  21. package/src/config/parse.ts +41 -0
  22. package/src/config/presets.ts +184 -0
  23. package/src/config/structural-presets.ts +555 -0
  24. package/src/config/tokens.ts +154 -0
  25. package/src/config/types.ts +210 -2
  26. package/src/extension/commands/commands.ts +36 -12
  27. package/src/extension/commands/powerline-completions.ts +4 -0
  28. package/src/extension/commands/queue-commands.ts +6 -0
  29. package/src/extension/core/segment-context.ts +27 -21
  30. package/src/extension/core/state.ts +16 -1
  31. package/src/extension/core/types.ts +11 -1
  32. package/src/extension/session/session-lifecycle.ts +29 -65
  33. package/src/extension/session/session-notifications.ts +91 -0
  34. package/src/extension/settings/appearance-write.ts +135 -0
  35. package/src/extension/settings/wishcraft-config-items.ts +119 -0
  36. package/src/extension/settings/wishcraft-config.ts +28 -114
  37. package/src/extension/skills/skill-manager.ts +5 -0
  38. package/src/extension/ui/deck/component.ts +371 -0
  39. package/src/extension/ui/deck/index.ts +34 -0
  40. package/src/extension/ui/deck/render.ts +260 -0
  41. package/src/extension/ui/deck/route-bodies.ts +209 -0
  42. package/src/extension/ui/deck/routes.ts +37 -0
  43. package/src/extension/ui/deck/session-snapshot.ts +122 -0
  44. package/src/extension/ui/deck/types.ts +97 -0
  45. package/src/extension/ui/powerline-menu-view.ts +19 -15
  46. package/src/extension/ui/signal-layout.ts +76 -0
  47. package/src/extension/ui/status-line-renderers.ts +20 -2
  48. package/src/motion/accessibility.ts +59 -0
  49. package/src/motion/catalog-extra.ts +429 -0
  50. package/src/motion/catalog.ts +336 -0
  51. package/src/motion/composer.ts +147 -0
  52. package/src/motion/frames.ts +84 -0
  53. package/src/motion/gallery.ts +77 -0
  54. package/src/motion/index.ts +78 -0
  55. package/src/motion/policy.ts +128 -0
  56. package/src/motion/scheduler.ts +159 -0
  57. package/src/motion/types.ts +132 -0
  58. package/src/render/timer.ts +1 -0
  59. package/src/signal/controller.ts +135 -0
  60. package/src/signal/integration.ts +46 -0
  61. package/src/signal/render.ts +178 -0
  62. package/src/theme/detect.ts +48 -0
  63. package/src/theme/tokens/index.ts +9 -0
  64. package/src/theme/tokens/mapping.ts +21 -0
  65. package/src/theme/tokens/types.ts +6 -0
  66. package/src/usage/token-budget.ts +23 -0
@@ -1,4 +1,5 @@
1
1
  import type { Theme, ThemeColor } from "@earendil-works/pi-coding-agent";
2
+ import type { MotionEvent } from "../motion/types.ts";
2
3
  import type { CostCurrencyCode } from "../usage/rates.ts";
3
4
 
4
5
  // Theme color - either a pi theme color name or a custom hex color
@@ -28,6 +29,176 @@ export type SemanticColor =
28
29
  // Color scheme mapping semantic names to actual colors
29
30
  export type ColorScheme = Partial<Record<SemanticColor, ColorValue>>;
30
31
 
32
+ /**
33
+ * Semantic design tokens (vNext). Presets fill abstract roles; the segment
34
+ * colors above are derived from them. See src/config/tokens.ts for the mapping
35
+ * and the default palette.
36
+ */
37
+ export interface WishcraftTokens {
38
+ /** Deck background. */
39
+ surface: ColorValue;
40
+ /** Raised areas: headers, footers, selected rows. */
41
+ surfaceRaised: ColorValue;
42
+ text: ColorValue;
43
+ textMuted: ColorValue;
44
+ primary: ColorValue;
45
+ secondary: ColorValue;
46
+ accent: ColorValue;
47
+ success: ColorValue;
48
+ warning: ColorValue;
49
+ error: ColorValue;
50
+ /** Focused control or active route. */
51
+ focus: ColorValue;
52
+ /** Selected row background. */
53
+ selection: ColorValue;
54
+ /** Resting motion, e.g. a still rail. */
55
+ motionDim: ColorValue;
56
+ /** The travelling head of a sweep. */
57
+ motionHot: ColorValue;
58
+ /** Cells just behind the head. */
59
+ motionTrail: ColorValue;
60
+ }
61
+
62
+ export type TokenRole = keyof WishcraftTokens;
63
+
64
+ /** Motion catalog id, e.g. `ember-relay`. */
65
+ export type MotionRef = string;
66
+
67
+ export type StructuralPresetName =
68
+ | "lanternwake"
69
+ | "threadbound"
70
+ | "scryglass"
71
+ | "runebloom"
72
+ | "moonwell"
73
+ | "hexforge"
74
+ | "vellum"
75
+ | "wisp"
76
+ | "starweave"
77
+ | "crucible";
78
+
79
+ export const STRUCTURAL_PRESET_NAMES = [
80
+ "lanternwake",
81
+ "threadbound",
82
+ "scryglass",
83
+ "runebloom",
84
+ "moonwell",
85
+ "hexforge",
86
+ "vellum",
87
+ "wisp",
88
+ "starweave",
89
+ "crucible",
90
+ ] as const satisfies readonly StructuralPresetName[];
91
+
92
+ export type ChromeFrame = "rounded" | "square" | "double" | "minimal" | "borderless";
93
+ export type ChromeDensity = "compact" | "medium" | "spacious";
94
+
95
+ export interface ChromeSpec {
96
+ frame: ChromeFrame;
97
+ corners: { tl: string; tr: string; bl: string; br: string };
98
+ dividers: { horizontal: string; vertical: string; cross: string };
99
+ density: ChromeDensity;
100
+ }
101
+
102
+ export type SignalLayout =
103
+ | "standard"
104
+ | "capsule"
105
+ | "woven"
106
+ | "sparse"
107
+ | "block"
108
+ | "arc"
109
+ | "editorial"
110
+ | "fluid"
111
+ | "constellation"
112
+ | "cell";
113
+
114
+ export interface SignalSpec {
115
+ layout: SignalLayout;
116
+ separators: {
117
+ left: string;
118
+ right: string;
119
+ subLeft?: string;
120
+ subRight?: string;
121
+ };
122
+ caps: {
123
+ leftOpen?: string;
124
+ leftClose?: string;
125
+ rightOpen?: string;
126
+ rightClose?: string;
127
+ };
128
+ /** Signature lane animation from the motion catalog. */
129
+ animation: MotionRef;
130
+ }
131
+
132
+ export type DeckNavigation = "tabs" | "rail" | "minimal";
133
+ export type DeckPanelStyle = "framed" | "borderless" | "inset";
134
+ export type DeckActivityStyle = "pulse" | "bar" | "none";
135
+
136
+ export interface DeckSpec {
137
+ navigation: DeckNavigation;
138
+ panelStyle: DeckPanelStyle;
139
+ activityStyle: DeckActivityStyle;
140
+ }
141
+
142
+ export interface WelcomeSpec {
143
+ lantern: boolean;
144
+ ambient: boolean;
145
+ motionId?: MotionRef;
146
+ }
147
+
148
+ export type GlyphMode = "nerd" | "ascii" | "auto";
149
+
150
+ export interface GlyphSet {
151
+ mode: GlyphMode;
152
+ /** Leading ornament for the model segment (Nerd). */
153
+ model?: string;
154
+ /** ASCII fallback for the model ornament. */
155
+ modelAscii?: string;
156
+ /** Default segment marker (Nerd). */
157
+ segment?: string;
158
+ /** ASCII fallback for segment markers. */
159
+ segmentAscii?: string;
160
+ }
161
+
162
+ /** vNext structural personality: tokens, chrome, signal grammar, motion, deck, welcome, glyphs. */
163
+ export interface StructuralPresetDef {
164
+ name: StructuralPresetName;
165
+ displayName: string;
166
+ description: string;
167
+ tokens: Partial<WishcraftTokens>;
168
+ chrome: ChromeSpec;
169
+ signal: SignalSpec;
170
+ motion: Partial<Record<MotionEvent, MotionRef>>;
171
+ deck: DeckSpec;
172
+ welcome: WelcomeSpec;
173
+ glyphs: GlyphSet;
174
+ }
175
+
176
+ /**
177
+ * Decoupled appearance layers. Each field can point at a different structural
178
+ * preset so palette, signal layout, chrome, and motion stay independent.
179
+ */
180
+ export interface AppearanceMixConfig {
181
+ base?: StructuralPresetName;
182
+ palette?: StructuralPresetName;
183
+ signalLayout?: StructuralPresetName;
184
+ chrome?: StructuralPresetName;
185
+ glyphs?: StructuralPresetName;
186
+ deck?: StructuralPresetName;
187
+ welcome?: StructuralPresetName;
188
+ motion?: StructuralPresetName | Partial<Record<MotionEvent, MotionRef>>;
189
+ }
190
+
191
+ export interface ResolvedAppearance {
192
+ base: StructuralPresetName;
193
+ tokens: WishcraftTokens;
194
+ chrome: ChromeSpec;
195
+ signal: SignalSpec;
196
+ motion: Partial<Record<MotionEvent, MotionRef>>;
197
+ deck: DeckSpec;
198
+ welcome: WelcomeSpec;
199
+ glyphs: GlyphSet;
200
+ }
201
+
31
202
  // Built-in segment identifiers
32
203
  export const BUILTIN_STATUS_LINE_SEGMENT_IDS = [
33
204
  "model",
@@ -78,7 +249,24 @@ export type StatusLineSeparatorStyle =
78
249
  export type PowerlinePlacement = "above" | "below";
79
250
 
80
251
  export type StatusLinePreset =
81
- "default" | "minimal" | "compact" | "full" | "nerd" | "ascii" | "chef";
252
+ | "default"
253
+ | "minimal"
254
+ | "compact"
255
+ | "full"
256
+ | "nerd"
257
+ | "ascii"
258
+ | "chef"
259
+ // vNext Structural Signature Presets
260
+ | "lanternwake"
261
+ | "threadbound"
262
+ | "scryglass"
263
+ | "runebloom"
264
+ | "moonwell"
265
+ | "hexforge"
266
+ | "vellum"
267
+ | "wisp"
268
+ | "starweave"
269
+ | "crucible";
82
270
 
83
271
  // Optional `{value}` template override shared by all segment option groups.
84
272
  // Replaces `{value}` in the rendered value text (e.g. "{value} tok/s").
@@ -153,7 +341,7 @@ export interface CustomStatusItem {
153
341
  excludeFromExtensionStatuses: boolean;
154
342
  }
155
343
 
156
- // Preset definition
344
+
157
345
  export interface PresetDef {
158
346
  leftSegments: StatusLineSegmentId[];
159
347
  rightSegments: StatusLineSegmentId[];
@@ -163,6 +351,24 @@ export interface PresetDef {
163
351
  segmentOptions?: StatusLineSegmentOptions;
164
352
  /** Color scheme for this preset */
165
353
  colors?: ColorScheme;
354
+ /**
355
+ * Semantic design tokens (vNext). When present, `colors` is derived from these
356
+ * and any explicit `colors` entry still wins. Optional, so the presets that
357
+ * predate tokens keep rendering exactly as before.
358
+ */
359
+ tokens?: Partial<WishcraftTokens>;
360
+ /** Deck and overlay frame geometry (vNext). */
361
+ chrome?: ChromeSpec;
362
+ /** Signal lane grammar and separators (vNext). */
363
+ signal?: SignalSpec;
364
+ /** Per-event motion overrides (vNext). */
365
+ motion?: Partial<Record<MotionEvent, MotionRef>>;
366
+ /** Deck navigation and panel chrome (vNext). */
367
+ deck?: DeckSpec;
368
+ /** Welcome screen lantern and ambient motion (vNext). */
369
+ welcome?: WelcomeSpec;
370
+ /** Nerd / ASCII glyph grammar (vNext). */
371
+ glyphs?: GlyphSet;
166
372
  }
167
373
 
168
374
  // Separator definition
@@ -311,4 +517,6 @@ export interface CustomPresetConfig {
311
517
  separator?: StatusLineSeparatorStyle;
312
518
  colors?: ColorScheme;
313
519
  segmentOptions?: StatusLineSegmentOptions;
520
+ /** Semantic design tokens; `colors` entries still win over derived values. */
521
+ tokens?: Partial<WishcraftTokens>;
314
522
  }
@@ -18,7 +18,9 @@ import {
18
18
  } from "../settings/settings-io.ts";
19
19
  import { showOpenPortsList, showSelectOverlay } from "../ui/menu-views.ts";
20
20
  import { showTpsOverlay, showUsageOverlay } from "../ui/token-overlays.ts";
21
- import { showPowerlineMainMenu } from "../ui/powerline-menu-view.ts";
21
+ import { openWishcraftDeck } from "../ui/deck/index.ts";
22
+ import { showPowerlineClassicMenu } from "../ui/powerline-menu-view.ts";
23
+ import { syncAppearanceForLayoutPreset } from "../settings/appearance-write.ts";
22
24
  import { openStashHistory } from "../shortcuts/shortcuts-router.ts";
23
25
  import { ensureShellSession, setBashModeActive } from "./bash-mode-actions.ts";
24
26
  import { setupCustomEditor } from "../ui/custom-editor.ts";
@@ -31,6 +33,7 @@ import { publishPowerlineStatuses } from "../core/status-export.ts";
31
33
  import { config, normalizePreset } from "../core/state.ts";
32
34
  import type { RuntimeState } from "../core/types.ts";
33
35
  import { getPowerlineArgumentCompletions } from "./powerline-completions.ts";
36
+ import { settleSignal } from "../../signal/integration.ts";
34
37
 
35
38
  export function registerCommands(pi: ExtensionAPI, rt: RuntimeState): void {
36
39
  registerCdCommand(pi, () => rt.currentCtx?.cwd ?? process.cwd());
@@ -39,9 +42,9 @@ export function registerCommands(pi: ExtensionAPI, rt: RuntimeState): void {
39
42
  registerSkillManagerCommand(pi, rt);
40
43
  registerWishcraftConfigCommand(pi, rt);
41
44
 
42
- // Command to toggle/configure
43
- pi.registerCommand("powerline", {
44
- description: "Configure powerline status (toggle, preset, placement)",
45
+ // `/signal` is the vNext command; `/powerline` remains a transparent alias.
46
+ const signalCommand: Parameters<ExtensionAPI["registerCommand"]>[1] = {
47
+ description: "Configure Signal status (toggle, preset, placement)",
45
48
  getArgumentCompletions(argumentPrefix) {
46
49
  return getPowerlineArgumentCompletions(argumentPrefix);
47
50
  },
@@ -54,8 +57,9 @@ export function registerCommands(pi: ExtensionAPI, rt: RuntimeState): void {
54
57
  rt.enabled = !rt.enabled;
55
58
  if (rt.enabled) {
56
59
  setupCustomEditor(pi, rt, ctx);
57
- ctx.ui.notify("Powerline enabled", "info");
60
+ ctx.ui.notify("Signal enabled", "info");
58
61
  } else {
62
+ settleSignal(rt);
59
63
  rt.shellSession?.dispose();
60
64
  rt.shellSession = null;
61
65
  rt.bashTranscript.clear();
@@ -87,12 +91,20 @@ export function registerCommands(pi: ExtensionAPI, rt: RuntimeState): void {
87
91
  rt.currentEditor = null;
88
92
  rt.statusRenderScheduler.cancel();
89
93
  resetLayoutCache(rt);
90
- ctx.ui.notify("Powerline disabled", "info");
94
+ ctx.ui.notify("Signal disabled", "info");
91
95
  }
92
96
  return;
93
97
  }
94
98
 
95
99
  const normalizedArgs = args.trim().toLowerCase();
100
+ if (normalizedArgs === "menu") {
101
+ await showPowerlineClassicMenu(rt, ctx);
102
+ return;
103
+ }
104
+ if (normalizedArgs === "deck") {
105
+ await openWishcraftDeck(rt, ctx, "signal");
106
+ return;
107
+ }
96
108
  if (normalizedArgs === "doctor") {
97
109
  await runPowerlineDoctor(rt, ctx);
98
110
  return;
@@ -123,12 +135,12 @@ export function registerCommands(pi: ExtensionAPI, rt: RuntimeState): void {
123
135
  )
124
136
  ) {
125
137
  ctx.ui.notify(
126
- `Powerline placement set to: ${config.placement}`,
138
+ `Signal placement set to: ${config.placement}`,
127
139
  "info",
128
140
  );
129
141
  } else {
130
142
  ctx.ui.notify(
131
- `Powerline placement set to: ${config.placement} (not persisted; check settings.json)`,
143
+ `Signal placement set to: ${config.placement} (not persisted; check settings.json)`,
132
144
  "warning",
133
145
  );
134
146
  }
@@ -144,7 +156,9 @@ export function registerCommands(pi: ExtensionAPI, rt: RuntimeState): void {
144
156
  setupCustomEditor(pi, rt, ctx);
145
157
  }
146
158
 
147
- if (writePowerlinePresetSetting(preset, ctx.cwd)) {
159
+ const persisted = writePowerlinePresetSetting(preset, ctx.cwd);
160
+ syncAppearanceForLayoutPreset(rt, ctx.cwd, preset);
161
+ if (persisted) {
148
162
  ctx.ui.notify(`Preset set to: ${preset}`, "info");
149
163
  } else {
150
164
  ctx.ui.notify(
@@ -159,6 +173,11 @@ export function registerCommands(pi: ExtensionAPI, rt: RuntimeState): void {
159
173
  const presetList = Object.keys(PRESETS).join(", ");
160
174
  ctx.ui.notify(`Available presets: ${presetList}`, "info");
161
175
  },
176
+ };
177
+ pi.registerCommand("signal", signalCommand);
178
+ pi.registerCommand("powerline", {
179
+ ...signalCommand,
180
+ description: "Compatibility alias for /signal",
162
181
  });
163
182
 
164
183
  pi.registerCommand("stash-history", {
@@ -250,8 +269,12 @@ export function registerCommands(pi: ExtensionAPI, rt: RuntimeState): void {
250
269
 
251
270
  pi.registerCommand("usage", {
252
271
  description: "Show session / today / week token usage overlay",
253
- handler: async (_args, ctx) => {
272
+ handler: async (args, ctx) => {
254
273
  rt.currentCtx = ctx;
274
+ if (args?.trim().toLowerCase() === "deck") {
275
+ await openWishcraftDeck(rt, ctx, "usage");
276
+ return;
277
+ }
255
278
  await showUsageOverlay(rt, ctx);
256
279
  },
257
280
  });
@@ -295,9 +318,10 @@ export function registerCommands(pi: ExtensionAPI, rt: RuntimeState): void {
295
318
  // Configurable powerline shortcuts (re-bound on /reload via settings.powerlineShortcuts).
296
319
  if (rt.resolvedShortcuts.menu) {
297
320
  pi.registerShortcut(rt.resolvedShortcuts.menu as KeyId, {
298
- description: "Powerline menu (navigate / configure / info)",
321
+ description: "Wishcraft Deck (operator control surface)",
299
322
  handler: async (ctx) => {
300
- await showPowerlineMainMenu(rt, ctx);
323
+ rt.currentCtx = ctx;
324
+ await openWishcraftDeck(rt, ctx, "home");
301
325
  },
302
326
  });
303
327
  }
@@ -19,6 +19,10 @@ export const POWERLINE_SUBCOMMANDS: ReadonlyArray<{
19
19
  value: "placement",
20
20
  description: "Move the primary row above or below the editor",
21
21
  },
22
+ {
23
+ value: "menu",
24
+ description: "Open Navigate / Configure / Status",
25
+ },
22
26
  ];
23
27
 
24
28
  const PLACEMENT_DESCRIPTIONS: Record<(typeof POWERLINE_PLACEMENT_VALUES)[number], string> = {
@@ -15,6 +15,7 @@ import { getCurrentEditorText } from "../shortcuts/shortcuts-router.ts";
15
15
  import { getQueueContext } from "../queue/queue-context.ts";
16
16
  import { config } from "../core/state.ts";
17
17
  import type { RuntimeState } from "../core/types.ts";
18
+ import { openWishcraftDeck } from "../ui/deck/index.ts";
18
19
 
19
20
  export function registerQueueCommands(
20
21
  pi: ExtensionAPI,
@@ -70,6 +71,11 @@ export function registerQueueCommands(
70
71
  return;
71
72
  }
72
73
 
74
+ if (action === "deck") {
75
+ await openWishcraftDeck(rt, ctx, "ideas");
76
+ return;
77
+ }
78
+
73
79
  if (action === "next") {
74
80
  const item = findNextIdea(rt, ctx);
75
81
  if (!item) {
@@ -5,6 +5,7 @@ import type {
5
5
 
6
6
  import type { SegmentContext, ColorScheme } from "../../config/types.ts";
7
7
  import { getPreset } from "../../config/presets.ts";
8
+ import { liveColorScheme } from "../../config/appearance.ts";
8
9
  import {
9
10
  collectHiddenExtensionStatusKeys,
10
11
  deriveAutoCustomItems,
@@ -14,14 +15,8 @@ import { getDefaultColors } from "../../theme/theme.ts";
14
15
  import { getGitStatus } from "../../git/status.ts";
15
16
  import { getQueueContext } from "../queue/queue-context.ts";
16
17
  import { getUsageTokenTotal } from "../../usage/ledger.ts";
17
- import {
18
- dayKey,
19
- loadUsageFileFromDisk,
20
- tokenTotal,
21
- totalsForRange,
22
- } from "../../usage/usage-store.ts";
23
- import { parseTokenBudget } from "../../usage/token-budget.ts";
24
- import { readSettings } from "../settings/settings-io.ts";
18
+ import { tokenBudgetSnapshotForDay } from "../../usage/token-budget.ts";
19
+ import { dayKey } from "../../usage/usage-store.ts";
25
20
  import {
26
21
  CUSTOM_COMPACTION_STATUS_KEY,
27
22
  EDITOR_STATUS_DEFER_MS,
@@ -119,13 +114,24 @@ export function installFooterStatusRepaintHook(
119
114
  };
120
115
  }
121
116
 
117
+ export interface BuildSegmentContextOptions {
118
+ /** Omit token-budget data from lightweight UI snapshots. */
119
+ includeTokenBudget?: boolean;
120
+ }
121
+
122
122
  export function buildSegmentContext(
123
123
  rt: RuntimeState,
124
124
  ctx: any,
125
125
  theme: Theme,
126
+ options: BuildSegmentContextOptions = {},
126
127
  ): SegmentContext {
127
128
  const presetDef = getPreset(config.preset);
128
- const colors: ColorScheme = presetDef.colors ?? getDefaultColors();
129
+ const colors: ColorScheme = liveColorScheme(
130
+ presetDef,
131
+ config.appearance,
132
+ config.preset,
133
+ getDefaultColors,
134
+ );
129
135
 
130
136
  // Build usage stats and get thinking level from session (cached; the full
131
137
  // event list is only re-scanned when events are appended or the trailing
@@ -199,6 +205,10 @@ export function buildSegmentContext(
199
205
  getQueueContext(ctx),
200
206
  rt.powerlineCompacting,
201
207
  );
208
+ const budgetSnapshot = tokenBudgetSnapshotForDay(
209
+ rt.tokenBudgetSnapshot,
210
+ dayKey(Date.now()),
211
+ );
202
212
 
203
213
  return {
204
214
  model: ctx.model,
@@ -228,18 +238,14 @@ export function buildSegmentContext(
228
238
  effectiveCustomItems,
229
239
  options: segmentOptions,
230
240
  segmentLabels: new Map(Object.entries(config.segmentLabels)),
231
- tokenBudget: (() => {
232
- const dailyLimit = parseTokenBudget(
233
- readSettings(ctx.cwd ?? process.cwd()).wishcraft,
234
- ).daily;
235
- const now = Date.now();
236
- const todayStart = Date.parse(`${dayKey(now)}T00:00:00`);
237
- const dailyUsed = tokenTotal(
238
- totalsForRange(loadUsageFileFromDisk(), todayStart, now + 1),
239
- );
240
- return { dailyLimit, dailyUsed };
241
- })(),
241
+ tokenBudget:
242
+ options.includeTokenBudget === false
243
+ ? undefined
244
+ : {
245
+ dailyLimit: budgetSnapshot.dailyLimit,
246
+ dailyUsed: budgetSnapshot.dailyUsed,
247
+ },
242
248
  theme,
243
249
  colors,
244
250
  };
245
- }
251
+ }
@@ -12,6 +12,8 @@ import {
12
12
  import { CoreContextUsageCache } from "../../usage/context.ts";
13
13
  import { createWelcomeDismissScheduler } from "../../welcome/auto-dismiss.ts";
14
14
  import { createRenderScheduler } from "../../render/timer.ts";
15
+ import { MotionScheduler, policyFromEnvironment } from "../../motion/index.ts";
16
+ import { createSignalRuntime } from "../../signal/controller.ts";
15
17
  import {
16
18
  resolveShortcutConfig,
17
19
  parseBashModeSettings,
@@ -47,6 +49,8 @@ export let config: PowerlineConfig = {
47
49
  segments: {},
48
50
  presets: {},
49
51
  segmentLabels: {},
52
+ appearance: {},
53
+ motionLevel: "full",
50
54
  };
51
55
 
52
56
  export function setConfig(next: PowerlineConfig): void {
@@ -113,6 +117,7 @@ export function createRuntimeState(
113
117
  currentEditor: null,
114
118
  costAlertNotified: false,
115
119
  tokenBudgetNotifiedLevel: 0,
120
+ tokenBudgetSnapshot: { day: "", dailyLimit: null, dailyUsed: 0 },
116
121
  bashModeActive: false,
117
122
  bashTranscript: new BashTranscriptStore(bashModeSettings),
118
123
  bashCompletionEngine: new BashCompletionEngine(),
@@ -135,6 +140,8 @@ export function createRuntimeState(
135
140
 
136
141
  resolvedShortcuts,
137
142
  bashModeSettings,
143
+ motionPolicy: policyFromEnvironment(process.env, config.motionLevel),
144
+ signal: createSignalRuntime(),
138
145
  } as RuntimeState;
139
146
 
140
147
  rt.welcomeDismissScheduler = createWelcomeDismissScheduler({
@@ -159,5 +166,13 @@ export function createRuntimeState(
159
166
  rt.tuiRef?.requestRender();
160
167
  }, STATUS_RENDER_DEBOUNCE_MS);
161
168
 
169
+ rt.motionScheduler = new MotionScheduler({
170
+ requestRender: () => {
171
+ // Motion only changes the Signal rail; segment data remains cached.
172
+ rt.lastLayoutResult = null;
173
+ rt.tuiRef?.requestRender();
174
+ },
175
+ });
176
+
162
177
  return rt;
163
- }
178
+ }
@@ -11,8 +11,11 @@ import type {
11
11
  SessionTokenStatsCache,
12
12
  } from "../../usage/ledger.ts";
13
13
  import type { CoreContextUsageCache } from "../../usage/context.ts";
14
+ import type { TokenBudgetSnapshot } from "../../usage/token-budget.ts";
14
15
  import type { WelcomeDismissScheduler } from "../../welcome/auto-dismiss.ts";
15
16
  import type { RenderScheduler } from "../../render/timer.ts";
17
+ import type { MotionPolicy, MotionScheduler } from "../../motion/index.ts";
18
+ import type { SignalRuntime } from "../../signal/controller.ts";
16
19
 
17
20
  export type ShortcutBinding = string | null;
18
21
 
@@ -66,6 +69,8 @@ export interface RuntimeState {
66
69
  costAlertNotified: boolean;
67
70
  /** Highest daily-budget warning already shown this session (0 / 80 / 100). */
68
71
  tokenBudgetNotifiedLevel: 0 | 80 | 100;
72
+ /** Event-driven token-budget snapshot consumed by render paths without disk I/O. */
73
+ tokenBudgetSnapshot: TokenBudgetSnapshot;
69
74
  bashModeActive: boolean;
70
75
  bashTranscript: BashTranscriptStore;
71
76
  bashCompletionEngine: BashCompletionEngine;
@@ -98,4 +103,9 @@ export interface RuntimeState {
98
103
 
99
104
  welcomeDismissScheduler: WelcomeDismissScheduler<any>;
100
105
  statusRenderScheduler: RenderScheduler;
101
- }
106
+ /** The single shared clock for every Wishcraft animation surface. */
107
+ motionScheduler: MotionScheduler;
108
+ motionPolicy: MotionPolicy;
109
+ /** Live Signal event, motion frame, and activity label. */
110
+ signal: SignalRuntime;
111
+ }