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.
Files changed (54) hide show
  1. package/assets/python/highlights.scm +137 -0
  2. package/assets/python/tree-sitter-python.wasm +0 -0
  3. package/bin/horizon.js +2 -0
  4. package/package.json +40 -0
  5. package/src/ai/client.ts +369 -0
  6. package/src/ai/system-prompt.ts +86 -0
  7. package/src/app.ts +1454 -0
  8. package/src/chat/messages.ts +48 -0
  9. package/src/chat/renderer.ts +243 -0
  10. package/src/chat/types.ts +18 -0
  11. package/src/components/code-panel.ts +329 -0
  12. package/src/components/footer.ts +72 -0
  13. package/src/components/hooks-panel.ts +224 -0
  14. package/src/components/input-bar.ts +193 -0
  15. package/src/components/mode-bar.ts +245 -0
  16. package/src/components/session-panel.ts +294 -0
  17. package/src/components/settings-panel.ts +372 -0
  18. package/src/components/splash.ts +156 -0
  19. package/src/components/strategy-panel.ts +489 -0
  20. package/src/components/tab-bar.ts +112 -0
  21. package/src/components/tutorial-panel.ts +680 -0
  22. package/src/components/widgets/progress-bar.ts +38 -0
  23. package/src/components/widgets/sparkline.ts +57 -0
  24. package/src/hooks/executor.ts +109 -0
  25. package/src/index.ts +22 -0
  26. package/src/keys/handler.ts +198 -0
  27. package/src/platform/auth.ts +36 -0
  28. package/src/platform/client.ts +159 -0
  29. package/src/platform/config.ts +121 -0
  30. package/src/platform/session-sync.ts +158 -0
  31. package/src/platform/supabase.ts +376 -0
  32. package/src/platform/sync.ts +149 -0
  33. package/src/platform/tiers.ts +103 -0
  34. package/src/platform/tools.ts +163 -0
  35. package/src/platform/types.ts +86 -0
  36. package/src/platform/usage.ts +224 -0
  37. package/src/research/apis.ts +367 -0
  38. package/src/research/tools.ts +205 -0
  39. package/src/research/widgets.ts +523 -0
  40. package/src/state/store.ts +256 -0
  41. package/src/state/types.ts +109 -0
  42. package/src/strategy/ascii-chart.ts +74 -0
  43. package/src/strategy/code-stream.ts +146 -0
  44. package/src/strategy/dashboard.ts +140 -0
  45. package/src/strategy/persistence.ts +82 -0
  46. package/src/strategy/prompts.ts +626 -0
  47. package/src/strategy/sandbox.ts +137 -0
  48. package/src/strategy/tools.ts +764 -0
  49. package/src/strategy/validator.ts +216 -0
  50. package/src/strategy/widgets.ts +270 -0
  51. package/src/syntax/setup.ts +54 -0
  52. package/src/theme/colors.ts +107 -0
  53. package/src/theme/icons.ts +27 -0
  54. package/src/util/hyperlink.ts +21 -0
@@ -0,0 +1,48 @@
1
+ import type { Message, ContentBlock } from "./types.ts";
2
+
3
+ let messageCounter = 0;
4
+
5
+ export function createMessage(
6
+ role: Message["role"],
7
+ blocks: ContentBlock[],
8
+ status: Message["status"] = "complete",
9
+ ): Message {
10
+ return {
11
+ id: `msg-${Date.now()}-${messageCounter++}`,
12
+ role,
13
+ content: blocks,
14
+ timestamp: Date.now(),
15
+ status,
16
+ };
17
+ }
18
+
19
+ export function createUserMessage(text: string): Message {
20
+ return createMessage("user", [{ type: "text", text }]);
21
+ }
22
+
23
+ export function createAssistantMessage(text: string, status: Message["status"] = "complete"): Message {
24
+ return createMessage("assistant", [{ type: "markdown", text }], status);
25
+ }
26
+
27
+ export function createSystemMessage(text: string): Message {
28
+ return createMessage("system", [{ type: "text", text }]);
29
+ }
30
+
31
+ export function createToolCallBlock(toolName: string, args: Record<string, unknown>): ContentBlock {
32
+ return { type: "tool-call", toolName, toolArgs: args };
33
+ }
34
+
35
+ export function createToolResultBlock(toolName: string, result: unknown): ContentBlock {
36
+ return { type: "tool-result", toolName, toolResult: result };
37
+ }
38
+
39
+ export function createErrorBlock(text: string): ContentBlock {
40
+ return { type: "error", text };
41
+ }
42
+
43
+ export function getMessageText(message: Message): string {
44
+ return message.content
45
+ .filter((b) => b.type === "text" || b.type === "markdown")
46
+ .map((b) => b.text ?? "")
47
+ .join("\n");
48
+ }
@@ -0,0 +1,243 @@
1
+ import {
2
+ BoxRenderable,
3
+ TextRenderable,
4
+ MarkdownRenderable,
5
+ SyntaxStyle,
6
+ RGBA,
7
+ type CliRenderer,
8
+ } from "@opentui/core";
9
+ import { COLORS } from "../theme/colors.ts";
10
+ import { renderToolWidget } from "../research/widgets.ts";
11
+ import { renderStrategyWidget } from "../strategy/widgets.ts";
12
+ import { linkifyUrls } from "../util/hyperlink.ts";
13
+ import type { Message, ContentBlock } from "./types.ts";
14
+
15
+ const h = (hex: string) => RGBA.fromHex(hex);
16
+ const syntaxStyle = SyntaxStyle.fromStyles({
17
+ "@keyword": { fg: h("#C586C0") },
18
+ "@function": { fg: h("#DCDCAA") },
19
+ "@function.builtin": { fg: h("#DCDCAA") },
20
+ "@function.method": { fg: h("#DCDCAA") },
21
+ "@string": { fg: h("#CE9178") },
22
+ "@number": { fg: h("#B5CEA8") },
23
+ "@comment": { fg: h("#6A9955"), italic: true },
24
+ "@variable": { fg: h("#9CDCFE") },
25
+ "@property": { fg: h("#9CDCFE") },
26
+ "@type": { fg: h("#4EC9B0") },
27
+ "@constructor": { fg: h("#4EC9B0") },
28
+ "@constant": { fg: h("#4FC1FF") },
29
+ "@constant.builtin": { fg: h("#569CD6") },
30
+ "@operator": { fg: h("#D4D4D4") },
31
+ "@escape": { fg: h("#D7BA7D") },
32
+ "@embedded": { fg: h("#D7BA7D") },
33
+ "@punctuation.special": { fg: h("#D7BA7D") },
34
+ });
35
+
36
+ // Braille spinner frames
37
+ const BRAILLE = [
38
+ "\u2801", "\u2803", "\u2807", "\u280f",
39
+ "\u281f", "\u283f", "\u287f", "\u28ff",
40
+ "\u28fe", "\u28fc", "\u28f8", "\u28f0",
41
+ "\u28e0", "\u28c0", "\u2880", "\u2800",
42
+ ];
43
+
44
+ // Tool name → human-readable label
45
+ function toolLabel(name: string): string {
46
+ return name.replace(/_/g, " ");
47
+ }
48
+
49
+ export class ChatRenderer {
50
+ private spinnerNodes: TextRenderable[] = [];
51
+ private spinnerTimer: ReturnType<typeof setInterval> | null = null;
52
+ private spinnerFrame = 0;
53
+ private _showToolCalls = true;
54
+
55
+ constructor(private renderer: CliRenderer) {}
56
+
57
+ setShowToolCalls(show: boolean): void { this._showToolCalls = show; }
58
+
59
+ startSpinnerAnimation(): void {
60
+ if (this.spinnerTimer) return;
61
+ this.spinnerFrame = 0;
62
+ this.spinnerTimer = setInterval(() => {
63
+ if (this.spinnerNodes.length === 0) return;
64
+ this.spinnerFrame = (this.spinnerFrame + 1) % BRAILLE.length;
65
+ const char = BRAILLE[this.spinnerFrame]!;
66
+ for (const node of this.spinnerNodes) {
67
+ node.content = `${char} `;
68
+ }
69
+ this.renderer.requestRender();
70
+ }, 60);
71
+ }
72
+
73
+ stopSpinnerAnimation(): void {
74
+ if (this.spinnerTimer) {
75
+ clearInterval(this.spinnerTimer);
76
+ this.spinnerTimer = null;
77
+ }
78
+ this.spinnerNodes = [];
79
+ }
80
+
81
+ renderMessage(message: Message): BoxRenderable {
82
+ const isUser = message.role === "user";
83
+
84
+ const box = new BoxRenderable(this.renderer, {
85
+ id: `msg-${message.id}`,
86
+ flexDirection: "column",
87
+ width: "100%",
88
+ backgroundColor: isUser ? COLORS.selection : undefined,
89
+ paddingLeft: 1,
90
+ paddingRight: 1,
91
+ marginTop: 1,
92
+ });
93
+
94
+ if (message.role === "user" || message.role === "assistant") {
95
+ box.add(new TextRenderable(this.renderer, {
96
+ id: `msg-label-${message.id}`,
97
+ content: isUser ? "U S E R" : "H O R I Z O N",
98
+ fg: isUser ? COLORS.textMuted : COLORS.borderDim,
99
+ }));
100
+ }
101
+
102
+ for (let i = 0; i < message.content.length; i++) {
103
+ const block = message.content[i]!;
104
+ const renderable = this.renderBlock(block, message, i);
105
+ if (renderable) box.add(renderable);
106
+ }
107
+
108
+ return box;
109
+ }
110
+
111
+ private renderBlock(
112
+ block: ContentBlock,
113
+ message: Message,
114
+ index: number,
115
+ ): BoxRenderable | TextRenderable | MarkdownRenderable | null {
116
+ switch (block.type) {
117
+ case "text":
118
+ return new TextRenderable(this.renderer, {
119
+ id: `msg-text-${message.id}-${index}`,
120
+ content: linkifyUrls(block.text ?? ""),
121
+ fg: COLORS.text,
122
+ });
123
+
124
+ case "markdown":
125
+ return new MarkdownRenderable(this.renderer, {
126
+ id: `msg-md-${message.id}-${index}`,
127
+ content: block.text ?? "",
128
+ syntaxStyle,
129
+ streaming: message.status === "streaming",
130
+ });
131
+
132
+ case "tool-widget": {
133
+ const widget = renderToolWidget(block.toolName ?? "", block.widgetData, this.renderer)
134
+ ?? renderStrategyWidget(block.toolName ?? "", block.widgetData, this.renderer);
135
+ return widget;
136
+ }
137
+
138
+ case "tool-call":
139
+ return this._showToolCalls ? this.renderToolCall(block, message.id, index) : null;
140
+
141
+ case "tool-result":
142
+ return this._showToolCalls ? this.renderToolResult(block, message.id, index) : null;
143
+
144
+ case "thinking":
145
+ return this.renderThinking(message.id, index);
146
+
147
+ case "error":
148
+ return new TextRenderable(this.renderer, {
149
+ id: `msg-err-${message.id}-${index}`,
150
+ content: `x ${block.text ?? "Unknown error"}`,
151
+ fg: COLORS.error,
152
+ });
153
+
154
+ default:
155
+ return null;
156
+ }
157
+ }
158
+
159
+ private renderThinking(msgId: string, index: number): BoxRenderable {
160
+ const box = new BoxRenderable(this.renderer, {
161
+ id: `msg-think-${msgId}-${index}`,
162
+ flexDirection: "row",
163
+ });
164
+
165
+ const spinner = new TextRenderable(this.renderer, {
166
+ id: `msg-think-spin-${msgId}-${index}`,
167
+ content: `${BRAILLE[0]} `,
168
+ fg: COLORS.textMuted,
169
+ });
170
+ this.spinnerNodes.push(spinner);
171
+ box.add(spinner);
172
+
173
+ box.add(new TextRenderable(this.renderer, {
174
+ id: `msg-think-label-${msgId}-${index}`,
175
+ content: "thinking",
176
+ fg: COLORS.textMuted,
177
+ }));
178
+
179
+ return box;
180
+ }
181
+
182
+ // Tool in-progress: braille spinner + name
183
+ private renderToolCall(block: ContentBlock, msgId: string, index: number): BoxRenderable {
184
+ const box = new BoxRenderable(this.renderer, {
185
+ id: `msg-tool-${msgId}-${index}`,
186
+ flexDirection: "row",
187
+ });
188
+
189
+ const spinner = new TextRenderable(this.renderer, {
190
+ id: `msg-tool-icon-${msgId}-${index}`,
191
+ content: `${BRAILLE[0]} `,
192
+ fg: COLORS.textMuted,
193
+ });
194
+ this.spinnerNodes.push(spinner);
195
+ box.add(spinner);
196
+
197
+ box.add(new TextRenderable(this.renderer, {
198
+ id: `msg-tool-name-${msgId}-${index}`,
199
+ content: toolLabel(block.toolName ?? "tool"),
200
+ fg: COLORS.textMuted,
201
+ }));
202
+
203
+ return box;
204
+ }
205
+
206
+ // Tool completed: tick or error icon + name
207
+ private renderToolResult(block: ContentBlock, msgId: string, index: number): BoxRenderable {
208
+ const box = new BoxRenderable(this.renderer, {
209
+ id: `msg-result-${msgId}-${index}`,
210
+ flexDirection: "row",
211
+ marginBottom: 1,
212
+ });
213
+
214
+ const hasError = block.toolResult && typeof block.toolResult === "object" && "error" in (block.toolResult as any);
215
+ box.add(new TextRenderable(this.renderer, {
216
+ id: `msg-result-icon-${msgId}-${index}`,
217
+ content: hasError ? "x " : ". ",
218
+ fg: hasError ? COLORS.error : COLORS.success,
219
+ }));
220
+
221
+ const label = toolLabel(block.toolName ?? "done");
222
+ const errorText = hasError ? ` — ${((block.toolResult as any).error ?? "").slice(0, 60)}` : "";
223
+ box.add(new TextRenderable(this.renderer, {
224
+ id: `msg-result-name-${msgId}-${index}`,
225
+ content: label + errorText,
226
+ fg: hasError ? COLORS.error : COLORS.textMuted,
227
+ }));
228
+
229
+ return box;
230
+ }
231
+
232
+ updateStreamingMessage(_msgId: string, container: BoxRenderable, text: string): void {
233
+ // Find the last MarkdownRenderable in the container (could be at any index after tool blocks)
234
+ const children = container.getChildren();
235
+ let lastMd: MarkdownRenderable | null = null;
236
+ for (const child of children) {
237
+ if (child instanceof MarkdownRenderable) lastMd = child;
238
+ }
239
+ if (lastMd) {
240
+ lastMd.content = text;
241
+ }
242
+ }
243
+ }
@@ -0,0 +1,18 @@
1
+ export type MessageRole = "user" | "assistant" | "system";
2
+
3
+ export interface ContentBlock {
4
+ type: "text" | "markdown" | "tool-call" | "tool-result" | "tool-widget" | "thinking" | "error";
5
+ text?: string;
6
+ toolName?: string;
7
+ toolArgs?: Record<string, unknown>;
8
+ toolResult?: unknown;
9
+ widgetData?: unknown;
10
+ }
11
+
12
+ export interface Message {
13
+ id: string;
14
+ role: MessageRole;
15
+ content: ContentBlock[];
16
+ timestamp: number;
17
+ status: "pending" | "thinking" | "streaming" | "complete" | "error";
18
+ }
@@ -0,0 +1,329 @@
1
+ // Code panel — split-view right panel with tabs: Code, Logs, Dashboard
2
+ // Sits beside the chat in a horizontal flex layout (not an overlay)
3
+
4
+ import {
5
+ BoxRenderable,
6
+ TextRenderable,
7
+ ScrollBoxRenderable,
8
+ MarkdownRenderable,
9
+ SyntaxStyle,
10
+ RGBA,
11
+ type CliRenderer,
12
+ } from "@opentui/core";
13
+ type TreeSitterClient = any;
14
+ import { COLORS } from "../theme/colors.ts";
15
+
16
+ const h = (hex: string) => RGBA.fromHex(hex);
17
+
18
+ // Python syntax colors (matches highlights.scm groups)
19
+ const syntaxStyle = SyntaxStyle.fromStyles({
20
+ "@keyword": { fg: h("#C586C0") }, // purple
21
+ "@function": { fg: h("#DCDCAA") }, // yellow
22
+ "@function.builtin": { fg: h("#DCDCAA") }, // yellow
23
+ "@function.method": { fg: h("#DCDCAA") }, // yellow
24
+ "@string": { fg: h("#CE9178") }, // orange
25
+ "@number": { fg: h("#B5CEA8") }, // light green
26
+ "@comment": { fg: h("#6A9955"), italic: true }, // green
27
+ "@variable": { fg: h("#9CDCFE") }, // light blue
28
+ "@property": { fg: h("#9CDCFE") }, // light blue
29
+ "@type": { fg: h("#4EC9B0") }, // teal
30
+ "@constructor": { fg: h("#4EC9B0") }, // teal
31
+ "@constant": { fg: h("#4FC1FF") }, // bright blue
32
+ "@constant.builtin": { fg: h("#569CD6") }, // blue
33
+ "@operator": { fg: h("#D4D4D4") }, // gray
34
+ "@escape": { fg: h("#D7BA7D") }, // gold
35
+ "@embedded": { fg: h("#D7BA7D") }, // gold
36
+ "@punctuation.special": { fg: h("#D7BA7D") }, // gold
37
+ });
38
+
39
+ type PanelTab = "code" | "logs" | "dashboard";
40
+
41
+ export class CodePanel {
42
+ readonly container: BoxRenderable;
43
+ private tabBar: BoxRenderable;
44
+ private tabTexts: Map<PanelTab, TextRenderable> = new Map();
45
+ private statusIcon: TextRenderable;
46
+
47
+ // Tab content areas
48
+ private codeScroll: ScrollBoxRenderable;
49
+ private codeMd: MarkdownRenderable;
50
+ private logsScroll: ScrollBoxRenderable;
51
+ private logsMd: MarkdownRenderable;
52
+ private dashScroll: ScrollBoxRenderable;
53
+ private dashMd: MarkdownRenderable;
54
+
55
+ private footerText: TextRenderable;
56
+ private _visible = false;
57
+ private _activeTab: PanelTab = "code";
58
+ private _code = "";
59
+ private _name = "";
60
+ private _phase = "";
61
+ private _logs = "";
62
+ private _dashboardHtml = "";
63
+ private _validationStatus: "pending" | "valid" | "invalid" | "none" = "none";
64
+
65
+ constructor(private renderer: CliRenderer) {
66
+ this.container = new BoxRenderable(renderer, {
67
+ id: "code-panel",
68
+ width: "50%",
69
+ height: "100%",
70
+ flexDirection: "column",
71
+ borderColor: COLORS.borderDim,
72
+ });
73
+ this.container.visible = false;
74
+
75
+ // ── Tab bar ──
76
+ this.tabBar = new BoxRenderable(renderer, {
77
+ id: "code-tabs",
78
+ height: 1,
79
+ width: "100%",
80
+ flexDirection: "row",
81
+ alignItems: "center",
82
+ paddingLeft: 1,
83
+ paddingRight: 1,
84
+ backgroundColor: COLORS.bgDarker,
85
+ flexShrink: 0,
86
+ });
87
+
88
+ // Validation status icon
89
+ this.statusIcon = new TextRenderable(renderer, {
90
+ id: "code-status", content: " ", fg: COLORS.textMuted,
91
+ });
92
+ this.tabBar.add(this.statusIcon);
93
+
94
+ // Tab pills
95
+ const tabs: { id: PanelTab; label: string }[] = [
96
+ { id: "code", label: "Code" },
97
+ { id: "logs", label: "Logs" },
98
+ { id: "dashboard", label: "Dashboard" },
99
+ ];
100
+ for (const tab of tabs) {
101
+ const text = new TextRenderable(renderer, {
102
+ id: `code-tab-${tab.id}`,
103
+ content: ` ${tab.label} `,
104
+ fg: COLORS.textMuted,
105
+ });
106
+ this.tabTexts.set(tab.id, text);
107
+ this.tabBar.add(text);
108
+ }
109
+
110
+ this.container.add(this.tabBar);
111
+
112
+ // ── Code tab content ──
113
+ this.codeScroll = new ScrollBoxRenderable(renderer, {
114
+ id: "code-scroll",
115
+ flexGrow: 1,
116
+ paddingLeft: 1,
117
+ paddingRight: 1,
118
+ paddingTop: 1,
119
+ stickyScroll: false,
120
+ });
121
+ this.codeMd = new MarkdownRenderable(renderer, {
122
+ id: "code-md",
123
+ content: "*no strategy loaded*\n\n*Describe a strategy in the chat to get started.*",
124
+ syntaxStyle,
125
+ });
126
+ this.codeScroll.add(this.codeMd);
127
+ this.container.add(this.codeScroll);
128
+
129
+ // ── Logs tab content ──
130
+ this.logsScroll = new ScrollBoxRenderable(renderer, {
131
+ id: "logs-scroll",
132
+ flexGrow: 1,
133
+ paddingLeft: 1,
134
+ paddingRight: 1,
135
+ paddingTop: 1,
136
+ stickyScroll: true,
137
+ stickyStart: "bottom",
138
+ });
139
+ this.logsScroll.visible = false;
140
+ this.logsMd = new MarkdownRenderable(renderer, {
141
+ id: "logs-md",
142
+ content: "*no process running*\n\n*Use run_strategy to start.*",
143
+ syntaxStyle,
144
+ });
145
+ this.logsScroll.add(this.logsMd);
146
+ this.container.add(this.logsScroll);
147
+
148
+ // ── Dashboard tab content ──
149
+ this.dashScroll = new ScrollBoxRenderable(renderer, {
150
+ id: "dash-scroll",
151
+ flexGrow: 1,
152
+ paddingLeft: 1,
153
+ paddingRight: 1,
154
+ paddingTop: 1,
155
+ stickyScroll: false,
156
+ });
157
+ this.dashScroll.visible = false;
158
+ this.dashMd = new MarkdownRenderable(renderer, {
159
+ id: "dash-md",
160
+ content: "*no dashboard generated*\n\n*Ask the LLM to build a dashboard.*",
161
+ syntaxStyle,
162
+ });
163
+ this.dashScroll.add(this.dashMd);
164
+ this.container.add(this.dashScroll);
165
+
166
+ // ── Footer ──
167
+ this.footerText = new TextRenderable(renderer, {
168
+ id: "code-footer",
169
+ content: " ^G close | Tab cycle | 1 code 2 logs 3 dash",
170
+ fg: COLORS.borderDim,
171
+ });
172
+ this.container.add(this.footerText);
173
+
174
+ this.updateTabBar();
175
+ }
176
+
177
+ get visible(): boolean { return this._visible; }
178
+ get activeTab(): PanelTab { return this._activeTab; }
179
+ getCode(): string { return this._code; }
180
+
181
+ /** Enable Python syntax highlighting by setting a tree-sitter client */
182
+ setTreeSitterClient(client: TreeSitterClient): void {
183
+ (this.codeMd as any).treeSitterClient = client;
184
+ (this.logsMd as any).treeSitterClient = client;
185
+ (this.dashMd as any).treeSitterClient = client;
186
+ }
187
+
188
+ // ── Tab switching ──
189
+
190
+ setTab(tab: PanelTab): void {
191
+ this._activeTab = tab;
192
+ this.codeScroll.visible = tab === "code";
193
+ this.logsScroll.visible = tab === "logs";
194
+ this.dashScroll.visible = tab === "dashboard";
195
+ // Refresh content for the active tab
196
+ if (tab === "code") this.updateCodeContent();
197
+ else if (tab === "logs") this.updateLogsContent();
198
+ else if (tab === "dashboard") this.updateDashContent();
199
+ this.updateTabBar();
200
+ this.renderer.requestRender();
201
+ }
202
+
203
+ cycleTab(): void {
204
+ const order: PanelTab[] = ["code", "logs", "dashboard"];
205
+ const idx = order.indexOf(this._activeTab);
206
+ this.setTab(order[(idx + 1) % order.length]!);
207
+ }
208
+
209
+ // ── Content setters ──
210
+
211
+ setCode(code: string, status: "pending" | "valid" | "invalid" | "none" = "none"): void {
212
+ this._code = code;
213
+ this._validationStatus = status;
214
+ if (this._activeTab === "code") this.updateCodeContent();
215
+ this.updateStatusIcon();
216
+ }
217
+
218
+ setStrategy(name: string, phase: string): void {
219
+ this._name = name;
220
+ this._phase = phase;
221
+ this.updateTabBar();
222
+ }
223
+
224
+ setLogs(logs: string): void {
225
+ this._logs = logs;
226
+ if (this._activeTab === "logs") this.updateLogsContent();
227
+ }
228
+
229
+ appendLog(line: string): void {
230
+ this._logs += (this._logs ? "\n" : "") + line;
231
+ // Keep last 200 lines
232
+ const lines = this._logs.split("\n");
233
+ if (lines.length > 200) this._logs = lines.slice(-200).join("\n");
234
+ if (this._activeTab === "logs") this.updateLogsContent();
235
+ }
236
+
237
+ setDashboardHtml(html: string): void {
238
+ this._dashboardHtml = html;
239
+ if (this._activeTab === "dashboard") this.updateDashContent();
240
+ }
241
+
242
+ // ── Visibility ──
243
+
244
+ show(): void {
245
+ this._visible = true;
246
+ this.container.visible = true;
247
+ this.renderer.requestRender();
248
+ }
249
+
250
+ hide(): void {
251
+ this._visible = false;
252
+ this.container.visible = false;
253
+ this.renderer.requestRender();
254
+ }
255
+
256
+ toggle(): void {
257
+ if (this._visible) this.hide();
258
+ else this.show();
259
+ }
260
+
261
+ // ── Internal rendering ──
262
+
263
+ private updateTabBar(): void {
264
+ for (const [id, text] of this.tabTexts) {
265
+ if (id === this._activeTab) {
266
+ text.fg = "#212121";
267
+ text.bg = COLORS.accent;
268
+ text.content = id === "code"
269
+ ? ` ${this._name || "Code"} `
270
+ : ` ${id.charAt(0).toUpperCase() + id.slice(1)} `;
271
+ } else {
272
+ text.fg = COLORS.textMuted;
273
+ text.bg = undefined;
274
+ text.content = ` ${id.charAt(0).toUpperCase() + id.slice(1)} `;
275
+ }
276
+ }
277
+ }
278
+
279
+ private updateStatusIcon(): void {
280
+ switch (this._validationStatus) {
281
+ case "valid":
282
+ this.statusIcon.content = ". ";
283
+ this.statusIcon.fg = COLORS.success;
284
+ break;
285
+ case "invalid":
286
+ this.statusIcon.content = "x ";
287
+ this.statusIcon.fg = COLORS.error;
288
+ break;
289
+ case "pending":
290
+ this.statusIcon.content = "~ ";
291
+ this.statusIcon.fg = COLORS.warning;
292
+ break;
293
+ default:
294
+ this.statusIcon.content = " ";
295
+ this.statusIcon.fg = COLORS.textMuted;
296
+ }
297
+ }
298
+
299
+ private updateCodeContent(): void {
300
+ if (!this._code) {
301
+ if (this._validationStatus === "pending") {
302
+ this.codeMd.content = "*Generating strategy code...*\n\n*The code will appear here when ready.*";
303
+ } else {
304
+ this.codeMd.content = "*no strategy loaded*\n\n*Describe a strategy in the chat to get started.*";
305
+ }
306
+ } else {
307
+ this.codeMd.content = "```python\n" + this._code + "\n```";
308
+ }
309
+ this.renderer.requestRender();
310
+ }
311
+
312
+ private updateLogsContent(): void {
313
+ if (!this._logs) {
314
+ this.logsMd.content = "*no process running*\n\n*Use run_strategy to start.*";
315
+ } else {
316
+ this.logsMd.content = "```\n" + this._logs + "\n```";
317
+ }
318
+ this.renderer.requestRender();
319
+ }
320
+
321
+ private updateDashContent(): void {
322
+ if (!this._dashboardHtml) {
323
+ this.dashMd.content = "*no dashboard generated*\n\n*Ask the LLM to build a dashboard.*";
324
+ } else {
325
+ this.dashMd.content = "```html\n" + this._dashboardHtml + "\n```";
326
+ }
327
+ this.renderer.requestRender();
328
+ }
329
+ }