@retaw106/mmc-pi 0.0.0-stage → 1.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,4 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
2
+ <rect width="32" height="32" rx="9" fill="#c5f36b"/>
3
+ <text x="16" y="22.5" font-family="ui-monospace, monospace" font-size="17" font-weight="800" text-anchor="middle" fill="#17220d">m</text>
4
+ </svg>
@@ -0,0 +1,36 @@
1
+ <!doctype html>
2
+ <html lang="zh-CN">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1">
6
+ <meta name="robots" content="noindex, nofollow">
7
+ <meta name="color-scheme" content="dark">
8
+ <title>mmc · 本机数据审阅</title>
9
+ <link rel="icon" href="/ui/favicon.svg" type="image/svg+xml">
10
+ <link rel="stylesheet" href="/ui/panel.css">
11
+ <script src="/ui/panel.js" defer></script>
12
+ </head>
13
+ <body>
14
+ <header class="topbar">
15
+ <a class="brand" href="/" aria-label="mmc 审阅面板首页"><span class="brand-mark">m</span><span>mmc <small>本机审阅</small></span></a>
16
+ <div class="top-actions"><span id="read-at" class="read-at">尚未读取</span><button id="refresh" class="button button-primary" type="button">刷新数据</button></div>
17
+ </header>
18
+ <main class="workspace" id="workspace">
19
+ <aside class="rail panel" id="project-pane" aria-label="项目">
20
+ <div class="pane-heading"><div><p class="eyebrow">WORKSPACE</p><h1>项目</h1></div><span id="project-count" class="count-pill">—</span></div>
21
+ <div id="source-status" class="source-status" aria-live="polite"></div>
22
+ <div id="projects" class="project-list"></div>
23
+ </aside>
24
+ <section class="list-pane panel" id="list-pane" aria-label="项目内容">
25
+ <div class="pane-heading"><div><p class="eyebrow" id="list-eyebrow">选择项目</p><h2 id="list-title">审阅内容</h2></div></div>
26
+ <div class="tabs" role="tablist" aria-label="内容类别"><button class="tab active" id="sessions-tab" role="tab" aria-selected="true" type="button">会话</button><button class="tab" id="memories-tab" role="tab" aria-selected="false" type="button">记忆</button></div>
27
+ <div id="list-controls" class="list-controls"></div>
28
+ <div id="items" class="item-list" aria-live="polite"></div>
29
+ </section>
30
+ <section class="detail-pane panel" id="detail-pane" aria-label="详情">
31
+ <div id="detail" class="detail-empty"><span class="empty-glyph">⌁</span><h2>选择一条记录</h2><p>原文、摘要与记忆都以只读方式呈现。</p></div>
32
+ </section>
33
+ </main>
34
+ <div id="toast" class="toast" role="status" aria-live="polite"></div>
35
+ </body>
36
+ </html>
@@ -0,0 +1,8 @@
1
+ :root{font-family:"Manrope","Noto Sans SC",sans-serif;color:#e8e9e6;background:#0d1010;font-synthesis:none;text-rendering:optimizeLegibility;-webkit-font-smoothing:antialiased;--bg:#0d1010;--surface:#131817;--surface-2:#191f1d;--line:#28312e;--muted:#89928d;--text:#e8e9e6;--accent:#c5f36b;--accent-ink:#1b2510;--amber:#f1ba67;--red:#f08d7e;--mono:"DM Mono",ui-monospace,monospace;--radius:12px;color-scheme:dark;font-variant-numeric:tabular-nums}
2
+ *{box-sizing:border-box}button:focus-visible,a:focus-visible,input:focus-visible,select:focus-visible,textarea:focus-visible{outline:2px solid var(--accent);outline-offset:2px}body{margin:0;min-height:100vh;background:radial-gradient(ellipse at 73% -18%,#28332a 0,transparent 35%),var(--bg);color:var(--text)}button,input,select,textarea{font:inherit}button{color:inherit}.topbar{height:64px;display:flex;align-items:center;justify-content:space-between;padding:0 24px;border-bottom:1px solid #29312e;background:#101413eF;position:sticky;top:0;z-index:5;backdrop-filter:blur(14px)}.brand{display:flex;align-items:center;gap:11px;color:var(--text);text-decoration:none;font-weight:800;letter-spacing:-.04em}.brand small{display:block;color:var(--muted);font-size:10px;letter-spacing:.08em;font-weight:500;margin-top:2px}.brand-mark{width:29px;height:29px;display:grid;place-items:center;background:var(--accent);color:#17220d;border-radius:9px;font-size:20px;font-weight:800}.top-actions{display:flex;align-items:center;gap:15px}.read-at{font:11px var(--mono);color:#aab4ae}.button{border:1px solid var(--line);border-radius:8px;padding:8px 13px;min-height:38px;background:#1b211f;cursor:pointer;transition:background .16s,border-color .16s,transform .12s}.button:hover{border-color:#71835a;background:#232b26}.button:active{transform:scale(.96)}.button-primary{background:var(--accent);border-color:var(--accent);color:var(--accent-ink);font-weight:700}.button-primary:hover{background:#d4ff85}.workspace{display:grid;grid-template-columns:minmax(220px,258px) minmax(285px,350px) minmax(0,1fr);gap:12px;padding:14px;height:calc(100vh - 64px);min-height:540px}.panel{background:linear-gradient(145deg,#151a18,#121615 70%);border:1px solid #242d29;border-radius:var(--radius);min-width:0;overflow:hidden;box-shadow:0 16px 38px #0002}.rail,.list-pane{display:flex;flex-direction:column}.pane-heading{display:flex;align-items:center;justify-content:space-between;padding:20px 18px 15px}.pane-heading h1,.pane-heading h2{font-size:18px;letter-spacing:-.035em;margin:2px 0 0}.eyebrow{font:9px var(--mono);letter-spacing:.13em;color:#8c9a8f;margin:0}.count-pill,.badge{display:inline-flex;align-items:center;justify-content:center;border-radius:999px;padding:4px 8px;background:#242c28;color:#b6c0b8;font:10px var(--mono);white-space:nowrap}.source-status{padding:0 14px 12px;display:grid;gap:5px}.source-row{display:flex;align-items:center;justify-content:space-between;padding:7px 9px;border-radius:7px;background:#111614;font-size:10px;color:#aab3ad}.source-label{display:flex;align-items:center;gap:7px}.dot{width:7px;height:7px;border-radius:50%;background:#68c995;box-shadow:0 0 9px #68c95566}.dot.bad{background:var(--red);box-shadow:0 0 9px #f08d7e55}.dot.warn{background:var(--amber);box-shadow:0 0 9px #f1ba6755}.source-state{font:9px var(--mono);color:#c3ccc6}.source-detail{grid-column:1/-1;color:#d2948a;font-size:10px;line-height:1.5;overflow-wrap:anywhere}.project-list,.item-list{overflow:auto;padding:0 8px 12px}.project-card,.item-card{width:100%;display:block;text-align:left;border:1px solid transparent;background:transparent;border-radius:9px;padding:11px 10px;cursor:pointer;transition:background .15s,border-color .15s;min-width:0}.project-card:hover,.item-card:hover{background:#1b221f}.project-card.selected,.item-card.selected{background:#202821;border-color:#3c4b35;box-shadow:inset 2px 0 var(--accent)}.project-path{display:block;font:11px var(--mono);color:#e3eadf;overflow-wrap:anywhere;line-height:1.55}.project-meta,.item-meta{display:flex;justify-content:space-between;gap:8px;margin-top:7px;color:var(--muted);font-size:10px}.project-flags{display:flex;gap:5px;flex-wrap:wrap;margin-top:7px}.flag{font:9px var(--mono);color:#d2bd8e;background:#30291d;border-radius:4px;padding:3px 5px}.tabs{display:flex;border-bottom:1px solid var(--line);padding:0 16px;gap:20px}.tab{border:0;border-bottom:2px solid transparent;background:none;padding:0 2px 11px;color:#89948d;cursor:pointer;font-size:12px}.tab.active{color:var(--accent);border-color:var(--accent);font-weight:700}.list-controls{padding:12px 14px 10px;display:grid;gap:8px;border-bottom:1px solid #222a27}.control-row{display:flex;gap:7px}.field{min-width:0;width:100%;background:#0f1412;border:1px solid #2c3631;border-radius:7px;color:var(--text);padding:8px 9px;font-size:11px;outline:none}.field:focus{border-color:#9fc75d;box-shadow:0 0 0 2px #b6e76a1f}.field::placeholder{color:#727e76}.control-row .field{flex:1}.checkline{display:flex;align-items:center;gap:7px;color:#aab4ad;font-size:10px}.checkline input{accent-color:var(--accent)}.item-card{padding:11px 10px;margin:2px 0}.item-title{font-size:12px;font-weight:600;color:#e2e7e2;line-height:1.45;display:block;overflow-wrap:anywhere}.item-sub{display:block;color:#8e9992;font-size:10px;margin-top:5px}.item-snippet{display:block;color:#aeb7b1;font-size:10px;line-height:1.5;margin-top:6px;white-space:pre-wrap;overflow:hidden;display:-webkit-box;-webkit-line-clamp:3;-webkit-box-orient:vertical}.item-card .badge{margin-left:5px}.item-card .flag{margin-left:5px}.detail-pane{overflow:auto;position:relative}.detail-content{padding:25px clamp(18px,3vw,38px) 35px;max-width:1100px;margin:0 auto}.detail-empty{height:100%;min-height:400px;display:flex;flex-direction:column;align-items:center;justify-content:center;text-align:center;color:var(--muted);padding:30px}.detail-empty h2{font-size:16px;color:#d6ddd7;margin:13px 0 3px}.detail-empty p{font-size:11px}.empty-glyph{font:50px var(--mono);color:#829464;opacity:.65}.detail-kicker{font:10px var(--mono);color:var(--accent);letter-spacing:.1em;text-transform:uppercase}.detail-title{font-size:22px;line-height:1.35;letter-spacing:-.04em;margin:7px 0 8px;overflow-wrap:anywhere;text-wrap:balance}.detail-meta{display:flex;flex-wrap:wrap;gap:7px;color:#a1aaa4;font-size:10px;margin-bottom:21px}.section{border-top:1px solid #2a332f;padding:18px 0}.section-head{display:flex;align-items:center;justify-content:space-between;gap:10px;margin-bottom:11px}.section-head h3{font-size:12px;margin:0;letter-spacing:.02em}.section-note{font:9px var(--mono);color:#77837b}.compartment{background:#171e1b;border:1px solid #2a342f;border-radius:9px;padding:13px;margin:8px 0}.comp-head{display:flex;justify-content:space-between;gap:10px;align-items:flex-start}.comp-title{font-size:12px;font-weight:700}.comp-meta{font:9px var(--mono);color:#98a59b;white-space:nowrap}.comp-content,.prose{font-size:11px;line-height:1.75;color:#c8d0ca;white-space:pre-wrap;overflow-wrap:anywhere}.comp-content{margin:9px 0}.comp-details{display:grid;gap:7px}.bound-row{display:flex;align-items:center;gap:6px;flex-wrap:wrap;color:#939e96;font-size:9px;margin-top:10px}.link-button{border:1px solid #46513f;background:#202820;color:#d6ebba;border-radius:5px;padding:5px 8px;font-size:10px;cursor:pointer}.link-button:disabled{opacity:.6;cursor:not-allowed}.message{border-left:2px solid #47534b;padding:8px 10px;margin:10px 0;background:#141a17;border-radius:0 7px 7px 0}.message.user{border-color:#9abc5e}.message.assistant{border-color:#668b9b}.message-head{display:flex;justify-content:space-between;gap:10px;color:#a4b09f;font:9px var(--mono);margin-bottom:7px}.message-body{font-size:11px;line-height:1.65;white-space:pre-wrap;overflow-wrap:anywhere;color:#e0e4df}.tool-block{border:1px solid #303b35;border-radius:6px;margin-top:8px;overflow:hidden}.tool-head{display:flex;justify-content:space-between;gap:8px;padding:7px 9px;background:#202723;color:#b9c3bc;font:9px var(--mono)}.tool-output{padding:9px;margin:0;color:#c7d0c9;font:10px/1.6 var(--mono);white-space:pre-wrap;overflow-wrap:anywhere;max-height:360px;overflow:auto}.pager{display:flex;justify-content:space-between;align-items:center;margin:12px 0}.note-card,.evidence-card{padding:10px;border:1px solid #29332e;border-radius:8px;background:#141a17;margin:7px 0}.note-card p,.evidence-card p{margin:0;white-space:pre-wrap;overflow-wrap:anywhere;font-size:11px;line-height:1.6}.memory-content{font-size:13px;line-height:1.8;white-space:pre-wrap;overflow-wrap:anywhere;padding:13px;background:#151c18;border-radius:8px;border:1px solid #29332e}.kv-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:9px}.kv{padding:9px;background:#141a17;border-radius:7px;min-width:0}.kv-label{font:9px var(--mono);color:#859188;display:block;margin-bottom:5px}.kv-value{font-size:11px;color:#dce2dc;overflow-wrap:anywhere}.draft-box{width:100%;min-height:95px;resize:vertical}.saved{font-size:10px;color:#a9ce73}.loading,.empty-state,.error-state{padding:24px 13px;text-align:center;color:#9ba59f;font-size:11px;line-height:1.6}.error-state{color:#efa093}.empty-state strong{display:block;color:#d9dfda;font-size:12px;margin-bottom:5px}.inline-error{padding:10px;border-radius:7px;background:#321f1c;color:#f0aa9e;font-size:10px;margin:8px 0}.toast{position:fixed;bottom:18px;left:50%;transform:translate(-50%,12px);opacity:0;pointer-events:none;background:#29352b;color:#e4f3d2;border:1px solid #516347;padding:9px 13px;border-radius:8px;font-size:11px;transition:opacity .18s,transform .18s;z-index:8}.toast.show{opacity:1;transform:translate(-50%,0)}.mobile-back{display:none}
3
+ @media(max-width:1000px){.workspace{grid-template-columns:220px minmax(260px,320px) minmax(0,1fr);gap:8px;padding:9px}.detail-content{padding:20px 18px}}
4
+ @media(max-width:760px){.topbar{height:56px;padding:0 13px}.read-at{display:none}.workspace{display:block;height:auto;min-height:calc(100vh - 56px);padding:8px}.panel{display:none;min-height:calc(100vh - 72px)}body[data-level="projects"] #project-pane,body[data-level="list"] #list-pane,body[data-level="detail"] #detail-pane{display:flex}body[data-level="detail"] #detail-pane{display:block}.pane-heading{padding:16px}.source-status{padding:0 12px 10px}.mobile-back{display:inline-flex;align-items:center;gap:5px;border:0;background:none;color:var(--accent);padding:0 0 14px;font-size:11px;cursor:pointer}.detail-content{padding:16px 14px 30px}.detail-title{font-size:19px}.top-actions{gap:8px}.button{padding:7px 10px}.kv-grid{grid-template-columns:1fr}}
5
+ @media(prefers-reduced-motion:reduce){*,*::before,*::after{scroll-behavior:auto!important;transition-duration:.01ms!important}}
6
+ .source-extra{padding:0 9px 8px;color:#8f9a93;font-size:9px}.source-extra summary{cursor:pointer;color:#aeb7b1}.source-extra p{margin:5px 0 0;overflow-wrap:anywhere;line-height:1.55}.message.jump-highlight{background:#263321;border-left-color:var(--accent);box-shadow:0 0 0 1px #c5f36b55;transition:background .18s,box-shadow .18s}.error-state strong{display:block;color:#f5b2a6;font-size:12px;margin:0 0 5px}.error-state p{margin:4px 0}
7
+ @media(max-width:760px){body[data-level="detail"] #detail-pane{overflow:visible}}
8
+ .edit-row{display:flex;align-items:center;gap:8px;margin-top:9px;flex-wrap:wrap}.edit-row .kv-label{margin:0}.edit-row .field{width:auto;min-width:90px;max-width:170px}.edit-actions{display:flex;gap:8px;margin-top:11px;flex-wrap:wrap}.conflict-box{border:1px solid #6b4a33;background:#2a1d15;border-radius:8px;padding:11px;margin-top:9px;display:grid;gap:8px}.conflict-box strong{font-size:11px;color:#f0b593}
@@ -0,0 +1,373 @@
1
+ (() => {
2
+ "use strict";
3
+
4
+ const $ = (selector, root = document) => root.querySelector(selector);
5
+ const state = { projects: [], sources: {}, project: null, tab: "sessions", sessions: [], memories: [], session: null, memory: null, branch: null, pageAfter: null, pageCursors: [], page: null, compartments: [], notes: [], detailBusy: false, hideHistorian: true, fullOutputs: false, draftTimer: 0, writeToken: null, writeBusy: false };
6
+ const el = (tag, className, text) => { const node = document.createElement(tag); if (className) node.className = className; if (text !== undefined && text !== null) node.textContent = String(text); return node; };
7
+ const btn = (label, action, className = "button") => { const node = el("button", className, label); node.type = "button"; node.dataset.action = action; return node; };
8
+ const path = (project, suffix = "") => `/api/projects/${encodeURIComponent(project)}${suffix}`;
9
+ const formatTime = (value) => { if (value == null || value === "") return "时间未知"; const date = typeof value === "number" ? new Date(value) : new Date(value); return Number.isNaN(date.getTime()) ? String(value) : date.toLocaleString("zh-CN", { hour12: false }); };
10
+ const sessionTitle = (session) => session.title || (session.harness === "pi" ? "Pi 会话 · 无标题" : "OpenCode 2 会话 · 无标题");
11
+ const number = (value) => value == null ? "—" : new Intl.NumberFormat("zh-CN").format(value);
12
+ const setReadAt = (data) => { if (data?.readAt) $("#read-at").textContent = `读取于 ${formatTime(data.readAt)}`; };
13
+ async function api(url) {
14
+ const response = await fetch(url, { method: "GET", cache: "no-store", headers: { Accept: "application/json" } });
15
+ let data; try { data = await response.json(); } catch { throw new Error(`HTTP ${response.status}:响应不是有效 JSON`); }
16
+ setReadAt(data);
17
+ if (!response.ok) { const error = data?.error || {}; const err = new Error(error.message || `请求失败(HTTP ${response.status})`); err.code = error.code; err.status = error.status; throw err; }
18
+ return data;
19
+ }
20
+ function toast(message, ms = 2200) { const node = $("#toast"); node.textContent = message; node.classList.add("show"); clearTimeout(toast.timer); toast.timer = setTimeout(() => node.classList.remove("show"), ms); }
21
+ // ---------------------------------------------------------------------------
22
+ // Write side (P3): token gate + the three memory write actions.
23
+ async function ensureWriteToken() {
24
+ if (state.writeToken) return state.writeToken;
25
+ const data = await api("/api/session");
26
+ state.writeToken = data.writeToken;
27
+ return data.writeToken;
28
+ }
29
+ function setWriteBusy(busy) {
30
+ state.writeBusy = busy;
31
+ for (const node of document.querySelectorAll('[data-action="mem-save"],[data-action="mem-archive"],[data-action="mem-restore"]')) node.disabled = busy;
32
+ }
33
+ function writeFormSnapshot() {
34
+ const area = $("#memory-edit-content");
35
+ const importance = $("#memory-edit-importance");
36
+ const category = $("#memory-edit-category");
37
+ const payload = { expected: state.memoryDetail?.semanticSnapshot };
38
+ if (!payload.expected) throw new Error("缺少语义快照,请重新打开该记忆");
39
+ if (area && area.value !== state.memoryDetail.memory.content) payload.content = area.value;
40
+ if (importance && Number.isFinite(Number(importance.value)) && Number(importance.value) !== state.memoryDetail.memory.importance) payload.importance = Number(importance.value);
41
+ if (category && category.value && category.value !== state.memoryDetail.memory.category) payload.category = category.value;
42
+ return payload;
43
+ }
44
+ async function writeMemory(action, extra = {}) {
45
+ const memory = state.memory;
46
+ if (!memory || !state.project || state.writeBusy) return;
47
+ setWriteBusy(true);
48
+ try {
49
+ const token = await ensureWriteToken();
50
+ const response = await fetch(path(state.project.projectPath, `/memories/${memory.id}/${action}`), {
51
+ method: "POST",
52
+ headers: { "content-type": "application/json", "x-mmc-write-token": token },
53
+ body: JSON.stringify({ ...writeFormSnapshot(), ...extra }),
54
+ });
55
+ const data = await response.json().catch(() => ({}));
56
+ if (!response.ok) { handleWriteError(action, data); return; }
57
+ if (action === "restore" && data.notice) toast(`已恢复。${data.notice}`, 8000);
58
+ else toast(action === "edit" ? "修改已保存" : action === "archive" ? "已归档" : "已恢复");
59
+ await refresh();
60
+ } catch (error) { toast(`写入失败:${error.message}`); }
61
+ finally { setWriteBusy(false); }
62
+ }
63
+ function handleWriteError(action, data) {
64
+ const error = data?.error || {};
65
+ if (error.code === "conflict") {
66
+ toast("冲突:记忆已被其他修改更新,草稿已保留。请对比后重试。", 6000);
67
+ renderConflictBox(error.currentSnapshot);
68
+ return;
69
+ }
70
+ if (error.code === "duplicate") { toast(`重复内容:该项目已有相同内容记忆 #${error.duplicateId}`, 6000); return; }
71
+ if ((error.code || "").startsWith("restore-forbidden")) { toast(`无法恢复:${error.message}`, 6000); return; }
72
+ if (error.code === "source-unavailable") { toast("存储不可写(缺失或 schema 版本更高),详见数据源状态。", 6000); return; }
73
+ toast(`写入失败(HTTP ${error.code || "error"}):${error.message || "未知错误"}`, 6000);
74
+ }
75
+ function renderConflictBox(currentSnapshot) {
76
+ const root = $("#memory-write-status");
77
+ if (!root) return;
78
+ root.replaceChildren();
79
+ const box = el("div", "conflict-box");
80
+ box.append(el("strong", "", "服务端已有更新的语义快照"));
81
+ const grid = el("div", "kv-grid");
82
+ if (currentSnapshot) {
83
+ kv(grid, "状态", currentSnapshot.status);
84
+ kv(grid, "类别", currentSnapshot.category);
85
+ kv(grid, "内容哈希", currentSnapshot.contentHash);
86
+ kv(grid, "更新时间", formatTime(currentSnapshot.updatedAt));
87
+ }
88
+ box.append(grid);
89
+ box.append(el("p", "section-note", "本机草稿未动。点击“载入服务端最新”重读详情后再决定是否合并修改。"));
90
+ const reload = btn("载入服务端最新", "mem-reload", "link-button");
91
+ box.append(reload);
92
+ root.append(box);
93
+ }
94
+ function empty(parent, title, detail = "") { const box = el("div", "empty-state"); box.append(el("strong", "", title), el("span", "", detail)); parent.append(box); }
95
+ function loading(parent, label = "正在读取…") { parent.replaceChildren(el("div", "loading", label)); }
96
+ function errorBox(parent, error) { const box = el("div", "error-state"); box.textContent = error.message; if (error.status) box.append(statusDescription(error.status)); parent.append(box); }
97
+ function sourceText(status) {
98
+ if (!status) return ["未知", "warn"];
99
+ switch (status.kind) {
100
+ case "ok": return ["可读取", "ok"];
101
+ case "missing": return ["未找到", "bad"];
102
+ case "schema-fence-unsupported": return [`版本不兼容 · 库 ${status.persistedVersion} / 支持 ${status.latestSupportedVersion}`, "bad"];
103
+ case "store-generation-unsupported": return [`格式不兼容 · ${status.actual} / 需要 ${status.expected}`, "bad"];
104
+ case "read-failed": return ["读取失败", "bad"];
105
+ default: return [status.kind || "未知", "warn"];
106
+ }
107
+ }
108
+ function statusDescription(status) {
109
+ const text = status.detail || status.path || (status.kind === "schema-fence-unsupported" ? `数据库版本 ${status.persistedVersion} 高于当前支持的 ${status.latestSupportedVersion}。面板不会迁移或修改数据。` : status.kind === "store-generation-unsupported" ? `检测到 ${status.actual},当前只支持 ${status.expected}。该宿主原文已禁读,其他数据区仍可使用。` : "");
110
+ const details = el("details", "source-extra"); const summary = el("summary", "", status.kind === "missing" ? "查看路径与诊断信息" : status.kind === "schema-fence-unsupported" ? "查看版本详情" : "查看诊断信息"); details.append(summary, el("p", "source-detail", text ? `${status.kind}:${text}` : status.kind)); return details;
111
+ }
112
+ function renderSources() {
113
+ const root = $("#source-status"); root.replaceChildren();
114
+ for (const [key, label] of [["context", "mmc 存储"], ["oc2", "OpenCode 2"], ["pi", "Pi"]]) {
115
+ const status = state.sources[key]; const [text, tone] = sourceText(status);
116
+ const row = el("div", "source-row"); const left = el("span", "source-label"); left.append(el("i", `dot ${tone === "ok" ? "" : tone}`), el("span", "", label)); row.append(left, el("span", "source-state", text)); root.append(row);
117
+ if (status && status.kind !== "ok") root.append(statusDescription(status));
118
+ }
119
+ }
120
+ function renderProjects() {
121
+ const root = $("#projects"); root.replaceChildren(); $("#project-count").textContent = String(state.projects.length);
122
+ if (state.sources.context?.kind !== "ok") { empty(root, "项目列表不可用", "项目枚举依赖 mmc context 存储。其他可用数据源状态仍显示在上方。"); return; }
123
+ if (!state.projects.length) { empty(root, "还没有项目", "项目在 mmc 存储中出现后会列在这里。"); return; }
124
+ for (const project of state.projects) {
125
+ const card = btn("", "project", "project-card" + (state.project?.projectPath === project.projectPath ? " selected" : ""));
126
+ card.dataset.project = project.projectPath;
127
+ const identity = el("span", "project-path", project.projectPath); const meta = el("span", "project-meta"); meta.append(el("span", "", `${number(project.sessionCount)} 会话`), el("span", "", formatTime(project.lastActivityAt)));
128
+ const flags = el("span", "project-flags"); flags.append(el("span", "badge", `池 ${number(project.poolSize)}`)); if (project.memoryOnly) flags.append(el("span", "flag", "仅记忆"));
129
+ card.append(identity, meta, flags); root.append(card);
130
+ }
131
+ }
132
+ function setLevel(level) { document.body.dataset.level = level; }
133
+ async function loadProjects(keep = true) {
134
+ loading($("#projects"), "读取项目与数据源…");
135
+ try {
136
+ const data = await api("/api/projects"); state.projects = data.projects || []; state.sources = data.sources || {}; renderSources(); renderProjects();
137
+ if (keep && state.project) {
138
+ const match = state.projects.find((item) => item.projectPath === state.project.projectPath);
139
+ if (match) { state.project = match; await loadCurrent(); }
140
+ else { state.project = null; state.session = state.memory = null; renderProjects(); showNoSelection(); }
141
+ }
142
+ } catch (error) { state.projects = []; renderProjects(); $("#source-status").replaceChildren(el("div", "error-state", `项目读取失败:${error.message}`)); }
143
+ }
144
+ function showNoSelection() { $("#list-eyebrow").textContent = "选择项目"; $("#list-title").textContent = "审阅内容"; $("#items").replaceChildren(); $("#list-controls").replaceChildren(); $("#detail").replaceChildren(); const box = el("div", "detail-empty"); box.append(el("span", "empty-glyph", "⌁"), el("h2", "", state.project ? `选择一个${state.tab === "sessions" ? "会话" : "记忆"}` : "选择一个项目"), el("p", "", state.project ? "从中间列表选择一条记录查看详情。" : "从左侧选择项目,查看会话或记忆。")); $("#detail").append(box); }
145
+ function renderListControls() {
146
+ const root = $("#list-controls"); root.replaceChildren();
147
+ root.append(btn("← 项目", "back-projects", "mobile-back"));
148
+ if (state.tab === "sessions") {
149
+ const query = el("input", "field"); query.placeholder = "按标题搜索"; query.type = "search"; query.dataset.filter = "title";
150
+ const harness = el("select", "field"); harness.dataset.filter = "harness"; for (const [value, label] of [["", "全部宿主"], ["opencode2", "OpenCode 2"], ["pi", "Pi"]]) { const option = el("option", "", label); option.value = value; harness.append(option); }
151
+ const since = el("input", "field"); since.type = "date"; since.dataset.filter = "since"; since.setAttribute("aria-label", "从日期筛选");
152
+ const until = el("input", "field"); until.type = "date"; until.dataset.filter = "until"; until.setAttribute("aria-label", "至日期筛选");
153
+ const row1 = el("div", "control-row"); row1.append(query); const row2 = el("div", "control-row"); row2.append(harness); const row3 = el("div", "control-row"); row3.append(since, until);
154
+ const check = el("label", "checkline"); const checkbox = document.createElement("input"); checkbox.type = "checkbox"; checkbox.checked = state.hideHistorian; checkbox.dataset.filter = "historian"; check.append(checkbox, el("span", "", "隐藏 historian 派生会话"));
155
+ root.append(row1, row2, row3, check);
156
+ for (const filter of root.querySelectorAll("[data-filter]")) filter.addEventListener("input", renderItems);
157
+ harness.addEventListener("change", renderItems); checkbox.addEventListener("change", () => { state.hideHistorian = checkbox.checked; renderItems(); });
158
+ } else {
159
+ const query = el("input", "field"); query.type = "search"; query.placeholder = "搜索当前项目记忆"; query.dataset.filter = "memory-q"; query.value = state.memoryQuery || "";
160
+ const category = el("select", "field"); category.dataset.filter = "memory-category"; for (const [value, label] of [["", "全部类别"], ["PROJECT_RULES", "项目规则"], ["ARCHITECTURE", "架构"], ["CONSTRAINTS", "约束"], ["CONFIG_VALUES", "配置值"], ["NAMING", "命名"]]) { const option = el("option", "", label); option.value = value; category.append(option); }
161
+ const status = el("select", "field"); status.dataset.filter = "memory-status"; for (const [value, label] of [["unarchived", "未归档(默认)"], ["expired-unarchived", "过期未归档"], ["archived", "已归档"], ["all", "全部状态"]]) { const option = el("option", "", label); option.value = value; status.append(option); }
162
+ const row = el("div", "control-row"); row.append(query); const row2 = el("div", "control-row"); row2.append(category, status); const search = btn("检索", "search-memories", "button"); root.append(row, row2, search);
163
+ query.addEventListener("keydown", (event) => { if (event.key === "Enter") loadMemories(); }); category.addEventListener("change", loadMemories); status.addEventListener("change", loadMemories);
164
+ }
165
+ }
166
+ function renderItems() {
167
+ const root = $("#items"); root.replaceChildren();
168
+ if (state.tab === "sessions") {
169
+ const title = ($('[data-filter="title"]', $("#list-controls"))?.value || "").trim().toLocaleLowerCase(); const harness = $('[data-filter="harness"]', $("#list-controls"))?.value || "";
170
+ const since = $('[data-filter="since"]', $("#list-controls"))?.value; const until = $('[data-filter="until"]', $("#list-controls"))?.value; const hide = $('[data-filter="historian"]', $("#list-controls"))?.checked ?? state.hideHistorian;
171
+ const filtered = state.sessions.filter((s) => (!hide || s.derived !== "historian") && (!harness || s.harness === harness) && (!title || (s.title || "").toLocaleLowerCase().includes(title)) && (!since || (s.lastActivityAt ?? 0) >= new Date(`${since}T00:00:00`).getTime()) && (!until || (s.lastActivityAt ?? 0) <= new Date(`${until}T23:59:59`).getTime()));
172
+ if (!filtered.length) { empty(root, state.sessions.length ? "没有符合条件的会话" : "暂无会话", state.sessions.length ? "调整筛选条件试试。" : "这个项目当前没有登记会话;如有记忆,可切换到记忆查看。"); return; }
173
+ for (const session of filtered) renderSessionCard(root, session);
174
+ } else {
175
+ if (!state.memories.length) { empty(root, state.memoryQuery ? "没有匹配的记忆" : "没有记忆", state.memoryQuery ? "检索没有结果,不代表数据源故障。" : "尝试更换状态筛选或搜索词。"); return; }
176
+ for (const memory of state.memories) renderMemoryCard(root, memory);
177
+ }
178
+ }
179
+ function renderSessionCard(root, session) {
180
+ const active = state.session?.sessionId === session.sessionId;
181
+ const card = el("article", `item-card${active ? " selected" : ""}`);
182
+ card.append(el("span", "item-title", sessionTitle(session)));
183
+ const meta = el("span", "item-meta"); meta.append(el("span", "", session.harness === "pi" ? "Pi" : "OpenCode 2"), el("span", "", formatTime(session.lastActivityAt))); card.append(meta);
184
+ const foot = el("span", "item-sub", `${session.sessionId} · ${number(session.compartmentCount)} 段摘要`); card.append(foot);
185
+ if (session.derived) card.append(el("span", "badge", session.derived === "child" ? "子会话" : "historian"));
186
+ if (session.harness === "pi") {
187
+ const branches = session.piBranches || [];
188
+ const box = el("div", "branch-box"); box.addEventListener("click", (event) => event.stopPropagation());
189
+ const label = el("span", "item-sub", branches.length ? `Pi 分支文件 · ${branches.length}` : "Pi 原文文件未找到"); box.append(label);
190
+ for (const branch of branches) {
191
+ const b = btn(`${branch.fileName} · ${branch.directoryName}`, "branch", "link-button branch-button"); b.dataset.sid = session.sessionId; b.dataset.branch = branch.fileName; b.title = branch.headerTimestamp || "时间未知"; box.append(b);
192
+ }
193
+ card.append(box);
194
+ } else { const open = btn("打开会话审阅", "session", "link-button"); open.dataset.sid = session.sessionId; card.append(open); }
195
+ root.append(card);
196
+ }
197
+ function renderMemoryCard(root, memory) {
198
+ const card = btn("", "memory", `item-card${state.memory?.id === memory.id ? " selected" : ""}`); card.dataset.mid = memory.id;
199
+ card.append(el("span", "item-title", `#${memory.id} · ${memory.category}`));
200
+ const meta = el("span", "item-meta"); meta.append(el("span", "", `${memory.status || "状态未知"} · 重要度 ${memory.importance ?? "—"}`), el("span", "", formatTime(memory.updatedAt))); card.append(meta);
201
+ card.append(el("span", "item-snippet", memory.content)); if (memory.expiredUnarchived) card.append(el("span", "flag", "已过期未归档"));
202
+ if (memory.supersededByMemoryId) card.append(el("span", "badge", `被 #${memory.supersededByMemoryId} 取代`));
203
+ if (memory.mergedFrom) card.append(el("span", "badge", "合并记录")); root.append(card);
204
+ }
205
+ async function loadProject(project) {
206
+ state.project = project; state.session = null; state.memory = null; state.branch = null; state.pageAfter = null; renderProjects(); $("#list-eyebrow").textContent = project.projectPath; $("#list-title").textContent = "会话"; renderListControls(); loading($("#items")); showNoSelection(); setLevel("list"); await loadCurrent();
207
+ }
208
+ async function loadCurrent() { if (!state.project) return; if (state.tab === "sessions") await loadSessions(); else await loadMemories(); }
209
+ async function loadSessions() {
210
+ loading($("#items"), "读取会话列表…");
211
+ try { const data = await api(path(state.project.projectPath, "/sessions")); state.sessions = data.sessions || []; renderItems(); }
212
+ catch (error) { errorBox($("#items"), error); }
213
+ }
214
+ async function loadMemories() {
215
+ if (!state.project) return; const controls = $("#list-controls"); const query = $('[data-filter="memory-q"]', controls)?.value || ""; state.memoryQuery = query;
216
+ const category = $('[data-filter="memory-category"]', controls)?.value || ""; const status = $('[data-filter="memory-status"]', controls)?.value || "unarchived";
217
+ const params = new URLSearchParams({ status }); if (query.trim()) params.set("q", query.trim()); if (category) params.set("category", category);
218
+ loading($("#items"), "检索当前项目记忆…");
219
+ try { const data = await api(path(state.project.projectPath, `/memories?${params}`)); state.memories = data.memories || []; renderItems(); if (data.truncated) $("#items").append(el("div", "loading", "结果过多,仅显示当前页上限;请缩小搜索范围。")); }
220
+ catch (error) { errorBox($("#items"), error); }
221
+ }
222
+ function tabTo(tab) {
223
+ state.tab = tab; state.session = null; state.memory = null; state.branch = null; state.pageAfter = null;
224
+ $("#sessions-tab").classList.toggle("active", tab === "sessions"); $("#sessions-tab").setAttribute("aria-selected", String(tab === "sessions"));
225
+ $("#memories-tab").classList.toggle("active", tab === "memories"); $("#memories-tab").setAttribute("aria-selected", String(tab === "memories"));
226
+ $("#list-title").textContent = tab === "sessions" ? "会话" : "记忆"; renderListControls(); $("#detail").replaceChildren(); loadCurrent();
227
+ }
228
+ function section(parent, title, note = "") { const wrap = el("section", "section"); const head = el("div", "section-head"); head.append(el("h3", "", title)); if (note) head.append(el("span", "section-note", note)); wrap.append(head); parent.append(wrap); return wrap; }
229
+ function addMobileBack(parent, target, label) { const back = btn(`← ${label}`, `back-${target}`, "mobile-back"); parent.append(back); }
230
+ function showDetailLoading(label = "正在读取详情…") { const root = $("#detail"); root.replaceChildren(el("div", "loading", label)); }
231
+ async function openSession(session, branch = null, after = null) {
232
+ if (state.session?.sessionId !== session?.sessionId || state.branch !== branch || after === null) state.pageCursors = [];
233
+ state.session = session; state.memory = null; state.branch = branch; state.pageAfter = after; state.fullOutputs = false; state.detailBusy = true; renderItems(); setLevel("detail"); showDetailLoading();
234
+ const query = new URLSearchParams({ limit: "40" }); if (branch) query.set("branch", branch); if (after != null) query.set("after", String(after));
235
+ const suffix = `/sessions/${encodeURIComponent(session.sessionId)}`;
236
+ const calls = [api(path(state.project.projectPath, `${suffix}/compartments${branch ? `?branch=${encodeURIComponent(branch)}` : ""}`)), api(path(state.project.projectPath, `${suffix}/notes`)), api(path(state.project.projectPath, `${suffix}/messages?${query}`))];
237
+ const [compartments, notes, messages] = await Promise.all(calls.map((p) => p.then((value) => ({ value }), (error) => ({ error }))));
238
+ state.compartments = compartments.value?.compartments || []; state.compartmentsApproximate = compartments.value?.renderTierApproximate === true; state.notes = notes.value?.notes || []; state.page = messages.value || null; state.detailErrors = { compartments: compartments.error, notes: notes.error, messages: messages.error }; state.detailBusy = false; renderSessionDetail();
239
+ }
240
+ function renderSessionDetail() {
241
+ const root = $("#detail"); root.replaceChildren(); if (!state.session) return;
242
+ const wrap = el("div", "detail-content"); addMobileBack(wrap, "list", "返回列表");
243
+ const s = state.session; wrap.append(el("div", "detail-kicker", `${s.harness === "pi" ? "PI SESSION" : "OPENCODE 2 SESSION"}${state.branch ? " · BRANCH SELECTED" : ""}`));
244
+ wrap.append(el("h2", "detail-title", sessionTitle(s))); const meta = el("div", "detail-meta"); meta.append(el("span", "badge", s.sessionId), el("span", "badge", `${number(s.compartmentCount)} 个摘要段`), el("span", "", formatTime(s.lastActivityAt))); if (state.branch) meta.append(el("span", "badge", state.branch)); wrap.append(meta);
245
+ const comps = section(wrap, "摘要分段", "层级为审阅展示,不是存储字段");
246
+ if (state.detailErrors?.compartments) errorBox(comps, state.detailErrors.compartments);
247
+ else if (!state.compartments.length) empty(comps, "没有摘要分段");
248
+ else for (const c of state.compartments) renderCompartment(comps, c);
249
+ const notes = section(wrap, "会话笔记", `${state.notes.length} 条`);
250
+ if (state.detailErrors?.notes) errorBox(notes, state.detailErrors.notes); else if (!state.notes.length) empty(notes, "没有关联笔记");
251
+ else for (const n of state.notes) { const card = el("article", "note-card"); const head = el("div", "message-head"); head.append(el("span", "", `${n.type} · ${n.status}`), el("span", "", formatTime(n.createdAt))); card.append(head, el("p", "", n.content)); if (n.anchorOrdinal != null) { const jump = btn(`跳到原文第 ${n.anchorOrdinal} 条`, "jump", "link-button"); jump.dataset.anchor = n.anchorOrdinal; card.append(jump); } else card.append(el("span", "item-sub", "未锚定原文")); notes.append(card); }
252
+ const raw = section(wrap, "原文", state.page ? `${state.page.source}${state.page.branch ? ` · ${state.page.branch.fileName}` : ""}` : ""); raw.id = "raw-messages";
253
+ if (state.detailErrors?.messages) {
254
+ const error = state.detailErrors.messages;
255
+ if (error.code === "source-unavailable") { const unavailable = el("div", "error-state"); const [reason] = error.status ? sourceText(error.status) : [error.message]; unavailable.append(el("strong", "", "宿主原文不可读取"), el("p", "", `${reason}。摘要和笔记仍可单独审阅。`)); if (error.status) unavailable.append(statusDescription(error.status)); raw.append(unavailable); }
256
+ else errorBox(raw, error);
257
+ } else if (state.page) renderMessages(raw, state.page); else empty(raw, "暂无原文", "这个会话当前没有可读消息。");
258
+ root.append(wrap);
259
+ }
260
+ function renderCompartment(parent, c) {
261
+ const tier = c.renderTier === "full" || c.renderTier === "decayed" ? c.renderTier : null;
262
+ const tierText = tier ? `${tier}${state.compartmentsApproximate ? " · 参考" : ""}` : "层级未提供";
263
+ const card = el("article", "compartment"); const head = el("div", "comp-head"); head.append(el("div", "comp-title", `${String(c.sequence + 1).padStart(2, "0")} · ${c.title || "无标题"}`), el("span", "comp-meta", `层级 ${tierText} · 重要度 ${c.importance} · ${c.legacy ? "legacy" : "linked"}`)); card.append(head);
264
+ card.append(el("p", "comp-content", c.content)); const details = c.details || {}; const detailBox = el("div", "comp-details"); for (const key of ["p1", "p2", "p3", "p4"]) if (details[key]) detailBox.append(el("div", "prose", details[key])); if (detailBox.childNodes.length) card.append(detailBox);
265
+ const bounds = c.bounds || {}; const boundRow = el("div", "bound-row"); boundRow.append(el("span", "", "边界")); for (const [side, label] of [["start", "起点"], ["end", "终点"]]) {
266
+ const bound = bounds[side] || { state: "unresolved" }; const stateName = bound.state === "synth-resolved" ? "经别名解析" : bound.state === "resolved" ? "已关联" : bound.state === "unlinked" ? "无边界 id" : "边界无法解析";
267
+ boundRow.append(el("span", "badge", `${label} · ${stateName}`));
268
+ if (bound.state === "resolved" || bound.state === "synth-resolved") { const jump = btn(`跳到 ${label} #${bound.anchor}`, "jump", "link-button"); jump.dataset.anchor = bound.anchor; boundRow.append(jump); if (bound.aliasedTo) boundRow.append(el("span", "section-note", `→ ${bound.aliasedTo}`)); }
269
+ }
270
+ card.append(boundRow); parent.append(card);
271
+ }
272
+ function renderMessages(parent, page) {
273
+ if (!page.rows?.length) empty(parent, "没有原文消息");
274
+ else for (const row of page.rows) {
275
+ const card = el("article", `message ${row.role === "user" ? "user" : "assistant"}`); card.dataset.messageKey = String(row.key); const head = el("div", "message-head"); head.append(el("span", "", `${row.role} · #${row.key} · ${row.id}`), el("span", "", formatTime(row.timeCreated))); card.append(head);
276
+ for (const part of row.parts || []) renderPart(card, part);
277
+ parent.append(card);
278
+ }
279
+ const pager = el("div", "pager"); if (state.pageAfter != null) pager.append(btn("← 上一页", "previous-page")); else pager.append(el("span", "section-note", "从会话开头"));
280
+ pager.append(el("span", "section-note", page.rows?.length ? `${page.rows[0].key} — ${page.rows[page.rows.length - 1].key}` : "空页"));
281
+ if (page.hasMore) pager.append(btn("下一页 →", "next-page")); else pager.append(el("span", "section-note", "已到末尾")); parent.append(pager);
282
+ }
283
+ function renderPart(parent, part) {
284
+ if (part && typeof part === "object" && part.type === "tool") {
285
+ const tool = el("div", "tool-block"); const head = el("div", "tool-head"); head.append(el("span", "", `${part.tool || "工具"}${part.callID ? ` · ${part.callID}` : ""}`)); const output = part.state?.output; const total = part.state?.outputTotalLength; const truncated = part.state?.outputTruncated;
286
+ if (truncated && !state.fullOutputs) { const toggle = btn(`展开完整输出 · ${number(total)} 字符`, "expand-output", "link-button"); head.append(toggle); }
287
+ else if (state.fullOutputs && total != null) head.append(el("span", "", `${number(total)} 字符`));
288
+ tool.append(head); if (output != null) tool.append(el("pre", "tool-output", typeof output === "string" ? output : JSON.stringify(output, null, 2)));
289
+ parent.append(tool); return;
290
+ }
291
+ if (part && typeof part === "object" && typeof part.text === "string") { parent.append(el("p", "message-body", part.text)); return; }
292
+ const unknown = el("details", "unknown-part"); unknown.append(el("summary", "item-sub", `其他内容 · ${part?.type || "未知类型"}`), el("pre", "tool-output", JSON.stringify(part, null, 2))); parent.append(unknown);
293
+ }
294
+ async function openMemory(memory) {
295
+ state.memory = memory; state.session = null; renderItems(); setLevel("detail"); showDetailLoading("读取记忆详情与证据…");
296
+ try { const data = await api(path(state.project.projectPath, `/memories/${encodeURIComponent(memory.id)}`)); state.memoryDetail = data; renderMemoryDetail(); }
297
+ catch (error) { $("#detail").replaceChildren(); errorBox($("#detail"), error); }
298
+ }
299
+ function kv(parent, label, value) { const item = el("div", "kv"); item.append(el("span", "kv-label", label), el("span", "kv-value", value == null || value === "" ? "—" : String(value))); parent.append(item); }
300
+ function renderMemoryDetail() {
301
+ const root = $("#detail"); root.replaceChildren(); const data = state.memoryDetail; if (!data) return; const m = data.memory; const wrap = el("div", "detail-content"); addMobileBack(wrap, "list", "返回列表"); wrap.append(el("div", "detail-kicker", `PROJECT MEMORY · #${m.id}`), el("h2", "detail-title", m.category));
302
+ const meta = el("div", "detail-meta"); meta.append(el("span", "badge", m.status || "状态未知")); if (m.expiredUnarchived) meta.append(el("span", "flag", "已过期未归档")); meta.append(el("span", "badge", `重要度 ${m.importance ?? "—"}`)); wrap.append(meta);
303
+ const content = section(wrap, "记忆正文"); content.append(el("div", "memory-content", m.content));
304
+ const fields = section(wrap, "字段", "存储中的当前值"); const grid = el("div", "kv-grid"); kv(grid, "类别", m.category); kv(grid, "状态", m.status); kv(grid, "有效期", formatTime(m.expiresAt)); kv(grid, "验证状态", m.verificationStatus); kv(grid, "范围", m.scope); kv(grid, "可共享", m.shareable ? "是" : "否"); kv(grid, "创建", formatTime(m.createdAt)); kv(grid, "更新", formatTime(m.updatedAt)); kv(grid, "取代记忆", m.supersededByMemoryId ? `#${m.supersededByMemoryId}` : "—"); kv(grid, "合并来源", m.mergedFrom); fields.append(grid);
305
+ const evidence = section(wrap, "证据", `${data.verifications?.length || 0} 核验 · ${data.mutationLog?.length || 0} 流水`);
306
+ if (!data.verifications?.length) empty(evidence, "暂无核验记录", "只展示存储中已有证据,不补造追溯信息。");
307
+ else for (const v of data.verifications) { const card = el("article", "evidence-card"); const head = el("div", "message-head"); head.append(el("span", "", v.filePath), el("span", "badge", v.verified ? "已核验" : "已映射 · 未核验")); card.append(head); const sub = el("div", "item-sub", `映射 ${formatTime(v.mappedAt)} · 核验 ${v.verified ? formatTime(v.verifiedAt) : "尚未核验"} · 来源 ${v.mappingOrigin}`); card.append(sub); evidence.append(card); }
308
+ const log = data.mutationLog || []; if (!log.length) empty(evidence, "暂无变更流水");
309
+ else { const terminal = log.filter((x) => x.terminal); const rest = log.filter((x) => !x.terminal); for (const [group, rows] of [["该记忆的终局事件", terminal], ["其他变更(时间倒序)", rest]]) if (rows.length) { evidence.append(el("div", "section-note", group)); for (const row of rows) { const card = el("article", "evidence-card"); const head = el("div", "message-head"); head.append(el("span", "", `${row.mutationType} · #${row.id}`), el("span", "", formatTime(row.queuedAt))); card.append(head); const detail = [`目标 #${row.targetMemoryId}`, row.supersededById ? `后继 #${row.supersededById}` : "", row.category || ""].filter(Boolean).join(" · "); card.append(el("p", "", detail)); if (row.newContent) card.append(el("div", "item-sub", row.newContent)); evidence.append(card); } } }
310
+ const editable = section(wrap, "编辑", "写入走 POST 端点:Origin + 写令牌 + 语义快照 CAS(D21/D26)"); editable.id = "memory-write-status";
311
+ const key = `mmc-memory-draft:${m.id}`;
312
+ const area = el("textarea", "field draft-box"); area.id = "memory-edit-content"; area.setAttribute("aria-label", "记忆编辑草稿"); area.placeholder = "编辑记忆内容;输入自动保存在本机草稿。";
313
+ let saved = ""; try { saved = localStorage.getItem(key) || ""; } catch { saved = ""; }
314
+ area.value = saved || m.content;
315
+ const status = el("span", "saved", saved && saved !== m.content ? "已恢复未提交的本机草稿" : "与本机草稿一致");
316
+ area.addEventListener("input", () => { try { localStorage.setItem(key, area.value); status.textContent = "草稿已保存"; } catch { status.textContent = "浏览器存储不可用,草稿未保存"; } });
317
+ const formRow = el("div", "edit-row");
318
+ const importance = el("input", "field"); importance.id = "memory-edit-importance"; importance.type = "number"; importance.min = "1"; importance.max = "100"; importance.value = String(m.importance ?? 50); importance.setAttribute("aria-label", "重要度(1-100)");
319
+ const category = el("select", "field"); category.id = "memory-edit-category"; category.setAttribute("aria-label", "类别");
320
+ for (const option of [m.category, "PROJECT_RULES", "ARCHITECTURE", "CONSTRAINTS", "CONFIG_VALUES", "NAMING"].filter((value, index, list) => value && list.indexOf(value) === index)) { const node = el("option", "", option); node.value = option; if (option === m.category) node.selected = true; category.append(node); }
321
+ formRow.append(el("span", "kv-label", "重要度"), importance, el("span", "kv-label", "类别"), category);
322
+ const buttons = el("div", "edit-actions");
323
+ if (m.status !== "archived") buttons.append(btn("归档", "mem-archive", "button")); else buttons.append(btn("恢复为 active", "mem-restore", "button"));
324
+ buttons.append(btn("保存修改", "mem-save", "button button-primary"));
325
+ editable.append(area, status, formRow, buttons, el("p", "section-note", "保存修改仅提交变更字段;归档/恢复不改动内容与类别。恢复立即写库,现役会话要等下一次自然 HARD fold 才可见。"));
326
+ root.append(wrap);
327
+ }
328
+ async function jumpTo(anchor) {
329
+ if (!state.session) return;
330
+ state.pageAfter = Math.max(0, Number(anchor) - 1); state.pageCursors = []; state.fullOutputs = false;
331
+ await openSession(state.session, state.branch, state.pageAfter);
332
+ const target = $(`[data-message-key="${Number(anchor)}"]`, $("#raw-messages"));
333
+ (target || $("#raw-messages"))?.scrollIntoView({ behavior: "smooth", block: target ? "center" : "start" });
334
+ if (target) { target.classList.add("jump-highlight"); setTimeout(() => target.classList.remove("jump-highlight"), 1800); }
335
+ }
336
+ async function refresh() {
337
+ const button = $("#refresh"); button.disabled = true; button.textContent = "刷新中…";
338
+ try {
339
+ const selectedPath = state.project?.projectPath; const sessionId = state.session?.sessionId; const selectedMemoryId = state.memory?.id; const branch = state.branch; const after = state.pageAfter;
340
+ await loadProjects(false);
341
+ state.project = state.projects.find((item) => item.projectPath === selectedPath) || null; renderProjects();
342
+ if (!state.project) { state.session = state.memory = null; showNoSelection(); }
343
+ else {
344
+ await loadCurrent();
345
+ if (state.tab === "sessions" && sessionId) { const current = state.sessions.find((item) => item.sessionId === sessionId); if (current) await openSession(current, branch, after); else { state.session = null; showNoSelection(); } }
346
+ if (state.tab === "memories" && selectedMemoryId != null) { const current = state.memories.find((item) => item.id === selectedMemoryId); if (current) await openMemory(current); else { state.memory = null; showNoSelection(); } }
347
+ }
348
+ toast("已重新读取当前视图");
349
+ } finally { button.disabled = false; button.textContent = "刷新数据"; }
350
+ }
351
+ document.addEventListener("click", async (event) => {
352
+ const target = event.target.closest("[data-action]"); if (!target) return; const action = target.dataset.action;
353
+ try {
354
+ if (action === "project") await loadProject(state.projects.find((p) => p.projectPath === target.dataset.project));
355
+ else if (action === "session") await openSession(state.sessions.find((s) => s.sessionId === target.dataset.sid), null, null);
356
+ else if (action === "branch") { const session = state.sessions.find((s) => s.sessionId === target.dataset.sid); await openSession(session, target.dataset.branch, null); }
357
+ else if (action === "memory") await openMemory(state.memories.find((m) => m.id === Number(target.dataset.mid)));
358
+ else if (action === "jump") await jumpTo(target.dataset.anchor);
359
+ else if (action === "next-page" && state.page?.hasMore) { state.pageCursors.push(state.pageAfter); await openSession(state.session, state.branch, state.page.nextAfter); }
360
+ else if (action === "previous-page") await openSession(state.session, state.branch, state.pageCursors.pop() ?? null);
361
+ else if (action === "expand-output") { state.fullOutputs = true; const params = new URLSearchParams({ limit: "40", fullOutputs: "1" }); if (state.branch) params.set("branch", state.branch); if (state.pageAfter != null) params.set("after", String(state.pageAfter)); const suffix = `/sessions/${encodeURIComponent(state.session.sessionId)}/messages?${params}`; try { state.page = await api(path(state.project.projectPath, suffix)); renderSessionDetail(); } catch (error) { toast(`完整输出读取失败:${error.message}`); } }
362
+ else if (action === "search-memories") await loadMemories();
363
+ else if (action === "mem-save") await writeMemory("edit");
364
+ else if (action === "mem-archive") { if (confirm("归档这条记忆?(可在详情页恢复)")) await writeMemory("archive"); }
365
+ else if (action === "mem-restore") await writeMemory("restore");
366
+ else if (action === "mem-reload") { if (state.memory) await openMemory(state.memories.find((item) => item.id === state.memory.id) || state.memory); }
367
+ else if (action === "back-list") setLevel("list"); else if (action === "back-projects") setLevel("projects");
368
+ } catch (error) { toast(error.message); }
369
+ });
370
+ $("#sessions-tab").addEventListener("click", () => tabTo("sessions")); $("#memories-tab").addEventListener("click", () => tabTo("memories")); $("#refresh").addEventListener("click", refresh);
371
+ document.addEventListener("keydown", (event) => { if (event.key === "Escape" && document.body.dataset.level === "detail") setLevel("list"); });
372
+ setLevel("projects"); loadProjects(false);
373
+ })();
package/package.json CHANGED
@@ -1,6 +1,87 @@
1
1
  {
2
- "name": "@retaw106/mmc-pi",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
2
+ "name": "@retaw106/mmc-pi",
3
+ "version": "1.2.2",
4
+ "type": "module",
5
+ "description": "Pi and OMP coding agent extension for Magic Context — cross-session memory and context management",
6
+ "main": "dist/index.js",
7
+ "license": "MIT",
8
+ "author": "ualtinok",
9
+ "repository": {
10
+ "type": "git",
11
+ "url": "git+https://github.com/retaw106/moon-magic-context.git",
12
+ "directory": "packages/pi-plugin"
13
+ },
14
+ "publishConfig": {
15
+ "access": "public"
16
+ },
17
+ "keywords": [
18
+ "pi",
19
+ "pi-coding-agent",
20
+ "extension",
21
+ "context",
22
+ "memory",
23
+ "magic-context",
24
+ "ai",
25
+ "llm"
26
+ ],
27
+ "files": [
28
+ "dist",
29
+ "README.md"
30
+ ],
31
+ "//": "The build intentionally bundles typebox (no --external typebox) and keeps it a regular dependency. OMP's extension loader rewrites every bare `typebox` import to its own omptype-based shim, whose schemas are callable objects rather than JSON Schema; they fail structuredClone during tool registration, so an external typebox import stops Magic Context loading on OMP. src/typebox-bundling.test.ts guards this.",
32
+ "scripts": {
33
+ "build": "bun ../../scripts/check-bun.mjs && bun ../../scripts/clean-dist-chunks.mjs dist index.js historian-calibration-extension.js && rm -rf dist/web && bun build src/index.ts src/historian-calibration-extension.ts --outdir dist --target node --format esm --splitting --external @earendil-works/pi-coding-agent --external @earendil-works/pi-tui --external node:sqlite && bun run build:web",
34
+ "build:web": "bun build src/web/main.ts --outfile dist/web/main.js --target node --format esm --external @earendil-works/pi-coding-agent --external @earendil-works/pi-tui --external node:sqlite && mkdir -p dist/web/ui && cp ../plugin/src/web/ui/* dist/web/ui/",
35
+ "build:e2e-argv": "bun build src/subagent-runner.ts --outfile ../../tests/docker/.generated/subagent-runner-e2e.mjs --target node --format esm --external node:sqlite",
36
+ "typecheck": "tsc --noEmit",
37
+ "test": "bun ../../scripts/check-bun.mjs && bun install --frozen-lockfile && bun test --parallel --timeout 30000",
38
+ "test:serial": "bun install --frozen-lockfile && bun test --timeout 30000",
39
+ "lint": "bun ../../scripts/check-bun.mjs && biome check src",
40
+ "lint:fix": "biome check --write src",
41
+ "format": "biome format --write src",
42
+ "format:check": "biome format src",
43
+ "clean": "rm -rf dist",
44
+ "prepublishOnly": "bun run build"
45
+ },
46
+ "dependencies": {
47
+ "ai-tokenizer": "^1.0.6",
48
+ "comment-json": "^5.0.0",
49
+ "typebox": "^1.3.1",
50
+ "zod": "^4.1.8"
51
+ },
52
+ "devDependencies": {
53
+ "@biomejs/biome": "^2.5.1",
54
+ "@earendil-works/pi-ai": "0.83.0",
55
+ "@earendil-works/pi-coding-agent": "0.83.0",
56
+ "@earendil-works/pi-tui": "0.83.0",
57
+ "@types/better-sqlite3": "^7.6.13",
58
+ "@types/bun": "^1.3.10",
59
+ "@types/node": "^22.20.0",
60
+ "pi-ai-086": "npm:@earendil-works/pi-ai@0.86.0",
61
+ "pi-coding-agent-086": "npm:@earendil-works/pi-coding-agent@0.86.0",
62
+ "pi-coding-agent-087": "npm:@earendil-works/pi-coding-agent@0.87.1",
63
+ "typescript": "^5.8.0"
64
+ },
65
+ "peerDependencies": {
66
+ "@earendil-works/pi-coding-agent": ">=0.80.2",
67
+ "@earendil-works/pi-tui": ">=0.80.2"
68
+ },
69
+ "peerDependenciesMeta": {
70
+ "@earendil-works/pi-coding-agent": {
71
+ "optional": true
72
+ },
73
+ "@earendil-works/pi-tui": {
74
+ "optional": true
75
+ }
76
+ },
77
+ "exports": {
78
+ ".": {
79
+ "import": "./dist/index.js"
80
+ }
81
+ },
82
+ "pi": {
83
+ "extensions": [
84
+ "./dist/index.js"
85
+ ]
86
+ }
87
+ }