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
40 changes: 35 additions & 5 deletions apps/staged/src/App.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@
import TopBar from './lib/features/layout/TopBar.svelte';
import ProjectHome from './lib/features/projects/ProjectHome.svelte';
import ProjectsList from './lib/features/projects/ProjectsList.svelte';
import ProjectsSidebar from './lib/features/projects/ProjectsSidebar.svelte';
import ProjectDeleteDialog from './lib/features/projects/ProjectDeleteDialog.svelte';
import ReposListView from './lib/features/projects/ReposListView.svelte';
import SessionLauncher from './lib/features/sessions/SessionLauncher.svelte';
import SettingsPage from './lib/features/settings/SettingsPage.svelte';
Expand Down Expand Up @@ -44,6 +46,7 @@
} from './lib/features/keyboard/shortcuts';
import { runSearchShortcut } from './lib/features/keyboard/searchTargets';
import { projectStateStore } from './lib/stores/projectState.svelte';
import { projectsDataStore } from './lib/stores/projectsData.svelte';
import { initBloxEnv } from './lib/stores/bloxEnv.svelte';
import { listenForSessionStatus } from './lib/listeners/sessionStatusListener';
import { listenForCacheInvalidation } from './lib/listeners/cacheInvalidationListener';
Expand Down Expand Up @@ -304,6 +307,9 @@
// Refresh provider discovery (and any loaded doctor report) once the
// backend finishes installing/upgrading the managed ACP bridges.
unlistenAcpToolsReconciled = listenForAcpToolsReconciled();
// Keep the shared project-list cache fresh for the app's lifetime — the
// store dedupes, so starting before any view consumes it is safe.
projectsDataStore.startListeners();

try {
await initPreferences();
Expand Down Expand Up @@ -493,6 +499,7 @@
unlistenCacheInvalidation?.();
unlistenPageLifecycle?.();
unlistenAcpToolsReconciled?.();
projectsDataStore.stopListeners();
stopUpdaterLoop?.();
});

