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
+ }
/>
))}