<feed xmlns='http://www.w3.org/2005/Atom'>
<title>dotemacs/tests/test-ai-vterm--display-saved.el, branch main</title>
<subtitle>My Emacs configuration
</subtitle>
<id>https://git.cjennings.net/dotemacs/atom?h=main</id>
<link rel='self' href='https://git.cjennings.net/dotemacs/atom?h=main'/>
<link rel='alternate' type='text/html' href='https://git.cjennings.net/dotemacs/'/>
<updated>2026-06-05T10:28:58+00:00</updated>
<entry>
<title>feat(term): replace vterm with ghostel as the terminal engine</title>
<updated>2026-06-05T10:28:58+00:00</updated>
<author>
<name>Craig Jennings</name>
<email>c@cjennings.net</email>
</author>
<published>2026-06-05T10:28:58+00:00</published>
<link rel='alternate' type='text/html' href='https://git.cjennings.net/dotemacs/commit/?id=ebdf9e466b0e1f86e9b7d76650ac32408273e7a7'/>
<id>urn:sha1:ebdf9e466b0e1f86e9b7d76650ac32408273e7a7</id>
<content type='text'>
I swapped the terminal engine from vterm to ghostel (libghostty-vt) everywhere. term-config replaces vterm-config (the F12 terminal, the C-; x menu, tmux history capture), and ai-term replaces ai-vterm (the F9 Claude-agent launcher). ghostel renders the agent TUI without vterm's flicker under heavy streaming, and one engine now covers every terminal workflow.

Two behavior changes fall out of the swap. F9 launches in a terminal frame now: ghostel renders in TTY frames, so the old GUI-only guard is gone. Terminal windows no longer dim when unfocused: ghostel resolves its palette into the native module per-terminal, so there's no per-window color hook to dim through the way vterm had.

auto-dim drops its vterm color-advice path, the dashboard Terminal button launches ghostel, and the vterm and vterm-toggle packages are removed. The tmux pane-history and copy-mode machinery carried over unchanged. It keys on the pty tty, which ghostel exposes.
</content>
</entry>
<entry>
<title>feat(ai-vterm): gate the F9 launcher to GUI frames</title>
<updated>2026-05-31T21:20:34+00:00</updated>
<author>
<name>Craig Jennings</name>
<email>c@cjennings.net</email>
</author>
<published>2026-05-31T21:20:34+00:00</published>
<link rel='alternate' type='text/html' href='https://git.cjennings.net/dotemacs/commit/?id=b72e794be60c5d4e94c61e5af8c08245773e3393'/>
<id>urn:sha1:b72e794be60c5d4e94c61e5af8c08245773e3393</id>
<content type='text'>
AI-vterm launches a graphical vterm side window, so F9 / C-F9 / M-F9 now decline with a message in a terminal frame instead of opening a vterm. The guard checks the current frame at command time rather than at load. That matters under the daemon, which serves GUI and terminal frames both with display-graphic-p nil at load, so a load-time gate would have disabled the launcher in its GUI frames too.

Routed the three window-behavior tests through a GUI-frame stub, since a batch run is itself a terminal frame.
</content>
</entry>
<entry>
<title>fix(ai-vterm): reuse the frame's half instead of splitting a third</title>
<updated>2026-05-25T09:10:38+00:00</updated>
<author>
<name>Craig Jennings</name>
<email>c@cjennings.net</email>
</author>
<published>2026-05-25T09:10:38+00:00</published>
<link rel='alternate' type='text/html' href='https://git.cjennings.net/dotemacs/commit/?id=3f75b39bbbc4e1c136d3f786024c5c1ed19011ce'/>
<id>urn:sha1:3f75b39bbbc4e1c136d3f786024c5c1ed19011ce</id>
<content type='text'>
F9 split a third window into a frame that was already divided in two, wedging the agent into the middle or a skinny extra column instead of taking the half it should occupy. The display rule only knew how to reuse a window already showing an agent or to split a fresh one. With a plain two-pane layout it fell through to the split and added a window.

