# Changelog

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

## [Unreleased]

### Recent (Dev Branch)
- **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`
