<feed xmlns='http://www.w3.org/2005/Atom'>
<title>dotemacs/modules/ai-vterm.el, branch load-graph-classify-end</title>
<subtitle>My Emacs configuration
</subtitle>
<id>https://git.cjennings.net/dotemacs/atom?h=load-graph-classify-end</id>
<link rel='self' href='https://git.cjennings.net/dotemacs/atom?h=load-graph-classify-end'/>
<link rel='alternate' type='text/html' href='https://git.cjennings.net/dotemacs/'/>
<updated>2026-05-24T21:57:56+00:00</updated>
<entry>
<title>docs(load-graph): classify domain, integration, and optional modules</title>
<updated>2026-05-24T21:57:56+00:00</updated>
<author>
<name>Craig Jennings</name>
<email>c@cjennings.net</email>
</author>
<published>2026-05-24T21:57:56+00:00</published>
<link rel='alternate' type='text/html' href='https://git.cjennings.net/dotemacs/commit/?id=cad351ec00c3f78cfb6e203d87c7309a620e485c'/>
<id>urn:sha1:cad351ec00c3f78cfb6e203d87c7309a620e485c</id>
<content type='text'>
Eighth classification batch: 17 domain/integration/optional modules — ai-config, ai-vterm, browser-config, calendar-sync, calibredb-epub-config, chrono-tools, dirvish-config, dwim-shell-config, erc-config, eshell-config, eww-config, flyspell-and-abbrev, games-config, gloss-config, httpd-config, jumper, latex-config. I annotated each header, added a Batch 8 table to the inventory, and extended the validation allowlist. 82 of 102 modules are now classified.

Almost all are eager only by init order and become command/hook/mode-loaded. calendar-sync stays eager when its .local.el is present. One new hidden dependency: calendar-sync guards its C-; g registration with a boundp shim and doesn't require keybindings, so the binding drops standalone.

I deferred elfeed-config rather than annotate it. Its header edit triggers byte-compilation, and the existing tests only pass when the module loads as interpreted source — the compiled cj/elfeed-process-entries inlines an elfeed struct accessor the stubs can't intercept, and the batch test environment has no elfeed package to build real structs. It needs its tests rewritten first, recorded in the inventory and a new todo task.

Also made the header allowlist scoping test durable: it used games-config (now classified) as its unclassified example; switched to a sentinel name plus a duplicate-entry guard.
</content>
</entry>
<entry>
<title>feat(ai-vterm): add graceful agent close on M-f9 / C-S-f9</title>
<updated>2026-05-22T00:38:05+00:00</updated>
<author>
<name>Craig Jennings</name>
<email>c@cjennings.net</email>
</author>
<published>2026-05-22T00:19:14+00:00</published>
<link rel='alternate' type='text/html' href='https://git.cjennings.net/dotemacs/commit/?id=c38683f13cf361adc93b72c1e87244a0153b2387'/>
<id>urn:sha1:c38683f13cf361adc93b72c1e87244a0153b2387</id>
<content type='text'>
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.
</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(integrations): five hygiene fixes from the module-by-module re-review</title>
<updated>2026-05-16T09:01:04+00:00</updated>
<author>
<name>Craig Jennings</name>
<email>c@cjennings.net</email>
</author>
<published>2026-05-16T09:01:04+00:00</published>
<link rel='alternate' type='text/html' href='https://git.cjennings.net/dotemacs/commit/?id=500687f8d7d5b87ceb33fd959e545746ec9db1ba'/>
<id>urn:sha1:500687f8d7d5b87ceb33fd959e545746ec9db1ba</id>
<content type='text'>
- 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.
</content>
</entry>
<entry>
<title>fix(ai-vterm): autoload cj/toggle-gptel to silence cross-module warning</title>
<updated>2026-05-15T07:21:57+00:00</updated>
<author>
<name>Craig Jennings</name>
<email>c@cjennings.net</email>
</author>
<published>2026-05-15T07:21:57+00:00</published>
<link rel='alternate' type='text/html' href='https://git.cjennings.net/dotemacs/commit/?id=c551230df1cb644d1d97c34a7fbdf9e3d8ca8c78'/>
<id>urn:sha1:c551230df1cb644d1d97c34a7fbdf9e3d8ca8c78</id>
<content type='text'>
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.
</content>
</entry>
<entry>
<title>refactor(ai-vterm): retire M-F9 buffer picker; bind to cj/toggle-gptel</title>
<updated>2026-05-15T04:59:34+00:00</updated>
<author>
<name>Craig Jennings</name>
<email>c@cjennings.net</email>
</author>
<published>2026-05-15T04:59:34+00:00</published>
<link rel='alternate' type='text/html' href='https://git.cjennings.net/dotemacs/commit/?id=6551b17f4735e0ca375319f07f249abefafde892'/>
<id>urn:sha1:6551b17f4735e0ca375319f07f249abefafde892</id>
<content type='text'>
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:

  &lt;f9&gt;    ai-vterm toggle      (unchanged)
  C-&lt;f9&gt;  ai-vterm picker      (unchanged)
  M-&lt;f9&gt;  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.
