Skip to content
Merged

Misc #27

Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
0b72ded
feat(graph,repos): integrate enterprise code repo AST indexing and op…
oksbwn Oct 1, 2026
045df2d
feat(graph): add canvas renderer, layout pre-computation & import res…
oksbwn Oct 1, 2026
05ff7f3
feat(graph): convert entity inspector to modal popup
oksbwn Oct 1, 2026
8bd2765
feat(mcp): add dynamic prompts & resource subscriptions
oksbwn Oct 1, 2026
2ffb5ba
feat(workspace-index): refactor multi-level index and unify subpage h…
oksbwn Oct 2, 2026
2b73b9d
git commit -m "refactor(nav): consolidate menus, settings, and subpag…
oksbwn Oct 2, 2026
dc1464c
UI/UX Fixed
oksbwn Oct 2, 2026
687bfb0
Subpage reafctored
oksbwn Oct 2, 2026
7a1a9c4
feat(ui): add theme-aware AI status flyout, align status bar pills, a…
oksbwn Oct 2, 2026
dda0b89
feat(ui): add hover fan-down popover and theme styling for downloads …
oksbwn Oct 2, 2026
d52b121
fix(calendar): calibrate note and task timestamps, lifecycles, and vi…
oksbwn Oct 2, 2026
39e38d6
Corener radius of the containers fixed!
oksbwn Oct 2, 2026
eed259f
feat(git): overhaul version control into note revision hub & live sync
oksbwn Oct 2, 2026
f308955
feat(media): standardize media viewer, transcripts, and asset table
oksbwn Oct 2, 2026
fdef409
Fixed Lints
oksbwn Oct 2, 2026
4cf9ed3
Notifications system Fixed
oksbwn Oct 3, 2026
ab377b6
feat(wireframe): modularize Wireframe Studio components, clean dead c…
oksbwn Oct 3, 2026
fef77b3
CSS Fixed
oksbwn Oct 3, 2026
839ce1d
fix(wireframe): restore stencil drag-and-drop and click insertion ren…
oksbwn Oct 3, 2026
64a54be
feat(wireframe): enable 8-point interactive resize handles on canvas …
oksbwn Oct 3, 2026
d1c3e12
fix(wireframe): forward canvas shortcuts, live-sync resizer styles, a…
oksbwn Oct 3, 2026
6544dcc
feat(wireframe): add column presets and fix layout overflow
oksbwn Oct 3, 2026
c187c3d
fix(wireframe): resolve eslint warnings and categories import
oksbwn Oct 3, 2026
b019ca8
fix(test): enable vitest fork isolation for clean test suite runs
oksbwn Oct 3, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,13 @@ All notable documentation and user-facing behavior changes are tracked in this f

## Unreleased

- Added **Attached Code Repositories & AST Knowledge Graph Integration**:
- Added **Diagrams & Media Asset Manager & Media Viewer Standardization**:
- High-density table layout with fixed 260px sidebar replacing card grid for managing workspace media, diagrams, PDFs, and recordings.
- Category filters with real-time counters and high-contrast badges for Diagrams, UI Prototypes, Images, PDFs, Videos, Audio, Transcripts, and Documents.
- Safe asset deletion workflow with reference checking to prevent accidental broken links in notes.
- Co-located asset bundle folders (`media/audio/<id>/` and `media/video/<id>/`) containing primary recording files alongside companion `transcript.json`.
- Unified full-screen `MediaPreviewPane` modal with de-duplicated header bar, 100% full-width stretch, and interactive Speech-to-Text transcript drawer.
- Interactive transcript card rendering (`.markdown-transcript-card`) in markdown note preview with View, Copy, and Download actions.
- Connect external Git repositories to workspace metadata (`.notes-app/metadata.json`) with branch and commit tracking.
- Multi-language code AST extractor (`CodeKnowledgeSource.js`) parsing JS/TS, Python, Go, Rust, Java, C/C++, Ruby, and PHP files into structured code entities (CodeModule, CodeClass, CodeInterface, CodeFunction, APIEndpoint, DBModel) and mapping `IMPORTS`/`CONTAINS`/`DOCUMENTS` graph relationships.
- Interactive **Attached Repositories Explorer** page (`AttachedReposPage.jsx`, `AttachedReposPage.css`) accessible via dashboard card, TitleBar menu (`Workspace → Attached Repositories`), and Command Palette (`open-attached-repos`).
Expand Down
13 changes: 13 additions & 0 deletions ai/diagnostics/AIHealth.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ const { aiService } = require('../core/AIService');

