Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ https://github.com/user-attachments/assets/b25c6b19-3167-426b-a47b-5d72373083da

- Work from the keyboard. Navigate conversations, archive, snooze, label, and undo actions, including bulk changes.
- Keep accounts separate. Switch between Gmail inboxes while your other accounts continue to sync.
- Write and send. Use rich text, attachments, saved snippets, Gmail signatures, and an undo-send delay. Drafts save locally and sync with Gmail.
- Write and send. Use rich text, attachments, saved snippets, verified Gmail send-as identities, Gmail signatures, and an undo-send delay. Drafts save locally and sync with Gmail.
- Find and organize mail. Search cached messages, search Gmail for older mail, create inbox splits with rules, and set follow-up reminders.
- Make it your own. Choose light or dark themes, desktop notifications, and unread badges.
- Add optional AI. Use your own provider for reply drafts and inline autocomplete. Smart splits use a separate TypeSafe key to sort mail from a description you write.
Expand Down
28 changes: 19 additions & 9 deletions docs/SPEC.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ Attn supports these capabilities:

Calendar, mobile, web, and Linux product support are outside the current scope. Outlook, IMAP, a unified inbox, scheduled send, and read tracking are also excluded.

Google Contacts import, custom send-as aliases, Gmail-native snooze, and Gmail filter administration are not supported. Contact autocomplete derives from mail headers. Gmail confidential-mode messages can contain only API placeholders.
Google Contacts import, send-as alias administration, Gmail-native snooze, and Gmail filter administration are not supported. Contact autocomplete derives from mail headers. Gmail confidential-mode messages can contain only API placeholders.

## 3. Design decisions

