Skip to content

Commit 17e8961

Browse files
authored
Restore context-preserving handoff from automatic investigations (#1644)
## Summary Restore the path from retained automatic evidence to a human investigation without discarding the available findings or turning the original transcript into a mutable chat. Completed automatic investigations offer **Investigate further**. It opens a new human conversation with the original resource and issue ID, using Hub's existing recent-verdict seeding. The header's separate action is explicitly labeled **Start fresh — ignore earlier findings** and sends `fresh: true`. The new action requires a completed background run with an issue ID; running, failed, stopped, stale and unavailable runs do not advertise this handoff. Copy does not promise findings when no eligible recent verdict exists. ## OSS versus hosted behavior - **Radar Cloud SaaS and licensed self-hosted Hub:** automatic retained evidence supports the new context-preserving handoff. The original transcript stays read-only. - **Standalone OSS Radar:** no organization sharing or hosted-copy controls are introduced. Existing human investigation behavior remains; the fresh-start tooltip now states its intent explicitly. This is verdict seeding, not a full transcript or SDK-session fork, and it does not change sharing permissions or model-provider consent. ## Verification | Journey | Proof | |---|---| | Completed automatic evidence → Investigate further | Actual local browser click sends resource + issueId without fresh | | Explicit fresh start | Actual local browser click sends same target + issueId and fresh:true | | Ineligible state / OSS human run | Eligibility matrix covers absent issueId, human trigger, running/error/stopped/stale/gone states | | Request serialization | Both start intents tested at API boundary | | Standalone build | Type-check, 23 targeted tests, full frontend/embed/Go build pass | Browser verification used real local Hub/Postgres and synthetic retained evidence on the existing kind cluster. Job POSTs were intentionally intercepted; this is UI request proof, not a newly executed model investigation. Hub separately tests prompt seeding and authorization. Screenshots were captured and inspected locally. ## Dependencies Follows merged [Radar #1640](#1640). Uses the issueId and prior-evidence authorization from merged [Hub #218](skyhook-dev/radar-hub#218). The retained-evidence entrypoint is in merged [Web #280](skyhook-dev/radar-hub-web#280). Published radar-app 1.13.1 does not include this change. After merging, publish a new Radar package and adopt it in Web to complete the context-preserving handoff. No package publication or deployment is performed by this PR. <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Low Risk** > UI-only diagnose flow changes with guarded eligibility; no auth or sharing logic modified in this diff. > > **Overview** > Restores a **context-preserving** path from completed automatic investigations into a new human run, while keeping the original automatic transcript read-only. > > Eligible background runs (`done`, with `issueId`) now show an **Investigate further** action that starts a new investigation with the same resource and issue ID and **without** `fresh: true`, so Hub can seed from recent verdict evidence. The header **+** control is relabeled **Start fresh — ignore earlier findings** and continues to pass `fresh: true` for an explicit clean slate. > > `canInvestigateFurther` centralizes eligibility (no handoff for in-flight, failed, stopped, stale, missing issue, human runs, or when the run is gone). Copy falls back to generic “start a new investigation” when further investigation isn’t offered. > > Adds Vitest coverage for the eligibility matrix and for `createRun` POST bodies for both start intents. > > <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit 0815ae8. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot).</sup> <!-- /CURSOR_SUMMARY -->
1 parent 7f5d05c commit 17e8961

4 files changed

Lines changed: 116 additions & 5 deletions

File tree

‎web/src/api/diagnose.test.ts‎

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import { afterEach, describe, expect, it, vi } from "vitest";
2+
import { createRun } from "./diagnose";
3+
4+
afterEach(() => vi.unstubAllGlobals());
5+
6+
describe("investigation start requests", () => {
7+
const target = {
8+
kind: "Deployment",
9+
namespace: "prod",
10+
name: "payments",
11+
issueId: "issue-1",
12+
};
13+
14+
it.each([
15+
["investigate further", target],
16+
["start fresh", { ...target, fresh: true }],
17+
])("preserves the %s intent on the wire", async (_name, request) => {
18+
const fetch = vi
19+
.fn()
20+
.mockResolvedValue(new Response(JSON.stringify({ id: "new-run" })));
21+
vi.stubGlobal("fetch", fetch);
22+
await createRun(request, { agent: "hub" });
23+
const init = fetch.mock.calls[0][1] as RequestInit;
24+
expect(init.method).toBe("POST");
25+
expect(JSON.parse(init.body as string)).toEqual({
26+
...request,
27+
agent: "hub",
28+
});
29+
expect(JSON.parse(init.body as string).issueId).toBe("issue-1");
30+
});
31+
});

‎web/src/components/diagnose/DiagnoseSurface.test.tsx‎

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { describe, expect, it } from "vitest";
22
import { canCopyRunLink, canStartNewInvestigation } from "./DiagnoseSurface";
33
import {
44
canContinueInvestigation,
5+
canInvestigateFurther,
56
canStopInvestigation,
67
} from "./InvestigationView";
78
import type { RunSummary } from "../../api/diagnose";
@@ -181,3 +182,43 @@ describe("canCopyRunLink", () => {
181182
expect(canCopyRunLink({ ...run("done"), radarUrl: "" })).toBe(false);
182183
});
183184
});
185+
186+
describe("canInvestigateFurther", () => {
187+
it("offers a context-preserving next step on a completed automatic investigation", () => {
188+
expect(
189+
canInvestigateFurther({
190+
...run("done"),
191+
trigger: "background",
192+
issueId: "issue-1",
193+
}),
194+
).toBe(true);
195+
});
196+
197+
it("does not promise findings for missing, unfinished, stale or human investigations", () => {
198+
expect(canInvestigateFurther(run("done"))).toBe(false);
199+
expect(
200+
canInvestigateFurther({ ...run("done"), trigger: "background" }),
201+
).toBe(false);
202+
for (const status of [
203+
"running",
204+
"stopping",
205+
"error",
206+
"stopped",
207+
"stale",
208+
] as const) {
209+
expect(
210+
canInvestigateFurther({
211+
...run(status),
212+
trigger: "background",
213+
issueId: "issue-1",
214+
}),
215+
).toBe(false);
216+
}
217+
expect(
218+
canInvestigateFurther(
219+
{ ...run("done"), trigger: "background", issueId: "issue-1" },
220+
true,
221+
),
222+
).toBe(false);
223+
});
224+
});

