Scaffolda tunn vertikal slice: pnpm-monorepo (shared/daemon/server/web)
- shared: typat WebSocket-protokoll (daemon/server/web som enda sanning) - daemon: node-pty kör agenten (default gemini), strömmar pty över WS, auto-reconnect; fast pty-storlek - server: WS-mux + token-auth + session-registry + headless-xterm scrollback (SerializeAddon) + routing av input till rätt session - web: Vue 3 + Vite + xterm.js, session-lista, mobil-layout och fallback-tangentbord (piltangenter/Enter/Esc/Tab/Ctrl-C) som skickar escape-sekvenser rakt till pty:n -> styr sessionen även utan hooks - workspace: pnpm-workspace.yaml (onlyBuiltDependencies: node-pty, esbuild), tsconfig.base, .npmrc (retry-inställningar), .env.example Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
90
packages/web/src/lib/helm.ts
Normal file
90
packages/web/src/lib/helm.ts
Normal file
@@ -0,0 +1,90 @@
|
||||
import { ref } from "vue";
|
||||
import type { ServerToWeb, SessionMeta, WebMessage } from "@agent-helm/shared";
|
||||
|
||||
/** Callbacks som terminal-vyn registrerar för den session den visar. */
|
||||
export interface HelmHandlers {
|
||||
onSnapshot?: (sessionId: string, data: string, meta: SessionMeta) => void;
|
||||
onOutput?: (sessionId: string, data: string) => void;
|
||||
onExit?: (sessionId: string, code: number) => void;
|
||||
}
|
||||
|
||||
export function createHelmClient() {
|
||||
const connected = ref(false);
|
||||
const connecting = ref(false);
|
||||
const error = ref<string | null>(null);
|
||||
const sessions = ref<SessionMeta[]>([]);
|
||||
const handlers: HelmHandlers = {};
|
||||
|
||||
let ws: WebSocket | null = null;
|
||||
|
||||
function send(msg: WebMessage): void {
|
||||
if (ws && ws.readyState === WebSocket.OPEN) ws.send(JSON.stringify(msg));
|
||||
}
|
||||
|
||||
function connect(url: string, token: string): void {
|
||||
error.value = null;
|
||||
connecting.value = true;
|
||||
ws = new WebSocket(url);
|
||||
|
||||
ws.onopen = () => send({ type: "web:hello", token });
|
||||
|
||||
ws.onmessage = (ev) => {
|
||||
let msg: ServerToWeb;
|
||||
try {
|
||||
msg = JSON.parse(ev.data as string) as ServerToWeb;
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
switch (msg.type) {
|
||||
case "server:welcome":
|
||||
connected.value = true;
|
||||
connecting.value = false;
|
||||
break;
|
||||
case "server:sessions":
|
||||
sessions.value = msg.sessions;
|
||||
break;
|
||||
case "server:snapshot":
|
||||
handlers.onSnapshot?.(msg.sessionId, msg.data, msg.meta);
|
||||
break;
|
||||
case "server:output":
|
||||
handlers.onOutput?.(msg.sessionId, msg.data);
|
||||
break;
|
||||
case "server:exit":
|
||||
sessions.value = sessions.value.map((s) =>
|
||||
s.sessionId === msg.sessionId ? { ...s, status: "exited", exitCode: msg.code } : s,
|
||||
);
|
||||
handlers.onExit?.(msg.sessionId, msg.code);
|
||||
break;
|
||||
case "server:error":
|
||||
error.value = msg.message;
|
||||
connecting.value = false;
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
ws.onclose = () => {
|
||||
connected.value = false;
|
||||
connecting.value = false;
|
||||
};
|
||||
ws.onerror = () => {
|
||||
error.value = "Kunde inte ansluta till servern.";
|
||||
connecting.value = false;
|
||||
};
|
||||
}
|
||||
|
||||
function subscribe(sessionId: string): void {
|
||||
send({ type: "web:subscribe", sessionId });
|
||||
}
|
||||
function sendInput(sessionId: string, data: string): void {
|
||||
send({ type: "web:input", sessionId, data });
|
||||
}
|
||||
function disconnect(): void {
|
||||
ws?.close();
|
||||
ws = null;
|
||||
connected.value = false;
|
||||
}
|
||||
|
||||
return { connected, connecting, error, sessions, handlers, connect, disconnect, subscribe, sendInput };
|
||||
}
|
||||
|
||||
export type HelmClient = ReturnType<typeof createHelmClient>;
|
||||
Reference in New Issue
Block a user