feat: combine TUI UX from alexhancock/tui-goodness with publishing config from jackamadeo/package-tui (#7683)
Co-authored-by: Jack Amadeo <jamadeo@squareup.com>
This commit is contained in:
@@ -0,0 +1,2 @@
|
||||
dist/
|
||||
node_modules/
|
||||
+7
-4
@@ -1,11 +1,14 @@
|
||||
{
|
||||
"name": "goose-acp-types",
|
||||
"name": "@block/goose-acp",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"main": "./src/index.ts",
|
||||
"types": "./src/index.ts",
|
||||
"main": "./dist/index.js",
|
||||
"types": "./dist/index.d.ts",
|
||||
"files": [
|
||||
"dist"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "tsc",
|
||||
"generate": "tsx generate-schema.ts",
|
||||
"lint": "tsc --noEmit",
|
||||
"format": "prettier --write src/"
|
||||
|
||||
@@ -27,12 +27,17 @@ import {
|
||||
type SetSessionModelResponse,
|
||||
} from "@agentclientprotocol/sdk";
|
||||
import { GooseExtClient } from "./generated/client.gen.js";
|
||||
import { createHttpStream } from "./http-stream.js";
|
||||
|
||||
export class GooseClient {
|
||||
private conn: ClientSideConnection;
|
||||
private ext: GooseExtClient;
|
||||
|
||||
constructor(toClient: () => Client, stream: Stream) {
|
||||
constructor(toClient: () => Client, streamOrUrl: Stream | string) {
|
||||
const stream =
|
||||
typeof streamOrUrl === "string"
|
||||
? createHttpStream(streamOrUrl)
|
||||
: streamOrUrl;
|
||||
this.conn = new ClientSideConnection(toClient, stream);
|
||||
this.ext = new GooseExtClient(this.conn);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,120 @@
|
||||
import type { AnyMessage, Stream } from "@agentclientprotocol/sdk";
|
||||
|
||||
const ACP_SESSION_HEADER = "Acp-Session-Id";
|
||||
|
||||
export function createHttpStream(serverUrl: string): Stream {
|
||||
let sessionId: string | null = null;
|
||||
const incoming: AnyMessage[] = [];
|
||||
const waiters: Array<() => void> = [];
|
||||
const sseAbort = new AbortController();
|
||||
|
||||
function pushMessage(msg: AnyMessage) {
|
||||
incoming.push(msg);
|
||||
const w = waiters.shift();
|
||||
if (w) w();
|
||||
}
|
||||
|
||||
function waitForMessage(): Promise<void> {
|
||||
if (incoming.length > 0) return Promise.resolve();
|
||||
return new Promise<void>((r) => waiters.push(r));
|
||||
}
|
||||
|
||||
async function consumeSSE(response: Response) {
|
||||
if (!response.body) return;
|
||||
const reader = response.body.getReader();
|
||||
const decoder = new TextDecoder();
|
||||
let buffer = "";
|
||||
|
||||
try {
|
||||
while (true) {
|
||||
const { done, value } = await reader.read();
|
||||
if (done) break;
|
||||
buffer += decoder.decode(value, { stream: true });
|
||||
|
||||
const parts = buffer.split("\n\n");
|
||||
buffer = parts.pop() || "";
|
||||
|
||||
for (const part of parts) {
|
||||
for (const line of part.split("\n")) {
|
||||
if (line.startsWith("data: ")) {
|
||||
try {
|
||||
const msg = JSON.parse(line.slice(6)) as AnyMessage;
|
||||
pushMessage(msg);
|
||||
} catch {
|
||||
// ignore malformed JSON
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e: unknown) {
|
||||
if (e instanceof DOMException && e.name === "AbortError") return;
|
||||
}
|
||||
}
|
||||
|
||||
let isFirstRequest = true;
|
||||
|
||||
const readable = new ReadableStream<AnyMessage>({
|
||||
async pull(controller) {
|
||||
await waitForMessage();
|
||||
while (incoming.length > 0) {
|
||||
controller.enqueue(incoming.shift()!);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
const writable = new WritableStream<AnyMessage>({
|
||||
async write(msg) {
|
||||
const isRequest =
|
||||
"method" in msg &&
|
||||
"id" in msg &&
|
||||
msg.id !== undefined &&
|
||||
msg.id !== null;
|
||||
|
||||
const headers: Record<string, string> = {
|
||||
"Content-Type": "application/json",
|
||||
Accept: "application/json, text/event-stream",
|
||||
};
|
||||
if (sessionId) {
|
||||
headers[ACP_SESSION_HEADER] = sessionId;
|
||||
}
|
||||
|
||||
if (isFirstRequest && isRequest) {
|
||||
isFirstRequest = false;
|
||||
|
||||
const response = await fetch(`${serverUrl}/acp`, {
|
||||
method: "POST",
|
||||
headers,
|
||||
body: JSON.stringify(msg),
|
||||
signal: sseAbort.signal,
|
||||
});
|
||||
|
||||
const sid = response.headers.get(ACP_SESSION_HEADER);
|
||||
if (sid) sessionId = sid;
|
||||
|
||||
consumeSSE(response);
|
||||
} else if (isRequest) {
|
||||
const abort = new AbortController();
|
||||
fetch(`${serverUrl}/acp`, {
|
||||
method: "POST",
|
||||
headers,
|
||||
body: JSON.stringify(msg),
|
||||
signal: abort.signal,
|
||||
}).catch(() => {});
|
||||
setTimeout(() => abort.abort(), 200);
|
||||
} else {
|
||||
await fetch(`${serverUrl}/acp`, {
|
||||
method: "POST",
|
||||
headers,
|
||||
body: JSON.stringify(msg),
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
close() {
|
||||
sseAbort.abort();
|
||||
},
|
||||
});
|
||||
|
||||
return { readable, writable };
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
export * from "./generated/types.gen.js";
|
||||
export * from "./generated/zod.gen.js";
|
||||
export { GooseClient } from "./goose-client.js";
|
||||
export { createHttpStream } from "./http-stream.js";
|
||||
|
||||
export {
|
||||
ClientSideConnection,
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"module": "NodeNext",
|
||||
"moduleResolution": "nodenext",
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src"
|
||||
"rootDir": "./src",
|
||||
},
|
||||
"include": ["src"]
|
||||
"include": ["src"],
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user