Skip to content

feat(tui): choose how much tool output shows before "Click to expand" - #54039

Closed
Nowaker wants to merge 7 commits into
anomalyco:devfrom
Nowaker:tui-tool-collapse
Closed

Nowaker wants to merge 7 commits into
anomalyco:devfrom
Nowaker:tui-tool-collapse

Conversation

@Nowaker

@Nowaker Nowaker commented Oct 9, 2026

Copy link
Copy Markdown

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

Related: #40096, #45538, #51229.

Stacked on #53877 (footer), which it builds on for the Click to expand · <time> · <duration> hint line; that PR is in turn stacked on #53195, #53654, #53639, #53848 and #53333. Their commits are included; the change in this PR is the last commit, feat(tui): choose how much tool output shows before "Click to expand".

Type of change

  • Bug fix
  • New feature
  • Refactor / code improvement
  • Documentation

What does this PR do?

How much of a tool's output the transcript shows is fixed: 10 lines for bash, 3 for plugin and MCP tools, and the whole file or diff for write, edit and apply_patch, which can't be folded at all. A long write or a chatty plugin call pushes the conversation off screen, and there is no way to keep tool calls to one line each.

This adds tool_output.collapse to tui.json: a number of lines, or "never", per group or per tool name.

{
  "tool_output": {
    "collapse": {
      "bash": 0,
      "generic": 10,
      "edit": 20,
      "vibeterm_prompt_session": 0
    }
  }
}
  • Groups: bash (default 10), generic - plugin, MCP and other tools without their own view (default 3), write, edit, apply_patch (default "never"). A tool name overrides its group.
  • A number shows that many lines, then … and Click to expand. The character cap stays at that many lines' worth, so long single lines still fold.
  • 0 hides the output until clicked: the block is its title or command, a blank line, and the hint.
  • "never" always shows everything, with no hint.
  • write, edit and apply_patch gain the expand toggle: collapsed, they show the first N rendered rows of the code or diff. A deleted file in a patch is already one line and never collapses.

Unset keys keep today's limits, and with no tool_output the transcript renders exactly as before.

How did you verify your code works?

  • bun typecheck and bun test in packages/tui, with new tests for the resolver (defaults, tool name over group, 0, "never", invalid values), the 0 case of the collapser, and rendering clipped rows collapsed, expanded and at 0.
  • An isolated TUI (scratch config/data dirs, throwaway tmux socket) against a fake OpenAI-compatible provider running bash, write, edit, apply_patch (on a gpt-* model) and a plugin tool. With no tool_output, a full transcript of all of them matches the build without this change line for line. Clicking a collapsed edit expands it to its diff and clicking again collapses it.

Screenshots / recordings

Every group at 0, with footer time and duration on and datetime_format HH:mm:ss:

  ┃  $ seq 1 15
  ┃
  ┃  Click to expand · 18:30:50 · 1.2s

  ┃  # Wrote notes.txt
  ┃
  ┃  Click to expand · 18:30:52 · 497ms

  ┃  ← Edit notes.txt
  ┃
  ┃  Click to expand · 18:33:17 · 149ms

  ┃  # vtps [session=ses_fake, prompt=hello]
  ┃
  ┃  Click to expand · 18:35:09 · 496ms

  ┃  # Created tmp/qa/runs/patch/proj/added.txt
  ┃
  ┃  Click to expand · 18:41:33 · 28ms

write, edit and bash at 3:

  ┃  # Wrote notes.txt
  ┃
  ┃    1 line 1
  ┃    2 line 2
  ┃    3 line 3
  ┃
  ┃  Click to expand

Checklist

  • I have tested my changes locally
  • I have not included unrelated changes in this PR

This PR is AI generated, but under direct supervision and on request of @Nowaker.

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.

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.
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.
@github-actions

github-actions Bot commented Oct 9, 2026

Copy link
Copy Markdown
Contributor

The following comment was made by an LLM, it may be inaccurate:

@opencode-agent opencode-agent Bot closed this Oct 9, 2026
@opencode-agent

opencode-agent Bot commented Oct 9, 2026

Copy link
Copy Markdown
Contributor

Thanks for this, @Nowaker. Being able to set how much tool output shows before "Click to expand", and to fold write/edit/apply_patch, is a nice improvement, and the description and examples are very clear.

As you mentioned, dev (v1) only takes critical fixes, so I'm closing this here. When you get to the v2 version, please open it against the v2 branch. v2 is a separate implementation, so the change will probably need reworking for its TUI rather than a straight port. It would also help to send it as its own PR, without the stacked footer, timestamp and navigation commits, so it can be reviewed on its own.

Nowaker added a commit to Nowaker/opencode that referenced this pull request Oct 9, 2026
…ck to expand"

Item 24 follow-up (9799e18): tool_output.collapse per group or tool
name, 0 hiding output until clicked, write/edit/apply_patch expand
toggles, upstream defaults unchanged; PR anomalyco#54039, the desktop and m4max
installs and their tui.json collapse settings.

AI-Tool: opencode 1.18.34-vt-163-907b3bc518
AI-Model: anthropic/claude-opus-5-5
AI-Variant: high
AI-Platform: linux
AI-Harness: Vibeterm ab1db14
AI-Session-ID: ses_ee68aab01ffei41gwUGJ5ESj6W
Nowaker added a commit to Nowaker/opencode that referenced this pull request Oct 11, 2026
…ck to expand"

Item 24 follow-up (9799e18): tool_output.collapse per group or tool
name, 0 hiding output until clicked, write/edit/apply_patch expand
toggles, upstream defaults unchanged; PR anomalyco#54039, the desktop and m4max
installs and their tui.json collapse settings.

AI-Tool: opencode 1.18.34-vt-163-907b3bc518
AI-Model: anthropic/claude-opus-5-5
AI-Variant: high
AI-Platform: linux
AI-Harness: Vibeterm ab1db14
AI-Session-ID: ses_ee68aab01ffei41gwUGJ5ESj6W
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant