@skyhook-io/radar-app 1.14.2 → 1.14.3

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 (74) hide show
  1. package/package.json +2 -2
  2. package/src/api/client.ts +30 -0
  3. package/src/components/CloudConnectFlow.tsx +21 -7
  4. package/src/components/CloudFunnelButton.tsx +85 -8
  5. package/src/components/cloudConnectHandoff.test.ts +10 -1
  6. package/src/components/cloudConnectHandoff.ts +15 -1
  7. package/src/components/diagnose/AIMarkdown.tsx +87 -0
  8. package/src/components/diagnose/ActivityTurn.tsx +1244 -0
  9. package/src/components/diagnose/AgentControls.tsx +603 -0
  10. package/src/components/diagnose/AnalysisStory.test.tsx +31 -25
  11. package/src/components/diagnose/AnalysisStory.tsx +13 -18
  12. package/src/components/diagnose/ApplyDialog.tsx +289 -0
  13. package/src/components/diagnose/AssessmentCard.tsx +1560 -0
  14. package/src/components/diagnose/DiagnoseSurface.test.tsx +9 -9
  15. package/src/components/diagnose/DiagnoseSurface.tsx +5 -1
  16. package/src/components/diagnose/EvidenceCard.tsx +611 -0
  17. package/src/components/diagnose/Home.test.tsx +1 -3
  18. package/src/components/diagnose/Home.tsx +2 -1
  19. package/src/components/diagnose/InvestigationEvidencePane.story.test.tsx +6 -8
  20. package/src/components/diagnose/InvestigationEvidencePane.test.tsx +5 -3
  21. package/src/components/diagnose/InvestigationEvidencePane.tsx +31 -3445
  22. package/src/components/diagnose/InvestigationEvidenceSections.tsx +523 -0
  23. package/src/components/diagnose/InvestigationView.tsx +11 -28
  24. package/src/components/diagnose/StoryExcerpt.tsx +118 -0
  25. package/src/components/diagnose/assessmentCopy.ts +169 -0
  26. package/src/components/diagnose/investigationCase.test.tsx +631 -2
  27. package/src/components/diagnose/investigationCase.ts +132 -24
  28. package/src/components/diagnose/investigationEvidence/adapters/coverage.test.tsx +660 -0
  29. package/src/components/diagnose/investigationEvidence/adapters/{helm.test.ts → helm.test.tsx} +63 -0
  30. package/src/components/diagnose/investigationEvidence/adapters/helm.ts +70 -1
  31. package/src/components/diagnose/investigationEvidence/adapters/listings.ts +134 -0
  32. package/src/components/diagnose/investigationEvidence/adapters/{permissions.test.ts → permissions.test.tsx} +50 -0
  33. package/src/components/diagnose/investigationEvidence/adapters/permissions.ts +18 -0
  34. package/src/components/diagnose/investigationEvidence/adapters/posture.ts +272 -0
  35. package/src/components/diagnose/investigationEvidence/adapters/prometheus.ts +49 -0
  36. package/src/components/diagnose/investigationEvidence/adapters/rankings.ts +147 -0
  37. package/src/components/diagnose/investigationEvidence/adapters/resource.ts +20 -10
  38. package/src/components/diagnose/investigationEvidence/bodies/index.tsx +127 -0
  39. package/src/components/diagnose/investigationEvidence/bodies/inventory.tsx +158 -0
  40. package/src/components/diagnose/investigationEvidence/bodies/metrics.tsx +373 -0
  41. package/src/components/diagnose/investigationEvidence/bodies/network.tsx +274 -0
  42. package/src/components/diagnose/investigationEvidence/bodies/platform.tsx +475 -0
  43. package/src/components/diagnose/investigationEvidence/bodies/posture.tsx +60 -0
  44. package/src/components/diagnose/investigationEvidence/bodies/ranking.tsx +76 -0
  45. package/src/components/diagnose/investigationEvidence/bodies/streams.tsx +212 -0
  46. package/src/components/diagnose/investigationEvidence/bodies/workload.tsx +335 -0
  47. package/src/components/diagnose/investigationEvidence/builder.ts +15 -0
  48. package/src/components/diagnose/investigationEvidence/cardParts.tsx +312 -0
  49. package/src/components/diagnose/investigationEvidence/excerpt.test.ts +105 -0
  50. package/src/components/diagnose/investigationEvidence/excerpt.ts +92 -0
  51. package/src/components/diagnose/investigationEvidence/identity.test.ts +5 -7
  52. package/src/components/diagnose/investigationEvidence/index.ts +1 -1
  53. package/src/components/diagnose/investigationEvidence/navigation.tsx +26 -0
  54. package/src/components/diagnose/investigationEvidence/observations.ts +11 -0
  55. package/src/components/diagnose/investigationEvidence/projection.test.ts +1 -1
  56. package/src/components/diagnose/investigationEvidence/types.ts +97 -2
  57. package/src/components/diagnose/investigationEvidenceKinds.ts +22 -0
  58. package/src/components/diagnose/investigationEvidencePartition.ts +296 -0
  59. package/src/components/diagnose/parts.test.tsx +3 -1
  60. package/src/components/diagnose/parts.tsx +26 -3904
  61. package/src/components/diagnose/target.ts +9 -3
  62. package/src/components/diagnose/toolCallLabel.test.ts +47 -1
  63. package/src/components/diagnose/toolCallLabel.ts +41 -2
  64. package/src/components/helm/HelmReleaseDrawer.tsx +1 -1
  65. package/src/components/home/MCPSetupDialog.tsx +1 -1
  66. package/src/components/resource/PrometheusChartsGrid.tsx +1 -1
  67. package/src/components/resource/WorkloadMetricsHelpDialog.tsx +1 -1
  68. package/src/components/resource/WorkloadMetricsSection.tsx +1 -1
  69. package/src/components/resources/PodFilePreview.tsx +1 -1
  70. package/src/components/ui/CommandPalette.tsx +12 -8
  71. package/src/components/ui/DiagnosticsOverlay.tsx +7 -5
  72. package/src/components/ui/Omnibar.tsx +33 -10
  73. package/src/components/ui/ShortcutHelpOverlay.tsx +8 -5
  74. package/src/components/ui/Disclosure.tsx +0 -47
