@tonyclaw/agent-inspector 3.0.29 → 3.0.31

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 (156) hide show
  1. package/.output/cli.js +791 -191
  2. package/.output/nitro.json +1 -1
  3. package/.output/public/assets/CompareDrawer-DB2KggLE.js +1 -0
  4. package/.output/public/assets/InspectorPet-CfwDw6kD.js +4108 -0
  5. package/.output/public/assets/ProxyViewerContainer-PcrO96_F.js +126 -0
  6. package/.output/public/assets/{ReplayDialog-CctnITbt.js → ReplayDialog-3Pl7EKjp.js} +1 -1
  7. package/.output/public/assets/{RequestAnatomy-BrN0RBbL.js → RequestAnatomy-BcPjcHLX.js} +1 -1
  8. package/.output/public/assets/ResponseView-DOx51wV3.js +2 -0
  9. package/.output/public/assets/StreamingChunkSequence-LkYJWM1A.js +1 -0
  10. package/.output/public/assets/_sessionId-BSg32vC6.js +1 -0
  11. package/.output/public/assets/{_sessionId-DKM91udC.js → _sessionId-C0CA68QV.js} +1 -1
  12. package/.output/public/assets/{index-Bq-nqolG.js → index-BpBUqdAB.js} +1 -1
  13. package/.output/public/assets/index-D93dCgAQ.js +1 -0
  14. package/.output/public/assets/{index-DPiESBo2.js → index-FV5sxqx3.js} +6 -6
  15. package/.output/public/assets/index-VUsF4nhf.js +76 -0
  16. package/.output/public/assets/index-hyQHZeQI.css +1 -0
  17. package/.output/public/assets/{json-viewer-CfaDJwx8.js → json-viewer-WctIbAh_.js} +1 -1
  18. package/.output/public/assets/jszip.min-DnSIoTix.js +2 -0
  19. package/.output/public/assets/qwen-mMn3f5ul.webp +0 -0
  20. package/.output/server/_libs/jszip.mjs +35 -9
  21. package/.output/server/_libs/lucide-react.mjs +80 -80
  22. package/.output/server/_libs/radix-ui__react-dialog.mjs +3 -3
  23. package/.output/server/_libs/tanstack__react-router.mjs +1 -1
  24. package/.output/server/{_sessionId-CwGXPD4C.mjs → _sessionId-BHBK3VbF.mjs} +15 -15
  25. package/.output/server/_sessionId-CyuJ6Kfl.mjs +81 -0
  26. package/.output/server/_ssr/{CompareDrawer-R_P1-VNr.mjs → CompareDrawer-27amlv_a.mjs} +138 -103
  27. package/.output/server/_ssr/InspectorPet-CQnGXfLs.mjs +925 -0
  28. package/.output/server/_ssr/{ProxyViewerContainer-DUgxRNbj.mjs → ProxyViewerContainer-DlAl0uF-.mjs} +1569 -1646
  29. package/.output/server/_ssr/{ReplayDialog-De9wKKzS.mjs → ReplayDialog-5m2jtlLa.mjs} +18 -18
  30. package/.output/server/_ssr/{RequestAnatomy-Db4MxUYo.mjs → RequestAnatomy--1WtQNY9.mjs} +58 -58
  31. package/.output/server/_ssr/{ResponseView-8rDXOiNB.mjs → ResponseView-CgbqS8sr.mjs} +60 -71
  32. package/.output/server/_ssr/{StreamingChunkSequence-N9teZHkA.mjs → StreamingChunkSequence-BwpnopWx.mjs} +16 -16
  33. package/.output/server/_ssr/{index-D4DvByuW.mjs → index-CNVi1TzT.mjs} +15 -15
  34. package/.output/server/_ssr/index-DMCjH_4V.mjs +81 -0
  35. package/.output/server/_ssr/index.mjs +2 -2
  36. package/.output/server/_ssr/{json-viewer-BjoHRbjS.mjs → json-viewer-DNGN2uoe.mjs} +58 -58
  37. package/.output/server/_ssr/{router-BYlePDj8.mjs → router-CgsqLnk5.mjs} +16007 -12632
  38. package/.output/server/_tanstack-start-manifest_v-BH5f_kWV.mjs +4 -0
  39. package/.output/server/index.mjs +99 -71
  40. package/.output/workers/logFinalizer.worker.js +16908 -0
  41. package/.output/workers/sessionWorkerEntry.js +16904 -0
  42. package/README.md +21 -5
  43. package/package.json +6 -2
  44. package/src/assets/logos/qwen.webp +0 -0
  45. package/src/cli.ts +85 -10
  46. package/src/components/OnboardingBanner.tsx +41 -21
  47. package/src/components/ProxyViewer.tsx +198 -269
  48. package/src/components/ProxyViewerContainer.tsx +128 -17
  49. package/src/components/ecosystem/AgentLabDialog.tsx +166 -19
  50. package/src/components/errors/SafeErrorBoundary.tsx +201 -0
  51. package/src/components/pi-agent/PiAgentPanel.tsx +209 -35
  52. package/src/components/pi-agent/piAgentChatLogic.ts +263 -0
  53. package/src/components/providers/ImportWizardDialog.tsx +18 -42
  54. package/src/components/providers/ProviderLogo.tsx +1 -1
  55. package/src/components/providers/ProvidersPanel.tsx +8 -5
  56. package/src/components/providers/SettingsDialog.tsx +34 -11
  57. package/src/components/proxy-viewer/ApplicationBar.tsx +63 -0
  58. package/src/components/proxy-viewer/CompareDrawer.tsx +92 -49
  59. package/src/components/proxy-viewer/LiveConnectionStatus.tsx +186 -0
  60. package/src/components/proxy-viewer/LogEntry.tsx +283 -275
  61. package/src/components/proxy-viewer/RawExportWarning.tsx +77 -0
  62. package/src/components/proxy-viewer/SessionLoadProgressBar.tsx +61 -0
  63. package/src/components/proxy-viewer/ThreadConnector.tsx +16 -1
  64. package/src/components/proxy-viewer/TurnGroup.tsx +13 -2
  65. package/src/components/proxy-viewer/accessibility.ts +8 -0
  66. package/src/components/proxy-viewer/lazy.ts +4 -0
  67. package/src/components/proxy-viewer/liveConnectionState.ts +158 -0
  68. package/src/components/proxy-viewer/proxyViewerLogic.ts +10 -0
  69. package/src/components/proxy-viewer/viewerState.ts +2 -6
  70. package/src/components/ui/select.tsx +1 -1
  71. package/src/components/ui/tabs.tsx +3 -3
  72. package/src/components/ui/transient-toast.tsx +7 -6
  73. package/src/lib/apiClient.ts +17 -2
  74. package/src/lib/ecosystemContract.ts +34 -0
  75. package/src/lib/export-logs.ts +1 -1
  76. package/src/lib/piAgentContract.ts +80 -0
  77. package/src/lib/providerContract.ts +70 -4
  78. package/src/lib/providerImportContract.ts +27 -0
  79. package/src/lib/providerModelMetadata.ts +16 -7
  80. package/src/lib/resourceLimits.ts +152 -0
  81. package/src/lib/safeDiagnostic.ts +38 -0
  82. package/src/lib/useProviders.ts +4 -4
  83. package/src/mcp/server.ts +39 -6
  84. package/src/mcp/toolHandlers.ts +131 -4
  85. package/src/proxy/chunkStorage.ts +20 -6
  86. package/src/proxy/config.ts +20 -6
  87. package/src/proxy/dataDir.ts +3 -0
  88. package/src/proxy/ecosystemExecutionConfirmation.ts +114 -0
  89. package/src/proxy/ecosystemExecutionRoute.ts +116 -0
  90. package/src/proxy/ecosystemTasks.ts +48 -0
  91. package/src/proxy/evidenceExporter.ts +23 -9
  92. package/src/proxy/groupEvidenceExporter.ts +26 -5
  93. package/src/proxy/groupStore.ts +13 -3
  94. package/src/proxy/handler.ts +103 -20
  95. package/src/proxy/identityProxy.ts +333 -14
  96. package/src/proxy/logFinalizer.ts +73 -6
  97. package/src/proxy/logImportUpload.ts +128 -0
  98. package/src/proxy/logImporter.ts +321 -70
  99. package/src/proxy/logIndex.ts +16 -6
  100. package/src/proxy/logger.ts +294 -41
  101. package/src/proxy/privateDataPath.ts +183 -0
  102. package/src/proxy/providerScanStore.ts +87 -0
  103. package/src/proxy/providerSecretStore.ts +58 -19
  104. package/src/proxy/providers.ts +175 -58
  105. package/src/proxy/rawStreamCapture.ts +66 -5
  106. package/src/proxy/runStore.ts +13 -3
  107. package/src/proxy/runtimeAdmission.ts +52 -0
  108. package/src/proxy/runtimeHealth.ts +206 -0
  109. package/src/proxy/runtimeShutdown.ts +75 -0
  110. package/src/proxy/sessionArchive.ts +15 -2
  111. package/src/proxy/sessionProcess.ts +19 -0
  112. package/src/proxy/sessionRuntime.ts +7 -0
  113. package/src/proxy/shutdownCoordinator.ts +90 -0
  114. package/src/proxy/sqliteLogIndex.ts +18 -2
  115. package/src/proxy/store.ts +18 -2
  116. package/src/routes/__root.tsx +18 -1
  117. package/src/routes/api/alerts.summary.ts +12 -8
  118. package/src/routes/api/alerts.ts +27 -9
  119. package/src/routes/api/ecosystem.packages.$packageId.help.ts +3 -8
  120. package/src/routes/api/ecosystem.packages.$packageId.install.ts +3 -8
  121. package/src/routes/api/ecosystem.packages.$packageId.runner-presets.ts +3 -11
  122. package/src/routes/api/ecosystem.packages.$packageId.upgrade.ts +3 -8
  123. package/src/routes/api/ecosystem.recipes.$recipeId.run.ts +3 -8
  124. package/src/routes/api/health.ts +9 -2
  125. package/src/routes/api/knowledge.project-context.ts +11 -0
  126. package/src/routes/api/knowledge.search.ts +33 -2
  127. package/src/routes/api/logs.$id.body.ts +16 -2
  128. package/src/routes/api/logs.import.ts +7 -18
  129. package/src/routes/api/logs.stream.ts +147 -69
  130. package/src/routes/api/logs.ts +55 -14
  131. package/src/routes/api/providers.$providerId.model-metadata.ts +22 -4
  132. package/src/routes/api/providers.$providerId.ts +37 -6
  133. package/src/routes/api/providers.export.ts +34 -16
  134. package/src/routes/api/providers.import.ts +42 -8
  135. package/src/routes/api/providers.scan.ts +13 -8
  136. package/src/routes/api/providers.ts +36 -5
  137. package/src/routes/api/runs.ts +12 -12
  138. package/src/routes/api/sessions.ts +15 -8
  139. package/src/routes/index.tsx +6 -0
  140. package/src/routes/livez.ts +13 -0
  141. package/src/routes/readyz.ts +18 -0
  142. package/src/routes/session/$sessionId.tsx +6 -0
  143. package/src/services/piAgent.ts +131 -102
  144. package/src/services/piAgentRetrieval.ts +1180 -0
  145. package/styles/globals.css +19 -3
  146. package/.output/public/assets/CompareDrawer-Cb5ccPeX.js +0 -1
  147. package/.output/public/assets/ProxyViewerContainer-COBe5GTT.js +0 -4234
  148. package/.output/public/assets/ResponseView-Cqq87lX_.js +0 -3
  149. package/.output/public/assets/StreamingChunkSequence-CnTa9mCr.js +0 -1
  150. package/.output/public/assets/index-DTLNVcgc.js +0 -76
  151. package/.output/public/assets/index-DliqmwUw.css +0 -1
  152. package/.output/public/assets/qwen-CONDcHqt.png +0 -0
  153. package/.output/server/_tanstack-start-manifest_v-DMCA8KK3.mjs +0 -4
  154. package/src/assets/logos/mcp.png +0 -0
  155. package/src/assets/logos/qwen.png +0 -0
  156. package/src/components/ui/mcp-logo.tsx +0 -20
