Skip to content

Commit 2f7181a

Browse files
Share composer control semantics
Generated-By: PostHog Code Task-Id: 40c57a59-b4e1-4760-8e56-ecd03e9c2f0f
1 parent 0e59e8e commit 2f7181a

9 files changed

Lines changed: 197 additions & 168 deletions

File tree

apps/mobile/src/app/task/index.tsx

Lines changed: 29 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,12 @@ import {
99
DEFAULT_CLAUDE_EXECUTION_MODE,
1010
getAvailableModes,
1111
} from "@posthog/core/sessions/executionModes";
12+
import {
13+
getComposerModelOptions,
14+
getConfigOptionLabel,
15+
getModelConfigOption,
16+
resolveComposerModelChange,
17+
} from "@posthog/core/task-detail/composerControls";
1218
import {
1319
DEFAULT_GATEWAY_MODEL,
1420
DEFAULT_REASONING_EFFORT,
@@ -61,12 +67,6 @@ import {
6167
} from "@/features/tasks/composer/attachments/pickers";
6268
import type { PendingAttachment } from "@/features/tasks/composer/attachments/types";
6369
import { DotBackground } from "@/features/tasks/composer/DotBackground";
64-
import {
65-
getMobileModelOptions,
66-
getModelConfigOption,
67-
getModelLabel,
68-
resolveAvailableModel,
69-
} from "@/features/tasks/composer/options";
7070
import { Pill } from "@/features/tasks/composer/Pill";
7171
import { RepositoryPickerInline } from "@/features/tasks/composer/RepositoryPickerInline";
7272
import { SelectSheet } from "@/features/tasks/composer/SelectSheet";
@@ -129,7 +129,7 @@ export default function NewTaskScreen() {
129129
const restingBottom = bottom("compact");
130130
const { configOptions, hasLiveConfig } = useCloudTaskConfigOptions("claude");
131131
const modelConfigOption = getModelConfigOption(configOptions);
132-
const mobileModelOptions = getMobileModelOptions(modelConfigOption);
132+
const mobileModelOptions = getComposerModelOptions(modelConfigOption);
133133
const {
134134
error,
135135
hasGithubIntegration,
@@ -212,12 +212,14 @@ export default function NewTaskScreen() {
212212

213213
useEffect(() => {
214214
if (!hasLiveConfig) return;
215-
const availableModel = resolveAvailableModel(modelConfigOption, model);
216-
if (availableModel === model) return;
217-
setModel(availableModel);
218-
if (!isSupportedReasoningEffort("claude", availableModel, reasoning)) {
219-
setReasoning(DEFAULT_REASONING_EFFORT);
220-
}
215+
const next = resolveComposerModelChange({
216+
adapter: "claude",
217+
modelOption: modelConfigOption,
218+
requestedModel: model,
219+
reasoning,
220+
});
221+
if (next.model !== model) setModel(next.model);
222+
if (next.reasoning !== reasoning) setReasoning(next.reasoning);
221223
}, [hasLiveConfig, model, modelConfigOption, reasoning]);
222224
const [creating, setCreating] = useState(false);
223225
const [repoSheetOpen, setRepoSheetOpen] = useState(false);
@@ -647,7 +649,12 @@ export default function NewTaskScreen() {
647649

648650
<Pill
649651
icon={<Robot size={14} color={themeColors.gray[11]} />}
650-
label={getModelLabel(modelConfigOption, model)}
652+
label={
653+
getConfigOptionLabel(
654+
modelConfigOption.options,
655+
model,
656+
) ?? model
657+
}
651658
onPress={() => setModelSheetOpen(true)}
652659
/>
653660

@@ -767,10 +774,14 @@ export default function NewTaskScreen() {
767774
title="Model"
768775
value={model}
769776
onChange={(value) => {
770-
setModel(value);
771-
if (!isSupportedReasoningEffort("claude", value, reasoning)) {
772-
setReasoning(DEFAULT_REASONING_EFFORT);
773-
}
777+
const next = resolveComposerModelChange({
778+
adapter: "claude",
779+
modelOption: modelConfigOption,
780+
requestedModel: value,
781+
reasoning,
782+
});
783+
setModel(next.model);
784+
setReasoning(next.reasoning);
774785
}}
775786
onClose={() => setModelSheetOpen(false)}
776787
options={mobileModelOptions.map((modelOption) => ({

apps/mobile/src/features/inbox/components/TinderView.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import {
44
inboxStatusLabel,
55
} from "@posthog/core/inbox/reportPresentation";
66
import type { RepositoryOption } from "@posthog/core/integrations/repositories";
7+
import { getModelConfigOption } from "@posthog/core/task-detail/composerControls";
78
import type {
89
SignalReport,
910
SignalReportPriority,
@@ -27,7 +28,6 @@ import {
2728
} from "react-native-safe-area-context";
2829
import { MarkdownText } from "@/features/chat/components/MarkdownText";
2930
import { usePreferencesStore } from "@/features/preferences/stores/preferencesStore";
30-
import { getModelConfigOption } from "@/features/tasks/composer/options";
3131
import { useCloudTaskConfigOptions } from "@/features/tasks/hooks/useCloudTaskConfigOptions";
3232
import type { CreateTaskOptions } from "@/features/tasks/types";
3333
import {

apps/mobile/src/features/tasks/composer/TaskChatComposer.tsx

Lines changed: 38 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,18 @@ import {
33
DEFAULT_CLAUDE_EXECUTION_MODE,
44
getAvailableModes,
55
} from "@posthog/core/sessions/executionModes";
6+
import {
7+
getComposerModelOptions,
8+
getConfigOptionLabel,
9+
getModelConfigOption,
10+
resolveComposerModelChange,
11+
resolveComposerPrimaryAction,
12+
} from "@posthog/core/task-detail/composerControls";
613
import {
714
DEFAULT_GATEWAY_MODEL,
815
DEFAULT_REASONING_EFFORT,
916
type ExecutionMode,
1017
getReasoningEffortOptions,
11-
isSupportedReasoningEffort,
1218
type SupportedReasoningEffort,
1319
} from "@posthog/shared";
1420
import * as Haptics from "expo-haptics";
@@ -56,12 +62,6 @@ import {
5662
pickPhotoFromLibrary,
5763
} from "./attachments/pickers";
5864
import type { PendingAttachment } from "./attachments/types";
59-
import {
60-
getMobileModelOptions,
61-
getModelConfigOption,
62-
getModelLabel,
63-
resolveAvailableModel,
64-
} from "./options";
6565
import { Pill } from "./Pill";
6666
import { SelectSheet } from "./SelectSheet";
6767

@@ -188,7 +188,7 @@ export function TaskChatComposer({
188188
const themeColors = useThemeColors();
189189
const { configOptions, hasLiveConfig } = useCloudTaskConfigOptions("claude");
190190
const modelConfigOption = getModelConfigOption(configOptions);
191-
const mobileModelOptions = getMobileModelOptions(modelConfigOption);
191+
const mobileModelOptions = getComposerModelOptions(modelConfigOption);
192192
const [message, setMessage] = useState(() => initialMessage ?? "");
193193
const [attachments, setAttachments] = useState<PendingAttachment[]>([]);
194194
const [attachmentSheetOpen, setAttachmentSheetOpen] = useState(false);
@@ -206,12 +206,14 @@ export function TaskChatComposer({
206206

207207
useEffect(() => {
208208
if (!hasLiveConfig) return;
209-
const availableModel = resolveAvailableModel(modelConfigOption, model);
210-
if (availableModel === model) return;
211-
onModelChange(availableModel);
212-
if (!isSupportedReasoningEffort("claude", availableModel, reasoning)) {
213-
onReasoningChange(DEFAULT_REASONING_EFFORT);
214-
}
209+
const next = resolveComposerModelChange({
210+
adapter: "claude",
211+
modelOption: modelConfigOption,
212+
requestedModel: model,
213+
reasoning,
214+
});
215+
if (next.model !== model) onModelChange(next.model);
216+
if (next.reasoning !== reasoning) onReasoningChange(next.reasoning);
215217
}, [
216218
hasLiveConfig,
217219
model,
@@ -239,9 +241,16 @@ export function TaskChatComposer({
239241
const showReasoningPill = reasoningOptions.length > 0;
240242

241243
const hasContent = message.trim().length > 0 || attachments.length > 0;
242-
const canSend = hasContent && !disabled && !isRecording;
243-
const showStop =
244-
!isUserTurn && !canSend && !isRecording && !isTranscribing && !!onStop;
244+
const primaryAction = resolveComposerPrimaryAction({
245+
hasContent,
246+
disabled,
247+
isRecording,
248+
isTranscribing,
249+
canStop: !isUserTurn && !!onStop,
250+
allowSendWhileRunning: true,
251+
});
252+
const canSend = primaryAction === "send";
253+
const showStop = primaryAction === "stop";
245254

246255
const handleSend = () => {
247256
const trimmed = message.trim();
@@ -410,7 +419,10 @@ export function TaskChatComposer({
410419

411420
<Pill
412421
icon={<Robot size={14} color={themeColors.gray[11]} />}
413-
label={getModelLabel(modelConfigOption, model)}
422+
label={
423+
getConfigOptionLabel(modelConfigOption.options, model) ??
424+
model
425+
}
414426
onPress={() => setModelSheetOpen(true)}
415427
/>
416428

@@ -486,10 +498,14 @@ export function TaskChatComposer({
486498
title="Model"
487499
value={model}
488500
onChange={(v) => {
489-
onModelChange(v);
490-
if (!isSupportedReasoningEffort("claude", v, reasoning)) {
491-
onReasoningChange(DEFAULT_REASONING_EFFORT);
492-
}
501+
const next = resolveComposerModelChange({
502+
adapter: "claude",
503+
modelOption: modelConfigOption,
504+
requestedModel: v,
505+
reasoning,
506+
});
507+
onModelChange(next.model);
508+
onReasoningChange(next.reasoning);
493509
}}
494510
onClose={() => setModelSheetOpen(false)}
495511
options={mobileModelOptions.map((m) => ({

apps/mobile/src/features/tasks/composer/options.test.ts

Lines changed: 0 additions & 68 deletions
This file was deleted.

apps/mobile/src/features/tasks/composer/options.ts

Lines changed: 0 additions & 56 deletions
This file was deleted.

apps/mobile/src/features/tasks/hooks/useCloudTaskConfigOptions.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ vi.mock("@/lib/posthogApiClient", () => ({
2626
}),
2727
}));
2828

29-
import { getModelConfigOption } from "../composer/options";
29+
import { getModelConfigOption } from "@posthog/core/task-detail/composerControls";
3030
import { useCloudTaskConfigOptions } from "./useCloudTaskConfigOptions";
3131

3232
function createWrapper(queryClient: QueryClient) {
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import { describe, expect, it } from "vitest";
2+
import { resolveComposerPrimaryAction } from "./composerControls";
3+
4+
describe("resolveComposerPrimaryAction", () => {
5+
it.each([
6+
[{ hasContent: true }, "send"],
7+
[{ canStop: true }, "stop"],
8+
[{ canStop: true, hasContent: true }, "send"],
9+
[{ canStop: true, hasContent: true, allowSendWhileRunning: false }, "stop"],
10+
[{ isRecording: true }, "mic-stop"],
11+
[{}, "mic"],
12+
[{ disabled: true, hasContent: true }, "disabled"],
13+
[{ isTranscribing: true }, "disabled"],
14+
])("derives %s", (overrides, expected) => {
15+
expect(
16+
resolveComposerPrimaryAction({
17+
hasContent: false,
18+
disabled: false,
19+
isRecording: false,
20+
isTranscribing: false,
21+
canStop: false,
22+
allowSendWhileRunning: true,
23+
...overrides,
24+
}),
25+
).toBe(expected);
26+
});
27+
});

0 commit comments

Comments
 (0)