Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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
11 changes: 11 additions & 0 deletions apps/mobile/src/features/home/HomeRouteScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,17 @@ export function HomeRouteScreen() {
onSelectThread={handleSelectThread}
onSelectPendingTask={openPendingTask}
onDeletePendingTask={confirmDeletePendingTask}
onNewThreadOnBranch={(thread) => {
navigation.navigate("NewTaskSheet", {
screen: "NewTaskDraft",
params: {
environmentId: String(thread.environmentId),
projectId: String(thread.projectId),
branch: thread.branch,
worktreePath: thread.worktreePath,
},
});
}}
onNewThreadInProject={(project) => {
navigation.navigate("NewTaskSheet", {
screen: "NewTaskDraft",
Expand Down
5 changes: 5 additions & 0 deletions apps/mobile/src/features/home/HomeScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,7 @@ interface HomeScreenProps {
readonly onRegenerateThreadTitle: (thread: EnvironmentThreadShell) => Promise<boolean>;
readonly onSelectPendingTask: (pendingTask: PendingNewTask) => void;
readonly onDeletePendingTask: (pendingTask: PendingNewTask) => void;
readonly onNewThreadOnBranch: (thread: EnvironmentThreadShell) => void;
readonly onNewThreadInProject: (project: EnvironmentProject) => void;
}

Expand Down Expand Up @@ -824,6 +825,7 @@ export function HomeScreen(props: HomeScreenProps) {
const movedId = `${thread.environmentId}:${thread.id}`;
return (
<ThreadListV2Row
onNewThreadOnBranch={props.onNewThreadOnBranch}
thread={thread}
variant={item.item.variant}
hasQueuedMessages={queuedThreadKeys.has(movedId)}
Expand Down Expand Up @@ -911,6 +913,7 @@ export function HomeScreen(props: HomeScreenProps) {
props.onDeletePendingTask,
props.onSelectPendingTask,
props.onSelectThread,
props.onNewThreadOnBranch,
props.savedConnectionsById,
serverConfigs,
shelfPreferencesLoaded,
Expand Down Expand Up @@ -1001,6 +1004,7 @@ export function HomeScreen(props: HomeScreenProps) {
const thread = item.thread;
return (
<ThreadListRow
onNewThreadOnBranch={props.onNewThreadOnBranch}
variant="compact"
thread={thread}
hasQueuedMessages={queuedThreadKeys.has(`${thread.environmentId}:${thread.id}`)}
Expand Down Expand Up @@ -1050,6 +1054,7 @@ export function HomeScreen(props: HomeScreenProps) {
props.onNewThreadInProject,
props.onSelectPendingTask,
props.onSelectThread,
props.onNewThreadOnBranch,
props.searchQuery,
props.savedConnectionsById,
threadSearchMatchByKey,
Expand Down
16 changes: 16 additions & 0 deletions apps/mobile/src/features/layout/AdaptiveWorkspaceLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -449,6 +449,21 @@ function AdaptiveWorkspaceLayoutContent(
});
}, [navigation]);

const handleNewThreadOnBranch = useCallback(
(thread: EnvironmentThreadShell) => {
navigation.navigate("NewTaskSheet", {
screen: "NewTaskDraft",
params: {
environmentId: String(thread.environmentId),
projectId: String(thread.projectId),
branch: thread.branch,
worktreePath: thread.worktreePath,
},
});
},
[navigation],
);

const handleNewThreadInProject = useCallback(
(project: EnvironmentProject) => {
navigation.navigate("NewTaskSheet", {
Expand Down Expand Up @@ -543,6 +558,7 @@ function AdaptiveWorkspaceLayoutContent(
onOpenSettings={handleOpenSettings}
onOpenEnvironmentSettings={handleOpenEnvironmentSettings}
onNewThreadInProject={handleNewThreadInProject}
onNewThreadOnBranch={handleNewThreadOnBranch}
onSelectThread={handleSelectThread}
onSearchQueryChange={setPrimarySidebarSearchQuery}
searchQuery={primarySidebarSearchQuery}
Expand Down
46 changes: 16 additions & 30 deletions apps/mobile/src/features/threads/NewTaskContextPickerScreens.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ import {
NATIVE_MAIL_SEARCH_TOOLBAR_SUPPORTED,
} from "../layout/native-mail-search-toolbar";
import { branchBadgeLabel, useNewTaskFlow } from "./new-task-flow-provider";
import { shouldCheckoutNewTaskBranch } from "./new-task-context-presentation";
import { checkoutNewTaskBranch } from "./checkout-new-task-branch";

function SelectionRow(props: {
readonly icon?: "arrow.triangle.branch" | ReactNode;
Expand Down Expand Up @@ -257,43 +257,29 @@ export function NewTaskBranchPickerRouteScreen() {
void Haptics.selectionAsync();

try {
let selectedBranch = branch;
const needsCheckout = shouldCheckoutNewTaskBranch({
branchIsCurrent: branch.current,
branchWorktreePath: branch.worktreePath,
if (!flow.selectedProject) return;
setSwitchingBranchName(branch.name);
const result = await checkoutNewTaskBranch({
branch,
project: flow.selectedProject,
workspaceMode: flow.workspaceMode,
switchRef,
});
if (needsCheckout && flow.selectedProject) {
setSwitchingBranchName(branch.name);
const result = await switchRef({
environmentId: flow.selectedProject.environmentId,
input: {
cwd: flow.selectedProject.workspaceRoot,
refName: branch.name,
},
});
if (result._tag === "Failure") {
if (mountedRef.current && navigation.isFocused() && !isAtomCommandInterrupted(result)) {
const error = squashAtomCommandFailure(result);
Alert.alert(
"Could not switch branch",
error instanceof Error ? error.message : "The branch could not be checked out.",
);
}
return;
if (result._tag === "Failure") {
if (mountedRef.current && navigation.isFocused() && !isAtomCommandInterrupted(result)) {
const error = squashAtomCommandFailure(result);
Alert.alert(
"Could not switch branch",
error instanceof Error ? error.message : "The branch could not be checked out.",
);
}
selectedBranch = {
...branch,
current: true,
isRemote: false,
name: result.value.refName ?? branch.name,
};
return;
}

// The checkout has already changed the repository. Persist the matching
// draft selection even if the native sheet was dismissed while the
// command was in flight; only visible-screen work is focus-gated below.
flow.selectBranch(selectedBranch);
flow.selectBranch(result.value);
if (!mountedRef.current || !navigation.isFocused()) {
return;
}
Expand Down
115 changes: 101 additions & 14 deletions apps/mobile/src/features/threads/NewTaskDraftRouteScreen.tsx
Original file line number Diff line number Diff line change
@@ -1,20 +1,39 @@
import type { StaticScreenProps } from "@react-navigation/native";
import { useMemo } from "react";
import { useNavigation, type StaticScreenProps } from "@react-navigation/native";
import { useEffect, useMemo, useState } from "react";
import { Alert, View } from "react-native";
import {
isAtomCommandInterrupted,
squashAtomCommandFailure,
} from "@t3tools/client-runtime/state/runtime";
import { AppText as Text } from "../../components/AppText";
import { useProjects } from "../../state/entities";
import { useAtomCommand } from "../../state/use-atom-command";
import { vcsEnvironment } from "../../state/vcs";
import { checkoutNewTaskBranch } from "./checkout-new-task-branch";
import { NativeStackScreenOptions } from "../../native/StackHeader";

import { NewTaskDraftScreen } from "./NewTaskDraftScreen";

type NewTaskDraftRouteParams = {
readonly environmentId?: string | string[];
readonly projectId?: string | string[];
readonly branch?: string | null;
readonly worktreePath?: string | null;
readonly title?: string | string[];
readonly pendingTaskId?: string | string[];
readonly draftId?: string | string[];
readonly incomingShareId?: string | string[];
};

export function NewTaskDraftRouteScreen({ route }: StaticScreenProps<NewTaskDraftRouteParams>) {
const params = route.params ?? {};
const params = useMemo(() => route.params ?? {}, [route.params]);
const pendingTaskId = Array.isArray(params.pendingTaskId)
? params.pendingTaskId[0]
: params.pendingTaskId;
const draftId = Array.isArray(params.draftId) ? params.draftId[0] : params.draftId;
const projects = useProjects();
const navigation = useNavigation();
const switchRef = useAtomCommand(vcsEnvironment.switchRef, { reportFailure: false });

// Keyed on the params object so a fresh navigation to this (already
// mounted) screen produces a new reference, letting the draft screen
Expand All @@ -25,27 +44,95 @@ export function NewTaskDraftRouteScreen({ route }: StaticScreenProps<NewTaskDraf
? params.environmentId[0]
: params.environmentId,
projectId: Array.isArray(params.projectId) ? params.projectId[0] : params.projectId,
branch: params.branch,
worktreePath: params.worktreePath,
}),
[route.params],
[params],
);

const [preparedProject, setPreparedProject] = useState<{
request: typeof initialProjectRef;
projectRef: typeof initialProjectRef;
workspaceRoot: string | undefined;
} | null>(null);
const project = projects.find(
(candidate) =>
candidate.environmentId === initialProjectRef.environmentId &&
candidate.id === initialProjectRef.projectId,
);
const environmentId = project?.environmentId;
const workspaceRoot = project?.workspaceRoot;
const needsPreparation = Boolean(initialProjectRef.branch && !pendingTaskId && !draftId);

useEffect(() => {
if (!needsPreparation || !initialProjectRef.branch) return;
let active = true;
void checkoutNewTaskBranch({
Comment thread
StiensWout marked this conversation as resolved.
Outdated
Comment thread
macroscopeapp[bot] marked this conversation as resolved.
Outdated
// A thread's branch is historical; only switchRef can establish that
// the shared project checkout now matches it.
branch: {
name: initialProjectRef.branch,
current: false,
isDefault: false,
worktreePath: initialProjectRef.worktreePath ?? null,
},
project: environmentId && workspaceRoot ? { environmentId, workspaceRoot } : null,
Comment thread
StiensWout marked this conversation as resolved.
Outdated
workspaceMode: "local",
switchRef,
}).then((result) => {
if (!active) return;
if (result._tag === "Failure") {
if (!isAtomCommandInterrupted(result)) {
const error = squashAtomCommandFailure(result);
Alert.alert(
"Could not switch branch",
error instanceof Error ? error.message : "The branch could not be checked out.",
);
}
navigation.goBack();
return;
}
setPreparedProject({
request: initialProjectRef,
projectRef: { ...initialProjectRef, branch: result.value.name },
workspaceRoot,
});
});
return () => {
active = false;
};
}, [environmentId, workspaceRoot, initialProjectRef, needsPreparation, navigation, switchRef]);

// Do not mount the composer, restore its draft, or expose send/queue actions
// until this exact navigation's checkout has succeeded.
const prepared =
preparedProject?.request === initialProjectRef &&
preparedProject.workspaceRoot === workspaceRoot;
const preparingBranch = needsPreparation && !prepared;
Comment thread
StiensWout marked this conversation as resolved.
Outdated

return (
<>
<NativeStackScreenOptions
options={{
title: Array.isArray(params.title) ? params.title[0] : (params.title ?? "New task"),
}}
/>
<NewTaskDraftScreen
initialProjectRef={initialProjectRef}
incomingShareId={
Array.isArray(params.incomingShareId) ? params.incomingShareId[0] : params.incomingShareId
}
pendingTaskId={
Array.isArray(params.pendingTaskId) ? params.pendingTaskId[0] : params.pendingTaskId
}
draftId={Array.isArray(params.draftId) ? params.draftId[0] : params.draftId}
/>
{preparingBranch ? (
<View className="flex-1 items-center justify-center bg-screen">
<Text className="text-foreground">Switching branch...</Text>
</View>
) : (
<NewTaskDraftScreen
initialProjectRef={prepared ? preparedProject.projectRef : initialProjectRef}
incomingShareId={
Array.isArray(params.incomingShareId)
? params.incomingShareId[0]
: params.incomingShareId
}
pendingTaskId={pendingTaskId}
draftId={draftId}
/>
)}
</>
);
}
24 changes: 24 additions & 0 deletions apps/mobile/src/features/threads/NewTaskDraftScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ import {
getComposerDraftSnapshot,
mergeComposerDraftContent,
restoreComposerDraftSnapshot,
updateComposerDraftSettings,
scheduleUnusedComposerAttachmentCleanup,
type ComposerDraft,
waitForComposerDraftsLoaded,
Expand Down Expand Up @@ -149,6 +150,8 @@ export function NewTaskDraftScreen(props: {
readonly initialProjectRef?: {
readonly environmentId?: string;
readonly projectId?: string;
readonly branch?: string | null;
readonly worktreePath?: string | null;
};
/** Queued outbox message id when editing an existing pending task. */
readonly pendingTaskId?: string;
Expand Down Expand Up @@ -536,6 +539,26 @@ export function NewTaskDraftScreen(props: {
if (appliedInitialProjectKeyRef.current === directProjectKey) {
return;
}
if (props.initialProjectRef?.branch) {
if (
selectedProject?.environmentId !== directProject.environmentId ||
selectedProject.id !== directProject.id
) {
setProject(directProject);
return;
}
if (!flow.draftKey) return;
// The route completes checkout before mounting this composer. Local
// mode reuses an existing worktree; worktree mode would create another.
updateComposerDraftSettings(flow.draftKey, {
workspaceSelection: {
mode: "local",
branch: props.initialProjectRef.branch,
worktreePath: props.initialProjectRef.worktreePath ?? null,
startFromOrigin: false,
},
});
}
appliedInitialProjectKeyRef.current = directProjectKey;
if (
selectedProject?.environmentId === directProject.environmentId &&
Expand Down Expand Up @@ -569,6 +592,7 @@ export function NewTaskDraftScreen(props: {
}, [
projectScopes,
projects,
flow.draftKey,
props.initialProjectRef,
props.incomingShareId,
props.pendingTaskId,
Expand Down
4 changes: 4 additions & 0 deletions apps/mobile/src/features/threads/ThreadNavigationSidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,7 @@ interface ThreadNavigationSidebarProps {
readonly selectedThreadKey: string | null;
readonly onOpenSettings: () => void;
readonly onOpenEnvironmentSettings: () => void;
readonly onNewThreadOnBranch: (thread: EnvironmentThreadShell) => void;
readonly onNewThreadInProject: (project: EnvironmentProject) => void;
readonly onSearchQueryChange: (query: string) => void;
readonly onSelectThread: (thread: EnvironmentThreadShell) => void;
Expand Down Expand Up @@ -890,6 +891,7 @@ function ThreadNavigationSidebarPane(
const scopeKey = scopedProjectKey(thread.environmentId, thread.projectId);
return (
<ThreadListV2Row
onNewThreadOnBranch={props.onNewThreadOnBranch}
thread={thread}
variant={item.item.variant}
hasQueuedMessages={queuedThreadKeys.has(`${thread.environmentId}:${thread.id}`)}
Expand Down Expand Up @@ -1024,6 +1026,7 @@ function ThreadNavigationSidebarPane(
const thread = item.thread;
return (
<ThreadListRow
onNewThreadOnBranch={props.onNewThreadOnBranch}
variant="sidebar"
thread={thread}
hasQueuedMessages={queuedThreadKeys.has(`${thread.environmentId}:${thread.id}`)}
Expand Down Expand Up @@ -1087,6 +1090,7 @@ function ThreadNavigationSidebarPane(
projectTitleByProjectKey,
regenerateThreadTitle,
props.onNewThreadInProject,
props.onNewThreadOnBranch,
props.searchQuery,
props.selectedThreadKey,
props.width,
Expand Down
Loading
Loading