@claude-code-kit/ui 0.1.0
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/dist/index.d.mts +284 -0
- package/dist/index.d.ts +284 -0
- package/dist/index.js +2419 -0
- package/dist/index.mjs +2364 -0
- package/package.json +68 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,2419 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __export = (target, all) => {
|
|
9
|
+
for (var name in all)
|
|
10
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
11
|
+
};
|
|
12
|
+
var __copyProps = (to, from, except, desc) => {
|
|
13
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
+
for (let key of __getOwnPropNames(from))
|
|
15
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
16
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
17
|
+
}
|
|
18
|
+
return to;
|
|
19
|
+
};
|
|
20
|
+
var __reExport = (target, mod, secondTarget) => (__copyProps(target, mod, "default"), secondTarget && __copyProps(secondTarget, mod, "default"));
|
|
21
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
22
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
23
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
24
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
25
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
26
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
27
|
+
mod
|
|
28
|
+
));
|
|
29
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
30
|
+
|
|
31
|
+
// src/index.ts
|
|
32
|
+
var index_exports = {};
|
|
33
|
+
__export(index_exports, {
|
|
34
|
+
CommandRegistry: () => CommandRegistry,
|
|
35
|
+
DEFAULT_BINDINGS: () => DEFAULT_BINDINGS,
|
|
36
|
+
Divider: () => Divider,
|
|
37
|
+
KeybindingSetup: () => KeybindingSetup,
|
|
38
|
+
MessageList: () => MessageList,
|
|
39
|
+
MultiSelect: () => MultiSelect,
|
|
40
|
+
ProgressBar: () => ProgressBar,
|
|
41
|
+
PromptInput: () => PromptInput,
|
|
42
|
+
REPL: () => REPL,
|
|
43
|
+
Select: () => Select,
|
|
44
|
+
Spinner: () => Spinner,
|
|
45
|
+
StatusIcon: () => StatusIcon,
|
|
46
|
+
StatusLine: () => StatusLine,
|
|
47
|
+
StreamingText: () => StreamingText,
|
|
48
|
+
clearCommand: () => clearCommand,
|
|
49
|
+
createCommandRegistry: () => createCommandRegistry,
|
|
50
|
+
defineCommand: () => defineCommand,
|
|
51
|
+
defineJSXCommand: () => defineJSXCommand,
|
|
52
|
+
defineLocalCommand: () => defineLocalCommand,
|
|
53
|
+
exitCommand: () => exitCommand,
|
|
54
|
+
helpCommand: () => helpCommand,
|
|
55
|
+
useKeybinding: () => useKeybinding,
|
|
56
|
+
useKeybindings: () => useKeybindings,
|
|
57
|
+
useStatusLine: () => useStatusLine
|
|
58
|
+
});
|
|
59
|
+
module.exports = __toCommonJS(index_exports);
|
|
60
|
+
__reExport(index_exports, require("@claude-code-kit/ink-renderer"), module.exports);
|
|
61
|
+
|
|
62
|
+
// src/Divider.tsx
|
|
63
|
+
var import_react = require("react");
|
|
64
|
+
var import_ink_renderer = require("@claude-code-kit/ink-renderer");
|
|
65
|
+
var import_jsx_runtime = require("react/jsx-runtime");
|
|
66
|
+
function Divider({ width, color, char = "\u2500", padding = 0, title }) {
|
|
67
|
+
const terminalSize = (0, import_react.useContext)(import_ink_renderer.TerminalSizeContext);
|
|
68
|
+
const terminalWidth = terminalSize?.columns ?? 80;
|
|
69
|
+
const effectiveWidth = Math.max(0, (width ?? terminalWidth - 2) - padding);
|
|
70
|
+
if (title) {
|
|
71
|
+
const titleWidth = (0, import_ink_renderer.stringWidth)(title) + 2;
|
|
72
|
+
const sideWidth = Math.max(0, effectiveWidth - titleWidth);
|
|
73
|
+
const leftWidth = Math.floor(sideWidth / 2);
|
|
74
|
+
const rightWidth = sideWidth - leftWidth;
|
|
75
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_ink_renderer.Text, { color, dimColor: !color, children: [
|
|
76
|
+
char.repeat(leftWidth),
|
|
77
|
+
" ",
|
|
78
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_ink_renderer.Text, { dimColor: true, children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_ink_renderer.Ansi, { children: title }) }),
|
|
79
|
+
" ",
|
|
80
|
+
char.repeat(rightWidth)
|
|
81
|
+
] });
|
|
82
|
+
}
|
|
83
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_ink_renderer.Text, { color, dimColor: !color, children: char.repeat(effectiveWidth) });
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// src/ProgressBar.tsx
|
|
87
|
+
var import_ink_renderer2 = require("@claude-code-kit/ink-renderer");
|
|
88
|
+
var import_jsx_runtime2 = require("react/jsx-runtime");
|
|
89
|
+
var BLOCKS = [" ", "\u258F", "\u258E", "\u258D", "\u258C", "\u258B", "\u258A", "\u2589", "\u2588"];
|
|
90
|
+
function ProgressBar({ ratio: inputRatio, width, fillColor, emptyColor }) {
|
|
91
|
+
const ratio = Math.min(1, Math.max(0, inputRatio));
|
|
92
|
+
const whole = Math.floor(ratio * width);
|
|
93
|
+
const segments = [BLOCKS[BLOCKS.length - 1].repeat(whole)];
|
|
94
|
+
if (whole < width) {
|
|
95
|
+
const remainder = ratio * width - whole;
|
|
96
|
+
const middle = Math.floor(remainder * BLOCKS.length);
|
|
97
|
+
segments.push(BLOCKS[middle]);
|
|
98
|
+
const empty = width - whole - 1;
|
|
99
|
+
if (empty > 0) {
|
|
100
|
+
segments.push(BLOCKS[0].repeat(empty));
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_ink_renderer2.Text, { color: fillColor, backgroundColor: emptyColor, children: segments.join("") });
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// src/StatusIcon.tsx
|
|
107
|
+
var import_figures = __toESM(require("figures"));
|
|
108
|
+
var import_ink_renderer3 = require("@claude-code-kit/ink-renderer");
|
|
109
|
+
var import_jsx_runtime3 = require("react/jsx-runtime");
|
|
110
|
+
var STATUS_CONFIG = {
|
|
111
|
+
success: { icon: import_figures.default.tick, color: "green" },
|
|
112
|
+
error: { icon: import_figures.default.cross, color: "red" },
|
|
113
|
+
warning: { icon: import_figures.default.warning, color: "yellow" },
|
|
114
|
+
info: { icon: import_figures.default.info, color: "blue" },
|
|
115
|
+
pending: { icon: import_figures.default.circle, color: void 0 },
|
|
116
|
+
loading: { icon: "\u2026", color: void 0 }
|
|
117
|
+
};
|
|
118
|
+
function StatusIcon({ status, withSpace = false }) {
|
|
119
|
+
const config = STATUS_CONFIG[status];
|
|
120
|
+
return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_ink_renderer3.Text, { color: config.color, dimColor: !config.color, children: [
|
|
121
|
+
config.icon,
|
|
122
|
+
withSpace && " "
|
|
123
|
+
] });
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// src/StatusLine.tsx
|
|
127
|
+
var import_react2 = require("react");
|
|
128
|
+
var import_ink_renderer4 = require("@claude-code-kit/ink-renderer");
|
|
129
|
+
var import_jsx_runtime4 = require("react/jsx-runtime");
|
|
130
|
+
function hasAnsi(s) {
|
|
131
|
+
return /\x1b\[/.test(s);
|
|
132
|
+
}
|
|
133
|
+
function StatusLine({
|
|
134
|
+
segments,
|
|
135
|
+
text,
|
|
136
|
+
paddingX = 1,
|
|
137
|
+
gap = 1,
|
|
138
|
+
borderStyle = "none",
|
|
139
|
+
borderColor
|
|
140
|
+
}) {
|
|
141
|
+
const border = borderStyle === "none" ? void 0 : borderStyle;
|
|
142
|
+
return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
|
|
143
|
+
import_ink_renderer4.Box,
|
|
144
|
+
{
|
|
145
|
+
flexDirection: "row",
|
|
146
|
+
paddingX,
|
|
147
|
+
borderStyle: border,
|
|
148
|
+
borderColor,
|
|
149
|
+
children: text !== void 0 ? hasAnsi(text) ? /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_ink_renderer4.Ansi, { children: text }) : /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_ink_renderer4.Text, { dimColor: true, children: text }) : segments?.map((seg, i) => /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_ink_renderer4.Box, { flexGrow: seg.flex ? 1 : 0, marginRight: i < segments.length - 1 ? gap : 0, children: hasAnsi(seg.content) ? /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_ink_renderer4.Ansi, { children: seg.content }) : /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_ink_renderer4.Text, { dimColor: true, color: seg.color, children: seg.content }) }, i))
|
|
150
|
+
}
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
function useStatusLine(updater, deps, intervalMs) {
|
|
154
|
+
const [value, setValue] = (0, import_react2.useState)(() => updater());
|
|
155
|
+
(0, import_react2.useEffect)(() => {
|
|
156
|
+
setValue(updater());
|
|
157
|
+
}, deps);
|
|
158
|
+
(0, import_react2.useEffect)(() => {
|
|
159
|
+
if (!intervalMs) return;
|
|
160
|
+
const id = setInterval(() => setValue(updater()), intervalMs);
|
|
161
|
+
return () => clearInterval(id);
|
|
162
|
+
}, [intervalMs]);
|
|
163
|
+
return value;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// src/commands/registry.ts
|
|
167
|
+
var CommandRegistry = class {
|
|
168
|
+
constructor() {
|
|
169
|
+
this.commands = /* @__PURE__ */ new Map();
|
|
170
|
+
}
|
|
171
|
+
register(...commands) {
|
|
172
|
+
for (const cmd of commands) {
|
|
173
|
+
this.commands.set(cmd.name, cmd);
|
|
174
|
+
if (cmd.aliases) {
|
|
175
|
+
for (const alias of cmd.aliases) {
|
|
176
|
+
this.commands.set(alias, cmd);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
get(name) {
|
|
182
|
+
return this.commands.get(name);
|
|
183
|
+
}
|
|
184
|
+
getAll() {
|
|
185
|
+
return [...new Set(this.commands.values())];
|
|
186
|
+
}
|
|
187
|
+
getVisible() {
|
|
188
|
+
return this.getAll().filter(
|
|
189
|
+
(cmd) => !cmd.isHidden && (cmd.isEnabled?.() ?? true)
|
|
190
|
+
);
|
|
191
|
+
}
|
|
192
|
+
parse(input) {
|
|
193
|
+
const trimmed = input.trim();
|
|
194
|
+
if (!trimmed.startsWith("/")) return null;
|
|
195
|
+
const spaceIdx = trimmed.indexOf(" ");
|
|
196
|
+
const name = spaceIdx === -1 ? trimmed.slice(1) : trimmed.slice(1, spaceIdx);
|
|
197
|
+
const args = spaceIdx === -1 ? "" : trimmed.slice(spaceIdx + 1).trim();
|
|
198
|
+
const command = this.get(name);
|
|
199
|
+
if (!command) return null;
|
|
200
|
+
if (command.isEnabled && !command.isEnabled()) return null;
|
|
201
|
+
return { command, args };
|
|
202
|
+
}
|
|
203
|
+
getSuggestions(partial) {
|
|
204
|
+
if (!partial.startsWith("/")) return [];
|
|
205
|
+
const search = partial.slice(1).toLowerCase();
|
|
206
|
+
return this.getVisible().filter(
|
|
207
|
+
(cmd) => cmd.name.toLowerCase().startsWith(search) || cmd.aliases?.some((a) => a.toLowerCase().startsWith(search))
|
|
208
|
+
);
|
|
209
|
+
}
|
|
210
|
+
};
|
|
211
|
+
function createCommandRegistry(commands) {
|
|
212
|
+
const registry = new CommandRegistry();
|
|
213
|
+
registry.register(...commands);
|
|
214
|
+
return registry;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// src/commands/defineCommand.ts
|
|
218
|
+
function defineCommand(cmd) {
|
|
219
|
+
return cmd;
|
|
220
|
+
}
|
|
221
|
+
function defineLocalCommand(cmd) {
|
|
222
|
+
return { ...cmd, type: "local" };
|
|
223
|
+
}
|
|
224
|
+
function defineJSXCommand(cmd) {
|
|
225
|
+
return { ...cmd, type: "jsx" };
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// src/commands/builtins.ts
|
|
229
|
+
var exitCommand = {
|
|
230
|
+
name: "exit",
|
|
231
|
+
description: "Exit the application",
|
|
232
|
+
aliases: ["quit", "q"],
|
|
233
|
+
type: "local",
|
|
234
|
+
execute: () => {
|
|
235
|
+
process.exit(0);
|
|
236
|
+
}
|
|
237
|
+
};
|
|
238
|
+
var helpCommand = (registry) => ({
|
|
239
|
+
name: "help",
|
|
240
|
+
description: "Show available commands",
|
|
241
|
+
aliases: ["?"],
|
|
242
|
+
type: "local",
|
|
243
|
+
execute: () => {
|
|
244
|
+
const commands = registry.getVisible();
|
|
245
|
+
const lines = commands.map((cmd) => {
|
|
246
|
+
const aliases = cmd.aliases?.length ? ` (${cmd.aliases.join(", ")})` : "";
|
|
247
|
+
return ` /${cmd.name}${aliases} \u2014 ${cmd.description}`;
|
|
248
|
+
});
|
|
249
|
+
return { type: "text", value: ["Available commands:", "", ...lines].join("\n") };
|
|
250
|
+
}
|
|
251
|
+
});
|
|
252
|
+
var clearCommand = {
|
|
253
|
+
name: "clear",
|
|
254
|
+
description: "Clear the screen",
|
|
255
|
+
type: "local",
|
|
256
|
+
execute: () => {
|
|
257
|
+
process.stdout.write("\x1B[2J\x1B[3J\x1B[H");
|
|
258
|
+
return { type: "skip" };
|
|
259
|
+
}
|
|
260
|
+
};
|
|
261
|
+
|
|
262
|
+
// src/keybindings/useKeybinding.ts
|
|
263
|
+
var import_react4 = require("react");
|
|
264
|
+
var import_ink_renderer5 = require("@claude-code-kit/ink-renderer");
|
|
265
|
+
|
|
266
|
+
// src/keybindings/KeybindingContext.tsx
|
|
267
|
+
var import_react3 = require("react");
|
|
268
|
+
|
|
269
|
+
// src/keybindings/match.ts
|
|
270
|
+
function getKeyName(input, key) {
|
|
271
|
+
if (key.escape) return "escape";
|
|
272
|
+
if (key.return) return "enter";
|
|
273
|
+
if (key.tab) return "tab";
|
|
274
|
+
if (key.backspace) return "backspace";
|
|
275
|
+
if (key.delete) return "delete";
|
|
276
|
+
if (key.upArrow) return "up";
|
|
277
|
+
if (key.downArrow) return "down";
|
|
278
|
+
if (key.leftArrow) return "left";
|
|
279
|
+
if (key.rightArrow) return "right";
|
|
280
|
+
if (key.pageUp) return "pageup";
|
|
281
|
+
if (key.pageDown) return "pagedown";
|
|
282
|
+
if (key.wheelUp) return "wheelup";
|
|
283
|
+
if (key.wheelDown) return "wheeldown";
|
|
284
|
+
if (key.home) return "home";
|
|
285
|
+
if (key.end) return "end";
|
|
286
|
+
if (input.length === 1) return input.toLowerCase();
|
|
287
|
+
return null;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// src/keybindings/parser.ts
|
|
291
|
+
function parseKeystroke(input) {
|
|
292
|
+
const parts = input.split("+");
|
|
293
|
+
const keystroke = {
|
|
294
|
+
key: "",
|
|
295
|
+
ctrl: false,
|
|
296
|
+
alt: false,
|
|
297
|
+
shift: false,
|
|
298
|
+
meta: false,
|
|
299
|
+
super: false
|
|
300
|
+
};
|
|
301
|
+
for (const part of parts) {
|
|
302
|
+
const lower = part.toLowerCase();
|
|
303
|
+
switch (lower) {
|
|
304
|
+
case "ctrl":
|
|
305
|
+
case "control":
|
|
306
|
+
keystroke.ctrl = true;
|
|
307
|
+
break;
|
|
308
|
+
case "alt":
|
|
309
|
+
case "opt":
|
|
310
|
+
case "option":
|
|
311
|
+
keystroke.alt = true;
|
|
312
|
+
break;
|
|
313
|
+
case "shift":
|
|
314
|
+
keystroke.shift = true;
|
|
315
|
+
break;
|
|
316
|
+
case "meta":
|
|
317
|
+
keystroke.meta = true;
|
|
318
|
+
break;
|
|
319
|
+
case "cmd":
|
|
320
|
+
case "command":
|
|
321
|
+
case "super":
|
|
322
|
+
case "win":
|
|
323
|
+
keystroke.super = true;
|
|
324
|
+
break;
|
|
325
|
+
case "esc":
|
|
326
|
+
keystroke.key = "escape";
|
|
327
|
+
break;
|
|
328
|
+
case "return":
|
|
329
|
+
keystroke.key = "enter";
|
|
330
|
+
break;
|
|
331
|
+
case "space":
|
|
332
|
+
keystroke.key = " ";
|
|
333
|
+
break;
|
|
334
|
+
case "\u2191":
|
|
335
|
+
keystroke.key = "up";
|
|
336
|
+
break;
|
|
337
|
+
case "\u2193":
|
|
338
|
+
keystroke.key = "down";
|
|
339
|
+
break;
|
|
340
|
+
case "\u2190":
|
|
341
|
+
keystroke.key = "left";
|
|
342
|
+
break;
|
|
343
|
+
case "\u2192":
|
|
344
|
+
keystroke.key = "right";
|
|
345
|
+
break;
|
|
346
|
+
default:
|
|
347
|
+
keystroke.key = lower;
|
|
348
|
+
break;
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
return keystroke;
|
|
352
|
+
}
|
|
353
|
+
function parseChord(input) {
|
|
354
|
+
if (input === " ") return [parseKeystroke("space")];
|
|
355
|
+
return input.trim().split(/\s+/).map(parseKeystroke);
|
|
356
|
+
}
|
|
357
|
+
function keystrokeToString(ks) {
|
|
358
|
+
const parts = [];
|
|
359
|
+
if (ks.ctrl) parts.push("ctrl");
|
|
360
|
+
if (ks.alt) parts.push("alt");
|
|
361
|
+
if (ks.shift) parts.push("shift");
|
|
362
|
+
if (ks.meta) parts.push("meta");
|
|
363
|
+
if (ks.super) parts.push("cmd");
|
|
364
|
+
const displayKey = keyToDisplayName(ks.key);
|
|
365
|
+
parts.push(displayKey);
|
|
366
|
+
return parts.join("+");
|
|
367
|
+
}
|
|
368
|
+
function keyToDisplayName(key) {
|
|
369
|
+
switch (key) {
|
|
370
|
+
case "escape":
|
|
371
|
+
return "Esc";
|
|
372
|
+
case " ":
|
|
373
|
+
return "Space";
|
|
374
|
+
case "tab":
|
|
375
|
+
return "tab";
|
|
376
|
+
case "enter":
|
|
377
|
+
return "Enter";
|
|
378
|
+
case "backspace":
|
|
379
|
+
return "Backspace";
|
|
380
|
+
case "delete":
|
|
381
|
+
return "Delete";
|
|
382
|
+
case "up":
|
|
383
|
+
return "\u2191";
|
|
384
|
+
case "down":
|
|
385
|
+
return "\u2193";
|
|
386
|
+
case "left":
|
|
387
|
+
return "\u2190";
|
|
388
|
+
case "right":
|
|
389
|
+
return "\u2192";
|
|
390
|
+
case "pageup":
|
|
391
|
+
return "PageUp";
|
|
392
|
+
case "pagedown":
|
|
393
|
+
return "PageDown";
|
|
394
|
+
case "home":
|
|
395
|
+
return "Home";
|
|
396
|
+
case "end":
|
|
397
|
+
return "End";
|
|
398
|
+
default:
|
|
399
|
+
return key;
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
function chordToString(chord) {
|
|
403
|
+
return chord.map(keystrokeToString).join(" ");
|
|
404
|
+
}
|
|
405
|
+
function parseBindings(blocks) {
|
|
406
|
+
const bindings = [];
|
|
407
|
+
for (const block of blocks) {
|
|
408
|
+
for (const [key, action] of Object.entries(block.bindings)) {
|
|
409
|
+
bindings.push({
|
|
410
|
+
chord: parseChord(key),
|
|
411
|
+
action,
|
|
412
|
+
context: block.context
|
|
413
|
+
});
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
return bindings;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
// src/keybindings/resolver.ts
|
|
420
|
+
function getBindingDisplayText(action, context, bindings) {
|
|
421
|
+
const binding = bindings.findLast(
|
|
422
|
+
(b) => b.action === action && b.context === context
|
|
423
|
+
);
|
|
424
|
+
return binding ? chordToString(binding.chord) : void 0;
|
|
425
|
+
}
|
|
426
|
+
function buildKeystroke(input, key) {
|
|
427
|
+
const keyName = getKeyName(input, key);
|
|
428
|
+
if (!keyName) return null;
|
|
429
|
+
const effectiveMeta = key.escape ? false : key.meta;
|
|
430
|
+
return {
|
|
431
|
+
key: keyName,
|
|
432
|
+
ctrl: key.ctrl,
|
|
433
|
+
alt: effectiveMeta,
|
|
434
|
+
shift: key.shift,
|
|
435
|
+
meta: effectiveMeta,
|
|
436
|
+
super: key.super
|
|
437
|
+
};
|
|
438
|
+
}
|
|
439
|
+
function keystrokesEqual(a, b) {
|
|
440
|
+
return a.key === b.key && a.ctrl === b.ctrl && a.shift === b.shift && (a.alt || a.meta) === (b.alt || b.meta) && a.super === b.super;
|
|
441
|
+
}
|
|
442
|
+
function chordPrefixMatches(prefix, binding) {
|
|
443
|
+
if (prefix.length >= binding.chord.length) return false;
|
|
444
|
+
for (let i = 0; i < prefix.length; i++) {
|
|
445
|
+
const prefixKey = prefix[i];
|
|
446
|
+
const bindingKey = binding.chord[i];
|
|
447
|
+
if (!prefixKey || !bindingKey) return false;
|
|
448
|
+
if (!keystrokesEqual(prefixKey, bindingKey)) return false;
|
|
449
|
+
}
|
|
450
|
+
return true;
|
|
451
|
+
}
|
|
452
|
+
function chordExactlyMatches(chord, binding) {
|
|
453
|
+
if (chord.length !== binding.chord.length) return false;
|
|
454
|
+
for (let i = 0; i < chord.length; i++) {
|
|
455
|
+
const chordKey = chord[i];
|
|
456
|
+
const bindingKey = binding.chord[i];
|
|
457
|
+
if (!chordKey || !bindingKey) return false;
|
|
458
|
+
if (!keystrokesEqual(chordKey, bindingKey)) return false;
|
|
459
|
+
}
|
|
460
|
+
return true;
|
|
461
|
+
}
|
|
462
|
+
function resolveKeyWithChordState(input, key, activeContexts, bindings, pending) {
|
|
463
|
+
if (key.escape && pending !== null) {
|
|
464
|
+
return { type: "chord_cancelled" };
|
|
465
|
+
}
|
|
466
|
+
const currentKeystroke = buildKeystroke(input, key);
|
|
467
|
+
if (!currentKeystroke) {
|
|
468
|
+
if (pending !== null) {
|
|
469
|
+
return { type: "chord_cancelled" };
|
|
470
|
+
}
|
|
471
|
+
return { type: "none" };
|
|
472
|
+
}
|
|
473
|
+
const testChord = pending ? [...pending, currentKeystroke] : [currentKeystroke];
|
|
474
|
+
const ctxSet = new Set(activeContexts);
|
|
475
|
+
const contextBindings = bindings.filter((b) => ctxSet.has(b.context));
|
|
476
|
+
const chordWinners = /* @__PURE__ */ new Map();
|
|
477
|
+
for (const binding of contextBindings) {
|
|
478
|
+
if (binding.chord.length > testChord.length && chordPrefixMatches(testChord, binding)) {
|
|
479
|
+
chordWinners.set(chordToString(binding.chord), binding.action);
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
let hasLongerChords = false;
|
|
483
|
+
for (const action of chordWinners.values()) {
|
|
484
|
+
if (action !== null) {
|
|
485
|
+
hasLongerChords = true;
|
|
486
|
+
break;
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
if (hasLongerChords) {
|
|
490
|
+
return { type: "chord_started", pending: testChord };
|
|
491
|
+
}
|
|
492
|
+
let exactMatch;
|
|
493
|
+
for (const binding of contextBindings) {
|
|
494
|
+
if (chordExactlyMatches(testChord, binding)) {
|
|
495
|
+
exactMatch = binding;
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
if (exactMatch) {
|
|
499
|
+
if (exactMatch.action === null) {
|
|
500
|
+
return { type: "unbound" };
|
|
501
|
+
}
|
|
502
|
+
return { type: "match", action: exactMatch.action };
|
|
503
|
+
}
|
|
504
|
+
if (pending !== null) {
|
|
505
|
+
return { type: "chord_cancelled" };
|
|
506
|
+
}
|
|
507
|
+
return { type: "none" };
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
// src/keybindings/KeybindingContext.tsx
|
|
511
|
+
var import_jsx_runtime5 = require("react/jsx-runtime");
|
|
512
|
+
var KeybindingContext = (0, import_react3.createContext)(null);
|
|
513
|
+
function KeybindingProvider({
|
|
514
|
+
bindings,
|
|
515
|
+
pendingChordRef,
|
|
516
|
+
pendingChord,
|
|
517
|
+
setPendingChord,
|
|
518
|
+
activeContexts,
|
|
519
|
+
registerActiveContext,
|
|
520
|
+
unregisterActiveContext,
|
|
521
|
+
handlerRegistryRef,
|
|
522
|
+
children
|
|
523
|
+
}) {
|
|
524
|
+
const getDisplayText = (0, import_react3.useMemo)(
|
|
525
|
+
() => (action, context) => getBindingDisplayText(action, context, bindings),
|
|
526
|
+
[bindings]
|
|
527
|
+
);
|
|
528
|
+
const registerHandler = (0, import_react3.useMemo)(
|
|
529
|
+
() => (registration) => {
|
|
530
|
+
const registry = handlerRegistryRef.current;
|
|
531
|
+
if (!registry) return () => {
|
|
532
|
+
};
|
|
533
|
+
if (!registry.has(registration.action)) {
|
|
534
|
+
registry.set(registration.action, /* @__PURE__ */ new Set());
|
|
535
|
+
}
|
|
536
|
+
registry.get(registration.action).add(registration);
|
|
537
|
+
return () => {
|
|
538
|
+
const handlers = registry.get(registration.action);
|
|
539
|
+
if (handlers) {
|
|
540
|
+
handlers.delete(registration);
|
|
541
|
+
if (handlers.size === 0) {
|
|
542
|
+
registry.delete(registration.action);
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
};
|
|
546
|
+
},
|
|
547
|
+
[handlerRegistryRef]
|
|
548
|
+
);
|
|
549
|
+
const invokeAction = (0, import_react3.useMemo)(
|
|
550
|
+
() => (action) => {
|
|
551
|
+
const registry = handlerRegistryRef.current;
|
|
552
|
+
if (!registry) return false;
|
|
553
|
+
const handlers = registry.get(action);
|
|
554
|
+
if (!handlers || handlers.size === 0) return false;
|
|
555
|
+
for (const registration of handlers) {
|
|
556
|
+
if (activeContexts.has(registration.context)) {
|
|
557
|
+
registration.handler();
|
|
558
|
+
return true;
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
return false;
|
|
562
|
+
},
|
|
563
|
+
[activeContexts, handlerRegistryRef]
|
|
564
|
+
);
|
|
565
|
+
const resolve = (0, import_react3.useMemo)(
|
|
566
|
+
() => (input, key, contexts) => resolveKeyWithChordState(
|
|
567
|
+
input,
|
|
568
|
+
key,
|
|
569
|
+
contexts,
|
|
570
|
+
bindings,
|
|
571
|
+
pendingChordRef.current
|
|
572
|
+
),
|
|
573
|
+
[bindings, pendingChordRef]
|
|
574
|
+
);
|
|
575
|
+
const value = (0, import_react3.useMemo)(
|
|
576
|
+
() => ({
|
|
577
|
+
resolve,
|
|
578
|
+
setPendingChord,
|
|
579
|
+
getDisplayText,
|
|
580
|
+
bindings,
|
|
581
|
+
pendingChord,
|
|
582
|
+
activeContexts,
|
|
583
|
+
registerActiveContext,
|
|
584
|
+
unregisterActiveContext,
|
|
585
|
+
registerHandler,
|
|
586
|
+
invokeAction
|
|
587
|
+
}),
|
|
588
|
+
[
|
|
589
|
+
resolve,
|
|
590
|
+
setPendingChord,
|
|
591
|
+
getDisplayText,
|
|
592
|
+
bindings,
|
|
593
|
+
pendingChord,
|
|
594
|
+
activeContexts,
|
|
595
|
+
registerActiveContext,
|
|
596
|
+
unregisterActiveContext,
|
|
597
|
+
registerHandler,
|
|
598
|
+
invokeAction
|
|
599
|
+
]
|
|
600
|
+
);
|
|
601
|
+
return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(KeybindingContext.Provider, { value, children });
|
|
602
|
+
}
|
|
603
|
+
function useOptionalKeybindingContext() {
|
|
604
|
+
return (0, import_react3.useContext)(KeybindingContext);
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
// src/keybindings/useKeybinding.ts
|
|
608
|
+
function useKeybinding(action, handler, options = {}) {
|
|
609
|
+
const { context = "Global", isActive = true } = options;
|
|
610
|
+
const keybindingContext = useOptionalKeybindingContext();
|
|
611
|
+
(0, import_react4.useEffect)(() => {
|
|
612
|
+
if (!keybindingContext || !isActive) return;
|
|
613
|
+
return keybindingContext.registerHandler({ action, context, handler });
|
|
614
|
+
}, [action, context, handler, keybindingContext, isActive]);
|
|
615
|
+
const handleInput = (0, import_react4.useCallback)(
|
|
616
|
+
(input, key, event) => {
|
|
617
|
+
if (!keybindingContext) return;
|
|
618
|
+
const contextsToCheck = [
|
|
619
|
+
...keybindingContext.activeContexts,
|
|
620
|
+
context,
|
|
621
|
+
"Global"
|
|
622
|
+
];
|
|
623
|
+
const uniqueContexts = [...new Set(contextsToCheck)];
|
|
624
|
+
const result = keybindingContext.resolve(input, key, uniqueContexts);
|
|
625
|
+
switch (result.type) {
|
|
626
|
+
case "match":
|
|
627
|
+
keybindingContext.setPendingChord(null);
|
|
628
|
+
if (result.action === action) {
|
|
629
|
+
if (handler() !== false) {
|
|
630
|
+
event.stopImmediatePropagation();
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
break;
|
|
634
|
+
case "chord_started":
|
|
635
|
+
keybindingContext.setPendingChord(result.pending);
|
|
636
|
+
event.stopImmediatePropagation();
|
|
637
|
+
break;
|
|
638
|
+
case "chord_cancelled":
|
|
639
|
+
keybindingContext.setPendingChord(null);
|
|
640
|
+
break;
|
|
641
|
+
case "unbound":
|
|
642
|
+
keybindingContext.setPendingChord(null);
|
|
643
|
+
event.stopImmediatePropagation();
|
|
644
|
+
break;
|
|
645
|
+
case "none":
|
|
646
|
+
break;
|
|
647
|
+
}
|
|
648
|
+
},
|
|
649
|
+
[action, context, handler, keybindingContext]
|
|
650
|
+
);
|
|
651
|
+
(0, import_ink_renderer5.useInput)(handleInput, { isActive });
|
|
652
|
+
}
|
|
653
|
+
function useKeybindings(handlers, options = {}) {
|
|
654
|
+
const { context = "Global", isActive = true } = options;
|
|
655
|
+
const keybindingContext = useOptionalKeybindingContext();
|
|
656
|
+
(0, import_react4.useEffect)(() => {
|
|
657
|
+
if (!keybindingContext || !isActive) return;
|
|
658
|
+
const unregisterFns = [];
|
|
659
|
+
for (const [action, handler] of Object.entries(handlers)) {
|
|
660
|
+
unregisterFns.push(
|
|
661
|
+
keybindingContext.registerHandler({ action, context, handler })
|
|
662
|
+
);
|
|
663
|
+
}
|
|
664
|
+
return () => {
|
|
665
|
+
for (const unregister of unregisterFns) {
|
|
666
|
+
unregister();
|
|
667
|
+
}
|
|
668
|
+
};
|
|
669
|
+
}, [context, handlers, keybindingContext, isActive]);
|
|
670
|
+
const handleInput = (0, import_react4.useCallback)(
|
|
671
|
+
(input, key, event) => {
|
|
672
|
+
if (!keybindingContext) return;
|
|
673
|
+
const contextsToCheck = [
|
|
674
|
+
...keybindingContext.activeContexts,
|
|
675
|
+
context,
|
|
676
|
+
"Global"
|
|
677
|
+
];
|
|
678
|
+
const uniqueContexts = [...new Set(contextsToCheck)];
|
|
679
|
+
const result = keybindingContext.resolve(input, key, uniqueContexts);
|
|
680
|
+
switch (result.type) {
|
|
681
|
+
case "match":
|
|
682
|
+
keybindingContext.setPendingChord(null);
|
|
683
|
+
if (result.action in handlers) {
|
|
684
|
+
const handler = handlers[result.action];
|
|
685
|
+
if (handler && handler() !== false) {
|
|
686
|
+
event.stopImmediatePropagation();
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
break;
|
|
690
|
+
case "chord_started":
|
|
691
|
+
keybindingContext.setPendingChord(result.pending);
|
|
692
|
+
event.stopImmediatePropagation();
|
|
693
|
+
break;
|
|
694
|
+
case "chord_cancelled":
|
|
695
|
+
keybindingContext.setPendingChord(null);
|
|
696
|
+
break;
|
|
697
|
+
case "unbound":
|
|
698
|
+
keybindingContext.setPendingChord(null);
|
|
699
|
+
event.stopImmediatePropagation();
|
|
700
|
+
break;
|
|
701
|
+
case "none":
|
|
702
|
+
break;
|
|
703
|
+
}
|
|
704
|
+
},
|
|
705
|
+
[context, handlers, keybindingContext]
|
|
706
|
+
);
|
|
707
|
+
(0, import_ink_renderer5.useInput)(handleInput, { isActive });
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
// src/keybindings/KeybindingProviderSetup.tsx
|
|
711
|
+
var import_react5 = require("react");
|
|
712
|
+
var import_ink_renderer6 = require("@claude-code-kit/ink-renderer");
|
|
713
|
+
|
|
714
|
+
// src/keybindings/loadUserBindings.ts
|
|
715
|
+
var import_chokidar = __toESM(require("chokidar"));
|
|
716
|
+
var import_fs = require("fs");
|
|
717
|
+
var import_promises = require("fs/promises");
|
|
718
|
+
var import_path = require("path");
|
|
719
|
+
|
|
720
|
+
// src/keybindings/defaultBindings.ts
|
|
721
|
+
var import_shared = require("@claude-code-kit/shared");
|
|
722
|
+
var feature = () => false;
|
|
723
|
+
var getPlatform = () => process.platform === "darwin" ? "macos" : process.platform === "win32" ? "windows" : "linux";
|
|
724
|
+
var isRunningWithBun = () => false;
|
|
725
|
+
var IMAGE_PASTE_KEY = getPlatform() === "windows" ? "alt+v" : "ctrl+v";
|
|
726
|
+
var SUPPORTS_TERMINAL_VT_MODE = getPlatform() !== "windows" || (isRunningWithBun() ? (0, import_shared.satisfies)(process.versions.bun ?? "0.0.0", ">=1.2.23") : (0, import_shared.satisfies)(process.versions.node, ">=22.17.0 <23.0.0 || >=24.2.0"));
|
|
727
|
+
var MODE_CYCLE_KEY = SUPPORTS_TERMINAL_VT_MODE ? "shift+tab" : "meta+m";
|
|
728
|
+
var DEFAULT_BINDINGS = [
|
|
729
|
+
{
|
|
730
|
+
context: "Global",
|
|
731
|
+
bindings: {
|
|
732
|
+
// ctrl+c and ctrl+d use special time-based double-press handling.
|
|
733
|
+
// They ARE defined here so the resolver can find them, but they
|
|
734
|
+
// CANNOT be rebound by users - validation in reservedShortcuts.ts
|
|
735
|
+
// will show an error if users try to override these keys.
|
|
736
|
+
"ctrl+c": "app:interrupt",
|
|
737
|
+
"ctrl+d": "app:exit",
|
|
738
|
+
"ctrl+l": "app:redraw",
|
|
739
|
+
"ctrl+t": "app:toggleTodos",
|
|
740
|
+
"ctrl+o": "app:toggleTranscript",
|
|
741
|
+
...feature() ? { "ctrl+shift+b": "app:toggleBrief" } : {},
|
|
742
|
+
"ctrl+shift+o": "app:toggleTeammatePreview",
|
|
743
|
+
"ctrl+r": "history:search",
|
|
744
|
+
// File navigation. cmd+ bindings only fire on kitty-protocol terminals;
|
|
745
|
+
// ctrl+shift is the portable fallback.
|
|
746
|
+
...feature() ? {
|
|
747
|
+
"ctrl+shift+f": "app:globalSearch",
|
|
748
|
+
"cmd+shift+f": "app:globalSearch",
|
|
749
|
+
"ctrl+shift+p": "app:quickOpen",
|
|
750
|
+
"cmd+shift+p": "app:quickOpen"
|
|
751
|
+
} : {},
|
|
752
|
+
...feature() ? { "meta+j": "app:toggleTerminal" } : {}
|
|
753
|
+
}
|
|
754
|
+
},
|
|
755
|
+
{
|
|
756
|
+
context: "Chat",
|
|
757
|
+
bindings: {
|
|
758
|
+
escape: "chat:cancel",
|
|
759
|
+
// ctrl+x chord prefix avoids shadowing readline editing keys (ctrl+a/b/e/f/...).
|
|
760
|
+
"ctrl+x ctrl+k": "chat:killAgents",
|
|
761
|
+
[MODE_CYCLE_KEY]: "chat:cycleMode",
|
|
762
|
+
"meta+p": "chat:modelPicker",
|
|
763
|
+
"meta+o": "chat:fastMode",
|
|
764
|
+
"meta+t": "chat:thinkingToggle",
|
|
765
|
+
enter: "chat:submit",
|
|
766
|
+
up: "history:previous",
|
|
767
|
+
down: "history:next",
|
|
768
|
+
// Undo has two bindings to support different terminal behaviors:
|
|
769
|
+
// - ctrl+_ for legacy terminals (send \x1f control char)
|
|
770
|
+
// - ctrl+shift+- for Kitty protocol (sends physical key with modifiers)
|
|
771
|
+
"ctrl+_": "chat:undo",
|
|
772
|
+
"ctrl+shift+-": "chat:undo",
|
|
773
|
+
// ctrl+x ctrl+e is the readline-native edit-and-execute-command binding.
|
|
774
|
+
"ctrl+x ctrl+e": "chat:externalEditor",
|
|
775
|
+
"ctrl+g": "chat:externalEditor",
|
|
776
|
+
"ctrl+s": "chat:stash",
|
|
777
|
+
// Image paste shortcut (platform-specific key defined above)
|
|
778
|
+
[IMAGE_PASTE_KEY]: "chat:imagePaste",
|
|
779
|
+
...feature() ? { "shift+up": "chat:messageActions" } : {},
|
|
780
|
+
// Voice activation (hold-to-talk). Registered so getShortcutDisplay
|
|
781
|
+
// finds it without hitting the fallback analytics log. To rebind,
|
|
782
|
+
// add a voice:pushToTalk entry (last wins); to disable, use /voice
|
|
783
|
+
// — null-unbinding space hits a pre-existing useKeybinding.ts trap
|
|
784
|
+
// where 'unbound' swallows the event (space dead for typing).
|
|
785
|
+
...feature() ? { space: "voice:pushToTalk" } : {}
|
|
786
|
+
}
|
|
787
|
+
},
|
|
788
|
+
{
|
|
789
|
+
context: "Autocomplete",
|
|
790
|
+
bindings: {
|
|
791
|
+
tab: "autocomplete:accept",
|
|
792
|
+
escape: "autocomplete:dismiss",
|
|
793
|
+
up: "autocomplete:previous",
|
|
794
|
+
down: "autocomplete:next"
|
|
795
|
+
}
|
|
796
|
+
},
|
|
797
|
+
{
|
|
798
|
+
context: "Settings",
|
|
799
|
+
bindings: {
|
|
800
|
+
// Settings menu uses escape only (not 'n') to dismiss
|
|
801
|
+
escape: "confirm:no",
|
|
802
|
+
// Config panel list navigation (reuses Select actions)
|
|
803
|
+
up: "select:previous",
|
|
804
|
+
down: "select:next",
|
|
805
|
+
k: "select:previous",
|
|
806
|
+
j: "select:next",
|
|
807
|
+
"ctrl+p": "select:previous",
|
|
808
|
+
"ctrl+n": "select:next",
|
|
809
|
+
// Toggle/activate the selected setting (space only — enter saves & closes)
|
|
810
|
+
space: "select:accept",
|
|
811
|
+
// Save and close the config panel
|
|
812
|
+
enter: "settings:close",
|
|
813
|
+
// Enter search mode
|
|
814
|
+
"/": "settings:search",
|
|
815
|
+
// Retry loading usage data (only active on error)
|
|
816
|
+
r: "settings:retry"
|
|
817
|
+
}
|
|
818
|
+
},
|
|
819
|
+
{
|
|
820
|
+
context: "Confirmation",
|
|
821
|
+
bindings: {
|
|
822
|
+
y: "confirm:yes",
|
|
823
|
+
n: "confirm:no",
|
|
824
|
+
enter: "confirm:yes",
|
|
825
|
+
escape: "confirm:no",
|
|
826
|
+
// Navigation for dialogs with lists
|
|
827
|
+
up: "confirm:previous",
|
|
828
|
+
down: "confirm:next",
|
|
829
|
+
tab: "confirm:nextField",
|
|
830
|
+
space: "confirm:toggle",
|
|
831
|
+
// Cycle modes (used in file permission dialogs and teams dialog)
|
|
832
|
+
"shift+tab": "confirm:cycleMode",
|
|
833
|
+
// Toggle permission explanation in permission dialogs
|
|
834
|
+
"ctrl+e": "confirm:toggleExplanation",
|
|
835
|
+
// Toggle permission debug info
|
|
836
|
+
"ctrl+d": "permission:toggleDebug"
|
|
837
|
+
}
|
|
838
|
+
},
|
|
839
|
+
{
|
|
840
|
+
context: "Tabs",
|
|
841
|
+
bindings: {
|
|
842
|
+
// Tab cycling navigation
|
|
843
|
+
tab: "tabs:next",
|
|
844
|
+
"shift+tab": "tabs:previous",
|
|
845
|
+
right: "tabs:next",
|
|
846
|
+
left: "tabs:previous"
|
|
847
|
+
}
|
|
848
|
+
},
|
|
849
|
+
{
|
|
850
|
+
context: "Transcript",
|
|
851
|
+
bindings: {
|
|
852
|
+
"ctrl+e": "transcript:toggleShowAll",
|
|
853
|
+
"ctrl+c": "transcript:exit",
|
|
854
|
+
escape: "transcript:exit",
|
|
855
|
+
// q — pager convention (less, tmux copy-mode). Transcript is a modal
|
|
856
|
+
// reading view with no prompt, so q-as-literal-char has no owner.
|
|
857
|
+
q: "transcript:exit"
|
|
858
|
+
}
|
|
859
|
+
},
|
|
860
|
+
{
|
|
861
|
+
context: "HistorySearch",
|
|
862
|
+
bindings: {
|
|
863
|
+
"ctrl+r": "historySearch:next",
|
|
864
|
+
escape: "historySearch:accept",
|
|
865
|
+
tab: "historySearch:accept",
|
|
866
|
+
"ctrl+c": "historySearch:cancel",
|
|
867
|
+
enter: "historySearch:execute"
|
|
868
|
+
}
|
|
869
|
+
},
|
|
870
|
+
{
|
|
871
|
+
context: "Task",
|
|
872
|
+
bindings: {
|
|
873
|
+
// Background running foreground tasks (bash commands, agents)
|
|
874
|
+
// In tmux, users must press ctrl+b twice (tmux prefix escape)
|
|
875
|
+
"ctrl+b": "task:background"
|
|
876
|
+
}
|
|
877
|
+
},
|
|
878
|
+
{
|
|
879
|
+
context: "ThemePicker",
|
|
880
|
+
bindings: {
|
|
881
|
+
"ctrl+t": "theme:toggleSyntaxHighlighting"
|
|
882
|
+
}
|
|
883
|
+
},
|
|
884
|
+
{
|
|
885
|
+
context: "Scroll",
|
|
886
|
+
bindings: {
|
|
887
|
+
pageup: "scroll:pageUp",
|
|
888
|
+
pagedown: "scroll:pageDown",
|
|
889
|
+
wheelup: "scroll:lineUp",
|
|
890
|
+
wheeldown: "scroll:lineDown",
|
|
891
|
+
"ctrl+home": "scroll:top",
|
|
892
|
+
"ctrl+end": "scroll:bottom",
|
|
893
|
+
// Selection copy. ctrl+shift+c is standard terminal copy.
|
|
894
|
+
// cmd+c only fires on terminals using the kitty keyboard
|
|
895
|
+
// protocol (kitty/WezTerm/ghostty/iTerm2) where the super
|
|
896
|
+
// modifier actually reaches the pty — inert elsewhere.
|
|
897
|
+
// Esc-to-clear and contextual ctrl+c are handled via raw
|
|
898
|
+
// useInput so they can conditionally propagate.
|
|
899
|
+
"ctrl+shift+c": "selection:copy",
|
|
900
|
+
"cmd+c": "selection:copy"
|
|
901
|
+
}
|
|
902
|
+
},
|
|
903
|
+
{
|
|
904
|
+
context: "Help",
|
|
905
|
+
bindings: {
|
|
906
|
+
escape: "help:dismiss"
|
|
907
|
+
}
|
|
908
|
+
},
|
|
909
|
+
// Attachment navigation (select dialog image attachments)
|
|
910
|
+
{
|
|
911
|
+
context: "Attachments",
|
|
912
|
+
bindings: {
|
|
913
|
+
right: "attachments:next",
|
|
914
|
+
left: "attachments:previous",
|
|
915
|
+
backspace: "attachments:remove",
|
|
916
|
+
delete: "attachments:remove",
|
|
917
|
+
down: "attachments:exit",
|
|
918
|
+
escape: "attachments:exit"
|
|
919
|
+
}
|
|
920
|
+
},
|
|
921
|
+
// Footer indicator navigation (tasks, teams, diff, loop)
|
|
922
|
+
{
|
|
923
|
+
context: "Footer",
|
|
924
|
+
bindings: {
|
|
925
|
+
up: "footer:up",
|
|
926
|
+
"ctrl+p": "footer:up",
|
|
927
|
+
down: "footer:down",
|
|
928
|
+
"ctrl+n": "footer:down",
|
|
929
|
+
right: "footer:next",
|
|
930
|
+
left: "footer:previous",
|
|
931
|
+
enter: "footer:openSelected",
|
|
932
|
+
escape: "footer:clearSelection"
|
|
933
|
+
}
|
|
934
|
+
},
|
|
935
|
+
// Message selector (rewind dialog) navigation
|
|
936
|
+
{
|
|
937
|
+
context: "MessageSelector",
|
|
938
|
+
bindings: {
|
|
939
|
+
up: "messageSelector:up",
|
|
940
|
+
down: "messageSelector:down",
|
|
941
|
+
k: "messageSelector:up",
|
|
942
|
+
j: "messageSelector:down",
|
|
943
|
+
"ctrl+p": "messageSelector:up",
|
|
944
|
+
"ctrl+n": "messageSelector:down",
|
|
945
|
+
"ctrl+up": "messageSelector:top",
|
|
946
|
+
"shift+up": "messageSelector:top",
|
|
947
|
+
"meta+up": "messageSelector:top",
|
|
948
|
+
"shift+k": "messageSelector:top",
|
|
949
|
+
"ctrl+down": "messageSelector:bottom",
|
|
950
|
+
"shift+down": "messageSelector:bottom",
|
|
951
|
+
"meta+down": "messageSelector:bottom",
|
|
952
|
+
"shift+j": "messageSelector:bottom",
|
|
953
|
+
enter: "messageSelector:select"
|
|
954
|
+
}
|
|
955
|
+
},
|
|
956
|
+
// Diff dialog navigation
|
|
957
|
+
{
|
|
958
|
+
context: "DiffDialog",
|
|
959
|
+
bindings: {
|
|
960
|
+
escape: "diff:dismiss",
|
|
961
|
+
left: "diff:previousSource",
|
|
962
|
+
right: "diff:nextSource",
|
|
963
|
+
up: "diff:previousFile",
|
|
964
|
+
down: "diff:nextFile",
|
|
965
|
+
enter: "diff:viewDetails"
|
|
966
|
+
// Note: diff:back is handled by left arrow in detail mode
|
|
967
|
+
}
|
|
968
|
+
},
|
|
969
|
+
// Model picker effort cycling (ant-only)
|
|
970
|
+
{
|
|
971
|
+
context: "ModelPicker",
|
|
972
|
+
bindings: {
|
|
973
|
+
left: "modelPicker:decreaseEffort",
|
|
974
|
+
right: "modelPicker:increaseEffort"
|
|
975
|
+
}
|
|
976
|
+
},
|
|
977
|
+
// Select component navigation (used by /model, /resume, permission prompts, etc.)
|
|
978
|
+
{
|
|
979
|
+
context: "Select",
|
|
980
|
+
bindings: {
|
|
981
|
+
up: "select:previous",
|
|
982
|
+
down: "select:next",
|
|
983
|
+
j: "select:next",
|
|
984
|
+
k: "select:previous",
|
|
985
|
+
"ctrl+n": "select:next",
|
|
986
|
+
"ctrl+p": "select:previous",
|
|
987
|
+
enter: "select:accept",
|
|
988
|
+
escape: "select:cancel"
|
|
989
|
+
}
|
|
990
|
+
},
|
|
991
|
+
// Plugin dialog actions (manage, browse, discover plugins)
|
|
992
|
+
// Navigation (select:*) uses the Select context above
|
|
993
|
+
{
|
|
994
|
+
context: "Plugin",
|
|
995
|
+
bindings: {
|
|
996
|
+
space: "plugin:toggle",
|
|
997
|
+
i: "plugin:install"
|
|
998
|
+
}
|
|
999
|
+
}
|
|
1000
|
+
];
|
|
1001
|
+
|
|
1002
|
+
// src/keybindings/reservedShortcuts.ts
|
|
1003
|
+
var getPlatform2 = () => process.platform === "darwin" ? "macos" : process.platform === "win32" ? "windows" : "linux";
|
|
1004
|
+
var NON_REBINDABLE = [
|
|
1005
|
+
{
|
|
1006
|
+
key: "ctrl+c",
|
|
1007
|
+
reason: "Cannot be rebound - used for interrupt/exit (hardcoded)",
|
|
1008
|
+
severity: "error"
|
|
1009
|
+
},
|
|
1010
|
+
{
|
|
1011
|
+
key: "ctrl+d",
|
|
1012
|
+
reason: "Cannot be rebound - used for exit (hardcoded)",
|
|
1013
|
+
severity: "error"
|
|
1014
|
+
},
|
|
1015
|
+
{
|
|
1016
|
+
key: "ctrl+m",
|
|
1017
|
+
reason: "Cannot be rebound - identical to Enter in terminals (both send CR)",
|
|
1018
|
+
severity: "error"
|
|
1019
|
+
}
|
|
1020
|
+
];
|
|
1021
|
+
var TERMINAL_RESERVED = [
|
|
1022
|
+
{
|
|
1023
|
+
key: "ctrl+z",
|
|
1024
|
+
reason: "Unix process suspend (SIGTSTP)",
|
|
1025
|
+
severity: "warning"
|
|
1026
|
+
},
|
|
1027
|
+
{
|
|
1028
|
+
key: "ctrl+\\",
|
|
1029
|
+
reason: "Terminal quit signal (SIGQUIT)",
|
|
1030
|
+
severity: "error"
|
|
1031
|
+
}
|
|
1032
|
+
];
|
|
1033
|
+
var MACOS_RESERVED = [
|
|
1034
|
+
{ key: "cmd+c", reason: "macOS system copy", severity: "error" },
|
|
1035
|
+
{ key: "cmd+v", reason: "macOS system paste", severity: "error" },
|
|
1036
|
+
{ key: "cmd+x", reason: "macOS system cut", severity: "error" },
|
|
1037
|
+
{ key: "cmd+q", reason: "macOS quit application", severity: "error" },
|
|
1038
|
+
{ key: "cmd+w", reason: "macOS close window/tab", severity: "error" },
|
|
1039
|
+
{ key: "cmd+tab", reason: "macOS app switcher", severity: "error" },
|
|
1040
|
+
{ key: "cmd+space", reason: "macOS Spotlight", severity: "error" }
|
|
1041
|
+
];
|
|
1042
|
+
function getReservedShortcuts() {
|
|
1043
|
+
const platform = getPlatform2();
|
|
1044
|
+
const reserved = [...NON_REBINDABLE, ...TERMINAL_RESERVED];
|
|
1045
|
+
if (platform === "macos") {
|
|
1046
|
+
reserved.push(...MACOS_RESERVED);
|
|
1047
|
+
}
|
|
1048
|
+
return reserved;
|
|
1049
|
+
}
|
|
1050
|
+
function normalizeKeyForComparison(key) {
|
|
1051
|
+
return key.trim().split(/\s+/).map(normalizeStep).join(" ");
|
|
1052
|
+
}
|
|
1053
|
+
function normalizeStep(step) {
|
|
1054
|
+
const parts = step.split("+");
|
|
1055
|
+
const modifiers = [];
|
|
1056
|
+
let mainKey = "";
|
|
1057
|
+
for (const part of parts) {
|
|
1058
|
+
const lower = part.trim().toLowerCase();
|
|
1059
|
+
if ([
|
|
1060
|
+
"ctrl",
|
|
1061
|
+
"control",
|
|
1062
|
+
"alt",
|
|
1063
|
+
"opt",
|
|
1064
|
+
"option",
|
|
1065
|
+
"meta",
|
|
1066
|
+
"cmd",
|
|
1067
|
+
"command",
|
|
1068
|
+
"shift"
|
|
1069
|
+
].includes(lower)) {
|
|
1070
|
+
if (lower === "control") modifiers.push("ctrl");
|
|
1071
|
+
else if (lower === "option" || lower === "opt") modifiers.push("alt");
|
|
1072
|
+
else if (lower === "command" || lower === "cmd") modifiers.push("cmd");
|
|
1073
|
+
else modifiers.push(lower);
|
|
1074
|
+
} else {
|
|
1075
|
+
mainKey = lower;
|
|
1076
|
+
}
|
|
1077
|
+
}
|
|
1078
|
+
modifiers.sort();
|
|
1079
|
+
return [...modifiers, mainKey].join("+");
|
|
1080
|
+
}
|
|
1081
|
+
|
|
1082
|
+
// src/keybindings/validate.ts
|
|
1083
|
+
function isKeybindingBlock(obj) {
|
|
1084
|
+
if (typeof obj !== "object" || obj === null) return false;
|
|
1085
|
+
const b = obj;
|
|
1086
|
+
return typeof b.context === "string" && typeof b.bindings === "object" && b.bindings !== null;
|
|
1087
|
+
}
|
|
1088
|
+
function isKeybindingBlockArray(arr) {
|
|
1089
|
+
return Array.isArray(arr) && arr.every(isKeybindingBlock);
|
|
1090
|
+
}
|
|
1091
|
+
var VALID_CONTEXTS = [
|
|
1092
|
+
"Global",
|
|
1093
|
+
"Chat",
|
|
1094
|
+
"Autocomplete",
|
|
1095
|
+
"Confirmation",
|
|
1096
|
+
"Help",
|
|
1097
|
+
"Transcript",
|
|
1098
|
+
"HistorySearch",
|
|
1099
|
+
"Task",
|
|
1100
|
+
"ThemePicker",
|
|
1101
|
+
"Settings",
|
|
1102
|
+
"Tabs",
|
|
1103
|
+
"Attachments",
|
|
1104
|
+
"Footer",
|
|
1105
|
+
"MessageSelector",
|
|
1106
|
+
"DiffDialog",
|
|
1107
|
+
"ModelPicker",
|
|
1108
|
+
"Select",
|
|
1109
|
+
"Plugin"
|
|
1110
|
+
];
|
|
1111
|
+
function isValidContext(value) {
|
|
1112
|
+
return VALID_CONTEXTS.includes(value);
|
|
1113
|
+
}
|
|
1114
|
+
function validateKeystroke(keystroke) {
|
|
1115
|
+
const parts = keystroke.toLowerCase().split("+");
|
|
1116
|
+
for (const part of parts) {
|
|
1117
|
+
const trimmed = part.trim();
|
|
1118
|
+
if (!trimmed) {
|
|
1119
|
+
return {
|
|
1120
|
+
type: "parse_error",
|
|
1121
|
+
severity: "error",
|
|
1122
|
+
message: `Empty key part in "${keystroke}"`,
|
|
1123
|
+
key: keystroke,
|
|
1124
|
+
suggestion: 'Remove extra "+" characters'
|
|
1125
|
+
};
|
|
1126
|
+
}
|
|
1127
|
+
}
|
|
1128
|
+
const parsed = parseKeystroke(keystroke);
|
|
1129
|
+
if (!parsed.key && !parsed.ctrl && !parsed.alt && !parsed.shift && !parsed.meta) {
|
|
1130
|
+
return {
|
|
1131
|
+
type: "parse_error",
|
|
1132
|
+
severity: "error",
|
|
1133
|
+
message: `Could not parse keystroke "${keystroke}"`,
|
|
1134
|
+
key: keystroke
|
|
1135
|
+
};
|
|
1136
|
+
}
|
|
1137
|
+
return null;
|
|
1138
|
+
}
|
|
1139
|
+
function validateBlock(block, blockIndex) {
|
|
1140
|
+
const warnings = [];
|
|
1141
|
+
if (typeof block !== "object" || block === null) {
|
|
1142
|
+
warnings.push({
|
|
1143
|
+
type: "parse_error",
|
|
1144
|
+
severity: "error",
|
|
1145
|
+
message: `Keybinding block ${blockIndex + 1} is not an object`
|
|
1146
|
+
});
|
|
1147
|
+
return warnings;
|
|
1148
|
+
}
|
|
1149
|
+
const b = block;
|
|
1150
|
+
const rawContext = b.context;
|
|
1151
|
+
let contextName;
|
|
1152
|
+
if (typeof rawContext !== "string") {
|
|
1153
|
+
warnings.push({
|
|
1154
|
+
type: "parse_error",
|
|
1155
|
+
severity: "error",
|
|
1156
|
+
message: `Keybinding block ${blockIndex + 1} missing "context" field`
|
|
1157
|
+
});
|
|
1158
|
+
} else if (!isValidContext(rawContext)) {
|
|
1159
|
+
warnings.push({
|
|
1160
|
+
type: "invalid_context",
|
|
1161
|
+
severity: "error",
|
|
1162
|
+
message: `Unknown context "${rawContext}"`,
|
|
1163
|
+
context: rawContext,
|
|
1164
|
+
suggestion: `Valid contexts: ${VALID_CONTEXTS.join(", ")}`
|
|
1165
|
+
});
|
|
1166
|
+
} else {
|
|
1167
|
+
contextName = rawContext;
|
|
1168
|
+
}
|
|
1169
|
+
if (typeof b.bindings !== "object" || b.bindings === null) {
|
|
1170
|
+
warnings.push({
|
|
1171
|
+
type: "parse_error",
|
|
1172
|
+
severity: "error",
|
|
1173
|
+
message: `Keybinding block ${blockIndex + 1} missing "bindings" field`
|
|
1174
|
+
});
|
|
1175
|
+
return warnings;
|
|
1176
|
+
}
|
|
1177
|
+
const bindings = b.bindings;
|
|
1178
|
+
for (const [key, action] of Object.entries(bindings)) {
|
|
1179
|
+
const keyError = validateKeystroke(key);
|
|
1180
|
+
if (keyError) {
|
|
1181
|
+
keyError.context = contextName;
|
|
1182
|
+
warnings.push(keyError);
|
|
1183
|
+
}
|
|
1184
|
+
if (action !== null && typeof action !== "string") {
|
|
1185
|
+
warnings.push({
|
|
1186
|
+
type: "invalid_action",
|
|
1187
|
+
severity: "error",
|
|
1188
|
+
message: `Invalid action for "${key}": must be a string or null`,
|
|
1189
|
+
key,
|
|
1190
|
+
context: contextName
|
|
1191
|
+
});
|
|
1192
|
+
} else if (typeof action === "string" && action.startsWith("command:")) {
|
|
1193
|
+
if (!/^command:[a-zA-Z0-9:\-_]+$/.test(action)) {
|
|
1194
|
+
warnings.push({
|
|
1195
|
+
type: "invalid_action",
|
|
1196
|
+
severity: "warning",
|
|
1197
|
+
message: `Invalid command binding "${action}" for "${key}": command name may only contain alphanumeric characters, colons, hyphens, and underscores`,
|
|
1198
|
+
key,
|
|
1199
|
+
context: contextName,
|
|
1200
|
+
action
|
|
1201
|
+
});
|
|
1202
|
+
}
|
|
1203
|
+
if (contextName && contextName !== "Chat") {
|
|
1204
|
+
warnings.push({
|
|
1205
|
+
type: "invalid_action",
|
|
1206
|
+
severity: "warning",
|
|
1207
|
+
message: `Command binding "${action}" must be in "Chat" context, not "${contextName}"`,
|
|
1208
|
+
key,
|
|
1209
|
+
context: contextName,
|
|
1210
|
+
action,
|
|
1211
|
+
suggestion: 'Move this binding to a block with "context": "Chat"'
|
|
1212
|
+
});
|
|
1213
|
+
}
|
|
1214
|
+
} else if (action === "voice:pushToTalk") {
|
|
1215
|
+
const ks = parseChord(key)[0];
|
|
1216
|
+
if (ks && !ks.ctrl && !ks.alt && !ks.shift && !ks.meta && !ks.super && /^[a-z]$/.test(ks.key)) {
|
|
1217
|
+
warnings.push({
|
|
1218
|
+
type: "invalid_action",
|
|
1219
|
+
severity: "warning",
|
|
1220
|
+
message: `Binding "${key}" to voice:pushToTalk prints into the input during warmup; use space or a modifier combo like meta+k`,
|
|
1221
|
+
key,
|
|
1222
|
+
context: contextName,
|
|
1223
|
+
action
|
|
1224
|
+
});
|
|
1225
|
+
}
|
|
1226
|
+
}
|
|
1227
|
+
}
|
|
1228
|
+
return warnings;
|
|
1229
|
+
}
|
|
1230
|
+
function checkDuplicateKeysInJson(jsonString) {
|
|
1231
|
+
const warnings = [];
|
|
1232
|
+
const bindingsBlockPattern = /"bindings"\s*:\s*\{([^{}]*(?:\{[^{}]*\}[^{}]*)*)\}/g;
|
|
1233
|
+
let blockMatch;
|
|
1234
|
+
while ((blockMatch = bindingsBlockPattern.exec(jsonString)) !== null) {
|
|
1235
|
+
const blockContent = blockMatch[1];
|
|
1236
|
+
if (!blockContent) continue;
|
|
1237
|
+
const textBeforeBlock = jsonString.slice(0, blockMatch.index);
|
|
1238
|
+
const contextMatch = textBeforeBlock.match(
|
|
1239
|
+
/"context"\s*:\s*"([^"]+)"[^{]*$/
|
|
1240
|
+
);
|
|
1241
|
+
const context = contextMatch?.[1] ?? "unknown";
|
|
1242
|
+
const keyPattern = /"([^"]+)"\s*:/g;
|
|
1243
|
+
const keysByName = /* @__PURE__ */ new Map();
|
|
1244
|
+
let keyMatch;
|
|
1245
|
+
while ((keyMatch = keyPattern.exec(blockContent)) !== null) {
|
|
1246
|
+
const key = keyMatch[1];
|
|
1247
|
+
if (!key) continue;
|
|
1248
|
+
const count = (keysByName.get(key) ?? 0) + 1;
|
|
1249
|
+
keysByName.set(key, count);
|
|
1250
|
+
if (count === 2) {
|
|
1251
|
+
warnings.push({
|
|
1252
|
+
type: "duplicate",
|
|
1253
|
+
severity: "warning",
|
|
1254
|
+
message: `Duplicate key "${key}" in ${context} bindings`,
|
|
1255
|
+
key,
|
|
1256
|
+
context,
|
|
1257
|
+
suggestion: `This key appears multiple times in the same context. JSON uses the last value, earlier values are ignored.`
|
|
1258
|
+
});
|
|
1259
|
+
}
|
|
1260
|
+
}
|
|
1261
|
+
}
|
|
1262
|
+
return warnings;
|
|
1263
|
+
}
|
|
1264
|
+
function validateUserConfig(userBlocks) {
|
|
1265
|
+
const warnings = [];
|
|
1266
|
+
if (!Array.isArray(userBlocks)) {
|
|
1267
|
+
warnings.push({
|
|
1268
|
+
type: "parse_error",
|
|
1269
|
+
severity: "error",
|
|
1270
|
+
message: "keybindings.json must contain an array",
|
|
1271
|
+
suggestion: "Wrap your bindings in [ ]"
|
|
1272
|
+
});
|
|
1273
|
+
return warnings;
|
|
1274
|
+
}
|
|
1275
|
+
for (let i = 0; i < userBlocks.length; i++) {
|
|
1276
|
+
warnings.push(...validateBlock(userBlocks[i], i));
|
|
1277
|
+
}
|
|
1278
|
+
return warnings;
|
|
1279
|
+
}
|
|
1280
|
+
function checkDuplicates(blocks) {
|
|
1281
|
+
const warnings = [];
|
|
1282
|
+
const seenByContext = /* @__PURE__ */ new Map();
|
|
1283
|
+
for (const block of blocks) {
|
|
1284
|
+
const contextMap = seenByContext.get(block.context) ?? /* @__PURE__ */ new Map();
|
|
1285
|
+
seenByContext.set(block.context, contextMap);
|
|
1286
|
+
for (const [key, action] of Object.entries(block.bindings)) {
|
|
1287
|
+
const normalizedKey = normalizeKeyForComparison(key);
|
|
1288
|
+
const existingAction = contextMap.get(normalizedKey);
|
|
1289
|
+
if (existingAction && existingAction !== action) {
|
|
1290
|
+
warnings.push({
|
|
1291
|
+
type: "duplicate",
|
|
1292
|
+
severity: "warning",
|
|
1293
|
+
message: `Duplicate binding "${key}" in ${block.context} context`,
|
|
1294
|
+
key,
|
|
1295
|
+
context: block.context,
|
|
1296
|
+
action: action ?? "null (unbind)",
|
|
1297
|
+
suggestion: `Previously bound to "${existingAction}". Only the last binding will be used.`
|
|
1298
|
+
});
|
|
1299
|
+
}
|
|
1300
|
+
contextMap.set(normalizedKey, action ?? "null");
|
|
1301
|
+
}
|
|
1302
|
+
}
|
|
1303
|
+
return warnings;
|
|
1304
|
+
}
|
|
1305
|
+
function checkReservedShortcuts(bindings) {
|
|
1306
|
+
const warnings = [];
|
|
1307
|
+
const reserved = getReservedShortcuts();
|
|
1308
|
+
for (const binding of bindings) {
|
|
1309
|
+
const keyDisplay = chordToString(binding.chord);
|
|
1310
|
+
const normalizedKey = normalizeKeyForComparison(keyDisplay);
|
|
1311
|
+
for (const res of reserved) {
|
|
1312
|
+
if (normalizeKeyForComparison(res.key) === normalizedKey) {
|
|
1313
|
+
warnings.push({
|
|
1314
|
+
type: "reserved",
|
|
1315
|
+
severity: res.severity,
|
|
1316
|
+
message: `"${keyDisplay}" may not work: ${res.reason}`,
|
|
1317
|
+
key: keyDisplay,
|
|
1318
|
+
context: binding.context,
|
|
1319
|
+
action: binding.action ?? void 0
|
|
1320
|
+
});
|
|
1321
|
+
}
|
|
1322
|
+
}
|
|
1323
|
+
}
|
|
1324
|
+
return warnings;
|
|
1325
|
+
}
|
|
1326
|
+
function getUserBindingsForValidation(userBlocks) {
|
|
1327
|
+
const bindings = [];
|
|
1328
|
+
for (const block of userBlocks) {
|
|
1329
|
+
for (const [key, action] of Object.entries(block.bindings)) {
|
|
1330
|
+
const chord = key.split(" ").map((k) => parseKeystroke(k));
|
|
1331
|
+
bindings.push({
|
|
1332
|
+
chord,
|
|
1333
|
+
action,
|
|
1334
|
+
context: block.context
|
|
1335
|
+
});
|
|
1336
|
+
}
|
|
1337
|
+
}
|
|
1338
|
+
return bindings;
|
|
1339
|
+
}
|
|
1340
|
+
function validateBindings(userBlocks, _parsedBindings) {
|
|
1341
|
+
const warnings = [];
|
|
1342
|
+
warnings.push(...validateUserConfig(userBlocks));
|
|
1343
|
+
if (isKeybindingBlockArray(userBlocks)) {
|
|
1344
|
+
warnings.push(...checkDuplicates(userBlocks));
|
|
1345
|
+
const userBindings = getUserBindingsForValidation(userBlocks);
|
|
1346
|
+
warnings.push(...checkReservedShortcuts(userBindings));
|
|
1347
|
+
}
|
|
1348
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1349
|
+
return warnings.filter((w) => {
|
|
1350
|
+
const key = `${w.type}:${w.key}:${w.context}`;
|
|
1351
|
+
if (seen.has(key)) return false;
|
|
1352
|
+
seen.add(key);
|
|
1353
|
+
return true;
|
|
1354
|
+
});
|
|
1355
|
+
}
|
|
1356
|
+
|
|
1357
|
+
// src/keybindings/loadUserBindings.ts
|
|
1358
|
+
function logForDebugging(msg) {
|
|
1359
|
+
if (process.env.DEBUG_KEYBINDINGS) console.error(msg);
|
|
1360
|
+
}
|
|
1361
|
+
function getClaudeConfigHomeDir() {
|
|
1362
|
+
return (0, import_path.join)(process.env.HOME ?? "~", ".claude");
|
|
1363
|
+
}
|
|
1364
|
+
function isENOENT(error) {
|
|
1365
|
+
return typeof error === "object" && error !== null && error.code === "ENOENT";
|
|
1366
|
+
}
|
|
1367
|
+
function errorMessage(error) {
|
|
1368
|
+
return error instanceof Error ? error.message : String(error);
|
|
1369
|
+
}
|
|
1370
|
+
function jsonParse(text) {
|
|
1371
|
+
return JSON.parse(text);
|
|
1372
|
+
}
|
|
1373
|
+
function createSignal() {
|
|
1374
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
1375
|
+
return {
|
|
1376
|
+
subscribe: (listener) => {
|
|
1377
|
+
listeners.add(listener);
|
|
1378
|
+
return () => listeners.delete(listener);
|
|
1379
|
+
},
|
|
1380
|
+
emit: (...args) => {
|
|
1381
|
+
for (const listener of listeners) listener(...args);
|
|
1382
|
+
},
|
|
1383
|
+
clear: () => listeners.clear()
|
|
1384
|
+
};
|
|
1385
|
+
}
|
|
1386
|
+
function isKeybindingCustomizationEnabled() {
|
|
1387
|
+
return true;
|
|
1388
|
+
}
|
|
1389
|
+
var FILE_STABILITY_THRESHOLD_MS = 500;
|
|
1390
|
+
var FILE_STABILITY_POLL_INTERVAL_MS = 200;
|
|
1391
|
+
var watcher = null;
|
|
1392
|
+
var initialized = false;
|
|
1393
|
+
var disposed = false;
|
|
1394
|
+
var cachedBindings = null;
|
|
1395
|
+
var cachedWarnings = [];
|
|
1396
|
+
var keybindingsChanged = createSignal();
|
|
1397
|
+
function isKeybindingBlock2(obj) {
|
|
1398
|
+
if (typeof obj !== "object" || obj === null) return false;
|
|
1399
|
+
const b = obj;
|
|
1400
|
+
return typeof b.context === "string" && typeof b.bindings === "object" && b.bindings !== null;
|
|
1401
|
+
}
|
|
1402
|
+
function isKeybindingBlockArray2(arr) {
|
|
1403
|
+
return Array.isArray(arr) && arr.every(isKeybindingBlock2);
|
|
1404
|
+
}
|
|
1405
|
+
function getKeybindingsPath() {
|
|
1406
|
+
return (0, import_path.join)(getClaudeConfigHomeDir(), "keybindings.json");
|
|
1407
|
+
}
|
|
1408
|
+
function getDefaultParsedBindings() {
|
|
1409
|
+
return parseBindings(DEFAULT_BINDINGS);
|
|
1410
|
+
}
|
|
1411
|
+
async function loadKeybindings() {
|
|
1412
|
+
const defaultBindings = getDefaultParsedBindings();
|
|
1413
|
+
if (!isKeybindingCustomizationEnabled()) {
|
|
1414
|
+
return { bindings: defaultBindings, warnings: [] };
|
|
1415
|
+
}
|
|
1416
|
+
const userPath = getKeybindingsPath();
|
|
1417
|
+
try {
|
|
1418
|
+
const content = await (0, import_promises.readFile)(userPath, "utf-8");
|
|
1419
|
+
const parsed = jsonParse(content);
|
|
1420
|
+
let userBlocks;
|
|
1421
|
+
if (typeof parsed === "object" && parsed !== null && "bindings" in parsed) {
|
|
1422
|
+
userBlocks = parsed.bindings;
|
|
1423
|
+
} else {
|
|
1424
|
+
const msg = 'keybindings.json must have a "bindings" array';
|
|
1425
|
+
const suggestion = 'Use format: { "bindings": [ ... ] }';
|
|
1426
|
+
logForDebugging(`[keybindings] Invalid keybindings.json: ${msg}`);
|
|
1427
|
+
return {
|
|
1428
|
+
bindings: defaultBindings,
|
|
1429
|
+
warnings: [{ type: "parse_error", severity: "error", message: msg, suggestion }]
|
|
1430
|
+
};
|
|
1431
|
+
}
|
|
1432
|
+
if (!isKeybindingBlockArray2(userBlocks)) {
|
|
1433
|
+
const msg = !Array.isArray(userBlocks) ? '"bindings" must be an array' : "keybindings.json contains invalid block structure";
|
|
1434
|
+
const suggestion = !Array.isArray(userBlocks) ? 'Set "bindings" to an array of keybinding blocks' : 'Each block must have "context" (string) and "bindings" (object)';
|
|
1435
|
+
logForDebugging(`[keybindings] Invalid keybindings.json: ${msg}`);
|
|
1436
|
+
return {
|
|
1437
|
+
bindings: defaultBindings,
|
|
1438
|
+
warnings: [{ type: "parse_error", severity: "error", message: msg, suggestion }]
|
|
1439
|
+
};
|
|
1440
|
+
}
|
|
1441
|
+
const userParsed = parseBindings(userBlocks);
|
|
1442
|
+
logForDebugging(`[keybindings] Loaded ${userParsed.length} user bindings from ${userPath}`);
|
|
1443
|
+
const mergedBindings = [...defaultBindings, ...userParsed];
|
|
1444
|
+
const duplicateKeyWarnings = checkDuplicateKeysInJson(content);
|
|
1445
|
+
const warnings = [
|
|
1446
|
+
...duplicateKeyWarnings,
|
|
1447
|
+
...validateBindings(userBlocks, mergedBindings)
|
|
1448
|
+
];
|
|
1449
|
+
if (warnings.length > 0) {
|
|
1450
|
+
logForDebugging(`[keybindings] Found ${warnings.length} validation issue(s)`);
|
|
1451
|
+
}
|
|
1452
|
+
return { bindings: mergedBindings, warnings };
|
|
1453
|
+
} catch (error) {
|
|
1454
|
+
if (isENOENT(error)) {
|
|
1455
|
+
return { bindings: defaultBindings, warnings: [] };
|
|
1456
|
+
}
|
|
1457
|
+
logForDebugging(`[keybindings] Error loading ${userPath}: ${errorMessage(error)}`);
|
|
1458
|
+
return {
|
|
1459
|
+
bindings: defaultBindings,
|
|
1460
|
+
warnings: [
|
|
1461
|
+
{
|
|
1462
|
+
type: "parse_error",
|
|
1463
|
+
severity: "error",
|
|
1464
|
+
message: `Failed to parse keybindings.json: ${errorMessage(error)}`
|
|
1465
|
+
}
|
|
1466
|
+
]
|
|
1467
|
+
};
|
|
1468
|
+
}
|
|
1469
|
+
}
|
|
1470
|
+
function loadKeybindingsSyncWithWarnings() {
|
|
1471
|
+
if (cachedBindings) {
|
|
1472
|
+
return { bindings: cachedBindings, warnings: cachedWarnings };
|
|
1473
|
+
}
|
|
1474
|
+
const defaultBindings = getDefaultParsedBindings();
|
|
1475
|
+
if (!isKeybindingCustomizationEnabled()) {
|
|
1476
|
+
cachedBindings = defaultBindings;
|
|
1477
|
+
cachedWarnings = [];
|
|
1478
|
+
return { bindings: cachedBindings, warnings: cachedWarnings };
|
|
1479
|
+
}
|
|
1480
|
+
const userPath = getKeybindingsPath();
|
|
1481
|
+
try {
|
|
1482
|
+
const content = (0, import_fs.readFileSync)(userPath, "utf-8");
|
|
1483
|
+
const parsed = jsonParse(content);
|
|
1484
|
+
let userBlocks;
|
|
1485
|
+
if (typeof parsed === "object" && parsed !== null && "bindings" in parsed) {
|
|
1486
|
+
userBlocks = parsed.bindings;
|
|
1487
|
+
} else {
|
|
1488
|
+
cachedBindings = defaultBindings;
|
|
1489
|
+
cachedWarnings = [
|
|
1490
|
+
{
|
|
1491
|
+
type: "parse_error",
|
|
1492
|
+
severity: "error",
|
|
1493
|
+
message: 'keybindings.json must have a "bindings" array',
|
|
1494
|
+
suggestion: 'Use format: { "bindings": [ ... ] }'
|
|
1495
|
+
}
|
|
1496
|
+
];
|
|
1497
|
+
return { bindings: cachedBindings, warnings: cachedWarnings };
|
|
1498
|
+
}
|
|
1499
|
+
if (!isKeybindingBlockArray2(userBlocks)) {
|
|
1500
|
+
const msg = !Array.isArray(userBlocks) ? '"bindings" must be an array' : "keybindings.json contains invalid block structure";
|
|
1501
|
+
const suggestion = !Array.isArray(userBlocks) ? 'Set "bindings" to an array of keybinding blocks' : 'Each block must have "context" (string) and "bindings" (object)';
|
|
1502
|
+
cachedBindings = defaultBindings;
|
|
1503
|
+
cachedWarnings = [{ type: "parse_error", severity: "error", message: msg, suggestion }];
|
|
1504
|
+
return { bindings: cachedBindings, warnings: cachedWarnings };
|
|
1505
|
+
}
|
|
1506
|
+
const userParsed = parseBindings(userBlocks);
|
|
1507
|
+
logForDebugging(`[keybindings] Loaded ${userParsed.length} user bindings from ${userPath}`);
|
|
1508
|
+
cachedBindings = [...defaultBindings, ...userParsed];
|
|
1509
|
+
const duplicateKeyWarnings = checkDuplicateKeysInJson(content);
|
|
1510
|
+
cachedWarnings = [
|
|
1511
|
+
...duplicateKeyWarnings,
|
|
1512
|
+
...validateBindings(userBlocks, cachedBindings)
|
|
1513
|
+
];
|
|
1514
|
+
if (cachedWarnings.length > 0) {
|
|
1515
|
+
logForDebugging(`[keybindings] Found ${cachedWarnings.length} validation issue(s)`);
|
|
1516
|
+
}
|
|
1517
|
+
return { bindings: cachedBindings, warnings: cachedWarnings };
|
|
1518
|
+
} catch {
|
|
1519
|
+
cachedBindings = defaultBindings;
|
|
1520
|
+
cachedWarnings = [];
|
|
1521
|
+
return { bindings: cachedBindings, warnings: cachedWarnings };
|
|
1522
|
+
}
|
|
1523
|
+
}
|
|
1524
|
+
async function initializeKeybindingWatcher() {
|
|
1525
|
+
if (initialized || disposed) return;
|
|
1526
|
+
if (!isKeybindingCustomizationEnabled()) {
|
|
1527
|
+
logForDebugging("[keybindings] Skipping file watcher - user customization disabled");
|
|
1528
|
+
return;
|
|
1529
|
+
}
|
|
1530
|
+
const userPath = getKeybindingsPath();
|
|
1531
|
+
const watchDir = (0, import_path.dirname)(userPath);
|
|
1532
|
+
try {
|
|
1533
|
+
const stats = await (0, import_promises.stat)(watchDir);
|
|
1534
|
+
if (!stats.isDirectory()) {
|
|
1535
|
+
logForDebugging(`[keybindings] Not watching: ${watchDir} is not a directory`);
|
|
1536
|
+
return;
|
|
1537
|
+
}
|
|
1538
|
+
} catch {
|
|
1539
|
+
logForDebugging(`[keybindings] Not watching: ${watchDir} does not exist`);
|
|
1540
|
+
return;
|
|
1541
|
+
}
|
|
1542
|
+
initialized = true;
|
|
1543
|
+
logForDebugging(`[keybindings] Watching for changes to ${userPath}`);
|
|
1544
|
+
watcher = import_chokidar.default.watch(userPath, {
|
|
1545
|
+
persistent: true,
|
|
1546
|
+
ignoreInitial: true,
|
|
1547
|
+
awaitWriteFinish: {
|
|
1548
|
+
stabilityThreshold: FILE_STABILITY_THRESHOLD_MS,
|
|
1549
|
+
pollInterval: FILE_STABILITY_POLL_INTERVAL_MS
|
|
1550
|
+
},
|
|
1551
|
+
ignorePermissionErrors: true,
|
|
1552
|
+
usePolling: false,
|
|
1553
|
+
atomic: true
|
|
1554
|
+
});
|
|
1555
|
+
watcher.on("add", handleChange);
|
|
1556
|
+
watcher.on("change", handleChange);
|
|
1557
|
+
watcher.on("unlink", handleDelete);
|
|
1558
|
+
}
|
|
1559
|
+
var subscribeToKeybindingChanges = keybindingsChanged.subscribe;
|
|
1560
|
+
async function handleChange(path) {
|
|
1561
|
+
logForDebugging(`[keybindings] Detected change to ${path}`);
|
|
1562
|
+
try {
|
|
1563
|
+
const result = await loadKeybindings();
|
|
1564
|
+
cachedBindings = result.bindings;
|
|
1565
|
+
cachedWarnings = result.warnings;
|
|
1566
|
+
keybindingsChanged.emit(result);
|
|
1567
|
+
} catch (error) {
|
|
1568
|
+
logForDebugging(`[keybindings] Error reloading: ${errorMessage(error)}`);
|
|
1569
|
+
}
|
|
1570
|
+
}
|
|
1571
|
+
function handleDelete(path) {
|
|
1572
|
+
logForDebugging(`[keybindings] Detected deletion of ${path}`);
|
|
1573
|
+
const defaultBindings = getDefaultParsedBindings();
|
|
1574
|
+
cachedBindings = defaultBindings;
|
|
1575
|
+
cachedWarnings = [];
|
|
1576
|
+
keybindingsChanged.emit({ bindings: defaultBindings, warnings: [] });
|
|
1577
|
+
}
|
|
1578
|
+
|
|
1579
|
+
// src/keybindings/KeybindingProviderSetup.tsx
|
|
1580
|
+
var import_jsx_runtime6 = require("react/jsx-runtime");
|
|
1581
|
+
var plural = (n, s) => n === 1 ? s : s + "s";
|
|
1582
|
+
function logForDebugging2(msg) {
|
|
1583
|
+
if (process.env.DEBUG_KEYBINDINGS) console.error(msg);
|
|
1584
|
+
}
|
|
1585
|
+
var CHORD_TIMEOUT_MS = 1e3;
|
|
1586
|
+
function KeybindingSetup({ children, onWarnings }) {
|
|
1587
|
+
const [{ bindings, warnings }, setLoadResult] = (0, import_react5.useState)(() => {
|
|
1588
|
+
const result = loadKeybindingsSyncWithWarnings();
|
|
1589
|
+
logForDebugging2(
|
|
1590
|
+
`[keybindings] KeybindingSetup initialized with ${result.bindings.length} bindings, ${result.warnings.length} warnings`
|
|
1591
|
+
);
|
|
1592
|
+
return result;
|
|
1593
|
+
});
|
|
1594
|
+
const [isReload, setIsReload] = (0, import_react5.useState)(false);
|
|
1595
|
+
(0, import_react5.useEffect)(() => {
|
|
1596
|
+
if (!onWarnings || warnings.length === 0) return;
|
|
1597
|
+
const errorCount = warnings.filter((w) => w.severity === "error").length;
|
|
1598
|
+
const warnCount = warnings.filter((w) => w.severity === "warning").length;
|
|
1599
|
+
let message;
|
|
1600
|
+
if (errorCount > 0 && warnCount > 0) {
|
|
1601
|
+
message = `Found ${errorCount} keybinding ${plural(errorCount, "error")} and ${warnCount} ${plural(warnCount, "warning")}`;
|
|
1602
|
+
} else if (errorCount > 0) {
|
|
1603
|
+
message = `Found ${errorCount} keybinding ${plural(errorCount, "error")}`;
|
|
1604
|
+
} else {
|
|
1605
|
+
message = `Found ${warnCount} keybinding ${plural(warnCount, "warning")}`;
|
|
1606
|
+
}
|
|
1607
|
+
onWarnings(message + " \xB7 /doctor for details", errorCount > 0);
|
|
1608
|
+
}, [warnings, isReload, onWarnings]);
|
|
1609
|
+
const pendingChordRef = (0, import_react5.useRef)(null);
|
|
1610
|
+
const [pendingChord, setPendingChordState] = (0, import_react5.useState)(null);
|
|
1611
|
+
const chordTimeoutRef = (0, import_react5.useRef)(null);
|
|
1612
|
+
const handlerRegistryRef = (0, import_react5.useRef)(
|
|
1613
|
+
/* @__PURE__ */ new Map()
|
|
1614
|
+
);
|
|
1615
|
+
const activeContextsRef = (0, import_react5.useRef)(/* @__PURE__ */ new Set());
|
|
1616
|
+
const registerActiveContext = (0, import_react5.useCallback)((context) => {
|
|
1617
|
+
activeContextsRef.current.add(context);
|
|
1618
|
+
}, []);
|
|
1619
|
+
const unregisterActiveContext = (0, import_react5.useCallback)(
|
|
1620
|
+
(context) => {
|
|
1621
|
+
activeContextsRef.current.delete(context);
|
|
1622
|
+
},
|
|
1623
|
+
[]
|
|
1624
|
+
);
|
|
1625
|
+
const clearChordTimeout = (0, import_react5.useCallback)(() => {
|
|
1626
|
+
if (chordTimeoutRef.current) {
|
|
1627
|
+
clearTimeout(chordTimeoutRef.current);
|
|
1628
|
+
chordTimeoutRef.current = null;
|
|
1629
|
+
}
|
|
1630
|
+
}, []);
|
|
1631
|
+
const setPendingChord = (0, import_react5.useCallback)(
|
|
1632
|
+
(pending) => {
|
|
1633
|
+
clearChordTimeout();
|
|
1634
|
+
if (pending !== null) {
|
|
1635
|
+
chordTimeoutRef.current = setTimeout(() => {
|
|
1636
|
+
logForDebugging2("[keybindings] Chord timeout - cancelling");
|
|
1637
|
+
pendingChordRef.current = null;
|
|
1638
|
+
setPendingChordState(null);
|
|
1639
|
+
}, CHORD_TIMEOUT_MS);
|
|
1640
|
+
}
|
|
1641
|
+
pendingChordRef.current = pending;
|
|
1642
|
+
setPendingChordState(pending);
|
|
1643
|
+
},
|
|
1644
|
+
[clearChordTimeout]
|
|
1645
|
+
);
|
|
1646
|
+
(0, import_react5.useEffect)(() => {
|
|
1647
|
+
void initializeKeybindingWatcher();
|
|
1648
|
+
const unsubscribe = subscribeToKeybindingChanges((result) => {
|
|
1649
|
+
setIsReload(true);
|
|
1650
|
+
setLoadResult(result);
|
|
1651
|
+
logForDebugging2(
|
|
1652
|
+
`[keybindings] Reloaded: ${result.bindings.length} bindings, ${result.warnings.length} warnings`
|
|
1653
|
+
);
|
|
1654
|
+
});
|
|
1655
|
+
return () => {
|
|
1656
|
+
unsubscribe();
|
|
1657
|
+
clearChordTimeout();
|
|
1658
|
+
};
|
|
1659
|
+
}, [clearChordTimeout]);
|
|
1660
|
+
return /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(
|
|
1661
|
+
KeybindingProvider,
|
|
1662
|
+
{
|
|
1663
|
+
bindings,
|
|
1664
|
+
pendingChordRef,
|
|
1665
|
+
pendingChord,
|
|
1666
|
+
setPendingChord,
|
|
1667
|
+
activeContexts: activeContextsRef.current,
|
|
1668
|
+
registerActiveContext,
|
|
1669
|
+
unregisterActiveContext,
|
|
1670
|
+
handlerRegistryRef,
|
|
1671
|
+
children: [
|
|
1672
|
+
/* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
|
|
1673
|
+
ChordInterceptor,
|
|
1674
|
+
{
|
|
1675
|
+
bindings,
|
|
1676
|
+
pendingChordRef,
|
|
1677
|
+
setPendingChord,
|
|
1678
|
+
activeContexts: activeContextsRef.current,
|
|
1679
|
+
handlerRegistryRef
|
|
1680
|
+
}
|
|
1681
|
+
),
|
|
1682
|
+
children
|
|
1683
|
+
]
|
|
1684
|
+
}
|
|
1685
|
+
);
|
|
1686
|
+
}
|
|
1687
|
+
function ChordInterceptor({
|
|
1688
|
+
bindings,
|
|
1689
|
+
pendingChordRef,
|
|
1690
|
+
setPendingChord,
|
|
1691
|
+
activeContexts,
|
|
1692
|
+
handlerRegistryRef
|
|
1693
|
+
}) {
|
|
1694
|
+
const handleInput = (0, import_react5.useCallback)(
|
|
1695
|
+
(input, key, event) => {
|
|
1696
|
+
if ((key.wheelUp || key.wheelDown) && pendingChordRef.current === null) {
|
|
1697
|
+
return;
|
|
1698
|
+
}
|
|
1699
|
+
const registry = handlerRegistryRef.current;
|
|
1700
|
+
const handlerContexts = /* @__PURE__ */ new Set();
|
|
1701
|
+
if (registry) {
|
|
1702
|
+
for (const handlers of registry.values()) {
|
|
1703
|
+
for (const registration of handlers) {
|
|
1704
|
+
handlerContexts.add(registration.context);
|
|
1705
|
+
}
|
|
1706
|
+
}
|
|
1707
|
+
}
|
|
1708
|
+
const contexts = [
|
|
1709
|
+
...handlerContexts,
|
|
1710
|
+
...activeContexts,
|
|
1711
|
+
"Global"
|
|
1712
|
+
];
|
|
1713
|
+
const wasInChord = pendingChordRef.current !== null;
|
|
1714
|
+
const result = resolveKeyWithChordState(
|
|
1715
|
+
input,
|
|
1716
|
+
key,
|
|
1717
|
+
contexts,
|
|
1718
|
+
bindings,
|
|
1719
|
+
pendingChordRef.current
|
|
1720
|
+
);
|
|
1721
|
+
switch (result.type) {
|
|
1722
|
+
case "chord_started":
|
|
1723
|
+
setPendingChord(result.pending);
|
|
1724
|
+
event.stopImmediatePropagation();
|
|
1725
|
+
break;
|
|
1726
|
+
case "match":
|
|
1727
|
+
setPendingChord(null);
|
|
1728
|
+
if (wasInChord) {
|
|
1729
|
+
const contextsSet = new Set(contexts);
|
|
1730
|
+
if (registry) {
|
|
1731
|
+
const handlers = registry.get(result.action);
|
|
1732
|
+
if (handlers && handlers.size > 0) {
|
|
1733
|
+
for (const registration of handlers) {
|
|
1734
|
+
if (contextsSet.has(registration.context)) {
|
|
1735
|
+
registration.handler();
|
|
1736
|
+
event.stopImmediatePropagation();
|
|
1737
|
+
break;
|
|
1738
|
+
}
|
|
1739
|
+
}
|
|
1740
|
+
}
|
|
1741
|
+
}
|
|
1742
|
+
}
|
|
1743
|
+
break;
|
|
1744
|
+
case "chord_cancelled":
|
|
1745
|
+
setPendingChord(null);
|
|
1746
|
+
event.stopImmediatePropagation();
|
|
1747
|
+
break;
|
|
1748
|
+
case "unbound":
|
|
1749
|
+
setPendingChord(null);
|
|
1750
|
+
event.stopImmediatePropagation();
|
|
1751
|
+
break;
|
|
1752
|
+
case "none":
|
|
1753
|
+
break;
|
|
1754
|
+
}
|
|
1755
|
+
},
|
|
1756
|
+
[bindings, pendingChordRef, setPendingChord, activeContexts, handlerRegistryRef]
|
|
1757
|
+
);
|
|
1758
|
+
(0, import_ink_renderer6.useInput)(handleInput);
|
|
1759
|
+
return null;
|
|
1760
|
+
}
|
|
1761
|
+
|
|
1762
|
+
// src/PromptInput.tsx
|
|
1763
|
+
var import_react6 = require("react");
|
|
1764
|
+
var import_ink_renderer7 = require("@claude-code-kit/ink-renderer");
|
|
1765
|
+
var import_jsx_runtime7 = require("react/jsx-runtime");
|
|
1766
|
+
function PromptInput({
|
|
1767
|
+
value,
|
|
1768
|
+
onChange,
|
|
1769
|
+
onSubmit,
|
|
1770
|
+
placeholder = "",
|
|
1771
|
+
prefix = "\u276F",
|
|
1772
|
+
prefixColor = "cyan",
|
|
1773
|
+
disabled = false,
|
|
1774
|
+
commands = [],
|
|
1775
|
+
onCommandSelect,
|
|
1776
|
+
history = []
|
|
1777
|
+
}) {
|
|
1778
|
+
const [cursor, setCursor] = (0, import_react6.useState)(0);
|
|
1779
|
+
const [historyIndex, setHistoryIndex] = (0, import_react6.useState)(-1);
|
|
1780
|
+
const [suggestionIndex, setSuggestionIndex] = (0, import_react6.useState)(0);
|
|
1781
|
+
const [showSuggestions, setShowSuggestions] = (0, import_react6.useState)(false);
|
|
1782
|
+
const suggestions = value.startsWith("/") && commands.length > 0 ? commands.filter((cmd) => `/${cmd.name}`.startsWith(value)) : [];
|
|
1783
|
+
const hasSuggestions = showSuggestions && suggestions.length > 0;
|
|
1784
|
+
const updateValue = (0, import_react6.useCallback)(
|
|
1785
|
+
(newValue, newCursor) => {
|
|
1786
|
+
onChange(newValue);
|
|
1787
|
+
setCursor(newCursor ?? newValue.length);
|
|
1788
|
+
setHistoryIndex(-1);
|
|
1789
|
+
setShowSuggestions(newValue.startsWith("/"));
|
|
1790
|
+
setSuggestionIndex(0);
|
|
1791
|
+
},
|
|
1792
|
+
[onChange]
|
|
1793
|
+
);
|
|
1794
|
+
(0, import_ink_renderer7.useInput)(
|
|
1795
|
+
(input, key) => {
|
|
1796
|
+
if (disabled) return;
|
|
1797
|
+
if (key.return) {
|
|
1798
|
+
if (hasSuggestions) {
|
|
1799
|
+
const cmd = suggestions[suggestionIndex];
|
|
1800
|
+
const cmdValue = `/${cmd.name}`;
|
|
1801
|
+
onCommandSelect?.(cmd.name);
|
|
1802
|
+
onChange(cmdValue);
|
|
1803
|
+
setCursor(cmdValue.length);
|
|
1804
|
+
setShowSuggestions(false);
|
|
1805
|
+
return;
|
|
1806
|
+
}
|
|
1807
|
+
if (value.length > 0) {
|
|
1808
|
+
onSubmit(value);
|
|
1809
|
+
}
|
|
1810
|
+
return;
|
|
1811
|
+
}
|
|
1812
|
+
if (key.escape) {
|
|
1813
|
+
if (hasSuggestions) {
|
|
1814
|
+
setShowSuggestions(false);
|
|
1815
|
+
}
|
|
1816
|
+
return;
|
|
1817
|
+
}
|
|
1818
|
+
if (key.tab) {
|
|
1819
|
+
if (hasSuggestions) {
|
|
1820
|
+
const cmd = suggestions[suggestionIndex];
|
|
1821
|
+
const cmdValue = `/${cmd.name} `;
|
|
1822
|
+
updateValue(cmdValue);
|
|
1823
|
+
}
|
|
1824
|
+
return;
|
|
1825
|
+
}
|
|
1826
|
+
if (key.upArrow) {
|
|
1827
|
+
if (hasSuggestions) {
|
|
1828
|
+
setSuggestionIndex((i) => i > 0 ? i - 1 : suggestions.length - 1);
|
|
1829
|
+
return;
|
|
1830
|
+
}
|
|
1831
|
+
if (history.length > 0) {
|
|
1832
|
+
const nextIndex = historyIndex + 1;
|
|
1833
|
+
if (nextIndex < history.length) {
|
|
1834
|
+
setHistoryIndex(nextIndex);
|
|
1835
|
+
const histValue = history[nextIndex];
|
|
1836
|
+
onChange(histValue);
|
|
1837
|
+
setCursor(histValue.length);
|
|
1838
|
+
}
|
|
1839
|
+
}
|
|
1840
|
+
return;
|
|
1841
|
+
}
|
|
1842
|
+
if (key.downArrow) {
|
|
1843
|
+
if (hasSuggestions) {
|
|
1844
|
+
setSuggestionIndex((i) => i < suggestions.length - 1 ? i + 1 : 0);
|
|
1845
|
+
return;
|
|
1846
|
+
}
|
|
1847
|
+
if (historyIndex > 0) {
|
|
1848
|
+
const nextIndex = historyIndex - 1;
|
|
1849
|
+
setHistoryIndex(nextIndex);
|
|
1850
|
+
const histValue = history[nextIndex];
|
|
1851
|
+
onChange(histValue);
|
|
1852
|
+
setCursor(histValue.length);
|
|
1853
|
+
} else if (historyIndex === 0) {
|
|
1854
|
+
setHistoryIndex(-1);
|
|
1855
|
+
onChange("");
|
|
1856
|
+
setCursor(0);
|
|
1857
|
+
}
|
|
1858
|
+
return;
|
|
1859
|
+
}
|
|
1860
|
+
if (key.leftArrow) {
|
|
1861
|
+
setCursor((c) => Math.max(0, c - 1));
|
|
1862
|
+
return;
|
|
1863
|
+
}
|
|
1864
|
+
if (key.rightArrow) {
|
|
1865
|
+
setCursor((c) => Math.min(value.length, c + 1));
|
|
1866
|
+
return;
|
|
1867
|
+
}
|
|
1868
|
+
if (key.home || key.ctrl && input === "a") {
|
|
1869
|
+
setCursor(0);
|
|
1870
|
+
return;
|
|
1871
|
+
}
|
|
1872
|
+
if (key.end || key.ctrl && input === "e") {
|
|
1873
|
+
setCursor(value.length);
|
|
1874
|
+
return;
|
|
1875
|
+
}
|
|
1876
|
+
if (key.ctrl && input === "w") {
|
|
1877
|
+
if (cursor > 0) {
|
|
1878
|
+
let i = cursor - 1;
|
|
1879
|
+
while (i > 0 && value[i - 1] === " ") i--;
|
|
1880
|
+
while (i > 0 && value[i - 1] !== " ") i--;
|
|
1881
|
+
const newValue = value.slice(0, i) + value.slice(cursor);
|
|
1882
|
+
updateValue(newValue, i);
|
|
1883
|
+
}
|
|
1884
|
+
return;
|
|
1885
|
+
}
|
|
1886
|
+
if (key.ctrl && input === "u") {
|
|
1887
|
+
const newValue = value.slice(cursor);
|
|
1888
|
+
updateValue(newValue, 0);
|
|
1889
|
+
return;
|
|
1890
|
+
}
|
|
1891
|
+
if (key.backspace) {
|
|
1892
|
+
if (cursor > 0) {
|
|
1893
|
+
const newValue = value.slice(0, cursor - 1) + value.slice(cursor);
|
|
1894
|
+
updateValue(newValue, cursor - 1);
|
|
1895
|
+
}
|
|
1896
|
+
return;
|
|
1897
|
+
}
|
|
1898
|
+
if (key.delete) {
|
|
1899
|
+
if (cursor < value.length) {
|
|
1900
|
+
const newValue = value.slice(0, cursor) + value.slice(cursor + 1);
|
|
1901
|
+
updateValue(newValue, cursor);
|
|
1902
|
+
}
|
|
1903
|
+
return;
|
|
1904
|
+
}
|
|
1905
|
+
if (key.ctrl || key.meta) return;
|
|
1906
|
+
if (input.length > 0) {
|
|
1907
|
+
const newValue = value.slice(0, cursor) + input + value.slice(cursor);
|
|
1908
|
+
updateValue(newValue, cursor + input.length);
|
|
1909
|
+
}
|
|
1910
|
+
},
|
|
1911
|
+
{ isActive: !disabled }
|
|
1912
|
+
);
|
|
1913
|
+
const renderTextWithCursor = () => {
|
|
1914
|
+
if (value.length === 0 && placeholder) {
|
|
1915
|
+
return /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(import_ink_renderer7.Text, { children: [
|
|
1916
|
+
/* @__PURE__ */ (0, import_jsx_runtime7.jsx)(import_ink_renderer7.Text, { inverse: true, children: " " }),
|
|
1917
|
+
/* @__PURE__ */ (0, import_jsx_runtime7.jsx)(import_ink_renderer7.Text, { dimColor: true, children: placeholder })
|
|
1918
|
+
] });
|
|
1919
|
+
}
|
|
1920
|
+
const before = value.slice(0, cursor);
|
|
1921
|
+
const atCursor = cursor < value.length ? value[cursor] : " ";
|
|
1922
|
+
const after = cursor < value.length ? value.slice(cursor + 1) : "";
|
|
1923
|
+
return /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(import_ink_renderer7.Text, { children: [
|
|
1924
|
+
before,
|
|
1925
|
+
/* @__PURE__ */ (0, import_jsx_runtime7.jsx)(import_ink_renderer7.Text, { inverse: true, children: atCursor }),
|
|
1926
|
+
after
|
|
1927
|
+
] });
|
|
1928
|
+
};
|
|
1929
|
+
return /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(import_ink_renderer7.Box, { flexDirection: "column", children: [
|
|
1930
|
+
/* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(import_ink_renderer7.Box, { children: [
|
|
1931
|
+
/* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(import_ink_renderer7.Text, { color: prefixColor, children: [
|
|
1932
|
+
prefix,
|
|
1933
|
+
" "
|
|
1934
|
+
] }),
|
|
1935
|
+
renderTextWithCursor()
|
|
1936
|
+
] }),
|
|
1937
|
+
hasSuggestions && /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(import_ink_renderer7.Box, { flexDirection: "column", marginLeft: 2, children: suggestions.map((cmd, i) => /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(import_ink_renderer7.Box, { children: [
|
|
1938
|
+
/* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
|
|
1939
|
+
import_ink_renderer7.Text,
|
|
1940
|
+
{
|
|
1941
|
+
inverse: i === suggestionIndex,
|
|
1942
|
+
color: i === suggestionIndex ? "cyan" : void 0,
|
|
1943
|
+
children: ` /${cmd.name}`
|
|
1944
|
+
}
|
|
1945
|
+
),
|
|
1946
|
+
/* @__PURE__ */ (0, import_jsx_runtime7.jsx)(import_ink_renderer7.Text, { dimColor: true, children: ` ${cmd.description}` })
|
|
1947
|
+
] }, cmd.name)) })
|
|
1948
|
+
] });
|
|
1949
|
+
}
|
|
1950
|
+
|
|
1951
|
+
// src/Spinner.tsx
|
|
1952
|
+
var import_react7 = require("react");
|
|
1953
|
+
var import_ink_renderer8 = require("@claude-code-kit/ink-renderer");
|
|
1954
|
+
var import_jsx_runtime8 = require("react/jsx-runtime");
|
|
1955
|
+
var FRAMES = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
|
|
1956
|
+
var SPINNER_INTERVAL = 80;
|
|
1957
|
+
var VERB_ROTATE_INTERVAL = 4e3;
|
|
1958
|
+
var ELAPSED_SHOW_AFTER = 1e3;
|
|
1959
|
+
var DEFAULT_COLOR = "#DA7756";
|
|
1960
|
+
function Spinner({
|
|
1961
|
+
label,
|
|
1962
|
+
verb,
|
|
1963
|
+
verbs,
|
|
1964
|
+
color = DEFAULT_COLOR,
|
|
1965
|
+
showElapsed = true
|
|
1966
|
+
}) {
|
|
1967
|
+
const [frameIndex, setFrameIndex] = (0, import_react7.useState)(0);
|
|
1968
|
+
const [verbIndex, setVerbIndex] = (0, import_react7.useState)(0);
|
|
1969
|
+
const [elapsed, setElapsed] = (0, import_react7.useState)(0);
|
|
1970
|
+
const startRef = (0, import_react7.useRef)(Date.now());
|
|
1971
|
+
const allVerbs = verbs ?? (verb ? [verb] : ["Thinking"]);
|
|
1972
|
+
(0, import_react7.useEffect)(() => {
|
|
1973
|
+
const id = setInterval(() => {
|
|
1974
|
+
setFrameIndex((i) => (i + 1) % FRAMES.length);
|
|
1975
|
+
setElapsed(Date.now() - startRef.current);
|
|
1976
|
+
}, SPINNER_INTERVAL);
|
|
1977
|
+
return () => clearInterval(id);
|
|
1978
|
+
}, []);
|
|
1979
|
+
(0, import_react7.useEffect)(() => {
|
|
1980
|
+
if (allVerbs.length <= 1) return;
|
|
1981
|
+
const id = setInterval(() => {
|
|
1982
|
+
setVerbIndex((i) => (i + 1) % allVerbs.length);
|
|
1983
|
+
}, VERB_ROTATE_INTERVAL);
|
|
1984
|
+
return () => clearInterval(id);
|
|
1985
|
+
}, [allVerbs.length]);
|
|
1986
|
+
const frame = FRAMES[frameIndex];
|
|
1987
|
+
const currentVerb = allVerbs[verbIndex % allVerbs.length];
|
|
1988
|
+
const elapsedSec = Math.floor(elapsed / 1e3);
|
|
1989
|
+
const showTime = showElapsed && elapsed >= ELAPSED_SHOW_AFTER;
|
|
1990
|
+
return /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(import_ink_renderer8.Box, { children: [
|
|
1991
|
+
/* @__PURE__ */ (0, import_jsx_runtime8.jsx)(import_ink_renderer8.Text, { color, children: frame }),
|
|
1992
|
+
/* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(import_ink_renderer8.Text, { children: [
|
|
1993
|
+
" ",
|
|
1994
|
+
currentVerb,
|
|
1995
|
+
"..."
|
|
1996
|
+
] }),
|
|
1997
|
+
label && /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(import_ink_renderer8.Text, { children: [
|
|
1998
|
+
" ",
|
|
1999
|
+
label
|
|
2000
|
+
] }),
|
|
2001
|
+
showTime && /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(import_ink_renderer8.Text, { dimColor: true, children: [
|
|
2002
|
+
" (",
|
|
2003
|
+
elapsedSec,
|
|
2004
|
+
"s)"
|
|
2005
|
+
] })
|
|
2006
|
+
] });
|
|
2007
|
+
}
|
|
2008
|
+
|
|
2009
|
+
// src/Select.tsx
|
|
2010
|
+
var import_react8 = require("react");
|
|
2011
|
+
var import_ink_renderer9 = require("@claude-code-kit/ink-renderer");
|
|
2012
|
+
var import_jsx_runtime9 = require("react/jsx-runtime");
|
|
2013
|
+
function useListNavigation(opts) {
|
|
2014
|
+
const { options, maxVisible, onCancel, onSelect, extraHandler } = opts;
|
|
2015
|
+
const [focusIndex, setFocusIndex] = (0, import_react8.useState)(0);
|
|
2016
|
+
const focusRef = (0, import_react8.useRef)(focusIndex);
|
|
2017
|
+
focusRef.current = focusIndex;
|
|
2018
|
+
const total = options.length;
|
|
2019
|
+
const max = maxVisible ?? total;
|
|
2020
|
+
const scrollOffset = (0, import_react8.useMemo)(() => {
|
|
2021
|
+
if (total <= max) return 0;
|
|
2022
|
+
const half = Math.floor(max / 2);
|
|
2023
|
+
if (focusIndex <= half) return 0;
|
|
2024
|
+
if (focusIndex >= total - max + half) return total - max;
|
|
2025
|
+
return focusIndex - half;
|
|
2026
|
+
}, [focusIndex, total, max]);
|
|
2027
|
+
const visibleOptions = (0, import_react8.useMemo)(
|
|
2028
|
+
() => options.slice(scrollOffset, scrollOffset + max),
|
|
2029
|
+
[options, scrollOffset, max]
|
|
2030
|
+
);
|
|
2031
|
+
const moveFocus = (0, import_react8.useCallback)(
|
|
2032
|
+
(dir) => {
|
|
2033
|
+
setFocusIndex((prev) => {
|
|
2034
|
+
let next = prev;
|
|
2035
|
+
for (let i = 0; i < total; i++) {
|
|
2036
|
+
next = (next + dir + total) % total;
|
|
2037
|
+
if (!options[next].disabled) return next;
|
|
2038
|
+
}
|
|
2039
|
+
return prev;
|
|
2040
|
+
});
|
|
2041
|
+
},
|
|
2042
|
+
[options, total]
|
|
2043
|
+
);
|
|
2044
|
+
(0, import_ink_renderer9.useInput)((input, key) => {
|
|
2045
|
+
if (extraHandler?.(input, key, focusRef.current)) return;
|
|
2046
|
+
if (key.upArrow || input === "k") {
|
|
2047
|
+
moveFocus(-1);
|
|
2048
|
+
} else if (key.downArrow || input === "j") {
|
|
2049
|
+
moveFocus(1);
|
|
2050
|
+
} else if (key.return) {
|
|
2051
|
+
if (!options[focusRef.current]?.disabled) {
|
|
2052
|
+
onSelect(focusRef.current);
|
|
2053
|
+
}
|
|
2054
|
+
} else if (key.escape) {
|
|
2055
|
+
onCancel?.();
|
|
2056
|
+
} else if (input >= "1" && input <= "9") {
|
|
2057
|
+
const idx = parseInt(input, 10) - 1;
|
|
2058
|
+
if (idx < total && !options[idx].disabled) {
|
|
2059
|
+
setFocusIndex(idx);
|
|
2060
|
+
onSelect(idx);
|
|
2061
|
+
}
|
|
2062
|
+
}
|
|
2063
|
+
});
|
|
2064
|
+
return { focusIndex, scrollOffset, visibleOptions, max, total };
|
|
2065
|
+
}
|
|
2066
|
+
function ScrollHint({ count, direction }) {
|
|
2067
|
+
return /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)(import_ink_renderer9.Text, { dimColor: true, children: [
|
|
2068
|
+
" ",
|
|
2069
|
+
direction === "up" ? "\u2191" : "\u2193",
|
|
2070
|
+
" ",
|
|
2071
|
+
count,
|
|
2072
|
+
" more"
|
|
2073
|
+
] });
|
|
2074
|
+
}
|
|
2075
|
+
function Select({
|
|
2076
|
+
options,
|
|
2077
|
+
defaultValue,
|
|
2078
|
+
onChange,
|
|
2079
|
+
onCancel,
|
|
2080
|
+
title,
|
|
2081
|
+
maxVisible
|
|
2082
|
+
}) {
|
|
2083
|
+
const handleSelect = (0, import_react8.useCallback)(
|
|
2084
|
+
(index) => onChange(options[index].value),
|
|
2085
|
+
[onChange, options]
|
|
2086
|
+
);
|
|
2087
|
+
const { focusIndex, scrollOffset, visibleOptions, max, total } = useListNavigation({ options, maxVisible, onCancel, onSelect: handleSelect });
|
|
2088
|
+
return /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)(import_ink_renderer9.Box, { flexDirection: "column", children: [
|
|
2089
|
+
title && /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(import_ink_renderer9.Box, { marginBottom: 1, children: /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(import_ink_renderer9.Text, { bold: true, children: title }) }),
|
|
2090
|
+
scrollOffset > 0 && /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(ScrollHint, { count: scrollOffset, direction: "up" }),
|
|
2091
|
+
visibleOptions.map((opt, i) => {
|
|
2092
|
+
const realIndex = scrollOffset + i;
|
|
2093
|
+
const isFocused = realIndex === focusIndex;
|
|
2094
|
+
const isSelected = opt.value === defaultValue;
|
|
2095
|
+
const isDisabled = opt.disabled === true;
|
|
2096
|
+
return /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)(import_ink_renderer9.Box, { children: [
|
|
2097
|
+
/* @__PURE__ */ (0, import_jsx_runtime9.jsxs)(import_ink_renderer9.Text, { color: isFocused ? "cyan" : void 0, children: [
|
|
2098
|
+
isFocused ? "\u276F" : " ",
|
|
2099
|
+
" "
|
|
2100
|
+
] }),
|
|
2101
|
+
/* @__PURE__ */ (0, import_jsx_runtime9.jsxs)(
|
|
2102
|
+
import_ink_renderer9.Text,
|
|
2103
|
+
{
|
|
2104
|
+
color: isDisabled ? "gray" : isFocused ? "cyan" : void 0,
|
|
2105
|
+
bold: isFocused,
|
|
2106
|
+
dimColor: isDisabled,
|
|
2107
|
+
children: [
|
|
2108
|
+
realIndex + 1,
|
|
2109
|
+
". ",
|
|
2110
|
+
opt.label
|
|
2111
|
+
]
|
|
2112
|
+
}
|
|
2113
|
+
),
|
|
2114
|
+
isSelected && /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(import_ink_renderer9.Text, { color: "green", children: " \u2713" }),
|
|
2115
|
+
opt.description && /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)(import_ink_renderer9.Text, { dimColor: true, children: [
|
|
2116
|
+
" ",
|
|
2117
|
+
opt.description
|
|
2118
|
+
] })
|
|
2119
|
+
] }, realIndex);
|
|
2120
|
+
}),
|
|
2121
|
+
scrollOffset + max < total && /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(ScrollHint, { count: total - scrollOffset - max, direction: "down" }),
|
|
2122
|
+
/* @__PURE__ */ (0, import_jsx_runtime9.jsx)(import_ink_renderer9.Box, { marginTop: 1, children: /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(import_ink_renderer9.Text, { dimColor: true, children: "Enter to confirm \xB7 Esc to exit" }) })
|
|
2123
|
+
] });
|
|
2124
|
+
}
|
|
2125
|
+
function MultiSelect({
|
|
2126
|
+
options,
|
|
2127
|
+
selectedValues = [],
|
|
2128
|
+
onToggle,
|
|
2129
|
+
onConfirm,
|
|
2130
|
+
onCancel,
|
|
2131
|
+
title,
|
|
2132
|
+
maxVisible
|
|
2133
|
+
}) {
|
|
2134
|
+
const [selected, setSelected] = (0, import_react8.useState)(() => new Set(selectedValues));
|
|
2135
|
+
const handleConfirm = (0, import_react8.useCallback)(
|
|
2136
|
+
() => onConfirm(Array.from(selected)),
|
|
2137
|
+
[onConfirm, selected]
|
|
2138
|
+
);
|
|
2139
|
+
const handleSpace = (0, import_react8.useCallback)(
|
|
2140
|
+
(input, _key, focusIndex2) => {
|
|
2141
|
+
if (input !== " ") return false;
|
|
2142
|
+
const opt = options[focusIndex2];
|
|
2143
|
+
if (!opt || opt.disabled) return true;
|
|
2144
|
+
setSelected((prev) => {
|
|
2145
|
+
const next = new Set(prev);
|
|
2146
|
+
if (next.has(opt.value)) next.delete(opt.value);
|
|
2147
|
+
else next.add(opt.value);
|
|
2148
|
+
return next;
|
|
2149
|
+
});
|
|
2150
|
+
onToggle(opt.value);
|
|
2151
|
+
return true;
|
|
2152
|
+
},
|
|
2153
|
+
[options, onToggle]
|
|
2154
|
+
);
|
|
2155
|
+
const { focusIndex, scrollOffset, visibleOptions, max, total } = useListNavigation({
|
|
2156
|
+
options,
|
|
2157
|
+
maxVisible,
|
|
2158
|
+
onCancel,
|
|
2159
|
+
onSelect: handleConfirm,
|
|
2160
|
+
extraHandler: handleSpace
|
|
2161
|
+
});
|
|
2162
|
+
return /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)(import_ink_renderer9.Box, { flexDirection: "column", children: [
|
|
2163
|
+
title && /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(import_ink_renderer9.Box, { marginBottom: 1, children: /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(import_ink_renderer9.Text, { bold: true, children: title }) }),
|
|
2164
|
+
scrollOffset > 0 && /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(ScrollHint, { count: scrollOffset, direction: "up" }),
|
|
2165
|
+
visibleOptions.map((opt, i) => {
|
|
2166
|
+
const realIndex = scrollOffset + i;
|
|
2167
|
+
const isFocused = realIndex === focusIndex;
|
|
2168
|
+
const isChecked = selected.has(opt.value);
|
|
2169
|
+
const isDisabled = opt.disabled === true;
|
|
2170
|
+
return /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)(import_ink_renderer9.Box, { children: [
|
|
2171
|
+
/* @__PURE__ */ (0, import_jsx_runtime9.jsxs)(import_ink_renderer9.Text, { color: isFocused ? "cyan" : void 0, children: [
|
|
2172
|
+
isFocused ? "\u276F" : " ",
|
|
2173
|
+
" "
|
|
2174
|
+
] }),
|
|
2175
|
+
/* @__PURE__ */ (0, import_jsx_runtime9.jsxs)(
|
|
2176
|
+
import_ink_renderer9.Text,
|
|
2177
|
+
{
|
|
2178
|
+
color: isDisabled ? "gray" : isFocused ? "cyan" : void 0,
|
|
2179
|
+
bold: isFocused,
|
|
2180
|
+
dimColor: isDisabled,
|
|
2181
|
+
children: [
|
|
2182
|
+
isChecked ? "[x]" : "[ ]",
|
|
2183
|
+
" ",
|
|
2184
|
+
realIndex + 1,
|
|
2185
|
+
". ",
|
|
2186
|
+
opt.label
|
|
2187
|
+
]
|
|
2188
|
+
}
|
|
2189
|
+
),
|
|
2190
|
+
opt.description && /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)(import_ink_renderer9.Text, { dimColor: true, children: [
|
|
2191
|
+
" ",
|
|
2192
|
+
opt.description
|
|
2193
|
+
] })
|
|
2194
|
+
] }, realIndex);
|
|
2195
|
+
}),
|
|
2196
|
+
scrollOffset + max < total && /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(ScrollHint, { count: total - scrollOffset - max, direction: "down" }),
|
|
2197
|
+
/* @__PURE__ */ (0, import_jsx_runtime9.jsx)(import_ink_renderer9.Box, { marginTop: 1, children: /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(import_ink_renderer9.Text, { dimColor: true, children: "Space to toggle \xB7 Enter to confirm \xB7 Esc to exit" }) })
|
|
2198
|
+
] });
|
|
2199
|
+
}
|
|
2200
|
+
|
|
2201
|
+
// src/MessageList.tsx
|
|
2202
|
+
var import_ink_renderer10 = require("@claude-code-kit/ink-renderer");
|
|
2203
|
+
var import_jsx_runtime10 = require("react/jsx-runtime");
|
|
2204
|
+
var ROLE_CONFIG = {
|
|
2205
|
+
user: { icon: "\u276F", label: "You", color: "cyan" },
|
|
2206
|
+
assistant: { icon: "\u25CF", label: "Claude", color: "#DA7756" },
|
|
2207
|
+
system: { icon: "\u273B", label: "System", color: void 0 }
|
|
2208
|
+
};
|
|
2209
|
+
function MessageItem({
|
|
2210
|
+
message,
|
|
2211
|
+
renderMessage
|
|
2212
|
+
}) {
|
|
2213
|
+
if (renderMessage) {
|
|
2214
|
+
return renderMessage(message);
|
|
2215
|
+
}
|
|
2216
|
+
const config = ROLE_CONFIG[message.role];
|
|
2217
|
+
const isSystem = message.role === "system";
|
|
2218
|
+
return /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)(import_ink_renderer10.Box, { flexDirection: "column", children: [
|
|
2219
|
+
/* @__PURE__ */ (0, import_jsx_runtime10.jsxs)(import_ink_renderer10.Box, { children: [
|
|
2220
|
+
/* @__PURE__ */ (0, import_jsx_runtime10.jsx)(import_ink_renderer10.Text, { color: config.color, dimColor: isSystem, children: config.icon }),
|
|
2221
|
+
/* @__PURE__ */ (0, import_jsx_runtime10.jsxs)(import_ink_renderer10.Text, { color: config.color, dimColor: isSystem, bold: !isSystem, children: [
|
|
2222
|
+
" ",
|
|
2223
|
+
config.label
|
|
2224
|
+
] })
|
|
2225
|
+
] }),
|
|
2226
|
+
message.content.split("\n").map((line, i) => /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(import_ink_renderer10.Box, { marginLeft: 2, children: /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(import_ink_renderer10.Text, { dimColor: isSystem, children: line }) }, i))
|
|
2227
|
+
] });
|
|
2228
|
+
}
|
|
2229
|
+
function MessageList({
|
|
2230
|
+
messages,
|
|
2231
|
+
streamingContent,
|
|
2232
|
+
renderMessage
|
|
2233
|
+
}) {
|
|
2234
|
+
return /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)(import_ink_renderer10.Box, { flexDirection: "column", children: [
|
|
2235
|
+
messages.map((message, i) => /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(import_ink_renderer10.Box, { flexDirection: "column", marginTop: i > 0 ? 1 : 0, children: /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(MessageItem, { message, renderMessage }) }, message.id)),
|
|
2236
|
+
streamingContent != null && streamingContent.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)(import_ink_renderer10.Box, { flexDirection: "column", marginTop: messages.length > 0 ? 1 : 0, children: [
|
|
2237
|
+
/* @__PURE__ */ (0, import_jsx_runtime10.jsxs)(import_ink_renderer10.Box, { children: [
|
|
2238
|
+
/* @__PURE__ */ (0, import_jsx_runtime10.jsx)(import_ink_renderer10.Text, { color: "#DA7756", children: "\u25CF" }),
|
|
2239
|
+
/* @__PURE__ */ (0, import_jsx_runtime10.jsxs)(import_ink_renderer10.Text, { color: "#DA7756", bold: true, children: [
|
|
2240
|
+
" ",
|
|
2241
|
+
"Claude"
|
|
2242
|
+
] })
|
|
2243
|
+
] }),
|
|
2244
|
+
streamingContent.split("\n").map((line, i) => /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(import_ink_renderer10.Box, { marginLeft: 2, children: /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)(import_ink_renderer10.Text, { children: [
|
|
2245
|
+
line,
|
|
2246
|
+
i === streamingContent.split("\n").length - 1 && /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(import_ink_renderer10.Text, { color: "#DA7756", children: "\u2588" })
|
|
2247
|
+
] }) }, i))
|
|
2248
|
+
] })
|
|
2249
|
+
] });
|
|
2250
|
+
}
|
|
2251
|
+
|
|
2252
|
+
// src/StreamingText.tsx
|
|
2253
|
+
var import_react9 = require("react");
|
|
2254
|
+
var import_ink_renderer11 = require("@claude-code-kit/ink-renderer");
|
|
2255
|
+
var import_jsx_runtime11 = require("react/jsx-runtime");
|
|
2256
|
+
function StreamingText({
|
|
2257
|
+
text,
|
|
2258
|
+
speed = 3,
|
|
2259
|
+
interval = 20,
|
|
2260
|
+
onComplete,
|
|
2261
|
+
color
|
|
2262
|
+
}) {
|
|
2263
|
+
const [revealed, setRevealed] = (0, import_react9.useState)(0);
|
|
2264
|
+
const onCompleteRef = (0, import_react9.useRef)(onComplete);
|
|
2265
|
+
onCompleteRef.current = onComplete;
|
|
2266
|
+
(0, import_react9.useEffect)(() => {
|
|
2267
|
+
if (revealed >= text.length) return;
|
|
2268
|
+
const id = setInterval(() => {
|
|
2269
|
+
setRevealed((prev) => {
|
|
2270
|
+
const next = Math.min(prev + speed, text.length);
|
|
2271
|
+
if (next >= text.length) {
|
|
2272
|
+
onCompleteRef.current?.();
|
|
2273
|
+
}
|
|
2274
|
+
return next;
|
|
2275
|
+
});
|
|
2276
|
+
}, interval);
|
|
2277
|
+
return () => clearInterval(id);
|
|
2278
|
+
}, [text.length, speed, interval, revealed >= text.length]);
|
|
2279
|
+
return /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(import_ink_renderer11.Text, { color, children: text.slice(0, revealed) });
|
|
2280
|
+
}
|
|
2281
|
+
|
|
2282
|
+
// src/REPL.tsx
|
|
2283
|
+
var import_react10 = require("react");
|
|
2284
|
+
var import_ink_renderer12 = require("@claude-code-kit/ink-renderer");
|
|
2285
|
+
var import_jsx_runtime12 = require("react/jsx-runtime");
|
|
2286
|
+
function REPL({
|
|
2287
|
+
onSubmit,
|
|
2288
|
+
onExit,
|
|
2289
|
+
messages,
|
|
2290
|
+
isLoading = false,
|
|
2291
|
+
streamingContent,
|
|
2292
|
+
commands = [],
|
|
2293
|
+
model,
|
|
2294
|
+
statusSegments,
|
|
2295
|
+
prefix = "\u276F",
|
|
2296
|
+
placeholder,
|
|
2297
|
+
history: externalHistory,
|
|
2298
|
+
renderMessage,
|
|
2299
|
+
spinner
|
|
2300
|
+
}) {
|
|
2301
|
+
const { exit } = (0, import_ink_renderer12.useApp)();
|
|
2302
|
+
const [inputValue, setInputValue] = (0, import_react10.useState)("");
|
|
2303
|
+
const [internalHistory, setInternalHistory] = (0, import_react10.useState)([]);
|
|
2304
|
+
const submittingRef = (0, import_react10.useRef)(false);
|
|
2305
|
+
const history = externalHistory ?? internalHistory;
|
|
2306
|
+
const promptCommands = commands.map((c) => ({
|
|
2307
|
+
name: c.name,
|
|
2308
|
+
description: c.description
|
|
2309
|
+
}));
|
|
2310
|
+
const handleSubmit = (0, import_react10.useCallback)(
|
|
2311
|
+
(value) => {
|
|
2312
|
+
if (submittingRef.current) return;
|
|
2313
|
+
const trimmed = value.trim();
|
|
2314
|
+
if (!trimmed) return;
|
|
2315
|
+
if (trimmed.startsWith("/")) {
|
|
2316
|
+
const spaceIndex = trimmed.indexOf(" ");
|
|
2317
|
+
const cmdName = spaceIndex >= 0 ? trimmed.slice(1, spaceIndex) : trimmed.slice(1);
|
|
2318
|
+
const cmdArgs = spaceIndex >= 0 ? trimmed.slice(spaceIndex + 1).trim() : "";
|
|
2319
|
+
const cmd = commands.find((c) => c.name === cmdName);
|
|
2320
|
+
if (cmd) {
|
|
2321
|
+
setInputValue("");
|
|
2322
|
+
cmd.onExecute(cmdArgs);
|
|
2323
|
+
return;
|
|
2324
|
+
}
|
|
2325
|
+
}
|
|
2326
|
+
submittingRef.current = true;
|
|
2327
|
+
setInputValue("");
|
|
2328
|
+
if (!externalHistory) {
|
|
2329
|
+
setInternalHistory((prev) => [trimmed, ...prev]);
|
|
2330
|
+
}
|
|
2331
|
+
const result = onSubmit(trimmed);
|
|
2332
|
+
if (result && typeof result.then === "function") {
|
|
2333
|
+
result.finally(() => {
|
|
2334
|
+
submittingRef.current = false;
|
|
2335
|
+
});
|
|
2336
|
+
} else {
|
|
2337
|
+
submittingRef.current = false;
|
|
2338
|
+
}
|
|
2339
|
+
},
|
|
2340
|
+
[commands, onSubmit, externalHistory]
|
|
2341
|
+
);
|
|
2342
|
+
(0, import_ink_renderer12.useInput)(
|
|
2343
|
+
(_input, key) => {
|
|
2344
|
+
if (key.ctrl && _input === "c" && isLoading) {
|
|
2345
|
+
return;
|
|
2346
|
+
}
|
|
2347
|
+
if (key.ctrl && _input === "d") {
|
|
2348
|
+
if (onExit) {
|
|
2349
|
+
onExit();
|
|
2350
|
+
} else {
|
|
2351
|
+
exit();
|
|
2352
|
+
}
|
|
2353
|
+
}
|
|
2354
|
+
},
|
|
2355
|
+
{ isActive: true }
|
|
2356
|
+
);
|
|
2357
|
+
const resolvedSegments = statusSegments ?? buildDefaultSegments(model);
|
|
2358
|
+
return /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)(import_ink_renderer12.Box, { flexDirection: "column", flexGrow: 1, children: [
|
|
2359
|
+
/* @__PURE__ */ (0, import_jsx_runtime12.jsxs)(import_ink_renderer12.Box, { flexDirection: "column", flexGrow: 1, children: [
|
|
2360
|
+
/* @__PURE__ */ (0, import_jsx_runtime12.jsx)(
|
|
2361
|
+
MessageList,
|
|
2362
|
+
{
|
|
2363
|
+
messages,
|
|
2364
|
+
streamingContent,
|
|
2365
|
+
renderMessage
|
|
2366
|
+
}
|
|
2367
|
+
),
|
|
2368
|
+
isLoading && !streamingContent && /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(import_ink_renderer12.Box, { marginTop: messages.length > 0 ? 1 : 0, children: spinner ?? /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(Spinner, {}) })
|
|
2369
|
+
] }),
|
|
2370
|
+
/* @__PURE__ */ (0, import_jsx_runtime12.jsx)(Divider, {}),
|
|
2371
|
+
/* @__PURE__ */ (0, import_jsx_runtime12.jsx)(
|
|
2372
|
+
PromptInput,
|
|
2373
|
+
{
|
|
2374
|
+
value: inputValue,
|
|
2375
|
+
onChange: setInputValue,
|
|
2376
|
+
onSubmit: handleSubmit,
|
|
2377
|
+
prefix,
|
|
2378
|
+
placeholder,
|
|
2379
|
+
disabled: isLoading,
|
|
2380
|
+
commands: promptCommands,
|
|
2381
|
+
history
|
|
2382
|
+
}
|
|
2383
|
+
),
|
|
2384
|
+
/* @__PURE__ */ (0, import_jsx_runtime12.jsx)(Divider, {}),
|
|
2385
|
+
resolvedSegments.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(StatusLine, { segments: resolvedSegments })
|
|
2386
|
+
] });
|
|
2387
|
+
}
|
|
2388
|
+
function buildDefaultSegments(model) {
|
|
2389
|
+
if (!model) return [];
|
|
2390
|
+
return [{ content: model, color: "green" }];
|
|
2391
|
+
}
|
|
2392
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
2393
|
+
0 && (module.exports = {
|
|
2394
|
+
CommandRegistry,
|
|
2395
|
+
DEFAULT_BINDINGS,
|
|
2396
|
+
Divider,
|
|
2397
|
+
KeybindingSetup,
|
|
2398
|
+
MessageList,
|
|
2399
|
+
MultiSelect,
|
|
2400
|
+
ProgressBar,
|
|
2401
|
+
PromptInput,
|
|
2402
|
+
REPL,
|
|
2403
|
+
Select,
|
|
2404
|
+
Spinner,
|
|
2405
|
+
StatusIcon,
|
|
2406
|
+
StatusLine,
|
|
2407
|
+
StreamingText,
|
|
2408
|
+
clearCommand,
|
|
2409
|
+
createCommandRegistry,
|
|
2410
|
+
defineCommand,
|
|
2411
|
+
defineJSXCommand,
|
|
2412
|
+
defineLocalCommand,
|
|
2413
|
+
exitCommand,
|
|
2414
|
+
helpCommand,
|
|
2415
|
+
useKeybinding,
|
|
2416
|
+
useKeybindings,
|
|
2417
|
+
useStatusLine,
|
|
2418
|
+
...require("@claude-code-kit/ink-renderer")
|
|
2419
|
+
});
|