Skip to content

feat(tui): add read-only operator shell - #23

Merged
BjornMelin merged 6 commits into
mainfrom
feat/tui-readonly-shell
May 8, 2026
Merged

feat(tui): add read-only operator shell#23
BjornMelin merged 6 commits into
mainfrom
feat/tui-readonly-shell

Conversation

@BjornMelin

@BjornMelin BjornMelin commented May 7, 2026

Copy link
Copy Markdown
Owner

Summary

  • Add mailroom tui, a Ratatui/Crossterm read-only operator shell over existing Mailroom services.
  • Render Dashboard, Search, Workflows, Automation, and Help views without introducing new Gmail/store/workflow ownership.
  • Document the TUI boundary in ADR/runbook/README/architecture/roadmap/AGENTS guidance.

Safety

  • The TUI is read-only in this PR.
  • It does not send drafts, archive/label/trash mail, apply automation snapshots, create automation run snapshots, fetch/export attachments, or edit .mailroom/automation.toml.
  • Search uses the existing local SQLite-backed mailbox search service.

Verification

  • cargo fmt --check
  • cargo clippy --all-targets --all-features -- -D warnings
  • cargo test
  • cargo run -- paths --json
  • cargo run -- doctor --json
  • cargo run -- tui --help
  • cargo run -- roadmap
  • cargo run -- tui in a PTY, then q
  • cargo run -- tui --search definitely-no-such-mailroom-test-token in a PTY, then q

Follow-up Issues

Copilot AI review requested due to automatic review settings May 7, 2026 19:44
@qodo-code-review

Copy link
Copy Markdown
ⓘ You've reached your Qodo monthly free-tier limit. Reviews pause until next month — upgrade your plan to continue now, or link your paid account if you already have one.

@coderabbitai

coderabbitai Bot commented May 7, 2026

Copy link
Copy Markdown

Review Change Stack

Walkthrough

This PR adds a Ratatui/Crossterm read-only terminal operator shell ("mailroom tui") with a new CLI subcommand and TuiArgs (--search), handler wiring (handle_tui_command), lib dispatch and metadata, and a full TUI implementation (run entrypoint, terminal guard, Snapshot loading, Dashboard/Search/Workflows/Automation/Help views, key handling, rendering, and tests). It exposes/readjusts read-only service entrypoints (search_read_only, list_workflows_read_only, rollout_read_only), refactors rollout validation, updates exports for AutomationRolloutReport, adds Ratatui/Crossterm dependencies, and includes decision/ops/architecture/README/roadmap documentation defining the TUI safety contract and ownership constraints.

Possibly related issues

Possibly related PRs

  • BjornMelin/mailroom#21: Related at the automation surface — this PR and #21 both touch AutomationRolloutReport and rollout logic used by the TUI.
  • BjornMelin/mailroom#15: Related to workflows query/re-exports — both PRs add/adjust list_workflows_read_only and workflow query plumbing.
  • BjornMelin/mailroom#19: Related to handler/CLI dispatch refactoring — this PR integrates a tui handler into the same handlers/lib dispatch surface.
🚥 Pre-merge checks | ✅ 8 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 19.30% which is insufficient. The required threshold is 50.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (8 passed)
Check name Status Explanation
Title check ✅ Passed The pull request title follows conventional commits naming conventions with a 'feat' type prefix, a scoped category '(tui)', and a clear description of the main change.
Description check ✅ Passed The pull request description provides a comprehensive overview of the TUI implementation, safety guarantees, verification steps, and follow-up issues, all directly related to the changeset.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
React + Next.Js Best Practices ✅ Passed Not applicable. PR adds Rust TUI using Ratatui/Crossterm, not React/Next.js. Check scope covers React, TypeScript, Next.js, client-side patterns—none present in this Rust backend project.
Docstring & Research Standards ✅ Passed Not applicable. Check targets TypeScript/JavaScript projects with Biome linter and TSDoc. This PR is Rust-based (Cargo.toml, .rs files) with no TS/JS files modified.
Web Interface Guidelines ✅ Passed Check is not applicable. PR adds a terminal UI (Ratatui/Crossterm), not a web interface. Web-specific rules like WAI-ARIA, CSS, HTML, and responsive design don't apply to terminal UIs.
Google Python Style Compliance ✅ Passed Check enforces Google Python Style Guide compliance. PR contains only Rust (.rs), Markdown (.md), and Cargo.toml files—no Python files. Check not applicable to this Rust project.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/tui-readonly-shell
✨ Simplify code
  • Create PR with simplified code
  • Commit simplified code in branch feat/tui-readonly-shell

Comment @coderabbitai help to get the list of available commands and usage tips.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: bc99c13684

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/tui.rs Outdated

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a new mailroom tui subcommand that provides a read-only Ratatui/Crossterm operator shell over existing Mailroom services/reports, and documents the intended “thin UI layer” boundary across the repo.

Changes:

  • Introduces a new TUI implementation (src/tui.rs) with Dashboard/Search/Workflows/Automation/Help views backed by existing services.
  • Wires the new tui subcommand through CLI parsing, handlers, and command metadata.
  • Adds dependencies (ratatui, crossterm) and updates architecture/runbook/ADR/roadmap documentation to define the read-only safety contract.

Reviewed changes

