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

Commit 6cfb8b3

Browse files
authored
feat(autoresearch): improve new-task composer controls (GROW-124) (#3205)
1 parent 6629ffb commit 6cfb8b3

5 files changed

Lines changed: 237 additions & 95 deletions

File tree

packages/shared/src/analytics-events.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1001,6 +1001,28 @@ export interface ClaudeSessionImportFailedProperties {
10011001
failed_step?: string;
10021002
}
10031003

1004+
/** Fired when a user arms autoresearch mode on the new-task composer. */
1005+
export interface AutoresearchArmedProperties {
1006+
/** Hands-off mode auto-applied on arm so the unattended loop isn't blocked on permission prompts. */
1007+
default_mode: "bypassPermissions" | "acceptEdits";
1008+
workspace_mode?: "local" | "worktree" | "cloud";
1009+
}
1010+
1011+
/** Fired when an armed autoresearch task is submitted and its run kicks off. */
1012+
export interface AutoresearchRunStartedProperties {
1013+
direction: "maximize" | "minimize";
1014+
/** Whether the user set a target metric value to stop early at. */
1015+
has_target: boolean;
1016+
max_iterations: number;
1017+
/** Build and measure stages differ, so each iteration splits into a build turn and a measure turn. */
1018+
stages_split: boolean;
1019+
implement_model?: string;
1020+
measure_model?: string;
1021+
implement_effort?: string;
1022+
measure_effort?: string;
1023+
workspace_mode?: "local" | "worktree" | "cloud";
1024+
}
1025+
10041026
// Event names as constants
10051027
export const ANALYTICS_EVENTS = {
10061028
// App lifecycle
@@ -1159,6 +1181,10 @@ export const ANALYTICS_EVENTS = {
11591181
DASHBOARD_ACTION: "Dashboard action",
11601182
CANVAS_PROMPT_SENT: "Canvas prompt sent",
11611183
CONTEXT_ACTION: "Context action",
1184+
1185+
// Autoresearch events
1186+
AUTORESEARCH_ARMED: "Autoresearch armed",
1187+
AUTORESEARCH_RUN_STARTED: "Autoresearch run started",
11621188
} as const;
11631189

11641190
// Event property mapping
@@ -1312,6 +1338,10 @@ export type EventPropertyMap = {
13121338
[ANALYTICS_EVENTS.DASHBOARD_ACTION]: DashboardActionProperties;
13131339
[ANALYTICS_EVENTS.CANVAS_PROMPT_SENT]: CanvasPromptSentProperties;
13141340
[ANALYTICS_EVENTS.CONTEXT_ACTION]: ContextActionProperties;
1341+
1342+
// Autoresearch events
1343+
[ANALYTICS_EVENTS.AUTORESEARCH_ARMED]: AutoresearchArmedProperties;
1344+
[ANALYTICS_EVENTS.AUTORESEARCH_RUN_STARTED]: AutoresearchRunStartedProperties;
13151345
};
13161346

13171347
/**

packages/ui/src/features/autoresearch/AutoresearchComposerControls.tsx

Lines changed: 80 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,11 @@ interface AutoresearchComposerControlsProps {
2222
}
2323

2424
/**
25-
* Autoresearch settings rendered inside the composer box (its header addon)
26-
* while the mode is armed — one input view, not a widget attached under it.
25+
* Autoresearch settings shown as a bar above the composer while the mode is
26+
* armed. It reads as one sentence — "Optimize to maximize until it reaches N
27+
* or after K iterations using <model>" — so each control explains itself in
28+
* place; tooltips on the labels add the detail.
29+
*
2730
* There is deliberately no metric or instructions field: the prompt IS the
2831
* optimization brief, and the agent names the metric in its reports.
2932
*
@@ -39,44 +42,63 @@ export function AutoresearchComposerControls({
3942
onExit,
4043
}: AutoresearchComposerControlsProps) {
4144
return (
42-
<div className="flex w-full flex-wrap items-center gap-x-2 gap-y-1 text-[12px]">
43-
<span className="flex shrink-0 items-center gap-1 font-medium text-violet-11">
44-
<ChartLineUp size={13} />
45-
Autoresearch
45+
<div className="flex w-full flex-wrap items-center gap-x-2 gap-y-1.5 text-[12px]">
46+
<Tooltip content="Runs an autonomous optimization loop: it measures a baseline from your brief, then edits the code and re-measures each round — keeping changes that move the metric and reverting the ones that don't. It stops when the metric reaches your target or hits the iteration cap, whichever comes first.">
47+
<span className="flex shrink-0 cursor-help items-center gap-1 font-medium text-violet-11">
48+
<ChartLineUp size={13} />
49+
Autoresearch
50+
</span>
51+
</Tooltip>
52+
53+
{/* The goal: which way to push the metric the brief describes. */}
54+
<span className="flex items-center gap-1.5 whitespace-nowrap text-(--gray-11)">
55+
<Tooltip content="Whether the agent should drive the metric from your brief up or down.">
56+
<span className="cursor-help">Optimize to</span>
57+
</Tooltip>
58+
<Select.Root
59+
size="1"
60+
value={draft.direction}
61+
onValueChange={(value) =>
62+
onChange({ direction: value as AutoresearchDirection })
63+
}
64+
disabled={disabled}
65+
>
66+
<Select.Trigger variant="soft" aria-label="Optimization direction" />
67+
<Select.Content>
68+
<Select.Item value="maximize">maximize</Select.Item>
69+
<Select.Item value="minimize">minimize</Select.Item>
70+
</Select.Content>
71+
</Select.Root>
72+
</span>
73+
74+
{/* Two stop conditions, whichever comes first: the metric hits the
75+
target value, or the run exhausts its iteration budget. Only the
76+
label text is wrapped in a tooltip — never the input — so focusing
77+
the field to type doesn't pop the tooltip open. */}
78+
<span className="flex items-center gap-1.5 whitespace-nowrap text-(--gray-11)">
79+
<Tooltip content="Finish early once the metric reaches this value. Leave blank to always run the full iteration budget.">
80+
<span className="cursor-help">until it reaches</span>
81+
</Tooltip>
82+
<TextField.Root
83+
size="1"
84+
className="w-24"
85+
value={draft.targetValue === null ? "" : String(draft.targetValue)}
86+
onChange={(event) => {
87+
const raw = event.target.value.trim();
88+
const numeric = Number(raw);
89+
onChange({
90+
targetValue:
91+
raw === "" || !Number.isFinite(numeric) ? null : numeric,
92+
});
93+
}}
94+
placeholder="optional"
95+
inputMode="decimal"
96+
aria-label="Target metric value to stop at (optional)"
97+
disabled={disabled}
98+
/>
4699
</span>
47-
<Select.Root
48-
size="1"
49-
value={draft.direction}
50-
onValueChange={(value) =>
51-
onChange({ direction: value as AutoresearchDirection })
52-
}
53-
disabled={disabled}
54-
>
55-
<Select.Trigger variant="soft" aria-label="Optimization direction" />
56-
<Select.Content>
57-
<Select.Item value="maximize">Maximize</Select.Item>
58-
<Select.Item value="minimize">Minimize</Select.Item>
59-
</Select.Content>
60-
</Select.Root>
61-
<TextField.Root
62-
size="1"
63-
className="w-24"
64-
value={draft.targetValue === null ? "" : String(draft.targetValue)}
65-
onChange={(event) => {
66-
const raw = event.target.value.trim();
67-
const numeric = Number(raw);
68-
onChange({
69-
targetValue:
70-
raw === "" || !Number.isFinite(numeric) ? null : numeric,
71-
});
72-
}}
73-
placeholder="Target"
74-
inputMode="decimal"
75-
aria-label="Target value (optional)"
76-
disabled={disabled}
77-
/>
78-
<span className="flex items-center gap-1 text-(--gray-11)">
79-
100+
<span className="flex items-center gap-1.5 whitespace-nowrap text-(--gray-11)">
101+
or after
80102
<TextField.Root
81103
size="1"
82104
className="w-14"
@@ -89,18 +111,28 @@ export function AutoresearchComposerControls({
89111
})
90112
}
91113
inputMode="numeric"
92-
aria-label="Iteration budget"
114+
aria-label="Maximum iterations"
93115
disabled={disabled}
94116
/>
95-
iterations
117+
<Tooltip content="Hard cap on build-and-measure rounds before the run stops.">
118+
<span className="cursor-help">iterations</span>
119+
</Tooltip>
96120
</span>
97-
<StagesPopover
98-
draft={draft}
99-
modelOptions={modelOptions}
100-
effortOptions={effortOptions}
101-
disabled={disabled}
102-
onChange={onChange}
103-
/>
121+
122+
{/* The engine: model + effort per stage. */}
123+
<span className="flex items-center gap-1.5 whitespace-nowrap text-(--gray-11)">
124+
<Tooltip content="Model and reasoning effort the loop runs on. Set the build and measure stages to different models to measure with a cheaper one.">
125+
<span className="cursor-help">using</span>
126+
</Tooltip>
127+
<StagesPopover
128+
draft={draft}
129+
modelOptions={modelOptions}
130+
effortOptions={effortOptions}
131+
disabled={disabled}
132+
onChange={onChange}
133+
/>
134+
</span>
135+
104136
<span className="ml-auto flex shrink-0 items-center">
105137
<Tooltip content="Exit autoresearch mode">
106138
<button

