Skip to content
Open
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
2 changes: 2 additions & 0 deletions MiniApp/Skills/miniapp-dev/api-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -297,6 +297,8 @@ app.onLocaleChange((locale) => {
});
```

MiniApp 加载完成后会立即收到一次当前激活状态(Tab 在前台收到 `activate`,在后台收到 `deactivate`),之后每次 Tab 切入/切走时触发对应钩子。适合用来暂停/恢复轮询、动画等后台不需要的工作。

## 国际化 i18n

### `app.t(table, fallback)` — 多语言字符串挑选
Expand Down
2 changes: 1 addition & 1 deletion src/web-ui/src/app/scenes/SceneViewport.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,7 @@ function renderScene(
return <PanelViewScene workspacePath={workspacePath} />;
default:
if (typeof id === 'string' && id.startsWith('miniapp:')) {
return <MiniAppScene appId={id.slice('miniapp:'.length)} />;
return <MiniAppScene appId={id.slice('miniapp:'.length)} isActive={isActive} />;
}
return null;
}
Expand Down
5 changes: 3 additions & 2 deletions src/web-ui/src/app/scenes/miniapps/MiniAppScene.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,10 @@ const MINIAPP_REFRESH_EVENTS = [

interface MiniAppSceneProps {
appId: string;
isActive?: boolean;
}

const MiniAppScene: React.FC<MiniAppSceneProps> = ({ appId }) => {
const MiniAppScene: React.FC<MiniAppSceneProps> = ({ appId, isActive = true }) => {
const openApp = useMiniAppStore((state) => state.openApp);
const closeApp = useMiniAppStore((state) => state.closeApp);
const markCustomizationActive = useMiniAppStore((state) => state.markCustomizationActive);
Expand Down Expand Up @@ -206,7 +207,7 @@ const MiniAppScene: React.FC<MiniAppSceneProps> = ({ appId }) => {
<Loader2 size={20} className="miniapp-scene__spinning" strokeWidth={1.5} />
</div>
)}
<MiniAppRunner key={`${app.id}-${key}`} app={app} />
<MiniAppRunner key={`${app.id}-${key}`} app={app} isActive={isActive} />
{customizePreview && (
<div className="miniapp-scene__preview-stage" role="region" aria-label={t('customize.previewTitle')}>
<div className="miniapp-scene__preview-stage-header">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,13 @@ import type { MiniAppRunScope } from '../customization/miniAppCustomizationTypes
interface MiniAppRunnerProps {
app: MiniApp;
runScope?: MiniAppRunScope;
/** Whether the hosting scene tab is active; drives activate/deactivate lifecycle events. */
isActive?: boolean;
}

const MiniAppRunner: React.FC<MiniAppRunnerProps> = ({ app, runScope }) => {
const MiniAppRunner: React.FC<MiniAppRunnerProps> = ({ app, runScope, isActive = true }) => {
const iframeRef = useRef<HTMLIFrameElement>(null);
useMiniAppBridge(iframeRef, app, runScope ?? { kind: 'active', appId: app.id });
useMiniAppBridge(iframeRef, app, runScope ?? { kind: 'active', appId: app.id }, isActive);

const writeCompiledHtml = useCallback(() => {
const iframe = iframeRef.current;
Expand Down
27 changes: 26 additions & 1 deletion src/web-ui/src/app/scenes/miniapps/hooks/useMiniAppBridge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@
* ai.* → Host AI client, agent.* → Host agent bridge (hidden subagent runs),
* deck.renderPage → hidden host WebView slide rasterization (export),
* clipboard.* → Host navigator.clipboard.
* Also handles bitfun/request-theme and pushes theme changes to the iframe.
* Also handles bitfun/request-theme and pushes theme changes to the iframe,
* and pushes activate/deactivate lifecycle events as the hosting scene tab
* gains/loses activation (consumed via app.onActivate/app.onDeactivate).
*/
import { useLayoutEffect, useRef, useEffect, RefObject } from 'react';
import { miniAppAPI } from '@/infrastructure/api/service-api/MiniAppAPI';
Expand Down Expand Up @@ -37,6 +39,7 @@ export function useMiniAppBridge(
iframeRef: RefObject<HTMLIFrameElement>,
app: MiniApp,
runScope: MiniAppRunScope,
isActive: boolean = true,
) {
const { workspacePath } = useCurrentWorkspace();
const { theme: currentTheme } = useTheme();
Expand All @@ -47,6 +50,12 @@ export function useMiniAppBridge(
workspacePathRef.current = workspacePath;
const localeRef = useRef(currentLanguage);
localeRef.current = currentLanguage;
const isActiveRef = useRef(isActive);
isActiveRef.current = isActive;
// The compiled bridge script announces readiness by sending bitfun/request-theme
// after registering its message listener; activation pushes before that point
// would be dropped by the still-loading iframe.
const bridgeReadyRef = useRef(false);

const runScopeRef = useRef<MiniAppRunScope>(runScope);
runScopeRef.current = runScope;
Expand Down Expand Up @@ -94,6 +103,11 @@ export function useMiniAppBridge(
'*',
);
}
bridgeReadyRef.current = true;
iframeRef.current?.contentWindow?.postMessage(
{ type: 'bitfun:event', event: isActiveRef.current ? 'activate' : 'deactivate' },
'*',
);
return;
}

Expand Down Expand Up @@ -394,6 +408,17 @@ export function useMiniAppBridge(
);
}, [currentLanguage, iframeRef]);

// Push activation transitions (scene tab shown/hidden) to the iframe.
// Before the bridge handshake the initial state is delivered by the
// bitfun/request-theme handler above, so pre-handshake pushes are skipped.
useEffect(() => {
if (!bridgeReadyRef.current || !iframeRef.current?.contentWindow) return;
iframeRef.current.contentWindow.postMessage(
{ type: 'bitfun:event', event: isActive ? 'activate' : 'deactivate' },
'*',
);
}, [isActive, iframeRef]);

// Listen for AI stream events from Tauri and forward them to the iframe.
useEffect(() => {
const currentAppId = app.id;
Expand Down
Loading