Skip to content
Open
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
22 changes: 14 additions & 8 deletions apps/mobile/src/features/threads/NewTaskDraftScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ import {
resolveNewTaskWorkspaceLabel,
} from "./new-task-context-presentation";
import { useIncomingShare } from "../sharing/IncomingShareProvider";
import { resolveNewTaskInteractionMode } from "./new-task-interaction-mode";

function NewTaskWorkspaceIcon(props: {
readonly workspaceMode: "local" | "worktree";
Expand Down Expand Up @@ -654,9 +655,9 @@ export function NewTaskDraftScreen(props: {
draft.workspaceSelection?.worktreePath ?? flow.selectedWorktreePath;
const startFromOrigin = draft.workspaceSelection?.startFromOrigin ?? flow.startFromOrigin;
const runtimeMode = draft.runtimeMode ?? flow.runtimeMode;
const interactionMode = flow.planModeEnabled
? (draft.interactionMode ?? flow.interactionMode)
: "default";
const interactionMode = resolveNewTaskInteractionMode(
draft.interactionMode ?? flow.interactionMode,
);
const initialMessageText = draft.text.trim();

if (
Expand Down Expand Up @@ -701,10 +702,12 @@ export function NewTaskDraftScreen(props: {
if (editingPendingTask) {
flow.finishEditingPendingTask();
} else {
// Drop the workspace selection with the content: the next task should
// re-resolve mode/branch/origin from the server's configured defaults
// instead of resurrecting this task's picks.
clearComposerDraftContent(draftKey, { clearWorkspaceSelection: true });
// Drop one-task controls with the content: the next task should return
// to Build mode and re-resolve workspace settings from server defaults.
clearComposerDraftContent(draftKey, {
clearInteractionMode: true,
clearWorkspaceSelection: true,
});
}
navigation.getParent()?.goBack();
return;
Expand Down Expand Up @@ -768,7 +771,10 @@ export function NewTaskDraftScreen(props: {
}
flow.finishEditingPendingTask();
} else {
clearComposerDraftContent(draftKey, { clearWorkspaceSelection: true });
clearComposerDraftContent(draftKey, {
clearInteractionMode: true,
clearWorkspaceSelection: true,
});
}
navigation.dispatch(
StackActions.replace("Thread", {
Expand Down
45 changes: 43 additions & 2 deletions apps/mobile/src/features/threads/ThreadSettingsSheet.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type {
ModelSelection,
ProviderInteractionMode,
ProviderOptionDescriptor,
ProviderOptionSelection,
RuntimeMode,
Expand Down Expand Up @@ -51,12 +52,17 @@ import {
NATIVE_SHEET_SURFACE_CONTENT_STYLE,
} from "../../native/sheet-surface";
import { useNewTaskFlow } from "./new-task-flow-provider";
import { interactionModeFromPlanToggle } from "./new-task-interaction-mode";
import {
createNativeMailSearchToolbarItem,
NATIVE_MAIL_SEARCH_TOOLBAR_CONTENT_INSET,
NATIVE_MAIL_SEARCH_TOOLBAR_SUPPORTED,
} from "../layout/native-mail-search-toolbar";
import { RUNTIME_MODE_CHOICES, selectableChoices } from "./thread-settings-options";
import {
buildThreadSettingsOptionItems,
RUNTIME_MODE_CHOICES,
selectableChoices,
} from "./thread-settings-options";
import {
modelMatchesCatalogQuery,
pendingModelAfterPress,
Expand Down Expand Up @@ -291,6 +297,8 @@ type ThreadSettingsSessionProps = {
readonly onUpdateOptionSelections: (selections: ReadonlyArray<ProviderOptionSelection>) => void;
readonly runtimeMode: RuntimeMode;
readonly onUpdateRuntimeMode: (mode: RuntimeMode) => void;
readonly interactionMode?: ProviderInteractionMode;
readonly onUpdateInteractionMode?: (mode: ProviderInteractionMode) => void;
};

export type ExistingThreadSettingsRouteSession = ThreadSettingsSessionProps & {
Expand Down Expand Up @@ -338,6 +346,8 @@ type ThreadSettingsSessionValue = {
readonly providerGroups: ReadonlyArray<ProviderGroup>;
readonly runtimeMode: RuntimeMode;
readonly onUpdateRuntimeMode: (mode: RuntimeMode) => void;
readonly interactionMode: ProviderInteractionMode | undefined;
readonly onUpdateInteractionMode: ((mode: ProviderInteractionMode) => void) | undefined;
readonly displayedDescriptors: ReadonlyArray<ProviderOptionDescriptor>;
readonly providerExpansionOverrides: ReadonlySet<string>;
readonly hasLegacyModels: boolean;
Expand Down Expand Up @@ -456,6 +466,8 @@ function ThreadSettingsSessionProvider(
providerGroups: props.providerGroups,
runtimeMode: props.runtimeMode,
onUpdateRuntimeMode: props.onUpdateRuntimeMode,
interactionMode: props.interactionMode,
onUpdateInteractionMode: props.onUpdateInteractionMode,
displayedDescriptors,
providerExpansionOverrides,
hasLegacyModels,
Expand Down Expand Up @@ -485,6 +497,8 @@ function ThreadSettingsSessionProvider(
pressModel,
providerFilter,
props.onUpdateRuntimeMode,
props.interactionMode,
props.onUpdateInteractionMode,
props.providerGroups,
props.runtimeMode,
searchQuery,
Expand Down Expand Up @@ -660,6 +674,10 @@ function ThreadSettingsOptionsItem(props: {
}) {
const insets = useSafeAreaInsets();
const session = useThreadSettingsSession();
const optionItems = buildThreadSettingsOptionItems(
session.displayedDescriptors,
session.interactionMode !== undefined && session.onUpdateInteractionMode !== undefined,
);
const bottomToolbarInset =
Platform.OS === "ios" && NATIVE_MAIL_SEARCH_TOOLBAR_SUPPORTED
? NATIVE_MAIL_SEARCH_TOOLBAR_CONTENT_INSET
Expand All @@ -672,7 +690,28 @@ function ThreadSettingsOptionsItem(props: {
className="mx-4 overflow-hidden rounded-2xl bg-card"
layout={THREAD_SETTINGS_OPTIONS_LAYOUT_TRANSITION}
>
{session.displayedDescriptors.map((descriptor) => {
{optionItems.map((item) => {
if (item.kind === "interaction-mode") {
return (
<Animated.View
key="interaction-mode"
entering={
props.animationsReady ? THREAD_SETTINGS_OPTION_ENTER_TRANSITION : undefined
}
exiting={props.animationsReady ? THREAD_SETTINGS_OPTION_EXIT_TRANSITION : undefined}
layout={THREAD_SETTINGS_OPTIONS_LAYOUT_TRANSITION}
>
<SwitchRow
label="Plan Mode"
value={session.interactionMode === "plan"}
onValueChange={(enabled) =>
session.onUpdateInteractionMode?.(interactionModeFromPlanToggle(enabled))
}
/>
</Animated.View>
);
}
const descriptor = item.descriptor;
if (descriptor.type === "select") {
return (
<Animated.View
Expand Down Expand Up @@ -1232,6 +1271,8 @@ export function NewTaskThreadSettingsRouteScreen() {
onUpdateOptionSelections={flow.setSelectedModelOptions}
runtimeMode={flow.runtimeMode}
onUpdateRuntimeMode={flow.setRuntimeMode}
interactionMode={flow.interactionMode}
onUpdateInteractionMode={flow.setInteractionMode}
>
<ThreadSettingsPickerNavigator onClose={() => navigation.goBack()} />
</ThreadSettingsSessionProvider>
Expand Down
58 changes: 6 additions & 52 deletions apps/mobile/src/features/threads/legacy-plan-mode.test.ts
Original file line number Diff line number Diff line change
@@ -1,57 +1,11 @@
import { describe, expect, it } from "@effect/vitest";

import { resolvePendingTaskInteractionMode } from "./legacy-plan-mode";
import { resolveLegacyPlanModeEnabled } from "./legacy-plan-mode";

describe("resolvePendingTaskInteractionMode", () => {
it("preserves a queued plan task while the preference is still loading", () => {
expect(
resolvePendingTaskInteractionMode({
preferenceLoaded: false,
planModeEnabled: false,
draftInteractionMode: "plan",
queuedInteractionMode: "plan",
}),
).toBe("plan");
});

it("forces build mode once the disabled preference has loaded", () => {
expect(
resolvePendingTaskInteractionMode({
preferenceLoaded: true,
planModeEnabled: false,
draftInteractionMode: "plan",
queuedInteractionMode: "plan",
}),
).toBe("default");
});

it("keeps a fresh draft in build mode while the preference is loading", () => {
expect(
resolvePendingTaskInteractionMode({
preferenceLoaded: false,
planModeEnabled: false,
draftInteractionMode: "plan",
queuedInteractionMode: undefined,
}),
).toBe("default");
});

it("honors the draft's mode when the plan preference is enabled", () => {
expect(
resolvePendingTaskInteractionMode({
preferenceLoaded: true,
planModeEnabled: true,
draftInteractionMode: "plan",
queuedInteractionMode: undefined,
}),
).toBe("plan");
expect(
resolvePendingTaskInteractionMode({
preferenceLoaded: true,
planModeEnabled: true,
draftInteractionMode: undefined,
queuedInteractionMode: "plan",
}),
).toBe("default");
describe("resolveLegacyPlanModeEnabled", () => {
it("stays disabled until an enabled preference has loaded", () => {
expect(resolveLegacyPlanModeEnabled({ loaded: false, preference: true })).toBe(false);
expect(resolveLegacyPlanModeEnabled({ loaded: true, preference: false })).toBe(false);
expect(resolveLegacyPlanModeEnabled({ loaded: true, preference: true })).toBe(true);
});
});
23 changes: 0 additions & 23 deletions apps/mobile/src/features/threads/legacy-plan-mode.ts
Original file line number Diff line number Diff line change
@@ -1,29 +1,6 @@
import {
DEFAULT_PROVIDER_INTERACTION_MODE,
type ProviderInteractionMode,
} from "@t3tools/contracts";

export function resolveLegacyPlanModeEnabled(input: {
readonly loaded: boolean;
readonly preference: boolean | undefined;
}): boolean {
return input.loaded && input.preference === true;
}

export function resolvePendingTaskInteractionMode(input: {
readonly preferenceLoaded: boolean;
readonly planModeEnabled: boolean;
readonly draftInteractionMode: ProviderInteractionMode | undefined;
readonly queuedInteractionMode: ProviderInteractionMode | undefined;
}): ProviderInteractionMode {
if (input.planModeEnabled) {
return input.draftInteractionMode ?? DEFAULT_PROVIDER_INTERACTION_MODE;
}
if (!input.preferenceLoaded) {
// Only an existing queued task may retain its previous mode while the
// preference is unknown. A fresh draft still defaults to Build so a stale
// persisted Plan selection cannot bypass a disabled preference at launch.
return input.queuedInteractionMode ?? DEFAULT_PROVIDER_INTERACTION_MODE;
}
return DEFAULT_PROVIDER_INTERACTION_MODE;
}
20 changes: 6 additions & 14 deletions apps/mobile/src/features/threads/new-task-flow-provider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ import type {
} from "@t3tools/contracts";
import {
CommandId,
DEFAULT_PROVIDER_INTERACTION_MODE,
DEFAULT_RUNTIME_MODE,
MessageId,
T3_PROJECT_FILE_NAME,
Expand Down Expand Up @@ -76,8 +75,8 @@ import {
type HomeProjectScope,
} from "../home/homeThreadList";
import { useMobileProjectGroupingSettings } from "../../state/project-grouping";
import { resolvePendingTaskInteractionMode } from "./legacy-plan-mode";
import { useLegacyPlanModeState } from "./use-legacy-plan-mode-enabled";
import { resolveNewTaskInteractionMode } from "./new-task-interaction-mode";
import {
resolveNewTaskBranchWorktreePath,
resolveNewTaskLocalWorkspaceSelection,
Expand Down Expand Up @@ -194,7 +193,7 @@ export function NewTaskFlowProvider(props: React.PropsWithChildren) {
const threads = useThreadShells();
const { savedConnectionsById } = useSavedRemoteConnections();
const groupingSettings = useMobileProjectGroupingSettings();
const { enabled: planModeEnabled, loaded: planModePreferenceLoaded } = useLegacyPlanModeState();
const { enabled: planModeEnabled } = useLegacyPlanModeState();
const projectScopes = useMemo(
() =>
sortHomeProjectScopes({
Expand Down Expand Up @@ -401,9 +400,7 @@ export function NewTaskFlowProvider(props: React.PropsWithChildren) {
selectedEnvironmentServerConfig?.settings.newWorktreesStartFromOrigin ??
true;
const runtimeMode = selectedProjectDraft.runtimeMode ?? DEFAULT_RUNTIME_MODE;
const interactionMode = planModeEnabled
? (selectedProjectDraft.interactionMode ?? DEFAULT_PROVIDER_INTERACTION_MODE)
: DEFAULT_PROVIDER_INTERACTION_MODE;
const interactionMode = resolveNewTaskInteractionMode(selectedProjectDraft.interactionMode);

// Stored selections only count while their provider is usable on the
// server; otherwise the server's default model wins instead of silently
Expand Down Expand Up @@ -866,12 +863,9 @@ export function NewTaskFlowProvider(props: React.PropsWithChildren) {
attachments: draft.attachments,
modelSelection: draftModelSelection,
runtimeMode: draft.runtimeMode ?? DEFAULT_RUNTIME_MODE,
interactionMode: resolvePendingTaskInteractionMode({
preferenceLoaded: planModePreferenceLoaded,
planModeEnabled,
draftInteractionMode: draft.interactionMode,
queuedInteractionMode: editingPendingTask?.interactionMode,
}),
interactionMode: resolveNewTaskInteractionMode(
draft.interactionMode ?? editingPendingTask?.interactionMode,
),
creation: {
projectId: selectedProject.id,
...(projectTitle !== undefined ? { projectTitle } : {}),
Expand Down Expand Up @@ -900,8 +894,6 @@ export function NewTaskFlowProvider(props: React.PropsWithChildren) {
selectedModel,
selectedProject,
selectedProjectDraftKey,
planModeEnabled,
planModePreferenceLoaded,
startFromOrigin,
workspaceMode,
],
Expand Down
18 changes: 18 additions & 0 deletions apps/mobile/src/features/threads/new-task-interaction-mode.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { describe, expect, it } from "vite-plus/test";

import {
interactionModeFromPlanToggle,
resolveNewTaskInteractionMode,
} from "./new-task-interaction-mode";

describe("new task interaction mode", () => {
it("defaults new chats to Build mode", () => {
expect(resolveNewTaskInteractionMode(undefined)).toBe("default");
expect(interactionModeFromPlanToggle(false)).toBe("default");
});

it("keeps an explicit Plan mode selection", () => {
expect(resolveNewTaskInteractionMode("plan")).toBe("plan");
expect(interactionModeFromPlanToggle(true)).toBe("plan");
});
});
14 changes: 14 additions & 0 deletions apps/mobile/src/features/threads/new-task-interaction-mode.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import {
DEFAULT_PROVIDER_INTERACTION_MODE,
type ProviderInteractionMode,
} from "@t3tools/contracts";

export function resolveNewTaskInteractionMode(
interactionMode: ProviderInteractionMode | undefined,
): ProviderInteractionMode {
return interactionMode ?? DEFAULT_PROVIDER_INTERACTION_MODE;
}

export function interactionModeFromPlanToggle(enabled: boolean): ProviderInteractionMode {
return enabled ? "plan" : DEFAULT_PROVIDER_INTERACTION_MODE;
}
Loading
Loading