Skip to content

Commit 875736f

Browse files
Cycle reasoning effort with Shift+Tab and show it on the prompt (#484)
* Cycle reasoning effort with Shift+Tab and show it on the prompt Shift+Tab walks the current model's supported effort ladder (wrapping), rebuilds inference sources so the next turn picks it up, and refreshes the prompt border label. Plain Tab still toggles focus. Docs no longer claim Shift+Tab toggles auto mode. * Document the Shift+Tab reasoning-effort cycle PRODUCT and TUI specs now name the binding; implementation notes how the runner rebuilds sources and the prompt-border label.
1 parent 7a189ff commit 875736f

12 files changed

Lines changed: 130 additions & 10 deletions

File tree

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ The chat director adds context management on top of the reactor:
7777

7878
## Permissions and auto mode
7979

80-
Corbits Code defaults to **auto mode** (`auto = true`). Workspace file writes/edits/deletes and unconstrained shell commands run without per-action prompts. Pass `--no-auto` to start in ask-on-every-consequential-action mode, or press **SHIFT+TAB** in the TUI to toggle at any time. Enabling auto prints a one-line reminder of the envelope below.
80+
Corbits Code defaults to **auto mode** (`auto = true`). Workspace file writes/edits/deletes and unconstrained shell commands run without per-action prompts. Pass `--no-auto` to start in ask-on-every-consequential-action mode (there is currently no in-session key to toggle auto). Press **Shift+Tab** in the TUI to cycle reasoning effort for the current model. Enabling auto prints a one-line reminder of the envelope below.
8181

8282
### What auto allows
8383

docs/IMPLEMENTATION.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,10 @@ When auto is on, the gate auto-allows workspace file tools in `AUTO_ALLOWED_TOOL
154154

155155
Unmatched shell auto-allows. Writes under the session state root (`~/.corbits/projects/<project-key>/…`, and legacy in-repo `.agent-state` during dual-read), mutating MCP, and unknown built-ins still prompt. Authorization hard-denies (catastrophic commands, open-ended shell search) remain independent of auto mode.
156156

157+
### Reasoning Effort
158+
159+
**Shift+Tab** in the TUI cycles reasoning effort for the live model (`cycleReasoningEffort` in `src/provider/reasoning-effort.ts`); the runner rebuilds inference sources and the prompt-border `profile · model · effort` label so the next turn picks it up. Plain Tab still toggles focus.
160+
157161
### Interrupt and Queue Steering
158162

159163
`ChatInputProps` carries `isProcessing?: boolean` and `onInterrupt?: (message: string) => void`. When `isProcessing` is true:

docs/PRODUCT.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ The evidence is in how the product fails today: the personas already produce exc
4848
$ corbits "Add JWT auth to the API"
4949
```
5050

51-
A full-screen terminal interface: a pinned header (session title and workflow progress), a scrollable event log, modals for permission prompts and operator questions, and a chat input for follow-up turns.
51+
A full-screen terminal interface: a pinned header (session title and workflow progress), a scrollable event log, modals for permission prompts and operator questions, and a chat input for follow-up turns. Press **Shift+Tab** to cycle reasoning effort for the current model; the prompt border shows the active level. Plain **Tab** still toggles focus between the prompt and the transcript.
5252

5353
**Behavior spec** (OpenTUI is the shipping shell): `docs/TUI.md` — layout,
5454
chrome budget, overlays, selectors, the `/` command list, prompt box, and

docs/TUI.md

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,8 @@ the pad is part of the bubble itself, not an extra turn-boundary gap, and
6161
assistant/tool rows are unchanged.
6262

6363
The prompt box's border carries the metadata that would otherwise cost a
64-
titlebar row: the model label sits right-aligned in the top rule; the brand
64+
titlebar row: the model label sits right-aligned in the top rule as
65+
`profile · model · effort` (empty segments omitted); the brand
6566
lockup sits at the left of the bottom rule with the working directory and git
6667
branch at its right (`AppShell.promptTopRule` / `promptBottomRule`,
6768
`src/tui/shell.ts`). Both rules cost zero transcript rows because they
@@ -601,6 +602,10 @@ holding the current scroll lease responds to them.
601602

602603
`Ctrl+G` (the Emacs/readline "abort" chord) cancels the most recently queued
603604
mid-run message. `Tab` toggles focus between the prompt and the transcript.
605+
`Shift+Tab` cycles reasoning effort for the current model (wrapping the
606+
supported ladder) and flashes the new level; the prompt-border effort
607+
segment updates immediately. A model with no effort levels flashes instead
608+
of mutating the session. Unshifted `Tab` still toggles focus.
604609
`e` (with Alt/Option) expands a collapsed row — a collapsed permission
605610
payload while an overlay owns focus, or a collapsed transcript row (tool
606611
output, a long diff) while the transcript does — one expand idiom shared

src/config/index.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -468,7 +468,8 @@ export async function loadConfig(
468468
// writes/edits and unconstrained shell) run without prompting, while shell
469469
// file-mutation stays denied and installs / recursive rm / worktree /
470470
// sensitive-path / opaque-wrapper shell still ask. Pass --no-auto to revert
471-
// to ask-on-every-write, or toggle live in the TUI with SHIFT+TAB.
471+
// to ask-on-every-write. There is currently no in-session key to toggle auto;
472+
// Shift+Tab in the TUI cycles reasoning effort instead.
472473
let auto = true;
473474
let configPath: string | undefined;
474475
let provider: string | undefined;

src/permission/gate.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -253,8 +253,9 @@ export type PermissionGate = {
253253
// Whether auto mode is currently on. Auto mode auto-approves non-destructive
254254
// consequential actions (file writes/edits, unconstrained shell) without prompting.
255255
getAuto: () => boolean;
256-
// Turn auto mode on or off for the rest of the session. SHIFT+TAB in the TUI
257-
// wires the toggle here so a switch takes effect on the next tool call.
256+
// Turn auto mode on or off for the rest of the session. Live callers (slash
257+
// commands, settings) wire the toggle here so a switch takes effect on the
258+
// next tool call. There is currently no in-session key chord for this.
258259
setAuto: (value: boolean) => void;
259260
// Whether --dangerously-skip-permissions is active for this session. Immutable
260261
// after gate construction; pre-gate sandboxes (path-escape, shell cwd bounds)

src/provider/reasoning-effort.test.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {
55
isReasoningEffort,
66
supportedEfforts,
77
validateEffort,
8+
cycleReasoningEffort,
89
setModelReasoningCapabilities,
910
modelReasoningCapability,
1011
clampEffort,
@@ -98,6 +99,27 @@ describe("validateEffort", () => {
9899
});
99100
});
100101

102+
describe("cycleReasoningEffort", () => {
103+
afterEach(() => setModelReasoningCapabilities({}));
104+
105+
test("walks the gpt-5 ladder and wraps", () => {
106+
expect(cycleReasoningEffort("gpt-5", undefined)).toBe("minimal");
107+
expect(cycleReasoningEffort("gpt-5", "minimal")).toBe("low");
108+
expect(cycleReasoningEffort("gpt-5", "low")).toBe("medium");
109+
expect(cycleReasoningEffort("gpt-5", "medium")).toBe("high");
110+
expect(cycleReasoningEffort("gpt-5", "high")).toBe("minimal");
111+
});
112+
113+
test("starts at the first supported level when current is unsupported", () => {
114+
expect(cycleReasoningEffort("gpt-5", "xhigh")).toBe("minimal");
115+
});
116+
117+
test("returns undefined for a non-reasoning model", () => {
118+
setModelReasoningCapabilities({ "chat-only-model": false });
119+
expect(cycleReasoningEffort("chat-only-model", "medium")).toBeUndefined();
120+
});
121+
});
122+
101123
describe("reasoning capability gate", () => {
102124
afterEach(() => setModelReasoningCapabilities({}));
103125

src/provider/reasoning-effort.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,25 @@ export function validateEffort(
9696
};
9797
}
9898

99+
/**
100+
* Next effort on the model's supported ladder (wraps around). Returns undefined
101+
* when the model supports no reasoning effort — callers flash a status and leave
102+
* the session config alone.
103+
*/
104+
export function cycleReasoningEffort(
105+
model: string,
106+
current: ReasoningEffort | undefined,
107+
isCodex = false,
108+
): ReasoningEffort | undefined {
109+
const supported = supportedEfforts(model, undefined, isCodex);
110+
if (supported.length === 0) return undefined;
111+
if (current === undefined || !supported.includes(current)) {
112+
return supported[0];
113+
}
114+
const idx = supported.indexOf(current);
115+
return supported[(idx + 1) % supported.length];
116+
}
117+
99118
// ---------------------------------------------------------------------------
100119
// Role-based product defaults (CL-5162)
101120
//

src/tui/keybindings.test.ts

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ import {
3737
setSentMessageHistory,
3838
setShellBridgeHooks,
3939
setShellExitHandler,
40+
setEffortCycleHandler,
4041
clearShellBridgeHooks,
4142
setShellRunState,
4243
shellFocusPrompt,
@@ -98,7 +99,7 @@ function chordsOf(keys: string): readonly (string | null)[] {
9899
const bytes = chordBytes(token.trim())
99100
// A token nothing can encode and that is not the known kitty-only chord is
100101
// a typo in the catalog, not an untestable chord.
101-
if (bytes === null && token.trim() !== "Ctrl+Enter") {
102+
if (bytes === null && token.trim() !== "Ctrl+Enter" && token.trim() !== "Shift+Tab") {
102103
throw new Error(`catalog row "${keys}" has unreadable chord "${token.trim()}"`)
103104
}
104105
return bytes
@@ -371,6 +372,22 @@ const PROBES: Readonly<Record<string, { readonly group: Group; readonly probe: P
371372
expect(focusOwner(shell.focus)).toBe("prompt")
372373
},
373374
},
375+
"Shift+Tab": {
376+
group: "surfaces",
377+
probe: ({ h, shell }) => {
378+
let cycles = 0
379+
setEffortCycleHandler(shell, () => {
380+
cycles++
381+
})
382+
shellFocusPrompt(shell)
383+
const before = focusOwner(shell.focus)
384+
// Classic terminals often emit CSI Z for Shift+Tab; the harness can also
385+
// inject name:"tab" with shift:true, which is what the shell handler reads.
386+
h.pressKey("Tab", { shift: true })
387+
expect(cycles).toBe(1)
388+
expect(focusOwner(shell.focus)).toBe(before)
389+
},
390+
},
374391
Esc: {
375392
group: "surfaces",
376393
probe: async ({ h, shell, chords }) => {

src/tui/keybindings.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ export const SHELL_SHORTCUTS: readonly ShellShortcut[] = [
3030
{ keys: "Alt+T", description: "show or hide the task list above the prompt" },
3131
{ keys: "Alt+O", description: "observe a live subagent session; a system row says so when there is none" },
3232
{ keys: "Tab", description: "move focus between the prompt and the transcript" },
33+
{ keys: "Shift+Tab", description: "cycle reasoning effort for the current model" },
3334
{ keys: "Esc", description: "close the open overlay, or leave subagent observe" },
3435
{ keys: "Ctrl+B / Ctrl+F", description: "move the cursor back / forward one character" },
3536
{ keys: "Ctrl+D", description: "delete the character under the cursor" },

0 commit comments

Comments
 (0)