Skip to content

Commit 9891b96

Browse files
Elaina-Leeclaude
andauthored
feat(Designer): Add Code Interpreter toggle card to Agent Loop settings (#8850)
* added code interpreter aligning to the design of otto * test: add unit tests for BuiltinToolsEditor component Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent e8f0c76 commit 9891b96

File tree

9 files changed

+416
-10
lines changed

9 files changed

+416
-10
lines changed

Localize/lang/strings.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -563,6 +563,7 @@
563563
"AhvQ7r": "Remove and clear all advanced parameters and their values",
564564
"Aid5oX": "Handoff description",
565565
"Ak2Lka": "Not connected to",
566+
"AkKqDo": "Built-in Tools",
566567
"AlPxuK": "Description",
567568
"AlWFOS": "Collapse chat panel",
568569
"Alq4/3": "Hybrid connector",
@@ -2216,6 +2217,7 @@
22162217
"_AhvQ7r.comment": "Button tooltip to remove all advanced parameters",
22172218
"_Aid5oX.comment": "Label text for the input field to enter a description of the handoff",
22182219
"_Ak2Lka.comment": "Chatbot text stating connection to api not made",
2220+
"_AkKqDo.comment": "Header label for the built-in tools section in agent loop settings",
22192221
"_AlPxuK.comment": "This is for a label for a badge, it is used for screen readers and not shown on the screen.",
22202222
"_AlWFOS.comment": "Collapse button title",
22212223
"_Alq4/3.comment": "Resource group title",
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
2+
3+
exports[`lib/builtintools > should render with basic props 1`] = `
4+
<div
5+
className="___pt95jf0_0000000 f22iagw f1vx9l62 ft85np5 fm9niy fggle4k f1yhwmi5"
6+
>
7+
<div
8+
className="___1hlb6fg_0000000 fl43uef fkhj508 f19n0e5"
9+
>
10+
Built-in Tools
11+
</div>
12+
<div
13+
className="___kzuet60_0000000 f22iagw f122n59 f1869bpl fu9asb0"
14+
>
15+
<div
16+
className="___1hxfasu_0000000 f22iagw f1vx9l62 fz7g6wx fe7p41z"
17+
>
18+
<span
19+
className="___bbz8wo0_0000000 fl43uef fy9rknc f19n0e5"
20+
>
21+
Code Interpreter
22+
</span>
23+
<span
24+
className="___l0twgo0_0000000 fy9rknc fkfq4zb"
25+
>
26+
Enable the agent to write and execute JavaScript code for calculations, data analysis, and file processing.
27+
</span>
28+
</div>
29+
<div
30+
className="fui-Switch r2i81i2"
31+
>
32+
<input
33+
checked={false}
34+
className="fui-Switch__input rsji9ng"
35+
id="switch-r0"
36+
onChange={[Function]}
37+
role="switch"
38+
type="checkbox"
39+
/>
40+
<div
41+
aria-hidden={true}
42+
className="fui-Switch__indicator r1c3hft5"
43+
>
44+
<svg
45+
aria-hidden={true}
46+
className="___9ctc0p0_1xvj9ao f1w7gpdv fez10in f1dd5bof"
47+
fill="currentColor"
48+
height="1em"
49+
viewBox="0 0 20 20"
50+
width="1em"
51+
xmlns="http://www.w3.org/2000/svg"
52+
>
53+
<path
54+
d="M10 2a8 8 0 1 0 0 16 8 8 0 0 0 0-16Z"
55+
fill="currentColor"
56+
/>
57+
</svg>
58+
</div>
59+
</div>
60+
</div>
61+
</div>
62+
`;
Lines changed: 223 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,223 @@
1+
import { BuiltinToolsEditor } from '../index';
2+
import type { BuiltinToolOption } from '../index';
3+
import { render, fireEvent, act } from '@testing-library/react';
4+
import { IntlProvider } from 'react-intl';
5+
import renderer from 'react-test-renderer';
6+
import { describe, vi, beforeEach, it, expect } from 'vitest';
7+
import { createLiteralValueSegment } from '../../editor/base/utils/helper';
8+
9+
const TestWrapper = ({ children }: { children: React.ReactNode }) => (
10+
<IntlProvider locale="en" messages={{}}>
11+
{children}
12+
</IntlProvider>
13+
);
14+
15+
describe('lib/builtintools', () => {
16+
const defaultOptions: BuiltinToolOption[] = [
17+
{
18+
value: 'code_interpreter',
19+
displayName: 'Code Interpreter',
20+
description: 'Enable the agent to write and execute JavaScript code for calculations, data analysis, and file processing.',
21+
},
22+
];
23+
24+
const defaultProps = {
25+
initialValue: [],
26+
options: defaultOptions,
27+
onChange: vi.fn(),
28+
};
29+
30+
beforeEach(() => {
31+
vi.clearAllMocks();
32+
});
33+
34+
it('should render with basic props', () => {
35+
const tree = renderer
36+
.create(
37+
<TestWrapper>
38+
<BuiltinToolsEditor {...defaultProps} />
39+
</TestWrapper>
40+
)
41+
.toJSON();
42+
expect(tree).toMatchSnapshot();
43+
});
44+
45+
it('should render header text', () => {
46+
const { getByText } = render(
47+
<TestWrapper>
48+
<BuiltinToolsEditor {...defaultProps} />
49+
</TestWrapper>
50+
);
51+
52+
expect(getByText('Built-in Tools')).toBeTruthy();
53+
});
54+
55+
it('should render tool display name and description', () => {
56+
const { getByText } = render(
57+
<TestWrapper>
58+
<BuiltinToolsEditor {...defaultProps} />
59+
</TestWrapper>
60+
);
61+
62+
expect(getByText('Code Interpreter')).toBeTruthy();
63+
expect(
64+
getByText('Enable the agent to write and execute JavaScript code for calculations, data analysis, and file processing.')
65+
).toBeTruthy();
66+
});
67+
68+
it('should render switch as unchecked when initialValue is empty', () => {
69+
const { getByRole } = render(
70+
<TestWrapper>
71+
<BuiltinToolsEditor {...defaultProps} initialValue={[]} />
72+
</TestWrapper>
73+
);
74+
75+
const switchEl = getByRole('switch') as HTMLInputElement;
76+
expect(switchEl.checked).toBe(false);
77+
});
78+
79+
it('should render switch as unchecked when initialValue is empty array string', () => {
80+
const { getByRole } = render(
81+
<TestWrapper>
82+
<BuiltinToolsEditor {...defaultProps} initialValue={[createLiteralValueSegment('[]')]} />
83+
</TestWrapper>
84+
);
85+
86+
const switchEl = getByRole('switch') as HTMLInputElement;
87+
expect(switchEl.checked).toBe(false);
88+
});
89+
90+
it('should render switch as checked when initialValue contains the tool', () => {
91+
const { getByRole } = render(
92+
<TestWrapper>
93+
<BuiltinToolsEditor {...defaultProps} initialValue={[createLiteralValueSegment('["code_interpreter"]')]} />
94+
</TestWrapper>
95+
);
96+
97+
const switchEl = getByRole('switch') as HTMLInputElement;
98+
expect(switchEl.checked).toBe(true);
99+
});
100+
101+
it('should call onChange with tool added when toggling on', () => {
102+
const onChange = vi.fn();
103+
const { getByRole } = render(
104+
<TestWrapper>
105+
<BuiltinToolsEditor {...defaultProps} initialValue={[]} onChange={onChange} />
106+
</TestWrapper>
107+
);
108+
109+
const switchEl = getByRole('switch');
110+
act(() => {
111+
fireEvent.click(switchEl);
112+
});
113+
114+
expect(onChange).toHaveBeenCalledTimes(1);
115+
const callArg = onChange.mock.calls[0][0];
116+
expect(callArg.value).toHaveLength(1);
117+
expect(callArg.value[0].value).toBe('["code_interpreter"]');
118+
});
119+
120+
it('should call onChange with tool removed when toggling off', () => {
121+
const onChange = vi.fn();
122+
const { getByRole } = render(
123+
<TestWrapper>
124+
<BuiltinToolsEditor {...defaultProps} initialValue={[createLiteralValueSegment('["code_interpreter"]')]} onChange={onChange} />
125+
</TestWrapper>
126+
);
127+
128+
const switchEl = getByRole('switch');
129+
act(() => {
130+
fireEvent.click(switchEl);
131+
});
132+
133+
expect(onChange).toHaveBeenCalledTimes(1);
134+
const callArg = onChange.mock.calls[0][0];
135+
expect(callArg.value).toHaveLength(1);
136+
expect(callArg.value[0].value).toBe('[]');
137+
});
138+
139+
it('should disable switch when readonly is true', () => {
140+
const { getByRole } = render(
141+
<TestWrapper>
142+
<BuiltinToolsEditor {...defaultProps} readonly={true} />
143+
</TestWrapper>
144+
);
145+
146+
const switchEl = getByRole('switch');
147+
expect(switchEl).toBeDisabled();
148+
});
149+
150+
it('should not call onChange when readonly and clicked', () => {
151+
const onChange = vi.fn();
152+
const { getByRole } = render(
153+
<TestWrapper>
154+
<BuiltinToolsEditor {...defaultProps} readonly={true} onChange={onChange} />
155+
</TestWrapper>
156+
);
157+
158+
const switchEl = getByRole('switch');
159+
act(() => {
160+
fireEvent.click(switchEl);
161+
});
162+
163+
expect(onChange).not.toHaveBeenCalled();
164+
});
165+
166+
it('should render multiple tool options', () => {
167+
const multipleOptions: BuiltinToolOption[] = [
168+
{
169+
value: 'code_interpreter',
170+
displayName: 'Code Interpreter',
171+
description: 'Execute code.',
172+
},
173+
{
174+
value: 'file_search',
175+
displayName: 'File Search',
176+
description: 'Search files.',
177+
},
178+
];
179+
180+
const { getAllByRole, getByText } = render(
181+
<TestWrapper>
182+
<BuiltinToolsEditor {...defaultProps} options={multipleOptions} />
183+
</TestWrapper>
184+
);
185+
186+
const switches = getAllByRole('switch');
187+
expect(switches).toHaveLength(2);
188+
expect(getByText('Code Interpreter')).toBeTruthy();
189+
expect(getByText('File Search')).toBeTruthy();
190+
});
191+
192+
it('should handle invalid JSON in initialValue gracefully', () => {
193+
const { getByRole } = render(
194+
<TestWrapper>
195+
<BuiltinToolsEditor {...defaultProps} initialValue={[createLiteralValueSegment('not-valid-json')]} />
196+
</TestWrapper>
197+
);
198+
199+
const switchEl = getByRole('switch') as HTMLInputElement;
200+
expect(switchEl.checked).toBe(false);
201+
});
202+
203+
it('should handle non-array JSON in initialValue gracefully', () => {
204+
const { getByRole } = render(
205+
<TestWrapper>
206+
<BuiltinToolsEditor {...defaultProps} initialValue={[createLiteralValueSegment('"string_value"')]} />
207+
</TestWrapper>
208+
);
209+
210+
const switchEl = getByRole('switch') as HTMLInputElement;
211+
expect(switchEl.checked).toBe(false);
212+
});
213+
214+
it('should render with empty options array', () => {
215+
const { queryByRole } = render(
216+
<TestWrapper>
217+
<BuiltinToolsEditor {...defaultProps} options={[]} />
218+
</TestWrapper>
219+
);
220+
221+
expect(queryByRole('switch')).toBeNull();
222+
});
223+
});
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
import { useCallback, useMemo } from 'react';
2+
import { useIntl } from 'react-intl';
3+
import { Switch } from '@fluentui/react-components';
4+
import type { ValueSegment } from '../editor';
5+
import type { ChangeHandler } from '../editor/base';
6+
import { createLiteralValueSegment } from '../editor/base/utils/helper';
7+
import { useBuiltinToolsStyles } from './styles';
8+
9+
export interface BuiltinToolOption {
10+
value: string;
11+
displayName: string;
12+
description: string;
13+
}
14+
15+
export interface BuiltinToolsEditorProps {
16+
initialValue: ValueSegment[];
17+
options?: BuiltinToolOption[];
18+
readonly?: boolean;
19+
onChange?: ChangeHandler;
20+
}
21+
22+
export const BuiltinToolsEditor = ({ initialValue, options = [], readonly, onChange }: BuiltinToolsEditorProps): JSX.Element => {
23+
const intl = useIntl();
24+
const styles = useBuiltinToolsStyles();
25+
26+
const headerText = intl.formatMessage({
27+
defaultMessage: 'Built-in Tools',
28+
id: 'AkKqDo',
29+
description: 'Header label for the built-in tools section in agent loop settings',
30+
});
31+
32+
const enabledTools = useMemo((): string[] => {
33+
try {
34+
const raw = initialValue.map((s) => s.value).join('');
35+
if (!raw) {
36+
return [];
37+
}
38+
const parsed = JSON.parse(raw);
39+
return Array.isArray(parsed) ? parsed : [];
40+
} catch {
41+
return [];
42+
}
43+
}, [initialValue]);
44+
45+
const handleToggle = useCallback(
46+
(toolValue: string, checked: boolean) => {
47+
const updated = checked
48+
? enabledTools.includes(toolValue)
49+
? enabledTools
50+
: [...enabledTools, toolValue]
51+
: enabledTools.filter((t) => t !== toolValue);
52+
const serialized = JSON.stringify(updated);
53+
onChange?.({ value: [createLiteralValueSegment(serialized)] });
54+
},
55+
[enabledTools, onChange]
56+
);
57+
58+
return (
59+
<div className={styles.container}>
60+
<div className={styles.header}>{headerText}</div>
61+
{options.map((option) => (
62+
<div key={option.value} className={styles.toolRow}>
63+
<div className={styles.toolInfo}>
64+
<span className={styles.toolName}>{option.displayName}</span>
65+
<span className={styles.toolDescription}>{option.description}</span>
66+
</div>
67+
<Switch
68+
checked={enabledTools.includes(option.value)}
69+
disabled={readonly}
70+
onChange={(_ev, data) => handleToggle(option.value, data.checked)}
71+
/>
72+
</div>
73+
))}
74+
</div>
75+
);
76+
};

0 commit comments

Comments
 (0)