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
8 changes: 8 additions & 0 deletions apps/code/src/main/services/agent/schemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ export const startSessionInput = z.object({
additionalDirectories: z.array(z.string()).optional(),
customInstructions: z.string().max(2000).optional(),
effort: effortLevelSchema.optional(),
model: z.string().optional(),
});

export type StartSessionInput = z.infer<typeof startSessionInput>;
Expand Down Expand Up @@ -292,3 +293,10 @@ export const getGatewayModelsInput = z.object({
});

export const getGatewayModelsOutput = z.array(modelOptionSchema);

export const getPreviewConfigOptionsInput = z.object({
apiHost: z.string(),
adapter: z.enum(["claude", "codex"]),
});

export const getPreviewConfigOptionsOutput = z.array(sessionConfigOptionSchema);
107 changes: 106 additions & 1 deletion apps/code/src/main/services/agent/service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,16 @@ import {
} from "@agentclientprotocol/sdk";
import { isMcpToolReadOnly } from "@posthog/agent";
import { hydrateSessionJsonl } from "@posthog/agent/adapters/claude/session/jsonl-hydration";
import { getEffortOptions } from "@posthog/agent/adapters/claude/session/models";
import { Agent } from "@posthog/agent/agent";
import { getAvailableModes } from "@posthog/agent/execution-mode";
import {
DEFAULT_GATEWAY_MODEL,
fetchGatewayModels,
formatGatewayModelName,
getProviderName,
isAnthropicModel,
isOpenAIModel,
} from "@posthog/agent/gateway-models";
import { getLlmGatewayUrl } from "@posthog/agent/posthog-api";
import type { OnLogCallback } from "@posthog/agent/types";
Expand Down Expand Up @@ -189,6 +194,8 @@ interface SessionConfig {
customInstructions?: string;
/** Effort level for Claude sessions */
effort?: EffortLevel;
/** Model to use for the session (e.g. "claude-sonnet-4-6") */
model?: string;
}

