Repository navigation
Conversation
The assistant footer only appears on the final message of a prompt and shows one number: the time since that prompt. Intermediate tool-call turns show no footer, so there is no way to see when a turn finished or which step of a long run took the time. Add a `turn_timing` option to tui.json with two independent booleans: - `time`: when each assistant turn finished, as the time of day if today and the date followed by the time otherwise (Locale.todayTimeOrDateFirst, e.g. `10/4/2026 11:26 PM`). Locale.todayTimeOrDateTime keeps its time-then-date order for its existing callers. - `duration`: how long each turn took (created -> completed). The final turn keeps the existing total since the prompt and labels it `total`, so both numbers are visible. Either option gives every completed assistant turn a footer. Both default to false, so the footer is unchanged unless enabled. /turn-times and /turn-durations (also in the command palette and bindable via session_toggle_turn_time / session_toggle_turn_duration) toggle them at runtime. Like /timestamps, a toggled value persists in kv.json and takes precedence over tui.json, which supplies the default.
Only prompts (/timestamps) and assistant turn footers (/turn-times, turn_timing.time) can show when something happened. Tool calls, thinking blocks, text blocks, errors and compactions never do, so a long run gives no way to see when a step finished or how long a tool call ran. Add a `timestamps` option to tui.json naming the entry types that show a timestamp: "all", "none", a comma-separated list such as "user,tool", or an array. The types are user, assistant, text, reasoning, tool, error and compaction. Tool calls show when they finished and how long they ran; thinking blocks put the time before their existing duration; text blocks and errors get a muted time line; the compaction divider gets the time in its title. All use Locale.todayTimeOrDateFirst, like the turn footer. Unset, nothing changes: /timestamps, /turn-times and turn_timing.time work and persist exactly as before. Set, the list decides every type on each start and overrides turn_timing.time; /timestamps and /turn-times still flip prompt and turn times, until the TUI exits. A kv-wins rule would make the option dead for most existing installs: kv.json usually already holds "timestamps", because the kv store persists its in-memory defaults alongside any other write. The JSON schema description documents every value, since opencode ships no commented tui.jsonc template.
Message footers name the model by its display name only, so the same model served through two providers (openai and a gateway clone, a direct API key and a proxy) reads identically, and nothing on screen says which provider answered a turn. model_label in tui.json picks the label: 'name' (default) keeps the display name; 'id' shows providerID/modelID, e.g. anthropic/claude-sonnet-4-5, in every assistant message footer and in the prompt's model indicator, where it replaces the model and provider names. Variant and duration stay as they are.
Assistant message footers name the model but not the variant it ran with, so a turn on high reasoning effort reads the same as one on the default. The variant is stored on every message and the prompt shows the current one, but nothing shows which one a past turn used. footer_variant in tui.json (default false) adds the message's variant, e.g. high or max, after the model in assistant message footers. Turns without a variant are unchanged. Documented in tui.mdx.
A long session can only be moved through by line, page or the whole transcript. messages_next/messages_previous exist but are unbound, and they skip any prompt within 10 rows of the viewport top, because they compare against a fixed threshold rather than the row they land on. Assistant parts render without an id the scroll position could be matched against. Bind three levels of navigation by default, all overridable in tui.json: - messages_previous / messages_next (ctrl+up / ctrl+down) stop on every user prompt, now through the same picker as the two below. - messages_landmark_previous / messages_landmark_next (ctrl+shift+up / ctrl+shift+down) stop on prompts, the last text part of each turn (its final response), and question, todowrite and task calls. Edits, reads and shell calls are left out because they are as dense as the blocks themselves. - messages_block_previous / messages_block_next (ctrl+alt+shift+up / ctrl+alt+shift+down) stop on every prompt, text part, reasoning part with content, and tool call. Reasoning without content - an encrypted block or an empty "Thinking" placeholder, which GPT models emit on every step - renders as a bare header and is skipped. Text, reasoning and tool boxes now carry their part id so the session route can find them among the scrollbox children, as it already does for user messages by message id. "Previous" and "next" are relative to the row a navigated block lands on, one below the viewport top, so repeated presses walk block by block; past either end the view goes to the top or back to the bottom. messages_first and messages_last also bind ctrl+home and ctrl+end.
Message footers grew one setting per detail, each with its own value shape: turn_timing.time/duration are booleans, timestamps is a type list, footer_variant is a boolean, and the agent, model and total have no setting at all. There was also no way to see a message's ID, which is what logs, the database and the API key everything by. footer in tui.json has one key per detail - agent, model, variant, time, duration, total and the new message_id - and every key takes the same value: true/"all", false/"none", "important", or a list of entry types from the timestamps vocabulary (user, assistant, text, reasoning, tool, error, compaction), which may include "important". "important" selects the landmarks ctrl+shift+up/down stops on, reusing navigationTargets(..., "landmark"); a message-level entry counts when the message or one of its parts is one (isLandmark in navigation.ts). A type a key does not apply to has no effect, which each key's schema description lists. message_id shows the msg_ ID at the end of user and assistant footers as plain text, so it can be selected with the mouse to copy. Time, duration and message_id also give a turn's earlier steps a footer once they finish, as turn_timing did. Every tool call takes time, duration and message_id the same way, whatever renders it - built-in, generic, plugin or MCP tools. A block tool puts them on its own last line and an inline row after its text, or on its own line when the arguments span lines, so a long prompt or command cannot push them out of sight; a block that ends in a "Click to expand" hint carries them on the hint line. A tool call shows its message's ID only when that message's own footer does not, so each ID appears once. While a tool call runs, its footer shows when it started and a stopwatch in whole seconds driven by one shared 1s clock that stops when nothing runs; once finished it shows the end time and the duration with tenths of a second up to 11 minutes. A prompt's footer sits one blank line below its text. The footer details are all muted except the agent name, the variant included. datetime_format takes a date and a time pattern in Day.js tokens (YYYY-MM-DD, HH:mm:ss, h:mm A ...) for every time the TUI writes: footers, entry timestamps, the compaction divider and the timeline and stash dialogs. Entries from today still show only the time, and an unset part keeps the locale output. Each part accepts only its own tokens, so "HH:MM:SS" is rejected rather than printed with the month in place of the minute. Times already on screen switch to the date and time at local midnight by themselves: the today check reads one shared day signal, driven by a single timeout that fires after midnight or within the hour. Unset keys keep today's behavior: turn_timing, footer_variant and timestamps map onto footer.time, footer.duration and footer.variant (footerSelections in config/index.tsx), and /timestamps, /turn-times and /turn-durations persist as before. A key set in footer decides on every start and its toggle lasts until exit, as timestamps already did. Defaults render the upstream footer unchanged.
…rn toggles `turn_timing.time` and `turn_timing.duration` were read after the kv.json values /turn-times and /turn-durations save, so after one use of either toggle a later `turn_timing` in tui.json was silently ignored, while the toggle kept persisting as if nothing decided it. `turn_timing` set in tui.json now decides turns on every start, the same way `footer` and the legacy `timestamps` list already do: /turn-times and /turn-durations then last until opencode exits and say so in a toast. `footerConfigured` answers per element and entry type, so a set `turn_timing.time` claims turn times without making /timestamps (prompt times) session-only. Session-only footer toggles also survive leaving and reopening a session now.
How much of a tool's output a block shows was fixed: 10 lines for bash, 3 for generic (plugin, MCP) tools, and the whole file or diff for write, edit and apply_patch, which had no way to fold at all. A long write or a chatty plugin call pushed the conversation off screen. tool_output.collapse in tui.json takes a number of lines or "never" per group (bash, generic, write, edit, apply_patch) or per tool name, a name winning over its group (toolOutputLimit). 0 hides the output until clicked, leaving the title, a blank line and the hint with the footer. The character cap stays at that many lines' worth, so long lines still fold. write, edit and apply_patch gain the expand toggle: ClippedRows shows the first N rendered rows of the code or diff and reports when it hides some. Unset keys keep the upstream limits, and the defaults render the transcript unchanged.
…remembered hide "Show/Hide generic tool output" saves its choice to kv.json and is off by default, and while it is off plugin and MCP output is not drawn at all. A `tool_output.collapse` limit for `generic` or for a tool's name therefore did nothing on a machine where that toggle was ever off: the limit had no output to collapse, and nothing said why. Naming `generic` or a tool in `tool_output.collapse` now shows that output on every start, collapsed to its limit, so 0 leaves the response one "Click to expand" away (packages/tui/src/config/index.tsx `genericToolOutputShown`). With `generic` set, the command then lasts until opencode exits and says so in a toast. With neither set the remembered toggle decides, as upstream draws it.
Contributor
|
The following comment was made by an LLM, it may be inaccurate: |
A long bash command, heredocs included, fills the transcript: only its output collapses. `tool_input.collapse` takes the same values as `tool_output.collapse` (rows, 0 or "never", per group or tool name) and cuts the command after that many rendered rows with the `ClippedRows` used by write and edit, so wrapping stays as it is. The default is "never", so the transcript is unchanged unless it is set. The command and the output keep separate limits; one click expands both, with the same hint and footer as output. `tool_output.tail` (per group `bash`/`generic` or tool name, default off) shows the last lines of collapsed output under "(N earlier lines)", using the `collapseTail` preview from `dev`'s `util/collapse-tool-output.ts`. `tool_output.head` (default 0) keeps that many first lines above a "(N more lines)" marker, and the last lines fill the rest of the collapse limit: collapse 5 with head 2 shows 2 first lines, the marker and 2 last lines. Collapse 0 still hides the output until clicked. `tool_output.collapse`, `tool_input.collapse`, `tool_output.tail` and `tool_output.head` name their groups as schema properties beside the per-tool record, so the JSON schema documents them.
Nowaker
force-pushed
the
tui-command-collapse
branch
from
October 14, 2026 16:52
1483f9f to
e8abdfb
Compare
1 task done
Author
|
Closing: this targets the v1 TUI, which upstream now maintains for critical fixes only, so it is not going to be merged. The branch stays on Nowaker/opencode if anyone wants to pick it up. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Note: I'm aware v1 is now the maintenance branch and new features go to v2 only. This PR is here for others to use, and I'll submit the v2 version once I've migrated my whole plugin system to it.
Issue for this PR
Closes #55420
Builds on #54039 (
tool_output.collapse), which builds on #53877; their commits come first. Review only the last commit.Type of change
What does this PR do?
A bash call's output collapses, but its command never does, so a long command or one with heredocs takes all its rows.
tool_input.collapsein tui.json takes the same values astool_output.collapse: per group (bash) or tool name, a number of rendered rows,0, or"never"(default). The command is cut with theClippedRowsthat write and edit already use, so it wraps exactly as before and only its height is capped.0hides it until clicked.tool_output.collapse.bashstill decides the output's lines. One click expands both, with the same hint and footer.tool_output.tail(per groupbash/genericor tool name, default off) shows the last lines of collapsed output under(N earlier lines)instead of the first. It usescollapseTailasdevhas it inutil/collapse-tool-output.ts.tool_output.head(per group or tool name, default0) keeps that many first lines above a(N more lines)marker whiletailis on, and the last lines fill the rest of the collapse limit:collapse5 withhead2 shows 2 first lines, the marker and 2 last lines.headis capped at the limit minus 2 so a last line always shows. A collapse of0still hides the output until clicked, whatevertailandheadsay; the docs spell that out.tool_output.collapse,tool_input.collapse,tool_output.tailandtool_output.headname their groups as schema properties beside the per-tool record, so the JSON schema documents them.With nothing set the transcript is unchanged.
How did you verify your code works?
bun typecheckin packages/tui.bun testin packages/tui: 243 pass; 7 diff viewer tests failed under a load average of 55 and pass when their files run alone (29 pass). Config decoding and precedence tests,collapseTail/collapseHeadTail/collapseOutputtests, and a frame test that a command clipped to 2 rows shows exactly the first 2 rows it wraps to at 40, 80 and 160 columns."tool_input": {"collapse": {"bash": 2}}long commands and heredoc commands show 2 rows and Click to expand, 2-row commands show no hint; a click shows the whole command and every heredoc line, a second click folds it back. With"tool_output": {"tail": {"bash": true}}a 40-line output shows(31 earlier lines)and lines 32-40. With"collapse": {"bash": 5}, "tail": {"bash": true}, "head": {"bash": 2}at 80 and 160 columns, the 40-line output shows lines 1-2,(36 more lines)and lines 39-40; a click shows all 40 lines and a second click folds it back. Withcollapse0 and the sametailandheadthe output stays hidden. With{}commands show in full as before.Screenshots / recordings
"tool_input": {"collapse": {"bash": 1}}:"tool_output": {"tail": {"bash": true}}:"tool_output": {"collapse": {"bash": 5}, "tail": {"bash": true}, "head": {"bash": 2}}:Checklist
This PR is AI generated, but under direct supervision and on request of @Nowaker.