Expand Down Expand Up @@ -242,7 +242,7 @@ Conflict rule: server state wins, except locally-pending actions replay on top o
- `Enter` or clicking a row opens the **full-window conversation** at a responsive readable measure up to 896px, positioned at its newest message or restored thread-bound draft. Its header contains the subject and a clickable `Esc` control that returns to the list. It omits the queue-position counter. The newest message is expanded; older messages start as one-line summaries and their bodies (including HTML frames) are not mounted until expanded. Clicking an expanded message's header collapses it into that same summary row; clicking the summary reopens it.
- While reading, `J`/`K` opens the next/previous conversation at its newest message or restored draft; at the first conversation, `K` returns to the full-width list instead of remaining in the reader. The adjacent conversations are fetched into the local renderer cache beforehand so this usually has no loading state. Unmodified `ArrowUp`/`ArrowDown`, `Space`/`Shift+Space`, and `PageUp`/`PageDown` scroll the current conversation, while `Shift+ArrowUp`/`Shift+ArrowDown` extend the selection exactly as `Shift+J`/`Shift+K` do — the arrow aliases behave the same in the list and the reader. Modifier+key chords retain their platform/browser meaning. Keyboard handling continues after clicking recipient, attachment, or trim controls and while focus is inside an HTML-mail frame; `Enter` on a focused mail link retains its native link action. When no inline draft is open and no transient overlay consumes it first, `Esc` or Back/List returns to the full-width list from every Tab stop—including focused buttons and mail links—with selection and scroll intact.
- **Reader layout:** conversation messages use flat rows with top separators and no trailing bottom border. Expanded headers have no filled banner. The active message has a short accent line beside an outlined avatar in both expanded and collapsed states. `N` and `P` move that marker; `O` expands or collapses the active message. Expand all messages and Collapse all messages affect readable messages without revealing hidden Trash copies. These controls are also available through the command palette. Expanded messages expose Reply, Reply all, and Forward actions when no draft is open. The reader summary shows message count and current user-label chips without a participant summary. An outlined or filled star beside the subject shows the current thread star state and updates after the Star command. Back to the originating list shows an Escape keycap only when no inline draft is open. The reader hint bar shows Reply, Mark done, Snooze or Change snooze, and Back. Wide windows also show N/P and O; narrow windows hide those hints while retaining their keyboard commands. Reply, Reply all, and Forward buttons show their shortcuts on hover. Message expansion controls have no hover hints.
- **Message display:** each message card shows the sender, with the active account rendered consistently as `Me` before and after send confirmation, plus a recipient summary ("to me, Priya · cc Daniel") that expands on click to the full From/To/Cc/Bcc/Reply-To set with the full date, the body, and attachment chips (filename + size — click downloads to the OS Downloads folder and reveals the file). The actual outgoing `From` header uses the primary Gmail send-as display name so recipients see the configured identity. Bare HTTP(S) and `www.` URLs in plain text or unlinked HTML text render as external links. Quoted trails and signatures auto-collapse behind a plain-text `...` control rendered inline at the trim boundary; the control stays in place while expanding/collapsing and a second click collapses again. `Tab` always retains native focus navigation across the product. For collapsed HTML mail, the `...` control precedes links inside the mail frame in keyboard order; reaching it reveals the hidden trail without changing the reading viewport dimensions, and the next Tab continues into the mail links. Revealing a long trail makes the existing reading surface scroll instead of growing the window. Text-like HTML and fallback text use Attn's padded native reading surface. Typography, media, tables, dimensions, alignment, and layout-only CSS remain native because they do not require a white document. Meaningful inline text colors remain distinct on the native dark surface, with low-contrast hues brightened and ordinary dark foregrounds normalized to the native text color. Uncolored quoted text is dimmed so preserved answer colors remain easy to distinguish. HTML whose rendered meaning depends on the winning non-neutral background or background image keeps a light document canvas shared by its body, trim control, and attachments. Attn does not add padding inside light documents; sender-authored body padding still takes precedence. Light HTML body containers retain a 10px corner radius. Decorative markup confined to a signature does not promote the message. A real authored canvas inside a quoted trail is content and retains the light treatment, while ordinary quoted formatting does not turn every later reply white. Wide mail gets an in-frame horizontal scrollbar, and the conversation reserves its vertical scrollbar gutter so expanding content does not shift the reader. Bcc appears only on the user's own sent copies — Gmail never exposes other senders' Bcc.
- **Message display:** each message card shows the sender, with the active account rendered consistently as `Me` before and after send confirmation, plus a recipient summary ("to me, Priya · cc Daniel") that expands on click to the full From/To/Cc/Bcc/Reply-To set with the full date, the body, and attachment chips (filename + size — click downloads to the OS Downloads folder and reveals the file). The actual outgoing `From` header uses the selected Gmail send-as address and display name so recipients see the configured identity. Bare HTTP(S) and `www.` URLs in plain text or unlinked HTML text render as external links. Quoted trails and signatures auto-collapse behind a plain-text `...` control rendered inline at the trim boundary; the control stays in place while expanding/collapsing and a second click collapses again. `Tab` always retains native focus navigation across the product. For collapsed HTML mail, the `...` control precedes links inside the mail frame in keyboard order; reaching it reveals the hidden trail without changing the reading viewport dimensions, and the next Tab continues into the mail links. Revealing a long trail makes the existing reading surface scroll instead of growing the window. Text-like HTML and fallback text use Attn's padded native reading surface. Typography, media, tables, dimensions, alignment, and layout-only CSS remain native because they do not require a white document. Meaningful inline text colors remain distinct on the native dark surface, with low-contrast hues brightened and ordinary dark foregrounds normalized to the native text color. Uncolored quoted text is dimmed so preserved answer colors remain easy to distinguish. HTML whose rendered meaning depends on the winning non-neutral background or background image keeps a light document canvas shared by its body, trim control, and attachments. Attn does not add padding inside light documents; sender-authored body padding still takes precedence. Light HTML body containers retain a 10px corner radius. Decorative markup confined to a signature does not promote the message. A real authored canvas inside a quoted trail is content and retains the light treatment, while ordinary quoted formatting does not turn every later reply white. Wide mail gets an in-frame horizontal scrollbar, and the conversation reserves its vertical scrollbar gutter so expanding content does not shift the reader. Bcc appears only on the user's own sent copies — Gmail never exposes other senders' Bcc.
- Bodies for the selected and adjacent conversations are preloaded so opening never shows a spinner.
- When the last row in a date group exits, its date heading fades in place while the mail row slides out.
- **Auto-advance:** after done/snooze/trash, selection (and the open reader) moves to the next conversation automatically (setting: next / previous / back to list).
Expand Down Expand Up @@ -410,20 +410,30 @@ Inline replies initially show a compact recipient summary. Clicking it exposes r
The inline composer's Save & close button and `Esc` save and close the draft, then restore focus to the reader at its source message. Back to the originating list is hidden while replying. A second `Esc` returns to that list. Attachment mutations, invalid recipients, and save errors still prevent closing. The inline close control shows the sole visible Escape keycap. Thread-bound drafts opened from Drafts
return to this same inline context whenever the parent conversation is locally available. A full-window composer hides the global mail shortcut footer. An inline draft keeps that footer with composer hints, while its own action footer remains inside the draft.

