Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 0 additions & 18 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,24 +7,6 @@ file is the human-readable summary of what changed and why.

## Unreleased

### TUI

- **Tighter prompt and footer spacing** — the input box swaps its full-row top
margin for a half-block edge (matching the existing bottom edge), the blank
row between the transcript and the input box is gone (the half-block edge is
the separation), and the status footer now sits on the terminal's last row
instead of two rows above it.

- **Visible selection in the question panel** — the highlighted answer (and the
hovered question tab) used a background one shade off the panel, which was
effectively invisible in light themes; the highlight is now derived from the
panel and text colors so it reads in both light and dark.

- **No stray line on tool output** — expanded tool-output panels (bash output,
generic tool results) no longer draw a background-colored left border glyph,
which showed up as an odd thin vertical line along the panel's left edge on
some terminals.

## 1.18.4-5

### TUI
Expand Down
4 changes: 3 additions & 1 deletion DIVERGENCES.md
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@ from the squash message first, or use a plain merge.
| `packages/tui/src/routes/session/index.tsx` | Sidebar visibility is session-local: removed upstream's persisted `kv.signal<"auto" \| "hide">("sidebar", "auto")` (and the `sidebar() === "auto" && wide()` branch of `sidebarVisible`); visibility is driven only by the non-persisted `sidebarOpen` signal, which the `session.sidebar.toggle` command flips (supersedes the earlier `"auto"` → `"hide"` default change) | The right sidebar starts hidden in every session regardless of terminal width or what the user did last session; `ctrl+b`/`<leader>b` only affects the current session. A stale `sidebar` key in existing kv.json files is simply never read. | Keep the kv signal removed and `sidebarVisible` reading only `parentID` + `sidebarOpen`; take upstream's surrounding changes. |
| `packages/tui/src/routes/session/index.tsx` | Hidden-sidebar surfaces: a `sidebarHint` memo (modified-file count from `sync.data.session_diff`, only when the sidebar is hidden and the session isn't a subagent; label from `sidebarHintLabel` in the fork-only `routes/session/sidebar-hint.ts`) plus a `sidebarShortcut` (`useCommandShortcut("session.sidebar.toggle")`), passed as the `Prompt`'s previously unused `hint` prop — e.g. `ctrl+b 5 modified files`; and a compact `<TodoPanel>` (fork-only `routes/session/todo-panel.tsx` + `todo-panel-model.ts`) mounted inside `<Show when={visible()}>` under an extra `<Show when={!sidebarVisible()}>`, directly above the `session_prompt` slot — up to 4 checkbox items windowed around the active todo plus a summary line (`2 of 6 finished · ctrl+b to show full TODO`), shown only while an unfinished todo exists | The sidebar starts hidden in this fork (see the row above), so users would otherwise never see the agent's plan or modified files or how to reveal them. The panel shows the active todos inline, so the footer hint only needs to cover modified files. | Keep our `sidebarShortcut`/`sidebarHint` memos, the `sidebar-hint` and `todo-panel` imports, the `TodoPanel` mount above the `session_prompt` slot, and the `hint={…}` prop on `<Prompt>` (all additive); take upstream's surrounding changes. If upstream starts passing its own `hint` to this `<Prompt>`, merge the two under one `Show`; if upstream adds its own children to the bottom box, keep `TodoPanel` directly above the prompt slot. |
| `packages/tui/src/routes/session/index.tsx` | Assistant-message trailer dedup: a `suppressed` memo in `AssistantMessage` hides the `▣ Agent · Model` prefix on the last message when it matches the currently selected agent (`local.agent.current()?.name === message.mode`) and model (`local.model.current()` vs the message's `providerID`/`modelID`), since the prompt footer right below already shows that selection; the duration still renders as `Baked for <duration>` (and `interrupted` keeps rendering on aborts), and the trailer box is skipped entirely when suppression leaves nothing to show | The last message's trailer and the prompt footer showed the same agent + model twice, one line apart. | Keep the `suppressed` memo and the `Show` gates around the trailer spans; take upstream's surrounding trailer changes (new suffixes should go inside the existing `<text>` with a `·` separator like `interrupted`). |
| `packages/tui/src/routes/session/index.tsx` | Trailer extracted from `AssistantMessage` into an exported `MessageTrailer` component (it owns the `model`/`final`/`duration`/`suppressed`/`aborted` memos, which moved with it) and the last assistant message's copy is hoisted out of the transcript: `AssistantMessage` renders it inline only `when={!props.last}`, and `Session` renders `<MessageTrailer message={lastAssistant()} last spaced={!todoPanelVisible()} />` in the pinned block between the todo panel and the `session_prompt` slot. Spacing moved with it — the pinned instance uses box `paddingTop` (only when no todo panel precedes it) / `paddingBottom` instead of the inline `<text marginTop={1}>`. `Session` gained a `todoPanelVisible` memo built on the fork-only `todoActive` predicate. | `Baked for <duration>` is footer-grade status, not transcript content: pinned below the todo panel it stays put instead of scrolling away with history, and the two status blocks above the prompt read as one group. | Keep `MessageTrailer` and both call sites; take upstream's surrounding changes. If upstream restructures the trailer, re-apply the extraction and keep the last message's copy out of the `<For>` over messages. |
| `packages/plugin/src/tui.ts` | `home_bottom` slot props widened from `{}` to `{ placement?: "center" \| "start" }` | The empty-session banner asks `home_bottom` contributors to left-align; optional prop keeps the home route and third-party plugins (no prop → centered) unchanged. | Keep our `placement` prop; take upstream's other slot-map changes. |
| `packages/tui/src/feature-plugins/home/tips.tsx` | `home_bottom` slot fn reads the new `placement` prop; with `"start"` the tips box left-aligns (`flex-start`) and drops its `paddingTop` 3 | The top-left session banner needs the tip flush under the logo; the centered home route keeps upstream spacing. | Keep our `placement` threading + conditional alignItems/paddingTop; take upstream's surrounding changes. |
| `packages/tui/src/feature-plugins/system/which-key.tsx` | `HomeHint` reads the new `placement` prop from the `home_bottom` slot; with `"start"` it left-aligns | Same top-left banner alignment as the tips row above. | Keep our `placement` threading + conditional alignItems; take upstream's surrounding changes. |
Expand All @@ -121,7 +122,8 @@ from the squash message first, or use a plain merge.
| `packages/tui/src/component/dialog-provider.tsx` | The "Other" custom-provider option routes into the fork-only `runCustomProviderFlow` (component/dialog-custom-provider.tsx, new file) — id → API key → optional base URL → model discovery via `/v1/models` → `provider.<id>` block PATCHed into global codev.json (key stays in auth.json) — instead of upstream's credential-only `ApiMethod`; the local `promptCustomProviderID` helper moved into that flow (upstream's "only stores a credential" description no longer holds). `ApiMethod`'s `custom` prop is now unreferenced but left in place. | An empty base URL keeps upstream's credential-only behavior; with one, the provider is usable without hand-editing codev.json. The configure gate's "Custom provider" option shares the same flow. | Keep our `onSelect` + import and the helper deletion; take upstream's changes to `ApiMethod`/other methods (the flow only wraps the entry point). |
| `packages/tui/src/component/prompt/index.tsx` | Gated the prompt footer's model/provider indicator on `useConnected()` (the shared "a real provider is configured" predicate) | Don't advertise the free hosted Zen default (e.g. "Big Pickle · OpenCode Zen") in the footer while nothing is configured — same rationale and predicate as `app.tsx`'s startup gate and the provider/model filters. A configured gateway/real provider, or a deliberately-authenticated opencode/opencode-go, still shows its model. | Keep the extra `Show` condition; take upstream's surrounding changes. |
| `packages/tui/src/component/use-connected.tsx`, `packages/tui/src/context/local.tsx` | use-connected.tsx: the provider predicate is extracted into an exported `isConnectedProvider`; `useConnected` now delegates to it (same logic). local.tsx: `fallbackModel`'s last resort is `sync.data.provider.find(isConnectedProvider) ?? sync.data.provider[0]` instead of a blind `sync.data.provider[0]` (covered by fork-only `test/component/use-connected.test.ts`, not tracked here). | The hosted Zen provider self-registers (free models need no auth) and is inserted ahead of config-only providers, so with no config `model` pin and empty recents (a fresh `codevhub install` — the pin was dropped in PR #142 / hub PR #221) the TUI defaulted to "Big Pickle · OpenCode Zen" and would send messages to upstream's hosted gateway instead of the configured one. Same predicate as `app.tsx`'s startup gate and the footer indicator; when no provider qualifies the old first-provider behavior is kept. | Keep the predicate export and the `find` fallback; take upstream's surrounding changes to either file. If upstream reworks `fallbackModel`, re-apply the connected-first selection to its last-resort branch. |
| `packages/tui/src/component/prompt/index.tsx` | The busy-state footer arm (`status().type !== "idle"`) also renders `props.hint` after the `esc interrupt` text, gated on `status().type !== "retry"` | The hidden-sidebar hint (see the `routes/session/index.tsx` hint row) only rendered in the footer `Switch`'s idle fallback arm, so it vanished for the whole duration of a running turn — exactly when todos are being created. Retry keeps its `space-between` error row uncluttered. | Keep the `<Show when={status().type !== "retry"}>{props.hint}</Show>` line in the busy `Match` arm (additive); take upstream's surrounding changes. |
| `packages/tui/src/component/prompt/index.tsx` | The busy-state footer arm (`status().type !== "idle"`) also renders `props.hint` after the `esc interrupt` text, gated on `status().type !== "retry"` | The hidden-sidebar hint (see the `routes/session/index.tsx` hint row) only rendered in the footer `Switch`'s idle fallback arm, so it vanished for the whole duration of a running turn — exactly when todos are being created. Retry keeps its `space-between` error row uncluttered. | Keep the `<Show when={status().type !== "retry" && showBusyHint()}>{props.hint}</Show>` line in the busy `Match` arm (additive); take upstream's surrounding changes. |
| `packages/tui/src/component/prompt/index.tsx` | Width-tiered busy footer: module-level `BUSY_FOOTER_PALETTE_MIN_WIDTH` (108) and `BUSY_FOOTER_HINT_MIN_WIDTH` (88) drive a `showPaletteHint` memo (`status().type === "idle" \|\| width >= 108`) wrapping the `<shortcut> commands` text and a `showBusyHint` memo (`width >= 88`) added to the busy arm's `props.hint` gate | The busy row carries spinner + interrupt hint + sidebar hint + context/cost; as the terminal narrows flexbox runs the gaps to zero (cramped but intact) and below ~85 columns the row wraps to two lines and clips the cost. Shedding by value keeps it on one line down to 70: the static palette hint goes first, then the modified-file count; the spinner and interrupt hint never go. | Keep both constants, both memos and the two `Show` gates; take upstream's surrounding changes. Re-measure the thresholds if segments are added to the row. |
| `packages/tui/src/component/prompt/index.tsx` | Removed the editor open-file label from the footer's right-side group and lifted the state behind it out of `Prompt`: the local `dismissedEditorSelectionKey` signal and the `editorContext`/`editorPath`/`editorSelectionLabel`/`editorFileLabel`/`editorFileLabelDisplay`/`editorContextLabelState` memos are gone, replaced by `useEditorLabel()` (fork-only `src/context/editor-label.tsx`) — `selection()` feeds the `prompt.editor_context.clear` `enabled` predicate and the submit-time editor part, `state()` replaces `editor.labelState()` there, and `dismiss()` replaces the local `dismissEditorContext`. The pure helpers `hasEditorRangeSelection`/`getEditorRangeLabel` moved to that context module and are imported back for `formatEditorContext`. | The label now lives in the sidebar footer (see its row above). The dismissal state had to move with it: the sidebar must label the *effective* selection — the one this file actually attaches on submit — or it would advertise context the prompt has dismissed and won't send. | Keep the deletions, the `useEditorLabel` import + its three call sites, and the helper import from `../../context/editor-label`; take upstream's surrounding changes. If upstream reworks the editor-context label, apply the change to `editorContextFileLabel` in the fork-only sidebar module rather than restoring the footer render. |
| `packages/tui/src/routes/session/index.tsx`, `packages/tui/src/component/prompt/index.tsx` | Left accent border removed entirely: the user-message block's `border={["left"]}`/`borderColor` (agent accent)/`customBorderChars` props and the prompt input box's two bordered boxes (main box with its `bottomLeft: "╹"` char, plus the height-1 `╹` terminator row) are replaced with `paddingLeft={1}`; the now-unused `borderHighlight` memo and the `tint`/`SplitBorder` imports are removed from the prompt (the `highlight`/`agentMetaAlpha` memos stay — they still color the agent name label); the fork's in-scrollbox banner box gains `paddingLeft={1}` so the banner/tips left edge lines up with the prompt/message blocks' background edge (the removed border glyph used to occupy that aligned column); `BlockTool` (expanded tool-output panels) likewise drops its `border={["left"]}`/`customBorderChars`/`borderColor` (the background-colored `┃` glyph over the panel background rendered as a stray vertical line on the panel's left edge) and bumps `paddingLeft` 2 → 3 to keep the content column | No vertical accent line next to sent messages, the prompt, or tool-output panels — removed rather than recolored to the background because background-colored glyphs render oddly on some terminals (e.g. PowerShell). `paddingLeft={1}` keeps the same 1-column offset, so text alignment with other transcript blocks is unchanged. | Keep our `paddingLeft={1}` boxes and the `borderHighlight`/`tint`/`SplitBorder` removals; take upstream's surrounding changes. If upstream adds new accent-colored left borders to these blocks, replace them with `paddingLeft={1}` the same way. |
| `packages/tui/src/component/spinner.tsx` | `SPINNER_FRAMES` swapped from cli-spinners "dots" (10 braille frames) to cli-spinners "sand" (35 braille frames, falling-sand animation); interval stays 80 ms | Fork visual identity for the shared `Spinner` component (in-progress tool/thinking rows, startup loading, session-list dialogs). | Keep our frames array; take upstream's surrounding changes. |
Expand Down
33 changes: 19 additions & 14 deletions packages/tui/src/component/prompt/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -63,16 +63,15 @@ import { useLocation } from "../../context/location"
registerOpencodeSpinner()

// "aesthetic" from sindresorhus/cli-spinners (MIT)
const FOOTER_FRAMES = [
"▰▱▱▱▱▱▱",
"▰▰▱▱▱▱▱",
"▰▰▰▱▱▱▱",
"▰▰▰▰▱▱▱",
"▰▰▰▰▰▱▱",
"▰▰▰▰▰▰▱",
"▰▰▰▰▰▰▰",
"▰▱▱▱▱▱▱",
]
const FOOTER_FRAMES = ["▰▱▱▱▱▱▱", "▰▰▱▱▱▱▱", "▰▰▰▱▱▱▱", "▰▰▰▰▱▱▱", "▰▰▰▰▰▱▱", "▰▰▰▰▰▰▱", "▰▰▰▰▰▰▰", "▰▱▱▱▱▱▱"]

// A busy footer carries the spinner, the interrupt hint and the sidebar hint on
// the left plus context/cost on the right. As the terminal narrows the row runs
// its gaps down to nothing and then wraps, so shed by value: the static palette
// hint first (idle-state discoverability, not live status), then the sidebar's
// modified-file count. The spinner and the interrupt hint always stay.
const BUSY_FOOTER_PALETTE_MIN_WIDTH = 108
const BUSY_FOOTER_HINT_MIN_WIDTH = 88

export type PromptProps = {
sessionID?: string
Expand Down Expand Up @@ -1339,6 +1338,10 @@ export function Prompt(props: PromptProps) {
FOOTER_FRAMES[frameIndex][charIndex] === "▰" ? color : inactive,
}
})
const showPaletteHint = createMemo(
() => status().type === "idle" || dimensions().width >= BUSY_FOOTER_PALETTE_MIN_WIDTH,
)
const showBusyHint = createMemo(() => dimensions().width >= BUSY_FOOTER_HINT_MIN_WIDTH)
const maxHeight = createMemo(() => tuiConfig.prompt?.max_height ?? Math.max(6, Math.floor(dimensions().height / 3)))
const moveLabelWidth = createMemo(() => Math.max(12, Math.min(44, dimensions().width - 48)))

Expand Down Expand Up @@ -1593,7 +1596,7 @@ export function Prompt(props: PromptProps) {
{store.interrupt > 0 ? "again to interrupt" : "interrupt"}
</span>
</text>
<Show when={status().type !== "retry"}>{props.hint}</Show>
<Show when={status().type !== "retry" && showBusyHint()}>{props.hint}</Show>
</box>
</Match>
<Match when={workspace.notice()}>
Expand Down Expand Up @@ -1677,9 +1680,11 @@ export function Prompt(props: PromptProps) {
</text>
</Match>
</Switch>
<text fg={theme.text}>
{paletteShortcut()} <span style={{ fg: theme.textMuted }}>commands</span>
</text>
<Show when={showPaletteHint()}>
<text fg={theme.text}>
{paletteShortcut()} <span style={{ fg: theme.textMuted }}>commands</span>
</text>
</Show>
</Match>
<Match when={store.mode === "shell"}>
<text fg={theme.text}>
Expand Down
Loading
Loading