fix: restore vscode native notification behavior
Move VS Code/Cursor desktop notifications onto the webview Notification API instead of the extension-host watcher path, which could not reliably produce native OS notifications. Route OpenCode runtime events from the shared sync pipeline into the VS Code webview so completion, error, question, and permission notifications use the same live event stream as the UI. Respect the OpenChamber notification settings in VS Code, including template rendering, completion cooldowns, permission auto-accept suppression, and the notify-while-focused mode. Use VS Code's window focus signal from the extension host instead of document.hasFocus() inside the webview, so hidden-only notifications are suppressed while Cursor or VS Code is focused across platforms.
This commit is contained in:
@@ -137,6 +137,18 @@ export class AgentManagerPanelProvider {
|
||||
});
|
||||
}
|
||||
|
||||
public notifyWindowFocusChanged(focused: boolean): void {
|
||||
if (!this._panel) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._panel.webview.postMessage({
|
||||
type: 'command',
|
||||
command: 'windowFocusChanged',
|
||||
payload: { focused },
|
||||
});
|
||||
}
|
||||
|
||||
private _sendCachedState() {
|
||||
if (!this._panel) {
|
||||
return;
|
||||
@@ -147,6 +159,7 @@ export class AgentManagerPanelProvider {
|
||||
status: this._cachedStatus,
|
||||
error: this._cachedError,
|
||||
});
|
||||
this.notifyWindowFocusChanged(vscode.window.state.focused);
|
||||
}
|
||||
|
||||
private _buildSseHeaders(extra?: Record<string, string>): Record<string, string> {
|
||||
|
||||
@@ -275,6 +275,18 @@ export class ChatViewProvider implements vscode.WebviewViewProvider {
|
||||
});
|
||||
}
|
||||
|
||||
public notifyWindowFocusChanged(focused: boolean): void {
|
||||
if (!this._view) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._view.webview.postMessage({
|
||||
type: 'command',
|
||||
command: 'windowFocusChanged',
|
||||
payload: { focused },
|
||||
});
|
||||
}
|
||||
|
||||
// Message delivery confirmation
|
||||
private _confirmMessage(messageId: string) {
|
||||
this._pendingMessages.delete(messageId);
|
||||
@@ -354,6 +366,7 @@ export class ChatViewProvider implements vscode.WebviewViewProvider {
|
||||
status: this._cachedStatus,
|
||||
error: this._cachedError,
|
||||
});
|
||||
this.notifyWindowFocusChanged(vscode.window.state.focused);
|
||||
}
|
||||
|
||||
private _scheduleBroadcast(): void {
|
||||
|
||||
@@ -149,12 +149,27 @@ export class SessionEditorPanelProvider {
|
||||
}
|
||||
}
|
||||
|
||||
public notifyWindowFocusChanged(focused: boolean): void {
|
||||
for (const entry of this._panels.values()) {
|
||||
entry.panel.webview.postMessage({
|
||||
type: 'command',
|
||||
command: 'windowFocusChanged',
|
||||
payload: { focused },
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private _sendCachedStateToPanel(entry: SessionPanelState) {
|
||||
entry.panel.webview.postMessage({
|
||||
type: 'connectionStatus',
|
||||
status: this._cachedStatus,
|
||||
error: this._cachedError,
|
||||
});
|
||||
entry.panel.webview.postMessage({
|
||||
type: 'command',
|
||||
command: 'windowFocusChanged',
|
||||
payload: { focused: vscode.window.state.focused },
|
||||
});
|
||||
}
|
||||
|
||||
private _disposePanel(sessionId: string) {
|
||||
|
||||
@@ -197,6 +197,14 @@ export async function activate(context: vscode.ExtensionContext) {
|
||||
})
|
||||
);
|
||||
|
||||
context.subscriptions.push(
|
||||
vscode.window.onDidChangeWindowState((state) => {
|
||||
chatViewProvider?.notifyWindowFocusChanged(state.focused);
|
||||
sessionEditorProvider?.notifyWindowFocusChanged(state.focused);
|
||||
agentManagerProvider?.notifyWindowFocusChanged(state.focused);
|
||||
})
|
||||
);
|
||||
|
||||
context.subscriptions.push(
|
||||
vscode.commands.registerCommand('openchamber.openAgentManager', () => {
|
||||
agentManagerProvider?.createOrShow();
|
||||
|
||||
@@ -26,6 +26,19 @@ const clearGlobalEventWatcherRetry = (): void => {
|
||||
globalEventWatcherRetryTimer = null;
|
||||
};
|
||||
|
||||
const unwrapGlobalEventPayload = (eventData: unknown): Record<string, unknown> | null => {
|
||||
if (!eventData || typeof eventData !== 'object') {
|
||||
return null;
|
||||
}
|
||||
|
||||
const record = eventData as { payload?: unknown };
|
||||
if (record.payload && typeof record.payload === 'object') {
|
||||
return record.payload as Record<string, unknown>;
|
||||
}
|
||||
|
||||
return eventData as Record<string, unknown>;
|
||||
};
|
||||
|
||||
const reconcileSessionActivityFromStatus = async (manager: OpenCodeManager): Promise<void> => {
|
||||
const baseUrl = manager.getApiUrl();
|
||||
if (!baseUrl) {
|
||||
@@ -61,7 +74,6 @@ const reconcileSessionActivityFromStatus = async (manager: OpenCodeManager): Pro
|
||||
const setSessionActivityPhase = (sessionId: string, phase: ActivityPhase): void => {
|
||||
if (!sessionId) return;
|
||||
|
||||
// Cancel existing cooldown timer
|
||||
const existingTimer = sessionActivityCooldowns.get(sessionId);
|
||||
if (existingTimer) {
|
||||
clearTimeout(existingTimer);
|
||||
@@ -69,36 +81,30 @@ const setSessionActivityPhase = (sessionId: string, phase: ActivityPhase): void
|
||||
}
|
||||
|
||||
const current = sessionActivityPhases.get(sessionId);
|
||||
if (current?.phase === phase) return; // No change
|
||||
if (current?.phase === phase) return;
|
||||
|
||||
sessionActivityPhases.set(sessionId, { phase, updatedAt: Date.now() });
|
||||
|
||||
// Notify webview if available
|
||||
if (chatViewProvider) {
|
||||
chatViewProvider.postMessage({
|
||||
type: 'openchamber:session-activity',
|
||||
properties: {
|
||||
sessionId,
|
||||
phase,
|
||||
},
|
||||
});
|
||||
}
|
||||
chatViewProvider?.postMessage({
|
||||
type: 'openchamber:session-activity',
|
||||
properties: {
|
||||
sessionId,
|
||||
phase,
|
||||
},
|
||||
});
|
||||
|
||||
// Schedule transition from cooldown to idle
|
||||
if (phase === 'cooldown') {
|
||||
const timer = setTimeout(() => {
|
||||
const now = sessionActivityPhases.get(sessionId);
|
||||
if (now?.phase === 'cooldown') {
|
||||
sessionActivityPhases.set(sessionId, { phase: 'idle', updatedAt: Date.now() });
|
||||
if (chatViewProvider) {
|
||||
chatViewProvider.postMessage({
|
||||
type: 'openchamber:session-activity',
|
||||
properties: {
|
||||
sessionId,
|
||||
phase: 'idle',
|
||||
},
|
||||
});
|
||||
}
|
||||
chatViewProvider?.postMessage({
|
||||
type: 'openchamber:session-activity',
|
||||
properties: {
|
||||
sessionId,
|
||||
phase: 'idle',
|
||||
},
|
||||
});
|
||||
}
|
||||
sessionActivityCooldowns.delete(sessionId);
|
||||
}, SESSION_COOLDOWN_DURATION_MS);
|
||||
@@ -230,22 +236,19 @@ export const startGlobalEventWatcher = async (
|
||||
const result = await client.global.event({
|
||||
signal,
|
||||
sseMaxRetryAttempts: 0,
|
||||
onSseEvent: (event) => {
|
||||
const payload = event.data;
|
||||
if (!payload || typeof payload !== 'object') {
|
||||
return;
|
||||
}
|
||||
const activity = deriveSessionActivity(payload as Record<string, unknown>);
|
||||
if (activity) {
|
||||
setSessionActivityPhase(activity.sessionId, activity.phase);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
console.log('[VSCode:Activity] connected');
|
||||
|
||||
for await (const _ of result.stream) {
|
||||
void _;
|
||||
for await (const event of result.stream) {
|
||||
const payload = unwrapGlobalEventPayload((event as { payload?: unknown }).payload ?? event);
|
||||
if (payload) {
|
||||
const activity = deriveSessionActivity(payload);
|
||||
if (activity) {
|
||||
setSessionActivityPhase(activity.sessionId, activity.phase);
|
||||
}
|
||||
}
|
||||
|
||||
if (signal.aborted) {
|
||||
break;
|
||||
}
|
||||
@@ -279,7 +282,6 @@ export const stopGlobalEventWatcher = (): void => {
|
||||
globalEventWatcherAbortController = null;
|
||||
chatViewProvider = null;
|
||||
|
||||
// Clear all cooldown timers
|
||||
for (const timer of sessionActivityCooldowns.values()) {
|
||||
clearTimeout(timer);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user