feat: make reasoning tab cycling opt-in (default off) (#1175)

This commit is contained in:
Devansh Jain
2026-02-26 15:42:52 -08:00
committed by GitHub
parent d184bd8aef
commit 6967f9dcc9
7 changed files with 203 additions and 1 deletions

View File

@@ -231,6 +231,7 @@ import {
buildQueuedUserText,
getQueuedNotificationSummaries,
} from "./helpers/queuedMessageParts";
import { resolveReasoningTabToggleCommand } from "./helpers/reasoningTabToggle";
import { safeJsonParseOr } from "./helpers/safeJsonParse";
import { getDeviceType, getLocalTime } from "./helpers/sessionContext";
import {
@@ -479,6 +480,7 @@ const NON_STATE_COMMANDS = new Set([
"/export",
"/download",
"/statusline",
"/reasoning-tab",
]);
// Check if a command is interactive (opens overlay, should not be queued)
@@ -925,6 +927,7 @@ export default function App({
messageHistory = [],
resumedExistingConversation = false,
tokenStreaming = false,
reasoningTabCycleEnabled: initialReasoningTabCycleEnabled = false,
showCompactions = false,
agentProvenance = null,
releaseNotes = null,
@@ -945,6 +948,7 @@ export default function App({
messageHistory?: Message[];
resumedExistingConversation?: boolean; // True if we explicitly resumed via --resume
tokenStreaming?: boolean;
reasoningTabCycleEnabled?: boolean;
showCompactions?: boolean;
agentProvenance?: AgentProvenance | null;
releaseNotes?: string | null; // Markdown release notes to display above header
@@ -1484,6 +1488,11 @@ export default function App({
const [tokenStreamingEnabled, setTokenStreamingEnabled] =
useState(tokenStreaming);
// Reasoning tier Tab cycling preference (opt-in only, persisted globally)
const [reasoningTabCycleEnabled, setReasoningTabCycleEnabled] = useState(
initialReasoningTabCycleEnabled,
);
// Show compaction messages preference (can be toggled at runtime)
const [showCompactionsEnabled, _setShowCompactionsEnabled] =
useState(showCompactions);
@@ -7348,6 +7357,52 @@ export default function App({
return { submitted: true };
}
// Special handling for /reasoning-tab command - opt-in toggle for Tab tier cycling
if (
trimmed === "/reasoning-tab" ||
trimmed.startsWith("/reasoning-tab ")
) {
const resolution = resolveReasoningTabToggleCommand(
trimmed,
reasoningTabCycleEnabled,
);
if (!resolution) {
return { submitted: false };
}
const cmd = commandRunner.start(
trimmed,
"Updating reasoning Tab shortcut...",
);
setCommandRunning(true);
try {
if (resolution.kind === "status") {
cmd.finish(resolution.message, true);
return { submitted: true };
}
if (resolution.kind === "invalid") {
cmd.fail(resolution.message);
return { submitted: true };
}
setReasoningTabCycleEnabled(resolution.enabled);
settingsManager.updateSettings({
reasoningTabCycleEnabled: resolution.enabled,
});
cmd.finish(resolution.message, true);
} catch (error) {
const errorDetails = formatErrorDetails(error, agentId);
cmd.fail(`Failed: ${errorDetails}`);
} finally {
setCommandRunning(false);
}
return { submitted: true };
}
// Special handling for /new command - start new conversation
if (msg.trim() === "/new") {
const cmd = commandRunner.start(
@@ -12406,7 +12461,11 @@ Plan file path: ${planFilePath}`;
}
permissionMode={uiPermissionMode}
onPermissionModeChange={handlePermissionModeChange}
onCycleReasoningEffort={handleCycleReasoningEffort}
onCycleReasoningEffort={
reasoningTabCycleEnabled
? handleCycleReasoningEffort
: undefined
}
onExit={handleExit}
onInterrupt={handleInterrupt}
interruptRequested={interruptRequested}

View File

@@ -288,6 +288,15 @@ export const commands: Record<string, Command> = {
return "Managing status line...";
},
},
"/reasoning-tab": {
desc: "Toggle Tab shortcut for reasoning tiers (/reasoning-tab on|off|status)",
args: "[on|off|status]",
order: 36.6,
handler: () => {
// Handled specially in App.tsx
return "Managing reasoning Tab shortcut...";
},
},
"/terminal": {
desc: "Setup terminal shortcuts [--revert]",
order: 37,

View File

@@ -0,0 +1,47 @@
export type ReasoningTabToggleResolution =
| { kind: "status"; message: string }
| { kind: "set"; enabled: boolean; message: string }
| { kind: "invalid"; message: string };
const ENABLE_ARGS = new Set(["on", "enable", "enabled", "true", "1"]);
const DISABLE_ARGS = new Set(["off", "disable", "disabled", "false", "0"]);
const USAGE = "Usage: /reasoning-tab [on|off|status] (default is off)";
export function resolveReasoningTabToggleCommand(
trimmedInput: string,
currentlyEnabled: boolean,
): ReasoningTabToggleResolution | null {
const trimmed = trimmedInput.trim();
if (trimmed !== "/reasoning-tab" && !trimmed.startsWith("/reasoning-tab ")) {
return null;
}
const rawArg = trimmed.slice("/reasoning-tab".length).trim().toLowerCase();
if (!rawArg || rawArg === "status") {
return {
kind: "status",
message: currentlyEnabled
? "Reasoning Tab shortcut is enabled. Tab now cycles reasoning tiers."
: "Reasoning Tab shortcut is disabled. Use /reasoning-tab on to enable it.",
};
}
if (ENABLE_ARGS.has(rawArg)) {
return {
kind: "set",
enabled: true,
message: "Reasoning Tab shortcut enabled.",
};
}
if (DISABLE_ARGS.has(rawArg)) {
return {
kind: "set",
enabled: false,
message: "Reasoning Tab shortcut disabled.",
};
}
return { kind: "invalid", message: USAGE };
}

View File

@@ -2044,6 +2044,7 @@ async function main(): Promise<void> {
messageHistory: resumeData?.messageHistory ?? EMPTY_MESSAGE_ARRAY,
resumedExistingConversation,
tokenStreaming: settings.tokenStreaming,
reasoningTabCycleEnabled: settings.reasoningTabCycleEnabled === true,
showCompactions: settings.showCompactions,
agentProvenance,
releaseNotes,
@@ -2062,6 +2063,7 @@ async function main(): Promise<void> {
messageHistory: resumeData?.messageHistory ?? EMPTY_MESSAGE_ARRAY,
resumedExistingConversation,
tokenStreaming: settings.tokenStreaming,
reasoningTabCycleEnabled: settings.reasoningTabCycleEnabled === true,
showCompactions: settings.showCompactions,
agentProvenance,
releaseNotes,

View File

@@ -62,6 +62,7 @@ export interface Settings {
lastAgent: string | null; // DEPRECATED: kept for migration to lastSession
lastSession?: SessionRef; // DEPRECATED: kept for backwards compat, use sessionsByServer
tokenStreaming: boolean;
reasoningTabCycleEnabled: boolean; // Tab cycles reasoning tiers only when explicitly enabled
showCompactions?: boolean;
enableSleeptime: boolean;
sessionContextEnabled: boolean; // Send device/agent context on first message of each session
@@ -126,6 +127,7 @@ export interface LocalProjectSettings {
const DEFAULT_SETTINGS: Settings = {
lastAgent: null,
tokenStreaming: false,
reasoningTabCycleEnabled: false,
showCompactions: false,
enableSleeptime: false,
conversationSwitchAlertEnabled: false,

View File

@@ -45,4 +45,30 @@ describe("reasoning tier cycle wiring", () => {
expect(callbackBlocks.length).toBeGreaterThanOrEqual(2);
});
test("tab-based reasoning cycling is opt-in only", () => {
const appPath = fileURLToPath(
new URL("../../cli/App.tsx", import.meta.url),
);
const indexPath = fileURLToPath(new URL("../../index.ts", import.meta.url));
const settingsPath = fileURLToPath(
new URL("../../settings-manager.ts", import.meta.url),
);
const appSource = readFileSync(appPath, "utf-8");
const indexSource = readFileSync(indexPath, "utf-8");
const settingsSource = readFileSync(settingsPath, "utf-8");
expect(settingsSource).toContain("reasoningTabCycleEnabled: boolean;");
expect(settingsSource).toContain("reasoningTabCycleEnabled: false,");
expect(indexSource).toContain(
"reasoningTabCycleEnabled: settings.reasoningTabCycleEnabled === true,",
);
expect(appSource).toMatch(
/if\s*\(\s*trimmed\s*===\s*"\/reasoning-tab"\s*\|\|\s*trimmed\.startsWith\("\/reasoning-tab "\)\s*\)\s*\{/,
);
expect(appSource).toMatch(
/onCycleReasoningEffort=\{\s*reasoningTabCycleEnabled\s*\?\s*handleCycleReasoningEffort\s*:\s*undefined\s*\}/,
);
});
});

View File

@@ -0,0 +1,57 @@
import { describe, expect, test } from "bun:test";
import { resolveReasoningTabToggleCommand } from "../../cli/helpers/reasoningTabToggle";
describe("reasoning tab toggle command parsing", () => {
test("returns null for non-matching commands", () => {
expect(resolveReasoningTabToggleCommand("/model", false)).toBeNull();
});
test("status/default reports current state", () => {
expect(resolveReasoningTabToggleCommand("/reasoning-tab", false)).toEqual({
kind: "status",
message:
"Reasoning Tab shortcut is disabled. Use /reasoning-tab on to enable it.",
});
expect(
resolveReasoningTabToggleCommand("/reasoning-tab status", true),
).toEqual({
kind: "status",
message:
"Reasoning Tab shortcut is enabled. Tab now cycles reasoning tiers.",
});
});
test("accepts enable aliases", () => {
for (const arg of ["on", "enable", "enabled", "true", "1"]) {
expect(
resolveReasoningTabToggleCommand(`/reasoning-tab ${arg}`, false),
).toEqual({
kind: "set",
enabled: true,
message: "Reasoning Tab shortcut enabled.",
});
}
});
test("accepts disable aliases", () => {
for (const arg of ["off", "disable", "disabled", "false", "0"]) {
expect(
resolveReasoningTabToggleCommand(`/reasoning-tab ${arg}`, true),
).toEqual({
kind: "set",
enabled: false,
message: "Reasoning Tab shortcut disabled.",
});
}
});
test("returns usage for invalid arg", () => {
expect(
resolveReasoningTabToggleCommand("/reasoning-tab maybe", true),
).toEqual({
kind: "invalid",
message: "Usage: /reasoning-tab [on|off|status] (default is off)",
});
});
});