Skip to content
Merged
Show file tree
Hide file tree
Changes from 18 commits
Commits
Show all changes
90 commits
Select commit Hold shift + click to select a range
db4e4ac
perf(tui): stabilize long-session scrolling
OutThisLife Apr 26, 2026
14fcff6
style(tui): apply formatter
OutThisLife Apr 26, 2026
458ce79
fix(tui): persist model switches by default
OutThisLife Apr 26, 2026
19d75d1
perf(tui): coalesce composer echo updates
OutThisLife Apr 26, 2026
9bb3bc4
perf(tui): optimistically echo simple input
OutThisLife Apr 26, 2026
5cd41d2
perf(tui): widen native input echo
OutThisLife Apr 26, 2026
ee7ef33
fix(tui): queue busy submissions gracefully
OutThisLife Apr 26, 2026
cd7c5e5
perf(tui): defer local input render during echo
OutThisLife Apr 26, 2026
1c964ed
fix(tui): rely on native cursor for input
OutThisLife Apr 26, 2026
355e0ae
fix(tui): keep streaming progress stable during interaction
OutThisLife Apr 26, 2026
3811210
fix(tui): address review feedback
OutThisLife Apr 26, 2026
bbd950e
fix(tui): keep stream cadence responsive while typing
OutThisLife Apr 26, 2026
8f0fa08
fix(tui): preserve composer width on narrow panes
OutThisLife Apr 26, 2026
bc17310
fix(tui): smooth selection drag behavior
OutThisLife Apr 26, 2026
7d68ea9
fix(tui): stream legacy thinking deltas visibly
OutThisLife Apr 26, 2026
e16e196
fix(tui): keep selection drag responsive
OutThisLife Apr 26, 2026
5ac4088
fix(tui): keep live progress visible while scrolling
OutThisLife Apr 26, 2026
7143d22
fix(tui): keep queued sends in queue UI
OutThisLife Apr 26, 2026
a0aebad
fix(tui): anchor details to stream timeline
OutThisLife Apr 26, 2026
2e6c3c7
fix(tui): address follow-up review nits
OutThisLife Apr 26, 2026
05dc2ee
fix(tui): tighten timeline detail spacing
OutThisLife Apr 26, 2026
d91e245
fix(tui): attach inline diffs to tool timeline
OutThisLife Apr 26, 2026
6814646
fix(tui): avoid duplicating flushed stream text
OutThisLife Apr 26, 2026
a8fcd1c
fix(tui): apply details mode live
OutThisLife Apr 26, 2026
cc16d0e
Merge remote-tracking branch 'origin/main' into bb/tui-long-session-perf
OutThisLife Apr 26, 2026
4a21920
fix(tui): address copilot review nits
OutThisLife Apr 26, 2026
bb59d3b
fix(tui): preserve completed thinking panel
OutThisLife Apr 26, 2026
015f6c8
fix(tui): support modified enter for multiline input
OutThisLife Apr 26, 2026
2be5e18
fix(tui): keep thinking color theme-neutral
OutThisLife Apr 26, 2026
a8bfe72
fix(tui): address latest review feedback
OutThisLife Apr 26, 2026
c9f7b70
fix(tui): filter thinking status noise
OutThisLife Apr 26, 2026
a30ffbe
fix(tui): show queued prompts when drained
OutThisLife Apr 26, 2026
7b5b524
refactor(tui): clean thinking and viewport helpers
OutThisLife Apr 26, 2026
3d21f97
fix(tui): keep live tool state before stream segments
OutThisLife Apr 26, 2026
350ee1b
refactor(tui): render progress in ordered stream timeline
OutThisLife Apr 26, 2026
d4dde6b
fix(tui): restore resumed transcript lineage
OutThisLife Apr 26, 2026
a7831b6
fix(tui): stabilize live progress rendering
OutThisLife Apr 26, 2026
3271ffb
fix(tui): pin todo panel above live output
OutThisLife Apr 26, 2026
cf84392
fix(tui): keep todo pinned outside transcript
OutThisLife Apr 26, 2026
cee4036
fix(tui): merge tool shelves in transcript
OutThisLife Apr 26, 2026
64de685
test(tui): remove stale turn freeze experiment
OutThisLife Apr 26, 2026
6a38739
fix(tui): format thinking paragraphs
OutThisLife Apr 26, 2026
f684620
fix(tui): isolate turn state from app render
OutThisLife Apr 26, 2026
a30db69
chore(tui): clean live progress lint
OutThisLife Apr 26, 2026
1566f1e
fix(tui): report actual session on exit
OutThisLife Apr 26, 2026
f5552f9
fix(tui): stabilize live todo progress
OutThisLife Apr 26, 2026
a5319fb
test(tui): cover live todo completion flow
OutThisLife Apr 26, 2026
4d3e3a7
chore(tui): sort imports
OutThisLife Apr 26, 2026
4943ea2
fix(tui): merge tools into contextual shelves
OutThisLife Apr 26, 2026
319c1c1
fix(tui): inline todo in transcript, group across thinking
OutThisLife Apr 26, 2026
c78b528
feat(tui): archive todos at turn end with incomplete hint
OutThisLife Apr 26, 2026
b36007b
feat(tui): allow collapsing archived todo panels
OutThisLife Apr 26, 2026
bde89c1
fix(cli): -c picks the most recently used session
OutThisLife Apr 26, 2026
debae25
perf(tui): incremental markdown during streaming
OutThisLife Apr 26, 2026
cb7cfba
fix(cli): surface last_active in search_sessions so -c works
OutThisLife Apr 26, 2026
2259eac
feat(tui): collapse completed todo panel on turn end
OutThisLife Apr 26, 2026
69ff201
feat(tui): anchor todo panel above streaming output
OutThisLife Apr 26, 2026
71eee26
perf(tui): full-pipeline instrumentation + profiling harness
OutThisLife Apr 26, 2026
cd7a200
perf(tui): instrument scroll fast-path decline reasons
OutThisLife Apr 26, 2026
7242361
fix(tui): wrap streaming markdown split in column Box
OutThisLife Apr 26, 2026
4a9070c
perf(tui): defer Md upgrade for fresh-mounted assistant rows
OutThisLife Apr 26, 2026
7ca16ee
perf(tui): scroll one row at a time per wheel event, half-viewport pe…
OutThisLife Apr 26, 2026
d3dedf1
revert(tui): drop DeferredMd, profiling showed it was neutral
OutThisLife Apr 26, 2026
f823535
perf(tui): instrument stdout drain — rule out terminal parse bottleneck
OutThisLife Apr 26, 2026
82f8422
perf(tui): profile harness gains --loop, --save, --compare
OutThisLife Apr 26, 2026
8d2b083
Add node-inspect-debugger and python-debugpy skills
OutThisLife Apr 26, 2026
9a46feb
experiment(tui): HERMES_TUI_INLINE flag to skip AlternateScreen
OutThisLife Apr 26, 2026
4cdb696
Add hermes-agent-skill-authoring skill
OutThisLife Apr 26, 2026
0cd9849
Promote debugging-hermes-tui-commands to in-repo skill
OutThisLife Apr 26, 2026
4395c2b
feat(tui): port claude-code's wheel accel state machine
OutThisLife Apr 26, 2026
85e9a23
feat(tui): HERMES_TUI_FPS=1 shows live fps counter
OutThisLife Apr 26, 2026
c370e2e
perf(tui): cache stringWidth/wrapText/sliceAnsi + skip-slice when lin…
OutThisLife Apr 27, 2026
2576751
perf(tui): unified Ink cache eviction on memory pressure + session reset
OutThisLife Apr 27, 2026
b115ea6
feat(tui): anchor LiveTodoPanel to latest user message row
OutThisLife Apr 27, 2026
527ac35
fix(tui): address Copilot review comments
OutThisLife Apr 27, 2026
b1c49d5
chore(tui): /clean recent perf work — KISS/DRY pass
OutThisLife Apr 27, 2026
7da2f07
Merge remote-tracking branch 'origin/main' into bb/tui-long-session-perf
OutThisLife Apr 27, 2026
2e4b65b
chore(tui): clean remaining Ink perf scaffolding
OutThisLife Apr 27, 2026
dda1277
fix(tui): address Copilot review follow-ups
OutThisLife Apr 27, 2026
625c31f
fix(tui): run built TUI with production React by default
OutThisLife Apr 27, 2026
b51c528
fix(tui): address virtual row and perf log review notes
OutThisLife Apr 27, 2026
c2ca02f
fix(tui): stabilize live todo panel count and anchor position
OutThisLife Apr 27, 2026
635948d
chore(tui): tighten todo-fix comments, drop dead archive call
OutThisLife Apr 27, 2026
ffa33e5
chore(tui): remove dead branch cleanup code
OutThisLife Apr 27, 2026
7945fce
Merge branch 'main' of github.com:NousResearch/hermes-agent into bb/t…
OutThisLife Apr 27, 2026
d81b1cd
chore: uptick
OutThisLife Apr 27, 2026
de790ea
test(tui): align viewport snapshot key test with quantization
OutThisLife Apr 27, 2026
c23463f
chore(tui): keep MRU resume split out of perf PR
OutThisLife Apr 27, 2026
3e16649
Revert "fix(tui): report actual session on exit"
OutThisLife Apr 27, 2026
d308ae2
fix(nix): refresh tui npm deps hash
OutThisLife Apr 27, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions tests/test_tui_gateway_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -347,6 +347,34 @@ def test_complete_slash_includes_provider_alias():
assert any(item["text"] == "provider" for item in resp["result"]["items"])


