horizon-code 0.6.0 → 0.6.1

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.0",
3
+ "version": "0.6.1",
4
4
  "description": "AI-powered trading strategy terminal for Polymarket",
5
5
  "type": "module",
6
6
  "bin": {
package/src/ai/client.ts CHANGED
@@ -32,7 +32,7 @@ export async function initMCP(): Promise<{ toolCount: number }> {
32
32
  });
33
33
  mcpTools = await mcpClient.tools();
34
34
  return { toolCount: Object.keys(mcpTools).length };
35
- } catch { return { toolCount: 0 }; }
35
+ } catch (e) { console.error("[mcp] init failed:", e); return { toolCount: 0 }; }
36
36
  }
37
37
 
38
38
  export async function closeMCP(): Promise<void> {
@@ -141,7 +141,7 @@ async function* consumeSSE(res: Response): AsyncGenerator<Record<string, any>> {
141
141
  if (!line.startsWith("data: ")) continue;
142
142
  const data = line.slice(6);
143
143
  if (data === "[DONE]") return;
144
- try { yield JSON.parse(data); } catch {}
144
+ try { yield JSON.parse(data); } catch (e) { console.error("[sse] malformed JSON:", data.slice(0, 100)); }
145
145
  }
146
146
  }
147
147
  }
@@ -162,7 +162,7 @@ function toolsToServerFormat(tools: Record<string, any>): Record<string, any> {
162
162
  const converted = zodSchema(tool.parameters);
163
163
  params = converted.jsonSchema ?? params;
164
164
  }
165
- } catch {}
165
+ } catch (e) { failures++; console.error("[tools] schema conversion failed for", name, e); }
166
166
  result[name] = { description: tool.description ?? "", parameters: params };
167
167
  }
168
168
  return result;
@@ -273,6 +273,7 @@ export async function* chat(
273
273
  let emittedResponseLen = 0;
274
274
  let emittedCode: string | null = null;
275
275
  let structuredActive = useStructuredOutput;
276
+ let emittedAnyText = false;
276
277
  const pendingToolCalls: { toolName: string; args: Record<string, unknown> }[] = [];
277
278
  let eventCount = 0;
278
279
 
@@ -297,6 +298,7 @@ export async function* chat(
297
298
  if (trimmed.length > 3 && !trimmed.startsWith("{") && !trimmed.startsWith("[")) {
298
299
  structuredActive = false;
299
300
  yield { type: "text-delta", textDelta: jsonBuf };
301
+ emittedAnyText = true;
300
302
  jsonBuf = "";
301
303
  continue;
302
304
  }
@@ -304,6 +306,7 @@ export async function* chat(
304
306
  const response = extractJsonStringField(jsonBuf, "response");
305
307
  if (response && response.length > emittedResponseLen) {
306
308
  yield { type: "text-delta", textDelta: response.slice(emittedResponseLen) };
309
+ emittedAnyText = true;
307
310
  emittedResponseLen = response.length;
308
311
  }
309
312
 
@@ -314,6 +317,7 @@ export async function* chat(
314
317
  }
315
318
  } else {
316
319
  yield { type: "text-delta", textDelta: delta };
320
+ emittedAnyText = true;
317
321
  }
318
322
  } else if (event.type === "tool-call") {
319
323
  hasToolCalls = true;
@@ -339,15 +343,17 @@ export async function* chat(
339
343
  // the model likely responded with plain text — flush jsonBuf as text
340
344
  if (structuredActive && jsonBuf && emittedResponseLen === 0) {
341
345
  yield { type: "text-delta", textDelta: jsonBuf };
346
+ emittedAnyText = true;
342
347
  }
343
348
 
344
349
  // If no tool calls, we're done
345
350
  if (!hasToolCalls || pendingToolCalls.length === 0) {
346
351
  if (eventCount === 0) {
347
352
  yield { type: "error", message: "No response from server. Check your connection or try again." };
348
- } else if (!hasToolCalls && jsonBuf === "" && emittedResponseLen === 0) {
349
- // Server sent events (meta, usage) but no actual content
350
- yield { type: "text-delta", textDelta: "*(Server returned no content. This may happen if the model is overloaded. Try again.)*" };
353
+ } else if (!emittedAnyText && !hasToolCalls && emittedCode === null) {
354
+ // Server sent events (meta, usage) but no actual text or code content
355
+ console.error("[chat] No content received. Events:", eventCount, "jsonBuf:", jsonBuf.slice(0, 200));
356
+ yield { type: "text-delta", textDelta: "*(No response received. Try again.)*" };
351
357
  }
352
358
  yield { type: "finish" };
353
359
  return;
@@ -391,6 +397,7 @@ export async function* chat(
391
397
  if (structuredActive) {
392
398
  jsonBuf = "";
393
399
  emittedResponseLen = 0;
400
+ emittedAnyText = false;
394
401
  }
395
402
 
396
403
  // Loop back — server will call OpenRouter again with the full conversation including tool results
@@ -131,7 +131,7 @@ export function getSystemPrompt(mode: Mode, verbosity: string = "normal"): strin
131
131
  const { buildProfileContext } = require("../platform/profile.ts");
132
132
  const profileCtx = buildProfileContext();
133
133
  if (profileCtx) prompt += "\n\n" + profileCtx;
134
- } catch {}
134
+ } catch (e) { console.error("[prompt] profile context failed:", e); }
135
135
  // Inject active strategy context so research tools auto-scope
136
136
  const draft = store.getActiveSession()?.strategyDraft;
137
137
  if (draft?.code) {
package/src/app.ts CHANGED
@@ -223,7 +223,7 @@ export class App {
223
223
  for (const s of dbSessions) {
224
224
  await deleteDbSession(s.id);
225
225
  }
226
- } catch {}
226
+ } catch (e: any) { console.error("[app] delete chats failed:", e?.message); }
227
227
  this.showSystemMsg("All chats deleted.");
228
228
  this.settingsPanel.hide();
229
229
  });
@@ -461,7 +461,7 @@ export class App {
461
461
  this.keyHandler.codePanelVisible = this.codePanel.visible;
462
462
 
463
463
  this.renderer.requestRender();
464
- } catch {}
464
+ } catch (e: any) { console.error("[app] state listener error:", e?.message); }
465
465
  });
