| 2026-07-11 |
Navi Code TUI: context-window fill gauge in the status bar
...
Add a ContextFill widget to the bottom StatusBar showing how full the LLM
context window is (absolute tokens + percent), colored by fill level:
dim < 70%, warning 70-89%, error >= 90%. Updates from stream_end and
compression_started/context_compressed events; seeded from get_session on
resume so the gauge isn't blank pre-turn. None from the backend keeps the
last known value.
Fix: name the widget attribute _ctx_fill, not _context — _context shadows
MessagePump._context (the internal context manager Textual wraps around
message processing) and hangs the app's message pump at mount time.
Polish: drop the "ctx " prefix (shows 12.0k/32.0k · 38%) and add a 1-char
margin between the activity indicator and the gauge.
Co-Authored-By: Claude <noreply@anthropic.com>
Eugene Sukhodolskiy
committed
20 days ago
|
tui: load session message history into the chat panel on resume
...
Resuming or switching a session showed a blank chat — the past conversation
was never replayed. GET /sessions/{id} already returns the messages; the TUI
just ignored them.
- ChatModel.load_history maps persisted Message dicts back to the same
ChatItems the live stream produces (user/assistant text, thinking, tool
started/result, plan), skipping is_display=False (context-only user,
summaries, compression events) so only what the user would have seen
live is rebuilt.
- ChatPanel.load_history wraps it (clears the render cache + refresh).
- attach_session loads the messages from get_session, so resume/switch
replays the history before the cwd/Connected banners.
Co-Authored-By: Claude <noreply@anthropic.com>
Eugene Sukhodolskiy
committed
20 days ago
|

tui: activity indicator in a bottom status bar + 1-line input
...
Add an animated "agent is working" indicator so a silent gap (long-running
tool, the pause before the first token, sub-agent reasoning) reads as
"alive" rather than "hung": a braille spinner + phase label that runs on
its own timer, independent of the WebSocket event flow.
- New ActivityIndicator widget (rotating spinner + phase label, colored
status_online), started/stopped from tui_app WS phase transitions
(thinking / responding / planning / running <tool>) and stopped on
stream_end/stopped/error and on connection drop.
- New StatusBar bottom line replaces Textual's Footer: indicator on the
left, dim key-combo summary on the right. One line instead of the menu;
the key bindings themselves stay on the App so the keys still work.
- Input box: TextArea min-height 3 -> 1, so the prompt is one line by
default (auto-resize + max-height 12 unchanged).
Co-Authored-By: Claude <noreply@anthropic.com>
Eugene Sukhodolskiy
committed
20 days ago
|
tui: drop unused Tokens/Iter rows from the status panel
...
set_tokens/set_iterations were never called and the two rows permanently
showed "-", so they were dead noise in the right column. Remove the rows
and their setters.
Co-Authored-By: Claude <noreply@anthropic.com>
Eugene Sukhodolskiy
committed
20 days ago
|

