Fix bottom anchoring and Claude wake routing

This commit is contained in:
Mohamed Boudra
2026-03-08 12:54:08 +07:00
parent 0c44e7db80
commit b339c5e61d
27 changed files with 3801 additions and 326 deletions
@@ -0,0 +1,37 @@
import type { BottomAnchorRouteRequest } from "@/components/use-bottom-anchor-controller";
export type RouteBottomAnchorIntent = {
routeKey: string;
reason: BottomAnchorRouteRequest["reason"];
};
export function deriveRouteBottomAnchorIntent(input: {
cachedIntent: RouteBottomAnchorIntent | null;
routeKey: string | null;
hasAppliedAuthoritativeHistoryAtEntry: boolean;
}): RouteBottomAnchorIntent | null {
if (!input.routeKey) {
return null;
}
if (input.cachedIntent?.routeKey === input.routeKey) {
return input.cachedIntent;
}
return {
routeKey: input.routeKey,
reason: input.hasAppliedAuthoritativeHistoryAtEntry ? "resume" : "initial-entry",
};
}
export function deriveRouteBottomAnchorRequest(input: {
intent: RouteBottomAnchorIntent | null;
effectiveAgentId: string | null;
}): BottomAnchorRouteRequest | null {
if (!input.intent || !input.effectiveAgentId) {
return null;
}
return {
reason: input.intent.reason,
agentId: input.effectiveAgentId,
requestKey: `${input.intent.routeKey}:${input.intent.reason}`,
};
}
@@ -0,0 +1,70 @@
import { describe, expect, it } from "vitest";
import {
deriveRouteBottomAnchorIntent,
deriveRouteBottomAnchorRequest,
} from "./agent-ready-screen-bottom-anchor";
describe("agent-ready-screen bottom anchor intent", () => {
it("latches initial-entry on first route entry before authoritative history is applied", () => {
const intentAtEntry = deriveRouteBottomAnchorIntent({
cachedIntent: null,
routeKey: "server-1:agent-1",
hasAppliedAuthoritativeHistoryAtEntry: false,
});
const intentAfterHistoryApplies = deriveRouteBottomAnchorIntent({
cachedIntent: intentAtEntry,
routeKey: "server-1:agent-1",
hasAppliedAuthoritativeHistoryAtEntry: true,
});
expect(intentAfterHistoryApplies).toEqual({
routeKey: "server-1:agent-1",
reason: "initial-entry",
});
expect(
deriveRouteBottomAnchorRequest({
intent: intentAfterHistoryApplies,
effectiveAgentId: "agent-1",
})
).toEqual({
agentId: "agent-1",
reason: "initial-entry",
requestKey: "server-1:agent-1:initial-entry",
});
});
it("creates resume requests when revisiting an already-hydrated route", () => {
const intent = deriveRouteBottomAnchorIntent({
cachedIntent: null,
routeKey: "server-1:agent-2",
hasAppliedAuthoritativeHistoryAtEntry: true,
});
expect(
deriveRouteBottomAnchorRequest({
intent,
effectiveAgentId: "agent-2",
})
).toEqual({
agentId: "agent-2",
reason: "resume",
requestKey: "server-1:agent-2:resume",
});
});
it("does not create a request until the effective agent exists", () => {
const intent = deriveRouteBottomAnchorIntent({
cachedIntent: null,
routeKey: "server-1:agent-3",
hasAppliedAuthoritativeHistoryAtEntry: false,
});
expect(
deriveRouteBottomAnchorRequest({
intent,
effectiveAgentId: null,
})
).toBeNull();
});
});
@@ -59,6 +59,10 @@ import { shouldClearAgentAttention } from "@/utils/agent-attention";
import type { DaemonClient } from "@server/client/daemon-client";
import { useExplorerOpenGesture } from "@/hooks/use-explorer-open-gesture";
import type { ExplorerCheckoutContext } from "@/stores/panel-store";
import {
deriveRouteBottomAnchorIntent,
deriveRouteBottomAnchorRequest,
} from "./agent-ready-screen-bottom-anchor";
const EMPTY_STREAM_ITEMS: StreamItem[] = [];
const IS_DEV = Boolean((globalThis as { __DEV__?: boolean }).__DEV__);
@@ -390,12 +394,19 @@ function AgentScreenContent({
const historySyncGeneration = useSessionStore(
(state) => state.sessions[serverId]?.historySyncGeneration ?? 0
);
const hasAppliedAuthoritativeHistory = useSessionStore((state) =>
resolvedAgentId
? state.sessions[serverId]?.agentAuthoritativeHistoryApplied?.get(
resolvedAgentId
) === true
: false
);
const agentHistorySyncGeneration = useSessionStore((state) =>
resolvedAgentId
? state.sessions[serverId]?.agentHistorySyncGeneration?.get(resolvedAgentId) ?? -1
: -1
);
const hasHydratedHistoryBefore = agentHistorySyncGeneration >= 0;
const hasHydratedHistoryBefore = hasAppliedAuthoritativeHistory;
// Select raw pending permissions - filter with useMemo to avoid new Map on every render
const allPendingPermissions = useSessionStore(
@@ -432,6 +443,10 @@ function AgentScreenContent({
});
const reconnectToastArmedRef = useRef(false);
const initAttemptTokenRef = useRef(0);
const routeBottomAnchorRequestRef = useRef<{
routeKey: string;
reason: "initial-entry" | "resume";
} | null>(null);
const attentionClientRef = useRef(client);
const attentionConnectedRef = useRef(isConnected);
const setFocusedAgentId = useCallback(
@@ -658,6 +673,20 @@ function AgentScreenContent({
});
const effectiveAgent = viewState.tag === "ready" ? viewState.agent : null;
const routeEntryKey = resolvedAgentId ? `${serverId}:${resolvedAgentId}` : null;
routeBottomAnchorRequestRef.current = deriveRouteBottomAnchorIntent({
cachedIntent: routeBottomAnchorRequestRef.current,
routeKey: routeEntryKey,
hasAppliedAuthoritativeHistoryAtEntry: hasAppliedAuthoritativeHistory,
});
const routeBottomAnchorRequest = useMemo(
() =>
deriveRouteBottomAnchorRequest({
intent: routeBottomAnchorRequestRef.current,
effectiveAgentId: effectiveAgent?.id ?? null,
}),
[effectiveAgent?.id]
);
useEffect(() => {
if (!isPendingCreateForRoute || !pendingCreate) {
return;
@@ -914,6 +943,8 @@ function AgentScreenContent({
shouldUseOptimisticStream ? mergedStreamItems : streamItems
}
pendingPermissions={pendingPermissions}
routeBottomAnchorRequest={routeBottomAnchorRequest}
isAuthoritativeHistoryReady={hasAppliedAuthoritativeHistory}
/>
</ReanimatedAnimated.View>
</View>
@@ -926,7 +957,12 @@ function AgentScreenContent({
autoFocus
isSubmitLoading={showPendingCreateSubmitLoading}
onAddImages={handleAddImagesCallback}
onMessageSent={() => streamViewRef.current?.scrollToBottom()}
onComposerHeightChange={() =>
streamViewRef.current?.prepareForViewportChange()
}
onMessageSent={() =>
streamViewRef.current?.scrollToBottom("message-sent")
}
/>
)}
@@ -166,6 +166,10 @@ export function WorkspaceDesktopTabsRow({
const labelCharCap = layoutItem?.labelCharCap ?? tab.label.length;
const renderedLabel = showLabel ? tab.label.slice(0, Math.max(1, labelCharCap)) : "";
const presentation = deriveWorkspaceTabPresentation({ tab, agent: tabAgent });
const tooltipLabel =
tab.kind === "agent" && tab.titleState === "loading"
? "Loading agent title"
: presentation.label;
const contextMenuTestId = `workspace-tab-context-${tab.key}`;
const isFirstTab = index === 0;
@@ -174,107 +178,114 @@ export function WorkspaceDesktopTabsRow({
return (
<ContextMenu key={tab.key}>
<ContextMenuTrigger
testID={`workspace-tab-${tab.key}`}
enabledOnMobile={false}
style={({ hovered, pressed }) => [
styles.tab,
{
minWidth: resolvedTabWidth,
width: resolvedTabWidth,
maxWidth: resolvedTabWidth,
},
isActive && styles.tabActive,
(hovered || pressed || isCloseHovered) && styles.tabHovered,
]}
onHoverIn={() => {
setHoveredTabKey(tab.key);
}}
onHoverOut={() => {
setHoveredTabKey((current) => (current === tab.key ? null : current));
}}
onPressIn={() => {
onNavigateTab(tab.tabId);
}}
onPress={() => {
onNavigateTab(tab.tabId);
}}
accessibilityLabel={
tab.kind === "agent" && tab.titleState === "loading"
? "Loading agent title"
: tab.label
}
>
<View
{...(dragHandleProps?.attributes as any)}
{...(dragHandleProps?.listeners as any)}
ref={dragHandleProps?.setActivatorNodeRef}
style={styles.tabHandle}
<Tooltip delayDuration={400} enabledOnDesktop enabledOnMobile={false}>
<TooltipTrigger
testID={`workspace-tab-tooltip-${tab.key}`}
accessibilityRole="none"
style={styles.tabTooltipTrigger}
>
<View style={styles.tabIcon}>
<WorkspaceTabIcon presentation={presentation} active={isActive} />
</View>
{showLabel ? (
presentation.titleState === "loading" ? (
<View
style={[
styles.tabLabelSkeleton,
shouldShowCloseButton && styles.tabLabelSkeletonWithCloseButton,
]}
/>
) : (
<Text
style={[
styles.tabLabel,
isActive && styles.tabLabelActive,
shouldShowCloseButton && styles.tabLabelWithCloseButton,
]}
numberOfLines={1}
>
{renderedLabel}
</Text>
)
) : null}
</View>
{shouldShowCloseButton ? (
<Pressable
testID={
tab.kind === "agent"
? `workspace-agent-close-${tab.agentId}`
: tab.kind === "terminal"
? `workspace-terminal-close-${tab.terminalId}`
: tab.kind === "draft"
? `workspace-draft-close-${tab.draftId}`
: `workspace-file-close-${encodeFilePathForPathSegment(tab.filePath)}`
}
disabled={isClosingTab}
<ContextMenuTrigger
testID={`workspace-tab-${tab.key}`}
enabledOnMobile={false}
style={({ hovered, pressed }) => [
styles.tab,
{
minWidth: resolvedTabWidth,
width: resolvedTabWidth,
maxWidth: resolvedTabWidth,
},
isActive && styles.tabActive,
(hovered || pressed || isCloseHovered) && styles.tabHovered,
]}
onHoverIn={() => {
setHoveredTabKey(tab.key);
setHoveredCloseTabKey(tab.key);
}}
onHoverOut={() => {
setHoveredTabKey((current) => (current === tab.key ? null : current));
setHoveredCloseTabKey((current) => (current === tab.key ? null : current));
}}
onPress={(event) => {
event.stopPropagation?.();
void onCloseTab(tab.tabId);
onPressIn={() => {
onNavigateTab(tab.tabId);
}}
style={({ hovered, pressed }) => [
styles.tabCloseButton,
styles.tabCloseButtonShown,
(hovered || pressed) && styles.tabCloseButtonActive,
]}
onPress={() => {
onNavigateTab(tab.tabId);
}}
accessibilityLabel={tooltipLabel}
>
{isClosingTab ? (
<ActivityIndicator size={12} color={theme.colors.foregroundMuted} />
) : (
<X size={12} color={theme.colors.foregroundMuted} />
)}
</Pressable>
) : null}
</ContextMenuTrigger>
<View
{...(dragHandleProps?.attributes as any)}
{...(dragHandleProps?.listeners as any)}
ref={dragHandleProps?.setActivatorNodeRef}
style={styles.tabHandle}
>
<View style={styles.tabIcon}>
<WorkspaceTabIcon presentation={presentation} active={isActive} />
</View>
{showLabel ? (
presentation.titleState === "loading" ? (
<View
style={[
styles.tabLabelSkeleton,
shouldShowCloseButton && styles.tabLabelSkeletonWithCloseButton,
]}
/>
) : (
<Text
style={[
styles.tabLabel,
isActive && styles.tabLabelActive,
shouldShowCloseButton && styles.tabLabelWithCloseButton,
]}
numberOfLines={1}
>
{renderedLabel}
</Text>
)
) : null}
</View>
{shouldShowCloseButton ? (
<Pressable
testID={
tab.kind === "agent"
? `workspace-agent-close-${tab.agentId}`
: tab.kind === "terminal"
? `workspace-terminal-close-${tab.terminalId}`
: tab.kind === "draft"
? `workspace-draft-close-${tab.draftId}`
: `workspace-file-close-${encodeFilePathForPathSegment(tab.filePath)}`
}
disabled={isClosingTab}
onHoverIn={() => {
setHoveredTabKey(tab.key);
setHoveredCloseTabKey(tab.key);
}}
onHoverOut={() => {
setHoveredTabKey((current) => (current === tab.key ? null : current));
setHoveredCloseTabKey((current) => (current === tab.key ? null : current));
}}
onPress={(event) => {
event.stopPropagation?.();
void onCloseTab(tab.tabId);
}}
style={({ hovered, pressed }) => [
styles.tabCloseButton,
styles.tabCloseButtonShown,
(hovered || pressed) && styles.tabCloseButtonActive,
]}
>
{isClosingTab ? (
<ActivityIndicator size={12} color={theme.colors.foregroundMuted} />
) : (
<X size={12} color={theme.colors.foregroundMuted} />
)}
</Pressable>
) : null}
</ContextMenuTrigger>
</TooltipTrigger>
<TooltipContent side="bottom" align="center" offset={8}>
<Text style={styles.newTabTooltipText}>{tooltipLabel}</Text>
</TooltipContent>
</Tooltip>
<ContextMenuContent align="start" width={DROPDOWN_WIDTH} testID={contextMenuTestId}>
{tab.kind === "agent" ? (
@@ -426,6 +437,9 @@ const styles = StyleSheet.create((theme) => ({
paddingRight: theme.spacing[2],
paddingVertical: theme.spacing[1],
},
tabTooltipTrigger: {
flexShrink: 0,
},
tab: {
paddingHorizontal: theme.spacing[3],
paddingVertical: theme.spacing[2],
@@ -12,8 +12,10 @@ import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import * as Clipboard from "expo-clipboard";
import {
ChevronDown,
Copy,
Folder,
GitBranch,
MoreVertical,
PanelRight,
Plus,
SquareTerminal,
@@ -24,6 +26,12 @@ import { SidebarMenuToggle } from "@/components/headers/menu-header";
import { HeaderToggleButton } from "@/components/headers/header-toggle-button";
import { ScreenHeader } from "@/components/headers/screen-header";
import { Combobox } from "@/components/ui/combobox";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import {
Tooltip,
TooltipContent,
@@ -1006,6 +1014,20 @@ function WorkspaceScreenContent({
[sessionAgents, toast]
);
const handleCopyWorkspacePath = useCallback(async () => {
if (!normalizedWorkspaceId.startsWith("/")) {
toast.error("Workspace path not available");
return;
}
try {
await Clipboard.setStringAsync(normalizedWorkspaceId);
toast.copied("Workspace path");
} catch {
toast.error("Copy failed");
}
}, [normalizedWorkspaceId, toast]);
const handleBulkCloseTabs = useCallback(
async (input: { tabsToClose: WorkspaceTabDescriptor[]; title: string; logLabel: string }) => {
const { tabsToClose, title, logLabel } = input;
@@ -1252,6 +1274,7 @@ function WorkspaceScreenContent({
if (target.kind === "agent") {
return (
<AgentReadyScreen
key={`${normalizedServerId}:${normalizedWorkspaceId}:${target.agentId}`}
serverId={normalizedServerId}
agentId={target.agentId}
showExplorerSidebar={false}
@@ -1331,13 +1354,45 @@ function WorkspaceScreenContent({
}
right={
<View style={styles.headerRight}>
<DropdownMenu>
<DropdownMenuTrigger
testID="workspace-header-menu-trigger"
style={styles.headerActionButton}
accessibilityRole="button"
accessibilityLabel="Workspace actions"
>
<MoreVertical
size={theme.iconSize.md}
color={theme.colors.foregroundMuted}
/>
</DropdownMenuTrigger>
<DropdownMenuContent
align="end"
width={220}
testID="workspace-header-menu"
>
<DropdownMenuItem
testID="workspace-header-copy-path"
leading={
<Copy
size={16}
color={theme.colors.foregroundMuted}
/>
}
disabled={!normalizedWorkspaceId.startsWith("/")}
onSelect={handleCopyWorkspacePath}
>
Copy workspace path
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
<HeaderToggleButton
testID="workspace-explorer-toggle"
onPress={handleToggleExplorer}
tooltipLabel="Toggle explorer"
tooltipKeys={["mod", "E"]}
tooltipSide="left"
style={styles.menuButton}
style={styles.headerActionButton}
accessible
accessibilityRole="button"
accessibilityLabel={isExplorerOpen ? "Close explorer" : "Open explorer"}
@@ -1599,10 +1654,11 @@ const styles = StyleSheet.create((theme) => ({
headerRight: {
flexDirection: "row",
alignItems: "center",
gap: theme.spacing[2],
gap: theme.spacing[1],
},
menuButton: {
padding: theme.spacing[3],
headerActionButton: {
paddingVertical: theme.spacing[2],
paddingHorizontal: theme.spacing[2],
borderRadius: theme.borderRadius.lg,
},
newTabActions: {