Add files using upload-large-folder tool
Browse files- src/components/CtrlOToExpand.tsx +51 -0
- src/components/ExitFlow.tsx +48 -0
- src/components/ExportDialog.tsx +128 -0
- src/components/FileEditToolDiff.tsx +181 -0
- src/components/FileEditToolUseRejectedMessage.tsx +170 -0
- src/components/FilePathLink.tsx +43 -0
- src/components/IdeStatusIndicator.tsx +58 -0
- src/components/IdleReturnDialog.tsx +118 -0
- src/components/InterruptedByUser.tsx +15 -0
- src/components/InvalidConfigDialog.tsx +156 -0
- src/components/InvalidSettingsDialog.tsx +89 -0
- src/components/KeybindingWarnings.tsx +55 -0
- src/components/LanguagePicker.tsx +86 -0
- src/keybindings/KeybindingContext.tsx +243 -0
- src/keybindings/KeybindingProviderSetup.tsx +308 -0
- src/keybindings/defaultBindings.ts +340 -0
- src/keybindings/loadUserBindings.ts +472 -0
- src/keybindings/match.ts +120 -0
- src/keybindings/parser.ts +203 -0
- src/keybindings/reservedShortcuts.ts +127 -0
- src/keybindings/resolver.ts +244 -0
- src/keybindings/schema.ts +236 -0
- src/keybindings/shortcutFormat.ts +63 -0
- src/keybindings/template.ts +52 -0
- src/keybindings/useKeybinding.ts +196 -0
- src/keybindings/useShortcutDisplay.ts +59 -0
- src/keybindings/validate.ts +498 -0
- src/memdir/findRelevantMemories.ts +141 -0
- src/memdir/memdir.ts +507 -0
- src/memdir/memoryAge.ts +53 -0
- src/memdir/memoryScan.ts +94 -0
- src/memdir/memoryTypes.ts +271 -0
- src/memdir/paths.ts +278 -0
- src/memdir/teamMemPaths.ts +292 -0
- src/memdir/teamMemPrompts.ts +100 -0
- src/migrations/migrateBypassPermissionsAcceptedToSettings.ts +40 -0
- src/migrations/migrateEnableAllProjectMcpServersToSettings.ts +118 -0
- src/migrations/migrateFennecToOpus.ts +45 -0
- src/migrations/migrateLegacyOpusToCurrent.ts +57 -0
- src/migrations/migrateOpusToOpus1m.ts +43 -0
- src/migrations/migrateReplBridgeEnabledToRemoteControlAtStartup.ts +22 -0
- src/migrations/migrateSonnet1mToSonnet45.ts +48 -0
- src/migrations/migrateSonnet45ToSonnet46.ts +67 -0
- src/migrations/resetAutoModeOptInForDefaultOffer.ts +51 -0
- src/moreright/useMoreRight.tsx +26 -0
- src/outputStyles/loadOutputStylesDir.ts +98 -0
- src/query/deps.ts +40 -0
- src/remote/RemoteSessionManager.ts +343 -0
- src/remote/SessionsWebSocket.ts +404 -0
- src/remote/sdkMessageAdapter.ts +302 -0
src/components/CtrlOToExpand.tsx
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { c as _c } from "react/compiler-runtime";
|
| 2 |
+
import chalk from 'chalk';
|
| 3 |
+
import React, { useContext } from 'react';
|
| 4 |
+
import { Text } from '../ink.js';
|
| 5 |
+
import { getShortcutDisplay } from '../keybindings/shortcutFormat.js';
|
| 6 |
+
import { useShortcutDisplay } from '../keybindings/useShortcutDisplay.js';
|
| 7 |
+
import { KeyboardShortcutHint } from './design-system/KeyboardShortcutHint.js';
|
| 8 |
+
import { InVirtualListContext } from './messageActions.js';
|
| 9 |
+
|
| 10 |
+
// Context to track if we're inside a sub agent
|
| 11 |
+
// Similar to MessageResponseContext, this helps us avoid showing
|
| 12 |
+
// too many "(ctrl+o to expand)" hints in sub agent output
|
| 13 |
+
const SubAgentContext = React.createContext(false);
|
| 14 |
+
export function SubAgentProvider(t0) {
|
| 15 |
+
const $ = _c(2);
|
| 16 |
+
const {
|
| 17 |
+
children
|
| 18 |
+
} = t0;
|
| 19 |
+
let t1;
|
| 20 |
+
if ($[0] !== children) {
|
| 21 |
+
t1 = <SubAgentContext.Provider value={true}>{children}</SubAgentContext.Provider>;
|
| 22 |
+
$[0] = children;
|
| 23 |
+
$[1] = t1;
|
| 24 |
+
} else {
|
| 25 |
+
t1 = $[1];
|
| 26 |
+
}
|
| 27 |
+
return t1;
|
| 28 |
+
}
|
| 29 |
+
export function CtrlOToExpand() {
|
| 30 |
+
const $ = _c(2);
|
| 31 |
+
const isInSubAgent = useContext(SubAgentContext);
|
| 32 |
+
const inVirtualList = useContext(InVirtualListContext);
|
| 33 |
+
const expandShortcut = useShortcutDisplay("app:toggleTranscript", "Global", "ctrl+o");
|
| 34 |
+
if (isInSubAgent || inVirtualList) {
|
| 35 |
+
return null;
|
| 36 |
+
}
|
| 37 |
+
let t0;
|
| 38 |
+
if ($[0] !== expandShortcut) {
|
| 39 |
+
t0 = <Text dimColor={true}><KeyboardShortcutHint shortcut={expandShortcut} action="expand" parens={true} /></Text>;
|
| 40 |
+
$[0] = expandShortcut;
|
| 41 |
+
$[1] = t0;
|
| 42 |
+
} else {
|
| 43 |
+
t0 = $[1];
|
| 44 |
+
}
|
| 45 |
+
return t0;
|
| 46 |
+
}
|
| 47 |
+
export function ctrlOToExpand(): string {
|
| 48 |
+
const shortcut = getShortcutDisplay('app:toggleTranscript', 'Global', 'ctrl+o');
|
| 49 |
+
return chalk.dim(`(${shortcut} to expand)`);
|
| 50 |
+
}
|
| 51 |
+
//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJuYW1lcyI6WyJjaGFsayIsIlJlYWN0IiwidXNlQ29udGV4dCIsIlRleHQiLCJnZXRTaG9ydGN1dERpc3BsYXkiLCJ1c2VTaG9ydGN1dERpc3BsYXkiLCJLZXlib2FyZFNob3J0Y3V0SGludCIsIkluVmlydHVhbExpc3RDb250ZXh0IiwiU3ViQWdlbnRDb250ZXh0IiwiY3JlYXRlQ29udGV4dCIsIlN1YkFnZW50UHJvdmlkZXIiLCJ0MCIsIiQiLCJfYyIsImNoaWxkcmVuIiwidDEiLCJDdHJsT1RvRXhwYW5kIiwiaXNJblN1YkFnZW50IiwiaW5WaXJ0dWFsTGlzdCIsImV4cGFuZFNob3J0Y3V0IiwiY3RybE9Ub0V4cGFuZCIsInNob3J0Y3V0IiwiZGltIl0sInNvdXJjZXMiOlsiQ3RybE9Ub0V4cGFuZC50c3giXSwic291cmNlc0NvbnRlbnQiOlsiaW1wb3J0IGNoYWxrIGZyb20gJ2NoYWxrJ1xuaW1wb3J0IFJlYWN0LCB7IHVzZUNvbnRleHQgfSBmcm9tICdyZWFjdCdcbmltcG9ydCB7IFRleHQgfSBmcm9tICcuLi9pbmsuanMnXG5pbXBvcnQgeyBnZXRTaG9ydGN1dERpc3BsYXkgfSBmcm9tICcuLi9rZXliaW5kaW5ncy9zaG9ydGN1dEZvcm1hdC5qcydcbmltcG9ydCB7IHVzZVNob3J0Y3V0RGlzcGxheSB9IGZyb20gJy4uL2tleWJpbmRpbmdzL3VzZVNob3J0Y3V0RGlzcGxheS5qcydcbmltcG9ydCB7IEtleWJvYXJkU2hvcnRjdXRIaW50IH0gZnJvbSAnLi9kZXNpZ24tc3lzdGVtL0tleWJvYXJkU2hvcnRjdXRIaW50LmpzJ1xuaW1wb3J0IHsgSW5WaXJ0dWFsTGlzdENvbnRleHQgfSBmcm9tICcuL21lc3NhZ2VBY3Rpb25zLmpzJ1xuXG4vLyBDb250ZXh0IHRvIHRyYWNrIGlmIHdlJ3JlIGluc2lkZSBhIHN1YiBhZ2VudFxuLy8gU2ltaWxhciB0byBNZXNzYWdlUmVzcG9uc2VDb250ZXh0LCB0aGlzIGhlbHBzIHVzIGF2b2lkIHNob3dpbmdcbi8vIHRvbyBtYW55IFwiKGN0cmwrbyB0byBleHBhbmQpXCIgaGludHMgaW4gc3ViIGFnZW50IG91dHB1dFxuY29uc3QgU3ViQWdlbnRDb250ZXh0ID0gUmVhY3QuY3JlYXRlQ29udGV4dChmYWxzZSlcblxuZXhwb3J0IGZ1bmN0aW9uIFN1YkFnZW50UHJvdmlkZXIoe1xuICBjaGlsZHJlbixcbn06IHtcbiAgY2hpbGRyZW46IFJlYWN0LlJlYWN0Tm9kZVxufSk6IFJlYWN0LlJlYWN0Tm9kZSB7XG4gIHJldHVybiAoXG4gICAgPFN1YkFnZW50Q29udGV4dC5Qcm92aWRlciB2YWx1ZT17dHJ1ZX0+e2NoaWxkcmVufTwvU3ViQWdlbnRDb250ZXh0LlByb3ZpZGVyPlxuICApXG59XG5cbmV4cG9ydCBmdW5jdGlvbiBDdHJsT1RvRXhwYW5kKCk6IFJlYWN0LlJlYWN0Tm9kZSB7XG4gIGNvbnN0IGlzSW5TdWJBZ2VudCA9IHVzZUNvbnRleHQoU3ViQWdlbnRDb250ZXh0KVxuICBjb25zdCBpblZpcnR1YWxMaXN0ID0gdXNlQ29udGV4dChJblZpcnR1YWxMaXN0Q29udGV4dClcbiAgY29uc3QgZXhwYW5kU2hvcnRjdXQgPSB1c2VTaG9ydGN1dERpc3BsYXkoXG4gICAgJ2FwcDp0b2dnbGVUcmFuc2NyaXB0JyxcbiAgICAnR2xvYmFsJyxcbiAgICAnY3RybCtvJyxcbiAgKVxuICBpZiAoaXNJblN1YkFnZW50IHx8IGluVmlydHVhbExpc3QpIHtcbiAgICByZXR1cm4gbnVsbFxuICB9XG4gIHJldHVybiAoXG4gICAgPFRleHQgZGltQ29sb3I+XG4gICAgICA8S2V5Ym9hcmRTaG9ydGN1dEhpbnQgc2hvcnRjdXQ9e2V4cGFuZFNob3J0Y3V0fSBhY3Rpb249XCJleHBhbmRcIiBwYXJlbnMgLz5cbiAgICA8L1RleHQ+XG4gIClcbn1cblxuZXhwb3J0IGZ1bmN0aW9uIGN0cmxPVG9FeHBhbmQoKTogc3RyaW5nIHtcbiAgY29uc3Qgc2hvcnRjdXQgPSBnZXRTaG9ydGN1dERpc3BsYXkoXG4gICAgJ2FwcDp0b2dnbGVUcmFuc2NyaXB0JyxcbiAgICAnR2xvYmFsJyxcbiAgICAnY3RybCtvJyxcbiAgKVxuICByZXR1cm4gY2hhbGsuZGltKGAoJHtzaG9ydGN1dH0gdG8gZXhwYW5kKWApXG59XG4iXSwibWFwcGluZ3MiOiI7QUFBQSxPQUFPQSxLQUFLLE1BQU0sT0FBTztBQUN6QixPQUFPQyxLQUFLLElBQUlDLFVBQVUsUUFBUSxPQUFPO0FBQ3pDLFNBQVNDLElBQUksUUFBUSxXQUFXO0FBQ2hDLFNBQVNDLGtCQUFrQixRQUFRLGtDQUFrQztBQUNyRSxTQUFTQyxrQkFBa0IsUUFBUSxzQ0FBc0M7QUFDekUsU0FBU0Msb0JBQW9CLFFBQVEseUNBQXlDO0FBQzlFLFNBQVNDLG9CQUFvQixRQUFRLHFCQUFxQjs7QUFFMUQ7QUFDQTtBQUNBO0FBQ0EsTUFBTUMsZUFBZSxHQUFHUCxLQUFLLENBQUNRLGFBQWEsQ0FBQyxLQUFLLENBQUM7QUFFbEQsT0FBTyxTQUFBQyxpQkFBQUMsRUFBQTtFQUFBLE1BQUFDLENBQUEsR0FBQUMsRUFBQTtFQUEwQjtJQUFBQztFQUFBLElBQUFILEVBSWhDO0VBQUEsSUFBQUksRUFBQTtFQUFBLElBQUFILENBQUEsUUFBQUUsUUFBQTtJQUVHQyxFQUFBLDZCQUFpQyxLQUFJLENBQUosS0FBRyxDQUFDLENBQUdELFNBQU8sQ0FBRSwyQkFBMkI7SUFBQUYsQ0FBQSxNQUFBRSxRQUFBO0lBQUFGLENBQUEsTUFBQUcsRUFBQTtFQUFBO0lBQUFBLEVBQUEsR0FBQUgsQ0FBQTtFQUFBO0VBQUEsT0FBNUVHLEVBQTRFO0FBQUE7QUFJaEYsT0FBTyxTQUFBQyxjQUFBO0VBQUEsTUFBQUosQ0FBQSxHQUFBQyxFQUFBO0VBQ0wsTUFBQUksWUFBQSxHQUFxQmYsVUFBVSxDQUFDTSxlQUFlLENBQUM7RUFDaEQsTUFBQVUsYUFBQSxHQUFzQmhCLFVBQVUsQ0FBQ0ssb0JBQW9CLENBQUM7RUFDdEQsTUFBQVksY0FBQSxHQUF1QmQsa0JBQWtCLENBQ3ZDLHNCQUFzQixFQUN0QixRQUFRLEVBQ1IsUUFDRixDQUFDO0VBQ0QsSUFBSVksWUFBNkIsSUFBN0JDLGFBQTZCO0lBQUEsT0FDeEIsSUFBSTtFQUFBO0VBQ1osSUFBQVAsRUFBQTtFQUFBLElBQUFDLENBQUEsUUFBQU8sY0FBQTtJQUVDUixFQUFBLElBQUMsSUFBSSxDQUFDLFFBQVEsQ0FBUixLQUFPLENBQUMsQ0FDWixDQUFDLG9CQUFvQixDQUFXUSxRQUFjLENBQWRBLGVBQWEsQ0FBQyxDQUFTLE1BQVEsQ0FBUixRQUFRLENBQUMsTUFBTSxDQUFOLEtBQUssQ0FBQyxHQUN4RSxFQUZDLElBQUksQ0FFRTtJQUFBUCxDQUFBLE1BQUFPLGNBQUE7SUFBQVAsQ0FBQSxNQUFBRCxFQUFBO0VBQUE7SUFBQUEsRUFBQSxHQUFBQyxDQUFBO0VBQUE7RUFBQSxPQUZQRCxFQUVPO0FBQUE7QUFJWCxPQUFPLFNBQVNTLGFBQWFBLENBQUEsQ0FBRSxFQUFFLE1BQU0sQ0FBQztFQUN0QyxNQUFNQyxRQUFRLEdBQUdqQixrQkFBa0IsQ0FDakMsc0JBQXNCLEVBQ3RCLFFBQVEsRUFDUixRQUNGLENBQUM7RUFDRCxPQUFPSixLQUFLLENBQUNzQixHQUFHLENBQUMsSUFBSUQsUUFBUSxhQUFhLENBQUM7QUFDN0MiLCJpZ25vcmVMaXN0IjpbXX0=
|
src/components/ExitFlow.tsx
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { c as _c } from "react/compiler-runtime";
|
| 2 |
+
import sample from 'lodash-es/sample.js';
|
| 3 |
+
import React from 'react';
|
| 4 |
+
import { gracefulShutdown } from '../utils/gracefulShutdown.js';
|
| 5 |
+
import { WorktreeExitDialog } from './WorktreeExitDialog.js';
|
| 6 |
+
const GOODBYE_MESSAGES = ['Goodbye!', 'See ya!', 'Bye!', 'Catch you later!'];
|
| 7 |
+
function getRandomGoodbyeMessage(): string {
|
| 8 |
+
return sample(GOODBYE_MESSAGES) ?? 'Goodbye!';
|
| 9 |
+
}
|
| 10 |
+
type Props = {
|
| 11 |
+
onDone: (message?: string) => void;
|
| 12 |
+
onCancel?: () => void;
|
| 13 |
+
showWorktree: boolean;
|
| 14 |
+
};
|
| 15 |
+
export function ExitFlow(t0) {
|
| 16 |
+
const $ = _c(5);
|
| 17 |
+
const {
|
| 18 |
+
showWorktree,
|
| 19 |
+
onDone,
|
| 20 |
+
onCancel
|
| 21 |
+
} = t0;
|
| 22 |
+
let t1;
|
| 23 |
+
if ($[0] !== onDone) {
|
| 24 |
+
t1 = async function onExit(resultMessage) {
|
| 25 |
+
onDone(resultMessage ?? getRandomGoodbyeMessage());
|
| 26 |
+
await gracefulShutdown(0, "prompt_input_exit");
|
| 27 |
+
};
|
| 28 |
+
$[0] = onDone;
|
| 29 |
+
$[1] = t1;
|
| 30 |
+
} else {
|
| 31 |
+
t1 = $[1];
|
| 32 |
+
}
|
| 33 |
+
const onExit = t1;
|
| 34 |
+
if (showWorktree) {
|
| 35 |
+
let t2;
|
| 36 |
+
if ($[2] !== onCancel || $[3] !== onExit) {
|
| 37 |
+
t2 = <WorktreeExitDialog onDone={onExit} onCancel={onCancel} />;
|
| 38 |
+
$[2] = onCancel;
|
| 39 |
+
$[3] = onExit;
|
| 40 |
+
$[4] = t2;
|
| 41 |
+
} else {
|
| 42 |
+
t2 = $[4];
|
| 43 |
+
}
|
| 44 |
+
return t2;
|
| 45 |
+
}
|
| 46 |
+
return null;
|
| 47 |
+
}
|
| 48 |
+
//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJuYW1lcyI6WyJzYW1wbGUiLCJSZWFjdCIsImdyYWNlZnVsU2h1dGRvd24iLCJXb3JrdHJlZUV4aXREaWFsb2ciLCJHT09EQllFX01FU1NBR0VTIiwiZ2V0UmFuZG9tR29vZGJ5ZU1lc3NhZ2UiLCJQcm9wcyIsIm9uRG9uZSIsIm1lc3NhZ2UiLCJvbkNhbmNlbCIsInNob3dXb3JrdHJlZSIsIkV4aXRGbG93IiwidDAiLCIkIiwiX2MiLCJ0MSIsIm9uRXhpdCIsInJlc3VsdE1lc3NhZ2UiLCJ0MiJdLCJzb3VyY2VzIjpbIkV4aXRGbG93LnRzeCJdLCJzb3VyY2VzQ29udGVudCI6WyJpbXBvcnQgc2FtcGxlIGZyb20gJ2xvZGFzaC1lcy9zYW1wbGUuanMnXG5pbXBvcnQgUmVhY3QgZnJvbSAncmVhY3QnXG5pbXBvcnQgeyBncmFjZWZ1bFNodXRkb3duIH0gZnJvbSAnLi4vdXRpbHMvZ3JhY2VmdWxTaHV0ZG93bi5qcydcbmltcG9ydCB7IFdvcmt0cmVlRXhpdERpYWxvZyB9IGZyb20gJy4vV29ya3RyZWVFeGl0RGlhbG9nLmpzJ1xuXG5jb25zdCBHT09EQllFX01FU1NBR0VTID0gWydHb29kYnllIScsICdTZWUgeWEhJywgJ0J5ZSEnLCAnQ2F0Y2ggeW91IGxhdGVyISddXG5cbmZ1bmN0aW9uIGdldFJhbmRvbUdvb2RieWVNZXNzYWdlKCk6IHN0cmluZyB7XG4gIHJldHVybiBzYW1wbGUoR09PREJZRV9NRVNTQUdFUykgPz8gJ0dvb2RieWUhJ1xufVxuXG50eXBlIFByb3BzID0ge1xuICBvbkRvbmU6IChtZXNzYWdlPzogc3RyaW5nKSA9PiB2b2lkXG4gIG9uQ2FuY2VsPzogKCkgPT4gdm9pZFxuICBzaG93V29ya3RyZWU6IGJvb2xlYW5cbn1cblxuZXhwb3J0IGZ1bmN0aW9uIEV4aXRGbG93KHtcbiAgc2hvd1dvcmt0cmVlLFxuICBvbkRvbmUsXG4gIG9uQ2FuY2VsLFxufTogUHJvcHMpOiBSZWFjdC5SZWFjdE5vZGUge1xuICBhc3luYyBmdW5jdGlvbiBvbkV4aXQocmVzdWx0TWVzc2FnZT86IHN0cmluZykge1xuICAgIG9uRG9uZShyZXN1bHRNZXNzYWdlID8/IGdldFJhbmRvbUdvb2RieWVNZXNzYWdlKCkpXG4gICAgYXdhaXQgZ3JhY2VmdWxTaHV0ZG93bigwLCAncHJvbXB0X2lucHV0X2V4aXQnKVxuICB9XG5cbiAgaWYgKHNob3dXb3JrdHJlZSkge1xuICAgIHJldHVybiA8V29ya3RyZWVFeGl0RGlhbG9nIG9uRG9uZT17b25FeGl0fSBvbkNhbmNlbD17b25DYW5jZWx9IC8+XG4gIH1cblxuICByZXR1cm4gbnVsbFxufVxuIl0sIm1hcHBpbmdzIjoiO0FBQUEsT0FBT0EsTUFBTSxNQUFNLHFCQUFxQjtBQUN4QyxPQUFPQyxLQUFLLE1BQU0sT0FBTztBQUN6QixTQUFTQyxnQkFBZ0IsUUFBUSw4QkFBOEI7QUFDL0QsU0FBU0Msa0JBQWtCLFFBQVEseUJBQXlCO0FBRTVELE1BQU1DLGdCQUFnQixHQUFHLENBQUMsVUFBVSxFQUFFLFNBQVMsRUFBRSxNQUFNLEVBQUUsa0JBQWtCLENBQUM7QUFFNUUsU0FBU0MsdUJBQXVCQSxDQUFBLENBQUUsRUFBRSxNQUFNLENBQUM7RUFDekMsT0FBT0wsTUFBTSxDQUFDSSxnQkFBZ0IsQ0FBQyxJQUFJLFVBQVU7QUFDL0M7QUFFQSxLQUFLRSxLQUFLLEdBQUc7RUFDWEMsTUFBTSxFQUFFLENBQUNDLE9BQWdCLENBQVIsRUFBRSxNQUFNLEVBQUUsR0FBRyxJQUFJO0VBQ2xDQyxRQUFRLENBQUMsRUFBRSxHQUFHLEdBQUcsSUFBSTtFQUNyQkMsWUFBWSxFQUFFLE9BQU87QUFDdkIsQ0FBQztBQUVELE9BQU8sU0FBQUMsU0FBQUMsRUFBQTtFQUFBLE1BQUFDLENBQUEsR0FBQUMsRUFBQTtFQUFrQjtJQUFBSixZQUFBO0lBQUFILE1BQUE7SUFBQUU7RUFBQSxJQUFBRyxFQUlqQjtFQUFBLElBQUFHLEVBQUE7RUFBQSxJQUFBRixDQUFBLFFBQUFOLE1BQUE7SUFDTlEsRUFBQSxrQkFBQUMsT0FBQUMsYUFBQTtNQUNFVixNQUFNLENBQUNVLGFBQTBDLElBQXpCWix1QkFBdUIsQ0FBQyxDQUFDLENBQUM7TUFDbEQsTUFBTUgsZ0JBQWdCLENBQUMsQ0FBQyxFQUFFLG1CQUFtQixDQUFDO0lBQUEsQ0FDL0M7SUFBQVcsQ0FBQSxNQUFBTixNQUFBO0lBQUFNLENBQUEsTUFBQUUsRUFBQTtFQUFBO0lBQUFBLEVBQUEsR0FBQUYsQ0FBQTtFQUFBO0VBSEQsTUFBQUcsTUFBQSxHQUFBRCxFQUdDO0VBRUQsSUFBSUwsWUFBWTtJQUFBLElBQUFRLEVBQUE7SUFBQSxJQUFBTCxDQUFBLFFBQUFKLFFBQUEsSUFBQUksQ0FBQSxRQUFBRyxNQUFBO01BQ1BFLEVBQUEsSUFBQyxrQkFBa0IsQ0FBU0YsTUFBTSxDQUFOQSxPQUFLLENBQUMsQ0FBWVAsUUFBUSxDQUFSQSxTQUFPLENBQUMsR0FBSTtNQUFBSSxDQUFBLE1BQUFKLFFBQUE7TUFBQUksQ0FBQSxNQUFBRyxNQUFBO01BQUFILENBQUEsTUFBQUssRUFBQTtJQUFBO01BQUFBLEVBQUEsR0FBQUwsQ0FBQTtJQUFBO0lBQUEsT0FBMURLLEVBQTBEO0VBQUE7RUFDbEUsT0FFTSxJQUFJO0FBQUEiLCJpZ25vcmVMaXN0IjpbXX0=
|
src/components/ExportDialog.tsx
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { join } from 'path';
|
| 2 |
+
import React, { useCallback, useState } from 'react';
|
| 3 |
+
import type { ExitState } from '../hooks/useExitOnCtrlCDWithKeybindings.js';
|
| 4 |
+
import { useTerminalSize } from '../hooks/useTerminalSize.js';
|
| 5 |
+
import { setClipboard } from '../ink/termio/osc.js';
|
| 6 |
+
import { Box, Text } from '../ink.js';
|
| 7 |
+
import { useKeybinding } from '../keybindings/useKeybinding.js';
|
| 8 |
+
import { getCwd } from '../utils/cwd.js';
|
| 9 |
+
import { writeFileSync_DEPRECATED } from '../utils/slowOperations.js';
|
| 10 |
+
import { ConfigurableShortcutHint } from './ConfigurableShortcutHint.js';
|
| 11 |
+
import { Select } from './CustomSelect/select.js';
|
| 12 |
+
import { Byline } from './design-system/Byline.js';
|
| 13 |
+
import { Dialog } from './design-system/Dialog.js';
|
| 14 |
+
import { KeyboardShortcutHint } from './design-system/KeyboardShortcutHint.js';
|
| 15 |
+
import TextInput from './TextInput.js';
|
| 16 |
+
type ExportDialogProps = {
|
| 17 |
+
content: string;
|
| 18 |
+
defaultFilename: string;
|
| 19 |
+
onDone: (result: {
|
| 20 |
+
success: boolean;
|
| 21 |
+
message: string;
|
| 22 |
+
}) => void;
|
| 23 |
+
};
|
| 24 |
+
type ExportOption = 'clipboard' | 'file';
|
| 25 |
+
export function ExportDialog({
|
| 26 |
+
content,
|
| 27 |
+
defaultFilename,
|
| 28 |
+
onDone
|
| 29 |
+
}: ExportDialogProps): React.ReactNode {
|
| 30 |
+
const [, setSelectedOption] = useState<ExportOption | null>(null);
|
| 31 |
+
const [filename, setFilename] = useState<string>(defaultFilename);
|
| 32 |
+
const [cursorOffset, setCursorOffset] = useState<number>(defaultFilename.length);
|
| 33 |
+
const [showFilenameInput, setShowFilenameInput] = useState(false);
|
| 34 |
+
const {
|
| 35 |
+
columns
|
| 36 |
+
} = useTerminalSize();
|
| 37 |
+
|
| 38 |
+
// Handle going back from filename input to option selection
|
| 39 |
+
const handleGoBack = useCallback(() => {
|
| 40 |
+
setShowFilenameInput(false);
|
| 41 |
+
setSelectedOption(null);
|
| 42 |
+
}, []);
|
| 43 |
+
const handleSelectOption = async (value: string): Promise<void> => {
|
| 44 |
+
if (value === 'clipboard') {
|
| 45 |
+
// Copy to clipboard immediately
|
| 46 |
+
const raw = await setClipboard(content);
|
| 47 |
+
if (raw) process.stdout.write(raw);
|
| 48 |
+
onDone({
|
| 49 |
+
success: true,
|
| 50 |
+
message: 'Conversation copied to clipboard'
|
| 51 |
+
});
|
| 52 |
+
} else if (value === 'file') {
|
| 53 |
+
setSelectedOption('file');
|
| 54 |
+
setShowFilenameInput(true);
|
| 55 |
+
}
|
| 56 |
+
};
|
| 57 |
+
const handleFilenameSubmit = () => {
|
| 58 |
+
const finalFilename = filename.endsWith('.txt') ? filename : filename.replace(/\.[^.]+$/, '') + '.txt';
|
| 59 |
+
const filepath = join(getCwd(), finalFilename);
|
| 60 |
+
try {
|
| 61 |
+
writeFileSync_DEPRECATED(filepath, content, {
|
| 62 |
+
encoding: 'utf-8',
|
| 63 |
+
flush: true
|
| 64 |
+
});
|
| 65 |
+
onDone({
|
| 66 |
+
success: true,
|
| 67 |
+
message: `Conversation exported to: ${filepath}`
|
| 68 |
+
});
|
| 69 |
+
} catch (error) {
|
| 70 |
+
onDone({
|
| 71 |
+
success: false,
|
| 72 |
+
message: `Failed to export conversation: ${error instanceof Error ? error.message : 'Unknown error'}`
|
| 73 |
+
});
|
| 74 |
+
}
|
| 75 |
+
};
|
| 76 |
+
|
| 77 |
+
// Dialog calls onCancel when Escape is pressed. If we are in the filename
|
| 78 |
+
// input sub-screen, go back to the option list instead of closing entirely.
|
| 79 |
+
const handleCancel = useCallback(() => {
|
| 80 |
+
if (showFilenameInput) {
|
| 81 |
+
handleGoBack();
|
| 82 |
+
} else {
|
| 83 |
+
onDone({
|
| 84 |
+
success: false,
|
| 85 |
+
message: 'Export cancelled'
|
| 86 |
+
});
|
| 87 |
+
}
|
| 88 |
+
}, [showFilenameInput, handleGoBack, onDone]);
|
| 89 |
+
const options = [{
|
| 90 |
+
label: 'Copy to clipboard',
|
| 91 |
+
value: 'clipboard',
|
| 92 |
+
description: 'Copy the conversation to your system clipboard'
|
| 93 |
+
}, {
|
| 94 |
+
label: 'Save to file',
|
| 95 |
+
value: 'file',
|
| 96 |
+
description: 'Save the conversation to a file in the current directory'
|
| 97 |
+
}];
|
| 98 |
+
|
| 99 |
+
// Custom input guide that changes based on dialog state
|
| 100 |
+
function renderInputGuide(exitState: ExitState): React.ReactNode {
|
| 101 |
+
if (showFilenameInput) {
|
| 102 |
+
return <Byline>
|
| 103 |
+
<KeyboardShortcutHint shortcut="Enter" action="save" />
|
| 104 |
+
<ConfigurableShortcutHint action="confirm:no" context="Confirmation" fallback="Esc" description="go back" />
|
| 105 |
+
</Byline>;
|
| 106 |
+
}
|
| 107 |
+
if (exitState.pending) {
|
| 108 |
+
return <Text>Press {exitState.keyName} again to exit</Text>;
|
| 109 |
+
}
|
| 110 |
+
return <ConfigurableShortcutHint action="confirm:no" context="Confirmation" fallback="Esc" description="cancel" />;
|
| 111 |
+
}
|
| 112 |
+
|
| 113 |
+
// Use Settings context so 'n' key doesn't cancel (allows typing 'n' in filename input)
|
| 114 |
+
useKeybinding('confirm:no', handleCancel, {
|
| 115 |
+
context: 'Settings',
|
| 116 |
+
isActive: showFilenameInput
|
| 117 |
+
});
|
| 118 |
+
return <Dialog title="Export Conversation" subtitle="Select export method:" color="permission" onCancel={handleCancel} inputGuide={renderInputGuide} isCancelActive={!showFilenameInput}>
|
| 119 |
+
{!showFilenameInput ? <Select options={options} onChange={handleSelectOption} onCancel={handleCancel} /> : <Box flexDirection="column">
|
| 120 |
+
<Text>Enter filename:</Text>
|
| 121 |
+
<Box flexDirection="row" gap={1} marginTop={1}>
|
| 122 |
+
<Text>></Text>
|
| 123 |
+
<TextInput value={filename} onChange={setFilename} onSubmit={handleFilenameSubmit} focus={true} showCursor={true} columns={columns} cursorOffset={cursorOffset} onChangeCursorOffset={setCursorOffset} />
|
| 124 |
+
</Box>
|
| 125 |
+
</Box>}
|
| 126 |
+
</Dialog>;
|
| 127 |
+
}
|
| 128 |
+
//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJuYW1lcyI6WyJqb2luIiwiUmVhY3QiLCJ1c2VDYWxsYmFjayIsInVzZVN0YXRlIiwiRXhpdFN0YXRlIiwidXNlVGVybWluYWxTaXplIiwic2V0Q2xpcGJvYXJkIiwiQm94IiwiVGV4dCIsInVzZUtleWJpbmRpbmciLCJnZXRDd2QiLCJ3cml0ZUZpbGVTeW5jX0RFUFJFQ0FURUQiLCJDb25maWd1cmFibGVTaG9ydGN1dEhpbnQiLCJTZWxlY3QiLCJCeWxpbmUiLCJEaWFsb2ciLCJLZXlib2FyZFNob3J0Y3V0SGludCIsIlRleHRJbnB1dCIsIkV4cG9ydERpYWxvZ1Byb3BzIiwiY29udGVudCIsImRlZmF1bHRGaWxlbmFtZSIsIm9uRG9uZSIsInJlc3VsdCIsInN1Y2Nlc3MiLCJtZXNzYWdlIiwiRXhwb3J0T3B0aW9uIiwiRXhwb3J0RGlhbG9nIiwiUmVhY3ROb2RlIiwic2V0U2VsZWN0ZWRPcHRpb24iLCJmaWxlbmFtZSIsInNldEZpbGVuYW1lIiwiY3Vyc29yT2Zmc2V0Iiwic2V0Q3Vyc29yT2Zmc2V0IiwibGVuZ3RoIiwic2hvd0ZpbGVuYW1lSW5wdXQiLCJzZXRTaG93RmlsZW5hbWVJbnB1dCIsImNvbHVtbnMiLCJoYW5kbGVHb0JhY2siLCJoYW5kbGVTZWxlY3RPcHRpb24iLCJ2YWx1ZSIsIlByb21pc2UiLCJyYXciLCJwcm9jZXNzIiwic3Rkb3V0Iiwid3JpdGUiLCJoYW5kbGVGaWxlbmFtZVN1Ym1pdCIsImZpbmFsRmlsZW5hbWUiLCJlbmRzV2l0aCIsInJlcGxhY2UiLCJmaWxlcGF0aCIsImVuY29kaW5nIiwiZmx1c2giLCJlcnJvciIsIkVycm9yIiwiaGFuZGxlQ2FuY2VsIiwib3B0aW9ucyIsImxhYmVsIiwiZGVzY3JpcHRpb24iLCJyZW5kZXJJbnB1dEd1aWRlIiwiZXhpdFN0YXRlIiwicGVuZGluZyIsImtleU5hbWUiLCJjb250ZXh0IiwiaXNBY3RpdmUiXSwic291cmNlcyI6WyJFeHBvcnREaWFsb2cudHN4Il0sInNvdXJjZXNDb250ZW50IjpbImltcG9ydCB7IGpvaW4gfSBmcm9tICdwYXRoJ1xuaW1wb3J0IFJlYWN0LCB7IHVzZUNhbGxiYWNrLCB1c2VTdGF0ZSB9IGZyb20gJ3JlYWN0J1xuaW1wb3J0IHR5cGUgeyBFeGl0U3RhdGUgfSBmcm9tICcuLi9ob29rcy91c2VFeGl0T25DdHJsQ0RXaXRoS2V5YmluZGluZ3MuanMnXG5pbXBvcnQgeyB1c2VUZXJtaW5hbFNpemUgfSBmcm9tICcuLi9ob29rcy91c2VUZXJtaW5hbFNpemUuanMnXG5pbXBvcnQgeyBzZXRDbGlwYm9hcmQgfSBmcm9tICcuLi9pbmsvdGVybWlvL29zYy5qcydcbmltcG9ydCB7IEJveCwgVGV4dCB9IGZyb20gJy4uL2luay5qcydcbmltcG9ydCB7IHVzZUtleWJpbmRpbmcgfSBmcm9tICcuLi9rZXliaW5kaW5ncy91c2VLZXliaW5kaW5nLmpzJ1xuaW1wb3J0IHsgZ2V0Q3dkIH0gZnJvbSAnLi4vdXRpbHMvY3dkLmpzJ1xuaW1wb3J0IHsgd3JpdGVGaWxlU3luY19ERVBSRUNBVEVEIH0gZnJvbSAnLi4vdXRpbHMvc2xvd09wZXJhdGlvbnMuanMnXG5pbXBvcnQgeyBDb25maWd1cmFibGVTaG9ydGN1dEhpbnQgfSBmcm9tICcuL0NvbmZpZ3VyYWJsZVNob3J0Y3V0SGludC5qcydcbmltcG9ydCB7IFNlbGVjdCB9IGZyb20gJy4vQ3VzdG9tU2VsZWN0L3NlbGVjdC5qcydcbmltcG9ydCB7IEJ5bGluZSB9IGZyb20gJy4vZGVzaWduLXN5c3RlbS9CeWxpbmUuanMnXG5pbXBvcnQgeyBEaWFsb2cgfSBmcm9tICcuL2Rlc2lnbi1zeXN0ZW0vRGlhbG9nLmpzJ1xuaW1wb3J0IHsgS2V5Ym9hcmRTaG9ydGN1dEhpbnQgfSBmcm9tICcuL2Rlc2lnbi1zeXN0ZW0vS2V5Ym9hcmRTaG9ydGN1dEhpbnQuanMnXG5pbXBvcnQgVGV4dElucHV0IGZyb20gJy4vVGV4dElucHV0LmpzJ1xuXG50eXBlIEV4cG9ydERpYWxvZ1Byb3BzID0ge1xuICBjb250ZW50OiBzdHJpbmdcbiAgZGVmYXVsdEZpbGVuYW1lOiBzdHJpbmdcbiAgb25Eb25lOiAocmVzdWx0OiB7IHN1Y2Nlc3M6IGJvb2xlYW47IG1lc3NhZ2U6IHN0cmluZyB9KSA9PiB2b2lkXG59XG5cbnR5cGUgRXhwb3J0T3B0aW9uID0gJ2NsaXBib2FyZCcgfCAnZmlsZSdcblxuZXhwb3J0IGZ1bmN0aW9uIEV4cG9ydERpYWxvZyh7XG4gIGNvbnRlbnQsXG4gIGRlZmF1bHRGaWxlbmFtZSxcbiAgb25Eb25lLFxufTogRXhwb3J0RGlhbG9nUHJvcHMpOiBSZWFjdC5SZWFjdE5vZGUge1xuICBjb25zdCBbLCBzZXRTZWxlY3RlZE9wdGlvbl0gPSB1c2VTdGF0ZTxFeHBvcnRPcHRpb24gfCBudWxsPihudWxsKVxuICBjb25zdCBbZmlsZW5hbWUsIHNldEZpbGVuYW1lXSA9IHVzZVN0YXRlPHN0cmluZz4oZGVmYXVsdEZpbGVuYW1lKVxuICBjb25zdCBbY3Vyc29yT2Zmc2V0LCBzZXRDdXJzb3JPZmZzZXRdID0gdXNlU3RhdGU8bnVtYmVyPihcbiAgICBkZWZhdWx0RmlsZW5hbWUubGVuZ3RoLFxuICApXG4gIGNvbnN0IFtzaG93RmlsZW5hbWVJbnB1dCwgc2V0U2hvd0ZpbGVuYW1lSW5wdXRdID0gdXNlU3RhdGUoZmFsc2UpXG4gIGNvbnN0IHsgY29sdW1ucyB9ID0gdXNlVGVybWluYWxTaXplKClcblxuICAvLyBIYW5kbGUgZ29pbmcgYmFjayBmcm9tIGZpbGVuYW1lIGlucHV0IHRvIG9wdGlvbiBzZWxlY3Rpb25cbiAgY29uc3QgaGFuZGxlR29CYWNrID0gdXNlQ2FsbGJhY2soKCkgPT4ge1xuICAgIHNldFNob3dGaWxlbmFtZUlucHV0KGZhbHNlKVxuICAgIHNldFNlbGVjdGVkT3B0aW9uKG51bGwpXG4gIH0sIFtdKVxuXG4gIGNvbnN0IGhhbmRsZVNlbGVjdE9wdGlvbiA9IGFzeW5jICh2YWx1ZTogc3RyaW5nKTogUHJvbWlzZTx2b2lkPiA9PiB7XG4gICAgaWYgKHZhbHVlID09PSAnY2xpcGJvYXJkJykge1xuICAgICAgLy8gQ29weSB0byBjbGlwYm9hcmQgaW1tZWRpYXRlbHlcbiAgICAgIGNvbnN0IHJhdyA9IGF3YWl0IHNldENsaXBib2FyZChjb250ZW50KVxuICAgICAgaWYgKHJhdykgcHJvY2Vzcy5zdGRvdXQud3JpdGUocmF3KVxuICAgICAgb25Eb25lKHsgc3VjY2VzczogdHJ1ZSwgbWVzc2FnZTogJ0NvbnZlcnNhdGlvbiBjb3BpZWQgdG8gY2xpcGJvYXJkJyB9KVxuICAgIH0gZWxzZSBpZiAodmFsdWUgPT09ICdmaWxlJykge1xuICAgICAgc2V0U2VsZWN0ZWRPcHRpb24oJ2ZpbGUnKVxuICAgICAgc2V0U2hvd0ZpbGVuYW1lSW5wdXQodHJ1ZSlcbiAgICB9XG4gIH1cblxuICBjb25zdCBoYW5kbGVGaWxlbmFtZVN1Ym1pdCA9ICgpID0+IHtcbiAgICBjb25zdCBmaW5hbEZpbGVuYW1lID0gZmlsZW5hbWUuZW5kc1dpdGgoJy50eHQnKVxuICAgICAgPyBmaWxlbmFtZVxuICAgICAgOiBmaWxlbmFtZS5yZXBsYWNlKC9cXC5bXi5dKyQvLCAnJykgKyAnLnR4dCdcbiAgICBjb25zdCBmaWxlcGF0aCA9IGpvaW4oZ2V0Q3dkKCksIGZpbmFsRmlsZW5hbWUpXG5cbiAgICB0cnkge1xuICAgICAgd3JpdGVGaWxlU3luY19ERVBSRUNBVEVEKGZpbGVwYXRoLCBjb250ZW50LCB7XG4gICAgICAgIGVuY29kaW5nOiAndXRmLTgnLFxuICAgICAgICBmbHVzaDogdHJ1ZSxcbiAgICAgIH0pXG4gICAgICBvbkRvbmUoe1xuICAgICAgICBzdWNjZXNzOiB0cnVlLFxuICAgICAgICBtZXNzYWdlOiBgQ29udmVyc2F0aW9uIGV4cG9ydGVkIHRvOiAke2ZpbGVwYXRofWAsXG4gICAgICB9KVxuICAgIH0gY2F0Y2ggKGVycm9yKSB7XG4gICAgICBvbkRvbmUoe1xuICAgICAgICBzdWNjZXNzOiBmYWxzZSxcbiAgICAgICAgbWVzc2FnZTogYEZhaWxlZCB0byBleHBvcnQgY29udmVyc2F0aW9uOiAke2Vycm9yIGluc3RhbmNlb2YgRXJyb3IgPyBlcnJvci5tZXNzYWdlIDogJ1Vua25vd24gZXJyb3InfWAsXG4gICAgICB9KVxuICAgIH1cbiAgfVxuXG4gIC8vIERpYWxvZyBjYWxscyBvbkNhbmNlbCB3aGVuIEVzY2FwZSBpcyBwcmVzc2VkLiBJZiB3ZSBhcmUgaW4gdGhlIGZpbGVuYW1lXG4gIC8vIGlucHV0IHN1Yi1zY3JlZW4sIGdvIGJhY2sgdG8gdGhlIG9wdGlvbiBsaXN0IGluc3RlYWQgb2YgY2xvc2luZyBlbnRpcmVseS5cbiAgY29uc3QgaGFuZGxlQ2FuY2VsID0gdXNlQ2FsbGJhY2soKCkgPT4ge1xuICAgIGlmIChzaG93RmlsZW5hbWVJbnB1dCkge1xuICAgICAgaGFuZGxlR29CYWNrKClcbiAgICB9IGVsc2Uge1xuICAgICAgb25Eb25lKHsgc3VjY2VzczogZmFsc2UsIG1lc3NhZ2U6ICdFeHBvcnQgY2FuY2VsbGVkJyB9KVxuICAgIH1cbiAgfSwgW3Nob3dGaWxlbmFtZUlucHV0LCBoYW5kbGVHb0JhY2ssIG9uRG9uZV0pXG5cbiAgY29uc3Qgb3B0aW9ucyA9IFtcbiAgICB7XG4gICAgICBsYWJlbDogJ0NvcHkgdG8gY2xpcGJvYXJkJyxcbiAgICAgIHZhbHVlOiAnY2xpcGJvYXJkJyxcbiAgICAgIGRlc2NyaXB0aW9uOiAnQ29weSB0aGUgY29udmVyc2F0aW9uIHRvIHlvdXIgc3lzdGVtIGNsaXBib2FyZCcsXG4gICAgfSxcbiAgICB7XG4gICAgICBsYWJlbDogJ1NhdmUgdG8gZmlsZScsXG4gICAgICB2YWx1ZTogJ2ZpbGUnLFxuICAgICAgZGVzY3JpcHRpb246ICdTYXZlIHRoZSBjb252ZXJzYXRpb24gdG8gYSBmaWxlIGluIHRoZSBjdXJyZW50IGRpcmVjdG9yeScsXG4gICAgfSxcbiAgXVxuXG4gIC8vIEN1c3RvbSBpbnB1dCBndWlkZSB0aGF0IGNoYW5nZXMgYmFzZWQgb24gZGlhbG9nIHN0YXRlXG4gIGZ1bmN0aW9uIHJlbmRlcklucHV0R3VpZGUoZXhpdFN0YXRlOiBFeGl0U3RhdGUpOiBSZWFjdC5SZWFjdE5vZGUge1xuICAgIGlmIChzaG93RmlsZW5hbWVJbnB1dCkge1xuICAgICAgcmV0dXJuIChcbiAgICAgICAgPEJ5bGluZT5cbiAgICAgICAgICA8S2V5Ym9hcmRTaG9ydGN1dEhpbnQgc2hvcnRjdXQ9XCJFbnRlclwiIGFjdGlvbj1cInNhdmVcIiAvPlxuICAgICAgICAgIDxDb25maWd1cmFibGVTaG9ydGN1dEhpbnRcbiAgICAgICAgICAgIGFjdGlvbj1cImNvbmZpcm06bm9cIlxuICAgICAgICAgICAgY29udGV4dD1cIkNvbmZpcm1hdGlvblwiXG4gICAgICAgICAgICBmYWxsYmFjaz1cIkVzY1wiXG4gICAgICAgICAgICBkZXNjcmlwdGlvbj1cImdvIGJhY2tcIlxuICAgICAgICAgIC8+XG4gICAgICAgIDwvQnlsaW5lPlxuICAgICAgKVxuICAgIH1cblxuICAgIGlmIChleGl0U3RhdGUucGVuZGluZykge1xuICAgICAgcmV0dXJuIDxUZXh0PlByZXNzIHtleGl0U3RhdGUua2V5TmFtZX0gYWdhaW4gdG8gZXhpdDwvVGV4dD5cbiAgICB9XG5cbiAgICByZXR1cm4gKFxuICAgICAgPENvbmZpZ3VyYWJsZVNob3J0Y3V0SGludFxuICAgICAgICBhY3Rpb249XCJjb25maXJtOm5vXCJcbiAgICAgICAgY29udGV4dD1cIkNvbmZpcm1hdGlvblwiXG4gICAgICAgIGZhbGxiYWNrPVwiRXNjXCJcbiAgICAgICAgZGVzY3JpcHRpb249XCJjYW5jZWxcIlxuICAgICAgLz5cbiAgICApXG4gIH1cblxuICAvLyBVc2UgU2V0dGluZ3MgY29udGV4dCBzbyAnbicga2V5IGRvZXNuJ3QgY2FuY2VsIChhbGxvd3MgdHlwaW5nICduJyBpbiBmaWxlbmFtZSBpbnB1dClcbiAgdXNlS2V5YmluZGluZygnY29uZmlybTpubycsIGhhbmRsZUNhbmNlbCwge1xuICAgIGNvbnRleHQ6ICdTZXR0aW5ncycsXG4gICAgaXNBY3RpdmU6IHNob3dGaWxlbmFtZUlucHV0LFxuICB9KVxuXG4gIHJldHVybiAoXG4gICAgPERpYWxvZ1xuICAgICAgdGl0bGU9XCJFeHBvcnQgQ29udmVyc2F0aW9uXCJcbiAgICAgIHN1YnRpdGxlPVwiU2VsZWN0IGV4cG9ydCBtZXRob2Q6XCJcbiAgICAgIGNvbG9yPVwicGVybWlzc2lvblwiXG4gICAgICBvbkNhbmNlbD17aGFuZGxlQ2FuY2VsfVxuICAgICAgaW5wdXRHdWlkZT17cmVuZGVySW5wdXRHdWlkZX1cbiAgICAgIGlzQ2FuY2VsQWN0aXZlPXshc2hvd0ZpbGVuYW1lSW5wdXR9XG4gICAgPlxuICAgICAgeyFzaG93RmlsZW5hbWVJbnB1dCA/IChcbiAgICAgICAgPFNlbGVjdFxuICAgICAgICAgIG9wdGlvbnM9e29wdGlvbnN9XG4gICAgICAgICAgb25DaGFuZ2U9e2hhbmRsZVNlbGVjdE9wdGlvbn1cbiAgICAgICAgICBvbkNhbmNlbD17aGFuZGxlQ2FuY2VsfVxuICAgICAgICAvPlxuICAgICAgKSA6IChcbiAgICAgICAgPEJveCBmbGV4RGlyZWN0aW9uPVwiY29sdW1uXCI+XG4gICAgICAgICAgPFRleHQ+RW50ZXIgZmlsZW5hbWU6PC9UZXh0PlxuICAgICAgICAgIDxCb3ggZmxleERpcmVjdGlvbj1cInJvd1wiIGdhcD17MX0gbWFyZ2luVG9wPXsxfT5cbiAgICAgICAgICAgIDxUZXh0PiZndDs8L1RleHQ+XG4gICAgICAgICAgICA8VGV4dElucHV0XG4gICAgICAgICAgICAgIHZhbHVlPXtmaWxlbmFtZX1cbiAgICAgICAgICAgICAgb25DaGFuZ2U9e3NldEZpbGVuYW1lfVxuICAgICAgICAgICAgICBvblN1Ym1pdD17aGFuZGxlRmlsZW5hbWVTdWJtaXR9XG4gICAgICAgICAgICAgIGZvY3VzPXt0cnVlfVxuICAgICAgICAgICAgICBzaG93Q3Vyc29yPXt0cnVlfVxuICAgICAgICAgICAgICBjb2x1bW5zPXtjb2x1bW5zfVxuICAgICAgICAgICAgICBjdXJzb3JPZmZzZXQ9e2N1cnNvck9mZnNldH1cbiAgICAgICAgICAgICAgb25DaGFuZ2VDdXJzb3JPZmZzZXQ9e3NldEN1cnNvck9mZnNldH1cbiAgICAgICAgICAgIC8+XG4gICAgICAgICAgPC9Cb3g+XG4gICAgICAgIDwvQm94PlxuICAgICAgKX1cbiAgICA8L0RpYWxvZz5cbiAgKVxufVxuIl0sIm1hcHBpbmdzIjoiQUFBQSxTQUFTQSxJQUFJLFFBQVEsTUFBTTtBQUMzQixPQUFPQyxLQUFLLElBQUlDLFdBQVcsRUFBRUMsUUFBUSxRQUFRLE9BQU87QUFDcEQsY0FBY0MsU0FBUyxRQUFRLDRDQUE0QztBQUMzRSxTQUFTQyxlQUFlLFFBQVEsNkJBQTZCO0FBQzdELFNBQVNDLFlBQVksUUFBUSxzQkFBc0I7QUFDbkQsU0FBU0MsR0FBRyxFQUFFQyxJQUFJLFFBQVEsV0FBVztBQUNyQyxTQUFTQyxhQUFhLFFBQVEsaUNBQWlDO0FBQy9ELFNBQVNDLE1BQU0sUUFBUSxpQkFBaUI7QUFDeEMsU0FBU0Msd0JBQXdCLFFBQVEsNEJBQTRCO0FBQ3JFLFNBQVNDLHdCQUF3QixRQUFRLCtCQUErQjtBQUN4RSxTQUFTQyxNQUFNLFFBQVEsMEJBQTBCO0FBQ2pELFNBQVNDLE1BQU0sUUFBUSwyQkFBMkI7QUFDbEQsU0FBU0MsTUFBTSxRQUFRLDJCQUEyQjtBQUNsRCxTQUFTQyxvQkFBb0IsUUFBUSx5Q0FBeUM7QUFDOUUsT0FBT0MsU0FBUyxNQUFNLGdCQUFnQjtBQUV0QyxLQUFLQyxpQkFBaUIsR0FBRztFQUN2QkMsT0FBTyxFQUFFLE1BQU07RUFDZkMsZUFBZSxFQUFFLE1BQU07RUFDdkJDLE1BQU0sRUFBRSxDQUFDQyxNQUFNLEVBQUU7SUFBRUMsT0FBTyxFQUFFLE9BQU87SUFBRUMsT0FBTyxFQUFFLE1BQU07RUFBQyxDQUFDLEVBQUUsR0FBRyxJQUFJO0FBQ2pFLENBQUM7QUFFRCxLQUFLQyxZQUFZLEdBQUcsV0FBVyxHQUFHLE1BQU07QUFFeEMsT0FBTyxTQUFTQyxZQUFZQSxDQUFDO0VBQzNCUCxPQUFPO0VBQ1BDLGVBQWU7RUFDZkM7QUFDaUIsQ0FBbEIsRUFBRUgsaUJBQWlCLENBQUMsRUFBRWpCLEtBQUssQ0FBQzBCLFNBQVMsQ0FBQztFQUNyQyxNQUFNLEdB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
|
src/components/FileEditToolDiff.tsx
ADDED
|
@@ -0,0 +1,181 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { c as _c } from "react/compiler-runtime";
|
| 2 |
+
import type { StructuredPatchHunk } from 'diff';
|
| 3 |
+
import * as React from 'react';
|
| 4 |
+
import { Suspense, use, useState } from 'react';
|
| 5 |
+
import { useTerminalSize } from '../hooks/useTerminalSize.js';
|
| 6 |
+
import { Box, Text } from '../ink.js';
|
| 7 |
+
import type { FileEdit } from '../tools/FileEditTool/types.js';
|
| 8 |
+
import { findActualString, preserveQuoteStyle } from '../tools/FileEditTool/utils.js';
|
| 9 |
+
import { adjustHunkLineNumbers, CONTEXT_LINES, getPatchForDisplay } from '../utils/diff.js';
|
| 10 |
+
import { logError } from '../utils/log.js';
|
| 11 |
+
import { CHUNK_SIZE, openForScan, readCapped, scanForContext } from '../utils/readEditContext.js';
|
| 12 |
+
import { firstLineOf } from '../utils/stringUtils.js';
|
| 13 |
+
import { StructuredDiffList } from './StructuredDiffList.js';
|
| 14 |
+
type Props = {
|
| 15 |
+
file_path: string;
|
| 16 |
+
edits: FileEdit[];
|
| 17 |
+
};
|
| 18 |
+
type DiffData = {
|
| 19 |
+
patch: StructuredPatchHunk[];
|
| 20 |
+
firstLine: string | null;
|
| 21 |
+
fileContent: string | undefined;
|
| 22 |
+
};
|
| 23 |
+
export function FileEditToolDiff(props) {
|
| 24 |
+
const $ = _c(7);
|
| 25 |
+
let t0;
|
| 26 |
+
if ($[0] !== props.edits || $[1] !== props.file_path) {
|
| 27 |
+
t0 = () => loadDiffData(props.file_path, props.edits);
|
| 28 |
+
$[0] = props.edits;
|
| 29 |
+
$[1] = props.file_path;
|
| 30 |
+
$[2] = t0;
|
| 31 |
+
} else {
|
| 32 |
+
t0 = $[2];
|
| 33 |
+
}
|
| 34 |
+
const [dataPromise] = useState(t0);
|
| 35 |
+
let t1;
|
| 36 |
+
if ($[3] === Symbol.for("react.memo_cache_sentinel")) {
|
| 37 |
+
t1 = <DiffFrame placeholder={true} />;
|
| 38 |
+
$[3] = t1;
|
| 39 |
+
} else {
|
| 40 |
+
t1 = $[3];
|
| 41 |
+
}
|
| 42 |
+
let t2;
|
| 43 |
+
if ($[4] !== dataPromise || $[5] !== props.file_path) {
|
| 44 |
+
t2 = <Suspense fallback={t1}><DiffBody promise={dataPromise} file_path={props.file_path} /></Suspense>;
|
| 45 |
+
$[4] = dataPromise;
|
| 46 |
+
$[5] = props.file_path;
|
| 47 |
+
$[6] = t2;
|
| 48 |
+
} else {
|
| 49 |
+
t2 = $[6];
|
| 50 |
+
}
|
| 51 |
+
return t2;
|
| 52 |
+
}
|
| 53 |
+
function DiffBody(t0) {
|
| 54 |
+
const $ = _c(6);
|
| 55 |
+
const {
|
| 56 |
+
promise,
|
| 57 |
+
file_path
|
| 58 |
+
} = t0;
|
| 59 |
+
const {
|
| 60 |
+
patch,
|
| 61 |
+
firstLine,
|
| 62 |
+
fileContent
|
| 63 |
+
} = use(promise);
|
| 64 |
+
const {
|
| 65 |
+
columns
|
| 66 |
+
} = useTerminalSize();
|
| 67 |
+
let t1;
|
| 68 |
+
if ($[0] !== columns || $[1] !== fileContent || $[2] !== file_path || $[3] !== firstLine || $[4] !== patch) {
|
| 69 |
+
t1 = <DiffFrame><StructuredDiffList hunks={patch} dim={false} width={columns} filePath={file_path} firstLine={firstLine} fileContent={fileContent} /></DiffFrame>;
|
| 70 |
+
$[0] = columns;
|
| 71 |
+
$[1] = fileContent;
|
| 72 |
+
$[2] = file_path;
|
| 73 |
+
$[3] = firstLine;
|
| 74 |
+
$[4] = patch;
|
| 75 |
+
$[5] = t1;
|
| 76 |
+
} else {
|
| 77 |
+
t1 = $[5];
|
| 78 |
+
}
|
| 79 |
+
return t1;
|
| 80 |
+
}
|
| 81 |
+
function DiffFrame(t0) {
|
| 82 |
+
const $ = _c(5);
|
| 83 |
+
const {
|
| 84 |
+
children,
|
| 85 |
+
placeholder
|
| 86 |
+
} = t0;
|
| 87 |
+
let t1;
|
| 88 |
+
if ($[0] !== children || $[1] !== placeholder) {
|
| 89 |
+
t1 = placeholder ? <Text dimColor={true}>…</Text> : children;
|
| 90 |
+
$[0] = children;
|
| 91 |
+
$[1] = placeholder;
|
| 92 |
+
$[2] = t1;
|
| 93 |
+
} else {
|
| 94 |
+
t1 = $[2];
|
| 95 |
+
}
|
| 96 |
+
let t2;
|
| 97 |
+
if ($[3] !== t1) {
|
| 98 |
+
t2 = <Box flexDirection="column"><Box borderColor="subtle" borderStyle="dashed" flexDirection="column" borderLeft={false} borderRight={false}>{t1}</Box></Box>;
|
| 99 |
+
$[3] = t1;
|
| 100 |
+
$[4] = t2;
|
| 101 |
+
} else {
|
| 102 |
+
t2 = $[4];
|
| 103 |
+
}
|
| 104 |
+
return t2;
|
| 105 |
+
}
|
| 106 |
+
async function loadDiffData(file_path: string, edits: FileEdit[]): Promise<DiffData> {
|
| 107 |
+
const valid = edits.filter(e => e.old_string != null && e.new_string != null);
|
| 108 |
+
const single = valid.length === 1 ? valid[0]! : undefined;
|
| 109 |
+
|
| 110 |
+
// SedEditPermissionRequest passes the entire file as old_string. Scanning for
|
| 111 |
+
// a needle ≥ CHUNK_SIZE allocates O(needle) for the overlap buffer — skip the
|
| 112 |
+
// file read entirely and diff the inputs we already have.
|
| 113 |
+
if (single && single.old_string.length >= CHUNK_SIZE) {
|
| 114 |
+
return diffToolInputsOnly(file_path, [single]);
|
| 115 |
+
}
|
| 116 |
+
try {
|
| 117 |
+
const handle = await openForScan(file_path);
|
| 118 |
+
if (handle === null) return diffToolInputsOnly(file_path, valid);
|
| 119 |
+
try {
|
| 120 |
+
// Multi-edit and empty old_string genuinely need full-file for sequential
|
| 121 |
+
// replacements — structuredPatch needs before/after strings. replace_all
|
| 122 |
+
// routes through the chunked path below (shows first-occurrence window;
|
| 123 |
+
// matches within the slice still replace via edit.replace_all).
|
| 124 |
+
if (!single || single.old_string === '') {
|
| 125 |
+
const file = await readCapped(handle);
|
| 126 |
+
if (file === null) return diffToolInputsOnly(file_path, valid);
|
| 127 |
+
const normalized = valid.map(e => normalizeEdit(file, e));
|
| 128 |
+
return {
|
| 129 |
+
patch: getPatchForDisplay({
|
| 130 |
+
filePath: file_path,
|
| 131 |
+
fileContents: file,
|
| 132 |
+
edits: normalized
|
| 133 |
+
}),
|
| 134 |
+
firstLine: firstLineOf(file),
|
| 135 |
+
fileContent: file
|
| 136 |
+
};
|
| 137 |
+
}
|
| 138 |
+
const ctx = await scanForContext(handle, single.old_string, CONTEXT_LINES);
|
| 139 |
+
if (ctx.truncated || ctx.content === '') {
|
| 140 |
+
return diffToolInputsOnly(file_path, [single]);
|
| 141 |
+
}
|
| 142 |
+
const normalized = normalizeEdit(ctx.content, single);
|
| 143 |
+
const hunks = getPatchForDisplay({
|
| 144 |
+
filePath: file_path,
|
| 145 |
+
fileContents: ctx.content,
|
| 146 |
+
edits: [normalized]
|
| 147 |
+
});
|
| 148 |
+
return {
|
| 149 |
+
patch: adjustHunkLineNumbers(hunks, ctx.lineOffset - 1),
|
| 150 |
+
firstLine: ctx.lineOffset === 1 ? firstLineOf(ctx.content) : null,
|
| 151 |
+
fileContent: ctx.content
|
| 152 |
+
};
|
| 153 |
+
} finally {
|
| 154 |
+
await handle.close();
|
| 155 |
+
}
|
| 156 |
+
} catch (e) {
|
| 157 |
+
logError(e as Error);
|
| 158 |
+
return diffToolInputsOnly(file_path, valid);
|
| 159 |
+
}
|
| 160 |
+
}
|
| 161 |
+
function diffToolInputsOnly(filePath: string, edits: FileEdit[]): DiffData {
|
| 162 |
+
return {
|
| 163 |
+
patch: edits.flatMap(e => getPatchForDisplay({
|
| 164 |
+
filePath,
|
| 165 |
+
fileContents: e.old_string,
|
| 166 |
+
edits: [e]
|
| 167 |
+
})),
|
| 168 |
+
firstLine: null,
|
| 169 |
+
fileContent: undefined
|
| 170 |
+
};
|
| 171 |
+
}
|
| 172 |
+
function normalizeEdit(fileContent: string, edit: FileEdit): FileEdit {
|
| 173 |
+
const actualOld = findActualString(fileContent, edit.old_string) || edit.old_string;
|
| 174 |
+
const actualNew = preserveQuoteStyle(edit.old_string, actualOld, edit.new_string);
|
| 175 |
+
return {
|
| 176 |
+
...edit,
|
| 177 |
+
old_string: actualOld,
|
| 178 |
+
new_string: actualNew
|
| 179 |
+
};
|
| 180 |
+
}
|
| 181 |
+
//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJuYW1lcyI6WyJTdHJ1Y3R1cmVkUGF0Y2hIdW5rIiwiUmVhY3QiLCJTdXNwZW5zZSIsInVzZSIsInVzZVN0YXRlIiwidXNlVGVybWluYWxTaXplIiwiQm94IiwiVGV4dCIsIkZpbGVFZGl0IiwiZmluZEFjdHVhbFN0cmluZyIsInByZXNlcnZlUXVvdGVTdHlsZSIsImFkanVzdEh1bmtMaW5lTnVtYmVycyIsIkNPTlRFWFRfTElORVMiLCJnZXRQYXRjaEZvckRpc3BsYXkiLCJsb2dFcnJvciIsIkNIVU5LX1NJWkUiLCJvcGVuRm9yU2NhbiIsInJlYWRDYXBwZWQiLCJzY2FuRm9yQ29udGV4dCIsImZpcnN0TGluZU9mIiwiU3RydWN0dXJlZERpZmZMaXN0IiwiUHJvcHMiLCJmaWxlX3BhdGgiLCJlZGl0cyIsIkRpZmZEYXRhIiwicGF0Y2giLCJmaXJzdExpbmUiLCJmaWxlQ29udGVudCIsIkZpbGVFZGl0VG9vbERpZmYiLCJwcm9wcyIsIiQiLCJfYyIsInQwIiwibG9hZERpZmZEYXRhIiwiZGF0YVByb21pc2UiLCJ0MSIsIlN5bWJvbCIsImZvciIsInQyIiwiRGlmZkJvZHkiLCJwcm9taXNlIiwiY29sdW1ucyIsIkRpZmZGcmFtZSIsImNoaWxkcmVuIiwicGxhY2Vob2xkZXIiLCJQcm9taXNlIiwidmFsaWQiLCJmaWx0ZXIiLCJlIiwib2xkX3N0cmluZyIsIm5ld19zdHJpbmciLCJzaW5nbGUiLCJsZW5ndGgiLCJ1bmRlZmluZWQiLCJkaWZmVG9vbElucHV0c09ubHkiLCJoYW5kbGUiLCJmaWxlIiwibm9ybWFsaXplZCIsIm1hcCIsIm5vcm1hbGl6ZUVkaXQiLCJmaWxlUGF0aCIsImZpbGVDb250ZW50cyIsImN0eCIsInRydW5jYXRlZCIsImNvbnRlbnQiLCJodW5rcyIsImxpbmVPZmZzZXQiLCJjbG9zZSIsIkVycm9yIiwiZmxhdE1hcCIsImVkaXQiLCJhY3R1YWxPbGQiLCJhY3R1YWxOZXciXSwic291cmNlcyI6WyJGaWxlRWRpdFRvb2xEaWZmLnRzeCJdLCJzb3VyY2VzQ29udGVudCI6WyJpbXBvcnQgdHlwZSB7IFN0cnVjdHVyZWRQYXRjaEh1bmsgfSBmcm9tICdkaWZmJ1xuaW1wb3J0ICogYXMgUmVhY3QgZnJvbSAncmVhY3QnXG5pbXBvcnQgeyBTdXNwZW5zZSwgdXNlLCB1c2VTdGF0ZSB9IGZyb20gJ3JlYWN0J1xuaW1wb3J0IHsgdXNlVGVybWluYWxTaXplIH0gZnJvbSAnLi4vaG9va3MvdXNlVGVybWluYWxTaXplLmpzJ1xuaW1wb3J0IHsgQm94LCBUZXh0IH0gZnJvbSAnLi4vaW5rLmpzJ1xuaW1wb3J0IHR5cGUgeyBGaWxlRWRpdCB9IGZyb20gJy4uL3Rvb2xzL0ZpbGVFZGl0VG9vbC90eXBlcy5qcydcbmltcG9ydCB7XG4gIGZpbmRBY3R1YWxTdHJpbmcsXG4gIHByZXNlcnZlUXVvdGVTdHlsZSxcbn0gZnJvbSAnLi4vdG9vbHMvRmlsZUVkaXRUb29sL3V0aWxzLmpzJ1xuaW1wb3J0IHtcbiAgYWRqdXN0SHVua0xpbmVOdW1iZXJzLFxuICBDT05URVhUX0xJTkVTLFxuICBnZXRQYXRjaEZvckRpc3BsYXksXG59IGZyb20gJy4uL3V0aWxzL2RpZmYuanMnXG5pbXBvcnQgeyBsb2dFcnJvciB9IGZyb20gJy4uL3V0aWxzL2xvZy5qcydcbmltcG9ydCB7XG4gIENIVU5LX1NJWkUsXG4gIG9wZW5Gb3JTY2FuLFxuICByZWFkQ2FwcGVkLFxuICBzY2FuRm9yQ29udGV4dCxcbn0gZnJvbSAnLi4vdXRpbHMvcmVhZEVkaXRDb250ZXh0LmpzJ1xuaW1wb3J0IHsgZmlyc3RMaW5lT2YgfSBmcm9tICcuLi91dGlscy9zdHJpbmdVdGlscy5qcydcbmltcG9ydCB7IFN0cnVjdHVyZWREaWZmTGlzdCB9IGZyb20gJy4vU3RydWN0dXJlZERpZmZMaXN0LmpzJ1xuXG50eXBlIFByb3BzID0ge1xuICBmaWxlX3BhdGg6IHN0cmluZ1xuICBlZGl0czogRmlsZUVkaXRbXVxufVxuXG50eXBlIERpZmZEYXRhID0ge1xuICBwYXRjaDogU3RydWN0dXJlZFBhdGNoSHVua1tdXG4gIGZpcnN0TGluZTogc3RyaW5nIHwgbnVsbFxuICBmaWxlQ29udGVudDogc3RyaW5nIHwgdW5kZWZpbmVkXG59XG5cbmV4cG9ydCBmdW5jdGlvbiBGaWxlRWRpdFRvb2xEaWZmKHByb3BzOiBQcm9wcyk6IFJlYWN0LlJlYWN0Tm9kZSB7XG4gIC8vIFNuYXBzaG90IG9uIG1vdW50IOKAlCB0aGUgZGlmZiBtdXN0IHN0YXkgY29uc2lzdGVudCBldmVuIGlmIHRoZSBmaWxlIGNoYW5nZXNcbiAgLy8gd2hpbGUgdGhlIGRpYWxvZyBpcyBvcGVuLiB1c2VNZW1vIG9uIHByb3BzLmVkaXRzIHdvdWxkIHJlLXJlYWQgdGhlIGZpbGUgb25cbiAgLy8gZXZlcnkgcmVuZGVyIGJlY2F1c2UgY2FsbGVycyBwYXNzIGZyZXNoIGFycmF5IGxpdGVyYWxzLlxuICBjb25zdCBbZGF0YVByb21pc2VdID0gdXNlU3RhdGUoKCkgPT5cbiAgICBsb2FkRGlmZkRhdGEocHJvcHMuZmlsZV9wYXRoLCBwcm9wcy5lZGl0cyksXG4gIClcbiAgcmV0dXJuIChcbiAgICA8U3VzcGVuc2UgZmFsbGJhY2s9ezxEaWZmRnJhbWUgcGxhY2Vob2xkZXIgLz59PlxuICAgICAgPERpZmZCb2R5IHByb21pc2U9e2RhdGFQcm9taXNlfSBmaWxlX3BhdGg9e3Byb3BzLmZpbGVfcGF0aH0gLz5cbiAgICA8L1N1c3BlbnNlPlxuICApXG59XG5cbmZ1bmN0aW9uIERpZmZCb2R5KHtcbiAgcHJvbWlzZSxcbiAgZmlsZV9wYXRoLFxufToge1xuICBwcm9taXNlOiBQcm9taXNlPERpZmZEYXRhPlxuICBmaWxlX3BhdGg6IHN0cmluZ1xufSk6IFJlYWN0LlJlYWN0Tm9kZSB7XG4gIGNvbnN0IHsgcGF0Y2gsIGZpcnN0TGluZSwgZmlsZUNvbnRlbnQgfSA9IHVzZShwcm9taXNlKVxuICBjb25zdCB7IGNvbHVtbnMgfSA9IHVzZVRlcm1pbmFsU2l6ZSgpXG4gIHJldHVybiAoXG4gICAgPERpZmZGcmFtZT5cbiAgICAgIDxTdHJ1Y3R1cmVkRGlmZkxpc3RcbiAgICAgICAgaHVua3M9e3BhdGNofVxuICAgICAgICBkaW09e2ZhbHNlfVxuICAgICAgICB3aWR0aD17Y29sdW1uc31cbiAgICAgICAgZmlsZVBhdGg9e2ZpbGVfcGF0aH1cbiAgICAgICAgZmlyc3RMaW5lPXtmaXJzdExpbmV9XG4gICAgICAgIGZpbGVDb250ZW50PXtmaWxlQ29udGVudH1cbiAgICAgIC8+XG4gICAgPC9EaWZmRnJhbWU+XG4gIClcbn1cblxuZnVuY3Rpb24gRGlmZkZyYW1lKHtcbiAgY2hpbGRyZW4sXG4gIHBsYWNlaG9sZGVyLFxufToge1xuICBjaGlsZHJlbj86IFJlYWN0LlJlYWN0Tm9kZVxuICBwbGFjZWhvbGRlcj86IGJvb2xlYW5cbn0pOiBSZWFjdC5SZWFjdE5vZGUge1xuICByZXR1cm4gKFxuICAgIDxCb3ggZmxleERpcmVjdGlvbj1cImNvbHVtblwiPlxuICAgICAgPEJveFxuICAgICAgICBib3JkZXJDb2xvcj1cInN1YnRsZVwiXG4gICAgICAgIGJvcmRlclN0eWxlPVwiZGFzaGVkXCJcbiAgICAgICAgZmxleERpcmVjdGlvbj1cImNvbHVtblwiXG4gICAgICAgIGJvcmRlckxlZnQ9e2ZhbHNlfVxuICAgICAgICBib3JkZXJSaWdodD17ZmFsc2V9XG4gICAgICA+XG4gICAgICAgIHtwbGFjZWhvbGRlciA/IDxUZXh0IGRpbUNvbG9yPuKApjwvVGV4dD4gOiBjaGlsZHJlbn1cbiAgICAgIDwvQm94PlxuICAgIDwvQm94PlxuICApXG59XG5cbmFzeW5jIGZ1bmN0aW9uIGxvYWREaWZmRGF0YShcbiAgZmlsZV9wYXRoOiBzdHJpbmcsXG4gIGVkaXRzOiBGaWxlRWRpdFtdLFxuKTogUHJvbWlzZTxEaWZmRGF0YT4ge1xuICBjb25zdCB2YWxpZCA9IGVkaXRzLmZpbHRlcihlID0+IGUub2xkX3N0cmluZyAhPSBudWxsICYmIGUubmV3X3N0cmluZyAhPSBudWxsKVxuICBjb25zdCBzaW5nbGUgPSB2YWxpZC5sZW5ndGggPT09IDEgPyB2YWxpZFswXSEgOiB1bmRlZmluZWRcblxuICAvLyBTZWRFZGl0UGVybWlzc2lvblJlcXVlc3QgcGFzc2VzIHRoZSBlbnRpcmUgZmlsZSBhcyBvbGRfc3RyaW5nLiBTY2FubmluZyBmb3JcbiAgLy8gYSBuZWVkbGUg4omlIENIVU5LX1NJWkUgYWxsb2NhdGVzIE8obmVlZGxlKSBmb3IgdGhlIG92ZXJsYXAgYnVmZmVyIOKAlCBza2lwIHRoZVxuICAvLyBmaWxlIHJlYWQgZW50aXJlbHkgYW5kIGRpZmYgdGhlIGlucHV0cyB3ZSBhbHJlYWR5IGhhdmUuXG4gIGlmIChzaW5nbGUgJiYgc2luZ2xlLm9sZF9zdHJpbmcubGVuZ3RoID49IENIVU5LX1NJWkUpIHtcbiAgICByZXR1cm4gZGlmZlRvb2xJbnB1dHNPbmx5KGZpbGVfcGF0aCwgW3NpbmdsZV0pXG4gIH1cblxuICB0cnkge1xuICAgIGNvbnN0IGhhbmRsZSA9IGF3YWl0IG9wZW5Gb3JTY2FuKGZpbGVfcGF0aClcbiAgICBpZiAoaGFuZGxlID09PSBudWxsKSByZXR1cm4gZGlmZlRvb2xJbnB1dHNPbmx5KGZpbGVfcGF0aCwgdmFsaWQpXG4gICAgdHJ5IHtcbiAgICAgIC8vIE11bHRpLWVkaXQgYW5kIGVtcHR5IG9sZF9zdHJpbmcgZ2VudWluZWx5IG5lZWQgZnVsbC1maWxlIGZvciBzZXF1ZW50aWFsXG4gICAgICAvLyByZXBsYWNlbWVudHMg4oCUIHN0cnVjdHVyZWRQYXRjaCBuZWVkcyBiZWZvcmUvYWZ0ZXIgc3RyaW5ncy4gcmVwbGFjZV9hbGxcbiAgICAgIC8vIHJvdXRlcyB0aHJvdWdoIHRoZSBjaHVua2VkIHBhdGggYmVsb3cgKHNob3dzIGZpcnN0LW9jY3VycmVuY2Ugd2luZG93O1xuICAgICAgLy8gbWF0Y2hlcyB3aXRoaW4gdGhlIHNsaWNlIHN0aWxsIHJlcGxhY2UgdmlhIGVkaXQucmVwbGFjZV9hbGwpLlxuICAgICAgaWYgKCFzaW5nbGUgfHwgc2luZ2xlLm9sZF9zdHJpbmcgPT09ICcnKSB7XG4gICAgICAgIGNvbnN0IGZpbGUgPSBhd2FpdCByZWFkQ2FwcGVkKGhhbmRsZSlcbiAgICAgICAgaWYgKGZpbGUgPT09IG51bGwpIHJldHVybiBkaWZmVG9vbElucHV0c09ubHkoZmlsZV9wYXRoLCB2YWxpZClcbiAgICAgICAgY29uc3Qgbm9ybWFsaXplZCA9IHZhbGlkLm1hcChlID0+IG5vcm1hbGl6ZUVkaXQoZmlsZSwgZSkpXG4gICAgICAgIHJldHVybiB7XG4gICAgICAgICAgcGF0Y2g6IGdldFBhdGNoRm9yRGlzcGxheSh7XG4gICAgICAgICAgICBmaWxlUGF0aDogZmlsZV9wYXRoLFxuICAgICAgICAgICAgZmlsZUNvbnRlbnRzOiBmaWxlLFxuICAgICAgICAgICAgZWRpdHM6IG5vcm1hbGl6ZWQsXG4gICAgICAgICAgfSksXG4gICAgICAgICAgZmlyc3RMaW5lOiBmaXJzdExpbmVPZihmaWxlKSxcbiAgICAgICAgICBmaWxlQ29udGVudDogZmlsZSxcbiAgICAgICAgfVxuICAgICAgfVxuXG4gICAgICBjb25zdCBjdHggPSBhd2FpdCBzY2FuRm9yQ29udGV4dChoYW5kbGUsIHNpbmdsZS5vbGRfc3RyaW5nLCBDT05URVhUX0xJTkVTKVxuICAgICAgaWYgKGN0eC50cnVuY2F0ZWQgfHwgY3R4LmNvbnRlbnQgPT09ICcnKSB7XG4gICAgICAgIHJldHVybiBkaWZmVG9vbElucHV0c09ubHkoZmlsZV9wYXRoLCBbc2luZ2xlXSlcbiAgICAgIH1cbiAgICAgIGNvbnN0IG5vcm1hbGl6ZWQgPSBub3JtYWxpemVFZGl0KGN0eC5jb250ZW50LCBzaW5nbGUpXG4gICAgICBjb25zdCBodW5rcyA9IGdldFBhdGNoRm9yRGlzcGxheSh7XG4gICAgICAgIGZpbGVQYXRoOiBmaWxlX3BhdGgsXG4gICAgICAgIGZpbGVDb250ZW50czogY3R4LmNvbnRlbnQsXG4gICAgICAgIGVkaXRzOiBbbm9ybWFsaXplZF0sXG4gICAgICB9KVxuICAgICAgcmV0dXJuIHtcbiAgICAgICAgcGF0Y2g6IGFkanVzdEh1bmtMaW5lTnVtYmVycyhodW5rcywgY3R4LmxpbmVPZmZzZXQgLSAxKSxcbiAgICAgICAgZmlyc3RMaW5lOiBjdHgubGluZU9mZnNldCA9PT0gMSA/IGZpcnN0TGluZU9mKGN0eC5jb250ZW50KSA6IG51bGwsXG4gICAgICAgIGZpbGVDb250ZW50OiBjdHguY29udGVudCxcbiAgICAgIH1cbiAgICB9IGZpbmFsbHkge1xuICAgICAgYXdhaXQgaGFuZGxlLmNsb3NlKClcbiAgICB9XG4gIH0gY2F0Y2ggKGUpIHtcbiAgICBsb2dFcnJvcihlIGFzIEVycm9yKVxuICAgIHJldHVybiBkaWZmVG9vbElucHV0c09ubHkoZmlsZV9wYXRoLCB2YWxpZClcbiAgfVxufVxuXG5mdW5jdGlvbiBkaWZmVG9vbElucHV0c09ubHkoZmlsZVBhdGg6IHN0cmluZywgZWRpdHM6IEZpbGVFZGl0W10pOiBEaWZmRGF0YSB7XG4gIHJldHVybiB7XG4gICAgcGF0Y2g6IGVkaXRzLmZsYXRNYXAoZSA9PlxuICAgICAgZ2V0UGF0Y2hGb3JEaXNwbGF5KHtcbiAgICAgICAgZmlsZVBhdGgsXG4gICAgICAgIGZpbGVDb250ZW50czogZS5vbGRfc3RyaW5nLFxuICAgICAgICBlZGl0czogW2VdLFxuICAgICAgfSksXG4gICAgKSxcbiAgICBmaXJzdExpbmU6IG51bGwsXG4gICAgZmlsZUNvbnRlbnQ6IHVuZGVmaW5lZCxcbiAgfVxufVxuXG5mdW5jdGlvbiBub3JtYWxpemVFZGl0KGZpbGVDb250ZW50OiBzdHJpbmcsIGVkaXQ6IEZpbGVFZGl0KTogRmlsZUVkaXQge1xuICBjb25zdCBhY3R1YWxPbGQgPVxuICAgIGZpbmRBY3R1YWxTdHJpbmcoZmlsZUNvbnRlbnQsIGVkaXQub2xkX3N0cmluZykgfHwgZWRpdC5vbGRfc3RyaW5nXG4gIGNvbnN0IGFjdHVhbE5ldyA9IHByZXNlcnZlUXVvdGVTdHlsZShcbiAgICBlZGl0Lm9sZF9zdHJpbmcsXG4gICAgYWN0dWFsT2xkLFxuICAgIGVkaXQubmV3X3N0cmluZyxcbiAgKVxuICByZXR1cm4geyAuLi5lZGl0LCBvbGRfc3RyaW5nOiBhY3R1YWxPbGQsIG5ld19zdHJpbmc6IGFjdHVhbE5l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
|
src/components/FileEditToolUseRejectedMessage.tsx
ADDED
|
@@ -0,0 +1,170 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { c as _c } from "react/compiler-runtime";
|
| 2 |
+
import type { StructuredPatchHunk } from 'diff';
|
| 3 |
+
import { relative } from 'path';
|
| 4 |
+
import * as React from 'react';
|
| 5 |
+
import { useTerminalSize } from 'src/hooks/useTerminalSize.js';
|
| 6 |
+
import { getCwd } from 'src/utils/cwd.js';
|
| 7 |
+
import { Box, Text } from '../ink.js';
|
| 8 |
+
import { HighlightedCode } from './HighlightedCode.js';
|
| 9 |
+
import { MessageResponse } from './MessageResponse.js';
|
| 10 |
+
import { StructuredDiffList } from './StructuredDiffList.js';
|
| 11 |
+
const MAX_LINES_TO_RENDER = 10;
|
| 12 |
+
type Props = {
|
| 13 |
+
file_path: string;
|
| 14 |
+
operation: 'write' | 'update';
|
| 15 |
+
// For updates - show diff
|
| 16 |
+
patch?: StructuredPatchHunk[];
|
| 17 |
+
firstLine: string | null;
|
| 18 |
+
fileContent?: string;
|
| 19 |
+
// For new file creation - show content preview
|
| 20 |
+
content?: string;
|
| 21 |
+
style?: 'condensed';
|
| 22 |
+
verbose: boolean;
|
| 23 |
+
};
|
| 24 |
+
export function FileEditToolUseRejectedMessage(t0) {
|
| 25 |
+
const $ = _c(38);
|
| 26 |
+
const {
|
| 27 |
+
file_path,
|
| 28 |
+
operation,
|
| 29 |
+
patch,
|
| 30 |
+
firstLine,
|
| 31 |
+
fileContent,
|
| 32 |
+
content,
|
| 33 |
+
style,
|
| 34 |
+
verbose
|
| 35 |
+
} = t0;
|
| 36 |
+
const {
|
| 37 |
+
columns
|
| 38 |
+
} = useTerminalSize();
|
| 39 |
+
let t1;
|
| 40 |
+
if ($[0] !== operation) {
|
| 41 |
+
t1 = <Text color="subtle">User rejected {operation} to </Text>;
|
| 42 |
+
$[0] = operation;
|
| 43 |
+
$[1] = t1;
|
| 44 |
+
} else {
|
| 45 |
+
t1 = $[1];
|
| 46 |
+
}
|
| 47 |
+
let t2;
|
| 48 |
+
if ($[2] !== file_path || $[3] !== verbose) {
|
| 49 |
+
t2 = verbose ? file_path : relative(getCwd(), file_path);
|
| 50 |
+
$[2] = file_path;
|
| 51 |
+
$[3] = verbose;
|
| 52 |
+
$[4] = t2;
|
| 53 |
+
} else {
|
| 54 |
+
t2 = $[4];
|
| 55 |
+
}
|
| 56 |
+
let t3;
|
| 57 |
+
if ($[5] !== t2) {
|
| 58 |
+
t3 = <Text bold={true} color="subtle">{t2}</Text>;
|
| 59 |
+
$[5] = t2;
|
| 60 |
+
$[6] = t3;
|
| 61 |
+
} else {
|
| 62 |
+
t3 = $[6];
|
| 63 |
+
}
|
| 64 |
+
let t4;
|
| 65 |
+
if ($[7] !== t1 || $[8] !== t3) {
|
| 66 |
+
t4 = <Box flexDirection="row">{t1}{t3}</Box>;
|
| 67 |
+
$[7] = t1;
|
| 68 |
+
$[8] = t3;
|
| 69 |
+
$[9] = t4;
|
| 70 |
+
} else {
|
| 71 |
+
t4 = $[9];
|
| 72 |
+
}
|
| 73 |
+
const text = t4;
|
| 74 |
+
if (style === "condensed" && !verbose) {
|
| 75 |
+
let t5;
|
| 76 |
+
if ($[10] !== text) {
|
| 77 |
+
t5 = <MessageResponse>{text}</MessageResponse>;
|
| 78 |
+
$[10] = text;
|
| 79 |
+
$[11] = t5;
|
| 80 |
+
} else {
|
| 81 |
+
t5 = $[11];
|
| 82 |
+
}
|
| 83 |
+
return t5;
|
| 84 |
+
}
|
| 85 |
+
if (operation === "write" && content !== undefined) {
|
| 86 |
+
let plusLines;
|
| 87 |
+
let t5;
|
| 88 |
+
if ($[12] !== content || $[13] !== verbose) {
|
| 89 |
+
const lines = content.split("\n");
|
| 90 |
+
const numLines = lines.length;
|
| 91 |
+
plusLines = numLines - MAX_LINES_TO_RENDER;
|
| 92 |
+
t5 = verbose ? content : lines.slice(0, MAX_LINES_TO_RENDER).join("\n");
|
| 93 |
+
$[12] = content;
|
| 94 |
+
$[13] = verbose;
|
| 95 |
+
$[14] = plusLines;
|
| 96 |
+
$[15] = t5;
|
| 97 |
+
} else {
|
| 98 |
+
plusLines = $[14];
|
| 99 |
+
t5 = $[15];
|
| 100 |
+
}
|
| 101 |
+
const truncatedContent = t5;
|
| 102 |
+
const t6 = truncatedContent || "(No content)";
|
| 103 |
+
const t7 = columns - 12;
|
| 104 |
+
let t8;
|
| 105 |
+
if ($[16] !== file_path || $[17] !== t6 || $[18] !== t7) {
|
| 106 |
+
t8 = <HighlightedCode code={t6} filePath={file_path} width={t7} dim={true} />;
|
| 107 |
+
$[16] = file_path;
|
| 108 |
+
$[17] = t6;
|
| 109 |
+
$[18] = t7;
|
| 110 |
+
$[19] = t8;
|
| 111 |
+
} else {
|
| 112 |
+
t8 = $[19];
|
| 113 |
+
}
|
| 114 |
+
let t9;
|
| 115 |
+
if ($[20] !== plusLines || $[21] !== verbose) {
|
| 116 |
+
t9 = !verbose && plusLines > 0 && <Text dimColor={true}>… +{plusLines} lines</Text>;
|
| 117 |
+
$[20] = plusLines;
|
| 118 |
+
$[21] = verbose;
|
| 119 |
+
$[22] = t9;
|
| 120 |
+
} else {
|
| 121 |
+
t9 = $[22];
|
| 122 |
+
}
|
| 123 |
+
let t10;
|
| 124 |
+
if ($[23] !== t8 || $[24] !== t9 || $[25] !== text) {
|
| 125 |
+
t10 = <MessageResponse><Box flexDirection="column">{text}{t8}{t9}</Box></MessageResponse>;
|
| 126 |
+
$[23] = t8;
|
| 127 |
+
$[24] = t9;
|
| 128 |
+
$[25] = text;
|
| 129 |
+
$[26] = t10;
|
| 130 |
+
} else {
|
| 131 |
+
t10 = $[26];
|
| 132 |
+
}
|
| 133 |
+
return t10;
|
| 134 |
+
}
|
| 135 |
+
if (!patch || patch.length === 0) {
|
| 136 |
+
let t5;
|
| 137 |
+
if ($[27] !== text) {
|
| 138 |
+
t5 = <MessageResponse>{text}</MessageResponse>;
|
| 139 |
+
$[27] = text;
|
| 140 |
+
$[28] = t5;
|
| 141 |
+
} else {
|
| 142 |
+
t5 = $[28];
|
| 143 |
+
}
|
| 144 |
+
return t5;
|
| 145 |
+
}
|
| 146 |
+
const t5 = columns - 12;
|
| 147 |
+
let t6;
|
| 148 |
+
if ($[29] !== fileContent || $[30] !== file_path || $[31] !== firstLine || $[32] !== patch || $[33] !== t5) {
|
| 149 |
+
t6 = <StructuredDiffList hunks={patch} dim={true} width={t5} filePath={file_path} firstLine={firstLine} fileContent={fileContent} />;
|
| 150 |
+
$[29] = fileContent;
|
| 151 |
+
$[30] = file_path;
|
| 152 |
+
$[31] = firstLine;
|
| 153 |
+
$[32] = patch;
|
| 154 |
+
$[33] = t5;
|
| 155 |
+
$[34] = t6;
|
| 156 |
+
} else {
|
| 157 |
+
t6 = $[34];
|
| 158 |
+
}
|
| 159 |
+
let t7;
|
| 160 |
+
if ($[35] !== t6 || $[36] !== text) {
|
| 161 |
+
t7 = <MessageResponse><Box flexDirection="column">{text}{t6}</Box></MessageResponse>;
|
| 162 |
+
$[35] = t6;
|
| 163 |
+
$[36] = text;
|
| 164 |
+
$[37] = t7;
|
| 165 |
+
} else {
|
| 166 |
+
t7 = $[37];
|
| 167 |
+
}
|
| 168 |
+
return t7;
|
| 169 |
+
}
|
| 170 |
+
//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJuYW1lcyI6WyJTdHJ1Y3R1cmVkUGF0Y2hIdW5rIiwicmVsYXRpdmUiLCJSZWFjdCIsInVzZVRlcm1pbmFsU2l6ZSIsImdldEN3ZCIsIkJveCIsIlRleHQiLCJIaWdobGlnaHRlZENvZGUiLCJNZXNzYWdlUmVzcG9uc2UiLCJTdHJ1Y3R1cmVkRGlmZkxpc3QiLCJNQVhfTElORVNfVE9fUkVOREVSIiwiUHJvcHMiLCJmaWxlX3BhdGgiLCJvcGVyYXRpb24iLCJwYXRjaCIsImZpcnN0TGluZSIsImZpbGVDb250ZW50IiwiY29udGVudCIsInN0eWxlIiwidmVyYm9zZSIsIkZpbGVFZGl0VG9vbFVzZVJlamVjdGVkTWVzc2FnZSIsInQwIiwiJCIsIl9jIiwiY29sdW1ucyIsInQxIiwidDIiLCJ0MyIsInQ0IiwidGV4dCIsInQ1IiwidW5kZWZpbmVkIiwicGx1c0xpbmVzIiwibGluZXMiLCJzcGxpdCIsIm51bUxpbmVzIiwibGVuZ3RoIiwic2xpY2UiLCJqb2luIiwidHJ1bmNhdGVkQ29udGVudCIsInQ2IiwidDciLCJ0OCIsInQ5IiwidDEwIl0sInNvdXJjZXMiOlsiRmlsZUVkaXRUb29sVXNlUmVqZWN0ZWRNZXNzYWdlLnRzeCJdLCJzb3VyY2VzQ29udGVudCI6WyJpbXBvcnQgdHlwZSB7IFN0cnVjdHVyZWRQYXRjaEh1bmsgfSBmcm9tICdkaWZmJ1xuaW1wb3J0IHsgcmVsYXRpdmUgfSBmcm9tICdwYXRoJ1xuaW1wb3J0ICogYXMgUmVhY3QgZnJvbSAncmVhY3QnXG5pbXBvcnQgeyB1c2VUZXJtaW5hbFNpemUgfSBmcm9tICdzcmMvaG9va3MvdXNlVGVybWluYWxTaXplLmpzJ1xuaW1wb3J0IHsgZ2V0Q3dkIH0gZnJvbSAnc3JjL3V0aWxzL2N3ZC5qcydcbmltcG9ydCB7IEJveCwgVGV4dCB9IGZyb20gJy4uL2luay5qcydcbmltcG9ydCB7IEhpZ2hsaWdodGVkQ29kZSB9IGZyb20gJy4vSGlnaGxpZ2h0ZWRDb2RlLmpzJ1xuaW1wb3J0IHsgTWVzc2FnZVJlc3BvbnNlIH0gZnJvbSAnLi9NZXNzYWdlUmVzcG9uc2UuanMnXG5pbXBvcnQgeyBTdHJ1Y3R1cmVkRGlmZkxpc3QgfSBmcm9tICcuL1N0cnVjdHVyZWREaWZmTGlzdC5qcydcblxuY29uc3QgTUFYX0xJTkVTX1RPX1JFTkRFUiA9IDEwXG5cbnR5cGUgUHJvcHMgPSB7XG4gIGZpbGVfcGF0aDogc3RyaW5nXG4gIG9wZXJhdGlvbjogJ3dyaXRlJyB8ICd1cGRhdGUnXG4gIC8vIEZvciB1cGRhdGVzIC0gc2hvdyBkaWZmXG4gIHBhdGNoPzogU3RydWN0dXJlZFBhdGNoSHVua1tdXG4gIGZpcnN0TGluZTogc3RyaW5nIHwgbnVsbFxuICBmaWxlQ29udGVudD86IHN0cmluZ1xuICAvLyBGb3IgbmV3IGZpbGUgY3JlYXRpb24gLSBzaG93IGNvbnRlbnQgcHJldmlld1xuICBjb250ZW50Pzogc3RyaW5nXG4gIHN0eWxlPzogJ2NvbmRlbnNlZCdcbiAgdmVyYm9zZTogYm9vbGVhblxufVxuXG5leHBvcnQgZnVuY3Rpb24gRmlsZUVkaXRUb29sVXNlUmVqZWN0ZWRNZXNzYWdlKHtcbiAgZmlsZV9wYXRoLFxuICBvcGVyYXRpb24sXG4gIHBhdGNoLFxuICBmaXJzdExpbmUsXG4gIGZpbGVDb250ZW50LFxuICBjb250ZW50LFxuICBzdHlsZSxcbiAgdmVyYm9zZSxcbn06IFByb3BzKTogUmVhY3QuUmVhY3ROb2RlIHtcbiAgY29uc3QgeyBjb2x1bW5zIH0gPSB1c2VUZXJtaW5hbFNpemUoKVxuICBjb25zdCB0ZXh0ID0gKFxuICAgIDxCb3ggZmxleERpcmVjdGlvbj1cInJvd1wiPlxuICAgICAgPFRleHQgY29sb3I9XCJzdWJ0bGVcIj5Vc2VyIHJlamVjdGVkIHtvcGVyYXRpb259IHRvIDwvVGV4dD5cbiAgICAgIDxUZXh0IGJvbGQgY29sb3I9XCJzdWJ0bGVcIj5cbiAgICAgICAge3ZlcmJvc2UgPyBmaWxlX3BhdGggOiByZWxhdGl2ZShnZXRDd2QoKSwgZmlsZV9wYXRoKX1cbiAgICAgIDwvVGV4dD5cbiAgICA8L0JveD5cbiAgKVxuXG4gIC8vIEZvciBjb25kZW5zZWQgc3R5bGUsIGp1c3Qgc2hvdyB0aGUgdGV4dFxuICBpZiAoc3R5bGUgPT09ICdjb25kZW5zZWQnICYmICF2ZXJib3NlKSB7XG4gICAgcmV0dXJuIDxNZXNzYWdlUmVzcG9uc2U+e3RleHR9PC9NZXNzYWdlUmVzcG9uc2U+XG4gIH1cblxuICAvLyBGb3IgbmV3IGZpbGUgY3JlYXRpb24sIHNob3cgY29udGVudCBwcmV2aWV3IChkaW1tZWQpXG4gIGlmIChvcGVyYXRpb24gPT09ICd3cml0ZScgJiYgY29udGVudCAhPT0gdW5kZWZpbmVkKSB7XG4gICAgY29uc3QgbGluZXMgPSBjb250ZW50LnNwbGl0KCdcXG4nKVxuICAgIGNvbnN0IG51bUxpbmVzID0gbGluZXMubGVuZ3RoXG4gICAgY29uc3QgcGx1c0xpbmVzID0gbnVtTGluZXMgLSBNQVhfTElORVNfVE9fUkVOREVSXG4gICAgY29uc3QgdHJ1bmNhdGVkQ29udGVudCA9IHZlcmJvc2VcbiAgICAgID8gY29udGVudFxuICAgICAgOiBsaW5lcy5zbGljZSgwLCBNQVhfTElORVNfVE9fUkVOREVSKS5qb2luKCdcXG4nKVxuXG4gICAgcmV0dXJuIChcbiAgICAgIDxNZXNzYWdlUmVzcG9uc2U+XG4gICAgICAgIDxCb3ggZmxleERpcmVjdGlvbj1cImNvbHVtblwiPlxuICAgICAgICAgIHt0ZXh0fVxuICAgICAgICAgIDxIaWdobGlnaHRlZENvZGVcbiAgICAgICAgICAgIGNvZGU9e3RydW5jYXRlZENvbnRlbnQgfHwgJyhObyBjb250ZW50KSd9XG4gICAgICAgICAgICBmaWxlUGF0aD17ZmlsZV9wYXRofVxuICAgICAgICAgICAgd2lkdGg9e2NvbHVtbnMgLSAxMn1cbiAgICAgICAgICAgIGRpbVxuICAgICAgICAgIC8+XG4gICAgICAgICAgeyF2ZXJib3NlICYmIHBsdXNMaW5lcyA+IDAgJiYgKFxuICAgICAgICAgICAgPFRleHQgZGltQ29sb3I+4oCmICt7cGx1c0xpbmVzfSBsaW5lczwvVGV4dD5cbiAgICAgICAgICApfVxuICAgICAgICA8L0JveD5cbiAgICAgIDwvTWVzc2FnZVJlc3BvbnNlPlxuICAgIClcbiAgfVxuXG4gIC8vIEZvciB1cGRhdGVzLCBzaG93IGRpZmZcbiAgaWYgKCFwYXRjaCB8fCBwYXRjaC5sZW5ndGggPT09IDApIHtcbiAgICByZXR1cm4gPE1lc3NhZ2VSZXNwb25zZT57dGV4dH08L01lc3NhZ2VSZXNwb25zZT5cbiAgfVxuXG4gIHJldHVybiAoXG4gICAgPE1lc3NhZ2VSZXNwb25zZT5cbiAgICAgIDxCb3ggZmxleERpcmVjdGlvbj1cImNvbHVtblwiPlxuICAgICAgICB7dGV4dH1cbiAgICAgICAgPFN0cnVjdHVyZWREaWZmTGlzdFxuICAgICAgICAgIGh1bmtzPXtwYXRjaH1cbiAgICAgICAgICBkaW1cbiAgICAgICAgICB3aWR0aD17Y29sdW1ucyAtIDEyfVxuICAgICAgICAgIGZpbGVQYXRoPXtmaWxlX3BhdGh9XG4gICAgICAgICAgZmlyc3RMaW5lPXtmaXJzdExpbmV9XG4gICAgICAgICAgZmlsZUNvbnRlbnQ9e2ZpbGVDb250ZW50fVxuICAgICAgICAvPlxuICAgICAgPC9Cb3g+XG4gICAgPC9NZXNzYWdlUmVzcG9uc2U+XG4gIClcbn1cbiJdLCJtYXBwaW5ncyI6IjtBQUFBLGNBQWNBLG1CQUFtQixRQUFRLE1BQU07QUFDL0MsU0FBU0MsUUFBUSxRQUFRLE1BQU07QUFDL0IsT0FBTyxLQUFLQyxLQUFLLE1BQU0sT0FBTztBQUM5QixTQUFTQyxlQUFlLFFBQVEsOEJBQThCO0FBQzlELFNBQVNDLE1BQU0sUUFBUSxrQkFBa0I7QUFDekMsU0FBU0MsR0FBRyxFQUFFQyxJQUFJLFFBQVEsV0FBVztBQUNyQyxTQUFTQyxlQUFlLFFBQVEsc0JBQXNCO0FBQ3RELFNBQVNDLGVBQWUsUUFBUSxzQkFBc0I7QUFDdEQsU0FBU0Msa0JBQWtCLFFBQVEseUJBQXlCO0FBRTVELE1BQU1DLG1CQUFtQixHQUFHLEVBQUU7QUFFOUIsS0FBS0MsS0FBSyxHQUFHO0VBQ1hDLFNBQVMsRUFBRSxNQUFN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
|
src/components/FilePathLink.tsx
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { c as _c } from "react/compiler-runtime";
|
| 2 |
+
import React from 'react';
|
| 3 |
+
import { pathToFileURL } from 'url';
|
| 4 |
+
import Link from '../ink/components/Link.js';
|
| 5 |
+
type Props = {
|
| 6 |
+
/** The absolute file path */
|
| 7 |
+
filePath: string;
|
| 8 |
+
/** Optional display text (defaults to filePath) */
|
| 9 |
+
children?: React.ReactNode;
|
| 10 |
+
};
|
| 11 |
+
|
| 12 |
+
/**
|
| 13 |
+
* Renders a file path as an OSC 8 hyperlink.
|
| 14 |
+
* This helps terminals like iTerm correctly identify file paths
|
| 15 |
+
* even when they appear inside parentheses or other text.
|
| 16 |
+
*/
|
| 17 |
+
export function FilePathLink(t0) {
|
| 18 |
+
const $ = _c(5);
|
| 19 |
+
const {
|
| 20 |
+
filePath,
|
| 21 |
+
children
|
| 22 |
+
} = t0;
|
| 23 |
+
let t1;
|
| 24 |
+
if ($[0] !== filePath) {
|
| 25 |
+
t1 = pathToFileURL(filePath);
|
| 26 |
+
$[0] = filePath;
|
| 27 |
+
$[1] = t1;
|
| 28 |
+
} else {
|
| 29 |
+
t1 = $[1];
|
| 30 |
+
}
|
| 31 |
+
const t2 = children ?? filePath;
|
| 32 |
+
let t3;
|
| 33 |
+
if ($[2] !== t1.href || $[3] !== t2) {
|
| 34 |
+
t3 = <Link url={t1.href}>{t2}</Link>;
|
| 35 |
+
$[2] = t1.href;
|
| 36 |
+
$[3] = t2;
|
| 37 |
+
$[4] = t3;
|
| 38 |
+
} else {
|
| 39 |
+
t3 = $[4];
|
| 40 |
+
}
|
| 41 |
+
return t3;
|
| 42 |
+
}
|
| 43 |
+
//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJuYW1lcyI6WyJSZWFjdCIsInBhdGhUb0ZpbGVVUkwiLCJMaW5rIiwiUHJvcHMiLCJmaWxlUGF0aCIsImNoaWxkcmVuIiwiUmVhY3ROb2RlIiwiRmlsZVBhdGhMaW5rIiwidDAiLCIkIiwiX2MiLCJ0MSIsInQyIiwidDMiLCJocmVmIl0sInNvdXJjZXMiOlsiRmlsZVBhdGhMaW5rLnRzeCJdLCJzb3VyY2VzQ29udGVudCI6WyJpbXBvcnQgUmVhY3QgZnJvbSAncmVhY3QnXG5pbXBvcnQgeyBwYXRoVG9GaWxlVVJMIH0gZnJvbSAndXJsJ1xuaW1wb3J0IExpbmsgZnJvbSAnLi4vaW5rL2NvbXBvbmVudHMvTGluay5qcydcblxudHlwZSBQcm9wcyA9IHtcbiAgLyoqIFRoZSBhYnNvbHV0ZSBmaWxlIHBhdGggKi9cbiAgZmlsZVBhdGg6IHN0cmluZ1xuICAvKiogT3B0aW9uYWwgZGlzcGxheSB0ZXh0IChkZWZhdWx0cyB0byBmaWxlUGF0aCkgKi9cbiAgY2hpbGRyZW4/OiBSZWFjdC5SZWFjdE5vZGVcbn1cblxuLyoqXG4gKiBSZW5kZXJzIGEgZmlsZSBwYXRoIGFzIGFuIE9TQyA4IGh5cGVybGluay5cbiAqIFRoaXMgaGVscHMgdGVybWluYWxzIGxpa2UgaVRlcm0gY29ycmVjdGx5IGlkZW50aWZ5IGZpbGUgcGF0aHNcbiAqIGV2ZW4gd2hlbiB0aGV5IGFwcGVhciBpbnNpZGUgcGFyZW50aGVzZXMgb3Igb3RoZXIgdGV4dC5cbiAqL1xuZXhwb3J0IGZ1bmN0aW9uIEZpbGVQYXRoTGluayh7IGZpbGVQYXRoLCBjaGlsZHJlbiB9OiBQcm9wcyk6IFJlYWN0LlJlYWN0Tm9kZSB7XG4gIHJldHVybiA8TGluayB1cmw9e3BhdGhUb0ZpbGVVUkwoZmlsZVBhdGgpLmhyZWZ9PntjaGlsZHJlbiA/PyBmaWxlUGF0aH08L0xpbms+XG59XG4iXSwibWFwcGluZ3MiOiI7QUFBQSxPQUFPQSxLQUFLLE1BQU0sT0FBTztBQUN6QixTQUFTQyxhQUFhLFFBQVEsS0FBSztBQUNuQyxPQUFPQyxJQUFJLE1BQU0sMkJBQTJCO0FBRTVDLEtBQUtDLEtBQUssR0FBRztFQUNYO0VBQ0FDLFFBQVEsRUFBRSxNQUFNO0VBQ2hCO0VBQ0FDLFFBQVEsQ0FBQyxFQUFFTCxLQUFLLENBQUNNLFNBQVM7QUFDNUIsQ0FBQzs7QUFFRDtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0EsT0FBTyxTQUFBQyxhQUFBQyxFQUFBO0VBQUEsTUFBQUMsQ0FBQSxHQUFBQyxFQUFBO0VBQXNCO0lBQUFOLFFBQUE7SUFBQUM7RUFBQSxJQUFBRyxFQUE2QjtFQUFBLElBQUFHLEVBQUE7RUFBQSxJQUFBRixDQUFBLFFBQUFMLFFBQUE7SUFDdENPLEVBQUEsR0FBQVYsYUFBYSxDQUFDRyxRQUFRLENBQUM7SUFBQUssQ0FBQSxNQUFBTCxRQUFBO0lBQUFLLENBQUEsTUFBQUUsRUFBQTtFQUFBO0lBQUFBLEVBQUEsR0FBQUYsQ0FBQTtFQUFBO0VBQVEsTUFBQUcsRUFBQSxHQUFBUCxRQUFvQixJQUFwQkQsUUFBb0I7RUFBQSxJQUFBUyxFQUFBO0VBQUEsSUFBQUosQ0FBQSxRQUFBRSxFQUFBLENBQUFHLElBQUEsSUFBQUwsQ0FBQSxRQUFBRyxFQUFBO0lBQTlEQyxFQUFBLElBQUMsSUFBSSxDQUFNLEdBQTRCLENBQTVCLENBQUFGLEVBQXVCLENBQUFHLElBQUksQ0FBQyxDQUFHLENBQUFGLEVBQW1CLENBQUUsRUFBOUQsSUFBSSxDQUFpRTtJQUFBSCxDQUFBLE1BQUFFLEVBQUEsQ0FBQUcsSUFBQTtJQUFBTCxDQUFBLE1BQUFHLEVBQUE7SUFBQUgsQ0FBQSxNQUFBSSxFQUFBO0VBQUE7SUFBQUEsRUFBQSxHQUFBSixDQUFBO0VBQUE7RUFBQSxPQUF0RUksRUFBc0U7QUFBQSIsImlnbm9yZUxpc3QiOltdfQ==
|
src/components/IdeStatusIndicator.tsx
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { c as _c } from "react/compiler-runtime";
|
| 2 |
+
import { basename } from 'path';
|
| 3 |
+
import * as React from 'react';
|
| 4 |
+
import { useIdeConnectionStatus } from '../hooks/useIdeConnectionStatus.js';
|
| 5 |
+
import type { IDESelection } from '../hooks/useIdeSelection.js';
|
| 6 |
+
import { Text } from '../ink.js';
|
| 7 |
+
import type { MCPServerConnection } from '../services/mcp/types.js';
|
| 8 |
+
type IdeStatusIndicatorProps = {
|
| 9 |
+
ideSelection: IDESelection | undefined;
|
| 10 |
+
mcpClients?: MCPServerConnection[];
|
| 11 |
+
};
|
| 12 |
+
export function IdeStatusIndicator(t0) {
|
| 13 |
+
const $ = _c(7);
|
| 14 |
+
const {
|
| 15 |
+
ideSelection,
|
| 16 |
+
mcpClients
|
| 17 |
+
} = t0;
|
| 18 |
+
const {
|
| 19 |
+
status: ideStatus
|
| 20 |
+
} = useIdeConnectionStatus(mcpClients);
|
| 21 |
+
const shouldShowIdeSelection = ideStatus === "connected" && (ideSelection?.filePath || ideSelection?.text && ideSelection.lineCount > 0);
|
| 22 |
+
if (ideStatus === null || !shouldShowIdeSelection || !ideSelection) {
|
| 23 |
+
return null;
|
| 24 |
+
}
|
| 25 |
+
if (ideSelection.text && ideSelection.lineCount > 0) {
|
| 26 |
+
const t1 = ideSelection.lineCount === 1 ? "line" : "lines";
|
| 27 |
+
let t2;
|
| 28 |
+
if ($[0] !== ideSelection.lineCount || $[1] !== t1) {
|
| 29 |
+
t2 = <Text color="ide" key="selection-indicator" wrap="truncate">⧉ {ideSelection.lineCount}{" "}{t1} selected</Text>;
|
| 30 |
+
$[0] = ideSelection.lineCount;
|
| 31 |
+
$[1] = t1;
|
| 32 |
+
$[2] = t2;
|
| 33 |
+
} else {
|
| 34 |
+
t2 = $[2];
|
| 35 |
+
}
|
| 36 |
+
return t2;
|
| 37 |
+
}
|
| 38 |
+
if (ideSelection.filePath) {
|
| 39 |
+
let t1;
|
| 40 |
+
if ($[3] !== ideSelection.filePath) {
|
| 41 |
+
t1 = basename(ideSelection.filePath);
|
| 42 |
+
$[3] = ideSelection.filePath;
|
| 43 |
+
$[4] = t1;
|
| 44 |
+
} else {
|
| 45 |
+
t1 = $[4];
|
| 46 |
+
}
|
| 47 |
+
let t2;
|
| 48 |
+
if ($[5] !== t1) {
|
| 49 |
+
t2 = <Text color="ide" key="selection-indicator" wrap="truncate">⧉ In {t1}</Text>;
|
| 50 |
+
$[5] = t1;
|
| 51 |
+
$[6] = t2;
|
| 52 |
+
} else {
|
| 53 |
+
t2 = $[6];
|
| 54 |
+
}
|
| 55 |
+
return t2;
|
| 56 |
+
}
|
| 57 |
+
}
|
| 58 |
+
//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJuYW1lcyI6WyJiYXNlbmFtZSIsIlJlYWN0IiwidXNlSWRlQ29ubmVjdGlvblN0YXR1cyIsIklERVNlbGVjdGlvbiIsIlRleHQiLCJNQ1BTZXJ2ZXJDb25uZWN0aW9uIiwiSWRlU3RhdHVzSW5kaWNhdG9yUHJvcHMiLCJpZGVTZWxlY3Rpb24iLCJtY3BDbGllbnRzIiwiSWRlU3RhdHVzSW5kaWNhdG9yIiwidDAiLCIkIiwiX2MiLCJzdGF0dXMiLCJpZGVTdGF0dXMiLCJzaG91bGRTaG93SWRlU2VsZWN0aW9uIiwiZmlsZVBhdGgiLCJ0ZXh0IiwibGluZUNvdW50IiwidDEiLCJ0MiJdLCJzb3VyY2VzIjpbIklkZVN0YXR1c0luZGljYXRvci50c3giXSwic291cmNlc0NvbnRlbnQiOlsiaW1wb3J0IHsgYmFzZW5hbWUgfSBmcm9tICdwYXRoJ1xuaW1wb3J0ICogYXMgUmVhY3QgZnJvbSAncmVhY3QnXG5pbXBvcnQgeyB1c2VJZGVDb25uZWN0aW9uU3RhdHVzIH0gZnJvbSAnLi4vaG9va3MvdXNlSWRlQ29ubmVjdGlvblN0YXR1cy5qcydcbmltcG9ydCB0eXBlIHsgSURFU2VsZWN0aW9uIH0gZnJvbSAnLi4vaG9va3MvdXNlSWRlU2VsZWN0aW9uLmpzJ1xuaW1wb3J0IHsgVGV4dCB9IGZyb20gJy4uL2luay5qcydcbmltcG9ydCB0eXBlIHsgTUNQU2VydmVyQ29ubmVjdGlvbiB9IGZyb20gJy4uL3NlcnZpY2VzL21jcC90eXBlcy5qcydcblxudHlwZSBJZGVTdGF0dXNJbmRpY2F0b3JQcm9wcyA9IHtcbiAgaWRlU2VsZWN0aW9uOiBJREVTZWxlY3Rpb24gfCB1bmRlZmluZWRcbiAgbWNwQ2xpZW50cz86IE1DUFNlcnZlckNvbm5lY3Rpb25bXVxufVxuXG5leHBvcnQgZnVuY3Rpb24gSWRlU3RhdHVzSW5kaWNhdG9yKHtcbiAgaWRlU2VsZWN0aW9uLFxuICBtY3BDbGllbnRzLFxufTogSWRlU3RhdHVzSW5kaWNhdG9yUHJvcHMpOiBSZWFjdC5SZWFjdE5vZGUge1xuICBjb25zdCB7IHN0YXR1czogaWRlU3RhdHVzIH0gPSB1c2VJZGVDb25uZWN0aW9uU3RhdHVzKG1jcENsaWVudHMpXG5cbiAgLy8gQ2hlY2sgaWYgd2Ugc2hvdWxkIHNob3cgdGhlIElERSBzZWxlY3Rpb24gaW5kaWNhdG9yXG4gIGNvbnN0IHNob3VsZFNob3dJZGVTZWxlY3Rpb24gPVxuICAgIGlkZVN0YXR1cyA9PT0gJ2Nvbm5lY3RlZCcgJiZcbiAgICAoaWRlU2VsZWN0aW9uPy5maWxlUGF0aCB8fFxuICAgICAgKGlkZVNlbGVjdGlvbj8udGV4dCAmJiBpZGVTZWxlY3Rpb24ubGluZUNvdW50ID4gMCkpXG5cbiAgaWYgKGlkZVN0YXR1cyA9PT0gbnVsbCB8fCAhc2hvdWxkU2hvd0lkZVNlbGVjdGlvbiB8fCAhaWRlU2VsZWN0aW9uKSB7XG4gICAgcmV0dXJuIG51bGxcbiAgfVxuXG4gIGlmIChpZGVTZWxlY3Rpb24udGV4dCAmJiBpZGVTZWxlY3Rpb24ubGluZUNvdW50ID4gMCkge1xuICAgIHJldHVybiAoXG4gICAgICA8VGV4dCBjb2xvcj1cImlkZVwiIGtleT1cInNlbGVjdGlvbi1pbmRpY2F0b3JcIiB3cmFwPVwidHJ1bmNhdGVcIj5cbiAgICAgICAg4qeJIHtpZGVTZWxlY3Rpb24ubGluZUNvdW50fXsnICd9XG4gICAgICAgIHtpZGVTZWxlY3Rpb24ubGluZUNvdW50ID09PSAxID8gJ2xpbmUnIDogJ2xpbmVzJ30gc2VsZWN0ZWRcbiAgICAgIDwvVGV4dD5cbiAgICApXG4gIH1cblxuICBpZiAoaWRlU2VsZWN0aW9uLmZpbGVQYXRoKSB7XG4gICAgcmV0dXJuIChcbiAgICAgIDxUZXh0IGNvbG9yPVwiaWRlXCIga2V5PVwic2VsZWN0aW9uLWluZGljYXRvclwiIHdyYXA9XCJ0cnVuY2F0ZVwiPlxuICAgICAgICDip4kgSW4ge2Jhc2VuYW1lKGlkZVNlbGVjdGlvbi5maWxlUGF0aCl9XG4gICAgICA8L1RleHQ+XG4gICAgKVxuICB9XG59XG4iXSwibWFwcGluZ3MiOiI7QUFBQSxTQUFTQSxRQUFRLFFBQVEs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
|
src/components/IdleReturnDialog.tsx
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { c as _c } from "react/compiler-runtime";
|
| 2 |
+
import React from 'react';
|
| 3 |
+
import { Box, Text } from '../ink.js';
|
| 4 |
+
import { formatTokens } from '../utils/format.js';
|
| 5 |
+
import { Select } from './CustomSelect/index.js';
|
| 6 |
+
import { Dialog } from './design-system/Dialog.js';
|
| 7 |
+
type IdleReturnAction = 'continue' | 'clear' | 'dismiss' | 'never';
|
| 8 |
+
type Props = {
|
| 9 |
+
idleMinutes: number;
|
| 10 |
+
totalInputTokens: number;
|
| 11 |
+
onDone: (action: IdleReturnAction) => void;
|
| 12 |
+
};
|
| 13 |
+
export function IdleReturnDialog(t0) {
|
| 14 |
+
const $ = _c(16);
|
| 15 |
+
const {
|
| 16 |
+
idleMinutes,
|
| 17 |
+
totalInputTokens,
|
| 18 |
+
onDone
|
| 19 |
+
} = t0;
|
| 20 |
+
let t1;
|
| 21 |
+
if ($[0] !== idleMinutes) {
|
| 22 |
+
t1 = formatIdleDuration(idleMinutes);
|
| 23 |
+
$[0] = idleMinutes;
|
| 24 |
+
$[1] = t1;
|
| 25 |
+
} else {
|
| 26 |
+
t1 = $[1];
|
| 27 |
+
}
|
| 28 |
+
const formattedIdle = t1;
|
| 29 |
+
let t2;
|
| 30 |
+
if ($[2] !== totalInputTokens) {
|
| 31 |
+
t2 = formatTokens(totalInputTokens);
|
| 32 |
+
$[2] = totalInputTokens;
|
| 33 |
+
$[3] = t2;
|
| 34 |
+
} else {
|
| 35 |
+
t2 = $[3];
|
| 36 |
+
}
|
| 37 |
+
const formattedTokens = t2;
|
| 38 |
+
const t3 = `You've been away ${formattedIdle} and this conversation is ${formattedTokens} tokens.`;
|
| 39 |
+
let t4;
|
| 40 |
+
if ($[4] !== onDone) {
|
| 41 |
+
t4 = () => onDone("dismiss");
|
| 42 |
+
$[4] = onDone;
|
| 43 |
+
$[5] = t4;
|
| 44 |
+
} else {
|
| 45 |
+
t4 = $[5];
|
| 46 |
+
}
|
| 47 |
+
let t5;
|
| 48 |
+
if ($[6] === Symbol.for("react.memo_cache_sentinel")) {
|
| 49 |
+
t5 = <Box flexDirection="column"><Text>If this is a new task, clearing context will save usage and be faster.</Text></Box>;
|
| 50 |
+
$[6] = t5;
|
| 51 |
+
} else {
|
| 52 |
+
t5 = $[6];
|
| 53 |
+
}
|
| 54 |
+
let t6;
|
| 55 |
+
if ($[7] === Symbol.for("react.memo_cache_sentinel")) {
|
| 56 |
+
t6 = {
|
| 57 |
+
value: "continue" as const,
|
| 58 |
+
label: "Continue this conversation"
|
| 59 |
+
};
|
| 60 |
+
$[7] = t6;
|
| 61 |
+
} else {
|
| 62 |
+
t6 = $[7];
|
| 63 |
+
}
|
| 64 |
+
let t7;
|
| 65 |
+
if ($[8] === Symbol.for("react.memo_cache_sentinel")) {
|
| 66 |
+
t7 = {
|
| 67 |
+
value: "clear" as const,
|
| 68 |
+
label: "Send message as a new conversation"
|
| 69 |
+
};
|
| 70 |
+
$[8] = t7;
|
| 71 |
+
} else {
|
| 72 |
+
t7 = $[8];
|
| 73 |
+
}
|
| 74 |
+
let t8;
|
| 75 |
+
if ($[9] === Symbol.for("react.memo_cache_sentinel")) {
|
| 76 |
+
t8 = [t6, t7, {
|
| 77 |
+
value: "never" as const,
|
| 78 |
+
label: "Don't ask me again"
|
| 79 |
+
}];
|
| 80 |
+
$[9] = t8;
|
| 81 |
+
} else {
|
| 82 |
+
t8 = $[9];
|
| 83 |
+
}
|
| 84 |
+
let t9;
|
| 85 |
+
if ($[10] !== onDone) {
|
| 86 |
+
t9 = <Select options={t8} onChange={value => onDone(value)} />;
|
| 87 |
+
$[10] = onDone;
|
| 88 |
+
$[11] = t9;
|
| 89 |
+
} else {
|
| 90 |
+
t9 = $[11];
|
| 91 |
+
}
|
| 92 |
+
let t10;
|
| 93 |
+
if ($[12] !== t3 || $[13] !== t4 || $[14] !== t9) {
|
| 94 |
+
t10 = <Dialog title={t3} onCancel={t4}>{t5}{t9}</Dialog>;
|
| 95 |
+
$[12] = t3;
|
| 96 |
+
$[13] = t4;
|
| 97 |
+
$[14] = t9;
|
| 98 |
+
$[15] = t10;
|
| 99 |
+
} else {
|
| 100 |
+
t10 = $[15];
|
| 101 |
+
}
|
| 102 |
+
return t10;
|
| 103 |
+
}
|
| 104 |
+
function formatIdleDuration(minutes: number): string {
|
| 105 |
+
if (minutes < 1) {
|
| 106 |
+
return '< 1m';
|
| 107 |
+
}
|
| 108 |
+
if (minutes < 60) {
|
| 109 |
+
return `${Math.floor(minutes)}m`;
|
| 110 |
+
}
|
| 111 |
+
const hours = Math.floor(minutes / 60);
|
| 112 |
+
const remainingMinutes = Math.floor(minutes % 60);
|
| 113 |
+
if (remainingMinutes === 0) {
|
| 114 |
+
return `${hours}h`;
|
| 115 |
+
}
|
| 116 |
+
return `${hours}h ${remainingMinutes}m`;
|
| 117 |
+
}
|
| 118 |
+
//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJuYW1lcyI6WyJSZWFjdCIsIkJveCIsIlRleHQiLCJmb3JtYXRUb2tlbnMiLCJTZWxlY3QiLCJEaWFsb2ciLCJJZGxlUmV0dXJuQWN0aW9uIiwiUHJvcHMiLCJpZGxlTWludXRlcyIsInRvdGFsSW5wdXRUb2tlbnMiLCJvbkRvbmUiLCJhY3Rpb24iLCJJZGxlUmV0dXJuRGlhbG9nIiwidDAiLCIkIiwiX2MiLCJ0MSIsImZvcm1hdElkbGVEdXJhdGlvbiIsImZvcm1hdHRlZElkbGUiLCJ0MiIsImZvcm1hdHRlZFRva2VucyIsInQzIiwidDQiLCJ0NSIsIlN5bWJvbCIsImZvciIsInQ2IiwidmFsdWUiLCJjb25zdCIsImxhYmVsIiwidDciLCJ0OCIsInQ5IiwidDEwIiwibWludXRlcyIsIk1hdGgiLCJmbG9vciIsImhvdXJzIiwicmVtYWluaW5nTWludXRlcyJdLCJzb3VyY2VzIjpbIklkbGVSZXR1cm5EaWFsb2cudHN4Il0sInNvdXJjZXNDb250ZW50IjpbImltcG9ydCBSZWFjdCBmcm9tICdyZWFjdCdcbmltcG9ydCB7IEJveCwgVGV4dCB9IGZyb20gJy4uL2luay5qcydcbmltcG9ydCB7IGZvcm1hdFRva2VucyB9IGZyb20gJy4uL3V0aWxzL2Zvcm1hdC5qcydcbmltcG9ydCB7IFNlbGVjdCB9IGZyb20gJy4vQ3VzdG9tU2VsZWN0L2luZGV4LmpzJ1xuaW1wb3J0IHsgRGlhbG9nIH0gZnJvbSAnLi9kZXNpZ24tc3lzdGVtL0RpYWxvZy5qcydcblxudHlwZSBJZGxlUmV0dXJuQWN0aW9uID0gJ2NvbnRpbnVlJyB8ICdjbGVhcicgfCAnZGlzbWlzcycgfCAnbmV2ZXInXG5cbnR5cGUgUHJvcHMgPSB7XG4gIGlkbGVNaW51dGVzOiBudW1iZXJcbiAgdG90YWxJbnB1dFRva2VuczogbnVtYmVyXG4gIG9uRG9uZTogKGFjdGlvbjogSWRsZVJldHVybkFjdGlvbikgPT4gdm9pZFxufVxuXG5leHBvcnQgZnVuY3Rpb24gSWRsZVJldHVybkRpYWxvZyh7XG4gIGlkbGVNaW51dGVzLFxuICB0b3RhbElucHV0VG9rZW5zLFxuICBvbkRvbmUsXG59OiBQcm9wcyk6IFJlYWN0LlJlYWN0Tm9kZSB7XG4gIGNvbnN0IGZvcm1hdHRlZElkbGUgPSBmb3JtYXRJZGxlRHVyYXRpb24oaWRsZU1pbnV0ZXMpXG4gIGNvbnN0IGZvcm1hdHRlZFRva2VucyA9IGZvcm1hdFRva2Vucyh0b3RhbElucHV0VG9rZW5zKVxuXG4gIHJldHVybiAoXG4gICAgPERpYWxvZ1xuICAgICAgdGl0bGU9e2BZb3UndmUgYmVlbiBhd2F5ICR7Zm9ybWF0dGVkSWRsZX0gYW5kIHRoaXMgY29udmVyc2F0aW9uIGlzICR7Zm9ybWF0dGVkVG9rZW5zfSB0b2tlbnMuYH1cbiAgICAgIG9uQ2FuY2VsPXsoKSA9PiBvbkRvbmUoJ2Rpc21pc3MnKX1cbiAgICA+XG4gICAgICA8Qm94IGZsZXhEaXJlY3Rpb249XCJjb2x1bW5cIj5cbiAgICAgICAgPFRleHQ+XG4gICAgICAgICAgSWYgdGhpcyBpcyBhIG5ldyB0YXNrLCBjbGVhcmluZyBjb250ZXh0IHdpbGwgc2F2ZSB1c2FnZSBhbmQgYmUgZmFzdGVyLlxuICAgICAgICA8L1RleHQ+XG4gICAgICA8L0JveD5cbiAgICAgIDxTZWxlY3RcbiAgICAgICAgb3B0aW9ucz17W1xuICAgICAgICAgIHtcbiAgICAgICAgICAgIHZhbHVlOiAnY29udGludWUnIGFzIGNvbnN0LFxuICAgICAgICAgICAgbGFiZWw6ICdDb250aW51ZSB0aGlzIGNvbnZlcnNhdGlvbicsXG4gICAgICAgICAgfSxcbiAgICAgICAgICB7XG4gICAgICAgICAgICB2YWx1ZTogJ2NsZWFyJyBhcyBjb25zdCxcbiAgICAgICAgICAgIGxhYmVsOiAnU2VuZCBtZXNzYWdlIGFzIGEgbmV3IGNvbnZlcnNhdGlvbicsXG4gICAgICAgICAgfSxcbiAgICAgICAgICB7XG4gICAgICAgICAgICB2YWx1ZTogJ25ldmVyJyBhcyBjb25zdCxcbiAgICAgICAgICAgIGxhYmVsOiBcIkRvbid0IGFzayBtZSBhZ2FpblwiLFxuICAgICAgICAgIH0sXG4gICAgICAgIF19XG4gICAgICAgIG9uQ2hhbmdlPXsodmFsdWU6IElkbGVSZXR1cm5BY3Rpb24pID0+IG9uRG9uZSh2YWx1ZSl9XG4gICAgICAvPlxuICAgIDwvRGlhbG9nPlxuICApXG59XG5cbmZ1bmN0aW9uIGZvcm1hdElkbGVEdXJhdGlvbihtaW51dGVzOiBudW1iZXIpOiBzdHJpbmcge1xuICBpZiAobWludXRlcyA8IDEpIHtcbiAgICByZXR1cm4gJzwgMW0nXG4gIH1cbiAgaWYgKG1pbnV0ZXMgPCA2MCkge1xuICAgIHJldHVybiBgJHtNYXRoLmZsb29yKG1pbnV0ZXMpfW1gXG4gIH1cbiAgY29uc3QgaG91cnMgPSBNYXRoLmZsb29yKG1pbnV0ZXMgLyA2MClcbiAgY29uc3QgcmVtYWluaW5nTWludXRlcyA9IE1hdGguZmxvb3IobWludXRlcyAlIDYwKVxuICBpZiAocmVtYWluaW5nTWludXRlcyA9PT0gMCkge1xuICAgIHJldHVybiBgJHtob3Vyc31oYFxuICB9XG4gIHJldHVybiBgJHtob3Vyc31oICR7cmVtYWluaW5nTWludXRlc31tYFxufVxuIl0sIm1hcHBpbmdzIjoiO0FBQUEsT0FBT0EsS0FBSyxNQUFNLE9BQU87QUFDekIsU0FBU0MsR0FBRyxFQUFFQyxJQUFJLFFBQVEsV0FBVztBQUNyQyxTQUFTQyxZQUFZLFFBQVEsb0JBQW9CO0FBQ2pELFNBQVNDLE1BQU0sUUFBUSx5QkFBeUI7QUFDaEQsU0FBU0MsTUFBTSxRQUFRLDJCQUEyQjtBQUVsRCxLQUFLQyxnQkFBZ0IsR0FBRyxVQUFVLEdBQUcsT0FBTyxHQUFHLFNBQVMsR0FBRyxPQUFPO0FBRWxFLEtBQUtDLEtBQUssR0FBRztFQUNYQyxXQUFXLEVBQUUsTUFBTTtFQUNuQkMsZ0JBQWdCLEVBQUUsTUFBTTtFQUN4QkMsTUFBTSxFQUFFLENBQUNDLE1BQU0sRUFBRUwsZ0JBQWdCLEVBQUUsR0FBRyxJQUFJO0FBQzVDLENBQUM7QUFFRCxPQUFPLFNBQUFNLGlCQUFBQyxFQUFBO0VBQUEsTUFBQUMsQ0FBQSxHQUFBQyxF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
|
src/components/InterruptedByUser.tsx
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { c as _c } from "react/compiler-runtime";
|
| 2 |
+
import * as React from 'react';
|
| 3 |
+
import { Text } from '../ink.js';
|
| 4 |
+
export function InterruptedByUser() {
|
| 5 |
+
const $ = _c(1);
|
| 6 |
+
let t0;
|
| 7 |
+
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
|
| 8 |
+
t0 = <><Text dimColor={true}>Interrupted </Text>{false ? <Text dimColor={true}>· [ANT-ONLY] /issue to report a model issue</Text> : <Text dimColor={true}>· What should Claude do instead?</Text>}</>;
|
| 9 |
+
$[0] = t0;
|
| 10 |
+
} else {
|
| 11 |
+
t0 = $[0];
|
| 12 |
+
}
|
| 13 |
+
return t0;
|
| 14 |
+
}
|
| 15 |
+
//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJuYW1lcyI6WyJSZWFjdCIsIlRleHQiLCJJbnRlcnJ1cHRlZEJ5VXNlciIsIiQiLCJfYyIsInQwIiwiU3ltYm9sIiwiZm9yIl0sInNvdXJjZXMiOlsiSW50ZXJydXB0ZWRCeVVzZXIudHN4Il0sInNvdXJjZXNDb250ZW50IjpbImltcG9ydCAqIGFzIFJlYWN0IGZyb20gJ3JlYWN0J1xuaW1wb3J0IHsgVGV4dCB9IGZyb20gJy4uL2luay5qcydcblxuZXhwb3J0IGZ1bmN0aW9uIEludGVycnVwdGVkQnlVc2VyKCk6IFJlYWN0LlJlYWN0Tm9kZSB7XG4gIHJldHVybiAoXG4gICAgPD5cbiAgICAgIDxUZXh0IGRpbUNvbG9yPkludGVycnVwdGVkIDwvVGV4dD5cbiAgICAgIHtcImV4dGVybmFsXCIgPT09ICdhbnQnID8gKFxuICAgICAgICA8VGV4dCBkaW1Db2xvcj7CtyBbQU5ULU9OTFldIC9pc3N1ZSB0byByZXBvcnQgYSBtb2RlbCBpc3N1ZTwvVGV4dD5cbiAgICAgICkgOiAoXG4gICAgICAgIDxUZXh0IGRpbUNvbG9yPsK3IFdoYXQgc2hvdWxkIENsYXVkZSBkbyBpbnN0ZWFkPzwvVGV4dD5cbiAgICAgICl9XG4gICAgPC8+XG4gIClcbn1cbiJdLCJtYXBwaW5ncyI6IjtBQUFBLE9BQU8sS0FBS0EsS0FBSyxNQUFNLE9BQU87QUFDOUIsU0FBU0MsSUFBSSxRQUFRLFdBQVc7QUFFaEMsT0FBTyxTQUFBQyxrQkFBQTtFQUFBLE1BQUFDLENBQUEsR0FBQUMsRUFBQTtFQUFBLElBQUFDLEVBQUE7RUFBQSxJQUFBRixDQUFBLFFBQUFHLE1BQUEsQ0FBQUMsR0FBQTtJQUVIRixFQUFBLEtBQ0UsQ0FBQyxJQUFJLENBQUMsUUFBUSxDQUFSLEtBQU8sQ0FBQyxDQUFDLFlBQVksRUFBMUIsSUFBSSxDQUNKLE1BQW9CLEdBQ25CLENBQUMsSUFBSSxDQUFDLFFBQVEsQ0FBUixLQUFPLENBQUMsQ0FBQywyQ0FBMkMsRUFBekQsSUFBSSxDQUdOLEdBREMsQ0FBQyxJQUFJLENBQUMsUUFBUSxDQUFSLEtBQU8sQ0FBQyxDQUFDLGdDQUFnQyxFQUE5QyxJQUFJLENBQ1AsQ0FBQyxHQUNBO0lBQUFGLENBQUEsTUFBQUUsRUFBQTtFQUFBO0lBQUFBLEVBQUEsR0FBQUYsQ0FBQTtFQUFBO0VBQUEsT0FQSEUsRUFPRztBQUFBIiwiaWdub3JlTGlzdCI6W119
|
src/components/InvalidConfigDialog.tsx
ADDED
|
@@ -0,0 +1,156 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { c as _c } from "react/compiler-runtime";
|
| 2 |
+
import React from 'react';
|
| 3 |
+
import { Box, render, Text } from '../ink.js';
|
| 4 |
+
import { KeybindingSetup } from '../keybindings/KeybindingProviderSetup.js';
|
| 5 |
+
import { AppStateProvider } from '../state/AppState.js';
|
| 6 |
+
import type { ConfigParseError } from '../utils/errors.js';
|
| 7 |
+
import { getBaseRenderOptions } from '../utils/renderOptions.js';
|
| 8 |
+
import { jsonStringify, writeFileSync_DEPRECATED } from '../utils/slowOperations.js';
|
| 9 |
+
import type { ThemeName } from '../utils/theme.js';
|
| 10 |
+
import { Select } from './CustomSelect/index.js';
|
| 11 |
+
import { Dialog } from './design-system/Dialog.js';
|
| 12 |
+
interface InvalidConfigHandlerProps {
|
| 13 |
+
error: ConfigParseError;
|
| 14 |
+
}
|
| 15 |
+
interface InvalidConfigDialogProps {
|
| 16 |
+
filePath: string;
|
| 17 |
+
errorDescription: string;
|
| 18 |
+
onExit: () => void;
|
| 19 |
+
onReset: () => void;
|
| 20 |
+
}
|
| 21 |
+
|
| 22 |
+
/**
|
| 23 |
+
* Dialog shown when the Claude config file contains invalid JSON
|
| 24 |
+
*/
|
| 25 |
+
function InvalidConfigDialog(t0) {
|
| 26 |
+
const $ = _c(19);
|
| 27 |
+
const {
|
| 28 |
+
filePath,
|
| 29 |
+
errorDescription,
|
| 30 |
+
onExit,
|
| 31 |
+
onReset
|
| 32 |
+
} = t0;
|
| 33 |
+
let t1;
|
| 34 |
+
if ($[0] !== onExit || $[1] !== onReset) {
|
| 35 |
+
t1 = value => {
|
| 36 |
+
if (value === "exit") {
|
| 37 |
+
onExit();
|
| 38 |
+
} else {
|
| 39 |
+
onReset();
|
| 40 |
+
}
|
| 41 |
+
};
|
| 42 |
+
$[0] = onExit;
|
| 43 |
+
$[1] = onReset;
|
| 44 |
+
$[2] = t1;
|
| 45 |
+
} else {
|
| 46 |
+
t1 = $[2];
|
| 47 |
+
}
|
| 48 |
+
const handleSelect = t1;
|
| 49 |
+
let t2;
|
| 50 |
+
if ($[3] !== filePath) {
|
| 51 |
+
t2 = <Text>The configuration file at <Text bold={true}>{filePath}</Text> contains invalid JSON.</Text>;
|
| 52 |
+
$[3] = filePath;
|
| 53 |
+
$[4] = t2;
|
| 54 |
+
} else {
|
| 55 |
+
t2 = $[4];
|
| 56 |
+
}
|
| 57 |
+
let t3;
|
| 58 |
+
if ($[5] !== errorDescription) {
|
| 59 |
+
t3 = <Text>{errorDescription}</Text>;
|
| 60 |
+
$[5] = errorDescription;
|
| 61 |
+
$[6] = t3;
|
| 62 |
+
} else {
|
| 63 |
+
t3 = $[6];
|
| 64 |
+
}
|
| 65 |
+
let t4;
|
| 66 |
+
if ($[7] !== t2 || $[8] !== t3) {
|
| 67 |
+
t4 = <Box flexDirection="column" gap={1}>{t2}{t3}</Box>;
|
| 68 |
+
$[7] = t2;
|
| 69 |
+
$[8] = t3;
|
| 70 |
+
$[9] = t4;
|
| 71 |
+
} else {
|
| 72 |
+
t4 = $[9];
|
| 73 |
+
}
|
| 74 |
+
let t5;
|
| 75 |
+
if ($[10] === Symbol.for("react.memo_cache_sentinel")) {
|
| 76 |
+
t5 = <Text bold={true}>Choose an option:</Text>;
|
| 77 |
+
$[10] = t5;
|
| 78 |
+
} else {
|
| 79 |
+
t5 = $[10];
|
| 80 |
+
}
|
| 81 |
+
let t6;
|
| 82 |
+
if ($[11] === Symbol.for("react.memo_cache_sentinel")) {
|
| 83 |
+
t6 = [{
|
| 84 |
+
label: "Exit and fix manually",
|
| 85 |
+
value: "exit"
|
| 86 |
+
}, {
|
| 87 |
+
label: "Reset with default configuration",
|
| 88 |
+
value: "reset"
|
| 89 |
+
}];
|
| 90 |
+
$[11] = t6;
|
| 91 |
+
} else {
|
| 92 |
+
t6 = $[11];
|
| 93 |
+
}
|
| 94 |
+
let t7;
|
| 95 |
+
if ($[12] !== handleSelect || $[13] !== onExit) {
|
| 96 |
+
t7 = <Box flexDirection="column">{t5}<Select options={t6} onChange={handleSelect} onCancel={onExit} /></Box>;
|
| 97 |
+
$[12] = handleSelect;
|
| 98 |
+
$[13] = onExit;
|
| 99 |
+
$[14] = t7;
|
| 100 |
+
} else {
|
| 101 |
+
t7 = $[14];
|
| 102 |
+
}
|
| 103 |
+
let t8;
|
| 104 |
+
if ($[15] !== onExit || $[16] !== t4 || $[17] !== t7) {
|
| 105 |
+
t8 = <Dialog title="Configuration Error" color="error" onCancel={onExit}>{t4}{t7}</Dialog>;
|
| 106 |
+
$[15] = onExit;
|
| 107 |
+
$[16] = t4;
|
| 108 |
+
$[17] = t7;
|
| 109 |
+
$[18] = t8;
|
| 110 |
+
} else {
|
| 111 |
+
t8 = $[18];
|
| 112 |
+
}
|
| 113 |
+
return t8;
|
| 114 |
+
}
|
| 115 |
+
|
| 116 |
+
/**
|
| 117 |
+
* Safe fallback theme name for error dialogs to avoid circular dependency.
|
| 118 |
+
* Uses a hardcoded dark theme that doesn't require reading from config.
|
| 119 |
+
*/
|
| 120 |
+
const SAFE_ERROR_THEME_NAME: ThemeName = 'dark';
|
| 121 |
+
export async function showInvalidConfigDialog({
|
| 122 |
+
error
|
| 123 |
+
}: InvalidConfigHandlerProps): Promise<void> {
|
| 124 |
+
// Extend RenderOptions with theme property for this specific usage
|
| 125 |
+
type SafeRenderOptions = Parameters<typeof render>[1] & {
|
| 126 |
+
theme?: ThemeName;
|
| 127 |
+
};
|
| 128 |
+
const renderOptions: SafeRenderOptions = {
|
| 129 |
+
...getBaseRenderOptions(false),
|
| 130 |
+
// IMPORTANT: Use hardcoded theme name to avoid circular dependency with getGlobalConfig()
|
| 131 |
+
// This allows the error dialog to show even when config file has JSON syntax errors
|
| 132 |
+
theme: SAFE_ERROR_THEME_NAME
|
| 133 |
+
};
|
| 134 |
+
await new Promise<void>(async resolve => {
|
| 135 |
+
const {
|
| 136 |
+
unmount
|
| 137 |
+
} = await render(<AppStateProvider>
|
| 138 |
+
<KeybindingSetup>
|
| 139 |
+
<InvalidConfigDialog filePath={error.filePath} errorDescription={error.message} onExit={() => {
|
| 140 |
+
unmount();
|
| 141 |
+
void resolve();
|
| 142 |
+
process.exit(1);
|
| 143 |
+
}} onReset={() => {
|
| 144 |
+
writeFileSync_DEPRECATED(error.filePath, jsonStringify(error.defaultConfig, null, 2), {
|
| 145 |
+
flush: false,
|
| 146 |
+
encoding: 'utf8'
|
| 147 |
+
});
|
| 148 |
+
unmount();
|
| 149 |
+
void resolve();
|
| 150 |
+
process.exit(0);
|
| 151 |
+
}} />
|
| 152 |
+
</KeybindingSetup>
|
| 153 |
+
</AppStateProvider>, renderOptions);
|
| 154 |
+
});
|
| 155 |
+
}
|
| 156 |
+
//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJuYW1lcyI6WyJSZWFjdCIsIkJveCIsInJlbmRlciIsIlRleHQiLCJLZXliaW5kaW5nU2V0dXAiLCJBcHBTdGF0ZVByb3ZpZGVyIiwiQ29uZmlnUGFyc2VFcnJvciIsImdldEJhc2VSZW5kZXJPcHRpb25zIiwianNvblN0cmluZ2lmeSIsIndyaXRlRmlsZVN5bmNfREVQUkVDQVRFRCIsIlRoZW1lTmFtZSIsIlNlbGVjdCIsIkRpYWxvZyIsIkludmFsaWRDb25maWdIYW5kbGVyUHJvcHMiLCJlcnJvciIsIkludmFsaWRDb25maWdEaWFsb2dQcm9wcyIsImZpbGVQYXRoIiwiZXJyb3JEZXNjcmlwdGlvbiIsIm9uRXhpdCIsIm9uUmVzZXQiLCJJbnZhbGlkQ29uZmlnRGlhbG9nIiwidDAiLCIkIiwiX2MiLCJ0MSIsInZhbHVlIiwiaGFuZGxlU2VsZWN0IiwidDIiLCJ0MyIsInQ0IiwidDUiLCJTeW1ib2wiLCJmb3IiLCJ0NiIsImxhYmVsIiwidDciLCJ0OCIsIlNBRkVfRVJST1JfVEhFTUVfTkFNRSIsInNob3dJbnZhbGlkQ29uZmlnRGlhbG9nIiwiUHJvbWlzZSIsIlNhZmVSZW5kZXJPcHRpb25zIiwiUGFyYW1ldGVycyIsInRoZW1lIiwicmVuZGVyT3B0aW9ucyIsInJlc29sdmUiLCJ1bm1vdW50IiwibWVzc2FnZSIsInByb2Nlc3MiLCJleGl0IiwiZGVmYXVsdENvbmZpZyIsImZsdXNoIiwiZW5jb2RpbmciXSwic291cmNlcyI6WyJJbnZhbGlkQ29uZmlnRGlhbG9nLnRzeCJdLCJzb3VyY2VzQ29udGVudCI6WyJpbXBvcnQgUmVhY3QgZnJvbSAncmVhY3QnXG5pbXBvcnQgeyBCb3gsIHJlbmRlciwgVGV4dCB9IGZyb20gJy4uL2luay5qcydcbmltcG9ydCB7IEtleWJpbmRpbmdTZXR1cCB9IGZyb20gJy4uL2tleWJpbmRpbmdzL0tleWJpbmRpbmdQcm92aWRlclNldHVwLmpzJ1xuaW1wb3J0IHsgQXBwU3RhdGVQcm92aWRlciB9IGZyb20gJy4uL3N0YXRlL0FwcFN0YXRlLmpzJ1xuaW1wb3J0IHR5cGUgeyBDb25maWdQYXJzZUVycm9yIH0gZnJvbSAnLi4vdXRpbHMvZXJyb3JzLmpzJ1xuaW1wb3J0IHsgZ2V0QmFzZVJlbmRlck9wdGlvbnMgfSBmcm9tICcuLi91dGlscy9yZW5kZXJPcHRpb25zLmpzJ1xuaW1wb3J0IHtcbiAganNvblN0cmluZ2lmeSxcbiAgd3JpdGVGaWxlU3luY19ERVBSRUNBVEVELFxufSBmcm9tICcuLi91dGlscy9zbG93T3BlcmF0aW9ucy5qcydcbmltcG9ydCB0eXBlIHsgVGhlbWVOYW1lIH0gZnJvbSAnLi4vdXRpbHMvdGhlbWUuanMnXG5pbXBvcnQgeyBTZWxlY3QgfSBmcm9tICcuL0N1c3RvbVNlbGVjdC9pbmRleC5qcydcbmltcG9ydCB7IERpYWxvZyB9IGZyb20gJy4vZGVzaWduLXN5c3RlbS9EaWFsb2cuanMnXG5cbmludGVyZmFjZSBJbnZhbGlkQ29uZmlnSGFuZGxlclByb3BzIHtcbiAgZXJyb3I6IENvbmZpZ1BhcnNlRXJyb3Jcbn1cblxuaW50ZXJmYWNlIEludmFsaWRDb25maWdEaWFsb2dQcm9wcyB7XG4gIGZpbGVQYXRoOiBzdHJpbmdcbiAgZXJyb3JEZXNjcmlwdGlvbjogc3RyaW5nXG4gIG9uRXhpdDogKCkgPT4gdm9pZFxuICBvblJlc2V0OiAoKSA9PiB2b2lkXG59XG5cbi8qKlxuICogRGlhbG9nIHNob3duIHdoZW4gdGhlIENsYXVkZSBjb25maWcgZmlsZSBjb250YWlucyBpbnZhbGlkIEpTT05cbiAqL1xuZnVuY3Rpb24gSW52YWxpZENvbmZpZ0RpYWxvZyh7XG4gIGZpbGVQYXRoLFxuICBlcnJvckRlc2NyaXB0aW9uLFxuICBvbkV4aXQsXG4gIG9uUmVzZXQsXG59OiBJbnZhbGlkQ29uZmlnRGlhbG9nUHJvcHMpOiBSZWFjdC5SZWFjdE5vZGUge1xuICAvLyBIYW5kbGVyIGZvciBTZWxlY3Qgb25DaGFuZ2VcbiAgY29uc3QgaGFuZGxlU2VsZWN0ID0gKHZhbHVlOiBzdHJpbmcpID0+IHtcbiAgICBpZiAodmFsdWUgPT09ICdleGl0Jykge1xuICAgICAgb25FeGl0KClcbiAgICB9IGVsc2Uge1xuICAgICAgb25SZXNldCgpXG4gICAgfVxuICB9XG5cbiAgcmV0dXJuIChcbiAgICA8RGlhbG9nIHRpdGxlPVwiQ29uZmlndXJhdGlvbiBFcnJvclwiIGNvbG9yPVwiZXJyb3JcIiBvbkNhbmNlbD17b25FeGl0fT5cbiAgICAgIDxCb3ggZmxleERpcmVjdGlvbj1cImNvbHVtblwiIGdhcD17MX0+XG4gICAgICAgIDxUZXh0PlxuICAgICAgICAgIFRoZSBjb25maWd1cmF0aW9uIGZpbGUgYXQgPFRleHQgYm9sZD57ZmlsZVBhdGh9PC9UZXh0PiBjb250YWluc1xuICAgICAgICAgIGludmFsaWQgSlNPTi5cbiAgICAgICAgPC9UZXh0PlxuICAgICAgICA8VGV4dD57ZXJyb3JEZXNjcmlwdGlvbn08L1RleHQ+XG4gICAgICA8L0JveD5cbiAgICAgIDxCb3ggZmxleERpcmVjdGlvbj1cImNvbHVtblwiPlxuICAgICAgICA8VGV4dCBib2xkPkNob29zZSBhbiBvcHRpb246PC9UZXh0PlxuICAgICAgICA8U2VsZWN0XG4gICAgICAgICAgb3B0aW9ucz17W1xuICAgICAgICAgICAgeyBsYWJlbDogJ0V4aXQgYW5kIGZpeCBtYW51YWxseScsIHZhbHVlOiAnZXhpdCcgfSxcbiAgICAgICAgICAgIHsgbGFiZWw6ICdSZXNldCB3aXRoIGRlZmF1bHQgY29uZmlndXJhdGlvbicsIHZhbHVlOiAncmVzZXQnIH0sXG4gICAgICAgICAgXX1cbiAgICAgICAgICBvbkNoYW5nZT17aGFuZGxlU2VsZWN0fVxuICAgICAgICAgIG9uQ2FuY2VsPXtvbkV4aXR9XG4gICAgICAgIC8+XG4gICAgICA8L0JveD5cbiAgICA8L0RpYWxvZz5cbiAgKVxufVxuXG4vKipcbiAqIFNhZmUgZmFsbGJhY2sgdGhlbWUgbmFtZSBmb3IgZXJyb3IgZGlhbG9ncyB0byBhdm9pZCBjaXJjdWxhciBkZXBlbmRlbmN5LlxuICogVXNlcyBhIGhhcmRjb2RlZCBkYXJrIHRoZW1lIHRoYXQgZG9lc24ndCByZXF1aXJlIHJlYWRpbmcgZnJvbSBjb25maWcuXG4gKi9cbmNvbnN0IFNBRkVfRVJST1JfVEhFTUVfTkFNRTogVGhlbWVOYW1lID0gJ2RhcmsnXG5cbmV4cG9ydCBhc3luYyBmdW5jdGlvbiBzaG93SW52YWxpZENvbmZpZ0RpYWxvZyh7XG4gIGVycm9yLFxufTogSW52YWxpZENvbmZpZ0hhbmRsZXJQcm9wcyk6IFByb21pc2U8dm9pZD4ge1xuICAvLyBFeHRlbmQgUmVuZGVyT3B0aW9ucyB3aXRoIHRoZW1lIHByb3BlcnR5IGZvciB0aGlzIHNwZWNpZmljIHVzYWdlXG4gIHR5cGUgU2FmZVJlbmRlck9wdGlvbnMgPSBQYXJhbWV0ZXJzPHR5cGVvZiByZW5kZXI+WzFdICYgeyB0aGVtZT86IFRoZW1lTmFtZSB9XG5cbiAgY29uc3QgcmVuZGVyT3B0aW9uczogU2FmZVJlbmRlck9wdGlvbnMgPSB7XG4gICAgLi4uZ2V0QmFzZVJlbmRlck9wdGlvbnMoZmFsc2UpLFxuICAgIC8vIElNUE9SVEFOVDogVXNlIGhhcmRjb2RlZCB0aGVtZSBuYW1lIHRvIGF2b2lkIGNpcmN1bGFyIGRlcGVuZGVuY3kgd2l0aCBnZXRHbG9iYWxDb25maWcoKVxuICAgIC8vIFRoaXMgYWxsb3dzIHRoZSBlcnJvciBkaWFsb2cgdG8gc2hvdyBldmVuIHdoZW4gY29uZmlnIGZpbGUgaGFzIEpTT04gc3ludGF4IGVycm9yc1xuICAgIHRoZW1lOiBTQUZFX0VSUk9SX1RIRU1FX05BTUUsXG4gIH1cblxuICBhd2FpdCBuZXcgUHJvbWlzZTx2b2lkPihhc3luYyByZXNvbHZlID0+IHtcbiAgICBjb25zdCB7IHVubW91bnQgfSA9IGF3YWl0IHJlbmRlcihcbiAgICAgIDxBcHBTdGF0ZVByb3ZpZGVyPlxuICAgICAgICA8S2V5YmluZGluZ1NldHVwPlxuICAgICAgICAgIDxJbnZhbGlkQ29uZmlnRGlhbG9nXG4gICAgICAgICAgICBmaWxlUGF0aD17ZXJyb3IuZmlsZVBhdGh9XG4gICAgICAgICAgICBlcnJvckRlc2NyaXB0aW9uPXtlcnJvci5tZXNzYWdlfVxuICAgICAgICAgICAgb25FeGl0PXsoKSA9PiB7XG4gICAgICAgICAgICAgIHVubW91bnQoKVxuICAgICAgICAgICAgICB2b2lkIHJlc29sdmUoKVxuICAgICAgICAgICAgICBwcm9jZXNzLmV4aXQoMSlcbiAgICAgICAgICAgIH19XG4gICAgICAgICAgICBvblJlc2V0PXsoKSA9PiB7XG4gICAgICAgICAgICAgIHdyaXRlRmlsZVN5bmNfREVQUkVDQVRFRChcbiAgICAgICAgICAgICAgICBlcnJvci5maWxlUGF0aCxcbiAgICAgICAgICAgICAgICBqc29uU3RyaW5naWZ5KGVycm9yLmRlZmF1bHRDb25maWcsIG51bGwsIDIpLFxuICAgICAgICAgICAgICAgIHsgZmx1c2g6IGZhbHNlLCBlbmNvZGluZzogJ3V0ZjgnIH0sXG4gICAgICAgICAgICAgIClcbiAgICAgICAgICAgICAgdW5tb3VudCgpXG4gICAgICAgICAgICAgIHZvaWQgcmVzb2x2ZSgpXG4gICAgICAgICAgICAgIHByb2Nlc3MuZXhpdCgwKVxuICAgICAgICAgICAgfX1cbiAgICAgICAgICAvPlxuICAgICAgICA8L0tleWJpbmRpbmdTZXR1cD5cbiAgICAgIDwvQXBwU3RhdGVQcm92aWRlcj4sXG4gICAgICByZW5kZXJPcHRpb25zLFxuICAgIClcbiAgfSlcbn1cbiJdLCJtYXBwaW5ncyI6IjtBQUFBLE9BQU9BLEtBQUssTUFBTSxPQUFPO0FBQ3pCLFNBQVNDLEdBQUcsRUFBRUMsTUFBTSxFQUFFQyxJQUFJLFFBQVEsV0FBVztBQUM3QyxTQUFTQyxlQUFlLFFBQVEsMkNBQTJDO0FBQzNFLFNBQVNDLGdCQUFnQixRQUFRLHNCQUFzQjtBQUN2RCxjQUFjQyxnQkFBZ0IsUUFBUSxvQkFBb0I7QUFDMUQsU0FBU0Msb0JBQW9CLFFBQVEsMkJBQTJCO0FBQ2hFLFNBQ0VDLGFBQWEsRUFDYkMsd0JBQXdCLFFBQ25CLDRCQUE0QjtBQUNuQyxjQUFjQyxTQUFTLFFBQVEsbUJBQW1CO0FBQ2xELFNBQVNDLE1B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
|
src/components/InvalidSettingsDialog.tsx
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { c as _c } from "react/compiler-runtime";
|
| 2 |
+
import React from 'react';
|
| 3 |
+
import { Text } from '../ink.js';
|
| 4 |
+
import type { ValidationError } from '../utils/settings/validation.js';
|
| 5 |
+
import { Select } from './CustomSelect/index.js';
|
| 6 |
+
import { Dialog } from './design-system/Dialog.js';
|
| 7 |
+
import { ValidationErrorsList } from './ValidationErrorsList.js';
|
| 8 |
+
type Props = {
|
| 9 |
+
settingsErrors: ValidationError[];
|
| 10 |
+
onContinue: () => void;
|
| 11 |
+
onExit: () => void;
|
| 12 |
+
};
|
| 13 |
+
|
| 14 |
+
/**
|
| 15 |
+
* Dialog shown when settings files have validation errors.
|
| 16 |
+
* User must choose to continue (skipping invalid files) or exit to fix them.
|
| 17 |
+
*/
|
| 18 |
+
export function InvalidSettingsDialog(t0) {
|
| 19 |
+
const $ = _c(13);
|
| 20 |
+
const {
|
| 21 |
+
settingsErrors,
|
| 22 |
+
onContinue,
|
| 23 |
+
onExit
|
| 24 |
+
} = t0;
|
| 25 |
+
let t1;
|
| 26 |
+
if ($[0] !== onContinue || $[1] !== onExit) {
|
| 27 |
+
t1 = function handleSelect(value) {
|
| 28 |
+
if (value === "exit") {
|
| 29 |
+
onExit();
|
| 30 |
+
} else {
|
| 31 |
+
onContinue();
|
| 32 |
+
}
|
| 33 |
+
};
|
| 34 |
+
$[0] = onContinue;
|
| 35 |
+
$[1] = onExit;
|
| 36 |
+
$[2] = t1;
|
| 37 |
+
} else {
|
| 38 |
+
t1 = $[2];
|
| 39 |
+
}
|
| 40 |
+
const handleSelect = t1;
|
| 41 |
+
let t2;
|
| 42 |
+
if ($[3] !== settingsErrors) {
|
| 43 |
+
t2 = <ValidationErrorsList errors={settingsErrors} />;
|
| 44 |
+
$[3] = settingsErrors;
|
| 45 |
+
$[4] = t2;
|
| 46 |
+
} else {
|
| 47 |
+
t2 = $[4];
|
| 48 |
+
}
|
| 49 |
+
let t3;
|
| 50 |
+
if ($[5] === Symbol.for("react.memo_cache_sentinel")) {
|
| 51 |
+
t3 = <Text dimColor={true}>Files with errors are skipped entirely, not just the invalid settings.</Text>;
|
| 52 |
+
$[5] = t3;
|
| 53 |
+
} else {
|
| 54 |
+
t3 = $[5];
|
| 55 |
+
}
|
| 56 |
+
let t4;
|
| 57 |
+
if ($[6] === Symbol.for("react.memo_cache_sentinel")) {
|
| 58 |
+
t4 = [{
|
| 59 |
+
label: "Exit and fix manually",
|
| 60 |
+
value: "exit"
|
| 61 |
+
}, {
|
| 62 |
+
label: "Continue without these settings",
|
| 63 |
+
value: "continue"
|
| 64 |
+
}];
|
| 65 |
+
$[6] = t4;
|
| 66 |
+
} else {
|
| 67 |
+
t4 = $[6];
|
| 68 |
+
}
|
| 69 |
+
let t5;
|
| 70 |
+
if ($[7] !== handleSelect) {
|
| 71 |
+
t5 = <Select options={t4} onChange={handleSelect} />;
|
| 72 |
+
$[7] = handleSelect;
|
| 73 |
+
$[8] = t5;
|
| 74 |
+
} else {
|
| 75 |
+
t5 = $[8];
|
| 76 |
+
}
|
| 77 |
+
let t6;
|
| 78 |
+
if ($[9] !== onExit || $[10] !== t2 || $[11] !== t5) {
|
| 79 |
+
t6 = <Dialog title="Settings Error" onCancel={onExit} color="warning">{t2}{t3}{t5}</Dialog>;
|
| 80 |
+
$[9] = onExit;
|
| 81 |
+
$[10] = t2;
|
| 82 |
+
$[11] = t5;
|
| 83 |
+
$[12] = t6;
|
| 84 |
+
} else {
|
| 85 |
+
t6 = $[12];
|
| 86 |
+
}
|
| 87 |
+
return t6;
|
| 88 |
+
}
|
| 89 |
+
//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJuYW1lcyI6WyJSZWFjdCIsIlRleHQiLCJWYWxpZGF0aW9uRXJyb3IiLCJTZWxlY3QiLCJEaWFsb2ciLCJWYWxpZGF0aW9uRXJyb3JzTGlzdCIsIlByb3BzIiwic2V0dGluZ3NFcnJvcnMiLCJvbkNvbnRpbnVlIiwib25FeGl0IiwiSW52YWxpZFNldHRpbmdzRGlhbG9nIiwidDAiLCIkIiwiX2MiLCJ0MSIsImhhbmRsZVNlbGVjdCIsInZhbHVlIiwidDIiLCJ0MyIsIlN5bWJvbCIsImZvciIsInQ0IiwibGFiZWwiLCJ0NSIsInQ2Il0sInNvdXJjZXMiOlsiSW52YWxpZFNldHRpbmdzRGlhbG9nLnRzeCJdLCJzb3VyY2VzQ29udGVudCI6WyJpbXBvcnQgUmVhY3QgZnJvbSAncmVhY3QnXG5pbXBvcnQgeyBUZXh0IH0gZnJvbSAnLi4vaW5rLmpzJ1xuaW1wb3J0IHR5cGUgeyBWYWxpZGF0aW9uRXJyb3IgfSBmcm9tICcuLi91dGlscy9zZXR0aW5ncy92YWxpZGF0aW9uLmpzJ1xuaW1wb3J0IHsgU2VsZWN0IH0gZnJvbSAnLi9DdXN0b21TZWxlY3QvaW5kZXguanMnXG5pbXBvcnQgeyBEaWFsb2cgfSBmcm9tICcuL2Rlc2lnbi1zeXN0ZW0vRGlhbG9nLmpzJ1xuaW1wb3J0IHsgVmFsaWRhdGlvbkVycm9yc0xpc3QgfSBmcm9tICcuL1ZhbGlkYXRpb25FcnJvcnNMaXN0LmpzJ1xuXG50eXBlIFByb3BzID0ge1xuICBzZXR0aW5nc0Vycm9yczogVmFsaWRhdGlvbkVycm9yW11cbiAgb25Db250aW51ZTogKCkgPT4gdm9pZFxuICBvbkV4aXQ6ICgpID0+IHZvaWRcbn1cblxuLyoqXG4gKiBEaWFsb2cgc2hvd24gd2hlbiBzZXR0aW5ncyBmaWxlcyBoYXZlIHZhbGlkYXRpb24gZXJyb3JzLlxuICogVXNlciBtdXN0IGNob29zZSB0byBjb250aW51ZSAoc2tpcHBpbmcgaW52YWxpZCBmaWxlcykgb3IgZXhpdCB0byBmaXggdGhlbS5cbiAqL1xuZXhwb3J0IGZ1bmN0aW9uIEludmFsaWRTZXR0aW5nc0RpYWxvZyh7XG4gIHNldHRpbmdzRXJyb3JzLFxuICBvbkNvbnRpbnVlLFxuICBvbkV4aXQsXG59OiBQcm9wcyk6IFJlYWN0LlJlYWN0Tm9kZSB7XG4gIGZ1bmN0aW9uIGhhbmRsZVNlbGVjdCh2YWx1ZTogc3RyaW5nKTogdm9pZCB7XG4gICAgaWYgKHZhbHVlID09PSAnZXhpdCcpIHtcbiAgICAgIG9uRXhpdCgpXG4gICAgfSBlbHNlIHtcbiAgICAgIG9uQ29udGludWUoKVxuICAgIH1cbiAgfVxuXG4gIHJldHVybiAoXG4gICAgPERpYWxvZyB0aXRsZT1cIlNldHRpbmdzIEVycm9yXCIgb25DYW5jZWw9e29uRXhpdH0gY29sb3I9XCJ3YXJuaW5nXCI+XG4gICAgICA8VmFsaWRhdGlvbkVycm9yc0xpc3QgZXJyb3JzPXtzZXR0aW5nc0Vycm9yc30gLz5cbiAgICAgIDxUZXh0IGRpbUNvbG9yPlxuICAgICAgICBGaWxlcyB3aXRoIGVycm9ycyBhcmUgc2tpcHBlZCBlbnRpcmVseSwgbm90IGp1c3QgdGhlIGludmFsaWQgc2V0dGluZ3MuXG4gICAgICA8L1RleHQ+XG4gICAgICA8U2VsZWN0XG4gICAgICAgIG9wdGlvbnM9e1tcbiAgICAgICAgICB7IGxhYmVsOiAnRXhpdCBhbmQgZml4IG1hbnVhbGx5JywgdmFsdWU6ICdleGl0JyB9LFxuICAgICAgICAgIHtcbiAgICAgICAgICAgIGxhYmVsOiAnQ29udGludWUgd2l0aG91dCB0aGVzZSBzZXR0aW5ncycsXG4gICAgICAgICAgICB2YWx1ZTogJ2NvbnRpbnVlJyxcbiAgICAgICAgICB9LFxuICAgICAgICBdfVxuICAgICAgICBvbkNoYW5nZT17aGFuZGxlU2VsZWN0fVxuICAgICAgLz5cbiAgICA8L0RpYWxvZz5cbiAgKVxufVxuIl0sIm1hcHBpbmdzIjoiO0FBQUEsT0FBT0EsS0FBSyxNQUFNLE9BQU87QUFDekIsU0FBU0MsSUFBSSxRQUFRLFdBQVc7QUFDaEMsY0FBY0MsZUFBZSxRQUFRLGlDQUFpQztBQUN0RSxTQUFTQyxNQUFNLFFB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
|
src/components/KeybindingWarnings.tsx
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { c as _c } from "react/compiler-runtime";
|
| 2 |
+
import React from 'react';
|
| 3 |
+
import { Box, Text } from '../ink.js';
|
| 4 |
+
import { getCachedKeybindingWarnings, getKeybindingsPath, isKeybindingCustomizationEnabled } from '../keybindings/loadUserBindings.js';
|
| 5 |
+
|
| 6 |
+
/**
|
| 7 |
+
* Displays keybinding validation warnings in the UI.
|
| 8 |
+
* Similar to McpParsingWarnings, this provides persistent visibility
|
| 9 |
+
* of configuration issues.
|
| 10 |
+
*
|
| 11 |
+
* Only shown when keybinding customization is enabled (ant users + feature gate).
|
| 12 |
+
*/
|
| 13 |
+
export function KeybindingWarnings() {
|
| 14 |
+
const $ = _c(2);
|
| 15 |
+
if (!isKeybindingCustomizationEnabled()) {
|
| 16 |
+
return null;
|
| 17 |
+
}
|
| 18 |
+
let t0;
|
| 19 |
+
let t1;
|
| 20 |
+
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
|
| 21 |
+
t1 = Symbol.for("react.early_return_sentinel");
|
| 22 |
+
bb0: {
|
| 23 |
+
const warnings = getCachedKeybindingWarnings();
|
| 24 |
+
if (warnings.length === 0) {
|
| 25 |
+
t1 = null;
|
| 26 |
+
break bb0;
|
| 27 |
+
}
|
| 28 |
+
const errors = warnings.filter(_temp);
|
| 29 |
+
const warns = warnings.filter(_temp2);
|
| 30 |
+
t0 = <Box flexDirection="column" marginTop={1} marginBottom={1}><Text bold={true} color={errors.length > 0 ? "error" : "warning"}>Keybinding Configuration Issues</Text><Box><Text dimColor={true}>Location: </Text><Text dimColor={true}>{getKeybindingsPath()}</Text></Box><Box marginLeft={1} flexDirection="column" marginTop={1}>{errors.map(_temp3)}{warns.map(_temp4)}</Box></Box>;
|
| 31 |
+
}
|
| 32 |
+
$[0] = t0;
|
| 33 |
+
$[1] = t1;
|
| 34 |
+
} else {
|
| 35 |
+
t0 = $[0];
|
| 36 |
+
t1 = $[1];
|
| 37 |
+
}
|
| 38 |
+
if (t1 !== Symbol.for("react.early_return_sentinel")) {
|
| 39 |
+
return t1;
|
| 40 |
+
}
|
| 41 |
+
return t0;
|
| 42 |
+
}
|
| 43 |
+
function _temp4(warning, i_0) {
|
| 44 |
+
return <Box key={`warning-${i_0}`} flexDirection="column"><Box><Text dimColor={true}>└ </Text><Text color="warning">[Warning]</Text><Text dimColor={true}> {warning.message}</Text></Box>{warning.suggestion && <Box marginLeft={3}><Text dimColor={true}>→ {warning.suggestion}</Text></Box>}</Box>;
|
| 45 |
+
}
|
| 46 |
+
function _temp3(error, i) {
|
| 47 |
+
return <Box key={`error-${i}`} flexDirection="column"><Box><Text dimColor={true}>└ </Text><Text color="error">[Error]</Text><Text dimColor={true}> {error.message}</Text></Box>{error.suggestion && <Box marginLeft={3}><Text dimColor={true}>→ {error.suggestion}</Text></Box>}</Box>;
|
| 48 |
+
}
|
| 49 |
+
function _temp2(w_0) {
|
| 50 |
+
return w_0.severity === "warning";
|
| 51 |
+
}
|
| 52 |
+
function _temp(w) {
|
| 53 |
+
return w.severity === "error";
|
| 54 |
+
}
|
| 55 |
+
//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJuYW1lcyI6WyJSZWFjdCIsIkJveCIsIlRleHQiLCJnZXRDYWNoZWRLZXliaW5kaW5nV2FybmluZ3MiLCJnZXRLZXliaW5kaW5nc1BhdGgiLCJpc0tleWJpbmRpbmdDdXN0b21pemF0aW9uRW5hYmxlZCIsIktleWJpbmRpbmdXYXJuaW5ncyIsIiQiLCJfYyIsInQwIiwidDEiLCJTeW1ib2wiLCJmb3IiLCJiYjAiLCJ3YXJuaW5ncyIsImxlbmd0aCIsImVycm9ycyIsImZpbHRlciIsIl90ZW1wIiwid2FybnMiLCJfdGVtcDIiLCJtYXAiLCJfdGVtcDMiLCJfdGVtcDQiLCJ3YXJuaW5nIiwiaV8wIiwiaSIsIm1lc3NhZ2UiLCJzdWdnZXN0aW9uIiwiZXJyb3IiLCJ3XzAiLCJ3Iiwic2V2ZXJpdHkiXSwic291cmNlcyI6WyJLZXliaW5kaW5nV2FybmluZ3MudHN4Il0sInNvdXJjZXNDb250ZW50IjpbImltcG9ydCBSZWFjdCBmcm9tICdyZWFjdCdcbmltcG9ydCB7IEJveCwgVGV4dCB9IGZyb20gJy4uL2luay5qcydcbmltcG9ydCB7XG4gIGdldENhY2hlZEtleWJpbmRpbmdXYXJuaW5ncyxcbiAgZ2V0S2V5YmluZGluZ3NQYXRoLFxuICBpc0tleWJpbmRpbmdDdXN0b21pemF0aW9uRW5hYmxlZCxcbn0gZnJvbSAnLi4va2V5YmluZGluZ3MvbG9hZFVzZXJCaW5kaW5ncy5qcydcblxuLyoqXG4gKiBEaXNwbGF5cyBrZXliaW5kaW5nIHZhbGlkYXRpb24gd2FybmluZ3MgaW4gdGhlIFVJLlxuICogU2ltaWxhciB0byBNY3BQYXJzaW5nV2FybmluZ3MsIHRoaXMgcHJvdmlkZXMgcGVyc2lzdGVudCB2aXNpYmlsaXR5XG4gKiBvZiBjb25maWd1cmF0aW9uIGlzc3Vlcy5cbiAqXG4gKiBPbmx5IHNob3duIHdoZW4ga2V5YmluZGluZyBjdXN0b21pemF0aW9uIGlzIGVuYWJsZWQgKGFudCB1c2VycyArIGZlYXR1cmUgZ2F0ZSkuXG4gKi9cbmV4cG9ydCBmdW5jdGlvbiBLZXliaW5kaW5nV2FybmluZ3MoKTogUmVhY3QuUmVhY3ROb2RlIHtcbiAgLy8gT25seSBzaG93IHdhcm5pbmdzIHdoZW4ga2V5YmluZGluZyBjdXN0b21pemF0aW9uIGlzIGVuYWJsZWRcbiAgaWYgKCFpc0tleWJpbmRpbmdDdXN0b21pemF0aW9uRW5hYmxlZCgpKSB7XG4gICAgcmV0dXJuIG51bGxcbiAgfVxuXG4gIGNvbnN0IHdhcm5pbmdzID0gZ2V0Q2FjaGVkS2V5YmluZGluZ1dhcm5pbmdzKClcblxuICBpZiAod2FybmluZ3MubGVuZ3RoID09PSAwKSB7XG4gICAgcmV0dXJuIG51bGxcbiAgfVxuXG4gIGNvbnN0IGVycm9ycyA9IHdhcm5pbmdzLmZpbHRlcih3ID0+IHcuc2V2ZXJpdHkgPT09ICdlcnJvcicpXG4gIGNvbnN0IHdhcm5zID0gd2FybmluZ3MuZmlsdGVyKHcgPT4gdy5zZXZlcml0eSA9PT0gJ3dhcm5pbmcnKVxuXG4gIHJldHVybiAoXG4gICAgPEJveCBmbGV4RGlyZWN0aW9uPVwiY29sdW1uXCIgbWFyZ2luVG9wPXsxfSBtYXJnaW5Cb3R0b209ezF9PlxuICAgICAgPFRleHQgYm9sZCBjb2xvcj17ZXJyb3JzLmxlbmd0aCA+IDAgPyAnZXJyb3InIDogJ3dhcm5pbmcnfT5cbiAgICAgICAgS2V5YmluZGluZyBDb25maWd1cmF0aW9uIElzc3Vlc1xuICAgICAgPC9UZXh0PlxuICAgICAgPEJveD5cbiAgICAgICAgPFRleHQgZGltQ29sb3I+TG9jYXRpb246IDwvVGV4dD5cbiAgICAgICAgPFRleHQgZGltQ29sb3I+e2dldEtleWJpbmRpbmdzUGF0aCgpfTwvVGV4dD5cbiAgICAgIDwvQm94PlxuICAgICAgPEJveCBtYXJnaW5MZWZ0PXsxfSBmbGV4RGlyZWN0aW9uPVwiY29sdW1uXCIgbWFyZ2luVG9wPXsxfT5cbiAgICAgICAge2Vycm9ycy5tYXAoKGVycm9yLCBpKSA9PiAoXG4gICAgICAgICAgPEJveCBrZXk9e2BlcnJvci0ke2l9YH0gZmxleERpcmVjdGlvbj1cImNvbHVtblwiPlxuICAgICAgICAgICAgPEJveD5cbiAgICAgICAgICAgICAgPFRleHQgZGltQ29sb3I+4pSUIDwvVGV4dD5cbiAgICAgICAgICAgICAgPFRleHQgY29sb3I9XCJlcnJvclwiPltFcnJvcl08L1RleHQ+XG4gICAgICAgICAgICAgIDxUZXh0IGRpbUNvbG9yPiB7ZXJyb3IubWVzc2FnZX08L1RleHQ+XG4gICAgICAgICAgICA8L0JveD5cbiAgICAgICAgICAgIHtlcnJvci5zdWdnZXN0aW9uICYmIChcbiAgICAgICAgICAgICAgPEJveCBtYXJnaW5MZWZ0PXszfT5cbiAgICAgICAgICAgICAgICA8VGV4dCBkaW1Db2xvcj7ihpIge2Vycm9yLnN1Z2dlc3Rpb259PC9UZXh0PlxuICAgICAgICAgICAgICA8L0JveD5cbiAgICAgICAgICAgICl9XG4gICAgICAgICAgPC9Cb3g+XG4gICAgICAgICkpfVxuICAgICAgICB7d2FybnMubWFwKCh3YXJuaW5nLCBpKSA9PiAoXG4gICAgICAgICAgPEJveCBrZXk9e2B3YXJuaW5nLSR7aX1gfSBmbGV4RGlyZWN0aW9uPVwiY29sdW1uXCI+XG4gICAgICAgICAgICA8Qm94PlxuICAgICAgICAgICAgICA8VGV4dCBkaW1Db2xvcj7ilJQgPC9UZXh0PlxuICAgICAgICAgICAgICA8VGV4dCBjb2xvcj1cIndhcm5pbmdcIj5bV2FybmluZ108L1RleHQ+XG4gICAgICAgICAgICAgIDxUZXh0IGRpbUNvbG9yPiB7d2FybmluZy5tZXNzYWdlfTwvVGV4dD5cbiAgICAgICAgICAgIDwvQm94PlxuICAgICAgICAgICAge3dhcm5pbmcuc3VnZ2VzdGlvbiAmJiAoXG4gICAgICAgICAgICAgIDxCb3ggbWFyZ2luTGVmdD17M30+XG4gICAgICAgICAgICAgICAgPFRleHQgZGltQ29sb3I+4oaSIHt3YXJuaW5nLnN1Z2dlc3Rpb259PC9UZXh0PlxuICAgICAgICAgICAgICA8L0JveD5cbiAgICAgICAgICAgICl9XG4gICAgICAgICAgPC9Cb3g+XG4gICAgICAgICkpfVxuICAgICAgPC9Cb3g+XG4gICAgPC9Cb3g+XG4gIClcbn1cbiJdLCJtYXBwaW5ncyI6IjtBQUFBLE9BQU9BLEtBQUssTUFBTSxPQUFPO0FBQ3pCLFNBQVNDLEdBQUcsRUFBRUMsSUFBSSxRQUFRLFdBQVc7QUFDckMsU0FDRUMsMkJBQTJCLEVBQzNCQyxrQkFBa0IsRUFDbEJDLGdDQUFnQyxRQUMzQixvQ0FBb0M7O0FBRTNDO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0EsT0FBTyxTQUFBQyxtQkFBQTtFQUFBLE1BQUFDLENBQUEsR0FBQUMsRUFBQTtFQUVMLElBQUksQ0FBQ0gsZ0NBQWdDLENBQUMsQ0FBQztJQUFBLE9BQzlCLElBQUk7RUFBQTtFQUNaLElBQUFJLEVBQUE7RUFBQSxJQUFBQyxFQUFBO0VBQUEsSUFBQUgsQ0FBQSxRQUFBSSxNQUFBLENBQUFDLEdBQUE7SUFLUUYsRUFBQSxHQUFBQyxNQUFJLENBQUFDLEdBQUEsQ0FBSiw2QkFBRyxDQUFDO0lBQUFDLEdBQUE7TUFIYixNQUFBQyxRQUFBLEdBQWlCWCwyQkFBMkIsQ0FBQyxDQUFDO01BRTlDLElBQUlXLFFBQVEsQ0FBQUMsTUFBTyxLQUFLLENBQUM7UUFDaEJMLEVBQUEsT0FBSTtRQUFKLE1BQUFHLEdBQUE7TUFBSTtNQUdiLE1BQUFHLE1BQUEsR0FBZUYsUUFBUSxDQUFBRyxNQUFPLENBQUNDLEtBQTJCLENBQUM7TUFDM0QsTUFBQUMsS0FBQSxHQUFjTCxRQUFRLENBQUFHLE1BQU8sQ0FBQ0csTUFBNkIsQ0FBQztNQUcxRFgsRUFBQSxJQUFDLEdBQUcsQ0FBZSxhQUFRLENBQVIsUUFBUSxDQUFZLFNBQUMsQ0FBRCxHQUFDLENBQWdCLFlBQUMsQ0FBRCxHQUFDLENBQ3ZELENBQUMsSUFBSSxDQUFDLElBQUksQ0FBSixLQUFHLENBQUMsQ0FBUSxLQUF1QyxDQUF2QyxDQUFBTyxNQUFNLENBQUFELE1BQU8sR0FBRyxDQUF1QixHQUF2QyxPQUF1QyxHQUF2QyxTQUFzQyxDQUFDLENBQUUsK0JBRTNELEVBRkMsSUFBSSxDQUdMLENBQUMsR0FBRyxDQUNGLENBQUMsSUFBSSxDQUFDLFFBQVEsQ0FBUixLQUFPLENBQUMsQ0FBQyxVQUFVLEVBQXhCLElBQUksQ0FDTCxDQUFDLElBQUksQ0FBQyxRQUFRLENBQVIsS0FBTyxDQUFDLENBQUUsQ0FBQVgsa0JBQWtCLENBQUMsRUFBRSxFQUFwQyxJQUFJLENBQ1AsRUFIQyxHQUFHLENBSUosQ0FBQyxHQUFHLENBQWEsVUFBQyxDQUFELEdBQUMsQ0FBZ0IsYUFBUSxDQUFSLFFBQVEsQ0FBWSxTQUFDLENBQUQsR0FBQyxDQUNwRCxDQUFBWSxNQUFNLENBQUFLLEdBQUksQ0FBQ0MsTUFhWCxFQUNBLENBQUFILEtBQUssQ0FBQUUsR0FBSSxDQUFDRSxNQWFWLEVBQ0gsRUE3QkMsR0FBRyxDQThCTixFQXRDQyxHQUFHLENBc0NFO0lBQUE7SUFBQWhCLENBQUEsTUFBQUUsRUFBQTtJQUFBRixDQUFBLE1BQUFHLEVBQUE7RUFBQTtJQUFBRCxFQUFBLEdBQUFGLENBQUE7SUFBQUcsRUFBQSxHQUFBSCxDQUFBO0VBQUE7RUFBQSxJQUFBRyxFQUFBLEtBQUFDLE1BQUEsQ0FBQUMsR0FBQTtJQUFBLE9BQUFGLEVBQUE7RUFBQTtFQUFBLE9BdENORCxFQXNDTTtBQUFBO0FBdERILFNBQUFjLE9BQUFDLE9BQUEsRUFBQUMsR0FBQTtFQUFBLE9Bd0NHLENBQUMsR0FBRyxDQUFNLEdBQWMsQ0FBZCxZQUFXQyxHQUFDLEVBQUMsQ0FBQyxDQUFnQixhQUFRLENBQVIsUUFBUSxDQUM5QyxDQUFDLEdBQUcsQ0FDRixDQUFDLElBQUksQ0FBQyxRQUFRLENBQVIsS0FBTyxDQUFDLENBQUMsRUFBRSxFQUFoQixJQUFJLENBQ0wsQ0FBQyxJQUFJLENBQU8sS0FBUyxDQUFULFNBQVMsQ0FBQyxTQUFTLEVBQTlCLElBQUksQ0FDTCxDQUFDLElBQUksQ0FBQyxRQUFRLENBQVIsS0FBTyxDQUFDLENBQUMsQ0FBRSxDQUFBRixPQUFPLENBQUFHLE9BQU8sQ0FBRSxFQUFoQyxJQUFJLENBQ1AsRUFKQyxHQUFHLENBS0gsQ0FBQUgsT0FBTyxDQUFBSSxVQUlQLElBSEMsQ0FBQyxHQUFHLENBQWEsVUFBQyxDQUFELEdBQUMsQ0FDaEIsQ0FBQyxJQUFJLENBQUMsUUFBUSxDQUFSLEtBQU8sQ0FBQyxDQUFDLEVBQUcsQ0FBQUosT0FBTyxDQUFBSSxVQUFVLENBQUUsRUFBcEMsSUFBSSxDQUNQLEVBRkMsR0FBRyxDQUdOLENBQ0YsRUFYQyxHQUFHLENBV0U7QUFBQTtBQW5EVCxTQUFBTixPQUFBTyxLQUFBLEVBQUFILENBQUE7RUFBQSxPQTBCRyxDQUFDLEdBQUcsQ0FBTSxHQUFZLENBQVosVUFBU0EsQ0FBQyxFQUFDLENBQUMsQ0FBZ0IsYUFBUSxDQUFSLFFBQVEsQ0FDNUMsQ0FBQyxHQUFHLENBQ0YsQ0FBQyxJQUFJLENBQUMsUUFBUSxDQUFSLEtBQU8sQ0FBQyxDQUFDLEVBQUUsRUFBaEIsSUFBSSxDQUNMLENBQUMsSUFBSSxDQUFPLEtBQU8sQ0FBUCxPQUFPLENBQUMsT0FBTyxFQUExQixJQUFJLENBQ0wsQ0FBQyxJQUFJLENBQUMsUUFBUSxDQUFSLEtBQU8sQ0FBQyxDQUFDLENBQUUsQ0FBQUcsS0FBSyxDQUFBRixPQUFPLENBQUUsRUFBOUIsSUFBSSxDQUNQLEVBSkMsR0FBRyxDQUtILENBQUFFLEtBQUssQ0FBQUQsVUFJTCxJQUhDLENBQUMsR0FBRyxDQUFhLFVBQUMsQ0FBRCxHQUFDLENBQ2hCLENBQUMsSUFBSSxDQUFDLFFBQVEsQ0FBUixLQUFPLENBQUMsQ0FBQyxFQUFHLENBQUFDLEtBQUssQ0FBQUQsVUFBVSxDQUFFLEVBQWxDLElBQUksQ0FDUCxFQUZDLEdBQUcsQ0FHTixDQUNGLEVBWEMsR0FBRyxDQVdFO0FBQUE7QUFyQ1QsU0FBQVIsT0FBQVUsR0FBQTtFQUFBLE9BYThCQyxHQUFDLENBQUFDLFFBQVMsS0FBSyxTQUFTO0FBQUE7QUFidEQsU0FBQWQsTUFBQWEsQ0FBQTtFQUFBLE9BWStCQSxDQUFDLENBQUFDLFFBQVMsS0FBSyxPQUFPO0FBQUEiLCJpZ25vcmVMaXN0IjpbXX0=
|
src/components/LanguagePicker.tsx
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { c as _c } from "react/compiler-runtime";
|
| 2 |
+
import figures from 'figures';
|
| 3 |
+
import React, { useState } from 'react';
|
| 4 |
+
import { Box, Text } from '../ink.js';
|
| 5 |
+
import { useKeybinding } from '../keybindings/useKeybinding.js';
|
| 6 |
+
import TextInput from './TextInput.js';
|
| 7 |
+
type Props = {
|
| 8 |
+
initialLanguage: string | undefined;
|
| 9 |
+
onComplete: (language: string | undefined) => void;
|
| 10 |
+
onCancel: () => void;
|
| 11 |
+
};
|
| 12 |
+
export function LanguagePicker(t0) {
|
| 13 |
+
const $ = _c(13);
|
| 14 |
+
const {
|
| 15 |
+
initialLanguage,
|
| 16 |
+
onComplete,
|
| 17 |
+
onCancel
|
| 18 |
+
} = t0;
|
| 19 |
+
const [language, setLanguage] = useState(initialLanguage);
|
| 20 |
+
const [cursorOffset, setCursorOffset] = useState((initialLanguage ?? "").length);
|
| 21 |
+
let t1;
|
| 22 |
+
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
|
| 23 |
+
t1 = {
|
| 24 |
+
context: "Settings"
|
| 25 |
+
};
|
| 26 |
+
$[0] = t1;
|
| 27 |
+
} else {
|
| 28 |
+
t1 = $[0];
|
| 29 |
+
}
|
| 30 |
+
useKeybinding("confirm:no", onCancel, t1);
|
| 31 |
+
let t2;
|
| 32 |
+
if ($[1] !== language || $[2] !== onComplete) {
|
| 33 |
+
t2 = function handleSubmit() {
|
| 34 |
+
const trimmed = language?.trim();
|
| 35 |
+
onComplete(trimmed || undefined);
|
| 36 |
+
};
|
| 37 |
+
$[1] = language;
|
| 38 |
+
$[2] = onComplete;
|
| 39 |
+
$[3] = t2;
|
| 40 |
+
} else {
|
| 41 |
+
t2 = $[3];
|
| 42 |
+
}
|
| 43 |
+
const handleSubmit = t2;
|
| 44 |
+
let t3;
|
| 45 |
+
if ($[4] === Symbol.for("react.memo_cache_sentinel")) {
|
| 46 |
+
t3 = <Text>Enter your preferred response and voice language:</Text>;
|
| 47 |
+
$[4] = t3;
|
| 48 |
+
} else {
|
| 49 |
+
t3 = $[4];
|
| 50 |
+
}
|
| 51 |
+
let t4;
|
| 52 |
+
if ($[5] === Symbol.for("react.memo_cache_sentinel")) {
|
| 53 |
+
t4 = <Text>{figures.pointer}</Text>;
|
| 54 |
+
$[5] = t4;
|
| 55 |
+
} else {
|
| 56 |
+
t4 = $[5];
|
| 57 |
+
}
|
| 58 |
+
const t5 = language ?? "";
|
| 59 |
+
let t6;
|
| 60 |
+
if ($[6] !== cursorOffset || $[7] !== handleSubmit || $[8] !== t5) {
|
| 61 |
+
t6 = <Box flexDirection="row" gap={1}>{t4}<TextInput value={t5} onChange={setLanguage} onSubmit={handleSubmit} focus={true} showCursor={true} placeholder={`e.g., Japanese, 日本語, Español${figures.ellipsis}`} columns={60} cursorOffset={cursorOffset} onChangeCursorOffset={setCursorOffset} /></Box>;
|
| 62 |
+
$[6] = cursorOffset;
|
| 63 |
+
$[7] = handleSubmit;
|
| 64 |
+
$[8] = t5;
|
| 65 |
+
$[9] = t6;
|
| 66 |
+
} else {
|
| 67 |
+
t6 = $[9];
|
| 68 |
+
}
|
| 69 |
+
let t7;
|
| 70 |
+
if ($[10] === Symbol.for("react.memo_cache_sentinel")) {
|
| 71 |
+
t7 = <Text dimColor={true}>Leave empty for default (English)</Text>;
|
| 72 |
+
$[10] = t7;
|
| 73 |
+
} else {
|
| 74 |
+
t7 = $[10];
|
| 75 |
+
}
|
| 76 |
+
let t8;
|
| 77 |
+
if ($[11] !== t6) {
|
| 78 |
+
t8 = <Box flexDirection="column" gap={1}>{t3}{t6}{t7}</Box>;
|
| 79 |
+
$[11] = t6;
|
| 80 |
+
$[12] = t8;
|
| 81 |
+
} else {
|
| 82 |
+
t8 = $[12];
|
| 83 |
+
}
|
| 84 |
+
return t8;
|
| 85 |
+
}
|
| 86 |
+
//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJuYW1lcyI6WyJmaWd1cmVzIiwiUmVhY3QiLCJ1c2VTdGF0ZSIsIkJveCIsIlRleHQiLCJ1c2VLZXliaW5kaW5nIiwiVGV4dElucHV0IiwiUHJvcHMiLCJpbml0aWFsTGFuZ3VhZ2UiLCJvbkNvbXBsZXRlIiwibGFuZ3VhZ2UiLCJvbkNhbmNlbCIsIkxhbmd1YWdlUGlja2VyIiwidDAiLCIkIiwiX2MiLCJzZXRMYW5ndWFnZSIsImN1cnNvck9mZnNldCIsInNldEN1cnNvck9mZnNldCIsImxlbmd0aCIsInQxIiwiU3ltYm9sIiwiZm9yIiwiY29udGV4dCIsInQyIiwiaGFuZGxlU3VibWl0IiwidHJpbW1lZCIsInRyaW0iLCJ1bmRlZmluZWQiLCJ0MyIsInQ0IiwicG9pbnRlciIsInQ1IiwidDYiLCJlbGxpcHNpcyIsInQ3IiwidDgiXSwic291cmNlcyI6WyJMYW5ndWFnZVBpY2tlci50c3giXSwic291cmNlc0NvbnRlbnQiOlsiaW1wb3J0IGZpZ3VyZXMgZnJvbSAnZmlndXJlcydcbmltcG9ydCBSZWFjdCwgeyB1c2VTdGF0ZSB9IGZyb20gJ3JlYWN0J1xuaW1wb3J0IHsgQm94LCBUZXh0IH0gZnJvbSAnLi4vaW5rLmpzJ1xuaW1wb3J0IHsgdXNlS2V5YmluZGluZyB9IGZyb20gJy4uL2tleWJpbmRpbmdzL3VzZUtleWJpbmRpbmcuanMnXG5pbXBvcnQgVGV4dElucHV0IGZyb20gJy4vVGV4dElucHV0LmpzJ1xuXG50eXBlIFByb3BzID0ge1xuICBpbml0aWFsTGFuZ3VhZ2U6IHN0cmluZyB8IHVuZGVmaW5lZFxuICBvbkNvbXBsZXRlOiAobGFuZ3VhZ2U6IHN0cmluZyB8IHVuZGVmaW5lZCkgPT4gdm9pZFxuICBvbkNhbmNlbDogKCkgPT4gdm9pZFxufVxuXG5leHBvcnQgZnVuY3Rpb24gTGFuZ3VhZ2VQaWNrZXIoe1xuICBpbml0aWFsTGFuZ3VhZ2UsXG4gIG9uQ29tcGxldGUsXG4gIG9uQ2FuY2VsLFxufTogUHJvcHMpOiBSZWFjdC5SZWFjdE5vZGUge1xuICBjb25zdCBbbGFuZ3VhZ2UsIHNldExhbmd1YWdlXSA9IHVzZVN0YXRlKGluaXRpYWxMYW5ndWFnZSlcbiAgY29uc3QgW2N1cnNvck9mZnNldCwgc2V0Q3Vyc29yT2Zmc2V0XSA9IHVzZVN0YXRlKFxuICAgIChpbml0aWFsTGFuZ3VhZ2UgPz8gJycpLmxlbmd0aCxcbiAgKVxuXG4gIC8vIFVzZSBjb25maWd1cmFibGUga2V5YmluZGluZyBmb3IgRVNDIHRvIGNhbmNlbFxuICAvLyBVc2UgU2V0dGluZ3MgY29udGV4dCBzbyAnbicga2V5IGRvZXNuJ3QgdHJpZ2dlciBjYW5jZWwgKGFsbG93cyB0eXBpbmcgJ24nIGluIGlucHV0KVxuICB1c2VLZXliaW5kaW5nKCdjb25maXJtOm5vJywgb25DYW5jZWwsIHsgY29udGV4dDogJ1NldHRpbmdzJyB9KVxuXG4gIGZ1bmN0aW9uIGhhbmRsZVN1Ym1pdCgpOiB2b2lkIHtcbiAgICBjb25zdCB0cmltbWVkID0gbGFuZ3VhZ2U/LnRyaW0oKVxuICAgIG9uQ29tcGxldGUodHJpbW1lZCB8fCB1bmRlZmluZWQpXG4gIH1cblxuICByZXR1cm4gKFxuICAgIDxCb3ggZmxleERpcmVjdGlvbj1cImNvbHVtblwiIGdhcD17MX0+XG4gICAgICA8VGV4dD5FbnRlciB5b3VyIHByZWZlcnJlZCByZXNwb25zZSBhbmQgdm9pY2UgbGFuZ3VhZ2U6PC9UZXh0PlxuICAgICAgPEJveCBmbGV4RGlyZWN0aW9uPVwicm93XCIgZ2FwPXsxfT5cbiAgICAgICAgPFRleHQ+e2ZpZ3VyZXMucG9pbnRlcn08L1RleHQ+XG4gICAgICAgIDxUZXh0SW5wdXRcbiAgICAgICAgICB2YWx1ZT17bGFuZ3VhZ2UgPz8gJyd9XG4gICAgICAgICAgb25DaGFuZ2U9e3NldExhbmd1YWdlfVxuICAgICAgICAgIG9uU3VibWl0PXtoYW5kbGVTdWJtaXR9XG4gICAgICAgICAgZm9jdXM9e3RydWV9XG4gICAgICAgICAgc2hvd0N1cnNvcj17dHJ1ZX1cbiAgICAgICAgICBwbGFjZWhvbGRlcj17YGUuZy4sIEphcGFuZXNlLCDml6XmnKzoqp4sIEVzcGHDsW9sJHtmaWd1cmVzLmVsbGlwc2lzfWB9XG4gICAgICAgICAgY29sdW1ucz17NjB9XG4gICAgICAgICAgY3Vyc29yT2Zmc2V0PXtjdXJzb3JPZmZzZXR9XG4gICAgICAgICAgb25DaGFuZ2VDdXJzb3JPZmZzZXQ9e3NldEN1cnNvck9mZnNldH1cbiAgICAgICAgLz5cbiAgICAgIDwvQm94PlxuICAgICAgPFRleHQgZGltQ29sb3I+TGVhdmUgZW1wdHkgZm9yIGRlZmF1bHQgKEVuZ2xpc2gpPC9UZXh0PlxuICAgIDwvQm94PlxuICApXG59XG4iXSwibWFwcGluZ3MiOiI7QUFBQSxPQUFPQSxPQUFPLE1BQU0sU0FBUztBQUM3QixPQUFPQyxLQUFLLElBQUlDLFFBQVEsUUFBUSxPQUFPO0FBQ3ZDLFNBQVNDLEdBQUcsRUFBRUMsSUFBSSxRQUFRLFdBQVc7QUFDckMsU0FBU0MsYUFBYSxRQUFRLGlDQUFpQztBQUMvRCxPQUFPQyxTQUFTLE1BQU0sZ0JBQWdCO0FBRXRDLEtBQUtDLEtBQUssR0FBRztFQUNYQyxlQUFlLEVBQUUsTUFBTSxHQUFHLFNBQVM7RUFDbkNDLFVBQVUsRUFBRSxDQUFDQyxRQUFRLEVBQUUsTUFBTSxHQUFHLFNBQVMsRUFBRSxHQUFHLElBQUk7RUFDbERDLFFBQVEsRUFBRSxHQUFHLEdBQUcsSUFBSTtBQUN0QixDQUFDO0FBRUQsT0FBTyxTQUFBQyxlQUFBQyxFQUFBO0VBQUEsTUFBQUMsQ0FBQSxHQUFBQyxFQUFBO0VBQXdCO0lBQUFQLGVBQUE7SUFBQUMsVUFBQTtJQUFBRTtFQUFBLElBQUFFLEVBSXZCO0VBQ04sT0FBQUgsUUFBQSxFQUFBTSxXQUFBLElBQWdDZCxRQUFRLENBQUNNLGVBQWUsQ0FBQztFQUN6RCxPQUFBUyxZQUFBLEVBQUFDLGVBQUEsSUFBd0NoQixRQUFRLENBQzlDLENBQUNNLGVBQXFCLElBQXJCLEVBQXFCLEVBQUFXLE1BQ3hCLENBQUM7RUFBQSxJQUFBQyxFQUFBO0VBQUEsSUFBQU4sQ0FBQSxRQUFBTyxNQUFBLENBQUFDLEdB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
|
src/keybindings/KeybindingContext.tsx
ADDED
|
@@ -0,0 +1,243 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { c as _c } from "react/compiler-runtime";
|
| 2 |
+
import React, { createContext, type RefObject, useContext, useLayoutEffect, useMemo } from 'react';
|
| 3 |
+
import type { Key } from '../ink.js';
|
| 4 |
+
import { type ChordResolveResult, getBindingDisplayText, resolveKeyWithChordState } from './resolver.js';
|
| 5 |
+
import type { KeybindingContextName, ParsedBinding, ParsedKeystroke } from './types.js';
|
| 6 |
+
|
| 7 |
+
/** Handler registration for action callbacks */
|
| 8 |
+
type HandlerRegistration = {
|
| 9 |
+
action: string;
|
| 10 |
+
context: KeybindingContextName;
|
| 11 |
+
handler: () => void;
|
| 12 |
+
};
|
| 13 |
+
type KeybindingContextValue = {
|
| 14 |
+
/** Resolve a key input to an action name (with chord support) */
|
| 15 |
+
resolve: (input: string, key: Key, activeContexts: KeybindingContextName[]) => ChordResolveResult;
|
| 16 |
+
|
| 17 |
+
/** Update the pending chord state */
|
| 18 |
+
setPendingChord: (pending: ParsedKeystroke[] | null) => void;
|
| 19 |
+
|
| 20 |
+
/** Get display text for an action (e.g., "ctrl+t") */
|
| 21 |
+
getDisplayText: (action: string, context: KeybindingContextName) => string | undefined;
|
| 22 |
+
|
| 23 |
+
/** All parsed bindings (for help display) */
|
| 24 |
+
bindings: ParsedBinding[];
|
| 25 |
+
|
| 26 |
+
/** Current pending chord keystrokes (null if not in a chord) */
|
| 27 |
+
pendingChord: ParsedKeystroke[] | null;
|
| 28 |
+
|
| 29 |
+
/** Currently active keybinding contexts (for priority resolution) */
|
| 30 |
+
activeContexts: Set<KeybindingContextName>;
|
| 31 |
+
|
| 32 |
+
/** Register a context as active (call on mount) */
|
| 33 |
+
registerActiveContext: (context: KeybindingContextName) => void;
|
| 34 |
+
|
| 35 |
+
/** Unregister a context (call on unmount) */
|
| 36 |
+
unregisterActiveContext: (context: KeybindingContextName) => void;
|
| 37 |
+
|
| 38 |
+
/** Register a handler for an action (used by useKeybinding) */
|
| 39 |
+
registerHandler: (registration: HandlerRegistration) => () => void;
|
| 40 |
+
|
| 41 |
+
/** Invoke all handlers for an action (used by ChordInterceptor) */
|
| 42 |
+
invokeAction: (action: string) => boolean;
|
| 43 |
+
};
|
| 44 |
+
const KeybindingContext = createContext<KeybindingContextValue | null>(null);
|
| 45 |
+
type ProviderProps = {
|
| 46 |
+
bindings: ParsedBinding[];
|
| 47 |
+
/** Ref for immediate access to pending chord (avoids React state delay) */
|
| 48 |
+
pendingChordRef: RefObject<ParsedKeystroke[] | null>;
|
| 49 |
+
/** State value for re-renders (UI updates) */
|
| 50 |
+
pendingChord: ParsedKeystroke[] | null;
|
| 51 |
+
setPendingChord: (pending: ParsedKeystroke[] | null) => void;
|
| 52 |
+
activeContexts: Set<KeybindingContextName>;
|
| 53 |
+
registerActiveContext: (context: KeybindingContextName) => void;
|
| 54 |
+
unregisterActiveContext: (context: KeybindingContextName) => void;
|
| 55 |
+
/** Ref to handler registry (used by ChordInterceptor) */
|
| 56 |
+
handlerRegistryRef: RefObject<Map<string, Set<HandlerRegistration>>>;
|
| 57 |
+
children: React.ReactNode;
|
| 58 |
+
};
|
| 59 |
+
export function KeybindingProvider(t0) {
|
| 60 |
+
const $ = _c(24);
|
| 61 |
+
const {
|
| 62 |
+
bindings,
|
| 63 |
+
pendingChordRef,
|
| 64 |
+
pendingChord,
|
| 65 |
+
setPendingChord,
|
| 66 |
+
activeContexts,
|
| 67 |
+
registerActiveContext,
|
| 68 |
+
unregisterActiveContext,
|
| 69 |
+
handlerRegistryRef,
|
| 70 |
+
children
|
| 71 |
+
} = t0;
|
| 72 |
+
let t1;
|
| 73 |
+
if ($[0] !== bindings) {
|
| 74 |
+
t1 = (action, context) => getBindingDisplayText(action, context, bindings);
|
| 75 |
+
$[0] = bindings;
|
| 76 |
+
$[1] = t1;
|
| 77 |
+
} else {
|
| 78 |
+
t1 = $[1];
|
| 79 |
+
}
|
| 80 |
+
const getDisplay = t1;
|
| 81 |
+
let t2;
|
| 82 |
+
if ($[2] !== handlerRegistryRef) {
|
| 83 |
+
t2 = registration => {
|
| 84 |
+
const registry = handlerRegistryRef.current;
|
| 85 |
+
if (!registry) {
|
| 86 |
+
return _temp;
|
| 87 |
+
}
|
| 88 |
+
if (!registry.has(registration.action)) {
|
| 89 |
+
registry.set(registration.action, new Set());
|
| 90 |
+
}
|
| 91 |
+
registry.get(registration.action).add(registration);
|
| 92 |
+
return () => {
|
| 93 |
+
const handlers = registry.get(registration.action);
|
| 94 |
+
if (handlers) {
|
| 95 |
+
handlers.delete(registration);
|
| 96 |
+
if (handlers.size === 0) {
|
| 97 |
+
registry.delete(registration.action);
|
| 98 |
+
}
|
| 99 |
+
}
|
| 100 |
+
};
|
| 101 |
+
};
|
| 102 |
+
$[2] = handlerRegistryRef;
|
| 103 |
+
$[3] = t2;
|
| 104 |
+
} else {
|
| 105 |
+
t2 = $[3];
|
| 106 |
+
}
|
| 107 |
+
const registerHandler = t2;
|
| 108 |
+
let t3;
|
| 109 |
+
if ($[4] !== activeContexts || $[5] !== handlerRegistryRef) {
|
| 110 |
+
t3 = action_0 => {
|
| 111 |
+
const registry_0 = handlerRegistryRef.current;
|
| 112 |
+
if (!registry_0) {
|
| 113 |
+
return false;
|
| 114 |
+
}
|
| 115 |
+
const handlers_0 = registry_0.get(action_0);
|
| 116 |
+
if (!handlers_0 || handlers_0.size === 0) {
|
| 117 |
+
return false;
|
| 118 |
+
}
|
| 119 |
+
for (const registration_0 of handlers_0) {
|
| 120 |
+
if (activeContexts.has(registration_0.context)) {
|
| 121 |
+
registration_0.handler();
|
| 122 |
+
return true;
|
| 123 |
+
}
|
| 124 |
+
}
|
| 125 |
+
return false;
|
| 126 |
+
};
|
| 127 |
+
$[4] = activeContexts;
|
| 128 |
+
$[5] = handlerRegistryRef;
|
| 129 |
+
$[6] = t3;
|
| 130 |
+
} else {
|
| 131 |
+
t3 = $[6];
|
| 132 |
+
}
|
| 133 |
+
const invokeAction = t3;
|
| 134 |
+
let t4;
|
| 135 |
+
if ($[7] !== bindings || $[8] !== pendingChordRef) {
|
| 136 |
+
t4 = (input, key, contexts) => resolveKeyWithChordState(input, key, contexts, bindings, pendingChordRef.current);
|
| 137 |
+
$[7] = bindings;
|
| 138 |
+
$[8] = pendingChordRef;
|
| 139 |
+
$[9] = t4;
|
| 140 |
+
} else {
|
| 141 |
+
t4 = $[9];
|
| 142 |
+
}
|
| 143 |
+
let t5;
|
| 144 |
+
if ($[10] !== activeContexts || $[11] !== bindings || $[12] !== getDisplay || $[13] !== invokeAction || $[14] !== pendingChord || $[15] !== registerActiveContext || $[16] !== registerHandler || $[17] !== setPendingChord || $[18] !== t4 || $[19] !== unregisterActiveContext) {
|
| 145 |
+
t5 = {
|
| 146 |
+
resolve: t4,
|
| 147 |
+
setPendingChord,
|
| 148 |
+
getDisplayText: getDisplay,
|
| 149 |
+
bindings,
|
| 150 |
+
pendingChord,
|
| 151 |
+
activeContexts,
|
| 152 |
+
registerActiveContext,
|
| 153 |
+
unregisterActiveContext,
|
| 154 |
+
registerHandler,
|
| 155 |
+
invokeAction
|
| 156 |
+
};
|
| 157 |
+
$[10] = activeContexts;
|
| 158 |
+
$[11] = bindings;
|
| 159 |
+
$[12] = getDisplay;
|
| 160 |
+
$[13] = invokeAction;
|
| 161 |
+
$[14] = pendingChord;
|
| 162 |
+
$[15] = registerActiveContext;
|
| 163 |
+
$[16] = registerHandler;
|
| 164 |
+
$[17] = setPendingChord;
|
| 165 |
+
$[18] = t4;
|
| 166 |
+
$[19] = unregisterActiveContext;
|
| 167 |
+
$[20] = t5;
|
| 168 |
+
} else {
|
| 169 |
+
t5 = $[20];
|
| 170 |
+
}
|
| 171 |
+
const value = t5;
|
| 172 |
+
let t6;
|
| 173 |
+
if ($[21] !== children || $[22] !== value) {
|
| 174 |
+
t6 = <KeybindingContext.Provider value={value}>{children}</KeybindingContext.Provider>;
|
| 175 |
+
$[21] = children;
|
| 176 |
+
$[22] = value;
|
| 177 |
+
$[23] = t6;
|
| 178 |
+
} else {
|
| 179 |
+
t6 = $[23];
|
| 180 |
+
}
|
| 181 |
+
return t6;
|
| 182 |
+
}
|
| 183 |
+
function _temp() {}
|
| 184 |
+
export function useKeybindingContext() {
|
| 185 |
+
const ctx = useContext(KeybindingContext);
|
| 186 |
+
if (!ctx) {
|
| 187 |
+
throw new Error("useKeybindingContext must be used within KeybindingProvider");
|
| 188 |
+
}
|
| 189 |
+
return ctx;
|
| 190 |
+
}
|
| 191 |
+
|
| 192 |
+
/**
|
| 193 |
+
* Optional hook that returns undefined outside of KeybindingProvider.
|
| 194 |
+
* Useful for components that may render before provider is available.
|
| 195 |
+
*/
|
| 196 |
+
export function useOptionalKeybindingContext() {
|
| 197 |
+
return useContext(KeybindingContext);
|
| 198 |
+
}
|
| 199 |
+
|
| 200 |
+
/**
|
| 201 |
+
* Hook to register a keybinding context as active while the component is mounted.
|
| 202 |
+
*
|
| 203 |
+
* When a context is registered, its keybindings take precedence over Global bindings.
|
| 204 |
+
* This allows context-specific bindings (like ThemePicker's ctrl+t) to override
|
| 205 |
+
* global bindings (like the todo toggle) when the context is active.
|
| 206 |
+
*
|
| 207 |
+
* @example
|
| 208 |
+
* ```tsx
|
| 209 |
+
* function ThemePicker() {
|
| 210 |
+
* useRegisterKeybindingContext('ThemePicker')
|
| 211 |
+
* // Now ThemePicker's ctrl+t binding takes precedence over Global
|
| 212 |
+
* }
|
| 213 |
+
* ```
|
| 214 |
+
*/
|
| 215 |
+
export function useRegisterKeybindingContext(context, t0) {
|
| 216 |
+
const $ = _c(5);
|
| 217 |
+
const isActive = t0 === undefined ? true : t0;
|
| 218 |
+
const keybindingContext = useOptionalKeybindingContext();
|
| 219 |
+
let t1;
|
| 220 |
+
let t2;
|
| 221 |
+
if ($[0] !== context || $[1] !== isActive || $[2] !== keybindingContext) {
|
| 222 |
+
t1 = () => {
|
| 223 |
+
if (!keybindingContext || !isActive) {
|
| 224 |
+
return;
|
| 225 |
+
}
|
| 226 |
+
keybindingContext.registerActiveContext(context);
|
| 227 |
+
return () => {
|
| 228 |
+
keybindingContext.unregisterActiveContext(context);
|
| 229 |
+
};
|
| 230 |
+
};
|
| 231 |
+
t2 = [context, keybindingContext, isActive];
|
| 232 |
+
$[0] = context;
|
| 233 |
+
$[1] = isActive;
|
| 234 |
+
$[2] = keybindingContext;
|
| 235 |
+
$[3] = t1;
|
| 236 |
+
$[4] = t2;
|
| 237 |
+
} else {
|
| 238 |
+
t1 = $[3];
|
| 239 |
+
t2 = $[4];
|
| 240 |
+
}
|
| 241 |
+
useLayoutEffect(t1, t2);
|
| 242 |
+
}
|
| 243 |
+
//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJuYW1lcyI6WyJSZWFjdCIsImNyZWF0ZUNvbnRleHQiLCJSZWZPYmplY3QiLCJ1c2VDb250ZXh0IiwidXNlTGF5b3V0RWZmZWN0IiwidXNlTWVtbyIsIktleSIsIkNob3JkUmVzb2x2ZVJlc3VsdCIsImdldEJpbmRpbmdEaXNwbGF5VGV4dCIsInJlc29sdmVLZXlXaXRoQ2hvcmRTdGF0ZSIsIktleWJpbmRpbmdDb250ZXh0TmFtZSIsIlBhcnNlZEJpbmRpbmciLCJQYXJzZWRLZXlzdHJva2UiLCJIYW5kbGVyUmVnaXN0cmF0aW9uIiwiYWN0aW9uIiwiY29udGV4dCIsImhhbmRsZXIiLCJLZXliaW5kaW5nQ29udGV4dFZhbHVlIiwicmVzb2x2ZSIsImlucHV0Iiwia2V5IiwiYWN0aXZlQ29udGV4dHMiLCJzZXRQZW5kaW5nQ2hvcmQiLCJwZW5kaW5nIiwiZ2V0RGlzcGxheVRleHQiLCJiaW5kaW5ncyIsInBlbmRpbmdDaG9yZCIsIlNldCIsInJlZ2lzdGVyQWN0aXZlQ29udGV4dCIsInVucmVnaXN0ZXJBY3RpdmVDb250ZXh0IiwicmVnaXN0ZXJIYW5kbGVyIiwicmVnaXN0cmF0aW9uIiwiaW52b2tlQWN0aW9uIiwiS2V5YmluZGluZ0NvbnRleHQiLCJQcm92aWRlclByb3BzIiwicGVuZGluZ0Nob3JkUmVmIiwiaGFuZGxlclJlZ2lzdHJ5UmVmIiwiTWFwIiwiY2hpbGRyZW4iLCJSZWFjdE5vZGUiLCJLZXliaW5kaW5nUHJvdmlkZXIiLCJ0MCIsIiQiLCJfYyIsInQxIiwiZ2V0RGlzcGxheSIsInQyIiwicmVnaXN0cnkiLCJjdXJyZW50IiwiX3RlbXAiLCJoYXMiLCJzZXQiLCJnZXQiLCJhZGQiLCJoYW5kbGVycyIsImRlbGV0ZSIsInNpemUiLCJ0MyIsImFjdGlvbl8wIiwicmVnaXN0cnlfMCIsImhhbmRsZXJzXzAiLCJyZWdpc3RyYXRpb25fMCIsInQ0IiwiY29udGV4dHMiLCJ0NSIsInZhbHVlIiwidDYiLCJ1c2VLZXliaW5kaW5nQ29udGV4dCIsImN0eCIsIkVycm9yIiwidXNlT3B0aW9uYWxLZXliaW5kaW5nQ29udGV4dCIsInVzZVJlZ2lzdGVyS2V5YmluZGluZ0NvbnRleHQiLCJpc0FjdGl2ZSIsInVuZGVmaW5lZCIsImtleWJpbmRpbmdDb250ZXh0Il0sInNvdXJjZXMiOlsiS2V5YmluZGluZ0NvbnRleHQudHN4Il0sInNvdXJjZXNDb250ZW50IjpbImltcG9ydCBSZWFjdCwge1xuICBjcmVhdGVDb250ZXh0LFxuICB0eXBlIFJlZk9iamVjdCxcbiAgdXNlQ29udGV4dCxcbiAgdXNlTGF5b3V0RWZmZWN0LFxuICB1c2VNZW1vLFxufSBmcm9tICdyZWFjdCdcbmltcG9ydCB0eXBlIHsgS2V5IH0gZnJvbSAnLi4vaW5rLmpzJ1xuaW1wb3J0IHtcbiAgdHlwZSBDaG9yZFJlc29sdmVSZXN1bHQsXG4gIGdldEJpbmRpbmdEaXNwbGF5VGV4dCxcbiAgcmVzb2x2ZUtleVdpdGhDaG9yZFN0YXRlLFxufSBmcm9tICcuL3Jlc29sdmVyLmpzJ1xuaW1wb3J0IHR5cGUge1xuICBLZXliaW5kaW5nQ29udGV4dE5hbWUsXG4gIFBhcnNlZEJpbmRpbmcsXG4gIFBhcnNlZEtleXN0cm9rZSxcbn0gZnJvbSAnLi90eXBlcy5qcydcblxuLyoqIEhhbmRsZXIgcmVnaXN0cmF0aW9uIGZvciBhY3Rpb24gY2FsbGJhY2tzICovXG50eXBlIEhhbmRsZXJSZWdpc3RyYXRpb24gPSB7XG4gIGFjdGlvbjogc3RyaW5nXG4gIGNvbnRleHQ6IEtleWJpbmRpbmdDb250ZXh0TmFtZVxuICBoYW5kbGVyOiAoKSA9PiB2b2lkXG59XG5cbnR5cGUgS2V5YmluZGluZ0NvbnRleHRWYWx1ZSA9IHtcbiAgLyoqIFJlc29sdmUgYSBrZXkgaW5wdXQgdG8gYW4gYWN0aW9uIG5hbWUgKHdpdGggY2hvcmQgc3VwcG9ydCkgKi9cbiAgcmVzb2x2ZTogKFxuICAgIGlucHV0OiBzdHJpbmcsXG4gICAga2V5OiBLZXksXG4gICAgYWN0aXZlQ29udGV4dHM6IEtleWJpbmRpbmdDb250ZXh0TmFtZVtdLFxuICApID0+IENob3JkUmVzb2x2ZVJlc3VsdFxuXG4gIC8qKiBVcGRhdGUgdGhlIHBlbmRpbmcgY2hvcmQgc3RhdGUgKi9cbiAgc2V0UGVuZGluZ0Nob3JkOiAocGVuZGluZzogUGFyc2VkS2V5c3Ryb2tlW10gfCBudWxsKSA9PiB2b2lkXG5cbiAgLyoqIEdldCBkaXNwbGF5IHRleHQgZm9yIGFuIGFjdGlvbiAoZS5nLiwgXCJjdHJsK3RcIikgKi9cbiAgZ2V0RGlzcGxheVRleHQ6IChcbiAgICBhY3Rpb246IHN0cmluZyxcbiAgICBjb250ZXh0OiBLZXliaW5kaW5nQ29udGV4dE5hbWUsXG4gICkgPT4gc3RyaW5nIHwgdW5kZWZpbmVkXG5cbiAgLyoqIEFsbCBwYXJzZWQgYmluZGluZ3MgKGZvciBoZWxwIGRpc3BsYXkpICovXG4gIGJpbmRpbmdzOiBQYXJzZWRCaW5kaW5nW11cblxuICAvKiogQ3VycmVudCBwZW5kaW5nIGNob3JkIGtleXN0cm9rZXMgKG51bGwgaWYgbm90IGluIGEgY2hvcmQpICovXG4gIHBlbmRpbmdDaG9yZDogUGFyc2VkS2V5c3Ryb2tlW10gfCBudWxsXG5cbiAgLyoqIEN1cnJlbnRseSBhY3RpdmUga2V5YmluZGluZyBjb250ZXh0cyAoZm9yIHByaW9yaXR5IHJlc29sdXRpb24pICovXG4gIGFjdGl2ZUNvbnRleHRzOiBTZXQ8S2V5YmluZGluZ0NvbnRleHROYW1lPlxuXG4gIC8qKiBSZWdpc3RlciBhIGNvbnRleHQgYXMgYWN0aXZlIChjYWxsIG9uIG1vdW50KSAqL1xuICByZWdpc3RlckFjdGl2ZUNvbnRleHQ6IChjb250ZXh0OiBLZXliaW5kaW5nQ29udGV4dE5hbWUpID0+IHZvaWRcblxuICAvKiogVW5yZWdpc3RlciBhIGNvbnRleHQgKGNhbGwgb24gdW5tb3VudCkgKi9cbiAgdW5yZWdpc3RlckFjdGl2ZUNvbnRleHQ6IChjb250ZXh0OiBLZXliaW5kaW5nQ29udGV4dE5hbWUpID0+IHZvaWRcblxuICAvKiogUmVnaXN0ZXIgYSBoYW5kbGVyIGZvciBhbiBhY3Rpb24gKHVzZWQgYnkgdXNlS2V5YmluZGluZykgKi9cbiAgcmVnaXN0ZXJIYW5kbGVyOiAocmVnaXN0cmF0aW9uOiBIYW5kbGVyUmVnaXN0cmF0aW9uKSA9PiAoKSA9PiB2b2lkXG5cbiAgLyoqIEludm9rZSBhbGwgaGFuZGxlcnMgZm9yIGFuIGFjdGlvbiAodXNlZCBieSBDaG9yZEludGVyY2VwdG9yKSAqL1xuICBpbnZva2VBY3Rpb246IChhY3Rpb246IHN0cmluZykgPT4gYm9vbGVhblxufVxuXG5jb25zdCBLZXliaW5kaW5nQ29udGV4dCA9IGNyZWF0ZUNvbnRleHQ8S2V5YmluZGluZ0NvbnRleHRWYWx1ZSB8IG51bGw+KG51bGwpXG5cbnR5cGUgUHJvdmlkZXJQcm9wcyA9IHtcbiAgYmluZGluZ3M6IFBhcnNlZEJpbmRpbmdbXVxuICAvKiogUmVmIGZvciBpbW1lZGlhdGUgYWNjZXNzIHRvIHBlbmRpbmcgY2hvcmQgKGF2b2lkcyBSZWFjdCBzdGF0ZSBkZWxheSkgKi9cbiAgcGVuZGluZ0Nob3JkUmVmOiBSZWZPYmplY3Q8UGFyc2VkS2V5c3Ryb2tlW10gfCBudWxsPlxuICAvKiogU3RhdGUgdmFsdWUgZm9yIHJlLXJlbmRlcnMgKFVJIHVwZGF0ZXMpICovXG4gIHBlbmRpbmdDaG9yZDogUGFyc2VkS2V5c3Ryb2tlW10gfCBudWxsXG4gIHNldFBlbmRpbmdDaG9yZDogKHBlbmRpbmc6IFBhcnNlZEtleXN0cm9rZVtdIHwgbnVsbCkgPT4gdm9pZFxuICBhY3RpdmVDb250ZXh0czogU2V0PEtleWJpbmRpbmdDb250ZXh0TmFtZT5cbiAgcmVnaXN0ZXJBY3RpdmVDb250ZXh0OiAoY29udGV4dDogS2V5YmluZGluZ0NvbnRleHROYW1lKSA9PiB2b2lkXG4gIHVucmVnaXN0ZXJBY3RpdmVDb250ZXh0OiAoY29udGV4dDogS2V5YmluZGluZ0NvbnRleHROYW1lKSA9PiB2b2lkXG4gIC8qKiBSZWYgdG8gaGFuZGxlciByZWdpc3RyeSAodXNlZCBieSBDaG9yZEludGVyY2VwdG9yKSAqL1xuICBoYW5kbGVyUmVnaXN0cnlSZWY6IFJlZk9iamVjdDxNYXA8c3RyaW5nLCBTZXQ8SGFuZGxlclJlZ2lzdHJhdGlvbj4+PlxuICBjaGlsZHJlbjogUmVhY3QuUmVhY3ROb2RlXG59XG5cbmV4cG9ydCBmdW5jdGlvbiBLZXliaW5kaW5nUHJvdmlkZXIoe1xuICBiaW5kaW5ncyxcbiAgcGVuZGluZ0Nob3JkUmVmLFxuICBwZW5kaW5nQ2hvcmQsXG4gIHNldFBlbmRpbmdDaG9yZCxcbiAgYWN0aXZlQ29udGV4dHMsXG4gIHJlZ2lzdGVyQWN0aXZlQ29udGV4dCxcbiAgdW5yZWdpc3RlckFjdGl2ZUNvbnRleHQsXG4gIGhhbmRsZXJSZWdpc3RyeVJlZixcbiAgY2hpbGRyZW4sXG59OiBQcm92aWRlclByb3BzKTogUmVhY3QuUmVhY3ROb2RlIHtcbiAgY29uc3QgdmFsdWUgPSB1c2VNZW1vPEtleWJpbmRpbmdDb250ZXh0VmFsdWU+KCgpID0+IHtcbiAgICBjb25zdCBnZXREaXNwbGF5ID0gKGFjdGlvbjogc3RyaW5nLCBjb250ZXh0OiBLZXliaW5kaW5nQ29udGV4dE5hbWUpID0+XG4gICAgICBnZXRCaW5kaW5nRGlzcGxheVRleHQoYWN0aW9uLCBjb250ZXh0LCBiaW5kaW5ncylcblxuICAgIC8vIFJlZ2lzdGVyIGEgaGFuZGxlciBmb3IgYW4gYWN0aW9uXG4gICAgY29uc3QgcmVnaXN0ZXJIYW5kbGVyID0gKHJlZ2lzdHJhdGlvbjogSGFuZGxlclJlZ2lzdHJhdGlvbikgPT4ge1xuICAgICAgY29uc3QgcmVnaXN0cnkgPSBoYW5kbGVyUmVnaXN0cnlSZWYuY3VycmVudFxuICAgICAgaWYgKCFyZWdpc3RyeSkgcmV0dXJuICgpID0+IHt9XG5cbiAgICAgIGlmICghcmVnaXN0cnkuaGFzKHJlZ2lzdHJhdGlvbi5hY3Rpb24pKSB7XG4gICAgICAgIHJlZ2lzdHJ5LnNldChyZWdpc3RyYXRpb24uYWN0aW9uLCBuZXcgU2V0KCkpXG4gICAgICB9XG4gICAgICByZWdpc3RyeS5nZXQocmVnaXN0cmF0aW9uLmFjdGlvbikhLmFkZChyZWdpc3RyYXRpb24pXG5cbiAgICAgIC8vIFJldHVybiB1bnJlZ2lzdGVyIGZ1bmN0aW9uXG4gICAgICByZXR1cm4gKCkgPT4ge1xuICAgICAgICBjb25zdCBoYW5kbGVycyA9IHJlZ2lzdHJ5LmdldChyZWdpc3RyYXRpb24uYWN0aW9uKVxuICAgICAgICBpZiAoaGFuZGxlcnMpIHtcbiAgICAgICAgICBoYW5kbGVycy5kZWxldGUocmVnaXN0cmF0aW9uKVxuICAgICAgICAgIGlmIChoYW5kbGVycy5zaXplID09PSAwKSB7XG4gICAgICAgICAgICByZWdpc3RyeS5kZWxldGUocmVnaXN0cmF0aW9uLmFjdGlvbilcbiAgICAgICAgICB9XG4gICAgICAgIH1cbiAgICAgIH1cbiAgICB9XG5cbiAgICAvLyBJbnZva2UgYWxsIGhhbmRsZXJzIGZvciBhbiBhY3Rpb25cbiAgICBjb25zdCBpbnZva2VBY3Rpb24gPSAoYWN0aW9uOiBzdHJpbmcpOiBib29sZWFuID0+IHtcbiAgICAgIGNvbnN0IHJlZ2lzdHJ5ID0gaGFuZGxlclJlZ2lzdHJ5UmVmLmN1cnJlbnRcbiAgICAgIGlmICghcmVnaXN0cnkpIHJldHVybiBmYWxzZVxuXG4gICAgICBjb25zdCBoYW5kbGVycyA9IHJlZ2lzdHJ5LmdldChhY3Rpb24pXG4gICAgICBpZiAoIWhhbmRsZXJzIHx8IGhhbmRsZXJzLnNpemUgPT09IDApIHJldHVybiBmYWxzZVxuXG4gICAgICAvLyBGaW5kIGhhbmRsZXJzIHdob3NlIGNvbnRleHQgaXMgYWN0aXZlXG4gICAgICBmb3IgKGNvbnN0IHJlZ2lzdHJhdGlvbiBvZiBoYW5kbGVycykge1xuICAgICAgICBpZiAoYWN0aXZlQ29udGV4dHMuaGFzKHJlZ2lzdHJhdGlvbi5jb250ZXh0KSkge1xuICAgICAgICAgIHJlZ2lzdHJhdGlvbi5oYW5kbGVyKClcbiAgICAgICAgICByZXR1cm4gdHJ1ZVxuICAgICAgICB9XG4gICAgICB9XG4gICAgICByZXR1cm4gZmFsc2VcbiAgICB9XG5cbiAgICByZXR1cm4ge1xuICAgICAgLy8gVXNlIHJlZiBmb3IgaW1tZWRpYXRlIGFjY2VzcyB0byBwZW5kaW5nIGNob3JkLCBhdm9pZGluZyBSZWFjdCBzdGF0ZSBkZWxheVxuICAgICAgLy8gVGhpcyBpcyBjcml0aWNhbCBmb3IgY2hvcmQgc2VxdWVuY2VzIHdoZXJlIHRoZSBzZWNvbmQga2V5IG1pZ2h0IGJlIHByZXNzZWRcbiAgICAgIC8vIGJlZm9yZSBSZWFjdCByZS1yZW5kZXJzIHdpdGggdGhlIHVwZGF0ZWQgcGVuZGluZ0Nob3JkIHN0YXRlXG4gICAgICByZXNvbHZlOiAoaW5wdXQsIGtleSwgY29udGV4dHMpID0+XG4gICAgICAgIHJlc29sdmVLZXlXaXRoQ2hvcmRTdGF0ZShcbiAgICAgICAgICBpbnB1dCxcbiAgICAgICAgICBrZXksXG4gICAgICAgICAgY29udGV4dHMsXG4gICAgICAgICAgYmluZGluZ3MsXG4gICAgICAgICAgcGVuZGluZ0Nob3JkUmVmLmN1cnJlbnQsXG4gICAgICAgICksXG4gICAgICBzZXRQZW5kaW5nQ2hvcmQsXG4gICAgICBnZXREaXNwbGF5VGV4dDogZ2V0RGlzcGxheSxcbiAgICAgIGJpbmRpbmdzLFxuICAgICAgcGVuZGluZ0Nob3JkLFxuICAgICAgYWN0aXZlQ29udGV4dHMsXG4gICAgICByZWdpc3RlckFjdGl2ZUNvbnRleHQsXG4gICAgICB1bnJlZ2lzdGVyQWN0aXZlQ29udGV4dCxcbiAgICAgIHJlZ2lzdGVySGFuZGxlcixcbiAgICAgIGludm9rZUFjdGlvbixcbiAgICB9XG4gIH0sIFtcbiAgICBiaW5kaW5ncyxcbiAgICBwZW5kaW5nQ2hvcmRSZWYsXG4gICAgcGVuZGluZ0Nob3JkLFxuICAgIHNldFBlbmRpbmdDaG9yZCxcbiAgICBhY3RpdmVDb250ZXh0cyxcbiAgICByZWdpc3RlckFjdGl2ZUNvbnRleHQsXG4gICAgdW5yZWdpc3RlckFjdGl2ZUNvbnRleHQsXG4gICAgaGFuZGxlclJlZ2lzdHJ5UmVmLFxuICBdKVxuXG4gIHJldHVybiAoXG4gICAgPEtleWJpbmRpbmdDb250ZXh0LlByb3ZpZGVyIHZhbHVlPXt2YWx1ZX0+XG4gICAgICB7Y2hpbGRyZW59XG4gICAgPC9LZXliaW5kaW5nQ29udGV4dC5Qcm92aWRlcj5cbiAgKVxufVxuXG5leHBvcnQgZnVuY3Rpb24gdXNlS2V5YmluZGluZ0NvbnRleHQoKTogS2V5YmluZGluZ0NvbnRleHRWYWx1ZSB7XG4gIGNvbnN0IGN0eCA9IHVzZUNvbnRleHQoS2V5YmluZGluZ0NvbnRleHQpXG4gIGlmICghY3R4KSB7XG4gICAgdGhyb3cgbmV3IEVycm9yKFxuICAgICAgJ3VzZUtleWJpbmRpbmdDb250ZXh0IG11c3QgYmUgdXNlZCB3aXRoaW4gS2V5YmluZGluZ1Byb3ZpZGVyJyxcbiAgICApXG4gIH1cbiAgcmV0dXJuIGN0eFxufVxuXG4vKipcbiAqIE9wdGlvbmFsIGhvb2sgdGhhdCByZXR1cm5zIHVuZGVmaW5lZCBvdXRzaWRlIG9mIEtleWJpbmRpbmdQcm92aWRlci5cbiAqIFVzZWZ1bCBmb3IgY29tcG9uZW50cyB0aGF0IG1heSByZW5kZXIgYmVmb3JlIHByb3ZpZGVyIGlzIGF2YWlsYWJsZS5cbiAqL1xuZXhwb3J0IGZ1bmN0aW9uIHVzZU9wdGlvbmFsS2V5YmluZGluZ0NvbnRleHQoKTogS2V5YmluZGluZ0NvbnRleHRWYWx1ZSB8IG51bGwge1xuICByZXR1cm4gdXNlQ29udGV4dChLZXliaW5kaW5nQ29udGV4dClcbn1cblxuLyoqXG4gKiBIb29rIHRvIHJlZ2lzdGVyIGEga2V5YmluZGluZyBjb250ZXh0IGFzIGFjdGl2ZSB3aGlsZSB0aGUgY29tcG9uZW50IGlzIG1vdW50ZWQuXG4gKlxuICogV2hlbiBhIGNvbnRleHQgaXMgcmVnaXN0ZXJlZCwgaXRzIGtleWJpbmRpbmdzIHRha2UgcHJlY2VkZW5jZSBvdmVyIEdsb2JhbCBiaW5kaW5ncy5cbiAqIFRoaXMgYWxsb3dzIGNvbnRleHQtc3BlY2lmaWMgYmluZGluZ3MgKGxpa2UgVGhlbWVQaWNrZXIncyBjdHJsK3QpIHRvIG92ZXJyaWRlXG4gKiBnbG9iYWwgYmluZGluZ3MgKGxpa2UgdGhlIHRvZG8gdG9nZ2xlKSB3aGVuIHRoZSBjb250ZXh0IGlzIGFjdGl2ZS5cbiAqXG4gKiBAZXhhbXBsZVxuICogYGBgdHN4XG4gKiBmdW5jdGlvbiBUaGVtZVBpY2tlcigpIHtcbiAqICAgdXNlUmVnaXN0ZXJLZXliaW5kaW5nQ29udGV4dCgnVGhlbWVQaWNrZXInKVxuICogICAvLyBOb3cgVGhlbWVQaWNrZXIncyBjdHJsK3QgYmluZGluZyB0YWtlcyBwcmVjZWRlbmNlIG92ZXIgR2xvYmFsXG4gKiB9XG4gKiBgYGBcbiAqL1xuZXhwb3J0IGZ1bmN0aW9uIHVzZVJlZ2lzdGVyS2V5YmluZGluZ0NvbnRleHQoXG4gIGNvbnRleHQ6IEtleWJpbmRpbmdDb250ZXh0TmFtZSxcbiAgaXNBY3RpdmU6IGJvb2xlYW4gPSB0cnVlLFxuKTogdm9pZCB7XG4gIGNvbnN0IGtleWJpbmRpbmdDb250ZXh0ID0gdXNlT3B0aW9uYWxLZXliaW5kaW5nQ29udGV4dCgpXG5cbiAgdXNlTGF5b3V0RWZmZWN0KCgpID0+IHtcbiAgICBpZiAoIWtleWJpbmRpbmdDb250ZXh0IHx8ICFpc0FjdGl2ZSkgcmV0dXJuXG5cbiAgICBrZXliaW5kaW5nQ29udGV4dC5yZWdpc3RlckFjdGl2ZUNvbnRleHQoY29udGV4dClcbiAgICByZXR1cm4gKCkgPT4ge1xuICAgICAga2V5YmluZGluZ0NvbnRleHQudW5yZWdpc3RlckFjdGl2ZUNvbnRleHQoY29udGV4dClcbiAgICB9XG4gIH0sIFtjb250ZXh0LCBrZXliaW5kaW5nQ29udGV4dCwgaXNBY3RpdmVdKVxufVxuIl0sIm1hcHBpbmdzIjoiO0FBQUEsT0FBT0EsS0FBSyxJQUNWQyxhQUFhLEVBQ2IsS0FBS0MsU0FBUyxFQUNkQyxVQUFVLEVBQ1ZDLGVBQWUsRUFDZkMsT0FBTyxRQUNGLE9BQU87QUFDZCxjQUFjQyxHQUFHLFFBQVEsV0FBVztBQUNwQyxTQUNFLEtBQUtDLGtCQUFrQixFQUN2QkMscUJBQXFCLEVBQ3JCQyx3QkFBd0IsUUFDbkIsZUFBZTtBQUN0QixjQUNFQyxxQkFBcUIs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
|
src/keybindings/KeybindingProviderSetup.tsx
ADDED
|
@@ -0,0 +1,308 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { c as _c } from "react/compiler-runtime";
|
| 2 |
+
/**
|
| 3 |
+
* Setup utilities for integrating KeybindingProvider into the app.
|
| 4 |
+
*
|
| 5 |
+
* This file provides the bindings and a composed provider that can be
|
| 6 |
+
* added to the app's component tree. It loads both default bindings and
|
| 7 |
+
* user-defined bindings from ~/.claude/keybindings.json, with hot-reload
|
| 8 |
+
* support when the file changes.
|
| 9 |
+
*/
|
| 10 |
+
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
| 11 |
+
import { useNotifications } from '../context/notifications.js';
|
| 12 |
+
import type { InputEvent } from '../ink/events/input-event.js';
|
| 13 |
+
// ChordInterceptor intentionally uses useInput to intercept all keystrokes before
|
| 14 |
+
// other handlers process them - this is required for chord sequence support
|
| 15 |
+
// eslint-disable-next-line custom-rules/prefer-use-keybindings
|
| 16 |
+
import { type Key, useInput } from '../ink.js';
|
| 17 |
+
import { count } from '../utils/array.js';
|
| 18 |
+
import { logForDebugging } from '../utils/debug.js';
|
| 19 |
+
import { plural } from '../utils/stringUtils.js';
|
| 20 |
+
import { KeybindingProvider } from './KeybindingContext.js';
|
| 21 |
+
import { initializeKeybindingWatcher, type KeybindingsLoadResult, loadKeybindingsSyncWithWarnings, subscribeToKeybindingChanges } from './loadUserBindings.js';
|
| 22 |
+
import { resolveKeyWithChordState } from './resolver.js';
|
| 23 |
+
import type { KeybindingContextName, ParsedBinding, ParsedKeystroke } from './types.js';
|
| 24 |
+
import type { KeybindingWarning } from './validate.js';
|
| 25 |
+
|
| 26 |
+
/**
|
| 27 |
+
* Timeout for chord sequences in milliseconds.
|
| 28 |
+
* If the user doesn't complete the chord within this time, it's cancelled.
|
| 29 |
+
*/
|
| 30 |
+
const CHORD_TIMEOUT_MS = 1000;
|
| 31 |
+
type Props = {
|
| 32 |
+
children: React.ReactNode;
|
| 33 |
+
};
|
| 34 |
+
|
| 35 |
+
/**
|
| 36 |
+
* Keybinding provider with default + user bindings and hot-reload support.
|
| 37 |
+
*
|
| 38 |
+
* Usage: Wrap your app with this provider to enable keybinding support.
|
| 39 |
+
*
|
| 40 |
+
* ```tsx
|
| 41 |
+
* <AppStateProvider>
|
| 42 |
+
* <KeybindingSetup>
|
| 43 |
+
* <REPL ... />
|
| 44 |
+
* </KeybindingSetup>
|
| 45 |
+
* </AppStateProvider>
|
| 46 |
+
* ```
|
| 47 |
+
*
|
| 48 |
+
* Features:
|
| 49 |
+
* - Loads default bindings from code
|
| 50 |
+
* - Merges with user bindings from ~/.claude/keybindings.json
|
| 51 |
+
* - Watches for file changes and reloads automatically (hot-reload)
|
| 52 |
+
* - User bindings override defaults (later entries win)
|
| 53 |
+
* - Chord support with automatic timeout
|
| 54 |
+
*/
|
| 55 |
+
/**
|
| 56 |
+
* Display keybinding warnings to the user via notifications.
|
| 57 |
+
* Shows a brief message pointing to /doctor for details.
|
| 58 |
+
*/
|
| 59 |
+
function useKeybindingWarnings(warnings, isReload) {
|
| 60 |
+
const $ = _c(9);
|
| 61 |
+
const {
|
| 62 |
+
addNotification,
|
| 63 |
+
removeNotification
|
| 64 |
+
} = useNotifications();
|
| 65 |
+
let t0;
|
| 66 |
+
if ($[0] !== addNotification || $[1] !== removeNotification || $[2] !== warnings) {
|
| 67 |
+
t0 = () => {
|
| 68 |
+
if (warnings.length === 0) {
|
| 69 |
+
removeNotification("keybinding-config-warning");
|
| 70 |
+
return;
|
| 71 |
+
}
|
| 72 |
+
const errorCount = count(warnings, _temp);
|
| 73 |
+
const warnCount = count(warnings, _temp2);
|
| 74 |
+
let message;
|
| 75 |
+
if (errorCount > 0 && warnCount > 0) {
|
| 76 |
+
message = `Found ${errorCount} keybinding ${plural(errorCount, "error")} and ${warnCount} ${plural(warnCount, "warning")}`;
|
| 77 |
+
} else {
|
| 78 |
+
if (errorCount > 0) {
|
| 79 |
+
message = `Found ${errorCount} keybinding ${plural(errorCount, "error")}`;
|
| 80 |
+
} else {
|
| 81 |
+
message = `Found ${warnCount} keybinding ${plural(warnCount, "warning")}`;
|
| 82 |
+
}
|
| 83 |
+
}
|
| 84 |
+
message = message + " \xB7 /doctor for details";
|
| 85 |
+
addNotification({
|
| 86 |
+
key: "keybinding-config-warning",
|
| 87 |
+
text: message,
|
| 88 |
+
color: errorCount > 0 ? "error" : "warning",
|
| 89 |
+
priority: errorCount > 0 ? "immediate" : "high",
|
| 90 |
+
timeoutMs: 60000
|
| 91 |
+
});
|
| 92 |
+
};
|
| 93 |
+
$[0] = addNotification;
|
| 94 |
+
$[1] = removeNotification;
|
| 95 |
+
$[2] = warnings;
|
| 96 |
+
$[3] = t0;
|
| 97 |
+
} else {
|
| 98 |
+
t0 = $[3];
|
| 99 |
+
}
|
| 100 |
+
let t1;
|
| 101 |
+
if ($[4] !== addNotification || $[5] !== isReload || $[6] !== removeNotification || $[7] !== warnings) {
|
| 102 |
+
t1 = [warnings, isReload, addNotification, removeNotification];
|
| 103 |
+
$[4] = addNotification;
|
| 104 |
+
$[5] = isReload;
|
| 105 |
+
$[6] = removeNotification;
|
| 106 |
+
$[7] = warnings;
|
| 107 |
+
$[8] = t1;
|
| 108 |
+
} else {
|
| 109 |
+
t1 = $[8];
|
| 110 |
+
}
|
| 111 |
+
useEffect(t0, t1);
|
| 112 |
+
}
|
| 113 |
+
function _temp2(w_0) {
|
| 114 |
+
return w_0.severity === "warning";
|
| 115 |
+
}
|
| 116 |
+
function _temp(w) {
|
| 117 |
+
return w.severity === "error";
|
| 118 |
+
}
|
| 119 |
+
export function KeybindingSetup({
|
| 120 |
+
children
|
| 121 |
+
}: Props): React.ReactNode {
|
| 122 |
+
// Load bindings synchronously for initial render
|
| 123 |
+
const [{
|
| 124 |
+
bindings,
|
| 125 |
+
warnings
|
| 126 |
+
}, setLoadResult] = useState<KeybindingsLoadResult>(() => {
|
| 127 |
+
const result = loadKeybindingsSyncWithWarnings();
|
| 128 |
+
logForDebugging(`[keybindings] KeybindingSetup initialized with ${result.bindings.length} bindings, ${result.warnings.length} warnings`);
|
| 129 |
+
return result;
|
| 130 |
+
});
|
| 131 |
+
|
| 132 |
+
// Track if this is a reload (not initial load)
|
| 133 |
+
const [isReload, setIsReload] = useState(false);
|
| 134 |
+
|
| 135 |
+
// Display warnings via notifications
|
| 136 |
+
useKeybindingWarnings(warnings, isReload);
|
| 137 |
+
|
| 138 |
+
// Chord state management - use ref for immediate access, state for re-renders
|
| 139 |
+
// The ref is used by resolve() to get the current value without waiting for re-render
|
| 140 |
+
// The state is used to trigger re-renders when needed (e.g., for UI updates)
|
| 141 |
+
const pendingChordRef = useRef<ParsedKeystroke[] | null>(null);
|
| 142 |
+
const [pendingChord, setPendingChordState] = useState<ParsedKeystroke[] | null>(null);
|
| 143 |
+
const chordTimeoutRef = useRef<NodeJS.Timeout | null>(null);
|
| 144 |
+
|
| 145 |
+
// Handler registry for action callbacks (used by ChordInterceptor to invoke handlers)
|
| 146 |
+
const handlerRegistryRef = useRef(new Map<string, Set<{
|
| 147 |
+
action: string;
|
| 148 |
+
context: KeybindingContextName;
|
| 149 |
+
handler: () => void;
|
| 150 |
+
}>>());
|
| 151 |
+
|
| 152 |
+
// Active context tracking for keybinding priority resolution
|
| 153 |
+
// Using a ref instead of state for synchronous updates - input handlers need
|
| 154 |
+
// to see the current value immediately, not after a React render cycle.
|
| 155 |
+
const activeContextsRef = useRef<Set<KeybindingContextName>>(new Set());
|
| 156 |
+
const registerActiveContext = useCallback((context: KeybindingContextName) => {
|
| 157 |
+
activeContextsRef.current.add(context);
|
| 158 |
+
}, []);
|
| 159 |
+
const unregisterActiveContext = useCallback((context_0: KeybindingContextName) => {
|
| 160 |
+
activeContextsRef.current.delete(context_0);
|
| 161 |
+
}, []);
|
| 162 |
+
|
| 163 |
+
// Clear chord timeout when component unmounts or chord changes
|
| 164 |
+
const clearChordTimeout = useCallback(() => {
|
| 165 |
+
if (chordTimeoutRef.current) {
|
| 166 |
+
clearTimeout(chordTimeoutRef.current);
|
| 167 |
+
chordTimeoutRef.current = null;
|
| 168 |
+
}
|
| 169 |
+
}, []);
|
| 170 |
+
|
| 171 |
+
// Wrapper for setPendingChord that manages timeout and syncs ref+state
|
| 172 |
+
const setPendingChord = useCallback((pending: ParsedKeystroke[] | null) => {
|
| 173 |
+
clearChordTimeout();
|
| 174 |
+
if (pending !== null) {
|
| 175 |
+
// Set timeout to cancel chord if not completed
|
| 176 |
+
chordTimeoutRef.current = setTimeout((pendingChordRef_0, setPendingChordState_0) => {
|
| 177 |
+
logForDebugging('[keybindings] Chord timeout - cancelling');
|
| 178 |
+
pendingChordRef_0.current = null;
|
| 179 |
+
setPendingChordState_0(null);
|
| 180 |
+
}, CHORD_TIMEOUT_MS, pendingChordRef, setPendingChordState);
|
| 181 |
+
}
|
| 182 |
+
|
| 183 |
+
// Update ref immediately for synchronous access in resolve()
|
| 184 |
+
pendingChordRef.current = pending;
|
| 185 |
+
// Update state to trigger re-renders for UI updates
|
| 186 |
+
setPendingChordState(pending);
|
| 187 |
+
}, [clearChordTimeout]);
|
| 188 |
+
useEffect(() => {
|
| 189 |
+
// Initialize file watcher (idempotent - only runs once)
|
| 190 |
+
void initializeKeybindingWatcher();
|
| 191 |
+
|
| 192 |
+
// Subscribe to changes
|
| 193 |
+
const unsubscribe = subscribeToKeybindingChanges(result_0 => {
|
| 194 |
+
// Any callback invocation is a reload since initial load happens
|
| 195 |
+
// synchronously in useState, not via this subscription
|
| 196 |
+
setIsReload(true);
|
| 197 |
+
setLoadResult(result_0);
|
| 198 |
+
logForDebugging(`[keybindings] Reloaded: ${result_0.bindings.length} bindings, ${result_0.warnings.length} warnings`);
|
| 199 |
+
});
|
| 200 |
+
return () => {
|
| 201 |
+
unsubscribe();
|
| 202 |
+
clearChordTimeout();
|
| 203 |
+
};
|
| 204 |
+
}, [clearChordTimeout]);
|
| 205 |
+
return <KeybindingProvider bindings={bindings} pendingChordRef={pendingChordRef} pendingChord={pendingChord} setPendingChord={setPendingChord} activeContexts={activeContextsRef.current} registerActiveContext={registerActiveContext} unregisterActiveContext={unregisterActiveContext} handlerRegistryRef={handlerRegistryRef}>
|
| 206 |
+
<ChordInterceptor bindings={bindings} pendingChordRef={pendingChordRef} setPendingChord={setPendingChord} activeContexts={activeContextsRef.current} handlerRegistryRef={handlerRegistryRef} />
|
| 207 |
+
{children}
|
| 208 |
+
</KeybindingProvider>;
|
| 209 |
+
}
|
| 210 |
+
|
| 211 |
+
/**
|
| 212 |
+
* Global chord interceptor that registers useInput FIRST (before children).
|
| 213 |
+
*
|
| 214 |
+
* This component intercepts keystrokes that are part of chord sequences and
|
| 215 |
+
* stops propagation before other handlers (like PromptInput) can see them.
|
| 216 |
+
*
|
| 217 |
+
* Without this, the second key of a chord (e.g., 'r' in "ctrl+c r") would be
|
| 218 |
+
* captured by PromptInput and added to the input field before the keybinding
|
| 219 |
+
* system could recognize it as completing a chord.
|
| 220 |
+
*/
|
| 221 |
+
type HandlerRegistration = {
|
| 222 |
+
action: string;
|
| 223 |
+
context: KeybindingContextName;
|
| 224 |
+
handler: () => void;
|
| 225 |
+
};
|
| 226 |
+
function ChordInterceptor(t0) {
|
| 227 |
+
const $ = _c(6);
|
| 228 |
+
const {
|
| 229 |
+
bindings,
|
| 230 |
+
pendingChordRef,
|
| 231 |
+
setPendingChord,
|
| 232 |
+
activeContexts,
|
| 233 |
+
handlerRegistryRef
|
| 234 |
+
} = t0;
|
| 235 |
+
let t1;
|
| 236 |
+
if ($[0] !== activeContexts || $[1] !== bindings || $[2] !== handlerRegistryRef || $[3] !== pendingChordRef || $[4] !== setPendingChord) {
|
| 237 |
+
t1 = (input, key, event) => {
|
| 238 |
+
if ((key.wheelUp || key.wheelDown) && pendingChordRef.current === null) {
|
| 239 |
+
return;
|
| 240 |
+
}
|
| 241 |
+
const registry = handlerRegistryRef.current;
|
| 242 |
+
const handlerContexts = new Set();
|
| 243 |
+
if (registry) {
|
| 244 |
+
for (const handlers of registry.values()) {
|
| 245 |
+
for (const registration of handlers) {
|
| 246 |
+
handlerContexts.add(registration.context);
|
| 247 |
+
}
|
| 248 |
+
}
|
| 249 |
+
}
|
| 250 |
+
const contexts = [...handlerContexts, ...activeContexts, "Global"];
|
| 251 |
+
const wasInChord = pendingChordRef.current !== null;
|
| 252 |
+
const result = resolveKeyWithChordState(input, key, contexts, bindings, pendingChordRef.current);
|
| 253 |
+
bb23: switch (result.type) {
|
| 254 |
+
case "chord_started":
|
| 255 |
+
{
|
| 256 |
+
setPendingChord(result.pending);
|
| 257 |
+
event.stopImmediatePropagation();
|
| 258 |
+
break bb23;
|
| 259 |
+
}
|
| 260 |
+
case "match":
|
| 261 |
+
{
|
| 262 |
+
setPendingChord(null);
|
| 263 |
+
if (wasInChord) {
|
| 264 |
+
const contextsSet = new Set(contexts);
|
| 265 |
+
if (registry) {
|
| 266 |
+
const handlers_0 = registry.get(result.action);
|
| 267 |
+
if (handlers_0 && handlers_0.size > 0) {
|
| 268 |
+
for (const registration_0 of handlers_0) {
|
| 269 |
+
if (contextsSet.has(registration_0.context)) {
|
| 270 |
+
registration_0.handler();
|
| 271 |
+
event.stopImmediatePropagation();
|
| 272 |
+
break;
|
| 273 |
+
}
|
| 274 |
+
}
|
| 275 |
+
}
|
| 276 |
+
}
|
| 277 |
+
}
|
| 278 |
+
break bb23;
|
| 279 |
+
}
|
| 280 |
+
case "chord_cancelled":
|
| 281 |
+
{
|
| 282 |
+
setPendingChord(null);
|
| 283 |
+
event.stopImmediatePropagation();
|
| 284 |
+
break bb23;
|
| 285 |
+
}
|
| 286 |
+
case "unbound":
|
| 287 |
+
{
|
| 288 |
+
setPendingChord(null);
|
| 289 |
+
event.stopImmediatePropagation();
|
| 290 |
+
break bb23;
|
| 291 |
+
}
|
| 292 |
+
case "none":
|
| 293 |
+
}
|
| 294 |
+
};
|
| 295 |
+
$[0] = activeContexts;
|
| 296 |
+
$[1] = bindings;
|
| 297 |
+
$[2] = handlerRegistryRef;
|
| 298 |
+
$[3] = pendingChordRef;
|
| 299 |
+
$[4] = setPendingChord;
|
| 300 |
+
$[5] = t1;
|
| 301 |
+
} else {
|
| 302 |
+
t1 = $[5];
|
| 303 |
+
}
|
| 304 |
+
const handleInput = t1;
|
| 305 |
+
useInput(handleInput);
|
| 306 |
+
return null;
|
| 307 |
+
}
|
| 308 |
+
//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJuYW1lcyI6WyJSZWFjdCIsInVzZUNhbGxiYWNrIiwidXNlRWZmZWN0IiwidXNlUmVmIiwidXNlU3RhdGUiLCJ1c2VOb3RpZmljYXRpb25zIiwiSW5wdXRFdmVudCIsIktleSIsInVzZUlucHV0IiwiY291bnQiLCJsb2dGb3JEZWJ1Z2dpbmciLCJwbHVyYWwiLCJLZXliaW5kaW5nUHJvdmlkZXIiLCJpbml0aWFsaXplS2V5YmluZGluZ1dhdGNoZXIiLCJLZXliaW5kaW5nc0xvYWRSZXN1bHQiLCJsb2FkS2V5YmluZGluZ3NTeW5jV2l0aFdhcm5pbmdzIiwic3Vic2NyaWJlVG9LZXliaW5kaW5nQ2hhbmdlcyIsInJlc29sdmVLZXlXaXRoQ2hvcmRTdGF0ZSIsIktleWJpbmRpbmdDb250ZXh0TmFtZSIsIlBhcnNlZEJpbmRpbmciLCJQYXJzZWRLZXlzdHJva2UiLCJLZXliaW5kaW5nV2FybmluZyIsIkNIT1JEX1RJTUVPVVRfTVMiLCJQcm9wcyIsImNoaWxkcmVuIiwiUmVhY3ROb2RlIiwidXNlS2V5YmluZGluZ1dhcm5pbmdzIiwid2FybmluZ3MiLCJpc1JlbG9hZCIsIiQiLCJfYyIsImFkZE5vdGlmaWNhdGlvbiIsInJlbW92ZU5vdGlmaWNhdGlvbiIsInQwIiwibGVuZ3RoIiwiZXJyb3JDb3VudCIsIl90ZW1wIiwid2FybkNvdW50IiwiX3RlbXAyIiwibWVzc2FnZSIsImtleSIsInRleHQiLCJjb2xvciIsInByaW9yaXR5IiwidGltZW91dE1zIiwidDEiLCJ3XzAiLCJ3Iiwic2V2ZXJpdHkiLCJLZXliaW5kaW5nU2V0dXAiLCJiaW5kaW5ncyIsInNldExvYWRSZXN1bHQiLCJyZXN1bHQiLCJzZXRJc1JlbG9hZCIsInBlbmRpbmdDaG9yZFJlZiIsInBlbmRpbmdDaG9yZCIsInNldFBlbmRpbmdDaG9yZFN0YXRlIiwiY2hvcmRUaW1lb3V0UmVmIiwiTm9kZUpTIiwiVGltZW91dCIsImhhbmRsZXJSZWdpc3RyeVJlZiIsIk1hcCIsIlNldCIsImFjdGlvbiIsImNvbnRleHQiLCJoYW5kbGVyIiwiYWN0aXZlQ29udGV4dHNSZWYiLCJyZWdpc3RlckFjdGl2ZUNvbnRleHQiLCJjdXJyZW50IiwiYWRkIiwidW5yZWdpc3RlckFjdGl2ZUNvbnRleHQiLCJkZWxldGUiLCJjbGVhckNob3JkVGltZW91dCIsImNsZWFyVGltZW91dCIsInNldFBlbmRpbmdDaG9yZCIsInBlbmRpbmciLCJzZXRUaW1lb3V0IiwidW5zdWJzY3JpYmUiLCJIYW5kbGVyUmVnaXN0cmF0aW9uIiwiQ2hvcmRJbnRlcmNlcHRvciIsImFjdGl2ZUNvbnRleHRzIiwiaW5wdXQiLCJldmVudCIsIndoZWVsVXAiLCJ3aGVlbERvd24iLCJyZWdpc3RyeSIsImhhbmRsZXJDb250ZXh0cyIsImhhbmRsZXJzIiwidmFsdWVzIiwicmVnaXN0cmF0aW9uIiwiY29udGV4dHMiLCJ3YXNJbkNob3JkIiwiYmIyMyIsInR5cGUiLCJzdG9wSW1tZWRpYXRlUHJvcGFnYXRpb24iLCJjb250ZXh0c1NldCIsImhhbmRsZXJzXzAiLCJnZXQiLCJzaXplIiwicmVnaXN0cmF0aW9uXzAiLCJoYXMiLCJoYW5kbGVJbnB1dCJdLCJzb3VyY2VzIjpbIktleWJpbmRpbmdQcm92aWRlclNldHVwLnRzeCJdLCJzb3VyY2VzQ29udGVudCI6WyIvKipcbiAqIFNldHVwIHV0aWxpdGllcyBmb3IgaW50ZWdyYXRpbmcgS2V5YmluZGluZ1Byb3ZpZGVyIGludG8gdGhlIGFwcC5cbiAqXG4gKiBUaGlzIGZpbGUgcHJvdmlkZXMgdGhlIGJpbmRpbmdzIGFuZCBhIGNvbXBvc2VkIHByb3ZpZGVyIHRoYXQgY2FuIGJlXG4gKiBhZGRlZCB0byB0aGUgYXBwJ3MgY29tcG9uZW50IHRyZWUuIEl0IGxvYWRzIGJvdGggZGVmYXVsdCBiaW5kaW5ncyBhbmRcbiAqIHVzZXItZGVmaW5lZCBiaW5kaW5ncyBmcm9tIH4vLmNsYXVkZS9rZXliaW5kaW5ncy5qc29uLCB3aXRoIGhvdC1yZWxvYWRcbiAqIHN1cHBvcnQgd2hlbiB0aGUgZmlsZSBjaGFuZ2VzLlxuICovXG5pbXBvcnQgUmVhY3QsIHsgdXNlQ2FsbGJhY2ssIHVzZUVmZmVjdCwgdXNlUmVmLCB1c2VTdGF0ZSB9IGZyb20gJ3JlYWN0J1xuaW1wb3J0IHsgdXNlTm90aWZpY2F0aW9ucyB9IGZyb20gJy4uL2NvbnRleHQvbm90aWZpY2F0aW9ucy5qcydcbmltcG9ydCB0eXBlIHsgSW5wdXRFdmVudCB9IGZyb20gJy4uL2luay9ldmVudHMvaW5wdXQtZXZlbnQuanMnXG4vLyBDaG9yZEludGVyY2VwdG9yIGludGVudGlvbmFsbHkgdXNlcyB1c2VJbnB1dCB0byBpbnRlcmNlcHQgYWxsIGtleXN0cm9rZXMgYmVmb3JlXG4vLyBvdGhlciBoYW5kbGVycyBwcm9jZXNzIHRoZW0gLSB0aGlzIGlzIHJlcXVpcmVkIGZvciBjaG9yZCBzZXF1ZW5jZSBzdXBwb3J0XG4vLyBlc2xpbnQtZGlzYWJsZS1uZXh0LWxpbmUgY3VzdG9tLXJ1bGVzL3ByZWZlci11c2Uta2V5YmluZGluZ3NcbmltcG9ydCB7IHR5cGUgS2V5LCB1c2VJbnB1dCB9IGZyb20gJy4uL2luay5qcydcbmltcG9ydCB7IGNvdW50IH0gZnJvbSAnLi4vdXRpbHMvYXJyYXkuanMnXG5pbXBvcnQgeyBsb2dGb3JEZWJ1Z2dpbmcgfSBmcm9tICcuLi91dGlscy9kZWJ1Zy5qcydcbmltcG9ydCB7IHBsdXJhbCB9IGZyb20gJy4uL3V0aWxzL3N0cmluZ1V0aWxzLmpzJ1xuaW1wb3J0IHsgS2V5YmluZGluZ1Byb3ZpZGVyIH0gZnJvbSAnLi9LZXliaW5kaW5nQ29udGV4dC5qcydcbmltcG9ydCB7XG4gIGluaXRpYWxpemVLZXliaW5kaW5nV2F0Y2hlcixcbiAgdHlwZSBLZXliaW5kaW5nc0xvYWRSZXN1bHQsXG4gIGxvYWRLZXliaW5kaW5nc1N5bmNXaXRoV2FybmluZ3MsXG4gIHN1YnNjcmliZVRvS2V5YmluZGluZ0NoYW5nZXMsXG59IGZyb20gJy4vbG9hZFVzZXJCaW5kaW5ncy5qcydcbmltcG9ydCB7IHJlc29sdmVLZXlXaXRoQ2hvcmRTdGF0ZSB9IGZyb20gJy4vcmVzb2x2ZXIuanMnXG5pbXBvcnQgdHlwZSB7XG4gIEtleWJpbmRpbmdDb250ZXh0TmFtZSxcbiAgUGFyc2VkQmluZGluZyxcbiAgUGFyc2VkS2V5c3Ryb2tlLFxufSBmcm9tICcuL3R5cGVzLmpzJ1xuaW1wb3J0IHR5cGUgeyBLZXliaW5kaW5nV2FybmluZyB9IGZyb20gJy4vdmFsaWRhdGUuanMnXG5cbi8qKlxuICogVGltZW91dCBmb3IgY2hvcmQgc2VxdWVuY2VzIGluIG1pbGxpc2Vjb25kcy5cbiAqIElmIHRoZSB1c2VyIGRvZXNuJ3QgY29tcGxldGUgdGhlIGNob3JkIHdpdGhpbiB0aGlzIHRpbWUsIGl0J3MgY2FuY2VsbGVkLlxuICovXG5jb25zdCBDSE9SRF9USU1FT1VUX01TID0gMTAwMFxuXG50eXBlIFByb3BzID0ge1xuICBjaGlsZHJlbjogUmVhY3QuUmVhY3ROb2RlXG59XG5cbi8qKlxuICogS2V5YmluZGluZyBwcm92aWRlciB3aXRoIGRlZmF1bHQgKyB1c2VyIGJpbmRpbmdzIGFuZCBob3QtcmVsb2FkIHN1cHBvcnQuXG4gKlxuICogVXNhZ2U6IFdyYXAgeW91ciBhcHAgd2l0aCB0aGlzIHByb3ZpZGVyIHRvIGVuYWJsZSBrZXliaW5kaW5nIHN1cHBvcnQuXG4gKlxuICogYGBgdHN4XG4gKiA8QXBwU3RhdGVQcm92aWRlcj5cbiAqICAgPEtleWJpbmRpbmdTZXR1cD5cbiAqICAgICA8UkVQTCAuLi4gLz5cbiAqICAgPC9LZXliaW5kaW5nU2V0dXA+XG4gKiA8L0FwcFN0YXRlUHJvdmlkZXI+XG4gKiBgYGBcbiAqXG4gKiBGZWF0dXJlczpcbiAqIC0gTG9hZHMgZGVmYXVsdCBiaW5kaW5ncyBmcm9tIGNvZGVcbiAqIC0gTWVyZ2VzIHdpdGggdXNlciBiaW5kaW5ncyBmcm9tIH4vLmNsYXVkZS9rZXliaW5kaW5ncy5qc29uXG4gKiAtIFdhdGNoZXMgZm9yIGZpbGUgY2hhbmdlcyBhbmQgcmVsb2FkcyBhdXRvbWF0aWNhbGx5IChob3QtcmVsb2FkKVxuICogLSBVc2VyIGJpbmRpbmdzIG92ZXJyaWRlIGRlZmF1bHRzIChsYXRlciBlbnRyaWVzIHdpbilcbiAqIC0gQ2hvcmQgc3VwcG9ydCB3aXRoIGF1dG9tYXRpYyB0aW1lb3V0XG4gKi9cbi8qKlxuICogRGlzcGxheSBrZXliaW5kaW5nIHdhcm5pbmdzIHRvIHRoZSB1c2VyIHZpYSBub3RpZmljYXRpb25zLlxuICogU2hvd3MgYSBicmllZiBtZXNzYWdlIHBvaW50aW5nIHRvIC9kb2N0b3IgZm9yIGRldGFpbHMuXG4gKi9cbmZ1bmN0aW9uIHVzZUtleWJpbmRpbmdXYXJuaW5ncyhcbiAgd2FybmluZ3M6IEtleWJpbmRpbmdXYXJuaW5nW10sXG4gIGlzUmVsb2FkOiBib29sZWFuLFxuKTogdm9pZCB7XG4gIGNvbnN0IHsgYWRkTm90aWZpY2F0aW9uLCByZW1vdmVOb3RpZmljYXRpb24gfSA9IHVzZU5vdGlmaWNhdGlvbnMoKVxuXG4gIHVzZUVmZmVjdCgoKSA9PiB7XG4gICAgY29uc3Qgbm90aWZpY2F0aW9uS2V5ID0gJ2tleWJpbmRpbmctY29uZmlnLXdhcm5pbmcnXG5cbiAgICBpZiAod2FybmluZ3MubGVuZ3RoID09PSAwKSB7XG4gICAgICByZW1vdmVOb3RpZmljYXRpb24obm90aWZpY2F0aW9uS2V5KVxuICAgICAgcmV0dXJuXG4gICAgfVxuXG4gICAgY29uc3QgZXJyb3JDb3VudCA9IGNvdW50KHdhcm5pbmdzLCB3ID0+IHcuc2V2ZXJpdHkgPT09ICdlcnJvcicpXG4gICAgY29uc3Qgd2FybkNvdW50ID0gY291bnQod2FybmluZ3MsIHcgPT4gdy5zZXZlcml0eSA9PT0gJ3dhcm5pbmcnKVxuXG4gICAgbGV0IG1lc3NhZ2U6IHN0cmluZ1xuICAgIGlmIChlcnJvckNvdW50ID4gMCAmJiB3YXJuQ291bnQgPiAwKSB7XG4gICAgICBtZXNzYWdlID0gYEZvdW5kICR7ZXJyb3JDb3VudH0ga2V5YmluZGluZyAke3BsdXJhbChlcnJvckNvdW50LCAnZXJyb3InKX0gYW5kICR7d2FybkNvdW50fSAke3BsdXJhbCh3YXJuQ291bnQsICd3YXJuaW5nJyl9YFxuICAgIH0gZWxzZSBpZiAoZXJyb3JDb3VudCA+IDApIHtcbiAgICAgIG1lc3NhZ2UgPSBgRm91bmQgJHtlcnJvckNvdW50fSBrZXliaW5kaW5nICR7cGx1cmFsKGVycm9yQ291bnQsICdlcnJvcicpfWBcbiAgICB9IGVsc2Uge1xuICAgICAgbWVzc2FnZSA9IGBGb3VuZCAke3dhcm5Db3VudH0ga2V5YmluZGluZyAke3BsdXJhbCh3YXJuQ291bnQsICd3YXJuaW5nJyl9YFxuICAgIH1cbiAgICBtZXNzYWdlICs9ICcgwrcgL2RvY3RvciBmb3IgZGV0YWlscydcblxuICAgIGFkZE5vdGlmaWNhdGlvbih7XG4gICAgICBrZXk6IG5vdGlmaWNhdGlvbktleSxcbiAgICAgIHRleHQ6IG1lc3NhZ2UsXG4gICAgICBjb2xvcjogZXJyb3JDb3VudCA+IDAgPyAnZXJyb3InIDogJ3dhcm5pbmcnLFxuICAgICAgcHJpb3JpdHk6IGVycm9yQ291bnQgPiAwID8gJ2ltbWVkaWF0ZScgOiAnaGlnaCcsXG4gICAgICAvLyBLZWVwIHZpc2libGUgZm9yIDYwIHNlY29uZHMgbGlrZSBzZXR0aW5ncyBlcnJvcnNcbiAgICAgIHRpbWVvdXRNczogNjAwMDAsXG4gICAgfSlcbiAgfSwgW3dhcm5pbmdzLCBpc1JlbG9hZCwgYWRkTm90aWZpY2F0aW9uLCByZW1vdmVOb3RpZmljYXRpb25dKVxufVxuXG5leHBvcnQgZnVuY3Rpb24gS2V5YmluZGluZ1NldHVwKHsgY2hpbGRyZW4gfTogUHJvcHMpOiBSZWFjdC5SZWFjdE5vZGUge1xuICAvLyBMb2FkIGJpbmRpbmdzIHN5bmNocm9ub3VzbHkgZm9yIGluaXRpYWwgcmVuZGVyXG4gIGNvbnN0IFt7IGJpbmRpbmdzLCB3YXJuaW5ncyB9LCBzZXRMb2FkUmVzdWx0XSA9XG4gICAgdXNlU3RhdGU8S2V5YmluZGluZ3NMb2FkUmVzdWx0PigoKSA9PiB7XG4gICAgICBjb25zdCByZXN1bHQgPSBsb2FkS2V5YmluZGluZ3NTeW5jV2l0aFdhcm5pbmdzKClcbiAgICAgIGxvZ0ZvckRlYnVnZ2luZyhcbiAgICAgICAgYFtrZXliaW5kaW5nc10gS2V5YmluZGluZ1NldHVwIGluaXRpYWxpemVkIHdpdGggJHtyZXN1bHQuYmluZGluZ3MubGVuZ3RofSBiaW5kaW5ncywgJHtyZXN1bHQud2FybmluZ3MubGVuZ3RofSB3YXJuaW5nc2AsXG4gICAgICApXG4gICAgICByZXR1cm4gcmVzdWx0XG4gICAgfSlcblxuICAvLyBUcmFjayBpZiB0aGlzIGlzIGEgcmVsb2FkIChub3QgaW5pdGlhbCBsb2FkKVxuICBjb25zdCBbaXNSZWxvYWQsIHNldElzUmVsb2FkXSA9IHVzZVN0YXRlKGZhbHNlKVxuXG4gIC8vIERpc3BsYXkgd2FybmluZ3MgdmlhIG5vdGlmaWNhdGlvbnNcbiAgdXNlS2V5YmluZGluZ1dhcm5pbmdzKHdhcm5pbmdzLCBpc1JlbG9hZClcblxuICAvLyBDaG9yZCBzdGF0ZSBtYW5hZ2VtZW50IC0gdXNlIHJlZiBmb3IgaW1tZWRpYXRlIGFjY2Vzcywgc3RhdGUgZm9yIHJlLXJlbmRlcnNcbiAgLy8gVGhlIHJlZiBpcyB1c2VkIGJ5IHJlc29sdmUoKSB0byBnZXQgdGhlIGN1cnJlbnQgdmFsdWUgd2l0aG91dCB3YWl0aW5nIGZvciByZS1yZW5kZXJcbiAgLy8gVGhlIHN0YXRlIGlzIHVzZWQgdG8gdHJpZ2dlciByZS1yZW5kZXJzIHdoZW4gbmVlZGVkIChlLmcuLCBmb3IgVUkgdXBkYXRlcylcbiAgY29uc3QgcGVuZGluZ0Nob3JkUmVmID0gdXNlUmVmPFBhcnNlZEtleXN0cm9rZVtdIHwgbnVsbD4obnVsbClcbiAgY29uc3QgW3BlbmRpbmdDaG9yZCwgc2V0UGVuZGluZ0Nob3JkU3RhdGVdID0gdXNlU3RhdGU8XG4gICAgUGFyc2VkS2V5c3Ryb2tlW10gfCBudWxsXG4gID4obnVsbClcbiAgY29uc3QgY2hvcmRUaW1lb3V0UmVmID0gdXNlUmVmPE5vZGVKUy5UaW1lb3V0IHwgbnVsbD4obnVsbClcblxuICAvLyBIYW5kbGVyIHJlZ2lzdHJ5IGZvciBhY3Rpb24gY2FsbGJhY2tzICh1c2VkIGJ5IENob3JkSW50ZXJjZXB0b3IgdG8gaW52b2tlIGhhbmRsZXJzKVxuICBjb25zdCBoYW5kbGVyUmVnaXN0cnlSZWYgPSB1c2VSZWYoXG4gICAgbmV3IE1hcDxcbiAgICAgIHN0cmluZyxcbiAgICAgIFNldDx7XG4gICAgICAgIGFjdGlvbjogc3RyaW5nXG4gICAgICAgIGNvbnRleHQ6IEtleWJpbmRpbmdDb250ZXh0TmFtZVxuICAgICAgICBoYW5kbGVyOiAoKSA9PiB2b2lkXG4gICAgICB9PlxuICAgID4oKSxcbiAgKVxuXG4gIC8vIEFjdGl2ZSBjb250ZXh0IHRyYWNraW5nIGZvciBrZXliaW5kaW5nIHByaW9yaXR5IHJlc29sdXRpb25cbiAgLy8gVXNpbmcgYSByZWYgaW5zdGVhZCBvZiBzdGF0ZSBmb3Igc3luY2hyb25vdXMgdXBkYXRlcyAtIGlucHV0IGhhbmRsZXJzIG5lZWRcbiAgLy8gdG8gc2VlIHRoZSBjdXJyZW50IHZhbHVlIGltbWVkaWF0ZWx5LCBub3QgYWZ0ZXIgYSBSZWFjdCByZW5kZXIgY3ljbGUuXG4gIGNvbnN0IGFjdGl2ZUNvbnRleHRzUmVmID0gdXNlUmVmPFNldDxLZXliaW5kaW5nQ29udGV4dE5hbWU+PihuZXcgU2V0KCkpXG5cbiAgY29uc3QgcmVnaXN0ZXJBY3RpdmVDb250ZXh0ID0gdXNlQ2FsbGJhY2soXG4gICAgKGNvbnRleHQ6IEtleWJpbmRpbmdDb250ZXh0TmFtZSkgPT4ge1xuICAgICAgYWN0aXZlQ29udGV4dHNSZWYuY3VycmVudC5hZGQoY29udGV4dClcbiAgICB9LFxuICAgIFtdLFxuICApXG5cbiAgY29uc3QgdW5yZWdpc3RlckFjdGl2ZUNvbnRleHQgPSB1c2VDYWxsYmFjayhcbiAgICAoY29udGV4dDogS2V5YmluZGluZ0NvbnRleHROYW1lKSA9PiB7XG4gICAgICBhY3RpdmVDb250ZXh0c1JlZi5jdXJyZW50LmRlbGV0ZShjb250ZXh0KVxuICAgIH0sXG4gICAgW10sXG4gIClcblxuICAvLyBDbGVhciBjaG9yZCB0aW1lb3V0IHdoZW4gY29tcG9uZW50IHVubW91bnRzIG9yIGNob3JkIGNoYW5nZXNcbiAgY29uc3QgY2xlYXJDaG9yZFRpbWVvdXQgPSB1c2VDYWxsYmFjaygoKSA9PiB7XG4gICAgaWYgKGNob3JkVGltZW91dFJlZi5jdXJyZW50KSB7XG4gICAgICBjbGVhclRpbWVvdXQoY2hvcmRUaW1lb3V0UmVmLmN1cnJlbnQpXG4gICAgICBjaG9yZFRpbWVvdXRSZWYuY3VycmVudCA9IG51bGxcbiAgICB9XG4gIH0sIFtdKVxuXG4gIC8vIFdyYXBwZXIgZm9yIHNldFBlbmRpbmdDaG9yZCB0aGF0IG1hbmFnZXMgdGltZW91dCBhbmQgc3luY3MgcmVmK3N0YXRlXG4gIGNvbnN0IHNldFBlbmRpbmdDaG9yZCA9IHVzZUNhbGxiYWNrKFxuICAgIChwZW5kaW5nOiBQYXJzZWRLZXlzdHJva2VbXSB8IG51bGwpID0+IHtcbiAgICAgIGNsZWFyQ2hvcmRUaW1lb3V0KClcblxuICAgICAgaWYgKHBlbmRpbmcgIT09IG51bGwpIHtcbiAgICAgICAgLy8gU2V0IHRpbWVvdXQgdG8gY2FuY2VsIGNob3JkIGlmIG5vdCBjb21wbGV0ZWRcbiAgICAgICAgY2hvcmRUaW1lb3V0UmVmLmN1cnJlbnQgPSBzZXRUaW1lb3V0KFxuICAgICAgICAgIChwZW5kaW5nQ2hvcmRSZWYsIHNldFBlbmRpbmdDaG9yZFN0YXRlKSA9PiB7XG4gICAgICAgICAgICBsb2dGb3JEZWJ1Z2dpbmcoJ1trZXliaW5kaW5nc10gQ2hvcmQgdGltZW91dCAtIGNhbmNlbGxpbmcnKVxuICAgICAgICAgICAgcGVuZGluZ0Nob3JkUmVmLmN1cnJlbnQgPSBudWxsXG4gICAgICAgICAgICBzZXRQZW5kaW5nQ2hvcmRTdGF0ZShudWxsKVxuICAgICAgICAgIH0sXG4gICAgICAgICAgQ0hPUkRfVElNRU9VVF9NUyxcbiAgICAgICAgICBwZW5kaW5nQ2hvcmRSZWYsXG4gICAgICAgICAgc2V0UGVuZGluZ0Nob3JkU3RhdGUsXG4gICAgICAgIClcbiAgICAgIH1cblxuICAgICAgLy8gVXBkYXRlIHJlZiBpbW1lZGlhdGVseSBmb3Igc3luY2hyb25vdXMgYWNjZXNzIGluIHJlc29sdmUoKVxuICAgICAgcGVuZGluZ0Nob3JkUmVmLmN1cnJlbnQgPSBwZW5kaW5nXG4gICAgICAvLyBVcGRhdGUgc3RhdGUgdG8gdHJpZ2dlciByZS1yZW5kZXJzIGZvciBVSSB1cGRhdGVzXG4gICAgICBzZXRQZW5kaW5nQ2hvcmRTdGF0ZShwZW5kaW5nKVxuICAgIH0sXG4gICAgW2NsZWFyQ2hvcmRUaW1lb3V0XSxcbiAgKVxuXG4gIHVzZUVmZmVjdCgoKSA9PiB7XG4gICAgLy8gSW5pdGlhbGl6ZSBmaWxlIHdhdGNoZXIgKGlkZW1wb3RlbnQgLSBvbmx5IHJ1bnMgb25jZSlcbiAgICB2b2lkIGluaXRpYWxpemVLZXliaW5kaW5nV2F0Y2hlcigpXG5cbiAgICAvLyBTdWJzY3JpYmUgdG8gY2hhbmdlc1xuICAgIGNvbnN0IHVuc3Vic2NyaWJlID0gc3Vic2NyaWJlVG9LZXliaW5kaW5nQ2hhbmdlcyhyZXN1bHQgPT4ge1xuICAgICAgLy8gQW55IGNhbGxiYWNrIGludm9jYXRpb24gaXMgYSByZWxvYWQgc2luY2UgaW5pdGlhbCBsb2FkIGhhcHBlbnNcbiAgICAgIC8vIHN5bmNocm9ub3VzbHkgaW4gdXNlU3RhdGUsIG5vdCB2aWEgdGhpcyBzdWJzY3JpcHRpb25cbiAgICAgIHNldElzUmVsb2FkKHRydWUpXG5cbiAgICAgIHNldExvYWRSZXN1bHQocmVzdWx0KVxuICAgICAgbG9nRm9yRGVidWdnaW5nKFxuICAgICAgICBgW2tleWJpbmRpbmdzXSBSZWxvYWRlZDogJHtyZXN1bHQuYmluZGluZ3MubGVuZ3RofSBiaW5kaW5ncywgJHtyZXN1bHQud2FybmluZ3MubGVuZ3RofSB3YXJuaW5nc2AsXG4gICAgICApXG4gICAgfSlcblxuICAgIHJldHVybiAoKSA9PiB7XG4gICAgICB1bnN1YnNjcmliZSgpXG4gICAgICBjbGVhckNob3JkVGltZW91dCgpXG4gICAgfVxuICB9LCBbY2xlYXJDaG9yZFRpbWVvdXRdKVxuXG4gIHJldHVybiAoXG4gICAgPEtleWJpbmRpbmdQcm92aWRlclxuICAgICAgYmluZGluZ3M9e2JpbmRpbmdzfVxuICAgICAgcGVuZGluZ0Nob3JkUmVmPXtwZW5kaW5nQ2hvcmRSZWZ9XG4gICAgICBwZW5kaW5nQ2hvcmQ9e3BlbmRpbmdDaG9yZH1cbiAgICAgIHNldFBlbmRpbmdDaG9yZD17c2V0UGVuZGluZ0Nob3JkfVxuICAgICAgYWN0aXZlQ29udGV4dHM9e2FjdGl2ZUNvbnRleHRzUmVmLmN1cnJlbnR9XG4gICAgICByZWdpc3RlckFjdGl2ZUNvbnRleHQ9e3JlZ2lzdGVyQWN0aXZlQ29udGV4dH1cbiAgICAgIHVucmVnaXN0ZXJBY3RpdmVDb250ZXh0PXt1bnJlZ2lzdGVyQWN0aXZlQ29udGV4dH1cbiAgICAgIGhhbmRsZXJSZWdpc3RyeVJlZj17aGFuZGxlclJlZ2lzdHJ5UmVmfVxuICAgID5cbiAgICAgIDxDaG9yZEludGVyY2VwdG9yXG4gICAgICAgIGJpbmRpbmdzPXtiaW5kaW5nc31cbiAgICAgICAgcGVuZGluZ0Nob3JkUmVmPXtwZW5kaW5nQ2hvcmRSZWZ9XG4gICAgICAgIHNldFBlbmRpbmdDaG9yZD17c2V0UGVuZGluZ0Nob3JkfVxuICAgICAgICBhY3RpdmVDb250ZXh0cz17YWN0aXZlQ29udGV4dHNSZWYuY3VycmVudH1cbiAgICAgICAgaGFuZGxlclJlZ2lzdHJ5UmVmPXtoYW5kbGVyUmVnaXN0cnlSZWZ9XG4gICAgICAvPlxuICAgICAge2NoaWxkcmVufVxuICAgIDwvS2V5YmluZGluZ1Byb3ZpZGVyPlxuICApXG59XG5cbi8qKlxuICogR2xvYmFsIGNob3JkIGludGVyY2VwdG9yIHRoYXQgcmVnaXN0ZXJzIHVzZUlucHV0IEZJUlNUIChiZWZvcmUgY2hpbGRyZW4pLlxuICpcbiAqIFRoaXMgY29tcG9uZW50IGludGVyY2VwdHMga2V5c3Ryb2tlcyB0aGF0IGFyZSBwYXJ0IG9mIGNob3JkIHNlcXVlbmNlcyBhbmRcbiAqIHN0b3BzIHByb3BhZ2F0aW9uIGJlZm9yZSBvdGhlciBoYW5kbGVycyAobGlrZSBQcm9tcHRJbnB1dCkgY2FuIHNlZSB0aGVtLlxuICpcbiAqIFdpdGhvdXQgdGhpcywgdGhlIHNlY29uZCBrZXkgb2YgYSBjaG9yZCAoZS5nLiwgJ3InIGluIFwiY3RybCtjIHJcIikgd291bGQgYmVcbiAqIGNhcHR1cmVkIGJ5IFByb21wdElucHV0IGFuZCBhZGRlZCB0byB0aGUgaW5wdXQgZmllbGQgYmVmb3JlIHRoZSBrZXliaW5kaW5nXG4gKiBzeXN0ZW0gY291bGQgcmVjb2duaXplIGl0IGFzIGNvbXBsZXRpbmcgYSBjaG9yZC5cbiAqL1xudHlwZSBIYW5kbGVyUmVnaXN0cmF0aW9uID0ge1xuICBhY3Rpb246IHN0cmluZ1xuICBjb250ZXh0OiBLZXliaW5kaW5nQ29udGV4dE5hbWVcbiAgaGFuZGxlcjogKCkgPT4gdm9pZFxufVxuXG5mdW5jdGlvbiBDaG9yZEludGVyY2VwdG9yKHtcbiAgYmluZGluZ3MsXG4gIHBlbmRpbmdDaG9yZFJlZixcbiAgc2V0UGVuZGluZ0Nob3JkLFxuICBhY3RpdmVDb250ZXh0cyxcbiAgaGFuZGxlclJlZ2lzdHJ5UmVmLFxufToge1xuICBiaW5kaW5nczogUGFyc2VkQmluZGluZ1tdXG4gIHBlbmRpbmdDaG9yZFJlZjogUmVhY3QuUmVmT2JqZWN0PFBhcnNlZEtleXN0cm9rZVtdIHwgbnVsbD5cbiAgc2V0UGVuZGluZ0Nob3JkOiAocGVuZGluZzogUGFyc2VkS2V5c3Ryb2tlW10gfCBudWxsKSA9PiB2b2lkXG4gIGFjdGl2ZUNvbnRleHRzOiBTZXQ8S2V5YmluZGluZ0NvbnRleHROYW1lPlxuICBoYW5kbGVyUmVnaXN0cnlSZWY6IFJlYWN0LlJlZk9iamVjdDxNYXA8c3RyaW5nLCBTZXQ8SGFuZGxlclJlZ2lzdHJhdGlvbj4+PlxufSk6IG51bGwge1xuICBjb25zdCBoYW5kbGVJbnB1dCA9IHVzZUNhbGxiYWNrKFxuICAgIChpbnB1dDogc3RyaW5nLCBrZXk6IEtleSwgZXZlbnQ6IElucHV0RXZlbnQpID0+IHtcbiAgICAgIC8vIFdoZWVsIGV2ZW50cyBjYW4gbmV2ZXIgc3RhcnQgY2hvcmQgc2VxdWVuY2VzIOKAlCBzY3JvbGw6bGluZVVwL0Rvd24gYXJlXG4gICAgICAvLyBzaW5nbGUta2V5IGJpbmRpbmdzIGhhbmRsZWQgYnkgcGVyLWNvbXBvbmVudCB1c2VLZXliaW5kaW5ncyBob29rcywgbm90XG4gICAgICAvLyBoZXJlLiBTa2lwIHRoZSByZWdpc3RyeSBzY2FuLiBNaWQtY2hvcmQgd2hlZWwgc3RpbGwgZmFsbHMgdGhyb3VnaCBzb1xuICAgICAgLy8gc2Nyb2xsaW5nIGNhbmNlbHMgdGhlIHBlbmRpbmcgY2hvcmQgbGlrZSBhbnkgb3RoZXIgbm9uLW1hdGNoaW5nIGtleS5cbiAgICAgIGlmICgoa2V5LndoZWVsVXAgfHwga2V5LndoZWVsRG93bikgJiYgcGVuZGluZ0Nob3JkUmVmLmN1cnJlbnQgPT09IG51bGwpIHtcbiAgICAgICAgcmV0dXJuXG4gICAgICB9XG5cbiAgICAgIC8vIEJ1aWxkIGNvbnRleHQgbGlzdCBmcm9tIHJlZ2lzdGVyZWQgaGFuZGxlcnMgKyBhY3RpdmVDb250ZXh0cyArIEdsb2JhbFxuICAgICAgLy8gVGhpcyBlbnN1cmVzIHdlIGNhbiByZXNvbHZlIGNob3JkcyBmb3IgYWxsIGNvbnRleHRzIHRoYXQgaGF2ZSBoYW5kbGVyc1xuICAgICAgY29uc3QgcmVnaXN0cnkgPSBoYW5kbGVyUmVnaXN0cnlSZWYuY3VycmVudFxuICAgICAgY29uc3QgaGFuZGxlckNvbnRleHRzID0gbmV3IFNldDxLZXliaW5kaW5nQ29udGV4dE5hbWU+KClcbiAgICAgIGlmIChyZWdpc3RyeSkge1xuICAgICAgICBmb3IgKGNvbnN0IGhhbmRsZXJzIG9mIHJlZ2lzdHJ5LnZhbHVlcygpKSB7XG4gICAgICAgICAgZm9yIChjb25zdCByZWdpc3RyYXRpb24gb2YgaGFuZGxlcnMpIHtcbiAgICAgICAgICAgIGhhbmRsZXJDb250ZXh0cy5hZGQocmVnaXN0cmF0aW9uLmNvbnRleHQpXG4gICAgICAgICAgfVxuICAgICAgICB9XG4gICAgICB9XG4gICAgICBjb25zdCBjb250ZXh0czogS2V5YmluZGluZ0NvbnRleHROYW1lW10gPSBbXG4gICAgICAgIC4uLmhhbmRsZXJDb250ZXh0cyxcbiAgICAgICAgLi4uYWN0aXZlQ29udGV4dHMsXG4gICAgICAgICdHbG9iYWwnLFxuICAgICAgXVxuXG4gICAgICAvLyBUcmFjayB3aGV0aGVyIHdlJ3JlIGNvbXBsZXRpbmcgYSBjaG9yZCAocGVuZGluZyB3YXMgbm9uLW51bGwpXG4gICAgICBjb25zdCB3YXNJbkNob3JkID0gcGVuZGluZ0Nob3JkUmVmLmN1cnJlbnQgIT09IG51bGxcblxuICAgICAgLy8gQ2hlY2sgaWYgdGhpcyBrZXlzdHJva2UgaXMgcGFydCBvZiBhIGNob3JkIHNlcXVlbmNlXG4gICAgICBjb25zdCByZXN1bHQgPSByZXNvbHZlS2V5V2l0aENob3JkU3RhdGUoXG4gICAgICAgIGlucHV0LFxuICAgICAgICBrZXksXG4gICAgICAgIGNvbnRleHRzLFxuICAgICAgICBiaW5kaW5ncyxcbiAgICAgICAgcGVuZGluZ0Nob3JkUmVmLmN1cnJlbnQsXG4gICAgICApXG5cbiAgICAgIHN3aXRjaCAocmVzdWx0LnR5cGUpIHtcbiAgICAgICAgY2FzZSAnY2hvcmRfc3RhcnRlZCc6XG4gICAgICAgICAgLy8gVGhpcyBrZXkgc3RhcnRzIGEgY2hvcmQgLSBzdG9yZSBwZW5kaW5nIHN0YXRlIGFuZCBzdG9wIHByb3BhZ2F0aW9uXG4gICAgICAgICAgc2V0UGVuZGluZ0Nob3JkKHJlc3VsdC5wZW5kaW5nKVxuICAgICAgICAgIGV2ZW50LnN0b3BJbW1lZGlhdGVQcm9wYWdhdGlvbigpXG4gICAgICAgICAgYnJlYWtcblxuICAgICAgICBjYXNlICdtYXRjaCc6IHtcbiAgICAgICAgICAvLyBDbGVhciBwZW5kaW5nIHN0YXRlXG4gICAgICAgICAgc2V0UGVuZGluZ0Nob3JkKG51bGwpXG5cbiAgICAgICAgICAvLyBPbmx5IGludm9rZSBoYW5kbGVycyBhbmQgc3RvcCBwcm9wYWdhdGlvbiBmb3IgY2hvcmQgY29tcGxldGlvbnNcbiAgICAgICAgICAvLyAobXVsdGkta2V5c3Ryb2tlIHNlcXVlbmNlcykuIFNpbmdsZS1rZXlzdHJva2UgbWF0Y2hlcyBzaG91bGQgcHJvcGFnYXRlXG4gICAgICAgICAgLy8gdG8gcGVyLWhvb2sgaGFuZGxlcnMgdG8gYXZvaWQgaW50ZXJmZXJpbmcgd2l0aCBvdGhlciBpbnB1dCBoYW5kbGluZ1xuICAgICAgICAgIC8vIChlLmcuLCBFbnRlciBuZWVkcyB0byByZWFjaCB1c2VUeXBlYWhlYWQgZm9yIGF1dG9jb21wbGV0ZSBhY2NlcHRhbmNlXG4gICAgICAgICAgLy8gYmVmb3JlIHRoZSBzdWJtaXQgaGFuZGxlciBmaXJlcykuXG4gICAgICAgICAgaWYgKHdhc0luQ2hvcmQpIHtcbiAgICAgICAgICAgIC8vIEZpbmQgYW5kIGludm9rZSB0aGUgaGFuZGxlciBmb3IgdGhpcyBhY3Rpb25cbiAgICAgICAgICAgIC8vIFdlIG5lZWQgdG8gY2hlY2sgdGhhdCB0aGUgaGFuZGxlcidzIGNvbnRleHQgaXMgaW4gb3VyIHJlc29sdmVkIGNvbnRleHRzXG4gICAgICAgICAgICAvLyAod2hpY2ggaW5jbHVkZXMgaGFuZGxlckNvbnRleHRzICsgYWN0aXZlQ29udGV4dHMgKyBHbG9iYWwpXG4gICAgICAgICAgICBjb25zdCBjb250ZXh0c1NldCA9IG5ldyBTZXQoY29udGV4dHMpXG4gICAgICAgICAgICBpZiAocmVnaXN0cnkpIHtcbiAgICAgICAgICAgICAgY29uc3QgaGFuZGxlcnMgPSByZWdpc3RyeS5nZXQocmVzdWx0LmFjdGlvbilcbiAgICAgICAgICAgICAgaWYgKGhhbmRsZXJzICYmIGhhbmRsZXJzLnNpemUgPiAwKSB7XG4gICAgICAgICAgICAgICAgLy8gRmluZCBoYW5kbGVycyB3aG9zZSBjb250ZXh0IGlzIGluIG91ciByZXNvbHZlZCBjb250ZXh0c1xuICAgICAgICAgICAgICAgIGZvciAoY29uc3QgcmVnaXN0cmF0aW9uIG9mIGhhbmRsZXJzKSB7XG4gICAgICAgICAgICAgICAgICBpZiAoY29udGV4dHNTZXQuaGFzKHJlZ2lzdHJhdGlvbi5jb250ZXh0KSkge1xuICAgICAgICAgICAgICAgICAgICByZWdpc3RyYXRpb24uaGFuZGxlcigpXG4gICAgICAgICAgICAgICAgICAgIGV2ZW50LnN0b3BJbW1lZGlhdGVQcm9wYWdhdGlvbigpXG4gICAgICAgICAgICAgICAgICAgIGJyZWFrIC8vIE9ubHkgaW52b2tlIHRoZSBmaXJzdCBtYXRjaGluZyBoYW5kbGVyXG4gICAgICAgICAgICAgICAgICB9XG4gICAgICAgICAgICAgICAgfVxuICAgICAgICAgICAgICB9XG4gICAgICAgICAgICB9XG4gICAgICAgICAgfVxuICAgICAgICAgIGJyZWFrXG4gICAgICAgIH1cblxuICAgICAgICBjYXNlICdjaG9yZF9jYW5jZWxsZWQnOlxuICAgICAgICAgIC8vIEludmFsaWQga2V5IGR1cmluZyBjaG9yZCAtIGNsZWFyIHBlbmRpbmcgc3RhdGUgYW5kIHN3YWxsb3cgdGhlXG4gICAgICAgICAgLy8ga2V5c3Ryb2tlIHNvIGl0IGRvZXNuJ3QgcHJvcGFnYXRlIGFzIGEgc3RhbmRhbG9uZSBhY3Rpb25cbiAgICAgICAgICAvLyAoZS5nLiwgY3RybCt4IGN0cmwrYyBzaG91bGQgbm90IGZpcmUgYXBwOmludGVycnVwdCkuXG4gICAgICAgICAgc2V0UGVuZGluZ0Nob3JkKG51bGwpXG4gICAgICAgICAgZXZlbnQuc3RvcEltbWVkaWF0ZVByb3BhZ2F0aW9uKClcbiAgICAgICAgICBicmVha1xuXG4gICAgICAgIGNhc2UgJ3VuYm91bmQnOlxuICAgICAgICAgIC8vIEtleSBpcyBleHBsaWNpdGx5IHVuYm91bmQgLSBjbGVhciBwZW5kaW5nIHN0YXRlIGFuZCBzd2FsbG93XG4gICAgICAgICAgLy8gdGhlIGtleXN0cm9rZSAoaXQgd2FzIHBhcnQgb2YgYSBjaG9yZCBzZXF1ZW5jZSkuXG4gICAgICAgICAgc2V0UGVuZGluZ0Nob3JkKG51bGwpXG4gICAgICAgICAgZXZlbnQuc3RvcEltbWVkaWF0ZVByb3BhZ2F0aW9uKClcbiAgICAgICAgICBicmVha1xuXG4gICAgICAgIGNhc2UgJ25vbmUnOlxuICAgICAgICAgIC8vIE5vIGNob3JkIGludm9sdmVtZW50IC0gbGV0IG90aGVyIGhhbmRsZXJzIHByb2Nlc3NcbiAgICAgICAgICBicmVha1xuICAgICAgfVxuICAgIH0sXG4gICAgW1xuICAgICAgYmluZGluZ3MsXG4gICAgICBwZW5kaW5nQ2hvcmRSZWYsXG4gICAgICBzZXRQZW5kaW5nQ2hvcmQsXG4gICAgICBhY3RpdmVDb250ZXh0cyxcbiAgICAgIGhhbmRsZXJSZWdpc3RyeVJlZixcbiAgICBdLFxuICApXG5cbiAgdXNlSW5wdXQoaGFuZGxlSW5wdXQpXG5cbiAgcmV0dXJuIG51bGxcbn1cbiJdLCJtYXBwaW5ncyI6IjtBQUFBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQSxPQUFPQSxLQUFLLElBQUlDLFdBQVcsRUFBRUMsU0FBUyxFQUFFQyxNQUFNLEVBQUVDLFFBQVEsUUFBUSxPQUFPO0FBQ3ZFLFNBQVNDLGdCQUFnQixRQUFRLDZCQUE2QjtBQUM5RCxjQUFjQyxVQUFVLFFBQVEsOEJBQThCO0FBQzlEO0FBQ0E7QUFDQTtBQUNBLFNBQVMsS0FBS0MsR0FBRyxFQUFFQyxRQUFRLFFBQVEsV0FBVztBQUM5QyxTQUFTQyxLQUFLLFFBQVEsbUJBQW1CO0FBQ3pDLFNBQVNDLGVBQWUsUUFBUSxtQkFBbUI7QUFDbkQsU0FBU0MsTUFBTSxRQUFRLHlCQUF5QjtBQUNoRCxTQUFTQyxrQkFBa0IsUUFBUSx3QkFBd0I7QUFDM0QsU0FDRUMsMkJBQTJCLEVBQzNCLEtBQUtDLHFCQUFxQixFQUMxQkMsK0JBQStCLEVBQy9CQyw0QkFBNEIsUUFDdkIsdUJBQXVCO0FBQzlCLFNBQVNDLHdCQUF3QixRQUFRLGVBQWU7QUFDeEQsY0FDRUMscUJBQXFCLEVBQ3JCQyxhQUFhLEVBQ2JDLGVBQWUsUUFDVixZQUFZO0FBQ25CLGNBQWNDLGlCQUFpQixRQUFRLGVBQWU7O0FBRXREO0FBQ0E7QUFDQTtBQUNBO0FBQ0EsTUFBTUMsZ0JBQWdCLEdBQUcsSUFBSTtBQUU3QixLQUFLQyxLQUFLLEdBQUc7RUFDWEMsUUFBUSxFQUFFeEIsS0FBSyxDQUFDeUIsU0FBUztBQUMzQixDQUFDOztBQUVEO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBLFNBQUFDLHNCQUFBQyxRQUFBLEVBQUFDLFFBQUE7RUFBQSxNQUFBQyxDQUFBLEdBQUFDLEVBQUE7RUFJRTtJQUFBQyxlQUFBO0lBQUFDO0VBQUEsSUFBZ0QzQixnQkFBZ0IsQ0FBQyxDQUFDO0VBQUEsSUFBQTRCLEVBQUE7RUFBQSxJQUFBSixDQUFBLFFBQUFFLGVBQUEsSUFBQUYsQ0FBQSxRQUFBRyxrQkFBQSxJQUFBSCxDQUFBLFFBQUFGLFFBQUE7SUFFeERNLEVBQUEsR0FBQUEsQ0FBQTtNQUdSLElBQUlOLFFBQVEsQ0FBQU8sTUFBTyxLQUFLLENBQUM7UUFDdkJGLGtCQUFrQixDQUhJLDJCQUdZLENBQUM7UUFBQTtNQUFBO01BSXJDLE1BQUFHLFVBQUEsR0FBbUIxQixLQUFLLENBQUNrQixRQUFRLEVBQUVTLEtBQTJCLENBQUM7TUFDL0QsTUFBQUMsU0FBQSxHQUFrQjVCLEtBQUssQ0FBQ2tCLFFBQVEsRUFBRVcsTUFBNkIsQ0FBQztNQUU1REMsR0FBQSxDQUFBQSxPQUFBO01BQ0osSUFBSUosVUFBVSxHQUFHLENBQWtCLElBQWJFLFNBQVMsR0FBRyxDQUFDO1FBQ2pDRSxPQUFBLENBQUFBLENBQUEsQ0FBVUEsU0FBU0osVUFBVSxlQUFleEIsTUFBTSxDQUFDd0IsVUFBVSxFQUFFLE9BQU8sQ0FBQyxRQUFRRSxTQUFTLElBQUkxQixNQUFNLENBQUMwQixTQUFTLEVBQUUsU0FBUyxDQUFDLEVBQUU7TUFBbkg7UUFDRixJQUFJRixVQUFVLEdBQUcsQ0FBQztVQUN2QkksT0FBQSxDQUFBQSxDQUFBLENBQVVBLFNBQVNKLFVBQVUsZUFBZXhCLE1BQU0sQ0FBQ3dCLFVBQVUsRUFBRSxPQUFPLENBQUMsRUFBRTtRQUFsRTtVQUVQSSxPQUFBLENBQUFBLENBQUEsQ0FBVUEsU0FBU0YsU0FBUyxlQUFlMUIsTUFBTSxDQUFDMEIsU0FBUyxFQUFFLFNBQVMsQ0FBQyxFQUFFO1FBQWxFO01BQ1I7TUFDREUsT0FBQSxHQUFBQSxPQUFPLEdBQUksMkJBQXdCO01BRW5DUixlQUFlLENBQUM7UUFBQVMsR0FBQSxFQXBCUSwyQkFBMkI7UUFBQUMsSUFBQSxFQXNCM0NGLE9BQU87UUFBQUcsS0FBQSxFQUNOUCxVQUFVLEdBQUcsQ0FBdUIsR0FBcEMsT0FBb0MsR0FBcEMsU0FBb0M7UUFBQVEsUUFBQSxFQUNqQ1IsVUFBVSxHQUFHLENBQXdCLEdBQXJDLFdBQXFDLEdBQXJDLE1BQXFDO1FBQUFTLFNBQUEsRUFFcEM7TUFDYixDQUFDLENBQUM7SUFBQSxDQUNIO0lBQUFmLENBQUEsTUFBQUUsZUFBQTtJQUFBRixDQUFBLE1BQUFHLGtCQUFBO0lBQUFILENBQUEsTUFBQUYsUUFBQTtJQUFBRSxDQUFBLE1BQUFJLEVBQUE7RUFBQTtJQUFBQSxFQUFBLEdBQUFKLENBQUE7RUFBQTtFQUFBLElBQUFnQixFQUFBO0VBQUEsSUFBQWhCLENBQUEsUUFBQUUsZUFBQSxJQUFBRixDQUFBLFFBQUFELFFBQUEsSUFBQUMsQ0FBQSxRQUFBRyxrQkFBQSxJQUFBSCxDQUFBLFFBQUFGLFFBQUE7SUFBRWtCLEVBQUEsSUFBQ2xCLFFBQVEsRUFBRUMsUUFBUSxFQUFFRyxlQUFlLEVBQUVDLGtCQUFrQixDQUFDO0lBQUFILENBQUEsTUFBQUUsZUFBQTtJQUFBRixDQUFBLE1BQUFELFFBQUE7SUFBQUMsQ0FBQSxNQUFBRyxrQkFBQTtJQUFBSCxDQUFBLE1BQUFGLFFBQUE7SUFBQUUsQ0FBQSxNQUFBZ0IsRUFBQTtFQUFBO0lBQUFBLEVBQUEsR0FBQWhCLENBQUE7RUFBQTtFQTdCNUQzQixTQUFTLENBQUMrQixFQTZCVCxFQUFFWSxFQUF5RCxDQUFDO0FBQUE7QUFuQy9ELFNBQUFQLE9BQUFRLEdBQUE7RUFBQSxPQWUyQ0MsR0FBQyxDQUFBQyxRQUFTLEtBQUssU0FBUztBQUFBO0FBZm5FLFNBQUFaLE1BQUFXLENBQUE7RUFBQSxPQWM0Q0EsQ0FBQyxDQUFBQyxRQUFTLEtBQUssT0FBTztBQUFBO0FBd0JsRSxPQUFPLFNBQVNDLGVBQWVBLENBQUM7RUFBRXpCO0FBQWdCLENBQU4sRUFBRUQsS0FBSyxDQUFDLEVBQUV2QixLQUFLLENBQUN5QixTQUFTLENBQUM7RUFDcEU7RUFDQSxNQUFNLENBQUM7SUFBRXlCLFFBQVE7SUFBRXZCO0VBQVMsQ0FBQyxFQUFFd0IsYUFBYSxDQUFDLEdBQzNDL0MsUUFBUSxDQUFDVSxxQkFBcUIsQ0FBQyxDQUFDLE1BQU07SUFDcEMsTUFBTXNDLE1BQU0sR0FBR3JDLCtCQUErQixDQUFDLENBQUM7SUFDaERMLGVBQWUsQ0FDYixrREFBa0QwQyxNQUFNLENBQUNGLFFBQVEsQ0FBQ2hCLE1BQU0sY0FBY2tCLE1BQU0sQ0FBQ3pCLFFBQVEsQ0FBQ08sTUFBTSxXQUM5RyxDQUFDO0lBQ0QsT0FBT2tCLE1BQU07RUFDZixDQUFDLENBQUM7O0VBRUo7RUFDQSxNQUFNLENBQUN4QixRQUFRLEVBQUV5QixXQUFXLENBQUMsR0FBR2pELFFBQVEsQ0FBQyxLQUFLLENBQUM7O0VBRS9DO0VBQ0FzQixxQkFBcUIsQ0FBQ0MsUUFBUSxFQUFFQyxRQUFRLENBQUM7O0VBRXpDO0VBQ0E7RUFDQTtFQUNBLE1BQU0wQixlQUFlLEdBQUduRCxNQUFNLENBQUNpQixlQUFlLEVBQUUsR0FBRyxJQUFJLENBQUMsQ0FBQyxJQUFJLENBQUM7RUFDOUQsTUFBTSxDQUFDbUMsWUFBWSxFQUFFQyxvQkFBb0IsQ0FBQyxHQUFHcEQsUUFBUSxDQUNuRGdCLGVBQWUsRUFBRSxHQUFHLElBQUksQ0FDekIsQ0FBQyxJQUFJLENBQUM7RUFDUCxNQUFNcUMsZUFBZSxHQUFHdEQsTUFBTSxDQUFDdUQsTUFBTSxDQUFDQyxPQUFPLEdBQUcsSUFBSSxDQUFDLENBQUMsSUFBSSxDQUFDOztFQUUzRDtFQUNBLE1BQU1DLGtCQUFrQixHQUFHekQsTUFBTSxDQUMvQixJQUFJMEQsR0FBRyxDQUNMLE1BQU0sRUFDTkMsR0FBRyxDQUFDO0lBQ0ZDLE1BQU0sRUFBRSxNQUFNO0lBQ2RDLE9BQU8sRUFBRTlDLHFCQUFxQjtJQUM5QitDLE9BQU8sRUFBRSxHQUFHLEdBQUcsSUFBSTtFQUNyQixDQUFDLENBQUMsQ0FDSCxDQUFDLENBQ0osQ0FBQzs7RUFFRDtFQUNBO0VBQ0E7RUFDQSxNQUFNQyxpQkFBaUIsR0FBRy9ELE1BQU0sQ0FBQzJELEdBQUcsQ0FBQzVDLHFCQUFxQixDQUFDLENBQUMsQ0FBQyxJQUFJNEMsR0FBRyxDQUFDLENBQUMsQ0FBQztFQUV2RSxNQUFNSyxxQkFBcUIsR0FBR2xFLFdBQVcsQ0FDdkMsQ0FBQytELE9BQU8sRUFBRTlDLHFCQUFxQixLQUFLO0lBQ2xDZ0QsaUJBQWlCLENBQUNFLE9BQU8sQ0FBQ0MsR0FBRyxDQUFDTCxPQUFPLENBQUM7RUFDeEMsQ0FBQyxFQUNELEVBQ0YsQ0FBQztFQUVELE1BQU1NLHVCQUF1QixHQUFHckUsV0FBVyxDQUN6QyxDQUFDK0QsU0FBTyxFQUFFOUMscUJBQXFCLEtBQUs7SUFDbENnRCxpQkFBaUIsQ0FBQ0UsT0FBTyxDQUFDRyxNQUFNLENBQUNQLFNBQU8sQ0FBQztFQUMzQyxDQUFDLEVBQ0QsRUFDRixDQUFDOztFQUVEO0VBQ0EsTUFBTVEsaUJBQWlCLEdBQUd2RSxXQUFXLENBQUMsTUFBTTtJQUMxQyxJQUFJd0QsZUFBZSxDQUFDVyxPQUFPLEVBQUU7TUFDM0JLLFlBQVksQ0FBQ2hCLGVBQWUsQ0FBQ1csT0FBTyxDQUFDO01BQ3JDWCxlQUFlLENBQUNXLE9BQU8sR0FBRyxJQUFJO0lBQ2hDO0VBQ0YsQ0FBQyxFQUFFLEVBQUUsQ0FBQzs7RUFFTjtFQUNBLE1BQU1NLGVBQWUsR0FBR3pFLFdBQVcsQ0FDakMsQ0FBQzBFLE9BQU8sRUFBRXZELGVBQWUsRUFBRSxHQUFHLElBQUksS0FBSztJQUNyQ29ELGlCQUFpQixDQUFDLENBQUM7SUFFbkIsSUFBSUcsT0FBTyxLQUFLLElBQUksRUFBRTtNQUNwQjtNQUNBbEIsZUFBZSxDQUFDVyxPQUFPLEdBQUdRLFVBQVUsQ0FDbEMsQ0FBQ3RCLGlCQUFlLEVBQUVFLHNCQUFvQixLQUFLO1FBQ3pDOUMsZUFBZSxDQUFDLDBDQUEwQyxDQUFDO1FBQzNENEMsaUJBQWUsQ0FBQ2MsT0FBTyxHQUFHLElBQUk7UUFDOUJaLHNCQUFvQixDQUFDLElBQUksQ0FBQztNQUM1QixDQUFDLEVBQ0RsQyxnQkFBZ0IsRUFDaEJnQyxlQUFlLEVBQ2ZFLG9CQUNGLENBQUM7SUFDSDs7SUFFQTtJQUNBRixlQUFlLENBQUNjLE9BQU8sR0FBR08sT0FBTztJQUNqQztJQUNBbkIsb0JBQW9CLENBQUNtQixPQUFPLENBQUM7RUFDL0IsQ0FBQyxFQUNELENBQUNILGlCQUFpQixDQUNwQixDQUFDO0VBRUR0RSxTQUFTLENBQUMsTUFBTTtJQUNkO0lBQ0EsS0FBS1csMkJBQTJCLENBQUMsQ0FBQzs7SUFFbEM7SUFDQSxNQUFNZ0UsV0FBVyxHQUFHN0QsNEJBQTRCLENBQUNvQyxRQUFNLElBQUk7TUFDekQ7TUFDQTtNQUNBQyxXQUFXLENBQUMsSUFBSSxDQUFDO01BRWpCRixhQUFhLENBQUNDLFFBQU0sQ0FBQztNQUNyQjFDLGVBQWUsQ0FDYiwyQkFBMkIwQyxRQUFNLENBQUNGLFFBQVEsQ0FBQ2hCLE1BQU0sY0FBY2tCLFFBQU0sQ0FBQ3pCLFFBQVEsQ0FBQ08sTUFBTSxXQUN2RixDQUFDO0lBQ0gsQ0FBQyxDQUFDO0lBRUYsT0FBTyxNQUFNO01BQ1gyQyxXQUFXLENBQUMsQ0FBQztNQUNiTCxpQkFBaUIsQ0FBQyxDQUFDO0lBQ3JCLENBQUM7RUFDSCxDQUFDLEVBQUUsQ0FBQ0EsaUJBQWlCLENBQUMsQ0FBQztFQUV2QixPQUNFLENBQUMsa0JBQWtCLENBQ2pCLFFBQVEsQ0FBQyxDQUFDdEIsUUFBUSxDQUFDLENBQ25CLGVBQWUsQ0FBQyxDQUFDSSxlQUFlLENBQUMsQ0FDakMsWUFBWSxDQUFDLENBQUNDLFlBQVksQ0FBQyxDQUMzQixlQUFlLENBQUMsQ0FBQ21CLGVBQWUsQ0FBQyxDQUNqQyxjQUFjLENBQUMsQ0FBQ1IsaUJBQWlCLENBQUNFLE9BQU8sQ0FBQyxDQUMxQyxxQkFBcUIsQ0FBQyxDQUFDRCxxQkFBcUIsQ0FBQyxDQUM3Qyx1QkFBdUIsQ0FBQyxDQUFDRyx1QkFBdUIsQ0FBQyxDQUNqRCxrQkFBa0IsQ0FBQyxDQUFDVixrQkFBa0IsQ0FBQztBQUU3QyxNQUFNLENBQUMsZ0JBQWdCLENBQ2YsUUFBUSxDQUFDLENBQUNWLFFBQVEsQ0FBQyxDQUNuQixlQUFlLENBQUMsQ0FBQ0ksZUFBZSxDQUFDLENBQ2pDLGVBQWUsQ0FBQyxDQUFDb0IsZUFBZSxDQUFDLENBQ2pDLGNBQWMsQ0FBQyxDQUFDUixpQkFBaUIsQ0FBQ0UsT0FBTyxDQUFDLENBQzFDLGtCQUFrQixDQUFDLENBQUNSLGtCQUFrQixDQUFDO0FBRS9DLE1BQU0sQ0FBQ3BDLFFBQVE7QUFDZixJQUFJLEVBQUUsa0JBQWtCLENBQUM7QUFFekI7O0FBRUE7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQSxLQUFLc0QsbUJBQW1CLEdBQUc7RUFDekJmLE1BQU0sRUFBRSxNQUFNO0VBQ2RDLE9BQU8sRUFBRTlDLHFCQUFxQjtFQUM5QitDLE9BQU8sRUFBRSxHQUFHLEdBQUcsSUFBSTtBQUNyQixDQUFDO0FBRUQsU0FBQWMsaUJBQUE5QyxFQUFBO0VBQUEsTUFBQUosQ0FBQSxHQUFBQyxFQUFBO0VBQTBCO0lBQUFvQixRQUFBO0lBQUFJLGVBQUE7SUFBQW9CLGVBQUE7SUFBQU0sY0FBQTtJQUFBcEI7RUFBQSxJQUFBM0IsRUFZekI7RUFBQSxJQUFBWSxFQUFBO0VBQUEsSUFBQWhCLENBQUEsUUFBQW1ELGNBQUEsSUFBQW5ELENBQUEsUUFBQXFCLFFBQUEsSUFBQXJCLENBQUEsUUFBQStCLGtCQUFBLElBQUEvQixDQUFBLFFBQUF5QixlQUFBLElBQUF6QixDQUFBLFFBQUE2QyxlQUFBO0lBRUc3QixFQUFBLEdBQUFBLENBQUFvQyxLQUFBLEVBQUF6QyxHQUFBLEVBQUEwQyxLQUFBO01BS0UsSUFBSSxDQUFDMUMsR0FBRyxDQUFBMkMsT0FBeUIsSUFBYjNDLEdBQUcsQ0FBQTRDLFNBQStDLEtBQWhDOUIsZUFBZSxDQUFBYyxPQUFRLEtBQUssSUFBSTtRQUFBO01BQUE7TUFNdEUsTUFBQWlCLFFBQUEsR0FBaUJ6QixrQkFBa0IsQ0FBQVEsT0FBUTtNQUMzQyxNQUFBa0IsZUFBQSxHQUF3QixJQUFJeEIsR0FBRyxDQUF3QixDQUFDO01BQ3hELElBQUl1QixRQUFRO1FBQ1YsS0FBSyxNQUFBRSxRQUFjLElBQUlGLFFBQVEsQ0FBQUcsTUFBTyxDQUFDLENBQUM7VUFDdEMsS0FBSyxNQUFBQyxZQUFrQixJQUFJRixRQUFRO1lBQ2pDRCxlQUFlLENBQUFqQixHQUFJLENBQUNvQixZQUFZLENBQUF6QixPQUFRLENBQUM7VUFBQTtRQUMxQztNQUNGO01BRUgsTUFBQTBCLFFBQUEsR0FBMEMsSUFDckNKLGVBQWUsS0FDZk4sY0FBYyxFQUNqQixRQUFRLENBQ1Q7TUFHRCxNQUFBVyxVQUFBLEdBQW1CckMsZUFBZSxDQUFBYyxPQUFRLEtBQUssSUFBSTtNQUduRCxNQUFBaEIsTUFBQSxHQUFlbkMsd0JBQXdCLENBQ3JDZ0UsS0FBSyxFQUNMekMsR0FBRyxFQUNIa0QsUUFBUSxFQUNSeEMsUUFBUSxFQUNSSSxlQUFlLENBQUFjLE9BQ2pCLENBQUM7TUFBQXdCLElBQUEsRUFFRCxRQUFReEMsTUFBTSxDQUFBeUMsSUFBSztRQUFBLEtBQ1osZUFBZTtVQUFBO1lBRWxCbkIsZUFBZSxDQUFDdEIsTUFBTSxDQUFBdUIsT0FBUSxDQUFDO1lBQy9CTyxLQUFLLENBQUFZLHdCQUF5QixDQUFDLENBQUM7WUFDaEMsTUFBQUYsSUFBQTtVQUFLO1FBQUEsS0FFRixPQUFPO1VBQUE7WUFFVmxCLGVBQWUsQ0FBQyxJQUFJLENBQUM7WUFPckIsSUFBSWlCLFVBQVU7Y0FJWixNQUFBSSxXQUFBLEdBQW9CLElBQUlqQyxHQUFHLENBQUM0QixRQUFRLENBQUM7Y0FDckMsSUFBSUwsUUFBUTtnQkFDVixNQUFBVyxVQUFBLEdBQWlCWCxRQUFRLENBQUFZLEdBQUksQ0FBQzdDLE1BQU0sQ0FBQVcsTUFBTyxDQUFDO2dCQUM1QyxJQUFJaUMsVUFBNkIsSUFBakJULFVBQVEsQ0FBQVcsSUFBSyxHQUFHLENBQUM7a0JBRS9CLEtBQUssTUFBQUMsY0FBa0IsSUFBSVosVUFBUTtvQkFDakMsSUFBSVEsV0FBVyxDQUFBSyxHQUFJLENBQUNYLGNBQVksQ0FBQXpCLE9BQVEsQ0FBQztzQkFDdkN5QixjQUFZLENBQUF4QixPQUFRLENBQUMsQ0FBQztzQkFDdEJpQixLQUFLLENBQUFZLHdCQUF5QixDQUFDLENBQUM7c0JBQ2hDO29CQUFLO2tCQUNOO2dCQUNGO2NBQ0Y7WUFDRjtZQUVILE1BQUFGLElBQUE7VUFBSztRQUFBLEtBR0YsaUJBQWlCO1VBQUE7WUFJcEJsQixlQUFlLENBQUMsSUFBSSxDQUFDO1lBQ3JCUSxLQUFLLENBQUFZLHdCQUF5QixDQUFDLENBQUM7WUFDaEMsTUFBQUYsSUFBQTtVQUFLO1FBQUEsS0FFRixTQUFTO1VBQUE7WUFHWmxCLGVBQWUsQ0FBQyxJQUFJLENBQUM7WUFDckJRLEtBQUssQ0FBQVksd0JBQXlCLENBQUMsQ0FBQztZQUNoQyxNQUFBRixJQUFBO1VBQUs7UUFBQSxLQUVGLE1BQU07TUFHYjtJQUFDLENBQ0Y7SUFBQS9ELENBQUEsTUFBQW1ELGNBQUE7SUFBQW5ELENBQUEsTUFBQXFCLFFBQUE7SUFBQXJCLENBQUEsTUFBQStCLGtCQUFBO0lBQUEvQixDQUFBLE1BQUF5QixlQUFBO0lBQUF6QixDQUFBLE1BQUE2QyxlQUFBO0lBQUE3QyxDQUFBLE1BQUFnQixFQUFBO0VBQUE7SUFBQUEsRUFBQSxHQUFBaEIsQ0FBQTtFQUFBO0VBaEdILE1BQUF3RSxXQUFBLEdBQW9CeEQsRUF3R25CO0VBRURyQyxRQUFRLENBQUM2RixXQUFXLENBQUM7RUFBQSxPQUVkLElBQUk7QUFBQSIsImlnbm9yZUxpc3QiOltdfQ==
|
src/keybindings/defaultBindings.ts
ADDED
|
@@ -0,0 +1,340 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { feature } from 'bun:bundle'
|
| 2 |
+
import { satisfies } from 'src/utils/semver.js'
|
| 3 |
+
import { isRunningWithBun } from '../utils/bundledMode.js'
|
| 4 |
+
import { getPlatform } from '../utils/platform.js'
|
| 5 |
+
import type { KeybindingBlock } from './types.js'
|
| 6 |
+
|
| 7 |
+
/**
|
| 8 |
+
* Default keybindings that match current Claude Code behavior.
|
| 9 |
+
* These are loaded first, then user keybindings.json overrides them.
|
| 10 |
+
*/
|
| 11 |
+
|
| 12 |
+
// Platform-specific image paste shortcut:
|
| 13 |
+
// - Windows: alt+v (ctrl+v is system paste)
|
| 14 |
+
// - Other platforms: ctrl+v
|
| 15 |
+
const IMAGE_PASTE_KEY = getPlatform() === 'windows' ? 'alt+v' : 'ctrl+v'
|
| 16 |
+
|
| 17 |
+
// Modifier-only chords (like shift+tab) may fail on Windows Terminal without VT mode
|
| 18 |
+
// See: https://github.com/microsoft/terminal/issues/879#issuecomment-618801651
|
| 19 |
+
// Node enabled VT mode in 24.2.0 / 22.17.0: https://github.com/nodejs/node/pull/58358
|
| 20 |
+
// Bun enabled VT mode in 1.2.23: https://github.com/oven-sh/bun/pull/21161
|
| 21 |
+
const SUPPORTS_TERMINAL_VT_MODE =
|
| 22 |
+
getPlatform() !== 'windows' ||
|
| 23 |
+
(isRunningWithBun()
|
| 24 |
+
? satisfies(process.versions.bun, '>=1.2.23')
|
| 25 |
+
: satisfies(process.versions.node, '>=22.17.0 <23.0.0 || >=24.2.0'))
|
| 26 |
+
|
| 27 |
+
// Platform-specific mode cycle shortcut:
|
| 28 |
+
// - Windows without VT mode: meta+m (shift+tab doesn't work reliably)
|
| 29 |
+
// - Other platforms: shift+tab
|
| 30 |
+
const MODE_CYCLE_KEY = SUPPORTS_TERMINAL_VT_MODE ? 'shift+tab' : 'meta+m'
|
| 31 |
+
|
| 32 |
+
export const DEFAULT_BINDINGS: KeybindingBlock[] = [
|
| 33 |
+
{
|
| 34 |
+
context: 'Global',
|
| 35 |
+
bindings: {
|
| 36 |
+
// ctrl+c and ctrl+d use special time-based double-press handling.
|
| 37 |
+
// They ARE defined here so the resolver can find them, but they
|
| 38 |
+
// CANNOT be rebound by users - validation in reservedShortcuts.ts
|
| 39 |
+
// will show an error if users try to override these keys.
|
| 40 |
+
'ctrl+c': 'app:interrupt',
|
| 41 |
+
'ctrl+d': 'app:exit',
|
| 42 |
+
'ctrl+l': 'app:redraw',
|
| 43 |
+
'ctrl+t': 'app:toggleTodos',
|
| 44 |
+
'ctrl+o': 'app:toggleTranscript',
|
| 45 |
+
...(feature('KAIROS') || feature('KAIROS_BRIEF')
|
| 46 |
+
? { 'ctrl+shift+b': 'app:toggleBrief' as const }
|
| 47 |
+
: {}),
|
| 48 |
+
'ctrl+shift+o': 'app:toggleTeammatePreview',
|
| 49 |
+
'ctrl+r': 'history:search',
|
| 50 |
+
// File navigation. cmd+ bindings only fire on kitty-protocol terminals;
|
| 51 |
+
// ctrl+shift is the portable fallback.
|
| 52 |
+
...(feature('QUICK_SEARCH')
|
| 53 |
+
? {
|
| 54 |
+
'ctrl+shift+f': 'app:globalSearch' as const,
|
| 55 |
+
'cmd+shift+f': 'app:globalSearch' as const,
|
| 56 |
+
'ctrl+shift+p': 'app:quickOpen' as const,
|
| 57 |
+
'cmd+shift+p': 'app:quickOpen' as const,
|
| 58 |
+
}
|
| 59 |
+
: {}),
|
| 60 |
+
...(feature('TERMINAL_PANEL') ? { 'meta+j': 'app:toggleTerminal' } : {}),
|
| 61 |
+
},
|
| 62 |
+
},
|
| 63 |
+
{
|
| 64 |
+
context: 'Chat',
|
| 65 |
+
bindings: {
|
| 66 |
+
escape: 'chat:cancel',
|
| 67 |
+
// ctrl+x chord prefix avoids shadowing readline editing keys (ctrl+a/b/e/f/...).
|
| 68 |
+
'ctrl+x ctrl+k': 'chat:killAgents',
|
| 69 |
+
[MODE_CYCLE_KEY]: 'chat:cycleMode',
|
| 70 |
+
'meta+p': 'chat:modelPicker',
|
| 71 |
+
'meta+o': 'chat:fastMode',
|
| 72 |
+
'meta+t': 'chat:thinkingToggle',
|
| 73 |
+
enter: 'chat:submit',
|
| 74 |
+
up: 'history:previous',
|
| 75 |
+
down: 'history:next',
|
| 76 |
+
// Editing shortcuts (defined here, migration in progress)
|
| 77 |
+
// Undo has two bindings to support different terminal behaviors:
|
| 78 |
+
// - ctrl+_ for legacy terminals (send \x1f control char)
|
| 79 |
+
// - ctrl+shift+- for Kitty protocol (sends physical key with modifiers)
|
| 80 |
+
'ctrl+_': 'chat:undo',
|
| 81 |
+
'ctrl+shift+-': 'chat:undo',
|
| 82 |
+
// ctrl+x ctrl+e is the readline-native edit-and-execute-command binding.
|
| 83 |
+
'ctrl+x ctrl+e': 'chat:externalEditor',
|
| 84 |
+
'ctrl+g': 'chat:externalEditor',
|
| 85 |
+
'ctrl+s': 'chat:stash',
|
| 86 |
+
// Image paste shortcut (platform-specific key defined above)
|
| 87 |
+
[IMAGE_PASTE_KEY]: 'chat:imagePaste',
|
| 88 |
+
...(feature('MESSAGE_ACTIONS')
|
| 89 |
+
? { 'shift+up': 'chat:messageActions' as const }
|
| 90 |
+
: {}),
|
| 91 |
+
// Voice activation (hold-to-talk). Registered so getShortcutDisplay
|
| 92 |
+
// finds it without hitting the fallback analytics log. To rebind,
|
| 93 |
+
// add a voice:pushToTalk entry (last wins); to disable, use /voice
|
| 94 |
+
// — null-unbinding space hits a pre-existing useKeybinding.ts trap
|
| 95 |
+
// where 'unbound' swallows the event (space dead for typing).
|
| 96 |
+
...(feature('VOICE_MODE') ? { space: 'voice:pushToTalk' } : {}),
|
| 97 |
+
},
|
| 98 |
+
},
|
| 99 |
+
{
|
| 100 |
+
context: 'Autocomplete',
|
| 101 |
+
bindings: {
|
| 102 |
+
tab: 'autocomplete:accept',
|
| 103 |
+
escape: 'autocomplete:dismiss',
|
| 104 |
+
up: 'autocomplete:previous',
|
| 105 |
+
down: 'autocomplete:next',
|
| 106 |
+
},
|
| 107 |
+
},
|
| 108 |
+
{
|
| 109 |
+
context: 'Settings',
|
| 110 |
+
bindings: {
|
| 111 |
+
// Settings menu uses escape only (not 'n') to dismiss
|
| 112 |
+
escape: 'confirm:no',
|
| 113 |
+
// Config panel list navigation (reuses Select actions)
|
| 114 |
+
up: 'select:previous',
|
| 115 |
+
down: 'select:next',
|
| 116 |
+
k: 'select:previous',
|
| 117 |
+
j: 'select:next',
|
| 118 |
+
'ctrl+p': 'select:previous',
|
| 119 |
+
'ctrl+n': 'select:next',
|
| 120 |
+
// Toggle/activate the selected setting (space only — enter saves & closes)
|
| 121 |
+
space: 'select:accept',
|
| 122 |
+
// Save and close the config panel
|
| 123 |
+
enter: 'settings:close',
|
| 124 |
+
// Enter search mode
|
| 125 |
+
'/': 'settings:search',
|
| 126 |
+
// Retry loading usage data (only active on error)
|
| 127 |
+
r: 'settings:retry',
|
| 128 |
+
},
|
| 129 |
+
},
|
| 130 |
+
{
|
| 131 |
+
context: 'Confirmation',
|
| 132 |
+
bindings: {
|
| 133 |
+
y: 'confirm:yes',
|
| 134 |
+
n: 'confirm:no',
|
| 135 |
+
enter: 'confirm:yes',
|
| 136 |
+
escape: 'confirm:no',
|
| 137 |
+
// Navigation for dialogs with lists
|
| 138 |
+
up: 'confirm:previous',
|
| 139 |
+
down: 'confirm:next',
|
| 140 |
+
tab: 'confirm:nextField',
|
| 141 |
+
space: 'confirm:toggle',
|
| 142 |
+
// Cycle modes (used in file permission dialogs and teams dialog)
|
| 143 |
+
'shift+tab': 'confirm:cycleMode',
|
| 144 |
+
// Toggle permission explanation in permission dialogs
|
| 145 |
+
'ctrl+e': 'confirm:toggleExplanation',
|
| 146 |
+
// Toggle permission debug info
|
| 147 |
+
'ctrl+d': 'permission:toggleDebug',
|
| 148 |
+
},
|
| 149 |
+
},
|
| 150 |
+
{
|
| 151 |
+
context: 'Tabs',
|
| 152 |
+
bindings: {
|
| 153 |
+
// Tab cycling navigation
|
| 154 |
+
tab: 'tabs:next',
|
| 155 |
+
'shift+tab': 'tabs:previous',
|
| 156 |
+
right: 'tabs:next',
|
| 157 |
+
left: 'tabs:previous',
|
| 158 |
+
},
|
| 159 |
+
},
|
| 160 |
+
{
|
| 161 |
+
context: 'Transcript',
|
| 162 |
+
bindings: {
|
| 163 |
+
'ctrl+e': 'transcript:toggleShowAll',
|
| 164 |
+
'ctrl+c': 'transcript:exit',
|
| 165 |
+
escape: 'transcript:exit',
|
| 166 |
+
// q — pager convention (less, tmux copy-mode). Transcript is a modal
|
| 167 |
+
// reading view with no prompt, so q-as-literal-char has no owner.
|
| 168 |
+
q: 'transcript:exit',
|
| 169 |
+
},
|
| 170 |
+
},
|
| 171 |
+
{
|
| 172 |
+
context: 'HistorySearch',
|
| 173 |
+
bindings: {
|
| 174 |
+
'ctrl+r': 'historySearch:next',
|
| 175 |
+
escape: 'historySearch:accept',
|
| 176 |
+
tab: 'historySearch:accept',
|
| 177 |
+
'ctrl+c': 'historySearch:cancel',
|
| 178 |
+
enter: 'historySearch:execute',
|
| 179 |
+
},
|
| 180 |
+
},
|
| 181 |
+
{
|
| 182 |
+
context: 'Task',
|
| 183 |
+
bindings: {
|
| 184 |
+
// Background running foreground tasks (bash commands, agents)
|
| 185 |
+
// In tmux, users must press ctrl+b twice (tmux prefix escape)
|
| 186 |
+
'ctrl+b': 'task:background',
|
| 187 |
+
},
|
| 188 |
+
},
|
| 189 |
+
{
|
| 190 |
+
context: 'ThemePicker',
|
| 191 |
+
bindings: {
|
| 192 |
+
'ctrl+t': 'theme:toggleSyntaxHighlighting',
|
| 193 |
+
},
|
| 194 |
+
},
|
| 195 |
+
{
|
| 196 |
+
context: 'Scroll',
|
| 197 |
+
bindings: {
|
| 198 |
+
pageup: 'scroll:pageUp',
|
| 199 |
+
pagedown: 'scroll:pageDown',
|
| 200 |
+
wheelup: 'scroll:lineUp',
|
| 201 |
+
wheeldown: 'scroll:lineDown',
|
| 202 |
+
'ctrl+home': 'scroll:top',
|
| 203 |
+
'ctrl+end': 'scroll:bottom',
|
| 204 |
+
// Selection copy. ctrl+shift+c is standard terminal copy.
|
| 205 |
+
// cmd+c only fires on terminals using the kitty keyboard
|
| 206 |
+
// protocol (kitty/WezTerm/ghostty/iTerm2) where the super
|
| 207 |
+
// modifier actually reaches the pty — inert elsewhere.
|
| 208 |
+
// Esc-to-clear and contextual ctrl+c are handled via raw
|
| 209 |
+
// useInput so they can conditionally propagate.
|
| 210 |
+
'ctrl+shift+c': 'selection:copy',
|
| 211 |
+
'cmd+c': 'selection:copy',
|
| 212 |
+
},
|
| 213 |
+
},
|
| 214 |
+
{
|
| 215 |
+
context: 'Help',
|
| 216 |
+
bindings: {
|
| 217 |
+
escape: 'help:dismiss',
|
| 218 |
+
},
|
| 219 |
+
},
|
| 220 |
+
// Attachment navigation (select dialog image attachments)
|
| 221 |
+
{
|
| 222 |
+
context: 'Attachments',
|
| 223 |
+
bindings: {
|
| 224 |
+
right: 'attachments:next',
|
| 225 |
+
left: 'attachments:previous',
|
| 226 |
+
backspace: 'attachments:remove',
|
| 227 |
+
delete: 'attachments:remove',
|
| 228 |
+
down: 'attachments:exit',
|
| 229 |
+
escape: 'attachments:exit',
|
| 230 |
+
},
|
| 231 |
+
},
|
| 232 |
+
// Footer indicator navigation (tasks, teams, diff, loop)
|
| 233 |
+
{
|
| 234 |
+
context: 'Footer',
|
| 235 |
+
bindings: {
|
| 236 |
+
up: 'footer:up',
|
| 237 |
+
'ctrl+p': 'footer:up',
|
| 238 |
+
down: 'footer:down',
|
| 239 |
+
'ctrl+n': 'footer:down',
|
| 240 |
+
right: 'footer:next',
|
| 241 |
+
left: 'footer:previous',
|
| 242 |
+
enter: 'footer:openSelected',
|
| 243 |
+
escape: 'footer:clearSelection',
|
| 244 |
+
},
|
| 245 |
+
},
|
| 246 |
+
// Message selector (rewind dialog) navigation
|
| 247 |
+
{
|
| 248 |
+
context: 'MessageSelector',
|
| 249 |
+
bindings: {
|
| 250 |
+
up: 'messageSelector:up',
|
| 251 |
+
down: 'messageSelector:down',
|
| 252 |
+
k: 'messageSelector:up',
|
| 253 |
+
j: 'messageSelector:down',
|
| 254 |
+
'ctrl+p': 'messageSelector:up',
|
| 255 |
+
'ctrl+n': 'messageSelector:down',
|
| 256 |
+
'ctrl+up': 'messageSelector:top',
|
| 257 |
+
'shift+up': 'messageSelector:top',
|
| 258 |
+
'meta+up': 'messageSelector:top',
|
| 259 |
+
'shift+k': 'messageSelector:top',
|
| 260 |
+
'ctrl+down': 'messageSelector:bottom',
|
| 261 |
+
'shift+down': 'messageSelector:bottom',
|
| 262 |
+
'meta+down': 'messageSelector:bottom',
|
| 263 |
+
'shift+j': 'messageSelector:bottom',
|
| 264 |
+
enter: 'messageSelector:select',
|
| 265 |
+
},
|
| 266 |
+
},
|
| 267 |
+
// PromptInput unmounts while cursor active — no key conflict.
|
| 268 |
+
...(feature('MESSAGE_ACTIONS')
|
| 269 |
+
? [
|
| 270 |
+
{
|
| 271 |
+
context: 'MessageActions' as const,
|
| 272 |
+
bindings: {
|
| 273 |
+
up: 'messageActions:prev' as const,
|
| 274 |
+
down: 'messageActions:next' as const,
|
| 275 |
+
k: 'messageActions:prev' as const,
|
| 276 |
+
j: 'messageActions:next' as const,
|
| 277 |
+
// meta = cmd on macOS; super for kitty keyboard-protocol — bind both.
|
| 278 |
+
'meta+up': 'messageActions:top' as const,
|
| 279 |
+
'meta+down': 'messageActions:bottom' as const,
|
| 280 |
+
'super+up': 'messageActions:top' as const,
|
| 281 |
+
'super+down': 'messageActions:bottom' as const,
|
| 282 |
+
// Mouse selection extends on shift+arrow (ScrollKeybindingHandler:573) when present —
|
| 283 |
+
// correct layered UX: esc clears selection, then shift+↑ jumps.
|
| 284 |
+
'shift+up': 'messageActions:prevUser' as const,
|
| 285 |
+
'shift+down': 'messageActions:nextUser' as const,
|
| 286 |
+
escape: 'messageActions:escape' as const,
|
| 287 |
+
'ctrl+c': 'messageActions:ctrlc' as const,
|
| 288 |
+
// Mirror MESSAGE_ACTIONS. Not imported — would pull React/ink into this config module.
|
| 289 |
+
enter: 'messageActions:enter' as const,
|
| 290 |
+
c: 'messageActions:c' as const,
|
| 291 |
+
p: 'messageActions:p' as const,
|
| 292 |
+
},
|
| 293 |
+
},
|
| 294 |
+
]
|
| 295 |
+
: []),
|
| 296 |
+
// Diff dialog navigation
|
| 297 |
+
{
|
| 298 |
+
context: 'DiffDialog',
|
| 299 |
+
bindings: {
|
| 300 |
+
escape: 'diff:dismiss',
|
| 301 |
+
left: 'diff:previousSource',
|
| 302 |
+
right: 'diff:nextSource',
|
| 303 |
+
up: 'diff:previousFile',
|
| 304 |
+
down: 'diff:nextFile',
|
| 305 |
+
enter: 'diff:viewDetails',
|
| 306 |
+
// Note: diff:back is handled by left arrow in detail mode
|
| 307 |
+
},
|
| 308 |
+
},
|
| 309 |
+
// Model picker effort cycling (ant-only)
|
| 310 |
+
{
|
| 311 |
+
context: 'ModelPicker',
|
| 312 |
+
bindings: {
|
| 313 |
+
left: 'modelPicker:decreaseEffort',
|
| 314 |
+
right: 'modelPicker:increaseEffort',
|
| 315 |
+
},
|
| 316 |
+
},
|
| 317 |
+
// Select component navigation (used by /model, /resume, permission prompts, etc.)
|
| 318 |
+
{
|
| 319 |
+
context: 'Select',
|
| 320 |
+
bindings: {
|
| 321 |
+
up: 'select:previous',
|
| 322 |
+
down: 'select:next',
|
| 323 |
+
j: 'select:next',
|
| 324 |
+
k: 'select:previous',
|
| 325 |
+
'ctrl+n': 'select:next',
|
| 326 |
+
'ctrl+p': 'select:previous',
|
| 327 |
+
enter: 'select:accept',
|
| 328 |
+
escape: 'select:cancel',
|
| 329 |
+
},
|
| 330 |
+
},
|
| 331 |
+
// Plugin dialog actions (manage, browse, discover plugins)
|
| 332 |
+
// Navigation (select:*) uses the Select context above
|
| 333 |
+
{
|
| 334 |
+
context: 'Plugin',
|
| 335 |
+
bindings: {
|
| 336 |
+
space: 'plugin:toggle',
|
| 337 |
+
i: 'plugin:install',
|
| 338 |
+
},
|
| 339 |
+
},
|
| 340 |
+
]
|
src/keybindings/loadUserBindings.ts
ADDED
|
@@ -0,0 +1,472 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* User keybinding configuration loader with hot-reload support.
|
| 3 |
+
*
|
| 4 |
+
* Loads keybindings from ~/.claude/keybindings.json and watches
|
| 5 |
+
* for changes to reload them automatically.
|
| 6 |
+
*
|
| 7 |
+
* NOTE: User keybinding customization is currently only available for
|
| 8 |
+
* Anthropic employees (USER_TYPE === 'ant'). External users always
|
| 9 |
+
* use the default bindings.
|
| 10 |
+
*/
|
| 11 |
+
|
| 12 |
+
import chokidar, { type FSWatcher } from 'chokidar'
|
| 13 |
+
import { readFileSync } from 'fs'
|
| 14 |
+
import { readFile, stat } from 'fs/promises'
|
| 15 |
+
import { dirname, join } from 'path'
|
| 16 |
+
import { getFeatureValue_CACHED_MAY_BE_STALE } from '../services/analytics/growthbook.js'
|
| 17 |
+
import { logEvent } from '../services/analytics/index.js'
|
| 18 |
+
import { registerCleanup } from '../utils/cleanupRegistry.js'
|
| 19 |
+
import { logForDebugging } from '../utils/debug.js'
|
| 20 |
+
import { getClaudeConfigHomeDir } from '../utils/envUtils.js'
|
| 21 |
+
import { errorMessage, isENOENT } from '../utils/errors.js'
|
| 22 |
+
import { createSignal } from '../utils/signal.js'
|
| 23 |
+
import { jsonParse } from '../utils/slowOperations.js'
|
| 24 |
+
import { DEFAULT_BINDINGS } from './defaultBindings.js'
|
| 25 |
+
import { parseBindings } from './parser.js'
|
| 26 |
+
import type { KeybindingBlock, ParsedBinding } from './types.js'
|
| 27 |
+
import {
|
| 28 |
+
checkDuplicateKeysInJson,
|
| 29 |
+
type KeybindingWarning,
|
| 30 |
+
validateBindings,
|
| 31 |
+
} from './validate.js'
|
| 32 |
+
|
| 33 |
+
/**
|
| 34 |
+
* Check if keybinding customization is enabled.
|
| 35 |
+
*
|
| 36 |
+
* Returns true if the tengu_keybinding_customization_release GrowthBook gate is enabled.
|
| 37 |
+
*
|
| 38 |
+
* This function is exported so other parts of the codebase (e.g., /doctor)
|
| 39 |
+
* can check the same condition consistently.
|
| 40 |
+
*/
|
| 41 |
+
export function isKeybindingCustomizationEnabled(): boolean {
|
| 42 |
+
return getFeatureValue_CACHED_MAY_BE_STALE(
|
| 43 |
+
'tengu_keybinding_customization_release',
|
| 44 |
+
false,
|
| 45 |
+
)
|
| 46 |
+
}
|
| 47 |
+
|
| 48 |
+
/**
|
| 49 |
+
* Time in milliseconds to wait for file writes to stabilize.
|
| 50 |
+
*/
|
| 51 |
+
const FILE_STABILITY_THRESHOLD_MS = 500
|
| 52 |
+
|
| 53 |
+
/**
|
| 54 |
+
* Polling interval for checking file stability.
|
| 55 |
+
*/
|
| 56 |
+
const FILE_STABILITY_POLL_INTERVAL_MS = 200
|
| 57 |
+
|
| 58 |
+
/**
|
| 59 |
+
* Result of loading keybindings, including any validation warnings.
|
| 60 |
+
*/
|
| 61 |
+
export type KeybindingsLoadResult = {
|
| 62 |
+
bindings: ParsedBinding[]
|
| 63 |
+
warnings: KeybindingWarning[]
|
| 64 |
+
}
|
| 65 |
+
|
| 66 |
+
let watcher: FSWatcher | null = null
|
| 67 |
+
let initialized = false
|
| 68 |
+
let disposed = false
|
| 69 |
+
let cachedBindings: ParsedBinding[] | null = null
|
| 70 |
+
let cachedWarnings: KeybindingWarning[] = []
|
| 71 |
+
const keybindingsChanged = createSignal<[result: KeybindingsLoadResult]>()
|
| 72 |
+
|
| 73 |
+
/**
|
| 74 |
+
* Tracks the date (YYYY-MM-DD) when we last logged a custom keybindings load event.
|
| 75 |
+
* Used to ensure we fire the event at most once per day.
|
| 76 |
+
*/
|
| 77 |
+
let lastCustomBindingsLogDate: string | null = null
|
| 78 |
+
|
| 79 |
+
/**
|
| 80 |
+
* Log a telemetry event when custom keybindings are loaded, at most once per day.
|
| 81 |
+
* This lets us estimate the percentage of users who customize their keybindings.
|
| 82 |
+
*/
|
| 83 |
+
function logCustomBindingsLoadedOncePerDay(userBindingCount: number): void {
|
| 84 |
+
const today = new Date().toISOString().slice(0, 10)
|
| 85 |
+
if (lastCustomBindingsLogDate === today) return
|
| 86 |
+
lastCustomBindingsLogDate = today
|
| 87 |
+
logEvent('tengu_custom_keybindings_loaded', {
|
| 88 |
+
user_binding_count: userBindingCount,
|
| 89 |
+
})
|
| 90 |
+
}
|
| 91 |
+
|
| 92 |
+
/**
|
| 93 |
+
* Type guard to check if an object is a valid KeybindingBlock.
|
| 94 |
+
*/
|
| 95 |
+
function isKeybindingBlock(obj: unknown): obj is KeybindingBlock {
|
| 96 |
+
if (typeof obj !== 'object' || obj === null) return false
|
| 97 |
+
const b = obj as Record<string, unknown>
|
| 98 |
+
return (
|
| 99 |
+
typeof b.context === 'string' &&
|
| 100 |
+
typeof b.bindings === 'object' &&
|
| 101 |
+
b.bindings !== null
|
| 102 |
+
)
|
| 103 |
+
}
|
| 104 |
+
|
| 105 |
+
/**
|
| 106 |
+
* Type guard to check if an array contains only valid KeybindingBlocks.
|
| 107 |
+
*/
|
| 108 |
+
function isKeybindingBlockArray(arr: unknown): arr is KeybindingBlock[] {
|
| 109 |
+
return Array.isArray(arr) && arr.every(isKeybindingBlock)
|
| 110 |
+
}
|
| 111 |
+
|
| 112 |
+
/**
|
| 113 |
+
* Get the path to the user keybindings file.
|
| 114 |
+
*/
|
| 115 |
+
export function getKeybindingsPath(): string {
|
| 116 |
+
return join(getClaudeConfigHomeDir(), 'keybindings.json')
|
| 117 |
+
}
|
| 118 |
+
|
| 119 |
+
/**
|
| 120 |
+
* Parse default bindings (cached for performance).
|
| 121 |
+
*/
|
| 122 |
+
function getDefaultParsedBindings(): ParsedBinding[] {
|
| 123 |
+
return parseBindings(DEFAULT_BINDINGS)
|
| 124 |
+
}
|
| 125 |
+
|
| 126 |
+
/**
|
| 127 |
+
* Load and parse keybindings from user config file.
|
| 128 |
+
* Returns merged default + user bindings along with validation warnings.
|
| 129 |
+
*
|
| 130 |
+
* For external users, always returns default bindings only.
|
| 131 |
+
* User customization is currently gated to Anthropic employees.
|
| 132 |
+
*/
|
| 133 |
+
export async function loadKeybindings(): Promise<KeybindingsLoadResult> {
|
| 134 |
+
const defaultBindings = getDefaultParsedBindings()
|
| 135 |
+
|
| 136 |
+
// Skip user config loading for external users
|
| 137 |
+
if (!isKeybindingCustomizationEnabled()) {
|
| 138 |
+
return { bindings: defaultBindings, warnings: [] }
|
| 139 |
+
}
|
| 140 |
+
|
| 141 |
+
const userPath = getKeybindingsPath()
|
| 142 |
+
|
| 143 |
+
try {
|
| 144 |
+
const content = await readFile(userPath, 'utf-8')
|
| 145 |
+
const parsed: unknown = jsonParse(content)
|
| 146 |
+
|
| 147 |
+
// Extract bindings array from object wrapper format: { "bindings": [...] }
|
| 148 |
+
let userBlocks: unknown
|
| 149 |
+
if (typeof parsed === 'object' && parsed !== null && 'bindings' in parsed) {
|
| 150 |
+
userBlocks = (parsed as { bindings: unknown }).bindings
|
| 151 |
+
} else {
|
| 152 |
+
// Invalid format - missing bindings property
|
| 153 |
+
const errorMessage = 'keybindings.json must have a "bindings" array'
|
| 154 |
+
const suggestion = 'Use format: { "bindings": [ ... ] }'
|
| 155 |
+
logForDebugging(`[keybindings] Invalid keybindings.json: ${errorMessage}`)
|
| 156 |
+
return {
|
| 157 |
+
bindings: defaultBindings,
|
| 158 |
+
warnings: [
|
| 159 |
+
{
|
| 160 |
+
type: 'parse_error',
|
| 161 |
+
severity: 'error',
|
| 162 |
+
message: errorMessage,
|
| 163 |
+
suggestion,
|
| 164 |
+
},
|
| 165 |
+
],
|
| 166 |
+
}
|
| 167 |
+
}
|
| 168 |
+
|
| 169 |
+
// Validate structure - bindings must be an array of valid keybinding blocks
|
| 170 |
+
if (!isKeybindingBlockArray(userBlocks)) {
|
| 171 |
+
const errorMessage = !Array.isArray(userBlocks)
|
| 172 |
+
? '"bindings" must be an array'
|
| 173 |
+
: 'keybindings.json contains invalid block structure'
|
| 174 |
+
const suggestion = !Array.isArray(userBlocks)
|
| 175 |
+
? 'Set "bindings" to an array of keybinding blocks'
|
| 176 |
+
: 'Each block must have "context" (string) and "bindings" (object)'
|
| 177 |
+
logForDebugging(`[keybindings] Invalid keybindings.json: ${errorMessage}`)
|
| 178 |
+
return {
|
| 179 |
+
bindings: defaultBindings,
|
| 180 |
+
warnings: [
|
| 181 |
+
{
|
| 182 |
+
type: 'parse_error',
|
| 183 |
+
severity: 'error',
|
| 184 |
+
message: errorMessage,
|
| 185 |
+
suggestion,
|
| 186 |
+
},
|
| 187 |
+
],
|
| 188 |
+
}
|
| 189 |
+
}
|
| 190 |
+
|
| 191 |
+
const userParsed = parseBindings(userBlocks)
|
| 192 |
+
logForDebugging(
|
| 193 |
+
`[keybindings] Loaded ${userParsed.length} user bindings from ${userPath}`,
|
| 194 |
+
)
|
| 195 |
+
|
| 196 |
+
// User bindings come after defaults, so they override
|
| 197 |
+
const mergedBindings = [...defaultBindings, ...userParsed]
|
| 198 |
+
|
| 199 |
+
logCustomBindingsLoadedOncePerDay(userParsed.length)
|
| 200 |
+
|
| 201 |
+
// Run validation on user config
|
| 202 |
+
// First check for duplicate keys in raw JSON (JSON.parse silently drops earlier values)
|
| 203 |
+
const duplicateKeyWarnings = checkDuplicateKeysInJson(content)
|
| 204 |
+
const warnings = [
|
| 205 |
+
...duplicateKeyWarnings,
|
| 206 |
+
...validateBindings(userBlocks, mergedBindings),
|
| 207 |
+
]
|
| 208 |
+
|
| 209 |
+
if (warnings.length > 0) {
|
| 210 |
+
logForDebugging(
|
| 211 |
+
`[keybindings] Found ${warnings.length} validation issue(s)`,
|
| 212 |
+
)
|
| 213 |
+
}
|
| 214 |
+
|
| 215 |
+
return { bindings: mergedBindings, warnings }
|
| 216 |
+
} catch (error) {
|
| 217 |
+
// File doesn't exist - use defaults (user can run /keybindings to create)
|
| 218 |
+
if (isENOENT(error)) {
|
| 219 |
+
return { bindings: defaultBindings, warnings: [] }
|
| 220 |
+
}
|
| 221 |
+
|
| 222 |
+
// Other error - log and return defaults with warning
|
| 223 |
+
logForDebugging(
|
| 224 |
+
`[keybindings] Error loading ${userPath}: ${errorMessage(error)}`,
|
| 225 |
+
)
|
| 226 |
+
return {
|
| 227 |
+
bindings: defaultBindings,
|
| 228 |
+
warnings: [
|
| 229 |
+
{
|
| 230 |
+
type: 'parse_error',
|
| 231 |
+
severity: 'error',
|
| 232 |
+
message: `Failed to parse keybindings.json: ${errorMessage(error)}`,
|
| 233 |
+
},
|
| 234 |
+
],
|
| 235 |
+
}
|
| 236 |
+
}
|
| 237 |
+
}
|
| 238 |
+
|
| 239 |
+
/**
|
| 240 |
+
* Load keybindings synchronously (for initial render).
|
| 241 |
+
* Uses cached value if available.
|
| 242 |
+
*/
|
| 243 |
+
export function loadKeybindingsSync(): ParsedBinding[] {
|
| 244 |
+
if (cachedBindings) {
|
| 245 |
+
return cachedBindings
|
| 246 |
+
}
|
| 247 |
+
|
| 248 |
+
const result = loadKeybindingsSyncWithWarnings()
|
| 249 |
+
return result.bindings
|
| 250 |
+
}
|
| 251 |
+
|
| 252 |
+
/**
|
| 253 |
+
* Load keybindings synchronously with validation warnings.
|
| 254 |
+
* Uses cached values if available.
|
| 255 |
+
*
|
| 256 |
+
* For external users, always returns default bindings only.
|
| 257 |
+
* User customization is currently gated to Anthropic employees.
|
| 258 |
+
*/
|
| 259 |
+
export function loadKeybindingsSyncWithWarnings(): KeybindingsLoadResult {
|
| 260 |
+
if (cachedBindings) {
|
| 261 |
+
return { bindings: cachedBindings, warnings: cachedWarnings }
|
| 262 |
+
}
|
| 263 |
+
|
| 264 |
+
const defaultBindings = getDefaultParsedBindings()
|
| 265 |
+
|
| 266 |
+
// Skip user config loading for external users
|
| 267 |
+
if (!isKeybindingCustomizationEnabled()) {
|
| 268 |
+
cachedBindings = defaultBindings
|
| 269 |
+
cachedWarnings = []
|
| 270 |
+
return { bindings: cachedBindings, warnings: cachedWarnings }
|
| 271 |
+
}
|
| 272 |
+
|
| 273 |
+
const userPath = getKeybindingsPath()
|
| 274 |
+
|
| 275 |
+
try {
|
| 276 |
+
// sync IO: called from sync context (React useState initializer)
|
| 277 |
+
const content = readFileSync(userPath, 'utf-8')
|
| 278 |
+
const parsed: unknown = jsonParse(content)
|
| 279 |
+
|
| 280 |
+
// Extract bindings array from object wrapper format: { "bindings": [...] }
|
| 281 |
+
let userBlocks: unknown
|
| 282 |
+
if (typeof parsed === 'object' && parsed !== null && 'bindings' in parsed) {
|
| 283 |
+
userBlocks = (parsed as { bindings: unknown }).bindings
|
| 284 |
+
} else {
|
| 285 |
+
// Invalid format - missing bindings property
|
| 286 |
+
cachedBindings = defaultBindings
|
| 287 |
+
cachedWarnings = [
|
| 288 |
+
{
|
| 289 |
+
type: 'parse_error',
|
| 290 |
+
severity: 'error',
|
| 291 |
+
message: 'keybindings.json must have a "bindings" array',
|
| 292 |
+
suggestion: 'Use format: { "bindings": [ ... ] }',
|
| 293 |
+
},
|
| 294 |
+
]
|
| 295 |
+
return { bindings: cachedBindings, warnings: cachedWarnings }
|
| 296 |
+
}
|
| 297 |
+
|
| 298 |
+
// Validate structure - bindings must be an array of valid keybinding blocks
|
| 299 |
+
if (!isKeybindingBlockArray(userBlocks)) {
|
| 300 |
+
const errorMessage = !Array.isArray(userBlocks)
|
| 301 |
+
? '"bindings" must be an array'
|
| 302 |
+
: 'keybindings.json contains invalid block structure'
|
| 303 |
+
const suggestion = !Array.isArray(userBlocks)
|
| 304 |
+
? 'Set "bindings" to an array of keybinding blocks'
|
| 305 |
+
: 'Each block must have "context" (string) and "bindings" (object)'
|
| 306 |
+
cachedBindings = defaultBindings
|
| 307 |
+
cachedWarnings = [
|
| 308 |
+
{
|
| 309 |
+
type: 'parse_error',
|
| 310 |
+
severity: 'error',
|
| 311 |
+
message: errorMessage,
|
| 312 |
+
suggestion,
|
| 313 |
+
},
|
| 314 |
+
]
|
| 315 |
+
return { bindings: cachedBindings, warnings: cachedWarnings }
|
| 316 |
+
}
|
| 317 |
+
|
| 318 |
+
const userParsed = parseBindings(userBlocks)
|
| 319 |
+
logForDebugging(
|
| 320 |
+
`[keybindings] Loaded ${userParsed.length} user bindings from ${userPath}`,
|
| 321 |
+
)
|
| 322 |
+
cachedBindings = [...defaultBindings, ...userParsed]
|
| 323 |
+
|
| 324 |
+
logCustomBindingsLoadedOncePerDay(userParsed.length)
|
| 325 |
+
|
| 326 |
+
// Run validation - check for duplicate keys in raw JSON first
|
| 327 |
+
const duplicateKeyWarnings = checkDuplicateKeysInJson(content)
|
| 328 |
+
cachedWarnings = [
|
| 329 |
+
...duplicateKeyWarnings,
|
| 330 |
+
...validateBindings(userBlocks, cachedBindings),
|
| 331 |
+
]
|
| 332 |
+
if (cachedWarnings.length > 0) {
|
| 333 |
+
logForDebugging(
|
| 334 |
+
`[keybindings] Found ${cachedWarnings.length} validation issue(s)`,
|
| 335 |
+
)
|
| 336 |
+
}
|
| 337 |
+
|
| 338 |
+
return { bindings: cachedBindings, warnings: cachedWarnings }
|
| 339 |
+
} catch {
|
| 340 |
+
// File doesn't exist or error - use defaults (user can run /keybindings to create)
|
| 341 |
+
cachedBindings = defaultBindings
|
| 342 |
+
cachedWarnings = []
|
| 343 |
+
return { bindings: cachedBindings, warnings: cachedWarnings }
|
| 344 |
+
}
|
| 345 |
+
}
|
| 346 |
+
|
| 347 |
+
/**
|
| 348 |
+
* Initialize file watching for keybindings.json.
|
| 349 |
+
* Call this once when the app starts.
|
| 350 |
+
*
|
| 351 |
+
* For external users, this is a no-op since user customization is disabled.
|
| 352 |
+
*/
|
| 353 |
+
export async function initializeKeybindingWatcher(): Promise<void> {
|
| 354 |
+
if (initialized || disposed) return
|
| 355 |
+
|
| 356 |
+
// Skip file watching for external users
|
| 357 |
+
if (!isKeybindingCustomizationEnabled()) {
|
| 358 |
+
logForDebugging(
|
| 359 |
+
'[keybindings] Skipping file watcher - user customization disabled',
|
| 360 |
+
)
|
| 361 |
+
return
|
| 362 |
+
}
|
| 363 |
+
|
| 364 |
+
const userPath = getKeybindingsPath()
|
| 365 |
+
const watchDir = dirname(userPath)
|
| 366 |
+
|
| 367 |
+
// Only watch if parent directory exists
|
| 368 |
+
try {
|
| 369 |
+
const stats = await stat(watchDir)
|
| 370 |
+
if (!stats.isDirectory()) {
|
| 371 |
+
logForDebugging(
|
| 372 |
+
`[keybindings] Not watching: ${watchDir} is not a directory`,
|
| 373 |
+
)
|
| 374 |
+
return
|
| 375 |
+
}
|
| 376 |
+
} catch {
|
| 377 |
+
logForDebugging(`[keybindings] Not watching: ${watchDir} does not exist`)
|
| 378 |
+
return
|
| 379 |
+
}
|
| 380 |
+
|
| 381 |
+
// Set initialized only after we've confirmed we can watch
|
| 382 |
+
initialized = true
|
| 383 |
+
|
| 384 |
+
logForDebugging(`[keybindings] Watching for changes to ${userPath}`)
|
| 385 |
+
|
| 386 |
+
watcher = chokidar.watch(userPath, {
|
| 387 |
+
persistent: true,
|
| 388 |
+
ignoreInitial: true,
|
| 389 |
+
awaitWriteFinish: {
|
| 390 |
+
stabilityThreshold: FILE_STABILITY_THRESHOLD_MS,
|
| 391 |
+
pollInterval: FILE_STABILITY_POLL_INTERVAL_MS,
|
| 392 |
+
},
|
| 393 |
+
ignorePermissionErrors: true,
|
| 394 |
+
usePolling: false,
|
| 395 |
+
atomic: true,
|
| 396 |
+
})
|
| 397 |
+
|
| 398 |
+
watcher.on('add', handleChange)
|
| 399 |
+
watcher.on('change', handleChange)
|
| 400 |
+
watcher.on('unlink', handleDelete)
|
| 401 |
+
|
| 402 |
+
// Register cleanup
|
| 403 |
+
registerCleanup(async () => disposeKeybindingWatcher())
|
| 404 |
+
}
|
| 405 |
+
|
| 406 |
+
/**
|
| 407 |
+
* Clean up the file watcher.
|
| 408 |
+
*/
|
| 409 |
+
export function disposeKeybindingWatcher(): void {
|
| 410 |
+
disposed = true
|
| 411 |
+
if (watcher) {
|
| 412 |
+
void watcher.close()
|
| 413 |
+
watcher = null
|
| 414 |
+
}
|
| 415 |
+
keybindingsChanged.clear()
|
| 416 |
+
}
|
| 417 |
+
|
| 418 |
+
/**
|
| 419 |
+
* Subscribe to keybinding changes.
|
| 420 |
+
* The listener receives the new parsed bindings when the file changes.
|
| 421 |
+
*/
|
| 422 |
+
export const subscribeToKeybindingChanges = keybindingsChanged.subscribe
|
| 423 |
+
|
| 424 |
+
async function handleChange(path: string): Promise<void> {
|
| 425 |
+
logForDebugging(`[keybindings] Detected change to ${path}`)
|
| 426 |
+
|
| 427 |
+
try {
|
| 428 |
+
const result = await loadKeybindings()
|
| 429 |
+
cachedBindings = result.bindings
|
| 430 |
+
cachedWarnings = result.warnings
|
| 431 |
+
|
| 432 |
+
// Notify all listeners with the full result
|
| 433 |
+
keybindingsChanged.emit(result)
|
| 434 |
+
} catch (error) {
|
| 435 |
+
logForDebugging(`[keybindings] Error reloading: ${errorMessage(error)}`)
|
| 436 |
+
}
|
| 437 |
+
}
|
| 438 |
+
|
| 439 |
+
function handleDelete(path: string): void {
|
| 440 |
+
logForDebugging(`[keybindings] Detected deletion of ${path}`)
|
| 441 |
+
|
| 442 |
+
// Reset to defaults when file is deleted
|
| 443 |
+
const defaultBindings = getDefaultParsedBindings()
|
| 444 |
+
cachedBindings = defaultBindings
|
| 445 |
+
cachedWarnings = []
|
| 446 |
+
|
| 447 |
+
keybindingsChanged.emit({ bindings: defaultBindings, warnings: [] })
|
| 448 |
+
}
|
| 449 |
+
|
| 450 |
+
/**
|
| 451 |
+
* Get the cached keybinding warnings.
|
| 452 |
+
* Returns empty array if no warnings or bindings haven't been loaded yet.
|
| 453 |
+
*/
|
| 454 |
+
export function getCachedKeybindingWarnings(): KeybindingWarning[] {
|
| 455 |
+
return cachedWarnings
|
| 456 |
+
}
|
| 457 |
+
|
| 458 |
+
/**
|
| 459 |
+
* Reset internal state for testing.
|
| 460 |
+
*/
|
| 461 |
+
export function resetKeybindingLoaderForTesting(): void {
|
| 462 |
+
initialized = false
|
| 463 |
+
disposed = false
|
| 464 |
+
cachedBindings = null
|
| 465 |
+
cachedWarnings = []
|
| 466 |
+
lastCustomBindingsLogDate = null
|
| 467 |
+
if (watcher) {
|
| 468 |
+
void watcher.close()
|
| 469 |
+
watcher = null
|
| 470 |
+
}
|
| 471 |
+
keybindingsChanged.clear()
|
| 472 |
+
}
|
src/keybindings/match.ts
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import type { Key } from '../ink.js'
|
| 2 |
+
import type { ParsedBinding, ParsedKeystroke } from './types.js'
|
| 3 |
+
|
| 4 |
+
/**
|
| 5 |
+
* Modifier keys from Ink's Key type that we care about for matching.
|
| 6 |
+
* Note: `fn` from Key is intentionally excluded as it's rarely used and
|
| 7 |
+
* not commonly configurable in terminal applications.
|
| 8 |
+
*/
|
| 9 |
+
type InkModifiers = Pick<Key, 'ctrl' | 'shift' | 'meta' | 'super'>
|
| 10 |
+
|
| 11 |
+
/**
|
| 12 |
+
* Extract modifiers from an Ink Key object.
|
| 13 |
+
* This function ensures we're explicitly extracting the modifiers we care about.
|
| 14 |
+
*/
|
| 15 |
+
function getInkModifiers(key: Key): InkModifiers {
|
| 16 |
+
return {
|
| 17 |
+
ctrl: key.ctrl,
|
| 18 |
+
shift: key.shift,
|
| 19 |
+
meta: key.meta,
|
| 20 |
+
super: key.super,
|
| 21 |
+
}
|
| 22 |
+
}
|
| 23 |
+
|
| 24 |
+
/**
|
| 25 |
+
* Extract the normalized key name from Ink's Key + input.
|
| 26 |
+
* Maps Ink's boolean flags (key.escape, key.return, etc.) to string names
|
| 27 |
+
* that match our ParsedKeystroke.key format.
|
| 28 |
+
*/
|
| 29 |
+
export function getKeyName(input: string, key: Key): string | null {
|
| 30 |
+
if (key.escape) return 'escape'
|
| 31 |
+
if (key.return) return 'enter'
|
| 32 |
+
if (key.tab) return 'tab'
|
| 33 |
+
if (key.backspace) return 'backspace'
|
| 34 |
+
if (key.delete) return 'delete'
|
| 35 |
+
if (key.upArrow) return 'up'
|
| 36 |
+
if (key.downArrow) return 'down'
|
| 37 |
+
if (key.leftArrow) return 'left'
|
| 38 |
+
if (key.rightArrow) return 'right'
|
| 39 |
+
if (key.pageUp) return 'pageup'
|
| 40 |
+
if (key.pageDown) return 'pagedown'
|
| 41 |
+
if (key.wheelUp) return 'wheelup'
|
| 42 |
+
if (key.wheelDown) return 'wheeldown'
|
| 43 |
+
if (key.home) return 'home'
|
| 44 |
+
if (key.end) return 'end'
|
| 45 |
+
if (input.length === 1) return input.toLowerCase()
|
| 46 |
+
return null
|
| 47 |
+
}
|
| 48 |
+
|
| 49 |
+
/**
|
| 50 |
+
* Check if all modifiers match between Ink Key and ParsedKeystroke.
|
| 51 |
+
*
|
| 52 |
+
* Alt and Meta: Ink historically set `key.meta` for Alt/Option. A `meta`
|
| 53 |
+
* modifier in config is treated as an alias for `alt` — both match when
|
| 54 |
+
* `key.meta` is true.
|
| 55 |
+
*
|
| 56 |
+
* Super (Cmd/Win): distinct from alt/meta. Only arrives via the kitty
|
| 57 |
+
* keyboard protocol on supporting terminals. A `cmd`/`super` binding will
|
| 58 |
+
* simply never fire on terminals that don't send it.
|
| 59 |
+
*/
|
| 60 |
+
function modifiersMatch(
|
| 61 |
+
inkMods: InkModifiers,
|
| 62 |
+
target: ParsedKeystroke,
|
| 63 |
+
): boolean {
|
| 64 |
+
// Check ctrl modifier
|
| 65 |
+
if (inkMods.ctrl !== target.ctrl) return false
|
| 66 |
+
|
| 67 |
+
// Check shift modifier
|
| 68 |
+
if (inkMods.shift !== target.shift) return false
|
| 69 |
+
|
| 70 |
+
// Alt and meta both map to key.meta in Ink (terminal limitation)
|
| 71 |
+
// So we check if EITHER alt OR meta is required in target
|
| 72 |
+
const targetNeedsMeta = target.alt || target.meta
|
| 73 |
+
if (inkMods.meta !== targetNeedsMeta) return false
|
| 74 |
+
|
| 75 |
+
// Super (cmd/win) is a distinct modifier from alt/meta
|
| 76 |
+
if (inkMods.super !== target.super) return false
|
| 77 |
+
|
| 78 |
+
return true
|
| 79 |
+
}
|
| 80 |
+
|
| 81 |
+
/**
|
| 82 |
+
* Check if a ParsedKeystroke matches the given Ink input + Key.
|
| 83 |
+
*
|
| 84 |
+
* The display text will show platform-appropriate names (opt on macOS, alt elsewhere).
|
| 85 |
+
*/
|
| 86 |
+
export function matchesKeystroke(
|
| 87 |
+
input: string,
|
| 88 |
+
key: Key,
|
| 89 |
+
target: ParsedKeystroke,
|
| 90 |
+
): boolean {
|
| 91 |
+
const keyName = getKeyName(input, key)
|
| 92 |
+
if (keyName !== target.key) return false
|
| 93 |
+
|
| 94 |
+
const inkMods = getInkModifiers(key)
|
| 95 |
+
|
| 96 |
+
// QUIRK: Ink sets key.meta=true when escape is pressed (see input-event.ts).
|
| 97 |
+
// This is a legacy behavior from how escape sequences work in terminals.
|
| 98 |
+
// We need to ignore the meta modifier when matching the escape key itself,
|
| 99 |
+
// otherwise bindings like "escape" (without modifiers) would never match.
|
| 100 |
+
if (key.escape) {
|
| 101 |
+
return modifiersMatch({ ...inkMods, meta: false }, target)
|
| 102 |
+
}
|
| 103 |
+
|
| 104 |
+
return modifiersMatch(inkMods, target)
|
| 105 |
+
}
|
| 106 |
+
|
| 107 |
+
/**
|
| 108 |
+
* Check if Ink's Key + input matches a parsed binding's first keystroke.
|
| 109 |
+
* For single-keystroke bindings only (Phase 1).
|
| 110 |
+
*/
|
| 111 |
+
export function matchesBinding(
|
| 112 |
+
input: string,
|
| 113 |
+
key: Key,
|
| 114 |
+
binding: ParsedBinding,
|
| 115 |
+
): boolean {
|
| 116 |
+
if (binding.chord.length !== 1) return false
|
| 117 |
+
const keystroke = binding.chord[0]
|
| 118 |
+
if (!keystroke) return false
|
| 119 |
+
return matchesKeystroke(input, key, keystroke)
|
| 120 |
+
}
|
src/keybindings/parser.ts
ADDED
|
@@ -0,0 +1,203 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import type {
|
| 2 |
+
Chord,
|
| 3 |
+
KeybindingBlock,
|
| 4 |
+
ParsedBinding,
|
| 5 |
+
ParsedKeystroke,
|
| 6 |
+
} from './types.js'
|
| 7 |
+
|
| 8 |
+
/**
|
| 9 |
+
* Parse a keystroke string like "ctrl+shift+k" into a ParsedKeystroke.
|
| 10 |
+
* Supports various modifier aliases (ctrl/control, alt/opt/option/meta,
|
| 11 |
+
* cmd/command/super/win).
|
| 12 |
+
*/
|
| 13 |
+
export function parseKeystroke(input: string): ParsedKeystroke {
|
| 14 |
+
const parts = input.split('+')
|
| 15 |
+
const keystroke: ParsedKeystroke = {
|
| 16 |
+
key: '',
|
| 17 |
+
ctrl: false,
|
| 18 |
+
alt: false,
|
| 19 |
+
shift: false,
|
| 20 |
+
meta: false,
|
| 21 |
+
super: false,
|
| 22 |
+
}
|
| 23 |
+
for (const part of parts) {
|
| 24 |
+
const lower = part.toLowerCase()
|
| 25 |
+
switch (lower) {
|
| 26 |
+
case 'ctrl':
|
| 27 |
+
case 'control':
|
| 28 |
+
keystroke.ctrl = true
|
| 29 |
+
break
|
| 30 |
+
case 'alt':
|
| 31 |
+
case 'opt':
|
| 32 |
+
case 'option':
|
| 33 |
+
keystroke.alt = true
|
| 34 |
+
break
|
| 35 |
+
case 'shift':
|
| 36 |
+
keystroke.shift = true
|
| 37 |
+
break
|
| 38 |
+
case 'meta':
|
| 39 |
+
keystroke.meta = true
|
| 40 |
+
break
|
| 41 |
+
case 'cmd':
|
| 42 |
+
case 'command':
|
| 43 |
+
case 'super':
|
| 44 |
+
case 'win':
|
| 45 |
+
keystroke.super = true
|
| 46 |
+
break
|
| 47 |
+
case 'esc':
|
| 48 |
+
keystroke.key = 'escape'
|
| 49 |
+
break
|
| 50 |
+
case 'return':
|
| 51 |
+
keystroke.key = 'enter'
|
| 52 |
+
break
|
| 53 |
+
case 'space':
|
| 54 |
+
keystroke.key = ' '
|
| 55 |
+
break
|
| 56 |
+
case '↑':
|
| 57 |
+
keystroke.key = 'up'
|
| 58 |
+
break
|
| 59 |
+
case '↓':
|
| 60 |
+
keystroke.key = 'down'
|
| 61 |
+
break
|
| 62 |
+
case '←':
|
| 63 |
+
keystroke.key = 'left'
|
| 64 |
+
break
|
| 65 |
+
case '→':
|
| 66 |
+
keystroke.key = 'right'
|
| 67 |
+
break
|
| 68 |
+
default:
|
| 69 |
+
keystroke.key = lower
|
| 70 |
+
break
|
| 71 |
+
}
|
| 72 |
+
}
|
| 73 |
+
|
| 74 |
+
return keystroke
|
| 75 |
+
}
|
| 76 |
+
|
| 77 |
+
/**
|
| 78 |
+
* Parse a chord string like "ctrl+k ctrl+s" into an array of ParsedKeystrokes.
|
| 79 |
+
*/
|
| 80 |
+
export function parseChord(input: string): Chord {
|
| 81 |
+
// A lone space character IS the space key binding, not a separator
|
| 82 |
+
if (input === ' ') return [parseKeystroke('space')]
|
| 83 |
+
return input.trim().split(/\s+/).map(parseKeystroke)
|
| 84 |
+
}
|
| 85 |
+
|
| 86 |
+
/**
|
| 87 |
+
* Convert a ParsedKeystroke to its canonical string representation for display.
|
| 88 |
+
*/
|
| 89 |
+
export function keystrokeToString(ks: ParsedKeystroke): string {
|
| 90 |
+
const parts: string[] = []
|
| 91 |
+
if (ks.ctrl) parts.push('ctrl')
|
| 92 |
+
if (ks.alt) parts.push('alt')
|
| 93 |
+
if (ks.shift) parts.push('shift')
|
| 94 |
+
if (ks.meta) parts.push('meta')
|
| 95 |
+
if (ks.super) parts.push('cmd')
|
| 96 |
+
// Use readable names for display
|
| 97 |
+
const displayKey = keyToDisplayName(ks.key)
|
| 98 |
+
parts.push(displayKey)
|
| 99 |
+
return parts.join('+')
|
| 100 |
+
}
|
| 101 |
+
|
| 102 |
+
/**
|
| 103 |
+
* Map internal key names to human-readable display names.
|
| 104 |
+
*/
|
| 105 |
+
function keyToDisplayName(key: string): string {
|
| 106 |
+
switch (key) {
|
| 107 |
+
case 'escape':
|
| 108 |
+
return 'Esc'
|
| 109 |
+
case ' ':
|
| 110 |
+
return 'Space'
|
| 111 |
+
case 'tab':
|
| 112 |
+
return 'tab'
|
| 113 |
+
case 'enter':
|
| 114 |
+
return 'Enter'
|
| 115 |
+
case 'backspace':
|
| 116 |
+
return 'Backspace'
|
| 117 |
+
case 'delete':
|
| 118 |
+
return 'Delete'
|
| 119 |
+
case 'up':
|
| 120 |
+
return '↑'
|
| 121 |
+
case 'down':
|
| 122 |
+
return '↓'
|
| 123 |
+
case 'left':
|
| 124 |
+
return '←'
|
| 125 |
+
case 'right':
|
| 126 |
+
return '→'
|
| 127 |
+
case 'pageup':
|
| 128 |
+
return 'PageUp'
|
| 129 |
+
case 'pagedown':
|
| 130 |
+
return 'PageDown'
|
| 131 |
+
case 'home':
|
| 132 |
+
return 'Home'
|
| 133 |
+
case 'end':
|
| 134 |
+
return 'End'
|
| 135 |
+
default:
|
| 136 |
+
return key
|
| 137 |
+
}
|
| 138 |
+
}
|
| 139 |
+
|
| 140 |
+
/**
|
| 141 |
+
* Convert a Chord to its canonical string representation for display.
|
| 142 |
+
*/
|
| 143 |
+
export function chordToString(chord: Chord): string {
|
| 144 |
+
return chord.map(keystrokeToString).join(' ')
|
| 145 |
+
}
|
| 146 |
+
|
| 147 |
+
/**
|
| 148 |
+
* Display platform type - a subset of Platform that we care about for display.
|
| 149 |
+
* WSL and unknown are treated as linux for display purposes.
|
| 150 |
+
*/
|
| 151 |
+
type DisplayPlatform = 'macos' | 'windows' | 'linux' | 'wsl' | 'unknown'
|
| 152 |
+
|
| 153 |
+
/**
|
| 154 |
+
* Convert a ParsedKeystroke to a platform-appropriate display string.
|
| 155 |
+
* Uses "opt" for alt on macOS, "alt" elsewhere.
|
| 156 |
+
*/
|
| 157 |
+
export function keystrokeToDisplayString(
|
| 158 |
+
ks: ParsedKeystroke,
|
| 159 |
+
platform: DisplayPlatform = 'linux',
|
| 160 |
+
): string {
|
| 161 |
+
const parts: string[] = []
|
| 162 |
+
if (ks.ctrl) parts.push('ctrl')
|
| 163 |
+
// Alt/meta are equivalent in terminals, show platform-appropriate name
|
| 164 |
+
if (ks.alt || ks.meta) {
|
| 165 |
+
// Only macOS uses "opt", all other platforms use "alt"
|
| 166 |
+
parts.push(platform === 'macos' ? 'opt' : 'alt')
|
| 167 |
+
}
|
| 168 |
+
if (ks.shift) parts.push('shift')
|
| 169 |
+
if (ks.super) {
|
| 170 |
+
parts.push(platform === 'macos' ? 'cmd' : 'super')
|
| 171 |
+
}
|
| 172 |
+
// Use readable names for display
|
| 173 |
+
const displayKey = keyToDisplayName(ks.key)
|
| 174 |
+
parts.push(displayKey)
|
| 175 |
+
return parts.join('+')
|
| 176 |
+
}
|
| 177 |
+
|
| 178 |
+
/**
|
| 179 |
+
* Convert a Chord to a platform-appropriate display string.
|
| 180 |
+
*/
|
| 181 |
+
export function chordToDisplayString(
|
| 182 |
+
chord: Chord,
|
| 183 |
+
platform: DisplayPlatform = 'linux',
|
| 184 |
+
): string {
|
| 185 |
+
return chord.map(ks => keystrokeToDisplayString(ks, platform)).join(' ')
|
| 186 |
+
}
|
| 187 |
+
|
| 188 |
+
/**
|
| 189 |
+
* Parse keybinding blocks (from JSON config) into a flat list of ParsedBindings.
|
| 190 |
+
*/
|
| 191 |
+
export function parseBindings(blocks: KeybindingBlock[]): ParsedBinding[] {
|
| 192 |
+
const bindings: ParsedBinding[] = []
|
| 193 |
+
for (const block of blocks) {
|
| 194 |
+
for (const [key, action] of Object.entries(block.bindings)) {
|
| 195 |
+
bindings.push({
|
| 196 |
+
chord: parseChord(key),
|
| 197 |
+
action,
|
| 198 |
+
context: block.context,
|
| 199 |
+
})
|
| 200 |
+
}
|
| 201 |
+
}
|
| 202 |
+
return bindings
|
| 203 |
+
}
|
src/keybindings/reservedShortcuts.ts
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { getPlatform } from '../utils/platform.js'
|
| 2 |
+
|
| 3 |
+
/**
|
| 4 |
+
* Shortcuts that are typically intercepted by the OS, terminal, or shell
|
| 5 |
+
* and will likely never reach the application.
|
| 6 |
+
*/
|
| 7 |
+
export type ReservedShortcut = {
|
| 8 |
+
key: string
|
| 9 |
+
reason: string
|
| 10 |
+
severity: 'error' | 'warning'
|
| 11 |
+
}
|
| 12 |
+
|
| 13 |
+
/**
|
| 14 |
+
* Shortcuts that cannot be rebound - they are hardcoded in Claude Code.
|
| 15 |
+
*/
|
| 16 |
+
export const NON_REBINDABLE: ReservedShortcut[] = [
|
| 17 |
+
{
|
| 18 |
+
key: 'ctrl+c',
|
| 19 |
+
reason: 'Cannot be rebound - used for interrupt/exit (hardcoded)',
|
| 20 |
+
severity: 'error',
|
| 21 |
+
},
|
| 22 |
+
{
|
| 23 |
+
key: 'ctrl+d',
|
| 24 |
+
reason: 'Cannot be rebound - used for exit (hardcoded)',
|
| 25 |
+
severity: 'error',
|
| 26 |
+
},
|
| 27 |
+
{
|
| 28 |
+
key: 'ctrl+m',
|
| 29 |
+
reason:
|
| 30 |
+
'Cannot be rebound - identical to Enter in terminals (both send CR)',
|
| 31 |
+
severity: 'error',
|
| 32 |
+
},
|
| 33 |
+
]
|
| 34 |
+
|
| 35 |
+
/**
|
| 36 |
+
* Terminal control shortcuts that are intercepted by the terminal/OS.
|
| 37 |
+
* These will likely never reach the application.
|
| 38 |
+
*
|
| 39 |
+
* Note: ctrl+s (XOFF) and ctrl+q (XON) are NOT included here because:
|
| 40 |
+
* - Most modern terminals disable flow control by default
|
| 41 |
+
* - We use ctrl+s for the stash feature
|
| 42 |
+
*/
|
| 43 |
+
export const TERMINAL_RESERVED: ReservedShortcut[] = [
|
| 44 |
+
{
|
| 45 |
+
key: 'ctrl+z',
|
| 46 |
+
reason: 'Unix process suspend (SIGTSTP)',
|
| 47 |
+
severity: 'warning',
|
| 48 |
+
},
|
| 49 |
+
{
|
| 50 |
+
key: 'ctrl+\\',
|
| 51 |
+
reason: 'Terminal quit signal (SIGQUIT)',
|
| 52 |
+
severity: 'error',
|
| 53 |
+
},
|
| 54 |
+
]
|
| 55 |
+
|
| 56 |
+
/**
|
| 57 |
+
* macOS-specific shortcuts that the OS intercepts.
|
| 58 |
+
*/
|
| 59 |
+
export const MACOS_RESERVED: ReservedShortcut[] = [
|
| 60 |
+
{ key: 'cmd+c', reason: 'macOS system copy', severity: 'error' },
|
| 61 |
+
{ key: 'cmd+v', reason: 'macOS system paste', severity: 'error' },
|
| 62 |
+
{ key: 'cmd+x', reason: 'macOS system cut', severity: 'error' },
|
| 63 |
+
{ key: 'cmd+q', reason: 'macOS quit application', severity: 'error' },
|
| 64 |
+
{ key: 'cmd+w', reason: 'macOS close window/tab', severity: 'error' },
|
| 65 |
+
{ key: 'cmd+tab', reason: 'macOS app switcher', severity: 'error' },
|
| 66 |
+
{ key: 'cmd+space', reason: 'macOS Spotlight', severity: 'error' },
|
| 67 |
+
]
|
| 68 |
+
|
| 69 |
+
/**
|
| 70 |
+
* Get all reserved shortcuts for the current platform.
|
| 71 |
+
* Includes non-rebindable shortcuts and terminal-reserved shortcuts.
|
| 72 |
+
*/
|
| 73 |
+
export function getReservedShortcuts(): ReservedShortcut[] {
|
| 74 |
+
const platform = getPlatform()
|
| 75 |
+
// Non-rebindable shortcuts first (highest priority)
|
| 76 |
+
const reserved = [...NON_REBINDABLE, ...TERMINAL_RESERVED]
|
| 77 |
+
|
| 78 |
+
if (platform === 'macos') {
|
| 79 |
+
reserved.push(...MACOS_RESERVED)
|
| 80 |
+
}
|
| 81 |
+
|
| 82 |
+
return reserved
|
| 83 |
+
}
|
| 84 |
+
|
| 85 |
+
/**
|
| 86 |
+
* Normalize a key string for comparison (lowercase, sorted modifiers).
|
| 87 |
+
* Chords (space-separated steps like "ctrl+x ctrl+b") are normalized
|
| 88 |
+
* per-step — splitting on '+' first would mangle "x ctrl" into a mainKey
|
| 89 |
+
* overwritten by the next step, collapsing the chord into its last key.
|
| 90 |
+
*/
|
| 91 |
+
export function normalizeKeyForComparison(key: string): string {
|
| 92 |
+
return key.trim().split(/\s+/).map(normalizeStep).join(' ')
|
| 93 |
+
}
|
| 94 |
+
|
| 95 |
+
function normalizeStep(step: string): string {
|
| 96 |
+
const parts = step.split('+')
|
| 97 |
+
const modifiers: string[] = []
|
| 98 |
+
let mainKey = ''
|
| 99 |
+
|
| 100 |
+
for (const part of parts) {
|
| 101 |
+
const lower = part.trim().toLowerCase()
|
| 102 |
+
if (
|
| 103 |
+
[
|
| 104 |
+
'ctrl',
|
| 105 |
+
'control',
|
| 106 |
+
'alt',
|
| 107 |
+
'opt',
|
| 108 |
+
'option',
|
| 109 |
+
'meta',
|
| 110 |
+
'cmd',
|
| 111 |
+
'command',
|
| 112 |
+
'shift',
|
| 113 |
+
].includes(lower)
|
| 114 |
+
) {
|
| 115 |
+
// Normalize modifier names
|
| 116 |
+
if (lower === 'control') modifiers.push('ctrl')
|
| 117 |
+
else if (lower === 'option' || lower === 'opt') modifiers.push('alt')
|
| 118 |
+
else if (lower === 'command' || lower === 'cmd') modifiers.push('cmd')
|
| 119 |
+
else modifiers.push(lower)
|
| 120 |
+
} else {
|
| 121 |
+
mainKey = lower
|
| 122 |
+
}
|
| 123 |
+
}
|
| 124 |
+
|
| 125 |
+
modifiers.sort()
|
| 126 |
+
return [...modifiers, mainKey].join('+')
|
| 127 |
+
}
|
src/keybindings/resolver.ts
ADDED
|
@@ -0,0 +1,244 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import type { Key } from '../ink.js'
|
| 2 |
+
import { getKeyName, matchesBinding } from './match.js'
|
| 3 |
+
import { chordToString } from './parser.js'
|
| 4 |
+
import type {
|
| 5 |
+
KeybindingContextName,
|
| 6 |
+
ParsedBinding,
|
| 7 |
+
ParsedKeystroke,
|
| 8 |
+
} from './types.js'
|
| 9 |
+
|
| 10 |
+
export type ResolveResult =
|
| 11 |
+
| { type: 'match'; action: string }
|
| 12 |
+
| { type: 'none' }
|
| 13 |
+
| { type: 'unbound' }
|
| 14 |
+
|
| 15 |
+
export type ChordResolveResult =
|
| 16 |
+
| { type: 'match'; action: string }
|
| 17 |
+
| { type: 'none' }
|
| 18 |
+
| { type: 'unbound' }
|
| 19 |
+
| { type: 'chord_started'; pending: ParsedKeystroke[] }
|
| 20 |
+
| { type: 'chord_cancelled' }
|
| 21 |
+
|
| 22 |
+
/**
|
| 23 |
+
* Resolve a key input to an action.
|
| 24 |
+
* Pure function - no state, no side effects, just matching logic.
|
| 25 |
+
*
|
| 26 |
+
* @param input - The character input from Ink
|
| 27 |
+
* @param key - The Key object from Ink with modifier flags
|
| 28 |
+
* @param activeContexts - Array of currently active contexts (e.g., ['Chat', 'Global'])
|
| 29 |
+
* @param bindings - All parsed bindings to search through
|
| 30 |
+
* @returns The resolution result
|
| 31 |
+
*/
|
| 32 |
+
export function resolveKey(
|
| 33 |
+
input: string,
|
| 34 |
+
key: Key,
|
| 35 |
+
activeContexts: KeybindingContextName[],
|
| 36 |
+
bindings: ParsedBinding[],
|
| 37 |
+
): ResolveResult {
|
| 38 |
+
// Find matching bindings (last one wins for user overrides)
|
| 39 |
+
let match: ParsedBinding | undefined
|
| 40 |
+
const ctxSet = new Set(activeContexts)
|
| 41 |
+
|
| 42 |
+
for (const binding of bindings) {
|
| 43 |
+
// Phase 1: Only single-keystroke bindings
|
| 44 |
+
if (binding.chord.length !== 1) continue
|
| 45 |
+
if (!ctxSet.has(binding.context)) continue
|
| 46 |
+
|
| 47 |
+
if (matchesBinding(input, key, binding)) {
|
| 48 |
+
match = binding
|
| 49 |
+
}
|
| 50 |
+
}
|
| 51 |
+
|
| 52 |
+
if (!match) {
|
| 53 |
+
return { type: 'none' }
|
| 54 |
+
}
|
| 55 |
+
|
| 56 |
+
if (match.action === null) {
|
| 57 |
+
return { type: 'unbound' }
|
| 58 |
+
}
|
| 59 |
+
|
| 60 |
+
return { type: 'match', action: match.action }
|
| 61 |
+
}
|
| 62 |
+
|
| 63 |
+
/**
|
| 64 |
+
* Get display text for an action from bindings (e.g., "ctrl+t" for "app:toggleTodos").
|
| 65 |
+
* Searches in reverse order so user overrides take precedence.
|
| 66 |
+
*/
|
| 67 |
+
export function getBindingDisplayText(
|
| 68 |
+
action: string,
|
| 69 |
+
context: KeybindingContextName,
|
| 70 |
+
bindings: ParsedBinding[],
|
| 71 |
+
): string | undefined {
|
| 72 |
+
// Find the last binding for this action in this context
|
| 73 |
+
const binding = bindings.findLast(
|
| 74 |
+
b => b.action === action && b.context === context,
|
| 75 |
+
)
|
| 76 |
+
return binding ? chordToString(binding.chord) : undefined
|
| 77 |
+
}
|
| 78 |
+
|
| 79 |
+
/**
|
| 80 |
+
* Build a ParsedKeystroke from Ink's input/key.
|
| 81 |
+
*/
|
| 82 |
+
function buildKeystroke(input: string, key: Key): ParsedKeystroke | null {
|
| 83 |
+
const keyName = getKeyName(input, key)
|
| 84 |
+
if (!keyName) return null
|
| 85 |
+
|
| 86 |
+
// QUIRK: Ink sets key.meta=true when escape is pressed (see input-event.ts).
|
| 87 |
+
// This is legacy terminal behavior - we should NOT record this as a modifier
|
| 88 |
+
// for the escape key itself, otherwise chord matching will fail.
|
| 89 |
+
const effectiveMeta = key.escape ? false : key.meta
|
| 90 |
+
|
| 91 |
+
return {
|
| 92 |
+
key: keyName,
|
| 93 |
+
ctrl: key.ctrl,
|
| 94 |
+
alt: effectiveMeta,
|
| 95 |
+
shift: key.shift,
|
| 96 |
+
meta: effectiveMeta,
|
| 97 |
+
super: key.super,
|
| 98 |
+
}
|
| 99 |
+
}
|
| 100 |
+
|
| 101 |
+
/**
|
| 102 |
+
* Compare two ParsedKeystrokes for equality. Collapses alt/meta into
|
| 103 |
+
* one logical modifier — legacy terminals can't distinguish them (see
|
| 104 |
+
* match.ts modifiersMatch), so "alt+k" and "meta+k" are the same key.
|
| 105 |
+
* Super (cmd/win) is distinct — only arrives via kitty keyboard protocol.
|
| 106 |
+
*/
|
| 107 |
+
export function keystrokesEqual(
|
| 108 |
+
a: ParsedKeystroke,
|
| 109 |
+
b: ParsedKeystroke,
|
| 110 |
+
): boolean {
|
| 111 |
+
return (
|
| 112 |
+
a.key === b.key &&
|
| 113 |
+
a.ctrl === b.ctrl &&
|
| 114 |
+
a.shift === b.shift &&
|
| 115 |
+
(a.alt || a.meta) === (b.alt || b.meta) &&
|
| 116 |
+
a.super === b.super
|
| 117 |
+
)
|
| 118 |
+
}
|
| 119 |
+
|
| 120 |
+
/**
|
| 121 |
+
* Check if a chord prefix matches the beginning of a binding's chord.
|
| 122 |
+
*/
|
| 123 |
+
function chordPrefixMatches(
|
| 124 |
+
prefix: ParsedKeystroke[],
|
| 125 |
+
binding: ParsedBinding,
|
| 126 |
+
): boolean {
|
| 127 |
+
if (prefix.length >= binding.chord.length) return false
|
| 128 |
+
for (let i = 0; i < prefix.length; i++) {
|
| 129 |
+
const prefixKey = prefix[i]
|
| 130 |
+
const bindingKey = binding.chord[i]
|
| 131 |
+
if (!prefixKey || !bindingKey) return false
|
| 132 |
+
if (!keystrokesEqual(prefixKey, bindingKey)) return false
|
| 133 |
+
}
|
| 134 |
+
return true
|
| 135 |
+
}
|
| 136 |
+
|
| 137 |
+
/**
|
| 138 |
+
* Check if a full chord matches a binding's chord.
|
| 139 |
+
*/
|
| 140 |
+
function chordExactlyMatches(
|
| 141 |
+
chord: ParsedKeystroke[],
|
| 142 |
+
binding: ParsedBinding,
|
| 143 |
+
): boolean {
|
| 144 |
+
if (chord.length !== binding.chord.length) return false
|
| 145 |
+
for (let i = 0; i < chord.length; i++) {
|
| 146 |
+
const chordKey = chord[i]
|
| 147 |
+
const bindingKey = binding.chord[i]
|
| 148 |
+
if (!chordKey || !bindingKey) return false
|
| 149 |
+
if (!keystrokesEqual(chordKey, bindingKey)) return false
|
| 150 |
+
}
|
| 151 |
+
return true
|
| 152 |
+
}
|
| 153 |
+
|
| 154 |
+
/**
|
| 155 |
+
* Resolve a key with chord state support.
|
| 156 |
+
*
|
| 157 |
+
* This function handles multi-keystroke chord bindings like "ctrl+k ctrl+s".
|
| 158 |
+
*
|
| 159 |
+
* @param input - The character input from Ink
|
| 160 |
+
* @param key - The Key object from Ink with modifier flags
|
| 161 |
+
* @param activeContexts - Array of currently active contexts
|
| 162 |
+
* @param bindings - All parsed bindings
|
| 163 |
+
* @param pending - Current chord state (null if not in a chord)
|
| 164 |
+
* @returns Resolution result with chord state
|
| 165 |
+
*/
|
| 166 |
+
export function resolveKeyWithChordState(
|
| 167 |
+
input: string,
|
| 168 |
+
key: Key,
|
| 169 |
+
activeContexts: KeybindingContextName[],
|
| 170 |
+
bindings: ParsedBinding[],
|
| 171 |
+
pending: ParsedKeystroke[] | null,
|
| 172 |
+
): ChordResolveResult {
|
| 173 |
+
// Cancel chord on escape
|
| 174 |
+
if (key.escape && pending !== null) {
|
| 175 |
+
return { type: 'chord_cancelled' }
|
| 176 |
+
}
|
| 177 |
+
|
| 178 |
+
// Build current keystroke
|
| 179 |
+
const currentKeystroke = buildKeystroke(input, key)
|
| 180 |
+
if (!currentKeystroke) {
|
| 181 |
+
if (pending !== null) {
|
| 182 |
+
return { type: 'chord_cancelled' }
|
| 183 |
+
}
|
| 184 |
+
return { type: 'none' }
|
| 185 |
+
}
|
| 186 |
+
|
| 187 |
+
// Build the full chord sequence to test
|
| 188 |
+
const testChord = pending
|
| 189 |
+
? [...pending, currentKeystroke]
|
| 190 |
+
: [currentKeystroke]
|
| 191 |
+
|
| 192 |
+
// Filter bindings by active contexts (Set lookup: O(n) instead of O(n·m))
|
| 193 |
+
const ctxSet = new Set(activeContexts)
|
| 194 |
+
const contextBindings = bindings.filter(b => ctxSet.has(b.context))
|
| 195 |
+
|
| 196 |
+
// Check if this could be a prefix for longer chords. Group by chord
|
| 197 |
+
// string so a later null-override shadows the default it unbinds —
|
| 198 |
+
// otherwise null-unbinding `ctrl+x ctrl+k` still makes `ctrl+x` enter
|
| 199 |
+
// chord-wait and the single-key binding on the prefix never fires.
|
| 200 |
+
const chordWinners = new Map<string, string | null>()
|
| 201 |
+
for (const binding of contextBindings) {
|
| 202 |
+
if (
|
| 203 |
+
binding.chord.length > testChord.length &&
|
| 204 |
+
chordPrefixMatches(testChord, binding)
|
| 205 |
+
) {
|
| 206 |
+
chordWinners.set(chordToString(binding.chord), binding.action)
|
| 207 |
+
}
|
| 208 |
+
}
|
| 209 |
+
let hasLongerChords = false
|
| 210 |
+
for (const action of chordWinners.values()) {
|
| 211 |
+
if (action !== null) {
|
| 212 |
+
hasLongerChords = true
|
| 213 |
+
break
|
| 214 |
+
}
|
| 215 |
+
}
|
| 216 |
+
|
| 217 |
+
// If this keystroke could start a longer chord, prefer that
|
| 218 |
+
// (even if there's an exact single-key match)
|
| 219 |
+
if (hasLongerChords) {
|
| 220 |
+
return { type: 'chord_started', pending: testChord }
|
| 221 |
+
}
|
| 222 |
+
|
| 223 |
+
// Check for exact matches (last one wins)
|
| 224 |
+
let exactMatch: ParsedBinding | undefined
|
| 225 |
+
for (const binding of contextBindings) {
|
| 226 |
+
if (chordExactlyMatches(testChord, binding)) {
|
| 227 |
+
exactMatch = binding
|
| 228 |
+
}
|
| 229 |
+
}
|
| 230 |
+
|
| 231 |
+
if (exactMatch) {
|
| 232 |
+
if (exactMatch.action === null) {
|
| 233 |
+
return { type: 'unbound' }
|
| 234 |
+
}
|
| 235 |
+
return { type: 'match', action: exactMatch.action }
|
| 236 |
+
}
|
| 237 |
+
|
| 238 |
+
// No match and no potential longer chords
|
| 239 |
+
if (pending !== null) {
|
| 240 |
+
return { type: 'chord_cancelled' }
|
| 241 |
+
}
|
| 242 |
+
|
| 243 |
+
return { type: 'none' }
|
| 244 |
+
}
|
src/keybindings/schema.ts
ADDED
|
@@ -0,0 +1,236 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* Zod schema for keybindings.json configuration.
|
| 3 |
+
* Used for validation and JSON schema generation.
|
| 4 |
+
*/
|
| 5 |
+
|
| 6 |
+
import { z } from 'zod/v4'
|
| 7 |
+
import { lazySchema } from '../utils/lazySchema.js'
|
| 8 |
+
|
| 9 |
+
/**
|
| 10 |
+
* Valid context names where keybindings can be applied.
|
| 11 |
+
*/
|
| 12 |
+
export const KEYBINDING_CONTEXTS = [
|
| 13 |
+
'Global',
|
| 14 |
+
'Chat',
|
| 15 |
+
'Autocomplete',
|
| 16 |
+
'Confirmation',
|
| 17 |
+
'Help',
|
| 18 |
+
'Transcript',
|
| 19 |
+
'HistorySearch',
|
| 20 |
+
'Task',
|
| 21 |
+
'ThemePicker',
|
| 22 |
+
'Settings',
|
| 23 |
+
'Tabs',
|
| 24 |
+
// New contexts for keybindings migration
|
| 25 |
+
'Attachments',
|
| 26 |
+
'Footer',
|
| 27 |
+
'MessageSelector',
|
| 28 |
+
'DiffDialog',
|
| 29 |
+
'ModelPicker',
|
| 30 |
+
'Select',
|
| 31 |
+
'Plugin',
|
| 32 |
+
] as const
|
| 33 |
+
|
| 34 |
+
/**
|
| 35 |
+
* Human-readable descriptions for each keybinding context.
|
| 36 |
+
*/
|
| 37 |
+
export const KEYBINDING_CONTEXT_DESCRIPTIONS: Record<
|
| 38 |
+
(typeof KEYBINDING_CONTEXTS)[number],
|
| 39 |
+
string
|
| 40 |
+
> = {
|
| 41 |
+
Global: 'Active everywhere, regardless of focus',
|
| 42 |
+
Chat: 'When the chat input is focused',
|
| 43 |
+
Autocomplete: 'When autocomplete menu is visible',
|
| 44 |
+
Confirmation: 'When a confirmation/permission dialog is shown',
|
| 45 |
+
Help: 'When the help overlay is open',
|
| 46 |
+
Transcript: 'When viewing the transcript',
|
| 47 |
+
HistorySearch: 'When searching command history (ctrl+r)',
|
| 48 |
+
Task: 'When a task/agent is running in the foreground',
|
| 49 |
+
ThemePicker: 'When the theme picker is open',
|
| 50 |
+
Settings: 'When the settings menu is open',
|
| 51 |
+
Tabs: 'When tab navigation is active',
|
| 52 |
+
Attachments: 'When navigating image attachments in a select dialog',
|
| 53 |
+
Footer: 'When footer indicators are focused',
|
| 54 |
+
MessageSelector: 'When the message selector (rewind) is open',
|
| 55 |
+
DiffDialog: 'When the diff dialog is open',
|
| 56 |
+
ModelPicker: 'When the model picker is open',
|
| 57 |
+
Select: 'When a select/list component is focused',
|
| 58 |
+
Plugin: 'When the plugin dialog is open',
|
| 59 |
+
}
|
| 60 |
+
|
| 61 |
+
/**
|
| 62 |
+
* All valid keybinding action identifiers.
|
| 63 |
+
*/
|
| 64 |
+
export const KEYBINDING_ACTIONS = [
|
| 65 |
+
// App-level actions (Global context)
|
| 66 |
+
'app:interrupt',
|
| 67 |
+
'app:exit',
|
| 68 |
+
'app:toggleTodos',
|
| 69 |
+
'app:toggleTranscript',
|
| 70 |
+
'app:toggleBrief',
|
| 71 |
+
'app:toggleTeammatePreview',
|
| 72 |
+
'app:toggleTerminal',
|
| 73 |
+
'app:redraw',
|
| 74 |
+
'app:globalSearch',
|
| 75 |
+
'app:quickOpen',
|
| 76 |
+
// History navigation
|
| 77 |
+
'history:search',
|
| 78 |
+
'history:previous',
|
| 79 |
+
'history:next',
|
| 80 |
+
// Chat input actions
|
| 81 |
+
'chat:cancel',
|
| 82 |
+
'chat:killAgents',
|
| 83 |
+
'chat:cycleMode',
|
| 84 |
+
'chat:modelPicker',
|
| 85 |
+
'chat:fastMode',
|
| 86 |
+
'chat:thinkingToggle',
|
| 87 |
+
'chat:submit',
|
| 88 |
+
'chat:newline',
|
| 89 |
+
'chat:undo',
|
| 90 |
+
'chat:externalEditor',
|
| 91 |
+
'chat:stash',
|
| 92 |
+
'chat:imagePaste',
|
| 93 |
+
'chat:messageActions',
|
| 94 |
+
// Autocomplete menu actions
|
| 95 |
+
'autocomplete:accept',
|
| 96 |
+
'autocomplete:dismiss',
|
| 97 |
+
'autocomplete:previous',
|
| 98 |
+
'autocomplete:next',
|
| 99 |
+
// Confirmation dialog actions
|
| 100 |
+
'confirm:yes',
|
| 101 |
+
'confirm:no',
|
| 102 |
+
'confirm:previous',
|
| 103 |
+
'confirm:next',
|
| 104 |
+
'confirm:nextField',
|
| 105 |
+
'confirm:previousField',
|
| 106 |
+
'confirm:cycleMode',
|
| 107 |
+
'confirm:toggle',
|
| 108 |
+
'confirm:toggleExplanation',
|
| 109 |
+
// Tabs navigation actions
|
| 110 |
+
'tabs:next',
|
| 111 |
+
'tabs:previous',
|
| 112 |
+
// Transcript viewer actions
|
| 113 |
+
'transcript:toggleShowAll',
|
| 114 |
+
'transcript:exit',
|
| 115 |
+
// History search actions
|
| 116 |
+
'historySearch:next',
|
| 117 |
+
'historySearch:accept',
|
| 118 |
+
'historySearch:cancel',
|
| 119 |
+
'historySearch:execute',
|
| 120 |
+
// Task/agent actions
|
| 121 |
+
'task:background',
|
| 122 |
+
// Theme picker actions
|
| 123 |
+
'theme:toggleSyntaxHighlighting',
|
| 124 |
+
// Help menu actions
|
| 125 |
+
'help:dismiss',
|
| 126 |
+
// Attachment navigation (select dialog image attachments)
|
| 127 |
+
'attachments:next',
|
| 128 |
+
'attachments:previous',
|
| 129 |
+
'attachments:remove',
|
| 130 |
+
'attachments:exit',
|
| 131 |
+
// Footer indicator actions
|
| 132 |
+
'footer:up',
|
| 133 |
+
'footer:down',
|
| 134 |
+
'footer:next',
|
| 135 |
+
'footer:previous',
|
| 136 |
+
'footer:openSelected',
|
| 137 |
+
'footer:clearSelection',
|
| 138 |
+
'footer:close',
|
| 139 |
+
// Message selector (rewind) actions
|
| 140 |
+
'messageSelector:up',
|
| 141 |
+
'messageSelector:down',
|
| 142 |
+
'messageSelector:top',
|
| 143 |
+
'messageSelector:bottom',
|
| 144 |
+
'messageSelector:select',
|
| 145 |
+
// Diff dialog actions
|
| 146 |
+
'diff:dismiss',
|
| 147 |
+
'diff:previousSource',
|
| 148 |
+
'diff:nextSource',
|
| 149 |
+
'diff:back',
|
| 150 |
+
'diff:viewDetails',
|
| 151 |
+
'diff:previousFile',
|
| 152 |
+
'diff:nextFile',
|
| 153 |
+
// Model picker actions (ant-only)
|
| 154 |
+
'modelPicker:decreaseEffort',
|
| 155 |
+
'modelPicker:increaseEffort',
|
| 156 |
+
// Select component actions (distinct from confirm: to avoid collisions)
|
| 157 |
+
'select:next',
|
| 158 |
+
'select:previous',
|
| 159 |
+
'select:accept',
|
| 160 |
+
'select:cancel',
|
| 161 |
+
// Plugin dialog actions
|
| 162 |
+
'plugin:toggle',
|
| 163 |
+
'plugin:install',
|
| 164 |
+
// Permission dialog actions
|
| 165 |
+
'permission:toggleDebug',
|
| 166 |
+
// Settings config panel actions
|
| 167 |
+
'settings:search',
|
| 168 |
+
'settings:retry',
|
| 169 |
+
'settings:close',
|
| 170 |
+
// Voice actions
|
| 171 |
+
'voice:pushToTalk',
|
| 172 |
+
] as const
|
| 173 |
+
|
| 174 |
+
/**
|
| 175 |
+
* Schema for a single keybinding block.
|
| 176 |
+
*/
|
| 177 |
+
export const KeybindingBlockSchema = lazySchema(() =>
|
| 178 |
+
z
|
| 179 |
+
.object({
|
| 180 |
+
context: z
|
| 181 |
+
.enum(KEYBINDING_CONTEXTS)
|
| 182 |
+
.describe(
|
| 183 |
+
'UI context where these bindings apply. Global bindings work everywhere.',
|
| 184 |
+
),
|
| 185 |
+
bindings: z
|
| 186 |
+
.record(
|
| 187 |
+
z
|
| 188 |
+
.string()
|
| 189 |
+
.describe('Keystroke pattern (e.g., "ctrl+k", "shift+tab")'),
|
| 190 |
+
z
|
| 191 |
+
.union([
|
| 192 |
+
z.enum(KEYBINDING_ACTIONS),
|
| 193 |
+
z
|
| 194 |
+
.string()
|
| 195 |
+
.regex(/^command:[a-zA-Z0-9:\-_]+$/)
|
| 196 |
+
.describe(
|
| 197 |
+
'Command binding (e.g., "command:help", "command:compact"). Executes the slash command as if typed.',
|
| 198 |
+
),
|
| 199 |
+
z.null().describe('Set to null to unbind a default shortcut'),
|
| 200 |
+
])
|
| 201 |
+
.describe(
|
| 202 |
+
'Action to trigger, command to invoke, or null to unbind',
|
| 203 |
+
),
|
| 204 |
+
)
|
| 205 |
+
.describe('Map of keystroke patterns to actions'),
|
| 206 |
+
})
|
| 207 |
+
.describe('A block of keybindings for a specific context'),
|
| 208 |
+
)
|
| 209 |
+
|
| 210 |
+
/**
|
| 211 |
+
* Schema for the entire keybindings.json file.
|
| 212 |
+
* Uses object wrapper format with optional $schema and $docs metadata.
|
| 213 |
+
*/
|
| 214 |
+
export const KeybindingsSchema = lazySchema(() =>
|
| 215 |
+
z
|
| 216 |
+
.object({
|
| 217 |
+
$schema: z
|
| 218 |
+
.string()
|
| 219 |
+
.optional()
|
| 220 |
+
.describe('JSON Schema URL for editor validation'),
|
| 221 |
+
$docs: z.string().optional().describe('Documentation URL'),
|
| 222 |
+
bindings: z
|
| 223 |
+
.array(KeybindingBlockSchema())
|
| 224 |
+
.describe('Array of keybinding blocks by context'),
|
| 225 |
+
})
|
| 226 |
+
.describe(
|
| 227 |
+
'Claude Code keybindings configuration. Customize keyboard shortcuts by context.',
|
| 228 |
+
),
|
| 229 |
+
)
|
| 230 |
+
|
| 231 |
+
/**
|
| 232 |
+
* TypeScript types derived from the schema.
|
| 233 |
+
*/
|
| 234 |
+
export type KeybindingsSchemaType = z.infer<
|
| 235 |
+
ReturnType<typeof KeybindingsSchema>
|
| 236 |
+
>
|
src/keybindings/shortcutFormat.ts
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import {
|
| 2 |
+
type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
| 3 |
+
logEvent,
|
| 4 |
+
} from '../services/analytics/index.js'
|
| 5 |
+
import { loadKeybindingsSync } from './loadUserBindings.js'
|
| 6 |
+
import { getBindingDisplayText } from './resolver.js'
|
| 7 |
+
import type { KeybindingContextName } from './types.js'
|
| 8 |
+
|
| 9 |
+
// TODO(keybindings-migration): Remove fallback parameter after migration is
|
| 10 |
+
// complete and we've confirmed no 'keybinding_fallback_used' events are being
|
| 11 |
+
// logged. The fallback exists as a safety net during migration - if bindings
|
| 12 |
+
// fail to load or an action isn't found, we fall back to hardcoded values.
|
| 13 |
+
// Once stable, callers should be able to trust that getBindingDisplayText
|
| 14 |
+
// always returns a value for known actions, and we can remove this defensive
|
| 15 |
+
// pattern.
|
| 16 |
+
|
| 17 |
+
// Track which action+context pairs have already logged a fallback event
|
| 18 |
+
// to avoid duplicate events from repeated calls in non-React contexts.
|
| 19 |
+
const LOGGED_FALLBACKS = new Set<string>()
|
| 20 |
+
|
| 21 |
+
/**
|
| 22 |
+
* Get the display text for a configured shortcut without React hooks.
|
| 23 |
+
* Use this in non-React contexts (commands, services, etc.).
|
| 24 |
+
*
|
| 25 |
+
* This lives in its own module (not useShortcutDisplay.ts) so that
|
| 26 |
+
* non-React callers like query/stopHooks.ts don't pull React into their
|
| 27 |
+
* module graph via the sibling hook.
|
| 28 |
+
*
|
| 29 |
+
* @param action - The action name (e.g., 'app:toggleTranscript')
|
| 30 |
+
* @param context - The keybinding context (e.g., 'Global')
|
| 31 |
+
* @param fallback - Fallback text if binding not found
|
| 32 |
+
* @returns The configured shortcut display text
|
| 33 |
+
*
|
| 34 |
+
* @example
|
| 35 |
+
* const expandShortcut = getShortcutDisplay('app:toggleTranscript', 'Global', 'ctrl+o')
|
| 36 |
+
* // Returns the user's configured binding, or 'ctrl+o' as default
|
| 37 |
+
*/
|
| 38 |
+
export function getShortcutDisplay(
|
| 39 |
+
action: string,
|
| 40 |
+
context: KeybindingContextName,
|
| 41 |
+
fallback: string,
|
| 42 |
+
): string {
|
| 43 |
+
const bindings = loadKeybindingsSync()
|
| 44 |
+
const resolved = getBindingDisplayText(action, context, bindings)
|
| 45 |
+
if (resolved === undefined) {
|
| 46 |
+
const key = `${action}:${context}`
|
| 47 |
+
if (!LOGGED_FALLBACKS.has(key)) {
|
| 48 |
+
LOGGED_FALLBACKS.add(key)
|
| 49 |
+
logEvent('tengu_keybinding_fallback_used', {
|
| 50 |
+
action:
|
| 51 |
+
action as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
| 52 |
+
context:
|
| 53 |
+
context as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
| 54 |
+
fallback:
|
| 55 |
+
fallback as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
| 56 |
+
reason:
|
| 57 |
+
'action_not_found' as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
| 58 |
+
})
|
| 59 |
+
}
|
| 60 |
+
return fallback
|
| 61 |
+
}
|
| 62 |
+
return resolved
|
| 63 |
+
}
|
src/keybindings/template.ts
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* Keybindings template generator.
|
| 3 |
+
* Generates a well-documented template file for ~/.claude/keybindings.json
|
| 4 |
+
*/
|
| 5 |
+
|
| 6 |
+
import { jsonStringify } from '../utils/slowOperations.js'
|
| 7 |
+
import { DEFAULT_BINDINGS } from './defaultBindings.js'
|
| 8 |
+
import {
|
| 9 |
+
NON_REBINDABLE,
|
| 10 |
+
normalizeKeyForComparison,
|
| 11 |
+
} from './reservedShortcuts.js'
|
| 12 |
+
import type { KeybindingBlock } from './types.js'
|
| 13 |
+
|
| 14 |
+
/**
|
| 15 |
+
* Filter out reserved shortcuts that cannot be rebound.
|
| 16 |
+
* These would cause /doctor to warn, so we exclude them from the template.
|
| 17 |
+
*/
|
| 18 |
+
function filterReservedShortcuts(blocks: KeybindingBlock[]): KeybindingBlock[] {
|
| 19 |
+
const reservedKeys = new Set(
|
| 20 |
+
NON_REBINDABLE.map(r => normalizeKeyForComparison(r.key)),
|
| 21 |
+
)
|
| 22 |
+
|
| 23 |
+
return blocks
|
| 24 |
+
.map(block => {
|
| 25 |
+
const filteredBindings: Record<string, string | null> = {}
|
| 26 |
+
for (const [key, action] of Object.entries(block.bindings)) {
|
| 27 |
+
if (!reservedKeys.has(normalizeKeyForComparison(key))) {
|
| 28 |
+
filteredBindings[key] = action
|
| 29 |
+
}
|
| 30 |
+
}
|
| 31 |
+
return { context: block.context, bindings: filteredBindings }
|
| 32 |
+
})
|
| 33 |
+
.filter(block => Object.keys(block.bindings).length > 0)
|
| 34 |
+
}
|
| 35 |
+
|
| 36 |
+
/**
|
| 37 |
+
* Generate a template keybindings.json file content.
|
| 38 |
+
* Creates a fully valid JSON file with all default bindings that users can customize.
|
| 39 |
+
*/
|
| 40 |
+
export function generateKeybindingsTemplate(): string {
|
| 41 |
+
// Filter out reserved shortcuts that cannot be rebound
|
| 42 |
+
const bindings = filterReservedShortcuts(DEFAULT_BINDINGS)
|
| 43 |
+
|
| 44 |
+
// Format as object wrapper with bindings array
|
| 45 |
+
const config = {
|
| 46 |
+
$schema: 'https://www.schemastore.org/claude-code-keybindings.json',
|
| 47 |
+
$docs: 'https://code.claude.com/docs/en/keybindings',
|
| 48 |
+
bindings,
|
| 49 |
+
}
|
| 50 |
+
|
| 51 |
+
return jsonStringify(config, null, 2) + '\n'
|
| 52 |
+
}
|
src/keybindings/useKeybinding.ts
ADDED
|
@@ -0,0 +1,196 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { useCallback, useEffect } from 'react'
|
| 2 |
+
import type { InputEvent } from '../ink/events/input-event.js'
|
| 3 |
+
import { type Key, useInput } from '../ink.js'
|
| 4 |
+
import { useOptionalKeybindingContext } from './KeybindingContext.js'
|
| 5 |
+
import type { KeybindingContextName } from './types.js'
|
| 6 |
+
|
| 7 |
+
type Options = {
|
| 8 |
+
/** Which context this binding belongs to (default: 'Global') */
|
| 9 |
+
context?: KeybindingContextName
|
| 10 |
+
/** Only handle when active (like useInput's isActive) */
|
| 11 |
+
isActive?: boolean
|
| 12 |
+
}
|
| 13 |
+
|
| 14 |
+
/**
|
| 15 |
+
* Ink-native hook for handling a keybinding.
|
| 16 |
+
*
|
| 17 |
+
* The handler stays in the component (React way).
|
| 18 |
+
* The binding (keystroke → action) comes from config.
|
| 19 |
+
*
|
| 20 |
+
* Supports chord sequences (e.g., "ctrl+k ctrl+s"). When a chord is started,
|
| 21 |
+
* the hook will manage the pending state automatically.
|
| 22 |
+
*
|
| 23 |
+
* Uses stopImmediatePropagation() to prevent other handlers from firing
|
| 24 |
+
* once this binding is handled.
|
| 25 |
+
*
|
| 26 |
+
* @example
|
| 27 |
+
* ```tsx
|
| 28 |
+
* useKeybinding('app:toggleTodos', () => {
|
| 29 |
+
* setShowTodos(prev => !prev)
|
| 30 |
+
* }, { context: 'Global' })
|
| 31 |
+
* ```
|
| 32 |
+
*/
|
| 33 |
+
export function useKeybinding(
|
| 34 |
+
action: string,
|
| 35 |
+
handler: () => void | false | Promise<void>,
|
| 36 |
+
options: Options = {},
|
| 37 |
+
): void {
|
| 38 |
+
const { context = 'Global', isActive = true } = options
|
| 39 |
+
const keybindingContext = useOptionalKeybindingContext()
|
| 40 |
+
|
| 41 |
+
// Register handler with the context for ChordInterceptor to invoke
|
| 42 |
+
useEffect(() => {
|
| 43 |
+
if (!keybindingContext || !isActive) return
|
| 44 |
+
return keybindingContext.registerHandler({ action, context, handler })
|
| 45 |
+
}, [action, context, handler, keybindingContext, isActive])
|
| 46 |
+
|
| 47 |
+
const handleInput = useCallback(
|
| 48 |
+
(input: string, key: Key, event: InputEvent) => {
|
| 49 |
+
// If no keybinding context available, skip resolution
|
| 50 |
+
if (!keybindingContext) return
|
| 51 |
+
|
| 52 |
+
// Build context list: registered active contexts + this context + Global
|
| 53 |
+
// More specific contexts (registered ones) take precedence over Global
|
| 54 |
+
const contextsToCheck: KeybindingContextName[] = [
|
| 55 |
+
...keybindingContext.activeContexts,
|
| 56 |
+
context,
|
| 57 |
+
'Global',
|
| 58 |
+
]
|
| 59 |
+
// Deduplicate while preserving order (first occurrence wins for priority)
|
| 60 |
+
const uniqueContexts = [...new Set(contextsToCheck)]
|
| 61 |
+
|
| 62 |
+
const result = keybindingContext.resolve(input, key, uniqueContexts)
|
| 63 |
+
|
| 64 |
+
switch (result.type) {
|
| 65 |
+
case 'match':
|
| 66 |
+
// Chord completed (if any) - clear pending state
|
| 67 |
+
keybindingContext.setPendingChord(null)
|
| 68 |
+
if (result.action === action) {
|
| 69 |
+
if (handler() !== false) {
|
| 70 |
+
event.stopImmediatePropagation()
|
| 71 |
+
}
|
| 72 |
+
}
|
| 73 |
+
break
|
| 74 |
+
case 'chord_started':
|
| 75 |
+
// User started a chord sequence - update pending state
|
| 76 |
+
keybindingContext.setPendingChord(result.pending)
|
| 77 |
+
event.stopImmediatePropagation()
|
| 78 |
+
break
|
| 79 |
+
case 'chord_cancelled':
|
| 80 |
+
// Chord was cancelled (escape or invalid key)
|
| 81 |
+
keybindingContext.setPendingChord(null)
|
| 82 |
+
break
|
| 83 |
+
case 'unbound':
|
| 84 |
+
// Explicitly unbound - clear any pending chord
|
| 85 |
+
keybindingContext.setPendingChord(null)
|
| 86 |
+
event.stopImmediatePropagation()
|
| 87 |
+
break
|
| 88 |
+
case 'none':
|
| 89 |
+
// No match - let other handlers try
|
| 90 |
+
break
|
| 91 |
+
}
|
| 92 |
+
},
|
| 93 |
+
[action, context, handler, keybindingContext],
|
| 94 |
+
)
|
| 95 |
+
|
| 96 |
+
useInput(handleInput, { isActive })
|
| 97 |
+
}
|
| 98 |
+
|
| 99 |
+
/**
|
| 100 |
+
* Handle multiple keybindings in one hook (reduces useInput calls).
|
| 101 |
+
*
|
| 102 |
+
* Supports chord sequences. When a chord is started, the hook will
|
| 103 |
+
* manage the pending state automatically.
|
| 104 |
+
*
|
| 105 |
+
* @example
|
| 106 |
+
* ```tsx
|
| 107 |
+
* useKeybindings({
|
| 108 |
+
* 'chat:submit': () => handleSubmit(),
|
| 109 |
+
* 'chat:cancel': () => handleCancel(),
|
| 110 |
+
* }, { context: 'Chat' })
|
| 111 |
+
* ```
|
| 112 |
+
*/
|
| 113 |
+
export function useKeybindings(
|
| 114 |
+
// Handler returning `false` means "not consumed" — the event propagates
|
| 115 |
+
// to later useInput/useKeybindings handlers. Useful for fall-through:
|
| 116 |
+
// e.g. ScrollKeybindingHandler's scroll:line* returns false when the
|
| 117 |
+
// ScrollBox content fits (scroll is a no-op), letting a child component's
|
| 118 |
+
// handler take the wheel event for list navigation instead. Promise<void>
|
| 119 |
+
// is allowed for fire-and-forget async handlers (the `!== false` check
|
| 120 |
+
// only skips propagation for a sync `false`, not a pending Promise).
|
| 121 |
+
handlers: Record<string, () => void | false | Promise<void>>,
|
| 122 |
+
options: Options = {},
|
| 123 |
+
): void {
|
| 124 |
+
const { context = 'Global', isActive = true } = options
|
| 125 |
+
const keybindingContext = useOptionalKeybindingContext()
|
| 126 |
+
|
| 127 |
+
// Register all handlers with the context for ChordInterceptor to invoke
|
| 128 |
+
useEffect(() => {
|
| 129 |
+
if (!keybindingContext || !isActive) return
|
| 130 |
+
|
| 131 |
+
const unregisterFns: Array<() => void> = []
|
| 132 |
+
for (const [action, handler] of Object.entries(handlers)) {
|
| 133 |
+
unregisterFns.push(
|
| 134 |
+
keybindingContext.registerHandler({ action, context, handler }),
|
| 135 |
+
)
|
| 136 |
+
}
|
| 137 |
+
|
| 138 |
+
return () => {
|
| 139 |
+
for (const unregister of unregisterFns) {
|
| 140 |
+
unregister()
|
| 141 |
+
}
|
| 142 |
+
}
|
| 143 |
+
}, [context, handlers, keybindingContext, isActive])
|
| 144 |
+
|
| 145 |
+
const handleInput = useCallback(
|
| 146 |
+
(input: string, key: Key, event: InputEvent) => {
|
| 147 |
+
// If no keybinding context available, skip resolution
|
| 148 |
+
if (!keybindingContext) return
|
| 149 |
+
|
| 150 |
+
// Build context list: registered active contexts + this context + Global
|
| 151 |
+
// More specific contexts (registered ones) take precedence over Global
|
| 152 |
+
const contextsToCheck: KeybindingContextName[] = [
|
| 153 |
+
...keybindingContext.activeContexts,
|
| 154 |
+
context,
|
| 155 |
+
'Global',
|
| 156 |
+
]
|
| 157 |
+
// Deduplicate while preserving order (first occurrence wins for priority)
|
| 158 |
+
const uniqueContexts = [...new Set(contextsToCheck)]
|
| 159 |
+
|
| 160 |
+
const result = keybindingContext.resolve(input, key, uniqueContexts)
|
| 161 |
+
|
| 162 |
+
switch (result.type) {
|
| 163 |
+
case 'match':
|
| 164 |
+
// Chord completed (if any) - clear pending state
|
| 165 |
+
keybindingContext.setPendingChord(null)
|
| 166 |
+
if (result.action in handlers) {
|
| 167 |
+
const handler = handlers[result.action]
|
| 168 |
+
if (handler && handler() !== false) {
|
| 169 |
+
event.stopImmediatePropagation()
|
| 170 |
+
}
|
| 171 |
+
}
|
| 172 |
+
break
|
| 173 |
+
case 'chord_started':
|
| 174 |
+
// User started a chord sequence - update pending state
|
| 175 |
+
keybindingContext.setPendingChord(result.pending)
|
| 176 |
+
event.stopImmediatePropagation()
|
| 177 |
+
break
|
| 178 |
+
case 'chord_cancelled':
|
| 179 |
+
// Chord was cancelled (escape or invalid key)
|
| 180 |
+
keybindingContext.setPendingChord(null)
|
| 181 |
+
break
|
| 182 |
+
case 'unbound':
|
| 183 |
+
// Explicitly unbound - clear any pending chord
|
| 184 |
+
keybindingContext.setPendingChord(null)
|
| 185 |
+
event.stopImmediatePropagation()
|
| 186 |
+
break
|
| 187 |
+
case 'none':
|
| 188 |
+
// No match - let other handlers try
|
| 189 |
+
break
|
| 190 |
+
}
|
| 191 |
+
},
|
| 192 |
+
[context, handlers, keybindingContext],
|
| 193 |
+
)
|
| 194 |
+
|
| 195 |
+
useInput(handleInput, { isActive })
|
| 196 |
+
}
|
src/keybindings/useShortcutDisplay.ts
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { useEffect, useRef } from 'react'
|
| 2 |
+
import {
|
| 3 |
+
type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
| 4 |
+
logEvent,
|
| 5 |
+
} from '../services/analytics/index.js'
|
| 6 |
+
import { useOptionalKeybindingContext } from './KeybindingContext.js'
|
| 7 |
+
import type { KeybindingContextName } from './types.js'
|
| 8 |
+
|
| 9 |
+
// TODO(keybindings-migration): Remove fallback parameter after migration is complete
|
| 10 |
+
// and we've confirmed no 'keybinding_fallback_used' events are being logged.
|
| 11 |
+
// The fallback exists as a safety net during migration - if bindings fail to load
|
| 12 |
+
// or an action isn't found, we fall back to hardcoded values. Once stable, callers
|
| 13 |
+
// should be able to trust that getBindingDisplayText always returns a value for
|
| 14 |
+
// known actions, and we can remove this defensive pattern.
|
| 15 |
+
|
| 16 |
+
/**
|
| 17 |
+
* Hook to get the display text for a configured shortcut.
|
| 18 |
+
* Returns the configured binding or a fallback if unavailable.
|
| 19 |
+
*
|
| 20 |
+
* @param action - The action name (e.g., 'app:toggleTranscript')
|
| 21 |
+
* @param context - The keybinding context (e.g., 'Global')
|
| 22 |
+
* @param fallback - Fallback text if keybinding context unavailable
|
| 23 |
+
* @returns The configured shortcut display text
|
| 24 |
+
*
|
| 25 |
+
* @example
|
| 26 |
+
* const expandShortcut = useShortcutDisplay('app:toggleTranscript', 'Global', 'ctrl+o')
|
| 27 |
+
* // Returns the user's configured binding, or 'ctrl+o' as default
|
| 28 |
+
*/
|
| 29 |
+
export function useShortcutDisplay(
|
| 30 |
+
action: string,
|
| 31 |
+
context: KeybindingContextName,
|
| 32 |
+
fallback: string,
|
| 33 |
+
): string {
|
| 34 |
+
const keybindingContext = useOptionalKeybindingContext()
|
| 35 |
+
const resolved = keybindingContext?.getDisplayText(action, context)
|
| 36 |
+
const isFallback = resolved === undefined
|
| 37 |
+
const reason = keybindingContext ? 'action_not_found' : 'no_context'
|
| 38 |
+
|
| 39 |
+
// Log fallback usage once per mount (not on every render) to avoid
|
| 40 |
+
// flooding analytics with events from frequent re-renders.
|
| 41 |
+
const hasLoggedRef = useRef(false)
|
| 42 |
+
useEffect(() => {
|
| 43 |
+
if (isFallback && !hasLoggedRef.current) {
|
| 44 |
+
hasLoggedRef.current = true
|
| 45 |
+
logEvent('tengu_keybinding_fallback_used', {
|
| 46 |
+
action:
|
| 47 |
+
action as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
| 48 |
+
context:
|
| 49 |
+
context as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
| 50 |
+
fallback:
|
| 51 |
+
fallback as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
| 52 |
+
reason:
|
| 53 |
+
reason as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
| 54 |
+
})
|
| 55 |
+
}
|
| 56 |
+
}, [isFallback, action, context, fallback, reason])
|
| 57 |
+
|
| 58 |
+
return isFallback ? fallback : resolved
|
| 59 |
+
}
|
src/keybindings/validate.ts
ADDED
|
@@ -0,0 +1,498 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { plural } from '../utils/stringUtils.js'
|
| 2 |
+
import { chordToString, parseChord, parseKeystroke } from './parser.js'
|
| 3 |
+
import {
|
| 4 |
+
getReservedShortcuts,
|
| 5 |
+
normalizeKeyForComparison,
|
| 6 |
+
} from './reservedShortcuts.js'
|
| 7 |
+
import type {
|
| 8 |
+
KeybindingBlock,
|
| 9 |
+
KeybindingContextName,
|
| 10 |
+
ParsedBinding,
|
| 11 |
+
} from './types.js'
|
| 12 |
+
|
| 13 |
+
/**
|
| 14 |
+
* Types of validation issues that can occur with keybindings.
|
| 15 |
+
*/
|
| 16 |
+
export type KeybindingWarningType =
|
| 17 |
+
| 'parse_error'
|
| 18 |
+
| 'duplicate'
|
| 19 |
+
| 'reserved'
|
| 20 |
+
| 'invalid_context'
|
| 21 |
+
| 'invalid_action'
|
| 22 |
+
|
| 23 |
+
/**
|
| 24 |
+
* A warning or error about a keybinding configuration issue.
|
| 25 |
+
*/
|
| 26 |
+
export type KeybindingWarning = {
|
| 27 |
+
type: KeybindingWarningType
|
| 28 |
+
severity: 'error' | 'warning'
|
| 29 |
+
message: string
|
| 30 |
+
key?: string
|
| 31 |
+
context?: string
|
| 32 |
+
action?: string
|
| 33 |
+
suggestion?: string
|
| 34 |
+
}
|
| 35 |
+
|
| 36 |
+
/**
|
| 37 |
+
* Type guard to check if an object is a valid KeybindingBlock.
|
| 38 |
+
*/
|
| 39 |
+
function isKeybindingBlock(obj: unknown): obj is KeybindingBlock {
|
| 40 |
+
if (typeof obj !== 'object' || obj === null) return false
|
| 41 |
+
const b = obj as Record<string, unknown>
|
| 42 |
+
return (
|
| 43 |
+
typeof b.context === 'string' &&
|
| 44 |
+
typeof b.bindings === 'object' &&
|
| 45 |
+
b.bindings !== null
|
| 46 |
+
)
|
| 47 |
+
}
|
| 48 |
+
|
| 49 |
+
/**
|
| 50 |
+
* Type guard to check if an array contains only valid KeybindingBlocks.
|
| 51 |
+
*/
|
| 52 |
+
function isKeybindingBlockArray(arr: unknown): arr is KeybindingBlock[] {
|
| 53 |
+
return Array.isArray(arr) && arr.every(isKeybindingBlock)
|
| 54 |
+
}
|
| 55 |
+
|
| 56 |
+
/**
|
| 57 |
+
* Valid context names for keybindings.
|
| 58 |
+
* Must match KeybindingContextName in types.ts
|
| 59 |
+
*/
|
| 60 |
+
const VALID_CONTEXTS: KeybindingContextName[] = [
|
| 61 |
+
'Global',
|
| 62 |
+
'Chat',
|
| 63 |
+
'Autocomplete',
|
| 64 |
+
'Confirmation',
|
| 65 |
+
'Help',
|
| 66 |
+
'Transcript',
|
| 67 |
+
'HistorySearch',
|
| 68 |
+
'Task',
|
| 69 |
+
'ThemePicker',
|
| 70 |
+
'Settings',
|
| 71 |
+
'Tabs',
|
| 72 |
+
'Attachments',
|
| 73 |
+
'Footer',
|
| 74 |
+
'MessageSelector',
|
| 75 |
+
'DiffDialog',
|
| 76 |
+
'ModelPicker',
|
| 77 |
+
'Select',
|
| 78 |
+
'Plugin',
|
| 79 |
+
]
|
| 80 |
+
|
| 81 |
+
/**
|
| 82 |
+
* Type guard to check if a string is a valid context name.
|
| 83 |
+
*/
|
| 84 |
+
function isValidContext(value: string): value is KeybindingContextName {
|
| 85 |
+
return (VALID_CONTEXTS as readonly string[]).includes(value)
|
| 86 |
+
}
|
| 87 |
+
|
| 88 |
+
/**
|
| 89 |
+
* Validate a single keystroke string and return any parse errors.
|
| 90 |
+
*/
|
| 91 |
+
function validateKeystroke(keystroke: string): KeybindingWarning | null {
|
| 92 |
+
const parts = keystroke.toLowerCase().split('+')
|
| 93 |
+
|
| 94 |
+
for (const part of parts) {
|
| 95 |
+
const trimmed = part.trim()
|
| 96 |
+
if (!trimmed) {
|
| 97 |
+
return {
|
| 98 |
+
type: 'parse_error',
|
| 99 |
+
severity: 'error',
|
| 100 |
+
message: `Empty key part in "${keystroke}"`,
|
| 101 |
+
key: keystroke,
|
| 102 |
+
suggestion: 'Remove extra "+" characters',
|
| 103 |
+
}
|
| 104 |
+
}
|
| 105 |
+
}
|
| 106 |
+
|
| 107 |
+
// Try to parse and see if it fails
|
| 108 |
+
const parsed = parseKeystroke(keystroke)
|
| 109 |
+
if (
|
| 110 |
+
!parsed.key &&
|
| 111 |
+
!parsed.ctrl &&
|
| 112 |
+
!parsed.alt &&
|
| 113 |
+
!parsed.shift &&
|
| 114 |
+
!parsed.meta
|
| 115 |
+
) {
|
| 116 |
+
return {
|
| 117 |
+
type: 'parse_error',
|
| 118 |
+
severity: 'error',
|
| 119 |
+
message: `Could not parse keystroke "${keystroke}"`,
|
| 120 |
+
key: keystroke,
|
| 121 |
+
}
|
| 122 |
+
}
|
| 123 |
+
|
| 124 |
+
return null
|
| 125 |
+
}
|
| 126 |
+
|
| 127 |
+
/**
|
| 128 |
+
* Validate a keybinding block from user config.
|
| 129 |
+
*/
|
| 130 |
+
function validateBlock(
|
| 131 |
+
block: unknown,
|
| 132 |
+
blockIndex: number,
|
| 133 |
+
): KeybindingWarning[] {
|
| 134 |
+
const warnings: KeybindingWarning[] = []
|
| 135 |
+
|
| 136 |
+
if (typeof block !== 'object' || block === null) {
|
| 137 |
+
warnings.push({
|
| 138 |
+
type: 'parse_error',
|
| 139 |
+
severity: 'error',
|
| 140 |
+
message: `Keybinding block ${blockIndex + 1} is not an object`,
|
| 141 |
+
})
|
| 142 |
+
return warnings
|
| 143 |
+
}
|
| 144 |
+
|
| 145 |
+
const b = block as Record<string, unknown>
|
| 146 |
+
|
| 147 |
+
// Validate context - extract to narrowed variable for type safety
|
| 148 |
+
const rawContext = b.context
|
| 149 |
+
let contextName: string | undefined
|
| 150 |
+
if (typeof rawContext !== 'string') {
|
| 151 |
+
warnings.push({
|
| 152 |
+
type: 'parse_error',
|
| 153 |
+
severity: 'error',
|
| 154 |
+
message: `Keybinding block ${blockIndex + 1} missing "context" field`,
|
| 155 |
+
})
|
| 156 |
+
} else if (!isValidContext(rawContext)) {
|
| 157 |
+
warnings.push({
|
| 158 |
+
type: 'invalid_context',
|
| 159 |
+
severity: 'error',
|
| 160 |
+
message: `Unknown context "${rawContext}"`,
|
| 161 |
+
context: rawContext,
|
| 162 |
+
suggestion: `Valid contexts: ${VALID_CONTEXTS.join(', ')}`,
|
| 163 |
+
})
|
| 164 |
+
} else {
|
| 165 |
+
contextName = rawContext
|
| 166 |
+
}
|
| 167 |
+
|
| 168 |
+
// Validate bindings
|
| 169 |
+
if (typeof b.bindings !== 'object' || b.bindings === null) {
|
| 170 |
+
warnings.push({
|
| 171 |
+
type: 'parse_error',
|
| 172 |
+
severity: 'error',
|
| 173 |
+
message: `Keybinding block ${blockIndex + 1} missing "bindings" field`,
|
| 174 |
+
})
|
| 175 |
+
return warnings
|
| 176 |
+
}
|
| 177 |
+
|
| 178 |
+
const bindings = b.bindings as Record<string, unknown>
|
| 179 |
+
for (const [key, action] of Object.entries(bindings)) {
|
| 180 |
+
// Validate key syntax
|
| 181 |
+
const keyError = validateKeystroke(key)
|
| 182 |
+
if (keyError) {
|
| 183 |
+
keyError.context = contextName
|
| 184 |
+
warnings.push(keyError)
|
| 185 |
+
}
|
| 186 |
+
|
| 187 |
+
// Validate action
|
| 188 |
+
if (action !== null && typeof action !== 'string') {
|
| 189 |
+
warnings.push({
|
| 190 |
+
type: 'invalid_action',
|
| 191 |
+
severity: 'error',
|
| 192 |
+
message: `Invalid action for "${key}": must be a string or null`,
|
| 193 |
+
key,
|
| 194 |
+
context: contextName,
|
| 195 |
+
})
|
| 196 |
+
} else if (typeof action === 'string' && action.startsWith('command:')) {
|
| 197 |
+
// Validate command binding format
|
| 198 |
+
if (!/^command:[a-zA-Z0-9:\-_]+$/.test(action)) {
|
| 199 |
+
warnings.push({
|
| 200 |
+
type: 'invalid_action',
|
| 201 |
+
severity: 'warning',
|
| 202 |
+
message: `Invalid command binding "${action}" for "${key}": command name may only contain alphanumeric characters, colons, hyphens, and underscores`,
|
| 203 |
+
key,
|
| 204 |
+
context: contextName,
|
| 205 |
+
action,
|
| 206 |
+
})
|
| 207 |
+
}
|
| 208 |
+
// Command bindings must be in Chat context
|
| 209 |
+
if (contextName && contextName !== 'Chat') {
|
| 210 |
+
warnings.push({
|
| 211 |
+
type: 'invalid_action',
|
| 212 |
+
severity: 'warning',
|
| 213 |
+
message: `Command binding "${action}" must be in "Chat" context, not "${contextName}"`,
|
| 214 |
+
key,
|
| 215 |
+
context: contextName,
|
| 216 |
+
action,
|
| 217 |
+
suggestion: 'Move this binding to a block with "context": "Chat"',
|
| 218 |
+
})
|
| 219 |
+
}
|
| 220 |
+
} else if (action === 'voice:pushToTalk') {
|
| 221 |
+
// Hold detection needs OS auto-repeat. Bare letters print into the
|
| 222 |
+
// input during warmup and the activation strip is best-effort —
|
| 223 |
+
// space (default) or a modifier combo like meta+k avoid that.
|
| 224 |
+
const ks = parseChord(key)[0]
|
| 225 |
+
if (
|
| 226 |
+
ks &&
|
| 227 |
+
!ks.ctrl &&
|
| 228 |
+
!ks.alt &&
|
| 229 |
+
!ks.shift &&
|
| 230 |
+
!ks.meta &&
|
| 231 |
+
!ks.super &&
|
| 232 |
+
/^[a-z]$/.test(ks.key)
|
| 233 |
+
) {
|
| 234 |
+
warnings.push({
|
| 235 |
+
type: 'invalid_action',
|
| 236 |
+
severity: 'warning',
|
| 237 |
+
message: `Binding "${key}" to voice:pushToTalk prints into the input during warmup; use space or a modifier combo like meta+k`,
|
| 238 |
+
key,
|
| 239 |
+
context: contextName,
|
| 240 |
+
action,
|
| 241 |
+
})
|
| 242 |
+
}
|
| 243 |
+
}
|
| 244 |
+
}
|
| 245 |
+
|
| 246 |
+
return warnings
|
| 247 |
+
}
|
| 248 |
+
|
| 249 |
+
/**
|
| 250 |
+
* Detect duplicate keys within the same bindings block in a JSON string.
|
| 251 |
+
* JSON.parse silently uses the last value for duplicate keys,
|
| 252 |
+
* so we need to check the raw string to warn users.
|
| 253 |
+
*
|
| 254 |
+
* Only warns about duplicates within the same context's bindings object.
|
| 255 |
+
* Duplicates across different contexts are allowed (e.g., "enter" in Chat
|
| 256 |
+
* and "enter" in Confirmation).
|
| 257 |
+
*/
|
| 258 |
+
export function checkDuplicateKeysInJson(
|
| 259 |
+
jsonString: string,
|
| 260 |
+
): KeybindingWarning[] {
|
| 261 |
+
const warnings: KeybindingWarning[] = []
|
| 262 |
+
|
| 263 |
+
// Find each "bindings" block and check for duplicates within it
|
| 264 |
+
// Pattern: "bindings" : { ... }
|
| 265 |
+
const bindingsBlockPattern =
|
| 266 |
+
/"bindings"\s*:\s*\{([^{}]*(?:\{[^{}]*\}[^{}]*)*)\}/g
|
| 267 |
+
|
| 268 |
+
let blockMatch
|
| 269 |
+
while ((blockMatch = bindingsBlockPattern.exec(jsonString)) !== null) {
|
| 270 |
+
const blockContent = blockMatch[1]
|
| 271 |
+
if (!blockContent) continue
|
| 272 |
+
|
| 273 |
+
// Find the context for this block by looking backwards
|
| 274 |
+
const textBeforeBlock = jsonString.slice(0, blockMatch.index)
|
| 275 |
+
const contextMatch = textBeforeBlock.match(
|
| 276 |
+
/"context"\s*:\s*"([^"]+)"[^{]*$/,
|
| 277 |
+
)
|
| 278 |
+
const context = contextMatch?.[1] ?? 'unknown'
|
| 279 |
+
|
| 280 |
+
// Find all keys within this bindings block
|
| 281 |
+
const keyPattern = /"([^"]+)"\s*:/g
|
| 282 |
+
const keysByName = new Map<string, number>()
|
| 283 |
+
|
| 284 |
+
let keyMatch
|
| 285 |
+
while ((keyMatch = keyPattern.exec(blockContent)) !== null) {
|
| 286 |
+
const key = keyMatch[1]
|
| 287 |
+
if (!key) continue
|
| 288 |
+
|
| 289 |
+
const count = (keysByName.get(key) ?? 0) + 1
|
| 290 |
+
keysByName.set(key, count)
|
| 291 |
+
|
| 292 |
+
if (count === 2) {
|
| 293 |
+
// Only warn on the second occurrence
|
| 294 |
+
warnings.push({
|
| 295 |
+
type: 'duplicate',
|
| 296 |
+
severity: 'warning',
|
| 297 |
+
message: `Duplicate key "${key}" in ${context} bindings`,
|
| 298 |
+
key,
|
| 299 |
+
context,
|
| 300 |
+
suggestion: `This key appears multiple times in the same context. JSON uses the last value, earlier values are ignored.`,
|
| 301 |
+
})
|
| 302 |
+
}
|
| 303 |
+
}
|
| 304 |
+
}
|
| 305 |
+
|
| 306 |
+
return warnings
|
| 307 |
+
}
|
| 308 |
+
|
| 309 |
+
/**
|
| 310 |
+
* Validate user keybinding config and return all warnings.
|
| 311 |
+
*/
|
| 312 |
+
export function validateUserConfig(userBlocks: unknown): KeybindingWarning[] {
|
| 313 |
+
const warnings: KeybindingWarning[] = []
|
| 314 |
+
|
| 315 |
+
if (!Array.isArray(userBlocks)) {
|
| 316 |
+
warnings.push({
|
| 317 |
+
type: 'parse_error',
|
| 318 |
+
severity: 'error',
|
| 319 |
+
message: 'keybindings.json must contain an array',
|
| 320 |
+
suggestion: 'Wrap your bindings in [ ]',
|
| 321 |
+
})
|
| 322 |
+
return warnings
|
| 323 |
+
}
|
| 324 |
+
|
| 325 |
+
for (let i = 0; i < userBlocks.length; i++) {
|
| 326 |
+
warnings.push(...validateBlock(userBlocks[i], i))
|
| 327 |
+
}
|
| 328 |
+
|
| 329 |
+
return warnings
|
| 330 |
+
}
|
| 331 |
+
|
| 332 |
+
/**
|
| 333 |
+
* Check for duplicate bindings within the same context.
|
| 334 |
+
* Only checks user bindings (not default + user merged).
|
| 335 |
+
*/
|
| 336 |
+
export function checkDuplicates(
|
| 337 |
+
blocks: KeybindingBlock[],
|
| 338 |
+
): KeybindingWarning[] {
|
| 339 |
+
const warnings: KeybindingWarning[] = []
|
| 340 |
+
const seenByContext = new Map<string, Map<string, string>>()
|
| 341 |
+
|
| 342 |
+
for (const block of blocks) {
|
| 343 |
+
const contextMap =
|
| 344 |
+
seenByContext.get(block.context) ?? new Map<string, string>()
|
| 345 |
+
seenByContext.set(block.context, contextMap)
|
| 346 |
+
|
| 347 |
+
for (const [key, action] of Object.entries(block.bindings)) {
|
| 348 |
+
const normalizedKey = normalizeKeyForComparison(key)
|
| 349 |
+
const existingAction = contextMap.get(normalizedKey)
|
| 350 |
+
|
| 351 |
+
if (existingAction && existingAction !== action) {
|
| 352 |
+
warnings.push({
|
| 353 |
+
type: 'duplicate',
|
| 354 |
+
severity: 'warning',
|
| 355 |
+
message: `Duplicate binding "${key}" in ${block.context} context`,
|
| 356 |
+
key,
|
| 357 |
+
context: block.context,
|
| 358 |
+
action: action ?? 'null (unbind)',
|
| 359 |
+
suggestion: `Previously bound to "${existingAction}". Only the last binding will be used.`,
|
| 360 |
+
})
|
| 361 |
+
}
|
| 362 |
+
|
| 363 |
+
contextMap.set(normalizedKey, action ?? 'null')
|
| 364 |
+
}
|
| 365 |
+
}
|
| 366 |
+
|
| 367 |
+
return warnings
|
| 368 |
+
}
|
| 369 |
+
|
| 370 |
+
/**
|
| 371 |
+
* Check for reserved shortcuts that may not work.
|
| 372 |
+
*/
|
| 373 |
+
export function checkReservedShortcuts(
|
| 374 |
+
bindings: ParsedBinding[],
|
| 375 |
+
): KeybindingWarning[] {
|
| 376 |
+
const warnings: KeybindingWarning[] = []
|
| 377 |
+
const reserved = getReservedShortcuts()
|
| 378 |
+
|
| 379 |
+
for (const binding of bindings) {
|
| 380 |
+
const keyDisplay = chordToString(binding.chord)
|
| 381 |
+
const normalizedKey = normalizeKeyForComparison(keyDisplay)
|
| 382 |
+
|
| 383 |
+
// Check against reserved shortcuts
|
| 384 |
+
for (const res of reserved) {
|
| 385 |
+
if (normalizeKeyForComparison(res.key) === normalizedKey) {
|
| 386 |
+
warnings.push({
|
| 387 |
+
type: 'reserved',
|
| 388 |
+
severity: res.severity,
|
| 389 |
+
message: `"${keyDisplay}" may not work: ${res.reason}`,
|
| 390 |
+
key: keyDisplay,
|
| 391 |
+
context: binding.context,
|
| 392 |
+
action: binding.action ?? undefined,
|
| 393 |
+
})
|
| 394 |
+
}
|
| 395 |
+
}
|
| 396 |
+
}
|
| 397 |
+
|
| 398 |
+
return warnings
|
| 399 |
+
}
|
| 400 |
+
|
| 401 |
+
/**
|
| 402 |
+
* Parse user blocks into bindings for validation.
|
| 403 |
+
* This is separate from the main parser to avoid importing it.
|
| 404 |
+
*/
|
| 405 |
+
function getUserBindingsForValidation(
|
| 406 |
+
userBlocks: KeybindingBlock[],
|
| 407 |
+
): ParsedBinding[] {
|
| 408 |
+
const bindings: ParsedBinding[] = []
|
| 409 |
+
for (const block of userBlocks) {
|
| 410 |
+
for (const [key, action] of Object.entries(block.bindings)) {
|
| 411 |
+
const chord = key.split(' ').map(k => parseKeystroke(k))
|
| 412 |
+
bindings.push({
|
| 413 |
+
chord,
|
| 414 |
+
action,
|
| 415 |
+
context: block.context,
|
| 416 |
+
})
|
| 417 |
+
}
|
| 418 |
+
}
|
| 419 |
+
return bindings
|
| 420 |
+
}
|
| 421 |
+
|
| 422 |
+
/**
|
| 423 |
+
* Run all validations and return combined warnings.
|
| 424 |
+
*/
|
| 425 |
+
export function validateBindings(
|
| 426 |
+
userBlocks: unknown,
|
| 427 |
+
_parsedBindings: ParsedBinding[],
|
| 428 |
+
): KeybindingWarning[] {
|
| 429 |
+
const warnings: KeybindingWarning[] = []
|
| 430 |
+
|
| 431 |
+
// Validate user config structure
|
| 432 |
+
warnings.push(...validateUserConfig(userBlocks))
|
| 433 |
+
|
| 434 |
+
// Check for duplicates in user config
|
| 435 |
+
if (isKeybindingBlockArray(userBlocks)) {
|
| 436 |
+
warnings.push(...checkDuplicates(userBlocks))
|
| 437 |
+
|
| 438 |
+
// Check for reserved/conflicting shortcuts - only check USER bindings
|
| 439 |
+
const userBindings = getUserBindingsForValidation(userBlocks)
|
| 440 |
+
warnings.push(...checkReservedShortcuts(userBindings))
|
| 441 |
+
}
|
| 442 |
+
|
| 443 |
+
// Deduplicate warnings (same key+context+type)
|
| 444 |
+
const seen = new Set<string>()
|
| 445 |
+
return warnings.filter(w => {
|
| 446 |
+
const key = `${w.type}:${w.key}:${w.context}`
|
| 447 |
+
if (seen.has(key)) return false
|
| 448 |
+
seen.add(key)
|
| 449 |
+
return true
|
| 450 |
+
})
|
| 451 |
+
}
|
| 452 |
+
|
| 453 |
+
/**
|
| 454 |
+
* Format a warning for display to the user.
|
| 455 |
+
*/
|
| 456 |
+
export function formatWarning(warning: KeybindingWarning): string {
|
| 457 |
+
const icon = warning.severity === 'error' ? '✗' : '⚠'
|
| 458 |
+
let msg = `${icon} Keybinding ${warning.severity}: ${warning.message}`
|
| 459 |
+
|
| 460 |
+
if (warning.suggestion) {
|
| 461 |
+
msg += `\n ${warning.suggestion}`
|
| 462 |
+
}
|
| 463 |
+
|
| 464 |
+
return msg
|
| 465 |
+
}
|
| 466 |
+
|
| 467 |
+
/**
|
| 468 |
+
* Format multiple warnings for display.
|
| 469 |
+
*/
|
| 470 |
+
export function formatWarnings(warnings: KeybindingWarning[]): string {
|
| 471 |
+
if (warnings.length === 0) return ''
|
| 472 |
+
|
| 473 |
+
const errors = warnings.filter(w => w.severity === 'error')
|
| 474 |
+
const warns = warnings.filter(w => w.severity === 'warning')
|
| 475 |
+
|
| 476 |
+
const lines: string[] = []
|
| 477 |
+
|
| 478 |
+
if (errors.length > 0) {
|
| 479 |
+
lines.push(
|
| 480 |
+
`Found ${errors.length} keybinding ${plural(errors.length, 'error')}:`,
|
| 481 |
+
)
|
| 482 |
+
for (const e of errors) {
|
| 483 |
+
lines.push(formatWarning(e))
|
| 484 |
+
}
|
| 485 |
+
}
|
| 486 |
+
|
| 487 |
+
if (warns.length > 0) {
|
| 488 |
+
if (lines.length > 0) lines.push('')
|
| 489 |
+
lines.push(
|
| 490 |
+
`Found ${warns.length} keybinding ${plural(warns.length, 'warning')}:`,
|
| 491 |
+
)
|
| 492 |
+
for (const w of warns) {
|
| 493 |
+
lines.push(formatWarning(w))
|
| 494 |
+
}
|
| 495 |
+
}
|
| 496 |
+
|
| 497 |
+
return lines.join('\n')
|
| 498 |
+
}
|
src/memdir/findRelevantMemories.ts
ADDED
|
@@ -0,0 +1,141 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { feature } from 'bun:bundle'
|
| 2 |
+
import { logForDebugging } from '../utils/debug.js'
|
| 3 |
+
import { errorMessage } from '../utils/errors.js'
|
| 4 |
+
import { getDefaultSonnetModel } from '../utils/model/model.js'
|
| 5 |
+
import { sideQuery } from '../utils/sideQuery.js'
|
| 6 |
+
import { jsonParse } from '../utils/slowOperations.js'
|
| 7 |
+
import {
|
| 8 |
+
formatMemoryManifest,
|
| 9 |
+
type MemoryHeader,
|
| 10 |
+
scanMemoryFiles,
|
| 11 |
+
} from './memoryScan.js'
|
| 12 |
+
|
| 13 |
+
export type RelevantMemory = {
|
| 14 |
+
path: string
|
| 15 |
+
mtimeMs: number
|
| 16 |
+
}
|
| 17 |
+
|
| 18 |
+
const SELECT_MEMORIES_SYSTEM_PROMPT = `You are selecting memories that will be useful to Claude Code as it processes a user's query. You will be given the user's query and a list of available memory files with their filenames and descriptions.
|
| 19 |
+
|
| 20 |
+
Return a list of filenames for the memories that will clearly be useful to Claude Code as it processes the user's query (up to 5). Only include memories that you are certain will be helpful based on their name and description.
|
| 21 |
+
- If you are unsure if a memory will be useful in processing the user's query, then do not include it in your list. Be selective and discerning.
|
| 22 |
+
- If there are no memories in the list that would clearly be useful, feel free to return an empty list.
|
| 23 |
+
- If a list of recently-used tools is provided, do not select memories that are usage reference or API documentation for those tools (Claude Code is already exercising them). DO still select memories containing warnings, gotchas, or known issues about those tools — active use is exactly when those matter.
|
| 24 |
+
`
|
| 25 |
+
|
| 26 |
+
/**
|
| 27 |
+
* Find memory files relevant to a query by scanning memory file headers
|
| 28 |
+
* and asking Sonnet to select the most relevant ones.
|
| 29 |
+
*
|
| 30 |
+
* Returns absolute file paths + mtime of the most relevant memories
|
| 31 |
+
* (up to 5). Excludes MEMORY.md (already loaded in system prompt).
|
| 32 |
+
* mtime is threaded through so callers can surface freshness to the
|
| 33 |
+
* main model without a second stat.
|
| 34 |
+
*
|
| 35 |
+
* `alreadySurfaced` filters paths shown in prior turns before the
|
| 36 |
+
* Sonnet call, so the selector spends its 5-slot budget on fresh
|
| 37 |
+
* candidates instead of re-picking files the caller will discard.
|
| 38 |
+
*/
|
| 39 |
+
export async function findRelevantMemories(
|
| 40 |
+
query: string,
|
| 41 |
+
memoryDir: string,
|
| 42 |
+
signal: AbortSignal,
|
| 43 |
+
recentTools: readonly string[] = [],
|
| 44 |
+
alreadySurfaced: ReadonlySet<string> = new Set(),
|
| 45 |
+
): Promise<RelevantMemory[]> {
|
| 46 |
+
const memories = (await scanMemoryFiles(memoryDir, signal)).filter(
|
| 47 |
+
m => !alreadySurfaced.has(m.filePath),
|
| 48 |
+
)
|
| 49 |
+
if (memories.length === 0) {
|
| 50 |
+
return []
|
| 51 |
+
}
|
| 52 |
+
|
| 53 |
+
const selectedFilenames = await selectRelevantMemories(
|
| 54 |
+
query,
|
| 55 |
+
memories,
|
| 56 |
+
signal,
|
| 57 |
+
recentTools,
|
| 58 |
+
)
|
| 59 |
+
const byFilename = new Map(memories.map(m => [m.filename, m]))
|
| 60 |
+
const selected = selectedFilenames
|
| 61 |
+
.map(filename => byFilename.get(filename))
|
| 62 |
+
.filter((m): m is MemoryHeader => m !== undefined)
|
| 63 |
+
|
| 64 |
+
// Fires even on empty selection: selection-rate needs the denominator,
|
| 65 |
+
// and -1 ages distinguish "ran, picked nothing" from "never ran".
|
| 66 |
+
if (feature('MEMORY_SHAPE_TELEMETRY')) {
|
| 67 |
+
/* eslint-disable @typescript-eslint/no-require-imports */
|
| 68 |
+
const { logMemoryRecallShape } =
|
| 69 |
+
require('./memoryShapeTelemetry.js') as typeof import('./memoryShapeTelemetry.js')
|
| 70 |
+
/* eslint-enable @typescript-eslint/no-require-imports */
|
| 71 |
+
logMemoryRecallShape(memories, selected)
|
| 72 |
+
}
|
| 73 |
+
|
| 74 |
+
return selected.map(m => ({ path: m.filePath, mtimeMs: m.mtimeMs }))
|
| 75 |
+
}
|
| 76 |
+
|
| 77 |
+
async function selectRelevantMemories(
|
| 78 |
+
query: string,
|
| 79 |
+
memories: MemoryHeader[],
|
| 80 |
+
signal: AbortSignal,
|
| 81 |
+
recentTools: readonly string[],
|
| 82 |
+
): Promise<string[]> {
|
| 83 |
+
const validFilenames = new Set(memories.map(m => m.filename))
|
| 84 |
+
|
| 85 |
+
const manifest = formatMemoryManifest(memories)
|
| 86 |
+
|
| 87 |
+
// When Claude Code is actively using a tool (e.g. mcp__X__spawn),
|
| 88 |
+
// surfacing that tool's reference docs is noise — the conversation
|
| 89 |
+
// already contains working usage. The selector otherwise matches
|
| 90 |
+
// on keyword overlap ("spawn" in query + "spawn" in a memory
|
| 91 |
+
// description → false positive).
|
| 92 |
+
const toolsSection =
|
| 93 |
+
recentTools.length > 0
|
| 94 |
+
? `\n\nRecently used tools: ${recentTools.join(', ')}`
|
| 95 |
+
: ''
|
| 96 |
+
|
| 97 |
+
try {
|
| 98 |
+
const result = await sideQuery({
|
| 99 |
+
model: getDefaultSonnetModel(),
|
| 100 |
+
system: SELECT_MEMORIES_SYSTEM_PROMPT,
|
| 101 |
+
skipSystemPromptPrefix: true,
|
| 102 |
+
messages: [
|
| 103 |
+
{
|
| 104 |
+
role: 'user',
|
| 105 |
+
content: `Query: ${query}\n\nAvailable memories:\n${manifest}${toolsSection}`,
|
| 106 |
+
},
|
| 107 |
+
],
|
| 108 |
+
max_tokens: 256,
|
| 109 |
+
output_format: {
|
| 110 |
+
type: 'json_schema',
|
| 111 |
+
schema: {
|
| 112 |
+
type: 'object',
|
| 113 |
+
properties: {
|
| 114 |
+
selected_memories: { type: 'array', items: { type: 'string' } },
|
| 115 |
+
},
|
| 116 |
+
required: ['selected_memories'],
|
| 117 |
+
additionalProperties: false,
|
| 118 |
+
},
|
| 119 |
+
},
|
| 120 |
+
signal,
|
| 121 |
+
querySource: 'memdir_relevance',
|
| 122 |
+
})
|
| 123 |
+
|
| 124 |
+
const textBlock = result.content.find(block => block.type === 'text')
|
| 125 |
+
if (!textBlock || textBlock.type !== 'text') {
|
| 126 |
+
return []
|
| 127 |
+
}
|
| 128 |
+
|
| 129 |
+
const parsed: { selected_memories: string[] } = jsonParse(textBlock.text)
|
| 130 |
+
return parsed.selected_memories.filter(f => validFilenames.has(f))
|
| 131 |
+
} catch (e) {
|
| 132 |
+
if (signal.aborted) {
|
| 133 |
+
return []
|
| 134 |
+
}
|
| 135 |
+
logForDebugging(
|
| 136 |
+
`[memdir] selectRelevantMemories failed: ${errorMessage(e)}`,
|
| 137 |
+
{ level: 'warn' },
|
| 138 |
+
)
|
| 139 |
+
return []
|
| 140 |
+
}
|
| 141 |
+
}
|
src/memdir/memdir.ts
ADDED
|
@@ -0,0 +1,507 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { feature } from 'bun:bundle'
|
| 2 |
+
import { join } from 'path'
|
| 3 |
+
import { getFsImplementation } from '../utils/fsOperations.js'
|
| 4 |
+
import { getAutoMemPath, isAutoMemoryEnabled } from './paths.js'
|
| 5 |
+
|
| 6 |
+
/* eslint-disable @typescript-eslint/no-require-imports */
|
| 7 |
+
const teamMemPaths = feature('TEAMMEM')
|
| 8 |
+
? (require('./teamMemPaths.js') as typeof import('./teamMemPaths.js'))
|
| 9 |
+
: null
|
| 10 |
+
|
| 11 |
+
import { getKairosActive, getOriginalCwd } from '../bootstrap/state.js'
|
| 12 |
+
import { getFeatureValue_CACHED_MAY_BE_STALE } from '../services/analytics/growthbook.js'
|
| 13 |
+
/* eslint-enable @typescript-eslint/no-require-imports */
|
| 14 |
+
import {
|
| 15 |
+
type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
| 16 |
+
logEvent,
|
| 17 |
+
} from '../services/analytics/index.js'
|
| 18 |
+
import { GREP_TOOL_NAME } from '../tools/GrepTool/prompt.js'
|
| 19 |
+
import { isReplModeEnabled } from '../tools/REPLTool/constants.js'
|
| 20 |
+
import { logForDebugging } from '../utils/debug.js'
|
| 21 |
+
import { hasEmbeddedSearchTools } from '../utils/embeddedTools.js'
|
| 22 |
+
import { isEnvTruthy } from '../utils/envUtils.js'
|
| 23 |
+
import { formatFileSize } from '../utils/format.js'
|
| 24 |
+
import { getProjectDir } from '../utils/sessionStorage.js'
|
| 25 |
+
import { getInitialSettings } from '../utils/settings/settings.js'
|
| 26 |
+
import {
|
| 27 |
+
MEMORY_FRONTMATTER_EXAMPLE,
|
| 28 |
+
TRUSTING_RECALL_SECTION,
|
| 29 |
+
TYPES_SECTION_INDIVIDUAL,
|
| 30 |
+
WHAT_NOT_TO_SAVE_SECTION,
|
| 31 |
+
WHEN_TO_ACCESS_SECTION,
|
| 32 |
+
} from './memoryTypes.js'
|
| 33 |
+
|
| 34 |
+
export const ENTRYPOINT_NAME = 'MEMORY.md'
|
| 35 |
+
export const MAX_ENTRYPOINT_LINES = 200
|
| 36 |
+
// ~125 chars/line at 200 lines. At p97 today; catches long-line indexes that
|
| 37 |
+
// slip past the line cap (p100 observed: 197KB under 200 lines).
|
| 38 |
+
export const MAX_ENTRYPOINT_BYTES = 25_000
|
| 39 |
+
const AUTO_MEM_DISPLAY_NAME = 'auto memory'
|
| 40 |
+
|
| 41 |
+
export type EntrypointTruncation = {
|
| 42 |
+
content: string
|
| 43 |
+
lineCount: number
|
| 44 |
+
byteCount: number
|
| 45 |
+
wasLineTruncated: boolean
|
| 46 |
+
wasByteTruncated: boolean
|
| 47 |
+
}
|
| 48 |
+
|
| 49 |
+
/**
|
| 50 |
+
* Truncate MEMORY.md content to the line AND byte caps, appending a warning
|
| 51 |
+
* that names which cap fired. Line-truncates first (natural boundary), then
|
| 52 |
+
* byte-truncates at the last newline before the cap so we don't cut mid-line.
|
| 53 |
+
*
|
| 54 |
+
* Shared by buildMemoryPrompt and claudemd getMemoryFiles (previously
|
| 55 |
+
* duplicated the line-only logic).
|
| 56 |
+
*/
|
| 57 |
+
export function truncateEntrypointContent(raw: string): EntrypointTruncation {
|
| 58 |
+
const trimmed = raw.trim()
|
| 59 |
+
const contentLines = trimmed.split('\n')
|
| 60 |
+
const lineCount = contentLines.length
|
| 61 |
+
const byteCount = trimmed.length
|
| 62 |
+
|
| 63 |
+
const wasLineTruncated = lineCount > MAX_ENTRYPOINT_LINES
|
| 64 |
+
// Check original byte count — long lines are the failure mode the byte cap
|
| 65 |
+
// targets, so post-line-truncation size would understate the warning.
|
| 66 |
+
const wasByteTruncated = byteCount > MAX_ENTRYPOINT_BYTES
|
| 67 |
+
|
| 68 |
+
if (!wasLineTruncated && !wasByteTruncated) {
|
| 69 |
+
return {
|
| 70 |
+
content: trimmed,
|
| 71 |
+
lineCount,
|
| 72 |
+
byteCount,
|
| 73 |
+
wasLineTruncated,
|
| 74 |
+
wasByteTruncated,
|
| 75 |
+
}
|
| 76 |
+
}
|
| 77 |
+
|
| 78 |
+
let truncated = wasLineTruncated
|
| 79 |
+
? contentLines.slice(0, MAX_ENTRYPOINT_LINES).join('\n')
|
| 80 |
+
: trimmed
|
| 81 |
+
|
| 82 |
+
if (truncated.length > MAX_ENTRYPOINT_BYTES) {
|
| 83 |
+
const cutAt = truncated.lastIndexOf('\n', MAX_ENTRYPOINT_BYTES)
|
| 84 |
+
truncated = truncated.slice(0, cutAt > 0 ? cutAt : MAX_ENTRYPOINT_BYTES)
|
| 85 |
+
}
|
| 86 |
+
|
| 87 |
+
const reason =
|
| 88 |
+
wasByteTruncated && !wasLineTruncated
|
| 89 |
+
? `${formatFileSize(byteCount)} (limit: ${formatFileSize(MAX_ENTRYPOINT_BYTES)}) — index entries are too long`
|
| 90 |
+
: wasLineTruncated && !wasByteTruncated
|
| 91 |
+
? `${lineCount} lines (limit: ${MAX_ENTRYPOINT_LINES})`
|
| 92 |
+
: `${lineCount} lines and ${formatFileSize(byteCount)}`
|
| 93 |
+
|
| 94 |
+
return {
|
| 95 |
+
content:
|
| 96 |
+
truncated +
|
| 97 |
+
`\n\n> WARNING: ${ENTRYPOINT_NAME} is ${reason}. Only part of it was loaded. Keep index entries to one line under ~200 chars; move detail into topic files.`,
|
| 98 |
+
lineCount,
|
| 99 |
+
byteCount,
|
| 100 |
+
wasLineTruncated,
|
| 101 |
+
wasByteTruncated,
|
| 102 |
+
}
|
| 103 |
+
}
|
| 104 |
+
|
| 105 |
+
/* eslint-disable @typescript-eslint/no-require-imports */
|
| 106 |
+
const teamMemPrompts = feature('TEAMMEM')
|
| 107 |
+
? (require('./teamMemPrompts.js') as typeof import('./teamMemPrompts.js'))
|
| 108 |
+
: null
|
| 109 |
+
/* eslint-enable @typescript-eslint/no-require-imports */
|
| 110 |
+
|
| 111 |
+
/**
|
| 112 |
+
* Shared guidance text appended to each memory directory prompt line.
|
| 113 |
+
* Shipped because Claude was burning turns on `ls`/`mkdir -p` before writing.
|
| 114 |
+
* Harness guarantees the directory exists via ensureMemoryDirExists().
|
| 115 |
+
*/
|
| 116 |
+
export const DIR_EXISTS_GUIDANCE =
|
| 117 |
+
'This directory already exists — write to it directly with the Write tool (do not run mkdir or check for its existence).'
|
| 118 |
+
export const DIRS_EXIST_GUIDANCE =
|
| 119 |
+
'Both directories already exist — write to them directly with the Write tool (do not run mkdir or check for their existence).'
|
| 120 |
+
|
| 121 |
+
/**
|
| 122 |
+
* Ensure a memory directory exists. Idempotent — called from loadMemoryPrompt
|
| 123 |
+
* (once per session via systemPromptSection cache) so the model can always
|
| 124 |
+
* write without checking existence first. FsOperations.mkdir is recursive
|
| 125 |
+
* by default and already swallows EEXIST, so the full parent chain
|
| 126 |
+
* (~/.claude/projects/<slug>/memory/) is created in one call with no
|
| 127 |
+
* try/catch needed for the happy path.
|
| 128 |
+
*/
|
| 129 |
+
export async function ensureMemoryDirExists(memoryDir: string): Promise<void> {
|
| 130 |
+
const fs = getFsImplementation()
|
| 131 |
+
try {
|
| 132 |
+
await fs.mkdir(memoryDir)
|
| 133 |
+
} catch (e) {
|
| 134 |
+
// fs.mkdir already handles EEXIST internally. Anything reaching here is
|
| 135 |
+
// a real problem (EACCES/EPERM/EROFS) — log so --debug shows why. Prompt
|
| 136 |
+
// building continues either way; the model's Write will surface the
|
| 137 |
+
// real perm error (and FileWriteTool does its own mkdir of the parent).
|
| 138 |
+
const code =
|
| 139 |
+
e instanceof Error && 'code' in e && typeof e.code === 'string'
|
| 140 |
+
? e.code
|
| 141 |
+
: undefined
|
| 142 |
+
logForDebugging(
|
| 143 |
+
`ensureMemoryDirExists failed for ${memoryDir}: ${code ?? String(e)}`,
|
| 144 |
+
{ level: 'debug' },
|
| 145 |
+
)
|
| 146 |
+
}
|
| 147 |
+
}
|
| 148 |
+
|
| 149 |
+
/**
|
| 150 |
+
* Log memory directory file/subdir counts asynchronously.
|
| 151 |
+
* Fire-and-forget — doesn't block prompt building.
|
| 152 |
+
*/
|
| 153 |
+
function logMemoryDirCounts(
|
| 154 |
+
memoryDir: string,
|
| 155 |
+
baseMetadata: Record<
|
| 156 |
+
string,
|
| 157 |
+
| number
|
| 158 |
+
| boolean
|
| 159 |
+
| AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS
|
| 160 |
+
>,
|
| 161 |
+
): void {
|
| 162 |
+
const fs = getFsImplementation()
|
| 163 |
+
void fs.readdir(memoryDir).then(
|
| 164 |
+
dirents => {
|
| 165 |
+
let fileCount = 0
|
| 166 |
+
let subdirCount = 0
|
| 167 |
+
for (const d of dirents) {
|
| 168 |
+
if (d.isFile()) {
|
| 169 |
+
fileCount++
|
| 170 |
+
} else if (d.isDirectory()) {
|
| 171 |
+
subdirCount++
|
| 172 |
+
}
|
| 173 |
+
}
|
| 174 |
+
logEvent('tengu_memdir_loaded', {
|
| 175 |
+
...baseMetadata,
|
| 176 |
+
total_file_count: fileCount,
|
| 177 |
+
total_subdir_count: subdirCount,
|
| 178 |
+
})
|
| 179 |
+
},
|
| 180 |
+
() => {
|
| 181 |
+
// Directory unreadable — log without counts
|
| 182 |
+
logEvent('tengu_memdir_loaded', baseMetadata)
|
| 183 |
+
},
|
| 184 |
+
)
|
| 185 |
+
}
|
| 186 |
+
|
| 187 |
+
/**
|
| 188 |
+
* Build the typed-memory behavioral instructions (without MEMORY.md content).
|
| 189 |
+
* Constrains memories to a closed four-type taxonomy (user / feedback / project /
|
| 190 |
+
* reference) — content that is derivable from the current project state (code
|
| 191 |
+
* patterns, architecture, git history) is explicitly excluded.
|
| 192 |
+
*
|
| 193 |
+
* Individual-only variant: no `## Memory scope` section, no <scope> tags
|
| 194 |
+
* in type blocks, and team/private qualifiers stripped from examples.
|
| 195 |
+
*
|
| 196 |
+
* Used by both buildMemoryPrompt (agent memory, includes content) and
|
| 197 |
+
* loadMemoryPrompt (system prompt, content injected via user context instead).
|
| 198 |
+
*/
|
| 199 |
+
export function buildMemoryLines(
|
| 200 |
+
displayName: string,
|
| 201 |
+
memoryDir: string,
|
| 202 |
+
extraGuidelines?: string[],
|
| 203 |
+
skipIndex = false,
|
| 204 |
+
): string[] {
|
| 205 |
+
const howToSave = skipIndex
|
| 206 |
+
? [
|
| 207 |
+
'## How to save memories',
|
| 208 |
+
'',
|
| 209 |
+
'Write each memory to its own file (e.g., `user_role.md`, `feedback_testing.md`) using this frontmatter format:',
|
| 210 |
+
'',
|
| 211 |
+
...MEMORY_FRONTMATTER_EXAMPLE,
|
| 212 |
+
'',
|
| 213 |
+
'- Keep the name, description, and type fields in memory files up-to-date with the content',
|
| 214 |
+
'- Organize memory semantically by topic, not chronologically',
|
| 215 |
+
'- Update or remove memories that turn out to be wrong or outdated',
|
| 216 |
+
'- Do not write duplicate memories. First check if there is an existing memory you can update before writing a new one.',
|
| 217 |
+
]
|
| 218 |
+
: [
|
| 219 |
+
'## How to save memories',
|
| 220 |
+
'',
|
| 221 |
+
'Saving a memory is a two-step process:',
|
| 222 |
+
'',
|
| 223 |
+
'**Step 1** — write the memory to its own file (e.g., `user_role.md`, `feedback_testing.md`) using this frontmatter format:',
|
| 224 |
+
'',
|
| 225 |
+
...MEMORY_FRONTMATTER_EXAMPLE,
|
| 226 |
+
'',
|
| 227 |
+
`**Step 2** — add a pointer to that file in \`${ENTRYPOINT_NAME}\`. \`${ENTRYPOINT_NAME}\` is an index, not a memory — each entry should be one line, under ~150 characters: \`- [Title](file.md) — one-line hook\`. It has no frontmatter. Never write memory content directly into \`${ENTRYPOINT_NAME}\`.`,
|
| 228 |
+
'',
|
| 229 |
+
`- \`${ENTRYPOINT_NAME}\` is always loaded into your conversation context — lines after ${MAX_ENTRYPOINT_LINES} will be truncated, so keep the index concise`,
|
| 230 |
+
'- Keep the name, description, and type fields in memory files up-to-date with the content',
|
| 231 |
+
'- Organize memory semantically by topic, not chronologically',
|
| 232 |
+
'- Update or remove memories that turn out to be wrong or outdated',
|
| 233 |
+
'- Do not write duplicate memories. First check if there is an existing memory you can update before writing a new one.',
|
| 234 |
+
]
|
| 235 |
+
|
| 236 |
+
const lines: string[] = [
|
| 237 |
+
`# ${displayName}`,
|
| 238 |
+
'',
|
| 239 |
+
`You have a persistent, file-based memory system at \`${memoryDir}\`. ${DIR_EXISTS_GUIDANCE}`,
|
| 240 |
+
'',
|
| 241 |
+
"You should build up this memory system over time so that future conversations can have a complete picture of who the user is, how they'd like to collaborate with you, what behaviors to avoid or repeat, and the context behind the work the user gives you.",
|
| 242 |
+
'',
|
| 243 |
+
'If the user explicitly asks you to remember something, save it immediately as whichever type fits best. If they ask you to forget something, find and remove the relevant entry.',
|
| 244 |
+
'',
|
| 245 |
+
...TYPES_SECTION_INDIVIDUAL,
|
| 246 |
+
...WHAT_NOT_TO_SAVE_SECTION,
|
| 247 |
+
'',
|
| 248 |
+
...howToSave,
|
| 249 |
+
'',
|
| 250 |
+
...WHEN_TO_ACCESS_SECTION,
|
| 251 |
+
'',
|
| 252 |
+
...TRUSTING_RECALL_SECTION,
|
| 253 |
+
'',
|
| 254 |
+
'## Memory and other forms of persistence',
|
| 255 |
+
'Memory is one of several persistence mechanisms available to you as you assist the user in a given conversation. The distinction is often that memory can be recalled in future conversations and should not be used for persisting information that is only useful within the scope of the current conversation.',
|
| 256 |
+
'- When to use or update a plan instead of memory: If you are about to start a non-trivial implementation task and would like to reach alignment with the user on your approach you should use a Plan rather than saving this information to memory. Similarly, if you already have a plan within the conversation and you have changed your approach persist that change by updating the plan rather than saving a memory.',
|
| 257 |
+
'- When to use or update tasks instead of memory: When you need to break your work in current conversation into discrete steps or keep track of your progress use tasks instead of saving to memory. Tasks are great for persisting information about the work that needs to be done in the current conversation, but memory should be reserved for information that will be useful in future conversations.',
|
| 258 |
+
'',
|
| 259 |
+
...(extraGuidelines ?? []),
|
| 260 |
+
'',
|
| 261 |
+
]
|
| 262 |
+
|
| 263 |
+
lines.push(...buildSearchingPastContextSection(memoryDir))
|
| 264 |
+
|
| 265 |
+
return lines
|
| 266 |
+
}
|
| 267 |
+
|
| 268 |
+
/**
|
| 269 |
+
* Build the typed-memory prompt with MEMORY.md content included.
|
| 270 |
+
* Used by agent memory (which has no getClaudeMds() equivalent).
|
| 271 |
+
*/
|
| 272 |
+
export function buildMemoryPrompt(params: {
|
| 273 |
+
displayName: string
|
| 274 |
+
memoryDir: string
|
| 275 |
+
extraGuidelines?: string[]
|
| 276 |
+
}): string {
|
| 277 |
+
const { displayName, memoryDir, extraGuidelines } = params
|
| 278 |
+
const fs = getFsImplementation()
|
| 279 |
+
const entrypoint = memoryDir + ENTRYPOINT_NAME
|
| 280 |
+
|
| 281 |
+
// Directory creation is the caller's responsibility (loadMemoryPrompt /
|
| 282 |
+
// loadAgentMemoryPrompt). Builders only read, they don't mkdir.
|
| 283 |
+
|
| 284 |
+
// Read existing memory entrypoint (sync: prompt building is synchronous)
|
| 285 |
+
let entrypointContent = ''
|
| 286 |
+
try {
|
| 287 |
+
// eslint-disable-next-line custom-rules/no-sync-fs
|
| 288 |
+
entrypointContent = fs.readFileSync(entrypoint, { encoding: 'utf-8' })
|
| 289 |
+
} catch {
|
| 290 |
+
// No memory file yet
|
| 291 |
+
}
|
| 292 |
+
|
| 293 |
+
const lines = buildMemoryLines(displayName, memoryDir, extraGuidelines)
|
| 294 |
+
|
| 295 |
+
if (entrypointContent.trim()) {
|
| 296 |
+
const t = truncateEntrypointContent(entrypointContent)
|
| 297 |
+
const memoryType = displayName === AUTO_MEM_DISPLAY_NAME ? 'auto' : 'agent'
|
| 298 |
+
logMemoryDirCounts(memoryDir, {
|
| 299 |
+
content_length: t.byteCount,
|
| 300 |
+
line_count: t.lineCount,
|
| 301 |
+
was_truncated: t.wasLineTruncated,
|
| 302 |
+
was_byte_truncated: t.wasByteTruncated,
|
| 303 |
+
memory_type:
|
| 304 |
+
memoryType as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
| 305 |
+
})
|
| 306 |
+
lines.push(`## ${ENTRYPOINT_NAME}`, '', t.content)
|
| 307 |
+
} else {
|
| 308 |
+
lines.push(
|
| 309 |
+
`## ${ENTRYPOINT_NAME}`,
|
| 310 |
+
'',
|
| 311 |
+
`Your ${ENTRYPOINT_NAME} is currently empty. When you save new memories, they will appear here.`,
|
| 312 |
+
)
|
| 313 |
+
}
|
| 314 |
+
|
| 315 |
+
return lines.join('\n')
|
| 316 |
+
}
|
| 317 |
+
|
| 318 |
+
/**
|
| 319 |
+
* Assistant-mode daily-log prompt. Gated behind feature('KAIROS').
|
| 320 |
+
*
|
| 321 |
+
* Assistant sessions are effectively perpetual, so the agent writes memories
|
| 322 |
+
* append-only to a date-named log file rather than maintaining MEMORY.md as
|
| 323 |
+
* a live index. A separate nightly /dream skill distills logs into topic
|
| 324 |
+
* files + MEMORY.md. MEMORY.md is still loaded into context (via claudemd.ts)
|
| 325 |
+
* as the distilled index — this prompt only changes where NEW memories go.
|
| 326 |
+
*/
|
| 327 |
+
function buildAssistantDailyLogPrompt(skipIndex = false): string {
|
| 328 |
+
const memoryDir = getAutoMemPath()
|
| 329 |
+
// Describe the path as a pattern rather than inlining today's literal path:
|
| 330 |
+
// this prompt is cached by systemPromptSection('memory', ...) and NOT
|
| 331 |
+
// invalidated on date change. The model derives the current date from the
|
| 332 |
+
// date_change attachment (appended at the tail on midnight rollover) rather
|
| 333 |
+
// than the user-context message — the latter is intentionally left stale to
|
| 334 |
+
// preserve the prompt cache prefix across midnight.
|
| 335 |
+
const logPathPattern = join(memoryDir, 'logs', 'YYYY', 'MM', 'YYYY-MM-DD.md')
|
| 336 |
+
|
| 337 |
+
const lines: string[] = [
|
| 338 |
+
'# auto memory',
|
| 339 |
+
'',
|
| 340 |
+
`You have a persistent, file-based memory system found at: \`${memoryDir}\``,
|
| 341 |
+
'',
|
| 342 |
+
"This session is long-lived. As you work, record anything worth remembering by **appending** to today's daily log file:",
|
| 343 |
+
'',
|
| 344 |
+
`\`${logPathPattern}\``,
|
| 345 |
+
'',
|
| 346 |
+
"Substitute today's date (from `currentDate` in your context) for `YYYY-MM-DD`. When the date rolls over mid-session, start appending to the new day's file.",
|
| 347 |
+
'',
|
| 348 |
+
'Write each entry as a short timestamped bullet. Create the file (and parent directories) on first write if it does not exist. Do not rewrite or reorganize the log — it is append-only. A separate nightly process distills these logs into `MEMORY.md` and topic files.',
|
| 349 |
+
'',
|
| 350 |
+
'## What to log',
|
| 351 |
+
'- User corrections and preferences ("use bun, not npm"; "stop summarizing diffs")',
|
| 352 |
+
'- Facts about the user, their role, or their goals',
|
| 353 |
+
'- Project context that is not derivable from the code (deadlines, incidents, decisions and their rationale)',
|
| 354 |
+
'- Pointers to external systems (dashboards, Linear projects, Slack channels)',
|
| 355 |
+
'- Anything the user explicitly asks you to remember',
|
| 356 |
+
'',
|
| 357 |
+
...WHAT_NOT_TO_SAVE_SECTION,
|
| 358 |
+
'',
|
| 359 |
+
...(skipIndex
|
| 360 |
+
? []
|
| 361 |
+
: [
|
| 362 |
+
`## ${ENTRYPOINT_NAME}`,
|
| 363 |
+
`\`${ENTRYPOINT_NAME}\` is the distilled index (maintained nightly from your logs) and is loaded into your context automatically. Read it for orientation, but do not edit it directly — record new information in today's log instead.`,
|
| 364 |
+
'',
|
| 365 |
+
]),
|
| 366 |
+
...buildSearchingPastContextSection(memoryDir),
|
| 367 |
+
]
|
| 368 |
+
|
| 369 |
+
return lines.join('\n')
|
| 370 |
+
}
|
| 371 |
+
|
| 372 |
+
/**
|
| 373 |
+
* Build the "Searching past context" section if the feature gate is enabled.
|
| 374 |
+
*/
|
| 375 |
+
export function buildSearchingPastContextSection(autoMemDir: string): string[] {
|
| 376 |
+
if (!getFeatureValue_CACHED_MAY_BE_STALE('tengu_coral_fern', false)) {
|
| 377 |
+
return []
|
| 378 |
+
}
|
| 379 |
+
const projectDir = getProjectDir(getOriginalCwd())
|
| 380 |
+
// Ant-native builds alias grep to embedded ugrep and remove the dedicated
|
| 381 |
+
// Grep tool, so give the model a real shell invocation there.
|
| 382 |
+
// In REPL mode, both Grep and Bash are hidden from direct use — the model
|
| 383 |
+
// calls them from inside REPL scripts, so the grep shell form is what it
|
| 384 |
+
// will write in the script anyway.
|
| 385 |
+
const embedded = hasEmbeddedSearchTools() || isReplModeEnabled()
|
| 386 |
+
const memSearch = embedded
|
| 387 |
+
? `grep -rn "<search term>" ${autoMemDir} --include="*.md"`
|
| 388 |
+
: `${GREP_TOOL_NAME} with pattern="<search term>" path="${autoMemDir}" glob="*.md"`
|
| 389 |
+
const transcriptSearch = embedded
|
| 390 |
+
? `grep -rn "<search term>" ${projectDir}/ --include="*.jsonl"`
|
| 391 |
+
: `${GREP_TOOL_NAME} with pattern="<search term>" path="${projectDir}/" glob="*.jsonl"`
|
| 392 |
+
return [
|
| 393 |
+
'## Searching past context',
|
| 394 |
+
'',
|
| 395 |
+
'When looking for past context:',
|
| 396 |
+
'1. Search topic files in your memory directory:',
|
| 397 |
+
'```',
|
| 398 |
+
memSearch,
|
| 399 |
+
'```',
|
| 400 |
+
'2. Session transcript logs (last resort — large files, slow):',
|
| 401 |
+
'```',
|
| 402 |
+
transcriptSearch,
|
| 403 |
+
'```',
|
| 404 |
+
'Use narrow search terms (error messages, file paths, function names) rather than broad keywords.',
|
| 405 |
+
'',
|
| 406 |
+
]
|
| 407 |
+
}
|
| 408 |
+
|
| 409 |
+
/**
|
| 410 |
+
* Load the unified memory prompt for inclusion in the system prompt.
|
| 411 |
+
* Dispatches based on which memory systems are enabled:
|
| 412 |
+
* - auto + team: combined prompt (both directories)
|
| 413 |
+
* - auto only: memory lines (single directory)
|
| 414 |
+
* Team memory requires auto memory (enforced by isTeamMemoryEnabled), so
|
| 415 |
+
* there is no team-only branch.
|
| 416 |
+
*
|
| 417 |
+
* Returns null when auto memory is disabled.
|
| 418 |
+
*/
|
| 419 |
+
export async function loadMemoryPrompt(): Promise<string | null> {
|
| 420 |
+
const autoEnabled = isAutoMemoryEnabled()
|
| 421 |
+
|
| 422 |
+
const skipIndex = getFeatureValue_CACHED_MAY_BE_STALE(
|
| 423 |
+
'tengu_moth_copse',
|
| 424 |
+
false,
|
| 425 |
+
)
|
| 426 |
+
|
| 427 |
+
// KAIROS daily-log mode takes precedence over TEAMMEM: the append-only
|
| 428 |
+
// log paradigm does not compose with team sync (which expects a shared
|
| 429 |
+
// MEMORY.md that both sides read + write). Gating on `autoEnabled` here
|
| 430 |
+
// means the !autoEnabled case falls through to the tengu_memdir_disabled
|
| 431 |
+
// telemetry block below, matching the non-KAIROS path.
|
| 432 |
+
if (feature('KAIROS') && autoEnabled && getKairosActive()) {
|
| 433 |
+
logMemoryDirCounts(getAutoMemPath(), {
|
| 434 |
+
memory_type:
|
| 435 |
+
'auto' as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
| 436 |
+
})
|
| 437 |
+
return buildAssistantDailyLogPrompt(skipIndex)
|
| 438 |
+
}
|
| 439 |
+
|
| 440 |
+
// Cowork injects memory-policy text via env var; thread into all builders.
|
| 441 |
+
const coworkExtraGuidelines =
|
| 442 |
+
process.env.CLAUDE_COWORK_MEMORY_EXTRA_GUIDELINES
|
| 443 |
+
const extraGuidelines =
|
| 444 |
+
coworkExtraGuidelines && coworkExtraGuidelines.trim().length > 0
|
| 445 |
+
? [coworkExtraGuidelines]
|
| 446 |
+
: undefined
|
| 447 |
+
|
| 448 |
+
if (feature('TEAMMEM')) {
|
| 449 |
+
if (teamMemPaths!.isTeamMemoryEnabled()) {
|
| 450 |
+
const autoDir = getAutoMemPath()
|
| 451 |
+
const teamDir = teamMemPaths!.getTeamMemPath()
|
| 452 |
+
// Harness guarantees these directories exist so the model can write
|
| 453 |
+
// without checking. The prompt text reflects this ("already exists").
|
| 454 |
+
// Only creating teamDir is sufficient: getTeamMemPath() is defined as
|
| 455 |
+
// join(getAutoMemPath(), 'team'), so recursive mkdir of the team dir
|
| 456 |
+
// creates the auto dir as a side effect. If the team dir ever moves
|
| 457 |
+
// out from under the auto dir, add a second ensureMemoryDirExists call
|
| 458 |
+
// for autoDir here.
|
| 459 |
+
await ensureMemoryDirExists(teamDir)
|
| 460 |
+
logMemoryDirCounts(autoDir, {
|
| 461 |
+
memory_type:
|
| 462 |
+
'auto' as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
| 463 |
+
})
|
| 464 |
+
logMemoryDirCounts(teamDir, {
|
| 465 |
+
memory_type:
|
| 466 |
+
'team' as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
| 467 |
+
})
|
| 468 |
+
return teamMemPrompts!.buildCombinedMemoryPrompt(
|
| 469 |
+
extraGuidelines,
|
| 470 |
+
skipIndex,
|
| 471 |
+
)
|
| 472 |
+
}
|
| 473 |
+
}
|
| 474 |
+
|
| 475 |
+
if (autoEnabled) {
|
| 476 |
+
const autoDir = getAutoMemPath()
|
| 477 |
+
// Harness guarantees the directory exists so the model can write without
|
| 478 |
+
// checking. The prompt text reflects this ("already exists").
|
| 479 |
+
await ensureMemoryDirExists(autoDir)
|
| 480 |
+
logMemoryDirCounts(autoDir, {
|
| 481 |
+
memory_type:
|
| 482 |
+
'auto' as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
| 483 |
+
})
|
| 484 |
+
return buildMemoryLines(
|
| 485 |
+
'auto memory',
|
| 486 |
+
autoDir,
|
| 487 |
+
extraGuidelines,
|
| 488 |
+
skipIndex,
|
| 489 |
+
).join('\n')
|
| 490 |
+
}
|
| 491 |
+
|
| 492 |
+
logEvent('tengu_memdir_disabled', {
|
| 493 |
+
disabled_by_env_var: isEnvTruthy(
|
| 494 |
+
process.env.CLAUDE_CODE_DISABLE_AUTO_MEMORY,
|
| 495 |
+
),
|
| 496 |
+
disabled_by_setting:
|
| 497 |
+
!isEnvTruthy(process.env.CLAUDE_CODE_DISABLE_AUTO_MEMORY) &&
|
| 498 |
+
getInitialSettings().autoMemoryEnabled === false,
|
| 499 |
+
})
|
| 500 |
+
// Gate on the GB flag directly, not isTeamMemoryEnabled() — that function
|
| 501 |
+
// checks isAutoMemoryEnabled() first, which is definitionally false in this
|
| 502 |
+
// branch. We want "was this user in the team-memory cohort at all."
|
| 503 |
+
if (getFeatureValue_CACHED_MAY_BE_STALE('tengu_herring_clock', false)) {
|
| 504 |
+
logEvent('tengu_team_memdir_disabled', {})
|
| 505 |
+
}
|
| 506 |
+
return null
|
| 507 |
+
}
|
src/memdir/memoryAge.ts
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* Days elapsed since mtime. Floor-rounded — 0 for today, 1 for
|
| 3 |
+
* yesterday, 2+ for older. Negative inputs (future mtime, clock skew)
|
| 4 |
+
* clamp to 0.
|
| 5 |
+
*/
|
| 6 |
+
export function memoryAgeDays(mtimeMs: number): number {
|
| 7 |
+
return Math.max(0, Math.floor((Date.now() - mtimeMs) / 86_400_000))
|
| 8 |
+
}
|
| 9 |
+
|
| 10 |
+
/**
|
| 11 |
+
* Human-readable age string. Models are poor at date arithmetic —
|
| 12 |
+
* a raw ISO timestamp doesn't trigger staleness reasoning the way
|
| 13 |
+
* "47 days ago" does.
|
| 14 |
+
*/
|
| 15 |
+
export function memoryAge(mtimeMs: number): string {
|
| 16 |
+
const d = memoryAgeDays(mtimeMs)
|
| 17 |
+
if (d === 0) return 'today'
|
| 18 |
+
if (d === 1) return 'yesterday'
|
| 19 |
+
return `${d} days ago`
|
| 20 |
+
}
|
| 21 |
+
|
| 22 |
+
/**
|
| 23 |
+
* Plain-text staleness caveat for memories >1 day old. Returns ''
|
| 24 |
+
* for fresh (today/yesterday) memories — warning there is noise.
|
| 25 |
+
*
|
| 26 |
+
* Use this when the consumer already provides its own wrapping
|
| 27 |
+
* (e.g. messages.ts relevant_memories → wrapMessagesInSystemReminder).
|
| 28 |
+
*
|
| 29 |
+
* Motivated by user reports of stale code-state memories (file:line
|
| 30 |
+
* citations to code that has since changed) being asserted as fact —
|
| 31 |
+
* the citation makes the stale claim sound more authoritative, not less.
|
| 32 |
+
*/
|
| 33 |
+
export function memoryFreshnessText(mtimeMs: number): string {
|
| 34 |
+
const d = memoryAgeDays(mtimeMs)
|
| 35 |
+
if (d <= 1) return ''
|
| 36 |
+
return (
|
| 37 |
+
`This memory is ${d} days old. ` +
|
| 38 |
+
`Memories are point-in-time observations, not live state — ` +
|
| 39 |
+
`claims about code behavior or file:line citations may be outdated. ` +
|
| 40 |
+
`Verify against current code before asserting as fact.`
|
| 41 |
+
)
|
| 42 |
+
}
|
| 43 |
+
|
| 44 |
+
/**
|
| 45 |
+
* Per-memory staleness note wrapped in <system-reminder> tags.
|
| 46 |
+
* Returns '' for memories ≤ 1 day old. Use this for callers that
|
| 47 |
+
* don't add their own system-reminder wrapper (e.g. FileReadTool output).
|
| 48 |
+
*/
|
| 49 |
+
export function memoryFreshnessNote(mtimeMs: number): string {
|
| 50 |
+
const text = memoryFreshnessText(mtimeMs)
|
| 51 |
+
if (!text) return ''
|
| 52 |
+
return `<system-reminder>${text}</system-reminder>\n`
|
| 53 |
+
}
|
src/memdir/memoryScan.ts
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* Memory-directory scanning primitives. Split out of findRelevantMemories.ts
|
| 3 |
+
* so extractMemories can import the scan without pulling in sideQuery and
|
| 4 |
+
* the API-client chain (which closed a cycle through memdir.ts — #25372).
|
| 5 |
+
*/
|
| 6 |
+
|
| 7 |
+
import { readdir } from 'fs/promises'
|
| 8 |
+
import { basename, join } from 'path'
|
| 9 |
+
import { parseFrontmatter } from '../utils/frontmatterParser.js'
|
| 10 |
+
import { readFileInRange } from '../utils/readFileInRange.js'
|
| 11 |
+
import { type MemoryType, parseMemoryType } from './memoryTypes.js'
|
| 12 |
+
|
| 13 |
+
export type MemoryHeader = {
|
| 14 |
+
filename: string
|
| 15 |
+
filePath: string
|
| 16 |
+
mtimeMs: number
|
| 17 |
+
description: string | null
|
| 18 |
+
type: MemoryType | undefined
|
| 19 |
+
}
|
| 20 |
+
|
| 21 |
+
const MAX_MEMORY_FILES = 200
|
| 22 |
+
const FRONTMATTER_MAX_LINES = 30
|
| 23 |
+
|
| 24 |
+
/**
|
| 25 |
+
* Scan a memory directory for .md files, read their frontmatter, and return
|
| 26 |
+
* a header list sorted newest-first (capped at MAX_MEMORY_FILES). Shared by
|
| 27 |
+
* findRelevantMemories (query-time recall) and extractMemories (pre-injects
|
| 28 |
+
* the listing so the extraction agent doesn't spend a turn on `ls`).
|
| 29 |
+
*
|
| 30 |
+
* Single-pass: readFileInRange stats internally and returns mtimeMs, so we
|
| 31 |
+
* read-then-sort rather than stat-sort-read. For the common case (N ≤ 200)
|
| 32 |
+
* this halves syscalls vs a separate stat round; for large N we read a few
|
| 33 |
+
* extra small files but still avoid the double-stat on the surviving 200.
|
| 34 |
+
*/
|
| 35 |
+
export async function scanMemoryFiles(
|
| 36 |
+
memoryDir: string,
|
| 37 |
+
signal: AbortSignal,
|
| 38 |
+
): Promise<MemoryHeader[]> {
|
| 39 |
+
try {
|
| 40 |
+
const entries = await readdir(memoryDir, { recursive: true })
|
| 41 |
+
const mdFiles = entries.filter(
|
| 42 |
+
f => f.endsWith('.md') && basename(f) !== 'MEMORY.md',
|
| 43 |
+
)
|
| 44 |
+
|
| 45 |
+
const headerResults = await Promise.allSettled(
|
| 46 |
+
mdFiles.map(async (relativePath): Promise<MemoryHeader> => {
|
| 47 |
+
const filePath = join(memoryDir, relativePath)
|
| 48 |
+
const { content, mtimeMs } = await readFileInRange(
|
| 49 |
+
filePath,
|
| 50 |
+
0,
|
| 51 |
+
FRONTMATTER_MAX_LINES,
|
| 52 |
+
undefined,
|
| 53 |
+
signal,
|
| 54 |
+
)
|
| 55 |
+
const { frontmatter } = parseFrontmatter(content, filePath)
|
| 56 |
+
return {
|
| 57 |
+
filename: relativePath,
|
| 58 |
+
filePath,
|
| 59 |
+
mtimeMs,
|
| 60 |
+
description: frontmatter.description || null,
|
| 61 |
+
type: parseMemoryType(frontmatter.type),
|
| 62 |
+
}
|
| 63 |
+
}),
|
| 64 |
+
)
|
| 65 |
+
|
| 66 |
+
return headerResults
|
| 67 |
+
.filter(
|
| 68 |
+
(r): r is PromiseFulfilledResult<MemoryHeader> =>
|
| 69 |
+
r.status === 'fulfilled',
|
| 70 |
+
)
|
| 71 |
+
.map(r => r.value)
|
| 72 |
+
.sort((a, b) => b.mtimeMs - a.mtimeMs)
|
| 73 |
+
.slice(0, MAX_MEMORY_FILES)
|
| 74 |
+
} catch {
|
| 75 |
+
return []
|
| 76 |
+
}
|
| 77 |
+
}
|
| 78 |
+
|
| 79 |
+
/**
|
| 80 |
+
* Format memory headers as a text manifest: one line per file with
|
| 81 |
+
* [type] filename (timestamp): description. Used by both the recall
|
| 82 |
+
* selector prompt and the extraction-agent prompt.
|
| 83 |
+
*/
|
| 84 |
+
export function formatMemoryManifest(memories: MemoryHeader[]): string {
|
| 85 |
+
return memories
|
| 86 |
+
.map(m => {
|
| 87 |
+
const tag = m.type ? `[${m.type}] ` : ''
|
| 88 |
+
const ts = new Date(m.mtimeMs).toISOString()
|
| 89 |
+
return m.description
|
| 90 |
+
? `- ${tag}${m.filename} (${ts}): ${m.description}`
|
| 91 |
+
: `- ${tag}${m.filename} (${ts})`
|
| 92 |
+
})
|
| 93 |
+
.join('\n')
|
| 94 |
+
}
|
src/memdir/memoryTypes.ts
ADDED
|
@@ -0,0 +1,271 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* Memory type taxonomy.
|
| 3 |
+
*
|
| 4 |
+
* Memories are constrained to four types capturing context NOT derivable
|
| 5 |
+
* from the current project state. Code patterns, architecture, git history,
|
| 6 |
+
* and file structure are derivable (via grep/git/CLAUDE.md) and should NOT
|
| 7 |
+
* be saved as memories.
|
| 8 |
+
*
|
| 9 |
+
* The two TYPES_SECTION_* exports below are intentionally duplicated rather
|
| 10 |
+
* than generated from a shared spec — keeping them flat makes per-mode edits
|
| 11 |
+
* trivial without reasoning through a helper's conditional rendering.
|
| 12 |
+
*/
|
| 13 |
+
|
| 14 |
+
export const MEMORY_TYPES = [
|
| 15 |
+
'user',
|
| 16 |
+
'feedback',
|
| 17 |
+
'project',
|
| 18 |
+
'reference',
|
| 19 |
+
] as const
|
| 20 |
+
|
| 21 |
+
export type MemoryType = (typeof MEMORY_TYPES)[number]
|
| 22 |
+
|
| 23 |
+
/**
|
| 24 |
+
* Parse a raw frontmatter value into a MemoryType.
|
| 25 |
+
* Invalid or missing values return undefined — legacy files without a
|
| 26 |
+
* `type:` field keep working, files with unknown types degrade gracefully.
|
| 27 |
+
*/
|
| 28 |
+
export function parseMemoryType(raw: unknown): MemoryType | undefined {
|
| 29 |
+
if (typeof raw !== 'string') return undefined
|
| 30 |
+
return MEMORY_TYPES.find(t => t === raw)
|
| 31 |
+
}
|
| 32 |
+
|
| 33 |
+
/**
|
| 34 |
+
* `## Types of memory` section for COMBINED mode (private + team directories).
|
| 35 |
+
* Includes <scope> tags and team/private qualifiers in examples.
|
| 36 |
+
*/
|
| 37 |
+
export const TYPES_SECTION_COMBINED: readonly string[] = [
|
| 38 |
+
'## Types of memory',
|
| 39 |
+
'',
|
| 40 |
+
'There are several discrete types of memory that you can store in your memory system. Each type below declares a <scope> of `private`, `team`, or guidance for choosing between the two.',
|
| 41 |
+
'',
|
| 42 |
+
'<types>',
|
| 43 |
+
'<type>',
|
| 44 |
+
' <name>user</name>',
|
| 45 |
+
' <scope>always private</scope>',
|
| 46 |
+
" <description>Contain information about the user's role, goals, responsibilities, and knowledge. Great user memories help you tailor your future behavior to the user's preferences and perspective. Your goal in reading and writing these memories is to build up an understanding of who the user is and how you can be most helpful to them specifically. For example, you should collaborate with a senior software engineer differently than a student who is coding for the very first time. Keep in mind, that the aim here is to be helpful to the user. Avoid writing memories about the user that could be viewed as a negative judgement or that are not relevant to the work you're trying to accomplish together.</description>",
|
| 47 |
+
" <when_to_save>When you learn any details about the user's role, preferences, responsibilities, or knowledge</when_to_save>",
|
| 48 |
+
" <how_to_use>When your work should be informed by the user's profile or perspective. For example, if the user is asking you to explain a part of the code, you should answer that question in a way that is tailored to the specific details that they will find most valuable or that helps them build their mental model in relation to domain knowledge they already have.</how_to_use>",
|
| 49 |
+
' <examples>',
|
| 50 |
+
" user: I'm a data scientist investigating what logging we have in place",
|
| 51 |
+
' assistant: [saves private user memory: user is a data scientist, currently focused on observability/logging]',
|
| 52 |
+
'',
|
| 53 |
+
" user: I've been writing Go for ten years but this is my first time touching the React side of this repo",
|
| 54 |
+
" assistant: [saves private user memory: deep Go expertise, new to React and this project's frontend — frame frontend explanations in terms of backend analogues]",
|
| 55 |
+
' </examples>',
|
| 56 |
+
'</type>',
|
| 57 |
+
'<type>',
|
| 58 |
+
' <name>feedback</name>',
|
| 59 |
+
' <scope>default to private. Save as team only when the guidance is clearly a project-wide convention that every contributor should follow (e.g., a testing policy, a build invariant), not a personal style preference.</scope>',
|
| 60 |
+
" <description>Guidance the user has given you about how to approach work — both what to avoid and what to keep doing. These are a very important type of memory to read and write as they allow you to remain coherent and responsive to the way you should approach work in the project. Record from failure AND success: if you only save corrections, you will avoid past mistakes but drift away from approaches the user has already validated, and may grow overly cautious. Before saving a private feedback memory, check that it doesn't contradict a team feedback memory — if it does, either don't save it or note the override explicitly.</description>",
|
| 61 |
+
' <when_to_save>Any time the user corrects your approach ("no not that", "don\'t", "stop doing X") OR confirms a non-obvious approach worked ("yes exactly", "perfect, keep doing that", accepting an unusual choice without pushback). Corrections are easy to notice; confirmations are quieter — watch for them. In both cases, save what is applicable to future conversations, especially if surprising or not obvious from the code. Include *why* so you can judge edge cases later.</when_to_save>',
|
| 62 |
+
' <how_to_use>Let these memories guide your behavior so that the user and other users in the project do not need to offer the same guidance twice.</how_to_use>',
|
| 63 |
+
' <body_structure>Lead with the rule itself, then a **Why:** line (the reason the user gave — often a past incident or strong preference) and a **How to apply:** line (when/where this guidance kicks in). Knowing *why* lets you judge edge cases instead of blindly following the rule.</body_structure>',
|
| 64 |
+
' <examples>',
|
| 65 |
+
" user: don't mock the database in these tests — we got burned last quarter when mocked tests passed but the prod migration failed",
|
| 66 |
+
' assistant: [saves team feedback memory: integration tests must hit a real database, not mocks. Reason: prior incident where mock/prod divergence masked a broken migration. Team scope: this is a project testing policy, not a personal preference]',
|
| 67 |
+
'',
|
| 68 |
+
' user: stop summarizing what you just did at the end of every response, I can read the diff',
|
| 69 |
+
" assistant: [saves private feedback memory: this user wants terse responses with no trailing summaries. Private because it's a communication preference, not a project convention]",
|
| 70 |
+
'',
|
| 71 |
+
" user: yeah the single bundled PR was the right call here, splitting this one would've just been churn",
|
| 72 |
+
' assistant: [saves private feedback memory: for refactors in this area, user prefers one bundled PR over many small ones. Confirmed after I chose this approach — a validated judgment call, not a correction]',
|
| 73 |
+
' </examples>',
|
| 74 |
+
'</type>',
|
| 75 |
+
'<type>',
|
| 76 |
+
' <name>project</name>',
|
| 77 |
+
' <scope>private or team, but strongly bias toward team</scope>',
|
| 78 |
+
' <description>Information that you learn about ongoing work, goals, initiatives, bugs, or incidents within the project that is not otherwise derivable from the code or git history. Project memories help you understand the broader context and motivation behind the work users are working on within this working directory.</description>',
|
| 79 |
+
' <when_to_save>When you learn who is doing what, why, or by when. These states change relatively quickly so try to keep your understanding of this up to date. Always convert relative dates in user messages to absolute dates when saving (e.g., "Thursday" → "2026-03-05"), so the memory remains interpretable after time passes.</when_to_save>',
|
| 80 |
+
" <how_to_use>Use these memories to more fully understand the details and nuance behind the user's request, anticipate coordination issues across users, make better informed suggestions.</how_to_use>",
|
| 81 |
+
' <body_structure>Lead with the fact or decision, then a **Why:** line (the motivation — often a constraint, deadline, or stakeholder ask) and a **How to apply:** line (how this should shape your suggestions). Project memories decay fast, so the why helps future-you judge whether the memory is still load-bearing.</body_structure>',
|
| 82 |
+
' <examples>',
|
| 83 |
+
" user: we're freezing all non-critical merges after Thursday — mobile team is cutting a release branch",
|
| 84 |
+
' assistant: [saves team project memory: merge freeze begins 2026-03-05 for mobile release cut. Flag any non-critical PR work scheduled after that date]',
|
| 85 |
+
'',
|
| 86 |
+
" user: the reason we're ripping out the old auth middleware is that legal flagged it for storing session tokens in a way that doesn't meet the new compliance requirements",
|
| 87 |
+
' assistant: [saves team project memory: auth middleware rewrite is driven by legal/compliance requirements around session token storage, not tech-debt cleanup — scope decisions should favor compliance over ergonomics]',
|
| 88 |
+
' </examples>',
|
| 89 |
+
'</type>',
|
| 90 |
+
'<type>',
|
| 91 |
+
' <name>reference</name>',
|
| 92 |
+
' <scope>usually team</scope>',
|
| 93 |
+
' <description>Stores pointers to where information can be found in external systems. These memories allow you to remember where to look to find up-to-date information outside of the project directory.</description>',
|
| 94 |
+
' <when_to_save>When you learn about resources in external systems and their purpose. For example, that bugs are tracked in a specific project in Linear or that feedback can be found in a specific Slack channel.</when_to_save>',
|
| 95 |
+
' <how_to_use>When the user references an external system or information that may be in an external system.</how_to_use>',
|
| 96 |
+
' <examples>',
|
| 97 |
+
' user: check the Linear project "INGEST" if you want context on these tickets, that\'s where we track all pipeline bugs',
|
| 98 |
+
' assistant: [saves team reference memory: pipeline bugs are tracked in Linear project "INGEST"]',
|
| 99 |
+
'',
|
| 100 |
+
" user: the Grafana board at grafana.internal/d/api-latency is what oncall watches — if you're touching request handling, that's the thing that'll page someone",
|
| 101 |
+
' assistant: [saves team reference memory: grafana.internal/d/api-latency is the oncall latency dashboard — check it when editing request-path code]',
|
| 102 |
+
' </examples>',
|
| 103 |
+
'</type>',
|
| 104 |
+
'</types>',
|
| 105 |
+
'',
|
| 106 |
+
]
|
| 107 |
+
|
| 108 |
+
/**
|
| 109 |
+
* `## Types of memory` section for INDIVIDUAL-ONLY mode (single directory).
|
| 110 |
+
* No <scope> tags. Examples use plain `[saves X memory: …]`. Prose that
|
| 111 |
+
* only makes sense with a private/team split is reworded.
|
| 112 |
+
*/
|
| 113 |
+
export const TYPES_SECTION_INDIVIDUAL: readonly string[] = [
|
| 114 |
+
'## Types of memory',
|
| 115 |
+
'',
|
| 116 |
+
'There are several discrete types of memory that you can store in your memory system:',
|
| 117 |
+
'',
|
| 118 |
+
'<types>',
|
| 119 |
+
'<type>',
|
| 120 |
+
' <name>user</name>',
|
| 121 |
+
" <description>Contain information about the user's role, goals, responsibilities, and knowledge. Great user memories help you tailor your future behavior to the user's preferences and perspective. Your goal in reading and writing these memories is to build up an understanding of who the user is and how you can be most helpful to them specifically. For example, you should collaborate with a senior software engineer differently than a student who is coding for the very first time. Keep in mind, that the aim here is to be helpful to the user. Avoid writing memories about the user that could be viewed as a negative judgement or that are not relevant to the work you're trying to accomplish together.</description>",
|
| 122 |
+
" <when_to_save>When you learn any details about the user's role, preferences, responsibilities, or knowledge</when_to_save>",
|
| 123 |
+
" <how_to_use>When your work should be informed by the user's profile or perspective. For example, if the user is asking you to explain a part of the code, you should answer that question in a way that is tailored to the specific details that they will find most valuable or that helps them build their mental model in relation to domain knowledge they already have.</how_to_use>",
|
| 124 |
+
' <examples>',
|
| 125 |
+
" user: I'm a data scientist investigating what logging we have in place",
|
| 126 |
+
' assistant: [saves user memory: user is a data scientist, currently focused on observability/logging]',
|
| 127 |
+
'',
|
| 128 |
+
" user: I've been writing Go for ten years but this is my first time touching the React side of this repo",
|
| 129 |
+
" assistant: [saves user memory: deep Go expertise, new to React and this project's frontend — frame frontend explanations in terms of backend analogues]",
|
| 130 |
+
' </examples>',
|
| 131 |
+
'</type>',
|
| 132 |
+
'<type>',
|
| 133 |
+
' <name>feedback</name>',
|
| 134 |
+
' <description>Guidance the user has given you about how to approach work — both what to avoid and what to keep doing. These are a very important type of memory to read and write as they allow you to remain coherent and responsive to the way you should approach work in the project. Record from failure AND success: if you only save corrections, you will avoid past mistakes but drift away from approaches the user has already validated, and may grow overly cautious.</description>',
|
| 135 |
+
' <when_to_save>Any time the user corrects your approach ("no not that", "don\'t", "stop doing X") OR confirms a non-obvious approach worked ("yes exactly", "perfect, keep doing that", accepting an unusual choice without pushback). Corrections are easy to notice; confirmations are quieter — watch for them. In both cases, save what is applicable to future conversations, especially if surprising or not obvious from the code. Include *why* so you can judge edge cases later.</when_to_save>',
|
| 136 |
+
' <how_to_use>Let these memories guide your behavior so that the user does not need to offer the same guidance twice.</how_to_use>',
|
| 137 |
+
' <body_structure>Lead with the rule itself, then a **Why:** line (the reason the user gave — often a past incident or strong preference) and a **How to apply:** line (when/where this guidance kicks in). Knowing *why* lets you judge edge cases instead of blindly following the rule.</body_structure>',
|
| 138 |
+
' <examples>',
|
| 139 |
+
" user: don't mock the database in these tests — we got burned last quarter when mocked tests passed but the prod migration failed",
|
| 140 |
+
' assistant: [saves feedback memory: integration tests must hit a real database, not mocks. Reason: prior incident where mock/prod divergence masked a broken migration]',
|
| 141 |
+
'',
|
| 142 |
+
' user: stop summarizing what you just did at the end of every response, I can read the diff',
|
| 143 |
+
' assistant: [saves feedback memory: this user wants terse responses with no trailing summaries]',
|
| 144 |
+
'',
|
| 145 |
+
" user: yeah the single bundled PR was the right call here, splitting this one would've just been churn",
|
| 146 |
+
' assistant: [saves feedback memory: for refactors in this area, user prefers one bundled PR over many small ones. Confirmed after I chose this approach — a validated judgment call, not a correction]',
|
| 147 |
+
' </examples>',
|
| 148 |
+
'</type>',
|
| 149 |
+
'<type>',
|
| 150 |
+
' <name>project</name>',
|
| 151 |
+
' <description>Information that you learn about ongoing work, goals, initiatives, bugs, or incidents within the project that is not otherwise derivable from the code or git history. Project memories help you understand the broader context and motivation behind the work the user is doing within this working directory.</description>',
|
| 152 |
+
' <when_to_save>When you learn who is doing what, why, or by when. These states change relatively quickly so try to keep your understanding of this up to date. Always convert relative dates in user messages to absolute dates when saving (e.g., "Thursday" → "2026-03-05"), so the memory remains interpretable after time passes.</when_to_save>',
|
| 153 |
+
" <how_to_use>Use these memories to more fully understand the details and nuance behind the user's request and make better informed suggestions.</how_to_use>",
|
| 154 |
+
' <body_structure>Lead with the fact or decision, then a **Why:** line (the motivation — often a constraint, deadline, or stakeholder ask) and a **How to apply:** line (how this should shape your suggestions). Project memories decay fast, so the why helps future-you judge whether the memory is still load-bearing.</body_structure>',
|
| 155 |
+
' <examples>',
|
| 156 |
+
" user: we're freezing all non-critical merges after Thursday — mobile team is cutting a release branch",
|
| 157 |
+
' assistant: [saves project memory: merge freeze begins 2026-03-05 for mobile release cut. Flag any non-critical PR work scheduled after that date]',
|
| 158 |
+
'',
|
| 159 |
+
" user: the reason we're ripping out the old auth middleware is that legal flagged it for storing session tokens in a way that doesn't meet the new compliance requirements",
|
| 160 |
+
' assistant: [saves project memory: auth middleware rewrite is driven by legal/compliance requirements around session token storage, not tech-debt cleanup — scope decisions should favor compliance over ergonomics]',
|
| 161 |
+
' </examples>',
|
| 162 |
+
'</type>',
|
| 163 |
+
'<type>',
|
| 164 |
+
' <name>reference</name>',
|
| 165 |
+
' <description>Stores pointers to where information can be found in external systems. These memories allow you to remember where to look to find up-to-date information outside of the project directory.</description>',
|
| 166 |
+
' <when_to_save>When you learn about resources in external systems and their purpose. For example, that bugs are tracked in a specific project in Linear or that feedback can be found in a specific Slack channel.</when_to_save>',
|
| 167 |
+
' <how_to_use>When the user references an external system or information that may be in an external system.</how_to_use>',
|
| 168 |
+
' <examples>',
|
| 169 |
+
' user: check the Linear project "INGEST" if you want context on these tickets, that\'s where we track all pipeline bugs',
|
| 170 |
+
' assistant: [saves reference memory: pipeline bugs are tracked in Linear project "INGEST"]',
|
| 171 |
+
'',
|
| 172 |
+
" user: the Grafana board at grafana.internal/d/api-latency is what oncall watches — if you're touching request handling, that's the thing that'll page someone",
|
| 173 |
+
' assistant: [saves reference memory: grafana.internal/d/api-latency is the oncall latency dashboard — check it when editing request-path code]',
|
| 174 |
+
' </examples>',
|
| 175 |
+
'</type>',
|
| 176 |
+
'</types>',
|
| 177 |
+
'',
|
| 178 |
+
]
|
| 179 |
+
|
| 180 |
+
/**
|
| 181 |
+
* `## What NOT to save in memory` section. Identical across both modes.
|
| 182 |
+
*/
|
| 183 |
+
export const WHAT_NOT_TO_SAVE_SECTION: readonly string[] = [
|
| 184 |
+
'## What NOT to save in memory',
|
| 185 |
+
'',
|
| 186 |
+
'- Code patterns, conventions, architecture, file paths, or project structure — these can be derived by reading the current project state.',
|
| 187 |
+
'- Git history, recent changes, or who-changed-what — `git log` / `git blame` are authoritative.',
|
| 188 |
+
'- Debugging solutions or fix recipes — the fix is in the code; the commit message has the context.',
|
| 189 |
+
'- Anything already documented in CLAUDE.md files.',
|
| 190 |
+
'- Ephemeral task details: in-progress work, temporary state, current conversation context.',
|
| 191 |
+
'',
|
| 192 |
+
// H2: explicit-save gate. Eval-validated (memory-prompt-iteration case 3,
|
| 193 |
+
// 0/2 → 3/3): prevents "save this week's PR list" → activity-log noise.
|
| 194 |
+
'These exclusions apply even when the user explicitly asks you to save. If they ask you to save a PR list or activity summary, ask what was *surprising* or *non-obvious* about it — that is the part worth keeping.',
|
| 195 |
+
]
|
| 196 |
+
|
| 197 |
+
/**
|
| 198 |
+
* Recall-side drift caveat. Single bullet under `## When to access memories`.
|
| 199 |
+
* Proactive: verify memory against current state before answering.
|
| 200 |
+
*/
|
| 201 |
+
export const MEMORY_DRIFT_CAVEAT =
|
| 202 |
+
'- Memory records can become stale over time. Use memory as context for what was true at a given point in time. Before answering the user or building assumptions based solely on information in memory records, verify that the memory is still correct and up-to-date by reading the current state of the files or resources. If a recalled memory conflicts with current information, trust what you observe now — and update or remove the stale memory rather than acting on it.'
|
| 203 |
+
|
| 204 |
+
/**
|
| 205 |
+
* `## When to access memories` section. Includes MEMORY_DRIFT_CAVEAT.
|
| 206 |
+
*
|
| 207 |
+
* H6 (branch-pollution evals #22856, case 5 1/3 on capy): the "ignore" bullet
|
| 208 |
+
* is the delta. Failure mode: user says "ignore memory about X" → Claude reads
|
| 209 |
+
* code correctly but adds "not Y as noted in memory" — treats "ignore" as
|
| 210 |
+
* "acknowledge then override" rather than "don't reference at all." The bullet
|
| 211 |
+
* names that anti-pattern explicitly.
|
| 212 |
+
*
|
| 213 |
+
* Token budget (H6a): merged old bullets 1+2, tightened both. Old 4 lines
|
| 214 |
+
* were ~70 tokens; new 4 lines are ~73 tokens. Net ~+3.
|
| 215 |
+
*/
|
| 216 |
+
export const WHEN_TO_ACCESS_SECTION: readonly string[] = [
|
| 217 |
+
'## When to access memories',
|
| 218 |
+
'- When memories seem relevant, or the user references prior-conversation work.',
|
| 219 |
+
'- You MUST access memory when the user explicitly asks you to check, recall, or remember.',
|
| 220 |
+
'- If the user says to *ignore* or *not use* memory: proceed as if MEMORY.md were empty. Do not apply remembered facts, cite, compare against, or mention memory content.',
|
| 221 |
+
MEMORY_DRIFT_CAVEAT,
|
| 222 |
+
]
|
| 223 |
+
|
| 224 |
+
/**
|
| 225 |
+
* `## Trusting what you recall` section. Heavier-weight guidance on HOW to
|
| 226 |
+
* treat a memory once you've recalled it — separate from WHEN to access.
|
| 227 |
+
*
|
| 228 |
+
* Eval-validated (memory-prompt-iteration.eval.ts, 2026-03-17):
|
| 229 |
+
* H1 (verify function/file claims): 0/2 → 3/3 via appendSystemPrompt. When
|
| 230 |
+
* buried as a bullet under "When to access", dropped to 0/3 — position
|
| 231 |
+
* matters. The H1 cue is about what to DO with a memory, not when to
|
| 232 |
+
* look, so it needs its own section-level trigger context.
|
| 233 |
+
* H5 (read-side noise rejection): 0/2 → 3/3 via appendSystemPrompt, 2/3
|
| 234 |
+
* in-place as a bullet. Partial because "snapshot" is intuitively closer
|
| 235 |
+
* to "when to access" than H1 is.
|
| 236 |
+
*
|
| 237 |
+
* Known gap: H1 doesn't cover slash-command claims (0/3 on the /fork case —
|
| 238 |
+
* slash commands aren't files or functions in the model's ontology).
|
| 239 |
+
*/
|
| 240 |
+
export const TRUSTING_RECALL_SECTION: readonly string[] = [
|
| 241 |
+
// Header wording matters: "Before recommending" (action cue at the decision
|
| 242 |
+
// point) tested better than "Trusting what you recall" (abstract). The
|
| 243 |
+
// appendSystemPrompt variant with this header went 3/3; the abstract header
|
| 244 |
+
// went 0/3 in-place. Same body text — only the header differed.
|
| 245 |
+
'## Before recommending from memory',
|
| 246 |
+
'',
|
| 247 |
+
'A memory that names a specific function, file, or flag is a claim that it existed *when the memory was written*. It may have been renamed, removed, or never merged. Before recommending it:',
|
| 248 |
+
'',
|
| 249 |
+
'- If the memory names a file path: check the file exists.',
|
| 250 |
+
'- If the memory names a function or flag: grep for it.',
|
| 251 |
+
'- If the user is about to act on your recommendation (not just asking about history), verify first.',
|
| 252 |
+
'',
|
| 253 |
+
'"The memory says X exists" is not the same as "X exists now."',
|
| 254 |
+
'',
|
| 255 |
+
'A memory that summarizes repo state (activity logs, architecture snapshots) is frozen in time. If the user asks about *recent* or *current* state, prefer `git log` or reading the code over recalling the snapshot.',
|
| 256 |
+
]
|
| 257 |
+
|
| 258 |
+
/**
|
| 259 |
+
* Frontmatter format example with the `type` field.
|
| 260 |
+
*/
|
| 261 |
+
export const MEMORY_FRONTMATTER_EXAMPLE: readonly string[] = [
|
| 262 |
+
'```markdown',
|
| 263 |
+
'---',
|
| 264 |
+
'name: {{memory name}}',
|
| 265 |
+
'description: {{one-line description — used to decide relevance in future conversations, so be specific}}',
|
| 266 |
+
`type: {{${MEMORY_TYPES.join(', ')}}}`,
|
| 267 |
+
'---',
|
| 268 |
+
'',
|
| 269 |
+
'{{memory content — for feedback/project types, structure as: rule/fact, then **Why:** and **How to apply:** lines}}',
|
| 270 |
+
'```',
|
| 271 |
+
]
|
src/memdir/paths.ts
ADDED
|
@@ -0,0 +1,278 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import memoize from 'lodash-es/memoize.js'
|
| 2 |
+
import { homedir } from 'os'
|
| 3 |
+
import { isAbsolute, join, normalize, sep } from 'path'
|
| 4 |
+
import {
|
| 5 |
+
getIsNonInteractiveSession,
|
| 6 |
+
getProjectRoot,
|
| 7 |
+
} from '../bootstrap/state.js'
|
| 8 |
+
import { getFeatureValue_CACHED_MAY_BE_STALE } from '../services/analytics/growthbook.js'
|
| 9 |
+
import {
|
| 10 |
+
getClaudeConfigHomeDir,
|
| 11 |
+
isEnvDefinedFalsy,
|
| 12 |
+
isEnvTruthy,
|
| 13 |
+
} from '../utils/envUtils.js'
|
| 14 |
+
import { findCanonicalGitRoot } from '../utils/git.js'
|
| 15 |
+
import { sanitizePath } from '../utils/path.js'
|
| 16 |
+
import {
|
| 17 |
+
getInitialSettings,
|
| 18 |
+
getSettingsForSource,
|
| 19 |
+
} from '../utils/settings/settings.js'
|
| 20 |
+
|
| 21 |
+
/**
|
| 22 |
+
* Whether auto-memory features are enabled (memdir, agent memory, past session search).
|
| 23 |
+
* Enabled by default. Priority chain (first defined wins):
|
| 24 |
+
* 1. CLAUDE_CODE_DISABLE_AUTO_MEMORY env var (1/true → OFF, 0/false → ON)
|
| 25 |
+
* 2. CLAUDE_CODE_SIMPLE (--bare) → OFF
|
| 26 |
+
* 3. CCR without persistent storage → OFF (no CLAUDE_CODE_REMOTE_MEMORY_DIR)
|
| 27 |
+
* 4. autoMemoryEnabled in settings.json (supports project-level opt-out)
|
| 28 |
+
* 5. Default: enabled
|
| 29 |
+
*/
|
| 30 |
+
export function isAutoMemoryEnabled(): boolean {
|
| 31 |
+
const envVal = process.env.CLAUDE_CODE_DISABLE_AUTO_MEMORY
|
| 32 |
+
if (isEnvTruthy(envVal)) {
|
| 33 |
+
return false
|
| 34 |
+
}
|
| 35 |
+
if (isEnvDefinedFalsy(envVal)) {
|
| 36 |
+
return true
|
| 37 |
+
}
|
| 38 |
+
// --bare / SIMPLE: prompts.ts already drops the memory section from the
|
| 39 |
+
// system prompt via its SIMPLE early-return; this gate stops the other half
|
| 40 |
+
// (extractMemories turn-end fork, autoDream, /remember, /dream, team sync).
|
| 41 |
+
if (isEnvTruthy(process.env.CLAUDE_CODE_SIMPLE)) {
|
| 42 |
+
return false
|
| 43 |
+
}
|
| 44 |
+
if (
|
| 45 |
+
isEnvTruthy(process.env.CLAUDE_CODE_REMOTE) &&
|
| 46 |
+
!process.env.CLAUDE_CODE_REMOTE_MEMORY_DIR
|
| 47 |
+
) {
|
| 48 |
+
return false
|
| 49 |
+
}
|
| 50 |
+
const settings = getInitialSettings()
|
| 51 |
+
if (settings.autoMemoryEnabled !== undefined) {
|
| 52 |
+
return settings.autoMemoryEnabled
|
| 53 |
+
}
|
| 54 |
+
return true
|
| 55 |
+
}
|
| 56 |
+
|
| 57 |
+
/**
|
| 58 |
+
* Whether the extract-memories background agent will run this session.
|
| 59 |
+
*
|
| 60 |
+
* The main agent's prompt always has full save instructions regardless of
|
| 61 |
+
* this gate — when the main agent writes memories, the background agent
|
| 62 |
+
* skips that range (hasMemoryWritesSince in extractMemories.ts); when it
|
| 63 |
+
* doesn't, the background agent catches anything missed.
|
| 64 |
+
*
|
| 65 |
+
* Callers must also gate on feature('EXTRACT_MEMORIES') — that check cannot
|
| 66 |
+
* live inside this helper because feature() only tree-shakes when used
|
| 67 |
+
* directly in an `if` condition.
|
| 68 |
+
*/
|
| 69 |
+
export function isExtractModeActive(): boolean {
|
| 70 |
+
if (!getFeatureValue_CACHED_MAY_BE_STALE('tengu_passport_quail', false)) {
|
| 71 |
+
return false
|
| 72 |
+
}
|
| 73 |
+
return (
|
| 74 |
+
!getIsNonInteractiveSession() ||
|
| 75 |
+
getFeatureValue_CACHED_MAY_BE_STALE('tengu_slate_thimble', false)
|
| 76 |
+
)
|
| 77 |
+
}
|
| 78 |
+
|
| 79 |
+
/**
|
| 80 |
+
* Returns the base directory for persistent memory storage.
|
| 81 |
+
* Resolution order:
|
| 82 |
+
* 1. CLAUDE_CODE_REMOTE_MEMORY_DIR env var (explicit override, set in CCR)
|
| 83 |
+
* 2. ~/.claude (default config home)
|
| 84 |
+
*/
|
| 85 |
+
export function getMemoryBaseDir(): string {
|
| 86 |
+
if (process.env.CLAUDE_CODE_REMOTE_MEMORY_DIR) {
|
| 87 |
+
return process.env.CLAUDE_CODE_REMOTE_MEMORY_DIR
|
| 88 |
+
}
|
| 89 |
+
return getClaudeConfigHomeDir()
|
| 90 |
+
}
|
| 91 |
+
|
| 92 |
+
const AUTO_MEM_DIRNAME = 'memory'
|
| 93 |
+
const AUTO_MEM_ENTRYPOINT_NAME = 'MEMORY.md'
|
| 94 |
+
|
| 95 |
+
/**
|
| 96 |
+
* Normalize and validate a candidate auto-memory directory path.
|
| 97 |
+
*
|
| 98 |
+
* SECURITY: Rejects paths that would be dangerous as a read-allowlist root
|
| 99 |
+
* or that normalize() doesn't fully resolve:
|
| 100 |
+
* - relative (!isAbsolute): "../foo" — would be interpreted relative to CWD
|
| 101 |
+
* - root/near-root (length < 3): "/" → "" after strip; "/a" too short
|
| 102 |
+
* - Windows drive-root (C: regex): "C:\" → "C:" after strip
|
| 103 |
+
* - UNC paths (\\server\share): network paths — opaque trust boundary
|
| 104 |
+
* - null byte: survives normalize(), can truncate in syscalls
|
| 105 |
+
*
|
| 106 |
+
* Returns the normalized path with exactly one trailing separator,
|
| 107 |
+
* or undefined if the path is unset/empty/rejected.
|
| 108 |
+
*/
|
| 109 |
+
function validateMemoryPath(
|
| 110 |
+
raw: string | undefined,
|
| 111 |
+
expandTilde: boolean,
|
| 112 |
+
): string | undefined {
|
| 113 |
+
if (!raw) {
|
| 114 |
+
return undefined
|
| 115 |
+
}
|
| 116 |
+
let candidate = raw
|
| 117 |
+
// Settings.json paths support ~/ expansion (user-friendly). The env var
|
| 118 |
+
// override does not (it's set programmatically by Cowork/SDK, which should
|
| 119 |
+
// always pass absolute paths). Bare "~", "~/", "~/.", "~/..", etc. are NOT
|
| 120 |
+
// expanded — they would make isAutoMemPath() match all of $HOME or its
|
| 121 |
+
// parent (same class of danger as "/" or "C:\").
|
| 122 |
+
if (
|
| 123 |
+
expandTilde &&
|
| 124 |
+
(candidate.startsWith('~/') || candidate.startsWith('~\\'))
|
| 125 |
+
) {
|
| 126 |
+
const rest = candidate.slice(2)
|
| 127 |
+
// Reject trivial remainders that would expand to $HOME or an ancestor.
|
| 128 |
+
// normalize('') = '.', normalize('.') = '.', normalize('foo/..') = '.',
|
| 129 |
+
// normalize('..') = '..', normalize('foo/../..') = '..'
|
| 130 |
+
const restNorm = normalize(rest || '.')
|
| 131 |
+
if (restNorm === '.' || restNorm === '..') {
|
| 132 |
+
return undefined
|
| 133 |
+
}
|
| 134 |
+
candidate = join(homedir(), rest)
|
| 135 |
+
}
|
| 136 |
+
// normalize() may preserve a trailing separator; strip before adding
|
| 137 |
+
// exactly one to match the trailing-sep contract of getAutoMemPath()
|
| 138 |
+
const normalized = normalize(candidate).replace(/[/\\]+$/, '')
|
| 139 |
+
if (
|
| 140 |
+
!isAbsolute(normalized) ||
|
| 141 |
+
normalized.length < 3 ||
|
| 142 |
+
/^[A-Za-z]:$/.test(normalized) ||
|
| 143 |
+
normalized.startsWith('\\\\') ||
|
| 144 |
+
normalized.startsWith('//') ||
|
| 145 |
+
normalized.includes('\0')
|
| 146 |
+
) {
|
| 147 |
+
return undefined
|
| 148 |
+
}
|
| 149 |
+
return (normalized + sep).normalize('NFC')
|
| 150 |
+
}
|
| 151 |
+
|
| 152 |
+
/**
|
| 153 |
+
* Direct override for the full auto-memory directory path via env var.
|
| 154 |
+
* When set, getAutoMemPath()/getAutoMemEntrypoint() return this path directly
|
| 155 |
+
* instead of computing `{base}/projects/{sanitized-cwd}/memory/`.
|
| 156 |
+
*
|
| 157 |
+
* Used by Cowork to redirect memory to a space-scoped mount where the
|
| 158 |
+
* per-session cwd (which contains the VM process name) would otherwise
|
| 159 |
+
* produce a different project-key for every session.
|
| 160 |
+
*/
|
| 161 |
+
function getAutoMemPathOverride(): string | undefined {
|
| 162 |
+
return validateMemoryPath(
|
| 163 |
+
process.env.CLAUDE_COWORK_MEMORY_PATH_OVERRIDE,
|
| 164 |
+
false,
|
| 165 |
+
)
|
| 166 |
+
}
|
| 167 |
+
|
| 168 |
+
/**
|
| 169 |
+
* Settings.json override for the full auto-memory directory path.
|
| 170 |
+
* Supports ~/ expansion for user convenience.
|
| 171 |
+
*
|
| 172 |
+
* SECURITY: projectSettings (.claude/settings.json committed to the repo) is
|
| 173 |
+
* intentionally excluded — a malicious repo could otherwise set
|
| 174 |
+
* autoMemoryDirectory: "~/.ssh" and gain silent write access to sensitive
|
| 175 |
+
* directories via the filesystem.ts write carve-out (which fires when
|
| 176 |
+
* isAutoMemPath() matches and hasAutoMemPathOverride() is false). This follows
|
| 177 |
+
* the same pattern as hasSkipDangerousModePermissionPrompt() etc.
|
| 178 |
+
*/
|
| 179 |
+
function getAutoMemPathSetting(): string | undefined {
|
| 180 |
+
const dir =
|
| 181 |
+
getSettingsForSource('policySettings')?.autoMemoryDirectory ??
|
| 182 |
+
getSettingsForSource('flagSettings')?.autoMemoryDirectory ??
|
| 183 |
+
getSettingsForSource('localSettings')?.autoMemoryDirectory ??
|
| 184 |
+
getSettingsForSource('userSettings')?.autoMemoryDirectory
|
| 185 |
+
return validateMemoryPath(dir, true)
|
| 186 |
+
}
|
| 187 |
+
|
| 188 |
+
/**
|
| 189 |
+
* Check if CLAUDE_COWORK_MEMORY_PATH_OVERRIDE is set to a valid override.
|
| 190 |
+
* Use this as a signal that the SDK caller has explicitly opted into
|
| 191 |
+
* the auto-memory mechanics — e.g. to decide whether to inject the
|
| 192 |
+
* memory prompt when a custom system prompt replaces the default.
|
| 193 |
+
*/
|
| 194 |
+
export function hasAutoMemPathOverride(): boolean {
|
| 195 |
+
return getAutoMemPathOverride() !== undefined
|
| 196 |
+
}
|
| 197 |
+
|
| 198 |
+
/**
|
| 199 |
+
* Returns the canonical git repo root if available, otherwise falls back to
|
| 200 |
+
* the stable project root. Uses findCanonicalGitRoot so all worktrees of the
|
| 201 |
+
* same repo share one auto-memory directory (anthropics/claude-code#24382).
|
| 202 |
+
*/
|
| 203 |
+
function getAutoMemBase(): string {
|
| 204 |
+
return findCanonicalGitRoot(getProjectRoot()) ?? getProjectRoot()
|
| 205 |
+
}
|
| 206 |
+
|
| 207 |
+
/**
|
| 208 |
+
* Returns the auto-memory directory path.
|
| 209 |
+
*
|
| 210 |
+
* Resolution order:
|
| 211 |
+
* 1. CLAUDE_COWORK_MEMORY_PATH_OVERRIDE env var (full-path override, used by Cowork)
|
| 212 |
+
* 2. autoMemoryDirectory in settings.json (trusted sources only: policy/local/user)
|
| 213 |
+
* 3. <memoryBase>/projects/<sanitized-git-root>/memory/
|
| 214 |
+
* where memoryBase is resolved by getMemoryBaseDir()
|
| 215 |
+
*
|
| 216 |
+
* Memoized: render-path callers (collapseReadSearchGroups → isAutoManagedMemoryFile)
|
| 217 |
+
* fire per tool-use message per Messages re-render; each miss costs
|
| 218 |
+
* getSettingsForSource × 4 → parseSettingsFile (realpathSync + readFileSync).
|
| 219 |
+
* Keyed on projectRoot so tests that change its mock mid-block recompute;
|
| 220 |
+
* env vars / settings.json / CLAUDE_CONFIG_DIR are session-stable in
|
| 221 |
+
* production and covered by per-test cache.clear.
|
| 222 |
+
*/
|
| 223 |
+
export const getAutoMemPath = memoize(
|
| 224 |
+
(): string => {
|
| 225 |
+
const override = getAutoMemPathOverride() ?? getAutoMemPathSetting()
|
| 226 |
+
if (override) {
|
| 227 |
+
return override
|
| 228 |
+
}
|
| 229 |
+
const projectsDir = join(getMemoryBaseDir(), 'projects')
|
| 230 |
+
return (
|
| 231 |
+
join(projectsDir, sanitizePath(getAutoMemBase()), AUTO_MEM_DIRNAME) + sep
|
| 232 |
+
).normalize('NFC')
|
| 233 |
+
},
|
| 234 |
+
() => getProjectRoot(),
|
| 235 |
+
)
|
| 236 |
+
|
| 237 |
+
/**
|
| 238 |
+
* Returns the daily log file path for the given date (defaults to today).
|
| 239 |
+
* Shape: <autoMemPath>/logs/YYYY/MM/YYYY-MM-DD.md
|
| 240 |
+
*
|
| 241 |
+
* Used by assistant mode (feature('KAIROS')): rather than maintaining
|
| 242 |
+
* MEMORY.md as a live index, the agent appends to a date-named log file
|
| 243 |
+
* as it works. A separate nightly /dream skill distills these logs into
|
| 244 |
+
* topic files + MEMORY.md.
|
| 245 |
+
*/
|
| 246 |
+
export function getAutoMemDailyLogPath(date: Date = new Date()): string {
|
| 247 |
+
const yyyy = date.getFullYear().toString()
|
| 248 |
+
const mm = (date.getMonth() + 1).toString().padStart(2, '0')
|
| 249 |
+
const dd = date.getDate().toString().padStart(2, '0')
|
| 250 |
+
return join(getAutoMemPath(), 'logs', yyyy, mm, `${yyyy}-${mm}-${dd}.md`)
|
| 251 |
+
}
|
| 252 |
+
|
| 253 |
+
/**
|
| 254 |
+
* Returns the auto-memory entrypoint (MEMORY.md inside the auto-memory dir).
|
| 255 |
+
* Follows the same resolution order as getAutoMemPath().
|
| 256 |
+
*/
|
| 257 |
+
export function getAutoMemEntrypoint(): string {
|
| 258 |
+
return join(getAutoMemPath(), AUTO_MEM_ENTRYPOINT_NAME)
|
| 259 |
+
}
|
| 260 |
+
|
| 261 |
+
/**
|
| 262 |
+
* Check if an absolute path is within the auto-memory directory.
|
| 263 |
+
*
|
| 264 |
+
* When CLAUDE_COWORK_MEMORY_PATH_OVERRIDE is set, this matches against the
|
| 265 |
+
* env-var override directory. Note that a true return here does NOT imply
|
| 266 |
+
* write permission in that case — the filesystem.ts write carve-out is gated
|
| 267 |
+
* on !hasAutoMemPathOverride() (it exists to bypass DANGEROUS_DIRECTORIES).
|
| 268 |
+
*
|
| 269 |
+
* The settings.json autoMemoryDirectory DOES get the write carve-out: it's the
|
| 270 |
+
* user's explicit choice from a trusted settings source (projectSettings is
|
| 271 |
+
* excluded — see getAutoMemPathSetting), and hasAutoMemPathOverride() remains
|
| 272 |
+
* false for it.
|
| 273 |
+
*/
|
| 274 |
+
export function isAutoMemPath(absolutePath: string): boolean {
|
| 275 |
+
// SECURITY: Normalize to prevent path traversal bypasses via .. segments
|
| 276 |
+
const normalizedPath = normalize(absolutePath)
|
| 277 |
+
return normalizedPath.startsWith(getAutoMemPath())
|
| 278 |
+
}
|
src/memdir/teamMemPaths.ts
ADDED
|
@@ -0,0 +1,292 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { lstat, realpath } from 'fs/promises'
|
| 2 |
+
import { dirname, join, resolve, sep } from 'path'
|
| 3 |
+
import { getFeatureValue_CACHED_MAY_BE_STALE } from '../services/analytics/growthbook.js'
|
| 4 |
+
import { getErrnoCode } from '../utils/errors.js'
|
| 5 |
+
import { getAutoMemPath, isAutoMemoryEnabled } from './paths.js'
|
| 6 |
+
|
| 7 |
+
/**
|
| 8 |
+
* Error thrown when a path validation detects a traversal or injection attempt.
|
| 9 |
+
*/
|
| 10 |
+
export class PathTraversalError extends Error {
|
| 11 |
+
constructor(message: string) {
|
| 12 |
+
super(message)
|
| 13 |
+
this.name = 'PathTraversalError'
|
| 14 |
+
}
|
| 15 |
+
}
|
| 16 |
+
|
| 17 |
+
/**
|
| 18 |
+
* Sanitize a file path key by rejecting dangerous patterns.
|
| 19 |
+
* Checks for null bytes, URL-encoded traversals, and other injection vectors.
|
| 20 |
+
* Returns the sanitized string or throws PathTraversalError.
|
| 21 |
+
*/
|
| 22 |
+
function sanitizePathKey(key: string): string {
|
| 23 |
+
// Null bytes can truncate paths in C-based syscalls
|
| 24 |
+
if (key.includes('\0')) {
|
| 25 |
+
throw new PathTraversalError(`Null byte in path key: "${key}"`)
|
| 26 |
+
}
|
| 27 |
+
// URL-encoded traversals (e.g. %2e%2e%2f = ../)
|
| 28 |
+
let decoded: string
|
| 29 |
+
try {
|
| 30 |
+
decoded = decodeURIComponent(key)
|
| 31 |
+
} catch {
|
| 32 |
+
// Malformed percent-encoding (e.g. %ZZ, lone %) — not valid URL-encoding,
|
| 33 |
+
// so no URL-encoded traversal is possible
|
| 34 |
+
decoded = key
|
| 35 |
+
}
|
| 36 |
+
if (decoded !== key && (decoded.includes('..') || decoded.includes('/'))) {
|
| 37 |
+
throw new PathTraversalError(`URL-encoded traversal in path key: "${key}"`)
|
| 38 |
+
}
|
| 39 |
+
// Unicode normalization attacks: fullwidth ../ (U+FF0E U+FF0F) normalize
|
| 40 |
+
// to ASCII ../ under NFKC. While path.resolve/fs.writeFile treat these as
|
| 41 |
+
// literal bytes (not separators), downstream layers or filesystems may
|
| 42 |
+
// normalize — reject for defense-in-depth (PSR M22187 vector 4).
|
| 43 |
+
const normalized = key.normalize('NFKC')
|
| 44 |
+
if (
|
| 45 |
+
normalized !== key &&
|
| 46 |
+
(normalized.includes('..') ||
|
| 47 |
+
normalized.includes('/') ||
|
| 48 |
+
normalized.includes('\\') ||
|
| 49 |
+
normalized.includes('\0'))
|
| 50 |
+
) {
|
| 51 |
+
throw new PathTraversalError(
|
| 52 |
+
`Unicode-normalized traversal in path key: "${key}"`,
|
| 53 |
+
)
|
| 54 |
+
}
|
| 55 |
+
// Reject backslashes (Windows path separator used as traversal vector)
|
| 56 |
+
if (key.includes('\\')) {
|
| 57 |
+
throw new PathTraversalError(`Backslash in path key: "${key}"`)
|
| 58 |
+
}
|
| 59 |
+
// Reject absolute paths
|
| 60 |
+
if (key.startsWith('/')) {
|
| 61 |
+
throw new PathTraversalError(`Absolute path key: "${key}"`)
|
| 62 |
+
}
|
| 63 |
+
return key
|
| 64 |
+
}
|
| 65 |
+
|
| 66 |
+
/**
|
| 67 |
+
* Whether team memory features are enabled.
|
| 68 |
+
* Team memory is a subdirectory of auto memory, so it requires auto memory
|
| 69 |
+
* to be enabled. This keeps all team-memory consumers (prompt, content
|
| 70 |
+
* injection, sync watcher, file detection) consistent when auto memory is
|
| 71 |
+
* disabled via env var or settings.
|
| 72 |
+
*/
|
| 73 |
+
export function isTeamMemoryEnabled(): boolean {
|
| 74 |
+
if (!isAutoMemoryEnabled()) {
|
| 75 |
+
return false
|
| 76 |
+
}
|
| 77 |
+
return getFeatureValue_CACHED_MAY_BE_STALE('tengu_herring_clock', false)
|
| 78 |
+
}
|
| 79 |
+
|
| 80 |
+
/**
|
| 81 |
+
* Returns the team memory path: <memoryBase>/projects/<sanitized-project-root>/memory/team/
|
| 82 |
+
* Lives as a subdirectory of the auto-memory directory, scoped per-project.
|
| 83 |
+
*/
|
| 84 |
+
export function getTeamMemPath(): string {
|
| 85 |
+
return (join(getAutoMemPath(), 'team') + sep).normalize('NFC')
|
| 86 |
+
}
|
| 87 |
+
|
| 88 |
+
/**
|
| 89 |
+
* Returns the team memory entrypoint: <memoryBase>/projects/<sanitized-project-root>/memory/team/MEMORY.md
|
| 90 |
+
* Lives as a subdirectory of the auto-memory directory, scoped per-project.
|
| 91 |
+
*/
|
| 92 |
+
export function getTeamMemEntrypoint(): string {
|
| 93 |
+
return join(getAutoMemPath(), 'team', 'MEMORY.md')
|
| 94 |
+
}
|
| 95 |
+
|
| 96 |
+
/**
|
| 97 |
+
* Resolve symlinks for the deepest existing ancestor of a path.
|
| 98 |
+
* The target file may not exist yet (we may be about to create it), so we
|
| 99 |
+
* walk up the directory tree until realpath() succeeds, then rejoin the
|
| 100 |
+
* non-existing tail onto the resolved ancestor.
|
| 101 |
+
*
|
| 102 |
+
* SECURITY (PSR M22186): path.resolve() does NOT resolve symlinks. An attacker
|
| 103 |
+
* who can place a symlink inside teamDir pointing outside (e.g. to
|
| 104 |
+
* ~/.ssh/authorized_keys) would pass a resolve()-based containment check.
|
| 105 |
+
* Using realpath() on the deepest existing ancestor ensures we compare the
|
| 106 |
+
* actual filesystem location, not the symbolic path.
|
| 107 |
+
*
|
| 108 |
+
*/
|
| 109 |
+
async function realpathDeepestExisting(absolutePath: string): Promise<string> {
|
| 110 |
+
const tail: string[] = []
|
| 111 |
+
let current = absolutePath
|
| 112 |
+
// Walk up until realpath succeeds. ENOENT means this segment doesn't exist
|
| 113 |
+
// yet; pop it onto the tail and try the parent. ENOTDIR means a non-directory
|
| 114 |
+
// component sits in the middle of the path; pop and retry so we can realpath
|
| 115 |
+
// the ancestor to detect symlink escapes.
|
| 116 |
+
// Loop terminates when we reach the filesystem root (dirname('/') === '/').
|
| 117 |
+
for (
|
| 118 |
+
let parent = dirname(current);
|
| 119 |
+
current !== parent;
|
| 120 |
+
parent = dirname(current)
|
| 121 |
+
) {
|
| 122 |
+
try {
|
| 123 |
+
const realCurrent = await realpath(current)
|
| 124 |
+
// Rejoin the non-existing tail in reverse order (deepest popped first)
|
| 125 |
+
return tail.length === 0
|
| 126 |
+
? realCurrent
|
| 127 |
+
: join(realCurrent, ...tail.reverse())
|
| 128 |
+
} catch (e: unknown) {
|
| 129 |
+
const code = getErrnoCode(e)
|
| 130 |
+
if (code === 'ENOENT') {
|
| 131 |
+
// Could be truly non-existent (safe to walk up) OR a dangling symlink
|
| 132 |
+
// whose target doesn't exist. Dangling symlinks are an attack vector:
|
| 133 |
+
// writeFile would follow the link and create the target outside teamDir.
|
| 134 |
+
// lstat distinguishes: it succeeds for dangling symlinks (the link entry
|
| 135 |
+
// itself exists), fails with ENOENT for truly non-existent paths.
|
| 136 |
+
try {
|
| 137 |
+
const st = await lstat(current)
|
| 138 |
+
if (st.isSymbolicLink()) {
|
| 139 |
+
throw new PathTraversalError(
|
| 140 |
+
`Dangling symlink detected (target does not exist): "${current}"`,
|
| 141 |
+
)
|
| 142 |
+
}
|
| 143 |
+
// lstat succeeded but isn't a symlink — ENOENT from realpath was
|
| 144 |
+
// caused by a dangling symlink in an ancestor. Walk up to find it.
|
| 145 |
+
} catch (lstatErr: unknown) {
|
| 146 |
+
if (lstatErr instanceof PathTraversalError) {
|
| 147 |
+
throw lstatErr
|
| 148 |
+
}
|
| 149 |
+
// lstat also failed (truly non-existent or inaccessible) — safe to walk up.
|
| 150 |
+
}
|
| 151 |
+
} else if (code === 'ELOOP') {
|
| 152 |
+
// Symlink loop — corrupted or malicious filesystem state.
|
| 153 |
+
throw new PathTraversalError(
|
| 154 |
+
`Symlink loop detected in path: "${current}"`,
|
| 155 |
+
)
|
| 156 |
+
} else if (code !== 'ENOTDIR' && code !== 'ENAMETOOLONG') {
|
| 157 |
+
// EACCES, EIO, etc. — cannot verify containment. Fail closed by wrapping
|
| 158 |
+
// as PathTraversalError so the caller can skip this entry gracefully
|
| 159 |
+
// instead of aborting the entire batch.
|
| 160 |
+
throw new PathTraversalError(
|
| 161 |
+
`Cannot verify path containment (${code}): "${current}"`,
|
| 162 |
+
)
|
| 163 |
+
}
|
| 164 |
+
tail.push(current.slice(parent.length + sep.length))
|
| 165 |
+
current = parent
|
| 166 |
+
}
|
| 167 |
+
}
|
| 168 |
+
// Reached filesystem root without finding an existing ancestor (rare —
|
| 169 |
+
// root normally exists). Fall back to the input; containment check will reject.
|
| 170 |
+
return absolutePath
|
| 171 |
+
}
|
| 172 |
+
|
| 173 |
+
/**
|
| 174 |
+
* Check whether a real (symlink-resolved) path is within the real team
|
| 175 |
+
* memory directory. Both sides are realpath'd so the comparison is between
|
| 176 |
+
* canonical filesystem locations.
|
| 177 |
+
*
|
| 178 |
+
* If teamDir does not exist, returns true (skips the check). This is safe:
|
| 179 |
+
* a symlink escape requires a pre-existing symlink inside teamDir, which
|
| 180 |
+
* requires teamDir to exist. If there's no directory, there's no symlink,
|
| 181 |
+
* and the first-pass string-level containment check is sufficient.
|
| 182 |
+
*/
|
| 183 |
+
async function isRealPathWithinTeamDir(
|
| 184 |
+
realCandidate: string,
|
| 185 |
+
): Promise<boolean> {
|
| 186 |
+
let realTeamDir: string
|
| 187 |
+
try {
|
| 188 |
+
// getTeamMemPath() includes a trailing separator; strip it because
|
| 189 |
+
// realpath() rejects trailing separators on some platforms.
|
| 190 |
+
realTeamDir = await realpath(getTeamMemPath().replace(/[/\\]+$/, ''))
|
| 191 |
+
} catch (e: unknown) {
|
| 192 |
+
const code = getErrnoCode(e)
|
| 193 |
+
if (code === 'ENOENT' || code === 'ENOTDIR') {
|
| 194 |
+
// Team dir doesn't exist — symlink escape impossible, skip check.
|
| 195 |
+
return true
|
| 196 |
+
}
|
| 197 |
+
// Unexpected error (EACCES, EIO) — fail closed.
|
| 198 |
+
return false
|
| 199 |
+
}
|
| 200 |
+
if (realCandidate === realTeamDir) {
|
| 201 |
+
return true
|
| 202 |
+
}
|
| 203 |
+
// Prefix-attack protection: require separator after the prefix so that
|
| 204 |
+
// "/foo/team-evil" doesn't match "/foo/team".
|
| 205 |
+
return realCandidate.startsWith(realTeamDir + sep)
|
| 206 |
+
}
|
| 207 |
+
|
| 208 |
+
/**
|
| 209 |
+
* Check if a resolved absolute path is within the team memory directory.
|
| 210 |
+
* Uses path.resolve() to convert relative paths and eliminate traversal segments.
|
| 211 |
+
* Does NOT resolve symlinks — for write validation use validateTeamMemWritePath()
|
| 212 |
+
* or validateTeamMemKey() which include symlink resolution.
|
| 213 |
+
*/
|
| 214 |
+
export function isTeamMemPath(filePath: string): boolean {
|
| 215 |
+
// SECURITY: resolve() converts to absolute and eliminates .. segments,
|
| 216 |
+
// preventing path traversal attacks (e.g. "team/../../etc/passwd")
|
| 217 |
+
const resolvedPath = resolve(filePath)
|
| 218 |
+
const teamDir = getTeamMemPath()
|
| 219 |
+
return resolvedPath.startsWith(teamDir)
|
| 220 |
+
}
|
| 221 |
+
|
| 222 |
+
/**
|
| 223 |
+
* Validate that an absolute file path is safe for writing to the team memory directory.
|
| 224 |
+
* Returns the resolved absolute path if valid.
|
| 225 |
+
* Throws PathTraversalError if the path contains injection vectors, escapes the
|
| 226 |
+
* directory via .. segments, or escapes via a symlink (PSR M22186).
|
| 227 |
+
*/
|
| 228 |
+
export async function validateTeamMemWritePath(
|
| 229 |
+
filePath: string,
|
| 230 |
+
): Promise<string> {
|
| 231 |
+
if (filePath.includes('\0')) {
|
| 232 |
+
throw new PathTraversalError(`Null byte in path: "${filePath}"`)
|
| 233 |
+
}
|
| 234 |
+
// First pass: normalize .. segments and check string-level containment.
|
| 235 |
+
// This is a fast rejection for obvious traversal attempts before we touch
|
| 236 |
+
// the filesystem.
|
| 237 |
+
const resolvedPath = resolve(filePath)
|
| 238 |
+
const teamDir = getTeamMemPath()
|
| 239 |
+
// Prefix attack protection: teamDir already ends with sep (from getTeamMemPath),
|
| 240 |
+
// so "team-evil/" won't match "team/"
|
| 241 |
+
if (!resolvedPath.startsWith(teamDir)) {
|
| 242 |
+
throw new PathTraversalError(
|
| 243 |
+
`Path escapes team memory directory: "${filePath}"`,
|
| 244 |
+
)
|
| 245 |
+
}
|
| 246 |
+
// Second pass: resolve symlinks on the deepest existing ancestor and verify
|
| 247 |
+
// the real path is still within the real team dir. This catches symlink-based
|
| 248 |
+
// escapes that path.resolve() alone cannot detect.
|
| 249 |
+
const realPath = await realpathDeepestExisting(resolvedPath)
|
| 250 |
+
if (!(await isRealPathWithinTeamDir(realPath))) {
|
| 251 |
+
throw new PathTraversalError(
|
| 252 |
+
`Path escapes team memory directory via symlink: "${filePath}"`,
|
| 253 |
+
)
|
| 254 |
+
}
|
| 255 |
+
return resolvedPath
|
| 256 |
+
}
|
| 257 |
+
|
| 258 |
+
/**
|
| 259 |
+
* Validate a relative path key from the server against the team memory directory.
|
| 260 |
+
* Sanitizes the key, joins with the team dir, resolves symlinks on the deepest
|
| 261 |
+
* existing ancestor, and verifies containment against the real team dir.
|
| 262 |
+
* Returns the resolved absolute path.
|
| 263 |
+
* Throws PathTraversalError if the key is malicious (PSR M22186).
|
| 264 |
+
*/
|
| 265 |
+
export async function validateTeamMemKey(relativeKey: string): Promise<string> {
|
| 266 |
+
sanitizePathKey(relativeKey)
|
| 267 |
+
const teamDir = getTeamMemPath()
|
| 268 |
+
const fullPath = join(teamDir, relativeKey)
|
| 269 |
+
// First pass: normalize .. segments and check string-level containment.
|
| 270 |
+
const resolvedPath = resolve(fullPath)
|
| 271 |
+
if (!resolvedPath.startsWith(teamDir)) {
|
| 272 |
+
throw new PathTraversalError(
|
| 273 |
+
`Key escapes team memory directory: "${relativeKey}"`,
|
| 274 |
+
)
|
| 275 |
+
}
|
| 276 |
+
// Second pass: resolve symlinks and verify real containment.
|
| 277 |
+
const realPath = await realpathDeepestExisting(resolvedPath)
|
| 278 |
+
if (!(await isRealPathWithinTeamDir(realPath))) {
|
| 279 |
+
throw new PathTraversalError(
|
| 280 |
+
`Key escapes team memory directory via symlink: "${relativeKey}"`,
|
| 281 |
+
)
|
| 282 |
+
}
|
| 283 |
+
return resolvedPath
|
| 284 |
+
}
|
| 285 |
+
|
| 286 |
+
/**
|
| 287 |
+
* Check if a file path is within the team memory directory
|
| 288 |
+
* and team memory is enabled.
|
| 289 |
+
*/
|
| 290 |
+
export function isTeamMemFile(filePath: string): boolean {
|
| 291 |
+
return isTeamMemoryEnabled() && isTeamMemPath(filePath)
|
| 292 |
+
}
|
src/memdir/teamMemPrompts.ts
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import {
|
| 2 |
+
buildSearchingPastContextSection,
|
| 3 |
+
DIRS_EXIST_GUIDANCE,
|
| 4 |
+
ENTRYPOINT_NAME,
|
| 5 |
+
MAX_ENTRYPOINT_LINES,
|
| 6 |
+
} from './memdir.js'
|
| 7 |
+
import {
|
| 8 |
+
MEMORY_DRIFT_CAVEAT,
|
| 9 |
+
MEMORY_FRONTMATTER_EXAMPLE,
|
| 10 |
+
TRUSTING_RECALL_SECTION,
|
| 11 |
+
TYPES_SECTION_COMBINED,
|
| 12 |
+
WHAT_NOT_TO_SAVE_SECTION,
|
| 13 |
+
} from './memoryTypes.js'
|
| 14 |
+
import { getAutoMemPath } from './paths.js'
|
| 15 |
+
import { getTeamMemPath } from './teamMemPaths.js'
|
| 16 |
+
|
| 17 |
+
/**
|
| 18 |
+
* Build the combined prompt when both auto memory and team memory are enabled.
|
| 19 |
+
* Closed four-type taxonomy (user / feedback / project / reference) with
|
| 20 |
+
* per-type <scope> guidance embedded in XML-style <type> blocks.
|
| 21 |
+
*/
|
| 22 |
+
export function buildCombinedMemoryPrompt(
|
| 23 |
+
extraGuidelines?: string[],
|
| 24 |
+
skipIndex = false,
|
| 25 |
+
): string {
|
| 26 |
+
const autoDir = getAutoMemPath()
|
| 27 |
+
const teamDir = getTeamMemPath()
|
| 28 |
+
|
| 29 |
+
const howToSave = skipIndex
|
| 30 |
+
? [
|
| 31 |
+
'## How to save memories',
|
| 32 |
+
'',
|
| 33 |
+
"Write each memory to its own file in the chosen directory (private or team, per the type's scope guidance) using this frontmatter format:",
|
| 34 |
+
'',
|
| 35 |
+
...MEMORY_FRONTMATTER_EXAMPLE,
|
| 36 |
+
'',
|
| 37 |
+
'- Keep the name, description, and type fields in memory files up-to-date with the content',
|
| 38 |
+
'- Organize memory semantically by topic, not chronologically',
|
| 39 |
+
'- Update or remove memories that turn out to be wrong or outdated',
|
| 40 |
+
'- Do not write duplicate memories. First check if there is an existing memory you can update before writing a new one.',
|
| 41 |
+
]
|
| 42 |
+
: [
|
| 43 |
+
'## How to save memories',
|
| 44 |
+
'',
|
| 45 |
+
'Saving a memory is a two-step process:',
|
| 46 |
+
'',
|
| 47 |
+
"**Step 1** — write the memory to its own file in the chosen directory (private or team, per the type's scope guidance) using this frontmatter format:",
|
| 48 |
+
'',
|
| 49 |
+
...MEMORY_FRONTMATTER_EXAMPLE,
|
| 50 |
+
'',
|
| 51 |
+
`**Step 2** — add a pointer to that file in the same directory's \`${ENTRYPOINT_NAME}\`. Each directory (private and team) has its own \`${ENTRYPOINT_NAME}\` index — each entry should be one line, under ~150 characters: \`- [Title](file.md) — one-line hook\`. They have no frontmatter. Never write memory content directly into a \`${ENTRYPOINT_NAME}\`.`,
|
| 52 |
+
'',
|
| 53 |
+
`- Both \`${ENTRYPOINT_NAME}\` indexes are loaded into your conversation context — lines after ${MAX_ENTRYPOINT_LINES} will be truncated, so keep them concise`,
|
| 54 |
+
'- Keep the name, description, and type fields in memory files up-to-date with the content',
|
| 55 |
+
'- Organize memory semantically by topic, not chronologically',
|
| 56 |
+
'- Update or remove memories that turn out to be wrong or outdated',
|
| 57 |
+
'- Do not write duplicate memories. First check if there is an existing memory you can update before writing a new one.',
|
| 58 |
+
]
|
| 59 |
+
|
| 60 |
+
const lines = [
|
| 61 |
+
'# Memory',
|
| 62 |
+
'',
|
| 63 |
+
`You have a persistent, file-based memory system with two directories: a private directory at \`${autoDir}\` and a shared team directory at \`${teamDir}\`. ${DIRS_EXIST_GUIDANCE}`,
|
| 64 |
+
'',
|
| 65 |
+
"You should build up this memory system over time so that future conversations can have a complete picture of who the user is, how they'd like to collaborate with you, what behaviors to avoid or repeat, and the context behind the work the user gives you.",
|
| 66 |
+
'',
|
| 67 |
+
'If the user explicitly asks you to remember something, save it immediately as whichever type fits best. If they ask you to forget something, find and remove the relevant entry.',
|
| 68 |
+
'',
|
| 69 |
+
'## Memory scope',
|
| 70 |
+
'',
|
| 71 |
+
'There are two scope levels:',
|
| 72 |
+
'',
|
| 73 |
+
`- private: memories that are private between you and the current user. They persist across conversations with only this specific user and are stored at the root \`${autoDir}\`.`,
|
| 74 |
+
`- team: memories that are shared with and contributed by all of the users who work within this project directory. Team memories are synced at the beginning of every session and they are stored at \`${teamDir}\`.`,
|
| 75 |
+
'',
|
| 76 |
+
...TYPES_SECTION_COMBINED,
|
| 77 |
+
...WHAT_NOT_TO_SAVE_SECTION,
|
| 78 |
+
'- You MUST avoid saving sensitive data within shared team memories. For example, never save API keys or user credentials.',
|
| 79 |
+
'',
|
| 80 |
+
...howToSave,
|
| 81 |
+
'',
|
| 82 |
+
'## When to access memories',
|
| 83 |
+
'- When memories (personal or team) seem relevant, or the user references prior work with them or others in their organization.',
|
| 84 |
+
'- You MUST access memory when the user explicitly asks you to check, recall, or remember.',
|
| 85 |
+
'- If the user says to *ignore* or *not use* memory: proceed as if MEMORY.md were empty. Do not apply remembered facts, cite, compare against, or mention memory content.',
|
| 86 |
+
MEMORY_DRIFT_CAVEAT,
|
| 87 |
+
'',
|
| 88 |
+
...TRUSTING_RECALL_SECTION,
|
| 89 |
+
'',
|
| 90 |
+
'## Memory and other forms of persistence',
|
| 91 |
+
'Memory is one of several persistence mechanisms available to you as you assist the user in a given conversation. The distinction is often that memory can be recalled in future conversations and should not be used for persisting information that is only useful within the scope of the current conversation.',
|
| 92 |
+
'- When to use or update a plan instead of memory: If you are about to start a non-trivial implementation task and would like to reach alignment with the user on your approach you should use a Plan rather than saving this information to memory. Similarly, if you already have a plan within the conversation and you have changed your approach persist that change by updating the plan rather than saving a memory.',
|
| 93 |
+
'- When to use or update tasks instead of memory: When you need to break your work in current conversation into discrete steps or keep track of your progress use tasks instead of saving to memory. Tasks are great for persisting information about the work that needs to be done in the current conversation, but memory should be reserved for information that will be useful in future conversations.',
|
| 94 |
+
...(extraGuidelines ?? []),
|
| 95 |
+
'',
|
| 96 |
+
...buildSearchingPastContextSection(autoDir),
|
| 97 |
+
]
|
| 98 |
+
|
| 99 |
+
return lines.join('\n')
|
| 100 |
+
}
|
src/migrations/migrateBypassPermissionsAcceptedToSettings.ts
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { logEvent } from 'src/services/analytics/index.js'
|
| 2 |
+
import { getGlobalConfig, saveGlobalConfig } from '../utils/config.js'
|
| 3 |
+
import { logError } from '../utils/log.js'
|
| 4 |
+
import {
|
| 5 |
+
hasSkipDangerousModePermissionPrompt,
|
| 6 |
+
updateSettingsForSource,
|
| 7 |
+
} from '../utils/settings/settings.js'
|
| 8 |
+
|
| 9 |
+
/**
|
| 10 |
+
* Migration: Move bypassPermissionsModeAccepted from global config to settings.json
|
| 11 |
+
* as skipDangerousModePermissionPrompt. This is a better home since settings.json
|
| 12 |
+
* is the user-configurable settings file.
|
| 13 |
+
*/
|
| 14 |
+
export function migrateBypassPermissionsAcceptedToSettings(): void {
|
| 15 |
+
const globalConfig = getGlobalConfig()
|
| 16 |
+
|
| 17 |
+
if (!globalConfig.bypassPermissionsModeAccepted) {
|
| 18 |
+
return
|
| 19 |
+
}
|
| 20 |
+
|
| 21 |
+
try {
|
| 22 |
+
if (!hasSkipDangerousModePermissionPrompt()) {
|
| 23 |
+
updateSettingsForSource('userSettings', {
|
| 24 |
+
skipDangerousModePermissionPrompt: true,
|
| 25 |
+
})
|
| 26 |
+
}
|
| 27 |
+
|
| 28 |
+
logEvent('tengu_migrate_bypass_permissions_accepted', {})
|
| 29 |
+
|
| 30 |
+
saveGlobalConfig(current => {
|
| 31 |
+
if (!('bypassPermissionsModeAccepted' in current)) return current
|
| 32 |
+
const { bypassPermissionsModeAccepted: _, ...updatedConfig } = current
|
| 33 |
+
return updatedConfig
|
| 34 |
+
})
|
| 35 |
+
} catch (error) {
|
| 36 |
+
logError(
|
| 37 |
+
new Error(`Failed to migrate bypass permissions accepted: ${error}`),
|
| 38 |
+
)
|
| 39 |
+
}
|
| 40 |
+
}
|
src/migrations/migrateEnableAllProjectMcpServersToSettings.ts
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { logEvent } from 'src/services/analytics/index.js'
|
| 2 |
+
import {
|
| 3 |
+
getCurrentProjectConfig,
|
| 4 |
+
saveCurrentProjectConfig,
|
| 5 |
+
} from '../utils/config.js'
|
| 6 |
+
import { logError } from '../utils/log.js'
|
| 7 |
+
import {
|
| 8 |
+
getSettingsForSource,
|
| 9 |
+
updateSettingsForSource,
|
| 10 |
+
} from '../utils/settings/settings.js'
|
| 11 |
+
|
| 12 |
+
/**
|
| 13 |
+
* Migration: Move MCP server approval fields from project config to local settings
|
| 14 |
+
* This migrates both enableAllProjectMcpServers and enabledMcpjsonServers to the
|
| 15 |
+
* settings system for better management and consistency.
|
| 16 |
+
*/
|
| 17 |
+
export function migrateEnableAllProjectMcpServersToSettings(): void {
|
| 18 |
+
const projectConfig = getCurrentProjectConfig()
|
| 19 |
+
|
| 20 |
+
// Check if any field exists in project config
|
| 21 |
+
const hasEnableAll = projectConfig.enableAllProjectMcpServers !== undefined
|
| 22 |
+
const hasEnabledServers =
|
| 23 |
+
projectConfig.enabledMcpjsonServers &&
|
| 24 |
+
projectConfig.enabledMcpjsonServers.length > 0
|
| 25 |
+
const hasDisabledServers =
|
| 26 |
+
projectConfig.disabledMcpjsonServers &&
|
| 27 |
+
projectConfig.disabledMcpjsonServers.length > 0
|
| 28 |
+
|
| 29 |
+
if (!hasEnableAll && !hasEnabledServers && !hasDisabledServers) {
|
| 30 |
+
return
|
| 31 |
+
}
|
| 32 |
+
|
| 33 |
+
try {
|
| 34 |
+
const existingSettings = getSettingsForSource('localSettings') || {}
|
| 35 |
+
const updates: Partial<{
|
| 36 |
+
enableAllProjectMcpServers: boolean
|
| 37 |
+
enabledMcpjsonServers: string[]
|
| 38 |
+
disabledMcpjsonServers: string[]
|
| 39 |
+
}> = {}
|
| 40 |
+
const fieldsToRemove: Array<
|
| 41 |
+
| 'enableAllProjectMcpServers'
|
| 42 |
+
| 'enabledMcpjsonServers'
|
| 43 |
+
| 'disabledMcpjsonServers'
|
| 44 |
+
> = []
|
| 45 |
+
|
| 46 |
+
// Migrate enableAllProjectMcpServers if it exists and hasn't been migrated
|
| 47 |
+
if (
|
| 48 |
+
hasEnableAll &&
|
| 49 |
+
existingSettings.enableAllProjectMcpServers === undefined
|
| 50 |
+
) {
|
| 51 |
+
updates.enableAllProjectMcpServers =
|
| 52 |
+
projectConfig.enableAllProjectMcpServers
|
| 53 |
+
fieldsToRemove.push('enableAllProjectMcpServers')
|
| 54 |
+
} else if (hasEnableAll) {
|
| 55 |
+
// Already migrated, just mark for removal
|
| 56 |
+
fieldsToRemove.push('enableAllProjectMcpServers')
|
| 57 |
+
}
|
| 58 |
+
|
| 59 |
+
// Migrate enabledMcpjsonServers if it exists
|
| 60 |
+
if (hasEnabledServers && projectConfig.enabledMcpjsonServers) {
|
| 61 |
+
const existingEnabledServers =
|
| 62 |
+
existingSettings.enabledMcpjsonServers || []
|
| 63 |
+
// Merge the servers (avoiding duplicates)
|
| 64 |
+
updates.enabledMcpjsonServers = [
|
| 65 |
+
...new Set([
|
| 66 |
+
...existingEnabledServers,
|
| 67 |
+
...projectConfig.enabledMcpjsonServers,
|
| 68 |
+
]),
|
| 69 |
+
]
|
| 70 |
+
fieldsToRemove.push('enabledMcpjsonServers')
|
| 71 |
+
}
|
| 72 |
+
|
| 73 |
+
// Migrate disabledMcpjsonServers if it exists
|
| 74 |
+
if (hasDisabledServers && projectConfig.disabledMcpjsonServers) {
|
| 75 |
+
const existingDisabledServers =
|
| 76 |
+
existingSettings.disabledMcpjsonServers || []
|
| 77 |
+
// Merge the servers (avoiding duplicates)
|
| 78 |
+
updates.disabledMcpjsonServers = [
|
| 79 |
+
...new Set([
|
| 80 |
+
...existingDisabledServers,
|
| 81 |
+
...projectConfig.disabledMcpjsonServers,
|
| 82 |
+
]),
|
| 83 |
+
]
|
| 84 |
+
fieldsToRemove.push('disabledMcpjsonServers')
|
| 85 |
+
}
|
| 86 |
+
|
| 87 |
+
// Update settings if there are any updates
|
| 88 |
+
if (Object.keys(updates).length > 0) {
|
| 89 |
+
updateSettingsForSource('localSettings', updates)
|
| 90 |
+
}
|
| 91 |
+
|
| 92 |
+
// Remove migrated fields from project config
|
| 93 |
+
if (
|
| 94 |
+
fieldsToRemove.includes('enableAllProjectMcpServers') ||
|
| 95 |
+
fieldsToRemove.includes('enabledMcpjsonServers') ||
|
| 96 |
+
fieldsToRemove.includes('disabledMcpjsonServers')
|
| 97 |
+
) {
|
| 98 |
+
saveCurrentProjectConfig(current => {
|
| 99 |
+
const {
|
| 100 |
+
enableAllProjectMcpServers: _enableAll,
|
| 101 |
+
enabledMcpjsonServers: _enabledServers,
|
| 102 |
+
disabledMcpjsonServers: _disabledServers,
|
| 103 |
+
...configWithoutFields
|
| 104 |
+
} = current
|
| 105 |
+
return configWithoutFields
|
| 106 |
+
})
|
| 107 |
+
}
|
| 108 |
+
|
| 109 |
+
// Log the migration event
|
| 110 |
+
logEvent('tengu_migrate_mcp_approval_fields_success', {
|
| 111 |
+
migratedCount: fieldsToRemove.length,
|
| 112 |
+
})
|
| 113 |
+
} catch (e: unknown) {
|
| 114 |
+
// Log migration failure but don't throw to avoid breaking startup
|
| 115 |
+
logError(e)
|
| 116 |
+
logEvent('tengu_migrate_mcp_approval_fields_error', {})
|
| 117 |
+
}
|
| 118 |
+
}
|
src/migrations/migrateFennecToOpus.ts
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import {
|
| 2 |
+
getSettingsForSource,
|
| 3 |
+
updateSettingsForSource,
|
| 4 |
+
} from '../utils/settings/settings.js'
|
| 5 |
+
|
| 6 |
+
/**
|
| 7 |
+
* Migrate users on removed fennec model aliases to their new Opus 4.6 aliases.
|
| 8 |
+
* - fennec-latest → opus
|
| 9 |
+
* - fennec-latest[1m] → opus[1m]
|
| 10 |
+
* - fennec-fast-latest → opus[1m] + fast mode
|
| 11 |
+
* - opus-4-5-fast → opus + fast mode
|
| 12 |
+
*
|
| 13 |
+
* Only touches userSettings. Reading and writing the same source keeps this
|
| 14 |
+
* idempotent without a completion flag. Fennec aliases in project/local/policy
|
| 15 |
+
* settings are left alone — we can't rewrite those, and reading merged
|
| 16 |
+
* settings here would cause infinite re-runs + silent global promotion.
|
| 17 |
+
*/
|
| 18 |
+
export function migrateFennecToOpus(): void {
|
| 19 |
+
if (process.env.USER_TYPE !== 'ant') {
|
| 20 |
+
return
|
| 21 |
+
}
|
| 22 |
+
|
| 23 |
+
const settings = getSettingsForSource('userSettings')
|
| 24 |
+
|
| 25 |
+
const model = settings?.model
|
| 26 |
+
if (typeof model === 'string') {
|
| 27 |
+
if (model.startsWith('fennec-latest[1m]')) {
|
| 28 |
+
updateSettingsForSource('userSettings', {
|
| 29 |
+
model: 'opus[1m]',
|
| 30 |
+
})
|
| 31 |
+
} else if (model.startsWith('fennec-latest')) {
|
| 32 |
+
updateSettingsForSource('userSettings', {
|
| 33 |
+
model: 'opus',
|
| 34 |
+
})
|
| 35 |
+
} else if (
|
| 36 |
+
model.startsWith('fennec-fast-latest') ||
|
| 37 |
+
model.startsWith('opus-4-5-fast')
|
| 38 |
+
) {
|
| 39 |
+
updateSettingsForSource('userSettings', {
|
| 40 |
+
model: 'opus[1m]',
|
| 41 |
+
fastMode: true,
|
| 42 |
+
})
|
| 43 |
+
}
|
| 44 |
+
}
|
| 45 |
+
}
|
src/migrations/migrateLegacyOpusToCurrent.ts
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import {
|
| 2 |
+
type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
| 3 |
+
logEvent,
|
| 4 |
+
} from '../services/analytics/index.js'
|
| 5 |
+
import { saveGlobalConfig } from '../utils/config.js'
|
| 6 |
+
import { isLegacyModelRemapEnabled } from '../utils/model/model.js'
|
| 7 |
+
import { getAPIProvider } from '../utils/model/providers.js'
|
| 8 |
+
import {
|
| 9 |
+
getSettingsForSource,
|
| 10 |
+
updateSettingsForSource,
|
| 11 |
+
} from '../utils/settings/settings.js'
|
| 12 |
+
|
| 13 |
+
/**
|
| 14 |
+
* Migrate first-party users off explicit Opus 4.0/4.1 model strings.
|
| 15 |
+
*
|
| 16 |
+
* The 'opus' alias already resolves to Opus 4.6 for 1P, so anyone still
|
| 17 |
+
* on an explicit 4.0/4.1 string pinned it in settings before 4.5 launched.
|
| 18 |
+
* parseUserSpecifiedModel now silently remaps these at runtime anyway —
|
| 19 |
+
* this migration cleans up the settings file so /model shows the right
|
| 20 |
+
* thing, and sets a timestamp so the REPL can show a one-time notification.
|
| 21 |
+
*
|
| 22 |
+
* Only touches userSettings. Legacy strings in project/local/policy settings
|
| 23 |
+
* are left alone (we can't/shouldn't rewrite those) and are still remapped at
|
| 24 |
+
* runtime by parseUserSpecifiedModel. Reading and writing the same source
|
| 25 |
+
* keeps this idempotent without a completion flag, and avoids silently
|
| 26 |
+
* promoting 'opus' to the global default for users who only pinned it in one
|
| 27 |
+
* project.
|
| 28 |
+
*/
|
| 29 |
+
export function migrateLegacyOpusToCurrent(): void {
|
| 30 |
+
if (getAPIProvider() !== 'firstParty') {
|
| 31 |
+
return
|
| 32 |
+
}
|
| 33 |
+
|
| 34 |
+
if (!isLegacyModelRemapEnabled()) {
|
| 35 |
+
return
|
| 36 |
+
}
|
| 37 |
+
|
| 38 |
+
const model = getSettingsForSource('userSettings')?.model
|
| 39 |
+
if (
|
| 40 |
+
model !== 'claude-opus-4-20250514' &&
|
| 41 |
+
model !== 'claude-opus-4-1-20250805' &&
|
| 42 |
+
model !== 'claude-opus-4-0' &&
|
| 43 |
+
model !== 'claude-opus-4-1'
|
| 44 |
+
) {
|
| 45 |
+
return
|
| 46 |
+
}
|
| 47 |
+
|
| 48 |
+
updateSettingsForSource('userSettings', { model: 'opus' })
|
| 49 |
+
saveGlobalConfig(current => ({
|
| 50 |
+
...current,
|
| 51 |
+
legacyOpusMigrationTimestamp: Date.now(),
|
| 52 |
+
}))
|
| 53 |
+
logEvent('tengu_legacy_opus_migration', {
|
| 54 |
+
from_model:
|
| 55 |
+
model as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
| 56 |
+
})
|
| 57 |
+
}
|
src/migrations/migrateOpusToOpus1m.ts
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { logEvent } from '../services/analytics/index.js'
|
| 2 |
+
import {
|
| 3 |
+
getDefaultMainLoopModelSetting,
|
| 4 |
+
isOpus1mMergeEnabled,
|
| 5 |
+
parseUserSpecifiedModel,
|
| 6 |
+
} from '../utils/model/model.js'
|
| 7 |
+
import {
|
| 8 |
+
getSettingsForSource,
|
| 9 |
+
updateSettingsForSource,
|
| 10 |
+
} from '../utils/settings/settings.js'
|
| 11 |
+
|
| 12 |
+
/**
|
| 13 |
+
* Migrate users with 'opus' pinned in their settings to 'opus[1m]' when they
|
| 14 |
+
* are eligible for the merged Opus 1M experience (Max/Team Premium on 1P).
|
| 15 |
+
*
|
| 16 |
+
* CLI invocations with --model opus are unaffected: that flag is a runtime
|
| 17 |
+
* override and does not touch userSettings, so it continues to use plain Opus.
|
| 18 |
+
*
|
| 19 |
+
* Pro subscribers are skipped — they retain separate Opus and Opus 1M options.
|
| 20 |
+
* 3P users are skipped — their model strings are full model IDs, not aliases.
|
| 21 |
+
*
|
| 22 |
+
* Idempotent: only writes if userSettings.model is exactly 'opus'.
|
| 23 |
+
*/
|
| 24 |
+
export function migrateOpusToOpus1m(): void {
|
| 25 |
+
if (!isOpus1mMergeEnabled()) {
|
| 26 |
+
return
|
| 27 |
+
}
|
| 28 |
+
|
| 29 |
+
const model = getSettingsForSource('userSettings')?.model
|
| 30 |
+
if (model !== 'opus') {
|
| 31 |
+
return
|
| 32 |
+
}
|
| 33 |
+
|
| 34 |
+
const migrated = 'opus[1m]'
|
| 35 |
+
const modelToSet =
|
| 36 |
+
parseUserSpecifiedModel(migrated) ===
|
| 37 |
+
parseUserSpecifiedModel(getDefaultMainLoopModelSetting())
|
| 38 |
+
? undefined
|
| 39 |
+
: migrated
|
| 40 |
+
updateSettingsForSource('userSettings', { model: modelToSet })
|
| 41 |
+
|
| 42 |
+
logEvent('tengu_opus_to_opus1m_migration', {})
|
| 43 |
+
}
|
src/migrations/migrateReplBridgeEnabledToRemoteControlAtStartup.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { saveGlobalConfig } from '../utils/config.js'
|
| 2 |
+
|
| 3 |
+
/**
|
| 4 |
+
* Migrate the `replBridgeEnabled` config key to `remoteControlAtStartup`.
|
| 5 |
+
*
|
| 6 |
+
* The old key was an implementation detail that leaked into user-facing config.
|
| 7 |
+
* This migration copies the value to the new key and removes the old one.
|
| 8 |
+
* Idempotent — only acts when the old key exists and the new one doesn't.
|
| 9 |
+
*/
|
| 10 |
+
export function migrateReplBridgeEnabledToRemoteControlAtStartup(): void {
|
| 11 |
+
saveGlobalConfig(prev => {
|
| 12 |
+
// The old key is no longer in the GlobalConfig type, so access it via
|
| 13 |
+
// an untyped cast. Only migrate if the old key exists and the new key
|
| 14 |
+
// hasn't been set yet.
|
| 15 |
+
const oldValue = (prev as Record<string, unknown>)['replBridgeEnabled']
|
| 16 |
+
if (oldValue === undefined) return prev
|
| 17 |
+
if (prev.remoteControlAtStartup !== undefined) return prev
|
| 18 |
+
const next = { ...prev, remoteControlAtStartup: Boolean(oldValue) }
|
| 19 |
+
delete (next as Record<string, unknown>)['replBridgeEnabled']
|
| 20 |
+
return next
|
| 21 |
+
})
|
| 22 |
+
}
|
src/migrations/migrateSonnet1mToSonnet45.ts
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import {
|
| 2 |
+
getMainLoopModelOverride,
|
| 3 |
+
setMainLoopModelOverride,
|
| 4 |
+
} from '../bootstrap/state.js'
|
| 5 |
+
import { getGlobalConfig, saveGlobalConfig } from '../utils/config.js'
|
| 6 |
+
import {
|
| 7 |
+
getSettingsForSource,
|
| 8 |
+
updateSettingsForSource,
|
| 9 |
+
} from '../utils/settings/settings.js'
|
| 10 |
+
|
| 11 |
+
/**
|
| 12 |
+
* Migrate users who had "sonnet[1m]" saved to the explicit "sonnet-4-5-20250929[1m]".
|
| 13 |
+
*
|
| 14 |
+
* The "sonnet" alias now resolves to Sonnet 4.6, so users who previously set
|
| 15 |
+
* "sonnet[1m]" (targeting Sonnet 4.5 with 1M context) need to be pinned to the
|
| 16 |
+
* explicit version to preserve their intended model.
|
| 17 |
+
*
|
| 18 |
+
* This is needed because Sonnet 4.6 1M was offered to a different group of users than
|
| 19 |
+
* Sonnet 4.5 1M, so we needed to pin existing sonnet[1m] users to Sonnet 4.5 1M.
|
| 20 |
+
*
|
| 21 |
+
* Reads from userSettings specifically (not merged settings) so we don't
|
| 22 |
+
* promote a project-scoped "sonnet[1m]" to the global default. Runs once,
|
| 23 |
+
* tracked by a completion flag in global config.
|
| 24 |
+
*/
|
| 25 |
+
export function migrateSonnet1mToSonnet45(): void {
|
| 26 |
+
const config = getGlobalConfig()
|
| 27 |
+
if (config.sonnet1m45MigrationComplete) {
|
| 28 |
+
return
|
| 29 |
+
}
|
| 30 |
+
|
| 31 |
+
const model = getSettingsForSource('userSettings')?.model
|
| 32 |
+
if (model === 'sonnet[1m]') {
|
| 33 |
+
updateSettingsForSource('userSettings', {
|
| 34 |
+
model: 'sonnet-4-5-20250929[1m]',
|
| 35 |
+
})
|
| 36 |
+
}
|
| 37 |
+
|
| 38 |
+
// Also migrate the in-memory override if already set
|
| 39 |
+
const override = getMainLoopModelOverride()
|
| 40 |
+
if (override === 'sonnet[1m]') {
|
| 41 |
+
setMainLoopModelOverride('sonnet-4-5-20250929[1m]')
|
| 42 |
+
}
|
| 43 |
+
|
| 44 |
+
saveGlobalConfig(current => ({
|
| 45 |
+
...current,
|
| 46 |
+
sonnet1m45MigrationComplete: true,
|
| 47 |
+
}))
|
| 48 |
+
}
|
src/migrations/migrateSonnet45ToSonnet46.ts
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import {
|
| 2 |
+
type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
| 3 |
+
logEvent,
|
| 4 |
+
} from '../services/analytics/index.js'
|
| 5 |
+
import {
|
| 6 |
+
isMaxSubscriber,
|
| 7 |
+
isProSubscriber,
|
| 8 |
+
isTeamPremiumSubscriber,
|
| 9 |
+
} from '../utils/auth.js'
|
| 10 |
+
import { getGlobalConfig, saveGlobalConfig } from '../utils/config.js'
|
| 11 |
+
import { getAPIProvider } from '../utils/model/providers.js'
|
| 12 |
+
import {
|
| 13 |
+
getSettingsForSource,
|
| 14 |
+
updateSettingsForSource,
|
| 15 |
+
} from '../utils/settings/settings.js'
|
| 16 |
+
|
| 17 |
+
/**
|
| 18 |
+
* Migrate Pro/Max/Team Premium first-party users off explicit Sonnet 4.5
|
| 19 |
+
* model strings to the 'sonnet' alias (which now resolves to Sonnet 4.6).
|
| 20 |
+
*
|
| 21 |
+
* Users may have been pinned to explicit Sonnet 4.5 strings by:
|
| 22 |
+
* - The earlier migrateSonnet1mToSonnet45 migration (sonnet[1m] → explicit 4.5[1m])
|
| 23 |
+
* - Manually selecting it via /model
|
| 24 |
+
*
|
| 25 |
+
* Reads userSettings specifically (not merged) so we only migrate what /model
|
| 26 |
+
* wrote — project/local pins are left alone.
|
| 27 |
+
* Idempotent: only writes if userSettings.model matches a Sonnet 4.5 string.
|
| 28 |
+
*/
|
| 29 |
+
export function migrateSonnet45ToSonnet46(): void {
|
| 30 |
+
if (getAPIProvider() !== 'firstParty') {
|
| 31 |
+
return
|
| 32 |
+
}
|
| 33 |
+
|
| 34 |
+
if (!isProSubscriber() && !isMaxSubscriber() && !isTeamPremiumSubscriber()) {
|
| 35 |
+
return
|
| 36 |
+
}
|
| 37 |
+
|
| 38 |
+
const model = getSettingsForSource('userSettings')?.model
|
| 39 |
+
if (
|
| 40 |
+
model !== 'claude-sonnet-4-5-20250929' &&
|
| 41 |
+
model !== 'claude-sonnet-4-5-20250929[1m]' &&
|
| 42 |
+
model !== 'sonnet-4-5-20250929' &&
|
| 43 |
+
model !== 'sonnet-4-5-20250929[1m]'
|
| 44 |
+
) {
|
| 45 |
+
return
|
| 46 |
+
}
|
| 47 |
+
|
| 48 |
+
const has1m = model.endsWith('[1m]')
|
| 49 |
+
updateSettingsForSource('userSettings', {
|
| 50 |
+
model: has1m ? 'sonnet[1m]' : 'sonnet',
|
| 51 |
+
})
|
| 52 |
+
|
| 53 |
+
// Skip notification for brand-new users — they never experienced the old default
|
| 54 |
+
const config = getGlobalConfig()
|
| 55 |
+
if (config.numStartups > 1) {
|
| 56 |
+
saveGlobalConfig(current => ({
|
| 57 |
+
...current,
|
| 58 |
+
sonnet45To46MigrationTimestamp: Date.now(),
|
| 59 |
+
}))
|
| 60 |
+
}
|
| 61 |
+
|
| 62 |
+
logEvent('tengu_sonnet45_to_46_migration', {
|
| 63 |
+
from_model:
|
| 64 |
+
model as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
| 65 |
+
has_1m: has1m,
|
| 66 |
+
})
|
| 67 |
+
}
|
src/migrations/resetAutoModeOptInForDefaultOffer.ts
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { feature } from 'bun:bundle'
|
| 2 |
+
import { logEvent } from 'src/services/analytics/index.js'
|
| 3 |
+
import { getGlobalConfig, saveGlobalConfig } from '../utils/config.js'
|
| 4 |
+
import { logError } from '../utils/log.js'
|
| 5 |
+
import { getAutoModeEnabledState } from '../utils/permissions/permissionSetup.js'
|
| 6 |
+
import {
|
| 7 |
+
getSettingsForSource,
|
| 8 |
+
updateSettingsForSource,
|
| 9 |
+
} from '../utils/settings/settings.js'
|
| 10 |
+
|
| 11 |
+
/**
|
| 12 |
+
* One-shot migration: clear skipAutoPermissionPrompt for users who accepted
|
| 13 |
+
* the old 2-option AutoModeOptInDialog but don't have auto as their default.
|
| 14 |
+
* Re-surfaces the dialog so they see the new "make it my default mode" option.
|
| 15 |
+
* Guard lives in GlobalConfig (~/.claude.json), not settings.json, so it
|
| 16 |
+
* survives settings resets and doesn't re-arm itself.
|
| 17 |
+
*
|
| 18 |
+
* Only runs when tengu_auto_mode_config.enabled === 'enabled'. For 'opt-in'
|
| 19 |
+
* users, clearing skipAutoPermissionPrompt would remove auto from the carousel
|
| 20 |
+
* (permissionSetup.ts:988) — the dialog would become unreachable and the
|
| 21 |
+
* migration would defeat itself. In practice the ~40 target ants are all
|
| 22 |
+
* 'enabled' (they reached the old dialog via bare Shift+Tab, which requires
|
| 23 |
+
* 'enabled'), but the guard makes it safe regardless.
|
| 24 |
+
*/
|
| 25 |
+
export function resetAutoModeOptInForDefaultOffer(): void {
|
| 26 |
+
if (feature('TRANSCRIPT_CLASSIFIER')) {
|
| 27 |
+
const config = getGlobalConfig()
|
| 28 |
+
if (config.hasResetAutoModeOptInForDefaultOffer) return
|
| 29 |
+
if (getAutoModeEnabledState() !== 'enabled') return
|
| 30 |
+
|
| 31 |
+
try {
|
| 32 |
+
const user = getSettingsForSource('userSettings')
|
| 33 |
+
if (
|
| 34 |
+
user?.skipAutoPermissionPrompt &&
|
| 35 |
+
user?.permissions?.defaultMode !== 'auto'
|
| 36 |
+
) {
|
| 37 |
+
updateSettingsForSource('userSettings', {
|
| 38 |
+
skipAutoPermissionPrompt: undefined,
|
| 39 |
+
})
|
| 40 |
+
logEvent('tengu_migrate_reset_auto_opt_in_for_default_offer', {})
|
| 41 |
+
}
|
| 42 |
+
|
| 43 |
+
saveGlobalConfig(c => {
|
| 44 |
+
if (c.hasResetAutoModeOptInForDefaultOffer) return c
|
| 45 |
+
return { ...c, hasResetAutoModeOptInForDefaultOffer: true }
|
| 46 |
+
})
|
| 47 |
+
} catch (error) {
|
| 48 |
+
logError(new Error(`Failed to reset auto mode opt-in: ${error}`))
|
| 49 |
+
}
|
| 50 |
+
}
|
| 51 |
+
}
|
src/moreright/useMoreRight.tsx
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// Stub for external builds — the real hook is internal only.
|
| 2 |
+
//
|
| 3 |
+
// Self-contained: no relative imports. Typecheck sees this file at
|
| 4 |
+
// scripts/external-stubs/src/moreright/ before overlay, where ../types/
|
| 5 |
+
// would resolve to scripts/external-stubs/src/types/ (doesn't exist).
|
| 6 |
+
|
| 7 |
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
| 8 |
+
type M = any;
|
| 9 |
+
export function useMoreRight(_args: {
|
| 10 |
+
enabled: boolean;
|
| 11 |
+
setMessages: (action: M[] | ((prev: M[]) => M[])) => void;
|
| 12 |
+
inputValue: string;
|
| 13 |
+
setInputValue: (s: string) => void;
|
| 14 |
+
setToolJSX: (args: M) => void;
|
| 15 |
+
}): {
|
| 16 |
+
onBeforeQuery: (input: string, all: M[], n: number) => Promise<boolean>;
|
| 17 |
+
onTurnComplete: (all: M[], aborted: boolean) => Promise<void>;
|
| 18 |
+
render: () => null;
|
| 19 |
+
} {
|
| 20 |
+
return {
|
| 21 |
+
onBeforeQuery: async () => true,
|
| 22 |
+
onTurnComplete: async () => {},
|
| 23 |
+
render: () => null
|
| 24 |
+
};
|
| 25 |
+
}
|
| 26 |
+
//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJuYW1lcyI6WyJNIiwidXNlTW9yZVJpZ2h0IiwiX2FyZ3MiLCJlbmFibGVkIiwic2V0TWVzc2FnZXMiLCJhY3Rpb24iLCJwcmV2IiwiaW5wdXRWYWx1ZSIsInNldElucHV0VmFsdWUiLCJzIiwic2V0VG9vbEpTWCIsImFyZ3MiLCJvbkJlZm9yZVF1ZXJ5IiwiaW5wdXQiLCJhbGwiLCJuIiwiUHJvbWlzZSIsIm9uVHVybkNvbXBsZXRlIiwiYWJvcnRlZCIsInJlbmRlciJdLCJzb3VyY2VzIjpbInVzZU1vcmVSaWdodC50c3giXSwic291cmNlc0NvbnRlbnQiOlsiLy8gU3R1YiBmb3IgZXh0ZXJuYWwgYnVpbGRzIOKAlCB0aGUgcmVhbCBob29rIGlzIGludGVybmFsIG9ubHkuXG4vL1xuLy8gU2VsZi1jb250YWluZWQ6IG5vIHJlbGF0aXZlIGltcG9ydHMuIFR5cGVjaGVjayBzZWVzIHRoaXMgZmlsZSBhdFxuLy8gc2NyaXB0cy9leHRlcm5hbC1zdHVicy9zcmMvbW9yZXJpZ2h0LyBiZWZvcmUgb3ZlcmxheSwgd2hlcmUgLi4vdHlwZXMvXG4vLyB3b3VsZCByZXNvbHZlIHRvIHNjcmlwdHMvZXh0ZXJuYWwtc3R1YnMvc3JjL3R5cGVzLyAoZG9lc24ndCBleGlzdCkuXG5cbi8vIGVzbGludC1kaXNhYmxlLW5leHQtbGluZSBAdHlwZXNjcmlwdC1lc2xpbnQvbm8tZXhwbGljaXQtYW55XG50eXBlIE0gPSBhbnlcblxuZXhwb3J0IGZ1bmN0aW9uIHVzZU1vcmVSaWdodChfYXJnczoge1xuICBlbmFibGVkOiBib29sZWFuXG4gIHNldE1lc3NhZ2VzOiAoYWN0aW9uOiBNW10gfCAoKHByZXY6IE1bXSkgPT4gTVtdKSkgPT4gdm9pZFxuICBpbnB1dFZhbHVlOiBzdHJpbmdcbiAgc2V0SW5wdXRWYWx1ZTogKHM6IHN0cmluZykgPT4gdm9pZFxuICBzZXRUb29sSlNYOiAoYXJnczogTSkgPT4gdm9pZFxufSk6IHtcbiAgb25CZWZvcmVRdWVyeTogKGlucHV0OiBzdHJpbmcsIGFsbDogTVtdLCBuOiBudW1iZXIpID0+IFByb21pc2U8Ym9vbGVhbj5cbiAgb25UdXJuQ29tcGxldGU6IChhbGw6IE1bXSwgYWJvcnRlZDogYm9vbGVhbikgPT4gUHJvbWlzZTx2b2lkPlxuICByZW5kZXI6ICgpID0+IG51bGxcbn0ge1xuICByZXR1cm4ge1xuICAgIG9uQmVmb3JlUXVlcnk6IGFzeW5jICgpID0+IHRydWUsXG4gICAgb25UdXJuQ29tcGxldGU6IGFzeW5jICgpID0+IHt9LFxuICAgIHJlbmRlcjogKCkgPT4gbnVsbCxcbiAgfVxufVxuIl0sIm1hcHBpbmdzIjoiQUFBQTtBQUNBO0FBQ0E7QUFDQTtBQUNBOztBQUVBO0FBQ0EsS0FBS0EsQ0FBQyxHQUFHLEdBQUc7QUFFWixPQUFPLFNBQVNDLFlBQVlBLENBQUNDLEtBQUssRUFBRTtFQUNsQ0MsT0FBTyxFQUFFLE9BQU87RUFDaEJDLFdBQVcsRUFBRSxDQUFDQyxNQUFNLEVBQUVMLENBQUMsRUFBRSxHQUFHLENBQUMsQ0FBQ00sSUFBSSxFQUFFTixDQUFDLEVBQUUsRUFBRSxHQUFHQSxDQUFDLEVBQUUsQ0FBQyxFQUFFLEdBQUcsSUFBSTtFQUN6RE8sVUFBVSxFQUFFLE1BQU07RUFDbEJDLGFBQWEsRUFBRSxDQUFDQyxDQUFDLEVBQUUsTUFBTSxFQUFFLEdBQUcsSUFBSTtFQUNsQ0MsVUFBVSxFQUFFLENBQUNDLElBQUksRUFBRVgsQ0FBQyxFQUFFLEdBQUcsSUFBSTtBQUMvQixDQUFDLENBQUMsRUFBRTtFQUNGWSxhQUFhLEVBQUUsQ0FBQ0MsS0FBSyxFQUFFLE1BQU0sRUFBRUMsR0FBRyxFQUFFZCxDQUFDLEVBQUUsRUFBRWUsQ0FBQyxFQUFFLE1BQU0sRUFBRSxHQUFHQyxPQUFPLENBQUMsT0FBTyxDQUFDO0VBQ3ZFQyxjQUFjLEVBQUUsQ0FBQ0gsR0FBRyxFQUFFZCxDQUFDLEVBQUUsRUFBRWtCLE9BQU8sRUFBRSxPQUFPLEVBQUUsR0FBR0YsT0FBTyxDQUFDLElBQUksQ0FBQztFQUM3REcsTUFBTSxFQUFFLEdBQUcsR0FBRyxJQUFJO0FBQ3BCLENBQUMsQ0FBQztFQUNBLE9BQU87SUFDTFAsYUFBYSxFQUFFLE1BQUFBLENBQUEsS0FBWSxJQUFJO0lBQy9CSyxjQUFjLEVBQUUsTUFBQUEsQ0FBQSxLQUFZLENBQUMsQ0FBQztJQUM5QkUsTUFBTSxFQUFFQSxDQUFBLEtBQU07RUFDaEIsQ0FBQztBQUNIIiwiaWdub3JlTGlzdCI6W119
|
src/outputStyles/loadOutputStylesDir.ts
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import memoize from 'lodash-es/memoize.js'
|
| 2 |
+
import { basename } from 'path'
|
| 3 |
+
import type { OutputStyleConfig } from '../constants/outputStyles.js'
|
| 4 |
+
import { logForDebugging } from '../utils/debug.js'
|
| 5 |
+
import { coerceDescriptionToString } from '../utils/frontmatterParser.js'
|
| 6 |
+
import { logError } from '../utils/log.js'
|
| 7 |
+
import {
|
| 8 |
+
extractDescriptionFromMarkdown,
|
| 9 |
+
loadMarkdownFilesForSubdir,
|
| 10 |
+
} from '../utils/markdownConfigLoader.js'
|
| 11 |
+
import { clearPluginOutputStyleCache } from '../utils/plugins/loadPluginOutputStyles.js'
|
| 12 |
+
|
| 13 |
+
/**
|
| 14 |
+
* Loads markdown files from .claude/output-styles directories throughout the project
|
| 15 |
+
* and from ~/.claude/output-styles directory and converts them to output styles.
|
| 16 |
+
*
|
| 17 |
+
* Each filename becomes a style name, and the file content becomes the style prompt.
|
| 18 |
+
* The frontmatter provides name and description.
|
| 19 |
+
*
|
| 20 |
+
* Structure:
|
| 21 |
+
* - Project .claude/output-styles/*.md -> project styles
|
| 22 |
+
* - User ~/.claude/output-styles/*.md -> user styles (overridden by project styles)
|
| 23 |
+
*
|
| 24 |
+
* @param cwd Current working directory for project directory traversal
|
| 25 |
+
*/
|
| 26 |
+
export const getOutputStyleDirStyles = memoize(
|
| 27 |
+
async (cwd: string): Promise<OutputStyleConfig[]> => {
|
| 28 |
+
try {
|
| 29 |
+
const markdownFiles = await loadMarkdownFilesForSubdir(
|
| 30 |
+
'output-styles',
|
| 31 |
+
cwd,
|
| 32 |
+
)
|
| 33 |
+
|
| 34 |
+
const styles = markdownFiles
|
| 35 |
+
.map(({ filePath, frontmatter, content, source }) => {
|
| 36 |
+
try {
|
| 37 |
+
const fileName = basename(filePath)
|
| 38 |
+
const styleName = fileName.replace(/\.md$/, '')
|
| 39 |
+
|
| 40 |
+
// Get style configuration from frontmatter
|
| 41 |
+
const name = (frontmatter['name'] || styleName) as string
|
| 42 |
+
const description =
|
| 43 |
+
coerceDescriptionToString(
|
| 44 |
+
frontmatter['description'],
|
| 45 |
+
styleName,
|
| 46 |
+
) ??
|
| 47 |
+
extractDescriptionFromMarkdown(
|
| 48 |
+
content,
|
| 49 |
+
`Custom ${styleName} output style`,
|
| 50 |
+
)
|
| 51 |
+
|
| 52 |
+
// Parse keep-coding-instructions flag (supports both boolean and string values)
|
| 53 |
+
const keepCodingInstructionsRaw =
|
| 54 |
+
frontmatter['keep-coding-instructions']
|
| 55 |
+
const keepCodingInstructions =
|
| 56 |
+
keepCodingInstructionsRaw === true ||
|
| 57 |
+
keepCodingInstructionsRaw === 'true'
|
| 58 |
+
? true
|
| 59 |
+
: keepCodingInstructionsRaw === false ||
|
| 60 |
+
keepCodingInstructionsRaw === 'false'
|
| 61 |
+
? false
|
| 62 |
+
: undefined
|
| 63 |
+
|
| 64 |
+
// Warn if force-for-plugin is set on non-plugin output style
|
| 65 |
+
if (frontmatter['force-for-plugin'] !== undefined) {
|
| 66 |
+
logForDebugging(
|
| 67 |
+
`Output style "${name}" has force-for-plugin set, but this option only applies to plugin output styles. Ignoring.`,
|
| 68 |
+
{ level: 'warn' },
|
| 69 |
+
)
|
| 70 |
+
}
|
| 71 |
+
|
| 72 |
+
return {
|
| 73 |
+
name,
|
| 74 |
+
description,
|
| 75 |
+
prompt: content.trim(),
|
| 76 |
+
source,
|
| 77 |
+
keepCodingInstructions,
|
| 78 |
+
}
|
| 79 |
+
} catch (error) {
|
| 80 |
+
logError(error)
|
| 81 |
+
return null
|
| 82 |
+
}
|
| 83 |
+
})
|
| 84 |
+
.filter(style => style !== null)
|
| 85 |
+
|
| 86 |
+
return styles
|
| 87 |
+
} catch (error) {
|
| 88 |
+
logError(error)
|
| 89 |
+
return []
|
| 90 |
+
}
|
| 91 |
+
},
|
| 92 |
+
)
|
| 93 |
+
|
| 94 |
+
export function clearOutputStyleCaches(): void {
|
| 95 |
+
getOutputStyleDirStyles.cache?.clear?.()
|
| 96 |
+
loadMarkdownFilesForSubdir.cache?.clear?.()
|
| 97 |
+
clearPluginOutputStyleCache()
|
| 98 |
+
}
|
src/query/deps.ts
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { randomUUID } from 'crypto'
|
| 2 |
+
import { queryModelWithStreaming } from '../services/api/claude.js'
|
| 3 |
+
import { autoCompactIfNeeded } from '../services/compact/autoCompact.js'
|
| 4 |
+
import { microcompactMessages } from '../services/compact/microCompact.js'
|
| 5 |
+
|
| 6 |
+
// -- deps
|
| 7 |
+
|
| 8 |
+
// I/O dependencies for query(). Passing a `deps` override into QueryParams
|
| 9 |
+
// lets tests inject fakes directly instead of spyOn-per-module — the most
|
| 10 |
+
// common mocks (callModel, autocompact) are each spied in 6-8 test files
|
| 11 |
+
// today with module-import-and-spy boilerplate.
|
| 12 |
+
//
|
| 13 |
+
// Using `typeof fn` keeps signatures in sync with the real implementations
|
| 14 |
+
// automatically. This file imports the real functions for both typing and
|
| 15 |
+
// the production factory — tests that import this file for typing are
|
| 16 |
+
// already importing query.ts (which imports everything), so there's no
|
| 17 |
+
// new module-graph cost.
|
| 18 |
+
//
|
| 19 |
+
// Scope is intentionally narrow (4 deps) to prove the pattern. Followup
|
| 20 |
+
// PRs can add runTools, handleStopHooks, logEvent, queue ops, etc.
|
| 21 |
+
export type QueryDeps = {
|
| 22 |
+
// -- model
|
| 23 |
+
callModel: typeof queryModelWithStreaming
|
| 24 |
+
|
| 25 |
+
// -- compaction
|
| 26 |
+
microcompact: typeof microcompactMessages
|
| 27 |
+
autocompact: typeof autoCompactIfNeeded
|
| 28 |
+
|
| 29 |
+
// -- platform
|
| 30 |
+
uuid: () => string
|
| 31 |
+
}
|
| 32 |
+
|
| 33 |
+
export function productionDeps(): QueryDeps {
|
| 34 |
+
return {
|
| 35 |
+
callModel: queryModelWithStreaming,
|
| 36 |
+
microcompact: microcompactMessages,
|
| 37 |
+
autocompact: autoCompactIfNeeded,
|
| 38 |
+
uuid: randomUUID,
|
| 39 |
+
}
|
| 40 |
+
}
|
src/remote/RemoteSessionManager.ts
ADDED
|
@@ -0,0 +1,343 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import type { SDKMessage } from '../entrypoints/agentSdkTypes.js'
|
| 2 |
+
import type {
|
| 3 |
+
SDKControlCancelRequest,
|
| 4 |
+
SDKControlPermissionRequest,
|
| 5 |
+
SDKControlRequest,
|
| 6 |
+
SDKControlResponse,
|
| 7 |
+
} from '../entrypoints/sdk/controlTypes.js'
|
| 8 |
+
import { logForDebugging } from '../utils/debug.js'
|
| 9 |
+
import { logError } from '../utils/log.js'
|
| 10 |
+
import {
|
| 11 |
+
type RemoteMessageContent,
|
| 12 |
+
sendEventToRemoteSession,
|
| 13 |
+
} from '../utils/teleport/api.js'
|
| 14 |
+
import {
|
| 15 |
+
SessionsWebSocket,
|
| 16 |
+
type SessionsWebSocketCallbacks,
|
| 17 |
+
} from './SessionsWebSocket.js'
|
| 18 |
+
|
| 19 |
+
/**
|
| 20 |
+
* Type guard to check if a message is an SDKMessage (not a control message)
|
| 21 |
+
*/
|
| 22 |
+
function isSDKMessage(
|
| 23 |
+
message:
|
| 24 |
+
| SDKMessage
|
| 25 |
+
| SDKControlRequest
|
| 26 |
+
| SDKControlResponse
|
| 27 |
+
| SDKControlCancelRequest,
|
| 28 |
+
): message is SDKMessage {
|
| 29 |
+
return (
|
| 30 |
+
message.type !== 'control_request' &&
|
| 31 |
+
message.type !== 'control_response' &&
|
| 32 |
+
message.type !== 'control_cancel_request'
|
| 33 |
+
)
|
| 34 |
+
}
|
| 35 |
+
|
| 36 |
+
/**
|
| 37 |
+
* Simple permission response for remote sessions.
|
| 38 |
+
* This is a simplified version of PermissionResult for CCR communication.
|
| 39 |
+
*/
|
| 40 |
+
export type RemotePermissionResponse =
|
| 41 |
+
| {
|
| 42 |
+
behavior: 'allow'
|
| 43 |
+
updatedInput: Record<string, unknown>
|
| 44 |
+
}
|
| 45 |
+
| {
|
| 46 |
+
behavior: 'deny'
|
| 47 |
+
message: string
|
| 48 |
+
}
|
| 49 |
+
|
| 50 |
+
export type RemoteSessionConfig = {
|
| 51 |
+
sessionId: string
|
| 52 |
+
getAccessToken: () => string
|
| 53 |
+
orgUuid: string
|
| 54 |
+
/** True if session was created with an initial prompt that's being processed */
|
| 55 |
+
hasInitialPrompt?: boolean
|
| 56 |
+
/**
|
| 57 |
+
* When true, this client is a pure viewer. Ctrl+C/Escape do NOT send
|
| 58 |
+
* interrupt to the remote agent; 60s reconnect timeout is disabled;
|
| 59 |
+
* session title is never updated. Used by `claude assistant`.
|
| 60 |
+
*/
|
| 61 |
+
viewerOnly?: boolean
|
| 62 |
+
}
|
| 63 |
+
|
| 64 |
+
export type RemoteSessionCallbacks = {
|
| 65 |
+
/** Called when an SDKMessage is received from the session */
|
| 66 |
+
onMessage: (message: SDKMessage) => void
|
| 67 |
+
/** Called when a permission request is received from CCR */
|
| 68 |
+
onPermissionRequest: (
|
| 69 |
+
request: SDKControlPermissionRequest,
|
| 70 |
+
requestId: string,
|
| 71 |
+
) => void
|
| 72 |
+
/** Called when the server cancels a pending permission request */
|
| 73 |
+
onPermissionCancelled?: (
|
| 74 |
+
requestId: string,
|
| 75 |
+
toolUseId: string | undefined,
|
| 76 |
+
) => void
|
| 77 |
+
/** Called when connection is established */
|
| 78 |
+
onConnected?: () => void
|
| 79 |
+
/** Called when connection is lost and cannot be restored */
|
| 80 |
+
onDisconnected?: () => void
|
| 81 |
+
/** Called on transient WS drop while reconnect backoff is in progress */
|
| 82 |
+
onReconnecting?: () => void
|
| 83 |
+
/** Called on error */
|
| 84 |
+
onError?: (error: Error) => void
|
| 85 |
+
}
|
| 86 |
+
|
| 87 |
+
/**
|
| 88 |
+
* Manages a remote CCR session.
|
| 89 |
+
*
|
| 90 |
+
* Coordinates:
|
| 91 |
+
* - WebSocket subscription for receiving messages from CCR
|
| 92 |
+
* - HTTP POST for sending user messages to CCR
|
| 93 |
+
* - Permission request/response flow
|
| 94 |
+
*/
|
| 95 |
+
export class RemoteSessionManager {
|
| 96 |
+
private websocket: SessionsWebSocket | null = null
|
| 97 |
+
private pendingPermissionRequests: Map<string, SDKControlPermissionRequest> =
|
| 98 |
+
new Map()
|
| 99 |
+
|
| 100 |
+
constructor(
|
| 101 |
+
private readonly config: RemoteSessionConfig,
|
| 102 |
+
private readonly callbacks: RemoteSessionCallbacks,
|
| 103 |
+
) {}
|
| 104 |
+
|
| 105 |
+
/**
|
| 106 |
+
* Connect to the remote session via WebSocket
|
| 107 |
+
*/
|
| 108 |
+
connect(): void {
|
| 109 |
+
logForDebugging(
|
| 110 |
+
`[RemoteSessionManager] Connecting to session ${this.config.sessionId}`,
|
| 111 |
+
)
|
| 112 |
+
|
| 113 |
+
const wsCallbacks: SessionsWebSocketCallbacks = {
|
| 114 |
+
onMessage: message => this.handleMessage(message),
|
| 115 |
+
onConnected: () => {
|
| 116 |
+
logForDebugging('[RemoteSessionManager] Connected')
|
| 117 |
+
this.callbacks.onConnected?.()
|
| 118 |
+
},
|
| 119 |
+
onClose: () => {
|
| 120 |
+
logForDebugging('[RemoteSessionManager] Disconnected')
|
| 121 |
+
this.callbacks.onDisconnected?.()
|
| 122 |
+
},
|
| 123 |
+
onReconnecting: () => {
|
| 124 |
+
logForDebugging('[RemoteSessionManager] Reconnecting')
|
| 125 |
+
this.callbacks.onReconnecting?.()
|
| 126 |
+
},
|
| 127 |
+
onError: error => {
|
| 128 |
+
logError(error)
|
| 129 |
+
this.callbacks.onError?.(error)
|
| 130 |
+
},
|
| 131 |
+
}
|
| 132 |
+
|
| 133 |
+
this.websocket = new SessionsWebSocket(
|
| 134 |
+
this.config.sessionId,
|
| 135 |
+
this.config.orgUuid,
|
| 136 |
+
this.config.getAccessToken,
|
| 137 |
+
wsCallbacks,
|
| 138 |
+
)
|
| 139 |
+
|
| 140 |
+
void this.websocket.connect()
|
| 141 |
+
}
|
| 142 |
+
|
| 143 |
+
/**
|
| 144 |
+
* Handle messages from WebSocket
|
| 145 |
+
*/
|
| 146 |
+
private handleMessage(
|
| 147 |
+
message:
|
| 148 |
+
| SDKMessage
|
| 149 |
+
| SDKControlRequest
|
| 150 |
+
| SDKControlResponse
|
| 151 |
+
| SDKControlCancelRequest,
|
| 152 |
+
): void {
|
| 153 |
+
// Handle control requests (permission prompts from CCR)
|
| 154 |
+
if (message.type === 'control_request') {
|
| 155 |
+
this.handleControlRequest(message)
|
| 156 |
+
return
|
| 157 |
+
}
|
| 158 |
+
|
| 159 |
+
// Handle control cancel requests (server cancelling a pending permission prompt)
|
| 160 |
+
if (message.type === 'control_cancel_request') {
|
| 161 |
+
const { request_id } = message
|
| 162 |
+
const pendingRequest = this.pendingPermissionRequests.get(request_id)
|
| 163 |
+
logForDebugging(
|
| 164 |
+
`[RemoteSessionManager] Permission request cancelled: ${request_id}`,
|
| 165 |
+
)
|
| 166 |
+
this.pendingPermissionRequests.delete(request_id)
|
| 167 |
+
this.callbacks.onPermissionCancelled?.(
|
| 168 |
+
request_id,
|
| 169 |
+
pendingRequest?.tool_use_id,
|
| 170 |
+
)
|
| 171 |
+
return
|
| 172 |
+
}
|
| 173 |
+
|
| 174 |
+
// Handle control responses (acknowledgments)
|
| 175 |
+
if (message.type === 'control_response') {
|
| 176 |
+
logForDebugging('[RemoteSessionManager] Received control response')
|
| 177 |
+
return
|
| 178 |
+
}
|
| 179 |
+
|
| 180 |
+
// Forward SDK messages to callback (type guard ensures proper narrowing)
|
| 181 |
+
if (isSDKMessage(message)) {
|
| 182 |
+
this.callbacks.onMessage(message)
|
| 183 |
+
}
|
| 184 |
+
}
|
| 185 |
+
|
| 186 |
+
/**
|
| 187 |
+
* Handle control requests from CCR (e.g., permission requests)
|
| 188 |
+
*/
|
| 189 |
+
private handleControlRequest(request: SDKControlRequest): void {
|
| 190 |
+
const { request_id, request: inner } = request
|
| 191 |
+
|
| 192 |
+
if (inner.subtype === 'can_use_tool') {
|
| 193 |
+
logForDebugging(
|
| 194 |
+
`[RemoteSessionManager] Permission request for tool: ${inner.tool_name}`,
|
| 195 |
+
)
|
| 196 |
+
this.pendingPermissionRequests.set(request_id, inner)
|
| 197 |
+
this.callbacks.onPermissionRequest(inner, request_id)
|
| 198 |
+
} else {
|
| 199 |
+
// Send an error response for unrecognized subtypes so the server
|
| 200 |
+
// doesn't hang waiting for a reply that never comes.
|
| 201 |
+
logForDebugging(
|
| 202 |
+
`[RemoteSessionManager] Unsupported control request subtype: ${inner.subtype}`,
|
| 203 |
+
)
|
| 204 |
+
const response: SDKControlResponse = {
|
| 205 |
+
type: 'control_response',
|
| 206 |
+
response: {
|
| 207 |
+
subtype: 'error',
|
| 208 |
+
request_id,
|
| 209 |
+
error: `Unsupported control request subtype: ${inner.subtype}`,
|
| 210 |
+
},
|
| 211 |
+
}
|
| 212 |
+
this.websocket?.sendControlResponse(response)
|
| 213 |
+
}
|
| 214 |
+
}
|
| 215 |
+
|
| 216 |
+
/**
|
| 217 |
+
* Send a user message to the remote session via HTTP POST
|
| 218 |
+
*/
|
| 219 |
+
async sendMessage(
|
| 220 |
+
content: RemoteMessageContent,
|
| 221 |
+
opts?: { uuid?: string },
|
| 222 |
+
): Promise<boolean> {
|
| 223 |
+
logForDebugging(
|
| 224 |
+
`[RemoteSessionManager] Sending message to session ${this.config.sessionId}`,
|
| 225 |
+
)
|
| 226 |
+
|
| 227 |
+
const success = await sendEventToRemoteSession(
|
| 228 |
+
this.config.sessionId,
|
| 229 |
+
content,
|
| 230 |
+
opts,
|
| 231 |
+
)
|
| 232 |
+
|
| 233 |
+
if (!success) {
|
| 234 |
+
logError(
|
| 235 |
+
new Error(
|
| 236 |
+
`[RemoteSessionManager] Failed to send message to session ${this.config.sessionId}`,
|
| 237 |
+
),
|
| 238 |
+
)
|
| 239 |
+
}
|
| 240 |
+
|
| 241 |
+
return success
|
| 242 |
+
}
|
| 243 |
+
|
| 244 |
+
/**
|
| 245 |
+
* Respond to a permission request from CCR
|
| 246 |
+
*/
|
| 247 |
+
respondToPermissionRequest(
|
| 248 |
+
requestId: string,
|
| 249 |
+
result: RemotePermissionResponse,
|
| 250 |
+
): void {
|
| 251 |
+
const pendingRequest = this.pendingPermissionRequests.get(requestId)
|
| 252 |
+
if (!pendingRequest) {
|
| 253 |
+
logError(
|
| 254 |
+
new Error(
|
| 255 |
+
`[RemoteSessionManager] No pending permission request with ID: ${requestId}`,
|
| 256 |
+
),
|
| 257 |
+
)
|
| 258 |
+
return
|
| 259 |
+
}
|
| 260 |
+
|
| 261 |
+
this.pendingPermissionRequests.delete(requestId)
|
| 262 |
+
|
| 263 |
+
const response: SDKControlResponse = {
|
| 264 |
+
type: 'control_response',
|
| 265 |
+
response: {
|
| 266 |
+
subtype: 'success',
|
| 267 |
+
request_id: requestId,
|
| 268 |
+
response: {
|
| 269 |
+
behavior: result.behavior,
|
| 270 |
+
...(result.behavior === 'allow'
|
| 271 |
+
? { updatedInput: result.updatedInput }
|
| 272 |
+
: { message: result.message }),
|
| 273 |
+
},
|
| 274 |
+
},
|
| 275 |
+
}
|
| 276 |
+
|
| 277 |
+
logForDebugging(
|
| 278 |
+
`[RemoteSessionManager] Sending permission response: ${result.behavior}`,
|
| 279 |
+
)
|
| 280 |
+
|
| 281 |
+
this.websocket?.sendControlResponse(response)
|
| 282 |
+
}
|
| 283 |
+
|
| 284 |
+
/**
|
| 285 |
+
* Check if connected to the remote session
|
| 286 |
+
*/
|
| 287 |
+
isConnected(): boolean {
|
| 288 |
+
return this.websocket?.isConnected() ?? false
|
| 289 |
+
}
|
| 290 |
+
|
| 291 |
+
/**
|
| 292 |
+
* Send an interrupt signal to cancel the current request on the remote session
|
| 293 |
+
*/
|
| 294 |
+
cancelSession(): void {
|
| 295 |
+
logForDebugging('[RemoteSessionManager] Sending interrupt signal')
|
| 296 |
+
this.websocket?.sendControlRequest({ subtype: 'interrupt' })
|
| 297 |
+
}
|
| 298 |
+
|
| 299 |
+
/**
|
| 300 |
+
* Get the session ID
|
| 301 |
+
*/
|
| 302 |
+
getSessionId(): string {
|
| 303 |
+
return this.config.sessionId
|
| 304 |
+
}
|
| 305 |
+
|
| 306 |
+
/**
|
| 307 |
+
* Disconnect from the remote session
|
| 308 |
+
*/
|
| 309 |
+
disconnect(): void {
|
| 310 |
+
logForDebugging('[RemoteSessionManager] Disconnecting')
|
| 311 |
+
this.websocket?.close()
|
| 312 |
+
this.websocket = null
|
| 313 |
+
this.pendingPermissionRequests.clear()
|
| 314 |
+
}
|
| 315 |
+
|
| 316 |
+
/**
|
| 317 |
+
* Force reconnect the WebSocket.
|
| 318 |
+
* Useful when the subscription becomes stale after container shutdown.
|
| 319 |
+
*/
|
| 320 |
+
reconnect(): void {
|
| 321 |
+
logForDebugging('[RemoteSessionManager] Reconnecting WebSocket')
|
| 322 |
+
this.websocket?.reconnect()
|
| 323 |
+
}
|
| 324 |
+
}
|
| 325 |
+
|
| 326 |
+
/**
|
| 327 |
+
* Create a remote session config from OAuth tokens
|
| 328 |
+
*/
|
| 329 |
+
export function createRemoteSessionConfig(
|
| 330 |
+
sessionId: string,
|
| 331 |
+
getAccessToken: () => string,
|
| 332 |
+
orgUuid: string,
|
| 333 |
+
hasInitialPrompt = false,
|
| 334 |
+
viewerOnly = false,
|
| 335 |
+
): RemoteSessionConfig {
|
| 336 |
+
return {
|
| 337 |
+
sessionId,
|
| 338 |
+
getAccessToken,
|
| 339 |
+
orgUuid,
|
| 340 |
+
hasInitialPrompt,
|
| 341 |
+
viewerOnly,
|
| 342 |
+
}
|
| 343 |
+
}
|
src/remote/SessionsWebSocket.ts
ADDED
|
@@ -0,0 +1,404 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { randomUUID } from 'crypto'
|
| 2 |
+
import { getOauthConfig } from '../constants/oauth.js'
|
| 3 |
+
import type { SDKMessage } from '../entrypoints/agentSdkTypes.js'
|
| 4 |
+
import type {
|
| 5 |
+
SDKControlCancelRequest,
|
| 6 |
+
SDKControlRequest,
|
| 7 |
+
SDKControlRequestInner,
|
| 8 |
+
SDKControlResponse,
|
| 9 |
+
} from '../entrypoints/sdk/controlTypes.js'
|
| 10 |
+
import { logForDebugging } from '../utils/debug.js'
|
| 11 |
+
import { errorMessage } from '../utils/errors.js'
|
| 12 |
+
import { logError } from '../utils/log.js'
|
| 13 |
+
import { getWebSocketTLSOptions } from '../utils/mtls.js'
|
| 14 |
+
import { getWebSocketProxyAgent, getWebSocketProxyUrl } from '../utils/proxy.js'
|
| 15 |
+
import { jsonParse, jsonStringify } from '../utils/slowOperations.js'
|
| 16 |
+
|
| 17 |
+
const RECONNECT_DELAY_MS = 2000
|
| 18 |
+
const MAX_RECONNECT_ATTEMPTS = 5
|
| 19 |
+
const PING_INTERVAL_MS = 30000
|
| 20 |
+
|
| 21 |
+
/**
|
| 22 |
+
* Maximum retries for 4001 (session not found). During compaction the
|
| 23 |
+
* server may briefly consider the session stale; a short retry window
|
| 24 |
+
* lets the client recover without giving up permanently.
|
| 25 |
+
*/
|
| 26 |
+
const MAX_SESSION_NOT_FOUND_RETRIES = 3
|
| 27 |
+
|
| 28 |
+
/**
|
| 29 |
+
* WebSocket close codes that indicate a permanent server-side rejection.
|
| 30 |
+
* The client stops reconnecting immediately.
|
| 31 |
+
* Note: 4001 (session not found) is handled separately with limited
|
| 32 |
+
* retries since it can be transient during compaction.
|
| 33 |
+
*/
|
| 34 |
+
const PERMANENT_CLOSE_CODES = new Set([
|
| 35 |
+
4003, // unauthorized
|
| 36 |
+
])
|
| 37 |
+
|
| 38 |
+
type WebSocketState = 'connecting' | 'connected' | 'closed'
|
| 39 |
+
|
| 40 |
+
type SessionsMessage =
|
| 41 |
+
| SDKMessage
|
| 42 |
+
| SDKControlRequest
|
| 43 |
+
| SDKControlResponse
|
| 44 |
+
| SDKControlCancelRequest
|
| 45 |
+
|
| 46 |
+
function isSessionsMessage(value: unknown): value is SessionsMessage {
|
| 47 |
+
if (typeof value !== 'object' || value === null || !('type' in value)) {
|
| 48 |
+
return false
|
| 49 |
+
}
|
| 50 |
+
// Accept any message with a string `type` field. Downstream handlers
|
| 51 |
+
// (sdkMessageAdapter, RemoteSessionManager) decide what to do with
|
| 52 |
+
// unknown types. A hardcoded allowlist here would silently drop new
|
| 53 |
+
// message types the backend starts sending before the client is updated.
|
| 54 |
+
return typeof value.type === 'string'
|
| 55 |
+
}
|
| 56 |
+
|
| 57 |
+
export type SessionsWebSocketCallbacks = {
|
| 58 |
+
onMessage: (message: SessionsMessage) => void
|
| 59 |
+
onClose?: () => void
|
| 60 |
+
onError?: (error: Error) => void
|
| 61 |
+
onConnected?: () => void
|
| 62 |
+
/** Fired when a transient close is detected and a reconnect is scheduled.
|
| 63 |
+
* onClose fires only for permanent close (server ended / attempts exhausted). */
|
| 64 |
+
onReconnecting?: () => void
|
| 65 |
+
}
|
| 66 |
+
|
| 67 |
+
// Common interface between globalThis.WebSocket and ws.WebSocket
|
| 68 |
+
type WebSocketLike = {
|
| 69 |
+
close(): void
|
| 70 |
+
send(data: string): void
|
| 71 |
+
ping?(): void // Bun & ws both support this
|
| 72 |
+
}
|
| 73 |
+
|
| 74 |
+
/**
|
| 75 |
+
* WebSocket client for connecting to CCR sessions via /v1/sessions/ws/{id}/subscribe
|
| 76 |
+
*
|
| 77 |
+
* Protocol:
|
| 78 |
+
* 1. Connect to wss://api.anthropic.com/v1/sessions/ws/{sessionId}/subscribe?organization_uuid=...
|
| 79 |
+
* 2. Send auth message: { type: 'auth', credential: { type: 'oauth', token: '...' } }
|
| 80 |
+
* 3. Receive SDKMessage stream from the session
|
| 81 |
+
*/
|
| 82 |
+
export class SessionsWebSocket {
|
| 83 |
+
private ws: WebSocketLike | null = null
|
| 84 |
+
private state: WebSocketState = 'closed'
|
| 85 |
+
private reconnectAttempts = 0
|
| 86 |
+
private sessionNotFoundRetries = 0
|
| 87 |
+
private pingInterval: NodeJS.Timeout | null = null
|
| 88 |
+
private reconnectTimer: NodeJS.Timeout | null = null
|
| 89 |
+
|
| 90 |
+
constructor(
|
| 91 |
+
private readonly sessionId: string,
|
| 92 |
+
private readonly orgUuid: string,
|
| 93 |
+
private readonly getAccessToken: () => string,
|
| 94 |
+
private readonly callbacks: SessionsWebSocketCallbacks,
|
| 95 |
+
) {}
|
| 96 |
+
|
| 97 |
+
/**
|
| 98 |
+
* Connect to the sessions WebSocket endpoint
|
| 99 |
+
*/
|
| 100 |
+
async connect(): Promise<void> {
|
| 101 |
+
if (this.state === 'connecting') {
|
| 102 |
+
logForDebugging('[SessionsWebSocket] Already connecting')
|
| 103 |
+
return
|
| 104 |
+
}
|
| 105 |
+
|
| 106 |
+
this.state = 'connecting'
|
| 107 |
+
|
| 108 |
+
const baseUrl = getOauthConfig().BASE_API_URL.replace('https://', 'wss://')
|
| 109 |
+
const url = `${baseUrl}/v1/sessions/ws/${this.sessionId}/subscribe?organization_uuid=${this.orgUuid}`
|
| 110 |
+
|
| 111 |
+
logForDebugging(`[SessionsWebSocket] Connecting to ${url}`)
|
| 112 |
+
|
| 113 |
+
// Get fresh token for each connection attempt
|
| 114 |
+
const accessToken = this.getAccessToken()
|
| 115 |
+
const headers = {
|
| 116 |
+
Authorization: `Bearer ${accessToken}`,
|
| 117 |
+
'anthropic-version': '2023-06-01',
|
| 118 |
+
}
|
| 119 |
+
|
| 120 |
+
if (typeof Bun !== 'undefined') {
|
| 121 |
+
// Bun's WebSocket supports headers/proxy options but the DOM typings don't
|
| 122 |
+
// eslint-disable-next-line eslint-plugin-n/no-unsupported-features/node-builtins
|
| 123 |
+
const ws = new globalThis.WebSocket(url, {
|
| 124 |
+
headers,
|
| 125 |
+
proxy: getWebSocketProxyUrl(url),
|
| 126 |
+
tls: getWebSocketTLSOptions() || undefined,
|
| 127 |
+
} as unknown as string[])
|
| 128 |
+
this.ws = ws
|
| 129 |
+
|
| 130 |
+
ws.addEventListener('open', () => {
|
| 131 |
+
logForDebugging(
|
| 132 |
+
'[SessionsWebSocket] Connection opened, authenticated via headers',
|
| 133 |
+
)
|
| 134 |
+
this.state = 'connected'
|
| 135 |
+
this.reconnectAttempts = 0
|
| 136 |
+
this.sessionNotFoundRetries = 0
|
| 137 |
+
this.startPingInterval()
|
| 138 |
+
this.callbacks.onConnected?.()
|
| 139 |
+
})
|
| 140 |
+
|
| 141 |
+
ws.addEventListener('message', (event: MessageEvent) => {
|
| 142 |
+
const data =
|
| 143 |
+
typeof event.data === 'string' ? event.data : String(event.data)
|
| 144 |
+
this.handleMessage(data)
|
| 145 |
+
})
|
| 146 |
+
|
| 147 |
+
ws.addEventListener('error', () => {
|
| 148 |
+
const err = new Error('[SessionsWebSocket] WebSocket error')
|
| 149 |
+
logError(err)
|
| 150 |
+
this.callbacks.onError?.(err)
|
| 151 |
+
})
|
| 152 |
+
|
| 153 |
+
// eslint-disable-next-line eslint-plugin-n/no-unsupported-features/node-builtins
|
| 154 |
+
ws.addEventListener('close', (event: CloseEvent) => {
|
| 155 |
+
logForDebugging(
|
| 156 |
+
`[SessionsWebSocket] Closed: code=${event.code} reason=${event.reason}`,
|
| 157 |
+
)
|
| 158 |
+
this.handleClose(event.code)
|
| 159 |
+
})
|
| 160 |
+
|
| 161 |
+
ws.addEventListener('pong', () => {
|
| 162 |
+
logForDebugging('[SessionsWebSocket] Pong received')
|
| 163 |
+
})
|
| 164 |
+
} else {
|
| 165 |
+
const { default: WS } = await import('ws')
|
| 166 |
+
const ws = new WS(url, {
|
| 167 |
+
headers,
|
| 168 |
+
agent: getWebSocketProxyAgent(url),
|
| 169 |
+
...getWebSocketTLSOptions(),
|
| 170 |
+
})
|
| 171 |
+
this.ws = ws
|
| 172 |
+
|
| 173 |
+
ws.on('open', () => {
|
| 174 |
+
logForDebugging(
|
| 175 |
+
'[SessionsWebSocket] Connection opened, authenticated via headers',
|
| 176 |
+
)
|
| 177 |
+
// Auth is handled via headers, so we're immediately connected
|
| 178 |
+
this.state = 'connected'
|
| 179 |
+
this.reconnectAttempts = 0
|
| 180 |
+
this.sessionNotFoundRetries = 0
|
| 181 |
+
this.startPingInterval()
|
| 182 |
+
this.callbacks.onConnected?.()
|
| 183 |
+
})
|
| 184 |
+
|
| 185 |
+
ws.on('message', (data: Buffer) => {
|
| 186 |
+
this.handleMessage(data.toString())
|
| 187 |
+
})
|
| 188 |
+
|
| 189 |
+
ws.on('error', (err: Error) => {
|
| 190 |
+
logError(new Error(`[SessionsWebSocket] Error: ${err.message}`))
|
| 191 |
+
this.callbacks.onError?.(err)
|
| 192 |
+
})
|
| 193 |
+
|
| 194 |
+
ws.on('close', (code: number, reason: Buffer) => {
|
| 195 |
+
logForDebugging(
|
| 196 |
+
`[SessionsWebSocket] Closed: code=${code} reason=${reason.toString()}`,
|
| 197 |
+
)
|
| 198 |
+
this.handleClose(code)
|
| 199 |
+
})
|
| 200 |
+
|
| 201 |
+
ws.on('pong', () => {
|
| 202 |
+
logForDebugging('[SessionsWebSocket] Pong received')
|
| 203 |
+
})
|
| 204 |
+
}
|
| 205 |
+
}
|
| 206 |
+
|
| 207 |
+
/**
|
| 208 |
+
* Handle incoming WebSocket message
|
| 209 |
+
*/
|
| 210 |
+
private handleMessage(data: string): void {
|
| 211 |
+
try {
|
| 212 |
+
const message: unknown = jsonParse(data)
|
| 213 |
+
|
| 214 |
+
// Forward SDK messages to callback
|
| 215 |
+
if (isSessionsMessage(message)) {
|
| 216 |
+
this.callbacks.onMessage(message)
|
| 217 |
+
} else {
|
| 218 |
+
logForDebugging(
|
| 219 |
+
`[SessionsWebSocket] Ignoring message type: ${typeof message === 'object' && message !== null && 'type' in message ? String(message.type) : 'unknown'}`,
|
| 220 |
+
)
|
| 221 |
+
}
|
| 222 |
+
} catch (error) {
|
| 223 |
+
logError(
|
| 224 |
+
new Error(
|
| 225 |
+
`[SessionsWebSocket] Failed to parse message: ${errorMessage(error)}`,
|
| 226 |
+
),
|
| 227 |
+
)
|
| 228 |
+
}
|
| 229 |
+
}
|
| 230 |
+
|
| 231 |
+
/**
|
| 232 |
+
* Handle WebSocket close
|
| 233 |
+
*/
|
| 234 |
+
private handleClose(closeCode: number): void {
|
| 235 |
+
this.stopPingInterval()
|
| 236 |
+
|
| 237 |
+
if (this.state === 'closed') {
|
| 238 |
+
return
|
| 239 |
+
}
|
| 240 |
+
|
| 241 |
+
this.ws = null
|
| 242 |
+
|
| 243 |
+
const previousState = this.state
|
| 244 |
+
this.state = 'closed'
|
| 245 |
+
|
| 246 |
+
// Permanent codes: stop reconnecting — server has definitively ended the session
|
| 247 |
+
if (PERMANENT_CLOSE_CODES.has(closeCode)) {
|
| 248 |
+
logForDebugging(
|
| 249 |
+
`[SessionsWebSocket] Permanent close code ${closeCode}, not reconnecting`,
|
| 250 |
+
)
|
| 251 |
+
this.callbacks.onClose?.()
|
| 252 |
+
return
|
| 253 |
+
}
|
| 254 |
+
|
| 255 |
+
// 4001 (session not found) can be transient during compaction: the
|
| 256 |
+
// server may briefly consider the session stale while the CLI worker
|
| 257 |
+
// is busy with the compaction API call and not emitting events.
|
| 258 |
+
if (closeCode === 4001) {
|
| 259 |
+
this.sessionNotFoundRetries++
|
| 260 |
+
if (this.sessionNotFoundRetries > MAX_SESSION_NOT_FOUND_RETRIES) {
|
| 261 |
+
logForDebugging(
|
| 262 |
+
`[SessionsWebSocket] 4001 retry budget exhausted (${MAX_SESSION_NOT_FOUND_RETRIES}), not reconnecting`,
|
| 263 |
+
)
|
| 264 |
+
this.callbacks.onClose?.()
|
| 265 |
+
return
|
| 266 |
+
}
|
| 267 |
+
this.scheduleReconnect(
|
| 268 |
+
RECONNECT_DELAY_MS * this.sessionNotFoundRetries,
|
| 269 |
+
`4001 attempt ${this.sessionNotFoundRetries}/${MAX_SESSION_NOT_FOUND_RETRIES}`,
|
| 270 |
+
)
|
| 271 |
+
return
|
| 272 |
+
}
|
| 273 |
+
|
| 274 |
+
// Attempt reconnection if we were connected
|
| 275 |
+
if (
|
| 276 |
+
previousState === 'connected' &&
|
| 277 |
+
this.reconnectAttempts < MAX_RECONNECT_ATTEMPTS
|
| 278 |
+
) {
|
| 279 |
+
this.reconnectAttempts++
|
| 280 |
+
this.scheduleReconnect(
|
| 281 |
+
RECONNECT_DELAY_MS,
|
| 282 |
+
`attempt ${this.reconnectAttempts}/${MAX_RECONNECT_ATTEMPTS}`,
|
| 283 |
+
)
|
| 284 |
+
} else {
|
| 285 |
+
logForDebugging('[SessionsWebSocket] Not reconnecting')
|
| 286 |
+
this.callbacks.onClose?.()
|
| 287 |
+
}
|
| 288 |
+
}
|
| 289 |
+
|
| 290 |
+
private scheduleReconnect(delay: number, label: string): void {
|
| 291 |
+
this.callbacks.onReconnecting?.()
|
| 292 |
+
logForDebugging(
|
| 293 |
+
`[SessionsWebSocket] Scheduling reconnect (${label}) in ${delay}ms`,
|
| 294 |
+
)
|
| 295 |
+
this.reconnectTimer = setTimeout(() => {
|
| 296 |
+
this.reconnectTimer = null
|
| 297 |
+
void this.connect()
|
| 298 |
+
}, delay)
|
| 299 |
+
}
|
| 300 |
+
|
| 301 |
+
private startPingInterval(): void {
|
| 302 |
+
this.stopPingInterval()
|
| 303 |
+
|
| 304 |
+
this.pingInterval = setInterval(() => {
|
| 305 |
+
if (this.ws && this.state === 'connected') {
|
| 306 |
+
try {
|
| 307 |
+
this.ws.ping?.()
|
| 308 |
+
} catch {
|
| 309 |
+
// Ignore ping errors, close handler will deal with connection issues
|
| 310 |
+
}
|
| 311 |
+
}
|
| 312 |
+
}, PING_INTERVAL_MS)
|
| 313 |
+
}
|
| 314 |
+
|
| 315 |
+
/**
|
| 316 |
+
* Stop ping interval
|
| 317 |
+
*/
|
| 318 |
+
private stopPingInterval(): void {
|
| 319 |
+
if (this.pingInterval) {
|
| 320 |
+
clearInterval(this.pingInterval)
|
| 321 |
+
this.pingInterval = null
|
| 322 |
+
}
|
| 323 |
+
}
|
| 324 |
+
|
| 325 |
+
/**
|
| 326 |
+
* Send a control response back to the session
|
| 327 |
+
*/
|
| 328 |
+
sendControlResponse(response: SDKControlResponse): void {
|
| 329 |
+
if (!this.ws || this.state !== 'connected') {
|
| 330 |
+
logError(new Error('[SessionsWebSocket] Cannot send: not connected'))
|
| 331 |
+
return
|
| 332 |
+
}
|
| 333 |
+
|
| 334 |
+
logForDebugging('[SessionsWebSocket] Sending control response')
|
| 335 |
+
this.ws.send(jsonStringify(response))
|
| 336 |
+
}
|
| 337 |
+
|
| 338 |
+
/**
|
| 339 |
+
* Send a control request to the session (e.g., interrupt)
|
| 340 |
+
*/
|
| 341 |
+
sendControlRequest(request: SDKControlRequestInner): void {
|
| 342 |
+
if (!this.ws || this.state !== 'connected') {
|
| 343 |
+
logError(new Error('[SessionsWebSocket] Cannot send: not connected'))
|
| 344 |
+
return
|
| 345 |
+
}
|
| 346 |
+
|
| 347 |
+
const controlRequest: SDKControlRequest = {
|
| 348 |
+
type: 'control_request',
|
| 349 |
+
request_id: randomUUID(),
|
| 350 |
+
request,
|
| 351 |
+
}
|
| 352 |
+
|
| 353 |
+
logForDebugging(
|
| 354 |
+
`[SessionsWebSocket] Sending control request: ${request.subtype}`,
|
| 355 |
+
)
|
| 356 |
+
this.ws.send(jsonStringify(controlRequest))
|
| 357 |
+
}
|
| 358 |
+
|
| 359 |
+
/**
|
| 360 |
+
* Check if connected
|
| 361 |
+
*/
|
| 362 |
+
isConnected(): boolean {
|
| 363 |
+
return this.state === 'connected'
|
| 364 |
+
}
|
| 365 |
+
|
| 366 |
+
/**
|
| 367 |
+
* Close the WebSocket connection
|
| 368 |
+
*/
|
| 369 |
+
close(): void {
|
| 370 |
+
logForDebugging('[SessionsWebSocket] Closing connection')
|
| 371 |
+
this.state = 'closed'
|
| 372 |
+
this.stopPingInterval()
|
| 373 |
+
|
| 374 |
+
if (this.reconnectTimer) {
|
| 375 |
+
clearTimeout(this.reconnectTimer)
|
| 376 |
+
this.reconnectTimer = null
|
| 377 |
+
}
|
| 378 |
+
|
| 379 |
+
if (this.ws) {
|
| 380 |
+
// Null out event handlers to prevent race conditions during reconnect.
|
| 381 |
+
// Under Bun (native WebSocket), onX handlers are the clean way to detach.
|
| 382 |
+
// Under Node (ws package), the listeners were attached with .on() in connect(),
|
| 383 |
+
// but since we're about to close and null out this.ws, no cleanup is needed.
|
| 384 |
+
this.ws.close()
|
| 385 |
+
this.ws = null
|
| 386 |
+
}
|
| 387 |
+
}
|
| 388 |
+
|
| 389 |
+
/**
|
| 390 |
+
* Force reconnect - closes existing connection and establishes a new one.
|
| 391 |
+
* Useful when the subscription becomes stale (e.g., after container shutdown).
|
| 392 |
+
*/
|
| 393 |
+
reconnect(): void {
|
| 394 |
+
logForDebugging('[SessionsWebSocket] Force reconnecting')
|
| 395 |
+
this.reconnectAttempts = 0
|
| 396 |
+
this.sessionNotFoundRetries = 0
|
| 397 |
+
this.close()
|
| 398 |
+
// Small delay before reconnecting (stored in reconnectTimer so it can be cancelled)
|
| 399 |
+
this.reconnectTimer = setTimeout(() => {
|
| 400 |
+
this.reconnectTimer = null
|
| 401 |
+
void this.connect()
|
| 402 |
+
}, 500)
|
| 403 |
+
}
|
| 404 |
+
}
|
src/remote/sdkMessageAdapter.ts
ADDED
|
@@ -0,0 +1,302 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import type {
|
| 2 |
+
SDKAssistantMessage,
|
| 3 |
+
SDKCompactBoundaryMessage,
|
| 4 |
+
SDKMessage,
|
| 5 |
+
SDKPartialAssistantMessage,
|
| 6 |
+
SDKResultMessage,
|
| 7 |
+
SDKStatusMessage,
|
| 8 |
+
SDKSystemMessage,
|
| 9 |
+
SDKToolProgressMessage,
|
| 10 |
+
} from '../entrypoints/agentSdkTypes.js'
|
| 11 |
+
import type {
|
| 12 |
+
AssistantMessage,
|
| 13 |
+
Message,
|
| 14 |
+
StreamEvent,
|
| 15 |
+
SystemMessage,
|
| 16 |
+
} from '../types/message.js'
|
| 17 |
+
import { logForDebugging } from '../utils/debug.js'
|
| 18 |
+
import { fromSDKCompactMetadata } from '../utils/messages/mappers.js'
|
| 19 |
+
import { createUserMessage } from '../utils/messages.js'
|
| 20 |
+
|
| 21 |
+
/**
|
| 22 |
+
* Converts SDKMessage from CCR to REPL Message types.
|
| 23 |
+
*
|
| 24 |
+
* The CCR backend sends SDK-format messages via WebSocket. The REPL expects
|
| 25 |
+
* internal Message types for rendering. This adapter bridges the two.
|
| 26 |
+
*/
|
| 27 |
+
|
| 28 |
+
/**
|
| 29 |
+
* Convert an SDKAssistantMessage to an AssistantMessage
|
| 30 |
+
*/
|
| 31 |
+
function convertAssistantMessage(msg: SDKAssistantMessage): AssistantMessage {
|
| 32 |
+
return {
|
| 33 |
+
type: 'assistant',
|
| 34 |
+
message: msg.message,
|
| 35 |
+
uuid: msg.uuid,
|
| 36 |
+
requestId: undefined,
|
| 37 |
+
timestamp: new Date().toISOString(),
|
| 38 |
+
error: msg.error,
|
| 39 |
+
}
|
| 40 |
+
}
|
| 41 |
+
|
| 42 |
+
/**
|
| 43 |
+
* Convert an SDKPartialAssistantMessage (streaming) to a StreamEvent
|
| 44 |
+
*/
|
| 45 |
+
function convertStreamEvent(msg: SDKPartialAssistantMessage): StreamEvent {
|
| 46 |
+
return {
|
| 47 |
+
type: 'stream_event',
|
| 48 |
+
event: msg.event,
|
| 49 |
+
}
|
| 50 |
+
}
|
| 51 |
+
|
| 52 |
+
/**
|
| 53 |
+
* Convert an SDKResultMessage to a SystemMessage
|
| 54 |
+
*/
|
| 55 |
+
function convertResultMessage(msg: SDKResultMessage): SystemMessage {
|
| 56 |
+
const isError = msg.subtype !== 'success'
|
| 57 |
+
const content = isError
|
| 58 |
+
? msg.errors?.join(', ') || 'Unknown error'
|
| 59 |
+
: 'Session completed successfully'
|
| 60 |
+
|
| 61 |
+
return {
|
| 62 |
+
type: 'system',
|
| 63 |
+
subtype: 'informational',
|
| 64 |
+
content,
|
| 65 |
+
level: isError ? 'warning' : 'info',
|
| 66 |
+
uuid: msg.uuid,
|
| 67 |
+
timestamp: new Date().toISOString(),
|
| 68 |
+
}
|
| 69 |
+
}
|
| 70 |
+
|
| 71 |
+
/**
|
| 72 |
+
* Convert an SDKSystemMessage (init) to a SystemMessage
|
| 73 |
+
*/
|
| 74 |
+
function convertInitMessage(msg: SDKSystemMessage): SystemMessage {
|
| 75 |
+
return {
|
| 76 |
+
type: 'system',
|
| 77 |
+
subtype: 'informational',
|
| 78 |
+
content: `Remote session initialized (model: ${msg.model})`,
|
| 79 |
+
level: 'info',
|
| 80 |
+
uuid: msg.uuid,
|
| 81 |
+
timestamp: new Date().toISOString(),
|
| 82 |
+
}
|
| 83 |
+
}
|
| 84 |
+
|
| 85 |
+
/**
|
| 86 |
+
* Convert an SDKStatusMessage to a SystemMessage
|
| 87 |
+
*/
|
| 88 |
+
function convertStatusMessage(msg: SDKStatusMessage): SystemMessage | null {
|
| 89 |
+
if (!msg.status) {
|
| 90 |
+
return null
|
| 91 |
+
}
|
| 92 |
+
|
| 93 |
+
return {
|
| 94 |
+
type: 'system',
|
| 95 |
+
subtype: 'informational',
|
| 96 |
+
content:
|
| 97 |
+
msg.status === 'compacting'
|
| 98 |
+
? 'Compacting conversation…'
|
| 99 |
+
: `Status: ${msg.status}`,
|
| 100 |
+
level: 'info',
|
| 101 |
+
uuid: msg.uuid,
|
| 102 |
+
timestamp: new Date().toISOString(),
|
| 103 |
+
}
|
| 104 |
+
}
|
| 105 |
+
|
| 106 |
+
/**
|
| 107 |
+
* Convert an SDKToolProgressMessage to a SystemMessage.
|
| 108 |
+
* We use a system message instead of ProgressMessage since the Progress type
|
| 109 |
+
* is a complex union that requires tool-specific data we don't have from CCR.
|
| 110 |
+
*/
|
| 111 |
+
function convertToolProgressMessage(
|
| 112 |
+
msg: SDKToolProgressMessage,
|
| 113 |
+
): SystemMessage {
|
| 114 |
+
return {
|
| 115 |
+
type: 'system',
|
| 116 |
+
subtype: 'informational',
|
| 117 |
+
content: `Tool ${msg.tool_name} running for ${msg.elapsed_time_seconds}s…`,
|
| 118 |
+
level: 'info',
|
| 119 |
+
uuid: msg.uuid,
|
| 120 |
+
timestamp: new Date().toISOString(),
|
| 121 |
+
toolUseID: msg.tool_use_id,
|
| 122 |
+
}
|
| 123 |
+
}
|
| 124 |
+
|
| 125 |
+
/**
|
| 126 |
+
* Convert an SDKCompactBoundaryMessage to a SystemMessage
|
| 127 |
+
*/
|
| 128 |
+
function convertCompactBoundaryMessage(
|
| 129 |
+
msg: SDKCompactBoundaryMessage,
|
| 130 |
+
): SystemMessage {
|
| 131 |
+
return {
|
| 132 |
+
type: 'system',
|
| 133 |
+
subtype: 'compact_boundary',
|
| 134 |
+
content: 'Conversation compacted',
|
| 135 |
+
level: 'info',
|
| 136 |
+
uuid: msg.uuid,
|
| 137 |
+
timestamp: new Date().toISOString(),
|
| 138 |
+
compactMetadata: fromSDKCompactMetadata(msg.compact_metadata),
|
| 139 |
+
}
|
| 140 |
+
}
|
| 141 |
+
|
| 142 |
+
/**
|
| 143 |
+
* Result of converting an SDKMessage
|
| 144 |
+
*/
|
| 145 |
+
export type ConvertedMessage =
|
| 146 |
+
| { type: 'message'; message: Message }
|
| 147 |
+
| { type: 'stream_event'; event: StreamEvent }
|
| 148 |
+
| { type: 'ignored' }
|
| 149 |
+
|
| 150 |
+
type ConvertOptions = {
|
| 151 |
+
/** Convert user messages containing tool_result content blocks into UserMessages.
|
| 152 |
+
* Used by direct connect mode where tool results come from the remote server
|
| 153 |
+
* and need to be rendered locally. CCR mode ignores user messages since they
|
| 154 |
+
* are handled differently. */
|
| 155 |
+
convertToolResults?: boolean
|
| 156 |
+
/**
|
| 157 |
+
* Convert user text messages into UserMessages for display. Used when
|
| 158 |
+
* converting historical events where user-typed messages need to be shown.
|
| 159 |
+
* In live WS mode these are already added locally by the REPL so they're
|
| 160 |
+
* ignored by default.
|
| 161 |
+
*/
|
| 162 |
+
convertUserTextMessages?: boolean
|
| 163 |
+
}
|
| 164 |
+
|
| 165 |
+
/**
|
| 166 |
+
* Convert an SDKMessage to REPL message format
|
| 167 |
+
*/
|
| 168 |
+
export function convertSDKMessage(
|
| 169 |
+
msg: SDKMessage,
|
| 170 |
+
opts?: ConvertOptions,
|
| 171 |
+
): ConvertedMessage {
|
| 172 |
+
switch (msg.type) {
|
| 173 |
+
case 'assistant':
|
| 174 |
+
return { type: 'message', message: convertAssistantMessage(msg) }
|
| 175 |
+
|
| 176 |
+
case 'user': {
|
| 177 |
+
const content = msg.message?.content
|
| 178 |
+
// Tool result messages from the remote server need to be converted so
|
| 179 |
+
// they render and collapse like local tool results. Detect via content
|
| 180 |
+
// shape (tool_result blocks) — parent_tool_use_id is NOT reliable: the
|
| 181 |
+
// agent-side normalizeMessage() hardcodes it to null for top-level
|
| 182 |
+
// tool results, so it can't distinguish tool results from prompt echoes.
|
| 183 |
+
const isToolResult =
|
| 184 |
+
Array.isArray(content) && content.some(b => b.type === 'tool_result')
|
| 185 |
+
if (opts?.convertToolResults && isToolResult) {
|
| 186 |
+
return {
|
| 187 |
+
type: 'message',
|
| 188 |
+
message: createUserMessage({
|
| 189 |
+
content,
|
| 190 |
+
toolUseResult: msg.tool_use_result,
|
| 191 |
+
uuid: msg.uuid,
|
| 192 |
+
timestamp: msg.timestamp,
|
| 193 |
+
}),
|
| 194 |
+
}
|
| 195 |
+
}
|
| 196 |
+
// When converting historical events, user-typed messages need to be
|
| 197 |
+
// rendered (they weren't added locally by the REPL). Skip tool_results
|
| 198 |
+
// here — already handled above.
|
| 199 |
+
if (opts?.convertUserTextMessages && !isToolResult) {
|
| 200 |
+
if (typeof content === 'string' || Array.isArray(content)) {
|
| 201 |
+
return {
|
| 202 |
+
type: 'message',
|
| 203 |
+
message: createUserMessage({
|
| 204 |
+
content,
|
| 205 |
+
toolUseResult: msg.tool_use_result,
|
| 206 |
+
uuid: msg.uuid,
|
| 207 |
+
timestamp: msg.timestamp,
|
| 208 |
+
}),
|
| 209 |
+
}
|
| 210 |
+
}
|
| 211 |
+
}
|
| 212 |
+
// User-typed messages (string content) are already added locally by REPL.
|
| 213 |
+
// In CCR mode, all user messages are ignored (tool results handled differently).
|
| 214 |
+
return { type: 'ignored' }
|
| 215 |
+
}
|
| 216 |
+
|
| 217 |
+
case 'stream_event':
|
| 218 |
+
return { type: 'stream_event', event: convertStreamEvent(msg) }
|
| 219 |
+
|
| 220 |
+
case 'result':
|
| 221 |
+
// Only show result messages for errors. Success results are noise
|
| 222 |
+
// in multi-turn sessions (isLoading=false is sufficient signal).
|
| 223 |
+
if (msg.subtype !== 'success') {
|
| 224 |
+
return { type: 'message', message: convertResultMessage(msg) }
|
| 225 |
+
}
|
| 226 |
+
return { type: 'ignored' }
|
| 227 |
+
|
| 228 |
+
case 'system':
|
| 229 |
+
if (msg.subtype === 'init') {
|
| 230 |
+
return { type: 'message', message: convertInitMessage(msg) }
|
| 231 |
+
}
|
| 232 |
+
if (msg.subtype === 'status') {
|
| 233 |
+
const statusMsg = convertStatusMessage(msg)
|
| 234 |
+
return statusMsg
|
| 235 |
+
? { type: 'message', message: statusMsg }
|
| 236 |
+
: { type: 'ignored' }
|
| 237 |
+
}
|
| 238 |
+
if (msg.subtype === 'compact_boundary') {
|
| 239 |
+
return {
|
| 240 |
+
type: 'message',
|
| 241 |
+
message: convertCompactBoundaryMessage(msg),
|
| 242 |
+
}
|
| 243 |
+
}
|
| 244 |
+
// hook_response and other subtypes
|
| 245 |
+
logForDebugging(
|
| 246 |
+
`[sdkMessageAdapter] Ignoring system message subtype: ${msg.subtype}`,
|
| 247 |
+
)
|
| 248 |
+
return { type: 'ignored' }
|
| 249 |
+
|
| 250 |
+
case 'tool_progress':
|
| 251 |
+
return { type: 'message', message: convertToolProgressMessage(msg) }
|
| 252 |
+
|
| 253 |
+
case 'auth_status':
|
| 254 |
+
// Auth status is handled separately, not converted to a display message
|
| 255 |
+
logForDebugging('[sdkMessageAdapter] Ignoring auth_status message')
|
| 256 |
+
return { type: 'ignored' }
|
| 257 |
+
|
| 258 |
+
case 'tool_use_summary':
|
| 259 |
+
// Tool use summaries are SDK-only events, not displayed in REPL
|
| 260 |
+
logForDebugging('[sdkMessageAdapter] Ignoring tool_use_summary message')
|
| 261 |
+
return { type: 'ignored' }
|
| 262 |
+
|
| 263 |
+
case 'rate_limit_event':
|
| 264 |
+
// Rate limit events are SDK-only events, not displayed in REPL
|
| 265 |
+
logForDebugging('[sdkMessageAdapter] Ignoring rate_limit_event message')
|
| 266 |
+
return { type: 'ignored' }
|
| 267 |
+
|
| 268 |
+
default: {
|
| 269 |
+
// Gracefully ignore unknown message types. The backend may send new
|
| 270 |
+
// types before the client is updated; logging helps with debugging
|
| 271 |
+
// without crashing or losing the session.
|
| 272 |
+
logForDebugging(
|
| 273 |
+
`[sdkMessageAdapter] Unknown message type: ${(msg as { type: string }).type}`,
|
| 274 |
+
)
|
| 275 |
+
return { type: 'ignored' }
|
| 276 |
+
}
|
| 277 |
+
}
|
| 278 |
+
}
|
| 279 |
+
|
| 280 |
+
/**
|
| 281 |
+
* Check if an SDKMessage indicates the session has ended
|
| 282 |
+
*/
|
| 283 |
+
export function isSessionEndMessage(msg: SDKMessage): boolean {
|
| 284 |
+
return msg.type === 'result'
|
| 285 |
+
}
|
| 286 |
+
|
| 287 |
+
/**
|
| 288 |
+
* Check if an SDKResultMessage indicates success
|
| 289 |
+
*/
|
| 290 |
+
export function isSuccessResult(msg: SDKResultMessage): boolean {
|
| 291 |
+
return msg.subtype === 'success'
|
| 292 |
+
}
|
| 293 |
+
|
| 294 |
+
/**
|
| 295 |
+
* Extract the result text from a successful SDKResultMessage
|
| 296 |
+
*/
|
| 297 |
+
export function getResultText(msg: SDKResultMessage): string | null {
|
| 298 |
+
if (msg.subtype === 'success') {
|
| 299 |
+
return msg.result
|
| 300 |
+
}
|
| 301 |
+
return null
|
| 302 |
+
}
|