chore: merge dev into v2
This commit is contained in:
commit
655dad8bf3
209 changed files with 6882 additions and 2469 deletions
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "@opencode-ai/desktop",
|
||||
"private": true,
|
||||
"version": "1.18.3",
|
||||
"version": "1.18.4",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"homepage": "https://opencode.ai",
|
||||
|
|
|
|||
95
packages/desktop/src/main/debug.ts
Normal file
95
packages/desktop/src/main/debug.ts
Normal file
|
|
@ -0,0 +1,95 @@
|
|||
import type { WebContents } from "electron"
|
||||
|
||||
const focusDebuggerOwners = new WeakSet<WebContents>()
|
||||
const forcedFocusNodes = new WeakMap<WebContents, number[]>()
|
||||
const focusableSelector = `
|
||||
a[href],
|
||||
button:not([disabled]),
|
||||
input:not([disabled]),
|
||||
select:not([disabled]),
|
||||
textarea:not([disabled]),
|
||||
summary,
|
||||
[contenteditable="true"],
|
||||
[tabindex]:not([tabindex="-1"])
|
||||
`
|
||||
|
||||
export async function setForceFocus(contents: WebContents, enabled: boolean) {
|
||||
const debuggerApi = contents.debugger
|
||||
if (!debuggerApi.isAttached()) {
|
||||
if (!enabled) {
|
||||
focusDebuggerOwners.delete(contents)
|
||||
forcedFocusNodes.delete(contents)
|
||||
return
|
||||
}
|
||||
debuggerApi.attach("1.3")
|
||||
focusDebuggerOwners.add(contents)
|
||||
debuggerApi.once("detach", () => {
|
||||
focusDebuggerOwners.delete(contents)
|
||||
forcedFocusNodes.delete(contents)
|
||||
})
|
||||
}
|
||||
|
||||
if (!enabled) {
|
||||
await Promise.allSettled(
|
||||
(forcedFocusNodes.get(contents) ?? []).map((nodeId) =>
|
||||
debuggerApi.sendCommand("CSS.forcePseudoState", {
|
||||
nodeId,
|
||||
forcedPseudoClasses: [],
|
||||
}),
|
||||
),
|
||||
)
|
||||
forcedFocusNodes.delete(contents)
|
||||
if (!focusDebuggerOwners.delete(contents)) return
|
||||
debuggerApi.detach()
|
||||
return
|
||||
}
|
||||
|
||||
await debuggerApi.sendCommand("DOM.enable")
|
||||
await debuggerApi.sendCommand("CSS.enable")
|
||||
const document: unknown = await debuggerApi.sendCommand("DOM.getDocument", {
|
||||
depth: -1,
|
||||
pierce: true,
|
||||
})
|
||||
const nodes: unknown = await debuggerApi.sendCommand("DOM.querySelectorAll", {
|
||||
nodeId: readDocumentNodeId(document),
|
||||
selector: focusableSelector,
|
||||
})
|
||||
const nodeIds = readNodeIds(nodes)
|
||||
forcedFocusNodes.set(contents, [...new Set([...(forcedFocusNodes.get(contents) ?? []), ...nodeIds])])
|
||||
await Promise.allSettled(
|
||||
nodeIds.map((nodeId) =>
|
||||
debuggerApi.sendCommand("CSS.forcePseudoState", {
|
||||
nodeId,
|
||||
forcedPseudoClasses: ["focus", "focus-visible"],
|
||||
}),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
function readDocumentNodeId(value: unknown) {
|
||||
if (
|
||||
!value ||
|
||||
typeof value !== "object" ||
|
||||
!("root" in value) ||
|
||||
!value.root ||
|
||||
typeof value.root !== "object" ||
|
||||
!("nodeId" in value.root) ||
|
||||
typeof value.root.nodeId !== "number"
|
||||
) {
|
||||
throw new Error("Invalid DOM.getDocument response")
|
||||
}
|
||||
return value.root.nodeId
|
||||
}
|
||||
|
||||
function readNodeIds(value: unknown) {
|
||||
if (
|
||||
!value ||
|
||||
typeof value !== "object" ||
|
||||
!("nodeIds" in value) ||
|
||||
!Array.isArray(value.nodeIds) ||
|
||||
!value.nodeIds.every((nodeId) => typeof nodeId === "number")
|
||||
) {
|
||||
throw new Error("Invalid DOM.querySelectorAll response")
|
||||
}
|
||||
return value.nodeIds
|
||||
}
|
||||
|
|
@ -33,6 +33,7 @@ import {
|
|||
type SidecarListener,
|
||||
} from "./server"
|
||||
import { setupAutoUpdater, showUpdaterDialog } from "./updater"
|
||||
import { safeWebContentsURL } from "./window-state"
|
||||
import {
|
||||
getLastFocusedWindow,
|
||||
registerRendererProtocol,
|
||||
|
|
@ -233,7 +234,7 @@ const main = Effect.gen(function* () {
|
|||
})
|
||||
|
||||
app.on("render-process-gone", (_event, webContents, details) => {
|
||||
writeLog("window", "app render process gone", { url: webContents.getURL(), details }, "error")
|
||||
writeLog("window", "app render process gone", { url: safeWebContentsURL(webContents), details }, "error")
|
||||
})
|
||||
|
||||
setRelaunchHandler(() => {
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import type { DesktopMenuAction } from "@opencode-ai/app/desktop-menu"
|
|||
|
||||
import type { FatalRendererError, ServerReadyData, TitlebarTheme } from "../preload/types"
|
||||
import { runDesktopMenuAction } from "./desktop-menu-actions"
|
||||
import { setForceFocus } from "./debug"
|
||||
import { assertAttachmentBudget, createPickedFileAuthorizations } from "./attachment-picker"
|
||||
import { getStore, removeStoreFileIfEmpty } from "./store"
|
||||
import { getPinchZoomEnabled, getWindowID, setPinchZoomEnabled, setTitlebar, updateTitlebar } from "./windows"
|
||||
|
|
@ -81,6 +82,9 @@ export function registerIpcHandlers(deps: Deps) {
|
|||
ipcMain.handle("updater-install", () => deps.updater.install())
|
||||
ipcMain.handle("set-background-color", (_event: IpcMainInvokeEvent, color: string) => deps.setBackgroundColor(color))
|
||||
ipcMain.handle("export-debug-logs", () => deps.exportDebugLogs())
|
||||
ipcMain.handle("set-force-focus", (event: IpcMainInvokeEvent, enabled: boolean) =>
|
||||
setForceFocus(event.sender, enabled),
|
||||
)
|
||||
ipcMain.handle("record-fatal-renderer-error", (_event: IpcMainInvokeEvent, error: FatalRendererError) =>
|
||||
deps.recordFatalRendererError(error),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import type { BrowserWindow } from "electron"
|
||||
import { write as writeLog } from "./logging"
|
||||
import { safeWindowURL } from "./window-state"
|
||||
|
||||
const sampleInterval = 1000
|
||||
const samplePeriod = 15000
|
||||
|
|
@ -46,7 +47,7 @@ export function createUnresponsiveSampler(win: BrowserWindow, name: string) {
|
|||
const message = [
|
||||
"renderer unresponsive samples",
|
||||
`Window: ${name}`,
|
||||
`URL: ${win.isDestroyed() ? "<destroyed>" : win.webContents.getURL()}`,
|
||||
`URL: ${safeWindowURL(win)}`,
|
||||
...entries.map((entry) => `<${entry[1]}> ${entry[0]}`),
|
||||
`Total Samples: ${total}`,
|
||||
].join("\n")
|
||||
|
|
|
|||
29
packages/desktop/src/main/window-state.ts
Normal file
29
packages/desktop/src/main/window-state.ts
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
export const destroyedWindowURL = "<destroyed>"
|
||||
|
||||
type WebContentsURLState = {
|
||||
isDestroyed(): boolean
|
||||
getURL(): string
|
||||
}
|
||||
|
||||
type WindowURLState = {
|
||||
isDestroyed(): boolean
|
||||
readonly webContents: WebContentsURLState
|
||||
}
|
||||
|
||||
export function safeWebContentsURL(webContents: WebContentsURLState) {
|
||||
try {
|
||||
if (webContents.isDestroyed()) return destroyedWindowURL
|
||||
return webContents.getURL()
|
||||
} catch {
|
||||
return destroyedWindowURL
|
||||
}
|
||||
}
|
||||
|
||||
export function safeWindowURL(win: WindowURLState) {
|
||||
try {
|
||||
if (win.isDestroyed()) return destroyedWindowURL
|
||||
return safeWebContentsURL(win.webContents)
|
||||
} catch {
|
||||
return destroyedWindowURL
|
||||
}
|
||||
}
|
||||
|
|
@ -13,6 +13,7 @@ import { getStore, removeStoreFile } from "./store"
|
|||
import { PINCH_ZOOM_ENABLED_KEY, WINDOW_IDS_KEY } from "./store-keys"
|
||||
import { createUnresponsiveSampler } from "./unresponsive"
|
||||
import { createWindowRegistry } from "./window-registry"
|
||||
import { safeWindowURL } from "./window-state"
|
||||
|
||||
const root = dirname(fileURLToPath(import.meta.url))
|
||||
const rendererRoot = join(root, "../renderer")
|
||||
|
|
@ -364,7 +365,7 @@ function wireWindowRecovery(win: BrowserWindow, name: string) {
|
|||
errorCode,
|
||||
errorDescription,
|
||||
validatedURL,
|
||||
currentURL: win.webContents.getURL(),
|
||||
currentURL: safeWindowURL(win),
|
||||
isMainFrame,
|
||||
},
|
||||
"error",
|
||||
|
|
@ -386,12 +387,7 @@ function wireWindowRecovery(win: BrowserWindow, name: string) {
|
|||
})
|
||||
win.webContents.on("render-process-gone", (_event, details) => {
|
||||
sampler.stopAndFlush()
|
||||
writeLog(
|
||||
"window",
|
||||
"renderer process gone",
|
||||
{ window: name, currentURL: win.webContents.getURL(), details },
|
||||
"error",
|
||||
)
|
||||
writeLog("window", "renderer process gone", { window: name, currentURL: safeWindowURL(win), details }, "error")
|
||||
void show(
|
||||
"OpenCode window terminated unexpectedly",
|
||||
[`Window: ${name}`, `Reason: ${details.reason}`, `Code: ${details.exitCode ?? "<unknown>"}`].join("\n"),
|
||||
|
|
@ -399,12 +395,12 @@ function wireWindowRecovery(win: BrowserWindow, name: string) {
|
|||
)
|
||||
})
|
||||
win.on("unresponsive", () => {
|
||||
writeLog("window", "renderer unresponsive", { window: name, currentURL: win.webContents.getURL() }, "error")
|
||||
writeLog("window", "renderer unresponsive", { window: name, currentURL: safeWindowURL(win) }, "error")
|
||||
sampler.start()
|
||||
void show("OpenCode is not responding", "You can relaunch the app, open the logs, or keep waiting.", true)
|
||||
})
|
||||
win.on("responsive", () => {
|
||||
writeLog("window", "renderer responsive", { window: name, currentURL: win.webContents.getURL() }, "error")
|
||||
writeLog("window", "renderer responsive", { window: name, currentURL: safeWindowURL(win) }, "error")
|
||||
sampler.stopAndFlush()
|
||||
})
|
||||
win.webContents.on("console-message", (_event, level, message, line, sourceId) => {
|
||||
|
|
|
|||
|
|
@ -121,6 +121,7 @@ const api: ElectronAPI = {
|
|||
runDesktopMenuAction: (action) => ipcRenderer.invoke("run-desktop-menu-action", action),
|
||||
setBackgroundColor: (color: string) => ipcRenderer.invoke("set-background-color", color),
|
||||
exportDebugLogs: () => ipcRenderer.invoke("export-debug-logs"),
|
||||
setForceFocus: (enabled) => ipcRenderer.invoke("set-force-focus", enabled),
|
||||
recordFatalRendererError: (error) => ipcRenderer.invoke("record-fatal-renderer-error", error),
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -104,5 +104,6 @@ export type ElectronAPI = {
|
|||
runDesktopMenuAction: (action: DesktopMenuAction) => Promise<void>
|
||||
setBackgroundColor: (color: string) => Promise<void>
|
||||
exportDebugLogs: () => Promise<string>
|
||||
setForceFocus: (enabled: boolean) => Promise<void>
|
||||
recordFatalRendererError: (error: FatalRendererError) => Promise<void>
|
||||
}
|
||||
|
|
|
|||
|
|
@ -240,6 +240,8 @@ const createPlatform = (windowState: DesktopWindowState): Platform => {
|
|||
|
||||
exportDebugLogs: () => window.api.exportDebugLogs(),
|
||||
|
||||
setForceFocus: (enabled) => window.api.setForceFocus(enabled),
|
||||
|
||||
recordFatalRendererError: (error) => window.api.recordFatalRendererError(error),
|
||||
|
||||
restart: async () => {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue