v2.2-paseo-providers: Paseo provider stack + v2.2.1 pane-scoped chat fixes
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>
This commit is contained in:
91
apps/server/src/routes/coder-proxy.ts
Normal file
91
apps/server/src/routes/coder-proxy.ts
Normal file
@@ -0,0 +1,91 @@
|
||||
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' });
|
||||
}
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user