summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorDax <[email protected]>2025-11-26 20:11:39 -0500
committerGitHub <[email protected]>2025-11-26 20:11:39 -0500
commit63bfe767200f4caf9a6c808af085e293f9816e99 (patch)
tree2d9e14f6fdb8d6552aaac6852fe043054df69f21
parent99d7ff47c409a80a9c3b8217e8ca09383f4e70ac (diff)
downloadopencode-63bfe767200f4caf9a6c808af085e293f9816e99.tar.gz
opencode-63bfe767200f4caf9a6c808af085e293f9816e99.zip
tui design refinement (#4809)
-rw-r--r--.opencode/opencode.jsonc8
-rw-r--r--packages/opencode/src/cli/cmd/tui/app.tsx53
-rw-r--r--packages/opencode/src/cli/cmd/tui/component/dialog-provider.tsx13
-rw-r--r--packages/opencode/src/cli/cmd/tui/component/prompt/autocomplete.tsx5
-rw-r--r--packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx13
-rw-r--r--packages/opencode/src/cli/cmd/tui/context/directory.ts12
-rw-r--r--packages/opencode/src/cli/cmd/tui/routes/home.tsx52
-rw-r--r--packages/opencode/src/cli/cmd/tui/routes/session/footer.tsx37
-rw-r--r--packages/opencode/src/cli/cmd/tui/routes/session/header.tsx93
-rw-r--r--packages/opencode/src/cli/cmd/tui/routes/session/index.tsx37
-rw-r--r--packages/opencode/src/cli/cmd/tui/routes/session/sidebar.tsx329
11 files changed, 387 insertions, 265 deletions
diff --git a/.opencode/opencode.jsonc b/.opencode/opencode.jsonc
index dd5a4c750..369832f9f 100644
--- a/.opencode/opencode.jsonc
+++ b/.opencode/opencode.jsonc
@@ -2,7 +2,7 @@
"$schema": "https://opencode.ai/config.json",
"plugin": ["opencode-openai-codex-auth"],
// "enterprise": {
- // "url": "http://localhost:3000",
+ // "url": "https://enterprise.dev.opencode.ai",
// },
"provider": {
"opencode": {
@@ -11,4 +11,10 @@
},
},
},
+ "mcp": {
+ "exa": {
+ "type": "remote",
+ "url": "https://mcp.exa.ai/mcp",
+ },
+ },
}
diff --git a/packages/opencode/src/cli/cmd/tui/app.tsx b/packages/opencode/src/cli/cmd/tui/app.tsx
index 7c72274ad..5ec737256 100644
--- a/packages/opencode/src/cli/cmd/tui/app.tsx
+++ b/packages/opencode/src/cli/cmd/tui/app.tsx
@@ -452,51 +452,14 @@ function App() {
}
}}
>
- <box flexDirection="column" flexGrow={1}>
- <Switch>
- <Match when={route.data.type === "home"}>
- <Home />
- </Match>
- <Match when={route.data.type === "session"}>
- <Session />
- </Match>
- </Switch>
- </box>
- <box
- height={1}
- backgroundColor={theme.backgroundPanel}
- flexDirection="row"
- justifyContent="space-between"
- flexShrink={0}
- >
- <box flexDirection="row">
- <box flexDirection="row" backgroundColor={theme.backgroundElement} paddingLeft={1} paddingRight={1}>
- <text fg={theme.textMuted}>open</text>
- <text fg={theme.text} attributes={TextAttributes.BOLD}>
- code{" "}
- </text>
- <text fg={theme.textMuted}>v{Installation.VERSION}</text>
- </box>
- <box paddingLeft={1} paddingRight={1}>
- <text fg={theme.textMuted}>
- {process.cwd().replace(Global.Path.home, "~")}
- {sync.data.vcs?.branch ? `:${sync.data.vcs.branch}` : ""}
- </text>
- </box>
- </box>
- <Show when={false}>
- <box flexDirection="row" flexShrink={0}>
- <text fg={theme.textMuted} paddingRight={1}>
- tab
- </text>
- <text fg={local.agent.color(local.agent.current().name)}>{""}</text>
- <text bg={local.agent.color(local.agent.current().name)} fg={theme.background} wrapMode={undefined}>
- <span style={{ bold: true }}> {local.agent.current().name.toUpperCase()}</span>
- <span> AGENT </span>
- </text>
- </box>
- </Show>
- </box>
+ <Switch>
+ <Match when={route.data.type === "home"}>
+ <Home />
+ </Match>
+ <Match when={route.data.type === "session"}>
+ <Session />
+ </Match>
+ </Switch>
</box>
)
}
diff --git a/packages/opencode/src/cli/cmd/tui/component/dialog-provider.tsx b/packages/opencode/src/cli/cmd/tui/component/dialog-provider.tsx
index 109d4d25a..30a8bb2fc 100644
--- a/packages/opencode/src/cli/cmd/tui/component/dialog-provider.tsx
+++ b/packages/opencode/src/cli/cmd/tui/component/dialog-provider.tsx
@@ -197,11 +197,24 @@ function ApiMethod(props: ApiMethodProps) {
const dialog = useDialog()
const sdk = useSDK()
const sync = useSync()
+ const { theme } = useTheme()
return (
<DialogPrompt
title={props.title}
placeholder="API key"
+ description={
+ props.providerID === "opencode" ? (
+ <box gap={1}>
+ <text fg={theme.textMuted}>
+ OpenCode Zen gives you access to all the best coding models at the cheapest prices with a single API key.
+ </text>
+ <text>
+ Go to <span style={{ fg: theme.primary }}>https://opencode.ai/zen</span> to get a key
+ </text>
+ </box>
+ ) : undefined
+ }
onConfirm={async (value) => {
if (!value) return
sdk.client.auth.set({
diff --git a/packages/opencode/src/cli/cmd/tui/component/prompt/autocomplete.tsx b/packages/opencode/src/cli/cmd/tui/component/prompt/autocomplete.tsx
index 8371c395f..4232f3ae8 100644
--- a/packages/opencode/src/cli/cmd/tui/component/prompt/autocomplete.tsx
+++ b/packages/opencode/src/cli/cmd/tui/component/prompt/autocomplete.tsx
@@ -293,6 +293,11 @@ export function Autocomplete(props: {
onSelect: () => command.trigger("prompt.editor", "prompt"),
},
{
+ display: "/connect",
+ description: "connect to a provider",
+ onSelect: () => command.trigger("provider.connect"),
+ },
+ {
display: "/help",
description: "show help",
onSelect: () => command.trigger("help.show"),
diff --git a/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx b/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx
index 90fb4c982..06e9a49e6 100644
--- a/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx
+++ b/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx
@@ -637,11 +637,7 @@ export function Prompt(props: PromptProps) {
flexGrow={1}
>
<textarea
- placeholder={
- props.showPlaceholder
- ? t`${dim(fg(theme.primary)(" → up/down"))} ${dim(fg("#64748b")("history"))} ${dim(fg("#a78bfa")("•"))} ${dim(fg(theme.primary)(keybind.print("input_newline")))} ${dim(fg("#64748b")("newline"))} ${dim(fg("#a78bfa")("•"))} ${dim(fg(theme.primary)(keybind.print("input_submit")))} ${dim(fg("#64748b")("submit"))}`
- : undefined
- }
+ placeholder={props.sessionID ? undefined : "Build anything..."}
textColor={theme.text}
focusedTextColor={theme.text}
minHeight={1}
@@ -781,7 +777,12 @@ export function Prompt(props: PromptProps) {
return
}
}}
- ref={(r: TextareaRenderable) => (input = r)}
+ ref={(r: TextareaRenderable) => {
+ input = r
+ setTimeout(() => {
+ input.cursorColor = highlight()
+ }, 0)
+ }}
onMouseDown={(r: MouseEvent) => r.target?.focus()}
focusedBackgroundColor={theme.backgroundElement}
cursorColor={highlight()}
diff --git a/packages/opencode/src/cli/cmd/tui/context/directory.ts b/packages/opencode/src/cli/cmd/tui/context/directory.ts
new file mode 100644
index 000000000..2ea8cf007
--- /dev/null
+++ b/packages/opencode/src/cli/cmd/tui/context/directory.ts
@@ -0,0 +1,12 @@
+import { createMemo } from "solid-js"
+import { useSync } from "./sync"
+import { Global } from "@/global"
+
+export function useDirectory() {
+ const sync = useSync()
+ return createMemo(() => {
+ const result = process.cwd().replace(Global.Path.home, "~")
+ if (sync.data.vcs?.branch) return result + ":" + sync.data.vcs.branch
+ return result
+ })
+}
diff --git a/packages/opencode/src/cli/cmd/tui/routes/home.tsx b/packages/opencode/src/cli/cmd/tui/routes/home.tsx
index 6f63258b9..33942c2a5 100644
--- a/packages/opencode/src/cli/cmd/tui/routes/home.tsx
+++ b/packages/opencode/src/cli/cmd/tui/routes/home.tsx
@@ -8,6 +8,8 @@ import { Locale } from "@/util/locale"
import { useSync } from "../context/sync"
import { Toast } from "../ui/toast"
import { useArgs } from "../context/args"
+import { Global } from "@/global"
+import { useDirectory } from "../context/directory"
// TODO: what is the best way to do this?
let once = false
@@ -15,6 +17,7 @@ let once = false
export function Home() {
const sync = useSync()
const { theme } = useTheme()
+ const mcp = createMemo(() => Object.keys(sync.data.mcp).length > 0)
const mcpError = createMemo(() => {
return Object.values(sync.data.mcp).some((x) => x.status === "failed")
})
@@ -47,31 +50,36 @@ export function Home() {
once = true
}
})
+ const directory = useDirectory()
return (
- <box flexGrow={1} justifyContent="center" alignItems="center" paddingLeft={2} paddingRight={2} gap={1}>
- <Logo />
- <box width={39}>
- <HelpRow keybind="command_list">Commands</HelpRow>
- <HelpRow keybind="session_list">List sessions</HelpRow>
- <HelpRow keybind="model_list">Switch model</HelpRow>
- <HelpRow keybind="agent_cycle">Switch agent</HelpRow>
+ <>
+ <box flexGrow={1} justifyContent="center" alignItems="center" paddingLeft={2} paddingRight={2} gap={1}>
+ <Logo />
+ <box width="100%" maxWidth={75} zIndex={1000} paddingTop={1}>
+ <Prompt ref={(r) => (prompt = r)} hint={Hint} />
+ </box>
+ <Toast />
</box>
- <box width="100%" maxWidth={75} zIndex={1000} paddingTop={1}>
- <Prompt ref={(r) => (prompt = r)} hint={Hint} />
+ <box paddingTop={1} paddingBottom={1} paddingLeft={2} paddingRight={2} flexDirection="row" flexShrink={0} gap={2}>
+ <text fg={theme.textMuted}>{directory()}</text>
+ <box gap={1} flexDirection="row" flexShrink={0}>
+ <Show when={mcp()}>
+ <text fg={theme.text}>
+ <Switch>
+ <Match when={mcpError()}>
+ <span style={{ fg: theme.error }}>⊙ </span>
+ </Match>
+ <Match when={true}>
+ <span style={{ fg: theme.success }}>⊙ </span>
+ </Match>
+ </Switch>
+ {Object.keys(sync.data.mcp).length} MCP
+ </text>
+ <text fg={theme.textMuted}>/status</text>
+ </Show>
+ </box>
</box>
- <Toast />
- </box>
- )
-}
-
-function HelpRow(props: ParentProps<{ keybind: keyof KeybindsConfig }>) {
- const keybind = useKeybind()
- const { theme } = useTheme()
- return (
- <box flexDirection="row" justifyContent="space-between" width="100%">
- <text fg={theme.text}>{props.children}</text>
- <text fg={theme.primary}>{keybind.print(props.keybind)}</text>
- </box>
+ </>
)
}
diff --git a/packages/opencode/src/cli/cmd/tui/routes/session/footer.tsx b/packages/opencode/src/cli/cmd/tui/routes/session/footer.tsx
new file mode 100644
index 000000000..6216a429e
--- /dev/null
+++ b/packages/opencode/src/cli/cmd/tui/routes/session/footer.tsx
@@ -0,0 +1,37 @@
+import { createMemo, Match, Show, Switch } from "solid-js"
+import { useTheme } from "../../context/theme"
+import { useSync } from "../../context/sync"
+import { useDirectory } from "../../context/directory"
+
+export function Footer() {
+ const { theme } = useTheme()
+ const sync = useSync()
+ const mcp = createMemo(() => Object.keys(sync.data.mcp))
+ const mcpError = createMemo(() => Object.values(sync.data.mcp).some((x) => x.status === "failed"))
+ const lsp = createMemo(() => Object.keys(sync.data.lsp))
+ const directory = useDirectory()
+ return (
+ <box flexDirection="row" justifyContent="space-between" gap={1}>
+ <text fg={theme.textMuted}>{directory()}</text>
+ <box gap={2} flexDirection="row" flexShrink={0}>
+ <text fg={theme.text}>
+ <span style={{ fg: theme.success }}>•</span> {lsp().length} LSP
+ </text>
+ <Show when={mcp().length}>
+ <text fg={theme.text}>
+ <Switch>
+ <Match when={mcpError()}>
+ <span style={{ fg: theme.error }}>⊙ </span>
+ </Match>
+ <Match when={true}>
+ <span style={{ fg: theme.success }}>⊙ </span>
+ </Match>
+ </Switch>
+ {mcp().length} MCP
+ </text>
+ </Show>
+ <text fg={theme.textMuted}>/status</text>
+ </box>
+ </box>
+ )
+}
diff --git a/packages/opencode/src/cli/cmd/tui/routes/session/header.tsx b/packages/opencode/src/cli/cmd/tui/routes/session/header.tsx
index 9988fbd55..eb780f521 100644
--- a/packages/opencode/src/cli/cmd/tui/routes/session/header.tsx
+++ b/packages/opencode/src/cli/cmd/tui/routes/session/header.tsx
@@ -3,15 +3,16 @@ import { useRouteData } from "@tui/context/route"
import { useSync } from "@tui/context/sync"
import { pipe, sumBy } from "remeda"
import { useTheme } from "@tui/context/theme"
-import { SplitBorder } from "@tui/component/border"
+import { SplitBorder, EmptyBorder } from "@tui/component/border"
import type { AssistantMessage, Session } from "@opencode-ai/sdk"
+import { useDirectory } from "../../context/directory"
+import { useKeybind } from "../../context/keybind"
const Title = (props: { session: Accessor<Session> }) => {
const { theme } = useTheme()
return (
<text fg={theme.text}>
- <span style={{ bold: true, fg: theme.accent }}>#</span>{" "}
- <span style={{ bold: true }}>{props.session().title}</span>
+ <span style={{ bold: true }}>#</span> <span style={{ bold: true }}>{props.session().title}</span>
</text>
)
}
@@ -53,43 +54,71 @@ export function Header() {
const model = sync.data.provider.find((x) => x.id === last.providerID)?.models[last.modelID]
let result = total.toLocaleString()
if (model?.limit.context) {
- result += "/" + Math.round((total / model.limit.context) * 100) + "%"
+ result += " " + Math.round((total / model.limit.context) * 100) + "%"
}
return result
})
const { theme } = useTheme()
+ const keybind = useKeybind()
return (
- <box paddingLeft={1} paddingRight={1} {...SplitBorder} borderColor={theme.backgroundElement} flexShrink={0}>
- <Show
- when={shareEnabled()}
- fallback={
- <box flexDirection="row" justifyContent="space-between" gap={1}>
- <Title session={session} />
- <ContextInfo context={context} cost={cost} />
- </box>
- }
+ <box flexShrink={0}>
+ <box
+ paddingTop={1}
+ paddingBottom={1}
+ paddingLeft={2}
+ paddingRight={1}
+ {...SplitBorder}
+ border={["left"]}
+ borderColor={theme.border}
+ flexShrink={0}
+ backgroundColor={theme.backgroundPanel}
>
- <Title session={session} />
- <box flexDirection="row" justifyContent="space-between" gap={1}>
- <box flexGrow={1} flexShrink={1}>
- <Switch>
- <Match when={session().share?.url}>
- <text fg={theme.textMuted} wrapMode="word">
- {session().share!.url}
- </text>
- </Match>
- <Match when={true}>
- <text fg={theme.text} wrapMode="word">
- /share <span style={{ fg: theme.textMuted }}>to create a shareable link</span>
- </text>
- </Match>
- </Switch>
- </box>
- <ContextInfo context={context} cost={cost} />
- </box>
- </Show>
+ <Switch>
+ <Match when={session()?.parentID}>
+ <box flexDirection="row" gap={2}>
+ <text fg={theme.text}>
+ <b>Subagent session</b>
+ </text>
+ <text fg={theme.text}>
+ Prev <span style={{ fg: theme.textMuted }}>{keybind.print("session_child_cycle_reverse")}</span>
+ </text>
+ <text fg={theme.text}>
+ Next <span style={{ fg: theme.textMuted }}>{keybind.print("session_child_cycle")}</span>
+ </text>
+ <box flexGrow={1} flexShrink={1} />
+ <ContextInfo context={context} cost={cost} />
+ </box>
+ </Match>
+ <Match when={!shareEnabled()}>
+ <box flexDirection="row" justifyContent="space-between" gap={1}>
+ <Title session={session} />
+ <ContextInfo context={context} cost={cost} />
+ </box>
+ </Match>
+ <Match when={true}>
+ <Title session={session} />
+ <box flexDirection="row" justifyContent="space-between" gap={1}>
+ <box flexGrow={1} flexShrink={1}>
+ <Switch>
+ <Match when={session().share?.url}>
+ <text fg={theme.textMuted} wrapMode="word">
+ {session().share!.url}
+ </text>
+ </Match>
+ <Match when={true}>
+ <text fg={theme.text} wrapMode="word">
+ /share <span style={{ fg: theme.textMuted }}>to create a shareable link</span>
+ </text>
+ </Match>
+ </Switch>
+ </box>
+ <ContextInfo context={context} cost={cost} />
+ </box>
+ </Match>
+ </Switch>
+ </box>
</box>
)
}
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 c0d173421..fdbcb34f9 100644
--- a/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx
+++ b/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx
@@ -62,6 +62,7 @@ import { Toast, useToast } from "../../ui/toast"
import { useKV } from "../../context/kv.tsx"
import { Editor } from "../../util/editor"
import stripAnsi from "strip-ansi"
+import { Footer } from "./footer.tsx"
addDefaultParsers(parsers.parsers)
@@ -114,7 +115,12 @@ export function Session() {
const [showTimestamps, setShowTimestamps] = createSignal(kv.get("timestamps", "hide") === "show")
const wide = createMemo(() => dimensions().width > 120)
- const sidebarVisible = createMemo(() => sidebar() === "show" || (sidebar() === "auto" && wide()))
+ const sidebarVisible = createMemo(() => {
+ if (session()?.parentID) return false
+ if (sidebar() === "show") return true
+ if (sidebar() === "auto" && wide()) return true
+ return false
+ })
const contentWidth = createMemo(() => dimensions().width - (sidebarVisible() ? 42 : 0) - 4)
const scrollAcceleration = createMemo(() => {
@@ -736,31 +742,9 @@ export function Session() {
sync,
}}
>
- <box flexDirection="row" paddingBottom={1} paddingTop={1} paddingLeft={2} paddingRight={2} gap={2}>
- <box flexGrow={1} gap={1}>
+ <box flexDirection="row">
+ <box flexGrow={1} paddingBottom={1} paddingTop={1} paddingLeft={2} paddingRight={2} gap={1}>
<Show when={session()}>
- <Show when={session().parentID}>
- <box
- backgroundColor={theme.backgroundPanel}
- justifyContent="space-between"
- flexDirection="row"
- paddingTop={1}
- paddingBottom={1}
- flexShrink={0}
- paddingLeft={2}
- paddingRight={2}
- >
- <text fg={theme.text}>
- Previous <span style={{ fg: theme.textMuted }}>{keybind.print("session_child_cycle_reverse")}</span>
- </text>
- <text fg={theme.text}>
- <b>Viewing subagent session</b>
- </text>
- <text fg={theme.text}>
- <span style={{ fg: theme.textMuted }}>{keybind.print("session_child_cycle")}</span> Next
- </text>
- </box>
- </Show>
<Show when={!sidebarVisible()}>
<Header />
</Show>
@@ -885,6 +869,9 @@ export function Session() {
sessionID={route.sessionID}
/>
</box>
+ <Show when={!sidebarVisible()}>
+ <Footer />
+ </Show>
</Show>
<Toast />
</box>
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 8302bfefe..c63f5116a 100644
--- a/packages/opencode/src/cli/cmd/tui/routes/session/sidebar.tsx
+++ b/packages/opencode/src/cli/cmd/tui/routes/session/sidebar.tsx
@@ -1,9 +1,14 @@
import { useSync } from "@tui/context/sync"
-import { createMemo, For, Show, Switch, Match, createSignal } from "solid-js"
+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"
+import { Global } from "@/global"
+import { Installation } from "@/installation"
+import { useKeybind } from "../../context/keybind"
+import { useDirectory } from "../../context/directory"
export function Sidebar(props: { sessionID: string }) {
const sync = useSync()
@@ -13,10 +18,12 @@ export function Sidebar(props: { sessionID: string }) {
const todo = createMemo(() => sync.data.todo[props.sessionID] ?? [])
const messages = createMemo(() => sync.data.message[props.sessionID] ?? [])
- const [mcpExpanded, setMcpExpanded] = createSignal(true)
- const [diffExpanded, setDiffExpanded] = createSignal(true)
- const [todoExpanded, setTodoExpanded] = createSignal(true)
- const [lspExpanded, setLspExpanded] = createSignal(true)
+ 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)))
@@ -41,87 +48,104 @@ export function Sidebar(props: { sessionID: string }) {
}
})
+ const keybind = useKeybind()
+ const directory = useDirectory()
+
+ const hasProviders = createMemo(() =>
+ sync.data.provider.some((x) => x.id !== "opencode" || Object.values(x.models).some((y) => y.cost?.input !== 0)),
+ )
+
return (
<Show when={session()}>
- <scrollbox width={40}>
- <box flexShrink={0} gap={1} paddingRight={1}>
- <box>
- <text fg={theme.text}>
- <b>{session().title}</b>
- </text>
- <Show when={session().share?.url}>
- <text fg={theme.textMuted}>{session().share!.url}</text>
- </Show>
- </box>
- <box>
- <text fg={theme.text}>
- <b>Context</b>
- </text>
- <text fg={theme.textMuted}>{context()?.tokens ?? 0} tokens</text>
- <text fg={theme.textMuted}>{context()?.percentage ?? 0}% used</text>
- <text fg={theme.textMuted}>{cost()} spent</text>
- </box>
- <Show when={mcpEntries().length > 0}>
+ <box
+ backgroundColor={theme.backgroundPanel}
+ width={42}
+ paddingTop={1}
+ paddingBottom={1}
+ paddingLeft={2}
+ paddingRight={2}
+ >
+ <scrollbox flexGrow={1}>
+ <box flexShrink={0} gap={1} paddingRight={1}>
<box>
- <box
- flexDirection="row"
- gap={1}
- onMouseDown={() => mcpEntries().length > 2 && setMcpExpanded(!mcpExpanded())}
- >
- <Show when={mcpEntries().length > 2}>
- <text fg={theme.text}>{mcpExpanded() ? "▼" : "▶"}</text>
- </Show>
- <text fg={theme.text}>
- <b>MCP</b>
- </text>
- </box>
- <Show when={mcpEntries().length <= 2 || mcpExpanded()}>
- <For each={mcpEntries()}>
- {([key, item]) => (
- <box flexDirection="row" gap={1}>
- <text
- flexShrink={0}
- style={{
- fg: {
- connected: theme.success,
- failed: theme.error,
- disabled: theme.textMuted,
- }[item.status],
- }}
- >
- •
- </text>
- <text fg={theme.text} wrapMode="word">
- {key}{" "}
- <span style={{ fg: theme.textMuted }}>
- <Switch>
- <Match when={item.status === "connected"}>Connected</Match>
- <Match when={item.status === "failed" && item}>{(val) => <i>{val().error}</i>}</Match>
- <Match when={item.status === "disabled"}>Disabled in configuration</Match>
- </Switch>
- </span>
- </text>
- </box>
- )}
- </For>
+ <text fg={theme.text}>
+ <b>{session().title}</b>
+ </text>
+ <Show when={session().share?.url}>
+ <text fg={theme.textMuted}>{session().share!.url}</text>
</Show>
</box>
- </Show>
- <Show when={sync.data.lsp.length > 0}>
+ <box>
+ <text fg={theme.text}>
+ <b>Context</b>
+ </text>
+ <text fg={theme.textMuted}>{context()?.tokens ?? 0} tokens</text>
+ <text fg={theme.textMuted}>{context()?.percentage ?? 0}% used</text>
+ <text fg={theme.textMuted}>{cost()} spent</text>
+ </box>
+ <Show when={mcpEntries().length > 0}>
+ <box>
+ <box
+ flexDirection="row"
+ gap={1}
+ onMouseDown={() => mcpEntries().length > 2 && setExpanded("mcp", !expanded.mcp)}
+ >
+ <Show when={mcpEntries().length > 2}>
+ <text fg={theme.text}>{expanded.mcp ? "▼" : "▶"}</text>
+ </Show>
+ <text fg={theme.text}>
+ <b>MCP</b>
+ </text>
+ </box>
+ <Show when={mcpEntries().length <= 2 || expanded.mcp}>
+ <For each={mcpEntries()}>
+ {([key, item]) => (
+ <box flexDirection="row" gap={1}>
+ <text
+ flexShrink={0}
+ style={{
+ fg: {
+ connected: theme.success,
+ failed: theme.error,
+ disabled: theme.textMuted,
+ }[item.status],
+ }}
+ >
+ •
+ </text>
+ <text fg={theme.text} wrapMode="word">
+ {key}{" "}
+ <span style={{ fg: theme.textMuted }}>
+ <Switch>
+ <Match when={item.status === "connected"}>Connected</Match>
+ <Match when={item.status === "failed" && item}>{(val) => <i>{val().error}</i>}</Match>
+ <Match when={item.status === "disabled"}>Disabled in configuration</Match>
+ </Switch>
+ </span>
+ </text>
+ </box>
+ )}
+ </For>
+ </Show>
+ </box>
+ </Show>
<box>
<box
flexDirection="row"
gap={1}
- onMouseDown={() => sync.data.lsp.length > 2 && setLspExpanded(!lspExpanded())}
+ onMouseDown={() => sync.data.lsp.length > 2 && setExpanded("lsp", !expanded.lsp)}
>
<Show when={sync.data.lsp.length > 2}>
- <text fg={theme.text}>{lspExpanded() ? "▼" : "▶"}</text>
+ <text fg={theme.text}>{expanded.lsp ? "▼" : "▶"}</text>
</Show>
<text fg={theme.text}>
<b>LSP</b>
</text>
</box>
- <Show when={sync.data.lsp.length <= 2 || lspExpanded()}>
+ <Show when={sync.data.lsp.length <= 2 || expanded.lsp}>
+ <Show when={sync.data.lsp.length === 0}>
+ <text fg={theme.textMuted}>LSPs will activate as files are read</text>
+ </Show>
<For each={sync.data.lsp}>
{(item) => (
<box flexDirection="row" gap={1}>
@@ -144,78 +168,115 @@ export function Sidebar(props: { sessionID: string }) {
</For>
</Show>
</box>
- </Show>
- <Show when={todo().length > 0}>
- <box>
- <box
- flexDirection="row"
- gap={1}
- onMouseDown={() => todo().length > 2 && setTodoExpanded(!todoExpanded())}
- >
- <Show when={todo().length > 2}>
- <text fg={theme.text}>{todoExpanded() ? "▼" : "▶"}</text>
+ <Show when={todo().length > 0 && todo().some((t) => t.status !== "completed")}>
+ <box>
+ <box
+ flexDirection="row"
+ gap={1}
+ onMouseDown={() => todo().length > 2 && setExpanded("todo", !expanded.todo)}
+ >
+ <Show when={todo().length > 2}>
+ <text fg={theme.text}>{expanded.todo ? "▼" : "▶"}</text>
+ </Show>
+ <text fg={theme.text}>
+ <b>Todo</b>
+ </text>
+ </box>
+ <Show when={todo().length <= 2 || expanded.todo}>
+ <For each={todo()}>
+ {(todo) => (
+ <text style={{ fg: todo.status === "in_progress" ? theme.success : theme.textMuted }}>
+ [{todo.status === "completed" ? "✓" : " "}] {todo.content}
+ </text>
+ )}
+ </For>
</Show>
- <text fg={theme.text}>
- <b>Todo</b>
- </text>
</box>
- <Show when={todo().length <= 2 || todoExpanded()}>
- <For each={todo()}>
- {(todo) => (
- <text style={{ fg: todo.status === "in_progress" ? theme.success : theme.textMuted }}>
- [{todo.status === "completed" ? "✓" : " "}] {todo.content}
- </text>
- )}
- </For>
- </Show>
- </box>
- </Show>
- <Show when={diff().length > 0}>
- <box>
- <box
- flexDirection="row"
- gap={1}
- onMouseDown={() => diff().length > 2 && setDiffExpanded(!diffExpanded())}
- >
- <Show when={diff().length > 2}>
- <text fg={theme.text}>{diffExpanded() ? "▼" : "▶"}</text>
+ </Show>
+ <Show when={diff().length > 0}>
+ <box>
+ <box
+ flexDirection="row"
+ gap={1}
+ onMouseDown={() => diff().length > 2 && setExpanded("diff", !expanded.diff)}
+ >
+ <Show when={diff().length > 2}>
+ <text fg={theme.text}>{expanded.diff ? "▼" : "▶"}</text>
+ </Show>
+ <text fg={theme.text}>
+ <b>Modified Files</b>
+ </text>
+ </box>
+ <Show when={diff().length <= 2 || expanded.diff}>
+ <For each={diff() || []}>
+ {(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 (
+ <box flexDirection="row" gap={1} justifyContent="space-between">
+ <text fg={theme.textMuted} wrapMode="char">
+ {file()}
+ </text>
+ <box flexDirection="row" gap={1} flexShrink={0}>
+ <Show when={item.additions}>
+ <text fg={theme.diffAdded}>+{item.additions}</text>
+ </Show>
+ <Show when={item.deletions}>
+ <text fg={theme.diffRemoved}>-{item.deletions}</text>
+ </Show>
+ </box>
+ </box>
+ )
+ }}
+ </For>
</Show>
- <text fg={theme.text}>
- <b>Modified Files</b>
+ </box>
+ </Show>
+ </box>
+ </scrollbox>
+
+ <box flexShrink={0} gap={1}>
+ <Show when={!hasProviders()}>
+ <box
+ backgroundColor={theme.backgroundElement}
+ paddingTop={1}
+ paddingBottom={1}
+ paddingLeft={2}
+ paddingRight={2}
+ flexDirection="row"
+ gap={1}
+ >
+ <text flexShrink={0}>⬖</text>
+ <box flexGrow={1} gap={1}>
+ <text>
+ <b>Getting started</b>
</text>
+ <text fg={theme.textMuted}>OpenCode includes free models so you can start immediately.</text>
+ <text fg={theme.textMuted}>
+ Connect from 75+ providers to use other models, including Claude, GPT, Gemini etc
+ </text>
+ <box flexDirection="row" gap={1} justifyContent="space-between">
+ <text>Connect provider</text>
+ <text fg={theme.textMuted}>/connect</text>
+ </box>
</box>
- <Show when={diff().length <= 2 || diffExpanded()}>
- <For each={diff() || []}>
- {(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 (
- <box flexDirection="row" gap={1} justifyContent="space-between">
- <text fg={theme.textMuted} wrapMode="char">
- {file()}
- </text>
- <box flexDirection="row" gap={1} flexShrink={0}>
- <Show when={item.additions}>
- <text fg={theme.diffAdded}>+{item.additions}</text>
- </Show>
- <Show when={item.deletions}>
- <text fg={theme.diffRemoved}>-{item.deletions}</text>
- </Show>
- </box>
- </box>
- )
- }}
- </For>
- </Show>
</box>
</Show>
+ <text fg={theme.textMuted}>{directory()}</text>
+ <text fg={theme.textMuted}>
+ <span style={{ fg: theme.success }}>•</span> <b>Open</b>
+ <span style={{ fg: theme.text }}>
+ <b>Code</b>
+ </span>{" "}
+ <span>{Installation.VERSION}</span>
+ </text>
</box>
- </scrollbox>
+ </box>
</Show>
)
}