packages/ui/src/features/message-editor/components/ModeSelector.tsx

Lines changed: 38 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
import type { SessionConfigOption } from "@agentclientprotocol/sdk";
2-
import { CaretDown } from "@phosphor-icons/react";
2+
import { CaretDown, ChartLineUp } from "@phosphor-icons/react";
33
import {
44
Button,
55
DropdownMenu,
6+
DropdownMenuCheckboxItem,
67
DropdownMenuContent,
78
DropdownMenuRadioGroup,
89
DropdownMenuRadioItem,
10+
DropdownMenuSeparator,
911
DropdownMenuTrigger,
1012
MenuLabel,
1113
} from "@posthog/quill";
@@ -19,16 +21,28 @@ interface ModeSelectorProps {
1921
onChange: (value: string) => void;
2022
allowBypassPermissions: boolean;
2123
disabled?: boolean;
24+
/**
25+
* When provided, an "Autoresearch" toggle renders as the last item of the
26+
* menu (new-task composer only). It arms/disarms the autonomous iteration
27+
* loop; `active` drives its checkmark. Applied after the menu closes, like a
28+
* mode change, so the composer doesn't relayout under the closing menu.
29+
*/
30+
autoresearch?: {
31+
active: boolean;
32+
onToggle: () => void;
33+
};
2234
}
2335

