Skip to content

Commit 4781e45

Browse files
authored
Merge pull request #2182 from wgqqqqq/fix/acp-hide-remote-servers
fix(acp-agents): hide remote servers without deleting
2 parents 0a01af4 + cb2215f commit 4781e45

7 files changed

Lines changed: 332 additions & 12 deletions

File tree

src/web-ui/src/infrastructure/config/components/AcpAgentsConfig.appearance.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ export const acpAgentsConfigAppearanceDescriptor: AppearanceSurfaceDescriptor =
1717
{ id: 'status' },
1818
{ id: 'confirmation' },
1919
{ id: 'remoteList' },
20+
{ id: 'hiddenRemoteList' },
21+
{ id: 'hiddenRemoteRow' },
2022
{ id: 'remoteServer' },
2123
{ id: 'remoteHeader' },
2224
{ id: 'remoteAgents' },

src/web-ui/src/infrastructure/config/components/AcpAgentsConfig.scss

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -253,6 +253,29 @@
253253
gap: $size-gap-3;
254254
}
255255

256+
&__hidden-remote-list {
257+
display: flex;
258+
flex-direction: column;
259+
margin-top: $size-gap-3;
260+
overflow: hidden;
261+
border: 1px solid var(--bf-appearance-token-border-subtle);
262+
border-radius: $size-radius-base;
263+
}
264+
265+
&__hidden-remote-row {
266+
display: grid;
267+
grid-template-columns: minmax(0, 1fr) auto;
268+
gap: $size-gap-3;
269+
align-items: center;
270+
min-width: 0;
271+
padding: $size-gap-3;
272+
border-bottom: 1px solid var(--bf-appearance-token-border-subtle);
273+
274+
&:last-child {
275+
border-bottom: 0;
276+
}
277+
}
278+
256279
&__remote-server {
257280
display: flex;
258281
flex-direction: column;
@@ -328,7 +351,8 @@
328351
@media (max-width: 860px) {
329352
&__toolbar,
330353
&__registry-row,
331-
&__remote-head {
354+
&__remote-head,
355+
&__hidden-remote-row {
332356
grid-template-columns: 1fr;
333357
}
334358

src/web-ui/src/infrastructure/config/components/AcpAgentsConfig.test.tsx

Lines changed: 133 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,22 @@ vi.mock('@/component-library', () => ({
4141
{children}
4242
</button>
4343
),
44+
IconButton: ({
45+
children,
46+
disabled,
47+
isLoading,
48+
onClick,
49+
tooltip: _tooltip,
50+
...props
51+
}: React.ButtonHTMLAttributes<HTMLButtonElement> & {
52+
children: React.ReactNode;
53+
isLoading?: boolean;
54+
tooltip?: React.ReactNode;
55+
}) => (
56+
<button type="button" disabled={disabled || isLoading} onClick={onClick} {...props}>
57+
{children}
58+
</button>
59+
),
4460
Input: ({
4561
value,
4662
onChange,
@@ -84,13 +100,18 @@ vi.mock('./common', () => ({
84100
children,
85101
title,
86102
description,
103+
extra,
87104
}: {
88105
children: React.ReactNode;
89106
title: string;
90107
description?: string;
108+
extra?: React.ReactNode;
91109
}) => (
92110
<section>
93-
<h2>{title}</h2>
111+
<div>
112+
<h2>{title}</h2>
113+
{extra}
114+
</div>
94115
{description ? <p>{description}</p> : null}
95116
{children}
96117
</section>
@@ -141,6 +162,7 @@ describe('AcpAgentsConfig', () => {
141162

142163
beforeEach(() => {
143164
(globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
165+
localStorage.clear();
144166
loadJsonConfigMock.mockResolvedValue(JSON.stringify({
145167
acpClients: {
146168
opencode: {
@@ -233,6 +255,116 @@ describe('AcpAgentsConfig', () => {
233255
});
234256
});
235257

258+
it('hides a saved remote server without deleting its SSH connection', async () => {
259+
listSavedConnectionsMock.mockResolvedValue([{
260+
id: 'huawei-server',
261+
name: 'Huawei Server',
262+
host: '119.8.182.138',
263+
port: 22,
264+
username: 'ssh-root',
265+
authType: { type: 'Password' },
266+
}]);
267+
268+
await act(async () => {
269+
root.render(<AcpAgentsConfig />);
270+
});
271+
await act(async () => {
272+
await Promise.resolve();
273+
await Promise.resolve();
274+
});
275+
276+
const hideButton = container.querySelector<HTMLButtonElement>(
277+
'button[aria-label="remote.hideConnection"]'
278+
);
279+
expect(hideButton).not.toBeNull();
280+
281+
await act(async () => {
282+
hideButton?.click();
283+
await Promise.resolve();
284+
});
285+
286+
expect(listSavedConnectionsMock).toHaveBeenCalledTimes(1);
287+
expect(container.textContent).not.toContain('Huawei Server');
288+
expect(JSON.parse(localStorage.getItem('bitfun:settings:acp-agents:hidden-remote-connections:v1') || '[]'))
289+
.toEqual(['huawei-server']);
290+
expect(container.textContent).toContain('remote.showHiddenConnections');
291+
});
292+
293+
it('restores a hidden remote server from the hidden list', async () => {
294+
localStorage.setItem(
295+
'bitfun:settings:acp-agents:hidden-remote-connections:v1',
296+
JSON.stringify(['huawei-server'])
297+
);
298+
listSavedConnectionsMock.mockResolvedValue([{
299+
id: 'huawei-server',
300+
name: 'Huawei Server',
301+
host: '119.8.182.138',
302+
port: 22,
303+
username: 'ssh-root',
304+
authType: { type: 'Password' },
305+
}]);
306+
307+
await act(async () => {
308+
root.render(<AcpAgentsConfig />);
309+
});
310+
await act(async () => {
311+
await Promise.resolve();
312+
await Promise.resolve();
313+
});
314+
315+
const showHiddenButton = Array.from(container.querySelectorAll('button'))
316+
.find(button => button.textContent?.includes('remote.showHiddenConnections'));
317+
expect(showHiddenButton).not.toBeUndefined();
318+
319+
await act(async () => {
320+
showHiddenButton?.click();
321+
await Promise.resolve();
322+
});
323+
324+
const restoreButton = container.querySelector<HTMLButtonElement>(
325+
'button[aria-label="remote.restoreConnection"]'
326+
);
327+
expect(restoreButton).not.toBeNull();
328+
329+
await act(async () => {
330+
restoreButton?.click();
331+
await Promise.resolve();
332+
await Promise.resolve();
333+
});
334+
335+
expect(localStorage.getItem('bitfun:settings:acp-agents:hidden-remote-connections:v1'))
336+
.toBe('[]');
337+
expect(container.textContent).toContain('Huawei Server');
338+
});
339+
340+
it('does not probe hidden remote servers until they are restored', async () => {
341+
localStorage.setItem(
342+
'bitfun:settings:acp-agents:hidden-remote-connections:v1',
343+
JSON.stringify(['huawei-server'])
344+
);
345+
listSavedConnectionsMock.mockResolvedValue([{
346+
id: 'huawei-server',
347+
name: 'Huawei Server',
348+
host: '119.8.182.138',
349+
port: 22,
350+
username: 'ssh-root',
351+
authType: { type: 'Password' },
352+
}]);
353+
354+
await act(async () => {
355+
root.render(<AcpAgentsConfig />);
356+
});
357+
await act(async () => {
358+
await Promise.resolve();
359+
await Promise.resolve();
360+
});
361+
362+
expect(probeClientRequirementsMock).not.toHaveBeenCalledWith({
363+
remoteConnectionId: 'huawei-server',
364+
force: undefined,
365+
});
366+
});
367+
236368
it('configures a preset adapter when the CLI is ready but the ACP layer is missing', async () => {
237369
probeClientRequirementsMock.mockResolvedValue([
238370
{

0 commit comments

Comments
 (0)