diff --git a/src/App.tsx b/src/App.tsx index 72a15dfa..14bb60ad 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -8,7 +8,7 @@ import InstanceTabs from "./components/instance-tabs" import SessionTabs from "./components/session-tabs" import MessageStream from "./components/message-stream" import PromptInput from "./components/prompt-input" -import LogsView from "./components/logs-view" +import InfoView from "./components/info-view" import { initMarkdown } from "./lib/markdown" import { createCommandRegistry } from "./lib/commands" import type { Command } from "./lib/commands" @@ -328,21 +328,21 @@ const App: Component = () => { action: async () => { const instance = activeInstance() const sessionId = activeSessionIdForInstance() - if (!instance || !sessionId || sessionId === "logs") return + if (!instance || !sessionId || sessionId === "info") return await handleCloseSession(instance.id, sessionId) }, }) commandRegistry.register({ - id: "switch-to-logs", - label: "Switch to Logs", - description: "Jump to logs view for current instance", + id: "switch-to-info", + label: "Switch to Info", + description: "Jump to info view for current instance", category: "Session", - keywords: ["logs", "console", "output"], + keywords: ["info", "info", "console", "output"], shortcut: { key: "L", meta: true, shift: true }, action: () => { const instance = activeInstance() - if (instance) setActiveSession(instance.id, "logs") + if (instance) setActiveSession(instance.id, "info") }, }) @@ -359,7 +359,7 @@ const App: Component = () => { const parentId = activeParentSessionId().get(instanceId) if (!parentId) return const familySessions = getSessionFamily(instanceId, parentId) - const ids = familySessions.map((s) => s.id).concat(["logs"]) + const ids = familySessions.map((s) => s.id).concat(["info"]) if (ids.length <= 1) return const current = ids.indexOf(activeSessionId().get(instanceId) || "") const next = (current + 1) % ids.length @@ -380,7 +380,7 @@ const App: Component = () => { const parentId = activeParentSessionId().get(instanceId) if (!parentId) return const familySessions = getSessionFamily(instanceId, parentId) - const ids = familySessions.map((s) => s.id).concat(["logs"]) + const ids = familySessions.map((s) => s.id).concat(["info"]) if (ids.length <= 1) return const current = ids.indexOf(activeSessionId().get(instanceId) || "") const prev = current <= 0 ? ids.length - 1 : current - 1 @@ -397,7 +397,7 @@ const App: Component = () => { action: async () => { const instance = activeInstance() const sessionId = activeSessionIdForInstance() - if (!instance || !instance.client || !sessionId || sessionId === "logs") return + if (!instance || !instance.client || !sessionId || sessionId === "info") return const sessions = getSessions(instance.id) const session = sessions.find((s) => s.id === sessionId) @@ -429,7 +429,7 @@ const App: Component = () => { action: async () => { const instance = activeInstance() const sessionId = activeSessionIdForInstance() - if (!instance || !instance.client || !sessionId || sessionId === "logs") return + if (!instance || !instance.client || !sessionId || sessionId === "info") return const sessions = getSessions(instance.id) const session = sessions.find((s) => s.id === sessionId) @@ -567,7 +567,7 @@ const App: Component = () => { action: async () => { const instance = activeInstance() const sessionId = activeSessionIdForInstance() - if (!instance || !instance.client || !sessionId || sessionId === "logs") return + if (!instance || !instance.client || !sessionId || sessionId === "info") return const sessions = getSessions(instance.id) const session = sessions.find((s) => s.id === sessionId) @@ -703,7 +703,7 @@ const App: Component = () => { if (!instance) return false const sessionId = activeSessionIdForInstance() - if (!sessionId || sessionId === "logs") return false + if (!sessionId || sessionId === "info") return false const sessions = getSessions(instance.id) const session = sessions.find((s) => s.id === sessionId) @@ -727,7 +727,7 @@ const App: Component = () => { const instance = activeInstance() const sessionId = activeSessionIdForInstance() - if (!instance || !sessionId || sessionId === "logs") return + if (!instance || !sessionId || sessionId === "info") return try { await abortSession(instance.id, sessionId) @@ -820,13 +820,13 @@ const App: Component = () => {
-
+

No session selected

Select a session to view messages

@@ -843,7 +843,7 @@ const App: Component = () => { } > - +
diff --git a/src/components/info-view.tsx b/src/components/info-view.tsx new file mode 100644 index 00000000..3d995fbc --- /dev/null +++ b/src/components/info-view.tsx @@ -0,0 +1,131 @@ +import { Component, For, createSignal, createEffect, Show, onMount, onCleanup } from "solid-js" +import { instances } from "../stores/instances" +import { ChevronDown } from "lucide-solid" +import type { LogEntry } from "../types/instance" +import InstanceInfo from "./instance-info" + +interface InfoViewProps { + instanceId: string +} + +const logsScrollState = new Map() + +const InfoView: Component = (props) => { + let scrollRef: HTMLDivElement | undefined + const savedState = logsScrollState.get(props.instanceId) + const [autoScroll, setAutoScroll] = createSignal(savedState?.autoScroll ?? false) + + const instance = () => instances().get(props.instanceId) + const logs = () => instance()?.logs ?? [] + + onMount(() => { + if (scrollRef && savedState) { + scrollRef.scrollTop = savedState.scrollTop + } + }) + + onCleanup(() => { + if (scrollRef) { + logsScrollState.set(props.instanceId, { + scrollTop: scrollRef.scrollTop, + autoScroll: autoScroll(), + }) + } + }) + + createEffect(() => { + if (autoScroll() && scrollRef && logs().length > 0) { + scrollRef.scrollTop = scrollRef.scrollHeight + } + }) + + const handleScroll = () => { + if (!scrollRef) return + + const isAtBottom = scrollRef.scrollHeight - scrollRef.scrollTop <= scrollRef.clientHeight + 50 + + setAutoScroll(isAtBottom) + } + + const scrollToBottom = () => { + if (scrollRef) { + scrollRef.scrollTop = scrollRef.scrollHeight + setAutoScroll(true) + } + } + + const formatTime = (timestamp: number) => { + const date = new Date(timestamp) + return date.toLocaleTimeString("en-US", { + hour12: false, + hour: "2-digit", + minute: "2-digit", + second: "2-digit", + }) + } + + const getLevelColor = (level: string) => { + switch (level) { + case "error": + return "text-red-600 dark:text-red-400" + case "warn": + return "text-yellow-600 dark:text-yellow-400" + case "debug": + return "text-gray-500 dark:text-gray-500" + default: + return "text-gray-900 dark:text-gray-100" + } + } + + return ( +
+
+
+ {(inst) => } +
+ +
+
+

Server Logs

+
+ +
+ 0} + fallback={ +
Waiting for server output...
+ } + > + + {(entry) => ( +
+ + {formatTime(entry.timestamp)} + + {entry.message} +
+ )} +
+
+
+ + + + +
+
+
+ ) +} + +export default InfoView diff --git a/src/components/instance-info.tsx b/src/components/instance-info.tsx new file mode 100644 index 00000000..196a2c4c --- /dev/null +++ b/src/components/instance-info.tsx @@ -0,0 +1,236 @@ +import { Component, Show, For, onMount, createSignal } from "solid-js" +import type { Instance } from "../types/instance" + +interface InstanceInfoProps { + instance: Instance + compact?: boolean +} + +function parseMcpStatus(status: unknown): Array<{ name: string; status: "running" | "stopped" | "error" }> { + if (!status || typeof status !== "object") return [] + + try { + const obj = status as Record + return Object.entries(obj).map(([name, statusValue]) => { + let mappedStatus: "running" | "stopped" | "error" + + if (statusValue === "connected") { + mappedStatus = "running" + } else if (statusValue === "disabled") { + mappedStatus = "stopped" + } else if (statusValue === "failed") { + mappedStatus = "error" + } else { + mappedStatus = "stopped" + } + + return { + name, + status: mappedStatus, + } + }) + } catch { + return [] + } +} + +const InstanceInfo: Component = (props) => { + const [isLoadingMetadata, setIsLoadingMetadata] = createSignal(true) + + const metadata = () => props.instance.metadata + const mcpServers = () => { + const status = metadata()?.mcpStatus + return parseMcpStatus(status) + } + + onMount(async () => { + if (!props.instance.client) { + setIsLoadingMetadata(false) + return + } + + setIsLoadingMetadata(true) + try { + const [projectResult, mcpResult] = await Promise.allSettled([ + props.instance.client.project.current(), + props.instance.client.mcp.status(), + ]) + + const project = projectResult.status === "fulfilled" ? projectResult.value.data : undefined + const mcpStatus = mcpResult.status === "fulfilled" ? mcpResult.value.data : undefined + + const { updateInstance } = await import("../stores/instances") + updateInstance(props.instance.id, { + metadata: { + project, + mcpStatus, + version: "0.15.8", + }, + }) + } catch (error) { + console.error("Failed to load instance metadata:", error) + } finally { + setIsLoadingMetadata(false) + } + }) + + return ( +
+
+

Instance Information

+
+
+
+
Folder
+
+ {props.instance.folder} +
+
+ + + {(project) => ( + <> +
+
+ Project +
+
+ {project().id} +
+
+ + +
+
+ Version Control +
+
+ + + + {project().vcs} +
+
+
+ + )} +
+ + +
+
+ OpenCode Version +
+
+ v{metadata()?.version} +
+
+
+ + +
+
+ Binary Path +
+
+ {props.instance.binaryPath} +
+
+
+ + 0}> +
+
+ MCP Servers +
+
+ + {(server) => ( +
+ {server.name} +
+ } + > +
+ + } + > +
+ +
+
+ )} + +
+
+ + + +
+
+ + + + + Loading... +
+
+
+ +
+
Server
+
+
+ Port: + {props.instance.port} +
+
+ PID: + {props.instance.pid} +
+
+ Status: + +
+ {props.instance.status} + +
+
+
+
+
+ ) +} + +export default InstanceInfo diff --git a/src/components/instance-welcome-view.tsx b/src/components/instance-welcome-view.tsx index a2128a12..87ce5671 100644 --- a/src/components/instance-welcome-view.tsx +++ b/src/components/instance-welcome-view.tsx @@ -1,6 +1,7 @@ import { Component, createSignal, Show, For, createEffect, onMount, onCleanup } from "solid-js" import type { Instance } from "../types/instance" import { getParentSessions, createSession, setActiveParentSession, agents } from "../stores/sessions" +import InstanceInfo from "./instance-info" interface InstanceWelcomeViewProps { instance: Instance @@ -9,13 +10,11 @@ interface InstanceWelcomeViewProps { const InstanceWelcomeView: Component = (props) => { const [selectedAgent, setSelectedAgent] = createSignal("") const [isCreating, setIsCreating] = createSignal(false) - const [isLoadingMetadata, setIsLoadingMetadata] = createSignal(true) const [selectedIndex, setSelectedIndex] = createSignal(0) const [focusMode, setFocusMode] = createSignal<"sessions" | "new-session" | null>("sessions") const parentSessions = () => getParentSessions(props.instance.id) const agentList = () => agents().get(props.instance.id) || [] - const metadata = () => props.instance.metadata createEffect(() => { const list = agentList() @@ -35,38 +34,6 @@ const InstanceWelcomeView: Component = (props) => { } }) - onMount(async () => { - await loadInstanceMetadata() - }) - - async function loadInstanceMetadata() { - if (!props.instance.client) return - - setIsLoadingMetadata(true) - try { - const [projectResult, mcpResult] = await Promise.allSettled([ - props.instance.client.project.current(), - props.instance.client.mcp.status(), - ]) - - const project = projectResult.status === "fulfilled" ? projectResult.value.data : undefined - const mcpStatus = mcpResult.status === "fulfilled" ? mcpResult.value.data : undefined - - const { updateInstance } = await import("../stores/instances") - updateInstance(props.instance.id, { - metadata: { - project, - mcpStatus, - version: "0.15.8", - }, - }) - } catch (error) { - console.error("Failed to load instance metadata:", error) - } finally { - setIsLoadingMetadata(false) - } - } - function scrollToIndex(index: number) { const element = document.querySelector(`[data-session-index="${index}"]`) if (element) { @@ -178,39 +145,6 @@ const InstanceWelcomeView: Component = (props) => { } } - function parseMcpStatus(status: unknown): Array<{ name: string; status: "running" | "stopped" | "error" }> { - if (!status || typeof status !== "object") return [] - - try { - const obj = status as Record - return Object.entries(obj).map(([name, statusValue]) => { - let mappedStatus: "running" | "stopped" | "error" - - if (statusValue === "connected") { - mappedStatus = "running" - } else if (statusValue === "disabled") { - mappedStatus = "stopped" - } else if (statusValue === "failed") { - mappedStatus = "error" - } else { - mappedStatus = "stopped" - } - - return { - name, - status: mappedStatus, - } - }) - } catch { - return [] - } - } - - const mcpServers = () => { - const status = metadata()?.mcpStatus - return parseMcpStatus(status) - } - return (
@@ -354,146 +288,8 @@ const InstanceWelcomeView: Component = (props) => {
-
-
-

Instance Information

-
-
-
-
Folder
-
- {props.instance.folder} -
-
- - - {(project) => ( - <> -
-
Project
-
- {project().id} -
-
- - -
-
- Version Control -
-
- - - - {project().vcs} -
-
-
- - )} -
- - -
-
OpenCode Version
-
- v{metadata()?.version} -
-
-
- - -
-
Binary Path
-
- {props.instance.binaryPath} -
-
-
- - 0}> -
-
MCP Servers
-
- - {(server) => ( -
- {server.name} -
- } - > -
- - } - > -
- -
-
- )} - -
-
- - - -
-
- - - - - Loading... -
-
-
- -
-
Server
-
-
- Port: - {props.instance.port} -
-
- PID: - {props.instance.pid} -
-
- Status: - -
- {props.instance.status} - -
-
-
-
+
+
diff --git a/src/components/session-tab.tsx b/src/components/session-tab.tsx index 7239392f..42fdb0cd 100644 --- a/src/components/session-tab.tsx +++ b/src/components/session-tab.tsx @@ -1,10 +1,10 @@ import { Component, Show } from "solid-js" import type { Session } from "../types/session" -import { MessageSquare, Terminal, X } from "lucide-solid" +import { MessageSquare, Info, X } from "lucide-solid" interface SessionTabProps { session?: Session - special?: "logs" + special?: "info" active: boolean isParent?: boolean onSelect: () => void @@ -13,7 +13,7 @@ interface SessionTabProps { const SessionTab: Component = (props) => { const label = () => { - if (props.special === "logs") return "Logs" + if (props.special === "info") return "Info" return props.session?.title || "Untitled" } @@ -22,16 +22,16 @@ const SessionTab: Component = (props) => {