Skip to content

Commit afb52ce

Browse files
committed
feat(ui): opencode global connect + client icons in install panel
- Add opencode to the home "Connect a client" grid: a global config snippet (mcp.mcpmux = { type: remote, url }) with NO per-workspace header, since a global config can hold only one. New opencode brand icon. - Show brand icons in the per-workspace install panel checklist (Cursor, Claude Code, VS Code, opencode, Zed), with a generic fallback. New zed icon. Tests: ConnectIDEs renders the opencode tile and copies a header-less global snippet; install panel renders an icon per client row. Claude-Session: https://claude.ai/code/session_01Baan9JmzR43uxxRUh7CAMF Signed-off-by: Mohammod Al Amin Ashik <maa.ashik00@gmail.com>
1 parent e2ec055 commit afb52ce

6 files changed

Lines changed: 81 additions & 3 deletions

File tree

Lines changed: 5 additions & 0 deletions
Loading
Lines changed: 4 additions & 0 deletions
Loading

apps/desktop/src/components/ConnectIDEs.tsx

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import claudeIcon from '@/assets/client-icons/claude.svg';
77
import windsurfIcon from '@/assets/client-icons/windsurf.svg';
88
import jetbrainsIcon from '@/assets/client-icons/jetbrains.svg';
99
import androidStudioIcon from '@/assets/client-icons/android-studio.svg';
10+
import opencodeIcon from '@/assets/client-icons/opencode.svg';
1011
import { addToVscode, addToCursor } from '@/lib/api/clientInstall';
1112

1213
type GridAction = 'deep_link' | 'copy_command' | 'copy_config';
@@ -94,6 +95,18 @@ export function ConnectIDEsGrid({ gatewayUrl, gatewayRunning }: ConnectIDEsGridP
9495
'loads mcpmux on the next `claude` invocation (existing sessions need ' +
9596
'/restart). Approve on this page when it connects.',
9697
},
98+
{
99+
id: 'opencode',
100+
name: 'opencode',
101+
label: 'opencode',
102+
icon: opencodeIcon,
103+
action: 'copy_config',
104+
handler: `"mcpmux": {\n "type": "remote",\n "url": "${mcpUrl}"\n}`,
105+
nextStep:
106+
'Copies a JSON snippet. In opencode, paste it under "mcp" in opencode.json ' +
107+
'(project) or ~/.config/opencode/opencode.json (global), then restart ' +
108+
'opencode. Approve on this page when it connects.',
109+
},
97110
{
98111
id: 'jetbrains',
99112
name: 'JetBrains IDEs',

apps/desktop/src/features/workspaces/WorkspaceInstallPanel.tsx

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,31 @@
11
import { useCallback, useEffect, useState } from 'react';
2-
import { Check, Copy, Download, Loader2, ShieldCheck, ShieldOff, AlertCircle } from 'lucide-react';
2+
import {
3+
AppWindow,
4+
Check,
5+
Copy,
6+
Download,
7+
Loader2,
8+
ShieldCheck,
9+
ShieldOff,
10+
AlertCircle,
11+
} from 'lucide-react';
312
import { Button } from '@mcpmux/ui';
13+
import cursorIcon from '@/assets/client-icons/cursor.svg';
14+
import claudeIcon from '@/assets/client-icons/claude.svg';
15+
import vscodeIcon from '@/assets/client-icons/vscode.png';
16+
import opencodeIcon from '@/assets/client-icons/opencode.svg';
17+
import zedIcon from '@/assets/client-icons/zed.svg';
418
import { getGatewayStatus } from '@/lib/api/gateway';
519
import { useNavigateTo, useSetPendingSettingsSection } from '@/stores';
20+
21+
/** Brand icon per supported client id (falls back to a generic glyph). */
22+
const CLIENT_ICONS: Record<string, string> = {
23+
cursor: cursorIcon,
24+
'claude-code': claudeIcon,
25+
vscode: vscodeIcon,
26+
opencode: opencodeIcon,
27+
zed: zedIcon,
28+
};
629
import {
730
generateWorkspaceConfigSnippet,
831
getGatewayAuthDisabled,
@@ -209,6 +232,15 @@ export function WorkspaceInstallPanel({ workspaceRoot }: { workspaceRoot: string
209232
onChange={() => toggleClient(c.id)}
210233
className="h-4 w-4 flex-shrink-0 accent-primary-500"
211234
/>
235+
{CLIENT_ICONS[c.id] ? (
236+
<img
237+
src={CLIENT_ICONS[c.id]}
238+
alt=""
239+
className="h-5 w-5 flex-shrink-0 object-contain"
240+
/>
241+
) : (
242+
<AppWindow className="h-5 w-5 flex-shrink-0 text-[rgb(var(--muted))]" />
243+
)}
212244
<div className="min-w-0 flex-1">
213245
<div className="text-sm font-medium text-[rgb(var(--foreground))]">{c.label}</div>
214246
<div className="truncate font-mono text-[11px] text-[rgb(var(--muted))]">

tests/ts/components/ConnectIDEs.test.tsx

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,9 +35,30 @@ describe('ConnectIDEs', () => {
3535
expect(screen.getByTestId('client-icon-vscode')).toBeInTheDocument();
3636
expect(screen.getByTestId('client-icon-cursor')).toBeInTheDocument();
3737
expect(screen.getByTestId('client-icon-claude-code')).toBeInTheDocument();
38+
expect(screen.getByTestId('client-icon-opencode')).toBeInTheDocument();
3839
expect(screen.getByTestId('client-icon-copy-config')).toBeInTheDocument();
3940
});
4041

42+
it('offers a global opencode config snippet (no workspace header)', async () => {
43+
const user = userEvent.setup();
44+
const writeText = vi.fn().mockResolvedValue(undefined);
45+
Object.defineProperty(navigator, 'clipboard', {
46+
value: { writeText },
47+
writable: true,
48+
configurable: true,
49+
});
50+
render(<ConnectIDEs gatewayUrl="http://localhost:45818" gatewayRunning={true} />);
51+
52+
await user.click(screen.getByTestId('client-icon-opencode'));
53+
await user.click(screen.getByRole('button', { name: /Copy config/i }));
54+
55+
const copied = writeText.mock.calls[0][0] as string;
56+
expect(copied).toContain('"type": "remote"');
57+
expect(copied).toContain('localhost:45818/mcp');
58+
// Global connect carries no per-workspace header.
59+
expect(copied).not.toContain('X-Mcpmux-Workspace');
60+
});
61+
4162
it('should show labels under icons', () => {
4263
render(
4364
<ConnectIDEs gatewayUrl="http://localhost:45818" gatewayRunning={true} />

tests/ts/components/WorkspaceInstallPanel.test.tsx

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -74,11 +74,14 @@ describe('WorkspaceInstallPanel', () => {
7474
.mockResolvedValue({ running: true, url: 'http://localhost:45818' });
7575
});
7676

77-
it('lists every supported client', async () => {
77+
it('lists every supported client, each with an icon', async () => {
7878
render(<WorkspaceInstallPanel workspaceRoot={ROOT} />);
7979
expect(await screen.findByText('Cursor')).toBeTruthy();
8080
for (const c of CLIENTS) {
81-
expect(screen.getByTestId(`workspace-install-client-${c.id}`)).toBeTruthy();
81+
const row = screen.getByTestId(`workspace-install-client-${c.id}`);
82+
expect(row).toBeTruthy();
83+
// Each known client renders a brand icon image.
84+
expect(row.querySelector('img')).toBeTruthy();
8285
}
8386
});
8487

0 commit comments

Comments
 (0)