I added a display action, cj/--ai-vterm-reuse-edge-window, that reuses the window already forming the target half (the right column on a desktop, the bottom row on a laptop), found by a new cj/window-at-edge helper. It records the displaced buffer with display-buffer-record-window, so toggling off restores that buffer through the native quit-restore-window. The slot's buffer swaps between the agent and whatever it displaced, and no window is created or deleted. The split path still handles a single-window frame or a layout split on the other axis, and the lone fullscreen agent keeps its bury-and-restore-in-place behavior.
</content>
</entry>
<entry>
<title>feat(ai-vterm): default to bottom-75% on laptop, right-50% on desktop</title>
<updated>2026-05-20T22:10:05+00:00</updated>
<author>
<name>Craig Jennings</name>
<email>c@cjennings.net</email>
</author>
<published>2026-05-20T22:10:05+00:00</published>
<link rel='alternate' type='text/html' href='https://git.cjennings.net/dotemacs/commit/?id=feedb78a517a1e86f6bb467756aa2605c7477223'/>
<id>urn:sha1:feedb78a517a1e86f6bb467756aa2605c7477223</id>
<content type='text'>
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.
</content>
</entry>
<entry>
<title>refactor(ai-vterm): rename Claude-specific names to a generic "agent"</title>
<updated>2026-05-11T12:18:20+00:00</updated>
<author>
<name>Craig Jennings</name>
<email>c@cjennings.net</email>
</author>
<published>2026-05-11T12:18:20+00:00</published>
<link rel='alternate' type='text/html' href='https://git.cjennings.net/dotemacs/commit/?id=59b0854464ef29511a0d09f1e76fd1140e675833'/>
<id>urn:sha1:59b0854464ef29511a0d09f1e76fd1140e675833</id>
<content type='text'>
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 [&lt;basename&gt;]` -&gt; `agent [&lt;basename&gt;]` (the `defconst` and the matching display-buffer-alist regex move together)
- `cj/ai-vterm-claude-command` -&gt; `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` -&gt; `-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.
</content>
</entry>
<entry>
<title>refactor(tests): extract shared buffer-cleanup and fake-vterm helpers</title>
<updated>2026-05-09T20:22:20+00:00</updated>
<author>
<name>Craig Jennings</name>
<email>c@cjennings.net</email>
</author>
<published>2026-05-09T20:22:20+00:00</published>
<link rel='alternate' type='text/html' href='https://git.cjennings.net/dotemacs/commit/?id=cdda0a01decb7dc61c71d830823a2ba36207306d'/>
<id>urn:sha1:cdda0a01decb7dc61c71d830823a2ba36207306d</id>
<content type='text'>
Eight test files across the ai-vterm and vterm-toggle suites each shipped a small variant of the same cleanup loop: walk `buffer-list`, kill any buffer whose name starts with a given prefix. Each file also re-implemented the `(string-prefix-p ...)` check inline. One file additionally had its own fake-vterm-mode-buffer constructor for tests that needed `cj/--vterm-toggle-buffer-p` to fire.

I pulled the shared logic into `tests/testutil-vterm-buffers.el`:

- `cj/test--kill-buffers-matching-prefix` is the primitive.
- `cj/test--kill-claude-buffers` and `cj/test--kill-test-vterm-buffers` are thin wrappers for the two prefixes that actually appear.
- `cj/test--make-fake-vterm-buffer` constructs a buffer with `major-mode` set to `vterm-mode` without launching a real vterm process.

Each affected test file now `(require 'testutil-vterm-buffers)` and calls the shared helpers directly. `test-vterm-toggle--buffer-filter.el` keeps a 3-line wrapper that calls both kill helpers in sequence (the only place that needs both prefixes). Net diff: -116 / +72 across 8 test files, plus ~30 lines in the new testutil. Roughly -45 lines after the abstraction is paid for.

No behavior change. 80 ai-vterm tests, 15 vterm-toggle tests, 15 cj-window-geometry tests all pass. Full make test green.
</content>
</entry>
<entry>
<title>fix(ai-vterm): harden F9 toggle across multi-window and buffer-move</title>
<updated>2026-05-09T16:03:10+00:00</updated>
<author>
<name>Craig Jennings</name>
<email>c@cjennings.net</email>
</author>
<published>2026-05-09T16:03:10+00:00</published>
<link rel='alternate' type='text/html' href='https://git.cjennings.net/dotemacs/commit/?id=26e97633c2141051dee418aff5d8993700cf39b2'/>
<id>urn:sha1:26e97633c2141051dee418aff5d8993700cf39b2</id>
<content type='text'>
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` -&gt; `rightmost`, `below` -&gt; `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.
</content>
</entry>
<entry>
<title>feat(ai-vterm): F9 toggle/redisplay/pick + persistent split geometry</title>
<updated>2026-05-09T00:21:26+00:00</updated>
<author>
<name>Craig Jennings</name>
<email>c@cjennings.net</email>
</author>
<published>2026-05-09T00:21:26+00:00</published>
<link rel='alternate' type='text/html' href='https://git.cjennings.net/dotemacs/commit/?id=eab070e5b542f525340ee7f07ea0560944639721'/>
<id>urn:sha1:eab070e5b542f525340ee7f07ea0560944639721</id>
<content type='text'>
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 -&gt; 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.
</content>
</entry>
</feed>
