Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion apps/web/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "tracevane-web",
"version": "0.2.4",
"version": "0.2.5",
"private": true,
"type": "module",
"scripts": {
Expand Down
34 changes: 14 additions & 20 deletions apps/web/src/features/ide-workbench/terminal/terminalTransport.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,9 @@ import {
type CreateWorkbenchTerminalOptions,
type WorkbenchTerminalEvent,
} from "./terminalClient";
import { createGatewayTerminalTransport } from "./terminalGatewayTransport";
import { createSseTerminalTransport } from "./terminalSseTransport";
import {
createTerminalTransportFallbackChain,
resolveTerminalTransportChannel,
type TerminalFallbackNotice,
type TerminalTransportFailure,
} from "./terminalTransportChain";
Expand All @@ -33,8 +32,9 @@ export interface WorkbenchTerminalTransportHandlers {

/**
* Mode-agnostic terminal realtime channel. Standalone mode uses the raw
* `/ws/terminal` WebSocket; OpenClaw gateway single-port mode tunnels the
* session through host gateway RPC (`terminalGatewayTransport.ts`).
* `/ws/terminal` WebSocket; OpenClaw gateway single-port mode uses the
* same-origin HTTP/SSE channel so PTY output is not delayed by host event
* scheduling.
*/
export interface WorkbenchTerminalTransport {
sendInput: (data: string) => void;
Expand Down Expand Up @@ -120,12 +120,10 @@ function createFailedTerminalTransport(

/**
* Picks the realtime transport from the injected runtime config:
* `realtimeTransport: "gateway-rpc"` (gateway exposure) tunnels through the
* OpenClaw host gateway, with an automatic fallback to the HTTP/SSE compat
* channel (`terminalSseTransport.ts`) when the host rejects the RPC attach
* (auth/pairing/protocol) — see `terminalTransportChain.ts`; `"raw-ws"` keeps
* the direct WebSocket; `"disabled"` or `features.terminalRealtime === false`
* yields a structured error state.
* `realtimeTransport: "gateway-rpc"` (gateway exposure) uses the HTTP/SSE
* channel because OpenClaw host gateway events are not flushed at interactive
* terminal latency. `"raw-ws"` keeps the direct WebSocket; `"disabled"` or
* `features.terminalRealtime === false` yields a structured error state.
*/
export function connectWorkbenchTerminal(
sid: string,
Expand All @@ -139,22 +137,18 @@ export function connectWorkbenchTerminal(
handlers,
);
}
const transportKind =
runtime?.realtimeTransport ?? (isGatewayExposure() ? "gateway-rpc" : "raw-ws");
const transportKind = resolveTerminalTransportChannel(
runtime?.realtimeTransport,
isGatewayExposure(),
);
if (transportKind === "disabled") {
return createFailedTerminalTransport(
"服务端已禁用终端实时通道(realtimeTransport=disabled)。",
handlers,
);
}
if (transportKind === "gateway-rpc") {
return createTerminalTransportFallbackChain<WorkbenchTerminalEvent>({
createPrimary: (chainHandlers) =>
createGatewayTerminalTransport(sid, options, chainHandlers),
createFallback: (chainHandlers) =>
createSseTerminalTransport(sid, options, chainHandlers),
handlers,
});
if (transportKind === "sse") {
return createSseTerminalTransport(sid, options, handlers);
}
return createRawWebSocketTerminalTransport(sid, options, handlers);
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
computeReconnectDelayMs,
createTerminalTransportFallbackChain,
isGatewayFallbackEligible,
resolveTerminalTransportChannel,
} from "./terminalTransportChain.ts";

function createMockFactory(name, created) {
Expand Down Expand Up @@ -186,3 +187,11 @@ test("buildCompatChannelNotice keeps reason and adds actionable guidance", () =>
const fallback = buildCompatChannelNotice({ kind: "network", message: "" });
assert.match(fallback.detail, /网关通道不可用/, "empty reason gets a sane default");
});

test("OpenClaw gateway exposure selects immediate SSE output", () => {
assert.equal(resolveTerminalTransportChannel("gateway-rpc", true), "sse");
assert.equal(resolveTerminalTransportChannel(undefined, true), "sse");
assert.equal(resolveTerminalTransportChannel("raw-ws", true), "raw-ws");
assert.equal(resolveTerminalTransportChannel(undefined, false), "raw-ws");
assert.equal(resolveTerminalTransportChannel("disabled", true), "disabled");
});
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,25 @@ export const TERMINAL_RECONNECT_MAX_ATTEMPTS = 5;
export const TERMINAL_RECONNECT_BASE_DELAY_MS = 1_000;
export const TERMINAL_RECONNECT_MAX_DELAY_MS = 15_000;

export type TerminalRealtimeTransportSetting =
| "gateway-rpc"
| "raw-ws"
| "disabled";

/**
* OpenClaw gateway events are host-scheduled and can delay interactive PTY
* output. Its same-origin HTTP/SSE stream is flushed immediately, so gateway
* exposure uses that channel while standalone mode keeps the raw WebSocket.
*/
export function resolveTerminalTransportChannel(
configured: TerminalRealtimeTransportSetting | undefined,
gatewayExposure: boolean,
): "sse" | "raw-ws" | "disabled" {
const setting = configured ?? (gatewayExposure ? "gateway-rpc" : "raw-ws");
if (setting === "disabled") return "disabled";
return setting === "gateway-rpc" ? "sse" : "raw-ws";
}

/**
* Exponential backoff for abnormal-close re-attach: 1s → 2s → 4s → 8s → 15s
* (capped). `attemptIndex` is zero-based (first retry waits 1s).
Expand Down
8 changes: 4 additions & 4 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@binbin/tracevane",
"version": "0.2.4",
"version": "0.2.5",
"description": "Tracevane - Local AI Agent Control Workbench",
"private": true,
"repository": {
Expand Down
Loading