Skip to content

Commit 1813e0f

Browse files
author
Tim Sinaeve
committed
feat: stabilize agentic workspace management with robust ID resolution and persistent logging
Fixes persistent deletion failures caused by name-to-UUID resolution errors and missing IPC logging handlers. Updates ChatPanel to maintain reactive workspace state sync and hardens SQLite foreign key handling during node creation.
1 parent 43825f0 commit 1813e0f

14 files changed

Lines changed: 955 additions & 237 deletions

File tree

App.tsx

Lines changed: 126 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -181,7 +181,7 @@ interface UpdateToastState {
181181

182182
export const MainApp: React.FC = () => {
183183
const { settings, saveSettings, loaded: settingsLoaded } = useSettings();
184-
const { nodes, items, addDocument, addFolder, updateItem, commitVersion, deleteItems, moveItems, getDescendantIds, duplicateItems, addDocumentsFromFiles, importNodesFromTransfer, createDocumentFromClipboard, setItemLock, isLoading: areDocumentsLoading } = useDocuments();
184+
const { nodes, items, addDocument, addFolder, updateItem, commitVersion, deleteItems, moveItems, getDescendantIds, duplicateItems, addDocumentsFromFiles, importNodesFromTransfer, createDocumentFromClipboard, setItemLock, refreshNodes, getLatestItems, isLoading: areDocumentsLoading } = useDocuments();
185185
const { templates, addTemplate, updateTemplate, deleteTemplate, deleteTemplates } = useTemplates();
186186
const { theme } = useTheme();
187187

@@ -196,6 +196,8 @@ export const MainApp: React.FC = () => {
196196
const [pendingRevealId, setPendingRevealId] = useState<string | null>(null);
197197
const [renamingNodeId, setRenamingNodeId] = useState<string | null>(null);
198198
const [selectedText, setSelectedText] = useState<string | undefined>(undefined);
199+
const [chatContextNodeIds, setChatContextNodeIds] = useState(new Set<string>());
200+
const [hasLoadedChatContext, setHasLoadedChatContext] = useState(false);
199201
const [pendingInsertText, setPendingInsertText] = useState<string | null>(null);
200202

201203
const [view, setView] = useState<'editor' | 'info' | 'settings'>('editor');
@@ -432,6 +434,38 @@ export const MainApp: React.FC = () => {
432434
return unsubscribe;
433435
}, [addLog]);
434436

437+
const addToChatContextAction = useCallback((ids: string[]) => {
438+
addLog('INFO', `User action: Adding ${ids.length} item(s) to chat context.`);
439+
setChatContextNodeIds(prev => {
440+
const next = new Set(prev);
441+
ids.forEach(id => {
442+
const item = items.find(i => i.id === id);
443+
if (item?.type === 'folder') {
444+
// Recursively find all documents within this folder
445+
const descendantIds = getDescendantIds(id);
446+
descendantIds.forEach(dId => {
447+
const dNode = items.find(n => n.id === dId);
448+
if (dNode?.type === 'document') {
449+
next.add(dId);
450+
}
451+
});
452+
} else if (item?.type === 'document') {
453+
next.add(id);
454+
}
455+
});
456+
return next;
457+
});
458+
if (!isChatPanelVisible) {
459+
setIsChatPanelVisible(true);
460+
}
461+
}, [addLog, isChatPanelVisible, items, getDescendantIds]);
462+
463+
useEffect(() => {
464+
if (settingsLoaded && hasLoadedChatContext) {
465+
storageService.save(LOCAL_STORAGE_KEYS.CHAT_CONTEXT_NODE_IDS, Array.from(chatContextNodeIds));
466+
}
467+
}, [chatContextNodeIds, settingsLoaded, hasLoadedChatContext]);
468+
435469
useEffect(() => {
436470
if (!isElectron || !window.electronAPI?.dbGetPath) {
437471
setDatabaseStatus({ message: 'Database actions unavailable in this environment.', tone: 'info' });
@@ -1501,13 +1535,22 @@ export const MainApp: React.FC = () => {
15011535
}
15021536
setExpandedFolderIds(new Set(ids));
15031537
})
1504-
.catch(() => {
1505-
// Loading failed; we'll fall back to the default empty set.
1506-
})
1538+
.catch(() => {})
15071539
.finally(() => {
1508-
if (!isCancelled) {
1509-
setHasLoadedExpandedFolders(true);
1540+
if (!isCancelled) setHasLoadedExpandedFolders(true);
1541+
});
1542+
1543+
storageService
1544+
.load<string[]>(LOCAL_STORAGE_KEYS.CHAT_CONTEXT_NODE_IDS, [])
1545+
.then(ids => {
1546+
if (isCancelled) return;
1547+
if (Array.isArray(ids)) {
1548+
setChatContextNodeIds(new Set(ids));
15101549
}
1550+
})
1551+
.catch(() => {})
1552+
.finally(() => {
1553+
if (!isCancelled) setHasLoadedChatContext(true);
15111554
});
15121555

15131556
return () => {
@@ -1866,6 +1909,52 @@ export const MainApp: React.FC = () => {
18661909
setRenamingNodeId(newDoc.id);
18671910
}, [addDocument, getParentIdForNewItem, ensureNodeVisible, addLog, activateDocumentTab]);
18681911

1912+
const handleAgentAddNode = useCallback(async (params: any) => {
1913+
addLog('INFO', `Agent action: Creating new ${params.node_type} "${params.title}"...`);
1914+
1915+
let resultNode: any;
1916+
if (params.node_type === 'folder') {
1917+
resultNode = await addFolder(params.parent_id || params.parentId, params.title);
1918+
} else {
1919+
resultNode = await addDocument({
1920+
parentId: params.parent_id || params.parentId,
1921+
title: params.title,
1922+
content: params.document?.content || params.content,
1923+
doc_type: params.document?.doc_type || params.docType,
1924+
language_hint: params.document?.language_hint || params.languageHint
1925+
});
1926+
1927+
if (params.document?.content || params.content) {
1928+
await commitVersion(resultNode.id, params.document?.content || params.content);
1929+
}
1930+
}
1931+
1932+
if (resultNode) {
1933+
ensureNodeVisible(resultNode);
1934+
activateDocumentTab(resultNode.id);
1935+
setSelectedIds(new Set([resultNode.id]));
1936+
setLastClickedId(resultNode.id);
1937+
1938+
// Map back to the 'Node' structure expected by agentService.ts
1939+
return {
1940+
node_id: resultNode.id,
1941+
parent_id: resultNode.parentId,
1942+
node_type: resultNode.type,
1943+
title: resultNode.title,
1944+
created_at: resultNode.createdAt,
1945+
updated_at: resultNode.updatedAt,
1946+
locked: resultNode.locked,
1947+
document: resultNode.type === 'document' ? {
1948+
node_id: resultNode.id,
1949+
content: resultNode.content,
1950+
doc_type: resultNode.doc_type,
1951+
language_hint: resultNode.language_hint,
1952+
} : undefined
1953+
};
1954+
}
1955+
throw new Error('Failed to create node via agent.');
1956+
}, [addDocument, addFolder, commitVersion, ensureNodeVisible, activateDocumentTab, addLog]);
1957+
18691958
const handleNewDocumentFromClipboard = useCallback(async (parentId?: string | null) => {
18701959
addLog('INFO', 'User action: Create New Document from Clipboard.');
18711960
const effectiveParentId = parentId !== undefined ? parentId : getParentIdForNewItem();
@@ -2405,6 +2494,11 @@ export const MainApp: React.FC = () => {
24052494
handleDeleteSelection(idsToDelete, { force: shiftKey });
24062495
}, [items, selectedIds, handleDeleteSelection, addLog]);
24072496

2497+
const handleAgentDeleteNodes = useCallback(async (ids: string[]) => {
2498+
addLog('INFO', `Agent action: Deleting ${ids.length} nodes.`);
2499+
await handleDeleteSelection(new Set(ids), { force: true });
2500+
}, [handleDeleteSelection, addLog]);
2501+
24082502
const handleDeleteTemplate = useCallback((id: string, shiftKey: boolean = false) => {
24092503
const templateToDelete = templates.find(t => t.template_id === id);
24102504
if (!templateToDelete) return;
@@ -2872,6 +2966,11 @@ export const MainApp: React.FC = () => {
28722966
{ label: 'New Folder', icon: FolderPlusIcon, action: () => handleNewFolder(parentIdForNewItem), shortcut: getCommand('new-folder')?.shortcutString },
28732967
{ label: 'New from Template...', icon: DocumentDuplicateIcon, action: newFromTemplateAction, shortcut: getCommand('new-from-template')?.shortcutString },
28742968
{ type: 'separator' },
2969+
{ label: 'Add to Chat Context', icon: SearchIcon, action: () => {
2970+
const docIds = selectedNodes.filter(n => n.type === 'document').map(n => n.id);
2971+
if (docIds.length > 0) addToChatContextAction(docIds);
2972+
}, disabled: !hasDocuments },
2973+
{ type: 'separator' },
28752974
{ label: 'Format', icon: FormatIcon, action: handleFormatDocument, disabled: !isFormattable || currentSelection.size !== 1, shortcut: getCommand('format-document')?.shortcutString },
28762975
{ label: firstSelectedNode?.locked ? 'Unlock Document' : 'Lock Document', icon: firstSelectedNode?.locked ? LockOpenIcon : LockClosedIcon, action: () => { if (firstSelectedNode && firstSelectedNode.type === 'document') { void handleSetNodeLockState(firstSelectedNode.id, !firstSelectedNode.locked); } }, disabled: !isDocument || currentSelection.size !== 1, shortcut: getCommand('toggle-document-lock')?.shortcutString },
28772976
{ label: 'Rename', icon: PencilIcon, action: () => handleStartRenamingNode(nodeId), disabled: currentSelection.size !== 1, shortcut: getCommand('rename-item')?.shortcutString },
@@ -2897,7 +2996,7 @@ export const MainApp: React.FC = () => {
28972996
position: { x: e.clientX, y: e.clientY },
28982997
items: menuItems
28992998
});
2900-
}, [selectedIds, items, handleNewDocument, handleNewFolder, handleDuplicateSelection, handleDeleteSelection, handleCopyNodeContent, addLog, enrichedCommands, handleOpenNewCodeFileModal, handleFormatDocument, handleStartRenamingNode, handleNewDocumentFromClipboard, handleSaveNodeToFile, handleSetNodeLockState]);
2999+
}, [selectedIds, items, handleNewDocument, handleNewFolder, handleDuplicateSelection, handleDeleteSelection, handleCopyNodeContent, addLog, enrichedCommands, handleOpenNewCodeFileModal, handleFormatDocument, handleStartRenamingNode, handleNewDocumentFromClipboard, handleSaveNodeToFile, handleSetNodeLockState, addToChatContextAction]);
29013000

29023001

29033002
const handleSidebarMouseDown = useCallback((e: React.MouseEvent) => {
@@ -3278,18 +3377,35 @@ export const MainApp: React.FC = () => {
32783377
onApplyToEditor={handleApplyToEditor}
32793378
onCreateDocument={handleCreateDocumentFromChat}
32803379
activeDocument={activeNode ? { title: activeNode.title, content: activeNode.content ?? '' } : undefined}
3380+
chatContextNodeIds={chatContextNodeIds}
3381+
onRemoveNodeFromContext={(id) => setChatContextNodeIds(prev => {
3382+
const next = new Set(prev);
3383+
next.delete(id);
3384+
return next;
3385+
})}
3386+
onAddNodesToContext={addToChatContextAction}
3387+
onClearAllContext={() => setChatContextNodeIds(new Set())}
32813388
selectedText={selectedText}
32823389
addLog={addLog}
32833390
nodes={nodes}
3284-
addNode={handleNewDocument}
3285-
updateNode={handleRenameNode}
3391+
addNode={handleAgentAddNode}
3392+
updateNode={async (id, updates) => {
3393+
await handleRenameNode(id, updates.title);
3394+
}}
32863395
updateDocumentContent={handleCommitVersion}
3287-
deleteNodes={handleDeleteNode}
3396+
deleteNodes={handleAgentDeleteNodes}
32883397
moveNodes={moveItems}
3398+
getLatestItems={async () => {
3399+
await refreshNodes();
3400+
const latestNodes = await repository.getNodeTree();
3401+
return latestNodes;
3402+
}}
32893403
runPython={async (code, nodeId) => {
3404+
// Real implementation would go here, currently handled by ChatPanel's inner context
32903405
return "";
32913406
}}
32923407
runScript={async (lang, code, nodeId) => {
3408+
// Real implementation would go here
32933409
return "";
32943410
}}
32953411
/>

FUNCTIONAL_MANUAL.md

Lines changed: 31 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -148,13 +148,36 @@ Import PDFs by dragging `.pdf` files from your operating system into the sidebar
148148

149149
#### RAG (Chat with Workspace)
150150

151-
The "Chat with Workspace" feature allows you to query your entire collection of documents using a local AI model.
151+
The "Chat with Workspace" feature allows you to query your entire collection of documents using a local AI model. It uses semantic search to find answers even when you don't use the exact keywords.
152+
153+
- **Indexing**: Before you can chat, DocForge needs to index your workspace. This process creates a semantic representation of your documents.
154+
- Click **Build Index** (or **Rebuild**) in the Chat Panel to start.
155+
- A progress bar will show the number of documents being processed.
156+
- Once complete, the "indexed" badge in the chat header will reflect the current state of your workspace.
157+
- **Semantic Retrieval**: When you ask a question, DocForge searches for the most relevant "chunks" of text across all your indexed files.
158+
- **Configurable Retrieval**: In Settings, you can adjust the **RAG Similarity Threshold**.
159+
- A **lower value** (e.g., 0.8) makes the search more restrictive, returning only highly relevant matches.
160+
- A **higher value** (e.g., 1.5) returns more broad context.
161+
- **Context Limit**: You can configure the chat to analyze up to **500 documents** at once for deep workspace insights.
162+
- **Source Attribution**: When the AI answers a question, it lists the specific documents it used as context.
163+
- Click a source badge to instantly open and navigate to that document.
164+
- Sources are streamed as soon as the search completes, before the AI starts typing its answer.
165+
- **Smart Privacy**: If the AI determines that none of the retrieved documents contain the answer to your question, the source list is automatically hidden to keep the chat interface clean.
166+
167+
#### Multi-Document Context
168+
169+
Beyond searching your entire workspace, you can explicitly "pin" documents to a conversation for focused reasoning.
170+
171+
- **Adding Context**:
172+
- **Right-Click**: Select one or more documents in the sidebar, right-click, and choose **Add to Chat Context**.
173+
- **Drag and Drop**: Drag one or more nodes from the sidebar and drop them directly onto the Chat Panel. A "Drop to add Context" overlay will appear.
174+
- **Context Badges**: Active context is displayed as badges at the top of the chat area.
175+
- **Active Document**: Automatically includes the file you are currently editing.
176+
- **Pinned Documents**: Indicated with a 📌 icon. These stay in the context even if you switch tabs.
177+
- **Selection active**: If you select text in the editor, it is automatically included in the next chat message.
178+
- **Removing Context**: Click the **X** on any context badge to remove that specific document from the current session.
179+
- **Cross-Document Reasoning**: Use this to ask questions like "Compare the requirements in Document A with the implementation in Document B."
152180

153-
- **Indexing**: Before you can chat, DocForge needs to index your workspace. This process creates a semantic representation of your documents, allowing the AI to find relevant context even if the exact keywords don't match.
154-
- **Configurable Retrieval**: In Settings, you can adjust the **RAG Similarity Threshold**. A lower value (e.g., 0.8) makes the search more restrictive, returning only highly relevant matches, while a higher value (e.g., 1.5) returns more context.
155-
- **Context Limit**: You can configure the chat to analyze up to **500 documents** at once for deep workspace insights.
156-
- **Source Attribution**: When the AI answers a question, it lists the specific documents it used as context. These sources are streamed immediately as soon as the search completes.
157-
- **Smart Privacy**: If the AI determines that none of the retrieved documents contain the answer to your question, the source list is automatically hidden to keep the chat interface clean.
158181

159182
#### Agentic Chat & Workspace Orchestration
160183

@@ -199,6 +222,8 @@ You can drop an item (or a group of items):
199222

200223
**Importing from your computer:** You can also drag files and folders directly from your operating system's file explorer into the sidebar. Dropping them on a folder will import them into that folder, while dropping them in an empty area will import them to the root. The original folder structure is preserved.
201224

225+
**Adding to Chat Context:** Drag and drop is also used to augment your AI conversations. Drag one or more documents from the sidebar and drop them onto the **Chat Panel** to instantly pin them as context for your next question.
226+
202227
### AI-Powered Refinement
203228

204229
Clicking the **Refine with AI** (sparkles) button in the editor toolbar sends your current document content to your configured local LLM. The AI's task is not to *answer* the document's request, but to *improve* the document itself. A modal will appear with the suggested refinement, which you can then accept or discard. The "Accept" button is the default and can be triggered by pressing `Enter`. This feature is available for Markdown and plaintext documents.

0 commit comments

Comments
 (0)