Skip to content

feat(compose): reply with AI from the conversation and an instruction - #577

Merged
maathimself merged 31 commits into
maathimself:mainfrom
prudenciooficial:feat/ai-reply
Oct 28, 2026
Merged

maathimself merged 31 commits into
maathimself:mainfrom
prudenciooficial:feat/ai-reply

Conversation

@prudenciooficial

@prudenciooficial prudenciooficial commented Oct 8, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Adds Reply with AI: the user says in a few words what the reply should say ("approve the artwork and ask for the print date") and the AI writes it from the conversation so far. The existing compose AI actions draft from the subject and recipients, or rewrite what is already typed; none of them reads the message being answered. Agreed in #652.

It is offered where a reply starts: a Reply with AI button in the reading pane toolbar, one on each open message in the conversation view, and an entry in the composer's AI menu when replying, in place of Write draft. The composer opens with the instruction field focused. The suggestion streams into the existing AI panel with Apply and Dismiss, and the instruction stays editable to ask again in other words.

Reading pane The suggestion, from the instruction and the thread
Reading pane Suggestion
Conversation view
Conversation view

When it is offered. It writes a draft (the writing assistant) from received mail (which only summaries send to the provider), so it needs both switches on, and the rich-text composer, since the plain-text one has no AI panel. The reading pane, the conversation view and the composer's menu all check the same thing; where it is not offered, a reply's menu keeps Write draft. The server enforces it too: the request uses a new reply purpose, which /api/ai/chat refuses unless both switches are on.

