Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 65 additions & 0 deletions src/web-ui/src/app/hooks/useGallerySceneAutoRefresh.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { useEffect, useRef } from 'react';
import type { SceneTabId } from '@/app/components/SceneBar/types';
import { useSceneManager } from './useSceneManager';

export interface UseGallerySceneAutoRefreshOptions {
/** Tab id from SceneBar (e.g. skills, agents, miniapps). */
sceneId: SceneTabId;
/** Reload lists; may be async. */
refetch: () => void | Promise<void>;
enabled?: boolean;
}

/**
* Gallery scenes stay mounted while inactive (SceneViewport). Refresh when:
* 1. User switches back to this tab (inactive → active).
* 2. The window regains visibility while this tab is active (e.g. external edits).
*
* Initial load remains the responsibility of each feature hook (workspacePath,
* search query, etc.); this hook only covers re-entry and focus.
*/
export function useGallerySceneAutoRefresh({
sceneId,
refetch,
enabled = true,
}: UseGallerySceneAutoRefreshOptions): void {
const { activeTabId } = useSceneManager();
const isActive = activeTabId === sceneId;
const refetchRef = useRef(refetch);
refetchRef.current = refetch;

/** null = not yet synced (skip first tick to avoid duplicating hook mount loads). */
const wasActiveRef = useRef<boolean | null>(null);

useEffect(() => {
if (!enabled) {
return;
}
if (wasActiveRef.current === null) {
wasActiveRef.current = isActive;
return;
}
if (isActive && !wasActiveRef.current) {
void Promise.resolve(refetchRef.current());
}
wasActiveRef.current = isActive;
}, [enabled, isActive]);

useEffect(() => {
if (!enabled) {
return;
}
const onVisibility = () => {
if (document.visibilityState !== 'visible') {
return;
}
if (activeTabId !== sceneId) {
return;
}
void Promise.resolve(refetchRef.current());
};

document.addEventListener('visibilitychange', onVisibility);
return () => document.removeEventListener('visibilitychange', onVisibility);
}, [enabled, activeTabId, sceneId]);
}
19 changes: 19 additions & 0 deletions src/web-ui/src/app/scenes/SceneViewport.scss
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,25 @@
margin: 0;
}

// ── Lazy chunk loading (matches flow_chat ProcessingIndicator dots) ──

&__lazy-fallback {
position: absolute;
inset: 0;
display: flex;
align-items: center;
justify-content: center;
pointer-events: none;
z-index: 1;

.processing-indicator {
max-width: none;
width: auto;
margin: 0;
padding: 0;
}
}

// ── Scene slot ────────────────────────────────────────