@@ -0,0 +1,523 @@
1
+ import { useId, useState, type ReactNode } from "react";
2
+ import { clsx } from "clsx";
3
+ import {
4
+ Activity,
5
+ AlertTriangle,
6
+ CircleAlert,
7
+ Info,
8
+ SearchCheck,
9
+ } from "lucide-react";
10
+ import { Collapse, CollapseChevron } from "@skyhook-io/k8s-ui";
11
+ import type { EvidenceCoverageGroup } from "./investigationEvidencePresentation";
12
+ import {
13
+ investigationEvidenceSourceDomId,
14
+ type InvestigationEvidenceGroup,
15
+ type InvestigationRootCauseEvidenceResolution,
16
+ } from "./investigationEvidence";
17
+ import type { InvestigationSourceExcerpt } from "./investigationSourceFocus";
18
+ import type {
19
+ InvestigationCaseItem,
20
+ InvestigationCaseResolution,
21
+ } from "./investigationCase";
22
+ import { useDisclosureReveal } from "./useDisclosureReveal";
23
+ import { SourceButton } from "./investigationEvidence/cardParts";
24
+ import { investigationEvidenceFullRowFlags } from "./investigationEvidencePartition";
25
+ import { EvidenceCard } from "./EvidenceCard";
26
+
27
+ /**
28
+ * Hypotheses the agent dropped, each pointing at the Radar observation whose
29
+ * result contradicted it. Entries whose item could not be placed on an
30
+ * observation are filtered out by the resolver and never rendered here.
31
+ */
32
+ export function RuledOutBlock({
33
+ entries,
34
+ onReveal,
35
+ }: {
36
+ entries: InvestigationCaseResolution["ruledOut"];
37
+ onReveal: (item: InvestigationCaseItem) => void;
38
+ }) {
39
+ const headingId = useId();
40
+ return (
41
+ <section
42
+ aria-labelledby={headingId}
43
+ data-testid="investigation-ruled-out"
44
+ className="rounded-lg border border-theme-border/80 bg-theme-base/20 px-3 py-2.5"
45
+ >
46
+ <h3
47
+ id={headingId}
48
+ className="flex items-center gap-2 text-xs font-semibold text-theme-text-secondary"
49
+ >
50
+ Also checked
51
+ <span className="text-[10px] font-semibold uppercase tracking-wide text-accent-text">
52
+ Agent
53
+ </span>
54
+ </h3>
55
+ <ul className="mt-1.5 space-y-1.5">
56
+ {entries.map((entry, position) => {
57
+ const observation = entry.item.observation!;
58
+ return (
59
+ <li
60
+ key={position}
61
+ className="flex min-w-0 flex-wrap items-baseline gap-x-2 gap-y-0.5 text-xs"
62
+ >
63
+ <span className="min-w-0 text-theme-text-secondary [overflow-wrap:anywhere]">
64
+ {entry.hypothesis}
65
+ </span>
66
+ <button
67
+ type="button"
68
+ onClick={() => onReveal(entry.item)}
69
+ className="shrink-0 rounded text-accent-text hover:underline focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent/50"
70
+ >
71
+ See {observation.title}
72
+ {entry.item.placement === "revision"
73
+ ? " (earlier observation)"
74
+ : ""}
75
+ </button>
76
+ </li>
77
+ );
78
+ })}
79
+ </ul>
80
+ </section>
81
+ );
82
+ }
83
+
84
+ export function AssessmentEvidenceQualification({
85
+ resolution,
86
+ onViewActivity,
87
+ }: {
88
+ resolution: InvestigationRootCauseEvidenceResolution;
89
+ onViewActivity: () => void;
90
+ }) {
91
+ return (
92
+ <div className="flex items-start gap-2 rounded-lg border border-amber-500/30 bg-amber-500/5 px-3 py-2.5">
93
+ <AlertTriangle
94
+ className="mt-0.5 h-4 w-4 shrink-0 text-amber-500"
95
+ aria-hidden
96
+ />
97
+ <div className="min-w-0 flex-1">
98
+ <p className="text-xs font-medium text-theme-text-primary">
99
+ {resolution.status === "invalid"
100
+ ? "Assessment references could not be matched"
101
+ : "Assessment does not cite specific Radar evidence"}
102
+ </p>
103
+ <p className="mt-0.5 text-xs leading-relaxed text-theme-text-tertiary">
104
+ {resolution.status === "invalid"
105
+ ? "Radar could not match the assessment’s references to this investigation. Review Activity before acting."
106
+ : "Review Activity and the Radar evidence below before acting on the agent’s conclusion."}
107
+ </p>
108
+ </div>
109
+ <button
110
+ type="button"
111
+ onClick={onViewActivity}
112
+ className="shrink-0 rounded-md px-2 py-1 text-xs font-medium text-accent-text hover:bg-theme-hover focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent/50"
113
+ >
114
+ View Activity
115
+ </button>
116
+ </div>
117
+ );
118
+ }
119
+
120
+ export function EmptyCollection({
121
+ collecting,
122
+ hasEarlierEvidence,
123
+ onViewActivity,
124
+ }: {
125
+ collecting: boolean;
126
+ hasEarlierEvidence: boolean;
127
+ onViewActivity: () => void;
128
+ }) {
129
+ return (
130
+ <div className="rounded-lg border border-dashed border-theme-border px-4 py-5 text-center">
131
+ {collecting ? (
132
+ <Activity className="mx-auto h-5 w-5 text-accent" aria-hidden />
133
+ ) : (
134
+ <SearchCheck
135
+ className="mx-auto h-5 w-5 text-theme-text-tertiary"
136
+ aria-hidden
137
+ />
138
+ )}
139
+ <p className="mt-2 text-sm font-medium text-theme-text-secondary">
140
+ {collecting
141
+ ? "Evidence will appear here"
142
+ : hasEarlierEvidence
143
+ ? "No current evidence was captured during verification"
144
+ : "No relevant evidence to show yet"}
145
+ </p>
146
+ <p className="mx-auto mt-1 max-w-md text-xs leading-relaxed text-theme-text-tertiary">
147
+ {collecting
148
+ ? "The Activity pane remains the live record while the agent investigates."
149
+ : hasEarlierEvidence
150
+ ? "Earlier observations remain below. This does not prove that those conditions resolved."
151
+ : "Investigation details are still available in Activity. This does not mean the resource is healthy."}
152
+ </p>
153
+ <button
154
+ type="button"
155
+ onClick={onViewActivity}
156
+ className="mt-2 rounded-md px-2 py-1 text-xs font-medium text-accent-text hover:bg-theme-hover focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent/50"
157
+ >
158
+ View Activity
159
+ </button>
160
+ </div>
161
+ );
162
+ }
163
+
164
+ export function CollapsedEvidenceCollection({
165
+ id,
166
+ title,
167
+ description,
168
+ groups,
169
+ animateGroupIds,
170
+ onViewSource,
171
+ open,
172
+ onOpenChange,
173
+ totalCount = groups.length,
174
+ keepWhenEmpty = false,
175
+ children,
176
+ }: {
177
+ id: string;
178
+ title: string;
179
+ description: string;
180
+ groups: InvestigationEvidenceGroup[];
181
+ animateGroupIds: ReadonlySet<string>;
182
+ onViewSource: (
183
+ sourceId: string,
184
+ excerpt?: InvestigationSourceExcerpt,
185
+ ) => void;
186
+ open: boolean;
187
+ onOpenChange: (open: boolean) => void;
188
+ totalCount?: number;
189
+ /** Render at zero: the children carry the empty state and the withheld-results note. */
190
+ keepWhenEmpty?: boolean;
191
+ children?: ReactNode;
192
+ }) {
193
+ const { elementRef, revealAfterToggle } = useDisclosureReveal<HTMLElement>();
194
+ if (totalCount === 0 && !keepWhenEmpty) return null;
195
+ const fullRowFlags = investigationEvidenceFullRowFlags(
196
+ groups.map((group) => group.latest.data.type),
197
+ );
198
+ return (
199
+ <section
200
+ ref={elementRef}
201
+ className="overflow-hidden rounded-lg border border-theme-border/80 bg-theme-base/20"
202
+ >
203
+ <button
204
+ type="button"
205
+ aria-expanded={open}
206
+ aria-controls={id}
207
+ onClick={() => {
208
+ onOpenChange(!open);
209
+ revealAfterToggle(!open);
210
+ }}
211
+ className="flex w-full min-w-0 items-center gap-2 px-3 py-2.5 text-left hover:bg-theme-hover/60 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent/50"
212
+ >
213
+ <span className="min-w-0 flex-1">
214
+ <span className="block text-xs font-semibold text-theme-text-secondary">
215
+ {title}
216
+ </span>
217
+ {description ? (
218
+ <span className="block text-xs text-theme-text-tertiary">
219
+ {description}
220
+ </span>
221
+ ) : null}
222
+ </span>
223
+ <span className="shrink-0 font-mono text-xs text-theme-text-tertiary">
224
+ {totalCount}
225
+ </span>
226
+ <CollapseChevron open={open} className="h-4 w-4" />
227
+ </button>
228
+ <div id={id}>
229
+ <Collapse open={open}>
230
+ <div
231
+ className={clsx(
232
+ "grid items-start gap-2.5 @min-[760px]/evidence:grid-cols-2",
233
+ "border-t border-theme-border/60 p-2.5",
234
+ )}
235
+ >
236
+ {groups.map((group, index) => (
237
+ <EvidenceCard
238
+ key={group.id}
239
+ group={group}
240
+
241
+ animateArrival={animateGroupIds.has(group.id)}
242
+ onViewSource={onViewSource}
243
+ spanFullRow={fullRowFlags[index]}
244
+ prominence="secondary"
245
+ />
246
+ ))}
247
+ {children ? <div className="col-span-full">{children}</div> : null}
248
+ </div>
249
+ </Collapse>
250
+ </div>
251
+ </section>
252
+ );
253
+ }
254
+
255
+ export function CoverageStrip({
256
+ groups,
257
+ visibleGroupIds,
258
+ summary,
259
+ onViewSource,
260
+ open,
261
+ onOpenChange,
262
+ }: {
263
+ groups: EvidenceCoverageGroup[];
264
+ visibleGroupIds: ReadonlySet<string>;
265
+ summary: string;
266
+ onViewSource: (
267
+ sourceId: string,
268
+ excerpt?: InvestigationSourceExcerpt,
269
+ ) => void;
270
+ open: boolean;
271
+ onOpenChange: (open: boolean) => void;
272
+ }) {
273
+ const hasError = groups.some((group) => group.hasError);
274
+ const historyOnly = groups.every((group) => group.historyOnly);
275
+ const quiet = historyOnly;
276
+ const { elementRef, revealAfterToggle } =
277
+ useDisclosureReveal<HTMLDivElement>();
278
+ const regionId = "investigation-evidence-coverage";
279
+ const anchoredSources = new Set<string>();
280
+ return (
281
+ <div
282
+ ref={elementRef}
283
+ className="overflow-hidden rounded-lg border border-theme-border/80 bg-theme-base/20"
284
+ >
285
+ <button
286
+ type="button"
287
+ aria-expanded={open}
288
+ aria-controls={regionId}
289
+ onClick={() => {
290
+ onOpenChange(!open);
291
+ revealAfterToggle(!open);
292
+ }}
293
+ className="flex w-full min-w-0 items-center gap-2 px-3 py-2 text-left hover:bg-theme-hover/50"
294
+ >
295
+ {quiet ? (
296
+ <Info
297
+ className="h-4 w-4 shrink-0 text-theme-text-tertiary"
298
+ aria-hidden
299
+ />
300
+ ) : hasError ? (
301
+ <CircleAlert className="h-4 w-4 shrink-0 text-red-400" aria-hidden />
302
+ ) : (
303
+ <AlertTriangle
304
+ className="h-4 w-4 shrink-0 text-amber-500"
305
+ aria-hidden
306
+ />
307
+ )}
308
+ <span className="min-w-0 flex-1">
309
+ <span
310
+ className={clsx(
311
+ "block text-xs",
312
+ quiet
313
+ ? "font-medium text-theme-text-secondary"
314
+ : "font-semibold text-theme-text-primary",
315
+ )}
316
+ >
317
+ {historyOnly
318
+ ? "Change history is limited"
319
+ : "Evidence coverage is incomplete"}
320
+ </span>
321
+ {!historyOnly && !open ? (
322
+ <span className="line-clamp-2 text-xs text-theme-text-tertiary [overflow-wrap:anywhere]">
323
+ {summary}
324
+ </span>
325
+ ) : null}
326
+ </span>
327
+ <CollapseChevron open={open} className="h-4 w-4" />
328
+ </button>
329
+ <div id={regionId}>
330
+ <Collapse open={open}>
331
+ <div className="divide-y divide-theme-border/60 border-t border-theme-border/60">
332
+ {groups.map((group) => {
333
+ const sourceIds = group.limitations
334
+ .flatMap((item) => item.sources)
335
+ .filter((source) => {
336
+ if (
337
+ (source.primaryGroupId &&
338
+ visibleGroupIds.has(source.primaryGroupId)) ||
339
+ anchoredSources.has(source.id)
340
+ )
341
+ return false;
342
+ anchoredSources.add(source.id);
343
+ return true;
344
+ })
345
+ .map((source) => source.id);
346
+ return (
347
+ <CoverageGroupRow
348
+ key={group.label}
349
+ group={group}
350
+ sourceIds={sourceIds}
351
+ onViewSource={onViewSource}
352
+ />
353
+ );
354
+ })}
355
+ </div>
356
+ </Collapse>
357
+ </div>
358
+ </div>
359
+ );
360
+ }
361
+
362
+ function CoverageGroupRow({
363
+ group,
364
+ sourceIds,
365
+ onViewSource,
366
+ }: {
367
+ group: EvidenceCoverageGroup;
368
+ sourceIds: string[];
369
+ onViewSource: (
370
+ sourceId: string,
371
+ excerpt?: InvestigationSourceExcerpt,
372
+ ) => void;
373
+ }) {
374
+ const [open, setOpen] = useState(false);
375
+ const regionId = useId();
376
+ const { elementRef, revealAfterToggle } =
377
+ useDisclosureReveal<HTMLDivElement>();
378
+ // One limitation whose message is the whole summary would repeat itself as
379
+ // its own detail row; show its source link on the summary row instead.
380
+ const single =
381
+ group.limitations.length === 1 &&
382
+ group.limitations[0].message === group.summary
383
+ ? group.limitations[0]
384
+ : undefined;
385
+ const anchors = sourceIds.map((id) => (
386
+ <span
387
+ key={id}
388
+ id={investigationEvidenceSourceDomId(id)}
389
+ className="sr-only scroll-mt-14"
390
+ aria-hidden
391
+ />
392
+ ));
393
+ if (single) {
394
+ return (
395
+ <div
396
+ ref={elementRef}
397
+ data-evidence-source-container
398
+ tabIndex={-1}
399
+ aria-label={`Evidence limitation for ${group.label}: ${group.summary}`}
400
+ className="flex items-center gap-2 px-3 py-2.5 text-xs outline-none focus:ring-2 focus:ring-accent/40"
401
+ >
402
+ {anchors}
403
+ {group.hasError ? (
404
+ <CircleAlert
405
+ className="h-3.5 w-3.5 shrink-0 text-red-400"
406
+ aria-hidden
407
+ />
408
+ ) : single.kind === "truncated" ? (
409
+ <AlertTriangle
410
+ className="h-3.5 w-3.5 shrink-0 text-amber-500"
411
+ aria-hidden
412
+ />
413
+ ) : (
414
+ <Info
415
+ className="h-3.5 w-3.5 shrink-0 text-theme-text-tertiary"
416
+ aria-hidden
417
+ />
418
+ )}
419
+ <p className="min-w-0 flex-1 leading-relaxed text-theme-text-secondary">
420
+ <span className="font-medium text-theme-text-primary">
421
+ {group.label}:
422
+ </span>{" "}
423
+ {group.summary}
424
+ </p>
425
+ {single.sources.at(-1) ? (
426
+ <SourceButton
427
+ ariaLabel={`View result for ${group.label}`}
428
+ buttonLabel={
429
+ single.sources.length > 1 ? "View latest in Activity" : undefined
430
+ }
431
+ onClick={() => onViewSource(single.sources.at(-1)!.id)}
432
+ />
433
+ ) : null}
434
+ </div>
435
+ );
436
+ }
437
+ return (
438
+ <div
439
+ ref={elementRef}
440
+ data-evidence-source-container
441
+ tabIndex={-1}
442
+ aria-label={`Evidence limitation for ${group.label}: ${group.summary}`}
443
+ className="outline-none focus:ring-2 focus:ring-accent/40"
444
+ >
445
+ {anchors}
446
+ <button
447
+ type="button"
448
+ aria-expanded={open}
449
+ aria-controls={regionId}
450
+ onClick={() => {
451
+ setOpen(!open);
452
+ revealAfterToggle(!open);
453
+ }}
454
+ className="flex w-full items-center gap-2 px-3 py-2.5 text-left text-xs hover:bg-theme-hover/50"
455
+ >
456
+ {group.hasError ? (
457
+ <CircleAlert
458
+ className="h-3.5 w-3.5 shrink-0 text-red-400"
459
+ aria-hidden
460
+ />
461
+ ) : (
462
+ <Info
463
+ className="h-3.5 w-3.5 shrink-0 text-theme-text-tertiary"
464
+ aria-hidden
465
+ />
466
+ )}
467
+ <span className="min-w-0 flex-1 leading-relaxed text-theme-text-secondary">
468
+ <span className="font-medium text-theme-text-primary">
469
+ {group.label}:{" "}
470
+ </span>
471
+ {group.summary}
472
+ </span>
473
+ <CollapseChevron open={open} className="h-3.5 w-3.5 shrink-0" />
474
+ </button>
475
+ <div id={regionId}>
476
+ <Collapse open={open}>
477
+ <ul className="space-y-2 border-t border-theme-border/60 px-3 py-2.5">
478
+ {group.limitations.map((limitation, index) => (
479
+ <li
480
+ key={index}
481
+ className="flex min-w-0 items-start gap-2 text-xs"
482
+ >
483
+ {limitation.kind === "error" ? (
484
+ <CircleAlert
485
+ className="mt-0.5 h-3.5 w-3.5 shrink-0 text-red-400"
486
+ aria-hidden
487
+ />
488
+ ) : limitation.kind === "truncated" ? (
489
+ <AlertTriangle
490
+ className="mt-0.5 h-3.5 w-3.5 shrink-0 text-amber-500"
491
+ aria-hidden
492
+ />
493
+ ) : (
494
+ <Info
495
+ className="mt-0.5 h-3.5 w-3.5 shrink-0 text-theme-text-tertiary"
496
+ aria-hidden
497
+ />
498
+ )}
499
+ <p className="min-w-0 flex-1 leading-relaxed text-theme-text-secondary">
500
+ <span className="font-medium text-theme-text-primary">
501
+ {limitation.source}:
502
+ </span>{" "}
503
+ {limitation.message}
504
+ </p>
505
+ {limitation.sources.at(-1) ? (
506
+ <SourceButton
507
+ ariaLabel={`View result for ${limitation.source}`}
508
+ buttonLabel={
509
+ limitation.sources.length > 1
510
+ ? "View latest in Activity"
511
+ : undefined
512
+ }
513
+ onClick={() => onViewSource(limitation.sources.at(-1)!.id)}
514
+ />
515
+ ) : null}
516
+ </li>
517
+ ))}
518
+ </ul>
519
+ </Collapse>
520
+ </div>
521
+ </div>
522
+ );
523
+ }
@@ -43,7 +43,7 @@ import {
43
43
  canInvestigateFurther,
44
44
  } from "./investigationState";
