Skip to content

Commit f734755

Browse files
committed
feat(desktop): move process events to dedicated rail section
Give the process log its own Events rail view with a full-height table, detail panel, and #events routing; remove the cramped table from Graph Overview and place the workspace logo above Settings in the rail bottom.
1 parent d36d329 commit f734755

5 files changed

Lines changed: 337 additions & 97 deletions

File tree

libs/naas-abi/naas_abi/apps/desktop/AGENTS.md

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,7 @@ desktop/
125125

126126
A workspace is a **folder on disk** (VS Code / Cursor semantics), not a Nexus tenant.
127127

128-
- **UI**: icon rail top (Nexus sidebar-top pattern): **logo button only** opens a glass portal dropdown to the right (square corners). Hover on the logo shows the full path; the menu lists recent workspaces with a checkmark on the active entry and **Open Folder…**. The status bar left shows the current workspace basename and git branch as read-only context (no switch action). Top bar and main body stay clean (panel toggle + section title only).
128+
- **UI**: icon rail bottom (above Settings): **logo button only** opens a glass portal dropdown to the right (square corners). Hover on the logo shows the full path; the menu lists recent workspaces with a checkmark on the active entry and **Open Folder…**. The status bar left shows the current workspace basename and git branch as read-only context (no switch action). Top bar and main body stay clean (panel toggle + section title only). Rail top is an empty spacer.
129129
- **Switch / open**: `POST /api/workspaces/open` or `PUT /api/settings` with a new `workspace_root`. Triggers `ensure_workspace`, harness restart, terminal reconnect, file index refresh, org/model context reload, and graph rescaffold.
130130
- **Recent list**: `recent_workspaces` setting (JSON array, max 10 paths). Updated on every open/switch.
131131
- **First run**: `maybe_upgrade_workspace_setting()` auto-detects `~/abi` (git + `.env`) when still on the factory default.
@@ -196,10 +196,15 @@ Scaffolded `instances.ttl` seeds concrete routing individuals:
196196
**Iteration 7** (current):
197197

198198
- `GET /api/processes` — paginated event log rows (timestamp, label, type, seven bucket columns with known/shared/unknown status, `graph_node_id` for graph focus)
199-
- Graph Overview tab: scrollable **Process events** table above the vis-network canvas; click a row to select and focus the matching process node (and its bucket spokes)
199+
200+
**Iteration 8** (current):
201+
202+
- **Events rail section**: dedicated `#events` main view with full-height process events table (`GET /api/processes`); row click opens detail panel, double-click or **View in Graph** navigates to Graph Overview and focuses the process node
203+
- Workspace logo switcher moved to rail bottom (above Settings); rail top is spacer only
204+
- Graph Overview tab: vis-network canvas only (process events table removed from split view above graph)
200205
- Tables tab still shows raw SQLite dumps (`processes`, `process_aspects`) for debugging
201206

202-
**Iteration 8 targets**:
207+
**Iteration 9 targets**:
203208
- Per-tool `SectionRoute` instances (terminal, file edit, SPARQL) with disposition-based routing
204209
- Bidirectional sync: settings UI writes back to `instances.ttl` when agents/models change
205210
- Visual BFO7 bucket diagram in Graph UI (seven-bucket layout, not just route summary)
@@ -443,8 +448,7 @@ The Code section explorer accepts files dragged from macOS Finder:
443448

444449
The Graph section combines a **vis-network** canvas with search, group filters, and a right-hand detail panel (inspired by `bob/docs/ontology/bob_ontology.html` and Nexus graph explorer).
445450

446-
- **Layout (Overview tab)**: toolbar with search + group filter chips; **process events table** (scrollable, max ~220px) above the vis-network canvas; optional left node list when search matches; center vis-network canvas; right inspector panel (~320px, square corners, slide-in on selection).
447-
- **Process events table**: `GET /api/processes`; columns When, Label, Type, plus seven BFO buckets; status styling (known/shared/unknown); click row focuses `graph_node_id` and connected bucket nodes in the graph below.
451+
- **Layout (Overview tab)**: toolbar with search + group filter chips; center vis-network canvas; right inspector panel (~320px, square corners, slide-in on selection).
448452
- **Search**: filters nodes by label/id/group as you type; matching nodes stay highlighted, others dim; Enter cycles matches and focuses the node; Escape clears search.
449453
- **Group filters**: toggle chips per node group (context, route, language_model, …); hidden groups are removed from the canvas.
450454
- **Right panel**: opens on node click; shows properties from `node.detail`, `can_realize` tags for language models, TTL annotations for BFO buckets, and incoming/outgoing relations (clickable to jump).
@@ -454,6 +458,15 @@ The Graph section combines a **vis-network** canvas with search, group filters,
454458
- **View in browser dev**: `uv run python libs/naas-abi/naas_abi/apps/desktop/run.py --browser-only` → Knowledge Graph rail icon → Overview tab → search or click a node.
455459
- **References**: `bob/docs/ontology/bob_ontology.html` (inspector + neighbourhood highlight), `apps/nexus/apps/web/src/components/graph/vis-network.tsx` (layout/physics), `knowledge-graph-section.tsx` (sidebar structure).
456460

461+
### Events section (process log)
462+
463+
Dedicated rail section (`#events`) for the SQLite process event log.
464+
465+
- **Layout**: full-height scrollable table (When, Label, Type, seven BFO bucket columns); right detail panel (~320px) on row select; optional navigation to Graph Overview via double-click or **View in Graph**.
466+
- **Data**: `GET /api/processes?limit=100` (paginated; same payload as iteration 7).
467+
- **Row actions**: single click selects row and shows bucket detail; double-click or detail action focuses `graph_node_id` in the Graph section (loads overview if needed).
468+
- **Panel**: side panel shows "Process log" hint; main table fills the content area.
469+
457470
## Adding features
458471

459472
- Keep the frontend dependency-free at runtime (vanilla JS + the vendored

libs/naas-abi/naas_abi/apps/desktop/gui/web/app.js

Lines changed: 129 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,8 @@ const ICONS = {
5656
check: '<path d="M20 6 9 17l-5-5"/>',
5757
"loader-circle": '<path d="M21 12a9 9 0 1 1-6.219-8.56"/>',
5858
"external-link": '<path d="M15 3h6v6"/><path d="M10 14 21 3"/><path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"/>',
59+
"scroll-text":
60+
'<path d="M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7Z"/><path d="M14 2v4a2 2 0 0 0 2 2h4"/><path d="M10 9H8"/><path d="M16 13H8"/><path d="M16 17H8"/>',
5961
};
6062

6163
function icon(name, size = 18) {
@@ -79,10 +81,11 @@ const SECTION_META = {
7981
chat: { title: "Chat", panel: "chat-panel" },
8082
code: { title: "Code", panel: "code-panel" },
8183
graph: { title: "Knowledge Graph", panel: "graph-panel" },
84+
events: { title: "Events", panel: "events-panel" },
8285
settings: { title: "Settings", panel: "settings-panel" },
8386
};
8487

85-
const SECTION_VIEWS = ["chat", "code", "graph", "settings"];
88+
const SECTION_VIEWS = ["chat", "code", "graph", "events", "settings"];
8689

8790
const DEFAULT_SECTION = "chat";
8891

@@ -249,7 +252,8 @@ const state = {
249252
graphView: "brain",
250253
graphOverview: null,
251254
graphEvents: null,
252-
graphSelectedEventId: null,
255+
selectedEventId: null,
256+
pendingGraphFocus: null,
253257
graphNetwork: null,
254258
graphNodesDataset: null,
255259
graphEdgesDataset: null,
@@ -392,12 +396,13 @@ function switchSection(section, { updateHash = true } = {}) {
392396
refreshGraphStats();
393397
if (state.graphTab === "overview") {
394398
loadGraphOverview();
395-
loadGraphEvents();
396399
} else if (state.graphTab === "sparql") {
397400
runActiveContextQuery();
398401
} else if (state.graphTab === "tables" && state.graphOverview) {
399402
renderGraphTables(state.graphOverview.tables);
400403
}
404+
} else if (section === "events") {
405+
loadEvents();
401406
} else if (section === "settings") {
402407
loadSettingsView();
403408
}
@@ -469,7 +474,10 @@ async function newChat(section) {
469474
renderMessagesFor(section);
470475
if (state.section === "graph") {
471476
state.graphEvents = null;
472-
loadGraphEvents();
477+
if (state.graphTab === "overview") loadGraphOverview();
478+
} else if (state.section === "events") {
479+
state.graphEvents = null;
480+
loadEvents();
473481
}
474482
return chat;
475483
}
@@ -2741,7 +2749,9 @@ async function applyWorkspaceSwitchResult(body) {
27412749
if (state.section === "graph" && state.graphTab === "overview") {
27422750
state.graphEvents = null;
27432751
loadGraphOverview();
2744-
loadGraphEvents();
2752+
} else if (state.section === "events") {
2753+
state.graphEvents = null;
2754+
loadEvents();
27452755
}
27462756
reconnectTerminal();
27472757
refreshWorkspaceEnvPanel();
@@ -2858,7 +2868,6 @@ function switchGraphTab(tab) {
28582868
$("graph-tab-tables").classList.toggle("hidden", tab !== "tables");
28592869
if (tab === "overview") {
28602870
loadGraphOverview();
2861-
loadGraphEvents();
28622871
} else if (tab === "sparql") {
28632872
if (!$("sparql-input").value.trim()) {
28642873
$("sparql-input").value = ACTIVE_CONTEXT_SPARQL;
@@ -3694,7 +3703,7 @@ function renderGraphEventBucketCell(bucket) {
36943703
return td;
36953704
}
36963705

3697-
function focusGraphProcessEvent(eventRow) {
3706+
function focusGraphProcessNode(eventRow) {
36983707
if (!eventRow?.graph_node_id || !state.graphNetwork || !state.graphOverview) return;
36993708
const processNodeId = eventRow.graph_node_id;
37003709
const nodeIds = new Set([processNodeId]);
@@ -3710,15 +3719,101 @@ function focusGraphProcessEvent(eventRow) {
37103719
showToast("Process not visible in current graph view", "info");
37113720
return;
37123721
}
3713-
state.graphSelectedEventId = eventRow.id;
3714-
renderGraphEventsTable(state.graphEvents?.items || []);
37153722
state.graphNetwork.selectNodes(visibleIds);
37163723
focusGraphNode(processNodeId, { openPanel: true, animate: true });
37173724
}
37183725

3719-
function renderGraphEventsTable(items) {
3720-
const host = $("graph-events-table-host");
3721-
const count = $("graph-events-count");
3726+
function selectProcessEvent(eventRow) {
3727+
state.selectedEventId = eventRow?.id ?? null;
3728+
renderEventsTable(state.graphEvents?.items || []);
3729+
renderEventDetail(eventRow);
3730+
}
3731+
3732+
function openProcessEventInGraph(eventRow) {
3733+
if (!eventRow?.graph_node_id) {
3734+
showToast("No graph node linked to this event", "info");
3735+
return;
3736+
}
3737+
state.pendingGraphFocus = eventRow;
3738+
if (state.section !== "graph") {
3739+
state.graphTab = "overview";
3740+
switchSection("graph");
3741+
return;
3742+
}
3743+
if (!state.graphOverview) {
3744+
loadGraphOverview();
3745+
return;
3746+
}
3747+
state.pendingGraphFocus = null;
3748+
focusGraphProcessNode(eventRow);
3749+
}
3750+
3751+
function renderEventDetail(eventRow) {
3752+
const host = $("events-detail-body");
3753+
const panel = $("events-detail-panel");
3754+
if (!host || !panel) return;
3755+
if (!eventRow) {
3756+
panel.classList.remove("open");
3757+
host.innerHTML = '<div class="graph-detail-empty">Select a row to inspect bucket values.</div>';
3758+
return;
3759+
}
3760+
panel.classList.add("open");
3761+
host.innerHTML = "";
3762+
const title = document.createElement("div");
3763+
title.className = "graph-detail-title";
3764+
const heading = document.createElement("h3");
3765+
heading.textContent = eventRow.process_label || eventRow.process_type || "Process event";
3766+
title.appendChild(heading);
3767+
const badge = document.createElement("span");
3768+
badge.className = "graph-detail-badge";
3769+
badge.textContent = eventRow.process_type || "event";
3770+
title.appendChild(badge);
3771+
host.appendChild(title);
3772+
3773+
const meta = document.createElement("div");
3774+
meta.className = "graph-detail-section";
3775+
meta.innerHTML = `<div class="graph-detail-section-title">When</div><div>${eventRow.timestamp || "—"}</div>`;
3776+
host.appendChild(meta);
3777+
3778+
const buckets = eventRow.buckets || {};
3779+
const bucketSection = document.createElement("div");
3780+
bucketSection.className = "graph-detail-section";
3781+
const bucketTitle = document.createElement("div");
3782+
bucketTitle.className = "graph-detail-section-title";
3783+
bucketTitle.textContent = "BFO buckets";
3784+
bucketSection.appendChild(bucketTitle);
3785+
const table = document.createElement("table");
3786+
table.className = "graph-aspects-table";
3787+
const head = table.insertRow();
3788+
["Bucket", "Value", "Status"].forEach((label) => {
3789+
const th = document.createElement("th");
3790+
th.textContent = label;
3791+
head.appendChild(th);
3792+
});
3793+
for (const [bucket, data] of Object.entries(buckets)) {
3794+
const tr = table.insertRow();
3795+
const status = data?.status || "unknown";
3796+
tr.innerHTML = `<td>${bucket}</td><td>${data?.label || "—"}</td><td>${status}</td>`;
3797+
}
3798+
bucketSection.appendChild(table);
3799+
host.appendChild(bucketSection);
3800+
3801+
if (eventRow.graph_node_id) {
3802+
const actions = document.createElement("div");
3803+
actions.className = "events-detail-actions";
3804+
const btn = document.createElement("button");
3805+
btn.type = "button";
3806+
btn.className = "settings-btn primary";
3807+
btn.textContent = "View in Graph";
3808+
btn.onclick = () => openProcessEventInGraph(eventRow);
3809+
actions.appendChild(btn);
3810+
host.appendChild(actions);
3811+
}
3812+
}
3813+
3814+
function renderEventsTable(items) {
3815+
const host = $("events-table-host");
3816+
const count = $("events-count");
37223817
if (!host) return;
37233818
host.innerHTML = "";
37243819
const total = state.graphEvents?.total ?? items.length;
@@ -3733,7 +3828,7 @@ function renderGraphEventsTable(items) {
37333828
return;
37343829
}
37353830
const table = document.createElement("table");
3736-
table.className = "graph-events-table";
3831+
table.className = "events-table";
37373832
const head = table.insertRow();
37383833
const columns = [
37393834
"timestamp",
@@ -3766,16 +3861,17 @@ function renderGraphEventsTable(items) {
37663861
}
37673862
for (const row of items) {
37683863
const tr = document.createElement("tr");
3769-
tr.className = "graph-event-row";
3770-
if (row.id === state.graphSelectedEventId) {
3864+
tr.className = "events-row";
3865+
if (row.id === state.selectedEventId) {
37713866
tr.classList.add("selected");
37723867
}
3773-
tr.onclick = () => focusGraphProcessEvent(row);
3868+
tr.onclick = () => selectProcessEvent(row);
3869+
tr.ondblclick = () => openProcessEventInGraph(row);
37743870
const buckets = row.buckets || {};
37753871
for (const column of columns) {
37763872
if (column === "timestamp" || column === "process_label" || column === "process_type") {
37773873
const td = document.createElement("td");
3778-
td.className = "graph-event-meta";
3874+
td.className = "events-meta";
37793875
td.textContent = row[column] ?? "";
37803876
td.title = td.textContent;
37813877
tr.appendChild(td);
@@ -3788,15 +3884,19 @@ function renderGraphEventsTable(items) {
37883884
host.appendChild(table);
37893885
}
37903886

3791-
async function loadGraphEvents() {
3792-
const host = $("graph-events-table-host");
3887+
async function loadEvents() {
3888+
const host = $("events-table-host");
37933889
if (host && !state.graphEvents) {
37943890
host.innerHTML = '<div class="graph-detail-empty">Loading events…</div>';
37953891
}
37963892
try {
37973893
const payload = await api("/api/processes?limit=100");
37983894
state.graphEvents = payload;
3799-
renderGraphEventsTable(payload.items || []);
3895+
renderEventsTable(payload.items || []);
3896+
if (state.selectedEventId) {
3897+
const selected = (payload.items || []).find((row) => row.id === state.selectedEventId);
3898+
renderEventDetail(selected || null);
3899+
}
38003900
} catch (err) {
38013901
if (host) {
38023902
host.innerHTML = "";
@@ -3936,8 +4036,10 @@ async function loadGraphOverview() {
39364036
if (state.graphTab === "tables") {
39374037
renderGraphTables(overview.tables);
39384038
}
3939-
if (state.graphSelectedEventId) {
3940-
renderGraphEventsTable(state.graphEvents?.items || []);
4039+
if (state.pendingGraphFocus) {
4040+
const row = state.pendingGraphFocus;
4041+
state.pendingGraphFocus = null;
4042+
focusGraphProcessNode(row);
39414043
}
39424044
} catch (err) {
39434045
const host = $("graph-detail-table");
@@ -4904,6 +5006,11 @@ function init() {
49045006
$("graph-search")?.focus();
49055007
});
49065008
$("graph-detail-close")?.addEventListener("click", clearGraphNodeSelection);
5009+
$("events-detail-close")?.addEventListener("click", () => {
5010+
state.selectedEventId = null;
5011+
renderEventsTable(state.graphEvents?.items || []);
5012+
renderEventDetail(null);
5013+
});
49075014
document.querySelectorAll(".graph-view-btn").forEach((btn) => {
49085015
btn.onclick = () => switchGraphView(btn.dataset.graphView);
49095016
});

0 commit comments

Comments
 (0)