@kingcrab/pi-imessage 0.0.42 → 1.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 +101 -108
- package/dist/agent/behavior.js +79 -0
- package/dist/agent/chats.js +67 -0
- package/dist/agent/commands.js +163 -0
- package/dist/agent/compaction.js +51 -0
- package/dist/agent/deliver.js +139 -0
- package/dist/agent/direct-send.js +68 -0
- package/dist/agent/harness.js +79 -0
- package/dist/agent/health.js +32 -0
- package/dist/agent/models.js +69 -0
- package/dist/agent/progress-format.js +87 -0
- package/dist/agent/progress.js +135 -0
- package/dist/agent/prompt.js +79 -0
- package/dist/agent/replies.js +9 -0
- package/dist/agent/run.js +159 -0
- package/dist/agent/scheduling.js +290 -0
- package/dist/agent/time.js +18 -0
- package/dist/cli.js +88 -191
- package/dist/config/settings.js +60 -0
- package/dist/extensions/browser.js +326 -0
- package/dist/extensions/final-text.js +23 -0
- package/dist/extensions/memory.js +109 -0
- package/dist/extensions/message.js +33 -0
- package/dist/extensions/read-image.js +72 -0
- package/dist/extensions/schedules.js +41 -0
- package/dist/extensions/subagent.js +54 -0
- package/dist/extensions/web.js +99 -0
- package/dist/extensions/workspace.js +98 -0
- package/dist/main.js +571 -74
- package/dist/transport/attachments.js +37 -0
- package/dist/transport/http.js +110 -0
- package/dist/{rich-text.js → transport/rich-text.js} +25 -13
- package/dist/{send.js → transport/send.js} +13 -13
- package/dist/transport/watch.js +124 -0
- package/dist/web/app.js +748 -0
- package/dist/web/history.js +87 -0
- package/dist/web/memory.js +81 -0
- package/dist/web/page.js +28 -0
- package/dist/web/schedules.js +74 -0
- package/dist/web/server.js +348 -0
- package/dist/web/style.css +543 -0
- package/dist/web/tasks.js +63 -0
- package/docs/adr/0001-durable-owns-state.md +11 -0
- package/docs/adr/0002-module-layering.md +11 -0
- package/docs/adr/0003-replies-at-most-once.md +11 -0
- package/docs/adr/0004-native-extensions.md +11 -0
- package/docs/adr/0005-history-migration.md +11 -0
- package/docs/adr/0006-jsonl-storage.md +11 -0
- package/docs/adr/0007-images-by-path.md +11 -0
- package/docs/adr/0008-compaction-and-run.md +11 -0
- package/docs/adr/0009-one-shot-cutover.md +11 -0
- package/docs/adr/0010-test-budget.md +11 -0
- package/docs/adr/0011-reconcile-unsent-answers.md +24 -0
- package/docs/adr/0012-run-in-owned-conversation.md +22 -0
- package/docs/adr/0013-thin-adapters-around-durable.md +18 -0
- package/docs/adr/0014-ephemeral-private-storage.md +13 -0
- package/docs/adr/0015-ordinary-stop-keeps-native-inbox.md +21 -0
- package/docs/adr/0016-process-http-transport.md +21 -0
- package/docs/adr/0017-browser-cli-owned-scope.md +22 -0
- package/docs/adr/0018-tasks-without-prompt-api.md +19 -0
- package/docs/adr/0019-remove-scheduled-services.md +41 -0
- package/docs/adr/0020-chat-task-progress.md +41 -0
- package/docs/adr/0021-readable-progress-without-link-previews.md +33 -0
- package/docs/adr/0022-progress-admission-and-settlement.md +35 -0
- package/docs/adr/0023-retain-progress-type-prefixes.md +18 -0
- package/docs/adr/0024-status-marker-before-type-prefix.md +13 -0
- package/docs/adr/0025-reusable-progress-and-command-preview.md +49 -0
- package/docs/adr/0026-inline-command-only-requests.md +16 -0
- package/docs/adr/0027-command-preview-width.md +10 -0
- package/docs/adr/0028-command-preview-120.md +8 -0
- package/docs/adr/0030-shared-web-read-cache.md +33 -0
- package/docs/adr/0031-quiet-compaction-progress.md +32 -0
- package/docs/adr/0032-native-background-schedules.md +58 -0
- package/docs/adr/0033-operator-scheduled-run.md +23 -0
- package/docs/adr/0034-machine-local-schedule-time.md +36 -0
- package/docs/adr/0035-external-business-task-extensions.md +56 -0
- package/docs/adr/0036-workspace-policy-and-neutral-service-id.md +37 -0
- package/docs/adr/0037-restart-during-in-flight-work.md +30 -0
- package/docs/adr/0038-restart-any-time.md +25 -0
- package/docs/adr/0039-rerun-interrupted-tools.md +24 -0
- package/docs/adr/0040-chat-progress-notices.md +44 -0
- package/docs/adr/0041-restart-and-recovery.md +28 -0
- package/docs/adr/0042-background-schedules-and-workspace-extensions.md +44 -0
- package/docs/adr/0043-remove-legacy-migration.md +23 -0
- package/docs/adr/0044-message-channels.md +32 -0
- package/docs/adr/0045-chat-scheduling-tools.md +51 -0
- package/docs/adr/0046-selective-command-redaction.md +31 -0
- package/docs/adr/0047-one-scheduling-path.md +158 -0
- package/docs/adr/README.md +50 -0
- package/docs/api.md +92 -0
- package/docs/architecture.md +164 -0
- package/docs/browser.md +93 -0
- package/docs/memory.md +27 -0
- package/docs/screenshot.png +0 -0
- package/docs/workspace-extensions.md +133 -0
- package/examples/workspace-extensions/system-context/index.ts +69 -0
- package/examples/workspace-extensions/system-context/package.json +3 -0
- package/ops/README.md +267 -0
- package/ops/preflight.mjs +28 -0
- package/package.json +33 -45
- package/dist/agent.d.ts +0 -60
- package/dist/agent.d.ts.map +0 -1
- package/dist/agent.js +0 -715
- package/dist/agent.js.map +0 -1
- package/dist/cli.d.ts +0 -15
- package/dist/cli.d.ts.map +0 -1
- package/dist/cli.js.map +0 -1
- package/dist/imessage.d.ts +0 -33
- package/dist/imessage.d.ts.map +0 -1
- package/dist/imessage.js +0 -76
- package/dist/imessage.js.map +0 -1
- package/dist/logger.d.ts +0 -33
- package/dist/logger.d.ts.map +0 -1
- package/dist/logger.js +0 -104
- package/dist/logger.js.map +0 -1
- package/dist/main.d.ts +0 -5
- package/dist/main.d.ts.map +0 -1
- package/dist/main.js.map +0 -1
- package/dist/memory.d.ts +0 -53
- package/dist/memory.d.ts.map +0 -1
- package/dist/memory.js +0 -196
- package/dist/memory.js.map +0 -1
- package/dist/model-health.d.ts +0 -11
- package/dist/model-health.d.ts.map +0 -1
- package/dist/model-health.js +0 -67
- package/dist/model-health.js.map +0 -1
- package/dist/pipeline.d.ts +0 -27
- package/dist/pipeline.d.ts.map +0 -1
- package/dist/pipeline.js +0 -59
- package/dist/pipeline.js.map +0 -1
- package/dist/queue.d.ts +0 -23
- package/dist/queue.d.ts.map +0 -1
- package/dist/queue.js +0 -89
- package/dist/queue.js.map +0 -1
- package/dist/rich-text.d.ts +0 -26
- package/dist/rich-text.d.ts.map +0 -1
- package/dist/rich-text.js.map +0 -1
- package/dist/self-echo.d.ts +0 -19
- package/dist/self-echo.d.ts.map +0 -1
- package/dist/self-echo.js +0 -54
- package/dist/self-echo.js.map +0 -1
- package/dist/send.d.ts +0 -24
- package/dist/send.d.ts.map +0 -1
- package/dist/send.js.map +0 -1
- package/dist/settings.d.ts +0 -44
- package/dist/settings.d.ts.map +0 -1
- package/dist/settings.js +0 -80
- package/dist/settings.js.map +0 -1
- package/dist/store.d.ts +0 -57
- package/dist/store.d.ts.map +0 -1
- package/dist/store.js +0 -124
- package/dist/store.js.map +0 -1
- package/dist/tasks.d.ts +0 -95
- package/dist/tasks.d.ts.map +0 -1
- package/dist/tasks.js +0 -384
- package/dist/tasks.js.map +0 -1
- package/dist/types.d.ts +0 -93
- package/dist/types.d.ts.map +0 -1
- package/dist/types.js +0 -38
- package/dist/types.js.map +0 -1
- package/dist/watch.d.ts +0 -21
- package/dist/watch.d.ts.map +0 -1
- package/dist/watch.js +0 -219
- package/dist/watch.js.map +0 -1
- package/dist/web/data.d.ts +0 -11
- package/dist/web/data.d.ts.map +0 -1
- package/dist/web/data.js +0 -56
- package/dist/web/data.js.map +0 -1
- package/dist/web/html.d.ts +0 -5
- package/dist/web/html.d.ts.map +0 -1
- package/dist/web/html.js +0 -16
- package/dist/web/html.js.map +0 -1
- package/dist/web/index.d.ts +0 -23
- package/dist/web/index.d.ts.map +0 -1
- package/dist/web/index.js +0 -279
- package/dist/web/index.js.map +0 -1
- package/dist/web/render.d.ts +0 -11
- package/dist/web/render.d.ts.map +0 -1
- package/dist/web/render.js +0 -56
- package/dist/web/render.js.map +0 -1
- package/dist/web/templates/logs.eta +0 -75
- package/dist/web/templates/memory.eta +0 -91
- package/dist/web/templates/page.eta +0 -98
package/dist/web/app.js
ADDED
|
@@ -0,0 +1,748 @@
|
|
|
1
|
+
const page = location.pathname === "/" ? "/chat" : location.pathname;
|
|
2
|
+
const conversationId = new URLSearchParams(location.search).get("conversationId");
|
|
3
|
+
const view = document.getElementById("view");
|
|
4
|
+
const toolbar = document.getElementById("toolbar");
|
|
5
|
+
const notice = document.getElementById("notice");
|
|
6
|
+
const state = document.getElementById("state");
|
|
7
|
+
const main = document.getElementById("main");
|
|
8
|
+
const sidebar = document.getElementById("chat-list");
|
|
9
|
+
document.getElementById("connection")?.remove();
|
|
10
|
+
let data = JSON.parse(state.textContent);
|
|
11
|
+
let chats = [];
|
|
12
|
+
let memoryQuery = { q: "", namespace: "", kind: "", status: "active", page: 1 };
|
|
13
|
+
let refreshing = false;
|
|
14
|
+
|
|
15
|
+
document.body.classList.add(`page-${page.slice(1)}`);
|
|
16
|
+
if (conversationId) document.body.classList.add("conversation-detail");
|
|
17
|
+
document.getElementById("title").textContent =
|
|
18
|
+
{
|
|
19
|
+
"/chat": "Recent Chats",
|
|
20
|
+
"/tasks": "Tasks",
|
|
21
|
+
"/scheduled": "Scheduled tasks",
|
|
22
|
+
"/logs": "Logs",
|
|
23
|
+
"/memory": "Memory / 记忆与规则",
|
|
24
|
+
"/settings": "Settings",
|
|
25
|
+
}[page] || "pi-imessage";
|
|
26
|
+
|
|
27
|
+
function element(tag, text, className) {
|
|
28
|
+
const node = document.createElement(tag);
|
|
29
|
+
if (text !== undefined) node.textContent = String(text);
|
|
30
|
+
if (className) node.className = className;
|
|
31
|
+
return node;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function link(text, href, className) {
|
|
35
|
+
const node = element("a", text, className);
|
|
36
|
+
node.href = href;
|
|
37
|
+
return node;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function showNotice(text) {
|
|
41
|
+
notice.textContent = text;
|
|
42
|
+
notice.hidden = !text;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async function fetchData(url) {
|
|
46
|
+
const response = await fetch(url, { cache: "no-store" });
|
|
47
|
+
if (!response.ok) throw new Error(`Unable to load ${url.split("?")[0]} (${response.status})`);
|
|
48
|
+
return response.json();
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function messageText(content) {
|
|
52
|
+
if (typeof content === "string") return content;
|
|
53
|
+
if (!Array.isArray(content)) return "";
|
|
54
|
+
return content
|
|
55
|
+
.map((block) => {
|
|
56
|
+
if (block.type === "text") return block.text;
|
|
57
|
+
if (block.type === "image") return "[Image attachment]";
|
|
58
|
+
if (block.type === "toolCall") return `[Tool: ${block.name}]`;
|
|
59
|
+
return "";
|
|
60
|
+
})
|
|
61
|
+
.filter(Boolean)
|
|
62
|
+
.join("\n");
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function messages(detail, chatGuid) {
|
|
66
|
+
return (detail.history?.items || [])
|
|
67
|
+
.flatMap((entry) =>
|
|
68
|
+
(entry.model || [])
|
|
69
|
+
.filter((message) => ["user", "assistant"].includes(message.role) && message.stopReason !== "toolUse")
|
|
70
|
+
.map((message) => {
|
|
71
|
+
const original = entry.data?.original || {};
|
|
72
|
+
const content = messageText(message.content);
|
|
73
|
+
const prefix = content.match(/^\[(?:Group '([^']*)'|DM|SMS) from ([^\]]+)\] /);
|
|
74
|
+
const sender =
|
|
75
|
+
message.role === "assistant"
|
|
76
|
+
? "pi"
|
|
77
|
+
: original.senderName || original.sender || prefix?.[2] || "user";
|
|
78
|
+
return {
|
|
79
|
+
id: `${entry.id}-${message.role}`,
|
|
80
|
+
time: Number(message.timestamp) || Date.parse(original.date) || 0,
|
|
81
|
+
role: message.role,
|
|
82
|
+
status:
|
|
83
|
+
message.role === "assistant" && !entry.data?.legacy
|
|
84
|
+
? detail.delivery?.drafts?.[entry.id]?.status ||
|
|
85
|
+
detail.delivery?.answers?.[entry.id] ||
|
|
86
|
+
"pending"
|
|
87
|
+
: undefined,
|
|
88
|
+
sender,
|
|
89
|
+
groupName: original.groupName || prefix?.[1],
|
|
90
|
+
text: prefix ? content.slice(prefix[0].length) : content,
|
|
91
|
+
channel: chatGuid.includes(";+;") ? "GROUP" : chatGuid.startsWith("SMS;") ? "SMS" : "DM",
|
|
92
|
+
};
|
|
93
|
+
}),
|
|
94
|
+
)
|
|
95
|
+
.filter((message) => message.text)
|
|
96
|
+
.sort((left, right) => left.time - right.time);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function formatTime(timestamp) {
|
|
100
|
+
if (!timestamp) return "[unknown time]";
|
|
101
|
+
const date = new Date(timestamp);
|
|
102
|
+
const parts = [
|
|
103
|
+
date.getMonth() + 1,
|
|
104
|
+
date.getDate(),
|
|
105
|
+
date.getHours(),
|
|
106
|
+
date.getMinutes(),
|
|
107
|
+
date.getSeconds(),
|
|
108
|
+
].map((value) => String(value).padStart(2, "0"));
|
|
109
|
+
return `[${date.getFullYear()}-${parts[0]}-${parts[1]} ${parts[2]}:${parts[3]}:${parts[4]}]`;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function chatName(chat, rows) {
|
|
113
|
+
const named = rows.findLast((row) => row.groupName);
|
|
114
|
+
if (named) return named.groupName;
|
|
115
|
+
if (chat.history?.groupName) return chat.history.groupName;
|
|
116
|
+
if (chat.chatGuid.includes(";+;")) return `Group ${chat.chatGuid.split(";").at(-1).replace(/^chat/, "")}`;
|
|
117
|
+
return chat.chatGuid.split(";").at(-1) || `Conversation ${chat.conversationId}`;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function compareConversations(left, right) {
|
|
121
|
+
const leftActivity = left.updatedAt ?? left.rows?.at(-1)?.time ?? 0;
|
|
122
|
+
const rightActivity = right.updatedAt ?? right.rows?.at(-1)?.time ?? 0;
|
|
123
|
+
return (
|
|
124
|
+
rightActivity - leftActivity || (right.conversationId ?? right.id) - (left.conversationId ?? left.id)
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function replyEnabled(settings, chatGuid) {
|
|
129
|
+
const { whitelist = [], blacklist = [] } = settings.chatAllowlist || {};
|
|
130
|
+
if (blacklist.includes(chatGuid)) return false;
|
|
131
|
+
if (whitelist.includes(chatGuid)) return true;
|
|
132
|
+
return !blacklist.includes("*") && whitelist.includes("*");
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function chatCard(chat, full, expanded) {
|
|
136
|
+
const card = element("section", undefined, "card");
|
|
137
|
+
card.id = `chat-${chat.conversationId}`;
|
|
138
|
+
const header = element("header", undefined, "card-header");
|
|
139
|
+
const heading = element("span");
|
|
140
|
+
heading.append(link(chat.name, `/chat?conversationId=${chat.conversationId}`));
|
|
141
|
+
heading.append(element("span", ` ${chat.rows.length} recent msgs `, "meta"));
|
|
142
|
+
heading.append(link("Task tree", `/tasks?conversationId=${chat.conversationId}`, "meta"));
|
|
143
|
+
header.append(heading);
|
|
144
|
+
const reply = element("span", chat.enabled ? "[on]" : "[off]", "reply-state");
|
|
145
|
+
reply.title = "Reply allowlist status (read-only). Change settings.json to update it.";
|
|
146
|
+
header.append(reply);
|
|
147
|
+
card.append(header);
|
|
148
|
+
const body = element("div", undefined, "card-messages");
|
|
149
|
+
if (chat.error) body.append(element("p", chat.error, "muted"));
|
|
150
|
+
else if (!chat.rows.length) body.append(element("p", "No messages yet.", "empty"));
|
|
151
|
+
else {
|
|
152
|
+
const table = element("table");
|
|
153
|
+
table.setAttribute("aria-label", `Messages in ${chat.name}`);
|
|
154
|
+
const rows = full
|
|
155
|
+
? chat.rows
|
|
156
|
+
: chat.rows.filter(
|
|
157
|
+
(message, index) =>
|
|
158
|
+
index >= chat.rows.length - 15 || expanded.has(`message-${chat.conversationId}-${message.id}`),
|
|
159
|
+
);
|
|
160
|
+
let previousTime;
|
|
161
|
+
for (const message of rows) {
|
|
162
|
+
if (previousTime && message.time - previousTime > 10 * 60 * 1000) {
|
|
163
|
+
const gap = table.insertRow().insertCell();
|
|
164
|
+
gap.colSpan = 6;
|
|
165
|
+
gap.textContent = "\u00a0";
|
|
166
|
+
}
|
|
167
|
+
previousTime = message.time;
|
|
168
|
+
const row = table.insertRow();
|
|
169
|
+
for (const [text, className] of [
|
|
170
|
+
[formatTime(message.time), "c-time"],
|
|
171
|
+
["[sid]", "c-source"],
|
|
172
|
+
[message.role === "assistant" ? "->" : "<-", "c-arrow"],
|
|
173
|
+
[`[${message.channel}]`, "c-channel"],
|
|
174
|
+
[`${message.sender}:`, "c-sender"],
|
|
175
|
+
]) {
|
|
176
|
+
row.append(element("td", text, `c-fix ${className}`));
|
|
177
|
+
}
|
|
178
|
+
const cell = element("td", undefined, "c-msg");
|
|
179
|
+
const detail = element("details", undefined, "message-detail");
|
|
180
|
+
detail.id = `message-${chat.conversationId}-${message.id}`;
|
|
181
|
+
const firstLine = message.text.split("\n").find((line) => line.trim()) || "[Empty message]";
|
|
182
|
+
const summary = element("summary", firstLine.length > 160 ? `${firstLine.slice(0, 160)}…` : firstLine);
|
|
183
|
+
if (message.status && message.status !== "sent") {
|
|
184
|
+
const status = element("span", `[${message.status}] `, "meta");
|
|
185
|
+
status.title = "Transport receipt / draft status; this is not confirmed as sent.";
|
|
186
|
+
summary.prepend(status);
|
|
187
|
+
}
|
|
188
|
+
detail.append(summary);
|
|
189
|
+
detail.append(element("pre", message.text));
|
|
190
|
+
cell.append(detail);
|
|
191
|
+
row.append(cell);
|
|
192
|
+
}
|
|
193
|
+
body.append(table);
|
|
194
|
+
}
|
|
195
|
+
card.append(body);
|
|
196
|
+
return card;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function renderChats() {
|
|
200
|
+
const expanded = new Set(Array.from(view.querySelectorAll("details[open]"), (node) => node.id));
|
|
201
|
+
const positions = new Map(
|
|
202
|
+
Array.from(view.querySelectorAll(".card"), (card) => {
|
|
203
|
+
const body = card.querySelector(".card-messages");
|
|
204
|
+
return [
|
|
205
|
+
card.id,
|
|
206
|
+
{ top: body.scrollTop, bottom: body.scrollHeight - body.scrollTop - body.clientHeight < 20 },
|
|
207
|
+
];
|
|
208
|
+
}),
|
|
209
|
+
);
|
|
210
|
+
view.replaceChildren();
|
|
211
|
+
sidebar.replaceChildren();
|
|
212
|
+
sidebar.hidden = false;
|
|
213
|
+
if (!chats.length) view.append(element("p", "No conversations.", "empty"));
|
|
214
|
+
for (const chat of chats) {
|
|
215
|
+
const card = chatCard(chat, Boolean(conversationId), expanded);
|
|
216
|
+
view.append(card);
|
|
217
|
+
for (const detail of card.querySelectorAll("details")) detail.open = expanded.has(detail.id);
|
|
218
|
+
if (!conversationId) {
|
|
219
|
+
const body = card.querySelector(".card-messages");
|
|
220
|
+
const previous = positions.get(card.id);
|
|
221
|
+
body.scrollTop = !previous || previous.bottom ? body.scrollHeight : previous.top;
|
|
222
|
+
}
|
|
223
|
+
sidebar.append(link(chat.name, `#chat-${chat.conversationId}`));
|
|
224
|
+
}
|
|
225
|
+
if (conversationId && data.history?.next) {
|
|
226
|
+
const next = new URL(location.href);
|
|
227
|
+
const bytes = new TextEncoder().encode(JSON.stringify(data.history.next));
|
|
228
|
+
next.searchParams.set(
|
|
229
|
+
"cursor",
|
|
230
|
+
btoa(Array.from(bytes, (byte) => String.fromCharCode(byte)).join(""))
|
|
231
|
+
.replaceAll("+", "-")
|
|
232
|
+
.replaceAll("/", "_")
|
|
233
|
+
.replace(/=+$/, ""),
|
|
234
|
+
);
|
|
235
|
+
view.append(link("Older messages >", next.pathname + next.search, "pagination"));
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
async function loadChats() {
|
|
240
|
+
if ((!conversationId && Array.isArray(data.conversations)) || (conversationId && data.chat)) {
|
|
241
|
+
const selected = conversationId ? [{ ...data, ...data.chat }] : data.conversations;
|
|
242
|
+
chats = selected
|
|
243
|
+
.map((detail) => {
|
|
244
|
+
const rows = messages(detail, detail.chatGuid);
|
|
245
|
+
return {
|
|
246
|
+
...detail,
|
|
247
|
+
rows,
|
|
248
|
+
name: chatName(detail, rows),
|
|
249
|
+
enabled: replyEnabled(data.settings, detail.chatGuid),
|
|
250
|
+
};
|
|
251
|
+
})
|
|
252
|
+
.sort(compareConversations);
|
|
253
|
+
renderChats();
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
// Older running servers still return native state until their next restart.
|
|
257
|
+
const [index, settings] = await Promise.all([
|
|
258
|
+
conversationId ? fetchData("/chat/data") : Promise.resolve(data),
|
|
259
|
+
fetchData("/settings/data"),
|
|
260
|
+
]);
|
|
261
|
+
const selected = (index.chats?.items || []).filter(
|
|
262
|
+
(chat) => !conversationId || String(chat.conversationId) === conversationId,
|
|
263
|
+
);
|
|
264
|
+
const loaded = [];
|
|
265
|
+
let offset = 0;
|
|
266
|
+
await Promise.all(
|
|
267
|
+
Array.from({ length: Math.min(4, selected.length) }, async () => {
|
|
268
|
+
while (offset < selected.length) {
|
|
269
|
+
const chat = selected[offset++];
|
|
270
|
+
try {
|
|
271
|
+
const detail = conversationId
|
|
272
|
+
? data
|
|
273
|
+
: await fetchData(`/chat/data?conversationId=${chat.conversationId}`);
|
|
274
|
+
const rows = messages(detail, chat.chatGuid);
|
|
275
|
+
loaded.push({
|
|
276
|
+
...chat,
|
|
277
|
+
rows,
|
|
278
|
+
name: chatName(chat, rows),
|
|
279
|
+
enabled: replyEnabled(settings, chat.chatGuid),
|
|
280
|
+
});
|
|
281
|
+
} catch (error) {
|
|
282
|
+
loaded.push({ ...chat, rows: [], name: chatName(chat, []), error: error.message });
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
}),
|
|
286
|
+
);
|
|
287
|
+
chats = loaded.sort(compareConversations);
|
|
288
|
+
renderChats();
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
function memoryRows() {
|
|
292
|
+
const records = [];
|
|
293
|
+
let incomplete = false;
|
|
294
|
+
for (const file of data) {
|
|
295
|
+
for (const line of file.records.split("\n").filter(Boolean)) {
|
|
296
|
+
try {
|
|
297
|
+
const record = JSON.parse(line);
|
|
298
|
+
if (typeof record.text !== "string") continue;
|
|
299
|
+
records.push({ ...record, namespace: record.namespace || file.namespace });
|
|
300
|
+
} catch {
|
|
301
|
+
incomplete = true;
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
const superseded = new Set(records.flatMap((record) => [record.supersedes || []].flat()));
|
|
306
|
+
for (const record of records)
|
|
307
|
+
record.effectiveStatus = superseded.has(record.id) ? "superseded" : record.status || "unknown";
|
|
308
|
+
if (incomplete)
|
|
309
|
+
showNotice("Memory snapshot is incomplete; some records or correction links may be missing.");
|
|
310
|
+
return records.sort((left, right) =>
|
|
311
|
+
String(right.created_at || "").localeCompare(String(left.created_at || "")),
|
|
312
|
+
);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
function renderMemory() {
|
|
316
|
+
const paged = !Array.isArray(data);
|
|
317
|
+
if (paged) memoryQuery = { ...data.query, page: data.page };
|
|
318
|
+
toolbar.replaceChildren();
|
|
319
|
+
setupToolbar();
|
|
320
|
+
const records = paged ? data.records : memoryRows();
|
|
321
|
+
const filtered = paged
|
|
322
|
+
? records
|
|
323
|
+
: records.filter(
|
|
324
|
+
(record) =>
|
|
325
|
+
(!memoryQuery.namespace || record.namespace === memoryQuery.namespace) &&
|
|
326
|
+
(!memoryQuery.kind || record.kind === memoryQuery.kind) &&
|
|
327
|
+
(!memoryQuery.status || record.effectiveStatus === memoryQuery.status) &&
|
|
328
|
+
JSON.stringify(record).toLowerCase().includes(memoryQuery.q.toLowerCase()),
|
|
329
|
+
);
|
|
330
|
+
const pages = paged ? data.pages : Math.max(1, Math.ceil(filtered.length / 20));
|
|
331
|
+
memoryQuery.page = Math.min(memoryQuery.page, pages);
|
|
332
|
+
if (paged && data.invalidRecords)
|
|
333
|
+
showNotice(`Skipped ${data.invalidRecords} invalid memory records. Source files were not modified.`);
|
|
334
|
+
view.replaceChildren(
|
|
335
|
+
element(
|
|
336
|
+
"p",
|
|
337
|
+
`${paged ? data.total : filtered.length} matching / ${paged ? data.recordCount : records.length} records · read-only`,
|
|
338
|
+
"counts",
|
|
339
|
+
),
|
|
340
|
+
);
|
|
341
|
+
if (!filtered.length) view.append(element("p", "No matching memory records.", "empty"));
|
|
342
|
+
for (const record of paged
|
|
343
|
+
? filtered
|
|
344
|
+
: filtered.slice((memoryQuery.page - 1) * 20, memoryQuery.page * 20)) {
|
|
345
|
+
const card = element("article", undefined, "memory-record");
|
|
346
|
+
const header = element("header", undefined, "row-head");
|
|
347
|
+
header.append(element("span", `${record.namespace} / ${record.kind || "unknown"}`));
|
|
348
|
+
header.append(
|
|
349
|
+
element("span", `${record.effectiveStatus} · ${record.event_time || record.created_at || ""}`, "meta"),
|
|
350
|
+
);
|
|
351
|
+
card.append(header, element("p", record.text, "memory-text"));
|
|
352
|
+
if (Array.isArray(record.subjects) && record.subjects.length)
|
|
353
|
+
card.append(element("p", record.subjects.join(" / "), "muted"));
|
|
354
|
+
const details = element("details", undefined, "record-details");
|
|
355
|
+
details.id = `record-${record.id}`;
|
|
356
|
+
const summary = element("summary", "...", "record-details-icon");
|
|
357
|
+
summary.setAttribute("aria-label", "Record details");
|
|
358
|
+
summary.title = "Details";
|
|
359
|
+
details.append(summary);
|
|
360
|
+
details.append(element("pre", JSON.stringify(record, null, 2)));
|
|
361
|
+
card.append(details);
|
|
362
|
+
view.append(card);
|
|
363
|
+
}
|
|
364
|
+
const pagination = element("nav", undefined, "pagination");
|
|
365
|
+
pagination.setAttribute("aria-label", "Memory pagination");
|
|
366
|
+
for (const [label, delta] of [
|
|
367
|
+
["< Previous", -1],
|
|
368
|
+
["Next >", 1],
|
|
369
|
+
]) {
|
|
370
|
+
const button = element("button", label);
|
|
371
|
+
button.type = "button";
|
|
372
|
+
button.disabled = memoryQuery.page + delta < 1 || memoryQuery.page + delta > pages;
|
|
373
|
+
button.addEventListener("click", () => {
|
|
374
|
+
memoryQuery.page += delta;
|
|
375
|
+
if (paged) void refresh();
|
|
376
|
+
else renderMemory();
|
|
377
|
+
main.scrollTop = 0;
|
|
378
|
+
});
|
|
379
|
+
pagination.append(button);
|
|
380
|
+
if (delta === -1) pagination.append(element("span", `Page ${memoryQuery.page} / ${pages}`));
|
|
381
|
+
}
|
|
382
|
+
view.append(pagination);
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
function renderSettings() {
|
|
386
|
+
view.replaceChildren(
|
|
387
|
+
element(
|
|
388
|
+
"p",
|
|
389
|
+
"Read-only runtime configuration. Edit settings.json and use /reload to apply changes.",
|
|
390
|
+
"muted",
|
|
391
|
+
),
|
|
392
|
+
);
|
|
393
|
+
for (const [title, rows] of [
|
|
394
|
+
[
|
|
395
|
+
"Chat replies",
|
|
396
|
+
[
|
|
397
|
+
["Allowlist", data.chatAllowlist.whitelist],
|
|
398
|
+
["Blocklist", data.chatAllowlist.blacklist],
|
|
399
|
+
],
|
|
400
|
+
],
|
|
401
|
+
[
|
|
402
|
+
"Rich text",
|
|
403
|
+
[
|
|
404
|
+
["Enabled", data.richText.enabled],
|
|
405
|
+
["Markdown", data.richText.markdown],
|
|
406
|
+
],
|
|
407
|
+
],
|
|
408
|
+
]) {
|
|
409
|
+
const card = element("section", undefined, "card");
|
|
410
|
+
card.append(element("header", title, "card-header"));
|
|
411
|
+
const table = element("table", undefined, "setting-table");
|
|
412
|
+
for (const [name, value] of rows) {
|
|
413
|
+
const row = table.insertRow();
|
|
414
|
+
row.append(element("th", name));
|
|
415
|
+
const cell = row.insertCell();
|
|
416
|
+
if (Array.isArray(value)) {
|
|
417
|
+
const list = element("ul");
|
|
418
|
+
for (const item of value) list.append(element("li", item));
|
|
419
|
+
cell.append(value.length ? list : element("span", "None", "muted"));
|
|
420
|
+
} else cell.textContent = value ? "on" : "off";
|
|
421
|
+
}
|
|
422
|
+
card.append(table);
|
|
423
|
+
view.append(card);
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
function renderLogs() {
|
|
428
|
+
const scrollPositions = Array.from(view.querySelectorAll(".card-content"), (node) => ({
|
|
429
|
+
top: node.scrollTop,
|
|
430
|
+
bottom: node.scrollHeight - node.scrollTop - node.clientHeight < 20,
|
|
431
|
+
}));
|
|
432
|
+
view.replaceChildren();
|
|
433
|
+
const host = element("section", undefined, "card");
|
|
434
|
+
host.append(element("header", "service.log", "card-header"));
|
|
435
|
+
const hostBody = element("div", undefined, "card-content");
|
|
436
|
+
hostBody.append(
|
|
437
|
+
element("pre", data.hostLog || "No service.log yet. Foreground output remains in the startup Terminal."),
|
|
438
|
+
);
|
|
439
|
+
host.append(hostBody);
|
|
440
|
+
view.append(host);
|
|
441
|
+
const tasks = element("section", undefined, "card");
|
|
442
|
+
tasks.append(element("header", "Native execution history", "card-header"));
|
|
443
|
+
const taskBody = element("div", undefined, "card-content");
|
|
444
|
+
const table = element("table", undefined, "task-table");
|
|
445
|
+
const heading = table.insertRow();
|
|
446
|
+
for (const label of ["ID", "Type", "State", "Details"]) heading.append(element("th", label));
|
|
447
|
+
for (const task of data.tasks?.items || []) {
|
|
448
|
+
const row = table.insertRow();
|
|
449
|
+
for (const value of [task.id, task.kind || task.type || "task", task.state?.status || task.status || "—"])
|
|
450
|
+
row.append(element("td", value));
|
|
451
|
+
const detail = element("details");
|
|
452
|
+
detail.id = `task-${task.id}`;
|
|
453
|
+
detail.append(element("summary", "Details"), element("pre", JSON.stringify(task, null, 2)));
|
|
454
|
+
row.insertCell().append(detail);
|
|
455
|
+
}
|
|
456
|
+
taskBody.append(table);
|
|
457
|
+
if (!data.tasks?.items?.length) taskBody.append(element("p", "No task records.", "empty"));
|
|
458
|
+
if (data.tasks?.next) {
|
|
459
|
+
const next = new URL(location.href);
|
|
460
|
+
next.searchParams.set(
|
|
461
|
+
"cursor",
|
|
462
|
+
btoa(JSON.stringify(data.tasks.next)).replaceAll("+", "-").replaceAll("/", "_").replace(/=+$/, ""),
|
|
463
|
+
);
|
|
464
|
+
taskBody.append(link("Older tasks >", next.pathname + next.search, "pagination"));
|
|
465
|
+
}
|
|
466
|
+
tasks.append(taskBody);
|
|
467
|
+
view.append(tasks);
|
|
468
|
+
[hostBody, taskBody].forEach((node, index) => {
|
|
469
|
+
const previous = scrollPositions[index];
|
|
470
|
+
node.scrollTop = !previous || previous.bottom ? node.scrollHeight : previous.top;
|
|
471
|
+
});
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
function renderScheduled() {
|
|
475
|
+
document.querySelector(".native-state")?.remove();
|
|
476
|
+
view.replaceChildren();
|
|
477
|
+
function time(timestamp, timezone) {
|
|
478
|
+
return typeof timestamp === "number"
|
|
479
|
+
? new Date(timestamp).toLocaleString("sv-SE", { timeZone: timezone })
|
|
480
|
+
: "-";
|
|
481
|
+
}
|
|
482
|
+
function history(runs, conversationId, timezone) {
|
|
483
|
+
const body = element("div", undefined, "card-content");
|
|
484
|
+
const table = element("table", undefined, "task-table");
|
|
485
|
+
const heading = table.insertRow();
|
|
486
|
+
for (const label of ["Task", "Due", "Started", "Finished", "State / delivery", "Result"])
|
|
487
|
+
heading.append(element("th", label));
|
|
488
|
+
for (const run of runs) {
|
|
489
|
+
const row = table.insertRow();
|
|
490
|
+
row
|
|
491
|
+
.insertCell()
|
|
492
|
+
.append(
|
|
493
|
+
link(
|
|
494
|
+
`${run.name || "Execution"} #${run.id}`,
|
|
495
|
+
`/tasks?conversationId=${conversationId}#tree-task-${run.id}`,
|
|
496
|
+
),
|
|
497
|
+
);
|
|
498
|
+
for (const value of [
|
|
499
|
+
time(run.input?.dueAt, timezone),
|
|
500
|
+
time(run.input?.startedAt, timezone),
|
|
501
|
+
time(run.result?.finishedAt, timezone),
|
|
502
|
+
[run.status, run.delivery].filter(Boolean).join(" / "),
|
|
503
|
+
run.error || run.result?.summary || "In progress",
|
|
504
|
+
])
|
|
505
|
+
row.append(element("td", value));
|
|
506
|
+
}
|
|
507
|
+
body.append(table);
|
|
508
|
+
if (!runs.length) body.append(element("p", "No executions yet.", "empty"));
|
|
509
|
+
return body;
|
|
510
|
+
}
|
|
511
|
+
for (const job of data.jobs || []) {
|
|
512
|
+
const details = element("details", undefined, "schedule-row");
|
|
513
|
+
details.id = `schedule-history-${job.id}`;
|
|
514
|
+
const latest = job.runs?.[0];
|
|
515
|
+
const status = !job.enabled
|
|
516
|
+
? "disabled"
|
|
517
|
+
: ["aborted", "faulted", "failed", "missing"].includes(job.status)
|
|
518
|
+
? job.status
|
|
519
|
+
: latest
|
|
520
|
+
? [latest.status, latest.delivery].filter(Boolean).join(" / ")
|
|
521
|
+
: "enabled";
|
|
522
|
+
const lastRun = latest ? time(latest.input?.startedAt, job.timezone) : "No executions yet";
|
|
523
|
+
const cadence = job.time !== undefined ? `daily ${job.time}` : `every ${job.intervalMs / 3600000}h`;
|
|
524
|
+
const summary = element("summary", undefined, "schedule-summary");
|
|
525
|
+
summary.append(
|
|
526
|
+
element("span", job.name, "schedule-name"),
|
|
527
|
+
element(
|
|
528
|
+
"span",
|
|
529
|
+
`${cadence} / Next: ${time(job.nextAt, job.timezone)} (${job.timezone})`,
|
|
530
|
+
"schedule-next",
|
|
531
|
+
),
|
|
532
|
+
element("span", `Last: ${lastRun}`, "schedule-last"),
|
|
533
|
+
element("span", status, "schedule-status"),
|
|
534
|
+
);
|
|
535
|
+
details.append(summary, history(job.runs || [], job.conversationId, job.timezone));
|
|
536
|
+
view.append(details);
|
|
537
|
+
}
|
|
538
|
+
if (!data.jobs?.length) view.append(element("p", "No scheduled tasks.", "empty"));
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
function renderTasks() {
|
|
542
|
+
view.replaceChildren();
|
|
543
|
+
const children = new Map();
|
|
544
|
+
const conversations = new Map(data.conversations.map((conversation) => [conversation.id, conversation]));
|
|
545
|
+
const chatNames = new Map(
|
|
546
|
+
(data.chats?.items || []).map((chat) => [
|
|
547
|
+
chat.conversationId,
|
|
548
|
+
chatName({ ...chat, history: { groupName: conversations.get(chat.conversationId)?.groupName } }, []),
|
|
549
|
+
]),
|
|
550
|
+
);
|
|
551
|
+
const conversationNames = new Map(chatNames);
|
|
552
|
+
const scheduleNames = new Map((data.schedules?.items || []).map((job) => [job.conversationId, job.name]));
|
|
553
|
+
for (const [id, name] of scheduleNames) conversationNames.set(id, name);
|
|
554
|
+
for (const session of data.sessions?.items || []) {
|
|
555
|
+
if (!conversationNames.has(session.conversationId))
|
|
556
|
+
conversationNames.set(session.conversationId, session.label);
|
|
557
|
+
}
|
|
558
|
+
function conversationName(id) {
|
|
559
|
+
const name = conversationNames.get(id);
|
|
560
|
+
if (name) return name;
|
|
561
|
+
const owner = conversations.get(id)?.owner;
|
|
562
|
+
if (!owner) return "";
|
|
563
|
+
const parentName = conversationName(owner.conversationId);
|
|
564
|
+
return parentName ? `${parentName} / subagent` : "subagent";
|
|
565
|
+
}
|
|
566
|
+
function belongsToChat(id) {
|
|
567
|
+
if (chatNames.has(id) || scheduleNames.has(id)) return true;
|
|
568
|
+
const owner = conversations.get(id)?.owner;
|
|
569
|
+
return owner ? belongsToChat(owner.conversationId) : false;
|
|
570
|
+
}
|
|
571
|
+
for (const conversation of data.conversations.toSorted(compareConversations)) {
|
|
572
|
+
const key = conversation.owner ? `task-${conversation.owner.taskId}` : "root";
|
|
573
|
+
if (!children.has(key)) children.set(key, []);
|
|
574
|
+
children.get(key).push({ type: "conversation", ...conversation });
|
|
575
|
+
}
|
|
576
|
+
for (const task of data.tasks) {
|
|
577
|
+
const key = task.owner === undefined ? `conversation-${task.conversationId}` : `task-${task.owner}`;
|
|
578
|
+
if (!children.has(key)) children.set(key, []);
|
|
579
|
+
children.get(key).push({ type: "task", ...task });
|
|
580
|
+
}
|
|
581
|
+
function renderNode(node) {
|
|
582
|
+
const item = element("li");
|
|
583
|
+
const key = `${node.type}-${node.id}`;
|
|
584
|
+
item.id = `tree-${key}`;
|
|
585
|
+
if (node.type === "conversation") {
|
|
586
|
+
const heading = element("span", `Conversation #${node.id}`);
|
|
587
|
+
const name = conversationName(node.id);
|
|
588
|
+
if (name) heading.append(element("span", ` / ${name} `, "meta"));
|
|
589
|
+
if (chatNames.has(node.id)) {
|
|
590
|
+
heading.append(link("Chat", `/chat?conversationId=${node.id}`, "meta"));
|
|
591
|
+
}
|
|
592
|
+
item.append(heading);
|
|
593
|
+
} else {
|
|
594
|
+
const detail = element("details");
|
|
595
|
+
detail.id = key;
|
|
596
|
+
const summary = element("summary", `${node.kind}${node.name ? `: ${node.name}` : ""} #${node.id}`);
|
|
597
|
+
const status = [node.status, node.phase, node.outcome].filter(Boolean).join(" / ");
|
|
598
|
+
const waiting = node.on?.length ? ` on [${node.on.join(", ")}]` : "";
|
|
599
|
+
const flags = `${node.background ? " [background]" : ""}${node.abortRequested ? " [abort requested]" : ""}`;
|
|
600
|
+
summary.append(element("span", ` ${status}${waiting}${flags}`, "meta"));
|
|
601
|
+
const content = element("pre", "Loading latest state...");
|
|
602
|
+
detail.append(summary, content);
|
|
603
|
+
let loaded = false;
|
|
604
|
+
detail.addEventListener("toggle", async () => {
|
|
605
|
+
if (!detail.open || loaded) return;
|
|
606
|
+
loaded = true;
|
|
607
|
+
try {
|
|
608
|
+
const task = await fetchData(`/tasks/task/data?taskId=${node.id}`);
|
|
609
|
+
content.textContent = JSON.stringify(task, null, 2);
|
|
610
|
+
} catch (error) {
|
|
611
|
+
loaded = false;
|
|
612
|
+
content.textContent = error.message;
|
|
613
|
+
}
|
|
614
|
+
});
|
|
615
|
+
item.append(detail);
|
|
616
|
+
}
|
|
617
|
+
const descendants = children.get(key) || [];
|
|
618
|
+
if (descendants.length) {
|
|
619
|
+
const list = element("ul", undefined, "task-tree");
|
|
620
|
+
for (const child of descendants) list.append(renderNode(child));
|
|
621
|
+
item.append(list);
|
|
622
|
+
}
|
|
623
|
+
return item;
|
|
624
|
+
}
|
|
625
|
+
const roots = conversationId
|
|
626
|
+
? data.conversations
|
|
627
|
+
.filter((conversation) => String(conversation.id) === conversationId)
|
|
628
|
+
.map((conversation) => ({ type: "conversation", ...conversation }))
|
|
629
|
+
: children.get("root") || [];
|
|
630
|
+
const visible = roots.filter((conversation) => belongsToChat(conversation.id));
|
|
631
|
+
if (!visible.length) view.append(element("p", "No conversations.", "empty"));
|
|
632
|
+
for (const conversation of visible.toSorted(compareConversations)) {
|
|
633
|
+
const card = element("section", undefined, "card");
|
|
634
|
+
card.id = `tasks-${conversation.id}`;
|
|
635
|
+
const list = element("ul", undefined, "task-tree task-root");
|
|
636
|
+
list.append(renderNode(conversation));
|
|
637
|
+
card.append(list);
|
|
638
|
+
view.append(card);
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
function setupToolbar() {
|
|
643
|
+
if (page === "/chat" || page === "/tasks") {
|
|
644
|
+
if (conversationId) {
|
|
645
|
+
toolbar.className = "toolbar";
|
|
646
|
+
toolbar.append(page === "/chat" ? link("< Recent Chats", "/") : link("< All conversations", "/tasks"));
|
|
647
|
+
}
|
|
648
|
+
} else if (page === "/memory") {
|
|
649
|
+
const records = Array.isArray(data) ? memoryRows() : data.records;
|
|
650
|
+
const form = element("form", undefined, "toolbar");
|
|
651
|
+
const searchLabel = element("label", "Search text / subjects / sources", "search");
|
|
652
|
+
const search = element("input");
|
|
653
|
+
search.type = "search";
|
|
654
|
+
search.name = "q";
|
|
655
|
+
search.value = memoryQuery.q;
|
|
656
|
+
search.placeholder = "Keyword, record ID or source";
|
|
657
|
+
searchLabel.append(search);
|
|
658
|
+
form.append(searchLabel);
|
|
659
|
+
for (const [name, label, values] of [
|
|
660
|
+
[
|
|
661
|
+
"namespace",
|
|
662
|
+
"Namespace",
|
|
663
|
+
data.namespaces || [...new Set(records.map((record) => record.namespace))].sort(),
|
|
664
|
+
],
|
|
665
|
+
[
|
|
666
|
+
"kind",
|
|
667
|
+
"Kind",
|
|
668
|
+
data.kinds || [...new Set(records.map((record) => record.kind).filter(Boolean))].sort(),
|
|
669
|
+
],
|
|
670
|
+
["status", "Status", ["active", "superseded", "deleted", "unknown"]],
|
|
671
|
+
]) {
|
|
672
|
+
const field = element("label", label);
|
|
673
|
+
const select = element("select");
|
|
674
|
+
select.name = name;
|
|
675
|
+
select.append(new Option("All", ""));
|
|
676
|
+
for (const value of values) select.append(new Option(value, value));
|
|
677
|
+
if (memoryQuery[name] && !values.includes(memoryQuery[name]))
|
|
678
|
+
select.append(new Option(memoryQuery[name], memoryQuery[name]));
|
|
679
|
+
select.value = memoryQuery[name];
|
|
680
|
+
field.append(select);
|
|
681
|
+
form.append(field);
|
|
682
|
+
}
|
|
683
|
+
const button = element("button", "Filter");
|
|
684
|
+
button.type = "submit";
|
|
685
|
+
form.append(button);
|
|
686
|
+
form.addEventListener("submit", (event) => {
|
|
687
|
+
event.preventDefault();
|
|
688
|
+
memoryQuery = { ...memoryQuery, ...Object.fromEntries(new FormData(form)), page: 1 };
|
|
689
|
+
void refresh();
|
|
690
|
+
main.scrollTop = 0;
|
|
691
|
+
});
|
|
692
|
+
toolbar.append(form);
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
async function render() {
|
|
697
|
+
const openDetails = new Set(Array.from(view.querySelectorAll("details[open]"), (node) => node.id));
|
|
698
|
+
const scrollTop = main.scrollTop;
|
|
699
|
+
const windowPositions = new Map(
|
|
700
|
+
Array.from(view.querySelectorAll(".card"), (card) => [card.id, card.scrollTop]),
|
|
701
|
+
);
|
|
702
|
+
if (page === "/chat") await loadChats();
|
|
703
|
+
else if (page === "/memory") renderMemory();
|
|
704
|
+
else if (page === "/settings") renderSettings();
|
|
705
|
+
else if (page === "/logs") renderLogs();
|
|
706
|
+
else if (page === "/tasks") renderTasks();
|
|
707
|
+
else if (page === "/scheduled") renderScheduled();
|
|
708
|
+
for (const node of view.querySelectorAll("details")) node.open = openDetails.has(node.id);
|
|
709
|
+
for (const card of view.querySelectorAll(".card")) card.scrollTop = windowPositions.get(card.id) || 0;
|
|
710
|
+
main.scrollTop = scrollTop;
|
|
711
|
+
view.setAttribute("aria-busy", "false");
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
async function refresh() {
|
|
715
|
+
if (refreshing) return;
|
|
716
|
+
refreshing = true;
|
|
717
|
+
document.getElementById("refresh").disabled = true;
|
|
718
|
+
try {
|
|
719
|
+
const query = new URLSearchParams(location.search);
|
|
720
|
+
if (page === "/chat") query.set("view", conversationId ? "display" : "overview");
|
|
721
|
+
if (page === "/memory") {
|
|
722
|
+
query.set("view", "records");
|
|
723
|
+
for (const [key, value] of Object.entries(memoryQuery)) query.set(key, String(value));
|
|
724
|
+
}
|
|
725
|
+
data = await fetchData(`${page}/data?${query}`);
|
|
726
|
+
state.textContent = JSON.stringify(data, null, 2);
|
|
727
|
+
showNotice("");
|
|
728
|
+
await render();
|
|
729
|
+
} catch (error) {
|
|
730
|
+
showNotice(error.message);
|
|
731
|
+
} finally {
|
|
732
|
+
refreshing = false;
|
|
733
|
+
document.getElementById("refresh").disabled = false;
|
|
734
|
+
view.setAttribute("aria-busy", "false");
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
if (page !== "/memory") setupToolbar();
|
|
739
|
+
refreshing = true;
|
|
740
|
+
render()
|
|
741
|
+
.catch((error) => {
|
|
742
|
+
showNotice(error.message);
|
|
743
|
+
view.setAttribute("aria-busy", "false");
|
|
744
|
+
})
|
|
745
|
+
.finally(() => {
|
|
746
|
+
refreshing = false;
|
|
747
|
+
});
|
|
748
|
+
document.getElementById("refresh").addEventListener("click", refresh);
|