From b94a3cfa6cfdef216ad28dd847c53c40a4438ce8 Mon Sep 17 00:00:00 2001 From: Ariane Emory Date: Wed, 17 Dec 2025 21:44:29 -0500 Subject: [PATCH] Persist sidebar section expansion states - Add persistence for MCP, LSP, Todo, and Modified Files sidebar sections - Each section's expanded/collapsed state is saved individually to kv.json - States are automatically restored when OpenCode restarts - Fix KV context bug that was destroying existing values on write - All sections default to expanded to maintain current behavior Changes: - cli/cmd/tui/routes/session/sidebar.tsx: Add KV-backed state management - cli/cmd/tui/context/kv.tsx: Fix serialization to preserve all values --- .../opencode/src/cli/cmd/tui/context/kv.tsx | 5 ++- .../cli/cmd/tui/routes/session/sidebar.tsx | 35 +++++++++++++------ 2 files changed, 29 insertions(+), 11 deletions(-) diff --git a/packages/opencode/src/cli/cmd/tui/context/kv.tsx b/packages/opencode/src/cli/cmd/tui/context/kv.tsx index 24a9a5544e1..403f09beb29 100644 --- a/packages/opencode/src/cli/cmd/tui/context/kv.tsx +++ b/packages/opencode/src/cli/cmd/tui/context/kv.tsx @@ -10,10 +10,12 @@ export const { use: useKV, provider: KVProvider } = createSimpleContext({ const [ready, setReady] = createSignal(false) const [kvStore, setKvStore] = createStore>() const file = Bun.file(path.join(Global.Path.state, "kv.json")) + let rawData: Record = {} file .json() .then((x) => { + rawData = x setKvStore(x) }) .catch(() => {}) @@ -41,7 +43,8 @@ export const { use: useKV, provider: KVProvider } = createSimpleContext({ }, set(key: string, value: any) { setKvStore(key, value) - Bun.write(file, JSON.stringify(kvStore, null, 2)) + rawData[key] = value + Bun.write(file, JSON.stringify(rawData, null, 2)) }, } return result diff --git a/packages/opencode/src/cli/cmd/tui/routes/session/sidebar.tsx b/packages/opencode/src/cli/cmd/tui/routes/session/sidebar.tsx index b64a18ae25d..7389fd1a63f 100644 --- a/packages/opencode/src/cli/cmd/tui/routes/session/sidebar.tsx +++ b/packages/opencode/src/cli/cmd/tui/routes/session/sidebar.tsx @@ -1,19 +1,19 @@ import { useSync } from "@tui/context/sync" -import { createMemo, For, Show, Switch, Match } from "solid-js" +import { createEffect, createMemo, For, Show, Switch, Match } from "solid-js" import { createStore } from "solid-js/store" import { useTheme } from "../../context/theme" import { Locale } from "@/util/locale" import path from "path" import type { AssistantMessage } from "@opencode-ai/sdk/v2" -import { Global } from "@/global" import { Installation } from "@/installation" -import { useKeybind } from "../../context/keybind" import { useDirectory } from "../../context/directory" import { useKV } from "../../context/kv" export function Sidebar(props: { sessionID: string }) { const sync = useSync() const { theme } = useTheme() + const directory = useDirectory() + const kv = useKV() const session = createMemo(() => sync.session.get(props.sessionID)!) const diff = createMemo(() => sync.data.session_diff[props.sessionID] ?? []) const todo = createMemo(() => sync.data.todo[props.sessionID] ?? []) @@ -26,6 +26,24 @@ export function Sidebar(props: { sessionID: string }) { lsp: true, }) + // Load saved sidebar expansion states from KV store when ready + createEffect(() => { + if (kv.ready) { + setExpanded({ + mcp: kv.get("sidebar_expanded_mcp", true), + diff: kv.get("sidebar_expanded_diff", true), + todo: kv.get("sidebar_expanded_todo", true), + lsp: kv.get("sidebar_expanded_lsp", true), + }) + } + }) + + // Wrapper that persists expansion state to KV store + const setExpandedWithPersist = (key: "mcp" | "diff" | "todo" | "lsp", value: boolean) => { + setExpanded(key, value) + kv.set(`sidebar_expanded_${key}`, value) + } + // Sort MCP servers alphabetically for consistent display order const mcpEntries = createMemo(() => Object.entries(sync.data.mcp).sort(([a], [b]) => a.localeCompare(b))) @@ -49,9 +67,6 @@ export function Sidebar(props: { sessionID: string }) { } }) - const directory = useDirectory() - const kv = useKV() - const hasProviders = createMemo(() => sync.data.provider.some((x) => x.id !== "opencode" || Object.values(x.models).some((y) => y.cost?.input !== 0)), ) @@ -90,7 +105,7 @@ export function Sidebar(props: { sessionID: string }) { mcpEntries().length > 2 && setExpanded("mcp", !expanded.mcp)} + onMouseDown={() => mcpEntries().length > 2 && setExpandedWithPersist("mcp", !expanded.mcp)} > 2}> {expanded.mcp ? "▼" : "▶"} @@ -143,7 +158,7 @@ export function Sidebar(props: { sessionID: string }) { sync.data.lsp.length > 2 && setExpanded("lsp", !expanded.lsp)} + onMouseDown={() => sync.data.lsp.length > 2 && setExpandedWithPersist("lsp", !expanded.lsp)} > 2}> {expanded.lsp ? "▼" : "▶"} @@ -183,7 +198,7 @@ export function Sidebar(props: { sessionID: string }) { todo().length > 2 && setExpanded("todo", !expanded.todo)} + onMouseDown={() => todo().length > 2 && setExpandedWithPersist("todo", !expanded.todo)} > 2}> {expanded.todo ? "▼" : "▶"} @@ -208,7 +223,7 @@ export function Sidebar(props: { sessionID: string }) { diff().length > 2 && setExpanded("diff", !expanded.diff)} + onMouseDown={() => diff().length > 2 && setExpandedWithPersist("diff", !expanded.diff)} > 2}> {expanded.diff ? "▼" : "▶"}