|
|
@@ -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>
|
|
|
)
|
|
|
}
|