tui: add --resume <session_id> and print resume hint on TUI close
...
Closing Navi Code now tells the user the session id and the exact command to
continue it, so a session is one re-run away instead of lost to state lookup.
- cli.py: add --resume <session_id> (mutex with --new-session). Raw and TUI
paths resolve the given id via api.get_session; a bad id is a hard error,
not a silent new session. After app.run() returns and the terminal is
restored, print "Session <id>" / "Resume with: navi-code --resume <id>"
when a session is attached.
- tui_app._resolve_session: make the explicit-id branch strict — a failed
get_session surfaces an error event and returns None instead of falling
through to creating a new session (a --resume typo used to silently start
a fresh session). Saved-state resume stays lenient.
- tests: resume-flag resolve/bad-id/mutex + hint format; tui strict-on-bad
explicit id and explicit-id resume.
Co-Authored-By: Claude <noreply@anthropic.com>
Eugene Sukhodolskiy
committed
20 days ago
|
| 2026-07-10 |
tui: bound chat history render cost (cache + visible-item cap)
...
The chat panel rebuilt every renderable on each WS event, including
per-token stream_delta — so markdown was re-parsed for the entire history
on every token and the TUI lagged as conversations grew.
- Cache renderables keyed by id(item)+signature so only the changed item
(the streaming assistant bubble) is rebuilt per token; the rest are
reused. Prune orphaned entries so cache stays bounded by live items.
- Add max_visible_items TUI setting (default 200): render only the last
N items with a "… N earlier messages not shown" hint when truncated.
Items stay in the in-memory model (non-destructive) but are not laid
out, so per-refresh cost is constant regardless of history length.
- Rename the cache attr to _chat_render_cache to avoid collision with
Textual Widget._render_cache, which it resets on mount.
Co-Authored-By: Claude <noreply@anthropic.com>
Eugene Sukhodolskiy
committed
21 days ago
|
tui: render Navi's answers as markdown
...
AssistantMessageRenderer now parses its content as markdown (headings, lists,
bold/italic, inline and fenced code blocks with theme-aware syntax
highlighting) via the existing ThemedMarkdownRenderable — the same renderer
the planning cards already use — instead of plain Text. The formatting
shows live as Navi streams. User messages stay plain text.
Co-Authored-By: Claude <noreply@anthropic.com>
Eugene Sukhodolskiy
committed
21 days ago
|

tui: show the currently-served model in the status panel
...
The status panel's Model line was fed the global ollama_default_model, not the
session/profile model, and the server never told the client which model
actually served a call. Now:
- Backends stamp the resolved model onto LLMChunk (first chunk) / LLMResponse.
The fallback backend reports the model that survived its server+model
priority list (may differ from the profile's first choice).
- New ModelInfo event ({"type":"model_info","model":...}) emitted once per
turn from agent._consume_stream, re-emitted only when the model changes
across iterations. Additive WS event — old clients ignore it.
- TUI: attach_session/switch fetch the profile's configured model (first of
profile.model) via api.get_profile_model so the panel shows a value before
the first request; model_info then refines it to the actually-served model.
Not forwarded to the chat panel. raw CLI prints "[model] ...".
Co-Authored-By: Claude <noreply@anthropic.com>
Eugene Sukhodolskiy
committed
21 days ago
|
docs: document bounded autonomy + navi-code TUI features
...
- profiles.md: new "Bounded autonomy" section (scope_boundary_enabled,
observe_skips_plan_enabled) + memory-facts scope filter; navi_code section
and config.json example updated
- mechanics.md: Agent Loop / Planning Pipeline / Context Builder rows for
scope boundary, observe short-circuit, MODE classification, scope filter
- navi_code.md: bounded autonomy note in profile section
- navi_code_cli.md: --raw and -h/--help params, multiline input, planning
and spawn_agent cards, nested subagent styling, planning_status/plan_ready/
turn_thinking rendering, tui/renderers/ in dev section
Co-Authored-By: Claude <noreply@anthropic.com>
Eugene Sukhodolskiy
committed
21 days ago
|
| 2026-07-09 |

Navi Code TUI: sub-agent spawn card + nested styling for subagent thinking/planning
...
Sub-agent activity was either rendered as a generic tool card (spawn_agent) or
dropped to raw debug output (turn_thinking was unhandled in the TUI chat model
and the raw CLI). The webclient nests subagent events inside the spawn_agent
card; the TUI is flat, so nesting is conveyed with a distinct spawn card plus
left-indented, dimmed subagent cards.
New renderers/subagent.py:
- SpawnAgentStartedRenderer: "↘ subagent · <profile_id>" card showing the task
(+ briefing dimmed), secondary border. Registered before ToolStartedRenderer.
- SpawnAgentResultRenderer: "↙ subagent · <profile_id> ✓/✗" card with the
result, success/error border. Registered before ToolResultRenderer.
Nested subagent styling (left indent via Padding + dim):
- ThinkingRenderer: is_subagent → "subagent thinking" title, dim border, indented.
- PlanningStatusRenderer / PlanReadyRenderer: is_subagent → indented (label/title
already mark it as subagent).
- ToolStartedRenderer / ToolResultRenderer: is_subagent → indented so inner
tool calls read as nested inside the spawn card.
chat_model handles turn_thinking → thinking_block item with is_subagent meta;
chat_panel._refresh forwards is_subagent to the thinking renderer. Raw CLI
render.py prints turn_thinking as a [thinking] block (with "(subagent)" prefix).
Tests: spawn card shows profile/task and status; subagent thinking/planning/tool
cards are indented and labeled; non-subagent tool card is not indented;
turn_thinking creates a subagent thinking_block. 593 passed, 1 skipped.
Co-Authored-By: Claude <noreply@anthropic.com>
Eugene Sukhodolskiy
committed
21 days ago
|

Navi Code: render planning as a progress line + plan card, not raw events
...
planning_status and plan_ready events were unhandled in the TUI chat model and
the raw CLI renderer, so they fell through to raw debug output (TUI:
"plan_ready: {...}" status lines; raw CLI: "[event: plan_ready] {msg}").
TUI:
- New renderers/planning.py: PlanningStatusRenderer renders a "⚙ Planning ·
<label>" line (info color; dim + "(subagent)" prefix for subagent planning);
PlanReadyRenderer renders the plan as a Panel wrapping themed Markdown, titled
"Plan" with an accent border ("subagent plan" + dim border for subagents).
- Registered both in default_registry().
- chat_model handles the events: non-subagent planning_status rolls in place
(Analysis → Execution plan → Plan review share one line instead of stacking);
plan_ready consumes the pending planning line and appends the plan card;
subagent planning is appended as its own dim line/card so it does not bleed
into the parent turn's indicator (matching the webclient).
- chat_panel._refresh maps the new kinds to the registry.
Raw CLI (render.py): planning_status prints "[planning] <label>"; plan_ready
prints a "[plan] … [/plan]" block with the plan text.
Tests: renderer accepts/render (label, subagent prefix, plan title/content);
chat_model rolling-in-place, plan_ready consumes indicator, subagent separation.
584 passed, 1 skipped.
Co-Authored-By: Claude <noreply@anthropic.com>
Eugene Sukhodolskiy
committed
22 days ago
|
navi-code CLI: make -h show help instead of sending it to the model
...
The CLI uses ignore_unknown_options=True so a prompt can contain flag-like
text, but that also meant the short -h flag was not recognized as the help
flag — it fell through to the PROMPT argument and was sent to Navi as a
message ("you've asked for help again..."). --help already worked because
Click auto-generates the long form.
Add help_option_names=["-h", "--help"] to context_settings so -h is a known
help option, parsed before the ignore-unknown fallback.
Test: -h via CliRunner exits 0 and shows usage. 577 passed, 1 skipped.
Co-Authored-By: Claude <noreply@anthropic.com>
Eugene Sukhodolskiy
committed
22 days ago
|
Navi Code TUI: drop forced line break, keep soft-wrap + Enter to send
...
The forced line break (Ctrl+Enter) never worked: the target terminal does not
distinguish any modifier+Enter from plain Enter — all of Ctrl/Alt/Shift+Enter
arrive as key='enter' character='\r' (verified via a one-off key probe). With
Enter bound to submit, any modifier+Enter newline binding would collide with
submit and is impossible to distinguish in software.
Remove the Ctrl+Enter branch from _PromptInput._on_key. The input stays
multi-line via soft-wrap (TextArea soft_wrap=True) and height: auto growth;
Enter still submits. Placeholder and module docstring updated to drop the
Ctrl+Enter mention and note why a newline key is not wired (revisit later).
Tests: removed test_ctrl_enter_inserts_newline_without_submitting; kept the
multiline-submit and input-grows tests. 576 passed, 1 skipped.
Co-Authored-By: Claude <noreply@anthropic.com>
Eugene Sukhodolskiy
committed
22 days ago
|

Navi Code TUI: make the message input multi-line
...
InputBox used textual.widgets.Input, a single-line widget. Swap to TextArea so
the prompt is genuinely multi-line: soft-wrap is on, the field grows with content
(height auto, min 3, max 12, then internal scroll), and Enter no longer collides
with newline insertion.
_PromptInput(TextArea) intercepts keys in _on_key before TextArea's own handler
(which maps enter -> "\n"):
- Enter -> submit: post UserSubmitted(text), then clear the field.
- Ctrl+Enter -> insert a hard line break.
Dropped dead set_prompt_char (referenced a non-existent self._prompt) and the
on_input_* handlers (submit now lives in the subclass). CSS selectors updated
from Input to TextArea. Placeholder notes the keybindings.
Tests: _input.value -> _input.text across the TUI suites; added Ctrl+Enter
inserts a newline without submitting, Enter submits the full multi-line buffer
(newlines preserved) and clears the field, and the input height grows with
content. 577 passed, 1 skipped.
Co-Authored-By: Claude <noreply@anthropic.com>
Eugene Sukhodolskiy
committed
22 days ago
|

Navi Code TUI: show final answer below tools, not scrolled out of view
...
The assistant answer bubble was created at stream_start (the first event of a
turn), placing it at the TOP of the turn — above the thinking blocks and tool
cards that follow. chat_panel._refresh() calls scroll_end, so the view pinned to
the bottom (tools/thinking) and the answer at the top scrolled out of view.
Users saw tool results, planning, and thinking, but not the final answer.
Fix in chat_model.handle_ws_event (client-only, no protocol/server change):
- stream_start no longer eagerly adds an empty assistant bubble; it resets the
current-assistant pointer so the first stream_delta opens the bubble lazily
at the position where text actually arrives.
- tool_started/tool_call reset the current-assistant pointer so the next
stream_delta opens a fresh bubble BELOW the tool cards — the final answer
lands at the bottom, visible after scroll_end.
- stream_end purges empty assistant/thinking bubbles and resets pointers.
Tests: stream_start creates no empty bubble; final assistant_message renders
after the last tool_call with no empty bubbles; empty bubbles purged on
stream_end. 574 passed, 1 skipped.
Co-Authored-By: Claude <noreply@anthropic.com>
Eugene Sukhodolskiy
committed
22 days ago
|
test(tui): mock backend api so TUI tests are server-independent
...
test_escape_stops_active_stream failed without a live server on :8000:
_startup calls api.create_session/get_session and _refresh_sessions calls
api.list_sessions, but only stop_session was mocked, so _ctx.session_id
stayed None. The whole file silently hit a real server (slow, flaky).
Add an autouse fixture mocking create_session/get_session/list_sessions.
Per-test stop_session mocks stay (own call lists). 571 passed, 0 failed.
Co-Authored-By: Claude <noreply@anthropic.com>
Eugene Sukhodolskiy
committed
22 days ago
|
agent: cwd-aware memory fact filter for bounded autonomy
...
Long-term memory stored context-dependent path facts (e.g. "project_root
→ /home/.../navi-1") as global user facts. search_facts injected them into
any session, so when working in another project the agent was told "the
project root is navi-1" and drifted there.
When scope_boundary_enabled and a session cwd is set, _memory_facts_msg now
drops facts whose value is an absolute path outside the session cwd tree.
Facts are kept when working inside that path (then they are correct), and
non-path/relative facts always pass. Free flight stays reproducible by
toggling the flag off. No facts deleted, extractor untouched.
Co-Authored-By: Claude <noreply@anthropic.com>
Eugene Sukhodolskiy
committed
22 days ago
|

agent: bounded autonomy — scope boundary + observe-vs-act
...
navi_code had unwanted "free flight": an observe request ("look at a
directory") triggered the full Phase 3 plan with milestones + auto-todo,
and goal_anchoring then drove the agent to finish those steps, climbing
into sibling projects and executing milestone docs it found.
Two toggleable, default-off profile flags (on for navi_code):
- scope_boundary_enabled: injects a standing system message keeping the
agent within the literally requested scope; forbids acting on
discovered TODO/roadmap/milestone docs (report only).
- observe_skips_plan_enabled: Phase 1 classifies MODE: observe|act; an
observe request skips Phase 2/3 — no multi-step plan, no auto-todo, no
"execute step by step" prompt. The agent just gathers info and answers.
Independent of force_plan (observe on the first message still skips).
Free flight stays reproducible by flipping both flags off.
Co-Authored-By: Claude <noreply@anthropic.com>
Eugene Sukhodolskiy
committed
22 days ago
|
fix(ws): include session_id/profile_id in session_sync, guard renderer
...
The server sent {"type": "session_sync"} without session_id/profile_id,
crashing the terminal client (render.py did None[:8]). Add the fields to
both session_sync sends and guard the renderer against a missing id.
Co-Authored-By: Claude <noreply@anthropic.com>
Eugene Sukhodolskiy
committed
22 days ago
|
docs(websocket): record deferred WS auth-message hardening
...
Move task #248 (accept the API token as the first WS frame instead of a
?api_token query param, which leaks into access logs) into the websocket
docs as a Future work section with rationale, protocol sketch, and scope.
Eugene Sukhodolskiy
committed
22 days ago
|
navi-code wrapper: fix symlink resolution and bash docstring
...
The wrapper had a Python-style triple-quote docstring at the top, which bash
parses as an empty string + an opening quote — the whole block becomes one
giant 'command name' bash tries to execute, so the script broke before
reaching any logic. Replaced with # comments.
Also resolve symlinks (readlink -f) before computing PROJECT_ROOT so the
wrapper finds the repo/.venv correctly when invoked via a PATH symlink, and
run the client with the venv python + PYTHONPATH=PROJECT_ROOT (no cd, no
pip install -e . required) so the user's cwd is preserved and sent to the
server for per-project path resolution.
Eugene Sukhodolskiy
committed
22 days ago
|
session store: lazy persistence — no empty sessions in DB
...
POST /sessions no longer inserts a DB row. The Session is registered in an
in-memory _pending registry on PgSessionStore and only upserted on the first
save() (first user message / meaningful state change). Empty sessions that
never receive a message never reach the DB and vanish on server restart or
via the hourly pending sweep.
- pg_session_store: _pending dict + lock; create() registers, get() checks
_pending first, save() upserts the sessions row (INSERT ... ON CONFLICT)
and pops _pending so the session_messages FK is satisfied; sweep_pending()
drops abandoned entries; pending_sweep_loop() background task.
- main.py: start/cancel pending_sweep_loop in lifespan.
- tests: 7 new tests for create/get/save/list/sweep semantics; updated
existing save() test comments for the upsert.
Eugene Sukhodolskiy
committed
22 days ago
|
| 2026-07-08 |
profiles: add gemma4:12b-it-qat-128k as top-priority model
...
New local QAT model added at the head of the model priority list across
all profiles, so it is preferred first with cloud fallback.
Eugene Sukhodolskiy
committed
23 days ago
|
Add MIT license
...
Project is going open source under a permissive MIT license.
- LICENSE: MIT, Copyright (c) 2026 Eugene Sukhodolskiy
- pyproject.toml: license field, OSI classifier, allow-direct-references for the git dependency
- README.md: license pointer
- .gitignore: ignore .env, venv, build artifacts, local db/workspace files
Eugene Sukhodolskiy
committed
23 days ago
|
| 2026-06-26 |
Remove CLAUDE.md from repo and add to .gitignore
...
CLAUDE.md is now local-only and will not be tracked by git.
Co-Authored-By: Claude <noreply@anthropic.com>
Eugene Sukhodolskiy
committed
on 26 Jun
|
Navi Code: stop via Esc + project cwd propagation
...
- TUI: Esc stops active stream cooperatively via POST /sessions/{id}/stop
- TUI: render stream_stopped as status message
- CLI/WebSocket: send shell cwd in client->server message field
- Orchestrator stores cwd in session.session_metadata
- ContextBuilder injects [Working directory] into LLM context
- Agent sets current_working_directory ContextVar per turn
- tools/base: ToolContext gains cwd field
- filesystem/terminal/code_exec resolve relative paths against session cwd
- Add bin/navi-code wrapper for PATH symlink; document in README.md
- Update docs/websocket.md and tests
Full pytest: 544 passed, 1 skipped.
Co-Authored-By: Claude <noreply@anthropic.com>
Eugene Sukhodolskiy
committed
on 26 Jun
|
compressor: structured summaries, profile-aware compression, adaptive keep_recent
...
- Replace free-form summary with strict Markdown template (Goal, Active Files,
Decisions, Completed Work, Pending Work/Todo, Errors, Key Values).
- Keep filesystem/code_exec/terminal tool results and messages with
is_compression_critical=True verbatim during compression instead of 300-char truncation.
- Make compression profile-aware: AgentProfile gains compression_keep_recent,
compression_max_tokens, compression_prompt_file. navi_code uses dedicated
compression prompt and larger keep_recent/max_tokens.
- Adaptive partition_messages(): important turns (user corrections, errors,
critical tools) survive longer; filler/social turns compress sooner.
- Increase default context_summary_max_tokens from 3000 to 4000.
- Propagate active profile changes to ContextCompressor and SubAgentRunner.
Co-Authored-By: Claude <noreply@anthropic.com>
Eugene Sukhodolskiy
committed
on 26 Jun
|
agent: skip planning for casual greetings + strengthen DIRECT shortcut in Phase 1
...
- Add fast _is_casual_message heuristic in navi/core/agent.py. Greetings and
social chat (e.g. 'привет', 'как дела', 'hi', 'thanks') bypass planning even
on the first session message, unless planning_mandatory is enabled.
- Strengthen Phase 1 planning prompt in navi/core/planning.py: explicitly
require DIRECT output for greetings, simple questions, and one-step
instructions.
- Fix broken ContextCompressed import in navi/core/compressor.py.
- Add unit tests covering the new heuristic and DIRECT prompt.
Co-Authored-By: Claude <noreply@anthropic.com>
Eugene Sukhodolskiy
committed
on 26 Jun
|
Navi Code TUI: fix input box layout, command palette duplicate IDs, status renderer, and WS input loop
...
- clients/terminal/tui/widgets/input_box.py: switch Horizontal to Vertical with width: 100% for Input so it renders and accepts input in real terminals; add refresh on Input.Changed.
- clients/terminal/tui/screens/command_palette.py: remove fixed ListItem IDs to avoid DuplicateIds on fast filter.
- clients/terminal/tui/chat_model.py + renderers/status.py + widgets/chat_panel.py: render backend status events as dim system messages instead of raw dicts.
- clients/terminal/tui/ws_bridge.py: start NaviWebSocketClient.input_loop so enqueued user messages are actually sent to the backend.
- clients/terminal/tui/tui_app.py: focus InputBox synchronously in on_mount so typing works immediately.
- tests/clients/test_tui_app.py: regression test for visible input text.
Co-Authored-By: Claude <noreply@anthropic.com>
Eugene Sukhodolskiy
committed
on 26 Jun
|
| 2026-06-24 |
Fix MCP names and profile API format consistency
...
- Replace stale mcp__navi_web__* / mcp__navi_3d__* names with canonical
mcp__navi-web__* / mcp__navi-3d__* across prompts and key_tools.
- Update /agents/profiles and /admin/profiles endpoints to expose
tools.agent / tools.subagent instead of deprecated enabled_tools fields.
- Update docs/mechanics.md to reference the new tools structure.
- Archive stale docs/visual.html.
Co-Authored-By: Claude <noreply@anthropic.com>
Eugene Sukhodolskiy
committed
on 24 Jun
|