@skyhook-io/radar-app 0.2.2 → 0.3.1

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.
Files changed (177) hide show
  1. package/README.md +7 -1
  2. package/package.json +33 -25
  3. package/src/App.tsx +1449 -382
  4. package/src/RadarApp.tsx +132 -19
  5. package/src/api/apiResources.ts +1 -1
  6. package/src/api/client.argoResourceSync.test.ts +69 -0
  7. package/src/api/client.delta.test.ts +89 -0
  8. package/src/api/client.deltaSync.test.ts +216 -0
  9. package/src/api/client.metrics.test.ts +106 -0
  10. package/src/api/client.rightsizing.test.ts +32 -0
  11. package/src/api/client.ts +2730 -271
  12. package/src/api/client.yaml.test.ts +45 -0
  13. package/src/api/diagnose.ts +289 -0
  14. package/src/api/quotas.ts +16 -0
  15. package/src/api/rbac.ts +57 -0
  16. package/src/api/timelineSource.test.ts +217 -0
  17. package/src/api/timelineSource.ts +582 -0
  18. package/src/components/ConnectionErrorView.tsx +186 -70
  19. package/src/components/ContextSwitcher.tsx +63 -18
  20. package/src/components/DebugOverlay.tsx +5 -3
  21. package/src/components/NamespaceSwitcher.tsx +41 -0
  22. package/src/components/UserMenu.tsx +69 -21
  23. package/src/components/applications/ApplicationsView.tsx +936 -0
  24. package/src/components/audit/AuditSettingsDialog.tsx +79 -17
  25. package/src/components/audit/AuditView.tsx +65 -62
  26. package/src/components/compare/CompareViewRoute.tsx +124 -0
  27. package/src/components/compare/useCompareCandidates.ts +27 -0
  28. package/src/components/compare/useCompareLauncher.tsx +79 -0
  29. package/src/components/cost/ApplicationCostTab.test.ts +204 -0
  30. package/src/components/cost/ApplicationCostTab.tsx +571 -0
  31. package/src/components/cost/CostTrendChart.tsx +106 -75
  32. package/src/components/cost/CostView.test.ts +12 -0
  33. package/src/components/cost/CostView.tsx +507 -223
  34. package/src/components/cost/CostViewTabs.test.tsx +21 -0
  35. package/src/components/cost/CostViewTabs.tsx +40 -0
  36. package/src/components/cost/CurrentAllocationUse.test.ts +21 -0
  37. package/src/components/cost/CurrentAllocationUse.tsx +126 -0
  38. package/src/components/cost/WorkloadCostTab.test.ts +153 -0
  39. package/src/components/cost/WorkloadCostTab.tsx +372 -0
  40. package/src/components/cost/cloud-console.test.ts +39 -0
  41. package/src/components/cost/cloud-console.ts +81 -0
  42. package/src/components/cost/errors.ts +8 -0
  43. package/src/components/cost/format.test.ts +27 -0
  44. package/src/components/cost/format.ts +46 -0
  45. package/src/components/cost/kinds.ts +5 -0
  46. package/src/components/curl/ServiceCurlButton.tsx +445 -0
  47. package/src/components/diagnose/AISettings.tsx +147 -0
  48. package/src/components/diagnose/DiagnoseContext.tsx +495 -0
  49. package/src/components/diagnose/DiagnoseSurface.tsx +394 -0
  50. package/src/components/diagnose/Home.tsx +163 -0
  51. package/src/components/diagnose/InvestigationView.tsx +622 -0
  52. package/src/components/diagnose/LocalDiagnoseAction.tsx +162 -0
  53. package/src/components/diagnose/launch.ts +65 -0
  54. package/src/components/diagnose/parts.tsx +1756 -0
  55. package/src/components/dock/BottomDock.tsx +2 -3
  56. package/src/components/dock/DockContext.tsx +1 -0
  57. package/src/components/dock/TerminalTab.tsx +1 -1
  58. package/src/components/dock/WorkloadLogsTab.tsx +21 -5
  59. package/src/components/dock/index.ts +1 -1
  60. package/src/components/execution/BatchExecutionView.test.ts +170 -0
  61. package/src/components/execution/BatchExecutionView.tsx +1329 -0
  62. package/src/components/execution/batch-run-actions.test.ts +48 -0
  63. package/src/components/execution/batch-run-actions.ts +24 -0
  64. package/src/components/execution/batch-timeline.test.ts +57 -0
  65. package/src/components/execution/batch-timeline.ts +46 -0
  66. package/src/components/execution/execution-definition.test.ts +208 -0
  67. package/src/components/execution/execution-definition.ts +245 -0
  68. package/src/components/gitops/ArgoResourceDiffLoader.tsx +23 -0
  69. package/src/components/gitops/GitOpsView.tsx +1042 -0
  70. package/src/components/gitops/RevisionMetaChip.tsx +63 -0
  71. package/src/components/helm/ChartBrowser.tsx +87 -31
  72. package/src/components/helm/HelmCompareRoute.tsx +1341 -0
  73. package/src/components/helm/HelmReleaseDrawer.test.ts +17 -0
  74. package/src/components/helm/HelmReleaseDrawer.tsx +1073 -102
  75. package/src/components/helm/HelmView.tsx +237 -96
  76. package/src/components/helm/InstallWizard.tsx +94 -38
  77. package/src/components/helm/ManifestDiffViewer.tsx +8 -27
  78. package/src/components/helm/OwnedResources.tsx +34 -59
  79. package/src/components/helm/RevisionHistory.tsx +52 -3
  80. package/src/components/helm/RoleGatedPanel.tsx +3 -3
  81. package/src/components/helm/TrackChartSourceDialog.tsx +185 -0
  82. package/src/components/helm/ValuesDiffPreview.tsx +17 -7
  83. package/src/components/helm/ValuesViewer.tsx +49 -53
  84. package/src/components/helm/helm-utils.ts +4 -0
  85. package/src/components/home/ActivitySummary.tsx +4 -1
  86. package/src/components/home/ClusterHealthCard.tsx +56 -42
  87. package/src/components/home/CostCard.tsx +21 -36
  88. package/src/components/home/GitOpsControllersCard.tsx +110 -0
  89. package/src/components/home/HelmSummary.tsx +3 -1
  90. package/src/components/home/HomeView.tsx +339 -105
  91. package/src/components/home/MCPSetupDialog.tsx +29 -87
  92. package/src/components/home/TrafficSummary.tsx +2 -2
  93. package/src/components/home/mcpToolCatalog.ts +333 -0
  94. package/src/components/issues/IssuesPane.tsx +151 -0
  95. package/src/components/logs/LogsViewer.tsx +4 -1
  96. package/src/components/logs/ScheduledWorkloadLogsViewer.tsx +135 -0
  97. package/src/components/logs/WorkloadLogsViewer.tsx +4 -1
  98. package/src/components/nav/PrimaryNavRail.tsx +285 -0
  99. package/src/components/portforward/PortForwardButton.tsx +118 -47
  100. package/src/components/portforward/PortForwardManager.tsx +253 -131
  101. package/src/components/resource/HPACharts.tsx +237 -0
  102. package/src/components/resource/PVCUsageBar.tsx +59 -0
  103. package/src/components/resource/PrometheusCharts.tsx +160 -584
  104. package/src/components/resource/PrometheusChartsGrid.tsx +270 -0
  105. package/src/components/resource/RestartChart.tsx +133 -0
  106. package/src/components/resource/RightsizingStrip.test.ts +109 -0
  107. package/src/components/resource/RightsizingStrip.tsx +363 -0
  108. package/src/components/resource-drawer/ResourceDrawer.tsx +3 -1
  109. package/src/components/resources/CompositeRenderer.tsx +101 -0
  110. package/src/components/resources/ImageFilesystemModal.tsx +19 -12
  111. package/src/components/resources/PodFilesystemModal.tsx +6 -5
  112. package/src/components/resources/ResourceDetailDrawer.tsx +13 -3
  113. package/src/components/resources/ResourcesView.tsx +194 -17
  114. package/src/components/resources/renderers/CronWorkflowRenderer.tsx +1 -0
  115. package/src/components/resources/renderers/HPARenderer.tsx +20 -1
  116. package/src/components/resources/renderers/NamespaceRenderer.tsx +31 -0
  117. package/src/components/resources/renderers/NodeRenderer.tsx +10 -4
  118. package/src/components/resources/renderers/PVCRenderer.tsx +19 -1
  119. package/src/components/resources/renderers/PodRenderer.tsx +30 -6
  120. package/src/components/resources/renderers/RoleBindingRenderer.tsx +45 -1
  121. package/src/components/resources/renderers/RoleRenderer.tsx +27 -1
  122. package/src/components/resources/renderers/ServiceAccountRenderer.tsx +28 -1
  123. package/src/components/resources/renderers/ServiceRenderer.tsx +81 -8
  124. package/src/components/resources/renderers/WorkloadRenderer.tsx +51 -4
  125. package/src/components/resources/renderers/index.ts +2 -0
  126. package/src/components/resources/resource-utils.ts +2 -1
  127. package/src/components/rightsizing/RightsizingScanView.tsx +938 -0
  128. package/src/components/rightsizing/copy.test.ts +56 -0
  129. package/src/components/rightsizing/model.test.ts +227 -0
  130. package/src/components/rightsizing/model.ts +158 -0
  131. package/src/components/rightsizing/presentation.test.ts +104 -0
  132. package/src/components/rightsizing/presentation.ts +94 -0
  133. package/src/components/settings/MyPermissionsDialog.tsx +241 -0
  134. package/src/components/settings/SettingsDialog.tsx +1505 -165
  135. package/src/components/shared/CreateResourceDialog.tsx +9 -2
  136. package/src/components/shared/LargeClusterNamespacePicker.tsx +3 -3
  137. package/src/components/timeline/LocalTimelineScrubber.tsx +212 -0
  138. package/src/components/timeline/RetainedTimelineScrubber.tsx +311 -0
  139. package/src/components/timeline/TimelineList.tsx +86 -13
  140. package/src/components/timeline/TimelineSwimlanes.tsx +9 -1299
  141. package/src/components/timeline/TimelineView.tsx +873 -24
  142. package/src/components/timeline/TimelineView.urlparams.test.ts +335 -0
  143. package/src/components/traffic/TrafficFilterSidebar.tsx +10 -45
  144. package/src/components/traffic/TrafficFlowList.tsx +29 -15
  145. package/src/components/traffic/TrafficGraph.tsx +42 -24
  146. package/src/components/traffic/TrafficView.tsx +32 -19
  147. package/src/components/ui/CommandPalette.tsx +8 -215
  148. package/src/components/ui/DiagnosticsOverlay.tsx +219 -9
  149. package/src/components/ui/Markdown.tsx +3 -3
  150. package/src/components/ui/Omnibar.tsx +602 -0
  151. package/src/components/ui/RadarOmnibar.tsx +52 -0
  152. package/src/components/ui/SearchSyntaxHelp.tsx +89 -0
  153. package/src/components/ui/ShortcutHelpOverlay.tsx +3 -2
  154. package/src/components/ui/UpdateNotification.tsx +48 -36
  155. package/src/components/ui/command-items.ts +178 -0
  156. package/src/components/workload/WorkloadView.tsx +1342 -158
  157. package/src/context/ConnectionContext.tsx +146 -21
  158. package/src/context/DiagnoseCustomization.tsx +93 -0
  159. package/src/context/NavCustomization.tsx +75 -0
  160. package/src/context/TimelineSource.tsx +50 -0
  161. package/src/contexts/CapabilitiesContext.tsx +32 -8
  162. package/src/filter/FilterLocationBridge.tsx +30 -0
  163. package/src/hooks/useClusterLoadState.ts +73 -0
  164. package/src/hooks/useDocumentTitle.ts +25 -0
  165. package/src/hooks/useEventSource.ts +6 -0
  166. package/src/hooks/useKeyboardShortcuts.tsx +1 -0
  167. package/src/hooks/useMediaQuery.ts +21 -0
  168. package/src/hooks/useNavRailPinned.ts +46 -0
  169. package/src/hooks/useRecentResources.ts +49 -0
  170. package/src/index.css +162 -1
  171. package/src/index.ts +73 -1
  172. package/src/main.tsx +7 -5
  173. package/src/types/clusterLoadState.ts +33 -0
  174. package/src/types.ts +2 -0
  175. package/src/utils/auditBadges.ts +53 -0
  176. package/src/utils/navigation.ts +64 -1
  177. package/src/components/ui/NamespaceSelector.tsx +0 -436
