Skip to content

Commit 9319e94

Browse files
author
evgyur
committed
feat: improve error handling and model limit detection
- Add automatic model limit learning from SDK errors - Improve error diagnostics with detailed SDK error payload - Add resilient chat selection (reset stale chat IDs) - Filter non-existent sub-chats from open tabs - Enhanced error parsing with JSON support and multiple patterns - Add raw payload logging for unknown error formats - Clamp maxThinkingTokens to model-specific limits automatically - Disable extended thinking for Opus 4.5 (64k limit)
1 parent 64c7d15 commit 9319e94

15 files changed

Lines changed: 12793 additions & 521 deletions

File tree

package-lock.json

Lines changed: 12201 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

scripts/package-windows.mjs

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@
88
import { spawn } from 'child_process'
99
import { fileURLToPath } from 'url'
1010
import { dirname, join } from 'path'
11-
import { existsSync } from 'fs'
11+
import { existsSync, mkdirSync, copyFileSync } from 'fs'
12+
import { execSync } from 'child_process'
1213

1314
const __filename = fileURLToPath(import.meta.url)
1415
const __dirname = dirname(__filename)
@@ -111,6 +112,35 @@ electronBuilder.on('close', (code) => {
111112
console.log(' Please close 1Code.exe and try again')
112113
process.exit(1)
113114
} else if (existsSync(exePath)) {
115+
// Fix: Ensure better-sqlite3 build directory is complete
116+
const betterSqlite3BuildSource = join(projectRoot, 'node_modules', 'better-sqlite3', 'build')
117+
const betterSqlite3BuildDest = join(projectRoot, 'release', 'win-unpacked', 'resources', 'app.asar.unpacked', 'node_modules', 'better-sqlite3', 'build')
118+
const betterSqlite3NodeFile = join(betterSqlite3BuildDest, 'Release', 'better_sqlite3.node')
119+
120+
if (existsSync(betterSqlite3BuildSource) && !existsSync(betterSqlite3NodeFile)) {
121+
try {
122+
// Use robocopy to copy entire build directory structure
123+
const robocopyCmd = `robocopy "${betterSqlite3BuildSource}" "${betterSqlite3BuildDest}" /E /NFL /NDL /NJH /NJS`
124+
execSync(robocopyCmd, { cwd: projectRoot, stdio: 'ignore' })
125+
if (existsSync(betterSqlite3NodeFile)) {
126+
console.log('\n✓ Fixed: Copied better-sqlite3 build directory')
127+
}
128+
} catch (err) {
129+
// Fallback: try manual copy
130+
try {
131+
const betterSqlite3Source = join(betterSqlite3BuildSource, 'Release', 'better_sqlite3.node')
132+
const betterSqlite3DestDir = join(betterSqlite3BuildDest, 'Release')
133+
if (existsSync(betterSqlite3Source)) {
134+
mkdirSync(betterSqlite3DestDir, { recursive: true })
135+
copyFileSync(betterSqlite3Source, betterSqlite3NodeFile)
136+
console.log('\n✓ Fixed: Copied better-sqlite3 native bindings')
137+
}
138+
} catch (err2) {
139+
console.log('\n⚠️ Warning: Could not copy better-sqlite3 native bindings:', err2.message)
140+
}
141+
}
142+
}
143+
114144
console.log('\n✅ Build completed successfully!')
115145
console.log(` Executable: ${exePath}`)
116146
process.exit(0)

src/env.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ declare global {
1111
// Renderer process (VITE_ prefix)
1212
readonly VITE_POSTHOG_KEY?: string
1313
readonly VITE_POSTHOG_HOST?: string
14+
readonly VITE_SENTRY_DSN?: string
1415
}
1516
}
1617

src/main/lib/auto-updater.ts

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -117,8 +117,22 @@ export async function initAutoUpdater(getWindow: () => BrowserWindow | null) {
117117

118118
// Event: Error
119119
autoUpdater.on("error", (error: Error) => {
120-
log.error("[AutoUpdater] Error:", error.message)
121-
sendToRenderer("update:error", error.message)
120+
// Check if this is a 404 error (update manifest not found)
121+
// This is normal for local builds or when updates aren't published yet
122+
const is404Error = error.message.includes("404") ||
123+
error.message.includes("latest.yml") ||
124+
error.message.includes("Cannot find channel")
125+
126+
if (is404Error) {
127+
log.info("[AutoUpdater] Update manifest not found (404) - this is normal for local builds")
128+
// Treat 404 as "no update available" rather than an error
129+
sendToRenderer("update:not-available", {
130+
version: app.getVersion(),
131+
})
132+
} else {
133+
log.error("[AutoUpdater] Error:", error.message)
134+
sendToRenderer("update:error", error.message)
135+
}
122136
})
123137

124138
// Register IPC handlers

src/main/lib/trpc/routers/claude.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -859,6 +859,13 @@ export const claudeRouter = router({
859859
if (msgAny.type === "error" || msgAny.error) {
860860
const sdkError =
861861
msgAny.error || msgAny.message || "Unknown SDK error"
862+
const sdkErrorPayload = {
863+
type: msgAny.type,
864+
error: msgAny.error,
865+
message: msgAny.message,
866+
code: msgAny.code,
867+
status: msgAny.status,
868+
}
862869
lastError = new Error(sdkError)
863870

864871
// Categorize SDK-level errors
@@ -901,10 +908,11 @@ export const claudeRouter = router({
901908
} else {
902909
safeEmit({
903910
type: "error",
904-
errorText: errorContext,
911+
errorText: `${errorContext}: ${sdkError}`,
905912
debugInfo: {
906913
category: errorCategory,
907914
sdkError: sdkError,
915+
sdkErrorPayload,
908916
sessionId: msgAny.session_id,
909917
messageId: msgAny.message?.id,
910918
},

src/main/windows/main.ts

Lines changed: 51 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {
77
clipboard,
88
session,
99
Menu,
10+
nativeImage,
1011
} from "electron"
1112
import { join } from "path"
1213
import { readFileSync, existsSync } from "fs"
@@ -63,15 +64,64 @@ function registerIpcHandlers(getWindow: () => BrowserWindow | null): void {
6364
})
6465
// Note: Update checking is now handled by auto-updater module (lib/auto-updater.ts)
6566
ipcMain.handle("app:set-badge", (_event, count: number | null) => {
67+
const win = getWindow()
6668
if (process.platform === "darwin") {
69+
// macOS: Use dock badge
6770
app.dock.setBadge(count ? String(count) : "")
71+
} else if (process.platform === "win32" && win) {
72+
// Windows: Use overlay icon with number badge
73+
if (count !== null && count > 0) {
74+
// Request badge icon from renderer (it will generate and send via IPC)
75+
// For now, show count in window title as fallback
76+
win.setTitle(`1Code (${count})`)
77+
// The actual overlay icon will be set when renderer sends the image data
78+
} else {
79+
win.setTitle("1Code")
80+
// Clear overlay icon
81+
win.setOverlayIcon(null, "")
82+
}
83+
}
84+
})
85+
86+
// Handle badge icon data from renderer (Windows overlay icon)
87+
ipcMain.handle("app:set-badge-icon", (_event, imageData: string | null) => {
88+
const win = getWindow()
89+
if (process.platform === "win32" && win) {
90+
if (imageData) {
91+
// Convert data URL to native image
92+
const image = nativeImage.createFromDataURL(imageData)
93+
win.setOverlayIcon(image, `${imageData ? "New messages" : ""}`)
94+
} else {
95+
win.setOverlayIcon(null, "")
96+
}
6897
}
6998
})
7099
ipcMain.handle(
71100
"app:show-notification",
72101
(_event, options: { title: string; body: string }) => {
73102
const { Notification } = require("electron")
74-
new Notification(options).show()
103+
// Electron Notification uses native Windows Notification API
104+
// This will show a standard Windows desktop notification
105+
const notification = new Notification({
106+
title: options.title,
107+
body: options.body,
108+
// Windows-specific options
109+
...(process.platform === "win32" && {
110+
// Use Windows 10+ action center
111+
silent: false, // Play notification sound
112+
}),
113+
})
114+
notification.show()
115+
116+
// Optional: Handle notification click
117+
notification.on("click", () => {
118+
// Focus the main window when notification is clicked
119+
const win = getWindow()
120+
if (win) {
121+
if (win.isMinimized()) win.restore()
122+
win.focus()
123+
}
124+
})
75125
},
76126
)
77127

src/preload/index.ts

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,9 @@
11
import { contextBridge, ipcRenderer } from "electron"
22
import { exposeElectronTRPC } from "trpc-electron/main"
33

4-
// Only initialize Sentry in production to avoid IPC errors in dev mode
5-
if (process.env.NODE_ENV === "production") {
6-
import("@sentry/electron/renderer").then((Sentry) => {
7-
Sentry.init()
8-
})
9-
}
4+
// Note: Sentry should NOT be initialized in preload script
5+
// Preload runs in isolated context and Sentry IPC won't work here
6+
// Sentry is initialized in main process and renderer process only
107

118
// Expose tRPC IPC bridge for type-safe communication
129
exposeElectronTRPC()
@@ -105,6 +102,7 @@ contextBridge.exposeInMainWorld("desktopApi", {
105102

106103
// Native features
107104
setBadge: (count: number | null) => ipcRenderer.invoke("app:set-badge", count),
105+
setBadgeIcon: (imageData: string | null) => ipcRenderer.invoke("app:set-badge-icon", imageData),
108106
showNotification: (options: { title: string; body: string }) =>
109107
ipcRenderer.invoke("app:show-notification", options),
110108
openExternal: (url: string) => ipcRenderer.invoke("shell:open-external", url),
@@ -208,6 +206,7 @@ export interface DesktopApi {
208206
toggleDevTools: () => Promise<void>
209207
setAnalyticsOptOut: (optedOut: boolean) => Promise<void>
210208
setBadge: (count: number | null) => Promise<void>
209+
setBadgeIcon: (imageData: string | null) => Promise<void>
211210
showNotification: (options: { title: string; body: string }) => Promise<void>
212211
openExternal: (url: string) => Promise<void>
213212
getApiBaseUrl: () => Promise<string>

src/renderer/components/dialogs/settings-tabs/agents-preferences-tab.tsx

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { useState, useEffect } from "react"
33
import {
44
extendedThinkingEnabledAtom,
55
soundNotificationsEnabledAtom,
6+
desktopNotificationsEnabledAtom,
67
analyticsOptOutAtom,
78
ctrlTabTargetAtom,
89
useNativeFrameAtom,
@@ -39,6 +40,7 @@ export function AgentsPreferencesTab() {
3940
extendedThinkingEnabledAtom,
4041
)
4142
const [soundEnabled, setSoundEnabled] = useAtom(soundNotificationsEnabledAtom)
43+
const [desktopNotificationsEnabled, setDesktopNotificationsEnabled] = useAtom(desktopNotificationsEnabledAtom)
4244
const [analyticsOptOut, setAnalyticsOptOut] = useAtom(analyticsOptOutAtom)
4345
const [ctrlTabTarget, setCtrlTabTarget] = useAtom(ctrlTabTargetAtom)
4446
const [useNativeFrame, setUseNativeFrame] = useAtom(useNativeFrameAtom)
@@ -121,6 +123,24 @@ export function AgentsPreferencesTab() {
121123
</div>
122124
<Switch checked={soundEnabled} onCheckedChange={setSoundEnabled} />
123125
</div>
126+
127+
{/* Desktop Notifications Toggle (Windows) */}
128+
{isWindows && (
129+
<div className="flex items-start justify-between">
130+
<div className="flex flex-col space-y-1">
131+
<span className="text-sm font-medium text-foreground">
132+
Desktop Notifications
133+
</span>
134+
<span className="text-xs text-muted-foreground">
135+
Show Windows desktop notification when agent completes work
136+
</span>
137+
</div>
138+
<Switch
139+
checked={desktopNotificationsEnabled}
140+
onCheckedChange={setDesktopNotificationsEnabled}
141+
/>
142+
</div>
143+
)}
124144
</div>
125145
</div>
126146

0 commit comments

Comments
 (0)