feat(gmail): replace +triage with +search for full-metadata Gmail search#676
feat(gmail): replace +triage with +search for full-metadata Gmail search#676malob wants to merge 1 commit intogoogleworkspace:mainfrom
Conversation
Replace +triage with +search: a general-purpose Gmail search helper with structured output, label resolution, and pagination. Breaking changes: - +triage removed. Use `gws gmail +search --query 'is:unread'` instead. - --query is now required (was optional, defaulted to 'is:unread'). - Default output format is JSON (was table). - Output schema changed: structured Mailbox objects for from/to/cc, Label objects (id + name) always included, threadId, snippet added. - +read --format json field names changed from snake_case to camelCase (e.g. thread_id → threadId) for consistency with project convention. Design: - SearchResult separate from OriginalMessage — different API formats (metadata vs full), different consumers (display vs composition), shared parsing at the function layer (parse_message_headers, Mailbox). - Three-tier error policy: infrastructure errors abort, per-message errors (404/410 race between list and get, malformed metadata) skip with warning, auth errors from per-message fetches treated as infrastructure. - buffered(10) preserves Gmail's relevance-ranked result order. - --max validated to 1..=500 (Gmail API maximum).
🦋 Changeset detectedLatest commit: f6028cc The changes in this PR will be included in the next version bump. This PR includes changesets to release 1 package
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
Summary of ChangesHello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed! This pull request replaces the +triage Gmail helper with a more capable +search command. The change addresses naming inconsistencies, removes anti-pattern output-filtering flags, and introduces necessary breaking changes to support structured metadata, pagination, and JSON-first output. These improvements enable better integration with downstream scripts and pipes while aligning with the project's established conventions. Highlights
Using Gemini Code AssistThe full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips. Invoking Gemini You can request assistance from Gemini at any point by creating a comment using either
Customization To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a Limitations & Feedback Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for GitHub and other Google products, sign up here. Footnotes
|
There was a problem hiding this comment.
Code Review
This pull request replaces the +triage Gmail helper with a new +search command that provides full metadata, label resolution, and pagination support. It also introduces a breaking change by converting Gmail JSON output fields from snake_case to camelCase for consistency across the project. Feedback was provided regarding the concurrency level used when fetching message metadata, noting a potential risk of hitting Gmail API rate limits during high-volume searches.
| let token = &token; | ||
| let label_map = &label_map; | ||
| async move { fetch_search_result(client, token, &msg_id, label_map).await } | ||
| }) |
There was a problem hiding this comment.
Using .buffered(10) with a potentially large number of messages (up to 500) could lead to hitting Gmail API rate limits if many concurrent requests are made in a short window. While send_with_retry is used, consider if a lower concurrency limit or a more robust rate-limiting strategy is needed for high-volume searches.
Description
Replaces
+triagewith+search, a general-purpose Gmail search helper with full metadata output, label resolution, and pagination.Why replace
+triage?Three reasons:
The name is wrong.
+triageaccepted arbitrary--querystrings and was used as a general-purpose search, but the name implied inbox triage.+searchreflects what it actually does.--labelswas an anti-pattern. The flag toggled whether label IDs appeared in output — an output-filtering flag, which the helper guidelines explicitly prohibit (cf. PR feat(gmail): add --thread-id, --delivered-to, and --sent-last flags to +triage #597). Labels should always be present, and resolved to human-readable names rather than raw IDs.The improvements needed are all breaking. Structured address types instead of raw header strings, pagination support, JSON as default format, required
--query— every change that makes the output useful for downstream scripts and pipes also breaks the existing schema. You can't evolve+triageinto+searchwithout breaking every consumer, so a clean replacement is more honest than a silent schema change behind the same name.What it does
+searchorchestrates three API calls:messages.list— fetch message IDs matching the querylabels.list— build an account-wide label ID→name mapmessages.get(format=metadata) — fan-out withbuffered(10), preserving Gmail's relevance-ranked orderOutput is a JSON envelope with structured types:
{ "messages": [ { "id": "18f1a2b3c4d", "threadId": "18f1a2b3c4d", "from": { "name": "Alice Smith", "email": "alice@example.com" }, "to": [{ "name": null, "email": "bob@example.com" }], "cc": [], "subject": "Project update", "date": "Thu, 26 Mar 2026 21:58:12 -0400", "snippet": "Here's the latest on...", "labels": [ { "id": "INBOX", "name": "INBOX" }, { "id": "Label_3066", "name": "Media/Incoming" } ] } ], "resultSizeEstimate": 42, "query": "from:alice", "nextPageToken": "..." }Addresses are parsed into
{ name, email }objects so downstream consumers (scripts, pipes,jq, agents) don't need to parse RFC 5322 header strings. Labels are resolved via thelabels.listAPI — each label includes both the raw ID and the resolved display name, so user-created labels showMedia/IncomingalongsideLabel_3066.Pagination: JSON output includes
nextPageTokenin the envelope. For non-JSON formats (table, CSV, YAML), the shared formatter strips envelope fields when it finds the messages array, so the token is printed as a stderr hint instead.Architecture decisions
SearchResultis separate fromOriginalMessage. Both parse Gmail message metadata, butSearchResultis for display (needs snippet, labels; usesformat=metadata) whileOriginalMessageis for reply/forward composition (needs body, parts, Message-ID, references; usesformat=full). A shared base struct was explored but rejected — the API boundary between the two formats doesn't map cleanly to a struct hierarchy. Shared infrastructure lives at the parsing layer (parse_message_headers(),Mailbox), not the struct layer.Three-tier error policy. The fan-out pattern (list IDs, then fetch each one) creates a window where a message can be deleted between the list call and the get call — a 404/410 on a per-message fetch is not an infrastructure failure, it's a race condition that should be skipped with a warning. But a 401/403 on a per-message fetch means auth broke mid-batch, which is infrastructure. The policy:
messages.list,labels.list) → abortClassification lives in
is_per_message_error()with tests for each code.--maxvalidates1..=500. Gmail API capsmaxResultsat 500. Clap rejects out-of-range values with a clear error instead of silently returning fewer results.Other changes in this PR
OriginalMessagecamelCase serialization. Added#[serde(rename_all = "camelCase")]toOriginalMessage. This changes+read --format jsonfield names from snake_case to camelCase (thread_id→threadId,body_text→bodyText, etc.) — a breaking change for+readJSON consumers. Bundled here rather than in a separate release so all Gmail JSON output follows the project's camelCase convention (established by the Model Armor helper), and consumers only need to update once alongside the+triage→+searchmigration.ParsedMessageHeadersandparse_message_headers()widened topub(super). Allowssearch.rsto reuse the shared header parsing infrastructure that was previously private tomod.rs.Live testing
Tested against a real Gmail account:
+search --query 'is:unread'(JSON)+search --query 'from:...' --max 5+search --query '...' --format table+search --query '...' --format yaml"messages": [])+search --query '...' --max 0Test coverage
720 total tests (24 new, net +15 after removing
+triagetests). New tests cover:parse_search_result(9): happy path with multi-address To, missing id/threadId/headers/From, empty headers, label resolution (unknown label fallback, empty labels, missing labelIds field)+searchin subcommand listDry Run Output: N/A —
+searchis read-only.Checklist:
AGENTS.mdguidelines (no generatedgoogle-*crates).cargo fmt --allto format the code perfectly.cargo clippy -- -D warningsand resolved all warnings.pnpx changeset) to document my changes.