2025-12-13 16:34:17 +02:00
import { createVSCodeAPIs } from './api' ;
2026-02-11 19:28:22 +02:00
import { onCommand , onThemeChange , proxyApiRequest , proxySessionMessageRequest , sendBridgeMessage , startSseProxy , stopSseProxy } from './api/bridge' ;
2026-03-31 18:47:00 +03:00
import { vscodeStreamPerfCount , vscodeStreamPerfMeasure , vscodeStreamPerfObserve } from './api/streamPerf' ;
2026-01-06 21:31:04 +02:00
import type { RuntimeAPIs } from '@openchamber/ui/lib/api/types' ;
2025-12-13 16:34:17 +02:00
import {
buildVSCodeThemeFromPalette ,
readVSCodeThemePalette ,
type VSCodeThemeKind ,
type VSCodeThemePayload ,
2026-01-06 21:31:04 +02:00
} from '@openchamber/ui/lib/theme/vscode/adapter' ;
2026-05-05 00:24:03 +02:00
import type { VSCodeActiveEditorFile } from '@/sync/input-store' ;
2025-12-13 16:34:17 +02:00
type ConnectionStatus = 'connecting' | 'connected' | 'error' | 'disconnected' ;
2026-01-03 22:11:16 +01:00
type PanelType = 'chat' | 'agentManager' ;
2025-12-13 16:34:17 +02:00
2026-01-06 21:31:04 +02:00
declare const __OPENCHAMBER_WEBVIEW_BUILD_TIME__ : string ;
2025-12-13 16:34:17 +02:00
declare global {
interface Window {
__OPENCHAMBER_RUNTIME_APIS__? : RuntimeAPIs ;
__VSCODE_CONFIG__ ?: {
2025-12-24 22:50:46 +02:00
apiUrl? : string ;
2025-12-13 16:34:17 +02:00
workspaceFolder : string ;
theme : string ;
connectionStatus : string ;
2025-12-24 03:31:07 +02:00
cliAvailable? : boolean ;
2026-05-01 12:23:58 +03:00
extensionVersion? : string ;
platform? : string ;
arch? : string ;
2026-01-03 22:11:16 +01:00
panelType? : PanelType ;
2026-01-14 17:13:47 +02:00
viewMode ?: 'sidebar' | 'editor' ;
initialSessionId? : string | null ;
2025-12-13 16:34:17 +02:00
};
__OPENCHAMBER_VSCODE_THEME__? : VSCodeThemePayload [ 'theme' ];
2025-12-15 02:29:39 +02:00
__OPENCHAMBER_VSCODE_SHIKI_THEMES__ ?: { light? : Record < string , unknown >; dark? : Record < string , unknown > } | null ;
2025-12-24 03:31:07 +02:00
__OPENCHAMBER_CONNECTION__ ?: { status : ConnectionStatus ; error? : string ; cliAvailable? : boolean };
2025-12-20 02:44:46 +02:00
__OPENCHAMBER_HOME__? : string ;
2026-01-03 22:11:16 +01:00
__OPENCHAMBER_PANEL_TYPE__? : PanelType ;
2026-05-19 02:06:32 +03:00
__OPENCHAMBER_VSCODE_WINDOW_FOCUSED__? : boolean ;
2025-12-13 16:34:17 +02:00
}
}
console . log ( '[OpenChamber] VS Code webview starting...' );
2026-01-06 21:31:04 +02:00
console . log ( '[OpenChamber] VS Code webview build:' , __OPENCHAMBER_WEBVIEW_BUILD_TIME__ );
2025-12-13 16:34:17 +02:00
console . log ( '[OpenChamber] Config:' , window . __VSCODE_CONFIG__ );
2026-01-06 21:31:04 +02:00
try {
if ( window . localStorage . getItem ( 'openchamber_stream_debug' ) === '1' ) {
console . log ( '[OpenChamber] Debug: openchamber_stream_debug=1' );
}
} catch {
// ignore
}
2025-12-13 16:34:17 +02:00
window . __OPENCHAMBER_RUNTIME_APIS__ = createVSCodeAPIs ();
const bootstrapConnectionStatus = () => {
const initialStatus = ( window . __VSCODE_CONFIG__ ? . connectionStatus as ConnectionStatus | undefined ) || 'connecting' ;
2025-12-24 03:31:07 +02:00
const cliAvailable = window . __VSCODE_CONFIG__ ? . cliAvailable ?? true ;
window . __OPENCHAMBER_CONNECTION__ = { status : initialStatus , cliAvailable };
2025-12-13 16:34:17 +02:00
};
bootstrapConnectionStatus ();
2026-05-07 01:31:17 +03:00
// Expose panel type globally for the VS Code app root to conditionally render.
2026-01-03 22:11:16 +01:00
window . __OPENCHAMBER_PANEL_TYPE__ = ( window . __VSCODE_CONFIG__ ? . panelType as PanelType ) || 'chat' ;
2025-12-13 16:34:17 +02:00
const handleConnectionMessage = ( event : MessageEvent ) => {
const msg = event . data ;
if ( msg ? . type === 'connectionStatus' ) {
const payload : ConnectionStatus = msg . status ;
const error : string | undefined = msg . error ;
2025-12-24 03:31:07 +02:00
const prevCliAvailable = window . __OPENCHAMBER_CONNECTION__ ? . cliAvailable ?? true ;
window . __OPENCHAMBER_CONNECTION__ = { status : payload , error , cliAvailable : prevCliAvailable };
2025-12-13 16:34:17 +02:00
window . dispatchEvent ( new CustomEvent ( 'openchamber:connection-status' , { detail : { status : payload , error } }));
}
};
window . addEventListener ( 'message' , handleConnectionMessage );
2025-12-24 22:50:46 +02:00
window . addEventListener ( 'openchamber:connection-status' , () => {
maybeHideLoadingOverlay ();
});
const fadeOutLoadingScreen = () => {
const loadingEl = document . getElementById ( 'initial-loading' );
if ( ! loadingEl ) return ;
loadingEl . classList . add ( 'fade-out' );
setTimeout (() => {
try {
loadingEl . remove ();
} catch {
// ignore
}
}, 300 );
};
const setLoadingStatusText = ( text : string , variant : 'normal' | 'error' = 'normal' ) => {
const statusEl = document . getElementById ( 'loading-status' );
if ( ! statusEl ) return ;
statusEl . textContent = text ;
if ( variant === 'error' ) {
statusEl . classList . add ( 'error-text' );
} else {
statusEl . classList . remove ( 'error-text' );
}
};
const waitForUiMount = ( timeoutMs = 8000 ) : Promise < boolean > => {
if ( typeof document === 'undefined' ) return Promise . resolve ( false );
const root = document . getElementById ( 'root' );
if ( ! root ) return Promise . resolve ( false );
const hasContent = () => root . childNodes . length > 0 ;
if ( hasContent ()) return Promise . resolve ( true );
return new Promise (( resolve ) => {
const observer = new MutationObserver (() => {
if ( hasContent ()) {
observer . disconnect ();
clearTimeout ( timeout );
resolve ( true );
}
});
observer . observe ( root , { childList : true , subtree : true });
const timeout = setTimeout (() => {
observer . disconnect ();
resolve ( false );
}, timeoutMs );
});
};
let uiMounted = false ;
let bootstrapProvidersReady = false ;
let bootstrapAgentsReady = false ;
let bootstrapFailed = false ;
const recordBootstrapFetch = ( pathname : string , ok : boolean ) => {
if ( ! pathname . startsWith ( '/api/' )) return ;
2026-03-31 18:47:00 +03:00
// Don't mark as failed while still connecting — early 503s are expected
const isConnected = window . __OPENCHAMBER_CONNECTION__ ? . status === 'connected' ;
2025-12-24 22:50:46 +02:00
if ( pathname . startsWith ( '/api/config/providers' )) {
2026-03-31 18:47:00 +03:00
if ( ok ) {
bootstrapProvidersReady = true ;
// Reset failed flag — a successful retry supersedes earlier 503s
if ( bootstrapAgentsReady || ! isConnected ) bootstrapFailed = false ;
} else if ( isConnected ) {
bootstrapFailed = true ;
}
2025-12-24 22:50:46 +02:00
return ;
}
if ( pathname === '/api/agent' || pathname . startsWith ( '/api/agent?' )) {
2026-03-31 18:47:00 +03:00
if ( ok ) {
bootstrapAgentsReady = true ;
if ( bootstrapProvidersReady || ! isConnected ) bootstrapFailed = false ;
} else if ( isConnected ) {
bootstrapFailed = true ;
}
2025-12-24 22:50:46 +02:00
}
};
const maybeHideLoadingOverlay = () => {
const connectionStatus = window . __OPENCHAMBER_CONNECTION__ ? . status ?? 'connecting' ;
if ( ! uiMounted ) {
return ;
}
if ( connectionStatus === 'connected' ) {
if ( bootstrapFailed ) {
setLoadingStatusText ( 'OpenCode connected, but initial data load failed.' , 'error' );
fadeOutLoadingScreen ();
return ;
}
if ( bootstrapProvidersReady && bootstrapAgentsReady ) {
fadeOutLoadingScreen ();
return ;
}
const providersText = bootstrapProvidersReady ? '✓ Providers' : '… Providers' ;
const agentsText = bootstrapAgentsReady ? '✓ Agents' : '… Agents' ;
setLoadingStatusText ( `Loading data ( ${ providersText } , ${ agentsText } )…` );
return ;
}
if ( connectionStatus === 'error' ) {
const error = window . __OPENCHAMBER_CONNECTION__ ? . error ;
setLoadingStatusText ( error || 'Connection error' , 'error' );
fadeOutLoadingScreen ();
return ;
}
if ( connectionStatus === 'disconnected' ) {
setLoadingStatusText ( 'Disconnected' , 'error' );
fadeOutLoadingScreen ();
return ;
}
setLoadingStatusText ( 'Starting OpenCode API…' );
};
2025-12-13 16:34:17 +02:00
const applyInitialTheme = ( theme : { metadata ?: { variant? : string }; colors ?: { surface ?: { background? : string ; foreground? : string } } }) => {
if ( typeof document === 'undefined' || ! theme ) return ;
const variant = theme . metadata ? . variant === 'dark' ? 'dark' : 'light' ;
const root = document . documentElement ;
root . classList . remove ( 'light' , 'dark' );
root . classList . add ( variant );
const background = theme . colors ? . surface ? . background ;
if ( background ) {
document . body . style . backgroundColor = background ;
let meta = document . querySelector ( 'meta[name="theme-color"]' ) as HTMLMetaElement | null ;
if ( ! meta ) {
meta = document . createElement ( 'meta' );
meta . setAttribute ( 'name' , 'theme-color' );
document . head . appendChild ( meta );
}
meta . setAttribute ( 'content' , background );
}
};
const emitVSCodeTheme = ( preferredKind? : VSCodeThemeKind ) => {
const palette = readVSCodeThemePalette ( preferredKind );
if ( ! palette ) {
return ;
}
const theme = buildVSCodeThemeFromPalette ( palette );
window . __OPENCHAMBER_VSCODE_THEME__ = theme ;
applyInitialTheme ( theme );
window . dispatchEvent ( new CustomEvent < VSCodeThemePayload >( 'openchamber:vscode-theme' , {
detail : { theme , palette },
}));
};
emitVSCodeTheme ( window . __VSCODE_CONFIG__ ? . theme as VSCodeThemeKind | undefined );
2025-12-15 02:29:39 +02:00
const scheduleThemeRecompute = ( kind? : VSCodeThemeKind ) => {
// VS Code updates webview CSS variables asynchronously around theme changes.
// Re-read on the next frames so we don't snapshot the old palette.
requestAnimationFrame (() => {
emitVSCodeTheme ( kind );
requestAnimationFrame (() => emitVSCodeTheme ( kind ));
});
};
2025-12-13 16:34:17 +02:00
onThemeChange (( payload ) => {
const kind = ( typeof payload === 'string'
? payload
: typeof payload === 'object' && payload
? payload.kind
: undefined ) as VSCodeThemeKind | undefined ;
2025-12-15 02:29:39 +02:00
if ( typeof payload === 'object' && payload ? . shikiThemes !== undefined ) {
window . __OPENCHAMBER_VSCODE_SHIKI_THEMES__ = payload . shikiThemes ;
window . dispatchEvent (
new CustomEvent ( 'openchamber:vscode-shiki-themes' , {
detail : { shikiThemes : payload.shikiThemes },
}),
);
}
scheduleThemeRecompute ( kind );
2025-12-13 16:34:17 +02:00
});
const workspaceFolder = window . __VSCODE_CONFIG__ ? . workspaceFolder ;
if ( workspaceFolder ) {
2026-01-06 21:31:04 +02:00
const normalizeWorkspacePath = ( value : string ) => {
2026-03-20 18:58:13 +02:00
const normalized = value
. replace ( /\\/g , '/' )
. replace ( /^([a-z]):\// , ( _ , letter : string ) => ` ${ letter . toUpperCase () } :/` )
. replace ( /^\/([a-z]):\// , ( _ , letter : string ) => `/ ${ letter . toUpperCase () } :/` );
2026-01-06 21:31:04 +02:00
if ( normalized === '/' ) {
return '/' ;
}
return normalized . length > 1 ? normalized . replace ( /\/+$/ , '' ) : normalized ;
};
const normalizedWorkspaceFolder = normalizeWorkspacePath ( workspaceFolder );
window . __OPENCHAMBER_HOME__ = normalizedWorkspaceFolder ;
2025-12-13 16:34:17 +02:00
try {
2026-01-06 21:31:04 +02:00
window . localStorage . setItem ( 'lastDirectory' , normalizedWorkspaceFolder );
window . localStorage . setItem ( 'homeDirectory' , normalizedWorkspaceFolder );
2026-01-20 20:20:17 +07:00
// VS Code defaults: show dotfiles, hide gitignored
if ( window . localStorage . getItem ( 'directoryTreeShowHidden' ) === null ) {
window . localStorage . setItem ( 'directoryTreeShowHidden' , 'true' );
}
if ( window . localStorage . getItem ( 'filesViewShowGitignored' ) === null ) {
window . localStorage . setItem ( 'filesViewShowGitignored' , 'false' );
}
2025-12-13 16:34:17 +02:00
} catch ( error ) {
console . warn ( 'Failed to persist workspace folder' , error );
}
}
const normalizeUrl = ( input : string | URL ) => {
try {
2026-04-18 13:46:51 +03:00
return typeof input === 'string' ? new URL ( input , window . location . href ) : new URL ( input . toString (), window . location . href );
2025-12-13 16:34:17 +02:00
} catch {
return null ;
}
};
2025-12-24 22:50:46 +02:00
const headersToRecord = ( headers : HeadersInit | undefined ) : Record < string , string > => {
if ( ! headers ) return {};
const normalized = headers instanceof Headers ? headers : new Headers ( headers );
const result : Record < string , string > = {};
normalized . forEach (( value , key ) => {
result [ key ] = value ;
});
return result ;
};
const decodeBase64 = ( value : string ) : Uint8Array => {
const binary = atob ( value );
const bytes = new Uint8Array ( binary . length );
for ( let i = 0 ; i < binary . length ; i += 1 ) {
bytes [ i ] = binary . charCodeAt ( i );
}
return bytes ;
};
2026-02-17 18:01:57 +02:00
const isNullBodyStatus = ( status : number ) : boolean => status === 204 || status === 205 || status === 304 ;
const buildProxiedResponse = (
2026-05-21 15:45:15 +03:00
proxied : { status : number ; headers : Record < string , string >; bodyBase64? : string ; bodyText? : string }
2026-02-17 18:01:57 +02:00
) : Response => {
if ( isNullBodyStatus ( proxied . status )) {
return new Response ( null , { status : proxied.status , headers : proxied.headers });
}
2026-05-21 15:45:15 +03:00
if ( typeof proxied . bodyText === 'string' ) {
return new Response ( proxied . bodyText , { status : proxied.status , headers : proxied.headers });
}
2026-02-17 18:01:57 +02:00
const body = proxied . bodyBase64 ? decodeBase64 ( proxied . bodyBase64 ) : new Uint8Array ();
return new Response ( body , { status : proxied.status , headers : proxied.headers });
};
2025-12-24 22:50:46 +02:00
const encodeBase64 = ( bytes : Uint8Array ) : string => {
const CHUNK = 0x8000 ;
let binary = '' ;
for ( let i = 0 ; i < bytes . length ; i += CHUNK ) {
binary += String . fromCharCode (... bytes . subarray ( i , i + CHUNK ));
}
return btoa ( binary );
};
2025-12-24 03:31:07 +02:00
2025-12-24 22:50:46 +02:00
const extractBodyBase64 = async ( input : RequestInfo | URL , init : RequestInit | undefined , method : string ) : Promise < string | undefined > => {
if ( method === 'GET' || method === 'HEAD' ) return undefined ;
2025-12-24 03:31:07 +02:00
2025-12-24 22:50:46 +02:00
if ( input instanceof Request ) {
const cloned = input . clone ();
const buffer = await cloned . arrayBuffer ();
const bytes = new Uint8Array ( buffer );
return bytes . length > 0 ? encodeBase64 ( bytes ) : undefined ;
2025-12-24 03:31:07 +02:00
}
2025-12-24 22:50:46 +02:00
const body = init ? . body ;
if ( ! body ) return undefined ;
if ( typeof body === 'string' ) {
return encodeBase64 ( new TextEncoder (). encode ( body ));
}
if ( body instanceof URLSearchParams ) {
return encodeBase64 ( new TextEncoder (). encode ( body . toString ()));
}
if ( body instanceof Blob ) {
const buffer = await body . arrayBuffer ();
const bytes = new Uint8Array ( buffer );
return bytes . length > 0 ? encodeBase64 ( bytes ) : undefined ;
}
console . warn ( '[OpenChamber] Unsupported request body type for proxy request:' , body );
return undefined ;
2025-12-24 03:31:07 +02:00
};
2025-12-13 16:34:17 +02:00
2026-02-11 19:28:22 +02:00
const extractBodyText = async ( input : RequestInfo | URL , init : RequestInit | undefined , method : string ) : Promise < string > => {
if ( method === 'GET' || method === 'HEAD' ) return '' ;
if ( input instanceof Request ) {
const cloned = input . clone ();
return await cloned . text ();
}
const body = init ? . body ;
if ( ! body ) return '' ;
if ( typeof body === 'string' ) {
return body ;
}
if ( body instanceof URLSearchParams ) {
return body . toString ();
}
if ( body instanceof Blob ) {
return await body . text ();
}
console . warn ( '[OpenChamber] Unsupported request body type for direct session proxy:' , body );
return '' ;
};
2025-12-24 22:50:46 +02:00
const isSseApiPath = ( pathname : string ) => pathname === '/api/event' || pathname === '/api/global/event' ;
2026-02-11 19:28:22 +02:00
const isSessionMessageApiPath = ( pathname : string ) => /^\/api\/session\/[^/]+\/message$/ . test ( pathname );
2025-12-24 22:50:46 +02:00
2025-12-13 16:34:17 +02:00
const handleLocalApiRequest = async ( url : URL , init? : RequestInit ) => {
const pathname = url . pathname ;
2026-02-17 18:01:57 +02:00
const normalizedPathname = pathname !== '/' ? pathname . replace ( /\/+$/ , '' ) : pathname ;
const method = (( init ? . method || 'GET' ) as string ). toUpperCase ();
if ( normalizedPathname === '/api/sessions/snapshot' && method === 'GET' ) {
2026-03-31 18:47:00 +03:00
const activity = await sendBridgeMessage < Record < string , { type : 'idle' | 'busy' | 'cooldown' }>>( 'api:session-activity:get' )
. catch (() => ({}));
return new Response (
JSON . stringify ({
statusSessions : {},
attentionSessions : {},
activitySessions : activity || {},
serverTime : Date.now (),
}),
{
status : 200 ,
headers : { 'Content-Type' : 'application/json' },
},
);
}
if ( /^\/api\/sessions\/[^/]+\/(view|unview)$/ . test ( normalizedPathname ) && method === 'POST' ) {
return new Response ( JSON . stringify ({ success : true }), {
2026-02-17 18:01:57 +02:00
status : 200 ,
headers : { 'Content-Type' : 'application/json' },
});
}
2026-05-19 02:06:32 +03:00
if ( normalizedPathname === '/api/notifications/auto-accept' && method === 'POST' ) {
const bodyText = await extractBodyText ( url , init , method );
const body = bodyText
? JSON . parse ( bodyText ) as { sessionId? : unknown ; enabled? : unknown }
: {};
const result = await sendBridgeMessage < { success? : boolean } > ( 'api:notifications/auto-accept' , body )
. catch (() => ({ success : false }));
return new Response ( JSON . stringify ( result ), {
status : result?.success === false ? 400 : 200 ,
headers : { 'Content-Type' : 'application/json' },
});
}
2026-03-31 18:47:00 +03:00
if ( /^\/api\/sessions\/[^/]+\/message-sent$/ . test ( normalizedPathname ) && method === 'POST' ) {
const sessionId = normalizedPathname . split ( '/' )[ 3 ] || '' ;
return new Response (
JSON . stringify ({
success : true ,
sessionId ,
messageSent : true ,
}),
{
status : 200 ,
headers : { 'Content-Type' : 'application/json' },
},
);
}
if ( normalizedPathname === '/api/session-activity' && method === 'GET' ) {
const activity = await sendBridgeMessage < Record < string , { type : 'idle' | 'busy' | 'cooldown' }>>( 'api:session-activity:get' )
. catch (() => ({}));
return new Response ( JSON . stringify ( activity || {}), {
2026-02-17 18:01:57 +02:00
status : 200 ,
headers : { 'Content-Type' : 'application/json' },
});
}
2026-03-31 18:47:00 +03:00
if ( normalizedPathname === '/api/sessions/status' && method === 'GET' ) {
return new Response (
JSON . stringify ({
sessions : {},
serverTime : Date.now (),
}),
{
status : 200 ,
headers : { 'Content-Type' : 'application/json' },
},
);
}
if ( normalizedPathname === '/api/sessions/attention' && method === 'GET' ) {
return new Response (
JSON . stringify ({
sessions : {},
serverTime : Date.now (),
}),
{
status : 200 ,
headers : { 'Content-Type' : 'application/json' },
},
);
}
if ( /^\/api\/sessions\/[^/]+\/status$/ . test ( normalizedPathname ) && method === 'GET' ) {
const sessionId = normalizedPathname . split ( '/' )[ 3 ] || '' ;
return new Response (
JSON . stringify ({
error : 'Session not found or no state available' ,
sessionId ,
}),
{
status : 404 ,
headers : { 'Content-Type' : 'application/json' },
},
);
}
if ( /^\/api\/sessions\/[^/]+\/attention$/ . test ( normalizedPathname ) && method === 'GET' ) {
const sessionId = normalizedPathname . split ( '/' )[ 3 ] || '' ;
return new Response (
JSON . stringify ({
error : 'Session not found or no attention state available' ,
sessionId ,
}),
{
status : 404 ,
headers : { 'Content-Type' : 'application/json' },
},
);
}
2026-02-17 18:01:57 +02:00
if ( normalizedPathname === '/api/tts/status' && method === 'GET' ) {
return new Response ( JSON . stringify ({ available : false }), {
status : 200 ,
headers : { 'Content-Type' : 'application/json' },
});
}
if ( normalizedPathname === '/api/tts/say/status' && method === 'GET' ) {
return new Response ( JSON . stringify ({ available : false , voices : [] }), {
status : 200 ,
headers : { 'Content-Type' : 'application/json' },
});
}
2026-05-19 02:06:32 +03:00
if (( pathname === '/api/tts/speak' || pathname === '/api/tts/say/speak' ) && method === 'POST' ) {
2026-02-17 18:01:57 +02:00
return new Response ( JSON . stringify ({ error : 'TTS endpoints are not available in VS Code runtime' }), {
status : 501 ,
headers : { 'Content-Type' : 'application/json' },
});
}
2025-12-13 16:34:17 +02:00
2025-12-24 03:31:07 +02:00
// Health endpoints: reflect actual connection status
2025-12-13 16:34:17 +02:00
if ( pathname === '/health' || pathname === '/api/health' ) {
2025-12-24 03:31:07 +02:00
const connectionStatus = window . __OPENCHAMBER_CONNECTION__ ? . status ;
const isReady = connectionStatus === 'connected' ;
const cliAvailable = window . __OPENCHAMBER_CONNECTION__ ? . cliAvailable ?? true ;
return new Response ( JSON . stringify ({
status : isReady ? 'ok' : 'connecting' ,
isOpenCodeReady : isReady ,
cliAvailable ,
}), {
2025-12-13 16:34:17 +02:00
status : 200 ,
headers : { 'Content-Type' : 'application/json' },
});
}
if ( pathname . startsWith ( '/api/fs/list' )) {
const targetPath = url . searchParams . get ( 'path' ) || '' ;
2026-01-20 20:20:17 +07:00
const respectGitignore = url . searchParams . get ( 'respectGitignore' ) === 'true' ;
const data = await sendBridgeMessage ( 'api:fs:list' , { path : targetPath , respectGitignore });
2025-12-13 16:34:17 +02:00
return new Response ( JSON . stringify ( data ), { status : 200 , headers : { 'Content-Type' : 'application/json' } });
}
if ( pathname . startsWith ( '/api/fs/mkdir' )) {
const body = init ? . body ? JSON . parse ( init . body as string ) : {};
const data = await sendBridgeMessage ( 'api:fs:mkdir' , { path : body.path });
return new Response ( JSON . stringify ( data ), { status : 200 , headers : { 'Content-Type' : 'application/json' } });
}
if ( pathname . startsWith ( '/api/fs/home' )) {
const data = await sendBridgeMessage ( 'api:fs/home' );
return new Response ( JSON . stringify ( data ), { status : 200 , headers : { 'Content-Type' : 'application/json' } });
}
if ( pathname . startsWith ( '/api/vscode/pick-files' )) {
const data = await sendBridgeMessage ( 'api:files/pick' );
return new Response ( JSON . stringify ( data ), { status : 200 , headers : { 'Content-Type' : 'application/json' } });
}
2026-02-28 02:21:12 +08:00
if ( pathname . startsWith ( '/api/vscode/drop-files' ) && method === 'POST' ) {
const body = init ? . body ? JSON . parse ( init . body as string ) : {};
const uris = Array . isArray (( body as { uris? : unknown [] }). uris )
? ( body as { uris : unknown [] }). uris . filter (( value ) : value is string => typeof value === 'string' )
: [];
const data = await sendBridgeMessage ( 'api:files/drop' , { uris });
return new Response ( JSON . stringify ( data ), { status : 200 , headers : { 'Content-Type' : 'application/json' } });
}
2026-03-04 01:41:01 +02:00
if ( pathname . startsWith ( '/api/vscode/save-image' ) && method === 'POST' ) {
const body = init ? . body ? JSON . parse ( init . body as string ) : {};
const fileName = typeof ( body as { fileName? : unknown }). fileName === 'string'
? ( body as { fileName : string }). fileName
: undefined ;
const dataUrl = typeof ( body as { dataUrl? : unknown }). dataUrl === 'string'
? ( body as { dataUrl : string }). dataUrl
: undefined ;
const data = await sendBridgeMessage ( 'api:files/save-image' , { fileName , dataUrl });
return new Response ( JSON . stringify ( data ), { status : 200 , headers : { 'Content-Type' : 'application/json' } });
}
2026-04-17 18:46:13 +03:00
if ( pathname . startsWith ( '/api/vscode/save-markdown' ) && method === 'POST' ) {
const body = init ? . body ? JSON . parse ( init . body as string ) : {};
const fileName = typeof ( body as { fileName? : unknown }). fileName === 'string'
? ( body as { fileName : string }). fileName
: undefined ;
const content = typeof ( body as { content? : unknown }). content === 'string'
? ( body as { content : string }). content
: undefined ;
const data = await sendBridgeMessage ( 'api:files/save-markdown' , { fileName , content });
return new Response ( JSON . stringify ( data ), { status : 200 , headers : { 'Content-Type' : 'application/json' } });
}
2025-12-24 22:50:46 +02:00
if ( pathname . startsWith ( '/api/config/agents/' )) {
const encodedName = pathname . slice ( '/api/config/agents/' . length );
const name = decodeURIComponent ( encodedName );
const verb = (( init ? . method || 'GET' ) as string ). toUpperCase ();
const body = init ? . body ? JSON . parse ( init . body as string ) : {};
2026-01-06 21:31:04 +02:00
const queryDirectory = url . searchParams . get ( 'directory' ) || undefined ;
const headerDirectory = (() => {
const headers = init ? . headers ;
if ( ! headers ) return undefined ;
if ( headers instanceof Headers ) {
return headers . get ( 'x-opencode-directory' ) || undefined ;
}
if ( Array . isArray ( headers )) {
const found = headers . find (([ key ]) => key . toLowerCase () === 'x-opencode-directory' );
return found ? .[ 1 ] || undefined ;
}
if ( typeof headers === 'object' ) {
for ( const [ key , value ] of Object . entries ( headers )) {
if ( key . toLowerCase () === 'x-opencode-directory' && typeof value === 'string' ) {
return value ;
}
}
}
return undefined ;
})();
const directory = queryDirectory || headerDirectory ;
2025-12-24 22:50:46 +02:00
try {
2026-01-06 21:31:04 +02:00
const data = await sendBridgeMessage ( 'api:config/agents' , { method : verb , name , body , directory });
2025-12-24 22:50:46 +02:00
return new Response ( JSON . stringify ( data ), { status : 200 , headers : { 'Content-Type' : 'application/json' } });
} catch ( error ) {
const message = error instanceof Error ? error.message : String ( error );
return new Response ( JSON . stringify ({ error : message }), { status : 500 , headers : { 'Content-Type' : 'application/json' } });
}
}
if ( pathname . startsWith ( '/api/config/commands/' )) {
const encodedName = pathname . slice ( '/api/config/commands/' . length );
const name = decodeURIComponent ( encodedName );
const verb = (( init ? . method || 'GET' ) as string ). toUpperCase ();
const body = init ? . body ? JSON . parse ( init . body as string ) : {};
2026-01-06 21:31:04 +02:00
const queryDirectory = url . searchParams . get ( 'directory' ) || undefined ;
const headerDirectory = (() => {
const headers = init ? . headers ;
if ( ! headers ) return undefined ;
if ( headers instanceof Headers ) {
return headers . get ( 'x-opencode-directory' ) || undefined ;
}
if ( Array . isArray ( headers )) {
const found = headers . find (([ key ]) => key . toLowerCase () === 'x-opencode-directory' );
return found ? .[ 1 ] || undefined ;
}
if ( typeof headers === 'object' ) {
for ( const [ key , value ] of Object . entries ( headers )) {
if ( key . toLowerCase () === 'x-opencode-directory' && typeof value === 'string' ) {
return value ;
}
}
}
return undefined ;
})();
const directory = queryDirectory || headerDirectory ;
2025-12-24 22:50:46 +02:00
try {
2026-01-06 21:31:04 +02:00
const data = await sendBridgeMessage ( 'api:config/commands' , { method : verb , name , body , directory });
2025-12-24 22:50:46 +02:00
return new Response ( JSON . stringify ( data ), { status : 200 , headers : { 'Content-Type' : 'application/json' } });
} catch ( error ) {
const message = error instanceof Error ? error.message : String ( error );
2025-12-30 15:11:07 +02:00
return new Response ( JSON . stringify ({ error : message }), { status : 500 , headers : { 'Content-Type' : 'application/json' } });
}
}
2026-02-23 04:22:33 +07:00
if ( pathname === '/api/config/mcp' ) {
const verb = (( init ? . method || 'GET' ) as string ). toUpperCase ();
const body = init ? . body ? JSON . parse ( init . body as string ) : {};
const queryDirectory = url . searchParams . get ( 'directory' ) || undefined ;
const headerDirectory = (() => {
const headers = init ? . headers ;
if ( ! headers ) return undefined ;
if ( headers instanceof Headers ) {
return headers . get ( 'x-opencode-directory' ) || undefined ;
}
if ( Array . isArray ( headers )) {
const found = headers . find (([ key ]) => key . toLowerCase () === 'x-opencode-directory' );
return found ? .[ 1 ] || undefined ;
}
if ( typeof headers === 'object' ) {
for ( const [ key , value ] of Object . entries ( headers )) {
if ( key . toLowerCase () === 'x-opencode-directory' && typeof value === 'string' ) {
return value ;
}
}
}
return undefined ;
})();
const directory = queryDirectory || headerDirectory ;
try {
const data = await sendBridgeMessage ( 'api:config/mcp' , { method : verb , body , directory });
return new Response ( JSON . stringify ( data ), { status : 200 , headers : { 'Content-Type' : 'application/json' } });
} catch ( error ) {
const message = error instanceof Error ? error.message : String ( error );
return new Response ( JSON . stringify ({ error : message }), { status : 500 , headers : { 'Content-Type' : 'application/json' } });
}
}
if ( pathname . startsWith ( '/api/config/mcp/' )) {
const encodedName = pathname . slice ( '/api/config/mcp/' . length );
const name = decodeURIComponent ( encodedName );
const verb = (( init ? . method || 'GET' ) as string ). toUpperCase ();
const body = init ? . body ? JSON . parse ( init . body as string ) : {};
const queryDirectory = url . searchParams . get ( 'directory' ) || undefined ;
const headerDirectory = (() => {
const headers = init ? . headers ;
if ( ! headers ) return undefined ;
if ( headers instanceof Headers ) {
return headers . get ( 'x-opencode-directory' ) || undefined ;
}
if ( Array . isArray ( headers )) {
const found = headers . find (([ key ]) => key . toLowerCase () === 'x-opencode-directory' );
return found ? .[ 1 ] || undefined ;
}
if ( typeof headers === 'object' ) {
for ( const [ key , value ] of Object . entries ( headers )) {
if ( key . toLowerCase () === 'x-opencode-directory' && typeof value === 'string' ) {
return value ;
}
}
}
return undefined ;
})();
const directory = queryDirectory || headerDirectory ;
try {
const data = await sendBridgeMessage ( 'api:config/mcp' , { method : verb , name , body , directory });
return new Response ( JSON . stringify ( data ), { status : 200 , headers : { 'Content-Type' : 'application/json' } });
} catch ( error ) {
const message = error instanceof Error ? error.message : String ( error );
return new Response ( JSON . stringify ({ error : message }), { status : 500 , headers : { 'Content-Type' : 'application/json' } });
}
}
2025-12-30 15:11:07 +02:00
// Skills file operations: /api/config/skills/:name/files/:filePath
const skillsFilesMatch = pathname . match ( /^\/api\/config\/skills\/([^/]+)\/files\/(.+)$/ );
if ( skillsFilesMatch ) {
const name = decodeURIComponent ( skillsFilesMatch [ 1 ]);
const filePath = decodeURIComponent ( skillsFilesMatch [ 2 ]);
const verb = (( init ? . method || 'GET' ) as string ). toUpperCase ();
const body = init ? . body ? JSON . parse ( init . body as string ) : {};
try {
const data = await sendBridgeMessage ( 'api:config/skills/files' , {
method : verb ,
name ,
filePath ,
content : body.content
});
return new Response ( JSON . stringify ( data ), { status : 200 , headers : { 'Content-Type' : 'application/json' } });
} catch ( error ) {
const message = error instanceof Error ? error.message : String ( error );
return new Response ( JSON . stringify ({ error : message }), { status : 500 , headers : { 'Content-Type' : 'application/json' } });
}
}
2025-12-30 21:03:16 +02:00
const skillsCatalogStatusFromPayload = ( payload : unknown ) : number => {
if ( ! payload || typeof payload !== 'object' ) return 200 ;
const data = payload as { ok? : boolean ; error ?: { kind? : string } };
if ( data . ok === false ) {
const kind = data . error ? . kind ;
if ( kind === 'conflicts' ) return 409 ;
if ( kind === 'authRequired' ) return 401 ;
return 400 ;
}
return 200 ;
};
// Skills catalog: /api/config/skills/catalog
if ( pathname === '/api/config/skills/catalog' ) {
const refresh = url . searchParams . get ( 'refresh' ) === 'true' ;
try {
const data = await sendBridgeMessage ( 'api:config/skills:catalog' , { refresh });
return new Response ( JSON . stringify ( data ), { status : skillsCatalogStatusFromPayload ( data ), headers : { 'Content-Type' : 'application/json' } });
} catch ( error ) {
const message = error instanceof Error ? error.message : String ( error );
return new Response ( JSON . stringify ({ ok : false , error : { kind : 'unknown' , message } }), { status : 500 , headers : { 'Content-Type' : 'application/json' } });
}
}
// Skills scan: /api/config/skills/scan
if ( pathname === '/api/config/skills/scan' ) {
const body = init ? . body ? JSON . parse ( init . body as string ) : {};
try {
const data = await sendBridgeMessage ( 'api:config/skills:scan' , body );
return new Response ( JSON . stringify ( data ), { status : skillsCatalogStatusFromPayload ( data ), headers : { 'Content-Type' : 'application/json' } });
} catch ( error ) {
const message = error instanceof Error ? error.message : String ( error );
return new Response ( JSON . stringify ({ ok : false , error : { kind : 'unknown' , message } }), { status : 500 , headers : { 'Content-Type' : 'application/json' } });
}
}
// Skills install: /api/config/skills/install
if ( pathname === '/api/config/skills/install' ) {
const body = init ? . body ? JSON . parse ( init . body as string ) : {};
try {
const data = await sendBridgeMessage ( 'api:config/skills:install' , body );
return new Response ( JSON . stringify ( data ), { status : skillsCatalogStatusFromPayload ( data ), headers : { 'Content-Type' : 'application/json' } });
} catch ( error ) {
const message = error instanceof Error ? error.message : String ( error );
return new Response ( JSON . stringify ({ ok : false , error : { kind : 'unknown' , message } }), { status : 500 , headers : { 'Content-Type' : 'application/json' } });
}
}
2025-12-30 15:11:07 +02:00
// Skills CRUD: /api/config/skills/:name or /api/config/skills
if ( pathname === '/api/config/skills' ) {
try {
const data = await sendBridgeMessage ( 'api:config/skills' , { method : 'GET' });
return new Response ( JSON . stringify ( data ), { status : 200 , headers : { 'Content-Type' : 'application/json' } });
} catch ( error ) {
const message = error instanceof Error ? error.message : String ( error );
return new Response ( JSON . stringify ({ error : message }), { status : 500 , headers : { 'Content-Type' : 'application/json' } });
}
}
if ( pathname . startsWith ( '/api/config/skills/' )) {
const encodedName = pathname . slice ( '/api/config/skills/' . length );
const name = decodeURIComponent ( encodedName );
const verb = (( init ? . method || 'GET' ) as string ). toUpperCase ();
const body = init ? . body ? JSON . parse ( init . body as string ) : {};
try {
const data = await sendBridgeMessage ( 'api:config/skills' , { method : verb , name , body });
return new Response ( JSON . stringify ( data ), { status : 200 , headers : { 'Content-Type' : 'application/json' } });
} catch ( error ) {
const message = error instanceof Error ? error.message : String ( error );
2025-12-24 22:50:46 +02:00
return new Response ( JSON . stringify ({ error : message }), { status : 500 , headers : { 'Content-Type' : 'application/json' } });
}
}
2025-12-13 16:34:17 +02:00
if ( pathname . startsWith ( '/api/config/settings' )) {
if (( init ? . method || 'GET' ). toUpperCase () === 'GET' ) {
const settings = await sendBridgeMessage ( 'api:config/settings:get' );
return new Response ( JSON . stringify ( settings ), { status : 200 , headers : { 'Content-Type' : 'application/json' } });
}
const body = init ? . body ? JSON . parse ( init . body as string ) : {};
const updated = await sendBridgeMessage ( 'api:config/settings:save' , body );
return new Response ( JSON . stringify ( updated ), { status : 200 , headers : { 'Content-Type' : 'application/json' } });
}
2026-04-30 14:39:55 -07:00
if ( normalizedPathname === '/api/behavior/agents-md' ) {
if ( method === 'GET' ) {
const data = await sendBridgeMessage ( 'api:behavior/agents-md:get' );
return new Response ( JSON . stringify ( data ), { status : 200 , headers : { 'Content-Type' : 'application/json' } });
}
if ( method === 'PUT' ) {
const body = init ? . body ? JSON . parse ( init . body as string ) : {};
const data = await sendBridgeMessage ( 'api:behavior/agents-md:save' , body );
return new Response ( JSON . stringify ( data ), { status : 200 , headers : { 'Content-Type' : 'application/json' } });
}
}
2026-04-07 22:51:14 +03:00
if ( pathname === '/api/magic-prompts' ) {
if ( method === 'GET' ) {
const data = await sendBridgeMessage ( 'api:magic-prompts:get' );
return new Response ( JSON . stringify ( data ), { status : 200 , headers : { 'Content-Type' : 'application/json' } });
}
if ( method === 'DELETE' ) {
const data = await sendBridgeMessage ( 'api:magic-prompts:reset-all' );
return new Response ( JSON . stringify ( data ), { status : 200 , headers : { 'Content-Type' : 'application/json' } });
}
}
if ( pathname . startsWith ( '/api/magic-prompts/' )) {
const id = decodeURIComponent ( pathname . slice ( '/api/magic-prompts/' . length ));
if ( method === 'PUT' ) {
const body = init ? . body ? JSON . parse ( init . body as string ) : {};
const data = await sendBridgeMessage ( 'api:magic-prompts:save' , { id , text : body?.text });
return new Response ( JSON . stringify ( data ), { status : 200 , headers : { 'Content-Type' : 'application/json' } });
}
if ( method === 'DELETE' ) {
const data = await sendBridgeMessage ( 'api:magic-prompts:reset' , { id });
return new Response ( JSON . stringify ( data ), { status : 200 , headers : { 'Content-Type' : 'application/json' } });
}
}
2026-03-31 18:47:00 +03:00
if ( pathname === '/api/config/opencode-resolution' && method === 'GET' ) {
try {
const data = await sendBridgeMessage ( 'api:config/opencode-resolution:get' );
return new Response ( JSON . stringify ( data ), { status : 200 , headers : { 'Content-Type' : 'application/json' } });
} catch ( error ) {
const message = error instanceof Error ? error.message : String ( error );
return new Response ( JSON . stringify ({ error : message }), { status : 500 , headers : { 'Content-Type' : 'application/json' } });
}
}
2025-12-13 16:34:17 +02:00
if ( pathname . startsWith ( '/api/config/reload' )) {
await sendBridgeMessage ( 'api:config/reload' );
return new Response ( JSON . stringify ({ restarted : true }), { status : 200 , headers : { 'Content-Type' : 'application/json' } });
}
if ( pathname . startsWith ( '/api/openchamber/models-metadata' )) {
try {
const data = await sendBridgeMessage ( 'api:models/metadata' );
return new Response ( JSON . stringify ( data ), { status : 200 , headers : { 'Content-Type' : 'application/json' } });
} catch ( error ) {
console . warn ( '[OpenChamber] Failed to fetch models metadata via bridge, returning empty set:' , error );
return new Response ( JSON . stringify ({}), { status : 200 , headers : { 'Content-Type' : 'application/json' } });
}
}
2026-03-31 18:47:00 +03:00
if ( pathname === '/api/zen/models' && method === 'GET' ) {
try {
const data = await sendBridgeMessage ( 'api:zen:models' );
return new Response ( JSON . stringify ( data ), { status : 200 , headers : { 'Content-Type' : 'application/json' } });
} catch ( error ) {
const message = error instanceof Error ? error.message : String ( error );
return new Response ( JSON . stringify ({ error : message , models : [] }), { status : 502 , headers : { 'Content-Type' : 'application/json' } });
}
}
2026-03-15 23:14:37 +02:00
if ( pathname . startsWith ( '/api/openchamber/update-check' )) {
try {
const currentVersion = url . searchParams . get ( 'currentVersion' ) || undefined ;
const instanceMode = url . searchParams . get ( 'instanceMode' ) || 'local' ;
const deviceClass = url . searchParams . get ( 'deviceClass' ) || 'desktop' ;
2026-05-01 12:23:58 +03:00
const platform = url . searchParams . get ( 'platform' ) || window . __VSCODE_CONFIG__ ? . platform || undefined ;
const arch = url . searchParams . get ( 'arch' ) || window . __VSCODE_CONFIG__ ? . arch || undefined ;
2026-03-15 23:14:37 +02:00
const reportUsageRaw = ( url . searchParams . get ( 'reportUsage' ) || 'true' ). toLowerCase ();
const reportUsage = ! ( reportUsageRaw === 'false' || reportUsageRaw === '0' || reportUsageRaw === 'no' );
const data = await sendBridgeMessage ( 'api:openchamber:update-check' , {
currentVersion ,
instanceMode ,
deviceClass ,
2026-03-22 23:02:53 +02:00
platform ,
arch ,
2026-03-15 23:14:37 +02:00
reportUsage ,
});
return new Response ( JSON . stringify ( data ), { status : 200 , headers : { 'Content-Type' : 'application/json' } });
} catch ( error ) {
const message = error instanceof Error ? error.message : String ( error );
return new Response ( JSON . stringify ({ available : false , error : message }), { status : 502 , headers : { 'Content-Type' : 'application/json' } });
}
}
2025-12-13 16:34:17 +02:00
if ( pathname === '/auth/session' ) {
// VS Code host is trusted; mirror web server shape to keep UI logic happy
const body = {
authenticated : true ,
requireSetup : false ,
authenticatedAt : Date.now (),
};
return new Response ( JSON . stringify ( body ), { status : 200 , headers : { 'Content-Type' : 'application/json' } });
}
if ( pathname . startsWith ( '/api/opencode/directory' )) {
const body = init ? . body ? JSON . parse ( init . body as string ) : {};
const result = await sendBridgeMessage ( 'api:opencode/directory' , { path : body.path });
return new Response ( JSON . stringify ( result ), { status : 200 , headers : { 'Content-Type' : 'application/json' } });
}
2026-02-01 19:37:18 +02:00
if ( pathname === '/api/quota/providers' ) {
try {
const data = await sendBridgeMessage ( 'api:quota:providers' );
return new Response ( JSON . stringify ( data ), { status : 200 , headers : { 'Content-Type' : 'application/json' } });
} catch ( error ) {
const message = error instanceof Error ? error.message : String ( error );
return new Response ( JSON . stringify ({ error : message }), { status : 500 , headers : { 'Content-Type' : 'application/json' } });
}
}
const quotaMatch = pathname . match ( /^\/api\/quota\/([^/]+)$/ );
if ( quotaMatch && ( init ? . method || 'GET' ). toUpperCase () === 'GET' ) {
const providerId = decodeURIComponent ( quotaMatch [ 1 ]);
try {
const data = await sendBridgeMessage ( 'api:quota:get' , { providerId });
return new Response ( JSON . stringify ( data ), { status : 200 , headers : { 'Content-Type' : 'application/json' } });
} catch ( error ) {
const message = error instanceof Error ? error.message : String ( error );
return new Response ( JSON . stringify ({ error : message }), { status : 500 , headers : { 'Content-Type' : 'application/json' } });
}
}
2025-12-27 02:22:59 +02:00
// Handle provider auth deletion: DELETE /api/provider/:providerId/auth
const providerAuthMatch = pathname . match ( /^\/api\/provider\/([^/]+)\/auth$/ );
if ( providerAuthMatch && ( init ? . method || 'GET' ). toUpperCase () === 'DELETE' ) {
const providerId = decodeURIComponent ( providerAuthMatch [ 1 ]);
2026-01-22 13:45:30 +02:00
const scope = url . searchParams . get ( 'scope' ) || 'auth' ;
2026-03-31 18:47:00 +03:00
const queryDirectory = url . searchParams . get ( 'directory' ) || undefined ;
2025-12-27 02:22:59 +02:00
try {
2026-03-31 18:47:00 +03:00
const data = await sendBridgeMessage ( 'api:provider/auth:delete' , { providerId , scope , directory : queryDirectory });
2026-01-22 13:45:30 +02:00
return new Response ( JSON . stringify ( data ), { status : 200 , headers : { 'Content-Type' : 'application/json' } });
} catch ( error ) {
const message = error instanceof Error ? error.message : String ( error );
return new Response ( JSON . stringify ({ error : message }), { status : 500 , headers : { 'Content-Type' : 'application/json' } });
}
}
// Handle provider source lookup: GET /api/provider/:providerId/source
const providerSourceMatch = pathname . match ( /^\/api\/provider\/([^/]+)\/source$/ );
if ( providerSourceMatch && ( init ? . method || 'GET' ). toUpperCase () === 'GET' ) {
const providerId = decodeURIComponent ( providerSourceMatch [ 1 ]);
2026-03-31 18:47:00 +03:00
const queryDirectory = url . searchParams . get ( 'directory' ) || undefined ;
2026-01-22 13:45:30 +02:00
try {
2026-03-31 18:47:00 +03:00
const data = await sendBridgeMessage ( 'api:provider/source:get' , { providerId , directory : queryDirectory });
2025-12-27 02:22:59 +02:00
return new Response ( JSON . stringify ( data ), { status : 200 , headers : { 'Content-Type' : 'application/json' } });
} catch ( error ) {
const message = error instanceof Error ? error.message : String ( error );
return new Response ( JSON . stringify ({ error : message }), { status : 500 , headers : { 'Content-Type' : 'application/json' } });
}
}
2025-12-13 16:34:17 +02:00
return null ;
};
const originalFetch = window . fetch . bind ( window );
window . fetch = async ( input : RequestInfo | URL , init? : RequestInit ) => {
const targetUrl = typeof input === 'string' || input instanceof URL ? normalizeUrl ( input ) : normalizeUrl (( input as Request ). url );
const method = ( init ? . method || ( input instanceof Request ? input . method : 'GET' )). toUpperCase ();
const pathname = targetUrl ? . pathname || '' ;
const normalizedPathname = pathname . replace ( /\/+/ , '/' );
if ( targetUrl && normalizedPathname === '/health' ) {
2025-12-24 03:31:07 +02:00
const connectionStatus = window . __OPENCHAMBER_CONNECTION__ ? . status ;
const isReady = connectionStatus === 'connected' ;
const cliAvailable = window . __OPENCHAMBER_CONNECTION__ ? . cliAvailable ?? true ;
return new Response ( JSON . stringify ({
status : isReady ? 'ok' : 'connecting' ,
isOpenCodeReady : isReady ,
cliAvailable ,
}), {
2025-12-13 16:34:17 +02:00
status : 200 ,
headers : { 'Content-Type' : 'application/json' },
});
}
if ( targetUrl && targetUrl . pathname . startsWith ( '/api/' )) {
const localResponse = await handleLocalApiRequest ( targetUrl , init );
if ( localResponse ) {
2025-12-24 22:50:46 +02:00
recordBootstrapFetch ( targetUrl . pathname , localResponse . ok );
maybeHideLoadingOverlay ();
2025-12-13 16:34:17 +02:00
return localResponse ;
}
2025-12-24 22:50:46 +02:00
const suffixPath = ` ${ targetUrl . pathname . replace ( /^\/api/ , '' ) }${ targetUrl . search } ` ;
const headersFromRequest = input instanceof Request ? headersToRecord ( input . headers ) : {};
const headersFromInit = headersToRecord ( init ? . headers );
const headers = { ... headersFromRequest , ... headersFromInit };
if ( isSseApiPath ( targetUrl . pathname )) {
2026-03-31 18:47:00 +03:00
const start = await vscodeStreamPerfMeasure ( 'vscode.webview.sse_start_ms' , () => startSseProxy ({ path : suffixPath , headers }));
2025-12-24 22:50:46 +02:00
if ( ! start . streamId ) {
return new Response ( null , { status : start.status || 503 , headers : start.headers || {} });
}
const streamId = start . streamId ;
const signal = ( input instanceof Request ? input.signal : init?.signal ) as AbortSignal | undefined ;
const encoder = new TextEncoder ();
let unsubscribe : (() => void ) | null = null ;
const stream = new ReadableStream < Uint8Array >({
start ( controller ) {
const onMessage = ( event : MessageEvent ) => {
const msg = event . data as { type ?: string ; streamId? : string ; chunk? : string ; error? : string };
if ( ! msg || msg . streamId !== streamId ) return ;
if ( msg . type === 'api:sse:chunk' && typeof msg . chunk === 'string' ) {
2026-03-31 18:47:00 +03:00
vscodeStreamPerfCount ( 'vscode.webview.sse_chunk' );
vscodeStreamPerfObserve ( 'vscode.webview.sse_chunk_bytes' , msg . chunk . length );
2025-12-24 22:50:46 +02:00
controller . enqueue ( encoder . encode ( msg . chunk ));
return ;
}
if ( msg . type === 'api:sse:end' ) {
2026-03-31 18:47:00 +03:00
vscodeStreamPerfCount ( 'vscode.webview.sse_end' );
2025-12-24 22:50:46 +02:00
unsubscribe ? .();
unsubscribe = null ;
if ( typeof msg . error === 'string' && msg . error . length > 0 ) {
controller . error ( new Error ( msg . error ));
} else {
controller . close ();
}
void stopSseProxy ({ streamId }). catch (() => {});
}
};
window . addEventListener ( 'message' , onMessage );
unsubscribe = () => window . removeEventListener ( 'message' , onMessage );
if ( signal ) {
const onAbort = () => {
unsubscribe ? .();
unsubscribe = null ;
try {
controller . error ( new DOMException ( 'Aborted' , 'AbortError' ));
} catch {
controller . close ();
}
void stopSseProxy ({ streamId }). catch (() => {});
};
if ( signal . aborted ) {
onAbort ();
return ;
}
signal . addEventListener ( 'abort' , onAbort , { once : true });
}
},
cancel() {
unsubscribe ? .();
unsubscribe = null ;
void stopSseProxy ({ streamId }). catch (() => {});
},
});
return new Response ( stream , { status : start.status || 200 , headers : start.headers || { 'content-type' : 'text/event-stream' } });
2025-12-13 16:34:17 +02:00
}
2026-02-11 19:28:22 +02:00
if ( method === 'POST' && isSessionMessageApiPath ( targetUrl . pathname )) {
const bodyText = await extractBodyText ( input , init , method );
const proxied = await proxySessionMessageRequest ({ path : suffixPath , headers , bodyText });
2026-02-17 18:01:57 +02:00
const response = buildProxiedResponse ( proxied );
2026-02-11 19:28:22 +02:00
recordBootstrapFetch ( targetUrl . pathname , response . ok );
maybeHideLoadingOverlay ();
return response ;
}
2025-12-24 22:50:46 +02:00
const bodyBase64 = await extractBodyBase64 ( input , init , method );
const proxied = await proxyApiRequest ({ method , path : suffixPath , headers , bodyBase64 });
2026-02-17 18:01:57 +02:00
const response = buildProxiedResponse ( proxied );
2025-12-24 22:50:46 +02:00
recordBootstrapFetch ( targetUrl . pathname , response . ok );
maybeHideLoadingOverlay ();
return response ;
2025-12-13 16:34:17 +02:00
}
if ( targetUrl && targetUrl . hostname . includes ( 'models.dev' )) {
try {
const data = await sendBridgeMessage ( 'api:models/metadata' );
return new Response ( JSON . stringify ( data ), { status : 200 , headers : { 'Content-Type' : 'application/json' } });
} catch ( error ) {
console . warn ( '[OpenChamber] models.dev request failed via bridge, returning empty metadata:' , error );
return new Response ( JSON . stringify ({}), { status : 200 , headers : { 'Content-Type' : 'application/json' } });
}
}
return originalFetch ( input as RequestInfo , init );
};
2025-12-24 22:50:46 +02:00
2026-05-19 15:53:54 +03:00
onCommand ( 'addContextSelection' , ( payload ) => {
const { filePath , filename , text } = payload as { filePath? : unknown ; filename? : unknown ; text? : unknown };
if ( typeof filePath !== 'string' || typeof filename !== 'string' || typeof text !== 'string' ) {
return ;
}
const trimmedPath = filePath . trim ();
const trimmedFilename = filename . trim ();
if ( ! trimmedPath || ! trimmedFilename || ! text . trim ()) {
return ;
}
2026-03-31 18:47:00 +03:00
import ( '@/sync/input-store' ). then (({ useInputStore }) => {
2026-05-19 15:53:54 +03:00
const file = new File ([ new Blob ([ text ], { type : 'text/plain' })], trimmedFilename , { type : 'text/plain' });
void useInputStore . getState (). addVSCodeSelectionAttachment ( trimmedPath , file );
2025-12-30 16:32:27 +01:00
});
});
2026-03-23 23:51:55 +02:00
onCommand ( 'addFileMentions' , ( payload ) => {
const rawPaths = Array . isArray (( payload as { paths? : unknown [] }) ? . paths )
? ( payload as { paths : unknown [] }). paths
: [];
const paths = rawPaths
. filter (( entry ) : entry is string => typeof entry === 'string' )
. map (( entry ) => entry . trim ())
. filter (( entry ) => entry . length > 0 );
if ( paths . length === 0 ) {
return ;
}
const mentionText = paths . map (( relativePath ) => `@ ${ relativePath } ` ). join ( ' ' );
2026-03-31 18:47:00 +03:00
import ( '@/sync/input-store' ). then (({ useInputStore }) => {
useInputStore . getState (). setPendingInputText ( mentionText , 'append-inline' );
2026-03-23 23:51:55 +02:00
});
});
2026-05-19 15:53:54 +03:00
onCommand ( 'addFileAttachments' , ( payload ) => {
const rawFiles = Array . isArray (( payload as { files? : unknown [] }) ? . files )
? ( payload as { files : unknown [] }). files
: [];
const files = rawFiles
. map (( entry ) => {
const record = entry as { filePath? : unknown ; fileName? : unknown ; fileSize? : unknown };
const filePath = typeof record . filePath === 'string' ? record . filePath . trim () : '' ;
const fileName = typeof record . fileName === 'string' ? record . fileName . trim () : '' ;
const fileSize = typeof record . fileSize === 'number' && Number . isFinite ( record . fileSize ) ? record.fileSize : null ;
return filePath && fileName ? { filePath , fileName , fileSize } : null ;
})
. filter (( entry ) : entry is { filePath : string ; fileName : string ; fileSize : number | null } => entry !== null );
if ( files . length === 0 ) {
return ;
}
import ( '@/sync/input-store' ). then (({ useInputStore }) => {
const inputStore = useInputStore . getState ();
for ( const file of files ) {
inputStore . addVSCodeFileAttachment ( file . filePath , file . fileName , file . fileSize );
}
});
});
2025-12-30 16:32:27 +01:00
// Listen for createSessionWithPrompt command from extension (Explain, Improve Code)
onCommand ( 'createSessionWithPrompt' , ( payload ) => {
const { prompt } = payload as { prompt : string };
2026-03-31 18:47:00 +03:00
2025-12-30 16:32:27 +01:00
Promise . all ([
2026-03-31 18:47:00 +03:00
import ( '@/sync/session-ui-store' ),
2026-01-06 21:31:04 +02:00
import ( '@/stores/useConfigStore' ),
2026-03-31 18:47:00 +03:00
import ( '@/sync/input-store' ),
]). then (([{ useSessionUIStore }, { useConfigStore }, { useInputStore }]) => {
const sessionStore = useSessionUIStore . getState ();
2025-12-30 16:32:27 +01:00
const configStore = useConfigStore . getState ();
2026-03-31 18:47:00 +03:00
2025-12-30 16:32:27 +01:00
// Get current provider/model/agent configuration
const { currentProviderId , currentModelId , currentAgentName } = configStore ;
2026-03-31 18:47:00 +03:00
2025-12-30 16:32:27 +01:00
if ( currentProviderId && currentModelId ) {
2026-05-19 15:53:54 +03:00
if ( ! sessionStore . currentSessionId ) {
sessionStore . openNewSessionDraft ();
}
2025-12-30 16:32:27 +01:00
// Send the message - this will create the session from the draft and send
sessionStore . sendMessage (
prompt ,
currentProviderId ,
currentModelId ,
currentAgentName ?? undefined ,
undefined , // attachments
undefined , // agentMentionName
undefined // additionalParts
). catch (( error : unknown ) => {
console . error ( '[OpenChamber] Failed to send prompt:' , error );
});
} else {
// If no provider/model configured, just set the text and let user send manually
2026-03-31 18:47:00 +03:00
useInputStore . getState (). setPendingInputText ( prompt );
2025-12-30 16:32:27 +01:00
}
});
});
2025-12-31 16:16:03 +01:00
// Listen for newSession command from extension title bar button
onCommand ( 'newSession' , () => {
2026-03-31 18:47:00 +03:00
import ( '@/sync/session-ui-store' ). then (({ useSessionUIStore }) => {
useSessionUIStore . getState (). openNewSessionDraft ();
2025-12-31 16:16:03 +01:00
});
// Also dispatch event to navigate to chat view in VSCodeLayout
window . dispatchEvent ( new CustomEvent ( 'openchamber:navigate' , { detail : { view : 'chat' } }));
});
// Listen for showSettings command from extension title bar button
onCommand ( 'showSettings' , () => {
// Dispatch event to navigate to settings view in VSCodeLayout
window . dispatchEvent ( new CustomEvent ( 'openchamber:navigate' , { detail : { view : 'settings' } }));
});
2026-05-19 16:41:16 +03:00
const getNotificationClaimKey = ( payload : { title? : unknown ; body? : unknown ; sessionId? : unknown ; tag? : unknown } | undefined ) : string => {
const tag = typeof payload ? . tag === 'string' ? payload . tag . trim () : '' ;
if ( tag ) return tag ;
return [ payload ? . sessionId , payload ? . title , payload ? . body ]
. filter (( value ) : value is string => typeof value === 'string' && value . trim (). length > 0 )
. map (( value ) => value . trim ())
. join ( '|' );
};
const claimOpenChamberNotification = async ( payload : { title? : unknown ; body? : unknown ; sessionId? : unknown ; tag? : unknown } | undefined ) : Promise < boolean > => {
const key = getNotificationClaimKey ( payload );
if ( ! key ) return true ;
try {
const result = await sendBridgeMessage < { claimed? : boolean } > ( 'api:notifications:claim' , { key });
return result ? . claimed === true ;
} catch {
return true ;
}
};
const showOpenChamberNotification = ( payload : { title? : unknown ; body? : unknown ; sessionId? : unknown ; tag? : unknown ; requireHidden? : unknown } | undefined ) => {
2026-05-19 02:06:32 +03:00
if ( typeof Notification === 'undefined' ) {
return false ;
}
2026-05-19 16:41:16 +03:00
const show = async () => {
2026-05-19 02:06:32 +03:00
const isVSCodeWindowFocused = window . __OPENCHAMBER_VSCODE_WINDOW_FOCUSED__ ?? document . hasFocus ();
if ( payload ? . requireHidden === true && isVSCodeWindowFocused ) {
return false ;
}
if ( Notification . permission !== 'granted' ) {
return false ;
}
const title = typeof payload ? . title === 'string' && payload . title . trim (). length > 0
? payload . title . trim ()
: 'OpenChamber' ;
const body = typeof payload ? . body === 'string' ? payload . body : '' ;
const sessionId = typeof payload ? . sessionId === 'string' && payload . sessionId . trim (). length > 0
? payload . sessionId . trim ()
: '' ;
2026-05-19 16:41:16 +03:00
if ( ! await claimOpenChamberNotification ({ ... payload , title , body , sessionId })) {
return false ;
}
2026-05-19 02:06:32 +03:00
const notification = new Notification ( title , { body });
notification . onclick = () => {
if ( sessionId ) {
import ( '@/sync/session-ui-store' ). then (({ useSessionUIStore }) => {
useSessionUIStore . getState (). setCurrentSession ( sessionId );
});
}
window . dispatchEvent ( new CustomEvent ( 'openchamber:navigate' , { detail : { view : 'chat' } }));
};
return true ;
};
if ( Notification . permission === 'default' ) {
void Notification . requestPermission (). then (( permission ) => {
if ( permission === 'granted' ) {
2026-05-19 16:41:16 +03:00
void show ();
2026-05-19 02:06:32 +03:00
}
});
return true ;
}
2026-05-19 16:41:16 +03:00
void show ();
return true ;
2026-05-19 02:06:32 +03:00
};
onCommand ( 'showNotification' , ( payload ) => {
showOpenChamberNotification ( payload as { title? : unknown ; body? : unknown ; sessionId? : unknown ; requireHidden? : unknown } | undefined );
});
onCommand ( 'windowFocusChanged' , ( payload ) => {
if ( typeof payload === 'object' && payload && typeof ( payload as { focused? : unknown }). focused === 'boolean' ) {
window . __OPENCHAMBER_VSCODE_WINDOW_FOCUSED__ = ( payload as { focused : boolean }). focused ;
}
});
const readyNotificationCooldowns = new Map < string , number >();
const READY_NOTIFICATION_COOLDOWN_MS = 5000 ;
const DEFAULT_NOTIFICATION_MESSAGE_MAX_LENGTH = 250 ;
let notificationSettingsSyncPromise : Promise < void > | null = null ;
const getPayloadString = ( value : unknown ) : string => typeof value === 'string' ? value . trim () : '' ;
const normalizeNotificationPlainText = ( text : string ) : string => text
. replace ( /```[\s\S]*?```/g , ' ' )
. replace ( /`([^`]*)`/g , '$1' )
. replace ( /^[\t ]*[-*+]\s+/gm , '' )
. replace ( /^#{1,6}\s+/gm , '' )
. replace ( /\*\*(.*?)\*\*/g , '$1' )
. replace ( /__(.*?)__/g , '$1' )
. replace ( /\*(.*?)\*/g , '$1' )
. replace ( /_(.*?)_/g , '$1' )
. replace ( /\[(.*?)\]\((.*?)\)/g , '$1' )
. replace ( /\s*\n\s*/g , ' ' )
. replace ( /\s+/g , ' ' )
. trim ();
const truncateNotificationText = ( text : string , maxLength : number ) : string => (
text . length <= maxLength ? text : ` ${ text . slice ( 0 , maxLength ) } ...`
);
const resolvePositiveNotificationNumber = ( value : unknown , fallback : number ) : number => (
typeof value === 'number' && Number . isFinite ( value ) && value > 0 ? value : fallback
);
const ensureNotificationSettingsSynced = async () => {
if ( ! notificationSettingsSyncPromise ) {
notificationSettingsSyncPromise = import ( '@/lib/persistence' )
. then (({ syncDesktopSettings }) => syncDesktopSettings ())
. catch (( error ) => {
console . warn ( '[OpenChamber] Failed to sync notification settings:' , error );
});
}
await notificationSettingsSyncPromise ;
};
const prepareNotificationLastMessage = (
message : string ,
settings : { maxLastMessageLength : number },
) : string => {
const maxLength = resolvePositiveNotificationNumber ( settings . maxLastMessageLength , DEFAULT_NOTIFICATION_MESSAGE_MAX_LENGTH );
return truncateNotificationText ( normalizeNotificationPlainText ( message ), maxLength );
};
const resolveTemplate = ( template : string , variables : Record < string , string >) : string => (
template . replace ( /\{(\w+)\}/g , ( _match , key : string ) => variables [ key ] ?? '' )
);
const shouldApplyTemplateMessage = ( template : string , resolved : string , variables : Record < string , string >) => {
if ( ! resolved ) return false ;
if ( template . includes ( '{last_message}' )) {
return variables . last_message . trim (). length > 0 ;
}
return true ;
};
const formatNotificationLabel = ( raw : string , fallback : string ) : string => {
if ( ! raw ) return fallback ;
return raw . split ( /[-_\s]+/ ). filter ( Boolean ). map (( part ) => part . charAt ( 0 ). toUpperCase () + part . slice ( 1 )). join ( ' ' );
};
const extractNotificationTextFromParts = ( parts : unknown ) : string => {
if ( ! Array . isArray ( parts )) return '' ;
return parts
. map (( part ) => {
if ( ! part || typeof part !== 'object' ) return '' ;
const entry = part as { type ?: unknown ; text? : unknown ; content? : unknown };
if ( entry . type === 'text' || typeof entry . text === 'string' || typeof entry . content === 'string' ) {
return typeof entry . text === 'string' ? entry.text : typeof entry . content === 'string' ? entry . content : '' ;
}
return '' ;
})
. filter ( Boolean )
. join ( '\n' )
. trim ();
};
const extractNotificationLastMessage = ( payload : Record < string , unknown >) : string => {
const properties = ( payload . properties ?? payload ) as Record < string , unknown >;
const info = properties . info as Record < string , unknown > | undefined ;
if ( ! info ) return '' ;
return extractNotificationTextFromParts ( info . parts ?? properties . parts ) || extractNotificationTextFromParts ( info . content );
};
const fetchLastAssistantMessageText = async ( sessionId : string , messageId? : string ) : Promise < string > => {
if ( ! sessionId ) return '' ;
try {
const response = await fetch ( `/api/session/ ${ encodeURIComponent ( sessionId ) } /message?limit=5` , {
method : 'GET' ,
headers : { Accept : 'application/json' },
signal : AbortSignal.timeout ( 3000 ),
});
if ( ! response . ok ) return '' ;
const messages = await response . json (). catch (() => null ) as unknown ;
if ( ! Array . isArray ( messages )) return '' ;
let target = messageId
? messages . find (( message ) => {
const info = message && typeof message === 'object'
? ( message as { info ?: { id? : unknown ; role? : unknown } }). info
: undefined ;
return info ? . id === messageId && info ? . role === 'assistant' ;
})
: null ;
if ( ! target ) {
for ( let index = messages . length - 1 ; index >= 0 ; index -= 1 ) {
const message = messages [ index ];
const info = message && typeof message === 'object'
? ( message as { info ?: { role? : unknown ; finish? : unknown } }). info
: undefined ;
if ( info ? . role === 'assistant' && info ? . finish === 'stop' ) {
target = message ;
break ;
}
}
}
if ( ! target || typeof target !== 'object' ) return '' ;
const message = target as { parts? : unknown ; content? : unknown ; info ?: { parts? : unknown ; content? : unknown } };
return extractNotificationTextFromParts ( message . parts ?? message . info ? . parts )
|| extractNotificationTextFromParts ( message . content ?? message . info ? . content );
} catch {
return '' ;
}
};
const getNotificationTemplate = (
settings : { notificationTemplates? : Record < string , { title ?: string ; message ?: string }> },
key : 'completion' | 'error' | 'question' ,
fallback : { title : string ; message : string },
) => {
const candidate = settings . notificationTemplates ? .[ key ];
return {
title : typeof candidate ? . title === 'string' ? candidate.title : fallback.title ,
message : typeof candidate ? . message === 'string' ? candidate.message : fallback.message ,
};
};
const buildNotificationVariables = ( payload : Record < string , unknown >, sessionId : string , lastMessage : string ) : Record < string , string > => {
const properties = ( payload . properties ?? payload ) as Record < string , unknown >;
const info = properties . info as Record < string , unknown > | undefined ;
const pathInfo = info ? . path as { root? : unknown ; cwd? : unknown } | undefined ;
const worktree = getPayloadString ( pathInfo ? . root ?? pathInfo ? . cwd );
const modelId = getPayloadString ( info ? . modelID ?? info ? . modelId ?? ( info ? . model as { modelID? : unknown } | undefined ) ? . modelID );
return {
project_name : worktree.split ( /[\\/]/ ). filter ( Boolean ). pop () || '' ,
worktree ,
branch : '' ,
session_name : getPayloadString ( properties . sessionTitle ?? ( properties . session as { title? : unknown } | undefined ) ? . title ?? info ? . sessionTitle ),
agent_name : formatNotificationLabel ( getPayloadString ( info ? . agent ?? info ? . mode ), 'Agent' ),
model_name : formatNotificationLabel ( modelId , 'Assistant' ),
last_message : lastMessage ,
session_id : sessionId ,
};
};
const getNotificationSessionId = ( payload : Record < string , unknown >) : string => {
const properties = ( payload . properties ?? payload ) as Record < string , unknown >;
const info = properties . info as Record < string , unknown > | undefined ;
return getPayloadString ( info ? . sessionID ?? info ? . sessionId ?? properties . sessionID ?? properties . sessionId ?? properties . session );
};
window . addEventListener ( 'openchamber:vscode-notification-event' , ( event ) => {
const detail = ( event as CustomEvent < { payload? : unknown } > ). detail ;
const payload = detail ? . payload ;
if ( ! payload || typeof payload !== 'object' ) {
return ;
}
const record = payload as Record < string , unknown >;
const type = getPayloadString ( record . type );
const properties = ( record . properties ?? record ) as Record < string , unknown >;
const info = properties . info as Record < string , unknown > | undefined ;
const sessionId = getNotificationSessionId ( record );
if ( ! sessionId ) {
return ;
}
Promise . all ([
import ( '@/stores/useUIStore' ),
import ( '@/stores/permissionStore' ),
]). then ( async ([{ useUIStore }, { usePermissionStore }]) => {
const localSettings = useUIStore . getState ();
await ensureNotificationSettingsSynced ();
const syncedSettings = useUIStore . getState ();
const settings = {
... syncedSettings ,
nativeNotificationsEnabled : localSettings.nativeNotificationsEnabled ,
notificationMode : localSettings.notificationMode ,
notifyOnCompletion : localSettings.notifyOnCompletion ,
notifyOnError : localSettings.notifyOnError ,
notifyOnQuestion : localSettings.notifyOnQuestion ,
notificationTemplates : localSettings.notificationTemplates ,
summarizeLastMessage : localSettings.summarizeLastMessage ,
summaryThreshold : localSettings.summaryThreshold ,
summaryLength : localSettings.summaryLength ,
maxLastMessageLength : localSettings.maxLastMessageLength ,
};
if ( ! settings . nativeNotificationsEnabled ) {
return ;
}
const requireHidden = settings . notificationMode !== 'always' ;
const messageId = getPayloadString ( info ? . id );
const rawLastMessage = extractNotificationLastMessage ( record ) || await fetchLastAssistantMessageText ( sessionId , messageId );
const lastMessage = prepareNotificationLastMessage (
rawLastMessage ,
settings ,
);
const variables = buildNotificationVariables ( record , sessionId , lastMessage );
if ( type === 'message.updated' && getPayloadString ( info ? . role ) === 'assistant' ) {
const finish = getPayloadString ( info ? . finish );
if ( finish === 'stop' ) {
if ( ! settings . notifyOnCompletion ) return ;
const now = Date . now ();
const lastAt = readyNotificationCooldowns . get ( sessionId ) ?? 0 ;
if ( now - lastAt < READY_NOTIFICATION_COOLDOWN_MS ) return ;
readyNotificationCooldowns . set ( sessionId , now );
const template = getNotificationTemplate ( settings , 'completion' , { title : '{agent_name} is ready' , message : '{model_name} completed the task' });
const title = resolveTemplate ( template . title , variables ) || 'Agent is ready' ;
const body = resolveTemplate ( template . message , variables );
showOpenChamberNotification ({
title ,
body : shouldApplyTemplateMessage ( template . message , body , variables ) ? body : ` ${ variables . model_name } completed the task` ,
sessionId ,
requireHidden ,
});
return ;
}
if ( finish === 'error' ) {
if ( ! settings . notifyOnError ) return ;
const template = getNotificationTemplate ( settings , 'error' , { title : 'Tool error' , message : '{last_message}' });
const title = resolveTemplate ( template . title , variables ) || 'Tool error' ;
const body = resolveTemplate ( template . message , variables );
showOpenChamberNotification ({
title ,
body : shouldApplyTemplateMessage ( template . message , body , variables ) ? body : 'An error occurred' ,
sessionId ,
requireHidden ,
});
}
}
if ( type === 'question.asked' ) {
if ( ! settings . notifyOnQuestion ) return ;
const questions = Array . isArray ( properties . questions ) ? properties . questions : [];
const firstQuestion = questions [ 0 ] as Record < string , unknown > | undefined ;
const header = getPayloadString ( firstQuestion ? . header );
const questionText = getPayloadString ( firstQuestion ? . question );
const questionVariables = { ... variables , last_message : questionText || header };
const template = getNotificationTemplate ( settings , 'question' , { title : 'Input needed' , message : '{last_message}' });
const title = resolveTemplate ( template . title , questionVariables ) || ( /plan\s*mode/i . test ( header ) ? 'Switch to plan mode' : /build\s*agent/i . test ( header ) ? 'Switch to build mode' : header || 'Input needed' );
const body = resolveTemplate ( template . message , questionVariables );
showOpenChamberNotification ({
title ,
body : shouldApplyTemplateMessage ( template . message , body , questionVariables ) ? body : questionText || 'Agent is waiting for your response' ,
sessionId ,
requireHidden ,
});
return ;
}
if ( type === 'permission.asked' ) {
if ( ! settings . notifyOnQuestion ) return ;
if ( usePermissionStore . getState (). isSessionAutoAccepting ( sessionId )) return ;
const permission = getPayloadString ( properties . permission );
const sessionTitle = getPayloadString ( properties . sessionTitle );
const fallbackMessage = sessionTitle || permission || 'Agent is waiting for your approval' ;
const permissionVariables = { ... variables , last_message : fallbackMessage };
const template = getNotificationTemplate ( settings , 'question' , { title : 'Permission required' , message : '{last_message}' });
const title = resolveTemplate ( template . title , permissionVariables ) || 'Permission required' ;
const body = resolveTemplate ( template . message , permissionVariables );
showOpenChamberNotification ({
title ,
body : shouldApplyTemplateMessage ( template . message , body , permissionVariables ) ? body : fallbackMessage ,
sessionId ,
requireHidden ,
});
}
});
});
2026-03-12 23:45:45 +02:00
// Listen for settings sync command from extension (broadcast to all VS Code webviews)
onCommand ( 'settingsSynced' , () => {
import ( '@openchamber/ui/lib/persistence' ). then (({ syncDesktopSettings }) => {
void syncDesktopSettings ();
});
});
2026-05-05 00:24:03 +02:00
// Listen for active editor file changes from the extension
onCommand ( 'activeEditorFile' , ( payload ) => {
import ( '@/sync/input-store' ). then (({ useInputStore }) => {
useInputStore . getState (). setActiveEditorFile (( payload as VSCodeActiveEditorFile | null ) ?? null );
});
});
2026-05-07 01:31:17 +03:00
import ( '@openchamber/ui/apps/renderVSCodeApp' )
. then ( async ({ renderVSCodeApp }) => {
renderVSCodeApp ( window . __OPENCHAMBER_RUNTIME_APIS__ ?? createVSCodeAPIs ());
2025-12-24 22:50:46 +02:00
await waitForUiMount ();
uiMounted = true ;
maybeHideLoadingOverlay ();
})
. catch (( error ) => {
console . error ( '[OpenChamber] Failed to bootstrap UI:' , error );
// If the UI bundle fails to load, remove the overlay so the user at least sees errors in the root.
uiMounted = true ;
fadeOutLoadingScreen ();
});