mirror of
https://github.com/sst/opencode.git
synced 2025-12-23 10:11:41 +00:00
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:
parent
5156b6df8a
commit
113a7b5996
3 changed files with 101 additions and 85 deletions
|
|
@ -1,5 +1,6 @@
|
|||
{
|
||||
"$schema": "https://opencode.ai/config.json",
|
||||
"plugin": ["opencode-openai-codex-auth"],
|
||||
"mcp": {
|
||||
"weather": {
|
||||
"type": "local",
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue