@oh-my-pi/pi-coding-agent 13.14.2 → 13.15.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (85) hide show
  1. package/CHANGELOG.md +150 -0
  2. package/package.json +10 -8
  3. package/src/autoresearch/command-initialize.md +34 -0
  4. package/src/autoresearch/command-resume.md +17 -0
  5. package/src/autoresearch/contract.ts +332 -0
  6. package/src/autoresearch/dashboard.ts +447 -0
  7. package/src/autoresearch/git.ts +243 -0
  8. package/src/autoresearch/helpers.ts +458 -0
  9. package/src/autoresearch/index.ts +693 -0
  10. package/src/autoresearch/prompt.md +227 -0
  11. package/src/autoresearch/resume-message.md +16 -0
  12. package/src/autoresearch/state.ts +386 -0
  13. package/src/autoresearch/tools/init-experiment.ts +310 -0
  14. package/src/autoresearch/tools/log-experiment.ts +833 -0
  15. package/src/autoresearch/tools/run-experiment.ts +640 -0
  16. package/src/autoresearch/types.ts +218 -0
  17. package/src/cli/args.ts +8 -2
  18. package/src/cli/initial-message.ts +58 -0
  19. package/src/config/keybindings.ts +423 -212
  20. package/src/config/model-registry.ts +1 -0
  21. package/src/config/model-resolver.ts +57 -9
  22. package/src/config/settings-schema.ts +38 -10
  23. package/src/config/settings.ts +1 -4
  24. package/src/export/html/template.css +43 -13
  25. package/src/export/html/template.generated.ts +1 -1
  26. package/src/export/html/template.html +1 -0
  27. package/src/export/html/template.js +107 -0
  28. package/src/extensibility/extensions/types.ts +31 -8
  29. package/src/internal-urls/docs-index.generated.ts +1 -1
  30. package/src/lsp/index.ts +1 -1
  31. package/src/main.ts +44 -44
  32. package/src/mcp/oauth-discovery.ts +1 -1
  33. package/src/modes/acp/acp-agent.ts +957 -0
  34. package/src/modes/acp/acp-event-mapper.ts +531 -0
  35. package/src/modes/acp/acp-mode.ts +13 -0
  36. package/src/modes/acp/index.ts +2 -0
  37. package/src/modes/components/agent-dashboard.ts +5 -4
  38. package/src/modes/components/custom-editor.ts +53 -51
  39. package/src/modes/components/extensions/extension-dashboard.ts +2 -1
  40. package/src/modes/components/history-search.ts +2 -1
  41. package/src/modes/components/hook-editor.ts +2 -1
  42. package/src/modes/components/hook-input.ts +8 -7
  43. package/src/modes/components/hook-selector.ts +15 -10
  44. package/src/modes/components/keybinding-hints.ts +9 -9
  45. package/src/modes/components/login-dialog.ts +3 -3
  46. package/src/modes/components/mcp-add-wizard.ts +2 -1
  47. package/src/modes/components/model-selector.ts +14 -3
  48. package/src/modes/components/oauth-selector.ts +2 -1
  49. package/src/modes/components/session-selector.ts +2 -1
  50. package/src/modes/components/settings-selector.ts +2 -1
  51. package/src/modes/components/status-line-segment-editor.ts +2 -1
  52. package/src/modes/components/tree-selector.ts +3 -2
  53. package/src/modes/components/user-message-selector.ts +3 -8
  54. package/src/modes/components/user-message.ts +16 -0
  55. package/src/modes/controllers/extension-ui-controller.ts +89 -4
  56. package/src/modes/controllers/input-controller.ts +48 -29
  57. package/src/modes/controllers/mcp-command-controller.ts +1 -1
  58. package/src/modes/index.ts +1 -0
  59. package/src/modes/interactive-mode.ts +17 -5
  60. package/src/modes/print-mode.ts +1 -1
  61. package/src/modes/prompt-action-autocomplete.ts +7 -7
  62. package/src/modes/rpc/rpc-mode.ts +7 -2
  63. package/src/modes/rpc/rpc-types.ts +1 -0
  64. package/src/modes/theme/theme.ts +53 -44
  65. package/src/modes/types.ts +9 -2
  66. package/src/modes/utils/hotkeys-markdown.ts +20 -20
  67. package/src/modes/utils/keybinding-matchers.ts +21 -0
  68. package/src/modes/utils/ui-helpers.ts +1 -1
  69. package/src/patch/hashline.ts +139 -127
  70. package/src/patch/index.ts +77 -59
  71. package/src/patch/shared.ts +19 -11
  72. package/src/prompts/tools/hashline.md +43 -116
  73. package/src/sdk.ts +34 -17
  74. package/src/session/agent-session.ts +436 -86
  75. package/src/session/messages.ts +23 -0
  76. package/src/session/session-manager.ts +97 -31
  77. package/src/tools/ask.ts +56 -30
  78. package/src/tools/bash-interceptor.ts +1 -39
  79. package/src/tools/bash-skill-urls.ts +1 -1
  80. package/src/tools/browser.ts +1 -1
  81. package/src/tools/gemini-image.ts +1 -1
  82. package/src/tools/resolve.ts +1 -1
  83. package/src/utils/child-process.ts +88 -0
  84. package/src/utils/image-input.ts +11 -1
  85. package/src/web/search/providers/codex.ts +10 -3
