@skyhook-io/radar-app 1.8.8 → 1.8.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@skyhook-io/radar-app",
3
- "version": "1.8.8",
3
+ "version": "1.8.9",
4
4
  "description": "Radar's full web UI as a reusable React component. Used by Radar's own binary and by external consumers like Radar Cloud.",
5
5
  "repository": {
6
6
  "type": "git",
package/src/App.tsx CHANGED
@@ -11,7 +11,7 @@ import { useDiagnoseLayout } from './components/diagnose/DiagnoseContext'
11
11
  import { DiagnoseSurface } from './components/diagnose/DiagnoseSurface'
12
12
  import { TopologyGraph, TopologySearch, TopologyBreadcrumb, TopologyFilterSidebar, TopologyControls, FreshnessControl, gitOpsRouteForKind, gitOpsRouteForResource, ScopePill, PaneLoader } from '@skyhook-io/k8s-ui'
13
13
  import { initNavigationMap } from '@skyhook-io/k8s-ui/utils/navigation'
14
- import { useAPIResources, CORE_RESOURCES } from './api/apiResources'
14
+ import { useAPIResources, findAPIResourceForRoute } from './api/apiResources'
15
15
  import { TimelineView } from './components/timeline/TimelineView'
16
16
  import { ResourcesView } from './components/resources/ResourcesView'
17
17
  import { serializeColumnFilters } from './components/resources/resource-utils'
@@ -61,7 +61,7 @@ import { useTheme } from './context/ThemeContext'
61
61
  import { Tooltip } from './components/ui/Tooltip'
62
62
  import { LargeClusterNamespacePicker } from './components/shared/LargeClusterNamespacePicker'
63
63
  import { SettingsDialog } from './components/settings/SettingsDialog'
64
- import type { TopologyNode, GroupingMode, MainView, SelectedResource, SelectedHelmRelease, NodeKind, TopologyMode, Topology, K8sEvent } from './types'
64
+ import type { APIResource, TopologyNode, GroupingMode, MainView, SelectedResource, SelectedHelmRelease, NodeKind, TopologyMode, Topology, K8sEvent } from './types'
65
65
  import { kindToPlural, pluralToKind, openExternal, apiVersionToGroup, relatedResourcePath, searchHitToSelectedResource } from './utils/navigation'
66
66
  import { type OmnibarHandle } from './components/ui/Omnibar'
67
67
  import { RadarOmnibar } from './components/ui/RadarOmnibar'
@@ -150,7 +150,8 @@ function getViewFromPath(pathname: string): ExtendedMainView {
150
150
  function namespaceFilterDisabled(
151
151
  view: ExtendedMainView,
152
152
  pathname: string,
153
- apiResources?: { name: string; kind: string; namespaced: boolean }[],
153
+ search = '',
154
+ apiResources?: APIResource[],
154
155
  ): { disabled: boolean; tooltip?: string } {
155
156
  if (
156
157
  view === 'cost' &&
@@ -170,7 +171,8 @@ function namespaceFilterDisabled(
170
171
  }
171
172
  if (view === 'resources') {
172
173
  const kindSlug = segments[1]
173
- const match = kindSlug ? apiResources?.find(r => r.name === kindSlug) : undefined
174
+ const group = new URLSearchParams(search).get('apiGroup') || ''
175
+ const match = kindSlug ? findAPIResourceForRoute(apiResources, kindSlug, group) : undefined
174
176
  if (match && !match.namespaced) {
175
177
  return {
176
178
  disabled: true,
@@ -185,7 +187,7 @@ function namespaceFilterDisabled(
185
187
  // correct regardless of which component renders it. A detail drawer that opens
186
188
  // over a list (?resource=…) is deliberately NOT titled — it's the same page, so
187
189
  // it keeps the list's title.
188
- function radarPageTitle(pathname: string, search = '', apiResources?: { name: string; kind: string; group?: string }[]): string | null {
190
+ function radarPageTitle(pathname: string, search = '', apiResources?: APIResource[]): string | null {
189
191
  const decode = (s: string) => {
190
192
  try {
191
193
  return decodeURIComponent(s)
@@ -196,7 +198,7 @@ function radarPageTitle(pathname: string, search = '', apiResources?: { name: st
196
198
  const capitalize = (text: string) =>
197
199
  text ? text.charAt(0).toUpperCase() + text.slice(1) : text
198
200
  const pluralKindTitle = (kind: string, resourceName: string) =>
199
- kind.toLowerCase() === resourceName.toLowerCase() ? kind : englishPlural(kind)
201
+ kind.toLowerCase() === resourceName.toLowerCase() || /Metrics$/.test(kind) ? kind : englishPlural(kind)
200
202
  const pathSegments = pathname.replace(/^\//, '').split('/').filter(Boolean)
201
203
  const view = getViewFromPath(pathname)
202
204
 
@@ -207,7 +209,8 @@ function radarPageTitle(pathname: string, search = '', apiResources?: { name: st
207
209
  if (view === 'resources') {
208
210
  const resourceName = decode(pathSegments[1] ?? '')
209
211
  if (!resourceName) return 'Resources'
210
- const match = apiResources?.find((r) => r.name === resourceName)
212
+ const group = new URLSearchParams(search).get('apiGroup') || ''
213
+ const match = findAPIResourceForRoute(apiResources, resourceName, group)
211
214
  return pluralKindTitle(match?.kind ?? pluralToKind(resourceName), resourceName)
212
215
  }
213
216
  // GitOps detail is /gitops/detail/<kind>/<ns>/<name> → the resource name;
@@ -387,10 +390,7 @@ function AppInner({ manageDocumentTitle = false, documentTitleSuffix, onClusterL
387
390
 
388
391
  // View-aware namespace scope: disabled on cluster-scoped surfaces so the
389
392
  // chip isn't a dead control next to the cluster switcher.
390
- // Fall back to the static core-resource list before discovery loads, so the
391
- // chip disables immediately on a cluster-scoped core kind (Nodes, PVs, …)
392
- // instead of flickering enabled until /api-resources resolves.
393
- const namespaceFilter = namespaceFilterDisabled(mainView, location.pathname, navApiResources ?? CORE_RESOURCES)
393
+ const namespaceFilter = namespaceFilterDisabled(mainView, location.pathname, location.search, navApiResources)
394
394
 
395
395
  // One URL-derived tab title for every view (see radarPageTitle). Driving it
396
396
  // from the URL — not the mounted component. Off unless the host opts in
package/src/RadarApp.tsx CHANGED
@@ -40,7 +40,10 @@ import type { ClusterLoadState } from "./types/clusterLoadState";
40
40
  import { TimelineSourceProvider } from "./context/TimelineSource";
41
41
  import type { TimelineSourceConfig } from "./api/timelineSource";
42
42
  import { DiagnoseCustomizationProvider } from "./context/DiagnoseCustomization";
43
- import type { RenderDiagnoseAction } from "./context/DiagnoseCustomization";
43
+ import type {
44
+ RenderDiagnoseAction,
45
+ DiagnoseConsentCopy,
46
+ } from "./context/DiagnoseCustomization";
44
47
  import { defaultDiagnoseAction } from "./components/diagnose/LocalDiagnoseAction";
45
48
  import { DiagnoseProvider } from "./components/diagnose/DiagnoseContext";
46
49
 
@@ -110,6 +113,15 @@ export interface RadarAppProps {
110
113
  * agent-free. See ./context/DiagnoseCustomization for the render-prop shape.
111
114
  */
112
115
  renderDiagnoseAction?: RenderDiagnoseAction;
116
+ /**
117
+ * Replaces the first-run consent card's trust copy. REQUIRED of any host whose
118
+ * backend runs the agent somewhere other than the user's own machine — the
119
+ * default copy states the agent runs locally, under the user's own model
120
+ * account, with transcripts kept on their disk, and none of that is true of a
121
+ * hosted runner. Radar keeps the card's chrome and the Approve/Cancel flow; a
122
+ * host only supplies the claims. See ./context/DiagnoseCustomization.
123
+ */
124
+ diagnoseConsent?: DiagnoseConsentCopy;
113
125
  /**
114
126
  * Initial route for `router: 'memory'` (ignored for 'browser'). Lets a host
115
127
  * deep-link a specific view (e.g. '/topology') without owning the URL bar —
@@ -184,6 +196,7 @@ export function RadarApp({
184
196
  manageDocumentTitle = false,
185
197
  documentTitleSuffix,
186
198
  renderDiagnoseAction,
199
+ diagnoseConsent,
187
200
  initialPath,
188
201
  onClusterLoadStateChange,
189
202
  timelineSource,
@@ -213,6 +226,7 @@ export function RadarApp({
213
226
  <TimelineSourceProvider config={timelineSource}>
214
227
  <DiagnoseCustomizationProvider
215
228
  value={renderDiagnoseAction ?? defaultDiagnoseAction}
229
+ consentCopy={diagnoseConsent}
216
230
  >
217
231
  <DiagnoseProvider>
218
232
  <App
@@ -3,7 +3,7 @@ import type { APIResource } from '../types'
3
3
  import { apiUrl, getAuthHeaders, getCredentialsMode } from './config'
4
4
 
5
5
  // Re-export pure functions from package
6
- export { categorizeResources, CORE_RESOURCES, formatGroupName, shortenGroupName, getKindLabel, getKindPlural } from '@skyhook-io/k8s-ui'
6
+ export { categorizeResources, CORE_RESOURCES, findAPIResourceForRoute, formatGroupName, shortenGroupName, getKindLabel, getKindPlural } from '@skyhook-io/k8s-ui'
7
7
  export type { ResourceCategory } from '@skyhook-io/k8s-ui'
8
8
 
9
9
  async function fetchJSON<T>(path: string): Promise<T> {
package/src/api/client.ts CHANGED
@@ -255,15 +255,6 @@ export interface DashboardResourceCounts {
255
255
  restricted?: string[] // Resource kinds the user cannot list due to RBAC
256
256
  }
257
257
 
258
- export interface DashboardEvent {
259
- type: string
260
- reason: string
261
- message: string
262
- involvedObject: string
263
- namespace: string
264
- timestamp: string
265
- }
266
-
267
258
  export interface DashboardChange {
268
259
  kind: string
269
260
  namespace: string
@@ -387,7 +378,6 @@ export interface DashboardResponse {
387
378
  health: DashboardHealth
388
379
  problems: DashboardProblem[]
389
380
  resourceCounts: DashboardResourceCounts
390
- recentEvents: DashboardEvent[]
391
381
  recentChanges: DashboardChange[]
392
382
  topologySummary: DashboardTopologySummary
393
383
  trafficSummary: DashboardTrafficSummary | null
@@ -361,6 +361,7 @@ export function DiagnoseProvider({ children }: { children: ReactNode }) {
361
361
  startRunRef.current(t);
362
362
  }, []);
363
363
  const openRun = useCallback((id: string) => {
364
+ setStartError(null);
364
365
  setActiveRunId(id);
365
366
  setView("investigation");
366
367
  setOpen(true);
@@ -23,6 +23,7 @@ import {
23
23
  agentLabelFor,
24
24
  openDiagnoseSettings,
25
25
  } from "./DiagnoseContext";
26
+ import { useDiagnoseConsentCopy } from "../../context/DiagnoseCustomization";
26
27
  import { InvestigationView } from "./InvestigationView";
27
28
  import { RecentList } from "./Home";
28
29
  import { ConsentCard } from "./parts";
@@ -136,6 +137,7 @@ function InvestigationMenu({ run }: { run: RunSummary }) {
136
137
  // Helm drawers, so it no longer floats viewport-fixed or DOM-measures the chrome.
137
138
  export function DiagnoseSurface({ topInset = 0 }: { topInset?: number }) {
138
139
  const d = useDiagnose();
140
+ const consentCopy = useDiagnoseConsentCopy();
139
141
  const {
140
142
  maximized,
141
143
  setMaximized,
@@ -203,6 +205,7 @@ export function DiagnoseSurface({ topInset = 0 }: { topInset?: number }) {
203
205
  agentName={d.agentLabel}
204
206
  agent={d.selectedAgent}
205
207
  isolated={d.isolated}
208
+ copy={consentCopy}
206
209
  onOpenSettings={openDiagnoseSettings}
207
210
  onApprove={d.approveConsent}
208
211
  onCancel={d.cancelConsent}
@@ -44,7 +44,11 @@ export function InvestigationView({
44
44
  maximized: boolean;
45
45
  }) {
46
46
  const { kind, namespace, name } = run;
47
- const { refreshRuns, openInvestigation } = useDiagnose();
47
+ const { refreshRuns, openInvestigation, startError } = useDiagnose();
48
+ const retryDiagnosis = useCallback(
49
+ () => openInvestigation({ kind, namespace, name }),
50
+ [openInvestigation, kind, namespace, name],
51
+ );
48
52
  const queryClient = useQueryClient();
49
53
  const [turns, setTurns] = useState<Turn[]>([]);
50
54
  // The run is gone server-side (evicted past the retention cap, or lost on a
@@ -505,14 +509,23 @@ export function InvestigationView({
505
509
  onApply={canApply ? requestApply : undefined}
506
510
  onAsk={isLast && !busy && !stale ? askFollowup : undefined}
507
511
  onCheckStatus={canCheck ? checkStatus : undefined}
512
+ onRetryDiagnosis={
513
+ isLast &&
514
+ t.status === "error" &&
515
+ !t.question &&
516
+ !t.apply &&
517
+ !stale
518
+ ? retryDiagnosis
519
+ : undefined
520
+ }
508
521
  hideVerdict={pinned && i === pinnedIdx}
509
522
  />
510
523
  );
511
524
  })}
512
- {actionError && (
525
+ {(actionError || startError) && (
513
526
  <div className="flex items-start gap-2 rounded-lg border border-red-500/30 bg-red-500/10 p-3 text-sm text-theme-text-primary">
514
527
  <AlertTriangle className="mt-0.5 h-4 w-4 shrink-0 text-red-400" />
515
- <span>{actionError}</span>
528
+ <span>{actionError || startError}</span>
516
529
  </div>
517
530
  )}
518
531
  </div>
@@ -22,6 +22,7 @@ import { stringify as toYaml } from "yaml";
22
22
  import { codeToHtml } from "shiki";
23
23
  import { DialogPortal } from "@skyhook-io/k8s-ui/components/ui/DialogPortal";
24
24
  import { useTheme } from "../../context/ThemeContext";
25
+ import { type DiagnoseConsentCopy } from "../../context/DiagnoseCustomization";
25
26
  import {
26
27
  type Diagnosis,
27
28
  type DiagnoseStep,
@@ -402,6 +403,7 @@ export function TurnView({
402
403
  onApply,
403
404
  onAsk,
404
405
  onCheckStatus,
406
+ onRetryDiagnosis,
405
407
  hideVerdict = false,
406
408
  }: {
407
409
  turn: Turn;
@@ -410,6 +412,7 @@ export function TurnView({
410
412
  onApply?: (fix: string) => void;
411
413
  onAsk?: (question: string) => void;
412
414
  onCheckStatus?: () => void;
415
+ onRetryDiagnosis?: () => void;
413
416
  // In the maximized workspace the pinned turn's verdict renders in the side rail,
414
417
  // so the transcript suppresses its own copy (reasoning + tool calls still show).
415
418
  hideVerdict?: boolean;
@@ -469,7 +472,18 @@ export function TurnView({
469
472
  {turn.status === "error" && turn.error && (
470
473
  <div className="flex items-start gap-2 rounded-lg border border-red-500/30 bg-red-500/10 p-3 text-sm text-theme-text-primary">
471
474
  <AlertTriangle className="mt-0.5 h-4 w-4 shrink-0 text-red-400" />
472
- <span>{turn.error}</span>
475
+ <div className="flex min-w-0 flex-col gap-2">
476
+ <span className="whitespace-pre-wrap break-words">{turn.error}</span>
477
+ {onRetryDiagnosis && (
478
+ <button
479
+ type="button"
480
+ onClick={onRetryDiagnosis}
481
+ className="btn-brand self-start px-3 py-1 text-xs"
482
+ >
483
+ Retry diagnosis
484
+ </button>
485
+ )}
486
+ </div>
473
487
  </div>
474
488
  )}
475
489
  </div>
@@ -568,95 +582,51 @@ export function RunContextCard({ run }: { run: RunSummary }) {
568
582
  );
569
583
  }
570
584
 
571
- // The first-run consent + trust card. Its copy is the OSS BYO-local trust story
572
- // ("your own agent, on your machine, nothing to Radar").
573
- // TODO(cloud): this copy must become embedder-overridable for Radar Cloud, where
574
- // the agent runs in the cloud (the company's self-hosted instance + their key /
575
- // local LLM, OR our SaaS) — a different, honestly-different trust story ("runs in
576
- // your Radar Cloud, audited, managed key"). Plumb an override through the
577
- // DiagnoseCustomization seam (same place Hub overrides the entry button) before
578
- // the k8s-ui lift, so OSS and Cloud don't ship the same claim over different data
579
- // flows. This is also a natural "upgrade to Cloud" surface.
580
- export function ConsentCard({
581
- agentName,
582
- agent,
583
- isolated = true,
585
+ // ConsentCardShell owns the card chrome (icon, layout, settings link, Approve/
586
+ // Cancel) so every copy tier — the OSS default, the hosted fallback, and a host
587
+ // override feeds the same frame instead of duplicating it.
588
+ function ConsentCardShell({
589
+ title,
590
+ body,
591
+ bullets,
592
+ settingsLabel,
593
+ approveLabel = "Approve & investigate",
584
594
  onOpenSettings,
585
595
  onApprove,
586
596
  onCancel,
587
- }: {
588
- agentName: string;
589
- agent?: string;
590
- isolated?: boolean;
597
+ }: DiagnoseConsentCopy & {
591
598
  onOpenSettings?: () => void;
592
599
  onApprove: () => void;
593
600
  onCancel: () => void;
594
601
  }) {
595
- // Cursor can't be isolated (no flag suppresses its global MCP servers), so it
596
- // gets its own honest framing rather than the isolated/my-setup pair.
597
- const isCursor = agent === "cursor-agent";
602
+ // `settingsLabel: null` hides the link outright a host with one fixed agent
603
+ // has nothing behind it. `undefined` keeps the OSS default label.
604
+ const resolvedSettingsLabel =
605
+ settingsLabel === undefined
606
+ ? "Change the agent and how it runs in Settings"
607
+ : settingsLabel;
598
608
  return (
599
609
  <div className="rounded-lg border border-theme-border bg-theme-elevated p-4">
600
610
  <div className="mb-2 flex items-center gap-2">
601
611
  <ShieldCheck className="h-4 w-4 text-accent" />
602
612
  <div className="text-sm font-medium text-theme-text-primary">
603
- {isolated && !isCursor
604
- ? "Run a read-only AI investigation?"
605
- : "Run an AI investigation?"}
613
+ {title}
606
614
  </div>
607
615
  </div>
608
- <p className="text-sm leading-relaxed text-theme-text-secondary">
609
- This runs{" "}
610
- <span className="font-medium text-theme-text-primary">
611
- your own {agentName}
612
- </span>{" "}
613
- on your machine — no Radar cloud, no API key, no account. Radar sends
614
- this resource&apos;s spec, recent events, and pod logs to it (and on to
615
- its model provider under your account, not to Radar). Transcripts are
616
- kept in your local Radar history on this machine until cleared.
617
- {isolated && !isCursor && (
618
- <>
619
- {" "}
620
- Through Radar the agent can only{" "}
621
- <span className="font-medium">read</span> — it cannot change your
622
- cluster.
623
- </>
624
- )}
625
- </p>
626
- <ul className="mt-2 space-y-1 text-xs text-theme-text-tertiary">
627
- {isCursor ? (
628
- <li>
629
- • Through Radar the agent only <span className="font-medium">reads</span>{" "}
630
- your cluster. But Cursor also loads your own global MCP servers and
631
- Radar can&apos;t exclude them (unlike Claude or Codex), so if any of
632
- those can make changes, Cursor could use them.
633
- </li>
634
- ) : isolated ? (
635
- <li>
636
- • Isolated: only Radar&apos;s read-only investigation tools — your
637
- other CLI config and MCP servers are excluded.
638
- {agent === "codex" && (
639
- <>
640
- {" "}
641
- Codex&apos;s sandboxed shell can still <em>read</em> files on
642
- your machine (it cannot write or reach the network).
643
- </>
644
- )}
645
- </li>
646
- ) : (
647
- <li>
648
- • &ldquo;My setup&rdquo;: the agent also runs with your own CLI
649
- config + MCP servers and can read local files. Only Radar&apos;s own
650
- tools are read-only.
651
- </li>
652
- )}
653
- </ul>
654
- {onOpenSettings && (
616
+ <div className="text-sm leading-relaxed text-theme-text-secondary">{body}</div>
617
+ {bullets && bullets.length > 0 && (
618
+ <ul className="mt-2 space-y-1 text-xs text-theme-text-tertiary">
619
+ {bullets.map((b, i) => (
620
+ <li key={i}>• {b}</li>
621
+ ))}
622
+ </ul>
623
+ )}
624
+ {onOpenSettings && resolvedSettingsLabel && (
655
625
  <button
656
626
  onClick={onOpenSettings}
657
627
  className="mt-3 text-xs text-accent hover:underline"
658
628
  >
659
- Change the agent and how it runs in Settings
629
+ {resolvedSettingsLabel}
660
630
  </button>
661
631
  )}
662
632
  <div className="mt-4 flex gap-2">
@@ -670,13 +640,110 @@ export function ConsentCard({
670
640
  onClick={onApprove}
671
641
  className="flex-1 rounded-lg btn-brand py-1.5 text-sm"
672
642
  >
673
- Approve &amp; investigate
643
+ {approveLabel}
674
644
  </button>
675
645
  </div>
676
646
  </div>
677
647
  );
678
648
  }
679
649
 
650
+ // The first-run consent + trust card. The copy is checkable fact about a data
651
+ // flow — not marketing — and the wrong claim is a lie, not a typo. It resolves
652
+ // in two tiers:
653
+ //
654
+ // 1. `copy` — the embedding host tells its own trust story. Only the host
655
+ // knows where its agent runs, whose key pays, and where transcripts live,
656
+ // so any host that runs the agent somewhere other than the OSS local CLI
657
+ // MUST override rather than let Radar assert the local story over its flow.
658
+ // 2. No `copy` — the OSS bring-your-own-local-CLI default, the only tier where
659
+ // "on your machine / no Radar cloud / your account" actually holds.
660
+ export function ConsentCard({
661
+ agentName,
662
+ agent,
663
+ isolated = true,
664
+ copy,
665
+ onOpenSettings,
666
+ onApprove,
667
+ onCancel,
668
+ }: {
669
+ agentName: string;
670
+ agent?: string;
671
+ isolated?: boolean;
672
+ copy?: DiagnoseConsentCopy;
673
+ onOpenSettings?: () => void;
674
+ onApprove: () => void;
675
+ onCancel: () => void;
676
+ }) {
677
+ const chrome = { onOpenSettings, onApprove, onCancel };
678
+
679
+ // Tier 1: a host (e.g. radar-hub-web) supplied its own copy — use it verbatim.
680
+ if (copy) return <ConsentCardShell {...copy} {...chrome} />;
681
+
682
+ // Tier 2: OSS BYO-local default. Cursor can't be isolated (no flag suppresses
683
+ // its global MCP servers), so it gets its own honest framing rather than the
684
+ // isolated/my-setup pair.
685
+ const isCursor = agent === "cursor-agent";
686
+ return (
687
+ <ConsentCardShell
688
+ {...chrome}
689
+ title={
690
+ isolated && !isCursor
691
+ ? "Run a read-only AI investigation?"
692
+ : "Run an AI investigation?"
693
+ }
694
+ body={
695
+ <>
696
+ This runs{" "}
697
+ <span className="font-medium text-theme-text-primary">
698
+ your own {agentName}
699
+ </span>{" "}
700
+ on your machine — no Radar cloud, no API key, no account. Radar sends
701
+ this resource&apos;s spec, recent events, and pod logs to it (and on to
702
+ its model provider under your account, not to Radar). Transcripts are
703
+ kept in your local Radar history on this machine until cleared.
704
+ {isolated && !isCursor && (
705
+ <>
706
+ {" "}
707
+ Through Radar the agent can only{" "}
708
+ <span className="font-medium">read</span> — it cannot change your
709
+ cluster.
710
+ </>
711
+ )}
712
+ </>
713
+ }
714
+ bullets={[
715
+ isCursor ? (
716
+ <>
717
+ Through Radar the agent only{" "}
718
+ <span className="font-medium">reads</span> your cluster. But Cursor
719
+ also loads your own global MCP servers and Radar can&apos;t exclude
720
+ them (unlike Claude or Codex), so if any of those can make changes,
721
+ Cursor could use them.
722
+ </>
723
+ ) : isolated ? (
724
+ <>
725
+ Isolated: only Radar&apos;s read-only investigation tools — your
726
+ other CLI config and MCP servers are excluded.
727
+ {agent === "codex" && (
728
+ <>
729
+ {" "}
730
+ Codex&apos;s sandboxed shell can still <em>read</em> files on
731
+ your machine (it cannot write or reach the network).
732
+ </>
733
+ )}
734
+ </>
735
+ ) : (
736
+ <>
737
+ &ldquo;My setup&rdquo;: the agent also runs with your own CLI config
738
+ + MCP servers and can read local files. Only Radar&apos;s own tools
739
+ are read-only.
740
+ </>
741
+ ),
742
+ ]}
743
+ />
744
+ );
745
+ }
746
+
680
747
  // The Apply confirmation — wider than a generic confirm so the recommended fix
681
748
  // (rendered markdown) is legible, making it unambiguous what the one click does.
682
749
  export function ApplyDialog({
@@ -1,4 +1,5 @@
1
- // Slot-based injection of a resource-level "Diagnose" action.
1
+ // Slot-based injection of a resource-level "Diagnose" action, and of the
2
+ // consent card's trust copy.
2
3
  //
3
4
  // Lets an embedding host (e.g. Radar Hub) inject a "Diagnose with AI" button
4
5
  // into every resource detail action bar — without forking WorkloadView or the
@@ -21,18 +22,47 @@ export type RenderDiagnoseAction = (ctx: {
21
22
  health?: "problem" | "healthy" | "unknown";
22
23
  }) => ReactNode;
23
24
 
25
+ /**
26
+ * Trust copy for the first-run consent card.
27
+ *
28
+ * The card makes concrete, checkable claims about *where* the agent runs,
29
+ * *whose* model account it bills, and *where* the transcript is stored. Those
30
+ * claims are only true of OSS's bring-your-own-local-CLI agent. A host that
31
+ * runs the agent anywhere else (Radar Cloud runs it as a sandboxed Job under a
32
+ * managed key) MUST supply its own copy — shipping OSS's over a different data
33
+ * flow states the opposite of what happens.
34
+ *
35
+ * Radar owns the card's chrome (icon, layout, Approve/Cancel) either way; a
36
+ * host only replaces the claims.
37
+ */
38
+ export type DiagnoseConsentCopy = {
39
+ title: string;
40
+ body: ReactNode;
41
+ /** Detail list under the body; each entry is rendered as its own "•" row. */
42
+ bullets?: ReactNode[];
43
+ /** Label for the settings link. `null` hides it — for hosts with one fixed
44
+ * agent and no isolation choice, where it would open an empty dialog. */
45
+ settingsLabel?: string | null;
46
+ approveLabel?: string;
47
+ };
48
+
24
49
  const DiagnoseCustomizationContext = createContext<RenderDiagnoseAction | undefined>(undefined);
50
+ const DiagnoseConsentCopyContext = createContext<DiagnoseConsentCopy | undefined>(undefined);
25
51
 
26
52
  export function DiagnoseCustomizationProvider({
27
53
  value,
54
+ consentCopy,
28
55
  children,
29
56
  }: {
30
57
  value: RenderDiagnoseAction | undefined;
58
+ consentCopy?: DiagnoseConsentCopy;
31
59
  children: ReactNode;
32
60
  }) {
33
61
  return (
34
62
  <DiagnoseCustomizationContext.Provider value={value}>
35
- {children}
63
+ <DiagnoseConsentCopyContext.Provider value={consentCopy}>
64
+ {children}
65
+ </DiagnoseConsentCopyContext.Provider>
36
66
  </DiagnoseCustomizationContext.Provider>
37
67
  );
38
68
  }
@@ -40,3 +70,7 @@ export function DiagnoseCustomizationProvider({
40
70
  export function useDiagnoseCustomization(): RenderDiagnoseAction | undefined {
41
71
  return useContext(DiagnoseCustomizationContext);
42
72
  }
73
+
74
+ export function useDiagnoseConsentCopy(): DiagnoseConsentCopy | undefined {
75
+ return useContext(DiagnoseConsentCopyContext);
76
+ }
package/src/index.ts CHANGED
@@ -26,7 +26,10 @@ export type {
26
26
  TimelineCoverageSpan,
27
27
  TimelineOverviewResult,
28
28
  } from './api/timelineSource';
29
- export type { RenderDiagnoseAction } from './context/DiagnoseCustomization';
29
+ export type {
30
+ RenderDiagnoseAction,
31
+ DiagnoseConsentCopy,
32
+ } from './context/DiagnoseCustomization';
30
33
  export { ShortcutHelpOverlay } from './components/ui/ShortcutHelpOverlay';
31
34
 
32
35
  // Shared cluster-switcher primitive — re-exported from @skyhook-io/k8s-ui so