@tonyclaw/agent-inspector 3.0.36 → 3.0.37

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 (41) hide show
  1. package/.output/nitro.json +1 -1
  2. package/.output/public/assets/{CompareDrawer-BF8Yk4Nq.js → CompareDrawer-BvF04FAX.js} +1 -1
  3. package/.output/public/assets/{InspectorPet-D01knLHJ.js → InspectorPet-CSCn4jWU.js} +1 -1
  4. package/.output/public/assets/ProxyViewerContainer-9mjL1YlZ.js +126 -0
  5. package/.output/public/assets/{ReplayDialog-NRgCp09o.js → ReplayDialog-DWJkKTDB.js} +1 -1
  6. package/.output/public/assets/{RequestAnatomy-BPiUptQ-.js → RequestAnatomy-CDFI55pJ.js} +1 -1
  7. package/.output/public/assets/{ResponseView-C6oCKVyP.js → ResponseView-tlIj9vRr.js} +1 -1
  8. package/.output/public/assets/{StreamingChunkSequence-B90BNPvD.js → StreamingChunkSequence-C7Ntivbf.js} +1 -1
  9. package/.output/public/assets/{_sessionId-c3quqe4s.js → _sessionId-BbrBvkvn.js} +1 -1
  10. package/.output/public/assets/{_sessionId-B7pqqX6S.js → _sessionId-DUL-l56X.js} +1 -1
  11. package/.output/public/assets/{index-D4Atppe6.js → index-By0CyDmk.js} +1 -1
  12. package/.output/public/assets/{index-D444FJGM.js → index-CEdulUJ3.js} +3 -3
  13. package/.output/public/assets/index-DCj1X8Qt.css +1 -0
  14. package/.output/public/assets/{index-CmGZ17BL.js → index-DF6bouqm.js} +1 -1
  15. package/.output/public/assets/{index-CjODQxYY.js → index-e-fWO2Du.js} +1 -1
  16. package/.output/public/assets/{json-viewer-CuYTDXFj.js → json-viewer-0fOrL_Cz.js} +1 -1
  17. package/.output/public/assets/{jszip.min-C-avw0w1.js → jszip.min-2Bsoez0Q.js} +1 -1
  18. package/.output/server/_libs/lucide-react.mjs +65 -65
  19. package/.output/server/_libs/radix-ui__react-select.mjs +7 -7
  20. package/.output/server/{_sessionId-BsDZ3m3b.mjs → _sessionId-D-GcecKN.mjs} +2 -2
  21. package/.output/server/{_sessionId-BTazjVlZ.mjs → _sessionId-DzY1bB_L.mjs} +1 -1
  22. package/.output/server/_ssr/{CompareDrawer-BMo93_DO.mjs → CompareDrawer-BXm8BitS.mjs} +3 -3
  23. package/.output/server/_ssr/{InspectorPet-DApzyYro.mjs → InspectorPet-CZUOr9sO.mjs} +1 -1
  24. package/.output/server/_ssr/{ProxyViewerContainer-CiXXtVXH.mjs → ProxyViewerContainer-BAQY5x_x.mjs} +120 -281
  25. package/.output/server/_ssr/{ReplayDialog-B8bP1qUr.mjs → ReplayDialog-BOODYnLb.mjs} +4 -4
  26. package/.output/server/_ssr/{RequestAnatomy-DkVynjyh.mjs → RequestAnatomy-Bd2OBiJP.mjs} +3 -3
  27. package/.output/server/_ssr/{ResponseView-BAQa0WUP.mjs → ResponseView-ZSjpdHLG.mjs} +3 -3
  28. package/.output/server/_ssr/{StreamingChunkSequence-B4_K4_VI.mjs → StreamingChunkSequence-BT-yQGLL.mjs} +3 -3
  29. package/.output/server/_ssr/{index-DhjNsApq.mjs → index-CSJlUfwz.mjs} +1 -1
  30. package/.output/server/_ssr/{index-BiwG5Sws.mjs → index-CXouA5fR.mjs} +2 -2
  31. package/.output/server/_ssr/index.mjs +2 -2
  32. package/.output/server/_ssr/{json-viewer-Dlpq5S-r.mjs → json-viewer-DF2oLr4x.mjs} +3 -3
  33. package/.output/server/_ssr/{router-6s7p8rsX.mjs → router-75eycvXZ.mjs} +6 -6
  34. package/.output/server/{_tanstack-start-manifest_v-ClIF-cfh.mjs → _tanstack-start-manifest_v-DjCobDeN.mjs} +1 -1
  35. package/.output/server/index.mjs +87 -87
  36. package/package.json +7 -1
  37. package/src/components/ProxyViewer.tsx +96 -328
  38. package/src/components/pi-agent/PiAgentPanel.tsx +23 -14
  39. package/src/components/pi-agent/piAgentChatLogic.ts +10 -0
  40. package/.output/public/assets/ProxyViewerContainer-CArcMNg_.js +0 -126
  41. package/.output/public/assets/index-BQqVblQv.css +0 -1
