@oh-my-pi/pi-coding-agent 15.6.0 → 15.7.1

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 (140) hide show
  1. package/CHANGELOG.md +35 -0
  2. package/dist/types/capability/rule-buckets.d.ts +30 -0
  3. package/dist/types/capability/rule.d.ts +7 -0
  4. package/dist/types/cli/completion-gen.d.ts +80 -0
  5. package/dist/types/commands/complete.d.ts +6 -0
  6. package/dist/types/commands/completions.d.ts +13 -0
  7. package/dist/types/commands/setup.d.ts +10 -1
  8. package/dist/types/config/settings-schema.d.ts +170 -10
  9. package/dist/types/discovery/builtin-defaults.d.ts +1 -0
  10. package/dist/types/discovery/builtin-rules/index.d.ts +7 -0
  11. package/dist/types/discovery/index.d.ts +1 -0
  12. package/dist/types/edit/hashline/block-resolver.d.ts +9 -0
  13. package/dist/types/edit/hashline/index.d.ts +1 -0
  14. package/dist/types/eval/py/kernel.d.ts +3 -0
  15. package/dist/types/eval/py/runtime.d.ts +11 -1
  16. package/dist/types/export/html/template.generated.d.ts +1 -1
  17. package/dist/types/main.d.ts +1 -0
  18. package/dist/types/modes/components/index.d.ts +1 -0
  19. package/dist/types/modes/components/segment-track.d.ts +22 -0
  20. package/dist/types/modes/components/welcome.d.ts +21 -0
  21. package/dist/types/modes/interactive-mode.d.ts +3 -2
  22. package/dist/types/modes/setup-wizard/index.d.ts +16 -0
  23. package/dist/types/modes/setup-wizard/scenes/glyph.d.ts +2 -0
  24. package/dist/types/modes/setup-wizard/scenes/outro.d.ts +2 -0
  25. package/dist/types/modes/setup-wizard/scenes/providers.d.ts +2 -0
  26. package/dist/types/modes/setup-wizard/scenes/sign-in.d.ts +19 -0
  27. package/dist/types/modes/setup-wizard/scenes/splash.d.ts +11 -0
  28. package/dist/types/modes/setup-wizard/scenes/theme.d.ts +2 -0
  29. package/dist/types/modes/setup-wizard/scenes/types.d.ts +43 -0
  30. package/dist/types/modes/setup-wizard/scenes/web-search.d.ts +19 -0
  31. package/dist/types/modes/setup-wizard/wizard-overlay.d.ts +14 -0
  32. package/dist/types/modes/theme/shimmer.d.ts +2 -0
  33. package/dist/types/modes/theme/theme.d.ts +11 -0
  34. package/dist/types/modes/types.d.ts +5 -1
  35. package/dist/types/tiny/device.d.ts +78 -0
  36. package/dist/types/tiny/dtype.d.ts +85 -0
  37. package/dist/types/tiny/models.d.ts +6 -6
  38. package/dist/types/tiny/text.d.ts +15 -0
  39. package/dist/types/tiny/title-client.d.ts +8 -0
  40. package/dist/types/tools/bash.d.ts +0 -1
  41. package/dist/types/tools/eval.d.ts +1 -1
  42. package/dist/types/tools/index.d.ts +0 -1
  43. package/dist/types/tui/code-cell.d.ts +2 -0
  44. package/dist/types/tui/output-block.d.ts +17 -0
  45. package/package.json +9 -9
  46. package/src/capability/rule-buckets.ts +64 -0
  47. package/src/capability/rule.ts +8 -0
  48. package/src/cli/completion-gen.ts +550 -0
  49. package/src/cli/setup-cli.ts +5 -3
  50. package/src/cli-commands.ts +2 -0
  51. package/src/cli.ts +1 -7
  52. package/src/commands/complete.ts +66 -0
  53. package/src/commands/completions.ts +60 -0
  54. package/src/commands/setup.ts +29 -4
  55. package/src/config/settings-schema.ts +70 -11
  56. package/src/discovery/builtin-defaults.ts +39 -0
  57. package/src/discovery/builtin-rules/index.ts +48 -0
  58. package/src/discovery/builtin-rules/rs-box-leak.md +48 -0
  59. package/src/discovery/builtin-rules/rs-future-prelude.md +23 -0
  60. package/src/discovery/builtin-rules/rs-lazylock.md +51 -0
  61. package/src/discovery/builtin-rules/rs-match-ergonomics.md +67 -0
  62. package/src/discovery/builtin-rules/rs-parking-lot.md +44 -0
  63. package/src/discovery/builtin-rules/rs-result-type.md +19 -0
  64. package/src/discovery/builtin-rules/ts-bare-catch.md +38 -0
  65. package/src/discovery/builtin-rules/ts-import-type.md +42 -0
  66. package/src/discovery/builtin-rules/ts-no-any.md +56 -0
  67. package/src/discovery/builtin-rules/ts-no-dynamic-import.md +39 -0
  68. package/src/discovery/builtin-rules/ts-no-return-type.md +45 -0
  69. package/src/discovery/builtin-rules/ts-no-tiny-functions.md +50 -0
  70. package/src/discovery/builtin-rules/ts-promise-with-resolvers.md +65 -0
  71. package/src/discovery/builtin-rules/ts-set-map.md +28 -0
  72. package/src/discovery/index.ts +1 -0
  73. package/src/edit/hashline/block-resolver.ts +14 -0
  74. package/src/edit/hashline/diff.ts +4 -1
  75. package/src/edit/hashline/execute.ts +2 -1
  76. package/src/edit/hashline/index.ts +1 -0
  77. package/src/eval/py/kernel.ts +37 -15
  78. package/src/eval/py/runtime.ts +57 -28
  79. package/src/export/html/template.generated.ts +1 -1
  80. package/src/export/html/template.js +0 -12
  81. package/src/export/ttsr.ts +2 -0
  82. package/src/internal-urls/docs-index.generated.ts +7 -8
  83. package/src/main.ts +18 -1
  84. package/src/modes/components/hook-selector.ts +15 -17
  85. package/src/modes/components/index.ts +1 -0
  86. package/src/modes/components/segment-track.ts +52 -0
  87. package/src/modes/components/tips.txt +2 -1
  88. package/src/modes/components/tool-execution.ts +5 -1
  89. package/src/modes/components/welcome.ts +47 -42
  90. package/src/modes/controllers/input-controller.ts +12 -21
  91. package/src/modes/interactive-mode.ts +17 -5
  92. package/src/modes/setup-wizard/index.ts +88 -0
  93. package/src/modes/setup-wizard/scenes/glyph.ts +96 -0
  94. package/src/modes/setup-wizard/scenes/outro.ts +35 -0
  95. package/src/modes/setup-wizard/scenes/providers.ts +69 -0
  96. package/src/modes/setup-wizard/scenes/sign-in.ts +193 -0
  97. package/src/modes/setup-wizard/scenes/splash.ts +201 -0
  98. package/src/modes/setup-wizard/scenes/theme.ts +299 -0
  99. package/src/modes/setup-wizard/scenes/types.ts +48 -0
  100. package/src/modes/setup-wizard/scenes/web-search.ts +128 -0
  101. package/src/modes/setup-wizard/wizard-overlay.ts +275 -0
  102. package/src/modes/theme/shimmer.ts +5 -0
  103. package/src/modes/theme/theme.ts +44 -20
  104. package/src/modes/types.ts +6 -1
  105. package/src/prompts/system/orchestrate-notice.md +1 -1
  106. package/src/prompts/tools/read.md +4 -0
  107. package/src/sdk.ts +5 -15
  108. package/src/slash-commands/builtin-registry.ts +8 -0
  109. package/src/tiny/device.ts +117 -0
  110. package/src/tiny/dtype.ts +101 -0
  111. package/src/tiny/models.ts +7 -6
  112. package/src/tiny/text.ts +36 -1
  113. package/src/tiny/title-client.ts +58 -3
  114. package/src/tiny/worker.ts +93 -29
  115. package/src/tools/bash.ts +16 -13
  116. package/src/tools/eval.ts +9 -4
  117. package/src/tools/index.ts +0 -11
  118. package/src/tools/read.ts +1 -0
  119. package/src/tools/renderers.ts +0 -2
  120. package/src/tui/code-cell.ts +6 -1
  121. package/src/tui/output-block.ts +199 -38
  122. package/dist/types/tools/recipe/index.d.ts +0 -46
  123. package/dist/types/tools/recipe/render.d.ts +0 -36
  124. package/dist/types/tools/recipe/runner.d.ts +0 -60
  125. package/dist/types/tools/recipe/runners/cargo.d.ts +0 -16
  126. package/dist/types/tools/recipe/runners/index.d.ts +0 -2
  127. package/dist/types/tools/recipe/runners/just.d.ts +0 -2
  128. package/dist/types/tools/recipe/runners/make.d.ts +0 -2
  129. package/dist/types/tools/recipe/runners/pkg.d.ts +0 -2
  130. package/dist/types/tools/recipe/runners/task.d.ts +0 -2
  131. package/src/prompts/tools/recipe.md +0 -16
  132. package/src/tools/recipe/index.ts +0 -81
  133. package/src/tools/recipe/render.ts +0 -19
  134. package/src/tools/recipe/runner.ts +0 -219
  135. package/src/tools/recipe/runners/cargo.ts +0 -131
  136. package/src/tools/recipe/runners/index.ts +0 -8
  137. package/src/tools/recipe/runners/just.ts +0 -73
  138. package/src/tools/recipe/runners/make.ts +0 -101
  139. package/src/tools/recipe/runners/pkg.ts +0 -167
  140. package/src/tools/recipe/runners/task.ts +0 -72
@@ -0,0 +1,275 @@
1
+ import { type Component, matchesKey, padding, truncateToWidth, visibleWidth } from "@oh-my-pi/pi-tui";
2
+ import { APP_NAME } from "@oh-my-pi/pi-utils";
3
+ import { gradientLogo, PI_LOGO } from "../components/welcome";
4
+ import { theme } from "../theme/theme";
5
+ import type { InteractiveModeContext } from "../types";
6
+ import { renderSetupOutro, SETUP_OUTRO_MS } from "./scenes/outro";
7
+ import { renderSetupSplash, SETUP_SPLASH_MS, SETUP_TICK_MS } from "./scenes/splash";
8
+ import type { SetupScene, SetupSceneController, SetupSceneHost, SetupSceneResult } from "./scenes/types";
9
+
10
+ type WizardPhase = "splash" | "transition" | "scene" | "outro" | "done";
11
+
12
+ const SCENE_MARGIN_X = 4;
13
+ const MIN_CONTENT_WIDTH = 20;
14
+ /** Cross-dissolve duration from the splash into the first scene. */
15
+ const SCENE_TRANSITION_MS = 420;
16
+
17
+ function centerLine(line: string, width: number): string {
18
+ const lineWidth = visibleWidth(line);
19
+ if (lineWidth >= width) return truncateToWidth(line, width);
20
+ const left = Math.floor((width - lineWidth) / 2);
21
+ return padding(left) + line + padding(width - left - lineWidth);
22
+ }
23
+
24
+ function clampLine(line: string, width: number): string {
25
+ const truncated = truncateToWidth(line, width);
26
+ return truncated + padding(Math.max(0, width - visibleWidth(truncated)));
27
+ }
28
+
29
+ function indentLine(line: string, width: number, indent: number): string {
30
+ const prefix = padding(Math.min(indent, Math.max(0, width - 1)));
31
+ return clampLine(prefix + line, width);
32
+ }
33
+ /** Stable per-row jitter in [0,1) for the dissolve reveal order. */
34
+ function rowNoise(y: number): number {
35
+ const h = Math.imul(y ^ 0x9e3779b9, 2654435761);
36
+ return ((h ^ (h >>> 15)) >>> 0) / 4294967296;
37
+ }
38
+
39
+ /**
40
+ * Top-biased cross-dissolve between two equal-height frames. As `progress`
41
+ * (0..1) advances, each row flips from `from` to `to` once it crosses a per-row
42
+ * threshold — top rows reveal first (so the scene's mark/header materializes
43
+ * before the splash water below it), with a little jitter for an organic edge.
44
+ */
45
+ function dissolveFrames(from: string[], to: string[], progress: number, height: number): string[] {
46
+ const eased = progress * progress * (3 - 2 * progress);
47
+ const denom = Math.max(1, height - 1);
48
+ const out: string[] = [];
49
+ for (let y = 0; y < height; y++) {
50
+ const threshold = 0.78 * (y / denom) + 0.22 * rowNoise(y);
51
+ out.push((eased >= threshold ? to[y] : from[y]) ?? "");
52
+ }
53
+ return out;
54
+ }
55
+
56
+ export class SetupWizardComponent implements Component {
57
+ #phase: WizardPhase = "splash";
58
+ #phaseStartedAt = performance.now();
59
+ #sceneIndex = 0;
60
+ #activeScene: SetupSceneController | undefined;
61
+ #timer: NodeJS.Timeout | undefined;
62
+ #done = Promise.withResolvers<void>();
63
+ #disposed = false;
64
+
65
+ constructor(
66
+ readonly ctx: InteractiveModeContext,
67
+ readonly scenes: readonly SetupScene[],
68
+ ) {}
69
+
70
+ run(): Promise<void> {
71
+ this.#phase = this.scenes.length === 0 ? "outro" : "splash";
72
+ this.#phaseStartedAt = performance.now();
73
+ this.#startTimer();
74
+ this.ctx.ui.requestRender();
75
+ return this.#done.promise;
76
+ }
77
+
78
+ dispose(): void {
79
+ this.#disposed = true;
80
+ this.#stopTimer();
81
+ this.#unmountActiveScene();
82
+ }
83
+
84
+ invalidate(): void {
85
+ this.#activeScene?.invalidate();
86
+ }
87
+
88
+ handleInput(data: string): void {
89
+ if (this.#phase === "done") return;
90
+ if (matchesKey(data, "ctrl+c")) {
91
+ this.#beginOutro();
92
+ return;
93
+ }
94
+ if (this.#phase === "splash") {
95
+ if (
96
+ matchesKey(data, "enter") ||
97
+ matchesKey(data, "return") ||
98
+ matchesKey(data, "space") ||
99
+ matchesKey(data, "escape")
100
+ ) {
101
+ this.#beginScene();
102
+ }
103
+ return;
104
+ }
105
+ if (this.#phase === "outro") {
106
+ if (
107
+ matchesKey(data, "enter") ||
108
+ matchesKey(data, "return") ||
109
+ matchesKey(data, "space") ||
110
+ matchesKey(data, "escape")
111
+ ) {
112
+ this.#complete();
113
+ }
114
+ return;
115
+ }
116
+ this.#activeScene?.handleInput?.(data);
117
+ }
118
+
119
+ render(width: number): string[] {
120
+ const safeWidth = Math.max(1, width);
121
+ const height = Math.max(1, this.ctx.ui.terminal.rows);
122
+ let lines: string[];
123
+ switch (this.#phase) {
124
+ case "splash":
125
+ lines = renderSetupSplash(safeWidth, height, performance.now() - this.#phaseStartedAt);
126
+ break;
127
+ case "transition": {
128
+ const elapsed = performance.now() - this.#phaseStartedAt;
129
+ const progress = Math.min(1, elapsed / SCENE_TRANSITION_MS);
130
+ const splash = renderSetupSplash(safeWidth, height, SETUP_SPLASH_MS + elapsed);
131
+ const scene = this.#renderScene(safeWidth, height);
132
+ lines = dissolveFrames(splash, scene, progress, height);
133
+ break;
134
+ }
135
+ case "outro":
136
+ lines = renderSetupOutro(safeWidth, height, performance.now() - this.#phaseStartedAt);
137
+ break;
138
+ case "scene":
139
+ lines = this.#renderScene(safeWidth, height);
140
+ break;
141
+ case "done":
142
+ lines = [];
143
+ break;
144
+ }
145
+ return this.#fitToScreen(lines, safeWidth, height);
146
+ }
147
+
148
+ #renderScene(width: number, height: number): string[] {
149
+ const scene = this.scenes[this.#sceneIndex];
150
+ const title = this.#activeScene?.title ?? scene?.title ?? "Setup";
151
+ const subtitle = this.#activeScene?.subtitle;
152
+ const contentWidth = Math.max(MIN_CONTENT_WIDTH, width - SCENE_MARGIN_X * 2);
153
+ const logo = gradientLogo(PI_LOGO, 0);
154
+ const header = [
155
+ "",
156
+ ...logo.map(line => centerLine(line, width)),
157
+ centerLine(theme.bold(theme.fg("accent", APP_NAME)), width),
158
+ centerLine(theme.fg("muted", `Setup step ${this.#sceneIndex + 1} of ${this.scenes.length}`), width),
159
+ "",
160
+ indentLine(theme.bold(title), width, SCENE_MARGIN_X),
161
+ ];
162
+ if (subtitle) {
163
+ header.push(indentLine(theme.fg("muted", subtitle), width, SCENE_MARGIN_X));
164
+ }
165
+ header.push("");
166
+
167
+ const footer = [
168
+ "",
169
+ centerLine(theme.fg("dim", "↑/↓ select · enter confirm · esc skip · ctrl+c exit setup"), width),
170
+ ];
171
+ const maxBodyLines = Math.max(0, height - header.length - footer.length);
172
+ const body = this.#activeScene?.render(contentWidth).slice(0, maxBodyLines) ?? [];
173
+ const lines = [...header, ...body.map(line => indentLine(line, width, SCENE_MARGIN_X))];
174
+ while (lines.length + footer.length < height) {
175
+ lines.push("");
176
+ }
177
+ lines.push(...footer);
178
+ return lines;
179
+ }
180
+
181
+ #fitToScreen(lines: string[], width: number, height: number): string[] {
182
+ const fitted = lines.slice(0, height).map(line => clampLine(line, width));
183
+ while (fitted.length < height) {
184
+ fitted.push(padding(width));
185
+ }
186
+ return fitted;
187
+ }
188
+
189
+ #startTimer(): void {
190
+ if (this.#timer) return;
191
+ this.#timer = setInterval(() => {
192
+ if (this.#disposed) return;
193
+ const elapsed = performance.now() - this.#phaseStartedAt;
194
+ if (this.#phase === "splash" && elapsed >= SETUP_SPLASH_MS) {
195
+ this.#beginScene();
196
+ } else if (this.#phase === "transition" && elapsed >= SCENE_TRANSITION_MS) {
197
+ this.#phase = "scene";
198
+ this.#phaseStartedAt = performance.now();
199
+ this.ctx.ui.requestRender();
200
+ } else if (this.#phase === "outro" && elapsed >= SETUP_OUTRO_MS) {
201
+ this.#complete();
202
+ } else {
203
+ this.ctx.ui.requestRender();
204
+ }
205
+ }, SETUP_TICK_MS);
206
+ }
207
+
208
+ #stopTimer(): void {
209
+ if (!this.#timer) return;
210
+ clearInterval(this.#timer);
211
+ this.#timer = undefined;
212
+ }
213
+
214
+ #mountSceneController(targetPhase: "scene" | "transition"): void {
215
+ if (this.#disposed) return;
216
+ this.#unmountActiveScene();
217
+ if (this.#sceneIndex >= this.scenes.length) {
218
+ this.#beginOutro();
219
+ return;
220
+ }
221
+ const scene = this.scenes[this.#sceneIndex];
222
+ const host: SetupSceneHost = {
223
+ ctx: this.ctx,
224
+ requestRender: () => this.ctx.ui.requestRender(),
225
+ finish: (_result: SetupSceneResult) => this.#finishScene(),
226
+ setFocus: component => this.ctx.ui.setFocus(component),
227
+ restoreFocus: () => this.ctx.ui.setFocus(this),
228
+ };
229
+ this.#activeScene = scene.mount(host);
230
+ this.#phase = targetPhase;
231
+ this.#phaseStartedAt = performance.now();
232
+ this.ctx.ui.setFocus(this);
233
+ void this.#activeScene.onMount?.();
234
+ this.ctx.ui.requestRender();
235
+ }
236
+
237
+ /** Enter the first scene through a dissolve from the splash. */
238
+ #beginScene(): void {
239
+ this.#mountSceneController("transition");
240
+ }
241
+
242
+ #mountCurrentScene(): void {
243
+ this.#mountSceneController("scene");
244
+ }
245
+
246
+ #finishScene(): void {
247
+ if (this.#phase !== "scene" && this.#phase !== "transition") return;
248
+ this.#unmountActiveScene();
249
+ this.#sceneIndex += 1;
250
+ this.#mountCurrentScene();
251
+ }
252
+
253
+ #unmountActiveScene(): void {
254
+ this.#activeScene?.onUnmount?.();
255
+ this.#activeScene?.dispose?.();
256
+ this.#activeScene = undefined;
257
+ }
258
+
259
+ #beginOutro(): void {
260
+ if (this.#phase === "done") return;
261
+ this.#unmountActiveScene();
262
+ this.#phase = "outro";
263
+ this.#phaseStartedAt = performance.now();
264
+ this.ctx.ui.setFocus(this);
265
+ this.#startTimer();
266
+ this.ctx.ui.requestRender();
267
+ }
268
+
269
+ #complete(): void {
270
+ if (this.#phase === "done") return;
271
+ this.#phase = "done";
272
+ this.#stopTimer();
273
+ this.#done.resolve();
274
+ }
275
+ }
@@ -147,6 +147,11 @@ function resolveMode(): ShimmerMode {
147
147
  return settings.get("display.shimmer");
148
148
  }
