Skip to content

feat: set terminal tab title to skin symbol (⚕ / ⚕ ⏳) - #4834

Closed
iRonin wants to merge 17 commits into
NousResearch:mainfrom
iRonin:feat/terminal-title
Closed

feat: set terminal tab title to skin symbol (⚕ / ⚕ ⏳)#4834
iRonin wants to merge 17 commits into
NousResearch:mainfrom
iRonin:feat/terminal-title

Conversation

@iRonin

@iRonin iRonin commented Apr 3, 2026

Copy link
Copy Markdown
Contributor

Sets the terminal window/tab title when Hermes starts and updates it as the session progresses.

Tab title format:

State Title
Idle
Agent thinking ⚕ ⏳

The symbol comes from the active skin's response_label branding so it adapts to the theme (⚕ default, ⚔ Ares, etc.).

OSC sequences used:

  • OSC 1 — tab/icon title (iTerm2 uses this as the tab label without appending the process name)
  • OSC 2 — window title bar

Using OSC 1 specifically prevents iTerm2 from appending Python to the tab label, which happens with OSC 0.

Updated at:

  • run() startup
  • /title command (when a title is set or committed from pending)
  • _preload_resumed_session() on resume
  • process_loop when agent starts (⚕ ⏳) and finishes ()

Guards: skipped when stdout is not a TTY, TERM=dumb, or NO_COLOR is set.

iRonin added 15 commits April 3, 2026 15:43
Adds two features to the Hermes CLI:

Ctrl+G — External Editor:
- Opens current input in $VISUAL / $EDITOR / VS Code / Cursor / vi
- Smart paste detection: if input contains a collapsed paste reference
  [Pasted text #N → path], opens that file directly for editing
- Uses run_in_terminal() for clean TUI suspend/resume
- Updates input buffer and paste line count on editor close

/keys (/shortcuts) — Keyboard Shortcuts Display:
- Categorized list of all keybindings (Input, Session, Drafting, Voice)
- Reads voice key from config for accurate display
- Registered in CommandDef with tab completion
…icators

Adds a Claude Code-style input stash to the Hermes CLI:

- Ctrl+S stashes current input (text + attached images) and clears the field
- Ctrl+S on empty input pops the stash back
- Stashed input auto-restores after the agent finishes responding
- Placeholder shows stash preview when idle, hint when agent is running
- Status bar shows a pinned indicator when a stash is active
- Uses (text, [images]) tuple so dragged/pasted images are preserved

Alternative to NousResearch#4259 with additional features: auto-restore after
response (the key UX from Claude Code), image stashing, placeholder
preview, status bar indicator, and proper buf.reset() cleanup.

Closes NousResearch#4255
Input starting with / is only routed to the command handler when the
first word matches a known command (via resolve_command). Bare paths
like /Users/ironin/file.md:45-46 now pass through as regular input
to the agent instead of triggering 'Unknown command'.

Fixes both the process_loop routing and the handle_enter interrupt
bypass — both had the same startswith('/') assumption.
Alt+Enter now queues the current input as a follow-up to be sent after
the agent finishes responding, instead of inserting a newline.

- Alt+Enter → puts message into _pending_input (non-interrupting)
- Enter (agent running) → still interrupts via _interrupt_queue
- _followup_queue list mirrors pending items for display
- Status bar shows 📬 N when follow-ups are queued
- Placeholder hints update: shows queue depth while agent runs,
  and persists after it finishes until queue drains
- Ctrl+J remains the newline key for multi-line input

Closes: the need for Shift+Enter queue (terminals can't distinguish
Shift+Enter from Enter; Alt+Enter is the reliable alternative)
Alt+Up pops the most recently queued follow-up (LIFO) from
_followup_queue, appends it to the current input with a newline---
separator, and marks it cancelled so process_loop skips it.

Repeated Alt+Up recalls one at a time until queue is empty.
_cancelled_followups set is checked in process_loop and discarded
on match to avoid sending the recalled message twice.
hermes -c "session name" -m anthropic/claude-sonnet-4-6 now works.
Previously -m was only on 'hermes chat', so the shorthand root-level
-c flag couldn't be combined with a model override.

Also stop stomping args.model/provider with None in the root→chat
passthrough — the values from the root parser are now preserved.
Addresses review feedback from britrik (NousResearch#4788):

- Replace text-based cancellation with UUID tags — identical messages
  queued twice no longer cancel each other incorrectly
- Wrap Alt+Enter payloads as {_followup_tag, payload} dicts so
  process_loop can identify followup items by ID, not content
- Fix phantom _followup_queue pops: display sync now only happens
  for tagged (Alt+Enter) items, not regular Enter messages
- _cancelled_followups stores UUIDs (bounded, auto-discarded on match)

Note: the image-payload cancel check was already correct in the original
— both sides extracted text via payload[0] — but UUID tagging makes the
intent unambiguous regardless of payload shape.
…dicator

Sets the terminal title via OSC 0 escape sequence (\x1b]0;...\x07):

  ⚕ Hermes — session name   (named session, idle)
  ⚕ Hermes ⏳                (agent thinking)
  ⚕ Hermes                   (unnamed session)

Symbol comes from the active skin's response_label (⚕ default,
⚔ Ares, etc.) so it adapts to the current theme.

