diff --git a/biome.jsonc b/biome.jsonc new file mode 100644 index 0000000..2b64f49 --- /dev/null +++ b/biome.jsonc @@ -0,0 +1,37 @@ +{ + // 本仓库**不使用** biome 的格式化器,只借它做诊断(且由 pi-lens 统一调度)。 + // + // # 为什么显式关闭 + // + // pi-lens 内置「安全格式化」,会对编辑过的文件跑 + // `biome format --write`(沿 biome 标准向上查找配置)。本机没有任何 + // biome 配置时,biome 用它自己的默认值:**tab 缩进 + 双引号**。 + // + // 本仓库的手工排版与那套默认不兼容:注释按语义换行、数组与调用按可读性 + // 手工折行,长注释常超过 100 列。实测把缩进/引号/lineWidth 全部对齐到本仓库 + // 习惯之后,`biome format --write` 仍然改动 17 个文件 —— 也就是说只要格式化器 + // 开着,每次编辑都会产生与内容无关的大面积 diff,把真正的改动埋掉。 + // + // 曾经发生过:一次 `git add -A` 把约 7000 行纯重排扫进了功能提交 + // (19a3161),使那次提交无法审查 —— 还掩盖了一处 Go 文件的删行。 + // + // # 因此 + // + // - formatter 关闭:保持本仓库既有排版,不再产生重排噪声 + // - linter 关闭:`biome lint --write` 会做自动修复,同样会改到与本次改动 + // 无关的行;本仓库的静态检查由 pi-lens 的其它 runner(tsc / tree-sitter / + // ast-grep / go vet)与各自的测试套件承担 + // + // 新文件仍应遵守本仓库的既有风格(2 空格缩进、单引号)。 + "vcs": { + "enabled": true, + "clientKind": "git", + "useIgnoreFile": true + }, + "formatter": { + "enabled": false + }, + "linter": { + "enabled": false + } +} diff --git a/client/electron/src/components/MailView.tsx b/client/electron/src/components/MailView.tsx index 52da78c..6f9ec6e 100644 --- a/client/electron/src/components/MailView.tsx +++ b/client/electron/src/components/MailView.tsx @@ -1,155 +1,128 @@ -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)} /> + ) : ( + + )} +
+ ); } /** @@ -160,65 +133,62 @@ 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 => ( + + ))} + +
+ ); } /** @@ -229,87 +199,85 @@ 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' + }`} + /> + + + +
+ ); } /** @@ -320,58 +288,53 @@ 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 平台的自动命名覆盖 +

+
+ + +
+
+ ); } /** @@ -379,287 +342,248 @@ 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 && ( + + )} -