| 2026-07-13 |

tui: theme repaцвет chat + bounded history + tail purge (Etap 4)
...
- apply_theme now re-renders already-drawn chat bubbles in the new palette:
ChatPanel.refresh_content() rebuilds each visible item's rich renderable (the
item renderers read get_active_theme(), already switched) and drops the
per-widget Content/height caches so the next paint rebuilds instead of
returning the old-theme cached Content. Theme switch / live preview no longer
leaves the conversation in the previous palette (5.B1).
- theme_picker live preview is debounced (~100 ms coalesce via set_timer):
apply_theme now re-renders every bubble, so applying it on every highlight
while scrolling the theme list would stutter — rapid moves collapse into one
apply, with the freshest theme winning. Escape/cancel dismiss the pending
timer so it cannot fire after a restore and re-apply the highlighted theme
over the restored one (4.P2).
- ChatModel.items is now bounded: a cap (visible window * 3) trims the oldest
items off the front via a single _append/_trim path, so a long autonomous
session no longer grows items without limit. The visible window is much
smaller than the cap, so trimming only drops already-off-screen items and the
truncation hint still reads correctly (3.P3).
- stream_end purges empty assistant/thinking bubbles off the tail (while-pop)
instead of a full-history list-comprehension copy — empty bubbles are created
at the end of the turn, so O(tail) suffices (1.P1).
Tests: apply_theme re-renders bubbles; theme picker debounces + escape cancels;
chat_model cap trims front / cap=None keeps everything; stream_end keeps
non-empty assistant + purges trailing empty thinking. Full suite: 946 passed, 1 skipped.
Eugene Sukhodolskiy
committed
11 hours ago
|

tui: async REST api client — stop blocking the event loop (Etap 3)
...
The terminal client's REST helpers were synchronous (httpx.Client); every
caller inside the Textual event loop (attach/switch/resume/profile/stop) blocked
the whole TUI for the duration of each network round-trip — the spinner froze,
input stopped responding, the screen did not repaint. Move the network layer to
async and a shared pooled client:
- api.py: all 7 helpers are now `async def` over a single lazily-created
httpx.AsyncClient (connection pooling) — awaits yield the loop while a request
is in flight, so the TUI never blocks on the network.
- tui_app.py + commands/builtin.py: every `api.*` call site is now `await`-ed
(all already ran in async workers / execute). _stop_stream_worker drops the
dead `iscoroutine` check (api.stop_session is async, the await is real).
- cli.py: _resolve_session_id and _handle_command await api.*; _run_raw wraps
the resolve + run in asyncio.run(_run_raw_async) so the sync click entrypoint
stays unchanged. Removed the now-unused _run_one_shot wrapper.
- sessions_picker.on_mount: list_sessions runs in a worker (run_worker) instead
of blocking on_mount — a slow/unreachable backend no longer hangs the modal;
the input is focusable immediately and the list populates when the request
returns.
Tests: every api mock across 6 client test files is now an `async def` fake
(test_tui_app, test_chat_panel, test_sessions_picker, test_terminal_client,
test_tui_export, test_input_box); the sessions picker tests use an async
_raise_not_found helper for the 404 path. Full suite: 939 passed, 1 skipped.
Eugene Sukhodolskiy
committed
11 hours ago
|

tui: throttle streaming markdown rebuild + cache height (Etap 2)
...
The assistant bubble re-parsed rich Markdown on every stream_delta and rich-rendered it
twice per delta (once to paint, once to measure height) — O(N^2) over the answer length.
Fix the cost while keeping live markdown formatting:
- _ChatItemView.maybe_update now throttles assistant_message rebuilds to ~one per 150 ms
(coalesced via set_timer). The ChatItem content still updates per delta; only the
Markdown parse + rich render is deferred. Plain renderables (thinking/planning are
Text) bypass the throttle — they are cheap. The first mutating delta rebuilds
immediately for a snappy start; rapid bursts coalesce.
- get_content_height is cached by (renderable identity, width) so the layout pass no
longer re-renders the rich renderable to measure it between throttled rebuilds.
- stream_end flushes any pending throttled rebuild so the final chunk of streamed text
appears immediately instead of after the throttle window.
- A deferred rebuild re-anchors scroll to the bottom (via ChatPanel._stick_to_bottom)
so the freshly rendered streaming text stays in view after its height grows.
- ThemedMarkdownRenderable.__rich_console__ now streams segments straight through
instead of materializing them into a list first (no per-paint O(N) allocation +
double walk); the link rewrite is inline in the loop.
Tests: adapted test_per_token_refresh_renders_only_the_streaming_item to the throttled
semantics (a burst yields a couple of rebuilds, not one per token, all for the assistant
item); added throttle-coalesces and stream_end-flushes tests. Full suite: 939 passed, 1 skipped.
Eugene Sukhodolskiy
committed
11 hours ago
|

