Ship Paseo-equivalent provider snapshot, AgentComposerBar, ACP dispatch rewrite with streaming/persist, permission prompts, and agent commands. Follow-up: pane-scoped chat resolution, CoderMessageList tool timeline, WS user-delta replace, and inference orphan tool_call stripping. Archive openspec v2-2; update CHANGELOG and CURRENT. Co-authored-by: Cursor <cursoragent@cursor.com>
92 lines
3.1 KiB
TypeScript
92 lines
3.1 KiB
TypeScript
import type { FastifyInstance } from 'fastify';
|
|
import WebSocket from 'ws';
|
|
|
|
function boocoderWsUrl(origin: string, path: string): string {
|
|
const u = new URL(origin);
|
|
u.protocol = u.protocol === 'https:' ? 'wss:' : 'ws:';
|
|
u.pathname = path;
|
|
u.search = '';
|
|
return u.toString();
|
|
}
|
|
|
|
/**
|
|
* Reverse-proxy BooCoder HTTP + WebSocket through BooChat's single origin.
|
|
* WS must be registered before the HTTP catch-all — fetch() cannot upgrade.
|
|
*/
|
|
export function registerCoderProxy(app: FastifyInstance, boocoderOrigin: string): void {
|
|
app.get<{ Params: { sessionId: string } }>(
|
|
'/api/coder/ws/sessions/:sessionId',
|
|
{ websocket: true },
|
|
(clientSocket, req) => {
|
|
const sessionId = req.params.sessionId;
|
|
const target = boocoderWsUrl(boocoderOrigin, `/api/ws/sessions/${sessionId}`);
|
|
const upstream = new WebSocket(target);
|
|
|
|
upstream.on('open', () => {
|
|
app.log.debug({ sessionId }, 'coder ws proxy: upstream connected');
|
|
});
|
|
|
|
upstream.on('message', (data, isBinary) => {
|
|
if (clientSocket.readyState !== clientSocket.OPEN) return;
|
|
clientSocket.send(data, { binary: isBinary });
|
|
});
|
|
|
|
upstream.on('close', (code, reason) => {
|
|
if (clientSocket.readyState === clientSocket.OPEN) {
|
|
clientSocket.close(code, reason.toString());
|
|
}
|
|
});
|
|
|
|
upstream.on('error', (err) => {
|
|
app.log.warn({ err, sessionId, target }, 'coder ws proxy: upstream error');
|
|
if (clientSocket.readyState === clientSocket.OPEN) {
|
|
clientSocket.close(1011, 'upstream error');
|
|
}
|
|
});
|
|
|
|
clientSocket.on('message', (data, isBinary) => {
|
|
if (upstream.readyState !== WebSocket.OPEN) return;
|
|
upstream.send(data, { binary: isBinary });
|
|
});
|
|
|
|
clientSocket.on('close', () => {
|
|
if (upstream.readyState === WebSocket.OPEN || upstream.readyState === WebSocket.CONNECTING) {
|
|
upstream.close();
|
|
}
|
|
});
|
|
|
|
clientSocket.on('error', () => {
|
|
if (upstream.readyState === WebSocket.OPEN || upstream.readyState === WebSocket.CONNECTING) {
|
|
upstream.close();
|
|
}
|
|
});
|
|
},
|
|
);
|
|
|
|
app.all('/api/coder/*', async (req, reply) => {
|
|
const targetPath = req.url.replace('/api/coder', '/api');
|
|
const targetUrl = `${boocoderOrigin}${targetPath}`;
|
|
const headers: Record<string, string> = {};
|
|
if (req.headers['content-type']) headers['content-type'] = req.headers['content-type'] as string;
|
|
if (req.headers['authorization']) headers['authorization'] = req.headers['authorization'] as string;
|
|
|
|
try {
|
|
const res = await fetch(targetUrl, {
|
|
method: req.method as string,
|
|
headers,
|
|
body: req.method !== 'GET' && req.method !== 'HEAD' ? JSON.stringify(req.body) : undefined,
|
|
});
|
|
reply.code(res.status);
|
|
for (const [key, value] of res.headers) {
|
|
if (key === 'transfer-encoding') continue;
|
|
reply.header(key, value);
|
|
}
|
|
const body = await res.text();
|
|
return reply.send(body);
|
|
} catch (err) {
|
|
app.log.error({ err, targetUrl }, 'coder proxy error');
|
|
reply.code(502).send({ error: 'boocoder backend unavailable' });
|
|
}
|
|
});
|
|
}
|