@oh-my-pi/pi-coding-agent 13.14.0 → 13.15.2
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.
- package/CHANGELOG.md +140 -0
- package/package.json +10 -8
- package/src/autoresearch/command-initialize.md +34 -0
- package/src/autoresearch/command-resume.md +17 -0
- package/src/autoresearch/contract.ts +332 -0
- package/src/autoresearch/dashboard.ts +447 -0
- package/src/autoresearch/git.ts +243 -0
- package/src/autoresearch/helpers.ts +458 -0
- package/src/autoresearch/index.ts +693 -0
- package/src/autoresearch/prompt.md +227 -0
- package/src/autoresearch/resume-message.md +16 -0
- package/src/autoresearch/state.ts +386 -0
- package/src/autoresearch/tools/init-experiment.ts +310 -0
- package/src/autoresearch/tools/log-experiment.ts +833 -0
- package/src/autoresearch/tools/run-experiment.ts +640 -0
- package/src/autoresearch/types.ts +218 -0
- package/src/cli/args.ts +8 -2
- package/src/cli/initial-message.ts +58 -0
- package/src/config/keybindings.ts +417 -212
- package/src/config/model-registry.ts +1 -0
- package/src/config/model-resolver.ts +57 -9
- package/src/config/settings-schema.ts +38 -10
- package/src/config/settings.ts +1 -4
- package/src/exec/bash-executor.ts +7 -5
- package/src/export/html/template.css +43 -13
- package/src/export/html/template.generated.ts +1 -1
- package/src/export/html/template.html +1 -0
- package/src/export/html/template.js +107 -0
- package/src/extensibility/extensions/types.ts +31 -8
- package/src/internal-urls/docs-index.generated.ts +1 -1
- package/src/lsp/index.ts +1 -1
- package/src/main.ts +44 -44
- package/src/mcp/oauth-discovery.ts +1 -1
- package/src/modes/acp/acp-agent.ts +957 -0
- package/src/modes/acp/acp-event-mapper.ts +531 -0
- package/src/modes/acp/acp-mode.ts +13 -0
- package/src/modes/acp/index.ts +2 -0
- package/src/modes/components/agent-dashboard.ts +5 -4
- package/src/modes/components/bash-execution.ts +40 -11
- package/src/modes/components/custom-editor.ts +47 -47
- package/src/modes/components/extensions/extension-dashboard.ts +2 -1
- package/src/modes/components/history-search.ts +2 -1
- package/src/modes/components/hook-editor.ts +2 -1
- package/src/modes/components/hook-input.ts +8 -7
- package/src/modes/components/hook-selector.ts +15 -10
- package/src/modes/components/keybinding-hints.ts +9 -9
- package/src/modes/components/login-dialog.ts +3 -3
- package/src/modes/components/mcp-add-wizard.ts +2 -1
- package/src/modes/components/model-selector.ts +14 -3
- package/src/modes/components/oauth-selector.ts +2 -1
- package/src/modes/components/python-execution.ts +2 -3
- package/src/modes/components/session-selector.ts +2 -1
- package/src/modes/components/settings-selector.ts +2 -1
- package/src/modes/components/status-line-segment-editor.ts +2 -1
- package/src/modes/components/tool-execution.ts +4 -5
- package/src/modes/components/tree-selector.ts +3 -2
- package/src/modes/components/user-message-selector.ts +3 -8
- package/src/modes/components/user-message.ts +16 -0
- package/src/modes/controllers/command-controller.ts +0 -2
- package/src/modes/controllers/extension-ui-controller.ts +89 -4
- package/src/modes/controllers/input-controller.ts +29 -23
- package/src/modes/controllers/mcp-command-controller.ts +1 -1
- package/src/modes/index.ts +1 -0
- package/src/modes/interactive-mode.ts +17 -5
- package/src/modes/print-mode.ts +1 -1
- package/src/modes/prompt-action-autocomplete.ts +7 -7
- package/src/modes/rpc/rpc-mode.ts +7 -2
- package/src/modes/rpc/rpc-types.ts +1 -0
- package/src/modes/theme/theme.ts +53 -44
- package/src/modes/types.ts +9 -2
- package/src/modes/utils/hotkeys-markdown.ts +19 -19
- package/src/modes/utils/keybinding-matchers.ts +21 -0
- package/src/modes/utils/ui-helpers.ts +1 -1
- package/src/patch/hashline.ts +139 -127
- package/src/patch/index.ts +77 -59
- package/src/patch/shared.ts +19 -11
- package/src/prompts/tools/hashline.md +43 -116
- package/src/sdk.ts +34 -17
- package/src/session/agent-session.ts +123 -30
- package/src/session/session-manager.ts +32 -31
- package/src/session/streaming-output.ts +87 -37
- package/src/tools/ask.ts +56 -30
- package/src/tools/bash-interactive.ts +2 -6
- package/src/tools/bash-interceptor.ts +1 -39
- package/src/tools/bash-skill-urls.ts +1 -1
- package/src/tools/browser.ts +1 -1
- package/src/tools/gemini-image.ts +1 -1
- package/src/tools/python.ts +2 -2
- package/src/tools/resolve.ts +1 -1
- package/src/utils/child-process.ts +88 -0
|
@@ -1,120 +1,438 @@
|
|
|
1
|
+
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
1
2
|
import * as path from "node:path";
|
|
2
3
|
import {
|
|
3
|
-
|
|
4
|
-
type
|
|
5
|
-
type
|
|
6
|
-
EditorKeybindingsManager,
|
|
4
|
+
type Keybinding,
|
|
5
|
+
type KeybindingDefinitions,
|
|
6
|
+
type KeybindingsConfig,
|
|
7
7
|
type KeyId,
|
|
8
|
-
|
|
9
|
-
|
|
8
|
+
setKeybindings,
|
|
9
|
+
TUI_KEYBINDINGS,
|
|
10
|
+
KeybindingsManager as TuiKeybindingsManager,
|
|
10
11
|
} from "@oh-my-pi/pi-tui";
|
|
11
12
|
import { getAgentDir, isEnoent, logger } from "@oh-my-pi/pi-utils";
|
|
12
13
|
|
|
13
14
|
/**
|
|
14
|
-
* Application-level
|
|
15
|
+
* Application-level keybindings (coding agent specific).
|
|
16
|
+
* Values are always `true` — used for declaration merging.
|
|
15
17
|
*/
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
18
|
+
interface AppKeybindings {
|
|
19
|
+
"app.interrupt": true;
|
|
20
|
+
"app.clear": true;
|
|
21
|
+
"app.exit": true;
|
|
22
|
+
"app.suspend": true;
|
|
23
|
+
"app.thinking.cycle": true;
|
|
24
|
+
"app.thinking.toggle": true;
|
|
25
|
+
"app.model.cycleForward": true;
|
|
26
|
+
"app.model.cycleBackward": true;
|
|
27
|
+
"app.model.select": true;
|
|
28
|
+
"app.tools.expand": true;
|
|
29
|
+
"app.editor.external": true;
|
|
30
|
+
"app.message.followUp": true;
|
|
31
|
+
"app.message.dequeue": true;
|
|
32
|
+
"app.clipboard.pasteImage": true;
|
|
33
|
+
"app.clipboard.copyLine": true;
|
|
34
|
+
"app.clipboard.copyPrompt": true;
|
|
35
|
+
"app.session.new": true;
|
|
36
|
+
"app.session.tree": true;
|
|
37
|
+
"app.session.fork": true;
|
|
38
|
+
"app.session.resume": true;
|
|
39
|
+
"app.session.togglePath": true;
|
|
40
|
+
"app.session.toggleSort": true;
|
|
41
|
+
"app.session.rename": true;
|
|
42
|
+
"app.session.delete": true;
|
|
43
|
+
"app.session.deleteNoninvasive": true;
|
|
44
|
+
"app.tree.foldOrUp": true;
|
|
45
|
+
"app.tree.unfoldOrDown": true;
|
|
46
|
+
"app.plan.toggle": true;
|
|
47
|
+
"app.history.search": true;
|
|
48
|
+
"app.stt.toggle": true;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export type AppKeybinding = keyof AppKeybindings;
|
|
52
|
+
|
|
53
|
+
declare module "@oh-my-pi/pi-tui" {
|
|
54
|
+
interface Keybindings extends AppKeybindings {}
|
|
55
|
+
}
|
|
40
56
|
|
|
41
57
|
/**
|
|
42
|
-
* All
|
|
58
|
+
* All keybindings definitions: TUI + app-specific.
|
|
43
59
|
*/
|
|
44
|
-
export
|
|
60
|
+
export const KEYBINDINGS = {
|
|
61
|
+
...TUI_KEYBINDINGS,
|
|
62
|
+
"app.interrupt": {
|
|
63
|
+
defaultKeys: "escape",
|
|
64
|
+
description: "Interrupt current operation",
|
|
65
|
+
},
|
|
66
|
+
"app.clear": {
|
|
67
|
+
defaultKeys: "ctrl+c",
|
|
68
|
+
description: "Clear screen or cancel",
|
|
69
|
+
},
|
|
70
|
+
"app.exit": {
|
|
71
|
+
defaultKeys: "ctrl+d",
|
|
72
|
+
description: "Exit application",
|
|
73
|
+
},
|
|
74
|
+
"app.suspend": {
|
|
75
|
+
defaultKeys: "ctrl+z",
|
|
76
|
+
description: "Suspend application",
|
|
77
|
+
},
|
|
78
|
+
"app.thinking.cycle": {
|
|
79
|
+
defaultKeys: "shift+tab",
|
|
80
|
+
description: "Cycle thinking level",
|
|
81
|
+
},
|
|
82
|
+
"app.thinking.toggle": {
|
|
83
|
+
defaultKeys: "ctrl+t",
|
|
84
|
+
description: "Toggle thinking mode",
|
|
85
|
+
},
|
|
86
|
+
"app.model.cycleForward": {
|
|
87
|
+
defaultKeys: "ctrl+p",
|
|
88
|
+
description: "Cycle to next model",
|
|
89
|
+
},
|
|
90
|
+
"app.model.cycleBackward": {
|
|
91
|
+
defaultKeys: "shift+ctrl+p",
|
|
92
|
+
description: "Cycle to previous model",
|
|
93
|
+
},
|
|
94
|
+
"app.model.select": {
|
|
95
|
+
defaultKeys: "ctrl+l",
|
|
96
|
+
description: "Select model",
|
|
97
|
+
},
|
|
98
|
+
"app.tools.expand": {
|
|
99
|
+
defaultKeys: "ctrl+o",
|
|
100
|
+
description: "Expand tools",
|
|
101
|
+
},
|
|
102
|
+
"app.editor.external": {
|
|
103
|
+
defaultKeys: "ctrl+g",
|
|
104
|
+
description: "Open external editor",
|
|
105
|
+
},
|
|
106
|
+
"app.message.followUp": {
|
|
107
|
+
defaultKeys: "ctrl+enter",
|
|
108
|
+
description: "Send follow-up message",
|
|
109
|
+
},
|
|
110
|
+
"app.message.dequeue": {
|
|
111
|
+
defaultKeys: "alt+up",
|
|
112
|
+
description: "Dequeue message",
|
|
113
|
+
},
|
|
114
|
+
"app.clipboard.pasteImage": {
|
|
115
|
+
defaultKeys: process.platform === "win32" ? "alt+v" : "ctrl+v",
|
|
116
|
+
description: "Paste image from clipboard",
|
|
117
|
+
},
|
|
118
|
+
"app.clipboard.copyLine": {
|
|
119
|
+
defaultKeys: "alt+shift+l",
|
|
120
|
+
description: "Copy current line",
|
|
121
|
+
},
|
|
122
|
+
"app.clipboard.copyPrompt": {
|
|
123
|
+
defaultKeys: "alt+shift+c",
|
|
124
|
+
description: "Copy prompt",
|
|
125
|
+
},
|
|
126
|
+
"app.session.new": {
|
|
127
|
+
defaultKeys: [],
|
|
128
|
+
description: "Create new session",
|
|
129
|
+
},
|
|
130
|
+
"app.session.tree": {
|
|
131
|
+
defaultKeys: [],
|
|
132
|
+
description: "Show session tree",
|
|
133
|
+
},
|
|
134
|
+
"app.session.fork": {
|
|
135
|
+
defaultKeys: [],
|
|
136
|
+
description: "Fork session",
|
|
137
|
+
},
|
|
138
|
+
"app.session.resume": {
|
|
139
|
+
defaultKeys: [],
|
|
140
|
+
description: "Resume session",
|
|
141
|
+
},
|
|
142
|
+
"app.session.togglePath": {
|
|
143
|
+
defaultKeys: "ctrl+p",
|
|
144
|
+
description: "Toggle session path display",
|
|
145
|
+
},
|
|
146
|
+
"app.session.toggleSort": {
|
|
147
|
+
defaultKeys: "ctrl+s",
|
|
148
|
+
description: "Toggle session sort order",
|
|
149
|
+
},
|
|
150
|
+
"app.session.rename": {
|
|
151
|
+
defaultKeys: "ctrl+r",
|
|
152
|
+
description: "Rename session",
|
|
153
|
+
},
|
|
154
|
+
"app.session.delete": {
|
|
155
|
+
defaultKeys: "ctrl+d",
|
|
156
|
+
description: "Delete session",
|
|
157
|
+
},
|
|
158
|
+
"app.session.deleteNoninvasive": {
|
|
159
|
+
defaultKeys: "ctrl+backspace",
|
|
160
|
+
description: "Delete session (non-invasive)",
|
|
161
|
+
},
|
|
162
|
+
"app.tree.foldOrUp": {
|
|
163
|
+
defaultKeys: ["ctrl+left", "alt+left"],
|
|
164
|
+
description: "Fold or move up",
|
|
165
|
+
},
|
|
166
|
+
"app.tree.unfoldOrDown": {
|
|
167
|
+
defaultKeys: ["ctrl+right", "alt+right"],
|
|
168
|
+
description: "Unfold or move down",
|
|
169
|
+
},
|
|
170
|
+
"app.plan.toggle": {
|
|
171
|
+
defaultKeys: "alt+shift+p",
|
|
172
|
+
description: "Toggle plan mode",
|
|
173
|
+
},
|
|
174
|
+
"app.history.search": {
|
|
175
|
+
defaultKeys: "ctrl+r",
|
|
176
|
+
description: "Search history",
|
|
177
|
+
},
|
|
178
|
+
"app.stt.toggle": {
|
|
179
|
+
defaultKeys: "alt+h",
|
|
180
|
+
description: "Toggle speech-to-text",
|
|
181
|
+
},
|
|
182
|
+
} as const satisfies KeybindingDefinitions;
|
|
45
183
|
|
|
46
184
|
/**
|
|
47
|
-
*
|
|
185
|
+
* Migration map from old keybinding names to new namespaced IDs.
|
|
48
186
|
*/
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
187
|
+
const KEYBINDING_NAME_MIGRATIONS = {
|
|
188
|
+
// App-specific (old names)
|
|
189
|
+
interrupt: "app.interrupt",
|
|
190
|
+
clear: "app.clear",
|
|
191
|
+
exit: "app.exit",
|
|
192
|
+
suspend: "app.suspend",
|
|
193
|
+
cycleThinkingLevel: "app.thinking.cycle",
|
|
194
|
+
cycleModelForward: "app.model.cycleForward",
|
|
195
|
+
cycleModelBackward: "app.model.cycleBackward",
|
|
196
|
+
selectModel: "app.model.select",
|
|
197
|
+
togglePlanMode: "app.plan.toggle",
|
|
198
|
+
historySearch: "app.history.search",
|
|
199
|
+
expandTools: "app.tools.expand",
|
|
200
|
+
toggleThinking: "app.thinking.toggle",
|
|
201
|
+
externalEditor: "app.editor.external",
|
|
202
|
+
followUp: "app.message.followUp",
|
|
203
|
+
dequeue: "app.message.dequeue",
|
|
204
|
+
pasteImage: "app.clipboard.pasteImage",
|
|
205
|
+
copyLine: "app.clipboard.copyLine",
|
|
206
|
+
copyPrompt: "app.clipboard.copyPrompt",
|
|
207
|
+
newSession: "app.session.new",
|
|
208
|
+
tree: "app.session.tree",
|
|
209
|
+
fork: "app.session.fork",
|
|
210
|
+
resume: "app.session.resume",
|
|
211
|
+
toggleSTT: "app.stt.toggle",
|
|
212
|
+
// TUI editor (old names for backward compatibility)
|
|
213
|
+
cursorUp: "tui.editor.cursorUp",
|
|
214
|
+
cursorDown: "tui.editor.cursorDown",
|
|
215
|
+
cursorLeft: "tui.editor.cursorLeft",
|
|
216
|
+
cursorRight: "tui.editor.cursorRight",
|
|
217
|
+
cursorWordLeft: "tui.editor.cursorWordLeft",
|
|
218
|
+
cursorWordRight: "tui.editor.cursorWordRight",
|
|
219
|
+
cursorLineStart: "tui.editor.cursorLineStart",
|
|
220
|
+
cursorLineEnd: "tui.editor.cursorLineEnd",
|
|
221
|
+
jumpForward: "tui.editor.jumpForward",
|
|
222
|
+
jumpBackward: "tui.editor.jumpBackward",
|
|
223
|
+
pageUp: "tui.editor.pageUp",
|
|
224
|
+
pageDown: "tui.editor.pageDown",
|
|
225
|
+
deleteCharBackward: "tui.editor.deleteCharBackward",
|
|
226
|
+
deleteCharForward: "tui.editor.deleteCharForward",
|
|
227
|
+
deleteWordBackward: "tui.editor.deleteWordBackward",
|
|
228
|
+
deleteWordForward: "tui.editor.deleteWordForward",
|
|
229
|
+
deleteToLineStart: "tui.editor.deleteToLineStart",
|
|
230
|
+
deleteToLineEnd: "tui.editor.deleteToLineEnd",
|
|
231
|
+
yank: "tui.editor.yank",
|
|
232
|
+
yankPop: "tui.editor.yankPop",
|
|
233
|
+
undo: "tui.editor.undo",
|
|
234
|
+
// TUI input (old names for backward compatibility)
|
|
235
|
+
newLine: "tui.input.newLine",
|
|
236
|
+
submit: "tui.input.submit",
|
|
237
|
+
tab: "tui.input.tab",
|
|
238
|
+
copy: "tui.input.copy",
|
|
239
|
+
// TUI select (old names for backward compatibility)
|
|
240
|
+
selectUp: "tui.select.up",
|
|
241
|
+
selectDown: "tui.select.down",
|
|
242
|
+
selectPageUp: "tui.select.pageUp",
|
|
243
|
+
selectPageDown: "tui.select.pageDown",
|
|
244
|
+
selectConfirm: "tui.select.confirm",
|
|
245
|
+
selectCancel: "tui.select.cancel",
|
|
246
|
+
// Upstream additional migrations
|
|
247
|
+
toggleSessionNamedFilter: "app.session.togglePath",
|
|
248
|
+
} as const satisfies Record<string, Keybinding>;
|
|
52
249
|
|
|
53
250
|
/**
|
|
54
|
-
*
|
|
251
|
+
* Check if a key is a legacy keybinding name.
|
|
55
252
|
*/
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
suspend: "ctrl+z",
|
|
61
|
-
cycleThinkingLevel: "shift+tab",
|
|
62
|
-
cycleModelForward: "ctrl+p",
|
|
63
|
-
cycleModelBackward: "shift+ctrl+p",
|
|
64
|
-
selectModel: "ctrl+l",
|
|
65
|
-
togglePlanMode: "alt+shift+p",
|
|
66
|
-
historySearch: "ctrl+r",
|
|
67
|
-
expandTools: "ctrl+o",
|
|
68
|
-
toggleThinking: "ctrl+t",
|
|
69
|
-
externalEditor: "ctrl+g",
|
|
70
|
-
followUp: "ctrl+enter",
|
|
71
|
-
dequeue: "alt+up",
|
|
72
|
-
pasteImage: "ctrl+v",
|
|
73
|
-
copyLine: "alt+shift+l",
|
|
74
|
-
copyPrompt: "alt+shift+c",
|
|
75
|
-
newSession: [],
|
|
76
|
-
tree: [],
|
|
77
|
-
fork: [],
|
|
78
|
-
resume: [],
|
|
79
|
-
toggleSTT: "alt+h",
|
|
80
|
-
};
|
|
253
|
+
function isLegacyKeybindingName(key: string): key is keyof typeof KEYBINDING_NAME_MIGRATIONS {
|
|
254
|
+
return key in KEYBINDING_NAME_MIGRATIONS;
|
|
255
|
+
}
|
|
256
|
+
|
|
81
257
|
/**
|
|
82
|
-
*
|
|
258
|
+
* Normalize input to KeybindingsConfig, validating types.
|
|
83
259
|
*/
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
}
|
|
260
|
+
function toKeybindingsConfig(value: unknown): KeybindingsConfig {
|
|
261
|
+
if (typeof value !== "object" || value === null) {
|
|
262
|
+
return {};
|
|
263
|
+
}
|
|
88
264
|
|
|
89
|
-
|
|
90
|
-
const
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
265
|
+
const config: KeybindingsConfig = {};
|
|
266
|
+
for (const [key, val] of Object.entries(value)) {
|
|
267
|
+
// Allow undefined, string (KeyId), or array of strings
|
|
268
|
+
if (val === undefined) {
|
|
269
|
+
config[key] = undefined;
|
|
270
|
+
} else if (typeof val === "string") {
|
|
271
|
+
config[key] = val as KeyId;
|
|
272
|
+
} else if (Array.isArray(val) && val.every(v => typeof v === "string")) {
|
|
273
|
+
config[key] = val as string[] as KeyId[];
|
|
274
|
+
}
|
|
275
|
+
// Silently skip invalid entries
|
|
276
|
+
}
|
|
277
|
+
return config;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* Migrate old keybinding names to new namespaced IDs.
|
|
282
|
+
* Returns both the migrated config and a flag indicating if migration occurred.
|
|
283
|
+
*/
|
|
284
|
+
function migrateKeybindingNames(rawConfig: unknown): {
|
|
285
|
+
config: KeybindingsConfig;
|
|
286
|
+
migrated: boolean;
|
|
287
|
+
} {
|
|
288
|
+
const config = toKeybindingsConfig(rawConfig);
|
|
289
|
+
const migrated: KeybindingsConfig = {};
|
|
290
|
+
let didMigrate = false;
|
|
291
|
+
|
|
292
|
+
for (const [key, value] of Object.entries(config)) {
|
|
293
|
+
if (isLegacyKeybindingName(key)) {
|
|
294
|
+
const newKey = KEYBINDING_NAME_MIGRATIONS[key];
|
|
295
|
+
migrated[newKey] = value;
|
|
296
|
+
didMigrate = true;
|
|
297
|
+
} else {
|
|
298
|
+
// Already a new-style key
|
|
299
|
+
migrated[key] = value;
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
return { config: migrated, migrated: didMigrate };
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* Order keybindings config to match KEYBINDINGS key order.
|
|
308
|
+
*/
|
|
309
|
+
function orderKeybindingsConfig(config: KeybindingsConfig): KeybindingsConfig {
|
|
310
|
+
const ordered: KeybindingsConfig = {};
|
|
311
|
+
for (const key of Object.keys(KEYBINDINGS)) {
|
|
312
|
+
const value = config[key];
|
|
313
|
+
if (value !== undefined) {
|
|
314
|
+
ordered[key] = value;
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
// Add any remaining keys that aren't in KEYBINDINGS
|
|
318
|
+
for (const key of Object.keys(config)) {
|
|
319
|
+
if (!(key in ordered)) {
|
|
320
|
+
ordered[key] = config[key];
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
return ordered;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* Load raw config from a file synchronously.
|
|
328
|
+
* Returns parsed JSON or null if file doesn't exist or is invalid.
|
|
329
|
+
*/
|
|
330
|
+
function loadRawConfig(filePath: string): unknown {
|
|
331
|
+
try {
|
|
332
|
+
if (!existsSync(filePath)) {
|
|
333
|
+
return null;
|
|
334
|
+
}
|
|
335
|
+
const content = readFileSync(filePath, "utf-8");
|
|
336
|
+
return JSON.parse(content);
|
|
337
|
+
} catch (error) {
|
|
338
|
+
if (isEnoent(error)) {
|
|
339
|
+
return null;
|
|
340
|
+
}
|
|
341
|
+
logger.warn("Failed to parse keybindings config", { path: filePath, error: String(error) });
|
|
342
|
+
return null;
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
/**
|
|
347
|
+
* Migrate keybindings config file from old format to new.
|
|
348
|
+
* Reads from agentDir/keybindings.json, migrates old names, and writes back.
|
|
349
|
+
*/
|
|
350
|
+
function loadKeybindingsConfig(filePath: string, writeBack: boolean): KeybindingsConfig {
|
|
351
|
+
const rawConfig = loadRawConfig(filePath);
|
|
352
|
+
|
|
353
|
+
if (rawConfig === null) {
|
|
354
|
+
return {};
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
const { config: migratedConfig, migrated } = migrateKeybindingNames(rawConfig);
|
|
358
|
+
if (writeBack && migrated) {
|
|
359
|
+
const ordered = orderKeybindingsConfig(migratedConfig);
|
|
360
|
+
try {
|
|
361
|
+
writeFileSync(filePath, `${JSON.stringify(ordered, null, 2)}\n`, "utf-8");
|
|
362
|
+
logger.debug("Migrated keybindings config", { path: filePath });
|
|
363
|
+
} catch (error) {
|
|
364
|
+
logger.warn("Failed to write migrated keybindings config", { path: filePath, error: String(error) });
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
return migratedConfig;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
function migrateKeybindingsConfigFile(agentDir: string): void {
|
|
372
|
+
const configPath = path.join(agentDir, "keybindings.json");
|
|
373
|
+
loadKeybindingsConfig(configPath, true);
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
/**
|
|
377
|
+
* Manages all keybindings (app + TUI).
|
|
378
|
+
* Extends the TUI KeybindingsManager with app-specific functionality.
|
|
379
|
+
*/
|
|
380
|
+
export class KeybindingsManager extends TuiKeybindingsManager {
|
|
381
|
+
#configPath: string | undefined;
|
|
382
|
+
|
|
383
|
+
constructor(userBindings: KeybindingsConfig = {}, configPath?: string) {
|
|
384
|
+
super(KEYBINDINGS, userBindings);
|
|
385
|
+
this.#configPath = configPath;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
/**
|
|
389
|
+
* Create from config file at agentDir/keybindings.json.
|
|
390
|
+
*/
|
|
391
|
+
static create(agentDir: string = getAgentDir()): KeybindingsManager {
|
|
392
|
+
const configPath = path.join(agentDir, "keybindings.json");
|
|
393
|
+
const userBindings = KeybindingsManager.#loadFromFile(configPath);
|
|
394
|
+
const manager = new KeybindingsManager(userBindings, configPath);
|
|
395
|
+
// Set globally so getKeybindings() returns this manager
|
|
396
|
+
setKeybindings(manager);
|
|
397
|
+
return manager;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
/**
|
|
401
|
+
* Create an in-memory keybindings manager without file persistence.
|
|
402
|
+
*/
|
|
403
|
+
static inMemory(userBindings: KeybindingsConfig = {}): KeybindingsManager {
|
|
404
|
+
return new KeybindingsManager(userBindings);
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
/**
|
|
408
|
+
* Reload keybindings from the config file.
|
|
409
|
+
*/
|
|
410
|
+
reload(): void {
|
|
411
|
+
if (!this.#configPath) return;
|
|
412
|
+
this.setUserBindings(KeybindingsManager.#loadFromFile(this.#configPath));
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
/**
|
|
416
|
+
* Get the effective resolved bindings (defaults + user overrides).
|
|
417
|
+
*/
|
|
418
|
+
getEffectiveConfig(): KeybindingsConfig {
|
|
419
|
+
return this.getResolvedBindings();
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
/**
|
|
423
|
+
* Get display string for a keybinding (e.g., "ctrl+c/escape").
|
|
424
|
+
*/
|
|
425
|
+
getDisplayString(keybinding: Keybinding): string {
|
|
426
|
+
const keys = this.getKeys(keybinding);
|
|
427
|
+
return formatKeyHints(keys.length === 0 ? [] : keys);
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
/**
|
|
431
|
+
* Load user bindings from a file, migrating old names if needed.
|
|
432
|
+
*/
|
|
433
|
+
static #loadFromFile(filePath: string): KeybindingsConfig {
|
|
434
|
+
return loadKeybindingsConfig(filePath, true);
|
|
435
|
+
}
|
|
118
436
|
}
|
|
119
437
|
|
|
120
438
|
/**
|
|
@@ -145,8 +463,6 @@ const KEY_LABELS: Record<string, string> = {
|
|
|
145
463
|
right: "Right",
|
|
146
464
|
};
|
|
147
465
|
|
|
148
|
-
const normalizeKeyId = (key: KeyId): KeyId => key.toLowerCase() as KeyId;
|
|
149
|
-
|
|
150
466
|
function formatKeyPart(part: string): string {
|
|
151
467
|
const lower = part.toLowerCase();
|
|
152
468
|
const modifier = MODIFIER_LABELS[lower];
|
|
@@ -166,116 +482,5 @@ export function formatKeyHints(keys: KeyId | KeyId[]): string {
|
|
|
166
482
|
return list.map(formatKeyHint).join("/");
|
|
167
483
|
}
|
|
168
484
|
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
*/
|
|
172
|
-
export class KeybindingsManager {
|
|
173
|
-
#appActionToKeys: Map<AppAction, KeyId[]>;
|
|
174
|
-
|
|
175
|
-
private constructor(private readonly config: KeybindingsConfig) {
|
|
176
|
-
this.#appActionToKeys = new Map();
|
|
177
|
-
this.#buildMaps();
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
/**
|
|
181
|
-
* Create from config file and set up editor keybindings.
|
|
182
|
-
*/
|
|
183
|
-
static async create(agentDir: string = getAgentDir()): Promise<KeybindingsManager> {
|
|
184
|
-
const configPath = path.join(agentDir, "keybindings.json");
|
|
185
|
-
const config = await KeybindingsManager.#loadFromFile(configPath);
|
|
186
|
-
const manager = new KeybindingsManager(config);
|
|
187
|
-
|
|
188
|
-
// Set up editor keybindings globally
|
|
189
|
-
const editorConfig: EditorKeybindingsConfig = {};
|
|
190
|
-
for (const [action, keys] of Object.entries(config)) {
|
|
191
|
-
if (!isAppAction(action)) {
|
|
192
|
-
editorConfig[action as EditorAction] = keys;
|
|
193
|
-
}
|
|
194
|
-
}
|
|
195
|
-
setEditorKeybindings(new EditorKeybindingsManager(editorConfig));
|
|
196
|
-
|
|
197
|
-
return manager;
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
/**
|
|
201
|
-
* Create in-memory.
|
|
202
|
-
*/
|
|
203
|
-
static inMemory(config: KeybindingsConfig = {}): KeybindingsManager {
|
|
204
|
-
return new KeybindingsManager(config);
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
static async #loadFromFile(path: string): Promise<KeybindingsConfig> {
|
|
208
|
-
try {
|
|
209
|
-
return await Bun.file(path).json();
|
|
210
|
-
} catch (error) {
|
|
211
|
-
if (isEnoent(error)) return {};
|
|
212
|
-
logger.warn("Failed to parse keybindings config", { path, error: String(error) });
|
|
213
|
-
return {};
|
|
214
|
-
}
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
#buildMaps(): void {
|
|
218
|
-
this.#appActionToKeys.clear();
|
|
219
|
-
|
|
220
|
-
// Set defaults for app actions
|
|
221
|
-
for (const [action, keys] of Object.entries(DEFAULT_APP_KEYBINDINGS)) {
|
|
222
|
-
const keyArray = Array.isArray(keys) ? keys : [keys];
|
|
223
|
-
this.#appActionToKeys.set(
|
|
224
|
-
action as AppAction,
|
|
225
|
-
keyArray.map(key => normalizeKeyId(key as KeyId)),
|
|
226
|
-
);
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
// Override with user config (app actions only)
|
|
230
|
-
for (const [action, keys] of Object.entries(this.config)) {
|
|
231
|
-
if (keys === undefined || !isAppAction(action)) continue;
|
|
232
|
-
const keyArray = Array.isArray(keys) ? keys : [keys];
|
|
233
|
-
this.#appActionToKeys.set(
|
|
234
|
-
action,
|
|
235
|
-
keyArray.map(key => normalizeKeyId(key as KeyId)),
|
|
236
|
-
);
|
|
237
|
-
}
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
/**
|
|
241
|
-
* Check if input matches an app action.
|
|
242
|
-
*/
|
|
243
|
-
matches(data: string, action: AppAction): boolean {
|
|
244
|
-
const keys = this.#appActionToKeys.get(action);
|
|
245
|
-
if (!keys) return false;
|
|
246
|
-
for (const key of keys) {
|
|
247
|
-
if (matchesKey(data, key)) return true;
|
|
248
|
-
}
|
|
249
|
-
return false;
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
/**
|
|
253
|
-
* Get keys bound to an app action.
|
|
254
|
-
*/
|
|
255
|
-
getKeys(action: AppAction): KeyId[] {
|
|
256
|
-
return this.#appActionToKeys.get(action) ?? [];
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
/**
|
|
260
|
-
* Get display string for an action.
|
|
261
|
-
*/
|
|
262
|
-
getDisplayString(action: AppAction): string {
|
|
263
|
-
return formatKeyHints(this.getKeys(action));
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
/**
|
|
267
|
-
* Get the full effective config.
|
|
268
|
-
*/
|
|
269
|
-
getEffectiveConfig(): Required<KeybindingsConfig> {
|
|
270
|
-
const result = { ...DEFAULT_KEYBINDINGS };
|
|
271
|
-
for (const [action, keys] of Object.entries(this.config)) {
|
|
272
|
-
if (keys !== undefined) {
|
|
273
|
-
(result as KeybindingsConfig)[action as KeyAction] = keys;
|
|
274
|
-
}
|
|
275
|
-
}
|
|
276
|
-
return result;
|
|
277
|
-
}
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
// Re-export for convenience
|
|
281
|
-
export type { EditorAction, KeyId };
|
|
485
|
+
export type { Keybinding, KeybindingsConfig, KeyId };
|
|
486
|
+
export { migrateKeybindingsConfigFile };
|