const path = require('path');
const fs = require('fs');
const os = require('os');

function getEmbeddingStats(db) {
let chunks = 0;
Expand Down Expand Up @@ -153,10 +154,22 @@ function getSubsystemHealth() {
console.error('[MCP Health] Failed to gather MCP stats:', err.message);
}

const freeMemBytes = typeof os.freemem === 'function' ? os.freemem() : 0;
const totalMemBytes = typeof os.totalmem === 'function' ? os.totalmem() : 0;
const freeMemMb = Math.round(freeMemBytes / 1024 / 1024);
const totalMemMb = Math.round(totalMemBytes / 1024 / 1024);
const isLowMemory = freeMemMb < 400;

return {
enabled: isEnabled,
initialized: isInitialized,
serverStatus: isEnabled ? 'Running' : 'Stopped',
systemMemory: {
freeMemMb,
totalMemMb,
isLowMemory,
mode: isLowMemory ? 'standby' : 'normal'
},
mcp: mcpStats,
database: {
status: dbStatus,
Expand Down
44 changes: 43 additions & 1 deletion ai/graph/EntityResolver.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,48 @@ const { createLogger } = require('../core/logger');

const log = createLogger('EntityResolver');

const CANONICAL_TYPE_MAP = {
person: 'Person',
organization: 'Organization',
company: 'Company',
location: 'Location',
event: 'Event',
concept: 'Concept',
project: 'Project',
task: 'Task',
decision: 'Decision',
idea: 'Idea',
tag: 'Tag',
image: 'Image',
document: 'Document',
folder: 'Folder',
workspace: 'Workspace',
note: 'Note',
section: 'Section',
externalurl: 'ExternalURL',
external_url: 'ExternalURL',
codeblock: 'CodeBlock',
diagram: 'Diagram',
keyterm: 'KeyTerm',
formula: 'Formula',
callout: 'Callout',
repo: 'Repo',
codemodule: 'CodeModule',
codeclass: 'CodeClass',
codeinterface: 'CodeInterface',
codefunction: 'CodeFunction',
apiendpoint: 'APIEndpoint',
dbmodel: 'DBModel'
};

function canonicalizeType(t) {
if (!t) return 'Concept';
const clean = String(t).trim();
const key = clean.toLowerCase().replace(/[^a-z0-9]/g, '');
if (CANONICAL_TYPE_MAP[key]) return CANONICAL_TYPE_MAP[key];
return clean.charAt(0).toUpperCase() + clean.slice(1);
}

class EntityResolver {
constructor(graphDb, embeddingService = null) {
this.graphDb = graphDb;
Expand Down Expand Up @@ -185,7 +227,7 @@ class EntityResolver {
}
}

return proposedType || 'Concept';
return canonicalizeType(proposedType);
}

/**
Expand Down
87 changes: 74 additions & 13 deletions ai/graph/GraphBuilder.js
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ class GraphBuilder {
const ExcalidrawKnowledgeSource = require('./sources/ExcalidrawKnowledgeSource');
const DrawioKnowledgeSource = require('./sources/DrawioKnowledgeSource');
const MermaidKnowledgeSource = require('./sources/MermaidKnowledgeSource');
const CodeKnowledgeSource = require('./sources/CodeKnowledgeSource');
const { CodeKnowledgeSource } = require('./sources/code');

const registry = new KnowledgeSourceRegistry();

Expand Down Expand Up @@ -119,6 +119,29 @@ class GraphBuilder {
: `ent-${item.source.sourceType()}-${String(ent.name).toLowerCase().replace(/[^a-z0-9]+/g, '-')}`;
this.graphDb.upsertEntity({ id, name: ent.name, canonical_name: ent.name, type: ent.type || 'Entity', properties: ent.properties || {} });
}

const evidenceMap = new Map();
if (Array.isArray(evidence)) {
for (const ev of evidence) {
const evId = evidenceStore.addEvidence({
sourceId: ev.sourceId || item.path,
extractor: ev.extractor || item.source.sourceType(),
subjectText: ev.subjectText || ev.subject_text || item.path,
subjectSpanStart: ev.subjectSpanStart ?? null,
subjectSpanEnd: ev.subjectSpanEnd ?? null,
predicateText: ev.predicateText || ev.predicate_text || 'related_to',
objectText: ev.objectText || ev.object_text || '',
objectSpanStart: ev.objectSpanStart ?? null,
objectSpanEnd: ev.objectSpanEnd ?? null,
rawSentence: ev.rawSentence || ev.raw_sentence || ev.subjectText || item.path,
confidence: ev.confidence || item.source.baseConfidence() || 1.0
});
if (evId && ev.subjectText && ev.predicateText && ev.objectText) {
evidenceMap.set(`${ev.subjectText}::${ev.predicateText}::${ev.objectText}`, evId);
}
}
}

for (const rel of relationships) {
const srcId = this.graphService?.entityResolver
? this.graphService.entityResolver.generateEntityId(rel.source_name, rel.source_type || 'Entity')
Expand All @@ -129,19 +152,15 @@ class GraphBuilder {
if (srcId !== tgtId) {
this.graphDb.upsertEntity({ id: srcId, name: rel.source_name, canonical_name: rel.source_name, type: rel.source_type || 'Entity' });
this.graphDb.upsertEntity({ id: tgtId, name: rel.target_name, canonical_name: rel.target_name, type: rel.target_type || 'Entity' });
this.graphDb.upsertRelationship({ source_id: srcId, target_id: tgtId, type: rel.type, weight: rel.weight, confidence: rel.confidence });
}
}
if (Array.isArray(evidence)) {
for (const ev of evidence) {
evidenceStore.addEvidence({
sourceId: item.path,
const evId = evidenceMap.get(`${rel.source_name}::${rel.type}::${rel.target_name}`) || null;
this.graphDb.upsertRelationship({
source_id: srcId,
target_id: tgtId,
type: rel.type,
weight: rel.weight,
confidence: rel.confidence,
extractor: item.source.sourceType(),
subjectText: ev.subjectText || ev.subject_text || item.path,
predicateText: ev.predicateText || ev.predicate_text || 'related_to',
objectText: ev.objectText || ev.object_text || '',
rawSentence: ev.rawSentence || ev.raw_sentence || ev.subjectText || item.path,
confidence: ev.confidence || item.source.baseConfidence() || 1.0
evidence_id: evId
});
}
}
Expand Down Expand Up @@ -199,11 +218,53 @@ class GraphBuilder {
const validator = new GraphValidationEngine(this.graphDb, this._logDb);
await validator.validate();

// Pre-compute & cache 2D force layout coordinates in SQLite
if (this.graphDb && typeof this.graphDb.recomputeLayout === 'function') {
try {
this.graphDb.recomputeLayout();
} catch (lErr) {
log.warn('GraphLayoutEngine pass skipped:', lErr.message);
}
}

// Optimize SQLite query planner
if (this.graphDb?.db) {
try { this.graphDb.db.exec('PRAGMA ANALYZE;'); } catch { /* ignore */ }
}

// Update attached repos scan stats in metadata.json
if (attachedRepos.length > 0 && workspaceRoot) {
try {
const metaPath = path.join(workspaceRoot, '.notes-app', 'metadata.json');
if (fs.existsSync(metaPath)) {
const metaObj = JSON.parse(fs.readFileSync(metaPath, 'utf8'));
if (Array.isArray(metaObj.attachedRepos)) {
let changed = false;
for (const r of metaObj.attachedRepos) {
if (!r || !r.id) continue;
let symbolCount = 0;
try {
const countRow = this.graphDb.db.prepare(
"SELECT COUNT(*) as count FROM entities WHERE properties LIKE ?"
).get(`%"repoId":"${r.id}"%`);
symbolCount = countRow?.count || 0;
} catch { /* ignore count query */ }

r.lastScannedAt = new Date().toISOString();
r.status = 'indexed';
r.symbolCount = symbolCount;
changed = true;
}
if (changed) {
fs.writeFileSync(metaPath, JSON.stringify(metaObj, null, 2), 'utf8');
}
}
}
} catch (metaErr) {
log.warn('Failed to update attached repo scan stats in metadata.json:', metaErr.message);
}
}

log.info(`Knowledge Graph rebuild complete. Processed: ${processedCount}, Failed: ${failedCount}`);
this._logDb?.addLog('graph', `Knowledge Graph rebuild complete. Processed: ${processedCount}, Failed: ${failedCount}`, 'info', {
processedCount,
Expand Down
81 changes: 77 additions & 4 deletions ai/graph/GraphDB.js
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ class GraphDB {
}

// Versioned database schema migrations (Gap 3)
const TARGET_SCHEMA_VERSION = 4;
const TARGET_SCHEMA_VERSION = 5;
let currentVersion = 0;
try {
const vRow = this.db.prepare('PRAGMA user_version').get();
Expand Down Expand Up @@ -158,6 +158,12 @@ class GraphDB {
this.db.exec(CREATE_ENTITY_EMBEDDINGS_TABLE);
} catch { /* ignore */ }
}
if (fromVersion < 5) {
// Version 5: Add layout coordinates and node degree cache
try { this.db.exec('ALTER TABLE entities ADD COLUMN pos_x REAL;'); } catch { /* ignore */ }
try { this.db.exec('ALTER TABLE entities ADD COLUMN pos_y REAL;'); } catch { /* ignore */ }
try { this.db.exec('ALTER TABLE entities ADD COLUMN degree INTEGER DEFAULT 0;'); } catch { /* ignore */ }
}
}

upsertEntityVector(entityId, vectorArray) {
Expand Down Expand Up @@ -376,8 +382,13 @@ class GraphDB {
} catch { /* ignore junction insert error */ }
}
} catch (err) {
if (err.message?.includes('FOREIGN KEY') && evidence_id) {
stmt.run(source_id, target_id, type, weight, clampedConfidence, extractor, metadataJson, null);
if (err.message?.includes('FOREIGN KEY')) {
try {
const now = new Date().toISOString();
this.db.prepare('INSERT OR IGNORE INTO entities (id, name, canonical_name, type, updated_at) VALUES (?, ?, ?, ?, ?)').run(source_id, source_id, source_id, 'Entity', now);
this.db.prepare('INSERT OR IGNORE INTO entities (id, name, canonical_name, type, updated_at) VALUES (?, ?, ?, ?, ?)').run(target_id, target_id, target_id, 'Entity', now);
stmt.run(source_id, target_id, type, weight, clampedConfidence, extractor, metadataJson, null);
} catch { /* ignore fallback error */ }
} else {
throw err;
}
Expand Down Expand Up @@ -443,7 +454,14 @@ class GraphDB {
.map(e => {
const props = typeof e.properties === 'string' ? JSON.parse(e.properties || '{}') : (e.properties || {});
const conf = typeof e.confidence === 'number' ? e.confidence : (typeof props.confidence === 'number' ? props.confidence : 1.0);
return { ...e, confidence: conf, properties: props };
return {
...e,
confidence: conf,
properties: props,
pos_x: typeof e.pos_x === 'number' && !isNaN(e.pos_x) ? e.pos_x : null,
pos_y: typeof e.pos_y === 'number' && !isNaN(e.pos_y) ? e.pos_y : null,
degree: typeof e.degree === 'number' ? e.degree : 0
};
})
.filter(e => e.confidence >= minConfidence);

Expand All @@ -467,6 +485,61 @@ class GraphDB {
return { entities, relationships };
}

/**
* Batch update entity coordinates and connectivity degrees
* @param {Map<string, {x: number, y: number, degree: number}>|Array<{id: string, pos_x: number, pos_y: number, degree: number}>} positions
*/
updateNodePositions(positions) {
if (!this.db || !positions) return 0;
try {
const updateStmt = this.db.prepare('UPDATE entities SET pos_x = ?, pos_y = ?, degree = ? WHERE id = ?');
let count = 0;

if (positions instanceof Map) {
for (const [id, pos] of positions.entries()) {
try {
updateStmt.run(pos.x, pos.y, pos.degree || 0, id);
count++;
} catch { /* continue on single node error */ }
}
} else if (Array.isArray(positions)) {
for (const pos of positions) {
try {
updateStmt.run(pos.pos_x ?? pos.x, pos.pos_y ?? pos.y, pos.degree || 0, pos.id);
count++;
} catch { /* continue */ }
}
}

return count;
} catch (err) {
log.warn('Failed to update node positions in SQLite:', err.message);
return 0;
}
}

/**
* Recompute full graph layout and cache coordinates to SQLite
* @param {Object} options
*/
recomputeLayout(options = {}) {
if (!this.db) return null;
try {
const GraphLayoutEngine = require('./layout/GraphLayoutEngine');
const { entities, relationships } = this.getAll(0.0);
if (!entities.length) return { nodes: [], positions: new Map() };

const layoutResult = GraphLayoutEngine.computeLayout(entities, relationships, options);
if (layoutResult?.positions) {
this.updateNodePositions(layoutResult.positions);
}
return layoutResult;
} catch (err) {
log.warn('Failed to recompute and cache graph layout:', err.message);
return null;
}
}

/**
* Find entity by note path
*/
Expand Down
8 changes: 7 additions & 1 deletion ai/graph/GraphSchema.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ CREATE TABLE IF NOT EXISTS entities (
extractor TEXT DEFAULT 'gliner',
model_version TEXT DEFAULT '2.1',
confidence REAL DEFAULT 1.0,
pos_x REAL,
pos_y REAL,
degree INTEGER DEFAULT 0,
created_at TEXT DEFAULT (datetime('now')),
updated_at TEXT DEFAULT (datetime('now'))
);
Expand Down Expand Up @@ -101,7 +104,10 @@ const ALTER_ENTITIES_ADD_COLUMNS = [
`ALTER TABLE entities ADD COLUMN source_count INTEGER DEFAULT 1;`,
`ALTER TABLE entities ADD COLUMN first_seen_at TEXT DEFAULT (datetime('now'));`,
`ALTER TABLE entities ADD COLUMN is_retired INTEGER DEFAULT 0;`,
`ALTER TABLE entities ADD COLUMN merged_into TEXT;`
`ALTER TABLE entities ADD COLUMN merged_into TEXT;`,
`ALTER TABLE entities ADD COLUMN pos_x REAL;`,
`ALTER TABLE entities ADD COLUMN pos_y REAL;`,
`ALTER TABLE entities ADD COLUMN degree INTEGER DEFAULT 0;`
];

const CREATE_RELATIONSHIP_EVIDENCE_TABLE = `
Expand Down
Loading
Loading