Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
64 changes: 45 additions & 19 deletions apps/mobile/src/features/threads/NewTaskDraftScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,9 @@ import {
resolveProviderOptionDescriptors,
} from "../../lib/providerOptions";
import { buildThreadRoutePath } from "../../lib/routes";
import { scopedProjectKey } from "../../lib/scopedEntities";
import { MOBILE_TYPOGRAPHY } from "../../lib/typography";
import { getComposerDraftSnapshot } from "../../state/use-composer-drafts";
import { useProjects } from "../../state/entities";
import { branchBadgeLabel, useNewTaskFlow } from "./new-task-flow-provider";
import { useCreateProjectThread } from "./use-project-actions";
Expand Down Expand Up @@ -63,6 +65,7 @@ export function NewTaskDraftScreen(props: {
const controlsBottomPadding = isKeyboardVisible ? 8 : Math.max(insets.bottom, 10);
const { logicalProjects, selectedProject, setProject } = flow;
const promptInputRef = useRef<ComposerEditorHandle>(null);
const loadedBranchesProjectKeyRef = useRef<string | null>(null);

const borderColor = useThemeColor("--color-border");
const sheetFadeOpaque = colorScheme === "dark" ? "rgba(14,14,14,0.98)" : "rgba(242,242,247,0.98)";
Expand All @@ -78,6 +81,12 @@ export function NewTaskDraftScreen(props: {
) ?? null;

if (directProject) {
if (
selectedProject?.environmentId === directProject.environmentId &&
selectedProject.id === directProject.id
) {
return;
}
setProject(directProject);
return;
}
Expand Down Expand Up @@ -105,10 +114,16 @@ export function NewTaskDraftScreen(props: {

useEffect(() => {
if (!selectedProject) {
loadedBranchesProjectKeyRef.current = null;
return;
}
const projectKey = `${selectedProject.environmentId}:${selectedProject.id}`;
if (loadedBranchesProjectKeyRef.current === projectKey) {
return;
}
loadedBranchesProjectKeyRef.current = projectKey;
void flow.loadBranches();
}, [flow, selectedProject]);
}, [flow.loadBranches, selectedProject]);

useEffect(() => {
if (!selectedProject) {
Expand Down Expand Up @@ -292,11 +307,7 @@ export function NewTaskDraftScreen(props: {
if (!event.startsWith("model:")) {
return;
}
// Defer state update so the native menu dismiss animation completes
// before re-rendering the menu actions (prevents submenu jump).
setTimeout(() => {
flow.setSelectedModelKey(event.slice("model:".length));
}, 150);
flow.setSelectedModelKey(event.slice("model:".length));
}

function handleEnvironmentMenuAction(event: string) {
Expand Down Expand Up @@ -366,27 +377,42 @@ export function NewTaskDraftScreen(props: {
);

async function handleStart(): Promise<void> {
const selectedProject = flow.selectedProject;
if (!selectedProject) {
return;
}
const draft = getComposerDraftSnapshot(
`new-task:${scopedProjectKey(selectedProject.environmentId, selectedProject.id)}`,
);
const modelSelection = draft.modelSelection ?? flow.selectedModel;
const workspaceMode = draft.workspaceSelection?.mode ?? flow.workspaceMode;
const selectedBranchName = draft.workspaceSelection?.branch ?? flow.selectedBranchName;
const selectedWorktreePath =
draft.workspaceSelection?.worktreePath ?? flow.selectedWorktreePath;
const runtimeMode = draft.runtimeMode ?? flow.runtimeMode;
const interactionMode = draft.interactionMode ?? flow.interactionMode;
const initialMessageText = draft.text.trim();

if (
!flow.selectedProject ||
!flow.selectedModel ||
flow.prompt.trim().length === 0 ||
!modelSelection ||
initialMessageText.length === 0 ||
flow.submitting ||
(flow.workspaceMode === "worktree" && !flow.selectedBranchName)
(workspaceMode === "worktree" && !selectedBranchName)
) {
return;
}

flow.setSubmitting(true);
const result = await createProjectThread({
project: flow.selectedProject,
modelSelection: flow.selectedModel,
envMode: flow.workspaceMode,
branch: flow.selectedBranchName,
worktreePath: flow.workspaceMode === "worktree" ? null : flow.selectedWorktreePath,
runtimeMode: flow.runtimeMode,
interactionMode: flow.interactionMode,
initialMessageText: flow.prompt.trim(),
initialAttachments: flow.attachments,
project: selectedProject,
modelSelection,
envMode: workspaceMode,
branch: selectedBranchName,
worktreePath: workspaceMode === "worktree" ? null : selectedWorktreePath,
runtimeMode,
interactionMode,
initialMessageText,
initialAttachments: draft.attachments,
});
flow.setSubmitting(false);

Expand Down
85 changes: 17 additions & 68 deletions apps/mobile/src/features/threads/ThreadRouteScreen.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,7 @@
import { Stack, useLocalSearchParams, useRouter } from "expo-router";
import { useCallback, useMemo, useState } from "react";
import * as Option from "effect/Option";
import {
EnvironmentId,
type ModelSelection,
type ProjectScript,
type ProviderInteractionMode,
type RuntimeMode,
} from "@t3tools/contracts";
import { EnvironmentId, type ProjectScript } from "@t3tools/contracts";
import { projectScriptCwd, projectScriptRuntimeEnv } from "@t3tools/shared/projectScripts";
import { Pressable, ScrollView, Text as RNText, View } from "react-native";
import { useWorkspaceState } from "../../state/workspace";
Expand Down Expand Up @@ -79,18 +73,6 @@ export function ThreadRouteScreen() {
const gitState = useSelectedThreadGitState();
const gitActions = useSelectedThreadGitActions();
const requests = useSelectedThreadRequests();
const updateThreadMetadata = useAtomCommand(
threadEnvironment.updateMetadata,
"thread metadata update",
);
const setThreadRuntimeMode = useAtomCommand(
threadEnvironment.setRuntimeMode,
"thread runtime mode",
);
const setThreadInteractionMode = useAtomCommand(
threadEnvironment.setInteractionMode,
"thread interaction mode",
);
const interruptThreadTurn = useAtomCommand(threadEnvironment.interruptTurn, "thread interrupt");
const router = useRouter();
const params = useLocalSearchParams<{
Expand All @@ -105,6 +87,18 @@ export function ThreadRouteScreen() {
const routeConnectionState =
routeEnvironmentRuntime?.connectionState ?? (environmentId ? "available" : connectionState);
const routeConnectionError = routeEnvironmentRuntime?.connectionError ?? null;
const selectedThreadWithDraftSettings = useMemo(
() =>
selectedThread
? {
...selectedThread,
modelSelection: composer.modelSelection ?? selectedThread.modelSelection,
runtimeMode: composer.runtimeMode ?? selectedThread.runtimeMode,
interactionMode: composer.interactionMode ?? selectedThread.interactionMode,
}
: null,
[composer.interactionMode, composer.modelSelection, composer.runtimeMode, selectedThread],
);

/* ─── Native header theming ──────────────────────────────────────── */
const iconColor = String(useThemeColor("--color-icon"));
Expand Down Expand Up @@ -157,51 +151,6 @@ export function ThreadRouteScreen() {
const handleOpenConnectionEditor = useCallback(() => {
void router.push("/connections");
}, [router]);
const handleUpdateThreadModelSelection = useCallback(
(modelSelection: ModelSelection) => {
if (!selectedThread) {
return;
}
return updateThreadMetadata({
environmentId: selectedThread.environmentId,
input: {
threadId: selectedThread.id,
modelSelection,
},
});
},
[selectedThread, updateThreadMetadata],
);
const handleUpdateThreadRuntimeMode = useCallback(
(runtimeMode: RuntimeMode) => {
if (!selectedThread) {
return;
}
return setThreadRuntimeMode({
environmentId: selectedThread.environmentId,
input: {
threadId: selectedThread.id,
runtimeMode,
},
});
},
[selectedThread, setThreadRuntimeMode],
);
const handleUpdateThreadInteractionMode = useCallback(
(interactionMode: ProviderInteractionMode) => {
if (!selectedThread) {
return;
}
return setThreadInteractionMode({
environmentId: selectedThread.environmentId,
input: {
threadId: selectedThread.id,
interactionMode,
},
});
},
[selectedThread, setThreadInteractionMode],
);
const handleStopThread = useCallback(() => {
if (
!selectedThread ||
Expand Down Expand Up @@ -435,7 +384,7 @@ export function ThreadRouteScreen() {

<View className="flex-1 bg-screen">
<ThreadDetailScreen
selectedThread={selectedThread}
selectedThread={selectedThreadWithDraftSettings ?? selectedThread}
contentPresentation={contentPresentation}
screenTone={connectionTone(routeConnectionState)}
connectionError={routeConnectionError}
Expand Down Expand Up @@ -466,9 +415,9 @@ export function ThreadRouteScreen() {
onStopThread={handleStopThread}
onSendMessage={composer.onSendMessage}
onReconnectEnvironment={handleReconnectEnvironment}
onUpdateThreadModelSelection={handleUpdateThreadModelSelection}
onUpdateThreadRuntimeMode={handleUpdateThreadRuntimeMode}
onUpdateThreadInteractionMode={handleUpdateThreadInteractionMode}
onUpdateThreadModelSelection={composer.onUpdateModelSelection}
onUpdateThreadRuntimeMode={composer.onUpdateRuntimeMode}
onUpdateThreadInteractionMode={composer.onUpdateInteractionMode}
onRespondToApproval={requests.onRespondToApproval}
onSelectUserInputOption={requests.onSelectUserInputOption}
onChangeUserInputCustomAnswer={requests.onChangeUserInputCustomAnswer}
Expand Down
Loading
Loading