Copilot reviewed 15 out of 16 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
src/tui.rs New Ratatui-based read-only TUI shell and view rendering over existing reports/search.
src/lib.rs Adds tui module and routes Commands::Tui to the new handler; extends roadmap output.
src/handlers/tui.rs New handler that resolves config and launches the TUI.
src/handlers/mod.rs Registers and re-exports the new TUI handler.
src/cli.rs Adds tui subcommand and TuiArgs (--search).
src/automation/mod.rs Re-exports AutomationRolloutReport for in-crate TUI consumption.
README.md Documents the read-only TUI as an available operator surface and adds usage examples.
docs/roadmap/v1-search-triage-draft-queue.md Updates milestone narrative to include the read-only TUI foundation.
docs/README.md Adds links to the new ADR and TUI operator runbook.
docs/operations/tui-operator-shell.md New runbook for using mailroom tui, including key bindings and safety contract.
docs/decisions/0008-read-only-tui-foundation.md New ADR defining scope/boundaries for the read-only TUI foundation.
docs/architecture/system-overview.md Documents the TUI layer as a thin, read-only shell over existing services.
docs/architecture/repository-layout.md Adds src/tui.rs to the layout and reiterates “no duplicate ownership” guidance.
Cargo.toml Adds Ratatui and Crossterm dependencies (Ratatui with default features disabled).
Cargo.lock Locks new transitive dependencies introduced by Ratatui/Crossterm.
AGENTS.md Adds explicit contributor guidance to keep TUI work thin and non-owning.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/tui.rs Outdated
Comment thread src/tui.rs

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/handlers/tui.rs`:
- Around line 9-10: Replace the inline blocking call to config::resolve with a
spawn_blocking task: run config::resolve(paths) inside
tokio::task::spawn_blocking(move || config::resolve(paths)), await the join
handle, and propagate the inner Result so errors still use the original ?-style
flow; do not make the spawn_blocking work abortable (don’t wrap it in a
cancellable select), then pass the resolved config_report to tui::run(paths,
config_report, args.search).await as before.

In `@src/tui.rs`:
- Around line 727-765: Add unit tests that exercise the new failure branches
introduced in this module: create Snapshot instances with each task (doctor,
verification, workflows, automation) set to Err variants representing task
failures and assert TuiApp renders the appropriate error/report view or sets
fields (e.g., verify that TuiApp::new(...).view or rendered error text contains
the expected failure mapping); also add tests for search/report rendering
failures by seeding TuiApp with a failing search state (use TuiApp, View, and
any search/report rendering helpers) and assert search-related error output and
search_editing flags behave as expected. Ensure each test targets a single
failure class and checks the contract (view selection, flags, or error text) for
that branch.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 973c6df3-0982-4a6b-816d-ed02c6629eee

📥 Commits

Reviewing files that changed from the base of the PR and between 3a3b0a0 and bc99c13.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (15)
  • AGENTS.md
  • Cargo.toml
  • README.md
  • docs/README.md
  • docs/architecture/repository-layout.md
  • docs/architecture/system-overview.md
  • docs/decisions/0008-read-only-tui-foundation.md
  • docs/operations/tui-operator-shell.md
  • docs/roadmap/v1-search-triage-draft-queue.md
  • src/automation/mod.rs
  • src/cli.rs
  • src/handlers/mod.rs
  • src/handlers/tui.rs
  • src/lib.rs
  • src/tui.rs
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: Agent
  • GitHub Check: rust
🧰 Additional context used
📓 Path-based instructions (4)
**/*.rs

📄 CodeRabbit inference engine (AGENTS.md)

Keep the Rust surface modular: command parsing, workspace paths, storage, Gmail adapters, and TUI should stay separate

Use anyhow for command dispatch and top-level context in src/lib.rs, and prefer typed thiserror errors in Gmail, workflow, and store layers

Keep error enums local to the layer that owns the failure semantics; do not introduce a repo-wide catch-all error enum

For new CLI JSON contracts, normalize success and failure to one top-level shape: success as { "success": true, "data": ... } and failure as { "success": false, "error": { "code": ..., "message": ..., "kind": ..., "operation": ..., "causes": [...] } }

Do not include debug or backtrace payloads in JSON error contracts; use stderr and Rust backtrace env vars for deep diagnostics

Keep error.code stable and operator-oriented, keep error.kind for deeper subsystem detail, and keep error.causes to an ordered message chain only

Keep blocking SQLite and filesystem work behind tokio::task::spawn_blocking; do not treat running spawn_blocking work as abortable

Add focused error-path tests for every new failure class, and add contract tests for new CLI JSON or exit-code behavior changes

Files:

  • src/automation/mod.rs
  • src/handlers/mod.rs
  • src/cli.rs
  • src/handlers/tui.rs
  • src/lib.rs
  • src/tui.rs
**/*.{js,jsx,ts,tsx,md,txt,json}

📄 CodeRabbit inference engine (Custom checks)

Reject Unicode EM DASH (code point U+2014) in comments and string literals. Use -- instead. Hard FAIL if any newly introduced content includes U+2014.

Files:

  • docs/architecture/repository-layout.md
  • docs/architecture/system-overview.md
  • docs/operations/tui-operator-shell.md
  • docs/README.md
  • docs/roadmap/v1-search-triage-draft-queue.md
  • AGENTS.md
  • docs/decisions/0008-read-only-tui-foundation.md
  • README.md
docs/{operations,workflows}/**

📄 CodeRabbit inference engine (AGENTS.md)

Put operator procedures in docs/operations/ or docs/workflows/

Files:

  • docs/operations/tui-operator-shell.md
docs/decisions/**

📄 CodeRabbit inference engine (AGENTS.md)

Put durable architecture choices in docs/decisions/

Files:

  • docs/decisions/0008-read-only-tui-foundation.md
🧠 Learnings (1)
📓 Common learnings
Learnt from: CR
Repo: BjornMelin/mailroom

Timestamp: 2026-05-07T19:44:52.368Z
Learning: Never commit live mailbox caches, OAuth credentials, tokens, or exported attachments by default
Learnt from: CR
Repo: BjornMelin/mailroom

Timestamp: 2026-05-07T19:44:52.368Z
Learning: Keep all runtime state under `.mailroom/` unless there is a deliberate reason to introduce a new ignored path
Learnt from: CR
Repo: BjornMelin/mailroom

Timestamp: 2026-05-07T19:44:52.368Z
Learning: Prefer one canonical representation for local mail metadata rather than duplicating state across multiple stores
Learnt from: CR
Repo: BjornMelin/mailroom

Timestamp: 2026-05-07T19:44:52.368Z
Learning: Treat delete/archive/label/send actions as deliberate operator actions; keep analysis and mutations clearly separated in the code and docs
Learnt from: CR
Repo: BjornMelin/mailroom

Timestamp: 2026-05-07T19:44:52.368Z
Learning: Prefer repo-local paths over hidden global machine state when reasonable
Learnt from: CR
Repo: BjornMelin/mailroom

Timestamp: 2026-05-07T19:44:52.368Z
Learning: Add new dependencies only when they earn their keep for the current milestone
Learnt from: CR
Repo: BjornMelin/mailroom

Timestamp: 2026-05-07T19:44:52.368Z
Learning: Preserve structured output for agent and shell workflows
Learnt from: CR
Repo: BjornMelin/mailroom

Timestamp: 2026-05-07T19:44:52.368Z
Learning: Prefer read-only audit and verification commands before broad mailbox mutations or high-volume automation apply runs
Learnt from: CR
Repo: BjornMelin/mailroom

Timestamp: 2026-05-07T19:44:52.368Z
Learning: Keep TUI work as a thin shell over existing services and reports; do not duplicate Gmail, workflow, automation, or store ownership in UI code
Learnt from: CR
Repo: BjornMelin/mailroom

Timestamp: 2026-05-07T19:44:52.368Z
Learning: If a plugin-assisted Codex workflow exists for an operation, document it alongside native commands rather than pretending the repo already implements it
Learnt from: CR
Repo: BjornMelin/mailroom

Timestamp: 2026-05-07T19:44:52.368Z
Learning: Preserve existing human-facing error text unless it is misleading, ambiguous, or missing required operator action
Learnt from: CR
Repo: BjornMelin/mailroom

Timestamp: 2026-05-07T19:44:52.368Z
Learning: When adding or changing CLI failures, keep exit codes in a small stable bucket set rather than creating one-off codes per variant
Learnt from: CR
Repo: BjornMelin/mailroom

Timestamp: 2026-05-07T19:44:52.368Z
Learning: Update docs when architecture, storage boundaries, or command surfaces change
Learnt from: CR
Repo: BjornMelin/mailroom

Timestamp: 2026-05-07T19:44:52.368Z
Learning: Keep docs concrete and aligned with the current binary surface
Learnt from: CR
Repo: BjornMelin/mailroom

Timestamp: 2026-05-07T19:44:52.368Z
Learning: Update operator-facing docs in the same change whenever CLI error contracts, JSON envelopes, or exit-code behavior change
🪛 LanguageTool
docs/decisions/0008-read-only-tui-foundation.md

[style] ~74-~74: This word has been used in one of the immediately preceding sentences. Using a synonym could make your text more interesting to read, unless the repetition is intentional.
Context: ...ure set Rejected because Mailroom only needs core widgets and the Crossterm backend ...

(EN_REPEATEDWORDS_NEED)

README.md

[style] ~172-~172: This sentence may be long and difficult for your reader to follow. Consider inserting a period and starting a new sentence here.
Context: ...](docs/operations/tui-operator-shell.md), with the durable design captured in [`docs/decisions/0008-read-only-tui-...

(WITH_THE_SENTENCE)


[style] ~234-~234: Three successive sentences begin with the same word. Consider rewording the sentence or use a thesaurus to find a synonym.
Context: ...tch archive/label runs land cleanly. 4. Expand unsubscribe assistance only after the d...

(ENGLISH_WORD_REPEAT_BEGINNING_RULE)

🔍 Remote MCP Firecrawl

Perfect! Now I have comprehensive information. Let me compile my findings.

Summary of Relevant Context for TUI Implementation Review

Based on comprehensive research using Context7 (Ratatui/Crossterm documentation), Firecrawl (web documentation and best practices), and direct inspection of the PR code, here are the key findings:

Event Handling Implementation

According to official Ratatui documentation, the simplest event handling approach involves "centralized catching, message passing" where events are polled in one place and handled accordingly. The PR's implementation in src/tui.rs uses event::poll(EVENT_POLL_INTERVAL)? with a 200ms fixed interval, which is a standard pattern. However, best practices for crossterm+Ratatui note that message passing to separate event handling threads is more responsive than fixed polling intervals, and the main thread should not directly listen for keystrokes.

Terminal Initialization [::Context7_query-docs::]

According to Ratatui's official documentation, the highest-level entry point for Ratatui applications is ratatui::run, which initializes the terminal, executes a provided closure, and automatically calls restore() upon completion or panic. The PR uses a manual approach instead: ratatui::try_init() with a custom TerminalGuard struct that implements Drop to call ratatui::restore(). This is a valid alternative pattern but adds slightly more code complexity.

Event Polling Pattern

Ratatui's official recipes show a Terminal and EventHandler pattern where event::poll is called with a timeout and event::read() is then called only if poll returns true. The PR correctly implements this pattern: if event::poll(EVENT_POLL_INTERVAL)? { ... event::read()? ... }.

Fixed Interval Polling Considerations

Official Ratatui best practices discussions note that fixed polling intervals like 100ms can introduce a slight latency in responsiveness, though the PR uses 200ms which is slightly more conservative. For a read-only dashboard UI, this is acceptable and provides a balance between responsiveness and CPU efficiency.