- **From identity:** every draft belongs to exactly one account, and its read-only From field shows that
account. New mail binds to the account active when the composer opened; replies, reply-alls, and forwards
always bind to the account that owns the source thread, whatever account is active. Reassigning a draft's
account and Gmail send-as aliases remain unsupported in v1 — to write from another account, switch
accounts first (F18, F18).
- **Gmail signature:** Attn caches the primary send-as signature under the existing `gmail.modify` grant and
- **From identity:** every draft belongs to exactly one account. The From selector lists that account's
primary address and verified Gmail send-as identities. Configure identities in Gmail settings.
New messages use the cached Gmail default identity, with the primary address as the offline fallback.
Replies, reply-all, and forwards match the source message against the account's verified identities.
An owned From address takes precedence, followed by the first matching To, Cc, or Bcc address.
Matching ignores case. If no identity matches, use the cached Gmail default identity.
Existing drafts retain their selected identity, including when upgrading a reply to reply-all.
Choose sender address in the command palette focuses the selector in full and inline composers.
A sender-only edit keeps a reply draft when saved and closed.
Changing From preserves the body and signature. The selected address survives autosave, restart,
Gmail draft sync, undo-send, and retry. Imported drafts retain their From address. A removed identity
remains visible on its saved draft but cannot send until the user chooses an available identity.
Sends use the chosen identity's display name and Reply-To. Attn never substitutes another sender.
Replies exclude all verified identities from self-recipients. Draft ownership remains unchanged.
Identity lists refresh with account sync and remain available offline. SMTP configuration stays in Gmail.
- **Gmail signature:** Attn caches send-as signatures under the existing `gmail.modify` grant and
inserts it as editable content when a new-message, reply, reply-all, or forward draft opens. The cache
refreshes at sync start and once per poll cycle, so compose never waits on the network and the last
fetched signature remains available offline. A composer that still contains only its planned fields and
this signature is untouched. Closing it discards the row, and the draft mirror skips it. That decision
uses the signature applied to the draft rather than the latest account cache. A Gmail setting change
therefore cannot turn an older untouched signature into authored content. Gmail does not expose its
separate reply and forward signature-default choices or the checkbox that removes the `-- ` separator
through this resource. Attn uses the primary signature for every local composer and does not invent a
through this resource. Attn uses the initially selected identity's signature for each local composer and does not invent a
separator absent from that HTML. A signature already present in an imported Gmail draft remains
editable and round-trips with that draft. An adjacent Gmail-marked separator collapses with the signature
in Attn, but exports before it with one line break, retaining its trailing space in plain text.
Expand Down
51 changes: 51 additions & 0 deletions e2e/fixtures/seed-send-as.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
{
"account": "seed@attn.test",
"labels": [],
"threads": [
{
"id": "t-received",
"messages": [
{
"id": "m-received",
"labelIds": ["INBOX"],
"receivedDaysAgo": 0,
"receivedAt": "10:00",
"from": "Maya <maya@example.com>",
"to": "WORK@example.org",
"subject": "received identity",
"bodyText": "Message for sender selection."
}
]
},
{
"id": "t-sent",
"messages": [
{
"id": "m-sent",
"labelIds": ["INBOX"],
"receivedDaysAgo": 0,
"receivedAt": "10:00",
"from": "Work <work@example.org>",
"to": "Maya <maya@example.com>",
"subject": "sent identity",
"bodyText": "Message for sender selection."
}
]
},
{
"id": "t-unmatched",
"messages": [
{
"id": "m-unmatched",
"labelIds": ["INBOX"],
"receivedDaysAgo": 0,
"receivedAt": "10:00",
"from": "Maya <maya@example.com>",
"to": "list@example.com",
"subject": "unmatched identity",
"bodyText": "Message for sender selection."
}
]
}
]
}
Loading
Loading