Skip to content

Commit 446b9a9

Browse files
gabe4codinggabrypavanelloclaude
authored
feat(inspector): primitive execution & always-visible event logs (#160)
* Update .gitignore to include board directory and restore PROMPT.md entry * feat(inspector): add manual event types and /api/execute-primitive endpoint - Add "manual" to AgnosticInspectorEvent.source union - Add 6 manual event types (manual_tool_call, manual_tool_result, etc.) - Map manual events to "agent" category in getEventCategory/getEventSummary - Add POST /api/execute-primitive endpoint dispatching by kind (tool/resource/prompt) - Record manual events before/after execution with source: "manual" - 30s default timeout, overrideable via request body - Extend recordAgentEvent with optional source parameter Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * feat(inspector): make RightPanel always-visible with all three tabs - Remove isStreaming from RightPanelProps and all usages - Always render Agent, Events, and Logs tabs - Replace handleClear/isClearDisabled with switch(activeTab) dispatch - Update right-panel tests for always-visible behavior Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * feat(inspector): enable browse-mode action button when onExecute provided - Action button enabled with primary style when onExecute prop exists - onClick switches to action mode (shows form), does not execute immediately - Remains disabled with "Coming soon" when onExecute is absent Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * feat(inspector): add executePrimitive utility with per-kind response mappers - mapToolResponse: maps tool call results to ExecutionResult (ok=!isError) - mapResourceResponse: maps resource read results with uri+mimeType - mapPromptResponse: maps prompt messages with role/content handling - executePrimitive: fetches /api/execute-primitive, dispatches to correct mapper - All mappers never throw, always return ExecutionResult Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * feat(inspector): wire onExecute through InspectorDashboard to PrimitiveDetail - Create handleExecutePrimitive in InspectorDashboard binding serverId as connectionId - Add onExecute prop to McpPrimitivesPanel and thread to PrimitiveDetail - selectedPrimitive.serverId used as connectionId for execute-primitive API Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * feat(inspector): add visual badge for manual events in AgentPanel - Show "MANUAL" badge on events with source: "manual" - Add manualBadge styles (amber accent color) - Manual events appear in Agent panel with visual distinction Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * test(inspector): add unit tests for executePrimitive mappers - mapToolResponse: success content, error flag, empty data - mapResourceResponse: contents mapping, empty data - mapPromptResponse: messages with roles, empty data - executePrimitive: success flow, network error, non-200 response Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * feat(inspector): add executePrimitive utility and wire onExecute through component tree - executePrimitive with per-kind mappers (tool/resource/prompt) - createExecuteFn binds baseUrl + connectionId for ExecuteFn signature - Wire onExecute through InspectorDashboard -> McpPrimitivesPanel -> PrimitiveDetail - Thread baseUrl through to fetch calls (consistent with other hooks) - 22 unit tests for mappers and executePrimitive function Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix(inspector): clear timeout timer after execute-primitive completes - Wrap Promise.race in try/finally to clear setTimeout on success - Prevents timer leak on successful executions Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * test(inspector): add endpoint integration and manual event type tests - Backend /api/execute-primitive endpoint integration tests - Manual event type mapping tests (getEventCategory, getEventSummary) - VALID_INSPECTOR_EVENT_TYPES validation for all 6 manual types - 83 additional tests for comprehensive coverage Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: address PR review feedback (redundant ternary, switch default, JSDoc) - Remove no-op ternary for structuredContent in mapToolResponse - Add exhaustive default case to kind switch in executePrimitive - Add @param JSDoc for source parameter in recordAgentEvent Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: gabrypavanello <gabry.pavanello@gmail.com> Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 35b6093 commit 446b9a9

15 files changed

+2466
-366
lines changed

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,4 +56,5 @@ specs/
5656
.pnpm-store/
5757
.agent/
5858
ralph.yml
59-
PROMPT.md
59+
PROMPT.md
60+
/board/

packages/inspector/src/connection.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1125,19 +1125,21 @@ export class ConnectionManager extends EventEmitter {
11251125
* @param type - Event type (agent-tool-call or agent-tool-result)
11261126
* @param payload - Event payload (tool name, args, result, etc.)
11271127
* @param protocol - Protocol used (mcp or openai)
1128+
* @param source - Event source: "agent" (default) for proxy/agent calls, "manual" for user-initiated executions
11281129
*/
11291130
recordAgentEvent(
11301131
type: InspectorEventType,
11311132
payload: unknown,
1132-
protocol?: "mcp" | "openai"
1133+
protocol?: "mcp" | "openai",
1134+
source?: AgnosticInspectorEvent["source"]
11331135
): AgnosticInspectorEvent {
11341136
const event: AgnosticInspectorEvent = {
11351137
id: `agent-${++this.agentEventIdCounter}`,
11361138
category: getEventCategory(type),
11371139
type,
11381140
timestamp: Date.now(),
11391141
payload,
1140-
source: "agent",
1142+
source: source ?? "agent",
11411143
protocol,
11421144
};
11431145

packages/inspector/src/dashboard/react/InspectorDashboard.tsx

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,8 @@ import {
2727
type StoppedConnection,
2828
type SelectedPrimitive,
2929
} from "./components/McpPrimitivesPanel";
30-
import type { Primitive } from "./components/PrimitiveDetail";
30+
import type { Primitive, ExecuteFn } from "./components/PrimitiveDetail";
31+
import { createExecuteFn } from "./utils/executePrimitive";
3132
import { RightPanel } from "./components/RightPanel";
3233
import { NoWidgetPlaceholder, type ConnectionState } from "./components/NoWidgetPlaceholder";
3334
import { OAuthDiscoveryPanel } from "./components/OAuthDiscoveryPanel";
@@ -566,6 +567,12 @@ export function InspectorDashboard({ baseUrl = "" }: InspectorDashboardProps): R
566567
setSelectedPrimitive(primitive);
567568
}, []);
568569

570+
// Create an ExecuteFn bound to the selected primitive's server (connectionId)
571+
const handleExecutePrimitive: ExecuteFn | undefined = useMemo(() => {
572+
if (!selectedPrimitive) return undefined;
573+
return createExecuteFn(baseUrl, selectedPrimitive.serverId);
574+
}, [baseUrl, selectedPrimitive]);
575+
569576
const handleCreateConnection = useCallback(
570577
async (params: import("@mcp-apps-kit/testing").ConnectionParams): Promise<boolean> => {
571578
const conn = await createConnection(params);
@@ -808,6 +815,7 @@ export function InspectorDashboard({ baseUrl = "" }: InspectorDashboardProps): R
808815
onSelectPrimitive={handleSelectPrimitive}
809816
resolvedPrimitive={resolvedPrimitive}
810817
onClosePrimitive={() => setSelectedPrimitive(null)}
818+
onExecute={handleExecutePrimitive}
811819
/>
812820

813821
{/* Center Column - screencast + globals bar */}
@@ -868,7 +876,6 @@ export function InspectorDashboard({ baseUrl = "" }: InspectorDashboardProps): R
868876
panelWidth={rightPanelWidth}
869877
resizeHandleProps={rightResizeHandleProps}
870878
isResizing={isRightResizing}
871-
isStreaming={isStreaming}
872879
/>
873880
</div>
874881

packages/inspector/src/dashboard/react/components/EventRow.tsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,9 @@ export function EventRow({ event, isAgentView = false }: EventRowProps): React.R
184184
const isAgentOutput = event.type === "agent-tool-result";
185185
const isAgentEvent = isAgentInput || isAgentOutput;
186186

187+
// Determine if this is a manual event (source === "manual")
188+
const isManualEvent = "source" in event && event.source === "manual";
189+
187190
// Extract reasoning (only for agent-tool-call)
188191
const reasoning = isAgentInput ? getPayloadReasoning(event.payload) : undefined;
189192

@@ -231,6 +234,9 @@ export function EventRow({ event, isAgentView = false }: EventRowProps): React.R
231234
{event.category}
232235
</span>
233236
)}
237+
{isManualEvent && (
238+
<span style={styles.eventBadgeManual as React.CSSProperties}>manual</span>
239+
)}
234240
{isAgentEvent && (
235241
<span
236242
style={{

packages/inspector/src/dashboard/react/components/McpPrimitivesPanel.tsx

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import type {
1616
} from "../types/mcp-primitives";
1717
import type { ConnectionParams } from "@mcp-apps-kit/testing";
1818
import { SidebarConnectionForm } from "./SidebarConnectionForm";
19-
import { PrimitiveDetail, type Primitive } from "./PrimitiveDetail";
19+
import { PrimitiveDetail, type Primitive, type ExecuteFn } from "./PrimitiveDetail";
2020

2121
// =============================================================================
2222
// Types
@@ -117,6 +117,8 @@ export interface McpPrimitivesPanelNewProps {
117117
resolvedPrimitive?: Primitive | null;
118118
/** Callback to close the primitive detail */
119119
onClosePrimitive?: () => void;
120+
/** Execute function for primitive execution (passed to PrimitiveDetail) */
121+
onExecute?: ExecuteFn;
120122
}
121123

122124
/** Legacy API props for backward compatibility with tests */
@@ -1781,6 +1783,7 @@ function ServerBlocksContent({
17811783
onSelectPrimitive,
17821784
resolvedPrimitive,
17831785
onClosePrimitive,
1786+
onExecute,
17841787
}: McpPrimitivesPanelNewProps): React.ReactElement {
17851788
const [searchFilter, setSearchFilter] = useState("");
17861789
const [isFormOpen, setIsFormOpen] = useState(false);
@@ -1973,7 +1976,11 @@ function ServerBlocksContent({
19731976
{/* Slide-over detail panel */}
19741977
<SlideOverDetail isVisible={!!resolvedPrimitive}>
19751978
{resolvedPrimitive && (
1976-
<PrimitiveDetail primitive={resolvedPrimitive} onClose={onClosePrimitive} />
1979+
<PrimitiveDetail
1980+
primitive={resolvedPrimitive}
1981+
onClose={onClosePrimitive}
1982+
onExecute={onExecute}
1983+
/>
19771984
)}
19781985
</SlideOverDetail>
19791986
</div>

packages/inspector/src/dashboard/react/components/PrimitiveDetail.tsx

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1395,14 +1395,25 @@ export function PrimitiveDetail({
13951395
{mode === "browse" && (
13961396
<div style={styles.footer} data-testid="primitive-detail-footer">
13971397
<CopyJsonButton data={primitiveData} />
1398-
<button
1399-
style={{ ...styles.button, ...styles.buttonDisabled }}
1400-
disabled
1401-
title="Coming soon"
1402-
data-testid="action-btn"
1403-
>
1404-
{icon} {label}
1405-
</button>
1398+
{onExecute ? (
1399+
<button
1400+
style={{ ...styles.button, ...styles.buttonPrimary }}
1401+
onClick={() => setMode("action")}
1402+
title={`${label} this ${primitive.kind}`}
1403+
data-testid="action-btn"
1404+
>
1405+
{icon} {label}
1406+
</button>
1407+
) : (
1408+
<button
1409+
style={{ ...styles.button, ...styles.buttonDisabled }}
1410+
disabled
1411+
title="Coming soon"
1412+
data-testid="action-btn"
1413+
>
1414+
{icon} {label}
1415+
</button>
1416+
)}
14061417
</div>
14071418
)}
14081419
</div>

packages/inspector/src/dashboard/react/components/RightPanel.tsx

Lines changed: 46 additions & 79 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,6 @@ export interface RightPanelProps {
2626
panelWidth: number;
2727
resizeHandleProps: React.HTMLAttributes<HTMLDivElement>;
2828
isResizing: boolean;
29-
isStreaming: boolean;
3029
}
3130

3231
export function RightPanel({
@@ -41,7 +40,6 @@ export function RightPanel({
4140
panelWidth,
4241
resizeHandleProps,
4342
isResizing,
44-
isStreaming,
4543
}: RightPanelProps): React.ReactElement {
4644
const noop = (): void => undefined;
4745
const [activeTab, setActiveTab] = useState<RightPanelTab>("agent");
@@ -65,24 +63,26 @@ export function RightPanel({
6563
return <div style={panelStyle} />;
6664
}
6765

68-
const handleClear = (() => {
69-
if (!isStreaming || activeTab === "agent") {
70-
return onClearAgent ?? noop;
66+
const handleClear = ((): (() => void) => {
67+
switch (activeTab) {
68+
case "agent":
69+
return onClearAgent ?? noop;
70+
case "events":
71+
return onClearEvents ?? noop;
72+
case "logs":
73+
return onClearLogs ?? noop;
7174
}
72-
if (activeTab === "events") {
73-
return onClearEvents ?? noop;
74-
}
75-
return onClearLogs ?? noop;
7675
})();
7776

78-
const isClearDisabled = (() => {
79-
if (!isStreaming || activeTab === "agent") {
80-
return !onClearAgent;
81-
}
82-
if (activeTab === "events") {
83-
return !onClearEvents;
77+
const isClearDisabled = ((): boolean => {
78+
switch (activeTab) {
79+
case "agent":
80+
return !onClearAgent;
81+
case "events":
82+
return !onClearEvents;
83+
case "logs":
84+
return !onClearLogs;
8485
}
85-
return !onClearLogs;
8686
})();
8787

8888
return (
@@ -105,52 +105,31 @@ export function RightPanel({
105105
>
106106
107107
</button>
108-
{isStreaming ? (
109-
<div style={styles.rightPanelTabs}>
110-
{tabs.map((tab) => (
111-
<button
112-
key={tab.id}
113-
style={{
114-
...styles.rightPanelTab,
115-
...(activeTab === tab.id ? styles.rightPanelTabActive : {}),
116-
}}
117-
onClick={() => setActiveTab(tab.id)}
118-
aria-pressed={activeTab === tab.id}
119-
>
120-
{tab.label}
121-
{tab.count > 0 && (
122-
<span
123-
style={{
124-
...styles.rightPanelTabCount,
125-
...(activeTab === tab.id ? styles.rightPanelTabCountActive : {}),
126-
}}
127-
>
128-
{tab.count}
129-
</span>
130-
)}
131-
</button>
132-
))}
133-
</div>
134-
) : (
135-
<div style={styles.rightPanelTabs}>
136-
<span
108+
<div style={styles.rightPanelTabs}>
109+
{tabs.map((tab) => (
110+
<button
111+
key={tab.id}
137112
style={{
138113
...styles.rightPanelTab,
139-
...styles.rightPanelTabActive,
140-
cursor: "default",
114+
...(activeTab === tab.id ? styles.rightPanelTabActive : {}),
141115
}}
116+
onClick={() => setActiveTab(tab.id)}
117+
aria-pressed={activeTab === tab.id}
142118
>
143-
Agent Logs
144-
{agentEvents.length > 0 && (
119+
{tab.label}
120+
{tab.count > 0 && (
145121
<span
146-
style={{ ...styles.rightPanelTabCount, ...styles.rightPanelTabCountActive }}
122+
style={{
123+
...styles.rightPanelTabCount,
124+
...(activeTab === tab.id ? styles.rightPanelTabCountActive : {}),
125+
}}
147126
>
148-
{agentEvents.length}
127+
{tab.count}
149128
</span>
150129
)}
151-
</span>
152-
</div>
153-
)}
130+
</button>
131+
))}
132+
</div>
154133
<div style={styles.rightPanelActions}>
155134
<button
156135
style={styles.rightPanelClearBtn}
@@ -162,31 +141,19 @@ export function RightPanel({
162141
</div>
163142
</div>
164143
<div style={styles.rightPanelContent}>
165-
{isStreaming ? (
166-
<>
167-
{activeTab === "logs" && (
168-
<LogsPanel logs={logs} onClearLogs={onClearLogs ?? noop} showHeader={false} />
169-
)}
170-
{activeTab === "events" && (
171-
<EventsPanel
172-
events={events}
173-
onClearEvents={onClearEvents ?? noop}
174-
showHeader={true}
175-
showTitle={false}
176-
showClearButton={false}
177-
/>
178-
)}
179-
{activeTab === "agent" && (
180-
<AgentPanel
181-
events={agentEvents}
182-
onClearEvents={onClearAgent ?? noop}
183-
showHeader={true}
184-
showTitle={false}
185-
showClearButton={false}
186-
/>
187-
)}
188-
</>
189-
) : (
144+
{activeTab === "logs" && (
145+
<LogsPanel logs={logs} onClearLogs={onClearLogs ?? noop} showHeader={false} />
146+
)}
147+
{activeTab === "events" && (
148+
<EventsPanel
149+
events={events}
150+
onClearEvents={onClearEvents ?? noop}
151+
showHeader={true}
152+
showTitle={false}
153+
showClearButton={false}
154+
/>
155+
)}
156+
{activeTab === "agent" && (
190157
<AgentPanel
191158
events={agentEvents}
192159
onClearEvents={onClearAgent ?? noop}

packages/inspector/src/dashboard/react/styles.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -831,6 +831,20 @@ export const styles: Record<string, CSSProperties> = {
831831
color: "#ff9800",
832832
},
833833

834+
// Manual event badge (for source === "manual")
835+
eventBadgeManual: {
836+
fontSize: "0.5rem",
837+
padding: "0.0625rem 0.25rem",
838+
borderRadius: "2px",
839+
textTransform: "uppercase",
840+
fontWeight: 600,
841+
letterSpacing: "0.02em",
842+
flexShrink: 0,
843+
backgroundColor: "rgba(168, 85, 247, 0.15)",
844+
color: "#a855f7",
845+
border: "1px solid rgba(168, 85, 247, 0.3)",
846+
},
847+
834848
eventType: {
835849
fontSize: "0.625rem",
836850
color: "#6b7280",

0 commit comments

Comments
 (0)