aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--docs/specs/google-keep-emacs-integration-spec.org153
-rw-r--r--init.el1
-rw-r--r--modules/dirvish-config.el9
-rw-r--r--modules/ledger-config.el40
-rw-r--r--tests/test-dirvish-config-wallpaper-program.el4
-rw-r--r--todo.org25
6 files changed, 206 insertions, 26 deletions
diff --git a/docs/specs/google-keep-emacs-integration-spec.org b/docs/specs/google-keep-emacs-integration-spec.org
new file mode 100644
index 000000000..0b57f731f
--- /dev/null
+++ b/docs/specs/google-keep-emacs-integration-spec.org
@@ -0,0 +1,153 @@
+#+TITLE: Google Keep <-> Emacs integration — Spec
+#+AUTHOR: Craig Jennings & Claude
+#+DATE: 2026-06-24
+#+TODO: TODO | DONE SUPERSEDED CANCELLED
+
+* Metadata
+| Status | draft |
+|----------+------------------------------------------------------------|
+| Owner | Craig |
+|----------+------------------------------------------------------------|
+| Reviewer | Codex (spec-review) |
+|----------+------------------------------------------------------------|
+| Related | [[file:../../todo.org][todo.org: google-keep in-editor integration]] |
+
+* Problem / Context
+
+Craig keeps quick notes in Google Keep but works almost entirely in Emacs. Today, reading or acting on a Keep note means leaving Emacs for the phone or the web app — a context switch for content that wants to live next to his org files. He wants Keep notes native to Emacs: browsable, searchable, greppable, and eventually editable, with a path to publish the result as a standalone package.
+
+Two hard constraints shape every choice:
+
+1. *No official API.* Google Keep has no public API. Every live client (gkeepapi and the tools built on it) reverse-engineers the private mobile endpoint. That layer is fragile: it breaks when Google changes auth, and it needs a Google master token, not a password.
+2. *The existing MCP is agent-only.* Craig already has a google-keep MCP with full read/write (create/update/find/labels/archive/list-items). But MCP tools are invoked by the *agent* (Claude), not from elisp — there is no elisp MCP client. So the in-editor integration cannot reuse the MCP as its data path; it needs its own.
+
+Researched 2026-06-24: there is no in-editor Emacs Google Keep package on MELPA or GitHub — only KeepToOrg, a one-shot Takeout-HTML-to-org importer (unmaintained). So this is a build, not an adopt.
+
+* Goals and Non-Goals
+** Goals
+- Keep notes visible and usable inside Emacs without leaving the editor.
+- An org-native representation, so notes are searchable/greppable and reuse org machinery.
+- A structure that starts as glue in =.emacs.d= and can be extracted to a publishable package (the VAMP / pearl module-to-package pattern).
+- A path to read-write (create/edit notes from Emacs) without making v1 wait on it.
+** Non-Goals
+- Full bidirectional offline sync, conflict resolution, or real-time updates.
+- Faithful round-tripping of every Keep feature (list checkboxes, collaborators, drawings, images).
+- Reusing the MCP from elisp (infeasible — agent-only).
+** Scope tiers
+- v1: read-only. Fetch Keep notes via the chosen data path and render them as an org page (each note an org header). A manual refresh command. Auth via auth-source. Graceful degradation when the bridge or credentials are missing.
+- Out of scope: write-back to Keep, list/checkbox fidelity, label/color/pin *editing*, the org-capture-style popup, package extraction.
+- vNext: read-write (create a note from a region or capture; edit a note back to Keep), the org-capture-style quick-note popup, list/checkbox rendering, and extracting the core to a standalone package.
+
+* Design
+
+** For the user
+
+A command (e.g. =cj/keep-refresh=) pulls the current Keep notes and writes them into one org file (e.g. =~/org/keep.org=). Each note becomes a top-level org heading: the title (or a derived title) as the heading text, the note body as the entry, and Keep metadata as properties — labels as org tags, plus =:KEEP_ID:=, =:PINNED:=, =:COLOR:=, =:ARCHIVED:=, =:UPDATED:= in a drawer. Pinned notes sort first. The file is plain org, so it is searchable with the agenda, greppable, and linkable. Opening it is just visiting the file; a keybinding and a dashboard entry make it one keystroke. v1 is read-only: editing the org file does not push back to Keep (a header note says so), so there is no accidental-mutation risk while the integration is young.
+
+** For the implementer
+
+Three layers, cleanly separable so the core can later be a package:
+
+1. *Data bridge (Python).* A small script using gkeepapi: authenticate with a stored master token, fetch notes, emit JSON (id, title, text, labels, pinned, color, archived, timestamps) on stdout. This is the one place the unofficial API lives, isolated so a break is contained and swappable. A Takeout-import path is the no-auth fallback (parse a Takeout dump into the same JSON shape).
+2. *Org renderer (elisp).* Runs the bridge as a subprocess, parses its JSON, and writes the org page (heading + body + properties per note), with =cj/keep-refresh= as the entry point. Reads the master token via =auth-source=.
+3. *Access UX (elisp).* Keybindings, a dashboard entry, and (vNext) a dedicated buffer/mode or the org-capture-style popup.
+
+* Alternatives Considered
+
+** A — Takeout import (one-shot HTML -> org)
+- Good, because no auth, fully offline, dead simple, and zero ongoing breakage risk.
+- Bad, because it is not live — Craig must manually export a Takeout archive, so notes are stale the moment they are imported.
+- Neutral, because it is the right tool for an archival snapshot, the wrong one for daily use. Kept as the v1 fallback / bootstrap, not the primary.
+
+** B (chosen for the data path) — gkeepapi via a Python subprocess bridge
+- Good, because it is the only path that gives live notes (and, in vNext, write-back) from inside Emacs, with the full note model.
+- Bad, because gkeepapi reverse-engineers a private API: it breaks on Google auth changes, needs Python plus a stored master token, and the bridge is glue Craig owns and maintains.
+- Neutral, because the fragility is isolated to one script; when it breaks, the renderer degrades to a warning and the Takeout fallback still works.
+
+** C — Reuse the google-keep MCP from elisp
+- Good, because the MCP already has full read/write and is maintained outside the config.
+- Bad, because MCP tools are invoked by the agent, not elisp — there is no elisp MCP client, so this is infeasible for an in-editor feature.
+- Neutral, because the MCP stays the right tool for agent-driven Keep access; it just can't power an in-editor integration.
+
+** D — A local HTTP server wrapping gkeepapi
+- Good, because elisp would talk clean HTTP instead of spawning a subprocess each refresh.
+- Bad, because a long-running personal server is more infrastructure than a single-user note view warrants.
+- Neutral, because it is a heavier variant of B; revisit only if subprocess latency ever bites.
+
+* Decisions [0/5]
+
+** TODO Presentation shape: org page of headers (v1), popup deferred
+- Owner / by-when: Craig / before implementation
+- Context: Craig named two shapes — an org-capture-style popup and a separate org page with each note as a header.
+- Decision: We will render notes as one *org page*, each note a top-level header (title + body + metadata properties, labels as tags), in v1; the org-capture-style quick-note *popup* is vNext.
+- Consequences: easier — reuses org search/agenda/grep/links, nothing bespoke to render, and read-only is safe. Harder — a popup-first quick-capture flow waits for vNext, and a large Keep collection makes a long file (mitigated by pinned-first sort and org folding).
+
+** TODO Direction: read-only in v1, read-write in vNext
+- Owner / by-when: Craig / before implementation
+- Context: the bridge can read and (later) write Keep; doing both in v1 raises the risk surface.
+- Decision: We will ship v1 read-only (fetch + render + refresh); create/edit-back-to-Keep is vNext.
+- Consequences: easier — no accidental Keep mutation while the integration is young, and value ships fast. Harder — editing a note still means the phone/web until vNext; the org file is a view, not a source of truth.
+
+** TODO Data path: gkeepapi subprocess bridge, Takeout import as fallback
+- Owner / by-when: Craig / before implementation
+- Context: the MCP is agent-only; the live options are gkeepapi or a Takeout import.
+- Decision: We will use a small Python gkeepapi bridge that emits JSON as the primary path, with a Takeout-import parser into the same JSON shape as the no-auth fallback. The MCP is not in the data path.
+- Consequences: easier — live notes now, write-back later, one isolated fragile component. Harder — a Python + gkeepapi dependency and a maintained bridge; the unofficial API can break and needs a master token.
+
+** TODO Auth: master token in authinfo.gpg via auth-source
+- Owner / by-when: Craig / before implementation
+- Context: gkeepapi authenticates with a Google *master token* (obtained once), not the account password.
+- Decision: We will store the master token in =authinfo.gpg= and read it via =auth-source= (the pattern the rest of the config uses), and document the one-time master-token retrieval.
+- Consequences: easier — consistent with existing credential handling, no plaintext secret, the daemon's auth-source cache applies. Harder — the one-time token retrieval is a manual setup step, and a revoked/expired token surfaces as an auth failure the renderer must report cleanly.
+
+** TODO Structure: google-keep-config.el glue + extractable core
+- Owner / by-when: Craig / before implementation
+- Context: Craig wants a module-to-package trajectory.
+- Decision: We will build the integration as =modules/google-keep-config.el= (the =.emacs.d= glue: paths, keys, dashboard, auth wiring) plus a self-contained core (the bridge runner + org renderer) written so the core can later move to a standalone =keep.el=-style package, mirroring the VAMP / pearl migration.
+- Consequences: easier — usable immediately in =.emacs.d=, with a clean seam for later extraction. Harder — the discipline of keeping the core free of =.emacs.d=-specific assumptions from the start.
+
+* Implementation phases
+
+** Phase 1 — Data bridge
+A Python script (gkeepapi) that authenticates with the stored master token and prints notes as JSON (id, title, text, labels, pinned, color, archived, updated) on stdout, plus a Takeout-import path producing the same JSON shape. Standalone and testable from the shell with a fixture; no Emacs yet. Tree stays working (new files only).
+
+** Phase 2 — Org renderer + refresh
+=modules/google-keep-config.el=: run the bridge as a subprocess, parse the JSON, and write the org page (heading + body + property drawer per note, labels as tags, pinned-first). =cj/keep-refresh= regenerates it; auth via =auth-source=. A header line marks the file read-only-view. Degrades to a =display-warning= when the bridge, Python, gkeepapi, or token is missing — never errors at load.
+
+** Phase 3 — Access UX + un-orphan
+Keybindings (a Keep prefix), a dashboard entry, and the =(require 'google-keep-config)= in =init.el=. Optional: a dedicated read-only major mode for the buffer.
+
+(vNext phases — not specced here, logged to todo.org: read-write create/edit; the org-capture-style popup; list/checkbox rendering; extract the core to a package.)
+
+* Acceptance criteria
+- [ ] =cj/keep-refresh= fetches the current Keep notes and writes them to the org page, one header per note with title/body/labels/metadata.
+- [ ] Pinned notes sort to the top; labels render as org tags; Keep id/color/pinned/archived/updated land in a property drawer.
+- [ ] The master token is read from =authinfo.gpg= via =auth-source=; no secret is hardcoded.
+- [ ] A missing bridge / Python / gkeepapi / token produces a clear =display-warning=, not a load error or a crash.
+- [ ] The Takeout-import fallback produces the same org page from a Takeout dump with no auth.
+- [ ] =make validate-modules= + launch smoke clean with =google-keep-config= required.
+
+* Readiness dimensions
+- Data model & ownership: Keep is the source of truth; the org page is a generated read-only view (v1). Each note maps to one org header; the bridge JSON is the contract between Python and elisp.
+- Errors, empty states & failure: auth failure, a broken gkeepapi, missing Python/token, or zero notes each degrade to a warning + an empty-or-stale page, never a crash. The unofficial API breaking is expected, not exceptional.
+- Security & privacy: the master token lives in =authinfo.gpg= (gpg-encrypted), read via auth-source; note content lands in a local org file the user already trusts for org data. No secret in the repo. The token grants broad Google access — documented as a risk.
+- Observability: the warning path names which piece is missing (Python / gkeepapi / token / bridge). The generated page's header shows the last refresh time.
+- Performance & scale: one subprocess per manual refresh over N notes (tens to low hundreds); trivial. No background polling in v1.
+- Reuse & lost opportunities: reuses org (rendering, search, agenda, links), auth-source (credentials), and the subprocess pattern. gkeepapi supplies the API client, so no endpoint code is written here.
+- Architecture fit & weak points: three layers (Python bridge / elisp renderer / UX glue) with the fragile API isolated in layer 1. Weak point: gkeepapi maintenance and Google auth churn — mitigated by isolation, graceful degradation, and the Takeout fallback.
+- Config surface: a Keep org-file path, the auth-source host entry, and a keybinding prefix. No tuning knobs in v1.
+- Documentation plan: a setup note (one-time master-token retrieval, =pip install gkeepapi=, the authinfo entry) and the module commentary. No user-migration doc (personal config).
+- Dev tooling: the bridge is shell-testable with a JSON fixture; the renderer gets ERT over the JSON-to-org transform; =make validate-modules= + launch smoke for the module.
+- Rollout, compatibility & rollback: additive — a new module + a require. Rollback = drop the require and delete the org page. No existing behavior changes.
+- External APIs & deps: gkeepapi (PyPI) and the unofficial Google Keep mobile endpoint it wraps — the single load-bearing external dependency and the central risk. Python 3 on PATH. A Google master token.
+
+* Risks, Rabbit Holes, and Drawbacks
+- The central risk is gkeepapi breaking when Google changes auth or the private endpoint. It has a history of auth churn. Mitigations: isolate it in the bridge, degrade to a warning, keep the Takeout-import fallback working, and never block Emacs load on it.
+- Credential risk: the master token grants broad account access. Keep it in =authinfo.gpg=, never the repo; document revocation.
+- Scope creep: the pull toward full bidirectional sync, list fidelity, and real-time. v1 is a read-only org view on purpose; write-back and richness are vNext, gated behind the org-page landing first.
+
+* Review and iteration history
+** 2026-06-24 Wed @ 22:40:00 -0400 — Claude — author
+- What: initial draft.
+- Why: Craig asked to spec the google-keep in-editor integration before building. It spans a fragile external API, an auth-source credential, a Python/elisp bridge, and a module-to-package trajectory, with real trade-offs on shape (org page vs popup), direction (read vs read-write), and data path (gkeepapi vs Takeout vs MCP) — worth pinning before code.
+- Artifacts: docs/specs/google-keep-emacs-integration-spec.org; the todo.org google-keep task (to be cross-linked at hand-off).
diff --git a/init.el b/init.el
index 16f019839..589e46591 100644
--- a/init.el
+++ b/init.el
@@ -120,6 +120,7 @@
(require 'prog-webdev)
(require 'prog-json)
(require 'prog-yaml)
+(require 'ledger-config) ;; plain-text accounting (ledger format)
;; ---------------------------------- Org Mode ---------------------------------
diff --git a/modules/dirvish-config.el b/modules/dirvish-config.el
index f33e8cf74..c4c5f1aae 100644
--- a/modules/dirvish-config.el
+++ b/modules/dirvish-config.el
@@ -400,18 +400,19 @@ regardless of what file or subdirectory the point is on."
"Return the (PROGRAM PRE-FILE-ARG...) list for setting wallpaper under ENV.
ENV is a display-server symbol: `x11' picks feh with --bg-fill, `wayland'
-picks swww with the img subcommand. Any other value returns nil so the
-caller can surface an \"unknown display server\" error.
+picks the `set-wallpaper' script (on PATH from dotfiles; it wraps the awww
+backend and persists the choice to waypaper's config). Any other value
+returns nil so the caller can surface an \"unknown display server\" error.
Pure helper used by `cj/set-wallpaper'."
(pcase env
('x11 '("feh" "--bg-fill"))
- ('wayland '("swww" "img"))
+ ('wayland '("set-wallpaper"))
(_ nil)))
(defun cj/set-wallpaper ()
"Set the image at point as the desktop wallpaper.
-Uses feh on X11, swww on Wayland."
+Uses feh on X11, the `set-wallpaper' script on Wayland."
(interactive)
(let* ((raw (dired-file-name-at-point))
(file (and raw (expand-file-name raw)))
diff --git a/modules/ledger-config.el b/modules/ledger-config.el
index 5b2712b57..018601043 100644
--- a/modules/ledger-config.el
+++ b/modules/ledger-config.el
@@ -2,15 +2,25 @@
;; author Craig Jennings <c@cjennings.net>
;;; Commentary:
+;; Editing support for ledger-format plain-text accounting files: ledger-mode,
+;; flycheck linting, company completion, clean-on-save, and a small report set.
+;; The reports and reconcile shell out to the `ledger' CLI; a load-time check
+;; warns when it is missing rather than letting a report fail cryptically.
;;; Code:
;; ------------------------------- Declarations --------------------------------
(declare-function ledger-mode-clean-buffer "ledger-mode")
+(declare-function cj/executable-find-or-warn "system-lib")
(defvar ledger-mode-map)
(defvar company-backends)
+(defcustom cj/ledger-clean-on-save t
+ "When non-nil, tidy a ledger buffer with `ledger-mode-clean-buffer' before save."
+ :type 'boolean
+ :group 'ledger)
+
;; -------------------------------- Ledger Mode --------------------------------
;; edit files in ledger format
@@ -19,36 +29,38 @@
"\\.ledger\\'"
"\\.journal\\'")
:preface
- (defun cj/ledger-save ()
- "Automatically clean the ledger buffer at each save."
- (interactive)
- (save-excursion
- (when (buffer-modified-p)
- (with-demoted-errors "Error cleaning ledger buffer: %S"
- (ledger-mode-clean-buffer))
- (save-buffer))))
- :bind
- (:map ledger-mode-map
- ("C-x C-s" . cj/ledger-save))
+ (defun cj/ledger--clean-before-save ()
+ "Tidy the ledger buffer before save when `cj/ledger-clean-on-save' is set.
+Errors are demoted so a malformed buffer still saves."
+ (when cj/ledger-clean-on-save
+ (with-demoted-errors "Error cleaning ledger buffer: %S"
+ (ledger-mode-clean-buffer))))
+ (defun cj/ledger--enable-clean-on-save ()
+ "Install the clean-on-save hook buffer-locally so it fires on every save path."
+ (add-hook 'before-save-hook #'cj/ledger--clean-before-save nil t))
+ :hook (ledger-mode . cj/ledger--enable-clean-on-save)
:custom
(ledger-clear-whole-transactions t)
(ledger-reconcile-default-commodity "$")
(ledger-report-use-header-line nil)
+ (ledger-highlight-xact-under-point t)
(ledger-reports
'(("bal" "%(binary) --strict -f %(ledger-file) bal")
("bal this month" "%(binary) --strict -f %(ledger-file) bal -p %(month) -S amount")
("bal this year" "%(binary) --strict -f %(ledger-file) bal -p 'this year'")
("net worth" "%(binary) --strict -f %(ledger-file) bal Assets Liabilities")
- ("account" "%(binary) --strict -f %(ledger-file) reg %(account)"))))
+ ("account" "%(binary) --strict -f %(ledger-file) reg %(account)")))
+ :config
+ (cj/executable-find-or-warn "ledger" 'ledger-mode))
;; ------------------------------ Flycheck Ledger ------------------------------
-;; syntax and unbalanced transaction linting
+;; syntax and unbalanced-transaction linting
(use-package flycheck-ledger
:after ledger-mode)
;; ------------------------------- Company Ledger ------------------------------
-;; autocompletion for ledger
+;; account/payee autocompletion for ledger
(use-package company-ledger
:after (company ledger-mode)
diff --git a/tests/test-dirvish-config-wallpaper-program.el b/tests/test-dirvish-config-wallpaper-program.el
index 556c13100..41d2ad8b2 100644
--- a/tests/test-dirvish-config-wallpaper-program.el
+++ b/tests/test-dirvish-config-wallpaper-program.el
@@ -28,9 +28,9 @@
'("feh" "--bg-fill"))))
(ert-deftest test-cj--wallpaper-program-for-wayland ()
- "Normal: wayland dispatches to swww with the img subcommand."
+ "Normal: wayland dispatches to the set-wallpaper script (awww backend + waypaper persist)."
(should (equal (cj/--wallpaper-program-for 'wayland)
- '("swww" "img"))))
+ '("set-wallpaper"))))
(ert-deftest test-cj--wallpaper-program-for-unknown-returns-nil ()
"Boundary: an unknown environment returns nil so the wrapper can fall back."
diff --git a/todo.org b/todo.org
index a0e022c99..cf3ce63ab 100644
--- a/todo.org
+++ b/todo.org
@@ -55,6 +55,14 @@ 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
+** TODO [#B] first f12 doesn't toggle the term window :bug:solo:
+The first =f12= of a session flashes the terminal open and immediately closes it, as if the toggle fired on then off; a second =f12= then works. Seen across two separate sessions. From the roam inbox 2026-06-24.
+** TODO [#C] org-capture popup leaks f12 / f10 / f11 / ai-term keys :bug:
+While the org-capture popup is open, the global F-keys (the =f12= term, =f10= / =f11=, the ai-term family) still fire and pop a terminal over the capture. Disable those keys for the duration of the capture popup if there's a clean way. Research first and report; if it's too invasive, defer or cancel rather than force it. From the roam inbox 2026-06-24.
+** TODO [#C] dirvish image previews missing in the pictures dir :bug:
+Dirvish (the =super + f= file manager) shows no image preview when browsing =~/Pictures=, so picking a wallpaper is blind. The preview pane is empty for image files where a thumbnail should render. Want image rendering in the dirvish preview pane for image directories. From the roam inbox 2026-06-24.
+** TODO [#B] calendar-sync: a declined single occurrence keeps :STATUS: accepted :bug:solo:
+A recurring event declined for just one occurrence still syncs out with =:STATUS: accepted=, so chime faithfully shows it. Root cause (diagnosed by a chime session, handed off 2026-06-24): =calendar-sync--apply-single-exception= (=calendar-sync.el:525=) merges the override's =:attendees= but never re-derives =:status= from them, so the occurrence keeps the series master's accepted status; =calendar-sync--filter-declined= (=calendar-sync.el:765=) keys off =:status= and so doesn't drop it. Fix (TDD): in =apply-single-exception=, when overriding =:attendees=, re-derive =:status= via =calendar-sync--find-user-status= against =calendar-sync-user-emails=. Test cases: (1) a declined single occurrence of an accepted series gets =:status= "declined"; (2) an override with no attendee block leaves the inherited status intact; (3) an accepted override stays accepted. Model on =tests/test-integration-calendar-sync-recurrence-exceptions.el=. Verify: a re-sync drops the 2026-06-24 Arusyak occurrence from =gcal.org= / =pcal.org= (or marks it =:STATUS: declined=).
** PROJECT [#A] Manual testing and validation
Exercised once the phases above land.
*** TODO theme-studio preview-locate discoverability read
@@ -378,10 +386,14 @@ What we're verifying: in dirvish, d now duplicates the file at point (delete-to-
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.
** PROJECT [#A] Theme-Studio Open Work
Parent grouping the open theme-studio / theming issues; close each child independently.
+*** TODO [#C] ansi-color dropdown plain, reuse info in the hover :feature:studio:
+Drop the parenthetical from the "ansi color" assignment-view dropdown label so it reads plain, and move the explanation to the hover instead: name the packages that reuse these colors (vterm / eshell / compilation / ghostel) and verify they actually do before stating it. From the roam inbox 2026-06-24.
+*** 2026-06-24 Wed @ 21:53:39 -0400 Editable face-list color grouping cancelled — subsumed by the gallery
+The roam-inbox ask was to default-group the editable nerd-icons face table by color family. Craig's call (2026-06-24): the gallery preview already clusters the icons by hue, which covers the underlying "see colors grouped" need, so grouping the 34-row editable table too isn't worth it. Cancelled.
*** 2026-06-24 Wed @ 18:09:26 -0400 theme-studio tier-1 simplifications landed
Behavior-preserving simplifications from the four-agent refactor/simplify assessment, all test-verified (full suite green). Landed: syncMockHeight + syncPkgHeight merged into syncPaneHeight(tableId, paneId); the dead generatorHues "manual" branch deleted (identical to fallback); locateInfoLine removed (fn + export + test, orphaned this session); the redundant pkgbody guard dropped (buildPkgTable self-guards); displayHex/displayName closures inlined; paintUI now calls worstCellHtml; generate.py's two nerd-icons loaders share _load_nerd_icons_artifact (sentinel keeps the null-file edge exact); face_coverage.classify rewritten with named locals (with a new characterization test). Two agent findings were wrong and skipped on verification: LOCATE_REG is live (read by previewSpan), and normalizePaletteEntryCore doesn't exist (hallucinated). Skipped on judgment: a RELEASED_BOX constant (mutable-dict aliasing hazard, only ~10 sites) and inlining apply_hover_box_default (its why-docstring earns the named function). Open for Craig: previewFaceAttrs (app-core.js) is test-only with a stale "the gate calls it" docstring — confirm delete vs keep.
-*** TODO [#D] theme-studio app.js module split (tier 2) :refactor:studio:
-Optional structural change, navigability-only. The highest-value extraction landed (see below): controls.js. The remaining clusters (picker, locate, io, tables) are further optional splits with diminishing returns — do them only if scrolling app.js is still real friction, following the same token-at-position pattern controls.js proved.
+*** 2026-06-24 Wed @ 21:53:39 -0400 app.js split — controls.js extracted, remaining splits declined
+The highest-value extraction landed (controls.js, see below). Craig's call (2026-06-24): stop there — the remaining clusters (picker, locate, io, tables) are diminishing navigability gain for more churn, so they're declined. The token-at-position pattern is proven and documented if any one is ever wanted.
**** 2026-06-24 Wed @ 19:16:47 -0400 Extracted the control factories to controls.js
Cut the contiguous dropdown / detail-editor / expander cluster (the custom color dropdown state + closeColorDropdown + mkColorDropdown through mkExpander, 205 lines) from app.js into controls.js, spliced back at a CONTROLS_J token via generate.py. app.js dropped 927 to 721 lines. The token sits at the exact extraction point, so the assembled page is byte-identical (just relocated source) — full suite green with no gate changes. mkBoxControl (a lone factory elsewhere in app.js) stayed put; it can join controls.js later.
*** TODO [#A] theme-studio: consistent assignment-view table columns :feature:studio:next:
@@ -483,8 +495,8 @@ Package faces model =inherit= explicitly, but UI faces currently expose only fg/
The calibre package preview has no elements to theme in the search list, and coloring switches to the string color on mismatched quotes. Investigate, then record a diagnosis and solution in this task before fixing. From the roam inbox 2026-06-15.
*** TODO [#C] theme-studio: break org-mode preview into grouped subsections :feature:studio:
Rather than cramming all org-mode preview into one pane, split into groups so each element is shown in a common, context-rich environment. From the roam inbox.
-*** TODO [#C] theme-studio: converter drops :inherit on UI faces :bug:studio:
-build-theme.el's UI tier passes inherit=nil to --attrs, so a UI face that relies only on its inherit field (no explicit fg/bg) loses the inheritance in the generated theme, while the studio preview shows the inherited color via resolveUiAttr. The package tier already emits :inherit; the UI tier should match. Surfaced while diagnosing why mode-line-inactive looked off in Emacs versus the preview (that case had explicit colors and turned out to be a stale deploy, but the inherit gap is real for any inherit-only UI face).
+*** 2026-06-24 Wed @ 22:30:00 -0400 converter :inherit on UI faces — verified already correct, not reproducible
+The reported bug (build-theme.el's UI tier dropping :inherit for inherit-only UI faces) does not reproduce in the current code. uiFaceBlank carries an inherit field, exportObj dumps the full UIMAP (inherit included), and build-theme/--attrs reads it. Direct test: a theme.json with ui face {inherit: mode-line, fg: null, bg: null} fed to build-theme/--ui-face-specs emits ((mode-line-inactive ((t (:inherit mode-line))))) — the :inherit survives. Closed as already-fixed / stale.
*** TODO [#C] theme-studio: elfeed ignores theme assignments :studio:studio:
The preview shows theme colors, but elfeed itself renders all-white with no variation. Note: this may be the shr-rendered entry/article view (elfeed-show), where color often comes from the document rather than the theme — confirm whether the symptom is in the search list or the article view. From the roam inbox.
*** VERIFY [#C] theme-studio face-consistency check :feature:studio:next:
@@ -3746,8 +3758,9 @@ These may override useful defaults - review and pick better bindings:
:END:
Display slack.el message and thread buffers in a dedicated popup window (side or bottom) and reuse that one window instead of spawning a new window per buffer. Likely a =display-buffer-alist= rule (or popper integration) in =modules/slack-config.el=.
-** TODO [#D] Evaluate google-keep Emacs package :quick:
-From the roam inbox. Look at the google-keep Emacs package — worth adding for in-editor Keep, or does the existing google-keep MCP cover it? Triage / shortlist, not a commitment.
+** TODO [#C] google-keep in-editor integration — build, module-to-package :feature:
+Build a native Keep integration: an org page of notes (each note an org header), read-only in v1, a gkeepapi Python subprocess bridge for data (the MCP is agent-only, not callable from elisp), auth via authinfo.gpg, eventually extracted to a standalone package. vNext: read-write, the org-capture-style popup, list rendering.
+Spec: [[file:docs/specs/google-keep-emacs-integration-spec.org][google-keep-emacs-integration-spec.org]]. Five decisions are drafted with recommended calls and await Craig's confirmation (shape, direction, data path, auth, structure); the spec stays draft until they resolve.
** TODO [#D] Theme Studio nerd-icons vNext follow-ups :feature:
Deferred from [[file:docs/specs/theme-studio-nerd-icons-colors-spec.org][theme-studio-nerd-icons-colors-spec.org]]: extend the legend to
buffer-mode and command/symbol categories if the file set proves insufficient;