Cargo.toml Dependency Configuration [::GitHub_Copilot_get_file_contents::]

The PR correctly adds ratatui (unspecified version—needs verification) and crossterm dependencies. However, I notice the Cargo.toml shows the ratatui and crossterm versions are not explicitly pinned in the shown output, which should be validated for consistency with the codebase's version requirements.

Architecture Patterns

Ratatui development best practices emphasize the MVC (Model-View-Controller) pattern, where state is tracked separately from rendering. The PR's TuiApp struct correctly implements this separation: state (search_input, view, snapshot) is kept separate from rendering functions.

Snapshot Loading with Blocking Tasks

The PR uses tokio::task::spawn_blocking() to run blocking operations (doctor inspection and verification) concurrently while fetching workflows asynchronously. This pattern is appropriate for keeping the event loop responsive, though concurrent execution of potentially long-running diagnostics could benefit from documented timeouts.

Search Integration

The TUI correctly integrates with existing mailbox search (using local SQLite FTS) rather than duplicating Gmail query logic, which aligns with the PR's stated boundary of remaining "a thin shell over existing services."

🔇 Additional comments (14)
src/automation/mod.rs (1)

8-8: Scoped re-export looks good.

AutomationRolloutReport is exposed crate-wide without expanding the public API surface.

Cargo.toml (1)

34-35: TUI dependency wiring is appropriately minimal.

The added crates and ratatui feature flags match the new read-only shell scope.

AGENTS.md (1)

39-39: Good architecture guardrail.

This keeps TUI ownership boundaries explicit and prevents service duplication drift.

src/handlers/mod.rs (1)

12-12: Handler registration is clean and consistent.

Module declaration and re-export follow the existing dispatch structure.

Also applies to: 27-27

src/cli.rs (1)

54-55: CLI surface addition is well-scoped.

tui plus optional --search keeps the command contract simple and composable.

Also applies to: 211-216

src/lib.rs (2)

113-113: Dispatch + operation metadata mapping for TUI is correct.

The new command path is integrated cleanly into runtime execution and error metadata.

Also applies to: 199-202


368-373: Roadmap output update is aligned with the new surface.

Including the TUI operations doc in roadmap output improves operator discoverability.

docs/architecture/repository-layout.md (1)

32-32: Architecture ownership guidance is strong and clear.

This keeps TUI as a shell layer and guards against duplicated domain ownership.

Also applies to: 53-55

docs/README.md (1)

20-31: Docs index update is well aligned with the new TUI surface.

The added decision and operations links keep the docs entrypoint consistent with the new read-only operator shell scope.

docs/architecture/system-overview.md (1)

39-50: TUI boundary is clearly and correctly documented.

The thin-shell wording and cross-links to decision and operations docs make the ownership contract explicit and enforceable.

Also applies to: 124-126

docs/roadmap/v1-search-triage-draft-queue.md (1)

76-88: Roadmap sequencing and TUI exclusions are precise.

This keeps the milestone focused and avoids reopening core ownership areas in the TUI slice.

Also applies to: 105-107

docs/decisions/0008-read-only-tui-foundation.md (1)

21-34: ADR scope and exclusions are strong and durable.

The decision clearly constrains first-slice behavior and prevents TUI ownership drift.

Also applies to: 45-75

docs/operations/tui-operator-shell.md (1)

40-53: Safety contract and troubleshooting coverage are solid.

The runbook clearly documents read-only limits and gives actionable recovery commands.

Also applies to: 63-87

README.md (1)

70-72: README command surface and cross-links are consistent with the new TUI slice.

The new tui examples and decision/runbook references are clear and aligned with the read-only boundary.

Also applies to: 171-174, 217-217, 225-225

Comment thread src/handlers/tui.rs Outdated
Comment thread src/tui.rs
@BjornMelin BjornMelin self-assigned this May 7, 2026
@BjornMelin

Copy link
Copy Markdown
Owner Author

@coderabbitai full review

@coderabbitai

coderabbitai Bot commented May 7, 2026

Copy link
Copy Markdown
✅ Actions performed

