Skip to content

Commit 65fe2d0

Browse files
Share composer control semantics
Generated-By: PostHog Code Task-Id: 40c57a59-b4e1-4760-8e56-ecd03e9c2f0f
1 parent d85caff commit 65fe2d0

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
@@ -3,6 +3,12 @@ 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+
} from "@posthog/core/task-detail/composerControls";
612
import {
713
DEFAULT_GATEWAY_MODEL,
814
DEFAULT_REASONING_EFFORT,
@@ -55,12 +61,6 @@ import {
5561
} from "@/features/tasks/composer/attachments/pickers";
5662
import type { PendingAttachment } from "@/features/tasks/composer/attachments/types";
5763
import { DotBackground } from "@/features/tasks/composer/DotBackground";
58-
import {
59-
getMobileModelOptions,
60-
getModelConfigOption,
61-
getModelLabel,
62-
resolveAvailableModel,
63-
} from "@/features/tasks/composer/options";
6464
import { Pill } from "@/features/tasks/composer/Pill";
6565
import { RepositoryPickerInline } from "@/features/tasks/composer/RepositoryPickerInline";
6666
import { SelectSheet } from "@/features/tasks/composer/SelectSheet";
@@ -131,7 +131,7 @@ export default function NewTaskScreen() {
131131
const restingBottom = bottom("compact");
132132
const { configOptions, hasLiveConfig } = useCloudTaskConfigOptions("claude");
133133
const modelConfigOption = getModelConfigOption(configOptions);
134-
const mobileModelOptions = getMobileModelOptions(modelConfigOption);
134+
const mobileModelOptions = getComposerModelOptions(modelConfigOption);
135135
const {
136136
error,
137137
hasGithubIntegration,
@@ -214,12 +214,14 @@ export default function NewTaskScreen() {
214214

215215
useEffect(() => {
216216
if (!hasLiveConfig) return;
217-
const availableModel = resolveAvailableModel(modelConfigOption, model);
218-
if (availableModel === model) return;
219-
setModel(availableModel);
220-
if (!isSupportedReasoningEffort("claude", availableModel, reasoning)) {
221-
setReasoning(DEFAULT_REASONING_EFFORT);
222-
}
217+
const next = resolveComposerModelChange({
218+
adapter: "claude",
219+
modelOption: modelConfigOption,
220+
requestedModel: model,
221+
reasoning,
222+
});
223+
if (next.model !== model) setModel(next.model);
224+
if (next.reasoning !== reasoning) setReasoning(next.reasoning);
223225
}, [hasLiveConfig, model, modelConfigOption, reasoning]);
224226
const [creating, setCreating] = useState(false);
225227
const [repoSheetOpen, setRepoSheetOpen] = useState(false);
@@ -649,7 +651,12 @@ export default function NewTaskScreen() {
649651

650652
<Pill
651653
icon={<Robot size={14} color={themeColors.gray[11]} />}
652-
label={getModelLabel(modelConfigOption, model)}
654+
label={
655+
getConfigOptionLabel(
656+
modelConfigOption.options,
657+
model,
658+
) ?? model
659+
}
653660
onPress={() => setModelSheetOpen(true)}
654661
/>
655662

@@ -769,10 +776,14 @@ export default function NewTaskScreen() {
769776
title="Model"
770777
value={model}
771778
onChange={(value) => {
772-
setModel(value);
773-
if (!isSupportedReasoningEffort("claude", value, reasoning)) {
774-
setReasoning(DEFAULT_REASONING_EFFORT);
775-
}
779+
const next = resolveComposerModelChange({
780+
adapter: "claude",
781+
modelOption: modelConfigOption,
782+
requestedModel: value,
783+
reasoning,
784+
});
785+
setModel(next.model);
786+
setReasoning(next.reasoning);
776787
}}
777788
onClose={() => setModelSheetOpen(false)}
778789
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
@@ -3,6 +3,7 @@ import {
33
formatSignalReportSummaryMarkdown,
44
inboxStatusLabel,
55
} from "@posthog/core/inbox/reportPresentation";
6+
import { getModelConfigOption } from "@posthog/core/task-detail/composerControls";
67
import type {
78
SignalReport,
89
SignalReportPriority,
@@ -26,7 +27,6 @@ import {
2627
} from "react-native-safe-area-context";
2728
import { MarkdownText } from "@/features/chat/components/MarkdownText";
2829
import { usePreferencesStore } from "@/features/preferences/stores/preferencesStore";
29-
import { getModelConfigOption } from "@/features/tasks/composer/options";
3030
import { useCloudTaskConfigOptions } from "@/features/tasks/hooks/useCloudTaskConfigOptions";
3131
import type {
3232
CreateTaskOptions,

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)