Skip to content

Uppercase mailbox action keys, universal undo, quick peek - #30

Closed
RooseveltAdvisors wants to merge 17 commits into
ssp-data:mainfrom
RooseveltAdvisors:fm/fm-neomd-uppercase-mailbox-actions-universa-23
Closed

RooseveltAdvisors wants to merge 17 commits into
ssp-data:mainfrom
RooseveltAdvisors:fm/fm-neomd-uppercase-mailbox-actions-universa-23

Conversation

@RooseveltAdvisors

Copy link
Copy Markdown

Summary

Three inbox TUI changes per the captain's intent:

  1. Uppercase mailbox action keys - I approve/screen-in, O screen-out, P papertrail, B work, F feed, $ spam. The lowercase i/p/b forms no longer fire. The four core keys (e archive, h remind, s compose, ; snippets) stay lowercase by contract, as do o open, t thread, and n read toggle. docs/keys.md, the bottom hint bar, and the ? overlay are all updated from the same source of truth (internal/ui/keys.go, make docs).

  2. Universal undo - u now reverses the most recent action of any journaled type:

    • Screener moves (F/I/O/P/$): previously batchScreenerCmd built the undo data but silently dropped it on success - the bug behind "u does not un-move after F/I". Fixed.
    • Archive/delete/M-chord/move-picker moves: already journaled, now via the unified journal.
    • Read state: n toggle and ctrl+n mark-all-read journal the old \Seen state, so u toggles back.
    • Journal entries are LIFO, capped at maxUndoStack, and mixed move+flag actions reverse in one command. Server-side undo failures surface in the status line (never silent). Screener classification lists are intentionally kept on undo (mail moves back, sender lists do not); permanent X delete is never journaled since it cannot be reversed.
  3. Quick peek - leader+space (<space><space>) toggles a preview pane (From/To/Subject + body) of the highlighted email without leaving the inbox list. esc or the chord again closes it; the cursor never moves; j/k re-targets the preview to the new row; the pane is exactly peekPaneHeight lines so the status bar never overflows; the body is fetched with BODY.PEEK so peeking never marks mail read. Works in read-only profiles (it mutates nothing).

Tests

  • TestLowercaseMailboxActionsAreDead - lowercase i/p/b are dead keys
  • TestEmailBindingMap - updated to the uppercase contract; read-only blocking extended to I/O/P/B
  • TestUndoJournalScreenerMove, TestUndoJournalReadToggle, TestUndoJournalMarkAllRead, TestUndoJournalMostRecentFirst, TestUndoJournalCap, TestUndoNothingToDo, TestUndoRefusesInReadOnly
  • TestPeekOpensAndClosesWithStableSelection, TestPeekStaleResultDropped, TestPeekResizesList
  • Full go test ./... and go test ./... -run Hardening pass; make docs regenerated.

Docs: docs/keys.md, README.md screener walkthrough, generated keybindings docs, AGENTS.md keyboard contract updated.

