feat: projects → workspaces → tabs
This commit is contained in:
@@ -105,9 +105,15 @@ function logAgentExplorer(event: string, details: Record<string, unknown>): void
|
||||
export function AgentReadyScreen({
|
||||
serverId,
|
||||
agentId,
|
||||
showHeader = true,
|
||||
showExplorerSidebar = true,
|
||||
wrapWithExplorerSidebarProvider = true,
|
||||
}: {
|
||||
serverId: string;
|
||||
agentId: string;
|
||||
showHeader?: boolean;
|
||||
showExplorerSidebar?: boolean;
|
||||
wrapWithExplorerSidebarProvider?: boolean;
|
||||
}) {
|
||||
const router = useRouter();
|
||||
const resolvedAgentId = agentId?.trim() || undefined;
|
||||
@@ -193,17 +199,23 @@ export function AgentReadyScreen({
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<ExplorerSidebarAnimationProvider>
|
||||
<AgentScreenContent
|
||||
serverId={resolvedServerId}
|
||||
agentId={resolvedAgentId}
|
||||
client={runtimeClient}
|
||||
isConnected={runtimeIsConnected}
|
||||
connectionStatus={connectionStatus}
|
||||
/>
|
||||
</ExplorerSidebarAnimationProvider>
|
||||
const content = (
|
||||
<AgentScreenContent
|
||||
serverId={resolvedServerId}
|
||||
agentId={resolvedAgentId}
|
||||
client={runtimeClient}
|
||||
isConnected={runtimeIsConnected}
|
||||
connectionStatus={connectionStatus}
|
||||
showHeader={showHeader}
|
||||
showExplorerSidebar={showExplorerSidebar}
|
||||
/>
|
||||
);
|
||||
|
||||
if (!wrapWithExplorerSidebarProvider) {
|
||||
return content;
|
||||
}
|
||||
|
||||
return <ExplorerSidebarAnimationProvider>{content}</ExplorerSidebarAnimationProvider>;
|
||||
}
|
||||
|
||||
type AgentScreenContentProps = {
|
||||
@@ -212,6 +224,8 @@ type AgentScreenContentProps = {
|
||||
client: DaemonClient;
|
||||
isConnected: boolean;
|
||||
connectionStatus: HostRuntimeConnectionStatus;
|
||||
showHeader: boolean;
|
||||
showExplorerSidebar: boolean;
|
||||
};
|
||||
|
||||
function toErrorMessage(error: unknown): string {
|
||||
@@ -231,6 +245,8 @@ function AgentScreenContent({
|
||||
client,
|
||||
isConnected,
|
||||
connectionStatus,
|
||||
showHeader,
|
||||
showExplorerSidebar,
|
||||
}: AgentScreenContentProps) {
|
||||
const { theme } = useUnistyles();
|
||||
const toast = useToast();
|
||||
@@ -990,7 +1006,7 @@ function AgentScreenContent({
|
||||
if (viewState.tag === "not_found") {
|
||||
return (
|
||||
<View style={styles.container} testID="agent-not-found">
|
||||
<MenuHeader title="Agent" />
|
||||
{showHeader ? <MenuHeader title="Agent" /> : null}
|
||||
<View style={styles.errorContainer}>
|
||||
<Text style={styles.errorText}>Agent not found</Text>
|
||||
</View>
|
||||
@@ -1001,7 +1017,7 @@ function AgentScreenContent({
|
||||
if (viewState.tag === "error") {
|
||||
return (
|
||||
<View style={styles.container} testID="agent-load-error">
|
||||
<MenuHeader title="Agent" />
|
||||
{showHeader ? <MenuHeader title="Agent" /> : null}
|
||||
<View style={styles.errorContainer}>
|
||||
<Text style={styles.errorText}>Failed to load agent</Text>
|
||||
<Text style={styles.statusText}>{viewState.message}</Text>
|
||||
@@ -1013,7 +1029,7 @@ function AgentScreenContent({
|
||||
if (viewState.tag === "boot" || !effectiveAgent) {
|
||||
return (
|
||||
<View style={styles.container} testID="agent-loading">
|
||||
<MenuHeader title="Agent" />
|
||||
{showHeader ? <MenuHeader title="Agent" /> : null}
|
||||
<View style={styles.errorContainer}>
|
||||
<ActivityIndicator size="large" color={theme.colors.foregroundMuted} />
|
||||
</View>
|
||||
@@ -1029,80 +1045,81 @@ function AgentScreenContent({
|
||||
>
|
||||
<View style={styles.container}>
|
||||
{/* Header */}
|
||||
<MenuHeader
|
||||
title={effectiveAgent.title || "Agent"}
|
||||
rightContent={
|
||||
<View style={styles.headerRightContent}>
|
||||
<HeaderToggleButton
|
||||
onPress={handleToggleExplorer}
|
||||
tooltipLabel="Toggle explorer"
|
||||
tooltipKeys={["mod", "E"]}
|
||||
tooltipSide="left"
|
||||
style={styles.menuButton}
|
||||
accessible
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel={isExplorerOpen ? "Close explorer" : "Open explorer"}
|
||||
accessibilityState={{ expanded: isExplorerOpen }}
|
||||
>
|
||||
{isMobile ? (
|
||||
checkout?.isGit ? (
|
||||
<GitBranch
|
||||
size={theme.iconSize.lg}
|
||||
color={
|
||||
isExplorerOpen
|
||||
? theme.colors.foreground
|
||||
: theme.colors.foregroundMuted
|
||||
}
|
||||
/>
|
||||
{showHeader ? (
|
||||
<MenuHeader
|
||||
title={effectiveAgent.title || "Agent"}
|
||||
rightContent={
|
||||
<View style={styles.headerRightContent}>
|
||||
<HeaderToggleButton
|
||||
onPress={handleToggleExplorer}
|
||||
tooltipLabel="Toggle explorer"
|
||||
tooltipKeys={["mod", "E"]}
|
||||
tooltipSide="left"
|
||||
style={styles.menuButton}
|
||||
accessible
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel={isExplorerOpen ? "Close explorer" : "Open explorer"}
|
||||
accessibilityState={{ expanded: isExplorerOpen }}
|
||||
>
|
||||
{isMobile ? (
|
||||
checkout?.isGit ? (
|
||||
<GitBranch
|
||||
size={theme.iconSize.lg}
|
||||
color={
|
||||
isExplorerOpen
|
||||
? theme.colors.foreground
|
||||
: theme.colors.foregroundMuted
|
||||
}
|
||||
/>
|
||||
) : (
|
||||
<Folder
|
||||
size={theme.iconSize.lg}
|
||||
color={
|
||||
isExplorerOpen
|
||||
? theme.colors.foreground
|
||||
: theme.colors.foregroundMuted
|
||||
}
|
||||
/>
|
||||
)
|
||||
) : (
|
||||
<Folder
|
||||
size={theme.iconSize.lg}
|
||||
<PanelRight
|
||||
size={theme.iconSize.md}
|
||||
color={
|
||||
isExplorerOpen
|
||||
? theme.colors.foreground
|
||||
: theme.colors.foregroundMuted
|
||||
}
|
||||
/>
|
||||
)
|
||||
) : (
|
||||
<PanelRight
|
||||
size={theme.iconSize.md}
|
||||
color={
|
||||
isExplorerOpen
|
||||
? theme.colors.foreground
|
||||
: theme.colors.foregroundMuted
|
||||
)}
|
||||
</HeaderToggleButton>
|
||||
<DropdownMenu
|
||||
onOpenChange={(open) => {
|
||||
if (open && agent?.cwd) {
|
||||
checkoutStatusQuery.refresh().catch(() => {});
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</HeaderToggleButton>
|
||||
<DropdownMenu
|
||||
onOpenChange={(open) => {
|
||||
if (open && agent?.cwd) {
|
||||
checkoutStatusQuery.refresh().catch(() => {});
|
||||
}
|
||||
}}
|
||||
>
|
||||
<DropdownMenuTrigger testID="agent-overflow-menu" style={styles.menuButton}>
|
||||
<MoreVertical
|
||||
size={isMobile ? theme.iconSize.lg : theme.iconSize.md}
|
||||
color={theme.colors.foregroundMuted}
|
||||
/>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end" width={DROPDOWN_WIDTH} testID="agent-overflow-content">
|
||||
<View style={styles.menuMetaContainer} testID="agent-details-sheet">
|
||||
<Pressable
|
||||
testID="agent-details-agent-id"
|
||||
style={({ hovered, pressed }) => [
|
||||
styles.menuMetaRow,
|
||||
(hovered || pressed) && styles.menuMetaRowActive,
|
||||
]}
|
||||
onPress={() => {
|
||||
void handleCopyMeta("Directory", effectiveAgent.cwd);
|
||||
}}
|
||||
>
|
||||
<Text style={styles.menuMetaLabel} numberOfLines={1}>
|
||||
Directory
|
||||
</Text>
|
||||
}}
|
||||
>
|
||||
<DropdownMenuTrigger testID="agent-overflow-menu" style={styles.menuButton}>
|
||||
<MoreVertical
|
||||
size={isMobile ? theme.iconSize.lg : theme.iconSize.md}
|
||||
color={theme.colors.foregroundMuted}
|
||||
/>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end" width={DROPDOWN_WIDTH} testID="agent-overflow-content">
|
||||
<View style={styles.menuMetaContainer} testID="agent-details-sheet">
|
||||
<Pressable
|
||||
testID="agent-details-agent-id"
|
||||
style={({ hovered, pressed }) => [
|
||||
styles.menuMetaRow,
|
||||
(hovered || pressed) && styles.menuMetaRowActive,
|
||||
]}
|
||||
onPress={() => {
|
||||
void handleCopyMeta("Directory", effectiveAgent.cwd);
|
||||
}}
|
||||
>
|
||||
<Text style={styles.menuMetaLabel} numberOfLines={1}>
|
||||
Directory
|
||||
</Text>
|
||||
<Text
|
||||
testID="agent-details-agent-id-value"
|
||||
style={styles.menuMetaValue}
|
||||
@@ -1224,11 +1241,12 @@ function AgentScreenContent({
|
||||
>
|
||||
{isInitializing ? "Refreshing..." : "Refresh"}
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</View>
|
||||
}
|
||||
/>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</View>
|
||||
}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
{/* Content Area with Keyboard Animation */}
|
||||
<View style={styles.contentContainer}>
|
||||
@@ -1272,14 +1290,14 @@ function AgentScreenContent({
|
||||
</FileDropZone>
|
||||
|
||||
{/* Explorer Sidebar - Desktop: inline, Mobile: overlay */}
|
||||
{!isMobile && isExplorerOpen && resolvedAgentId && (
|
||||
{showExplorerSidebar && !isMobile && isExplorerOpen && resolvedAgentId ? (
|
||||
<ExplorerSidebar
|
||||
serverId={serverId}
|
||||
agentId={resolvedAgentId}
|
||||
cwd={effectiveAgent.cwd}
|
||||
workspaceId={effectiveAgent.cwd}
|
||||
workspaceRoot={effectiveAgent.cwd}
|
||||
isGit={isGitCheckout}
|
||||
/>
|
||||
)}
|
||||
) : null}
|
||||
|
||||
{isArchivingCurrentAgent ? (
|
||||
<View style={styles.archivingOverlay} testID="agent-archiving-overlay">
|
||||
@@ -1304,14 +1322,14 @@ function AgentScreenContent({
|
||||
)}
|
||||
|
||||
{/* Mobile Explorer Sidebar Overlay */}
|
||||
{isMobile && resolvedAgentId && (
|
||||
{showExplorerSidebar && isMobile && resolvedAgentId ? (
|
||||
<ExplorerSidebar
|
||||
serverId={serverId}
|
||||
agentId={resolvedAgentId}
|
||||
cwd={effectiveAgent.cwd}
|
||||
workspaceId={effectiveAgent.cwd}
|
||||
workspaceRoot={effectiveAgent.cwd}
|
||||
isGit={isGitCheckout}
|
||||
/>
|
||||
)}
|
||||
) : null}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1342,8 +1342,8 @@ function DraftAgentScreenContent({
|
||||
{!isMobile && isExplorerOpen && explorerServerId && draftExplorerCheckout ? (
|
||||
<ExplorerSidebar
|
||||
serverId={explorerServerId}
|
||||
agentId={DRAFT_AGENT_ID}
|
||||
cwd={draftExplorerCheckout.cwd}
|
||||
workspaceId={draftExplorerCheckout.cwd}
|
||||
workspaceRoot={draftExplorerCheckout.cwd}
|
||||
isGit={explorerIsGit}
|
||||
/>
|
||||
) : null}
|
||||
@@ -1365,8 +1365,8 @@ function DraftAgentScreenContent({
|
||||
{isMobile && explorerServerId && draftExplorerCheckout ? (
|
||||
<ExplorerSidebar
|
||||
serverId={explorerServerId}
|
||||
agentId={DRAFT_AGENT_ID}
|
||||
cwd={draftExplorerCheckout.cwd}
|
||||
workspaceId={draftExplorerCheckout.cwd}
|
||||
workspaceRoot={draftExplorerCheckout.cwd}
|
||||
isGit={explorerIsGit}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
@@ -1,206 +0,0 @@
|
||||
import { useCallback, useEffect, useMemo } from "react";
|
||||
import type { ReactNode } from "react";
|
||||
import { ActivityIndicator, Pressable, Text, View } from "react-native";
|
||||
import { useRouter } from "expo-router";
|
||||
import { StyleSheet, useUnistyles } from "react-native-unistyles";
|
||||
import { BackHeader } from "@/components/headers/back-header";
|
||||
import { useSessionDirectory } from "@/hooks/use-session-directory";
|
||||
import { useDaemonRegistry } from "@/contexts/daemon-registry-context";
|
||||
import type { Agent } from "@/contexts/session-context";
|
||||
import {
|
||||
buildHostAgentDetailRoute,
|
||||
buildHostAgentDraftRoute,
|
||||
} from "@/utils/host-routes";
|
||||
|
||||
type AgentMatch = {
|
||||
serverId: string;
|
||||
serverLabel: string;
|
||||
agent: Agent;
|
||||
};
|
||||
|
||||
export function LegacyAgentIdScreen({ agentId }: { agentId: string }) {
|
||||
const router = useRouter();
|
||||
const { theme } = useUnistyles();
|
||||
const { daemons } = useDaemonRegistry();
|
||||
const sessionDirectory = useSessionDirectory();
|
||||
const resolvedAgentId = typeof agentId === "string" ? agentId.trim() : undefined;
|
||||
|
||||
const matches = useMemo<AgentMatch[]>(() => {
|
||||
if (!resolvedAgentId) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const results: AgentMatch[] = [];
|
||||
sessionDirectory.forEach((session, serverId) => {
|
||||
if (!session) {
|
||||
return;
|
||||
}
|
||||
const agent = session.agents.get(resolvedAgentId);
|
||||
if (!agent) {
|
||||
return;
|
||||
}
|
||||
const serverLabel =
|
||||
daemons.find((daemon) => daemon.serverId === serverId)?.label ?? serverId;
|
||||
results.push({ serverId, serverLabel, agent });
|
||||
});
|
||||
|
||||
return results;
|
||||
}, [daemons, resolvedAgentId, sessionDirectory]);
|
||||
|
||||
const hasSessions = sessionDirectory.size > 0;
|
||||
const isRedirecting = Boolean(resolvedAgentId && matches.length === 1);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isRedirecting) {
|
||||
return;
|
||||
}
|
||||
const match = matches[0];
|
||||
router.replace(buildHostAgentDetailRoute(match.serverId, match.agent.id) as any);
|
||||
}, [isRedirecting, matches, router]);
|
||||
|
||||
const handleGoDraft = useCallback(() => {
|
||||
const firstMatchServerId = matches[0]?.serverId ?? null;
|
||||
if (firstMatchServerId) {
|
||||
router.replace(buildHostAgentDraftRoute(firstMatchServerId) as any);
|
||||
return;
|
||||
}
|
||||
router.replace("/" as any);
|
||||
}, [matches, router]);
|
||||
|
||||
const handleSelectMatch = useCallback(
|
||||
(match: AgentMatch) => {
|
||||
router.replace(buildHostAgentDetailRoute(match.serverId, match.agent.id) as any);
|
||||
},
|
||||
[router]
|
||||
);
|
||||
|
||||
let body: ReactNode = null;
|
||||
|
||||
if (!resolvedAgentId) {
|
||||
body = (
|
||||
<View style={styles.centerState}>
|
||||
<Text style={styles.title}>Missing agent</Text>
|
||||
<Text style={styles.subtitle}>
|
||||
This link is missing an agent id. Go back to the Agents screen to pick one.
|
||||
</Text>
|
||||
<Pressable style={styles.primaryButton} onPress={handleGoDraft}>
|
||||
<Text style={styles.primaryButtonText}>New agent</Text>
|
||||
</Pressable>
|
||||
</View>
|
||||
);
|
||||
} else if (!hasSessions || isRedirecting) {
|
||||
body = (
|
||||
<View style={styles.centerState}>
|
||||
<ActivityIndicator size="large" color={theme.colors.primary} />
|
||||
<Text style={styles.subtitle}>
|
||||
{isRedirecting ? "Opening your agent..." : "Looking for your agent..."}
|
||||
</Text>
|
||||
</View>
|
||||
);
|
||||
} else if (matches.length === 0) {
|
||||
body = (
|
||||
<View style={styles.centerState}>
|
||||
<Text style={styles.title}>Agent not found</Text>
|
||||
<Text style={styles.subtitle}>
|
||||
We could not find {resolvedAgentId} on any host right now. Hosts reconnect automatically—open it
|
||||
again from the Agents screen after it comes back online.
|
||||
</Text>
|
||||
<Pressable style={styles.primaryButton} onPress={handleGoDraft}>
|
||||
<Text style={styles.primaryButtonText}>New agent</Text>
|
||||
</Pressable>
|
||||
</View>
|
||||
);
|
||||
} else if (matches.length > 1) {
|
||||
body = (
|
||||
<View style={styles.centerState}>
|
||||
<Text style={styles.title}>Pick a host</Text>
|
||||
<Text style={styles.subtitle}>
|
||||
Multiple hosts have an agent with this id. Choose the one you intended to open.
|
||||
</Text>
|
||||
<View style={styles.matchList}>
|
||||
{matches.map((match) => (
|
||||
<Pressable
|
||||
key={`${match.serverId}:${match.agent.id}`}
|
||||
style={styles.matchButton}
|
||||
onPress={() => handleSelectMatch(match)}
|
||||
>
|
||||
<Text style={styles.matchLabel}>{match.serverLabel}</Text>
|
||||
<Text style={styles.matchMeta} numberOfLines={1}>
|
||||
{match.agent.cwd}
|
||||
</Text>
|
||||
</Pressable>
|
||||
))}
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<BackHeader title="Agent" onBack={handleGoDraft} />
|
||||
<View style={styles.content}>{body}</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create((theme) => ({
|
||||
container: {
|
||||
flex: 1,
|
||||
backgroundColor: theme.colors.surface0,
|
||||
},
|
||||
content: {
|
||||
flex: 1,
|
||||
paddingHorizontal: theme.spacing[6],
|
||||
paddingVertical: theme.spacing[8],
|
||||
},
|
||||
centerState: {
|
||||
flex: 1,
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
gap: theme.spacing[4],
|
||||
},
|
||||
title: {
|
||||
fontSize: theme.fontSize.xl,
|
||||
fontWeight: theme.fontWeight.semibold,
|
||||
color: theme.colors.foreground,
|
||||
textAlign: "center",
|
||||
},
|
||||
subtitle: {
|
||||
fontSize: theme.fontSize.sm,
|
||||
color: theme.colors.foregroundMuted,
|
||||
textAlign: "center",
|
||||
},
|
||||
primaryButton: {
|
||||
marginTop: theme.spacing[2],
|
||||
paddingHorizontal: theme.spacing[4],
|
||||
paddingVertical: theme.spacing[3],
|
||||
borderRadius: theme.borderRadius.lg,
|
||||
backgroundColor: theme.colors.primary,
|
||||
},
|
||||
primaryButtonText: {
|
||||
color: theme.colors.primaryForeground,
|
||||
fontSize: theme.fontSize.sm,
|
||||
fontWeight: theme.fontWeight.semibold,
|
||||
},
|
||||
matchList: {
|
||||
width: "100%",
|
||||
gap: theme.spacing[3],
|
||||
},
|
||||
matchButton: {
|
||||
width: "100%",
|
||||
paddingHorizontal: theme.spacing[4],
|
||||
paddingVertical: theme.spacing[3],
|
||||
borderRadius: theme.borderRadius.lg,
|
||||
backgroundColor: theme.colors.surface2,
|
||||
},
|
||||
matchLabel: {
|
||||
fontSize: theme.fontSize.base,
|
||||
fontWeight: theme.fontWeight.semibold,
|
||||
color: theme.colors.foreground,
|
||||
},
|
||||
matchMeta: {
|
||||
marginTop: theme.spacing[1],
|
||||
fontSize: theme.fontSize.sm,
|
||||
color: theme.colors.foregroundMuted,
|
||||
},
|
||||
}));
|
||||
Reference in New Issue
Block a user