mirror of
https://github.com/getcompanion-ai/co-mono.git
synced 2026-04-20 18:02:11 +00:00
Add onCompleted callback system for guaranteed console log delivery
- Add onCompleted() callback registration in RuntimeMessageBridge - Modify wrapperFunction to call completion callbacks before returning - Update ConsoleRuntimeProvider to immediate send + completion batch pattern - Extract DOWNLOADABLE_FILE_RUNTIME_DESCRIPTION from ATTACHMENTS_RUNTIME_DESCRIPTION - Logs sent immediately (fire-and-forget), unsent logs batched at completion - Ensures all console logs arrive before tool execution completes
This commit is contained in:
parent
b288cd9448
commit
af0297cd16
4 changed files with 70 additions and 20 deletions
|
|
@ -25,7 +25,7 @@ export class ConsoleRuntimeProvider implements SandboxRuntimeProvider {
|
||||||
|
|
||||||
getRuntime(): (sandboxId: string) => void {
|
getRuntime(): (sandboxId: string) => void {
|
||||||
return (_sandboxId: string) => {
|
return (_sandboxId: string) => {
|
||||||
// Console capture
|
// Console capture with immediate send + completion batch pattern
|
||||||
const originalConsole = {
|
const originalConsole = {
|
||||||
log: console.log,
|
log: console.log,
|
||||||
error: console.error,
|
error: console.error,
|
||||||
|
|
@ -33,6 +33,9 @@ export class ConsoleRuntimeProvider implements SandboxRuntimeProvider {
|
||||||
info: console.info,
|
info: console.info,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Track pending logs (not yet confirmed sent)
|
||||||
|
const pendingLogs: Array<{ method: string; text: string; args: any[] }> = [];
|
||||||
|
|
||||||
["log", "error", "warn", "info"].forEach((method) => {
|
["log", "error", "warn", "info"].forEach((method) => {
|
||||||
(console as any)[method] = (...args: any[]) => {
|
(console as any)[method] = (...args: any[]) => {
|
||||||
const text = args
|
const text = args
|
||||||
|
|
@ -45,7 +48,12 @@ export class ConsoleRuntimeProvider implements SandboxRuntimeProvider {
|
||||||
})
|
})
|
||||||
.join(" ");
|
.join(" ");
|
||||||
|
|
||||||
// Send to extension if available (online mode)
|
const logEntry = { method, text, args };
|
||||||
|
|
||||||
|
// Add to pending logs
|
||||||
|
pendingLogs.push(logEntry);
|
||||||
|
|
||||||
|
// Try to send immediately (fire-and-forget)
|
||||||
if ((window as any).sendRuntimeMessage) {
|
if ((window as any).sendRuntimeMessage) {
|
||||||
(window as any)
|
(window as any)
|
||||||
.sendRuntimeMessage({
|
.sendRuntimeMessage({
|
||||||
|
|
@ -54,8 +62,15 @@ export class ConsoleRuntimeProvider implements SandboxRuntimeProvider {
|
||||||
text,
|
text,
|
||||||
args, // Send raw args for provider collection
|
args, // Send raw args for provider collection
|
||||||
})
|
})
|
||||||
|
.then(() => {
|
||||||
|
// Remove from pending on successful send
|
||||||
|
const index = pendingLogs.indexOf(logEntry);
|
||||||
|
if (index !== -1) {
|
||||||
|
pendingLogs.splice(index, 1);
|
||||||
|
}
|
||||||
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
// Ignore errors in fire-and-forget console messages
|
// Keep in pending array if send fails
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -64,6 +79,25 @@ export class ConsoleRuntimeProvider implements SandboxRuntimeProvider {
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Register completion callback to send any remaining logs
|
||||||
|
if ((window as any).onCompleted) {
|
||||||
|
(window as any).onCompleted(async (_success: boolean) => {
|
||||||
|
// Send any logs that haven't been sent yet
|
||||||
|
if (pendingLogs.length > 0 && (window as any).sendRuntimeMessage) {
|
||||||
|
await Promise.all(
|
||||||
|
pendingLogs.map((logEntry) =>
|
||||||
|
(window as any).sendRuntimeMessage({
|
||||||
|
type: "console",
|
||||||
|
method: logEntry.method,
|
||||||
|
text: logEntry.text,
|
||||||
|
args: logEntry.args,
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Track errors for HTML artifacts
|
// Track errors for HTML artifacts
|
||||||
let lastError: { message: string; stack: string } | null = null;
|
let lastError: { message: string; stack: string } | null = null;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -27,6 +27,7 @@ export class RuntimeMessageBridge {
|
||||||
private static generateSandboxBridge(sandboxId: string): string {
|
private static generateSandboxBridge(sandboxId: string): string {
|
||||||
// Returns stringified function that uses window.parent.postMessage
|
// Returns stringified function that uses window.parent.postMessage
|
||||||
return `
|
return `
|
||||||
|
window.__completionCallbacks = [];
|
||||||
window.sendRuntimeMessage = async (message) => {
|
window.sendRuntimeMessage = async (message) => {
|
||||||
const messageId = 'msg_' + Date.now() + '_' + Math.random().toString(36).substring(2, 9);
|
const messageId = 'msg_' + Date.now() + '_' + Math.random().toString(36).substring(2, 9);
|
||||||
|
|
||||||
|
|
@ -57,18 +58,25 @@ window.sendRuntimeMessage = async (message) => {
|
||||||
}, 30000);
|
}, 30000);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
window.onCompleted = (callback) => {
|
||||||
|
window.__completionCallbacks.push(callback);
|
||||||
|
};
|
||||||
`.trim();
|
`.trim();
|
||||||
}
|
}
|
||||||
|
|
||||||
private static generateUserScriptBridge(sandboxId: string): string {
|
private static generateUserScriptBridge(sandboxId: string): string {
|
||||||
// Returns stringified function that uses chrome.runtime.sendMessage
|
// Returns stringified function that uses chrome.runtime.sendMessage
|
||||||
return `
|
return `
|
||||||
|
window.__completionCallbacks = [];
|
||||||
window.sendRuntimeMessage = async (message) => {
|
window.sendRuntimeMessage = async (message) => {
|
||||||
return await chrome.runtime.sendMessage({
|
return await chrome.runtime.sendMessage({
|
||||||
...message,
|
...message,
|
||||||
sandboxId: ${JSON.stringify(sandboxId)}
|
sandboxId: ${JSON.stringify(sandboxId)}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
window.onCompleted = (callback) => {
|
||||||
|
window.__completionCallbacks.push(callback);
|
||||||
|
};
|
||||||
`.trim();
|
`.trim();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -54,7 +54,10 @@ export { PersistentStorageDialog } from "./dialogs/PersistentStorageDialog.js";
|
||||||
export { SessionListDialog } from "./dialogs/SessionListDialog.js";
|
export { SessionListDialog } from "./dialogs/SessionListDialog.js";
|
||||||
export { ApiKeysTab, ProxyTab, SettingsDialog, SettingsTab } from "./dialogs/SettingsDialog.js";
|
export { ApiKeysTab, ProxyTab, SettingsDialog, SettingsTab } from "./dialogs/SettingsDialog.js";
|
||||||
// Prompts
|
// Prompts
|
||||||
export { ARTIFACTS_RUNTIME_PROVIDER_DESCRIPTION } from "./prompts/tool-prompts.js";
|
export {
|
||||||
|
ARTIFACTS_RUNTIME_PROVIDER_DESCRIPTION,
|
||||||
|
DOWNLOADABLE_FILE_RUNTIME_DESCRIPTION,
|
||||||
|
} from "./prompts/tool-prompts.js";
|
||||||
// Storage
|
// Storage
|
||||||
export { AppStorage, getAppStorage, setAppStorage } from "./storage/app-storage.js";
|
export { AppStorage, getAppStorage, setAppStorage } from "./storage/app-storage.js";
|
||||||
export { IndexedDBStorageBackend } from "./storage/backends/indexeddb-storage-backend.js";
|
export { IndexedDBStorageBackend } from "./storage/backends/indexeddb-storage-backend.js";
|
||||||
|
|
|
||||||
|
|
@ -218,6 +218,27 @@ Example:
|
||||||
await createArtifact('image.png', base64);
|
await createArtifact('image.png', base64);
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Downloadable File Runtime Provider
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
export const DOWNLOADABLE_FILE_RUNTIME_DESCRIPTION = `
|
||||||
|
Downloadable Files (one-time downloads for the user - YOU cannot read these back):
|
||||||
|
- await returnDownloadableFile(filename, content, mimeType?) - Create downloadable file (async!)
|
||||||
|
* Use for: Processed/transformed data, generated images, analysis results
|
||||||
|
* Important: This creates a download for the user. You will NOT be able to access this file's content later.
|
||||||
|
* If you need to access the data later, use createArtifact() instead (if available).
|
||||||
|
* Always use await with returnDownloadableFile
|
||||||
|
* REQUIRED: For Blob/Uint8Array binary content, you MUST supply a proper MIME type (e.g., "image/png").
|
||||||
|
If omitted, throws an Error with stack trace pointing to the offending line.
|
||||||
|
* Strings without a MIME default to text/plain.
|
||||||
|
* Objects are auto-JSON stringified and default to application/json unless a MIME is provided.
|
||||||
|
* Canvas images: Use toBlob() with await Promise wrapper
|
||||||
|
* Examples:
|
||||||
|
- await returnDownloadableFile('cleaned-data.csv', csvString, 'text/csv')
|
||||||
|
- await returnDownloadableFile('analysis.json', {results: [...]}, 'application/json')
|
||||||
|
- await returnDownloadableFile('chart.png', blob, 'image/png')`;
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// Attachments Runtime Provider
|
// Attachments Runtime Provider
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
@ -235,22 +256,6 @@ User Attachments (files the user added to the conversation):
|
||||||
* Example: const xlsxBytes = readBinaryAttachment(files[0].id);
|
* Example: const xlsxBytes = readBinaryAttachment(files[0].id);
|
||||||
* Example: const XLSX = await import('https://esm.run/xlsx'); const workbook = XLSX.read(xlsxBytes);
|
* Example: const XLSX = await import('https://esm.run/xlsx'); const workbook = XLSX.read(xlsxBytes);
|
||||||
|
|
||||||
Downloadable Files (one-time downloads for the user - YOU cannot read these back):
|
|
||||||
- await returnDownloadableFile(filename, content, mimeType?) - Create downloadable file (async!)
|
|
||||||
* Use for: Processed/transformed data, generated images, analysis results
|
|
||||||
* Important: This creates a download for the user. You will NOT be able to access this file's content later.
|
|
||||||
* If you need to access the data later, use createArtifact() instead (if available).
|
|
||||||
* Always use await with returnDownloadableFile
|
|
||||||
* REQUIRED: For Blob/Uint8Array binary content, you MUST supply a proper MIME type (e.g., "image/png").
|
|
||||||
If omitted, throws an Error with stack trace pointing to the offending line.
|
|
||||||
* Strings without a MIME default to text/plain.
|
|
||||||
* Objects are auto-JSON stringified and default to application/json unless a MIME is provided.
|
|
||||||
* Canvas images: Use toBlob() with await Promise wrapper
|
|
||||||
* Examples:
|
|
||||||
- await returnDownloadableFile('cleaned-data.csv', csvString, 'text/csv')
|
|
||||||
- await returnDownloadableFile('analysis.json', {results: [...]}, 'application/json')
|
|
||||||
- await returnDownloadableFile('chart.png', blob, 'image/png')
|
|
||||||
|
|
||||||
Common pattern - Process attachment and create download:
|
Common pattern - Process attachment and create download:
|
||||||
const files = listAttachments();
|
const files = listAttachments();
|
||||||
const csvFile = files.find(f => f.fileName.endsWith('.csv'));
|
const csvFile = files.find(f => f.fileName.endsWith('.csv'));
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue