@groeponline/pi-wishcraft 1.1.0 → 1.2.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 (64) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/README.md +16 -7
  3. package/ROADMAP.md +41 -260
  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 +68 -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 +230 -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 +7 -1
  30. package/src/extension/core/state.ts +14 -0
  31. package/src/extension/core/types.ts +7 -0
  32. package/src/extension/session/session-lifecycle.ts +20 -0
  33. package/src/extension/settings/appearance-write.ts +135 -0
  34. package/src/extension/settings/wishcraft-config-items.ts +119 -0
  35. package/src/extension/settings/wishcraft-config.ts +28 -114
  36. package/src/extension/skills/skill-manager.ts +5 -0
  37. package/src/extension/ui/deck/component.ts +358 -0
  38. package/src/extension/ui/deck/index.ts +34 -0
  39. package/src/extension/ui/deck/render.ts +260 -0
  40. package/src/extension/ui/deck/route-bodies.ts +209 -0
  41. package/src/extension/ui/deck/routes.ts +37 -0
  42. package/src/extension/ui/deck/session-snapshot.ts +104 -0
  43. package/src/extension/ui/deck/types.ts +92 -0
  44. package/src/extension/ui/powerline-menu-view.ts +19 -15
  45. package/src/extension/ui/signal-layout.ts +77 -0
  46. package/src/extension/ui/status-line-renderers.ts +20 -2
  47. package/src/motion/accessibility.ts +59 -0
  48. package/src/motion/catalog-extra.ts +429 -0
  49. package/src/motion/catalog.ts +336 -0
  50. package/src/motion/composer.ts +147 -0
  51. package/src/motion/frames.ts +84 -0
  52. package/src/motion/gallery.ts +77 -0
  53. package/src/motion/index.ts +78 -0
  54. package/src/motion/policy.ts +128 -0
  55. package/src/motion/scheduler.ts +159 -0
  56. package/src/motion/types.ts +132 -0
  57. package/src/render/timer.ts +1 -0
  58. package/src/signal/controller.ts +123 -0
  59. package/src/signal/integration.ts +42 -0
  60. package/src/signal/render.ts +178 -0
  61. package/src/theme/detect.ts +48 -0
  62. package/src/theme/tokens/index.ts +9 -0
  63. package/src/theme/tokens/mapping.ts +62 -0
  64. package/src/theme/tokens/types.ts +39 -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,
