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
53 changes: 52 additions & 1 deletion src/crates/assembly/core/src/service/workspace/service.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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 `<userRoot>/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<Vec<AssistantWorkspaceDescriptor>> {
Expand Down Expand Up @@ -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();
Expand Down
18 changes: 18 additions & 0 deletions src/web-ui/src/shared/types/global-state.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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());
Expand Down
26 changes: 24 additions & 2 deletions src/web-ui/src/shared/types/global-state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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');

Expand Down Expand Up @@ -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: `<userRoot>/data/miniapps/`. */
const MINIAPP_DATA_PATH_SEGMENT = '/data/miniapps/';

/**
* MiniApp agent runs work inside directories the MiniApp owns under the app data
* dir (`<userRoot>/data/miniapps/<appId>/...`, 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',
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -569,7 +591,7 @@ export function createGlobalStateAPI(): GlobalStateAPI {
async getRecentWorkspaces(): Promise<WorkspaceInfo[]> {
const workspaces = (await globalAPI.getRecentWorkspaces())
.map(mapWorkspaceInfo)
.filter(ws => !isLinkedWorktreeWorkspace(ws));
.filter(ws => !isExcludedFromRecentWorkspaces(ws));
logger.debug('getRecentWorkspaces returned', summarizeWorkspacesForLog(workspaces));
return workspaces;
},
Expand Down