feat(ui): add runtime logger and replace console usage
This commit is contained in:
@@ -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<string, unknown>) {
|
||||
|
||||
if (context) {
|
||||
console.log(`${HTTP_PREFIX} ${message}`, context)
|
||||
httpLogger.info(message, context)
|
||||
return
|
||||
}
|
||||
console.log(`${HTTP_PREFIX} ${message}`)
|
||||
httpLogger.info(message)
|
||||
}
|
||||
|
||||
async function request<T>(path: string, init?: RequestInit): Promise<T> {
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
},
|
||||
() => {
|
||||
|
||||
@@ -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<Preferences>
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
128
packages/ui/src/lib/logger.ts
Normal file
128
packages/ui/src/lib/logger.ts
Normal file
@@ -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<LoggerNamespace, Logger>()
|
||||
const enabledNamespaces = new Set<LoggerNamespace>()
|
||||
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,
|
||||
}
|
||||
@@ -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<Highlighter> | 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
import { runtimeEnv } from "../runtime-env"
|
||||
import { getLogger } from "../logger"
|
||||
const log = getLogger("actions")
|
||||
|
||||
|
||||
export async function restartCli(): Promise<boolean> {
|
||||
try {
|
||||
@@ -20,7 +23,7 @@ export async function restartCli(): Promise<boolean> {
|
||||
return false
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to restart CLI", error)
|
||||
log.error("Failed to restart CLI", error)
|
||||
return false
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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<string, unknown>) {
|
||||
if (context) {
|
||||
console.log(`${SSE_PREFIX} ${message}`, context)
|
||||
log.info(message, context)
|
||||
return
|
||||
}
|
||||
console.log(`${SSE_PREFIX} ${message}`)
|
||||
log.info(message)
|
||||
}
|
||||
|
||||
class ServerEvents {
|
||||
|
||||
@@ -20,6 +20,9 @@ import type {
|
||||
InstanceStreamStatus,
|
||||
WorkspaceEventPayload,
|
||||
} from "../../../server/src/api-types"
|
||||
import { getLogger } from "./logger"
|
||||
|
||||
const log = getLogger("sse")
|
||||
|
||||
type InstanceEventPayload = Extract<WorkspaceEventPayload, { type: "instance.event" }>
|
||||
type InstanceStatusPayload = Extract<WorkspaceEventPayload, { type: "instance.eventStatus" }>
|
||||
@@ -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 })
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user