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
13 changes: 12 additions & 1 deletion apps/mobile/src/app/task/[id].tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,17 @@ import {
DEFAULT_GATEWAY_MODEL,
DEFAULT_REASONING_EFFORT,
type ExecutionMode,
isModalModelId,
isSupportedReasoningEffort,
KIMI_MODEL_FLAG,
type SupportedReasoningEffort,
serializeCloudPrompt,
type Task,
} from "@posthog/shared";
import { useQueryClient } from "@tanstack/react-query";
import * as Haptics from "expo-haptics";
import { useLocalSearchParams, useRouter } from "expo-router";
import { useFeatureFlag } from "posthog-react-native";
import { useCallback, useEffect, useRef, useState } from "react";
import {
ActivityIndicator,
Expand Down Expand Up @@ -187,10 +190,18 @@ export default function TaskDetailScreen() {
const composerMode: ExecutionMode =
(composerConfigMatchesAdapter ? composerConfig?.mode : undefined) ??
getDefaultExecutionModeForAdapter(composerAdapter);
const composerModel =
const kimiEnabled = !!useFeatureFlag(KIMI_MODEL_FLAG);
const persistedComposerModel =
(composerConfigMatchesAdapter ? composerConfig?.model : undefined) ??
task?.latest_run?.model ??
(composerAdapter === "codex" ? DEFAULT_CODEX_MODEL : DEFAULT_GATEWAY_MODEL);
// Fall a persisted Kimi selection back to the default when the flag is off so
// a hidden model never gets sent on the retry-after-terminal path. The
// composer independently re-resolves against the live config once mounted.
const composerModel =
!kimiEnabled && isModalModelId(persistedComposerModel)
? DEFAULT_GATEWAY_MODEL
: persistedComposerModel;
const requestedComposerReasoning = composerConfigMatchesAdapter
? composerConfig?.reasoning
: undefined;
Expand Down
17 changes: 14 additions & 3 deletions apps/mobile/src/app/task/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
type ExecutionMode,
getReasoningEffortOptions,
isSupportedReasoningEffort,
KIMI_MODEL_FLAG,
type SupportedReasoningEffort,
serializeCloudPrompt,
} from "@posthog/shared";
Expand All @@ -24,7 +25,8 @@ import {
PaperclipIcon,
StopIcon,
} from "phosphor-react-native";
import { useCallback, useEffect, useState } from "react";
import { useFeatureFlag } from "posthog-react-native";
import { useCallback, useEffect, useMemo, useState } from "react";
import {
ActivityIndicator,
Pressable,
Expand Down Expand Up @@ -53,6 +55,7 @@ import {
import type { PendingAttachment } from "@/features/tasks/composer/attachments/types";
import { DotBackground } from "@/features/tasks/composer/DotBackground";
import {
filterKimiModelConfigOptions,
getMobileExecutionModes,
getModelConfigOption,
} from "@/features/tasks/composer/options";
Expand Down Expand Up @@ -104,8 +107,16 @@ export default function NewTaskScreen() {
const keyboard = useReanimatedKeyboardAnimation();
const restingBottom = bottom("compact");
const [adapter, setAdapter] = useState<Adapter>("claude");
const { configOptions, hasLiveConfig, isConfigReady } =
useCloudTaskConfigOptions(adapter);
const {
configOptions: liveConfigOptions,
hasLiveConfig,
isConfigReady,
} = useCloudTaskConfigOptions(adapter);
const kimiEnabled = !!useFeatureFlag(KIMI_MODEL_FLAG);
const configOptions = useMemo(
() => filterKimiModelConfigOptions(liveConfigOptions, kimiEnabled),
[liveConfigOptions, kimiEnabled],
);
const modelConfigOption = getModelConfigOption(configOptions);
const {
error,
Expand Down
18 changes: 15 additions & 3 deletions apps/mobile/src/features/tasks/composer/TaskChatComposer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
DEFAULT_GATEWAY_MODEL,
DEFAULT_REASONING_EFFORT,
type ExecutionMode,
KIMI_MODEL_FLAG,
type SupportedReasoningEffort,
} from "@posthog/shared";
import * as Haptics from "expo-haptics";
Expand All @@ -19,7 +20,8 @@ import {
Stack,
Stop,
} from "phosphor-react-native";
import { useCallback, useEffect, useState } from "react";
import { useFeatureFlag } from "posthog-react-native";
import { useCallback, useEffect, useMemo, useState } from "react";
import {
ActivityIndicator,
Keyboard,
Expand All @@ -42,7 +44,11 @@ import {
pickPhotoFromLibrary,
} from "./attachments/pickers";
import type { PendingAttachment } from "./attachments/types";
import { getModelConfigOption, resolveComposerPrimaryAction } from "./options";
import {
filterKimiModelConfigOptions,
getModelConfigOption,
resolveComposerPrimaryAction,
} from "./options";
import { Pill } from "./Pill";
import {
type ComposerContent,
Expand Down Expand Up @@ -106,7 +112,13 @@ export function TaskChatComposer({
onCancelEdit,
}: TaskChatComposerProps) {
const themeColors = useThemeColors();
const { configOptions, hasLiveConfig } = useCloudTaskConfigOptions(adapter);
const { configOptions: liveConfigOptions, hasLiveConfig } =
useCloudTaskConfigOptions(adapter);
const kimiEnabled = !!useFeatureFlag(KIMI_MODEL_FLAG);
const configOptions = useMemo(
() => filterKimiModelConfigOptions(liveConfigOptions, kimiEnabled),
[liveConfigOptions, kimiEnabled],
);
const modelConfigOption = getModelConfigOption(configOptions);
const [message, setMessage] = useState(() => initialMessage ?? "");
const [attachments, setAttachments] = useState<PendingAttachment[]>([]);
Expand Down
75 changes: 75 additions & 0 deletions apps/mobile/src/features/tasks/composer/options.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,28 @@ import {
} from "@posthog/shared";
import { describe, expect, it } from "vitest";
import {
filterKimiModelConfigOptions,
filterKimiModelOption,
getComposerModelOptions,
getMobileExecutionModes,
resolveComposerPrimaryAction,
} from "./options";

const KIMI = "moonshotai/kimi-k3";

const kimiModelOption: CloudTaskConfigOption = {
id: "model",
name: "Model",
type: "select",
currentValue: DEFAULT_GATEWAY_MODEL,
options: [
{ value: DEFAULT_GATEWAY_MODEL, name: "Claude Opus 4.8" },
{ value: KIMI, name: "Kimi K3" },
],
category: "model",
description: "Choose a model",
};

const modelOption: CloudTaskConfigOption = {
id: "model",
name: "Model",
Expand Down Expand Up @@ -63,6 +80,64 @@ describe("mobile composer options", () => {
]);
});

describe("filterKimiModelOption", () => {
it("keeps Kimi K3 when the flag is on", () => {
const filtered = filterKimiModelOption(kimiModelOption, true);
expect(filtered.options.map((o) => o.value)).toContain(KIMI);
});

it("drops Kimi K3 when the flag is off", () => {
const filtered = filterKimiModelOption(kimiModelOption, false);
expect(filtered.options.map((o) => o.value)).not.toContain(KIMI);
expect(filtered.options.map((o) => o.value)).toEqual([
DEFAULT_GATEWAY_MODEL,
]);
});

it("rewrites a persisted Kimi selection to a visible model when the flag is off", () => {
const filtered = filterKimiModelOption(
{ ...kimiModelOption, currentValue: KIMI },
false,
);
expect(filtered.currentValue).toBe(DEFAULT_GATEWAY_MODEL);
});

it("leaves an already-visible selection untouched when the flag is off", () => {
const filtered = filterKimiModelOption(kimiModelOption, false);
expect(filtered.currentValue).toBe(DEFAULT_GATEWAY_MODEL);
});
});

describe("filterKimiModelConfigOptions", () => {
const modeOption: CloudTaskConfigOption = {
id: "mode",
name: "Mode",
type: "select",
currentValue: "plan",
options: [{ value: "plan", name: "Plan" }],
category: "mode",
description: "Execution mode",
};

it("returns the config set untouched when the flag is on", () => {
const input = [modeOption, kimiModelOption];
expect(filterKimiModelConfigOptions(input, true)).toBe(input);
});

it("strips Kimi from only the model option when the flag is off", () => {
const filtered = filterKimiModelConfigOptions(
[modeOption, kimiModelOption],
false,
);
const model = filtered.find((option) => option.category === "model");
const mode = filtered.find((option) => option.category === "mode");
expect(model?.options.map((o) => o.value)).toEqual([
DEFAULT_GATEWAY_MODEL,
]);
expect(mode).toBe(modeOption);
});
});

it.each([
[{ hasContent: true }, "send"],
[{ canStop: true }, "stop"],
Expand Down
40 changes: 40 additions & 0 deletions apps/mobile/src/features/tasks/composer/options.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { ModeInfo } from "@posthog/core/sessions/executionModes";
import {
type CloudTaskConfigOption,
isModalModelId,
isRestrictedModelOption,
} from "@posthog/shared";

Expand All @@ -27,6 +28,45 @@ export function getModelConfigOption(
return option;
}

/**
* Drops the Kimi K3 model from the live model config when the feature flag is
* off, and rewrites a persisted or server-default Kimi selection to the first
* remaining option so it never leaks into the picker. Mirrors the desktop
* `stripKimiModelOption` filter, but over the mobile `CloudTaskConfigOption`.
*/
export function filterKimiModelOption(
modelOption: CloudTaskConfigOption,
kimiEnabled: boolean,
): CloudTaskConfigOption {
if (kimiEnabled) return modelOption;
const options = modelOption.options.filter(
(option) => !isModalModelId(option.value),
);
return {
...modelOption,
options,
currentValue: isModalModelId(modelOption.currentValue)
? (options[0]?.value ?? modelOption.currentValue)
: modelOption.currentValue,
};
}

/**
* Applies {@link filterKimiModelOption} to the model option within a live
* config set, leaving the other categories untouched. Callers filter once at
* the source so the model auto-resolve effect and the picker share Kimi-free
* options.
*/
export function filterKimiModelConfigOptions(
configOptions: readonly CloudTaskConfigOption[],
kimiEnabled: boolean,
): readonly CloudTaskConfigOption[] {
if (kimiEnabled) return configOptions;
return configOptions.map((option) =>
option.category === "model" ? filterKimiModelOption(option, false) : option,
);
}

export function getComposerModelOptions(
modelOption: CloudTaskConfigOption,
): MobileModelOption[] {
Expand Down
Loading