<feed xmlns='http://www.w3.org/2005/Atom'>
<title>dotemacs/modules/ai-config.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-05-28T01:49:23+00:00</updated>
<entry>
<title>feat(ai): remember the AI-Assistant panel width across toggles</title>
<updated>2026-05-28T01:49:23+00:00</updated>
<author>
<name>Craig Jennings</name>
<email>c@cjennings.net</email>
</author>
<published>2026-05-28T01:49:23+00:00</published>
<link rel='alternate' type='text/html' href='https://git.cjennings.net/dotemacs/commit/?id=fa924acccc2e85bc39433cae608dcc9292795ed2'/>
<id>urn:sha1:fa924acccc2e85bc39433cae608dcc9292795ed2</id>
<content type='text'>
The *AI-Assistant* side window always opened at a fixed 0.4 width, so resizing it by hand was lost the next time it opened. Now the F-key toggle captures the panel's width when it closes and reopens at that width for the rest of the session, the same way the music playlist remembers its height.

The panel has three entry points that all open the same buffer: the toggle, loading a saved conversation, and escalating a quick-ask. I gave them one shared remembered-width var (cj/--ai-assistant-width, owned by ai-config; the other two forward-declare it to avoid a circular require), so the panel comes back at one consistent width whichever door opens it. Capture lives only in the toggle's close path; the other two just replay.

One latent edge: ai-conversations replays with its configurable side, which defaults to right. If that's ever set to top or bottom, the remembered width fraction would land as a height. It can't happen at the default, so I left it as a known edge rather than complicating the call now.

The escalation test needed a top-level defvar for the shared var: a value-less defvar in the module is only file-local to the byte-compiler, so without it the function read the var dynamically and hit void when the test loaded ai-quick-ask without ai-config. ai-quick-ask 13/13 and ai-conversations 47/47 green.
</content>
</entry>
<entry>
<title>refactor(load-graph): route C-; registration through the keymap API</title>
<updated>2026-05-25T00:59:28+00:00</updated>
<author>
<name>Craig Jennings</name>
<email>c@cjennings.net</email>
</author>
<published>2026-05-25T00:59:28+00:00</published>
<link rel='alternate' type='text/html' href='https://git.cjennings.net/dotemacs/commit/?id=08014b2f15e099a1c5e662a17a41290f37aeebf4'/>
<id>urn:sha1:08014b2f15e099a1c5e662a17a41290f37aeebf4</id>
<content type='text'>
Migrated all 31 cj/custom-keymap registration sites across 24 modules from direct (keymap-set cj/custom-keymap ...) calls to cj/register-prefix-map and cj/register-command. Consumers no longer reference cj/custom-keymap directly, so keybindings.el is the sole owner of the C-; prefix and modules reach it only through the API (each already requires keybindings from Phase 2).

Behavior-preserving: I dumped every C-; binding before and after the migration and they're identical: 279 bindings, each resolving to the same command. The which-key label blocks are untouched, since they use string key descriptions and never assumed the keymap existed. I byte-compiled all 24 files (no new free-variable warnings, because the cj/custom-keymap references are gone), and make test, validate-modules, and an init load all pass.
</content>
</entry>
<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>refactor(auth): consolidate the auth-source secret lookup into one helper</title>
<updated>2026-05-23T00:32:32+00:00</updated>
<author>
<name>Craig Jennings</name>
<email>c@cjennings.net</email>
</author>
<published>2026-05-23T00:32:32+00:00</published>
<link rel='alternate' type='text/html' href='https://git.cjennings.net/dotemacs/commit/?id=f6e5885b47e3ab244b293f4e478af7e520180710'/>
<id>urn:sha1:f6e5885b47e3ab244b293f4e478af7e520180710</id>
<content type='text'>
The auth-source-search + funcall-the-secret block was copied four times: calendar-sync--calendar-url, cj/auth-source-secret (ai-config), cj/--auth-source-password (transcription), and cj/slack--get-credential. Each searched authinfo, pulled :secret, and called it when the netrc backend returned a function.

I pulled that into cj/auth-source-secret-value in system-lib (a leaf, so calendar-sync doesn't have to depend on ai-config and drag in the gptel stack). It takes an optional user and returns the secret or nil. The four callers now delegate to it: ai-config layers its required-secret error on top, and the others keep their nil-on-miss behavior. With the direct auth-source-search calls gone, I dropped the now-unused (require 'auth-source) from transcription, slack, and calendar-sync. The helper's autoload covers it.

