horizon-code 0.6.0 → 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.0",
3
+ "version": "0.6.2",
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();
@@ -1471,6 +1479,11 @@ export class App {
1471
1479
  currentBlocks.push({ type: "tool-call", toolName: part.toolName });
1472
1480
  rebuildContainer(currentBlocks, "streaming");
1473
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
+
1474
1487
  // Execute before-hooks for this tool
1475
1488
  const beforeEvent = TOOL_BEFORE_HOOK[part.toolName];
1476
1489
  if (beforeEvent) {
@@ -1494,13 +1507,20 @@ export class App {
1494
1507
  }
1495
1508
  } else if (part.type === "tool-result") {
1496
1509
  // Replace the spinning tool-call with a completed tool-result (same line, not stacked)
1510
+ // Include toolResult so renderer can detect errors and show brief explanations
1511
+ const hasError = part.result && typeof part.result === "object" && "error" in (part.result as any);
1512
+ const resultBlock: import("./chat/types.ts").ContentBlock = {
1513
+ type: "tool-result",
1514
+ toolName: part.toolName,
1515
+ ...(hasError ? { toolResult: part.result } : {}),
1516
+ };
1497
1517
  const callIdx = currentBlocks.findIndex(
1498
1518
  (b) => b.type === "tool-call" && b.toolName === part.toolName,
1499
1519
  );
1500
1520
  if (callIdx !== -1) {
1501
- currentBlocks[callIdx] = { type: "tool-result", toolName: part.toolName };
1521
+ currentBlocks[callIdx] = resultBlock;
1502
1522
  } else {
1503
- currentBlocks.push({ type: "tool-result", toolName: part.toolName });
1523
+ currentBlocks.push(resultBlock);
1504
1524
  }
1505
1525
 
1506
1526
  if (WIDGET_TOOLS.has(part.toolName)) {
@@ -1631,6 +1651,13 @@ export class App {
1631
1651
  // Server already recorded usage — just update the UI
1632
1652
  this.updateBudgetMeter();
1633
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
+ }
1634
1661
  // Server tells us the tier, model, budget state
1635
1662
  this.modeBar.setBudgetUsage(
1636
1663
  part.budgetTotal > 0 ? part.budgetUsed / part.budgetTotal : 0,
@@ -1660,9 +1687,13 @@ export class App {
1660
1687
  }
1661
1688
  }
1662
1689
 
1663
- const finalBlocks = currentBlocks.map((b) =>
1664
- b.type === "markdown" ? { ...b, text: fullText || "*(no response)*" } : b
1665
- );
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
+ }
1666
1697
  store.updateMessageIn(sessionId, msgId, { content: finalBlocks, status: "complete" });
1667
1698
  rebuildContainer(finalBlocks, "complete");
1668
1699
 
@@ -1774,7 +1805,7 @@ export class App {
1774
1805
  const { saveActiveSession, stopAutoSave } = await import("./platform/session-sync.ts");
1775
1806
  stopAutoSave();
1776
1807
  await saveActiveSession();
1777
- } catch {}
1808
+ } catch (e: any) { console.error("[app] shutdown save failed:", e?.message); }
1778
1809
  if (dashboard.running) dashboard.stop();
1779
1810
  cleanupStrategyProcesses();
1780
1811
  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
  }
@@ -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.*",
@@ -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 ──