Skip to content
Closed
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
16 changes: 15 additions & 1 deletion apps/mobile/src/components/ProviderIcon.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Image } from "expo-image";
import { Path, Svg } from "react-native-svg";
import { Path, Rect, Svg } from "react-native-svg";
import { useAppearancePreferences } from "../features/settings/appearance/AppearancePreferencesProvider";

type ProviderIconProps = {
Expand All @@ -23,6 +23,20 @@ export function ProviderIcon(props: ProviderIconProps) {
);
}

if (props.provider?.trim().toLowerCase() === "piagent") {
return (
<Svg width={size} height={size} viewBox="0 0 800 800" fill="none">
<Rect width="800" height="800" rx="160" fill="#000" />
<Path
fill="#fff"
fillRule="evenodd"
d="M165.29 165.29H517.36V400H400V517.36H282.65V634.72H165.29ZM282.65 282.65V400H400V282.65Z"
/>
<Path fill="#fff" d="M517.36 400H634.72V634.72H517.36Z" />
</Svg>
);
}

if (props.provider === "claudeAgent") {
return (
<Svg width={size} height={size} viewBox="0 0 256 257" fill="none">
Expand Down
5 changes: 4 additions & 1 deletion apps/mobile/src/features/threads/NewTaskDraftScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -880,7 +880,10 @@ export function NewTaskDraftScreen(props: {
const selectedWorktreePath =
draft.workspaceSelection?.worktreePath ?? flow.selectedWorktreePath;
const startFromOrigin = draft.workspaceSelection?.startFromOrigin ?? flow.startFromOrigin;
const runtimeMode = draft.runtimeMode ?? flow.runtimeMode;
// The flow reconciles the draft mode against the selected provider's
// current capabilities. Dispatch that value even when the draft still
// contains the pre-switch mode.
const runtimeMode = flow.runtimeMode;
const interactionMode = resolveProviderInteractionMode(
selectedEnvironmentServerConfig?.providers.find(
(provider) => provider.instanceId === modelSelection?.instanceId,
Expand Down
4 changes: 4 additions & 0 deletions apps/mobile/src/features/threads/ThreadComposer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import type {
RuntimeMode,
ServerConfig as T3ServerConfig,
} from "@t3tools/contracts";
import { getProviderSupportedRuntimeModes } from "@t3tools/client-runtime/runtime-mode-options";
import { StackActions, useFocusEffect, useNavigation } from "@react-navigation/native";
import type { ReactNode } from "react";
import {
Expand Down Expand Up @@ -335,6 +336,7 @@ export const ThreadComposer = memo(function ThreadComposer(props: ThreadComposer
) ?? null
);
}, [props.serverConfig, props.selectedThread.modelSelection.instanceId]);
const supportedRuntimeModes = getProviderSupportedRuntimeModes(selectedProviderStatus);
const composerOwnerKey = scopedThreadKey(props.environmentId, props.selectedThread.id);

const composerMenu = useComposerCommandMenu({
Expand Down Expand Up @@ -500,6 +502,7 @@ export const ThreadComposer = memo(function ThreadComposer(props: ThreadComposer
onUpdateOptionSelections: (options) =>
props.onUpdateModelSelection({ ...currentModelSelection, options }),
runtimeMode: currentRuntimeMode,
supportedRuntimeModes,
onUpdateRuntimeMode: props.onUpdateRuntimeMode,
}),
[
Expand All @@ -508,6 +511,7 @@ export const ThreadComposer = memo(function ThreadComposer(props: ThreadComposer
props.onUpdateModelSelection,
props.onUpdateRuntimeMode,
providerOptionDescriptors,
supportedRuntimeModes,
settingsOwnerId,
threadProviderGroups,
],
Expand Down
46 changes: 41 additions & 5 deletions apps/mobile/src/features/threads/ThreadSettingsSheet.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ import type {
RuntimeMode,
ServerProvider,
} from "@t3tools/contracts";
import {
getProviderSupportedRuntimeModes,
reconcileRuntimeMode,
} from "@t3tools/client-runtime/runtime-mode-options";
import { useAtomValue } from "@effect/atom-react";
import type { LegendListRenderItemProps } from "@legendapp/list/react-native";
import { AnimatedLegendList } from "@legendapp/list/reanimated";
Expand Down Expand Up @@ -68,7 +72,7 @@ import {
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 { filterRuntimeModeChoices, selectableChoices } from "./thread-settings-options";
import {
canCommitPendingModel,
modelMatchesCatalogQuery,
Expand Down Expand Up @@ -329,6 +333,7 @@ type ThreadSettingsSessionProps = {
readonly optionDescriptors: ReadonlyArray<ProviderOptionDescriptor>;
readonly onUpdateOptionSelections: (selections: ReadonlyArray<ProviderOptionSelection>) => void;
readonly runtimeMode: RuntimeMode;
readonly supportedRuntimeModes?: ReadonlyArray<RuntimeMode> | undefined;
readonly onUpdateRuntimeMode: (mode: RuntimeMode) => void;
};

Expand Down Expand Up @@ -378,6 +383,11 @@ type ThreadSettingsSessionValue = {
readonly providerInstanceId?: ProviderInstanceId;
readonly providerGroups: ReadonlyArray<ProviderGroup>;
readonly runtimeMode: RuntimeMode;
readonly runtimeModeChoices: ReadonlyArray<{
readonly mode: RuntimeMode;
readonly label: string;
readonly description: string;
}>;
readonly onUpdateRuntimeMode: (mode: RuntimeMode) => void;
readonly displayedDescriptors: ReadonlyArray<ProviderOptionDescriptor>;
readonly providerExpansionOverrides: ReadonlySet<string>;
Expand Down Expand Up @@ -410,6 +420,28 @@ function ThreadSettingsSessionProvider(
() => new Set(),
);
const [pendingModel, setPendingModel] = useState<ModelOption | null>(null);
// Model selection is staged in this sheet. Resolve runtime capabilities from
// that staged option so switching to a narrower provider immediately
// updates the Runtime submenu before Save applies the model.
// Always read the staged option back from the current provider snapshot:
// status refreshes can change capabilities while this sheet is open.
const latestPendingModel = useMemo(
() =>
pendingModel
? props.providerGroups
.flatMap((group) => group.models)
.find((option) => option.key === pendingModel.key)
: undefined,
[pendingModel, props.providerGroups],
);
const stagedSupportedRuntimeModes = pendingModel
? latestPendingModel?.supportedRuntimeModes
: props.supportedRuntimeModes;
const runtimeMode = reconcileRuntimeMode(props.runtimeMode, stagedSupportedRuntimeModes);
const runtimeModeChoices = useMemo(
() => filterRuntimeModeChoices(stagedSupportedRuntimeModes),
[stagedSupportedRuntimeModes],
);

const isApplied = useCallback(
(option: ModelOption) =>
Expand Down Expand Up @@ -502,7 +534,8 @@ function ThreadSettingsSessionProvider(
environmentId: props.environmentId,
providerInstanceId: props.providerInstanceId,
providerGroups: props.providerGroups,
runtimeMode: props.runtimeMode,
runtimeMode,
runtimeModeChoices,
onUpdateRuntimeMode: props.onUpdateRuntimeMode,
displayedDescriptors,
providerExpansionOverrides,
Expand Down Expand Up @@ -536,7 +569,8 @@ function ThreadSettingsSessionProvider(
providerFilter,
props.onUpdateRuntimeMode,
props.providerGroups,
props.runtimeMode,
runtimeMode,
runtimeModeChoices,
searchQuery,
showLegacyToggle,
toggleProvider,
Expand Down Expand Up @@ -766,7 +800,8 @@ function ThreadSettingsOptionsItem(props: {
isLast
label="Runtime"
value={
RUNTIME_MODE_CHOICES.find((choice) => choice.mode === session.runtimeMode)?.label
session.runtimeModeChoices.find((choice) => choice.mode === session.runtimeMode)
?.label
}
onPress={() => props.onOpenSubmenu({ kind: "runtime" })}
/>
Expand Down Expand Up @@ -940,7 +975,7 @@ function ThreadSettingsChoiceContent(props: {
const submenuContent =
props.submenu.kind === "runtime"
? {
rows: RUNTIME_MODE_CHOICES.map((choice) => ({
rows: session.runtimeModeChoices.map((choice) => ({
id: choice.mode,
label: choice.label,
description: choice.description,
Expand Down Expand Up @@ -1350,6 +1385,7 @@ export function NewTaskThreadSettingsRouteScreen() {
optionDescriptors={optionDescriptors}
onUpdateOptionSelections={flow.setSelectedModelOptions}
runtimeMode={flow.runtimeMode}
supportedRuntimeModes={getProviderSupportedRuntimeModes(flow.selectedProviderStatus)}
onUpdateRuntimeMode={flow.setRuntimeMode}
>
<ThreadSettingsPickerNavigator onClose={() => navigation.goBack()} />
Expand Down
26 changes: 23 additions & 3 deletions apps/mobile/src/features/threads/new-task-flow-provider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,10 @@ import {
} from "../../state/use-remote-environment-registry";
import { EnvironmentProject } from "@t3tools/client-runtime/state/shell";
import { type VcsRef } from "@t3tools/client-runtime/state/vcs";
import {
getProviderSupportedRuntimeModes,
reconcileRuntimeMode,
} from "@t3tools/client-runtime/runtime-mode-options";
import {
buildHomeProjectScopes,
sortHomeProjectScopes,
Expand Down Expand Up @@ -415,7 +419,7 @@ export function NewTaskFlowProvider(props: React.PropsWithChildren) {
draftStartFromOrigin ??
selectedEnvironmentServerConfig?.settings.newWorktreesStartFromOrigin ??
true;
const runtimeMode = selectedProjectDraft.runtimeMode ?? DEFAULT_RUNTIME_MODE;
const configuredRuntimeMode = selectedProjectDraft.runtimeMode ?? DEFAULT_RUNTIME_MODE;

// Antigravity keeps unavailable selections so sign-out or a catalog change
// cannot switch the user's model. Other providers retain their fallback
Expand Down Expand Up @@ -473,6 +477,16 @@ export function NewTaskFlowProvider(props: React.PropsWithChildren) {
) ?? null,
[selectedEnvironmentServerConfig, selectedModel?.instanceId],
);
const runtimeMode = reconcileRuntimeMode(
configuredRuntimeMode,
getProviderSupportedRuntimeModes(selectedProviderStatus),
);
useEffect(() => {
if (selectedProjectDraftKey === null || runtimeMode === configuredRuntimeMode) {
return;
}
updateComposerDraftSettings(selectedProjectDraftKey, { runtimeMode });
}, [configuredRuntimeMode, runtimeMode, selectedProjectDraftKey]);
const planModeEnabled =
legacyPlanModeEnabled && selectedProviderStatus?.showInteractionModeToggle !== false;
const interactionMode = planModeEnabled
Expand All @@ -493,15 +507,20 @@ export function NewTaskFlowProvider(props: React.PropsWithChildren) {
const provider = selectedEnvironmentServerConfig?.providers.find(
(candidate) => candidate.instanceId === selection.instanceId,
);
const runtimeMode = reconcileRuntimeMode(
configuredRuntimeMode,
getProviderSupportedRuntimeModes(provider),
);
updateComposerDraftSettings(selectedProjectDraftKey, {
modelSelection: selection,
...(runtimeMode !== configuredRuntimeMode ? { runtimeMode } : {}),
...(provider?.showInteractionModeToggle === false
? { interactionMode: DEFAULT_PROVIDER_INTERACTION_MODE }
: {}),
});
setStickyComposerModelSelection(selection);
},
[modelOptions, selectedEnvironmentServerConfig, selectedProjectDraftKey],
[configuredRuntimeMode, modelOptions, selectedEnvironmentServerConfig, selectedProjectDraftKey],
);
const setSelectedModelOptions = useCallback(
(options: ReadonlyArray<ProviderOptionSelection> | undefined) => {
Expand Down Expand Up @@ -906,7 +925,7 @@ export function NewTaskFlowProvider(props: React.PropsWithChildren) {
text,
attachments: draft.attachments,
modelSelection: draftModelSelection,
runtimeMode: draft.runtimeMode ?? DEFAULT_RUNTIME_MODE,
runtimeMode,
interactionMode: resolvePendingTaskInteractionMode({
preferenceLoaded: planModePreferenceLoaded,
planModeEnabled: legacyPlanModeEnabled,
Expand Down Expand Up @@ -946,6 +965,7 @@ export function NewTaskFlowProvider(props: React.PropsWithChildren) {
selectedProjectDraftKey,
legacyPlanModeEnabled,
planModePreferenceLoaded,
runtimeMode,
startFromOrigin,
workspaceMode,
],
Expand Down
25 changes: 24 additions & 1 deletion apps/mobile/src/features/threads/thread-settings-options.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import type { ProviderOptionDescriptor } from "@t3tools/contracts";
import { describe, expect, it } from "vite-plus/test";

import { selectableChoices } from "./thread-settings-options";
import { filterRuntimeModeChoices, selectableChoices } from "./thread-settings-options";

const effortDescriptor: Extract<ProviderOptionDescriptor, { type: "select" }> = {
id: "effort",
Expand All @@ -27,3 +27,26 @@ describe("selectableChoices", () => {
]);
});
});

describe("filterRuntimeModeChoices", () => {
it("keeps every runtime mode for providers without the optional capability", () => {
expect(filterRuntimeModeChoices(undefined).map((choice) => choice.mode)).toEqual([
"approval-required",
"auto-accept-edits",
"auto",
"full-access",
]);
});

it("filters to explicitly supported modes while preserving product order", () => {
expect(
filterRuntimeModeChoices(["full-access", "approval-required"]).map((choice) => choice.mode),
).toEqual(["approval-required", "full-access"]);
});

it("keeps the defensive fallback visible for an explicitly empty capability list", () => {
expect(filterRuntimeModeChoices([]).map((choice) => choice.mode)).toEqual([
"approval-required",
]);
});
});
7 changes: 7 additions & 0 deletions apps/mobile/src/features/threads/thread-settings-options.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { ProviderOptionDescriptor, RuntimeMode } from "@t3tools/contracts";
import { filterRuntimeModeOptions } from "@t3tools/client-runtime/runtime-mode-options";

/**
* Desktop-oriented effort keywords that don't belong in the phone picker.
Expand Down Expand Up @@ -36,6 +37,12 @@ export const RUNTIME_MODE_CHOICES: ReadonlyArray<{
},
];

export function filterRuntimeModeChoices(
supportedRuntimeModes: ReadonlyArray<RuntimeMode> | undefined,
) {
return filterRuntimeModeOptions(RUNTIME_MODE_CHOICES, supportedRuntimeModes);
}

export function selectableChoices(
descriptor: Extract<ProviderOptionDescriptor, { type: "select" }>,
) {
Expand Down
12 changes: 11 additions & 1 deletion apps/mobile/src/features/threads/use-project-actions.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
import { useCallback } from "react";

import { scopeThreadRef } from "@t3tools/client-runtime/environment";
import {
getProviderSupportedRuntimeModes,
reconcileRuntimeMode,
} from "@t3tools/client-runtime/runtime-mode-options";
import { EnvironmentProject } from "@t3tools/client-runtime/state/shell";
import { mapAtomCommandResult } from "@t3tools/client-runtime/state/runtime";
import {
Expand Down Expand Up @@ -127,6 +131,12 @@ export function useCreateProjectThread() {
const provider = serverConfig?.providers.find(
(candidate) => candidate.instanceId === input.modelSelection.instanceId,
);
// Attachment preparation above can cross a provider snapshot refresh;
// resolve the mode from the live config immediately before dispatch.
const runtimeMode = reconcileRuntimeMode(
input.runtimeMode,
getProviderSupportedRuntimeModes(provider),
);

const result = await startTurn({
environmentId: input.project.environmentId,
Expand All @@ -141,7 +151,7 @@ export function useCreateProjectThread() {
attachments: input.initialAttachments,
uploadedAttachments: prepared.attachments,
modelSelection: input.modelSelection,
runtimeMode: input.runtimeMode,
runtimeMode,
interactionMode: resolveProviderInteractionMode(provider, input.interactionMode),
workspaceMode: input.envMode,
branch: input.branch,
Expand Down
14 changes: 14 additions & 0 deletions apps/mobile/src/features/usage/usageProviders.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { describe, expect, it, vi } from "vite-plus/test";

vi.mock("../settings/appearance/AppearancePreferencesProvider", () => ({
useAppearancePreferences: () => ({ themeAppearance: "dark" }),
}));

import { PROVIDER_LABEL, PROVIDER_ORDER } from "./usageProviders";

describe("mobile usage provider presentation", () => {
it("includes Pi in charts and model breakdowns", () => {
expect(PROVIDER_ORDER).toContain("pi");
expect(PROVIDER_LABEL.pi).toBe("Pi Agent");
});
});
8 changes: 5 additions & 3 deletions apps/mobile/src/features/usage/usageProviders.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,23 +5,25 @@ import { useAppearancePreferences } from "../settings/appearance/AppearancePrefe
* Series and table order. The chart stacks providers from the bottom in this
* order, so it also fixes which band sits on top of the bars.
*/
export const PROVIDER_ORDER: readonly UsageProviderKind[] = ["codex", "claude", "grok"];
export const PROVIDER_ORDER: readonly UsageProviderKind[] = ["codex", "claude", "grok", "pi"];

export const PROVIDER_LABEL: Record<UsageProviderKind, string> = {
claude: "Claude Code",
codex: "Codex",
grok: "Grok Build",
pi: "Pi Agent",
};

/**
* Claude's brand orange holds in both themes; Codex and Grok are neutrals and
* must flip with the theme or their bars vanish against the matching background.
* Claude and Pi keep their series colours in both themes; Codex and Grok are
* neutrals and must flip or their bars vanish against the matching background.
*/
export function useProviderColors(): Record<UsageProviderKind, string> {
const { themeAppearance: scheme } = useAppearancePreferences();
return {
claude: "#d97757",
codex: scheme === "dark" ? "#e6e6e6" : "#3c3c43",
grok: scheme === "dark" ? "#a1a1aa" : "#52525b",
pi: "#8b9cf6",
};
}
Loading
Loading