@@ -1,41 +1,43 @@
1
1
  import { Editor, type KeyId, matchesKey, parseKittySequence } from "@oh-my-pi/pi-tui";
2
- import type { AppAction } from "../../config/keybindings";
2
+ import type { AppKeybinding } from "../../config/keybindings";
3
3
 
4
4
  type ConfigurableEditorAction = Extract<
5
- AppAction,
6
- | "interrupt"
7
- | "clear"
8
- | "exit"
9
- | "suspend"
10
- | "cycleThinkingLevel"
11
- | "cycleModelForward"
12
- | "cycleModelBackward"
13
- | "selectModel"
14
- | "expandTools"
15
- | "toggleThinking"
16
- | "externalEditor"
17
- | "historySearch"
18
- | "dequeue"
19
- | "pasteImage"
20
- | "copyPrompt"
5
+ AppKeybinding,
6
+ | "app.interrupt"
7
+ | "app.clear"
8
+ | "app.exit"
9
+ | "app.suspend"
10
+ | "app.thinking.cycle"
11
+ | "app.model.cycleForward"
12
+ | "app.model.cycleBackward"
13
+ | "app.model.select"
14
+ | "app.model.selectTemporary"
15
+ | "app.tools.expand"
16
+ | "app.thinking.toggle"
17
+ | "app.editor.external"
18
+ | "app.history.search"
19
+ | "app.message.dequeue"
20
+ | "app.clipboard.pasteImage"
21
+ | "app.clipboard.copyPrompt"
21
22
  >;
22
23
 
23
24
  const DEFAULT_ACTION_KEYS: Record<ConfigurableEditorAction, KeyId[]> = {
24
- interrupt: ["escape"],
25
- clear: ["ctrl+c"],
26
- exit: ["ctrl+d"],
27
- suspend: ["ctrl+z"],
28
- cycleThinkingLevel: ["shift+tab"],
29
- cycleModelForward: ["ctrl+p"],
30
- cycleModelBackward: ["shift+ctrl+p"],
31
- selectModel: ["ctrl+l"],
32
- expandTools: ["ctrl+o"],
33
- toggleThinking: ["ctrl+t"],
34
- externalEditor: ["ctrl+g"],
35
- historySearch: ["ctrl+r"],
36
- dequeue: ["alt+up"],
37
- pasteImage: ["ctrl+v"],
38
- copyPrompt: ["alt+shift+c"],
25
+ "app.interrupt": ["escape"],
26
+ "app.clear": ["ctrl+c"],
27
+ "app.exit": ["ctrl+d"],
28
+ "app.suspend": ["ctrl+z"],
29
+ "app.thinking.cycle": ["shift+tab"],
30
+ "app.model.cycleForward": ["ctrl+p"],
31
+ "app.model.cycleBackward": ["shift+ctrl+p"],
32
+ "app.model.select": ["ctrl+l"],
33
+ "app.model.selectTemporary": ["alt+p"],
34
+ "app.tools.expand": ["ctrl+o"],
35
+ "app.thinking.toggle": ["ctrl+t"],
36
+ "app.editor.external": ["ctrl+g"],
37
+ "app.history.search": ["ctrl+r"],
38
+ "app.message.dequeue": ["alt+up"],
39
+ "app.clipboard.pasteImage": ["ctrl+v"],
40
+ "app.clipboard.copyPrompt": ["alt+shift+c"],
39
41
  };
40
42
 
41
43
  /**
@@ -56,7 +58,7 @@ export class CustomEditor extends Editor {
56
58
  onHistorySearch?: () => void;
57
59
  onSuspend?: () => void;
58
60
  onShowHotkeys?: () => void;
59
- onQuickSelectModel?: () => void;
61
+ onSelectModelTemporary?: () => void;
60
62
  /** Called when the configured copy-prompt shortcut is pressed. */
61
63
  onCopyPrompt?: () => void;
62
64
  /** Called when the configured image-paste shortcut is pressed. */
@@ -115,74 +117,74 @@ export class CustomEditor extends Editor {
115
117
  }
116
118
 
117
119
  // Intercept configured image paste (async - fires and handles result)
