horizon-code 0.1.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/assets/python/highlights.scm +137 -0
- package/assets/python/tree-sitter-python.wasm +0 -0
- package/bin/horizon.js +2 -0
- package/package.json +40 -0
- package/src/ai/client.ts +369 -0
- package/src/ai/system-prompt.ts +86 -0
- package/src/app.ts +1454 -0
- package/src/chat/messages.ts +48 -0
- package/src/chat/renderer.ts +243 -0
- package/src/chat/types.ts +18 -0
- package/src/components/code-panel.ts +329 -0
- package/src/components/footer.ts +72 -0
- package/src/components/hooks-panel.ts +224 -0
- package/src/components/input-bar.ts +193 -0
- package/src/components/mode-bar.ts +245 -0
- package/src/components/session-panel.ts +294 -0
- package/src/components/settings-panel.ts +372 -0
- package/src/components/splash.ts +156 -0
- package/src/components/strategy-panel.ts +489 -0
- package/src/components/tab-bar.ts +112 -0
- package/src/components/tutorial-panel.ts +680 -0
- package/src/components/widgets/progress-bar.ts +38 -0
- package/src/components/widgets/sparkline.ts +57 -0
- package/src/hooks/executor.ts +109 -0
- package/src/index.ts +22 -0
- package/src/keys/handler.ts +198 -0
- package/src/platform/auth.ts +36 -0
- package/src/platform/client.ts +159 -0
- package/src/platform/config.ts +121 -0
- package/src/platform/session-sync.ts +158 -0
- package/src/platform/supabase.ts +376 -0
- package/src/platform/sync.ts +149 -0
- package/src/platform/tiers.ts +103 -0
- package/src/platform/tools.ts +163 -0
- package/src/platform/types.ts +86 -0
- package/src/platform/usage.ts +224 -0
- package/src/research/apis.ts +367 -0
- package/src/research/tools.ts +205 -0
- package/src/research/widgets.ts +523 -0
- package/src/state/store.ts +256 -0
- package/src/state/types.ts +109 -0
- package/src/strategy/ascii-chart.ts +74 -0
- package/src/strategy/code-stream.ts +146 -0
- package/src/strategy/dashboard.ts +140 -0
- package/src/strategy/persistence.ts +82 -0
- package/src/strategy/prompts.ts +626 -0
- package/src/strategy/sandbox.ts +137 -0
- package/src/strategy/tools.ts +764 -0
- package/src/strategy/validator.ts +216 -0
- package/src/strategy/widgets.ts +270 -0
- package/src/syntax/setup.ts +54 -0
- package/src/theme/colors.ts +107 -0
- package/src/theme/icons.ts +27 -0
- package/src/util/hyperlink.ts +21 -0
|
@@ -0,0 +1,372 @@
|
|
|
1
|
+
// Settings panel — interactive settings with toggles, sliders, and selectors
|
|
2
|
+
|
|
3
|
+
import { BoxRenderable, TextRenderable, type CliRenderer } from "@opentui/core";
|
|
4
|
+
import { COLORS } from "../theme/colors.ts";
|
|
5
|
+
|
|
6
|
+
import type { ThemeName } from "../theme/colors.ts";
|
|
7
|
+
|
|
8
|
+
export interface HorizonSettings {
|
|
9
|
+
autoFocus: boolean;
|
|
10
|
+
verbosity: "short" | "normal" | "verbose";
|
|
11
|
+
modelPower: "fast" | "standard" | "pro" | "ultra";
|
|
12
|
+
autoCompact: boolean;
|
|
13
|
+
compactThreshold: number;
|
|
14
|
+
showToolCalls: boolean;
|
|
15
|
+
soundEnabled: boolean;
|
|
16
|
+
theme: ThemeName;
|
|
17
|
+
payAsYouGo: boolean;
|
|
18
|
+
liveMode: boolean;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const DEFAULT_SETTINGS: HorizonSettings = {
|
|
22
|
+
autoFocus: true,
|
|
23
|
+
verbosity: "normal",
|
|
24
|
+
modelPower: "standard",
|
|
25
|
+
autoCompact: true,
|
|
26
|
+
compactThreshold: 80,
|
|
27
|
+
showToolCalls: true,
|
|
28
|
+
soundEnabled: false,
|
|
29
|
+
theme: "dark",
|
|
30
|
+
payAsYouGo: false,
|
|
31
|
+
liveMode: false,
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
interface SettingDef {
|
|
35
|
+
key: keyof HorizonSettings | "__divider" | "__delete_chats";
|
|
36
|
+
label: string;
|
|
37
|
+
description: string;
|
|
38
|
+
type: "toggle" | "slider" | "select" | "divider" | "action";
|
|
39
|
+
options?: string[];
|
|
40
|
+
min?: number;
|
|
41
|
+
max?: number;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const SETTINGS_DEFS: SettingDef[] = [
|
|
45
|
+
{
|
|
46
|
+
key: "autoFocus", label: "Auto Focus",
|
|
47
|
+
description: "Focus input bar after sending a message",
|
|
48
|
+
type: "toggle",
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
key: "verbosity", label: "Response Style",
|
|
52
|
+
description: "Short = terse, Normal = default, Verbose = detailed",
|
|
53
|
+
type: "select", options: ["short", "normal", "verbose"],
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
key: "modelPower", label: "Intelligence",
|
|
57
|
+
description: "Higher = smarter, drains budget faster (0.25x/1x/2x/4x)",
|
|
58
|
+
type: "select", options: ["fast", "standard", "pro", "ultra"],
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
key: "autoCompact", label: "Smart Compact",
|
|
62
|
+
description: "Auto-compact when context reaches threshold to avoid quality loss",
|
|
63
|
+
type: "toggle",
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
key: "compactThreshold", label: "Compact Warning",
|
|
67
|
+
description: "Warn about compaction at this context %",
|
|
68
|
+
type: "slider", min: 50, max: 95,
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
key: "showToolCalls", label: "Show Tool Calls",
|
|
72
|
+
description: "Display tool call indicators in chat",
|
|
73
|
+
type: "toggle",
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
key: "soundEnabled", label: "Sound",
|
|
77
|
+
description: "Play terminal bell when generation completes",
|
|
78
|
+
type: "toggle",
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
key: "theme", label: "Theme",
|
|
82
|
+
description: "Applied instantly. Ultra themes: poly, limitless, apple-1984",
|
|
83
|
+
type: "select", options: ["dark", "light", "midnight", "nord", "solarized", "poly", "poly-dark", "limitless", "apple-1984"],
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
key: "__divider", label: "", description: "", type: "divider",
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
key: "payAsYouGo", label: "Pay As You Go",
|
|
90
|
+
description: "Allow extra charges when budget exhausted (off = hard stop)",
|
|
91
|
+
type: "toggle",
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
key: "liveMode", label: "Live Trading",
|
|
95
|
+
description: "Allow live deployments. OFF = paper only, even if requested",
|
|
96
|
+
type: "toggle",
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
key: "__divider", label: "", description: "", type: "divider",
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
key: "__delete_chats", label: "Delete All Chats",
|
|
103
|
+
description: "Permanently anonymize and delete all chat history",
|
|
104
|
+
type: "action",
|
|
105
|
+
},
|
|
106
|
+
];
|
|
107
|
+
|
|
108
|
+
export class SettingsPanel {
|
|
109
|
+
readonly container: BoxRenderable;
|
|
110
|
+
private listBox: BoxRenderable;
|
|
111
|
+
private _visible = false;
|
|
112
|
+
private selectedIndex = 0;
|
|
113
|
+
private _settings: HorizonSettings = { ...DEFAULT_SETTINGS };
|
|
114
|
+
private _confirmingDelete = false;
|
|
115
|
+
private onChangeCallback: ((settings: HorizonSettings) => void) | null = null;
|
|
116
|
+
private onDeleteChatsCallback: (() => void) | null = null;
|
|
117
|
+
|
|
118
|
+
constructor(private renderer: CliRenderer) {
|
|
119
|
+
this.container = new BoxRenderable(renderer, {
|
|
120
|
+
id: "settings-panel",
|
|
121
|
+
width: "50%",
|
|
122
|
+
height: "100%",
|
|
123
|
+
flexDirection: "column",
|
|
124
|
+
borderColor: COLORS.borderDim,
|
|
125
|
+
});
|
|
126
|
+
this.container.visible = false;
|
|
127
|
+
|
|
128
|
+
// Header
|
|
129
|
+
const header = new BoxRenderable(renderer, {
|
|
130
|
+
id: "settings-header",
|
|
131
|
+
height: 1,
|
|
132
|
+
width: "100%",
|
|
133
|
+
flexDirection: "row",
|
|
134
|
+
alignItems: "center",
|
|
135
|
+
paddingLeft: 1,
|
|
136
|
+
backgroundColor: COLORS.bgDarker,
|
|
137
|
+
flexShrink: 0,
|
|
138
|
+
});
|
|
139
|
+
header.add(new TextRenderable(renderer, {
|
|
140
|
+
id: "settings-title",
|
|
141
|
+
content: " Settings ",
|
|
142
|
+
fg: "#212121",
|
|
143
|
+
bg: COLORS.accent,
|
|
144
|
+
}));
|
|
145
|
+
this.container.add(header);
|
|
146
|
+
|
|
147
|
+
// Settings list
|
|
148
|
+
this.listBox = new BoxRenderable(renderer, {
|
|
149
|
+
id: "settings-list",
|
|
150
|
+
flexDirection: "column",
|
|
151
|
+
flexGrow: 1,
|
|
152
|
+
paddingTop: 1,
|
|
153
|
+
paddingLeft: 2,
|
|
154
|
+
paddingRight: 2,
|
|
155
|
+
});
|
|
156
|
+
this.container.add(this.listBox);
|
|
157
|
+
|
|
158
|
+
// Footer
|
|
159
|
+
this.container.add(new TextRenderable(renderer, {
|
|
160
|
+
id: "settings-footer",
|
|
161
|
+
content: " up/down navigate left/right change esc close",
|
|
162
|
+
fg: COLORS.borderDim,
|
|
163
|
+
}));
|
|
164
|
+
|
|
165
|
+
this.renderSettings();
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
get visible(): boolean { return this._visible; }
|
|
169
|
+
get settings(): HorizonSettings { return this._settings; }
|
|
170
|
+
|
|
171
|
+
onChange(cb: (settings: HorizonSettings) => void): void { this.onChangeCallback = cb; }
|
|
172
|
+
onDeleteChats(cb: () => void): void { this.onDeleteChatsCallback = cb; }
|
|
173
|
+
|
|
174
|
+
show(): void {
|
|
175
|
+
this._visible = true;
|
|
176
|
+
this.container.visible = true;
|
|
177
|
+
this.renderSettings();
|
|
178
|
+
this.renderer.requestRender();
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
hide(): void {
|
|
182
|
+
this._visible = false;
|
|
183
|
+
this.container.visible = false;
|
|
184
|
+
this.renderer.requestRender();
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
toggle(): void {
|
|
188
|
+
if (this._visible) this.hide();
|
|
189
|
+
else this.show();
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
navigate(delta: number): void {
|
|
193
|
+
this._confirmingDelete = false;
|
|
194
|
+
let next = this.selectedIndex + delta;
|
|
195
|
+
// Skip dividers
|
|
196
|
+
while (next >= 0 && next < SETTINGS_DEFS.length && SETTINGS_DEFS[next]?.type === "divider") {
|
|
197
|
+
next += delta;
|
|
198
|
+
}
|
|
199
|
+
this.selectedIndex = Math.max(0, Math.min(SETTINGS_DEFS.length - 1, next));
|
|
200
|
+
this.renderSettings();
|
|
201
|
+
this.renderer.requestRender();
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/** Called on Enter or up/down delta=0 — confirms delete if pending */
|
|
205
|
+
confirmAction(): void {
|
|
206
|
+
const def = SETTINGS_DEFS[this.selectedIndex];
|
|
207
|
+
if (!def || def.key !== "__delete_chats") return;
|
|
208
|
+
if (this._confirmingDelete) {
|
|
209
|
+
this.onDeleteChatsCallback?.();
|
|
210
|
+
this._confirmingDelete = false;
|
|
211
|
+
} else {
|
|
212
|
+
this._confirmingDelete = true;
|
|
213
|
+
}
|
|
214
|
+
this.renderSettings();
|
|
215
|
+
this.renderer.requestRender();
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
adjust(dir: "left" | "right"): void {
|
|
219
|
+
const def = SETTINGS_DEFS[this.selectedIndex]!;
|
|
220
|
+
const key = def.key;
|
|
221
|
+
|
|
222
|
+
// Delete action requires Enter to confirm — arrows don't trigger it
|
|
223
|
+
if (def.type === "divider" || def.type === "action") return;
|
|
224
|
+
|
|
225
|
+
if (def.type === "toggle") {
|
|
226
|
+
(this._settings as any)[key] = !(this._settings as any)[key];
|
|
227
|
+
} else if (def.type === "slider") {
|
|
228
|
+
const val = (this._settings as any)[key] as number;
|
|
229
|
+
const step = key === "compactThreshold" ? 5 : 1;
|
|
230
|
+
if (dir === "left") (this._settings as any)[key] = Math.max(def.min ?? 0, val - step);
|
|
231
|
+
else (this._settings as any)[key] = Math.min(def.max ?? 100, val + step);
|
|
232
|
+
} else if (def.type === "select" && def.options) {
|
|
233
|
+
const idx = def.options.indexOf((this._settings as any)[key]);
|
|
234
|
+
if (dir === "left") (this._settings as any)[key] = def.options[Math.max(0, idx - 1)];
|
|
235
|
+
else (this._settings as any)[key] = def.options[Math.min(def.options.length - 1, idx + 1)];
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
this.onChangeCallback?.(this._settings);
|
|
239
|
+
this.renderSettings();
|
|
240
|
+
this.renderer.requestRender();
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
private renderSettings(): void {
|
|
244
|
+
for (const child of this.listBox.getChildren()) this.listBox.remove(child.id);
|
|
245
|
+
|
|
246
|
+
for (let i = 0; i < SETTINGS_DEFS.length; i++) {
|
|
247
|
+
const def = SETTINGS_DEFS[i]!;
|
|
248
|
+
const selected = i === this.selectedIndex;
|
|
249
|
+
|
|
250
|
+
// Divider
|
|
251
|
+
if (def.type === "divider") {
|
|
252
|
+
this.listBox.add(new TextRenderable(this.renderer, {
|
|
253
|
+
id: `setting-div-${i}`,
|
|
254
|
+
content: " " + "\u2500".repeat(36),
|
|
255
|
+
fg: COLORS.borderDim,
|
|
256
|
+
marginBottom: 1,
|
|
257
|
+
}));
|
|
258
|
+
continue;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// Action: delete all chats
|
|
262
|
+
if (def.type === "action" && def.key === "__delete_chats") {
|
|
263
|
+
const row = new BoxRenderable(this.renderer, {
|
|
264
|
+
id: `setting-row-${i}`, flexDirection: "column", width: "100%", marginBottom: 1,
|
|
265
|
+
});
|
|
266
|
+
const labelRow = new BoxRenderable(this.renderer, {
|
|
267
|
+
id: `setting-label-${i}`, flexDirection: "row", width: "100%",
|
|
268
|
+
});
|
|
269
|
+
const pointer = selected ? "> " : " ";
|
|
270
|
+
|
|
271
|
+
if (selected && this._confirmingDelete) {
|
|
272
|
+
labelRow.add(new TextRenderable(this.renderer, {
|
|
273
|
+
id: `setting-ptr-${i}`, content: pointer, fg: COLORS.error,
|
|
274
|
+
}));
|
|
275
|
+
labelRow.add(new TextRenderable(this.renderer, {
|
|
276
|
+
id: `setting-name-${i}`,
|
|
277
|
+
content: "Confirm? Press again to delete all",
|
|
278
|
+
fg: COLORS.error, attributes: 1,
|
|
279
|
+
}));
|
|
280
|
+
} else {
|
|
281
|
+
labelRow.add(new TextRenderable(this.renderer, {
|
|
282
|
+
id: `setting-ptr-${i}`, content: pointer, fg: selected ? COLORS.error : COLORS.textMuted,
|
|
283
|
+
}));
|
|
284
|
+
labelRow.add(new TextRenderable(this.renderer, {
|
|
285
|
+
id: `setting-name-${i}`,
|
|
286
|
+
content: def.label,
|
|
287
|
+
fg: selected ? COLORS.error : COLORS.textMuted,
|
|
288
|
+
}));
|
|
289
|
+
}
|
|
290
|
+
row.add(labelRow);
|
|
291
|
+
if (selected && !this._confirmingDelete) {
|
|
292
|
+
row.add(new TextRenderable(this.renderer, {
|
|
293
|
+
id: `setting-desc-${i}`, content: ` ${def.description}`, fg: COLORS.borderDim,
|
|
294
|
+
}));
|
|
295
|
+
}
|
|
296
|
+
this.listBox.add(row);
|
|
297
|
+
continue;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
const value = (this._settings as any)[def.key];
|
|
301
|
+
|
|
302
|
+
// Setting row
|
|
303
|
+
const row = new BoxRenderable(this.renderer, {
|
|
304
|
+
id: `setting-row-${i}`,
|
|
305
|
+
flexDirection: "column",
|
|
306
|
+
width: "100%",
|
|
307
|
+
marginBottom: 1,
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
// Label + value on one line
|
|
311
|
+
const labelRow = new BoxRenderable(this.renderer, {
|
|
312
|
+
id: `setting-label-${i}`,
|
|
313
|
+
flexDirection: "row",
|
|
314
|
+
width: "100%",
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
const pointer = selected ? "> " : " ";
|
|
318
|
+
labelRow.add(new TextRenderable(this.renderer, {
|
|
319
|
+
id: `setting-ptr-${i}`,
|
|
320
|
+
content: pointer,
|
|
321
|
+
fg: selected ? COLORS.accent : COLORS.textMuted,
|
|
322
|
+
}));
|
|
323
|
+
|
|
324
|
+
labelRow.add(new TextRenderable(this.renderer, {
|
|
325
|
+
id: `setting-name-${i}`,
|
|
326
|
+
content: def.label.padEnd(20),
|
|
327
|
+
fg: selected ? COLORS.text : COLORS.textMuted,
|
|
328
|
+
attributes: selected ? 1 : 0,
|
|
329
|
+
}));
|
|
330
|
+
|
|
331
|
+
// Value rendering
|
|
332
|
+
let valueContent = "";
|
|
333
|
+
let valueColor: any = COLORS.text;
|
|
334
|
+
|
|
335
|
+
if (def.type === "toggle") {
|
|
336
|
+
valueContent = value ? "[on]" : "[off]";
|
|
337
|
+
valueColor = value ? COLORS.success : COLORS.borderDim;
|
|
338
|
+
} else if (def.type === "slider") {
|
|
339
|
+
const min = def.min ?? 0;
|
|
340
|
+
const max = def.max ?? 100;
|
|
341
|
+
const range = max - min;
|
|
342
|
+
const filled = Math.round(((value - min) / range) * 12);
|
|
343
|
+
const bar = "\u2588".repeat(filled) + "\u2591".repeat(12 - filled);
|
|
344
|
+
valueContent = `${bar} ${value}`;
|
|
345
|
+
valueColor = COLORS.text;
|
|
346
|
+
} else if (def.type === "select") {
|
|
347
|
+
valueContent = `< ${value} >`;
|
|
348
|
+
valueColor = COLORS.accent;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
labelRow.add(new BoxRenderable(this.renderer, { id: `setting-sp-${i}`, flexGrow: 1 }));
|
|
352
|
+
labelRow.add(new TextRenderable(this.renderer, {
|
|
353
|
+
id: `setting-val-${i}`,
|
|
354
|
+
content: valueContent,
|
|
355
|
+
fg: valueColor,
|
|
356
|
+
}));
|
|
357
|
+
|
|
358
|
+
row.add(labelRow);
|
|
359
|
+
|
|
360
|
+
// Description (only for selected)
|
|
361
|
+
if (selected) {
|
|
362
|
+
row.add(new TextRenderable(this.renderer, {
|
|
363
|
+
id: `setting-desc-${i}`,
|
|
364
|
+
content: ` ${def.description}`,
|
|
365
|
+
fg: COLORS.borderDim,
|
|
366
|
+
}));
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
this.listBox.add(row);
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
}
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import { BoxRenderable, TextRenderable, InputRenderable, type CliRenderer } from "@opentui/core";
|
|
2
|
+
import { COLORS } from "../theme/colors.ts";
|
|
3
|
+
|
|
4
|
+
// Big block letters — HORIZON
|
|
5
|
+
const LOGO = [
|
|
6
|
+
" \u2588\u2588\u2557 \u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2557 \u2588\u2588\u2557",
|
|
7
|
+
" \u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2554\u2550\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2551\u255a\u2550\u2550\u2588\u2588\u2588\u2554\u255d\u2588\u2588\u2554\u2550\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2551",
|
|
8
|
+
" \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255d\u2588\u2588\u2551 \u2588\u2588\u2588\u2554\u255d \u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2554\u2588\u2588\u2557\u2588\u2588\u2551",
|
|
9
|
+
" \u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2551 \u2588\u2588\u2588\u2554\u255d \u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2551\u255a\u2588\u2588\u2588\u2588\u2551",
|
|
10
|
+
" \u2588\u2588\u2551 \u2588\u2588\u2551\u255a\u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255d\u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2551\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2551\u255a\u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255d\u2588\u2588\u2551 \u255a\u2588\u2588\u2588\u2551",
|
|
11
|
+
" \u255a\u2550\u255d \u255a\u2550\u255d \u255a\u2550\u2550\u2550\u2550\u2550\u255d \u255a\u2550\u255d \u255a\u2550\u255d\u255a\u2550\u255d\u255a\u2550\u2550\u2550\u2550\u2550\u2550\u255d \u255a\u2550\u2550\u2550\u2550\u2550\u255d \u255a\u2550\u255d \u255a\u2550\u2550\u255d",
|
|
12
|
+
];
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
export class Splash {
|
|
16
|
+
private overlay: BoxRenderable;
|
|
17
|
+
private input: InputRenderable;
|
|
18
|
+
private authText: TextRenderable;
|
|
19
|
+
private _visible = true;
|
|
20
|
+
private onSubmitCallback: ((text: string) => void) | null = null;
|
|
21
|
+
|
|
22
|
+
constructor(private renderer: CliRenderer) {
|
|
23
|
+
this.overlay = new BoxRenderable(renderer, {
|
|
24
|
+
id: "splash-overlay",
|
|
25
|
+
position: "absolute",
|
|
26
|
+
left: 0,
|
|
27
|
+
top: 0,
|
|
28
|
+
width: "100%",
|
|
29
|
+
height: "100%",
|
|
30
|
+
backgroundColor: COLORS.bg,
|
|
31
|
+
flexDirection: "column",
|
|
32
|
+
alignItems: "center",
|
|
33
|
+
justifyContent: "center",
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
// ── Logo (centered) ──
|
|
37
|
+
for (let i = 0; i < LOGO.length; i++) {
|
|
38
|
+
this.overlay.add(new TextRenderable(renderer, {
|
|
39
|
+
id: `splash-logo-${i}`,
|
|
40
|
+
content: LOGO[i]!,
|
|
41
|
+
fg: COLORS.secondary,
|
|
42
|
+
}));
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Tagline
|
|
46
|
+
this.overlay.add(new TextRenderable(renderer, {
|
|
47
|
+
id: "splash-tagline",
|
|
48
|
+
content: "The Mathematical Company of California",
|
|
49
|
+
fg: COLORS.textMuted,
|
|
50
|
+
marginTop: 2,
|
|
51
|
+
}));
|
|
52
|
+
|
|
53
|
+
// Input
|
|
54
|
+
const inputWrapper = new BoxRenderable(renderer, {
|
|
55
|
+
id: "splash-input-wrap",
|
|
56
|
+
width: "60%",
|
|
57
|
+
minWidth: 50,
|
|
58
|
+
maxWidth: 80,
|
|
59
|
+
height: 3,
|
|
60
|
+
flexDirection: "row",
|
|
61
|
+
alignItems: "center",
|
|
62
|
+
marginTop: 3,
|
|
63
|
+
backgroundColor: COLORS.bgSecondary,
|
|
64
|
+
border: true,
|
|
65
|
+
borderStyle: "rounded",
|
|
66
|
+
borderColor: COLORS.borderDim,
|
|
67
|
+
focusedBorderColor: COLORS.secondary,
|
|
68
|
+
paddingLeft: 1,
|
|
69
|
+
paddingRight: 1,
|
|
70
|
+
});
|
|
71
|
+
this.overlay.add(inputWrapper);
|
|
72
|
+
|
|
73
|
+
const prompt = new TextRenderable(renderer, {
|
|
74
|
+
id: "splash-prompt",
|
|
75
|
+
content: "> ",
|
|
76
|
+
fg: COLORS.secondary,
|
|
77
|
+
});
|
|
78
|
+
inputWrapper.add(prompt);
|
|
79
|
+
|
|
80
|
+
this.input = new InputRenderable(renderer, {
|
|
81
|
+
id: "splash-input",
|
|
82
|
+
placeholder: "What would you like to do?",
|
|
83
|
+
textColor: COLORS.text,
|
|
84
|
+
placeholderColor: COLORS.textMuted,
|
|
85
|
+
flexGrow: 1,
|
|
86
|
+
});
|
|
87
|
+
inputWrapper.add(this.input);
|
|
88
|
+
|
|
89
|
+
this.input.on("enter", () => {
|
|
90
|
+
const text = this.input.value.trim();
|
|
91
|
+
if (text.length === 0) return;
|
|
92
|
+
this.input.value = "";
|
|
93
|
+
this.onSubmitCallback?.(text);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
// Auth status
|
|
97
|
+
this.authText = new TextRenderable(renderer, {
|
|
98
|
+
id: "splash-auth",
|
|
99
|
+
content: "",
|
|
100
|
+
fg: COLORS.textMuted,
|
|
101
|
+
marginTop: 2,
|
|
102
|
+
});
|
|
103
|
+
this.overlay.add(this.authText);
|
|
104
|
+
|
|
105
|
+
// Hints
|
|
106
|
+
this.overlay.add(new TextRenderable(renderer, {
|
|
107
|
+
id: "splash-hints",
|
|
108
|
+
content: "^E previous chats \u00b7 /tutorial guide \u00b7 / commands \u00b7 ^C quit",
|
|
109
|
+
fg: COLORS.borderDim,
|
|
110
|
+
marginTop: 2,
|
|
111
|
+
}));
|
|
112
|
+
|
|
113
|
+
renderer.root.add(this.overlay);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
get visible(): boolean { return this._visible; }
|
|
117
|
+
|
|
118
|
+
onSubmit(cb: (text: string) => void): void { this.onSubmitCallback = cb; }
|
|
119
|
+
|
|
120
|
+
setAuthStatus(authenticated: boolean, email?: string, firstTime?: boolean): void {
|
|
121
|
+
if (authenticated) {
|
|
122
|
+
this.authText.content = `Logged in as ${email ?? "user"}`;
|
|
123
|
+
this.authText.fg = COLORS.success;
|
|
124
|
+
this.input.placeholder = "What would you like to do?";
|
|
125
|
+
|
|
126
|
+
if (firstTime) {
|
|
127
|
+
this.authText.content += `\n\n New here? Type /tutorial to see what Horizon can do`;
|
|
128
|
+
}
|
|
129
|
+
} else {
|
|
130
|
+
this.authText.content = 'Type /login to authenticate with Google';
|
|
131
|
+
this.authText.fg = COLORS.warning;
|
|
132
|
+
this.input.placeholder = "Type /login to get started";
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
setLoading(message: string): void {
|
|
137
|
+
this.authText.content = message;
|
|
138
|
+
this.authText.fg = COLORS.textMuted;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
focus(): void { this.renderer.focusRenderable(this.input); }
|
|
142
|
+
|
|
143
|
+
show(): void {
|
|
144
|
+
if (this._visible) return;
|
|
145
|
+
this._visible = true;
|
|
146
|
+
this.overlay.visible = true;
|
|
147
|
+
this.renderer.requestRender();
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
dismiss(): void {
|
|
151
|
+
if (!this._visible) return;
|
|
152
|
+
this._visible = false;
|
|
153
|
+
this.overlay.visible = false;
|
|
154
|
+
this.renderer.requestRender();
|
|
155
|
+
}
|
|
156
|
+
}
|