Skip to content
Draft
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
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ Full docs live in [docs/](./docs). There's no docs site yet.
- [Remote access from a phone or another machine](./docs/user/remote-access.md)
- [Keeping app and server in sync](./docs/user/updating.md)
- [Source control integrations](./docs/user/source-control.md)
- Multiple accounts: [Codex](./docs/user/providers-codex.md) · [Claude](./docs/user/providers-claude.md)
- Providers: [Codex](./docs/user/providers-codex.md) · [Claude](./docs/user/providers-claude.md) · [Grok](./docs/user/providers-grok.md)
- Linux: [run T3 Code as a background service](./docs/user/background-service.md)

Building from source? Start at [docs/internals/overview.md](./docs/internals/overview.md).
Expand Down
39 changes: 34 additions & 5 deletions apps/mobile/src/features/threads/ThreadComposer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import type { ReactNode } from "react";
import { memo, useCallback, useEffect, useMemo, useRef, useState, type RefObject } from "react";
import {
ActivityIndicator,
Alert,
Image,
Platform,
Pressable,
Expand Down Expand Up @@ -53,7 +54,11 @@ import {
import { ControlPill, ControlPillMenu } from "../../components/ControlPill";
import { ProviderIcon } from "../../components/ProviderIcon";
import type { DraftComposerImageAttachment } from "../../lib/composerImages";
import { buildModelOptions, groupByProvider } from "../../lib/modelOptions";
import {
buildModelOptions,
groupByProvider,
isSameInstanceSessionBoundChangeBlocked,
} from "../../lib/modelOptions";
import { useScaledTextRole } from "../settings/appearance/useScaledTextRole";
import type { RemoteClientConnectionState } from "../../lib/connection";
import {
Expand All @@ -62,10 +67,10 @@ import {
scoreQueryMatch,
} from "@t3tools/shared/searchRanking";
import {
applyProviderOptionMenuEvent,
buildProviderOptionMenuActions,
providerOptionsConfigurationLabel,
resolveProviderOptionDescriptors,
resolveProviderOptionMenuChange,
} from "../../lib/providerOptions";
import { useComposerPathSearch } from "../../state/use-composer-path-search";
import { ComposerCommandPopover, type ComposerCommandItem } from "./ComposerCommandPopover";
Expand Down Expand Up @@ -114,6 +119,13 @@ export interface ThreadComposerProps {
readonly onUpdateModelSelection: (modelSelection: ModelSelection) => void;
readonly onUpdateRuntimeMode: (runtimeMode: RuntimeMode) => void;
readonly onUpdateInteractionMode: (interactionMode: ProviderInteractionMode) => void;
/**
* Session-bound lock for the started provider instance, derived once in
* composer state from the projection-first runtime. Same-instance model and
* option changes are rejected; cross-provider handoff stays allowed.
*/
readonly optionChangeBlocked?: boolean;
readonly optionChangeBlockedInstanceId?: string | null;
readonly onReconnectEnvironment: () => void;
readonly onExpandedChange?: (expanded: boolean) => void;
}
Expand Down Expand Up @@ -600,6 +612,11 @@ export const ThreadComposer = memo(function ThreadComposer(props: ThreadComposer
}),
[currentModelOption?.capabilities, currentModelSelection.options],
);
const providerOptionsLocked = isSameInstanceSessionBoundChangeBlocked({
optionChangeBlocked: props.optionChangeBlocked === true,
committedInstanceId: props.optionChangeBlockedInstanceId ?? "",
requestedInstanceId: currentModelSelection.instanceId,
});
const configurationLabel = useMemo(
() => providerOptionsConfigurationLabel(providerOptionDescriptors),
[providerOptionDescriptors],
Expand Down Expand Up @@ -689,11 +706,23 @@ export const ThreadComposer = memo(function ThreadComposer(props: ThreadComposer
}

function handleOptionsMenuAction(event: string) {
const providerOptions = applyProviderOptionMenuEvent(providerOptionDescriptors, event);
if (providerOptions) {
// Session-bound options stay tappable; the pure resolver ignores the
// current value, warns on locked alternates, and only returns an update
// payload when the change may apply.
const decision = resolveProviderOptionMenuChange(providerOptionDescriptors, event, {
optionsLocked: providerOptionsLocked,
});
if (decision) {
if (decision.action === "ignore") {
return;
}
if (decision.action === "warn") {
Alert.alert(decision.title, decision.description);
return;
}
props.onUpdateModelSelection({
...currentModelSelection,
options: providerOptions,
options: decision.options,
});
return;
}
Expand Down
4 changes: 4 additions & 0 deletions apps/mobile/src/features/threads/ThreadDetailScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,8 @@ export interface ThreadDetailScreenProps {
readonly onUpdateThreadModelSelection: (modelSelection: ModelSelection) => void;
readonly onUpdateThreadRuntimeMode: (runtimeMode: RuntimeMode) => void;
readonly onUpdateThreadInteractionMode: (interactionMode: ProviderInteractionMode) => void;
readonly optionChangeBlocked?: boolean;
readonly optionChangeBlockedInstanceId?: string | null;
readonly onRespondToApproval: (
requestId: RuntimeRequestId,
decision: ProviderApprovalDecision,
Expand Down Expand Up @@ -496,6 +498,8 @@ export const ThreadDetailScreen = memo(function ThreadDetailScreen(props: Thread
onUpdateModelSelection={props.onUpdateThreadModelSelection}
onUpdateRuntimeMode={props.onUpdateThreadRuntimeMode}
onUpdateInteractionMode={props.onUpdateThreadInteractionMode}
optionChangeBlocked={props.optionChangeBlocked}
optionChangeBlockedInstanceId={props.optionChangeBlockedInstanceId}
onExpandedChange={setComposerExpanded}
/>
</View>
Expand Down
2 changes: 2 additions & 0 deletions apps/mobile/src/features/threads/ThreadRouteScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -781,6 +781,8 @@ function ThreadRouteContent(
onUpdateThreadModelSelection={composer.onUpdateModelSelection}
onUpdateThreadRuntimeMode={composer.onUpdateRuntimeMode}
onUpdateThreadInteractionMode={composer.onUpdateInteractionMode}
optionChangeBlocked={composer.optionChangeBlocked}
optionChangeBlockedInstanceId={composer.optionChangeBlockedInstanceId}
onRespondToApproval={requests.onRespondToApproval}
onSelectUserInputOption={requests.onSelectUserInputOption}
onChangeUserInputCustomAnswer={requests.onChangeUserInputCustomAnswer}
Expand Down
Loading
Loading