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