diff --git a/packages/opencode/src/cli/cmd/tui/feature-plugins/sidebar/context-viewer.tsx b/packages/opencode/src/cli/cmd/tui/feature-plugins/sidebar/context-viewer.tsx
new file mode 100644
index 0000000000..d2c5120e60
--- /dev/null
+++ b/packages/opencode/src/cli/cmd/tui/feature-plugins/sidebar/context-viewer.tsx
@@ -0,0 +1,200 @@
+import type { TuiPluginApi } from "@opencode-ai/plugin/tui"
+import type { Part as SdkPart } from "@opencode-ai/sdk/v2"
+import { createMemo, createSignal } from "solid-js"
+import { DialogSelect, type DialogSelectOption } from "@tui/ui/dialog-select"
+import { useDialog } from "@tui/ui/dialog"
+import { Keybind } from "@/util/keybind"
+
+type Value = {
+ sessionID: string
+ messageID: string
+ partID: string
+ kind: string
+}
+
+const money = new Intl.NumberFormat("en-US", {
+ style: "currency",
+ currency: "USD",
+})
+
+function estimate(part: SdkPart): number {
+ switch (part.type) {
+ case "text":
+ return Math.ceil((part.text?.length ?? 0) / 4)
+ case "tool": {
+ if (part.state.status !== "completed") return 0
+ const raw = JSON.stringify(part.state.input ?? "").length + (part.state.output ?? "").length
+ return Math.ceil(raw / 4)
+ }
+ case "reasoning":
+ return Math.ceil((part.text?.length ?? 0) / 4)
+ case "subtask":
+ return Math.ceil((part.prompt?.length ?? 0) / 4)
+ default:
+ return 0
+ }
+}
+
+function label(part: SdkPart): string {
+ switch (part.type) {
+ case "text":
+ return (part.text ?? "").slice(0, 60).replace(/\n/g, " ") || "(empty)"
+ case "tool": {
+ const icon =
+ part.state.status === "completed"
+ ? "✓"
+ : part.state.status === "error"
+ ? "✗"
+ : part.state.status === "running"
+ ? "…"
+ : "○"
+ const flag = part.state.status === "completed" && part.state.time?.compacted ? " [compacted]" : ""
+ return `[${part.tool}] ${icon}${flag}`
+ }
+ case "reasoning":
+ return "[reasoning] " + (part.text ?? "").slice(0, 40).replace(/\n/g, " ")
+ case "subtask":
+ return `[subtask] ${part.description ?? part.prompt?.slice(0, 40) ?? ""}`
+ case "file":
+ return `[file] ${part.filename ?? part.url ?? ""}`
+ case "step-start":
+ return "[step-start]"
+ case "step-finish":
+ return `[step-finish] ${part.reason ?? ""}`
+ case "patch":
+ return `[patch] ${part.files?.length ?? 0} files`
+ case "agent":
+ return `[@${part.name}]`
+ case "retry":
+ return `[retry] #${part.attempt}`
+ case "compaction":
+ return "[compaction]"
+ case "snapshot":
+ return "[snapshot]"
+ default:
+ return `[unknown]`
+ }
+}
+
+function desc(part: SdkPart): string | undefined {
+ if (part.type === "tool" && part.state.status === "completed") return part.state.title ?? undefined
+ return undefined
+}
+
+export function show(api: TuiPluginApi, sessionID: string) {
+ api.ui.dialog.setSize("xlarge")
+ api.ui.dialog.replace(() => )
+}
+
+function Viewer(props: { api: TuiPluginApi; sessionID: string }) {
+ const dialog = useDialog()
+ const theme = () => props.api.theme.current
+ const [pending, setPending] = createSignal()
+ const msgs = createMemo(() => props.api.state.session.messages(props.sessionID))
+ const cost = createMemo(() => msgs().reduce((sum, m) => sum + (m.role === "assistant" ? m.cost : 0), 0))
+
+ const options = createMemo(() => {
+ const result: DialogSelectOption[] = []
+ const all = msgs()
+ for (let i = 0; i < all.length; i++) {
+ const msg = all[i]
+ const parts = props.api.state.part(msg.id)
+ const role = msg.role === "user" ? "User" : "Assistant"
+ let info = ""
+ if (msg.role === "assistant") {
+ const t = msg.tokens
+ const total = t.input + t.output + t.reasoning + t.cache.read + t.cache.write
+ info = ` · ${total.toLocaleString()} tok`
+ if (msg.cost > 0) info += ` · ${money.format(msg.cost)}`
+ }
+ const category = `#${i + 1} ${role}${info}`
+
+ for (const part of parts) {
+ const tok = estimate(part)
+ const deleting = pending() === part.id
+ result.push({
+ title: deleting ? "Press again to confirm delete" : label(part),
+ value: {
+ sessionID: props.sessionID,
+ messageID: msg.id,
+ partID: part.id,
+ kind: part.type,
+ },
+ description: deleting ? undefined : desc(part),
+ footer: tok > 0 ? `~${tok.toLocaleString()} tok` : undefined,
+ category,
+ bg: deleting ? theme().error : undefined,
+ })
+ }
+
+ if (parts.length === 0) {
+ result.push({
+ title: "(no parts)",
+ value: {
+ sessionID: props.sessionID,
+ messageID: msg.id,
+ partID: "",
+ kind: "empty",
+ },
+ category,
+ disabled: true,
+ })
+ }
+ }
+ return result
+ })
+
+ return (
+ setPending(undefined)}
+ keybind={[
+ {
+ keybind: Keybind.parse("alt+c")[0],
+ title: "compact",
+ onTrigger: async (opt: DialogSelectOption) => {
+ if (opt.value.kind !== "tool") return
+ const parts = props.api.state.part(opt.value.messageID)
+ const part = parts.find((p) => p.id === opt.value.partID)
+ if (!part || part.type !== "tool") return
+ if (part.state.status !== "completed") return
+ if (part.state.time?.compacted) return
+ await props.api.client.part.update({
+ sessionID: opt.value.sessionID,
+ messageID: opt.value.messageID,
+ partID: opt.value.partID,
+ part: {
+ ...part,
+ state: {
+ ...part.state,
+ time: { ...part.state.time, compacted: Date.now() },
+ },
+ },
+ })
+ },
+ },
+ {
+ keybind: Keybind.parse("alt+d")[0],
+ title: "delete",
+ onTrigger: async (opt: DialogSelectOption) => {
+ if (!opt.value.partID) return
+ if (pending() !== opt.value.partID) {
+ setPending(opt.value.partID)
+ return
+ }
+ setPending(undefined)
+ await props.api.client.part.delete({
+ sessionID: opt.value.sessionID,
+ messageID: opt.value.messageID,
+ partID: opt.value.partID,
+ })
+ },
+ },
+ ]}
+ />
+ )
+}
diff --git a/packages/opencode/src/cli/cmd/tui/feature-plugins/sidebar/context.tsx b/packages/opencode/src/cli/cmd/tui/feature-plugins/sidebar/context.tsx
index 9ffe779791..f92ceb45e0 100644
--- a/packages/opencode/src/cli/cmd/tui/feature-plugins/sidebar/context.tsx
+++ b/packages/opencode/src/cli/cmd/tui/feature-plugins/sidebar/context.tsx
@@ -1,6 +1,7 @@
-import type { AssistantMessage } from "@opencode-ai/sdk/v2"
+import type { AssistantMessage, Part } from "@opencode-ai/sdk/v2"
import type { TuiPlugin, TuiPluginApi, TuiPluginModule } from "@opencode-ai/plugin/tui"
import { createMemo } from "solid-js"
+import { show as showViewer } from "./context-viewer"
const id = "internal:sidebar-context"
@@ -9,6 +10,20 @@ const money = new Intl.NumberFormat("en-US", {
currency: "USD",
})
+function counts(parts: readonly Part[]) {
+ let tools = 0
+ let errors = 0
+ let files = 0
+ for (const p of parts) {
+ if (p.type === "tool") {
+ tools++
+ if (p.state.status === "error") errors++
+ if (p.state.status === "completed" && (p.tool === "read" || p.tool === "glob" || p.tool === "grep")) files++
+ }
+ }
+ return { tools, errors, files }
+}
+
function View(props: { api: TuiPluginApi; session_id: string }) {
const theme = () => props.api.theme.current
const msg = createMemo(() => props.api.state.session.messages(props.session_id))
@@ -32,13 +47,32 @@ function View(props: { api: TuiPluginApi; session_id: string }) {
}
})
+ const stats = createMemo(() => {
+ const all = msg()
+ let tools = 0
+ let errors = 0
+ let files = 0
+ for (const m of all) {
+ const c = counts(props.api.state.part(m.id))
+ tools += c.tools
+ errors += c.errors
+ files += c.files
+ }
+ return { messages: all.length, tools, errors, files }
+ })
+
return (
-
+ props.api.command.trigger("session.context_panel.toggle")}>
Context
- {state().tokens.toLocaleString()} tokens
- {state().percent ?? 0}% used
+
+ {state().tokens.toLocaleString()} tokens · {state().percent ?? 0}% used
+
+
+ {stats().messages} msgs · {stats().tools} tools{stats().errors > 0 ? ` · ${stats().errors} err` : ""}
+ {stats().files > 0 ? ` · ${stats().files} reads` : ""}
+
{money.format(cost())} spent
)
@@ -53,6 +87,23 @@ const tui: TuiPlugin = async (api) => {
},
},
})
+
+ api.command.register(() => [
+ {
+ title: "View context",
+ value: "context.viewer",
+ category: "Session",
+ description: "Browse session messages and parts with token estimates",
+ slash: { name: "context" },
+ onSelect() {
+ const route = api.route.current
+ if (route.name !== "session") return
+ const sessionID = (route.params as { sessionID?: string }).sessionID
+ if (!sessionID) return
+ showViewer(api, sessionID)
+ },
+ },
+ ])
}
const plugin: TuiPluginModule & { id: string } = {
diff --git a/packages/opencode/src/cli/cmd/tui/routes/session/context-panel.tsx b/packages/opencode/src/cli/cmd/tui/routes/session/context-panel.tsx
new file mode 100644
index 0000000000..7d1767242c
--- /dev/null
+++ b/packages/opencode/src/cli/cmd/tui/routes/session/context-panel.tsx
@@ -0,0 +1,484 @@
+import { useSync } from "@tui/context/sync"
+import { useTheme, selectedForeground } from "@tui/context/theme"
+import { useKeybind } from "@tui/context/keybind"
+import { useSDK } from "@tui/context/sdk"
+import { useTuiConfig } from "../../context/tui-config"
+import { useToast } from "../../ui/toast"
+import { getScrollAcceleration } from "../../util/scroll"
+import { Keybind } from "@/util/keybind"
+import { Locale } from "@/util/locale"
+import type { Part, AssistantMessage } from "@opencode-ai/sdk/v2"
+import { batch, createEffect, createMemo, createSignal, For, on, Show } from "solid-js"
+import { createStore } from "solid-js/store"
+import { useKeyboard, useTerminalDimensions } from "@opentui/solid"
+import { InputRenderable, RGBA, ScrollBoxRenderable, TextAttributes } from "@opentui/core"
+import * as fuzzysort from "fuzzysort"
+
+const PANEL_WIDTH = 80
+
+const money = new Intl.NumberFormat("en-US", {
+ style: "currency",
+ currency: "USD",
+})
+
+function estimate(part: Part): number {
+ switch (part.type) {
+ case "text":
+ return Math.ceil((part.text?.length ?? 0) / 4)
+ case "tool": {
+ if (part.state.status !== "completed") return 0
+ const raw = JSON.stringify(part.state.input ?? "").length + (part.state.output ?? "").length
+ return Math.ceil(raw / 4)
+ }
+ case "reasoning":
+ return Math.ceil((part.text?.length ?? 0) / 4)
+ case "subtask":
+ return Math.ceil((part.prompt?.length ?? 0) / 4)
+ default:
+ return 0
+ }
+}
+
+type Item = {
+ id: string
+ title: string
+ description?: string
+ footer?: string
+ category: string
+ sessionID: string
+ messageID: string
+ partID: string
+ kind: string
+ disabled?: boolean
+}
+
+function items(
+ sessionID: string,
+ messages: ReturnType["data"]["message"][string],
+ parts: ReturnType["data"]["part"],
+): Item[] {
+ const result: Item[] = []
+ const all = messages ?? []
+ for (let i = 0; i < all.length; i++) {
+ const msg = all[i]
+ const msgParts = parts[msg.id] ?? []
+ const role = msg.role === "user" ? "User" : "Assistant"
+ let info = ""
+ if (msg.role === "assistant") {
+ const t = (msg as AssistantMessage).tokens
+ const total = t.input + t.output + t.reasoning + t.cache.read + t.cache.write
+ info = ` · ${total.toLocaleString()} tok`
+ if ((msg as AssistantMessage).cost > 0) info += ` · ${money.format((msg as AssistantMessage).cost)}`
+ }
+ const category = `#${i + 1} ${role}${info}`
+
+ for (const part of msgParts) {
+ const tok = estimate(part)
+ result.push({
+ id: part.id,
+ title: partLabel(part),
+ description: partDesc(part),
+ footer: tok > 0 ? `~${tok.toLocaleString()} tok` : undefined,
+ category,
+ sessionID,
+ messageID: msg.id,
+ partID: part.id,
+ kind: part.type,
+ })
+ }
+
+ if (msgParts.length === 0) {
+ result.push({
+ id: `empty-${msg.id}`,
+ title: "(no parts)",
+ category,
+ sessionID,
+ messageID: msg.id,
+ partID: "",
+ kind: "empty",
+ disabled: true,
+ })
+ }
+ }
+ return result
+}
+
+function partLabel(part: Part): string {
+ switch (part.type) {
+ case "text":
+ return (part.text ?? "").slice(0, 60).replace(/\n/g, " ") || "(empty)"
+ case "tool": {
+ const icon =
+ part.state.status === "completed"
+ ? "✓"
+ : part.state.status === "error"
+ ? "✗"
+ : part.state.status === "running"
+ ? "…"
+ : "○"
+ const flag = part.state.status === "completed" && part.state.time?.compacted ? " [compacted]" : ""
+ return `[${part.tool}] ${icon}${flag}`
+ }
+ case "reasoning":
+ return "[reasoning] " + (part.text ?? "").slice(0, 40).replace(/\n/g, " ")
+ case "subtask":
+ return `[subtask] ${part.description ?? part.prompt?.slice(0, 40) ?? ""}`
+ case "file":
+ return `[file] ${part.filename ?? part.url ?? ""}`
+ case "step-start":
+ return "[step-start]"
+ case "step-finish":
+ return `[step-finish] ${part.reason ?? ""}`
+ case "patch":
+ return `[patch] ${part.files?.length ?? 0} files`
+ case "agent":
+ return `[@${part.name}]`
+ case "retry":
+ return `[retry] #${part.attempt}`
+ case "compaction":
+ return "[compaction]"
+ case "snapshot":
+ return "[snapshot]"
+ default:
+ return "[unknown]"
+ }
+}
+
+function partDesc(part: Part): string | undefined {
+ if (part.type === "tool" && part.state.status === "completed") return part.state.title ?? undefined
+ return undefined
+}
+
+export { PANEL_WIDTH }
+
+export function ContextPanel(props: { sessionID: string }) {
+ const sync = useSync()
+ const sdk = useSDK()
+ const { theme } = useTheme()
+ const keybind = useKeybind()
+ const tuiConfig = useTuiConfig()
+ const toast = useToast()
+ const dimensions = useTerminalDimensions()
+ const acceleration = createMemo(() => getScrollAcceleration(tuiConfig))
+
+ const msgs = createMemo(() => sync.data.message[props.sessionID] ?? [])
+ const cost = createMemo(() =>
+ msgs().reduce((sum, m) => sum + (m.role === "assistant" ? (m as AssistantMessage).cost : 0), 0),
+ )
+
+ const all = createMemo(() => items(props.sessionID, msgs(), sync.data.part))
+
+ const [store, setStore] = createStore({
+ selected: 0,
+ filter: "",
+ input: "keyboard" as "keyboard" | "mouse",
+ })
+
+ const [pending, setPending] = createSignal()
+
+ const filtered = createMemo(() => {
+ const opts = all().filter((x) => !x.disabled)
+ const needle = store.filter.toLowerCase()
+ if (!needle) return opts
+ return fuzzysort
+ .go(needle, opts, {
+ keys: ["title", "category"],
+ scoreFn: (r) => r[0].score * 2 + r[1].score,
+ })
+ .map((x) => x.obj)
+ })
+
+ const flatten = createMemo(() => store.filter.length > 0)
+
+ const grouped = createMemo<[string, Item[]][]>(() => {
+ if (flatten()) return [["", filtered()]]
+ const map = new Map()
+ for (const item of filtered()) {
+ const key = item.category
+ const arr = map.get(key) ?? []
+ arr.push(item)
+ map.set(key, arr)
+ }
+ return Array.from(map.entries())
+ })
+
+ const flat = createMemo(() => grouped().flatMap(([, opts]) => opts))
+
+ const selected = createMemo(() => flat()[store.selected])
+
+ createEffect(
+ on(
+ () => store.filter,
+ () => {
+ setTimeout(() => moveTo(0, true), 0)
+ },
+ ),
+ )
+
+ createEffect(() => {
+ filtered()
+ setStore("input", "keyboard")
+ })
+
+ function move(dir: number) {
+ if (flat().length === 0) return
+ let next = store.selected + dir
+ if (next < 0) next = flat().length - 1
+ if (next >= flat().length) next = 0
+ moveTo(next, true)
+ }
+
+ function moveTo(next: number, center = false) {
+ setStore("selected", next)
+ if (!scroll) return
+ const target = scroll.getChildren().find((c) => c.id === selected()?.id)
+ if (!target) return
+ const y = target.y - scroll.y
+ if (center) {
+ scroll.scrollBy(y - Math.floor(scroll.height / 2))
+ } else {
+ if (y >= scroll.height) scroll.scrollBy(y - scroll.height + 1)
+ if (y < 0) scroll.scrollBy(y)
+ }
+ }
+
+ async function compact() {
+ const s = selected()
+ if (!s || s.kind !== "tool") return
+ const parts = sync.data.part[s.messageID] ?? []
+ const part = parts.find((p) => p.id === s.partID)
+ if (!part || part.type !== "tool") return
+ if (part.state.status !== "completed") return
+ if (part.state.time?.compacted) return
+ await sdk.client.part.update({
+ sessionID: s.sessionID,
+ messageID: s.messageID,
+ partID: s.partID,
+ part: {
+ ...part,
+ state: {
+ ...part.state,
+ time: { ...part.state.time, compacted: Date.now() },
+ },
+ },
+ })
+ toast.show({ message: "Part compacted", variant: "success" })
+ }
+
+ async function del() {
+ const s = selected()
+ if (!s || !s.partID) return
+ if (pending() !== s.partID) {
+ setPending(s.partID)
+ return
+ }
+ setPending(undefined)
+ await sdk.client.part.delete({
+ sessionID: s.sessionID,
+ messageID: s.messageID,
+ partID: s.partID,
+ })
+ }
+
+ async function delSection() {
+ const s = selected()
+ if (!s) return
+ const key = `section:${s.messageID}`
+ if (pending() !== key) {
+ setPending(key)
+ return
+ }
+ setPending(undefined)
+ await sdk.client.session.deleteMessage({
+ sessionID: s.sessionID,
+ messageID: s.messageID,
+ })
+ toast.show({ message: "Message deleted", variant: "success" })
+ }
+
+ const compactKey = Keybind.parse("alt+c")[0]
+ const deleteKey = Keybind.parse("alt+d")[0]
+ const sectionKey = Keybind.parse("alt+s")[0]
+
+ let input: InputRenderable
+ let scroll: ScrollBoxRenderable
+
+ useKeyboard((evt) => {
+ setStore("input", "keyboard")
+ if (evt.name === "up" || (evt.ctrl && evt.name === "p")) move(-1)
+ if (evt.name === "down" || (evt.ctrl && evt.name === "n")) move(1)
+ if (evt.name === "pageup") move(-10)
+ if (evt.name === "pagedown") move(10)
+ if (evt.name === "home") moveTo(0)
+ if (evt.name === "end") moveTo(flat().length - 1)
+
+ if (compactKey && Keybind.match(compactKey, keybind.parse(evt))) {
+ evt.preventDefault()
+ compact()
+ }
+ if (deleteKey && Keybind.match(deleteKey, keybind.parse(evt))) {
+ evt.preventDefault()
+ del()
+ }
+ if (sectionKey && Keybind.match(sectionKey, keybind.parse(evt))) {
+ evt.preventDefault()
+ delSection()
+ }
+ })
+
+ const fg = selectedForeground(theme)
+ const maxTitle = createMemo(() => PANEL_WIDTH - 22)
+
+ return (
+
+
+
+ Context · {money.format(cost())}
+
+ {keybind.print("context_panel")} close
+
+
+
+ {
+ batch(() => {
+ setStore("filter", e)
+ })
+ }}
+ focusedBackgroundColor={theme.backgroundPanel}
+ cursorColor={theme.primary}
+ focusedTextColor={theme.textMuted}
+ ref={(r) => {
+ input = r
+ setTimeout(() => {
+ if (!input || input.isDestroyed) return
+ input.focus()
+ }, 1)
+ }}
+ placeholder="Filter parts..."
+ placeholderColor={theme.textMuted}
+ />
+
+
+ 0}
+ fallback={
+
+ No results found
+
+ }
+ >
+ (scroll = r)}
+ flexGrow={1}
+ paddingTop={1}
+ scrollAcceleration={acceleration()}
+ verticalScrollbarOptions={{
+ trackOptions: {
+ backgroundColor: theme.background,
+ foregroundColor: theme.borderActive,
+ },
+ }}
+ >
+
+ {([category, opts], index) => (
+ <>
+
+ 0 ? 1 : 0}>
+
+ {pending() === `section:${opts[0]?.messageID}` ? "Press again to delete section" : category}
+
+
+
+
+ {(opt) => {
+ const active = createMemo(() => opt.id === selected()?.id)
+ const deleting = createMemo(() => pending() === opt.partID)
+ return (
+ setStore("input", "mouse")}
+ onMouseOver={() => {
+ if (store.input !== "mouse") return
+ const idx = flat().findIndex((x) => x.id === opt.id)
+ if (idx !== -1) moveTo(idx)
+ }}
+ onMouseDown={() => {
+ const idx = flat().findIndex((x) => x.id === opt.id)
+ if (idx !== -1) moveTo(idx)
+ }}
+ backgroundColor={
+ active() ? (deleting() ? theme.error : theme.primary) : RGBA.fromInts(0, 0, 0, 0)
+ }
+ paddingLeft={1}
+ paddingRight={1}
+ >
+
+ {Locale.truncate(deleting() ? "Press again to confirm delete" : opt.title, maxTitle())}
+
+
+ {" "}
+ {opt.description}
+
+
+
+
+
+ {opt.footer}
+
+
+
+ )
+ }}
+
+ >
+ )}
+
+
+
+
+
+
+
+ compact{" "}
+
+ alt+c
+
+
+
+ delete{" "}
+
+ alt+d
+
+
+
+ section{" "}
+
+ alt+s
+
+
+
+ )
+}
diff --git a/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx b/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx
index 396d756301..6201868230 100644
--- a/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx
+++ b/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx
@@ -61,6 +61,7 @@ import { DialogTimeline } from "./dialog-timeline"
import { DialogForkFromTimeline } from "./dialog-fork-from-timeline"
import { DialogSessionRename } from "../../component/dialog-session-rename"
import { Sidebar } from "./sidebar"
+import { ContextPanel, PANEL_WIDTH as CONTEXT_PANEL_WIDTH } from "./context-panel"
import { SubagentFooter } from "./subagent-footer.tsx"
import { Flag } from "@/flag/flag"
import { LANGUAGE_EXTENSIONS } from "@/lsp/language"
@@ -144,6 +145,7 @@ export function Session() {
const dimensions = useTerminalDimensions()
const [sidebar, setSidebar] = kv.signal<"auto" | "hide">("sidebar", "auto")
const [sidebarOpen, setSidebarOpen] = createSignal(false)
+ const [contextPanel, setContextPanel] = kv.signal<"hide" | "show">("context_panel", "hide")
const [conceal, setConceal] = createSignal(true)
const [showThinking, setShowThinking] = kv.signal("thinking_visibility", true)
const [timestamps, setTimestamps] = kv.signal<"hide" | "show">("timestamps", "hide")
@@ -155,14 +157,24 @@ export function Session() {
const [showGenericToolOutput, setShowGenericToolOutput] = kv.signal("generic_tool_output_visibility", false)
const wide = createMemo(() => dimensions().width > 120)
+ const contextPanelVisible = createMemo(() => {
+ if (session()?.parentID) return false
+ return contextPanel() === "show"
+ })
const sidebarVisible = createMemo(() => {
if (session()?.parentID) return false
+ if (contextPanelVisible()) return false
if (sidebarOpen()) return true
if (sidebar() === "auto" && wide()) return true
return false
})
const showTimestamps = createMemo(() => timestamps() === "show")
- const contentWidth = createMemo(() => dimensions().width - (sidebarVisible() ? 42 : 0) - 4)
+ const rightWidth = createMemo(() => {
+ if (contextPanelVisible()) return CONTEXT_PANEL_WIDTH
+ if (sidebarVisible()) return 42
+ return 0
+ })
+ const contentWidth = createMemo(() => dimensions().width - rightWidth() - 4)
const providers = createMemo(() => Model.index(sync.data.provider))
const scrollAcceleration = createMemo(() => getScrollAcceleration(tuiConfig))
@@ -565,6 +577,20 @@ export function Session() {
dialog.clear()
},
},
+ {
+ title: contextPanelVisible() ? "Hide context panel" : "Show context panel",
+ value: "session.context_panel.toggle",
+ keybind: "context_panel",
+ category: "Session",
+ slash: {
+ name: "context-panel",
+ aliases: ["cp"],
+ },
+ onSelect: (dialog) => {
+ setContextPanel(() => (contextPanelVisible() ? "hide" : "show"))
+ dialog.clear()
+ },
+ },
{
title: conceal() ? "Disable code concealment" : "Enable code concealment",
value: "session.toggle.conceal",
@@ -1199,6 +1225,9 @@ export function Session() {
+
+
+
)
diff --git a/packages/opencode/src/config/config.ts b/packages/opencode/src/config/config.ts
index efae2ca551..bbe2faf109 100644
--- a/packages/opencode/src/config/config.ts
+++ b/packages/opencode/src/config/config.ts
@@ -614,6 +614,7 @@ export namespace Config {
editor_open: z.string().optional().default("e").describe("Open external editor"),
theme_list: z.string().optional().default("t").describe("List available themes"),
sidebar_toggle: z.string().optional().default("b").describe("Toggle sidebar"),
+ context_panel: z.string().optional().default("i").describe("Toggle context panel"),
scrollbar_toggle: z.string().optional().default("none").describe("Toggle session scrollbar"),
username_toggle: z.string().optional().default("none").describe("Toggle username visibility"),
status_view: z.string().optional().default("s").describe("View status"),