@@ -1,7 +1,6 @@
1
1
  import {
2
2
  type ChangeEvent,
3
3
  type JSX,
4
- type KeyboardEvent,
5
4
  type ReactNode,
6
5
  useCallback,
7
6
  useEffect,
@@ -32,7 +31,6 @@ import {
32
31
  PanelRightClose,
33
32
  PanelRightOpen,
34
33
  RefreshCw,
35
- Search,
36
34
  Siren,
37
35
  Moon,
38
36
  Sun,
@@ -79,7 +77,7 @@ import { AlertsDialog } from "./alerts/AlertsDialog";
79
77
  import { GroupsDialog } from "./groups/GroupsDialog";
80
78
  import { SettingsDialog } from "./providers/SettingsDialog";
81
79
  import { AgentLabDialog } from "./ecosystem/AgentLabDialog";
82
- import { PiAgentPanel } from "./pi-agent/PiAgentPanel";
80
+ import { PiAgentPanel, type PiAgentSearchResult } from "./pi-agent/PiAgentPanel";
83
81
  import { LazyFeatureBoundary } from "./errors/SafeErrorBoundary";
84
82
  import { computeCacheTrends } from "./proxy-viewer/cacheTrend";
85
83
  import { LazyCompareDrawer, LazyInspectorPet } from "./proxy-viewer/lazy";
@@ -116,228 +114,6 @@ function truncateSessionId(id: string): string {
116
114
  return `${id.slice(0, 12)}...${id.slice(-12)}`;
117
115
  }
118
116
 
119
- type SessionSearchSuggestion = {
120
- id: string;
121
- label: string;
122
- detail: string;
123
- kind: "session" | "model" | "all";
124
- value: string;
125
- };
126
-
127
- function normalizeSearchQuery(value: string): string {
128
- return value.trim().toLowerCase();
129
- }
130
-
131
- function includesSearchText(value: string | null | undefined, query: string): boolean {
132
- return value !== null && value !== undefined && value.toLowerCase().includes(query);
133
- }
134
-
135
- function buildSessionSuggestionDetail(sessionId: string, logs: readonly CapturedLog[]): string {
136
- const scopedLogs = logs.filter((log) => log.sessionId === sessionId);
137
- if (scopedLogs.length === 0) return "Stored session";
138
- const models = new Set<string>();
139
- const providers = new Set<string>();
140
- for (const log of scopedLogs) {
141
- if (log.model !== null && log.model !== "") models.add(log.model);
142
- if (
143
- log.providerName !== null &&
144
- log.providerName !== undefined &&
145
- log.providerName.trim() !== ""
146
- ) {
147
- providers.add(log.providerName);
148
- }
149
- }
150
- const modelLabel = models.size === 0 ? "unknown model" : [...models].slice(0, 2).join(", ");
151
- const providerLabel =
152
- providers.size === 0 ? "unknown provider" : [...providers].slice(0, 2).join(", ");
153
- return `${String(scopedLogs.length)} req · ${providerLabel} · ${modelLabel}`;
154
- }
155
-
156
- function sessionMatchesSearchQuery(
157
- sessionId: string,
158
- logs: readonly CapturedLog[],
159
- query: string,
160
- ): boolean {
161
- if (includesSearchText(sessionId, query)) return true;
162
- return logs.some(
163
- (log) =>
164
- log.sessionId === sessionId &&
165
- (includesSearchText(log.model, query) ||
166
- includesSearchText(log.providerName, query) ||
167
- includesSearchText(log.path, query)),
168
- );
169
- }
170
-
171
- function buildSessionSearchSuggestions({
172
- query,
173
- sessions,
174
- models,
175
- logs,
176
- }: {
177
- query: string;
178
- sessions: readonly string[];
179
- models: readonly string[];
180
- logs: readonly CapturedLog[];
181
- }): SessionSearchSuggestion[] {
182
- const normalizedQuery = normalizeSearchQuery(query);
183
- const suggestions: SessionSearchSuggestion[] = [];
184
-
185
- if (normalizedQuery === "") {
186
- suggestions.push({
187
- id: "all",
188
- label: "All sessions",
189
- detail: `${String(logs.length)} visible requests`,
190
- kind: "all",
191
- value: "__all__",
192
- });
193
- for (const session of sessions.slice(0, 5)) {
194
- suggestions.push({
195
- id: `session-${session}`,
196
- label: truncateSessionId(session),
197
- detail: buildSessionSuggestionDetail(session, logs),
198
- kind: "session",
199
- value: session,
200
- });
201
- }
202
- return suggestions;
203
- }
204
-
205
- const matchingSessions = sessions.filter((session) =>
206
- sessionMatchesSearchQuery(session, logs, normalizedQuery),
207
- );
208
- for (const session of matchingSessions.slice(0, 5)) {
209
- suggestions.push({
210
- id: `session-${session}`,
211
- label: truncateSessionId(session),
212
- detail: buildSessionSuggestionDetail(session, logs),
213
- kind: "session",
214
- value: session,
215
- });
216
- }
217
-
218
- const matchingModels = models.filter((model) => includesSearchText(model, normalizedQuery));
219
- for (const model of matchingModels.slice(0, 3)) {
220
- suggestions.push({
221
- id: `model-${model}`,
222
- label: model,
223
- detail: "Model filter",
224
- kind: "model",
225
- value: model,
226
- });
227
- }
228
-
229
- return suggestions.slice(0, 7);
230
- }
231
-
232
- function buildAssistantQuestionFromSearch(query: string, selectedSession: string): string {
233
- const trimmed = query.trim();
234
- const scope =
235
- selectedSession === "__all__" ? "all visible Inspector sessions" : `session ${selectedSession}`;
236
- if (trimmed === "") {
237
- return `Analyze ${scope}. What failed, slowed down, or deserves attention?`;
238
- }
239
- return `Search ${scope} for "${trimmed}" and explain the relevant failures, latency, token usage, provider behavior, and evidence.`;
240
- }
241
-
242
- function SessionSearchBox({
243
- query,
244
- suggestions,
245
- selectedSession,
246
- selectedModel,
247
- onQueryChange,
248
- onApplySuggestion,
249
- onClear,
250
- onAskAssistant,
251
- }: {
252
- query: string;
253
- suggestions: readonly SessionSearchSuggestion[];
254
- selectedSession: string;
255
- selectedModel: string;
256
- onQueryChange: (value: string) => void;
257
- onApplySuggestion: (suggestion: SessionSearchSuggestion) => void;
258
- onClear: () => void;
259
- onAskAssistant: () => void;
260
- }): JSX.Element {
261
- const handleKeyDown = (event: KeyboardEvent<HTMLInputElement>): void => {
262
- if (event.key !== "Enter") return;
263
- const firstSuggestion = suggestions[0];
264
- if (firstSuggestion === undefined) return;
265
- event.preventDefault();
266
- onApplySuggestion(firstSuggestion);
267
- };
268
-
269
- return (
270
- <div className="rounded-xl bg-black/20 p-2 shadow-[inset_0_0_0_1px_rgba(255,255,255,0.055)]">
271
- <div className="relative">
272
- <Search className="pointer-events-none absolute left-3 top-1/2 size-3.5 -translate-y-1/2 text-cyan-100/70" />
273
- <input
274
- type="search"
275
- value={query}
276
- onChange={(event) => onQueryChange(event.currentTarget.value)}
277
- onKeyDown={handleKeyDown}
278
- placeholder="Search sessions, models, providers..."
279
- className="h-10 w-full rounded-full bg-black/30 px-9 pr-9 text-xs text-foreground outline-none shadow-[inset_0_0_0_1px_rgba(103,232,249,0.13)] transition-colors placeholder:text-muted-foreground hover:bg-black/35 focus-visible:ring-1 focus-visible:ring-cyan-200/70"
280
- aria-label="Search sessions and models"
281
- />
282
- {(query !== "" || selectedSession !== "__all__" || selectedModel !== "__all__") && (
283
- <button
284
- type="button"
285
- onClick={onClear}
286
- className="absolute right-2 top-1/2 inline-flex size-6 -translate-y-1/2 items-center justify-center rounded-full text-muted-foreground transition-colors hover:bg-white/[0.07] hover:text-foreground focus-visible:ring-1 focus-visible:ring-ring focus-visible:outline-none"
287
- aria-label="Clear session search"
288
- title="Clear search"
289
- >
290
- <X className="size-3.5" />
291
- </button>
292
- )}
293
- </div>
294
-
295
- <div className="mt-2 flex flex-wrap gap-1.5">
296
- <button
297
- type="button"
298
- onClick={onAskAssistant}
299
- className="inline-flex h-7 items-center rounded-full bg-cyan-400/10 px-2.5 text-[11px] font-medium text-cyan-100 shadow-[inset_0_0_0_1px_rgba(103,232,249,0.14)] transition-colors hover:bg-cyan-400/16"
300
- >
301
- Ask Assistant
302
- </button>
303
- {selectedSession !== "__all__" && (
304
- <span className="inline-flex h-7 max-w-full items-center rounded-full bg-white/[0.045] px-2.5 font-mono text-[10px] text-muted-foreground">
305
- session:{truncateSessionId(selectedSession)}
306
- </span>
307
- )}
308
- {selectedModel !== "__all__" && (
309
- <span className="inline-flex h-7 max-w-full items-center rounded-full bg-white/[0.045] px-2.5 text-[10px] text-muted-foreground">
310
- model:{selectedModel}
311
- </span>
312
- )}
313
- </div>
314
-
315
- <div className="mt-2 space-y-1">
316
- {suggestions.map((suggestion) => (
317
- <button
318
- key={suggestion.id}
319
- type="button"
320
- onClick={() => onApplySuggestion(suggestion)}
321
- className="grid w-full grid-cols-[minmax(0,1fr)_auto] items-center gap-2 rounded-lg px-2 py-1.5 text-left transition-colors hover:bg-white/[0.045] focus-visible:ring-1 focus-visible:ring-ring focus-visible:outline-none"
322
- >
323
- <span className="min-w-0">
324
- <span className="block truncate text-[11px] font-medium text-foreground">
325
- {suggestion.label}
326
- </span>
327
- <span className="block truncate text-[10px] text-muted-foreground">
328
- {suggestion.detail}
329
- </span>
330
- </span>
331
- <span className="rounded-full bg-white/[0.045] px-2 py-0.5 text-[9px] uppercase tracking-[0.12em] text-muted-foreground">
332
- {suggestion.kind}
333
- </span>
334
- </button>
335
- ))}
336
- </div>
337
- </div>
338
- );
339
- }
340
-
341
117
  function buildDisplayNumberByLogId(groups: readonly ConversationGroupData[]): Map<number, number> {
342
118
  const result = new Map<number, number>();
343
119
  for (const group of groups) {
@@ -1514,11 +1290,9 @@ export type ProxyViewerProps = {
1514
1290
  export function ProxyViewer({
1515
1291
  logs,
1516
1292
  sessions,
1517
- models,
1518
1293
  selectedSession,
1519
1294
  selectedModel,
1520
1295
  onSessionChange,
1521
- onModelChange,
1522
1296
  onClearAll,
1523
1297
  onDeleteCurrentView,
1524
1298
  onReloadSession,
@@ -1556,9 +1330,8 @@ export function ProxyViewer({
1556
1330
  const [detailsPanelStorageLoaded, setDetailsPanelStorageLoaded] = useState(false);
1557
1331
  const [petVisible, setPetVisible] = useState(true);
1558
1332
  const [petVisibilityStorageLoaded, setPetVisibilityStorageLoaded] = useState(false);
1559
- const [sessionSearchQuery, setSessionSearchQuery] = useState("");
1560
- const [assistantSuggestedQuestion, setAssistantSuggestedQuestion] = useState<string | undefined>(
1561
- undefined,
1333
+ const [assistantSearchResult, setAssistantSearchResult] = useState<PiAgentSearchResult | null>(
1334
+ null,
1562
1335
  );
1563
1336
  const [comparePair, setComparePair] = useState<[CapturedLog, CapturedLog] | null>(null);
1564
1337
  const logListRef = useRef<HTMLDivElement>(null);
@@ -1710,6 +1483,7 @@ export function ProxyViewer({
1710
1483
  useEffect(() => {
1711
1484
  cancelTransientTasks();
1712
1485
  setComparePair(null);
1486
+ setAssistantSearchResult(null);
1713
1487
  }, [cancelTransientTasks, selectedSession, selectedModel]);
1714
1488
 
1715
1489
  const handleClearCurrentView = useCallback(() => {
@@ -1770,7 +1544,18 @@ export function ProxyViewer({
1770
1544
  setPetVisible((value) => !value);
1771
1545
  }, []);
1772
1546
 
1773
- const groups = useMemo(() => groupLogsByConversation(logs), [logs]);
1547
+ const assistantResultLogIds = useMemo(
1548
+ () => (assistantSearchResult === null ? null : new Set<number>(assistantSearchResult.logIds)),
1549
+ [assistantSearchResult],
1550
+ );
1551
+ const displayedLogs = useMemo(
1552
+ () =>
1553
+ assistantResultLogIds === null
1554
+ ? logs
1555
+ : logs.filter((log) => assistantResultLogIds.has(log.id)),
1556
+ [assistantResultLogIds, logs],
1557
+ );
1558
+ const groups = useMemo(() => groupLogsByConversation(displayedLogs), [displayedLogs]);
1774
1559
  const displayNumberByLogId = useMemo(() => buildDisplayNumberByLogId(groups), [groups]);
1775
1560
  const sessionContextScope = useMemo(
1776
1561
  () => resolveSessionContextScope({ pinnedSessionId, selectedSession, sessions, logs }),
@@ -1799,16 +1584,6 @@ export function ProxyViewer({
1799
1584
  ? "Deletes every stored log, streaming chunk, and session archive entry across all sessions."
1800
1585
  : "Deletes every stored log, streaming chunk, and session archive entry for this session, including history that is not currently loaded.";
1801
1586
  const reloadSessionLabel = logs.length === 0 ? "Load" : "Reload";
1802
- const sessionSearchSuggestions = useMemo(
1803
- () =>
1804
- buildSessionSearchSuggestions({
1805
- query: sessionSearchQuery,
1806
- sessions,
1807
- models,
1808
- logs,
1809
- }),
1810
- [logs, models, sessionSearchQuery, sessions],
1811
- );
1812
1587
  const activeScopeLabel =
1813
1588
  hasSessionContext && sessionContextScope !== null
1814
1589
  ? truncateSessionId(sessionContextScope.label)
@@ -1820,13 +1595,13 @@ export function ProxyViewer({
1820
1595
  const slateStats = useMemo(
1821
1596
  () =>
1822
1597
  buildSessionSlateStats({
1823
- logs,
1598
+ logs: displayedLogs,
1824
1599
  conversationGroups: groups,
1825
1600
  isLoading,
1826
1601
  timeDisplayFormat,
1827
1602
  slowResponseThresholdSeconds,
1828
1603
  }),
1829
- [groups, isLoading, logs, slowResponseThresholdSeconds, timeDisplayFormat],
1604
+ [displayedLogs, groups, isLoading, slowResponseThresholdSeconds, timeDisplayFormat],
1830
1605
  );
1831
1606
  const handleCompareWithPrevious = useCallback(
1832
1607
  (log: CapturedLog) => {
@@ -1835,36 +1610,6 @@ export function ProxyViewer({
1835
1610
  },
1836
1611
  [comparisonPredecessors],
1837
1612
  );
1838
- const handleApplySessionSearchSuggestion = useCallback(
1839
- (suggestion: SessionSearchSuggestion) => {
1840
- switch (suggestion.kind) {
1841
- case "all":
1842
- onSessionChange("__all__");
1843
- onModelChange("__all__");
1844
- setSessionSearchQuery("");
1845
- break;
1846
- case "session":
1847
- onSessionChange(suggestion.value);
1848
- setSessionSearchQuery(suggestion.value);
1849
- break;
1850
- case "model":
1851
- onModelChange(suggestion.value);
1852
- setSessionSearchQuery(suggestion.value);
1853
- break;
1854
- }
1855
- },
1856
- [onModelChange, onSessionChange],
1857
- );
1858
- const handleClearSessionSearch = useCallback(() => {
1859
- setSessionSearchQuery("");
1860
- onSessionChange("__all__");
1861
- onModelChange("__all__");
1862
- }, [onModelChange, onSessionChange]);
1863
- const handleAskAssistantFromSearch = useCallback(() => {
1864
- setAssistantSuggestedQuestion(
1865
- buildAssistantQuestionFromSearch(sessionSearchQuery, selectedSession),
1866
- );
1867
- }, [selectedSession, sessionSearchQuery]);
1868
1613
  const labSessionId =
1869
1614
  pinnedSessionId !== undefined
1870
1615
  ? pinnedSessionId
@@ -1916,22 +1661,29 @@ export function ProxyViewer({
1916
1661
  {!detailsPanelCollapsed && (
1917
1662
  <aside className="bg-background/95 order-1 border-b border-white/[0.045] lg:order-2 lg:h-full lg:overflow-y-auto lg:border-b-0 lg:border-l lg:border-white/[0.055] inspector-scrollbar">
1918
1663
  <div className="space-y-4 p-4">
1919
- <SidebarPanel label="Tools">
1920
- <div className="flex flex-wrap items-center gap-1.5">
1921
- <ThemeModeToggle />
1922
- <UiScaleControl
1923
- preference={uiScalePreference}
1924
- resolvedScale={resolvedUiScale}
1925
- onChange={setUiScalePreference}
1926
- />
1927
- <GroupsDialog />
1928
- <AgentLabDialog currentSessionId={labSessionId} logCount={logs.length} />
1929
- </div>
1930
- </SidebarPanel>
1664
+ <PiAgentPanel
1665
+ currentSessionId={labSessionId}
1666
+ logCount={logs.length}
1667
+ onResultChange={setAssistantSearchResult}
1668
+ />
1931
1669
 
1932
- <SidebarPanel label="Session">
1670
+ <SidebarPanel label="Current scope">
1671
+ <div className="mb-2 rounded-md bg-black/20 px-3 py-2 shadow-[inset_0_1px_0_rgba(255,255,255,0.035)]">
1672
+ <div
1673
+ className="truncate font-mono text-xs font-semibold text-cyan-100"
1674
+ title={activeScopeLabel}
1675
+ >
1676
+ {activeScopeLabel}
1677
+ </div>
1678
+ <div className="mt-1 text-[11px] text-muted-foreground">
1679
+ {isPinnedSessionPage ? "Pinned session" : "Live Inspector data"}
1680
+ </div>
1681
+ </div>
1933
1682
  <div className="grid grid-cols-2 gap-2">
1934
- <SidebarMetric label="Requests" value={logs.length} />
1683
+ <SidebarMetric
1684
+ label={assistantSearchResult === null ? "Requests" : "Results"}
1685
+ value={displayedLogs.length}
1686
+ />
1935
1687
  <SidebarMetric label="Sessions" value={sessions.length} />
1936
1688
  <SidebarMetric
1937
1689
  label="Input"
@@ -1949,26 +1701,19 @@ export function ProxyViewer({
1949
1701
  )}
1950
1702
  </SidebarPanel>
1951
1703
 
1952
- <SidebarPanel label="Workspace">
1953
- <div className="bg-black/20 rounded-md px-3 py-2 shadow-[inset_0_1px_0_rgba(255,255,255,0.035)]">
1954
- <div
1955
- className="truncate font-mono text-xs font-semibold text-cyan-100"
1956
- title={activeScopeLabel}
1957
- >
1958
- {activeScopeLabel}
1959
- </div>
1960
- <div className="mt-1 text-[11px] text-muted-foreground">
1961
- {isPinnedSessionPage ? "Session workspace" : "Live workspace"}
1962
- </div>
1704
+ <SidebarPanel label="Tools">
1705
+ <div className="flex flex-wrap items-center gap-1.5">
1706
+ <ThemeModeToggle />
1707
+ <UiScaleControl
1708
+ preference={uiScalePreference}
1709
+ resolvedScale={resolvedUiScale}
1710
+ onChange={setUiScalePreference}
1711
+ />
1712
+ <GroupsDialog />
1713
+ <AgentLabDialog currentSessionId={labSessionId} logCount={logs.length} />
1963
1714
  </div>
1964
1715
  </SidebarPanel>
1965
1716
 
1966
- <PiAgentPanel
1967
- currentSessionId={labSessionId}
1968
- logCount={logs.length}
1969
- suggestedQuestion={assistantSuggestedQuestion}
1970
- />
1971
-
1972
1717
  {hasSessionContext && sessionContextScope !== null && (
1973
1718
  <SessionContextBar
1974
1719
  label={sessionContextScope.label}
@@ -1986,25 +1731,6 @@ export function ProxyViewer({
1986
1731
  sessionMemberships.length > 0 && (
1987
1732
  <SessionMembershipPanel memberships={sessionMemberships} />
1988
1733
  )}
1989
-
1990
- <SidebarPanel label="Filters">
1991
- {hideSessionFilter ? (
1992
- <p className="text-[11px] leading-5 text-muted-foreground">
1993
- This session is pinned by the current URL.
1994
- </p>
1995
- ) : (
1996
- <SessionSearchBox
1997
- query={sessionSearchQuery}
1998
- suggestions={sessionSearchSuggestions}
1999
- selectedSession={selectedSession}
2000
- selectedModel={selectedModel}
2001
- onQueryChange={setSessionSearchQuery}
2002
- onApplySuggestion={handleApplySessionSearchSuggestion}
2003
- onClear={handleClearSessionSearch}
2004
- onAskAssistant={handleAskAssistantFromSearch}
2005
- />
2006
- )}
2007
- </SidebarPanel>
2008
1734
  </div>
2009
1735
  </aside>
2010
1736
  )}
@@ -2151,7 +1877,30 @@ export function ProxyViewer({
2151
1877
  )}
2152
1878
  </div>
2153
1879
  )}
2154
- <LogPaginationBar logs={logs} pagination={pagination} embedded={true} />
1880
+ {assistantSearchResult !== null && (
1881
+ <div className="mt-2 flex items-center gap-3 rounded-md bg-cyan-400/[0.07] px-3 py-2 shadow-[inset_2px_0_0_rgba(103,232,249,0.35),inset_0_0_0_1px_rgba(103,232,249,0.08)]">
1882
+ <div className="min-w-0 flex-1">
1883
+ <div className="truncate text-xs font-medium text-cyan-50">
1884
+ {assistantSearchResult.question}
1885
+ </div>
1886
+ <div className="mt-0.5 text-[10px] text-muted-foreground">
1887
+ Showing {String(displayedLogs.length)} loaded logs from{" "}
1888
+ {String(assistantSearchResult.logIds.length)} linked results
1889
+ </div>
1890
+ </div>
1891
+ <button
1892
+ type="button"
1893
+ onClick={() => setAssistantSearchResult(null)}
1894
+ className="inline-flex h-7 shrink-0 items-center gap-1 rounded-md bg-black/20 px-2 text-[11px] text-muted-foreground transition-colors hover:bg-black/30 hover:text-foreground focus-visible:ring-1 focus-visible:ring-ring focus-visible:outline-none"
1895
+ >
1896
+ <X className="size-3" />
1897
+ All logs
1898
+ </button>
1899
+ </div>
1900
+ )}
1901
+ {assistantSearchResult === null && (
1902
+ <LogPaginationBar logs={logs} pagination={pagination} embedded={true} />
1903
+ )}
2155
1904
  {onNotify === undefined && exportError !== null && (
2156
1905
  <div className="mt-2 rounded-md border border-destructive/30 bg-destructive/10 px-3 py-2 text-xs text-destructive">
2157
1906
  {exportError}
@@ -2182,7 +1931,24 @@ export function ProxyViewer({
2182
1931
 
2183
1932
  <div className="px-4 py-4 lg:px-5">
2184
1933
  <SessionOpeningSlate stats={slateStats} />
2185
- {logs.length === 0 ? (
1934
+ {assistantSearchResult !== null && displayedLogs.length === 0 ? (
1935
+ <div className="py-16 text-center text-muted-foreground">
1936
+ <div className="mx-auto max-w-xl rounded-[8px] bg-white/[0.035] px-5 py-8 shadow-[inset_0_1px_0_rgba(255,255,255,0.045),0_18px_52px_rgba(0,0,0,0.18)]">
1937
+ <p className="text-sm font-medium text-foreground">No linked logs are loaded</p>
1938
+ <p className="mt-2 text-xs leading-5 text-muted-foreground">
1939
+ Assistant found evidence, but none of its linked log IDs are in the current
1940
+ loaded window. Open an evidence card in Assistant or restore the full log view.
1941
+ </p>
1942
+ <button
1943
+ type="button"
1944
+ onClick={() => setAssistantSearchResult(null)}
1945
+ className="mt-4 inline-flex h-8 items-center rounded-md bg-cyan-400/10 px-3 text-xs text-cyan-100 transition-colors hover:bg-cyan-400/16"
1946
+ >
1947
+ Show all logs
1948
+ </button>
1949
+ </div>
1950
+ </div>
1951
+ ) : logs.length === 0 ? (
2186
1952
  selectedSession !== "__all__" ? (
2187
1953
  <div className="py-16 text-center text-muted-foreground">
2188
1954
  <div className="bg-white/[0.035] shadow-[inset_0_1px_0_rgba(255,255,255,0.045),0_18px_52px_rgba(0,0,0,0.18)] mx-auto max-w-xl space-y-4 rounded-[8px] px-4 py-8">
@@ -2290,9 +2056,11 @@ export function ProxyViewer({
2290
2056
  timeDisplayFormat={timeDisplayFormat}
2291
2057
  />
2292
2058
  </div>
2293
- <div className="mt-3">
2294
- <LogPaginationBar logs={logs} pagination={pagination} />
2295
- </div>
2059
+ {assistantSearchResult === null && (
2060
+ <div className="mt-3">
2061
+ <LogPaginationBar logs={logs} pagination={pagination} />
2062
+ </div>
2063
+ )}
2296
2064
  </div>
2297
2065
  )}
2298
2066
  </div>
@@ -1,8 +1,9 @@
1
1
  import { BrainCircuit, Loader2, RotateCcw, Send, Sparkles } from "lucide-react";
2
- import { useEffect, useMemo, useState, type ChangeEvent, type JSX } from "react";
2
+ import { useMemo, useState, type ChangeEvent, type JSX } from "react";
3
3
  import {
4
4
  buildPiAgentHistory,
5
5
  createEmptyPiAgentConversation,
6
+ getPiAgentResultLogIds,
6
7
  normalizePiAgentResponse,
7
8
  PI_AGENT_SCOPE_PILLS,
8
9
  PI_AGENT_STARTERS,
@@ -19,7 +20,13 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from ".
19
20
  type PiAgentPanelProps = {
20
21
  currentSessionId: string | null;
21
22
  logCount: number;
22
- suggestedQuestion?: string;
23
+ onResultChange: (result: PiAgentSearchResult | null) => void;
24
+ };
25
+
26
+ export type PiAgentSearchResult = {
27
+ question: string;
28
+ logIds: number[];
29
+ reportedLogCount: number;
23
30
  };
24
31
 
25
32
  const DEFAULT_QUESTION =
@@ -200,7 +207,7 @@ function ConversationMessage({ message }: { message: PiAgentConversationMessage
200
207
  export function PiAgentPanel({
201
208
  currentSessionId,
202
209
  logCount,
203
- suggestedQuestion,
210
+ onResultChange,
204
211
  }: PiAgentPanelProps): JSX.Element {
205
212
  const { providers } = useProviders();
206
213
  const piProviders = useMemo(
@@ -220,11 +227,6 @@ export function PiAgentPanel({
220
227
  const question = conversation.question;
221
228
  const canAsk = question.trim() !== "" && piProviders.length > 0 && !loading;
222
229
 
223
- useEffect(() => {
224
- if (suggestedQuestion === undefined || suggestedQuestion.trim() === "") return;
225
- setConversation((current) => ({ ...current, question: suggestedQuestion, error: null }));
226
- }, [suggestedQuestion]);
227
-
228
230
  const handleQuestionChange = (event: ChangeEvent<HTMLTextAreaElement>): void => {
229
231
  setConversation((current) => ({ ...current, question: event.currentTarget.value }));
230
232
  };
@@ -236,6 +238,7 @@ export function PiAgentPanel({
236
238
 
237
239
  const resetConversation = (): void => {
238
240
  setConversation({ ...createEmptyPiAgentConversation(), question: DEFAULT_QUESTION });
241
+ onResultChange(null);
239
242
  };
240
243
 
241
244
  const setStarter = (questionText: string): void => {
@@ -308,6 +311,11 @@ export function PiAgentPanel({
308
311
  },
309
312
  ],
310
313
  }));
314
+ onResultChange({
315
+ question: trimmedQuestion,
316
+ logIds: getPiAgentResultLogIds(normalized),
317
+ reportedLogCount: normalized.logCount,
318
+ });
311
319
  })
312
320
  .catch((err: unknown) => {
313
321
  setConversation((current) => ({
@@ -325,9 +333,9 @@ export function PiAgentPanel({
325
333
  <div className="flex items-center gap-2">
326
334
  <BrainCircuit className="size-4 text-cyan-200" />
327
335
  <div className="min-w-0 flex-1">
328
- <div className="text-xs font-semibold text-foreground">Inspector Assistant</div>
336
+ <div className="text-xs font-semibold text-foreground">Search with Assistant</div>
329
337
  <div className="text-[11px] text-muted-foreground">
330
- {currentSessionId === null ? "Ask saved Inspector data" : "Ask this session"}
338
+ {currentSessionId === null ? "Search all Inspector evidence" : "Search this session"}
331
339
  </div>
332
340
  </div>
333
341
  <button
@@ -343,8 +351,9 @@ export function PiAgentPanel({
343
351
  {conversation.messages.length === 0 && (
344
352
  <div className="mt-3 rounded-md bg-black/[0.18] px-3 py-2 shadow-[inset_0_0_0_1px_rgba(255,255,255,0.045)]">
345
353
  <p className="text-[11px] leading-5 text-muted-foreground">
346
- Ask your Inspector memory across saved traffic, sessions, tools, Provider signals,
347
- alerts, runs, groups, and knowledge candidates. Answers include linked evidence.
354
+ Describe the traffic you want to find in plain language. Assistant searches saved
355
+ traffic, sessions, tools, alerts, runs, groups, and knowledge, then uses linked evidence
356
+ to shape the log results.
348
357
  </p>
349
358
  <div className="mt-2 grid gap-1.5">
350
359
  {PI_AGENT_SCOPE_PILLS.map((pill) => (
@@ -427,7 +436,7 @@ export function PiAgentPanel({
427
436
  value={question}
428
437
  onChange={handleQuestionChange}
429
438
  className="mt-3 min-h-20 w-full resize-y rounded-md bg-black/25 px-3 py-2 text-xs leading-5 text-foreground outline-none shadow-[inset_0_0_0_1px_rgba(255,255,255,0.06)] transition-colors placeholder:text-muted-foreground focus-visible:ring-1 focus-visible:ring-ring"
430
- placeholder="Ask across saved Inspector data..."
439
+ placeholder="Search failures, slow requests, token spikes, tool calls..."
431
440
  />
432
441
 
433
442
  <button
@@ -437,7 +446,7 @@ export function PiAgentPanel({
437
446
  className="mt-2 inline-flex h-8 w-full items-center justify-center gap-2 rounded-md bg-cyan-400/12 px-3 text-xs font-medium text-cyan-100 shadow-[inset_0_0_0_1px_rgba(103,232,249,0.18)] transition-colors hover:bg-cyan-400/18 disabled:cursor-not-allowed disabled:opacity-50"
438
447
  >
439
448
  {loading ? <Loader2 className="size-3.5 animate-spin" /> : <Send className="size-3.5" />}
440
- <span>{loading ? "Thinking..." : "Ask Assistant"}</span>
449
+ <span>{loading ? "Searching..." : "Search logs"}</span>
441
450
  </button>
442
451
 
443
452
  {conversation.error !== null && (
@@ -44,6 +44,16 @@ export type PiAgentResponseView = {
44
44
  queryMetrics: PiAgentMetricView[];
45
45
  };
46
46
 
47
+ export function getPiAgentResultLogIds(response: PiAgentResponseView): number[] {
48
+ return [
49
+ ...new Set(
50
+ response.evidence
51
+ .map((evidence) => evidence.logId)
52
+ .filter((logId): logId is number => logId !== null),
53
+ ),
54
+ ];
55
+ }
56
+
47
57
  export type PiAgentConversationMessage = PiAgentHistoryMessage & {
48
58
  id: string;
49
59
  response: PiAgentResponseView | null;