Full review triggered.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@README.md`:
- Around line 232-234: The three consecutive list items in README.md (items
starting with "Expand" for the TUI, automation ergonomics, and unsubscribe
assistance) are repetitive; reword items 2–4 to vary sentence openings and
rhythm—e.g., change item 2 to "Turn the TUI from read-only inspection into
explicit workflow..." or "Add explicit workflow, draft, cleanup, and automation
action flows to the TUI with confirmation screens," change item 3 to "Delay
broader automation ergonomics until a few low-surprise micro-batch archive/label
runs complete cleanly," and change item 4 to "Defer unsubscribe assistance until
the deeper sync validates list-header coverage in the local cache." Use any of
these alternate openings to avoid repeating "Expand."

In `@src/mailbox/search.rs`:
- Around line 8-11: The call to store::init inside async fn search blocks the
async runtime; change it to run on a blocking executor (e.g., use
tokio::task::spawn_blocking or reuse the existing init_store_task helper) so
initialization doesn't block the async thread: perform store::init via
spawn_blocking (or call init_store_task) before awaiting
search_read_only(config_report, request), and propagate errors the same way so
search retains its Result signature.

In `@src/tui.rs`:
- Around line 61-67: The code defines TerminalGuard with a Drop impl to call
ratatui::restore() and calls ratatui::try_init(), but try_init() doesn't install
a panic hook so Drop may not run on panic; replace the try_init() call with
ratatui::init() (e.g., change the initialization to let mut terminal =
ratatui::init()?;) so ratatui installs its panic hook and removes the need for
the TerminalGuard; then delete the TerminalGuard struct and its Drop impl
(symbols: TerminalGuard, Drop::drop, ratatui::try_init, ratatui::init,
ratatui::restore, terminal).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 916cbcd1-fbd1-475c-9364-a7d852dcdc69

📥 Commits

Reviewing files that changed from the base of the PR and between 3a3b0a0 and cff1575.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (21)
  • AGENTS.md
  • Cargo.toml
  • README.md
  • docs/README.md
  • docs/architecture/repository-layout.md
  • docs/architecture/system-overview.md
  • docs/decisions/0008-read-only-tui-foundation.md
  • docs/operations/tui-operator-shell.md
  • docs/roadmap/v1-search-triage-draft-queue.md
  • src/automation/mod.rs
  • src/automation/service.rs
  • src/cli.rs
  • src/handlers/mod.rs
  • src/handlers/tui.rs
  • src/lib.rs
  • src/mailbox.rs
  • src/mailbox/search.rs
  • src/tui.rs
  • src/workflows/mod.rs
  • src/workflows/service/mod.rs
  • src/workflows/service/queries.rs
📜 Review details
🧰 Additional context used
📓 Path-based instructions (3)
**/*.rs

📄 CodeRabbit inference engine (AGENTS.md)

Write Rust code; native center of gravity for the mailroom project

Run cargo fmt --check as part of verification before submitting code

Run cargo clippy --all-targets --all-features -- -D warnings as part of verification

Run cargo test as part of verification before submitting code

Files:

  • src/handlers/mod.rs
  • src/mailbox.rs
  • src/cli.rs
  • src/workflows/mod.rs
  • src/workflows/service/mod.rs
  • src/handlers/tui.rs
  • src/lib.rs
  • src/mailbox/search.rs
  • src/workflows/service/queries.rs
  • src/automation/mod.rs
  • src/automation/service.rs
  • src/tui.rs
**/*.{js,jsx,ts,tsx,md,txt,json}

📄 CodeRabbit inference engine (Custom checks)

Reject Unicode EM DASH (code point U+2014) in comments and string literals. Use -- instead. Hard FAIL if any newly introduced content includes U+2014.

Files:

  • AGENTS.md
  • docs/architecture/repository-layout.md
  • docs/operations/tui-operator-shell.md
  • docs/architecture/system-overview.md
  • docs/roadmap/v1-search-triage-draft-queue.md
  • docs/README.md
  • docs/decisions/0008-read-only-tui-foundation.md
  • README.md
src/lib.rs

📄 CodeRabbit inference engine (AGENTS.md)

Use anyhow for command dispatch and top-level context in src/lib.rs as the application boundary

Files:

  • src/lib.rs
🧠 Learnings (1)
📓 Common learnings
Learnt from: CR
Repo: BjornMelin/mailroom

Timestamp: 2026-05-07T23:09:40.403Z
Learning: Keep the Rust surface modular: command parsing, workspace paths, storage, Gmail adapters, and TUI should stay separate
Learnt from: CR
Repo: BjornMelin/mailroom

Timestamp: 2026-05-07T23:09:40.403Z
Learning: Prefer typed `thiserror` errors in Gmail, workflow, and store layers rather than repo-wide catch-all error enums
Learnt from: CR
Repo: BjornMelin/mailroom

Timestamp: 2026-05-07T23:09:40.403Z
Learning: For new CLI JSON contracts, normalize success to `{ "success": true, "data": ... }` and failure to `{ "success": false, "error": { "code": ..., "message": ..., "kind": ..., "operation": ..., "causes": [...] } }`
Learnt from: CR
Repo: BjornMelin/mailroom

Timestamp: 2026-05-07T23:09:40.403Z
Learning: Do not include debug or backtrace payloads in JSON error contracts; use stderr and Rust backtrace env vars for deep diagnostics
Learnt from: CR
Repo: BjornMelin/mailroom

Timestamp: 2026-05-07T23:09:40.403Z
Learning: Keep blocking SQLite and filesystem work behind `tokio::task::spawn_blocking`; do not treat running `spawn_blocking` work as abortable
Learnt from: CR
Repo: BjornMelin/mailroom

Timestamp: 2026-05-07T23:09:40.403Z
Learning: Add focused error-path tests for every new failure class; if CLI JSON or exit-code behavior changes, add contract tests in the same pass
Learnt from: CR
Repo: BjornMelin/mailroom

Timestamp: 2026-05-07T23:09:40.403Z
Learning: Never commit live mailbox caches, OAuth credentials, tokens, or exported attachments by default
Learnt from: CR
Repo: BjornMelin/mailroom

Timestamp: 2026-05-07T23:09:40.403Z
Learning: Keep all runtime state under `.mailroom/` unless there is a deliberate reason to introduce a new ignored path
Learnt from: CR
Repo: BjornMelin/mailroom

Timestamp: 2026-05-07T23:09:40.403Z
Learning: Prefer one canonical representation for local mail metadata rather than duplicating state across multiple stores
Learnt from: CR
Repo: BjornMelin/mailroom

Timestamp: 2026-05-07T23:09:40.403Z
Learning: Treat delete/archive/label/send actions as deliberate operator actions; keep analysis and mutations clearly separated in the code and docs
Learnt from: CR
Repo: BjornMelin/mailroom

Timestamp: 2026-05-07T23:09:40.403Z
Learning: Prefer repo-local paths over hidden global machine state when reasonable
Learnt from: CR
Repo: BjornMelin/mailroom

Timestamp: 2026-05-07T23:09:40.403Z
Learning: Add new dependencies only when they earn their keep for the current milestone
Learnt from: CR
Repo: BjornMelin/mailroom

Timestamp: 2026-05-07T23:09:40.403Z
Learning: Preserve structured output for agent and shell workflows
Learnt from: CR
Repo: BjornMelin/mailroom

Timestamp: 2026-05-07T23:09:40.403Z
Learning: Prefer read-only audit and verification commands before broad mailbox mutations or high-volume automation apply runs
Learnt from: CR
Repo: BjornMelin/mailroom

Timestamp: 2026-05-07T23:09:40.403Z
Learning: Keep TUI work as a thin shell over existing services and reports; do not duplicate Gmail, workflow, automation, or store ownership in UI code
Learnt from: CR
Repo: BjornMelin/mailroom

Timestamp: 2026-05-07T23:09:40.403Z
Learning: If a plugin-assisted Codex workflow exists for an operation, document it alongside native commands rather than pretending the repo already implements it
Learnt from: CR
Repo: BjornMelin/mailroom

Timestamp: 2026-05-07T23:09:40.403Z
Learning: Keep error enums local to the layer that owns the failure semantics; do not introduce a repo-wide catch-all error enum unless it clearly reduces total code and cognitive load
Learnt from: CR
Repo: BjornMelin/mailroom

Timestamp: 2026-05-07T23:09:40.403Z
Learning: Keep `error.code` stable and operator-oriented, keep `error.kind` for deeper subsystem detail, and keep `error.causes` to an ordered message chain only
Learnt from: CR
Repo: BjornMelin/mailroom

Timestamp: 2026-05-07T23:09:40.403Z
Learning: Preserve existing human-facing error text unless it is misleading, ambiguous, or missing required operator action
Learnt from: CR
Repo: BjornMelin/mailroom

Timestamp: 2026-05-07T23:09:40.403Z
Learning: When adding or changing CLI failures, keep exit codes in a small stable bucket set rather than creating one-off codes per variant
Learnt from: CR
Repo: BjornMelin/mailroom

Timestamp: 2026-05-07T23:09:40.403Z
Learning: Update docs when architecture, storage boundaries, or command surfaces change
Learnt from: CR
Repo: BjornMelin/mailroom

Timestamp: 2026-05-07T23:09:40.403Z
Learning: Put durable architecture choices in `docs/decisions/`
Learnt from: CR
Repo: BjornMelin/mailroom

Timestamp: 2026-05-07T23:09:40.403Z
Learning: Put operator procedures in `docs/operations/` or `docs/workflows/`
Learnt from: CR
Repo: BjornMelin/mailroom

Timestamp: 2026-05-07T23:09:40.403Z
Learning: Keep docs concrete and aligned with the current binary surface
Learnt from: CR
Repo: BjornMelin/mailroom

Timestamp: 2026-05-07T23:09:40.403Z
Learning: Update operator-facing docs in the same change whenever CLI error contracts, JSON envelopes, or exit-code behavior change
🪛 LanguageTool
docs/decisions/0008-read-only-tui-foundation.md

[style] ~75-~75: This word has been used in one of the immediately preceding sentences. Using a synonym could make your text more interesting to read, unless the repetition is intentional.
Context: ...ure set Rejected because Mailroom only needs core widgets and the Crossterm backend ...

(EN_REPEATEDWORDS_NEED)

README.md

[style] ~172-~172: This sentence may be long and difficult for your reader to follow. Consider inserting a period and starting a new sentence here.
Context: ...](docs/operations/tui-operator-shell.md), with the durable design captured in [`docs/decisions/0008-read-only-tui-...

(WITH_THE_SENTENCE)


[style] ~234-~234: Three successive sentences begin with the same word. Consider rewording the sentence or use a thesaurus to find a synonym.
Context: ...tch archive/label runs land cleanly. 4. Expand unsubscribe assistance only after the d...

(ENGLISH_WORD_REPEAT_BEGINNING_RULE)

🔍 Remote MCP Firecrawl

Summary of additional facts relevant to this PR review

  • Ratatui: disabling default features is normal for no-std / backend control; v0.30+ adds ratatui::run()/init()/restore() helper that calls init before and restore after a closure (recommended convenience for ensuring terminal restore). Also disabling default-features can turn off layout-cache (may affect perf unless layout-cache explicitly enabled). Sources: Ratatui docs / highlights pages.,

  • Ratatui / Crossterm coupling: Ratatui has historically re-exported/backed multiple crossterm versions and there are feature/compat concerns; mixed crossterm versions may appear to work but can fail due to global state (raw-mode state, event buffers). Best practice is to ensure the application depends on the same crossterm version used by ratatui or use ratatui’s documented backend feature selection. Source: ratatui GitHub discussion/issue.

  • Crossterm raw-mode semantics and restore: crossterm provides enable_raw_mode() / disable_raw_mode(); programs must disable raw mode on exit (including on panic). Typical patterns: call disable_raw_mode() in a Drop guard or use a central panic hook that disables raw mode before printing backtraces. There are platform differences (raw-mode semantics on Windows vs UNIX) and prior crossterm issues reference these concerns. Sources: crossterm docs and discussion.,, and practical writeups on restoring raw-mode in panic hooks.

Implications for this PR (concise, actionable)

  • Using ratatui::run() would simplify terminal init/restore and panic-safety; if the PR implements manual init/restore via a TerminalGuard Drop type that reliably calls restore(), that's acceptable but verify it handles panics and mixed-version crossterm globals. (See ratatui::run() for comparison.)
  • Ensure the repo's Cargo.toml pins or otherwise aligns the crossterm version used by the app with the version ratatui expects (avoid mixed crossterm globals that can break raw-mode/event buffering).
  • Confirm raw-mode restore on panic/early-exit: the TUI should restore terminal state on panic (Drop guard, panic hook, or ratatui::run()). Also consider try_lock / weak-ref patterns if using panic hooks as described in community writeups.,

