Merge PR #3675: keep moved sessions resumable (@riverpilot)

# Conflicts:
#	packages/coding-agent/src/modes/controllers/command-controller.ts
This commit is contained in:
can1357
2026-06-28 18:46:19 +02:00
10 changed files with 187 additions and 79 deletions
@@ -42,7 +42,6 @@ import type { AsyncJobSnapshotItem } from "../../session/agent-session";
import type { AuthStorage, OAuthAccountIdentity } from "../../session/auth-storage";
import type { CompactMode } from "../../session/compact-modes";
import type { NewSessionOptions } from "../../session/session-entries";
import { SessionManager } from "../../session/session-manager";
import { formatShakeSummary, type ShakeMode, type ShakeResult } from "../../session/shake-types";
import { limitMatchesActiveAccount } from "../../slash-commands/helpers/active-oauth-account";
import { outputMeta } from "../../tools/output-meta";
@@ -935,13 +934,13 @@ export class CommandController {
}
/**
* `/move` — switch to a fresh empty session in a different directory.
* `/move` — relocate the current session to a different directory.
*
* With no `targetPath` (TUI only), opens an autocomplete overlay so the user
* can pick or type a directory. With a `targetPath`, resolves it directly.
* If the target directory does not exist, the user is asked whether to create
* it. A brand-new empty session is then started in the target directory and
* the current session is left behind (resumable via `/resume`).
* it. The active session file and artifacts are moved into the target
* directory's session bucket so `/resume` from that directory can find it.
*/
async handleMoveCommand(targetPath?: string): Promise<void> {
if (this.ctx.session.isStreaming) {
@@ -1003,47 +1002,18 @@ export class CommandController {
}
}
let newSessionFile: string | undefined;
try {
// Create a fresh empty session file in the target directory's session
// folder, then switch to it. The current session is left behind and
// remains resumable via /resume.
newSessionFile = SessionManager.createEmptySessionFile(resolvedPath);
const switched = await this.ctx.session.switchSession(newSessionFile);
if (!switched) {
await this.ctx.sessionManager.dropSession(newSessionFile);
return;
}
await this.ctx.sessionManager.moveTo(resolvedPath);
} catch (err) {
if (newSessionFile) {
try {
await this.ctx.sessionManager.dropSession(newSessionFile);
} catch (dropErr) {
this.ctx.showError(
`Move failed: ${err instanceof Error ? err.message : String(err)}; failed to remove empty session: ${dropErr instanceof Error ? dropErr.message : String(dropErr)}`,
);
return;
}
}
this.ctx.showError(`Move failed: ${err instanceof Error ? err.message : String(err)}`);
return;
}
this.ctx.session.markMovedFromEmptySessionFile(newSessionFile!);
await this.ctx.applyCwdChange(resolvedPath);
this.ctx.chatContainer.clear();
this.ctx.pendingMessagesContainer.clear();
this.ctx.compactionQueuedMessages = [];
this.ctx.streamingComponent = undefined;
this.ctx.streamingMessage = undefined;
this.ctx.pendingTools.clear();
this.ctx.statusLine.invalidate();
this.ctx.statusLine.resetActiveTime();
this.ctx.updateEditorTopBorder();
this.ctx.updateEditorBorderColor();
await this.ctx.reloadTodos();
this.ctx.ui.requestRender(true, { clearScrollback: true });
this.ctx.ui.requestRender();
this.ctx.present([
new Spacer(1),