@tonyclaw/agent-inspector 3.0.35 → 3.0.36

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 (42) hide show
  1. package/.output/nitro.json +1 -1
  2. package/.output/public/assets/{CompareDrawer-R4_8sh1x.js → CompareDrawer-BF8Yk4Nq.js} +1 -1
  3. package/.output/public/assets/{InspectorPet-BXOiEEuy.js → InspectorPet-D01knLHJ.js} +1 -1
  4. package/.output/public/assets/ProxyViewerContainer-CArcMNg_.js +126 -0
  5. package/.output/public/assets/{ReplayDialog-ChEc6SEZ.js → ReplayDialog-NRgCp09o.js} +1 -1
  6. package/.output/public/assets/{RequestAnatomy-Ur266kvo.js → RequestAnatomy-BPiUptQ-.js} +1 -1
  7. package/.output/public/assets/{ResponseView-JmIcAvf3.js → ResponseView-C6oCKVyP.js} +1 -1
  8. package/.output/public/assets/{StreamingChunkSequence-BxRDf8X0.js → StreamingChunkSequence-B90BNPvD.js} +1 -1
  9. package/.output/public/assets/{_sessionId-T0zMdPhW.js → _sessionId-B7pqqX6S.js} +1 -1
  10. package/.output/public/assets/{_sessionId-BcTM61oL.js → _sessionId-c3quqe4s.js} +1 -1
  11. package/.output/public/assets/index-BQqVblQv.css +1 -0
  12. package/.output/public/assets/{index-BCy0oH7t.js → index-CjODQxYY.js} +1 -1
  13. package/.output/public/assets/{index-DXzxeHNI.js → index-CmGZ17BL.js} +1 -1
  14. package/.output/public/assets/{index-nPB39bG1.js → index-D444FJGM.js} +3 -3
  15. package/.output/public/assets/{index-DoGM_H4R.js → index-D4Atppe6.js} +1 -1
  16. package/.output/public/assets/{json-viewer-AR5MqNsi.js → json-viewer-CuYTDXFj.js} +1 -1
  17. package/.output/public/assets/{jszip.min-Dp1qaNzl.js → jszip.min-C-avw0w1.js} +1 -1
  18. package/.output/server/_libs/lucide-react.mjs +15 -15
  19. package/.output/server/_libs/radix-ui__react-id.mjs +1 -1
  20. package/.output/server/_libs/radix-ui__react-select.mjs +7 -7
  21. package/.output/server/{_sessionId-D0V28kT_.mjs → _sessionId-BTazjVlZ.mjs} +1 -1
  22. package/.output/server/{_sessionId-3-efukTe.mjs → _sessionId-BsDZ3m3b.mjs} +23 -23
  23. package/.output/server/_ssr/{CompareDrawer-CwUnVrgf.mjs → CompareDrawer-BMo93_DO.mjs} +24 -24
  24. package/.output/server/_ssr/{InspectorPet-DEPoGC_D.mjs → InspectorPet-DApzyYro.mjs} +1 -1
  25. package/.output/server/_ssr/{ProxyViewerContainer-DT2GnM6k.mjs → ProxyViewerContainer-CiXXtVXH.mjs} +313 -119
  26. package/.output/server/_ssr/{ReplayDialog-DWjyvYZk.mjs → ReplayDialog-B8bP1qUr.mjs} +25 -25
  27. package/.output/server/_ssr/{RequestAnatomy-DstKoDNo.mjs → RequestAnatomy-DkVynjyh.mjs} +23 -23
  28. package/.output/server/_ssr/{ResponseView-_dBVL1_Y.mjs → ResponseView-BAQa0WUP.mjs} +24 -24
  29. package/.output/server/_ssr/{StreamingChunkSequence-Dv3MUYgf.mjs → StreamingChunkSequence-B4_K4_VI.mjs} +23 -23
  30. package/.output/server/_ssr/{index-D9TfBt-A.mjs → index-BiwG5Sws.mjs} +23 -23
  31. package/.output/server/_ssr/{index-DmUQkdzY.mjs → index-DhjNsApq.mjs} +1 -1
  32. package/.output/server/_ssr/index.mjs +2 -2
  33. package/.output/server/_ssr/{json-viewer-CWGsCD5R.mjs → json-viewer-Dlpq5S-r.mjs} +24 -24
  34. package/.output/server/_ssr/{router-BGaTQE2M.mjs → router-6s7p8rsX.mjs} +11 -11
  35. package/.output/server/{_tanstack-start-manifest_v-BjoievXr.mjs → _tanstack-start-manifest_v-ClIF-cfh.mjs} +1 -1
  36. package/.output/server/index.mjs +93 -93
  37. package/package.json +1 -1
  38. package/src/components/ProxyViewer.tsx +286 -84
  39. package/src/components/pi-agent/PiAgentPanel.tsx +90 -33
  40. package/src/services/piAgent.ts +7 -5
  41. package/.output/public/assets/ProxyViewerContainer-F4pd4poo.js +0 -126
  42. package/.output/public/assets/index-D17dU-mX.css +0 -1
@@ -1,6 +1,7 @@
1
1
  import {
2
2
  type ChangeEvent,
3
3
  type JSX,
4
+ type KeyboardEvent,
4
5
  type ReactNode,
5
6
  useCallback,
6
7
  useEffect,
@@ -115,6 +116,228 @@ function truncateSessionId(id: string): string {
115
116
  return `${id.slice(0, 12)}...${id.slice(-12)}`;
116
117
  }
117
118
 
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
+
118
341
  function buildDisplayNumberByLogId(groups: readonly ConversationGroupData[]): Map<number, number> {
119
342
  const result = new Map<number, number>();
120
343
  for (const group of groups) {
@@ -1333,6 +1556,10 @@ export function ProxyViewer({
1333
1556
  const [detailsPanelStorageLoaded, setDetailsPanelStorageLoaded] = useState(false);
1334
1557
  const [petVisible, setPetVisible] = useState(true);
1335
1558
  const [petVisibilityStorageLoaded, setPetVisibilityStorageLoaded] = useState(false);
1559
+ const [sessionSearchQuery, setSessionSearchQuery] = useState("");
1560
+ const [assistantSuggestedQuestion, setAssistantSuggestedQuestion] = useState<string | undefined>(
1561
+ undefined,
1562
+ );
1336
1563
  const [comparePair, setComparePair] = useState<[CapturedLog, CapturedLog] | null>(null);
1337
1564
  const logListRef = useRef<HTMLDivElement>(null);
1338
1565
  const logListWrapperRef = useRef<HTMLDivElement>(null);
@@ -1572,11 +1799,16 @@ export function ProxyViewer({
1572
1799
  ? "Deletes every stored log, streaming chunk, and session archive entry across all sessions."
1573
1800
  : "Deletes every stored log, streaming chunk, and session archive entry for this session, including history that is not currently loaded.";
1574
1801
  const reloadSessionLabel = logs.length === 0 ? "Load" : "Reload";
1575
- const sessionInputValue = selectedSession === "__all__" ? "" : selectedSession;
1576
- const sessionSelectValue =
1577
- selectedSession === "__all__" || sessions.includes(selectedSession)
1578
- ? selectedSession
1579
- : "__typed__";
1802
+ const sessionSearchSuggestions = useMemo(
1803
+ () =>
1804
+ buildSessionSearchSuggestions({
1805
+ query: sessionSearchQuery,
1806
+ sessions,
1807
+ models,
1808
+ logs,
1809
+ }),
1810
+ [logs, models, sessionSearchQuery, sessions],
1811
+ );
1580
1812
  const activeScopeLabel =
1581
1813
  hasSessionContext && sessionContextScope !== null
1582
1814
  ? truncateSessionId(sessionContextScope.label)
@@ -1603,20 +1835,36 @@ export function ProxyViewer({
1603
1835
  },
1604
1836
  [comparisonPredecessors],
1605
1837
  );
1606
- const handleSessionInputChange = useCallback(
1607
- (event: ChangeEvent<HTMLInputElement>) => {
1608
- const value = event.currentTarget.value.trim();
1609
- onSessionChange(value === "" ? "__all__" : value);
1610
- },
1611
- [onSessionChange],
1612
- );
1613
- const handleSessionSelectChange = useCallback(
1614
- (value: string) => {
1615
- if (value === "__typed__") return;
1616
- onSessionChange(value);
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
+ }
1617
1855
  },
1618
- [onSessionChange],
1856
+ [onModelChange, onSessionChange],
1619
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]);
1620
1868
  const labSessionId =
1621
1869
  pinnedSessionId !== undefined
1622
1870
  ? pinnedSessionId
@@ -1715,7 +1963,11 @@ export function ProxyViewer({
1715
1963
  </div>
1716
1964
  </SidebarPanel>
1717
1965
 
1718
- <PiAgentPanel currentSessionId={labSessionId} logCount={logs.length} />
1966
+ <PiAgentPanel
1967
+ currentSessionId={labSessionId}
1968
+ logCount={logs.length}
1969
+ suggestedQuestion={assistantSuggestedQuestion}
1970
+ />
1719
1971
 
1720
1972
  {hasSessionContext && sessionContextScope !== null && (
1721
1973
  <SessionContextBar
@@ -1736,72 +1988,22 @@ export function ProxyViewer({
1736
1988
  )}
1737
1989
 
1738
1990
  <SidebarPanel label="Filters">
1739
- <div className="space-y-2">
1740
- {!hideSessionFilter && (
1741
- <>
1742
- <div className="relative">
1743
- <Search className="pointer-events-none absolute left-2.5 top-1/2 size-3.5 -translate-y-1/2 text-muted-foreground" />
1744
- <input
1745
- type="search"
1746
- value={sessionInputValue}
1747
- onChange={handleSessionInputChange}
1748
- placeholder="Paste session ID"
1749
- className="bg-black/25 hover:bg-white/[0.045] focus-visible:ring-ring h-9 w-full rounded-md px-8 font-mono text-xs text-foreground outline-none shadow-[inset_0_0_0_1px_rgba(255,255,255,0.06)] transition-colors placeholder:font-sans placeholder:text-muted-foreground focus-visible:ring-1"
1750
- aria-label="Filter by session ID"
1751
- title="Filter by exact session ID"
1752
- />
1753
- {sessionInputValue !== "" && (
1754
- <button
1755
- type="button"
1756
- onClick={() => onSessionChange("__all__")}
1757
- className="absolute right-1.5 top-1/2 inline-flex size-6 -translate-y-1/2 items-center justify-center rounded-md text-muted-foreground transition-colors hover:bg-white/[0.06] hover:text-foreground focus-visible:ring-1 focus-visible:ring-ring focus-visible:outline-none"
1758
- aria-label="Clear session filter"
1759
- title="Clear session filter"
1760
- >
1761
- <X className="size-3.5" />
1762
- </button>
1763
- )}
1764
- </div>
1765
- <Select value={sessionSelectValue} onValueChange={handleSessionSelectChange}>
1766
- <SelectTrigger
1767
- aria-label="Select a session"
1768
- className="bg-black/25 hover:bg-white/[0.045] h-9 w-full border-0 text-xs text-foreground shadow-[inset_0_0_0_1px_rgba(255,255,255,0.06)]"
1769
- >
1770
- <SelectValue placeholder="All sessions" />
1771
- </SelectTrigger>
1772
- <SelectContent>
1773
- <SelectItem value="__all__">All sessions</SelectItem>
1774
- {sessionSelectValue === "__typed__" && (
1775
- <SelectItem value="__typed__">
1776
- Typed: {truncateSessionId(selectedSession)}
1777
- </SelectItem>
1778
- )}
1779
- {sessions.map((s) => (
1780
- <SelectItem key={s} value={s}>
1781
- {truncateSessionId(s)}
1782
- </SelectItem>
1783
- ))}
1784
- </SelectContent>
1785
- </Select>
1786
- </>
1787
- )}
1788
- <Select value={selectedModel} onValueChange={onModelChange}>
1789
- <SelectTrigger
1790
- aria-label="Select a model"
1791
- className="bg-black/25 hover:bg-white/[0.045] h-9 w-full border-0 text-xs text-foreground shadow-[inset_0_0_0_1px_rgba(255,255,255,0.06)]"
1792
- >
1793
- <SelectValue placeholder="All models" />
1794
- </SelectTrigger>
1795
- <SelectContent>
1796
- <SelectItem value="__all__">All models</SelectItem>
1797
- {models.map((m) => (
1798
- <SelectItem key={m} value={m}>
1799
- {m}
1800
- </SelectItem>
1801
- ))}
1802
- </SelectContent>
1803
- </Select>
1804
- </div>
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
+ )}
1805
2007
  </SidebarPanel>
1806
2008
  </div>
1807
2009
  </aside>
@@ -1,5 +1,5 @@
1
1
  import { BrainCircuit, Loader2, RotateCcw, Send, Sparkles } from "lucide-react";
2
- import { useMemo, useState, type ChangeEvent, type JSX } from "react";
2
+ import { useEffect, useMemo, useState, type ChangeEvent, type JSX } from "react";
3
3
  import {
4
4
  buildPiAgentHistory,
5
5
  createEmptyPiAgentConversation,
@@ -19,6 +19,7 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from ".
19
19
  type PiAgentPanelProps = {
20
20
  currentSessionId: string | null;
21
21
  logCount: number;
22
+ suggestedQuestion?: string;
22
23
  };
23
24
 
24
25
  const DEFAULT_QUESTION =
@@ -65,14 +66,39 @@ function nextMessageId(role: "user" | "assistant"): string {
65
66
  return `${role}-${String(Date.now())}-${String(Math.random()).slice(2)}`;
66
67
  }
67
68
 
69
+ function TerminalLabel({
70
+ label,
71
+ value,
72
+ tone = "muted",
73
+ }: {
74
+ label: string;
75
+ value?: string;
76
+ tone?: "cyan" | "muted";
77
+ }): JSX.Element {
78
+ return (
79
+ <div className="flex min-w-0 items-center gap-1.5 font-mono text-[10px]">
80
+ <span className={tone === "cyan" ? "text-cyan-100" : "text-muted-foreground"}>{label}</span>
81
+ {value !== undefined && (
82
+ <>
83
+ <span className="text-muted-foreground/55">:</span>
84
+ <span className="truncate text-muted-foreground">{value}</span>
85
+ </>
86
+ )}
87
+ </div>
88
+ );
89
+ }
90
+
68
91
  function MetricStrip({ metrics }: { metrics: readonly PiAgentMetricView[] }): JSX.Element | null {
69
92
  if (metrics.length === 0) return null;
70
93
  return (
71
- <div className="mt-2 grid grid-cols-2 gap-1.5">
94
+ <div className="grid grid-cols-2 gap-1.5">
72
95
  {metrics.slice(0, 8).map((metric) => (
73
- <div key={metric.key} className="rounded-sm bg-black/20 px-2 py-1">
74
- <div className="truncate text-[9px] uppercase text-muted-foreground">{metric.label}</div>
75
- <div className="truncate font-mono text-[11px] text-cyan-50">{metric.value}</div>
96
+ <div
97
+ key={metric.key}
98
+ className="rounded-sm bg-black/25 px-2 py-1 shadow-[inset_2px_0_0_rgba(103,232,249,0.22)]"
99
+ >
100
+ <div className="truncate font-mono text-[9px] text-muted-foreground">{metric.label}</div>
101
+ <div className="truncate font-mono text-[11px] text-cyan-50/95">{metric.value}</div>
76
102
  </div>
77
103
  ))}
78
104
  </div>
@@ -94,7 +120,7 @@ function EvidenceCard({ evidence }: { evidence: PiAgentEvidenceView }): JSX.Elem
94
120
  type="button"
95
121
  onClick={openLog}
96
122
  disabled={evidence.logId === null}
97
- className="w-full rounded-sm bg-black/20 px-2 py-2 text-left shadow-[inset_0_0_0_1px_rgba(255,255,255,0.05)] transition-colors hover:bg-black/30 disabled:cursor-default disabled:hover:bg-black/20"
123
+ className="w-full rounded-sm bg-black/25 px-2 py-2 text-left shadow-[inset_2px_0_0_rgba(103,232,249,0.22),inset_0_0_0_1px_rgba(255,255,255,0.045)] transition-colors hover:bg-black/35 disabled:cursor-default disabled:hover:bg-black/25"
98
124
  >
99
125
  <div className="flex min-w-0 items-center justify-between gap-2">
100
126
  <span className="truncate font-mono text-[10px] text-cyan-100">{title}</span>
@@ -119,21 +145,37 @@ function EvidenceCard({ evidence }: { evidence: PiAgentEvidenceView }): JSX.Elem
119
145
 
120
146
  function AssistantMessage({ response }: { response: PiAgentResponseView }): JSX.Element {
121
147
  return (
122
- <div className="rounded-md bg-black/25 p-3 shadow-[inset_0_0_0_1px_rgba(255,255,255,0.05)]">
123
- <AnswerMarkdown text={response.answer} />
124
- <div className="mt-2 break-words font-mono text-[10px] text-cyan-100/70">
125
- {response.providerName} / {response.model} / {response.protocol} /{" "}
126
- {String(response.logCount)} matched logs
148
+ <div className="overflow-hidden rounded-md bg-[#050b12]/85 shadow-[inset_0_0_0_1px_rgba(103,232,249,0.12),0_10px_24px_rgba(0,0,0,0.18)]">
149
+ <div className="flex min-w-0 items-center justify-between gap-2 border-b border-cyan-100/[0.08] bg-cyan-400/[0.045] px-3 py-2">
150
+ <TerminalLabel label="assistant@inspector" value="output" tone="cyan" />
151
+ <span className="shrink-0 font-mono text-[9px] text-muted-foreground">
152
+ {String(response.logCount)} logs
153
+ </span>
127
154
  </div>
128
- <MetricStrip metrics={response.summaryMetrics} />
129
- <MetricStrip metrics={response.queryMetrics} />
130
- {response.evidence.length > 0 && (
131
- <div className="mt-2 space-y-1.5">
132
- {response.evidence.slice(0, 6).map((evidence) => (
133
- <EvidenceCard key={`${evidence.source}-${evidence.id}`} evidence={evidence} />
134
- ))}
155
+ <div className="space-y-3 p-3">
156
+ <div className="rounded-sm bg-black/20 px-2.5 py-2 text-xs leading-5 shadow-[inset_2px_0_0_rgba(103,232,249,0.28)]">
157
+ <AnswerMarkdown text={response.answer} />
135
158
  </div>
136
- )}
159
+ <div className="grid gap-1.5 rounded-sm bg-black/20 px-2 py-1.5">
160
+ <TerminalLabel label="provider" value={response.providerName} />
161
+ <TerminalLabel label="model" value={response.model} />
162
+ <TerminalLabel label="protocol" value={response.protocol} />
163
+ </div>
164
+ {(response.summaryMetrics.length > 0 || response.queryMetrics.length > 0) && (
165
+ <div className="space-y-1.5">
166
+ <TerminalLabel label="metrics" tone="cyan" />
167
+ <MetricStrip metrics={[...response.summaryMetrics, ...response.queryMetrics]} />
168
+ </div>
169
+ )}
170
+ {response.evidence.length > 0 && (
171
+ <div className="space-y-1.5">
172
+ <TerminalLabel label="evidence" value="click to inspect source log" tone="cyan" />
173
+ {response.evidence.slice(0, 6).map((evidence) => (
174
+ <EvidenceCard key={`${evidence.source}-${evidence.id}`} evidence={evidence} />
175
+ ))}
176
+ </div>
177
+ )}
178
+ </div>
137
179
  </div>
138
180
  );
139
181
  }
@@ -143,13 +185,23 @@ function ConversationMessage({ message }: { message: PiAgentConversationMessage
143
185
  return <AssistantMessage response={message.response} />;
144
186
  }
145
187
  return (
146
- <div className="rounded-md bg-cyan-400/10 px-3 py-2 text-xs leading-5 text-cyan-50">
147
- {message.content}
188
+ <div className="overflow-hidden rounded-md bg-black/25 shadow-[inset_0_0_0_1px_rgba(255,255,255,0.055)]">
189
+ <div className="border-b border-white/[0.06] px-3 py-1.5">
190
+ <TerminalLabel label="user@inspector" value="query" tone="cyan" />
191
+ </div>
192
+ <div className="px-3 py-2 font-mono text-[11px] leading-5 text-cyan-50">
193
+ <span className="select-none text-cyan-100/60">&gt; </span>
194
+ {message.content}
195
+ </div>
148
196
  </div>
149
197
  );
150
198
  }
151
199
 
152
- export function PiAgentPanel({ currentSessionId, logCount }: PiAgentPanelProps): JSX.Element {
200
+ export function PiAgentPanel({
201
+ currentSessionId,
202
+ logCount,
203
+ suggestedQuestion,
204
+ }: PiAgentPanelProps): JSX.Element {
153
205
  const { providers } = useProviders();
154
206
  const piProviders = useMemo(
155
207
  () => sortProviderBySource(providers.filter(providerSupportsPiAgent)),
@@ -168,6 +220,11 @@ export function PiAgentPanel({ currentSessionId, logCount }: PiAgentPanelProps):
168
220
  const question = conversation.question;
169
221
  const canAsk = question.trim() !== "" && piProviders.length > 0 && !loading;
170
222
 
223
+ useEffect(() => {
224
+ if (suggestedQuestion === undefined || suggestedQuestion.trim() === "") return;
225
+ setConversation((current) => ({ ...current, question: suggestedQuestion, error: null }));
226
+ }, [suggestedQuestion]);
227
+
171
228
  const handleQuestionChange = (event: ChangeEvent<HTMLTextAreaElement>): void => {
172
229
  setConversation((current) => ({ ...current, question: event.currentTarget.value }));
173
230
  };
@@ -226,7 +283,7 @@ export function PiAgentPanel({ currentSessionId, logCount }: PiAgentPanelProps):
226
283
  if (!response.ok) {
227
284
  setConversation((current) => ({
228
285
  ...current,
229
- error: `Pi Agent failed: HTTP ${String(response.status)} ${raw}`,
286
+ error: `Inspector Assistant failed: HTTP ${String(response.status)} ${raw}`,
230
287
  }));
231
288
  return;
232
289
  }
@@ -235,7 +292,7 @@ export function PiAgentPanel({ currentSessionId, logCount }: PiAgentPanelProps):
235
292
  if (normalized === null) {
236
293
  setConversation((current) => ({
237
294
  ...current,
238
- error: "Pi Agent returned an unexpected response.",
295
+ error: "Inspector Assistant returned an unexpected response.",
239
296
  }));
240
297
  return;
241
298
  }
@@ -255,7 +312,7 @@ export function PiAgentPanel({ currentSessionId, logCount }: PiAgentPanelProps):
255
312
  .catch((err: unknown) => {
256
313
  setConversation((current) => ({
257
314
  ...current,
258
- error: err instanceof Error ? err.message : "Pi Agent request failed.",
315
+ error: err instanceof Error ? err.message : "Inspector Assistant request failed.",
259
316
  }));
260
317
  })
261
318
  .finally(() => {
@@ -268,7 +325,7 @@ export function PiAgentPanel({ currentSessionId, logCount }: PiAgentPanelProps):
268
325
  <div className="flex items-center gap-2">
269
326
  <BrainCircuit className="size-4 text-cyan-200" />
270
327
  <div className="min-w-0 flex-1">
271
- <div className="text-xs font-semibold text-foreground">Pi Agent</div>
328
+ <div className="text-xs font-semibold text-foreground">Inspector Assistant</div>
272
329
  <div className="text-[11px] text-muted-foreground">
273
330
  {currentSessionId === null ? "Ask saved Inspector data" : "Ask this session"}
274
331
  </div>
@@ -286,9 +343,8 @@ export function PiAgentPanel({ currentSessionId, logCount }: PiAgentPanelProps):
286
343
  {conversation.messages.length === 0 && (
287
344
  <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)]">
288
345
  <p className="text-[11px] leading-5 text-muted-foreground">
289
- ChatBI for Inspector memory: Pi searches saved traffic, sessions, tools, Provider
290
- signals, alerts, runs, groups, and knowledge candidates, then answers with linked
291
- evidence.
346
+ Ask your Inspector memory across saved traffic, sessions, tools, Provider signals,
347
+ alerts, runs, groups, and knowledge candidates. Answers include linked evidence.
292
348
  </p>
293
349
  <div className="mt-2 grid gap-1.5">
294
350
  {PI_AGENT_SCOPE_PILLS.map((pill) => (
@@ -371,7 +427,7 @@ export function PiAgentPanel({ currentSessionId, logCount }: PiAgentPanelProps):
371
427
  value={question}
372
428
  onChange={handleQuestionChange}
373
429
  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"
374
- placeholder="Ask Pi Agent across saved Inspector data..."
430
+ placeholder="Ask across saved Inspector data..."
375
431
  />
376
432
 
377
433
  <button
@@ -381,7 +437,7 @@ export function PiAgentPanel({ currentSessionId, logCount }: PiAgentPanelProps):
381
437
  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"
382
438
  >
383
439
  {loading ? <Loader2 className="size-3.5 animate-spin" /> : <Send className="size-3.5" />}
384
- <span>{loading ? "Thinking..." : "Ask Pi"}</span>
440
+ <span>{loading ? "Thinking..." : "Ask Assistant"}</span>
385
441
  </button>
386
442
 
387
443
  {conversation.error !== null && (
@@ -389,12 +445,13 @@ export function PiAgentPanel({ currentSessionId, logCount }: PiAgentPanelProps):
389
445
  )}
390
446
  {piProviders.length === 0 && (
391
447
  <p className="mt-2 text-[11px] leading-5 text-muted-foreground">
392
- Configure a Provider with Responses, Chat, or Anthropic URL to enable Pi Agent.
448
+ Configure a Provider with Responses, Chat, or Anthropic URL to enable Inspector Assistant.
393
449
  </p>
394
450
  )}
395
451
  {logCount === 0 && (
396
452
  <p className="mt-2 text-[11px] leading-5 text-muted-foreground">
397
- No logs are visible in this scope yet; Pi Agent can still answer Provider setup questions.
453
+ No logs are visible in this scope yet; Inspector Assistant can still answer Provider setup
454
+ questions.
398
455
  </p>
399
456
  )}
400
457
  </section>