horizon-code 0.1.1 → 0.2.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/bin/horizon.js +18 -1
- package/package.json +2 -2
- package/src/app.ts +85 -11
- package/src/components/code-panel.ts +141 -14
- package/src/platform/session-sync.ts +1 -1
- package/src/state/types.ts +1 -0
- package/src/strategy/code-stream.ts +3 -1
- package/src/strategy/dashboard.ts +189 -18
- package/src/strategy/prompts.ts +426 -6
- package/src/strategy/tools.ts +311 -54
- package/src/strategy/validator.ts +98 -0
- package/src/updater.ts +118 -0
package/bin/horizon.js
CHANGED
|
@@ -1,2 +1,19 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
|
-
import "../src/
|
|
2
|
+
import { checkForUpdates } from "../src/updater.ts";
|
|
3
|
+
|
|
4
|
+
// Auto-update before loading the app
|
|
5
|
+
const { updated, from, to } = await checkForUpdates();
|
|
6
|
+
if (updated) {
|
|
7
|
+
// Re-exec so the new version's code loads
|
|
8
|
+
process.stderr.write(`\x1b[2mUpdated ${from} → ${to}. Restarting...\x1b[0m\n`);
|
|
9
|
+
const result = Bun.spawnSync(["bun", "run", import.meta.path], {
|
|
10
|
+
stdin: "inherit",
|
|
11
|
+
stdout: "inherit",
|
|
12
|
+
stderr: "inherit",
|
|
13
|
+
env: { ...process.env, __HORIZON_SKIP_UPDATE: "1" },
|
|
14
|
+
});
|
|
15
|
+
process.exit(result.exitCode ?? 0);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// Load the app
|
|
19
|
+
import("../src/index.ts");
|
package/package.json
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "horizon-code",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "AI-powered trading strategy terminal for Polymarket",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
|
-
"
|
|
7
|
+
"hzcode": "./bin/horizon.js"
|
|
8
8
|
},
|
|
9
9
|
"files": [
|
|
10
10
|
"src",
|
package/src/app.ts
CHANGED
|
@@ -18,7 +18,7 @@ import { store } from "./state/store.ts";
|
|
|
18
18
|
import { createUserMessage } from "./chat/messages.ts";
|
|
19
19
|
import { chat } from "./ai/client.ts";
|
|
20
20
|
import { dashboard } from "./strategy/dashboard.ts";
|
|
21
|
-
import { cleanupStrategyProcesses, runningProcesses } from "./strategy/tools.ts";
|
|
21
|
+
import { cleanupStrategyProcesses, runningProcesses, parseLocalMetrics } from "./strategy/tools.ts";
|
|
22
22
|
import { abortChat } from "./ai/client.ts";
|
|
23
23
|
import type { ModelPower } from "./platform/tiers.ts";
|
|
24
24
|
import { listSavedStrategies, loadStrategy } from "./strategy/persistence.ts";
|
|
@@ -92,6 +92,7 @@ export class App {
|
|
|
92
92
|
private inChatMode = false;
|
|
93
93
|
private authenticated = false;
|
|
94
94
|
private _openIdsSaveTimer: ReturnType<typeof setTimeout> | null = null;
|
|
95
|
+
private _hasLocalMetrics = false;
|
|
95
96
|
|
|
96
97
|
// Per-tab stream state
|
|
97
98
|
private tabStreams: Map<string, {
|
|
@@ -403,6 +404,27 @@ export class App {
|
|
|
403
404
|
this.codePanel.setStrategy(draft.name, draft.phase);
|
|
404
405
|
}
|
|
405
406
|
|
|
407
|
+
// Feed active deployment metrics into the dashboard tab
|
|
408
|
+
// Skip if local process metrics are active (they take priority)
|
|
409
|
+
if (!this._hasLocalMetrics) {
|
|
410
|
+
const state = store.get();
|
|
411
|
+
const running = state.deployments.find((d) => d.status === "running" || d.status === "starting");
|
|
412
|
+
if (running) {
|
|
413
|
+
this.codePanel.setMetrics({
|
|
414
|
+
name: running.name,
|
|
415
|
+
status: running.status,
|
|
416
|
+
dryRun: running.dry_run,
|
|
417
|
+
metrics: running.metrics,
|
|
418
|
+
positions: running.positions,
|
|
419
|
+
orders: running.orders,
|
|
420
|
+
pnlHistory: running.pnl_history,
|
|
421
|
+
startedAt: running.started_at,
|
|
422
|
+
});
|
|
423
|
+
} else {
|
|
424
|
+
this.codePanel.setMetrics(null);
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
|
|
406
428
|
// Sync code panel visibility to key handler for Tab cycling
|
|
407
429
|
this.keyHandler.codePanelVisible = this.codePanel.visible;
|
|
408
430
|
|
|
@@ -412,28 +434,82 @@ export class App {
|
|
|
412
434
|
|
|
413
435
|
renderer.on("resize", () => renderer.requestRender());
|
|
414
436
|
|
|
415
|
-
// Poll background processes — update count, live logs, clean dead
|
|
437
|
+
// Poll background processes — update count, live logs, parse metrics, clean dead
|
|
416
438
|
setInterval(() => {
|
|
417
439
|
let alive = 0;
|
|
418
440
|
let latestLogs = "";
|
|
419
441
|
const deadPids: number[] = [];
|
|
420
442
|
const now = Date.now();
|
|
443
|
+
let localMetricsData: ReturnType<typeof parseLocalMetrics> = null;
|
|
444
|
+
let localProcessStartedAt = 0;
|
|
421
445
|
|
|
422
446
|
for (const [pid, m] of runningProcesses) {
|
|
423
447
|
if (m.proc.exitCode === null) {
|
|
424
448
|
alive++;
|
|
425
|
-
|
|
426
|
-
const
|
|
427
|
-
|
|
449
|
+
// Filter out __HZ_METRICS__ lines from visible logs
|
|
450
|
+
const stdoutLines = m.stdout.slice(-30);
|
|
451
|
+
const stderrLines = m.stderr.slice(-10).filter((l: string) => !l.startsWith("__HZ_METRICS__"));
|
|
452
|
+
latestLogs = stdoutLines.join("\n") + (stderrLines.length > 0 ? "\n--- stderr ---\n" + stderrLines.join("\n") : "");
|
|
453
|
+
|
|
454
|
+
// Parse latest metrics from this process
|
|
455
|
+
const metrics = parseLocalMetrics(m);
|
|
456
|
+
if (metrics) {
|
|
457
|
+
localMetricsData = metrics;
|
|
458
|
+
localProcessStartedAt = m.startedAt;
|
|
459
|
+
}
|
|
428
460
|
} else if (now - m.startedAt > 300000) {
|
|
429
|
-
// Dead for 5+ minutes — clean up
|
|
430
461
|
deadPids.push(pid);
|
|
431
462
|
}
|
|
432
463
|
}
|
|
433
464
|
for (const pid of deadPids) runningProcesses.delete(pid);
|
|
434
465
|
|
|
435
466
|
this.modeBar.setBgProcessCount(alive);
|
|
436
|
-
|
|
467
|
+
|
|
468
|
+
// Feed local metrics to the Metrics tab (takes priority over platform deployments)
|
|
469
|
+
if (localMetricsData) {
|
|
470
|
+
const draft = store.getActiveSession()?.strategyDraft;
|
|
471
|
+
this.codePanel.setMetrics({
|
|
472
|
+
name: draft?.name ?? "Local Strategy",
|
|
473
|
+
status: "running",
|
|
474
|
+
dryRun: true,
|
|
475
|
+
metrics: {
|
|
476
|
+
total_pnl: localMetricsData.pnl,
|
|
477
|
+
realized_pnl: localMetricsData.rpnl,
|
|
478
|
+
unrealized_pnl: localMetricsData.upnl,
|
|
479
|
+
total_exposure: 0,
|
|
480
|
+
position_count: localMetricsData.positions,
|
|
481
|
+
open_order_count: localMetricsData.orders,
|
|
482
|
+
win_rate: 0,
|
|
483
|
+
total_trades: 0,
|
|
484
|
+
max_drawdown_pct: 0,
|
|
485
|
+
sharpe_ratio: 0,
|
|
486
|
+
profit_factor: 0,
|
|
487
|
+
avg_return_per_trade: 0,
|
|
488
|
+
gross_profit: 0,
|
|
489
|
+
gross_loss: 0,
|
|
490
|
+
},
|
|
491
|
+
positions: (localMetricsData.pos ?? []).map((p) => ({
|
|
492
|
+
market_id: p.id,
|
|
493
|
+
slug: p.id,
|
|
494
|
+
question: "",
|
|
495
|
+
side: p.side === "Yes" || p.side === "Buy" ? "BUY" as const : "SELL" as const,
|
|
496
|
+
size: p.sz,
|
|
497
|
+
avg_entry_price: p.entry,
|
|
498
|
+
cost_basis: p.sz * p.entry,
|
|
499
|
+
realized_pnl: p.rpnl,
|
|
500
|
+
unrealized_pnl: p.upnl,
|
|
501
|
+
})),
|
|
502
|
+
orders: [],
|
|
503
|
+
pnlHistory: localMetricsData.hist ?? [],
|
|
504
|
+
startedAt: localProcessStartedAt,
|
|
505
|
+
});
|
|
506
|
+
this._hasLocalMetrics = true;
|
|
507
|
+
} else if (this._hasLocalMetrics && alive === 0) {
|
|
508
|
+
// Local process stopped — clear local metrics, let platform data take over
|
|
509
|
+
this._hasLocalMetrics = false;
|
|
510
|
+
store.update({});
|
|
511
|
+
}
|
|
512
|
+
|
|
437
513
|
if (alive > 0 && latestLogs && this.codePanel.visible && this.codePanel.activeTab === "logs") {
|
|
438
514
|
this.codePanel.setLogs(latestLogs);
|
|
439
515
|
}
|
|
@@ -655,7 +731,7 @@ export class App {
|
|
|
655
731
|
const code = autoFixStrategyCode(loaded.code);
|
|
656
732
|
store.setStrategyDraft({
|
|
657
733
|
name: arg, code, params: {}, explanation: "", riskConfig: null,
|
|
658
|
-
validationStatus: "none", validationErrors: [], phase: "generated",
|
|
734
|
+
validationStatus: "none", validationErrors: [], validationWarnings: [], phase: "generated",
|
|
659
735
|
});
|
|
660
736
|
this.codePanel.setCode(code, "pending");
|
|
661
737
|
this.codePanel.setStrategy(arg, "loaded");
|
|
@@ -1226,12 +1302,10 @@ export class App {
|
|
|
1226
1302
|
}
|
|
1227
1303
|
}
|
|
1228
1304
|
|
|
1229
|
-
//
|
|
1305
|
+
// Switch to dashboard/metrics tab when dashboard is spawned
|
|
1230
1306
|
if (part.toolName === "spawn_dashboard") {
|
|
1231
1307
|
const result = part.result as any;
|
|
1232
1308
|
if (result?.success) {
|
|
1233
|
-
const html = (part as any).args?.custom_html;
|
|
1234
|
-
if (html) this.codePanel.setDashboardHtml(html);
|
|
1235
1309
|
this.codePanel.setTab("dashboard");
|
|
1236
1310
|
if (!this.codePanel.visible) this.codePanel.show();
|
|
1237
1311
|
}
|
|
@@ -12,9 +12,32 @@ import {
|
|
|
12
12
|
} from "@opentui/core";
|
|
13
13
|
type TreeSitterClient = any;
|
|
14
14
|
import { COLORS } from "../theme/colors.ts";
|
|
15
|
+
import { store } from "../state/store.ts";
|
|
16
|
+
import type { DeploymentMetrics, Position, Order } from "../state/types.ts";
|
|
15
17
|
|
|
16
18
|
const h = (hex: string) => RGBA.fromHex(hex);
|
|
17
19
|
|
|
20
|
+
function sparkline(values: number[], width: number): string {
|
|
21
|
+
if (values.length < 2) return "";
|
|
22
|
+
const chars = "\u2581\u2582\u2583\u2584\u2585\u2586\u2587\u2588";
|
|
23
|
+
const min = Math.min(...values);
|
|
24
|
+
const max = Math.max(...values);
|
|
25
|
+
const range = max - min || 1;
|
|
26
|
+
const step = values.length / width;
|
|
27
|
+
let result = "";
|
|
28
|
+
for (let i = 0; i < width && i * step < values.length; i++) {
|
|
29
|
+
const idx = Math.min(Math.floor(i * step), values.length - 1);
|
|
30
|
+
const normalized = (values[idx]! - min) / range;
|
|
31
|
+
const charIdx = Math.min(Math.floor(normalized * (chars.length - 1)), chars.length - 1);
|
|
32
|
+
result += chars[charIdx];
|
|
33
|
+
}
|
|
34
|
+
return result;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function pad(s: string, width: number): string {
|
|
38
|
+
return s.length >= width ? s : s + " ".repeat(width - s.length);
|
|
39
|
+
}
|
|
40
|
+
|
|
18
41
|
// Python syntax colors (matches highlights.scm groups)
|
|
19
42
|
const syntaxStyle = SyntaxStyle.fromStyles({
|
|
20
43
|
"@keyword": { fg: h("#C586C0") }, // purple
|
|
@@ -59,7 +82,16 @@ export class CodePanel {
|
|
|
59
82
|
private _name = "";
|
|
60
83
|
private _phase = "";
|
|
61
84
|
private _logs = "";
|
|
62
|
-
private
|
|
85
|
+
private _metricsData: {
|
|
86
|
+
name: string;
|
|
87
|
+
status: string;
|
|
88
|
+
dryRun: boolean;
|
|
89
|
+
metrics: DeploymentMetrics;
|
|
90
|
+
positions: Position[];
|
|
91
|
+
orders: Order[];
|
|
92
|
+
pnlHistory: number[];
|
|
93
|
+
startedAt: number;
|
|
94
|
+
} | null = null;
|
|
63
95
|
private _validationStatus: "pending" | "valid" | "invalid" | "none" = "none";
|
|
64
96
|
|
|
65
97
|
constructor(private renderer: CliRenderer) {
|
|
@@ -95,7 +127,7 @@ export class CodePanel {
|
|
|
95
127
|
const tabs: { id: PanelTab; label: string }[] = [
|
|
96
128
|
{ id: "code", label: "Code" },
|
|
97
129
|
{ id: "logs", label: "Logs" },
|
|
98
|
-
{ id: "dashboard", label: "
|
|
130
|
+
{ id: "dashboard", label: "Metrics" },
|
|
99
131
|
];
|
|
100
132
|
for (const tab of tabs) {
|
|
101
133
|
const text = new TextRenderable(renderer, {
|
|
@@ -157,7 +189,7 @@ export class CodePanel {
|
|
|
157
189
|
this.dashScroll.visible = false;
|
|
158
190
|
this.dashMd = new MarkdownRenderable(renderer, {
|
|
159
191
|
id: "dash-md",
|
|
160
|
-
content: "*no
|
|
192
|
+
content: "*no active deployment*\n\n*Deploy a strategy to see live metrics here.*",
|
|
161
193
|
syntaxStyle,
|
|
162
194
|
});
|
|
163
195
|
this.dashScroll.add(this.dashMd);
|
|
@@ -234,8 +266,17 @@ export class CodePanel {
|
|
|
234
266
|
if (this._activeTab === "logs") this.updateLogsContent();
|
|
235
267
|
}
|
|
236
268
|
|
|
237
|
-
|
|
238
|
-
|
|
269
|
+
setMetrics(data: {
|
|
270
|
+
name: string;
|
|
271
|
+
status: string;
|
|
272
|
+
dryRun: boolean;
|
|
273
|
+
metrics: DeploymentMetrics;
|
|
274
|
+
positions: Position[];
|
|
275
|
+
orders: Order[];
|
|
276
|
+
pnlHistory: number[];
|
|
277
|
+
startedAt: number;
|
|
278
|
+
} | null): void {
|
|
279
|
+
this._metricsData = data;
|
|
239
280
|
if (this._activeTab === "dashboard") this.updateDashContent();
|
|
240
281
|
}
|
|
241
282
|
|
|
@@ -262,16 +303,17 @@ export class CodePanel {
|
|
|
262
303
|
|
|
263
304
|
private updateTabBar(): void {
|
|
264
305
|
for (const [id, text] of this.tabTexts) {
|
|
306
|
+
const label = id === "code" ? (this._name || "Code")
|
|
307
|
+
: id === "dashboard" ? "Metrics"
|
|
308
|
+
: id.charAt(0).toUpperCase() + id.slice(1);
|
|
265
309
|
if (id === this._activeTab) {
|
|
266
310
|
text.fg = "#212121";
|
|
267
311
|
text.bg = COLORS.accent;
|
|
268
|
-
text.content =
|
|
269
|
-
? ` ${this._name || "Code"} `
|
|
270
|
-
: ` ${id.charAt(0).toUpperCase() + id.slice(1)} `;
|
|
312
|
+
text.content = ` ${label} `;
|
|
271
313
|
} else {
|
|
272
314
|
text.fg = COLORS.textMuted;
|
|
273
315
|
text.bg = undefined;
|
|
274
|
-
text.content = ` ${
|
|
316
|
+
text.content = ` ${label} `;
|
|
275
317
|
}
|
|
276
318
|
}
|
|
277
319
|
}
|
|
@@ -304,7 +346,20 @@ export class CodePanel {
|
|
|
304
346
|
this.codeMd.content = "*no strategy loaded*\n\n*Describe a strategy in the chat to get started.*";
|
|
305
347
|
}
|
|
306
348
|
} else {
|
|
307
|
-
|
|
349
|
+
let content = "```python\n" + this._code + "\n```";
|
|
350
|
+
|
|
351
|
+
// Show warnings if any
|
|
352
|
+
const draft = store.getActiveSession()?.strategyDraft;
|
|
353
|
+
if (draft?.validationWarnings?.length) {
|
|
354
|
+
content += "\n\n---\n";
|
|
355
|
+
for (const w of draft.validationWarnings) {
|
|
356
|
+
const icon = w.severity === "warning" ? "!" : "i";
|
|
357
|
+
const lineRef = w.line ? ` (line ${w.line})` : "";
|
|
358
|
+
content += `\n${icon} ${w.message}${lineRef}`;
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
this.codeMd.content = content;
|
|
308
363
|
}
|
|
309
364
|
this.renderer.requestRender();
|
|
310
365
|
}
|
|
@@ -319,11 +374,83 @@ export class CodePanel {
|
|
|
319
374
|
}
|
|
320
375
|
|
|
321
376
|
private updateDashContent(): void {
|
|
322
|
-
if (!this.
|
|
323
|
-
this.dashMd.content = "*no
|
|
324
|
-
|
|
325
|
-
|
|
377
|
+
if (!this._metricsData) {
|
|
378
|
+
this.dashMd.content = "*no active deployment*\n\n*Deploy a strategy to see live metrics here.*";
|
|
379
|
+
this.renderer.requestRender();
|
|
380
|
+
return;
|
|
326
381
|
}
|
|
382
|
+
|
|
383
|
+
const d = this._metricsData;
|
|
384
|
+
const m = d.metrics;
|
|
385
|
+
const statusIcon = d.status === "running" ? "\u25CF" : d.status === "error" ? "x" : "\u25CB";
|
|
386
|
+
const modeLabel = d.dryRun ? "paper" : "LIVE";
|
|
387
|
+
|
|
388
|
+
// Uptime
|
|
389
|
+
const uptimeMs = Date.now() - d.startedAt;
|
|
390
|
+
const hours = Math.floor(uptimeMs / 3600000);
|
|
391
|
+
const mins = Math.floor((uptimeMs % 3600000) / 60000);
|
|
392
|
+
const uptime = hours > 0 ? `${hours}h ${mins}m` : `${mins}m`;
|
|
393
|
+
|
|
394
|
+
// Sparkline from pnl history
|
|
395
|
+
const spark = sparkline(d.pnlHistory, 30);
|
|
396
|
+
|
|
397
|
+
// Format helpers
|
|
398
|
+
const pnl = (v: number) => (v >= 0 ? `+$${v.toFixed(2)}` : `-$${Math.abs(v).toFixed(2)}`);
|
|
399
|
+
const pct = (v: number) => `${v.toFixed(1)}%`;
|
|
400
|
+
|
|
401
|
+
const lines: string[] = [];
|
|
402
|
+
lines.push(`**${d.name}** ${statusIcon} ${d.status} | ${modeLabel} | ${uptime}`);
|
|
403
|
+
lines.push("---");
|
|
404
|
+
lines.push("");
|
|
405
|
+
|
|
406
|
+
// P&L section
|
|
407
|
+
lines.push("```");
|
|
408
|
+
lines.push(` Total P&L ${pad(pnl(m.total_pnl), 12)} Realized ${pad(pnl(m.realized_pnl), 12)}`);
|
|
409
|
+
lines.push(` Unrealized ${pad(pnl(m.unrealized_pnl), 12)} Exposure ${pad("$" + m.total_exposure.toFixed(2), 12)}`);
|
|
410
|
+
lines.push("");
|
|
411
|
+
lines.push(` Sharpe ${pad(m.sharpe_ratio.toFixed(2), 12)} Win Rate ${pad(pct(m.win_rate * 100), 12)}`);
|
|
412
|
+
lines.push(` Max DD ${pad(pct(m.max_drawdown_pct), 12)} Trades ${pad(String(m.total_trades), 12)}`);
|
|
413
|
+
lines.push(` Profit F ${pad(m.profit_factor.toFixed(2), 12)} Avg Return ${pad(pnl(m.avg_return_per_trade), 12)}`);
|
|
414
|
+
lines.push("```");
|
|
415
|
+
lines.push("");
|
|
416
|
+
|
|
417
|
+
// Equity sparkline
|
|
418
|
+
if (spark) {
|
|
419
|
+
lines.push("**Equity**");
|
|
420
|
+
lines.push("```");
|
|
421
|
+
lines.push(" " + spark);
|
|
422
|
+
lines.push("```");
|
|
423
|
+
lines.push("");
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
// Positions
|
|
427
|
+
if (d.positions.length > 0) {
|
|
428
|
+
lines.push(`**Positions** (${d.positions.length})`);
|
|
429
|
+
lines.push("```");
|
|
430
|
+
for (const p of d.positions.slice(0, 8)) {
|
|
431
|
+
const side = p.side === "BUY" ? "BUY " : "SELL";
|
|
432
|
+
const slug = p.slug.length > 24 ? p.slug.slice(0, 24) + "..." : p.slug;
|
|
433
|
+
lines.push(` ${side} ${pad(slug, 28)} ${pad(String(p.size), 5)} @ ${p.avg_entry_price.toFixed(2)} ${pnl(p.unrealized_pnl)}`);
|
|
434
|
+
}
|
|
435
|
+
if (d.positions.length > 8) lines.push(` ... and ${d.positions.length - 8} more`);
|
|
436
|
+
lines.push("```");
|
|
437
|
+
lines.push("");
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
// Recent orders
|
|
441
|
+
if (d.orders.length > 0) {
|
|
442
|
+
lines.push(`**Recent Orders** (${d.orders.length})`);
|
|
443
|
+
lines.push("```");
|
|
444
|
+
for (const o of d.orders.slice(0, 5)) {
|
|
445
|
+
const time = new Date(o.created_at).toLocaleTimeString("en-US", { hour12: false, hour: "2-digit", minute: "2-digit" });
|
|
446
|
+
const side = o.side === "BUY" ? "BUY " : "SELL";
|
|
447
|
+
const slug = o.slug.length > 20 ? o.slug.slice(0, 20) + "..." : o.slug;
|
|
448
|
+
lines.push(` ${time} ${side} ${pad(String(o.size), 4)} @ ${o.price.toFixed(2)} ${pad(o.status, 10)} ${slug}`);
|
|
449
|
+
}
|
|
450
|
+
lines.push("```");
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
this.dashMd.content = lines.join("\n");
|
|
327
454
|
this.renderer.requestRender();
|
|
328
455
|
}
|
|
329
456
|
}
|
|
@@ -50,7 +50,7 @@ export async function loadSessions(): Promise<void> {
|
|
|
50
50
|
mode: (dbs.mode as any) ?? "research",
|
|
51
51
|
isStreaming: false,
|
|
52
52
|
streamingMsgId: null,
|
|
53
|
-
strategyDraft: dbs.strategy_draft ?? null,
|
|
53
|
+
strategyDraft: dbs.strategy_draft ? { ...dbs.strategy_draft, validationWarnings: dbs.strategy_draft.validationWarnings ?? [] } : null,
|
|
54
54
|
});
|
|
55
55
|
|
|
56
56
|
// Mark all loaded messages as synced
|
package/src/state/types.ts
CHANGED
|
@@ -79,6 +79,7 @@ export interface StrategyDraft {
|
|
|
79
79
|
} | null;
|
|
80
80
|
validationStatus: "pending" | "valid" | "invalid" | "none";
|
|
81
81
|
validationErrors: { line: number | null; message: string }[];
|
|
82
|
+
validationWarnings: { line: number | null; message: string; severity: "warning" | "info" }[];
|
|
82
83
|
phase: "generated" | "iterated" | "validated" | "saved" | "deployed";
|
|
83
84
|
strategyId?: string;
|
|
84
85
|
filePath?: string;
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// This replaces propose_strategy/update_strategy tools — the LLM writes code
|
|
4
4
|
// directly in its response, and we catch it and handle validation/saving.
|
|
5
5
|
|
|
6
|
-
import { validateStrategyCode, autoFixStrategyCode } from "./validator.ts";
|
|
6
|
+
import { validateStrategyCode, autoFixStrategyCode, getStrategyWarnings } from "./validator.ts";
|
|
7
7
|
import { saveStrategy } from "./persistence.ts";
|
|
8
8
|
import { store } from "../state/store.ts";
|
|
9
9
|
import type { StrategyDraft } from "../state/types.ts";
|
|
@@ -67,6 +67,7 @@ function extractParams(code: string): Record<string, unknown> {
|
|
|
67
67
|
export async function finalizeStrategy(code: string, overrideName?: string): Promise<StrategyDraft> {
|
|
68
68
|
const fixedCode = autoFixStrategyCode(code);
|
|
69
69
|
const errors = validateStrategyCode(fixedCode);
|
|
70
|
+
const warnings = getStrategyWarnings(fixedCode);
|
|
70
71
|
const name = overrideName ?? extractStrategyName(fixedCode);
|
|
71
72
|
|
|
72
73
|
const draft: StrategyDraft = {
|
|
@@ -77,6 +78,7 @@ export async function finalizeStrategy(code: string, overrideName?: string): Pro
|
|
|
77
78
|
riskConfig: extractRiskConfig(fixedCode),
|
|
78
79
|
validationStatus: errors.length === 0 ? "valid" : "invalid",
|
|
79
80
|
validationErrors: errors,
|
|
81
|
+
validationWarnings: warnings,
|
|
80
82
|
phase: "generated",
|
|
81
83
|
};
|
|
82
84
|
|