From 7d0652a192d039a8bf81dac262f8b3f51d44fe2d Mon Sep 17 00:00:00 2001 From: elkaix Date: Thu, 13 Aug 2026 21:28:36 -0400 Subject: [PATCH 1/2] fix(tui): persist thinking effort cycled via Ctrl-T/Shift-Tab --- .../pythinker-code/src/tui/commands/config.ts | 17 +----- .../src/tui/controllers/editor-keyboard.ts | 10 +++ .../src/tui/utils/persist-effort.ts | 27 ++++++++ .../tui/controllers/editor-keyboard.test.ts | 61 +++++++++++++++++++ 4 files changed, 100 insertions(+), 15 deletions(-) create mode 100644 apps/pythinker-code/src/tui/utils/persist-effort.ts create mode 100644 apps/pythinker-code/test/tui/controllers/editor-keyboard.test.ts diff --git a/apps/pythinker-code/src/tui/commands/config.ts b/apps/pythinker-code/src/tui/commands/config.ts index 36078248..c637f383 100644 --- a/apps/pythinker-code/src/tui/commands/config.ts +++ b/apps/pythinker-code/src/tui/commands/config.ts @@ -35,6 +35,7 @@ import { ThemeSelectorComponent } from '../components/dialogs/theme-selector'; import { UpdatePreferenceSelectorComponent } from '../components/dialogs/update-preference-selector'; import { saveTuiConfig } from '../config'; import { generateKeybindingsTemplate } from '../keybindings'; +import { persistDefaultModelSelection } from '../utils/persist-effort'; import type { ThemeName } from '#/tui/theme'; import { currentTheme, isBuiltInTheme, lightColors, loadCustomThemeMerged } from '#/tui/theme'; import { @@ -769,21 +770,7 @@ async function performModelSwitch(host: SlashCommandHost, alias: string, effort: } async function persistModelSelection(host: SlashCommandHost, alias: string, effort: string): Promise { - const defaultThinking = effort !== 'off'; - const config = await host.harness.getConfig({ reload: true }); - if ( - config.defaultModel === alias && - config.defaultThinking === defaultThinking && - config.thinking?.effort === effort - ) { - return false; - } - await host.harness.setConfig({ - defaultModel: alias, - defaultThinking, - thinking: { effort }, - }); - return true; + return persistDefaultModelSelection(host.harness, alias, effort); } // --------------------------------------------------------------------------- diff --git a/apps/pythinker-code/src/tui/controllers/editor-keyboard.ts b/apps/pythinker-code/src/tui/controllers/editor-keyboard.ts index d8c8fbfd..dbfafc7b 100644 --- a/apps/pythinker-code/src/tui/controllers/editor-keyboard.ts +++ b/apps/pythinker-code/src/tui/controllers/editor-keyboard.ts @@ -2,6 +2,7 @@ import { Editor, parseKey } from '@earendil-works/pi-tui'; import { coerceEffortForModel, effortLevelsForModel, + type PythinkerHarness, type Session, } from '@pythoughts/pythinker-code-sdk'; @@ -28,6 +29,7 @@ import { type ParsedKeybinding, } from '#/tui/keybindings'; import { isPrintableChar, printableChar } from '#/tui/utils/printable-key'; +import { persistDefaultModelSelection } from '#/tui/utils/persist-effort'; function effectiveContextBindings( bindings: readonly ParsedKeybinding[], @@ -45,6 +47,7 @@ function effectiveContextBindings( export interface EditorKeyboardHost { state: TUIState; session: Session | undefined; + readonly harness: PythinkerHarness; cancelInFlight: (() => void) | undefined; handleUserInput(text: string): void; @@ -323,6 +326,13 @@ export class EditorKeyboardController { host.track('thinking_toggle', { enabled: next !== 'off', effort: next }); // No transcript notice: the footer already shows the new level live, and // rapid cycling would stack a line per keypress in the chat history. + try { + await persistDefaultModelSelection(host.harness, alias, next); + } catch (error) { + host.showError( + `Thinking effort set to ${next}, but failed to save default: ${formatErrorMessage(error)}`, + ); + } } private cancelCurrentCompaction(): void { diff --git a/apps/pythinker-code/src/tui/utils/persist-effort.ts b/apps/pythinker-code/src/tui/utils/persist-effort.ts new file mode 100644 index 00000000..f746c609 --- /dev/null +++ b/apps/pythinker-code/src/tui/utils/persist-effort.ts @@ -0,0 +1,27 @@ +import type { PythinkerHarness } from '@pythoughts/pythinker-code-sdk'; + +/** + * Save the model + thinking-effort pair as the startup default. + * Returns false when the config already holds the same selection. + */ +export async function persistDefaultModelSelection( + harness: PythinkerHarness, + alias: string, + effort: string, +): Promise { + const defaultThinking = effort !== 'off'; + const config = await harness.getConfig({ reload: true }); + if ( + config.defaultModel === alias && + config.defaultThinking === defaultThinking && + config.thinking?.effort === effort + ) { + return false; + } + await harness.setConfig({ + defaultModel: alias, + defaultThinking, + thinking: { effort }, + }); + return true; +} diff --git a/apps/pythinker-code/test/tui/controllers/editor-keyboard.test.ts b/apps/pythinker-code/test/tui/controllers/editor-keyboard.test.ts new file mode 100644 index 00000000..a16e5c15 --- /dev/null +++ b/apps/pythinker-code/test/tui/controllers/editor-keyboard.test.ts @@ -0,0 +1,61 @@ +import { describe, expect, it, vi } from 'vitest'; + +import { + EditorKeyboardController, + type EditorKeyboardHost, +} from '#/tui/controllers/editor-keyboard'; +import type { ImageAttachmentStore } from '#/tui/utils/image-attachment-store'; + +function makeHost() { + const editor: Record = {}; + const setConfig = vi.fn(() => Promise.resolve()); + const getConfig = vi.fn(() => + Promise.resolve({ defaultModel: undefined, defaultThinking: undefined, thinking: undefined }), + ); + const host = { + state: { + editor, + ui: { addInputListener: vi.fn(() => () => {}), requestRender: vi.fn() }, + appState: { + model: 'test/model', + thinkingLevel: 'low', + availableModels: { + 'test/model': { + capabilities: ['thinking'], + supportEfforts: ['low', 'medium', 'high', 'max'], + }, + }, + }, + }, + session: { setThinking: vi.fn(() => Promise.resolve()) }, + harness: { getConfig, setConfig }, + cancelInFlight: undefined, + setAppState: vi.fn(), + track: vi.fn(), + showError: vi.fn(), + showNotice: vi.fn(), + dispatchFooter: vi.fn(), + updateEditorBorderHighlight: vi.fn(), + updateQueueDisplay: vi.fn(), + } as unknown as EditorKeyboardHost; + return { host, editor, setConfig, getConfig }; +} + +describe('EditorKeyboardController thinking-effort cycling', () => { + it('persists the cycled effort as the startup default', async () => { + const { host, editor, setConfig } = makeHost(); + const controller = new EditorKeyboardController(host, {} as unknown as ImageAttachmentStore); + controller.install(); + + const onCycleEffort = editor['onCycleEffort'] as () => void; + expect(typeof onCycleEffort).toBe('function'); + onCycleEffort(); + await vi.waitFor(() => { + expect(setConfig).toHaveBeenCalledWith({ + defaultModel: 'test/model', + defaultThinking: true, + thinking: { effort: 'medium' }, + }); + }); + }); +}); From a6159bcf260d73997899f672050a51c156126ee3 Mon Sep 17 00:00:00 2001 From: elkaix Date: Thu, 13 Aug 2026 21:29:24 -0400 Subject: [PATCH 2/2] chore: add changeset --- .changeset/persist-cycled-effort.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/persist-cycled-effort.md diff --git a/.changeset/persist-cycled-effort.md b/.changeset/persist-cycled-effort.md new file mode 100644 index 00000000..3b924c00 --- /dev/null +++ b/.changeset/persist-cycled-effort.md @@ -0,0 +1,5 @@ +--- +"@pythoughts/pythinker-code": patch +--- + +Keep the thinking effort chosen with Ctrl-T/Shift-Tab as the default across restarts.