118
- if (this.#matchesAction(data, "pasteImage") && this.onPasteImage) {
120
+ if (this.#matchesAction(data, "app.clipboard.pasteImage") && this.onPasteImage) {
119
121
  void this.onPasteImage();
120
122
  return;
121
123
  }
122
124
 
123
125
  // Intercept configured external editor shortcut
124
- if (this.#matchesAction(data, "externalEditor") && this.onExternalEditor) {
126
+ if (this.#matchesAction(data, "app.editor.external") && this.onExternalEditor) {
125
127
  this.onExternalEditor();
126
128
  return;
127
129
  }
128
130
 
129
- // Intercept Alt+P for quick model switching
130
- if (matchesKey(data, "alt+p") && this.onQuickSelectModel) {
131
- this.onQuickSelectModel();
131
+ // Intercept configured temporary model selector shortcut
132
+ if (this.#matchesAction(data, "app.model.selectTemporary") && this.onSelectModelTemporary) {
133
+ this.onSelectModelTemporary();
132
134
  return;
133
135
  }
134
136
 
135
137
  // Intercept configured suspend shortcut
136
- if (this.#matchesAction(data, "suspend") && this.onSuspend) {
138
+ if (this.#matchesAction(data, "app.suspend") && this.onSuspend) {
137
139
  this.onSuspend();
138
140
  return;
139
141
  }
140
142
 
141
143
  // Intercept configured thinking block visibility toggle
142
- if (this.#matchesAction(data, "toggleThinking") && this.onToggleThinking) {
144
+ if (this.#matchesAction(data, "app.thinking.toggle") && this.onToggleThinking) {
143
145
  this.onToggleThinking();
144
146
  return;
145
147
  }
146
148
 
147
149
  // Intercept configured model selector shortcut
148
- if (this.#matchesAction(data, "selectModel") && this.onSelectModel) {
150
+ if (this.#matchesAction(data, "app.model.select") && this.onSelectModel) {
149
151
  this.onSelectModel();
150
152
  return;
151
153
  }
152
154
 
153
155
  // Intercept configured history search shortcut
154
- if (this.#matchesAction(data, "historySearch") && this.onHistorySearch) {
156
+ if (this.#matchesAction(data, "app.history.search") && this.onHistorySearch) {
155
157
  this.onHistorySearch();
156
158
  return;
157
159
  }
158
160
 
159
161
  // Intercept configured tool output expansion shortcut
160
- if (this.#matchesAction(data, "expandTools") && this.onExpandTools) {
162
+ if (this.#matchesAction(data, "app.tools.expand") && this.onExpandTools) {
161
163
  this.onExpandTools();
162
164
  return;
163
165
  }
164
166
 
165
167
  // Intercept configured backward model cycling (check before forward cycling)
166
- if (this.#matchesAction(data, "cycleModelBackward") && this.onCycleModelBackward) {
168
+ if (this.#matchesAction(data, "app.model.cycleBackward") && this.onCycleModelBackward) {
167
169
  this.onCycleModelBackward();
168
170
  return;
169
171
  }
170
172
 
171
173
  // Intercept configured forward model cycling
172
- if (this.#matchesAction(data, "cycleModelForward") && this.onCycleModelForward) {
174
+ if (this.#matchesAction(data, "app.model.cycleForward") && this.onCycleModelForward) {
173
175
  this.onCycleModelForward();
174
176
  return;
175
177
  }
176
178
 
177
179
  // Intercept configured thinking level cycling
178
- if (this.#matchesAction(data, "cycleThinkingLevel") && this.onCycleThinkingLevel) {
180
+ if (this.#matchesAction(data, "app.thinking.cycle") && this.onCycleThinkingLevel) {
179
181
  this.onCycleThinkingLevel();
180
182
  return;
181
183
  }
182
184
 
183
185
  // Intercept configured interrupt shortcut.
184
186
  // Default behavior keeps autocomplete dismissal, but parent can prioritize global interrupt handling.
185
- if (this.#matchesAction(data, "interrupt") && this.onEscape) {
187
+ if (this.#matchesAction(data, "app.interrupt") && this.onEscape) {
186
188
  if (!this.isShowingAutocomplete() || this.shouldBypassAutocompleteOnEscape?.()) {
187
189
  this.onEscape();
188
190
  return;
@@ -190,13 +192,13 @@ export class CustomEditor extends Editor {
190
192
  }
191
193
 
192
194
  // Intercept configured clear shortcut
193
- if (this.#matchesAction(data, "clear") && this.onClear) {
195
+ if (this.#matchesAction(data, "app.clear") && this.onClear) {
194
196
  this.onClear();
195
197
  return;
196
198
  }
197
199
 
198
200
  // Intercept configured exit shortcut (only when editor is empty)
199
- if (this.#matchesAction(data, "exit")) {
201
+ if (this.#matchesAction(data, "app.exit")) {
200
202
  if (this.getText().length === 0 && this.onExit) {
201
203
  this.onExit();
202
204
  }
@@ -205,13 +207,13 @@ export class CustomEditor extends Editor {
205
207
  }
206
208
 
207
209
  // Intercept configured dequeue shortcut (restore queued message to editor)
208
- if (this.#matchesAction(data, "dequeue") && this.onDequeue) {
210
+ if (this.#matchesAction(data, "app.message.dequeue") && this.onDequeue) {
209
211
  this.onDequeue();
210
212
  return;
211
213
  }
212
214
 
213
215
  // Intercept configured copy-prompt shortcut
214
- if (this.#matchesAction(data, "copyPrompt") && this.onCopyPrompt) {
216
+ if (this.#matchesAction(data, "app.clipboard.copyPrompt") && this.onCopyPrompt) {
215
217
  this.onCopyPrompt();
216
218
  return;
217
219
  }
@@ -24,6 +24,7 @@ import {
24
24
  import { Settings } from "../../../config/settings";
25
25
  import { DynamicBorder } from "../../../modes/components/dynamic-border";
26
26
  import { theme } from "../../../modes/theme/theme";
27
+ import { matchesAppInterrupt } from "../../../modes/utils/keybinding-matchers";
27
28
  import { ExtensionList } from "./extension-list";
28
29
  import { InspectorPanel } from "./inspector-panel";
29
30
  import { applyFilter, createInitialState, filterByProvider, refreshState, toggleProvider } from "./state-manager";
@@ -251,7 +252,7 @@ export class ExtensionDashboard extends Container {
251
252
  }
252
253
 
253
254
  // Escape - clear search first, then close
254
- if (matchesKey(data, "escape") || matchesKey(data, "esc")) {
255
+ if (matchesAppInterrupt(data)) {
255
256
  if (this.#state.searchQuery.length > 0) {
256
257
  this.#state.searchQuery = "";
257
258
  this.#state.searchFiltered = this.#state.tabFiltered;
@@ -11,6 +11,7 @@ import {
11
11
  visibleWidth,
12
12
  } from "@oh-my-pi/pi-tui";
13
13
  import { theme } from "../../modes/theme/theme";
14
+ import { matchesAppInterrupt } from "../../modes/utils/keybinding-matchers";
14
15
  import type { HistoryEntry, HistoryStorage } from "../../session/history-storage";
15
16
  import { DynamicBorder } from "./dynamic-border";
16
17
 
@@ -137,7 +138,7 @@ export class HistorySearchComponent extends Container {
137
138
  return;
138
139
  }
139
140
 
140
- if (matchesKey(keyData, "escape") || matchesKey(keyData, "esc")) {
141
+ if (matchesAppInterrupt(keyData)) {
141
142
  this.#onCancel();
142
143
  return;
143
144
  }
@@ -4,6 +4,7 @@
4
4
  */
5
5
  import { Container, Editor, matchesKey, Spacer, Text, type TUI } from "@oh-my-pi/pi-tui";
6
6
  import { getEditorTheme, theme } from "../../modes/theme/theme";
7
+ import { matchesAppInterrupt } from "../../modes/utils/keybinding-matchers";
7
8
  import { getEditorCommand, openInEditor } from "../../utils/external-editor";
8
9
  import { DynamicBorder } from "./dynamic-border";
9
10
 
@@ -67,7 +68,7 @@ export class HookEditorComponent extends Container {
67
68
  }
68
69
 
69
70
  // Escape to cancel
70
- if (matchesKey(keyData, "escape") || matchesKey(keyData, "esc")) {
71
+ if (matchesAppInterrupt(keyData)) {
71
72
  this.#onCancelCallback();
72
73
  return;
73
74
  }
@@ -1,8 +1,9 @@
1
1
  /**
2
2
  * Simple text input component for hooks.
3
3
  */
4
- import { Container, Input, matchesKey, Spacer, Text, type TUI } from "@oh-my-pi/pi-tui";
5
- import { theme } from "../../modes/theme/theme";
4
+ import { Container, Input, Markdown, matchesKey, Spacer, Text, type TUI } from "@oh-my-pi/pi-tui";
5
+ import { getMarkdownTheme, theme } from "../../modes/theme/theme";
6
+ import { matchesAppInterrupt } from "../../modes/utils/keybinding-matchers";
6
7
  import { CountdownTimer } from "./countdown-timer";
7
8
  import { DynamicBorder } from "./dynamic-border";
8
9
 
@@ -16,7 +17,7 @@ export class HookInputComponent extends Container {
16
17
  #input: Input;
17
18
  #onSubmitCallback: (value: string) => void;
18
19
  #onCancelCallback: () => void;
19
- #titleText: Text;
20
+ #titleComponent: Markdown;
20
21
  #baseTitle: string;
21
22
  #countdown: CountdownTimer | undefined;
22
23
 
@@ -36,15 +37,15 @@ export class HookInputComponent extends Container {
36
37
  this.addChild(new DynamicBorder());
37
38
  this.addChild(new Spacer(1));
38
39
 
39
- this.#titleText = new Text(theme.fg("accent", title), 1, 0);
40
- this.addChild(this.#titleText);
40
+ this.#titleComponent = new Markdown(title, 1, 0, getMarkdownTheme(), { color: t => theme.fg("accent", t) });
41
+ this.addChild(this.#titleComponent);
41
42
  this.addChild(new Spacer(1));
42
43
 
43
44
  if (opts?.timeout && opts.timeout > 0 && opts.tui) {
44
45
  this.#countdown = new CountdownTimer(
45
46
  opts.timeout,
46
47
  opts.tui,
47
- s => this.#titleText.setText(theme.fg("accent", `${this.#baseTitle} (${s}s)`)),
48
+ s => this.#titleComponent.setText(`${this.#baseTitle} (${s}s)`),
48
49
  () => {
49
50
  opts.onTimeout?.();
50
51
  this.#onCancelCallback();
@@ -65,7 +66,7 @@ export class HookInputComponent extends Container {
65
66
  this.#countdown?.reset();
66
67
  if (matchesKey(keyData, "enter") || matchesKey(keyData, "return") || keyData === "\n") {
67
68
  this.#onSubmitCallback(this.#input.getValue());
68
- } else if (matchesKey(keyData, "escape") || matchesKey(keyData, "esc")) {
69
+ } else if (matchesAppInterrupt(keyData)) {
69
70
  this.#onCancelCallback();
70
71
  } else {
71
72
  this.#input.handleInput(keyData);
@@ -4,8 +4,10 @@
4
4
  */
5
5
  import {
6
6
  Container,
7
+ Markdown,
7
8
  matchesKey,
8
9
  padding,
10
+ renderInlineMarkdown,
9
11
  replaceTabs,
10
12
  Spacer,
11
13
  Text,
@@ -13,7 +15,8 @@ import {
13
15
  truncateToWidth,
14
16
  visibleWidth,
15
17
  } from "@oh-my-pi/pi-tui";
16
- import { theme } from "../../modes/theme/theme";
18
+ import { getMarkdownTheme, theme } from "../../modes/theme/theme";
19
+ import { matchesSelectCancel } from "../../modes/utils/keybinding-matchers";
17
20
  import { CountdownTimer } from "./countdown-timer";
18
21
  import { DynamicBorder } from "./dynamic-border";
19
22
 
@@ -59,7 +62,7 @@ export class HookSelectorComponent extends Container {
59
62
  #outlinedList: OutlinedList | undefined;
60
63
  #onSelectCallback: (option: string) => void;
61
64
  #onCancelCallback: () => void;
62
- #titleText: Text;
65
+ #titleComponent: Markdown;
63
66
  #baseTitle: string;
64
67
  #countdown: CountdownTimer | undefined;
65
68
  #onLeftCallback: (() => void) | undefined;
@@ -85,15 +88,15 @@ export class HookSelectorComponent extends Container {
85
88
  this.addChild(new DynamicBorder());
86
89
  this.addChild(new Spacer(1));
87
90
 
88
- this.#titleText = new Text(theme.fg("accent", title), 1, 0);
89
- this.addChild(this.#titleText);
91
+ this.#titleComponent = new Markdown(title, 1, 0, getMarkdownTheme(), { color: t => theme.fg("accent", t) });
92
+ this.addChild(this.#titleComponent);
90
93
  this.addChild(new Spacer(1));
91
94
 
92
95
  if (opts?.timeout && opts.timeout > 0 && opts.tui) {
93
96
  this.#countdown = new CountdownTimer(
94
97
  opts.timeout,
95
98
  opts.tui,
96
- s => this.#titleText.setText(theme.fg("accent", `${this.#baseTitle} (${s}s)`)),
99
+ s => this.#titleComponent.setText(`${this.#baseTitle} (${s}s)`),
97
100
  () => {
98
101
  opts?.onTimeout?.();
99
102
  // Auto-select current option on timeout (typically the first/recommended option)
@@ -131,12 +134,14 @@ export class HookSelectorComponent extends Container {
131
134
  );
132
135
  const endIndex = Math.min(startIndex + this.#maxVisible, this.#options.length);
133
136
 
137
+ const mdTheme = getMarkdownTheme();
134
138
  for (let i = startIndex; i < endIndex; i++) {
135
139
  const isSelected = i === this.#selectedIndex;
136
- const text = isSelected
137
- ? theme.fg("accent", `${theme.nav.cursor} `) + theme.fg("accent", this.#options[i])
138
- : ` ${theme.fg("text", this.#options[i])}`;
139
- lines.push(text);
140
+ const label = isSelected
141
+ ? renderInlineMarkdown(this.#options[i], mdTheme, t => theme.fg("accent", t))
142
+ : renderInlineMarkdown(this.#options[i], mdTheme, t => theme.fg("text", t));
143
+ const prefix = isSelected ? theme.fg("accent", `${theme.nav.cursor} `) : " ";
144
+ lines.push(prefix + label);
140
145
  }
141
146
 
142
147
  if (startIndex > 0 || endIndex < this.#options.length) {
@@ -169,7 +174,7 @@ export class HookSelectorComponent extends Container {
169
174
  this.#onLeftCallback?.();
170
175
  } else if (matchesKey(keyData, "right")) {
171
176
  this.#onRightCallback?.();
172
- } else if (matchesKey(keyData, "escape") || matchesKey(keyData, "esc") || matchesKey(keyData, "ctrl+c")) {
177
+ } else if (matchesSelectCancel(keyData)) {
173
178
  this.#onCancelCallback();
174
179
  }
175
180
  }
@@ -1,8 +1,8 @@
1
1
  /**
2
2
  * Utilities for formatting keybinding hints in the UI.
3
3
  */
4
- import { type EditorAction, getEditorKeybindings, type KeyId } from "@oh-my-pi/pi-tui";
5
- import type { AppAction, KeybindingsManager } from "../../config/keybindings";
4
+ import { getKeybindings, type Keybinding, type KeyId } from "@oh-my-pi/pi-tui";
5
+ import type { AppKeybinding, KeybindingsManager } from "../../config/keybindings";
6
6
  import { theme } from "../../modes/theme/theme";
7
7
 
8
8
  /**
@@ -17,14 +17,14 @@ function formatKeys(keys: KeyId[]): string {
17
17
  /**
18
18
  * Get display string for an editor action.
19
19
  */
20
- export function editorKey(action: EditorAction): string {
21
- return formatKeys(getEditorKeybindings().getKeys(action));
20
+ export function editorKey(action: Keybinding): string {
21
+ return formatKeys(getKeybindings().getKeys(action));
22
22
  }
23
23
 
24
24
  /**
25
25
  * Get display string for an app action.
26
26
  */
27
- export function appKey(keybindings: KeybindingsManager, action: AppAction): string {
27
+ export function appKey(keybindings: KeybindingsManager, action: AppKeybinding): string {
28
28
  return formatKeys(keybindings.getKeys(action));
29
29
  }
30
30
 
@@ -32,11 +32,11 @@ export function appKey(keybindings: KeybindingsManager, action: AppAction): stri
32
32
  * Format a keybinding hint with consistent styling: dim key, muted description.
33
33
  * Looks up the key from editor keybindings automatically.
34
34
  *
35
- * @param action - Editor action name (e.g., "selectConfirm", "expandTools")
35
+ * @param action - Keybinding action name (e.g., "tui.select.confirm", "app.tools.expand")
36
36
  * @param description - Description text (e.g., "to expand", "cancel")
37
37
  * @returns Formatted string with dim key and muted description
38
38
  */
39
- export function keyHint(action: EditorAction, description: string): string {
39
+ export function keyHint(action: Keybinding, description: string): string {
40
40
  return theme.fg("dim", editorKey(action)) + theme.fg("muted", ` ${description}`);
41
41
  }
42
42
 
@@ -45,11 +45,11 @@ export function keyHint(action: EditorAction, description: string): string {
45
45
  * Requires the KeybindingsManager instance.
46
46
  *
47
47
  * @param keybindings - KeybindingsManager instance
48
- * @param action - App action name (e.g., "interrupt", "externalEditor")
48
+ * @param action - App keybinding name (e.g., "app.interrupt", "app.editor.external")
49
49
  * @param description - Description text
50
50
  * @returns Formatted string with dim key and muted description
51
51
  */
52
- export function appKeyHint(keybindings: KeybindingsManager, action: AppAction, description: string): string {
52
+ export function appKeyHint(keybindings: KeybindingsManager, action: AppKeybinding, description: string): string {
53
53
  return theme.fg("dim", appKey(keybindings, action)) + theme.fg("muted", ` ${description}`);
54
54
  }
55
55
 
@@ -1,5 +1,5 @@
1
1
  import { getOAuthProviders } from "@oh-my-pi/pi-ai";
2
- import { Container, getEditorKeybindings, Input, Spacer, Text, type TUI } from "@oh-my-pi/pi-tui";
2
+ import { Container, getKeybindings, Input, Spacer, Text, type TUI } from "@oh-my-pi/pi-tui";
3
3
  import { theme } from "../../modes/theme/theme";
4
4
  import { openPath } from "../../utils/open";
5
5
  import { DynamicBorder } from "./dynamic-border";
@@ -151,9 +151,9 @@ export class LoginDialogComponent extends Container {
151
151
  }
152
152
 
153
153
  handleInput(data: string): void {
154
- const kb = getEditorKeybindings();
154
+ const kb = getKeybindings();
155
155
 
156
- if (kb.matches(data, "selectCancel")) {
156
+ if (kb.matches(data, "tui.select.cancel")) {
157
157
  this.#cancel();
158
158
  return;
159
159
  }
@@ -19,6 +19,7 @@ import { analyzeAuthError, discoverOAuthEndpoints } from "../../mcp/oauth-discov
19
19
  import type { MCPHttpServerConfig, MCPServerConfig, MCPSseServerConfig, MCPStdioServerConfig } from "../../mcp/types";
20
20
  import { shortenPath } from "../../tools/render-utils";
21
21
  import { theme } from "../theme/theme";
22
+ import { matchesAppInterrupt } from "../utils/keybinding-matchers";
22
23
  import { DynamicBorder } from "./dynamic-border";
23
24
 
24
25
  type TransportType = "stdio" | "http" | "sse";
@@ -452,7 +453,7 @@ export class MCPAddWizard extends Container {
452
453
  }
453
454
 
454
455
  // Handle Escape (always handled by wizard)
455
- if (matchesKey(keyData, "escape")) {
456
+ if (matchesAppInterrupt(keyData)) {
456
457
  if (this.#currentStep === "name") {
457
458
  // Cancel wizard
458
459
  this.#onCancelCallback();
@@ -1,6 +1,17 @@
1
1
  import { ThinkingLevel } from "@oh-my-pi/pi-agent-core";
2
2
  import { getSupportedEfforts, type Model, modelsAreEqual } from "@oh-my-pi/pi-ai";
3
- import { Container, Input, matchesKey, Spacer, type Tab, TabBar, Text, type TUI, visibleWidth } from "@oh-my-pi/pi-tui";
3
+ import {
4
+ Container,
5
+ getKeybindings,
6
+ Input,
7
+ matchesKey,
8
+ Spacer,
9
+ type Tab,
10
+ TabBar,
11
+ Text,
12
+ type TUI,
13
+ visibleWidth,
14
+ } from "@oh-my-pi/pi-tui";
4
15
  import { MODEL_ROLE_IDS, MODEL_ROLES, type ModelRegistry, type ModelRole } from "../../config/model-registry";
5
16
  import { resolveModelRoleValue } from "../../config/model-resolver";
6
17
  import type { Settings } from "../../config/settings";
@@ -647,7 +658,7 @@ export class ModelSelectorComponent extends Container {
647
658
  }
648
659
 
649
660
  // Escape or Ctrl+C - close selector
650
- if (matchesKey(keyData, "escape") || matchesKey(keyData, "esc") || matchesKey(keyData, "ctrl+c")) {
661
+ if (getKeybindings().matches(keyData, "tui.select.cancel")) {
651
662
  this.#onCancelCallback();
652
663
  return;
653
664
  }
@@ -698,7 +709,7 @@ export class ModelSelectorComponent extends Container {
698
709
  return;
699
710
  }
700
711
 
701
- if (matchesKey(keyData, "escape") || matchesKey(keyData, "esc") || matchesKey(keyData, "ctrl+c")) {
712
+ if (getKeybindings().matches(keyData, "tui.select.cancel")) {
702
713
  if (this.#menuStep === "thinking" && this.#menuSelectedRole !== null) {
703
714
  this.#menuStep = "role";
704
715
  const roleIndex = MENU_ROLE_ACTIONS.findIndex(action => action.role === this.#menuSelectedRole);
@@ -1,6 +1,7 @@
1
1
  import { getOAuthProviders, type OAuthProviderInfo } from "@oh-my-pi/pi-ai";
2
2
  import { Container, matchesKey, Spacer, TruncatedText } from "@oh-my-pi/pi-tui";
3
3
  import { theme } from "../../modes/theme/theme";
4
+ import { matchesSelectCancel } from "../../modes/utils/keybinding-matchers";
4
5
  import type { AuthStorage } from "../../session/auth-storage";
5
6
  import { DynamicBorder } from "./dynamic-border";
6
7
  /**
@@ -202,7 +203,7 @@ export class OAuthSelectorComponent extends Container {
202
203
  }
203
204
  }
204
205
  // Escape or Ctrl+C
205
- else if (matchesKey(keyData, "escape") || matchesKey(keyData, "esc") || matchesKey(keyData, "ctrl+c")) {
206
+ else if (matchesSelectCancel(keyData)) {
206
207
  this.stopValidation();
207
208
  this.#onCancelCallback();
208
209
  }
@@ -11,6 +11,7 @@ import {
11
11
  visibleWidth,
12
12
  } from "@oh-my-pi/pi-tui";
13
13
  import { theme } from "../../modes/theme/theme";
14
+ import { matchesAppInterrupt } from "../../modes/utils/keybinding-matchers";
14
15
  import type { SessionInfo } from "../../session/session-manager";
15
16
  import { fuzzyFilter } from "../../utils/fuzzy";
16
17
  import { DynamicBorder } from "./dynamic-border";
@@ -219,7 +220,7 @@ class SessionList implements Component {
219
220
  return;
220
221
  }
221
222
  // Escape - cancel
222
- if (matchesKey(keyData, "escape") || matchesKey(keyData, "esc")) {
223
+ if (matchesAppInterrupt(keyData)) {
223
224
  if (this.onCancel) {
224
225
  this.onCancel();
225
226
  }
@@ -21,6 +21,7 @@ import type {
21
21
  } from "../../config/settings-schema";
22
22
  import { SETTING_TABS, TAB_METADATA } from "../../config/settings-schema";
23
23
  import { getCurrentThemeName, getSelectListTheme, getSettingsListTheme, theme } from "../../modes/theme/theme";
24
+ import { matchesAppInterrupt } from "../../modes/utils/keybinding-matchers";
24
25
  import { getTabBarTheme } from "../shared";
25
26
  import { DynamicBorder } from "./dynamic-border";
26
27
  import { PluginSettingsComponent } from "./plugin-settings";
@@ -521,7 +522,7 @@ export class SettingsSelectorComponent extends Container {
521
522
  }
522
523
 
523
524
  // Escape at top level cancels
524
- if ((matchesKey(data, "escape") || matchesKey(data, "esc")) && !this.#currentSubmenu) {
525
+ if (matchesAppInterrupt(data) && !this.#currentSubmenu) {
525
526
  this.callbacks.onCancel();
526
527
  return;
527
528
  }
@@ -11,6 +11,7 @@
11
11
  import { Container, matchesKey, padding } from "@oh-my-pi/pi-tui";
12
12
  import type { StatusLineSegmentId } from "../../config/settings-schema";
13
13
  import { theme } from "../../modes/theme/theme";
14
+ import { matchesAppInterrupt } from "../../modes/utils/keybinding-matchers";
14
15
  import { ALL_SEGMENT_IDS } from "./status-line/segments";
15
16
 
16
17
  // Segment display names and short descriptions
@@ -239,7 +240,7 @@ export class StatusLineSegmentEditorComponent extends Container {
239
240
  const left = this.#getSegmentsForColumn("left").map(s => s.id);
240
241
  const right = this.#getSegmentsForColumn("right").map(s => s.id);
241
242
  this.callbacks.onSave(left, right);
242
- } else if (matchesKey(data, "escape") || matchesKey(data, "esc")) {
243
+ } else if (matchesAppInterrupt(data)) {
243
244
  this.callbacks.onCancel();
244
245
  }
245
246
  }
@@ -12,6 +12,7 @@ import {
12
12
  } from "@oh-my-pi/pi-tui";
13
13
  import type { TreeFilterMode } from "../../config/settings-schema";
14
14
  import { theme } from "../../modes/theme/theme";
15
+ import { matchesAppInterrupt } from "../../modes/utils/keybinding-matchers";
15
16
  import type { SessionTreeNode } from "../../session/session-manager";
16
17
  import { shortenPath } from "../../tools/render-utils";
17
18
  import { DynamicBorder } from "./dynamic-border";
@@ -702,7 +703,7 @@ class TreeList implements Component {
702
703
  if (selected && this.onSelect) {
703
704
  this.onSelect(selected.node.entry.id);
704
705
  }
705
- } else if (matchesKey(keyData, "escape") || matchesKey(keyData, "esc")) {
706
+ } else if (matchesAppInterrupt(keyData)) {
706
707
  if (this.#searchQuery) {
707
708
  this.#searchQuery = "";
708
709
  this.#applyFilter();
@@ -807,7 +808,7 @@ class LabelInput implements Component {
807
808
  if (matchesKey(keyData, "enter") || matchesKey(keyData, "return") || keyData === "\n") {
808
809
  const value = this.#input.getValue().trim();
809
810
  this.onSubmit?.(this.entryId, value || undefined);
810
- } else if (matchesKey(keyData, "escape") || matchesKey(keyData, "esc")) {
811
+ } else if (matchesAppInterrupt(keyData)) {
811
812
  this.onCancel?.();
812
813
  } else {
813
814
  this.#input.handleInput(keyData);
@@ -1,5 +1,6 @@
1
1
  import { type Component, Container, matchesKey, Spacer, Text, truncateToWidth } from "@oh-my-pi/pi-tui";
2
2
  import { theme } from "../../modes/theme/theme";
3
+ import { matchesSelectCancel } from "../../modes/utils/keybinding-matchers";
3
4
  import { DynamicBorder } from "./dynamic-border";
4
5
 
5
6
  interface UserMessageItem {
@@ -91,14 +92,8 @@ class UserMessageList implements Component {
91
92
  this.onSelect(selected.id);
92
93
  }
93
94
  }
94
- // Escape - cancel
95
- else if (matchesKey(keyData, "escape") || matchesKey(keyData, "esc")) {
96
- if (this.onCancel) {
97
- this.onCancel();
98
- }
99
- }
100
- // Ctrl+C - cancel
101
- else if (matchesKey(keyData, "ctrl+c")) {
95
+ // Escape / cancel
96
+ else if (matchesSelectCancel(keyData)) {
102
97
  if (this.onCancel) {
103
98
  this.onCancel();
104
99
  }