Add audit and memory UI
This commit is contained in:
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user