Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
c77530e
feat: worktrees for multi-repo project folders
CreepyGnome Nov 12, 2026
f67c06b
fix: review findings and baseline test race
CreepyGnome Nov 12, 2026
4fd4ac3
fix: coderabbit review findings
CreepyGnome Nov 12, 2026
4c67566
Merge remote-tracking branch 'upstream/main' into feat/repo-worktrees
CreepyGnome Nov 12, 2026
6c8b2be
fix: shared folder restore and tool call files
CreepyGnome Nov 12, 2026
efbb701
fix: more coderabbit restore and cleanup findings
CreepyGnome Nov 12, 2026
8479618
Merge remote-tracking branch 'upstream/main' into feat/repo-worktrees
CreepyGnome Nov 12, 2026
9740f51
Merge remote-tracking branch 'upstream/main' into feat/repo-worktrees
CreepyGnome Nov 12, 2026
cdf0728
Merge remote-tracking branch 'upstream/main' into feat/repo-worktrees
CreepyGnome Nov 13, 2026
6b542d5
Merge remote-tracking branch 'upstream/main' into feat/repo-worktrees
CreepyGnome Nov 13, 2026
23992b7
Merge remote-tracking branch 'upstream/main' into feat/repo-worktrees
CreepyGnome Nov 13, 2026
a3d21f1
Merge remote-tracking branch 'upstream/main' into feat/repo-worktrees
CreepyGnome Nov 13, 2026
fe2ec54
Merge remote-tracking branch 'upstream/main' into feat/repo-worktrees
CreepyGnome Nov 13, 2026
43d65e3
Merge remote-tracking branch 'upstream/main' into feat/repo-worktrees
CreepyGnome Nov 13, 2026
613d5a4
Merge remote-tracking branch 'upstream/main' into feat/repo-worktrees
CreepyGnome Nov 13, 2026
4af72f8
Merge remote-tracking branch 'upstream/main' into feat/repo-worktrees
CreepyGnome Nov 13, 2026
c4473e4
Merge remote-tracking branch 'upstream/main' into feat/repo-worktrees
CreepyGnome Nov 13, 2026
028c886
Merge remote-tracking branch 'upstream/main' into feat/repo-worktrees
CreepyGnome Nov 13, 2026
0d19c43
Merge remote-tracking branch 'upstream/main' into feat/repo-worktrees
CreepyGnome Nov 13, 2026
7415f8c
Merge remote-tracking branch 'upstream/main' into feat/repo-worktrees
CreepyGnome Nov 14, 2026
c28b594
Merge remote-tracking branch 'upstream/main' into feat/repo-worktrees
CreepyGnome Nov 14, 2026
d78c81d
Merge remote-tracking branch 'upstream/main' into feat/repo-worktrees
CreepyGnome Nov 14, 2026
b235e52
Merge remote-tracking branch 'upstream/main' into feat/repo-worktrees
CreepyGnome Nov 14, 2026
eec223f
Merge remote-tracking branch 'upstream/main' into feat/repo-worktrees
CreepyGnome Nov 14, 2026
03b35da
Merge remote-tracking branch 'upstream/main' into feat/repo-worktrees
CreepyGnome Nov 14, 2026
7b15be9
Merge remote-tracking branch 'upstream/main' into feat/repo-worktrees
CreepyGnome Nov 14, 2026
0a67119
Merge remote-tracking branch 'upstream/main' into feat/repo-worktrees
CreepyGnome Nov 14, 2026
12eddf2
Merge remote-tracking branch 'upstream/main' into feat/repo-worktrees
CreepyGnome Nov 14, 2026
e7c2fe5
Merge remote-tracking branch 'upstream/main' into feat/repo-worktrees
CreepyGnome Nov 14, 2026
d430d48
Merge remote-tracking branch 'upstream/main' into feat/repo-worktrees
CreepyGnome Nov 14, 2026
877ddb5
Merge remote-tracking branch 'upstream/main' into feat/repo-worktrees
CreepyGnome Nov 14, 2026
059ea24
Merge remote-tracking branch 'upstream/main' into feat/repo-worktrees
CreepyGnome Nov 15, 2026
dc69934
Merge remote-tracking branch 'upstream/main' into feat/repo-worktrees
CreepyGnome Nov 15, 2026
ab8e83b
Merge remote-tracking branch 'upstream/main' into feat/repo-worktrees
CreepyGnome Nov 15, 2026
ed1a69a
Merge remote-tracking branch 'upstream/main' into feat/repo-worktrees
CreepyGnome Nov 15, 2026
66a16d3
Merge remote-tracking branch 'upstream/main' into feat/repo-worktrees
CreepyGnome Nov 15, 2026
e84c7eb
Merge remote-tracking branch 'upstream/main' into feat/repo-worktrees
CreepyGnome Nov 15, 2026
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
4 changes: 2 additions & 2 deletions apps/mobile/src/features/review/ReviewSheet.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ import {
useAdaptiveWorkspaceLayout,
useWorkspacePaneState,
} from "../layout/AdaptiveWorkspaceLayout";
import { useSelectedThreadWorktree } from "../../state/use-selected-thread-worktree";
import { useSelectedThreadSourceControl } from "../../state/use-selected-thread-worktree";
import { useReviewCacheForThread } from "./reviewState";
import {
isNativeReviewDiffDrawEvent,
Expand Down Expand Up @@ -354,7 +354,7 @@ export function ReviewSheet(props: ReviewSheetProps) {
const isEnvironmentReady = environment.presentation?.connection.phase === "connected";
const { draftMessage, draftContext } = useThreadDraftForThread({ environmentId, threadId });
const reviewCache = useReviewCacheForThread({ environmentId, threadId });
const { selectedThreadCwd } = useSelectedThreadWorktree();
const { sourceControlCwd: selectedThreadCwd } = useSelectedThreadSourceControl();
const topContentInset = useContentTopInset();
const bottomContentInset = useSheetContentBottomInset();

Expand Down
17 changes: 14 additions & 3 deletions apps/mobile/src/features/review/useReviewSections.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import { lastReviewGitSectionsAtom, reviewEnvironment } from "../../state/review
import { lastReviewTurnDiffsAtom } from "../../state/reviewTurnDiffs";
import { environmentSession } from "../../state/session";
import { useSelectedThreadProjection } from "../../state/use-thread-detail";
import { useSelectedThreadWorktree } from "../../state/use-selected-thread-worktree";
import { useSelectedThreadSourceControl } from "../../state/use-selected-thread-worktree";
import {
buildReviewSectionItems,
getReadyReviewCheckpoints,
Expand Down Expand Up @@ -52,7 +52,11 @@ export function useReviewSections(input: {
});
const { canReadFiles } = fileAccess;
const selectedThread = useSelectedThreadProjection();
const { selectedThreadCwd } = useSelectedThreadWorktree();
// Changes come from the repository worktree chosen for Git; turn diffs cover all of them.
const { sourceControlCwd: selectedThreadCwd, repoWorktree } = useSelectedThreadSourceControl();
// Review comments carry the section title, so it names the repository their paths belong to.
const gitSectionSuffix =
repoWorktree === null ? "" : ` in ${repoWorktree.repository} · ${repoWorktree.branch}`;
const diffPreview = useEnvironmentQuery(
canReadFiles && enabled && environmentId !== undefined && selectedThreadCwd !== null
? reviewEnvironment.diffPreview({
Expand Down Expand Up @@ -81,10 +85,17 @@ export function useReviewSections(input: {
);
// Turns show their last answered diff; the active turn's live query below overrides it.
// Section ids and the selection do not depend on the live query, so they are resolved first.
const gitSections =
canReadFiles || fileAccess.isPending ? (diffPreview.data?.sources ?? lastGitSections) : [];
const cachedSections = buildReviewSectionItems({
checkpoints: readyCheckpoints,
gitSections:
canReadFiles || fileAccess.isPending ? (diffPreview.data?.sources ?? lastGitSections) : [],
gitSectionSuffix === ""
? gitSections
: gitSections.map((section) => ({
...section,
title: `${section.title}${gitSectionSuffix}`,
})),
turnDiffById: reviewTurnDiffsBySectionId(readyCheckpoints, lastTurnDiffs),
loadingGitSections: fileAccess.isPending || diffPreview.isPending,
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import {
import { SettingsScreen } from "./components/SettingsScreen";
import { useSettingsEnvironmentMenu } from "./components/SettingsEnvironmentFilterHeader";
import { BranchNamingSettings } from "./components/BranchNamingSettings";
import { RepoWorktreeSettings } from "./components/RepoWorktreeSettings";
import { DatabaseStorageNotice } from "./components/DatabaseStorageNotice";
import { ListChoiceRow as SettingsChoiceRow } from "../../components/ui/ListChoiceRow";
import { SettingsSection } from "./components/SettingsSection";
Expand Down Expand Up @@ -67,6 +68,8 @@ const PAGE_PROJECT_KEYS: Record<SettingsPage, readonly ProjectScopedServerSettin
"source-control": ["defaultAutoPull", "removeAgentCreditsOnMerge"],
worktrees: [
"newWorktreesStartFromOrigin",
"repoWorktrees",
"repoWorktreeInstructions",
"worktreeSubmodules",
"branchNamingMode",
"branchNamePrefix",
Expand Down Expand Up @@ -486,6 +489,17 @@ function ServerSettingsDetail(props: { readonly page: SettingsPage }) {
disabled={disabledFor("newWorktreesStartFromOrigin")}
onValueChange={(value) => write({ newWorktreesStartFromOrigin: value })}
/>
<RepoWorktreeSettings
key={targetsKey}
enabled={uniform("repoWorktrees")}
instructions={uniform("repoWorktreeInstructions")}
disabled={{
enabled: disabledFor("repoWorktrees"),
instructions: disabledFor("repoWorktreeInstructions"),
}}
projectSelected={projectSelected}
onChange={write}
/>
</SettingsSection>
<SettingsSection
title="Worktree submodules"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import type { ServerSettingsPatch } from "@t3tools/contracts";
import { useRef } from "react";
import { View } from "react-native";

import { AppText as Text, AppTextInput } from "../../../components/ui/AppText";
import { SettingsSwitchRow } from "./SettingsSwitchRow";

const GUIDANCE_HELP =
"Instructions for agents working in these repositories, such as base branches and submodule rules.";

/** Rows inside an existing section. The first draws a top border, so another row must precede it. */
export function RepoWorktreeSettings(props: {
enabled: boolean | null;
instructions: string | null;
/** Each setting can be managed by policy on its own. */
disabled: { readonly enabled: boolean; readonly instructions: boolean };
projectSelected: boolean;
onChange: (patch: ServerSettingsPatch) => void;
}) {
const instructionsEdited = useRef(false);
return (
<>
<View className="border-t border-border-subtle">
<SettingsSwitchRow
icon="folder"
label="Multi-Repository Project Folder"
subtitle={
props.projectSelected
? "Create or reuse separate worktrees for the Git repositories inside this project folder as needed."
: "Create or reuse separate worktrees for the Git repositories inside each project folder as needed. Projects can override it."
}
value={props.enabled}
disabled={props.disabled.enabled}
onValueChange={(value) => props.onChange({ repoWorktrees: value })}
/>
</View>
{props.enabled !== false ? (
<View className="gap-2 border-t border-border-subtle px-4 py-3">
<View className="gap-1">
<Text textRole="body" className="text-foreground">
Repository Guidance
</Text>
<Text textRole="footnote" className="text-foreground-muted">
{GUIDANCE_HELP}
</Text>
</View>
<AppTextInput
key={props.instructions}
accessibilityLabel="Repository Guidance"
accessibilityHint={GUIDANCE_HELP}
onChangeText={() => {
instructionsEdited.current = true;
}}
defaultValue={props.instructions ?? ""}
placeholder={
props.instructions === null
? "Mixed. Enter guidance for all selected targets."
: "Start api work from develop. Start shared/framework work from team-develop."
}
editable={!props.disabled.instructions}
multiline
autoCapitalize="sentences"
className="min-h-24 rounded-xl px-3 py-2 text-base text-foreground"
onEndEditing={(event) => {
const value = event.nativeEvent.text.trim();
if (
!props.disabled.instructions &&
instructionsEdited.current &&
(props.instructions === null || value !== props.instructions)
)
props.onChange({ repoWorktreeInstructions: value });
instructionsEdited.current = false;
}}
/>
</View>
) : null}
</>
);
}
2 changes: 2 additions & 0 deletions apps/mobile/src/features/settings/settings-search.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,8 @@ const SETTINGS_SEARCH_TERMS = {
"branch prefix",
"submodules",
"start from remote",
"multi-repository project folder",
"repository guidance",
],
SettingsLabs: [
"beta",
Expand Down
13 changes: 9 additions & 4 deletions apps/mobile/src/features/threads/ThreadRouteScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,10 @@ import { useAtomCommand } from "../../state/use-atom-command";
import { useSelectedThreadGitActions } from "../../state/use-selected-thread-git-actions";
import { useSelectedThreadGitState } from "../../state/use-selected-thread-git-state";
import { useSelectedThreadRequests } from "../../state/use-selected-thread-requests";
import { useSelectedThreadWorktree } from "../../state/use-selected-thread-worktree";
import {
useSelectedThreadSourceControl,
useSelectedThreadWorktree,
} from "../../state/use-selected-thread-worktree";
import { useThreadComposerState } from "../../state/use-thread-composer-state";
import { resolveMergeBackTargetThreadId } from "@t3tools/client-runtime/state/thread-relationships";
import { resolveLatestMergeBackRun } from "@t3tools/client-runtime/state/thread-workflows";
Expand Down Expand Up @@ -538,11 +541,13 @@ function ThreadRouteContent(
]
.filter(Boolean)
.join(" · ");
// The Git menu follows the repository chosen for Git; files and terminals stay in the workspace.
const { sourceControlCwd } = useSelectedThreadSourceControl();
const gitStatus = useEnvironmentQuery(
selectedThread !== null && selectedThreadCwd !== null
selectedThread !== null && sourceControlCwd !== null
? vcsEnvironment.status({
environmentId: selectedThread.environmentId,
input: { cwd: selectedThreadCwd },
input: { cwd: sourceControlCwd },
})
: null,
);
Expand All @@ -564,7 +569,7 @@ function ThreadRouteContent(

const gitActionProgressTarget = {
environmentId: selectedThread?.environmentId ?? null,
cwd: selectedThreadCwd,
cwd: sourceControlCwd,
};
const gitActionProgress = useGitActionProgress(gitActionProgressTarget);

Expand Down
10 changes: 6 additions & 4 deletions apps/mobile/src/features/threads/git/GitCommitSheet.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import { useEnvironmentQuery } from "../../../state/query";
import { useThreadSelection } from "../../../state/use-thread-selection";
import { useSelectedThreadGitActions } from "../../../state/use-selected-thread-git-actions";
import { useSelectedThreadGitState } from "../../../state/use-selected-thread-git-state";
import { useSelectedThreadWorktree } from "../../../state/use-selected-thread-worktree";
import { useSelectedThreadSourceControl } from "../../../state/use-selected-thread-worktree";
import { vcsEnvironment } from "../../../state/vcs";

type GitCommitSheetProps = StaticScreenProps<{
Expand All @@ -24,10 +24,12 @@ type GitCommitSheetProps = StaticScreenProps<{
export function GitCommitSheet(_props: GitCommitSheetProps) {
const navigation = useNavigation();
const { selectedThread } = useThreadSelection();
const { selectedThreadCwd } = useSelectedThreadWorktree();
const { sourceControlCwd: selectedThreadCwd, repoWorktree } = useSelectedThreadSourceControl();
const gitState = useSelectedThreadGitState();
const gitActions = useSelectedThreadGitActions();
const { canWriteSourceControl, canChangeThreadBranch } = gitActions;
// A repository worktree's new branch is not the thread's, so it needs only source control.
const canCreateFeatureBranch = repoWorktree === null ? canChangeThreadBranch : true;

const gitStatus = useEnvironmentQuery(
selectedThread !== null && selectedThreadCwd !== null
Expand All @@ -52,7 +54,7 @@ export function GitCommitSheet(_props: GitCommitSheetProps) {
const selectedDeletions = selectedFiles.reduce((sum, file) => sum + file.deletions, 0);

const runCommitAction = async (featureBranch: boolean) => {
if (!canWriteSourceControl || (featureBranch && !canChangeThreadBranch)) return;
if (!canWriteSourceControl || (featureBranch && !canCreateFeatureBranch)) return;
const commitMessage = dialogCommitMessage.trim();
navigation.goBack();
await gitActions.onRunSelectedThreadGitAction({
Expand Down Expand Up @@ -202,7 +204,7 @@ export function GitCommitSheet(_props: GitCommitSheetProps) {
icon="arrow.branch"
tone="text"
fullWidth
disabled={!canWriteSourceControl || !canChangeThreadBranch || noneSelected || busy}
disabled={!canWriteSourceControl || !canCreateFeatureBranch || noneSelected || busy}
onPress={() => void runCommitAction(true)}
/>
</View>
Expand Down
12 changes: 9 additions & 3 deletions apps/mobile/src/features/threads/git/GitConfirmSheet.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { FitSheet } from "../../../components/ui/FitSheet";
import { SheetIntro } from "../../../components/ui/SheetIntro";
import { useSelectedThreadGitActions } from "../../../state/use-selected-thread-git-actions";
import { useSelectedThreadGitState } from "../../../state/use-selected-thread-git-state";
import { useSelectedThreadSourceControl } from "../../../state/use-selected-thread-worktree";
import { useThreadSelection } from "../../../state/use-thread-selection";
import { SheetActionButton } from "./gitSheetComponents";

Expand All @@ -30,6 +31,7 @@ export function GitConfirmSheet(props: GitConfirmSheetProps) {
const gitState = useSelectedThreadGitState();
const gitActions = useSelectedThreadGitActions();
const { canWriteSourceControl, canChangeThreadBranch } = gitActions;
const { repoWorktree } = useSelectedThreadSourceControl();
const { selectedThreadProject } = useThreadSelection();
const terminology = sourceControlClients.get(
selectedThreadProject?.repositoryIdentity?.provider,
Expand Down Expand Up @@ -85,24 +87,28 @@ export function GitConfirmSheet(props: GitConfirmSheetProps) {
return;
}

// The new branch goes where the action runs, which may be a repository worktree.
const branches =
gitState.selectedThreadBranches.length > 0
repoWorktree === null && gitState.selectedThreadBranches.length > 0
? gitState.selectedThreadBranches
: await gitActions.refreshSelectedThreadBranches();
: await gitActions.refreshSelectedThreadBranches({ sourceControl: true });
if (branches === null) return;
const newBranchName = resolveAutoFeatureBranchName(
Arr.filterMap(branches, (branch) =>
branch.isRemote ? Result.failVoid : Result.succeed(branch.name),
),
);
const created = await gitActions.onCreateSelectedThreadBranch(newBranchName);
const created = await gitActions.onCreateSelectedThreadBranch(newBranchName, {
sourceControl: true,
});
if (created === null) return;
await gitActions.onRunSelectedThreadGitAction({ action: confirmAction });
}, [
canChangeThreadBranch,
confirmAction,
gitActions,
gitState.selectedThreadBranches,
repoWorktree,
includesCommit,
params,
navigation,
Expand Down
Loading
Loading