Skip to content

feat(tui): add footer to choose every message footer detail the same way - #53877

Closed
Nowaker wants to merge 6 commits into
anomalyco:devfrom
Nowaker:tui-footer-elements
Closed

Nowaker wants to merge 6 commits into
anomalyco:devfrom
Nowaker:tui-footer-elements

Conversation

@Nowaker

@Nowaker Nowaker commented Oct 8, 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: #42498, #38666, #50891 (finished tool calls only; no live timer while a call runs).

Stacked on #53195 (turn timing), #53654 (timestamps), #53639 (model_label), #53848 (footer_variant) and #53333 (landmark navigation), because it unifies their settings. Their commits are included; the change in this PR is the last commit, feat(tui): add footer to choose every message footer detail the same way.

Type of change

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

What does this PR do?

Message footers show a handful of details - agent, model, variant, finish time, duration, total - and each one is switched on its own way: some are booleans, timestamps is a list of entry types, and some can't be switched at all. Tool calls only get a time when the right setting is set, and on a plugin or MCP tool with a long multi-line argument the time ends up at the end of the last line, easy to miss. There is also no way to see a message's ID, which is what logs, the database and the API use, and no way to change how times and dates are written: they always come out in the runtime's locale, which Bun resolves to en-US (2:20 PM, 10/7/2026) whatever LANG says.

