Bundles the pending TUI work into a coherent batch. Bug fixes from
external review:
* expandPlaceholders: single-pass alternation regex over the original
input prevents `#p\d+` / `#img\d+` tokens inside pasted content from
being re-expanded after the bracket form is inlined.
* /incognito: gate savePromptHistory and the Ctrl+V image-write branch
on `!m.incognito` so the no-persistence contract holds.
* history.txt: write at mode 0600 (chmod existing 0644 files), create
parent dir at 0700, truncate to 500 entries on every save, slog.Warn
on errors instead of swallowing.
* triggerPickerAction: guard m.config.Engine before SetModel, matching
the /model handler.
* Picker key handler: navigation/enter/q consume, escape/ctrl+c close
the picker AND fall through to global handlers (so streaming cancel
and double-tap quit work with an overlay open), default swallows
stray input.
* Paste line count: report total non-empty lines instead of newline
count, ignoring trailing newlines (no more "+0 lines" for "abc").
* Ctrl+O restored to expand-output; Ctrl+Y is the new copy-response
bind. /keys help text updated; picker help entries reordered.
* Tighter perms on .gnoma/pasted_image_*.png (0600).
Race-safety refactor: ApplyTheme used to mutate ~25 package-level
lipgloss styles in place. Replaced with an immutable themeStyles
snapshot and atomic.Pointer[themeStyles] swap. Readers go through a
theme() helper (one atomic load) instead of touching package vars
directly. No locks, no nested-RLock risk if rendering ever moves
off-thread.
Includes pre-existing in-flight work: TUISection in config with
persistent theme/vim settings; /copy /theme /vim slash commands;
provider-name completion; session.SetProvider for the provider picker.
Tests: placeholder_test.go (6 regression + happy-path cases including
the pasted-content collision), history_test.go (5 cases covering perms
on new and existing files, on-disk truncation, blank-input, newline
flattening), provider_test.go (provider switching + picker transitions
+ SLM gating).
Adds the in-TUI surface for the profile system:
- Status bar carries " · profile: <name>" next to the SLM badge when
profile mode is engaged (renders nothing in legacy single-config
installations).
- /profile (no args) shows the active profile and lists available ones.
- /profile <name> switches by re-executing gnoma via syscall.Exec under
--profile <name>. Critical cleanups (quality.json snapshot, SLM
backend Close, session.Close) fire explicitly before exec since
defers don't run after exec replaces the process image. Using
syscall.Exec rather than a child process avoids stacking a process
level on every switch and propagates the new gnoma's exit code
directly to the shell.
- Autocomplete after "/profile " offers configured profile names; the
completion source is threaded from main.go via tui.Config.
Conversation history is not preserved across a switch — profile change
implies different context, different keys, different permission mode,
so a clean reset is the correct semantic.
The TUI gave no indication that an SLM was configured or active.
You'd see the primary provider on the status line and nothing else,
even with [slm].enabled=true and a successfully booted backend.
Two surfaces added:
1. Status-bar SLM badge. The left side of the status line gains a
dim " · slm: <model> ⚙" suffix when the backend booted, " · slm: ✗"
when it failed, and nothing when SLM is disabled. The ⚙ marker
indicates the model advertises tool support.
2. Per-turn classifier visibility. The existing routing event already
produced "routed → <arm> (task: <type>)" lines in the chat history;
it now also reports which classifier made the decision, e.g.
"routed → ollama/ministral-3:3b (task: explain, by: slm_fallback)".
Lets you tell in real time whether the SLM is actually classifying
or falling back to the keyword heuristic.
Plumbing:
- new tui.SLMInfo struct on tui.Config
- main.go populates it after StartBackend returns
- stream.Event gains RoutingClassifier; engine.runLoop fills it from
task.ClassifierSource on the first round
Removes five unused funcs/vars/fields that golangci-lint had been
flagging (anthropic.toolCallDoneEvent, mistral.translateMessages,
hook.newError, subprocess.vibeParser.lastAssistantMsgID, tui.cBase),
two ineffectual assignments (tui/rendering.go visible-window loop,
subprocess stream_test setup), and a stale if/HasPrefix that's now a
strings.TrimPrefix.
Wires errcheck onto every subprocess / stream lifecycle path so a
failed close or shutdown is at least logged rather than silently
dropped:
- engine/loop.go: stream.Close on both the error and success paths
- mcp/manager.go: Shutdown when StartAll partial-fails; Transport
close after Initialize failure
- mcp/transport.go: stdin.Close + syscall.Kill on graceful-timeout
fallback
- slm/download.go: Close propagated as a named-return error on the
success path; explicitly discarded on the rollback path
- slm/classifier.go, slm/manager.go, hook/prompt.go, context/summarize.go,
config/write.go, cmd/gnoma/main.go, tool/fs/grep.go: explicit
ignores or error logging on Close / Shutdown / WalkDir / Scanln
Production-code errcheck and ineffassign are now zero. Remaining
golangci-lint output is test-only Close-in-defer noise plus
stylistic staticcheck QF suggestions, left alone.
- Suggestion dropdown now renders between separator and input (not in
chat area) — no more box at the top of an empty chat
- Ghost text suppressed when dropdown is visible (eliminates the
'fig' / trailing text on the right)
- Bottom separator shows purple 'cmd' label when typing '/' and
yellow 'exec' label when typing '!'
- '! <cmd>' prefix executes a raw shell command inline and shows
output in the chat (same as /shell but one-shot)
Startup: HarvestAliases, HarvestInventory, DiscoverCLIAgents, and
DiscoverLocalModels now run concurrently. Worst case latency drops
from sum(all) to max(all) — eliminates the 15s inventory timeout
from blocking the main path.
TUI: typing '/co' now shows a bordered dropdown of all matching
commands with descriptions. ↑↓ navigate, Tab/Enter accepts the
highlighted entry, Esc dismisses. Ghost-text still works for
unique unambiguous matches.
Replaces the text dump with a navigable bordered overlay.
↑↓ to move, Enter to cycle/toggle values, Esc to close.
Shows: Model (cycles through discovered arms), Permission mode,
Incognito toggle.
Three compounding bugs prevented tool calling with llama.cpp:
- Stream parser set argsComplete on partial JSON (e.g. "{"), dropping
subsequent argument deltas — fix: use json.Valid to detect completeness
- Missing tool_choice default — llama.cpp needs explicit "auto" to
activate its GBNF grammar constraint; now set when tools are present
- Tool names in history used internal format (fs.ls) while definitions
used API format (fs_ls) — now re-sanitized in translateMessage
Additional changes:
- Disable SDK retries for local providers (500s are deterministic)
- Dynamic capability probing via /props (llama.cpp) and /api/show
(Ollama), replacing hardcoded model prefix list
- Engine respects forced arm ToolUse capability when router is active
- Bundled /init skill with Go template blocks, context-aware for local
vs cloud models, deduplication rules against CLAUDE.md
- Tool result compaction for local models — previous round results
replaced with size markers to stay within small context windows
- Text-only fallback when tool-parse errors occur on local models
- "text-only" TUI indicator when model lacks tool support
- Session ResetError for retry after stream failures
- AllowedTools per-turn filtering in engine buildRequest
- Split app.go (2091→1378 lines) into rendering.go, events.go, init.go
- Add EventRouting stream event for router arm transparency
- Add session auto-naming from first user message
- Add context window progress bar in status bar
- Add /keys cheatsheet, /replay for resumed sessions
- Add inline cost-per-turn after assistant responses
- Add diff previews in fs.write/fs.edit permission prompts
- Collapse tool output to 3 lines by default (ctrl+o expands)
- Use AddPrefix for system context instead of InjectMessage
- Handle ContentThinking and ContentToolResult in session resume
- Show session title in resume picker
- Add /model numeric selection snapshot safety