diff options
| -rw-r--r-- | .claude/rules/todo-format.md | 54 | ||||
| -rw-r--r-- | early-init.el | 38 | ||||
| -rw-r--r-- | init.el | 1 | ||||
| -rw-r--r-- | modules/ai-term.el | 67 | ||||
| -rw-r--r-- | modules/erc-config.el | 19 | ||||
| -rw-r--r-- | modules/gcmh-config.el | 30 | ||||
| -rw-r--r-- | modules/system-defaults.el | 19 | ||||
| -rw-r--r-- | modules/system-utils.el | 2 | ||||
| -rw-r--r-- | modules/term-config.el | 11 | ||||
| -rw-r--r-- | tests/test-ai-term--f9-in-term.el | 18 | ||||
| -rw-r--r-- | tests/test-ai-term--next-agent-buffer.el | 73 | ||||
| -rw-r--r-- | tests/test-system-defaults-functions.el | 14 | ||||
| -rw-r--r-- | tests/test-system-defaults.el | 13 | ||||
| -rw-r--r-- | tests/test-term-tmux-history.el | 17 | ||||
| -rw-r--r-- | todo.org | 170 |
15 files changed, 359 insertions, 187 deletions
diff --git a/.claude/rules/todo-format.md b/.claude/rules/todo-format.md index b1fb57b8f..b9e93bb5a 100644 --- a/.claude/rules/todo-format.md +++ b/.claude/rules/todo-format.md @@ -130,7 +130,7 @@ becomes The agenda view (`org-agenda`) shows entries at the section + top-task level. Letting `**` tasks stay task-shaped preserves their visibility as "things that recently shipped." Letting `***+` sub-tasks flip to dated entries keeps the agenda from being clogged with a long list of completed sub-tasks at every depth — those become history within their parent instead. -`VERIFY` is the documented exception: it follows the dated-rewrite rule at **all** depths (including `**`), because a resolved VERIFY is an answered question rather than a finished task. See the VERIFY section below. +`VERIFY` follows the dated-rewrite rule at `***` and deeper, the same as any sub-task. At `**` it does *not*: a top-level VERIFY completes task-shaped — a `DONE`/`CANCELLED` keyword plus a `CLOSED:` line, exactly like a top-level `TODO`. Dated headers never appear at `**`. Level 2 always carries a terminal keyword; dated headers are a `***`-and-deeper shape only. See the VERIFY section below. ## VERIFY tasks @@ -191,19 +191,31 @@ The sibling rule is the active force that keeps `todo.org` flat. Without it, VERIFYs accumulate one level deeper than their trigger every time — turning a clean parent tree into a long pole of nested sub-headings. -### Completion — dated rewrite + content replacement +### Completion — depth decides the heading shape -When a VERIFY resolves, **rewrite the heading and body together** at the -same depth — regardless of whether the VERIFY is at `**` or `***`: +When a VERIFY resolves, **rewrite the heading and body together**. The body +replacement is the same at every depth (step 2 below); the heading shape +depends on the VERIFY's level, mirroring the depth-based rule for ordinary +tasks — dated entries at `***` and deeper, terminal keyword at `**`. -1. **Replace the heading.** Drop the `VERIFY` keyword (and any priority - cookie / tags) and replace with a timestamp + short description: +1. **Replace the heading — by depth.** - *** 2026-05-15 Fri @ 14:00:00 -0500 <what was answered or done> + - **At `***` and deeper — dated event-log entry.** Drop the `VERIFY` + keyword (and any priority cookie / tags) and replace with a timestamp + + short description: - Generate the timestamp with `date "+%Y-%m-%d %a @ %H:%M:%S %z"`. - Match the original depth (a `**` VERIFY becomes `** YYYY-MM-DD ...`; - a `***` VERIFY becomes `*** YYYY-MM-DD ...`). + *** 2026-05-15 Fri @ 14:00:00 -0500 <what was answered or done> + + Generate the timestamp with `date "+%Y-%m-%d %a @ %H:%M:%S %z"`. + + - **At `**` — terminal keyword, like any top-level task.** Change + `VERIFY` to `DONE` (answered / check passed) or `CANCELLED` (abandoned), + keep the heading text, priority cookie, and tags, and add a + `CLOSED: [YYYY-MM-DD Day]` line. Never a dated heading — a `**` dated + header is a defect; repair it to `DONE`/`CANCELLED` + `CLOSED:`. + + ** DONE [#B] <original VERIFY topic> :tags: + CLOSED: [2026-05-15 Fri] 2. **Replace the body.** Drop the original question/instruction prose and replace with either: @@ -213,16 +225,18 @@ same depth — regardless of whether the VERIFY is at `**` or `***`: instruction or pending-decision marker — what was done, when, where the artifact lives). -The completed VERIFY becomes an in-place event log entry. The original -question is preserved by the dated heading + body shape; anyone scanning -the agenda or `git log` can see what was asked and what landed. - -**Note on the top-level case.** Regular `**` DONE tasks stay task-shaped -with a `DONE` keyword + `CLOSED:` line per *Completion — depth-based* -above. VERIFYs at `**` are the exception — they convert to dated log -entries on completion because a resolved VERIFY isn't a "done task," it's -an answered question. The dated-rewrite rule wins for VERIFYs at all -depths. +Either way the completed VERIFY records what was asked and what landed: at +`***` and deeper as a dated event-log entry, at `**` as a `DONE`/`CANCELLED` +task whose body holds the answer. Anyone scanning the agenda or `git log` +can see both. + +**Note on the top-level case.** A `**` VERIFY completes exactly like a `**` +`TODO`: a `DONE`/`CANCELLED` keyword + `CLOSED:` line, with the answer or +action in the body. The earlier habit of dating a resolved top-level VERIFY +— treating "answered question, not a finished task" as license for a `**` +dated header — is retired. It put dated headers at level 2, where the agenda +truncates them out of a clean keyword scan. Dated rewrite is for `***` and +deeper only; `**` always carries a terminal keyword. ### Don't leave stale placeholders diff --git a/early-init.el b/early-init.el index d45faef79..7ae814734 100644 --- a/early-init.el +++ b/early-init.el @@ -60,14 +60,23 @@ (lambda () (setq debug-on-error nil))) -;; ------------------------------ Bug Workarounds ------------------------------ - -;; Disable async native compilation to prevent "Selecting deleted buffer" errors -;; This is a known issue in Emacs 30.x where async compilation buffers get -;; deleted before the compilation process completes. Synchronous compilation -;; is slower initially but avoids these race conditions. -(setq native-comp-deferred-compilation nil) ;; Disable async/deferred compilation -(setq native-comp-async-report-warnings-errors nil) ;; Silence async warnings +;; ------------------------------ Native Compilation --------------------------- + +;; Enable JIT native compilation. Packages are natively compiled on first load +;; (asynchronously, in the background) and cached as .eln for later sessions. +;; This was previously disabled via `(setq native-comp-deferred-compilation +;; nil)' -- the obsolete alias of `native-comp-jit-compilation'. Despite the old +;; comment, setting it nil turns JIT OFF entirely (not "synchronous"), so most +;; modules ran interpreted for the daemon's lifetime and the +;; `native-comp-speed'/jobs settings in system-defaults.el were dead. The old +;; "Selecting deleted buffer" async race that prompted the disable was an Emacs +;; 28/29 issue; this is 30.2. +(setq native-comp-jit-compilation t) + +;; Log async-compile warnings to the *Async-native-compile-log* buffer rather +;; than popping a window. (system-defaults.el also routes `comp' display-warnings +;; to a file via `cj/log-comp-warning'.) +(setq native-comp-async-report-warnings-errors 'silent) ;; ------------------------------- Load Freshness ------------------------------ ;; Prefer newer .el source over stale .elc byte-compiled files. Without this, @@ -99,11 +108,13 @@ local repos." :group 'cj) ;; ---------------------------- Startup Performance ---------------------------- -;; increases garbage collection threshold and turns off file-name-handler -;; during startup and restores the settings once emacs has loaded. +;; Bump the GC threshold and turn off the file-name-handler during startup for +;; speed. The file-name-handler is restored once Emacs has loaded. The GC +;; threshold is deliberately NOT restored here -- `gcmh' (configured in +;; system-defaults.el) owns `gc-cons-threshold' for the rest of the session, +;; keeping it high during activity and collecting on idle. Restoring the stock +;; 800KB here would fight gcmh and bring back frequent GC pauses. -(defvar cj/orig-gc-cons-threshold gc-cons-threshold - "Temporary variable to allow restoration of value post-startup.") (setq gc-cons-threshold most-positive-fixnum) (defvar cj/orig-file-name-handler-alist file-name-handler-alist @@ -112,8 +123,7 @@ local repos." (add-hook 'emacs-startup-hook (lambda () - (setq gc-cons-threshold cj/orig-gc-cons-threshold - file-name-handler-alist cj/orig-file-name-handler-alist))) + (setq file-name-handler-alist cj/orig-file-name-handler-alist))) ;; ------------------------------ Site Start Files ----------------------------- ;; don't load site-start or default.el files @@ -26,6 +26,7 @@ (require 'host-environment) ;; convenience functions re: host environment (require 'keyboard-compat) ;; terminal/GUI keyboard compatibility (require 'system-defaults) ;; native comp; log; unicode, backup, exec path +(require 'gcmh-config) ;; garbage collection strategy (gcmh) (require 'keybindings) ;; system-wide keybindings and keybinding discovery ;; -------------------------- Utilities And Libraries -------------------------- diff --git a/modules/ai-term.el b/modules/ai-term.el index 25e56c508..ff8da0035 100644 --- a/modules/ai-term.el +++ b/modules/ai-term.el @@ -52,15 +52,19 @@ ;; picker, even when an agent buffer is currently displayed. ;; Used when the user wants to start a new project session ;; instead of toggling the current one. +;; - s-F9 `cj/ai-term-next' -- step to the next open agent in the +;; queue. The queue is the live agent buffers in buffer-name +;; order (a stable rotation). When an agent window is on +;; screen, swap it to the next agent and focus it, wrapping +;; after the last; when none is shown but agents exist, show +;; the first. This is the "switch among existing agents" +;; surface F9 deliberately doesn't provide. ;; - M-F9 `cj/ai-term-close' -- gracefully close an agent: kill its ;; tmux session (stopping the agent process), then its terminal ;; buffer. Its window stays in the layout (swapped to the ;; working buffer), so closing never collapses a split. Confirms ;; first. Targets the current agent, the sole live agent, or ;; prompts among several. -;; - C-S-F9 `cj/ai-term-close' -- same close command, second binding. -;; (M-F9 is the primary; C-S-F9 may be swallowed by the -;; Wayland/PGTK layer on some machines.) ;; ;; Existing windmove (Shift-arrows) handles code <-> agent focus ;; toggling. Buffer-move (C-M-arrows) handles side-swap. Neither @@ -181,6 +185,21 @@ recently-selected first. Non-AI-term buffers are filtered out via `cj/--ai-term-buffer-p'." (seq-filter #'cj/--ai-term-buffer-p (buffer-list))) +(defun cj/--ai-term-next-agent-buffer (current buffers) + "Return the agent buffer after CURRENT in BUFFERS, wrapping to the first. + +BUFFERS is an ordered list of live agent buffers. When CURRENT is the +last element, wrap to the first. When CURRENT is nil or not a member of +BUFFERS, return the first buffer. Returns nil when BUFFERS is empty. + +Pure decision helper (no buffer or window side effects) so the cycle +order driving `cj/ai-term-next' (s-F9) is exercisable in tests." + (when buffers + (if (memq current buffers) + (or (cadr (memq current buffers)) + (car buffers)) + (car buffers)))) + (defun cj/--ai-term-most-recent-non-agent-buffer () "Return the most-recently-selected live non-agent buffer, or nil. @@ -882,7 +901,7 @@ With prefix ARG, display the buffer without selecting its window when a buffer is being shown (no effect on the toggle-off branch). See `cj/ai-term-pick-project' (C-F9) to force the project picker. -M-F9 (and C-S-F9) close an agent via `cj/ai-term-close'." +M-F9 closes an agent via `cj/ai-term-close'." (interactive "P") (pcase (cj/--ai-term-dispatch) (`(toggle-off . ,win) @@ -952,7 +971,7 @@ buffers; nil when none are alive." Targets the current agent buffer, the sole live agent, or prompts when several are alive (see `cj/--ai-term-close-target'). Asks for confirmation first -- this kills the running agent process, which can -interrupt work in progress. Bound to M-<f9> (primary) and C-S-<f9>." +interrupt work in progress. Bound to M-<f9>." (interactive) (let ((buffer (cj/--ai-term-close-target))) (unless buffer @@ -963,10 +982,42 @@ interrupt work in progress. Bound to M-<f9> (primary) and C-S-<f9>." (cj/--ai-term-close-buffer buffer) (message "Closed agent %s." name))))) +;; ------------------------- Step to the next agent ---------------------------- + +(defun cj/ai-term-next () + "Step to the next open AI-term agent in the queue. + +The queue is the live agent buffers ordered by buffer name -- a stable +rotation, unaffected by which agent was most recently selected. When an +agent window is on screen, swap it to the next agent in the queue +\(wrapping after the last) and select it. When no agent is displayed but +agents exist, show the first. Signals `user-error' when none are open. + +Bound to s-<f9>. Unlike <f9> (toggle the most-recent agent on/off), this +is the \"switch among existing agents\" surface; C-<f9> opens the project +picker and M-<f9> closes an agent." + (interactive) + (let* ((buffers (sort (cj/--ai-term-agent-buffers) + (lambda (a b) + (string< (buffer-name a) (buffer-name b))))) + (win (cj/--ai-term-displayed-agent-window)) + (current (and win (window-buffer win))) + (next (cj/--ai-term-next-agent-buffer current buffers))) + (unless next + (user-error "No AI-term agent buffers open")) + (if win + (progn + (set-window-buffer win next) + (select-window win)) + (display-buffer next) + (let ((w (get-buffer-window next))) + (when w (select-window w)))) + (message "Agent: %s" (buffer-name next)))) + (keymap-global-set "<f9>" #'cj/ai-term) (keymap-global-set "C-<f9>" #'cj/ai-term-pick-project) +(keymap-global-set "s-<f9>" #'cj/ai-term-next) (keymap-global-set "M-<f9>" #'cj/ai-term-close) -(keymap-global-set "C-S-<f9>" #'cj/ai-term-close) ;; ghostel's semi-char mode forwards keys not in `ghostel-keymap-exceptions' to ;; the terminal program, so a plain <f9> typed while point is inside an agent @@ -977,15 +1028,15 @@ interrupt work in progress. Bound to M-<f9> (primary) and C-S-<f9>." (with-eval-after-load 'ghostel (keymap-set ghostel-mode-map "<f9>" #'cj/ai-term) (keymap-set ghostel-mode-map "C-<f9>" #'cj/ai-term-pick-project) + (keymap-set ghostel-mode-map "s-<f9>" #'cj/ai-term-next) (keymap-set ghostel-mode-map "M-<f9>" #'cj/ai-term-close) - (keymap-set ghostel-mode-map "C-S-<f9>" #'cj/ai-term-close) ;; The bindings above live in `ghostel-mode-map', but in semi-char mode ;; ghostel's own `ghostel-semi-char-mode-map' forwards every key not in ;; `ghostel-keymap-exceptions' to the pty -- and that map outranks the ;; major-mode map, so it would swallow the F9 family before the bindings ;; above fire. Add the family to the exceptions and rebuild the semi-char ;; map so the keys fall through to `ghostel-mode-map' inside agent buffers. - (dolist (key '("<f9>" "C-<f9>" "M-<f9>" "C-S-<f9>")) + (dolist (key '("<f9>" "C-<f9>" "s-<f9>" "M-<f9>")) (add-to-list 'ghostel-keymap-exceptions key)) (ghostel--rebuild-semi-char-keymap)) diff --git a/modules/erc-config.el b/modules/erc-config.el index c0fa9c325..c89e46bb3 100644 --- a/modules/erc-config.el +++ b/modules/erc-config.el @@ -338,16 +338,15 @@ NICK is the sender and MESSAGE is the message text." :after erc :hook (erc-mode . erc-nicks-mode)) -;; ------------------------------ ERC Yank To Gist ----------------------------- -;; automatically create a Gist if pasting more than 5 lines -;; this module requires https://github.com/defunkt/gist -;; via ruby: 'gem install gist' via the aur: yay -S gist - -(use-package erc-yank - :after erc - :bind - (:map erc-mode-map - ("C-y" . erc-yank))) +;; -------------------------------- ERC Yank ---------------------------------- +;; The erc-yank package was dropped 2026-06-20: a paste over 5 lines became a +;; PUBLIC gist (it called `gist -P', the clipboard paste flag, with no +;; `--private'), behind only a single y-or-n-p and with no guard if the `gist' +;; binary was absent -- a one-keystroke path to publishing whatever sat on the +;; system clipboard. No replacement binding is needed: erc-mode-map defines no +;; C-y of its own, so with erc-yank gone C-y falls through to the ordinary +;; global `yank' and a paste stays local. Gist a large snippet by hand when +;; that's actually wanted. (provide 'erc-config) ;;; erc-config.el ends here diff --git a/modules/gcmh-config.el b/modules/gcmh-config.el new file mode 100644 index 000000000..beceb1a01 --- /dev/null +++ b/modules/gcmh-config.el @@ -0,0 +1,30 @@ +;;; gcmh-config.el --- Garbage collection strategy via gcmh -*- lexical-binding: t -*- + +;;; Commentary: +;; gcmh (the Garbage Collector Magic Hack) owns `gc-cons-threshold' for the +;; session. It keeps the threshold very high while you are active so GC never +;; pauses mid-edit, then drops it and collects on idle, when a pause is +;; invisible. This replaces the old hand-rolled scheme -- a stock-800KB restore +;; in early-init.el plus a minibuffer setup/exit bump -- which pinned GC at +;; 800000 (Emacs's bare-editor default), far too low for a config this size and +;; the cause of frequent GC pauses during completion, agenda builds, and LSP/AI +;; activity. +;; +;; Kept in its own module, not system-defaults.el: that module is pre-loaded by +;; the comp-errors test harness, which has no package system, so an `:ensure' +;; package there errors at load time. early-init.el bumps the threshold to +;; `most-positive-fixnum' for startup and deliberately does not restore it; +;; `gcmh-mode' takes ownership from here on. + +;;; Code: + +(use-package gcmh + :ensure t + :demand t + :config + (setq gcmh-idle-delay 'auto ; scale the idle GC delay to GC cost + gcmh-high-cons-threshold (* 1 1024 1024 1024)) ; 1 GB during activity + (gcmh-mode 1)) + +(provide 'gcmh-config) +;;; gcmh-config.el ends here diff --git a/modules/system-defaults.el b/modules/system-defaults.el index 0062a82cf..6d9c811a6 100644 --- a/modules/system-defaults.el +++ b/modules/system-defaults.el @@ -212,18 +212,13 @@ appears only once per session." (setq custom-safe-themes t) ;; treat all themes as safe (stop asking) (setq server-client-instructions nil) ;; I already know what to do when done with the frame -;; ------------------ Reduce Garbage Collections In Minibuffer ----------------- - -(defun cj/minibuffer-setup-hook () - "Hook to prevent garbage collection while user's in minibuffer." - (setq gc-cons-threshold most-positive-fixnum)) - -(defun cj/minibuffer-exit-hook () - "Hook to trigger garbage collection when exiting minibuffer." - (setq gc-cons-threshold 800000)) - -(add-hook 'minibuffer-setup-hook #'cj/minibuffer-setup-hook) -(add-hook 'minibuffer-exit-hook #'cj/minibuffer-exit-hook) +;; ----------------------------- Garbage Collection ---------------------------- +;; GC is managed by gcmh in modules/gcmh-config.el: it keeps gc-cons-threshold +;; high during activity and collects on idle, replacing the old stock-800KB +;; scheme (an early-init restore plus a minibuffer setup/exit bump). gcmh lives +;; in its own module rather than here because system-defaults.el is pre-loaded +;; by the comp-errors test harness, which has no package system -- an `:ensure' +;; package loaded here would error at load time and break those tests. ;; ----------------------------- Bookmark Settings ----------------------------- diff --git a/modules/system-utils.el b/modules/system-utils.el index 7cf958674..254a2f502 100644 --- a/modules/system-utils.el +++ b/modules/system-utils.el @@ -102,7 +102,7 @@ detached from Emacs." (interactive) (save-some-buffers) (kill-emacs)) -(keymap-global-set "C-<f10>" #'cj/server-shutdown) +(keymap-global-set "C-x C" #'cj/server-shutdown) ;;; ---------------------------- History Persistence ---------------------------- diff --git a/modules/term-config.el b/modules/term-config.el index 0a7991409..c1c28911d 100644 --- a/modules/term-config.el +++ b/modules/term-config.el @@ -246,12 +246,13 @@ run its own project-named tmux session instead of a bare, auto-named one. ;; rebuild is what actually lets the key through to `ghostel-mode-map' / the ;; global map. C-; and F12 are the prefix + toggle; the modified arrows are ;; windmove (S-arrows, focus) and buffer-move (C-M-arrows, swap), which the - ;; ai-term workflow expects to work from inside an agent buffer. F8, F10 and - ;; C-F10 are global bindings (org agenda, music-playlist toggle, server - ;; shutdown) that reach Emacs by falling through to the global map once the - ;; semi-char map stops forwarding them. + ;; ai-term workflow expects to work from inside an agent buffer. F8 and F10 + ;; are global bindings (org agenda, music-playlist toggle) that reach Emacs by + ;; falling through to the global map once the semi-char map stops forwarding + ;; them. (Server shutdown moved off C-F10 to C-x C, which is deliberately + ;; left forwarding to the terminal program inside an agent buffer.) (with-eval-after-load 'ghostel - (dolist (key '("C-;" "<f8>" "<f12>" "<f10>" "C-<f10>" + (dolist (key '("C-;" "<f8>" "<f12>" "<f10>" "S-<up>" "S-<down>" "S-<left>" "S-<right>" "C-M-<up>" "C-M-<down>" "C-M-<left>" "C-M-<right>")) (add-to-list 'ghostel-keymap-exceptions key)) diff --git a/tests/test-ai-term--f9-in-term.el b/tests/test-ai-term--f9-in-term.el index dad11ffc0..0477f2517 100644 --- a/tests/test-ai-term--f9-in-term.el +++ b/tests/test-ai-term--f9-in-term.el @@ -26,27 +26,29 @@ (should (eq (keymap-lookup ghostel-mode-map "<f9>") #'cj/ai-term))) (ert-deftest test-ai-term-f9-family-bound-in-ghostel-mode-map () - "Normal: the C-/M-/C-S- F9 variants are bound in `ghostel-mode-map' too. -`M-<f9>' and `C-S-<f9>' both close an agent via `cj/ai-term-close'." + "Normal: the C-/s-/M- F9 variants are bound in `ghostel-mode-map' too. +`s-<f9>' steps to the next agent; `M-<f9>' closes an agent via +`cj/ai-term-close'." (should (eq (keymap-lookup ghostel-mode-map "C-<f9>") #'cj/ai-term-pick-project)) - (should (eq (keymap-lookup ghostel-mode-map "M-<f9>") #'cj/ai-term-close)) - (should (eq (keymap-lookup ghostel-mode-map "C-S-<f9>") #'cj/ai-term-close))) + (should (eq (keymap-lookup ghostel-mode-map "s-<f9>") #'cj/ai-term-next)) + (should (eq (keymap-lookup ghostel-mode-map "M-<f9>") #'cj/ai-term-close))) (ert-deftest test-ai-term-f9-still-bound-globally () "Normal: the global F9 family bindings are intact. `<f9>' toggles the ai-term agent window; `C-<f9>' picks a project -agent; `M-<f9>' and `C-S-<f9>' close an agent via `cj/ai-term-close'." +agent; `s-<f9>' steps to the next agent; `M-<f9>' closes an agent +via `cj/ai-term-close'." (should (eq (lookup-key (current-global-map) (kbd "<f9>")) #'cj/ai-term)) (should (eq (lookup-key (current-global-map) (kbd "C-<f9>")) #'cj/ai-term-pick-project)) - (should (eq (lookup-key (current-global-map) (kbd "M-<f9>")) #'cj/ai-term-close)) - (should (eq (lookup-key (current-global-map) (kbd "C-S-<f9>")) #'cj/ai-term-close))) + (should (eq (lookup-key (current-global-map) (kbd "s-<f9>")) #'cj/ai-term-next)) + (should (eq (lookup-key (current-global-map) (kbd "M-<f9>")) #'cj/ai-term-close))) (ert-deftest test-ai-term-f9-family-in-keymap-exceptions () "Regression: the F9 family is in `ghostel-keymap-exceptions' so semi-char mode lets it reach Emacs instead of forwarding it to the terminal program. Binding in `ghostel-mode-map' alone is not enough -- the semi-char map outranks it and forwards any key not in the exceptions to the pty." - (dolist (key '("<f9>" "C-<f9>" "M-<f9>" "C-S-<f9>")) + (dolist (key '("<f9>" "C-<f9>" "s-<f9>" "M-<f9>")) (should (member key ghostel-keymap-exceptions))) ;; The rebuilt semi-char map must no longer forward <f9> to the pty. (should-not (eq (keymap-lookup ghostel-semi-char-mode-map "<f9>") diff --git a/tests/test-ai-term--next-agent-buffer.el b/tests/test-ai-term--next-agent-buffer.el new file mode 100644 index 000000000..330714a92 --- /dev/null +++ b/tests/test-ai-term--next-agent-buffer.el @@ -0,0 +1,73 @@ +;;; test-ai-term--next-agent-buffer.el --- Tests for cj/--ai-term-next-agent-buffer -*- lexical-binding: t; -*- + +;;; Commentary: +;; The pure decision helper behind `cj/ai-term-next' (s-F9). Given the +;; current agent buffer and the ordered list of live agent buffers, it +;; returns the next buffer in the queue, wrapping after the last. A nil +;; or non-member CURRENT returns the first; an empty list returns nil. +;; No buffer or window side effects -- list logic only. + +;;; Code: + +(require 'ert) + +(add-to-list 'load-path (expand-file-name "modules" user-emacs-directory)) +(require 'ai-term) + +(ert-deftest test-ai-term--next-agent-buffer-advances-from-first () + "Normal: current is the first element -> returns the second." + (let ((a (get-buffer-create "agent [a]")) + (b (get-buffer-create "agent [b]")) + (c (get-buffer-create "agent [c]"))) + (unwind-protect + (should (eq b (cj/--ai-term-next-agent-buffer a (list a b c)))) + (mapc #'kill-buffer (list a b c))))) + +(ert-deftest test-ai-term--next-agent-buffer-advances-from-middle () + "Normal: current in the middle -> returns the following element." + (let ((a (get-buffer-create "agent [a]")) + (b (get-buffer-create "agent [b]")) + (c (get-buffer-create "agent [c]"))) + (unwind-protect + (should (eq c (cj/--ai-term-next-agent-buffer b (list a b c)))) + (mapc #'kill-buffer (list a b c))))) + +(ert-deftest test-ai-term--next-agent-buffer-wraps-after-last () + "Boundary: current is the last element -> wraps to the first." + (let ((a (get-buffer-create "agent [a]")) + (b (get-buffer-create "agent [b]")) + (c (get-buffer-create "agent [c]"))) + (unwind-protect + (should (eq a (cj/--ai-term-next-agent-buffer c (list a b c)))) + (mapc #'kill-buffer (list a b c))))) + +(ert-deftest test-ai-term--next-agent-buffer-single-element-returns-itself () + "Boundary: a one-agent queue wraps current back to itself." + (let ((a (get-buffer-create "agent [a]"))) + (unwind-protect + (should (eq a (cj/--ai-term-next-agent-buffer a (list a)))) + (kill-buffer a)))) + +(ert-deftest test-ai-term--next-agent-buffer-nil-current-returns-first () + "Boundary: nil current (no agent displayed) -> returns the first." + (let ((a (get-buffer-create "agent [a]")) + (b (get-buffer-create "agent [b]"))) + (unwind-protect + (should (eq a (cj/--ai-term-next-agent-buffer nil (list a b)))) + (mapc #'kill-buffer (list a b))))) + +(ert-deftest test-ai-term--next-agent-buffer-non-member-current-returns-first () + "Error: current not in the queue -> returns the first rather than nil." + (let ((a (get-buffer-create "agent [a]")) + (b (get-buffer-create "agent [b]")) + (stray (get-buffer-create "agent [stray]"))) + (unwind-protect + (should (eq a (cj/--ai-term-next-agent-buffer stray (list a b)))) + (mapc #'kill-buffer (list a b stray))))) + +(ert-deftest test-ai-term--next-agent-buffer-empty-queue-returns-nil () + "Boundary: an empty queue returns nil (nothing to switch to)." + (should (null (cj/--ai-term-next-agent-buffer nil '())))) + +(provide 'test-ai-term--next-agent-buffer) +;;; test-ai-term--next-agent-buffer.el ends here diff --git a/tests/test-system-defaults-functions.el b/tests/test-system-defaults-functions.el index a5210be01..2562ff6aa 100644 --- a/tests/test-system-defaults-functions.el +++ b/tests/test-system-defaults-functions.el @@ -79,20 +79,6 @@ (should (eq (cj/disabled) nil)) (should (commandp #'cj/disabled))) -;;; cj/minibuffer-setup-hook / cj/minibuffer-exit-hook - -(ert-deftest test-system-defaults-minibuffer-setup-inflates-gc-threshold () - "Normal: entering the minibuffer raises `gc-cons-threshold' to most-positive-fixnum." - (let ((gc-cons-threshold 800000)) - (cj/minibuffer-setup-hook) - (should (= gc-cons-threshold most-positive-fixnum)))) - -(ert-deftest test-system-defaults-minibuffer-exit-restores-gc-threshold () - "Normal: leaving the minibuffer restores `gc-cons-threshold' to 800000." - (let ((gc-cons-threshold most-positive-fixnum)) - (cj/minibuffer-exit-hook) - (should (= gc-cons-threshold 800000)))) - ;;; unpropertize-kill-ring (ert-deftest test-system-defaults-unpropertize-kill-ring-strips-properties () diff --git a/tests/test-system-defaults.el b/tests/test-system-defaults.el index 928124f56..f653e1fbb 100644 --- a/tests/test-system-defaults.el +++ b/tests/test-system-defaults.el @@ -63,19 +63,6 @@ test clears it first to capture the path derived from the sandbox." (expand-file-name dir))) (should (string-suffix-p "backups" (directory-file-name dir))))))) -;;; minibuffer GC hooks - -(ert-deftest test-system-defaults-minibuffer-gc-hooks-registered () - "Normal: the minibuffer GC raise/restore hooks are installed. -Their bodies are tested in test-system-defaults-functions.el; this asserts -they are actually wired onto the minibuffer hooks." - (test-system-defaults--with-load-environment - (let ((minibuffer-setup-hook nil) - (minibuffer-exit-hook nil)) - (test-system-defaults--load) - (should (memq 'cj/minibuffer-setup-hook minibuffer-setup-hook)) - (should (memq 'cj/minibuffer-exit-hook minibuffer-exit-hook))))) - ;;; Customize-save warning (ert-deftest test-system-defaults-customize-save-warns-once () diff --git a/tests/test-term-tmux-history.el b/tests/test-term-tmux-history.el index 51e9725c4..e36b3e98e 100644 --- a/tests/test-term-tmux-history.el +++ b/tests/test-term-tmux-history.el @@ -336,14 +336,15 @@ instead of being forwarded to the terminal program." (should-not (eq (keymap-lookup ghostel-semi-char-mode-map "C-M-<left>") 'ghostel--send-event))) -(ert-deftest test-term-f10-music-and-shutdown-in-keymap-exceptions () - "Regression: F10 (music playlist toggle) and C-F10 (server shutdown) are in -`ghostel-keymap-exceptions' so they reach Emacs from inside a ghostel buffer -instead of being forwarded to the terminal program. Both are global bindings, -so dropping them from the semi-char map lets the lookup fall through to the -global map." - (dolist (key '("<f10>" "C-<f10>")) - (should (member key ghostel-keymap-exceptions))) +(ert-deftest test-term-f10-music-in-keymap-exceptions () + "Regression: F10 (music playlist toggle) is in `ghostel-keymap-exceptions' +so it reaches Emacs from inside a ghostel buffer instead of being forwarded +to the terminal program. It is a global binding, so dropping it from the +semi-char map lets the lookup fall through to the global map. Server +shutdown moved off C-F10 to C-x C, which is deliberately NOT an exception +(C-x C stays forwarding to the terminal program inside an agent buffer)." + (should (member "<f10>" ghostel-keymap-exceptions)) + (should-not (member "C-<f10>" ghostel-keymap-exceptions)) (should-not (eq (keymap-lookup ghostel-semi-char-mode-map "<f10>") 'ghostel--send-event))) @@ -55,27 +55,15 @@ Tags are additive. For example, a small wrong-behavior fix can be =:bug:quick:=, and a feature that requires internal restructuring can be =:feature:refactor:=. * Emacs Open Work -** DONE [#B] F9 toggle collapses a 3-window layout to 2 :bug: +** DONE [#B] Codebase refactoring program — remaining batch :refactor:solo: CLOSED: [2026-06-20 Sat] -Fixed 2026-06-20 (option 1 — reversible toggle, Craig's call). In a 3+ window layout where -the agent had its own split, toggle-on reused the working window at the bottom edge, -displacing its buffer and collapsing three windows to two. Added a flag -(=cj/--ai-term-last-toggle-deleted-split=) set when toggle-off delete-windows the agent's own -window; =cj/--ai-term-reuse-edge-window= consumes it and falls through to a fresh re-split, so -the agent returns to its own window and the others are untouched. The flag only changes the 3+ -window case (2-window slot-reuse unchanged). TDD regression -=test-ai-term--reuse-edge-window-3win-toggle-restores-own-window=; full =make test= green; -live-reloaded. Commit 64916462. GUI sign-off is a VERIFY under Manual testing and validation. - -** TODO [#B] Codebase refactoring program — remaining batch :refactor:solo: -Resumes the full-codebase refactoring scan run of 2026-06-20 (8-agent fan-out over -modules/ + scripts/theme-studio/). The goal: apply every scan finding except the -won't-do items, one focused refactor per commit. 25 done and pushed across the -2026-06-20 sessions (see =.ai/sessions/= for the logs); 8 remain, listed below. -The 5 medium extractions are done (calibredb-epub nov helpers fccf29b0, ai-term -toggle-off 62fee96b, calendar-sync exception parser 23f405b4, dirvish playlist-target -a1ca2fb0, custom-case title-case-word 4cc9ca0b); the 2 big single-file and 6 -theme-studio items below remain. +Complete 2026-06-20: all 13 scan findings addressed across the day's sessions (see +=.ai/sessions/= for the logs). 5 medium extractions + 2 big single-file refactors + +6 theme-studio items including the browser-gates harness rewrite. The only item not +done is the item-8 plan() factory, consciously skipped as premature abstraction +(heterogeneous call sites — see "Remaining — item-8 plan() factory" below). +The original scan: full-codebase 8-agent fan-out over modules/ + scripts/theme-studio/, +one focused refactor per commit, won't-do items excluded. *** Working protocol (apply to every item) - TDD: write/keep a failing-then-green test; harvest new test seams the refactor opens. @@ -114,22 +102,18 @@ delete (10a56789), test-file inline-integrity dedup — subTest loop + shared inline-strip.mjs (13969c70), generate.py lazy _build()/__getattr__ (6df4ebdc), browser-gates assertPreviewFaces for the 3 preview gates (5627f137). -*** Remaining — browser-gates harness rewrite (HIGHEST RISK, deferred for review) -Two parts of the browser-gates.js item are intentionally NOT done in the -autonomous no-approvals run — they rewrite the harness that verifies everything, -so a subtle helper bug manufactures silent false-greens across all 44 gates: -- =gate(name, body)= helper for the ~39 gates' =let ok;notes;A=...; title; result-div= - boilerplate. LOW value (pure boilerplate; note format drifted 17 " | " vs 24 - " fails="). CRITICAL CONSTRAINT on any attempt: each gate keeps its literal - =location.hash==='#NAMEtest'= (run-tests.sh greps it to discover gates) — wrap only - the body, e.g. =if(location.hash==='#x')gate('X','x',A=>{...})=. Verify: all 44 gates - green AND a deliberately-broken assertion still FAILS (proved feasible — chrome is - available; the assertPreviewFaces commit ran exactly that check). -- =withSavedState(keys, body)= for the ~13 mutating gates' inconsistent - PALETTE/MAP/UIMAP/SYNTAX snapshot-restore (7 mutating gates currently restore - NOTHING — a real state-pollution bug, not just dedup). Needs per-gate key analysis. -Both warrant Craig's eye before/after given the harness-rewrite risk. The -=assertPreviewFaces= part of this item is already done (5627f137). +*** DONE — browser-gates harness rewrite (with Craig's go-ahead, 2026-06-20) +- =gate(id, body)= helper (05697e83): the 38 standard gates' ok/notes/A + title + + result-div boilerplate, note format standardized to " fails=". Each call site keeps + its literal =location.hash==='#NAMEtest'=. 6 custom gates stay inline. First automated + attempt deleted gates (a closing-finder spanned boundaries) — caught by a gate-count + guard, reverted, redone anchored on each gate's unique =d.id=. Verified all 44 green + + a forced A(false) in a converted gate still FAILs. +- =withSavedState(keys, body)= (a473aa7c): wraps the 7 restore-nothing gates, scoped to + the globals each mutates; JSON-clone snapshot + finally-restore (structuredClone threw + on the studio objects — caught by the gate run as "no verdict", switched to JSON like + the gates' own local saves). The 14 self-restoring gates left as-is. Verified 44 green, + restore round-trip holds, broken assertion in a wrapped gate still FAILs. *** Remaining — item-8 plan() factory (deferred, low value) The =plan(overrides)= factory for the ~30 planPaletteGenerator calls (test-app-core.mjs @@ -168,7 +152,8 @@ ghostel is held at 0.33.0 (=ghostel-20260604.2049=, commit 5779a2adceb2) in =mod archsetup automated the zig 0.15.2 pin (managed =install_zig_pin= step, sha-verified, unit-tested). If the un-pinned ghostel bumps its ghostty dependency to a newer zig, send archsetup the new version + sha256 so it bumps its =ZIG_VERSION= / =ZIG_SHA256= constants (=inbox-send archsetup=). -** VERIFY [#A] calendar-sync drops final occurrences, resurrects cancelled meetings :bug:solo:next: +** CANCELLED [#A] calendar-sync drops final occurrences, resurrects cancelled meetings :bug:solo:next: +CLOSED: [2026-06-20 Sat 22:51] :PROPERTIES: :LAST_REVIEWED: 2026-06-13 :END: @@ -177,13 +162,13 @@ RFC 5545 conformance holes in =modules/calendar-sync.el=, all agenda-visible (fr - =:973,1015,1024= — UNTIL treated as exclusive (strict =calendar-sync--before-date-p=); RFC and Google make it inclusive, so the LAST instance of every UNTIL-bounded series vanishes. Tests assert loose count ranges, so it's unpinned. Allow equality. - =:578= — comma-separated EXDATE lists (Google emits them) never parse; the exclusion drops silently and cancelled occurrences reappear on the agenda. Split on "," before parsing; no comma-case test exists. - =:902= — timed events without DTEND render as all-day (time lost); multi-day all-day spans collapse to one day (end date unused, exclusive-DTEND unhandled). Emit start-time-only stamps and org date ranges. +----- -** VERIFY [#A] Native compilation disabled config-wide; GC at stock 800KB :bug:next: -:PROPERTIES: -:LAST_REVIEWED: 2026-06-13 -:END: -Needs from Craig: re-enabling native-comp config-wide is a stability/perf judgment, not a mechanical fix. Was it disabled deliberately (a crash, a build without native-comp, async-warning noise)? If you want it back on, confirm and I'll re-enable + raise the GC threshold and verify a clean full launch; otherwise this stays parked. I won't flip it blind. -From the 2026-06 config audit (verified against the live daemon). =early-init.el:69= =(setq native-comp-deferred-compilation nil)= — the obsolete alias of =native-comp-jit-compilation= — turns JIT native compilation OFF entirely, not "synchronous" as the comment claims: 19 .eln files exist for 184 packages, ~100 of 121 modules run interpreted for the daemon's lifetime, and system-defaults.el:42-44's speed-3/8-jobs/always-compile settings are dead. Plus =early-init.el:113-116= restores =gc-cons-threshold= to the captured STOCK default (800000, verified) post-startup — frequent small GC pauses forever. Together these plausibly feed the filed org-capture 15-20s task more than anything in the capture path itself. Actions: retest the old "Selecting deleted buffer" race on 30.2 and re-enable JIT (or AOT sweep); set a deliberate 16-64MB threshold (or gcmh). Check both before burning time on the capture-perf debug task. +2026-06-20 Sat @ 22:52:51 -0400 Can't reproduce. closing + +** DONE [#A] Native compilation disabled config-wide; GC at stock 800KB :bug:next: +CLOSED: [2026-06-20 Sat] +Both fixed 2026-06-20. =early-init.el:69= was =(setq native-comp-deferred-compilation nil)= — the obsolete alias of =native-comp-jit-compilation= — which turned JIT native-comp OFF entirely (not "synchronous"); replaced with =(setq native-comp-jit-compilation t)= + =native-comp-async-report-warnings-errors 'silent=. The old "Selecting deleted buffer" async race was an Emacs 28/29 issue; this is 30.2. GC: dropped the early-init post-startup restore to stock 800KB and the system-defaults minibuffer setup/exit hooks, replaced with gcmh (idle-delay 'auto, 1GB high threshold) — keeps the threshold high during activity, collects on idle. Verified via a clean throwaway-daemon launch (native-comp-jit t, gcmh-mode t, no backtrace) and a batch proof of gcmh's threshold cycle; applied live to the running daemon. Restart confirmation filed under Manual testing and validation. ** VERIFY [#B] calendar-sync robustness: atomic writes, curl --fail, zero-event false errors :bug:solo:next: :PROPERTIES: @@ -211,7 +196,8 @@ From the 2026-06 config audit, =modules/transcription-config.el=: - =:210= — =make-process :stderr= with a file PATH creates a BUFFER named like the path (verified by probe); the "Errored. Logs in <file>" notification points at a log without the error text, and the hidden stderr buffer leaks per transcription. Route stderr into the process buffer or write it out in the sentinel. - =:370-374= — video path derives txt/log from the temp mp3's /tmp path; the transcript lands in /tmp and dies on reboot, contradicting the "alongside the source" docstring. Pass the video's path as the output base. -** 2026-06-20 Sat @ 10:29:42 -0400 Dirvish: d duplicates, D force-deletes (guarded) +** DONE [#C] Dirvish: free D for hard-delete, move duplicate :feature:quick:next: +CLOSED: [2026-06-20 Sat] Decided with Craig 2026-06-20: remove delete-to-trash entirely, bind =d= = =cj/dirvish-duplicate-file= and =D= = =cj/dirvish-hard-delete= (sudo rm -rf after a =yes-or-no-p= naming the exact targets). Built in =modules/dirvish-config.el= (=cj/--dirvish-hard-delete-command= pure builder + =cj/dirvish-hard-delete= command; keymap =d=/=D= swap). 4 ERT tests for the command builder; full suite green; live-reloaded into the daemon (=dirvish-mode-map= =d=/=D= rebinding confirmed). Manual keypress + sudo-flow check filed under Manual testing and validation. ** VERIFY [#C] page-signal pager account deregistered — re-registration needs your hands @@ -220,7 +206,8 @@ Decided with Craig 2026-06-20: remove delete-to-trash entirely, bind =d= = =cj/d :END: Reported by .emacs.d 2026-06-12 01:01: the dedicated pager number (+15045173983, the Claude Pager Google Voice number on signal-cli) returns "User ... is not registered" on every send — Signal appears to have deregistered it (GV numbers get periodically re-verified). Re-registration requires captcha/SMS, which only you can do. Until then every page-signal call fails; .emacs.d's config-audit page fell back to email. Wrapper lives at claude-templates/bin/page-signal. -** 2026-06-20 Sat @ 10:29:42 -0400 C-; b + arrow pulls a window away from a sole window +** DONE [#C] Pull a fullscreen terminal window away with C-; b + arrow :feature:next: +CLOSED: [2026-06-20 Sat] Decided with Craig 2026-06-20: when the selected window is the sole window, =C-; b= + arrow keeps that window on the arrow's edge and slivers =other-buffer= in on the opposite side (=minimize-window=, so the current window keeps almost the whole frame), focus staying put; each further arrow then shrinks it step by step via =windsize=, reading the same as resizing an existing split. Generalizes to any sole window, not just terminals — resize was a no-op there before. Built in =modules/ui-navigation.el= (=cj/window-pull-side= pure mapping + =cj/window--pull-away= + a =one-window-p= branch in =cj/window-resize-sticky=). ERT tests for the mapping and both sticky paths; geometry verified in a headless frame (down -> terminal 37/40 at the bottom, reveal 2 lines slivered on top via window-min-height=1, windsize-down then steps it down); full suite green; live-reloaded into the daemon. Refined from a first cut that split toward the arrow and jumped to 50%, per Craig's feedback. Manual gesture check filed under Manual testing and validation. ** VERIFY [#C] Remove unused system-power keybindings :refactor:quick:next: @@ -245,6 +232,18 @@ Fixed 2026-06-13: cmail gets =mu4e-trash-folder= "/cmail/Trash"; refile is a per Fixed 2026-06-13: lockscreen-cmd resolves to =loginctl lock-session= on Wayland (logind Lock → hypridle → hyprlock, the path idle/sleep locking already uses), =slock= on X11; also added the missing =(require 'host-environment)=. Live in the daemon; manual lock test under the Manual testing parent. ** PROJECT [#A] Manual testing and validation Exercised once the phases above land. +*** VERIFY native-comp + gcmh survive a daemon restart cleanly +What we're verifying: re-enabling JIT native compilation and switching GC to gcmh holds up across a full daemon restart and a real work session. The fix is live in the current daemon and a throwaway daemon launched clean, but the already-loaded modules only get natively compiled on a fresh start (a background async burst), and the old "Selecting deleted buffer" race needs a real GUI session to rule out on 30.2. +- Restart the Emacs daemon (clean state): kill it and start fresh, or reboot. +- Use Emacs normally for a while — the first session after restart triggers background native compilation of ~100 modules. Watch for any "Selecting deleted buffer" errors or compilation crashes (check the *Async-native-compile-log* buffer and comp-warnings.log). +- After things settle, confirm the settings are live: +#+begin_src emacs-lisp +(list :jit native-comp-jit-compilation + :gcmh gcmh-mode + :gcmh-high gcmh-high-cons-threshold) +#+end_src +- Edit normally (completion, agenda, AI buffers) and notice whether the periodic GC jank is gone. +Expected: restart is clean (no backtrace); the background native-comp burst finishes without "Selecting deleted buffer" errors; the form returns (:jit t :gcmh t :gcmh-high 1073741824); editing feels smoother with no frequent GC pauses. If the async race recurs on 30.2, capture the error and reopen as a TODO — the fallback is an AOT sweep or going back to JIT-off. *** VERIFY mu4e buffers are themed (headers, main, message view) What we're verifying: with the mu4e modes excluded from global font-lock, mu4e's manual face properties survive, so the buffers pick up the theme. The headers + main + view-headers are the ones global font-lock was stripping. - Restart Emacs (cleanest), or kill and reopen the mu4e buffers @@ -452,24 +451,10 @@ What we're verifying: in dirvish, d now duplicates the file at point (delete-to- - Answer no first (confirm nothing happens), then press D again and answer yes - Note whether sudo prompts for a password and whether the file actually disappears Expected: d duplicates; D names the exact targets and only deletes on yes; the files are gone with no trash copy. If sudo needs a password that shell-command can't supply, flag it — the delete may need to route through a tty instead. -*** VERIFY F9 agent toggle no longer shrinks the window after a C-; b pull-away -What we're verifying: the f9 split-shrink bug is fixed. The toggle now captures the agent window's total-height (not body-height), so the capture/replay round-trip is immune to the mode line's pixel height — the agent should keep the same height across repeated toggles even with the WIP theme's near-zero =mode-line-inactive= (=:height 2=). The batch harness can't reproduce this (a TTY mode line is always a full line), so this needs a real GUI frame. -- Open the agent (F9) and maximize it so it fills the frame (=C-x 1= inside it) -- Pull it down with =C-; b <down>= (the reveal slivers in above; the agent becomes the bottom window) and press =<escape>= to end the sticky resize -- Note the agent window's height (eyeball it, or =M-:= the form below) -#+begin_src emacs-lisp -(window-total-height (get-buffer-window (current-buffer))) -#+end_src -- Press F9 to toggle the agent off, then F9 again to toggle it back on. Repeat 5–6 times. -- Re-check the height after each on-toggle -Expected: the agent window stays the same height every cycle (no one-line-per-toggle shrink). Before the fix it lost ~1 line each toggle. If it still shrinks, capture the height sequence and reopen this as a TODO — the remaining drift would point at a different rounding source than the mode-line pixel height. -*** VERIFY F9 toggle preserves all windows in a 3-window layout -What we're verifying: toggling the agent off then on in a 3-window layout no longer eats a working window. The fix re-splits the agent into its own window on toggle-on instead of reusing the working window at the edge, so the layout is preserved across the toggle (off then on returns the same three windows). Batch tests already assert the window count; this is the visual read in a real frame. -- Set up three windows: a code/file window on top, a second working window (e.g. todo.org) in the middle, and the agent (F9) as a full-width strip at the bottom -- Note the three windows and which buffers they show -- Press F9 to toggle the agent off (the bottom strip closes, two windows remain) -- Press F9 again to toggle it back on -Expected: you are back to three windows — the agent returns as its own bottom strip, and both working windows (code + middle) are still showing their buffers. Before the fix, toggle-on replaced the middle/bottom working window with the agent, leaving two windows. +*** 2026-06-20 Sat @ 22:11:00 -0400 F9 agent toggle no longer shrinks after a C-; b pull-away +Craig confirmed in his live GUI frame: the agent window keeps its height across repeated F9 toggles after a C-; b pull-away, even under the WIP theme's near-zero mode-line-inactive. The total-height capture/replay fix holds (dbee95ae). +*** 2026-06-20 Sat @ 22:11:00 -0400 F9 toggle preserves all windows in a 3-window layout +Craig confirmed in his live GUI frame: toggling the agent off then on in a 3-window layout returns the same three windows — both working windows survive and the agent re-splits its own bottom strip. The reversible-toggle fix holds (64916462). ** PROJECT [#A] Theme-Studio Open Work Parent grouping the open theme-studio / theming issues; close each child independently. @@ -1017,7 +1002,8 @@ Add the buffer-local var, set it on each "Run a test..." selection, use it as th *** TODO [#B] TS/JS coverage status sync Update the =dev-fkeys.el= header comment (L33) — TS/JS is no longer punted; the cmd-builder at L384 emits vitest/jest. Document the prefer-vitest fallback. -** PROJECT [#B] Migrate All Terminals From Vterm to Ghostel +** DONE [#B] Migrate All Terminals From Vterm to Ghostel +CLOSED: [2026-06-20 Sat 22:50] :PROPERTIES: :LAST_REVIEWED: 2026-06-04 :END: @@ -1025,14 +1011,16 @@ Replace vterm with ghostel (libghostty-vt) as the single terminal engine across Decisions D1-D7 are settled in the spec's Agreed-decisions section. Build order below; each phase stays green (suite + byte-compile) at every step. -*** TODO [#B] Follow-up: theme ghostel ANSI faces in dupre +*** 2026-06-20 Sat @ 22:49:41 -0400 Follow-up: theme ghostel ANSI faces in dupre D2 — set the 16 ghostel-color-* + ghostel-default faces in dupre-faces/palette. Roam-inbox note (2026-06-14): theme-studio assignments don't reach ghostel — it paints from its own ANSI palette, not the theme. Also investigate ghostel's property-file color mechanism as an alternative and surface the options for working with that limitation. -*** TODO [#B] Follow-up: evaluate ghostel-eshell + ghostel-compile +*** 2026-06-20 Sat @ 22:50:28 -0400 CANCELLED [#B] Follow-up: evaluate ghostel-eshell + ghostel-compile +CLOSED: [2026-06-20 Sat 22:49] D3 — ghostel-eshell as eshell visual backend; ghostel-compile against F4 dev-fkeys. -*** TODO [#B] Investigate ghostel selection/highlight color +*** 2026-06-20 Sat @ 22:50:32 -0400 DONE [#B] Investigate ghostel selection/highlight color +CLOSED: [2026-06-20 Sat 22:50] Look at how selected text is highlighted in a ghostel buffer — the region face in =ghostel-copy-mode= and any live selection — surfaced during the copy-mode debugging. Check whether the highlight is legible against the dupre background and consistent with the rest of the config; if it needs theming, fold it in with D2 (theming the ghostel faces in dupre). *** 2026-06-04 Thu @ 23:57:09 -0500 Phase 0 done: characterization baseline green @@ -3576,23 +3564,42 @@ Restart the daemon, open a GUI frame, trigger an encrypted decrypt, confirm =pin *** TODO [#C] Archive the original L3813 task After this work lands, mark the original "Finish terminal GPG pinentry configuration" task DONE with a =CLOSED:= stamp and a one-line note pointing at this parent task. -** TODO [#A] Unified popup placement and dismissal rules :feature: -All transient popups should follow one set of principles. Placement: when the Emacs frame is wider than tall, the popup rises from the right; when square or taller, from the bottom — settle the aspect-ratio threshold and the pop-out percentage. Dismissal: C-c C-c when there's an accept action, C-c C-k when there's a cancel, otherwise =q= closes the window. This generalizes two existing tasks — ai-term adaptive placement (the aspect-ratio docking) and the messenger window/key unification spec (the C-c C-c / C-c C-k dismissal) — into one config-wide policy. From the roam inbox. - -** TODO [#A] Unify Signel and All Messengers into one UX :feature: +** TODO [#A] Unified popup and messenger UX — placement, dismissal, one library :feature: :PROPERTIES: -:LAST_REVIEWED: 2026-06-16 +:LAST_REVIEWED: 2026-06-20 :END: +Merged 2026-06-20 from the config-wide popup-policy task and the messenger-unification +task — they're the same policy at two scopes (the messenger windows are the first +concrete application of the general popup rules). Two parts: + +(A) Config-wide popup policy. All transient popups follow one set of principles. +Placement: when the Emacs frame is wider than tall, the popup rises from the right; +when square or taller, from the bottom — settle the aspect-ratio threshold and the +pop-out percentage. Dismissal: C-c C-c when there's an accept action, C-c C-k when +there's a cancel, otherwise =q= closes the window. Generalizes ai-term adaptive +placement (the aspect-ratio docking) and the messenger window/key rules below into +one config-wide policy. From the roam inbox. + +(B) Messenger unification (first application of the policy above). Spec: [[file:docs/specs/messenger-unification-spec.org][messenger-unification-spec.org]] ([[id:4bfc2011-8ffc-4765-8886-91df12141171][by id]], Draft, 2026-06-11; keybinding-alphabet section + smoke-first parity added 2026-06-16). One library (=cj-messenger-lib.el=) gives every messenger the same shape: chat windows rise from the bottom (the signel rule, generalized), C-c C-c confirms, C-c C-k cancels, C-c C-a attaches — dispatched per backend through a registry + minor mode. Signel already conforms (reference backend); telega and slack join in phases 2-3; ERC later. All eight decisions settled 2026-06-11 (cancel closes an idle window; telega's filter-cancel shadow accepted; slack rooms join the bottom rule). Spec held open — Craig has more ideas to fold in before it's marked Ready. ** TODO [#B] agenda sources: roam Projects missing, no existence filtering :bug:solo: +:PROPERTIES: +:LAST_REVIEWED: 2026-06-20 +:END: From the 2026-06 config audit, =modules/org-agenda-config.el=: - =:182-191= — commentary and docstrings promise org-roam nodes tagged "Project" as agenda sources, but =cj/--org-agenda-scan-files= never scans them, and files added by the roam finalize-hook are wiped on the next =cj/build-org-agenda-list= cache rebuild (≤1h). Add a roam Project pass (mirror =org-refile-config.el:101-109=) or correct the docs. - =:186,456= — agenda file list built unconditionally (inbox/calendars may not exist on a fresh machine) and =org-agenda-skip-unavailable-files= is unset — the exact interactive-prompt class that once hung the chime daemon. Filter with =file-exists-p= + set the var as backstop. ** TODO [#B] Auto-dim: org headings, links, and tags do not dim in unfocused windows :bug: +:PROPERTIES: +:LAST_REVIEWED: 2026-06-20 +:END: auto-dim-other-buffers-affected-faces (auto-dim-config.el) remaps font-lock and a few org faces to the flat dim face, but not org-level-1..8, org-link, or org-tag, so headings, links (seen in daily-prep.org), and tags like :solo: stay lit when the window loses focus. Decide the dim approach: a flat-dim remap like font-lock (quick) versus dedicated -dim variants surfaced through org-faces / theme-studio (richer, matches the keyword work; Craig flagged org-tags may want the org-faces treatment). Consolidates three roam-inbox captures. ** TODO [#B] "? = curated help menu" convention across modes :feature: +:PROPERTIES: +:LAST_REVIEWED: 2026-06-20 +:END: From the calibredb keybindings work 2026-06-06. The pattern that worked: in a modal/major-mode buffer (calibredb), bind =?= to a curated transient of the frequent workflows, and move the package's own full dispatch to =H=. It fixes the "I can't discover the keys" problem that which-key can't help with (which-key only pops up after a prefix, not for top-level single keys in a mode-map). Task: survey the modes/modules Craig works in and identify where a =?= -> curated-help-menu (transient) makes sense. Candidates: any major-mode buffer with single-key bindings and no good discovery affordance -- calibredb (done), nov, dirvish, mu4e, ghostel/term, signel, pearl/linear, ELFeed, etc. For each, note whether =?= is free or already a help dispatch, and whether a curated menu (vs the package's own) adds value. Establish it as a convention (and maybe a small helper/macro to define a curated =?= menu consistently). @@ -3610,10 +3617,14 @@ Ask: Reference values -- modus-vivendi: refine-changed bg #4a4a00 fg #efef80, changed bg #363300 fg #efef80. modus-operandi: refine-changed bg #fac090 fg #553d00, changed bg #ffdfa9 fg #553d00. Side-by-side legibility render: [[file:assets/2026-06-07-dupre-diff-face-legibility-compare.png][assets/2026-06-07-dupre-diff-face-legibility-compare.png]]. -** TODO [#B] erc-yank silently publishes >5-line pastes as public gists :bug: -=modules/erc-config.el:345= — C-y in any ERC buffer auto-creates a public gist for anything over 5 lines: clipboard content goes to a public URL with no confirmation, and no executable-find guard for =gist= (errors mid-send if absent). Privacy trap. Add a =yes-or-no-p= gate or drop the package for plain C-y. From the 2026-06 config audit. +** DONE [#A] erc-yank silently publishes >5-line pastes as public gists :bug:quick:solo: +CLOSED: [2026-06-20 Sat] +Dropped erc-yank 2026-06-20 (Craig's call: drop, not harden). The package turned a >5-line paste into a PUBLIC gist (=gist -P=, the clipboard-paste flag, no =--private=) behind a single y-or-n-p, with no executable-find guard for =gist=. It also gisted the system clipboard rather than the kill-ring text being yanked. No replacement binding needed: =erc-mode-map= defines no C-y of its own, so removing the package lets C-y fall through to the ordinary global =yank=. Verified live: effective C-y in an ERC buffer = =yank=. (Audit's "no confirmation" was slightly off — the package did prompt — but public-by-default + one-keystroke confirm + no guard made dropping it the clean fix.) ** TODO [#B] F7 diff-aware coverage classifies every changed file "not tracked" :bug:solo: +:PROPERTIES: +:LAST_REVIEWED: 2026-06-20 +:END: =modules/coverage-core.el:252= — =cj/--coverage-intersect= joins covered×changed by exact string key, but simplecov.json keys are ABSOLUTE paths while the git-diff parser returns repo-RELATIVE ones — zero matches ever, so working-tree/staged/branch scopes report ":tracked nil" for everything and F7's main feature is inert (whole-project scope works, same-source keys). Unit tests hand-build matching keys so they pass; add one integration test feeding a real undercover report + real diff. Normalize both sides to repo-relative. From the 2026-06 config audit. ** TODO [#B] Fix up test runner :bug: @@ -8728,3 +8739,14 @@ Dump from the live daemon by default (reflects the packages actually run); the b ** DONE [#C] todo.org org-lint follow-ups :refactor: CLOSED: [2026-06-20 Sat] From the lint-org sweeps (2026-06-15, refreshed 2026-06-20). Resolved 2026-06-20: the misplaced-heading false positive was reworded (the bug-capture task's prose quoted heading-like "* TODO" strings), and the broken link was repointed from the missing =~/code/signel/todo.org= to =~/code/smoke/todo.org= (smoke is the evolved Signal package). The obsolete-properties-drawer entries no longer reproduce under a full org-lint pass. Both lint-org --check and the built-in org-lint now report zero. +** DONE [#B] F9 toggle collapses a 3-window layout to 2 :bug: +CLOSED: [2026-06-20 Sat] +Fixed 2026-06-20 (option 1 — reversible toggle, Craig's call). In a 3+ window layout where +the agent had its own split, toggle-on reused the working window at the bottom edge, +displacing its buffer and collapsing three windows to two. Added a flag +(=cj/--ai-term-last-toggle-deleted-split=) set when toggle-off delete-windows the agent's own +window; =cj/--ai-term-reuse-edge-window= consumes it and falls through to a fresh re-split, so +the agent returns to its own window and the others are untouched. The flag only changes the 3+ +window case (2-window slot-reuse unchanged). TDD regression +=test-ai-term--reuse-edge-window-3win-toggle-restores-own-window=; full =make test= green; +live-reloaded. Commit 64916462. GUI sign-off is a VERIFY under Manual testing and validation. |
