Add audit and memory UI

This commit is contained in:
2026-05-20 23:24:12 +08:00
parent 61cdd8bbaf
commit 3d815d3ae3
6 changed files with 532 additions and 4 deletions
+109
View File
@@ -3,6 +3,7 @@ const state = {
messages: [],
currentConversationId: "",
conversations: [],
auditFilter: "all",
};
async function jsonFetch(url, options) {
@@ -372,6 +373,81 @@ async function refreshEvents(taskId) {
return events;
}
async function refreshCommandAudit() {
const container = document.querySelector("#command-audit");
if (!container) return;
const events = await jsonFetch("/v1/audit/commands?limit=20");
container.innerHTML = "";
if (!events.length) {
const empty = document.createElement("p");
empty.className = "compact-empty";
empty.textContent = "No command events yet.";
container.append(empty);
return;
}
for (const event of events) {
const payload = event.payload || {};
const item = document.createElement("article");
item.className = "audit-item";
const command = document.createElement("code");
command.textContent = payload.command || "shell command";
const meta = document.createElement("div");
meta.className = "audit-meta";
meta.append(
auditBadge(payload.action_type || "shell_command"),
auditBadge(payload.risk_level || "unknown", payload.risk_level),
auditBadge(payload.ok ? "ok" : "failed", payload.ok ? "ok" : "bad"),
);
const detail = document.createElement("span");
detail.textContent = [
payload.approved ? "approved" : "direct",
payload.blocked ? "blocked" : "",
payload.returncode !== null && payload.returncode !== undefined ? `exit ${payload.returncode}` : "",
].filter(Boolean).join(" · ");
item.append(command, meta, detail);
container.append(item);
}
}
function auditBadge(text, tone = "") {
const badge = document.createElement("span");
badge.className = "audit-badge";
badge.textContent = text;
if (tone) badge.dataset.tone = tone;
return badge;
}
async function refreshMemory(query = "") {
const container = document.querySelector("#memory-list");
if (!container) return;
const workspace = document.querySelector("#workspace")?.value || "./workspace";
const url = query.trim()
? `/v1/memory/search?q=${encodeURIComponent(query.trim())}&workspace=${encodeURIComponent(workspace)}`
: `/v1/memory?workspace=${encodeURIComponent(workspace)}&limit=20`;
const payload = await jsonFetch(url);
const results = payload.results || [];
container.innerHTML = "";
if (!results.length) {
const empty = document.createElement("p");
empty.className = "compact-empty";
empty.textContent = query.trim() ? "No matching memories." : "No workspace memories.";
container.append(empty);
return;
}
for (const memory of results) {
const item = document.createElement("article");
item.className = "memory-item";
const text = document.createElement("p");
text.textContent = memory.text;
const meta = document.createElement("span");
meta.textContent = `${memory.memory_type || "note"} · ${Number(memory.importance || 0).toFixed(1)} · ${memory.created_at || ""}`;
item.append(text, meta);
container.append(item);
}
}
function summarizeEvent(payload) {
if (!payload || typeof payload !== "object") return "";
if (payload.role && payload.latency_ms) {
@@ -496,6 +572,8 @@ async function handleAssistantStreamEvent(pending, name, data, context) {
setStatus("#task-status", data.task_id, data.status === "completed" ? "ok" : "warn");
finishInlineReasoning(pending, data.reasoning_content);
await refreshEvents(data.task_id);
await refreshCommandAudit();
await refreshMemory();
await refreshConversations();
return;
}
@@ -592,6 +670,7 @@ async function selectConversation(conversationId) {
for (const message of conversation.messages) addStoredMessage(message);
}
await refreshConversations();
await refreshMemory();
}
async function createNewConversation() {
@@ -677,6 +756,34 @@ function bindChat() {
document.querySelector("#reload-chat")?.addEventListener("click", () => {
if (state.currentConversationId) selectConversation(state.currentConversationId).catch(console.error);
});
document.querySelector("#refresh-audit")?.addEventListener("click", () => {
refreshCommandAudit().catch(console.error);
});
document.querySelector("#refresh-memory")?.addEventListener("click", () => {
refreshMemory().catch(console.error);
});
document.querySelector("#memory-search-button")?.addEventListener("click", () => {
refreshMemory(document.querySelector("#memory-search-inline")?.value || "").catch(console.error);
});
document.querySelector("#memory-form")?.addEventListener("submit", async (event) => {
event.preventDefault();
const input = document.querySelector("#memory-text");
const text = input?.value.trim() || "";
if (!text) return;
await jsonFetch("/v1/memory", {
method: "POST",
headers: {"Content-Type": "application/json"},
body: JSON.stringify({
text,
workspace: document.querySelector("#workspace")?.value || "./workspace",
conversation_id: state.currentConversationId || null,
memory_type: "note",
importance: 0.6,
}),
});
input.value = "";
await refreshMemory();
});
document.querySelector("#conversation-list")?.addEventListener("click", (event) => {
const item = event.target.closest("[data-conversation-id]");
if (item) selectConversation(item.dataset.conversationId).catch(console.error);
@@ -798,6 +905,8 @@ document.querySelector("#memory-search")?.addEventListener("click", async () =>
bindChat();
checkRuntime();
loadSimplePages().catch(console.error);
refreshCommandAudit().catch(console.error);
refreshMemory().catch(console.error);
refreshConversations().then(() => {
if (state.conversations[0]) {
return selectConversation(state.conversations[0].conversation_id);
+117 -2
View File
@@ -683,7 +683,7 @@ dd {
.debug-panel {
display: grid;
grid-template-columns: minmax(0, 1fr);
grid-template-columns: minmax(0, 1fr) minmax(260px, 0.8fr) minmax(280px, 0.85fr);
gap: 16px;
min-height: 180px;
}
@@ -701,6 +701,30 @@ dd {
font-size: 13px;
}
.debug-heading {
display: flex;
align-items: center;
justify-content: space-between;
gap: 10px;
margin-bottom: 10px;
}
.debug-heading h3 {
margin: 0;
}
.mini-button,
.memory-form button,
.memory-search-row button {
border: 0;
border-radius: 7px;
padding: 7px 9px;
background: #edf2f7;
color: #1f2937;
font-size: 12px;
font-weight: 800;
}
pre,
#events {
margin: 0;
@@ -728,6 +752,97 @@ pre,
color: var(--muted);
}
.audit-list,
.memory-list {
display: grid;
gap: 8px;
max-height: 170px;
overflow: auto;
}
.audit-item,
.memory-item {
display: grid;
gap: 6px;
padding: 9px;
background: #f8fafc;
border: 1px solid #dbe3ee;
border-radius: 8px;
}
.audit-item code {
min-width: 0;
overflow: hidden;
color: #0f172a;
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
font-size: 12px;
text-overflow: ellipsis;
white-space: nowrap;
}
.audit-meta {
display: flex;
flex-wrap: wrap;
gap: 5px;
}
.audit-badge {
padding: 2px 7px;
border-radius: 999px;
background: #e2e8f0;
color: #475569;
font-size: 11px;
font-weight: 800;
}
.audit-badge[data-tone="low"],
.audit-badge[data-tone="ok"] {
background: #dcfce7;
color: #166534;
}
.audit-badge[data-tone="medium"],
.audit-badge[data-tone="high"] {
background: #fef3c7;
color: #854d0e;
}
.audit-badge[data-tone="critical"],
.audit-badge[data-tone="bad"] {
background: #fee2e2;
color: #991b1b;
}
.audit-item > span,
.memory-item span,
.compact-empty {
margin: 0;
color: var(--muted);
font-size: 11px;
}
.memory-form,
.memory-search-row {
display: grid;
grid-template-columns: minmax(0, 1fr) auto;
gap: 8px;
margin-bottom: 8px;
}
.memory-form input,
.memory-search-row input {
min-width: 0;
padding: 8px 9px;
font-size: 12px;
}
.memory-item p {
margin: 0;
color: #0f172a;
font-size: 12px;
line-height: 1.35;
}
.composer {
display: grid;
gap: 10px;
@@ -789,7 +904,6 @@ pre,
}
.chat-header,
.debug-panel,
.composer-actions {
grid-template-columns: 1fr;
flex-direction: column;
@@ -798,5 +912,6 @@ pre,
.debug-panel {
display: grid;
grid-template-columns: 1fr;
}
}