fix: store slash commands in input history (#3148)
Previously only 4 commands (/plan, /goal, /mcp, /ssh) stored their text in history via per-handler addToHistory calls. All other built-in slash commands were silently skipped because executeBuiltinSlashCommand returned true before the input controller's addToHistory was reached. - Centralize history recording in the input controller after successful slash command dispatch, for both Enter and Ctrl+Enter submit paths. - Remove all 10 per-command addToHistory calls from slash command handlers to prevent duplicates. - Add shouldSkipHistory() security filter to exclude commands that may carry secrets: /login <url> (OAuth callback with code=/state= params) and /mcp add --token <token> (bearer token). - Add regression tests for the security filter (8 cases). - Update 7 existing test files to remove handler-level addToHistory assertions (now the input controller's responsibility).
This commit is contained in:
@@ -27,6 +27,27 @@ import { ensureSupportedImageInput, ImageInputTooLargeError, loadImageInput } fr
|
||||
import { resizeImage } from "../../utils/image-resize";
|
||||
import { generateSessionTitle, setSessionTerminalTitle } from "../../utils/title-generator";
|
||||
|
||||
/**
|
||||
* Slash commands that may carry secrets in their arguments should never be
|
||||
* persisted to history. /login <url> receives an OAuth callback URL containing
|
||||
* code= and state= params. /mcp add --token <token> receives a bearer token.
|
||||
*/
|
||||
export function shouldSkipHistory(slashText: string): boolean {
|
||||
if (!slashText.startsWith("/")) return false;
|
||||
const name = slashText.slice(1).split(/\s+/, 1)[0];
|
||||
// /login <url> — the redirect URL carries OAuth code= and state= params.
|
||||
// /login <provider> (e.g. "anthropic") is safe — it just opens the selector.
|
||||
if (name === "login" && slashText.length > "/login".length) {
|
||||
const arg = slashText.slice("/login".length).trim();
|
||||
return arg.includes("://") || arg.startsWith("http");
|
||||
}
|
||||
if (name === "mcp") {
|
||||
const args = slashText.slice("/mcp".length).trim();
|
||||
return args.startsWith("add") && /--token\s/.test(args);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
interface Expandable {
|
||||
setExpanded(expanded: boolean): void;
|
||||
}
|
||||
@@ -572,6 +593,7 @@ export class InputController {
|
||||
ctx: this.ctx,
|
||||
});
|
||||
if (slashResult === true) {
|
||||
if (!shouldSkipHistory(text)) this.ctx.editor.addToHistory(text);
|
||||
return;
|
||||
}
|
||||
if (typeof slashResult === "string") {
|
||||
@@ -1011,6 +1033,7 @@ export class InputController {
|
||||
ctx: this.ctx,
|
||||
});
|
||||
if (slashResult === true) {
|
||||
if (!shouldSkipHistory(text)) this.ctx.editor.addToHistory(text);
|
||||
return;
|
||||
}
|
||||
if (typeof slashResult === "string") {
|
||||
|
||||
Reference in New Issue
Block a user