Tools/sources used

  • Firecrawl firecrawl_search (ratatui docs, ratatui GitHub issue, crossterm docs, raw-mode blog post)
🔇 Additional comments (29)
src/handlers/mod.rs (1)

12-12: Handler module wiring is clean and consistent.

This follows the established handler registration pattern and keeps TUI dispatch isolated in the handlers layer.

Also applies to: 27-27

AGENTS.md (1)

39-39: Strong boundary guidance for TUI ownership.

This rule is precise and helps prevent service duplication in UI code.

src/automation/mod.rs (1)

8-8: Automation module exports look well-scoped.

The added re-exports support read-only rollout/report consumers without blurring ownership boundaries.

Also applies to: 18-19

src/cli.rs (1)

54-55: TUI command surface is clean and focused.

The new subcommand and seed argument are concise and consistent with the existing CLI structure.

Also applies to: 211-216

docs/architecture/repository-layout.md (1)

32-32: Repository ownership guidance is clear and enforceable.

These additions make TUI boundaries explicit and reduce the risk of cross-layer ownership drift.

Also applies to: 53-55

docs/README.md (1)

20-31: Docs index update is consistent and helpful.

The added ADR and renumbered reading path keep the onboarding sequence aligned with the new TUI surface.

src/mailbox.rs (1)

25-25: Mailbox read-only search export is a good addition.

This keeps the API explicit for read-only consumers while preserving the mailbox module boundary.

Cargo.toml (1)

34-35: Use explicit crossterm_0_29 feature for clarity.

While ratatui 0.30.0 and crossterm 0.29.0 are compatible and the direct dependency on line 35 ensures single-version resolution, the generic "crossterm" feature on line 34 is implicit. Replace with the explicit "crossterm_0_29" feature to document the intended version compatibility:

ratatui = { version = "0.30.0", default-features = false, features = ["crossterm_0_29"] }
src/workflows/mod.rs (1)

9-13: LGTM.

Re-export additions for list_workflows_read_only and promote_workflow are correctly surfaced at the module boundary alongside existing workflow service items.

src/workflows/service/mod.rs (1)

16-19: LGTM.

list_workflows_read_only and promote_workflow are correctly promoted to the service public API.

src/lib.rs (1)

113-113: LGTM.

Commands::Tui dispatch correctly takes &paths (matching the async handler signature) without an unnecessary clone, consistent with other non-spawn_blocking-wrapped async handlers like Search and Gmail.

docs/roadmap/v1-search-triage-draft-queue.md (1)

76-107: LGTM.

The TUI foundation section accurately describes the scope (Dashboard/Search/Workflows/Automation/Help, local SQLite, no write operations) and correctly positions the next slice as production action flows. Aligned with the implementation and the broader ADR.

docs/decisions/0008-read-only-tui-foundation.md (1)

1-77: LGTM.

Well-structured ADR with clear Context/Decision/Why/Consequences/Rejected-alternatives sections. The explicit enumeration of excluded operations (Gmail mutations, draft send, cleanup, attachment export, automation apply) and the Ratatui feature-set rationale are good decision record hygiene. Based on learnings: "Put durable architecture choices in docs/decisions/."

docs/operations/tui-operator-shell.md (1)

1-89: LGTM.

Concrete runbook covering invocation, views, key bindings, safety contract, and troubleshooting with accurate CLI commands. Based on learnings: "Put operator procedures in docs/operations/" and "Keep docs concrete and aligned with the current binary surface."

src/workflows/service/queries.rs (1)

15-53: LGTM.

The delegation refactor (list_workflowsstore::init + list_workflows_read_only) cleanly separates the store-initializing CLI path from the migration-free TUI path. The resolve_workflow_account_id(config_report, None) call in list_workflows_read_only will surface WorkflowServiceError::NoActiveAccount when the database has no synced account, which the TUI correctly treats as a reportable condition rather than an initialization trigger — consistent with the documented safety contract.

src/handlers/tui.rs (1)

12-12: ⚡ Quick win

Both terminal integration concerns are properly addressed.

Verification confirms the implementation handles both potential issues from the Ratatui/Crossterm integration:

  1. Panic safetysrc/tui.rs implements a TerminalGuard struct with a Drop impl that calls ratatui::restore() (line 40, 61-65), ensuring the terminal is restored even if a panic occurs after enabling raw mode.

  2. Crossterm version alignmentCargo.toml explicitly pins crossterm = "0.29.0", which aligns with ratatui 0.30.0's expected version range. Both versions are compatible with no mismatch risk.

docs/architecture/system-overview.md (2)

34-50: LGTM!

The TUI layer documentation clearly establishes the read-only boundary and thin-shell constraint, aligning with the project's architectural principle that the TUI must not create a second rules engine or storage model. The enumeration of rendered views (Dashboard, Search, Workflows, Automation) and the explicit mutation gating requirement are well-documented.


124-127: LGTM!

Cross-references to the new decision record and operations doc are correctly placed alongside other ownership references.

src/mailbox/search.rs (1)

13-68: LGTM!

The search_read_only function correctly validates input (non-empty terms, positive limit), resolves the account ID, and uses spawn_blocking for the SQLite FTS query. This aligns with the project's pattern of keeping blocking database work off the async runtime and supports the TUI's read-only surface without initializing state.

src/tui.rs (5)

27-59: LGTM!

The run function correctly:

  • Loads a snapshot of read-only data before entering the render loop
  • Seeds the Search view from initial_search if provided
  • Uses a guard pattern for terminal cleanup
  • Handles key events with proper filtering for KeyEventKind::Press

The structure maintains the TUI as a thin shell over existing services without duplicating business logic.


189-226: LGTM!

The load_snapshot function correctly uses spawn_blocking for the synchronous DoctorReport::inspect and audit::verification calls, while the async service calls (list_workflows_read_only, rollout_read_only) are awaited directly since they handle their own blocking work internally. Error handling preserves context via error_chain.


711-726: LGTM!

The truncate helper correctly handles edge cases: empty max, small max (≤3), and normal truncation with ellipsis. The boundary conditions are well-tested in the test module.


736-893: LGTM!

Comprehensive test coverage for:

  • Truncation helpers (lines 749-757)
  • View initialization and navigation (lines 759-773)
  • Diagnostic failure context preservation (lines 775-787)
  • Error rendering for all views (lines 789-831)
  • Read-only invariant: snapshot load and search do not create runtime state (lines 833-870)

The use of TestBackend for rendering tests is appropriate for TUI modules.


39-40: The code is correct: ratatui::try_init() and ratatui::restore() are available in ratatui 0.30.0 with the crossterm feature enabled.

The project specifies ratatui = { version = "0.30.0", ... features = ["crossterm"] }, which provides these convenience API functions. The implementation is sound: try_init() initializes the terminal and returns a Result, and TerminalGuard correctly calls restore() on drop to clean up.

README.md (2)

70-71: LGTM!

The TUI command examples are correctly placed in the native commands section and demonstrate both basic invocation and search-seeded startup.


171-175: LGTM!

Cross-references to the new TUI documentation follow the established pattern for other subsystem docs.

src/automation/service.rs (3)

287-292: LGTM!

The validation helper uses the typed AutomationServiceError::InvalidRolloutLimit error, consistent with the project's preference for typed thiserror errors over catch-all enums.


196-204: LGTM!

The refactored rollout function correctly validates the request, ensures runtime directories exist, initializes the store, then delegates to the shared build_rollout_report. This maintains the write-path invariants while sharing report-building logic with the read-only variant.


206-212: rollout_read_only gracefully handles uninitialized store via NoActiveAccount blocker.

Unlike rollout, this function skips ensure_runtime_dirs_task and init_store_task. When the store doesn't exist (e.g., fresh checkout without prior store init), resolve_automation_account_id returns a NoActiveAccount error, which is explicitly handled as a rollout blocker by is_rollout_blocker (line 617). The function returns Ok(AutomationRolloutReport) with blockers populated rather than propagating an error. This is the intentional design for read-only operations and is consistent with the search_read_only pattern verified in the test suite—no runtime state is created, and graceful degradation is applied.

Comment thread README.md Outdated
Comment thread src/mailbox/search.rs
Comment thread src/tui.rs

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

let account_id = resolve_search_account_id(config_report)?;

P2 Badge Offload the read-only account lookup