149
149
 
150
+ /** Whether shimmer animations are active (any mode other than `disabled`). */
151
+ export function shimmerEnabled(): boolean {
152
+ return resolveMode() !== "disabled";
153
+ }
154
+
150
155
  /**
151
156
  * Apply a shimmer sweep across one or more segments, treating them as a
152
157
  * single continuous string for band positioning. Each segment can supply
@@ -1310,6 +1310,24 @@ export class Theme {
1310
1310
  return ansi;
1311
1311
  }
1312
1312
 
1313
+ /**
1314
+ * Foreground ANSI for text drawn **on top of** `fillColor` used as a solid
1315
+ * background (e.g. a powerline chip). Picks near-black or near-white by the
1316
+ * fill's perceived luminance (Rec. 601 luma) so the label stays legible on
1317
+ * both bright and dark fills, across light and dark themes.
1318
+ *
1319
+ * Reads the RGB out of the already-resolved truecolor escape; when the fill
1320
+ * is encoded as a 256-palette index (limited terminals) the RGB is
1321
+ * unavailable, so it falls back to the theme `text` color.
1322
+ */
1323
+ getContrastFgAnsi(fillColor: ThemeColor): string {
1324
+ const ansi = this.#fgColors[fillColor];
1325
+ const match = ansi ? /38;2;(\d+);(\d+);(\d+)/.exec(ansi) : null;
1326
+ if (!match) return this.#fgColors.text;
1327
+ const luma = 0.299 * Number(match[1]) + 0.587 * Number(match[2]) + 0.114 * Number(match[3]);
1328
+ return luma > 140 ? "\x1b[38;2;0;0;0m" : "\x1b[38;2;255;255;255m";
1329
+ }
1330
+
1313
1331
  getColorMode(): ColorMode {
1314
1332
  return this.mode;
1315
1333
  }