@@ -125,7 +126,12 @@ export function buildSegmentContext(
125
126
  theme: Theme,
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
@@ -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 {
@@ -135,6 +139,8 @@ export function createRuntimeState(
135
139
 
136
140
  resolvedShortcuts,
137
141
  bashModeSettings,
142
+ motionPolicy: policyFromEnvironment(process.env, config.motionLevel),
143
+ signal: createSignalRuntime(),
138
144
  } as RuntimeState;
139
145
 
140
146
  rt.welcomeDismissScheduler = createWelcomeDismissScheduler({
@@ -159,5 +165,13 @@ export function createRuntimeState(
159
165
  rt.tuiRef?.requestRender();
160
166
  }, STATUS_RENDER_DEBOUNCE_MS);
161
167
 
168
+ rt.motionScheduler = new MotionScheduler({
169
+ requestRender: () => {
170
+ // Motion only changes the Signal rail; segment data remains cached.
171
+ rt.lastLayoutResult = null;
172
+ rt.tuiRef?.requestRender();
173
+ },
174
+ });
175
+
162
176
  return rt;
163
177
  }
@@ -13,6 +13,8 @@ import type {
13
13
  import type { CoreContextUsageCache } from "../../usage/context.ts";
14
14
  import type { WelcomeDismissScheduler } from "../../welcome/auto-dismiss.ts";
15
15
  import type { RenderScheduler } from "../../render/timer.ts";
16
+ import type { MotionPolicy, MotionScheduler } from "../../motion/index.ts";
17
+ import type { SignalRuntime } from "../../signal/controller.ts";
16
18
 
17
19
  export type ShortcutBinding = string | null;
18
20
 
@@ -98,4 +100,9 @@ export interface RuntimeState {
98
100
 
99
101
  welcomeDismissScheduler: WelcomeDismissScheduler<any>;
100
102
  statusRenderScheduler: RenderScheduler;
103
+ /** The single shared clock for every Wishcraft animation surface. */
104
+ motionScheduler: MotionScheduler;
105
+ motionPolicy: MotionPolicy;
106
+ /** Live Signal event, motion frame, and activity label. */
107
+ signal: SignalRuntime;
101
108
  }
@@ -3,6 +3,7 @@ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
3
3
  import { BashTranscriptStore } from "../../../bash-mode/transcript.ts";
4
4
  import { BashCompletionEngine } from "../../../bash-mode/completion.ts";
5
5
  import { parsePowerlineConfig } from "../../config/powerline-config.ts";
6
+ import { policyFromEnvironment } from "../../motion/accessibility.ts";
6
7
  import { registerCustomSegments } from "../../segments/index.ts";
7
8
  import { registerCustomPresets } from "../../config/presets.ts";
8
9
  import { invalidateGitStatus } from "../../git/status.ts";
@@ -76,6 +77,10 @@ import {
76
77
  clearSkillsCountPublisher,
77
78
  } from "../skills/skill-status.ts";
78
79
  import { maybeAppendReadHint } from "./read-hints.ts";
80
+ import {
81
+ dispatchSignalEvent,
82
+ settleSignal,
83
+ } from "../../signal/integration.ts";
79
84
 
80
85
  /**
81
86
  * Fire the configured `powerline.costAlert` warning at most once per session.
@@ -162,6 +167,7 @@ export function registerSessionLifecycle(
162
167
  ): void {
163
168
  // Track session start
164
169
  pi.on("session_start", async (event, ctx) => {
170
+ settleSignal(rt);
165
171
  clearSkillsCountPublisher();
166
172
  rt.shellSession?.dispose();
167
173
  rt.shellSession = null;
@@ -183,6 +189,7 @@ export function registerSessionLifecycle(
183
189
  rt.bashModeSettings = parseBashModeSettings(settings, rt.resolvedShortcuts);
184
190
  rt.showLastPrompt = settings.showLastPrompt !== false;
185
191
  setConfig(parsePowerlineConfig(settings.powerline, PRESET_NAMES));
192
+ rt.motionPolicy = policyFromEnvironment(process.env, config.motionLevel);
186
193
  rt.queueStore.setSentRetentionMs(
187
194
  config.queue.retentionHours * 60 * 60 * 1000,
188
195
  );
@@ -238,6 +245,7 @@ export function registerSessionLifecycle(
238
245
  rt.welcomeOverlayShouldDismiss = false;
239
246
  rt.welcomeDismissScheduler.cancel();
240
247
  rt.statusRenderScheduler.cancel();
248
+ rt.motionScheduler.dispose();
241
249
  rt.restoreFooterStatusRepaintHook?.();
242
250
  rt.restoreFooterStatusRepaintHook = null;
243
251
  rt.stashShortcutInputUnsubscribe?.();
@@ -320,6 +328,7 @@ export function registerSessionLifecycle(
320
328
  onVibeAgentStart();
321
329
  dismissWelcome(rt, ctx);
322
330
  rt.currentCtx = ctx;
331
+ dispatchSignalEvent(rt, config.appearance, "thinking");
323
332
  });
324
333
 
325
334
  pi.on("message_update", async (event, ctx) => {
@@ -333,6 +342,9 @@ export function registerSessionLifecycle(
333
342
  rt.currentCtx = ctx;
334
343
  rt.layoutDirty = true;
335
344
  rt.statusRenderScheduler.schedule(CONTEXT_STATUS_RENDER_MS);
345
+ if (rt.signal.event !== "streaming") {
346
+ dispatchSignalEvent(rt, config.appearance, "streaming");
347
+ }
336
348
  }
337
349
  });
338
350
 
@@ -374,6 +386,7 @@ export function registerSessionLifecycle(
374
386
  rt.powerlineCompacting = true;
375
387
  rt.currentCtx = ctx;
376
388
  requestQueueRender(rt);
389
+ dispatchSignalEvent(rt, config.appearance, "compact");
377
390
  });
378
391
 
379
392
  pi.on("session_compact", async (event, ctx) => {
@@ -406,6 +419,12 @@ export function registerSessionLifecycle(
406
419
  // Also dismiss on tool calls (agent is working) + refresh vibe if rate limit allows
407
420
  pi.on("tool_call", async (event, ctx) => {
408
421
  dismissWelcome(rt, ctx);
422
+ dispatchSignalEvent(
423
+ rt,
424
+ config.appearance,
425
+ "tool.start",
426
+ `tool ${event.toolName}`,
427
+ );
409
428
  if (ctx.hasUI) {
410
429
  // Extract recent agent context from session for richer vibe generation
411
430
  const agentContext = getRecentAgentContext(ctx);
@@ -422,6 +441,7 @@ export function registerSessionLifecycle(
422
441
  rt.isStreaming = false;
423
442
  rt.liveAssistantUsage = null;
424
443
  rt.coreContextUsageCache.reset();
444
+ dispatchSignalEvent(rt, config.appearance, "success");
425
445
 
426
446
  let hasUI = false;
427
447
  try {