tui: code-review Etap 1 — crash/injection/reconnect fixes + drop artifact renderer
...
Point fixes from the full TUI/terminal-client code review (docs/code_review_tui.md),
each with a covering test:
- command_palette: guard on_list_view_selected against IndexError when the
filter is empty and the placeholder row is selected.
- cli /profile: read session_id (not the absent "id") from the server — the
handler no longer KeyErrors.
- todo_list: escape task text/validation/index/milestone before interpolating
into rich markup so a task like "fix [bug]" no longer breaks the line styling.
- builtin /help: drop bold markup tags from status content — StatusRenderer
builds a literal Text() that would show the tags as literal square brackets.
- tui_app on_user_submitted: always enqueue when a bridge exists (the input loop
buffers across reconnect) instead of dropping the message on connected=False.
- chat_model stream_start: also reset _current_thinking so a dropped turn (no
thinking_end) does not glue the next turn's reasoning onto the old block.
- todo renderer update: coerce validation to str before .strip() so a non-string
JSON value does not AttributeError.
- renderers: remove the unused ArtifactRenderer (no production path emits
type=artifact — TUI shows files via the OS); keep the diff renderer test.
Full suite: 937 passed, 1 skipped.
Eugene Sukhodolskiy
committed
12 hours ago
|

tui: unify syntax highlighting under Theme.code_theme; highlight filesystem read output
...
All syntax-highlighted code in the TUI now resolves one Pygments style per
theme from a single point: Theme.code_theme (gnexus-dark=dracula,
gnexus-light=paraiso-light). Previously the dracula/github-light name was
duplicated in artifact.py and markdown_content.py, and the light theme's
"github-light" is not a real Pygments style (only github-dark exists), so
rich silently fell back to "default" and light-mode code was never
highlighted — fixed by switching to paraiso-light.
New renderers/syntax.py::highlight_code() is the shared Syntax factory
(always theme.code_theme + background_color=theme.surface.hex); artifact.py
and the new filesystem read path build through it, and
_theme_aware_code_theme now reads Theme.code_theme via ThemeRegistry.
filesystem FilesystemToolResultRenderer._render_read: the file body is now
syntax-highlighted via highlight_code with the language guessed from the
read path (guess_language). The server's "{num:>width}: {line}" prefix is
stripped (_strip_number_prefix) so Syntax renders its own line-number column
and multi-line constructs (triple-quoted strings, block comments) highlight
correctly across line boundaries; numbered=False renders without numbers.
The header plaque (accent path) and the large-file warning stay as Text;
body is Group(header, [warning,] Text, Syntax). Unknown extensions fall back
to the "text" lexer (plain).
Diff content keeps marker-only coloring (combining Pygments token styles
with the +/- tint is non-trivial); grep/list/info/find unchanged.
Tests: tests/clients/test_code_theme.py pins the single-point contract
(markdown/artifact/read all resolve theme.code_theme, dark != light);
test_filesystem_renderer read tests rewritten for the Group+Syntax body,
plus language-guess and numbered=False coverage. Full suite: 932 passed.
Eugene Sukhodolskiy
committed
13 hours ago
|

mcp: dedicated runner task per client (fix cross-task cancel-scope on shutdown)
...
MCP client SDK transports (stdio/sse/streamable_http) + ClientSession are
anyio task groups whose cancel scopes require __aenter__/__aexit__ in the
SAME asyncio task. McpClient entered the transport in one task (lifespan
connect / health-check reconnect / request retry) and exited it in another
(lifespan teardown) -> RuntimeError: Attempted to exit cancel scope in a
different task than it was entered in.
Refactor McpClient to own a single long-running runner task that holds the
AsyncExitStack and performs ALL transport enter/exit + list_tools/call_tool.
The public async API (connect/disconnect/list_tools/call_tool/mark_disconnected)
just enqueues a _Cmd and awaits a Future, so callers from any task no longer
cross cancel-scope boundaries. connected/instructions mirror from the runner
onto the instance to stay sync-readable. disconnect() enqueues a stop command
and awaits shield(runner) so teardown isn't interrupted by lifespan cancel.
Also call mcp_manager.stop_health_check() BEFORE disconnect_all() in
AppContainer.shutdown() so the health-check task cannot enqueue onto a
client whose runner is being torn down. mark_disconnected() is now async
(queued) and its manager caller updated.
Regression test: connect in one task, list_tools in a second, disconnect in
a third — the exact scenario that raised the RuntimeError before.
Eugene Sukhodolskiy
committed
15 hours ago
|

