@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.
- 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
package/src/main.ts
CHANGED
|
@@ -49,6 +49,7 @@ import {
|
|
|
49
49
|
} from "./extensibility/plugins/marketplace";
|
|
50
50
|
import type { MCPManager } from "./mcp";
|
|
51
51
|
import { InteractiveMode, runAcpMode, runPrintMode, runRpcMode } from "./modes";
|
|
52
|
+
import { ALL_SCENES, runSetupWizard, selectSetupScenes } from "./modes/setup-wizard";
|
|
52
53
|
import { initTheme, stopThemeWatcher } from "./modes/theme/theme";
|
|
53
54
|
import type { SubmittedUserInput } from "./modes/types";
|
|
54
55
|
import {
|
|
@@ -257,6 +258,8 @@ async function runInteractiveMode(
|
|
|
257
258
|
setExtensionUIContext: (uiContext: ExtensionUIContext, hasUI: boolean) => void,
|
|
258
259
|
lspServers: LspStartupServerInfo[] | undefined,
|
|
259
260
|
mcpManager: MCPManager | undefined,
|
|
261
|
+
resuming: boolean,
|
|
262
|
+
forceSetupWizard: boolean,
|
|
260
263
|
eventBus?: EventBus,
|
|
261
264
|
initialMessage?: string,
|
|
262
265
|
initialImages?: ImageContent[],
|
|
@@ -271,7 +274,18 @@ async function runInteractiveMode(
|
|
|
271
274
|
eventBus,
|
|
272
275
|
);
|
|
273
276
|
|
|
274
|
-
await
|
|
277
|
+
const setupScenes = await selectSetupScenes(settings.get("setupVersion"), ALL_SCENES, mode, {
|
|
278
|
+
resuming,
|
|
279
|
+
isTTY: process.stdin.isTTY && process.stdout.isTTY,
|
|
280
|
+
setupWizardEnabled: settings.get("startup.setupWizard"),
|
|
281
|
+
force: forceSetupWizard,
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
await mode.init({ suppressWelcomeIntro: setupScenes.length > 0 });
|
|
285
|
+
|
|
286
|
+
if (setupScenes.length > 0) {
|
|
287
|
+
await runSetupWizard(mode, setupScenes);
|
|
288
|
+
}
|
|
275
289
|
|
|
276
290
|
versionCheckPromise
|
|
277
291
|
.then(newVersion => {
|
|
@@ -693,6 +707,7 @@ interface RunRootCommandDependencies {
|
|
|
693
707
|
discoverAuthStorage?: typeof discoverAuthStorage;
|
|
694
708
|
runAcpMode?: typeof runAcpMode;
|
|
695
709
|
settings?: Settings;
|
|
710
|
+
forceSetupWizard?: boolean;
|
|
696
711
|
}
|
|
697
712
|
|
|
698
713
|
export async function runRootCommand(
|
|
@@ -1028,6 +1043,8 @@ export async function runRootCommand(
|
|
|
1028
1043
|
setToolUIContext,
|
|
1029
1044
|
lspServers,
|
|
1030
1045
|
mcpManager,
|
|
1046
|
+
Boolean(parsedArgs.continue || parsedArgs.resume || parsedArgs.fork),
|
|
1047
|
+
deps.forceSetupWizard === true,
|
|
1031
1048
|
eventBus,
|
|
1032
1049
|
initialMessage,
|
|
1033
1050
|
initialImages,
|
|
@@ -24,6 +24,7 @@ import {
|
|
|
24
24
|
} from "../../modes/utils/keybinding-matchers";
|
|
25
25
|
import { CountdownTimer } from "./countdown-timer";
|
|
26
26
|
import { DynamicBorder } from "./dynamic-border";
|
|
27
|
+
import { renderSegmentTrack } from "./segment-track";
|
|
27
28
|
|
|
28
29
|
/** One segment of a {@link HookSelectorSlider} — a label, its accent color, and
|
|
29
30
|
* an optional detail line (e.g. the resolved model name) shown beneath the
|
|
@@ -206,28 +207,25 @@ export class HookSelectorComponent extends Container {
|
|
|
206
207
|
}
|
|
207
208
|
}
|
|
208
209
|
|
|
209
|
-
/** Render the slider block
|
|
210
|
-
*
|
|
211
|
-
*
|
|
212
|
-
*
|
|
213
|
-
*
|
|
210
|
+
/** Render the slider block in the style of the status line: each option is a
|
|
211
|
+
* distinctly colored segment, the active one filled as a powerline chip
|
|
212
|
+
* (its accent as the background, a luminance-matched label, flanked by
|
|
213
|
+
* triangle caps) and the rest shown as plain colored labels joined by a thin
|
|
214
|
+
* separator. Edge arrows brighten while there is room to move. When the
|
|
215
|
+
* active segment carries a `detail` (e.g. the resolved model name) a muted
|
|
216
|
+
* second line is appended. Returns one or two `\n`-joined lines. */
|
|
214
217
|
#renderSliderLine(): string {
|
|
215
218
|
const slider = this.#slider;
|
|
216
219
|
if (!slider) return "";
|
|
217
220
|
const segments = slider.segments;
|
|
218
|
-
const
|
|
219
|
-
const track = segments
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
: theme.fg("dim", segment.label),
|
|
224
|
-
)
|
|
225
|
-
.join(sep);
|
|
226
|
-
const leftArrow = theme.fg(this.#sliderIndex > 0 ? "accent" : "dim", "◂");
|
|
227
|
-
const rightArrow = theme.fg(this.#sliderIndex < segments.length - 1 ? "accent" : "dim", "▸");
|
|
221
|
+
const active = this.#sliderIndex;
|
|
222
|
+
const track = renderSegmentTrack(segments, active);
|
|
223
|
+
|
|
224
|
+
const leftArrow = theme.fg(active > 0 ? "accent" : "dim", "◂");
|
|
225
|
+
const rightArrow = theme.fg(active < segments.length - 1 ? "accent" : "dim", "▸");
|
|
228
226
|
const caption = slider.caption ? `${theme.fg("dim", slider.caption)} ` : "";
|
|
229
|
-
const trackLine = `${caption}${leftArrow}
|
|
230
|
-
const detail = segments[
|
|
227
|
+
const trackLine = `${caption}${leftArrow} ${track} ${rightArrow}`;
|
|
228
|
+
const detail = segments[active]?.detail;
|
|
231
229
|
if (!detail) return trackLine;
|
|
232
230
|
return `${trackLine}\n ${theme.fg("dim", "↳")} ${theme.fg("muted", detail)}`;
|
|
233
231
|
}
|
|
@@ -20,6 +20,7 @@ export * from "./model-selector";
|
|
|
20
20
|
export * from "./oauth-selector";
|
|
21
21
|
export * from "./queue-mode-selector";
|
|
22
22
|
export * from "./read-tool-group";
|
|
23
|
+
export * from "./segment-track";
|
|
23
24
|
export * from "./session-selector";
|
|
24
25
|
export * from "./settings-selector";
|
|
25
26
|
export * from "./show-images-selector";
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared renderer for a horizontal row of colored "segments" styled after the
|
|
3
|
+
* status line: each segment shows in its own accent, the active one is filled
|
|
4
|
+
* as a powerline chip (its accent as the background, a luminance-matched label,
|
|
5
|
+
* flanked by triangle caps) and the rest are plain colored labels joined by a
|
|
6
|
+
* thin separator.
|
|
7
|
+
*
|
|
8
|
+
* Used by the plan-mode model-tier slider ({@link HookSelectorComponent}) and
|
|
9
|
+
* the ctrl+p role-cycle status so both surfaces read identically.
|
|
10
|
+
*/
|
|
11
|
+
import { type ThemeColor, theme } from "../theme/theme";
|
|
12
|
+
|
|
13
|
+
export interface TrackSegment {
|
|
14
|
+
label: string;
|
|
15
|
+
/** Theme color for the segment; defaults to `accent`. */
|
|
16
|
+
color?: ThemeColor;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const FG_RESET = "\x1b[39m";
|
|
20
|
+
const BG_RESET = "\x1b[49m";
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Render `segments` as a colored chip track with `activeIndex` filled. Returns
|
|
24
|
+
* a single line of styled text with no surrounding caption or arrows — callers
|
|
25
|
+
* frame it as they need.
|
|
26
|
+
*/
|
|
27
|
+
export function renderSegmentTrack(segments: TrackSegment[], activeIndex: number): string {
|
|
28
|
+
// Powerline triangles point *into* the chip so the colored caps merge with
|
|
29
|
+
// the filled body: left cap points left, right cap points right.
|
|
30
|
+
const capLeft = theme.sep.powerlineRight;
|
|
31
|
+
const capRight = theme.sep.powerlineLeft;
|
|
32
|
+
const thinSep = theme.fg("statusLineSep", theme.sep.powerlineThin);
|
|
33
|
+
|
|
34
|
+
let track = "";
|
|
35
|
+
segments.forEach((segment, i) => {
|
|
36
|
+
if (i > 0) {
|
|
37
|
+
// A thin separator reads cleanly only between two plain labels; the chip
|
|
38
|
+
// caps already delimit the active segment, so pad around it instead.
|
|
39
|
+
track += i === activeIndex || i - 1 === activeIndex ? " " : ` ${thinSep} `;
|
|
40
|
+
}
|
|
41
|
+
const color = segment.color ?? "accent";
|
|
42
|
+
const fg = theme.getFgAnsi(color);
|
|
43
|
+
if (i !== activeIndex) {
|
|
44
|
+
track += `${fg}${segment.label}${FG_RESET}`;
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
const bg = fg.replace("\x1b[38;", "\x1b[48;");
|
|
48
|
+
const label = `${bg}${theme.getContrastFgAnsi(color)}\x1b[1m ${segment.label} \x1b[22m${BG_RESET}`;
|
|
49
|
+
track += `${fg}${capLeft}${label}${fg}${capRight}${FG_RESET}`;
|
|
50
|
+
});
|
|
51
|
+
return track;
|
|
52
|
+
}
|
|
@@ -9,4 +9,5 @@ Did you know? Each kitty/tmux split keeps its own session — `omp -c` resumes t
|
|
|
9
9
|
Drop the word `ultrathink` in your message for harder multi-step reasoning — watch it glow rainbow as you type
|
|
10
10
|
Say `orchestrate` in your message to drive a multi-phase task with parallel subagents — watch it glow as you type
|
|
11
11
|
Log in to several accounts of the same provider — `/login` again — and omp load-balances across them automatically
|
|
12
|
-
Run `omp auth-broker serve` once and every machine pulls live tokens over the wire — refresh keys never leave the host; `omp auth-gateway` fronts it as a drop-in proxy any OpenAI-compatible client can hit
|
|
12
|
+
Run `omp auth-broker serve` once and every machine pulls live tokens over the wire — refresh keys never leave the host; `omp auth-gateway` fronts it as a drop-in proxy any OpenAI-compatible client can hit
|
|
13
|
+
Press alt+p (or /switch) to switch provider, and ctrl+p to cycle role models smol -> slow -> etc
|
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
} from "@oh-my-pi/pi-tui";
|
|
16
16
|
import { getProjectDir, logger, sanitizeText } from "@oh-my-pi/pi-utils";
|
|
17
17
|
import { EDIT_MODE_STRATEGIES, type EditMode, type PerFileDiffPreview } from "../../edit";
|
|
18
|
+
import { shimmerEnabled } from "../../modes/theme/shimmer";
|
|
18
19
|
import type { Theme } from "../../modes/theme/theme";
|
|
19
20
|
import { theme } from "../../modes/theme/theme";
|
|
20
21
|
import { BASH_DEFAULT_PREVIEW_LINES } from "../../tools/bash";
|
|
@@ -380,7 +381,10 @@ export class ToolExecutionComponent extends Container {
|
|
|
380
381
|
this.#toolName === "task" &&
|
|
381
382
|
(this.#result?.details as { async?: { state?: string } } | undefined)?.async?.state === "running";
|
|
382
383
|
const isPartialTask = this.#isPartial && this.#toolName === "task" && !isBackgroundAsyncTask;
|
|
383
|
-
|
|
384
|
+
// Sweep the border of bash/eval execution blocks while they're pending.
|
|
385
|
+
const isPendingExecBlock =
|
|
386
|
+
this.#isPartial && shimmerEnabled() && (this.#toolName === "bash" || this.#toolName === "eval");
|
|
387
|
+
const needsSpinner = isStreamingArgs || isPartialTask || isPendingExecBlock;
|
|
384
388
|
if (needsSpinner && !this.#spinnerInterval) {
|
|
385
389
|
this.#spinnerInterval = setInterval(() => {
|
|
386
390
|
const frameCount = theme.spinnerFrames.length;
|
|
@@ -325,8 +325,7 @@ export class WelcomeComponent implements Component {
|
|
|
325
325
|
}
|
|
326
326
|
}
|
|
327
327
|
|
|
328
|
-
|
|
329
|
-
const PI_LOGO = ["▀██████████▀", " ╘██ ██ ", " ██ ██ ", " ██ ██ ", " ▄██▄ ▄██▄ "];
|
|
328
|
+
export const PI_LOGO = ["▀██████████▀", " ╘██ ██ ", " ██ ██ ", " ██ ██ ", " ▄██▄ ▄██▄ "];
|
|
330
329
|
|
|
331
330
|
/** Multi-stop palette for the diagonal gradient. */
|
|
332
331
|
const GRADIENT_STOPS: ReadonlyArray<readonly [number, number, number]> = [
|
|
@@ -343,63 +342,69 @@ const GRADIENT_RAMP_256 = [199, 171, 135, 99, 75, 51, 87];
|
|
|
343
342
|
/** Half-width of the shine highlight band, expressed in gradient-t units. */
|
|
344
343
|
const SHINE_HALF_WIDTH = 0.18;
|
|
345
344
|
|
|
346
|
-
interface ShineConfig {
|
|
345
|
+
export interface ShineConfig {
|
|
347
346
|
/** Overall opacity of the shine overlay, in [0, 1]. */
|
|
348
347
|
strength: number;
|
|
349
348
|
/** Center of the shine band along the diagonal, in [0, 1]. */
|
|
350
349
|
pos: number;
|
|
351
350
|
}
|
|
352
351
|
|
|
352
|
+
/**
|
|
353
|
+
* Resolve the gradient SGR foreground escape for a normalized position `t`
|
|
354
|
+
* (0..1) along the diagonal, compositing the optional sliding shine highlight.
|
|
355
|
+
* Shared by {@link gradientLogo} and the setup splash so both stay
|
|
356
|
+
* color-identical (truecolor when available, 256-color ramp otherwise).
|
|
357
|
+
*/
|
|
358
|
+
export function gradientEscape(t: number, shine?: ShineConfig): string {
|
|
359
|
+
const shineStrength = shine && shine.strength > 0 ? shine.strength : 0;
|
|
360
|
+
const shinePos = shine ? shine.pos : 0;
|
|
361
|
+
if (TERMINAL.trueColor) {
|
|
362
|
+
// 5-stop palette widens the visible color range and avoids the
|
|
363
|
+
// deep-blue valley a naive HSL lerp falls into.
|
|
364
|
+
const stops = GRADIENT_STOPS;
|
|
365
|
+
const seg = t * (stops.length - 1);
|
|
366
|
+
const i = Math.min(stops.length - 2, Math.floor(seg));
|
|
367
|
+
const f = seg - i;
|
|
368
|
+
const a = stops[i];
|
|
369
|
+
const b = stops[i + 1];
|
|
370
|
+
let r = a[0] + (b[0] - a[0]) * f;
|
|
371
|
+
let g = a[1] + (b[1] - a[1]) * f;
|
|
372
|
+
let bl = a[2] + (b[2] - a[2]) * f;
|
|
373
|
+
if (shineStrength > 0) {
|
|
374
|
+
const dist = Math.abs(t - shinePos);
|
|
375
|
+
const intensity = Math.max(0, 1 - dist / SHINE_HALF_WIDTH) * shineStrength;
|
|
376
|
+
if (intensity > 0) {
|
|
377
|
+
r += (255 - r) * intensity;
|
|
378
|
+
g += (255 - g) * intensity;
|
|
379
|
+
bl += (255 - bl) * intensity;
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
return `\x1b[38;2;${Math.round(r)};${Math.round(g)};${Math.round(bl)}m`;
|
|
383
|
+
}
|
|
384
|
+
const ramp = GRADIENT_RAMP_256;
|
|
385
|
+
let idx = Math.min(ramp.length - 1, Math.max(0, Math.floor(t * (ramp.length - 1) + 0.5)));
|
|
386
|
+
if (shineStrength > 0) {
|
|
387
|
+
const dist = Math.abs(t - shinePos);
|
|
388
|
+
const intensity = Math.max(0, 1 - dist / SHINE_HALF_WIDTH) * shineStrength;
|
|
389
|
+
// Promote to the brightest ramp slot when the shine band peaks here.
|
|
390
|
+
if (intensity > 0.5) idx = ramp.length - 1;
|
|
391
|
+
}
|
|
392
|
+
return `\x1b[38;5;${ramp[idx]}m`;
|
|
393
|
+
}
|
|
394
|
+
|
|
353
395
|
/**
|
|
354
396
|
* Apply a multi-stop diagonal gradient (bottom-left → top-right) plus an
|
|
355
397
|
* optional sliding shine band across multi-line art. `phase` (0..1) shifts the
|
|
356
398
|
* gradient along the diagonal, wrapping at 1. When `shine` is provided, a soft
|
|
357
399
|
* white highlight is composited on top, centered at `shine.pos`.
|
|
358
400
|
*/
|
|
359
|
-
function gradientLogo(lines: readonly string[], phase = 0, shine?: ShineConfig): string[] {
|
|
401
|
+
export function gradientLogo(lines: readonly string[], phase = 0, shine?: ShineConfig): string[] {
|
|
360
402
|
const reset = "\x1b[0m";
|
|
361
403
|
const rows = lines.length;
|
|
362
404
|
const cols = Math.max(...lines.map(l => l.length));
|
|
363
405
|
// span+1 so `base` stays strictly < 1: avoids the wrap-around at the
|
|
364
406
|
// far corner mapping back to t=0 (hot pink) on the resting frame.
|
|
365
407
|
const span = Math.max(1, cols + rows - 1);
|
|
366
|
-
const shineStrength = shine && shine.strength > 0 ? shine.strength : 0;
|
|
367
|
-
const shinePos = shine ? shine.pos : 0;
|
|
368
|
-
const colorAt = TERMINAL.trueColor
|
|
369
|
-
? (t: number): string => {
|
|
370
|
-
// 5-stop palette widens the visible color range and avoids the
|
|
371
|
-
// deep-blue valley a naive HSL lerp falls into.
|
|
372
|
-
const stops = GRADIENT_STOPS;
|
|
373
|
-
const seg = t * (stops.length - 1);
|
|
374
|
-
const i = Math.min(stops.length - 2, Math.floor(seg));
|
|
375
|
-
const f = seg - i;
|
|
376
|
-
const a = stops[i];
|
|
377
|
-
const b = stops[i + 1];
|
|
378
|
-
let r = a[0] + (b[0] - a[0]) * f;
|
|
379
|
-
let g = a[1] + (b[1] - a[1]) * f;
|
|
380
|
-
let bl = a[2] + (b[2] - a[2]) * f;
|
|
381
|
-
if (shineStrength > 0) {
|
|
382
|
-
const dist = Math.abs(t - shinePos);
|
|
383
|
-
const intensity = Math.max(0, 1 - dist / SHINE_HALF_WIDTH) * shineStrength;
|
|
384
|
-
if (intensity > 0) {
|
|
385
|
-
r += (255 - r) * intensity;
|
|
386
|
-
g += (255 - g) * intensity;
|
|
387
|
-
bl += (255 - bl) * intensity;
|
|
388
|
-
}
|
|
389
|
-
}
|
|
390
|
-
return `\x1b[38;2;${Math.round(r)};${Math.round(g)};${Math.round(bl)}m`;
|
|
391
|
-
}
|
|
392
|
-
: (t: number): string => {
|
|
393
|
-
const ramp = GRADIENT_RAMP_256;
|
|
394
|
-
let idx = Math.min(ramp.length - 1, Math.max(0, Math.floor(t * (ramp.length - 1) + 0.5)));
|
|
395
|
-
if (shineStrength > 0) {
|
|
396
|
-
const dist = Math.abs(t - shinePos);
|
|
397
|
-
const intensity = Math.max(0, 1 - dist / SHINE_HALF_WIDTH) * shineStrength;
|
|
398
|
-
// Promote to the brightest ramp slot when the shine band peaks here.
|
|
399
|
-
if (intensity > 0.5) idx = ramp.length - 1;
|
|
400
|
-
}
|
|
401
|
-
return `\x1b[38;5;${ramp[idx]}m`;
|
|
402
|
-
};
|
|
403
408
|
return lines.map((line, y) => {
|
|
404
409
|
let result = "";
|
|
405
410
|
for (let x = 0; x < line.length; x++) {
|
|
@@ -411,7 +416,7 @@ function gradientLogo(lines: readonly string[], phase = 0, shine?: ShineConfig):
|
|
|
411
416
|
// Diagonal: bottom-left (x=0, y=rows-1) → top-right (x=cols-1, y=0)
|
|
412
417
|
const base = (x + (rows - 1 - y)) / span;
|
|
413
418
|
const t = (((base + phase) % 1) + 1) % 1;
|
|
414
|
-
result +=
|
|
419
|
+
result += gradientEscape(t, shine) + char + reset;
|
|
415
420
|
}
|
|
416
421
|
return result;
|
|
417
422
|
});
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import * as fs from "node:fs/promises";
|
|
2
|
-
import {
|
|
2
|
+
import type { AgentMessage } from "@oh-my-pi/pi-agent-core";
|
|
3
3
|
import type { AutocompleteProvider, SlashCommand } from "@oh-my-pi/pi-tui";
|
|
4
4
|
import { $env, sanitizeText } from "@oh-my-pi/pi-utils";
|
|
5
|
+
import { getRoleInfo } from "../../config/model-registry";
|
|
5
6
|
import { isSettingsInitialized, settings } from "../../config/settings";
|
|
7
|
+
import { renderSegmentTrack } from "../../modes/components/segment-track";
|
|
6
8
|
import { TinyTitleDownloadProgressComponent } from "../../modes/components/tiny-title-download-progress";
|
|
7
9
|
import { expandEmoticons } from "../../modes/emoji-autocomplete";
|
|
8
10
|
import { createPromptActionAutocompleteProvider } from "../../modes/prompt-action-autocomplete";
|
|
@@ -769,27 +771,16 @@ export class InputController {
|
|
|
769
771
|
|
|
770
772
|
this.ctx.statusLine.invalidate();
|
|
771
773
|
this.ctx.updateEditorBorderColor();
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
const cycleSeparator = theme.fg("dim", " > ");
|
|
780
|
-
const cycleLabel = cycleOrder
|
|
781
|
-
.map(role => {
|
|
782
|
-
if (role === result.role) {
|
|
783
|
-
return theme.bold(theme.fg("accent", role));
|
|
784
|
-
}
|
|
785
|
-
return theme.fg("muted", role);
|
|
786
|
-
})
|
|
787
|
-
.join(cycleSeparator);
|
|
788
|
-
const orderLabel = ` (cycle: ${cycleLabel})`;
|
|
789
|
-
this.ctx.showStatus(
|
|
790
|
-
`Switched to ${roleLabelStyled}: ${result.model.name || result.model.id}${thinkingStr}${tempLabel}${orderLabel}`,
|
|
791
|
-
{ dim: false },
|
|
774
|
+
// The status line already reports the resolved model + thinking level, so
|
|
775
|
+
// the cycle status is just a status-line-style chip track (active role
|
|
776
|
+
// filled), matching the plan-approval model slider. A dim suffix flags a
|
|
777
|
+
// temporary switch since that isn't shown elsewhere.
|
|
778
|
+
const track = renderSegmentTrack(
|
|
779
|
+
cycleOrder.map(role => ({ label: role, color: getRoleInfo(role, settings).color })),
|
|
780
|
+
cycleOrder.indexOf(result.role),
|
|
792
781
|
);
|
|
782
|
+
const tempLabel = options?.temporary ? theme.fg("dim", " (temporary)") : "";
|
|
783
|
+
this.ctx.showStatus(`${track}${tempLabel}`, { dim: false });
|
|
793
784
|
} catch (error) {
|
|
794
785
|
this.ctx.showError(error instanceof Error ? error.message : String(error));
|
|
795
786
|
}
|
|
@@ -116,7 +116,14 @@ import {
|
|
|
116
116
|
onThemeChange,
|
|
117
117
|
theme,
|
|
118
118
|
} from "./theme/theme";
|
|
119
|
-
import type {
|
|
119
|
+
import type {
|
|
120
|
+
CompactionQueuedMessage,
|
|
121
|
+
InteractiveModeContext,
|
|
122
|
+
InteractiveModeInitOptions,
|
|
123
|
+
SubmittedUserInput,
|
|
124
|
+
TodoItem,
|
|
125
|
+
TodoPhase,
|
|
126
|
+
} from "./types";
|
|
120
127
|
import { UiHelpers } from "./utils/ui-helpers";
|
|
121
128
|
|
|
122
129
|
const HINT_SHIMMER_PALETTE: ShimmerPalette = {
|
|
@@ -368,7 +375,7 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
368
375
|
this.ui.requestRender(true);
|
|
369
376
|
};
|
|
370
377
|
this.editor.onAutocompleteUpdate = () => {
|
|
371
|
-
this.ui.requestRender();
|
|
378
|
+
this.ui.requestRender(false, { allowUnknownViewportMutation: true });
|
|
372
379
|
};
|
|
373
380
|
this.#syncEditorMaxHeight();
|
|
374
381
|
this.#resizeHandler = () => {
|
|
@@ -431,7 +438,10 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
431
438
|
this.#observerRegistry = new SessionObserverRegistry();
|
|
432
439
|
}
|
|
433
440
|
|
|
434
|
-
|
|
441
|
+
playWelcomeIntro(): void {
|
|
442
|
+
this.#welcomeComponent?.playIntro(() => this.ui.requestRender());
|
|
443
|
+
}
|
|
444
|
+
async init(options: InteractiveModeInitOptions = {}): Promise<void> {
|
|
435
445
|
if (this.isInitialized) return;
|
|
436
446
|
|
|
437
447
|
this.keybindings = logger.time("InteractiveMode.init:keybindings", () => KeybindingsManager.create());
|
|
@@ -489,7 +499,9 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
489
499
|
this.ui.addChild(new Spacer(1));
|
|
490
500
|
this.ui.addChild(this.#welcomeComponent);
|
|
491
501
|
this.ui.addChild(new Spacer(1));
|
|
492
|
-
|
|
502
|
+
if (!options.suppressWelcomeIntro) {
|
|
503
|
+
this.playWelcomeIntro();
|
|
504
|
+
}
|
|
493
505
|
|
|
494
506
|
// Add changelog if provided
|
|
495
507
|
if (this.#changelogMarkdown) {
|
|
@@ -2259,7 +2271,7 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
2259
2271
|
this.ui.requestRender(true);
|
|
2260
2272
|
};
|
|
2261
2273
|
nextEditor.onAutocompleteUpdate = () => {
|
|
2262
|
-
this.ui.requestRender();
|
|
2274
|
+
this.ui.requestRender(false, { allowUnknownViewportMutation: true });
|
|
2263
2275
|
};
|
|
2264
2276
|
nextEditor.setMaxHeight(this.#computeEditorMaxHeight());
|
|
2265
2277
|
if (this.historyStorage) {
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import type { Settings } from "../../config/settings";
|
|
2
|
+
import type { InteractiveModeContext } from "../types";
|
|
3
|
+
import { glyphSetupScene } from "./scenes/glyph";
|
|
4
|
+
import { providersSetupScene } from "./scenes/providers";
|
|
5
|
+
import { themeSetupScene } from "./scenes/theme";
|
|
6
|
+
import type { SetupScene } from "./scenes/types";
|
|
7
|
+
import { SetupWizardComponent } from "./wizard-overlay";
|
|
8
|
+
|
|
9
|
+
export type { SetupScene, SetupSceneController, SetupSceneHost, SetupSceneResult } from "./scenes/types";
|
|
10
|
+
|
|
11
|
+
export const ALL_SCENES = [
|
|
12
|
+
providersSetupScene,
|
|
13
|
+
glyphSetupScene,
|
|
14
|
+
themeSetupScene,
|
|
15
|
+
] as const satisfies readonly SetupScene[];
|
|
16
|
+
|
|
17
|
+
export const CURRENT_SETUP_VERSION = ALL_SCENES.reduce((max, scene) => Math.max(max, scene.minVersion), 0);
|
|
18
|
+
|
|
19
|
+
export interface SetupSceneSelectionOptions {
|
|
20
|
+
resuming?: boolean;
|
|
21
|
+
isTTY?: boolean;
|
|
22
|
+
skipEnv?: string;
|
|
23
|
+
setupWizardEnabled?: boolean;
|
|
24
|
+
force?: boolean;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function setupSkipEnvEnabled(value: string | undefined): boolean {
|
|
28
|
+
if (value === undefined) return false;
|
|
29
|
+
const normalized = value.trim().toLowerCase();
|
|
30
|
+
return normalized !== "" && normalized !== "0" && normalized !== "false" && normalized !== "no";
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export async function selectSetupScenes(
|
|
34
|
+
storedVersion: number,
|
|
35
|
+
scenes: readonly SetupScene[],
|
|
36
|
+
ctx?: InteractiveModeContext,
|
|
37
|
+
options: SetupSceneSelectionOptions = {},
|
|
38
|
+
): Promise<SetupScene[]> {
|
|
39
|
+
const isTTY = options.isTTY ?? (process.stdin.isTTY && process.stdout.isTTY);
|
|
40
|
+
if (!isTTY) return [];
|
|
41
|
+
if (!options.force) {
|
|
42
|
+
if (options.resuming) return [];
|
|
43
|
+
if (setupSkipEnvEnabled(options.skipEnv ?? Bun.env.OMP_SKIP_SETUP)) return [];
|
|
44
|
+
if (options.setupWizardEnabled === false) return [];
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const selected: SetupScene[] = [];
|
|
48
|
+
for (const scene of scenes) {
|
|
49
|
+
if (!options.force && scene.minVersion <= storedVersion) continue;
|
|
50
|
+
if (scene.shouldRun) {
|
|
51
|
+
if (!ctx) continue;
|
|
52
|
+
if (!(await scene.shouldRun(ctx))) continue;
|
|
53
|
+
}
|
|
54
|
+
selected.push(scene);
|
|
55
|
+
}
|
|
56
|
+
return selected;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export async function markSetupWizardComplete(
|
|
60
|
+
settings: Settings,
|
|
61
|
+
version: number = CURRENT_SETUP_VERSION,
|
|
62
|
+
): Promise<void> {
|
|
63
|
+
settings.set("setupVersion", version);
|
|
64
|
+
await settings.flush();
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export async function runSetupWizard(
|
|
68
|
+
ctx: InteractiveModeContext,
|
|
69
|
+
scenes: readonly SetupScene[] = ALL_SCENES,
|
|
70
|
+
): Promise<void> {
|
|
71
|
+
if (scenes.length === 0) return;
|
|
72
|
+
const component = new SetupWizardComponent(ctx, scenes);
|
|
73
|
+
const overlay = ctx.ui.showOverlay(component, {
|
|
74
|
+
width: "100%",
|
|
75
|
+
maxHeight: "100%",
|
|
76
|
+
anchor: "top-left",
|
|
77
|
+
margin: 0,
|
|
78
|
+
});
|
|
79
|
+
try {
|
|
80
|
+
await component.run();
|
|
81
|
+
await markSetupWizardComplete(ctx.settings);
|
|
82
|
+
} finally {
|
|
83
|
+
component.dispose();
|
|
84
|
+
ctx.ui.setFocus(component);
|
|
85
|
+
overlay.hide();
|
|
86
|
+
}
|
|
87
|
+
ctx.playWelcomeIntro();
|
|
88
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { type SelectItem, SelectList } from "@oh-my-pi/pi-tui";
|
|
2
|
+
import { getSelectListTheme, type SymbolPreset, setSymbolPreset, theme } from "../../theme/theme";
|
|
3
|
+
import type { SetupScene, SetupSceneController, SetupSceneHost } from "./types";
|
|
4
|
+
|
|
5
|
+
const GLYPH_PRESETS = ["nerd", "unicode", "ascii"] as const satisfies readonly SymbolPreset[];
|
|
6
|
+
|
|
7
|
+
const GLYPH_LABELS: Readonly<Record<SymbolPreset, string>> = {
|
|
8
|
+
nerd: "Nerd Font",
|
|
9
|
+
unicode: "Unicode",
|
|
10
|
+
ascii: "ASCII",
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
const GLYPH_SAMPLES: Readonly<Record<SymbolPreset, string>> = {
|
|
14
|
+
nerd: " ",
|
|
15
|
+
unicode: "✔ ✖ 📁 ⬢ ╭─╮ ├─ • ⠋ →",
|
|
16
|
+
ascii: "[ok] [x] > + [D] +-+ |-- * ->",
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
/** One picker row per preset; the description column shows live sample glyphs instead of prose. */
|
|
20
|
+
const GLYPH_ITEMS: readonly SelectItem[] = GLYPH_PRESETS.map((preset, index) => ({
|
|
21
|
+
value: preset,
|
|
22
|
+
label: `${index + 1} ${GLYPH_LABELS[preset]}`,
|
|
23
|
+
description: preset === "nerd" ? `${GLYPH_SAMPLES.nerd} ╭─╮ ├─ ◆ ✔ ✖` : GLYPH_SAMPLES[preset],
|
|
24
|
+
}));
|
|
25
|
+
|
|
26
|
+
class GlyphSceneController implements SetupSceneController {
|
|
27
|
+
title = "Choose glyph mode";
|
|
28
|
+
subtitle = "Pick the row that renders cleanly in your terminal.";
|
|
29
|
+
#selectList: SelectList;
|
|
30
|
+
#previewRequest = 0;
|
|
31
|
+
#committing = false;
|
|
32
|
+
|
|
33
|
+
constructor(private readonly host: SetupSceneHost) {
|
|
34
|
+
this.#selectList = new SelectList(GLYPH_ITEMS, GLYPH_ITEMS.length, getSelectListTheme());
|
|
35
|
+
const current = theme.getSymbolPreset();
|
|
36
|
+
const currentIndex = GLYPH_PRESETS.indexOf(current);
|
|
37
|
+
this.#selectList.setSelectedIndex(currentIndex >= 0 ? currentIndex : 0);
|
|
38
|
+
this.#selectList.onSelectionChange = item => {
|
|
39
|
+
this.#preview(item.value as SymbolPreset);
|
|
40
|
+
};
|
|
41
|
+
this.#selectList.onSelect = item => {
|
|
42
|
+
void this.#commit(item.value as SymbolPreset);
|
|
43
|
+
};
|
|
44
|
+
this.#selectList.onCancel = () => host.finish("skipped");
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
invalidate(): void {
|
|
48
|
+
this.#selectList.invalidate();
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
handleInput(data: string): void {
|
|
52
|
+
if (this.#committing) return;
|
|
53
|
+
const quickIndex = data >= "1" && data <= "3" ? Number(data) - 1 : -1;
|
|
54
|
+
if (quickIndex >= 0) {
|
|
55
|
+
const preset = GLYPH_PRESETS[quickIndex];
|
|
56
|
+
this.#selectList.setSelectedIndex(quickIndex);
|
|
57
|
+
this.#preview(preset);
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
this.#selectList.handleInput(data);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
render(width: number): string[] {
|
|
64
|
+
return [
|
|
65
|
+
theme.fg("muted", "If a row shows boxes, tofu, or misaligned icons, pick another."),
|
|
66
|
+
"",
|
|
67
|
+
...this.#selectList.render(width),
|
|
68
|
+
];
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
async #commit(preset: SymbolPreset): Promise<void> {
|
|
72
|
+
if (this.#committing) return;
|
|
73
|
+
this.#committing = true;
|
|
74
|
+
this.#previewRequest += 1;
|
|
75
|
+
this.host.ctx.settings.set("symbolPreset", preset);
|
|
76
|
+
await setSymbolPreset(preset);
|
|
77
|
+
this.host.ctx.ui.invalidate();
|
|
78
|
+
this.host.finish("done");
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
#preview(preset: SymbolPreset): void {
|
|
82
|
+
const request = ++this.#previewRequest;
|
|
83
|
+
void setSymbolPreset(preset).then(() => {
|
|
84
|
+
if (request !== this.#previewRequest || this.#committing) return;
|
|
85
|
+
this.host.ctx.ui.invalidate();
|
|
86
|
+
this.host.requestRender();
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export const glyphSetupScene: SetupScene = {
|
|
92
|
+
id: "glyph-mode",
|
|
93
|
+
title: "Choose glyph mode",
|
|
94
|
+
minVersion: 1,
|
|
95
|
+
mount: host => new GlyphSceneController(host),
|
|
96
|
+
};
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { padding, truncateToWidth, visibleWidth } from "@oh-my-pi/pi-tui";
|
|
2
|
+
import { gradientLogo, PI_LOGO } from "../../components/welcome";
|
|
3
|
+
import { theme } from "../../theme/theme";
|
|
4
|
+
import { renderStarfield, SETUP_TICK_MS } from "./splash";
|
|
5
|
+
|
|
6
|
+
export const SETUP_OUTRO_MS = 1200;
|
|
7
|
+
|
|
8
|
+
function centerLine(line: string, width: number): string {
|
|
9
|
+
const lineWidth = visibleWidth(line);
|
|
10
|
+
if (lineWidth >= width) return truncateToWidth(line, width);
|
|
11
|
+
const left = Math.floor((width - lineWidth) / 2);
|
|
12
|
+
return padding(left) + line + padding(width - left - lineWidth);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function clampLine(line: string, width: number): string {
|
|
16
|
+
const truncated = truncateToWidth(line, width);
|
|
17
|
+
return truncated + padding(Math.max(0, width - visibleWidth(truncated)));
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function renderSetupOutro(width: number, height: number, elapsedMs: number): string[] {
|
|
21
|
+
const frame = Math.floor(elapsedMs / SETUP_TICK_MS);
|
|
22
|
+
const lines = renderStarfield(width, height, frame + 1000);
|
|
23
|
+
const progress = Math.max(0, Math.min(1, elapsedMs / SETUP_OUTRO_MS));
|
|
24
|
+
const logo = gradientLogo(PI_LOGO, progress * 1.2, { pos: (progress * 2) % 1, strength: 1 - progress });
|
|
25
|
+
const title = theme.bold(theme.fg("success", `${theme.status.success} Setup saved`));
|
|
26
|
+
const subtitle = theme.fg("muted", "Handing off to the normal CLI…");
|
|
27
|
+
const sweepWidth = Math.max(1, Math.min(width - 8, Math.floor((width - 8) * progress)));
|
|
28
|
+
const sweep = `${theme.fg("accent", "━".repeat(sweepWidth))}${theme.fg("dim", "─".repeat(Math.max(0, width - 8 - sweepWidth)))}`;
|
|
29
|
+
const content = [...logo, "", title, subtitle, "", sweep];
|
|
30
|
+
const start = Math.max(0, Math.floor((height - content.length) / 2));
|
|
31
|
+
for (let i = 0; i < content.length && start + i < lines.length; i++) {
|
|
32
|
+
lines[start + i] = centerLine(content[i] ?? "", width);
|
|
33
|
+
}
|
|
34
|
+
return lines.map(line => clampLine(line, width));
|
|
35
|
+
}
|