fix(vscode): wire native notifications from runtime events
This commit is contained in:
@@ -127,7 +127,7 @@ export const NotificationSettings: React.FC = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const canShowNotifications = isDesktop || (isBrowser && typeof Notification !== 'undefined' && Notification.permission === 'granted');
|
const canShowNotifications = isDesktop || isVSCode || (isBrowser && typeof Notification !== 'undefined' && Notification.permission === 'granted');
|
||||||
|
|
||||||
const updateTemplate = (
|
const updateTemplate = (
|
||||||
event: 'completion' | 'error' | 'question' | 'subtask',
|
event: 'completion' | 'error' | 'question' | 'subtask',
|
||||||
@@ -489,7 +489,7 @@ export const NotificationSettings: React.FC = () => {
|
|||||||
{isVSCode && (
|
{isVSCode && (
|
||||||
<div className="mt-1 px-2">
|
<div className="mt-1 px-2">
|
||||||
<p className="typography-meta text-muted-foreground/70">
|
<p className="typography-meta text-muted-foreground/70">
|
||||||
VS Code runtime handles notifications separately natively.
|
When enabled, notifications are delivered through VS Code native notifications.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -173,7 +173,7 @@ export const OpenChamberVisualSettings: React.FC<OpenChamberVisualSettingsProps>
|
|||||||
const hasLayoutSettings = shouldShow('fontSize') || shouldShow('terminalFontSize') || shouldShow('spacing') || shouldShow('cornerRadius') || shouldShow('inputBarOffset');
|
const hasLayoutSettings = shouldShow('fontSize') || shouldShow('terminalFontSize') || shouldShow('spacing') || shouldShow('cornerRadius') || shouldShow('inputBarOffset');
|
||||||
const hasBehaviorSettings = shouldShow('toolOutput')
|
const hasBehaviorSettings = shouldShow('toolOutput')
|
||||||
|| shouldShow('diffLayout')
|
|| shouldShow('diffLayout')
|
||||||
|| shouldShow('mobileStatusBar')
|
|| (shouldShow('mobileStatusBar') && isMobile)
|
||||||
|| shouldShow('dotfiles')
|
|| shouldShow('dotfiles')
|
||||||
|| shouldShow('reasoning')
|
|| shouldShow('reasoning')
|
||||||
|| shouldShow('queueMode')
|
|| shouldShow('queueMode')
|
||||||
@@ -548,9 +548,9 @@ export const OpenChamberVisualSettings: React.FC<OpenChamberVisualSettingsProps>
|
|||||||
</section>
|
</section>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{(shouldShow('mobileStatusBar') || shouldShow('dotfiles') || shouldShow('queueMode') || shouldShow('persistDraft') || shouldShow('reasoning') || shouldShow('textJustificationActivity')) && (
|
{((shouldShow('mobileStatusBar') && isMobile) || shouldShow('dotfiles') || shouldShow('queueMode') || shouldShow('persistDraft') || shouldShow('reasoning') || shouldShow('textJustificationActivity')) && (
|
||||||
<section className="p-2 space-y-0.5">
|
<section className="p-2 space-y-0.5">
|
||||||
{shouldShow('mobileStatusBar') && (
|
{shouldShow('mobileStatusBar') && isMobile && (
|
||||||
<div
|
<div
|
||||||
className="group flex cursor-pointer items-center gap-2 py-1.5"
|
className="group flex cursor-pointer items-center gap-2 py-1.5"
|
||||||
role="button"
|
role="button"
|
||||||
|
|||||||
@@ -257,6 +257,7 @@ export const SettingsView: React.FC<SettingsViewProps> = ({ onClose, forceMobile
|
|||||||
return SETTINGS_PAGE_METADATA
|
return SETTINGS_PAGE_METADATA
|
||||||
.filter((page) => page.slug !== 'home')
|
.filter((page) => page.slug !== 'home')
|
||||||
.filter((page) => isPageAvailable(page, runtimeCtx))
|
.filter((page) => isPageAvailable(page, runtimeCtx))
|
||||||
|
.filter((page) => !(runtimeCtx.isVSCode && page.slug === 'projects'))
|
||||||
.filter((page) => !(isMobile && page.slug === 'shortcuts'));
|
.filter((page) => !(isMobile && page.slug === 'shortcuts'));
|
||||||
}, [runtimeCtx, isMobile]);
|
}, [runtimeCtx, isMobile]);
|
||||||
|
|
||||||
|
|||||||
@@ -132,7 +132,6 @@ export const useEventStream = () => {
|
|||||||
} = useSessionStore();
|
} = useSessionStore();
|
||||||
|
|
||||||
const { checkConnection } = useConfigStore();
|
const { checkConnection } = useConfigStore();
|
||||||
const nativeNotificationsEnabled = useUIStore((state) => state.nativeNotificationsEnabled);
|
|
||||||
const fallbackDirectory = useDirectoryStore((state) => state.currentDirectory);
|
const fallbackDirectory = useDirectoryStore((state) => state.currentDirectory);
|
||||||
|
|
||||||
const activeSessionDirectory = React.useMemo(() => {
|
const activeSessionDirectory = React.useMemo(() => {
|
||||||
@@ -378,6 +377,7 @@ export const useEventStream = () => {
|
|||||||
const questionToastShownRef = React.useRef<Set<string>>(new Set());
|
const questionToastShownRef = React.useRef<Set<string>>(new Set());
|
||||||
const notifiedMessagesRef = React.useRef<Set<string>>(new Set());
|
const notifiedMessagesRef = React.useRef<Set<string>>(new Set());
|
||||||
const notifiedQuestionsRef = React.useRef<Set<string>>(new Set());
|
const notifiedQuestionsRef = React.useRef<Set<string>>(new Set());
|
||||||
|
const serverNotificationEventSeenRef = React.useRef(false);
|
||||||
const modeSwitchToastShownRef = React.useRef<Set<string>>(new Set());
|
const modeSwitchToastShownRef = React.useRef<Set<string>>(new Set());
|
||||||
const lastUserAgentSelectionRef = React.useRef<Map<string, { created: number; messageId: string }>>(new Map());
|
const lastUserAgentSelectionRef = React.useRef<Map<string, { created: number; messageId: string }>>(new Map());
|
||||||
|
|
||||||
@@ -403,6 +403,50 @@ export const useEventStream = () => {
|
|||||||
>(() => Promise.resolve());
|
>(() => Promise.resolve());
|
||||||
const scheduleReconnectRef = React.useRef<(hint?: string) => void>(() => {});
|
const scheduleReconnectRef = React.useRef<(hint?: string) => void>(() => {});
|
||||||
|
|
||||||
|
const isNotificationContextHidden = React.useCallback((isVSCodeRuntime: boolean): boolean => {
|
||||||
|
if (visibilityStateRef.current === 'hidden') {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (isVSCodeRuntime && typeof document !== 'undefined') {
|
||||||
|
return !document.hasFocus();
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const dispatchRuntimeNotification = React.useCallback((payload: {
|
||||||
|
title: string;
|
||||||
|
body?: string;
|
||||||
|
tag?: string;
|
||||||
|
requireHidden?: boolean;
|
||||||
|
}) => {
|
||||||
|
const runtimeAPIs = getRegisteredRuntimeAPIs();
|
||||||
|
if (!runtimeAPIs?.notifications) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const title = typeof payload.title === 'string' ? payload.title.trim() : '';
|
||||||
|
if (!title) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const settings = useUIStore.getState();
|
||||||
|
if (!settings.nativeNotificationsEnabled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const isVSCodeRuntime = Boolean(runtimeAPIs.runtime?.isVSCode);
|
||||||
|
const shouldRequireHidden = Boolean(payload.requireHidden) || settings.notificationMode === 'hidden-only';
|
||||||
|
if (shouldRequireHidden && !isNotificationContextHidden(isVSCodeRuntime)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
void runtimeAPIs.notifications.notifyAgentCompletion({
|
||||||
|
title,
|
||||||
|
body: typeof payload.body === 'string' ? payload.body : '',
|
||||||
|
tag: typeof payload.tag === 'string' ? payload.tag : undefined,
|
||||||
|
});
|
||||||
|
}, [isNotificationContextHidden]);
|
||||||
|
|
||||||
const maybeBootstrapIfStale = React.useCallback(
|
const maybeBootstrapIfStale = React.useCallback(
|
||||||
(reason: string) => {
|
(reason: string) => {
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
@@ -1253,8 +1297,9 @@ export const useEventStream = () => {
|
|||||||
const finishCandidate = (message as { finish?: unknown }).finish;
|
const finishCandidate = (message as { finish?: unknown }).finish;
|
||||||
const finish = typeof finishCandidate === 'string' ? finishCandidate : null;
|
const finish = typeof finishCandidate === 'string' ? finishCandidate : null;
|
||||||
const eventHasStopFinish = finish === 'stop';
|
const eventHasStopFinish = finish === 'stop';
|
||||||
|
const eventHasErrorFinish = finish === 'error';
|
||||||
|
|
||||||
if (!hasParts && !completedFromServer && !hasCompletedStatus && !eventHasStopFinish) break;
|
if (!hasParts && !completedFromServer && !hasCompletedStatus && !eventHasStopFinish && !eventHasErrorFinish) break;
|
||||||
|
|
||||||
if ((messageExt as { role?: unknown }).role === 'assistant' && hasParts) {
|
if ((messageExt as { role?: unknown }).role === 'assistant' && hasParts) {
|
||||||
const hasQuestionTool = partsArray.some((part) => (
|
const hasQuestionTool = partsArray.some((part) => (
|
||||||
@@ -1277,6 +1322,44 @@ export const useEventStream = () => {
|
|||||||
|
|
||||||
updateMessageInfo(sessionId, messageId, message as unknown as Message);
|
updateMessageInfo(sessionId, messageId, message as unknown as Message);
|
||||||
|
|
||||||
|
const messageRole = typeof (message as { role?: unknown }).role === 'string'
|
||||||
|
? (message as { role: string }).role
|
||||||
|
: null;
|
||||||
|
const runtimeAPIs = getRegisteredRuntimeAPIs();
|
||||||
|
const shouldSynthesizeNotifications = Boolean(runtimeAPIs?.runtime?.isVSCode) && !serverNotificationEventSeenRef.current;
|
||||||
|
if (shouldSynthesizeNotifications && messageRole === 'assistant') {
|
||||||
|
const settings = useUIStore.getState();
|
||||||
|
const sessionInfo = useSessionStore.getState().sessions.find((entry) => entry.id === sessionId);
|
||||||
|
const sessionTitle = typeof sessionInfo?.title === 'string' ? sessionInfo.title.trim() : '';
|
||||||
|
|
||||||
|
if (eventHasStopFinish && settings.notifyOnCompletion !== false) {
|
||||||
|
const isSubtask = Boolean(sessionInfo?.parentID);
|
||||||
|
if (!(settings.notifyOnSubtasks === false && isSubtask)) {
|
||||||
|
const notificationKey = `ready:${sessionId}:${messageId}`;
|
||||||
|
if (!notifiedMessagesRef.current.has(notificationKey)) {
|
||||||
|
notifiedMessagesRef.current.add(notificationKey);
|
||||||
|
dispatchRuntimeNotification({
|
||||||
|
title: 'Agent is ready',
|
||||||
|
body: sessionTitle || 'Task completed',
|
||||||
|
tag: `ready-${sessionId}`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (eventHasErrorFinish && settings.notifyOnError !== false) {
|
||||||
|
const notificationKey = `error:${sessionId}:${messageId}`;
|
||||||
|
if (!notifiedMessagesRef.current.has(notificationKey)) {
|
||||||
|
notifiedMessagesRef.current.add(notificationKey);
|
||||||
|
dispatchRuntimeNotification({
|
||||||
|
title: 'Tool error',
|
||||||
|
body: sessionTitle || 'An error occurred',
|
||||||
|
tag: `error-${sessionId}`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (hasParts && (messageExt as { role?: unknown }).role !== 'user') {
|
if (hasParts && (messageExt as { role?: unknown }).role !== 'user') {
|
||||||
const storeState = useSessionStore.getState();
|
const storeState = useSessionStore.getState();
|
||||||
const existingMessages = storeState.messages.get(sessionId) || [];
|
const existingMessages = storeState.messages.get(sessionId) || [];
|
||||||
@@ -1445,6 +1528,25 @@ export const useEventStream = () => {
|
|||||||
|
|
||||||
addPermission(request);
|
addPermission(request);
|
||||||
|
|
||||||
|
const runtimeAPIs = getRegisteredRuntimeAPIs();
|
||||||
|
if (runtimeAPIs?.runtime?.isVSCode && !serverNotificationEventSeenRef.current) {
|
||||||
|
const settings = useUIStore.getState();
|
||||||
|
if (settings.notifyOnQuestion !== false) {
|
||||||
|
const notificationKey = `permission:${request.sessionID}:${request.id}`;
|
||||||
|
if (!notifiedQuestionsRef.current.has(notificationKey)) {
|
||||||
|
notifiedQuestionsRef.current.add(notificationKey);
|
||||||
|
const sessionTitle =
|
||||||
|
useSessionStore.getState().sessions.find((s) => s.id === request.sessionID)?.title ||
|
||||||
|
'Agent is waiting for your approval';
|
||||||
|
dispatchRuntimeNotification({
|
||||||
|
title: 'Permission required',
|
||||||
|
body: sessionTitle,
|
||||||
|
tag: `permission-${request.sessionID}:${request.id}`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Notify if permission is for another session (common with child sessions).
|
// Notify if permission is for another session (common with child sessions).
|
||||||
const toastKey = `${request.sessionID}:${request.id}`;
|
const toastKey = `${request.sessionID}:${request.id}`;
|
||||||
if (!permissionToastShownRef.current.has(toastKey)) {
|
if (!permissionToastShownRef.current.has(toastKey)) {
|
||||||
@@ -1515,9 +1617,28 @@ export const useEventStream = () => {
|
|||||||
const request = props as unknown as QuestionRequest;
|
const request = props as unknown as QuestionRequest;
|
||||||
addQuestion(request);
|
addQuestion(request);
|
||||||
|
|
||||||
|
const runtimeAPIs = getRegisteredRuntimeAPIs();
|
||||||
|
if (runtimeAPIs?.runtime?.isVSCode && !serverNotificationEventSeenRef.current) {
|
||||||
|
const settings = useUIStore.getState();
|
||||||
|
if (settings.notifyOnQuestion !== false) {
|
||||||
|
const notificationKey = `question:${request.sessionID}:${request.id}`;
|
||||||
|
if (!notifiedQuestionsRef.current.has(notificationKey)) {
|
||||||
|
notifiedQuestionsRef.current.add(notificationKey);
|
||||||
|
const firstQuestion = Array.isArray(request.questions) ? request.questions[0] : undefined;
|
||||||
|
const questionHeader = typeof firstQuestion?.header === 'string' ? firstQuestion.header.trim() : '';
|
||||||
|
const questionText = typeof firstQuestion?.question === 'string' ? firstQuestion.question.trim() : '';
|
||||||
|
dispatchRuntimeNotification({
|
||||||
|
title: questionHeader || 'Input needed',
|
||||||
|
body: questionText || 'Agent is waiting for your response',
|
||||||
|
tag: `question-${request.sessionID}:${request.id}`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const toastKey = `${request.sessionID}:${request.id}`;
|
const toastKey = `${request.sessionID}:${request.id}`;
|
||||||
|
|
||||||
// notifications are emitted server-side (see openchamber:notification)
|
// web/desktop use server-emitted notifications; VS Code may synthesize locally
|
||||||
|
|
||||||
if (!questionToastShownRef.current.has(toastKey)) {
|
if (!questionToastShownRef.current.has(toastKey)) {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
@@ -1586,15 +1707,12 @@ export const useEventStream = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
case 'openchamber:notification': {
|
case 'openchamber:notification': {
|
||||||
|
serverNotificationEventSeenRef.current = true;
|
||||||
const title = typeof (props as { title?: unknown }).title === 'string' ? (props as { title: string }).title : '';
|
const title = typeof (props as { title?: unknown }).title === 'string' ? (props as { title: string }).title : '';
|
||||||
const body = typeof (props as { body?: unknown }).body === 'string' ? (props as { body: string }).body : '';
|
const body = typeof (props as { body?: unknown }).body === 'string' ? (props as { body: string }).body : '';
|
||||||
const tag = typeof (props as { tag?: unknown }).tag === 'string' ? (props as { tag: string }).tag : undefined;
|
const tag = typeof (props as { tag?: unknown }).tag === 'string' ? (props as { tag: string }).tag : undefined;
|
||||||
const requireHidden = Boolean((props as { requireHidden?: unknown }).requireHidden);
|
const requireHidden = Boolean((props as { requireHidden?: unknown }).requireHidden);
|
||||||
|
|
||||||
if (requireHidden && visibilityStateRef.current !== 'hidden') {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
// When the sidecar stdout notification channel is active (production desktop builds),
|
// When the sidecar stdout notification channel is active (production desktop builds),
|
||||||
// skip this SSE notification to avoid duplicating the native notification already
|
// skip this SSE notification to avoid duplicating the native notification already
|
||||||
// shown by the Tauri process. In dev mode the stdout channel is not available,
|
// shown by the Tauri process. In dev mode the stdout channel is not available,
|
||||||
@@ -1603,14 +1721,7 @@ export const useEventStream = () => {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!nativeNotificationsEnabled) {
|
dispatchRuntimeNotification({ title, body, tag, requireHidden });
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
const runtimeAPIs = getRegisteredRuntimeAPIs();
|
|
||||||
if (runtimeAPIs?.notifications && title) {
|
|
||||||
void runtimeAPIs.notifications.notifyAgentCompletion({ title, body, tag });
|
|
||||||
}
|
|
||||||
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -1629,7 +1740,6 @@ export const useEventStream = () => {
|
|||||||
}
|
}
|
||||||
}, [
|
}, [
|
||||||
currentSessionId,
|
currentSessionId,
|
||||||
nativeNotificationsEnabled,
|
|
||||||
addStreamingPart,
|
addStreamingPart,
|
||||||
completeStreamingMessage,
|
completeStreamingMessage,
|
||||||
updateMessageInfo,
|
updateMessageInfo,
|
||||||
@@ -1650,6 +1760,7 @@ export const useEventStream = () => {
|
|||||||
bootstrapState,
|
bootstrapState,
|
||||||
effectiveDirectory,
|
effectiveDirectory,
|
||||||
updateSessionStatus,
|
updateSessionStatus,
|
||||||
|
dispatchRuntimeNotification,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// --- Stable callback refs (Part A) ---
|
// --- Stable callback refs (Part A) ---
|
||||||
@@ -2109,6 +2220,7 @@ export const useEventStream = () => {
|
|||||||
notifiedMessagesRef.current.clear();
|
notifiedMessagesRef.current.clear();
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps -- Intentionally accessing current ref value at cleanup time
|
// eslint-disable-next-line react-hooks/exhaustive-deps -- Intentionally accessing current ref value at cleanup time
|
||||||
notifiedQuestionsRef.current.clear();
|
notifiedQuestionsRef.current.clear();
|
||||||
|
serverNotificationEventSeenRef.current = false;
|
||||||
|
|
||||||
pendingResumeRef.current = false;
|
pendingResumeRef.current = false;
|
||||||
visibilityStateRef.current = resolveVisibilityState();
|
visibilityStateRef.current = resolveVisibilityState();
|
||||||
|
|||||||
@@ -79,6 +79,16 @@ type ApiProxyResponsePayload = {
|
|||||||
bodyBase64: string;
|
bodyBase64: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type NotificationBridgePayload = {
|
||||||
|
title?: string;
|
||||||
|
body?: string;
|
||||||
|
tag?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type NotificationsNotifyRequestPayload = {
|
||||||
|
payload?: NotificationBridgePayload;
|
||||||
|
};
|
||||||
|
|
||||||
interface FileEntry {
|
interface FileEntry {
|
||||||
name: string;
|
name: string;
|
||||||
path: string;
|
path: string;
|
||||||
@@ -2792,6 +2802,28 @@ export async function handleBridgeMessage(message: BridgeRequest, ctx?: BridgeCo
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
case 'notifications:can-notify': {
|
||||||
|
return { id, type, success: true, data: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'notifications:notify': {
|
||||||
|
const request = (payload || {}) as NotificationsNotifyRequestPayload;
|
||||||
|
const notification = request.payload || {};
|
||||||
|
const title = typeof notification.title === 'string' ? notification.title.trim() : '';
|
||||||
|
const body = typeof notification.body === 'string' ? notification.body.trim() : '';
|
||||||
|
|
||||||
|
const message = title && body
|
||||||
|
? `${title}: ${body}`
|
||||||
|
: title || body;
|
||||||
|
|
||||||
|
if (!message) {
|
||||||
|
return { id, type, success: true, data: { shown: false } };
|
||||||
|
}
|
||||||
|
|
||||||
|
void vscode.window.showInformationMessage(message);
|
||||||
|
return { id, type, success: true, data: { shown: true } };
|
||||||
|
}
|
||||||
|
|
||||||
// ============== Git Operations ==============
|
// ============== Git Operations ==============
|
||||||
|
|
||||||
case 'api:git/check': {
|
case 'api:git/check': {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import type { RuntimeAPIs, TerminalAPI, NotificationsAPI } from '@openchamber/ui/lib/api/types';
|
import type { RuntimeAPIs, TerminalAPI } from '@openchamber/ui/lib/api/types';
|
||||||
import { createVSCodeFilesAPI } from './files';
|
import { createVSCodeFilesAPI } from './files';
|
||||||
import { createVSCodeSettingsAPI } from './settings';
|
import { createVSCodeSettingsAPI } from './settings';
|
||||||
import { createVSCodePermissionsAPI } from './permissions';
|
import { createVSCodePermissionsAPI } from './permissions';
|
||||||
@@ -7,6 +7,7 @@ import { createVSCodeEditorAPI } from './editor';
|
|||||||
import { createVSCodeGitAPI } from './git';
|
import { createVSCodeGitAPI } from './git';
|
||||||
import { createVSCodeActionsAPI } from './vscode';
|
import { createVSCodeActionsAPI } from './vscode';
|
||||||
import { createVSCodeGitHubAPI } from './github';
|
import { createVSCodeGitHubAPI } from './github';
|
||||||
|
import { createVSCodeNotificationsAPI } from './notifications';
|
||||||
|
|
||||||
// Stub APIs return sensible defaults instead of throwing
|
// Stub APIs return sensible defaults instead of throwing
|
||||||
const createStubTerminalAPI = (): TerminalAPI => ({
|
const createStubTerminalAPI = (): TerminalAPI => ({
|
||||||
@@ -17,11 +18,6 @@ const createStubTerminalAPI = (): TerminalAPI => ({
|
|||||||
close: async () => {},
|
close: async () => {},
|
||||||
});
|
});
|
||||||
|
|
||||||
const createStubNotificationsAPI = (): NotificationsAPI => ({
|
|
||||||
notifyAgentCompletion: async () => true,
|
|
||||||
canNotify: () => true,
|
|
||||||
});
|
|
||||||
|
|
||||||
export const createVSCodeAPIs = (): RuntimeAPIs => ({
|
export const createVSCodeAPIs = (): RuntimeAPIs => ({
|
||||||
runtime: { platform: 'vscode', isDesktop: false, isVSCode: true, label: 'VS Code Extension' },
|
runtime: { platform: 'vscode', isDesktop: false, isVSCode: true, label: 'VS Code Extension' },
|
||||||
terminal: createStubTerminalAPI(),
|
terminal: createStubTerminalAPI(),
|
||||||
@@ -29,7 +25,7 @@ export const createVSCodeAPIs = (): RuntimeAPIs => ({
|
|||||||
files: createVSCodeFilesAPI(),
|
files: createVSCodeFilesAPI(),
|
||||||
settings: createVSCodeSettingsAPI(),
|
settings: createVSCodeSettingsAPI(),
|
||||||
permissions: createVSCodePermissionsAPI(),
|
permissions: createVSCodePermissionsAPI(),
|
||||||
notifications: createStubNotificationsAPI(),
|
notifications: createVSCodeNotificationsAPI(),
|
||||||
github: createVSCodeGitHubAPI(),
|
github: createVSCodeGitHubAPI(),
|
||||||
tools: createVSCodeToolsAPI(),
|
tools: createVSCodeToolsAPI(),
|
||||||
editor: createVSCodeEditorAPI(),
|
editor: createVSCodeEditorAPI(),
|
||||||
|
|||||||
@@ -0,0 +1,23 @@
|
|||||||
|
import type { NotificationPayload, NotificationsAPI } from '@openchamber/ui/lib/api/types';
|
||||||
|
import { sendBridgeMessage } from './bridge';
|
||||||
|
|
||||||
|
type NotifyResponse = { shown?: boolean };
|
||||||
|
|
||||||
|
export const createVSCodeNotificationsAPI = (): NotificationsAPI => ({
|
||||||
|
async notifyAgentCompletion(payload?: NotificationPayload): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
const response = await sendBridgeMessage<NotifyResponse>('notifications:notify', { payload });
|
||||||
|
return Boolean(response?.shown);
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async canNotify(): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
return await sendBridgeMessage<boolean>('notifications:can-notify');
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user