@jhizzard/termdeck 0.10.0 → 0.10.3
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/package.json +4 -1
- package/packages/client/public/app.js +42 -3
- package/packages/client/public/graph.html +1 -0
- package/packages/client/public/graph.js +119 -23
- package/packages/client/public/style.css +53 -0
- package/packages/server/src/flashback-diag.js +51 -0
- package/packages/server/src/graph-routes.js +100 -0
- package/packages/server/src/index.js +62 -3
- package/packages/server/src/mnestra-bridge/index.js +63 -9
- package/packages/server/src/session.js +95 -5
- package/packages/server/src/setup/mnestra-migrations/011_project_tag_backfill.sql +237 -0
- package/packages/server/src/setup/mnestra-migrations/012_project_tag_re_taxonomy.sql +397 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jhizzard/termdeck",
|
|
3
|
-
"version": "0.10.
|
|
3
|
+
"version": "0.10.3",
|
|
4
4
|
"description": "Browser-based terminal multiplexer with metadata overlays, panel flashback memory recall, and AI-aware session management",
|
|
5
5
|
"bin": {
|
|
6
6
|
"termdeck": "./packages/cli/src/index.js"
|
|
@@ -42,6 +42,9 @@
|
|
|
42
42
|
"ws": "^8.16.0",
|
|
43
43
|
"yaml": "^2.3.4"
|
|
44
44
|
},
|
|
45
|
+
"devDependencies": {
|
|
46
|
+
"@anthropic-ai/sdk": "^0.39.0"
|
|
47
|
+
},
|
|
45
48
|
"keywords": [
|
|
46
49
|
"terminal",
|
|
47
50
|
"multiplexer",
|
|
@@ -237,6 +237,9 @@
|
|
|
237
237
|
case 'meta':
|
|
238
238
|
updatePanelMeta(id, msg.session.meta);
|
|
239
239
|
break;
|
|
240
|
+
case 'proactive_memory':
|
|
241
|
+
showProactiveToast(id, msg.hit);
|
|
242
|
+
break;
|
|
240
243
|
case 'exit':
|
|
241
244
|
updatePanelMeta(id, {
|
|
242
245
|
status: 'exited',
|
|
@@ -1245,6 +1248,9 @@
|
|
|
1245
1248
|
case 'meta':
|
|
1246
1249
|
updatePanelMeta(id, msg.session.meta);
|
|
1247
1250
|
break;
|
|
1251
|
+
case 'proactive_memory':
|
|
1252
|
+
showProactiveToast(id, msg.hit);
|
|
1253
|
+
break;
|
|
1248
1254
|
case 'exit':
|
|
1249
1255
|
updatePanelMeta(id, { status: 'exited', statusDetail: `Exited (${msg.exitCode})` });
|
|
1250
1256
|
const p = document.getElementById(`panel-${id}`);
|
|
@@ -1253,6 +1259,17 @@
|
|
|
1253
1259
|
case 'status_broadcast':
|
|
1254
1260
|
updateGlobalStats(msg.sessions);
|
|
1255
1261
|
break;
|
|
1262
|
+
case 'config_changed':
|
|
1263
|
+
// Sprint 40 T1: parity with the main panel WS handler. The
|
|
1264
|
+
// server broadcasts config_changed to ALL ws clients, including
|
|
1265
|
+
// reconnected sessions; previously the reconnect path silently
|
|
1266
|
+
// dropped these. Idempotent — safe to re-receive.
|
|
1267
|
+
if (msg.config) {
|
|
1268
|
+
state.config = { ...state.config, ...msg.config };
|
|
1269
|
+
if (typeof renderSettingsPanel === 'function') renderSettingsPanel();
|
|
1270
|
+
if (typeof updateRagIndicator === 'function') updateRagIndicator();
|
|
1271
|
+
}
|
|
1272
|
+
break;
|
|
1256
1273
|
}
|
|
1257
1274
|
} catch (err) { console.error('[client] reconnect ws message failed:', err); }
|
|
1258
1275
|
};
|
|
@@ -2509,12 +2526,29 @@
|
|
|
2509
2526
|
{
|
|
2510
2527
|
targets: ['#btn-status', '#btn-config'],
|
|
2511
2528
|
title: 'Status and config',
|
|
2512
|
-
body: `<strong>status</strong> opens a global-metrics modal (session counts by state, RAG mode, memory bridge). <strong>config</strong> shows your loaded project list and theme defaults
|
|
2529
|
+
body: `<strong>status</strong> opens a global-metrics modal (session counts by state, RAG mode, memory bridge). <strong>config</strong> shows your loaded project list and theme defaults — plus a live RAG-mode toggle (Sprint 36) that flips Flashback on/off without a server restart.`,
|
|
2530
|
+
},
|
|
2531
|
+
{
|
|
2532
|
+
targets: ['#btn-sprint', '#btn-graph'],
|
|
2533
|
+
title: 'Sprint runner and knowledge graph',
|
|
2534
|
+
body: `<strong>sprint</strong> opens the in-dashboard 4+1 sprint runner (Sprint 37): name the sprint, define T1–T4 lane goals, click kick off — TermDeck spawns four panels and injects boot prompts via the two-stage submit pattern automatically. Optional <strong>--isolation=worktree</strong> creates a git worktree per lane so concurrent edits can't stomp. <strong>graph</strong> opens the D3.js force-directed knowledge graph (Sprint 38) of your memory_items + memory_relationships in a new tab — click any node to open its memory in a drawer, filter by relationship type, search, zoom/pan.`,
|
|
2513
2535
|
},
|
|
2514
2536
|
{
|
|
2515
2537
|
targets: ['#btn-how', '#btn-help'],
|
|
2516
2538
|
title: 'How this works and help',
|
|
2517
|
-
body: `Click <strong>how this works</strong> any time to replay this tour. <strong>help</strong> opens the full TermDeck documentation in a new tab.`,
|
|
2539
|
+
body: `Click <strong>how this works</strong> any time to replay this tour. <strong>help</strong> opens the full TermDeck documentation in a new tab. The <strong>📖 Guide</strong> tab on the right edge of the screen — also opens with the <kbd>g</kbd> keyboard shortcut — is the always-on Orchestrator Guide (Sprint 37): nine sections covering the 4+1 sprint pattern, inject mandate, CLAUDE.md hierarchy, memory-first discipline, sprint discipline, restart-prompt rituals, scaffolding files, channel inject patterns. Search built in.`,
|
|
2540
|
+
},
|
|
2541
|
+
{
|
|
2542
|
+
target: '#guideRail',
|
|
2543
|
+
title: 'Right-rail Orchestrator Guide',
|
|
2544
|
+
body: `The <strong>📖 Guide</strong> rail is your orchestration cheat-sheet — collapsed by default, one click (or <kbd>g</kbd>) to expand. It auto-scrolls to the relevant section based on what you're focused on: clicking a terminal panel jumps the Guide to the 4+1 pattern; opening the project drawer jumps to CLAUDE.md hierarchy. Useful when you forget exactly how the two-stage submit pattern works at 2 AM in the middle of a sprint inject.`,
|
|
2545
|
+
fallback: '#btn-how',
|
|
2546
|
+
},
|
|
2547
|
+
{
|
|
2548
|
+
target: '#btnPreviewProject',
|
|
2549
|
+
title: 'Orchestration preview',
|
|
2550
|
+
body: `The <strong>preview</strong> button next to the project + button (Sprint 37) shows you exactly what <code>termdeck init --project <name></code> would create for the selected project — file tree, contents per file, expand-on-click. Read-only by default; optional generate button writes the scaffolding (CLAUDE.md, CONTRADICTIONS.md, project_facts.md, .claude/settings.json, docs/orchestration/, RESTART-PROMPT.md template). Lets you see-before-commit instead of running the CLI blind.`,
|
|
2551
|
+
fallback: '#btn-how',
|
|
2518
2552
|
},
|
|
2519
2553
|
{
|
|
2520
2554
|
target: '.panel-header',
|
|
@@ -2553,10 +2587,15 @@
|
|
|
2553
2587
|
title: 'Prompt bar',
|
|
2554
2588
|
body: `Type any command here to launch it as a new terminal — <kbd>claude code ~/myproject</kbd>, <kbd>python3 manage.py runserver</kbd>, <kbd>npm run dev</kbd>. Pick a project from the dropdown to auto-cd into its path and apply its default theme. <kbd>Ctrl+Shift+N</kbd> focuses this bar from anywhere.`,
|
|
2555
2589
|
},
|
|
2590
|
+
{
|
|
2591
|
+
target: null,
|
|
2592
|
+
title: 'Knowledge graph + memory inference',
|
|
2593
|
+
body: `Sprint 38 brought your <strong>memory_relationships</strong> table to life. The <strong>graph</strong> button (top toolbar) renders your memories as a force-directed network — supersedes / relates_to / contradicts / elaborates / caused_by / blocks / inspired_by / cross_project_link edges, color-coded, filterable. The Mnestra MCP server now exposes four new tools: <code>memory_link</code>, <code>memory_unlink</code>, <code>memory_related</code>, and <code>memory_recall_graph</code> — Claude Code can connect related memories explicitly, traverse N-hop neighborhoods, and recall via graph-aware re-ranking (vector_score × edge_weight × recency). Edges populate automatically from Joshua's private rag-system classifier; a nightly cron in Sprint 39+ will surface cross-project connections.`,
|
|
2594
|
+
},
|
|
2556
2595
|
{
|
|
2557
2596
|
target: null,
|
|
2558
2597
|
title: 'You are ready.',
|
|
2559
|
-
body: `That's every major surface. Click <strong>how this works</strong> in the top toolbar to replay this walkthrough. <strong>help</strong> opens the full docs. Questions, bugs, feedback: <a href="https://github.com/jhizzard/termdeck/issues" target="_blank" style="color:var(--tg-accent)">github.com/jhizzard/termdeck/issues</a>. Now launch something.`,
|
|
2598
|
+
body: `That's every major surface. Click <strong>how this works</strong> in the top toolbar to replay this walkthrough. <strong>help</strong> opens the full docs. Press <kbd>g</kbd> any time to crack open the Orchestrator Guide. Questions, bugs, feedback: <a href="https://github.com/jhizzard/termdeck/issues" target="_blank" style="color:var(--tg-accent)">github.com/jhizzard/termdeck/issues</a>. Now launch something.`,
|
|
2560
2599
|
},
|
|
2561
2600
|
];
|
|
2562
2601
|
|
|
@@ -51,6 +51,7 @@
|
|
|
51
51
|
<div class="graph-empty" id="graphEmpty" hidden>
|
|
52
52
|
<h3 id="graphEmptyTitle">No memories yet</h3>
|
|
53
53
|
<p id="graphEmptyBody">This project has no <code>memory_items</code> rows. Run a Claude Code session in this project; the session-end hook will populate Mnestra and edges will be inferred on the next nightly cron.</p>
|
|
54
|
+
<button type="button" class="graph-empty-action" id="graphEmptyAllProjects" hidden>View All Projects</button>
|
|
54
55
|
</div>
|
|
55
56
|
|
|
56
57
|
<svg class="graph-svg" id="graphSvg" role="img" aria-label="Force-directed knowledge graph">
|
|
@@ -186,6 +186,51 @@
|
|
|
186
186
|
window.history.replaceState(null, '', url);
|
|
187
187
|
}
|
|
188
188
|
|
|
189
|
+
// -------- Stateful UI reset (Sprint 41 T3) ------------------------------
|
|
190
|
+
|
|
191
|
+
// Clear the SVG render and tear down the running simulation. Called at the
|
|
192
|
+
// start of every fetchGraph so a re-fetch from a different mode/project
|
|
193
|
+
// can't paint over a stale render.
|
|
194
|
+
function clearGraphSvg() {
|
|
195
|
+
if (state.sim) {
|
|
196
|
+
state.sim.stop();
|
|
197
|
+
state.sim = null;
|
|
198
|
+
}
|
|
199
|
+
const r = root();
|
|
200
|
+
if (r) {
|
|
201
|
+
while (r.firstChild) r.removeChild(r.firstChild);
|
|
202
|
+
}
|
|
203
|
+
state.nodeSel = null;
|
|
204
|
+
state.edgeSel = null;
|
|
205
|
+
state.labelSel = null;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// Ephemeral toast in the top-right of the graph stage. Used to surface
|
|
209
|
+
// truncation warnings on the All Projects view. Auto-dismisses; calling
|
|
210
|
+
// again before dismissal replaces the message and resets the timer.
|
|
211
|
+
function showToast(msg, durationMs = 6000) {
|
|
212
|
+
const stageEl = stage();
|
|
213
|
+
if (!stageEl) return;
|
|
214
|
+
let el = document.getElementById('graphToast');
|
|
215
|
+
if (!el) {
|
|
216
|
+
el = document.createElement('div');
|
|
217
|
+
el.id = 'graphToast';
|
|
218
|
+
el.className = 'graph-toast';
|
|
219
|
+
stageEl.appendChild(el);
|
|
220
|
+
}
|
|
221
|
+
el.textContent = msg;
|
|
222
|
+
el.hidden = false;
|
|
223
|
+
// Force a reflow so the .show transition fires when toggling rapid-fire.
|
|
224
|
+
void el.offsetWidth;
|
|
225
|
+
el.classList.add('show');
|
|
226
|
+
if (showToast._timer) clearTimeout(showToast._timer);
|
|
227
|
+
showToast._timer = setTimeout(() => {
|
|
228
|
+
el.classList.remove('show');
|
|
229
|
+
// Hide after the transition completes so it can't catch clicks.
|
|
230
|
+
setTimeout(() => { el.hidden = true; }, 220);
|
|
231
|
+
}, durationMs);
|
|
232
|
+
}
|
|
233
|
+
|
|
189
234
|
// -------- API ------------------------------------------------------------
|
|
190
235
|
|
|
191
236
|
async function api(path) {
|
|
@@ -208,14 +253,32 @@
|
|
|
208
253
|
}
|
|
209
254
|
|
|
210
255
|
async function fetchGraph() {
|
|
256
|
+
// Sprint 41 T3 — reset all stateful UI before the new fetch starts so a
|
|
257
|
+
// re-fetch from a different mode/project starts from a clean slate. Fixes
|
|
258
|
+
// the three-way race where "Loading graph…" + "No memories yet" + a stale
|
|
259
|
+
// node render all paint over each other after a mode/project switch.
|
|
260
|
+
hideEmpty();
|
|
261
|
+
clearGraphSvg();
|
|
262
|
+
state.nodes = [];
|
|
263
|
+
state.edges = [];
|
|
211
264
|
setLoading('Loading graph…');
|
|
212
265
|
try {
|
|
213
266
|
let data;
|
|
214
|
-
if (state.mode === '
|
|
267
|
+
if (state.mode === 'project' && state.project === '__all__') {
|
|
268
|
+
data = await api('/api/graph/all');
|
|
269
|
+
if (data.enabled === false) return showDisabled(data);
|
|
270
|
+
state.nodes = data.nodes || [];
|
|
271
|
+
state.edges = data.edges || [];
|
|
272
|
+
if (data.truncated) {
|
|
273
|
+
showToast(
|
|
274
|
+
`Showing ${state.nodes.length} most-recent of ${data.totalAvailable} memories — narrow by project to see specific clusters.`,
|
|
275
|
+
);
|
|
276
|
+
}
|
|
277
|
+
} else if (state.mode === 'memory') {
|
|
215
278
|
data = await api(`/api/graph/memory/${encodeURIComponent(state.memoryId)}?depth=${state.depth}`);
|
|
216
279
|
if (data.enabled === false) return showDisabled(data);
|
|
217
|
-
state.nodes = data.nodes;
|
|
218
|
-
state.edges = data.edges;
|
|
280
|
+
state.nodes = data.nodes || [];
|
|
281
|
+
state.edges = data.edges || [];
|
|
219
282
|
// Use the root memory's project as the view's "current project" for
|
|
220
283
|
// the legend / drawer / fallback color when nodes span projects.
|
|
221
284
|
if (data.root && data.root.project) state.project = data.root.project;
|
|
@@ -224,8 +287,8 @@
|
|
|
224
287
|
state.project = name;
|
|
225
288
|
data = await api(`/api/graph/project/${encodeURIComponent(name)}`);
|
|
226
289
|
if (data.enabled === false) return showDisabled(data);
|
|
227
|
-
state.nodes = data.nodes;
|
|
228
|
-
state.edges = data.edges;
|
|
290
|
+
state.nodes = data.nodes || [];
|
|
291
|
+
state.edges = data.edges || [];
|
|
229
292
|
}
|
|
230
293
|
writeUrlState();
|
|
231
294
|
hideLoading();
|
|
@@ -238,6 +301,9 @@
|
|
|
238
301
|
renderGraph();
|
|
239
302
|
updateStats();
|
|
240
303
|
} catch (err) {
|
|
304
|
+
// Even on error, drop the empty-state overlay so the failure message
|
|
305
|
+
// shows alone instead of stacking on top of "No memories yet".
|
|
306
|
+
hideEmpty();
|
|
241
307
|
setLoading(`Failed: ${err.message}`);
|
|
242
308
|
}
|
|
243
309
|
}
|
|
@@ -256,14 +322,30 @@
|
|
|
256
322
|
}
|
|
257
323
|
function showEmpty() {
|
|
258
324
|
$('graphEmpty').hidden = false;
|
|
259
|
-
|
|
260
|
-
|
|
325
|
+
const allBtn = $('graphEmptyAllProjects');
|
|
326
|
+
if (state.mode === 'project' && state.project === '__all__') {
|
|
327
|
+
$('graphEmptyTitle').textContent = 'No memories yet';
|
|
328
|
+
$('graphEmptyBody').innerHTML =
|
|
329
|
+
'No <code>memory_items</code> rows in the database. Run a Claude Code session and the session-end hook will populate Mnestra; edges will be inferred on the next nightly cron.';
|
|
330
|
+
if (allBtn) allBtn.hidden = true;
|
|
331
|
+
} else if (state.mode === 'project') {
|
|
332
|
+
$('graphEmptyTitle').textContent = `No memories tagged "${state.project}"`;
|
|
333
|
+
$('graphEmptyBody').innerHTML =
|
|
334
|
+
'Your <code>memory_items</code> may be mis-tagged under a parent directory. ' +
|
|
335
|
+
'Try the All Projects view, or check the actual distribution with ' +
|
|
336
|
+
'<code>SELECT project, count(*) FROM memory_items GROUP BY project</code>.';
|
|
337
|
+
if (allBtn) allBtn.hidden = false;
|
|
261
338
|
} else {
|
|
262
339
|
$('graphEmptyTitle').textContent = 'No neighbors yet';
|
|
340
|
+
$('graphEmptyBody').textContent =
|
|
341
|
+
'This memory has no edges in memory_relationships. The next graph-inference cron run will infer them if any are warranted.';
|
|
342
|
+
if (allBtn) allBtn.hidden = true;
|
|
263
343
|
}
|
|
264
344
|
}
|
|
265
345
|
function hideEmpty() {
|
|
266
346
|
$('graphEmpty').hidden = true;
|
|
347
|
+
const allBtn = $('graphEmptyAllProjects');
|
|
348
|
+
if (allBtn) allBtn.hidden = true;
|
|
267
349
|
}
|
|
268
350
|
function showDisabled(data) {
|
|
269
351
|
hideLoading();
|
|
@@ -610,28 +692,31 @@
|
|
|
610
692
|
readUrlState();
|
|
611
693
|
await loadConfig();
|
|
612
694
|
|
|
613
|
-
// Project picker
|
|
695
|
+
// Project picker. The "All projects" option is always present so the user
|
|
696
|
+
// can recover from mis-tagged data (Sprint 41 T3); per-project options are
|
|
697
|
+
// appended after it from /api/config.
|
|
614
698
|
const sel = $('graphProject');
|
|
615
699
|
sel.innerHTML = '';
|
|
616
|
-
|
|
700
|
+
sel.disabled = false;
|
|
701
|
+
const allOpt = document.createElement('option');
|
|
702
|
+
allOpt.value = '__all__';
|
|
703
|
+
allOpt.textContent = 'All projects';
|
|
704
|
+
sel.appendChild(allOpt);
|
|
705
|
+
for (const p of state.projects) {
|
|
617
706
|
const opt = document.createElement('option');
|
|
618
|
-
opt.value =
|
|
619
|
-
opt.textContent =
|
|
707
|
+
opt.value = p;
|
|
708
|
+
opt.textContent = p;
|
|
620
709
|
sel.appendChild(opt);
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
}
|
|
629
|
-
// Pick state.project, or first project from config.
|
|
630
|
-
if (!state.project && state.mode !== 'memory') {
|
|
631
|
-
state.project = state.projects[0];
|
|
710
|
+
}
|
|
711
|
+
// Pick state.project, or fall back to the first configured project, or to
|
|
712
|
+
// __all__ when nothing is configured. Memory mode inherits its project
|
|
713
|
+
// from the root node and skips this resolution.
|
|
714
|
+
if (state.mode !== 'memory') {
|
|
715
|
+
if (!state.project) {
|
|
716
|
+
state.project = state.projects.length > 0 ? state.projects[0] : '__all__';
|
|
632
717
|
}
|
|
633
|
-
if (state.project) sel.value = state.project;
|
|
634
718
|
}
|
|
719
|
+
if (state.project) sel.value = state.project;
|
|
635
720
|
|
|
636
721
|
sel.addEventListener('change', () => {
|
|
637
722
|
state.mode = 'project';
|
|
@@ -640,6 +725,17 @@
|
|
|
640
725
|
fetchGraph();
|
|
641
726
|
});
|
|
642
727
|
|
|
728
|
+
const emptyAllBtn = $('graphEmptyAllProjects');
|
|
729
|
+
if (emptyAllBtn) {
|
|
730
|
+
emptyAllBtn.addEventListener('click', () => {
|
|
731
|
+
state.mode = 'project';
|
|
732
|
+
state.project = '__all__';
|
|
733
|
+
state.memoryId = null;
|
|
734
|
+
sel.value = '__all__';
|
|
735
|
+
fetchGraph();
|
|
736
|
+
});
|
|
737
|
+
}
|
|
738
|
+
|
|
643
739
|
$('graphSearch').addEventListener('input', (e) => applySearch(e.target.value));
|
|
644
740
|
$('graphReheat').addEventListener('click', () => {
|
|
645
741
|
if (state.sim) state.sim.alpha(0.6).restart();
|
|
@@ -3468,6 +3468,59 @@
|
|
|
3468
3468
|
border-radius: 3px;
|
|
3469
3469
|
font-size: 11px;
|
|
3470
3470
|
}
|
|
3471
|
+
/* Sprint 41 T3 — interactive button inside the .graph-empty overlay.
|
|
3472
|
+
The parent has pointer-events: none so taps on the SVG behind pass
|
|
3473
|
+
through; the button must opt back in for the click to register. */
|
|
3474
|
+
.graph-empty .graph-empty-action {
|
|
3475
|
+
pointer-events: auto;
|
|
3476
|
+
margin-top: 6px;
|
|
3477
|
+
background: var(--tg-surface);
|
|
3478
|
+
color: var(--tg-text);
|
|
3479
|
+
border: 1px solid var(--tg-border-active);
|
|
3480
|
+
border-radius: var(--tg-radius-sm);
|
|
3481
|
+
padding: 6px 14px;
|
|
3482
|
+
font-family: var(--tg-mono);
|
|
3483
|
+
font-size: 12px;
|
|
3484
|
+
cursor: pointer;
|
|
3485
|
+
transition: background 0.12s ease, border-color 0.12s ease, color 0.12s ease;
|
|
3486
|
+
}
|
|
3487
|
+
.graph-empty .graph-empty-action:hover {
|
|
3488
|
+
background: var(--tg-surface-hover);
|
|
3489
|
+
border-color: var(--tg-accent);
|
|
3490
|
+
color: var(--tg-text-bright);
|
|
3491
|
+
}
|
|
3492
|
+
.graph-empty .graph-empty-action:focus-visible {
|
|
3493
|
+
outline: 2px solid var(--tg-accent);
|
|
3494
|
+
outline-offset: 2px;
|
|
3495
|
+
}
|
|
3496
|
+
|
|
3497
|
+
/* Sprint 41 T3 — ephemeral toast for graph-side notifications
|
|
3498
|
+
(truncation warnings, etc). Sits in the top-right of #graphStage. */
|
|
3499
|
+
.graph-toast {
|
|
3500
|
+
position: absolute;
|
|
3501
|
+
top: 14px;
|
|
3502
|
+
right: 14px;
|
|
3503
|
+
max-width: 360px;
|
|
3504
|
+
background: rgba(15, 17, 23, 0.95);
|
|
3505
|
+
color: var(--tg-text);
|
|
3506
|
+
border: 1px solid var(--tg-border-active);
|
|
3507
|
+
border-radius: var(--tg-radius-sm);
|
|
3508
|
+
padding: 10px 14px;
|
|
3509
|
+
font-family: var(--tg-mono);
|
|
3510
|
+
font-size: 12px;
|
|
3511
|
+
line-height: 1.4;
|
|
3512
|
+
box-shadow: 0 4px 14px rgba(0, 0, 0, 0.35);
|
|
3513
|
+
backdrop-filter: blur(6px);
|
|
3514
|
+
z-index: 60;
|
|
3515
|
+
opacity: 0;
|
|
3516
|
+
transform: translateY(-6px);
|
|
3517
|
+
transition: opacity 0.18s ease, transform 0.18s ease;
|
|
3518
|
+
pointer-events: none;
|
|
3519
|
+
}
|
|
3520
|
+
.graph-toast.show {
|
|
3521
|
+
opacity: 1;
|
|
3522
|
+
transform: translateY(0);
|
|
3523
|
+
}
|
|
3471
3524
|
|
|
3472
3525
|
.graph-tooltip {
|
|
3473
3526
|
position: fixed;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
// Flashback diagnostic ring buffer (Sprint 39 T1).
|
|
2
|
+
//
|
|
3
|
+
// Six decision points along the Flashback pipeline write structured events
|
|
4
|
+
// here so production-flow regressions surface as a readable timeline instead
|
|
5
|
+
// of a silent gate failure. The ring is in-memory and lost on restart by
|
|
6
|
+
// design — persistence is a Sprint-40+ concern. Public surface:
|
|
7
|
+
//
|
|
8
|
+
// log({ sessionId, event, ...fields }) — append one event
|
|
9
|
+
// snapshot({ sessionId?, eventType?, limit? }) — read back filtered tail
|
|
10
|
+
// _resetForTest() — test-only ring clear
|
|
11
|
+
//
|
|
12
|
+
// Event shape (all events): { ts, sessionId, event, ...event-specific fields }.
|
|
13
|
+
//
|
|
14
|
+
// Event types and their producers:
|
|
15
|
+
// pattern_match — session.js _detectErrors (PATTERNS.error /
|
|
16
|
+
// errorLineStart / shellError matched)
|
|
17
|
+
// error_detected — session.js _detectErrors at onErrorDetected
|
|
18
|
+
// entry, before rate-limit check
|
|
19
|
+
// rate_limit_blocked — session.js _detectErrors when 30s limiter rejects
|
|
20
|
+
// bridge_query — mnestra-bridge queryMnestra at call return
|
|
21
|
+
// bridge_result — mnestra-bridge queryMnestra at call return
|
|
22
|
+
// proactive_memory_emit — index.js onErrorDetected WS send block
|
|
23
|
+
//
|
|
24
|
+
// The route GET /api/flashback/diag (registered in index.js) returns
|
|
25
|
+
// snapshot() output as JSON for ad-hoc inspection by Joshua and consumption
|
|
26
|
+
// by T4's production-flow e2e test.
|
|
27
|
+
|
|
28
|
+
const RING_SIZE = 200;
|
|
29
|
+
|
|
30
|
+
let ring = [];
|
|
31
|
+
|
|
32
|
+
function log(event) {
|
|
33
|
+
ring.push({ ts: new Date().toISOString(), ...event });
|
|
34
|
+
if (ring.length > RING_SIZE) {
|
|
35
|
+
ring = ring.slice(-RING_SIZE);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function snapshot({ sessionId, eventType, limit = RING_SIZE } = {}) {
|
|
40
|
+
let out = ring;
|
|
41
|
+
if (sessionId) out = out.filter((e) => e.sessionId === sessionId);
|
|
42
|
+
if (eventType) out = out.filter((e) => e.event === eventType);
|
|
43
|
+
const cap = Math.max(1, Math.min(RING_SIZE, Number(limit) || RING_SIZE));
|
|
44
|
+
return out.slice(-cap);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function _resetForTest() {
|
|
48
|
+
ring = [];
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
module.exports = { log, snapshot, _resetForTest, RING_SIZE };
|
|
@@ -26,6 +26,10 @@ const NODE_LABEL_LEN = 200;
|
|
|
26
26
|
const MAX_DEPTH = 4;
|
|
27
27
|
const DEFAULT_DEPTH = 2;
|
|
28
28
|
const MAX_NODES_PER_PROJECT = 2000;
|
|
29
|
+
// Sprint 41 T3 — All Projects view cap. Same ceiling as the per-project cap;
|
|
30
|
+
// the global view trades cluster-fidelity for breadth and warns the client via
|
|
31
|
+
// `truncated`/`totalAvailable` when the ceiling clips the corpus.
|
|
32
|
+
const MAX_NODES_GLOBAL = 2000;
|
|
29
33
|
|
|
30
34
|
function snippet(content, len = NODE_LABEL_LEN) {
|
|
31
35
|
if (!content) return '';
|
|
@@ -158,6 +162,60 @@ async function fetchProjectGraph(pool, projectName) {
|
|
|
158
162
|
return { nodes, edges };
|
|
159
163
|
}
|
|
160
164
|
|
|
165
|
+
async function fetchAllGraph(pool) {
|
|
166
|
+
// Sprint 41 T3 — backs the "All projects" picker option in /graph.html.
|
|
167
|
+
// Returns the most-recent MAX_NODES_GLOBAL active+non-archived memories plus
|
|
168
|
+
// every edge whose endpoints both land in the result set. `totalAvailable`
|
|
169
|
+
// and `truncated` let the client surface a toast when the corpus overflows
|
|
170
|
+
// the cap.
|
|
171
|
+
const totalSql = `
|
|
172
|
+
SELECT COUNT(*)::int AS c
|
|
173
|
+
FROM memory_items
|
|
174
|
+
WHERE is_active = TRUE AND archived = FALSE
|
|
175
|
+
`;
|
|
176
|
+
const totalRes = await pool.query(totalSql);
|
|
177
|
+
const totalAvailable = Number(totalRes.rows[0]?.c || 0);
|
|
178
|
+
|
|
179
|
+
const nodesSql = `
|
|
180
|
+
WITH all_nodes AS (
|
|
181
|
+
SELECT ${NODE_COLUMNS_SQL}
|
|
182
|
+
FROM memory_items
|
|
183
|
+
WHERE is_active = TRUE AND archived = FALSE
|
|
184
|
+
ORDER BY created_at DESC
|
|
185
|
+
LIMIT ${MAX_NODES_GLOBAL}
|
|
186
|
+
)
|
|
187
|
+
SELECT
|
|
188
|
+
n.*,
|
|
189
|
+
COALESCE((
|
|
190
|
+
SELECT COUNT(*)::int
|
|
191
|
+
FROM memory_relationships r
|
|
192
|
+
WHERE r.source_id = n.id OR r.target_id = n.id
|
|
193
|
+
), 0) AS degree
|
|
194
|
+
FROM all_nodes n
|
|
195
|
+
`;
|
|
196
|
+
const nodesRes = await pool.query(nodesSql);
|
|
197
|
+
const nodes = nodesRes.rows.map(rowToNode);
|
|
198
|
+
if (nodes.length === 0) {
|
|
199
|
+
return { nodes: [], edges: [], totalAvailable, truncated: false };
|
|
200
|
+
}
|
|
201
|
+
const ids = nodes.map((n) => n.id);
|
|
202
|
+
|
|
203
|
+
const edgesSql = `
|
|
204
|
+
SELECT ${EDGE_COLUMNS_BASE_SQL}, ${EDGE_COLUMNS_T2_SQL}
|
|
205
|
+
FROM memory_relationships
|
|
206
|
+
WHERE source_id = ANY($1::uuid[])
|
|
207
|
+
AND target_id = ANY($1::uuid[])
|
|
208
|
+
`;
|
|
209
|
+
const edgesRes = await pool.query(edgesSql, [ids]);
|
|
210
|
+
const edges = edgesRes.rows.map(rowToEdge);
|
|
211
|
+
return {
|
|
212
|
+
nodes,
|
|
213
|
+
edges,
|
|
214
|
+
totalAvailable,
|
|
215
|
+
truncated: totalAvailable > MAX_NODES_GLOBAL,
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
|
|
161
219
|
async function fetchNeighborhood(pool, rootId, depth) {
|
|
162
220
|
// Inline recursive CTE so T4 ships independently of T1's
|
|
163
221
|
// expand_memory_neighborhood RPC. When that RPC lands, the CTE here can be
|
|
@@ -467,6 +525,46 @@ function createGraphRoutes({ app, getPool }) {
|
|
|
467
525
|
}
|
|
468
526
|
});
|
|
469
527
|
|
|
528
|
+
// Sprint 41 T3 — All Projects view. Sibling of /api/graph/project/:name with
|
|
529
|
+
// no project filter; cap is MAX_NODES_GLOBAL and the response carries
|
|
530
|
+
// `truncated`/`totalAvailable` so the client can surface a "showing N of M"
|
|
531
|
+
// toast when the corpus overflows.
|
|
532
|
+
app.get('/api/graph/all', async (req, res) => {
|
|
533
|
+
const pool = getPool();
|
|
534
|
+
if (!pool) {
|
|
535
|
+
return res.json(disabledPayload({
|
|
536
|
+
nodes: [],
|
|
537
|
+
edges: [],
|
|
538
|
+
totalAvailable: 0,
|
|
539
|
+
truncated: false,
|
|
540
|
+
}));
|
|
541
|
+
}
|
|
542
|
+
try {
|
|
543
|
+
const { nodes, edges, totalAvailable, truncated } = await fetchAllGraph(pool);
|
|
544
|
+
const byType = {};
|
|
545
|
+
for (const e of edges) {
|
|
546
|
+
byType[e.kind] = (byType[e.kind] || 0) + 1;
|
|
547
|
+
}
|
|
548
|
+
res.json({
|
|
549
|
+
enabled: true,
|
|
550
|
+
stats: {
|
|
551
|
+
nodes: nodes.length,
|
|
552
|
+
edges: edges.length,
|
|
553
|
+
byType,
|
|
554
|
+
truncated,
|
|
555
|
+
totalAvailable,
|
|
556
|
+
},
|
|
557
|
+
nodes,
|
|
558
|
+
edges,
|
|
559
|
+
totalAvailable,
|
|
560
|
+
truncated,
|
|
561
|
+
});
|
|
562
|
+
} catch (err) {
|
|
563
|
+
console.warn('[graph] /api/graph/all failed:', err.message);
|
|
564
|
+
res.status(500).json({ error: 'graph all query failed', detail: err.message });
|
|
565
|
+
}
|
|
566
|
+
});
|
|
567
|
+
|
|
470
568
|
app.get('/api/graph/memory/:id', async (req, res) => {
|
|
471
569
|
const id = req.params.id;
|
|
472
570
|
if (!UUID_RE.test(id)) {
|
|
@@ -540,6 +638,7 @@ module.exports = {
|
|
|
540
638
|
createGraphRoutes,
|
|
541
639
|
// Exported for tests + reuse:
|
|
542
640
|
fetchProjectGraph,
|
|
641
|
+
fetchAllGraph,
|
|
543
642
|
fetchNeighborhood,
|
|
544
643
|
fetchStats,
|
|
545
644
|
fetchInferenceStats,
|
|
@@ -550,6 +649,7 @@ module.exports = {
|
|
|
550
649
|
UUID_RE,
|
|
551
650
|
PROJECT_RE,
|
|
552
651
|
MAX_NODES_PER_PROJECT,
|
|
652
|
+
MAX_NODES_GLOBAL,
|
|
553
653
|
MAX_DEPTH,
|
|
554
654
|
DEFAULT_DEPTH,
|
|
555
655
|
};
|