feat: streamline opencode loading screens and reuse the reload flow for vscode Restart API
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.
This commit is contained in:
@@ -5,6 +5,7 @@ import { RuntimeAPIProvider } from '@/contexts/RuntimeAPIProvider';
|
|||||||
import { registerRuntimeAPIs } from '@/contexts/runtimeAPIRegistry';
|
import { registerRuntimeAPIs } from '@/contexts/runtimeAPIRegistry';
|
||||||
import { TooltipProvider } from '@/components/ui/tooltip';
|
import { TooltipProvider } from '@/components/ui/tooltip';
|
||||||
import { Toaster } from '@/components/ui/sonner';
|
import { Toaster } from '@/components/ui/sonner';
|
||||||
|
import { ConfigUpdateOverlay } from '@/components/ui/ConfigUpdateOverlay';
|
||||||
import { ErrorBoundary } from '@/components/ui/ErrorBoundary';
|
import { ErrorBoundary } from '@/components/ui/ErrorBoundary';
|
||||||
import { VSCodeLayout } from '@/components/layout/VSCodeLayout';
|
import { VSCodeLayout } from '@/components/layout/VSCodeLayout';
|
||||||
import { usePushVisibilityBeacon } from '@/hooks/usePushVisibilityBeacon';
|
import { usePushVisibilityBeacon } from '@/hooks/usePushVisibilityBeacon';
|
||||||
@@ -124,6 +125,7 @@ export function VSCodeApp({ apis }: VSCodeAppProps) {
|
|||||||
<SyncAppEffects embeddedBackgroundWorkEnabled={true} />
|
<SyncAppEffects embeddedBackgroundWorkEnabled={true} />
|
||||||
<VSCodeLayout />
|
<VSCodeLayout />
|
||||||
<Toaster />
|
<Toaster />
|
||||||
|
<ConfigUpdateOverlay />
|
||||||
</div>
|
</div>
|
||||||
</TooltipProvider>
|
</TooltipProvider>
|
||||||
</FireworksProvider>
|
</FireworksProvider>
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import {
|
|||||||
import { OpenChamberLogo } from "./OpenChamberLogo";
|
import { OpenChamberLogo } from "./OpenChamberLogo";
|
||||||
|
|
||||||
export const ConfigUpdateOverlay: React.FC = () => {
|
export const ConfigUpdateOverlay: React.FC = () => {
|
||||||
const [{ isUpdating, message }, setState] = React.useState(() => getConfigUpdateSnapshot());
|
const [{ isUpdating }, setState] = React.useState(() => getConfigUpdateSnapshot());
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
return subscribeConfigUpdate(setState);
|
return subscribeConfigUpdate(setState);
|
||||||
@@ -16,12 +16,11 @@ export const ConfigUpdateOverlay: React.FC = () => {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// No status text — the update message is internal jargon and reads as noise.
|
||||||
|
// The animated logo alone signals "working".
|
||||||
return (
|
return (
|
||||||
<div className="fixed inset-0 z-[9999] flex flex-col items-center justify-center gap-6 bg-background/90">
|
<div className="fixed inset-0 z-[9999] flex items-center justify-center bg-background/90">
|
||||||
<OpenChamberLogo width={80} height={80} />
|
<OpenChamberLogo width={80} height={80} isAnimated />
|
||||||
<p className="typography-body text-muted-foreground">
|
|
||||||
{message}
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -82,6 +82,7 @@ export const OpenChamberLogo: React.FC<OpenChamberLogoProps> = ({
|
|||||||
className = '',
|
className = '',
|
||||||
width = 70,
|
width = 70,
|
||||||
height = 70,
|
height = 70,
|
||||||
|
isAnimated = false,
|
||||||
}) => {
|
}) => {
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
const themeContext = useOptionalThemeSystem();
|
const themeContext = useOptionalThemeSystem();
|
||||||
@@ -193,6 +194,9 @@ export const OpenChamberLogo: React.FC<OpenChamberLogoProps> = ({
|
|||||||
role="img"
|
role="img"
|
||||||
aria-label={t('openChamberLogo.aria.logo')}
|
aria-label={t('openChamberLogo.aria.logo')}
|
||||||
>
|
>
|
||||||
|
{isAnimated ? (
|
||||||
|
<style>{`@keyframes oc-logo-glow{0%,100%{filter:drop-shadow(0 0 0 transparent)}50%{filter:drop-shadow(0 0 4px var(--oc-glow-color))}}.oc-logo-glow{animation:oc-logo-glow 1.8s ease-in-out infinite}@media (prefers-reduced-motion:reduce){.oc-logo-glow{animation:none}}`}</style>
|
||||||
|
) : null}
|
||||||
{/* Left face - base fill */}
|
{/* Left face - base fill */}
|
||||||
<path
|
<path
|
||||||
d={`M${center.x} ${center.y} L${left.x} ${left.y} L${bottomLeft.x} ${bottomLeft.y} L${bottom.x} ${bottom.y} Z`}
|
d={`M${center.x} ${center.y} L${left.x} ${left.y} L${bottomLeft.x} ${bottomLeft.y} L${bottom.x} ${bottom.y} Z`}
|
||||||
@@ -241,7 +245,11 @@ export const OpenChamberLogo: React.FC<OpenChamberLogoProps> = ({
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
{/* OpenCode logo on top face */}
|
{/* OpenCode logo on top face */}
|
||||||
<g opacity={1}>
|
<g
|
||||||
|
opacity={1}
|
||||||
|
className={isAnimated ? 'oc-logo-glow' : undefined}
|
||||||
|
style={isAnimated ? ({ '--oc-glow-color': strokeColor } as React.CSSProperties) : undefined}
|
||||||
|
>
|
||||||
{/*
|
{/*
|
||||||
Isometric transform for top face:
|
Isometric transform for top face:
|
||||||
OpenCode logo (32x40 viewBox) centered and projected to isometric plane
|
OpenCode logo (32x40 viewBox) centered and projected to isometric plane
|
||||||
|
|||||||
@@ -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 {
|
public notifyWindowFocusChanged(focused: boolean): void {
|
||||||
if (!this._view) {
|
if (!this._view) {
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -262,6 +262,13 @@ export async function activate(context: vscode.ExtensionContext) {
|
|||||||
context.subscriptions.push(
|
context.subscriptions.push(
|
||||||
vscode.commands.registerCommand('openchamber.restartApi', async () => {
|
vscode.commands.registerCommand('openchamber.restartApi', async () => {
|
||||||
try {
|
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();
|
await openCodeManager?.restart();
|
||||||
vscode.window.showInformationMessage('OpenChamber: API connection restarted');
|
vscode.window.showInformationMessage('OpenChamber: API connection restarted');
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|||||||
@@ -107,6 +107,17 @@ export function getWebviewHtml(options: WebviewHtmlOptions): string {
|
|||||||
opacity: 0;
|
opacity: 0;
|
||||||
pointer-events: none;
|
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 */
|
/* Logo colors use VS Code foreground color */
|
||||||
#initial-loading .logo-stroke {
|
#initial-loading .logo-stroke {
|
||||||
stroke: var(--vscode-foreground);
|
stroke: var(--vscode-foreground);
|
||||||
@@ -153,9 +164,8 @@ export function getWebviewHtml(options: WebviewHtmlOptions): string {
|
|||||||
<path class="logo-fill-dim" d="M-8 -4 L8 -4 L8 12 L-8 12 Z"/>
|
<path class="logo-fill-dim" d="M-8 -4 L8 -4 L8 12 L-8 12 Z"/>
|
||||||
</g>
|
</g>
|
||||||
</svg>
|
</svg>
|
||||||
<div class="status-text" id="loading-status">
|
<!-- Status text stays empty while things are fine; populated only on error. -->
|
||||||
${initialStatus === 'connecting' ? 'Starting OpenCode API…' : initialStatus === 'connected' ? 'Initializing…' : 'Connecting…'}
|
<div class="status-text" id="loading-status"></div>
|
||||||
</div>
|
|
||||||
${!cliAvailable ? `<div class="error-text">OpenCode CLI not found. Please install it first.</div>` : ''}
|
${!cliAvailable ? `<div class="error-text">OpenCode CLI not found. Please install it first.</div>` : ''}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -184,17 +194,13 @@ export function getWebviewHtml(options: WebviewHtmlOptions): string {
|
|||||||
if (msg && msg.type === 'connectionStatus') {
|
if (msg && msg.type === 'connectionStatus') {
|
||||||
var statusEl = document.getElementById('loading-status');
|
var statusEl = document.getElementById('loading-status');
|
||||||
if (statusEl) {
|
if (statusEl) {
|
||||||
if (msg.status === 'connecting') {
|
// Only show text when something is wrong — progress states stay silent
|
||||||
statusEl.textContent = 'Starting OpenCode API…';
|
// (the animated logo already signals "working").
|
||||||
statusEl.classList.remove('error-text');
|
if (msg.status === 'error') {
|
||||||
} else if (msg.status === 'connected') {
|
|
||||||
statusEl.textContent = 'Connected!';
|
|
||||||
statusEl.classList.remove('error-text');
|
|
||||||
} else if (msg.status === 'error') {
|
|
||||||
statusEl.textContent = msg.error || 'Connection error';
|
statusEl.textContent = msg.error || 'Connection error';
|
||||||
statusEl.classList.add('error-text');
|
statusEl.classList.add('error-text');
|
||||||
} else {
|
} else {
|
||||||
statusEl.textContent = 'Reconnecting…';
|
statusEl.textContent = '';
|
||||||
statusEl.classList.remove('error-text');
|
statusEl.classList.remove('error-text');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -181,9 +181,8 @@ const maybeHideLoadingOverlay = () => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const providersText = bootstrapProvidersReady ? '✓ Providers' : '… Providers';
|
// Still loading providers/agents — stay silent (the animated logo signals work).
|
||||||
const agentsText = bootstrapAgentsReady ? '✓ Agents' : '… Agents';
|
setLoadingStatusText('');
|
||||||
setLoadingStatusText(`Loading data (${providersText}, ${agentsText})…`);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -200,7 +199,8 @@ const maybeHideLoadingOverlay = () => {
|
|||||||
return;
|
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 } } }) => {
|
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' } }));
|
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 getNotificationClaimKey = (payload: { title?: unknown; body?: unknown; sessionId?: unknown; tag?: unknown } | undefined): string => {
|
||||||
const tag = typeof payload?.tag === 'string' ? payload.tag.trim() : '';
|
const tag = typeof payload?.tag === 'string' ? payload.tag.trim() : '';
|
||||||
if (tag) return tag;
|
if (tag) return tag;
|
||||||
|
|||||||
Reference in New Issue
Block a user