@@ -0,0 +1,201 @@
1
+ import {
2
+ Component,
3
+ type ErrorInfo,
4
+ type JSX,
5
+ type ReactNode,
6
+ useCallback,
7
+ useMemo,
8
+ useState,
9
+ } from "react";
10
+
11
+ import { copyTextToClipboard } from "../../lib/clipboard";
12
+ import { createSafeDiagnosticReference } from "../../lib/safeDiagnostic";
13
+ import { Button } from "../ui/button";
14
+ import { CrabLogo } from "../ui/crab-logo";
15
+
16
+ export type SafeErrorFallbackProps = {
17
+ reference: string;
18
+ title: string;
19
+ description: string;
20
+ variant: "page" | "feature";
21
+ onRetry?: () => void;
22
+ onReload?: () => void;
23
+ };
24
+
25
+ export function SafeErrorFallback({
26
+ reference,
27
+ title,
28
+ description,
29
+ variant,
30
+ onRetry,
31
+ onReload,
32
+ }: SafeErrorFallbackProps): JSX.Element {
33
+ const [copied, setCopied] = useState(false);
34
+ const handleCopy = useCallback(() => {
35
+ void copyTextToClipboard(reference).then((success) => {
36
+ if (!success) return;
37
+ setCopied(true);
38
+ });
39
+ }, [reference]);
40
+
41
+ const content = (
42
+ <div
43
+ role="alert"
44
+ className={
45
+ variant === "page"
46
+ ? "border-border/70 bg-card/80 rounded-lg border p-7 shadow-sm"
47
+ : "border-amber-400/20 bg-amber-500/[0.07] rounded-lg border p-4 shadow-sm"
48
+ }
49
+ >
50
+ <div className="flex items-center gap-3">
51
+ <CrabLogo className="size-9 shrink-0 text-amber-500" />
52
+ <div>
53
+ <div className="text-muted-foreground text-xs font-semibold uppercase tracking-wider">
54
+ Agent Inspector recovery
55
+ </div>
56
+ <h1 className={variant === "page" ? "mt-1 text-xl font-semibold" : "mt-1 font-semibold"}>
57
+ {title}
58
+ </h1>
59
+ </div>
60
+ </div>
61
+ <p className="text-muted-foreground mt-4 max-w-xl text-sm leading-6">{description}</p>
62
+ <div className="border-border/60 bg-background/70 mt-4 flex flex-wrap items-center gap-2 rounded-md border px-3 py-2">
63
+ <span className="text-muted-foreground text-xs">Diagnostic reference</span>
64
+ <code className="min-w-0 flex-1 break-all font-mono text-xs text-foreground">
65
+ {reference}
66
+ </code>
67
+ <Button type="button" variant="outline" size="sm" onClick={handleCopy}>
68
+ {copied ? "Copied" : "Copy reference"}
69
+ </Button>
70
+ </div>
71
+ <p className="text-muted-foreground mt-2 text-xs">
72
+ Error details are hidden to protect captured prompts, source content, headers, and secrets.
73
+ </p>
74
+ <div className="mt-5 flex flex-wrap gap-2">
75
+ {onRetry !== undefined && (
76
+ <Button type="button" size="sm" onClick={onRetry}>
77
+ Retry
78
+ </Button>
79
+ )}
80
+ {onReload !== undefined && (
81
+ <Button type="button" variant="outline" size="sm" onClick={onReload}>
82
+ Reload Inspector
83
+ </Button>
84
+ )}
85
+ </div>
86
+ </div>
87
+ );
88
+
89
+ if (variant === "feature") {
90
+ return content;
91
+ }
92
+
93
+ return (
94
+ <main className="min-h-screen bg-background text-foreground">
95
+ <div className="mx-auto flex min-h-screen w-full max-w-3xl flex-col justify-center px-6 py-16">
96
+ {content}
97
+ </div>
98
+ </main>
99
+ );
100
+ }
101
+
102
+ type SafeErrorBoundaryProps = {
103
+ children: ReactNode;
104
+ scope: string;
105
+ title: string;
106
+ description: string;
107
+ variant?: "page" | "feature";
108
+ onRetry?: () => void;
109
+ };
110
+
111
+ type SafeErrorBoundaryState = {
112
+ caughtAt: number;
113
+ error: unknown;
114
+ failed: boolean;
115
+ };
116
+
117
+ const INITIAL_ERROR_STATE: SafeErrorBoundaryState = {
118
+ caughtAt: 0,
119
+ error: null,
120
+ failed: false,
121
+ };
122
+
123
+ export class SafeErrorBoundary extends Component<SafeErrorBoundaryProps, SafeErrorBoundaryState> {
124
+ override state: SafeErrorBoundaryState = INITIAL_ERROR_STATE;
125
+
126
+ static getDerivedStateFromError(error: unknown): SafeErrorBoundaryState {
127
+ return { caughtAt: Date.now(), error, failed: true };
128
+ }
129
+
130
+ override componentDidCatch(_error: unknown, _errorInfo: ErrorInfo): void {
131
+ // The safe reference is intentionally the only error detail exposed by this boundary.
132
+ }
133
+
134
+ private readonly retry = (): void => {
135
+ this.setState(INITIAL_ERROR_STATE);
136
+ this.props.onRetry?.();
137
+ };
138
+
139
+ override render(): ReactNode {
140
+ if (!this.state.failed) {
141
+ return this.props.children;
142
+ }
143
+
144
+ const reference = createSafeDiagnosticReference(
145
+ this.props.scope,
146
+ this.state.error,
147
+ this.state.caughtAt,
148
+ );
149
+ return (
150
+ <SafeErrorFallback
151
+ reference={reference}
152
+ title={this.props.title}
153
+ description={this.props.description}
154
+ variant={this.props.variant ?? "feature"}
155
+ onRetry={this.retry}
156
+ onReload={() => window.location.reload()}
157
+ />
158
+ );
159
+ }
160
+ }
161
+
162
+ export function LazyFeatureBoundary({
163
+ feature,
164
+ children,
165
+ }: {
166
+ feature: string;
167
+ children: ReactNode;
168
+ }): JSX.Element {
169
+ return (
170
+ <SafeErrorBoundary
171
+ scope={`lazy-${feature}`}
172
+ title={`${feature} could not be displayed`}
173
+ description="The rest of the workspace is still available. Retry this feature or reload Inspector."
174
+ variant="feature"
175
+ >
176
+ {children}
177
+ </SafeErrorBoundary>
178
+ );
179
+ }
180
+
181
+ export function SafeRouteError({
182
+ error,
183
+ reset,
184
+ scope,
185
+ }: {
186
+ error: Error;
187
+ reset: () => void;
188
+ scope: string;
189
+ }): JSX.Element {
190
+ const reference = useMemo(() => createSafeDiagnosticReference(scope, error), [error, scope]);
191
+ return (
192
+ <SafeErrorFallback
193
+ reference={reference}
194
+ title="Inspector workspace could not be displayed"
195
+ description="Agent Inspector stopped this view safely. Retry the route or reload the application."
196
+ variant="page"
197
+ onRetry={reset}
198
+ onReload={() => window.location.reload()}
199
+ />
200
+ );
201
+ }
@@ -1,7 +1,18 @@
1
- import { BrainCircuit, Loader2, Send } from "lucide-react";
1
+ import { BrainCircuit, Loader2, RotateCcw, Send, Sparkles } from "lucide-react";
2
2
  import { useMemo, useState, type ChangeEvent, type JSX } from "react";