def test_complete_slash_includes_tui_details_command():
resp = server.handle_request(
{"id": "1", "method": "complete.slash", "params": {"text": "/det"}}
)

assert any(item["text"] == "/details" for item in resp["result"]["items"])


def test_complete_slash_details_args():
resp_root = server.handle_request(
{"id": "0", "method": "complete.slash", "params": {"text": "/details"}}
)
resp_section = server.handle_request(
{"id": "1", "method": "complete.slash", "params": {"text": "/details t"}}
)
resp_mode = server.handle_request(
{
"id": "2",
"method": "complete.slash",
"params": {"text": "/details thinking e"},
}
)

assert resp_root["result"]["replace_from"] == len("/details")
assert any(item["text"] == "thinking" for item in resp_section["result"]["items"])
assert any(item["text"] == "expanded" for item in resp_mode["result"]["items"])


def test_config_set_reasoning_updates_live_session_and_agent(tmp_path, monkeypatch):
monkeypatch.setattr(server, "_hermes_home", tmp_path)
agent = types.SimpleNamespace(reasoning_config=None)
Expand Down
75 changes: 75 additions & 0 deletions tui_gateway/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -3710,6 +3710,65 @@ def _(rid, params: dict) -> dict:
return _ok(rid, {"items": items})


def _details_completion_item(value: str, meta: str = "") -> dict:
return {"text": value, "display": value, "meta": meta}