This adds one footer object to tui.json with one key per detail: agent, model, variant, time, duration, total and the new message_id. Every key takes the same value:

  • "all" / true, or "none" / false;
  • "important" - only on the entries ctrl+shift+up/down stops on (prompts, each turn's final response, question/todo/subagent calls), reusing that navigation's landmark list;
  • a list of entry types from the timestamps vocabulary (user, assistant, text, reasoning, tool, error, compaction), which may include "important".
{
  "footer": {
    "variant": "all",
    "time": "important,tool",
    "duration": "assistant,tool",
    "message_id": ["user", "assistant", "tool"]
  }
}
  • tool covers every tool call, whatever renders it: built-in, generic, plugin and MCP tools. A block tool shows its footer on its own last line, or on its Click to expand hint line when it has one; an inline row shows it after its text, or on its own line when the arguments span lines.
  • While a tool call runs, its footer shows when it started and a stopwatch in whole seconds (16:25:29 · 37s, 2m 5s), all running calls sharing one 1s clock that stops when none is running. Once finished it shows the end time and the duration, with tenths of a second up to 11 minutes (4.8s, 2m 5.3s, then 11m 0s).
  • A prompt's footer (time, message ID) sits one blank line below the prompt's text, unless the text already ends with a blank line.
  • Each message ID appears once: a tool call shows its message's ID only when the message's own footer doesn't (message_id: "tool", or "important" leaving a plain step's footer out).
  • message_id is plain text, so it can be selected with the mouse and copied.
  • A key a detail doesn't apply to has no effect; each key's schema description and the docs table list which types it applies to (total only exists on a turn's final message, for example).
  • All footer details are muted except the agent name.

A separate datetime_format sets the date and time patterns, in Day.js tokens, for every time the TUI writes (footers, entry timestamps, the compaction divider, the timeline and stash dialogs). Entries from today still show only the time. Each part accepts only its own tokens, so HH:MM:SS is rejected instead of printing the month as the minute:

{ "datetime_format": { "date": "YYYY-MM-DD", "time": "HH:mm:ss" } }

gives 14:20:07 today and 2026-10-07 14:20:07 for older entries. At local midnight, times already on screen switch to the date-and-time form by themselves; one shared timer drives this, firing after midnight or within the hour, so a suspend or clock change is caught too.

Nothing changes unless footer or datetime_format is set. The older keys keep working and map onto it: turn_timing.time/duration, footer_variant and timestamps. /timestamps, /turn-times and /turn-durations still toggle prompt times, turn times and turn durations; for a key set in footer the toggle lasts until exit, as it already did for timestamps.

How did you verify your code works?

  • bun typecheck and bun test in packages/tui, with new tests for value parsing, the legacy mapping, "important" resolution, the message-ID-once rule, the shared ticker starting and stopping, the stopwatch and decimal rules, the prompt footer gap, the tool footer formatter, the multi-line row, datetime_format tokens and validation, and a fake clock crossing midnight flipping a rendered footer with a single shared timer.
  • An isolated TUI (scratch config/data dirs, throwaway tmux socket) against a fake OpenAI-compatible provider, with a plugin tool vtps that takes a multi-line prompt and returns enough output to collapse. With no footer the transcript matches dev; the legacy keys render as before. Clicking the hint still expands and collapses the block.

Screenshots / recordings

footer set to "all" for every key, datetime_format YYYY-MM-DD / HH:mm:ss, generic tool output shown:

  ┃  Delivery log line 3.
  ┃  …
  ┃
  ┃  Click to expand · 16:13:02 · 1.5s

     ▣  Build · QA Model · 16:13:02 · 3.0s · msg_11d5cbdb6001Y7BvuwpNg3jjhA
     + Thought: 16:13:03 · 151ms
     All footers checked.
     16:13:03
     ▣  Build · QA Model · 16:13:03 · 1.3s · 4.4s total · msg_11d5cc98d001WzgzZfn9ab8KoJ

  ┃  second prompt
  ┃  16:13:12 · msg_11d5cf048001t1Th9GIpHBia6q

  ┃  # Todos
  ┃
  ┃  [✓] check footers
  ┃
  ┃  16:13:13 · 11ms

     ▣  Build · QA Model · high · 16:13:13 · 1.1s · msg_11d5cf098001nOJGO4X12JoZrJ

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. The footer details are all
muted except the agent name, the variant included.

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.
@opencode-agent opencode-agent Bot closed this Oct 8, 2026
@opencode-agent

opencode-agent Bot commented Oct 8, 2026

Copy link
Copy Markdown
Contributor

Thanks for this, @Nowaker. The single footer setting is a clear idea, and the before/after transcript makes it easy to follow.

v1 (the dev branch) only takes critical fixes now: data loss, security problems, or crashes or breakage with no workaround. So this feature won't be merged there, and I'm closing this pull request. It can stay here for anyone who wants to use it.

As you mentioned, the right place for it is the v2 branch. v2 is a separate implementation, so the change will probably need to be redone rather than ported. We'd welcome a v2 pull request once you've migrated.

@github-actions

github-actions Bot commented Oct 8, 2026

Copy link
Copy Markdown
Contributor

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

Nowaker added a commit to Nowaker/opencode that referenced this pull request Oct 8, 2026
Item 24: the footer setting, its legacy mapping, the muted variant and
the tool-call footer (1ca196f, efc7b3d, 4c6be13), PR anomalyco#53877,
and the desktop and m4max installs and tui.json settings. Timeline rows
link the turn timing, timestamps, footer variant, model label and
block navigation features this changed.

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 23a3dfa
AI-Session-ID: ses_ee68aab01ffei41gwUGJ5ESj6W
Nowaker added a commit to Nowaker/opencode that referenced this pull request Oct 8, 2026
…int line

Item 24 follow-ups (42ce394): the datetime_format setting, a tool
call showing its message ID only when the message footer does not, and
the footer on a block tool's expand hint line; PR anomalyco#53877 branch and
body, the desktop and m4max installs and their tui.json 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 fcbe738
AI-Session-ID: ses_ee68aab01ffei41gwUGJ5ESj6W
Nowaker added a commit to Nowaker/opencode that referenced this pull request Oct 8, 2026
…er gap

Item 24 follow-ups (ea714f8): a running tool call shows its start
time and a whole-second stopwatch from one shared clock, finished
durations keep tenths up to 11 minutes, and a prompt's footer sits one
blank line below its text; PR anomalyco#53877 branch and body, and the desktop
and m4max installs through the new v1 database guard gate.

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
Item 24: the footer setting, its legacy mapping, the muted variant and
the tool-call footer (1ca196f, efc7b3d, 4c6be13), PR anomalyco#53877,
and the desktop and m4max installs and tui.json settings. Timeline rows
link the turn timing, timestamps, footer variant, model label and
block navigation features this changed.

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 23a3dfa
AI-Session-ID: ses_ee68aab01ffei41gwUGJ5ESj6W
Nowaker added a commit to Nowaker/opencode that referenced this pull request Oct 11, 2026
…int line

Item 24 follow-ups (42ce394): the datetime_format setting, a tool
call showing its message ID only when the message footer does not, and
the footer on a block tool's expand hint line; PR anomalyco#53877 branch and
body, the desktop and m4max installs and their tui.json 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 fcbe738
AI-Session-ID: ses_ee68aab01ffei41gwUGJ5ESj6W
Nowaker added a commit to Nowaker/opencode that referenced this pull request Oct 11, 2026
…er gap

Item 24 follow-ups (ea714f8): a running tool call shows its start
time and a whole-second stopwatch from one shared clock, finished
durations keep tenths up to 11 minutes, and a prompt's footer sits one
blank line below its text; PR anomalyco#53877 branch and body, and the desktop
and m4max installs through the new v1 database guard gate.

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 12, 2026
Item 24 follow-up (c642043): today checks read a shared local-day
signal, so footers and timestamps already on screen switch to the date
and time at 00:00 from one timer; PR anomalyco#53877 branch and body, the
restacked tui-tool-collapse branch, and the desktop and m4max installs.

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