Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
a385ecd
🤖 feat: add workspace archiving support
ammar-agent Dec 21, 2025
7ff7da0
fix: use ArchiveIcon instead of × in sidebar
ammar-agent Dec 21, 2025
531a7a1
fix: make archive icon smaller
ammar-agent Dec 21, 2025
44dc935
refactor: create ProjectPage component with archived workspaces
ammar-agent Dec 21, 2025
ae42ce3
fix: archived workspaces no longer tracked by app
ammar-agent Dec 21, 2025
14fdb18
fix: vertically center archive icon in sidebar
ammar-agent Dec 21, 2025
cff002c
fix: simplify ArchivedWorkspaces - workspaces prop already filtered
ammar-agent Dec 21, 2025
c96bb53
fix: improve ProjectPage layout - center chat input, show archived above
ammar-agent Dec 21, 2025
3160f1c
fix: propagate archived/archivedAt fields in getAllWorkspaceMetadata
ammar-agent Dec 21, 2025
54295cb
fix: reorder ProjectPage - creation above archived, ensure centered
ammar-agent Dec 21, 2025
28e1a72
fix: simplify ProjectPage layout to match main - let ChatInput handle…
ammar-agent Dec 21, 2025
e295aa1
fix: properly center archived workspaces using translate-x-1/2
ammar-agent Dec 21, 2025
f06dd5a
feat: add Storybook story for ProjectPage with archived workspaces
ammar-agent Dec 21, 2025
d20d4b9
fix: clean CSS layout for ProjectPage - siblings with flex distribution
ammar-agent Dec 21, 2025
b15a72e
fix: min gap between chat and archived, remove inner scrollbox
ammar-agent Dec 21, 2025
5cc080e
fix: remove collapsible behavior from ArchivedWorkspaces
ammar-agent Dec 21, 2025
32b7ed0
fix: use justify-center instead of spacers - consistent gap regardles…
ammar-agent Dec 21, 2025
c5778f8
feat: add bulk delete/restore for archived workspaces
ammar-agent Dec 21, 2025
5b80c76
fix: use standard Dialog for BulkProgressModal
ammar-agent Dec 21, 2025
977a13d
fix: add confirmation for bulk delete since it uses force
ammar-agent Dec 21, 2025
f56594d
WIP: workspace archiving - bulk operations, layout fixes, stories
ammar-agent Dec 21, 2025
bb367ce
refactor: derive archived state from timestamps, not boolean
ammar-agent Dec 21, 2025
cbc0ec9
fix: make ProjectPage scroll correctly with many archived workspaces
ammar-agent Dec 21, 2025
bda7262
refactor: add shared isWorkspaceArchived helper
ammar-agent Dec 21, 2025
4b5489f
fix: use force-delete modal for archived workspace deletion
ammar-agent Dec 21, 2025
431520b
fix: stop active streams when archiving workspaces
ammar-agent Dec 21, 2025
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
10 changes: 9 additions & 1 deletion .storybook/mocks/orpc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import {
type TaskSettings,
} from "@/common/types/tasks";
import { createAsyncMessageQueue } from "@/common/utils/asyncMessageQueue";
import { isWorkspaceArchived } from "@/common/utils/archive";