466
466
 
467
467
  renderer.on("resize", () => renderer.requestRender());
@@ -578,7 +578,7 @@ export class App {
578
578
  this.codePanel.appendLog(r.actionTaken);
579
579
  }
580
580
  }
581
- } catch {}
581
+ } catch (e: any) { console.error("[app] alert check failed:", e?.message); }
582
582
  })();
583
583
  })(localMetricsData);
584
584
 
@@ -590,7 +590,7 @@ export class App {
590
590
  for (const [pid] of runningProcesses) {
591
591
  if (isRecording(pid)) recordMetrics(pid, metrics as any);
592
592
  }
593
- } catch {}
593
+ } catch (e: any) { console.error("[app] replay record failed:", e?.message); }
594
594
  })();
595
595
  })(localMetricsData);
596
596
  } else if (this._hasLocalMetrics && alive === 0) {
@@ -720,11 +720,11 @@ export class App {
720
720
  this.showSystemMsg(`Logged in as ${loginResult.email}`);
721
721
  // Start session sync + platform sync now that we have a live session
722
722
  import("./platform/session-sync.ts").then(({ loadSessions, startAutoSave }) => {
723
- loadSessions().catch(() => {});
723
+ loadSessions().catch((e: any) => console.error("[app] session/sync init failed:", e?.message));
724
724
  startAutoSave();
725
- }).catch(() => {});
725
+ }).catch((e: any) => console.error("[app] session/sync init failed:", e?.message));
726
726
  import("./platform/sync.ts").then(({ platformSync }) => {
727
- platformSync.start(30000).catch(() => {});
727
+ platformSync.start(30000).catch((e: any) => console.error("[app] platformSync start failed:", e?.message));
728
728
  });
