mirror of
https://github.com/anomalyco/opencode.git
synced 2026-09-08 14:54:38 +00:00
fix(tui): simplify narrow interrupt footer (#46533)
Co-authored-by: jlongster <17031+jlongster@users.noreply.github.com>
This commit is contained in:
parent
30f998c5b9
commit
c596a8f11d
4 changed files with 115 additions and 12 deletions
|
|
@ -156,7 +156,11 @@ export interface Page {
|
|||
readonly render: (input: { readonly data?: Record<string, any> }) => JSX.Element
|
||||
}
|
||||
|
||||
type PromptFooterInput = { readonly sessionID?: string; readonly mode: "normal" | "shell" }
|
||||
type PromptFooterInput = {
|
||||
readonly sessionID?: string
|
||||
readonly mode: "normal" | "shell"
|
||||
readonly showDetails: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* The host UI's slot tree. Every path is one slot: a named boundary a plugin
|
||||
|
|
|
|||
|
|
@ -1595,7 +1595,11 @@ export function Prompt(props: PromptProps) {
|
|||
if (agentLabel()) revealedPromptMetadata.add(local)
|
||||
})
|
||||
const borderHighlight = createMemo(() => tint(theme.border.default, highlight(), agentMetaAlpha()))
|
||||
const footerInput = () => ({ sessionID: props.sessionID, mode: store.mode })
|
||||
const footerInput = () => ({
|
||||
sessionID: props.sessionID,
|
||||
mode: store.mode,
|
||||
showDetails: store.interrupt === 0 || dimensions().width >= 80,
|
||||
})
|
||||
|
||||
const placeholderText = createMemo(() => {
|
||||
if (props.showPlaceholder === false) return undefined
|
||||
|
|
|
|||
|
|
@ -8,7 +8,12 @@ const money = new Intl.NumberFormat("en-US", {
|
|||
currency: "USD",
|
||||
})
|
||||
|
||||
export function PromptFooter(props: { context: Plugin.Context; sessionID?: string; mode: "normal" | "shell" }) {
|
||||
export function PromptFooter(props: {
|
||||
context: Plugin.Context
|
||||
sessionID?: string
|
||||
mode: "normal" | "shell"
|
||||
showDetails: boolean
|
||||
}) {
|
||||
const dimensions = useTerminalDimensions()
|
||||
const [liveHovered, setLiveHovered] = createSignal(false)
|
||||
const subagents = createMemo(() => {
|
||||
|
|
@ -69,7 +74,7 @@ export function PromptFooter(props: { context: Plugin.Context; sessionID?: strin
|
|||
</text>
|
||||
</box>
|
||||
</Show>
|
||||
<Show when={status().length > 0}>
|
||||
<Show when={props.showDetails && status().length > 0}>
|
||||
<text fg={props.context.theme.text.subdued} wrapMode="none" truncate flexShrink={1}>
|
||||
<Show when={live()}> · </Show>
|
||||
{status().join(" · ")}
|
||||
|
|
@ -77,14 +82,14 @@ export function PromptFooter(props: { context: Plugin.Context; sessionID?: strin
|
|||
</Show>
|
||||
</box>
|
||||
</Match>
|
||||
<Match when={dimensions().width >= 44}>
|
||||
<Match when={props.showDetails && dimensions().width >= 44}>
|
||||
<text fg={props.context.theme.text.default} flexShrink={0}>
|
||||
{shortcut("agent.cycle")} <span style={{ fg: props.context.theme.text.subdued }}>agents</span>
|
||||
</text>
|
||||
</Match>
|
||||
</Switch>
|
||||
<Show when={dimensions().width >= 44}>
|
||||
<text fg={props.context.theme.text.default} flexShrink={0}>
|
||||
<Show when={props.showDetails}>
|
||||
<text fg={props.context.theme.text.default} wrapMode="none" flexShrink={0}>
|
||||
{shortcut("command.palette.show")} <span style={{ fg: props.context.theme.text.subdued }}>commands</span>
|
||||
</text>
|
||||
</Show>
|
||||
|
|
@ -106,7 +111,14 @@ export default Plugin.define({
|
|||
setup(context) {
|
||||
context.ui.slot({
|
||||
append: "prompt.footer",
|
||||
render: (props) => <PromptFooter context={context} sessionID={props.sessionID} mode={props.mode} />,
|
||||
render: (props) => (
|
||||
<PromptFooter
|
||||
context={context}
|
||||
sessionID={props.sessionID}
|
||||
mode={props.mode}
|
||||
showDetails={props.showDetails}
|
||||
/>
|
||||
),
|
||||
})
|
||||
},
|
||||
})
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import { expect, test } from "bun:test"
|
|||
import { RGBA, TextRenderable } from "@opentui/core"
|
||||
import { testRender } from "@opentui/solid"
|
||||
import type { Context } from "@opencode-ai/plugin/tui/context"
|
||||
import { createSignal } from "solid-js"
|
||||
import { PromptFooter } from "../../src/feature-plugins/prompt/footer"
|
||||
|
||||
test("prompt footer separates simultaneous subagent, shell, and usage status", async () => {
|
||||
|
|
@ -38,10 +39,13 @@ test("prompt footer separates simultaneous subagent, shell, and usage status", a
|
|||
},
|
||||
},
|
||||
} as unknown as Context
|
||||
const app = await testRender(() => <PromptFooter context={context} sessionID="session" mode="normal" />, {
|
||||
width: 80,
|
||||
height: 2,
|
||||
})
|
||||
const app = await testRender(
|
||||
() => <PromptFooter context={context} sessionID="session" mode="normal" showDetails={true} />,
|
||||
{
|
||||
width: 80,
|
||||
height: 2,
|
||||
},
|
||||
)
|
||||
|
||||
try {
|
||||
await app.renderOnce()
|
||||
|
|
@ -59,3 +63,82 @@ test("prompt footer separates simultaneous subagent, shell, and usage status", a
|
|||
app.renderer.destroy()
|
||||
}
|
||||
})
|
||||
|
||||
test("prompt footer can hide details", async () => {
|
||||
const color = RGBA.fromInts(200, 200, 200)
|
||||
const context = {
|
||||
location: { directory: "/workspace" },
|
||||
theme: {
|
||||
text: {
|
||||
default: color,
|
||||
subdued: color,
|
||||
},
|
||||
},
|
||||
keymap: {
|
||||
shortcuts: (id: string) => {
|
||||
if (id === "command.palette.show") return ["ctrl+p"]
|
||||
if (id === "agent.cycle") return ["shift+tab"]
|
||||
return []
|
||||
},
|
||||
},
|
||||
data: {
|
||||
session: {
|
||||
family: () => ["session"],
|
||||
status: () => "running",
|
||||
get: () => ({ id: "session", location: { directory: "/workspace" } }),
|
||||
cost: () => 1,
|
||||
message: {
|
||||
list: () => [
|
||||
{
|
||||
id: "message",
|
||||
type: "assistant",
|
||||
model: { providerID: "provider", id: "model" },
|
||||
tokens: { input: 1_000, output: 0, reasoning: 0, cache: { read: 0, write: 0 } },
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
shell: { list: () => [] },
|
||||
location: {
|
||||
model: { list: () => [{ providerID: "provider", id: "model", limit: { context: 10_000 } }] },
|
||||
},
|
||||
},
|
||||
} as unknown as Context
|
||||
const [showDetails, setShowDetails] = createSignal(true)
|
||||
const [sessionID, setSessionID] = createSignal<string | undefined>("session")
|
||||
const app = await testRender(
|
||||
() => (
|
||||
<box width="100%" flexDirection="row" justifyContent="space-between" gap={2}>
|
||||
<PromptFooter
|
||||
context={context}
|
||||
sessionID={sessionID()}
|
||||
mode="normal"
|
||||
showDetails={showDetails()}
|
||||
/>
|
||||
</box>
|
||||
),
|
||||
{
|
||||
width: 80,
|
||||
height: 1,
|
||||
},
|
||||
)
|
||||
|
||||
try {
|
||||
await app.renderOnce()
|
||||
expect(app.captureCharFrame()).toContain("1.0K (10%) · $1.00")
|
||||
expect(app.captureCharFrame()).toContain("ctrl+p commands")
|
||||
|
||||
setShowDetails(false)
|
||||
await app.renderOnce()
|
||||
const frame = app.captureCharFrame()
|
||||
expect(frame).not.toContain("1.0K (10%)")
|
||||
expect(frame).not.toContain("$1.00")
|
||||
expect(frame).not.toContain("ctrl+p commands")
|
||||
|
||||
setSessionID(undefined)
|
||||
await app.renderOnce()
|
||||
expect(app.captureCharFrame()).not.toContain("shift+tab agents")
|
||||
} finally {
|
||||
app.renderer.destroy()
|
||||
}
|
||||
})
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue