feat(floating-ai): step 5 — add ai:action IPC side-channel
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 <noreply@anthropic.com>
This commit is contained in:
@@ -642,7 +642,7 @@ Use these existing patterns from the codebase:
|
|||||||
|
|
||||||
## Step 5: Add `ai:action` IPC Side-Channel
|
## Step 5: Add `ai:action` IPC Side-Channel
|
||||||
|
|
||||||
**Status**: [ ]
|
**Status**: [x] (2026-02-28)
|
||||||
**Prerequisites**: Step 4 completed
|
**Prerequisites**: Step 4 completed
|
||||||
**Modifies**:
|
**Modifies**:
|
||||||
- `src/preload/trpc.ts`
|
- `src/preload/trpc.ts`
|
||||||
|
|||||||
@@ -24,6 +24,10 @@ import { searchNotes, type SearchResult } from '../db/vectordb';
|
|||||||
const TOOL_CALLING_PROVIDERS = new Set(['openai', 'anthropic', 'copilot']);
|
const TOOL_CALLING_PROVIDERS = new Set(['openai', 'anthropic', 'copilot']);
|
||||||
|
|
||||||
const AI_STREAM_CHANNEL = 'ai:stream';
|
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
|
// Types
|
||||||
@@ -185,9 +189,11 @@ function buildProjectTools(projectId: string): StructuredTool[] {
|
|||||||
priority: input.priority ?? 'medium',
|
priority: input.priority ?? 'medium',
|
||||||
dueDate: input.dueDate ? new Date(input.dueDate).getTime() : null,
|
dueDate: input.dueDate ? new Date(input.dueDate).getTime() : null,
|
||||||
projectId,
|
projectId,
|
||||||
|
isAiSuggested: 1,
|
||||||
createdAt: Date.now(),
|
createdAt: Date.now(),
|
||||||
})
|
})
|
||||||
.run();
|
.run();
|
||||||
|
sendAction(currentSender, { type: 'task_created', taskId: id });
|
||||||
return `Task added: ${input.title}`;
|
return `Task added: ${input.title}`;
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -262,6 +268,7 @@ function buildProjectTools(projectId: string): StructuredTool[] {
|
|||||||
createdAt: Date.now(),
|
createdAt: Date.now(),
|
||||||
}).run();
|
}).run();
|
||||||
}
|
}
|
||||||
|
sendAction(currentSender, { type: 'checkpoints_suggested', count: parsed.length });
|
||||||
return jsonStr;
|
return jsonStr;
|
||||||
} catch {
|
} catch {
|
||||||
return '[]';
|
return '[]';
|
||||||
@@ -311,6 +318,7 @@ function buildProjectTools(projectId: string): StructuredTool[] {
|
|||||||
createdAt: Date.now(),
|
createdAt: Date.now(),
|
||||||
}).run();
|
}).run();
|
||||||
}
|
}
|
||||||
|
sendAction(currentSender, { type: 'tasks_suggested', count: parsed.length });
|
||||||
return jsonStr;
|
return jsonStr;
|
||||||
} catch {
|
} catch {
|
||||||
return '[]';
|
return '[]';
|
||||||
@@ -348,9 +356,11 @@ function buildGlobalTools(): StructuredTool[] {
|
|||||||
priority: input.priority ?? 'medium',
|
priority: input.priority ?? 'medium',
|
||||||
dueDate: input.dueDate ? new Date(input.dueDate).getTime() : null,
|
dueDate: input.dueDate ? new Date(input.dueDate).getTime() : null,
|
||||||
projectId: input.projectId ?? null,
|
projectId: input.projectId ?? null,
|
||||||
|
isAiSuggested: 1,
|
||||||
createdAt: Date.now(),
|
createdAt: Date.now(),
|
||||||
})
|
})
|
||||||
.run();
|
.run();
|
||||||
|
sendAction(currentSender, { type: 'task_created', taskId: id });
|
||||||
return `Task added: ${input.title}`;
|
return `Task added: ${input.title}`;
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -849,12 +859,18 @@ function sendStreamChunk(sender: Electron.WebContents | undefined, token: string
|
|||||||
sender.send(AI_STREAM_CHANNEL, { token, done });
|
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)
|
// Orchestrate (public entry point)
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
export async function orchestrate(input: OrchestrateInput): Promise<OrchestrateResult> {
|
export async function orchestrate(input: OrchestrateInput): Promise<OrchestrateResult> {
|
||||||
const { message, context, sender } = input;
|
const { message, context, sender } = input;
|
||||||
|
currentSender = sender;
|
||||||
|
|
||||||
// Quick check: is an LLM available?
|
// Quick check: is an LLM available?
|
||||||
const llm = await getLLM();
|
const llm = await getLLM();
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ contextBridge.exposeInMainWorld('electronTRPC', {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const AI_STREAM_CHANNEL = 'ai:stream';
|
const AI_STREAM_CHANNEL = 'ai:stream';
|
||||||
|
const AI_ACTION_CHANNEL = 'ai:action';
|
||||||
|
|
||||||
contextBridge.exposeInMainWorld('electronAI', {
|
contextBridge.exposeInMainWorld('electronAI', {
|
||||||
/** Subscribe to AI streaming chunks. Returns an unsubscribe function. */
|
/** Subscribe to AI streaming chunks. Returns an unsubscribe function. */
|
||||||
@@ -30,4 +31,13 @@ contextBridge.exposeInMainWorld('electronAI', {
|
|||||||
ipcRenderer.removeListener(AI_STREAM_CHANNEL, handler);
|
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);
|
||||||
|
};
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ interface ElectronTRPC {
|
|||||||
|
|
||||||
interface ElectronAI {
|
interface ElectronAI {
|
||||||
onStreamChunk: (cb: (data: { token: string; done: boolean }) => void) => () => void;
|
onStreamChunk: (cb: (data: { token: string; done: boolean }) => void) => () => void;
|
||||||
|
onAction: (cb: (data: { type: string; taskId?: string; count?: number }) => void) => () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
|
|||||||
Reference in New Issue
Block a user