From 6f6efd85685ecf3a3bb721d149741333359c2ea1 Mon Sep 17 00:00:00 2001 From: hailneed Date: Fri, 18 Sep 2026 16:14:25 +0300 Subject: [PATCH 1/4] feat(ui): add project-first session and subagent explorer --- src-node/dashboard.js | 74 +++++++++++++++++++++++++++++++++++++++---- 1 file changed, 67 insertions(+), 7 deletions(-) diff --git a/src-node/dashboard.js b/src-node/dashboard.js index 5c583df..8746ab0 100644 --- a/src-node/dashboard.js +++ b/src-node/dashboard.js @@ -57,6 +57,65 @@ function renderSession(session, riskCount = 0) { `; } +function projectIdentity(session) { + return String(session.cwd || session.projectSlug || 'unknown').replace(/[\\\/]+$/, '').toLowerCase(); +} + +function projectName(session) { + const raw = String(session.cwd || session.projectSlug || 'Unknown project').replace(/[\\\/]+$/, ''); + return raw.split(/[\\\/]/).filter(Boolean).pop() || session.projectSlug || 'Unknown project'; +} + +function buildProjectGroups(sessions) { + const groups = new Map(); + for (const session of sessions) { + const key = projectIdentity(session); + if (!groups.has(key)) groups.set(key, { + key, + name: projectName(session), + path: session.cwd || session.projectSlug || 'Unknown path', + slug: session.projectSlug || '', + sessions: [], + subagents: [], + prompts: 0, + tools: 0, + latest: '', + }); + const group = groups.get(key); + (session.isSubagent ? group.subagents : group.sessions).push(session); + group.prompts += Number(session.userPrompts || 0); + group.tools += Number(session.toolCalls || 0); + if (String(session.modifiedAt || '') > group.latest) group.latest = String(session.modifiedAt || ''); + } + return [...groups.values()].sort((a,b) => String(b.latest).localeCompare(String(a.latest))); +} + +function renderProjectOverview(group, index) { + const id = `project-${index}`; + const search = [group.name, group.path, group.slug].join(' ').toLowerCase(); + return ` +
□${num(group.sessions.length + group.subagents.length)} traces
+

${esc(group.name)}

${esc(group.path)}
+
${num(group.sessions.length)}sessions
${num(group.subagents.length)}subagents
${num(group.prompts)}prompts
${num(group.tools)}tools
+ Inspect project → +
`; +} + +function renderProjectGroup(group, index, riskBySession) { + const id = `project-${index}`; + const search = [group.name, group.path, group.slug, ...group.sessions.map((s)=>s.title), ...group.subagents.map((s)=>s.title)].join(' ').toLowerCase(); + const primary = group.sessions.map((s) => renderSession(s, riskBySession.get(s.sessionId) || 0)).join(''); + const subagents = group.subagents.map((s) => renderSession(s, 0)).join(''); + return `
+
□
${esc(group.name)}${esc(group.path)}
${num(group.sessions.length)} sessions${num(group.subagents.length)} subagents${esc(group.latest || '—')}
+
+
Primary sessionsResume, fork or export the context you need.
${num(group.sessions.length)} total
+
${primary || '
No primary sessions in this project.
'}
+
◇ Subagent transcripts${num(group.subagents.length)} total
${subagents || '
No subagent transcripts in this project.
'}
+
+
`; +} + export function renderDashboard({ items, analysis, sessions = [], sessionAnalysis = { totals: {}, toolCounts: {}, modelCounts: {}, projectCounts: {} }, memoryIntelligence = { totals: {}, risks: [], conflicts: [], stale: [], candidates: [], riskySessions: [] }, root }) { const m = memoryIntelligence.totals || {}; const t = sessionAnalysis.totals || {}; @@ -64,12 +123,13 @@ export function renderDashboard({ items, analysis, sessions = [], sessionAnalysi const sources = items.map(renderSource).join(''); const risks = (memoryIntelligence.risks || []).slice(0,12).map(renderRisk).join(''); const conflicts = (memoryIntelligence.conflicts || []).slice(0,8).map(renderConflict).join(''); - const sessionRows = sessions.filter((s) => !s.isSubagent).slice(0,18).map((s) => renderSession(s, riskBySession.get(s.sessionId) || 0)).join(''); - const subagentRows = sessions.filter((s) => s.isSubagent).slice(0,8).map((s) => renderSession(s, 0)).join(''); + const projectGroups = buildProjectGroups(sessions); + const projectOverview = projectGroups.map(renderProjectOverview).join(''); + const projectBrowser = projectGroups.map((group,index) => renderProjectGroup(group,index,riskBySession)).join(''); const tools = topEntries(sessionAnalysis.toolCounts, 10).map(([name,count]) => `
${toolIcon(name)} ${esc(name)}
${num(count)}
`).join(''); const models = topEntries(sessionAnalysis.modelCounts, 8).map(([name,count]) => `
⬡ ${esc(name)}
${num(count)}
`).join(''); const coverage = Number(m.preservationCoverage || 0); - const recentSessionRows = sessions.filter((s) => !s.isSubagent).slice(0,5).map((s) => `▣${esc((s.cwd || s.projectSlug || '').split(/[\\/]/).filter(Boolean).pop() || s.projectSlug)}${esc(s.title)}${num(s.userPrompts)}${num(s.toolCalls)}${esc(s.models[0] || '—')}`).join(''); + const recentSessionRows = sessions.filter((s) => !s.isSubagent).slice(0,5).map((s) => `▣${esc((s.cwd || s.projectSlug || '').split(/[\\/]/).filter(Boolean).pop() || s.projectSlug)}${esc(s.title)}${num(s.userPrompts)}${num(s.toolCalls)}${esc(s.models[0] || '—')}`).join(''); const memoryItems = items.filter((item) => item.kind === 'memory'); const memoryRows = memoryItems.slice(0,10).map((item,index) => { const id=`memory-${index}`; return ``; }).join(''); const firstMemory = memoryItems[0] || null; @@ -79,7 +139,7 @@ export function renderDashboard({ items, analysis, sessions = [], sessionAnalysi const activityBars = activitySource.map((s) => ``).join(''); return `AgentMemora // Memory Intelligence
AgentMemoraMEMORY & CONTEXT CONTROL PLANE
Local · Read-only
INDEX LOCAL-01SCOPE AGENT MEMORYMODE PASSIVE INSPECTIONWRITE GUARD ENFORCED● SOURCE GRAPH ONLINE
OPERATIONAL MEMORY INTELLIGENCE // LOCAL NODE

Know what your agents remember. Detect what they forgot.

Correlate durable memory, session traces, subagent conversations and context-loss signals from one local intelligence surface.

context.scan complete
durable.memory ${num(m.memories)} sources
session.history ${num(t.sessions)} primary
subagent.history ${num(t.subagents)} traces
${coverage}%preserved
${num(m.contextLossRisks)}at risk
${num(t.projects)}projects
${num(m.conflicts)}conflicts
@@ -90,13 +150,13 @@ export function renderDashboard({ items, analysis, sessions = [], sessionAnalysi

Recent Context Sessions

View detailed sessions ↓
${recentSessionRows || ''}
Project / ContextPromptsToolsModel
No primary sessions found.
-

Memory Intelligence Workspace

durable sources // provenance // readable context // subagent comms

Memory Files

All sources ↓
All ${num(memoryItems.length)}ClaudeCodexCursorCopilot
${memoryRows || '
No durable memory files found.
'}

Memory Evidence Viewer

SOURCE VERIFIED
▤
${firstMemory ? esc(firstMemory.relativePath) : 'No memory selected'}${firstMemory ? esc(shortPath(firstMemory.path)) : 'Run a scan to discover supported memory sources'}
${firstMemory ? esc(firstMemory.provider) : '—'}
ContentProvenanceRelated SessionsConflicts ${num(m.conflicts)}
${firstMemory ? esc((firstMemory.content || '').slice(0,14000)) : 'No durable memory content available.'}
${subagentConversations || 'No subagent transcripts found.'}
+

Memory Intelligence Workspace

durable sources // provenance // readable context // subagent comms

Memory Files

All sources ↓
All ${num(memoryItems.length)}ClaudeCodexCursorCopilot
${memoryRows || '
No durable memory files found.
'}

Memory Evidence Viewer

SOURCE VERIFIED
▤
${firstMemory ? esc(firstMemory.relativePath) : 'No memory selected'}${firstMemory ? esc(shortPath(firstMemory.path)) : 'Run a scan to discover supported memory sources'}
${firstMemory ? esc(firstMemory.provider) : '—'}
ContentProvenanceRelated SessionsConflicts ${num(m.conflicts)}
${firstMemory ? esc((firstMemory.content || '').slice(0,14000)) : 'No durable memory content available.'}
${subagentConversations || 'No subagent transcripts found.'}

Intelligence Signals

heuristic indicators // source-backed // review before action

Context-loss Candidates

${num(m.contextLossRisks)} AT RISK
${risks || '
No session-only memory candidates detected.
'}

Potential Conflicts

${num(m.conflicts)} SIGNALS
${conflicts || '
No conservative key/value conflicts detected.
'}
-

Session Detail & Context Transfer

resume exact history · fork safely · export a portable capsule
${sessionRows || '
No Claude Code primary sessions found.
'}
${subagentRows ? '
SHOW ALL SUBAGENT TRANSCRIPTS (' + num(t.subagents) + ')
' + subagentRows + '
' : ''}
+

Project Explorer & Context Transfer

choose a project first · then inspect every primary session and subagent trace
Start with a project.See how many sessions and subagents belong to it, then open only the context you need.
${num(projectGroups.length)} PROJECTS INDEXED
${projectOverview || '
No Claude Code projects found.
'}
${projectBrowser}

All Memory & Context Sources

${esc(root)}
${sources || '
No supported durable memory/context sources found.
'}
@@ -106,7 +166,7 @@ export function renderDashboard({ items, analysis, sessions = [], sessionAnalysi function toast(message){const t=document.getElementById('toast');t.textContent=message;t.classList.add('show');setTimeout(()=>t.classList.remove('show'),1400)} async function copyText(text){try{await navigator.clipboard.writeText(text);toast('COPIED TO CLIPBOARD')}catch{toast('COPY FAILED')}} function downloadCapsule(id,name){const value=document.getElementById(id)?.value||'';const blob=new Blob([value],{type:'text/markdown'});const a=document.createElement('a');a.href=URL.createObjectURL(blob);a.download=name;a.click();URL.revokeObjectURL(a.href);toast('CONTEXT CAPSULE EXPORTED')} - const sessionQ=document.getElementById('sessionQ');sessionQ?.addEventListener('input',()=>{const q=sessionQ.value.toLowerCase();document.querySelectorAll('[data-session-search]').forEach(el=>el.style.display=el.dataset.sessionSearch.includes(q)?'block':'none')}); + const sessionQ=document.getElementById('sessionQ');sessionQ?.addEventListener('input',()=>{const q=sessionQ.value.toLowerCase().trim();document.querySelectorAll('.project-group').forEach(group=>{const projectMatch=(group.dataset.projectSearch||'').includes(q);let childMatch=false;group.querySelectorAll('[data-session-search]').forEach(card=>{const match=!q||projectMatch||(card.dataset.sessionSearch||'').includes(q);card.style.display=match?'':'none';if(match)childMatch=true});group.style.display=!q||projectMatch||childMatch?'':'none';if(q&&(projectMatch||childMatch))group.open=true});document.querySelectorAll('.project-card').forEach(card=>{card.style.display=!q||(card.dataset.projectSearch||'').includes(q)?'':'none'})}); const contextQ=document.getElementById('contextQ');contextQ?.addEventListener('input',()=>{const q=contextQ.value.toLowerCase();document.querySelectorAll('[data-context-search]').forEach(el=>el.style.display=el.dataset.contextSearch.includes(q)?'grid':'none')}); function selectMemory(id,row){document.querySelectorAll('.memory-file-row').forEach(el=>el.classList.remove('active'));row?.classList.add('active');const source=document.getElementById(id+'-content');document.getElementById('previewTitle').textContent=row?.dataset.title||'Memory';document.getElementById('previewPath').textContent=row?.dataset.path||'';document.getElementById('previewProvider').textContent=row?.dataset.provider||'';document.getElementById('previewCode').textContent=source?.value||'';} const globalQ=document.getElementById('globalQ');globalQ?.addEventListener('input',()=>{const q=globalQ.value.toLowerCase().trim();document.querySelectorAll('[data-searchable]').forEach(el=>el.style.display=!q||el.dataset.searchable.includes(q)?'':'none')}); From 2f314f445abbeefca5041ef5bd66b6a8e42f9fe6 Mon Sep 17 00:00:00 2001 From: hailneed Date: Fri, 18 Sep 2026 16:14:46 +0300 Subject: [PATCH 2/4] test(ui): cover project grouping and complete subagent listing --- test-node/dashboard.test.js | 27 +++++++++++++++++++++++++-- 1 file changed, 25 insertions(+), 2 deletions(-) diff --git a/test-node/dashboard.test.js b/test-node/dashboard.test.js index 525fa74..7ce7166 100644 --- a/test-node/dashboard.test.js +++ b/test-node/dashboard.test.js @@ -44,13 +44,30 @@ const subagent = { ], }; +const extraSubagents = Array.from({ length: 10 }, (_, index) => ({ + ...subagent, + id: `subagent-extra-${index + 1}`, + sessionId: `agent-extra-${index + 1}`, + title: `Subagent trace ${index + 1}`, + toolCalls: index + 1, +})); + +const secondProject = { + ...primary, + id: 'primary-2', + sessionId: 'session-789', + projectSlug: 'second-project', + cwd: '/Users/demo/projects/second-project', + title: 'Second project session', +}; + test('dashboard renders the intelligence terminal memory workspace', () => { const html = renderDashboard({ items: [memoryItem], analysis: {}, - sessions: [primary, subagent], + sessions: [primary, subagent, ...extraSubagents, secondProject], sessionAnalysis: { - totals: { sessions: 1, subagents: 1, projects: 1, userPrompts: 4, assistantMessages: 3, toolCalls: 11 }, + totals: { sessions: 2, subagents: 11, projects: 2, userPrompts: 8, assistantMessages: 3, toolCalls: 76 }, toolCounts: { Read: 4, Edit: 2, Bash: 2 }, modelCounts: { 'claude-sonnet': 1 }, projectCounts: { demo: 2 }, @@ -72,6 +89,12 @@ test('dashboard renders the intelligence terminal memory workspace', () => { assert.match(html, /Memory Intelligence Workspace/); assert.match(html, /Memory Evidence Viewer/); assert.match(html, /Subagent Comms/); + assert.match(html, /Project Explorer & Context Transfer|Project Explorer & Context Transfer/); + assert.match(html, /agentmemora/); + assert.match(html, /second-project/); + assert.match(html, /11 total/); + assert.match(html, /Subagent trace 10/); + assert.doesNotMatch(html, /SHOW ALL SUBAGENT TRANSCRIPTS/); assert.match(html, /Review memory conflicts/); assert.match(html, /Context Coverage/); assert.match(html, /Local · Read-only/); From 113b9007ff6d65090aa1f567492dd11ed08cc52f Mon Sep 17 00:00:00 2001 From: hailneed Date: Fri, 18 Sep 2026 16:14:58 +0300 Subject: [PATCH 3/4] chore(release): prepare v0.2.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index cd42ed5..43ec236 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "agentmemora", - "version": "0.1.0", + "version": "0.2.0", "description": "Local-first memory and context control plane for AI coding agents.", "type": "module", "bin": { From 59b6bf5a422ab2dc716ab822200725dc3f058f36 Mon Sep 17 00:00:00 2001 From: hailneed Date: Fri, 18 Sep 2026 16:15:01 +0300 Subject: [PATCH 4/4] docs: describe project-first context explorer --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index f995729..d641433 100644 --- a/README.md +++ b/README.md @@ -59,7 +59,7 @@ Session/tool/model statistics remain available as supporting evidence, but **mem ## Memory Intelligence -AgentMemora currently combines supported durable sources with Claude Code local session history and produces a **Memory Posture** view: +AgentMemora currently combines supported durable sources with Claude Code local session history and produces a **Memory Posture** view. The project-first explorer groups primary sessions and subagent traces by local project so you can see the full context footprint before opening individual histories: - durable memory and instruction inventory - session-only memory candidates @@ -68,6 +68,7 @@ AgentMemora currently combines supported durable sources with Claude Code local - stale memory detection (90+ days) - provenance for every source - session → memory coverage estimate +- project-level session and subagent inventory - exact-session Resume and Fork commands - deterministic Context Capsule export