Changes

  • utils/aiReply.js (new) builds the request:
    • The thread up to the message being answered, the newest 10, one copy of each, drafts left out, the user's own messages marked. A reply opened from the ungrouped list carries no thread_id, so the thread is found through resolve-message from the answered Message-ID. A message with no thread is read on its own, and if nothing loads, the quoted original the composer already holds is used.
    • Each message is read with aiMessageText(body, { subject }) (fix(ai): send actions the visible message, not CSS and quoted history (#646) #647), as the AI actions read it.
    • It stays under the proxy's 32,000-character limit: messages go whole, newest first, and older ones are left out when the rest does not fit. Only the message being answered can be cut, when it is too long by itself, at a paragraph or sentence break. The model is told what was left out or cut, and the panel says so: "too long to send", or "the latest 10 messages" when only the cap left some out.
    • Each email sits between <<<EMAIL <token> …>>> and <<<END <token>>> lines, with a token drawn for each request. Who wrote it, which one is answered and where it was cut are said on that line, and the model is told that only lines carrying the token are markers, so a received body cannot pass for another email, the user, or an instruction. The user's instruction is a message of its own, the only one to follow. The prompt also asks for the conversation's language, a bracketed placeholder instead of an invented fact, and leaves the signature to the composer (or signs with the sender's name when it is off, feat: add a compose signature toggle and mailbox default #556).
  • backend/src/services/aiAccess.js, routes/ai.js: the reply purpose, which needs both switches. A refusal names the switch that is off, and api.ai.chat passes it on, so the panel says "summaries are off" or "the writing assistant is off" as it is.
  • ComposeModal.jsx: the action and ComposeAiPanel, one component for the phone and desktop layouts. The conversation is loaded once per composer, so rewording only asks the AI again. The panel's status lines are a live region and its error an alert.
  • MessagePane.jsx, ConversationMessageCard.jsx, ConversationPane.jsx, composeFromMessage.js: the buttons, which follow the user's reply default, and an aiReply flag that opens the composer with the panel. A message reopened by Undo send drops the flag.
  • The 7 new strings in all 11 locales.

Apply already keeps model output as text and keeps blank lines between paragraphs, from #651.

Testing

  • Unit tests for building the request (aiReply.test.js), including a received body with forged markers, a forged "(the user)" line and an instruction line, which stay text inside that email. Render tests: the reply is written from the thread and the instruction, under the reply purpose; Apply keeps the quoted original; rewording does not reload the thread; markup in a suggestion stays text; with summaries off a reply's menu offers Write draft; each refusal names its switch; the cap and the too-long notices; the button opens the composer with the instruction focused and follows the reply default; it is not offered in plain-text mode; Undo does not reopen the panel. Backend tests for the reply purpose with each switch off. The new tests were checked to fail with their fix reverted.
  • Frontend lint and build pass, and the frontend suite apart from one InboxReplyDraftObserver test that fails on main in my environment too. The backend suite and both lints pass. The screenshots are from an earlier revision, checked in Chromium (desktop and phone width) against a throwaway stack with GreenMail and a stub OpenAI-compatible provider (fictitious data).

Contributor License Agreement

By submitting this pull request I confirm that:

  • I have read and agree to the Contributor License Agreement.
  • My contribution is my own original work (or I have identified any
    third-party material and confirmed it is compatible with the CLA).
  • I have the right to submit this contribution under the terms of the CLA.

My English is basic, so this description was written with help from Claude.

🤖 Generated with the help of Claude Code

prudenciooficial and others added 2 commits October 8, 2026 18:42
When replying, the AI menu offers "Reply with AI". The user types what
the reply should say ("approve the artwork", "decline politely and ask
for a new proof") and the AI writes it from the conversation so far:
the thread up to the message being answered, oldest first, with the
user's own messages marked and the quoted copies of earlier messages
cut out. A reply opened from a list that is not grouped into
conversations does not carry the thread, so it is looked up from the
message being answered; when nothing can be loaded, the quoted original
the composer already holds is used.

The conversation is read once per composer, so rewording the
instruction and generating again only asks the AI. The request stays
within the chat proxy's limits: at most ten messages, the newest kept
first and the oldest shortened or left out.

The conversation is mail other people wrote, so the prompt tells the
model to ignore instructions inside it, and Apply now escapes the
suggestion before it reaches the editor. Before, a suggestion containing
markup, such as a link, became real markup in the user's message; this
applies to the existing actions too.

The two copies of the suggestion panel (phone and desktop layouts) are
now one component.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
The reading pane's toolbar gets a "Reply with AI" button next to Reply,
and each open message in the conversation view gets one beside Reply
all. It opens the reply with the AI panel already waiting for the
instruction, focused, so the user types what to say and nothing else.

It is shown only when the AI's compose feature is on and the user writes
in rich text, since the plain-text composer has no AI panel. A message
reopened by Undo send is the user's own text, so it reopens without the
panel.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
prudenciooficial and others added 3 commits October 9, 2026 12:26
…eat/ai-reply

# Conflicts:
#	frontend/src/components/ComposeModal.jsx
…eat/ai-reply

# Conflicts:
#	frontend/src/components/ComposeModal.jsx
With maathimself#556 a message can go out without its signature while the
signature text is kept. The reply prompt asked the AI to leave the
closing to a signature whenever one existed, so with the signature
turned off the reply ended with no name. It now asks for the sender's
name unless the signature is on.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
prudenciooficial added a commit to prudenciooficial/mailflow that referenced this pull request Oct 9, 2026
…maathimself#577)

The fork's copy of maathimself#577, squashed from the feat/ai-reply branch as of
843711e, while that pull request awaits review.

"Reply with AI" sits in the reading pane toolbar, on each open message
in the conversation view and in the composer's AI menu when replying.
The user types what the reply should say; the AI writes it from the
thread up to the message being answered, with quotes cut and the user's
own messages marked. Apply escapes the suggestion before it reaches the
editor. With the signature turned off (maathimself#556) the reply signs with the
sender's name.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
prudenciooficial and others added 9 commits October 9, 2026 21:02
…eat/ai-reply

# Conflicts:
#	frontend/src/components/MessagePane.render.test.js
The thread now marks its drafts (is_draft). A saved reply draft is part
of the thread but was never sent, so the AI was shown it as a message
the user had written in the conversation. It is left out now.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
…eat/ai-reply

# Conflicts:
#	frontend/src/utils/composeFromMessage.js
…eat/ai-reply

# Conflicts:
#	frontend/src/components/ComposeModal.jsx
…eat/ai-reply

# Conflicts:
#	frontend/src/components/ConversationPane.render.test.js
Write draft wrote straight away, from the subject and recipients alone.
It now opens the same instruction field as Reply with AI: the user says
what the email should say, and with the field left empty the AI writes
from the subject on its own, as before. The new prompt signs with the
sender's name when there is no signature, instead of leaving a
"[Your name]" placeholder. In a reply the menu offers Reply with AI in
its place, since a draft from the subject knows nothing of the
conversation.

Apply turned blank lines between paragraphs into adjacent paragraphs,
which the editor sets almost touching, so a suggestion that read well
in the panel ran together in the message. A blank line is now an empty
paragraph, as Enter pressed twice leaves it.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
…eat/ai-reply

# Conflicts:
#	frontend/src/components/ConversationPane.render.test.js
…eat/ai-reply

# Conflicts:
#	frontend/src/components/ComposeModal.jsx
#	frontend/src/locales/cs.json
#	frontend/src/locales/de.json
#	frontend/src/locales/en.json
#	frontend/src/locales/es.json
#	frontend/src/locales/fr.json
#	frontend/src/locales/it.json
#	frontend/src/locales/ko.json
#	frontend/src/locales/pl.json
#	frontend/src/locales/ptBR.json
#	frontend/src/locales/ru.json
#	frontend/src/locales/zhCN.json
@maathimself

Copy link
Copy Markdown
Owner

Thanks for the work on this. I'm not going to merge it as it stands: it's a new feature that sends received mail to the AI provider, and per CONTRIBUTING a feature like this needs an issue first so we can agree on the direction. It also isn't on the plan for 4.1 (scheduled send, sorting and pinned emails, the full-width list and category totals).

If you'd like to pursue it, please open an issue describing it. A few things would need to change for it to land later:

  • It's gated on the compose feature, which admins know as "draft & improve emails" and which until now only sent the user's own draft. Reading the thread sends up to ten received messages, a different privacy scope, so it needs its own setting (or at least the summarize one).
  • htmlToText and stripQuotedReply are quadratic on crafted input: 320 KB of <div in one message blocks the tab for about 20 s once the button is clicked. Anything run over email HTML has to stay linear on hostile input.
  • Messages are cut at 12k/6k characters without telling the model or the user. Since Summarize and custom AI actions silently omit email text after 6000 characters #633 the rule is the whole email up to 32k, with any cut disclosed.

Two parts are useful on their own: escaping the suggestion when it's applied, and keeping blank lines between paragraphs. If you split those into a small PR, I'll review it separately. I'm leaving this one open for now.

prudenciooficial and others added 2 commits October 19, 2026 14:41
…eat/ai-reply

# Conflicts:
#	frontend/src/components/ComposeModal.render.test.js
htmlToText and stripQuotedReply used patterns that search ahead from
every position, so a crafted message took time growing with the square
of its size: 320 KB of "<div " held the tab for about 50 seconds once
Reply with AI was clicked, and a long line starting "Am" with many
"schrieb" for about 9. HTML is now read by a scanner that looks at each
character a bounded number of times, and quote headers are matched by
how the line starts and ends. A "<" not followed by a letter, "/" or "!"
is kept as text, as a browser keeps it.

Reported in review by maathimself.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
@prudenciooficial

Copy link
Copy Markdown
Contributor Author

Thank you for the review and the clear direction.

I'll leave this PR as it is until we agree on the issue.

My English is basic, so this reply was written with the help of Claude (Claude Code).

prudenciooficial added a commit to prudenciooficial/mailflow that referenced this pull request Oct 19, 2026
…maathimself#577)

The fork's copy of maathimself#577, squashed from the feat/ai-reply branch as of
29a16d0, while that pull request waits on its issue (maathimself#652).

"Reply with AI" sits in the reading pane toolbar, on each open message
in the conversation view and in the composer's AI menu when replying;
Write draft takes an instruction too. The AI writes from the thread up
to the message being answered, with quotes cut, drafts left out and the
user's own messages marked, parsed in linear time. Apply escapes the
suggestion and keeps its blank lines.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
prudenciooficial and others added 4 commits October 20, 2026 16:26
…eat/ai-reply

# Conflicts:
#	frontend/src/locales/cs.json
#	frontend/src/locales/de.json
#	frontend/src/locales/en.json
#	frontend/src/locales/es.json
#	frontend/src/locales/fr.json
#	frontend/src/locales/it.json
#	frontend/src/locales/pl.json
#	frontend/src/locales/ptBR.json
#	frontend/src/locales/ru.json
#	frontend/src/locales/zhCN.json
…I switch

Since maathimself#644 each AI request says what it is for, and the server refuses
it when the administrator has turned that off. Write draft writes from
what the user typed, so it asks as the writing assistant. Reply with AI
writes from received mail, which until now only summaries sent to the
provider, so it asks under summaries and is offered only when both are
on. A refusal shows why, as the other AI actions do.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
…uest limit

Messages were cut at 12,000 and 6,000 characters, and older ones left
out past 24,000, without telling the model or the user. As since maathimself#633,
the conversation now goes whole when it fits one AI request. When it
does not, the oldest messages are left out first, and a message being
answered too long by itself is cut at a paragraph or sentence break
with cutPoint, now exported from aiActions.js. The model is told how
many messages were left out and how much of a cut one it is reading,
and the panel says the suggestion is based on the latest part of the
conversation.

Also from the review of maathimself#651, which carries the same code: the draft
reaches Improve, Shorten and Grammar with one newline per paragraph,
and Apply trims trailing spaces line by line instead of with a
quadratic pattern.

Reported in review by maathimself and Monkey7539.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
@prudenciooficial

Copy link
Copy Markdown
Contributor Author

An update on the three points from your review, while #652 waits for your view:

  • Privacy scope: since Feature Request: Per-user AI Assistant permissions #644, Reply with AI asks under summaries, since it sends received mail, and is offered only when both the writing assistant and summaries are on. Whether it should have a setting of its own is the open question in Reply with AI: draft a reply from the conversation and an instruction #652.
  • Linear on hostile input: done in 2b8b911.
  • The whole conversation, with any cut disclosed: done in 2aef458. The conversation goes whole up to the 32,000-character request. When it does not fit, the oldest messages are left out first, and a message being answered that is too long by itself is cut at a paragraph or sentence break with cutPoint (now exported from aiActions.js). The model is told what was left out, and the panel says the suggestion is based on the latest part of the conversation.

My English is basic, so this reply was written with the help of Claude (Claude Code).

maathimself pushed a commit that referenced this pull request Oct 21, 2026
…as text (#651)

Two fixes to Apply on an AI suggestion in the composer (Write draft,
Improve, Shorten, Grammar), split out of #577.

The blank lines between paragraphs are kept. Apply turned them into
adjacent paragraphs, which the editor sets almost touching, so a
suggestion that read well in the AI panel ran together in the message.
A blank line is now an empty paragraph, as Enter pressed twice leaves
it. The draft also reaches the model with one newline per paragraph
(getText with blockSeparator '\n'), so a single Enter no longer comes
back as a blank line.

Markup in the suggestion stays text. The suggestion went into the
editor as HTML, unescaped, so markup the model returned, such as a
link, became real markup in the user's message. aiTextToHtml now
escapes it, and trims trailing spaces line by line so a long run of
spaces stays linear.
prudenciooficial and others added 3 commits October 23, 2026 17:24
…mail

An instruction is usually a few words ("ask for a quote"), and the model
took it literally, giving a two-line note. Write draft now asks for a
complete email: a greeting by name when the recipients show it, the
context and the reason for writing, the request or information in two
or three short paragraphs with the next steps the instruction implies,
and a courteous closing, at about 120 to 220 words unless the
instruction asks otherwise. It still invents no facts, dates or prices.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
…as landed

maathimself#651 brought the same Apply conversion to main as utils/aiText.js, so the
copy in aiReply.js and its tests go, and the composer imports it from
there once. The merge before this left it imported twice.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
prudenciooficial added a commit to prudenciooficial/mailflow that referenced this pull request Oct 23, 2026
…maathimself#577)

The fork's copy of maathimself#577, squashed from the feat/ai-reply branch as of
4b9355c, while that pull request waits on its issue (maathimself#652).

"Reply with AI" sits in the reading pane toolbar, on each open message
in the conversation view and in the composer's AI menu when replying;
Write draft takes an instruction too and develops it into a full email.
The AI writes from the whole thread up to the message being answered,
up to the request limit with any cut disclosed, quotes cut, drafts left
out and the user's own messages marked, parsed in linear time.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
@maathimself

Copy link
Copy Markdown
Owner

Reviewed 4b9355c merged onto v4.0.5: it merges cleanly, the full suites pass, and in Chromium, Firefox and WebKit the flow works on desktop and phone. Privacy holds on the server (with summaries off, no received mail reaches the provider), Apply keeps model output as text, and the result only ever lands in the user's draft. Thread selection is scoped to the user's own accounts, drafts are left out, and the ten-message cap works as agreed in #652.

Changes needed:

  1. Composer menu ignores the summaries setting. ComposeModal.jsx passes aiReplyEnabled={isReply} in both toolbars (~1896 and ~2496). With the writing assistant on and summaries off, a reply's AI menu offers Reply with AI and hides Write draft; the server then refuses the request. Use isReply && aiReplyAvailable(aiStatus, plaintextEmail) as the reading pane and conversation view already do, and keep Write draft when Reply with AI isn't available.
  2. Wrong refusal message. ai_feature_disabled is mapped to compose.toolbar.aiFeatureOff ("the AI writing assistant off") even though Reply with AI is requested under summaries. The server sends feature, but utils/api.js builds the error with only status and code, so either pass feature through there or branch on the reply action, and use message.aiFeatureOff for summaries.
  3. Use the shared extraction. aiReply.js has its own htmlToText and stripQuotedReply next to aiMessageText from fix(ai): send actions the visible message, not CSS and quoted history (#646) #647, and on real (sanitized) mail it loses content the shared one keeps: a note that quotes someone mid-text ("On the call, Anna wrote: …"), Gmail and Outlook forwards, "From: Berlin / Date: 3 May" style notices, prose ending in "schrieb:", and Apple inline answers below the "On … wrote:" line. Its closingTagAt is also quadratic on an unclosed <style>, <head> or <script> (300 KB: 6 to 17 s in Node, up to 32 s in WebKit). The sanitizer keeps those inputs out today, so this is defense in depth, but the comment at the top of the file says linear and it isn't. Please build each message with aiMessageText(body, { subject }) and drop the second pipeline.
  4. Unforgeable markers. A received body containing --- Message 2 of 2 (the one being answered) ---, From: … (the user) or Instruction for the reply: lines appears in the built prompt exactly like the real ones. Please wrap each message in a per-request random marker (or neutralize lines that start with those markers), put the user's instruction in its own message as the only instruction, and add a test that a body can't create a second answered marker or instruction line.
  5. Server-side gate. Reply with AI is requested as summarize, so "writing assistant and summaries both on" is only enforced in the client. Please add a reply purpose in aiAccess.js that requires both switches, with a test.

Also, please keep this PR to Reply with AI: Write draft now asks for an instruction and targets 120 to 220 words, which goes beyond what #652 agreed; a separate PR is welcome for that. And the PR description needs updating: the gating it describes has changed, escaping and blank lines on Apply landed in #651, and older messages are left out rather than shortened (only the message being answered can be cut).

Small: the partial notice says "too long to send" even when the ten-message cap left messages out; the conversation card always opens a plain reply while the reading pane uses the user's reply default; and the panel's status and error lines could use aria-live.

prudenciooficial and others added 3 commits October 24, 2026 15:48
Reply with AI writes a draft from received mail, so it falls under the
writing assistant and summaries at once. It was asked for as
summarize, which left "both on" to the client. /api/ai/chat now takes
purpose 'reply' and refuses it unless both switches are on, naming
the one that is off as feature.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
…markers it cannot forge

From the maathimself#577 review:

- Each message is read with aiMessageText (maathimself#647). aiReply.js no longer
  has its own HTML and quote parsing, which lost forwards, inline
  answers and text that only looked like a quote header, and whose
  skip of an unclosed <style> was quadratic.
- Each email is framed by lines carrying a token drawn for the request,
  and the model is told only those lines are markers. Who wrote an
  email, which one is answered and where it was cut are said there, so
  a body cannot add an answered email or a user's message. The user's
  instruction goes in a message of its own, the only one to follow.
- The request uses the new 'reply' purpose. A refusal says which
  switch is off: summaries now get message.aiFeatureOff, since
  api.ai.chat keeps the server's feature.
- A reply's AI menu offers Reply with AI only where the reading pane
  does (both switches on, rich-text composer), and Write draft
  otherwise.
- Write draft is back to main's: the instruction and longer drafts go
  to a separate PR.
- The panel tells "older messages left out by the ten-message cap"
  from "too long to send", and its status and error lines are live
  regions.
- The conversation card's Reply with AI follows the reply default, as
  the reading pane's does.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
@prudenciooficial

Copy link
Copy Markdown
Contributor Author

Thanks for the thorough review. All of it is addressed in c4e17d0 and 10957e8, with main merged in:

  1. Composer menu: both toolbars get isReply && aiReplyAvailable(aiStatus, plaintextEmail), so with summaries off a reply's menu keeps Write draft.
  2. Refusal message: api.ai.chat now keeps the server's feature. A refused reply shows message.aiFeatureOff when summaries are off and compose.toolbar.aiFeatureOff when the writing assistant is.
  3. Shared extraction: each message is built with aiMessageText(body, { subject }), and so is the fallback from the quoted original. htmlToText and stripQuotedReply are gone, and the quadratic closingTagAt with them.
  4. Unforgeable markers: each email sits between <<<EMAIL <token> …>>> and <<<END <token>>> lines.
    • The token is 96 bits, drawn per request, and drawn again if a body happens to contain it.
    • Who wrote the email, which one is answered and where it was cut are said on that marker line. The system prompt says only lines carrying the token are markers.
    • The instruction is a user message of its own, the only one to follow.
    • A test sends a body with the old markers, a forged (the user) From line, a fake token marker and an Instruction for the reply: line. It checks that there is exactly one answered marker, that those lines stay inside that email, and that there is one instruction.
  5. Server-side gate: aiAccess.js has AI_PURPOSES with reply: ['compose', 'summarize']. /api/ai/chat refuses reply unless both switches are on, and names the one that is off. There are tests for each switch.

Write draft is back to main's behaviour. I'll open the instruction and the longer draft as a separate PR once this one settles, since it builds on the panel here. The description is updated.

Small ones:

  • When only the ten-message cap left messages out, the panel now says "Based on the latest 10 messages of the conversation: older ones were left out", not the too-long note.
  • The conversation card follows the reply default.
  • The panel's status lines are in a role="status" live region, and the error is role="alert".

Something to check: the request now has two user messages in a row. OpenAI-compatible APIs and the Codex Responses API accept that, but some self-hosted chat templates require the roles to alternate (Mistral's on vLLM, for example). If you would rather keep a single user message, I can put the instruction after the conversation in the same message, outside the markers.

Tests: each new test was checked to fail with its fix reverted.

  • Backend: the suite and both lints pass.
  • Frontend: lint and build pass. The suite passes apart from one InboxReplyDraftObserver test, "explicit opening reports cancellation…", which fails on main in my environment too.
  • I did not repeat the browser check on this revision.

My English is basic, so this reply was written with the help of Claude (Claude Code).

prudenciooficial and others added 3 commits October 25, 2026 05:48
The message actions are shared now (maathimself#650), so Reply with AI is an icon
button from MessageActions.jsx, by Reply, in the reading pane and on each
message of a conversation. On a phone the toolbar is sized for its 44 px
buttons with no room for another, so there it is in More, after the read
toggle.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
@maathimself

Copy link
Copy Markdown
Owner

Reviewed 48aa8bc merged onto 4.1.0. All five changes and the small ones are in, each new test fails with its fix reverted, the full suites pass, and in Chromium, Firefox and WebKit the flow works on desktop and phone, including Send later on a reply written with AI. With summaries off, no received mail reaches the provider. The InboxReplyDraftObserver test passes here, so that failure is specific to your environment.

On the two user messages: keep them. Every AI request already starts with two system messages in a row, so a template that requires alternating roles already fails on the existing actions. If that needs fixing, it belongs in the server for every action, not in this PR.

One small thing for later, not needed here: the subject line sits outside the markers, so it could move into the answered email's marker line.

Merging now, thanks. This ships in the next release. You mentioned a separate PR for Write draft with an instruction; it's welcome, please link #652 in it.

@maathimself maathimself left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approved: re-reviewed and tested, see the comment above.

@maathimself
maathimself merged commit e7835a0 into maathimself:main Oct 28, 2026
2 checks passed
Monkey7539 added a commit to Monkey7539/mailflow that referenced this pull request Oct 28, 2026
Brings in upstream 0441d23 (42 commits): 4.1.0, Reply with AI from a
conversation and with an instruction (maathimself#577), replying to a sent
message's recipients (maathimself#692), the conversation bar and cards drawn from
the reading pane's buttons (maathimself#650), a moved message's new uid found
without COPYUID, read-state and count fixes (maathimself#701), and the scheduled
send follow-ups, among others.

Conflicts:
- ConversationPane.jsx: upstream rebuilt the conversation bar as icon
  buttons above the message list and removed ThreadBtn, so dev's old
  bar goes. Dev's two changes to it move into the new bar: no Snooze
  button without a snooze target, and the picker's Change snooze time
  action.
- ComposeModal.render.test.js: both sides appended a describe block;
  upstream's first.

Also: dev's conversation tests find the bar's buttons by aria-label
instead of title, since Archive's title now carries its shortcut.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants