@tonyclaw/agent-inspector 3.0.36 → 3.0.38

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 (40) hide show
  1. package/.output/nitro.json +1 -1
  2. package/.output/public/assets/{CompareDrawer-BF8Yk4Nq.js → CompareDrawer-BXQWabQJ.js} +1 -1
  3. package/.output/public/assets/{InspectorPet-D01knLHJ.js → InspectorPet-CpQEXKcr.js} +1 -1
  4. package/.output/public/assets/ProxyViewerContainer-8pNJsCJ5.js +126 -0
  5. package/.output/public/assets/{ReplayDialog-NRgCp09o.js → ReplayDialog-CnYt4UKE.js} +1 -1
  6. package/.output/public/assets/{RequestAnatomy-BPiUptQ-.js → RequestAnatomy-BI25al3m.js} +1 -1
  7. package/.output/public/assets/{ResponseView-C6oCKVyP.js → ResponseView-D3y-I9nb.js} +1 -1
  8. package/.output/public/assets/{StreamingChunkSequence-B90BNPvD.js → StreamingChunkSequence-n90dYzPp.js} +1 -1
  9. package/.output/public/assets/{_sessionId-B7pqqX6S.js → _sessionId-CogrNhCS.js} +1 -1
  10. package/.output/public/assets/{_sessionId-c3quqe4s.js → _sessionId-DLbOYm3h.js} +1 -1
  11. package/.output/public/assets/{index-CmGZ17BL.js → index-BWWA1Rf6.js} +1 -1
  12. package/.output/public/assets/{index-D444FJGM.js → index-Ca0grOxW.js} +3 -3
  13. package/.output/public/assets/index-DKwrwHsI.css +1 -0
  14. package/.output/public/assets/{index-D4Atppe6.js → index-DXGp27k9.js} +1 -1
  15. package/.output/public/assets/{index-CjODQxYY.js → index-I5zN_KrA.js} +1 -1
  16. package/.output/public/assets/{json-viewer-CuYTDXFj.js → json-viewer-BoB2BfHg.js} +1 -1
  17. package/.output/public/assets/{jszip.min-C-avw0w1.js → jszip.min-CAXkxQd6.js} +1 -1
  18. package/.output/server/_libs/lucide-react.mjs +59 -59
  19. package/.output/server/{_sessionId-BsDZ3m3b.mjs → _sessionId-DScqlEFC.mjs} +2 -2
  20. package/.output/server/{_sessionId-BTazjVlZ.mjs → _sessionId-_hORwhWH.mjs} +1 -1
  21. package/.output/server/_ssr/{CompareDrawer-BMo93_DO.mjs → CompareDrawer-BtGm5i_L.mjs} +3 -3
  22. package/.output/server/_ssr/{InspectorPet-DApzyYro.mjs → InspectorPet-BnN6lVqM.mjs} +1 -1
  23. package/.output/server/_ssr/{ProxyViewerContainer-CiXXtVXH.mjs → ProxyViewerContainer-aduEJ_Xy.mjs} +112 -272
  24. package/.output/server/_ssr/{ReplayDialog-B8bP1qUr.mjs → ReplayDialog-BSGZ-TFs.mjs} +4 -4
  25. package/.output/server/_ssr/{RequestAnatomy-DkVynjyh.mjs → RequestAnatomy-DN_eSnDN.mjs} +3 -3
  26. package/.output/server/_ssr/{ResponseView-BAQa0WUP.mjs → ResponseView-Dk9Td_HM.mjs} +3 -3
  27. package/.output/server/_ssr/{StreamingChunkSequence-B4_K4_VI.mjs → StreamingChunkSequence-DthH8Lx4.mjs} +3 -3
  28. package/.output/server/_ssr/{index-BiwG5Sws.mjs → index-CqeZgFFF.mjs} +2 -2
  29. package/.output/server/_ssr/{index-DhjNsApq.mjs → index-DvVHL942.mjs} +1 -1
  30. package/.output/server/_ssr/index.mjs +2 -2
  31. package/.output/server/_ssr/{json-viewer-Dlpq5S-r.mjs → json-viewer-DLKA2jUq.mjs} +3 -3
  32. package/.output/server/_ssr/{router-6s7p8rsX.mjs → router-CRKEDn6n.mjs} +6 -6
  33. package/.output/server/{_tanstack-start-manifest_v-ClIF-cfh.mjs → _tanstack-start-manifest_v-pQO9kTmV.mjs} +1 -1
  34. package/.output/server/index.mjs +95 -95
  35. package/package.json +7 -1
  36. package/src/components/ProxyViewer.tsx +103 -331
  37. package/src/components/pi-agent/PiAgentPanel.tsx +23 -14
  38. package/src/components/pi-agent/piAgentChatLogic.ts +10 -0
  39. package/.output/public/assets/ProxyViewerContainer-CArcMNg_.js +0 -126
  40. 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
@@ -1915,23 +1660,39 @@ export function ProxyViewer({
1915
1660
  >
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
- <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>
1663
+ <div className="flex min-h-full flex-col gap-4 p-4">
1664
+ <div className="bg-background/95 sticky top-0 z-20 -mx-4 -mt-4 px-4 pt-4 pb-3 shadow-[0_12px_24px_rgba(0,0,0,0.18),inset_0_-1px_0_rgba(255,255,255,0.045)] backdrop-blur">
1665
+ <SidebarPanel label="Tools">
1666
+ <div className="flex flex-wrap items-center gap-1.5">
1667
+ <ThemeModeToggle />
1668
+ <UiScaleControl
1669
+ preference={uiScalePreference}
1670
+ resolvedScale={resolvedUiScale}
1671
+ onChange={setUiScalePreference}
1672
+ />
1673
+ <GroupsDialog />
1674
+ <AgentLabDialog currentSessionId={labSessionId} logCount={logs.length} />
1675
+ </div>
1676
+ </SidebarPanel>
1677
+ </div>
1931
1678
 
1932
- <SidebarPanel label="Session">
1679
+ <SidebarPanel label="Current scope">
1680
+ <div className="mb-2 rounded-md bg-black/20 px-3 py-2 shadow-[inset_0_1px_0_rgba(255,255,255,0.035)]">
1681
+ <div
1682
+ className="truncate font-mono text-xs font-semibold text-cyan-100"
1683
+ title={activeScopeLabel}
1684
+ >
1685
+ {activeScopeLabel}
1686
+ </div>
1687
+ <div className="mt-1 text-[11px] text-muted-foreground">
1688
+ {isPinnedSessionPage ? "Pinned session" : "Live Inspector data"}
1689
+ </div>
1690
+ </div>
1933
1691
  <div className="grid grid-cols-2 gap-2">
1934
- <SidebarMetric label="Requests" value={logs.length} />
1692
+ <SidebarMetric
1693
+ label={assistantSearchResult === null ? "Requests" : "Results"}
1694
+ value={displayedLogs.length}
1695
+ />
1935
1696
  <SidebarMetric label="Sessions" value={sessions.length} />
1936
1697
  <SidebarMetric
1937
1698
  label="Input"
@@ -1949,26 +1710,6 @@ export function ProxyViewer({
1949
1710
  )}
1950
1711
  </SidebarPanel>
1951
1712
 
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>
1963
- </div>
1964
- </SidebarPanel>
1965
-
1966
- <PiAgentPanel
1967
- currentSessionId={labSessionId}
1968
- logCount={logs.length}
1969
- suggestedQuestion={assistantSuggestedQuestion}
1970
- />
1971
-
1972
1713
  {hasSessionContext && sessionContextScope !== null && (
1973
1714
  <SessionContextBar
1974
1715
  label={sessionContextScope.label}
@@ -1987,24 +1728,13 @@ export function ProxyViewer({
1987
1728
  <SessionMembershipPanel memberships={sessionMemberships} />
1988
1729
  )}
1989
1730
 
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>
1731
+ <div className="flex-1" />
1732
+
1733
+ <PiAgentPanel
1734
+ currentSessionId={labSessionId}
1735
+ logCount={logs.length}
1736
+ onResultChange={setAssistantSearchResult}
1737
+ />
2008
1738
  </div>
2009
1739
  </aside>
2010
1740
  )}
@@ -2151,7 +1881,30 @@ export function ProxyViewer({
2151
1881
  )}
2152
1882
  </div>
2153
1883
  )}
2154
- <LogPaginationBar logs={logs} pagination={pagination} embedded={true} />
1884
+ {assistantSearchResult !== null && (
1885
+ <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)]">
1886
+ <div className="min-w-0 flex-1">
1887
+ <div className="truncate text-xs font-medium text-cyan-50">
1888
+ {assistantSearchResult.question}
1889
+ </div>
1890
+ <div className="mt-0.5 text-[10px] text-muted-foreground">
1891
+ Showing {String(displayedLogs.length)} loaded logs from{" "}
1892
+ {String(assistantSearchResult.logIds.length)} linked results
1893
+ </div>
1894
+ </div>
1895
+ <button
1896
+ type="button"
1897
+ onClick={() => setAssistantSearchResult(null)}
1898
+ 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"
1899
+ >
1900
+ <X className="size-3" />
1901
+ All logs
1902
+ </button>
1903
+ </div>
1904
+ )}
1905
+ {assistantSearchResult === null && (
1906
+ <LogPaginationBar logs={logs} pagination={pagination} embedded={true} />
1907
+ )}
2155
1908
  {onNotify === undefined && exportError !== null && (
2156
1909
  <div className="mt-2 rounded-md border border-destructive/30 bg-destructive/10 px-3 py-2 text-xs text-destructive">
2157
1910
  {exportError}
@@ -2182,7 +1935,24 @@ export function ProxyViewer({
2182
1935
 
2183
1936
  <div className="px-4 py-4 lg:px-5">
2184
1937
  <SessionOpeningSlate stats={slateStats} />
2185
- {logs.length === 0 ? (
1938
+ {assistantSearchResult !== null && displayedLogs.length === 0 ? (
1939
+ <div className="py-16 text-center text-muted-foreground">
1940
+ <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)]">
1941
+ <p className="text-sm font-medium text-foreground">No linked logs are loaded</p>
1942
+ <p className="mt-2 text-xs leading-5 text-muted-foreground">
1943
+ Assistant found evidence, but none of its linked log IDs are in the current
1944
+ loaded window. Open an evidence card in Assistant or restore the full log view.
1945
+ </p>
1946
+ <button
1947
+ type="button"
1948
+ onClick={() => setAssistantSearchResult(null)}
1949
+ 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"
1950
+ >
1951
+ Show all logs
1952
+ </button>
1953
+ </div>
1954
+ </div>
1955
+ ) : logs.length === 0 ? (
2186
1956
  selectedSession !== "__all__" ? (
2187
1957
  <div className="py-16 text-center text-muted-foreground">
2188
1958
  <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 +2060,11 @@ export function ProxyViewer({
2290
2060
  timeDisplayFormat={timeDisplayFormat}
2291
2061
  />
2292
2062
  </div>
2293
- <div className="mt-3">
2294
- <LogPaginationBar logs={logs} pagination={pagination} />
2295
- </div>
2063
+ {assistantSearchResult === null && (
2064
+ <div className="mt-3">
2065
+ <LogPaginationBar logs={logs} pagination={pagination} />
2066
+ </div>
2067
+ )}
2296
2068
  </div>
2297
2069
  )}
2298
2070
  </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;