@retaw106/mmc-pi 1.2.2 → 2.0.0
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.
- package/README.md +49 -98
- package/dist/cli.js +7095 -0
- package/dist/config-init.js +5595 -0
- package/dist/{index-65ee50rm.js → index-dx9vhsy8.js} +3 -2
- package/dist/{index-ztb4k1mt.js → index-kxehmcmt.js} +4163 -3958
- package/dist/index.js +1847 -1812
- package/dist/{message-time-backfill-h02fzzp8.js → message-time-backfill-wnxmrd09.js} +1 -1
- package/dist/{read-session-chunk-af3sdznq.js → read-session-chunk-7m31yz4a.js} +1 -1
- package/dist/web/main.js +850 -150
- package/dist/web/ui/index.html +1 -0
- package/dist/web/ui/panel.css +5 -0
- package/dist/web/ui/panel.js +220 -70
- package/package.json +7 -2
package/dist/web/ui/index.html
CHANGED
|
@@ -16,6 +16,7 @@
|
|
|
16
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
17
|
</header>
|
|
18
18
|
<main class="workspace" id="workspace">
|
|
19
|
+
<div id="fixture-notice" class="fixture-notice" role="status" aria-live="polite" hidden></div>
|
|
19
20
|
<aside class="rail panel" id="project-pane" aria-label="项目">
|
|
20
21
|
<div class="pane-heading"><div><p class="eyebrow">WORKSPACE</p><h1>项目</h1></div><span id="project-count" class="count-pill">—</span></div>
|
|
21
22
|
<div id="source-status" class="source-status" aria-live="polite"></div>
|
package/dist/web/ui/panel.css
CHANGED
|
@@ -3,6 +3,11 @@
|
|
|
3
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
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
5
|
@media(prefers-reduced-motion:reduce){*,*::before,*::after{scroll-behavior:auto!important;transition-duration:.01ms!important}}
|
|
6
|
+
.fixture-notice{grid-column:1 / -1;display:flex;align-items:center;gap:10px;flex-wrap:wrap;padding:10px 13px;border:1px solid #665331;border-radius:10px;background:linear-gradient(100deg,#302719,#211d17);color:#eed8a5;font-size:11px;box-shadow:0 8px 20px #0002}.fixture-notice[hidden]{display:none}.fixture-notice-title{color:#f6dfad}.fixture-notice-copy{color:#c9b98e}.fixture-commands{display:inline-flex;align-items:center;gap:7px;flex-wrap:wrap;color:#a99a76}.fixture-command{display:inline-flex;align-items:center;gap:4px;padding:3px 6px;border:1px solid #5c4b2e;border-radius:5px;background:#1d1a15}.fixture-command-host{color:#d9c797}.fixture-command code{font:10px var(--mono);color:#f0e2bd;overflow-wrap:anywhere}.project-name{display:block;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:#f0f4ec;font-size:12px;font-weight:700;letter-spacing:-.01em}.project-path{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:#aab5ac;font-size:10px!important;line-height:1.5!important}.project-card{overflow:hidden}.list-pane .eyebrow{max-width:25ch;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
6
7
|
.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
8
|
@media(max-width:760px){body[data-level="detail"] #detail-pane{overflow:visible}}
|
|
8
9
|
.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}
|
|
10
|
+
.message-head{align-items:center}.message-role{color:#dfe9d9;font:600 10px var(--mono);letter-spacing:.04em}.message-aux{display:flex;align-items:center;gap:7px;min-width:0;flex:1;color:#7f8b83;font-weight:400}.message-index{color:#a6b09f}.message-id{min-width:0;max-width:min(32ch,45%);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;opacity:.62}.message-time{flex:0 0 auto;color:#7f8b83}.message-body{font-size:12px;line-height:1.78}.tool-block{background:#111714;transition:border-color .16s,background .16s}.tool-block.expanded{border-color:#536549;background:#141b17}.tool-head{align-items:center;min-height:42px}.tool-title{display:flex;align-items:baseline;gap:7px;min-width:0;flex:1;overflow:hidden}.tool-name{color:#d9e6d3;font-weight:700;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.tool-call{color:#7f8c83;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.tool-toggle{flex:0 0 auto;min-height:32px;padding:5px 0 5px 9px;border:0;background:none;color:#c5f36b;font:9px var(--mono);cursor:pointer;white-space:nowrap}.tool-toggle:hover{color:#e0ff9d;text-decoration:underline}.tool-toggle:active{transform:scale(.96)}.tool-output[hidden]{display:none}.tool-empty{color:#7f8b83;font-family:inherit}.unknown-part summary{cursor:pointer;min-height:32px;display:flex;align-items:center}@media(max-width:760px){.message-head{flex-wrap:wrap;row-gap:5px}.message-aux{order:3;flex-basis:100%}.message-time{margin-left:auto}.tool-head{flex-wrap:wrap}.tool-toggle{min-height:38px;white-space:normal;text-align:right;max-width:100%}}
|
|
11
|
+
.session-card[role="button"]{cursor:pointer}.session-card[role="button"]:focus-visible{outline:2px solid var(--accent);outline-offset:-2px}.comp-details-fold,.comp-raw,.ungrouped-raw{margin-top:10px;border-top:1px solid #27312b;padding-top:8px}.comp-details-fold>summary,.comp-raw>summary,.ungrouped-raw>summary{cursor:pointer;list-style:none}.comp-details-fold>summary::-webkit-details-marker,.comp-raw>summary::-webkit-details-marker,.ungrouped-raw>summary::-webkit-details-marker{display:none}.comp-details-fold>summary::before,.comp-raw>summary::before,.ungrouped-raw>summary::before{content:"+";display:inline-block;width:17px;color:var(--accent)}.comp-details-fold[open]>summary::before,.comp-raw[open]>summary::before,.ungrouped-raw[open]>summary::before{content:"-"}.link-summary{color:var(--accent);font-size:10px;line-height:1.6}.comp-details{display:grid;gap:9px;margin-top:9px}.prose{display:grid;gap:3px;padding:8px 10px;border-radius:7px;background:#111714;color:#c4cec3;font-size:11px;line-height:1.65;white-space:pre-wrap}.prose-label{font:600 9px var(--mono);color:#9eae95;letter-spacing:.08em}.comp-message-panel{display:grid;gap:8px;margin-top:10px}.comp-message-panel>.message{margin:0}.boundary-warning{margin:8px 0 0;color:#e8bd79;font-size:10px;line-height:1.6}.ungrouped-raw{margin-top:14px;padding:12px 13px;border:1px solid #5a4930;border-radius:9px;background:#1a1712}.ungrouped-intro{margin:0 0 8px}.ungrouped-raw .comp-message-panel{border-top:1px solid #4a3c29;padding-top:8px}.comp-raw .pager,.ungrouped-raw .pager{margin-bottom:2px}
|
|
12
|
+
.detail-meta{min-width:0;max-width:100%}.detail-meta>.badge,.detail-meta>.flag{min-width:0;max-width:100%;white-space:normal;overflow-wrap:anywhere;word-break:break-word;text-align:left}.tool-block,.tool-output{min-width:0;max-width:100%}@media(max-width:760px){.detail-pane{min-width:0;overflow-x:hidden}.detail-content{width:100%;min-width:0;max-width:100%;margin:0}.tool-output{overflow-x:auto}}
|
|
13
|
+
.comp-counts{margin-top:2px;color:#77837b;font:9px var(--mono);cursor:help}.expandable-text{white-space:pre-wrap;overflow-wrap:anywhere}.expandable-preview,.expandable-full{white-space:inherit}.expandable-preview[hidden],.expandable-full[hidden]{display:none}.preview-toggle{border:0;background:none;color:var(--accent);padding:4px 3px;min-height:32px;cursor:pointer;font:10px var(--mono);vertical-align:baseline}.preview-toggle:hover{text-decoration:underline}.preview-toggle:active{transform:scale(.96)}
|
package/dist/web/ui/panel.js
CHANGED
|
@@ -2,11 +2,75 @@
|
|
|
2
2
|
"use strict";
|
|
3
3
|
|
|
4
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,
|
|
5
|
+
const state = { projects: [], sources: {}, fixtureMode: false, project: null, tab: "sessions", sessions: [], memories: [], session: null, memory: null, branch: null, compartments: [], notes: [], segmentViews: new Map(), ungroupedView: null, detailToken: 0, detailBusy: false, hideHistorian: true, draftTimer: 0, writeToken: null, writeBusy: false };
|
|
6
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
7
|
const btn = (label, action, className = "button") => { const node = el("button", className, label); node.type = "button"; node.dataset.action = action; return node; };
|
|
8
8
|
const path = (project, suffix = "") => `/api/projects/${encodeURIComponent(project)}${suffix}`;
|
|
9
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 PREVIEW_CODEPOINTS = 200;
|
|
11
|
+
function expandableText(parent, value, className, tag = "div") {
|
|
12
|
+
const text = value == null ? "" : String(value); const codepoints = Array.from(text); const box = el(tag, `expandable-text${className ? ` ${className}` : ""}`);
|
|
13
|
+
if (codepoints.length <= PREVIEW_CODEPOINTS) { box.textContent = text; parent.append(box); return box; }
|
|
14
|
+
const preview = el("span", "expandable-preview", codepoints.slice(0, PREVIEW_CODEPOINTS).join("")); const full = el("span", "expandable-full", text); full.hidden = true;
|
|
15
|
+
const toggle = btn("...", "toggle-preview", "preview-toggle"); toggle.setAttribute("aria-expanded", "false"); toggle.setAttribute("aria-label", "展开全文"); box.append(preview, full, toggle); parent.append(box); return box;
|
|
16
|
+
}
|
|
17
|
+
function setPreviewExpanded(toggle, expanded) {
|
|
18
|
+
const box = toggle.closest(".expandable-text"); if (!box) return;
|
|
19
|
+
const preview = $(".expandable-preview", box); const full = $(".expandable-full", box); if (preview) preview.hidden = expanded; if (full) full.hidden = !expanded; toggle.textContent = expanded ? "收起" : "..."; toggle.setAttribute("aria-expanded", String(expanded)); toggle.setAttribute("aria-label", expanded ? "收起全文" : "展开全文");
|
|
20
|
+
}
|
|
21
|
+
function formatCharCount(value) { return Number.isSafeInteger(value) && value >= 0 ? `${number(value)} 字符` : "未统计"; }
|
|
22
|
+
function displayPath(project) {
|
|
23
|
+
return typeof project?.displayPath === "string" && project.displayPath.trim() ? project.displayPath : null;
|
|
24
|
+
}
|
|
25
|
+
function pathLeaf(value) {
|
|
26
|
+
const original = String(value || "");
|
|
27
|
+
if (!original) return "";
|
|
28
|
+
if (/^[\\/]+$/.test(original) || /^[A-Za-z]:[\\/]?$/.test(original)) return original;
|
|
29
|
+
const withoutTrailingSeparators = original.replace(/[\\/]+$/, "");
|
|
30
|
+
return withoutTrailingSeparators.match(/[^\\/]+$/)?.[0] || original;
|
|
31
|
+
}
|
|
32
|
+
function shortIdentity(value) {
|
|
33
|
+
const identity = String(value || "");
|
|
34
|
+
return identity.length > 26 ? `${identity.slice(0, 17)}…${identity.slice(-8)}` : identity;
|
|
35
|
+
}
|
|
36
|
+
function projectName(project) {
|
|
37
|
+
const pathValue = displayPath(project);
|
|
38
|
+
if (pathValue) return pathLeaf(pathValue) || "未识别项目";
|
|
39
|
+
const identity = shortIdentity(project?.projectPath);
|
|
40
|
+
return identity ? `未识别项目 · ${identity}` : "未识别项目";
|
|
41
|
+
}
|
|
42
|
+
function projectTooltip(project) {
|
|
43
|
+
const identity = String(project?.projectPath || "未知身份");
|
|
44
|
+
const pathValue = displayPath(project);
|
|
45
|
+
return pathValue ? `路径:${pathValue}\n内部身份:${identity}` : `内部身份:${identity}`;
|
|
46
|
+
}
|
|
47
|
+
function setListEyebrow(project) {
|
|
48
|
+
const eyebrow = $("#list-eyebrow");
|
|
49
|
+
if (!project) {
|
|
50
|
+
eyebrow.textContent = "选择项目";
|
|
51
|
+
eyebrow.removeAttribute("title");
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
eyebrow.textContent = projectName(project);
|
|
55
|
+
eyebrow.title = projectTooltip(project);
|
|
56
|
+
}
|
|
57
|
+
function renderFixtureNotice() {
|
|
58
|
+
const root = $("#fixture-notice");
|
|
59
|
+
if (!root) return;
|
|
60
|
+
root.replaceChildren();
|
|
61
|
+
root.hidden = !state.fixtureMode;
|
|
62
|
+
if (!state.fixtureMode) return;
|
|
63
|
+
root.append(el("strong", "fixture-notice-title", "演示数据,未读取真实数据库"));
|
|
64
|
+
root.append(el("span", "fixture-notice-copy", "当前面板使用隔离夹具,仅用于演示和浏览器验证。"));
|
|
65
|
+
const commands = el("span", "fixture-commands");
|
|
66
|
+
commands.append(el("span", "fixture-command-label", "读取配置中的真实数据:"));
|
|
67
|
+
for (const [label, command] of [["Pi", "bun packages/pi-plugin/src/web/main.ts"], ["OpenCode 2", "bun packages/plugin/src/web/main.ts"]]) {
|
|
68
|
+
const item = el("span", "fixture-command");
|
|
69
|
+
item.append(el("span", "fixture-command-host", `${label} `), el("code", "", command));
|
|
70
|
+
commands.append(item);
|
|
71
|
+
}
|
|
72
|
+
root.append(commands);
|
|
73
|
+
}
|
|
10
74
|
const sessionTitle = (session) => session.title || (session.harness === "pi" ? "Pi 会话 · 无标题" : "OpenCode 2 会话 · 无标题");
|
|
11
75
|
const number = (value) => value == null ? "—" : new Intl.NumberFormat("zh-CN").format(value);
|
|
12
76
|
const setReadAt = (data) => { if (data?.readAt) $("#read-at").textContent = `读取于 ${formatTime(data.readAt)}`; };
|
|
@@ -124,16 +188,19 @@
|
|
|
124
188
|
for (const project of state.projects) {
|
|
125
189
|
const card = btn("", "project", "project-card" + (state.project?.projectPath === project.projectPath ? " selected" : ""));
|
|
126
190
|
card.dataset.project = project.projectPath;
|
|
127
|
-
|
|
191
|
+
card.title = projectTooltip(project);
|
|
192
|
+
const identity = el("span", "project-name", projectName(project));
|
|
193
|
+
const pathLine = el("span", "project-path", displayPath(project) || `内部身份:${project.projectPath || "未知身份"}`); pathLine.title = projectTooltip(project);
|
|
194
|
+
const meta = el("span", "project-meta"); meta.append(el("span", "", `${number(project.sessionCount)} 会话`), el("span", "", formatTime(project.lastActivityAt)));
|
|
128
195
|
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);
|
|
196
|
+
card.append(identity, pathLine, meta, flags); root.append(card);
|
|
130
197
|
}
|
|
131
198
|
}
|
|
132
199
|
function setLevel(level) { document.body.dataset.level = level; }
|
|
133
200
|
async function loadProjects(keep = true) {
|
|
134
201
|
loading($("#projects"), "读取项目与数据源…");
|
|
135
202
|
try {
|
|
136
|
-
const data = await api("/api/projects"); state.projects = data.projects || []; state.sources = data.sources || {}; renderSources(); renderProjects();
|
|
203
|
+
const data = await api("/api/projects"); state.projects = data.projects || []; state.sources = data.sources || {}; state.fixtureMode = data.fixtureMode === true; renderFixtureNotice(); renderSources(); renderProjects();
|
|
137
204
|
if (keep && state.project) {
|
|
138
205
|
const match = state.projects.find((item) => item.projectPath === state.project.projectPath);
|
|
139
206
|
if (match) { state.project = match; await loadCurrent(); }
|
|
@@ -141,7 +208,7 @@
|
|
|
141
208
|
}
|
|
142
209
|
} catch (error) { state.projects = []; renderProjects(); $("#source-status").replaceChildren(el("div", "error-state", `项目读取失败:${error.message}`)); }
|
|
143
210
|
}
|
|
144
|
-
function showNoSelection() {
|
|
211
|
+
function showNoSelection() { setListEyebrow(state.project); $("#list-title").textContent = "审阅内容"; $("#items").replaceChildren(); $("#list-controls").replaceChildren(); const detail = $("#detail"); detail.classList.add("detail-empty"); 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
212
|
function renderListControls() {
|
|
146
213
|
const root = $("#list-controls"); root.replaceChildren();
|
|
147
214
|
root.append(btn("← 项目", "back-projects", "mobile-back"));
|
|
@@ -178,20 +245,23 @@
|
|
|
178
245
|
}
|
|
179
246
|
function renderSessionCard(root, session) {
|
|
180
247
|
const active = state.session?.sessionId === session.sessionId;
|
|
181
|
-
const
|
|
248
|
+
const canOpenCard = true;
|
|
249
|
+
const card = el("article", `item-card session-card${active ? " selected" : ""}`);
|
|
250
|
+
card.dataset.sid = session.sessionId;
|
|
251
|
+
if (canOpenCard) { card.dataset.action = "session"; card.tabIndex = 0; card.setAttribute("role", "button"); card.setAttribute("aria-label", `打开会话:${sessionTitle(session)}`); }
|
|
182
252
|
card.append(el("span", "item-title", sessionTitle(session)));
|
|
183
253
|
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
254
|
const foot = el("span", "item-sub", `${session.sessionId} · ${number(session.compartmentCount)} 段摘要`); card.append(foot);
|
|
185
255
|
if (session.derived) card.append(el("span", "badge", session.derived === "child" ? "子会话" : "historian"));
|
|
186
256
|
if (session.harness === "pi") {
|
|
187
257
|
const branches = session.piBranches || [];
|
|
188
|
-
const box = el("div", "branch-box");
|
|
258
|
+
const box = el("div", "branch-box");
|
|
189
259
|
const label = el("span", "item-sub", branches.length ? `Pi 分支文件 · ${branches.length}` : "Pi 原文文件未找到"); box.append(label);
|
|
190
260
|
for (const branch of branches) {
|
|
191
261
|
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
262
|
}
|
|
193
263
|
card.append(box);
|
|
194
|
-
}
|
|
264
|
+
}
|
|
195
265
|
root.append(card);
|
|
196
266
|
}
|
|
197
267
|
function renderMemoryCard(root, memory) {
|
|
@@ -203,7 +273,7 @@
|
|
|
203
273
|
if (memory.mergedFrom) card.append(el("span", "badge", "合并记录")); root.append(card);
|
|
204
274
|
}
|
|
205
275
|
async function loadProject(project) {
|
|
206
|
-
state.project = project; state.session = null; state.memory = null; state.branch = null; state.
|
|
276
|
+
state.detailToken += 1; state.project = project; state.session = null; state.memory = null; state.branch = null; state.segmentViews = new Map(); state.ungroupedView = null; renderProjects(); setListEyebrow(project); $("#list-title").textContent = "会话"; renderListControls(); loading($("#items")); showNoSelection(); setLevel("list"); await loadCurrent();
|
|
207
277
|
}
|
|
208
278
|
async function loadCurrent() { if (!state.project) return; if (state.tab === "sessions") await loadSessions(); else await loadMemories(); }
|
|
209
279
|
async function loadSessions() {
|
|
@@ -220,76 +290,135 @@
|
|
|
220
290
|
catch (error) { errorBox($("#items"), error); }
|
|
221
291
|
}
|
|
222
292
|
function tabTo(tab) {
|
|
223
|
-
state.tab = tab; state.session = null; state.memory = null; state.branch = null; state.
|
|
293
|
+
state.detailToken += 1; state.tab = tab; state.session = null; state.memory = null; state.branch = null; state.segmentViews = new Map(); state.ungroupedView = null;
|
|
224
294
|
$("#sessions-tab").classList.toggle("active", tab === "sessions"); $("#sessions-tab").setAttribute("aria-selected", String(tab === "sessions"));
|
|
225
295
|
$("#memories-tab").classList.toggle("active", tab === "memories"); $("#memories-tab").setAttribute("aria-selected", String(tab === "memories"));
|
|
226
296
|
$("#list-title").textContent = tab === "sessions" ? "会话" : "记忆"; renderListControls(); $("#detail").replaceChildren(); loadCurrent();
|
|
227
297
|
}
|
|
228
298
|
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
299
|
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
|
|
232
|
-
if (
|
|
233
|
-
state.session
|
|
234
|
-
|
|
300
|
+
function showDetailLoading(label = "正在读取详情…") { const root = $("#detail"); root.classList.add("detail-empty"); root.replaceChildren(el("div", "loading", label)); }
|
|
301
|
+
async function openSession(session, branch = null) {
|
|
302
|
+
if (!session) return;
|
|
303
|
+
const sameSession = state.session?.sessionId === session.sessionId && state.branch === branch;
|
|
304
|
+
if (!sameSession) { state.segmentViews = new Map(); state.ungroupedView = null; }
|
|
305
|
+
const token = ++state.detailToken;
|
|
306
|
+
state.session = session; state.memory = null; state.branch = branch; state.detailBusy = true; renderItems(); setLevel("detail"); showDetailLoading();
|
|
307
|
+
if (session.harness === "pi" && !branch) { state.compartments = []; state.notes = []; state.detailErrors = {}; state.detailBusy = false; renderSessionDetail(); toast("Pi 会话需要先选择分支;未猜测默认分支。", 5000); return; }
|
|
235
308
|
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`))
|
|
237
|
-
const [compartments, notes
|
|
238
|
-
|
|
309
|
+
const calls = [api(path(state.project.projectPath, `${suffix}/compartments${branch ? `?branch=${encodeURIComponent(branch)}` : ""}`)), api(path(state.project.projectPath, `${suffix}/notes`))];
|
|
310
|
+
const [compartments, notes] = await Promise.all(calls.map((p) => p.then((value) => ({ value }), (error) => ({ error }))));
|
|
311
|
+
if (token !== state.detailToken || state.session?.sessionId !== session.sessionId || state.branch !== branch) return;
|
|
312
|
+
state.compartments = compartments.value?.compartments || []; state.compartmentsApproximate = compartments.value?.renderTierApproximate === true; state.notes = notes.value?.notes || []; state.detailErrors = { compartments: compartments.error, notes: notes.error }; state.detailBusy = false; renderSessionDetail();
|
|
239
313
|
}
|
|
240
314
|
function renderSessionDetail() {
|
|
241
|
-
const root = $("#detail"); root.replaceChildren(); if (!state.session) return;
|
|
315
|
+
const root = $("#detail"); root.classList.remove("detail-empty"); root.replaceChildren(); if (!state.session) return;
|
|
242
316
|
const wrap = el("div", "detail-content"); addMobileBack(wrap, "list", "返回列表");
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
317
|
+
const s = state.session; wrap.append(el("div", "detail-kicker", `${s.harness === "pi" ? "PI SESSION" : "OPENCODE 2 SESSION"}${state.branch ? " · BRANCH SELECTED" : ""}`));
|
|
318
|
+
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);
|
|
319
|
+
if (s.harness === "pi" && !state.branch) { const prompt = section(wrap, "选择 Pi 分支", "Pi 原文与摘要按分支独立读取"); prompt.append(el("p", "boundary-warning", "请回到会话卡,选择具体分支文件后再查看摘要和原文。面板不会猜测默认分支。")); root.append(wrap); return; }
|
|
320
|
+
const comps = section(wrap, "摘要分段", "层级为审阅展示,不是存储字段");
|
|
246
321
|
if (state.detailErrors?.compartments) errorBox(comps, state.detailErrors.compartments);
|
|
247
322
|
else if (!state.compartments.length) empty(comps, "没有摘要分段");
|
|
248
323
|
else for (const c of state.compartments) renderCompartment(comps, c);
|
|
249
324
|
const notes = section(wrap, "会话笔记", `${state.notes.length} 条`);
|
|
250
325
|
if (state.detailErrors?.notes) errorBox(notes, state.detailErrors.notes); else if (!state.notes.length) empty(notes, "没有关联笔记");
|
|
251
|
-
|
|
252
|
-
const
|
|
253
|
-
if (
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
} else if (state.page) renderMessages(raw, state.page); else empty(raw, "暂无原文", "这个会话当前没有可读消息。");
|
|
258
|
-
root.append(wrap);
|
|
326
|
+
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); expandableText(card, n.content, "note-content", "p"); 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); }
|
|
327
|
+
const invalid = state.compartments.filter((c) => !compartmentRange(c));
|
|
328
|
+
if (invalid.length) renderUngroupedSection(wrap, "边界未确认的原文", "这些消息不能安全归属到摘要段", invalid.map((c) => `第 ${number(c.sequence + 1)} 段:${boundaryReason(c)}`).join(";"));
|
|
329
|
+
else if (!state.compartments.length) renderUngroupedSection(wrap, "会话原文", "没有摘要段,原文仍可按需读取", "");
|
|
330
|
+
else { const ranges = state.compartments.map(compartmentRange).filter(Boolean); const tailStart = Math.max(...ranges.map((range) => range.end)) + 1; renderUngroupedSection(wrap, "未归摘要尾部原文", "摘要边界之后的消息不自动归属到任何分段,仍按需读取", "", tailStart - 1); }
|
|
331
|
+
root.append(wrap);
|
|
259
332
|
}
|
|
260
333
|
function renderCompartment(parent, c) {
|
|
261
334
|
const tier = c.renderTier === "full" || c.renderTier === "decayed" ? c.renderTier : null;
|
|
262
335
|
const tierText = tier ? `${tier}${state.compartmentsApproximate ? " · 参考" : ""}` : "层级未提供";
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
336
|
+
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);
|
|
337
|
+
const countLine = el("div", "comp-counts", `原文 ${formatCharCount(c.messageCharCount)} · 默认摘要 ${formatCharCount(c.summaryCharCount)}`); countLine.title = "统计正文与工具输出全文,不含工具输入、图片附件、技能及其他元数据"; countLine.setAttribute("aria-label", `${countLine.textContent};统计正文与工具输出全文,不含工具输入、图片附件、技能及其他元数据`); card.append(countLine); expandableText(card, c.content, "comp-content", "p"); const details = c.details || {}; const detailKeys = ["p1", "p2", "p3", "p4"].filter((key) => details[key]); if (detailKeys.length) { const detailFold = el("details", "comp-details-fold"); detailFold.append(el("summary", "link-summary", "其他摘要版本")); const detailBox = el("div", "comp-details"); for (const key of detailKeys) { const item = el("div", "prose"); item.append(el("strong", "prose-label", key.toUpperCase())); expandableText(item, details[key], "prose-text", "span"); detailBox.append(item); } detailFold.append(detailBox); card.append(detailFold); }
|
|
338
|
+
const bounds = c.bounds || {}; const boundRow = el("div", "bound-row"); boundRow.append(el("span", "", "边界")); for (const [side, label] of [["start", "起点"], ["end", "终点"]]) {
|
|
266
339
|
const bound = bounds[side] || { state: "unresolved" }; const stateName = bound.state === "synth-resolved" ? "经别名解析" : bound.state === "resolved" ? "已关联" : bound.state === "unlinked" ? "无边界 id" : "边界无法解析";
|
|
267
340
|
boundRow.append(el("span", "badge", `${label} · ${stateName}`));
|
|
268
|
-
|
|
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);
|
|
341
|
+
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}`)); }
|
|
278
342
|
}
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
}
|
|
283
|
-
function
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
343
|
+
card.append(boundRow);
|
|
344
|
+
const range = compartmentRange(c); if (range) { const view = getSegmentView(c); const raw = el("details", "comp-raw"); raw.dataset.compartmentKey = segmentKey(c); raw.open = view.open; raw.append(el("summary", "link-summary", `展开该段原文 · ${range.start}–${range.end}`)); const body = el("div", "comp-message-panel"); raw.append(body); raw.addEventListener("toggle", () => { view.open = raw.open; if (raw.open) { if (view.skipToggleRequest) view.skipToggleRequest = false; else loadSegmentMessages(c, segmentKey(c), body); } }); card.append(raw); } else card.append(el("p", "boundary-warning", "原文归属:未分组。请在下方“边界未确认的原文”中按需读取。")); parent.append(card);
|
|
345
|
+
}
|
|
346
|
+
function segmentKey(c) { return String(c.id ?? `sequence-${c.sequence}`); }
|
|
347
|
+
function numericBoundary(bound) { return (bound?.state === "resolved" || bound?.state === "synth-resolved") && Number.isSafeInteger(bound.anchor) ? bound.anchor : null; }
|
|
348
|
+
function compartmentRange(c) { const start = numericBoundary(c.bounds?.start); const end = numericBoundary(c.bounds?.end); return start !== null && end !== null && start <= end ? { start, end } : null; }
|
|
349
|
+
function boundaryReason(c) { const bounds = c.bounds || {}; return ["start", "end"].map((side) => { const value = bounds[side]?.state; return `${side === "start" ? "起点" : "终点"}:${value === "unlinked" ? "无边界 id" : value === "synth-resolved" || value === "resolved" ? "数字边界无效" : "边界无法解析"}`; }).join(";"); }
|
|
350
|
+
function getSegmentView(c) { const key = segmentKey(c); let view = state.segmentViews.get(key); if (!view) { const range = compartmentRange(c); view = { open: false, after: range ? range.start - 1 : null, cursors: [], page: null, loading: false, error: null, fullOutputs: false, openToolOutputs: new Set(), request: 0 }; state.segmentViews.set(key, view); } return view; }
|
|
351
|
+
function renderUngroupedSection(parent, title, note, reason, initialAfter = null) {
|
|
352
|
+
if (!state.ungroupedView) state.ungroupedView = { open: false, after: initialAfter, cursors: [], page: null, loading: false, error: null, fullOutputs: false, openToolOutputs: new Set(), request: 0 };
|
|
353
|
+
const view = state.ungroupedView; const raw = el("details", "ungrouped-raw"); raw.id = "raw-messages"; raw.open = view.open; raw.append(el("summary", "link-summary", title)); const intro = el("div", "ungrouped-intro"); if (note) intro.append(el("p", "section-note", note)); if (reason) intro.append(el("p", "boundary-warning", reason)); raw.append(intro); const body = el("div", "comp-message-panel"); raw.append(body); raw.addEventListener("toggle", () => { view.open = raw.open; if (raw.open) loadRawFallback(body); }); parent.append(raw);
|
|
354
|
+
}
|
|
355
|
+
function renderMessages(parent, page, scope = {}) {
|
|
356
|
+
if (!page.rows?.length) empty(parent, "没有原文消息");
|
|
357
|
+
else for (const row of page.rows) {
|
|
358
|
+
const card = el("article", `message ${row.role === "user" ? "user" : "assistant"}`); card.dataset.messageKey = String(row.key); const head = el("div", "message-head");
|
|
359
|
+
const role = row.role === "user" ? "用户" : row.role === "assistant" ? "助手" : row.role || "未知角色";
|
|
360
|
+
head.append(el("strong", "message-role", role));
|
|
361
|
+
const identity = el("span", "message-aux");
|
|
362
|
+
identity.append(el("span", "message-index", `消息 #${row.key}`));
|
|
363
|
+
const messageId = String(row.id || "无 ID"); const idLabel = messageId.length > 32 ? `${messageId.slice(0, 15)}…${messageId.slice(-10)}` : messageId;
|
|
364
|
+
const id = el("span", "message-id", idLabel); id.title = `消息 ID:${messageId}`; identity.append(id); head.append(identity, el("time", "message-time", formatTime(row.timeCreated))); card.append(head);
|
|
365
|
+
for (const [index, part] of (row.parts || []).entries()) renderPart(card, part, row, index, scope);
|
|
366
|
+
parent.append(card);
|
|
367
|
+
}
|
|
368
|
+
const view = scope.view; const isSegment = Boolean(scope.compartment); const pager = el("div", "pager"); if (view.after != null && view.cursors.length) { const previous = btn("← 上一页", isSegment ? "previous-segment-page" : "previous-raw-page"); if (isSegment) previous.dataset.segmentKey = scope.key; pager.append(previous); } else pager.append(el("span", "section-note", "从范围开头"));
|
|
369
|
+
pager.append(el("span", "section-note", page.rows?.length ? `${page.rows[0].key} — ${page.rows[page.rows.length - 1].key}` : "空页"));
|
|
370
|
+
if (page.hasMore) { const next = btn("下一页 →", isSegment ? "next-segment-page" : "next-raw-page"); if (isSegment) next.dataset.segmentKey = scope.key; pager.append(next); } else pager.append(el("span", "section-note", "已到范围末尾")); parent.append(pager);
|
|
371
|
+
}
|
|
372
|
+
function setToolBlockOpen(block, open) {
|
|
373
|
+
block.classList.toggle("expanded", open);
|
|
374
|
+
const output = $(".tool-output", block); if (output) output.hidden = !open;
|
|
375
|
+
const toggle = $('[data-action="expand-output"]', block); if (!toggle) return;
|
|
376
|
+
toggle.setAttribute("aria-expanded", String(open)); toggle.setAttribute("aria-label", `${toggle.dataset.toolName || "工具"}:${open ? "收起输出" : "展开输出"}`); toggle.textContent = open ? "收起输出" : toggle.dataset.closedLabel;
|
|
377
|
+
}
|
|
378
|
+
function messageQuery(view, range, fullOutputs = false) {
|
|
379
|
+
const params = new URLSearchParams({ limit: "40" }); if (state.branch) params.set("branch", state.branch); if (view.after != null) params.set("after", String(view.after)); if (range) params.set("end", String(range.end)); if (fullOutputs) params.set("fullOutputs", "1"); return params;
|
|
380
|
+
}
|
|
381
|
+
function renderSegmentMessages(parent, c, key) {
|
|
382
|
+
const view = getSegmentView(c); parent.replaceChildren(); if (view.loading) { parent.append(el("div", "loading", "读取该段原文…")); return; }
|
|
383
|
+
if (view.error) { errorBox(parent, view.error); return; }
|
|
384
|
+
if (!view.page) { parent.append(el("p", "section-note", compartmentRange(c) ? "展开后按该段边界读取原文。" : "边界未确认,原文不会伪造归属。")); return; }
|
|
385
|
+
renderMessages(parent, view.page, { key, view, compartment: c, parent });
|
|
386
|
+
}
|
|
387
|
+
async function loadSegmentMessages(c, key, parent, options = {}) {
|
|
388
|
+
const range = compartmentRange(c); if (!range) { renderSegmentMessages(parent, c, key); return; }
|
|
389
|
+
const view = getSegmentView(c); const token = state.detailToken; const request = ++view.request; view.loading = true; view.error = null; renderSegmentMessages(parent, c, key);
|
|
390
|
+
try {
|
|
391
|
+
const full = options.fullOutputs === true || view.fullOutputs === true; const page = await api(path(state.project.projectPath, `/sessions/${encodeURIComponent(state.session.sessionId)}/messages?${messageQuery(view, range, full)}`));
|
|
392
|
+
if (token !== state.detailToken || request !== view.request || !state.session) return;
|
|
393
|
+
view.page = page; view.loading = false; view.fullOutputs = full; renderSegmentMessages(parent, c, key);
|
|
394
|
+
if (options.focusKey) { const block = [...parent.querySelectorAll(".tool-block")].find((node) => node.dataset.toolKey === options.focusKey); if (block) setToolBlockOpen(block, true); }
|
|
395
|
+
if (options.anchor != null) focusMessage(parent, options.anchor);
|
|
396
|
+
} catch (error) { if (token !== state.detailToken || request !== view.request) return; view.loading = false; view.error = error; renderSegmentMessages(parent, c, key); }
|
|
397
|
+
}
|
|
398
|
+
async function loadRawFallback(parent, anchor = null, fullOutputs = false, focusKey = null) {
|
|
399
|
+
const view = state.ungroupedView; if (!view || !state.session) return; const token = state.detailToken; const request = ++view.request; if (anchor != null) { view.after = Math.max(0, Number(anchor) - 1); view.cursors = []; }
|
|
400
|
+
view.loading = true; view.error = null; parent.replaceChildren(el("div", "loading", "读取未分组原文…"));
|
|
401
|
+
try {
|
|
402
|
+
const page = await api(path(state.project.projectPath, `/sessions/${encodeURIComponent(state.session.sessionId)}/messages?${messageQuery(view, null, fullOutputs || view.fullOutputs)}`));
|
|
403
|
+
if (token !== state.detailToken || request !== view.request || !state.session) return;
|
|
404
|
+
view.page = page; view.loading = false; view.fullOutputs = view.fullOutputs || fullOutputs; parent.replaceChildren(); renderMessages(parent, page, { key: "ungrouped", view, parent }); if (focusKey) { const block = [...parent.querySelectorAll(".tool-block")].find((node) => node.dataset.toolKey === focusKey); if (block) setToolBlockOpen(block, true); } if (anchor != null) focusMessage(parent, anchor);
|
|
405
|
+
} catch (error) { if (token !== state.detailToken || request !== view.request) return; view.loading = false; view.error = error; parent.replaceChildren(); errorBox(parent, error); }
|
|
406
|
+
}
|
|
407
|
+
function focusMessage(parent, anchor) { const target = $(`[data-message-key="${Number(anchor)}"]`, parent); if (!target) return false; target.scrollIntoView({ behavior: "smooth", block: "center" }); target.classList.add("jump-highlight"); setTimeout(() => target.classList.remove("jump-highlight"), 1800); return true; }
|
|
408
|
+
async function loadFullOutputs(toolKey, scope) {
|
|
409
|
+
if (!scope?.view || !scope.compartment) return;
|
|
410
|
+
scope.view.openToolOutputs.add(toolKey); await loadSegmentMessages(scope.compartment, scope.key, scope.parent, { fullOutputs: true, focusKey: toolKey });
|
|
411
|
+
}
|
|
412
|
+
function renderPart(parent, part, row, index, scope = {}) {
|
|
413
|
+
if (part && typeof part === "object" && part.type === "tool") {
|
|
414
|
+
const toolKey = `${row.key}:${index}`; const openTools = scope.view.openToolOutputs; const isOpen = openTools.has(toolKey); const tool = el("div", "tool-block"); tool.dataset.toolKey = toolKey; const head = el("div", "tool-head"); const title = el("span", "tool-title"); title.append(el("strong", "tool-name", part.tool || "工具")); if (part.callID) title.append(el("span", "tool-call", part.callID)); head.append(title);
|
|
415
|
+
const output = part.state?.output; const total = part.state?.outputTotalLength; const truncated = part.state?.outputTruncated && !scope.view.fullOutputs; const closedLabel = truncated ? `读取并展开完整输出 · ${number(total)} 字符` : `展开输出${total != null ? ` · ${number(total)} 字符` : ""}`; const toggle = btn(isOpen ? "收起输出" : closedLabel, "expand-output", "tool-toggle"); toggle.dataset.toolKey = toolKey; toggle.dataset.toolName = part.tool || "工具"; toggle.dataset.closedLabel = closedLabel; if (truncated) toggle.dataset.requiresFullOutput = "true"; toggle.setAttribute("aria-expanded", String(isOpen)); toggle.setAttribute("aria-label", `${part.tool || "工具"}:${isOpen ? "收起输出" : "展开输出"}`); head.append(toggle); tool.append(head);
|
|
416
|
+
if (output != null) expandableText(tool, typeof output === "string" ? output : JSON.stringify(output, null, 2), "tool-output", "pre"); else tool.append(el("div", "tool-output tool-empty", "无输出"));
|
|
417
|
+
setToolBlockOpen(tool, isOpen); if (isOpen) tool.classList.add("expanded");
|
|
289
418
|
parent.append(tool); return;
|
|
290
419
|
}
|
|
291
|
-
|
|
292
|
-
|
|
420
|
+
if (part && typeof part === "object" && typeof part.text === "string") { expandableText(parent, part.text, "message-body", "p"); return; }
|
|
421
|
+
const unknown = el("details", "unknown-part"); unknown.append(el("summary", "item-sub", `其他内容 · ${part?.type || "未知类型"}`)); expandableText(unknown, JSON.stringify(part, null, 2), "tool-output", "pre"); parent.append(unknown);
|
|
293
422
|
}
|
|
294
423
|
async function openMemory(memory) {
|
|
295
424
|
state.memory = memory; state.session = null; renderItems(); setLevel("detail"); showDetailLoading("读取记忆详情与证据…");
|
|
@@ -297,8 +426,8 @@
|
|
|
297
426
|
catch (error) { $("#detail").replaceChildren(); errorBox($("#detail"), error); }
|
|
298
427
|
}
|
|
299
428
|
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
|
-
|
|
301
|
-
|
|
429
|
+
function renderMemoryDetail() {
|
|
430
|
+
const root = $("#detail"); root.classList.remove("detail-empty"); 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
431
|
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
432
|
const content = section(wrap, "记忆正文"); content.append(el("div", "memory-content", m.content));
|
|
304
433
|
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);
|
|
@@ -325,24 +454,28 @@
|
|
|
325
454
|
editable.append(area, status, formRow, buttons, el("p", "section-note", "保存修改仅提交变更字段;归档/恢复不改动内容与类别。恢复立即写库,现役会话要等下一次自然 HARD fold 才可见。"));
|
|
326
455
|
root.append(wrap);
|
|
327
456
|
}
|
|
457
|
+
async function openCompartmentAt(c, anchor) {
|
|
458
|
+
const key = segmentKey(c); const node = [...document.querySelectorAll("[data-compartment-key]")].find((item) => item.dataset.compartmentKey === key); if (!node) return false;
|
|
459
|
+
const raw = node; const body = $(".comp-message-panel", raw); if (!body) return false; const view = getSegmentView(c); const wasOpen = raw.open; if (!wasOpen) { view.skipToggleRequest = true; raw.open = true; } view.open = true; view.after = Math.max(0, Number(anchor) - 1); view.cursors = []; await loadSegmentMessages(c, key, body, { anchor }); return focusMessage(body, anchor);
|
|
460
|
+
}
|
|
328
461
|
async function jumpTo(anchor) {
|
|
329
462
|
if (!state.session) return;
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
463
|
+
const value = Number(anchor); if (!Number.isSafeInteger(value)) return;
|
|
464
|
+
const matches = state.compartments.filter((c) => { const range = compartmentRange(c); return range && value >= range.start && value <= range.end; });
|
|
465
|
+
if (matches.length) { await Promise.all(matches.map((c) => openCompartmentAt(c, value))); return; }
|
|
466
|
+
const fallback = $("#raw-messages"); const body = fallback && $(".comp-message-panel", fallback); if (body && state.ungroupedView) { fallback.open = true; state.ungroupedView.open = true; await loadRawFallback(body, value); if (!focusMessage(body, value)) toast(`消息 #${value} 未在未分组页中找到`); return; }
|
|
467
|
+
toast(`消息 #${value} 未能归属到有效摘要段,未安全跳转`);
|
|
335
468
|
}
|
|
336
469
|
async function refresh() {
|
|
337
470
|
const button = $("#refresh"); button.disabled = true; button.textContent = "刷新中…";
|
|
338
471
|
try {
|
|
339
|
-
const selectedPath = state.project?.projectPath; const sessionId = state.session?.sessionId; const selectedMemoryId = state.memory?.id; const branch = state.branch;
|
|
472
|
+
const selectedPath = state.project?.projectPath; const sessionId = state.session?.sessionId; const selectedMemoryId = state.memory?.id; const branch = state.branch;
|
|
340
473
|
await loadProjects(false);
|
|
341
474
|
state.project = state.projects.find((item) => item.projectPath === selectedPath) || null; renderProjects();
|
|
342
475
|
if (!state.project) { state.session = state.memory = null; showNoSelection(); }
|
|
343
476
|
else {
|
|
344
477
|
await loadCurrent();
|
|
345
|
-
if (state.tab === "sessions" && sessionId) { const current = state.sessions.find((item) => item.sessionId === sessionId); if (current) await openSession(current, branch
|
|
478
|
+
if (state.tab === "sessions" && sessionId) { const current = state.sessions.find((item) => item.sessionId === sessionId); if (current) await openSession(current, branch); else { state.session = null; showNoSelection(); } }
|
|
346
479
|
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
480
|
}
|
|
348
481
|
toast("已重新读取当前视图");
|
|
@@ -351,14 +484,31 @@
|
|
|
351
484
|
document.addEventListener("click", async (event) => {
|
|
352
485
|
const target = event.target.closest("[data-action]"); if (!target) return; const action = target.dataset.action;
|
|
353
486
|
try {
|
|
487
|
+
if (action === "toggle-preview") { setPreviewExpanded(target, target.getAttribute("aria-expanded") !== "true"); return; }
|
|
354
488
|
if (action === "project") await loadProject(state.projects.find((p) => p.projectPath === target.dataset.project));
|
|
355
|
-
|
|
356
|
-
|
|
489
|
+
else if (action === "session") await openSession(state.sessions.find((s) => s.sessionId === target.dataset.sid), null);
|
|
490
|
+
else if (action === "branch") { const session = state.sessions.find((s) => s.sessionId === target.dataset.sid); event.stopPropagation(); await openSession(session, target.dataset.branch); }
|
|
357
491
|
else if (action === "memory") await openMemory(state.memories.find((m) => m.id === Number(target.dataset.mid)));
|
|
358
492
|
else if (action === "jump") await jumpTo(target.dataset.anchor);
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
493
|
+
else if (action === "next-raw-page" || action === "previous-raw-page") {
|
|
494
|
+
const view = state.ungroupedView; const body = $("#raw-messages .comp-message-panel"); if (!view || !body) return;
|
|
495
|
+
if (action === "next-raw-page" && view.page?.hasMore) { view.cursors.push(view.after); view.after = view.page.nextAfter; }
|
|
496
|
+
else if (action === "previous-raw-page" && view.cursors.length) view.after = view.cursors.pop();
|
|
497
|
+
else return;
|
|
498
|
+
await loadRawFallback(body, null, view.fullOutputs);
|
|
499
|
+
}
|
|
500
|
+
else if (action === "next-segment-page" || action === "previous-segment-page") {
|
|
501
|
+
const c = state.compartments.find((item) => segmentKey(item) === target.dataset.segmentKey); if (!c) return; const view = getSegmentView(c); const body = [...document.querySelectorAll("[data-compartment-key]")].find((item) => item.dataset.compartmentKey === target.dataset.segmentKey)?.querySelector(".comp-message-panel"); if (!body) return;
|
|
502
|
+
if (action === "next-segment-page" && view.page?.hasMore) { view.cursors.push(view.after); view.after = view.page.nextAfter; }
|
|
503
|
+
else if (action === "previous-segment-page" && view.cursors.length) view.after = view.cursors.pop();
|
|
504
|
+
else return;
|
|
505
|
+
await loadSegmentMessages(c, target.dataset.segmentKey, body, { fullOutputs: view.fullOutputs });
|
|
506
|
+
}
|
|
507
|
+
else if (action === "expand-output") {
|
|
508
|
+
const key = target.dataset.toolKey; const block = target.closest(".tool-block"); if (!key || !block) return;
|
|
509
|
+
const raw = target.closest(".comp-raw,.ungrouped-raw"); const compKey = raw?.dataset.compartmentKey; const compartment = state.compartments.find((item) => segmentKey(item) === compKey); const view = compartment ? getSegmentView(compartment) : state.ungroupedView; const openTools = view.openToolOutputs; const open = openTools.has(key); if (open) { openTools.delete(key); setToolBlockOpen(block, false); }
|
|
510
|
+
else { openTools.add(key); if (target.dataset.requiresFullOutput === "true") { if (compartment) await loadFullOutputs(key, { view, compartment, key: compKey, parent: $(".comp-message-panel", raw) }); else if (raw) await loadRawFallback($(".comp-message-panel", raw), null, true, key); else setToolBlockOpen(block, true); } else setToolBlockOpen(block, true); }
|
|
511
|
+
}
|
|
362
512
|
else if (action === "search-memories") await loadMemories();
|
|
363
513
|
else if (action === "mem-save") await writeMemory("edit");
|
|
364
514
|
else if (action === "mem-archive") { if (confirm("归档这条记忆?(可在详情页恢复)")) await writeMemory("archive"); }
|
|
@@ -368,6 +518,6 @@
|
|
|
368
518
|
} catch (error) { toast(error.message); }
|
|
369
519
|
});
|
|
370
520
|
$("#sessions-tab").addEventListener("click", () => tabTo("sessions")); $("#memories-tab").addEventListener("click", () => tabTo("memories")); $("#refresh").addEventListener("click", refresh);
|
|
371
|
-
|
|
521
|
+
document.addEventListener("keydown", (event) => { const card = event.target?.matches?.('[data-action="session"]') ? event.target : null; if (card && (event.key === "Enter" || event.key === " ")) { event.preventDefault(); card.click(); return; } if (event.key === "Escape" && document.body.dataset.level === "detail") setLevel("list"); });
|
|
372
522
|
setLevel("projects"); loadProjects(false);
|
|
373
523
|
})();
|
package/package.json
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@retaw106/mmc-pi",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.0.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Pi and OMP coding agent extension for Magic Context — cross-session memory and context management",
|
|
6
6
|
"main": "dist/index.js",
|
|
7
|
+
"bin": {
|
|
8
|
+
"mmc-pi": "./dist/cli.js"
|
|
9
|
+
},
|
|
7
10
|
"license": "MIT",
|
|
8
11
|
"author": "ualtinok",
|
|
9
12
|
"repository": {
|
|
@@ -30,8 +33,10 @@
|
|
|
30
33
|
],
|
|
31
34
|
"//": "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
35
|
"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",
|
|
36
|
+
"build": "bun ../../scripts/check-bun.mjs && bun ../../scripts/clean-dist-chunks.mjs dist index.js historian-calibration-extension.js config-init.js cli.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 && bun run build:config-init && bun run build:cli",
|
|
34
37
|
"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/",
|
|
38
|
+
"build:config-init": "cd ../plugin && bun build scripts/config-init-entry.ts --outfile ../pi-plugin/dist/config-init.js --target node --format esm",
|
|
39
|
+
"build:cli": "cd ../plugin && bun build scripts/cli.ts --outfile ../pi-plugin/dist/cli.js --target node --format esm",
|
|
35
40
|
"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
41
|
"typecheck": "tsc --noEmit",
|
|
37
42
|
"test": "bun ../../scripts/check-bun.mjs && bun install --frozen-lockfile && bun test --parallel --timeout 30000",
|