From 6c498c5f402ab9dc783dd872f0b5401e0ab8f467 Mon Sep 17 00:00:00 2001 From: Roberto Musso Date: Sat, 28 Feb 2026 09:23:04 +0100 Subject: [PATCH] =?UTF-8?q?feat(floating-ai):=20step=205=20=E2=80=94=20add?= =?UTF-8?q?=20ai:action=20IPC=20side-channel?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add a new ai:action IPC channel so the renderer can react to AI tool side-effects (task creation, checkpoint/task suggestions). Also mark AI-created tasks with isAiSuggested: 1 in both project and global add_task tools. Co-Authored-By: Claude Opus 4.6 --- docs/floating-ai-integration-guide.md | 2 +- src/main/ai/orchestrator.ts | 16 ++++++++++++++++ src/preload/trpc.ts | 10 ++++++++++ src/renderer/lib/ipcLink.ts | 1 + 4 files changed, 28 insertions(+), 1 deletion(-) diff --git a/docs/floating-ai-integration-guide.md b/docs/floating-ai-integration-guide.md index 69b14a6..8371f87 100644 --- a/docs/floating-ai-integration-guide.md +++ b/docs/floating-ai-integration-guide.md @@ -642,7 +642,7 @@ Use these existing patterns from the codebase: ## Step 5: Add `ai:action` IPC Side-Channel -**Status**: [ ] +**Status**: [x] (2026-02-28) **Prerequisites**: Step 4 completed **Modifies**: - `src/preload/trpc.ts` diff --git a/src/main/ai/orchestrator.ts b/src/main/ai/orchestrator.ts index d13d71b..598da6d 100644 --- a/src/main/ai/orchestrator.ts +++ b/src/main/ai/orchestrator.ts @@ -24,6 +24,10 @@ import { searchNotes, type SearchResult } from '../db/vectordb'; const TOOL_CALLING_PROVIDERS = new Set(['openai', 'anthropic', 'copilot']); const AI_STREAM_CHANNEL = 'ai:stream'; +const AI_ACTION_CHANNEL = 'ai:action'; + +/** Module-level sender ref — set at the start of orchestrate() so tool closures can emit actions. */ +let currentSender: Electron.WebContents | undefined; // --------------------------------------------------------------------------- // Types @@ -185,9 +189,11 @@ function buildProjectTools(projectId: string): StructuredTool[] { priority: input.priority ?? 'medium', dueDate: input.dueDate ? new Date(input.dueDate).getTime() : null, projectId, + isAiSuggested: 1, createdAt: Date.now(), }) .run(); + sendAction(currentSender, { type: 'task_created', taskId: id }); return `Task added: ${input.title}`; }, { @@ -262,6 +268,7 @@ function buildProjectTools(projectId: string): StructuredTool[] { createdAt: Date.now(), }).run(); } + sendAction(currentSender, { type: 'checkpoints_suggested', count: parsed.length }); return jsonStr; } catch { return '[]'; @@ -311,6 +318,7 @@ function buildProjectTools(projectId: string): StructuredTool[] { createdAt: Date.now(), }).run(); } + sendAction(currentSender, { type: 'tasks_suggested', count: parsed.length }); return jsonStr; } catch { return '[]'; @@ -348,9 +356,11 @@ function buildGlobalTools(): StructuredTool[] { priority: input.priority ?? 'medium', dueDate: input.dueDate ? new Date(input.dueDate).getTime() : null, projectId: input.projectId ?? null, + isAiSuggested: 1, createdAt: Date.now(), }) .run(); + sendAction(currentSender, { type: 'task_created', taskId: id }); return `Task added: ${input.title}`; }, { @@ -849,12 +859,18 @@ function sendStreamChunk(sender: Electron.WebContents | undefined, token: string sender.send(AI_STREAM_CHANNEL, { token, done }); } +function sendAction(sender: Electron.WebContents | undefined, action: { type: string; taskId?: string; count?: number }): void { + if (!sender || sender.isDestroyed()) return; + sender.send(AI_ACTION_CHANNEL, action); +} + // --------------------------------------------------------------------------- // Orchestrate (public entry point) // --------------------------------------------------------------------------- export async function orchestrate(input: OrchestrateInput): Promise { const { message, context, sender } = input; + currentSender = sender; // Quick check: is an LLM available? const llm = await getLLM(); diff --git a/src/preload/trpc.ts b/src/preload/trpc.ts index 19ea582..8b177b7 100644 --- a/src/preload/trpc.ts +++ b/src/preload/trpc.ts @@ -20,6 +20,7 @@ contextBridge.exposeInMainWorld('electronTRPC', { }); const AI_STREAM_CHANNEL = 'ai:stream'; +const AI_ACTION_CHANNEL = 'ai:action'; contextBridge.exposeInMainWorld('electronAI', { /** Subscribe to AI streaming chunks. Returns an unsubscribe function. */ @@ -30,4 +31,13 @@ contextBridge.exposeInMainWorld('electronAI', { ipcRenderer.removeListener(AI_STREAM_CHANNEL, handler); }; }, + + /** Subscribe to AI action events (task created, suggestions, etc.). Returns unsubscribe. */ + onAction: (cb: (data: { type: string; taskId?: string; count?: number }) => void) => { + const handler = (_event: Electron.IpcRendererEvent, data: { type: string; taskId?: string; count?: number }) => cb(data); + ipcRenderer.on(AI_ACTION_CHANNEL, handler); + return () => { + ipcRenderer.removeListener(AI_ACTION_CHANNEL, handler); + }; + }, }); diff --git a/src/renderer/lib/ipcLink.ts b/src/renderer/lib/ipcLink.ts index 1daa343..7f7e39b 100644 --- a/src/renderer/lib/ipcLink.ts +++ b/src/renderer/lib/ipcLink.ts @@ -15,6 +15,7 @@ interface ElectronTRPC { interface ElectronAI { onStreamChunk: (cb: (data: { token: string; done: boolean }) => void) => () => void; + onAction: (cb: (data: { type: string; taskId?: string; count?: number }) => void) => () => void; } declare global {