@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.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
+ };