mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-27 04:47:03 -06: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:
@@ -3,7 +3,10 @@ import type { NodePluginToolDescriptor } from "../../packages/gateway-protocol/s
|
||||
import type { OpenClawConfig } from "../config/types.openclaw.js";
|
||||
import type { PluginNodeHostCommandRegistration } from "../plugins/registry-types.js";
|
||||
import { getActivePluginRegistry } from "../plugins/runtime.js";
|
||||
import type { OpenClawPluginNodeHostCommandAvailabilityContext } from "../plugins/types.js";
|
||||
import type {
|
||||
OpenClawPluginNodeHostCommandAvailabilityContext,
|
||||
OpenClawPluginNodeHostCommandIo,
|
||||
} from "../plugins/types.js";
|
||||
import { createLazyRuntimeModule } from "../shared/lazy-runtime.js";
|
||||
|
||||
/**
|
||||
@@ -33,6 +36,7 @@ export async function ensureNodeHostPluginRegistry(params: {
|
||||
/** List registered node-host capabilities and command ids in deterministic order. */
|
||||
export function listRegisteredNodeHostCapsAndCommands(
|
||||
context: OpenClawPluginNodeHostCommandAvailabilityContext,
|
||||
options: { includeDuplex?: boolean } = {},
|
||||
): {
|
||||
caps: string[];
|
||||
commands: string[];
|
||||
@@ -43,6 +47,9 @@ export function listRegisteredNodeHostCapsAndCommands(
|
||||
const commands = new Set<string>();
|
||||
const nodePluginTools = new Map<string, NodePluginToolDescriptor>();
|
||||
for (const entry of registry?.nodeHostCommands ?? []) {
|
||||
if (entry.command.duplex === true && options.includeDuplex === false) {
|
||||
continue;
|
||||
}
|
||||
// Availability belongs to the node-local plugin. Gateway policy still keeps
|
||||
// the command registered so a differently configured remote node can expose it.
|
||||
if (entry.command.isAvailable?.(context) === false) {
|
||||
@@ -113,6 +120,7 @@ function buildNodePluginToolDescriptor(
|
||||
export async function invokeRegisteredNodeHostCommand(
|
||||
command: string,
|
||||
paramsJSON?: string | null,
|
||||
io?: OpenClawPluginNodeHostCommandIo,
|
||||
): Promise<string | null> {
|
||||
const registry = getActivePluginRegistry();
|
||||
const match = (registry?.nodeHostCommands ?? []).find(
|
||||
@@ -121,5 +129,19 @@ export async function invokeRegisteredNodeHostCommand(
|
||||
if (!match) {
|
||||
return null;
|
||||
}
|
||||
if (match.command.duplex === true) {
|
||||
if (!io) {
|
||||
throw new Error(`node command requires duplex transport: ${command}`);
|
||||
}
|
||||
return await match.command.handle(paramsJSON, io);
|
||||
}
|
||||
return await match.command.handle(paramsJSON);
|
||||
}
|
||||
|
||||
export function isRegisteredNodeHostCommandDuplex(command: string): boolean {
|
||||
const registry = getActivePluginRegistry();
|
||||
return (
|
||||
(registry?.nodeHostCommands ?? []).find((entry) => entry.command.command === command)?.command
|
||||
.duplex === true
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user