feat(tui): add an External llama.cpp pane to the LLM tab - #50
Open
sachin-detrax wants to merge 2 commits into
Open
feat(tui): add an External llama.cpp pane to the LLM tab#50sachin-detrax wants to merge 2 commits into
sachin-detrax wants to merge 2 commits into
Conversation
Switching a managed install back to an external llama-server was only reachable through `/models <base-url>` — the LLM tab showed the current mode but had no way to change it. Add `external` as a third pane alongside Local and Cloud, cycled with ←/→. The pane is a single row because an external llama-server is one base URL. Enter opens an inline editor seeded with the current URL; saving it runs the existing `/health` probe and only then persists `localModels.url` + `mode: "external"`, so an unreachable address never steals a working route. A scheme-less host gets `http://`; unparseable input keeps the editor open behind an "invalid URL" hint. Two fixes to the persist path itself, shared with `/models <base-url>`: point the chat route at `local-llama` when a cloud provider is active (the saved URL was otherwise inert), and refresh the local-models panel so the tab stops reporting managed mode. Both run after the probe, and the route switch is skipped when it is already local so editing a URL stays quiet. Going back to managed is unchanged: pick a model in the Local pane. The managed daemon keeps running after a switch — the pane says so, and `s` stops it.
Pointing the chat route at an external llama-server left the managed daemon running and holding its VRAM for a route nothing used. Tear it down once the switch actually lands — after the /health probe and the config write, so a failed switch changes nothing. Chat only: `stopChatDaemonOnly()` leaves the embedding daemon and hybrid recall alone, unlike the full `stopDaemon()` the cloud path uses, which also flips `memory.embeddings.enabled` off. It no-ops when nothing is running so callers can fire it unconditionally. Skipped when the new URL is the managed daemon's own loopback address — driving a daemon you started yourself with `atomic-agent models start` is a supported setup, and stopping it would kill the server just pointed at. `pointsAtManagedDaemon` takes the port as an argument so it stays pure and testable.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #49.
What
Adds
externalas a third pane of the LLM tab, cycled with ←/→:The pane is a single row because an external llama-server is one base URL. Enter opens an inline editor seeded with the current URL; saving it runs the existing
/healthprobe and only then persistslocalModels.url+mode: "external", so an unreachable address never steals a working route. A scheme-less host getshttp://prepended; unparseable input keeps the editor open behind aninvalid URLhint rather than closing and silently discarding the edit.Once the route is live the row reads
* base URL http://192.168.1.50:8080 [ok · 12ms]and the status line switches tomode external · <url>.Going back to managed is unchanged: pick a model in the Local pane, which already persists
mode: "managed".Fixes in the persist path
Both are shared with
/models <base-url>, which had the same defects:local-llamawhen a cloud provider is active, so the saved URL is not inert. Skipped when the route is already local, so editing a URL does not emit a spurious "Switched active text provider" line.localModels.refresh()so the panel stops reporting managed mode.Both run after the probe succeeds, so a failed switch changes nothing.
Managed daemon teardown
Switching to external now stops the managed chat daemon, which would otherwise hold its VRAM for a route nothing uses.
stopChatDaemonOnly()deliberately leaves the embedding daemon andmemory.embeddings.enabledalone — unlike the cloud path'sstopDaemon(), which stops both and turns hybrid recall off. It no-ops when nothing is running.Skipped when the new URL is the managed daemon's own loopback address: driving a daemon started with
atomic-agent models startis a supported setup, and stopping it would kill the server just pointed at.pointsAtManagedDaemon(url, port)takes the port as an argument so it stays pure and testable.Implementation notes
LlmPanelModegainsexternal; the two cursor fields become three behindcursorFieldFor(mode), replacing themode === "cloud" ? … : …branches in the reducer, selectors, and key bindings.←/→now carry a direction (switchLlmMode(state, dispatch, delta)); with three panes a plain toggle no longer works.handleLlmModalKey, so panel hotkeys (s,n,c,/) go into the buffer while it is open.parseExternalUrlis shared between the Enter guard and the modal renderer so the hint and the guard cannot disagree.Testing
src/tui/llm-panel/llm-panel-external.test.ts— 10 tests: pane cycling in both directions, row active/inactive state, editor open/type/backspace/save/cancel, scheme-less normalization, the unparseable-input guard, per-pane cursors.pointsAtManagedDaemoncovered insrc/tui/persist-user-local-models-config.test.ts(loopback spellings, other ports, other hosts, garbage input).onPersistLlamaUrl(http://192.168.1.50:8080)fired from typing192.168.1.50:8080.tsc --noEmitclean,npm run buildsucceeds.src/tui: 656 passed / 6 failed, the same 6 that fail onmain(splash-banner copy, chat-log, persist-embedding, two TuiApp smoke tests, one llm-panel selectors test). One of them,TuiApp (smoke) > shows the two-mode LLM panel, asserts header copy this PR changes — the assertion is updated, but the test still fails for its pre-existing reason (the test terminal is too short to render the full header, so it takes the compact path). Not fixed here.Not included
/models externalslash subcommand./modelsland on the External pane when it is the active route.resolveModeFromActiveRoutewould needconfigMode, which is still at its default until the first refresh — managed users would land on the wrong pane at startup.