From 467e614f337196b14a2771726c115e1d7fc409bc Mon Sep 17 00:00:00 2001 From: Fernando Gomes Date: Mon, 6 Jul 2026 15:47:57 -0300 Subject: [PATCH 1/2] feat(autoresearch): improve new-task composer controls (GROW-124) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Move the Autoresearch toggle into the agent-mode dropdown as its last item; arming it defaults the mode to bypassPermissions (or acceptEdits) so the unattended loop isn't blocked on permission prompts. Move the target/iterations controls out of the composer InputGroup — whose click handler refocused the editor on every click, stealing focus from the inputs — into a bar between the dropdown row and the input. Rewrite that bar to read as a sentence ("Optimize to maximize until it reaches N or after K iterations using ") with per-label tooltips, replacing the unlabeled Maximize / Target / <= controls. Feature-flag gating (autoresearchEnabled) is preserved on both the dropdown toggle and the controls bar. Generated-By: PostHog Code Task-Id: 14b7baf0-7253-47ec-96a5-110520071729 --- .../AutoresearchComposerControls.tsx | 128 +++++++++++------- .../components/ModeSelector.tsx | 40 +++++- .../message-editor/components/PromptInput.tsx | 10 ++ .../task-detail/components/TaskInput.tsx | 90 ++++++------ 4 files changed, 175 insertions(+), 93 deletions(-) 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 +1157,14 @@ export function TaskInput({ modeOption={modeOption} onModeChange={handleModeChange} allowBypassPermissions={allowBypassPermissions} + autoresearch={ + autoresearchService && autoresearchEnabled + ? { + active: !!autoresearchDraft, + onToggle: handleAutoresearchToggle, + } + : undefined + } enableCommands enableBashMode={false} modelSelector={ From 4319801e04a722c9bb08f58ccaf5aa299054aa77 Mon Sep 17 00:00:00 2001 From: Fernando Gomes Date: Mon, 6 Jul 2026 16:10:20 -0300 Subject: [PATCH 2/2] feat(autoresearch): capture arm + run-started analytics events (GROW-124) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add two product-analytics events for autoresearch mode usage: - "Autoresearch armed" — fired when the mode is turned on from the agent-mode dropdown (default_mode, workspace_mode). - "Autoresearch run started" — fired when an armed task is submitted (direction, has_target, max_iterations, stages_split, per-stage model/effort, workspace_mode). Both fire only through the feature-flag-gated arm/submit paths, so no events emit when autoresearch is disabled. Generated-By: PostHog Code Task-Id: 14b7baf0-7253-47ec-96a5-110520071729 --- packages/shared/src/analytics-events.ts | 30 ++++++++++++++++ .../task-detail/components/TaskInput.tsx | 34 +++++++++++++++++-- 2 files changed, 62 insertions(+), 2 deletions(-) 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/task-detail/components/TaskInput.tsx b/packages/ui/src/features/task-detail/components/TaskInput.tsx index 76bbffb537..1b4cacc500 100644 --- a/packages/ui/src/features/task-detail/components/TaskInput.tsx +++ b/packages/ui/src/features/task-detail/components/TaskInput.tsx @@ -9,12 +9,14 @@ import { isValidConfigValue } from "@posthog/core/task-detail/configOptions"; import { useServiceOptional } from "@posthog/di/react"; import { useHostTRPC, useHostTRPCClient } from "@posthog/host-router/react"; import { ButtonGroup } from "@posthog/quill"; +import { ANALYTICS_EVENTS } from "@posthog/shared"; import type { Task } from "@posthog/shared/domain-types"; import { openSettings } from "@posthog/ui/features/settings/hooks/useOpenSettings"; import type { TaskInputReportAssociation } from "@posthog/ui/features/task-detail/stores/taskInputPrefillStore"; import { useTaskInputPrefillStore } from "@posthog/ui/features/task-detail/stores/taskInputPrefillStore"; import { navigateToInbox } from "@posthog/ui/router/navigationBridge"; import { useAppView } from "@posthog/ui/router/useAppView"; +import { track } from "@posthog/ui/shell/analytics"; import { Box, Flex, Text, Tooltip } from "@radix-ui/themes"; import { useQuery } from "@tanstack/react-query"; import { AnimatePresence, motion } from "framer-motion"; @@ -695,6 +697,10 @@ export function TaskInput({ if (modeOption && isValidConfigValue(modeOption, autonomousMode)) { setConfigOption(modeOption.id, autonomousMode); } + track(ANALYTICS_EVENTS.AUTORESEARCH_ARMED, { + default_mode: autonomousMode, + workspace_mode: workspaceMode, + }); }, [ sessionId, currentModel, @@ -702,6 +708,7 @@ export function TaskInput({ allowBypassPermissions, modeOption, setConfigOption, + workspaceMode, ]); // The preview config can still be loading when the user arms the mode; @@ -800,16 +807,32 @@ export function TaskInput({ // Stages ride through as configured; identical stages mean a single-turn // loop, any difference makes the run split. Unresolved fields fall back // to the composer's values so the recorded config is concrete. - autoresearchPendingRun.set({ + const resolvedRun = { ...draft, implementModel: draft.implementModel ?? currentModel ?? null, measureModel: draft.measureModel ?? currentModel ?? null, implementEffort: draft.implementEffort ?? currentReasoningLevel ?? null, measureEffort: draft.measureEffort ?? currentReasoningLevel ?? null, + }; + autoresearchPendingRun.set({ + ...resolvedRun, instructions: contentToXml(content).trim(), }); const submitted = await handleSubmit(override); if (submitted) { + track(ANALYTICS_EVENTS.AUTORESEARCH_RUN_STARTED, { + direction: resolvedRun.direction, + has_target: resolvedRun.targetValue !== null, + max_iterations: resolvedRun.maxIterations, + stages_split: + resolvedRun.implementModel !== resolvedRun.measureModel || + resolvedRun.implementEffort !== resolvedRun.measureEffort, + implement_model: resolvedRun.implementModel ?? undefined, + measure_model: resolvedRun.measureModel ?? undefined, + implement_effort: resolvedRun.implementEffort ?? undefined, + measure_effort: resolvedRun.measureEffort ?? undefined, + workspace_mode: effectiveWorkspaceMode, + }); useAutoresearchDraftStore.getState().clearDraft(sessionId); useDraftStore.getState().actions.setDraft(sessionId, null); try { @@ -821,7 +844,14 @@ export function TaskInput({ autoresearchPendingRun.clear(); } return submitted; - }, [canSubmit, currentModel, currentReasoningLevel, handleSubmit, sessionId]); + }, [ + canSubmit, + currentModel, + currentReasoningLevel, + effectiveWorkspaceMode, + handleSubmit, + sessionId, + ]); const submitTask = autoresearchDraft ? handleAutoresearchSubmit