feat: workspace source-of-truth hard cut
This commit is contained in:
@@ -0,0 +1,18 @@
|
||||
import type { WorkspaceDescriptor } from '@/stores/session-store'
|
||||
import { projectDisplayNameFromProjectId } from '@/utils/project-display-name'
|
||||
|
||||
export function resolveWorkspaceHeader(input: {
|
||||
workspace: WorkspaceDescriptor
|
||||
}): { title: string; subtitle: string } {
|
||||
return {
|
||||
title: input.workspace.name,
|
||||
subtitle: projectDisplayNameFromProjectId(input.workspace.projectId),
|
||||
}
|
||||
}
|
||||
|
||||
export function shouldRenderMissingWorkspaceDescriptor(input: {
|
||||
workspace: WorkspaceDescriptor | null
|
||||
hasHydratedWorkspaces: boolean
|
||||
}): boolean {
|
||||
return !input.workspace && input.hasHydratedWorkspaces
|
||||
}
|
||||
@@ -49,7 +49,10 @@ import { ExplorerSidebarAnimationProvider } from "@/contexts/explorer-sidebar-an
|
||||
import { useToast } from "@/contexts/toast-context";
|
||||
import { useExplorerOpenGesture } from "@/hooks/use-explorer-open-gesture";
|
||||
import { usePanelStore, type ExplorerCheckoutContext } from "@/stores/panel-store";
|
||||
import { useSessionStore, type Agent } from "@/stores/session-store";
|
||||
import {
|
||||
useSessionStore,
|
||||
type Agent,
|
||||
} from "@/stores/session-store";
|
||||
import {
|
||||
buildWorkspaceTabPersistenceKey,
|
||||
useWorkspaceTabsStore,
|
||||
@@ -80,11 +83,9 @@ import { WorkspaceDraftAgentTab } from "@/screens/workspace/workspace-draft-agen
|
||||
import { WorkspaceDesktopTabsRow } from "@/screens/workspace/workspace-desktop-tabs-row";
|
||||
import type { WorkspaceTabDescriptor } from "@/screens/workspace/workspace-tabs-types";
|
||||
import {
|
||||
deriveProjectDisplayName,
|
||||
deriveProjectKey,
|
||||
deriveProjectName,
|
||||
deriveRemoteProjectKey,
|
||||
} from "@/utils/agent-grouping";
|
||||
resolveWorkspaceHeader,
|
||||
shouldRenderMissingWorkspaceDescriptor,
|
||||
} from "@/screens/workspace/workspace-header-source";
|
||||
|
||||
const TERMINALS_QUERY_STALE_TIME = 5_000;
|
||||
const DROPDOWN_WIDTH = 220;
|
||||
@@ -152,24 +153,6 @@ function decodeSegment(value: string): string {
|
||||
}
|
||||
}
|
||||
|
||||
function deriveWorkspaceName(workspaceId: string): string {
|
||||
const normalized = workspaceId.replace(/\\/g, "/");
|
||||
const segments = normalized.split("/").filter(Boolean);
|
||||
const last = segments[segments.length - 1];
|
||||
return last ?? workspaceId;
|
||||
}
|
||||
|
||||
function deriveWorkspaceProjectDisplayName(input: {
|
||||
workspaceId: string;
|
||||
checkout: CheckoutStatusPayload | null;
|
||||
}): string {
|
||||
const projectKey =
|
||||
deriveRemoteProjectKey(input.checkout?.remoteUrl ?? null) ??
|
||||
deriveProjectKey(input.workspaceId);
|
||||
const projectName = deriveProjectName(projectKey);
|
||||
return deriveProjectDisplayName({ projectKey, projectName });
|
||||
}
|
||||
|
||||
function formatProviderLabel(provider: Agent["provider"]): string {
|
||||
if (provider === "claude") {
|
||||
return "Claude";
|
||||
@@ -407,18 +390,17 @@ function WorkspaceScreenContent({
|
||||
staleTime: 15_000,
|
||||
});
|
||||
|
||||
const workspaceName = useMemo(
|
||||
() => deriveWorkspaceName(normalizedWorkspaceId),
|
||||
[normalizedWorkspaceId]
|
||||
const workspaceDescriptor = useSessionStore(
|
||||
(state) =>
|
||||
state.sessions[normalizedServerId]?.workspaces.get(normalizedWorkspaceId) ??
|
||||
null
|
||||
);
|
||||
const headerProjectName = useMemo(
|
||||
() =>
|
||||
deriveWorkspaceProjectDisplayName({
|
||||
workspaceId: normalizedWorkspaceId,
|
||||
checkout: checkoutQuery.data ?? null,
|
||||
}),
|
||||
[checkoutQuery.data, normalizedWorkspaceId]
|
||||
const hasHydratedWorkspaces = useSessionStore(
|
||||
(state) => state.sessions[normalizedServerId]?.hasHydratedWorkspaces ?? false
|
||||
);
|
||||
const workspaceHeader = workspaceDescriptor
|
||||
? resolveWorkspaceHeader({ workspace: workspaceDescriptor })
|
||||
: null;
|
||||
|
||||
const isGitCheckout = checkoutQuery.data?.isGit ?? false;
|
||||
const areWorkspaceAgentsHydrated = useSessionStore(
|
||||
@@ -1228,6 +1210,21 @@ function WorkspaceScreenContent({
|
||||
}, [activeAgent, normalizedServerId, router]);
|
||||
|
||||
const renderContent = () => {
|
||||
if (
|
||||
shouldRenderMissingWorkspaceDescriptor({
|
||||
workspace: workspaceDescriptor,
|
||||
hasHydratedWorkspaces,
|
||||
})
|
||||
) {
|
||||
return (
|
||||
<View style={styles.emptyState}>
|
||||
<Text style={styles.emptyStateText}>
|
||||
Workspace descriptor missing for this route.
|
||||
</Text>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
const target = activeTab?.target ?? null;
|
||||
if (!target) {
|
||||
return (
|
||||
@@ -1324,10 +1321,10 @@ function WorkspaceScreenContent({
|
||||
<SidebarMenuToggle />
|
||||
<View style={styles.headerTitleContainer}>
|
||||
<Text style={styles.headerTitle} numberOfLines={1}>
|
||||
{workspaceName}
|
||||
{workspaceHeader ? workspaceHeader.title : "Loading workspace…"}
|
||||
</Text>
|
||||
<Text style={styles.headerProjectTitle} numberOfLines={1}>
|
||||
{headerProjectName}
|
||||
{workspaceHeader ? workspaceHeader.subtitle : "Awaiting workspace descriptor"}
|
||||
</Text>
|
||||
</View>
|
||||
</>
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import {
|
||||
resolveWorkspaceHeader,
|
||||
shouldRenderMissingWorkspaceDescriptor,
|
||||
} from './workspace-header-source'
|
||||
import { buildSidebarProjectsFromWorkspaces } from '@/hooks/use-sidebar-workspaces-list'
|
||||
import type { WorkspaceDescriptor } from '@/stores/session-store'
|
||||
|
||||
describe('workspace source of truth consumption', () => {
|
||||
it('uses the same descriptor name in header and sidebar row', () => {
|
||||
const workspace: WorkspaceDescriptor = {
|
||||
id: '/repo/main',
|
||||
projectId: 'remote:github.com/getpaseo/paseo',
|
||||
name: 'feat/workspace-sot',
|
||||
status: 'running',
|
||||
activityAt: new Date('2026-03-01T00:00:00.000Z'),
|
||||
}
|
||||
|
||||
const header = resolveWorkspaceHeader({ workspace })
|
||||
const sidebarProjects = buildSidebarProjectsFromWorkspaces({
|
||||
serverId: 'srv',
|
||||
workspaces: [workspace],
|
||||
projectOrder: [],
|
||||
workspaceOrderByScope: {},
|
||||
})
|
||||
|
||||
expect(header.title).toBe('feat/workspace-sot')
|
||||
expect(header.subtitle).toBe('getpaseo/paseo')
|
||||
expect(sidebarProjects[0]?.workspaces[0]?.name).toBe(header.title)
|
||||
expect(sidebarProjects[0]?.workspaces[0]?.statusBucket).toBe('running')
|
||||
})
|
||||
|
||||
it('renders explicit missing state only after workspace hydration', () => {
|
||||
expect(
|
||||
shouldRenderMissingWorkspaceDescriptor({
|
||||
workspace: null,
|
||||
hasHydratedWorkspaces: true,
|
||||
})
|
||||
).toBe(true)
|
||||
|
||||
expect(
|
||||
shouldRenderMissingWorkspaceDescriptor({
|
||||
workspace: null,
|
||||
hasHydratedWorkspaces: false,
|
||||
})
|
||||
).toBe(false)
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user