def _details_completions(text: str) -> list[dict] | None:
if not text.lower().startswith("/details"):
return None

stripped = text.strip()
if stripped and not "/details".startswith(stripped.lower().split()[0]):
return None

body = text[len("/details"):]
if body.startswith(" "):
body = body[1:]
parts = body.split()
has_trailing_space = text.endswith(" ")
sections = ("thinking", "tools", "subagents", "activity")
modes = ("hidden", "collapsed", "expanded")

if not body or (len(parts) == 0 and has_trailing_space):
return [
*[_details_completion_item(mode, "global mode") for mode in modes],
_details_completion_item("cycle", "cycle global mode"),
*[_details_completion_item(section, "section override") for section in sections],
Comment thread
OutThisLife marked this conversation as resolved.
Outdated
]

if len(parts) == 1 and not has_trailing_space:
prefix = parts[0].lower()
candidates = [*modes, "cycle", *sections]
return [
_details_completion_item(
candidate,
"section override" if candidate in sections else "global mode",
)
for candidate in candidates
if candidate.startswith(prefix) and candidate != prefix
]
Comment thread
OutThisLife marked this conversation as resolved.

if len(parts) == 1 and has_trailing_space and parts[0].lower() in sections:
return [
*[_details_completion_item(mode, f"set {parts[0].lower()}") for mode in modes],
_details_completion_item("reset", f"clear {parts[0].lower()} override"),
]

if len(parts) == 2 and not has_trailing_space and parts[0].lower() in sections:
prefix = parts[1].lower()
return [
_details_completion_item(
candidate,
f"clear {parts[0].lower()} override" if candidate == "reset" else f"set {parts[0].lower()}",
)
for candidate in (*modes, "reset")
if candidate.startswith(prefix) and candidate != prefix
]

return []


@method("complete.slash")
def _(rid, params: dict) -> dict:
text = params.get("text", "")
Expand Down Expand Up @@ -3742,6 +3801,11 @@ def _(rid, params: dict) -> dict:
"display": "/compact",
"meta": "Toggle compact display mode",
},
{
"text": "/details",
"display": "/details",
"meta": "Control agent detail visibility",
},
{
"text": "/logs",
"display": "/logs",
Expand All @@ -3753,6 +3817,17 @@ def _(rid, params: dict) -> dict:
item["text"] == extra["text"] for item in items
):
items.append(extra)