2436
export function ModeSelector({
2537
modeOption,
2638
onChange,
2739
allowBypassPermissions,
2840
disabled,
41+
autoresearch,
2942
}: ModeSelectorProps) {
3043
const [open, setOpen] = useState(false);
3144
const pendingValueRef = useRef<string | null>(null);
45+
const pendingAutoresearchRef = useRef(false);
3246
const displayOption = useRetainedConfigOption(modeOption);
3347

3448
if (!displayOption || displayOption.type !== "select") return null;
@@ -58,10 +72,15 @@ export function ModeSelector({
5872
open={open}
5973
onOpenChange={setOpen}
6074
onOpenChangeComplete={(isOpen) => {
61-
if (!isOpen && pendingValueRef.current !== null) {
75+
if (isOpen) return;
76+
if (pendingValueRef.current !== null) {
6277
onChange(pendingValueRef.current);
6378
pendingValueRef.current = null;
6479
}
80+
if (pendingAutoresearchRef.current) {
81+
pendingAutoresearchRef.current = false;
82+
autoresearch?.onToggle();
83+
}
6584
}}
6685
>
6786
<DropdownMenuTrigger
@@ -107,6 +126,23 @@ export function ModeSelector({
107126
);
108127
})}
109128
</DropdownMenuRadioGroup>
129+
{autoresearch && (
130+
<>
131+
<DropdownMenuSeparator />
132+
<DropdownMenuCheckboxItem
133+
checked={autoresearch.active}
134+
onCheckedChange={() => {
135+
pendingAutoresearchRef.current = true;
136+
setOpen(false);
137+
}}
138+
>
139+
<span className="text-violet-11">
140+
<ChartLineUp size={12} />
141+
</span>
142+
<span className="whitespace-nowrap">Autoresearch</span>
143+
</DropdownMenuCheckboxItem>
144+
</>
145+
)}
110146
</DropdownMenuContent>
111147
</DropdownMenu>
112148
);

packages/ui/src/features/message-editor/components/PromptInput.tsx

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,14 @@ export interface PromptInputProps {
4040
modeOption?: SessionConfigOption;
4141
onModeChange?: (value: string) => void;
4242
allowBypassPermissions?: boolean;
43+
/**
44+
* When provided, the mode dropdown gains an "Autoresearch" toggle as its
45+
* last item (new-task composer only). `active` drives its checkmark.
46+
*/
47+
autoresearch?: {
48+
active: boolean;
49+
onToggle: () => void;
50+
};
4351
// capabilities
4452
enableBashMode?: boolean;
4553
enableCommands?: boolean;
@@ -94,6 +102,7 @@ export const PromptInput = forwardRef<EditorHandle, PromptInputProps>(
94102
modeOption,
95103
onModeChange,
96104
allowBypassPermissions = false,
105+
autoresearch,
97106
enableBashMode = false,
98107
enableCommands = true,
99108
modelSelector,
@@ -395,6 +404,7 @@ export const PromptInput = forwardRef<EditorHandle, PromptInputProps>(
395404
onChange={onModeChange}
396405
allowBypassPermissions={allowBypassPermissions}
397406
disabled={disabled}
407+
autoresearch={autoresearch}
398408
/>
399409
)}
400410
{modelSelector && <span>{modelSelector}</span>}

0 commit comments

Comments
 (0)