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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "horizon-code",
3
- "version": "0.6.1",
3
+ "version": "0.6.2",
4
4
  "description": "AI-powered trading strategy terminal for Polymarket",
5
5
  "type": "module",
6
6
  "bin": {
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.map((b) =>
1679
- b.type === "markdown" ? { ...b, text: fullText || "*(no response)*" } : b
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
- // Remove placeholder on first widget
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.*",
@@ -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, { signal: AbortSignal.timeout(10000) });
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: "200",
81
+ limit: "100",
43
82
  });
44
- const data = await get(`${GAMMA}/events?${params}`).catch(() => []);
45
- _eventsCache = { data: data ?? [], ts: Date.now() };
46
- return _eventsCache.data;
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 — uses cursor pagination to get more results */
316
- async function fetchKalshiEvents(limit = 200): Promise<any[]> {
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 get(`${KALSHI}/events?${params}`).catch(() => ({ events: [], cursor: "" }));
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
- _kalshiCache = { data: all, ts: Date.now() };
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 get(`${KALSHI}/events?${params}`);
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 get(`${KALSHI}/events?${params}`);
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 get(`${KALSHI}/events/${encodeURIComponent(ticker)}?with_nested_markets=true`);
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 get(`${KALSHI}/markets/${encodeURIComponent(ticker)}/orderbook`);
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 get(`${KALSHI}/markets/${encodeURIComponent(ticker)}/candlesticks?period_interval=${periodInterval}`).catch(() => ({ candlesticks: [] }));
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
@@ -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 market = e.markets?.[0];
607
- const yesBid = market?.yesBid ?? market?.lastPrice ?? null;
608
- const yesAsk = market?.yesAsk ?? null;
609
- const price = yesBid != null ? `${yesBid}c` : "--";
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", width: "100%" });
612
- row.add(new TextRenderable(renderer, { id: uid(), content: `${(idx + 1).toString().padStart(2)} `, fg: COLORS.textMuted }));
613
- row.add(new TextRenderable(renderer, { id: uid(), content: (e.title ?? "").slice(0, 40).padEnd(43), fg: COLORS.text, attributes: 1 }));
614
- row.add(new TextRenderable(renderer, { id: uid(), content: ` Y ${price}`.padEnd(10), fg: COLORS.success }));
615
- if (yesAsk != null) row.add(new TextRenderable(renderer, { id: uid(), content: `A ${yesAsk}c`.padEnd(8), fg: COLORS.error }));
616
- if (market?.volume) row.add(new TextRenderable(renderer, { id: uid(), content: ` vol ${market.volume.toLocaleString()}`, fg: COLORS.textMuted }));
617
- row.add(new BoxRenderable(renderer, { id: uid(), flexGrow: 1 }));
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
- exchangeBadge(box, renderer, "kalshi", data.title ?? data.ticker ?? "");
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", width: "100%" });
784
- const title = (m.title ?? m.ticker ?? "").padEnd(40).slice(0, 40);
785
- row.add(new TextRenderable(renderer, { id: uid(), content: title, fg: COLORS.textMuted }));
786
- const bid = m.yesBid ?? m.lastPrice ?? null;
787
- const ask = m.yesAsk ?? null;
788
- row.add(new TextRenderable(renderer, { id: uid(), content: `Bid ${bid != null ? bid + "c" : "--"}`.padEnd(10), fg: COLORS.success }));
789
- row.add(new TextRenderable(renderer, { id: uid(), content: `Ask ${ask != null ? ask + "c" : "--"}`.padEnd(10), fg: COLORS.error }));
790
- if (m.spread != null) {
791
- row.add(new TextRenderable(renderer, { id: uid(), content: `spread ${m.spread}c`.padEnd(14), fg: COLORS.textMuted }));
792
- }
793
- if (m.volume) {
794
- row.add(new TextRenderable(renderer, { id: uid(), content: `vol ${m.volume.toLocaleString()}`, fg: COLORS.textMuted }));
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