From c98198e0eee0ae4632672c72936c91e4f9e6bd53 Mon Sep 17 00:00:00 2001 From: Bob Lee Date: Wed, 29 Jul 2026 18:31:56 -0700 Subject: [PATCH] fix(workspace): exclude MiniApp workspaces from recent workspaces list MiniApp agent runs work inside directories the MiniApp owns under `/data/miniapps/` (deck folders, customization drafts). Those directories were tracked as ordinary workspaces, so they appeared in the recent workspaces list on the Welcome page and in the nav workspace switcher, and could even be auto-opened on startup when no workspace was active. Filter them out at the web data source next to the existing linked worktree exclusion, which also hides entries already recorded in local history. On the backend, workspace options for MiniApp-owned paths no longer add to recent, and startup history load drops entries persisted before this change. MiniApp workspaces stay registered, so their agent sessions keep resolving them. --- .../core/src/service/workspace/service.rs | 53 ++++++++++++++++++- .../src/shared/types/global-state.test.ts | 18 +++++++ src/web-ui/src/shared/types/global-state.ts | 26 ++++++++- 3 files changed, 94 insertions(+), 3 deletions(-) diff --git a/src/crates/assembly/core/src/service/workspace/service.rs b/src/crates/assembly/core/src/service/workspace/service.rs index 053f3ada54..9e77e86a8d 100644 --- a/src/crates/assembly/core/src/service/workspace/service.rs +++ b/src/crates/assembly/core/src/service/workspace/service.rs @@ -1748,7 +1748,12 @@ impl WorkspaceService { .recent_workspaces .clone() .into_iter() - .filter(|id| manager.get_workspaces().contains_key(id)) + .filter(|id| { + manager.get_workspaces().get(id).is_some_and(|workspace| { + // Drop MiniApp-owned workspaces recorded before they were excluded. + !self.is_miniapp_owned_path(&workspace.root_path) + }) + }) .collect(); if filtered_recent != data.recent_workspaces { should_persist_cleaned_history = true; @@ -2111,9 +2116,20 @@ impl WorkspaceService { } } + if self.is_miniapp_owned_path(path) { + options.add_to_recent = false; + } + options } + /// MiniApp agent runs and customization drafts work inside directories the + /// MiniApp owns under `/data/miniapps/`. They are app storage, not + /// user projects, so they must stay out of recent workspace history. + fn is_miniapp_owned_path(&self, path: &Path) -> bool { + path.starts_with(self.path_manager.miniapps_dir()) + } + async fn discover_assistant_workspaces( &self, ) -> BitFunResult> { @@ -2579,6 +2595,41 @@ mod tests { ); } + #[tokio::test] + async fn track_workspace_activity_keeps_miniapp_workspaces_out_of_recent_history() { + let env = TestEnvironment::new(); + let service = build_test_workspace_service(env.path_manager.clone()).await; + let miniapp_workspace_root = env + .path_manager + .miniapp_dir("builtin-ppt-live") + .join("decks") + .join("deck-1785130332234"); + std::fs::create_dir_all(&miniapp_workspace_root) + .expect("MiniApp workspace directory should be created"); + + let tracked = service + .track_workspace_activity( + miniapp_workspace_root.clone(), + WorkspaceCreateOptions::default(), + WorkspaceActivityMode::RefreshMetadata, + ) + .await + .expect("MiniApp workspace tracking should succeed"); + + assert_eq!( + service + .get_workspace_by_path(&miniapp_workspace_root) + .await + .map(|workspace| workspace.id), + Some(tracked.id), + "MiniApp workspace should still be registered so its agent session can resolve it" + ); + assert!( + service.get_recent_workspaces().await.is_empty(), + "MiniApp-owned workspaces should never enter recent workspace history" + ); + } + #[tokio::test] async fn touch_only_workspace_activity_preserves_worktree_metadata() { let env = TestEnvironment::new(); diff --git a/src/web-ui/src/shared/types/global-state.test.ts b/src/web-ui/src/shared/types/global-state.test.ts index d607adaaa5..cf6cba43ea 100644 --- a/src/web-ui/src/shared/types/global-state.test.ts +++ b/src/web-ui/src/shared/types/global-state.test.ts @@ -99,6 +99,24 @@ describe('createGlobalStateAPI workspace startup bootstrap', () => { expect(globalApiMocks.initializeWorkspaceStartupState).toHaveBeenCalledTimes(1); }); + it('keeps MiniApp-owned workspaces out of the recent list', async () => { + const snapshot = createWorkspaceSnapshot(); + const miniAppWorkspace = { + ...snapshot.recentWorkspaces[0], + id: 'workspace-miniapp', + name: 'deck-1785130332234-eilik4', + rootPath: + '/Users/me/Library/Application Support/bitfun/data/miniapps/builtin-ppt-live/decks/deck-1785130332234-eilik4', + }; + snapshot.recentWorkspaces = [...snapshot.recentWorkspaces, miniAppWorkspace]; + bootstrapGlobals.__BITFUN_BOOTSTRAP_WORKSPACE_STARTUP_STATE__ = snapshot; + + const api = createGlobalStateAPI(); + const state = await api.initializeWorkspaceStartupState(); + + expect(state.recentWorkspaces.map(workspace => workspace.id)).toEqual(['workspace-1']); + }); + it('falls back to the startup command when the bootstrap snapshot is invalid', async () => { bootstrapGlobals.__BITFUN_BOOTSTRAP_WORKSPACE_STARTUP_STATE__ = { recentWorkspaces: [] }; globalApiMocks.initializeWorkspaceStartupState.mockResolvedValue(createWorkspaceSnapshot()); diff --git a/src/web-ui/src/shared/types/global-state.ts b/src/web-ui/src/shared/types/global-state.ts index 2774c94a70..d289d417da 100644 --- a/src/web-ui/src/shared/types/global-state.ts +++ b/src/web-ui/src/shared/types/global-state.ts @@ -11,6 +11,7 @@ import type { WorkspaceInfo as APIWorkspaceInfo, } from '@/infrastructure/api/service-api/GlobalAPI'; import { createLogger } from '../utils/logger'; +import { normalizePath } from '../utils/pathUtils'; const logger = createLogger('GlobalStateAPI'); @@ -151,6 +152,27 @@ export function isLinkedWorktreeWorkspace(workspace: WorkspaceInfo | null | unde return Boolean(workspace?.worktree && !workspace.worktree.isMain); } +/** MiniApp storage root shared by every platform: `/data/miniapps/`. */ +const MINIAPP_DATA_PATH_SEGMENT = '/data/miniapps/'; + +/** + * MiniApp agent runs work inside directories the MiniApp owns under the app data + * dir (`/data/miniapps//...`, drafts included). They are MiniApp + * storage rather than user projects, so they never belong in workspace history. + */ +export function isMiniAppWorkspace(workspace: WorkspaceInfo | null | undefined): boolean { + const rootPath = workspace?.rootPath; + if (!rootPath) { + return false; + } + return normalizePath(rootPath).includes(MINIAPP_DATA_PATH_SEGMENT); +} + +/** Temporary or app-owned workspaces that must not pollute the recent list. */ +function isExcludedFromRecentWorkspaces(workspace: WorkspaceInfo): boolean { + return isLinkedWorktreeWorkspace(workspace) || isMiniAppWorkspace(workspace); +} + export enum WorkspaceAction { Opened = 'opened', @@ -395,7 +417,7 @@ function mapWorkspaceStartupStateSnapshot( ): WorkspaceStartupState { const recentWorkspaces = snapshot.recentWorkspaces .map(mapWorkspaceInfo) - .filter(ws => !isLinkedWorktreeWorkspace(ws)); + .filter(ws => !isExcludedFromRecentWorkspaces(ws)); return { cleanupRemovedCount: snapshot.cleanupRemovedCount, currentWorkspace: snapshot.currentWorkspace ? mapWorkspaceInfo(snapshot.currentWorkspace) : null, @@ -569,7 +591,7 @@ export function createGlobalStateAPI(): GlobalStateAPI { async getRecentWorkspaces(): Promise { const workspaces = (await globalAPI.getRecentWorkspaces()) .map(mapWorkspaceInfo) - .filter(ws => !isLinkedWorktreeWorkspace(ws)); + .filter(ws => !isExcludedFromRecentWorkspaces(ws)); logger.debug('getRecentWorkspaces returned', summarizeWorkspacesForLog(workspaces)); return workspaces; },