surface lsp status in instance info
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import { Component, Show, For, createSignal, createEffect, onCleanup } from "solid-js"
|
||||
import type { Instance, RawMcpStatus } from "../types/instance"
|
||||
import { updateInstance } from "../stores/instances"
|
||||
import { fetchLspStatus, updateInstance } from "../stores/instances"
|
||||
|
||||
interface InstanceInfoProps {
|
||||
instance: Instance
|
||||
@@ -52,6 +52,7 @@ const InstanceInfo: Component<InstanceInfoProps> = (props) => {
|
||||
const status = metadata()?.mcpStatus
|
||||
return status ? parseMcpStatus(status) : []
|
||||
}
|
||||
const lspServers = () => metadata()?.lspStatus ?? []
|
||||
|
||||
createEffect(() => {
|
||||
const instance = props.instance
|
||||
@@ -82,9 +83,10 @@ const InstanceInfo: Component<InstanceInfoProps> = (props) => {
|
||||
|
||||
void (async () => {
|
||||
try {
|
||||
const [projectResult, mcpResult] = await Promise.allSettled([
|
||||
const [projectResult, mcpResult, lspResult] = await Promise.allSettled([
|
||||
client.project.current(),
|
||||
client.mcp.status(),
|
||||
fetchLspStatus(instanceId),
|
||||
])
|
||||
|
||||
if (cancelled) {
|
||||
@@ -93,11 +95,13 @@ const InstanceInfo: Component<InstanceInfoProps> = (props) => {
|
||||
|
||||
const project = projectResult.status === "fulfilled" ? projectResult.value.data : undefined
|
||||
const mcpStatus = mcpResult.status === "fulfilled" ? (mcpResult.value.data as RawMcpStatus) : undefined
|
||||
const lspStatus = lspResult.status === "fulfilled" ? lspResult.value ?? [] : undefined
|
||||
|
||||
const nextMetadata = {
|
||||
...(instance.metadata ?? {}),
|
||||
...(project ? { project } : {}),
|
||||
...(mcpStatus ? { mcpStatus } : {}),
|
||||
...(lspStatus ? { lspStatus } : {}),
|
||||
}
|
||||
|
||||
if (!nextMetadata.version) {
|
||||
@@ -213,6 +217,34 @@ const InstanceInfo: Component<InstanceInfoProps> = (props) => {
|
||||
</div>
|
||||
</Show>
|
||||
|
||||
<Show when={!isLoadingMetadata() && lspServers().length > 0}>
|
||||
<div>
|
||||
<div class="text-xs font-medium text-muted uppercase tracking-wide mb-1.5">
|
||||
LSP Servers
|
||||
</div>
|
||||
<div class="space-y-1.5">
|
||||
<For each={lspServers()}>
|
||||
{(server) => (
|
||||
<div class="px-2 py-1.5 rounded border bg-surface-secondary border-base">
|
||||
<div class="flex items-center justify-between gap-2">
|
||||
<div class="flex flex-col flex-1 min-w-0">
|
||||
<span class="text-xs text-primary font-medium truncate">{server.name ?? server.id}</span>
|
||||
<span class="text-[11px] text-secondary truncate" title={server.root}>
|
||||
{server.root}
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-1.5 flex-shrink-0 text-xs text-secondary">
|
||||
<div class={`status-dot ${server.status === "connected" ? "ready animate-pulse" : "error"}`} />
|
||||
<span>{server.status === "connected" ? "Connected" : "Error"}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
</div>
|
||||
</Show>
|
||||
|
||||
<Show when={!isLoadingMetadata() && mcpServers().length > 0}>
|
||||
<div>
|
||||
<div class="text-xs font-medium text-muted uppercase tracking-wide mb-1.5">
|
||||
|
||||
@@ -6,12 +6,13 @@ import {
|
||||
MessagePartRemovedEvent
|
||||
} from "../types/message"
|
||||
import type {
|
||||
EventSessionUpdated,
|
||||
EventLspUpdated,
|
||||
EventPermissionReplied,
|
||||
EventPermissionUpdated,
|
||||
EventSessionCompacted,
|
||||
EventSessionError,
|
||||
EventSessionIdle,
|
||||
EventPermissionUpdated,
|
||||
EventPermissionReplied
|
||||
EventSessionUpdated,
|
||||
} from "@opencode-ai/sdk"
|
||||
|
||||
interface SSEConnection {
|
||||
@@ -44,6 +45,7 @@ type SSEEvent =
|
||||
| EventSessionIdle
|
||||
| EventPermissionUpdated
|
||||
| EventPermissionReplied
|
||||
| EventLspUpdated
|
||||
| TuiToastEvent
|
||||
| { type: string; properties?: Record<string, unknown> } // Fallback for unknown event types
|
||||
|
||||
@@ -148,6 +150,9 @@ class SSEManager {
|
||||
case "permission.replied":
|
||||
this.onPermissionReplied?.(instanceId, event as EventPermissionReplied)
|
||||
break
|
||||
case "lsp.updated":
|
||||
this.onLspUpdated?.(instanceId, event as EventLspUpdated)
|
||||
break
|
||||
default:
|
||||
console.warn("[SSE] Unknown event type:", event.type)
|
||||
}
|
||||
@@ -217,6 +222,7 @@ class SSEManager {
|
||||
onSessionIdle?: (instanceId: string, event: EventSessionIdle) => void
|
||||
onPermissionUpdated?: (instanceId: string, event: EventPermissionUpdated) => void
|
||||
onPermissionReplied?: (instanceId: string, event: EventPermissionReplied) => void
|
||||
onLspUpdated?: (instanceId: string, event: EventLspUpdated) => void
|
||||
onConnectionLost?: (instanceId: string, reason: string) => void | Promise<void>
|
||||
|
||||
getStatus(instanceId: string): "connecting" | "connected" | "disconnected" | "error" | null {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { createSignal } from "solid-js"
|
||||
import type { Instance, LogEntry } from "../types/instance"
|
||||
import type { Permission } from "@opencode-ai/sdk"
|
||||
import type { LspStatus, Permission } from "@opencode-ai/sdk"
|
||||
import type { ClientPart, Message } from "../types/message"
|
||||
import { sdkManager } from "../lib/sdk-manager"
|
||||
import { sseManager } from "../lib/sse-manager"
|
||||
@@ -233,6 +233,26 @@ async function stopInstance(id: string) {
|
||||
removeInstance(id)
|
||||
}
|
||||
|
||||
async function fetchLspStatus(instanceId: string): Promise<LspStatus[] | undefined> {
|
||||
const instance = instances().get(instanceId)
|
||||
if (!instance) {
|
||||
console.warn(`[LSP] Skipping fetch; instance ${instanceId} not found`)
|
||||
return undefined
|
||||
}
|
||||
if (!instance.client) {
|
||||
console.warn(`[LSP] Skipping fetch; instance ${instanceId} client not ready`)
|
||||
return undefined
|
||||
}
|
||||
const lsp = instance.client.lsp
|
||||
if (!lsp?.status) {
|
||||
console.warn(`[LSP] Skipping fetch; lsp.status API unavailable for instance ${instanceId}`)
|
||||
return undefined
|
||||
}
|
||||
console.log(`[HTTP] GET /lsp.status for instance ${instanceId}`)
|
||||
const response = await lsp.status()
|
||||
return response.data ?? []
|
||||
}
|
||||
|
||||
function getActiveInstance(): Instance | null {
|
||||
const id = activeInstanceId()
|
||||
return id ? instances().get(id) || null : null
|
||||
@@ -546,6 +566,29 @@ sseManager.onConnectionLost = (instanceId, reason) => {
|
||||
})
|
||||
}
|
||||
|
||||
sseManager.onLspUpdated = async (instanceId) => {
|
||||
console.log(`[LSP] Received lsp.updated event for instance ${instanceId}`)
|
||||
try {
|
||||
const lspStatus = await fetchLspStatus(instanceId)
|
||||
if (!lspStatus) {
|
||||
return
|
||||
}
|
||||
const instance = instances().get(instanceId)
|
||||
if (!instance) {
|
||||
console.warn(`[LSP] Instance ${instanceId} disappeared before metadata update`)
|
||||
return
|
||||
}
|
||||
updateInstance(instanceId, {
|
||||
metadata: {
|
||||
...(instance.metadata ?? {}),
|
||||
lspStatus,
|
||||
},
|
||||
})
|
||||
} catch (error) {
|
||||
console.error("Failed to refresh LSP status:", error)
|
||||
}
|
||||
}
|
||||
|
||||
async function acknowledgeDisconnectedInstance(): Promise<void> {
|
||||
const pending = disconnectedInstance()
|
||||
if (!pending) {
|
||||
@@ -593,4 +636,5 @@ export {
|
||||
sendPermissionResponse,
|
||||
disconnectedInstance,
|
||||
acknowledgeDisconnectedInstance,
|
||||
fetchLspStatus,
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { OpencodeClient } from "@opencode-ai/sdk/client"
|
||||
import type { Project as SDKProject } from "@opencode-ai/sdk"
|
||||
import type { LspStatus, Project as SDKProject } from "@opencode-ai/sdk"
|
||||
|
||||
export interface LogEntry {
|
||||
timestamp: number
|
||||
@@ -24,6 +24,7 @@ export type RawMcpStatus = Record<string, {
|
||||
export interface InstanceMetadata {
|
||||
project?: ProjectInfo
|
||||
mcpStatus?: RawMcpStatus
|
||||
lspStatus?: LspStatus[]
|
||||
version?: string
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user