summaryrefslogtreecommitdiff
path: root/modules/ai-vterm.el
Commit message (Collapse)AuthorAgeFilesLines
* feat(ai-vterm): add graceful agent close on M-f9 / C-S-f9Craig Jennings2026-05-211-28/+91
| | | | | | | | cj/ai-vterm-close tears an agent down cleanly: it kills the agent's tmux session (stopping the process), removes the vterm window when it isn't the only one in the frame, then kills the buffer. It targets the current agent buffer, the sole live agent, or prompts among several, and confirms before killing since that interrupts work in progress. I also folded the whole F9 family onto ai-vterm. M-f9 used to run cj/toggle-gptel, but gptel is broken right now (the local fork doesn't load, so gptel-make-anthropic is void), and grouping every ai-vterm command under F9 reads better anyway. M-f9 is the primary close binding. C-S-f9 is a second binding that the Wayland/PGTK layer may swallow on some machines. I covered it with 7 tests over the tmux-kill helper, the per-buffer teardown, and target selection, mocking process-file and the prompt at the boundary.
* feat(ai-vterm): default to bottom-75% on laptop, right-50% on desktopCraig Jennings2026-05-201-18/+52
| | | | | | | | The agent window's default placement was hardcoded to a right-side split at 50% width. That's wrong on a laptop, where the screen is shorter and a bottom split with more height fits better than a narrow side panel. Pick the default from the host: bottom at 75% height on a laptop, right at 50% width on a desktop, branching on env-laptop-p in cj/--ai-vterm-default-direction and cj/--ai-vterm-default-size. The defaults still feed the existing toggle-capture mechanism, so re-orienting the window mid-session sticks the same way it did before. Renamed cj/ai-vterm-window-width to cj/ai-vterm-desktop-width and added cj/ai-vterm-laptop-height so each axis has its own knob.
* refactor(integrations): five hygiene fixes from the module-by-module re-reviewCraig Jennings2026-05-161-2/+10
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | - markdown-config.el: two related fixes on `markdown-preview'. First, the URL was `https://localhost:8080/imp' but simple-httpd serves plaintext on port 8080 -- the browser hit a TLS handshake against a non-TLS listener and the preview never rendered. Changed to `http://' and switched from `browse-url-generic' to plain `browse-url' so the user's default protocol handler picks the browser. Second, the function used to start the network listener as a side effect of opening a preview; that's split into a separate `cj/markdown-preview-server-start' command and `markdown-preview' now signals a `user-error' (with the recovery command in the message) when the server isn't running. - slack-config.el: wrap the `which-key-add-keymap-based-replacements' call in `with-eval-after-load 'which-key'. Matches the pattern other config modules use and means a slow / missing which-key load won't block requiring slack-config. - ai-vterm.el: pass the inner shell-command-string through `shell-quote-argument' before wrapping in the tmux invocation. The default value with embedded double quotes was safe under the prior literal-single-quote wrap, but a user-customized `cj/ai-vterm-agent-command' containing a single quote silently broke the shell parse. Two existing tests updated to tolerate the post-quote escape shape; new regression test asserts a single-quote-bearing custom command survives. - eshell-config.el: scope the `TERM=xterm-256color' override to eshell-spawned processes only via an `eshell-mode' hook that prepends to a buffer-local `process-environment'. The previous global `setenv' at config-time changed `TERM' for every subsequent `start-process' across the Emacs session, so any subprocess (not just eshell pipelines) inherited `xterm-256color' regardless of whether the receiver could interpret the escapes.
* fix(ai-vterm): autoload cj/toggle-gptel to silence cross-module warningCraig Jennings2026-05-151-0/+6
| | | | | | | | | | | | | | | | | | | | make compile warned that cj/toggle-gptel is not known to be defined when ai-vterm.el is byte-compiled. The M-F9 binding still worked during normal startup because init.el loads ai-config.el after ai-vterm.el, but the dependency was implicit -- byte-compile saw the function symbol unresolved, and loading ai-vterm.el in isolation left M-F9 bound to an undefined function. Declare cj/toggle-gptel as an interactive autoload pointing at ai-config. This silences the warning, keeps ai-vterm.el free of a load-time (require 'ai-config), and makes the load-order contract explicit: the binding works as long as ai-config eventually loads. Test asserts that requiring ai-vterm in isolation leaves cj/toggle-gptel fboundp as an autoload sigil (not a real function). A regression that adds (require 'ai-config) at the top of ai-vterm.el would flip this, and a regression that drops the autoload form would leave fboundp nil.
* refactor(ai-vterm): retire M-F9 buffer picker; bind to cj/toggle-gptelCraig Jennings2026-05-141-65/+9
| | | | | | | | | | | | | | | | | | | | | | | | | | | M-F9 used to invoke `cj/ai-vterm-pick-buffer' (a buffer picker narrowed to alive AI-agent buffers). In practice the F9 plain-key toggle + C-F9 project picker covered the common cases, and the buffer picker rarely earned its keystroke. Rebind M-F9 to `cj/toggle-gptel' so the F9 family covers the two main in-Emacs AI surfaces at one keystroke each: <f9> ai-vterm toggle (unchanged) C-<f9> ai-vterm picker (unchanged) M-<f9> gptel *AI-Assistant* (NEW) Removed entirely: - `cj/ai-vterm-pick-buffer' (the command itself). - `cj/--ai-vterm-pick-buffer-candidates' (its helper). - `tests/test-ai-vterm--pick-buffer-candidates.el' (deleted). Updated: - `tests/test-ai-vterm--f9-in-vterm.el' binding assertions (vterm-mode-map and global) flipped to `cj/toggle-gptel'. - Module commentary + `cj/ai-vterm' docstring describe the new M-F9 behavior. - `cj/toggle-gptel' lives in `modules/ai-config.el'; the binding stays in `ai-vterm.el' next to the rest of the F9 family so the dispatch shape is visible in one place.
* fix(ai-vterm): force buffer swap after F9 toggle-off in lone windowCraig Jennings2026-05-141-1/+13
| | | | | | The original fix (9600611) set a flag at toggle-off and let the next toggle-on detect it. The flag mechanism is right, but the toggle-off itself wasn't observable when bury-buffer couldn't switch the lone window onto a different buffer -- `bury-buffer' falls back to `switch-to-prev-buffer', which no-ops when the window's prev-buffer history contains only the agent itself (common right after a `C-x 1' that cleared the surrounding windows' histories). Without an observable swap, the second F9 found the agent still displayed and routed back through toggle-off, looping the user with no visible effect. Dispatcher now explicitly forces the window onto another buffer (`(other-buffer agent-buf t)`) when the lone window is still showing the agent after `bury-buffer'. The round-trip test now exercises the real `bury-buffer' path instead of simulating it; a new test asserts the lone window's buffer is non-agent after toggle-off.
* fix(ai-vterm): preserve single-window layout across F9 toggleCraig Jennings2026-05-131-10/+41
| | | | | | | | When the agent buffer is the only window in the frame, F9 buries it (correct) but the next F9 redisplayed it as a side split instead of restoring the full-frame layout -- the display-saved path always called `display-buffer-in-direction`, which insists on a split. New `cj/--ai-vterm-last-was-bury` flag tracks which toggle-off path ran. `cj/ai-vterm` sets it to t in the bury branch (one-window-p) and nil in the delete-window branch. `cj/--ai-vterm-display-saved` checks the flag at toggle-on: if t and the frame is still single-window, it replaces the selected window's buffer in place rather than splitting. Either branch consumes the flag so it never stays stale. 5 tests in test-ai-vterm--single-window-toggle.el cover the flag's set/clear paths, the still-one-window guard, and the end-to-end roundtrip.
* fix(ai-vterm): make F9 toggle the agent from inside an agent bufferCraig Jennings2026-05-121-0/+12
| | | | | | vterm binds `<f1>`..`<f12>` to `vterm--self-insert`, so a plain `<f9>` typed while point is in an agent buffer goes to the terminal program instead of the global toggle. That's invisible most of the time — you press F9 from another window — but it bites when the agent buffer is the only window in the frame, because there's nowhere else to press it from. I re-bound the F9 family in `vterm-mode-map` (via `with-eval-after-load 'vterm`) so that `<f9>`, `C-<f9>`, and `M-<f9>` reach `cj/ai-vterm`, `cj/ai-vterm-pick-project`, and `cj/ai-vterm-pick-buffer` from there too. The C-/M- variants aren't actually in vterm's intercept set, but binding them keeps things uniform. New `tests/test-ai-vterm--f9-in-vterm.el`: 4 ERT tests over the `vterm-mode-map` and global bindings. F12's `cj/vterm-toggle` has the same shape of bug and isn't touched here.
* feat(ai-vterm): order the project picker by most-recently-usedCraig Jennings2026-05-111-11/+57
| | | | | | The picker's active group (projects with a live tmux session) used to sort alphabetically. It now leads with projects opened this session, most-recent first, then the rest of the active group alpha, then the no-session group alpha. An in-session list (`cj/--ai-vterm-mru'), pushed to the front by `cj/--ai-vterm-show-or-create' on every open, drives the order. An empty list reproduces the old alphabetical behavior. I also pulled in a fix: `cj/--ai-vterm-tmux-session-name' now sanitizes `.' and `:' in the basename to `_'. tmux disallows those chars in session names and silently rewrites them, so `.emacs.d' really runs in session `aiv-_emacs_d', not `aiv-.emacs.d'. The computed name never matched, so `.emacs.d' was wrongly treated as having no session and landed in the no-session picker group. (A crash-recovery would also spawn a duplicate instead of reattaching.) Sanitizing the same way tmux does keeps the names in sync.
* feat(ai-vterm): keep emacsclient files out of the agent windowCraig Jennings2026-05-111-0/+45
| | | | | | `server-start' leaves `server-window' nil, so `server-switch-buffer' opens an `emacsclient -n' file in the selected window. When I'm typing in the agent vterm, the selected window is the agent window, so "tell the agent to open something" replaced the agent buffer with that file. I wired `server-window' to a function. When the selected window shows an `agent [...]' buffer, it puts the file in a non-agent window instead, splitting one off to the left of the agent when the agent is the only window. emacsclient invocations from anywhere else still go through `pop-to-buffer' unchanged. `cj/--ai-vterm-non-agent-window' picks the target window. It skips the minibuffer, dedicated windows, and any window already showing an agent buffer.
* refactor(ai-vterm): rename Claude-specific names to a generic "agent"Craig Jennings2026-05-111-66/+69
| | | | | | | | | | | | | | | | | I may add other terminal agents to this launcher (aider, an open-source LLM TUI), so the buffer prefix, the user knob, and the internal helpers shouldn't say "Claude". The module name (ai-vterm) and the `cj/ai-vterm-*` customs were already generic. This finishes the job: - buffer prefix `claude [<basename>]` -> `agent [<basename>]` (the `defconst` and the matching display-buffer-alist regex move together) - `cj/ai-vterm-claude-command` -> `cj/ai-vterm-agent-command` (the default still runs the `claude` CLI, with a docstring note on swapping it) - `cj/--ai-vterm-claude-buffers` / `-displayed-claude-window` / `-reuse-existing-claude` -> `-agent-*`, and their test files renamed to match - prose in the module commentary and docstrings, plus the matching test docstrings and buffer-name literals `vterm-config.el` hardcodes the same buffer prefix in `cj/--vterm-toggle-buffer-p` (F12 excludes agent buffers from its candidate set), so that literal moved too. Collapsing it into the shared `cj/--ai-vterm-name-prefix` is a cleanup for another day. After a reload, a project's buffer opens as `agent [foo]` instead of `claude [foo]`. Old buffers keep their names until killed. I also corrected two stale `eshell-vterm-config.el` references in ai-vterm.el docstrings (that module was split into `vterm-config.el`). Two things keep saying "Claude": the `cj/ai-vterm-agent-command` default value (the actual CLI), and the "Claude Code" example in `vterm-config.el`'s cursor-restore docstring (a concrete TUI example, not branding). 90 tests pass. `make validate-modules` clean.
* feat(ai-vterm): name the tmux session's first window "ai"Craig Jennings2026-05-111-6/+20
| | | | I pass `tmux new-session -n` so the window running the AI tool shows up as "ai" in the window list instead of auto-naming after the running program. A shell opened by hand in a later window still auto-names (e.g. "zsh"), so the two read distinctly. The name is a new `defcustom` (`cj/ai-vterm-tmux-window-name`), symmetric with the session-prefix custom.
* feat(ai-vterm): surface surviving tmux sessions in the project pickerCraig Jennings2026-05-111-21/+119
| | | | | | Each project's tmux session is now named `<cj/ai-vterm-tmux-session-prefix><basename>` (default `aiv-`), so `tmux ls` can be filtered to AI-vterm's own sessions. After an Emacs crash the C-F9 project picker reads `tmux list-sessions`, matches surviving sessions back to their directories, and sorts those to the top: `[detached]` when only the tmux session is alive, `[running]` when a vterm buffer exists. The rest follow alphabetically. With tmux missing or no server running, it falls back to a plain alphabetical list. The picker's collection is a completion table that pins display order so Vertico doesn't re-sort and undo the active-first grouping. The prefix is a new `defcustom` rather than `claude-`, which collides with hand-rolled tmux sessions. Sessions named before this change use the bare basename and won't be matched afterward. One `tmux kill-server` clears any orphans.
* refactor(cj-window-geometry): rename to cj-window-geometry-libCraig Jennings2026-05-101-1/+1
| | | | | | | | | | | | Same naming-convention fix as the other library renames in this series. Rename modules/cj-window-geometry.el -> modules/cj-window-geometry-lib.el and tests/test-cj-window-geometry.el -> tests/test-cj-window-geometry-lib.el. Update provide forms, file headers, and the (require 'cj-window-geometry) call sites in cj-window-toggle-lib.el, ai-vterm.el, vterm-config.el, and the test file. No behavior change.
* refactor(cj-window-toggle): rename to cj-window-toggle-lib for naming ↵Craig Jennings2026-05-101-1/+1
| | | | | | | | | | | | consistency Rename modules/cj-window-toggle.el -> modules/cj-window-toggle-lib.el and tests/test-cj-window-toggle.el -> tests/test-cj-window-toggle-lib.el. Update provide forms, file headers, and the (require 'cj-window-toggle) call sites in ai-vterm.el, vterm-config.el, and the test file. Same rationale as the cj-cache and cj-org-text renames -- library files in this codebase are suffixed `-lib'. No behavior change.
* refactor: extract toggle-state helpers shared by F9 and F12Craig Jennings2026-05-101-59/+13
| | | | | | The F12 commit (554b32d) flagged this as a follow-up: ~120 lines of capture-state and display-saved logic were duplicated between modules/ai-vterm.el and modules/eshell-vterm-config.el. The only differences were the default direction (right for F9, below for F12) and the customization name for the fallback size. Extract the shared logic into modules/cj-window-toggle.el so both consumers reduce to thin delegates that pass their state-var symbols and defaults. The state vars stay where they were, so existing tests against each consumer's helpers keep working. 10 new tests cover the parameterized helpers in isolation. All consumer tests still pass.
* refactor: extract window-geometry helpers shared by F9 and F12Craig Jennings2026-05-091-49/+8
| | | | | | | | | | | | | | `ai-vterm.el` (F9) and `eshell-vterm-config.el` (F12) both grew the same geometry-preservation pattern: classify a window's position, capture its body size, map cardinal direction to its frame-edge variant. The shared helpers were sitting as near-duplicates in both modules. With two real consumers established, the abstraction has the right shape. I pulled them into `cj-window-geometry.el`. The new module exposes three pure helpers: - `cj/window-direction` returns right/below/left/above based on edges relative to `frame-root-window`. Takes an optional DEFAULT for the single-window-frame fallback so each consumer picks its own (ai-vterm wants 'right, vterm-toggle wants 'below). - `cj/window-body-size` returns body-cols (right/left) or body-lines (below/above). Same body-vs-total reasoning as before: divider-independent, matches what the user sees. - `cj/cardinal-to-edge-direction` maps right/left/below/above to rightmost/leftmost/bottom/top, used by each consumer's `display-saved` action. `ai-vterm.el` and `eshell-vterm-config.el` now `(require 'cj-window-geometry)` and call the shared helpers directly. The consumer-specific `capture-state` and `display-saved` bodies stay in each module because they bind to consumer-specific state vars. Extracting those would either need parameter-passing-via-symbol or a macro, both heavier than the duplication they would remove. Tests: 15 in `test-cj-window-geometry.el` covering all four directions, body-size on both axes, cardinal-to-edge mapping, default-arg fallback, and the unknown-direction nil case. Deleted `test-ai-vterm--window-geometry.el` (now redundant) and trimmed four duplicate window-direction/size tests from `test-vterm-toggle--display.el`. Net LOC: each consumer ~40-50 lines lighter, with the new module + tests paying roughly half that back. Full make test green. make validate-modules green.
* feat(ai-vterm): show [running] in picker and F9 redisplays last-usedCraig Jennings2026-05-091-7/+29
| | | | | | | | The C-F9 project picker now flags projects whose claude buffer is alive with a " [running]" suffix on the abbreviated path. I added `cj/--ai-vterm-format-candidate` to compute the display name and routed the picker through it. Before the change, the picker showed every candidate identically, so you couldn't tell at a glance whether picking a project would attach to an existing session or start a fresh one. F9 with two or more alive claude buffers used to open the project picker. That meant after toggling claude-A off, opening claude-B via C-F9, then toggling B off, the next F9 dropped into a picker rather than redisplaying B (the one you just toggled off). I renamed `redisplay-single` to `redisplay-recent` in `cj/--ai-vterm-dispatch` and broadened the trigger from "exactly one alive" to "one or more alive". F9 now redisplays the MRU claude buffer, so it consistently means "toggle THE claude I was last using". The project picker stays explicit on C-F9 for "start a different project", and M-F9 still picks among existing claudes. 2 new tests for the indicator (`format-candidate` flagged + unflagged), 2 dispatch tests renamed to match the new contract. 80 ai-vterm tests pass. Full make test green.
* fix(ai-vterm): harden F9 toggle across multi-window and buffer-moveCraig Jennings2026-05-091-25/+80
| | | | | | | | | | | | | | | | Live-testing surfaced four edge-case failures in the F9 toggle geometry preservation. Each gets a dedicated regression test. - Multi-window squeeze: a captured fraction-of-frame replayed at the wrong size in 3+ window layouts because `display-buffer-in-direction` interprets float widths as fractions of the new window's parent, not the frame. In a flat 3-window layout the parent is the root, but in nested splits it's a sub-tree, and the captured fraction blew the layout up. I switched to absolute integer body-cols and body-lines as the captured unit. The unit is layout-independent. - One-col peek: a claude window captured rightmost (no right divider, body=total) replayed into a middle position (with divider, body=total-1) showed 1 col of the sibling buffer peeking through where claude should have ended. I wrap the integer size in a `(body-columns . N)` / `(body-lines . N)` cons so `display-buffer-in-direction` sets the body explicitly, divider-independent. - Position swap and compounding gap: `direction=right` in `display-buffer-in-direction` splits the selected window, not the frame edge. In multi-window layouts the new claude landed mid-frame instead of where it came from. Each toggle compounded a 1-col loss because the new position picked up a divider the original lacked. I map the cardinal direction to its frame-edge variant (`right` -> `rightmost`, `below` -> `bottom`, etc.) so claude always returns to the captured edge. - Extra window after buffer-move: buffer-move (C-M-arrows) doesn't update the claude window's `quit-restore` parameter, so `quit-window` falls through to bury rather than delete. The window stays alive showing some other buffer. Toggle-on doesn't recognize it and creates a fresh side window, landing at N+1 windows. I switched to `delete-window` with a `one-window-p` guard for the single-window-frame case. One tradeoff: in a layout where claude was deliberately in a middle position (e.g. agenda | claude | todo), the next toggle pulls it to the frame edge rather than the middle. The side-panel pattern is the design intent and the common case. 7 new regression tests covering each scenario. 80 ai-vterm tests pass. Full make test green.
* feat(ai-vterm): F9 toggle/redisplay/pick + persistent split geometryCraig Jennings2026-05-081-26/+334
| | | | | | | | | | | | | | | | | | | | | | | F9 was a single command that always opened the project picker. Three small frustrations stacked up. With one claude buffer open and not visible, F9 was a redundant prompt to pick a project that already had a session. With claude visible, there was no way to bury it without M-x quit-window. With two projects' buffers alive, swapping between them was a buffer-switch chore. F9 is now a dispatch: - Claude visible in this frame: quit the window (toggle off) and capture the geometry first. - Exactly one claude buffer alive but hidden: re-display it (DWIM single-buffer case). - Zero or two-plus alive: fall through to the project picker. C-F9 is the always-pick-project entry point for explicit project switches. M-F9 is a buffer picker over the alive claude buffers. If a claude window is currently shown, the picked buffer replaces it in that window so the split orientation and size carry over. The shown buffer sorts last in the picker with a [shown] marker so RET picks "the other one." Split geometry persists across toggles. Two module-level vars (cj/--ai-vterm-last-direction, cj/--ai-vterm-last-size) capture at toggle-off and feed a custom display action. After M-S-t flips claude from right to bottom, F9 toggle-off-then-on returns it at the bottom. After a mouse resize, the next toggle restores that fraction. State is per-session. Restarts reset to default right/0.5. Two display-buffer fixes came out of testing: - save-window-excursion around (vterm name) keeps the dashboard from being buried on a fresh F9 at startup. vterm calls pop-to-buffer-same-window internally, which would otherwise replace the selected window's buffer before the alist could route the new one. - The action chain swaps display-buffer-use-some-window for a more specific cj/--ai-vterm-reuse-existing-claude. The generic version stole non-claude windows on C-F9 when the user was focused inside claude (claude on bottom, code on top -> new project landed in the code window). The specific version only reuses windows that already show a claude buffer. I reclaimed C-F9 from the gptel toggle in ai-config.el. C-; a t still binds gptel. I added eight new test files (claude-buffers, displayed-claude-window, dispatch, pick-buffer-candidates, window-geometry, capture-state, display-saved, reuse-existing-claude) plus a regression test on cj/--ai-vterm-show-or-create for the dashboard-preservation fix. All 73 ai-vterm tests pass and the full make test suite is green.
* fix(ai-vterm): direction-based display + per-project tmux session namesCraig Jennings2026-05-071-4/+47
| | | | | | | | | | | | Two post-ship issues blocked practical use of the new launcher. The display rule used `display-buffer-in-side-window` with `(dedicated . t)`. Side-window dedication caused `set-window-buffer` to error during `buffer-move` (C-M-arrows), which left a half-finished swap with both sides showing the claude buffer. Then `switch-to-buffer` on a non-claude buffer in that dedicated window split instead of replacing. I rewrote the rule as `display-buffer-reuse-window -> display-buffer-use-some-window -> display-buffer-in-direction (right)`. The resulting window is ordinary, not dedicated, so swap and replace work normally. I also narrowed `vterm-toggle`'s broad lambda (which matches any vterm-mode buffer) to exclude `claude [` buffers. Otherwise vterm-toggle's `:defer` made it install last and capture our buffers first with its own bottom-split + dedicated treatment. The tmux side: vterm's auto-launch hook ran a bare `tmux\n`, so each session got an auto-named one. After an Emacs crash the tmux session would survive but I couldn't find it. A second F9 just spawned another. The launcher now sends `tmux new-session -A -s <basename> -c <dir> '<claude>; exec bash'`. The `-A` reattaches to a same-named session if it already exists. The `exec bash` keeps the tmux window alive if claude itself exits. A `cj/--ai-vterm-suppress-tmux` flag tells the existing vterm hook to skip its bare tmux step so the named launch runs instead. 11 new tests across 2 files cover the session-name and launch-command helpers. I updated tests for show-or-create and the display rule. All 34 ai-vterm tests are green.
* feat(ai-vterm): add Claude launcher with vertical-split vtermCraig Jennings2026-05-071-0/+198
The new module picks a Claude-template project from a filtered completing-read list. It scans the same roots the `ai` shell launcher uses, then opens or reuses a vterm buffer named `claude [<repo>]` on the right. F9 launches it. The prior `cj/toggle-gptel` binding moves from F9 to C-F9 so both AI tools share the same physical key. The display rule chains reuse-window -> use-some-window -> in-direction (right). The resulting window isn't dedicated. That matters because side-window dedication was breaking `buffer-move` (C-M-arrows) and `switch-to-buffer` replacement on the claude buffer. I also narrowed `vterm-toggle`'s display rule to skip `claude [` buffers. Otherwise it claimed them first with its bottom-split + dedicated treatment. I added 23 tests across 5 files: the buffer-name transform, candidate walker, show-or-create dispatch, picker, and display rule. Design lives at docs/design/ai-vterm.org.