tui: copy chat to system clipboard via reading mode + selection fixes
...
Two ways to copy chat content out of the TUI now work:
1. In-app selection (Ctrl+C, OSC 52): _ChatItemView.render() now builds a
selection-aware Content from the rich renderable instead of letting Static
wrap a Panel into a non-selection-aware RichVisual — so the magenta
drag-select highlight actually renders and get_selection returns clean,
chrome-free text. rich Segment styles are converted to Textual Style
(Style.from_rich_style) so the selection_style merge doesn't crash, and
None segment styles are guarded. get_content_height is overridden to
compute auto-height straight from the rich renderable; without it the
width-keyed Content churned new Visual objects per width change and spread
the layout-settling cascade across refreshes, leaving a deferred scroll_end
at an intermediate max_scroll_y (chat no longer stuck to bottom). Theme
defines screen-selection-background/foreground so the highlight is visible.
2. Reading mode (Ctrl+R) for terminals without OSC 52 (e.g. GNOME Console,
TERM_PROGRAM=kgx, which silently ignores OSC 52 so Ctrl+C never reaches the
OS clipboard). Toggling the app's reading-mode class hides the chrome around
the chat (right status/todo column, input prompt, status bar, chat outer
border) and re-renders every message without its Panel borders via the
registry's new render_plain — so Shift+drag + Ctrl+Shift+C copies just the
conversation text to the system clipboard. ContentRenderer.render_plain
defaults to unwrapping Panel/Padding(Panel) to its body; assistant messages
and plan_ready override it to raw Text(content) so rich Markdown doesn't
re-wrap fenced code blocks in their own bordered Panels. ChatPanel tracks
reading mode and re-renders visible widgets in place (scroll preserved);
new streamed items pick up the mode on mount.
Help (/help) now lists keys and documents both copy paths.
Tests: render_plain parametrized across all item types, message renderer
plain tests, chat panel reading-mode toggle (borders off/on, scroll kept,
new items borderless), tui_app reading-mode chrome hide, plus the existing
selection suite. Full suite green (922 passed, 1 skipped).
Eugene Sukhodolskiy
committed
15 hours ago
|
tui: make chat content selectable/copyable without bubble chrome
...
Chat items render rich Panels (rounded borders + Navi/You titles), but the
base Widget.get_selection only extracts text from widgets whose render is
Text/Content -- Panels return None, so a drag-select over a message or tool
output copied nothing (the highlight showed the borders, but Ctrl+C yielded
no message content).
Override _ChatItemView.get_selection: re-render the item's renderable via
the app console at the widget width (line layout identical to the screen),
strip the outer panel border lines and the │ / │ gutters, and remap the
mouse offsets onto the clean content so a partial-line selection lands on
the right characters. Plain renderables (status, turn_meta, filesystem tool
output) have no chrome and map 1:1. Ctrl+C (Screen.copy_text) then copies
the clean text via OSC 52.
Eugene Sukhodolskiy
committed
17 hours ago
|
tui: restore tool args/metadata, assistant-then-tool order, turn_meta on resume
...
ChatModel.load_history rebuilds the resumed session so the chat renders the
same way as a live turn:
- Recover tool-call args via tool_call_id (the persisted tool message stores
only the result, not the call args) and forward the stored metadata. Without
this the filesystem renderer saw args={} -> action=None and fell back to a
plain, un-highlighted result -- the resume "no syntax highlighting" symptom.
- Emit assistant_message BEFORE tool_started when an assistant message has both
text and tool_calls, matching the live stream where stream_delta precedes
tool_started; previously the answer was stranded under its own tool cards.
- Append a turn_meta item when the persisted assistant message carries
elapsed_seconds, reproducing the single duration line stream_end appends.
is_subagent for resumed tool calls is intentionally deferred (needs storage).
Eugene Sukhodolskiy
committed
17 hours ago
|
tui: let the user scroll up while the agent streams + keyboard scroll bindings
...
ChatPanel._sync called scroll_end on every WS event (every stream_delta),
yanking the view back to the bottom on each token — impossible to scroll up
and read earlier messages during a response. Stick-to-bottom: capture
is_vertical_scroll_end before any DOM change and only scroll_end when the
user was already at the bottom; auto-follow resumes when they scroll back.
Add app-level keyboard bindings so the chat scrolls without leaving the
input box: Ctrl+Shift+Up/Down (one line), Ctrl+End (jump to bottom). These
keys are not claimed by TextArea (it uses shift+up/down for select and
ctrl+e/end for line end), so they bubble from the input to the app.
Eugene Sukhodolskiy
committed
18 hours ago
|
compressor: target hysteresis — shrink to 65% after compression, not just below the trigger
...
Compression triggered at 90% but left the context just under the trigger, so a
fixed keep_recent (navi_code: 12 turns ≈ 104 messages) re-triggered a couple
of messages later — the context yo-yoed at the trigger line instead of gaining
headroom. Add context_compression_target (0.65): in turn-based (preturn) mode
compress_context shrinks keep_recent until the verbatim kept region fits 65% of
the window, folding the extra turns into the same single summary LLM call; a
safety net in compress_session token-budget-truncates if the kept region alone
still exceeds the target (midturn kept a huge in-flight turn, or the
keep_recent floor can't fit). Trigger 90% → target 65% leaves real headroom.
Eugene Sukhodolskiy
committed
18 hours ago
|
filesystem: strip trailing newline in _number_diff so diff lines aren't blank-separated
...
_unified_diff (used by edit_lines / smart_edit) feeds difflib with
"line + \n" so difflib does not emit a "\ No newline at end of file" marker.
With lineterm="" difflib keeps that trailing "\n" on content lines, so the
final "\n".join produced "\n\n" between every diff line — a blank line
between each, bloating the model-facing output and rendering with double
spacing in both clients. Strip the trailing "\n" in _number_diff before
processing; no-op for the _diff action, whose lines come from splitlines()
without trailing newlines.
Eugene Sukhodolskiy
committed
18 hours ago
|
renderers: colour diff line numbers + read line numbers, keep content neutral
...
Standard for diff and read output: the line number (and the +/- marker for
diffs) is the coloured anchor; the content itself reads plainly.
diff: "{num} {marker} {content}" with num+marker in the marker colour
(green/red), content neutral. Fixed the number-column regex to "^( +)(\d+)|"
so files with more than 9 lines (width > 1) still match — the old
single-space pattern silently fell back to whole-line colouring.
read: "{num}: {content}" with the number in the accent colour, content
neutral (was dim number).
Applied to both clients: the TUI diff/filesystem renderers and the webclient
ToolCard (renderDiff / renderRead). The model-facing text from the server is
unchanged — this is display-only.
Eugene Sukhodolskiy
committed
18 hours ago
|
tui: sort milestone groups by status rank so finished ones sink
...
Todo widget already sorted steps within a milestone by status, but the
milestone groups themselves stayed in plan order — a done milestone sat
mid-list while its steps were pushed down. Group milestones by their
worst step status (in_progress < pending < failed/skipped/done) with a
stable sort that preserves plan order within equal rank; the unnamed
(legacy, no-milestone) group participates in the same ranking.
Eugene Sukhodolskiy
committed
18 hours ago
|

Автономность мелких моделей: OUTPUT DISCIPLINE, milestone-todo, перехват финала хода
...
Два последовательных захода над одной проблемой — мелкие модели (12–30B) в navi_code
теряют автономность на трудных шагах: «остановился поболтать» вместо действия и
слишком большое расстояние между пунктами плана.
Заход 1 (З1–З3):
- З1: OUTPUT DISCIPLINE в системном промпте navi_code — «act, don't announce»,
без few-shot антипримеров. Контракт хода: ответ без tool_calls = конец хода, поэтому
объявление намерения текстом убивает автономность; правила заставляют вызывать
инструмент в том же ходе.
- З2: плоский todo + метка группы milestone + декомпозиция. _parse_plan_steps
возвращает list[tuple[milestone, text]]; milestone — метка группировки (не сущность,
без статуса), «done» вычисляется при рендеринге; подшаги = больше плоских шагов
(без вложенности). TUI side-panel группирует по milestone (плоский фолбэк при пустом
milestone). Plan depth: max 15→20 + правило декомпозиции.
- З3: adaptive re-plan «длинный шаг» — nudge «разбей шаг» при in_progress ≥ порога
итераций без смены todo (порог 4, раньше общего anti-stall warning на 8).
Заход 2 (шаги 1–3, после cloud-теста 31b vs 12b):
Корневая структурная причина: весь спасательный механизм (anti-stall warning с явным
предложением reflect, adaptive re-plan) живёт только внутри tool-цикла — nudge
инжектируется в pre_turn *следующей* итерации, которой при «остановился поболтать»
нет (ход закрылся по return до post_turn). 31b застревала через «продолжаю
tool-итерации» → дожала до warning → спаслась; 12b — через «замолчала текстом» → мимо
всех nudge.
- Шаг 1: перехват финала хода. Если модель выдала bare-text, но в todo есть открытые
шаги (pending/in_progress) и лимит не исчерпан — НЕ эмитить StreamEnd, а сохранить
ассистентский текст в session.context, поставить системный nudge и continue
(без StreamEnd, без workers — консистентно с multi-iteration tool-турами). Счётчик
final_interceptions на AgentTurnContext, лимит final_intercept_limit (default 2),
эскалация жёсткости (мягкий → «second stop»). has_open_steps в todo.py: пустой
todo → False (защита casual-сообщений), failed/skipped терминальны. Профильные
флаги final_intercept_enabled/limit в base.py + loader + admin.
- Шаг 2: жёсткий reflect-триггер в промпте — «~3 tool attempts on the same step
without progress → call reflect IN THIS TURN (tool call, not reasoning aloud)».
- Шаг 3: открыть replan для застревания — «call replan when reflect showed the whole
approach is dead (not one failed step, but the approach itself)».
Тесты: 874 passed, 1 skipped. Новые — has_open_steps (5), final intercept (5),
milestone-группировка, adaptive long-step nudge, парсер шагов с milestone-маркером.
cloud: navi_code model → gemma4:31b-cloud для тестирования догадки (31b признала
застревание, 12b — нет); .env cloud-host уже gitignored.
Co-Authored-By: Claude <noreply@anthropic.com>
Eugene Sukhodolskiy
committed
20 hours ago
|
| 2026-07-12 |
config: raise context compression threshold 0.70 -> 0.90
...
Midturn compression now splits the in-flight turn correctly, so it's safe to
let the context grow closer to the limit before compressing. 0.90 gives the
model more raw context per turn and trips the compressor less often.
Worker test context_tokens bumped 50_000 -> 60_000 (76% -> 91.5%) to stay
above the new threshold; other compression tests pass the threshold as an
explicit argument and are unaffected.
Co-Authored-By: Claude <noreply@anthropic.com>
Eugene Sukhodolskiy
committed
1 day ago
|

filesystem: show real file line numbers in unified diffs
...
_number_diff parses @@ hunk headers and prefixes each content line with its
real file line number: removed/context lines use the old (from) number,
added lines use the new (to) number, in `{marker} {num}│ {content}` form
(marker first so existing startswith highlighting stays valid; column
right-aligned to the largest line number). Hunk/file headers and the
`\ No newline` marker are left unchanged.
Applied in _unified_diff (covers edit / edit_lines / smart_edit) and the
`diff` action — a single server-side change, so both the TUI and the web
client receive numbered diffs in the tool result text.
TUI highlight_unified_diff renders the `{marker} {num}│` column dim and the
content in the marker color, so the number reads as meta, not as part of the
added/removed text. Lines without the prefix (standalone `diff` event, older
callers) are still colored whole.
Co-Authored-By: Claude <noreply@anthropic.com>
Eugene Sukhodolskiy
committed
1 day ago
|

compressor: split in-flight turn in midturn mode + flush incrementally
...
Two related fixes for long autonomous turns in planning profiles (navi_code):
A1 — partition_messages turn-based branch held the in-flight (current) turn
verbatim when turns > keep_recent, so midturn compression was shallow
(e.g. 150 -> 141 messages). Now, when keep_recent_messages is set, the
in-flight turn is split like partition_current_turn_messages: head (user
request) + tail (recent tool steps) kept, middle summarized. The adaptive
swap is disabled in midturn mode (it could move the in-flight turn into
old_turns and summarize the current request whole). First branch
(len(turns) <= keep_recent) is untouched, so locked-in midturn tests hold.
B1 — agent.run_stream had no try/finally around the for-loop, so an
asyncio.CancelledError (server restart/shutdown) unwound the stack with no
flush: all in-memory turn messages (sequence_number < 0) were lost, only the
user message survived. Add incremental save() after planning, after the
assistant tool-call decision, and after each tool result, so a crash loses at
most the single in-flight tool call, not the whole turn. B2 (try/except
safety-net) was dropped: B1 leaves no window where an append is unsaved.
Tests: midturn split with many turns (partition + compress_session), and
crash/cancel persistence via a snapshot session store that mirrors the DB
boundary.
Co-Authored-By: Claude <noreply@anthropic.com>
Eugene Sukhodolskiy
committed
1 day ago
|

permissions: remove broken client-side gate; add backend design doc
...
The terminal-TUI permission gate was a race: _execute_tools_with_sink
emits ToolStarted and starts the tool on the backend immediately, so by
the time the client dialog appeared the destructive action had already
run -- 'deny' could only stop the session, not un-execute the tool. It
also existed only in the terminal TUI (webclient/android had nothing),
and the system-prompt 'Strict Confirmation' nudge was an unreliable
duplicate.
Remove the broken machinery for a clean slate and plan the real
replacement from zero:
- delete clients/terminal/tui/permissions.py, screens/permission_dialog.py
and their tests
- strip tui_app.py (engine, _deny_tool, _show_permission_dialog,
_confirm_shell_command, _stop_session_worker, on_permission_request,
tool_started gate) and the PermissionRequest TUI event; !cmd now runs
directly (user-typed, no agent gate)
- drop the 'Strict Confirmation' prompt rule from navi_code/developer/
tool_developer
- add docs/permissions.md: authoritative backend gate design (engine +
registry + event + endpoint + agent gate + postgres policy + global
rules config, sub-agents gated) -- design only, not yet implemented
- update docs/index.md, navi_code_cli.md, profiles.md, testing.md
Until Phase 1 lands there is no destructive-action confirmation -- a
conscious period without the false security of the old race-gate.
Eugene Sukhodolskiy
committed
1 day ago
|
replan: integrated mid-task re-planning tool
...
Add a replan tool that re-runs the planner over the live session context
+ todo + scratchpad when the plan's structure is stale due to discoveries
(NOT failed steps -- that stays [Adaptive re-plan]). Integrated approach:
PlanningEngine.run gains is_replan/replan_context (suppresses DIRECT
shortcut and observe-skip, frames Phase 1 as a revision); ReplanRunner
packs reason/goal/todo/findings/errors and captures PlanReady; the tool
is exposed to navi_code/developer/tool_developer via a
current_replan_runner ContextVar set per-iteration in run_stream (correct
after switch_profile). New plan replaces the todo. Lazy events import
breaks the navi.tools -> navi.core -> navi.tools cycle.
Eugene Sukhodolskiy
committed
1 day ago
|

recall: carry self-instruction (message) on the recall_update wire
...
The recall card could show call_type/trigger_at but not the self-instruction
(additional_context_message) — it was absent from RecallUpdate.to_wire, so
the user couldn't see what future-self was about to do at the scheduled or
fired moment. Extend the wire payload.
- events.RecallUpdate: add `message` field; to_wire emits "message".
- scheduler._publish_recall_update: accept and forward `message`.
- Publish sites carry message=recall.additional_context_message:
schedule_recall (scheduled) and orchestrator._finalize_recall
(rescheduled / fired / cancelled). manage_recall cancel/skip omit it
(no recall object handy; already visible in the prior scheduled card).
- TUI RecallRenderer: preview the message (first line + "(+N lines)",
capped at 80) on a `msg:` body line when present.
- Tests: RecallUpdate.to_wire carries message (defaults None); renderer
preview (single/multiline/truncate/empty) and scheduled-card renders it.
Co-Authored-By: Claude <noreply@anthropic.com>
Eugene Sukhodolskiy
committed
1 day ago
|

navi-code TUI: render schedule_recall lifecycle (recall_update)
...
The scheduler/orchestrator already push RecallUpdate (wire type
"recall_update") for the schedule_recall lifecycle, but the TUI had no
mapping or renderer — every event fell to the unknown-event catch-all and
rendered as a raw dict dump. Wire it into a dedicated card.
- chat_model: add a "recall" ChatItem kind and a recall_update branch
(before the catch-all) that keeps the wire fields in meta.
- chat_panel _item_msg: reconstruct {"type": "recall_update", **meta}
for a recall item so the registry sees it.
- renderers/recall.py: new RecallRenderer — a compact, color-coded card
per action (⏰ scheduled/info, ▶ fired·resuming/accent, ✕ cancelled/error,
↻ skipped/warning, ↻ rescheduled/info) showing call_type and a trimmed
trigger_at. Registered before the generic renderers.
- tests: 10 tests covering accepts, each action's title/border color,
trigger formatting, empty body, chat_model mapping, and _item_msg
round-trip.
MVP only — the self-instruction (additional_context_message) is not on the
wire, so it remains visible in the schedule_recall tool-call card above;
extending RecallUpdate.to_wire to carry it is a follow-up.
Co-Authored-By: Claude <noreply@anthropic.com>
Eugene Sukhodolskiy
committed
1 day ago
|
filesystem edit: render red/green diff in TUI without changing model context
...
edit is now the primary editing method, yet its output was a dry one-line
status ("Edited …: replaced X B with Y B") while edit_lines/smart_edit showed a
highlighted unified diff. Carry the unified diff in ToolResult.metadata["diff"]
(kept out of the model-facing output, so the agent's context is unchanged) and
render it in the TUI: dim summary line plus highlighted diff (green +, red -,
dim @@). Falls back to plain text when no diff metadata is present.
Co-Authored-By: Claude <noreply@anthropic.com>
Eugene Sukhodolskiy
committed
1 day ago
|
filesystem: make edit/edit_lines the default, smart_edit a last-resort fallback
...
Agent overused smart_edit (whole-file LLM call, weak context). Rewrite the
FilesystemTool description decision tree so edit (exact text) is the default and
edit_lines (by line numbers) is the deterministic option; smart_edit is reserved
for genuinely semantic changes that cannot be expressed as exact text or line
numbers. Strengthen the old_not_unique hint to steer back to edit/edit_lines
before smart_edit. Add an "Editing policy" section to the navi_code and
developer profile prompts reinforcing the same priority.
Co-Authored-By: Claude <noreply@anthropic.com>
Eugene Sukhodolskiy
committed
1 day ago
|
tui: move filesystem tool_started info into the card body
...
Previously the action and primary path were crammed into the title
("→ filesystem read src/main.py") and the body only held the extra args,
so for simple actions (info/delete/mkdir/exists) the card body was empty.
Title is now compact ("→ filesystem <action>"). The body always carries
the information: "path: <path>" (accent) leads, "destination: <dest>"
(accent) follows for move/copy/diff, then the per-action args (range/
content/old+new/operations/instruction/pattern/question …) as before.
The card is never empty.
Tests updated: title carries action only (path is in the body),
destination lands in the body for move/copy/diff, info shows path in body,
path value is styled accent. 42 filesystem-renderer tests total. Full
suite 827 passed, 1 skipped.
Co-Authored-By: Claude <noreply@anthropic.com>
Eugene Sukhodolskiy
committed
1 day ago
|

tui: styled filesystem tool_started card — compact args, action in title
...
The generic ToolStartedRenderer dumped all of args as JSON, so filesystem
cards showed "→ filesystem" with no action in the title and the full
content/old/new text for write/edit/smart_edit flooding the card.
New FilesystemToolStartedRenderer (clients/terminal/tui/renderers/filesystem.py):
- Title carries the action and primary path: "→ filesystem read src/main.py";
move/copy/diff also append "→ <destination>".
- Body is a compact per-action summary instead of JSON:
- read: range (offset–limit) + numbered flag
- write/append: content preview (first line + "(+N lines)" hint, not full)
- edit: old + new previews
- edit_lines: operations count + op kinds (replace/delete/insert, first 5)
- smart_edit: instruction preview
- grep: pattern + glob + regex flag
- find/find_up: pattern
- query: question preview
- list: recursive flag
- info/delete/mkdir/exists: empty body (path is in the title)
- Body keys render dim, values in text.
- Sub-agent cards indented, matching ToolStartedRenderer.
Registered before the generic ToolStartedRenderer (first-match wins). The
filesystem tool, WS protocol, and events are untouched.
13 new tests cover accepts, title (action/path/destination), empty body for
info, and per-action body summaries (read/write/edit/edit_lines/grep/
smart_edit/query), content preview truncation, subagent indent, and body key
styling. 42 filesystem-renderer tests total. Full suite 827 passed, 1 skipped.
Co-Authored-By: Claude <noreply@anthropic.com>
Eugene Sukhodolskiy
committed
1 day ago
|
tui: styled filesystem find + find_up output — stage 3
...
find: the "[N matches for 'pattern' in path ⠠ …]" header plaque is dim
(warning color on ⠠ truncated); "No matches for …" renders dim; each match
line "{path} ({size})" renders the path in text, " (" and the size in
dim, and the "<dir>" marker in info.
find_up: a resolved path renders in accent; "not found (searched: …)"
renders dim.
Fallback narrowed to write/edit/append/move/copy/delete/mkdir/exists/
query/unknown. 5 new tests (find header/matches/truncated/no-matches,
find_up found/not-found); 29 filesystem-renderer tests total. Full suite
814 passed, 1 skipped.
Co-Authored-By: Claude <noreply@anthropic.com>
Eugene Sukhodolskiy
committed
1 day ago
|

tui: styled filesystem read + info output — stage 2
...
read: the "[path | N lines | size]" header plaque is now dim with the
path highlighted in accent (offset/limit plaque "… lines A–B of N …"
likewise); the "⚠ Large file …" warning line renders in warning color; the
file body renders in text (was dim — readable now) with no syntax
highlighting (rejected by design); numbered reads dim the line numbers and
render line content in text. read errors (not_found, file_too_large) render
in tool_error.
info: each "key: value" line highlights the key (incl. colon) in accent,
the alignment padding in dim, and the value in text.
Fallback narrowed to write/edit/append/move/copy/delete/mkdir/exists/find/
find_up/query/unknown (statuses and find/query intentionally untouched).
Diff/list/grep from stage 1 unchanged.
6 new tests (read plaque/offset/warn/numbered/error, info keys+values);
24 filesystem-renderer tests total. Full suite 809 passed, 1 skipped.
Co-Authored-By: Claude <noreply@anthropic.com>
Eugene Sukhodolskiy
committed
1 day ago
|

tui: styled filesystem tool output (diff / list / grep) — stage 1
...
The TUI rendered every tool result as a single dim Text in a Panel — no
special handling for filesystem, so unified diffs from edit_lines/smart_edit/
diff were shown as plain grey text (the existing DiffRenderer only fires on
type:"diff", which filesystem never emits), directory listings were an
unaligned text blob, and grep matches had no pattern highlighting.
New FilesystemToolResultRenderer (clients/terminal/tui/renderers/filesystem.py)
intercepts filesystem tool_call events by tool=="filesystem" + args.action:
- diff / edit_lines / smart_edit: reuse a shared highlight_unified_diff()
helper (extracted from DiffRenderer) — + green, - red, @@ dim; the
"Applied …" summary line of edit_lines/smart_edit is separated from the
diff body via a Group; "Files are identical." renders dim.
- list: header plaque dim (warning color on ⚠ truncated); directories
shown as "▸ name/ (n items)" in info color; files as name (text) +
size (accent) + time (dim), preserving the tool's fixed-width alignment;
"?" entries in warning; "(empty directory)" dim.
- grep: header plaque dim; "rel:line:" location dim; matched pattern
occurrences highlighted in accent (case-insensitive, re.escape'd);
"No matches …" dim.
- read / info / write / edit / append / move / copy / delete / mkdir /
exists / find / find_up / query / unknown: plain dim fallback (unchanged
for now — stage 2). Errors (success=False): plain in tool_error color.
Registered before the generic ToolResultRenderer (first-match wins). The
filesystem tool, WS protocol, events, and webclient are untouched.
22 new tests in tests/clients/test_filesystem_renderer.py cover accepts,
diff/edit_lines/identical/error, list (dir/file/truncated), grep
(pattern/no-matches/no-pattern-arg), fallback, title+status, subagent
indent. DiffRenderer behavior unchanged (existing test still passes).
Co-Authored-By: Claude <noreply@anthropic.com>
Eugene Sukhodolskiy
committed
1 day ago
|
compression: profile-aware worker + real-token baseline estimator
...
Item 2 — thread the active profile into CompressionWorker so
compress_context applies per-profile overrides (compression_keep_recent,
compression_max_tokens, compression_prompt_file). navi_code now compresses
with keep_recent=12 instead of the global 8.
Item 1 — estimate the next LLM call's context from the *real* prompt_tokens
of the previous call (bulk) plus a heuristic delta for messages appended
since, replacing the chars//3 estimate that undercounts code-heavy tool
output and fired midturn compression too late (Navi kept working until the
window was exhausted). Baseline is recorded after each stream and cleared
after compression; check_context_size and the midturn gate use it, with a
heuristic fallback when no baseline exists or the context shrank.
Co-Authored-By: Claude <noreply@anthropic.com>
Eugene Sukhodolskiy
committed
1 day ago
|