@oh-my-pi/pi-coding-agent 15.6.0 → 15.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +35 -0
- package/dist/types/capability/rule-buckets.d.ts +30 -0
- package/dist/types/capability/rule.d.ts +7 -0
- package/dist/types/cli/completion-gen.d.ts +80 -0
- package/dist/types/commands/complete.d.ts +6 -0
- package/dist/types/commands/completions.d.ts +13 -0
- package/dist/types/commands/setup.d.ts +10 -1
- package/dist/types/config/settings-schema.d.ts +170 -10
- package/dist/types/discovery/builtin-defaults.d.ts +1 -0
- package/dist/types/discovery/builtin-rules/index.d.ts +7 -0
- package/dist/types/discovery/index.d.ts +1 -0
- package/dist/types/edit/hashline/block-resolver.d.ts +9 -0
- package/dist/types/edit/hashline/index.d.ts +1 -0
- package/dist/types/eval/py/kernel.d.ts +3 -0
- package/dist/types/eval/py/runtime.d.ts +11 -1
- package/dist/types/export/html/template.generated.d.ts +1 -1
- package/dist/types/main.d.ts +1 -0
- package/dist/types/modes/components/index.d.ts +1 -0
- package/dist/types/modes/components/segment-track.d.ts +22 -0
- package/dist/types/modes/components/welcome.d.ts +21 -0
- package/dist/types/modes/interactive-mode.d.ts +3 -2
- package/dist/types/modes/setup-wizard/index.d.ts +16 -0
- package/dist/types/modes/setup-wizard/scenes/glyph.d.ts +2 -0
- package/dist/types/modes/setup-wizard/scenes/outro.d.ts +2 -0
- package/dist/types/modes/setup-wizard/scenes/providers.d.ts +2 -0
- package/dist/types/modes/setup-wizard/scenes/sign-in.d.ts +19 -0
- package/dist/types/modes/setup-wizard/scenes/splash.d.ts +11 -0
- package/dist/types/modes/setup-wizard/scenes/theme.d.ts +2 -0
- package/dist/types/modes/setup-wizard/scenes/types.d.ts +43 -0
- package/dist/types/modes/setup-wizard/scenes/web-search.d.ts +19 -0
- package/dist/types/modes/setup-wizard/wizard-overlay.d.ts +14 -0
- package/dist/types/modes/theme/shimmer.d.ts +2 -0
- package/dist/types/modes/theme/theme.d.ts +11 -0
- package/dist/types/modes/types.d.ts +5 -1
- package/dist/types/tiny/device.d.ts +78 -0
- package/dist/types/tiny/dtype.d.ts +85 -0
- package/dist/types/tiny/models.d.ts +6 -6
- package/dist/types/tiny/text.d.ts +15 -0
- package/dist/types/tiny/title-client.d.ts +8 -0
- package/dist/types/tools/bash.d.ts +0 -1
- package/dist/types/tools/eval.d.ts +1 -1
- package/dist/types/tools/index.d.ts +0 -1
- package/dist/types/tui/code-cell.d.ts +2 -0
- package/dist/types/tui/output-block.d.ts +17 -0
- package/package.json +9 -9
- package/src/capability/rule-buckets.ts +64 -0
- package/src/capability/rule.ts +8 -0
- package/src/cli/completion-gen.ts +550 -0
- package/src/cli/setup-cli.ts +5 -3
- package/src/cli-commands.ts +2 -0
- package/src/cli.ts +1 -7
- package/src/commands/complete.ts +66 -0
- package/src/commands/completions.ts +60 -0
- package/src/commands/setup.ts +29 -4
- package/src/config/settings-schema.ts +70 -11
- package/src/discovery/builtin-defaults.ts +39 -0
- package/src/discovery/builtin-rules/index.ts +48 -0
- package/src/discovery/builtin-rules/rs-box-leak.md +48 -0
- package/src/discovery/builtin-rules/rs-future-prelude.md +23 -0
- package/src/discovery/builtin-rules/rs-lazylock.md +51 -0
- package/src/discovery/builtin-rules/rs-match-ergonomics.md +67 -0
- package/src/discovery/builtin-rules/rs-parking-lot.md +44 -0
- package/src/discovery/builtin-rules/rs-result-type.md +19 -0
- package/src/discovery/builtin-rules/ts-bare-catch.md +38 -0
- package/src/discovery/builtin-rules/ts-import-type.md +42 -0
- package/src/discovery/builtin-rules/ts-no-any.md +56 -0
- package/src/discovery/builtin-rules/ts-no-dynamic-import.md +39 -0
- package/src/discovery/builtin-rules/ts-no-return-type.md +45 -0
- package/src/discovery/builtin-rules/ts-no-tiny-functions.md +50 -0
- package/src/discovery/builtin-rules/ts-promise-with-resolvers.md +65 -0
- package/src/discovery/builtin-rules/ts-set-map.md +28 -0
- package/src/discovery/index.ts +1 -0
- package/src/edit/hashline/block-resolver.ts +14 -0
- package/src/edit/hashline/diff.ts +4 -1
- package/src/edit/hashline/execute.ts +2 -1
- package/src/edit/hashline/index.ts +1 -0
- package/src/eval/py/kernel.ts +37 -15
- package/src/eval/py/runtime.ts +57 -28
- package/src/export/html/template.generated.ts +1 -1
- package/src/export/html/template.js +0 -12
- package/src/export/ttsr.ts +2 -0
- package/src/internal-urls/docs-index.generated.ts +7 -8
- package/src/main.ts +18 -1
- package/src/modes/components/hook-selector.ts +15 -17
- package/src/modes/components/index.ts +1 -0
- package/src/modes/components/segment-track.ts +52 -0
- package/src/modes/components/tips.txt +2 -1
- package/src/modes/components/tool-execution.ts +5 -1
- package/src/modes/components/welcome.ts +47 -42
- package/src/modes/controllers/input-controller.ts +12 -21
- package/src/modes/interactive-mode.ts +17 -5
- package/src/modes/setup-wizard/index.ts +88 -0
- package/src/modes/setup-wizard/scenes/glyph.ts +96 -0
- package/src/modes/setup-wizard/scenes/outro.ts +35 -0
- package/src/modes/setup-wizard/scenes/providers.ts +69 -0
- package/src/modes/setup-wizard/scenes/sign-in.ts +193 -0
- package/src/modes/setup-wizard/scenes/splash.ts +201 -0
- package/src/modes/setup-wizard/scenes/theme.ts +299 -0
- package/src/modes/setup-wizard/scenes/types.ts +48 -0
- package/src/modes/setup-wizard/scenes/web-search.ts +128 -0
- package/src/modes/setup-wizard/wizard-overlay.ts +275 -0
- package/src/modes/theme/shimmer.ts +5 -0
- package/src/modes/theme/theme.ts +44 -20
- package/src/modes/types.ts +6 -1
- package/src/prompts/system/orchestrate-notice.md +1 -1
- package/src/prompts/tools/read.md +4 -0
- package/src/sdk.ts +5 -15
- package/src/slash-commands/builtin-registry.ts +8 -0
- package/src/tiny/device.ts +117 -0
- package/src/tiny/dtype.ts +101 -0
- package/src/tiny/models.ts +7 -6
- package/src/tiny/text.ts +36 -1
- package/src/tiny/title-client.ts +58 -3
- package/src/tiny/worker.ts +93 -29
- package/src/tools/bash.ts +16 -13
- package/src/tools/eval.ts +9 -4
- package/src/tools/index.ts +0 -11
- package/src/tools/read.ts +1 -0
- package/src/tools/renderers.ts +0 -2
- package/src/tui/code-cell.ts +6 -1
- package/src/tui/output-block.ts +199 -38
- package/dist/types/tools/recipe/index.d.ts +0 -46
- package/dist/types/tools/recipe/render.d.ts +0 -36
- package/dist/types/tools/recipe/runner.d.ts +0 -60
- package/dist/types/tools/recipe/runners/cargo.d.ts +0 -16
- package/dist/types/tools/recipe/runners/index.d.ts +0 -2
- package/dist/types/tools/recipe/runners/just.d.ts +0 -2
- package/dist/types/tools/recipe/runners/make.d.ts +0 -2
- package/dist/types/tools/recipe/runners/pkg.d.ts +0 -2
- package/dist/types/tools/recipe/runners/task.d.ts +0 -2
- package/src/prompts/tools/recipe.md +0 -16
- package/src/tools/recipe/index.ts +0 -81
- package/src/tools/recipe/render.ts +0 -19
- package/src/tools/recipe/runner.ts +0 -219
- package/src/tools/recipe/runners/cargo.ts +0 -131
- package/src/tools/recipe/runners/index.ts +0 -8
- package/src/tools/recipe/runners/just.ts +0 -73
- package/src/tools/recipe/runners/make.ts +0 -101
- package/src/tools/recipe/runners/pkg.ts +0 -167
- package/src/tools/recipe/runners/task.ts +0 -72
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
import { padding, type SelectItem, SelectList, truncateToWidth, visibleWidth } from "@oh-my-pi/pi-tui";
|
|
2
|
+
import {
|
|
3
|
+
enableAutoTheme,
|
|
4
|
+
getAvailableThemes,
|
|
5
|
+
getCurrentThemeName,
|
|
6
|
+
getSelectListTheme,
|
|
7
|
+
isLightTheme,
|
|
8
|
+
previewTheme,
|
|
9
|
+
type SymbolPreset,
|
|
10
|
+
setColorBlindMode,
|
|
11
|
+
setSymbolPreset,
|
|
12
|
+
theme,
|
|
13
|
+
} from "../../theme/theme";
|
|
14
|
+
import type { SetupScene, SetupSceneController, SetupSceneHost } from "./types";
|
|
15
|
+
|
|
16
|
+
type ThemeMode = "curated" | "all";
|
|
17
|
+
|
|
18
|
+
const CURATED_ITEMS: readonly SelectItem[] = [
|
|
19
|
+
{ value: "auto", label: "Match terminal", description: "Titanium in dark terminals, Light in light terminals" },
|
|
20
|
+
{ value: "theme:titanium", label: "Titanium", description: "Default dark theme" },
|
|
21
|
+
{ value: "theme:light", label: "Light", description: "Default light theme" },
|
|
22
|
+
{ value: "colorblind", label: "Colorblind colors", description: "Adjust red/green contrast" },
|
|
23
|
+
{ value: "ansi", label: "ANSI-safe", description: "ASCII glyphs with the dark terminal theme" },
|
|
24
|
+
{ value: "browse", label: "Browse all…", description: "Show every built-in and custom theme" },
|
|
25
|
+
];
|
|
26
|
+
|
|
27
|
+
function fitLine(line: string, width: number): string {
|
|
28
|
+
const truncated = truncateToWidth(line, width);
|
|
29
|
+
return truncated + padding(Math.max(0, width - visibleWidth(truncated)));
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function fillStyledLine(content: string, width: number): string {
|
|
33
|
+
return content + padding(Math.max(0, width - visibleWidth(content)));
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function renderMockStatusLine(width: number): string {
|
|
37
|
+
const sep = theme.fg("statusLineSep", ` ${theme.sep.pipe} `);
|
|
38
|
+
const left = [
|
|
39
|
+
theme.fg("statusLineModel", `${theme.icon.model} sonnet`),
|
|
40
|
+
theme.fg("statusLinePath", "~/project"),
|
|
41
|
+
theme.fg("statusLineGitDirty", `${theme.icon.git} main +2`),
|
|
42
|
+
].join(sep);
|
|
43
|
+
const right = [
|
|
44
|
+
theme.fg("statusLineContext", `${theme.icon.context} 42%`),
|
|
45
|
+
theme.fg("statusLineCost", `${theme.icon.cost} 0.18`),
|
|
46
|
+
].join(sep);
|
|
47
|
+
const innerWidth = Math.max(1, width - 2);
|
|
48
|
+
const leftWidth = visibleWidth(left);
|
|
49
|
+
const rightWidth = visibleWidth(right);
|
|
50
|
+
const gap = padding(Math.max(1, innerWidth - leftWidth - rightWidth - 2));
|
|
51
|
+
return theme.bg("statusLineBg", fitLine(` ${left}${gap}${right} `, width));
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function renderMockEditor(width: number): string[] {
|
|
55
|
+
const box = theme.boxRound;
|
|
56
|
+
const innerWidth = Math.max(1, width - 2);
|
|
57
|
+
const horizontal = box.horizontal.repeat(innerWidth);
|
|
58
|
+
const top = theme.fg("borderAccent", `${box.topLeft}${horizontal}${box.topRight}`);
|
|
59
|
+
const bottom = theme.fg("borderMuted", `${box.bottomLeft}${horizontal}${box.bottomRight}`);
|
|
60
|
+
const prompt = `${theme.fg("accent", ">")} ${theme.fg("text", "Ask anything, edit files, run tools")}${theme.inverse(" ")}`;
|
|
61
|
+
const hint = theme.fg("dim", "enter send · shift+enter newline · / commands");
|
|
62
|
+
return [
|
|
63
|
+
top,
|
|
64
|
+
`${theme.fg("borderAccent", box.vertical)}${fitLine(prompt, innerWidth)}${theme.fg("borderAccent", box.vertical)}`,
|
|
65
|
+
`${theme.fg("borderMuted", box.vertical)}${fillStyledLine(hint, innerWidth)}${theme.fg("borderMuted", box.vertical)}`,
|
|
66
|
+
bottom,
|
|
67
|
+
];
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function renderThemePreview(width: number): string[] {
|
|
71
|
+
const previewWidth = Math.max(24, Math.min(width, 88));
|
|
72
|
+
return [
|
|
73
|
+
theme.bold("Preview"),
|
|
74
|
+
`${theme.fg("success", `${theme.status.success} success`)} ${theme.fg("warning", `${theme.status.warning} warning`)} ${theme.fg("error", `${theme.status.error} error`)} ${theme.fg("accent", "accent")}`,
|
|
75
|
+
"",
|
|
76
|
+
theme.fg("muted", "Status line"),
|
|
77
|
+
renderMockStatusLine(previewWidth),
|
|
78
|
+
theme.fg("muted", "Editor"),
|
|
79
|
+
...renderMockEditor(previewWidth),
|
|
80
|
+
];
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
class ThemeSceneController implements SetupSceneController {
|
|
84
|
+
title = "Pick a theme";
|
|
85
|
+
subtitle = "Move through the list to preview; Enter saves the highlighted choice.";
|
|
86
|
+
#mode: ThemeMode = "curated";
|
|
87
|
+
#selectList: SelectList;
|
|
88
|
+
#loadingAllThemes = false;
|
|
89
|
+
#message: string | undefined;
|
|
90
|
+
#previewRequest = 0;
|
|
91
|
+
#disposed = false;
|
|
92
|
+
readonly #originalTheme = getCurrentThemeName();
|
|
93
|
+
readonly #originalSymbolPreset: SymbolPreset;
|
|
94
|
+
readonly #originalColorBlindMode: boolean;
|
|
95
|
+
|
|
96
|
+
constructor(private readonly host: SetupSceneHost) {
|
|
97
|
+
this.#originalSymbolPreset = host.ctx.settings.get("symbolPreset");
|
|
98
|
+
this.#originalColorBlindMode = host.ctx.settings.get("colorBlindMode");
|
|
99
|
+
this.#selectList = this.#createSelectList(CURATED_ITEMS, this.#currentCuratedIndex());
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
dispose(): void {
|
|
103
|
+
this.#disposed = true;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
invalidate(): void {
|
|
107
|
+
this.#selectList.invalidate();
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
handleInput(data: string): void {
|
|
111
|
+
const quickIndex = data >= "1" && data <= "9" ? Number(data) - 1 : -1;
|
|
112
|
+
if (quickIndex >= 0) {
|
|
113
|
+
this.#selectList.setSelectedIndex(quickIndex);
|
|
114
|
+
this.#previewByIndex(quickIndex);
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
this.#selectList.handleInput(data);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
render(width: number): string[] {
|
|
121
|
+
const lines = [
|
|
122
|
+
theme.fg("muted", "Theme changes preview live. Nothing is saved until you press Enter."),
|
|
123
|
+
this.#mode === "all"
|
|
124
|
+
? theme.fg("dim", "Browsing all themes · Esc returns to curated choices")
|
|
125
|
+
: theme.fg("dim", "Esc skips this step"),
|
|
126
|
+
"",
|
|
127
|
+
...renderThemePreview(width),
|
|
128
|
+
"",
|
|
129
|
+
];
|
|
130
|
+
if (this.#loadingAllThemes) {
|
|
131
|
+
lines.push(theme.fg("dim", "Loading themes…"));
|
|
132
|
+
} else {
|
|
133
|
+
lines.push(...this.#selectList.render(width));
|
|
134
|
+
}
|
|
135
|
+
if (this.#message) {
|
|
136
|
+
lines.push("", this.#message);
|
|
137
|
+
}
|
|
138
|
+
return lines;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
#createSelectList(items: readonly SelectItem[], selectedIndex: number): SelectList {
|
|
142
|
+
const list = new SelectList(items, Math.min(10, Math.max(1, items.length)), getSelectListTheme());
|
|
143
|
+
list.setSelectedIndex(selectedIndex);
|
|
144
|
+
list.onSelectionChange = item => {
|
|
145
|
+
void this.#preview(item.value);
|
|
146
|
+
};
|
|
147
|
+
list.onSelect = item => {
|
|
148
|
+
void this.#select(item.value);
|
|
149
|
+
};
|
|
150
|
+
list.onCancel = () => {
|
|
151
|
+
if (this.#mode === "all") {
|
|
152
|
+
this.#mode = "curated";
|
|
153
|
+
this.#selectList = this.#createSelectList(CURATED_ITEMS, this.#currentCuratedIndex());
|
|
154
|
+
this.host.requestRender();
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
this.#restorePreview();
|
|
158
|
+
this.host.finish("skipped");
|
|
159
|
+
};
|
|
160
|
+
return list;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
#currentCuratedIndex(): number {
|
|
164
|
+
const current = getCurrentThemeName();
|
|
165
|
+
if (current === "titanium") return 1;
|
|
166
|
+
if (current === "light") return 2;
|
|
167
|
+
return 0;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
#previewByIndex(index: number): void {
|
|
171
|
+
const items = this.#mode === "curated" ? CURATED_ITEMS : undefined;
|
|
172
|
+
const value = items?.[index]?.value;
|
|
173
|
+
if (value) void this.#preview(value);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
async #select(value: string): Promise<void> {
|
|
177
|
+
if (value === "browse") {
|
|
178
|
+
await this.#showAllThemes();
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
await this.#commit(value);
|
|
182
|
+
this.host.finish("done");
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
async #showAllThemes(): Promise<void> {
|
|
186
|
+
if (this.#loadingAllThemes) return;
|
|
187
|
+
this.#loadingAllThemes = true;
|
|
188
|
+
this.#message = undefined;
|
|
189
|
+
this.host.requestRender();
|
|
190
|
+
try {
|
|
191
|
+
const themes = await getAvailableThemes();
|
|
192
|
+
if (this.#disposed) return;
|
|
193
|
+
const items = themes.map(name => ({
|
|
194
|
+
value: `theme:${name}`,
|
|
195
|
+
label: name,
|
|
196
|
+
description: name === this.#originalTheme ? "current" : undefined,
|
|
197
|
+
}));
|
|
198
|
+
const selectedIndex = Math.max(0, themes.indexOf(this.#originalTheme ?? ""));
|
|
199
|
+
this.#mode = "all";
|
|
200
|
+
this.#selectList = this.#createSelectList(items, selectedIndex);
|
|
201
|
+
} catch (error) {
|
|
202
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
203
|
+
this.#message = theme.fg("error", `Failed to load themes: ${message}`);
|
|
204
|
+
} finally {
|
|
205
|
+
this.#loadingAllThemes = false;
|
|
206
|
+
this.host.requestRender();
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
async #commit(value: string): Promise<void> {
|
|
211
|
+
if (value === "auto") {
|
|
212
|
+
this.host.ctx.settings.set("theme.dark", "titanium");
|
|
213
|
+
this.host.ctx.settings.set("theme.light", "light");
|
|
214
|
+
await this.#applyPreviewPresentation(this.#originalSymbolPreset, this.#originalColorBlindMode);
|
|
215
|
+
enableAutoTheme();
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
if (value === "colorblind") {
|
|
219
|
+
this.host.ctx.settings.set("colorBlindMode", true);
|
|
220
|
+
await this.#applyPreviewPresentation(this.#originalSymbolPreset, true);
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
if (value === "ansi") {
|
|
224
|
+
this.host.ctx.settings.set("symbolPreset", "ascii");
|
|
225
|
+
this.host.ctx.settings.set("theme.dark", "dark-terminal");
|
|
226
|
+
await this.#applyPreviewPresentation("ascii", this.#originalColorBlindMode);
|
|
227
|
+
enableAutoTheme();
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
const themeName = this.#themeNameFromValue(value);
|
|
231
|
+
if (!themeName) return;
|
|
232
|
+
await this.#applyPreviewPresentation(this.#originalSymbolPreset, this.#originalColorBlindMode);
|
|
233
|
+
if (isLightTheme(themeName)) {
|
|
234
|
+
this.host.ctx.settings.set("theme.light", themeName);
|
|
235
|
+
} else {
|
|
236
|
+
this.host.ctx.settings.set("theme.dark", themeName);
|
|
237
|
+
}
|
|
238
|
+
await previewTheme(themeName);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
async #preview(value: string): Promise<void> {
|
|
242
|
+
const request = ++this.#previewRequest;
|
|
243
|
+
this.#message = undefined;
|
|
244
|
+
if (value === "browse") {
|
|
245
|
+
this.host.requestRender();
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
let result: { success: boolean; error?: string } = { success: true };
|
|
250
|
+
if (value === "auto") {
|
|
251
|
+
await this.#applyPreviewPresentation(this.#originalSymbolPreset, this.#originalColorBlindMode);
|
|
252
|
+
enableAutoTheme();
|
|
253
|
+
} else if (value === "colorblind") {
|
|
254
|
+
await this.#applyPreviewPresentation(this.#originalSymbolPreset, true);
|
|
255
|
+
} else if (value === "ansi") {
|
|
256
|
+
await this.#applyPreviewPresentation("ascii", this.#originalColorBlindMode);
|
|
257
|
+
result = await previewTheme("dark-terminal");
|
|
258
|
+
} else {
|
|
259
|
+
const themeName = this.#themeNameFromValue(value);
|
|
260
|
+
if (themeName) {
|
|
261
|
+
await this.#applyPreviewPresentation(this.#originalSymbolPreset, this.#originalColorBlindMode);
|
|
262
|
+
result = await previewTheme(themeName);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
if (request !== this.#previewRequest || this.#disposed) return;
|
|
266
|
+
if (!result.success) {
|
|
267
|
+
this.#message = theme.fg("error", result.error ?? "Theme preview failed");
|
|
268
|
+
}
|
|
269
|
+
this.host.ctx.ui.invalidate();
|
|
270
|
+
this.host.requestRender();
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
async #applyPreviewPresentation(symbolPreset: SymbolPreset, colorBlindMode: boolean): Promise<void> {
|
|
274
|
+
await setSymbolPreset(symbolPreset);
|
|
275
|
+
await setColorBlindMode(colorBlindMode);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
#restorePreview(): void {
|
|
279
|
+
void (async () => {
|
|
280
|
+
await this.#applyPreviewPresentation(this.#originalSymbolPreset, this.#originalColorBlindMode);
|
|
281
|
+
if (this.#originalTheme) {
|
|
282
|
+
await previewTheme(this.#originalTheme);
|
|
283
|
+
}
|
|
284
|
+
this.host.ctx.ui.invalidate();
|
|
285
|
+
this.host.requestRender();
|
|
286
|
+
})();
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
#themeNameFromValue(value: string): string | undefined {
|
|
290
|
+
return value.startsWith("theme:") ? value.slice("theme:".length) : undefined;
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
export const themeSetupScene: SetupScene = {
|
|
295
|
+
id: "theme",
|
|
296
|
+
title: "Pick a theme",
|
|
297
|
+
minVersion: 1,
|
|
298
|
+
mount: host => new ThemeSceneController(host),
|
|
299
|
+
};
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import type { Component } from "@oh-my-pi/pi-tui";
|
|
2
|
+
import type { InteractiveModeContext } from "../../types";
|
|
3
|
+
|
|
4
|
+
export type SetupSceneResult = "done" | "skipped";
|
|
5
|
+
|
|
6
|
+
export interface SetupSceneHost {
|
|
7
|
+
ctx: InteractiveModeContext;
|
|
8
|
+
requestRender(): void;
|
|
9
|
+
finish(result: SetupSceneResult): void;
|
|
10
|
+
setFocus(component: Component | null): void;
|
|
11
|
+
restoreFocus(): void;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface SetupSceneController extends Component {
|
|
15
|
+
title: string;
|
|
16
|
+
subtitle?: string;
|
|
17
|
+
onMount?(): void | Promise<void>;
|
|
18
|
+
onUnmount?(): void;
|
|
19
|
+
dispose?(): void;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* A single panel inside a tabbed setup scene. The host scene owns the tab bar
|
|
24
|
+
* and forwards rendering/input to the active tab.
|
|
25
|
+
*/
|
|
26
|
+
export interface SetupTab {
|
|
27
|
+
readonly id: string;
|
|
28
|
+
readonly label: string;
|
|
29
|
+
/**
|
|
30
|
+
* While `true` the tab owns all keyboard input (e.g. an in-progress OAuth
|
|
31
|
+
* login). The parent scene MUST NOT switch tabs or finish while modal.
|
|
32
|
+
*/
|
|
33
|
+
readonly modal: boolean;
|
|
34
|
+
render(width: number): string[];
|
|
35
|
+
handleInput(data: string): void;
|
|
36
|
+
invalidate(): void;
|
|
37
|
+
/** Called when the tab becomes active (including initial mount). */
|
|
38
|
+
onActivate?(): void;
|
|
39
|
+
dispose(): void;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface SetupScene {
|
|
43
|
+
id: string;
|
|
44
|
+
title: string;
|
|
45
|
+
minVersion: number;
|
|
46
|
+
shouldRun?(ctx: InteractiveModeContext): boolean | Promise<boolean>;
|
|
47
|
+
mount(host: SetupSceneHost): SetupSceneController;
|
|
48
|
+
}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { type SelectItem, SelectList, truncateToWidth } from "@oh-my-pi/pi-tui";
|
|
2
|
+
import { SETTINGS_SCHEMA } from "../../../config/settings-schema";
|
|
3
|
+
import { getSearchProvider, setPreferredSearchProvider } from "../../../web/search/provider";
|
|
4
|
+
import { isSearchProviderPreference, type SearchProviderId } from "../../../web/search/types";
|
|
5
|
+
import { getSelectListTheme, theme } from "../../theme/theme";
|
|
6
|
+
import type { SetupSceneHost, SetupTab } from "./types";
|
|
7
|
+
|
|
8
|
+
const MAX_VISIBLE = 8;
|
|
9
|
+
|
|
10
|
+
/** Reuse the settings schema as the single source of truth for labels/descriptions. */
|
|
11
|
+
const WEB_SEARCH_ITEMS: readonly SelectItem[] = SETTINGS_SCHEMA["providers.webSearch"].ui.options.map(option => ({
|
|
12
|
+
value: option.value,
|
|
13
|
+
label: option.label,
|
|
14
|
+
description: option.description,
|
|
15
|
+
}));
|
|
16
|
+
|
|
17
|
+
type Availability = "checking" | boolean;
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* "Web search" panel: picks the provider the web_search tool should prefer and
|
|
21
|
+
* reports whether the highlighted provider is ready to use given current
|
|
22
|
+
* credentials (env keys or OAuth sign-ins from the Sign in tab).
|
|
23
|
+
*/
|
|
24
|
+
export class WebSearchTab implements SetupTab {
|
|
25
|
+
readonly id = "web-search";
|
|
26
|
+
readonly label = "Web search";
|
|
27
|
+
readonly modal = false;
|
|
28
|
+
|
|
29
|
+
#list: SelectList;
|
|
30
|
+
#availability = new Map<SearchProviderId, Availability>();
|
|
31
|
+
#status: string[] = [];
|
|
32
|
+
#disposed = false;
|
|
33
|
+
|
|
34
|
+
constructor(private readonly host: SetupSceneHost) {
|
|
35
|
+
this.#list = new SelectList(WEB_SEARCH_ITEMS, MAX_VISIBLE, getSelectListTheme());
|
|
36
|
+
const current = host.ctx.settings.get("providers.webSearch");
|
|
37
|
+
const index = WEB_SEARCH_ITEMS.findIndex(item => item.value === current);
|
|
38
|
+
if (index >= 0) this.#list.setSelectedIndex(index);
|
|
39
|
+
this.#list.onSelectionChange = item => this.#onHighlight(item.value);
|
|
40
|
+
this.#list.onSelect = item => this.#apply(item.value);
|
|
41
|
+
this.#list.onCancel = () => host.finish("skipped");
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
onActivate(): void {
|
|
45
|
+
// Auth may have changed in the Sign in tab; re-check from scratch.
|
|
46
|
+
this.#availability.clear();
|
|
47
|
+
this.#status = [];
|
|
48
|
+
const selected = this.#list.getSelectedItem();
|
|
49
|
+
if (selected) this.#onHighlight(selected.value);
|
|
50
|
+
this.host.requestRender();
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
handleInput(data: string): void {
|
|
54
|
+
this.#list.handleInput(data);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
invalidate(): void {
|
|
58
|
+
this.#list.invalidate();
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
dispose(): void {
|
|
62
|
+
this.#disposed = true;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
render(width: number): string[] {
|
|
66
|
+
const lines = [
|
|
67
|
+
theme.fg("muted", "Choose the provider the web_search tool should prefer."),
|
|
68
|
+
"",
|
|
69
|
+
...this.#list.render(width),
|
|
70
|
+
];
|
|
71
|
+
const selected = this.#list.getSelectedItem();
|
|
72
|
+
if (selected) {
|
|
73
|
+
lines.push("", ...this.#readinessLines(selected.value).map(line => truncateToWidth(line, width)));
|
|
74
|
+
}
|
|
75
|
+
if (this.#status.length > 0) {
|
|
76
|
+
lines.push("", ...this.#status.map(line => truncateToWidth(line, width)));
|
|
77
|
+
}
|
|
78
|
+
return lines;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
#onHighlight(value: string): void {
|
|
82
|
+
this.#status = [];
|
|
83
|
+
if (value !== "auto") this.#checkAvailability(value as SearchProviderId);
|
|
84
|
+
this.host.requestRender();
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
#checkAvailability(id: SearchProviderId): void {
|
|
88
|
+
if (this.#availability.has(id)) return;
|
|
89
|
+
this.#availability.set(id, "checking");
|
|
90
|
+
void (async () => {
|
|
91
|
+
let ready = false;
|
|
92
|
+
try {
|
|
93
|
+
const provider = await getSearchProvider(id);
|
|
94
|
+
ready = await provider.isAvailable(this.host.ctx.session.modelRegistry.authStorage);
|
|
95
|
+
} catch {
|
|
96
|
+
ready = false;
|
|
97
|
+
}
|
|
98
|
+
if (this.#disposed) return;
|
|
99
|
+
this.#availability.set(id, ready);
|
|
100
|
+
this.host.requestRender();
|
|
101
|
+
})();
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
#apply(value: string): void {
|
|
105
|
+
if (!isSearchProviderPreference(value)) return;
|
|
106
|
+
this.host.ctx.settings.set("providers.webSearch", value);
|
|
107
|
+
setPreferredSearchProvider(value);
|
|
108
|
+
const label = WEB_SEARCH_ITEMS.find(item => item.value === value)?.label ?? value;
|
|
109
|
+
this.#status = [theme.fg("success", `${theme.status.success} Web search set to ${label}`)];
|
|
110
|
+
if (value !== "auto" && this.#availability.get(value as SearchProviderId) === false) {
|
|
111
|
+
this.#status.push(theme.fg("dim", "Not configured yet — add its API key or sign in to enable it."));
|
|
112
|
+
}
|
|
113
|
+
this.host.requestRender();
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
#readinessLines(value: string): string[] {
|
|
117
|
+
if (value === "auto") {
|
|
118
|
+
return [theme.fg("dim", "Automatically uses the first configured provider.")];
|
|
119
|
+
}
|
|
120
|
+
const state = this.#availability.get(value as SearchProviderId);
|
|
121
|
+
if (state === undefined || state === "checking") {
|
|
122
|
+
return [theme.fg("dim", "Checking availability…")];
|
|
123
|
+
}
|
|
124
|
+
return state
|
|
125
|
+
? [theme.fg("success", `${theme.status.success} Ready to use`)]
|
|
126
|
+
: [theme.fg("warning", `${theme.status.pending} Needs credentials`)];
|
|
127
|
+
}
|
|
128
|
+
}
|