From 971abe24d796724580202ffd004bfcc66256e42b Mon Sep 17 00:00:00 2001 From: Shantur Rathore Date: Fri, 5 Dec 2025 15:07:49 +0000 Subject: [PATCH] feat(ui): add runtime logger and replace console usage --- package-lock.json | 1 + packages/ui/README.md | 24 +++- packages/ui/package.json | 1 + packages/ui/src/App.tsx | 18 ++- packages/ui/src/components/agent-selector.tsx | 6 +- packages/ui/src/components/diff-viewer.tsx | 5 +- .../components/filesystem-browser-dialog.tsx | 5 +- packages/ui/src/components/instance-info.tsx | 5 +- .../src/components/instance-welcome-view.tsx | 5 +- .../components/instance/instance-shell.tsx | 7 +- packages/ui/src/components/markdown.tsx | 9 +- packages/ui/src/components/model-selector.tsx | 5 +- .../components/opencode-binary-selector.tsx | 5 +- packages/ui/src/components/prompt-input.tsx | 7 +- .../src/components/remote-access-overlay.tsx | 7 +- packages/ui/src/components/session-list.tsx | 5 +- packages/ui/src/components/session-picker.tsx | 5 +- .../src/components/session/session-view.tsx | 13 +- packages/ui/src/components/tool-call.tsx | 5 +- packages/ui/src/components/tool-call/utils.ts | 5 +- packages/ui/src/components/unified-picker.tsx | 7 +- packages/ui/src/lib/api-client.ts | 15 +- packages/ui/src/lib/command-utils.ts | 5 +- .../ui/src/lib/hooks/use-app-lifecycle.ts | 7 +- packages/ui/src/lib/hooks/use-commands.ts | 17 ++- packages/ui/src/lib/logger.ts | 128 ++++++++++++++++++ packages/ui/src/lib/markdown.ts | 5 +- packages/ui/src/lib/native/cli.ts | 5 +- .../ui/src/lib/native/electron/functions.ts | 5 +- packages/ui/src/lib/native/tauri/functions.ts | 5 +- packages/ui/src/lib/runtime-env.ts | 9 +- packages/ui/src/lib/server-events.ts | 7 +- packages/ui/src/lib/sse-manager.ts | 9 +- packages/ui/src/lib/storage.ts | 5 +- packages/ui/src/renderer/index.html | 3 +- packages/ui/src/renderer/loading.html | 3 +- packages/ui/src/stores/instance-config.tsx | 7 +- packages/ui/src/stores/instances.ts | 33 +++-- packages/ui/src/stores/message-v2/bus.ts | 5 +- packages/ui/src/stores/preferences.tsx | 26 ++-- packages/ui/src/stores/session-actions.ts | 26 ++-- packages/ui/src/stores/session-api.ts | 29 ++-- packages/ui/src/stores/session-events.ts | 31 ++--- packages/ui/src/stores/session-state.ts | 9 +- 44 files changed, 406 insertions(+), 138 deletions(-) create mode 100644 packages/ui/src/lib/logger.ts diff --git a/package-lock.json b/package-lock.json index df1dfbed..b6207a92 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8895,6 +8895,7 @@ "@kobalte/core": "0.13.11", "@opencode-ai/sdk": "^1.0.133", "@solidjs/router": "^0.13.0", + "debug": "^4.4.3", "github-markdown-css": "^5.8.1", "lucide-solid": "^0.300.0", "marked": "^12.0.0", diff --git a/packages/ui/README.md b/packages/ui/README.md index 99e2b2f5..2991780b 100644 --- a/packages/ui/README.md +++ b/packages/ui/README.md @@ -26,8 +26,30 @@ This starts the Vite dev server at `http://localhost:3000`. To build the production assets: -```bash +``` npm run build ``` The output will be generated in the `dist` directory, which is then consumed by the Server or Electron app. + +## Debug Logging + +The UI now routes all logging through a lightweight wrapper around [`debug`](https://github.com/debug-js/debug). The logger exposes four namespaces that can be toggled at runtime: + +- `sse` – Server-sent event transport and handlers +- `api` – HTTP/API calls and workspace lifecycle +- `session` – Session/model state, prompt handling, tool calls +- `actions` – User-driven interactions in UI components + +You can enable or disable namespaces inside DevTools by importing the helpers: + +```ts +import { listLoggerNamespaces, enableLogger, disableLogger } from "./src/lib/logger" + +listLoggerNamespaces() // => [{ name: "sse", enabled: false }, ...] +enableLogger("sse") // turn on SSE logs +disableLogger("sse") // turn them off again +``` + +Enabled namespaces are persisted in `localStorage` under `opencode:logger:namespaces`, so your preference survives reloads. + diff --git a/packages/ui/package.json b/packages/ui/package.json index fe506545..0cd6962b 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -14,6 +14,7 @@ "@kobalte/core": "0.13.11", "@opencode-ai/sdk": "^1.0.133", "@solidjs/router": "^0.13.0", + "debug": "^4.4.3", "github-markdown-css": "^5.8.1", "lucide-solid": "^0.300.0", "marked": "^12.0.0", diff --git a/packages/ui/src/App.tsx b/packages/ui/src/App.tsx index 4a5d53e9..7e38bb6b 100644 --- a/packages/ui/src/App.tsx +++ b/packages/ui/src/App.tsx @@ -12,6 +12,7 @@ import { initMarkdown } from "./lib/markdown" import { useTheme } from "./lib/theme" import { useCommands } from "./lib/hooks/use-commands" import { useAppLifecycle } from "./lib/hooks/use-app-lifecycle" +import { getLogger } from "./lib/logger" import { hasInstances, isSelectingFolder, @@ -42,6 +43,8 @@ import { updateSessionModel, } from "./stores/sessions" +const log = getLogger("actions") + const App: Component = () => { const { isDark } = useTheme() const { @@ -61,7 +64,7 @@ const App: Component = () => { const [remoteAccessOpen, setRemoteAccessOpen] = createSignal(false) createEffect(() => { - void initMarkdown(isDark()).catch(console.error) + void initMarkdown(isDark()).catch((error) => log.error("Failed to initialize markdown", error)) }) const activeInstance = createMemo(() => getActiveInstance()) @@ -106,13 +109,16 @@ const App: Component = () => { setShowFolderSelection(false) setIsAdvancedSettingsOpen(false) - console.log("Created instance:", instanceId, "Port:", instances().get(instanceId)?.port) + log.info("Created instance", { + instanceId, + port: instances().get(instanceId)?.port, + }) } catch (error) { clearLaunchError() if (isMissingBinaryError(error)) { setLaunchErrorBinary(selectedBinary) } - console.error("Failed to create instance:", error) + log.error("Failed to create instance", error) } finally { setIsSelectingFolder(false) } @@ -137,7 +143,7 @@ const App: Component = () => { try { await acknowledgeDisconnectedInstance() } catch (error) { - console.error("Failed to finalize disconnected instance:", error) + log.error("Failed to finalize disconnected instance", error) } } @@ -165,7 +171,7 @@ const App: Component = () => { const session = await createSession(instanceId) setActiveParentSession(instanceId, session.id) } catch (error) { - console.error("Failed to create session:", error) + log.error("Failed to create session", error) } } @@ -189,7 +195,7 @@ const App: Component = () => { try { await fetchSessions(instanceId) } catch (error) { - console.error("Failed to refresh sessions after closing:", error) + log.error("Failed to refresh sessions after closing", error) } } diff --git a/packages/ui/src/components/agent-selector.tsx b/packages/ui/src/components/agent-selector.tsx index 261e26f1..80c5ddfa 100644 --- a/packages/ui/src/components/agent-selector.tsx +++ b/packages/ui/src/components/agent-selector.tsx @@ -3,6 +3,9 @@ import { For, Show, createEffect, createMemo } from "solid-js" import { agents, fetchAgents, sessions } from "../stores/sessions" import { ChevronDown } from "lucide-solid" import type { Agent } from "../types/session" +import { getLogger } from "../lib/logger" +const log = getLogger("session") + interface AgentSelectorProps { instanceId: string @@ -49,10 +52,11 @@ export default function AgentSelector(props: AgentSelectorProps) { createEffect(() => { if (instanceAgents().length === 0) { - fetchAgents(props.instanceId).catch(console.error) + fetchAgents(props.instanceId).catch((error) => log.error("Failed to fetch agents", error)) } }) + const handleChange = async (value: Agent | null) => { if (value && value.name !== props.currentAgent) { await props.onAgentChange(value.name) diff --git a/packages/ui/src/components/diff-viewer.tsx b/packages/ui/src/components/diff-viewer.tsx index 47ac4f90..b2fef6a2 100644 --- a/packages/ui/src/components/diff-viewer.tsx +++ b/packages/ui/src/components/diff-viewer.tsx @@ -8,6 +8,9 @@ import { normalizeDiffText } from "../lib/diff-utils" import { setCacheEntry } from "../lib/global-cache" import type { CacheEntryParams } from "../lib/global-cache" import type { DiffViewMode } from "../stores/preferences" +import { getLogger } from "../lib/logger" +const log = getLogger("session") + disableCache() @@ -110,7 +113,7 @@ export function ToolCallDiffViewer(props: ToolCallDiffViewerProps) { > {(data) => ( { - console.warn("Failed to render diff view", error) + log.warn("Failed to render diff view", error) return
{props.diffText}
}}> = (props) function handleNavigateTo(path: string) { void fetchDirectory(path, true).catch((err) => { - console.error("Failed to open directory", err) + log.error("Failed to open directory", err) setError(err instanceof Error ? err.message : "Unable to open directory") }) } diff --git a/packages/ui/src/components/instance-info.tsx b/packages/ui/src/components/instance-info.tsx index d30dda20..e3af7c11 100644 --- a/packages/ui/src/components/instance-info.tsx +++ b/packages/ui/src/components/instance-info.tsx @@ -1,6 +1,9 @@ import { Component, Show, For, createSignal, createEffect, onCleanup } from "solid-js" import type { Instance, RawMcpStatus } from "../types/instance" import { fetchLspStatus, updateInstance } from "../stores/instances" +import { getLogger } from "../lib/logger" + +const log = getLogger("session") interface InstanceInfoProps { instance: Instance @@ -113,7 +116,7 @@ const InstanceInfo: Component = (props) => { } catch (error) { if (!cancelled) { - console.error("Failed to load instance metadata:", error) + log.error("Failed to load instance metadata", error) } } finally { pendingMetadataRequests.delete(instanceId) diff --git a/packages/ui/src/components/instance-welcome-view.tsx b/packages/ui/src/components/instance-welcome-view.tsx index d3398b18..39faa6f1 100644 --- a/packages/ui/src/components/instance-welcome-view.tsx +++ b/packages/ui/src/components/instance-welcome-view.tsx @@ -6,6 +6,9 @@ import KeyboardHint from "./keyboard-hint" import Kbd from "./kbd" import { keyboardRegistry, type KeyboardShortcut } from "../lib/keyboard-registry" import { isMac } from "../lib/keyboard-utils" +import { getLogger } from "../lib/logger" +const log = getLogger("actions") + interface InstanceWelcomeViewProps { @@ -189,7 +192,7 @@ const InstanceWelcomeView: Component = (props) => { const session = await createSession(props.instance.id) setActiveParentSession(props.instance.id, session.id) } catch (error) { - console.error("Failed to create session:", error) + log.error("Failed to create session:", error) } finally { setIsCreating(false) } diff --git a/packages/ui/src/components/instance/instance-shell.tsx b/packages/ui/src/components/instance/instance-shell.tsx index 5088f15c..c942864b 100644 --- a/packages/ui/src/components/instance/instance-shell.tsx +++ b/packages/ui/src/components/instance/instance-shell.tsx @@ -17,6 +17,9 @@ import CommandPalette from "../command-palette" import Kbd from "../kbd" import ContextUsagePanel from "../session/context-usage-panel" import SessionView from "../session/session-view" +import { getLogger } from "../../lib/logger" +const log = getLogger("session") + interface InstanceShellProps { instance: Instance @@ -119,13 +122,13 @@ const InstanceShell: Component = (props) => { onClose={(id) => { const result = props.onCloseSession(id) if (result instanceof Promise) { - void result.catch((error) => console.error("Failed to close session:", error)) + void result.catch((error) => log.error("Failed to close session:", error)) } }} onNew={() => { const result = props.onNewSession() if (result instanceof Promise) { - void result.catch((error) => console.error("Failed to create session:", error)) + void result.catch((error) => log.error("Failed to create session:", error)) } }} showHeader diff --git a/packages/ui/src/components/markdown.tsx b/packages/ui/src/components/markdown.tsx index 508ecffe..2cc2ccdd 100644 --- a/packages/ui/src/components/markdown.tsx +++ b/packages/ui/src/components/markdown.tsx @@ -1,6 +1,9 @@ import { createEffect, createSignal, onMount, onCleanup } from "solid-js" import { renderMarkdown, onLanguagesLoaded, initMarkdown, decodeHtmlEntities } from "../lib/markdown" import type { TextPart } from "../types/message" +import { getLogger } from "../lib/logger" +const log = getLogger("session") + interface MarkdownProps { part: TextPart @@ -43,7 +46,7 @@ export function Markdown(props: MarkdownProps) { notifyRendered() } } catch (error) { - console.error("Failed to render markdown:", error) + log.error("Failed to render markdown:", error) if (latestRequestedText === text) { setHtml(text) notifyRendered() @@ -68,7 +71,7 @@ export function Markdown(props: MarkdownProps) { notifyRendered() } } catch (error) { - console.error("Failed to render markdown:", error) + log.error("Failed to render markdown:", error) if (latestRequestedText === text) { setHtml(text) part.renderCache = { text, html: text, theme: themeKey } @@ -124,7 +127,7 @@ export function Markdown(props: MarkdownProps) { notifyRendered() } } catch (error) { - console.error("Failed to re-render markdown after language load:", error) + log.error("Failed to re-render markdown after language load:", error) } }) diff --git a/packages/ui/src/components/model-selector.tsx b/packages/ui/src/components/model-selector.tsx index 9f8d4a9f..2f940f86 100644 --- a/packages/ui/src/components/model-selector.tsx +++ b/packages/ui/src/components/model-selector.tsx @@ -3,6 +3,9 @@ import { createEffect, createMemo, createSignal } from "solid-js" import { providers, fetchProviders } from "../stores/sessions" import { ChevronDown } from "lucide-solid" import type { Model } from "../types/session" +import { getLogger } from "../lib/logger" +const log = getLogger("session") + interface ModelSelectorProps { instanceId: string @@ -25,7 +28,7 @@ export default function ModelSelector(props: ModelSelectorProps) { createEffect(() => { if (instanceProviders().length === 0) { - fetchProviders(props.instanceId).catch(console.error) + fetchProviders(props.instanceId).catch((error) => log.error("Failed to fetch providers", error)) } }) diff --git a/packages/ui/src/components/opencode-binary-selector.tsx b/packages/ui/src/components/opencode-binary-selector.tsx index 86f4e4a5..8576fcdb 100644 --- a/packages/ui/src/components/opencode-binary-selector.tsx +++ b/packages/ui/src/components/opencode-binary-selector.tsx @@ -4,6 +4,9 @@ import { useConfig } from "../stores/preferences" import { serverApi } from "../lib/api-client" import FileSystemBrowserDialog from "./filesystem-browser-dialog" import { openNativeFileDialog, supportsNativeDialogs } from "../lib/native/native-functions" +import { getLogger } from "../lib/logger" +const log = getLogger("actions") + interface BinaryOption { path: string @@ -83,7 +86,7 @@ const OpenCodeBinarySelector: Component = (props) = setTimeout(() => { pathsToValidate.forEach((path) => { - validateBinary(path).catch(console.error) + validateBinary(path).catch((error) => log.error("Failed to validate binary", { path, error })) }) }, 0) }) diff --git a/packages/ui/src/components/prompt-input.tsx b/packages/ui/src/components/prompt-input.tsx index 461e3143..d7b8281d 100644 --- a/packages/ui/src/components/prompt-input.tsx +++ b/packages/ui/src/components/prompt-input.tsx @@ -10,6 +10,9 @@ import Kbd from "./kbd" import { getActiveInstance } from "../stores/instances" import { agents, getSessionDraftPrompt, setSessionDraftPrompt, clearSessionDraftPrompt } from "../stores/sessions" import { showAlertDialog } from "../stores/alerts" +import { getLogger } from "../lib/logger" +const log = getLogger("actions") + interface PromptInputProps { instanceId: string @@ -563,7 +566,7 @@ export default function PromptInput(props: PromptInputProps) { }) setHistoryIndex(-1) } catch (historyError) { - console.error("Failed to update prompt history:", historyError) + log.error("Failed to update prompt history:", historyError) } } @@ -586,7 +589,7 @@ export default function PromptInput(props: PromptInputProps) { } void refreshHistory() } catch (error) { - console.error("Failed to send message:", error) + log.error("Failed to send message:", error) showAlertDialog("Failed to send message", { title: "Send failed", detail: error instanceof Error ? error.message : String(error), diff --git a/packages/ui/src/components/remote-access-overlay.tsx b/packages/ui/src/components/remote-access-overlay.tsx index 0fad9387..2f91aa01 100644 --- a/packages/ui/src/components/remote-access-overlay.tsx +++ b/packages/ui/src/components/remote-access-overlay.tsx @@ -8,6 +8,9 @@ import { serverApi } from "../lib/api-client" import { restartCli } from "../lib/native/cli" import { preferences, setListeningMode } from "../stores/preferences" import { showConfirmDialog } from "../stores/alerts" +import { getLogger } from "../lib/logger" +const log = getLogger("actions") + interface RemoteAccessOverlayProps { open: boolean @@ -62,7 +65,7 @@ export function RemoteAccessOverlay(props: RemoteAccessOverlayProps) { const dataUrl = await toDataURL(url, { margin: 1, scale: 4 }) setQrCodes((prev) => ({ ...prev, [url]: dataUrl })) } catch (err) { - console.error("Failed to generate QR code", err) + log.error("Failed to generate QR code", err) } } } @@ -101,7 +104,7 @@ export function RemoteAccessOverlay(props: RemoteAccessOverlayProps) { try { window.open(url, "_blank", "noopener,noreferrer") } catch (err) { - console.error("Failed to open URL", err) + log.error("Failed to open URL", err) } } diff --git a/packages/ui/src/components/session-list.tsx b/packages/ui/src/components/session-list.tsx index 448475cb..074d8bcc 100644 --- a/packages/ui/src/components/session-list.tsx +++ b/packages/ui/src/components/session-list.tsx @@ -7,6 +7,9 @@ import Kbd from "./kbd" import { keyboardRegistry } from "../lib/keyboard-registry" import { formatShortcut } from "../lib/keyboard-utils" import { showToastNotification } from "../lib/notifications" +import { getLogger } from "../lib/logger" +const log = getLogger("session") + interface SessionListProps { @@ -106,7 +109,7 @@ const SessionList: Component = (props) => { await navigator.clipboard.writeText(sessionId) showToastNotification({ message: "Session ID copied", variant: "success" }) } catch (error) { - console.error(`Failed to copy session ID ${sessionId}:`, error) + log.error(`Failed to copy session ID ${sessionId}:`, error) showToastNotification({ message: "Unable to copy session ID", variant: "error" }) } } diff --git a/packages/ui/src/components/session-picker.tsx b/packages/ui/src/components/session-picker.tsx index 60384f2c..00f26114 100644 --- a/packages/ui/src/components/session-picker.tsx +++ b/packages/ui/src/components/session-picker.tsx @@ -4,6 +4,9 @@ import type { Session, Agent } from "../types/session" import { getParentSessions, createSession, setActiveParentSession } from "../stores/sessions" import { instances, stopInstance } from "../stores/instances" import { agents } from "../stores/sessions" +import { getLogger } from "../lib/logger" +const log = getLogger("session") + interface SessionPickerProps { instanceId: string @@ -55,7 +58,7 @@ const SessionPicker: Component = (props) => { setActiveParentSession(props.instanceId, session.id) props.onClose() } catch (error) { - console.error("Failed to create session:", error) + log.error("Failed to create session:", error) } finally { setIsCreating(false) } diff --git a/packages/ui/src/components/session/session-view.tsx b/packages/ui/src/components/session/session-view.tsx index 511ca727..263b1ef1 100644 --- a/packages/ui/src/components/session/session-view.tsx +++ b/packages/ui/src/components/session/session-view.tsx @@ -8,6 +8,9 @@ import PromptInput from "../prompt-input" import { instances } from "../../stores/instances" import { loadMessages, sendMessage, forkSession, isSessionMessagesLoading, setActiveParentSession, setActiveSession, runShellCommand } from "../../stores/sessions" import { showAlertDialog } from "../../stores/alerts" +import { getLogger } from "../../lib/logger" + +const log = getLogger("session") function isTextPart(part: ClientPart): part is ClientPart & { type: "text"; text: string } { return part?.type === "text" && typeof (part as any).text === "string" @@ -33,7 +36,7 @@ export const SessionView: Component = (props) => { const currentSession = session() if (currentSession) { - loadMessages(props.instanceId, currentSession.id).catch(console.error) + loadMessages(props.instanceId, currentSession.id).catch((error) => log.error("Failed to load messages", error)) } }) @@ -84,7 +87,7 @@ export const SessionView: Component = (props) => { } } } catch (error) { - console.error("Failed to revert:", error) + log.error("Failed to revert message", error) showAlertDialog("Failed to revert to message", { title: "Revert failed", variant: "error", @@ -94,7 +97,7 @@ export const SessionView: Component = (props) => { async function handleFork(messageId?: string) { if (!messageId) { - console.warn("Fork requires a user message id") + log.warn("Fork requires a user message id") return } @@ -109,7 +112,7 @@ export const SessionView: Component = (props) => { setActiveSession(props.instanceId, forkedSession.id) } - await loadMessages(props.instanceId, forkedSession.id).catch(console.error) + await loadMessages(props.instanceId, forkedSession.id).catch((error) => log.error("Failed to load forked session messages", error)) if (restoredText) { const textarea = document.querySelector(".prompt-input") as HTMLTextAreaElement @@ -120,7 +123,7 @@ export const SessionView: Component = (props) => { } } } catch (error) { - console.error("Failed to fork session:", error) + log.error("Failed to fork session", error) showAlertDialog("Failed to fork session", { title: "Fork failed", variant: "error", diff --git a/packages/ui/src/components/tool-call.tsx b/packages/ui/src/components/tool-call.tsx index f23ff9ee..4eb655a9 100644 --- a/packages/ui/src/components/tool-call.tsx +++ b/packages/ui/src/components/tool-call.tsx @@ -11,6 +11,9 @@ import type { TextPart, RenderCache } from "../types/message" import { resolveToolRenderer } from "./tool-call/renderers" import type { DiffPayload, DiffRenderOptions, MarkdownRenderOptions, ToolCallPart, ToolRendererContext } from "./tool-call/types" import { getRelativePath, getToolIcon, getToolName, isToolStateCompleted, isToolStateError, isToolStateRunning } from "./tool-call/utils" +import { getLogger } from "../lib/logger" + +const log = getLogger("session") type ToolState = import("@opencode-ai/sdk").ToolState @@ -540,7 +543,7 @@ export default function ToolCall(props: ToolCallProps) { const sessionId = permission.sessionID || props.sessionId await sendPermissionResponse(props.instanceId, sessionId, permission.id, response) } catch (error) { - console.error("Failed to send permission response:", error) + log.error("Failed to send permission response", error) setPermissionError(error instanceof Error ? error.message : "Unable to update permission") } finally { setPermissionSubmitting(false) diff --git a/packages/ui/src/components/tool-call/utils.ts b/packages/ui/src/components/tool-call/utils.ts index f25d5271..8155f3e7 100644 --- a/packages/ui/src/components/tool-call/utils.ts +++ b/packages/ui/src/components/tool-call/utils.ts @@ -2,6 +2,9 @@ import { isRenderableDiffText } from "../../lib/diff-utils" import { getLanguageFromPath } from "../../lib/markdown" import type { ToolState } from "@opencode-ai/sdk" import type { DiffPayload } from "./types" +import { getLogger } from "../../lib/logger" +const log = getLogger("session") + export type ToolStateRunning = import("@opencode-ai/sdk").ToolStateRunning export type ToolStateCompleted = import("@opencode-ai/sdk").ToolStateCompleted @@ -134,7 +137,7 @@ export function formatUnknown(value: unknown): { text: string; language?: string try { return { text: JSON.stringify(value, null, 2), language: "json" } } catch (error) { - console.error("Failed to stringify tool call output", error) + log.error("Failed to stringify tool call output", error) return { text: String(value) } } } diff --git a/packages/ui/src/components/unified-picker.tsx b/packages/ui/src/components/unified-picker.tsx index 4952c11a..3f9e9ebc 100644 --- a/packages/ui/src/components/unified-picker.tsx +++ b/packages/ui/src/components/unified-picker.tsx @@ -2,6 +2,9 @@ import { Component, createSignal, createEffect, For, Show, onCleanup } from "sol import type { Agent } from "../types/session" import type { OpencodeClient } from "@opencode-ai/sdk/client" import { serverApi } from "../lib/api-client" +import { getLogger } from "../lib/logger" +const log = getLogger("actions") + const SEARCH_RESULT_LIMIT = 100 const SEARCH_DEBOUNCE_MS = 200 @@ -124,7 +127,7 @@ const UnifiedPicker: Component = (props) => { return snapshot }) .catch((error) => { - console.error(`[UnifiedPicker] Failed to load workspace files:`, error) + log.error(`[UnifiedPicker] Failed to load workspace files:`, error) setAllFiles([]) setCachedWorkspaceId(null) throw error @@ -178,7 +181,7 @@ const UnifiedPicker: Component = (props) => { applyFileResults(mapEntriesToFileItems(results)) } catch (error) { if (workspaceId === props.workspaceId) { - console.error(`[UnifiedPicker] Failed to fetch files:`, error) + log.error(`[UnifiedPicker] Failed to fetch files:`, error) if (shouldApplyResults(requestId, workspaceId)) { applyFileResults([]) } diff --git a/packages/ui/src/lib/api-client.ts b/packages/ui/src/lib/api-client.ts index 45b57cba..12f79865 100644 --- a/packages/ui/src/lib/api-client.ts +++ b/packages/ui/src/lib/api-client.ts @@ -17,6 +17,7 @@ import type { WorkspaceEventPayload, WorkspaceEventType, } from "../../../server/src/api-types" +import { getLogger } from "./logger" const FALLBACK_API_BASE = "http://127.0.0.1:9898" const RUNTIME_BASE = typeof window !== "undefined" ? window.location?.origin : undefined @@ -38,15 +39,15 @@ function buildEventsUrl(base: string | undefined, path: string): string { return path } -const HTTP_PREFIX = "[HTTP]" +const httpLogger = getLogger("api") +const sseLogger = getLogger("sse") function logHttp(message: string, context?: Record) { - if (context) { - console.log(`${HTTP_PREFIX} ${message}`, context) + httpLogger.info(message, context) return } - console.log(`${HTTP_PREFIX} ${message}`) + httpLogger.info(message) } async function request(path: string, init?: RequestInit): Promise { @@ -186,18 +187,18 @@ export const serverApi = { return request(`/api/storage/instances/${encodeURIComponent(id)}`, { method: "DELETE" }) }, connectEvents(onEvent: (event: WorkspaceEventPayload) => void, onError?: () => void) { - console.log(`[SSE] Connecting to ${EVENTS_URL}`) + sseLogger.info(`Connecting to ${EVENTS_URL}`) const source = new EventSource(EVENTS_URL) source.onmessage = (event) => { try { const payload = JSON.parse(event.data) as WorkspaceEventPayload onEvent(payload) } catch (error) { - console.error("[SSE] Failed to parse event", error) + sseLogger.error("Failed to parse event", error) } } source.onerror = () => { - console.warn("[SSE] EventSource error, closing stream") + sseLogger.warn("EventSource error, closing stream") onError?.() } return source diff --git a/packages/ui/src/lib/command-utils.ts b/packages/ui/src/lib/command-utils.ts index 535454b5..992586d5 100644 --- a/packages/ui/src/lib/command-utils.ts +++ b/packages/ui/src/lib/command-utils.ts @@ -2,6 +2,9 @@ import type { Command } from "./commands" import type { Command as SDKCommand } from "@opencode-ai/sdk" import { showAlertDialog } from "../stores/alerts" import { activeSessionId, executeCustomCommand } from "../stores/sessions" +import { getLogger } from "./logger" + +const log = getLogger("actions") export function commandRequiresArguments(template?: string): boolean { if (!template) return false @@ -47,7 +50,7 @@ export function buildCustomCommandEntries(instanceId: string, commands: SDKComma try { await executeCustomCommand(instanceId, sessionId, cmd.name, args) } catch (error) { - console.error("Failed to run custom command:", error) + log.error("Failed to run custom command", error) showAlertDialog("Failed to run custom command. Check the console for details.", { title: "Command failed", variant: "error", diff --git a/packages/ui/src/lib/hooks/use-app-lifecycle.ts b/packages/ui/src/lib/hooks/use-app-lifecycle.ts index 16734244..9d0d4e8c 100644 --- a/packages/ui/src/lib/hooks/use-app-lifecycle.ts +++ b/packages/ui/src/lib/hooks/use-app-lifecycle.ts @@ -8,6 +8,9 @@ import { keyboardRegistry } from "../keyboard-registry" import { abortSession, getSessions, isSessionBusy } from "../../stores/sessions" import { showCommandPalette, hideCommandPalette } from "../../stores/command-palette" import type { Instance } from "../../types/instance" +import { getLogger } from "../logger" + +const log = getLogger("actions") interface UseAppLifecycleOptions { setEscapeInDebounce: (value: boolean) => void @@ -115,9 +118,9 @@ export function useAppLifecycle(options: UseAppLifecycleOptions) { try { await abortSession(instance.id, sessionId) - console.log("Session aborted successfully") + log.info("Session aborted successfully", { instanceId: instance.id, sessionId }) } catch (error) { - console.error("Failed to abort session:", error) + log.error("Failed to abort session", error) } }, () => { diff --git a/packages/ui/src/lib/hooks/use-commands.ts b/packages/ui/src/lib/hooks/use-commands.ts index 6b5ff746..1469719e 100644 --- a/packages/ui/src/lib/hooks/use-commands.ts +++ b/packages/ui/src/lib/hooks/use-commands.ts @@ -17,6 +17,9 @@ import type { Instance } from "../../types/instance" import type { MessageRecord } from "../../stores/message-v2/types" import { messageStoreBus } from "../../stores/message-v2/bus" import { cleanupBlankSessions } from "../../stores/session-state" +import { getLogger } from "../logger" + +const log = getLogger("actions") export interface UseCommandsOptions { preferences: Accessor @@ -236,15 +239,16 @@ export function useCommands(options: UseCommandsOptions) { modelID: session.model.modelId, }, }) - } catch (error: unknown) { + } catch (error) { setSessionCompactionState(instance.id, sessionId, false) - console.error("Failed to compact session:", error) + log.error("Failed to compact session", error) const message = error instanceof Error ? error.message : "Failed to compact session" showAlertDialog(`Compact failed: ${message}`, { title: "Compact failed", variant: "error", }) } + }, }) @@ -322,12 +326,13 @@ export function useCommands(options: UseCommandsOptions) { } } } catch (error) { - console.error("Failed to revert message:", error) + log.error("Failed to revert message", error) showAlertDialog("Failed to revert message", { title: "Undo failed", variant: "error", }) } + }, }) @@ -503,7 +508,7 @@ export function useCommands(options: UseCommandsOptions) { category: "System", keywords: ["/help", "shortcuts", "help"], action: () => { - console.log("Show help modal (not implemented)") + log.info("Show help modal (not implemented)") }, }) } @@ -513,11 +518,11 @@ export function useCommands(options: UseCommandsOptions) { const result = command.action?.() if (result instanceof Promise) { void result.catch((error) => { - console.error("Command execution failed:", error) + log.error("Command execution failed", error) }) } } catch (error) { - console.error("Command execution failed:", error) + log.error("Command execution failed", error) } } diff --git a/packages/ui/src/lib/logger.ts b/packages/ui/src/lib/logger.ts new file mode 100644 index 00000000..3865833e --- /dev/null +++ b/packages/ui/src/lib/logger.ts @@ -0,0 +1,128 @@ +import debug from "debug" + +export type LoggerNamespace = "sse" | "api" | "session" | "actions" + +interface Logger { + log: (...args: unknown[]) => void + info: (...args: unknown[]) => void + warn: (...args: unknown[]) => void + error: (...args: unknown[]) => void +} + +interface NamespaceState { + name: LoggerNamespace + enabled: boolean +} + +const KNOWN_NAMESPACES: LoggerNamespace[] = ["sse", "api", "session", "actions"] +const STORAGE_KEY = "opencode:logger:namespaces" + +const namespaceLoggers = new Map() +const enabledNamespaces = new Set() +const rawConsole = typeof globalThis !== "undefined" ? globalThis.console : undefined + +function applyEnabledNamespaces(): void { + if (enabledNamespaces.size === 0) { + debug.disable() + } else { + debug.enable(Array.from(enabledNamespaces).join(",")) + } +} + +function persistEnabledNamespaces(): void { + if (typeof window === "undefined" || !window?.localStorage) return + try { + window.localStorage.setItem(STORAGE_KEY, JSON.stringify(Array.from(enabledNamespaces))) + } catch (error) { + rawConsole?.warn?.("Failed to persist logger namespaces", error) + } +} + +function hydrateNamespacesFromStorage(): void { + if (typeof window === "undefined" || !window?.localStorage) return + try { + const stored = window.localStorage.getItem(STORAGE_KEY) + if (!stored) return + const parsed: unknown = JSON.parse(stored) + if (!Array.isArray(parsed)) return + for (const name of parsed) { + if (KNOWN_NAMESPACES.includes(name as LoggerNamespace)) { + enabledNamespaces.add(name as LoggerNamespace) + } + } + } catch (error) { + rawConsole?.warn?.("Failed to hydrate logger namespaces", error) + } +} + +hydrateNamespacesFromStorage() +applyEnabledNamespaces() + +function buildLogger(namespace: LoggerNamespace): Logger { + const base = debug(namespace) + const baseLogger: (...args: any[]) => void = base + const formatAndLog = (level: string, args: any[]) => { + baseLogger(level, ...args) + } + return { + log: (...args: any[]) => baseLogger(...args), + info: (...args: any[]) => baseLogger(...args), + warn: (...args: any[]) => formatAndLog("[warn]", args), + error: (...args: any[]) => formatAndLog("[error]", args), + } +} + +function getLogger(namespace: LoggerNamespace): Logger { + if (!KNOWN_NAMESPACES.includes(namespace)) { + throw new Error(`Unknown logger namespace: ${namespace}`) + } + if (!namespaceLoggers.has(namespace)) { + namespaceLoggers.set(namespace, buildLogger(namespace)) + } + return namespaceLoggers.get(namespace)! +} + +function listLoggerNamespaces(): NamespaceState[] { + return KNOWN_NAMESPACES.map((name) => ({ name, enabled: enabledNamespaces.has(name) })) +} + +function enableLogger(namespace: LoggerNamespace): void { + if (!KNOWN_NAMESPACES.includes(namespace)) { + throw new Error(`Unknown logger namespace: ${namespace}`) + } + if (enabledNamespaces.has(namespace)) return + enabledNamespaces.add(namespace) + persistEnabledNamespaces() + applyEnabledNamespaces() +} + +function disableLogger(namespace: LoggerNamespace): void { + if (!KNOWN_NAMESPACES.includes(namespace)) { + throw new Error(`Unknown logger namespace: ${namespace}`) + } + if (!enabledNamespaces.has(namespace)) return + enabledNamespaces.delete(namespace) + persistEnabledNamespaces() + applyEnabledNamespaces() +} + +function disableAllLoggers(): void { + enabledNamespaces.clear() + persistEnabledNamespaces() + applyEnabledNamespaces() +} + +function enableAllLoggers(): void { + KNOWN_NAMESPACES.forEach((namespace) => enabledNamespaces.add(namespace)) + persistEnabledNamespaces() + applyEnabledNamespaces() +} + +export { + getLogger, + listLoggerNamespaces, + enableLogger, + disableLogger, + enableAllLoggers, + disableAllLoggers, +} diff --git a/packages/ui/src/lib/markdown.ts b/packages/ui/src/lib/markdown.ts index 25bbe773..50144e47 100644 --- a/packages/ui/src/lib/markdown.ts +++ b/packages/ui/src/lib/markdown.ts @@ -1,5 +1,8 @@ import { marked } from "marked" import { createHighlighter, type Highlighter, bundledLanguages } from "shiki/bundle/full" +import { getLogger } from "./logger" + +const log = getLogger("actions") let highlighter: Highlighter | null = null let highlighterPromise: Promise | null = null @@ -71,7 +74,7 @@ function triggerLanguageListeners() { try { listener() } catch (error) { - console.error("Error in language listener:", error) + log.error("Error in language listener", error) } } } diff --git a/packages/ui/src/lib/native/cli.ts b/packages/ui/src/lib/native/cli.ts index 6ec57143..2fb92dc4 100644 --- a/packages/ui/src/lib/native/cli.ts +++ b/packages/ui/src/lib/native/cli.ts @@ -1,4 +1,7 @@ import { runtimeEnv } from "../runtime-env" +import { getLogger } from "../logger" +const log = getLogger("actions") + export async function restartCli(): Promise { try { @@ -20,7 +23,7 @@ export async function restartCli(): Promise { return false } } catch (error) { - console.error("Failed to restart CLI", error) + log.error("Failed to restart CLI", error) return false } diff --git a/packages/ui/src/lib/native/electron/functions.ts b/packages/ui/src/lib/native/electron/functions.ts index dbefabb2..cd27a1c7 100644 --- a/packages/ui/src/lib/native/electron/functions.ts +++ b/packages/ui/src/lib/native/electron/functions.ts @@ -1,4 +1,7 @@ import type { NativeDialogOptions } from "../native-functions" +import { getLogger } from "../../logger" +const log = getLogger("actions") + interface ElectronDialogResult { canceled?: boolean @@ -33,7 +36,7 @@ export async function openElectronNativeDialog(options: NativeDialogOptions): Pr const result = await api.openDialog(options) return coerceFirstPath(result) } catch (error) { - console.error("[native] electron dialog failed", error) + log.error("[native] electron dialog failed", error) return null } } diff --git a/packages/ui/src/lib/native/tauri/functions.ts b/packages/ui/src/lib/native/tauri/functions.ts index 9415c0a3..ba82534b 100644 --- a/packages/ui/src/lib/native/tauri/functions.ts +++ b/packages/ui/src/lib/native/tauri/functions.ts @@ -1,4 +1,7 @@ import type { NativeDialogOptions } from "../native-functions" +import { getLogger } from "../../logger" +const log = getLogger("actions") + interface TauriDialogModule { open?: ( @@ -49,7 +52,7 @@ export async function openTauriNativeDialog(options: NativeDialogOptions): Promi return response } catch (error) { - console.error("[native] tauri dialog failed", error) + log.error("[native] tauri dialog failed", error) return null } } diff --git a/packages/ui/src/lib/runtime-env.ts b/packages/ui/src/lib/runtime-env.ts index 5fffbd69..3f7c270b 100644 --- a/packages/ui/src/lib/runtime-env.ts +++ b/packages/ui/src/lib/runtime-env.ts @@ -1,3 +1,5 @@ +import { getLogger } from "./logger" + export type HostRuntime = "electron" | "tauri" | "web" export type PlatformKind = "desktop" | "mobile" @@ -61,6 +63,8 @@ function detectPlatform(): PlatformKind { return "desktop" } +const log = getLogger("actions") + let cachedEnv: RuntimeEnvironment | null = null export function detectRuntimeEnvironment(): RuntimeEnvironment { @@ -71,9 +75,8 @@ export function detectRuntimeEnvironment(): RuntimeEnvironment { host: detectHost(), platform: detectPlatform(), } - if (typeof console !== "undefined") { - const message = `[runtime] host=${cachedEnv.host} platform=${cachedEnv.platform}` - console.info(message) + if (typeof window !== "undefined") { + log.info(`[runtime] host=${cachedEnv.host} platform=${cachedEnv.platform}`) } return cachedEnv } diff --git a/packages/ui/src/lib/server-events.ts b/packages/ui/src/lib/server-events.ts index c63eb036..ba56e408 100644 --- a/packages/ui/src/lib/server-events.ts +++ b/packages/ui/src/lib/server-events.ts @@ -1,16 +1,17 @@ import type { WorkspaceEventPayload, WorkspaceEventType } from "../../../server/src/api-types" import { serverApi } from "./api-client" +import { getLogger } from "./logger" const RETRY_BASE_DELAY = 1000 const RETRY_MAX_DELAY = 10000 -const SSE_PREFIX = "[SSE]" +const log = getLogger("sse") function logSse(message: string, context?: Record) { if (context) { - console.log(`${SSE_PREFIX} ${message}`, context) + log.info(message, context) return } - console.log(`${SSE_PREFIX} ${message}`) + log.info(message) } class ServerEvents { diff --git a/packages/ui/src/lib/sse-manager.ts b/packages/ui/src/lib/sse-manager.ts index 5902864c..c1a9c1a1 100644 --- a/packages/ui/src/lib/sse-manager.ts +++ b/packages/ui/src/lib/sse-manager.ts @@ -20,6 +20,9 @@ import type { InstanceStreamStatus, WorkspaceEventPayload, } from "../../../server/src/api-types" +import { getLogger } from "./logger" + +const log = getLogger("sse") type InstanceEventPayload = Extract type InstanceStatusPayload = Extract @@ -80,11 +83,11 @@ class SSEManager { private handleEvent(instanceId: string, event: SSEEvent | InstanceStreamEvent): void { if (!event || typeof event !== "object" || typeof (event as { type?: unknown }).type !== "string") { - console.warn("[SSE] Dropping malformed event", event) + log.warn("Dropping malformed event", event) return } - console.log("[SSE] Received event:", event.type, event) + log.info("Received event", { type: event.type, event }) switch (event.type) { case "message.updated": @@ -124,7 +127,7 @@ class SSEManager { this.onLspUpdated?.(instanceId, event as EventLspUpdated) break default: - console.warn("[SSE] Unknown event type:", event.type) + log.warn("Unknown SSE event type", { type: event.type }) } } diff --git a/packages/ui/src/lib/storage.ts b/packages/ui/src/lib/storage.ts index 88769b40..a5f1afa6 100644 --- a/packages/ui/src/lib/storage.ts +++ b/packages/ui/src/lib/storage.ts @@ -1,6 +1,9 @@ import type { AppConfig, InstanceData } from "../../../server/src/api-types" import { serverApi } from "./api-client" import { serverEvents } from "./server-events" +import { getLogger } from "./logger" + +const log = getLogger("actions") export type ConfigData = AppConfig @@ -19,7 +22,7 @@ function isDeepEqual(a: unknown, b: unknown): boolean { try { return JSON.stringify(a) === JSON.stringify(b) } catch (error) { - console.warn("Failed to compare config objects", error) + log.warn("Failed to compare config objects", error) } } diff --git a/packages/ui/src/renderer/index.html b/packages/ui/src/renderer/index.html index e84c6c3f..dd372bac 100644 --- a/packages/ui/src/renderer/index.html +++ b/packages/ui/src/renderer/index.html @@ -19,7 +19,8 @@ try { document.documentElement.setAttribute('data-theme', 'dark') } catch (error) { - console.warn('Failed to apply initial theme', error) + const rawConsole = globalThis?.["console"] + rawConsole?.warn?.('Failed to apply initial theme', error) } })() diff --git a/packages/ui/src/renderer/loading.html b/packages/ui/src/renderer/loading.html index 32112d85..7278ddd2 100644 --- a/packages/ui/src/renderer/loading.html +++ b/packages/ui/src/renderer/loading.html @@ -9,7 +9,8 @@ try { document.documentElement.setAttribute('data-theme', 'dark') } catch (error) { - console.warn('Failed to apply initial theme', error) + const rawConsole = globalThis?.["console"] + rawConsole?.warn?.('Failed to apply initial theme', error) } })() diff --git a/packages/ui/src/stores/instance-config.tsx b/packages/ui/src/stores/instance-config.tsx index de578386..5e50641a 100644 --- a/packages/ui/src/stores/instance-config.tsx +++ b/packages/ui/src/stores/instance-config.tsx @@ -1,6 +1,9 @@ import { createContext, createMemo, createSignal, onCleanup, type Accessor, type ParentComponent, useContext } from "solid-js" import type { InstanceData } from "../../../server/src/api-types" import { storage } from "../lib/storage" +import { getLogger } from "../lib/logger" + +const log = getLogger("api") const DEFAULT_INSTANCE_DATA: InstanceData = { messageHistory: [], agentModelSelections: {} } @@ -54,7 +57,7 @@ async function ensureInstanceConfig(instanceId: string): Promise { attachSubscription(instanceId) }) .catch((error) => { - console.warn("Failed to load instance data:", error) + log.warn("Failed to load instance data", error) setInstanceData(instanceId, DEFAULT_INSTANCE_DATA) attachSubscription(instanceId) }) @@ -74,7 +77,7 @@ async function updateInstanceConfig(instanceId: string, mutator: (draft: Instanc try { await storage.saveInstanceData(instanceId, draft) } catch (error) { - console.warn("Failed to persist instance data:", error) + log.warn("Failed to persist instance data", error) } setInstanceData(instanceId, draft) } diff --git a/packages/ui/src/stores/instances.ts b/packages/ui/src/stores/instances.ts index e6ae9def..967d7ec2 100644 --- a/packages/ui/src/stores/instances.ts +++ b/packages/ui/src/stores/instances.ts @@ -19,6 +19,9 @@ import { setSessionPendingPermission } from "./session-state" import { setHasInstances } from "./ui" import { messageStoreBus } from "./message-v2/bus" import { clearCacheForInstance } from "../lib/global-cache" +import { getLogger } from "../lib/logger" + +const log = getLogger("api") const [instances, setInstances] = createSignal>(new Map()) @@ -99,7 +102,7 @@ function attachClient(descriptor: WorkspaceDescriptor) { }) sseManager.seedStatus(descriptor.id, "connecting") void hydrateInstanceData(descriptor.id).catch((error) => { - console.error("Failed to hydrate instance data", error) + log.error("Failed to hydrate instance data", error) }) } @@ -123,7 +126,7 @@ async function hydrateInstanceData(instanceId: string) { if (!instance?.client) return await fetchCommands(instanceId, instance.client) } catch (error) { - console.error("Failed to fetch initial data:", error) + log.error("Failed to fetch initial data", error) } } @@ -135,7 +138,7 @@ void (async function initializeWorkspaces() { setHasInstances(false) } } catch (error) { - console.error("Failed to load workspaces", error) + log.error("Failed to load workspaces", error) } })() @@ -305,7 +308,7 @@ async function createInstance(folder: string, _binaryPath?: string): Promise { const instance = instances().get(instanceId) if (!instance) { - console.warn(`[LSP] Skipping fetch; instance ${instanceId} not found`) + log.warn("[LSP] Skipping status fetch; instance not found", { instanceId }) return undefined } if (!instance.client) { - console.warn(`[LSP] Skipping fetch; instance ${instanceId} client not ready`) + log.warn("[LSP] Skipping status fetch; client not ready", { instanceId }) return undefined } const lsp = instance.client.lsp if (!lsp?.status) { - console.warn(`[LSP] Skipping fetch; lsp.status API unavailable for instance ${instanceId}`) + log.warn("[LSP] Skipping status fetch; API unavailable", { instanceId }) return undefined } - console.log(`[HTTP] GET /lsp.status for instance ${instanceId}`) + log.info("lsp.status", { instanceId }) const response = await lsp.status() return response.data ?? [] } @@ -536,13 +539,13 @@ async function sendPermissionResponse( try { await instance.client.postSessionIdPermissionsPermissionId({ path: { id: sessionId, permissionID: permissionId }, - body: { response } + body: { response }, }) // Remove from queue after successful response removePermissionFromQueue(instanceId, permissionId) } catch (error) { - console.error("Failed to send permission response:", error) + log.error("Failed to send permission response", error) throw error } } @@ -561,7 +564,7 @@ sseManager.onConnectionLost = (instanceId, reason) => { } sseManager.onLspUpdated = async (instanceId) => { - console.log(`[LSP] Received lsp.updated event for instance ${instanceId}`) + log.info("lsp.updated", { instanceId }) try { const lspStatus = await fetchLspStatus(instanceId) if (!lspStatus) { @@ -569,7 +572,7 @@ sseManager.onLspUpdated = async (instanceId) => { } const instance = instances().get(instanceId) if (!instance) { - console.warn(`[LSP] Instance ${instanceId} disappeared before metadata update`) + log.warn("[LSP] Instance disappeared before metadata update", { instanceId }) return } updateInstance(instanceId, { @@ -579,7 +582,7 @@ sseManager.onLspUpdated = async (instanceId) => { }, }) } catch (error) { - console.error("Failed to refresh LSP status:", error) + log.error("Failed to refresh LSP status", error) } } @@ -592,7 +595,7 @@ async function acknowledgeDisconnectedInstance(): Promise { try { await stopInstance(pending.id) } catch (error) { - console.error("Failed to stop disconnected instance:", error) + log.error("Failed to stop disconnected instance", error) } finally { setDisconnectedInstance(null) if (instances().size === 0) { diff --git a/packages/ui/src/stores/message-v2/bus.ts b/packages/ui/src/stores/message-v2/bus.ts index 0ccd98eb..45250647 100644 --- a/packages/ui/src/stores/message-v2/bus.ts +++ b/packages/ui/src/stores/message-v2/bus.ts @@ -1,6 +1,9 @@ import { createInstanceMessageStore } from "./instance-store" import type { InstanceMessageStore } from "./instance-store" import { clearCacheForInstance } from "../../lib/global-cache" +import { getLogger } from "../../lib/logger" + +const log = getLogger("session") class MessageStoreBus { private stores = new Map() @@ -55,7 +58,7 @@ class MessageStoreBus { try { handler(instanceId) } catch (error) { - console.error("Failed to run message store teardown handler", error) + log.error("Failed to run message store teardown handler", error) } } } diff --git a/packages/ui/src/stores/preferences.tsx b/packages/ui/src/stores/preferences.tsx index 85b13e98..b20c5a8a 100644 --- a/packages/ui/src/stores/preferences.tsx +++ b/packages/ui/src/stores/preferences.tsx @@ -6,6 +6,9 @@ import { getInstanceConfig, updateInstanceConfig as updateInstanceData, } from "./instance-config" +import { getLogger } from "../lib/logger" + +const log = getLogger("actions") type DeepReadonly = T extends (...args: any[]) => unknown ? T @@ -81,7 +84,7 @@ function deepEqual(a: unknown, b: unknown): boolean { try { return JSON.stringify(a) === JSON.stringify(b) } catch (error) { - console.warn("Failed to compare preference values", error) + log.warn("Failed to compare preference values", error) } } return false @@ -148,11 +151,11 @@ async function syncConfig(source?: ConfigData): Promise { applyConfig(cleaned) if (migrated) { void storage.updateConfig(cleaned).catch((error: unknown) => { - console.error("Failed to persist legacy config cleanup:", error) + log.error("Failed to persist legacy config cleanup", error) }) } } catch (error) { - console.error("Failed to load config:", error) + log.error("Failed to load config", error) applyConfig(buildFallbackConfig()) } } @@ -172,7 +175,7 @@ function logConfigDiff(previous: ConfigData, next: ConfigData) { } const changes = diffObjects(previous, next) if (changes.length > 0) { - console.debug("[Config] Changes", changes) + log.info("[Config] Changes", changes) } } @@ -214,9 +217,9 @@ async function persistFullConfig(next: ConfigData): Promise { await ensureConfigLoaded() await storage.updateConfig(next) } catch (error) { - console.error("Failed to save config:", error) + log.error("Failed to save config", error) void syncConfig().catch((syncError: unknown) => { - console.error("Failed to refresh config:", syncError) + log.error("Failed to refresh config", syncError) }) } } @@ -303,8 +306,9 @@ function toggleUsageMetrics(): void { } function toggleAutoCleanupBlankSessions(): void { - console.log("toggle auto cleanup") - updatePreferences({ autoCleanupBlankSessions: !preferences().autoCleanupBlankSessions }) + const nextValue = !preferences().autoCleanupBlankSessions + log.info("toggle auto cleanup", { value: nextValue }) + updatePreferences({ autoCleanupBlankSessions: nextValue }) } function addRecentFolder(path: string): void { @@ -394,7 +398,7 @@ async function getAgentModelPreference(instanceId: string, agent: string): Promi } void ensureConfigLoaded().catch((error: unknown) => { - console.error("Failed to initialize config:", error) + log.error("Failed to initialize config", error) }) interface ConfigContextValue { @@ -466,12 +470,12 @@ const configContextValue: ConfigContextValue = { const ConfigProvider: ParentComponent = (props) => { onMount(() => { ensureConfigLoaded().catch((error: unknown) => { - console.error("Failed to initialize config:", error) + log.error("Failed to initialize config", error) }) const unsubscribe = storage.onConfigChanged((config) => { syncConfig(config).catch((error: unknown) => { - console.error("Failed to refresh config:", error) + log.error("Failed to refresh config", error) }) }) diff --git a/packages/ui/src/stores/session-actions.ts b/packages/ui/src/stores/session-actions.ts index f1e5d60a..e0ac5c49 100644 --- a/packages/ui/src/stores/session-actions.ts +++ b/packages/ui/src/stores/session-actions.ts @@ -6,6 +6,9 @@ import { sessions, withSession } from "./session-state" import { getDefaultModel, isModelValid } from "./session-models" import { updateSessionInfo } from "./message-v2/session-info" import { messageStoreBus } from "./message-v2/bus" +import { getLogger } from "../lib/logger" + +const log = getLogger("actions") const ID_LENGTH = 26 const BASE62_CHARS = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz" @@ -168,26 +171,27 @@ async function sendMessage( }), } - console.log("[sendMessage] Sending prompt:", { + log.info("sendMessage", { + instanceId, sessionId, requestBody, }) try { - console.log(`[HTTP] POST /session.prompt_async for instance ${instanceId}`, { sessionId, requestBody }) - const response = await instance.client.session.promptAsync({ + log.info("session.prompt", { instanceId, sessionId, requestBody }) + const response = await instance.client.session.prompt({ path: { id: sessionId }, body: requestBody, }) - console.log("[sendMessage] Response:", response) + log.info("sendMessage response", response) if (response.error) { - console.error("[sendMessage] Server returned error:", response.error) + log.error("sendMessage server error", response.error) throw new Error(JSON.stringify(response.error) || "Failed to send message") } } catch (error) { - console.error("[sendMessage] Failed to send prompt:", error) + log.error("Failed to send prompt", error) throw error } } @@ -262,16 +266,16 @@ async function abortSession(instanceId: string, sessionId: string): Promise { }) try { - console.log(`[HTTP] GET /session.list for instance ${instanceId}`) + log.info("session.list", { instanceId }) const response = await instance.client.session.list() const sessionMap = new Map() @@ -132,7 +135,7 @@ async function fetchSessions(instanceId: string): Promise { pruneDraftPrompts(instanceId, new Set(sessionMap.keys())) } catch (error) { - console.error("Failed to fetch sessions:", error) + log.error("Failed to fetch sessions:", error) throw error } finally { setLoading((prev) => { @@ -166,7 +169,7 @@ async function createSession(instanceId: string, agent?: string): Promise { @@ -269,7 +272,7 @@ async function forkSession( request.body = { messageID: options.messageId } } - console.log(`[HTTP] POST /session.fork for instance ${instanceId}`, request) + log.info(`[HTTP] POST /session.fork for instance ${instanceId}`, request) const response = await instance.client.session.fork(request) if (!response.data) { @@ -352,7 +355,7 @@ async function deleteSession(instanceId: string, sessionId: string): Promise { @@ -394,7 +397,7 @@ async function deleteSession(instanceId: string, sessionId: string): Promise { @@ -415,7 +418,7 @@ async function fetchAgents(instanceId: string): Promise { } try { - console.log(`[HTTP] GET /app.agents for instance ${instanceId}`) + log.info(`[HTTP] GET /app.agents for instance ${instanceId}`) const response = await instance.client.app.agents() const agentList = (response.data ?? []).map((agent) => ({ name: agent.name, @@ -435,7 +438,7 @@ async function fetchAgents(instanceId: string): Promise { return next }) } catch (error) { - console.error("Failed to fetch agents:", error) + log.error("Failed to fetch agents:", error) } } @@ -446,7 +449,7 @@ async function fetchProviders(instanceId: string): Promise { } try { - console.log(`[HTTP] GET /config.providers for instance ${instanceId}`) + log.info(`[HTTP] GET /config.providers for instance ${instanceId}`) const response = await instance.client.config.providers() if (!response.data) return @@ -469,7 +472,7 @@ async function fetchProviders(instanceId: string): Promise { return next }) } catch (error) { - console.error("Failed to fetch providers:", error) + log.error("Failed to fetch providers:", error) } } @@ -515,7 +518,7 @@ async function loadMessages(instanceId: string, sessionId: string, force = false }) try { - console.log(`[HTTP] GET /session.${"messages"} for instance ${instanceId}`, { sessionId }) + log.info(`[HTTP] GET /session.${"messages"} for instance ${instanceId}`, { sessionId }) const response = await instance.client.session["messages"]({ path: { id: sessionId } }) if (!response.data || !Array.isArray(response.data)) { @@ -604,7 +607,7 @@ async function loadMessages(instanceId: string, sessionId: string, force = false seedSessionMessagesV2(instanceId, sessionForV2, messages, messagesInfo) } catch (error) { - console.error("Failed to load messages:", error) + log.error("Failed to load messages:", error) throw error } finally { setLoading((prev) => { diff --git a/packages/ui/src/stores/session-events.ts b/packages/ui/src/stores/session-events.ts index 2a0cfd5c..edbbe5db 100644 --- a/packages/ui/src/stores/session-events.ts +++ b/packages/ui/src/stores/session-events.ts @@ -15,16 +15,15 @@ import type { } from "@opencode-ai/sdk" import type { MessageStatus } from "./message-v2/types" +import { getLogger } from "../lib/logger" import { showToastNotification, ToastVariant } from "../lib/notifications" import { instances, addPermissionToQueue, removePermissionFromQueue } from "./instances" import { showAlertDialog } from "./alerts" -import { - sessions, - setSessions, - withSession, -} from "./session-state" +import { sessions, setSessions, withSession } from "./session-state" import { normalizeMessagePart } from "./message-v2/normalizers" import { updateSessionInfo } from "./message-v2/session-info" + +const log = getLogger("sse") import { loadMessages } from "./session-api" import { setSessionCompactionState } from "./session-compaction" import { @@ -213,7 +212,7 @@ function handleSessionUpdate(instanceId: string, event: EventSessionUpdated): vo }) setSessionRevertV2(instanceId, info.id, info.revert ?? null) - console.log(`[SSE] New session created: ${info.id}`, newSession) + log.info(`[SSE] New session created: ${info.id}`, newSession) } else { const mergedTime = { ...existingSession.time, @@ -252,14 +251,14 @@ function handleSessionIdle(_instanceId: string, event: EventSessionIdle): void { const sessionId = event.properties?.sessionID if (!sessionId) return - console.log(`[SSE] Session idle: ${sessionId}`) + log.info(`[SSE] Session idle: ${sessionId}`) } function handleSessionCompacted(instanceId: string, event: EventSessionCompacted): void { const sessionID = event.properties?.sessionID if (!sessionID) return - console.log(`[SSE] Session compacted: ${sessionID}`) + log.info(`[SSE] Session compacted: ${sessionID}`) setSessionCompactionState(instanceId, sessionID, false) @@ -269,7 +268,7 @@ function handleSessionCompacted(instanceId: string, event: EventSessionCompacted session.time = time }) - loadMessages(instanceId, sessionID, true).catch(console.error) + loadMessages(instanceId, sessionID, true).catch((error) => log.error("Failed to reload session after compaction", error)) const instanceSessions = sessions().get(instanceId) const session = instanceSessions?.get(sessionID) @@ -287,7 +286,7 @@ function handleSessionCompacted(instanceId: string, event: EventSessionCompacted function handleSessionError(_instanceId: string, event: EventSessionError): void { const error = event.properties?.error - console.error(`[SSE] Session error:`, error) + log.error(`[SSE] Session error:`, error) let message = "Unknown error" @@ -309,16 +308,16 @@ function handleMessageRemoved(instanceId: string, event: MessageRemovedEvent): v const sessionID = event.properties?.sessionID if (!sessionID) return - console.log(`[SSE] Message removed from session ${sessionID}, reloading messages`) - loadMessages(instanceId, sessionID, true).catch(console.error) + log.info(`[SSE] Message removed from session ${sessionID}, reloading messages`) + loadMessages(instanceId, sessionID, true).catch((error) => log.error("Failed to reload messages after removal", error)) } function handleMessagePartRemoved(instanceId: string, event: MessagePartRemovedEvent): void { const sessionID = event.properties?.sessionID if (!sessionID) return - console.log(`[SSE] Message part removed from session ${sessionID}, reloading messages`) - loadMessages(instanceId, sessionID, true).catch(console.error) + log.info(`[SSE] Message part removed from session ${sessionID}, reloading messages`) + loadMessages(instanceId, sessionID, true).catch((error) => log.error("Failed to reload messages after part removal", error)) } function handleTuiToast(_instanceId: string, event: TuiToastEvent): void { @@ -342,7 +341,7 @@ function handlePermissionUpdated(instanceId: string, event: EventPermissionUpdat const permission = event.properties if (!permission) return - console.log(`[SSE] Permission updated: ${permission.id} (${permission.type})`) + log.info(`[SSE] Permission updated: ${permission.id} (${permission.type})`) addPermissionToQueue(instanceId, permission) upsertPermissionV2(instanceId, permission) } @@ -351,7 +350,7 @@ function handlePermissionReplied(instanceId: string, event: EventPermissionRepli const { permissionID } = event.properties if (!permissionID) return - console.log(`[SSE] Permission replied: ${permissionID}`) + log.info(`[SSE] Permission replied: ${permissionID}`) removePermissionFromQueue(instanceId, permissionID) removePermissionV2(instanceId, permissionID) } diff --git a/packages/ui/src/stores/session-state.ts b/packages/ui/src/stores/session-state.ts index b397469a..452c9354 100644 --- a/packages/ui/src/stores/session-state.ts +++ b/packages/ui/src/stores/session-state.ts @@ -6,6 +6,9 @@ import { showToastNotification } from "../lib/notifications" import { messageStoreBus } from "./message-v2/bus" import { instances } from "./instances" import { showConfirmDialog } from "./alerts" +import { getLogger } from "../lib/logger" + +const log = getLogger("session") export interface SessionInfo { cost: number @@ -248,7 +251,7 @@ async function isBlankSession(session: Session, instanceId: string, fetchIfNeede const response = await instance.client.session.messages({ path: { id: session.id } }) messages = response.data || [] } catch (error) { - console.error(`Failed to fetch messages for session ${session.id}:`, error) + log.error(`Failed to fetch messages for session ${session.id}`, error) return isFreshSession } @@ -309,13 +312,13 @@ async function cleanupBlankSessions(instanceId: string, excludeSessionId?: strin if (!isBlank) return false await deleteSession(instanceId, sessionId).catch((error: Error) => { - console.error(`Failed to delete blank session ${sessionId}:`, error) + log.error(`Failed to delete blank session ${sessionId}`, error) }) return true }) if (cleanupPromises.length > 0) { - console.log(`Cleaning up ${cleanupPromises.length} blank sessions`) + log.info(`Cleaning up ${cleanupPromises.length} blank sessions`) const deletionResults = await Promise.all(cleanupPromises) const deletedCount = deletionResults.filter(Boolean).length