diff --git a/src/components/info-view.tsx b/src/components/info-view.tsx index 57a55af4..41555f8d 100644 --- a/src/components/info-view.tsx +++ b/src/components/info-view.tsx @@ -1,7 +1,6 @@ -import { Component, For, createSignal, createEffect, Show, onMount, onCleanup } from "solid-js" -import { instances } from "../stores/instances" +import { Component, For, createSignal, createEffect, Show, onMount, onCleanup, createMemo } from "solid-js" +import { instances, getInstanceLogs } from "../stores/instances" import { ChevronDown } from "lucide-solid" -import type { LogEntry } from "../types/instance" import InstanceInfo from "./instance-info" interface InfoViewProps { @@ -16,7 +15,7 @@ const InfoView: Component = (props) => { const [autoScroll, setAutoScroll] = createSignal(savedState?.autoScroll ?? false) const instance = () => instances().get(props.instanceId) - const logs = () => instance()?.logs ?? [] + const logs = createMemo(() => getInstanceLogs(props.instanceId)) onMount(() => { if (scrollRef && savedState) { diff --git a/src/components/logs-view.tsx b/src/components/logs-view.tsx index 86412ffa..16802e30 100644 --- a/src/components/logs-view.tsx +++ b/src/components/logs-view.tsx @@ -1,7 +1,6 @@ -import { Component, For, createSignal, createEffect, Show, onMount, onCleanup } from "solid-js" -import { instances } from "../stores/instances" -import { Trash2, ChevronDown } from "lucide-solid" -import type { LogEntry } from "../types/instance" +import { Component, For, createSignal, createEffect, Show, onMount, onCleanup, createMemo } from "solid-js" +import { instances, getInstanceLogs } from "../stores/instances" +import { ChevronDown } from "lucide-solid" interface LogsViewProps { instanceId: string @@ -15,7 +14,7 @@ const LogsView: Component = (props) => { const [autoScroll, setAutoScroll] = createSignal(savedState?.autoScroll ?? false) const instance = () => instances().get(props.instanceId) - const logs = () => instance()?.logs ?? [] + const logs = createMemo(() => getInstanceLogs(props.instanceId)) onMount(() => { if (scrollRef && savedState) { diff --git a/src/components/message-stream.tsx b/src/components/message-stream.tsx index a481ab9f..f635a76a 100644 --- a/src/components/message-stream.tsx +++ b/src/components/message-stream.tsx @@ -153,6 +153,7 @@ interface MessageCacheEntry { interface ToolCacheEntry { toolPart: any messageInfo?: any + signature: string item: ToolDisplayItem } @@ -170,6 +171,13 @@ export default function MessageStream(props: MessageStreamProps) { const scrollStateKey = () => makeScrollKey(props.instanceId, props.sessionId) const connectionStatus = () => sseManager.getStatus(props.instanceId) + function createToolSignature(message: Message, toolPart: any, toolIndex: number, messageInfo?: any): string { + const messageId = message.id + const partId = typeof toolPart?.id === "string" ? toolPart.id : `${messageId}-tool-${toolIndex}` + const status = toolPart?.state?.status ?? messageInfo?.state?.status ?? "" + const version = message.version ?? 0 + return `${messageId}:${partId}:${status}:${version}` + } const sessionInfo = createMemo(() => { return ( @@ -344,12 +352,14 @@ export default function MessageStream(props: MessageStreamProps) { const toolPart = displayParts.tool[toolIndex] const toolKey = typeof toolPart?.id === "string" ? toolPart.id : `${message.id}-tool-${toolIndex}` + const toolSignature = createToolSignature(message, toolPart, toolIndex, messageInfo) const toolEntry = toolItemCache.get(toolKey) - if (toolEntry && toolEntry.toolPart === toolPart && toolEntry.messageInfo === messageInfo) { - toolEntry.item.toolPart = toolPart - toolEntry.item.messageInfo = messageInfo + if (toolEntry && toolEntry.signature === toolSignature) { toolEntry.toolPart = toolPart toolEntry.messageInfo = messageInfo + toolEntry.signature = toolSignature + toolEntry.item.toolPart = toolPart + toolEntry.item.messageInfo = messageInfo newToolCache.set(toolKey, toolEntry) items.push(toolEntry.item) } else { @@ -359,7 +369,7 @@ export default function MessageStream(props: MessageStreamProps) { toolPart, messageInfo, } - newToolCache.set(toolKey, { toolPart, messageInfo, item: toolItem }) + newToolCache.set(toolKey, { toolPart, messageInfo, signature: toolSignature, item: toolItem }) items.push(toolItem) } } diff --git a/src/components/session-list.tsx b/src/components/session-list.tsx index 59cd6cd2..1327e6b9 100644 --- a/src/components/session-list.tsx +++ b/src/components/session-list.tsx @@ -4,16 +4,6 @@ import { MessageSquare, Info, Plus, X } from "lucide-solid" import KeyboardHint from "./keyboard-hint" import { keyboardRegistry } from "../lib/keyboard-registry" -interface SessionListItem { - id: string - title: string - isSpecial?: boolean - isActive: boolean - isParent?: boolean - onSelect: () => void - onClose?: () => void -} - interface SessionListProps { instanceId: string sessions: Map @@ -33,6 +23,24 @@ const MAX_WIDTH = 500 const DEFAULT_WIDTH = 280 const STORAGE_KEY = "opencode-session-sidebar-width" +function arraysEqual(prev: readonly string[] | undefined, next: readonly string[]): boolean { + if (!prev) { + return false + } + + if (prev.length !== next.length) { + return false + } + + for (let i = 0; i < prev.length; i++) { + if (prev[i] !== next[i]) { + return false + } + } + + return true +} + const SessionList: Component = (props) => { const [sidebarWidth, setSidebarWidth] = createSignal(DEFAULT_WIDTH) const [isResizing, setIsResizing] = createSignal(false) @@ -151,44 +159,38 @@ const SessionList: Component = (props) => { removeTouchListeners() }) - const sessionSections = createMemo(() => { - const parentItems: SessionListItem[] = [] - const childItems: SessionListItem[] = [] - - for (const [id, session] of props.sessions.entries()) { - const item: SessionListItem = { - id, - title: session.title || "Untitled", - isActive: id === props.activeSessionId, - isParent: session.parentId === null, - onSelect: () => props.onSelect(id), - onClose: session.parentId === null ? () => props.onClose(id) : undefined, + const parentSessionIds = createMemo( + () => { + const ids: string[] = [] + for (const session of props.sessions.values()) { + if (session.parentId === null) { + ids.push(session.id) + } } + ids.push("info") + return ids + }, + undefined, + { equals: arraysEqual }, + ) - if (session.parentId === null) { - parentItems.push(item) - } else { - childItems.push(item) + const childSessionIds = createMemo( + () => { + const children: { id: string; updated: number }[] = [] + for (const session of props.sessions.values()) { + if (session.parentId !== null) { + children.push({ id: session.id, updated: session.time.updated ?? 0 }) + } } - } - - childItems.sort((a, b) => { - const sessionA = props.sessions.get(a.id) - const sessionB = props.sessions.get(b.id) - if (!sessionA || !sessionB) return 0 - return sessionB.time.updated - sessionA.time.updated - }) - - parentItems.push({ - id: "info", - title: "Info", - isSpecial: true, - isActive: props.activeSessionId === "info", - onSelect: () => props.onSelect("info"), - }) - - return { parentItems, childItems } - }) + if (children.length <= 1) { + return children.map((entry) => entry.id) + } + children.sort((a, b) => b.updated - a.updated) + return children.map((entry) => entry.id) + }, + undefined, + { equals: arraysEqual }, + ) return (
= (props) => {
User Session & Info
- - {(item) => ( -
- +
+ ) + } - {item.title} + const session = () => props.sessions.get(id) + if (!session()) { + return null + } - + const isActive = () => props.activeSessionId === id + const title = () => session()?.title || "Untitled" + + return ( +
+ -
- )} + +
+ ) + }} - 0}> + 0}>
Agent Sessions
- - {(item) => ( -
- -
- )} + return ( +
+ +
+ ) + }}
diff --git a/src/stores/instances.ts b/src/stores/instances.ts index 3f8b69ec..3ac732b8 100644 --- a/src/stores/instances.ts +++ b/src/stores/instances.ts @@ -7,15 +7,43 @@ import { preferences, updateLastUsedBinary } from "./preferences" const [instances, setInstances] = createSignal>(new Map()) const [activeInstanceId, setActiveInstanceId] = createSignal(null) +const [instanceLogs, setInstanceLogs] = createSignal>(new Map()) const MAX_LOG_ENTRIES = 1000 +function ensureLogContainer(id: string) { + setInstanceLogs((prev) => { + if (prev.has(id)) { + return prev + } + const next = new Map(prev) + next.set(id, []) + return next + }) +} + +function removeLogContainer(id: string) { + setInstanceLogs((prev) => { + if (!prev.has(id)) { + return prev + } + const next = new Map(prev) + next.delete(id) + return next + }) +} + +function getInstanceLogs(instanceId: string): LogEntry[] { + return instanceLogs().get(instanceId) ?? [] +} + function addInstance(instance: Instance) { setInstances((prev) => { const next = new Map(prev) next.set(instance.id, instance) return next }) + ensureLogContainer(instance.id) } function updateInstance(id: string, updates: Partial) { @@ -35,6 +63,7 @@ function removeInstance(id: string) { next.delete(id) return next }) + removeLogContainer(id) if (activeInstanceId() === id) { setActiveInstanceId(null) @@ -54,7 +83,6 @@ async function createInstance(folder: string, binaryPath?: string): Promise { + setInstanceLogs((prev) => { const next = new Map(prev) - const instance = next.get(id) - if (instance) { - const logs = [...instance.logs, entry] - if (logs.length > MAX_LOG_ENTRIES) { - logs.shift() - } - next.set(id, { ...instance, logs }) - } + const existing = next.get(id) ?? [] + const updated = existing.length >= MAX_LOG_ENTRIES ? [...existing.slice(1), entry] : [...existing, entry] + next.set(id, updated) return next }) } function clearLogs(id: string) { - setInstances((prev) => { - const next = new Map(prev) - const instance = next.get(id) - if (instance) { - next.set(id, { ...instance, logs: [] }) + setInstanceLogs((prev) => { + if (!prev.has(id)) { + return prev } + const next = new Map(prev) + next.set(id, []) return next }) } @@ -164,4 +187,6 @@ export { getActiveInstance, addLog, clearLogs, + instanceLogs, + getInstanceLogs, } diff --git a/src/types/instance.ts b/src/types/instance.ts index 3bb21457..74adc998 100644 --- a/src/types/instance.ts +++ b/src/types/instance.ts @@ -35,7 +35,6 @@ export interface Instance { status: "starting" | "ready" | "error" | "stopped" error?: string client: OpencodeClient | null - logs: LogEntry[] metadata?: InstanceMetadata binaryPath?: string environmentVariables?: Record