Prevent streaming updates from re-rendering session UI
This commit is contained in:
@@ -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<InfoViewProps> = (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) {
|
||||
|
||||
@@ -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<LogsViewProps> = (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) {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<string, Session>
|
||||
@@ -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<SessionListProps> = (props) => {
|
||||
const [sidebarWidth, setSidebarWidth] = createSignal(DEFAULT_WIDTH)
|
||||
const [isResizing, setIsResizing] = createSignal(false)
|
||||
@@ -151,44 +159,38 @@ const SessionList: Component<SessionListProps> = (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 (
|
||||
<div
|
||||
@@ -221,30 +223,50 @@ const SessionList: Component<SessionListProps> = (props) => {
|
||||
<div class="session-section-header px-3 py-2 text-xs font-semibold text-primary/70 uppercase tracking-wide">
|
||||
User Session & Info
|
||||
</div>
|
||||
<For each={sessionSections().parentItems}>
|
||||
{(item) => (
|
||||
<div class="session-list-item group">
|
||||
<button
|
||||
class={`session-item-base ${
|
||||
item.isActive ? "session-item-active" : "session-item-inactive"
|
||||
} ${item.isSpecial ? "session-item-special" : ""}`}
|
||||
onClick={item.onSelect}
|
||||
title={item.title}
|
||||
role="button"
|
||||
aria-selected={item.isActive}
|
||||
>
|
||||
<Show when={item.isSpecial} fallback={<MessageSquare class="w-4 h-4 flex-shrink-0" />}>
|
||||
<Info class="w-4 h-4 flex-shrink-0" />
|
||||
</Show>
|
||||
<For each={parentSessionIds()}>
|
||||
{(id) => {
|
||||
if (id === "info") {
|
||||
const isActive = () => props.activeSessionId === "info"
|
||||
return (
|
||||
<div class="session-list-item group">
|
||||
<button
|
||||
class={`session-item-base ${isActive() ? "session-item-active" : "session-item-inactive"} session-item-special`}
|
||||
onClick={() => props.onSelect("info")}
|
||||
title="Info"
|
||||
role="button"
|
||||
aria-selected={isActive()}
|
||||
>
|
||||
<Info class="w-4 h-4 flex-shrink-0" />
|
||||
<span class="session-item-title truncate">Info</span>
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
<span class="session-item-title truncate">{item.title}</span>
|
||||
const session = () => props.sessions.get(id)
|
||||
if (!session()) {
|
||||
return null
|
||||
}
|
||||
|
||||
<Show when={!item.isSpecial && item.onClose}>
|
||||
const isActive = () => props.activeSessionId === id
|
||||
const title = () => session()?.title || "Untitled"
|
||||
|
||||
return (
|
||||
<div class="session-list-item group">
|
||||
<button
|
||||
class={`session-item-base ${isActive() ? "session-item-active" : "session-item-inactive"}`}
|
||||
onClick={() => props.onSelect(id)}
|
||||
title={title()}
|
||||
role="button"
|
||||
aria-selected={isActive()}
|
||||
>
|
||||
<MessageSquare class="w-4 h-4 flex-shrink-0" />
|
||||
<span class="session-item-title truncate">{title()}</span>
|
||||
<span
|
||||
class="session-item-close opacity-0 group-hover:opacity-100 hover:bg-status-error hover:text-white rounded p-0.5 transition-all"
|
||||
onClick={(event) => {
|
||||
event.stopPropagation()
|
||||
item.onClose?.()
|
||||
props.onClose(id)
|
||||
}}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
@@ -252,51 +274,43 @@ const SessionList: Component<SessionListProps> = (props) => {
|
||||
>
|
||||
<X class="w-3 h-3" />
|
||||
</span>
|
||||
</Show>
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
}}
|
||||
</For>
|
||||
</div>
|
||||
|
||||
<Show when={sessionSections().childItems.length > 0}>
|
||||
<Show when={childSessionIds().length > 0}>
|
||||
<div class="session-section">
|
||||
<div class="session-section-header px-3 py-2 text-xs font-semibold text-primary/70 uppercase tracking-wide">
|
||||
Agent Sessions
|
||||
</div>
|
||||
<For each={sessionSections().childItems}>
|
||||
{(item) => (
|
||||
<div class="session-list-item group">
|
||||
<button
|
||||
class={`session-item-base ${
|
||||
item.isActive ? "session-item-active" : "session-item-inactive"
|
||||
} ${item.isSpecial ? "session-item-special" : ""}`}
|
||||
onClick={item.onSelect}
|
||||
title={item.title}
|
||||
role="button"
|
||||
aria-selected={item.isActive}
|
||||
>
|
||||
<MessageSquare class="w-4 h-4 flex-shrink-0" />
|
||||
<For each={childSessionIds()}>
|
||||
{(id) => {
|
||||
const session = () => props.sessions.get(id)
|
||||
if (!session()) {
|
||||
return null
|
||||
}
|
||||
|
||||
<span class="session-item-title truncate">{item.title}</span>
|
||||
const isActive = () => props.activeSessionId === id
|
||||
const title = () => session()?.title || "Untitled"
|
||||
|
||||
<Show when={!item.isSpecial && item.onClose}>
|
||||
<span
|
||||
class="session-item-close opacity-0 group-hover:opacity-100 hover:bg-status-error hover:text-white rounded p-0.5 transition-all"
|
||||
onClick={(event) => {
|
||||
event.stopPropagation()
|
||||
item.onClose?.()
|
||||
}}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
aria-label="Close session"
|
||||
>
|
||||
<X class="w-3 h-3" />
|
||||
</span>
|
||||
</Show>
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
return (
|
||||
<div class="session-list-item group">
|
||||
<button
|
||||
class={`session-item-base ${isActive() ? "session-item-active" : "session-item-inactive"}`}
|
||||
onClick={() => props.onSelect(id)}
|
||||
title={title()}
|
||||
role="button"
|
||||
aria-selected={isActive()}
|
||||
>
|
||||
<MessageSquare class="w-4 h-4 flex-shrink-0" />
|
||||
<span class="session-item-title truncate">{title()}</span>
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
}}
|
||||
</For>
|
||||
</div>
|
||||
</Show>
|
||||
|
||||
Reference in New Issue
Block a user