Merge pull request #650 from rowboatlabs/mail-enhancements

feat(email): drafts, search, read-state controls & Superhuman-style shortcuts
This commit is contained in:
arkml 2026-07-03 01:12:40 +05:30 committed by GitHub
commit 3ba94402d3
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 2064 additions and 259 deletions

View file

@ -74,7 +74,7 @@ import { summarizeMeeting } from '@x/core/dist/knowledge/summarize_meeting.js';
import { getAccessToken } from '@x/core/dist/auth/tokens.js';
import { getRowboatConfig } from '@x/core/dist/config/rowboat.js';
import { runLiveNoteAgent } from '@x/core/dist/knowledge/live-note/runner.js';
import { listImportantThreads, listEverythingElseThreads, saveMessageBodyHeight, triggerSync as triggerGmailSync, sendThreadReply, archiveThread, trashThread, markThreadRead, getAccountEmail, getAccountName, getConnectionStatus as getGmailConnectionStatus } from '@x/core/dist/knowledge/sync_gmail.js';
import { listImportantThreads, listEverythingElseThreads, saveMessageBodyHeight, triggerSync as triggerGmailSync, sendThreadReply, saveThreadDraft, deleteThreadDraft, listDraftThreads, searchThreads, archiveThread, trashThread, markThreadRead, downloadAttachment, getAccountEmail, getAccountName, getConnectionStatus as getGmailConnectionStatus } from '@x/core/dist/knowledge/sync_gmail.js';
import { searchContacts as searchGmailContacts, warmContactIndex } from '@x/core/dist/knowledge/gmail_contacts.js';
import { searchSentContacts, warmSentContacts } from '@x/core/dist/knowledge/gmail_sent_contacts.js';
import { getGoogleDocsConnectionStatus, importGoogleDoc, syncGoogleDocDown, syncGoogleDocUp, getGoogleDocLink } from '@x/core/dist/knowledge/google_docs.js';
@ -782,6 +782,18 @@ export function setupIpcHandlers() {
'gmail:sendReply': async (_event, args) => {
return sendThreadReply(args);
},
'gmail:saveDraft': async (_event, args) => {
return saveThreadDraft(args);
},
'gmail:deleteDraft': async (_event, args) => {
return deleteThreadDraft(args.draftId);
},
'gmail:getDrafts': async () => {
return listDraftThreads();
},
'gmail:search': async (_event, args) => {
return searchThreads(args.query, { limit: args.limit });
},
'gmail:getConnectionStatus': async () => {
return getGmailConnectionStatus();
},
@ -798,7 +810,10 @@ export function setupIpcHandlers() {
return trashThread(args.threadId);
},
'gmail:markThreadRead': async (_event, args) => {
return markThreadRead(args.threadId);
return markThreadRead(args.threadId, args.read);
},
'gmail:downloadAttachment': async (_event, args) => {
return downloadAttachment(args);
},
'gmail:saveMessageHeight': async (_event, args) => {
saveMessageBodyHeight(args.threadId, args.messageId, args.height);

View file

@ -169,6 +169,25 @@
color: var(--gm-placeholder);
}
.gmail-search-clear {
display: inline-flex;
align-items: center;
justify-content: center;
width: 20px;
height: 20px;
border: none;
border-radius: 4px;
background: transparent;
color: var(--gm-text-muted);
cursor: pointer;
transition: background 120ms ease, color 120ms ease;
}
.gmail-search-clear:hover {
background: var(--gm-icon-hover-bg);
color: var(--gm-text);
}
.gmail-icon-button {
display: inline-flex;
align-items: center;
@ -205,6 +224,46 @@
flex-direction: column;
}
/* Native list virtualization: offscreen rows skip layout and paint entirely.
Applied only to rows without a mounted ThreadDetail (those hold iframes and
composers, which must keep rendering while offscreen). */
.gmail-row-group-cv {
content-visibility: auto;
contain-intrinsic-size: auto 40px;
}
/* While the list is scrolling, rows ignore the pointer so hover restyles and
prefetch timers don't compete with frame rendering. */
.gmail-shell[data-scrolling] .gmail-row-shell {
pointer-events: none;
}
/* Archived/trashed rows slide out and collapse before removal. Removing the
class (failed action) snaps the row back. */
.gmail-row-group-leaving {
overflow: hidden;
pointer-events: none;
animation: gmail-row-leave 160ms ease-in forwards;
}
@keyframes gmail-row-leave {
0% {
opacity: 1;
transform: translateX(0);
max-height: 48px;
}
60% {
opacity: 0;
transform: translateX(32px);
max-height: 48px;
}
100% {
opacity: 0;
transform: translateX(32px);
max-height: 0;
}
}
.gmail-list-header {
position: sticky;
top: 0;
@ -319,6 +378,20 @@
background: var(--gm-bg-row-selected-hover);
}
/* The j/k keyboard cursor. Declared after the hover rules (same specificity)
so the focus ring survives hovering the focused row. */
.gmail-row-focused,
.gmail-row-focused:hover {
background: var(--gm-bg-row-hover);
box-shadow: inset 0 0 0 1px var(--gm-accent);
}
.gmail-row-selected.gmail-row-focused,
.gmail-row-selected.gmail-row-focused:hover {
background: var(--gm-bg-row-selected);
box-shadow: inset 2px 0 0 var(--gm-accent), inset 0 0 0 1px var(--gm-accent);
}
.gmail-row-unread {
color: var(--gm-text);
}
@ -394,12 +467,50 @@
border-top: 1px solid var(--gm-border);
border-bottom: 1px solid var(--gm-border);
box-shadow: inset 2px 0 0 var(--gm-accent);
/* Replays whenever the detail is shown hidden details are display: none,
so un-hiding restarts the animation. */
animation: gmail-detail-open 140ms ease-out;
}
.gmail-detail-hidden {
display: none;
}
@keyframes gmail-detail-open {
from {
opacity: 0;
transform: translateY(-6px);
}
to {
opacity: 1;
transform: none;
}
}
/* The inline reply/forward composer pops in from below the thread. */
.gmail-compose-inline {
animation: gmail-compose-open 140ms ease-out;
}
@keyframes gmail-compose-open {
from {
opacity: 0;
transform: translateY(8px);
}
to {
opacity: 1;
transform: none;
}
}
@media (prefers-reduced-motion: reduce) {
.gmail-detail-inline,
.gmail-compose-inline,
.gmail-row-group-leaving {
animation: none;
}
}
.gmail-detail-toolbar {
display: flex;
align-items: center;

File diff suppressed because it is too large Load diff