</content>
</entry>
<entry>
<title>fix(ai-vterm): force buffer swap after F9 toggle-off in lone window</title>
<updated>2026-05-14T05:19:44+00:00</updated>
<author>
<name>Craig Jennings</name>
<email>c@cjennings.net</email>
</author>
<published>2026-05-14T05:19:44+00:00</published>
<link rel='alternate' type='text/html' href='https://git.cjennings.net/dotemacs/commit/?id=6a4f1a1185dd4a7d316376491ff814eea83b9cc0'/>
<id>urn:sha1:6a4f1a1185dd4a7d316376491ff814eea83b9cc0</id>
<content type='text'>
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.
</content>
</entry>
<entry>
<title>fix(ai-vterm): preserve single-window layout across F9 toggle</title>
<updated>2026-05-13T20:33:38+00:00</updated>
<author>
<name>Craig Jennings</name>
<email>c@cjennings.net</email>
</author>
<published>2026-05-13T20:33:38+00:00</published>
<link rel='alternate' type='text/html' href='https://git.cjennings.net/dotemacs/commit/?id=9600611d5f8382ffc849d56a67ba5eb980d64e04'/>
<id>urn:sha1:9600611d5f8382ffc849d56a67ba5eb980d64e04</id>
<content type='text'>
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.
</content>
</entry>
<entry>
<title>fix(ai-vterm): make F9 toggle the agent from inside an agent buffer</title>
<updated>2026-05-12T17:53:11+00:00</updated>
<author>
<name>Craig Jennings</name>
<email>c@cjennings.net</email>
</author>
<published>2026-05-12T17:53:11+00:00</published>
<link rel='alternate' type='text/html' href='https://git.cjennings.net/dotemacs/commit/?id=c72d4abc17ae7bed792fa610c0a67b917e191f4b'/>
<id>urn:sha1:c72d4abc17ae7bed792fa610c0a67b917e191f4b</id>
<content type='text'>
vterm binds `&lt;f1&gt;`..`&lt;f12&gt;` to `vterm--self-insert`, so a plain `&lt;f9&gt;` 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 `&lt;f9&gt;`, `C-&lt;f9&gt;`, and `M-&lt;f9&gt;` 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.
</content>
</entry>
<entry>
<title>feat(ai-vterm): order the project picker by most-recently-used</title>
<updated>2026-05-11T18:27:26+00:00</updated>
<author>
<name>Craig Jennings</name>
<email>c@cjennings.net</email>
</author>
<published>2026-05-11T18:27:26+00:00</published>
<link rel='alternate' type='text/html' href='https://git.cjennings.net/dotemacs/commit/?id=c14d6c8d0f669d694dd765d6f592ce6eb72c50f5'/>
<id>urn:sha1:c14d6c8d0f669d694dd765d6f592ce6eb72c50f5</id>
<content type='text'>
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.
</content>
</entry>
</feed>
