wip: agui integration

This commit is contained in:
2026-02-25 19:51:58 +01:00
parent 5efbcfe03a
commit fcdf869a7c
59 changed files with 3467 additions and 267 deletions

View File

@@ -326,7 +326,19 @@ When answering questions:
2. If asked about something outside your tools (weather, news, websites), explain that you can only access the user's local blog content.
3. Be concise and helpful. Format post information clearly when displaying it.
4. If a search returns no results, suggest alternative queries or filters.
5. When asked to describe or analyze an image, use the view_image tool to see the actual image content.`;
5. When asked to describe or analyze an image, use the view_image tool to see the actual image content.
Agentic UI Contract:
- You may include structured UI payloads in your assistant response so the app can render interactive widgets.
- You DO have the ability to return interactive AGUI payloads (including bar charts) as JSON, even though you cannot draw bitmap images.
- When the user asks for a chart or guided workflow, prefer returning a valid AGUI payload over refusing.
- Use JSON with specVersion: "1" and an elements array.
- Prefer actionable widgets (cards, forms, tabs, inputs, metrics, tables, charts) when they reduce follow-up friction.
- Keep textual guidance and UI semantically consistent.
- Include only valid, supported action names. Supported actions include: openSettings, openPost, openMedia, openPanel, setActiveView, toggleSidebar, togglePanel, toggleAssistantSidebar.
- Supported element types include: text, metric, list, table, action, chart, form, input, datePicker, card, image, tabs.
- For tabs elements, include each tab with id, label, and nested elements.
- Never invent unsupported specVersion values or unsupported element/action names.`;
}
/**

View File

@@ -66,6 +66,9 @@ export interface ModelInfo {
}
export interface SendMessageOptions {
metadata?: {
surface?: 'tab' | 'sidebar';
};
onDelta?: (delta: string) => void;
onToolCall?: (toolCall: { name: string; args: unknown }) => void;
onToolResult?: (result: { name: string; result: unknown }) => void;
@@ -237,7 +240,7 @@ export class OpenCodeManager {
userMessage: string,
options: SendMessageOptions = {}
): Promise<SendMessageResult> {
const { onDelta, onToolCall, onToolResult } = options;
const { metadata, onDelta, onToolCall, onToolResult } = options;
try {
const readyCheck = await this.checkReady();
@@ -272,11 +275,15 @@ export class OpenCodeManager {
// Build message history from DB (excluding system messages)
const dbMessages = conversation.messages.filter(m => m.role !== 'system');
const surfaceHint = metadata?.surface
? `\n\n[Client UI surface: ${metadata.surface}. Render response UI for this surface while keeping content functionally equivalent.]`
: '';
const userMessageForModel = `${userMessage}${surfaceHint}`;
// Add the new user message
dbMessages.push({
conversationId,
role: 'user',
content: userMessage,
content: userMessageForModel,
createdAt: new Date(),
});

View File

@@ -256,12 +256,13 @@ export function registerChatHandlers(): void {
// ============ Chat Messaging ============
// Send a message
ipcMain.handle('chat:sendMessage', async (_, conversationId: string, message: string) => {
ipcMain.handle('chat:sendMessage', async (_, conversationId: string, message: string, metadata?: { surface?: 'tab' | 'sidebar' }) => {
try {
const manager = await getOpenCodeManager();
const mainWindow = mainWindowGetter?.();
const result = await manager.sendMessage(conversationId, message, {
metadata,
onDelta: (delta) => {
if (mainWindow) {
mainWindow.webContents.send('chat-stream-delta', { conversationId, delta });
@@ -286,6 +287,22 @@ export function registerChatHandlers(): void {
}
});
ipcMain.handle('chat:addSystemEvent', async (_, conversationId: string, content: string) => {
try {
const engine = getChatEngine();
await engine.addMessage({
conversationId,
role: 'system',
content,
createdAt: new Date(),
});
return { success: true };
} catch (error) {
console.error('[Chat IPC] Error adding system event:', error);
return { success: false, error: (error as Error).message };
}
});
// Abort a running message
ipcMain.handle('chat:abortMessage', async (_, conversationId: string) => {
try {

View File

@@ -84,7 +84,11 @@ function runWebContentsMenuAction(sender: any, action: AppMenuAction): boolean {
sender.selectAll?.();
return true;
case 'toggleDevTools':
sender.toggleDevTools?.();
if (sender.isDevToolsOpened?.()) {
sender.closeDevTools?.();
} else {
sender.openDevTools?.({ mode: 'detach' });
}
return true;
case 'reload':
sender.reload?.();

View File

@@ -49,6 +49,20 @@ interface Rectangle {
// Check if dev server is likely running (only in development)
const isDev = process.env.NODE_ENV === 'development';
function toggleDetachedDevTools(targetWindow: BrowserWindow | null): void {
const webContents = targetWindow?.webContents;
if (!webContents) {
return;
}
if (webContents.isDevToolsOpened()) {
webContents.closeDevTools();
return;
}
webContents.openDevTools({ mode: 'detach' });
}
function getWindowStatePath(): string | null {
if (typeof app.getPath !== 'function') {
return null;
@@ -246,7 +260,7 @@ function createWindow(): void {
// F12 or Ctrl+Shift+I to toggle DevTools
if (input.key === 'F12' ||
(input.control && input.shift && input.key.toLowerCase() === 'i')) {
mainWindow?.webContents.toggleDevTools();
toggleDetachedDevTools(mainWindow);
event.preventDefault();
}
});
@@ -255,13 +269,13 @@ function createWindow(): void {
const rendererPath = path.join(__dirname, '../renderer/index.html');
if (isDev) {
mainWindow.loadURL('http://localhost:5173');
mainWindow.webContents.openDevTools();
mainWindow.webContents.openDevTools({ mode: 'detach' });
} else if (fs.existsSync(rendererPath)) {
mainWindow.loadFile(rendererPath);
} else {
// Fallback to dev server if built files don't exist
mainWindow.loadURL('http://localhost:5173');
mainWindow.webContents.openDevTools();
mainWindow.webContents.openDevTools({ mode: 'detach' });
}
// Forward events to renderer
@@ -571,6 +585,11 @@ function createApplicationMenu(): Menu {
return;
}
if (action === 'toggleDevTools') {
toggleDetachedDevTools(mainWindow);
return;
}
if (action === 'viewOnGitHub') {
void shell.openExternal('https://github.com/rfc1437/bDS');
return;

View File

@@ -299,7 +299,8 @@ export const electronAPI: ElectronAPI = {
deleteConversation: (id: string) => ipcRenderer.invoke('chat:deleteConversation', id),
// Messaging
sendMessage: (conversationId: string, message: string) => ipcRenderer.invoke('chat:sendMessage', conversationId, message),
sendMessage: (conversationId: string, message: string, metadata?: { surface?: 'tab' | 'sidebar' }) => ipcRenderer.invoke('chat:sendMessage', conversationId, message, metadata),
addSystemEvent: (conversationId: string, content: string) => ipcRenderer.invoke('chat:addSystemEvent', conversationId, content),
abortMessage: (conversationId: string) => ipcRenderer.invoke('chat:abortMessage', conversationId),
getHistory: (conversationId: string) => ipcRenderer.invoke('chat:getHistory', conversationId),
clearMessages: (conversationId: string) => ipcRenderer.invoke('chat:clearMessages', conversationId),

View File

@@ -431,6 +431,10 @@ export interface ChatTitleUpdate {
title: string;
}
export interface ChatSendMetadata {
surface?: 'tab' | 'sidebar';
}
export interface SiteValidationReport {
sitemapPath: string;
sitemapChanged: boolean;
@@ -726,7 +730,8 @@ export interface ElectronAPI {
deleteConversation: (id: string) => Promise<boolean>;
// Messaging
sendMessage: (conversationId: string, message: string) => Promise<{ success: boolean; message?: string; error?: string }>;
sendMessage: (conversationId: string, message: string, metadata?: ChatSendMetadata) => Promise<{ success: boolean; message?: string; error?: string }>;
addSystemEvent: (conversationId: string, content: string) => Promise<{ success: boolean; error?: string }>;
abortMessage: (conversationId: string) => Promise<void>;
getHistory: (conversationId: string) => Promise<ChatMessage[]>;
clearMessages: (conversationId: string) => Promise<void>;

View File

@@ -24,6 +24,7 @@
"menu.item.viewMedia": "Medien",
"menu.item.toggleSidebar": "Seitenleiste umschalten",
"menu.item.togglePanel": "Panel umschalten",
"menu.item.toggleAssistantSidebar": "Assistenz-Seitenleiste umschalten",
"menu.item.toggleDevTools": "Entwicklerwerkzeuge umschalten",
"menu.item.reload": "Neu laden",
"menu.item.forceReload": "Erzwungen neu laden",

View File

@@ -24,6 +24,7 @@
"menu.item.viewMedia": "Media",
"menu.item.toggleSidebar": "Toggle Sidebar",
"menu.item.togglePanel": "Toggle Panel",
"menu.item.toggleAssistantSidebar": "Toggle Assistant Sidebar",
"menu.item.toggleDevTools": "Toggle Developer Tools",
"menu.item.reload": "Reload",
"menu.item.forceReload": "Force Reload",

View File

@@ -24,6 +24,7 @@
"menu.item.viewMedia": "Medios",
"menu.item.toggleSidebar": "Alternar barra lateral",
"menu.item.togglePanel": "Alternar panel",
"menu.item.toggleAssistantSidebar": "Alternar barra del asistente",
"menu.item.toggleDevTools": "Alternar herramientas de desarrollo",
"menu.item.reload": "Recargar",
"menu.item.forceReload": "Forzar recarga",

View File

@@ -24,6 +24,7 @@
"menu.item.viewMedia": "Médias",
"menu.item.toggleSidebar": "Basculer la barre latérale",
"menu.item.togglePanel": "Basculer le panneau",
"menu.item.toggleAssistantSidebar": "Basculer le panneau Assistant",
"menu.item.toggleDevTools": "Basculer les outils de développement",
"menu.item.reload": "Recharger",
"menu.item.forceReload": "Forcer le rechargement",

View File

@@ -24,6 +24,7 @@
"menu.item.viewMedia": "Contenuti media",
"menu.item.toggleSidebar": "Attiva/disattiva barra laterale",
"menu.item.togglePanel": "Attiva/disattiva pannello",
"menu.item.toggleAssistantSidebar": "Attiva/disattiva barra assistente",
"menu.item.toggleDevTools": "Attiva/disattiva strumenti sviluppatore",
"menu.item.reload": "Ricarica",
"menu.item.forceReload": "Forza ricaricamento",

View File

@@ -19,6 +19,7 @@ export type AppMenuAction =
| 'viewMedia'
| 'toggleSidebar'
| 'togglePanel'
| 'toggleAssistantSidebar'
| 'toggleDevTools'
| 'reload'
| 'forceReload'
@@ -103,6 +104,7 @@ export const APP_MENU_GROUPS: AppMenuGroupDefinition[] = [
{ label: 'menu.item.viewMedia', action: 'viewMedia', accelerator: 'CmdOrCtrl+2' },
{ label: 'menu.item.toggleSidebar', action: 'toggleSidebar', accelerator: 'CmdOrCtrl+B' },
{ label: 'menu.item.togglePanel', action: 'togglePanel', accelerator: 'CmdOrCtrl+J' },
{ label: 'menu.item.toggleAssistantSidebar', action: 'toggleAssistantSidebar', accelerator: 'CmdOrCtrl+\\' },
{ label: 'menu.item.toggleDevTools', action: 'toggleDevTools', accelerator: 'CmdOrCtrl+Shift+I' },
{ label: '', action: 'view-separator-1', separator: true },
{ label: 'menu.item.reload', action: 'reload' },
@@ -156,6 +158,7 @@ export const APP_MENU_ACTION_EVENT_MAP: Partial<Record<AppMenuAction, string>> =
viewMedia: 'menu:viewMedia',
toggleSidebar: 'menu:toggleSidebar',
togglePanel: 'menu:togglePanel',
toggleAssistantSidebar: 'menu:toggleAssistantSidebar',
toggleDevTools: 'menu:toggleDevTools',
previewPost: 'menu:previewPost',
publishSelected: 'menu:publishSelected',