diff --git a/client/electron/src/components/MailView.tsx b/client/electron/src/components/MailView.tsx index 6f9ec6e..52da78c 100644 --- a/client/electron/src/components/MailView.tsx +++ b/client/electron/src/components/MailView.tsx @@ -1,128 +1,155 @@ -import { useEffect, useState } from 'react'; -import Markdown from 'react-markdown'; -import remarkGfm from 'remark-gfm'; -import { useMailStore } from '../stores/mailStore'; -import { useSessionStore } from '../stores/sessionStore'; -import { useContactStore } from '../stores/contactStore'; -import { useAuthStore } from '../stores/authStore'; +import { useEffect, useState } from "react"; +import Markdown from "react-markdown"; +import remarkGfm from "remark-gfm"; +import { useMailStore } from "../stores/mailStore"; +import { useSessionStore } from "../stores/sessionStore"; +import { useContactStore } from "../stores/contactStore"; +import { useAuthStore } from "../stores/authStore"; import { - sessionReplyTarget, - mailReplyTarget, - mailCounterpart, - replyAllCC, - participantAddress -} from '../lib/replyTarget'; -import * as api from '../api/client'; -import type { Mail } from '../types'; -import { MailIcon, ShieldIcon, PersonIcon, BotIcon, CheckIcon, CloseIcon, ForwardIcon, TreeIcon, TagIcon, GaugeIcon } from './icons'; -import AddressInput from './AddressInput'; -import { AttachmentList, AttachmentPicker, type PendingAttachment } from './Attachments'; -import ThreadView from './ThreadView'; -import PermissionChip, { permissionModeHint } from './PermissionChip'; -import BackButton from './BackButton'; + sessionReplyTarget, + mailReplyTarget, + mailCounterpart, + replyAllCC, + participantAddress, +} from "../lib/replyTarget"; +import * as api from "../api/client"; +import type { Mail } from "../types"; +import { + MailIcon, + ShieldIcon, + PersonIcon, + BotIcon, + CheckIcon, + CloseIcon, + ForwardIcon, + TreeIcon, + TagIcon, + GaugeIcon, +} from "./icons"; +import AddressInput from "./AddressInput"; +import { + AttachmentList, + AttachmentPicker, + type PendingAttachment, +} from "./Attachments"; +import ThreadView from "./ThreadView"; +import PermissionChip, { permissionModeHint } from "./PermissionChip"; +import BackButton from "./BackButton"; export default function MailView() { - const currentMail = useMailStore(s => s.currentMail); - const markRead = useMailStore(s => s.markRead); - const currentSession = useSessionStore(s => s.currentSession); - const currentSessionMails = useSessionStore(s => s.currentSessionMails); - // 当前登录用户名:判定「哪封是我发的」的唯一基准。 - // 曾经写死成 'human'(单用户时代的遗留),多用户下登录名可能是 jianf, - // 判据恒为假 —— 于是回复自己发的信时对端取成了自己。 - const me = useAuthStore(s => s.user?.username || ''); - // 转发面板作用于哪封邮件;null = 未打开 - const [forwarding, setForwarding] = useState(null); - // 正在看哪封邮件的对话树;null = 看正常的邮件视图 - const [threadOf, setThreadOf] = useState(null); + const currentMail = useMailStore((s) => s.currentMail); + const markRead = useMailStore((s) => s.markRead); + const currentSession = useSessionStore((s) => s.currentSession); + const currentSessionMails = useSessionStore((s) => s.currentSessionMails); + // 当前登录用户名:判定「哪封是我发的」的唯一基准。 + // 曾经写死成 'human'(单用户时代的遗留),多用户下登录名可能是 jianf, + // 判据恒为假 —— 于是回复自己发的信时对端取成了自己。 + const me = useAuthStore((s) => s.user?.username || ""); + // 转发面板作用于哪封邮件;null = 未打开 + const [forwarding, setForwarding] = useState(null); + // 正在看哪封邮件的对话树;null = 看正常的邮件视图 + const [threadOf, setThreadOf] = useState(null); - // 切换邮件时关掉树视图:树是针对某封邮件的,留着会显示上一封的线索 - const currentMailID = currentMail?.mail_id; - useEffect(() => { - setThreadOf(null); - }, [currentMailID]); + // 切换邮件时关掉树视图:树是针对某封邮件的,留着会显示上一封的线索 + const currentMailID = currentMail?.mail_id; + useEffect(() => { + setThreadOf(null); + }, [currentMailID]); - if (currentSession && currentSessionMails.length > 0) { - const last = currentSessionMails[currentSessionMails.length - 1]; - // 会话视图的对端是**会话的属性**,不能由「最后一封是谁发的」决定: - // 人在这里打字就是「给这次任务的对方追加一句」,而最后一封很可能是自己刚发的, - // 那时取对端会取成自己 —— 信就发给了自己(生产已发生)。 - const sessionTarget = sessionReplyTarget(currentSessionMails, currentSession, me); - return ( -
-
-
- - - {currentSession.session_alias - ? `.${currentSession.session_alias}` - : '(未命名会话)'} - - - - {currentSessionMails.length} 封 - -
- - -
-

{currentSession.subject}

-
- -
- {currentSessionMails.map(m => ( - setForwarding(m)} /> - ))} -
- {/* 会话视图原先只有回复,转发入口只存在于单封邮件视图 —— + if (currentSession && currentSessionMails.length > 0) { + const last = currentSessionMails[currentSessionMails.length - 1]; + // 会话视图的对端是**会话的属性**,不能由「最后一封是谁发的」决定: + // 人在这里打字就是「给这次任务的对方追加一句」,而最后一封很可能是自己刚发的, + // 那时取对端会取成自己 —— 信就发给了自己(生产已发生)。 + const sessionTarget = sessionReplyTarget( + currentSessionMails, + currentSession, + me, + ); + return ( +
+
+
+ + + {currentSession.session_alias + ? `.${currentSession.session_alias}` + : "(未命名会话)"} + + + + {currentSessionMails.length} 封 + +
+ + +
+

+ {currentSession.subject} +

+
+ +
+ {currentSessionMails.map((m) => ( + setForwarding(m)} + /> + ))} +
+ {/* 会话视图原先只有回复,转发入口只存在于单封邮件视图 —— 而人多数时间待在会话视图里,等于转发功能在 UI 上找不到 */} - {forwarding ? ( - setForwarding(null)} /> - ) : ( - - )} -
- ); - } + {forwarding ? ( + setForwarding(null)} /> + ) : ( + + )} +
+ ); + } - if (!currentMail) { - return ( -
-
- -

选择一封邮件查看,或点击左侧「新建」写邮件

-
-
- ); - } + if (!currentMail) { + return ( +
+
+ +

+ 选择一封邮件查看,或点击左侧「新建」写邮件 +

+
+
+ ); + } - if (threadOf) { - return setThreadOf(null)} />; - } + if (threadOf) { + return setThreadOf(null)} />; + } - return ( -
-
markRead(currentMail.mail_id)} - onForward={() => setForwarding(currentMail)} - onThread={() => setThreadOf(currentMail.mail_id)} - /> -
-
- {currentMail.body} -
- - {currentMail.mail_type === 'permission_request' && ( - - )} -
- {forwarding ? ( - setForwarding(null)} /> - ) : ( - - )} -
- ); + return ( +
+
markRead(currentMail.mail_id)} + onForward={() => setForwarding(currentMail)} + onThread={() => setThreadOf(currentMail.mail_id)} + /> +
+
+ {currentMail.body} +
+ + {currentMail.mail_type === "permission_request" && ( + + )} +
+ {forwarding ? ( + setForwarding(null)} /> + ) : ( + + )} +
+ ); } /** @@ -133,62 +160,65 @@ export default function MailView() { * 三档:plan(只读)/ workspace(目录内,越界问人)/ full(全权)。 */ function PermissionEditor() { - const session = useSessionStore(s => s.currentSession); - const setPermissionMode = useSessionStore(s => s.setPermissionMode); - const [editing, setEditing] = useState(false); - const [busy, setBusy] = useState(false); + const session = useSessionStore((s) => s.currentSession); + const setPermissionMode = useSessionStore((s) => s.setPermissionMode); + const [editing, setEditing] = useState(false); + const [busy, setBusy] = useState(false); - if (!session) return null; + if (!session) return null; - const mode = session.permission_mode || 'workspace'; - const enforcement = session.permission_enforcement || 'advisory'; + const mode = session.permission_mode || "workspace"; + const enforcement = session.permission_enforcement || "advisory"; - if (!editing) { - return ( - - ); - } + if (!editing) { + return ( + + ); + } - const modes = [ - { value: 'plan', label: '只读', desc: '不许写/改/执行' }, - { value: 'workspace', label: '目录内', desc: '越界问人' }, - { value: 'full', label: '全权', desc: '自动放行' }, - ]; + const modes = [ + { value: "plan", label: "只读", desc: "不许写/改/执行" }, + { value: "workspace", label: "目录内", desc: "越界问人" }, + { value: "full", label: "全权", desc: "自动放行" }, + ]; - return ( -
- {modes.map(o => ( - - ))} - -
- ); + return ( +
+ {modes.map((o) => ( + + ))} + +
+ ); } /** @@ -199,85 +229,87 @@ function PermissionEditor() { * 而不是「这件事值得多少个来回」。 */ function BudgetEditor() { - const budget = useSessionStore(s => s.budget); - const setBudget = useSessionStore(s => s.setBudget); - const [editing, setEditing] = useState(false); - const [draft, setDraft] = useState(''); - const [busy, setBusy] = useState(false); + const budget = useSessionStore((s) => s.budget); + const setBudget = useSessionStore((s) => s.setBudget); + const [editing, setEditing] = useState(false); + const [draft, setDraft] = useState(""); + const [busy, setBusy] = useState(false); - if (!budget) return null; + if (!budget) return null; - const exhausted = !budget.unlimited && budget.remaining === 0; + const exhausted = !budget.unlimited && budget.remaining === 0; - const open = () => { - setDraft(budget.unlimited ? '' : String(budget.max_rounds)); - setEditing(true); - }; + const open = () => { + setDraft(budget.unlimited ? "" : String(budget.max_rounds)); + setEditing(true); + }; - const commit = async (patch: { max_rounds?: number; reset?: boolean }) => { - setBusy(true); - await setBudget(patch); - setBusy(false); - setEditing(false); - }; + const commit = async (patch: { max_rounds?: number; reset?: boolean }) => { + setBusy(true); + await setBudget(patch); + setBusy(false); + setEditing(false); + }; - if (!editing) { - return ( - - ); - } + if (!editing) { + return ( + + ); + } - const invalid = draft.trim() !== '' && !/^\d+$/.test(draft.trim()); + const invalid = draft.trim() !== "" && !/^\d+$/.test(draft.trim()); - return ( -
- 往返预算 - setDraft(e.target.value)} - inputMode="numeric" - placeholder="不限" - autoFocus - className={`w-16 text-xs border rounded px-1.5 py-1 focus:outline-none focus:ring-2 focus:ring-blue-100 ${ - invalid ? 'border-red-300' : 'border-gray-300' - }`} - /> - - - -
- ); + return ( +
+ 往返预算 + setDraft(e.target.value)} + inputMode="numeric" + placeholder="不限" + autoFocus + className={`w-16 text-xs border rounded px-1.5 py-1 focus:outline-none focus:ring-2 focus:ring-blue-100 ${ + invalid ? "border-red-300" : "border-gray-300" + }`} + /> + + + +
+ ); } /** @@ -288,53 +320,58 @@ function BudgetEditor() { * 提议 + 人确认,既让 Agent 表达意图,又保证寻址稳定性由人掌握。 */ function RenameProposalBar() { - const proposal = useSessionStore(s => s.renameProposal); - const current = useSessionStore(s => s.currentSession); - const accept = useSessionStore(s => s.acceptRename); - const dismiss = useSessionStore(s => s.dismissRename); - const [busy, setBusy] = useState(false); + const proposal = useSessionStore((s) => s.renameProposal); + const current = useSessionStore((s) => s.currentSession); + const accept = useSessionStore((s) => s.acceptRename); + const dismiss = useSessionStore((s) => s.dismissRename); + const [busy, setBusy] = useState(false); - if (!proposal) return null; + if (!proposal) return null; - const from = current?.session_alias ? `.${current.session_alias}` : '(未命名)'; + const from = current?.session_alias + ? `.${current.session_alias}` + : "(未命名)"; - return ( -
-
- -
-

- Agent 建议把会话别名从 {from} 改为{' '} - .{proposal.alias} -

- {proposal.reason && ( -

{proposal.reason}

- )} -

- 改名后需用 name@path.{proposal.alias} 寻址;旧别名立即失效。 - 接受后此别名不再被 Agent 平台的自动命名覆盖 -

-
- - -
-
- ); + return ( +
+
+ +
+

+ Agent 建议把会话别名从 {from}{" "} + 改为{" "} + .{proposal.alias} +

+ {proposal.reason && ( +

+ {proposal.reason} +

+ )} +

+ 改名后需用 name@path.{proposal.alias} 寻址;旧别名立即失效。 + 接受后此别名不再被 Agent 平台的自动命名覆盖 +

+
+ + +
+
+ ); } /** @@ -342,248 +379,287 @@ function RenameProposalBar() { * 收件人用与写信页一致的三段式补全,正文引用由服务端生成(保证格式统一)。 */ function ForwardBar({ mail, onClose }: { mail: Mail; onClose: () => void }) { - const [to, setTo] = useState(''); - const [cc, setCc] = useState(''); - const [ccOpen, setCcOpen] = useState(false); - const [comment, setComment] = useState(''); - const [busy, setBusy] = useState(false); - const [error, setError] = useState(null); - const fetchInbox = useMailStore(s => s.fetchInbox); - const fetchSent = useMailStore(s => s.fetchSent); - const fetchSessions = useSessionStore(s => s.fetchSessions); - const fetchContacts = useContactStore(s => s.fetchContacts); + const [to, setTo] = useState(""); + const [cc, setCc] = useState(""); + const [ccOpen, setCcOpen] = useState(false); + const [comment, setComment] = useState(""); + const [busy, setBusy] = useState(false); + const [error, setError] = useState(null); + const fetchInbox = useMailStore((s) => s.fetchInbox); + const fetchSent = useMailStore((s) => s.fetchSent); + const fetchSessions = useSessionStore((s) => s.fetchSessions); + const fetchContacts = useContactStore((s) => s.fetchContacts); - const submit = async () => { - if (!to.trim() || busy) return; - setBusy(true); - setError(null); - try { - await api.forwardMail(mail.mail_id, { - to: to.trim(), - cc: cc.trim(), - comment: comment.trim() - }); - await Promise.all([fetchInbox('all'), fetchSent(), fetchSessions(), fetchContacts()]); - onClose(); - } catch (err) { - setError(err instanceof Error ? err.message : String(err)); - } finally { - setBusy(false); - } - }; + const submit = async () => { + if (!to.trim() || busy) return; + setBusy(true); + setError(null); + try { + await api.forwardMail(mail.mail_id, { + to: to.trim(), + cc: cc.trim(), + comment: comment.trim(), + }); + await Promise.all([ + fetchInbox("all"), + fetchSent(), + fetchSessions(), + fetchContacts(), + ]); + onClose(); + } catch (err) { + setError(err instanceof Error ? err.message : String(err)); + } finally { + setBusy(false); + } + }; - return ( -
-
- - - 转发「{mail.subject}」 - -
- -
+ return ( +
+
+ + + 转发「{mail.subject}」 + +
+ +
- + - {ccOpen && ( - - )} + {ccOpen && ( + + )} -