aboutsummaryrefslogtreecommitdiff
Commit message (Collapse)AuthorAgeFilesLines
* feat(ai-conversations-browser): dired-style browser for saved GPTel ↵Craig Jennings2026-05-163-0/+488
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | conversations `cj/gptel-load-conversation` prompts via `completing-read`. A dedicated browser shows what each conversation is about at a glance and supports single-key load / delete / rename without having to scroll a minibuffer list. New module `modules/ai-conversations-browser.el` + `cj/gptel-browse-conversations` entry point bound to `C-; a b` ("browse conversations"). Opens `*GPTel-Conversations*` in `cj/gptel-browser-mode` (a `special-mode` derivative). Each row shows date, time, topic slug, and a preview of the most recent message (length configurable via `cj/gptel-browser-preview-length`, default 60 chars). Rows sort newest first. In the browser: - `RET` / `l`: load the conversation (delegates to `cj/gptel-load-conversation` with the file pre-selected via a `cl-letf` stub on `completing-read` so the user isn't prompted twice), then bury the window. - `d`: delete the file under point after `y-or-n-p` confirmation, re-render. - `r`: rename the file under point. Preserves the timestamp, slugifies the new topic, refuses unchanged input and existing targets. - `g`: refresh. - `n` / `p`: next / previous row. - `q`: quit-window. 21 tests cover the helpers (topic parsing, header stripping, preview shaping for truncate / short / empty cases, row-for-file with conversation + non-conversation filenames, rows enumeration, render output for empty + populated cases, newest-first sort, rename-target preservation of timestamp + slug, rename-target error on missing timestamp) and the file-touching actions (delete with y, cancel with n, rename, rename-on-empty-line error).
* feat(ai-rewrite): add directive-picker wrappers around gptel-rewriteCraig Jennings2026-05-163-2/+273
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | `gptel-rewrite` is the killer feature for the keep-gptel decision, and it now lives behind two commands instead of the bare call: - `cj/gptel-rewrite-with-directive` (`C-; a r`, replacing the former bare `gptel-rewrite` binding): completing-read on a directive name from `cj/gptel-rewrite-directives`, then rewrite the active region. - `cj/gptel-rewrite-redo-with-different-directive` (`C-; a R`): replay the prior region with a different directive. The region is preserved via markers stored buffer-local on the first call so it survives accept/reject of the prior rewrite. I picked the hook injection approach over an `:after`-advice + state-capture pattern. `gptel-rewrite-directives-hook` is an abnormal hook gptel-rewrite already consults for a per-call system message. Wrapping the call in a one-shot `let`-binding on that hook gives the directive exactly the lifetime of the rewrite and leaves nothing to clean up. Mutating `gptel-directives` globally would mean either restoring it afterward or living with the change -- both worse than the hook. Directives ship inline as a `defcustom` alist with the six names called out in the task -- `terse`, `fix-grammar`, `refactor-readability`, `add-docstring`, `explain-as-comment`, `shorten`. Customization is a `customize-variable` or `setq` away. 9 tests cover the defcustom shape (default names present, bodies non-empty strings), the wrapper (normal path, no-region error, unknown-directive error, last-state recording), and the redo (replays the prior region, errors when no previous, excludes the current directive from the re-pick prompt). `gptel-rewrite` stubbed in tests so no rewrite UI fires.
* feat(ai-quick-ask): add cj/gptel-quick-ask one-shot commandCraig Jennings2026-05-164-1/+277
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | New module `modules/ai-quick-ask.el`. Bound to `C-; a q` via `cj/ai-keymap` ("quick ask"). `cj/gptel-quick-ask` reads a prompt in the minibuffer, creates a transient `*GPTel-Quick*` buffer in `cj/gptel-quick-mode` (a special-mode derivative with `q` / `escape` / `c` bindings), inserts "Q: <prompt>" plus a response marker, then calls `gptel-request` with `:stream t` so the answer streams into the buffer. Doesn't touch `*AI-Assistant*`, doesn't autosave. Two follow-up commands work in the buffer: - `cj/gptel-quick-dismiss` (`q` / `escape`): delete the window and kill the buffer. Idempotent when the buffer is absent. - `cj/gptel-quick-continue` (`c`): extract the prompt + response, seed them into `*AI-Assistant*` under proper org headings (matching the `cj/gptel--fresh-org-prefix` shape), display the side window, then dismiss the quick buffer. 13 tests cover the pure helpers (initial-text shape, response extraction across normal / multi-line / no-marker / empty inputs, seed-text shape), the ask path (buffer created in right mode, prompt recorded, gptel-request called, empty-prompt error), the dismiss path (kills buffer / no-op when absent), and the continue path (seeds `*AI-Assistant*`, dismisses quick buffer, errors outside a quick buffer). `gptel-request` is stubbed in tests so nothing hits the network.
* feat(ai-conversations): add cj/gptel-autosave-toggle with [AS] mode-line ↵Craig Jennings2026-05-163-0/+107
| | | | | | | | | | | | | | | | | | | | | | | | | indicator `cj/gptel-autosave-enabled` flipped to t inside the save/load entry points with no way back off short of editing the variable or clearing the buffer, and no visible indicator that it was on. Two pieces: - `cj/gptel-autosave-toggle` flips the buffer-local state in the current GPTel buffer. Bound to `C-; a A` via `cj/ai-keymap` (which-key: "toggle autosave"). When autosave is OFF and no filepath is configured yet, the command prompts to save the conversation first so a save target exists; otherwise it just flips the bit. - `cj/gptel-autosave-mode-line-format` surfaces " [AS]" in the mode-line when autosave is on, blank when off. Installed via a `gptel-mode-hook` so every GPTel buffer picks it up. The install helper is idempotent. 6 new tests cover enable/disable paths, the no-filepath prompt path, the not-a-gptel-buffer error path, the mode-line format evaluation, and the install idempotence.
* test(gptel-tools): cover the helpers across the five remaining toolsCraig Jennings2026-05-167-77/+697
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | The gptel-tools files had zero direct coverage outside of `update_text_file`, which landed with its rewrite earlier this session. This commit adds 52 tests across the five other tools. For three of the tools the helpers were already top-level defuns (`read_text_file`, `list_directory_files`, `move_to_trash`). The other two had their main bodies inlined into the `gptel-make-tool` lambda -- I extracted them so the work is testable without mocking gptel itself: read_buffer.el -> `cj/read-buffer--get-content` write_text_file.el -> `cj/write-text-file--run` plus `--validate-path`, `--backup-name`, `--ensure-parent` Test files, by tool: - read_buffer.el (5 tests): normal, empty, buffer-object, text-property-stripping, missing buffer. - write_text_file.el (10 tests): validate-path, backup-name shape, ensure-parent (creates missing / rejects unwritable), run with normal / overwrite / existing-no-overwrite / empty content / outside-home. - read_text_file.el (12 tests): validate-file-path (normal + three error shapes), metadata plist shape, size limits (no-op / hard cap / warning bypass with no-confirm), binary detection (text vs null-byte), special-type EPUB and generic-binary paths. - list_directory_files.el (15 tests): mode-to-permissions (file / dir / executable), get-file-info (file / directory), extension filter (keep / drop / always-dir / nil-extension), format-file- entry, list-directory flat / recursive / error, format-output with and without files. - move_to_trash.el (10 tests): unique-name (no conflict / conflict with timestamp / no-extension), validate-path (HOME / /tmp / outside / critical-dir / missing), perform on file and directory. Each test file uses the same load-path / gptel-stub idiom (`eval-and-compile` block, gptel stub when the real package isn't available) so the byte-compile hook is happy.
* test(ai-conversations): add 36 ERT tests covering helpers and entry pointsCraig Jennings2026-05-161-0/+409
| | | | | | | | | | | | | | | | | | | | | | | | | | | ai-conversations.el shipped without direct tests. This file covers every helper and interactive entry point across Normal / Boundary / Error. Helpers: `cj/gptel--slugify-topic` (ASCII, empty input, all-special, unicode stripped, idempotent, trim, digits); `cj/gptel--timestamp- from-filename` (normal decode, year-edge boundaries, malformed inputs returning nil); `cj/gptel--existing-topics` and `cj/gptel-- latest-file-for-topic` (multi-topic / multi-timestamp temp dirs, empty dir, missing dir, prefix-overlap isolation); `cj/gptel-- conversation-candidates` (newest-first and oldest-first sort order, display-string shape, error on missing dir); `cj/gptel--save-buffer- to-file` (visibility headers prepended, round-trip through `cj/ gptel--strip-visibility-headers`). Autosave: post-response hook saves only when gptel-mode + enabled + filepath are all set; autosave-after-send swallows write errors via `message` instead of signaling; the install-once guard prevents double-registration. Interactive entry points: save/delete exercised via `cl-letf` stubs on `completing-read` and `y-or-n-p`. Per-test temp directories; no writes outside them.
* fix(ai-config): hook gptel-magit wiring per-feature, not on magitCraig Jennings2026-05-162-90/+105
| | | | | | | | | | | | | | | | | | | | | | | | | | | The wiring keyed on `with-eval-after-load 'magit` fires while two of its three references are still undefined. `magit.el` calls `(provide 'magit)` BEFORE its `cl-eval-when (load eval)` block requires `magit-commit` and `magit-stash`. At that moment the `magit-commit` transient prefix doesn't exist, and `transient-append-suffix` silently no-ops on missing prefixes (default `transient-error-on-insert-failure` is nil). The "g Generate commit" and "x Explain" suffixes never landed. Only the M-g binding worked, because `git-commit` IS required before provide. Three per-feature hooks replace the single `'magit` hook: one each on `git-commit`, `magit-commit`, and `magit-diff`. Each hooks the exact dependency the wiring needs, side-stepping the load-order race entirely. The companion test was rewritten to check `after-load-alist` registration rather than drive the hooks through `provide`. Emacs 30 batch mode doesn't fire registered `eval-after-load` callbacks on `provide` alone -- only an actual `load` does. Inspecting the registration is the stronger guard anyway: the regression is "a single `'magit` hook," and the right shape of that check is "no entry under `magit`, entries under `git-commit`, `magit-commit`, `magit-diff`."
* feat(gptel-tools): wire update_text_file as a local tool with testsCraig Jennings2026-05-163-124/+559
| | | | | | | | | | | | | | | | | | | | | I rewrote `update_text_file.el` in pure Elisp. The previous version shelled out to sed for everything, had a stray quote terminator at EOF (line 149) that broke loading, produced literal backslash-n where actual newlines were expected, and prompted via `y-or-n-p` redundantly with gptel's own `:confirm t` flag. The five operations -- replace, append, prepend, insert-at-line, delete-lines -- split into pure string transforms that test without touching the disk. The file-level wrapper validates the path, enforces a 10MB size limit, takes a timestamped backup, and writes atomically. No backup is created when the operation is a no-op. Patterns are literal substrings (not regex) so the model can't trip over metacharacter quoting. `tests/test-update-text-file.el` covers Normal / Boundary / Error per operation plus the file-level wrapper. 48 tests green. Added `update_text_file` to `cj/gptel-local-tool-features` so gptel exposes the tool after restart.
* fix(ai-config): force tab-width=8 in gptel org-mode prompt buffersCraig Jennings2026-05-163-0/+143
| | | | | | | | gptel's `gptel--with-buffer-copy-internal` copies the source buffer's `major-mode` symbol but doesn't run mode hooks. An inherited-org-mode prompt buffer keeps `tab-width` at this config's global default of 4 instead of the 8 that `org-mode-hook` would set. When gptel later parses the prompt buffer with `org-element`, Org's `tab-width=8` guard raises "Tab width in Org files must be 8, not 4." I was hitting this on every second `gptel-magit-generate-message` from COMMIT_EDITMSG. `vc-config.el` sets `git-commit-major-mode 'org-mode'`, and the diffs contained list-shaped content that `org-element--list-struct` parsed. The advice forces `tab-width=8` in the prompt buffer when its inherited mode is org-mode. It's a local workaround for an upstream gap. An upstream patch to run `(delay-mode-hooks (funcall major-mode))` in the buffer-copy is the real fix. I'll send it next.
* fix(ai-config): Ensure gptel-magit is installed via use-packageCraig Jennings2026-05-152-16/+32
| | | | | | | Replace raw autoload calls with a `use-package` declaration so `use-package-always-ensure` installs gptel-magit on machines that haven't run `package-install`, fixing the "Cannot open load file" error on transient setup.
* fix(flycheck): correct abbrev-mode no-arg toggle in cj/prose-helpers-onCraig Jennings2026-05-152-6/+93
| | | | | | | | | | | | | | | | | The shape (if (not (abbrev-mode)) (abbrev-mode)) calls abbrev-mode with no argument -- that's the toggle signature, not a query. When the mode was already on the function flipped it off then on instead of being a no-op. Replaced with (unless (bound-and-true-p VAR) (MODE 1)) for both abbrev-mode and flycheck-mode. 4 ERT tests cover both-off, both-on, and the two mixed states. Also ran the module hardening pass across 24 newly-added modules, renamed the six completed Review sub-tasks to Harden, filed 11 new findings under their Harden parents, and broke three design specs (EMMS-free music, dev F-keys, dev-setup-project) into 20 dependency-ordered sub-tasks via parallel subagents. Verified the sqlite finalizer bug from 2026-04-26 is gone and closed its tracking entry.
* test(recording): skip integration tests when screencast access failsCraig Jennings2026-05-151-0/+34
| | | | | | | | The three integration tests in test-video-audio-recording-process-cleanup spawn wf-recorder via cj/ffmpeg-record-video and assert on pgrep counts. They guard with executable-find and XDG_SESSION_TYPE checks, but neither catches the case where the subprocess can run wf-recorder yet lacks Wayland screencast permission. wf-recorder picks a region, retries "Failed to copy frame" 17 times, then exits with code 183 inside a second. The assertion fires against an empty pgrep. I added test-cleanup--can-capture-frames, which calls cj/ffmpeg-record-video against a temp dir, waits 1s, and checks pgrep. If wf-recorder didn't survive, the three integration tests skip. The result is cached, so the ~2.5s cost is paid once per batch. I added the same guard to test-integration-video-recording-multiple-start-stop-cycles. Its assertion is (= count initial-count), so it trivially passed in any environment where capture didn't work. Skipping is more honest than passing for the wrong reason.
* refactor(org-babel): drop redundant cj structure templateCraig Jennings2026-05-151-3/+3
| | | | Now that yasnippet handles `<cj` + TAB in every buffer including org-mode, the `cj` entry in `org-structure-template-alist` is redundant. I removed it and left a one-line comment pointing to where the marker now lives.
* feat(snippets): add universal <cj marker snippetCraig Jennings2026-05-151-0/+7
| | | | | | | | | | | | New `snippets/fundamental-mode/cj-comment-block` expands `<cj` + TAB to the literal three-line marker block: #+begin_src cj: comment <cursor> #+end_src It lives in `fundamental-mode/` so yas's parent-chain lookup finds it in every buffer, plus the activation hook from the previous commit explicitly turns on `fundamental-mode` as an extra mode in every buffer — so even modes that don't descend from `fundamental-mode` (like `special-mode`-derived buffers) pick it up. The marker is what my Python scanner skill picks out across files. Now I can plant it in any buffer, not just org where the old org-tempo entry lived.
* feat(yas): activate yasnippet globally with fundamental-mode extrasCraig Jennings2026-05-152-3/+141
| | | | | | | | The yasnippet use-package block switches from `:hook (prog-mode . yas-minor-mode)` to `:demand t` + `(yas-global-mode 1)`. That makes yas-minor-mode active in every buffer, not just prog-mode-derived ones. I added a small helper, `cj/--yas-activate-fundamental-extras`, attached as `:hook (yas-minor-mode . ...)`. It calls `(yas-activate-extra-mode 'fundamental-mode)` so the snippet table at `snippets/fundamental-mode/` is consulted in every buffer regardless of the buffer's own major mode. That's what makes universal triggers like `<cj` work everywhere. The new `tests/test-prog-general-yas-activation.el` covers both wiring (yas-global-mode on, fundamental-mode in yas-extra-modes, yas-minor-mode active in org/text buffers) and end-to-end expansion (the marker snippet expands correctly in fundamental, text, org, emacs-lisp, and python-ts modes). 9 tests, all green; full unit suite green with no regressions.
* refactor(org-roam-config): indirect node-tags accessor for testabilityCraig Jennings2026-05-151-1/+13
| | | | | | | | | | | | | | | | | | | | `cj/org-roam-filter-by-tag' called `org-roam-node-tags' directly. That accessor is generated by `cl-defstruct' and ships with a compiler-macro that inlines the call to an `aref' against the `cl-struct-org-roam-node-tags' tag variable at byte-compile time. In tests, `cl-letf' on `(symbol-function 'org-roam-node-tags)' sets the function cell but the byte-compiled call site never consults it -- it executes the inlined `aref' instead. When org-roam isn't loaded (legitimate for a tag-filter unit test), the inlined code fails with `void-variable cl-struct-org-roam-node-tags'. Wrap the accessor in `cj/--org-roam-node-tags' that calls through `funcall' with a quoted symbol. Quoted symbols skip the compiler-macro (which only fires on direct call forms), so the funcall resolves the function cell at runtime and picks up the test's `cl-letf' stub. Production behavior is unchanged; tests no longer need org-roam loaded.
* refactor(org-webclipper): use setq, not setopt, for pandoc sleep timeCraig Jennings2026-05-151-2/+8
| | | | | | | | | | | | | | | | | | | `org-web-tools-pandoc-sleep-time' is a plain float with no custom-set handler that needs to fire. `setopt' adds the entire customize-variable validation machinery -- which, lazily, depends on wid-edit being loaded. The handler's tests stub `require' so org-web-tools never really loads, then mock `setopt' via `cl-letf' on the function cell. That mock has no effect on byte-compiled code because `setopt' is a macro: the production handler has already expanded to a call into `setopt--set'. When `setopt--set' runs, it walks into the customize machinery and hits an unbound `widget-field-keymap' (wid-edit not loaded), and the test fails with a confusing wrong-type-argument. `setq' has identical runtime effect for this variable and dodges the customize machinery entirely. Tests now pass without contorted mocking.
* fix(system-commands): require keybindings at load time, not just compile timeCraig Jennings2026-05-151-1/+6
| | | | | | | | | | | | | | | | The module had `(eval-when-compile (require 'keybindings))`, which silences the byte-compiler but doesn't make `cj/custom-keymap' available when the module is required. The top-level `(keymap-set cj/custom-keymap "!" cj/system-command-map)' at the tail of the file then fails with `void-variable cj/custom-keymap'. Normal Emacs startup happened to work because `init.el' requires `keybindings' before `system-commands'. But requiring the module in isolation -- including from `make test-file FILE=test-system-commands-resolve-and-run.el' -- blows up. Fix: use a plain `(require 'keybindings)' so the load-time dependency matches the load-time reference.
* fix(custom-buffer-file): Info dispatcher returns full org bracket linkCraig Jennings2026-05-152-5/+26
| | | | | | | | | | | | | | | | The Info-mode entry in cj/buffer-source-functions copied the bare target string info:(manual)Node. Per the task body that introduced the dispatcher, the intended output is the labeled org-link form [[info:(manual)Node][(manual) Node]] -- a paste into notes lands as a clickable link with a human-readable label, not a bare URI. The label uses (manual) Node so the manual name and node name are both grep-friendly in note files. Existing test on a compressed .info.gz file now asserts the bracket form. Added a boundary test for an uncompressed .info file (the other branch of the suffix-stripping logic) so both compression shapes are locked in.
* fix(ai-vterm): autoload cj/toggle-gptel to silence cross-module warningCraig Jennings2026-05-152-0/+19
| | | | | | | | | | | | | | | | | | | | 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.
* test(architecture): guard top-level timers + add startup-contract smoke testCraig Jennings2026-05-155-21/+129
| | | | | | | | | | | | | | | | | | | | | | | | | | Add a tiny source-level architecture suite at tests/test-architecture-startup-contracts.el with two checks: - Only keybindings.el may globally own the exact C-; prefix. Catches accidental cross-module rebinding before it ships. - Top-level timer scheduling (run-with-timer / run-at-time / run-with-idle-timer) must be guarded by (unless noninteractive ...) so requiring a module in batch / test mode does not schedule startup timers. Timer calls inside defuns are exempt -- the test only rejects forms that execute their body when the module loads. Four modules had unguarded top-level timer scheduling and would have tripped the new test. Wrap their startup hooks/timers in (unless noninteractive ...): - modules/org-agenda-config.el: 10s idle cache build - modules/org-refile-config.el: 5s idle cache build - modules/quick-video-capture.el: after-init-hook + 2s fallback - modules/wrap-up.el: emacs-startup-hook bury-buffers delay The contract being protected is "requiring a module in batch should not start a clock running." Test failures will now point straight at the offending file/form.
* feat(coverage): report modules missing from SimpleCov + project-module scoreCraig Jennings2026-05-152-14/+161
| | | | | | | | | | | | | | | | | | | | | | | | | =make coverage= used to print a line-weighted percentage that only saw files SimpleCov instrumented. 104 modules existed on disk but only 49 appeared in =.coverage/simplecov.json=, so the headline number was flattering: untouched modules counted for nothing. The summary script now adds two things on top of the existing report: - A =Not in SimpleCov report= section listing modules present under =modules/*.el= but absent from the SimpleCov output. Missing-module detection is exactly direct =modules/*.el=; subdirectories and =.elc= files are ignored. - A =Project module coverage= line that is module-weighted across every direct =modules/*.el= file. Tracked modules contribute their per-file coverage percentage; missing modules contribute 0%. The original line-weighted SimpleCov percentage stays as the =instrumented coverage= number. The new module-weighted score is the honest project-level reading: missing modules count as 0% without inventing a fake executable-line denominator for them. Tests assert the missing-module section, the new percentage, and the ignore rules for .elc / nested files.
* docs(design): commit music-config-without-emms spec + readiness reviewCraig Jennings2026-05-152-0/+842
| | | | | | | | | | | | | | | | | | | | | | | | | | | The spec lays out the EMMS-removal design: package-owned track and playlist structs, a narrow backend protocol with mpv as the v1 backend, state-change hooks replacing EMMS player hooks, an overlay-based selected-track marker, a fake-backend test architecture, a quantified performance budget, a 22-step parity walk, and the migration plan. The review tracks implementation readiness: which migration-plan step is safe to start, which decisions still block the rest, and the exact spec edits required. Two decisions landed this session and are now baked into the spec: - Platform support: Linux and macOS get full features; Windows runs in degraded mode (play/stop/next/previous only) because Emacs cannot natively connect to mpv's Windows named-pipe IPC. Anyone who wants full Windows parity can wire mpvc.exe shellout or a w32-* named-pipe client as a follow-up. - File-extension scope: cj/music-file-extensions stays as-is. webm and ape files in ~/music are intentionally skipped. Socket path now references temporary-file-directory instead of a hardcoded /tmp/ prefix so the spec stays consistent with the Windows section.
* docs(design): rewrite flycheck modeline customization specCraig Jennings2026-05-151-0/+315
| | | | | | | | | | | | | | | | | | Replaces the .ai/ draft (2025-11-14) with a corrected and tightened version under docs/design/. The earlier draft had stale line numbers pointing at a modeline-config.el layout that no longer exists, conflated Option 3's risky-local-variable requirement with Option 4's inline (:eval ...) approach, and missed the active-window gating convention used by the rest of the modeline. The new spec uses concrete line refs against current code, calls out flycheck-mode-line-color (which the old draft missed), recommends calling flycheck-mode-line-status-text directly instead of returning the nested (:eval ...) cons, and gates the segment to active window for consistency with cj/modeline-vc-branch and cj/modeline-misc-info. todo.org task points at the new path and drops the broken docs/flycheck-modeline-customization-spec.org link.
* docs(design): add company-to-corfu migration specCraig Jennings2026-05-151-0/+324
| | | | | | | | | | | | | | | | | | Replaces a thin third-party config snippet (one use-package corfu + one use-package cape, with no migration steps and no prescient piece) with a full spec covering the current company stack: corfu, cape, corfu-popupinfo, kind-icon, corfu-prescient. Maps every current company setting to its corfu equivalent (idle-delay, prefix-length, tooltip-limit, wrap, require-match, global-mode exclusions, doc popups, icon kinds, prescient sort). Walks the per-module fixups -- selection-framework, mail-config, ledger-config, latex-config, eshell-config, and the three prog-* mode hooks. Adds a test plan and risks section. todo.org points at the new doc; the broken :COMPLETE_CONFIG: property (which referenced the wrong line range in someday-maybe) is gone.
* feat(custom-buffer-file): extend buffer-source dispatch to mu4e and InfoCraig Jennings2026-05-152-1/+76
| | | | | | | | | | | | | | | | | Add two dispatchers to cj/buffer-source-functions so C-; b p yields a useful link form in two more major modes. mu4e-view-mode returns "mu4e:msgid:<id>" so the result pastes into org as a clickable link and matches mu4e's own org-protocol handler. Falls through to buffer-file-name when point isn't on a real message. Info-mode returns "info:(manual)node" -- the form org-info-store-link produces. file-name-base only strips one extension, so a compressed "emacs.info.gz" comes back as "emacs.info"; trim the trailing ".info" to get the bare manual name. Falls through when Info hasn't populated its current-file / current-node vars yet. Tests cover normal + boundary fallthrough for each new mode.
* refactor(org-noter-config): rebind insert-note to n; sync to angle bracketsCraig Jennings2026-05-152-7/+54
| | | | | | | | | | | | | | | `cj/org-noter-insert-note-dwim' is the most-used action in a noter session; it deserves the doubled-prefix letter. Move it from `C-; n i' to `C-; n n'. Sibling-stepping moves off `n'/`p' (which were sync-next / sync-prev) onto the angle-bracket pair `>'/`<' to free up `n' and to read more naturally as direction. `.' stays as sync-current-note. Updated `which-key' labels to match. Four new ERT tests in `tests/test-org-noter-config-keymap.el' lock the keymap shape so a casual edit doesn't silently drift the layout.
* refactor(calibredb-epub): reorder defuns above their use-package consumersCraig Jennings2026-05-151-69/+69
| | | | | | | | | | | | | | | | | | | | | | | `make compile' had been flagging `cj/calibredb-clear-filters' and `cj/nov-jump-to-calibredb' as "defined multiple times in this file" since 2026-05-12. Investigation: there's only one `(defun ...)' of each in the source -- use-package's `:bind' expansion makes the byte-compiler count the referenced symbol as a definition when the function is defined in the same file, then it sees the real `defun' later and warns about a redefinition. Reorder so each `defun' appears before the `use-package' that references it via `:bind': - `cj/calibredb-clear-filters' moved above (use-package calibredb). - `cj/nov--metadata-get', `cj/nov--file-path', and `cj/nov-jump-to-calibredb' moved above (use-package nov). The two helpers had to move with the public function so the byte-compiler doesn't emit fresh free-function warnings. Source content unchanged; only line positions move. Both duplicate-definition warnings are gone after this; full unit suite still green.
* feat(custom-buffer-file): make C-; b p dispatch by major-mode, with testsCraig Jennings2026-05-152-11/+185
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | Old behavior: `C-; b p' called `cj/copy-path-to-buffer-file-as-kill', which only worked in file-visiting buffers and errored otherwise. That meant the most useful "give me a clickable handle on this buffer" key did nothing in eww, elfeed, dired (file-at-point ≠ buffer's default-directory), and other browsing-shaped modes. Replace with a `major-mode'-aware dispatch: - `cj/buffer-source-functions' alist maps major-mode → thunk returning a string (or nil to fall through). - `cj/copy-buffer-source-as-kill' looks up the current mode, calls the thunk, falls back to `buffer-file-name', errors only when both yield nil. - `cj/copy-path-to-buffer-file-as-kill' kept as a `defalias' for backwards compat (the old name is referenced in adjacent tests). First-batch dispatches: - eww-mode -> (eww-current-url) - elfeed-show-mode -> (elfeed-entry-link elfeed-show-entry) - dired-mode -> (dired-get-filename nil t) - dirvish-mode -> same - doc-view / pdf-view: covered by the buffer-file-name fallback (they already set buffer-file-name correctly). 10 new ERT tests cover the dispatch paths, the buffer-file-name fallback, the user-error on nil source, the alias target, and the `C-; b p' keymap entry. which-key label flipped from "copy file path" to "copy buffer source" to match. Deferred to a follow-up task: mu4e-view-mode, org-mode at a heading, help-mode, Info-mode, magit-log/commit/status, xref/grep/ compilation, image-mode, archive-mode -- each needs a format decision before implementation.
* refactor(ai-vterm): retire M-F9 buffer picker; bind to cj/toggle-gptelCraig Jennings2026-05-143-149/+18
| | | | | | | | | | | | | | | | | | | | | | | | | | | 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.
* chore(ai-config): refresh gptel model menusCraig Jennings2026-05-141-8/+7
| | | | | | | | | | | | | | | | | | | Anthropic: bump Opus 4.6 → 4.7 (current frontier). Sonnet 4.6 and Haiku 4.5 stay -- still current. Default `gptel-model' setq also bumped to claude-opus-4-7 in both places it was set. OpenAI: drop the cohort retired from ChatGPT on 2026-02-13 (gpt-4o, gpt-5 original, gpt-4.1, o1). Replace with the current lineup: gpt-5.5 (flagship), gpt-5.4-mini (fast/cheap), o3 (reasoning). All three are documented at developers.openai.com/api/docs/models/. Drive-by: stale docstring example in cj/gptel--current-model-selection bumped to match. gptel's bundled :models list only knows up to May-2025 IDs but gptel-make-anthropic / gptel-make-openai accept any string and pass it straight to the API, so newer names work without a gptel upgrade.
* feat(markdown-config): register markdown as an org src-block languageCraig Jennings2026-05-142-0/+34
| | | | | | | | | | | | | | | | | | | `#+begin_src markdown ... #+end_src' blocks rendered and exported fine but `org-lint' warned on every one of them ("Unknown source block language: 'markdown'"), and `C-c '' inside the block fell back to `fundamental-mode' instead of opening it in `markdown-mode' for editing. Add a `with-eval-after-load 'org' form that pushes `("markdown" . markdown)' onto `org-src-lang-modes'. New ERT test in `tests/test-markdown-config.el' asserts the entry resolves to `markdown' after `(require 'markdown-config)'. Surfaced while clearing `org-lint' on `todo.org' from 55 issues down to 1 -- the last one was this warning on a Linear ticket-body draft that was genuinely markdown. Registering the language is the right fix; relabeling the block as `text' or `example' would lose accuracy.
* refactor(org-config): surface narrowing + sparse-tree under C-; OCraig Jennings2026-05-142-5/+62
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | Narrowing and sparse-tree commands existed in the `:bind' block on `C-c'-style shortcuts but nothing in `cj/org-map' surfaced them, so which-key never showed them and discoverability was poor. Add direct bindings under `C-; O', flat (no sub-prefixes for narrow / sparse-tree). Lowercase creates; capital of the same letter cancels: - `n' / `N' narrow-to-subtree / widen - `s' / `S' match-sparse-tree / show-all - `t' / `T' show-todo-tree / show-all - `>' / `<' forward / backward sibling narrow (kept as-is) - `R' reveal-context (no lowercase pair -- `r' is the table-row sub-prefix) Both `S' and `T' resolve to the same `org-show-all' command so the mental model is just "capital cancels the lowercase I just ran" without having to recall which letter the cancel actually lives on. Free up F2: the old `(<f2> . org-reveal)' binding in the org-mode `:bind' block is now redundant with `C-; O R'. Drop it; F2 becomes available for whatever wants it next. Four new ERT assertions in `test-org-config-keymap-ownership.el' lock the shape -- the old sparse-tree-submap test was rewritten for the flat layout and the narrow-submap test became narrow-bindings (also flat).
* refactor(dashboard): regroup launcher icons into 4/4/4 by purposeCraig Jennings2026-05-141-33/+39
| | | | | | | | | | | | | | | | | | | | | | Telegram had landed alone on a third row of one icon, with the first two rows holding a mixed bag (Code next to Email next to Agenda next to Files next to Music; Feeds next to IRC next to Slack next to Flashcards next to Books next to Terminal). No category showed up grouped, and the asymmetry was bugging me every dashboard open. Regroup by what the icons actually do. Three rows of four: - Row 1 Work: Code / Files / Terminal / Agenda - Row 2 Read & Learn: Feeds / Books / Flashcards / Music - Row 3 Communication: Email / IRC / Slack / Telegram Reorder the `define-key' calls on `dashboard-mode-map' to mirror the row layout -- reading the keymap top-to-bottom now matches reading the icons left-to-right. Drive-by fix in the same commit: Music had an icon but no `dashboard-mode-map' keybinding (mouse-only). Bound to `m'.
* feat(transcription): extend dired T to transcribe videos via ffmpeg, with testsCraig Jennings2026-05-144-19/+270
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | Pressing `T' in dired/dirvish on an audio file already transcribed it; on a video file it bounced with "Not an audio file". Real recordings ship as .mp4 / .mkv at least as often as raw .m4a, so the one-key flow ended at the wrong place. Pipeline now: - audio path -> direct into `cj/--start-transcription-process' (unchanged). - video path -> async ffmpeg extracts the audio track to a temp .mp3 under `temporary-file-directory' (libmp3lame, VBR q:a 4, ~165kbps -- right size for speech, accepted by every backend), then transcribes that file with the temp marked for cleanup after the transcription sentinel fires. Surface changes: - `cj/video-file-extensions' added to user-constants.el (mp4, mkv, mov, webm, avi, m4v, wmv, flv, mpg, mpeg, 3gp, ogv). - New predicates `cj/--video-file-p' / `cj/--media-file-p'. - New `cj/--extract-audio-from-video' (async ffmpeg with success callback; surfaces `cj/--notify' on failure; user-errors if ffmpeg isn't on PATH). - `cj/--start-transcription-process' gains optional `cleanup-file'. Sentinel deletes it after the existing logic runs. Backwards compatible -- the audio flow doesn't pass it. - `cj/transcribe-audio' renamed to `cj/transcribe-media' (dispatcher on audio vs video). `cj/transcribe-audio-at-point' renamed to `cj/transcribe-media-at-point'. Both old names kept as `defalias' so M-x history and any external references still work. - `T' in dired-mode-map + dirvish-mode-map points at `cj/transcribe-media-at-point'. - Module commentary USAGE block updated. 15 new ERT tests in `tests/test-transcription-video.el' cover the predicates (happy/boundary/error), ffmpeg invocation (correct args + missing-ffmpeg path), the dispatcher (audio direct, video via extraction, non-media rejected), the aliases, and the T binding. One existing test in `test-transcription-status-and-commands.el' updated to stub the new delegate name. Verified locally that ffmpeg is on PATH with libmp3lame, and that the exact arg list my code uses produces a valid MP3 from a synthetic test video.
* refactor: clear transcription C-; T menu, move telega launcher to C-; TCraig Jennings2026-05-143-33/+20
| | | | | | | | | | | | | | | | The transcription menu wasn't earning its top-level keymap slot -- the commands (transcribe-audio, switch-backend, view-transcriptions, kill-transcription) are run rarely enough that `M-x' is fine. Drop the `cj/transcribe-map' keymap, its `(keymap-set cj/custom-keymap "T" ...)' binding, and the which-key labels. Commands stay callable by name. That frees `C-; T' for telega, where the mnemonic actually fits. Move the launcher from `C-; G' to `C-; T'. Update the which-key label, the module commentary, and the keymap-binding test assertion. The dashboard `g' single-letter binding stays put -- `t' there is vterm, so dashboard letters and the global `C-;' prefix don't share a key space anyway.
* refactor(org-config): flatten table ops directly under the org menuCraig Jennings2026-05-142-19/+34
| | | | | | | | | | | | | | Drop the `T' sub-prefix so table operations sit directly under `C-; O': `O r i' / `O r d' for rows, `O c i' / `O c d' for columns. Move `cj/org-clear-element-cache' from `c' (which now hosts the table-column sub-prefix) to capital `C'. Single-key org commands under this menu live on capitals from here on so the lowercase letters stay free for table sub-prefixes. Drop `cj/org-table-map' entirely -- its bindings now live directly on `cj/org-map'. Three tests in `test-org-config-keymap-ownership.el' updated/added: `C' for clear-cache, plus row and column binding assertions.
* refactor(org-config): move org-table-map under the org menuCraig Jennings2026-05-141-9/+11
| | | | | | | | | | | | | | | | `(keymap-set cj/custom-keymap "T" cj/org-table-map)' at top level silently collided with `cj/transcribe-map' bound to the same key in `modules/transcription-config.el'. Whichever module loaded last won, the other prefix became unreachable, and which-key still showed both labels in their respective sections -- so the visible documentation didn't match what actually fired. Move the table map under the existing `cj/org-map' (`C-; O') as the "T" sub-prefix, so `C-; T r i' becomes `C-; O T r i' and friends. The org menu only had one entry before (clear element cache); table operations are a natural neighbor. Frees `C-; T' at the top level for the transcription menu, which was the only other module fighting over it.
* feat(telega-config): guard launcher with a helpful message when telega is ↵Craig Jennings2026-05-143-5/+47
| | | | | | | | | | | | | | | | | | | | missing Without the guard, both `C-; G' and the dashboard Telegram icon trigger telega's autoload stub directly. When the package isn't installed yet the user sees `Cannot open load file: telega' in `*Messages*' with no hint about what to do. Wrap the launcher in `cj/telega' that checks `featurep' / `locate-library' first. If telega is present, delegate to it. Otherwise signal a `user-error' pointing at `scripts/setup-telega.sh' and the manual `M-x package-install RET telega' fallback. Rebind `C-; G' and the dashboard "g" key + Telegram icon callback to the wrapper. Two new tests in `test-telega-config.el' cover the wrapper paths (absent -> user-error with the recovery hint; present -> delegates to `telega') alongside the updated binding assertion.
* feat(setup-telega): install the telega Emacs package alongside docker setupCraig Jennings2026-05-142-2/+88
| | | | | | | | | | | | | | | | | | | | modules/telega-config.el uses `:ensure nil' on the use-package block (a stale MELPA archive index can 404 and take startup down if auto-install runs in init). The trade-off was that a fresh clone needed a one-time `M-x package-install RET telega' before the dashboard launcher or `C-; G' would work -- the autoload stub would fail with `Cannot open load file: telega' instead. Hit it on this machine just now: dashboard pressed, autoload tried to load telega.el, no telega.el on the load-path, cryptic error. Add `ensure_telega_package' to the setup script: probe with `(package-installed-p 'telega)' under `emacs --batch'; if absent, refresh MELPA and install via package.el; if that fails, surface the manual recovery path. Wire it into `main' after the docker checks. Four new bats tests cover the missing-emacs, already- installed, install-succeeds, and install-fails paths with `emacs' stubbed at the function level.
* refactor(system-commands): use string interactive spec so undercover instrumentsCraig Jennings2026-05-142-2/+32
| | | | | | | | | | | | | | | | `cj/system-cmd' had `(interactive (list (read-shell-command "System command: ")))' -- the destructured-list interactive spec. Undercover.el relies on edebug instrumentation, which doesn't see past that form, so the entire function body registered as 0 hits under coverage even though tests call the function directly. Switch to the equivalent string spec `(interactive "sSystem command: ")'. Same UX (prompt, history, single-string result), but the body now instruments correctly and coverage moves from 34/49 to 50/51. Add one more test that captures the `run-at-time' lambda in `cj/system-cmd-restart-emacs' and invokes it directly so the inner `call-process-shell-command' branch registers, taking coverage to 51/51.
* test(system-defaults): switch to single top-level require so undercover ↵Craig Jennings2026-05-141-90/+106
| | | | | | | | | | | | | | | | | | | | | | | | instruments The helper-functions test was per-test reloading system-defaults.el via `(load ...)' inside a `cl-letf' sandbox that stubs the side-effecting primitives (server-start, set-locale-environment, etc). Tests passed, but the coverage gauge stayed stuck at 1/12 because undercover.el only instruments the first load of a matching source; subsequent re-loads inside test bodies don't get tracked, so the function bodies showed as uncovered even though every test called them. Rewrite the test to call `(require 'system-defaults)' once at top level, wrapped in the same `cl-letf' stubs. The functions get instrumented exactly once. Drop the now-unused per-test sandbox macro. Add two more tests for the `(when (memq ...))' list-without- comp guard and the non-string-message format branch so coverage reaches 12/12. (`test-system-defaults-vc-follow-symlinks.el' still uses the per-test `(load ...)' pattern because that test *is* the load-side-effect verification, not a function-body test.)
* chore(todo): close Python tree-sitter predicate bug as upstream-resolvedCraig Jennings2026-05-141-0/+23
| | | | | | | | | | | | | | | | | | | | The treesit-query-error redisplay flood diagnosed 2026-04-26 no longer reproduces. Versions: emacs 30.2-3 (was 30.2-2 at the time of the investigation, upgraded 2026-05-03), tree-sitter 0.26.8 (unchanged). The upstream Emacs version string is unchanged, but the Arch package revision bump most likely carries a downstream patch to treesit.c's predicate translation. Verified by re-running the documented repro: the exact failing query from python.el captures cleanly via `treesit-query-capture', and `font-lock-ensure' on a real .py file under `python-ts-mode' returns with no `treesit-query-error'. No local override needed. Mark the todo.org entry DONE, fix the stale `inbox/' path on the investigation-doc link (file now lives under `docs/'), update the cross-reference from the grammar-bootstrap task to note this no longer blocks it, and append a RESOLVED 2026-05-14 footer to the investigation doc so future-me can see why it got closed.
* fix(org-roam-config): save journal buffer after copying DONE taskCraig Jennings2026-05-142-2/+33
| | | | | | | | | | | | | | `cj/org-roam-copy-todo-to-today' tried to save the target journal buffer via `org-after-refile-insert-hook' bound to `#'save-buffer', but that value is the wrong shape (single function instead of a hook list), and the only other save mechanism -- the `:after' advice on `org-refile' that calls `org-save-all-org-buffers' -- doesn't attach until `:defer .5' elapses, so the very first DONE transition after startup leaves the journal unsaved. Drop the broken hook binding and save the target buffer explicitly after the refile call. New ERT test asserts `buffer-modified-p' on the journal buffer is nil after the function returns.
* test(music-config): cover playlist load/save/edit/toggle/show + ↵Craig Jennings2026-05-141-0/+173
| | | | create-radio-station
* test(music-config): cover add-directory, fuzzy-select, playlist-clear, ↵Craig Jennings2026-05-141-0/+191
| | | | next/previous, consume-toggle
* test(ai-config): cover available-backends, change-model, add-file, toggle, ↵Craig Jennings2026-05-141-0/+160
| | | | context-clear
* test(transcription-config): cover notify, start-process, sentinel bodiesCraig Jennings2026-05-141-0/+165
|
* test(dirvish-config): cover ediff-files, create-playlist, set-wallpaper wrappersCraig Jennings2026-05-141-0/+144
|
* test(org-agenda-config): cover scan-files, todo-list wrappers, main-display, ↵Craig Jennings2026-05-141-0/+162
| | | | add-timestamp