729
729
  } else {
730
730
  this.showSystemMsg(`Login failed: ${loginResult.error}`);
@@ -1079,15 +1079,19 @@ export class App {
1079
1079
  const { hasLiveSession } = await import("./platform/supabase.ts");
1080
1080
  const live = await hasLiveSession();
1081
1081
 
1082
+ const { loadSessions, startAutoSave } = await import("./platform/session-sync.ts");
1082
1083
  if (live) {
1083
- const { loadSessions, startAutoSave } = await import("./platform/session-sync.ts");
1084
- await loadSessions().catch(() => {});
1084
+ await loadSessions().catch((e) => {
1085
+ this.splash.setLoading(`Failed to load chats: ${e?.message ?? "unknown error"}`);
1086
+ });
1085
1087
  startAutoSave();
1088
+ } else {
1089
+ this.splash.setLoading("Session expired -- type /login to restore your chats");
1086
1090
  }
1087
1091
 
1088
1092
  // Start platform sync (works with API key)
1089
1093
  const { platformSync } = await import("./platform/sync.ts");
1090
- platformSync.start(30000).catch(() => {});
1094
+ platformSync.start(30000).catch((e: any) => console.error("[app] platformSync start failed:", e?.message));
1091
1095
 
1092
1096
  // Final status
1093
1097
  const firstTime = !cfg.has_launched;
@@ -1128,6 +1132,10 @@ export class App {
1128
1132
  store.update({ sessions });
1129
1133
  for (const [id] of this.messageRenderables) this.scrollBox.remove(`msg-${id}`);
1130
1134
  this.messageRenderables.clear();
1135
+ this.codePanel.clearWidgets();
1136
+ this.codePanel.setLogs("");
1137
+ this.codePanel.setCode("", "none");
1138
+ this.codePanel.setMetrics(null);
1131
1139
  this.updateContextMeter();
1132
1140
  this.inputBar.focus();
1133
1141
  this.renderer.requestRender();
@@ -1494,13 +1502,20 @@ export class App {
1494
1502
  }
1495
1503
  } else if (part.type === "tool-result") {
1496
1504
  // Replace the spinning tool-call with a completed tool-result (same line, not stacked)
1505
+ // Include toolResult so renderer can detect errors and show brief explanations
1506
+ const hasError = part.result && typeof part.result === "object" && "error" in (part.result as any);
1507
+ const resultBlock: import("./chat/types.ts").ContentBlock = {
1508
+ type: "tool-result",
1509
+ toolName: part.toolName,
1510
+ ...(hasError ? { toolResult: part.result } : {}),
1511
+ };
1497
1512
  const callIdx = currentBlocks.findIndex(
1498
1513
  (b) => b.type === "tool-call" && b.toolName === part.toolName,
1499
1514
  );
1500
1515
  if (callIdx !== -1) {
1501
- currentBlocks[callIdx] = { type: "tool-result", toolName: part.toolName };
1516
+ currentBlocks[callIdx] = resultBlock;
1502
1517
  } else {
1503
- currentBlocks.push({ type: "tool-result", toolName: part.toolName });
1518
+ currentBlocks.push(resultBlock);
1504
1519
  }
1505
1520
 
1506
1521
  if (WIDGET_TOOLS.has(part.toolName)) {
@@ -1774,7 +1789,7 @@ export class App {
1774
1789
  const { saveActiveSession, stopAutoSave } = await import("./platform/session-sync.ts");
1775
1790
  stopAutoSave();
1776
1791
  await saveActiveSession();
1777
- } catch {}
1792
+ } catch (e: any) { console.error("[app] shutdown save failed:", e?.message); }
1778
1793
  if (dashboard.running) dashboard.stop();
1779
1794
  cleanupStrategyProcesses();
1780
1795
  destroyTreeSitterClient();
@@ -33,7 +33,7 @@ const syntaxStyle = SyntaxStyle.fromStyles({
33
33
  "@punctuation.special": { fg: h("#D7BA7D") },
34
34
  });
35
35
 
36
- // Braille spinner frames
36
+ // Braille spinner frames (tool calls)
37
37
  const BRAILLE = [
38
38
  "\u2801", "\u2803", "\u2807", "\u280f",
39
39
  "\u281f", "\u283f", "\u287f", "\u28ff",
@@ -41,6 +41,11 @@ const BRAILLE = [
41
41
  "\u28e0", "\u28c0", "\u2880", "\u2800",
42
42
  ];
43
43
 
44
+ // Dot orbit spinner for header (different visual rhythm)
45
+ const ORBIT = [
46
+ "\u25DC", "\u25DD", "\u25DE", "\u25DF", // ◜ ◝ ◞ ◟
47
+ ];
48
+
44
49
  // Tool name → human-readable label
45
50
  function toolLabel(name: string): string {
46
51
  return name.replace(/_/g, " ");
@@ -62,9 +67,14 @@ export class ChatRenderer {
62
67
  this.spinnerTimer = setInterval(() => {
63
68
  if (this.spinnerNodes.length === 0) return;
64
69
  this.spinnerFrame = (this.spinnerFrame + 1) % BRAILLE.length;
65
- const char = BRAILLE[this.spinnerFrame]!;
70
+ const brailleChar = BRAILLE[this.spinnerFrame]!;
71
+ const orbitChar = ORBIT[this.spinnerFrame % ORBIT.length]!;
66
72
  for (const node of this.spinnerNodes) {
67
- node.content = `${char} `;
73
+ if (node.id.includes("stream-spin")) {
74
+ node.content = ` ${orbitChar}`;
75
+ } else {
76
+ node.content = `${brailleChar} `;
77
+ }
68
78
  }
69
79
  this.renderer.requestRender();
70
80
  }, 60);
@@ -80,6 +90,7 @@ export class ChatRenderer {
80
90
 
81
91
  renderMessage(message: Message): BoxRenderable {
82
92
  const isUser = message.role === "user";
93
+ const isStreaming = message.status === "streaming" || message.status === "thinking";
83
94
 
84
95
  const box = new BoxRenderable(this.renderer, {
85
96
  id: `msg-${message.id}`,
@@ -92,16 +103,60 @@ export class ChatRenderer {
92
103
  });
93
104
 
94
105
  if (message.role === "user" || message.role === "assistant") {
95
- box.add(new TextRenderable(this.renderer, {
106
+ const headerBox = new BoxRenderable(this.renderer, {
107
+ id: `msg-header-${message.id}`,
108
+ flexDirection: "row",
109
+ width: "100%",
110
+ });
111
+ headerBox.add(new TextRenderable(this.renderer, {
96
112
  id: `msg-label-${message.id}`,
97
113
  content: isUser ? "U S E R" : "H O R I Z O N",
98
114
  fg: isUser ? COLORS.textMuted : COLORS.borderDim,
99
115
  }));
116
+
117
+ // Streaming indicator next to the role label — always visible at top
118
+ if (!isUser && isStreaming) {
119
+ const streamSpinner = new TextRenderable(this.renderer, {
120
+ id: `msg-stream-spin-${message.id}`,
121
+ content: ` ${ORBIT[0]}`,
122
+ fg: COLORS.accent,
123
+ });
124
+ this.spinnerNodes.push(streamSpinner);
125
+ headerBox.add(streamSpinner);
126
+ }
127
+
128
+ box.add(headerBox);
100
129
  }
101
130
 
131
+ // Render tool blocks first, then content blocks (text/markdown/widgets)
132
+ // This ensures tools are always at the top, generation text at the bottom
133
+ const toolBlocks: { block: ContentBlock; idx: number }[] = [];
134
+ const contentBlocks: { block: ContentBlock; idx: number }[] = [];
135
+
102
136
  for (let i = 0; i < message.content.length; i++) {
103
137
  const block = message.content[i]!;
104
- const renderable = this.renderBlock(block, message, i);
138
+ if (block.type === "tool-call" || block.type === "tool-result") {
139
+ toolBlocks.push({ block, idx: i });
140
+ } else {
141
+ contentBlocks.push({ block, idx: i });
142
+ }
143
+ }
144
+
145
+ for (const { block, idx } of toolBlocks) {
146
+ const renderable = this.renderBlock(block, message, idx);
147
+ if (renderable) box.add(renderable);
148
+ }
149
+
150
+ // Add spacing between tool section and content
151
+ if (toolBlocks.length > 0 && contentBlocks.length > 0) {
152
+ box.add(new TextRenderable(this.renderer, {
153
+ id: `msg-tool-spacer-${message.id}`,
154
+ content: "",
155
+ }));
156
+ }
157
+
158
+ for (const { block, idx } of contentBlocks) {
159
+ const renderable = this.renderBlock(block, message, idx);
105
160
  if (renderable) box.add(renderable);
106
161
  }
107
162
 
@@ -144,12 +199,19 @@ export class ChatRenderer {
144
199
  case "thinking":
145
200
  return this.renderThinking(message.id, index);
146
201
 
147
- case "error":
202
+ case "error": {
203
+ const rawErr = block.text ?? "Unknown error";
204
+ // Clean up error text: first line only, remove stack traces and verbose prefixes
205
+ const cleanErr = rawErr
206
+ .replace(/^Error:\s*/i, "")
207
+ .replace(/\n[\s\S]*/, "")
208
+ .slice(0, 120);
148
209
  return new TextRenderable(this.renderer, {
149
210
  id: `msg-err-${message.id}-${index}`,
150
- content: `x ${block.text ?? "Unknown error"}`,
211
+ content: `\u2716 ${cleanErr}`,
151
212
  fg: COLORS.error,
152
213
  });
214
+ }
153
215
 
154
216
  default:
155
217
  return null;
@@ -179,7 +241,7 @@ export class ChatRenderer {
179
241
  return box;
180
242
  }
181
243
 
182
- // Tool in-progress: braille spinner + name
244
+ // Tool in-progress: accent braille spinner + name
183
245
  private renderToolCall(block: ContentBlock, msgId: string, index: number): BoxRenderable {
184
246
  const box = new BoxRenderable(this.renderer, {
185
247
  id: `msg-tool-${msgId}-${index}`,
@@ -189,7 +251,7 @@ export class ChatRenderer {
189
251
  const spinner = new TextRenderable(this.renderer, {
190
252
  id: `msg-tool-icon-${msgId}-${index}`,
191
253
  content: `${BRAILLE[0]} `,
192
- fg: COLORS.textMuted,
254
+ fg: COLORS.accent,
193
255
  });
194
256
  this.spinnerNodes.push(spinner);
195
257
  box.add(spinner);
@@ -197,34 +259,54 @@ export class ChatRenderer {
197
259
  box.add(new TextRenderable(this.renderer, {
198
260
  id: `msg-tool-name-${msgId}-${index}`,
199
261
  content: toolLabel(block.toolName ?? "tool"),
200
- fg: COLORS.textMuted,
262
+ fg: COLORS.text,
201
263
  }));
202
264
 
203
265
  return box;
204
266
  }
205
267
 
206
- // Tool completed: tick or error icon + name
268
+ // Tool completed: green check or red error with brief explanation
207
269
  private renderToolResult(block: ContentBlock, msgId: string, index: number): BoxRenderable {
208
270
  const box = new BoxRenderable(this.renderer, {
209
271
  id: `msg-result-${msgId}-${index}`,
210
272
  flexDirection: "row",
211
- marginBottom: 1,
212
273
  });
213
274
 
214
275
  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
276
 
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
- }));
277
+ if (hasError) {
278
+ // Red x + tool name + brief, human-readable error
279
+ box.add(new TextRenderable(this.renderer, {
280
+ id: `msg-result-icon-${msgId}-${index}`,
281
+ content: "\u2716 ",
282
+ fg: COLORS.error,
283
+ }));
284
+
285
+ const label = toolLabel(block.toolName ?? "tool");
286
+ const rawError = String((block.toolResult as any).error ?? "failed");
287
+ // Clean up common error patterns to be user-friendly
288
+ const briefError = rawError
289
+ .replace(/^Error:\s*/i, "")
290
+ .replace(/\n[\s\S]*/, "") // first line only
291
+ .slice(0, 80);
292
+ box.add(new TextRenderable(this.renderer, {
293
+ id: `msg-result-name-${msgId}-${index}`,
294
+ content: `${label} -- ${briefError}`,
295
+ fg: COLORS.error,
296
+ }));
297
+ } else {
298
+ // Green check + tool name
299
+ box.add(new TextRenderable(this.renderer, {
300
+ id: `msg-result-icon-${msgId}-${index}`,
301
+ content: "\u2713 ",
302
+ fg: COLORS.success,
303
+ }));
304
+ box.add(new TextRenderable(this.renderer, {
305
+ id: `msg-result-name-${msgId}-${index}`,
306
+ content: toolLabel(block.toolName ?? "done"),
307
+ fg: COLORS.textMuted,
308
+ }));
309
+ }
228
310
 
229
311
  return box;
230
312
  }
@@ -1,7 +1,7 @@
1
1
  import { saveConfig, loadConfig, getApiKey } from "./config.ts";
2
2
  import { platform } from "./client.ts";
3
3
 
4
- export type AuthStatus = "authenticated" | "no_key" | "invalid_key";
4
+ export type AuthStatus = "authenticated" | "no_key" | "invalid_key" | "network_error";
5
5
 
6
6
  export async function checkAuth(): Promise<AuthStatus> {
7
7
  const key = getApiKey();
@@ -13,8 +13,8 @@ export async function checkAuth(): Promise<AuthStatus> {
13
13
  return "authenticated";
14
14
  } catch (err: any) {
15
15
  if (err?.status === 401 || err?.status === 403) return "invalid_key";
16
- // Network error or server down assume key is fine
17
- return "authenticated";
16
+ // Network error can't verify, report as network error
17
+ return "network_error";
18
18
  }
19
19
  }
20
20
 
@@ -84,7 +84,7 @@ export async function loadSessions(): Promise<void> {
84
84
  openTabIds: openIds,
85
85
  });
86
86
  }
87
- } catch {}
87
+ } catch (e: any) { console.error("[session-sync] loadSessions failed:", e?.message ?? e); }
88
88
  }
89
89
 
90
90
  /**
@@ -132,7 +132,7 @@ export async function saveActiveSession(): Promise<void> {
132
132
 
133
133
  // Update session metadata
134
134
  await updateDbSession(dbId, { name: session.name }).catch(() => {});
135
- } catch {}
135
+ } catch (e: any) { console.error("[session-sync] saveActiveSession failed:", e?.message ?? e); }
136
136
  }
137
137
 
138
138
  /**
@@ -140,7 +140,7 @@ export async function saveActiveSession(): Promise<void> {
140
140
  */
141
141
  export async function deleteSessionFromDb(sessionId: string): Promise<void> {
142
142
  if (!(await isLoggedIn())) return;
143
- try { await deleteDbSession(sessionId); } catch {}
143
+ try { await deleteDbSession(sessionId); } catch (e: any) { console.error("[session-sync] deleteSessionFromDb failed:", e?.message ?? e); }
144
144
  }
145
145
 
146
146
  /**
@@ -34,7 +34,8 @@ export function getSupabase(): SupabaseClient {
34
34
  refresh_token: encryptEnvVar(session.refresh_token),
35
35
  };
36
36
  config.session_encrypted = true;
37
- } catch {
37
+ } catch (e) {
38
+ console.error("[supabase] encryption failed, using plaintext:", e);
38
39
  // Fallback to plaintext if encryption fails
39
40
  config.supabase_session = {
40
41
  access_token: session.access_token,
@@ -67,7 +68,7 @@ function saveSessionTokens(config: ReturnType<typeof loadConfig>, accessToken: s
67
68
  config.supabase_session = { access_token: encryptEnvVar(accessToken), refresh_token: encryptEnvVar(refreshToken) };
68
69
  config.session_encrypted = true;
69
70
  return;
70
- } catch {}
71
+ } catch (e) { console.error("[supabase] saveSessionTokens encryption failed:", e); }
71
72
  }
72
73
  config.supabase_session = { access_token: accessToken, refresh_token: refreshToken };
73
74
  config.session_encrypted = false;
@@ -125,7 +126,7 @@ export async function restoreSession(): Promise<boolean> {
125
126
  if (config.api_key) platform.setApiKey(config.api_key);
126
127
  return true;
127
128
  }
128
- } catch {}
129
+ } catch (e) { console.error("[supabase] setSession failed:", e); }
129
130
 
130
131
  // Access token expired — try refreshing with just the refresh token
131
132
  try {
@@ -142,7 +143,7 @@ export async function restoreSession(): Promise<boolean> {
142
143
  if (config.api_key) platform.setApiKey(config.api_key);
143
144
  return true;
144
145
  }
145
- } catch {}
146
+ } catch (e) { console.error("[supabase] refreshSession failed:", e); }
146
147
 
147
148
  // Both failed — don't delete the session, it might work next time
148
149
  // (network issue, Supabase outage, etc.). The API key still works for chat.
@@ -224,7 +225,7 @@ export async function loginWithBrowser(): Promise<{ success: boolean; error?: st
224
225
  try {
225
226
  const apiKey = await createApiKey();
226
227
  if (apiKey) { config.api_key = apiKey; platform.setApiKey(apiKey); }
227
- } catch {}
228
+ } catch (e) { console.error("[supabase] createApiKey failed:", e); }
228
229
  } else {
229
230
  platform.setApiKey(config.api_key);
230
231
  }
@@ -235,10 +236,10 @@ export async function loginWithBrowser(): Promise<{ success: boolean; error?: st
235
236
  }
236
237
  if (res.status === 410) return { success: false, error: "Session expired. Type /login to sign in again." };
237
238
  if (res.status === 404) return { success: false, error: "Auth session not found." };
238
- } catch {}
239
+ } catch (e) { console.error("[supabase] poll timeout:", e); }
239
240
  }
240
241
 
241
- try { await sb.from("cli_auth_sessions").delete().eq("id", sessionId); } catch {}
242
+ try { await sb.from("cli_auth_sessions").delete().eq("id", sessionId); } catch (e) { console.error("[supabase] cleanup auth session failed:", e); }
242
243
  return { success: false, error: "Login timed out (90s). Try /login again." };
243
244
  }
244
245
 
@@ -260,7 +261,7 @@ export async function loginWithPassword(email: string, password: string): Promis
260
261
  try {
261
262
  const apiKey = await createApiKey();
262
263
  if (apiKey) { config.api_key = apiKey; platform.setApiKey(apiKey); }
263
- } catch {}
264
+ } catch (e) { console.error("[supabase] createApiKey failed:", e); }
264
265
  } else {
265
266
  platform.setApiKey(config.api_key);
266
267
  }
@@ -295,7 +296,7 @@ async function createApiKey(): Promise<string | null> {
295
296
  });
296
297
  if (error) return null;
297
298
  return rawKey;
298
- } catch { return null; }
299
+ } catch (e) { console.error("[supabase] createApiKey failed:", e); return null; }
299
300
  }
300
301
 
301
302
  // ── Logout ──
@@ -8,8 +8,11 @@ const pnlHistories: Map<string, number[]> = new Map();
8
8
 
9
9
  export class PlatformSync {
10
10
  private timer: ReturnType<typeof setInterval> | null = null;
11
+ private _polling = false;
11
12
 
12
13
  async start(intervalMs = 30000): Promise<void> {
14
+ if (this.timer) return; // Prevent duplicate timers
15
+
13
16
  const loggedIn = await isLoggedIn();
14
17
  if (!loggedIn && !platform.authenticated) return;
15
18
 
@@ -22,6 +25,8 @@ export class PlatformSync {
22
25
  }
23
26
 
24
27
  private async poll(): Promise<void> {
28
+ if (this._polling) return;
29
+ this._polling = true;
25
30
  try {
26
31
  const strategies = await platform.listStrategies();
27
32
 
@@ -88,8 +93,11 @@ export class PlatformSync {
88
93
  });
89
94
 
90
95
  store.update({ deployments, connection: "connected" });
91
- } catch {
96
+ } catch (e) {
97
+ console.error("[sync] poll failed:", e);
92
98
  store.update({ connection: "disconnected" });
99
+ } finally {
100
+ this._polling = false;
93
101
  }
94
102
  }
95
103
  }