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}
+
+
}
+ >
+
+
+ }
+ >
+
+
+
+
+ )}
+
+
+
+
+
+
+
+
+
+
+
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}
-
-
}
- >
-
-
- }
- >
-
-
-
-
- )}
-
-
-
-
-
-
-
-
-
-
-
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) => {