details_items = _details_completions(text)
if details_items is not None:
return _ok(
rid,
{
"items": details_items,
"replace_from": text.rfind(" ") + 1 if " " in text else len(text),
Comment thread
OutThisLife marked this conversation as resolved.
},
)

return _ok(
rid,
{"items": items, "replace_from": text.rfind(" ") + 1 if " " in text else 1},
Expand Down
24 changes: 12 additions & 12 deletions ui-tui/packages/hermes-ink/src/ink/components/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ import {
FOCUS_IN,
FOCUS_OUT
} from '../termio/csi.js'
import { DBP, DFE, DISABLE_MOUSE_TRACKING, EBP, EFE, HIDE_CURSOR, SHOW_CURSOR } from '../termio/dec.js'
import { DBP, DFE, DISABLE_MOUSE_TRACKING, EBP, EFE, SHOW_CURSOR } from '../termio/dec.js'

import AppContext from './AppContext.js'
import { ClockProvider } from './ClockContext.js'
Expand Down Expand Up @@ -206,10 +206,9 @@ export default class App extends PureComponent<Props, State> {
)
}
override componentDidMount() {
// In accessibility mode, keep the native cursor visible for screen magnifiers and other tools
if (this.props.stdout.isTTY) {
this.props.stdout.write(HIDE_CURSOR)
}
// Keep the native terminal cursor visible. Ink parks it at the declared
// input caret after each frame, so the terminal emulator provides the
// normal blinking block/bar without React-driven blink re-renders.
}
override componentWillUnmount() {
if (this.props.stdout.isTTY) {
Expand Down Expand Up @@ -470,7 +469,7 @@ export default class App extends PureComponent<Props, State> {
}

if (this.props.stdout.isTTY) {
this.props.stdout.write(HIDE_CURSOR + EFE)
this.props.stdout.write(EFE)
}

this.inputEmitter.emit('resume')
Expand Down Expand Up @@ -569,18 +568,19 @@ function processKeysInBatch(app: App, items: ParsedInput[], _unused1: undefined,

/** Exported for testing. Mutates app.props.selection and click/hover state. */
export function handleMouseEvent(app: App, m: ParsedMouse): void {
// Allow disabling click handling while keeping wheel scroll (which goes
// through the keybinding system as 'wheelup'/'wheeldown', not here).
if (isMouseClicksDisabled()) {
return
}

const sel = app.props.selection
// Terminal coords are 1-indexed; screen buffer is 0-indexed
const col = m.col - 1
const row = m.row - 1
const baseButton = m.button & 0x03

// Allow disabling app click/selection handling while keeping wheel scroll
// and DOM mouse dispatch alive. Put this after coordinate/button decoding
// and exempt non-left buttons so scrollbar/right-click handlers still work.
if (isMouseClicksDisabled() && baseButton === 0) {
return
}

if (m.action === 'press') {
if ((m.button & 0x20) !== 0 && baseButton === 3) {
if (app.mouseCaptureTarget) {
Expand Down
30 changes: 14 additions & 16 deletions ui-tui/packages/hermes-ink/src/ink/components/ScrollBox.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,19 @@ function ScrollBox({ children, ref, stickyScroll, ...style }: PropsWithChildren<
})
}

const scrollByNow = (dy: number) => {
const el = domRef.current

if (!el) {
return
}

el.stickyScroll = false
el.scrollAnchor = undefined
el.pendingScrollDelta = (el.pendingScrollDelta ?? 0) + Math.floor(dy)
scrollMutated(el)
}

useImperativeHandle(
ref,
(): ScrollBoxHandle => ({
Expand Down Expand Up @@ -155,22 +168,7 @@ function ScrollBox({ children, ref, stickyScroll, ...style }: PropsWithChildren<
}
scrollMutated(box)
},
scrollBy(dy: number) {
const el = domRef.current

if (!el) {
return
}

el.stickyScroll = false
// Wheel input cancels any in-flight anchor seek — user override.
el.scrollAnchor = undefined
// Accumulate in pendingScrollDelta; renderer drains it at a capped
// rate so fast flicks show intermediate frames. Pure accumulator:
// scroll-up followed by scroll-down naturally cancels.
el.pendingScrollDelta = (el.pendingScrollDelta ?? 0) + Math.floor(dy)
scrollMutated(el)
},
scrollBy: scrollByNow,
scrollToBottom() {
const el = domRef.current

Expand Down
Loading
Loading