mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-10-03 22:19:42 +02:00
Merge master into PR #156 (remote tmux durability)
Resolutions: - session.ts: keep the extracted _buildRespawnPaneOptions() helper (COD-108) and add master's docker/owner fields to it - tmux-manager.ts: docker branch first, then remote via buildRemoteSessionCommand (now an options object threading claudeMode/allowedTools into buildRemoteLaunchCommand, preserving the 6.3 multi-user permission downgrade) - case-routes.ts: keep master's adminOnly helper; gate the new COD-105 discovery endpoint admin-only in multi-user mode (hosts are machine-level infra) - settings-ui.js: union of remoteAutoReconnect + master's header-button defaults - session-routes.ts: union of imports; session gets remote + owner Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
+76
-6
@@ -50,7 +50,9 @@ import {
|
||||
type EffortLevel,
|
||||
type GeminiConfig,
|
||||
type SessionRemote,
|
||||
type SessionDocker,
|
||||
} from './types.js';
|
||||
import { probeDockerCliVersion } from './docker-hosts.js';
|
||||
import type { TerminalMultiplexer, MuxSession } from './mux-interface.js';
|
||||
import { TaskTracker, type BackgroundTask } from './task-tracker.js';
|
||||
import { RalphTracker } from './ralph-tracker.js';
|
||||
@@ -178,6 +180,8 @@ export function isAltScreenStripMode(mode: SessionMode): boolean {
|
||||
const DEFAULT_PTY_COLS = 120;
|
||||
const DEFAULT_PTY_ROWS = 40;
|
||||
const TMUX_DISPLAY_TIMEOUT_MS = 2000;
|
||||
/** Delay before the in-container Claude CLI version probe (lets the container start). */
|
||||
const DOCKER_CLI_VERSION_PROBE_DELAY_MS = 3000;
|
||||
|
||||
/**
|
||||
* Ask tmux for the current window geometry of `muxName` so a re-attaching PTY
|
||||
@@ -212,8 +216,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;
|
||||
export function resolveMuxAttachCwd(workingDir: string, remote?: SessionRemote, docker?: SessionDocker): string {
|
||||
// Remote and docker sessions run the CLI elsewhere (ssh / docker exec); the LOCAL
|
||||
// wrapper pane never needs the workspace as its cwd, so launch it in /tmp.
|
||||
return remote || docker ? '/tmp' : workingDir;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -402,6 +408,14 @@ export class Session extends EventEmitter {
|
||||
// Remote execution metadata, present when this session runs over SSH through local tmux.
|
||||
private readonly _remote?: SessionRemote;
|
||||
|
||||
// Docker execution metadata, present when this session runs inside a container via
|
||||
// local tmux + `docker exec`. The container is per-CASE (shared by sibling sessions).
|
||||
private readonly _docker?: SessionDocker;
|
||||
|
||||
// Owning username in multi-user mode (undefined in single-user). Stamped at create
|
||||
// from req.authUser and round-tripped through recovery like _remote/_docker.
|
||||
private _owner?: string;
|
||||
|
||||
// Session color for visual differentiation
|
||||
private _color: import('./types.js').SessionColor = 'default';
|
||||
|
||||
@@ -475,6 +489,10 @@ export class Session extends EventEmitter {
|
||||
attachmentHistory?: SessionAttachmentHistoryItem[];
|
||||
/** Remote execution metadata for sessions launched through SSH inside local tmux. */
|
||||
remote?: SessionRemote;
|
||||
/** Docker execution metadata for sessions launched inside a container via local tmux. */
|
||||
docker?: SessionDocker;
|
||||
/** Owning username (multi-user mode); undefined in single-user. */
|
||||
owner?: string;
|
||||
}
|
||||
) {
|
||||
super();
|
||||
@@ -548,6 +566,8 @@ export class Session extends EventEmitter {
|
||||
}
|
||||
this._tmuxHistoryLimit = config.tmuxHistoryLimit ?? DEFAULT_TMUX_HISTORY_LIMIT;
|
||||
this._remote = config.remote;
|
||||
this._docker = config.docker;
|
||||
this._owner = config.owner;
|
||||
if (config.attachmentHistory && config.attachmentHistory.length > 0) {
|
||||
this.restoreAttachmentHistory(config.attachmentHistory);
|
||||
}
|
||||
@@ -649,6 +669,21 @@ export class Session extends EventEmitter {
|
||||
return this._claudeSessionId;
|
||||
}
|
||||
|
||||
/** Docker execution metadata when this session runs inside a container, else undefined. */
|
||||
get docker(): SessionDocker | undefined {
|
||||
return this._docker;
|
||||
}
|
||||
|
||||
/** Owning username in multi-user mode, else undefined. */
|
||||
get owner(): string | undefined {
|
||||
return this._owner;
|
||||
}
|
||||
|
||||
/** Set the owning username (used by recovery to restore ownership). */
|
||||
set owner(username: string | undefined) {
|
||||
this._owner = username;
|
||||
}
|
||||
|
||||
// Adopt a Claude conversation ID observed from an external source (e.g. hook
|
||||
// payload). In interactive PTY mode Claude CLI emits no JSON to stdout, so
|
||||
// `_handleJsonMessage` never sees `session_id`; hooks are the only signal
|
||||
@@ -1008,6 +1043,8 @@ export class Session extends EventEmitter {
|
||||
status: this._status,
|
||||
workingDir: this.workingDir,
|
||||
remote: this._remote,
|
||||
docker: this._docker,
|
||||
owner: this._owner,
|
||||
currentTaskId: this._currentTaskId,
|
||||
createdAt: this.createdAt,
|
||||
lastActivityAt: this._lastActivityAt,
|
||||
@@ -1197,7 +1234,7 @@ export class Session extends EventEmitter {
|
||||
name: 'xterm-256color',
|
||||
cols: ptyCols,
|
||||
rows: ptyRows,
|
||||
cwd: resolveMuxAttachCwd(this.workingDir, this._remote),
|
||||
cwd: resolveMuxAttachCwd(this.workingDir, this._remote, this._docker),
|
||||
// COD-75: codex/gemini get COLORTERM=truecolor — mirrors buildEnvExports()
|
||||
// in tmux-manager.ts so the attach client and the tmux session agree.
|
||||
env: buildMuxAttachEnv(this.mode === 'codex' || this.mode === 'gemini'),
|
||||
@@ -1270,6 +1307,8 @@ export class Session extends EventEmitter {
|
||||
effort: this._effort,
|
||||
historyLimit: this._tmuxHistoryLimit,
|
||||
remote: this._remote,
|
||||
docker: this._docker,
|
||||
owner: this._owner,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1379,7 +1418,7 @@ export class Session extends EventEmitter {
|
||||
// repaint/alt-screen mode; issue #154). Remote sessions run claude on
|
||||
// another host, so a local probe wouldn't reflect their version — skip them
|
||||
// and let the banner scrape handle those. Cached process-wide, best-effort.
|
||||
if (this.mode === 'claude' && !this._remote && !this._cliVersion) {
|
||||
if (this.mode === 'claude' && !this._remote && !this._docker && !this._cliVersion) {
|
||||
const probedVersion = getClaudeCliVersion();
|
||||
if (probedVersion) {
|
||||
this._cliVersion = probedVersion;
|
||||
@@ -1392,6 +1431,31 @@ export class Session extends EventEmitter {
|
||||
}
|
||||
}
|
||||
|
||||
// Docker sessions run claude INSIDE the container, so the local probe above
|
||||
// reports the HOST claude (wrong version, and leaving cliVersion undefined
|
||||
// silently disables wheel-forwarding, #154). Probe the IN-CONTAINER version
|
||||
// instead — deferred so the container is up after the mux attach below.
|
||||
if (this.mode === 'claude' && this._docker && !this._cliVersion) {
|
||||
const dockerMeta = this._docker;
|
||||
setTimeout(() => {
|
||||
if (this._isStopped || this._cliVersion) return;
|
||||
void probeDockerCliVersion(dockerMeta, this.mode)
|
||||
.then((version) => {
|
||||
if (!version || this._isStopped || this._cliVersion) return;
|
||||
this._cliVersion = version;
|
||||
this.emit('cliInfoUpdated', {
|
||||
version: this._cliVersion,
|
||||
model: this._cliModel,
|
||||
accountType: this._cliAccountType,
|
||||
latestVersion: this._cliLatestVersion,
|
||||
});
|
||||
})
|
||||
.catch(() => {
|
||||
/* best-effort */
|
||||
});
|
||||
}, DOCKER_CLI_VERSION_PROBE_DELAY_MS);
|
||||
}
|
||||
|
||||
// If mux wrapping is enabled, create or attach to a mux session
|
||||
if (this._useMux && this._mux) {
|
||||
try {
|
||||
@@ -1415,6 +1479,8 @@ export class Session extends EventEmitter {
|
||||
effort: this._effort,
|
||||
historyLimit: this._tmuxHistoryLimit,
|
||||
remote: this._remote,
|
||||
docker: this._docker,
|
||||
owner: this._owner,
|
||||
},
|
||||
spawnErrLabel: 'mux attachment',
|
||||
});
|
||||
@@ -1524,7 +1590,7 @@ export class Session extends EventEmitter {
|
||||
|
||||
// === Auto-accept workspace trust dialog ===
|
||||
// Claude CLI 2.x shows "Yes, I trust this folder" prompt on first launch per directory.
|
||||
// Codeman sessions always use --dangerously-skip-permissions, so auto-accept.
|
||||
// Codeman sessions run permission-skipping or classifier-guarded (auto) modes, so auto-accept.
|
||||
if (!this._trustDialogAccepted && data.includes('trust this folder')) {
|
||||
this._trustDialogAccepted = true;
|
||||
console.log(`[Session] Auto-accepting workspace trust dialog for: ${this.id}`);
|
||||
@@ -1785,6 +1851,8 @@ export class Session extends EventEmitter {
|
||||
envOverrides: this._envOverrides,
|
||||
historyLimit: this._tmuxHistoryLimit,
|
||||
remote: this._remote,
|
||||
docker: this._docker,
|
||||
owner: this._owner,
|
||||
},
|
||||
createSessionOptions: {
|
||||
sessionId: this.id,
|
||||
@@ -1795,6 +1863,8 @@ export class Session extends EventEmitter {
|
||||
envOverrides: this._envOverrides,
|
||||
historyLimit: this._tmuxHistoryLimit,
|
||||
remote: this._remote,
|
||||
docker: this._docker,
|
||||
owner: this._owner,
|
||||
},
|
||||
spawnErrLabel: 'shell mux attachment',
|
||||
});
|
||||
@@ -1922,7 +1992,7 @@ export class Session extends EventEmitter {
|
||||
model ? `(model: ${model})` : ''
|
||||
);
|
||||
|
||||
const args = buildPromptArgs(prompt, model);
|
||||
const args = buildPromptArgs(prompt, model, this._claudeMode, this._allowedTools);
|
||||
|
||||
try {
|
||||
this.ptyProcess = pty.spawn('claude', args, {
|
||||
|
||||
Reference in New Issue
Block a user