Skip to content

Add stable investigation links and hosted sharing UI - #1585

Merged
nadaverell merged 10 commits into
mainfrom
feature/radar-investigation-sharing
Sep 5, 2026
Merged

nadaverell merged 10 commits into
mainfrom
feature/radar-investigation-sharing

Conversation

@nadaverell

@nadaverell nadaverell commented Sep 2, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Radar investigations become stable browser state instead of transient panel state. Existing retained runs reopen through ?ai-run=<id>, survive navigation and reloads, and expose a local radar_url in Diagnose/CLI results. When Radar is embedded behind an authorized host, the same UI renders the host's private, organization-shared, and automatic-investigation capabilities.

OSS remains local-first: it gains bookmarkable same-instance URLs, but no copy-link or sharing UI. Cloud collaboration remains entirely server-authorized.

What changed

Stable retained-run navigation

  • add direct retained-run lookup and URL synchronization for ai-run
  • preserve the selected run across Radar routes, unrelated query parameters, hashes, Back/Forward, and embedded-router navigation
  • render a controlled recovery state when a linked run has expired or been cleared, without repeatedly polling the same missing exact ID
  • return radar_url from local Diagnose and background/CLI results

Hosted capability-driven UI

  • render copy-link, visibility, ownership, actor attribution, organization history, and automatic-run states only when the host supplies those capabilities
  • gate copy-link on a non-empty canonical radarUrl, so it is absent in ordinary OSS
  • gate sharing controls on canManageVisibility and the follow-up composer on canContinue
  • represent the server's stopping state, keep polling while a turn drains, and prevent new starts/follow-ups until it becomes terminal
  • let terminal transcript events outrank a lagging running summary, without making automatic or genuinely sessionless runs resumable
  • keep Stop for a live human turn even before a resumable session exists; automatic runs remain immutable
  • disable the composer while the verdict reveal is still finishing so an apparently accepted message cannot be discarded
  • remove selected-run actions from the docked History header so it cannot act on a stale transcript
  • send the configured host authentication headers on Diagnose API requests

Behavior by environment

Behavior OSS Radar Radar Cloud
Retained-run deep link Same-instance bookmark using ?ai-run=<id> Authorized canonical org/cluster/run link
Copy investigation link Not shown Shown only when Hub supplies radarUrl
Privacy/organization labels Not shown Driven by Hub capabilities
Share/unshare Not available Creator-only when authorized by Hub
Organization history Not available Personal and organization sections follow Hub results
Automatic-run state Not synthesized Organization artifact, explicitly read-only
Access boundary Existing Radar process/server boundary Enforced by Hub on every read, stream, and mutation

An OSS URL is not a portable sharing artifact: it works only while the same reachable Radar instance retains that run. This PR adds no OSS identity, permission, cross-instance lookup, or transcript export model.

Live-tested journeys

Environment Journey Result
OSS Open retained history, select a run, reload its ?ai-run= URL Exact transcript restored
OSS Navigate to Resources and back with a selected run Run/query state retained
OSS Navigate from an expanded investigation using the sidebar Investigation closes and the selected destination is immediately visible
OSS Open a run captured against another cluster Existing read-only stale-cluster warning shown
OSS Open an unknown run for 10.5s Controlled unavailable state; no ongoing exact-ID polling
OSS Inspect local retained run at desktop width No copy button, privacy badge, share control, or organization grouping
Cloud Open/copy/share an owner-private run Canonical org/cluster/run URL copied; state changes to Organization
Cloud Open the shared run as another org member Author and transcript shown; follow-up enabled; no visibility control
Cloud Open automatic and sessionless terminal runs Automatic run read-only with fresh-run escape; sessionless run read-only
Cloud Open a live human run with no session ID Stop remains available and successfully ends the turn
Cloud Stop while the owning turn drains UI shows Stopping, keeps polling, and does not offer another start/follow-up
Cloud Receive terminal transcript before summary refresh Stop disappears immediately; no false read-only flash during verdict reveal
Cloud Open docked History while another run is selected Header has no stale copy/share/menu actions
Cloud Open an unknown hosted run for 10.5s One exact GET, then controlled recovery while history polling continues

Verification

  • frontend suite: 70 files / 583 tests
  • npm run tsc
  • npm run build
  • repository Go tests/build from the companion QA pass
  • OSS and combined Cloud Playwright journeys above at 1280px width
  • git diff --check
  • independent cross-model convergence review: READY