‎web/src/components/diagnose/DiagnoseSurface.tsx‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -527,7 +527,7 @@ export function DiagnoseSurface({ topInset = 0 }: { topInset?: number }) {
527527
{headerRun &&
528528
canStartNewInvestigation(d.view, headerRun, d.needsConsent) && (
529529
<Tooltip
530-
content="New investigation on this resource"
530+
content="Start fresh — ignore earlier findings"
531531
position="bottom"
532532
>
533533
<button
@@ -541,7 +541,7 @@ export function DiagnoseSurface({ topInset = 0 }: { topInset?: number }) {
541541
})
542542
}
543543
className="rounded-md p-1 text-theme-text-tertiary hover:bg-theme-hover hover:text-theme-text-primary"
544-
aria-label="New investigation on this resource"
544+
aria-label="Start fresh — ignore earlier findings"
545545
>
546546
<Plus className="h-4 w-4" />
547547
</button>

‎web/src/components/diagnose/InvestigationView.tsx‎

Lines changed: 42 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,15 @@ export function canContinueInvestigation(
7373
);
7474
}
7575

76+
export function canInvestigateFurther(run: RunSummary, gone = false): boolean {
77+
return (
78+
!gone &&
79+
run.trigger === "background" &&
80+
run.status === "done" &&
81+
!!run.issueId
82+
);
83+
}
84+
7685
export function InvestigationView({
7786
run,
7887
agentLabel,
@@ -685,9 +694,39 @@ export function InvestigationView({
685694
</div>
686695
) : !canContinue ? (
687696
<div className="rounded-lg border border-theme-border bg-theme-base px-3 py-2 text-xs text-theme-text-secondary">
688-
{run.trigger === "background"
689-
? "Automatic investigation · read-only. Start a new investigation on this resource to continue digging."
690-
: "This investigation is read-only."}
697+
{run.trigger === "background" ? (
698+
<>
699+
<p>Automatic investigation · read-only.</p>
700+
{canInvestigateFurther(run, gone) ? (
701+
<div className="mt-2 flex flex-wrap items-center gap-2">
702+
<button
703+
className="btn-brand px-3 py-2 text-xs"
704+
onClick={() =>
705+
openInvestigation({
706+
kind,
707+
namespace,
708+
name,
709+
issueId: run.issueId,
710+
})
711+
}
712+
>
713+
Investigate further
714+
</button>
715+
<span>
716+
Re-checks current state, including recent automatic
717+
findings when available.
718+
</span>
719+
</div>
720+
) : (
721+
<p>
722+
Start a new investigation on this resource to continue
723+
digging.
724+
</p>
725+
)}
726+
</>
727+
) : (
728+
"This investigation is read-only."
729+
)}
691730
</div>
692731
) : (
693732
<div className="flex items-end gap-2">

0 commit comments

Comments
 (0)