feat: make reasoning tab cycling opt-in (default off) (#1175)
This commit is contained in:
@@ -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}
|
||||
|
||||
@@ -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,
|
||||
|
||||
47
src/cli/helpers/reasoningTabToggle.ts
Normal file
47
src/cli/helpers/reasoningTabToggle.ts
Normal 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 };
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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*\}/,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
57
src/tests/cli/reasoning-tab-toggle.test.ts
Normal file
57
src/tests/cli/reasoning-tab-toggle.test.ts
Normal 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)",
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user