synaps 0.3.3

Terminal-native AI agent runtime — parallel orchestration, reactive subagents, MCP, autonomous supervision
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
# Changelog

All notable changes to this project will be documented in this file.

## [Unreleased]

## [0.3.3] — 2026-06-14

### Added
- **`night-city` theme** — a premium neon-noir palette (Cyberpunk / Blade Runner / Neuromancer accents on a near-black base). Strictly opt-in via `theme = night-city`.
- **Tool transcript overhaul** — per-tool colored gutter + glyph identity (bash / read / write / edit / grep / find / ls / subagent / ext), subtle per-tool panel backgrounds (input darker, output lighter), paired input→output ordering for parallel tool calls, and an `ok / timed out` status footer under each result. All themeable via new `tool_*` theme keys.

### Performance
- **Streaming redraw throttled ~60fps → ~10fps during active turns** (#131). The per-delta full `RenderModel` rebuild — which scales with session size — was the main-thread CPU cost; the spinner only needs ~10fps and deltas arrive faster than the eye reads. The idle/final frame still renders immediately, so end-of-turn state never lags.

### Fixed
- **Tool colors no longer leak across themes** — `tool_*` accents now derive from each theme's *own* palette (only `night-city` carries the neon set), and the no-config default theme is unchanged. Pick `dracula` and the tool gutters look like dracula, not Night City.
- **Thinking spinner could spin forever** on an empty thinking step — the placeholder is now dropped when the agent produces text/tools or the turn ends. A real model `…` is preserved via a sentinel so it's never mistaken for the placeholder.

## [0.3.2] — 2026-06-14

### Fixed
- **`synaps.log` was silently dead** — after the A3 split the tracing `EnvFilter` only enabled `synaps_cli=debug`, but the code now emits from `agent_core` / `agent_engine` / `agent_tui` targets. With `RUST_LOG` unset (the default) every log line was filtered out. Added the three crate targets to the filter.
- **Version header showed a bogus `<hash>-dirty` on source-tarball installs** — `build.rs` shelled out to `git` and trusted whatever repo it found; AUR/Homebrew extract the source *inside* the package's own git checkout, so it baked an unrelated commit + a spurious `-dirty`. Now it only trusts git when its repo root **is** the synaps workspace root; any packaged build (AUR / Homebrew / crates.io) shows `vX.Y.Z · release` instead. CI-built release binaries still carry the real commit.
- **Empty `end_turn` could misclassify as a silent-stop error in the default (telemetry-off) config** — `has_stop_reason` was derived from a telemetry-gated field. Now captured unconditionally. (#130 follow-up)
- **In-stream API error bodies reached the screen unsanitized** — a hostile/MITM endpoint could inject terminal escape sequences via an `error` event message. Error text is now sanitized like notices.
- **Mid-stream transport drops are now retried** through the unified retry budget instead of hard-failing the turn.
- **`build.rs` `-dirty` no longer triggers on untracked files** (`--untracked-files=no`); SSE line buffer gained an 8 MiB hard cap against a no-newline OOM; telemetry records now carry the real retry attempt number.

### Changed
- **crates.io publish job** runs with verification enabled and keys idempotency off the process exit code (not log grep), so a genuine publish failure fails the release loudly instead of silently skipping a crate.

### Docs
- User + contributor docs (README, AGENTS, in-app `/help`, and the 36-page wiki) cross-checked against the code and refreshed for the 0.2→0.3 / A3 workspace reality.

## [0.3.1] — 2026-06-14

### Fixed
- **`cargo install synaps` from crates.io was broken in 0.3.0** — after the A3 workspace split, `synaps-engine` embedded its help data via `include_str!("../../../assets/help.json")`, a file at the *workspace root, outside the crate*. `cargo publish` only packages files inside the crate, so the published `synaps-engine`/`synaps` 0.3.0 could not compile (`No such file: assets/help.json`). The asset now lives inside the crate at `crates/agent-engine/assets/help.json`, and the crates.io publish job runs **with verification** (no `--no-verify`) so this class of packaging bug is caught before the permanent upload. Binary installs (GitHub release, Homebrew, AUR, shell installer) were never affected — they bundle the full workspace. The broken 0.3.0 crates are yanked.

## [0.3.0] — 2026-06-13

### Added
- **Workspace architecture split (A3)** — the monolith is now three library crates (`agent-core`, `agent-engine`, `agent-tui`) behind the `synaps` binary. Cleaner boundaries, faster incremental builds, independent test surfaces. No user-facing CLI changes.
- **Dedicated render thread** — the terminal is driven from its own thread off an immutable `RenderModel` snapshot, decoupling draw latency from the event loop and closing a class of resize/teardown races. The signal watchdog is retired now that rendering is isolated.
- **`/stats` command** — per-session token + cost receipt with a cache-split footer.
- **`/context` command** — inspect live context-window usage.
- **Configurable prompt-cache TTL (`cache_ttl` config key)** — `5m` (default, unchanged behavior), `1h`, or `hybrid` (1h on the stable tools/system prefix, 5m on the per-turn message tail). Long-gap sessions (> 5 min between turns) can keep their cache warm instead of paying full input price every turn. Invalid values fall back to 5m with a boot warning. Spec: `docs/specs/cache-ttl-spec.md` (v4)
  - Pricing is TTL-aware: 1h cache writes billed at 2.0× input (5m stays 1.25×); when the API reports no 5m/1h split, aggregate writes are billed at the 5m rate (fail-cheap)
  - `extended-cache-ttl-2025-04-11` beta header sent only on API-key auth — 1h is live-verified working bare over OAuth, whose beta set is left untouched
  - Silent-downgrade detector: if 1h is configured but the account never honors it, a one-time-per-session notice fires; the configured mode is never auto-flipped
  - RPC protocol: `agent_end.usage` gains optional `cache_creation_5m` / `cache_creation_1h` fields (omitted when unknown — additive, existing consumers unaffected); same split flows through server-mode `Usage` messages and subagent aggregation
- **`/model <name>` and `/thinking` persist to config** — in-session changes survive restarts.
- **Paste while streaming** — input can be pasted mid-turn without dropping characters.
- **Build commit in the TUI header** — the version now shows the exact git commit the binary was built from (e.g. `v0.3.0 · 88e08e0`), with a `-dirty` marker for builds from a modified tree.

### Changed
- **Honest billing** — reported cost now reflects exactly one Usage event per request and TTL-aware cache-write pricing. Figures will differ from 0.2.1 (more accurate, not more expensive).
- **Signal shutdown overhaul** — SIGINT/SIGTERM handling rebuilt around the real root causes: clean teardown ordering, bounded budgets, and no watchdog.

### Fixed
- **The "stopping" bug (#130)** — in large conversations the agent could silently end a turn after running tools, never landing the synthesizing reply. Anthropic delivers some failures (e.g. `overloaded_error`) as an in-stream `error` event under a 200 response; the SSE parser had no `error` arm and dropped it, leaving an empty stream that was treated as a clean end-of-turn. The parser now surfaces these as visible, actionable errors; an empty model response is never swallowed; and the retry budget is unified so HTTP 5xx, transport drops, and in-stream errors all draw from one policy (429 keeps its dedicated budget). Prompt caching is unaffected — retries re-send the byte-identical request, hitting cache.
- **Cache-TTL usage split was read from the wrong SSE event** — live `message_delta` frames carry only the aggregate; the 5m/1h breakdown arrives on `message_start`. The split is now captured at `message_start` with a delta-arm fallback, restoring telemetry split keys, the downgrade detector, and correct 1h write pricing in streaming sessions.
- **Signal delivery + shutdown** — busy-loop and unkillable-process on a dead PTY fixed (three-layer); SIGINT clean-exit path restored.
- **Toast renderer panic** on small-terminal resize (`min > max`).
- **Idle logo gradient** frozen until the first keystroke.
- **Retry backoff** is now header-aware (honours `retry-after`/reset) with a higher 429 budget.
- **`/model`, `/thinking`, `/compact` side effects** now run on the live engine dispatch path.
- **Server-mode clients** now receive `Notice` events.

### Performance
- **Boot:** `--continue` no longer parses every session file (~11s → ~1s); the skills fs-walk moved off the async runtime; `plugins.json` read hoisted out of the extension-load loop.
- **Sessions:** `/sessions` reads headers + recent-only instead of parsing 76MB; `save_chain`'s 76MB collision scan dropped.
- **Runtime:** request body serialized once across retries; `sanitize_thinking_blocks` O(N²) → single tail pass.
- **TUI:** dirty-checked widget events killed the ~30% idle CPU burn (#119); heavy `App` structures Arc-projected; `/extensions` audit bounded to a tail read.

## [0.2.1] — 2026-06-12

### Fixed
- **AUR build failure on LTO-enabled systems** — the rustls migration (0.2.0) introduced `ring`, whose C/asm objects are GCC-LTO-incompatible with `rust-lld`: makepkg's `-flto=auto` produced bitcode the linker can't read, failing every `ring_core_*` symbol. PKGBUILD now sets `options=(!lto)`; stale `openssl` runtime dependency dropped (TLS is rustls since 0.2.0). Binaries unchanged from 0.2.0.

## [0.2.0] — 2026-06-12

### Performance
- **Typed SSE pipeline — per-token DOM allocation eliminated** — the streaming hot loop no longer builds a `serde_json::Value` per event
  - New `runtime/sse.rs::SseLineBuffer` — zero-copy line buffering: lines borrowed as `&str`, memchr (SIMD) newline search, O(1) read-cursor advance with periodic compaction (was two copies + an O(n) drain per line); UTF-8 splits across chunk boundaries handled by construction, with an exhaustive every-offset re-chunking test suite
  - Typed `AnthropicEvent` wire model with `Cow<'a, str>` borrowing — text deltas borrow from the line buffer on the escape-free fast path, own only when JSON escapes force decoding
  - `ParseState` + `process_event` consolidate the three parse sites (main loop, tail-flush, end-of-stream finalize) into a single write path — duplicate-site drift now structurally impossible; 15 behavioral seam tests pin the contract
- **Dirty-flag render loop + streaming redraw coalescing** — the TUI burned a full core during streaming (unconditional full-frame draw on every 16ms tick and every SSE delta); `needs_redraw` flag draws only on actual state change, idle costs zero draws
- **Streaming raised 30→60fps** — redraw coalescing throttle tightened now that frames are cheap
- **Draw-path allocation elimination** — viewport `Paragraph` moves the line Vec instead of deep-cloning every frame; ASCII-art rendering collapses same-style runs from one Span-per-char to one Span-per-line; per-frame version `format!` → `concat!` const
- **`mem::take` at session resume** — `rebuild_display_messages` no longer deep-copies the entire message history (potentially MBs) to dodge the borrow checker
- **Cache strategy: sliding-4 → single-last** — one stationary `cache_control` marker on the last message replaces ~57 lines of sliding-breakpoint logic
  - Bench evidence: equivalent hit rate (96–97% both strategies); live-verified 99% hit on warm turn
  - Prefix-invalidation bug class (mutating old markers) eliminated by construction; 5 new unit tests on a previously untested function

### Fixed
- **SSE double-emit on partial final line** — when the final `content_block_stop` arrived without a trailing newline, the tail-flush pushed the block but left `in_thinking`/`in_tool_use` set, so the end-of-stream flush pushed it again — duplicated content block sent to the model on the next turn
- **PTY zombie reaping** — `PtyHandle::Drop` killed the child but never `wait()`ed; every timed-out or dropped shell session left a zombie. Now reaped with bounded-retry `try_wait()` (all other child-process sites already used `kill_on_drop`)
- **Extension crash-loop health lied** — `restart_count` reset on successful handshake, so an extension that initialized fine and died on the next request never hit the exhaustion limit and reported Running. Consecutive-failure counter now resets only when the restarted process actually serves a call; new `total_restarts` (never reset) feeds health so recovered extensions report Degraded
- **Integration test rot from `env_clear`** — security-hardened extension spawns silently broke 20+ test fixtures parameterized via env vars; fixtures moved to argv parameters, plus poison-cascade and parallel-env-race cleanup — full suite green, 3 consecutive runs
- **Hardcoded Claude Code identity removed from API preamble** — replaced with a configurable `identity` config key + proper SynapsCLI default
- **Dotted (namespaced) config keys exempt from unknown-key warnings**

### Added
- **claude-fable-5 support** — known-models entry with adaptive thinking, pricing ($10/$50 per MTok), and 1M context opt-in
- **Telemetry module** — structured per-request API records: `TelemetryLevel` (off/basic/full), usage with cache TTL breakdown, rate-limit headers, cache-diagnosis records; JSONL writer to `~/.cache/synaps/api-log.jsonl` (0600 + `O_NOFOLLOW`), wired into config keys and the Anthropic SSE stream
- **Cache strategy benchmark suite (`bench/`)** — 21 tool-heavy questions with deterministic outcomes across 4 swappable breakpoint strategies (none / single-last / last-3 / sliding-4); per-turn JSONL logging of tokens, cache read/write, hit %, cost, latency; `compare.py` for side-by-side runs
- **`synaps completions <shell>`** — shell completions via clap_complete (bash, zsh, fish, elvish, powershell)
- **First-run banner** — boot with no OAuth, no `ANTHROPIC_API_KEY`, and no provider keys shows a getting-started banner with the three setup paths instead of a silent TUI

### Changed
- **reqwest 0.11 → 0.12 — duplicate HTTP stack collapsed** — reqwest 0.11 was the sole consumer of hyper 0.14/http 0.2/h2 0.3 alongside axum's hyper 1.x; the old stack (plus native-tls/OpenSSL) is gone, TLS is now rustls with native root certs — explicit in the manifest, smaller binary, zero source changes
- **Humanized API + network errors** — raw JSON dumps and reqwest debug strings replaced with actionable text (529 → "overloaded, wait", 401 → "run synaps login", context overflow → "run /compact"); retry notices ("⏳ API error, retrying…") are now display-only system lines instead of fake assistant content persisted into session history
- **Input recovery on stream error** — when a stream dies after retries, the user's popped message is restored to the input box instead of silently destroyed
- **Config parse warnings with did-you-mean** — unknown keys suggest corrections via levenshtein (`modle` → did you mean `model`?); unparseable values warn and fall back to defaults instead of silently misbehaving
- **Login/status polish** — token-refresh errors now say `synaps login` (was a nonexistent command); `synaps status` refreshes the OAuth token before the request instead of 401ing on expired tokens; `--help` rewritten with real about text and `--profile` documented

### Earlier in this cycle
- **Engine refactor + `chat` headless mode** — `daemon`, `run`, and `client` subcommands deleted; `chat` is now fully-featured headless mode with MCP, extensions, skills, sessions, compaction, and the event bus (same engine as the TUI, stdin/stdout rendering)
  - New `engine/` module: `setup.rs` (boot), `commands.rs` (headless slash commands), `stream.rs` (StreamEvent consumer), `session.rs` (ConversationState)
  - `chatui/` module deleted — `tui/` is the sole frontend
  - New `pricing.rs` — centralized Anthropic pricing (Opus/Sonnet/Haiku) with cache billing support
  - New `harbor/synaps_agent.py` — Terminal-Bench integration agent
  - Clippy CI added (`cargo clippy --all-targets` gates PRs)
  - Published on crates.io as `synaps` (v0.1.x); available via `cargo install synaps`, AUR (`yay -S synaps`), Homebrew, and GitHub Releases
- **Path B Phase 6 — The Big Move (extension contracts)** — synaps-cli core no longer contains whisper-specific code; the local-voice plugin owns it
  - Deleted `src/voice/{models,download,rebuild}.rs` (whisper.cpp catalog, HuggingFace downloader, cmake-rebuild orchestrator)
  - Slimmed `src/voice/discovery.rs` from ~400 LOC to ~190: only `discover()` / `discover_in()` / `DiscoveredVoiceSidecar` remain; `read_build_info`, `detect_host_backend`, `probe_*` are gone (callers use the Phase 5 `info.get` cache instead)
  - Deleted in-core `/voice models|help|download|rebuild` action arms and parsers — those subcommands now route through the plugin via the Phase 2 interactive command contract; missing-plugin case errors with a clear message
  - Deleted in-core whisper download UI: `App.{download_progress, download_filename, download_rx, voice_download_in_flight, model_browser_selected, cached_voice_compiled_backend}` plus `start_download` / `on_download_progress` / `on_download_complete` and the `download_change` event-loop arm; the sticky progress widget now exclusively renders generic `extensions::active_tasks::ActiveTasks` (Phase 3)
  - Deleted `EditorKind::{ModelBrowser, WhisperModelPicker}`, `ActiveEditor::ModelBrowser`, `ModelBrowserRow`, `model_browser_rows`, `whisper_model_options`, `render_model_browser`, `render_download_progress_line`, plus the `voice_stt_model` / `voice_stt_backend` / `voice_language` setting definitions — the plugin replaces them via the Phase 4 settings categories contract
  - **Migration shim:** on first launch, legacy global voice config keys (`voice_stt_model_path`, `voice_stt_model`, `voice_stt_backend`, `voice_language`) are copied into the plugin namespace `~/.synaps-cli/plugins/local-voice/config` if the destination is empty; legacy keys remain in place for safety. Reads at runtime prefer the plugin namespace and fall back to the legacy global key with a deprecation warning.
  - Net change: `~−2,400` LOC removed from synaps-cli core; behaviour preserved when the local-voice plugin is installed
- **Phase 2 — Extensions Capability Platform** — extensions are now first-class citizens
  - Slash command extension contract (`commands` in plugin manifest)
  - Settings panel extension contract (`settings` in plugin manifest)
  - Keybind extension contract (User-tier binds via plugin manifest)
  - Model-provider extension contract — extensions can register OpenAI-compatible providers as full agentic backends with tool-call support
  - Capability discovery + permission gating per extension
  - Slices P–W complete; 836 tests green (682 lib + 154 bin)
- **Voice dictation** — toggle-driven mic capture wired to the input buffer
  - `/voice` slash command with subcommands: `models`, `download <id>`, `help`, `rebuild [backend]`
  - Configurable toggle key — defaults to F8; supported: F8, F2, F12, C-V, C-G (Ctrl+Shift and Ctrl+Alt are unreliable in terminals)
  - Toggle-only flow: press once → start listening (🎤 listening pill), press again → stop, transcribe, append to input
  - VAD-aware: final transcripts during an armed session insert text without disarming
  - Settings → Voice category: toggle key, language cycler (14 langs), STT model, STT backend
  - `voice_language` cycler: `auto / en / es / fr / de / it / pt / nl / ja / zh / ko / ar / hi / ru`
  - Hot-reload keybind on settings save — no restart required
  - Kitty keyboard protocol enabled for reliable F-key + modifier capture
  - Voice sidecar lives in the `local-voice-plugin` (synaps-skills repo) — synaps core stays voice-free
- **Whisper model manager** — discover, download, switch models in-app
  - 10-entry catalog hard-coded in `src/voice/models.rs`: tiny / base / small / medium / large-v3 / large-v3-turbo + `.en` variants, with real SHA256s from HuggingFace
  - `/voice models` renders an installed/uninstalled table
  - `/voice download <id>` — streaming download with atomic `.partial → rename` install, SHA256 verification, cancellable, single-in-flight
  - `EditorKind::ModelBrowser` — Settings → Voice → STT model browses the catalog inline, Up/Down to nav, Enter installs-or-selects
  - Inline footer download progress while a model fetches
  - Models live under `~/.synaps-cli/models/whisper/`
- **Whisper backend selection** — pick CPU / CUDA / Metal / Vulkan / OpenBLAS or auto-detect
  - `voice_stt_backend` cycler in Settings → Voice
  - `auto` probes the host (`nvcc`, `vulkaninfo`, `pkg-config`, target-os) and picks the best available
  - "Current build: <backend>" annotation surfaces the sidecar's compiled feature set
  - ⚠ rebuild annotation when the selected backend differs from the compiled one
  - `/voice rebuild [backend]` invokes `local-voice-plugin/scripts/setup.sh --features <backend>` and streams build output as System rows
  - Sidecar reports its build via `--print-build-info` (JSON: `{backend, features, version}`)
- **Chat UI rendering polish**
  - System / Error messages now split on `\n` and word-wrap on each sub-line (same path as User / Text)
  - Tab-aware soft wrap: continuation rows indent under the last-tab anchor column (4-col tab stops); safety clamp falls back to leading-space indent if the anchor exceeds 60% of width
  - Fixes `/voice help`, `/chain ls`, `/keybinds`, plugin command output rendering
- **Extension System** — process-based JSON-RPC hooks (before_message, before_tool_call, after_tool_call, on_session_start, on_session_end)
  - Tool-specific filtering: hooks can target specific tools
  - Context injection via HookResult::Inject
  - Permission-gated with 6 permission types
  - Drop-in plugin support
  - Silent loading (only show failures)
- **Watcher notify_inbox hook** — event bus notification on agent completion
  - Config option: `[hooks] notify_inbox = true` in watcher agent config
  - Drops JSON events into `~/.synaps-cli/inbox/` when agents complete
  - Works in both 'once' and 'deploy' modes
  - Event payload includes agent name, session number, elapsed time, exit code, timestamp
- **Table rendering improvements**
  - Cells wrap instead of truncating with ellipsis
  - Inline markdown (bold/italic/code) rendered in table cells
  - Smarter column shrinking — preserves narrow columns
- **Structural refactoring** — improved code organization
  - `catalog.rs` → `catalog/` directory with per-provider modules
  - `palettes.rs` → `palettes/` directory with per-palette files
  - `tools/subagent*.rs` → `tools/subagent/` directory
  - `tools/tests.rs` → distributed inline test modules per tool
  - `runtime/api.rs` split into `api.rs` + `api_sync.rs` + `request.rs`
  - `chatui/mod.rs` helpers extracted to `helpers.rs` + `lifecycle.rs`
- **UTF-8 char boundary panics** — multiple fixes for multi-byte character handling
  - Hook output truncation now finds valid char boundaries
  - Bash output highlighter fixed for emoji/CJK characters
  - Additional edge cases found by PR review
- **Hook system improvements**
  - Handle HookResult::Block in before_message hook
  - Set tool_runtime_name in all before_tool_call hook paths
  - Track truncation explicitly in bash tool instead of string matching

### Added
- **Multi-Provider Runtime** — OpenAI-compatible provider engine
  - 17 providers: Groq, Cerebras, NVIDIA NIM, OpenRouter, Google AI Studio, DeepInfra, HuggingFace, Fireworks, Hyperbolic, Scaleway, SiliconFlow, Together, Chutes, Codestral, Perplexity, OVHcloud, + Local (Ollama/LM Studio/vLLM)
  - 55+ models cataloged with tier ratings (S+/S/A+/A/B)
  - `provider/model` shorthand: `/model groq/llama-3.3-70b-versatile`
  - SSE streaming with tool calling across all providers
  - StreamDecoder with HashMap-based tool call accumulation
  - Anthropic↔OpenAI message/tool translation layer
  - Provider router in `api.rs` — model prefix routes automatically
  - Config: `provider.<name> = <key>` in `~/.synaps-cli/config`
  - Env var fallback: `GROQ_API_KEY`, `CEREBRAS_API_KEY`, etc.
  - Local model support: `provider.local.url = http://localhost:11434/v1`
- **`/ping` command** — health-check all configured provider models
  - Non-blocking, results stream in live as each model responds
  - Shows ✅/❌/⏳/🔒/⌛ with latency per model
  - Results cached for model picker display
- **Settings → Providers** — TUI panel for API key management
  - View all 17 providers with key status (✅ set / ⬚ not set)
  - Enter to set/update key, d/Del to clear
  - Key masking (last 4 chars only)
  - Local endpoint URL editing
  - Press `p` to ping all models
  - Scrollable list with overflow indicators
- **Settings → Model Picker** — expanded to show provider models
  - Models grouped by provider with headers (── Groq ──, etc.)
  - Only shows providers with keys configured
  - Header rows are unselectable (skip on navigation)
  - Health status shown when ping data available
- **App starts without Anthropic credentials** — users with only provider keys can boot the TUI and use free models
- **Session Naming** — `/saveas <name>` aliases for sessions
  - Name format `[a-z0-9-]{1,40}`, validated and collision-checked
  - `/saveas` (no arg) clears the name
  - `synaps --continue <name>` resolves session names
  - `/sessions` shows `[@name]` tags on named sessions
- **Chain Naming** — `/chain name/list/unname` bookmarks for compaction lineages with auto-advance
  - `/chain name <name>` bookmarks the current session's lineage
  - `/chain list` shows all named chains (`*` marks active)
  - `/chain unname <name>` removes a bookmark
  - `/chain` (no args) shows lineage + "bookmarked by: @name" if present
  - Chain pointers stored at `~/.synaps-cli/chains/<name>.json`
  - On `/compact`, chain pointers auto-advance to the new session
- **Unified Session Resolution** — `resolve_session()`: chain name → session name → partial ID, used by `--continue` and `/resume`
  - Shared by `synaps --continue`, `/resume`, and server `--continue`
  - Resolution path surfaced as a system message (`↳ resolved via chain 'foo'` / `↳ resolved via name 'bar'`)
  - `--continue` value_name changed from `SESSION_ID` to `NAME_OR_ID`
- **Event Bus** — universal message ingestion for agent sessions
  - `synaps send` CLI command with atomic file writes
  - inotify inbox watcher via `notify` crate (spawn_blocking, non-blocking)
  - Priority EventQueue with severity ordering (Critical→front, High→after)
  - `tokio::sync::Notify` for instant TUI wake on event push
  - Events auto-trigger model turns when agent is idle
  - Events buffer during streaming via `pending_events`, flush on completion
  - Styled TUI event cards with severity icons (🔴🟠🟡🔵)
  - XML-wrapped event format with prompt injection hardening
  - 256KB file size cap, symlink guard, 0700 permissions
- **Reactive Subagents** — dispatch, poll, steer, collect
  - `subagent_start` — spawn and return handle_id immediately
  - `subagent_status` — non-blocking progress snapshot
  - `subagent_steer` — inject guidance mid-flight via steering channel
  - `subagent_collect` — non-blocking result check
  - `subagent_resume` — restart timed-out agents (stub)
  - `SubagentHandle` with shared `RwLock<SubagentState>`
  - `SubagentRegistry` with cleanup_finished on stream Done
  - Abort cancels all running reactive subagents
  - Thread handles stored for graceful shutdown
  - 13 unit tests for handle + registry
- **Chain Sessions** — `/compact` creates linked child session
  - Old session preserved on disk with `compacted_into` forward link
  - New session starts with `parent_session` back link
  - `/chain` command walks the session lineage
  - System prompt included in compaction summary
  - Configurable compaction model (defaults to Sonnet)
  - ModelPicker for compaction model in settings
  - Non-blocking compaction with spinner animation
  - Compacted summary hidden from TUI display
  - Message queuing during compaction
- **`respond` tool** (stub — returns honest failure until wired)
- **`send_channel` tool** (stub — returns honest failure until wired)
- **`/status` command + `synaps status` subcommand**: check account usage (5-hour, 7-day, Sonnet) with progress bars and reset countdowns. Hits OAuth usage API.
- **`/compact` slash command**: summarize & compact conversation history when context gets long
  - Structured checkpoint format (goals, progress, decisions, file ops, next steps)
  - Iterative compaction — re-compacting merges new work into existing summary
  - File operation tracking (read/write/edit paths preserved across compactions)
  - Custom focus instructions via `/compact <focus>`
  - Uses dedicated low-effort API call (no tools, summarization system prompt)
- **`context_window` setting wired to API**: `200k` (default) omits beta header; `1m` sends `context-1m-2025-08-07` on supported models (Opus 4.6+, Sonnet 4.x); previously was UI-only display cap
- **Claude Code marketplace compatibility**: probe both `.synaps-plugin` and `.claude-plugin` layouts, `${CLAUDE_PLUGIN_ROOT}` substitution in skill bodies
- **Plugins subdir sources and cascade uninstall**: install from subdir-based plugin repos, cascade-remove plugins when their marketplace is deleted
- **Settings → Plugins marketplace overlay**: "Open Plugin Marketplace" action row in Settings, opens plugins modal as nested overlay
- **Hidden binary**: GamblersDen bundled as `hidden` binary alongside `synaps`
- **Single binary architecture**: all binaries consolidated into `synaps`
  - `synaps` (no args) = TUI (was `chatui`)
  - `synaps chat` = fully-featured headless mode (MCP, extensions, skills, sessions)
  - `synaps server` = WebSocket API (was `server`)
  - `synaps agent` = headless worker (was `synaps-agent`)
  - `synaps watcher` = supervisor (was `watcher`)
  - `synaps login` = OAuth (was `login`)
  - `synaps send` = push events into a running session
  - `synaps status` = check account usage
  - (Removed: `run`, `client`, `daemon`)
- **Live theme preview in /settings**: scroll themes to preview, Enter confirms, Esc reverts
- **Theme hot-reload**: `/theme <name>` applies instantly without restart (ArcSwap)
- **Settings picker scroll**: theme/model picker scrolls with cursor
- **Adaptive thinking for Opus 4.7+**: `{type: "adaptive", display: "summarized"}` with effort mapping (xhigh/high/medium/low/adaptive)
- **Model-agnostic context window**: bar denominator adapts per-model (1M Opus 4.7, 200K Sonnet/Haiku)
- **Per-turn context tracking**: usage bar shows actual request size, not cumulative cost
- **Effort parameter**: thinking depth on adaptive models controlled via `output_config.effort`
- **"adaptive" thinking option**: new cycler value in `/settings` — lets model decide thinking depth
- **Tab-cycle for slash commands**: `/s` + Tab cycles through sessions → settings → system
- **Streaming command guard**: known slash commands no longer leak into model stream as steering
- **Usage log opt-in**: `SYNAPS_USAGE_LOG=1` writes to `~/.cache/synaps/usage.log` (0600, O_NOFOLLOW)
- **Opus 4.6 in model picker**: was missing from `/settings` model list
- **`settings` + `plugins` in tab-complete**: were missing from `BUILTIN_COMMANDS`
- **Plugin Keybinds** — plugins declare custom keyboard shortcuts in `plugin.json`
  - `KeybindRegistry` with parser, matching, and conflict resolution
  - Key notation: `C-x` (Ctrl), `A-x` (Alt), `S-x` (Shift), `F1`–`F12`, special keys
  - 4 action types: `slash_command`, `load_skill`, `inject_prompt`, `run_script`
  - User overrides via `keybind.*` in config — override or disable plugin binds
  - Priority: core > user > plugin (core binds never overridable)
  - `/keybinds` command shows all registered binds with source attribution
  - 23 unit tests for parser, registry, conflicts, overrides
- **Plugin Agent Namespaced Resolution** — `subagent(agent: "dev-tools:sage")` resolves plugin agents via `plugin:agent` syntax
  - Searches `plugins/<plugin>/skills/*/agents/<agent>.md`
  - Input validation, path traversal protection, ambiguity detection
  - Updated error messages to mention `plugin:agent` syntax
- **Mouse Text Selection & Clipboard** — left-click drag to select text in chat area
  - Right-click with selection → copy to system clipboard
  - Right-click without selection → paste from clipboard
  - Singleton clipboard thread (no thread-per-copy accumulation)
  - Paste suppression with 150ms TTL (prevents terminal double-paste)
  - Selection highlight via `Block::inner()` computed content rect
  - Theme-aware highlight color

### Fixed
- **Empty thinking/text blocks rejected by API** — fixed two related 400 errors that could permanently brick a session:
  - `messages.N.content.M.thinking: each thinking block must contain thinking` — streaming accumulator (`runtime/api.rs`) was pushing thinking content blocks even when no `thinking_delta` arrived (only a signature, or stream cut off). Now skipped at all three accumulation sites (mid-stream, tail buffer, post-loop fallthrough).
  - `messages: text content blocks must be non-empty` — defensive sanitizer (`runtime/helpers.rs::sanitize_thinking_blocks`) added; runs on every outbound API call from both `call_api_stream_inner` and `call_api`. Strips empty `thinking`, `redacted_thinking.data`, and `text` blocks; drops assistant messages whose entire content gets stripped; merges resulting consecutive same-role messages to preserve Anthropic's strict role-alternation rule. Auto-heals sessions persisted before the streaming fix landed.
  - 9 unit tests covering the drop-and-merge cases.
- **Config file permissions** — now 0600 (was 0644, world-readable with API keys)
- **API key masking** — shows last 4 chars only (was showing first 8 + last 4)
- **ProviderConfig Debug** — custom impl redacts `api_key` as `[REDACTED]` in logs
- **Provider base URLs** — corrected siliconflow (.com→.cn), chutes (→llm.chutes.ai)
- **Dead models removed** — groq/llama-3.1-70b, groq/qwen-qwq-32b, groq/gemma2-9b (all decommissioned)
- **Google stream_options** — skip `include_usage` for googleapis.com (rejects it)
- **Tool→user message ordering** — insert space-content assistant between tool result and user message for strict providers
- **Finish-frame tool call dedup** — NVIDIA sends final argument chunk with finish_reason; was being dropped as "resend"
- **Model ID extraction** — health prefix (✅ 253ms) no longer embedded in model string sent to API
- **`/status` on non-Anthropic** — shows friendly message instead of crashing
- **"Calling Claude..."** — now shows actual model name in `synaps run`
- **Paste in settings** — `Event::Paste` now handled in API key, text, and custom model editors
- **Missing provider key** — `/model sambanova/llama` with no key shows clear error instead of silent Anthropic misroute
- **UTF-8 truncation panic** — `bash` and `shell` output truncation now finds valid char boundaries before truncating, preventing crash on multi-byte characters (emoji, CJK)
- **Zero-width terminal panic** — markdown word-wrap `chunks(0)` crash on rapid tmux pane resize, clamped to `.max(1)`
- **Local model connection** — friendly "is Ollama running?" instead of raw TCP error
- **`/help` updated** — mentions provider/model syntax and /settings for key management
- **`ping_print` lifecycle** — resets after all results arrive via pending counter
- **MCP tool name causes 400 errors** — Anthropic rejects `mcp_` prefixed tool names (rate limit pool misrouting). Renamed `mcp_connect` → `connect_mcp_server`, tool prefix `mcp__server__tool` → `ext__server__tool`.
- **`/saveas` on empty sessions** — `save_session()` bailed on empty `api_messages`, so the name never persisted. Now calls `session.save()` directly.
- Compaction no longer overwrites original session (loads from disk)
- Event content sanitized against prompt injection (XML tags, case-insensitive)
- Atomic inbox writes (.json.tmp → .json rename)
- inotify watcher runs on spawn_blocking (no longer starves tokio runtime)
- Events during streaming buffered and flushed after MessageHistory
- Spurious auto-triggers prevented by event_received guard
- push() calls notify_one() (was missing — events silently queued)
- Session save ordering: new session saved before old session updated
- chars().count() moved outside lock scope
- High-priority event FIFO ordering (was LIFO among Highs)
- Queue-full events left in inbox for retry (not silently dropped)
- push_priority logs evicted event ID
- **Session file corruption**: atomic writes via write-to-tmp then rename
- **Tool input parse errors surfaced to model**: malformed tool_use JSON no longer silently falls through to empty input; model sees `invalid tool input JSON: ...` and can self-correct
- **Custom theme crash**: `unreachable!()` in draw replaced with graceful fallback colors for non-Rgb themes
- ASCII logo alignment: use unicode display width for consistent centering
- 'default' theme missing from settings picker
- Context bar pinned at 100% after 2-3 turns (was using cumulative tokens / hardcoded 200K)
- Thinking blocks invisible on Opus 4.7 (display defaulted to "omitted")
- `budget_tokens: 0` sentinel leaked to non-adaptive models → 400 error
- `/settings` cycling capped at xhigh (`apply_setting` silently rejected "adaptive")
- Stale test asserting `thinking_level_for_budget(0) == "low"` (production returns "adaptive")
- Usage log world-readable at `/tmp/` → moved to `~/.cache/` with 0600

### Changed
- Compaction logic moved from chatui to `core/compaction.rs`
- `SubagentHandle`/`Registry`/`Status` moved to `runtime/subagent.rs`
- `ApiOptions` struct replaces `use_1m_context: bool` threading
- `build_auth_header` + `build_beta_header` extracted as helpers
- `clone_repo` helper deduplicates plugin installer
- All compaction prompts colocated in `core/compaction.rs`
- Tool descriptions guide model choice (subagent vs subagent_start)
- Stub tools unregistered from tool registry (model doesn't see unimplemented tools)
- SubagentState uses RwLock instead of Mutex
- **`define_settings!` macro**: settings schema + apply handler defined once in `settings/defs.rs` via declarative macro — zero drift possible (replaced manual sync + parity tests)
- **Single source of truth for commands**: removed duplicate `ALL_COMMANDS` array; `commands.rs` now sources from `skills::BUILTIN_COMMANDS`
- **`src/cmd_*.rs` → `src/cmd/` module**: subcommand handlers moved to dedicated directory, `cmd_` prefix stripped
- Binary name: `chatui`/`synaps-cli`/`synaps-agent` → `synaps` (single binary with subcommands)
- `src/chatui/main.rs` → `src/chatui/mod.rs` (module, not binary)
- watcher spawns `synaps agent` instead of standalone `synaps-agent`
- `thinking_level_for_budget()` consolidated from 4 copies into single source of truth in `core/models.rs`
- `DEFAULT_LEGACY_ADAPTIVE_FALLBACK` constant replaces magic `16384` in clamp sites
- Dead-code warnings suppressed with explanatory comments (reaper handles, settings help field)
- Auto-cache toggle removed (manual breakpoints won: 90% vs 53%)
- README rewritten from internal documentation to product landing page

### Removed
- `SPEC-WATCHER.md` — internal spec, not needed in repo
- Auto-cache config toggle (`auto_cache = true/false`)

## [0.1.0] — 2026-04-12

### Added
- Interactive TUI (`chatui`) with streaming, markdown, syntax highlighting
- Headless chat (`chat`) for scripting and piping
- Autonomous agent supervision (`watcher`) with heartbeat, cost limits, handoff
- 10 built-in tools: bash, read, write, edit, grep, find, ls, subagent, mcp_connect, load_skill
- Interactive shell sessions: shell_start, shell_send, shell_end
- 18 color themes
- MCP integration with lazy server spawning
- Skills & plugins subsystem
- OAuth + API key authentication
- WebSocket server/client transport
- `/settings` full-screen modal
- `/plugins` management UI
- Session persistence and `/resume`