feat(cli): implement compact tool output (#20974)

This commit is contained in:
Jarrod Whelan
2026-03-30 16:43:29 -07:00
committed by GitHub
parent 3e95b8ec59
commit 1df5c98b33
45 changed files with 2670 additions and 386 deletions
@@ -4,7 +4,7 @@
* SPDX-License-Identifier: Apache-2.0
*/
import { act } from 'react';
import { act, useState, useCallback } from 'react';
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { renderHook } from '../../test-utils/render.js';
import { ToolActionsProvider, useToolActions } from './ToolActionsContext.js';
@@ -71,16 +71,61 @@ describe('ToolActionsContext', () => {
beforeEach(() => {
vi.clearAllMocks();
// Default to a pending promise to avoid unwanted async state updates in tests
// that don't specifically test the IdeClient initialization.
vi.mocked(IdeClient.getInstance).mockReturnValue(new Promise(() => {}));
});
const wrapper = ({ children }: { children: React.ReactNode }) => (
<ToolActionsProvider config={mockConfig} toolCalls={mockToolCalls}>
{children}
</ToolActionsProvider>
);
const WrapperReactComp = ({ children }: { children: React.ReactNode }) => {
const [expandedTools, setExpandedTools] = useState<Set<string>>(new Set());
const isExpanded = useCallback(
(callId: string) => expandedTools.has(callId),
[expandedTools],
);
const toggleExpansion = useCallback((callId: string) => {
setExpandedTools((prev) => {
const next = new Set(prev);
if (next.has(callId)) {
next.delete(callId);
} else {
next.add(callId);
}
return next;
});
}, []);
const toggleAllExpansion = useCallback((callIds: string[]) => {
setExpandedTools((prev) => {
const next = new Set(prev);
const anyCollapsed = callIds.some((id) => !next.has(id));
if (anyCollapsed) {
callIds.forEach((id) => next.add(id));
} else {
callIds.forEach((id) => next.delete(id));
}
return next;
});
}, []);
return (
<ToolActionsProvider
config={mockConfig}
toolCalls={mockToolCalls}
isExpanded={isExpanded}
toggleExpansion={toggleExpansion}
toggleAllExpansion={toggleAllExpansion}
>
{children}
</ToolActionsProvider>
);
};
it('publishes to MessageBus for tools with correlationId', async () => {
const { result } = await renderHook(() => useToolActions(), { wrapper });
const { result } = await renderHook(() => useToolActions(), {
wrapper: WrapperReactComp,
});
await result.current.confirm(
'modern-call',
@@ -98,7 +143,9 @@ describe('ToolActionsContext', () => {
});
it('handles cancel by calling confirm with Cancel outcome', async () => {
const { result } = await renderHook(() => useToolActions(), { wrapper });
const { result } = await renderHook(() => useToolActions(), {
wrapper: WrapperReactComp,
});
await result.current.cancel('modern-call');
@@ -127,7 +174,9 @@ describe('ToolActionsContext', () => {
);
vi.mocked(mockConfig.getIdeMode).mockReturnValue(true);
const { result } = await renderHook(() => useToolActions(), { wrapper });
const { result } = await renderHook(() => useToolActions(), {
wrapper: WrapperReactComp,
});
await act(async () => {
deferredIdeClient.resolve(mockIdeClient);
@@ -169,7 +218,9 @@ describe('ToolActionsContext', () => {
);
vi.mocked(mockConfig.getIdeMode).mockReturnValue(true);
const { result } = await renderHook(() => useToolActions(), { wrapper });
const { result } = await renderHook(() => useToolActions(), {
wrapper: WrapperReactComp,
});
await act(async () => {
deferredIdeClient.resolve(mockIdeClient);
@@ -214,7 +265,13 @@ describe('ToolActionsContext', () => {
const { result } = await renderHook(() => useToolActions(), {
wrapper: ({ children }) => (
<ToolActionsProvider config={mockConfig} toolCalls={[legacyTool]}>
<ToolActionsProvider
config={mockConfig}
toolCalls={[legacyTool]}
isExpanded={vi.fn().mockReturnValue(false)}
toggleExpansion={vi.fn()}
toggleAllExpansion={vi.fn()}
>
{children}
</ToolActionsProvider>
),
@@ -233,4 +290,58 @@ describe('ToolActionsContext', () => {
);
expect(mockMessageBus.publish).not.toHaveBeenCalled();
});
describe('toggleAllExpansion', () => {
it('expands all when none are expanded', async () => {
const { result } = await renderHook(() => useToolActions(), {
wrapper: WrapperReactComp,
});
act(() => {
result.current.toggleAllExpansion(['modern-call', 'edit-call']);
});
expect(result.current.isExpanded('modern-call')).toBe(true);
expect(result.current.isExpanded('edit-call')).toBe(true);
});
it('expands all when some are expanded', async () => {
const { result } = await renderHook(() => useToolActions(), {
wrapper: WrapperReactComp,
});
act(() => {
result.current.toggleExpansion('modern-call');
});
expect(result.current.isExpanded('modern-call')).toBe(true);
expect(result.current.isExpanded('edit-call')).toBe(false);
act(() => {
result.current.toggleAllExpansion(['modern-call', 'edit-call']);
});
expect(result.current.isExpanded('modern-call')).toBe(true);
expect(result.current.isExpanded('edit-call')).toBe(true);
});
it('collapses all when all are expanded', async () => {
const { result } = await renderHook(() => useToolActions(), {
wrapper: WrapperReactComp,
});
act(() => {
result.current.toggleExpansion('modern-call');
result.current.toggleExpansion('edit-call');
});
expect(result.current.isExpanded('modern-call')).toBe(true);
expect(result.current.isExpanded('edit-call')).toBe(true);
act(() => {
result.current.toggleAllExpansion(['modern-call', 'edit-call']);
});
expect(result.current.isExpanded('modern-call')).toBe(false);
expect(result.current.isExpanded('edit-call')).toBe(false);
});
});
});