feat: add context-aware ask keymaps
This commit is contained in:
@@ -36,7 +36,7 @@ The codebase is split so the implementation reads through file boundaries and na
|
||||
|
||||
- `src/config/defaults.ts` — canonical runtime defaults
|
||||
- `src/config/schema.ts` — persisted config schema and runtime type
|
||||
- `src/config/migrate.ts` — persisted-file validation boundary, including keymap normalization/fallback
|
||||
- `src/config/migrate.ts` — persisted-file validation boundary, including schema migration and context-aware keymap normalization/fallback
|
||||
- `src/config/migrations/` — ordered schema-version migration framework for persisted config shape changes
|
||||
- `src/config/path-migrations.ts` — file-location migration before config load
|
||||
- `src/config/store.ts` — load/save/backup/runtime subscription store
|
||||
@@ -44,7 +44,7 @@ The codebase is split so the implementation reads through file boundaries and na
|
||||
### UI
|
||||
|
||||
- `src/ui/controller.ts` — connects key input, editor lifecycle, live config subscription, dirty-dismiss confirmation, and pure state transitions
|
||||
- `src/ui/input.ts` — raw input to commands using resolved config-backed keymaps
|
||||
- `src/ui/input.ts` — raw input to commands using resolved context-aware config-backed keymaps
|
||||
- `src/ui/dismiss-guard.ts` — pure helpers for dirty-flow exit confirmation behavior
|
||||
- `src/ui/render.ts` and `src/ui/render-*.ts` — screen rendering, including config-backed footer/keymap hints
|
||||
- `src/ui/settings-list.ts` / `src/ui/show-settings.ts` — lightweight ask settings list rendering and launcher
|
||||
@@ -76,7 +76,7 @@ The codebase is split so the implementation reads through file boundaries and na
|
||||
- persisted ask settings are migrated to the current schema version, validated, and normalized before use
|
||||
- config schema migrations preserve user-provided values and add new defaults only when fields are absent
|
||||
- replay payload lookup scans only the current session branch and revalidates payloads before use
|
||||
- invalid persisted keymaps fall back to default keymaps for the current session without discarding valid behaviour settings
|
||||
- invalid persisted keymaps fall back to default keymaps for the current session without discarding valid behaviour, notification, or answer settings
|
||||
- invalid notification channels are skipped and fall back to the default bell channel if none are valid
|
||||
- ask settings behaviour and notification enabled changes persist immediately from the settings list
|
||||
- when the ask config file is missing, the first ask use writes a default persisted config snapshot under `~/.pi/agent/extensions/`
|
||||
|
||||
+138
-66
@@ -28,7 +28,7 @@ Unsupported future versions or invalid files are backed up and defaults are load
|
||||
|
||||
```json
|
||||
{
|
||||
"schemaVersion": 3,
|
||||
"schemaVersion": 4,
|
||||
"answer": {
|
||||
"extractionModels": [
|
||||
{ "provider": "openai-codex", "id": "gpt-5.4-mini" },
|
||||
@@ -45,12 +45,40 @@ Unsupported future versions or invalid files are backed up and defaults are load
|
||||
"showFooterHints": true
|
||||
},
|
||||
"keymaps": {
|
||||
"cancel": "esc",
|
||||
"dismiss": "ctrl+c",
|
||||
"toggle": "space",
|
||||
"confirm": "enter",
|
||||
"optionNote": "n",
|
||||
"questionNote": "shift+n"
|
||||
"global": { "dismiss": ["ctrl+c"], "settings": ["?"] },
|
||||
"main": {
|
||||
"confirm": ["enter"],
|
||||
"cancel": ["esc"],
|
||||
"toggle": ["space"],
|
||||
"nextTab": ["tab", "right"],
|
||||
"previousTab": ["shift+tab", "left"],
|
||||
"nextOption": ["down"],
|
||||
"previousOption": ["up"],
|
||||
"optionNote": ["n"],
|
||||
"questionNote": ["shift+n"]
|
||||
},
|
||||
"editor": {
|
||||
"submit": ["enter"],
|
||||
"close": ["esc"],
|
||||
"nextTabWhenEmpty": ["tab", "right"],
|
||||
"previousTabWhenEmpty": ["shift+tab", "left"],
|
||||
"nextOptionWhenEmpty": ["down"],
|
||||
"previousOptionWhenEmpty": ["up"]
|
||||
},
|
||||
"noteEditor": {
|
||||
"save": ["enter"],
|
||||
"close": ["esc"],
|
||||
"nextTabWhenEmpty": ["tab", "right"],
|
||||
"previousTabWhenEmpty": ["shift+tab", "left"],
|
||||
"nextOptionWhenEmpty": ["down"],
|
||||
"previousOptionWhenEmpty": ["up"]
|
||||
},
|
||||
"settingsModal": {
|
||||
"close": ["esc", "ctrl+c", "?"],
|
||||
"nextOption": ["down"],
|
||||
"previousOption": ["up"],
|
||||
"toggle": ["enter", "space"]
|
||||
}
|
||||
},
|
||||
"notifications": {
|
||||
"enabled": true,
|
||||
@@ -159,33 +187,69 @@ cmux example:
|
||||
|
||||
## Keymaps
|
||||
|
||||
`keymaps` must contain all 6 configurable actions if present.
|
||||
|
||||
### Configurable actions
|
||||
|
||||
- `cancel`
|
||||
- `dismiss`
|
||||
- `toggle`
|
||||
- `confirm`
|
||||
- `optionNote`
|
||||
- `questionNote`
|
||||
`keymaps` is context-aware. Each action accepts either one key id string or an array of key id strings.
|
||||
Arrays are aliases: any listed key triggers the same action.
|
||||
|
||||
### Defaults
|
||||
|
||||
- `cancel: "esc"`
|
||||
- `dismiss: "ctrl+c"`
|
||||
- `toggle: "space"`
|
||||
- `confirm: "enter"`
|
||||
- `optionNote: "n"`
|
||||
- `questionNote: "shift+n"`
|
||||
```json
|
||||
"keymaps": {
|
||||
"global": {
|
||||
"dismiss": ["ctrl+c"],
|
||||
"settings": ["?"]
|
||||
},
|
||||
"main": {
|
||||
"confirm": ["enter"],
|
||||
"cancel": ["esc"],
|
||||
"toggle": ["space"],
|
||||
"nextTab": ["tab", "right"],
|
||||
"previousTab": ["shift+tab", "left"],
|
||||
"nextOption": ["down"],
|
||||
"previousOption": ["up"],
|
||||
"optionNote": ["n"],
|
||||
"questionNote": ["shift+n"]
|
||||
},
|
||||
"editor": {
|
||||
"submit": ["enter"],
|
||||
"close": ["esc"],
|
||||
"nextTabWhenEmpty": ["tab", "right"],
|
||||
"previousTabWhenEmpty": ["shift+tab", "left"],
|
||||
"nextOptionWhenEmpty": ["down"],
|
||||
"previousOptionWhenEmpty": ["up"]
|
||||
},
|
||||
"noteEditor": {
|
||||
"save": ["enter"],
|
||||
"close": ["esc"],
|
||||
"nextTabWhenEmpty": ["tab", "right"],
|
||||
"previousTabWhenEmpty": ["shift+tab", "left"],
|
||||
"nextOptionWhenEmpty": ["down"],
|
||||
"previousOptionWhenEmpty": ["up"]
|
||||
},
|
||||
"settingsModal": {
|
||||
"close": ["esc", "ctrl+c", "?"],
|
||||
"nextOption": ["down"],
|
||||
"previousOption": ["up"],
|
||||
"toggle": ["enter", "space"]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Contexts
|
||||
|
||||
- `global`: active in the main ask flow and editors; duplicates with those contexts are invalid
|
||||
- `main`: question and review flow
|
||||
- `editor`: custom answer editor
|
||||
- `noteEditor`: question/option note editor
|
||||
- `settingsModal`: `/ask-settings` and `?` settings overlay
|
||||
|
||||
### Allowed bindings
|
||||
|
||||
Each configurable action accepts any single `pi-tui` key id string, as long as it is:
|
||||
Each action accepts any `pi-tui` key id string, as long as it is:
|
||||
|
||||
- supported by `pi-tui`
|
||||
- not reserved
|
||||
- not duplicated across configurable actions
|
||||
- not a fixed numeric shortcut (`1` through `9`)
|
||||
- not duplicated within the same context
|
||||
- not duplicated between `global` and `main`, `editor`, or `noteEditor`
|
||||
|
||||
Examples of valid bindings:
|
||||
|
||||
@@ -196,7 +260,7 @@ Examples of valid bindings:
|
||||
- `n`
|
||||
- `shift+n`
|
||||
- `alt+f7`
|
||||
- `ctrl+[`
|
||||
- `ctrl+[`
|
||||
- `ctrl+shift+p`
|
||||
- `super+k`
|
||||
|
||||
@@ -213,43 +277,19 @@ Examples:
|
||||
- `pageup` -> `pageUp`
|
||||
- `pagedown` -> `pageDown`
|
||||
|
||||
### Reserved bindings
|
||||
|
||||
These bindings are fixed and cannot be used by configurable actions:
|
||||
|
||||
- `?`
|
||||
- `tab`
|
||||
- `shift+tab`
|
||||
- `left`
|
||||
- `right`
|
||||
- `up`
|
||||
- `down`
|
||||
- `1`
|
||||
- `2`
|
||||
- `3`
|
||||
- `4`
|
||||
- `5`
|
||||
- `6`
|
||||
- `7`
|
||||
- `8`
|
||||
- `9`
|
||||
|
||||
### Fixed bindings
|
||||
|
||||
These are intentionally not configurable:
|
||||
|
||||
- `?` opens ask settings
|
||||
- `tab`, `shift+tab`, `left`, `right` move between tabs
|
||||
- `up`, `down` move between options/actions
|
||||
- `1..9` triggers option/review shortcuts
|
||||
- when `behaviour.doublePressReviewShortcuts` is enabled, review-tab shortcuts `1`, `2`, and `3` require the same key twice
|
||||
- `@` remains file-reference affordance in editors
|
||||
- `@` remains the file-reference affordance in editors
|
||||
|
||||
## Invalid keymaps behavior
|
||||
|
||||
If configured keymaps are invalid:
|
||||
|
||||
- valid `behaviour` settings still load
|
||||
- valid `behaviour`, `notifications`, and `answer` settings still load
|
||||
- invalid `keymaps` fall back to default keymaps for the current session
|
||||
- ask remains usable
|
||||
- a warning notice is shown
|
||||
@@ -257,16 +297,17 @@ If configured keymaps are invalid:
|
||||
|
||||
Invalid keymaps include:
|
||||
|
||||
- missing one of the 6 required actions
|
||||
- missing one of the required contexts or actions
|
||||
- unsupported key syntax
|
||||
- duplicate bindings across configurable actions
|
||||
- use of reserved bindings
|
||||
- duplicate bindings within one context
|
||||
- duplicate bindings between `global` and `main`, `editor`, or `noteEditor`
|
||||
- use of fixed numeric shortcuts (`1` through `9`)
|
||||
|
||||
## Example custom config
|
||||
|
||||
```json
|
||||
{
|
||||
"schemaVersion": 3,
|
||||
"schemaVersion": 4,
|
||||
"answer": {
|
||||
"extractionRetries": 1,
|
||||
"extractionTimeoutMs": 30000,
|
||||
@@ -281,12 +322,43 @@ Invalid keymaps include:
|
||||
"showFooterHints": false
|
||||
},
|
||||
"keymaps": {
|
||||
"cancel": "q",
|
||||
"dismiss": "ctrl+c",
|
||||
"toggle": "ctrl+t",
|
||||
"confirm": "ctrl+k",
|
||||
"optionNote": "x",
|
||||
"questionNote": "shift+x"
|
||||
"global": {
|
||||
"dismiss": ["ctrl+c"],
|
||||
"settings": ["?"]
|
||||
},
|
||||
"main": {
|
||||
"confirm": ["ctrl+k"],
|
||||
"cancel": ["q"],
|
||||
"toggle": ["ctrl+t"],
|
||||
"nextTab": ["tab", "right"],
|
||||
"previousTab": ["shift+tab", "left"],
|
||||
"nextOption": ["down"],
|
||||
"previousOption": ["up"],
|
||||
"optionNote": ["x"],
|
||||
"questionNote": ["shift+x"]
|
||||
},
|
||||
"editor": {
|
||||
"submit": ["ctrl+k"],
|
||||
"close": ["q"],
|
||||
"nextTabWhenEmpty": ["tab", "right"],
|
||||
"previousTabWhenEmpty": ["shift+tab", "left"],
|
||||
"nextOptionWhenEmpty": ["down"],
|
||||
"previousOptionWhenEmpty": ["up"]
|
||||
},
|
||||
"noteEditor": {
|
||||
"save": ["ctrl+k"],
|
||||
"close": ["q"],
|
||||
"nextTabWhenEmpty": ["tab", "right"],
|
||||
"previousTabWhenEmpty": ["shift+tab", "left"],
|
||||
"nextOptionWhenEmpty": ["down"],
|
||||
"previousOptionWhenEmpty": ["up"]
|
||||
},
|
||||
"settingsModal": {
|
||||
"close": ["esc", "ctrl+c", "?"],
|
||||
"nextOption": ["j", "down"],
|
||||
"previousOption": ["k", "up"],
|
||||
"toggle": ["enter", "space"]
|
||||
}
|
||||
},
|
||||
"notifications": {
|
||||
"enabled": true,
|
||||
@@ -305,11 +377,11 @@ Invalid keymaps include:
|
||||
When editing this config for a user:
|
||||
|
||||
- preserve unrelated fields
|
||||
- keep `schemaVersion` at `3`
|
||||
- keep `schemaVersion` at `4`
|
||||
- preserve `answer.extractionModels` as explicit provider/id pairs
|
||||
- keep `answer.extractionRetries` between `0` and `3`
|
||||
- do not assign reserved bindings to configurable actions
|
||||
- do not create duplicate configurable bindings
|
||||
- do not assign fixed numeric shortcuts (`1` through `9`) to configurable actions
|
||||
- do not create duplicate bindings within a context or between `global` and `main`, `editor`, or `noteEditor`
|
||||
- preserve existing `notifications.channels` unless the user asks to change notification targets
|
||||
- only toggle `notifications.enabled` unless the user asks to configure channels
|
||||
- use a `cmux notify` command channel when the user asks for cmux notifications
|
||||
|
||||
+25
-15
@@ -209,7 +209,7 @@ This document defines the stable external behavior. It does not explain internal
|
||||
- ask settings list with binary behaviour and notification toggles
|
||||
- `?` in the ask flow and `/ask-settings` in pi open the same lightweight ask settings overlay
|
||||
- settings persist immediately when changed: `Auto-submit when answered without notes`, `Confirm dismiss when dirty`, `Double-press review shortcuts`, `Notifications`, and `Show footer hints`
|
||||
- `Keymaps` is a persisted config section for `cancel`, `dismiss`, `toggle`, `confirm`, `optionNote`, and `questionNote`
|
||||
- `Keymaps` is a persisted, context-aware config section for global, main-flow, editor, note-editor, and settings-modal actions
|
||||
- the settings list shows the absolute config file path for customizing keymaps, notifications, and extraction settings
|
||||
- if the flow is already on the review tab, all questions are answered, and no notes exist, enabling auto-submit can complete the current ask flow immediately
|
||||
- elaborate results are phrased as direct follow-up instructions, for example: `User asked to elaborate on question "Which option would you like to select?" option "Option A" with note "why this one?"`
|
||||
@@ -218,26 +218,36 @@ This document defines the stable external behavior. It does not explain internal
|
||||
|
||||
Main flow:
|
||||
|
||||
- `?`: open ask settings
|
||||
- inside ask settings, `Esc`, `Ctrl+C`, and `?` close it
|
||||
- `Tab`, `Shift+Tab`, `Left`, `Right`: move between tabs
|
||||
- `Up`, `Down`: move between options
|
||||
- `1..9`: select or toggle the matching option; on the review tab, `1`, `2`, and `3` trigger `Submit`, `Elaborate`, and `Cancel`
|
||||
- `global.settings` opens ask settings; default: `?`
|
||||
- `global.dismiss` dismisses the active ask surface; default: `Ctrl+C`
|
||||
- `main.nextTab` / `main.previousTab` move between tabs; defaults: `Tab`/`Right`, `Shift+Tab`/`Left`
|
||||
- `main.nextOption` / `main.previousOption` move between options or review actions; defaults: `Down`, `Up`
|
||||
- `main.confirm`, `main.cancel`, and `main.toggle` confirm, cancel, or toggle; defaults: `Enter`, `Esc`, `Space`
|
||||
- `main.optionNote` and `main.questionNote` open option/question notes; defaults: `n`, `Shift+N`
|
||||
- `1..9` is fixed and selects or toggles the matching option; on the review tab, `1`, `2`, and `3` trigger `Submit`, `Elaborate`, and `Cancel`
|
||||
- when `Double-press review shortcuts` is enabled, review-tab `1`, `2`, and `3` require the same key twice without a timeout, and the review screen shows an inline hint for the pending action
|
||||
- the following actions are configurable via persisted `keymaps`: `Enter` by default confirms or submits, `Esc` cancels, `Ctrl+C` dismisses, `Space` toggles, `n` edits the active option note, and `Shift+N` edits the current question note
|
||||
|
||||
Editing flow:
|
||||
|
||||
- the configurable `confirm` binding submits the current editor input and closes the editor; in note editors this saves the note only and keeps the ask flow open
|
||||
- the configurable `cancel` binding saves draft and closes the editor
|
||||
- `editor.submit` submits the current custom-answer editor input and closes the editor; default: `Enter`
|
||||
- `noteEditor.save` saves the current note editor and keeps the ask flow open; default: `Enter`
|
||||
- `editor.close` / `noteEditor.close` save draft and close the editor; default: `Esc`
|
||||
- `global.dismiss` dismisses the entire flow immediately without saving the current editor draft when no dirty-dismiss confirmation is pending
|
||||
- `global.settings` opens ask settings when the editor is empty; otherwise the key is delegated to the editor as text/input
|
||||
- when editor has text, arrow keys and `Tab` stay in the editor so the cursor can move while typing
|
||||
- when editor is empty, editor-context `*WhenEmpty` navigation actions move options or tabs without requiring the editor close binding first
|
||||
- `@` remains a fixed file-reference affordance in editors
|
||||
|
||||
Settings modal:
|
||||
|
||||
- `settingsModal.close` closes settings; defaults: `Esc`, `Ctrl+C`, `?`
|
||||
- `settingsModal.nextOption` / `settingsModal.previousOption` move between settings; defaults: `Down`, `Up`
|
||||
- `settingsModal.toggle` toggles the highlighted setting and saves immediately; defaults: `Enter`, `Space`
|
||||
|
||||
Dirty dismiss:
|
||||
|
||||
- when `Confirm dismiss when dirty` is enabled, cancelling or dismissing a dirty ask flow requires the same action a second time
|
||||
- the dirty-dismiss warning stays visible until the user changes tabs in the ask flow
|
||||
- the configurable `dismiss` binding dismisses the entire flow immediately without saving the current editor draft when no dirty-dismiss confirmation is pending
|
||||
- `?`: open ask settings when the editor is empty; otherwise enter `?` as text
|
||||
- inside ask settings, behaviour changes save immediately without explicit save feedback
|
||||
- when editor has text, arrow keys and `Tab` stay in the editor so the cursor can move while typing
|
||||
- when editor is empty, `Up`/`Down` move options and `Tab`/`Shift+Tab`/`Left`/`Right` move between tabs without requiring the configurable cancel key first
|
||||
- navigation resumes only after closing the editor with the configurable `cancel` binding, unless the editor is empty and the navigation keys above are used
|
||||
|
||||
## Non-interactive mode
|
||||
|
||||
|
||||
Reference in New Issue
Block a user