Skip to content

Commit 35ee451

Browse files
author
colinmcneil
committed
UX improvements:
- Claude desktop write/delete - Catalog item icons for metadata
1 parent 649f930 commit 35ee451

File tree

3 files changed

+143
-57
lines changed

3 files changed

+143
-57
lines changed

src/extension/ui/src/Usage.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ export interface RegistryChangedRecord extends Record {
2121
export interface ClaudeConfigChangedRecord extends Record {
2222
event: 'claude-config-changed';
2323
properties: {
24-
action: 'add' | 'remove';
24+
action: 'add' | 'remove' | 'write' | 'delete';
2525
};
2626
};
2727

src/extension/ui/src/components/ClaudeConfigSyncStatus.tsx

Lines changed: 93 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { Badge, Button, Checkbox, Dialog, DialogContent, DialogContentText, Dial
33
import { useEffect, useState } from "react";
44
import { writeFilesToHost } from "../FileWatcher";
55
import { trackEvent } from "../Usage";
6+
import { ExecResult } from "@docker/extension-api-client-types/dist/v1";
67

78
const DOCKER_MCP_CONFIG = {
89
"command": "docker",
@@ -61,23 +62,33 @@ const getNeverShowAgain = () => {
6162
}
6263

6364
export const ClaudeConfigSyncStatus = ({ client, setHasConfig }: { client: v1.DockerDesktopClient, setHasConfig: (hasConfig: boolean) => void }) => {
64-
const [claudeConfig, setClaudeConfig] = useState<{ mcpServers: { [key: string]: any } } | null>(null)
65+
const [claudeConfig, setClaudeConfig] = useState<{ mcpServers: { [key: string]: any } } | null | undefined>(null)
6566
const [showConfigModal, setShowConfigModal] = useState<boolean>(false)
67+
6668
const [showRestartModal, setShowRestartModal] = useState<boolean>(false)
6769
const [status, setStatus] = useState<ClaudeConfigStatus>({ state: 'loading', message: '...', color: 'default' })
6870
const [configPath, setConfigPath] = useState<string | null>(null)
69-
useEffect(() => {
70-
const refreshConfig = async () => {
71-
try {
72-
const config = await getClaudeConfig(client)
73-
const newConfig = JSON.parse(config)
74-
setClaudeConfig(newConfig)
7571

76-
} catch (error) {
72+
const [deleteReady, setDeleteReady] = useState<boolean>(false)
73+
const refreshConfig = async () => {
74+
try {
75+
const config = await getClaudeConfig(client)
76+
const newConfig = JSON.parse(config)
77+
setClaudeConfig(newConfig)
78+
79+
} catch (error) {
80+
if ((error as ExecResult).stderr && (error as ExecResult).stderr.includes('bind source path does not exist')) {
81+
setClaudeConfig(undefined)
82+
}
83+
else {
7784
console.error('Error parsing config. Using cached config if available.', error)
7885
}
7986

87+
8088
}
89+
90+
}
91+
useEffect(() => {
8192
refreshConfig()
8293
const interval = setInterval(() => {
8394
refreshConfig()
@@ -93,6 +104,9 @@ export const ClaudeConfigSyncStatus = ({ client, setHasConfig }: { client: v1.Do
93104

94105

95106
useEffect(() => {
107+
if (claudeConfig === undefined) {
108+
return setStatus({ state: 'missing claude config', message: 'No claude desktop config found', color: 'error' })
109+
}
96110
if (claudeConfig) {
97111
const servers = claudeConfig.mcpServers
98112
if (!servers) {
@@ -113,39 +127,83 @@ export const ClaudeConfigSyncStatus = ({ client, setHasConfig }: { client: v1.Do
113127

114128
return <Badge badgeContent={status.state} color={status.color} sx={{ ml: 4 }}>
115129
<Dialog open={Boolean(showConfigModal && configPath)} onClose={() => setShowConfigModal(false)} maxWidth="lg">
116-
<DialogTitle>Current Claude Desktop Config</DialogTitle>
117130

118-
{status.state === 'has docker_mcp' && <Button onClick={async () => {
119-
trackEvent('claude-config-changed', { action: 'remove' })
120-
// Remove docker_mcp from config
121-
if (claudeConfig && claudeConfig.mcpServers) {
122-
const newConfig = { ...claudeConfig }
123-
delete newConfig.mcpServers.mcp_docker
124-
setClaudeConfig(newConfig)
125-
await writeFilesToHost(client, [{ path: 'config.json', content: JSON.stringify(newConfig, null, 2) }], [{ source: configPath!, target: '/claude_desktop_config/config.json' }], '/claude_desktop_config')
126-
setShowRestartModal(!getNeverShowAgain())
127-
setShowConfigModal(false)
131+
<DialogTitle>Current Claude Desktop Config</DialogTitle>
132+
<Stack direction="column" spacing={2} sx={{ p: 2 }}>
133+
{
134+
status.state === 'has docker_mcp' && <Button onClick={async () => {
135+
trackEvent('claude-config-changed', { action: 'remove' })
136+
// Remove docker_mcp from config
137+
if (claudeConfig && claudeConfig.mcpServers) {
138+
const newConfig = { ...claudeConfig }
139+
delete newConfig.mcpServers.mcp_docker
140+
setClaudeConfig(newConfig)
141+
await writeFilesToHost(client, [{ path: 'config.json', content: JSON.stringify(newConfig, null, 2) }], [{ source: configPath!, target: '/claude_desktop_config/config.json' }], '/claude_desktop_config')
142+
setShowRestartModal(!getNeverShowAgain())
143+
}
144+
}}>Remove Docker MCP</Button>}
145+
{
146+
status.state === 'missing docker_mcp' && <Button onClick={() => {
147+
trackEvent('claude-config-changed', { action: 'add' })
148+
// Add docker_mcp to config
149+
if (claudeConfig && claudeConfig.mcpServers) {
150+
const newConfig = { ...claudeConfig }
151+
newConfig.mcpServers.mcp_docker = DOCKER_MCP_CONFIG
152+
setClaudeConfig(newConfig)
153+
writeFilesToHost(client, [{ path: 'config.json', content: JSON.stringify(newConfig, null, 2) }], [{ source: configPath!, target: '/claude_desktop_config/config.json' }], '/claude_desktop_config')
154+
setShowRestartModal(!getNeverShowAgain())
155+
}
156+
}}>Add Docker MCP</Button>
128157
}
129-
}}>Remove Docker MCP</Button>}
130-
131-
{status.state === 'missing docker_mcp' && <Button onClick={() => {
132-
trackEvent('claude-config-changed', { action: 'add' })
133-
// Add docker_mcp to config
134-
if (claudeConfig && claudeConfig.mcpServers) {
135-
const newConfig = { ...claudeConfig }
136-
newConfig.mcpServers.mcp_docker = DOCKER_MCP_CONFIG
137-
setClaudeConfig(newConfig)
138-
writeFilesToHost(client, [{ path: 'config.json', content: JSON.stringify(newConfig, null, 2) }], [{ source: configPath!, target: '/claude_desktop_config/config.json' }], '/claude_desktop_config')
139-
setShowRestartModal(!getNeverShowAgain())
140-
setShowConfigModal(false)
158+
{
159+
status.state === 'missing claude config' && <Button onClick={async () => {
160+
trackEvent('claude-config-changed', { action: 'write' })
161+
await writeFilesToHost(client, [{
162+
path: 'claude_desktop_config.json', content: JSON.stringify({
163+
mcpServers: {
164+
mcp_docker: DOCKER_MCP_CONFIG
165+
}
166+
}, null, 2)
167+
}], [{ source: configPath!.split('/').slice(0, -1).join('/'), target: '/claude_desktop_config' }], '/claude_desktop_config')
168+
refreshConfig()
169+
client.desktopUI.toast.success('Claude Desktop Config written')
170+
}
171+
}>Write Claude Desktop Config</Button>
141172
}
142-
}}>Add Docker MCP</Button>
143-
}
173+
{status.state !== 'missing claude config' && <Button color="error" variant={deleteReady ? 'contained' : 'outlined'} onClick={async () => {
174+
if (!deleteReady) {
175+
setDeleteReady(true)
176+
setTimeout(() => {
177+
setDeleteReady(false)
178+
}, 10000)
179+
}
180+
else {
181+
trackEvent('claude-config-changed', { action: 'delete' })
182+
try {
183+
await client.docker.cli.exec('run', ['--rm', '--mount', `type=bind,source="${configPath!.split('/').slice(0, -1).join('/')}",target=/claude_desktop_config`, 'alpine:latest', 'sh', '-c', '"rm -f /claude_desktop_config/claude_desktop_config.json"'])
184+
refreshConfig()
185+
setDeleteReady(false)
186+
} catch (error) {
187+
188+
if ((error as ExecResult).stderr) {
189+
client.desktopUI.toast.error('Error deleting config' + (error as ExecResult).stderr)
190+
console.error('Error deleting config', error)
191+
}
192+
193+
194+
}
195+
}
196+
}
197+
}>{deleteReady ? 'Click again to confirm deletion' : 'Delete Claude Desktop Config'}</Button>}
198+
199+
</Stack>
144200
<DialogContent>
145201
<DialogContentText component='pre'>
146-
<Typography >{JSON.stringify(claudeConfig, null, 2)}</Typography>
202+
{!claudeConfig && <Typography>No config found</Typography>}
203+
{claudeConfig && <Typography sx={{ fontSize: '1.1em', minWidth: 700, fontFamily: 'monospace', backgroundColor: 'black', color: 'white', padding: 2, borderRadius: 2 }}>{JSON.stringify(claudeConfig, null, 2)}</Typography>}
147204
</DialogContentText>
148205
</DialogContent>
206+
149207
</Dialog >
150208

151209
<Dialog open={showRestartModal} onClose={() => setShowRestartModal(false)} >
@@ -165,5 +223,5 @@ export const ClaudeConfigSyncStatus = ({ client, setHasConfig }: { client: v1.Do
165223
<Button sx={{ width: 150, height: 'auto', cursor: 'pointer' }} onClick={() => { setShowConfigModal(!showConfigModal) }} color='primary' variant="outlined">
166224
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 184 40" fill="currentColor"><path shapeRendering="optimizeQuality" fill="#D97757" d="m7.75 26.27 7.77-4.36.13-.38-.13-.21h-.38l-1.3-.08-4.44-.12-3.85-.16-3.73-.2-.94-.2L0 19.4l.09-.58.79-.53 1.13.1 2.5.17 3.75.26 2.72.16 4.03.42h.64l.09-.26-.22-.16-.17-.16-3.88-2.63-4.2-2.78-2.2-1.6L3.88 11l-.6-.76-.26-1.66L4.1 7.39l1.45.1.37.1 1.47 1.13 3.14 2.43 4.1 3.02.6.5.24-.17.03-.12-.27-.45L13 9.9l-2.38-4.1-1.06-1.7-.28-1.02c-.1-.42-.17-.77-.17-1.2L10.34.21l.68-.22 1.64.22.69.6 1.02 2.33 1.65 3.67 2.56 4.99.75 1.48.4 1.37.15.42h.26v-.24l.21-2.81.39-3.45.38-4.44.13-1.25.62-1.5L23.1.57l.96.46.79 1.13-.11.73-.47 3.05-.92 4.78-.6 3.2h.35l.4-.4 1.62-2.15 2.72-3.4 1.2-1.35 1.4-1.49.9-.71h1.7l1.25 1.86-.56 1.92-1.75 2.22-1.45 1.88-2.08 2.8-1.3 2.24.12.18.31-.03 4.7-1 2.54-.46 3.03-.52 1.37.64.15.65-.54 1.33-3.24.8-3.8.76-5.66 1.34-.07.05.08.1 2.55.24 1.09.06h2.67l4.97.37 1.3.86.78 1.05-.13.8-2 1.02-2.7-.64-6.3-1.5-2.16-.54h-.3v.18l1.8 1.76 3.3 2.98 4.13 3.84.21.95-.53.75-.56-.08-3.63-2.73-1.4-1.23-3.17-2.67h-.21v.28l.73 1.07 3.86 5.8.2 1.78-.28.58-1 .35-1.1-.2L26 33.14l-2.33-3.57-1.88-3.2-.23.13-1.11 11.95-.52.61-1.2.46-1-.76-.53-1.23.53-2.43.64-3.17.52-2.52.47-3.13.28-1.04-.02-.07-.23.03-2.36 3.24-3.59 4.85-2.84 3.04-.68.27-1.18-.61.11-1.09.66-.97 3.93-5 2.37-3.1 1.53-1.79-.01-.26h-.09L6.8 30.56l-1.86.24-.8-.75.1-1.23.38-.4 3.14-2.16Z"></path><path shapeRendering="optimizeQuality" d="M64.48 33.54c-5.02 0-8.45-2.8-10.07-7.11a19.19 19.19 0 0 1-1.23-7.03c0-7.23 3.24-12.25 10.4-12.25 4.81 0 7.78 2.1 9.47 7.11h2.06l-.28-6.91c-2.88-1.86-6.48-2.8-10.86-2.8-6.17 0-11.42 2.76-14.34 7.74a16.77 16.77 0 0 0-2.22 8.65c0 5.53 2.61 10.43 7.51 13.15a17.51 17.51 0 0 0 8.73 2.06c4.78 0 8.57-.91 11.93-2.5l.87-7.62h-2.1c-1.26 3.48-2.76 5.57-5.25 6.68-1.22.55-2.76.83-4.62.83ZM86.13 7.15l.2-3.4h-1.42l-6.32 1.9v1.03l2.8 1.3v23.78c0 1.62-.83 1.98-3 2.25v1.74h10.75v-1.74c-2.18-.27-3-.63-3-2.25V7.16Zm42.75 29h.83l7.27-1.38v-1.78l-1.02-.08c-1.7-.16-2.14-.51-2.14-1.9V18.33l.2-4.07h-1.15l-6.87.99v1.74l.67.12c1.86.27 2.41.79 2.41 2.09v11.3c-1.78 1.38-3.48 2.25-5.5 2.25-2.24 0-3.63-1.14-3.63-3.8V18.34l.2-4.07h-1.18l-6.88.99v1.74l.71.12c1.86.27 2.41.79 2.41 2.09v10.43c0 4.42 2.5 6.52 6.48 6.52 3.04 0 5.53-1.62 7.4-3.87l-.2 3.87ZM108.9 22.08c0-5.65-3-7.82-8.42-7.82-4.78 0-8.25 1.98-8.25 5.26 0 .98.35 1.73 1.06 2.25l3.64-.48c-.16-1.1-.24-1.77-.24-2.05 0-1.86.99-2.8 3-2.8 2.97 0 4.47 2.09 4.47 5.45v1.1l-7.5 2.25c-2.5.68-3.92 1.27-4.87 2.65a5 5 0 0 0-.7 2.8c0 3.2 2.2 5.46 5.96 5.46 2.72 0 5.13-1.23 7.23-3.56.75 2.33 1.9 3.56 3.95 3.56 1.66 0 3.16-.67 4.5-1.98l-.4-1.38c-.58.16-1.14.24-1.73.24-1.15 0-1.7-.91-1.7-2.69v-8.26Zm-9.6 10.87c-2.05 0-3.32-1.19-3.32-3.28 0-1.42.67-2.25 2.1-2.73l6.08-1.93v5.84c-1.94 1.47-3.08 2.1-4.86 2.1Zm63.3 1.82v-1.78l-1.03-.08c-1.7-.16-2.13-.51-2.13-1.9V7.15l.2-3.4h-1.43l-6.32 1.9v1.03l2.8 1.3v7.82a8.83 8.83 0 0 0-5.37-1.54c-6.28 0-11.18 4.78-11.18 11.93 0 5.89 3.52 9.96 9.32 9.96 3 0 5.61-1.46 7.23-3.72l-.2 3.72h.84l7.27-1.38Zm-13.16-18.14c3 0 5.25 1.74 5.25 4.94v9a7.2 7.2 0 0 1-5.21 2.1c-4.3 0-6.48-3.4-6.48-7.94 0-5.1 2.49-8.1 6.44-8.1Zm28.53 4.5c-.56-2.64-2.18-4.14-4.43-4.14-3.36 0-5.69 2.53-5.69 6.16 0 5.37 2.84 8.85 7.43 8.85a8.6 8.6 0 0 0 7.39-4.35l1.34.36c-.6 4.66-4.82 8.14-10 8.14-6.08 0-10.27-4.5-10.27-10.9 0-6.45 4.55-10.99 10.63-10.99 4.54 0 7.74 2.73 8.77 7.47l-15.84 4.86v-2.14l10.67-3.31Z"></path></svg>
167225
</Button>
168-
</Badge>
226+
</Badge >
169227
}

src/extension/ui/src/components/PromptCard.tsx

Lines changed: 49 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,18 @@
1-
import { CircularProgress, Stack } from "@mui/material";
1+
import { Badge, CircularProgress, Stack, Tooltip } from "@mui/material";
22
import Button from '@mui/material/Button';
33
import { Card, CardActions, CardContent, CardMedia, Typography } from "@mui/material";
44
import { Ref } from "../Refs";
55
import { useState } from "react";
66
import { trackEvent } from "../Usage";
7+
import { Article, AttachFile, Build } from "@mui/icons-material";
78

89
export interface CatalogItem {
910
description?: string;
1011
icon?: string;
1112
ref: string;
13+
prompts: number;
14+
resources: object[];
15+
tools: object[];
1216
}
1317

1418
export interface CatalogItemWithName extends CatalogItem {
@@ -23,7 +27,6 @@ export function CatalogItemCard({ openUrl, item, canRegister, registered, regist
2327
<CardContent>
2428
<a href="">
2529
<Stack onClick={openUrl} direction="row" spacing={2} justifyContent="space-between" sx={{ cursor: 'pointer' }}>
26-
2730
<Typography gutterBottom variant="h5" component="div">
2831
{item.name}
2932
</Typography>
@@ -40,25 +43,50 @@ export function CatalogItemCard({ openUrl, item, canRegister, registered, regist
4043
</Typography>
4144
</CardContent>
4245
<CardActions>
43-
<Button
44-
size="small"
45-
onClick={() => {
46-
trackEvent('registry-changed', { name: item.name, ref: item.ref, action: registered ? 'remove' : 'add' });
47-
setIsRegistering(true)
48-
if (registered) {
49-
unregister(item).then(() => {
50-
setIsRegistering(false)
51-
})
52-
} else {
53-
register(item).then(() => {
54-
setIsRegistering(false)
55-
})
56-
}
57-
}}
58-
disabled={!canRegister || isRegistering}
59-
>
60-
{isRegistering ? <CircularProgress size={20} /> : registered ? 'Remove' : 'Add'}
61-
</Button>
46+
<Stack direction="row" spacing={2} sx={{ alignItems: 'center', justifyContent: 'space-between', width: '100%' }}>
47+
<Stack direction="row" spacing={2}>
48+
<Tooltip title="Prompts">
49+
<Badge badgeContent={item.prompts || "0"} color="primary">
50+
<Article />
51+
</Badge>
52+
</Tooltip>
53+
<Tooltip title="Resources">
54+
55+
<Badge badgeContent={item.resources?.length || "0"} color="secondary">
56+
57+
<AttachFile />
58+
</Badge>
59+
</Tooltip>
60+
<Tooltip title="Tools">
61+
<Badge badgeContent={item.tools?.length || "0"} color="success">
62+
<Build />
63+
</Badge>
64+
</Tooltip>
65+
66+
</Stack>
67+
<Button
68+
size="small"
69+
70+
71+
onClick={() => {
72+
trackEvent('registry-changed', { name: item.name, ref: item.ref, action: registered ? 'remove' : 'add' });
73+
setIsRegistering(true)
74+
if (registered) {
75+
unregister(item).then(() => {
76+
setIsRegistering(false)
77+
})
78+
} else {
79+
register(item).then(() => {
80+
setIsRegistering(false)
81+
})
82+
}
83+
}}
84+
disabled={!canRegister || isRegistering}
85+
>
86+
{isRegistering ? <CircularProgress size={20} /> : registered ? 'Remove' : 'Add'}
87+
</Button>
88+
</Stack>
89+
6290
</CardActions>
6391
</Stack>
6492
</Card >

0 commit comments

Comments
 (0)