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
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": {
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 IntelligenceINDEX 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
| Project / Context | Prompts | Tools | Model | |
${recentSessionRows || '| No primary sessions found. |
'}
-Memory Intelligence Workspace
durable sources // provenance // readable context // subagent commsAll ${num(memoryItems.length)}ClaudeCodexCursorCopilot
${memoryRows || '
No durable memory files found.
'}
Memory Evidence Viewer
SOURCE VERIFIEDContentProvenanceRelated 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 commsAll ${num(memoryItems.length)}ClaudeCodexCursorCopilot
${memoryRows || '
No durable memory files found.
'}
Memory Evidence Viewer
SOURCE VERIFIEDContentProvenanceRelated 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 actionContext-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 traceStart 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')});
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/);