Skip to content

Commit 2e8cc56

Browse files
committed
Merge remote-tracking branch 'theirs/main' into max/default-tool-json
2 parents 3e95d9d + 70dc1b7 commit 2e8cc56

File tree

15 files changed

+346
-123
lines changed

15 files changed

+346
-123
lines changed

client/src/components/DynamicJsonForm.tsx

Lines changed: 2 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -2,33 +2,9 @@ import { useState, useEffect, useCallback, useRef } from "react";
22
import { Button } from "@/components/ui/button";
33
import { Input } from "@/components/ui/input";
44
import JsonEditor from "./JsonEditor";
5-
import { updateValueAtPath } from "@/utils/jsonPathUtils";
5+
import { updateValueAtPath } from "@/utils/jsonUtils";
66
import { generateDefaultValue } from "@/utils/schemaUtils";
7-
8-
export type JsonValue =
9-
| string
10-
| number
11-
| boolean
12-
| null
13-
| undefined
14-
| JsonValue[]
15-
| { [key: string]: JsonValue };
16-
17-
export type JsonSchemaType = {
18-
type:
19-
| "string"
20-
| "number"
21-
| "integer"
22-
| "boolean"
23-
| "array"
24-
| "object"
25-
| "null";
26-
description?: string;
27-
required?: boolean;
28-
default?: JsonValue;
29-
properties?: Record<string, JsonSchemaType>;
30-
items?: JsonSchemaType;
31-
};
7+
import type { JsonValue, JsonSchemaType } from "@/utils/jsonUtils";
328

