mirror of
https://github.com/openclaw/openclaw.git
synced 2026-07-28 05:01:17 +00:00
feat(terminal): open Codex/Claude catalog sessions in a terminal on their owning host
Catalog session rows (sidebar context menu + click), the built-in viewer header, and a new "Open Codex/Claude sessions in" preference can launch the native CLI (codex resume / claude --resume) in the operator terminal on the machine that owns the session. - Gateway-local sessions spawn through the existing terminal launch policy (sandbox/enabled gates preserved) with the resume command in the session cwd. - Paired-node sessions run through a new seq-ordered node PTY relay: a duplex node-host command streams PTY output via node.invoke.progress and receives keystrokes/resize via a new node.invoke.input event, behind the unchanged terminal.* client protocol (TerminalSessionManager gains a backend abstraction; node relay reuses the streaming-invoke controller). - Owner boundary: each plugin owns its resume command and builds argv from a validated thread id; the gateway routes node opens through the node command allowlist and plugin invoke policy (no advertisement-only trust), and nodes re-verify session eligibility before spawning. - UI setting catalogOpenTarget + canOpenTerminal capability gate every entry point; capability requires the owning host to actually have the CLI. Node PATH is normalized before command-availability probes, Windows .cmd/.bat shims spawn via ComSpec, and catalog terminal opens reattach persisted tabs before opening the new tab.
This commit is contained in:
@@ -184,4 +184,39 @@ describe("plugin node-host registry", () => {
|
||||
await expect(invokeRegisteredNodeHostCommand("missing.command", null)).resolves.toBeNull();
|
||||
expect(handle).toHaveBeenCalledWith('{"ok":true}');
|
||||
});
|
||||
|
||||
it("gates duplex commands from embedded-worker manifests and supplies their IO context", async () => {
|
||||
const handle = vi.fn(async (paramsJSON?: string | null) => paramsJSON ?? "");
|
||||
const registry = createEmptyPluginRegistry();
|
||||
registry.nodeHostCommands = [
|
||||
{
|
||||
pluginId: "terminal",
|
||||
pluginName: "Terminal",
|
||||
command: {
|
||||
command: "terminal.resume.v1",
|
||||
cap: "terminal",
|
||||
duplex: true,
|
||||
handle,
|
||||
},
|
||||
source: "test",
|
||||
},
|
||||
];
|
||||
setActivePluginRegistry(registry);
|
||||
|
||||
expect(
|
||||
listRegisteredNodeHostCapsAndCommands(availabilityContext, { includeDuplex: false }),
|
||||
).toEqual({ caps: [], commands: [], nodePluginTools: [] });
|
||||
const io = {
|
||||
signal: new AbortController().signal,
|
||||
emitChunk: async () => {},
|
||||
onInput: () => {},
|
||||
};
|
||||
await expect(
|
||||
invokeRegisteredNodeHostCommand("terminal.resume.v1", '{"threadId":"id"}', io),
|
||||
).resolves.toBe('{"threadId":"id"}');
|
||||
expect(handle).toHaveBeenCalledWith('{"threadId":"id"}', io);
|
||||
await expect(invokeRegisteredNodeHostCommand("terminal.resume.v1", null)).rejects.toThrow(
|
||||
"requires duplex transport",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user