feat: inject toolset swap and slash-command context reminders (#1022)

This commit is contained in:
Charles Packer
2026-02-18 18:15:38 -08:00
committed by GitHub
parent f90244de82
commit a0a5bbfc72
10 changed files with 458 additions and 16 deletions

View File

@@ -75,6 +75,27 @@ describe("commandRunner", () => {
});
expect(buffers.order).toEqual(["cmd-1"]);
});
test("onCommandFinished fires once on running->finished transition", () => {
const buffers = createBuffers();
const buffersRef = { current: buffers };
const finishedEvents: Array<{ input: string; output: string }> = [];
const runner = createCommandRunner({
buffersRef,
refreshDerived: () => {},
createId: () => "cmd-1",
onCommandFinished: (event) => {
finishedEvents.push({ input: event.input, output: event.output });
},
});
const cmd = runner.start("/model", "Opening model selector...");
cmd.update({ output: "Still opening...", phase: "running" });
cmd.finish("Switched", true);
cmd.finish("Switched again", true);
expect(finishedEvents).toEqual([{ input: "/model", output: "Switched" }]);
});
});
describe("command input preservation in handlers", () => {

View File

@@ -0,0 +1,35 @@
import { describe, expect, test } from "bun:test";
import { readFileSync } from "node:fs";
import { fileURLToPath } from "node:url";
function readAppSource(): string {
const appPath = fileURLToPath(new URL("../../cli/App.tsx", import.meta.url));
return readFileSync(appPath, "utf-8");
}
describe("interaction reminder wiring", () => {
test("command runner finish events are wired into shared reminder state", () => {
const source = readAppSource();
expect(source).toContain("const recordCommandReminder = useCallback(");
expect(source).toContain(
"enqueueCommandIoReminder(sharedReminderStateRef.current",
);
expect(source).toContain("onCommandFinished: recordCommandReminder");
});
test("model/toolset handlers enqueue toolset change reminder snapshots", () => {
const source = readAppSource();
expect(source).toContain(
"const maybeRecordToolsetChangeReminder = useCallback(",
);
expect(source).toContain(
"const previousToolNamesSnapshot = getToolNames();",
);
expect(source).toContain('source: "/model (auto toolset)"');
expect(source).toContain('source: "/model (manual toolset override)"');
expect(source).toContain('source: "/toolset"');
expect(source).toContain(
"enqueueToolsetChangeReminder(sharedReminderStateRef.current",
);
});
});

View File

@@ -18,14 +18,29 @@ describe("shared reminder catalog", () => {
expect(unique.size).toBe(SHARED_REMINDER_IDS.length);
});
test("all reminders target all runtime modes", () => {
for (const reminder of SHARED_REMINDER_CATALOG) {
expect(reminder.modes).toContain("interactive");
expect(reminder.modes).toContain("headless-one-shot");
expect(reminder.modes).toContain("headless-bidirectional");
test("every runtime mode has at least one reminder", () => {
const modes: Array<
"interactive" | "headless-one-shot" | "headless-bidirectional"
> = ["interactive", "headless-one-shot", "headless-bidirectional"];
for (const mode of modes) {
expect(
SHARED_REMINDER_CATALOG.some((entry) => entry.modes.includes(mode)),
).toBe(true);
}
});
test("command and toolset reminders are interactive-only", () => {
const commandReminder = SHARED_REMINDER_CATALOG.find(
(entry) => entry.id === "command-io",
);
const toolsetReminder = SHARED_REMINDER_CATALOG.find(
(entry) => entry.id === "toolset-change",
);
expect(commandReminder?.modes).toEqual(["interactive"]);
expect(toolsetReminder?.modes).toEqual(["interactive"]);
});
test("provider ids and catalog ids stay in lockstep", () => {
expect(Object.keys(sharedReminderProviders).sort()).toEqual(
[...SHARED_REMINDER_IDS].sort(),

View File

@@ -1,7 +1,12 @@
import { afterEach, describe, expect, test } from "bun:test";
import type { SkillSource } from "../../agent/skills";
import type { ReflectionSettings } from "../../cli/helpers/memoryReminder";
import { SHARED_REMINDER_IDS } from "../../reminders/catalog";
import {
SHARED_REMINDER_CATALOG,
SHARED_REMINDER_IDS,
type SharedReminderId,
type SharedReminderMode,
} from "../../reminders/catalog";
import {
buildSharedReminderParts,
sharedReminderProviders,
@@ -11,6 +16,12 @@ import { createSharedReminderState } from "../../reminders/state";
const originalProviders = { ...sharedReminderProviders };
const providerMap = sharedReminderProviders;
function reminderIdsForMode(mode: SharedReminderMode): SharedReminderId[] {
return SHARED_REMINDER_CATALOG.filter((entry) =>
entry.modes.includes(mode),
).map((entry) => entry.id);
}
afterEach(() => {
for (const reminderId of SHARED_REMINDER_IDS) {
providerMap[reminderId] = originalProviders[reminderId];
@@ -58,15 +69,23 @@ describe("shared reminder parity", () => {
state: createSharedReminderState(),
});
expect(interactive.appliedReminderIds).toEqual(SHARED_REMINDER_IDS);
expect(oneShot.appliedReminderIds).toEqual(SHARED_REMINDER_IDS);
expect(bidirectional.appliedReminderIds).toEqual(SHARED_REMINDER_IDS);
expect(interactive.parts.map((part) => part.text)).toEqual(
SHARED_REMINDER_IDS,
expect(interactive.appliedReminderIds).toEqual(
reminderIdsForMode("interactive"),
);
expect(oneShot.appliedReminderIds).toEqual(
reminderIdsForMode("headless-one-shot"),
);
expect(bidirectional.appliedReminderIds).toEqual(
reminderIdsForMode("headless-bidirectional"),
);
expect(interactive.parts.map((part) => part.text)).toEqual(
reminderIdsForMode("interactive"),
);
expect(oneShot.parts.map((part) => part.text)).toEqual(
reminderIdsForMode("headless-one-shot"),
);
expect(oneShot.parts.map((part) => part.text)).toEqual(SHARED_REMINDER_IDS);
expect(bidirectional.parts.map((part) => part.text)).toEqual(
SHARED_REMINDER_IDS,
reminderIdsForMode("headless-bidirectional"),
);
});
});

View File

@@ -0,0 +1,90 @@
import { describe, expect, test } from "bun:test";
import {
type SharedReminderContext,
sharedReminderProviders,
} from "../../reminders/engine";
import {
createSharedReminderState,
enqueueCommandIoReminder,
enqueueToolsetChangeReminder,
type SharedReminderState,
} from "../../reminders/state";
function baseContext(
state: SharedReminderState,
mode: SharedReminderContext["mode"] = "interactive",
): SharedReminderContext {
return {
mode,
agent: {
id: "agent-1",
name: "Agent 1",
description: null,
lastRunAt: null,
},
state,
sessionContextReminderEnabled: false,
reflectionSettings: {
trigger: "off",
behavior: "reminder",
stepCount: 25,
},
skillSources: [],
resolvePlanModeReminder: () => "",
};
}
describe("interaction reminders", () => {
test("command-io provider renders escaped command input/output and drains queue", async () => {
const state = createSharedReminderState();
enqueueCommandIoReminder(state, {
input: '/model && echo "<unsafe>"',
output: "Models dialog dismissed <ok>",
success: true,
});
const reminder = await sharedReminderProviders["command-io"](
baseContext(state),
);
expect(reminder).toContain("<user-command-input>");
expect(reminder).toContain("&lt;unsafe&gt;");
expect(reminder).toContain("<user-command-output>");
expect(reminder).toContain("&lt;ok&gt;");
expect(state.pendingCommandIoReminders).toHaveLength(0);
});
test("toolset-change provider renders previous/new toolset and drains queue", async () => {
const state = createSharedReminderState();
enqueueToolsetChangeReminder(state, {
source: "/toolset",
previousToolset: "default",
newToolset: "codex",
previousTools: ["Read", "Write"],
newTools: ["ReadFile", "ApplyPatch", "ShellCommand"],
});
const reminder = await sharedReminderProviders["toolset-change"](
baseContext(state),
);
expect(reminder).toContain("<source>/toolset</source>");
expect(reminder).toContain("<previous-toolset>default</previous-toolset>");
expect(reminder).toContain("<new-toolset>codex</new-toolset>");
expect(reminder).toContain("<previous-tools>Read, Write</previous-tools>");
expect(reminder).toContain(
"<new-tools>ReadFile, ApplyPatch, ShellCommand</new-tools>",
);
expect(state.pendingToolsetChangeReminders).toHaveLength(0);
});
test("interaction reminder providers return null when there is no queued data", async () => {
const state = createSharedReminderState();
const commandReminder = await sharedReminderProviders["command-io"](
baseContext(state),
);
const toolsetReminder = await sharedReminderProviders["toolset-change"](
baseContext(state),
);
expect(commandReminder).toBeNull();
expect(toolsetReminder).toBeNull();
});
});