Skip to content

Commit 4072821

Browse files
author
colinmcneil
committed
Add JSON RPC stream
1 parent 6fff4c2 commit 4072821

File tree

2 files changed

+137
-44
lines changed

2 files changed

+137
-44
lines changed

src/extension/ui/src/App.tsx

Lines changed: 113 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,18 @@ import React, { useEffect } from 'react';
22
import Button from '@mui/material/Button';
33
import DelIcon from '@mui/icons-material/Delete';
44
import { createDockerDesktopClient } from '@docker/extension-api-client';
5-
import { IconButton, Link, List, ListItem, ListItemButton, ListItemText, Paper, Stack, TextField, Typography } from '@mui/material';
5+
import { Chip, IconButton, Link, List, ListItem, ListItemButton, ListItemText, Paper, Stack, TextField, Typography } from '@mui/material';
66
import { getRunArgs } from './args';
77
import Convert from 'ansi-to-html';
88

99
const convert = new Convert({ newline: true });
1010

11+
type RPCMessage = {
12+
jsonrpc?: string;
13+
method: string;
14+
params: any;
15+
}
16+
1117
// Note: This line relies on Docker Desktop's presence as a host application.
1218
// If you're running this React app in a browser, it won't work properly.
1319
const client = createDockerDesktopClient();
@@ -33,10 +39,11 @@ export function App() {
3339

3440
const [promptInput, setPromptInput] = React.useState<string>('');
3541

36-
const [runOut, setRunOut] = React.useState<string>('');
42+
const [runOut, setRunOut] = React.useState<RPCMessage[]>([]);
3743

3844
const scrollRef = React.useRef<HTMLDivElement>(null);
3945

46+
const [showDebug, setShowDebug] = React.useState(false);
4047

4148
useEffect(() => {
4249
localStorage.setItem('projects', JSON.stringify(projects));
@@ -90,12 +97,38 @@ export function App() {
9097
const delim = client.host.platform === 'win32' ? '\\' : '/';
9198

9299
const startPrompt = async () => {
93-
let output = ""
94-
const updateOutput = (data: string) => {
95-
output += data;
100+
let output: RPCMessage[] = []
101+
const updateOutput = (line: RPCMessage) => {
102+
if (line.method === 'functions') {
103+
const functions = line.params;
104+
for (const func of functions) {
105+
const functionId = func.id;
106+
const existingFunction = output.find(o =>
107+
o.method === 'functions'
108+
&&
109+
o.params.find((p: { id: string }) => p.id === functionId)
110+
);
111+
if (existingFunction) {
112+
const existingFunctionParamsIndex = existingFunction.params.findIndex((p: { id: string }) => p.id === functionId);
113+
existingFunction.params[existingFunctionParamsIndex] = { ...existingFunction.params[existingFunctionParamsIndex], ...func };
114+
output = output.map(
115+
o => o.method === 'functions'
116+
?
117+
{ ...o, params: o.params.map((p: { id: string }) => p.id === functionId ? { ...p, ...func } : p) }
118+
:
119+
o
120+
);
121+
} else {
122+
output = [...output, line];
123+
}
124+
}
125+
}
126+
else {
127+
output = [...output, line];
128+
}
96129
setRunOut(output);
97130
}
98-
updateOutput("Pulling images\n")
131+
updateOutput({ method: 'message', params: { debug: 'Pulling images' } })
99132
try {
100133
const pullWriteFiles = await client.docker.cli.exec("pull", ["vonwig/function_write_files"]);
101134
const pullPrompts = await client.docker.cli.exec("pull", ["vonwig/prompts"]);
@@ -106,12 +139,12 @@ export function App() {
106139
"vonwig/function_write_files",
107140
`'` + JSON.stringify({ files: [{ path: ".openai-api-key", content: openAIKey, executable: false }] }) + `'`
108141
]);
109-
updateOutput(JSON.stringify({ pullWriteFiles, pullPrompts, writeKey }));
142+
updateOutput({ method: 'message', params: { debug: JSON.stringify({ pullWriteFiles, pullPrompts, writeKey }) } });
110143
}
111144
catch (e) {
112-
updateOutput(JSON.stringify(e));
145+
updateOutput({ method: 'message', params: { debug: JSON.stringify(e) } });
113146
}
114-
updateOutput("Running prompts\n")
147+
updateOutput({ method: 'message', params: { debug: 'Running prompts...' } })
115148
const args = getRunArgs(selectedPrompt!, selectedProject!, "", client.host.platform)
116149

117150
client.docker.cli.exec("run", args, {
@@ -120,24 +153,33 @@ export function App() {
120153
onOutput: ({ stdout, stderr }) => {
121154
if (stdout && stdout.startsWith('{')) {
122155
let rpcMessage = stdout.split('}Content-Length:')[0]
123-
if (!rpcMessage.endsWith('}')) {
156+
if (!rpcMessage.endsWith('}}')) {
124157
rpcMessage += '}'
125158
}
126159
const json = JSON.parse(rpcMessage)
127-
if (json.params.content) {
128-
output += json.params.content
129-
}
160+
updateOutput(json)
161+
// {
162+
// "jsonrpc": "2.0",
163+
// "method": "functions",
164+
// "params": [
165+
// {
166+
// "function": {
167+
// "name": "run-eslint",
168+
// "arguments": "{\n \""
169+
// },
170+
// "id": "call_53E2o4fq1QEmIHixWcKZmOqo"
171+
// }
172+
// ]
173+
// }
130174
}
131175
if (stderr) {
132-
output += stderr
176+
updateOutput({ method: 'message', params: { debug: stderr } });
133177
}
134-
setRunOut(output);
135178
},
136179
onError: (err) => {
137180
console.error(err);
138-
output += err;
139-
setRunOut(output);
140-
}
181+
updateOutput({ method: 'message', params: { debug: err } });
182+
},
141183
}
142184
});
143185
}
@@ -196,18 +238,31 @@ export function App() {
196238
{/* Prompts column */}
197239
<Paper sx={{ padding: 1 }}>
198240
<Typography variant="h3">Prompts</Typography>
199-
<TextField
200-
sx={{ width: '100%', mt: 1 }}
201-
placeholder='Enter GitHub ref or URL'
202-
value={promptInput}
203-
onChange={(e) => setPromptInput(e.target.value)}
204-
/>
205-
{promptInput.length > 0 && (
241+
<Stack direction='row' spacing={1} alignItems={'center'} justifyContent={'space-between'}>
242+
<TextField
243+
fullWidth
244+
placeholder='Enter GitHub ref or URL'
245+
value={promptInput}
246+
onChange={(e) => setPromptInput(e.target.value)}
247+
/>
248+
{promptInput.length > 0 && (
249+
<Button onClick={() => {
250+
setPrompts([...prompts, promptInput]);
251+
setPromptInput('');
252+
}}>Add prompt</Button>
253+
)}
206254
<Button onClick={() => {
207-
setPrompts([...prompts, promptInput]);
208-
setPromptInput('');
209-
}}>Add prompt</Button>
210-
)}
255+
client.desktopUI.dialog.showOpenDialog({
256+
properties: ['openDirectory', 'multiSelections']
257+
}).then((result) => {
258+
if (result.canceled) {
259+
return;
260+
}
261+
setPrompts([...prompts, ...result.filePaths.map(p => `local://${p}`)]);
262+
});
263+
}}>Add local prompt</Button>
264+
</Stack>
265+
211266
<List>
212267
{prompts.map((prompt) => (
213268
<ListItem
@@ -232,8 +287,12 @@ export function App() {
232287
}>
233288
<ListItemButton sx={{ padding: 0, pl: 1.5 }} onClick={() => {
234289
setSelectedPrompt(prompt);
235-
}}>
236-
<ListItemText primary={prompt.split(delim).pop()} secondary={prompt} />
290+
}}>{
291+
prompt.startsWith('local://') ?
292+
<><ListItemText primary={<>{prompt.split(delim).pop()}<Chip sx={{ ml: 1 }} label='local' /></>} secondary={prompt.replace('local://', '')} /></>
293+
:
294+
<ListItemText primary={prompt.split('/').pop()} secondary={prompt} />
295+
}
237296
</ListItemButton>
238297
</ListItem>
239298
))}
@@ -259,10 +318,30 @@ export function App() {
259318
)}
260319
{/* Show run output */}
261320
{
262-
runOut && (
321+
runOut.length > 0 && (
263322
<Paper sx={{ p: 1 }}>
264-
<Typography variant='h3'>Run output</Typography>
265-
<div style={{ whiteSpace: 'pre-wrap' }} dangerouslySetInnerHTML={{ __html: convert.toHtml(runOut) }} />
323+
<Stack direction='row' spacing={1} alignItems={'center'} justifyContent={'space-between'}>
324+
<Typography variant='h3'>Run output</Typography>
325+
<Button onClick={() => setShowDebug(!showDebug)}>{showDebug ? 'Hide' : 'Show'} debug</Button>
326+
</Stack>
327+
328+
<div style={{ overflow: 'auto', maxHeight: '100vh' }}>
329+
{runOut.map((line, i) => {
330+
if (line.method === 'message') {
331+
if (line.params.debug) {
332+
return showDebug ? <Typography key={i} variant='body1' sx={theme => ({ color: theme.palette.docker.grey[400] })}>{line.params.debug}</Typography> : null;
333+
}
334+
if (line.params.role === 'assistant') {
335+
return <Typography key={i} variant='body1' sx={theme => ({ color: theme.palette.docker.blue[400] })}>{line.params.content}</Typography>
336+
}
337+
return <pre key={i} style={{ whiteSpace: 'pre-wrap', display: 'inline' }} dangerouslySetInnerHTML={{ __html: convert.toHtml(line.params.content) }} />
338+
}
339+
if (line.method === 'functions') {
340+
return <Typography key={i} variant='body1' sx={{ whiteSpace: 'pre-wrap' }}>{JSON.stringify(line.params, null, 2)}</Typography>
341+
}
342+
return <Typography key={i} variant='body1'>{JSON.stringify(line)}</Typography>
343+
})}
344+
</div>
266345
</Paper>
267346
)
268347
}

src/extension/ui/src/args.ts

Lines changed: 24 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,32 @@
1-
export const getRunArgs = (prompt_ref: string, project_dir: string, username: string, platform: string) => {
2-
return [
1+
export const getRunArgs = (promptRef: string, projectDir: string, username: string, platform: string) => {
2+
const isLocal = promptRef.startsWith('local://');
3+
let promptArgs: string[] = ["--prompts", promptRef];
4+
let mountArgs: string[] = [];
5+
6+
if (isLocal) {
7+
const localPromptPath = promptRef.replace('local://', '');
8+
const pathSeparator = platform === 'win32' ? '\\' : '/';
9+
promptRef = localPromptPath.split(pathSeparator).pop() || 'unknown-local-prompt';
10+
promptArgs = ["--prompts-dir", `/app/${promptRef}`];
11+
mountArgs = ["--mount", `type=bind,source=${localPromptPath},target=/app/${promptRef}`];
12+
}
13+
14+
const baseArgs: string[] = [
315
'--rm',
4-
'-v',
5-
'/var/run/docker.sock:/var/run/docker.sock',
6-
'-v',
7-
'openai_key:/root',
8-
'--mount',
9-
'type=volume,source=docker-prompts,target=/prompts',
16+
'-v', '/var/run/docker.sock:/var/run/docker.sock',
17+
'-v', 'openai_key:/root',
18+
'--mount', 'type=volume,source=docker-prompts,target=/prompts'
19+
];
20+
21+
const runArgs: string[] = [
1022
'vonwig/prompts:latest',
1123
'run',
12-
"--host-dir", project_dir,
24+
"--host-dir", projectDir,
1325
"--user", username,
1426
"--platform", platform,
15-
"--prompts", prompt_ref,
27+
...promptArgs,
1628
'--jsonrpc'
1729
];
30+
31+
return [...baseArgs, ...mountArgs, ...runArgs];
1832
}

0 commit comments

Comments
 (0)