Skip to content
This repository was archived by the owner on Aug 6, 2026. It is now read-only.

Commit 602324f

Browse files
refactor(core): extract composer controls
Generated-By: PostHog Code Task-Id: c1bbe3cf-742b-4b24-bf96-d11a18b4cf22
1 parent 6b60e36 commit 602324f

3 files changed

Lines changed: 128 additions & 2 deletions

File tree

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+
});
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
import {
2+
type Adapter,
3+
type CloudTaskConfigOption,
4+
DEFAULT_REASONING_EFFORT,
5+
isRestrictedModelOption,
6+
isSupportedReasoningEffort,
7+
type SupportedReasoningEffort,
8+
} from "@posthog/shared";
9+
10+
export interface ComposerModelOption {
11+
value: string;
12+
label: string;
13+
description?: string;
14+
disabled: boolean;
15+
}
16+
17+
export function getModelConfigOption(
18+
configOptions: readonly CloudTaskConfigOption[],
19+
): CloudTaskConfigOption {
20+
const option = configOptions.find((item) => item.category === "model");
21+
if (!option) throw new Error("Cloud task model configuration is unavailable");
22+
return option;
23+
}
24+
25+
export function getComposerModelOptions(
26+
modelOption: CloudTaskConfigOption,
27+
): ComposerModelOption[] {
28+
return modelOption.options.map((option) => ({
29+
value: option.value,
30+
label: option.name,
31+
description: option.description,
32+
disabled: isRestrictedModelOption(option._meta),
33+
}));
34+
}
35+
36+
export function getConfigOptionLabel(
37+
options: ReadonlyArray<{ value: string; name: string }>,
38+
value: string | undefined,
39+
): string | undefined {
40+
return options.find((option) => option.value === value)?.name ?? value;
41+
}
42+
43+
export function resolveAvailableModel(
44+
modelOption: CloudTaskConfigOption,
45+
value: string,
46+
): string {
47+
const selected = modelOption.options.find((option) => option.value === value);
48+
return selected && !isRestrictedModelOption(selected._meta)
49+
? value
50+
: modelOption.currentValue;
51+
}
52+
53+
export function resolveComposerModelChange({
54+
adapter,
55+
modelOption,
56+
requestedModel,
57+
reasoning,
58+
}: {
59+
adapter: Adapter;
60+
modelOption: CloudTaskConfigOption;
61+
requestedModel: string;
62+
reasoning: SupportedReasoningEffort;
63+
}): { model: string; reasoning: SupportedReasoningEffort } {
64+
const model = resolveAvailableModel(modelOption, requestedModel);
65+
return {
66+
model,
67+
reasoning: isSupportedReasoningEffort(adapter, model, reasoning)
68+
? reasoning
69+
: DEFAULT_REASONING_EFFORT,
70+
};
71+
}
72+
73+
export type ComposerPrimaryAction =
74+
| "send"
75+
| "stop"
76+
| "mic"
77+
| "mic-stop"
78+
| "disabled";
79+
80+
export function resolveComposerPrimaryAction({
81+
hasContent,
82+
disabled,
83+
isRecording,
84+
isTranscribing,
85+
canStop,
86+
allowSendWhileRunning,
87+
}: {
88+
hasContent: boolean;
89+
disabled: boolean;
90+
isRecording: boolean;
91+
isTranscribing: boolean;
92+
canStop: boolean;
93+
allowSendWhileRunning: boolean;
94+
}): ComposerPrimaryAction {
95+
if (disabled || isTranscribing) return "disabled";
96+
if (canStop && (!allowSendWhileRunning || !hasContent)) return "stop";
97+
if (hasContent && !isRecording) return "send";
98+
return isRecording ? "mic-stop" : "mic";
99+
}

‎packages/ui/src/features/sessions/components/UnifiedModelSelector.tsx‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {
99
Robot,
1010
Spinner,
1111
} from "@phosphor-icons/react";
12+
import { getConfigOptionLabel } from "@posthog/core/task-detail/composerControls";
1213
import {
1314
Button,
1415
DropdownMenu,
@@ -78,8 +79,7 @@ export function UnifiedModelSelector({
7879
}, [selectOption]);
7980

8081
const currentValue = selectOption?.currentValue;
81-
const currentLabel =
82-
options.find((opt) => opt.value === currentValue)?.name ?? currentValue;
82+
const currentLabel = getConfigOptionLabel(options, currentValue);
8383

8484
const otherAdapter = getOtherAdapter(adapter);
8585

0 commit comments

Comments
 (0)