@@ -1929,17 +1947,20 @@ export function setThemeInstance(themeInstance: Theme): void {
1929
1947
  */
1930
1948
  export async function setSymbolPreset(preset: SymbolPreset): Promise<void> {
1931
1949
  currentSymbolPresetOverride = preset;
1932
- if (currentThemeName) {
1933
- try {
1934
- theme = await loadTheme(currentThemeName, getCurrentThemeOptions());
1935
- } catch {
1936
- // Fall back to dark theme with new preset
1937
- theme = await loadTheme("dark", getCurrentThemeOptions());
1938
- }
1939
- if (onThemeChangeCallback) {
1940
- onThemeChangeCallback();
1941
- }
1950
+ if (!currentThemeName) return;
1951
+
1952
+ const requestId = ++themeLoadRequestId;
1953
+ try {
1954
+ const loadedTheme = await loadTheme(currentThemeName, getCurrentThemeOptions());
1955
+ if (requestId !== themeLoadRequestId) return;
1956
+ theme = loadedTheme;
1957
+ } catch {
1958
+ if (requestId !== themeLoadRequestId) return;
1959
+ // Fall back to dark theme with new preset
1960
+ theme = await loadTheme("dark", getCurrentThemeOptions());
1961
+ if (requestId !== themeLoadRequestId) return;
1942
1962
  }
1963
+ onThemeChangeCallback?.();
1943
1964
  }
1944
1965
 
1945
1966
  /**
@@ -1955,17 +1976,20 @@ export function getSymbolPresetOverride(): SymbolPreset | undefined {
1955
1976
  */
1956
1977
  export async function setColorBlindMode(enabled: boolean): Promise<void> {
1957
1978
  currentColorBlindMode = enabled;
1958
- if (currentThemeName) {
1959
- try {
1960
- theme = await loadTheme(currentThemeName, getCurrentThemeOptions());
1961
- } catch {
1962
- // Fall back to dark theme
1963
- theme = await loadTheme("dark", getCurrentThemeOptions());
1964
- }
1965
- if (onThemeChangeCallback) {
1966
- onThemeChangeCallback();
1967
- }
1979
+ if (!currentThemeName) return;
1980
+
1981
+ const requestId = ++themeLoadRequestId;
1982
+ try {
1983
+ const loadedTheme = await loadTheme(currentThemeName, getCurrentThemeOptions());
1984
+ if (requestId !== themeLoadRequestId) return;
1985
+ theme = loadedTheme;
1986
+ } catch {
1987
+ if (requestId !== themeLoadRequestId) return;
1988
+ // Fall back to dark theme
1989
+ theme = await loadTheme("dark", getCurrentThemeOptions());
1990
+ if (requestId !== themeLoadRequestId) return;
1968
1991
  }
1992
+ onThemeChangeCallback?.();
1969
1993
  }
1970
1994
 
1971
1995
  /**
@@ -58,6 +58,10 @@ export type TodoPhase = {
58
58
  tasks: TodoItem[];
59
59
  };
60
60
 
61
+ export interface InteractiveModeInitOptions {
62
+ suppressWelcomeIntro?: boolean;
63
+ }
64
+
61
65
  export interface InteractiveModeContext {
62
66
  // UI access
63
67
  ui: TUI;
@@ -129,7 +133,8 @@ export interface InteractiveModeContext {
129
133
  todoPhases: TodoPhase[];
130
134
 
131
135
  // Lifecycle
132
- init(): Promise<void>;
136
+ init(options?: InteractiveModeInitOptions): Promise<void>;
137
+ playWelcomeIntro(): void;
133
138
  shutdown(): Promise<void>;
134
139
  checkShutdownRequested(): Promise<void>;
135
140
 
@@ -2,7 +2,7 @@
2
2
  The user's message above is an **orchestration request**. Execute it as the orchestrator under the contract below. This contract overrides any default tendency to yield early, narrate, or do the work yourself.
3
3
 
4
4
  <role>
5
- You decompose, dispatch, verify, and iterate. You do **not** edit code. Every file mutation goes through a `task` subagent. Your tool budget is: reading for planning, `task` for dispatch, verification (`bun check`, `bun test`, `recipe`, `lsp diagnostics`), git via `bash`, and `todo_write` for tracking.
5
+ You decompose, dispatch, verify, and iterate. You do **not** edit code. Every file mutation goes through a `task` subagent. Your tool budget is: reading for planning, `task` for dispatch, verification (`bun check`, `bun test`, `lsp diagnostics`), git via `bash`, and `todo_write` for tracking.
6
6
  </role>
7
7
 
8
8
  <rules>
@@ -46,7 +46,11 @@ Extracts text from PDF, Word, PowerPoint, Excel, RTF, and EPUB. Notebooks (`.ipy
46
46
 
47
47
  # Images
48
48
 
49
+ {{#if INSPECT_IMAGE_ENABLED}}
49
50
  Reading an image path returns metadata (mime, bytes, dimensions, channels, alpha). For actual visual analysis, call `inspect_image` with the path and a question describing what to inspect.
51
+ {{else}}
52
+ Reading an image path returns the decoded image inline (PNG, JPEG, GIF, WEBP) for direct visual analysis.
53
+ {{/if}}
50
54
 
51
55
  # Archives
52
56
 
package/src/sdk.ts CHANGED
@@ -38,6 +38,7 @@ import { type AsyncJob, AsyncJobManager, isBackgroundJobSupportEnabled } from ".
38
38
  import { createAutoresearchExtension } from "./autoresearch";
39
39
  import { loadCapability } from "./capability";
40
40
  import { type Rule, ruleCapability, setActiveRules } from "./capability/rule";
41
+ import { bucketRules } from "./capability/rule-buckets";
41
42
  import { ModelRegistry } from "./config/model-registry";
42
43
  import {
43
44
  formatModelString,
@@ -1045,21 +1046,10 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
1045
1046
  options.rules !== undefined
1046
1047
  ? { items: options.rules, warnings: undefined }
1047
1048
  : await loadCapability<Rule>(ruleCapability.id, { cwd });
1048
- const rulebookRules: Rule[] = [];
1049
- const alwaysApplyRules: Rule[] = [];
1050
- for (const rule of rulesResult.items) {
1051
- const isTtsrRule = rule.condition && rule.condition.length > 0 ? ttsrManager.addRule(rule) : false;
1052
- if (isTtsrRule) {
1053
- continue;
1054
- }
1055
- if (rule.alwaysApply === true) {
1056
- alwaysApplyRules.push(rule);
1057
- continue;
1058
- }
1059
- if (rule.description) {
1060
- rulebookRules.push(rule);
1061
- }
1062
- }
1049
+ const { rulebookRules, alwaysApplyRules } = bucketRules(rulesResult.items, ttsrManager, {
1050
+ builtinRules: ttsrSettings.builtinRules,
1051
+ disabledRules: ttsrSettings.disabledRules,
1052
+ });
1063
1053
  if (existingSession.injectedTtsrRules.length > 0) {
1064
1054
  ttsrManager.restoreInjected(existingSession.injectedTtsrRules);
1065
1055
  }
@@ -160,6 +160,14 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<SlashCommandSpec> = [
160
160
  runtime.ctx.editor.setText("");
161
161
  },
162
162
  },
163
+ {
164
+ name: "switch",
165
+ description: "Switch model for this session (same as alt+p)",
166
+ handleTui: (_command, runtime) => {
167
+ runtime.ctx.showModelSelector({ temporaryOnly: true });
168
+ runtime.ctx.editor.setText("");
169
+ },
170
+ },
163
171
  {
164
172
  name: "fast",
165
173
  description: "Toggle priority service tier (OpenAI service_tier=priority, Anthropic speed=fast)",
@@ -0,0 +1,117 @@
1
+ import type { DeviceType } from "@huggingface/transformers";
2
+ import { $env } from "@oh-my-pi/pi-utils";
3
+
4
+ export type TinyModelDevice = DeviceType;
5
+
6
+ export interface TinyModelDevicePreference {
7
+ device: TinyModelDevice;
8
+ raw: string | undefined;
9
+ }
10
+
11
+ const CPU_DEVICE: TinyModelDevice = "cpu";
12
+ const CPU_ONLY_ORDER: readonly TinyModelDevice[] = [CPU_DEVICE];
13
+ const DARWIN_WEBGPU_UNSAFE_ORDER: readonly TinyModelDevice[] = [CPU_DEVICE];
14
+
15
+ const DEVICE_VALUES: Record<TinyModelDevice, true> = {
16
+ auto: true,
17
+ gpu: true,
18
+ cpu: true,
19
+ wasm: true,
20
+ webgpu: true,
21
+ cuda: true,
22
+ dml: true,
23
+ coreml: true,
24
+ webnn: true,
25
+ "webnn-npu": true,
26
+ "webnn-gpu": true,
27
+ "webnn-cpu": true,
28
+ };
29
+
30
+ function defaultTinyModelDevice(): TinyModelDevice {
31
+ if (process.platform === "win32") return "dml";
32
+ if (process.platform === "linux" && process.arch === "x64") return "cuda";
33
+ return CPU_DEVICE;
34
+ }
35
+
36
+ function usesDarwinWorkerWebGpu(device: TinyModelDevice): boolean {
37
+ return process.platform === "darwin" && (device === "gpu" || device === "webgpu" || device === "auto");
38
+ }
39
+
40
+ export function normalizeTinyModelDevice(value: string | undefined): TinyModelDevice | undefined {
41
+ const raw = value?.trim().toLowerCase();
42
+ if (!raw) return undefined;
43
+ if (raw === "metal") return "webgpu";
44
+ if (raw in DEVICE_VALUES) return raw as TinyModelDevice;
45
+ throw new Error(
46
+ `Unsupported PI_TINY_DEVICE=${JSON.stringify(value)}. Use cpu, gpu, metal, webgpu, auto, cuda, dml, coreml, wasm, webnn, webnn-gpu, webnn-cpu, or webnn-npu.`,
47
+ );
48
+ }
49
+
50
+ export function resolveTinyModelDevicePreference(
51
+ value: string | undefined = $env.PI_TINY_DEVICE,
52
+ ): TinyModelDevicePreference {
53
+ return {
54
+ device: normalizeTinyModelDevice(value) ?? defaultTinyModelDevice(),
55
+ raw: value,
56
+ };
57
+ }
58
+
59
+ export function tinyModelDeviceLoadOrder(preference: TinyModelDevicePreference): readonly TinyModelDevice[] {
60
+ if (preference.device === CPU_DEVICE) return CPU_ONLY_ORDER;
61
+ if (usesDarwinWorkerWebGpu(preference.device)) return DARWIN_WEBGPU_UNSAFE_ORDER;
62
+ return [preference.device, CPU_DEVICE];
63
+ }
64
+
65
+ /** Sentinel `providers.tinyModelDevice` value meaning "use the built-in platform default". */
66
+ export const TINY_MODEL_DEVICE_DEFAULT = "default";
67
+
68
+ /** Accepted values for the `providers.tinyModelDevice` setting (validation + UI). */
69
+ export const TINY_MODEL_DEVICE_SETTING_VALUES = [
70
+ TINY_MODEL_DEVICE_DEFAULT,
71
+ "gpu",
72
+ "cpu",
73
+ "metal",
74
+ "webgpu",
75
+ "cuda",
76
+ "dml",
77
+ "coreml",
78
+ "auto",
79
+ "wasm",
80
+ "webnn",
81
+ "webnn-gpu",
82
+ "webnn-cpu",
83
+ "webnn-npu",
84
+ ] as const;
85
+
86
+ /** Submenu metadata for the `providers.tinyModelDevice` setting. */
87
+ export const TINY_MODEL_DEVICE_SETTING_OPTIONS = [
88
+ { value: "default", label: "Default", description: "DirectML on Windows, CUDA on Linux x64, CPU elsewhere" },
89
+ { value: "gpu", label: "GPU", description: "Accelerated provider (WebGPU/Metal, CUDA, or DirectML)" },
90
+ { value: "cpu", label: "CPU", description: "CPU-only inference" },
91
+ { value: "metal", label: "Metal", description: "WebGPU alias for Apple GPUs" },
92
+ { value: "webgpu", label: "WebGPU", description: "WebGPU/Metal backend" },
93
+ { value: "cuda", label: "CUDA", description: "NVIDIA CUDA (Linux x64)" },
94
+ { value: "dml", label: "DirectML", description: "DirectML backend (Windows)" },
95
+ { value: "coreml", label: "CoreML", description: "Apple CoreML (opt-in; can fail to load)" },
96
+ { value: "auto", label: "Auto", description: "Let ONNX Runtime choose a provider" },
97
+ { value: "wasm", label: "WASM", description: "WebAssembly backend" },
98
+ { value: "webnn", label: "WebNN", description: "WebNN backend" },
99
+ { value: "webnn-gpu", label: "WebNN GPU", description: "WebNN GPU device" },
100
+ { value: "webnn-cpu", label: "WebNN CPU", description: "WebNN CPU device" },
101
+ { value: "webnn-npu", label: "WebNN NPU", description: "WebNN NPU device" },
102
+ ] as const satisfies ReadonlyArray<{
103
+ value: (typeof TINY_MODEL_DEVICE_SETTING_VALUES)[number];
104
+ label: string;
105
+ description: string;
106
+ }>;
107
+
108
+ /**
109
+ * Map a `providers.tinyModelDevice` setting value onto a `PI_TINY_DEVICE` env
110
+ * value for the worker. Returns `undefined` for the default sentinel so the
111
+ * worker keeps its built-in platform default; the worker still validates the
112
+ * forwarded value via {@link normalizeTinyModelDevice}.
113
+ */
114
+ export function tinyModelDeviceSettingToEnv(value: string | undefined): string | undefined {
115
+ if (!value || value === TINY_MODEL_DEVICE_DEFAULT) return undefined;
116
+ return value;
117
+ }
@@ -0,0 +1,101 @@
1
+ import type { DataType } from "@huggingface/transformers";
2
+ import { $env } from "@oh-my-pi/pi-utils";
3
+
4
+ /** ONNX quantization / precision for local tiny models (transformers.js `dtype`). */
5
+ export type TinyModelDtype = DataType;
6
+
7
+ const DTYPE_VALUES: Record<TinyModelDtype, true> = {
8
+ auto: true,
9
+ fp32: true,
10
+ fp16: true,
11
+ q8: true,
12
+ int8: true,
13
+ uint8: true,
14
+ q4: true,
15
+ bnb4: true,
16
+ q4f16: true,
17
+ q2: true,
18
+ q2f16: true,
19
+ q1: true,
20
+ q1f16: true,
21
+ };
22
+
23
+ /**
24
+ * Validate and canonicalize a `PI_TINY_DTYPE` value. Returns `undefined` when
25
+ * unset/blank so callers fall back to the per-model spec dtype, and throws on an
26
+ * unrecognized value so a misconfiguration fails loudly instead of silently
27
+ * loading a different precision than requested.
28
+ */
29
+ export function normalizeTinyModelDtype(value: string | undefined): TinyModelDtype | undefined {
30
+ const raw = value?.trim().toLowerCase();
31
+ if (!raw) return undefined;
32
+ if (raw in DTYPE_VALUES) return raw as TinyModelDtype;
33
+ throw new Error(
34
+ `Unsupported PI_TINY_DTYPE=${JSON.stringify(value)}. Use auto, fp32, fp16, q8, int8, uint8, q4, bnb4, q4f16, q2, q2f16, q1, or q1f16.`,
35
+ );
36
+ }
37
+
38
+ /**
39
+ * Resolve the `PI_TINY_DTYPE` override. `undefined` means "use the per-model spec
40
+ * dtype" (currently `q4` for every shipped model); a concrete value overrides the
41
+ * precision for whichever local tiny model loads.
42
+ */
43
+ export function resolveTinyModelDtypeOverride(
44
+ value: string | undefined = $env.PI_TINY_DTYPE,
45
+ ): TinyModelDtype | undefined {
46
+ return normalizeTinyModelDtype(value);
47
+ }
48
+
49
+ /** Sentinel `providers.tinyModelDtype` value meaning "use each model's shipped dtype". */
50
+ export const TINY_MODEL_DTYPE_DEFAULT = "default";
51
+
52
+ /** Accepted values for the `providers.tinyModelDtype` setting (validation + UI). */
53
+ export const TINY_MODEL_DTYPE_SETTING_VALUES = [
54
+ TINY_MODEL_DTYPE_DEFAULT,
55
+ "q4",
56
+ "q4f16",
57
+ "q8",
58
+ "fp16",
59
+ "fp32",
60
+ "int8",
61
+ "uint8",
62
+ "bnb4",
63
+ "q2",
64
+ "q2f16",
65
+ "q1",
66
+ "q1f16",
67
+ "auto",
68
+ ] as const;
69
+
70
+ /** Submenu metadata for the `providers.tinyModelDtype` setting. */
71
+ export const TINY_MODEL_DTYPE_SETTING_OPTIONS = [
72
+ { value: "default", label: "Default", description: "Each model's shipped dtype (currently q4)" },
73
+ { value: "q4", label: "q4", description: "4-bit weights; smallest and fastest" },
74
+ { value: "q4f16", label: "q4f16", description: "4-bit weights with fp16 activations" },
75
+ { value: "q8", label: "q8", description: "8-bit quantization" },
76
+ { value: "fp16", label: "fp16", description: "16-bit float; higher fidelity, larger" },
77
+ { value: "fp32", label: "fp32", description: "Full precision; largest and slowest" },
78
+ { value: "int8", label: "int8", description: "Signed 8-bit integer" },
79
+ { value: "uint8", label: "uint8", description: "Unsigned 8-bit integer" },
80
+ { value: "bnb4", label: "bnb4", description: "bitsandbytes 4-bit" },
81
+ { value: "q2", label: "q2", description: "2-bit weights" },
82
+ { value: "q2f16", label: "q2f16", description: "2-bit weights with fp16 activations" },
83
+ { value: "q1", label: "q1", description: "1-bit weights" },
84
+ { value: "q1f16", label: "q1f16", description: "1-bit weights with fp16 activations" },
85
+ { value: "auto", label: "Auto", description: "Let transformers.js choose per device" },
86
+ ] as const satisfies ReadonlyArray<{
87
+ value: (typeof TINY_MODEL_DTYPE_SETTING_VALUES)[number];
88
+ label: string;
89
+ description: string;
90
+ }>;
91
+
92
+ /**
93
+ * Map a `providers.tinyModelDtype` setting value onto a `PI_TINY_DTYPE` env value
94
+ * for the worker. Returns `undefined` for the default sentinel so the worker keeps
95
+ * each model's shipped dtype; the worker still validates the forwarded value via
96
+ * {@link normalizeTinyModelDtype}.
97
+ */
98
+ export function tinyModelDtypeSettingToEnv(value: string | undefined): string | undefined {
99
+ if (!value || value === TINY_MODEL_DTYPE_DEFAULT) return undefined;
100
+ return value;
101
+ }