Add Cmd+O open links and attachments palette#180
Conversation
Greptile SummaryThis PR adds a Cmd+O palette for quickly opening links and attachments from the currently focused email, and refactors the three palettes (Command, Agent, Links/Attachments) to share a new
Confidence Score: 5/5Safe to merge; the new palette, link extraction, and keyboard routing are all well-scoped with no data-loss paths. The implementation is thorough: deduplication, Escape-with-preview, iframe forwarding, and the PaletteShell refactor all behave correctly. The two noted items are narrow UX edge cases that do not break existing functionality. OpenLinksAttachmentsPalette.tsx — the two edge cases both live here.
|
| Filename | Overview |
|---|---|
| src/renderer/components/OpenLinksAttachmentsPalette.tsx | New palette component; Escape-with-preview and preview-close correctness are solid, but Cmd+1–9 shortcuts are not suppressed while a preview is showing, and empty-body emails trigger repeated IPC loads. |
| src/renderer/utils/openables.ts | New utility: extracts deduplicated http/https links from email HTML (anchors + bare-text scan) and maps attachments to OpenableItem; trimUrlCandidate is correctly applied only to bare-text matches. |
| src/renderer/components/PaletteShell.tsx | New shared palette primitives extracted cleanly; used by all three palettes. |
| src/renderer/hooks/useKeyboardShortcuts.ts | Cmd+O handler added consistently alongside Cmd+J/K; APP_SHORTCUT_MODIFIER_KEYS export correctly includes 'j' and 'o' to forward those combos from the email-body iframe. |
| src/renderer/utils/attachments.ts | formatFileSize and isPreviewable extracted here; new version adds Math.min cap. |
| src/renderer/components/EmailDetail.tsx | iframe keydown forwarding now uses the shared APP_SHORTCUT_MODIFIER_KEYS set. |
| src/renderer/components/AgentCommandPalette.tsx | Refactored to use PaletteShell primitives; behaviour unchanged. |
| src/renderer/store/index.ts | isOpenLinksAttachmentsOpen and open/closeLinksAttachments added; integrated into getKeyboardMode. |
Sequence Diagram
%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
participant U as User
participant KS as useKeyboardShortcuts
participant S as Zustand Store
participant P as OpenLinksAttachmentsPalette
participant IPC as window.api.gmail
participant OL as openables.ts
U->>KS: Cmd+O keydown
KS->>S: openLinksAttachments()
S-->>P: "isOpen=true"
P->>P: reset query, focus input
alt storeEmail has body
P->>OL: buildOpenables(storeEmail)
OL-->>P: OpenableItem[]
else body missing / empty
P->>IPC: getEmail(sourceEmailId)
IPC-->>P: DashboardEmail (full body)
P->>OL: buildOpenables(loadedEmail)
OL-->>P: OpenableItem[]
end
P-->>U: render links + attachments list
alt User selects link
P->>S: closeLinksAttachments()
P->>U: window.open(url, _blank)
else User selects previewable attachment
P->>IPC: attachments.preview(...)
IPC-->>P: base64 data
P-->>U: AttachmentPreviewModal
U->>P: Escape
P->>P: setPreviewAttachment(null)
else User selects downloadable attachment
P->>IPC: attachments.download(...)
end
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
participant U as User
participant KS as useKeyboardShortcuts
participant S as Zustand Store
participant P as OpenLinksAttachmentsPalette
participant IPC as window.api.gmail
participant OL as openables.ts
U->>KS: Cmd+O keydown
KS->>S: openLinksAttachments()
S-->>P: "isOpen=true"
P->>P: reset query, focus input
alt storeEmail has body
P->>OL: buildOpenables(storeEmail)
OL-->>P: OpenableItem[]
else body missing / empty
P->>IPC: getEmail(sourceEmailId)
IPC-->>P: DashboardEmail (full body)
P->>OL: buildOpenables(loadedEmail)
OL-->>P: OpenableItem[]
end
P-->>U: render links + attachments list
alt User selects link
P->>S: closeLinksAttachments()
P->>U: window.open(url, _blank)
else User selects previewable attachment
P->>IPC: attachments.preview(...)
IPC-->>P: base64 data
P-->>U: AttachmentPreviewModal
U->>P: Escape
P->>P: setPreviewAttachment(null)
else User selects downloadable attachment
P->>IPC: attachments.download(...)
end
Reviews (6): Last reviewed commit: "Merge branch 'main' into codex/open-link..." | Re-trigger Greptile
9985c19 to
e2d0c20
Compare
- Keep balanced parens in bare-text URLs (e.g. Wikipedia links) while still trimming surrounding punctuation; cover both cases in e2e tests - Handle Cmd+O before the input-focus/mode bails so it works in the same focus states as Cmd+K/J/F - Replace the hardcoded iframe shortcut allowlist in EmailDetail with a shared APP_SHORTCUT_MODIFIER_KEYS set, and forward Cmd+J too - Extract shared PaletteShell/usePaletteSelection and use them in CommandPalette, AgentCommandPalette, and OpenLinksAttachmentsPalette - Finish the platform shortcut-label migration (settings/help entries, ComposeToolbar, AgentPanel, EmailPreviewSidebar, undo toasts) so non-Mac UIs no longer mix "Cmd+" and "Ctrl+" labels - Restore KeyboardHints module-scope constants; drop a dead finally guard in the palette load effect Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Summary
Validation
npm run typechecknpm run buildEXO_DEMO_MODE=true npx playwright test --project=unit tests/unit/open-links-attachments.spec.ts(4 passed)EXO_DEMO_MODE=true npx playwright test --project=e2e tests/e2e/open-links-attachments.spec.ts(8 passed)<br>, dividers,Source, and the heart