Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
83 commits
Select commit Hold shift + click to select a range
3616fd7
test(email-compose): cover local saves after a sender switch moves a …
cursoragent Oct 24, 2026
742d6ce
fix(email-compose): follow a draft to its new id after a From switch
cursoragent Oct 24, 2026
28b1b79
fix(email): show a moved draft before evicting its source thread
cursoragent Oct 24, 2026
d64d878
test(email-compose): type the failure notice fake in the identity test
cursoragent Oct 24, 2026
b7a5325
fix(email-compose): reopen a compose's draft and From after a reload
cursoragent Oct 24, 2026
ec6fb71
docs(email): describe draft identity after a From switch or reload
cursoragent Oct 24, 2026
e6fd3eb
docs(email): list the compose host's moved-draft and route capabilities
cursoragent Oct 24, 2026
aa2f854
style(email): split the draft transfer refresh comment into sentences
cursoragent Oct 24, 2026
9d13328
test(email-compose): cover switching a reply composer between reply a…
cursoragent Oct 24, 2026
8804859
fix(email-compose): reopen a saved forward as a forward
cursoragent Oct 25, 2026
074e2e9
fix(email-compose): swap the quoted thread when switching reply and f…
cursoragent Oct 25, 2026
0da920d
fix(email-compose): name the quoted content a forward or reply carries
cursoragent Oct 25, 2026
9afe335
test(email-compose): keep a provider-rejected draft movable to anothe…
cursoragent Oct 25, 2026
2ef4e8e
fix(email-compose): gate the From switch on its own operation predicate
cursoragent Oct 25, 2026
fd403a6
docs(email): describe reply and forward switching and rejected-draft …
cursoragent Oct 25, 2026
9b52720
test(recipients): rank the typed address and name matches first
cursoragent Oct 25, 2026
4cdb832
test(email-compose): reopen a queued draft from its route after two r…
cursoragent Oct 25, 2026
9413593
fix(recipients): commit the typed address and rank name matches first
cursoragent Oct 25, 2026
8891904
test(email-compose): clear a send error once the draft meets it
cursoragent Oct 25, 2026
e4b5a7a
fix(email-compose): keep a reopened draft on its route while its save…
cursoragent Oct 25, 2026
6bcecd0
fix(email-compose): drop a send error as soon as the draft meets it
cursoragent Oct 25, 2026
410abda
test(email-compose): leave bounce senders out of a reply
cursoragent Oct 25, 2026
ea5a3bf
fix(email-compose): leave bounce senders out of reply recipients
cursoragent Oct 25, 2026
451b720
fix(recipients): read a named chip's address to screen readers
cursoragent Oct 25, 2026
c85539b
docs(email): describe recipient ranking, send errors, and bounce replies
cursoragent Oct 25, 2026
57d8c55
test(email-compose): format the send requirement and bounce tests
cursoragent Oct 25, 2026
4a7f993
chore(email-compose): merge the queued-draft route fix from #8792
cursoragent Oct 25, 2026
595b22f
docs(email-compose): say why bounce senders get no reply
cursoragent Oct 25, 2026
af5a0ac
Merge remote-tracking branch 'origin/synoet/compose-draft-identity-74…
cursoragent Oct 25, 2026
36ba8d0
test(email-compose): cover what a queued send reports after it leaves
cursoragent Oct 25, 2026
ee5b40b
Merge remote-tracking branch 'origin/synoet/compose-reply-forward-b49…
cursoragent Oct 25, 2026
61e7890
test(email-compose): cover a thread marked done after its send failed
cursoragent Oct 25, 2026
d82c4c1
fix(email-compose): follow a queued send to its outcome
cursoragent Oct 25, 2026
42140da
docs(email): describe the send notice and a failed send's thread
cursoragent Oct 25, 2026
41cf3f5
Merge remote-tracking branch 'origin/synoet/compose-recipients-acd3' …
cursoragent Oct 25, 2026
7310a4d
docs(email): record queued-send progress in the compose architecture
cursoragent Oct 25, 2026
4ebc725
fix(email-compose): keep a moved draft when its thread cannot open
cursoragent Oct 25, 2026
3f4519e
test(email-compose): cover a send accepted after its undo deadline
cursoragent Oct 25, 2026
2feb574
fix(email-compose): withdraw Undo once a send's deadline has passed
cursoragent Oct 25, 2026
409887e
feat(email-compose): record the send outcome this device observed
cursoragent Oct 25, 2026
70cb244
fix(email-compose): keep the quoted-content toggle's name stable
cursoragent Oct 25, 2026
c456256
test(email-compose): expect Delete to leave before the server settles
cursoragent Oct 25, 2026
bd6c239
test(email-thread): expect a draft-only thread to close when Delete s…
cursoragent Oct 25, 2026
b6d25f3
fix(email-compose): leave a standalone draft before deleting it
cursoragent Oct 25, 2026
4b7c41a
fix(email-thread): stop polling operations for a thread that is gone
cursoragent Oct 25, 2026
7f19893
docs(email): describe Delete leaving before the draft deletion settles
cursoragent Oct 25, 2026
a680a7c
Merge remote-tracking branch 'origin/synoet/compose-send-completion-c…
cursoragent Oct 25, 2026
4fdf8f6
refactor(email-thread): keep the operations poll policy at module scope
cursoragent Oct 25, 2026
18d1350
docs(email-compose): tighten the discard ordering wording
cursoragent Oct 25, 2026
3a0faab
test(email-compose): expect Delete to survive a host that cannot leave
cursoragent Oct 25, 2026
1d3a052
fix(email-compose): delete a draft even when its host cannot leave
cursoragent Oct 25, 2026
32710e5
docs(email): note that Delete survives a host that cannot leave
cursoragent Oct 25, 2026
cef96b3
test(email-compose): cover edits made around an emptied draft's delete
cursoragent Oct 25, 2026
fb96a63
test(email-compose): hold the emptied draft's delete past autosave
cursoragent Oct 25, 2026
fc8121d
test(email-compose): cover an emptied draft's uncertain delete enqueue
cursoragent Oct 25, 2026
736fbac
fix(email): release a draft whose delete never reached the queue
cursoragent Oct 25, 2026
6bd5bf9
fix(email-compose): save edits made while an emptied draft is deleted
cursoragent Oct 25, 2026
6f117fd
Merge remote-tracking branch 'origin/synoet/compose-draft-exit-0a58' …
cursoragent Oct 25, 2026
9de789e
test(email-compose): cover typing that reaches a field before To focuses
cursoragent Oct 25, 2026
2438c86
fix(recipients): keep focus in a field the user reached before mount …
cursoragent Oct 25, 2026
f46f3cd
test(email-compose): expect offline edits to say they are only on thi…
cursoragent Oct 25, 2026
2c3c114
fix(email-compose): say when offline edits are only saved on this device
cursoragent Oct 25, 2026
6367251
test(email): expect a later surface to start from the outbox already …
cursoragent Oct 25, 2026
999c524
fix(email): share one outbox snapshot across email surfaces
cursoragent Oct 25, 2026
92fbcd2
test(email): expect the outbox to be ready for the first compose
cursoragent Oct 25, 2026
9d64c6a
fix(email): read the outbox when the signed-in session starts
cursoragent Oct 25, 2026
6f7448d
test(email-compose): expect the sender row to stay while inboxes load
cursoragent Oct 25, 2026
a89099e
fix(email-compose): keep the From row while linked inboxes load
cursoragent Oct 25, 2026
bd73b1d
docs(email): describe offline draft status, shared outbox and compose…
cursoragent Oct 25, 2026
e944520
style(email): reword the outbox snapshot comment
cursoragent Oct 25, 2026
0169007
refactor(email-compose): give the sender row and offline status their…
cursoragent Oct 25, 2026
57a3ff9
feat(email): trace the From-switch draft move
cursoragent Oct 25, 2026
c8e3895
test(email-compose): expect no Undo left on offer after an accepted s…
cursoragent Oct 25, 2026
2e259bf
fix(email-compose): withdraw the queued Undo before offering the dead…
cursoragent Oct 25, 2026
c76ba5d
test(email-compose): cover where the reply signature preview sits
cursoragent Oct 25, 2026
8280ff2
fix(email-compose): preview the reply signature above the quoted text
cursoragent Oct 25, 2026
b50ad5f
docs(email): say where the reply signature preview sits
cursoragent Oct 25, 2026
6fc9a0b
Merge remote-tracking branch 'origin/synoet/compose-lag-offline-a016'…
cursoragent Oct 25, 2026
b9d662b
test(email-compose): follow a send another tab delivers and retires
cursoragent Oct 25, 2026
780f7d3
fix(email-compose): report a send another tab finished
cursoragent Oct 25, 2026
93f7ee7
docs(email): say how a send another tab finished is reported
cursoragent Oct 25, 2026
500f01e
feat(email-compose): record a send outcome read from the server
cursoragent Oct 25, 2026
af881c7
Merge origin/main into synoet/signature-preview-position-718f
cursoragent Oct 27, 2026
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
3 changes: 3 additions & 0 deletions apps/web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,9 @@
"@graphql-codegen/cli": "^7.1.3",
"@graphql-codegen/typed-document-node": "^7.0.3",
"@graphql-codegen/typescript-operations": "^6.0.5",
"@opentelemetry/api": "^1.9.1",
"@opentelemetry/core": "^2.9.0",
"@opentelemetry/sdk-trace-base": "^2.9.0",
"@playwright/test": "^1.55.1",
"@solidjs/meta": "^0.29.4",
"@solidjs/testing-library": "^0.8.10",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { Button } from '@ui';
import { Show } from 'solid-js';
import type { DraftSyncViewState } from '../core/local-draft';