Dependencies and rollout

This PR is self-contained for OSS bookmark behavior and the hosted component contract. Cloud authorization and durable transcript semantics live in radar-hub#218; Fleet handoff and cross-organization bootstrap live in radar-hub-web#280.

After approval, Cloud rollout requires publishing a new @skyhook-io/radar-app version and bumping Hub Web to it. This PR does not publish, tag, deploy, or expand access by itself.


Note

Medium Risk
Changes URL/history synchronization, investigation lifecycle gates, and authenticated Diagnose API usage across navigation and cluster switches; sharing UI is capability-gated but org-wide visibility is a sensitive mutation on hosted backends.

Overview
Investigations are now bookmarkable browser state via durable ?ai-run=<id> links instead of one-shot deep links. The local server adds GET /diagnose/runs/{id} for runs outside the bounded history list; the diagnose CLI JSON output includes an escaped radar_url.

DiagnoseProvider keeps the focused run in the URL (with popstate sync and Fleet/embed guards), resolves missing list entries through getRun, and avoids hammering 404s for cleared runs. App navigation and cluster switches preserve ai-run; maximized investigations close before primary-rail navigation so the destination is visible.

When the host exposes capabilities, the UI adds copy-link (radarUrl), private/organization visibility, grouped organization/automatic history, actor labels on turns, and stricter Stop / follow-up / new investigation rules for stopping, background runs, and lagging summaries. Diagnose API calls send auth headers; OSS still has no share/copy UI unless radarUrl is present.

Reviewed by Cursor Bugbot for commit 22d6119. Bugbot is set up for automated code reviews on this repo. Configure here.

@nadaverell
nadaverell requested a review from hisco as a code owner September 2, 2026 00:24
@qodo-code-review

Copy link
Copy Markdown

PR Summary by Qodo

Add stable investigation links and capability-driven sharing

✨ Enhancement 🐞 Bug fix 🕐 40+ Minutes

Grey Divider

AI Description

• Keeps investigation IDs as durable URL state and resolves retained runs directly.
• Adds capability-driven sharing, link copying, grouped history, and actor attribution.
• Marks automatic investigations read-only and exposes stable Diagnose URLs in CLI JSON.
Diagram

graph TD
  CLI["Diagnose CLI"] --> URL["ai-run URL"] --> Context["Diagnose Context"] --> API["Run API"] --> Store[("Run Store")]
  Context --> Surface["Investigation UI"] --> Clipboard["Copied Link"]
  Surface --> API
Loading
High-Level Assessment

The direct-by-ID endpoint combined with durable query state is the best fit for existing local, embedded, and Fleet routes. Relying on bounded history would not produce durable links, while dedicated permalink routes would complicate cluster-scoped route ownership; server-provided capability fields also avoid unsafe client-side permission inference.

Files changed (11) +452 / -131

Enhancement (10) +435 / -125
diagnosecli.goInclude stable Radar URLs in Diagnose JSON +3/-1

Include stable Radar URLs in Diagnose JSON

• Builds an escaped '?ai-run=<id>' URL from the local Radar base address and returns it as 'radar_url' in JSON results.

internal/diagnosecli/diagnosecli.go

ai_diagnose.goResolve retained investigations by stable ID +15/-0

Resolve retained investigations by stable ID

• Adds a handler that returns a retained run summary by ID. Missing runs return a clear 404 response, avoiding dependence on the bounded history list.

internal/server/ai_diagnose.go

server.goRegister the direct investigation lookup route +1/-0

Register the direct investigation lookup route

• Exposes 'GET /diagnose/runs/{id}' alongside the existing run list and mutation endpoints.

internal/server/server.go

diagnose.tsAdd sharing metadata and run APIs +35/-0

Add sharing metadata and run APIs

• Extends run and stream types with visibility, ownership, continuation, trigger, URL, and actor metadata. Adds direct run lookup and visibility update requests.

web/src/api/diagnose.ts

AISettings.tsxClarify hosted history deletion scope +9/-4

Clarify hosted history deletion scope

• Explains that hosted history includes private, shared, and automatic investigations. The confirmation state warns that clearing history also removes terminal shared and automatic runs.

web/src/components/diagnose/AISettings.tsx

DiagnoseContext.tsxSynchronize focused investigations with durable URL state +125/-33