interface ManagedSession {
Expand Down Expand Up @@ -465,9 +472,10 @@ export class AgentService extends TypedEventEmitter<AgentServiceEvents> {
permissionMode,
customInstructions,
effort,
model,
} = config;

// Preview sessions don't need a real repo — use a temp directory
// Preview config doesn't need a real repo — use a temp directory
const repoPath = taskId === "__preview__" ? tmpdir() : rawRepoPath;

if (!isRetry) {
Expand Down Expand Up @@ -638,6 +646,7 @@ export class AgentService extends TypedEventEmitter<AgentServiceEvents> {
sessionId: existingSessionId,
systemPrompt,
...(permissionMode && { permissionMode }),
...(model != null && { model }),
claudeCode: {
options: {
...(additionalDirectories?.length && {
Expand Down Expand Up @@ -669,6 +678,7 @@ export class AgentService extends TypedEventEmitter<AgentServiceEvents> {
taskRunId,
systemPrompt,
...(permissionMode && { permissionMode }),
...(model != null && { model }),
claudeCode: {
options: {
...(additionalDirectories?.length && { additionalDirectories }),
Expand Down Expand Up @@ -1362,6 +1372,7 @@ For git operations while detached:
customInstructions:
"customInstructions" in params ? params.customInstructions : undefined,
effort: "effort" in params ? params.effort : undefined,
model: "model" in params ? params.model : undefined,
};
}

Expand Down Expand Up @@ -1513,4 +1524,98 @@ For git operations while detached:
return getModelTier(a.modelId) - getModelTier(b.modelId);
});
}

async getPreviewConfigOptions(
apiHost: string,
adapter: "claude" | "codex" = "claude",
): Promise<SessionConfigOption[]> {
const gatewayUrl = getLlmGatewayUrl(apiHost);
const gatewayModels = await fetchGatewayModels({ gatewayUrl });

const modelFilter = adapter === "codex" ? isOpenAIModel : isAnthropicModel;

const modelOptions = gatewayModels
.filter((model) => modelFilter(model))
.map((model) => ({
value: model.id,
name: formatGatewayModelName(model),
description: `Context: ${model.context_window.toLocaleString()} tokens`,
}));

const defaultModel =
adapter === "codex"
? (modelOptions[0]?.value ?? "")
: DEFAULT_GATEWAY_MODEL;

const resolvedModelId = modelOptions.some((o) => o.value === defaultModel)
? defaultModel
: (modelOptions[0]?.value ?? defaultModel);

if (!modelOptions.some((o) => o.value === resolvedModelId)) {
modelOptions.unshift({
value: resolvedModelId,
name: resolvedModelId,
description: "Custom model",
});
}

const modeOptions = getAvailableModes().map((mode) => ({
value: mode.id,
name: mode.name,
description: mode.description ?? undefined,
}));

const configOptions: SessionConfigOption[] = [
{
id: "mode",
name: "Approval Preset",
type: "select",
currentValue: "plan",
options: modeOptions,
category: "mode",
description:
"Choose an approval and sandboxing preset for your session",
},
{
id: "model",
name: "Model",
type: "select",
currentValue: resolvedModelId,
options: modelOptions,
category: "model",
description: "Choose which model Claude should use",
},
];

if (adapter === "codex") {
configOptions.push({
id: "reasoning_effort",
name: "Reasoning Level",
type: "select",
currentValue: "high",
options: [
{ value: "low", name: "Low" },
{ value: "medium", name: "Medium" },
{ value: "high", name: "High" },
],
category: "thought_level",
description: "Controls how much reasoning effort the model uses",
});
} else {
const effortOpts = getEffortOptions(resolvedModelId);
if (effortOpts) {
configOptions.push({
id: "effort",
name: "Effort",
type: "select",
currentValue: "high",
options: effortOpts,
category: "thought_level",
description: "Controls how much effort Claude puts into its response",
});
}
}

return configOptions;
}
}
9 changes: 9 additions & 0 deletions apps/code/src/main/trpc/routers/agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import {
cancelSessionInput,
getGatewayModelsInput,
getGatewayModelsOutput,
getPreviewConfigOptionsInput,
getPreviewConfigOptionsOutput,
listSessionsInput,
listSessionsOutput,
notifySessionContextInput,
Expand Down Expand Up @@ -193,4 +195,11 @@ export const agentRouter = router({
.input(getGatewayModelsInput)
.output(getGatewayModelsOutput)
.query(({ input }) => getService().getGatewayModels(input.apiHost)),

getPreviewConfigOptions: publicProcedure
.input(getPreviewConfigOptionsInput)
.output(getPreviewConfigOptionsOutput)
.query(({ input }) =>
getService().getPreviewConfigOptions(input.apiHost, input.adapter),
),
});
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,14 @@ import type { MessageEditorHandle } from "@features/message-editor/components/Me
import { ModeIndicatorInput } from "@features/message-editor/components/ModeIndicatorInput";
import { useDraftStore } from "@features/message-editor/stores/draftStore";
import { useOnboardingStore } from "@features/onboarding/stores/onboardingStore";
import { getSessionService } from "@features/sessions/service/service";
import {
cycleModeOption,
getCurrentModeFromConfigOptions,
} from "@features/sessions/stores/sessionStore";
import { useSettingsStore } from "@features/settings/stores/settingsStore";
import { TaskInputEditor } from "@features/task-detail/components/TaskInputEditor";
import { WorkspaceModeSelect } from "@features/task-detail/components/WorkspaceModeSelect";
import { usePreviewSession } from "@features/task-detail/hooks/usePreviewSession";
import { usePreviewConfig } from "@features/task-detail/hooks/usePreviewConfig";
import { useTaskCreation } from "@features/task-detail/hooks/useTaskCreation";
import {
useGithubBranches,
Expand Down Expand Up @@ -104,9 +103,14 @@ export function TutorialStep({ onComplete, onBack }: TutorialStepProps) {
const cloudBranches = cloudBranchData?.branches;
const cloudDefaultBranch = cloudBranchData?.defaultBranch ?? null;

// Preview session for config options — always claude
const { modeOption, thoughtOption, previewTaskId, isConnecting } =
usePreviewSession("claude");
// Preview config options — always claude
const {
modeOption,
modelOption,
thoughtOption,
isLoading: isPreviewLoading,
setConfigOption,
} = usePreviewConfig("claude");

const currentExecutionMode =
getCurrentModeFromConfigOptions(modeOption ? [modeOption] : undefined) ??
Expand Down Expand Up @@ -193,13 +197,9 @@ export function TutorialStep({ onComplete, onBack }: TutorialStepProps) {
const handleCycleMode = useCallback(() => {
const nextValue = cycleModeOption(modeOption, allowBypassPermissions);
if (nextValue && modeOption) {
getSessionService().setSessionConfigOption(
previewTaskId,
modeOption.id,
nextValue,
);
setConfigOption(modeOption.id, nextValue);
}
}, [modeOption, allowBypassPermissions, previewTaskId]);
}, [modeOption, allowBypassPermissions, setConfigOption]);

useHotkeys(
"shift+tab",
Expand Down Expand Up @@ -388,9 +388,16 @@ export function TutorialStep({ onComplete, onBack }: TutorialStepProps) {
directoryTooltip="Select a repository first"
onEmptyChange={setEditorIsEmpty}
adapter="claude"
previewTaskId={previewTaskId}
modelOption={modelOption}
thoughtOption={thoughtOption}
onConfigOptionChange={(configId, value) => {
setConfigOption(configId, value);
if (configId === modelOption?.id) {
handleModelChange(value);
}
}}
onAdapterChange={() => {}}
isPreviewConnecting={isConnecting}
isLoading={isPreviewLoading}
autoFocus={false}
tourHighlight={
isHighlighted("model-selector")
Expand All @@ -399,7 +406,6 @@ export function TutorialStep({ onComplete, onBack }: TutorialStepProps) {
? "submit-button"
: null
}
onModelChange={handleModelChange}
/>
</div>
</TourHighlight>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,25 +1,21 @@
import { Select, Text } from "@radix-ui/themes";
import { getSessionService } from "../service/service";
import {
flattenSelectOptions,
useAdapterForTask,
useSessionForTask,
useThoughtLevelConfigOptionForTask,
} from "../stores/sessionStore";
import type { SessionConfigOption } from "@agentclientprotocol/sdk";
import { Brain } from "@phosphor-icons/react";
import { Flex, Select, Text } from "@radix-ui/themes";
import { flattenSelectOptions } from "../stores/sessionStore";

interface ReasoningLevelSelectorProps {
taskId?: string;
thoughtOption?: SessionConfigOption;
adapter?: "claude" | "codex";
onChange?: (value: string) => void;
disabled?: boolean;
}

export function ReasoningLevelSelector({
taskId,
thoughtOption,
adapter,
onChange,
disabled,
}: ReasoningLevelSelectorProps) {
const session = useSessionForTask(taskId);
const thoughtOption = useThoughtLevelConfigOptionForTask(taskId);
const adapter = useAdapterForTask(taskId);

if (!thoughtOption || thoughtOption.type !== "select") {
return null;
}
Expand All @@ -30,37 +26,35 @@ export function ReasoningLevelSelector({
const activeLabel =
options.find((opt) => opt.value === activeLevel)?.name ?? activeLevel;

const handleChange = (value: string) => {
if (taskId && session?.status === "connected") {
getSessionService().setSessionConfigOption(
taskId,
thoughtOption.id,
value,
);
}
};

return (
<Select.Root
value={activeLevel}
onValueChange={handleChange}
onValueChange={(value) => onChange?.(value)}
disabled={disabled}
size="1"
>
<Select.Trigger
variant="ghost"
style={{
fontSize: "12px",
fontSize: "var(--font-size-1)",
color: "var(--gray-11)",
padding: "4px 8px",
marginLeft: "4px",
height: "auto",
minHeight: "unset",
gap: "6px",
}}
>
<Text style={{ fontSize: "12px" }}>
{adapter === "codex" ? "Reasoning" : "Effort"}: {activeLabel}
</Text>
<Flex align="center" gap="1">
<Brain
size={14}
weight="regular"
style={{ color: "var(--gray-9)", flexShrink: 0 }}
/>
<Text size="1">
{adapter === "codex" ? "Reasoning" : "Effort"}: {activeLabel}
</Text>
</Flex>
</Select.Trigger>
<Select.Content position="popper" sideOffset={4}>
{options.map((level) => (
Expand Down
Loading
Loading