/** Session usage data structure matching SessionUsageFileSchema */
export interface MockSessionUsage {
Expand Down Expand Up @@ -251,7 +252,14 @@ export function createMockORPCClient(options: MockORPCClientOptions = {}): APICl
},
},
workspace: {
list: async () => workspaces,
list: async (input?: { archived?: boolean }) => {
if (input?.archived) {
return workspaces.filter((w) => isWorkspaceArchived(w.archivedAt, w.unarchivedAt));
}
return workspaces.filter((w) => !isWorkspaceArchived(w.archivedAt, w.unarchivedAt));
},
archive: async () => ({ success: true }),
unarchive: async () => ({ success: true }),
create: async (input: { projectPath: string; branchName: string }) => ({
success: true,
metadata: {
Expand Down
103 changes: 36 additions & 67 deletions src/browser/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,17 +17,12 @@ import { buildSortedWorkspacesByProject } from "./utils/ui/workspaceFiltering";
import { useResumeManager } from "./hooks/useResumeManager";
import { useUnreadTracking } from "./hooks/useUnreadTracking";
import { useWorkspaceStoreRaw, useWorkspaceRecency } from "./stores/WorkspaceStore";
import { ChatInput } from "./components/ChatInput/index";
import type { ChatInputAPI } from "./components/ChatInput/types";

import { useStableReference, compareMaps } from "./hooks/useStableReference";
import { CommandRegistryProvider, useCommandRegistry } from "./contexts/CommandRegistryContext";
import { useOpenTerminal } from "./hooks/useOpenTerminal";
import type { CommandAction } from "./contexts/CommandRegistryContext";
import { ModeProvider } from "./contexts/ModeContext";
import { ProviderOptionsProvider } from "./contexts/ProviderOptionsContext";
import { ThemeProvider, useTheme, type ThemeMode } from "./contexts/ThemeContext";
import { ThinkingProvider } from "./contexts/ThinkingContext";
import { CommandPalette } from "./components/CommandPalette";
import { buildCoreSources, type BuildSourcesParams } from "./utils/commands/sources";

Expand All @@ -48,12 +43,12 @@ import { getRuntimeTypeForTelemetry } from "@/common/telemetry";
import { useStartWorkspaceCreation, getFirstProjectPath } from "./hooks/useStartWorkspaceCreation";
import { useAPI } from "@/browser/contexts/API";
import { AuthTokenModal } from "@/browser/components/AuthTokenModal";
import { ProjectPage } from "@/browser/components/ProjectPage";

import { SettingsProvider, useSettings } from "./contexts/SettingsContext";
import { SettingsModal } from "./components/Settings/SettingsModal";
import { SplashScreenProvider } from "./components/splashScreens/SplashScreenProvider";
import { TutorialProvider } from "./contexts/TutorialContext";
import { ConnectionStatusIndicator } from "./components/ConnectionStatusIndicator";
import { TooltipProvider } from "./components/ui/tooltip";
import { useFeatureFlags } from "./contexts/FeatureFlagsContext";
import { FeatureFlagsProvider } from "./contexts/FeatureFlagsContext";
Expand Down Expand Up @@ -102,25 +97,16 @@ function AppInner() {
const isMobile = typeof window !== "undefined" && window.innerWidth <= 768;
const [sidebarCollapsed, setSidebarCollapsed] = usePersistedState("sidebarCollapsed", isMobile);
const defaultProjectPath = getFirstProjectPath(projects);
const creationChatInputRef = useRef<ChatInputAPI | null>(null);
const creationProjectPath = !selectedWorkspace
? (pendingNewWorkspaceProject ?? (projects.size === 1 ? defaultProjectPath : null))
: null;
const handleCreationChatReady = useCallback((api: ChatInputAPI) => {
creationChatInputRef.current = api;
api.focus();
}, []);

const startWorkspaceCreation = useStartWorkspaceCreation({
projects,
beginWorkspaceCreation,
});

useEffect(() => {
if (creationProjectPath) {
creationChatInputRef.current?.focus();
}
}, [creationProjectPath]);
// ProjectPage handles its own focus when mounted

const handleToggleSidebar = useCallback(() => {
setSidebarCollapsed((prev) => !prev);
Expand Down Expand Up @@ -502,12 +488,6 @@ function AppInner() {
} else if (matchesKeybind(e, KEYBINDS.OPEN_SETTINGS)) {
e.preventDefault();
openSettings();
} else if (matchesKeybind(e, KEYBINDS.FOCUS_CHAT)) {
// Focus creation chat when on new chat page (no workspace selected)
if (creationProjectPath && creationChatInputRef.current) {
e.preventDefault();
creationChatInputRef.current.focus();
}
}
};

Expand Down Expand Up @@ -661,51 +641,40 @@ function AppInner() {
const projectName =
projectPath.split("/").pop() ?? projectPath.split("\\").pop() ?? "Project";
return (
<ModeProvider projectPath={projectPath}>
<ProviderOptionsProvider>
<ThinkingProvider projectPath={projectPath}>
<ConnectionStatusIndicator />
<ChatInput
variant="creation"
projectPath={projectPath}
projectName={projectName}
onProviderConfig={handleProviderConfig}
onReady={handleCreationChatReady}
onWorkspaceCreated={(metadata) => {
// IMPORTANT: Add workspace to store FIRST (synchronous) to ensure
// the store knows about it before React processes the state updates.
// This prevents race conditions where the UI tries to access the
// workspace before the store has created its aggregator.
workspaceStore.addWorkspace(metadata);

// Add to workspace metadata map (triggers React state update)
setWorkspaceMetadata((prev) =>
new Map(prev).set(metadata.id, metadata)
);

// Only switch to new workspace if user hasn't selected another one
// during the creation process (selectedWorkspace was null when creation started)
setSelectedWorkspace((current) => {
if (current !== null) {
// User has already selected another workspace - don't override
return current;
}
return toWorkspaceSelection(metadata);
});

// Track telemetry
telemetry.workspaceCreated(
metadata.id,
getRuntimeTypeForTelemetry(metadata.runtimeConfig)
);

// Clear pending state
clearPendingWorkspaceCreation();
}}
/>
</ThinkingProvider>
</ProviderOptionsProvider>
</ModeProvider>
<ProjectPage
projectPath={projectPath}
projectName={projectName}
onProviderConfig={handleProviderConfig}
onWorkspaceCreated={(metadata) => {
// IMPORTANT: Add workspace to store FIRST (synchronous) to ensure
// the store knows about it before React processes the state updates.
// This prevents race conditions where the UI tries to access the
// workspace before the store has created its aggregator.
workspaceStore.addWorkspace(metadata);

// Add to workspace metadata map (triggers React state update)
setWorkspaceMetadata((prev) => new Map(prev).set(metadata.id, metadata));

// Only switch to new workspace if user hasn't selected another one
// during the creation process (selectedWorkspace was null when creation started)
setSelectedWorkspace((current) => {
if (current !== null) {
// User has already selected another workspace - don't override
return current;
}
return toWorkspaceSelection(metadata);
});

// Track telemetry
telemetry.workspaceCreated(
metadata.id,
getRuntimeTypeForTelemetry(metadata.runtimeConfig)
);

// Clear pending state
clearPendingWorkspaceCreation();
}}
/>
);
})()
) : (
Expand Down
Loading