Synchronize focused investigations with durable URL state

• Keeps 'ai-run' in browser history, supports back and forward navigation, and fetches exact runs by ID. Preserves directly loaded runs outside the bounded history page while disabling pseudo-links on unsupported Fleet routes.

web/src/components/diagnose/DiagnoseContext.tsx

DiagnoseSurface.tsxAdd link copying and visibility controls +139/-18

Add link copying and visibility controls

• Adds stable investigation link copying and capability-gated private or organization visibility controls. Organization sharing requires confirmation and updates the cached run summary after success.

web/src/components/diagnose/DiagnoseSurface.tsx

Home.tsxGroup and label investigation history +87/-63

Group and label investigation history

• Separates personal investigations from shared or automatic organization runs. Each history entry now displays whether it is private, shared, or automatic.

web/src/components/diagnose/Home.tsx

InvestigationView.tsxEnforce continuation capabilities and automatic read-only state +11/-4

Enforce continuation capabilities and automatic read-only state

• Uses server-provided continuation capability to gate follow-up questions. Automatic investigations that cannot continue display an explicit read-only notice while shared human investigations remain interactive.

web/src/components/diagnose/InvestigationView.tsx

parts.tsxDisplay actors on shared transcript turns +10/-2

Display actors on shared transcript turns

• Carries optional actor metadata on transcript turns and renders the human author above attributed questions.

web/src/components/diagnose/parts.tsx

Bug fix (1) +17 / -6
App.tsxPreserve investigation focus across app navigation +17/-6

Preserve investigation focus across app navigation

• Retains 'ai-run' when switching views or cluster context while still clearing resource-specific parameters. Reads the live browser location so History API changes are not lost through stale router snapshots.

web/src/App.tsx

Comment thread web/src/components/diagnose/DiagnoseContext.tsx
Comment thread web/src/components/diagnose/DiagnoseContext.tsx Outdated
Comment thread web/src/components/diagnose/Home.tsx
Comment thread web/src/components/diagnose/InvestigationView.tsx Outdated
@qodo-code-review

qodo-code-review Bot commented Sep 2, 2026 •

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (0) 📘 Rule violations (0) 📜 Skill insights (0)

Grey Divider


Action required

1. OSS runs mislabeled private ✓ Resolved 🐞 Bug ≡ Correctness
Description
RecentList renders every non-background run without visibility === "organization" as “Private,”
but the local backend does not emit visibility or provide a privacy boundary. Local OSS users are
therefore shown an access-control guarantee that does not exist.
Code

web/src/components/diagnose/Home.tsx[R143-147]

+                  {r.trigger === "background"
+                    ? "Automatic"
+                    : r.visibility === "organization"
+                      ? "Shared"
+                      : "Private"}
Relevance

●●● Strong

The PR explicitly preserves ownerless OSS behavior, so missing visibility must not be labeled as
private.

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The local Go RunSummary contains none of the optional hosted sharing fields. The new ternary
nevertheless falls through to “Private” whenever trigger and organization visibility are absent,
which is the shape of every local run.

internal/ai/runs.go[114-133]
web/src/api/diagnose.ts[107-115]
web/src/components/diagnose/Home.tsx[142-147]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The new status badge treats missing visibility metadata as private, causing ownerless local OSS runs to be presented as access-controlled private investigations.

## Issue Context
Local RunSummary has no visibility, ownership, trigger, or sharing capability fields. Render Private/Shared/Automatic labels only when the server supplied the corresponding hosted metadata; otherwise use a neutral label or omit the badge.

## Fix Focus Areas
- web/src/components/diagnose/Home.tsx[107-116]
- web/src/components/diagnose/Home.tsx[142-147]
- internal/ai/runs.go[114-133]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. Memory routing mutates browser URL ✓ Resolved 🐞 Bug ≡ Correctness
Description
writeRunIDToLocation directly updates window.history without checking RadarApp's router
strategy, so opening or closing an investigation in router="memory" mode changes the host browser
URL. The global popstate synchronization can consequently control panel state even though
application navigation is intentionally isolated in a MemoryRouter.
Code

web/src/components/diagnose/DiagnoseContext.tsx[R207-210]

