Skip to content

Commit d11b656

Browse files
Refactor basic-host to use ServerSelect component
Extract server dropdown into ServerSelect component and simplify ToolCallInfoPanel to read server name from toolCallInfo.serverInfo. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <[email protected]>
1 parent 8bff1df commit d11b656

File tree

2 files changed

+112
-104
lines changed

2 files changed

+112
-104
lines changed

examples/basic-host/src/index.module.css

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,3 @@
1-
.connecting {
2-
padding: 1rem 0;
3-
text-align: center;
4-
color: #666;
5-
}
6-
71
.callToolPanel, .toolCallInfoPanel {
82
margin: 0 auto;
93
padding: 1rem;
@@ -16,9 +10,6 @@
1610
}
1711

1812
.callToolPanel {
19-
display: flex;
20-
flex-direction: column;
21-
gap: 1rem;
2213
max-width: 480px;
2314

2415
form {
@@ -39,12 +30,16 @@
3930
padding: 0.5rem;
4031
border: 1px solid #ccc;
4132
border-radius: 4px;
42-
font-family: monospace;
4333
font-size: inherit;
4434
}
4535

46-
textarea {
36+
.toolSelect {
37+
font-family: monospace;
38+
}
39+
40+
.toolInput {
4741
min-height: 6rem;
42+
font-family: monospace;
4843
resize: vertical;
4944

5045
&[aria-invalid="true"] {
@@ -92,10 +87,15 @@
9287
gap: 0.5rem;
9388
min-width: 0;
9489

95-
.toolName {
90+
h2 {
91+
display: flex;
92+
flex-direction: column;
9693
margin: 0;
97-
font-family: monospace;
9894
font-size: 1.5rem;
95+
96+
.toolName {
97+
font-family: monospace;
98+
}
9999
}
100100
}
101101

examples/basic-host/src/index.tsx

Lines changed: 99 additions & 91 deletions
Original file line numberDiff line numberDiff line change
@@ -4,84 +4,39 @@ import { callTool, connectToServer, hasAppHtml, initializeApp, loadSandboxProxy,
44
import styles from "./index.module.css";
55

66

7-
// Wrapper to track server name with each tool call
8-
interface ToolCallEntry {
9-
serverName: string;
10-
info: ToolCallInfo;
11-
}
12-
13-
// Host receives connected servers via promise, uses single use() call
7+
// Host passes serversPromise to CallToolPanel
148
interface HostProps {
159
serversPromise: Promise<ServerInfo[]>;
1610
}
1711
function Host({ serversPromise }: HostProps) {
18-
const servers = use(serversPromise);
19-
const [toolCalls, setToolCalls] = useState<ToolCallEntry[]>([]);
20-
21-
if (servers.length === 0) {
22-
return <p>No servers configured. Set SERVERS environment variable.</p>;
23-
}
12+
const [toolCalls, setToolCalls] = useState<ToolCallInfo[]>([]);
2413

2514
return (
2615
<>
27-
{toolCalls.map((entry, i) => (
28-
<ToolCallInfoPanel key={i} serverName={entry.serverName} toolCallInfo={entry.info} />
16+
{toolCalls.map((info, i) => (
17+
<ToolCallInfoPanel key={i} toolCallInfo={info} />
2918
))}
3019
<CallToolPanel
31-
servers={servers}
32-
addToolCall={(serverName, info) => setToolCalls([...toolCalls, { serverName, info }])}
20+
serversPromise={serversPromise}
21+
addToolCall={(info) => setToolCalls([...toolCalls, info])}
3322
/>
3423
</>
3524
);
3625
}
3726

3827

39-
// CallToolPanel manages server selection from already-connected servers
28+
// CallToolPanel renders the unified form with Suspense around ServerSelect
4029
interface CallToolPanelProps {
41-
servers: ServerInfo[];
42-
addToolCall: (serverName: string, info: ToolCallInfo) => void;
43-
}
44-
function CallToolPanel({ servers, addToolCall }: CallToolPanelProps) {
45-
const [selectedIndex, setSelectedIndex] = useState(0);
46-
const selectedServer = servers[selectedIndex];
47-
48-
return (
49-
<div className={styles.callToolPanel}>
50-
<label>
51-
Server
52-
<select
53-
value={selectedIndex}
54-
onChange={(e) => setSelectedIndex(Number(e.target.value))}
55-
>
56-
{servers.map((server, i) => (
57-
<option key={i} value={i}>
58-
{server.name}
59-
</option>
60-
))}
61-
</select>
62-
</label>
63-
<ToolCallForm
64-
key={selectedIndex}
65-
serverName={selectedServer.name}
66-
serverInfo={selectedServer}
67-
addToolCall={addToolCall}
68-
/>
69-
</div>
70-
);
71-
}
72-
73-
74-
// ToolCallForm receives already-resolved serverInfo
75-
interface ToolCallFormProps {
76-
serverName: string;
77-
serverInfo: ServerInfo;
78-
addToolCall: (serverName: string, info: ToolCallInfo) => void;
30+
serversPromise: Promise<ServerInfo[]>;
31+
addToolCall: (info: ToolCallInfo) => void;
7932
}
80-
function ToolCallForm({ serverName, serverInfo, addToolCall }: ToolCallFormProps) {
81-
const toolNames = Array.from(serverInfo.tools.keys());
82-
const [selectedTool, setSelectedTool] = useState(toolNames[0] ?? "");
33+
function CallToolPanel({ serversPromise, addToolCall }: CallToolPanelProps) {
34+
const [selectedServer, setSelectedServer] = useState<ServerInfo | null>(null);
35+
const [selectedTool, setSelectedTool] = useState("");
8336
const [inputJson, setInputJson] = useState("{}");
8437

38+
const toolNames = selectedServer ? Array.from(selectedServer.tools.keys()) : [];
39+
8540
const isValidJson = useMemo(() => {
8641
try {
8742
JSON.parse(inputJson);
@@ -91,49 +46,104 @@ function ToolCallForm({ serverName, serverInfo, addToolCall }: ToolCallFormProps
9146
}
9247
}, [inputJson]);
9348

49+
const handleServerSelect = (server: ServerInfo) => {
50+
setSelectedServer(server);
51+
const [firstTool] = server.tools.keys();
52+
setSelectedTool(firstTool ?? "");
53+
};
54+
9455
const handleSubmit = () => {
95-
const toolCallInfo = callTool(serverInfo, selectedTool, JSON.parse(inputJson));
96-
addToolCall(serverName, toolCallInfo);
56+
if (!selectedServer) return;
57+
const toolCallInfo = callTool(selectedServer, selectedTool, JSON.parse(inputJson));
58+
addToolCall(toolCallInfo);
9759
};
9860

9961
return (
100-
<form onSubmit={(e) => { e.preventDefault(); handleSubmit(); }}>
101-
<label>
102-
Tool
103-
<select
104-
value={selectedTool}
105-
onChange={(e) => setSelectedTool(e.target.value)}
106-
>
107-
{toolNames.map((name) => (
108-
<option key={name} value={name}>{name}</option>
109-
))}
110-
</select>
111-
</label>
112-
<label>
113-
Input
114-
<textarea
115-
aria-invalid={!isValidJson}
116-
value={inputJson}
117-
onChange={(e) => setInputJson(e.target.value)}
118-
/>
119-
</label>
120-
<button type="submit" disabled={!selectedTool || !isValidJson}>
121-
Call Tool
122-
</button>
123-
</form>
62+
<div className={styles.callToolPanel}>
63+
<form onSubmit={(e) => { e.preventDefault(); handleSubmit(); }}>
64+
<label>
65+
Server
66+
<Suspense fallback={<select disabled><option>Loading...</option></select>}>
67+
<ServerSelect serversPromise={serversPromise} onSelect={handleServerSelect} />
68+
</Suspense>
69+
</label>
70+
<label>
71+
Tool
72+
<select
73+
className={styles.toolSelect}
74+
value={selectedTool}
75+
onChange={(e) => setSelectedTool(e.target.value)}
76+
>
77+
{selectedServer && toolNames.map((name) => (
78+
<option key={name} value={name}>{name}</option>
79+
))}
80+
</select>
81+
</label>
82+
<label>
83+
Input
84+
<textarea
85+
className={styles.toolInput}
86+
aria-invalid={!isValidJson}
87+
value={inputJson}
88+
onChange={(e) => setInputJson(e.target.value)}
89+
/>
90+
</label>
91+
<button type="submit" disabled={!selectedTool || !isValidJson}>
92+
Call Tool
93+
</button>
94+
</form>
95+
</div>
96+
);
97+
}
98+
99+
100+
// ServerSelect calls use() and renders the server <select>
101+
interface ServerSelectProps {
102+
serversPromise: Promise<ServerInfo[]>;
103+
onSelect: (server: ServerInfo) => void;
104+
}
105+
function ServerSelect({ serversPromise, onSelect }: ServerSelectProps) {
106+
const servers = use(serversPromise);
107+
const [selectedIndex, setSelectedIndex] = useState(0);
108+
109+
useEffect(() => {
110+
if (servers.length > selectedIndex) {
111+
onSelect(servers[selectedIndex]);
112+
}
113+
}, [servers]);
114+
115+
if (servers.length === 0) {
116+
return <select disabled><option>No servers configured</option></select>;
117+
}
118+
119+
return (
120+
<select
121+
value={selectedIndex}
122+
onChange={(e) => {
123+
const newIndex = Number(e.target.value);
124+
setSelectedIndex(newIndex);
125+
onSelect(servers[newIndex]);
126+
}}
127+
>
128+
{servers.map((server, i) => (
129+
<option key={i} value={i}>{server.name}</option>
130+
))}
131+
</select>
124132
);
125133
}
126134

127135

128136
interface ToolCallInfoPanelProps {
129-
serverName: string;
130137
toolCallInfo: ToolCallInfo;
131138
}
132-
function ToolCallInfoPanel({ serverName, toolCallInfo }: ToolCallInfoPanelProps) {
139+
function ToolCallInfoPanel({ toolCallInfo }: ToolCallInfoPanelProps) {
133140
return (
134141
<div className={styles.toolCallInfoPanel}>
135142
<div className={styles.inputInfoPanel}>
136-
<h2 className={styles.toolName}>{serverName}:{toolCallInfo.tool.name}</h2>
143+
<h2>
144+
<span>{toolCallInfo.serverInfo.name}</span>
145+
<span className={styles.toolName}>{toolCallInfo.tool.name}</span>
146+
</h2>
137147
<JsonBlock value={toolCallInfo.input} />
138148
</div>
139149
<div className={styles.outputInfoPanel}>
@@ -239,9 +249,7 @@ async function connectToAllServers(): Promise<ServerInfo[]> {
239249
createRoot(document.getElementById("root")!).render(
240250
<StrictMode>
241251
<ErrorBoundary>
242-
<Suspense fallback={<p className={styles.connecting}>Connecting to servers...</p>}>
243-
<Host serversPromise={connectToAllServers()} />
244-
</Suspense>
252+
<Host serversPromise={connectToAllServers()} />
245253
</ErrorBoundary>
246254
</StrictMode>,
247255
);

0 commit comments

Comments
 (0)