fix(tui): applied follow-up slash commands
Route follow-up shortcut submissions through the builtin slash-command dispatcher before queueing them as deferred prompts. Added regression coverage for /goal set submitted through InputController.handleFollowUp while a stream is active. Fixes #2038
This commit is contained in:
@@ -2,6 +2,10 @@
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed follow-up shortcut submission of builtin slash commands so `/goal set ...` applies goal mode instead of queueing as plain text.
|
||||
|
||||
## [15.10.1] - 2026-06-07
|
||||
|
||||
### Added
|
||||
|
||||
@@ -589,7 +589,7 @@ export class InputController {
|
||||
|
||||
/** Send editor text as a follow-up message (queued behind current stream). */
|
||||
async handleFollowUp(): Promise<void> {
|
||||
const text = this.ctx.editor.getText().trim();
|
||||
let text = this.ctx.editor.getText().trim();
|
||||
if (!text) return;
|
||||
|
||||
// Compaction first: while compacting, free text gets queued via
|
||||
@@ -603,6 +603,16 @@ export class InputController {
|
||||
return;
|
||||
}
|
||||
|
||||
const slashResult = await executeBuiltinSlashCommand(text, {
|
||||
ctx: this.ctx,
|
||||
});
|
||||
if (slashResult === true) {
|
||||
return;
|
||||
}
|
||||
if (typeof slashResult === "string") {
|
||||
text = slashResult;
|
||||
}
|
||||
|
||||
// Skill commands invoke through the custom-message path regardless of
|
||||
// which keybinding submitted them. Enter routes them as `steer`;
|
||||
// Ctrl+Enter (this handler) routes them as `followUp`.
|
||||
|
||||
@@ -71,6 +71,8 @@ function createStubInputControllerContext(opts: { skillCommands: Map<string, str
|
||||
// Annotate parameters so `mock.calls[N]` is typed as a tuple (not `[]`) and
|
||||
// `message` carries required skill prompt details for assertion below.
|
||||
const promptCustomMessage = vi.fn(async (_message: { details: SkillPromptDetails }, _options?: unknown) => {});
|
||||
const prompt = vi.fn(async (_text: string, _options?: unknown) => {});
|
||||
const handleGoalModeCommand = vi.fn(async (_rest?: string) => {});
|
||||
const updatePendingMessagesDisplay = vi.fn();
|
||||
const requestRender = vi.fn();
|
||||
const showError = vi.fn();
|
||||
@@ -86,9 +88,12 @@ function createStubInputControllerContext(opts: { skillCommands: Map<string, str
|
||||
isEvalRunning: false,
|
||||
extensionRunner: undefined,
|
||||
enqueueCustomMessageDisplay,
|
||||
prompt,
|
||||
promptCustomMessage,
|
||||
},
|
||||
showError,
|
||||
handleGoalModeCommand,
|
||||
goalModeEnabled: false,
|
||||
updatePendingMessagesDisplay,
|
||||
// Defaults that InputController touches on submit but don't matter here.
|
||||
isBashMode: false,
|
||||
@@ -101,7 +106,7 @@ function createStubInputControllerContext(opts: { skillCommands: Map<string, str
|
||||
withLocalSubmission: async (_text: string, fn: () => unknown) => fn(),
|
||||
} as unknown as InteractiveModeContext;
|
||||
|
||||
return { ctx, editor, enqueueCustomMessageDisplay, promptCustomMessage };
|
||||
return { ctx, editor, enqueueCustomMessageDisplay, prompt, promptCustomMessage, handleGoalModeCommand };
|
||||
}
|
||||
|
||||
describe("InputController #invokeSkillCommand (E1-E3)", () => {
|
||||
@@ -166,6 +171,21 @@ describe("InputController #invokeSkillCommand (E1-E3)", () => {
|
||||
expect(messageArg.details.__pendingDisplayTag).toBe("sk-test-0");
|
||||
});
|
||||
|
||||
it("E2b: streaming follow-up applies builtin slash commands instead of queueing them", async () => {
|
||||
const { ctx, editor, prompt, handleGoalModeCommand } = createStubInputControllerContext({
|
||||
skillCommands,
|
||||
isStreaming: true,
|
||||
});
|
||||
|
||||
const controller = new InputController(ctx);
|
||||
editor.setText("/goal set Ship the release");
|
||||
await controller.handleFollowUp();
|
||||
|
||||
expect(handleGoalModeCommand).toHaveBeenCalledWith("set Ship the release");
|
||||
expect(prompt).not.toHaveBeenCalled();
|
||||
expect(editor.getText()).toBe("");
|
||||
});
|
||||
|
||||
it("E3: not streaming -> enqueueCustomMessageDisplay NOT called and tag absent", async () => {
|
||||
const { ctx, editor, enqueueCustomMessageDisplay, promptCustomMessage } = createStubInputControllerContext({
|
||||
skillCommands,
|
||||
|
||||
Reference in New Issue
Block a user