@@ -0,0 +1,394 @@
1
+ // The right-docked shell of the AI surface. Two layouts:
2
+ // - docked: a single-pane right column (app reflows left via the provider's push)
3
+ // - expanded: a master-detail workspace that fills ONLY the content area (does
4
+ // not cover the left nav rail or top bar) — recent list on the left, the
5
+ // selected investigation/report on the right.
6
+ import { useState } from "react";
7
+ import {
8
+ Sparkles,
9
+ X,
10
+ Maximize2,
11
+ Minimize2,
12
+ ChevronLeft,
13
+ Settings2,
14
+ MoreVertical,
15
+ TerminalSquare,
16
+ Copy,
17
+ Check,
18
+ } from "lucide-react";
19
+ import { Tooltip } from "../ui/Tooltip";
20
+ import {
21
+ useDiagnose,
22
+ useDiagnoseLayout,
23
+ agentLabelFor,
24
+ openDiagnoseSettings,
25
+ } from "./DiagnoseContext";
26
+ import { useDiagnoseCustomization } from "../../context/DiagnoseCustomization";
27
+ import { InvestigationView } from "./InvestigationView";
28
+ import { RecentList } from "./Home";
29
+ import { ConsentCard } from "./parts";
30
+ import { buildLaunchCommand, launchAgentLabel, openInTerminal } from "./launch";
31
+ import { type RunSummary } from "../../api/diagnose";
32
+
33
+ function capWord(s: string): string {
34
+ return s ? s[0].toUpperCase() + s.slice(1) : s;
35
+ }
36
+
37
+ // buildConfigLine renders the active AI config as the header subtitle. Codex shows
38
+ // its isolation mode + effective reasoning effort (Default → medium); a model
39
+ // override is shown for either agent. Reflects a run's recorded settings, or the
40
+ // current defaults on Home.
41
+ function buildConfigLine(cfg: {
42
+ agent?: string;
43
+ isolated?: boolean;
44
+ model?: string;
45
+ effort?: string;
46
+ }): string {
47
+ const parts = [agentLabelFor(cfg.agent ?? "")];
48
+ if (cfg.agent === "codex") {
49
+ parts.push(cfg.isolated === false ? "My setup" : "Isolated");
50
+ parts.push(`${capWord(cfg.effort || "medium")} effort`);
51
+ }
52
+ if (cfg.model) parts.push(capWord(cfg.model));
53
+ return "via " + parts.join(" · ");
54
+ }
55
+
56
+ // InvestigationMenu hands an investigation off to the user's own full agent. The
57
+ // PRIMARY action copies a resume command they can paste wherever they actually
58
+ // work (terminal, tmux, IDE) — destination-agnostic. Running it in Radar's built-in
59
+ // terminal is the secondary "run it here" convenience.
60
+ function InvestigationMenu({ run }: { run: RunSummary }) {
61
+ const [open, setOpen] = useState(false);
62
+ const [copied, setCopied] = useState(false);
63
+ const label = launchAgentLabel(run);
64
+ const command = buildLaunchCommand(run, `${window.location.origin}/mcp`);
65
+ // No resumable session yet (or stale run) → nothing to hand off.
66
+ if (!command) return null;
67
+
68
+ const toggle = () => {
69
+ setCopied(false);
70
+ setOpen((v) => !v);
71
+ };
72
+ const copy = () => {
73
+ void navigator.clipboard?.writeText(command);
74
+ setCopied(true);
75
+ setTimeout(() => {
76
+ setCopied(false);
77
+ setOpen(false);
78
+ }, 1100);
79
+ };
80
+
81
+ return (
82
+ <div className="relative flex items-center">
83
+ <Tooltip content="More" position="bottom">
84
+ <button
85
+ onClick={toggle}
86
+ className="rounded-md p-1 text-theme-text-tertiary hover:bg-theme-hover hover:text-theme-text-primary"
87
+ aria-label="More actions"
88
+ aria-haspopup="menu"
89
+ aria-expanded={open}
90
+ >
91
+ <MoreVertical className="h-4 w-4" />
92
+ </button>
93
+ </Tooltip>
94
+ {open && (
95
+ <>
96
+ <div className="fixed inset-0 z-10" onClick={() => setOpen(false)} />
97
+ <div className="absolute right-0 top-full z-20 mt-1 w-64 rounded-lg border border-theme-border bg-theme-surface py-1 shadow-theme-lg">
98
+ <button
99
+ onClick={copy}
100
+ className="flex w-full items-start gap-2 px-3 py-1.5 text-left text-sm text-theme-text-primary hover:bg-theme-hover"
101
+ >
102
+ {copied ? (
103
+ <Check className="mt-0.5 h-4 w-4 shrink-0 text-emerald-500" />
104
+ ) : (
105
+ <Copy className="mt-0.5 h-4 w-4 shrink-0 text-theme-text-tertiary" />
106
+ )}
107
+ <span>
108
+ {copied ? "Copied ✓" : `Copy command to continue in ${label}`}
109
+ {!copied && (
110
+ <span className="block text-[11px] text-theme-text-tertiary">
111
+ Paste it wherever you run {label} — resumes this exact
112
+ session.
113
+ </span>
114
+ )}
115
+ </span>
116
+ </button>
117
+ <button
118
+ onClick={() => {
119
+ openInTerminal(command, "Diagnose");
120
+ setOpen(false);
121
+ }}
122
+ className="flex w-full items-center gap-2 px-3 py-1.5 text-left text-sm text-theme-text-primary hover:bg-theme-hover"
123
+ >
124
+ <TerminalSquare className="h-4 w-4 shrink-0 text-theme-text-tertiary" />
125
+ Run in a Radar terminal
126
+ </button>
127
+ </div>
128
+ </>
129
+ )}
130
+ </div>
131
+ );
132
+ }
133
+
134
+ // The panel is an ABSOLUTE slot inside the app's body frame (the column under the
135
+ // header, right of the nav rail) — App renders it there and passes topInset (the
136
+ // header height; 0 in chromeless embeds). It shares that frame with the resource/
137
+ // Helm drawers, so it no longer floats viewport-fixed or DOM-measures the chrome.
138
+ export function DiagnoseSurface({ topInset = 0 }: { topInset?: number }) {
139
+ const d = useDiagnose();
140
+ // Injected settings action: undefined = Radar's own Settings dialog;
141
+ // null = hide the gear + links.
142
+ const { consentCopy, onOpenSettings: hostOpenSettings } = useDiagnoseCustomization();
143
+ const openSettings =
144
+ hostOpenSettings === undefined ? openDiagnoseSettings : hostOpenSettings;
145
+ const {
146
+ maximized,
147
+ setMaximized,
148
+ panelWidth: width,
149
+ setPanelWidth: setWidth,
150
+ panelNarrow: narrow,
151
+ panelBounds: { min: minW, max: maxW },
152
+ panelWidthKey: widthKey,
153
+ } = useDiagnoseLayout();
154
+
155
+ const startResize = (e: React.MouseEvent) => {
156
+ e.preventDefault();
157
+ const onMove = (m: MouseEvent) =>
158
+ setWidth(() =>
159
+ Math.min(maxW, Math.max(minW, window.innerWidth - m.clientX)),
160
+ );
161
+ const onUp = () => {
162
+ document.removeEventListener("mousemove", onMove);
163
+ document.removeEventListener("mouseup", onUp);
164
+ setWidth((w) => {
165
+ try {
166
+ localStorage.setItem(widthKey, String(w));
167
+ } catch {
168
+ /* storage disabled — width just won't persist */
169
+ }
170
+ return w;
171
+ });
172
+ };
173
+ document.addEventListener("mousemove", onMove);
174
+ document.addEventListener("mouseup", onUp);
175
+ };
176
+
177
+ const activeRun = d.runs.find((r) => r.id === d.activeRunId) ?? null;
178
+ // A focused run shows the agent it actually ran with; Home reflects the current pick.
179
+ const activeAgentLabel = activeRun?.agent
180
+ ? agentLabelFor(activeRun.agent)
181
+ : d.agentLabel;
182
+ // Header subtitle: the config a focused run actually used (it records agent /
183
+ // isolation / model / effort), or the current defaults on Home. Codex shows mode
184
+ // + reasoning effort; model is shown only when overridden. Clicking opens Settings.
185
+ const configLine = buildConfigLine(
186
+ activeRun ?? {
187
+ agent: d.selectedAgent,
188
+ isolated: d.isolated,
189
+ model: d.model,
190
+ effort: d.effort,
191
+ },
192
+ );
193
+ const detailTitle = activeRun
194
+ ? `${activeRun.kind} ${activeRun.namespace ? `${activeRun.namespace}/` : ""}${activeRun.name}`
195
+ : "AI investigations";
196
+
197
+ // Absolute within the body frame: maximized fills it; docked is a right slot.
198
+ // topInset clears the header (the frame spans the full column incl. the header).
199
+ const positionStyle: React.CSSProperties = maximized
200
+ ? { top: topInset, left: 0, right: 0, bottom: 0 }
201
+ : { top: topInset, right: 0, bottom: 0, width, maxWidth: "100%" };
202
+
203
+ // The detail pane (right side when expanded; the whole body when docked).
204
+ // Keyed by run id so toggling Expand doesn't remount a focused run's view.
205
+ const detail = d.needsConsent ? (
206
+ <div className="flex-1 overflow-y-auto px-4 py-3">
207
+ <div className={maximized ? "mx-auto max-w-3xl" : ""}>
208
+ <ConsentCard
209
+ agentName={d.agentLabel}
210
+ agent={d.selectedAgent}
211
+ isolated={d.isolated}
212
+ copy={consentCopy}
213
+ onOpenSettings={openSettings ?? undefined}
214
+ onApprove={d.approveConsent}
215
+ onCancel={d.cancelConsent}
216
+ />
217
+ </div>
218
+ </div>
219
+ ) : activeRun ? (
220
+ <InvestigationView
221
+ key={activeRun.id}
222
+ run={activeRun}
223
+ agentLabel={activeAgentLabel}
224
+ maximized={maximized}
225
+ />
226
+ ) : d.activeRunId && !d.runsLoaded ? (
227
+ // Deep-linked to a run before the list has ever loaded: show the load
228
+ // state (the 4s poll retries while the panel is open) — never the generic
229
+ // placeholder, and never a premature "no longer available".
230
+ <div className="flex flex-1 items-center justify-center px-6 text-center text-sm text-theme-text-tertiary">
231
+ {d.runsLoadFailed
232
+ ? "Couldn't load investigations — retrying…"
233
+ : "Loading investigations…"}
234
+ </div>
235
+ ) : d.activeRunId && d.runsLoaded ? (
236
+ // A focused id that isn't in the loaded list — a deep link (?ai-run=…) to a
237
+ // cleared/evicted/unknown run. Say so; the generic "select an
238
+ // investigation" placeholder would read as a broken link.
239
+ <div className="flex flex-1 flex-col items-center justify-center gap-3 px-6 text-center">
240
+ <p className="text-sm text-theme-text-secondary">
241
+ This investigation is no longer available — history keeps the most
242
+ recent investigations, and this one has been cleared.
243
+ </p>
244
+ <button
245
+ onClick={d.goHome}
246
+ className="rounded-lg border border-theme-border px-3 py-1.5 text-sm text-theme-text-secondary hover:bg-theme-hover"
247
+ >
248
+ View investigations
249
+ </button>
250
+ </div>
251
+ ) : d.startError ? (
252
+ <div className="flex flex-1 flex-col items-center justify-center gap-3 px-6 text-center">
253
+ <p className="text-sm text-theme-text-secondary">{d.startError}</p>
254
+ <button
255
+ onClick={d.dismissError}
256
+ className="rounded-lg border border-theme-border px-3 py-1.5 text-sm text-theme-text-secondary hover:bg-theme-hover"
257
+ >
258
+ Dismiss
259
+ </button>
260
+ </div>
261
+ ) : (
262
+ <div className="flex flex-1 items-center justify-center px-6 text-center text-sm text-theme-text-tertiary">
263
+ Select an investigation, or open a resource and click Diagnose.
264
+ </div>
265
+ );
266
+
267
+ const showBreadcrumb = !maximized && d.view !== "home";
268
+ // The maximized workspace always shows the recent-investigations list (there's
269
+ // room for it in full-wide) — it's the master pane of the master-detail layout.
270
+ const showHistory = maximized;
271
+
272
+ return (
273
+ <div
274
+ role="dialog"
275
+ aria-label="AI investigations"
276
+ className="absolute z-40 flex flex-col border-l border-theme-border bg-theme-surface shadow-drawer"
277
+ style={{
278
+ ...positionStyle,
279
+ animation: "slide-in-from-right 0.22s cubic-bezier(0.32,0.72,0,1)",
280
+ }}
281
+ >
282
+ {!maximized && !narrow && (
283
+ <div
284
+ onMouseDown={startResize}
285
+ className="absolute left-0 top-0 z-10 h-full w-1.5 cursor-ew-resize bg-theme-border/40 transition-colors hover:bg-accent/50"
286
+ role="separator"
287
+ aria-orientation="vertical"
288
+ aria-label="Resize panel"
289
+ />
290
+ )}
291
+
292
+ {/* Header */}
293
+ <div className="flex items-center justify-between border-b border-theme-border px-4 py-2.5">
294
+ <div className="flex min-w-0 items-center gap-2">
295
+ {!showBreadcrumb && (
296
+ <Sparkles className="h-4 w-4 shrink-0 text-accent" />
297
+ )}
298
+ <div className="min-w-0">
299
+ {showBreadcrumb && (
300
+ <button
301
+ onClick={d.goHome}
302
+ className="-ml-1 mb-0.5 flex items-center gap-0.5 rounded px-1 text-[11px] text-theme-text-tertiary hover:text-theme-text-primary"
303
+ >
304
+ <ChevronLeft className="h-3 w-3" />
305
+ Investigations
306
+ </button>
307
+ )}
308
+ <div className="truncate text-sm font-medium text-theme-text-primary">
309
+ {detailTitle}
310
+ </div>
311
+ <div className="flex items-center gap-1 text-xs text-theme-text-tertiary">
312
+ <span className="truncate">{configLine}</span>
313
+ {openSettings && (
314
+ <Tooltip content="AI settings" position="bottom">
315
+ <button
316
+ onClick={openSettings}
317
+ className="shrink-0 rounded p-0.5 text-theme-text-tertiary hover:text-theme-text-primary"
318
+ aria-label="AI settings"
319
+ >
320
+ <Settings2 className="h-3 w-3" />
321
+ </button>
322
+ </Tooltip>
323
+ )}
324
+ </div>
325
+ </div>
326
+ </div>
327
+ <div className="flex shrink-0 items-center gap-0.5">
328
+ {activeRun && <InvestigationMenu run={activeRun} />}
329
+ <Tooltip content={maximized ? "Restore" : "Expand"} position="bottom">
330
+ <button
331
+ onClick={() => setMaximized((v) => !v)}
332
+ className="rounded-md p-1 text-theme-text-tertiary hover:bg-theme-hover hover:text-theme-text-primary"
333
+ aria-label={maximized ? "Restore" : "Expand"}
334
+ >
335
+ {maximized ? (
336
+ <Minimize2 className="h-4 w-4" />
337
+ ) : (
338
+ <Maximize2 className="h-4 w-4" />
339
+ )}
340
+ </button>
341
+ </Tooltip>
342
+ <Tooltip content="Close" position="bottom">
343
+ <button
344
+ onClick={d.close}
345
+ className="rounded-md p-1 text-theme-text-tertiary hover:bg-theme-hover hover:text-theme-text-primary"
346
+ aria-label="Close"
347
+ >
348
+ <X className="h-4 w-4" />
349
+ </button>
350
+ </Tooltip>
351
+ </div>
352
+ </div>
353
+
354
+ {/* Body. The detail wrapper keeps a stable position + key across both
355
+ layouts so toggling Expand doesn't remount a live InvestigationView
356
+ (which would discard its transcript and re-run the agent). The aside
357
+ only appears when expanded; keys keep the detail node identity-stable
358
+ as it comes and goes. */}
359
+ <div className="flex min-h-0 flex-1">
360
+ {showHistory && (
361
+ <aside
362
+ key="recent"
363
+ className="w-72 shrink-0 overflow-y-auto border-r border-theme-border px-3 py-3"
364
+ >
365
+ <RecentList
366
+ agentLabel={d.agentLabel}
367
+ runs={d.runs}
368
+ selectedId={d.activeRunId}
369
+ onSelect={d.openRun}
370
+ historyDegraded={d.historyDegraded}
371
+ />
372
+ </aside>
373
+ )}
374
+ {!maximized && d.view === "home" ? (
375
+ <div
376
+ key="main"
377
+ className="flex-1 overflow-y-auto overflow-x-hidden px-4 py-3"
378
+ >
379
+ <RecentList
380
+ agentLabel={d.agentLabel}
381
+ runs={d.runs}
382
+ onSelect={d.openRun}
383
+ historyDegraded={d.historyDegraded}
384
+ />
385
+ </div>
386
+ ) : (
387
+ <div key="main" className="flex min-h-0 min-w-0 flex-1 flex-col">
388
+ {detail}
389
+ </div>
390
+ )}
391
+ </div>
392
+ </div>
393
+ );
394
+ }
@@ -0,0 +1,163 @@
1
+ // The recent-investigations list — now backed by server-side runs (the source of
2
+ // truth), so background/running investigations appear here live. Used both as the
3
+ // docked Home view and the master pane of the maximized workspace.
4
+ import { Loader2, Sparkles } from "lucide-react";
5
+ import { StatusDot, type StatusTone } from "@skyhook-io/k8s-ui";
6
+ import { type RunSummary } from "../../api/diagnose";
7
+
8
+ // Compact "3m ago" / "2h ago" / date label.
9
+ function relativeTime(ts: number, now: number): string {
10
+ const s = Math.max(0, Math.round((now - ts) / 1000));
11
+ if (s < 60) return "just now";
12
+ if (s < 3600) return `${Math.floor(s / 60)}m ago`;
13
+ if (s < 86400) return `${Math.floor(s / 3600)}h ago`;
14
+ if (s < 7 * 86400) return `${Math.floor(s / 86400)}d ago`;
15
+ return new Date(ts).toLocaleDateString();
16
+ }
17
+
18
+ // Map a run status to the design-system status tone (StatusDot). stopped is
19
+ // user-initiated → neutral/unknown, NOT a failure (distinct from error).
20
+ function runTone(status: RunSummary["status"]): StatusTone {
21
+ switch (status) {
22
+ case "error":
23
+ return "unhealthy";
24
+ case "stale":
25
+ return "degraded";
26
+ case "done":
27
+ return "healthy";
28
+ default: // stopped
29
+ return "unknown";
30
+ }
31
+ }
32
+
33
+ function statusDot(status: RunSummary["status"]) {
34
+ if (status === "running")
35
+ return <Loader2 className="h-3 w-3 shrink-0 animate-spin text-accent" />;
36
+ return <StatusDot tone={runTone(status)} className="shrink-0" />;
37
+ }
38
+
39
+ // A short text status for terminal non-done states, so the run's outcome doesn't
40
+ // rely on decoding a 6px colored dot (and so "I stopped it" reads differently from
41
+ // "it failed"). Done/running are conveyed by the dot + time already.
42
+ function statusWord(
43
+ status: RunSummary["status"],
44
+ ): { text: string; cls: string } | null {
45
+ switch (status) {
46
+ case "error":
47
+ return { text: "Failed", cls: "text-red-400" };
48
+ case "stopped":
49
+ return { text: "Stopped", cls: "text-theme-text-tertiary" };
50
+ case "stale":
51
+ // Plain words, not the internal status name: "stale" means the run was
52
+ // about a cluster that's no longer connected.
53
+ return { text: "Different cluster", cls: "text-amber-500" };
54
+ default:
55
+ return null;
56
+ }
57
+ }
58
+
59
+ export function RecentList({
60
+ agentLabel,
61
+ runs,
62
+ onSelect,
63
+ selectedId,
64
+ historyDegraded = false,
65
+ }: {
66
+ agentLabel: string;
67
+ runs: RunSummary[];
68
+ onSelect: (id: string) => void;
69
+ selectedId?: string | null;
70
+ historyDegraded?: boolean;
71
+ }) {
72
+ const now = Date.now();
73
+
74
+ // Persistence broke (disk error) — without this the user reasonably assumes
75
+ // their history survives a restart, and it won't.
76
+ const degradedNote = historyDegraded ? (
77
+ <div className="mb-2 rounded-md border border-amber-500/40 bg-amber-500/10 px-2.5 py-1.5 text-[11px] leading-snug text-theme-text-secondary">
78
+ History isn&apos;t being saved right now (disk error) — investigations
79
+ won&apos;t survive a restart.
80
+ </div>
81
+ ) : null;
82
+
83
+ if (runs.length === 0) {
84
+ return (
85
+ <div>
86
+ {degradedNote}
87
+ <div className="flex flex-col items-center px-4 py-12 text-center">
88
+ <Sparkles className="mb-3 h-7 w-7 text-accent" />
89
+ <div className="text-sm font-medium text-theme-text-primary">
90
+ No investigations yet
91
+ </div>
92
+ <p className="mt-1 max-w-xs text-sm text-theme-text-tertiary">
93
+ Open a resource and use its{" "}
94
+ <Sparkles className="inline h-3.5 w-3.5 align-text-bottom text-accent" />{" "}
95
+ action to investigate it with {agentLabel} —{" "}
96
+ <span className="font-medium text-theme-text-secondary">Diagnose</span>{" "}
97
+ a problem, or just ask about it. Investigations run in the background
98
+ and are kept in your history here.
99
+ </p>
100
+ </div>
101
+ </div>
102
+ );
103
+ }
104
+
105
+ return (
106
+ <div className="space-y-2">
107
+ {degradedNote}
108
+ <div className="text-[11px] font-medium uppercase tracking-wide text-theme-text-tertiary">
109
+ Investigations
110
+ </div>
111
+ {runs.map((r) => (
112
+ <button
113
+ key={r.id}
114
+ onClick={() => onSelect(r.id)}
115
+ className={`flex w-full flex-col gap-0.5 rounded-md border px-2.5 py-2 text-left ${
116
+ r.id === selectedId
117
+ ? "border-accent/50 bg-accent/10"
118
+ : "border-theme-border/60 bg-theme-base/40 hover:bg-theme-hover"
119
+ }`}
120
+ >
121
+ <div className="flex items-center gap-2">
122
+ {statusDot(r.status)}
123
+ <span className="min-w-0 flex-1 truncate text-sm text-theme-text-primary">
124
+ {r.kind} {r.namespace ? `${r.namespace}/` : ""}
125
+ {r.name}
126
+ </span>
127
+ <span className="shrink-0 text-[11px] text-theme-text-tertiary">
128
+ {r.status === "running" ? (
129
+ "running…"
130
+ ) : (
131
+ <>
132
+ {(() => {
133
+ const w = statusWord(r.status);
134
+ return w ? (
135
+ <span className={`font-medium ${w.cls}`}>
136
+ {w.text} ·{" "}
137
+ </span>
138
+ ) : null;
139
+ })()}
140
+ {relativeTime(new Date(r.updatedAt).getTime(), now)}
141
+ </>
142
+ )}
143
+ </span>
144
+ </div>
145
+ {(r.status === "stale" && r.context) || r.preview ? (
146
+ <div className="truncate pl-3.5 text-xs text-theme-text-tertiary">
147
+ {/* A foreign-cluster run names its cluster — in mixed multi-
148
+ context history, identical-looking rows otherwise give no way
149
+ to tell WHICH cluster an investigation was about. */}
150
+ {r.status === "stale" && r.context ? (
151
+ <span className="text-amber-600/80 dark:text-amber-500/80">
152
+ {r.context}
153
+ </span>
154
+ ) : null}
155
+ {r.status === "stale" && r.context && r.preview ? " · " : ""}
156
+ {r.preview}
157
+ </div>
158
+ ) : null}
159
+ </button>
160
+ ))}
161
+ </div>
162
+ );
163
+ }