mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-11-14 10:39:32 +01:00
fix(adopt): an adopted session has no workspace of ours
The wrapper's workingDir is the neutral /tmp, and everything master grew since 1.33.2 that branches on session.remote or session.docker read that as a local session working in /tmp, because an adopted session keeps its connection on adopt.remote and adopt.docker. So uploads wrote into /tmp and closing the tab recursively deleted the shared folder; the Git panel, its fetch and the fetch scheduler ran git on whatever sat there; the image watcher, Read My Mind and the Discord bridge used that folder; last-response paired a transcript by it and could show another conversation's answer as this one's; a magic link in a container's output registered a file from this host's /tmp; and guarded input could type into a human's composer. Each now refuses or answers unsupported, the browser hides the rows that cannot work, and Session Options shows where the foreign pane was instead of /tmp. The custom-model route refuses as well, since it restarts the CLI in the pane. The local version probe is skipped: the local binary says nothing about a process someone else started, possibly on another host. Also moves the guard test to port 0, which test-ports-guard now requires. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
f464a22bc3
commit
7de4201955
22 files changed
+269
-42
No files matched your search
@@ -79,6 +79,8 @@ export interface GitFetchSessionView {
|
||||
/** `unknown`, not `boolean`: a session's remote/Docker markers are objects, and the only question here is whether either is set at all. */
|
||||
remote?: unknown;
|
||||
docker?: unknown;
|
||||
/** Set on a wrapper around a foreign tmux session: its `workingDir` is a neutral folder, never a repository. */
|
||||
adopt?: unknown;
|
||||
gitFetchEnabled?: boolean;
|
||||
gitFetchIntervalSeconds?: number;
|
||||
}
|
||||
@@ -176,7 +178,7 @@ export class GitFetchScheduler {
|
||||
const groups = new Map<string, GroupMember[]>();
|
||||
for (const id of this.ports.sessionIds()) {
|
||||
const view = this.ports.getSession(id);
|
||||
if (!view || view.remote || view.docker) continue;
|
||||
if (!view || view.remote || view.docker || view.adopt) continue;
|
||||
const effective = effectiveGitFetch(view, settings);
|
||||
if (!effective.enabled) continue;
|
||||
const folder = resolve(view.workingDir);
|
||||
|
||||
+1
-1
@@ -346,7 +346,7 @@ export interface GitFetchEntry {
|
||||
export interface GitFetchReport {
|
||||
/** `ok` when at least one repository was fetched (or attempted); the other states are as in the overview. */
|
||||
state: 'ok' | 'not-a-repo' | 'unsupported' | 'error';
|
||||
reason?: 'remote' | 'docker';
|
||||
reason?: 'remote' | 'docker' | 'adopted';
|
||||
error?: string;
|
||||
/** One entry per repository fetched, in the same order the panel lists them. */
|
||||
repos: GitFetchEntry[];
|
||||
|
||||
@@ -95,7 +95,7 @@ export interface GitWorkspaceStatus {
|
||||
* `unsupported`: a remote or Docker session (never inspected). `error`: git failed; see `error`.
|
||||
*/
|
||||
state: 'ok' | 'not-a-repo' | 'unsupported' | 'error';
|
||||
reason?: 'remote' | 'docker';
|
||||
reason?: 'remote' | 'docker' | 'adopted';
|
||||
error?: string;
|
||||
repoRoot?: string;
|
||||
/** Null when HEAD is detached. */
|
||||
@@ -482,7 +482,7 @@ export interface GitRepoEntry {
|
||||
export interface GitWorkspaceOverview {
|
||||
/** `ok` when at least one repository was found; the other states are as in `GitWorkspaceStatus`. */
|
||||
state: 'ok' | 'not-a-repo' | 'unsupported' | 'error';
|
||||
reason?: 'remote' | 'docker';
|
||||
reason?: 'remote' | 'docker' | 'adopted';
|
||||
error?: string;
|
||||
repos: GitRepoEntry[];
|
||||
/** More than `repoLimit` repositories were found; only the first are reported. */
|
||||
|
||||
+5
-2
@@ -3181,8 +3181,10 @@ export class Session extends EventEmitter {
|
||||
// wheel-forwarding to Claude's own transcript (the only route to history in
|
||||
// repaint/alt-screen mode; issue #154). Remote sessions run claude on
|
||||
// another host, so a local probe wouldn't reflect their version; they get
|
||||
// their own over-ssh probe below. Cached process-wide, best-effort.
|
||||
if (cliNeedsVersionProbe(this.mode) && !this._remote && !this._docker && !this._cliVersion) {
|
||||
// their own over-ssh probe below. Cached process-wide, best-effort. An adopted
|
||||
// session's agent is a process someone else started, possibly elsewhere, so
|
||||
// the local binary says nothing about it either.
|
||||
if (cliNeedsVersionProbe(this.mode) && !this._remote && !this._docker && !this._adopt && !this._cliVersion) {
|
||||
const probedVersion = resolveSessionCliVersion(this.mode);
|
||||
if (probedVersion) {
|
||||
this._cliVersion = probedVersion;
|
||||
@@ -5133,6 +5135,7 @@ export class Session extends EventEmitter {
|
||||
(!allowLease && this.guardedInputHeld) ||
|
||||
this.remote ||
|
||||
this.docker ||
|
||||
this._adopt ||
|
||||
this.paneExit ||
|
||||
!this._muxSession
|
||||
)
|
||||
|
||||
@@ -75,6 +75,8 @@ export interface EditHistorySession {
|
||||
mode: string;
|
||||
remote: SessionRemote | undefined;
|
||||
docker?: unknown;
|
||||
/** Set on a wrapper around a foreign tmux session: Codeman holds no transcript id for what runs there. */
|
||||
adopt?: unknown;
|
||||
claudeSessionId: string | null;
|
||||
/** The session's own `CLAUDE_CONFIG_DIR` (Codeman env override), if it has one. */
|
||||
claudeConfigDir?: string;
|
||||
@@ -106,6 +108,7 @@ export interface SyncOptions {
|
||||
export function unsupportedReason(session: EditHistorySession): string | undefined {
|
||||
if (session.remote) return 'remote';
|
||||
if (session.docker) return 'docker';
|
||||
if (session.adopt) return 'adopted';
|
||||
if (getCli(session.mode)?.capabilities.transcript !== 'claude-jsonl') return 'mode';
|
||||
return undefined;
|
||||
}
|
||||
|
||||
@@ -57,7 +57,9 @@ export const DISCORD_RECEIVED_DIR = '.codeman-discord';
|
||||
* and is listed, or the uploads of a workspace like `~/.codeman/app` would
|
||||
* never be collected), and nothing for a remote (SSH) session, whose
|
||||
* workingDir is the remote path and would name a same-named LOCAL directory
|
||||
* here. The working directory is a user-chosen path, so it is probed BOUNDED
|
||||
* here, or for an adopted session, whose workingDir is the neutral `/tmp` that
|
||||
* other sessions may really work in (it receives no uploads). The working
|
||||
* directory is a user-chosen path, so it is probed BOUNDED
|
||||
* first (#516): one on a mount that stopped answering reads `unknown` and is
|
||||
* skipped, never touched. The sweep keeps the probe's stall cap; the delete,
|
||||
* acting on one path at the user's request, passes `pastCap`. The check is
|
||||
@@ -66,10 +68,10 @@ export const DISCORD_RECEIVED_DIR = '.codeman-discord';
|
||||
* can.
|
||||
*/
|
||||
export async function uploadDirs(
|
||||
session: { workingDir: string; remote?: unknown },
|
||||
session: { workingDir: string; remote?: unknown; adopt?: unknown },
|
||||
probe: PathProbeOptions = {}
|
||||
): Promise<string[]> {
|
||||
if (session.remote) return [];
|
||||
if (session.remote || session.adopt) return [];
|
||||
if ((await probePathKind(session.workingDir, probe)) !== 'directory') return [];
|
||||
const dataDir = await realDir(getDataDir());
|
||||
const dirs: string[] = [];
|
||||
|
||||
@@ -425,9 +425,11 @@ Object.assign(CodemanApp.prototype, {
|
||||
overview.state === 'not-a-repo'
|
||||
? 'No git repository here: this session’s folder is not one, and none was found inside it (up to two levels down).'
|
||||
: overview.state === 'unsupported'
|
||||
? overview.reason === 'docker'
|
||||
? 'Git status is not available for Docker sessions, or for folders inside a Docker case workspace.'
|
||||
: 'Git status is not available for remote (SSH) sessions.'
|
||||
? overview.reason === 'adopted'
|
||||
? 'Git status is not available for a tmux session opened outside Codeman.'
|
||||
: overview.reason === 'docker'
|
||||
? 'Git status is not available for Docker sessions, or for folders inside a Docker case workspace.'
|
||||
: 'Git status is not available for remote (SSH) sessions.'
|
||||
: `Could not read the repository: ${overview.error || 'git failed'}`;
|
||||
body.append(el('div', 'git-status-empty', why));
|
||||
clearChrome();
|
||||
|
||||
@@ -2757,8 +2757,9 @@ Object.assign(CodemanApp.prototype, {
|
||||
/** Copy the open session's working folder (Session Options > Session > Working folder). */
|
||||
async copySessionPath() {
|
||||
const session = this.sessions.get(this.editingSessionId);
|
||||
if (!session?.workingDir) return;
|
||||
const ok = await this._copyText(session.workingDir);
|
||||
const path = session?.adopt?.paneCurrentPath || session?.workingDir;
|
||||
if (!path) return;
|
||||
const ok = await this._copyText(path);
|
||||
this.showToast(ok ? 'Path copied' : 'Copy failed', ok ? 'success' : 'error');
|
||||
},
|
||||
|
||||
@@ -2833,8 +2834,9 @@ Object.assign(CodemanApp.prototype, {
|
||||
this.updateGitFetchStatus(sessionId);
|
||||
const imageWatcherToggle = document.getElementById('modalImageWatcherEnabled');
|
||||
imageWatcherToggle.checked = session.imageWatcherEnabled ?? true;
|
||||
// The server never watches a remote session's directory (it is on another host), so the row is hidden.
|
||||
imageWatcherToggle.closest('.set-row').style.display = session.remote ? 'none' : '';
|
||||
// The server never watches a remote session's directory (it is on another host), nor an adopted
|
||||
// session's (a neutral wrapper folder), so the row is hidden.
|
||||
imageWatcherToggle.closest('.set-row').style.display = session.remote || session.adopt ? 'none' : '';
|
||||
document.getElementById('modalFlickerFilterEnabled').checked = session.flickerFilterEnabled ?? false;
|
||||
|
||||
// Populate session name input with prefix/suffix split
|
||||
@@ -2853,17 +2855,21 @@ Object.assign(CodemanApp.prototype, {
|
||||
}
|
||||
|
||||
// Working folder: the path the CLI was launched in on the Codeman host
|
||||
// (an adopted session's is the neutral wrapper folder, so the row shows where the pane was)
|
||||
const _pathEl = document.getElementById('modalSessionPath');
|
||||
_pathEl.textContent = session.workingDir || '';
|
||||
_pathEl.title = session.workingDir || '';
|
||||
document.getElementById('modalSessionPathCopy').disabled = !session.workingDir;
|
||||
const _shownPath = session.adopt?.paneCurrentPath || session.workingDir || '';
|
||||
_pathEl.textContent = _shownPath;
|
||||
_pathEl.title = _shownPath;
|
||||
document.getElementById('modalSessionPathCopy').disabled = !_shownPath;
|
||||
// A remote session's workingDir is the REMOTE path; a docker session's is the
|
||||
// host bind-mount source the container sees.
|
||||
document.getElementById('modalSessionPathDesc').textContent = session.remote
|
||||
? "Where this session's CLI runs, on the remote host it reaches over SSH, not on this machine."
|
||||
: session.docker
|
||||
? "On the machine hosting Codeman, mounted into the Docker container this session's CLI runs in."
|
||||
: "Where this session's CLI runs, on the machine hosting Codeman.";
|
||||
document.getElementById('modalSessionPathDesc').textContent = session.adopt
|
||||
? 'Where the tmux session was when it was opened here. It was started outside Codeman and keeps running if this tab closes.'
|
||||
: session.remote
|
||||
? "Where this session's CLI runs, on the remote host it reaches over SSH, not on this machine."
|
||||
: session.docker
|
||||
? "On the machine hosting Codeman, mounted into the Docker container this session's CLI runs in."
|
||||
: "Where this session's CLI runs, on the machine hosting Codeman.";
|
||||
|
||||
// Initialize color picker with current session color
|
||||
const currentColor = session.color || 'default';
|
||||
@@ -2890,11 +2896,11 @@ Object.assign(CodemanApp.prototype, {
|
||||
if (respawnTabBtn) respawnTabBtn.style.display = '';
|
||||
}
|
||||
|
||||
// Git is local git only: a remote (SSH) or Docker case answers 'unsupported' on both git routes and is
|
||||
// never fetched by the interval scheduler, so its Git tab has nothing to say and its rail entry goes
|
||||
// hidden the same way.
|
||||
// Git is local git only: a remote (SSH) or Docker case, or an adopted session, answers 'unsupported' on
|
||||
// both git routes and is never fetched by the interval scheduler, so its Git tab has nothing to say and
|
||||
// its rail entry goes hidden the same way.
|
||||
const gitTabBtn = document.querySelector('#sessionOptionsModal .set-rail-item[data-tab="git"]');
|
||||
if (gitTabBtn) gitTabBtn.style.display = session.remote || session.docker ? 'none' : '';
|
||||
if (gitTabBtn) gitTabBtn.style.display = session.remote || session.docker || session.adopt ? 'none' : '';
|
||||
|
||||
// Populate Ralph Wiggum form with current session values (skip for external CLI sessions)
|
||||
if (!isExternalCli) {
|
||||
@@ -6111,7 +6117,7 @@ Object.assign(CodemanApp.prototype, {
|
||||
|
||||
/** The Codeman-managed worktree a session runs in, or null (see managedWorktreeOfDir). */
|
||||
_managedWorktreeOfSession(session) {
|
||||
if (!session || session.remote || session.docker) return null;
|
||||
if (!session || session.remote || session.docker || session.adopt) return null;
|
||||
return managedWorktreeOfDir(session.workingDir, this.cases);
|
||||
},
|
||||
|
||||
|
||||
@@ -6711,7 +6711,7 @@ Object.assign(CodemanApp.prototype, {
|
||||
_keyboardHoldEligible(sessionId = this.activeSessionId) {
|
||||
if (this.loadAppSettingsFromStorage?.()?.keyboardHoldsRows === false) return false;
|
||||
const session = sessionId ? this.sessions?.get(sessionId) : null;
|
||||
if (!session || session.remote || session.docker || session.cliMouseTracking) return false;
|
||||
if (!session || session.remote || session.docker || session.adopt || session.cliMouseTracking) return false;
|
||||
const modes = window.__codemanKeyboardHoldModes;
|
||||
return Array.isArray(modes) && modes.includes(session.mode || 'claude');
|
||||
},
|
||||
|
||||
@@ -197,7 +197,7 @@ export function registerDiscordRoutes(app: FastifyInstance, deps: DiscordRouteDe
|
||||
}
|
||||
let upload: { name: string; data: Uint8Array } | undefined;
|
||||
if (file !== undefined) {
|
||||
const problem = await fileProblem(session.workingDir, !!session.remote, file);
|
||||
const problem = await fileProblem(session.workingDir, !!session.remote || session.isAdopted, file);
|
||||
if (typeof problem === 'string') {
|
||||
refundNotifyToken(session.id);
|
||||
reply.code(400);
|
||||
|
||||
@@ -99,6 +99,7 @@ export function registerEditHistoryRoutes(app: FastifyInstance, ctx: SessionPort
|
||||
mode: session.mode,
|
||||
remote: session.remote,
|
||||
docker: session.docker,
|
||||
adopt: session.adopt,
|
||||
claudeSessionId: session.claudeSessionId,
|
||||
claudeConfigDir: session.claudeConfigDir(),
|
||||
};
|
||||
|
||||
@@ -81,6 +81,8 @@ export function registerGitStatusRoutes(
|
||||
const session = findSessionOrFail(ctx, id, req);
|
||||
if (session.remote) return { success: true, data: emptyOverview('unsupported', { reason: 'remote' }) };
|
||||
if (session.docker) return { success: true, data: emptyOverview('unsupported', { reason: 'docker' }) };
|
||||
// An adopted wrapper's folder is the neutral `/tmp`, so a scan would run git on whatever sits there.
|
||||
if (session.isAdopted) return { success: true, data: emptyOverview('unsupported', { reason: 'adopted' }) };
|
||||
return {
|
||||
success: true,
|
||||
data: await getGitWorkspaceOverview(session.workingDir, {
|
||||
@@ -103,9 +105,12 @@ export function registerGitStatusRoutes(
|
||||
const { repo, path, kind } = query;
|
||||
const limits = limitsFrom(query);
|
||||
const session = findSessionOrFail(ctx, id, req);
|
||||
if (session.remote || session.docker) {
|
||||
if (session.remote || session.docker || session.isAdopted) {
|
||||
reply.code(400);
|
||||
return createErrorResponse(ApiErrorCode.INVALID_INPUT, 'Git is not available for remote or Docker sessions');
|
||||
return createErrorResponse(
|
||||
ApiErrorCode.INVALID_INPUT,
|
||||
'Git is not available for remote, Docker or adopted sessions'
|
||||
);
|
||||
}
|
||||
const repoRoot = repo
|
||||
? await findWorkspaceRepo(session.workingDir, repo, {
|
||||
@@ -159,6 +164,7 @@ export function registerGitStatusRoutes(
|
||||
const session = findSessionOrFail(ctx, id, req);
|
||||
if (session.remote) return { success: true, data: emptyReport('unsupported', { reason: 'remote' }) };
|
||||
if (session.docker) return { success: true, data: emptyReport('unsupported', { reason: 'docker' }) };
|
||||
if (session.isAdopted) return { success: true, data: emptyReport('unsupported', { reason: 'adopted' }) };
|
||||
// Fail CLOSED on a Docker case list that cannot be read: a fetch runs the repository's hooks, so it never
|
||||
// runs without knowing which folders a container can write to. Answered as a failed run (`state: 'error'`),
|
||||
// published like one, so the Session Options Git row shows it.
|
||||
|
||||
@@ -91,8 +91,10 @@ export function registerReadMyMindRoutes(app: FastifyInstance, ctx: SessionPort
|
||||
const transcriptPath = ctx.getTranscriptPath(id);
|
||||
const transcript = transcriptPath ? await readTranscriptSignals(transcriptPath) : null;
|
||||
// Remote-SSH cases skip git: workingDir is not local. Docker cases are
|
||||
// fine (the workspace is bind-mounted at the same host path).
|
||||
const workspace = session.remote ? null : await collectWorkspaceSignals(session.workingDir);
|
||||
// fine (the workspace is bind-mounted at the same host path). An adopted
|
||||
// session's workingDir is a neutral wrapper folder, so it skips git too.
|
||||
const workspace =
|
||||
session.remote || session.isAdopted ? null : await collectWorkspaceSignals(session.workingDir);
|
||||
|
||||
const lastPromptTs = profile.recentPrompts[profile.recentPrompts.length - 1]?.ts;
|
||||
const tracker = ctx.runSummaryTrackers.get(id);
|
||||
|
||||
@@ -1742,6 +1742,13 @@ export function registerSessionRoutes(
|
||||
'Custom model endpoints are not supported for remote (SSH) or Docker sessions yet'
|
||||
);
|
||||
}
|
||||
// Its own check: this restarts the CLI in the pane, and an adopted pane's CLI is not ours to restart.
|
||||
if (session.isAdopted) {
|
||||
return createErrorResponse(
|
||||
ApiErrorCode.INVALID_INPUT,
|
||||
'Custom model endpoints are not available for adopted sessions: Codeman did not start this agent'
|
||||
);
|
||||
}
|
||||
if (session.isBusy()) {
|
||||
return createErrorResponse(ApiErrorCode.SESSION_BUSY, 'Session is busy');
|
||||
}
|
||||
@@ -3042,7 +3049,13 @@ export function registerSessionRoutes(
|
||||
* effort: the tab rename has already happened and must not fail on this.
|
||||
*/
|
||||
async function syncClaudeTitle(session: Session): Promise<void> {
|
||||
if (getCli(session.mode)?.capabilities.transcript !== 'claude-jsonl' || session.remote || session.docker) return;
|
||||
if (
|
||||
getCli(session.mode)?.capabilities.transcript !== 'claude-jsonl' ||
|
||||
session.remote ||
|
||||
session.docker ||
|
||||
session.isAdopted
|
||||
)
|
||||
return;
|
||||
try {
|
||||
const projectsDir = join(process.env.HOME || '/tmp', '.claude', 'projects');
|
||||
const hookPath = ctx.getTranscriptPath(session.id);
|
||||
@@ -3120,6 +3133,17 @@ export function registerSessionRoutes(
|
||||
// The body of GET /api/sessions/:id/last-response, so the Discord bridge reads the same transcript
|
||||
// the response viewer does (per-CLI reader, pane fallback) without a second copy of this logic.
|
||||
const lastResponseFor = async (session: Session, context?: string) => {
|
||||
// ⚠️ An adopted session has no transcript Codeman can name. Every reader below pairs one by
|
||||
// the id Codeman launched the CLI with or by the session's folder, and neither means anything
|
||||
// here: the CLI chose its own id, and the folder is the neutral `/tmp`, so the newest file for
|
||||
// that folder would be another conversation's answer shown as this one's. Nothing said yet is
|
||||
// the honest answer.
|
||||
if (session.isAdopted) {
|
||||
return context === 'full' || context === 'turn'
|
||||
? { text: '', timestamp: '', messages: [] }
|
||||
: { text: '', timestamp: '' };
|
||||
}
|
||||
|
||||
// Codex sessions don't write to ~/.claude/projects — their transcripts
|
||||
// live in ~/.codex/sessions/**. Branch to a Codex-specific reader so the
|
||||
// response-viewer works for Codex panes too.
|
||||
@@ -3953,8 +3977,11 @@ export function registerSessionRoutes(
|
||||
const { id } = req.params as { id: string };
|
||||
const body = parseBody(GitFetchSettingsSchema, req.body, 'Invalid request body');
|
||||
const session = findSessionOrFail(ctx, id, req);
|
||||
if (session.remote || session.docker) {
|
||||
return createErrorResponse(ApiErrorCode.INVALID_INPUT, 'Git fetch does not run for remote or Docker sessions');
|
||||
if (session.remote || session.docker || session.isAdopted) {
|
||||
return createErrorResponse(
|
||||
ApiErrorCode.INVALID_INPUT,
|
||||
'Git fetch does not run for remote, Docker or adopted sessions'
|
||||
);
|
||||
}
|
||||
|
||||
const enabled = 'enabled' in body ? (body.enabled ?? undefined) : session.gitFetchEnabled;
|
||||
@@ -4060,6 +4087,10 @@ export function registerSessionRoutes(
|
||||
if (body.enabled && session.remote) {
|
||||
return createErrorResponse(ApiErrorCode.INVALID_INPUT, 'Image watching does not run for remote sessions');
|
||||
}
|
||||
// An adopted session's folder is the neutral wrapper folder, not the foreign pane's work.
|
||||
if (body.enabled && session.isAdopted) {
|
||||
return createErrorResponse(ApiErrorCode.INVALID_INPUT, 'Image watching does not run for adopted sessions');
|
||||
}
|
||||
|
||||
if (body.enabled) {
|
||||
imageWatcher.watchSession(session.id, session.workingDir);
|
||||
@@ -6025,6 +6056,13 @@ export function registerSessionRoutes(
|
||||
'Prompt uploads are not supported for remote (SSH) sessions'
|
||||
);
|
||||
}
|
||||
// An adopted session has no workspace of ours: its folder is the neutral `/tmp`, shared with
|
||||
// whatever else runs there, and an agent in a container or on an ssh host could not read it.
|
||||
// The foreign pane's own folder is not ours to write into either.
|
||||
if (session.isAdopted) {
|
||||
reply.code(400);
|
||||
return createErrorResponse(ApiErrorCode.INVALID_INPUT, 'Prompt uploads are not supported for adopted sessions');
|
||||
}
|
||||
|
||||
// The upload is written under session.workingDir (ensureUploadDir below), and
|
||||
// a live session's workspace can sit on a network mount that stopped answering.
|
||||
|
||||
@@ -1185,9 +1185,10 @@ export function registerSystemRoutes(
|
||||
// Handle image watcher toggle dynamically
|
||||
toggleService((merged.imageWatcherEnabled as boolean) ?? false, imageWatcher, 'Image watcher', () => {
|
||||
// Re-watch all active sessions that have image watcher enabled (never a remote session: its
|
||||
// workingDir is a path on the remote host, not on this disk)
|
||||
// workingDir is a path on the remote host, not on this disk; nor an adopted one, whose folder
|
||||
// is the neutral wrapper folder and not the foreign pane's work)
|
||||
for (const session of ctx.sessions.values()) {
|
||||
if (session.imageWatcherEnabled && !session.remote) {
|
||||
if (session.imageWatcherEnabled && !session.remote && !session.isAdopted) {
|
||||
imageWatcher.watchSession(session.id, session.workingDir);
|
||||
}
|
||||
}
|
||||
|
||||
+5
-1
@@ -552,9 +552,10 @@ export class WebServer extends EventEmitter {
|
||||
agentExited: (id) => !!this.sessions.get(id)?.paneExit,
|
||||
awaitingChoice: (id) => approvalInbox.listPending().some((item) => item.sessionId === id && item.kind !== 'idle'),
|
||||
// Remote (ssh) sessions keep their folder on another host, so received files have nowhere local to go.
|
||||
// An adopted session's folder is the neutral wrapper folder, which its agent need not be able to read.
|
||||
sessionDir: (sessionId) => {
|
||||
const session = this.sessions.get(sessionId);
|
||||
return session && !session.remote ? session.workingDir : undefined;
|
||||
return session && !session.remote && !session.isAdopted ? session.workingDir : undefined;
|
||||
},
|
||||
});
|
||||
private teamWatcher: TeamWatcher = new TeamWatcher();
|
||||
@@ -2281,6 +2282,9 @@ export class WebServer extends EventEmitter {
|
||||
): Promise<void> {
|
||||
const session = this.sessions.get(sessionId);
|
||||
if (!session) return;
|
||||
// ⚠️ A link scanned from an adopted pane was printed by a process on another host or in a
|
||||
// container, while confinement here would be to this host's `/tmp`, the wrapper's folder.
|
||||
if (session.isAdopted) return;
|
||||
const event =
|
||||
source === 'codex-generated'
|
||||
? await registerGeneratedArtifactAttachment({
|
||||
|
||||
Reference in new issue
Block a user