From 416db90795a523def961d4d84668e30006beaa53 Mon Sep 17 00:00:00 2001 From: Julius Marminge <51714798+juliusmarminge@users.noreply.github.com> Date: Wed, 7 Oct 2026 00:16:32 -0700 Subject: [PATCH 1/4] =?UTF-8?q?feat(web,mobile):=20Continue=20on=E2=80=A6?= =?UTF-8?q?=20a=20linked=20environment?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Moving a thread to a linked environment was reachable only through the t3_thread_handoff MCP tool. Users now pick "Continue on…" from the web sidebar and chat-header thread menus, the command palette, and mobile's long-press and thread header menus. Targets are read from threadHandoff.options when the menu opens. The thread shows the move's state above the composer: waiting for the turn to end (with Keep it here), under way, moved (with Open on ), or failed. While the thread is departing or departed, the composer is closed on both clients. Co-Authored-By: Claude Opus 5.5 (1M context) --- apps/mobile/src/Stack.tsx | 11 ++ apps/mobile/src/features/home/HomeScreen.tsx | 3 + .../features/threads/ThreadDetailScreen.tsx | 18 ++- .../features/threads/ThreadGitControls.tsx | 14 ++ .../features/threads/ThreadHandoffCard.tsx | 94 ++++++++++++ .../features/threads/ThreadHandoffSheet.tsx | 145 ++++++++++++++++++ .../src/features/threads/ThreadHeader.tsx | 10 +- .../threads/ThreadNavigationSidebar.tsx | 3 + .../features/threads/ThreadRouteScreen.tsx | 11 ++ .../features/threads/thread-list-v2-items.tsx | 19 ++- .../src/state/thread-list-environments.ts | 4 + apps/mobile/src/state/threadHandoff.ts | 5 + apps/web/src/components/ChatView.tsx | 32 ++-- apps/web/src/components/CommandPalette.tsx | 44 ++++++ apps/web/src/components/Sidebar.tsx | 20 +++ .../components/chat/ThreadHandoffBanner.tsx | 100 ++++++++++++ .../components/threadActionMenu.logic.test.ts | 42 +++++ .../src/components/threadActionMenu.logic.ts | 26 ++++ apps/web/src/components/threadHandoffMenu.ts | 79 ++++++++++ .../web/src/hooks/useThreadActionMenu.test.ts | 5 + apps/web/src/hooks/useThreadActionMenu.ts | 20 +++ apps/web/src/state/threadHandoff.ts | 5 + docs/user/remote-access.md | 4 +- packages/client-runtime/src/state/models.ts | 3 + .../src/state/peerLinks.test.ts | 28 +++- .../client-runtime/src/state/peerLinks.ts | 61 +++++++- vite.config.ts | 1 + 27 files changed, 789 insertions(+), 18 deletions(-) create mode 100644 apps/mobile/src/features/threads/ThreadHandoffCard.tsx create mode 100644 apps/mobile/src/features/threads/ThreadHandoffSheet.tsx create mode 100644 apps/mobile/src/state/threadHandoff.ts create mode 100644 apps/web/src/components/chat/ThreadHandoffBanner.tsx create mode 100644 apps/web/src/components/threadHandoffMenu.ts create mode 100644 apps/web/src/state/threadHandoff.ts diff --git a/apps/mobile/src/Stack.tsx b/apps/mobile/src/Stack.tsx index 00645ddc091a..8ac49aba7a99 100644 --- a/apps/mobile/src/Stack.tsx +++ b/apps/mobile/src/Stack.tsx @@ -68,6 +68,7 @@ import { GitCommitSheet } from "./features/threads/git/GitCommitSheet"; import { GitConfirmSheet } from "./features/threads/git/GitConfirmSheet"; import { GitOverviewSheet } from "./features/threads/git/GitOverviewSheet"; import { ThreadAgentsSheet } from "./features/threads/ThreadAgentsSheet"; +import { ThreadHandoffSheet } from "./features/threads/ThreadHandoffSheet"; import { ThreadQueueSheet } from "./features/threads/ThreadQueueControl"; import { ThreadRouteScreen } from "./features/threads/ThreadRouteScreen"; import { McpAppFullscreenScreen } from "./features/threads/McpAppFullscreenScreen"; @@ -550,6 +551,7 @@ const WORKSPACE_OVERLAY_ROUTES = new Set([ "SettingsLegal", "SettingsSheet", "ThreadAgents", + "ThreadHandoff", "ThreadQueue", "ThreadReviewComment", "ThreadDevicePreview", @@ -862,6 +864,15 @@ const RootStackConfig = createWorkspaceStackNavigator({ sheetGrabberVisible: true, }, }), + ThreadHandoff: createNativeStackScreen({ + screen: ThreadHandoffSheet, + options: { + ...FORM_SHEET_PRESENTATION_OPTIONS, + headerShown: false, + sheetAllowedDetents: [0.5, 0.9], + sheetGrabberVisible: true, + }, + }), GitOverview: createNativeStackScreen({ screen: GitOverviewSheet, linking: `${THREAD_LINKING_PREFIX}/git`, diff --git a/apps/mobile/src/features/home/HomeScreen.tsx b/apps/mobile/src/features/home/HomeScreen.tsx index a6f098a05ea4..f1c93b20d8ea 100644 --- a/apps/mobile/src/features/home/HomeScreen.tsx +++ b/apps/mobile/src/features/home/HomeScreen.tsx @@ -528,6 +528,7 @@ export function HomeScreen(props: HomeScreenProps) { pinReorderEnvironmentIds, activeReorderEnvironmentIds, titleRegenerationEnvironmentIds, + handoffEnvironmentIds, } = listEnvironments; const resolveProviderInstance = useThreadRowProviderInstanceResolver(providersByEnvironmentId); const pendingOrder = usePendingThreadOrder(nowMinute, snoozeWakeTick); @@ -774,6 +775,7 @@ export function HomeScreen(props: HomeScreenProps) { onRenameThread={handleRenameThread} onRegenerateThreadTitle={handleRegenerateThreadTitle} titleRegenerationSupported={titleRegenerationEnvironmentIds.has(thread.environmentId)} + handoffSupported={handoffEnvironmentIds.has(thread.environmentId)} settlementSupported={settlementEnvironmentIds.has(thread.environmentId)} onSettleThread={handleSettleThread} snoozeSupported={snoozeEnvironmentIds.has(thread.environmentId)} @@ -837,6 +839,7 @@ export function HomeScreen(props: HomeScreenProps) { snoozeEnvironmentIds, threadSearchMatchByKey, titleRegenerationEnvironmentIds, + handoffEnvironmentIds, toggleSettledShelf, toggleSnoozedShelf, toggleWorkingShelf, diff --git a/apps/mobile/src/features/threads/ThreadDetailScreen.tsx b/apps/mobile/src/features/threads/ThreadDetailScreen.tsx index 6bc5084e0d53..52d4efea3379 100644 --- a/apps/mobile/src/features/threads/ThreadDetailScreen.tsx +++ b/apps/mobile/src/features/threads/ThreadDetailScreen.tsx @@ -2,6 +2,7 @@ import * as Dialog from "../../components/ui/Dialog"; import { useAtomValue } from "@effect/atom-react"; import { useChildThreadInputs, useThreadReportedModelSelection } from "../../state/entities"; import { UsageLimitRecoveryCard } from "./UsageLimitRecoveryCard"; +import { ThreadHandoffCard, threadHandoffClosesComposer } from "./ThreadHandoffCard"; import { useNavigation } from "@react-navigation/native"; import type { WorktreeSetupCardProps } from "./worktree-setup-card"; import type { ComposerTextPaste } from "../../native/T3ComposerEditor.types"; @@ -562,7 +563,9 @@ export const ThreadDetailScreen = memo(function ThreadDetailScreen(props: Thread useState(null); const activeUserInputRequestId = props.activePendingUserInput?.requestId ?? null; const composerSlotHidden = - activeUserInputRequestId !== null || props.creationState?.kind === "failed"; + activeUserInputRequestId !== null || + props.creationState?.kind === "failed" || + threadHandoffClosesComposer(props.selectedThread); const [usageLimitsPanel, setUsageLimitsPanel] = useState<{ readonly key: string; readonly threadKey: string; @@ -1215,6 +1218,16 @@ export const ThreadDetailScreen = memo(function ThreadDetailScreen(props: Thread /> ) : null} + + +