diff --git a/apps/server/src/terminal/Manager.test.ts b/apps/server/src/terminal/Manager.test.ts index 42ebe1a0e16d..d4056e74fa9e 100644 --- a/apps/server/src/terminal/Manager.test.ts +++ b/apps/server/src/terminal/Manager.test.ts @@ -747,6 +747,7 @@ it.layer( threadId: "thread-1", terminalId: DEFAULT_TERMINAL_ID, afterSequence: initialSequence, + afterEpoch: initialSnapshot.snapshot.attachEpoch, }, (event) => Ref.update(resumedEvents, (events) => [...events, event]), ); @@ -775,6 +776,7 @@ it.layer( threadId: "thread-1", terminalId: DEFAULT_TERMINAL_ID, afterSequence: opened.sequence, + afterEpoch: opened.attachEpoch, }, (event) => Ref.update(attachEvents, (events) => [...events, event]), ); @@ -799,6 +801,7 @@ it.layer( threadId: "thread-1", terminalId: DEFAULT_TERMINAL_ID, afterSequence: opened.sequence + 1, + afterEpoch: opened.attachEpoch, }, (event) => Ref.update(attachEvents, (events) => [...events, event]), ); @@ -835,6 +838,7 @@ it.layer( threadId: "thread-1", terminalId: DEFAULT_TERMINAL_ID, afterSequence: opened.sequence, + afterEpoch: opened.attachEpoch, }, (event) => Ref.update(attachEvents, (events) => [...events, event]), ); diff --git a/apps/server/src/terminal/Manager.ts b/apps/server/src/terminal/Manager.ts index 8599f5bea3b3..66f5d405fb62 100644 --- a/apps/server/src/terminal/Manager.ts +++ b/apps/server/src/terminal/Manager.ts @@ -254,6 +254,8 @@ interface TerminalSessionState { exitSignal: number | null; updatedAt: string; eventSequence: number; + /** Coder: a new value per shell session, because each session restarts its sequences. */ + attachEpoch: string; inputCount: number; cols: number; rows: number; @@ -346,6 +348,7 @@ function snapshot(session: TerminalSessionState): TerminalSessionSnapshot { label: terminalWireLabel(session), updatedAt: session.updatedAt, sequence: session.eventSequence, + attachEpoch: session.attachEpoch, }; } @@ -2660,6 +2663,7 @@ export const makeWithOptions = Effect.fn("TerminalManager.makeWithOptions")(func exitSignal: null, updatedAt: yield* nowIso, eventSequence: 0, + attachEpoch: crypto.randomUUID(), inputCount: 0, cols, rows, @@ -2863,6 +2867,10 @@ export const makeWithOptions = Effect.fn("TerminalManager.makeWithOptions")(func const afterSequence = input.afterSequence; const snapshotSequence = initialSnapshot.sequence; if (afterSequence === undefined || snapshotSequence === undefined) return null; + // A cursor from another shell session numbers different events. + if (input.afterEpoch === undefined || input.afterEpoch !== initialSnapshot.attachEpoch) { + return null; + } if (afterSequence > snapshotSequence) return null; if (afterSequence === snapshotSequence) return []; const replay = attachReplay.get(toSessionKey(input.threadId, input.terminalId)); @@ -3147,6 +3155,7 @@ export const makeWithOptions = Effect.fn("TerminalManager.makeWithOptions")(func exitSignal: null, updatedAt: yield* nowIso, eventSequence: 0, + attachEpoch: crypto.randomUUID(), inputCount: 0, cols, rows, diff --git a/apps/web/src/components/GitActionsControl.tsx b/apps/web/src/components/GitActionsControl.tsx index 79ed4a4c1b3a..4dee0914b47a 100644 --- a/apps/web/src/components/GitActionsControl.tsx +++ b/apps/web/src/components/GitActionsControl.tsx @@ -1088,6 +1088,17 @@ export default function GitActionsControl({ const changesTotals = gitStatusForActions?.branchChanges ?? gitStatusForActions?.workingTree; const allFiles = gitStatusForActions?.workingTree.files ?? []; + // Coder: status paths are repository-relative, and the Files surface opens paths relative to + // the thread's folder, which can be a subfolder of the repository. + const changedFileOpenPath = (filePath: string): string | null => { + const rootPath = gitStatusForActions?.workingTree.rootPath; + if (!rootPath || !gitCwd) return filePath; + const root = rootPath.replace(/\/+$/, ""); + const base = gitCwd.replace(/\/+$/, ""); + if (!base.startsWith(`${root}/`)) return filePath; + const prefix = `${base.slice(root.length + 1)}/`; + return filePath.startsWith(prefix) ? filePath.slice(prefix.length) : null; + }; const selectedFiles = allFiles.filter((f) => !excludedFiles.has(f.path)); const allSelected = excludedFiles.size === 0; const noneSelected = selectedFiles.length === 0; @@ -1903,8 +1914,11 @@ export default function GitActionsControl({