From a0a5bbfc72fe92a713ebf878d1bb5760a5ad7a9d Mon Sep 17 00:00:00 2001 From: Charles Packer Date: Wed, 18 Feb 2026 18:15:38 -0800 Subject: [PATCH] feat: inject toolset swap and slash-command context reminders (#1022) --- src/cli/App.tsx | 79 +++++++++++++- src/cli/commands/runner.ts | 30 +++++ src/reminders/catalog.ts | 14 ++- src/reminders/engine.ts | 103 ++++++++++++++++++ src/reminders/state.ts | 42 +++++++ src/tests/cli/commandRunner.test.ts | 21 ++++ .../cli/interaction-reminder-wiring.test.ts | 35 ++++++ src/tests/reminders/catalog.test.ts | 25 ++++- src/tests/reminders/engine-parity.test.ts | 35 ++++-- .../reminders/interaction-reminders.test.ts | 90 +++++++++++++++ 10 files changed, 458 insertions(+), 16 deletions(-) create mode 100644 src/tests/cli/interaction-reminder-wiring.test.ts create mode 100644 src/tests/reminders/interaction-reminders.test.ts diff --git a/src/cli/App.tsx b/src/cli/App.tsx index 6ed8738..77d2c6d 100644 --- a/src/cli/App.tsx +++ b/src/cli/App.tsx @@ -85,6 +85,8 @@ import { import { buildSharedReminderParts } from "../reminders/engine"; import { createSharedReminderState, + enqueueCommandIoReminder, + enqueueToolsetChangeReminder, resetSharedReminderState, syncReminderStateFromContextTracker, } from "../reminders/state"; @@ -95,6 +97,7 @@ import { analyzeToolApproval, checkToolPermission, executeTool, + getToolNames, releaseToolExecutionContext, savePermissionRule, type ToolExecutionResult, @@ -117,7 +120,11 @@ import { setActiveCommandId as setActiveProfileCommandId, validateProfileLoad, } from "./commands/profile"; -import { type CommandHandle, createCommandRunner } from "./commands/runner"; +import { + type CommandFinishedEvent, + type CommandHandle, + createCommandRunner, +} from "./commands/runner"; import { AgentSelector } from "./components/AgentSelector"; // ApprovalDialog removed - all approvals now render inline import { ApprovalPreview } from "./components/ApprovalPreview"; @@ -2281,14 +2288,47 @@ export default function App({ commitEligibleLines(b); }, [commitEligibleLines]); + const recordCommandReminder = useCallback((event: CommandFinishedEvent) => { + const input = event.input.trim(); + if (!input.startsWith("/")) { + return; + } + enqueueCommandIoReminder(sharedReminderStateRef.current, { + input, + output: event.output, + success: event.success, + }); + }, []); + + const maybeRecordToolsetChangeReminder = useCallback( + (params: { + source: string; + previousToolset: string | null; + newToolset: string | null; + previousTools: string[]; + newTools: string[]; + }) => { + const toolsetChanged = params.previousToolset !== params.newToolset; + const previousSnapshot = params.previousTools.join("\n"); + const nextSnapshot = params.newTools.join("\n"); + const toolsChanged = previousSnapshot !== nextSnapshot; + if (!toolsetChanged && !toolsChanged) { + return; + } + enqueueToolsetChangeReminder(sharedReminderStateRef.current, params); + }, + [], + ); + const commandRunner = useMemo( () => createCommandRunner({ buffersRef, refreshDerived, createId: uid, + onCommandFinished: recordCommandReminder, }), - [refreshDerived], + [recordCommandReminder, refreshDerived], ); const startOverlayCommand = useCallback( @@ -10034,6 +10074,8 @@ ${SYSTEM_REMINDER_CLOSE} const persistedToolsetPreference = settingsManager.getToolsetPreference(agentId); + const previousToolsetSnapshot = currentToolset; + const previousToolNamesSnapshot = getToolNames(); let toolsetNoticeLine: string | null = null; if (persistedToolsetPreference === "auto") { @@ -10048,11 +10090,25 @@ ${SYSTEM_REMINDER_CLOSE} "Auto toolset selected: switched to " + toolsetName + ". Use /toolset to set a manual override."; + maybeRecordToolsetChangeReminder({ + source: "/model (auto toolset)", + previousToolset: previousToolsetSnapshot, + newToolset: toolsetName, + previousTools: previousToolNamesSnapshot, + newTools: getToolNames(), + }); } else { const { forceToolsetSwitch } = await import("../tools/toolset"); if (currentToolset !== persistedToolsetPreference) { await forceToolsetSwitch(persistedToolsetPreference, agentId); setCurrentToolset(persistedToolsetPreference); + maybeRecordToolsetChangeReminder({ + source: "/model (manual toolset override)", + previousToolset: previousToolsetSnapshot, + newToolset: persistedToolsetPreference, + previousTools: previousToolNamesSnapshot, + newTools: getToolNames(), + }); } setCurrentToolsetPreference(persistedToolsetPreference); toolsetNoticeLine = @@ -10093,6 +10149,7 @@ ${SYSTEM_REMINDER_CLOSE} consumeOverlayCommand, currentToolset, isAgentBusy, + maybeRecordToolsetChangeReminder, resetPendingReasoningCycle, withCommandLock, ], @@ -10297,6 +10354,8 @@ ${SYSTEM_REMINDER_CLOSE} const { forceToolsetSwitch, switchToolsetForModel } = await import( "../tools/toolset" ); + const previousToolsetSnapshot = currentToolset; + const previousToolNamesSnapshot = getToolNames(); if (toolsetId === "auto") { const modelHandle = @@ -10317,6 +10376,13 @@ ${SYSTEM_REMINDER_CLOSE} settingsManager.setToolsetPreference(agentId, "auto"); setCurrentToolsetPreference("auto"); setCurrentToolset(derivedToolset); + maybeRecordToolsetChangeReminder({ + source: "/toolset", + previousToolset: previousToolsetSnapshot, + newToolset: derivedToolset, + previousTools: previousToolNamesSnapshot, + newTools: getToolNames(), + }); cmd.finish( `Toolset mode set to auto (currently ${derivedToolset}).`, true, @@ -10328,6 +10394,13 @@ ${SYSTEM_REMINDER_CLOSE} settingsManager.setToolsetPreference(agentId, toolsetId); setCurrentToolsetPreference(toolsetId); setCurrentToolset(toolsetId); + maybeRecordToolsetChangeReminder({ + source: "/toolset", + previousToolset: previousToolsetSnapshot, + newToolset: toolsetId, + previousTools: previousToolNamesSnapshot, + newTools: getToolNames(), + }); cmd.finish( `Switched toolset to ${toolsetId} (manual override)`, true, @@ -10342,9 +10415,11 @@ ${SYSTEM_REMINDER_CLOSE} agentId, commandRunner, consumeOverlayCommand, + currentToolset, currentModelHandle, isAgentBusy, llmConfig, + maybeRecordToolsetChangeReminder, withCommandLock, ], ); diff --git a/src/cli/commands/runner.ts b/src/cli/commands/runner.ts index 05b3a1d..edaeeac 100644 --- a/src/cli/commands/runner.ts +++ b/src/cli/commands/runner.ts @@ -24,12 +24,22 @@ export type CommandHandle = { fail: (output: string) => void; }; +export type CommandFinishedEvent = { + id: string; + input: string; + output: string; + success: boolean; + dimOutput?: boolean; + preformatted?: boolean; +}; + type CreateId = (prefix: string) => string; type RunnerDeps = { buffersRef: MutableRefObject; refreshDerived: () => void; createId: CreateId; + onCommandFinished?: (event: CommandFinishedEvent) => void; }; function upsertCommandLine( @@ -56,13 +66,33 @@ export function createCommandRunner({ buffersRef, refreshDerived, createId, + onCommandFinished, }: RunnerDeps) { function getHandle(id: string, input: string): CommandHandle { const update = (updateData: CommandUpdate) => { + const previous = buffersRef.current.byId.get(id); + const wasFinished = + previous?.kind === "command" && previous.phase === "finished"; + upsertCommandLine(buffersRef.current, id, input, updateData); if (!buffersRef.current.order.includes(id)) { buffersRef.current.order.push(id); } + + const next = buffersRef.current.byId.get(id); + const becameFinished = + !wasFinished && next?.kind === "command" && next.phase === "finished"; + if (becameFinished) { + onCommandFinished?.({ + id, + input: next.input, + output: next.output, + success: next.success !== false, + dimOutput: next.dimOutput, + preformatted: next.preformatted, + }); + } + refreshDerived(); }; diff --git a/src/reminders/catalog.ts b/src/reminders/catalog.ts index 34a2b04..80c2674 100644 --- a/src/reminders/catalog.ts +++ b/src/reminders/catalog.ts @@ -9,7 +9,9 @@ export type SharedReminderId = | "permission-mode" | "plan-mode" | "reflection-step-count" - | "reflection-compaction"; + | "reflection-compaction" + | "command-io" + | "toolset-change"; export interface SharedReminderDefinition { id: SharedReminderId; @@ -50,6 +52,16 @@ export const SHARED_REMINDER_CATALOG: ReadonlyArray = "Compaction-triggered reflection reminder/auto-launch behavior", modes: ["interactive", "headless-one-shot", "headless-bidirectional"], }, + { + id: "command-io", + description: "Recent slash command input/output context", + modes: ["interactive"], + }, + { + id: "toolset-change", + description: "Client-side toolset change context", + modes: ["interactive"], + }, ]; export const SHARED_REMINDER_IDS = SHARED_REMINDER_CATALOG.map( diff --git a/src/reminders/engine.ts b/src/reminders/engine.ts index d5f5809..eba40b1 100644 --- a/src/reminders/engine.ts +++ b/src/reminders/engine.ts @@ -242,6 +242,107 @@ async function buildReflectionCompactionReminder( return buildCompactionMemoryReminder(context.agent.id); } +const MAX_COMMAND_REMINDERS_PER_TURN = 10; +const MAX_TOOLSET_REMINDERS_PER_TURN = 5; +const MAX_COMMAND_INPUT_CHARS = 2000; +const MAX_COMMAND_OUTPUT_CHARS = 4000; +const MAX_TOOL_LIST_CHARS = 3000; + +function escapeXml(value: string): string { + return value + .replace(/&/g, "&") + .replace(//g, ">"); +} + +function truncate(value: string, maxChars: number): string { + if (value.length <= maxChars) { + return value; + } + return `${value.slice(0, maxChars)}... [truncated]`; +} + +function formatToolList(tools: string[]): string { + const uniqueTools = Array.from(new Set(tools)); + if (uniqueTools.length === 0) { + return "(none)"; + } + return truncate(uniqueTools.join(", "), MAX_TOOL_LIST_CHARS); +} + +async function buildCommandIoReminder( + context: SharedReminderContext, +): Promise { + if (context.state.pendingCommandIoReminders.length === 0) { + return null; + } + + const queued = context.state.pendingCommandIoReminders.splice(0); + const recent = queued.slice(-MAX_COMMAND_REMINDERS_PER_TURN); + const dropped = queued.length - recent.length; + + const commandBlocks = recent.map((entry) => { + const status = entry.success ? "success" : "error"; + const safeInput = escapeXml(truncate(entry.input, MAX_COMMAND_INPUT_CHARS)); + const safeOutput = escapeXml( + truncate(entry.output || "(no output)", MAX_COMMAND_OUTPUT_CHARS), + ); + return ` +${safeInput} +${safeOutput} +${status} +`; + }); + + const droppedLine = + dropped > 0 ? `\nOmitted ${dropped} older command event(s).` : ""; + + return `${SYSTEM_REMINDER_OPEN} +The following slash commands were executed in the Letta Code harness since your last user message. +Treat these as execution context from the CLI, not new user requests.${droppedLine} +${commandBlocks.join("\n")} +${SYSTEM_REMINDER_CLOSE} + +`; +} + +async function buildToolsetChangeReminder( + context: SharedReminderContext, +): Promise { + if (context.state.pendingToolsetChangeReminders.length === 0) { + return null; + } + + const queued = context.state.pendingToolsetChangeReminders.splice(0); + const recent = queued.slice(-MAX_TOOLSET_REMINDERS_PER_TURN); + const dropped = queued.length - recent.length; + + const changeBlocks = recent.map((entry) => { + const source = escapeXml(entry.source); + const previousToolset = escapeXml(entry.previousToolset ?? "unknown"); + const newToolset = escapeXml(entry.newToolset ?? "unknown"); + const previousTools = escapeXml(formatToolList(entry.previousTools)); + const newTools = escapeXml(formatToolList(entry.newTools)); + return ` +${source} +${previousToolset} +${newToolset} +${previousTools} +${newTools} +`; + }); + + const droppedLine = + dropped > 0 ? `\nOmitted ${dropped} older toolset change event(s).` : ""; + + return `${SYSTEM_REMINDER_OPEN} +The user just changed your toolset (specifically, client-side tools that are attached to the Letta Code harness, which may be a subset of your total tools).${droppedLine} +${changeBlocks.join("\n")} +${SYSTEM_REMINDER_CLOSE} + +`; +} + export const sharedReminderProviders: Record< SharedReminderId, SharedReminderProvider @@ -252,6 +353,8 @@ export const sharedReminderProviders: Record< "plan-mode": buildPlanModeReminder, "reflection-step-count": buildReflectionStepReminder, "reflection-compaction": buildReflectionCompactionReminder, + "command-io": buildCommandIoReminder, + "toolset-change": buildToolsetChangeReminder, }; export function assertSharedReminderCoverage(): void { diff --git a/src/reminders/state.ts b/src/reminders/state.ts index d80fa5d..f3eb444 100644 --- a/src/reminders/state.ts +++ b/src/reminders/state.ts @@ -1,6 +1,22 @@ import type { ContextTracker } from "../cli/helpers/contextTracker"; import type { PermissionMode } from "../permissions/mode"; +const MAX_PENDING_INTERACTION_REMINDERS = 25; + +export interface CommandIoReminder { + input: string; + output: string; + success: boolean; +} + +export interface ToolsetChangeReminder { + source: string; + previousToolset: string | null; + newToolset: string | null; + previousTools: string[]; + newTools: string[]; +} + export interface SharedReminderState { hasSentSessionContext: boolean; hasInjectedSkillsReminder: boolean; @@ -10,6 +26,8 @@ export interface SharedReminderState { turnCount: number; pendingSkillsReinject: boolean; pendingReflectionTrigger: boolean; + pendingCommandIoReminders: CommandIoReminder[]; + pendingToolsetChangeReminders: ToolsetChangeReminder[]; } export function createSharedReminderState(): SharedReminderState { @@ -22,6 +40,8 @@ export function createSharedReminderState(): SharedReminderState { turnCount: 0, pendingSkillsReinject: false, pendingReflectionTrigger: false, + pendingCommandIoReminders: [], + pendingToolsetChangeReminders: [], }; } @@ -42,3 +62,25 @@ export function syncReminderStateFromContextTracker( contextTracker.pendingReflectionTrigger = false; } } + +function pushBounded(items: T[], entry: T): void { + items.push(entry); + if (items.length <= MAX_PENDING_INTERACTION_REMINDERS) { + return; + } + items.splice(0, items.length - MAX_PENDING_INTERACTION_REMINDERS); +} + +export function enqueueCommandIoReminder( + state: SharedReminderState, + reminder: CommandIoReminder, +): void { + pushBounded(state.pendingCommandIoReminders, reminder); +} + +export function enqueueToolsetChangeReminder( + state: SharedReminderState, + reminder: ToolsetChangeReminder, +): void { + pushBounded(state.pendingToolsetChangeReminders, reminder); +} diff --git a/src/tests/cli/commandRunner.test.ts b/src/tests/cli/commandRunner.test.ts index fb13cb2..fa77e07 100644 --- a/src/tests/cli/commandRunner.test.ts +++ b/src/tests/cli/commandRunner.test.ts @@ -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", () => { diff --git a/src/tests/cli/interaction-reminder-wiring.test.ts b/src/tests/cli/interaction-reminder-wiring.test.ts new file mode 100644 index 0000000..814087a --- /dev/null +++ b/src/tests/cli/interaction-reminder-wiring.test.ts @@ -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", + ); + }); +}); diff --git a/src/tests/reminders/catalog.test.ts b/src/tests/reminders/catalog.test.ts index 00e3d04..458ef6e 100644 --- a/src/tests/reminders/catalog.test.ts +++ b/src/tests/reminders/catalog.test.ts @@ -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(), diff --git a/src/tests/reminders/engine-parity.test.ts b/src/tests/reminders/engine-parity.test.ts index 4f32aeb..0c66f04 100644 --- a/src/tests/reminders/engine-parity.test.ts +++ b/src/tests/reminders/engine-parity.test.ts @@ -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"), ); }); }); diff --git a/src/tests/reminders/interaction-reminders.test.ts b/src/tests/reminders/interaction-reminders.test.ts new file mode 100644 index 0000000..3947bd9 --- /dev/null +++ b/src/tests/reminders/interaction-reminders.test.ts @@ -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 ""', + output: "Models dialog dismissed ", + success: true, + }); + + const reminder = await sharedReminderProviders["command-io"]( + baseContext(state), + ); + expect(reminder).toContain(""); + expect(reminder).toContain("<unsafe>"); + expect(reminder).toContain(""); + expect(reminder).toContain("<ok>"); + 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("/toolset"); + expect(reminder).toContain("default"); + expect(reminder).toContain("codex"); + expect(reminder).toContain("Read, Write"); + expect(reminder).toContain( + "ReadFile, ApplyPatch, ShellCommand", + ); + 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(); + }); +});