tui: improve sidebar visibility logic and session title display

- Fix sidebar visibility calculation to prevent layout issues
- Add session title and share URL to sidebar header
- Remove empty text parts from assistant messages
- Adjust user message spacing for first message
- Add OpenAI codex auth plugin to configuration
This commit is contained in:
Dax Raad 2025-10-08 19:38:17 -04:00
parent 5156b6df8a
commit 113a7b5996
3 changed files with 101 additions and 85 deletions

View file

@ -1,5 +1,6 @@
{
"$schema": "https://opencode.ai/config.json",
"plugin": ["opencode-openai-codex-auth"],
"mcp": {
"weather": {
"type": "local",

View file

@ -46,6 +46,7 @@ export function Session() {
const dimensions = useTerminalDimensions()
const [sidebar, setSidebar] = createSignal<"show" | "hide" | "auto">("auto")
const wide = createMemo(() => dimensions().width > 120)
const sidebarVisible = createMemo(() => sidebar() === "show" || (sidebar() === "auto" && wide()))
createEffect(() => sync.session.sync(route.sessionID))
@ -234,7 +235,7 @@ export function Session() {
category: "Session",
onSelect: (dialog) => {
setSidebar((prev) => {
if (prev === "auto") return wide() ? "hide" : "show"
if (prev === "auto") return sidebarVisible() ? "hide" : "show"
if (prev === "show") return "hide"
return "show"
})
@ -284,16 +285,16 @@ export function Session() {
return (
<box flexDirection="row" paddingBottom={1} paddingTop={1} paddingLeft={2} paddingRight={2} gap={2}>
<box flexGrow={1}>
<box flexGrow={1} gap={1}>
<Show when={session()}>
<Header />
<Show when={!sidebarVisible()}>
<Header />
</Show>
<scrollbox
ref={(r) => (scroll = r)}
scrollbarOptions={{ visible: false }}
stickyScroll={true}
stickyStart="bottom"
paddingTop={1}
paddingBottom={1}
flexGrow={1}
>
<For each={messages()}>
@ -365,6 +366,7 @@ export function Session() {
</Match>
<Match when={message.role === "user"}>
<UserMessage
index={index()}
onMouseUp={() =>
dialog.replace(() => <DialogMessage messageID={message.id} sessionID={route.sessionID} />)
}
@ -395,7 +397,7 @@ export function Session() {
</box>
</Show>
</box>
<Show when={sidebar() === "show" || (sidebar() === "auto" && wide())}>
<Show when={sidebarVisible()}>
<Sidebar sessionID={route.sessionID} />
</Show>
</box>
@ -412,7 +414,7 @@ const MIME_BADGE: Record<string, string> = {
"application/x-directory": "dir",
}
function UserMessage(props: { message: UserMessage; parts: Part[]; onMouseUp: () => void }) {
function UserMessage(props: { message: UserMessage; parts: Part[]; onMouseUp: () => void; index: number }) {
const text = createMemo(() => props.parts.flatMap((x) => (x.type === "text" && !x.synthetic ? [x] : []))[0])
const files = createMemo(() => props.parts.flatMap((x) => (x.type === "file" ? [x] : [])))
const sync = useSync()
@ -433,7 +435,7 @@ function UserMessage(props: { message: UserMessage; parts: Part[]; onMouseUp: ()
paddingTop={1}
paddingBottom={1}
paddingLeft={2}
marginTop={1}
marginTop={props.index === 0 ? 0 : 1}
backgroundColor={hover() ? Theme.backgroundElement : Theme.backgroundPanel}
customBorderChars={SplitBorder.customBorderChars}
borderColor={Theme.secondary}
@ -549,9 +551,11 @@ function ReasoningPart(props: { part: ReasoningPart; message: AssistantMessage }
function TextPart(props: { part: TextPart; message: AssistantMessage }) {
return (
<box id={"text-" + props.part.id} paddingLeft={3} marginTop={1} flexShrink={0}>
<text>{props.part.text.trim()}</text>
</box>
<Show when={props.part.text.trim()}>
<box id={"text-" + props.part.id} paddingLeft={3} marginTop={1} flexShrink={0}>
<text>{props.part.text.trim()}</text>
</box>
</Show>
)
}
@ -596,7 +600,6 @@ function ToolPart(props: { part: ToolPart; message: AssistantMessage }) {
const el = this as BoxRenderable
const parent = el.parent
if (!parent) {
setMargin(0)
return
}
if (el.height > 1) {
@ -608,6 +611,7 @@ function ToolPart(props: { part: ToolPart; message: AssistantMessage }) {
const previous = children[index - 1]
if (!previous) {
setMargin(0)
return
}
if (previous.height > 1 || previous.id.startsWith("text-")) {
setMargin(1)

View file

@ -8,6 +8,7 @@ import type { AssistantMessage } from "@opencode-ai/sdk"
export function Sidebar(props: { sessionID: string }) {
const sync = useSync()
const sdk = useSDK()
const session = createMemo(() => sync.session.get(props.sessionID)!)
const todo = createMemo(() => sync.data.todo[props.sessionID] ?? [])
const messages = createMemo(() => sync.data.message[props.sessionID] ?? [])
const files = createMemo(() => {
@ -51,97 +52,107 @@ export function Sidebar(props: { sessionID: string }) {
})
return (
<box flexShrink={0} gap={1} width={40}>
<box>
<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>
<box>
<text>
<b>MCP</b>
</text>
<For each={Object.entries(mcp() ?? {})}>
{([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 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>
</box>
<Show when={sync.data.lsp.length > 0}>
<Show when={session()}>
<box flexShrink={0} gap={1} width={40}>
<box>
<text>
<b>LSP</b>
<b>{session().title}</b>
</text>
<For each={sync.data.lsp}>
{(item) => (
<Show when={session().share?.url}>
<text fg={Theme.textMuted}>{session().share!.url}</text>
</Show>
</box>
<box>
<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>
<box>
<text>
<b>MCP</b>
</text>
<For each={Object.entries(mcp() ?? {})}>
{([key, item]) => (
<box flexDirection="row" gap={1}>
<text
flexShrink={0}
style={{
fg: {
connected: Theme.success,
error: Theme.error,
failed: Theme.error,
disabled: Theme.textMuted,
}[item.status],
}}
>
</text>
<text fg={Theme.textMuted}>
{item.id} {item.root}
<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>
</box>
</Show>
<Show when={files().length > 0}>
<box>
<text>
<b>Modified Files</b>
</text>
<For each={files()}>{(file) => <text fg={Theme.textMuted}>{Locale.truncateMiddle(file, 40)}</text>}</For>
</box>
</Show>
<Show when={todo().length > 0}>
<box>
<text>
<b>Todo</b>
</text>
<For each={todo()}>
{(todo) => (
<text style={{ fg: todo.status === "in_progress" ? Theme.success : Theme.textMuted }}>
[{todo.status === "completed" ? "✓" : " "}] {todo.content}
</text>
)}
</For>
</box>
</Show>
</box>
<Show when={sync.data.lsp.length > 0}>
<box>
<text>
<b>LSP</b>
</text>
<For each={sync.data.lsp}>
{(item) => (
<box flexDirection="row" gap={1}>
<text
flexShrink={0}
style={{
fg: {
connected: Theme.success,
error: Theme.error,
}[item.status],
}}
>
</text>
<text fg={Theme.textMuted}>
{item.id} {item.root}
</text>
</box>
)}
</For>
</box>
</Show>
<Show when={files().length > 0}>
<box>
<text>
<b>Modified Files</b>
</text>
<For each={files()}>{(file) => <text fg={Theme.textMuted}>{Locale.truncateMiddle(file, 40)}</text>}</For>
</box>
</Show>
<Show when={todo().length > 0}>
<box>
<text>
<b>Todo</b>
</text>
<For each={todo()}>
{(todo) => (
<text style={{ fg: todo.status === "in_progress" ? Theme.success : Theme.textMuted }}>
[{todo.status === "completed" ? "✓" : " "}] {todo.content}
</text>
)}
</For>
</box>
</Show>
</box>
</Show>
)
}