From a4314c189b775a7ecc19ef33c958db0dc0bba8f4 Mon Sep 17 00:00:00 2001 From: Bohdan Triapitsyn Date: Sat, 30 May 2026 01:03:08 +0300 Subject: [PATCH] feat: streamline opencode loading screens and reuse the reload flow for vscode Restart API MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Loading screens: drop the internal-jargon progress text on the opencode reload overlay (ConfigUpdateOverlay) and the vscode init splash — show text only on errors. Replace it with a glow pulse on the OpenCode mark on the cube's top face; OpenChamberLogo's isAnimated prop was a no-op and now actually animates. vscode shows the glow on the inline splash logo and the React app stops writing 'Loading data (… Providers, … Agents)…'. Restart API: 'OpenChamber: Restart API Connection' now runs the same full reload flow used after an OpenCode update — the command asks the chat webview to call reloadOpenCodeConfiguration() (overlay + managed restart via the bridge + config/data refresh) instead of a bare manager restart, falling back to the old restart when no webview is open. Mounts ConfigUpdateOverlay in the vscode app so the overlay actually shows there. --- packages/ui/src/apps/VSCodeApp.tsx | 2 ++ .../src/components/ui/ConfigUpdateOverlay.tsx | 11 ++++---- .../ui/src/components/ui/OpenChamberLogo.tsx | 12 ++++++-- packages/vscode/src/ChatViewProvider.ts | 17 +++++++++++ packages/vscode/src/extension.ts | 7 +++++ packages/vscode/src/webviewHtml.ts | 28 +++++++++++-------- packages/vscode/webview/main.tsx | 17 ++++++++--- 7 files changed, 71 insertions(+), 23 deletions(-) diff --git a/packages/ui/src/apps/VSCodeApp.tsx b/packages/ui/src/apps/VSCodeApp.tsx index 16b7e74e..b3c74248 100644 --- a/packages/ui/src/apps/VSCodeApp.tsx +++ b/packages/ui/src/apps/VSCodeApp.tsx @@ -5,6 +5,7 @@ import { RuntimeAPIProvider } from '@/contexts/RuntimeAPIProvider'; import { registerRuntimeAPIs } from '@/contexts/runtimeAPIRegistry'; import { TooltipProvider } from '@/components/ui/tooltip'; import { Toaster } from '@/components/ui/sonner'; +import { ConfigUpdateOverlay } from '@/components/ui/ConfigUpdateOverlay'; import { ErrorBoundary } from '@/components/ui/ErrorBoundary'; import { VSCodeLayout } from '@/components/layout/VSCodeLayout'; import { usePushVisibilityBeacon } from '@/hooks/usePushVisibilityBeacon'; @@ -124,6 +125,7 @@ export function VSCodeApp({ apis }: VSCodeAppProps) { + diff --git a/packages/ui/src/components/ui/ConfigUpdateOverlay.tsx b/packages/ui/src/components/ui/ConfigUpdateOverlay.tsx index 1702237f..fe50ad0e 100644 --- a/packages/ui/src/components/ui/ConfigUpdateOverlay.tsx +++ b/packages/ui/src/components/ui/ConfigUpdateOverlay.tsx @@ -6,7 +6,7 @@ import { import { OpenChamberLogo } from "./OpenChamberLogo"; export const ConfigUpdateOverlay: React.FC = () => { - const [{ isUpdating, message }, setState] = React.useState(() => getConfigUpdateSnapshot()); + const [{ isUpdating }, setState] = React.useState(() => getConfigUpdateSnapshot()); React.useEffect(() => { return subscribeConfigUpdate(setState); @@ -16,12 +16,11 @@ export const ConfigUpdateOverlay: React.FC = () => { return null; } + // No status text — the update message is internal jargon and reads as noise. + // The animated logo alone signals "working". return ( -
- -

- {message} -

+
+
); }; diff --git a/packages/ui/src/components/ui/OpenChamberLogo.tsx b/packages/ui/src/components/ui/OpenChamberLogo.tsx index 89e6922b..34969e11 100644 --- a/packages/ui/src/components/ui/OpenChamberLogo.tsx +++ b/packages/ui/src/components/ui/OpenChamberLogo.tsx @@ -82,6 +82,7 @@ export const OpenChamberLogo: React.FC = ({ className = '', width = 70, height = 70, + isAnimated = false, }) => { const { t } = useI18n(); const themeContext = useOptionalThemeSystem(); @@ -193,6 +194,9 @@ export const OpenChamberLogo: React.FC = ({ role="img" aria-label={t('openChamberLogo.aria.logo')} > + {isAnimated ? ( + + ) : null} {/* Left face - base fill */} = ({ /> {/* OpenCode logo on top face */} - - {/* + + {/* Isometric transform for top face: OpenCode logo (32x40 viewBox) centered and projected to isometric plane */} diff --git a/packages/vscode/src/ChatViewProvider.ts b/packages/vscode/src/ChatViewProvider.ts index f112006f..9e99d2e7 100644 --- a/packages/vscode/src/ChatViewProvider.ts +++ b/packages/vscode/src/ChatViewProvider.ts @@ -306,6 +306,23 @@ export class ChatViewProvider implements vscode.WebviewViewProvider { }); } + /** + * Ask the webview to run the full OpenCode reload flow (overlay + managed + * restart via the bridge + config/data refresh) — the same flow used after an + * OpenCode update. Returns false if no webview is resolved to drive it. + */ + public reloadOpenCode(): boolean { + if (!this._view) { + return false; + } + + this._view.webview.postMessage({ + type: 'command', + command: 'reloadOpenCode', + }); + return true; + } + public notifyWindowFocusChanged(focused: boolean): void { if (!this._view) { return; diff --git a/packages/vscode/src/extension.ts b/packages/vscode/src/extension.ts index aa351e66..1b429779 100644 --- a/packages/vscode/src/extension.ts +++ b/packages/vscode/src/extension.ts @@ -262,6 +262,13 @@ export async function activate(context: vscode.ExtensionContext) { context.subscriptions.push( vscode.commands.registerCommand('openchamber.restartApi', async () => { try { + // Prefer the full in-app reload flow (overlay + managed restart via the + // bridge + config/data refresh) driven by the webview — same as after an + // OpenCode update. Fall back to a bare manager restart when no webview is + // open to drive it. + if (chatViewProvider?.reloadOpenCode()) { + return; + } await openCodeManager?.restart(); vscode.window.showInformationMessage('OpenChamber: API connection restarted'); } catch (e) { diff --git a/packages/vscode/src/webviewHtml.ts b/packages/vscode/src/webviewHtml.ts index 7b157b38..5e9f3b3f 100644 --- a/packages/vscode/src/webviewHtml.ts +++ b/packages/vscode/src/webviewHtml.ts @@ -107,6 +107,17 @@ export function getWebviewHtml(options: WebviewHtmlOptions): string { opacity: 0; pointer-events: none; } + /* Glow pulse on the OpenCode mark on the cube's top face — signals loading without text. */ + @keyframes oc-logo-glow { + 0%, 100% { filter: drop-shadow(0 0 0 transparent); } + 50% { filter: drop-shadow(0 0 4px var(--vscode-foreground)); } + } + #initial-loading .logo-inner { + animation: oc-logo-glow 1.8s ease-in-out infinite; + } + @media (prefers-reduced-motion: reduce) { + #initial-loading .logo-inner { animation: none; } + } /* Logo colors use VS Code foreground color */ #initial-loading .logo-stroke { stroke: var(--vscode-foreground); @@ -153,9 +164,8 @@ export function getWebviewHtml(options: WebviewHtmlOptions): string { -
- ${initialStatus === 'connecting' ? 'Starting OpenCode API…' : initialStatus === 'connected' ? 'Initializing…' : 'Connecting…'} -
+ +
${!cliAvailable ? `
OpenCode CLI not found. Please install it first.
` : ''}
@@ -184,17 +194,13 @@ export function getWebviewHtml(options: WebviewHtmlOptions): string { if (msg && msg.type === 'connectionStatus') { var statusEl = document.getElementById('loading-status'); if (statusEl) { - if (msg.status === 'connecting') { - statusEl.textContent = 'Starting OpenCode API…'; - statusEl.classList.remove('error-text'); - } else if (msg.status === 'connected') { - statusEl.textContent = 'Connected!'; - statusEl.classList.remove('error-text'); - } else if (msg.status === 'error') { + // Only show text when something is wrong — progress states stay silent + // (the animated logo already signals "working"). + if (msg.status === 'error') { statusEl.textContent = msg.error || 'Connection error'; statusEl.classList.add('error-text'); } else { - statusEl.textContent = 'Reconnecting…'; + statusEl.textContent = ''; statusEl.classList.remove('error-text'); } } diff --git a/packages/vscode/webview/main.tsx b/packages/vscode/webview/main.tsx index 263b0a32..26038643 100644 --- a/packages/vscode/webview/main.tsx +++ b/packages/vscode/webview/main.tsx @@ -181,9 +181,8 @@ const maybeHideLoadingOverlay = () => { return; } - const providersText = bootstrapProvidersReady ? '✓ Providers' : '… Providers'; - const agentsText = bootstrapAgentsReady ? '✓ Agents' : '… Agents'; - setLoadingStatusText(`Loading data (${providersText}, ${agentsText})…`); + // Still loading providers/agents — stay silent (the animated logo signals work). + setLoadingStatusText(''); return; } @@ -200,7 +199,8 @@ const maybeHideLoadingOverlay = () => { return; } - setLoadingStatusText('Starting OpenCode API…'); + // Connecting — no jargon; the animated logo conveys progress. + setLoadingStatusText(''); }; const applyInitialTheme = (theme: { metadata?: { variant?: string }; colors?: { surface?: { background?: string; foreground?: string } } }) => { @@ -1283,6 +1283,15 @@ onCommand('showSettings', () => { window.dispatchEvent(new CustomEvent('openchamber:navigate', { detail: { view: 'settings' } })); }); +// Run the same full OpenCode reload flow the app uses after an update: shows the +// reload overlay, restarts the managed OpenCode (via the bridge's /api/config/reload), +// and refreshes config/data. Triggered by the "Restart API Connection" command. +onCommand('reloadOpenCode', () => { + void import('@openchamber/ui/stores/useAgentsStore').then(({ reloadOpenCodeConfiguration }) => { + void reloadOpenCodeConfiguration(); + }); +}); + 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;