Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
ea5f35f
feat(clients): per-project default environment for new threads
flamboh Oct 10, 2026
9f2601c
fix(clients): offline default falls back to a connected machine
flamboh Oct 10, 2026
39f5531
fix(clients): keep manual picks and deleted-project fallback in new d…
flamboh Oct 10, 2026
a2c637b
fix(clients): address review on default environment routing
flamboh Oct 11, 2026
99fe172
docs: note the offline default fallback
flamboh Oct 11, 2026
8878a10
test(clients): trim overlapping default environment tests
flamboh Oct 11, 2026
2e86d9b
fix(web): list the default environment after the model in project set…
flamboh Oct 11, 2026
0334370
fix(web): keep a machine picked in the PR panel for questions
flamboh Oct 11, 2026
f1d2470
fix(clients): new threads prefer a connected copy of the project
flamboh Oct 10, 2026
a9f9d95
Merge remote-tracking branch 'origin/main' into t3/default-project-en…
flamboh Oct 13, 2026
74df093
Merge remote-tracking branch 'fork/t3/default-project-environment' in…
flamboh Oct 13, 2026
fe8b379
Merge origin/main into t3/default-project-environment
flamboh Oct 16, 2026
e1c6b69
Merge t3/default-project-environment into t3/new-thread-connected-copy
flamboh Oct 16, 2026
b45a513
Merge remote-tracking branch 'origin/main' into t3/preserve-eligible-…
flamboh Oct 16, 2026
402ac8a
Merge remote-tracking branch 'origin/main' into t3/preserve-eligible-…
flamboh Oct 16, 2026
cb20bb2
fix(web): preserve connected automatic drafts on new thread
flamboh Oct 16, 2026
1885596
Merge branch 't3/preserve-eligible-draft-bottom' into t3/preserve-eli…
flamboh Oct 16, 2026
64139d0
Merge origin/main into t3/default-project-environment
flamboh Oct 19, 2026
741778d
Merge default project environment updates into connected copy routing
flamboh Oct 19, 2026
13f6745
test(mobile): server controls test stubs the environments catalog
flamboh Oct 19, 2026
4d7f395
Merge branch 't3/default-project-environment' into HEAD
flamboh Oct 19, 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
1 change: 1 addition & 0 deletions apps/mobile/src/features/home/HomeRouteScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ export function HomeRouteScreen() {
params: {
environmentId: String(thread.environmentId),
projectId: String(thread.projectId),
environmentSelection: "manual",
branch: thread.branch,
worktreePath: thread.worktreePath,
},
Expand Down
1 change: 1 addition & 0 deletions apps/mobile/src/features/keyboard/CommandPalette.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -311,6 +311,7 @@ export function CommandPalette(props: {
navigation.navigate("NewTaskSheet", {
screen: "NewTaskDraft",
params: {
environmentSelection: "manual",
environmentId: project.environmentId,
projectId: project.id,
title: project.title,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -424,6 +424,7 @@ function AdaptiveWorkspaceLayoutContent(
params: {
environmentId: String(thread.environmentId),
projectId: String(thread.projectId),
environmentSelection: "manual",
branch: thread.branch,
worktreePath: thread.worktreePath,
},
Expand Down
7 changes: 6 additions & 1 deletion apps/mobile/src/features/projects/AddProjectScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -670,7 +670,10 @@ function openNewTaskDraft(
},
) {
navigation.dispatch(
CommonActions.reset({ index: 0, routes: [{ name: "NewTaskDraft", params }] }),
CommonActions.reset({
index: 0,
routes: [{ name: "NewTaskDraft", params: { ...params, environmentSelection: "manual" } }],
}),
);
}

Expand Down Expand Up @@ -717,6 +720,7 @@ function useCreateProject(environment: EnvironmentOption | null) {
{
name: "NewTaskDraft",
params: {
environmentSelection: "manual",
environmentId: existing.environmentId,
projectId: existing.id,
title: existing.title,
Expand Down Expand Up @@ -748,6 +752,7 @@ function useCreateProject(environment: EnvironmentOption | null) {
{
name: "NewTaskDraft",
params: {
environmentSelection: "manual",
environmentId: environment.environmentId,
projectId,
title: inferProjectTitleFromPath(workspaceRoot),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ vi.mock("../../state/session", () => ({
readEnvironmentScope: () => true,
useEnvironmentsWithScope: () => new Set(["local"]),
}));
vi.mock("../../state/environments", () => ({ useEnvironments: () => ({ environments: [] }) }));
vi.mock("../../state/server", () => ({ serverEnvironment: { updateSettings: {} } }));
vi.mock("../../state/use-atom-command", () => ({
useAtomCommand: () => mocks.updateSettings,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import { View } from "react-native";
import { useSafeAreaInsets } from "react-native-safe-area-context";

import { RUNTIME_MODE_CHOICES } from "../threads/thread-settings-options";
import { useEnvironments } from "../../state/environments";
import { serverEnvironment } from "../../state/server";
import { useAtomCommand } from "../../state/use-atom-command";
import { SettingsScreen } from "./components/SettingsScreen";
Expand Down Expand Up @@ -51,7 +52,12 @@ const PAGE_TITLES: Record<SettingsPage, string> = {
};

const PAGE_PROJECT_KEYS: Record<SettingsPage, readonly ProjectScopedServerSettingKey[]> = {
"new-threads": ["defaultThreadEnvMode", "worktreeSubmodules", "defaultRuntimeMode"],
"new-threads": [
"defaultEnvironmentId",
"defaultThreadEnvMode",
"worktreeSubmodules",
"defaultRuntimeMode",
],
"source-control": [
"defaultAutoPull",
"removeAgentCreditsOnMerge",
Expand Down Expand Up @@ -142,7 +148,23 @@ function ServerSettingsDetail(props: { readonly page: SettingsPage }) {
const insets = useSafeAreaInsets();
const navigation = useNavigation();
const { selectedTargets, projectGroups, selectedProjectKey } = useSettingsEnvironmentFilter();
const { environments } = useEnvironments();
const selectedProject = projectGroups.find((group) => group.key === selectedProjectKey);
const projectEnvironments = selectedProject
? [
...new Map(
selectedProject.members.map(({ project }) => [
project.environmentId,
{
environmentId: project.environmentId,
label:
environments.find((target) => target.environmentId === project.environmentId)
?.label ?? project.environmentId,
},
]),
).values(),
]
: [];
const writableEnvironments = useEnvironmentsWithScope(selectedTargets, AuthSettingsWriteScope);
const projectSelected = selectedProjectKey !== null;
const targets = resolveMobileSettingsTargets(
Expand Down Expand Up @@ -293,6 +315,42 @@ function ServerSettingsDetail(props: { readonly page: SettingsPage }) {
) : null}
{props.page === "new-threads" ? (
<>
{projectEnvironments.length > 1 ? (
<SettingsSection
title="Default environment"
trailing={
pendingWrites === 0 && isMixed("defaultEnvironmentId") ? (
<MixedValuesLabel projectSelected={projectSelected} />
) : null
}
>
<SettingsChoiceRow
separated={false}
label="Automatic"
description="Use the current environment or automatic routing."
selected={
!isMixed("defaultEnvironmentId") &&
uniform("defaultEnvironmentId") === null
}
disabled={disabledFor("defaultEnvironmentId")}
onPress={() => write({ defaultEnvironmentId: null })}
/>
{projectEnvironments.map((environment) => (
<SettingsChoiceRow
key={environment.environmentId}
label={environment.label}
description="Start new threads here when connected."
selected={
!isMixed("defaultEnvironmentId") &&
uniform("defaultEnvironmentId") === environment.environmentId
}
separated
disabled={disabledFor("defaultEnvironmentId")}
onPress={() => write({ defaultEnvironmentId: environment.environmentId })}
/>
))}
</SettingsSection>
) : null}
<SettingsSection
title="Default workspace"
trailing={
Expand Down
2 changes: 2 additions & 0 deletions apps/mobile/src/features/threads/NewTaskDraftRouteScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import { NewTaskDraftScreen } from "./NewTaskDraftScreen";
type NewTaskDraftRouteParams = {
readonly environmentId?: string | string[];
readonly projectId?: string | string[];
readonly environmentSelection?: "manual";
readonly branch?: string | null;
readonly worktreePath?: string | null;
readonly title?: string | string[];
Expand All @@ -45,6 +46,7 @@ export function NewTaskDraftRouteScreen({ route }: StaticScreenProps<NewTaskDraf
? params.environmentId[0]
: params.environmentId,
projectId: Array.isArray(params.projectId) ? params.projectId[0] : params.projectId,
environmentSelection: params.environmentSelection,
branch: params.branch,
worktreePath: params.worktreePath,
cloning: (Array.isArray(params.cloning) ? params.cloning[0] : params.cloning) === "1",
Expand Down
39 changes: 35 additions & 4 deletions apps/mobile/src/features/threads/NewTaskDraftScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,7 @@ import {
useHardwareKeyboardCommand,
} from "../keyboard/hardwareKeyboardCommands";
import { resolveProjectThreadCreationBranch } from "./projectThreadCreationValidation";
import { useNewTaskProjectTarget } from "./use-new-task-project-target";
import { resolveDraftProjectSelection } from "./new-task-project-selection";
import {
resolveNewTaskBranchLabel,
Expand Down Expand Up @@ -187,6 +188,7 @@ function NewTaskWorkspaceIcon(props: {

export function NewTaskDraftScreen(props: {
readonly initialProjectRef?: {
readonly environmentSelection?: "manual";
readonly environmentId?: string;
readonly projectId?: string;
readonly branch?: string | null;
Expand All @@ -198,6 +200,7 @@ export function NewTaskDraftScreen(props: {
readonly incomingShareId?: string;
}) {
const projects = useProjects();
const resolveProjectTarget = useNewTaskProjectTarget();
const flow = useNewTaskFlow();
const navigation = useNavigation();
const {
Expand Down Expand Up @@ -643,15 +646,33 @@ export function NewTaskDraftScreen(props: {
lastInitialProjectRefRef.current = props.initialProjectRef;
appliedInitialProjectKeyRef.current = null;
}
if (
appliedInitialProjectKeyRef.current !== null &&
projects.some(
(project) =>
`${project.environmentId}:${project.id}` === appliedInitialProjectKeyRef.current,
)
) {
return;
}
const initialEnvironmentId = props.initialProjectRef?.environmentId;
const initialProjectId = props.initialProjectRef?.projectId;
if (initialEnvironmentId && initialProjectId) {
const directProject =
const requestedProject =
projects.find(
(project) =>
project.environmentId === initialEnvironmentId && project.id === initialProjectId,
) ?? null;

const directProject =
requestedProject &&
props.initialProjectRef?.environmentSelection !== "manual" &&
!props.initialProjectRef?.branch &&
!props.initialProjectRef?.worktreePath &&
!props.initialProjectRef?.cloning &&
!props.incomingShareId
? resolveProjectTarget(requestedProject)
: requestedProject;
if (directProject) {
// Apply the route's project once. Re-applying on every change would
// instantly revert environment/project switches made in the picker.
Expand All @@ -664,7 +685,7 @@ export function NewTaskDraftScreen(props: {
selectedProject?.environmentId !== directProject.environmentId ||
selectedProject.id !== directProject.id
) {
setProject(directProject);
setProject(directProject, { environmentSelection: "manual" });
return;
}
if (!flow.draftKey) return;
Expand All @@ -680,13 +701,22 @@ export function NewTaskDraftScreen(props: {
});
}
appliedInitialProjectKeyRef.current = directProjectKey;
const manual =
props.initialProjectRef?.environmentSelection === "manual" ||
Boolean(
props.initialProjectRef?.branch ||
props.initialProjectRef?.worktreePath ||
props.initialProjectRef?.cloning ||
props.incomingShareId,
);
if (
!manual &&
selectedProject?.environmentId === directProject.environmentId &&
selectedProject.id === directProject.id
) {
return;
}
setProject(directProject);
setProject(directProject, manual ? { environmentSelection: "manual" } : undefined);
return;
}

Expand All @@ -701,14 +731,15 @@ export function NewTaskDraftScreen(props: {
return;
}
if (selection.kind === "select") {
setProject(selection.project);
setProject(resolveProjectTarget(selection.project));
return;
}

navigation.dispatch(StackActions.replace("NewTask"));
}, [
projectScopes,
projects,
resolveProjectTarget,
flow.draftKey,
props.initialProjectRef,
props.incomingShareId,
Expand Down
10 changes: 6 additions & 4 deletions apps/mobile/src/features/threads/NewTaskRouteScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import { useWorkspaceState } from "../../state/workspace";
import { useAdaptiveWorkspaceLayout } from "../layout/AdaptiveWorkspaceLayout";
import { usePromptLinkResolver } from "../prompt-links/usePromptLinkResolver";
import { useIncomingShare } from "../sharing/IncomingShareProvider";
import { useNewTaskProjectTarget } from "./use-new-task-project-target";
import { useNewTaskFlow } from "./new-task-flow-provider";
import { filterProjectScopes, getProjectScopeSelectionTarget } from "./new-task-project-selection";

Expand Down Expand Up @@ -144,8 +145,9 @@ function NewTaskHeader(props: {

export function NewTaskRouteScreen({ route }: StaticScreenProps<NewTaskRouteParams | undefined>) {
const projects = useProjects();
const resolveProjectTarget = useNewTaskProjectTarget();
const [searchText, setSearchText] = useState("");
const { projectScopes, selectedEnvironmentId, setProject } = useNewTaskFlow();
const { projectScopes, selectedEnvironmentId, manualProjectRef, setProject } = useNewTaskFlow();
const { state: catalogState } = useWorkspaceState();
const navigation = useNavigation();
const isFocused = useIsFocused();
Expand Down Expand Up @@ -451,9 +453,9 @@ export function NewTaskRouteScreen({ route }: StaticScreenProps<NewTaskRoutePara
>
{visibleScopes.map((scope, scopeIndex) => {
const hasMultipleProjects = scope.projects.length > 1;
const selectionTarget = getProjectScopeSelectionTarget(
scope,
selectedEnvironmentId,
const selectionTarget = resolveProjectTarget(
getProjectScopeSelectionTarget(scope, selectedEnvironmentId),
{ manualProjectRef },
);
if (Platform.OS === "android") {
return (
Expand Down
Loading
Loading