The transcription tests that exercise the delegated path stay green, and the primitive and the error wrapper get their own tests.
</content>
</entry>
<entry>
<title>fix(ai-config): require gptel backend libs so the fork's constructors load</title>
<updated>2026-05-22T20:34:07+00:00</updated>
<author>
<name>Craig Jennings</name>
<email>c@cjennings.net</email>
</author>
<published>2026-05-22T20:34:07+00:00</published>
<link rel='alternate' type='text/html' href='https://git.cjennings.net/dotemacs/commit/?id=c835491b716e2c1243c124a65e01b22f641657c4'/>
<id>urn:sha1:c835491b716e2c1243c124a65e01b22f641657c4</id>
<content type='text'>
cj/toggle-gptel and gptel chat errored with "Symbol's function definition is void: gptel-make-anthropic". The local gptel fork on :load-path with :ensure nil ships no generated autoloads, so (require 'gptel) loads gptel.el but never gptel-anthropic.el or gptel-openai.el, where the gptel-make-* constructors live. cj/ensure-gptel-backends then reached gptel-make-anthropic before it was defined.

cj/ensure-gptel-backends now requires gptel-anthropic and gptel-openai first, through a small cj/--gptel-load-backend-libs helper. Verified end-to-end: with the fork on load-path, the constructors are fbound and both backends build.
</content>
</entry>
<entry>
<title>refactor(ai-config): switch gptel to local fork, drop tab-width advice</title>
<updated>2026-05-18T06:24:30+00:00</updated>
<author>
<name>Craig Jennings</name>
<email>c@cjennings.net</email>
</author>
<published>2026-05-18T06:24:30+00:00</published>
<link rel='alternate' type='text/html' href='https://git.cjennings.net/dotemacs/commit/?id=b3d41f9a0c63b13ad497a48677de933a3fb5a5cf'/>
<id>urn:sha1:b3d41f9a0c63b13ad497a48677de933a3fb5a5cf</id>
<content type='text'>
I switched the gptel use-package form to `:load-path "~/code/gptel"`
with `:ensure nil` so Emacs loads from the fork instead of the MELPA
release. The fork now carries the narrow `tab-width' copy in
`gptel-org--create-prompt' that karthink redirected the upstream PR
to, which replaces the local `:around' advice on
`gptel--with-buffer-copy-internal' I'd been carrying.

I also dropped the stale test file
`tests/test-ai-config-gptel-prompt-tab-width.el' and the matching
stub in `tests/testutil-ai-config.el'. Both existed only to test the
advice I removed.
</content>
</entry>
<entry>
<title>fix(ai-config): gptel-model must be a symbol, not a string</title>
<updated>2026-05-16T11:07:46+00:00</updated>
<author>
<name>Craig Jennings</name>
<email>c@cjennings.net</email>
</author>
<published>2026-05-16T11:07:46+00:00</published>
<link rel='alternate' type='text/html' href='https://git.cjennings.net/dotemacs/commit/?id=a3723120073f9486ac771ccf150ec32008d8c2ef'/>
<id>urn:sha1:a3723120073f9486ac771ccf150ec32008d8c2ef</id>
<content type='text'>
The default-backend swap to gpt-5.5 (commit 0f029ab5) set
`gptel-model' as the string "gpt-5.5".  gptel's modeline-display
code calls `symbolp' on the model value and signals
`wrong-type-argument symbolp "gpt-5.5"' on every render, which
manifested as Emacs freezing in the AI-Assistant buffer ("Querying
ChatGPT..." → error in process sentinel → repeated redisplay errors).

Both default-setting sites now use `'gpt-5.5' (interned symbol).
The Anthropic backend tolerated string model names so the original
"claude-opus-4-7" string worked, which is why this hadn't surfaced
before.
</content>
</entry>
<entry>
<title>chore(ai-config): switch default gptel backend to ChatGPT / gpt-5.5</title>
<updated>2026-05-16T10:58:24+00:00</updated>
<author>
<name>Craig Jennings</name>
<email>c@cjennings.net</email>
</author>
<published>2026-05-16T10:58:24+00:00</published>
<link rel='alternate' type='text/html' href='https://git.cjennings.net/dotemacs/commit/?id=0f029ab5f844c6399584aec4f5e1cc352e8ab726'/>
<id>urn:sha1:0f029ab5f844c6399584aec4f5e1cc352e8ab726</id>
<content type='text'>
Two places set the default backend + model on gptel initialization
-- `cj/ensure-gptel-backends' (the lazy-init fallback) and the
`use-package gptel :config' block (the eager-set after initialization).
Both now pick the ChatGPT backend with `gpt-5.5' instead of Claude
with `claude-opus-4-7'.
</content>
</entry>
<entry>
<title>feat(gptel-tools): wire web_fetch as a local tool</title>
<updated>2026-05-16T10:17:21+00:00</updated>
<author>
<name>Craig Jennings</name>
<email>c@cjennings.net</email>
</author>
<published>2026-05-16T10:17:21+00:00</published>
<link rel='alternate' type='text/html' href='https://git.cjennings.net/dotemacs/commit/?id=99d93203a867294addf4927ceec5644b9d3bf322'/>
<id>urn:sha1:99d93203a867294addf4927ceec5644b9d3bf322</id>
<content type='text'>
Fourth ADOPT entry from `docs/design/gptel-tools-shortlist.org'.
Lets gptel pull a URL into the conversation so the model can read
docs / current API shapes / etc. without me copy-pasting.

Shape:

- URL must be `http://' or `https://' (file://, ftp://, javascript:,
  scheme-less, etc. are rejected at the validator).
- HTML responses go through `pandoc -f html -t plain' so the model
  gets a reading shape that isn't full of markup; falls back to
  `w3m -dump -T text/html' if pandoc isn't on PATH; signals
  `user-error' if neither is.  Pass `raw=t' to skip stripping.
- Output capped at 200KB by default, hard cap 1MB; `max_bytes'
  argument lets the caller pick a lower cap.  Truncation reported
  inline.
- 4xx / 5xx response codes signal `error' with the code -- the
  alternative is returning an error page body, which the model
  would treat as content.

`:confirm t' on the tool because every call is a real outbound
network request.  The tool's description warns that URLs go
wherever the user-agent points, including internal networks if
that's what the URL names.

`tests/test-gptel-tools-web-fetch.el' -- 20 tests across Normal /
Boundary / Error.  URL validator covers http / https / non-string
/ empty / non-http schemes.  `--effective-max-bytes' covers default
/ low-clamp / hard-cap / passthrough.  Truncate helper covers
under-cap / at-cap / over-cap with the marker.  HTML stripper runs
against real pandoc / w3m (both installed in dev env, neither
should mangle simple markup).  Orchestrator stubs
`cj/gptel-web-fetch--retrieve' via `cl-letf' to cover normal /
raw / 4xx / 5xx / oversize / bad-scheme paths.

Wired into `cj/gptel-local-tool-features' so gptel exposes the
tool on next restart.

Note: `call-process-region' invocation flattened to a single
`with-temp-buffer' with DELETE=t -- the initial draft nested a
second temp buffer and routed output to the inner one, which got
killed before `buffer-string' on the outer ran.  Test caught it.
</content>
</entry>
<entry>
<title>feat(gptel-tools): wire git_status / git_log / git_diff as local tools</title>
<updated>2026-05-16T09:26:20+00:00</updated>
<author>
<name>Craig Jennings</name>
<email>c@cjennings.net</email>
</author>
<published>2026-05-16T09:26:20+00:00</published>
<link rel='alternate' type='text/html' href='https://git.cjennings.net/dotemacs/commit/?id=ceeae9b5e2625e23e6e3792d06a6c8122a36d18b'/>
<id>urn:sha1:ceeae9b5e2625e23e6e3792d06a6c8122a36d18b</id>
<content type='text'>
Three read-only git context tools so gptel can see what's changed
without me pasting `git status` / `git log` / `git diff` output into
every chat turn.  Builds the first batch from the ADOPT bucket in
`docs/design/gptel-tools-shortlist.org`.

Shape per tool:

- `gptel-tools/git_status.el` — `git status --short --branch` for a
  directory inside a git working tree under HOME.  Returns the
  porcelain output, or a "Clean working tree" marker when only the
  branch line is present.
- `gptel-tools/git_log.el` — `git log --oneline -nN` with an optional
  `--since` filter.  N defaults to 20, capped at 100; nil / non-
  integer / out-of-range N falls back to the default.
- `gptel-tools/git_diff.el` — `git diff [REF1 [REF2]] [-- FILE]`.
  Output capped at ~500KB so a runaway diff can't blow up context;
  truncation is reported inline.

Validation is uniform: path must resolve under HOME, must be a
directory, must be inside a git working tree (verified via
`git rev-parse --is-inside-work-tree`).  Color is disabled via
`-c color.ui=false` at the git level (`git status` doesn't accept
`--no-color` directly).

Tests run against real temp git repos created via `process-file`,
not mocked — there's nothing in gptel-tools/git_*.el that's
process-mockable in a meaningful way, and a real `git init` + a
couple of commits is cheaper than building a fake.  31 tests total:
7 for git_status, 11 for git_log, 13 for git_diff.

Wired into `cj/gptel-local-tool-features` so gptel exposes the
three tools on next restart.
</content>
</entry>
</feed>
