Skip to content

feat: per-axis Cursor model-state selector (effort/thinking/fast/context)#67

Open
justin-carper wants to merge 13 commits into
mainfrom
cursor-model-states
Open

feat: per-axis Cursor model-state selector (effort/thinking/fast/context)#67
justin-carper wants to merge 13 commits into
mainfrom
cursor-model-states

Conversation

@justin-carper

Copy link
Copy Markdown
Collaborator

What

Adds a per-axis Cursor model-state selector to the plugin. Cursor models expose several independent state axes — effort/reasoning level, thinking on/off, fast on/off, context window — which the catalog pre-enumerates as a full cartesian variants[] (e.g. claude-opus-4-8 has 40 variants). A flat 40-entry cycler is unusable, so this feature lets the user cycle each axis independently via hotkeys, with a live status widget and per-session persistence.

Why

The plugin previously surfaced only a subset of states (effort + fast) folded into one cycler. Thinking, context, Max-mode (effort max), and per-axis composition were unreachable. This delivers the full state matrix through an efficient per-axis UI instead of an unusable flat list.

How

Two-target plugin package:

  • ./server (existing Hooks) — unchanged behavior. chat.params gains an additive read+merge of the composed selection.
  • ./tui (new TuiPlugin) — registers per-axis cycle hotkeys, a live status widget in the session_prompt_right slot, and persists the composed selection.

Pure, unit-tested core:

  • src/model-axes.tsbuildModelAxes derives independent axes from item.variants[]; isValidCombo / snapCombo handle asymmetric models (e.g. gpt-5.5 only offers fast at context=272k).
  • src/tui/axis-state.tsseedSelection, cycleAxis (immutable, wraps), formatSelection, reconcileSelection.

Cross-half bridge: opencode exposes no client API for persistent session model options and no cross-half KV endpoint, so the two plugin halves share state through a file both compute identically: src/tui/state-file.ts writes <cacheDir>/cursor-states.json keyed by sessionID (atomic temp+rename); the server chat.params hook reads it and merges string-only params into output.options.params. src/provider/controls.ts (the Cursor wire-shaping path) is unchanged.

Default hotkeys (overridable in tui.json): ctrl+e effort/reasoning, ctrl+t thinking, ctrl+f fast, ctrl+k context; shift+<key> = previous. A toast shows each new value; the widget shows the composite (high · think · fast · 1m).

--pure / no-TUI fallback: buildModelVariants still feeds opencode's native variant_cycle, now re-sourced from axes as a curated, non-cartesian short list (one entry per non-default axis value — never 40).

Requirements

  • opencode >= 1.17.0 (engines.opencode) — first version shipping the TUI plugin API.
  • @opentui/core, @opentui/solid, solid-js are optional peer dependencies (host-provided at runtime, external in the build) so server-only / --pure installs are unaffected.

Testing

  • npm test324 passing across 30 files (new model-axes, axis-state, state-file suites; updated model-variants, model-v2, model-discovery).
  • npm run build and npm run typecheck clean.
  • Pure logic is tested against a committed fixture of the live 30-model catalog (test/fixtures/cursor-catalog.json), covering the 40-variant, asymmetric, toggle-only, and no-axis shapes.

Manual smoke (requires a real opencode >= 1.17.0 runtime — not automatable here)

  1. Build, npm pack, install the tarball's ./tui in a scratch tui.json plugin array and the ./server half as the provider plugin.
  2. Open a session with cursor/claude-opus-4-8 and send one message (an assistant message is how the TUI half detects the active Cursor model).
  3. ctrl+e → toast shows Effort: <next>, widget updates. shift+ctrl+e reverses.
  4. ctrl+t / ctrl+f / ctrl+k → thinking / fast / context change; widget reflects the composite.
  5. Switch to cursor/gemini-3.1-pro → no axes; hotkeys inert; widget shows model name only.
  6. Confirm <cacheDir>/cursor-states.json contains { "<sessionID>": { ...selection } }.
  7. Send another message; confirm (server logs) the Cursor request carries the composed params.

Known v1 limitations

  • One global in-memory selection. The persisted state file is per-session-keyed, but the in-memory selection + widget are global, so two concurrently-rendered session prompts would display one shared selection. Fine for a single active prompt; revisit for multi-prompt.
  • Native --pure fallback + default-ON toggles. A toggle that defaults ON (e.g. composer-2.5's fast) produces no curated fallback entry for that axis. The TUI cycler covers the full range; only the deprioritized --pure list is affected.
  • Axis reflects the last Cursor assistant model in the session (there is no model-selection event to hook).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant