fix(vibe): only override the vibe snapshot when teardown lost the live toolset
The persisted pre-vibe snapshot was applied on every reconciliation that re-entered vibe mode, including cold resumes and switches in from a non-vibe session. Those paths build their toolset from the current CLI flags and settings, so replacing it with a historical snapshot silently drops tools the session was started with: resuming a session that entered vibe under --tools read with --tools read,bash restored only read on exit. Gate the override on the one case the snapshot exists for: the vibe -> vibe switch, where #clearTransientModeState kept the already-reduced live set.
This commit is contained in:
@@ -2513,6 +2513,14 @@ export class InteractiveMode implements InteractiveModeContext {
|
||||
this.#vibeModeOwnerScope?.ownerId === targetVibeScope.ownerId &&
|
||||
this.#vibeModeOwnerScope.parentSessionId === targetVibeScope.parentSessionId &&
|
||||
this.#vibeModeOwnerScope.parentSessionFile === targetVibeScope.parentSessionFile;
|
||||
// #clearTransientModeState below keeps the live active set instead of
|
||||
// applying a snapshot, so for a vibe -> vibe switch the live toolset is
|
||||
// already the reduced vibe set and cannot serve as the pre-vibe snapshot.
|
||||
// That is the only case the persisted snapshot is for: a cold resume or a
|
||||
// switch in from a non-vibe session built its toolset from the current CLI
|
||||
// flags and settings, and that set — not a historical one — is what exiting
|
||||
// vibe must restore.
|
||||
const vibeToolsetLostToTeardown = this.vibeModeEnabled && !preserveVibe;
|
||||
await this.#clearTransientModeState({ preserveVibe, vibeScopeAlreadySuspended });
|
||||
await VibeSessionRegistry.global().rehydrate(vibeSession);
|
||||
const goalEnabled = this.session.settings.get("goal.enabled");
|
||||
@@ -2552,7 +2560,9 @@ export class InteractiveMode implements InteractiveModeContext {
|
||||
if (!preserveVibe) {
|
||||
await this.#enterVibeMode({
|
||||
persistModeChange: false,
|
||||
previousTools: readPersistedToolNames(sessionContext.modeData?.previousTools),
|
||||
previousTools: vibeToolsetLostToTeardown
|
||||
? readPersistedToolNames(sessionContext.modeData?.previousTools)
|
||||
: undefined,
|
||||
});
|
||||
}
|
||||
return;
|
||||
|
||||
@@ -322,6 +322,73 @@ describe("InteractiveMode vibe mode toggle", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("keeps the freshly built toolset when resuming a vibe session from outside vibe mode", async () => {
|
||||
const model = session.model;
|
||||
if (!model) throw new Error("Expected active model");
|
||||
const openFixture = (toolNames: string[]) => {
|
||||
const opened = new AgentSession({
|
||||
agent: new Agent({
|
||||
initialState: {
|
||||
model,
|
||||
systemPrompt: ["Test"],
|
||||
tools: [],
|
||||
messages: [],
|
||||
},
|
||||
}),
|
||||
sessionManager: SessionManager.create(tempDir.path(), tempDir.path()),
|
||||
settings: Settings.isolated({}),
|
||||
modelRegistry,
|
||||
toolRegistry: new Map(toolNames.map(name => [name, stubTool(name)])),
|
||||
builtInToolNames: toolNames,
|
||||
createVibeTools: () => VIBE_TOOL_NAMES.map(stubTool),
|
||||
});
|
||||
return {
|
||||
session: opened,
|
||||
mode: new InteractiveMode(opened, "test", undefined, undefined, undefined, undefined, new EventBus()),
|
||||
};
|
||||
};
|
||||
|
||||
// Target session entered vibe when only `read` and `todo` existed, so its
|
||||
// persisted snapshot predates `bash`.
|
||||
const { session: targetSession, mode: targetMode } = openFixture(["read", "todo"]);
|
||||
let targetFile: string;
|
||||
try {
|
||||
await targetMode.init({ suppressWelcomeIntro: true });
|
||||
await targetSession.setActiveToolsByName(["read", "todo"]);
|
||||
await targetMode.handleVibeModeCommand();
|
||||
await targetSession.sessionManager.ensureOnDisk();
|
||||
const file = targetSession.sessionFile;
|
||||
if (!file) throw new Error("Expected persisted session file");
|
||||
targetFile = file;
|
||||
} finally {
|
||||
targetMode.stop();
|
||||
await targetSession.dispose();
|
||||
}
|
||||
|
||||
// The resuming process is not in vibe mode, so the teardown path never
|
||||
// runs and its live toolset — built from the current CLI flags and
|
||||
// settings, here including `bash` — is the real pre-vibe set. The stale
|
||||
// persisted snapshot must not override it, or `bash` would be dropped for
|
||||
// the rest of the session.
|
||||
const { session: resumed, mode: resumedMode } = openFixture(["read", "todo", "bash"]);
|
||||
try {
|
||||
await resumedMode.init({ suppressWelcomeIntro: true });
|
||||
await resumed.setActiveToolsByName(["read", "todo", "bash"]);
|
||||
expect(resumedMode.vibeModeEnabled).toBe(false);
|
||||
|
||||
expect(await resumed.switchSession(targetFile)).toBe(true);
|
||||
expect(resumedMode.vibeModeEnabled).toBe(true);
|
||||
expect(resumed.getActiveToolNames()).not.toContain("bash");
|
||||
|
||||
await resumedMode.handleVibeModeCommand();
|
||||
expect(resumedMode.vibeModeEnabled).toBe(false);
|
||||
expect(resumed.getActiveToolNames().toSorted()).toEqual(["bash", "read", "todo"]);
|
||||
} finally {
|
||||
resumedMode.stop();
|
||||
await resumed.dispose();
|
||||
}
|
||||
});
|
||||
|
||||
it("passes the session's active model into vibe rehydration on resume", async () => {
|
||||
await mode.init({ suppressWelcomeIntro: true });
|
||||
await mode.handleVibeModeCommand();
|
||||
|
||||
Reference in New Issue
Block a user