45
45
  import type { AssessmentExplanation } from "./parts";
46
- import { describeToolCall } from "./toolCallLabel";
46
+ import { investigationRunningLabel } from "./toolCallLabel";
47
47
  import { AGENT_ROLE_LABELS } from "./AgentCase";
48
48
  import {
49
49
  Fragment,
@@ -99,9 +99,9 @@ import {
99
99
  import { resolveInvestigationCase } from "./investigationCase";
100
100
  import {
101
101
  InvestigationEvidencePane,
102
- partitionInvestigationEvidence,
103
102
  type InvestigationTimelineScope,
104
103
  } from "./InvestigationEvidencePane";
104
+ import { partitionInvestigationEvidence } from "./investigationEvidencePartition";
105
105
  import type { DiagnosisResourceRef } from "./diagnoseEvidenceTypes";
106
106
  import { formatInvestigationTarget } from "./target";
107
107
  import { parseContextName } from "../../utils/context-name";
@@ -1458,37 +1458,20 @@ export function InvestigationView({
1458
1458
  toolSeenRef.current = toolSignature;
1459
1459
  toolChangedAtRef.current = Date.now();
1460
1460
  }, [toolSignature]);
1461
- const investigatingLabel = (() => {
1462
- const items = lastTurn?.timeline ?? [];
1463
- const reads = items.filter((item) => item.kind === "tool").length;
1461
+ const investigatingLabel = investigationRunningLabel({
1462
+ lastTool: lastTool && lastTool.kind === "tool" ? lastTool : undefined,
1463
+ reads: (lastTurn?.timeline ?? []).filter((item) => item.kind === "tool")
1464
+ .length,
1464
1465
  // The render that first shows a changed tool is not quiet yet; the effect
1465
1466
  // that stamps the change runs after it.
1466
- const quietFor =
1467
+ quietFor:
1467
1468
  toolSeenRef.current === toolSignature
1468
1469
  ? Date.now() - toolChangedAtRef.current
1469
- : 0;
1470
- let current = "Investigating";
1471
- if (lastTool && lastTool.kind === "tool" && lastTool.tool) {
1472
- current =
1473
- lastTool.status === "running" || quietFor < 8000
1474
- ? describeToolCall(lastTool.tool, lastTool.summary)
1475
- : "Thinking";
1476
- }
1477
- const elapsed = runningSinceRef.current
1470
+ : 0,
1471
+ elapsedSeconds: runningSinceRef.current
1478
1472
  ? Math.round((Date.now() - runningSinceRef.current) / 1000)
1479
- : 0;
1480
- // The tool text changes width as it changes; the counters sit apart on
1481
- // the right so they never move with it.
1482
- return {
1483
- current: `${current}…`,
1484
- meta: [
1485
- reads > 0 ? `${reads} ${reads === 1 ? "read" : "reads"}` : undefined,
1486
- elapsed >= 2 ? `${elapsed} s` : undefined,
1487
- ]
1488
- .filter(Boolean)
1489
- .join(" · "),
1490
- };
1491
- })();
1473
+ : 0,
1474
+ });
1492
1475
  const showSplitWorkspace = maximized;
1493
1476
  const splitGridClass = showSplitWorkspace
1494
1477
  ? "@min-[1000px]/investigation:grid-cols-[minmax(320px,min(30%,520px))_minmax(0,1fr)]"