import { useSync } from "@tui/context/sync" import { 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 { TodoItem } from "../../component/todo-item" export function Sidebar(props: { sessionID: string; overlay?: boolean }) { const sync = useSync() const { theme } = useTheme() 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] ?? []) const messages = createMemo(() => sync.data.message[props.sessionID] ?? []) const [expanded, setExpanded] = createStore({ mcp: true, diff: true, todo: true, lsp: true, }) // Sort MCP servers alphabetically for consistent display order const mcpEntries = createMemo(() => Object.entries(sync.data.mcp).sort(([a], [b]) => a.localeCompare(b))) // Count connected and error MCP servers for collapsed header display const connectedMcpCount = createMemo(() => mcpEntries().filter(([_, item]) => item.status === "connected").length) const errorMcpCount = createMemo( () => mcpEntries().filter( ([_, item]) => item.status === "failed" || item.status === "needs_auth" || item.status === "needs_client_registration", ).length, ) const cost = createMemo(() => { const total = messages().reduce((sum, x) => sum + (x.role === "assistant" ? x.cost : 0), 0) return new Intl.NumberFormat("en-US", { style: "currency", currency: "USD", }).format(total) }) const context = createMemo(() => { const last = messages().findLast((x) => x.role === "assistant" && x.tokens.output > 0) as AssistantMessage if (!last) return const total = last.tokens.input + last.tokens.output + last.tokens.reasoning + last.tokens.cache.read + last.tokens.cache.write const model = sync.data.provider.find((x) => x.id === last.providerID)?.models[last.modelID] return { tokens: total.toLocaleString(), percentage: model?.limit.context ? Math.round((total / model.limit.context) * 100) : null, } }) const directory = useDirectory() return ( {session().title} {session().share!.url} Context {context()?.tokens ?? 0} tokens {context()?.percentage ?? 0}% used {cost()} spent 0}> mcpEntries().length > 2 && setExpanded("mcp", !expanded.mcp)} > 2}> {expanded.mcp ? "▼" : "▶"} MCP {" "} ({connectedMcpCount()} active {errorMcpCount() > 0 ? `, ${errorMcpCount()} error${errorMcpCount() > 1 ? "s" : ""}` : ""}) {([key, item]) => ( )[item.status], }} > • {key}{" "} Connected {(val) => {val().error}} Disabled Needs auth Needs client ID )} sync.data.lsp.length > 2 && setExpanded("lsp", !expanded.lsp)} > 2}> {expanded.lsp ? "▼" : "▶"} LSP {sync.data.config.lsp === false ? "LSPs have been disabled in settings" : "LSPs will activate as files are read"} {(item) => ( {item.id} {item.root} )} 0 && todo().some((t) => t.status !== "completed")}> todo().length > 2 && setExpanded("todo", !expanded.todo)} > 2}> {expanded.todo ? "▼" : "▶"} Todo {(todo) => } 0}> diff().length > 2 && setExpanded("diff", !expanded.diff)} > 2}> {expanded.diff ? "▼" : "▶"} Modified Files {(item) => { const file = createMemo(() => { const splits = item.file.split(path.sep).filter(Boolean) const last = splits.at(-1)! const rest = splits.slice(0, -1).join(path.sep) if (!rest) return last return Locale.truncateMiddle(rest, 30 - last.length) + "/" + last }) return ( {file()} +{item.additions} -{item.deletions} ) }} {directory().split("/").slice(0, -1).join("/")}/ {directory().split("/").at(-1)} {" "} Tinfoil {" "} {Installation.VERSION} ) }