Expand Down Expand Up @@ -592,12 +599,21 @@
subpath={diffRoute.subpath}
onClose={() => closeDiffRouteIfCurrent(diffRoute)}
/>
{:else if reposUiEnabled && navigation.showReposList}
<ReposListView />
{:else if navigation.selectedProjectId}
<ProjectHome selectedProjectId={navigation.selectedProjectId} />
{:else}
<ProjectsList />
<div class="workspace">
<!-- Hoisted out of ProjectHome so it survives project↔repos
transitions; hidden on the landing page. -->
{#if navigation.selectedProjectId || (reposUiEnabled && navigation.showReposList)}
<ProjectsSidebar />
Comment on lines +606 to +607

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Keep run-action tracking alive with the sidebar

When users switch from a project to Repos, this keeps ProjectsSidebar mounted while ProjectHome unmounts; I checked the repo and only ProjectHome/ProjectsList call projectRunActionsStore.startListening, and ProjectHome cleanup calls stopListening, which clears executions. Any running run-action badge/activity in the surviving sidebar therefore disappears or stops updating on the Repos route; start/hydrate the run-action store from the sidebar or App as well.

Useful? React with 👍 / 👎.

{/if}
{#if reposUiEnabled && navigation.showReposList}
<ReposListView />
{:else if navigation.selectedProjectId}
<ProjectHome selectedProjectId={navigation.selectedProjectId} />
{:else}
<ProjectsList />
{/if}
</div>
{/if}
</div>
</main>
Expand All @@ -607,6 +623,10 @@
<SessionLauncher onClose={() => (showSessionLab = false)} />
{/if}

<!-- Shared remove-project confirmation — serves every route's delete entry
point (sidebar, landing grid, ProjectHome top bar/shortcut). -->
<ProjectDeleteDialog />

<ReferenceModalHost />
<Toaster position="bottom-right" visibleToasts={4} duration={8000} closeButton expand />
{/if}
Expand Down Expand Up @@ -635,6 +655,16 @@
flex-direction: column;
}

/* Sidebar + route view side by side (mirrors ProjectHome's old wrapper). */
.workspace {
flex: 1;
min-width: 0;
min-height: 0;
display: flex;
background-color: var(--bg-chrome);
overflow: hidden;
}

.reset-shell {
display: flex;
align-items: center;
Expand Down
41 changes: 22 additions & 19 deletions apps/staged/src/lib/features/layout/navigation.svelte.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,10 @@ import {
clearSnapshot,
SNAPSHOT_KEYS,
} from '../../shared/webSnapshot';
import * as commands from '../../api/commands';
import type { DiffScope } from '../../commands';
import type { CommitTimelineItem } from '../../types';
import { projectStateStore } from '../../stores/projectState.svelte';
import { projectsList } from '../projects/projectsSidebarState.svelte';
import { projectsDataStore } from '../../stores/projectsData.svelte';
import { requestProjectsListRestore } from '../projects/projectsListViewState.svelte';
import { reposUiEnabled } from '../../featureFlags';

Expand Down Expand Up @@ -142,6 +141,11 @@ function persistLastProject(projectId: string | null): void {
* user is sent to the home screen instead.
*/
export async function initNavigation(): Promise<void> {
// Kick the shared projects load immediately so the data is warming while we
// read the persisted route — every consumer (sidebar, landing page, this
// validation) shares the one fetch.
const projectsLoad = projectsDataStore.ensureLoaded();

// `selectedProjectId` may already be set synchronously from the localStorage
// mirror (web cold boot). Fall back to the async persistent store otherwise —
// it is the source of truth in Tauri mode. Either way we render immediately
Expand All @@ -152,24 +156,23 @@ export async function initNavigation(): Promise<void> {

// Validate the project still exists; this runs in the background relative to
// the first paint, which already shows the restored project.
try {
const { data: projects } = await commands.listProjects();
projectsList.current = projects;
const existingIds = new Set(projects.map((p) => p.id));
if (existingIds.has(lastProjectId)) {
setDetailStack([rootRoute(), { kind: 'project', projectId: lastProjectId }]);
} else {
// Project was deleted — back out to home and clear the persisted values.
navigation.selectedProjectId = null;
await setStoreValue(LAST_PROJECT_STORE_KEY, null);
clearSnapshot(SNAPSHOT_KEYS.lastProject);
}
// Remove unread entries for projects that no longer exist
await projectStateStore.pruneDeletedProjects(existingIds);
} catch {
await projectsLoad;
if (!projectsDataStore.loaded) {
// If we can't list projects (e.g. store error), keep whatever we restored.
console.warn('[Navigation] Could not verify last project, keeping restored route');
return;
}
const existingIds = new Set(projectsDataStore.projects.map((p) => p.id));
if (existingIds.has(lastProjectId)) {
setDetailStack([rootRoute(), { kind: 'project', projectId: lastProjectId }]);
} else {
// Project was deleted — back out to home and clear the persisted values.
navigation.selectedProjectId = null;
await setStoreValue(LAST_PROJECT_STORE_KEY, null);
clearSnapshot(SNAPSHOT_KEYS.lastProject);
}
// Remove unread entries for projects that no longer exist
await projectStateStore.pruneDeletedProjects(existingIds);
}

/** Navigate to the repos list view. */
Expand Down Expand Up @@ -224,7 +227,7 @@ function isModalOpen(): boolean {
/** Navigate to the previous project in the list. */
export function selectPreviousProject(): void {
if (currentRoute().kind !== 'project' || !navigation.selectedProjectId || isModalOpen()) return;
const projects = projectsList.current;
const projects = projectsDataStore.projects;
const currentIndex = projects.findIndex((p) => p.id === navigation.selectedProjectId);
if (currentIndex > 0) {
selectProject(projects[currentIndex - 1].id);
Expand All @@ -234,7 +237,7 @@ export function selectPreviousProject(): void {
/** Navigate to the next project in the list. */
export function selectNextProject(): void {
if (currentRoute().kind !== 'project' || !navigation.selectedProjectId || isModalOpen()) return;
const projects = projectsList.current;
const projects = projectsDataStore.projects;
const currentIndex = projects.findIndex((p) => p.id === navigation.selectedProjectId);
if (currentIndex >= 0 && currentIndex < projects.length - 1) {
selectProject(projects[currentIndex + 1].id);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
<!--
ProjectDeleteDialog.svelte - Shared remove-project confirmation dialog

Mounted once in App.svelte and driven by projectActions.pendingDelete, so
every remove-project entry point (sidebar context menu on the project and
repos routes, landing-grid context menu, ProjectHome's top-bar button and
shortcut) shares one confirmation flow.
-->
<script lang="ts">
import * as AlertDialog from '$lib/components/ui/alert-dialog';
import { projectDisplayName } from '../../shared/utils';
import { projectActions } from './projectActions.svelte';
</script>

<AlertDialog.Root
open={projectActions.pendingDelete !== null}
onOpenChange={(v) => !v && projectActions.cancelPendingDelete()}
>
<AlertDialog.Content>
{#if projectActions.pendingDelete}
<AlertDialog.Header>
<AlertDialog.Title>Remove Project</AlertDialog.Title>
<AlertDialog.Description>
{`Remove "${projectDisplayName(projectActions.pendingDelete)}" from Staged? There are unmerged changes in this project's branches. Deleting this project will lose any changes not pushed to GitHub.`}
</AlertDialog.Description>
</AlertDialog.Header>
<AlertDialog.Footer>
<AlertDialog.Cancel>Cancel</AlertDialog.Cancel>
<AlertDialog.Action
variant="destructive"
onclick={() => projectActions.confirmPendingDelete()}
>
Remove
</AlertDialog.Action>
</AlertDialog.Footer>
{/if}
</AlertDialog.Content>
</AlertDialog.Root>
Loading