When the new search_read_only path is used from the TUI, this call still performs SQLite work synchronously via store::accounts::get_active / store::mailbox::inspect_mailbox before the actual search is moved to spawn_blocking. On a busy or slow local store, submitting a TUI search can block the async runtime/UI thread, and it also violates the repo’s AGENTS.md guidance to keep blocking SQLite work behind tokio::task::spawn_blocking; move this lookup into the existing blocking section or a separate blocking task.

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/mailbox/search.rs (1)

40-40: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

resolve_search_account_id blocks the async thread with SQLite I/O.

resolve_search_account_id calls store::accounts::get_active() and store::mailbox::inspect_mailbox() synchronously. Both are blocking SQLite operations and must not run on a Tokio worker thread. This affects both the CLI and the new TUI path that calls search_read_only directly.

As per coding guidelines: "Keep blocking SQLite and filesystem work behind tokio::task::spawn_blocking."

🔧 Proposed fix
-    let account_id = resolve_search_account_id(config_report)?;
+    let config_for_account = config_report.clone();
+    let account_id =
+        spawn_blocking(move || resolve_search_account_id(&config_for_account)).await??;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/mailbox/search.rs` at line 40, The call to resolve_search_account_id
performs synchronous SQLite I/O (store::accounts::get_active and
store::mailbox::inspect_mailbox) on a Tokio worker thread; change the flow so
these blocking operations run inside tokio::task::spawn_blocking and are awaited
instead of executing on the async runtime. Either make resolve_search_account_id
async and inside it wrap get_active()/inspect_mailbox() calls in spawn_blocking
(returning the resolved account_id), or keep it synchronous and call
tokio::task::spawn_blocking from the caller (where resolve_search_account_id is
invoked) to execute the function off the async thread; ensure the returned
Result/AccountId is awaited and propagated correctly.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@src/mailbox/search.rs`:
- Line 40: The call to resolve_search_account_id performs synchronous SQLite I/O
(store::accounts::get_active and store::mailbox::inspect_mailbox) on a Tokio
worker thread; change the flow so these blocking operations run inside
tokio::task::spawn_blocking and are awaited instead of executing on the async
runtime. Either make resolve_search_account_id async and inside it wrap
get_active()/inspect_mailbox() calls in spawn_blocking (returning the resolved
account_id), or keep it synchronous and call tokio::task::spawn_blocking from
the caller (where resolve_search_account_id is invoked) to execute the function
off the async thread; ensure the returned Result/AccountId is awaited and
propagated correctly.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 81fb49d6-66a2-41b0-8173-b8faf081f10c

📥 Commits

Reviewing files that changed from the base of the PR and between cff1575 and 825cbf9.

📒 Files selected for processing (3)
  • Cargo.toml
  • README.md
  • src/mailbox/search.rs
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: rust
🧰 Additional context used
📓 Path-based instructions (2)
src/**/*.rs

📄 CodeRabbit inference engine (AGENTS.md)

Keep the Rust surface modular: command parsing, workspace paths, storage, Gmail adapters, and TUI should stay separate

Use anyhow for command dispatch and top-level context in src/lib.rs, and prefer typed thiserror errors in Gmail, workflow, and store layers

Keep error enums local to the layer that owns the failure semantics; do not introduce a repo-wide catch-all error enum unless it clearly reduces total code and cognitive load

For new CLI JSON contracts, normalize success to { "success": true, "data": ... } and failure to { "success": false, "error": { "code": ..., "message": ..., "kind": ..., "operation": ..., "causes": [...] } }

Do not include debug or backtrace payloads in the JSON error contract; use stderr and Rust backtrace env vars for deep diagnostics instead

Keep error.code stable and operator-oriented, keep error.kind for deeper subsystem detail, and keep error.causes to an ordered message chain only

When adding or changing CLI failures, keep exit codes in a small stable bucket set rather than creating one-off codes per variant

Keep blocking SQLite and filesystem work behind tokio::task::spawn_blocking; do not treat running spawn_blocking work as abortable

Add focused error-path tests for every new failure class. If CLI JSON or exit-code behavior changes, add contract tests for the new output and exit mapping in the same pass

Preserve structured output for agent and shell workflows

Run cargo fmt --check to verify code formatting

Run cargo clippy --all-targets --all-features -- -D warnings to verify linting

Run cargo test to verify all tests pass

Files:

  • src/mailbox/search.rs
