diff --git a/packages/shared/src/analytics-events.ts b/packages/shared/src/analytics-events.ts index 62850de083..7c0abc192c 100644 --- a/packages/shared/src/analytics-events.ts +++ b/packages/shared/src/analytics-events.ts @@ -993,6 +993,28 @@ export interface ClaudeSessionImportFailedProperties { failed_step?: string; } +/** Fired when a user arms autoresearch mode on the new-task composer. */ +export interface AutoresearchArmedProperties { + /** Hands-off mode auto-applied on arm so the unattended loop isn't blocked on permission prompts. */ + default_mode: "bypassPermissions" | "acceptEdits"; + workspace_mode?: "local" | "worktree" | "cloud"; +} + +/** Fired when an armed autoresearch task is submitted and its run kicks off. */ +export interface AutoresearchRunStartedProperties { + direction: "maximize" | "minimize"; + /** Whether the user set a target metric value to stop early at. */ + has_target: boolean; + max_iterations: number; + /** Build and measure stages differ, so each iteration splits into a build turn and a measure turn. */ + stages_split: boolean; + implement_model?: string; + measure_model?: string; + implement_effort?: string; + measure_effort?: string; + workspace_mode?: "local" | "worktree" | "cloud"; +} + // Event names as constants export const ANALYTICS_EVENTS = { // App lifecycle @@ -1150,6 +1172,10 @@ export const ANALYTICS_EVENTS = { DASHBOARD_ACTION: "Dashboard action", CANVAS_PROMPT_SENT: "Canvas prompt sent", CONTEXT_ACTION: "Context action", + + // Autoresearch events + AUTORESEARCH_ARMED: "Autoresearch armed", + AUTORESEARCH_RUN_STARTED: "Autoresearch run started", } as const; // Event property mapping @@ -1302,6 +1328,10 @@ export type EventPropertyMap = { [ANALYTICS_EVENTS.DASHBOARD_ACTION]: DashboardActionProperties; [ANALYTICS_EVENTS.CANVAS_PROMPT_SENT]: CanvasPromptSentProperties; [ANALYTICS_EVENTS.CONTEXT_ACTION]: ContextActionProperties; + + // Autoresearch events + [ANALYTICS_EVENTS.AUTORESEARCH_ARMED]: AutoresearchArmedProperties; + [ANALYTICS_EVENTS.AUTORESEARCH_RUN_STARTED]: AutoresearchRunStartedProperties; }; /** diff --git a/packages/ui/src/features/autoresearch/AutoresearchComposerControls.tsx b/packages/ui/src/features/autoresearch/AutoresearchComposerControls.tsx index 81b9635ae7..a563797cc0 100644 --- a/packages/ui/src/features/autoresearch/AutoresearchComposerControls.tsx +++ b/packages/ui/src/features/autoresearch/AutoresearchComposerControls.tsx @@ -22,8 +22,11 @@ interface AutoresearchComposerControlsProps { } /** - * Autoresearch settings rendered inside the composer box (its header addon) - * while the mode is armed — one input view, not a widget attached under it. + * Autoresearch settings shown as a bar above the composer while the mode is + * armed. It reads as one sentence — "Optimize to maximize until it reaches N + * or after K iterations using " — so each control explains itself in + * place; tooltips on the labels add the detail. + * * There is deliberately no metric or instructions field: the prompt IS the * optimization brief, and the agent names the metric in its reports. * @@ -39,44 +42,63 @@ export function AutoresearchComposerControls({ onExit, }: AutoresearchComposerControlsProps) { return ( -
- - - Autoresearch +
+ + + + Autoresearch + + + + {/* The goal: which way to push the metric the brief describes. */} + + + Optimize to + + + onChange({ direction: value as AutoresearchDirection }) + } + disabled={disabled} + > + + + maximize + minimize + + + + + {/* Two stop conditions, whichever comes first: the metric hits the + target value, or the run exhausts its iteration budget. Only the + label text is wrapped in a tooltip — never the input — so focusing + the field to type doesn't pop the tooltip open. */} + + + until it reaches + + { + const raw = event.target.value.trim(); + const numeric = Number(raw); + onChange({ + targetValue: + raw === "" || !Number.isFinite(numeric) ? null : numeric, + }); + }} + placeholder="optional" + inputMode="decimal" + aria-label="Target metric value to stop at (optional)" + disabled={disabled} + /> - - onChange({ direction: value as AutoresearchDirection }) - } - disabled={disabled} - > - - - Maximize - Minimize - - - { - const raw = event.target.value.trim(); - const numeric = Number(raw); - onChange({ - targetValue: - raw === "" || !Number.isFinite(numeric) ? null : numeric, - }); - }} - placeholder="Target" - inputMode="decimal" - aria-label="Target value (optional)" - disabled={disabled} - /> - - ≤ + + or after - iterations + + iterations + - + + {/* The engine: model + effort per stage. */} + + + using + + + + - - )} {cloudRegion === "dev" && ( + {autoresearchDraft && ( +
+ + useAutoresearchDraftStore + .getState() + .updateDraft(sessionId, patch) + } + onExit={() => + useAutoresearchDraftStore + .getState() + .clearDraft(sessionId) + } + /> +
+ )} - useAutoresearchDraftStore - .getState() - .updateDraft(sessionId, patch) - } - onExit={() => - useAutoresearchDraftStore - .getState() - .clearDraft(sessionId) - } - /> - ) : undefined - } editorHeight="large" disabled={isCreatingTask} isLoading={isCreatingTask} @@ -1161,6 +1187,14 @@ export function TaskInput({ modeOption={modeOption} onModeChange={handleModeChange} allowBypassPermissions={allowBypassPermissions} + autoresearch={ + autoresearchService && autoresearchEnabled + ? { + active: !!autoresearchDraft, + onToggle: handleAutoresearchToggle, + } + : undefined + } enableCommands enableBashMode={false} modelSelector={