Updated at:
- run() startup
- _preload_resumed_session() when a titled session is resumed
- /title command when a title is set or committed from pending
- process_loop when agent starts (thinking=True) and finishes

Skipped when stdout is not a TTY, TERM=dumb, or NO_COLOR is set.
The old '📨 Queued:' label after an interrupt was misleading —
it made it look like the message was silently deferred, when actually:
1. Enter interrupted the running agent
2. The message was immediately re-queued to send next

New labels:
  '⚡ Sending after interrupt: ...'          (single message)
  '⚡ Sending N messages after interrupt: ...' (multiple combined)

Also added a comment clarifying this block is only reached when
busy_input_mode == 'interrupt' (the default). In 'queue' mode,
Enter routes to _pending_input directly so this code never runs.
Three input UX improvements:

1. ESC ESC — clear input buffer (and attached images)
   Pressing ESC twice quickly discards the current draft without
   conflicting with Alt key sequences (escape+enter, escape+up, etc.)

2. Ctrl+P — peek collapsed paste content inline
   When input contains a [Pasted text #N → path] reference, prints
   the first 20 lines in the terminal so the user can verify content
   without opening an editor (Ctrl+G). Falls back to previewing the
   current input text when no paste reference is present.

3. \r\n normalisation in handle_paste
   Windows-style (CRLF) and old Mac-style (CR) line endings are
   normalised to LF before the 5-line collapse threshold is checked.
   Prevents markdown pasted from Windows sources being treated as
   single-line and bypassing the file-reference collapse.
Ctrl+P is now context-aware:
  - paste ref in input → peek first 20 lines of paste file inline
  - text in input     → preview first 20 lines of current input
  - empty input       → full conversation pager (newest first, via less)

New show_history_full() method:
  - Reverses conversation order so most recent message is at the top
  - No truncation — full text of every user and assistant turn
  - Tool call names listed inline on the header line
  - Strips REASONING_SCRATCHPAD blocks
  - Pipes through 'less -R --no-init --quit-if-one-screen'
    (falls back to plain print if less is unavailable)
  - Header shows message count and keyboard hints (q, /)

New /history full (aliases: f, all):
  - Calls show_history_full() from the slash command interface
  - /history (no arg) still calls the existing show_history()
- Drop 'Hermes' and session name from tab title — symbol only
- Use OSC 1 (tab/icon title) + OSC 2 (window title) instead of OSC 0
  so iTerm2 does not append the Python process name to the tab label
- Thinking indicator: ⚕ ⏳ (was ⚕ Hermes ⏳)
- Idle: ⚕ (was ⚕ Hermes / ⚕ Hermes — session)
@iRonin
iRonin force-pushed the feat/terminal-title branch from 96b63f9 to 003f44a Compare April 3, 2026 19:44
… proxy

Inside the TUI, sys.stdout is patched by prompt_toolkit's patch_stdout.
OSC escape sequences written to StdoutProxy are buffered or discarded
and never reach the terminal emulator — so /title didn't update the tab
and Python still appeared as the process name.

Fix: use sys.__stdout__ (the pre-patch original) and write via os.write()
directly to the file descriptor, bypassing the proxy entirely.
@iRonin
iRonin force-pushed the feat/terminal-title branch from 003f44a to 86b3d41 Compare April 3, 2026 19:57
Users on tmux/screen, or whose iTerm2 profile appends the job name
(Python) to the tab title, can now disable OSC title sequences:

  display:
    terminal_title: false

Default: true (enabled).
@teknium1

teknium1 commented Apr 4, 2026

Copy link
Copy Markdown
Contributor

Thanks for the work here @iRonin — the terminal tab title feature itself is well done (good OSC 1/2 usage, patch_stdout bypass, config opt-out, TTY guards).

However, this PR bundles 17 commits across 8+ unrelated features into a single PR: Ctrl+G editor, Ctrl+S stash, Alt+Enter follow-up queuing, Ctrl+P history pager, Ctrl+D behavior change, double-ESC clear, -m/--model flag changes, and the tab title feature. There's also a package-lock.json that doesn't belong in this repo.

All of these touch cli.py, which is a ~7400 line critical file — reviewing bundled changes to it safely isn't feasible.

We need atomic PRs: one feature per PR. If you split the tab title commits (acc9712f, 09db976f, 86b3d419, 0c1c4c05) into their own PR against current main, we'd be happy to merge that quickly. Same for the other features — several look interesting on their own, they just need to be separate PRs so we can evaluate and merge them independently.

@teknium1 teknium1 closed this Apr 4, 2026
@iRonin

iRonin commented Apr 4, 2026

Copy link
Copy Markdown
Contributor Author

@teknium1 sorry about that, Hermess messed up lol 💩

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.

2 participants