+    window.history[push ? "pushState" : "replaceState"](
+      window.history.state,
+      "",
+      `${url.pathname}${url.search}${url.hash}`,
Relevance

●● Moderate

Potentially conflicts with MemoryRouter, but no close historical precedent confirms the intended
router integration.

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The new helper writes the native browser history, while RadarApp explicitly documents and implements
MemoryRouter as a mode where the URL bar must not change. DiagnoseProvider receives no router-mode
signal, and its new popstate listener also reads browser history independently of MemoryRouter.

web/src/components/diagnose/DiagnoseContext.tsx[201-211]
web/src/components/diagnose/DiagnoseContext.tsx[519-550]
web/src/RadarApp.tsx[64-79]
web/src/RadarApp.tsx[220-250]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Diagnose directly writes browser history even when RadarApp uses MemoryRouter, violating the documented router contract and leaking panel navigation into the host URL.

## Issue Context
Pass the active router strategy into DiagnoseProvider or provide it through context. Suppress both native History API writes and browser popstate synchronization when routing is memory-backed.

## Fix Focus Areas
- web/src/components/diagnose/DiagnoseContext.tsx[192-224]
- web/src/components/diagnose/DiagnoseContext.tsx[519-550]
- web/src/RadarApp.tsx[220-250]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended

3. Run requests omit auth headers ✓ Resolved 📘 Rule violation ⚙ Maintainability
Description
The new getRun and updateRunVisibility requests do not include headers from getAuthHeaders().
Hosted consumers using the configured authorization-header provider may therefore be unable to load
stable links or change sharing visibility.
Code

web/src/api/diagnose.ts[R209-212]

+  const res = await fetch(`${RUNS()}/${encodeURIComponent(id)}`, {
+    credentials: getCredentialsMode(),
+    signal,
+  });
Relevance

●●● Strong

Authenticated frontend requests should consistently use shared authorization headers; this is an
explicit repository compliance rule.

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
PR Compliance ID 3036564 requires authenticated frontend HTTP calls to obtain headers through
getAuthHeaders(). The new GET only sets credentials and signal, while the new PATCH supplies only
Content-Type; config.ts documents that hosted library consumers use the shared provider to
inject authorization tokens.

Rule 3036564: Use shared API config helpers for all new frontend HTTP/WebSocket calls
web/src/api/diagnose.ts[209-224]
web/src/api/config.ts[102-117]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The new investigation GET and PATCH requests omit headers supplied by `getAuthHeaders()`, preventing configured host authentication from reaching the backend.

## Issue Context
Preserve the existing `Content-Type` header on the PATCH request while spreading in the shared authentication headers. Import `getAuthHeaders` from the API configuration module.

## Fix Focus Areas
- web/src/api/diagnose.ts[4-4]
- web/src/api/diagnose.ts[209-212]
- web/src/api/diagnose.ts[221-225]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


4. Status pill bypasses Badge ✓ Resolved 📘 Rule violation ⚙ Maintainability
Description
The new Automatic/Shared/Private status pill is hand-built as a styled span instead of using the
shared Badge component with a semantic appearance prop. This bypasses centralized badge styling
and behavior.
Code

web/src/components/diagnose/Home.tsx[R142-144]

+                <span className="shrink-0 rounded bg-theme-elevated px-1.5 py-0.5 text-[10px] font-medium text-theme-text-tertiary">
+                  {r.trigger === "background"
+                    ? "Automatic"
Relevance

●●● Strong

This directly violates the explicit repository rule requiring shared Badge components for status
pills.

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
PR Compliance ID 3036677 requires status labels and pill indicators to use the shared Badge
component with semantic props. The cited code introduces a rounded, colored status label using a
custom span and utility classes.

Rule 3036677: Use Badge components instead of hard-coded badge color strings
web/src/components/diagnose/Home.tsx[142-148]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The investigation visibility/trigger status is rendered as a custom pill rather than through the shared `Badge` component.

## Issue Context
Import the shared badge and render Automatic, Shared, or Private through `Badge` using an appropriate semantic `severity` or `kind` prop instead of recreating badge styling on a `span`.

## Fix Focus Areas
- web/src/components/diagnose/Home.tsx[4-6]
- web/src/components/diagnose/Home.tsx[142-148]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


5. Copy failures report success ✓ Resolved 🐞 Bug ☼ Reliability
Description
CopyRunLink ignores both an unavailable clipboard API and rejection from writeText, then
immediately displays “Link copied.” Users can therefore paste an old or empty clipboard value while
the UI claims the investigation link was copied.
Code

web/src/components/diagnose/DiagnoseSurface.tsx[R166-169]

+  const copy = () => {
+    void navigator.clipboard?.writeText(stableRunURL(run));
+    setCopied(true);
+    setTimeout(() => setCopied(false), 1100);
Relevance

●●● Strong

The UI reports success without handling missing or rejected clipboard operations, a clear
reliability bug.

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The optional clipboard call may perform no operation, and its returned promise is explicitly
discarded. The next statement unconditionally sets copied to true, which drives the “Link copied”
tooltip and check icon.

web/src/components/diagnose/DiagnoseSurface.tsx[164-184]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The copy-link control enters its success state regardless of whether the clipboard write exists or succeeds.

## Issue Context
Await `navigator.clipboard.writeText`, set the copied state only after fulfillment, and expose a failure state when clipboard access is unavailable or rejected. A supported fallback copy mechanism may be used where appropriate.

## Fix Focus Areas
- web/src/components/diagnose/DiagnoseSurface.tsx[164-188]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Context sources
✅ Compliance rules (platform): 41 rules
Review mode: ⚖️ Balanced

Grey Divider

Tip of the day
💡 Did you know, you can keep summaries lean with Finding overflow, which tucks the rest behind 'View more'

More tips ↗ | Customize Qodo ↗ | Qodo docs ↗

Grey Divider

Qodo Logo

Comment thread web/src/api/diagnose.ts
Comment thread web/src/components/diagnose/Home.tsx Outdated
Comment thread web/src/components/diagnose/DiagnoseContext.tsx
Comment thread web/src/components/diagnose/Home.tsx Outdated
Comment thread web/src/components/diagnose/DiagnoseSurface.tsx Outdated
Comment thread web/src/components/diagnose/DiagnoseContext.tsx
@nadaverell nadaverell changed the title Add stable links and sharing UI for investigations Add stable investigation links and hosted sharing UI Sep 2, 2026
Comment thread web/src/components/diagnose/InvestigationView.tsx Outdated

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 01f18ab. Configure here.

Comment thread web/src/components/diagnose/InvestigationView.tsx
@nadaverell
nadaverell merged commit ec3d37f into main Sep 5, 2026
9 checks passed
@nadaverell
nadaverell deleted the feature/radar-investigation-sharing branch September 5, 2026 21:02
nadaverell added a commit that referenced this pull request Sep 5, 2026
## Summary

Keep investigation links useful without implying that private bookmarks
grant access or that every unavailable transcript was deleted.

- Private hosted links say “Copy private link (only you can open it)”.
Organization-shared links retain the existing label.
- Both unavailable-investigation surfaces explain possible privacy,
changed access or cleared history, with account/org and creator
guidance.
- No permission changes. Standalone OSS still has no hosted copy/share
controls; those require Hub capabilities.

## Testing

- Radar `make tsc`, `make build`, and 19 targeted DiagnoseSurface tests.
- Hub Web consumer production build with this locally packed Radar
source.
- Live browser checks: private-link tooltip and member opening a revoked
share. Screenshots captured locally.

Follow-up to merged
[#1585](#1585); used with [Hub
#218](skyhook-dev/radar-hub#218) and [Web
#280](skyhook-dev/radar-hub-web#280).
Release/package adoption remains a separate ordered step; no publishing
is performed by this PR.

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Low Risk**
> Copy and accessibility text only; no API, permissions, or link
behavior changes.
> 
> **Overview**
> Updates Diagnose UI copy so users aren’t misled about who can open
shared links or why an investigation can’t be loaded.
> 
> **Copy link control** now takes run `visibility`. Private runs use
tooltip and `aria-label` **“Copy private link (only you can open it)”**;
organization-shared runs keep **“Copy investigation link.”**
> 
> **Unavailable investigation** empty states (deep-linked run missing
from the list in `DiagnoseSurface`, and stream-gone with no transcript
in `InvestigationView`) replace the old “cleared from history” wording
with broader guidance: private run, revoked/changed access, or cleared
history, plus account/org checks or asking the creator. Inline comments
are aligned with that framing.
> 
> <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
e720aeb. Bugbot is set up for automated
code reviews on this repo. Configure
[here](https://www.cursor.com/dashboard/bugbot).</sup>
<!-- /CURSOR_SUMMARY -->
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant