email reply fixes

This commit is contained in:
debu-2 2026-10-06 04:26:32 +05:30
parent 8653f319f7
commit d58fb8841b
5 changed files with 100 additions and 85 deletions

View file

@ -31,6 +31,21 @@ def log_outbound_mime_headers(message: Message) -> None:
)
def log_signed_mime_headers(message_bytes: bytes) -> None:
"""Log threading headers from the final wire-format MIME (post-DKIM)."""
parsed = BytesParser(policy=SMTP_POLICY).parsebytes(message_bytes)
logger.info(
"smtp_signed_mime_headers message_id=%s in_reply_to=%s references=%s "
"from=%s to=%s subject=%s",
parsed.get("Message-ID"),
parsed.get("In-Reply-To"),
parsed.get("References"),
parsed.get("From"),
parsed.get("To"),
parsed.get("Subject"),
)
def _mime_attachment(part: ExtractedMimePart) -> MIMEBase:
maintype, _, subtype = part.content_type.partition("/")
if not subtype:

View file

@ -1,4 +1,4 @@
import { MoreHorizontal, Plus, UserRound } from "lucide-react";
import { Info, MoreHorizontal, Plus, UserRound } from "lucide-react";
import { useCallback, useEffect, useState } from "react";
import {
createHandoffRule,
@ -9,7 +9,7 @@ import {
} from "../api";
import AgentSpark from "./AgentSpark";
import HumanHandoffDrawer from "./HumanHandoffDrawer";
import { buildRuleCardSummary, EMAIL_TYPE_OPTIONS } from "../utils/handoffUtils";
import { buildRuleCardChips, EMAIL_TYPE_OPTIONS } from "../utils/handoffUtils";
function typeLabel(id) {
return EMAIL_TYPE_OPTIONS.find((item) => item.id === id)?.title || id;
@ -128,14 +128,12 @@ export default function HandoffSettings() {
return (
<div className="handoff-settings">
<div className="handoff-settings-intro">
<p>
<strong>Configure Human Handoff rules for your Agent.</strong>
</p>
<p>
Saved settings are stored for future use. They do not send, forward, mirror, or relay
email today — all mail continues through the normal Gentic inbox and outbox.
</p>
<div className="handoff-settings-notice">
<Info size={14} strokeWidth={2} className="handoff-settings-notice-icon" aria-hidden="true" />
<div>
<strong>Configuration only</strong>
<p>Handoff rules are saved as configuration only. Email routing remains unchanged.</p>
</div>
</div>
<div className="handoff-settings-toolbar">
@ -155,10 +153,7 @@ export default function HandoffSettings() {
<UserRound size={24} strokeWidth={1.75} />
</div>
<h2>No handoff rules yet</h2>
<p>
Configure when your Agent should involve a Human Owner. Settings are saved only;
no emails are sent automatically.
</p>
<p>Configure when your Agent should involve a Human Owner.</p>
<button type="button" className="handoff-btn primary" onClick={openCreate}>
<Plus size={16} strokeWidth={1.75} /> Add handoff rule
</button>
@ -167,28 +162,23 @@ export default function HandoffSettings() {
<div className="human-handoff-list">
{rules.map((rule) => (
<article key={rule.id} className="human-handoff-card">
<div className="human-handoff-card-top">
<div className="human-handoff-card-body">
<span className="human-handoff-card-avatar" aria-hidden="true">
<UserRound size={18} strokeWidth={1.75} />
<UserRound size={17} strokeWidth={1.75} />
</span>
<div className="human-handoff-card-meta">
<strong>{rule.owner_name}</strong>
<small>{rule.owner_email}</small>
</div>
<strong className="human-handoff-card-name">{rule.owner_name}</strong>
<span className={`human-handoff-status${rule.active ? " active" : ""}`}>
{rule.active ? "Active" : "Paused"}
</span>
</div>
<p className="human-handoff-card-summary">{buildRuleCardSummary(rule)}</p>
{rule.instructions && (
<blockquote className="human-handoff-card-quote">&ldquo;{rule.instructions}&rdquo;</blockquote>
)}
<small className="human-handoff-card-email">{rule.owner_email}</small>
<div className="human-handoff-card-actions">
<button type="button" className="handoff-btn subtle" onClick={() => openEdit(rule)}>Edit</button>
<button type="button" className="handoff-btn subtle" onClick={() => openEdit(rule)}>
Edit
</button>
<div className="handoff-card-menu-wrap">
<button
type="button"
className="handoff-btn icon"
className="handoff-btn icon quiet"
aria-label="More actions"
onClick={() => setMenuOpenId((current) => (current === rule.id ? null : rule.id))}
>
@ -207,6 +197,12 @@ export default function HandoffSettings() {
)}
</div>
</div>
</div>
<div className="human-handoff-card-chips">
{buildRuleCardChips(rule).map((chip) => (
<span key={chip} className="handoff-meta-chip">{chip}</span>
))}
</div>
</article>
))}
</div>
@ -227,7 +223,7 @@ export default function HandoffSettings() {
<header className="handoff-drawer-head">
<div>
<h2 id="handoff-test-title">Test Human Handoff</h2>
<p>Check whether your current rules would cause a particular email to be handed to you.</p>
<p>Check whether your current rules would match a particular email.</p>
</div>
</header>
<div className="handoff-drawer-body">

View file

@ -6,10 +6,7 @@ export default function HumanHandoff() {
<header className="human-handoff-head">
<div>
<h1>Human Handoff</h1>
<p>
Configure when your Agent should involve a Human Owner. Settings are saved here;
email routing is not affected until handoff is enabled in a future release.
</p>
<p>Configure when your Agent should involve a Human Owner.</p>
</div>
</header>
<HandoffSettings />

View file

@ -527,10 +527,10 @@ h1 em { color:var(--coral); font-style:normal; font-weight:500; }
.security-toggle span { position:absolute; inset:0; border-radius:999px; background:#D9D5E0; transition:background .2s; cursor:pointer; }
.security-toggle span::after { content:""; position:absolute; top:3px; left:3px; width:18px; height:18px; border-radius:50%; background:#fff; box-shadow:0 1px 3px rgba(20,18,30,0.18); transition:transform .2s; }
.security-toggle input:checked + span { background:#7357F6; }
.human-handoff-page { max-width:920px; margin:0 auto; padding:8px 4px 32px; }
.human-handoff-head { margin-bottom:20px; display:flex; align-items:flex-start; justify-content:space-between; gap:16px; }
.human-handoff-head h1 { margin:0 0 8px; font:700 28px/1.15 "Manrope"; color:#171424; }
.human-handoff-head p { margin:0; max-width:640px; color:#6f6b7c; font-size:13px; line-height:1.6; }
.human-handoff-page { max-width:910px; margin:0 auto; padding:8px 4px 32px; }
.human-handoff-head { margin-bottom:16px; display:flex; align-items:flex-start; justify-content:space-between; gap:16px; }
.human-handoff-head h1 { margin:0 0 4px; font:700 26px/1.15 "Manrope"; color:#171424; }
.human-handoff-head p { margin:0; max-width:700px; color:#77758a; font-size:12px; line-height:1.5; }
.human-handoff-tabs { margin-top:16px; display:inline-flex; gap:6px; padding:4px; border:1px solid #ece9f2; border-radius:10px; background:#faf9fc; }
.human-handoff-tabs button { height:32px; padding:0 14px; border:0; border-radius:8px; background:transparent; color:#6f6b7c; font-size:12px; font-weight:600; }
.human-handoff-tabs button.active { background:#fff; color:#171424; box-shadow:0 1px 4px rgba(20,18,30,0.08); }
@ -565,37 +565,41 @@ h1 em { color:var(--coral); font-style:normal; font-weight:500; }
.handoff-relay-status-failed { color:#c2410c; }
.handoff-show-quoted { margin-top:8px; padding:0; border:0; background:transparent; color:#6d5bd0; font-size:11px; font-weight:600; cursor:pointer; }
.handoff-quoted-body { margin:8px 0 0; padding:10px 12px; border:1px solid #ece9f2; border-radius:8px; background:#faf9fc; color:#77758a; font-size:11px; line-height:1.45; white-space:pre-wrap; overflow:auto; }
.handoff-settings-intro { margin-bottom:18px; padding:18px 20px; border:1px solid #ece7ff; border-radius:14px; background:#fbf9ff; }
.handoff-settings-intro p { margin:0 0 10px; color:#4a4652; font-size:12px; line-height:1.6; }
.handoff-settings-intro p strong { color:#171424; }
.handoff-settings-intro small { color:#77758a; font-size:11px; line-height:1.5; }
.handoff-settings-notice { margin-bottom:14px; padding:14px 16px; display:flex; align-items:flex-start; gap:10px; border:1px solid #eeeafe; border-radius:10px; background:#faf9ff; }
.handoff-settings-notice-icon { flex:0 0 auto; margin-top:1px; color:#8b7cf0; }
.handoff-settings-notice strong { display:block; margin-bottom:2px; color:#3d3850; font-size:12px; font-weight:600; }
.handoff-settings-notice p { margin:0; color:#77758a; font-size:11.5px; line-height:1.45; }
.handoff-flow-diagram { margin:14px 0 10px; padding:14px; display:grid; justify-items:center; gap:4px; border:1px dashed #ddd4ff; border-radius:10px; background:#fff; color:#4a4652; font-size:12px; font-weight:600; }
.handoff-flow-arrow { color:#9a96a5; font-weight:400; }
.handoff-settings-toolbar { margin-bottom:14px; display:flex; justify-content:flex-end; }
.human-handoff-empty,.human-handoff-list { margin-top:8px; }
.human-handoff-empty { padding:40px 28px; display:grid; justify-items:center; text-align:center; border:1px solid #ece9f2; border-radius:16px; background:#fff; box-shadow:0 8px 28px rgba(20,18,30,0.04); }
.human-handoff-empty-icon { width:52px; height:52px; margin-bottom:14px; display:grid; place-items:center; border-radius:14px; background:#f6f2ff; color:#7357f6; }
.handoff-settings-toolbar { margin:0 0 14px; display:flex; justify-content:flex-end; }
.human-handoff-empty,.human-handoff-list { margin-top:0; }
.human-handoff-empty { padding:40px 28px; display:grid; justify-items:center; text-align:center; border:1px solid #e8e5ec; border-radius:12px; background:#fff; box-shadow:0 1px 2px rgba(20,18,30,.025); }
.human-handoff-empty-icon { width:52px; height:52px; margin-bottom:14px; display:grid; place-items:center; border-radius:14px; background:#f3f0fa; color:#6d5bd0; }
.human-handoff-empty h2 { margin:0 0 8px; font:700 18px/1.2 "DM Sans"; }
.human-handoff-empty p { margin:0 0 18px; max-width:420px; color:#77758a; font-size:12px; line-height:1.55; }
.human-handoff-list { display:grid; gap:12px; }
.human-handoff-card { padding:18px 20px; border:1px solid #ece9f2; border-radius:14px; background:#fff; box-shadow:0 8px 24px rgba(20,18,30,0.035); }
.human-handoff-card-top { display:grid; grid-template-columns:auto 1fr auto; gap:12px; align-items:center; }
.human-handoff-card-avatar { width:38px; height:38px; display:grid; place-items:center; border-radius:50%; background:#f6f2ff; color:#7357f6; }
.human-handoff-card-meta strong { display:block; font:650 14px/1.3 "DM Sans"; color:#171424; }
.human-handoff-card-meta small { color:#77758a; font-size:11px; }
.human-handoff-status { height:22px; padding:0 8px; display:inline-flex; align-items:center; border-radius:999px; background:#f1eff3; color:#716d7a; font-size:10px; font-weight:600; }
.human-handoff-list { display:grid; gap:10px; }
.human-handoff-card { padding:14px 16px; border:1px solid #e8e5ec; border-radius:12px; background:#fff; box-shadow:0 1px 2px rgba(20,18,30,.025); }
.human-handoff-card-body { display:grid; grid-template-areas:"avatar name status" "avatar email actions"; grid-template-columns:36px minmax(0,1fr) auto; column-gap:12px; row-gap:2px; align-items:center; }
.human-handoff-card-avatar { grid-area:avatar; width:36px; height:36px; display:grid; place-items:center; border-radius:50%; background:#f3f0fa; color:#5f4db8; align-self:center; }
.human-handoff-card-name { grid-area:name; font:650 14px/1.25 "DM Sans"; color:#171424; }
.human-handoff-card-email { grid-area:email; color:#77758a; font-size:11px; line-height:1.3; }
.human-handoff-status { grid-area:status; height:20px; padding:0 7px; display:inline-flex; align-items:center; justify-self:end; border-radius:999px; background:#f1eff3; color:#716d7a; font-size:9.5px; font-weight:600; }
.human-handoff-status.active { background:#edf9f4; color:#11966a; }
.human-handoff-card-summary { margin:12px 0 8px; color:#5a5562; font-size:12px; }
.human-handoff-card-actions { grid-area:actions; display:flex; align-items:center; justify-self:end; gap:4px; }
.human-handoff-card-chips { display:flex; flex-wrap:wrap; gap:6px; margin-top:10px; padding-left:48px; }
.handoff-meta-chip { height:21px; padding:0 8px; display:inline-flex; align-items:center; border-radius:6px; background:#f4f2f8; color:#6f6b7c; font-size:10.5px; font-weight:500; white-space:nowrap; }
.human-handoff-card-quote { margin:0 0 14px; padding-left:12px; border-left:2px solid #ece7ff; color:#4a4652; font-size:12px; line-height:1.55; }
.human-handoff-card-actions { display:flex; align-items:center; justify-content:flex-end; gap:8px; }
.handoff-card-menu-wrap { position:relative; }
.handoff-card-menu { position:absolute; right:0; top:calc(100% + 4px); min-width:160px; padding:6px; display:grid; gap:2px; border:1px solid #ece9f2; border-radius:10px; background:#fff; box-shadow:0 8px 24px rgba(20,18,30,0.12); z-index:5; }
.handoff-card-menu button { width:100%; min-height:34px; padding:0 10px; border:0; border-radius:8px; background:transparent; text-align:left; font-size:12px; font-weight:600; color:#4a4652; }
.handoff-card-menu button:hover { background:#f7f5fa; }
.handoff-btn { height:36px; padding:0 14px; display:inline-flex; align-items:center; justify-content:center; gap:7px; border:1px solid #dfdde5; border-radius:9px; background:#fff; color:#241f2b; font-size:12px; font-weight:600; }
.handoff-btn { height:36px; padding:0 14px; display:inline-flex; align-items:center; justify-content:center; gap:7px; border:1px solid #dfdde5; border-radius:9px; background:#fff; color:#241f2b; font-size:12px; font-weight:600; cursor:pointer; transition:background .12s ease; }
.handoff-btn:hover:not(:disabled) { background:#f7f5fa; }
.handoff-btn.primary { border-color:#6d5bd0; background:#7357f6; color:#fff; }
.handoff-btn.subtle { color:#5a5562; background:#fafafa; }
.handoff-btn.icon { width:36px; padding:0; }
.handoff-btn.primary:hover:not(:disabled) { background:#6a4fe8; }
.handoff-btn.subtle { height:34px; color:#5a5562; background:#fff; border-color:#e8e5ec; }
.handoff-btn.icon { width:34px; height:34px; padding:0; border-color:#e8e5ec; }
.handoff-btn.icon.quiet { color:#9a96a5; }
.handoff-btn:disabled { opacity:.55; cursor:not-allowed; }
.handoff-drawer-backdrop { position:fixed; inset:0; z-index:60; display:flex; justify-content:flex-end; background:rgba(20,18,30,0.28); }
.handoff-drawer,.handoff-test-modal { width:min(560px,100vw); height:100%; display:flex; flex-direction:column; background:#fff; box-shadow:-12px 0 40px rgba(20,18,30,0.12); }

View file

@ -76,22 +76,25 @@ export function buildHandoffSummary({
? "when they require your personal decision or attention"
: "when your handoff instructions apply";
return `Saved rule: ${labelText} that are ${typeText} would involve ${ownerEmail || "the human owner"} ${instructionText}. Email routing is not active yet.`;
return `Handoff ${labelText} that are ${typeText} to ${ownerEmail || "the human owner"} ${instructionText}.`;
}
export function buildRuleCardSummary(rule) {
const parts = [];
export function buildRuleCardChips(rule) {
const chips = [];
if (rule.labels?.includes(ALL_MAIL_LABEL)) {
parts.push("All mail");
chips.push("All mail");
} else if (rule.labels?.length) {
parts.push(rule.labels.join(", "));
chips.push(...rule.labels);
} else {
chips.push("All mail");
}
if (rule.email_types?.includes(ALL_TYPES_ID)) {
parts.push("All types");
chips.push("All types");
} else if (rule.email_types?.length) {
parts.push(typeTitles(rule.email_types).join(", "));
chips.push(...typeTitles(rule.email_types));
} else {
chips.push("All types");
}
const meta = parts.length ? parts.join(" • ") : "All eligible emails";
const replies = rule.allow_replies ? "Replies enabled" : "View only";
return `${meta} • ${replies}`;
chips.push(rule.allow_replies ? "Replies enabled" : "View only");
return chips;
}