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
15 changes: 10 additions & 5 deletions apps/mobile/src/features/threads/NewTaskDraftScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -704,10 +704,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 draft-local model/workspace selections with the content. The
// next task re-resolves project defaults before sticky app defaults.
clearComposerDraftContent(draftKey, {
clearModelSelection: true,
clearWorkspaceSelection: true,
});
}
navigation.getParent()?.goBack();
return;
Expand Down Expand Up @@ -771,7 +773,10 @@ export function NewTaskDraftScreen(props: {
}
flow.finishEditingPendingTask();
} else {
clearComposerDraftContent(draftKey, { clearWorkspaceSelection: true });
clearComposerDraftContent(draftKey, {
clearModelSelection: true,
clearWorkspaceSelection: true,
});
}
navigation.dispatch(
StackActions.replace("Thread", {
Expand Down
38 changes: 27 additions & 11 deletions apps/mobile/src/features/threads/new-task-flow-provider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import {
buildModelOptions,
groupByProvider,
resolveDefaultableModelSelection,
resolveNewTaskModelSelection,
resolveSelectableModelSelection,
} from "../../lib/modelOptions";
import { scopedProjectKey } from "../../lib/scopedEntities";
Expand All @@ -48,8 +49,10 @@ import {
removeComposerDraftAttachment,
replaceComposerDraftAttachments,
setComposerDraftText,
setStickyComposerModelSelection,
updateComposerDraftSettings,
useComposerDraft,
useStickyComposerModelSelection,
} from "../../state/use-composer-drafts";
import { useDebouncedValue, usePaginatedBranches } from "../../state/queries";
import { vcsEnvironment } from "../../state/vcs";
Expand Down Expand Up @@ -418,21 +421,33 @@ export function NewTaskFlowProvider(props: React.PropsWithChildren) {
selectedEnvironmentServerConfig,
selectedProject?.defaultModelSelection ?? null,
);
const storedStickyModelSelection = useStickyComposerModelSelection();
const stickyModelSelection = resolveDefaultableModelSelection(
selectedEnvironmentServerConfig,
storedStickyModelSelection,
);
const modelOptions = useMemo(
() =>
buildModelOptions(
selectedEnvironmentServerConfig,
draftModelSelection ?? projectDefaultModelSelection,
draftModelSelection ?? projectDefaultModelSelection ?? stickyModelSelection,
),
[selectedEnvironmentServerConfig, draftModelSelection, projectDefaultModelSelection],
[
selectedEnvironmentServerConfig,
draftModelSelection,
projectDefaultModelSelection,
stickyModelSelection,
],
);

const selectedModel =
draftModelSelection ??
projectDefaultModelSelection ??
modelOptions.find((option) => option.isDefault)?.selection ??
modelOptions[0]?.selection ??
null;
// An unsent draft keeps its explicit pick. Fresh drafts resolve the project
// default before the last manual app-wide selection and provider default.
const selectedModel = resolveNewTaskModelSelection({
draftSelection: draftModelSelection,
projectDefaultSelection: projectDefaultModelSelection,
stickySelection: stickyModelSelection,
modelOptions,
});
const selectedModelKey = selectedModel
? `${selectedModel.instanceId}:${selectedModel.model}`
: null;
Expand Down Expand Up @@ -462,9 +477,9 @@ export function NewTaskFlowProvider(props: React.PropsWithChildren) {
if (!option) {
return;
}
updateComposerDraftSettings(selectedProjectDraftKey, {
modelSelection: options ? { ...option.selection, options } : option.selection,
});
const selection = options ? { ...option.selection, options } : option.selection;
updateComposerDraftSettings(selectedProjectDraftKey, { modelSelection: selection });
setStickyComposerModelSelection(selection);
},
[modelOptions, selectedProjectDraftKey],
);
Expand All @@ -482,6 +497,7 @@ export function NewTaskFlowProvider(props: React.PropsWithChildren) {
updateComposerDraftSettings(selectedProjectDraftKey, {
modelSelection: nextSelection,
});
setStickyComposerModelSelection(nextSelection);
},
[selectedModel, selectedProjectDraftKey],
);
Expand Down
30 changes: 29 additions & 1 deletion apps/mobile/src/lib/modelOptions.test.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
import { describe, expect, it } from "vite-plus/test";

import { ProviderInstanceId, type ServerConfig } from "@t3tools/contracts";
import { ProviderInstanceId, type ModelSelection, type ServerConfig } from "@t3tools/contracts";

import {
buildModelOptions,
groupByProvider,
resolveDefaultableModelSelection,
resolveNewTaskModelSelection,
resolveSelectableModelSelection,
type ModelOption,
} from "./modelOptions";

describe("mobile model options", () => {
Expand Down Expand Up @@ -171,4 +173,30 @@ describe("mobile model options", () => {
// Offline: nothing to validate against, selection passes through.
expect(resolveDefaultableModelSelection(null, legacy)).toBe(legacy);
});

it("resolves new tasks from draft, project, sticky, then provider defaults", () => {
const draft = { instanceId: ProviderInstanceId.make("codex"), model: "draft" };
const project = { instanceId: ProviderInstanceId.make("codex"), model: "project" };
const sticky = { instanceId: ProviderInstanceId.make("codex"), model: "sticky" };
const providerDefault = {
selection: { instanceId: ProviderInstanceId.make("codex"), model: "default" },
isDefault: true,
} as ModelOption;
const resolve = (
draftSelection: ModelSelection | null,
projectDefaultSelection: ModelSelection | null,
stickySelection: ModelSelection | null,
) =>
resolveNewTaskModelSelection({
draftSelection,
projectDefaultSelection,
stickySelection,
modelOptions: [providerDefault],
});

expect(resolve(draft, project, sticky)).toBe(draft);
expect(resolve(null, project, sticky)).toBe(project);
expect(resolve(null, null, sticky)).toBe(sticky);
expect(resolve(null, null, null)).toBe(providerDefault.selection);
});
});
16 changes: 16 additions & 0 deletions apps/mobile/src/lib/modelOptions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,22 @@ export function resolveDefaultableModelSelection(
return model?.isLegacy === true ? null : usable;
}

export function resolveNewTaskModelSelection(input: {
Comment thread
shivamhwp marked this conversation as resolved.
readonly draftSelection: ModelSelection | null;
readonly projectDefaultSelection: ModelSelection | null;
readonly stickySelection: ModelSelection | null;
readonly modelOptions: ReadonlyArray<ModelOption>;
}): ModelSelection | null {
return (
input.draftSelection ??
input.projectDefaultSelection ??
input.stickySelection ??
input.modelOptions.find((option) => option.isDefault)?.selection ??
input.modelOptions[0]?.selection ??
null
);
}

export function buildModelOptions(
config: T3ServerConfig | null | undefined,
fallbackModelSelection: ModelSelection | null,
Expand Down
Loading
Loading