**/*.{js,jsx,ts,tsx,md,txt,json}

📄 CodeRabbit inference engine (Custom checks)

Reject Unicode EM DASH (code point U+2014) in comments and string literals. Use -- instead. Hard FAIL if any newly introduced content includes U+2014.

Files:

  • README.md
🧠 Learnings (2)
📓 Common learnings
Learnt from: CR
Repo: BjornMelin/mailroom

Timestamp: 2026-05-07T23:34:26.464Z
Learning: Never commit live mailbox caches, OAuth credentials, tokens, or exported attachments by default
Learnt from: CR
Repo: BjornMelin/mailroom

Timestamp: 2026-05-07T23:34:26.464Z
Learning: Keep all runtime state under `.mailroom/` unless there is a deliberate reason to introduce a new ignored path
Learnt from: CR
Repo: BjornMelin/mailroom

Timestamp: 2026-05-07T23:34:26.464Z
Learning: Prefer one canonical representation for local mail metadata rather than duplicating state across multiple stores
Learnt from: CR
Repo: BjornMelin/mailroom

Timestamp: 2026-05-07T23:34:26.464Z
Learning: Treat delete/archive/label/send actions as deliberate operator actions; keep analysis and mutations clearly separated in the code and docs
Learnt from: CR
Repo: BjornMelin/mailroom

Timestamp: 2026-05-07T23:34:26.464Z
Learning: Prefer repo-local paths over hidden global machine state when reasonable
Learnt from: CR
Repo: BjornMelin/mailroom

Timestamp: 2026-05-07T23:34:26.464Z
Learning: Add new dependencies only when they earn their keep for the current milestone
Learnt from: CR
Repo: BjornMelin/mailroom

Timestamp: 2026-05-07T23:34:26.464Z
Learning: Prefer read-only audit and verification commands before broad mailbox mutations or high-volume automation apply runs
Learnt from: CR
Repo: BjornMelin/mailroom

Timestamp: 2026-05-07T23:34:26.464Z
Learning: Keep TUI work as a thin shell over existing services and reports; do not duplicate Gmail, workflow, automation, or store ownership in UI code
Learnt from: CR
Repo: BjornMelin/mailroom

Timestamp: 2026-05-07T23:34:26.464Z
Learning: If a plugin-assisted Codex workflow exists for an operation, document it alongside native commands rather than pretending the repo already implements it
📚 Learning: 2026-05-07T23:34:50.823Z
Learnt from: BjornMelin
Repo: BjornMelin/mailroom PR: 23
File: src/tui.rs:61-67
Timestamp: 2026-05-07T23:34:50.823Z
Learning: When reviewing Rust TUI code using ratatui 0.30.0, do not flag `ratatui::try_init()` as missing a panic hook. In this version, `try_init()` installs a panic hook that restores the terminal (similar to `ratatui::init()`), but returns `Result` so callers can handle failures without panicking. In a CLI/TUI handler context (e.g., files like `src/tui.rs`), the correct pattern is to use `try_init()` and ensure terminal restoration via `TerminalGuard`’s `Drop` implementation rather than expecting `init()`-style panic behavior.

Applied to files:

  • src/mailbox/search.rs
🪛 LanguageTool
README.md

[style] ~172-~172: This sentence may be long and difficult for your reader to follow. Consider inserting a period and starting a new sentence here.
Context: ...](docs/operations/tui-operator-shell.md), with the durable design captured in [`docs/decisions/0008-read-only-tui-...

(WITH_THE_SENTENCE)

🔍 Remote MCP Firecrawl

Summary — additional facts useful for reviewing PR #23 (read-only Ratatui TUI)

  • Ratatui offers convenience init/run/restore helpers and recommends using ratatui::run() to ensure terminal is initialized/restored (helps panic-safety); docs and examples show both run/init/restore and manual Terminal usage.()
  • Ratatui 0.30+ supports selecting Crossterm major versions via feature flags (crossterm_0_28, crossterm_0_29). Mixed Crossterm majors can cause: separate event queues, raw-mode globals not restored correctly, and type incompatibilities — so the repo should ensure the same Crossterm major is used by ratatui and application code. (Cargo.toml in PR already sets ratatui default-features = false + feature crossterm_0_29 and adds crossterm.)
  • ratatui-crossterm re-exports the selected Crossterm as its backend; using that re-export (or aligning versions) avoids conflicts in downstream code.
  • Crossterm raw-mode APIs: enable_raw_mode()/disable_raw_mode() are the recommended top-level calls; programs must disable raw mode on exit (including on panic). Using these simplifies panic cleanup compared to Termion’s RawTerminal semantics.
  • Panic-hook guidance: Ratatui docs include explicit patterns for panic hooks for each backend. Termion requires extra care (RawTerminal stores previous state; docs show creating a RawTerminal, suspending raw mode for the hook, and notes pitfalls). Termion panic handling is more complex and can deadlock if not designed carefully. Review should ensure PR’s TUI uses ratatui/crossterm patterns (or implements robust panic/restore handling).
  • Community advisory and issue history: Ratatui maintainers warn about Crossterm version incompatibility (pinned/pinned-feature guidance, examples of projects switching to ratatui::crossterm re-export to avoid mismatches). This is an established, frequent source of runtime and subtle bugs (raw-mode persistence, lost events). Confirm the repository’s Cargo graph avoids multiple Crossterm majors.
  • Practical implication for this PR: because Cargo.toml adds ratatui with default-features = false and feature crossterm_0_29 plus a direct crossterm dependency, verify:
    • The ratatui crate version used by src/tui.rs is compatible with the chosen ratatui features (init/run/restore availability).
    • No other dependency pulls a different Crossterm major (use cargo tree -p crossterm to confirm). If other crates need Crossterm, prefer using ratatui’s re-export or align feature flags.
    • Ensure panic/early-exit paths call restore/disable_raw_mode (or use ratatui::run()) so terminal state is restored on panics or test failures.

Sources / tools used

  • Ratatui docs (Backends, Feature Flags, Panic Hooks) via Firecrawl firecrawl_search.,,
  • Ratatui / crates metadata (Terminal API, run/init/restore guidance) via Firecrawl firecrawl_search.
  • Crossterm raw-mode docs via Firecrawl firecrawl_search.
  • Ratatui issue discussing Crossterm incompatibility (practical community guidance) via Firecrawl firecrawl_search.
🔇 Additional comments (8)
src/mailbox/search.rs (1)

8-12: store::init correctly moved to spawn_blocking.

The previous concern about blocking the async runtime on store::init has been addressed. The ?? correctly unwraps the JoinError and the inner Result.

README.md (5)

8-8: Clear documentation of TUI posture.

The consistent emphasis on "read-only" and the explicit priority ordering (CLI first, TUI second) effectively communicate the TUI's role as a thin inspection shell over existing services.

Also applies to: 10-10, 48-48


70-71: TUI command examples follow established patterns.

The placement between search and attachment commands is logical, and the syntax is consistent with other command examples. The --search flag example clearly demonstrates the feature.


217-217: Docs map entries are consistent.

The documentation map entries follow the established format and correctly reference the same TUI documentation files mentioned in the operations section.

Also applies to: 225-225


232-234: Roadmap items now have varied structure.

The previous review concern about repetitive "Expand" openings has been addressed. The current wording clearly distinguishes each item while maintaining logical sequencing and dependencies.


171-174: The referenced documentation files are present in the repository:

  • docs/operations/tui-operator-shell.md
  • docs/decisions/0008-read-only-tui-foundation.md

No issues with this section.

Cargo.toml (2)

34-35: LGTM! Dependencies justified by TUI milestone.

The ratatui and crossterm additions are appropriate for the read-only TUI feature. The configuration follows best practices with explicit feature flag selection and proper version alignment.

Based on learnings, these dependencies earn their keep for the current milestone.


34-35: ⚡ Quick win

Dependency configuration is correct; verify locally that no transitive dependencies pull a different Crossterm major version.

The ratatui and crossterm dependency configuration is properly aligned:

  • ratatui's crossterm_0_29 feature flag matches the direct crossterm 0.29.0 dependency
  • Using default-features = false for ratatui is appropriate

Crossterm version conflicts are a common source of subtle runtime bugs. Verify during local testing or in CI that no other dependency in the tree pulls a different Crossterm major version (e.g., via cargo tree -p crossterm).

Copy link
Copy Markdown
Owner Author

[codex] Addressed the latest Codex review in 4d0d552. search_read_only now resolves the account ID inside the same spawn_blocking closure as the SQLite FTS query, so the store::accounts::get_active / store::mailbox::inspect_mailbox lookup no longer runs on the async runtime. Verified locally with cargo check, cargo test mailbox::tests::search, cargo clippy --all-targets --all-features -- -D warnings, full cargo test, and cargo run -- search definitely-no-such-mailroom-test-token --json.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/mailbox/search.rs (1)

32-38: 🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win

Add error-path tests for the three new failure classes.

Per coding guidelines, every new failure class requires a focused error-path test. These three paths lack coverage:

  1. Empty terms"search terms cannot be empty" (line 32–33)
  2. limit == 0"search limit must be greater than zero" (line 36–37)
  3. No active account → "no active Gmail account found; run mailroom auth login first" (line 85–87)

The existing search_request_default_limit_is_nonzero test validates only that the default limit is nonzero, not error handling when zero is explicitly passed.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/mailbox/search.rs` around lines 32 - 38, Add focused unit/integration
tests in src/mailbox/search.rs covering the three error paths: (1) call the
search entry point with an empty terms vector and assert the returned error
contains "search terms cannot be empty" (use the same function that reads
`terms`), (2) call it with request.limit == 0 and assert the error contains
"search limit must be greater than zero" (exercise the branch that reads
`request.limit`), and (3) simulate/no-op the account lookup to produce the "no
active Gmail account found; run `mailroom auth login` first" error and assert
that exact message is returned (exercise the code path that checks for an active
account around the `no active Gmail account found` check). Ensure each test is
named clearly (e.g., search_empty_terms_errors, search_zero_limit_errors,
search_no_active_account_errors) and asserts the specific error string.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@src/mailbox/search.rs`:
- Around line 32-38: Add focused unit/integration tests in src/mailbox/search.rs
covering the three error paths: (1) call the search entry point with an empty
terms vector and assert the returned error contains "search terms cannot be
empty" (use the same function that reads `terms`), (2) call it with
request.limit == 0 and assert the error contains "search limit must be greater
than zero" (exercise the branch that reads `request.limit`), and (3)
simulate/no-op the account lookup to produce the "no active Gmail account found;
run `mailroom auth login` first" error and assert that exact message is returned
(exercise the code path that checks for an active account around the `no active
Gmail account found` check). Ensure each test is named clearly (e.g.,
search_empty_terms_errors, search_zero_limit_errors,
search_no_active_account_errors) and asserts the specific error string.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 5b50af71-503f-458a-bd70-769df952fc63

📥 Commits

Reviewing files that changed from the base of the PR and between 825cbf9 and 4d0d552.

📒 Files selected for processing (1)
  • src/mailbox/search.rs
📜 Review details
🧰 Additional context used
📓 Path-based instructions (1)
**/*.rs

📄 CodeRabbit inference engine (AGENTS.md)

For new CLI JSON contracts, normalize success and failure to the standard top-level shape: { "success": true, "data": ... } for success and { "success": false, "error": { "code": ..., "message": ..., "kind": ..., "operation": ..., "causes": [...] } } for failure

Keep error.code stable and operator-oriented, keep error.kind for deeper subsystem detail, and keep error.causes to an ordered message chain only

Do not include debug or backtrace payloads in the JSON error contract; use stderr and Rust backtrace env vars for deep diagnostics instead

Keep blocking SQLite and filesystem work behind tokio::task::spawn_blocking; do not treat running spawn_blocking work as abortable

Add focused error-path tests for every new failure class. If CLI JSON or exit-code behavior changes, add contract tests for the new output and exit mapping in the same pass

When adding or changing CLI failures, keep exit codes in a small stable bucket set rather than creating one-off codes per variant

Files:

  • src/mailbox/search.rs
🧠 Learnings (2)
📓 Common learnings
Learnt from: CR
Repo: BjornMelin/mailroom

Timestamp: 2026-05-07T23:46:55.128Z
Learning: Native center of gravity is Rust; all new code should be written in Rust unless there is a deliberate reason to use another language
Learnt from: CR
Repo: BjornMelin/mailroom

Timestamp: 2026-05-07T23:46:55.128Z
Learning: Keep the Rust surface modular: command parsing, workspace paths, storage, Gmail adapters, and TUI should stay separate
Learnt from: CR
Repo: BjornMelin/mailroom

