diff options
| author | Dax <[email protected]> | 2025-11-26 20:11:39 -0500 |
|---|---|---|
| committer | GitHub <[email protected]> | 2025-11-26 20:11:39 -0500 |
| commit | 63bfe767200f4caf9a6c808af085e293f9816e99 (patch) | |
| tree | 2d9e14f6fdb8d6552aaac6852fe043054df69f21 | |
| parent | 99d7ff47c409a80a9c3b8217e8ca09383f4e70ac (diff) | |
| download | opencode-63bfe767200f4caf9a6c808af085e293f9816e99.tar.gz opencode-63bfe767200f4caf9a6c808af085e293f9816e99.zip | |
tui design refinement (#4809)
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> ) } |