3
- import { PiAgentResponseSchema } from "../../lib/piAgentContract";
3
+ import {
4
+ buildPiAgentHistory,
5
+ createEmptyPiAgentConversation,
6
+ normalizePiAgentResponse,
7
+ PI_AGENT_STARTERS,
8
+ type PiAgentConversationMessage,
9
+ type PiAgentEvidenceView,
10
+ type PiAgentMetricView,
11
+ type PiAgentResponseView,
12
+ } from "./piAgentChatLogic";
4
13
  import { useProviders } from "../../lib/useProviders";
14
+ import { dispatchLogFocusRequest } from "../proxy-viewer/logFocus";
15
+ import { AnswerMarkdown } from "../proxy-viewer/AnswerMarkdown";
5
16
  import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "../ui/select";
6
17
 
7
18
  type PiAgentPanelProps = {
@@ -49,25 +60,115 @@ function sortProviderBySource<T extends { name: string; source?: "company" | "pe
49
60
  });
50
61
  }
51
62
 
63
+ function nextMessageId(role: "user" | "assistant"): string {
64
+ return `${role}-${String(Date.now())}-${String(Math.random()).slice(2)}`;
65
+ }
66
+
67
+ function MetricStrip({ metrics }: { metrics: readonly PiAgentMetricView[] }): JSX.Element | null {
68
+ if (metrics.length === 0) return null;
69
+ return (
70
+ <div className="mt-2 grid grid-cols-2 gap-1.5">
71
+ {metrics.slice(0, 8).map((metric) => (
72
+ <div key={metric.key} className="rounded-sm bg-black/20 px-2 py-1">
73
+ <div className="truncate text-[9px] uppercase text-muted-foreground">{metric.label}</div>
74
+ <div className="truncate font-mono text-[11px] text-cyan-50">{metric.value}</div>
75
+ </div>
76
+ ))}
77
+ </div>
78
+ );
79
+ }
80
+
81
+ function EvidenceCard({ evidence }: { evidence: PiAgentEvidenceView }): JSX.Element {
82
+ const openLog = (): void => {
83
+ if (evidence.logId === null) return;
84
+ dispatchLogFocusRequest({ logId: evidence.logId, tab: "anatomy" });
85
+ };
86
+ const title =
87
+ evidence.logId === null
88
+ ? `${evidence.source}:${evidence.id}`
89
+ : `${evidence.source}:log #${String(evidence.logId)}`;
90
+
91
+ return (
92
+ <button
93
+ type="button"
94
+ onClick={openLog}
95
+ disabled={evidence.logId === null}
96
+ 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"
97
+ >
98
+ <div className="flex min-w-0 items-center justify-between gap-2">
99
+ <span className="truncate font-mono text-[10px] text-cyan-100">{title}</span>
100
+ {evidence.score !== null && (
101
+ <span className="shrink-0 font-mono text-[9px] text-muted-foreground">
102
+ {evidence.score.toFixed(1)}
103
+ </span>
104
+ )}
105
+ </div>
106
+ <div className="mt-1 flex flex-wrap gap-x-2 gap-y-1 text-[10px] text-muted-foreground">
107
+ {evidence.providerName !== null && <span>{evidence.providerName}</span>}
108
+ {evidence.model !== null && <span>{evidence.model}</span>}
109
+ {evidence.status !== null && <span>status {String(evidence.status)}</span>}
110
+ {evidence.elapsedMs !== null && <span>{String(evidence.elapsedMs)} ms</span>}
111
+ </div>
112
+ <p className="mt-1 line-clamp-3 text-[11px] leading-4 text-foreground/85">
113
+ {evidence.excerpt}
114
+ </p>
115
+ </button>
116
+ );
117
+ }
118
+
119
+ function AssistantMessage({ response }: { response: PiAgentResponseView }): JSX.Element {
120
+ return (
121
+ <div className="rounded-md bg-black/25 p-3 shadow-[inset_0_0_0_1px_rgba(255,255,255,0.05)]">
122
+ <AnswerMarkdown text={response.answer} />
123
+ <div className="mt-2 break-words font-mono text-[10px] text-cyan-100/70">
124
+ {response.providerName} / {response.model} / {response.protocol} /{" "}
125
+ {String(response.logCount)} matched logs
126
+ </div>
127
+ <MetricStrip metrics={response.summaryMetrics} />
128
+ <MetricStrip metrics={response.queryMetrics} />
129
+ {response.evidence.length > 0 && (
130
+ <div className="mt-2 space-y-1.5">
131
+ {response.evidence.slice(0, 6).map((evidence) => (
132
+ <EvidenceCard key={`${evidence.source}-${evidence.id}`} evidence={evidence} />
133
+ ))}
134
+ </div>
135
+ )}
136
+ </div>
137
+ );
138
+ }
139
+
140
+ function ConversationMessage({ message }: { message: PiAgentConversationMessage }): JSX.Element {
141
+ if (message.role === "assistant" && message.response !== null) {
142
+ return <AssistantMessage response={message.response} />;
143
+ }
144
+ return (
145
+ <div className="rounded-md bg-cyan-400/10 px-3 py-2 text-xs leading-5 text-cyan-50">
146
+ {message.content}
147
+ </div>
148
+ );
149
+ }
150
+
52
151
  export function PiAgentPanel({ currentSessionId, logCount }: PiAgentPanelProps): JSX.Element {
53
152
  const { providers } = useProviders();
54
153
  const piProviders = useMemo(
55
154
  () => sortProviderBySource(providers.filter(providerSupportsPiAgent)),
56
155
  [providers],
57
156
  );
58
- const [question, setQuestion] = useState(DEFAULT_QUESTION);
157
+ const [conversation, setConversation] = useState(() => ({
158
+ ...createEmptyPiAgentConversation(),
159
+ question: DEFAULT_QUESTION,
160
+ }));
59
161
  const [providerId, setProviderId] = useState<string>("__auto__");
60
162
  const [model, setModel] = useState<string>("__auto__");
61
- const [answer, setAnswer] = useState<string>("");
62
- const [status, setStatus] = useState<string>("");
63
163
  const [loading, setLoading] = useState(false);
64
164
 
65
165
  const selectedProvider = piProviders.find((provider) => provider.id === providerId);
66
166
  const modelOptions = selectedProvider?.models ?? [];
167
+ const question = conversation.question;
67
168
  const canAsk = question.trim() !== "" && piProviders.length > 0 && !loading;
68
169
 
69
170
  const handleQuestionChange = (event: ChangeEvent<HTMLTextAreaElement>): void => {
70
- setQuestion(event.currentTarget.value);
171
+ setConversation((current) => ({ ...current, question: event.currentTarget.value }));
71
172
  };
72
173
 
73
174
  const handleProviderChange = (value: string): void => {
@@ -75,48 +176,86 @@ export function PiAgentPanel({ currentSessionId, logCount }: PiAgentPanelProps):
75
176
  setModel("__auto__");
76
177
  };
77
178
 
179
+ const resetConversation = (): void => {
180
+ setConversation({ ...createEmptyPiAgentConversation(), question: DEFAULT_QUESTION });
181
+ };
182
+
183
+ const setStarter = (questionText: string): void => {
184
+ setConversation((current) => ({ ...current, question: questionText, error: null }));
185
+ };
186
+
78
187
  const askPiAgent = (): void => {
79
188
  if (!canAsk) return;
189
+ const trimmedQuestion = question.trim();
190
+ const history = buildPiAgentHistory(conversation.messages);
191
+ const userMessage: PiAgentConversationMessage = {
192
+ id: nextMessageId("user"),
193
+ role: "user",
194
+ content: trimmedQuestion,
195
+ response: null,
196
+ };
197
+
80
198
  setLoading(true);
81
- setStatus("");
82
- setAnswer("");
199
+ setConversation((current) => ({
200
+ ...current,
201
+ messages: [...current.messages, userMessage],
202
+ question: "",
203
+ error: null,
204
+ }));
83
205
 
84
206
  const body = {
85
- question: question.trim(),
207
+ question: trimmedQuestion,
208
+ history,
86
209
  ...(currentSessionId !== null ? { sessionId: currentSessionId } : {}),
87
210
  ...(providerId !== "__auto__" ? { providerId } : {}),
88
211
  ...(model !== "__auto__" ? { model } : {}),
89
- logLimit: 24,
212
+ logLimit: 16,
90
213
  };
91
214
 
92
215
  void fetch("/api/pi-agent", {
93
216
  method: "POST",
94
- headers: { "content-type": "application/json" },
217
+ headers: {
218
+ "content-type": "application/json",
219
+ "x-agent-inspector-csrf": "1",
220
+ },
95
221
  body: JSON.stringify(body),
96
222
  })
97
223
  .then(async (response) => {
98
224
  const raw = await response.text();
99
225
  if (!response.ok) {
100
- setStatus(`Pi Agent failed: HTTP ${String(response.status)}`);
101
- setAnswer(raw);
226
+ setConversation((current) => ({
227
+ ...current,
228
+ error: `Pi Agent failed: HTTP ${String(response.status)} ${raw}`,
229
+ }));
102
230
  return;
103
231
  }
104
232
  const parsedJson: unknown = JSON.parse(raw);
105
- const parsed = PiAgentResponseSchema.safeParse(parsedJson);
106
- if (!parsed.success) {
107
- setStatus("Pi Agent returned an unexpected response.");
108
- setAnswer(raw);
233
+ const normalized = normalizePiAgentResponse(parsedJson);
234
+ if (normalized === null) {
235
+ setConversation((current) => ({
236
+ ...current,
237
+ error: "Pi Agent returned an unexpected response.",
238
+ }));
109
239
  return;
110
240
  }
111
- setStatus(
112
- `${parsed.data.providerName} / ${parsed.data.model} / ${parsed.data.protocol} / ${String(
113
- parsed.data.logCount,
114
- )} logs`,
115
- );
116
- setAnswer(parsed.data.answer);
241
+ setConversation((current) => ({
242
+ ...current,
243
+ messages: [
244
+ ...current.messages,
245
+ {
246
+ id: nextMessageId("assistant"),
247
+ role: "assistant",
248
+ content: normalized.answer,
249
+ response: normalized,
250
+ },
251
+ ],
252
+ }));
117
253
  })
118
254
  .catch((err: unknown) => {
119
- setStatus(err instanceof Error ? err.message : "Pi Agent request failed.");
255
+ setConversation((current) => ({
256
+ ...current,
257
+ error: err instanceof Error ? err.message : "Pi Agent request failed.",
258
+ }));
120
259
  })
121
260
  .finally(() => {
122
261
  setLoading(false);
@@ -130,9 +269,17 @@ export function PiAgentPanel({ currentSessionId, logCount }: PiAgentPanelProps):
130
269
  <div className="min-w-0 flex-1">
131
270
  <div className="text-xs font-semibold text-foreground">Pi Agent</div>
132
271
  <div className="text-[11px] text-muted-foreground">
133
- {currentSessionId === null ? "Inspector-wide analysis" : "Session analysis"}
272
+ {currentSessionId === null ? "Inspector-wide ChatBI" : "Session ChatBI"}
134
273
  </div>
135
274
  </div>
275
+ <button
276
+ type="button"
277
+ onClick={resetConversation}
278
+ className="inline-flex size-7 items-center justify-center rounded-md bg-black/25 text-muted-foreground transition-colors hover:text-foreground"
279
+ title="Reset conversation"
280
+ >
281
+ <RotateCcw className="size-3.5" />
282
+ </button>
136
283
  </div>
137
284
 
138
285
  <div className="mt-3 grid grid-cols-2 gap-2">
@@ -164,11 +311,43 @@ export function PiAgentPanel({ currentSessionId, logCount }: PiAgentPanelProps):
164
311
  </Select>
165
312
  </div>
166
313
 
314
+ {conversation.messages.length === 0 && (
315
+ <div className="mt-3 flex flex-wrap gap-1.5">
316
+ {PI_AGENT_STARTERS.map((starter) => (
317
+ <button
318
+ key={starter.id}
319
+ type="button"
320
+ onClick={() => {
321
+ setStarter(starter.question);
322
+ }}
323
+ className="inline-flex h-7 items-center gap-1.5 rounded-sm bg-black/25 px-2 text-[11px] text-cyan-50 transition-colors hover:bg-cyan-400/12"
324
+ >
325
+ <Sparkles className="size-3" />
326
+ <span>{starter.label}</span>
327
+ </button>
328
+ ))}
329
+ </div>
330
+ )}
331
+
332
+ {conversation.messages.length > 0 && (
333
+ <div className="mt-3 max-h-96 space-y-2 overflow-y-auto pr-1">
334
+ {conversation.messages.map((message) => (
335
+ <ConversationMessage key={message.id} message={message} />
336
+ ))}
337
+ {loading && (
338
+ <div className="inline-flex items-center gap-2 rounded-md bg-black/25 px-3 py-2 text-xs text-muted-foreground">
339
+ <Loader2 className="size-3.5 animate-spin" />
340
+ <span>Searching Inspector...</span>
341
+ </div>
342
+ )}
343
+ </div>
344
+ )}
345
+
167
346
  <textarea
168
347
  value={question}
169
348
  onChange={handleQuestionChange}
170
- className="mt-2 min-h-24 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"
171
- placeholder="Ask Pi Agent about this session..."
349
+ 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"
350
+ placeholder="Ask Pi Agent across saved Inspector data..."
172
351
  />
173
352
 
174
353
  <button
@@ -181,19 +360,14 @@ export function PiAgentPanel({ currentSessionId, logCount }: PiAgentPanelProps):
181
360
  <span>{loading ? "Thinking..." : "Ask Pi"}</span>
182
361
  </button>
183
362
 
363
+ {conversation.error !== null && (
364
+ <p className="mt-2 break-words text-[11px] leading-5 text-red-200">{conversation.error}</p>
365
+ )}
184
366
  {piProviders.length === 0 && (
185
367
  <p className="mt-2 text-[11px] leading-5 text-muted-foreground">
186
368
  Configure a Provider with Responses, Chat, or Anthropic URL to enable Pi Agent.
187
369
  </p>
188
370
  )}
189
- {status !== "" && (
190
- <p className="mt-2 break-words font-mono text-[10px] text-cyan-100/70">{status}</p>
191
- )}
192
- {answer !== "" && (
193
- <div className="mt-3 max-h-64 overflow-y-auto whitespace-pre-wrap rounded-md bg-black/25 p-3 text-xs leading-5 text-foreground shadow-[inset_0_0_0_1px_rgba(255,255,255,0.05)]">
194
- {answer}
195
- </div>
196
- )}
197
371
  {logCount === 0 && (
198
372
  <p className="mt-2 text-[11px] leading-5 text-muted-foreground">
199
373
  No logs are visible in this scope yet; Pi Agent can still answer Provider setup questions.