mirror of
https://github.com/badlogic/pi-mono.git
synced 2026-11-05 23:37:25 +00:00
fix(coding-agent): persist explicit loadout resets
This commit is contained in:
parent
a50ee9d1d0
commit
6f6f5683e1
4 changed files with 93 additions and 34 deletions
|
|
@ -28,6 +28,11 @@ import {
|
|||
|
||||
type ResourceCollection = keyof ResolvedPaths;
|
||||
|
||||
export interface SessionLoadoutSelection {
|
||||
overrides: LoadoutOverride[];
|
||||
explicitReset: boolean;
|
||||
}
|
||||
|
||||
type FlatEntry =
|
||||
| { type: "reset" }
|
||||
| { type: "group"; group: LoadoutResourceGroup }
|
||||
|
|
@ -107,7 +112,7 @@ class SessionLoadoutList implements Component, Focusable {
|
|||
private resetSelected = false;
|
||||
private _focused = false;
|
||||
|
||||
onApply?: (overrides: LoadoutOverride[]) => void;
|
||||
onApply?: (selection: SessionLoadoutSelection) => void;
|
||||
onCancel?: () => void;
|
||||
onChange?: () => void;
|
||||
|
||||
|
|
@ -253,9 +258,14 @@ class SessionLoadoutList implements Component, Focusable {
|
|||
return;
|
||||
}
|
||||
if (keybindings.matches(data, "tui.select.confirm")) {
|
||||
this.onApply?.(
|
||||
buildSessionLoadoutOverrides(this.snapshot, this.enabledByReference, this.clearExistingOverrides),
|
||||
);
|
||||
this.onApply?.({
|
||||
overrides: buildSessionLoadoutOverrides(
|
||||
this.snapshot,
|
||||
this.enabledByReference,
|
||||
this.clearExistingOverrides,
|
||||
),
|
||||
explicitReset: this.clearExistingOverrides,
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (data === " ") {
|
||||
|
|
@ -330,7 +340,7 @@ export class SessionLoadoutSelectorComponent extends Container implements Focusa
|
|||
snapshot: LoadoutSnapshot;
|
||||
agentDir: string;
|
||||
terminalHeight?: number;
|
||||
onApply: (overrides: LoadoutOverride[]) => void;
|
||||
onApply: (selection: SessionLoadoutSelection) => void;
|
||||
onCancel: () => void;
|
||||
requestRender: () => void;
|
||||
}) {
|
||||
|
|
|
|||
|
|
@ -75,12 +75,7 @@ import type {
|
|||
import { FooterDataProvider, type ReadonlyFooterDataProvider } from "../../core/footer-data-provider.ts";
|
||||
import { configureHttpDispatcher, formatHttpIdleTimeoutMs } from "../../core/http-dispatcher.ts";
|
||||
import { type AppKeybinding, KeybindingsManager } from "../../core/keybindings.ts";
|
||||
import {
|
||||
appendSessionLoadout,
|
||||
getSessionLoadout,
|
||||
type LoadoutOverride,
|
||||
loadoutOverridesEqual,
|
||||
} from "../../core/loadout.ts";
|
||||
import { appendSessionLoadout, getSessionLoadout, loadoutOverridesEqual } from "../../core/loadout.ts";
|
||||
import { createCompactionSummaryMessage } from "../../core/messages.ts";
|
||||
import {
|
||||
defaultModelPerProvider,
|
||||
|
|
@ -132,7 +127,10 @@ import {
|
|||
OAuthSelectorComponent,
|
||||
} from "./components/oauth-selector.ts";
|
||||
import { ScopedModelsSelectorComponent } from "./components/scoped-models-selector.ts";
|
||||
import { SessionLoadoutSelectorComponent } from "./components/session-loadout-selector.ts";
|
||||
import {
|
||||
type SessionLoadoutSelection,
|
||||
SessionLoadoutSelectorComponent,
|
||||
} from "./components/session-loadout-selector.ts";
|
||||
import { SessionSelectorComponent } from "./components/session-selector.ts";
|
||||
import { SettingsSelectorComponent } from "./components/settings-selector.ts";
|
||||
import { SkillInvocationMessageComponent } from "./components/skill-invocation-message.ts";
|
||||
|
|
@ -4179,7 +4177,7 @@ export class InteractiveMode {
|
|||
}
|
||||
}
|
||||
|
||||
private async applySessionLoadout(overrides: LoadoutOverride[], persist: boolean): Promise<void> {
|
||||
private async applySessionLoadout(selection: SessionLoadoutSelection, persist: boolean): Promise<void> {
|
||||
if (this.loadoutActionBlocked("applying")) return;
|
||||
const loader = this.getLoadoutLoader();
|
||||
if (!loader) {
|
||||
|
|
@ -4187,13 +4185,13 @@ export class InteractiveMode {
|
|||
return;
|
||||
}
|
||||
const previousOverrides = loader.getLoadoutSnapshot().overrides;
|
||||
loader.setLoadoutOverrides(overrides);
|
||||
loader.setLoadoutOverrides(selection.overrides);
|
||||
if (!(await this.handleReloadCommand())) {
|
||||
loader.setLoadoutOverrides(previousOverrides);
|
||||
return;
|
||||
}
|
||||
if (persist && !loadoutOverridesEqual(previousOverrides, overrides)) {
|
||||
appendSessionLoadout(this.sessionManager, overrides);
|
||||
if (persist && (selection.explicitReset || !loadoutOverridesEqual(previousOverrides, selection.overrides))) {
|
||||
appendSessionLoadout(this.sessionManager, selection.overrides);
|
||||
}
|
||||
this.showLoadoutDiagnostics();
|
||||
}
|
||||
|
|
@ -4210,10 +4208,10 @@ export class InteractiveMode {
|
|||
snapshot: loader.getLoadoutSnapshot(),
|
||||
agentDir: this.runtimeHost.services.agentDir,
|
||||
terminalHeight: this.ui.terminal.rows,
|
||||
onApply: (overrides) => {
|
||||
onApply: (selection) => {
|
||||
if (this.loadoutActionBlocked("applying")) return;
|
||||
done();
|
||||
void this.applySessionLoadout(overrides, true);
|
||||
void this.applySessionLoadout(selection, true);
|
||||
},
|
||||
onCancel: () => {
|
||||
done();
|
||||
|
|
@ -4241,7 +4239,7 @@ export class InteractiveMode {
|
|||
"Continue with the last session's extensions, skills, prompts, and themes? Missing resources will be skipped.",
|
||||
);
|
||||
if (!accepted) return;
|
||||
await this.applySessionLoadout(saved.overrides, false);
|
||||
await this.applySessionLoadout({ overrides: saved.overrides, explicitReset: false }, false);
|
||||
}
|
||||
|
||||
private showSettingsSelector(): void {
|
||||
|
|
|
|||
|
|
@ -76,10 +76,13 @@ describe("SessionLoadoutSelectorComponent", () => {
|
|||
list.handleInput!("\n");
|
||||
|
||||
expect(onApply).toHaveBeenCalledOnce();
|
||||
expect(onApply.mock.calls[0]?.[0]).toEqual([
|
||||
{ reference: missingPromptReference, enabled: true },
|
||||
{ reference: extensionReference, enabled: false },
|
||||
]);
|
||||
expect(onApply.mock.calls[0]?.[0]).toEqual({
|
||||
overrides: [
|
||||
{ reference: missingPromptReference, enabled: true },
|
||||
{ reference: extensionReference, enabled: false },
|
||||
],
|
||||
explicitReset: false,
|
||||
});
|
||||
});
|
||||
|
||||
it("discards with Escape and emits no staged changes", () => {
|
||||
|
|
@ -119,7 +122,7 @@ describe("SessionLoadoutSelectorComponent", () => {
|
|||
list.handleInput!(" ");
|
||||
list.handleInput!("\n");
|
||||
|
||||
expect(onApply).toHaveBeenCalledWith([]);
|
||||
expect(onApply).toHaveBeenCalledWith({ overrides: [], explicitReset: true });
|
||||
expect(stripAnsi(selector.render(100).join("\n"))).toContain("[x] Use persistent settings");
|
||||
});
|
||||
|
||||
|
|
@ -142,7 +145,27 @@ describe("SessionLoadoutSelectorComponent", () => {
|
|||
list.handleInput!(" ");
|
||||
list.handleInput!("\n");
|
||||
|
||||
expect(onApply).toHaveBeenCalledWith([{ reference: extensionReference, enabled: false }]);
|
||||
expect(onApply).toHaveBeenCalledWith({
|
||||
overrides: [{ reference: extensionReference, enabled: false }],
|
||||
explicitReset: true,
|
||||
});
|
||||
});
|
||||
|
||||
it("distinguishes applying an unchanged empty loadout from an explicit reset", () => {
|
||||
const snapshot = createSnapshot();
|
||||
snapshot.overrides = [];
|
||||
const onApply = vi.fn();
|
||||
const selector = new SessionLoadoutSelectorComponent({
|
||||
snapshot,
|
||||
agentDir: "/agent",
|
||||
onApply,
|
||||
onCancel: () => {},
|
||||
requestRender: () => {},
|
||||
});
|
||||
|
||||
selector.getResourceList().handleInput!("\n");
|
||||
|
||||
expect(onApply).toHaveBeenCalledWith({ overrides: [], explicitReset: false });
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import {
|
|||
type LoadoutOverride,
|
||||
type LoadoutSnapshot,
|
||||
} from "../../src/core/loadout.ts";
|
||||
import type { SessionLoadoutSelection } from "../../src/modes/interactive/components/session-loadout-selector.ts";
|
||||
import { InteractiveMode } from "../../src/modes/interactive/interactive-mode.ts";
|
||||
import { createHarness, type Harness } from "./harness.ts";
|
||||
|
||||
|
|
@ -21,7 +22,7 @@ type RestoreContext = {
|
|||
sessionManager: Harness["sessionManager"];
|
||||
getLoadoutRestoreDecisionKey: () => string;
|
||||
showExtensionConfirm: (title: string, message: string) => Promise<boolean>;
|
||||
applySessionLoadout: (overrides: LoadoutOverride[], persist: boolean) => Promise<void>;
|
||||
applySessionLoadout: (selection: SessionLoadoutSelection, persist: boolean) => Promise<void>;
|
||||
};
|
||||
|
||||
type ApplyContext = {
|
||||
|
|
@ -44,7 +45,7 @@ type ReloadBlockedContext = {
|
|||
|
||||
type InteractiveModePrivate = {
|
||||
maybeRestoreSavedLoadout(this: RestoreContext): Promise<void>;
|
||||
applySessionLoadout(this: ApplyContext, overrides: LoadoutOverride[], persist: boolean): Promise<void>;
|
||||
applySessionLoadout(this: ApplyContext, selection: SessionLoadoutSelection, persist: boolean): Promise<void>;
|
||||
loadoutActionBlocked(this: Pick<ApplyContext, "session" | "showWarning">, action: "opening" | "applying"): boolean;
|
||||
handleReloadCommand(this: ReloadBlockedContext): Promise<boolean>;
|
||||
};
|
||||
|
|
@ -81,7 +82,7 @@ describe("interactive session loadout restore", () => {
|
|||
await interactiveModePrototype.maybeRestoreSavedLoadout.call(context);
|
||||
|
||||
expect(showExtensionConfirm).toHaveBeenCalledOnce();
|
||||
expect(applySessionLoadout).toHaveBeenCalledWith(overrides, false);
|
||||
expect(applySessionLoadout).toHaveBeenCalledWith({ overrides, explicitReset: false }, false);
|
||||
expect(harness.sessionManager.getEntries()).toHaveLength(entryCount);
|
||||
});
|
||||
|
||||
|
|
@ -165,7 +166,7 @@ describe("interactive /loadout application", () => {
|
|||
showWarning,
|
||||
};
|
||||
|
||||
await interactiveModePrototype.applySessionLoadout.call(context, overrides, true);
|
||||
await interactiveModePrototype.applySessionLoadout.call(context, { overrides, explicitReset: false }, true);
|
||||
|
||||
expect(setLoadoutOverrides).toHaveBeenCalledWith(overrides);
|
||||
expect(handleReloadCommand).toHaveBeenCalledOnce();
|
||||
|
|
@ -173,9 +174,10 @@ describe("interactive /loadout application", () => {
|
|||
expect(showWarning).toHaveBeenCalledWith("Saved loadout skill is unavailable");
|
||||
});
|
||||
|
||||
it("does not write a reset marker when the declined saved loadout is already inactive", async () => {
|
||||
it("writes a reset tombstone when reset is explicit after a declined restore", async () => {
|
||||
const harness = await createTestHarness();
|
||||
appendSessionLoadout(harness.sessionManager, overrides);
|
||||
const entryCount = harness.sessionManager.getEntries().length;
|
||||
const setLoadoutOverrides = vi.fn();
|
||||
const context: ApplyContext = {
|
||||
session: { isStreaming: false, isCompacting: false, isBashRunning: false },
|
||||
|
|
@ -190,9 +192,35 @@ describe("interactive /loadout application", () => {
|
|||
showWarning: vi.fn(),
|
||||
};
|
||||
|
||||
await interactiveModePrototype.applySessionLoadout.call(context, [], true);
|
||||
await interactiveModePrototype.applySessionLoadout.call(context, { overrides: [], explicitReset: true }, true);
|
||||
|
||||
expect(setLoadoutOverrides).toHaveBeenCalledOnce();
|
||||
expect(setLoadoutOverrides).toHaveBeenCalledWith([]);
|
||||
expect(harness.sessionManager.getEntries()).toHaveLength(entryCount + 1);
|
||||
expect(getSessionLoadout(harness.sessionManager)?.overrides).toEqual([]);
|
||||
});
|
||||
|
||||
it("does not write a reset tombstone for an unchanged apply after a declined restore", async () => {
|
||||
const harness = await createTestHarness();
|
||||
appendSessionLoadout(harness.sessionManager, overrides);
|
||||
const entryCount = harness.sessionManager.getEntries().length;
|
||||
const setLoadoutOverrides = vi.fn();
|
||||
const context: ApplyContext = {
|
||||
session: { isStreaming: false, isCompacting: false, isBashRunning: false },
|
||||
sessionManager: harness.sessionManager,
|
||||
getLoadoutLoader: () => ({
|
||||
getLoadoutSnapshot: () => ({ resources: [], overrides: [], diagnostics: [] }),
|
||||
setLoadoutOverrides,
|
||||
}),
|
||||
loadoutActionBlocked: () => false,
|
||||
handleReloadCommand: async () => true,
|
||||
showLoadoutDiagnostics: () => {},
|
||||
showWarning: vi.fn(),
|
||||
};
|
||||
|
||||
await interactiveModePrototype.applySessionLoadout.call(context, { overrides: [], explicitReset: false }, true);
|
||||
|
||||
expect(setLoadoutOverrides).toHaveBeenCalledWith([]);
|
||||
expect(harness.sessionManager.getEntries()).toHaveLength(entryCount);
|
||||
expect(getSessionLoadout(harness.sessionManager)?.overrides).toEqual(overrides);
|
||||
});
|
||||
|
||||
|
|
@ -212,7 +240,7 @@ describe("interactive /loadout application", () => {
|
|||
showWarning: vi.fn(),
|
||||
};
|
||||
|
||||
await interactiveModePrototype.applySessionLoadout.call(context, overrides, true);
|
||||
await interactiveModePrototype.applySessionLoadout.call(context, { overrides, explicitReset: false }, true);
|
||||
|
||||
expect(setLoadoutOverrides).toHaveBeenNthCalledWith(1, overrides);
|
||||
expect(setLoadoutOverrides).toHaveBeenNthCalledWith(2, []);
|
||||
|
|
@ -244,7 +272,7 @@ describe("interactive /loadout application", () => {
|
|||
showWarning,
|
||||
};
|
||||
|
||||
await interactiveModePrototype.applySessionLoadout.call(context, overrides, true);
|
||||
await interactiveModePrototype.applySessionLoadout.call(context, { overrides, explicitReset: false }, true);
|
||||
|
||||
expect(setLoadoutOverrides).not.toHaveBeenCalled();
|
||||
expect(handleReloadCommand).not.toHaveBeenCalled();
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue