diff --git a/src/session.ts b/src/session.ts index daeb1667..839053b1 100644 --- a/src/session.ts +++ b/src/session.ts @@ -49,6 +49,7 @@ import { type CodexConfig, type EffortLevel, type GeminiConfig, + type SessionRemote, } from './types.js'; import type { TerminalMultiplexer, MuxSession } from './mux-interface.js'; import { TaskTracker, type BackgroundTask } from './task-tracker.js'; @@ -209,6 +210,10 @@ export function queryTmuxWindowSize(muxName: string, socket: string): { cols: nu return { cols: DEFAULT_PTY_COLS, rows: DEFAULT_PTY_ROWS }; } +export function resolveMuxAttachCwd(workingDir: string, remote?: SessionRemote): string { + return remote ? '/tmp' : workingDir; +} + /** * Represents a JSON message from Claude CLI's stream-json output format. * Messages are newline-delimited JSON objects parsed from PTY output. @@ -385,6 +390,9 @@ export class Session extends EventEmitter { // tmux history-limit (scrollback lines) applied to this session's pane. private readonly _tmuxHistoryLimit: number; + // Remote execution metadata, present when this session runs over SSH through local tmux. + private readonly _remote?: SessionRemote; + // Session color for visual differentiation private _color: import('./types.js').SessionColor = 'default'; @@ -456,6 +464,8 @@ export class Session extends EventEmitter { tmuxHistoryLimit?: number; /** Restored per-session attachment history. May include server-private external paths. */ attachmentHistory?: SessionAttachmentHistoryItem[]; + /** Remote execution metadata for sessions launched through SSH inside local tmux. */ + remote?: SessionRemote; } ) { super(); @@ -528,6 +538,7 @@ export class Session extends EventEmitter { this._effort = config.effort; } this._tmuxHistoryLimit = config.tmuxHistoryLimit ?? DEFAULT_TMUX_HISTORY_LIMIT; + this._remote = config.remote; if (config.attachmentHistory && config.attachmentHistory.length > 0) { this.restoreAttachmentHistory(config.attachmentHistory); } @@ -987,6 +998,7 @@ export class Session extends EventEmitter { pid: this.pid, status: this._status, workingDir: this.workingDir, + remote: this._remote, currentTaskId: this._currentTaskId, createdAt: this.createdAt, lastActivityAt: this._lastActivityAt, @@ -1171,7 +1183,7 @@ export class Session extends EventEmitter { name: 'xterm-256color', cols: ptyCols, rows: ptyRows, - cwd: this.workingDir, + cwd: resolveMuxAttachCwd(this.workingDir, this._remote), env: buildMuxAttachEnv(), }); } catch (spawnErr) { @@ -1282,6 +1294,7 @@ export class Session extends EventEmitter { envOverrides: this._envOverrides, effort: this._effort, historyLimit: this._tmuxHistoryLimit, + remote: this._remote, }, createSessionOptions: { sessionId: this.id, @@ -1299,6 +1312,7 @@ export class Session extends EventEmitter { envOverrides: this._envOverrides, effort: this._effort, historyLimit: this._tmuxHistoryLimit, + remote: this._remote, }, spawnErrLabel: 'mux attachment', }); @@ -1637,6 +1651,7 @@ export class Session extends EventEmitter { niceConfig: this._niceConfig, envOverrides: this._envOverrides, historyLimit: this._tmuxHistoryLimit, + remote: this._remote, }, createSessionOptions: { sessionId: this.id, @@ -1646,6 +1661,7 @@ export class Session extends EventEmitter { niceConfig: this._niceConfig, envOverrides: this._envOverrides, historyLimit: this._tmuxHistoryLimit, + remote: this._remote, }, spawnErrLabel: 'shell mux attachment', }); diff --git a/src/tmux-manager.ts b/src/tmux-manager.ts index 0af5caa8..8f5c52ed 100644 --- a/src/tmux-manager.ts +++ b/src/tmux-manager.ts @@ -42,8 +42,10 @@ import { type CodexConfig, type EffortLevel, type GeminiConfig, + type SessionRemote, } from './types.js'; import { buildEffortCliArgs } from './session-cli-builder.js'; +import { defaultRemoteCommandForMode, remoteSshTarget } from './remote-hosts.js'; import { wrapWithNice, SAFE_PATH_PATTERN, @@ -669,6 +671,16 @@ function buildSpawnCommand(options: { return '$SHELL'; } +export function buildRemoteLaunchCommand(options: { mode: SessionMode; remote: SessionRemote }): string { + const { mode, remote } = options; + const modeCommand = remote.commands?.[mode] || defaultRemoteCommandForMode(mode); + const args = ['ssh', '-o', 'BatchMode=yes', '-t']; + if (remote.port) args.push('-p', String(remote.port)); + const remoteCommand = `cd ${shellescape(remote.remotePath)} && ${modeCommand}`; + args.push(remoteSshTarget(remote), `bash -lc ${shellescape(remoteCommand)}`); + return args.map((arg) => shellescape(arg)).join(' '); +} + /** * Set sensitive environment variables on a tmux session via setenv. * These are inherited by panes but not visible in ps output or tmux history. diff --git a/src/types/api.ts b/src/types/api.ts index 1e03a3d6..b7201022 100644 --- a/src/types/api.ts +++ b/src/types/api.ts @@ -130,7 +130,6 @@ export interface CaseInfo { /** Remote case metadata for display and session creation */ remote?: { hostId: string; - hostLabel: string; host: string; username: string; path: string; diff --git a/src/web/public/index.html b/src/web/public/index.html index 3f6edd2d..bf699c72 100644 --- a/src/web/public/index.html +++ b/src/web/public/index.html @@ -1649,6 +1649,36 @@ Absolute path to an existing project folder, e.g. /home/you/my-project + +