diff --git a/README.md b/README.md index a3a826b..ab8a7b7 100644 --- a/README.md +++ b/README.md @@ -140,8 +140,8 @@ multi-line editor; slash-command, unified `@` workspace/Plugin references and session events; `/mode`, `/model`, `/resume`, `/plugins` and other upstream slash commands; searchable model and reasoning-effort selectors, plus MCP and workflow panels; status-bar-only Shift+Tab mode cycling -(`build` → `edit` → `guarded` → `yolo`), independent `/plan` toggling with an input-border indicator, Ctrl+N model and empty-prompt Tab effort -cycling; structured session-goal status in the right side of the turn footer; +(`build` → `edit` → `guarded` → `yolo`), independent `/plan` toggling with an input-border indicator, Ctrl+N model and Ctrl+T effort +cycling (Tab also cycles effort on an empty prompt); structured session-goal status in the right side of the turn footer; animated active-turn timer with a static `ZCODE_TUI_REDUCED_MOTION=1` fallback; responsive context-remaining and session-token metrics. diff --git a/packages/zcode-tui/src/index.ts b/packages/zcode-tui/src/index.ts index 71c6db8..8413885 100644 --- a/packages/zcode-tui/src/index.ts +++ b/packages/zcode-tui/src/index.ts @@ -1542,6 +1542,10 @@ class ZCodeTui { void this.switchModel(); return { consume: true }; } + if (matchesKey(data, "ctrl+t")) { + void this.switchEffort(); + return { consume: true }; + } if (matchesKey(data, "tab") && (this.turnAbortController || this.inputQueue.paused) && Boolean(this.editor.getText().trim()) diff --git a/test/tui/fixtures/input-actions.ts b/test/tui/fixtures/input-actions.ts index d3c66c3..d3b5b43 100644 --- a/test/tui/fixtures/input-actions.ts +++ b/test/tui/fixtures/input-actions.ts @@ -3,8 +3,11 @@ let history = [ { role: "user", messageId: "question-1", content: "Original question" }, { role: "agent", messageId: "answer-1", content: "Original answer" } ]; +let effort = "low"; await runTui({ initialModel: "scenario/model", workspaceDirectory: process.cwd(), + initialThoughtLevel: effort, + effortOptions: [{ id: "low", label: "Low" }, { id: "high", label: "High" }], loadSessionTranscript: async () => history, readClipboardImage: async () => ({ dataUrl: "data:image/png;base64,aW1hZ2U=", mediaType: "image/png" }), submitPrompt: async (input) => { @@ -19,6 +22,10 @@ await runTui({ history = []; return { response: "Rewound", resetSessionProjection: true, restoredMessages: [] }; } + if (String(input).startsWith("/effort ")) { + effort = String(input).slice("/effort ".length); + return { response: `Reasoning effort switched to ${effort}.`, thoughtLevel: effort }; + } return { response: `Sent: ${String(input)}` }; } }); diff --git a/test/tui/input-actions.test.ts b/test/tui/input-actions.test.ts index de32614..a2cf426 100644 --- a/test/tui/input-actions.test.ts +++ b/test/tui/input-actions.test.ts @@ -135,3 +135,23 @@ test("fragmented terminal palette replies do not enter the draft or trigger inpu await session.sendAndWait(" tail\r", "unaltered draft submitted", /Sent: retained draft tail/u); await session.exit(); }, 20_000); + +test("Ctrl+T cycles reasoning effort without clearing a typed prompt", async () => { + await using workspace = await ScenarioWorkspace.create({ prefix: "zcode-effort-shortcut-" }); + await using session = TerminalSession.start({ + command: [process.execPath, join(import.meta.dir, "fixtures/input-actions.ts")], + workspace + }); + await session.waitForScreen("ready", /Original answer/u); + session.send("Draft question"); + await session.waitForScreen("draft typed", /Draft question/u); + session.send("\x14"); + await session.waitForScreen("effort raised", /⚡ high/u); + await session.waitForScreen("draft preserved after effort raised", /Draft question/u); + session.send("\x14"); + await session.waitForScreen("effort lowered", /⚡ low/u); + await session.waitForScreen("draft preserved after effort lowered", /Draft question/u); + await session.assertScreenExcludes("effort shortcut does not submit the draft", /Sent: Draft question/u); + await session.sendAndWait("\r", "draft submitted", /Sent: Draft question/u); + await session.exit(); +}, 20_000);