339
interface DynamicJsonFormProps {
3410
schema: JsonSchemaType;

client/src/components/JsonView.tsx

Lines changed: 28 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,18 @@
11
import { useState, memo, useMemo, useCallback, useEffect } from "react";
2-
import { JsonValue } from "./DynamicJsonForm";
2+
import type { JsonValue } from "@/utils/jsonUtils";
33
import clsx from "clsx";
44
import { Copy, CheckCheck } from "lucide-react";
55
import { Button } from "@/components/ui/button";
66
import { useToast } from "@/hooks/use-toast";
7+
import { getDataType, tryParseJson } from "@/utils/jsonUtils";
78

89
interface JsonViewProps {
910
data: unknown;
1011
name?: string;
1112
initialExpandDepth?: number;
1213
className?: string;
1314
withCopyButton?: boolean;
14-
}
15-
16-
function tryParseJson(str: string): { success: boolean; data: JsonValue } {
17-
const trimmed = str.trim();
18-
if (
19-
!(trimmed.startsWith("{") && trimmed.endsWith("}")) &&
20-
!(trimmed.startsWith("[") && trimmed.endsWith("]"))
21-
) {
22-
return { success: false, data: str };
23-
}
24-
try {
25-
return { success: true, data: JSON.parse(str) };
26-
} catch {
27-
return { success: false, data: str };
28-
}
15+
isError?: boolean;
2916
}
3017

3118
const JsonView = memo(
@@ -35,6 +22,7 @@ const JsonView = memo(
3522
initialExpandDepth = 3,
3623
className,
3724
withCopyButton = true,
25+
isError = false,
3826
}: JsonViewProps) => {
3927
const { toast } = useToast();
4028
const [copied, setCopied] = useState(false);
@@ -100,6 +88,7 @@ const JsonView = memo(
10088
name={name}
10189
depth={0}
10290
initialExpandDepth={initialExpandDepth}
91+
isError={isError}
10392
/>
10493
</div>
10594
</div>
@@ -114,28 +103,28 @@ interface JsonNodeProps {
114103
name?: string;
115104
depth: number;
116105
initialExpandDepth: number;
106+
isError?: boolean;
117107
}
118108

119109
const JsonNode = memo(
120-
({ data, name, depth = 0, initialExpandDepth }: JsonNodeProps) => {
110+
({
111+
data,
112+
name,
113+
depth = 0,
114+
initialExpandDepth,
115+
isError = false,
116+
}: JsonNodeProps) => {
121117
const [isExpanded, setIsExpanded] = useState(depth < initialExpandDepth);
122-
123-
const getDataType = (value: JsonValue): string => {
124-
if (Array.isArray(value)) return "array";
125-
if (value === null) return "null";
126-
return typeof value;
127-
};
128-
129-
const dataType = getDataType(data);
130-
131-
const typeStyleMap: Record<string, string> = {
118+
const [typeStyleMap] = useState<Record<string, string>>({
132119
number: "text-blue-600",
133120
boolean: "text-amber-600",
134121
null: "text-purple-600",
135122
undefined: "text-gray-600",
136-
string: "text-green-600 break-all whitespace-pre-wrap",
123+
string: "text-green-600 group-hover:text-green-500",
124+
error: "text-red-600 group-hover:text-red-500",
137125
default: "text-gray-700",
138-
};
126+
});
127+
const dataType = getDataType(data);
139128

140129
const renderCollapsible = (isArray: boolean) => {
141130
const items = isArray
@@ -236,7 +225,14 @@ const JsonNode = memo(
236225
{name}:
237226
</span>
238227
)}
239-
<pre className={typeStyleMap.string}>"{value}"</pre>
228+
<pre
229+
className={clsx(
230+
typeStyleMap.string,
231+
"break-all whitespace-pre-wrap",
232+
)}
233+
>
234+
"{value}"
235+
</pre>
240236
</div>
241237
);
242238
}
@@ -250,8 +246,8 @@ const JsonNode = memo(
250246
)}
251247
<pre
252248
className={clsx(
253-
typeStyleMap.string,
254-
"cursor-pointer group-hover:text-green-500",
249+
isError ? typeStyleMap.error : typeStyleMap.string,
250+
"cursor-pointer break-all whitespace-pre-wrap",
255251
)}
256252
onClick={() => setIsExpanded(!isExpanded)}
257253
title={isExpanded ? "Click to collapse" : "Click to expand"}

client/src/components/Sidebar.tsx

Lines changed: 55 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -103,14 +103,19 @@ const Sidebar = ({
103103
<div className="p-4 flex-1 overflow-auto">
104104
<div className="space-y-4">
105105
<div className="space-y-2">
106-
<label className="text-sm font-medium">Transport Type</label>
106+
<label
107+
className="text-sm font-medium"
108+
htmlFor="transport-type-select"
109+
>
110+
Transport Type
111+
</label>
107112
<Select
108113
value={transportType}
109114
onValueChange={(value: "stdio" | "sse") =>
110115
setTransportType(value)
111116
}
112117
>
113-
<SelectTrigger>
118+
<SelectTrigger id="transport-type-select">
114119
<SelectValue placeholder="Select transport type" />
115120
</SelectTrigger>
116121
<SelectContent>
@@ -123,17 +128,26 @@ const Sidebar = ({
123128
{transportType === "stdio" ? (
124129
<>
125130
<div className="space-y-2">
126-
<label className="text-sm font-medium">Command</label>
131+
<label className="text-sm font-medium" htmlFor="command-input">
132+
Command
133+
</label>
127134
<Input
135+
id="command-input"
128136
placeholder="Command"
129137
value={command}
130138
onChange={(e) => setCommand(e.target.value)}
131139
className="font-mono"
132140
/>
133141
</div>
134142
<div className="space-y-2">
135-
<label className="text-sm font-medium">Arguments</label>
143+
<label
144+
className="text-sm font-medium"
145+
htmlFor="arguments-input"
146+
>
147+
Arguments
148+
</label>
136149
<Input
150+
id="arguments-input"
137151
placeholder="Arguments (space-separated)"
138152
value={args}
139153
onChange={(e) => setArgs(e.target.value)}
@@ -144,8 +158,11 @@ const Sidebar = ({
144158
) : (
145159
<>
146160
<div className="space-y-2">
147-
<label className="text-sm font-medium">URL</label>
161+
<label className="text-sm font-medium" htmlFor="sse-url-input">
162+
URL
163+
</label>
148164
<Input
165+
id="sse-url-input"
149166
placeholder="URL"
150167
value={sseUrl}
151168
onChange={(e) => setSseUrl(e.target.value)}
@@ -157,6 +174,7 @@ const Sidebar = ({
157174
variant="outline"
158175
onClick={() => setShowBearerToken(!showBearerToken)}
159176
className="flex items-center w-full"
177+
aria-expanded={showBearerToken}
160178
>
161179
{showBearerToken ? (
162180
<ChevronDown className="w-4 h-4 mr-2" />
@@ -167,8 +185,14 @@ const Sidebar = ({
167185
</Button>
168186
{showBearerToken && (
169187
<div className="space-y-2">
170-
<label className="text-sm font-medium">Bearer Token</label>
188+
<label
189+
className="text-sm font-medium"
190+
htmlFor="bearer-token-input"
191+
>
192+
Bearer Token
193+
</label>
171194
<Input
195+
id="bearer-token-input"
172196
placeholder="Bearer Token"
173197
value={bearerToken}
174198
onChange={(e) => setBearerToken(e.target.value)}
@@ -187,6 +211,7 @@ const Sidebar = ({
187211
onClick={() => setShowEnvVars(!showEnvVars)}
188212
className="flex items-center w-full"
189213
data-testid="env-vars-button"
214+
aria-expanded={showEnvVars}
190215
>
191216
{showEnvVars ? (
192217
<ChevronDown className="w-4 h-4 mr-2" />
@@ -201,6 +226,7 @@ const Sidebar = ({
201226
<div key={idx} className="space-y-2 pb-4">
202227
<div className="flex gap-2">
203228
<Input
229+
aria-label={`Environment variable key ${idx + 1}`}
204230
placeholder="Key"
205231
value={key}
206232
onChange={(e) => {
@@ -243,6 +269,7 @@ const Sidebar = ({
243269
</div>
244270
<div className="flex gap-2">
245271
<Input
272+
aria-label={`Environment variable value ${idx + 1}`}
246273
type={shownEnvVars.has(key) ? "text" : "password"}
247274
placeholder="Value"
248275
value={value}
@@ -309,6 +336,7 @@ const Sidebar = ({
309336
onClick={() => setShowConfig(!showConfig)}
310337
className="flex items-center w-full"
311338
data-testid="config-button"
339+
aria-expanded={showConfig}
312340
>
313341
{showConfig ? (
314342
<ChevronDown className="w-4 h-4 mr-2" />
@@ -325,7 +353,10 @@ const Sidebar = ({
325353
return (
326354
<div key={key} className="space-y-2">
327355
<div className="flex items-center gap-1">
328-
<label className="text-sm font-medium text-green-600 break-all">
356+
<label
357+
className="text-sm font-medium text-green-600 break-all"
358+
htmlFor={`${configKey}-input`}
359+
>
329360
{configItem.label}
330361
</label>
331362
<Tooltip>
@@ -339,6 +370,7 @@ const Sidebar = ({
339370
</div>
340371
{typeof configItem.value === "number" ? (
341372
<Input
373+
id={`${configKey}-input`}
342374
type="number"
343375
data-testid={`${configKey}-input`}
344376
value={configItem.value}
@@ -365,7 +397,7 @@ const Sidebar = ({
365397
setConfig(newConfig);
366398
}}
367399
>
368-
<SelectTrigger>
400+
<SelectTrigger id={`${configKey}-input`}>
369401
<SelectValue />
370402
</SelectTrigger>
371403
<SelectContent>
@@ -375,6 +407,7 @@ const Sidebar = ({
375407
</Select>
376408
) : (
377409
<Input
410+
id={`${configKey}-input`}
378411
data-testid={`${configKey}-input`}
379412
value={configItem.value}
380413
onChange={(e) => {
@@ -398,7 +431,13 @@ const Sidebar = ({
398431
<div className="space-y-2">
399432
{connectionStatus === "connected" && (
400433
<div className="grid grid-cols-2 gap-4">
401-
<Button data-testid="connect-button" onClick={onConnect}>
434+
<Button
435+
data-testid="connect-button"
436+
onClick={() => {
437+
onDisconnect();
438+
onConnect();
439+
}}
440+
>
402441
<RotateCcw className="w-4 h-4 mr-2" />
403442
{transportType === "stdio" ? "Restart" : "Reconnect"}
404443
</Button>
@@ -448,14 +487,19 @@ const Sidebar = ({
448487

449488
{loggingSupported && connectionStatus === "connected" && (
450489
<div className="space-y-2">
451-
<label className="text-sm font-medium">Logging Level</label>
490+
<label
491+
className="text-sm font-medium"
492+
htmlFor="logging-level-select"
493+
>
494+
Logging Level
495+
</label>
452496
<Select
453497
value={logLevel}
454498
onValueChange={(value: LoggingLevel) =>
455499
sendLogLevelRequest(value)
456500
}
457501
>
458-
<SelectTrigger>
502+
<SelectTrigger id="logging-level-select">
459503
<SelectValue placeholder="Select logging level" />
460504
</SelectTrigger>
461505
<SelectContent>

client/src/components/ToolsTab.tsx

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@ import { Input } from "@/components/ui/input";
55
import { Label } from "@/components/ui/label";
66
import { TabsContent } from "@/components/ui/tabs";
77
import { Textarea } from "@/components/ui/textarea";
8-
import DynamicJsonForm, { JsonSchemaType, JsonValue } from "./DynamicJsonForm";
8+
import DynamicJsonForm from "./DynamicJsonForm";
9+
import type { JsonValue, JsonSchemaType } from "@/utils/jsonUtils";
910
import { generateDefaultValue } from "@/utils/schemaUtils";
1011
import {
1112
CallToolResultSchema,
@@ -74,11 +75,18 @@ const ToolsTab = ({
7475
return (
7576
<>
7677
<h4 className="font-semibold mb-2">
77-
Tool Result: {isError ? "Error" : "Success"}
78+
Tool Result:{" "}
79+
{isError ? (
80+
<span className="text-red-600 font-semibold">Error</span>
81+
) : (
82+
<span className="text-green-600 font-semibold">Success</span>
83+
)}
7884
</h4>
7985
{structuredResult.content.map((item, index) => (
8086
<div key={index} className="mb-2">
81-
{item.type === "text" && <JsonView data={item.text} />}
87+
{item.type === "text" && (
88+
<JsonView data={item.text} isError={isError} />
89+
)}
8290
{item.type === "image" && (
8391
<img
8492
src={`data:${item.mimeType};base64,${item.data}`}

client/src/components/__tests__/DynamicJsonForm.test.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { render, screen, fireEvent, waitFor } from "@testing-library/react";
22
import { describe, it, expect, jest } from "@jest/globals";
33
import DynamicJsonForm from "../DynamicJsonForm";
4-
import type { JsonSchemaType } from "../DynamicJsonForm";
4+
import type { JsonSchemaType } from "@/utils/jsonUtils";
55

66
describe("DynamicJsonForm String Fields", () => {
77
const renderForm = (props = {}) => {

client/src/components/ui/button.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,4 +54,4 @@ const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
5454
);
5555
Button.displayName = "Button";
5656

57-
export { Button, buttonVariants };
57+
export { Button };

0 commit comments

Comments
 (0)