feat: set terminal tab title to skin symbol (⚕ / ⚕ ⏳) - #4834
Conversation
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)
96b63f9 to
003f44a
Compare
… 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.
003f44a to
86b3d41
Compare
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).
|
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, All of these touch We need atomic PRs: one feature per PR. If you split the tab title commits ( |
|
@teknium1 sorry about that, Hermess messed up lol 💩 |
Sets the terminal window/tab title when Hermes starts and updates it as the session progresses.
Tab title format:
⚕⚕ ⏳The symbol comes from the active skin's
response_labelbranding 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 barUsing OSC 1 specifically prevents iTerm2 from appending
Pythonto the tab label, which happens with OSC 0.Updated at:
run()startup/titlecommand (when a title is set or committed from pending)_preload_resumed_session()on resumeprocess_loopwhen agent starts (⚕ ⏳) and finishes (⚕)Guards: skipped when stdout is not a TTY,
TERM=dumb, orNO_COLORis set.