@retaw106/mmc-pi 1.2.3 → 2.1.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 +23 -6
- package/dist/cli.js +7095 -0
- package/dist/config-init.js +5595 -0
- package/dist/{index-ztb4k1mt.js → index-4n1w7ehq.js} +4244 -3960
- package/dist/{index-65ee50rm.js → index-dx9vhsy8.js} +3 -2
- package/dist/{index-wv28nczk.js → index-ny303arh.js} +76 -3
- package/dist/index.js +3319 -2081
- package/dist/{message-time-backfill-h02fzzp8.js → message-time-backfill-a3p9r5rh.js} +2 -2
- package/dist/{read-session-chunk-af3sdznq.js → read-session-chunk-7wcjah1z.js} +2 -2
- package/dist/web/main.js +1555 -203
- package/dist/web/ui/index.html +1 -0
- package/dist/web/ui/panel.css +7 -0
- package/dist/web/ui/panel.js +332 -124
- package/package.json +7 -2
package/dist/web/ui/panel.js
CHANGED
|
@@ -2,11 +2,90 @@
|
|
|
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, includeMaintenance: false, sessionTitleQuery: "", sessionHarness: "", sessionSince: "", sessionUntil: "", expandedSessionIds: new Set(), orphanExpanded: false, preFilterExpanded: null, sessionError: null, memoryError: null, memoryQuery: "", memoryStatus: "unarchived", memoryCategories: [], memorySort: "updated-desc", memoryCursor: null, memoryNextCursor: null, memoryHasMore: false, memoryMatchedCount: 0, memoryCategoriesCatalog: [], draftTimer: 0, writeToken: null, writeBusy: false, pendingProjectPath: null };
|
|
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 VIEW_STATE_KEY = "mmc-panel-view-state";
|
|
11
|
+
function sessionKey(session) { return `${session?.harness || "unknown"}:${session?.sessionId || ""}`; }
|
|
12
|
+
function readViewState() {
|
|
13
|
+
try { const saved = JSON.parse(sessionStorage.getItem(VIEW_STATE_KEY) || "null"); if (!saved || typeof saved !== "object") return; state.pendingProjectPath = saved.projectPath || null; state.pendingSelectedSession = saved.selectedSession || null; state.pendingSelectedMemory = saved.selectedMemory ?? null; state.pendingBranch = saved.branch || null; state.pendingScrollTop = Number(saved.listScrollTop) || 0; state.tab = saved.tab === "memories" ? "memories" : "sessions"; state.includeMaintenance = saved.includeMaintenance === true; state.sessionTitleQuery = typeof saved.sessionTitleQuery === "string" ? saved.sessionTitleQuery : ""; state.sessionHarness = typeof saved.sessionHarness === "string" ? saved.sessionHarness : ""; state.sessionSince = typeof saved.sessionSince === "string" ? saved.sessionSince : ""; state.sessionUntil = typeof saved.sessionUntil === "string" ? saved.sessionUntil : ""; state.expandedSessionIds = new Set(Array.isArray(saved.expandedSessionIds) ? saved.expandedSessionIds : []); state.orphanExpanded = saved.orphanExpanded === true; state.memoryQuery = typeof saved.memoryQuery === "string" ? saved.memoryQuery : ""; state.memoryStatus = typeof saved.memoryStatus === "string" ? saved.memoryStatus : "unarchived"; state.memoryCategories = Array.isArray(saved.memoryCategories) ? saved.memoryCategories : []; state.memorySort = typeof saved.memorySort === "string" ? saved.memorySort : "updated-desc"; state.memoryCursor = null; } catch { /* 浏览器禁用会话存储时仍可正常浏览 */ }
|
|
14
|
+
}
|
|
15
|
+
function saveViewState() {
|
|
16
|
+
try { sessionStorage.setItem(VIEW_STATE_KEY, JSON.stringify({ projectPath: state.project?.projectPath || state.pendingProjectPath, tab: state.tab, includeMaintenance: state.includeMaintenance, sessionTitleQuery: state.sessionTitleQuery || "", sessionHarness: state.sessionHarness || "", sessionSince: state.sessionSince || "", sessionUntil: state.sessionUntil || "", expandedSessionIds: [...state.expandedSessionIds], orphanExpanded: state.orphanExpanded, memoryQuery: state.memoryQuery, memoryStatus: state.memoryStatus, memoryCategories: state.memoryCategories, memorySort: state.memorySort, selectedSession: state.session ? sessionKey(state.session) : null, selectedMemory: state.memory?.id ?? null, branch: state.branch, listScrollTop: $("#items")?.scrollTop || 0 })); } catch { /* 非关键状态,存储失败不阻断面板 */ }
|
|
17
|
+
}
|
|
18
|
+
function sessionTime(session) { return session?.latestMessageTime || { quality: "unreadable", value: null, detail: null }; }
|
|
19
|
+
function sessionTimeText(session) { const time = sessionTime(session); if (time.quality === "no-messages") return "无消息"; if (time.quality === "unreadable") return "时间不可读"; if (time.quality === "partial-unreadable") return time.value == null ? "时间不可读 · 部分不可读" : `${formatTime(time.value)} · 部分不可读`; return time.value == null ? "时间不可读" : formatTime(time.value); }
|
|
20
|
+
function sessionTimeTitle(session) { const time = sessionTime(session); if (time.quality === "normal" && time.value != null) return formatTime(time.value); if (time.quality === "partial-unreadable") return `已知时间仅为下界;部分分支不可读${time.detail ? `:${time.detail}` : ""}`; return time.quality === "no-messages" ? "没有符合条件的 user/assistant 消息" : "无法可靠读取消息时间"; }
|
|
21
|
+
function sessionSort(a, b) { const rank = { normal: 0, "partial-unreadable": 1, "no-messages": 2, unreadable: 3 }; const at = sessionTime(a); const bt = sessionTime(b); const ar = rank[at.quality] ?? 3; const br = rank[bt.quality] ?? 3; if (ar !== br) return ar - br; if (ar < 2 && at.value !== bt.value) return (bt.value ?? -Infinity) - (at.value ?? -Infinity); const ah = `${a.harness || ""}:${a.sessionId || ""}`; const bh = `${b.harness || ""}:${b.sessionId || ""}`; return ah.localeCompare(bh);
|
|
22
|
+
}
|
|
23
|
+
const RELATION_REASON = { "missing-parent": "缺少父会话", "cross-project": "父会话属于其他项目", duplicate: "重复注册", cycle: "父关系成环", "maintenance-parent-hidden": "父为已隐藏的维护会话" };
|
|
24
|
+
const CATEGORY_LABEL = { PROJECT_RULES: "项目规则", ARCHITECTURE: "架构", CONFIG_VALUES: "配置值", ARCHITECTURE_DECISIONS: "架构决策", CONSTRAINTS: "约束", CONFIG_DEFAULTS: "配置默认值", NAMING: "命名", USER_PREFERENCES: "用户偏好", USER_DIRECTIVES: "用户指令", ENVIRONMENT: "环境", WORKFLOW_RULES: "工作流规则", KNOWN_ISSUES: "已知问题" };
|
|
25
|
+
const PREVIEW_CODEPOINTS = 200;
|
|
26
|
+
function expandableText(parent, value, className, tag = "div") {
|
|
27
|
+
const text = value == null ? "" : String(value); const codepoints = Array.from(text); const box = el(tag, `expandable-text${className ? ` ${className}` : ""}`);
|
|
28
|
+
if (codepoints.length <= PREVIEW_CODEPOINTS) { box.textContent = text; parent.append(box); return box; }
|
|
29
|
+
const preview = el("span", "expandable-preview", codepoints.slice(0, PREVIEW_CODEPOINTS).join("")); const full = el("span", "expandable-full", text); full.hidden = true;
|
|
30
|
+
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;
|
|
31
|
+
}
|
|
32
|
+
function setPreviewExpanded(toggle, expanded) {
|
|
33
|
+
const box = toggle.closest(".expandable-text"); if (!box) return;
|
|
34
|
+
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 ? "收起全文" : "展开全文");
|
|
35
|
+
}
|
|
36
|
+
function formatCharCount(value) { return Number.isSafeInteger(value) && value >= 0 ? `${number(value)} 字符` : "未统计"; }
|
|
37
|
+
function displayPath(project) {
|
|
38
|
+
return typeof project?.displayPath === "string" && project.displayPath.trim() ? project.displayPath : null;
|
|
39
|
+
}
|
|
40
|
+
function pathLeaf(value) {
|
|
41
|
+
const original = String(value || "");
|
|
42
|
+
if (!original) return "";
|
|
43
|
+
if (/^[\\/]+$/.test(original) || /^[A-Za-z]:[\\/]?$/.test(original)) return original;
|
|
44
|
+
const withoutTrailingSeparators = original.replace(/[\\/]+$/, "");
|
|
45
|
+
return withoutTrailingSeparators.match(/[^\\/]+$/)?.[0] || original;
|
|
46
|
+
}
|
|
47
|
+
function shortIdentity(value) {
|
|
48
|
+
const identity = String(value || "");
|
|
49
|
+
return identity.length > 26 ? `${identity.slice(0, 17)}…${identity.slice(-8)}` : identity;
|
|
50
|
+
}
|
|
51
|
+
function projectName(project) {
|
|
52
|
+
const pathValue = displayPath(project);
|
|
53
|
+
if (pathValue) return pathLeaf(pathValue) || "未识别项目";
|
|
54
|
+
const identity = shortIdentity(project?.projectPath);
|
|
55
|
+
return identity ? `未识别项目 · ${identity}` : "未识别项目";
|
|
56
|
+
}
|
|
57
|
+
function projectTooltip(project) {
|
|
58
|
+
const identity = String(project?.projectPath || "未知身份");
|
|
59
|
+
const pathValue = displayPath(project);
|
|
60
|
+
return pathValue ? `路径:${pathValue}\n内部身份:${identity}` : `内部身份:${identity}`;
|
|
61
|
+
}
|
|
62
|
+
function setListEyebrow(project) {
|
|
63
|
+
const eyebrow = $("#list-eyebrow");
|
|
64
|
+
if (!project) {
|
|
65
|
+
eyebrow.textContent = "选择项目";
|
|
66
|
+
eyebrow.removeAttribute("title");
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
eyebrow.textContent = projectName(project);
|
|
70
|
+
eyebrow.title = projectTooltip(project);
|
|
71
|
+
}
|
|
72
|
+
function renderFixtureNotice() {
|
|
73
|
+
const root = $("#fixture-notice");
|
|
74
|
+
if (!root) return;
|
|
75
|
+
root.replaceChildren();
|
|
76
|
+
root.hidden = !state.fixtureMode;
|
|
77
|
+
if (!state.fixtureMode) return;
|
|
78
|
+
root.append(el("strong", "fixture-notice-title", "演示数据,未读取真实数据库"));
|
|
79
|
+
root.append(el("span", "fixture-notice-copy", "当前面板使用隔离夹具,仅用于演示和浏览器验证。"));
|
|
80
|
+
const commands = el("span", "fixture-commands");
|
|
81
|
+
commands.append(el("span", "fixture-command-label", "读取配置中的真实数据:"));
|
|
82
|
+
for (const [label, command] of [["Pi", "bun packages/pi-plugin/src/web/main.ts"], ["OpenCode 2", "bun packages/plugin/src/web/main.ts"]]) {
|
|
83
|
+
const item = el("span", "fixture-command");
|
|
84
|
+
item.append(el("span", "fixture-command-host", `${label} `), el("code", "", command));
|
|
85
|
+
commands.append(item);
|
|
86
|
+
}
|
|
87
|
+
root.append(commands);
|
|
88
|
+
}
|
|
10
89
|
const sessionTitle = (session) => session.title || (session.harness === "pi" ? "Pi 会话 · 无标题" : "OpenCode 2 会话 · 无标题");
|
|
11
90
|
const number = (value) => value == null ? "—" : new Intl.NumberFormat("zh-CN").format(value);
|
|
12
91
|
const setReadAt = (data) => { if (data?.readAt) $("#read-at").textContent = `读取于 ${formatTime(data.readAt)}`; };
|
|
@@ -124,76 +203,109 @@
|
|
|
124
203
|
for (const project of state.projects) {
|
|
125
204
|
const card = btn("", "project", "project-card" + (state.project?.projectPath === project.projectPath ? " selected" : ""));
|
|
126
205
|
card.dataset.project = project.projectPath;
|
|
127
|
-
|
|
206
|
+
card.title = projectTooltip(project);
|
|
207
|
+
const identity = el("span", "project-name", projectName(project));
|
|
208
|
+
const pathLine = el("span", "project-path", displayPath(project) || `内部身份:${project.projectPath || "未知身份"}`); pathLine.title = projectTooltip(project);
|
|
209
|
+
const meta = el("span", "project-meta"); meta.append(el("span", "", `${number(project.sessionCount)} 会话`), el("span", "", formatTime(project.lastActivityAt)));
|
|
128
210
|
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);
|
|
211
|
+
card.append(identity, pathLine, meta, flags); root.append(card);
|
|
130
212
|
}
|
|
131
213
|
}
|
|
132
214
|
function setLevel(level) { document.body.dataset.level = level; }
|
|
133
215
|
async function loadProjects(keep = true) {
|
|
134
|
-
loading($("#projects"), "读取项目与数据源…");
|
|
216
|
+
const hadOld = state.projects.length > 0; if (!hadOld) loading($("#projects"), "读取项目与数据源…");
|
|
135
217
|
try {
|
|
136
|
-
const data = await api("/api/projects"); state.projects = data.projects || []; state.sources = data.sources || {}; renderSources(); renderProjects();
|
|
137
|
-
|
|
218
|
+
const data = await api("/api/projects"); state.projects = data.projects || []; state.sources = data.sources || {}; state.fixtureMode = data.fixtureMode === true; renderFixtureNotice(); renderSources(); renderProjects();
|
|
219
|
+
if (!state.project && state.pendingProjectPath) {
|
|
220
|
+
const match = state.projects.find((item) => item.projectPath === state.pendingProjectPath); if (match) await loadProject(match);
|
|
221
|
+
} else if (keep && state.project) {
|
|
138
222
|
const match = state.projects.find((item) => item.projectPath === state.project.projectPath);
|
|
139
223
|
if (match) { state.project = match; await loadCurrent(); }
|
|
140
224
|
else { state.project = null; state.session = state.memory = null; renderProjects(); showNoSelection(); }
|
|
141
225
|
}
|
|
142
|
-
} catch (error) { state.projects = []; renderProjects();
|
|
226
|
+
} catch (error) { if (!hadOld) state.projects = []; renderProjects(); const box = el("div", "error-state"); box.append(el("strong", "", hadOld ? "读取失败,以下为旧项目列表" : "项目读取失败"), el("p", "", error.message)); const retry = btn("重试", "retry-projects", "link-button"); box.append(retry); $("#source-status").replaceChildren(box); }
|
|
143
227
|
}
|
|
144
|
-
function showNoSelection() { $("#list-
|
|
228
|
+
function showNoSelection() { setListEyebrow(state.project); $("#list-title").textContent = state.project ? (state.tab === "sessions" ? "会话" : "记忆") : "审阅内容"; if (!state.project) $("#list-controls").replaceChildren(); $("#items").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
229
|
function renderListControls() {
|
|
146
230
|
const root = $("#list-controls"); root.replaceChildren();
|
|
147
231
|
root.append(btn("← 项目", "back-projects", "mobile-back"));
|
|
148
232
|
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", "至日期筛选");
|
|
233
|
+
const query = el("input", "field"); query.placeholder = "按标题搜索"; query.type = "search"; query.dataset.filter = "title"; query.value = state.sessionTitleQuery || ""; query.setAttribute("aria-label", "按标题搜索会话");
|
|
234
|
+
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; option.selected = value === (state.sessionHarness || ""); harness.append(option); }
|
|
235
|
+
const since = el("input", "field"); since.type = "date"; since.dataset.filter = "since"; since.value = state.sessionSince || ""; since.setAttribute("aria-label", "从日期筛选");
|
|
236
|
+
const until = el("input", "field"); until.type = "date"; until.dataset.filter = "until"; until.value = state.sessionUntil || ""; until.setAttribute("aria-label", "至日期筛选");
|
|
153
237
|
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.
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
238
|
+
const check = el("label", "checkline"); const checkbox = document.createElement("input"); checkbox.type = "checkbox"; checkbox.checked = state.includeMaintenance; checkbox.dataset.filter = "maintenance"; check.append(checkbox, el("span", "", "显示维护会话"));
|
|
239
|
+
const clear = btn("清除筛选", "clear-session-filters", "link-button"); clear.hidden = !sessionFiltersActive();
|
|
240
|
+
root.append(row1, row2, row3, check, clear);
|
|
241
|
+
query.addEventListener("input", () => { state.sessionTitleQuery = query.value; onSessionFilterChange(); });
|
|
242
|
+
harness.addEventListener("change", () => { state.sessionHarness = harness.value; onSessionFilterChange(); });
|
|
243
|
+
since.addEventListener("change", () => { state.sessionSince = since.value; onSessionFilterChange(); });
|
|
244
|
+
until.addEventListener("change", () => { state.sessionUntil = until.value; onSessionFilterChange(); });
|
|
245
|
+
checkbox.addEventListener("change", () => { state.includeMaintenance = checkbox.checked; saveViewState(); loadSessions(); });
|
|
158
246
|
} else {
|
|
159
|
-
const query = el("input", "field"); query.type = "search"; query.placeholder = "搜索当前项目记忆"; query.dataset.filter = "memory-q"; query.
|
|
160
|
-
const
|
|
161
|
-
const
|
|
162
|
-
const row = el("div", "control-row"); row.append(query); const row2 = el("div", "control-row"); row2.append(
|
|
163
|
-
|
|
247
|
+
const query = el("input", "field"); query.type = "search"; query.placeholder = "搜索当前项目记忆"; query.value = state.memoryQuery || ""; query.dataset.filter = "memory-q"; query.setAttribute("aria-label", "搜索当前项目记忆");
|
|
248
|
+
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; option.selected = value === state.memoryStatus; status.append(option); }
|
|
249
|
+
const sort = el("select", "field"); sort.dataset.filter = "memory-sort"; for (const [value, label] of [["updated-desc", "最近更新"], ["importance-desc", "重要性"], ["created-desc", "创建时间"]]) { const option = el("option", "", label); option.value = value; option.selected = value === state.memorySort; sort.append(option); }
|
|
250
|
+
const row = el("div", "control-row"); row.append(query); const row2 = el("div", "control-row"); row2.append(status, sort);
|
|
251
|
+
const categories = el("div", "memory-category-options"); categories.setAttribute("aria-label", "按类别筛选"); for (const value of state.memoryCategoriesCatalog) { const label = el("label", "category-chip"); const checkbox = document.createElement("input"); checkbox.type = "checkbox"; checkbox.value = value; checkbox.checked = state.memoryCategories.includes(value); checkbox.dataset.category = value; label.append(checkbox, el("span", "", CATEGORY_LABEL[value] || value)); categories.append(label); }
|
|
252
|
+
const actions = el("div", "control-row memory-actions"); const search = btn("检索", "search-memories", "button button-primary"); const clear = btn("清除筛选", "clear-memory-filters", "link-button"); clear.hidden = !(state.memoryQuery || state.memoryCategories.length || state.memoryStatus !== "unarchived" || state.memorySort !== "updated-desc"); actions.append(search, clear);
|
|
253
|
+
root.append(row, row2, categories, actions);
|
|
254
|
+
query.addEventListener("input", () => { state.memoryQuery = query.value; saveViewState(); }); query.addEventListener("keydown", (event) => { if (event.key === "Enter") loadMemories(true); }); status.addEventListener("change", () => { state.memoryStatus = status.value; saveViewState(); loadMemories(true); }); sort.addEventListener("change", () => { state.memorySort = sort.value; saveViewState(); loadMemories(true); });
|
|
255
|
+
for (const checkbox of categories.querySelectorAll("input")) checkbox.addEventListener("change", () => { state.memoryCategories = [...categories.querySelectorAll("input:checked")].map((item) => item.value); saveViewState(); loadMemories(true); });
|
|
164
256
|
}
|
|
165
257
|
}
|
|
258
|
+
function sessionFiltersActive() { return Boolean((state.sessionTitleQuery || "").trim() || state.sessionHarness || state.sessionSince || state.sessionUntil); }
|
|
259
|
+
function onSessionFilterChange() { const active = sessionFiltersActive(); if (active && !state.preFilterExpanded) state.preFilterExpanded = new Set(state.expandedSessionIds); if (!active && state.preFilterExpanded) { state.expandedSessionIds = state.preFilterExpanded; state.preFilterExpanded = null; } const clear = $('[data-action="clear-session-filters"]', $("#list-controls")); if (clear) clear.hidden = !active; saveViewState(); renderItems(); }
|
|
260
|
+
function dateBoundary(value, end = false) { if (!value) return null; const parts = value.split("-").map(Number); if (parts.length !== 3 || parts.some((item) => !Number.isInteger(item))) return null; return new Date(parts[0], parts[1] - 1, parts[2] + (end ? 1 : 0), 0, 0, 0, 0).getTime(); }
|
|
261
|
+
function sessionMatches(session) { const title = (state.sessionTitleQuery || "").trim().toLocaleLowerCase(); const time = sessionTime(session); if (state.sessionHarness && session.harness !== state.sessionHarness) return false; if (title && !(session.title || "").toLocaleLowerCase().includes(title)) return false; const since = dateBoundary(state.sessionSince); const until = dateBoundary(state.sessionUntil, true); if (since != null && (time.value == null || time.value < since)) return false; if (until != null && (time.value == null || time.value >= until)) return false; return true; }
|
|
262
|
+
function makeSessionTree() {
|
|
263
|
+
const nodes = (state.sessions || []).filter((node) => state.includeMaintenance || !node.maintenance); const byId = new Map(); for (const node of nodes) { const list = byId.get(node.sessionId) || []; list.push(node); byId.set(node.sessionId, list); }
|
|
264
|
+
const children = new Map(); const parentOf = new Map();
|
|
265
|
+
for (const node of nodes) { if (node.relation?.state !== "child" || !node.parentId) continue; const parents = byId.get(node.parentId) || []; if (parents.length !== 1) continue; parentOf.set(sessionKey(node), parents[0]); const key = sessionKey(parents[0]); const list = children.get(key) || []; list.push(node); children.set(key, list); }
|
|
266
|
+
for (const list of children.values()) list.sort(sessionSort);
|
|
267
|
+
const matches = new Map(); let rootsMatched = 0; let childrenMatched = 0;
|
|
268
|
+
for (const node of nodes) { const matched = sessionMatches(node); matches.set(sessionKey(node), matched); if (matched && node.relation?.state === "root") rootsMatched++; if (matched && node.relation?.state !== "root") childrenMatched++; }
|
|
269
|
+
const descendantMatch = (node, seen = new Set()) => { const key = sessionKey(node); if (seen.has(key)) return false; seen.add(key); if (matches.get(key)) return true; return (children.get(key) || []).some((child) => descendantMatch(child, new Set(seen))); };
|
|
270
|
+
const included = new Set(); let contextAncestors = 0;
|
|
271
|
+
const includeNode = (node) => { const key = sessionKey(node); if (included.has(key)) return; if (!sessionFiltersActive() || descendantMatch(node)) { included.add(key); if (!matches.get(key)) contextAncestors++; const parent = parentOf.get(key); if (parent) includeNode(parent); } };
|
|
272
|
+
for (const node of nodes) if (node.relation?.state === "root" || node.relation?.state === "orphan") includeNode(node);
|
|
273
|
+
for (const node of nodes) if (matches.get(sessionKey(node))) includeNode(node);
|
|
274
|
+
if (!sessionFiltersActive()) for (const node of nodes) included.add(sessionKey(node));
|
|
275
|
+
if (sessionFiltersActive()) for (const node of nodes) if (matches.get(sessionKey(node))) { let current = node; while (current) { if (current.relation?.state === "orphan") state.orphanExpanded = true; if (current !== node) state.expandedSessionIds.add(sessionKey(current)); current = parentOf.get(sessionKey(current)); } }
|
|
276
|
+
const build = (node) => ({ node, matched: matches.get(sessionKey(node)) === true, context: included.has(sessionKey(node)) && !matches.get(sessionKey(node)), children: (children.get(sessionKey(node)) || []).filter((child) => included.has(sessionKey(child))).map(build) });
|
|
277
|
+
return { roots: nodes.filter((node) => node.relation?.state === "root" && included.has(sessionKey(node))).sort(sessionSort).map(build), orphans: nodes.filter((node) => node.relation?.state === "orphan" && included.has(sessionKey(node))).sort(sessionSort).map(build), rootsMatched, childrenMatched, contextAncestors };
|
|
278
|
+
}
|
|
166
279
|
function renderItems() {
|
|
167
280
|
const root = $("#items"); root.replaceChildren();
|
|
168
281
|
if (state.tab === "sessions") {
|
|
169
|
-
|
|
170
|
-
const
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
282
|
+
root.setAttribute("role", "tree"); root.setAttribute("aria-label", "会话树"); const tree = makeSessionTree();
|
|
283
|
+
const counts = el("div", "session-counts", `匹配主会话 ${number(tree.rootsMatched)} · 子会话 ${number(tree.childrenMatched)} · 定位祖先 ${number(tree.contextAncestors)}`); root.append(counts);
|
|
284
|
+
if (!tree.roots.length && !tree.orphans.length) { if (state.sessionError) renderListError(root, state.sessionError, false); else empty(root, state.sessions.length ? "没有符合条件的会话" : "暂无会话", state.sessions.length ? "调整筛选条件试试。" : "这个项目当前没有登记会话;如有记忆,可切换到记忆查看。"); return; }
|
|
285
|
+
for (const item of tree.roots) renderSessionNode(root, item, 1);
|
|
286
|
+
if (tree.orphans.length) renderOrphanGroup(root, tree.orphans);
|
|
287
|
+
if (state.sessionError) renderListError(root, state.sessionError, true);
|
|
174
288
|
} else {
|
|
175
|
-
|
|
289
|
+
root.removeAttribute("role"); const summary = el("div", "memory-counts", `匹配 ${number(state.memoryMatchedCount)} · 已加载 ${number(state.memories.length)}`); root.append(summary);
|
|
290
|
+
if (!state.memories.length) { if (state.memoryError) renderListError(root, state.memoryError, false); else empty(root, state.memoryMatchedCount ? "当前页没有可显示的记忆" : "没有匹配的记忆", state.memoryMatchedCount ? "请尝试重新检索。" : "调整搜索、类别或状态筛选。"); return; }
|
|
176
291
|
for (const memory of state.memories) renderMemoryCard(root, memory);
|
|
292
|
+
if (state.memoryHasMore) { const more = btn("加载更多", "load-more-memories", "button"); more.setAttribute("aria-label", `加载更多记忆,已加载 ${state.memories.length} 条`); root.append(more); }
|
|
293
|
+
if (state.memoryError) renderListError(root, state.memoryError, true);
|
|
177
294
|
}
|
|
178
295
|
}
|
|
179
|
-
function
|
|
180
|
-
|
|
181
|
-
const
|
|
182
|
-
|
|
183
|
-
const
|
|
184
|
-
const
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
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);
|
|
296
|
+
function renderListError(root, error, oldData) { const box = el("div", "error-state list-error"); box.append(el("strong", "", oldData ? "读取失败,以下为旧查询结果" : "读取失败"), el("p", "", error.message)); const retry = btn("重试", state.tab === "sessions" ? "retry-sessions" : "retry-memories", "link-button"); box.append(retry); root.append(box); }
|
|
297
|
+
function renderSessionNode(parent, item, level) {
|
|
298
|
+
const session = item.node; const key = sessionKey(session); const children = item.children; const expanded = state.expandedSessionIds.has(key); const active = state.session && sessionKey(state.session) === key; const id = `session-children-${encodeURIComponent(key).replace(/%/g, "")}`;
|
|
299
|
+
const node = el("div", `session-tree-node${active ? " selected" : ""}${item.context ? " context-ancestor" : ""}`); node.dataset.sid = session.sessionId; node.dataset.harness = session.harness; node.dataset.sessionKey = key; node.setAttribute("role", "treeitem"); node.setAttribute("aria-level", String(level)); node.setAttribute("aria-selected", String(Boolean(active))); node.tabIndex = 0;
|
|
300
|
+
const row = el("div", "session-row");
|
|
301
|
+
if (children.length) { const toggle = btn(expanded ? "−" : "+", "toggle-session", "session-toggle"); toggle.dataset.sessionKey = key; toggle.setAttribute("aria-expanded", String(expanded)); toggle.setAttribute("aria-controls", id); toggle.setAttribute("aria-label", `${expanded ? "收起" : "展开"}${sessionTitle(session)}的子会话`); row.append(toggle); } else row.append(el("span", "session-toggle-spacer"));
|
|
302
|
+
const open = btn("", "session", "session-open"); open.dataset.sid = session.sessionId; open.dataset.harness = session.harness; open.setAttribute("aria-label", `打开会话:${sessionTitle(session)}`); open.append(el("span", "item-title", sessionTitle(session)));
|
|
303
|
+
const meta = el("span", "item-meta"); const time = el("time", "session-time", sessionTimeText(session)); time.title = sessionTimeTitle(session); meta.append(el("span", "", session.harness === "pi" ? "Pi" : "OpenCode 2"), time); open.append(meta);
|
|
304
|
+
const foot = el("span", "item-sub", `${session.sessionId} · ${number(session.compartmentCount)} 段摘要${children.length ? ` · ${number(children.length)} 个子会话` : ""}`); open.append(foot); if (item.context) open.append(el("span", "context-label", "用于定位")); if (session.maintenance) open.append(el("span", "badge maintenance-badge", `维护 · ${session.maintenance}`)); if (session.relation?.state === "orphan") open.append(el("span", "relation-note", RELATION_REASON[session.relation.reason] || "未归属子会话")); if (session.forkSessionId) { const source = el("span", "item-sub", `来源:${session.forkSessionId}`); source.title = "forkSessionId 仅表示来源,不是父子关系"; open.append(source); } row.append(open); node.append(row);
|
|
305
|
+
if (session.harness === "pi") { const branches = session.piBranches || []; const box = el("div", "branch-box"); box.append(el("span", "item-sub", branches.length ? `Pi 分支文件 · ${branches.length}` : "Pi 原文文件未找到")); for (const branch of branches) { const b = btn(`${branch.fileName} · ${branch.directoryName}`, "branch", "link-button branch-button"); b.dataset.sid = session.sessionId; b.dataset.harness = session.harness; b.dataset.branch = branch.fileName; b.title = branch.headerTimestamp || "时间未知"; box.append(b); } node.append(box); }
|
|
306
|
+
if (children.length) { const group = el("div", "session-children"); group.id = id; group.setAttribute("role", "group"); group.hidden = !expanded; if (expanded) for (const child of children) renderSessionNode(group, child, level + 1); node.append(group); } parent.append(node);
|
|
196
307
|
}
|
|
308
|
+
function renderOrphanGroup(root, items) { const group = el("section", "orphan-group"); group.setAttribute("aria-label", "未归属子会话"); const heading = el("div", "orphan-heading"); const toggle = btn(state.orphanExpanded ? "−" : "+", "toggle-orphans", "session-toggle"); toggle.setAttribute("aria-expanded", String(state.orphanExpanded)); toggle.setAttribute("aria-controls", "orphan-session-group"); toggle.setAttribute("aria-label", `${state.orphanExpanded ? "收起" : "展开"}未归属子会话`); heading.append(toggle, el("strong", "", `未归属子会话 · ${items.length}`)); group.append(heading); const body = el("div", "orphan-body"); body.id = "orphan-session-group"; body.setAttribute("role", "group"); body.hidden = !state.orphanExpanded; if (state.orphanExpanded) for (const item of items) renderSessionNode(body, item, 1); group.append(body); root.append(group); }
|
|
197
309
|
function renderMemoryCard(root, memory) {
|
|
198
310
|
const card = btn("", "memory", `item-card${state.memory?.id === memory.id ? " selected" : ""}`); card.dataset.mid = memory.id;
|
|
199
311
|
card.append(el("span", "item-title", `#${memory.id} · ${memory.category}`));
|
|
@@ -203,102 +315,168 @@
|
|
|
203
315
|
if (memory.mergedFrom) card.append(el("span", "badge", "合并记录")); root.append(card);
|
|
204
316
|
}
|
|
205
317
|
async function loadProject(project) {
|
|
206
|
-
state.project = project; state.session = null; state.memory = null; state.branch = null; state.
|
|
318
|
+
const restoring = !state.project && state.pendingProjectPath === project?.projectPath; state.detailToken += 1; state.project = project; state.pendingProjectPath = project?.projectPath || null; state.session = null; state.memory = null; state.branch = null; if (!restoring) { state.expandedSessionIds = new Set(); state.orphanExpanded = false; } state.preFilterExpanded = null; state.sessionError = null; state.memoryError = null; state.sessions = []; state.memories = []; state.memoryCursor = null; state.memoryNextCursor = null; state.memoryHasMore = false; state.memoryMatchedCount = 0; renderProjects(); setListEyebrow(project); $("#list-title").textContent = state.tab === "sessions" ? "会话" : "记忆"; renderListControls(); loading($("#items")); showNoSelection(); setLevel("list"); saveViewState(); await loadCurrent();
|
|
319
|
+
if (state.tab === "sessions" && state.pendingSelectedSession) { const selected = state.sessions.find((item) => sessionKey(item) === state.pendingSelectedSession); if (selected) await openSession(selected, state.pendingBranch); }
|
|
320
|
+
if (state.tab === "memories" && state.pendingSelectedMemory != null) { const selected = state.memories.find((item) => item.id === Number(state.pendingSelectedMemory)); if (selected) await openMemory(selected); }
|
|
321
|
+
state.pendingSelectedSession = null; state.pendingSelectedMemory = null; state.pendingBranch = null; if (state.pendingScrollTop) requestAnimationFrame(() => { $("#items").scrollTop = state.pendingScrollTop; });
|
|
207
322
|
}
|
|
208
323
|
async function loadCurrent() { if (!state.project) return; if (state.tab === "sessions") await loadSessions(); else await loadMemories(); }
|
|
209
324
|
async function loadSessions() {
|
|
210
|
-
loading($("#items"), "读取会话列表…");
|
|
211
|
-
try { const data = await api(path(state.project.projectPath,
|
|
212
|
-
catch (error) {
|
|
325
|
+
const hadOld = state.sessions.length > 0; if (!hadOld) loading($("#items"), "读取会话列表…");
|
|
326
|
+
try { const suffix = state.includeMaintenance ? "/sessions?includeMaintenance=1" : "/sessions"; const data = await api(path(state.project.projectPath, suffix)); state.sessions = data.sessions || []; state.sessionError = null; renderItems(); saveViewState(); }
|
|
327
|
+
catch (error) { state.sessionError = error; if (hadOld) renderItems(); else { const root = $("#items"); root.replaceChildren(); renderListError(root, error, false); } }
|
|
213
328
|
}
|
|
214
|
-
async function loadMemories() {
|
|
329
|
+
async function loadMemories(reset = true) {
|
|
215
330
|
if (!state.project) return; const controls = $("#list-controls"); const query = $('[data-filter="memory-q"]', controls)?.value || ""; state.memoryQuery = query;
|
|
216
|
-
|
|
217
|
-
const
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
331
|
+
state.memoryStatus = $('[data-filter="memory-status"]', controls)?.value || state.memoryStatus || "unarchived"; state.memorySort = $('[data-filter="memory-sort"]', controls)?.value || state.memorySort || "updated-desc";
|
|
332
|
+
const hadOld = state.memories.length > 0; if (reset) { state.memoryCursor = null; state.memoryHasMore = false; }
|
|
333
|
+
const params = new URLSearchParams({ status: state.memoryStatus, sort: state.memorySort }); if (query.trim()) params.set("q", query.trim()); for (const category of state.memoryCategories) params.append("category", category); if (!reset && state.memoryCursor) params.set("cursor", state.memoryCursor);
|
|
334
|
+
if (!hadOld) loading($("#items"), "检索当前项目记忆…");
|
|
335
|
+
try { const data = await api(path(state.project.projectPath, `/memories?${params}`)); const byId = new Map(reset ? [] : state.memories.map((memory) => [memory.id, memory])); for (const memory of data.memories || []) byId.set(memory.id, memory); state.memories = [...byId.values()]; state.memoryCategoriesCatalog = Array.isArray(data.categories) ? data.categories : state.memoryCategoriesCatalog; state.memoryMatchedCount = Number.isFinite(data.matchedCount) ? data.matchedCount : state.memories.length; state.memoryNextCursor = data.nextCursor || null; state.memoryCursor = data.nextCursor || null; state.memoryHasMore = data.hasMore === true; state.memoryError = null; renderListControls(); renderItems(); saveViewState(); }
|
|
336
|
+
catch (error) { state.memoryError = error; if (hadOld) renderItems(); else { const root = $("#items"); root.replaceChildren(); renderListError(root, error, false); } }
|
|
221
337
|
}
|
|
222
338
|
function tabTo(tab) {
|
|
223
|
-
state.tab = tab; state.session = null; state.memory = null; state.branch = null; state.
|
|
224
|
-
$("#sessions-tab").classList.toggle("active", tab === "sessions"); $("#sessions-tab").setAttribute("aria-selected", String(tab === "sessions"));
|
|
339
|
+
state.detailToken += 1; state.tab = tab; state.session = null; state.memory = null; state.branch = null; state.segmentViews = new Map(); state.ungroupedView = null;
|
|
340
|
+
state.memoryError = state.sessionError = null; state.memoryCursor = null; $("#sessions-tab").classList.toggle("active", tab === "sessions"); $("#sessions-tab").setAttribute("aria-selected", String(tab === "sessions"));
|
|
225
341
|
$("#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();
|
|
342
|
+
$("#list-title").textContent = tab === "sessions" ? "会话" : "记忆"; renderListControls(); $("#detail").replaceChildren(); saveViewState(); loadCurrent();
|
|
227
343
|
}
|
|
228
344
|
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
345
|
function addMobileBack(parent, target, label) { const back = btn(`← ${label}`, `back-${target}`, "mobile-back"); parent.append(back); }
|
|
230
|
-
function
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
346
|
+
function ensureSessionAncestorsExpanded(session) { const byId = new Map((state.sessions || []).map((item) => [sessionKey(item), item])); let current = session; const seen = new Set(); while (current && current.parentId && !seen.has(sessionKey(current))) { seen.add(sessionKey(current)); const parent = [...byId.values()].find((item) => item.sessionId === current.parentId); if (!parent) break; state.expandedSessionIds.add(sessionKey(parent)); current = parent; } if (session?.relation?.state === "orphan") state.orphanExpanded = true; }
|
|
347
|
+
function showDetailLoading(label = "正在读取详情…") { const root = $("#detail"); root.classList.add("detail-empty"); root.replaceChildren(el("div", "loading", label)); }
|
|
348
|
+
async function openSession(session, branch = null) {
|
|
349
|
+
if (!session) return;
|
|
350
|
+
const previousSession = state.session; const previousBranch = state.branch; const previousCompartments = state.compartments; const previousNotes = state.notes; const previousDetailErrors = state.detailErrors;
|
|
351
|
+
const sameSession = state.session?.sessionId === session.sessionId && state.branch === branch;
|
|
352
|
+
if (!sameSession) { state.segmentViews = new Map(); state.ungroupedView = null; }
|
|
353
|
+
const token = ++state.detailToken;
|
|
354
|
+
state.session = session; state.memory = null; state.branch = branch; ensureSessionAncestorsExpanded(session); state.detailBusy = true; saveViewState(); renderItems(); setLevel("detail"); showDetailLoading();
|
|
355
|
+
if (session.harness === "pi" && !branch) { state.compartments = []; state.notes = []; state.detailErrors = {}; state.detailBusy = false; renderSessionDetail(); toast("Pi 会话需要先选择分支;未猜测默认分支。", 5000); return; }
|
|
235
356
|
const suffix = `/sessions/${encodeURIComponent(session.sessionId)}`;
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
357
|
+
const calls = [api(path(state.project.projectPath, `${suffix}/compartments${branch ? `?branch=${encodeURIComponent(branch)}` : ""}`)), api(path(state.project.projectPath, `${suffix}/notes`))];
|
|
358
|
+
const [compartments, notes] = await Promise.all(calls.map((p) => p.then((value) => ({ value }), (error) => ({ error }))));
|
|
359
|
+
if (token !== state.detailToken || state.session?.sessionId !== session.sessionId || state.branch !== branch) return;
|
|
360
|
+
if (compartments.error && notes.error && previousSession) { state.session = previousSession; state.branch = previousBranch; state.compartments = previousCompartments; state.notes = previousNotes; state.detailErrors = previousDetailErrors; state.detailBusy = false; renderItems(); renderSessionDetail(); toast(`详情读取失败,仍显示旧会话:${compartments.error.message}`, 6000); return; }
|
|
361
|
+
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
362
|
}
|
|
240
363
|
function renderSessionDetail() {
|
|
241
|
-
const root = $("#detail"); root.replaceChildren(); if (!state.session) return;
|
|
364
|
+
const root = $("#detail"); root.classList.remove("detail-empty"); root.replaceChildren(); if (!state.session) return;
|
|
242
365
|
const wrap = el("div", "detail-content"); addMobileBack(wrap, "list", "返回列表");
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
366
|
+
const s = state.session; wrap.append(el("div", "detail-kicker", `${s.harness === "pi" ? "PI SESSION" : "OPENCODE 2 SESSION"}${state.branch ? " · BRANCH SELECTED" : ""}`));
|
|
367
|
+
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", "", sessionTimeText(s))); if (state.branch) meta.append(el("span", "badge", state.branch)); wrap.append(meta);
|
|
368
|
+
if (s.harness === "pi" && !state.branch) { const prompt = section(wrap, "选择 Pi 分支", "Pi 原文与摘要按分支独立读取"); prompt.append(el("p", "boundary-warning", "请回到会话卡,选择具体分支文件后再查看摘要和原文。面板不会猜测默认分支。")); root.append(wrap); return; }
|
|
369
|
+
const comps = section(wrap, "摘要分段", "层级为审阅展示,不是存储字段");
|
|
246
370
|
if (state.detailErrors?.compartments) errorBox(comps, state.detailErrors.compartments);
|
|
247
371
|
else if (!state.compartments.length) empty(comps, "没有摘要分段");
|
|
248
372
|
else for (const c of state.compartments) renderCompartment(comps, c);
|
|
249
373
|
const notes = section(wrap, "会话笔记", `${state.notes.length} 条`);
|
|
250
374
|
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);
|
|
375
|
+
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); }
|
|
376
|
+
const invalid = state.compartments.filter((c) => !compartmentRange(c));
|
|
377
|
+
if (invalid.length) renderUngroupedSection(wrap, "边界未确认的原文", "这些消息不能安全归属到摘要段", invalid.map((c) => `第 ${number(c.sequence + 1)} 段:${boundaryReason(c)}`).join(";"));
|
|
378
|
+
else if (!state.compartments.length) renderUngroupedSection(wrap, "会话原文", "没有摘要段,原文仍可按需读取", "");
|
|
379
|
+
else { const ranges = state.compartments.map(compartmentRange).filter(Boolean); const tailStart = Math.max(...ranges.map((range) => range.end)) + 1; renderUngroupedSection(wrap, "未归摘要尾部原文", "摘要边界之后的消息不自动归属到任何分段,仍按需读取", "", tailStart - 1); }
|
|
380
|
+
root.append(wrap);
|
|
259
381
|
}
|
|
260
382
|
function renderCompartment(parent, c) {
|
|
261
383
|
const tier = c.renderTier === "full" || c.renderTier === "decayed" ? c.renderTier : null;
|
|
262
384
|
const tierText = tier ? `${tier}${state.compartmentsApproximate ? " · 参考" : ""}` : "层级未提供";
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
385
|
+
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);
|
|
386
|
+
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); }
|
|
387
|
+
const bounds = c.bounds || {}; const boundRow = el("div", "bound-row"); boundRow.append(el("span", "", "边界")); for (const [side, label] of [["start", "起点"], ["end", "终点"]]) {
|
|
266
388
|
const bound = bounds[side] || { state: "unresolved" }; const stateName = bound.state === "synth-resolved" ? "经别名解析" : bound.state === "resolved" ? "已关联" : bound.state === "unlinked" ? "无边界 id" : "边界无法解析";
|
|
267
389
|
boundRow.append(el("span", "badge", `${label} · ${stateName}`));
|
|
268
|
-
|
|
390
|
+
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
391
|
}
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
const
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
392
|
+
card.append(boundRow);
|
|
393
|
+
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);
|
|
394
|
+
}
|
|
395
|
+
function segmentKey(c) { return String(c.id ?? `sequence-${c.sequence}`); }
|
|
396
|
+
function numericBoundary(bound) { return (bound?.state === "resolved" || bound?.state === "synth-resolved") && Number.isSafeInteger(bound.anchor) ? bound.anchor : null; }
|
|
397
|
+
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; }
|
|
398
|
+
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(";"); }
|
|
399
|
+
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; }
|
|
400
|
+
function renderUngroupedSection(parent, title, note, reason, initialAfter = null) {
|
|
401
|
+
if (!state.ungroupedView) state.ungroupedView = { open: false, after: initialAfter, cursors: [], page: null, loading: false, error: null, fullOutputs: false, openToolOutputs: new Set(), request: 0 };
|
|
402
|
+
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);
|
|
403
|
+
}
|
|
404
|
+
function renderMessages(parent, page, scope = {}) {
|
|
405
|
+
if (!page.rows?.length) empty(parent, "没有原文消息");
|
|
406
|
+
else for (const row of page.rows) {
|
|
407
|
+
const card = el("article", `message ${row.role === "user" ? "user" : "assistant"}`); card.dataset.messageKey = String(row.key); const head = el("div", "message-head");
|
|
408
|
+
const role = row.role === "user" ? "用户" : row.role === "assistant" ? "助手" : row.role || "未知角色";
|
|
409
|
+
head.append(el("strong", "message-role", role));
|
|
410
|
+
const identity = el("span", "message-aux");
|
|
411
|
+
identity.append(el("span", "message-index", `消息 #${row.key}`));
|
|
412
|
+
const messageId = String(row.id || "无 ID"); const idLabel = messageId.length > 32 ? `${messageId.slice(0, 15)}…${messageId.slice(-10)}` : messageId;
|
|
413
|
+
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);
|
|
414
|
+
for (const [index, part] of (row.parts || []).entries()) renderPart(card, part, row, index, scope);
|
|
415
|
+
parent.append(card);
|
|
416
|
+
}
|
|
417
|
+
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", "从范围开头"));
|
|
418
|
+
pager.append(el("span", "section-note", page.rows?.length ? `${page.rows[0].key} — ${page.rows[page.rows.length - 1].key}` : "空页"));
|
|
419
|
+
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);
|
|
420
|
+
}
|
|
421
|
+
function setToolBlockOpen(block, open) {
|
|
422
|
+
block.classList.toggle("expanded", open);
|
|
423
|
+
const output = $(".tool-output", block); if (output) output.hidden = !open;
|
|
424
|
+
const toggle = $('[data-action="expand-output"]', block); if (!toggle) return;
|
|
425
|
+
toggle.setAttribute("aria-expanded", String(open)); toggle.setAttribute("aria-label", `${toggle.dataset.toolName || "工具"}:${open ? "收起输出" : "展开输出"}`); toggle.textContent = open ? "收起输出" : toggle.dataset.closedLabel;
|
|
426
|
+
}
|
|
427
|
+
function messageQuery(view, range, fullOutputs = false) {
|
|
428
|
+
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;
|
|
429
|
+
}
|
|
430
|
+
function renderSegmentMessages(parent, c, key) {
|
|
431
|
+
const view = getSegmentView(c); parent.replaceChildren(); if (view.loading) { parent.append(el("div", "loading", "读取该段原文…")); return; }
|
|
432
|
+
if (view.error) { errorBox(parent, view.error); return; }
|
|
433
|
+
if (!view.page) { parent.append(el("p", "section-note", compartmentRange(c) ? "展开后按该段边界读取原文。" : "边界未确认,原文不会伪造归属。")); return; }
|
|
434
|
+
renderMessages(parent, view.page, { key, view, compartment: c, parent });
|
|
435
|
+
}
|
|
436
|
+
async function loadSegmentMessages(c, key, parent, options = {}) {
|
|
437
|
+
const range = compartmentRange(c); if (!range) { renderSegmentMessages(parent, c, key); return; }
|
|
438
|
+
const view = getSegmentView(c); const token = state.detailToken; const request = ++view.request; view.loading = true; view.error = null; renderSegmentMessages(parent, c, key);
|
|
439
|
+
try {
|
|
440
|
+
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)}`));
|
|
441
|
+
if (token !== state.detailToken || request !== view.request || !state.session) return;
|
|
442
|
+
view.page = page; view.loading = false; view.fullOutputs = full; renderSegmentMessages(parent, c, key);
|
|
443
|
+
if (options.focusKey) { const block = [...parent.querySelectorAll(".tool-block")].find((node) => node.dataset.toolKey === options.focusKey); if (block) setToolBlockOpen(block, true); }
|
|
444
|
+
if (options.anchor != null) focusMessage(parent, options.anchor);
|
|
445
|
+
} catch (error) { if (token !== state.detailToken || request !== view.request) return; view.loading = false; view.error = error; renderSegmentMessages(parent, c, key); }
|
|
446
|
+
}
|
|
447
|
+
async function loadRawFallback(parent, anchor = null, fullOutputs = false, focusKey = null) {
|
|
448
|
+
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 = []; }
|
|
449
|
+
view.loading = true; view.error = null; parent.replaceChildren(el("div", "loading", "读取未分组原文…"));
|
|
450
|
+
try {
|
|
451
|
+
const page = await api(path(state.project.projectPath, `/sessions/${encodeURIComponent(state.session.sessionId)}/messages?${messageQuery(view, null, fullOutputs || view.fullOutputs)}`));
|
|
452
|
+
if (token !== state.detailToken || request !== view.request || !state.session) return;
|
|
453
|
+
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);
|
|
454
|
+
} catch (error) { if (token !== state.detailToken || request !== view.request) return; view.loading = false; view.error = error; parent.replaceChildren(); errorBox(parent, error); }
|
|
455
|
+
}
|
|
456
|
+
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; }
|
|
457
|
+
async function loadFullOutputs(toolKey, scope) {
|
|
458
|
+
if (!scope?.view || !scope.compartment) return;
|
|
459
|
+
scope.view.openToolOutputs.add(toolKey); await loadSegmentMessages(scope.compartment, scope.key, scope.parent, { fullOutputs: true, focusKey: toolKey });
|
|
460
|
+
}
|
|
461
|
+
function renderPart(parent, part, row, index, scope = {}) {
|
|
462
|
+
if (part && typeof part === "object" && part.type === "tool") {
|
|
463
|
+
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);
|
|
464
|
+
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);
|
|
465
|
+
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", "无输出"));
|
|
466
|
+
setToolBlockOpen(tool, isOpen); if (isOpen) tool.classList.add("expanded");
|
|
289
467
|
parent.append(tool); return;
|
|
290
468
|
}
|
|
291
|
-
|
|
292
|
-
|
|
469
|
+
if (part && typeof part === "object" && typeof part.text === "string") { expandableText(parent, part.text, "message-body", "p"); return; }
|
|
470
|
+
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
471
|
}
|
|
294
472
|
async function openMemory(memory) {
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
473
|
+
const previousMemory = state.memory; const previousDetail = state.memoryDetail; state.memory = memory; state.session = null; saveViewState(); renderItems(); setLevel("detail"); showDetailLoading("读取记忆详情与证据…");
|
|
474
|
+
try { const data = await api(path(state.project.projectPath, `/memories/${encodeURIComponent(memory.id)}`)); state.memoryDetail = data; renderMemoryDetail(); }
|
|
475
|
+
catch (error) { if (previousMemory && previousDetail) { state.memory = previousMemory; state.memoryDetail = previousDetail; renderMemoryDetail(); toast(`详情读取失败,仍显示旧记忆:${error.message}`, 6000); } else { $("#detail").replaceChildren(); errorBox($("#detail"), error); } }
|
|
298
476
|
}
|
|
299
477
|
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
|
-
|
|
478
|
+
function renderMemoryDetail() {
|
|
479
|
+
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
480
|
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
481
|
const content = section(wrap, "记忆正文"); content.append(el("div", "memory-content", m.content));
|
|
304
482
|
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,25 +503,30 @@
|
|
|
325
503
|
editable.append(area, status, formRow, buttons, el("p", "section-note", "保存修改仅提交变更字段;归档/恢复不改动内容与类别。恢复立即写库,现役会话要等下一次自然 HARD fold 才可见。"));
|
|
326
504
|
root.append(wrap);
|
|
327
505
|
}
|
|
506
|
+
async function openCompartmentAt(c, anchor) {
|
|
507
|
+
const key = segmentKey(c); const node = [...document.querySelectorAll("[data-compartment-key]")].find((item) => item.dataset.compartmentKey === key); if (!node) return false;
|
|
508
|
+
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);
|
|
509
|
+
}
|
|
328
510
|
async function jumpTo(anchor) {
|
|
329
511
|
if (!state.session) return;
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
512
|
+
const value = Number(anchor); if (!Number.isSafeInteger(value)) return;
|
|
513
|
+
const matches = state.compartments.filter((c) => { const range = compartmentRange(c); return range && value >= range.start && value <= range.end; });
|
|
514
|
+
if (matches.length) { await Promise.all(matches.map((c) => openCompartmentAt(c, value))); return; }
|
|
515
|
+
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; }
|
|
516
|
+
toast(`消息 #${value} 未能归属到有效摘要段,未安全跳转`);
|
|
335
517
|
}
|
|
336
518
|
async function refresh() {
|
|
337
519
|
const button = $("#refresh"); button.disabled = true; button.textContent = "刷新中…";
|
|
338
520
|
try {
|
|
339
|
-
const selectedPath = state.project?.projectPath; const
|
|
521
|
+
const selectedPath = state.project?.projectPath; const selectedSessionKey = state.session ? sessionKey(state.session) : null; const selectedMemoryId = state.memory?.id; const branch = state.branch; const scrollTop = $("#items")?.scrollTop || 0; saveViewState();
|
|
340
522
|
await loadProjects(false);
|
|
341
523
|
state.project = state.projects.find((item) => item.projectPath === selectedPath) || null; renderProjects();
|
|
342
524
|
if (!state.project) { state.session = state.memory = null; showNoSelection(); }
|
|
343
525
|
else {
|
|
344
526
|
await loadCurrent();
|
|
345
|
-
if (state.tab === "sessions" &&
|
|
346
|
-
if (state.tab === "memories" && selectedMemoryId != null) { const current = state.memories.find((item) => item.id === selectedMemoryId); if (current) await openMemory(current); else
|
|
527
|
+
if (state.tab === "sessions" && selectedSessionKey) { const current = state.sessions.find((item) => sessionKey(item) === selectedSessionKey); if (current) await openSession(current, branch); else if (!state.session) showNoSelection(); }
|
|
528
|
+
if (state.tab === "memories" && selectedMemoryId != null) { const current = state.memories.find((item) => item.id === selectedMemoryId); if (current) await openMemory(current); else if (state.memoryDetail) renderMemoryDetail(); else showNoSelection(); }
|
|
529
|
+
requestAnimationFrame(() => { $("#items").scrollTop = scrollTop; saveViewState(); });
|
|
347
530
|
}
|
|
348
531
|
toast("已重新读取当前视图");
|
|
349
532
|
} finally { button.disabled = false; button.textContent = "刷新数据"; }
|
|
@@ -351,15 +534,40 @@
|
|
|
351
534
|
document.addEventListener("click", async (event) => {
|
|
352
535
|
const target = event.target.closest("[data-action]"); if (!target) return; const action = target.dataset.action;
|
|
353
536
|
try {
|
|
537
|
+
if (action === "toggle-preview") { setPreviewExpanded(target, target.getAttribute("aria-expanded") !== "true"); return; }
|
|
354
538
|
if (action === "project") await loadProject(state.projects.find((p) => p.projectPath === target.dataset.project));
|
|
355
|
-
|
|
356
|
-
|
|
539
|
+
else if (action === "session") await openSession(state.sessions.find((s) => sessionKey(s) === `${target.dataset.harness || "unknown"}:${target.dataset.sid}`), null);
|
|
540
|
+
else if (action === "toggle-session") { const key = target.dataset.sessionKey; if (state.expandedSessionIds.has(key)) state.expandedSessionIds.delete(key); else state.expandedSessionIds.add(key); saveViewState(); renderItems(); }
|
|
541
|
+
else if (action === "toggle-orphans") { state.orphanExpanded = !state.orphanExpanded; saveViewState(); renderItems(); }
|
|
542
|
+
else if (action === "branch") { const session = state.sessions.find((s) => s.sessionId === target.dataset.sid); event.stopPropagation(); await openSession(session, target.dataset.branch); }
|
|
357
543
|
else if (action === "memory") await openMemory(state.memories.find((m) => m.id === Number(target.dataset.mid)));
|
|
358
544
|
else if (action === "jump") await jumpTo(target.dataset.anchor);
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
545
|
+
else if (action === "next-raw-page" || action === "previous-raw-page") {
|
|
546
|
+
const view = state.ungroupedView; const body = $("#raw-messages .comp-message-panel"); if (!view || !body) return;
|
|
547
|
+
if (action === "next-raw-page" && view.page?.hasMore) { view.cursors.push(view.after); view.after = view.page.nextAfter; }
|
|
548
|
+
else if (action === "previous-raw-page" && view.cursors.length) view.after = view.cursors.pop();
|
|
549
|
+
else return;
|
|
550
|
+
await loadRawFallback(body, null, view.fullOutputs);
|
|
551
|
+
}
|
|
552
|
+
else if (action === "next-segment-page" || action === "previous-segment-page") {
|
|
553
|
+
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;
|
|
554
|
+
if (action === "next-segment-page" && view.page?.hasMore) { view.cursors.push(view.after); view.after = view.page.nextAfter; }
|
|
555
|
+
else if (action === "previous-segment-page" && view.cursors.length) view.after = view.cursors.pop();
|
|
556
|
+
else return;
|
|
557
|
+
await loadSegmentMessages(c, target.dataset.segmentKey, body, { fullOutputs: view.fullOutputs });
|
|
558
|
+
}
|
|
559
|
+
else if (action === "expand-output") {
|
|
560
|
+
const key = target.dataset.toolKey; const block = target.closest(".tool-block"); if (!key || !block) return;
|
|
561
|
+
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); }
|
|
562
|
+
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); }
|
|
563
|
+
}
|
|
564
|
+
else if (action === "search-memories") await loadMemories(true);
|
|
565
|
+
else if (action === "load-more-memories") await loadMemories(false);
|
|
566
|
+
else if (action === "clear-session-filters") { state.sessionTitleQuery = ""; state.sessionHarness = ""; state.sessionSince = ""; state.sessionUntil = ""; onSessionFilterChange(); }
|
|
567
|
+
else if (action === "clear-memory-filters") { state.memoryQuery = ""; state.memoryStatus = "unarchived"; state.memoryCategories = []; state.memorySort = "updated-desc"; renderListControls(); await loadMemories(true); }
|
|
568
|
+
else if (action === "retry-sessions") await loadSessions();
|
|
569
|
+
else if (action === "retry-memories") await loadMemories(true);
|
|
570
|
+
else if (action === "retry-projects") await loadProjects(true);
|
|
363
571
|
else if (action === "mem-save") await writeMemory("edit");
|
|
364
572
|
else if (action === "mem-archive") { if (confirm("归档这条记忆?(可在详情页恢复)")) await writeMemory("archive"); }
|
|
365
573
|
else if (action === "mem-restore") await writeMemory("restore");
|
|
@@ -367,7 +575,7 @@
|
|
|
367
575
|
else if (action === "back-list") setLevel("list"); else if (action === "back-projects") setLevel("projects");
|
|
368
576
|
} catch (error) { toast(error.message); }
|
|
369
577
|
});
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
578
|
+
$("#sessions-tab").addEventListener("click", () => tabTo("sessions")); $("#memories-tab").addEventListener("click", () => tabTo("memories")); $("#refresh").addEventListener("click", refresh); $("#items").addEventListener("scroll", () => saveViewState(), { passive: true });
|
|
579
|
+
document.addEventListener("keydown", (event) => { const target = event.target; if ((event.key === "Enter" || event.key === " ") && target?.matches?.('[data-action="session"], [data-action="memory"]')) { event.preventDefault(); target.click(); return; } const treeNode = target?.matches?.(".session-tree-node") ? target : null; if (treeNode && (event.key === "Enter" || event.key === " ")) { event.preventDefault(); const session = state.sessions.find((item) => sessionKey(item) === treeNode.dataset.sessionKey); if (session) openSession(session); return; } if (event.key === "Escape" && document.body.dataset.level === "detail") setLevel("list"); });
|
|
580
|
+
readViewState(); $("#sessions-tab").classList.toggle("active", state.tab === "sessions"); $("#sessions-tab").setAttribute("aria-selected", String(state.tab === "sessions")); $("#memories-tab").classList.toggle("active", state.tab === "memories"); $("#memories-tab").setAttribute("aria-selected", String(state.tab === "memories")); setLevel("projects"); loadProjects(false);
|
|
373
581
|
})();
|