/** In-composer banner for draft failures; saving itself stays silent. */
/** In-composer banner for draft failures and offline edits; saving itself stays silent. */
export function DraftSyncStatus(props: {
state?: DraftSyncViewState;
busy: boolean;
Expand All @@ -15,7 +15,7 @@ export function DraftSyncStatus(props: {
{(state) => (
<div
class="flex flex-wrap items-center gap-2 border-b border-edge-muted p-3 text-sm"
role="alert"
role={state().failed ? 'alert' : 'status'}
>
<div class="min-w-0 flex-1">
<p>{state().message}</p>
Expand Down
12 changes: 10 additions & 2 deletions apps/web/src/features/email-compose/compose-adapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ import {
import { confirmLocalDraftSave } from '@queries/email/local-draft-confirmation';
import {
clearLocalAttachmentReceipt,
forgetLocalDraft,
readLocalDraft,
recordLocalAttachment,
reviveLocalDraft,
Expand Down Expand Up @@ -111,6 +112,7 @@ import {
} from './queries/draft-lifecycle';
import { createEmailInboxSource } from './queries/inbox-source';
import { queuedDraftSaveArgs } from './queries/queued-draft';
import { watchEmailSendProgress } from './queries/send-progress';
import {
restoreDraftBodyAfterUndo,
runQueuedUndoSend,
Expand Down Expand Up @@ -150,7 +152,7 @@ export function createEmailComposeContext(
options.threadTransport?.() ??
(graphqlSoupFlag().enabled ? 'graphql' : 'rest')
);
const sends = useQueuedEmailSends(() => true);
const sends = useQueuedEmailSends();
// Attach handlers run as event handlers, which have no Solid owner of
// their own; the dialog needs the surface's.
const dialogOwner = getOwner();
Expand Down Expand Up @@ -357,6 +359,7 @@ export function createEmailComposeContext(
get retryDraft() {
return queueActive() ? retryEmailDraftSynchronization : undefined;
},
forgetLocalDraft: (draftId) => forgetLocalDraft(draftId),
readDraft: readEmailDraft,
watchDrafts: watchEmailDrafts,
async saveDraft({
Expand Down Expand Up @@ -571,6 +574,7 @@ export function createEmailComposeContext(
emailSendMatchesDraft(intent, draftId) &&
emailSendLocked(intent)
)),
watchSend: watchEmailSendProgress,
async sendMessage({ inboxId, ...input }) {
if (
!inboxSource
Expand All @@ -597,7 +601,7 @@ export function createEmailComposeContext(
!inbox.settings.signature_on_replies_forwards)
? ''
: (inbox.settings.signature ?? '');
return await sendEmailQueued({
const queued = await sendEmailQueued({
signatureHtml,
expectedLocalVersion: input.expectedLocalVersion,
draft: {
Expand All @@ -623,6 +627,10 @@ export function createEmailComposeContext(
restoreBodyText: input.restoreBodyText,
restoreBodyMacro: input.restoreBodyMacro,
});
// The thread shown after Send renders from this snapshot on mount; it
// must already hold the send, or the thread shows the draft instead.
await sends.refresh();
return queued;
},
async unschedule({ draftId, threadId, inboxId, draftGeneration }) {
const local = sends
Expand Down
27 changes: 23 additions & 4 deletions apps/web/src/features/email-compose/compose-host-adapter.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,18 @@
import { useSplitBackInterceptor } from '@components/app/split-layout/back-interceptor';
import { useSplitLayout } from '@components/app/split-layout/layout';
import { useSplitPanel } from '@components/app/split-layout/layoutUtils';
import type { createComposeRouteDraft } from './compose-route-draft';
import type { EmailComposeHost } from './context/compose-capabilities';
import { createPanelFocusSibling } from './editor-adapter';
export function createEmailComposeHost(): EmailComposeHost {

function createSplitComposeHost() {
const { replaceSplit } = useSplitLayout();
const panel = useSplitPanel();
const showThread = (id: string) => {
replaceSplit({ content: { type: 'email', id }, mergeHistory: true });
};
return {
showThread: (id: string) => {
replaceSplit({ content: { type: 'email', id }, mergeHistory: true });
},
showThread,
showDraft: (id: string) => {
replaceSplit({
content: {
Expand All @@ -27,3 +30,19 @@ export function createEmailComposeHost(): EmailComposeHost {
},
};
}

/** A thread route names the draft's thread, so it follows a moved draft there. */
export function createEmailComposeHost(): EmailComposeHost {
const host = createSplitComposeHost();
return {
...host,
showMovedDraft: ({ threadId }) => host.showThread(threadId),
};
}

/** The compose route keeps a moved draft in place and records its new id instead. */
export function createComposeRouteHost(
route: Pick<ReturnType<typeof createComposeRouteDraft>, 'record'>
): EmailComposeHost {
return { ...createSplitComposeHost(), recordDraft: route.record };
}
Loading
Loading