&__scene {
Expand Down
14 changes: 13 additions & 1 deletion src/web-ui/src/app/scenes/SceneViewport.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import type { SceneTabId } from '../components/SceneBar/types';
import { useSceneManager } from '../hooks/useSceneManager';
import { useI18n } from '@/infrastructure/i18n/hooks/useI18n';
import { useDialogCompletionNotify } from '../hooks/useDialogCompletionNotify';
import { ProcessingIndicator } from '@/flow_chat/components/modern/ProcessingIndicator';
import './SceneViewport.scss';

const SessionScene = lazy(() => import('./session/SessionScene'));
Expand Down Expand Up @@ -56,7 +57,18 @@ const SceneViewport: React.FC<SceneViewportProps> = ({ workspacePath, isEntering
return (
<div className="bitfun-scene-viewport">
<div className="bitfun-scene-viewport__clip">
<Suspense fallback={null}>
<Suspense
fallback={(
<div
className="bitfun-scene-viewport__lazy-fallback"
role="status"
aria-busy="true"
aria-label={t('loading.scenes')}
>
<ProcessingIndicator visible />
</div>
)}
>
{openTabs.map(tab => (
<div
key={tab.id}
Expand Down
19 changes: 6 additions & 13 deletions src/web-ui/src/app/scenes/agents/AgentsScene.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import { getCardGradient } from '@/shared/utils/cardGradients';
import { getAgentBadge } from './utils';
import './AgentsView.scss';
import './AgentsScene.scss';
import { useGallerySceneAutoRefresh } from '@/app/hooks/useGallerySceneAutoRefresh';

const HIDDEN_AGENT_IDS = new Set(['Claw']);

Expand Down Expand Up @@ -77,6 +78,11 @@ const AgentsHomeView: React.FC = () => {
t,
});

useGallerySceneAutoRefresh({
sceneId: 'agents',
refetch: () => void loadAgents(),
});

const coreAgentMeta = useMemo((): Record<string, CoreAgentMeta> => ({
agentic: {
role: t('coreAgentsZone.modes.agentic.role'),
Expand Down Expand Up @@ -190,19 +196,6 @@ const AgentsHomeView: React.FC = () => {
</button>
)}
/>
<button
type="button"
className="gallery-action-btn"
onClick={() => void loadAgents()}
disabled={loading}
aria-label={t('page.refresh')}
title={t('page.refresh')}
>
<RefreshCw
size={15}
className={loading ? 'gallery-spinning' : undefined}
/>
</button>
</>
)}
/>
Expand Down
11 changes: 11 additions & 0 deletions src/web-ui/src/app/scenes/assistant/AssistantScene.scss
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,16 @@
&__loading {
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
pointer-events: none;

.processing-indicator {
max-width: none;
width: auto;
margin: 0;
padding: 0;
}
}
}
16 changes: 15 additions & 1 deletion src/web-ui/src/app/scenes/assistant/AssistantScene.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import React, { Suspense, lazy, useMemo, useEffect } from 'react';
import { useWorkspaceContext } from '@/infrastructure/contexts/WorkspaceContext';
import { useI18n } from '@/infrastructure/i18n/hooks/useI18n';
import { WorkspaceKind } from '@/shared/types';
import { ProcessingIndicator } from '@/flow_chat/components/modern/ProcessingIndicator';
import { useMyAgentStore } from '../my-agent/myAgentStore';
import './AssistantScene.scss';

Expand All @@ -11,6 +13,7 @@ interface AssistantSceneProps {
}

const AssistantScene: React.FC<AssistantSceneProps> = ({ workspacePath }) => {
const { t } = useI18n('common');
const selectedAssistantWorkspaceId = useMyAgentStore((s) => s.selectedAssistantWorkspaceId);
const setSelectedAssistantWorkspaceId = useMyAgentStore((s) => s.setSelectedAssistantWorkspaceId);
const { currentWorkspace, assistantWorkspacesList } = useWorkspaceContext();
Expand Down Expand Up @@ -67,7 +70,18 @@ const AssistantScene: React.FC<AssistantSceneProps> = ({ workspacePath }) => {

return (
<div className="bitfun-assistant-scene">
<Suspense fallback={<div className="bitfun-assistant-scene__loading" />}>
<Suspense
fallback={(
<div
className="bitfun-assistant-scene__loading"
role="status"
aria-busy="true"
aria-label={t('loading.scenes')}
>
<ProcessingIndicator visible />
</div>
)}
>
<ProfileScene
key={resolvedAssistantWorkspace?.id ?? 'default-assistant-workspace'}
workspacePath={resolvedAssistantWorkspace?.rootPath ?? workspacePath}
Expand Down
32 changes: 16 additions & 16 deletions src/web-ui/src/app/scenes/miniapps/views/MiniAppGalleryView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import {
FolderPlus,
LayoutGrid,
Play,
RefreshCw,
Sparkles,
Square,
Tag,
Expand All @@ -31,6 +30,7 @@ import { getMiniAppIconGradient, renderMiniAppIcon } from '../utils/miniAppIcons
import { useCurrentWorkspace } from '@/infrastructure/contexts/WorkspaceContext';
import { useMiniAppStore } from '../miniAppStore';
import { useI18n } from '@/infrastructure/i18n';
import { useGallerySceneAutoRefresh } from '@/app/hooks/useGallerySceneAutoRefresh';
import './MiniAppGalleryView.scss';

const log = createLogger('MiniAppGalleryView');
Expand All @@ -41,6 +41,7 @@ const MiniAppGalleryView: React.FC = () => {
const runningWorkerIds = useMiniAppStore((state) => state.runningWorkerIds);
const setApps = useMiniAppStore((state) => state.setApps);
const setLoading = useMiniAppStore((state) => state.setLoading);
const setRunningWorkerIds = useMiniAppStore((state) => state.setRunningWorkerIds);
const markWorkerStopped = useMiniAppStore((state) => state.markWorkerStopped);
const { workspacePath } = useCurrentWorkspace();
const { openScene, activateScene, closeScene, openTabs } = useSceneManager();
Expand Down Expand Up @@ -134,15 +135,26 @@ const MiniAppGalleryView: React.FC = () => {
}
};

const handleRefresh = async () => {
const refetchMiniAppGallery = useCallback(async () => {
setLoading(true);
try {
const refreshed = await miniAppAPI.listMiniApps();
const [refreshed, running] = await Promise.all([
miniAppAPI.listMiniApps(),
miniAppAPI.workerListRunning(),
]);
setApps(refreshed);
setRunningWorkerIds(running);
} catch (error) {
log.error('Failed to refresh miniapp gallery', error);
} finally {
setLoading(false);
}
};
}, [setApps, setLoading, setRunningWorkerIds]);

useGallerySceneAutoRefresh({
sceneId: 'miniapps',
refetch: refetchMiniAppGallery,
});

const handleAddFromFolder = async () => {
try {
Expand Down Expand Up @@ -219,18 +231,6 @@ const MiniAppGalleryView: React.FC = () => {
>
<FolderPlus size={15} />
</button>
<button
type="button"
className="gallery-action-btn"
onClick={handleRefresh}
disabled={loading}
title={t('refreshList')}
>
<RefreshCw
size={15}
className={loading ? 'gallery-spinning' : undefined}
/>
</button>
</>
)}
/>
Expand Down
32 changes: 1 addition & 31 deletions src/web-ui/src/app/scenes/profile/views/AssistantConfigPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ import {
} from 'lucide-react';
import {
Button,
ConfirmDialog,
IconButton,
Input,
} from '@/component-library';
Expand Down Expand Up @@ -93,7 +92,6 @@ const AssistantConfigPage: React.FC = () => {
const {
document: identityDocument,
updateField: updateIdentityField,
resetPersonaFiles,
reload: reloadIdentityDocument,
} = useAgentIdentityDocument(workspacePath);

Expand All @@ -111,8 +109,6 @@ const AssistantConfigPage: React.FC = () => {
const [editValue, setEditValue] = useState('');
const nameInputRef = useRef<HTMLInputElement>(null);
const metaInputRef = useRef<HTMLInputElement>(null);
const [isResetDialogOpen, setIsResetDialogOpen] = useState(false);

const [rightView, setRightView] = useState<RightPanelView>('info');
const [personaDoc, setPersonaDoc] = useState<PersonaDocState | null>(null);
const personaSaveTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
Expand Down Expand Up @@ -450,16 +446,6 @@ const AssistantConfigPage: React.FC = () => {
)}
</div>
</div>
<IconButton
type="button"
size="xs"
className="acp-left-header__reset"
aria-label={t('identity.resetTooltip')}
tooltip={t('identity.resetTooltip')}
onClick={() => setIsResetDialogOpen(true)}
>
<RefreshCw size={13} />
</IconButton>
</div>

<AssistantQuickInput
Expand All @@ -468,6 +454,7 @@ const AssistantConfigPage: React.FC = () => {
assistantName={identityName}
/>
<div className="acp-sessions-area">
<h2 className="acp-sessions-area__title">{t('nursery.assistant.sessionsSectionTitle')}</h2>
<SessionsSection
workspaceId={workspace?.id}
workspacePath={workspacePath}
Expand All @@ -483,23 +470,6 @@ const AssistantConfigPage: React.FC = () => {
{rightView === 'personaDoc' ? renderPersonaDocPanel() : renderInfoPanel()}
</div>
</div>

<ConfirmDialog
isOpen={isResetDialogOpen}
title={t('identity.resetConfirmTitle')}
message={t('identity.resetConfirmMessage')}
confirmText={t('identity.resetConfirmAction')}
cancelText={t('identity.resetCancel')}
confirmDanger
onClose={() => setIsResetDialogOpen(false)}
onConfirm={() => {
setIsResetDialogOpen(false);
resetPersonaFiles()
.then(() => notificationService.success(t('identity.resetSuccess')))
.catch(() => notificationService.error(t('identity.resetFailed')));
}}
onCancel={() => setIsResetDialogOpen(false)}
/>
</div>
);
};
Expand Down
Loading
Loading