horizon-code 0.6.1 → 0.6.2
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/package.json +1 -1
- package/src/app.ts +19 -3
- package/src/components/code-panel.ts +15 -8
- package/src/research/apis.ts +60 -15
- package/src/research/widgets.ts +34 -30
package/package.json
CHANGED
package/src/app.ts
CHANGED
|
@@ -1479,6 +1479,11 @@ export class App {
|
|
|
1479
1479
|
currentBlocks.push({ type: "tool-call", toolName: part.toolName });
|
|
1480
1480
|
rebuildContainer(currentBlocks, "streaming");
|
|
1481
1481
|
|
|
1482
|
+
// Clear widgets placeholder early so user doesn't see stale text while tool executes
|
|
1483
|
+
if (WIDGET_TOOLS.has(part.toolName) && this.settingsPanel.settings.widgetsInTab) {
|
|
1484
|
+
this.codePanel.clearPlaceholder();
|
|
1485
|
+
}
|
|
1486
|
+
|
|
1482
1487
|
// Execute before-hooks for this tool
|
|
1483
1488
|
const beforeEvent = TOOL_BEFORE_HOOK[part.toolName];
|
|
1484
1489
|
if (beforeEvent) {
|
|
@@ -1646,6 +1651,13 @@ export class App {
|
|
|
1646
1651
|
// Server already recorded usage — just update the UI
|
|
1647
1652
|
this.updateBudgetMeter();
|
|
1648
1653
|
} else if (part.type === "meta") {
|
|
1654
|
+
// Server is processing — transition from "thinking" to "streaming"
|
|
1655
|
+
// so the header shows the orbit spinner instead of the static thinking block
|
|
1656
|
+
const hasThinking = currentBlocks.some((b) => b.type === "thinking");
|
|
1657
|
+
if (hasThinking) {
|
|
1658
|
+
currentBlocks = [];
|
|
1659
|
+
rebuildContainer(currentBlocks, "streaming");
|
|
1660
|
+
}
|
|
1649
1661
|
// Server tells us the tier, model, budget state
|
|
1650
1662
|
this.modeBar.setBudgetUsage(
|
|
1651
1663
|
part.budgetTotal > 0 ? part.budgetUsed / part.budgetTotal : 0,
|
|
@@ -1675,9 +1687,13 @@ export class App {
|
|
|
1675
1687
|
}
|
|
1676
1688
|
}
|
|
1677
1689
|
|
|
1678
|
-
const finalBlocks = currentBlocks
|
|
1679
|
-
b.type
|
|
1680
|
-
|
|
1690
|
+
const finalBlocks = currentBlocks
|
|
1691
|
+
.filter((b) => b.type !== "thinking")
|
|
1692
|
+
.map((b) => b.type === "markdown" ? { ...b, text: fullText || "*(no response)*" } : b);
|
|
1693
|
+
// If everything was filtered out (only had thinking), show no-response
|
|
1694
|
+
if (finalBlocks.length === 0) {
|
|
1695
|
+
finalBlocks.push({ type: "markdown", text: "*(no response)*" });
|
|
1696
|
+
}
|
|
1681
1697
|
store.updateMessageIn(sessionId, msgId, { content: finalBlocks, status: "complete" });
|
|
1682
1698
|
rebuildContainer(finalBlocks, "complete");
|
|
1683
1699
|
|
|
@@ -269,16 +269,22 @@ export class CodePanel {
|
|
|
269
269
|
}
|
|
270
270
|
|
|
271
271
|
private _widgetCount = 0;
|
|
272
|
+
private _placeholderRemoved = false;
|
|
273
|
+
|
|
274
|
+
/** Remove the placeholder text — call when widgets are about to load */
|
|
275
|
+
clearPlaceholder(): void {
|
|
276
|
+
if (this._placeholderRemoved) return;
|
|
277
|
+
this._placeholderRemoved = true;
|
|
278
|
+
const children = this.widgetsList.getChildren();
|
|
279
|
+
for (const child of [...children]) {
|
|
280
|
+
this.widgetsList.remove(child.id);
|
|
281
|
+
}
|
|
282
|
+
this.renderer.requestRender();
|
|
283
|
+
}
|
|
272
284
|
|
|
273
285
|
/** Add a widget renderable to the widgets tab */
|
|
274
286
|
addWidget(toolName: string, widget: BoxRenderable): void {
|
|
275
|
-
|
|
276
|
-
if (this._widgetCount === 0) {
|
|
277
|
-
const children = this.widgetsList.getChildren();
|
|
278
|
-
for (const child of [...children]) {
|
|
279
|
-
this.widgetsList.remove(child);
|
|
280
|
-
}
|
|
281
|
-
}
|
|
287
|
+
this.clearPlaceholder();
|
|
282
288
|
this._widgetCount++;
|
|
283
289
|
|
|
284
290
|
// Separator between widgets
|
|
@@ -307,9 +313,10 @@ export class CodePanel {
|
|
|
307
313
|
clearWidgets(): void {
|
|
308
314
|
const children = this.widgetsList.getChildren();
|
|
309
315
|
for (const child of [...children]) {
|
|
310
|
-
this.widgetsList.remove(child);
|
|
316
|
+
this.widgetsList.remove(child.id);
|
|
311
317
|
}
|
|
312
318
|
this._widgetCount = 0;
|
|
319
|
+
this._placeholderRemoved = false;
|
|
313
320
|
this.widgetsList.add(new TextRenderable(this.renderer, {
|
|
314
321
|
id: `widgets-empty-${Date.now()}`,
|
|
315
322
|
content: "*no widgets yet*\n\n*Enable \"Widgets in Tab\" in /settings.*",
|
package/src/research/apis.ts
CHANGED
|
@@ -6,12 +6,29 @@ const KALSHI = "https://api.elections.kalshi.com/trade-api/v2";
|
|
|
6
6
|
const DATA_API = "https://data-api.polymarket.com";
|
|
7
7
|
// EXA and CALA now proxied through API server — keys held server-side
|
|
8
8
|
|
|
9
|
-
async function get(url: string): Promise<any> {
|
|
10
|
-
const res = await fetch(url, {
|
|
9
|
+
async function get(url: string, timeoutMs = 10000): Promise<any> {
|
|
10
|
+
const res = await fetch(url, {
|
|
11
|
+
signal: AbortSignal.timeout(timeoutMs),
|
|
12
|
+
headers: { "Accept": "application/json", "User-Agent": "HorizonCode/0.6" },
|
|
13
|
+
});
|
|
11
14
|
if (!res.ok) throw new Error(`${res.status} ${url}`);
|
|
12
15
|
return res.json();
|
|
13
16
|
}
|
|
14
17
|
|
|
18
|
+
/** GET with retry — for flaky APIs like Kalshi */
|
|
19
|
+
async function getWithRetry(url: string, retries = 2, timeoutMs = 15000): Promise<any> {
|
|
20
|
+
let lastErr: Error | null = null;
|
|
21
|
+
for (let i = 0; i <= retries; i++) {
|
|
22
|
+
try {
|
|
23
|
+
return await get(url, timeoutMs);
|
|
24
|
+
} catch (e: any) {
|
|
25
|
+
lastErr = e;
|
|
26
|
+
if (i < retries) await new Promise((r) => setTimeout(r, 500 * (i + 1)));
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
throw lastErr;
|
|
30
|
+
}
|
|
31
|
+
|
|
15
32
|
async function post(url: string, body: any, headers: Record<string, string> = {}): Promise<any> {
|
|
16
33
|
const res = await fetch(url, {
|
|
17
34
|
method: "POST",
|
|
@@ -32,6 +49,28 @@ async function post(url: string, body: any, headers: Record<string, string> = {}
|
|
|
32
49
|
let _eventsCache: { data: any[]; ts: number } = { data: [], ts: 0 };
|
|
33
50
|
const CACHE_TTL = 60_000; // 60 seconds
|
|
34
51
|
|
|
52
|
+
/** Strip Gamma event to only the fields we use — reduces 16MB → ~200KB in memory */
|
|
53
|
+
function slimEvent(e: any): any {
|
|
54
|
+
return {
|
|
55
|
+
title: e.title, slug: e.slug, id: e.id,
|
|
56
|
+
description: (e.description ?? "").slice(0, 500),
|
|
57
|
+
volume: e.volume, liquidity: e.liquidity,
|
|
58
|
+
volume24hr: e.volume24hr, openInterest: e.openInterest,
|
|
59
|
+
startDate: e.startDate, endDate: e.endDate,
|
|
60
|
+
oneDayPriceChange: e.oneDayPriceChange,
|
|
61
|
+
tags: e.tags,
|
|
62
|
+
markets: (e.markets ?? []).map((m: any) => ({
|
|
63
|
+
question: m.question, conditionId: m.conditionId,
|
|
64
|
+
outcomePrices: m.outcomePrices,
|
|
65
|
+
volume: m.volume, liquidity: m.liquidity,
|
|
66
|
+
bestBid: m.bestBid, bestAsk: m.bestAsk,
|
|
67
|
+
lastTradePrice: m.lastTradePrice,
|
|
68
|
+
oneDayPriceChange: m.oneDayPriceChange,
|
|
69
|
+
clobTokenIds: m.clobTokenIds,
|
|
70
|
+
})),
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
35
74
|
async function fetchAllEvents(): Promise<any[]> {
|
|
36
75
|
if (_eventsCache.data.length > 0 && Date.now() - _eventsCache.ts < CACHE_TTL) {
|
|
37
76
|
return _eventsCache.data;
|
|
@@ -39,11 +78,14 @@ async function fetchAllEvents(): Promise<any[]> {
|
|
|
39
78
|
const params = new URLSearchParams({
|
|
40
79
|
active: "true", closed: "false",
|
|
41
80
|
order: "volume24hr", ascending: "false",
|
|
42
|
-
limit: "
|
|
81
|
+
limit: "100",
|
|
43
82
|
});
|
|
44
|
-
const
|
|
45
|
-
|
|
46
|
-
|
|
83
|
+
const raw = await get(`${GAMMA}/events?${params}`).catch(() => []);
|
|
84
|
+
const data = (raw ?? []).map(slimEvent);
|
|
85
|
+
if (data.length > 0) {
|
|
86
|
+
_eventsCache = { data, ts: Date.now() };
|
|
87
|
+
}
|
|
88
|
+
return data;
|
|
47
89
|
}
|
|
48
90
|
|
|
49
91
|
export async function gammaEvents(opts: { query?: string; limit?: number } = {}): Promise<any[]> {
|
|
@@ -312,8 +354,8 @@ function parseKalshiMarket(m: any): any {
|
|
|
312
354
|
let _kalshiCache: { data: any[]; ts: number } = { data: [], ts: 0 };
|
|
313
355
|
const KALSHI_CACHE_TTL = 60_000;
|
|
314
356
|
|
|
315
|
-
/** Fetch events from Kalshi —
|
|
316
|
-
async function fetchKalshiEvents(limit =
|
|
357
|
+
/** Fetch events from Kalshi — single page to avoid slow pagination */
|
|
358
|
+
async function fetchKalshiEvents(limit = 100): Promise<any[]> {
|
|
317
359
|
if (_kalshiCache.data.length > 0 && Date.now() - _kalshiCache.ts < KALSHI_CACHE_TTL) {
|
|
318
360
|
return _kalshiCache.data;
|
|
319
361
|
}
|
|
@@ -330,7 +372,7 @@ async function fetchKalshiEvents(limit = 200): Promise<any[]> {
|
|
|
330
372
|
});
|
|
331
373
|
if (cursor) params.set("cursor", cursor);
|
|
332
374
|
|
|
333
|
-
const data = await
|
|
375
|
+
const data = await getWithRetry(`${KALSHI}/events?${params}`).catch(() => ({ events: [], cursor: "" }));
|
|
334
376
|
const events = data?.events ?? [];
|
|
335
377
|
if (events.length === 0) break;
|
|
336
378
|
|
|
@@ -339,7 +381,10 @@ async function fetchKalshiEvents(limit = 200): Promise<any[]> {
|
|
|
339
381
|
if (!cursor) break;
|
|
340
382
|
}
|
|
341
383
|
|
|
342
|
-
|
|
384
|
+
// Only cache successful non-empty results — don't pollute cache with failures
|
|
385
|
+
if (all.length > 0) {
|
|
386
|
+
_kalshiCache = { data: all, ts: Date.now() };
|
|
387
|
+
}
|
|
343
388
|
return all;
|
|
344
389
|
}
|
|
345
390
|
|
|
@@ -383,7 +428,7 @@ export async function kalshiEvents(opts: { query?: string; limit?: number } = {}
|
|
|
383
428
|
status: "open", with_nested_markets: "true",
|
|
384
429
|
series_ticker: seriesTicker, limit: String(limit),
|
|
385
430
|
});
|
|
386
|
-
const data = await
|
|
431
|
+
const data = await getWithRetry(`${KALSHI}/events?${params}`);
|
|
387
432
|
const events = data?.events ?? [];
|
|
388
433
|
if (events.length > 0) return events.slice(0, limit).map(formatKalshiEvent);
|
|
389
434
|
} catch {}
|
|
@@ -413,7 +458,7 @@ export async function kalshiEvents(opts: { query?: string; limit?: number } = {}
|
|
|
413
458
|
status: "open", with_nested_markets: "true",
|
|
414
459
|
series_ticker: query.toUpperCase(), limit: String(limit),
|
|
415
460
|
});
|
|
416
|
-
const data = await
|
|
461
|
+
const data = await getWithRetry(`${KALSHI}/events?${params}`);
|
|
417
462
|
events = data?.events ?? [];
|
|
418
463
|
} catch {}
|
|
419
464
|
}
|
|
@@ -443,7 +488,7 @@ function formatKalshiEvent(e: any): any {
|
|
|
443
488
|
}
|
|
444
489
|
|
|
445
490
|
export async function kalshiEventDetail(ticker: string): Promise<any> {
|
|
446
|
-
const data = await
|
|
491
|
+
const data = await getWithRetry(`${KALSHI}/events/${encodeURIComponent(ticker)}?with_nested_markets=true`);
|
|
447
492
|
const event = data?.event ?? data;
|
|
448
493
|
if (!event) throw new Error(`Kalshi event not found: ${ticker}`);
|
|
449
494
|
|
|
@@ -457,7 +502,7 @@ export async function kalshiEventDetail(ticker: string): Promise<any> {
|
|
|
457
502
|
}
|
|
458
503
|
|
|
459
504
|
export async function kalshiOrderBook(ticker: string): Promise<any> {
|
|
460
|
-
const book = await
|
|
505
|
+
const book = await getWithRetry(`${KALSHI}/markets/${encodeURIComponent(ticker)}/orderbook`);
|
|
461
506
|
const ob = book?.orderbook ?? {};
|
|
462
507
|
// yes array: [[price_cents, size], ...] — bids for YES outcome
|
|
463
508
|
// no array: [[price_cents, size], ...] — asks (complement pricing)
|
|
@@ -489,7 +534,7 @@ export async function kalshiPriceHistory(ticker: string, period = "1w"): Promise
|
|
|
489
534
|
const intervalMap: Record<string, number> = { "1h": 1, "6h": 1, "1d": 60, "1w": 60, "1m": 1440, "max": 1440 };
|
|
490
535
|
const periodInterval = intervalMap[period] ?? 60;
|
|
491
536
|
|
|
492
|
-
const data = await
|
|
537
|
+
const data = await getWithRetry(`${KALSHI}/markets/${encodeURIComponent(ticker)}/candlesticks?period_interval=${periodInterval}`).catch(() => ({ candlesticks: [] }));
|
|
493
538
|
const candles = data?.candlesticks ?? [];
|
|
494
539
|
|
|
495
540
|
// Prices can be dollar strings or cent integers — normalize to decimal
|
package/src/research/widgets.ts
CHANGED
|
@@ -601,26 +601,28 @@ function renderKalshiList(data: any, renderer: CliRenderer): BoxRenderable {
|
|
|
601
601
|
exchangeBadge(box, renderer, "kalshi", "Markets");
|
|
602
602
|
const events = Array.isArray(data) ? data : (data?.events ?? []);
|
|
603
603
|
|
|
604
|
+
// Helper: truncate with ellipsis
|
|
605
|
+
const trunc = (s: string, max: number) => s.length > max ? s.slice(0, max - 1) + "\u2026" : s;
|
|
606
|
+
// Helper: format price as cents
|
|
607
|
+
const fmtCents = (v: any) => v != null ? `${(+v * 100).toFixed(0)}c` : "--";
|
|
608
|
+
|
|
604
609
|
for (let idx = 0; idx < Math.min(events.length, 8); idx++) {
|
|
605
610
|
const e = events[idx];
|
|
606
|
-
const
|
|
607
|
-
const
|
|
608
|
-
const
|
|
609
|
-
const
|
|
611
|
+
const m = e.markets?.[0];
|
|
612
|
+
const title = trunc(e.title ?? "", 36);
|
|
613
|
+
const bid = fmtCents(m?.yesBid ?? m?.lastPrice);
|
|
614
|
+
const ask = fmtCents(m?.yesAsk);
|
|
615
|
+
const vol = m?.volume ? `${(m.volume / 1000).toFixed(0)}k` : "";
|
|
616
|
+
const cat = trunc(e.category ?? "", 10);
|
|
610
617
|
|
|
611
|
-
const row = new BoxRenderable(renderer, { id: uid(), flexDirection: "row"
|
|
612
|
-
row.add(new TextRenderable(renderer, { id: uid(), content: `${(idx + 1).toString().padStart(2)}
|
|
613
|
-
row.add(new TextRenderable(renderer, { id: uid(), content:
|
|
614
|
-
row.add(new TextRenderable(renderer, { id: uid(), content:
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
row.add(new
|
|
618
|
-
row.add(new TextRenderable(renderer, { id: uid(), content: e.category ?? "", fg: COLORS.textMuted }));
|
|
618
|
+
const row = new BoxRenderable(renderer, { id: uid(), flexDirection: "row" });
|
|
619
|
+
row.add(new TextRenderable(renderer, { id: uid(), content: `${(idx + 1).toString().padStart(2)} `, fg: COLORS.textMuted }));
|
|
620
|
+
row.add(new TextRenderable(renderer, { id: uid(), content: title.padEnd(37), fg: COLORS.text }));
|
|
621
|
+
row.add(new TextRenderable(renderer, { id: uid(), content: bid.padStart(5), fg: COLORS.success }));
|
|
622
|
+
row.add(new TextRenderable(renderer, { id: uid(), content: `/${ask.padEnd(5)}`, fg: COLORS.error }));
|
|
623
|
+
row.add(new TextRenderable(renderer, { id: uid(), content: vol.padStart(6), fg: COLORS.textMuted }));
|
|
624
|
+
row.add(new TextRenderable(renderer, { id: uid(), content: ` ${cat}`, fg: COLORS.borderDim }));
|
|
619
625
|
box.add(row);
|
|
620
|
-
|
|
621
|
-
if (e.ticker) {
|
|
622
|
-
box.add(new TextRenderable(renderer, { id: uid(), content: ` ${e.ticker}`, fg: COLORS.borderDim }));
|
|
623
|
-
}
|
|
624
626
|
}
|
|
625
627
|
|
|
626
628
|
return box;
|
|
@@ -773,26 +775,28 @@ function renderProbability(data: any, renderer: CliRenderer): BoxRenderable {
|
|
|
773
775
|
function renderKalshiDetail(data: any, renderer: CliRenderer): BoxRenderable {
|
|
774
776
|
const box = new BoxRenderable(renderer, { id: uid(), flexDirection: "column", marginBottom: 1 });
|
|
775
777
|
|
|
776
|
-
|
|
778
|
+
const trunc = (s: string, max: number) => s.length > max ? s.slice(0, max - 1) + "\u2026" : s;
|
|
779
|
+
const fmtCents = (v: any) => v != null ? `${(+v * 100).toFixed(0)}c` : "--";
|
|
780
|
+
|
|
781
|
+
exchangeBadge(box, renderer, "kalshi", trunc(data.title ?? data.ticker ?? "", 50));
|
|
777
782
|
if (data.category) {
|
|
778
783
|
box.add(new TextRenderable(renderer, { id: uid(), content: ` ${data.category} ${data.ticker ?? ""}`, fg: COLORS.textMuted }));
|
|
779
784
|
}
|
|
780
785
|
sep(box, renderer);
|
|
781
786
|
|
|
782
787
|
for (const m of (data.markets ?? []).slice(0, 8)) {
|
|
783
|
-
const row = new BoxRenderable(renderer, { id: uid(), flexDirection: "row"
|
|
784
|
-
const title = (m.title ?? m.ticker ?? ""
|
|
785
|
-
|
|
786
|
-
const
|
|
787
|
-
const
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
}
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
}
|
|
788
|
+
const row = new BoxRenderable(renderer, { id: uid(), flexDirection: "row" });
|
|
789
|
+
const title = trunc(m.title ?? m.ticker ?? "", 32);
|
|
790
|
+
const bid = fmtCents(m.yesBid ?? m.lastPrice);
|
|
791
|
+
const ask = fmtCents(m.yesAsk);
|
|
792
|
+
const spread = m.spread != null ? `${(+m.spread * 100).toFixed(0)}c` : "";
|
|
793
|
+
const vol = m.volume ? `${(m.volume / 1000).toFixed(0)}k` : "";
|
|
794
|
+
|
|
795
|
+
row.add(new TextRenderable(renderer, { id: uid(), content: title.padEnd(33), fg: COLORS.text }));
|
|
796
|
+
row.add(new TextRenderable(renderer, { id: uid(), content: bid.padStart(5), fg: COLORS.success }));
|
|
797
|
+
row.add(new TextRenderable(renderer, { id: uid(), content: `/${ask.padEnd(5)}`, fg: COLORS.error }));
|
|
798
|
+
row.add(new TextRenderable(renderer, { id: uid(), content: spread.padStart(5), fg: COLORS.textMuted }));
|
|
799
|
+
row.add(new TextRenderable(renderer, { id: uid(), content: vol.padStart(6), fg: COLORS.textMuted }));
|
|
796
800
|
box.add(row);
|
|
797
801
|
}
|
|
798
802
|
|