Skip to content

Commit 92480c1

Browse files
Apply PR #26262: feat(desktop): Add Export Logs
2 parents 04897c5 + f952316 commit 92480c1

18 files changed

Lines changed: 508 additions & 20 deletions

File tree

bun.lock

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

packages/app/src/app.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@ declare global {
7979
}
8080
api?: {
8181
setTitlebar?: (theme: { mode: "light" | "dark" }) => Promise<void>
82+
exportDebugLogs?: () => Promise<string>
8283
}
8384
}
8485
}

packages/app/src/context/platform.tsx

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,16 @@ type OpenDirectoryPickerOptions = { title?: string; multiple?: boolean }
99
type OpenFilePickerOptions = { title?: string; multiple?: boolean; accept?: string[]; extensions?: string[] }
1010
type SaveFilePickerOptions = { title?: string; defaultPath?: string }
1111
type UpdateInfo = { updateAvailable: boolean; version?: string }
12+
type PlatformName = "web" | "desktop"
13+
type DesktopOS = "macos" | "windows" | "linux"
14+
15+
export type FatalRendererErrorLog = {
16+
error: string
17+
url: string
18+
version?: string
19+
platform: PlatformName
20+
os?: DesktopOS
21+
}
1222

1323
export type WslRuntimeCheck = {
1424
available: boolean
@@ -94,10 +104,10 @@ export type WslServersPlatform = {
94104

95105
export type Platform = {
96106
/** Platform discriminator */
97-
platform: "web" | "desktop"
107+
platform: PlatformName
98108

99109
/** Desktop OS (Tauri only) */
100-
os?: "macos" | "windows" | "linux"
110+
os?: DesktopOS
101111

102112
/** App version */
103113
version?: string
@@ -170,6 +180,12 @@ export type Platform = {
170180

171181
/** Read image from clipboard (desktop only) */
172182
readClipboardImage?(): Promise<File | null>
183+
184+
/** Export collected diagnostic logs (desktop only) */
185+
exportDebugLogs?(): Promise<string>
186+
187+
/** Record a fatal renderer error in platform logs (desktop only) */
188+
recordFatalRendererError?(error: FatalRendererErrorLog): Promise<void>
173189
}
174190

175191
export type DisplayBackend = "auto" | "wayland"

packages/app/src/desktop-menu.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@ export const DESKTOP_MENU: DesktopMenu[] = [
8080
{ type: "item", label: "Settings", command: "settings.open", accelerator: { macos: "Cmd+," } },
8181
{ type: "item", label: "Reload Webview", action: "view.reload" },
8282
{ type: "item", label: "Restart", action: "app.relaunch" },
83+
{ type: "item", label: "Export Logs...", command: "logs.export" },
8384
{ type: "separator" },
8485
{ type: "item", role: "hide" },
8586
{ type: "item", role: "hideOthers" },
@@ -201,6 +202,7 @@ export const DESKTOP_MENU: DesktopMenu[] = [
201202
items: [
202203
{ type: "item", label: "OpenCode Documentation", href: "https://opencode.ai/docs" },
203204
{ type: "item", label: "Support Forum", href: "https://discord.com/invite/opencode" },
205+
{ type: "item", label: "Export Logs...", command: "logs.export" },
204206
{ type: "separator" },
205207
{
206208
type: "item",

packages/app/src/i18n/en.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -470,6 +470,7 @@ export const dict = {
470470
"error.page.action.restart": "Restart",
471471
"error.page.action.report": "Report Error",
472472
"error.page.action.reported": "Error Reported",
473+
"error.page.action.exportLogs": "Export Logs",
473474
"error.page.action.checking": "Checking...",
474475
"error.page.action.checkUpdates": "Check for updates",
475476
"error.page.action.updateTo": "Update to {{version}}",

packages/app/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ export { loadLocaleDict, normalizeLocale, type Locale } from "./context/language
55
export { useWslServers } from "./context/wsl-servers"
66
export {
77
type DisplayBackend,
8+
type FatalRendererErrorLog,
89
type Platform,
910
PlatformProvider,
1011
type WslInstalledDistro,

packages/app/src/pages/error.tsx

Lines changed: 36 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { TextField } from "@opencode-ai/ui/text-field"
22
import * as Sentry from "@sentry/solid"
33
import { Logo } from "@opencode-ai/ui/logo"
44
import { Button } from "@opencode-ai/ui/button"
5-
import { Component, createSignal, Show } from "solid-js"
5+
import { Component, createSignal, onMount, Show } from "solid-js"
66
import { createStore } from "solid-js/store"
77
import { usePlatform } from "@/context/platform"
88
import { useLanguage } from "@/context/language"
@@ -221,12 +221,30 @@ interface ErrorPageProps {
221221
export const ErrorPage: Component<ErrorPageProps> = (props) => {
222222
const platform = usePlatform()
223223
const language = useLanguage()
224+
const formattedError = () => formatError(props.error, language.t)
225+
let recordedFatalError: Promise<void> | undefined
224226
const [store, setStore] = createStore({
225227
checking: false,
226228
version: undefined as string | undefined,
227229
actionError: undefined as string | undefined,
228230
})
229231

232+
function ensureFatalErrorRecorded() {
233+
recordedFatalError ??=
234+
platform.recordFatalRendererError?.({
235+
error: formattedError(),
236+
url: location.href,
237+
version: platform.version,
238+
platform: platform.platform,
239+
os: platform.os,
240+
}) ?? Promise.resolve()
241+
return recordedFatalError
242+
}
243+
244+
onMount(() => {
245+
void ensureFatalErrorRecorded().catch(() => undefined)
246+
})
247+
230248
async function checkForUpdates() {
231249
if (!platform.checkUpdate) return
232250
setStore("checking", true)
@@ -254,6 +272,17 @@ export const ErrorPage: Component<ErrorPageProps> = (props) => {
254272
})
255273
}
256274

275+
async function exportDebugLogs() {
276+
const exportLogs = platform.exportDebugLogs
277+
if (!exportLogs) return
278+
await ensureFatalErrorRecorded()
279+
.then(() => exportLogs())
280+
.then(() => setStore("actionError", undefined))
281+
.catch((err) => {
282+
setStore("actionError", formatError(err, language.t))
283+
})
284+
}
285+
257286
return (
258287
<div class="relative flex-1 h-screen w-screen min-h-0 flex flex-col items-center justify-center bg-background-base font-sans">
259288
<div class="w-2/3 max-w-3xl flex flex-col items-center justify-center gap-8">
@@ -263,7 +292,7 @@ export const ErrorPage: Component<ErrorPageProps> = (props) => {
263292
<p class="text-sm text-text-weak">{language.t("error.page.description")}</p>
264293
</div>
265294
<TextField
266-
value={formatError(props.error, language.t)}
295+
value={formattedError()}
267296
readOnly
268297
copyable
269298
multiline
@@ -275,6 +304,11 @@ export const ErrorPage: Component<ErrorPageProps> = (props) => {
275304
<Button size="large" onClick={platform.restart}>
276305
{language.t("error.page.action.restart")}
277306
</Button>
307+
<Show when={platform.platform === "desktop" && platform.exportDebugLogs}>
308+
<Button size="large" variant="ghost" onClick={exportDebugLogs}>
309+
{language.t("error.page.action.exportLogs")}
310+
</Button>
311+
</Show>
278312
<Show when={Sentry.isEnabled}>
279313
{(_) => {
280314
const [reported, setReported] = createSignal(false)

packages/app/src/pages/layout.tsx

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1082,6 +1082,18 @@ export default function Layout(props: ParentProps) {
10821082
keybind: "mod+comma",
10831083
onSelect: () => openSettings(),
10841084
},
1085+
...(platform.platform === "desktop" && platform.exportDebugLogs
1086+
? [
1087+
{
1088+
id: "logs.export",
1089+
title: "Export logs",
1090+
category: language.t("command.category.settings"),
1091+
onSelect: () => {
1092+
void platform.exportDebugLogs?.()
1093+
},
1094+
},
1095+
]
1096+
: []),
10851097
{
10861098
id: "session.previous",
10871099
title: language.t("command.session.previous"),

packages/desktop/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
},
2525
"main": "./out/main/index.js",
2626
"dependencies": {
27+
"@zip.js/zip.js": "2.7.62",
2728
"effect": "catalog:",
2829
"electron-context-menu": "4.1.2",
2930
"electron-log": "^5",

packages/desktop/src/main/index.ts

Lines changed: 33 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import type { InitStep, ServerReadyData, SqliteMigrationProgress } from "../prel
1616
import { checkAppExists, resolveAppPath, wslPath } from "./apps"
1717
import { CHANNEL, UPDATER_ENABLED } from "./constants"
1818
import { registerIpcHandlers, sendDeepLinks, sendMenuCommand, sendSqliteMigrationProgress } from "./ipc"
19-
import { initLogging } from "./logging"
19+
import { exportDebugLogs, initCrashReporter, initLogging, startNetLog, write as writeLog } from "./logging"
2020
import { parseMarkdown } from "./markdown"
2121
import { createMenu } from "./menu"
2222
import {
@@ -32,6 +32,7 @@ import {
3232
createLoadingWindow,
3333
createMainWindow,
3434
registerRendererProtocol,
35+
setRelaunchHandler,
3536
setBackgroundColor,
3637
setDockIcon,
3738
} from "./windows"
@@ -49,6 +50,7 @@ const APP_IDS: Record<string, string> = {
4950
prod: "ai.opencode.desktop",
5051
}
5152
const TEST_ONBOARDING = process.env.OPENCODE_TEST_ONBOARDING === "1"
53+
const jsCallStackFeature = "DocumentPolicyIncludeJSCallStacksInCrashReports"
5254

5355
let logger: ReturnType<typeof initLogging>
5456
let mainWindow: BrowserWindow | null = null
@@ -141,6 +143,7 @@ const main = Effect.gen(function* () {
141143
)
142144
if (onboardingTestRoot) app.setPath("sessionData", join(onboardingTestRoot, "session"))
143145
logger = initLogging()
146+
initCrashReporter()
144147

145148
const wslServers = createWslServersController(
146149
app.getVersion(),
@@ -181,6 +184,8 @@ const main = Effect.gen(function* () {
181184
ensureLoopbackNoProxy()
182185
useEnvProxy()
183186
app.commandLine.appendSwitch("proxy-bypass-list", "<-loopback>")
187+
const features = app.commandLine.getSwitchValue("enable-features")
188+
app.commandLine.appendSwitch("enable-features", features ? `${jsCallStackFeature},${features}` : jsCallStackFeature)
184189
if (!app.isPackaged) app.commandLine.appendSwitch("remote-debugging-port", "9222")
185190

186191
if (!app.requestSingleInstanceLock()) {
@@ -216,6 +221,21 @@ const main = Effect.gen(function* () {
216221
void stopSidecars()
217222
})
218223

224+
app.on("child-process-gone", (_event, details) => {
225+
writeLog("utility", "child process gone", { details }, "error")
226+
})
227+
228+
app.on("render-process-gone", (_event, webContents, details) => {
229+
writeLog("window", "app render process gone", { url: webContents.getURL(), details }, "error")
230+
})
231+
232+
setRelaunchHandler(() => {
233+
void killSidecar().finally(() => {
234+
app.relaunch()
235+
app.exit(0)
236+
})
237+
})
238+
219239
for (const signal of ["SIGINT", "SIGTERM"] as const) {
220240
process.on(signal, () => {
221241
void stopSidecars().finally(() => app.exit(0))
@@ -272,6 +292,8 @@ const main = Effect.gen(function* () {
272292
checkUpdate: async () => checkUpdate(),
273293
installUpdate: async () => installUpdate(stopSidecars),
274294
setBackgroundColor: (color) => setBackgroundColor(color),
295+
exportDebugLogs: () => exportDebugLogs(),
296+
recordFatalRendererError: (error) => writeLog("renderer", "fatal renderer error", { ...error }, "error"),
275297
})
276298

277299
yield* Effect.promise(() => app.whenReady())
@@ -281,6 +303,13 @@ const main = Effect.gen(function* () {
281303
registerRendererProtocol()
282304
setDockIcon()
283305
setupAutoUpdater()
306+
yield* Effect.promise(() => startNetLog()).pipe(
307+
Effect.catch((error) =>
308+
Effect.sync(() => {
309+
logger.warn("failed to start net log", error)
310+
}),
311+
),
312+
)
284313

285314
const needsMigration = ((): boolean => {
286315
if (process.env.OPENCODE_DB === ":memory:") return false
@@ -336,9 +365,9 @@ const main = Effect.gen(function* () {
336365
needsMigration,
337366
userDataPath: app.getPath("userData"),
338367
onSqliteProgress: (progress) => initEmitter.emit("sqlite", progress),
339-
onStdout: (message) => logger.log("sidecar stdout", { message }),
340-
onStderr: (message) => logger.warn("sidecar stderr", { message }),
341-
onExit: (code) => logger.warn("sidecar exited", { code }),
368+
onStdout: (message) => writeLog("server", "stdout", { message }),
369+
onStderr: (message) => writeLog("server", "stderr", { message }, "warn"),
370+
onExit: (code) => writeLog("utility", "sidecar exited", { code }, "warn"),
342371
}),
343372
)
344373
server = listener

0 commit comments

Comments
 (0)