RooseveltAdvisors and others added 17 commits September 1, 2026 10:16
…instant actions (#5)

Drive the client from the home row, and make every action land immediately.

Bindings (inbox + reader, guarded behind the existing input-mode returns so
they never fire inside the filter, `:` command line, IMAP search, or the
reminder prompt):

  e  archive / mark done      h  remind me
  s  start an email           ;  snippets / templates picker

Case reconciliation — email actions are now lowercase (i/o/p/b/t/v replace
I/O/P/B/T/V, uppercase kept as aliases, as are A and c). Six keys stay
uppercase because their lowercase letter is load-bearing: F (f=forward),
S (s=compose), U (u=page up), X (x=trash), N (n=toggle read), R (r=reply).
Two visible moves: `h` no longer exits the reader (q/esc do), and the reader's
read-only $EDITOR view moved from `e` to `<space>e`.

Instant actions — archive/delete/screen/move/remind apply to the list on the
keystroke via Model.optimisticAct, reconcile in place on the server ack (no
folder re-fetch, which is what made every keypress feel like a page load), and
roll back visibly on failure. Batches carry an optID so overlapping actions
never consume each other's rollback snapshot.

Infinite scroll — reaching the bottom appends the next page via the new
imap.Client.FetchHeadersBefore UID cursor; the list cursor stays put, an empty
page latches the folder exhausted, and cross-folder views are not paged.

Snippets — `;` reads <config dir>/snippets/*.md at open time; an optional
leading `Subject:` line sets the subject and the rest is staged into the
$EDITOR buffer through the existing mailto-body path.

Tests: TestEmailBindingMap, TestEmailBindingsGuardedInsideInputFields,
TestReaderArchiveAndRemindKeys, TestOptimisticArchiveIsInstant,
TestOptimisticArchiveRollsBackVisiblyOnFailure,
TestOverlappingOptimisticBatchesRollBackIndependently,
TestOptimisticRemindMovesRowsAtOnce, TestScrollToBottomTriggersNextPage,
TestNextPageAppendsAndKeepsScrollPosition, TestScrollLatchSurvivesTheKeyHandler,
TestScrollDoesNotPageAdHocViews, TestEmptyNextPageMarksFolderExhausted,
TestNextPageForAnotherFolderIsDropped, TestFullFolderLoadResetsPagingState,
TestSnippetParseSplitsSubjectAndBody, TestSnippetPickerComposesPrefilled.


Claude-Session: https://claude.ai/code/session_014hpboaND66f8HSC1LG43Rn

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
…erminal (#11)

The reader yank menu (`y` then `m`) put nothing on the clipboard. The shared
helper only shelled out to a local clipboard tool, which cannot work when neomd
runs over ssh: on the gpu box `xclip` is on PATH but DISPLAY is empty, so it was
invoked and failed with a bare `exit status 1`. Even a successful local tool
would have set the remote machine's clipboard rather than the terminal the user
pastes from.

copyToClipboard now writes an OSC 52 sequence to the terminal first and only
then falls back to a local tool; either succeeding is a success. Inside tmux the
sequence is emitted twice — plain, and DCS-passthrough wrapped with every ESC
doubled — so it lands under either `set-clipboard on` or `allow-passthrough on`.
A local tool whose display variable is unset is now skipped rather than run and
failed, and a tool that does fail reports its stderr instead of an opaque exit
status.

The helper moved from internal/ui/contacts_picker.go to internal/ui/clipboard.go
alongside its tests, so the contacts picker's `y`/`Y` is fixed by the same change.

Verified on a real pty: copyToClipboard emits ESC ]52;c;<base64> BEL decoding to
neomd://mid/%3Cabc@example.com%3E and exits 0 on this headless host, where the
old path returned exit status 1.


Claude-Session: https://claude.ai/code/session_014ojr1K5YFUvGwx5KNp4cLY

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Add a read-only CLI path for resolving copied neomd:// links and bare Message-IDs across configured IMAP accounts. Preserve the widget UID reader while exposing text, JSON, raw, and stdin batch output.
Share one calendar-aware parser between reminders and send-later, and restore the visible cursor after local filtering so actions target the highlighted result.
- Mailbox action keys are uppercase in the inbox: I approve, O screen-out,
  P papertrail, B work (F feed and $ spam unchanged). Lowercase i/p/b no
  longer fire; core e/h/s/; keys are untouched.
- u is now a universal undo backed by an action journal (undoAction):
  batchScreenerCmd previously dropped its undo data on success, so F/I/P/O
  moves could not be reversed. The journal records moves AND \Seen flag
  changes (n toggle, ctrl+n mark-all-read), pops LIFO, and reports
  server-side undo failures in the status line.
- Leader+space toggles a quick-peek pane (From/To/Subject/body) for the
  highlighted email without leaving the inbox list; esc or the chord
  closes it, the cursor stays put, j/k re-targets the preview, and the
  body is fetched with BODY.PEEK so peeking never marks mail read.

Tests: TestLowercaseMailboxActionsAreDead, TestUndoJournal*,
TestPeek*, docs/keys.md + hint bar + ? overlay kept in sync via
TestDocumentedKeyBindings.
@RooseveltAdvisors

Copy link
Copy Markdown
Author

Opened against the wrong repository by an automated agent; closing. The identical PR lives at RooseveltAdvisors/neomd.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants