diff --git a/src/routes/tangent/hooks/useRunAutomatedResearch.ts b/src/routes/tangent/hooks/useRunAutomatedResearch.ts index 8b64c94ef..fa84579ce 100644 --- a/src/routes/tangent/hooks/useRunAutomatedResearch.ts +++ b/src/routes/tangent/hooks/useRunAutomatedResearch.ts @@ -1,8 +1,12 @@ import { useMutation } from "@tanstack/react-query"; import useToastNotification from "@/hooks/useToastNotification"; -import type { ScenarioEntry } from "@/routes/tangent/idb/tangentDb"; import { + saveScenario, + type ScenarioEntry, +} from "@/routes/tangent/idb/tangentDb"; +import { + buildOpencodeSessionUrl, createOpencodeSession, resolveInstanceId, sendAutoresearchMessage, @@ -25,6 +29,13 @@ export function useRunAutomatedResearch() { ); const prompt = buildAutoresearchPrompt(scenario); await sendAutoresearchMessage(instanceId, sessionId, prompt); + + const url = buildOpencodeSessionUrl(instanceId, sessionId); + await saveScenario({ + ...scenario, + research: { instanceId, sessionId, url, startedAt: Date.now() }, + updatedAt: Date.now(), + }); }, onSuccess: () => { notify("Automated research started", "success"); diff --git a/src/routes/tangent/idb/tangentDb.ts b/src/routes/tangent/idb/tangentDb.ts index 7ea9959b1..4a33867f5 100644 --- a/src/routes/tangent/idb/tangentDb.ts +++ b/src/routes/tangent/idb/tangentDb.ts @@ -41,6 +41,17 @@ interface ScenarioPlan { failure_playbook?: unknown[]; } +/** + * Reference to the OpenCode agent session created when automated research is + * started for a scenario. Persisted so the UI can surface a follow link. + */ +interface ScenarioResearch { + instanceId: string; + sessionId: string; + url: string; + startedAt: number; +} + export interface ScenarioEntry { id: string; run: ScenarioRunRef; @@ -50,6 +61,7 @@ export interface ScenarioEntry { /** Only the ideas the user selected when building the scenario. */ ideas: ScenarioIdea[]; plan: ScenarioPlan; + research?: ScenarioResearch; createdAt: number; updatedAt: number; } diff --git a/src/routes/tangent/services/autoresearchOpencode.ts b/src/routes/tangent/services/autoresearchOpencode.ts index 7c07ba721..f521f0668 100644 --- a/src/routes/tangent/services/autoresearchOpencode.ts +++ b/src/routes/tangent/services/autoresearchOpencode.ts @@ -3,6 +3,7 @@ import { createInstanceApiTangentInstancesPost, listInstancesApiTangentInstancesGet, } from "@/api/sdk.gen"; +import { API_URL } from "@/utils/constants"; /** * Workspace directory the OpenCode agent runs in. OpenCode scopes sessions to @@ -96,6 +97,19 @@ export async function createOpencodeSession( return data.id; } +/** + * Build the OpenCode web UI URL for following a created session, mirroring the + * structure produced by the backend redirect. The base resolves to the + * configured backend (or the current origin in relative-path mode). + */ +export function buildOpencodeSessionUrl( + instanceId: string, + sessionId: string, +): string { + const base = API_URL || window.location.origin; + return `${base}/api/tangent/instances/${instanceId}/opencode/app/default/Lw/session/${sessionId}`; +} + /** * Send a prompt to an OpenCode session without waiting for the agent to finish * (fire-and-forget via `prompt_async`). diff --git a/src/routes/v2/shared/components/MlExperimentPlanner/MlExperimentPlannerContent.tsx b/src/routes/v2/shared/components/MlExperimentPlanner/MlExperimentPlannerContent.tsx index 2b0007cd1..5a1014580 100644 --- a/src/routes/v2/shared/components/MlExperimentPlanner/MlExperimentPlannerContent.tsx +++ b/src/routes/v2/shared/components/MlExperimentPlanner/MlExperimentPlannerContent.tsx @@ -12,6 +12,7 @@ import { import { Button } from "@/components/ui/button"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { BlockStack, InlineStack } from "@/components/ui/layout"; +import { Link } from "@/components/ui/link"; import { ScrollArea } from "@/components/ui/scroll-area"; import { Table, @@ -123,17 +124,28 @@ function ScenarioRow({ - + {scenario.research ? ( + event.stopPropagation()} + > + Open research session + + ) : ( + + )} ); @@ -180,13 +192,19 @@ function ScenarioDetail({ {scenario.plan.name} - + {scenario.research ? ( + + Open research session + + ) : ( + + )} {scenario.rationale} @@ -219,8 +237,11 @@ export function MlExperimentPlannerContent({ selectedScenarioId, }: MlExperimentPlannerContentProps) { const { scenarios } = useRunScenarios(runId); - const { mutate: runResearch, isPending: isResearchPending } = - useRunAutomatedResearch(); + const { + mutate: runResearch, + isPending: isResearchPending, + variables: researchVariables, + } = useRunAutomatedResearch(); const [selectedId, setSelectedId] = useState( selectedScenarioId ?? null, ); @@ -250,7 +271,9 @@ export function MlExperimentPlannerContent({ scenario={selectedScenario} onBack={() => setSelectedId(null)} onRunResearch={() => runResearch(selectedScenario)} - isResearchPending={isResearchPending} + isResearchPending={ + isResearchPending && researchVariables?.id === selectedScenario.id + } /> ); @@ -276,7 +299,9 @@ export function MlExperimentPlannerContent({ scenario={scenario} onSelect={() => setSelectedId(scenario.id)} onRunResearch={() => runResearch(scenario)} - isResearchPending={isResearchPending} + isResearchPending={ + isResearchPending && researchVariables?.id === scenario.id + } /> ))}