Skip to content

Commit b42e30c

Browse files
committed
fix(mobile): align non-git new thread behavior
1 parent 196c8ea commit b42e30c

4 files changed

Lines changed: 115 additions & 21 deletions

File tree

‎apps/mobile/src/features/threads/NewTaskDraftScreen.tsx‎

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ import {
6363
resolveNewTaskWorkspaceLabel,
6464
} from "./new-task-context-presentation";
6565
import { useIncomingShare } from "../sharing/IncomingShareProvider";
66+
import { resolveProjectThreadWorkspaceSelection } from "./projectThreadCreationValidation";
6667

6768
function NewTaskWorkspaceIcon(props: {
6869
readonly workspaceMode: "local" | "worktree";
@@ -641,10 +642,15 @@ export function NewTaskDraftScreen(props: {
641642
selectedEnvironmentServerConfig,
642643
draft.modelSelection ?? null,
643644
) ?? flow.selectedModel;
644-
const workspaceMode = draft.workspaceSelection?.mode ?? flow.workspaceMode;
645-
const selectedBranchName = draft.workspaceSelection?.branch ?? flow.selectedBranchName;
646-
const selectedWorktreePath =
647-
draft.workspaceSelection?.worktreePath ?? flow.selectedWorktreePath;
645+
const workspaceSelection = resolveProjectThreadWorkspaceSelection({
646+
isGitRepo: flow.isGitRepo,
647+
environmentMode: draft.workspaceSelection?.mode ?? flow.workspaceMode,
648+
branch: draft.workspaceSelection?.branch ?? flow.selectedBranchName,
649+
worktreePath: draft.workspaceSelection?.worktreePath ?? flow.selectedWorktreePath,
650+
});
651+
const workspaceMode = workspaceSelection.environmentMode;
652+
const selectedBranchName = workspaceSelection.branch;
653+
const selectedWorktreePath = workspaceSelection.worktreePath;
648654
const startFromOrigin = draft.workspaceSelection?.startFromOrigin ?? flow.startFromOrigin;
649655
const runtimeMode = draft.runtimeMode ?? flow.runtimeMode;
650656
const interactionMode = flow.planModeEnabled
@@ -909,7 +915,7 @@ export function NewTaskDraftScreen(props: {
909915
</View>
910916
);
911917

912-
const workspaceControls = (
918+
const workspaceControls = flow.isGitRepo ? (
913919
<View className="flex-row items-center gap-1 px-2">
914920
<ComposerInlineControl
915921
accessibilityHint={`Switches to ${flow.workspaceMode === "local" ? "a new worktree" : "the current checkout"}`}
@@ -937,11 +943,11 @@ export function NewTaskDraftScreen(props: {
937943
onPress={() => openContextPicker("NewTaskBranch")}
938944
/>
939945
</View>
940-
);
946+
) : null;
941947

942948
const composerDock = (
943949
<View className="bg-sheet px-4 pt-1" style={{ paddingBottom: controlsBottomPadding }}>
944-
<View className="pb-1">{workspaceControls}</View>
950+
{workspaceControls ? <View className="pb-1">{workspaceControls}</View> : null}
945951

946952
<ComposerSurface
947953
animateLayout={false}

‎apps/mobile/src/features/threads/new-task-flow-provider.tsx‎

Lines changed: 42 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ import {
6767
setPendingConnectionError,
6868
useSavedRemoteConnections,
6969
} from "../../state/use-remote-environment-registry";
70+
import { vcsEnvironment } from "../../state/vcs";
7071
import { EnvironmentProject } from "@t3tools/client-runtime/state/shell";
7172
import { type VcsRef } from "@t3tools/client-runtime/state/vcs";
7273
import {
@@ -81,6 +82,7 @@ import {
8182
resolveNewTaskBranchWorktreePath,
8283
resolveNewTaskLocalWorkspaceSelection,
8384
} from "./new-task-context-presentation";
85+
import { resolveProjectThreadWorkspaceSelection } from "./projectThreadCreationValidation";
8486

8587
type WorkspaceMode = "local" | "worktree";
8688

@@ -124,6 +126,7 @@ type NewTaskFlowContextValue = {
124126
readonly selectedEnvironmentId: EnvironmentId | null;
125127
readonly selectedProjectKey: string | null;
126128
readonly selectedModelKey: string | null;
129+
readonly isGitRepo: boolean;
127130
readonly workspaceMode: WorkspaceMode;
128131
readonly selectedBranchName: string | null;
129132
readonly selectedWorktreePath: string | null;
@@ -348,6 +351,17 @@ export function NewTaskFlowProvider(props: React.PropsWithChildren) {
348351
const selectedEnvironmentServerConfig = useEnvironmentServerConfig(
349352
selectedProject?.environmentId ?? null,
350353
);
354+
const gitStatus = useEnvironmentQuery(
355+
selectedProject !== null && selectedProject.workspaceRoot.length > 0
356+
? vcsEnvironment.status({
357+
environmentId: selectedProject.environmentId,
358+
input: { cwd: selectedProject.workspaceRoot },
359+
})
360+
: null,
361+
);
362+
// Match web/desktop: assume git only while status is unresolved so the
363+
// composer controls do not flicker away during the initial query.
364+
const isGitRepo = gitStatus.data?.isRepo ?? true;
351365
// While a queued pending task is being edited its draft lives under a key
352366
// scoped to the queued message, so per-project new-task drafts stay intact.
353367
const selectedProjectDraftKey = editingPendingTask
@@ -387,9 +401,15 @@ export function NewTaskFlowProvider(props: React.PropsWithChildren) {
387401
projectSetting: selectedProject?.defaultThreadEnvMode,
388402
projectFilePending: t3ProjectFileQuery.isPending,
389403
});
390-
const workspaceMode = selectedProjectDraft.workspaceSelection?.mode ?? defaultWorkspaceMode;
391-
const selectedBranchName = selectedProjectDraft.workspaceSelection?.branch ?? null;
392-
const selectedWorktreePath = selectedProjectDraft.workspaceSelection?.worktreePath ?? null;
404+
const workspaceSelection = resolveProjectThreadWorkspaceSelection({
405+
isGitRepo,
406+
environmentMode: selectedProjectDraft.workspaceSelection?.mode ?? defaultWorkspaceMode,
407+
branch: selectedProjectDraft.workspaceSelection?.branch ?? null,
408+
worktreePath: selectedProjectDraft.workspaceSelection?.worktreePath ?? null,
409+
});
410+
const workspaceMode = workspaceSelection.environmentMode;
411+
const selectedBranchName = workspaceSelection.branch;
412+
const selectedWorktreePath = workspaceSelection.worktreePath;
393413
// Keep the user's explicit choice separate from the resolved display value:
394414
// only the explicit flag is ever written back to the draft, so the resolved
395415
// value keeps tracking the server setting when the config loads late.
@@ -530,12 +550,12 @@ export function NewTaskFlowProvider(props: React.PropsWithChildren) {
530550
const debouncedBranchQuery = useDebouncedValue(branchQuery, BRANCH_SEARCH_DEBOUNCE_MS);
531551
const branchTarget = useMemo(
532552
() => ({
533-
environmentId: selectedProject?.environmentId ?? null,
553+
environmentId: isGitRepo ? (selectedProject?.environmentId ?? null) : null,
534554
// `|| null` also skips the stand-in project's empty workspaceRoot.
535-
cwd: selectedProject?.workspaceRoot || null,
555+
cwd: isGitRepo ? selectedProject?.workspaceRoot || null : null,
536556
query: debouncedBranchQuery,
537557
}),
538-
[debouncedBranchQuery, selectedProject?.environmentId, selectedProject?.workspaceRoot],
558+
[debouncedBranchQuery, isGitRepo, selectedProject?.environmentId, selectedProject?.workspaceRoot],
539559
);
540560
const branchState = usePaginatedBranches(branchTarget);
541561
const branchSearchIsDebouncing = branchQuery.trim() !== debouncedBranchQuery.trim();
@@ -725,12 +745,12 @@ export function NewTaskFlowProvider(props: React.PropsWithChildren) {
725745
const refreshBranches = branchState.refresh;
726746
const loadMoreBranches = branchState.loadNext;
727747
const loadBranches = useCallback(() => {
728-
if (!selectedProject) {
748+
if (!selectedProject || !isGitRepo) {
729749
return;
730750
}
731751
setPendingConnectionError(null);
732752
refreshBranches();
733-
}, [refreshBranches, selectedProject]);
753+
}, [isGitRepo, refreshBranches, selectedProject]);
734754

735755
useEffect(() => {
736756
if (
@@ -824,10 +844,15 @@ export function NewTaskFlowProvider(props: React.PropsWithChildren) {
824844
if (text.length === 0 || !draftModelSelection) {
825845
return null;
826846
}
827-
const workspaceSelection = draft.workspaceSelection;
828-
// Fall back to the resolved mode (server default) so queued tasks drain
847+
// Fall back to the resolved default mode so queued tasks drain
829848
// with the same mode the composer displayed.
830-
const mode = workspaceSelection?.mode ?? workspaceMode;
849+
const workspaceSelection = resolveProjectThreadWorkspaceSelection({
850+
isGitRepo,
851+
environmentMode: draft.workspaceSelection?.mode ?? workspaceMode,
852+
branch: draft.workspaceSelection?.branch ?? null,
853+
worktreePath: draft.workspaceSelection?.worktreePath ?? null,
854+
});
855+
const mode = workspaceSelection.environmentMode;
831856
// When the selection is the stand-in built from the queued snapshot,
832857
// persist the original (possibly absent) snapshot values — the
833858
// stand-in's placeholder title/workspaceRoot must never be written back
@@ -859,12 +884,12 @@ export function NewTaskFlowProvider(props: React.PropsWithChildren) {
859884
...(projectTitle !== undefined ? { projectTitle } : {}),
860885
...(projectCwd !== undefined ? { projectCwd } : {}),
861886
workspaceMode: mode,
862-
branch: workspaceSelection?.branch ?? null,
863-
worktreePath: mode === "worktree" ? null : (workspaceSelection?.worktreePath ?? null),
887+
branch: workspaceSelection.branch,
888+
worktreePath: mode === "worktree" ? null : workspaceSelection.worktreePath,
864889
// The draft only carries the flag when the user touched it; fall
865890
// back to the resolved default (server settings) so queued tasks
866891
// drain with the same origin mode the composer displayed.
867-
...((workspaceSelection?.startFromOrigin ?? startFromOrigin)
892+
...((draft.workspaceSelection?.startFromOrigin ?? startFromOrigin)
868893
? { startFromOrigin: true }
869894
: {}),
870895
},
@@ -874,6 +899,7 @@ export function NewTaskFlowProvider(props: React.PropsWithChildren) {
874899
[
875900
editingPendingProject,
876901
editingPendingTask,
902+
isGitRepo,
877903
selectedEnvironmentServerConfig,
878904
selectedModel,
879905
selectedProject,
@@ -981,6 +1007,7 @@ export function NewTaskFlowProvider(props: React.PropsWithChildren) {
9811007
selectedEnvironmentId,
9821008
selectedProjectKey,
9831009
selectedModelKey,
1010+
isGitRepo,
9841011
workspaceMode,
9851012
selectedBranchName,
9861013
selectedWorktreePath,
@@ -1049,6 +1076,7 @@ export function NewTaskFlowProvider(props: React.PropsWithChildren) {
10491076
filteredBranches,
10501077
finishEditingPendingTask,
10511078
interactionMode,
1079+
isGitRepo,
10521080
planModeEnabled,
10531081
loadBranches,
10541082
loadMoreBranches,
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import { describe, expect, it } from "vite-plus/test";
2+
3+
import { resolveProjectThreadWorkspaceSelection } from "./projectThreadCreationValidation";
4+
5+
describe("resolveProjectThreadWorkspaceSelection", () => {
6+
it("preserves git project workspace selections", () => {
7+
expect(
8+
resolveProjectThreadWorkspaceSelection({
9+
isGitRepo: true,
10+
environmentMode: "worktree",
11+
branch: "main",
12+
worktreePath: "/repo/.t3/worktrees/feature",
13+
}),
14+
).toEqual({
15+
environmentMode: "worktree",
16+
branch: "main",
17+
worktreePath: "/repo/.t3/worktrees/feature",
18+
});
19+
});
20+
21+
it("forces non-git projects onto the local checkout without git metadata", () => {
22+
expect(
23+
resolveProjectThreadWorkspaceSelection({
24+
isGitRepo: false,
25+
environmentMode: "worktree",
26+
branch: "stale-branch",
27+
worktreePath: "/stale/worktree",
28+
}),
29+
).toEqual({
30+
environmentMode: "local",
31+
branch: null,
32+
worktreePath: null,
33+
});
34+
});
35+
});

‎apps/mobile/src/features/threads/projectThreadCreationValidation.ts‎

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,31 @@ export const ProjectThreadCreationValidationError = Schema.Union([
3232
]);
3333
export type ProjectThreadCreationValidationError = typeof ProjectThreadCreationValidationError.Type;
3434

35+
export function resolveProjectThreadWorkspaceSelection(input: {
36+
readonly isGitRepo: boolean;
37+
readonly environmentMode: "local" | "worktree";
38+
readonly branch: string | null;
39+
readonly worktreePath: string | null;
40+
}): {
41+
readonly environmentMode: "local" | "worktree";
42+
readonly branch: string | null;
43+
readonly worktreePath: string | null;
44+
} {
45+
if (input.isGitRepo) {
46+
return {
47+
environmentMode: input.environmentMode,
48+
branch: input.branch,
49+
worktreePath: input.worktreePath,
50+
};
51+
}
52+
53+
return {
54+
environmentMode: "local",
55+
branch: null,
56+
worktreePath: null,
57+
};
58+
}
59+
3560
export function validateProjectThreadCreation(input: {
3661
readonly environmentId: EnvironmentId;
3762
readonly projectId: ProjectId;

0 commit comments

Comments
 (0)