Commit Graph

453 Commits

Author SHA1 Message Date
Hunter Bown 91dda88317 Merge pull request #508 from Hmbown/claude/improve-0.8.8-docs-BMhHj
feat(v0.8.8): linux ARM64 prebuilts + install docs overhaul
2026-05-03 08:19:07 -05:00
Hunter Bown bef1895bed Merge pull request #518 from Hmbown/feat/489-memory-mvp
feat(memory): user-memory MVP — persistent notes, `# ` quick-add, /memory, remember tool (#489–#493)
2026-05-03 08:18:47 -05:00
Hunter Bown 7321165933 Merge pull request #517 from Hmbown/feat/505-inline-diff-rendering
feat(tools): inline unified-diff in edit_file / write_file results (#505)
2026-05-03 08:18:44 -05:00
Hunter Bown 591e9bf6f6 Merge pull request #515 from Hmbown/feat/498-osc8-hyperlinks
feat(tui): OSC 8 hyperlinks for Cmd+click-openable URLs (#498)
2026-05-03 08:18:21 -05:00
Hunter Bown aad2a3b564 Merge pull request #514 from Hmbown/chore/v0.8.8-stabilization
v0.8.8 stabilization — sub-agent caps, mutex contention, RLM polish, CI cleanup
2026-05-03 08:10:49 -05:00
Hunter Bown 7547d168a4 feat(memory): user-memory MVP — persistent notes, # quick-add, /memory, remember tool (#489–#493)
Adds a small, opt-in user-memory layer so the model has access to durable
preferences and conventions across sessions, and the user can dump quick
notes without leaving the TUI.

### What ships

- **Hierarchy loader** (#490): on every prompt assembly the engine reads
  `Config::memory_path()` (defaults to `~/.deepseek/memory.md`, override via
  `memory_path` in config or `DEEPSEEK_MEMORY_PATH`) and injects the file
  as a `<user_memory>` block alongside the existing `<project_instructions>`
  block. Goes above the volatile-content boundary so prefix-cache stays warm.
  Oversize files (>100 KiB) are truncated with a marker.
- **`# foo` composer quick-add** (#492): typing a single line that starts
  with `#` (but not `##` / `#!`) appends a timestamped bullet to the memory
  file and consumes the input — no turn fires. The composer status line
  surfaces the path that was written. Multi-`#` prefixes deliberately fall
  through so users can paste Markdown headings.
- **`/memory` slash command** (#491): `/memory` (or `/memory show`) prints
  the resolved path and contents inline; `/memory path`, `/memory clear`,
  and `/memory edit` (prints `${VISUAL:-${EDITOR:-vi}} <path>`) cover the
  rest of the manual-curation surface.
- **`remember` tool** (auto-update): model-callable tool that takes a
  `note` string and appends it as a bullet — the same persistence path as
  `# foo`. Auto-approved (writes only the user's own memory file). Only
  registered when memory is enabled, so it doesn't pollute the catalog when
  the feature is off.
- **Opt-in toggle** (#493): default behaviour is off. Enable with
  `[memory] enabled = true` in `config.toml` or `DEEPSEEK_MEMORY=on` in
  the environment.

### What's wired

- New `crates/tui/src/memory.rs` module (`load`, `as_system_block`,
  `compose_block`, `append_entry`).
- New `crates/tui/src/tools/remember.rs` (`RememberTool` + 3 tests).
- New `crates/tui/src/commands/memory.rs` (`memory(app, arg)` handler).
- `EngineConfig` gains `memory_enabled: bool` + `memory_path: PathBuf`.
- `ToolContext` gains `memory_path: Option<PathBuf>`.
- `App` exposes `memory_path` + `use_memory` from `AppOptions` (previously
  destructured-and-dropped); `main.rs` populates `use_memory` from
  `config.memory_enabled()`.
- `system_prompt_for_mode_with_context_and_skills` accepts an optional
  `user_memory_block` parameter; the engine computes it via
  `memory::compose_block(...)` and threads it through.
- Composer Enter handler intercepts `# foo` only when
  `config.memory_enabled()` is true; otherwise falls through to existing
  turn-submission path.
- `MemoryConfig` table (`[memory] enabled`) added to `Config`, surfaced
  in `config.example.toml`, plumbed through `merge_config`.

### Tests

- 8 unit tests in `memory::tests` covering `load` (missing / whitespace /
  real), `as_system_block` (xml shape, empty input, oversize truncation),
  and `append_entry` (creation, repeated append, empty-after-strip rejection).
- 3 unit tests in `tools::remember::tests` covering disabled-state error,
  successful append, and missing-`note`-field validation.

### Verification

cargo fmt --all -- --check                                          ✓
cargo clippy --workspace --all-targets --all-features --locked --   -D warnings   ✓
cargo test --workspace --all-features --locked                      ✓ (1821 + supporting; was 1809 on main)

Closes #490 #491 #492 #493
Refines #489 (EPIC parent — phase-1 MVP delivered; phase-2 items
494–497 stay on the v0.9.0 board)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 02:51:17 -05:00
Hunter Bown f2cf3843ec feat(tools): inline unified-diff in edit_file / write_file results (#505)
`edit_file` and `write_file` now capture the file contents before and
after the mutation, generate a unified diff with `similar`, and emit it
at the head of the `ToolResult` body. The TUI's existing
`output_looks_like_diff` detector (history.rs:1335) sees the `@@`
header in the first 5 lines and routes the payload through
`diff_render::render_diff`, which already renders unified diffs with
line numbers and coloured `+`/`-` gutters.

The model also benefits — it sees exactly which lines changed instead
of just `Replaced N occurrence(s)` or `Wrote N bytes`. Identical
content produces an empty diff, in which case the body falls back to
`<summary>\n(no changes)`.

### What's wired

- New `crates/tui/src/tools/diff_format.rs` exposes
  `make_unified_diff(path, old, new) -> String` using
  `similar::TextDiff::from_lines(...).unified_diff().context_radius(3)`.
- `WriteFileTool::execute` snapshots prior contents (or empty for new
  files), writes, then emits `<diff>\n<summary>` where summary is
  `Wrote N bytes to PATH` for existing files and
  `Created PATH (N bytes)` for new ones.
- `EditFileTool::execute` snapshots, replaces, writes, emits
  `<diff>\nReplaced N occurrence(s) in PATH`.
- `similar = "2"` added to `crates/tui/Cargo.toml`. Pure-Rust, no
  C deps; v2.7.0 in Cargo.lock.

### Tests

- 4 unit tests in `diff_format::tests` covering identical inputs,
  replacement, new-file (against empty), and presence of the `@@`
  header in the first 5 lines (so the TUI detector trips).
- Existing `test_write_file_tool` / `test_edit_file_tool` updated to
  assert both the summary line and the unified-diff body
  (`--- a/`, `-old`, `+new`).

### Verification

cargo fmt --all -- --check                                          ✓
cargo clippy --workspace --all-targets --all-features --locked --   -D warnings   ✓
cargo test --workspace --all-features --locked                      ✓ (1824 + supporting; was 1820)

Closes #505

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 02:30:44 -05:00
Hunter Bown b0f878bad6 Merge branch 'chore/v0.8.8-stabilization' into feat/498-osc8-hyperlinks 2026-05-03 02:18:00 -05:00
Hunter Bown dd3882e1bf fix(utils): display_path normalizes home-relative suffix to platform separator
`display_path_with_home` joined the `~` prefix with `MAIN_SEPARATOR_STR`
but called `rest.display()` for the suffix, which preserves whatever
separators the input carried. On Windows that produced mixed-separator
output like `~\projects/foo` for any path that came in with forward
slashes — visible in the tests that #506 added to lock down the
contract (the tests passed locally on Unix but failed on the
windows-latest CI runner).

Walk `rest.components()` and join each `Normal` component with
`MAIN_SEPARATOR_STR`. Pure-Rust, no extra deps, behavior is
byte-identical on Unix because the input separator was already `/`.

Verified locally:
- `cargo test -p deepseek-tui --locked display_path` ✓ (5 passed)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 02:17:50 -05:00
Hunter Bown 3013a54c78 feat(tui): emit OSC 8 hyperlinks so URLs are Cmd+click-openable (#498)
Modern terminals (iTerm2, Terminal.app 13+, Ghostty, Kitty, WezTerm,
Alacritty, recent gnome-terminal/konsole) make a URL clickable when it's
wrapped in:

    \x1b]8;;TARGET\x1b\\LABEL\x1b]8;;\x1b\\

Terminals that don't understand the sequence simply render the visible
LABEL and ignore the escape, so emitting OSC 8 is a strict UX upgrade
for supporting terminals and a no-op for the rest.

### What's wired

- New `crates/tui/src/tui/osc8.rs` module with `wrap_link(target, label)`,
  `strip_into(s, &mut out)`, and a process-wide `ENABLED` AtomicBool that
  defaults to `true`.
- `markdown_render::render_line_with_links` now wraps recognized URLs
  (`http(s)://…`) in OSC 8 when the runtime flag is on. Display width is
  computed from the bare URL — the escapes are zero-width on supporting
  terminals.
- `ui_text::line_to_string` and `line_to_plain` strip OSC 8 wrappers when
  the span content contains an escape, so selection / clipboard output
  carries clean URLs and not the raw escape codes.
- `[tui] osc8_links: bool` config (default `true`) added to `TuiConfig`,
  documented in `docs/CONFIGURATION.md`, and surfaced in
  `config.example.toml`. `run_tui` applies it at startup.

### Tests

- 7 unit tests in `osc8::tests` covering wrap, strip-with-ESC-terminator,
  strip-with-BEL-terminator, plain passthrough, mixed escapes, default
  state, and round-trip set/unset.
- 2 markdown_render tests proving URLs in paragraph blocks emit the OSC 8
  wrapper when enabled and emit plain text when disabled.
- 2 ui_text tests proving `line_to_plain` strips OSC 8 wrappers from spans
  and passes plain spans through unchanged.

Tests that touch the global ENABLED flag serialize through a static
Mutex inside the test module so cargo's parallel runner can't observe a
torn read.

### Verification

cargo fmt --all -- --check                                          ✓
cargo clippy --workspace --all-targets --all-features --locked --   -D warnings   ✓
cargo test --workspace --all-features --locked                      ✓ (1820 + supporting; was 1809)

Closes #498

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 02:13:14 -05:00
Hunter Bown ad8064b143 chore(v0.8.8): stabilization batch — sub-agent caps, mutex contention, RLM polish, CI cleanup
Bundles the v0.8.8 stabilization fixes that were already implemented in the
working tree, plus the workflow/doc reconciliation called out in #507.

### Sub-agent runtime fixes
- **#509** Default sub-agent cap raised to 10 (configurable via
  `[subagents].max_concurrent` in `config.toml`, hard ceiling 20). The
  running-count calculation now ignores non-running, no-handle, and finished
  handles so completed agents stop counting against the cap.
- **#510** `SharedSubAgentManager` is now `Arc<RwLock<...>>`; the read paths
  that previously held a `Mutex` for inspection now take a read lock,
  eliminating the multi-agent fan-out UI freeze.
- **#511** `compact_tool_result_for_context` summarizes `agent_result` /
  `agent_wait` payloads before they are folded into the parent context.
- **#512** RLM tool cards map to `ToolFamily::Rlm` and render `rlm`, not
  `swarm`. Stale "swarm" wording cleaned in docs/comments/tests.
- **#513** (foreground stopgap only) Foreground RLM work is visible in the
  Agents sidebar projection. Full async RLM lifecycle remains v0.8.9 — the
  issue stays open with a refined scope.

### TUI / UX fixes
- **#487** Offline composer queue is now session-scoped; legacy unscoped
  queues fail closed.
- **#488** Composer Option+Backspace deletes by word; cross-platform key
  routing helpers added.
- **#443/#444** Keyboard enhancement flags pop on normal AND panic exit; the
  raw-mode startup probe is now bounded by a configurable timeout.
- **#449** Production footer reads statusline colors from `app.ui_theme`
  rather than the bespoke palette.
- **#506** `display_path_with_home` no longer mutates `HOME` in tests; the
  flake on shared-env CI is gone.

### Self-update / packaging
- **#503** `update.rs` arch mapping uses release-asset naming (`arm64`/`x64`)
  instead of the raw Rust constants. The platform-asset selector also rejects
  `.sha256` siblings as primary binaries. Tests now live alongside the source
  in `mod tests` (the `#[path]`-based integration test was removed because it
  duplicated test runs and forced a `pub(crate)` helper that no real caller
  used).
- **`Max 5 in flight` wording updated** in `agent_spawn` description,
  `prompts/base.md`, and `docs/TOOL_SURFACE.md` so the model sees the real
  default cap (10) and the configuration knob name.

### CI / release docs (#507)
- Pruned three duplicated/dead workflows: `crates-publish.yml`, `parity.yml`,
  `publish-npm.yml`. Their gates already run in `ci.yml` for every push/PR.
- `release.yml` build job now allows `parity` to be skipped (it only runs on
  tag push), unblocking `workflow_dispatch` reruns. The job still fails
  closed on a real parity failure.
- `RELEASE_RUNBOOK.md` reconciled: crate publishing is documented as the
  manual `scripts/release/publish-crates.sh` flow (no automated workflow);
  references to the deleted workflows removed.
- `CLAUDE.md` notes the `RELEASE_TAG_PAT` requirement for the auto-tag →
  release.yml chain (without it, the tag is created but `release.yml` does
  not fire) and documents the `workflow_dispatch` parity-skip behavior.

### Docs
- `docs/COMPETITIVE_ANALYSIS.md` added — capability matrix vs OpenCode and
  Codex CLI, gap analysis, and recommended implementation order.

### Verification (this branch)
- `cargo fmt --all -- --check` ✓
- `cargo check --workspace --all-targets --locked` ✓
- `cargo clippy --workspace --all-targets --all-features --locked -- -D warnings` ✓
- `cargo test --workspace --all-features --locked` ✓ (1809 + supporting)
- Parity gates ✓ (snapshot, parity_protocol, parity_state)
- `cargo build --release --locked -p deepseek-tui-cli -p deepseek-tui` ✓
- Lockfile drift guard ✓
- `deepseek doctor --json` clean
- `deepseek eval` (offline harness) success=true, 0 tool errors

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 01:57:37 -05:00
Claude 2009f037b3 docs(zh-CN): unify cross-reference style in ARM64 section
Audited README.zh-CN.md against sparanoid/chinese-copywriting-guidelines.
The whole file already follows the CJK<->Latin spacing rule and uses
full-width punctuation correctly — automated scan returns zero
violations. The only inconsistency was the new "Linux ARM64" block
referencing the source-install section as `[「从源码安装」]`, while every
other cross-reference in this README uses the bare-link style. Drop the
`「」` brackets so it matches.

(Heti / sivan/heti is a runtime CSS library — it can't apply to README
rendering on GitHub, but is worth wiring up if we ever publish a docs
site.)

https://claude.ai/code/session_01Fg1FKMtDxVnC4pp6bNBRCS
2026-05-03 05:21:31 +00:00
Claude 0e5afe0b01 feat(v0.8.8): linux ARM64 prebuilts + install docs overhaul
Triggered by a Telegram report from a Chinese user trying to deploy
DeepSeek TUI on a HarmonyOS ARM64 thin-and-light: `npm i -g deepseek-tui`
exited with `Unsupported architecture: arm64 on platform linux` because
v0.8.7 only published x64 Linux artifacts. They worked around it with
`cargo install`, but the README never documented that path for ARM users.

This PR closes that gap on three layers:

- **Release workflow** — add `aarch64-unknown-linux-gnu` to the build
  matrix using GitHub's `ubuntu-24.04-arm` runner. v0.8.8 will publish
  `deepseek-linux-arm64` and `deepseek-tui-linux-arm64` alongside the
  existing x64/macOS/Windows assets, plus add the row to the Release
  body's manual-download table.

- **npm wrapper** — uncomment the linux/arm64 row in `ASSET_MATRIX`,
  rewrite the `Unsupported architecture/platform` error to print the
  full `cargo install deepseek-tui-cli deepseek-tui --locked` recipe
  and link to docs/INSTALL.md, and add `DEEPSEEK_TUI_OPTIONAL_INSTALL=1`
  so CI matrices that include unsupported platforms can keep running
  without a binary.

- **Docs** — new docs/INSTALL.md covering every supported platform,
  prebuilt vs. cargo install vs. manual download, cross-compiling x64
  -> ARM64 with `cross` or `gcc-aarch64-linux-gnu`, China mirror setup,
  and a troubleshooting section for the common arm64, MISSING_COMPANION_BINARY,
  and self-update arch-mapping (#503) errors. README and README.zh-CN
  now have an explicit Linux ARM64 quickstart pointing at `cargo install`
  for v0.8.7 today and `npm i -g` for v0.8.8+; the v0.8.7 known-issue
  block is updated to mention both #503 and the missing arm64 prebuilt.

https://claude.ai/code/session_01Fg1FKMtDxVnC4pp6bNBRCS
2026-05-03 04:42:53 +00:00
Hunter Bown d132ea553b docs(readme): note v0.8.7 self-update bug + workaround (#503)
deepseek update fails on every platform because the arch mapping uses
the Rust ARCH constant ('aarch64'/'x86_64') instead of the release
artifact naming ('arm64'/'x64'). Until v0.8.8 ships the fix, users
need to update via npm or cargo.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 22:38:28 -05:00
Hunter Bown e0b6a1b967 feat(v0.8.8): Don't auto-approve git -C ... (fixes #416) 2026-05-02 22:32:13 -05:00
Hunter Bown ccb387374a docs: add Buy Me a Coffee button + FUNDING.yml
GitHub READMEs don't render JS, so use the static shields.io BMC badge
in your custom #5F7FFF color instead of the script tag. FUNDING.yml
adds the native Sponsor button to the repo sidebar.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 22:27:44 -05:00
Hunter Bown 98ab76a99c docs: add v0.8.7 changelog + README release notes
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 22:26:31 -05:00
Hunter Bown 15da881e1d chore(release): bump version to 0.8.7
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 21:56:37 -05:00
Hunter Bown a4102ccad4 fix(tui): allow selection across all transcript cell types
The selection-tightening from 7125172f restricted copy/select to user
and assistant cell bodies only, which made it impossible to copy text
from system notes, thinking blocks, or tool output. Drop the
body-start gate so the rendered transcript block is selectable in full.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 21:54:57 -05:00
Hunter Bown 7125172f67 fix(tui): tighten selection and live task panels 2026-05-02 21:05:15 -05:00
Hunter Bown 5bfc1feb62 v0.8.6: survivability, UX polish, and release hardening
Merge the v0.8.6 feature batch and release hardening.\n\nIncludes the full #373-#380/#382-#402 milestone scope, version bump to 0.8.6, secure /share temp-file handling, Windows-safe self-update replacement, and CI portability fixes.\n\nRemote PR checks passed on the final head before merge.
2026-05-02 20:11:33 -05:00
Hunter Bown c2f0a56538 docs: add v0.8.5 changelog + contributor thanks to README 2026-05-02 16:54:11 -05:00
Hunter Bown 2d61513a9e v0.8.5: config test fixes + default_model session-apply bugfix (#381)
* feat: add config UI support for TUI and web modes

- Introduced a new `config_ui.rs` module to handle configuration UI for TUI and web.
- Updated `TuiOptions` and `App` structures to include `config_path` and `config_profile`.
- Implemented functions to build and apply configuration documents.
- Added tests to ensure the new configuration UI behaves as expected.
- Integrated web configuration session handling into the event loop.
- Updated various modules to accommodate the new configuration options and UI.

* refactor(tui): remove local path reference for schemaui dependency

Remove the local file system path reference for schemaui in favor of
using the published crate from the registry. This change updates the
Cargo.toml to use only the version specification and adds the source
and checksum information to Cargo.lock.

* fix: add AGENTS.md guide and improve config error handling

- Add comprehensive AGENTS.md file with project instructions for AI
  assistants, including build commands, dependencies, and GitHub
  operations guidance
- Introduce is_error field to CommandResult struct for better error
  tracking
- Refactor config application logic to properly handle errors using
  the new is_error flag
- Add test utilities for WebConfigSession to support testing
- Optimize web config event polling by extracting drain logic into
  separate function
- Add unit tests for session-only config application and engine sync
  requirements

* fix(security): add SSRF protection to fetch_url (#261)

Block private, link-local, and cloud metadata IPs in fetch_url HTTP requests. Co-authored-by: JasonOA888

* test(portability): inject paths instead of mutating HOME (Windows fix)

CI's `Test (windows-latest)` job failed because both my new tests
(composer_history and the spawn_supervised crash-dump test) mutated
HOME to redirect `dirs::home_dir()`. That works on macOS / Linux but
not on Windows, where dirs::home_dir() reads USERPROFILE / queries
SHGetKnownFolderPath rather than HOME.

Fix: refactor both modules to expose path-injecting helpers so tests
never need to touch the env var:

- composer_history: split load_history / append_history into thin
  wrappers around load_history_from(&Path) / append_history_to(&Path).
  Tests use the *_to / *_from form with a tempdir path.
- utils::write_panic_dump: same pattern — write_panic_dump_to(&Path)
  takes the crash dir directly. The spawn_supervised end-to-end test
  splits into two: one verifies panic-doesn't-propagate (no on-disk
  side effect needed), one verifies write_panic_dump_to writes the
  expected log format.

Production callers continue to use the env-driven default (`HOME`/
`USERPROFILE` via `dirs::home_dir()`) so no behavior change. Tests
work identically on every platform now.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(tui): clear chat area each frame so stale cells don't bleed into sidebar

ChatWidget's render path was `Paragraph::new(lines).render(content_area, buf)`
with no Block and no Clear — ratatui's Paragraph only writes cells that
contain text, leaving any cell the current frame's paragraph doesn't
touch holding the *previous* frame's contents. With wide tool output
(`gh pr list`, `git log`) emitting ISO-8601 timestamps like
`2026-05-02T07:29:24Z`, then a subsequent shorter-paragraph frame, the
old timestamp tails (`:24Z`, `7:29:24Z`, etc.) persisted on the right
edge of the chat area, visually colliding with the section headers in
the sidebar (`Plan` rendering as `:24Zan`, `Agents` as `:24Zents`).

Fix: render `Clear` over the full content_area before drawing the
Paragraph. Cheap (one buffer-fill per frame) and guarantees stale cells
can never persist into the next frame's render.

Reported in v0.8.5 testing right after install. The other v0.8.5
bordered widgets (composer, sidebar sections, footer) already render
into a Block with a solid background style, so they were never
affected — only the chat area used a bare Paragraph.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(theme): vendor + theme schemaui to deepseek navy palette (config UI)

The schemaui-0.12.0 crate the contributor brought in via #365 ships
hardcoded Color::Gray / Color::DarkGray / Color::White / Color::Yellow
references across its rendering components. Visually it clashed with
the rest of deepseek-tui — the editor area read as gray-on-black on a
TUI that's otherwise navy ink + sky accents. Two ship-day options
weren't acceptable: defaulting back to the legacy modal lost the new
editor's UX, and living with gray was off-brand.

This commit forks schemaui at 0.12.0 into vendor/schemaui-0.12.0 and
themes the rendering layer to match deepseek-tui's palette. The patch
is wired in via a workspace-level [patch.crates-io] override so the
deepseek-tui Cargo.toml continues to depend on `schemaui = "0.12.0"`
and would automatically resolve back to crates.io if we ever drop the
override (e.g. once upstream lands a ColorTheme API).

Changes inside the vendored fork:

- New `src/deepseek_palette.rs` with the brand RGB values:
  SURFACE_INK / SURFACE_RAISED for backgrounds, BORDER_DIM /
  BORDER_ACTIVE for chrome, TEXT_PRIMARY / TEXT_MUTED / TEXT_DIM,
  ACCENT_SKY / ACCENT_BLUE / ACCENT_PURPLE, and STATUS_OK / WARN /
  ERROR. Values mirror crates/tui/src/palette.rs in the workspace.
- `src/lib.rs` exposes the palette module under `cfg(feature = "tui")`.
- `src/tui/view/frame.rs::draw` paints a navy backdrop across the
  full frame area before any child widget renders, so any cell that
  doesn't get explicitly written reads as ink instead of the terminal
  default.
- `tabstrip.rs`, `overlay.rs`, `popup.rs`, `body.rs`, `sections.rs`,
  `footer.rs`, `help.rs`, `fields.rs`: every Color::Gray / DarkGray /
  White / Yellow / Cyan / Blue / Magenta / Red / Green / LightBlue
  swapped out for a deepseek_palette token, plus explicit `bg(...)`
  fills on the top-level Block styles and Paragraph wrappers.
- `Cargo.toml` adds an empty `[workspace]` so the vendored crate
  builds standalone (its dev-deps don't drift into ours).

Workspace-level changes:

- `Cargo.toml` adds `[patch.crates-io] schemaui = { path =
  "vendor/schemaui-0.12.0" }`. Production deepseek-tui builds pick up
  the themed fork transparently.
- `.gitignore` excludes `vendor/.../web/ui/node_modules/` (15 MB of
  npm artefacts the Rust build doesn't need) and the vendored
  Cargo.lock (regenerated locally per build).

Verification:
- cargo build --workspace --all-features: clean
- cargo clippy --workspace --all-targets --all-features --locked: clean
- cargo test --workspace: 1777 passed, 0 failed
- /config inside `deepseek` now opens a navy-themed editor matching
  the rest of the TUI; tabs, body panel, footer, popup, and help
  overlay all read on brand.

Future work tracked separately: upstream a `with_theme(ColorTheme)`
builder API to schemaui so we can drop the fork. Until then, sync the
fork against new schemaui releases when we want their fixes.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Revert "feat(theme): vendor + theme schemaui to deepseek navy palette"

This reverts ed597ccc — vendoring 28,913 lines of schemaui to recolor
a config editor was the wrong tradeoff. Maintenance cost for a
cosmetic match wasn't worth it, and the recolor wasn't even fully
working (terminal-default bg kept bleeding through Style::default()
calls in the form fields).

The simpler path: keep the schemaui-driven editor available as
`/config tui` for users who want the form-style UX, but make bare
`/config` open the legacy native modal that already matches the
deepseek-tui navy chrome by inheritance. No fork, no vendored copy,
no ongoing sync burden.

Changes:
- `git rm -r vendor/schemaui-0.12.0/` (28,913 lines gone)
- Drop `[patch.crates-io]` from workspace Cargo.toml — schemaui
  resolves back to crates.io v0.12.0 unmodified.
- Drop the corresponding `.gitignore` exclusions (no more vendor dir
  to filter).
- `config_ui::parse_mode` default mode flipped from `Tui` to `Native`.
  Bare `/config` → legacy navy modal. Explicit `/config tui` → the
  contributor's schemaui editor (still available, gray-on-default
  chrome, but opt-in). `/config web` and `/config <key>` /
  `/config <key> <value>` unchanged.
- Help text updated to list `[native|tui|web]` in that order.

Verified: cargo build / clippy --workspace --all-features --locked
with -D warnings: clean.

The contributor's work (#365) ships and gets credit; users discover
the alternate editor via the help text.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(tui): paint chat area with explicit navy ink instead of Clear

The Clear-instead-of-fill in 0ae2cead reset cells to the terminal's
default background, which read as a brown-gray on most user setups
even though the rest of the TUI chrome is navy. Replace the Clear
with an explicit Block fill at palette::DEEPSEEK_INK, and pass the
same bg through to the Paragraph itself so streamed text cells
inherit ink rather than bouncing back to terminal default.

Net effect: the chat area visually unifies with the sidebar /
composer / footer instead of showing as a contrasting brown-gray
panel in the middle of an otherwise navy frame.

Stale-cell guarantee from #372-followup is preserved — the Block
fills every cell in the area on each frame, so wide tool output
(`gh pr list` ISO timestamps, etc.) still can't bleed past the
current frame's actual text.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(config): update tests for Native default + fix default_model override in session-only apply

- Update test_show_config_defaults_to_native and
  execute_config_opens_config_view_action to expect
  OpenConfigView (Native) instead of OpenConfigEditor(Tui),
  matching the parse_mode default change from ce98f054.

- Fix apply_document bug where default_model was processed
  in the main key-value loop after model, causing
  set_config_value('default_model') to overwrite the
  runtime model. default_model is now only applied when
  persist=true, preventing session-only edits from being
  silently reverted.

* style: cargo fmt

* chore: remove end-of-night report (session artifact)

---------

Co-authored-by: unic <yuniqueunic@gmail.com>
Co-authored-by: Jason <jason@aveoresearchlabs.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: YuniqueUnic <YuniqueUnic@users.noreply.github.com>
2026-05-02 16:25:03 -05:00
Hunter Bown 43ccbe4a30 chore: merge v0.8.5 backlog into main (#364)
v0.8.5 backlog: atomic writes, panic safety, DeepseekCN, /config <key> <value>
2026-05-02 14:56:59 -05:00
Hafeez Pizofreude aec8d65202 docs: add Star History chart to README (#369)
Co-authored-by: pizofreude
2026-05-02 14:56:31 -05:00
Hunter Bown 07be656412 style: cargo fmt --all sweep
Applies the workspace formatter to the v0.8.5 commits — local builds
ran without `cargo fmt --check` so a few format inconsistencies
slipped through and CI's `parity` job (which runs fmt --check) failed.
Mechanical reflow only; no functional changes.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 14:50:59 -05:00
Hunter Bown 216f6be349 chore(release): bump version to 0.8.5
Workspace + npm wrapper + every internal crate path-dep pin moved from
0.8.4 → 0.8.5. scripts/release/check-versions.sh confirms parity across
the three sources. cargo build / clippy / test all clean.

Pushing this commit to main is the trigger for auto-tag.yml to create
the v0.8.5 tag, which fires release.yml to build the cross-platform
matrix and draft the GitHub Release. The npm publish remains a manual
follow-up (2FA on every publish, no automation token provisioned).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 14:48:18 -05:00
Hunter Bown b678058ff0 test: serialise composer_history env mutation + update rlm test for #358
Two test fixes uncovered by the full-suite run:

1. composer_history tests were using a module-local mutex to serialize
   their HOME env mutation, but other tests in the workspace (config,
   commands::restore, etc.) ALSO mutate HOME without that lock. Switch
   to the crate-wide `test_support::lock_test_env()` so all HOME-
   mutating tests share one mutex.

2. The `prompts::tests::rlm_first_class_guidance_present` test was
   pinning the OLD "RLM Is First-Class" framing that #358 deliberately
   reframed as "RLM Is a Specialty Tool". Renamed the test to
   `rlm_specialty_tool_guidance_present` and updated the assertions to
   guard the new framing — so a future encouraging-language regression
   lights up CI.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 10:40:57 -05:00
Hunter Bown 735287774f fix(approval): cache denials per session — ESC on dangerous command stops re-prompting (#360)
When the user pressed ESC (or Deny / Abort) on an approval prompt, the
TUI correctly told the engine to deny the call. But the model would
often retry the same command — same name, same args, same approval
fingerprint — and the user would see the dialog again, frustrating in
the same way the equivalent yes-yes-yes loop would be.

Symmetric to the existing `approval_session_approved` "always approve"
cache: add `approval_session_denied: HashSet<String>` populated when
the user denies (not when the timeout fired — a timeout might mean
the user stepped away rather than refused). Subsequent ApprovalRequired
events whose approval_key or tool_name match the cache auto-deny via
`engine.deny_tool_call(...)` without re-showing the dialog. Logged via
`tool.approval.auto_deny_session` so the audit log captures the silent
denial.

Closes #360.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 10:35:25 -05:00
Hunter Bown 162e2e027c feat(composer): cross-session input history persistence (#366)
Pressing Up-arrow at the composer now recalls submissions from previous
sessions, not just the current one. Implementation:

- New `crates/tui/src/composer_history.rs` module with `load_history()`
  + `append_history()`. Persists to `~/.deepseek/composer_history.txt`
  (one entry per line, oldest first). Capped at 1000 entries — entries
  older than the cap are pruned at append time so the file never grows
  unboundedly.
- `App::new` now seeds `input_history` from the persisted file at
  startup, so Up-arrow at first launch shows yesterday's prompts.
- `App::submit_message` mirrors each non-slash submission to the
  persisted history. Slash commands and empty/whitespace submissions
  are skipped — those don't help recall and would pollute the stream.
- Consecutive-duplicate dedup so re-submitting the same prompt doesn't
  bloat the file.

The persisted history is global (not per-workspace) — matches the
arrow-up recall pattern users expect from shells and Claude Code. Per-
workspace scoping is a follow-up if multi-project users find it noisy.

Tests: 6 unit tests cover round-trip, slash-skip, empty-skip,
consecutive-duplicate dedup, cap-pruning, and missing-file safety. The
test module uses an internal Mutex to serialize HOME env mutations so
tests can still run in parallel without stomping each other.

Closes #366.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 10:32:45 -05:00
Hunter Bown 40f7037d8e docs(prompts): tell the model to skip markdown tables in terminal output (#372)
Markdown tables don't render correctly in a terminal — monospace fonts
plus variable-width content (especially CJK characters) can't reliably
align column borders. Adds an "Output formatting" section to both
base.md and base.txt instructing the model to prefer plain prose,
bulleted/numbered lists, code blocks, or `- **Label**: value` pairs
over tables. If column-aligned data is genuinely necessary, the
guidance asks for narrow, ASCII-only, 2–3 column tables.

Closes #372.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 10:27:54 -05:00
Hunter Bown 1986a15dd5 feat(persistence): schema migration framework — forward upgrade path for ~/.deepseek/ records (#350)
Every persistence layer in crates/tui/src/ already gates `schema_version
> CURRENT_*` to reject newer-than-supported records (good — prevents
silent truncation when an older binary tries to load a v3 file with v4
fields). What was missing: the **forward upgrade path** for older
records. When we bump CURRENT_SESSION_SCHEMA_VERSION from 3 to 4 to
add a field, every v3 session on disk would silently load with the
new field's serde default — which is OK for additions but breaks
catastrophically for renames or shape changes.

This commit lays down the framework:

**`crates/tui/src/schema_migration.rs`** — new module:

- `SchemaMigration` trait. Each persistence domain implements it once
  with `CURRENT_VERSION`, `DOMAIN`, and an ordered `MIGRATIONS` list
  of `fn(&mut serde_json::Value) -> Result<(), MigrationError>` steps.
  Index `i` migrates from version `i+1` to `i+2`.

- `SchemaMigration::migrate(value, from_version)` — runs every required
  step, stamping `value["schema_version"]` after each step so a partial
  failure leaves a known-state record rather than mixed.

- `MigrationError` — typed error with from/to versions + reason.

- `backup_before_migrate(path, domain)` — creates a `.bak` copy of the
  source file before mutation. Errors are warn-logged and ignored
  (continues because `write_atomic` is itself crash-safe). The `.bak`
  is left on disk as a manual recovery artifact — no automatic GC.

**`schema_migration::registry`** — submodule that registers every
existing persistence domain (session, offline_queue, runtime, task,
automation, automation_run) at its current version with an empty
MIGRATIONS list. No domain has shipped a schema bump yet, so today's
behavior is a no-op. The next bump is now a 4-step recipe:

1. Write the `migrate_<domain>_v<N>_to_v<N+1>` step in this module.
2. Append it to `MIGRATIONS` and bump `CURRENT_VERSION`.
3. Wire `<Domain>Migration::migrate(...)` into the load function in
   the owning module.
4. Add a fixture-based integration test.

Tests: 6 unit tests covering no-op, all-steps, partial migration,
newer-than-current rejection, backup creation, and backup-failure
robustness.

Wiring into individual load sites (session_manager, runtime_threads,
task_manager, automation_manager) is intentionally deferred until the
first actual schema bump needs it — wiring without migrations would
add code paths nothing exercises, and the framework is the part that
needs to land before the next bump can ship safely.

Closes #350.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 10:23:33 -05:00
Hunter Bown 4a282e767b chore(deps): remove 8 unused dependencies flagged by cargo-machete (#341)
cargo-machete found 8 direct dependencies that are declared but never
used in the source tree. Removing them tightens the dependency graph
and shrinks Cargo.lock by 40 lines (transitive crate removals where
nothing else pulled them in).

Removed:
- deepseek-core: tokio (the core scaffold doesn't drive any tasks itself)
- deepseek-config: serde_json (TOML-only crate; no JSON serialization)
- deepseek-mcp: deepseek-protocol (proxy boundary doesn't consume protocol types)
- deepseek-app-server: tracing (no tracing! macros in the transport layer)
- deepseek-tui: bytes, csv, deepseek-tui-cli, tokio-stream
  - bytes: no Bytes-typed I/O paths in the TUI
  - csv: agent_swarm/spawn_agents_on_csv removed in #336/#357
  - deepseek-tui-cli: TUI is the runtime, not the dispatcher; no facade calls
  - tokio-stream: futures-util::StreamExt is sufficient for our SSE / mpsc paths

Verified by grep across each crate's `src/` — no `use` of the dep, no
fully-qualified path references. cargo build, cargo clippy -D warnings,
and cargo test continue to pass with the slimmed graph.

Closes #341.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 10:18:31 -05:00
Hunter Bown f0e1a6c63a docs(prompts): tighten /rlm guidance — specialty tool, not first-class (#358)
The previous rlm prompt guidance ("Treat rlm as a normal reasoning
tool, not a last-resort escape hatch") encouraged the model to reach
for rlm in cases where a direct read_file or focused agent_spawn would
do better. The "RLM Is First-Class" framing was too encouraging given
that rlm is genuinely a specialty tool: it pays off ONLY when the input
can't fit in the model's context window.

Three audit items from #358 addressed:

1. **Reaching for rlm too often.** Reframed as "specialty tool" with
   explicit do-not-use-when guidance front-loaded. The decomposition
   workflow now says "ONLY when an input genuinely doesn't fit" with
   a concrete size threshold (~50K tokens / a whole file / a long
   transcript / a multi-document corpus).

2. **Tool description encourages overuse.** The rlm tool's description()
   now leads with "DO NOT use this tool when..." (input fits, grep
   suffices, short classification, interactive exploration), and only
   then describes the legitimate use cases. Adds explicit cost/speed
   caveat.

3. **Helpers documented as if they were tools.** Both the rlm tool
   description and base.md/base.txt now state plainly: `llm_query`,
   `llm_query_batched`, `rlm_query`, `rlm_query_batched` live INSIDE
   the Python REPL. They are functions the sub-agent uses, NOT
   separately-callable tools the model invokes.

Closes #358.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 10:16:31 -05:00
Hunter Bown bc13dbfee7 fix(tui): show live submit disposition in composer hint (#345)
When the user has typed something into the composer and hits Enter, the
message goes to one of four fates depending on engine state:

- Immediate (idle + online) — most common, sends right away
- Steer (busy + tool execution) — forwards mid-turn
- QueueFollowUp (busy + streaming text) — parks for after TurnComplete
- Queue (offline) — parks on offline queue

Previously the user had no way to tell which would fire BEFORE pressing
Enter. The disposition flips with fast-changing internal state (whether
the model is currently streaming text vs. running a tool, whether
network connectivity has just dropped) and only the post-submit status
toast hinted at the result — which is too late if you wanted a different
behaviour.

Fix: extend the composer's bottom hint line so when the composer has
non-empty content, it shows what Enter will do RIGHT NOW. The hint flips
live with engine state, so the user sees the real behaviour before
pressing Enter:

  ↵ steer into current turn        (sky blue, busy + tool execution)
  ↵ queue for next turn            (muted, busy + streaming)
  ↵ offline queue (no engine)      (warning yellow, offline)

The Immediate case stays unhinted — that's the default and surfacing it
would be noise.

Closes #345.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 10:13:45 -05:00
Hunter Bown c88b980d52 perf(session): metadata-only path for list_sessions — string-extract metadata block (#337)
`SessionManager::list_sessions` previously called
`serde_json::from_reader` to extract just the `metadata` field, which
forced serde to scan every JSON token in the file just to validate
structure — including the entire `messages` and `tool_log` arrays we
were about to discard. For a user with hundreds of long sessions, a
single startup `list_sessions()` was reading and parsing tens of MB of
JSON.

Optimization: read at most 64 KB up front and string-extract the
top-level `metadata` object with a brace-balanced, string-aware scanner.
Real metadata blocks are < 1 KB and always appear before the large
`messages` payload, so the prefix read covers every realistic case.
Falls back to a full-file read only if the metadata block isn't
extractable from the prefix (legacy or oddly-formatted file).

Net: typical session metadata load goes from O(file size) to O(1 KB)
regardless of conversation length, and the disk read is bounded.

Tests:
- extract_top_level_metadata_skips_huge_messages_array — verifies the
  scanner correctly extracts metadata from a session whose `messages`
  array contains the literal string `"metadata"` in a user message.
- extract_top_level_metadata_handles_braces_inside_strings — verifies
  brace-in-string handling so `{` / `}` inside JSON string values
  don't throw off the depth counter.

Closes #337.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 10:11:37 -05:00
Hunter Bown c5627ebb14 fix(config): /logout + new key now uses the new key (#343)
After running /logout and entering a new API key, subsequent requests
could still be sent with the old key because the resolution path checked
the OS keyring before the in-memory override. The keyring still held
the old credential, so it shadowed the freshly-typed one.

Three changes:

1. **`Config::deepseek_api_key()` — explicit override is now path 0.**
   When `self.api_key` is explicitly set (non-empty, non-sentinel), it
   wins over keyring/env/provider-config. This is what the user just
   typed, so it should be authoritative. Existing keyring-based flows
   are unaffected: users who store their key via `auth set` have
   `self.api_key = None`, so path 1 (keyring) still wins for them.

2. **`clear_api_key()` now wipes the keyring + provider-scoped keys.**
   Previously only the legacy root `api_key = ...` line was stripped
   from config.toml. Now every known provider slot in the OS keyring
   (deepseek, nvidia-nim, openrouter, novita, fireworks, sglang) is
   deleted, and every `api_key` line nested in a `[providers.<name>]`
   table is also stripped.

3. **`/logout` clears the in-memory `Config` too.** The dispatcher
   handler in ui.rs::execute_command_input wipes `config.api_key` and
   every `config.providers.*.api_key` so a future clone of the
   long-lived Config doesn't leak the stale value. The companion
   onboarding flow in ui.rs also stamps the new key onto `config`
   itself rather than only on a one-shot clone, so subsequent
   /provider switches see the new credential.

Test coverage:
- `clear_api_key_strips_root_and_provider_scoped_keys` — verifies all
  three credential locations get wiped from a fixture config.toml.
- `deepseek_api_key_prefers_explicit_in_memory_override` — guards the
  precedence flip.
- `deepseek_api_key_ignores_sentinel_placeholder` — confirms the
  legacy `KEYRING_SENTINEL` placeholder still falls through.

Closes #343.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 10:08:52 -05:00
Hunter Bown 899c703d81 fix(tui): convert remaining tokio::spawn sites to spawn_supervised + restore terminal on panic (#346)
Completes the panic-safety work #346 started in a8be33b3. Converts every
trivial production tokio::spawn site to spawn_supervised so a panicking
task writes a crash dump to ~/.deepseek/crashes/ and the parent process
stays alive.

Sites converted:
- tools/rlm.rs:190 — RLM progress drain
- tools/subagent/mod.rs:888 — run_subagent_task spawn
- tools/subagent/mod.rs:988 — run_subagent_task resume
- core/engine.rs:744 — sub-agent mailbox drainer
- core/engine.rs:1601 — engine event-loop spawn
- lsp/client.rs:127 — LSP writer
- lsp/client.rs:129 — LSP reader
- lsp/client.rs:135 — LSP dispatcher
- rlm/bridge.rs:188 — bridge progress drain
- task_manager.rs:790 — task worker loop
- automation_manager.rs:822 — automation scheduler

Sites left as-is (already panic-safe with their own catch_unwind):
- runtime_threads.rs:1242, 1462 — custom AssertUnwindSafe + catch_unwind
- mcp.rs:322 — MCP SSE loop with custom catch_unwind

Sites that don't need conversion:
- runtime_api.rs:287 — axum::serve runs in the parent task, not spawned
- runtime_api.rs:1583+ — test-helper spawn_test_server inside #[cfg(test)]
- All other spawn calls are in #[cfg(test)] modules where panics are
  expected to propagate.

Also:
- main.rs panic hook now restores the terminal (LeaveAlternateScreen +
  disable_raw_mode) before invoking the original hook, so a panicked TUI
  doesn't leave the user's shell stuck in alt-screen mode.
- Adds spawn_supervised_tests::panicking_task_writes_crash_dump_and_does_not_kill_parent
  that proves a panicking task produces a dated crash log under
  ~/.deepseek/crashes/<task>.log and the parent task completes Ok.

Closes #346.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 10:02:28 -05:00
Hunter Bown 0c55c732a2 chore(tools): full SwarmOutcome cascade — delete swarm.rs + event variant + UI handlers (#357)
Completes the v0.8.5 cleanup #336 started: with the model-callable swarm
surface gone, the supporting event/UI/state plumbing has no consumers.

- Delete crates/tui/src/tools/swarm.rs (2215 lines, parked under
  #![allow(dead_code)] since #336)
- Drop pub mod swarm from tools/mod.rs
- Remove Event::SwarmProgress variant + handler in tui/ui.rs
- Remove app.rs swarm fields: pending_swarm_task_count, swarm_jobs,
  last_swarm_id, swarm_card_index (and SwarmOutcome import + retain)
- Remove subagent_routing.rs swarm helpers: seed_fanout_card_from_tool_call,
  sync_fanout_card_from_tool_result, sync_fanout_card_from_swarm_outcome,
  worker_slot_from_swarm_task, status_to_lifecycle, swarm_task_status_to_lifecycle
- Simplify active_fanout_counts to read directly from the active FanoutCard
- Simplify handle_subagent_mailbox is_fanout to only "rlm" dispatches
- Strip dead "agent_swarm" / "spawn_agents_on_csv" string match arms in
  ui.rs (tool dispatch, task panel refresh, ListSubAgents trigger,
  active-cell skip), tool_card.rs (ToolFamily::Fanout), and tool_routing.rs
  (extract_fanout_prompts function deleted entirely)
- Trim WorkerSlot to id/agent_id/status (label/model/nickname were only
  populated by worker_slot_from_swarm_task); remove unused with_agent ctor
- Remove unused SubAgentManager::max_agents and ::available_slots methods
  (only swarm.rs called them)
- Update widgets/agent_card.rs doc comments to point at rlm + future
  multi-child dispatch instead of agent_swarm

FanoutCard decision: kept. It remains the visual primitive for rlm and
for any future multi-child dispatch the parent agent makes via repeated
agent_spawn calls.

Net: 2698 lines removed, 90 added.

Closes #357.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 09:56:33 -05:00
Hunter Bown 42eea19066 fix(tui): route bracketed paste to provider picker key entry instead of composer (#342)
Add handle_paste(text) -> ViewAction method to the ModalView trait with a
default no-op. ProviderPickerView overrides it in KeyEntry stage to
sanitize and append pasted text to api_key_input (rejecting whitespace
in the same way as the Char handler).

Wire into the Event::Paste handler in ui.rs: before falling through to
app.insert_paste_text(), check view_stack.handle_paste(). If the top
modal consumes the paste, skip the composer entirely. If a modal is
open but does NOT consume the paste, also skip the composer — any
modal that receives paste while focused should handle it, not leak
into the chat input.
2026-05-02 02:33:26 -05:00
Hunter Bown 47bb91a9b7 fix(commands): wire /config <key> <value> to setter — args no longer silently ignored (#338)
Add config_command(app, arg) that dispatches three paths:
  /config (no args)       -> opens interactive editor (existing behavior)
  /config <key>           -> shows current value of a single setting
  /config <key> <value>   -> sets value via existing set_config_value

Keys like model, approval_mode, locale, auto_compact, calm_mode,
show_thinking, mode, max_history, sidebar_width, sidebar_focus,
composer_density, composer_border, transcript_spacing are all read
live from App state for the /config <key> display path.

Unknown keys show a helpful error referencing /help config.
2026-05-02 02:28:25 -05:00
Hunter Bown e5f56dee82 feat(config): add ApiProvider::DeepseekCN variant for China endpoint (#361)
Add DeepseekCN as a first-class provider variant with:
- Enum variant + parse/as_str/display_name/all methods
- DEFAULT_DEEPSEEKCN_BASE_URL (https://api.deepseeki.com)
- Auto-detection when base_url contains api.deepseeki.com
- Locale-based auto-suggest: if no provider is configured and
  system locale (LC_ALL/LC_MESSAGES/LANG) starts with 'zh-*',
  the TUI defaults to DeepseekCN at startup
- ProvidersConfig.deepseek_cn for provider-scoped credentials
- All match arms updated across config.rs, client.rs,
  provider_picker.rs, main.rs, and ui.rs
- provider_picker tests updated for the 7th provider entry
2026-05-02 02:10:57 -05:00
Hunter Bown a8be33b35b fix(tui): panic safety foundations — spawn_supervised wrapper + process panic hook (#346)
Add spawn_supervised(name, location, future) to utils.rs that wraps
futures in AssertUnwindSafe + catch_unwind, logs panics via tracing::error!,
and writes crash dumps to ~/.deepseek/crashes/.

Add process-level panic hook to main.rs that writes crash dumps before
the default hook fires.

Convert persistence_actor::spawn_persistence_actor as the first
spawn_supervised caller to prove the wiring. Remaining 34 tokio::spawn
sites marked as follow-up for a focused PR.

Also fix save_mcp_config in main.rs to use write_atomic (missed in #355).
2026-05-02 01:53:50 -05:00
Hunter Bown 5bd63c779a fix(tui): atomic file writes for ~/.deepseek/ persistence (#355)
Add write_atomic helper (NamedTempFile + fsync + rename) in utils.rs.
Convert all non-append fs::write sites:
  - session_manager.rs: save_session/save_checkpoint/save_offline_queue_state
  - workspace_trust.rs: write_trust_file_at
  - task_manager.rs: write_json_atomic → delegates to write_atomic
  - runtime_threads.rs: write_json_atomic → delegates to write_atomic
  - mcp.rs: save_config/init_config/save_legacy
  - audit.rs: buffered append with flush_and_sync after each event
  - runtime_threads append_event: add sync_all after flush
2026-05-02 01:50:21 -05:00
Hunter Bown aa23182674 chore(tools): remove /swarm command + agent_swarm/spawn_agents_on_csv tool surface; park swarm.rs pending #357 cascade (#336)
Surface removed: /swarm slash command, agent_swarm, spawn_agents_on_csv, swarm_status, swarm_result, swarm_cancel tools, report_agent_job_result. Prompts/docs/tests updated. swarm.rs parked with #![allow(dead_code)] pending the full cascade in #357. RLM prompt audit tracked in #358.
2026-05-02 01:30:23 -05:00
Hunter Bown 0ca0570a86 fix(tui): preserve composer draft when navigating input history (#283) 2026-05-02 00:52:08 -05:00
Hunter Bown f1f601c28b perf(tui): persistence actor for session save/checkpoint I/O
Replaces synchronous disk writes on the UI thread with a dedicated
persistence actor task. The UI now try_sends a PersistRequest and
returns immediately — keyboard input is never gated on write completion.

Changes:
- New persistence_actor module with bounded-coalescing actor
- Actor spawns at TUI startup; global singleton so no App struct change
- All persist_checkpoint/persist_session_snapshot/clear_checkpoint calls
  replaced with persistence_actor::persist(PersistRequest::...)
- Dropped redundant TurnStarted persist (nothing changed between
  SendMessage's checkpoint and TurnStarted)
- Fixed collapsible_if clippy lint

This is the P0 fix for the post-send terminal freeze caused by
serialising 500KB+ sessions to disk on the UI thread.
2026-05-02 00:13:45 -05:00
Hunter Bown 6659026dc1 Merge feat/swarm-command-phase-a into feat/v0.8.5
Resolved conflict in commands/mod.rs: both branches added entries
to the COMMANDS registry; kept the /swarm command entry.
2026-05-02 00:04:43 -05:00
Hunter Bown 24fc5b6de1 feat(i18n): Phase 1c-extra — keybinding descriptions, /home, /settings, /help labels
Closes the gate the maintainer set for v0.8.5: every / command, /help,
and /settings should look perfect in both English and Chinese before
multi-agent work begins. v0.8.4 shipped Phase 1a/b/c (88 MessageIds)
but four mixed-language gaps remained:

1. **Keybinding descriptions** (41 entries) — the help overlay showed
   translated section labels (Phase 1c) over English description text.
   `KeybindingEntry` now carries `description_id: MessageId` instead
   of a raw `&'static str`; all 41 descriptions translated to
   en/ja/zh-Hans/pt-BR.

2. **Settings: header** — `Settings::display` now takes a `Locale`
   and resolves the title via `MessageId::SettingsTitle`. The
   field-name keys (auto_compact, calm_mode, etc.) intentionally stay
   English — they are the literal TOML keys users edit.

3. **/home dashboard** — entirely English before. ~25 lines of section
   headers, mode tips, and quick-action hints translated. Path
   interpolations route through `display_path` (privacy invariant).

4. **/help <topic>** text command — the inline labels `Usage:` and
   `Aliases:` plus the `Unknown command:` fallback all use tr().

Also adds three buffer-render tests confirming the help overlay /
settings / home dashboard render in zh-Hans without missing markers
or English bleed-through.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 23:57:35 -05:00
Hunter Bown 87f42656a7 feat(swarm): add /swarm command with sequential|mixture|distill|deliberate modes (Phase A foundation, #303) 2026-05-01 23:48:24 -05:00