Timestamp: 2026-05-07T23:46:55.128Z
Learning: Use `anyhow` for command dispatch and top-level context in `src/lib.rs`, and prefer typed `thiserror` errors in Gmail, workflow, and store layers
Learnt from: CR
Repo: BjornMelin/mailroom

Timestamp: 2026-05-07T23:46:55.128Z
Learning: Keep error enums local to the layer that owns the failure semantics; do not introduce a repo-wide catch-all error enum unless it clearly reduces total code and cognitive load
Learnt from: CR
Repo: BjornMelin/mailroom

Timestamp: 2026-05-07T23:46:55.128Z
Learning: Never commit live mailbox caches, OAuth credentials, tokens, or exported attachments to git by default
Learnt from: CR
Repo: BjornMelin/mailroom

Timestamp: 2026-05-07T23:46:55.128Z
Learning: Keep all runtime state under `.mailroom/` unless there is a deliberate reason to introduce a new ignored path
Learnt from: CR
Repo: BjornMelin/mailroom

Timestamp: 2026-05-07T23:46:55.128Z
Learning: Prefer one canonical representation for local mail metadata rather than duplicating state across multiple stores
Learnt from: CR
Repo: BjornMelin/mailroom

Timestamp: 2026-05-07T23:46:55.128Z
Learning: Treat delete/archive/label/send actions as deliberate operator actions; keep analysis and mutations clearly separated in the code and docs
Learnt from: CR
Repo: BjornMelin/mailroom

Timestamp: 2026-05-07T23:46:55.128Z
Learning: Prefer repo-local paths over hidden global machine state when reasonable
Learnt from: CR
Repo: BjornMelin/mailroom

Timestamp: 2026-05-07T23:46:55.128Z
Learning: Add new dependencies only when they earn their keep for the current milestone
Learnt from: CR
Repo: BjornMelin/mailroom

Timestamp: 2026-05-07T23:46:55.128Z
Learning: Preserve structured output for agent and shell workflows in CLI output
Learnt from: CR
Repo: BjornMelin/mailroom

Timestamp: 2026-05-07T23:46:55.128Z
Learning: Prefer read-only audit and verification commands before broad mailbox mutations or high-volume automation apply runs
Learnt from: CR
Repo: BjornMelin/mailroom

Timestamp: 2026-05-07T23:46:55.128Z
Learning: Keep TUI work as a thin shell over existing services and reports; do not duplicate Gmail, workflow, automation, or store ownership in UI code
Learnt from: CR
Repo: BjornMelin/mailroom

Timestamp: 2026-05-07T23:46:55.128Z
Learning: If a plugin-assisted Codex workflow exists for an operation, document it alongside native commands rather than pretending the repo already implements it
Learnt from: CR
Repo: BjornMelin/mailroom

Timestamp: 2026-05-07T23:46:55.128Z
Learning: Update operator-facing docs in the same change whenever CLI error contracts, JSON envelopes, or exit-code behavior change
Learnt from: CR
Repo: BjornMelin/mailroom

Timestamp: 2026-05-07T23:46:55.128Z
Learning: Keep docs concrete and aligned with the current binary surface
Learnt from: CR
Repo: BjornMelin/mailroom

Timestamp: 2026-05-07T23:46:55.128Z
Learning: Update docs when architecture, storage boundaries, or command surfaces change
Learnt from: CR
Repo: BjornMelin/mailroom

Timestamp: 2026-05-07T23:46:55.128Z
Learning: Primary interfaces are CLI first, TUI second; design commands with this priority in mind
Learnt from: CR
Repo: BjornMelin/mailroom

Timestamp: 2026-05-07T23:46:55.128Z
Learning: Use SQLite FTS5 as the single operational store for mail metadata
Learnt from: CR
Repo: BjornMelin/mailroom

Timestamp: 2026-05-07T23:46:55.128Z
Learning: Scope is single user, single mailbox first; do not introduce multi-user or multi-mailbox complexity until there is a clear operational need
Learnt from: CR
Repo: BjornMelin/mailroom

Timestamp: 2026-05-07T23:46:55.128Z
Learning: Runtime workspace is `.mailroom/`; use this as the canonical location for all local state
Learnt from: CR
Repo: BjornMelin/mailroom

Timestamp: 2026-05-07T23:46:55.128Z
Learning: Sensitive state must remain out of git; ensure credentials, tokens, and live mailbox data are in .gitignore
Learnt from: CR
Repo: BjornMelin/mailroom

Timestamp: 2026-05-07T23:46:55.128Z
Learning: Preserve existing human-facing error text unless it is misleading, ambiguous, or missing required operator action
📚 Learning: 2026-05-07T23:34:50.823Z
Learnt from: BjornMelin
Repo: BjornMelin/mailroom PR: 23
File: src/tui.rs:61-67
Timestamp: 2026-05-07T23:34:50.823Z
Learning: When reviewing Rust TUI code using ratatui 0.30.0, do not flag `ratatui::try_init()` as missing a panic hook. In this version, `try_init()` installs a panic hook that restores the terminal (similar to `ratatui::init()`), but returns `Result` so callers can handle failures without panicking. In a CLI/TUI handler context (e.g., files like `src/tui.rs`), the correct pattern is to use `try_init()` and ensure terminal restoration via `TerminalGuard`’s `Drop` implementation rather than expecting `init()`-style panic behavior.

Applied to files:

  • src/mailbox/search.rs
🔍 Remote MCP Firecrawl

Summary — additional, review-relevant facts found (concise)

  • PR page: confirmed PR #23 description, verification steps, CI/agent comments, requested changes, and that author addressed a search-blocking fix in commit 4d0d552 (account ID resolution moved into spawn_blocking).#23

  • Ratatui (docs.rs / v0.30.x): ratatui provides convenience init()/try_init()/run()/restore() helpers; run() installs panic hooks and restores terminal automatically. Manual init/restore + a Drop guard is a supported alternative. Ratatui supports backend selection via features (crossterm, crossterm_0_28, crossterm_0_29, termion, termwiz) and re-exports ratatui_crossterm::crossterm. Recommend matching backend feature flags to the direct crossterm dependency to avoid mixed-major issues.https://docs.rs/ratatui/latest/ratatui/

  • Crossterm (docs.rs / v0.29.0): crossterm exposes enable_raw_mode()/disable_raw_mode(), event APIs, and optional async/event-stream features. Feature flags include event-stream and backend-related options. Crossterm 0.29 is the expected major for ratatui's crossterm_0_29 feature. Ensure no other transitive dependency pulls a different Crossterm major.https://docs.rs/crossterm/latest/crossterm/

Actionable reviewer checks to run (minimal, concrete)

  1. Verify Cargo dependency alignment locally/CI: run cargo tree -p crossterm and ensure only crossterm 0.29.x is present (or that all ratatui/crossterm users resolve to the same major). [Rationale: mixed Crossterm majors cause raw-mode/event/global-state bugs]https://docs.rs/ratatui/latest/ratatui/ , https://docs.rs/crossterm/latest/crossterm/

  2. Confirm terminal restore/panic-safety: check src/tui.rs uses either ratatui::run()/init()/try_init() + restore() or a Drop guard that reliably calls restore() (and that tests/execution paths exercise panic/early-return cleanup). PR uses a TerminalGuard + try_init() pattern — acceptable per docs but verify Drop runs on panic in the runtime context.https://docs.rs/ratatui/latest/ratatui/

  3. Confirm blocking I/O is offloaded: verify search/account-resolution and other SQLite work run inside tokio::task::spawn_blocking (author notes they moved resolve into the same closure). Re-run mailbox search tests and TUI interactive verification from PR verification steps. [PR evidence + docs]#23 , https://docs.rs/ratatui/latest/ratatui/

Citations

🔇 Additional comments (2)
src/mailbox/search.rs (2)

8-12: LGTM — store::init correctly offloaded to spawn_blocking.

The clone + spawn_blocking(move || store::init(&init_config)).await?? pattern is consistent with the rest of the service layer. The past blocking-on-async-runtime concern is fully resolved here.


79-83: ⚡ Quick win

No issue. The crate correctly declares edition = "2024" in Cargo.toml, which supports the let_chains syntax used in lines 79-83.

			> Likely an incorrect or invalid review comment.

@BjornMelin
BjornMelin merged commit 5dd5bb2 into main May 8, 2026
3 checks passed
@BjornMelin
BjornMelin deleted the feat/tui-readonly-shell branch May 8, 2026 00:12
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