@oh-my-pi/pi-coding-agent 15.5.15 → 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 +81 -0
- package/dist/types/capability/rule-buckets.d.ts +30 -0
- package/dist/types/capability/rule.d.ts +7 -0
- package/dist/types/cli/classify-install-target.d.ts +0 -10
- package/dist/types/cli/completion-gen.d.ts +80 -0
- package/dist/types/cli/initial-message.d.ts +1 -1
- package/dist/types/cli/tiny-models-cli.d.ts +9 -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/commands/tiny-models.d.ts +22 -0
- package/dist/types/commit/analysis/conventional.d.ts +1 -1
- package/dist/types/commit/analysis/summary.d.ts +1 -1
- package/dist/types/commit/changelog/generate.d.ts +1 -1
- package/dist/types/commit/changelog/index.d.ts +2 -2
- package/dist/types/commit/map-reduce/map-phase.d.ts +1 -1
- package/dist/types/commit/map-reduce/reduce-phase.d.ts +1 -1
- package/dist/types/config/model-id-affixes.d.ts +10 -0
- package/dist/types/config/settings-schema.d.ts +402 -17
- 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/helpers.d.ts +1 -1
- package/dist/types/discovery/index.d.ts +1 -0
- package/dist/types/discovery/substitute-plugin-root.d.ts +0 -4
- 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/js/shared/rewrite-imports.d.ts +16 -1
- 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/internal-urls/agent-protocol.d.ts +2 -1
- package/dist/types/internal-urls/artifact-protocol.d.ts +2 -1
- package/dist/types/internal-urls/local-protocol.d.ts +2 -1
- package/dist/types/internal-urls/memory-protocol.d.ts +2 -1
- package/dist/types/internal-urls/omp-protocol.d.ts +2 -1
- package/dist/types/internal-urls/router.d.ts +8 -1
- package/dist/types/internal-urls/rule-protocol.d.ts +2 -1
- package/dist/types/internal-urls/skill-protocol.d.ts +2 -1
- package/dist/types/internal-urls/types.d.ts +26 -0
- package/dist/types/main.d.ts +1 -0
- package/dist/types/memory-backend/index.d.ts +1 -0
- package/dist/types/memory-backend/resolve.d.ts +2 -1
- package/dist/types/memory-backend/types.d.ts +7 -1
- package/dist/types/mnemosyne/backend.d.ts +4 -0
- package/dist/types/mnemosyne/config.d.ts +29 -0
- package/dist/types/mnemosyne/index.d.ts +3 -0
- package/dist/types/mnemosyne/state.d.ts +72 -0
- package/dist/types/modes/components/custom-editor.d.ts +2 -3
- package/dist/types/modes/components/hook-selector.d.ts +27 -0
- package/dist/types/modes/components/index.d.ts +2 -0
- package/dist/types/modes/components/segment-track.d.ts +22 -0
- package/dist/types/modes/components/status-line/context-thresholds.d.ts +6 -0
- package/dist/types/modes/components/tiny-title-download-progress.d.ts +11 -0
- package/dist/types/modes/components/welcome.d.ts +22 -0
- package/dist/types/modes/controllers/extension-ui-controller.d.ts +4 -1
- package/dist/types/modes/gradient-highlight.d.ts +23 -0
- package/dist/types/modes/interactive-mode.d.ts +7 -4
- package/dist/types/modes/internal-url-autocomplete.d.ts +43 -0
- package/dist/types/modes/orchestrate.d.ts +10 -0
- 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/defaults/index.d.ts +8406 -8406
- 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/modes/ultrathink.d.ts +3 -3
- package/dist/types/modes/utils/keybinding-matchers.d.ts +5 -0
- package/dist/types/sdk.d.ts +3 -0
- package/dist/types/session/agent-session.d.ts +33 -0
- package/dist/types/system-prompt.d.ts +2 -0
- package/dist/types/task/executor.d.ts +2 -0
- package/dist/types/task/render.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 +185 -0
- package/dist/types/tiny/text.d.ts +19 -0
- package/dist/types/tiny/title-client.d.ts +32 -0
- package/dist/types/tiny/title-protocol.d.ts +74 -0
- package/dist/types/tiny/worker.d.ts +2 -0
- package/dist/types/tools/bash.d.ts +3 -2
- package/dist/types/tools/eval.d.ts +1 -1
- package/dist/types/tools/index.d.ts +7 -4
- package/dist/types/tools/memory-edit.d.ts +40 -0
- package/dist/types/tools/{hindsight-recall.d.ts → memory-recall.d.ts} +6 -6
- package/dist/types/tools/{hindsight-reflect.d.ts → memory-reflect.d.ts} +6 -6
- package/dist/types/tools/memory-render.d.ts +60 -0
- package/dist/types/tools/{hindsight-retain.d.ts → memory-retain.d.ts} +6 -6
- package/dist/types/tools/todo-write.d.ts +8 -0
- package/dist/types/tools/tool-result.d.ts +2 -0
- package/dist/types/tui/code-cell.d.ts +2 -0
- package/dist/types/tui/output-block.d.ts +17 -0
- package/dist/types/utils/title-generator.d.ts +3 -0
- package/package.json +18 -14
- package/scripts/build-binary.ts +1 -0
- 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/tiny-models-cli.ts +127 -0
- package/src/cli-commands.ts +3 -0
- package/src/cli.ts +9 -15
- package/src/commands/complete.ts +66 -0
- package/src/commands/completions.ts +60 -0
- package/src/commands/setup.ts +29 -4
- package/src/commands/tiny-models.ts +36 -0
- package/src/config/model-equivalence.ts +43 -2
- package/src/config/model-id-affixes.ts +64 -0
- package/src/config/model-registry.ts +84 -10
- package/src/config/settings-schema.ts +275 -15
- 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 +9 -8
- package/src/edit/hashline/execute.ts +2 -1
- package/src/edit/hashline/index.ts +1 -0
- package/src/eval/__tests__/shared-executors.test.ts +36 -0
- package/src/eval/js/shared/local-module-loader.ts +13 -1
- package/src/eval/js/shared/rewrite-imports.ts +31 -26
- 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/agent-protocol.ts +18 -1
- package/src/internal-urls/artifact-protocol.ts +19 -1
- package/src/internal-urls/docs-index.generated.ts +8 -7
- package/src/internal-urls/local-protocol.ts +14 -1
- package/src/internal-urls/memory-protocol.ts +6 -1
- package/src/internal-urls/omp-protocol.ts +5 -1
- package/src/internal-urls/router.ts +20 -1
- package/src/internal-urls/rule-protocol.ts +8 -1
- package/src/internal-urls/skill-protocol.ts +8 -1
- package/src/internal-urls/types.ts +27 -0
- package/src/lsp/render.ts +1 -1
- package/src/main.ts +18 -1
- package/src/mcp/oauth-flow.ts +2 -2
- package/src/memory-backend/index.ts +1 -0
- package/src/memory-backend/resolve.ts +4 -1
- package/src/memory-backend/types.ts +8 -1
- package/src/mnemosyne/backend.ts +374 -0
- package/src/mnemosyne/config.ts +160 -0
- package/src/mnemosyne/index.ts +3 -0
- package/src/mnemosyne/state.ts +548 -0
- package/src/modes/acp/acp-agent.ts +11 -6
- package/src/modes/components/agent-dashboard.ts +4 -4
- package/src/modes/components/custom-editor.ts +3 -2
- package/src/modes/components/diff.ts +2 -2
- package/src/modes/components/extensions/extension-list.ts +3 -2
- package/src/modes/components/footer.ts +5 -6
- package/src/modes/components/history-search.ts +3 -3
- package/src/modes/components/hook-selector.ts +92 -8
- package/src/modes/components/index.ts +2 -0
- package/src/modes/components/mcp-add-wizard.ts +3 -3
- package/src/modes/components/model-selector.ts +5 -4
- package/src/modes/components/oauth-selector.ts +3 -3
- package/src/modes/components/segment-track.ts +52 -0
- package/src/modes/components/session-observer-overlay.ts +19 -13
- package/src/modes/components/session-selector.ts +3 -3
- package/src/modes/components/settings-defs.ts +7 -0
- package/src/modes/components/status-line/context-thresholds.ts +11 -0
- package/src/modes/components/status-line/segments.ts +2 -2
- package/src/modes/components/tiny-title-download-progress.ts +90 -0
- package/src/modes/components/tips.txt +13 -0
- package/src/modes/components/tool-execution.ts +72 -4
- package/src/modes/components/tree-selector.ts +3 -3
- package/src/modes/components/user-message-selector.ts +3 -3
- package/src/modes/components/welcome.ts +102 -43
- package/src/modes/controllers/command-controller.ts +16 -1
- package/src/modes/controllers/extension-ui-controller.ts +3 -1
- package/src/modes/controllers/input-controller.ts +69 -21
- package/src/modes/gradient-highlight.ts +70 -0
- package/src/modes/interactive-mode.ts +75 -114
- package/src/modes/internal-url-autocomplete.ts +143 -0
- package/src/modes/orchestrate.ts +36 -0
- package/src/modes/prompt-action-autocomplete.ts +12 -0
- 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/modes/ultrathink.ts +9 -53
- package/src/modes/utils/keybinding-matchers.ts +11 -0
- package/src/prompts/system/memory-consolidation-system.md +8 -0
- package/src/prompts/system/memory-extraction-system.md +26 -0
- package/src/prompts/{commands/orchestrate.md → system/orchestrate-notice.md} +6 -17
- package/src/prompts/system/system-prompt.md +2 -0
- package/src/prompts/system/tiny-title-system.md +8 -0
- package/src/prompts/tools/memory-edit.md +8 -0
- package/src/prompts/tools/read.md +4 -0
- package/src/prompts/tools/task.md +4 -7
- package/src/sdk.ts +13 -21
- package/src/session/agent-session.ts +128 -44
- package/src/slash-commands/builtin-registry.ts +18 -1
- package/src/system-prompt.ts +4 -0
- package/src/task/commands.ts +1 -5
- package/src/task/executor.ts +8 -0
- package/src/task/index.ts +2 -0
- package/src/task/render.ts +69 -26
- package/src/tiny/device.ts +117 -0
- package/src/tiny/dtype.ts +101 -0
- package/src/tiny/models.ts +218 -0
- package/src/tiny/text.ts +54 -0
- package/src/tiny/title-client.ts +395 -0
- package/src/tiny/title-protocol.ts +51 -0
- package/src/tiny/worker.ts +587 -0
- package/src/tools/bash.ts +74 -29
- package/src/tools/browser/tab-worker.ts +1 -1
- package/src/tools/eval.ts +9 -4
- package/src/tools/index.ts +17 -22
- package/src/tools/memory-edit.ts +59 -0
- package/src/tools/memory-recall.ts +100 -0
- package/src/tools/memory-reflect.ts +88 -0
- package/src/tools/memory-render.ts +185 -0
- package/src/tools/memory-retain.ts +91 -0
- package/src/tools/read.ts +1 -0
- package/src/tools/renderers.ts +4 -2
- package/src/tools/todo-write.ts +128 -29
- package/src/tools/tool-result.ts +8 -0
- package/src/tui/code-cell.ts +6 -1
- package/src/tui/output-block.ts +199 -38
- package/src/utils/title-generator.ts +115 -13
- 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/hindsight-recall.ts +0 -69
- package/src/tools/hindsight-reflect.ts +0 -58
- package/src/tools/hindsight-retain.ts +0 -57
- 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,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
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { TabBar } from "@oh-my-pi/pi-tui";
|
|
2
|
+
import { getTabBarTheme } from "../../shared";
|
|
3
|
+
import { SignInTab } from "./sign-in";
|
|
4
|
+
import type { SetupScene, SetupSceneController, SetupSceneHost, SetupTab } from "./types";
|
|
5
|
+
import { WebSearchTab } from "./web-search";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Tabbed "Set up your providers" scene. Composes independent panels (model
|
|
9
|
+
* sign-in, web search) behind a {@link TabBar}; the active panel owns
|
|
10
|
+
* rendering and input, while modal panels (e.g. an in-flight OAuth login)
|
|
11
|
+
* temporarily suppress tab switching.
|
|
12
|
+
*/
|
|
13
|
+
class ProvidersSceneController implements SetupSceneController {
|
|
14
|
+
title = "Set up your providers";
|
|
15
|
+
subtitle = "Sign in and pick a web search provider. Press Esc when you're done.";
|
|
16
|
+
|
|
17
|
+
#tabs: SetupTab[];
|
|
18
|
+
#tabBar: TabBar;
|
|
19
|
+
|
|
20
|
+
constructor(host: SetupSceneHost) {
|
|
21
|
+
this.#tabs = [new SignInTab(host), new WebSearchTab(host)];
|
|
22
|
+
this.#tabBar = new TabBar(
|
|
23
|
+
"Providers",
|
|
24
|
+
this.#tabs.map(tab => ({ id: tab.id, label: tab.label })),
|
|
25
|
+
getTabBarTheme(),
|
|
26
|
+
);
|
|
27
|
+
this.#tabBar.onTabChange = () => {
|
|
28
|
+
this.#activeTab().onActivate?.();
|
|
29
|
+
host.requestRender();
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
#activeTab(): SetupTab {
|
|
34
|
+
return this.#tabs[this.#tabBar.getActiveIndex()] ?? this.#tabs[0];
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
onMount(): void {
|
|
38
|
+
this.#activeTab().onActivate?.();
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
invalidate(): void {
|
|
42
|
+
for (const tab of this.#tabs) tab.invalidate();
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
handleInput(data: string): void {
|
|
46
|
+
const tab = this.#activeTab();
|
|
47
|
+
if (tab.modal) {
|
|
48
|
+
tab.handleInput(data);
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
if (this.#tabBar.handleInput(data)) return;
|
|
52
|
+
tab.handleInput(data);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
render(width: number): string[] {
|
|
56
|
+
return [...this.#tabBar.render(width), "", ...this.#activeTab().render(width)];
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
dispose(): void {
|
|
60
|
+
for (const tab of this.#tabs) tab.dispose();
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export const providersSetupScene: SetupScene = {
|
|
65
|
+
id: "providers",
|
|
66
|
+
title: "Set up your providers",
|
|
67
|
+
minVersion: 1,
|
|
68
|
+
mount: host => new ProvidersSceneController(host),
|
|
69
|
+
};
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
import type { AuthStorage } from "@oh-my-pi/pi-ai";
|
|
2
|
+
import type { OAuthProvider } from "@oh-my-pi/pi-ai/utils/oauth/types";
|
|
3
|
+
import { Input, matchesKey, truncateToWidth } from "@oh-my-pi/pi-tui";
|
|
4
|
+
import { getAgentDbPath } from "@oh-my-pi/pi-utils";
|
|
5
|
+
import { OAuthSelectorComponent } from "../../components/oauth-selector";
|
|
6
|
+
import { theme } from "../../theme/theme";
|
|
7
|
+
import type { SetupSceneHost, SetupTab } from "./types";
|
|
8
|
+
|
|
9
|
+
/** Providers whose OAuth flow needs a pasted code/redirect URL rather than a callback server. */
|
|
10
|
+
const CALLBACK_SERVER_PROVIDERS: Partial<Record<OAuthProvider, true>> = {
|
|
11
|
+
anthropic: true,
|
|
12
|
+
"openai-codex": true,
|
|
13
|
+
"gitlab-duo": true,
|
|
14
|
+
"google-gemini-cli": true,
|
|
15
|
+
"google-antigravity": true,
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
interface PromptState {
|
|
19
|
+
message: string;
|
|
20
|
+
placeholder?: string;
|
|
21
|
+
input: Input;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* "Sign in" panel: lets the user authenticate one or more model providers via
|
|
26
|
+
* OAuth. Unlike a standalone scene it never auto-advances the wizard — the user
|
|
27
|
+
* may sign in to several providers and then continue with Esc.
|
|
28
|
+
*/
|
|
29
|
+
export class SignInTab implements SetupTab {
|
|
30
|
+
readonly id = "sign-in";
|
|
31
|
+
readonly label = "Sign in";
|
|
32
|
+
|
|
33
|
+
#authStorage: AuthStorage;
|
|
34
|
+
#selector: OAuthSelectorComponent;
|
|
35
|
+
#statusLines: string[] = [];
|
|
36
|
+
#prompt: PromptState | undefined;
|
|
37
|
+
#promptResolve: ((value: string) => void) | undefined;
|
|
38
|
+
#loginAbort: AbortController | undefined;
|
|
39
|
+
#loggingInProvider: string | undefined;
|
|
40
|
+
#disposed = false;
|
|
41
|
+
|
|
42
|
+
constructor(private readonly host: SetupSceneHost) {
|
|
43
|
+
this.#authStorage = host.ctx.session.modelRegistry.authStorage;
|
|
44
|
+
this.#selector = this.#createSelector();
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Modal while an OAuth flow is running so the scene won't switch tabs or finish. */
|
|
48
|
+
get modal(): boolean {
|
|
49
|
+
return this.#loggingInProvider !== undefined;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
dispose(): void {
|
|
53
|
+
this.#disposed = true;
|
|
54
|
+
this.#selector.stopValidation();
|
|
55
|
+
this.#loginAbort?.abort();
|
|
56
|
+
this.#resolvePrompt("");
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
invalidate(): void {
|
|
60
|
+
this.#selector.invalidate();
|
|
61
|
+
this.#prompt?.input.invalidate();
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
handleInput(data: string): void {
|
|
65
|
+
if (this.#loggingInProvider) {
|
|
66
|
+
if (matchesKey(data, "escape") || matchesKey(data, "ctrl+c")) {
|
|
67
|
+
this.#loginAbort?.abort();
|
|
68
|
+
}
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
this.#selector.handleInput(data);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
render(width: number): string[] {
|
|
75
|
+
const lines = [theme.fg("muted", "Pick a provider to sign in — you can connect more than one."), ""];
|
|
76
|
+
if (this.#loggingInProvider) {
|
|
77
|
+
lines.push(theme.bold(`Signing in to ${this.#loggingInProvider}`), "");
|
|
78
|
+
} else {
|
|
79
|
+
lines.push(...this.#selector.render(width));
|
|
80
|
+
}
|
|
81
|
+
if (this.#statusLines.length > 0) {
|
|
82
|
+
lines.push("", ...this.#statusLines.map(line => truncateToWidth(line, width)));
|
|
83
|
+
}
|
|
84
|
+
if (this.#prompt) {
|
|
85
|
+
lines.push("", theme.fg("warning", this.#prompt.message));
|
|
86
|
+
if (this.#prompt.placeholder) {
|
|
87
|
+
lines.push(theme.fg("dim", this.#prompt.placeholder));
|
|
88
|
+
}
|
|
89
|
+
lines.push(this.#prompt.input.render(width)[0] ?? "");
|
|
90
|
+
}
|
|
91
|
+
return lines;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
#createSelector(): OAuthSelectorComponent {
|
|
95
|
+
return new OAuthSelectorComponent(
|
|
96
|
+
"login",
|
|
97
|
+
this.#authStorage,
|
|
98
|
+
providerId => {
|
|
99
|
+
void this.#login(providerId);
|
|
100
|
+
},
|
|
101
|
+
() => this.host.finish("skipped"),
|
|
102
|
+
{ requestRender: () => this.host.requestRender() },
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
async #login(providerId: string): Promise<void> {
|
|
107
|
+
if (this.#loggingInProvider || this.#disposed) return;
|
|
108
|
+
const useManualInput = CALLBACK_SERVER_PROVIDERS[providerId as OAuthProvider] === true;
|
|
109
|
+
this.#selector.stopValidation();
|
|
110
|
+
this.#loggingInProvider = providerId;
|
|
111
|
+
this.#statusLines = [theme.fg("dim", "Starting OAuth flow…")];
|
|
112
|
+
this.#loginAbort = new AbortController();
|
|
113
|
+
this.host.restoreFocus();
|
|
114
|
+
this.host.requestRender();
|
|
115
|
+
try {
|
|
116
|
+
await this.#authStorage.login(providerId as OAuthProvider, {
|
|
117
|
+
signal: this.#loginAbort.signal,
|
|
118
|
+
onAuth: info => {
|
|
119
|
+
this.#statusLines.push(theme.fg("accent", `Open this URL: ${info.url}`));
|
|
120
|
+
if (info.instructions) {
|
|
121
|
+
this.#statusLines.push(theme.fg("warning", info.instructions));
|
|
122
|
+
}
|
|
123
|
+
if (useManualInput) {
|
|
124
|
+
this.#statusLines.push(theme.fg("dim", "Paste the returned code or redirect URL when prompted."));
|
|
125
|
+
}
|
|
126
|
+
this.host.ctx.openInBrowser(info.url);
|
|
127
|
+
this.host.requestRender();
|
|
128
|
+
},
|
|
129
|
+
onPrompt: prompt => this.#showPrompt(prompt),
|
|
130
|
+
onProgress: message => {
|
|
131
|
+
this.#statusLines.push(theme.fg("dim", message));
|
|
132
|
+
this.host.requestRender();
|
|
133
|
+
},
|
|
134
|
+
onManualCodeInput: () =>
|
|
135
|
+
this.#showPrompt({ message: "Paste the authorization code (or full redirect URL):" }),
|
|
136
|
+
});
|
|
137
|
+
await this.host.ctx.session.modelRegistry.refresh();
|
|
138
|
+
if (this.#disposed) return;
|
|
139
|
+
this.#statusLines = [
|
|
140
|
+
theme.fg("success", `${theme.status.success} Signed in to ${providerId}`),
|
|
141
|
+
theme.fg("dim", `Credentials saved to ${getAgentDbPath()}`),
|
|
142
|
+
];
|
|
143
|
+
this.#loggingInProvider = undefined;
|
|
144
|
+
this.#loginAbort = undefined;
|
|
145
|
+
this.#selector.stopValidation();
|
|
146
|
+
this.#selector = this.#createSelector();
|
|
147
|
+
this.host.restoreFocus();
|
|
148
|
+
this.host.requestRender();
|
|
149
|
+
} catch (error) {
|
|
150
|
+
if (this.#disposed) return;
|
|
151
|
+
if (this.#loginAbort?.signal.aborted) {
|
|
152
|
+
this.#statusLines = [theme.fg("dim", "Login cancelled.")];
|
|
153
|
+
} else {
|
|
154
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
155
|
+
this.#statusLines = [
|
|
156
|
+
theme.fg("error", `Login failed: ${message}`),
|
|
157
|
+
theme.fg("dim", "Choose another provider or press Esc to continue."),
|
|
158
|
+
];
|
|
159
|
+
}
|
|
160
|
+
this.#loggingInProvider = undefined;
|
|
161
|
+
this.#loginAbort = undefined;
|
|
162
|
+
this.host.restoreFocus();
|
|
163
|
+
this.host.requestRender();
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
#showPrompt(prompt: { message: string; placeholder?: string }): Promise<string> {
|
|
168
|
+
this.#resolvePrompt("");
|
|
169
|
+
const input = new Input();
|
|
170
|
+
const pending = Promise.withResolvers<string>();
|
|
171
|
+
this.#promptResolve = pending.resolve;
|
|
172
|
+
this.#prompt = { message: prompt.message, placeholder: prompt.placeholder, input };
|
|
173
|
+
input.onSubmit = value => {
|
|
174
|
+
this.#resolvePrompt(value);
|
|
175
|
+
};
|
|
176
|
+
input.onEscape = () => {
|
|
177
|
+
this.#resolvePrompt("");
|
|
178
|
+
};
|
|
179
|
+
this.host.setFocus(input);
|
|
180
|
+
this.host.requestRender();
|
|
181
|
+
return pending.promise;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
#resolvePrompt(value: string): void {
|
|
185
|
+
const resolve = this.#promptResolve;
|
|
186
|
+
if (!resolve) return;
|
|
187
|
+
this.#promptResolve = undefined;
|
|
188
|
+
this.#prompt = undefined;
|
|
189
|
+
this.host.restoreFocus();
|
|
190
|
+
resolve(value);
|
|
191
|
+
this.host.requestRender();
|
|
192
|
+
}
|
|
193
|
+
}
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
import { padding, truncateToWidth, visibleWidth } from "@oh-my-pi/pi-tui";
|
|
2
|
+
import { gradientEscape, gradientLogo, PI_LOGO, type ShineConfig } from "../../components/welcome";
|
|
3
|
+
import { theme } from "../../theme/theme";
|
|
4
|
+
|
|
5
|
+
export const SETUP_SPLASH_MS = 2600;
|
|
6
|
+
export const SETUP_TICK_MS = 33;
|
|
7
|
+
|
|
8
|
+
/** Brand mark at 2x: every glyph doubled horizontally, every row doubled vertically. */
|
|
9
|
+
const LARGE_LOGO = PI_LOGO.flatMap(line => {
|
|
10
|
+
let wide = "";
|
|
11
|
+
for (const char of line) {
|
|
12
|
+
wide += char === " " ? " " : `${char}${char}`;
|
|
13
|
+
}
|
|
14
|
+
return [wide, wide];
|
|
15
|
+
});
|
|
16
|
+
const LOGO_WIDTH = Math.max(...LARGE_LOGO.map(line => visibleWidth(line)));
|
|
17
|
+
const LOGO_HEIGHT = LARGE_LOGO.length;
|
|
18
|
+
const RESET = "\x1b[0m";
|
|
19
|
+
|
|
20
|
+
/** Full scene needs comfortable room; below this we drop to a centered mark. */
|
|
21
|
+
const MIN_SCENE_WIDTH = 56;
|
|
22
|
+
const MIN_SCENE_HEIGHT = 22;
|
|
23
|
+
|
|
24
|
+
const SKIP_HINT = "press enter to skip";
|
|
25
|
+
|
|
26
|
+
/** Density ramp for the rippling water, lightest → heaviest. */
|
|
27
|
+
const WATER_RAMP = [
|
|
28
|
+
{ min: 0.62, char: "█" },
|
|
29
|
+
{ min: 0.5, char: "▓" },
|
|
30
|
+
{ min: 0.36, char: "▒" },
|
|
31
|
+
{ min: 0.24, char: "░" },
|
|
32
|
+
];
|
|
33
|
+
|
|
34
|
+
function clampLine(line: string, width: number): string {
|
|
35
|
+
const truncated = truncateToWidth(line, width);
|
|
36
|
+
return truncated + padding(Math.max(0, width - visibleWidth(truncated)));
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function centerLine(line: string, width: number): string {
|
|
40
|
+
const lineWidth = visibleWidth(line);
|
|
41
|
+
if (lineWidth >= width) return truncateToWidth(line, width);
|
|
42
|
+
const left = Math.floor((width - lineWidth) / 2);
|
|
43
|
+
return padding(left) + line + padding(width - left - lineWidth);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function starAt(x: number, y: number, frame: number): string {
|
|
47
|
+
const hash = (x * 73856093) ^ (y * 19349663) ^ (frame * 83492791);
|
|
48
|
+
const bucket = Math.abs(hash) % 97;
|
|
49
|
+
if (bucket === 0) return theme.fg("accent", "✦");
|
|
50
|
+
if (bucket === 1) return theme.fg("muted", "·");
|
|
51
|
+
return " ";
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function renderStarfield(width: number, height: number, frame: number): string[] {
|
|
55
|
+
const lines: string[] = [];
|
|
56
|
+
for (let y = 0; y < height; y++) {
|
|
57
|
+
let line = "";
|
|
58
|
+
for (let x = 0; x < width; x++) {
|
|
59
|
+
line += starAt(x, y, frame >> 3);
|
|
60
|
+
}
|
|
61
|
+
lines.push(line);
|
|
62
|
+
}
|
|
63
|
+
return lines;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Continuous diagonal gradient position (bottom-left → top-right) across the whole screen. */
|
|
67
|
+
function screenGradientT(x: number, y: number, width: number, height: number, phase: number): number {
|
|
68
|
+
const span = Math.max(1, width + height - 1);
|
|
69
|
+
const base = (x + (height - 1 - y)) / span;
|
|
70
|
+
return (((base + phase) % 1) + 1) % 1;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Twinkling sparkle for the upper "sky". Returns a styled glyph, or null for empty space. */
|
|
74
|
+
function skyGlyph(x: number, y: number, frame: number): string | null {
|
|
75
|
+
const hash = (x * 73856093) ^ (y * 19349663) ^ (frame * 83492791);
|
|
76
|
+
const bucket = Math.abs(hash) % 150;
|
|
77
|
+
if (bucket === 0) return theme.fg("accent", "✦");
|
|
78
|
+
if (bucket === 1) return theme.fg("border", "✧");
|
|
79
|
+
if (bucket === 2) return theme.fg("border", "·");
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** Static value-jitter in [0,1) that softens the water's threshold banding. */
|
|
84
|
+
function waterJitter(x: number, y: number): number {
|
|
85
|
+
let h = Math.imul(x, 374761393) + Math.imul(y, 668265263);
|
|
86
|
+
h = Math.imul(h ^ (h >>> 13), 1274126177);
|
|
87
|
+
h ^= h >>> 16;
|
|
88
|
+
return (h >>> 0) / 4294967296;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Rippling water amplitude in [0,1] at (x, y): three travelling sine waves
|
|
93
|
+
* interfere, then a radial edge falloff and a downward fade concentrate the
|
|
94
|
+
* ripples beneath the mark and dissolve them toward the edges/bottom. `t`
|
|
95
|
+
* advances each tick, so the surface drifts.
|
|
96
|
+
*/
|
|
97
|
+
function waterAmplitude(
|
|
98
|
+
x: number,
|
|
99
|
+
y: number,
|
|
100
|
+
cx: number,
|
|
101
|
+
waterTop: number,
|
|
102
|
+
waterHeight: number,
|
|
103
|
+
width: number,
|
|
104
|
+
t: number,
|
|
105
|
+
): number {
|
|
106
|
+
const dx = (x - cx) / 2;
|
|
107
|
+
const dy = y - waterTop;
|
|
108
|
+
const dist = Math.sqrt(dx * dx + dy * dy);
|
|
109
|
+
const wave =
|
|
110
|
+
0.5 * Math.sin(dist * 0.55 - t) +
|
|
111
|
+
0.3 * Math.sin(x * 0.22 + y * 0.45 - t * 0.7) +
|
|
112
|
+
0.2 * Math.sin(Math.abs(dx) * 0.8 + dy * 0.5 - t * 1.4);
|
|
113
|
+
const level = 0.5 + 0.5 * wave;
|
|
114
|
+
const edge = Math.max(0, 1 - Math.abs(x - cx) / (width * 0.5));
|
|
115
|
+
const fade = Math.max(0, 1 - (dy / Math.max(1, waterHeight)) * 0.55);
|
|
116
|
+
return level * edge ** 0.7 * fade;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Animated setup splash, in the spirit of the omp landing page: the brand π
|
|
121
|
+
* mark rendered with the live diagonal gradient + shine sweep, rising out of a
|
|
122
|
+
* rippling, gradient-lit water surface, under a faint twinkling starfield. The
|
|
123
|
+
* mark and water share one continuous gradient so the sweep reads across the
|
|
124
|
+
* whole scene; the water surface drifts each frame.
|
|
125
|
+
*/
|
|
126
|
+
export function renderSetupSplash(width: number, height: number, elapsedMs: number): string[] {
|
|
127
|
+
const w = Math.max(1, width);
|
|
128
|
+
const h = Math.max(1, height);
|
|
129
|
+
const progress = Math.max(0, Math.min(1, elapsedMs / SETUP_SPLASH_MS));
|
|
130
|
+
const phase = progress * 1.8;
|
|
131
|
+
const shine: ShineConfig = { pos: (progress * 2.5) % 1, strength: Math.max(0, 1 - progress * 0.35) };
|
|
132
|
+
|
|
133
|
+
if (w < MIN_SCENE_WIDTH || h < MIN_SCENE_HEIGHT) return renderCompactSplash(w, h, phase, shine);
|
|
134
|
+
|
|
135
|
+
const frame = Math.floor(elapsedMs / SETUP_TICK_MS);
|
|
136
|
+
const cx = Math.floor(w / 2);
|
|
137
|
+
const surfaceTime = frame * 0.13;
|
|
138
|
+
|
|
139
|
+
const cells: string[][] = Array.from({ length: h }, () => new Array<string>(w).fill(" "));
|
|
140
|
+
const put = (x: number, y: number, glyph: string): void => {
|
|
141
|
+
if (y >= 0 && y < h && x >= 0 && x < w) cells[y][x] = glyph;
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
const hx = Math.floor((w - LOGO_WIDTH) / 2);
|
|
145
|
+
const hy = Math.max(2, Math.floor(h * 0.16));
|
|
146
|
+
const waterTop = hy + LOGO_HEIGHT;
|
|
147
|
+
const waterHeight = Math.max(1, h - waterTop);
|
|
148
|
+
|
|
149
|
+
// 1. rippling water surface (shares the screen-wide gradient with the mark)
|
|
150
|
+
for (let y = waterTop; y < h; y++) {
|
|
151
|
+
for (let x = 0; x < w; x++) {
|
|
152
|
+
const amp = waterAmplitude(x, y, cx, waterTop, waterHeight, w, surfaceTime) + (waterJitter(x, y) - 0.5) * 0.06;
|
|
153
|
+
const cell = WATER_RAMP.find(step => amp > step.min);
|
|
154
|
+
if (cell) put(x, y, gradientEscape(screenGradientT(x, y, w, h, phase), shine) + cell.char + RESET);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
// 2. twinkling starfield in the sky above the water
|
|
158
|
+
for (let y = 0; y < waterTop - 1; y++) {
|
|
159
|
+
for (let x = 0; x < w; x++) {
|
|
160
|
+
const star = skyGlyph(x, y, frame >> 3);
|
|
161
|
+
if (star) put(x, y, star);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
// 3. hero — the brand mark with the live gradient + shine sweep
|
|
165
|
+
LARGE_LOGO.forEach((line, row) => {
|
|
166
|
+
let col = 0;
|
|
167
|
+
for (const ch of line) {
|
|
168
|
+
if (ch !== " ") {
|
|
169
|
+
put(
|
|
170
|
+
hx + col,
|
|
171
|
+
hy + row,
|
|
172
|
+
gradientEscape(screenGradientT(hx + col, hy + row, w, h, phase), shine) + ch + RESET,
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
col++;
|
|
176
|
+
}
|
|
177
|
+
});
|
|
178
|
+
// 4. skip hint on a cleared strip at the bottom so it stays legible over the water
|
|
179
|
+
const hintWidth = visibleWidth(SKIP_HINT);
|
|
180
|
+
const hintStart = Math.floor((w - hintWidth) / 2);
|
|
181
|
+
const hintRow = h - 1;
|
|
182
|
+
for (let x = hintStart - 1; x <= hintStart + hintWidth; x++) put(x, hintRow, " ");
|
|
183
|
+
let col = hintStart;
|
|
184
|
+
for (const ch of SKIP_HINT) put(col++, hintRow, ch === " " ? " " : theme.fg("dim", ch));
|
|
185
|
+
|
|
186
|
+
return cells.map(row => row.join(""));
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/** Centered fallback for windows too small to hold the full scene. */
|
|
190
|
+
function renderCompactSplash(width: number, height: number, phase: number, shine: ShineConfig): string[] {
|
|
191
|
+
const art = height >= 14 ? LARGE_LOGO : PI_LOGO;
|
|
192
|
+
const content = [...gradientLogo(art, phase, shine), "", theme.bold("O h M y P i")];
|
|
193
|
+
const start = Math.max(0, Math.floor((height - content.length) / 2));
|
|
194
|
+
const lines: string[] = [];
|
|
195
|
+
for (let y = 0; y < height; y++) {
|
|
196
|
+
const item = content[y - start];
|
|
197
|
+
lines.push(clampLine(item !== undefined ? centerLine(item, width) : "", width));
|
|
198
|
+
}
|
|
199
|
+
if (height > 2) lines[height - 2] = clampLine(centerLine(theme.fg("dim", SKIP_HINT), width), width);
|
|
200
|
+
return lines;
|
|
201
|
+
}
|