klyro 1.0.5 → 1.0.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agent/custom-agents.d.ts +3 -0
- package/dist/agent/custom-agents.js +96 -0
- package/dist/agent/orchestrator.d.ts +22 -2
- package/dist/agent/orchestrator.js +30 -4
- package/dist/agent/runtime.d.ts +5 -0
- package/dist/agent/runtime.js +164 -50
- package/dist/checkpoints/store.d.ts +9 -0
- package/dist/checkpoints/store.js +20 -0
- package/dist/cli/completion.js +2 -2
- package/dist/cli/config.d.ts +4 -4
- package/dist/cli/eval.d.ts +15 -1
- package/dist/cli/eval.js +34 -2
- package/dist/cli/hooks.d.ts +54 -5
- package/dist/cli/hooks.js +85 -6
- package/dist/cli/init.d.ts +6 -0
- package/dist/cli/init.js +60 -0
- package/dist/cli/repl.js +146 -25
- package/dist/cli/run.d.ts +7 -1
- package/dist/cli/run.js +92 -50
- package/dist/cli/slash/custom.d.ts +25 -0
- package/dist/cli/slash/custom.js +166 -0
- package/dist/cli/slash/parser.d.ts +9 -1
- package/dist/cli/slash/parser.js +31 -9
- package/dist/context/memory.js +18 -1
- package/dist/eval/harness.d.ts +21 -3
- package/dist/eval/harness.js +31 -3
- package/dist/eval/judge.d.ts +32 -0
- package/dist/eval/judge.js +63 -0
- package/dist/eval/tasks.js +134 -0
- package/dist/index.js +150 -127
- package/dist/mcp/client.d.ts +15 -0
- package/dist/mcp/client.js +42 -2
- package/dist/mcp/config.d.ts +3 -1
- package/dist/mcp/config.js +19 -1
- package/dist/mcp/registry.d.ts +13 -0
- package/dist/mcp/registry.js +41 -1
- package/dist/mcp/remote.d.ts +29 -0
- package/dist/mcp/remote.js +153 -0
- package/dist/policy/approval.d.ts +15 -1
- package/dist/policy/approval.js +8 -0
- package/dist/providers/endpoints.d.ts +43 -0
- package/dist/providers/endpoints.js +104 -0
- package/dist/providers.js +13 -10
- package/dist/tui/app.js +140 -13
- package/dist/tui/app.test.js +24 -0
- package/dist/tui/approval.js +53 -1
- package/package.json +1 -1
package/dist/tui/app.js
CHANGED
|
@@ -9,6 +9,7 @@ import Spinner from 'ink-spinner';
|
|
|
9
9
|
import { execFileSync } from 'node:child_process';
|
|
10
10
|
import { TuiApprovalBridge, ApprovalModal } from './approval.js';
|
|
11
11
|
import { parse as parseSlash, suggestCommands } from '../cli/slash/parser.js';
|
|
12
|
+
import { loadCustomCommands, listCompletableFiles } from '../cli/slash/custom.js';
|
|
12
13
|
import { tokens, g } from './tokens.js';
|
|
13
14
|
import { initialScroll, scrollReducer, resolveTopRow, maxTopFor, badgeLabel, } from './scroll-model.js';
|
|
14
15
|
import { buildIndex, itemAtRow, MeasureCache } from './measure.js';
|
|
@@ -268,8 +269,14 @@ export function App(props) {
|
|
|
268
269
|
// is an explicit index (null = end); rendering clamps it.
|
|
269
270
|
const [vimMode, setVimMode] = useState('insert');
|
|
270
271
|
const [vimCursor, setVimCursor] = useState(null);
|
|
272
|
+
// Count prefix (3h) and pending operator (d for dd) — refs, not state:
|
|
273
|
+
// they never render on their own, only through the next command.
|
|
274
|
+
const vimCountRef = useRef(0);
|
|
275
|
+
const vimPendingOp = useRef(null);
|
|
271
276
|
const setVimModeLive = useCallback((m) => {
|
|
272
277
|
setVimMode(m);
|
|
278
|
+
vimCountRef.current = 0;
|
|
279
|
+
vimPendingOp.current = null;
|
|
273
280
|
if (m === 'insert')
|
|
274
281
|
setVimCursor(null);
|
|
275
282
|
}, []);
|
|
@@ -679,9 +686,32 @@ export function App(props) {
|
|
|
679
686
|
n.delete(id);
|
|
680
687
|
else
|
|
681
688
|
n.add(id); return n; });
|
|
682
|
-
// `/` autocomplete — top 6
|
|
689
|
+
// `/` autocomplete — fuzzy top 6 over builtins + custom command files
|
|
690
|
+
// while completing a command name (no space yet)
|
|
691
|
+
const customCmds = useMemo(() => {
|
|
692
|
+
try {
|
|
693
|
+
return loadCustomCommands(props.cwd).map((c) => ({ name: c.name, hint: c.description || 'custom command' }));
|
|
694
|
+
}
|
|
695
|
+
catch {
|
|
696
|
+
return [];
|
|
697
|
+
}
|
|
698
|
+
}, [props.cwd]);
|
|
683
699
|
const slashSuggest = input.startsWith('/') && !input.slice(1).includes(' ') && !awaitingApproval
|
|
684
|
-
? suggestCommands(input, 6)
|
|
700
|
+
? suggestCommands(input, 6, customCmds)
|
|
701
|
+
: [];
|
|
702
|
+
// `@` file completion — fuzzy over the workspace file index while typing
|
|
703
|
+
// a mention token (no space yet). Directories carry trailing `/`.
|
|
704
|
+
const fileIndex = useMemo(() => {
|
|
705
|
+
try {
|
|
706
|
+
return listCompletableFiles(props.cwd);
|
|
707
|
+
}
|
|
708
|
+
catch {
|
|
709
|
+
return [];
|
|
710
|
+
}
|
|
711
|
+
}, [props.cwd]);
|
|
712
|
+
const atToken = /^@(\S*)$/.exec(input);
|
|
713
|
+
const pathSuggest = atToken && !awaitingApproval
|
|
714
|
+
? suggestCommands(`/${atToken[1] ?? ''}`, 6, fileIndex.map((f) => ({ name: f, hint: 'file' }))).map((d) => ({ ...d, name: `@${d.name}` }))
|
|
685
715
|
: [];
|
|
686
716
|
useInput((inputStr, key) => {
|
|
687
717
|
if (key.escape && queuedInputs.length > 0) {
|
|
@@ -689,13 +719,19 @@ export function App(props) {
|
|
|
689
719
|
disarmEsc();
|
|
690
720
|
return;
|
|
691
721
|
}
|
|
692
|
-
// Tab completes the top slash
|
|
722
|
+
// Tab completes the top slash or @-path suggestion
|
|
693
723
|
if ((key.tab || inputStr === '\t') && slashSuggest.length > 0) {
|
|
694
724
|
const top = slashSuggest[0];
|
|
695
725
|
setInput(`/${top.name} `);
|
|
696
726
|
setVimCursor(null);
|
|
697
727
|
return;
|
|
698
728
|
}
|
|
729
|
+
if ((key.tab || inputStr === '\t') && pathSuggest.length > 0) {
|
|
730
|
+
const top = pathSuggest[0];
|
|
731
|
+
setInput(`${top.name} `);
|
|
732
|
+
setVimCursor(null);
|
|
733
|
+
return;
|
|
734
|
+
}
|
|
699
735
|
// Scroll keys (work in any mode, including while running).
|
|
700
736
|
// scroll.md §6 flow: Keyboard → getTranscriptCommand → handle.
|
|
701
737
|
// design.md §11: PageUp/Ctrl+U half-up, PageDown/Ctrl+D half-down,
|
|
@@ -763,7 +799,8 @@ export function App(props) {
|
|
|
763
799
|
}
|
|
764
800
|
// Vim normal mode: motion/editing keys act on the input buffer instead
|
|
765
801
|
// of typing. Return/arrows/tab/ctrl fall through to the shared logic
|
|
766
|
-
// below; every other printable key is swallowed.
|
|
802
|
+
// below; every other printable key is swallowed. Supports counts
|
|
803
|
+
// (3h/2x), word motions (w/b), line ops (dd/D) — no operators/visual.
|
|
767
804
|
if (vimMode === 'normal' && !awaitingApproval) {
|
|
768
805
|
// Legacy Shift+Enter partner: an Esc that just switched modes followed
|
|
769
806
|
// by Return within 75ms still means newline, not submit.
|
|
@@ -774,57 +811,147 @@ export function App(props) {
|
|
|
774
811
|
}
|
|
775
812
|
const cur = vimCursor === null ? input.length : Math.max(0, Math.min(vimCursor, input.length));
|
|
776
813
|
const move = (d) => { setVimCursor(Math.max(0, Math.min(cur + d, input.length))); return true; };
|
|
814
|
+
const isWord = (c) => /[A-Za-z0-9_]/.test(c);
|
|
815
|
+
const wordFwd = (pos, n) => {
|
|
816
|
+
let p = pos;
|
|
817
|
+
for (let k = 0; k < n && p < input.length; k++) {
|
|
818
|
+
while (p < input.length && isWord(input[p]))
|
|
819
|
+
p++;
|
|
820
|
+
while (p < input.length && !isWord(input[p]))
|
|
821
|
+
p++;
|
|
822
|
+
}
|
|
823
|
+
return Math.min(p, input.length);
|
|
824
|
+
};
|
|
825
|
+
const wordBack = (pos, n) => {
|
|
826
|
+
let p = pos;
|
|
827
|
+
for (let k = 0; k < n && p > 0; k++) {
|
|
828
|
+
while (p > 0 && !isWord(input[p - 1]))
|
|
829
|
+
p--;
|
|
830
|
+
while (p > 0 && isWord(input[p - 1]))
|
|
831
|
+
p--;
|
|
832
|
+
}
|
|
833
|
+
return Math.max(0, p);
|
|
834
|
+
};
|
|
835
|
+
const takeCount = () => {
|
|
836
|
+
const n = vimCountRef.current <= 1 ? 1 : vimCountRef.current;
|
|
837
|
+
vimCountRef.current = 0;
|
|
838
|
+
return n;
|
|
839
|
+
};
|
|
777
840
|
if (!key.ctrl && !key.meta && !key.return && !key.tab && !key.upArrow && !key.downArrow && !key.leftArrow && !key.rightArrow) {
|
|
841
|
+
// Pending `d` operator: second `d` deletes the current line.
|
|
842
|
+
if (vimPendingOp.current === 'd') {
|
|
843
|
+
vimPendingOp.current = null;
|
|
844
|
+
if (inputStr === 'd') {
|
|
845
|
+
const start = input.lastIndexOf('\n', cur - 1) + 1;
|
|
846
|
+
const nl = input.indexOf('\n', cur);
|
|
847
|
+
const end = nl === -1 ? input.length : nl + 1;
|
|
848
|
+
setInput(input.slice(0, start) + input.slice(end));
|
|
849
|
+
setVimCursor(Math.min(start, Math.max(0, input.slice(0, start).length)));
|
|
850
|
+
setHistIdx(null);
|
|
851
|
+
vimCountRef.current = 0;
|
|
852
|
+
return;
|
|
853
|
+
}
|
|
854
|
+
// Only dd is supported — anything else cancels the operator.
|
|
855
|
+
vimCountRef.current = 0;
|
|
856
|
+
}
|
|
778
857
|
if (inputStr === 'i') {
|
|
779
858
|
setVimMode('insert');
|
|
780
859
|
setVimCursor(null);
|
|
860
|
+
vimCountRef.current = 0;
|
|
781
861
|
return;
|
|
782
862
|
}
|
|
783
863
|
if (inputStr === 'a') {
|
|
784
864
|
setVimCursor(Math.min(cur + 1, input.length));
|
|
785
865
|
setVimMode('insert');
|
|
866
|
+
vimCountRef.current = 0;
|
|
867
|
+
return;
|
|
868
|
+
}
|
|
869
|
+
if (/^[1-9]$/.test(inputStr)) {
|
|
870
|
+
vimCountRef.current = vimCountRef.current * 10 + Number(inputStr);
|
|
786
871
|
return;
|
|
787
872
|
}
|
|
788
873
|
if (inputStr === 'h') {
|
|
789
|
-
|
|
874
|
+
const n = takeCount();
|
|
875
|
+
move(-n);
|
|
790
876
|
return;
|
|
791
877
|
}
|
|
792
878
|
if (inputStr === 'l') {
|
|
793
|
-
|
|
879
|
+
const n = takeCount();
|
|
880
|
+
move(n);
|
|
794
881
|
return;
|
|
795
882
|
}
|
|
796
|
-
if (inputStr === '0') {
|
|
883
|
+
if (inputStr === '0' && vimCountRef.current === 0) {
|
|
797
884
|
setVimCursor(0);
|
|
798
885
|
return;
|
|
799
886
|
}
|
|
887
|
+
if (inputStr === '0') {
|
|
888
|
+
const n = takeCount();
|
|
889
|
+
move(-n);
|
|
890
|
+
return;
|
|
891
|
+
}
|
|
800
892
|
if (inputStr === '$') {
|
|
893
|
+
takeCount();
|
|
801
894
|
setVimCursor(input.length);
|
|
802
895
|
return;
|
|
803
896
|
}
|
|
804
897
|
if (inputStr === 'x') {
|
|
898
|
+
const n = takeCount();
|
|
805
899
|
if (cur < input.length)
|
|
806
|
-
setInput(input.slice(0, cur) + input.slice(cur +
|
|
900
|
+
setInput(input.slice(0, cur) + input.slice(cur + n));
|
|
901
|
+
setHistIdx(null);
|
|
902
|
+
return;
|
|
903
|
+
}
|
|
904
|
+
if (inputStr === 'w') {
|
|
905
|
+
const n = takeCount();
|
|
906
|
+
setVimCursor(wordFwd(cur, n));
|
|
907
|
+
return;
|
|
908
|
+
}
|
|
909
|
+
if (inputStr === 'b') {
|
|
910
|
+
const n = takeCount();
|
|
911
|
+
setVimCursor(wordBack(cur, n));
|
|
912
|
+
return;
|
|
913
|
+
}
|
|
914
|
+
if (inputStr === 'd') {
|
|
915
|
+
vimPendingOp.current = 'd';
|
|
916
|
+
vimCountRef.current = 0;
|
|
917
|
+
return;
|
|
918
|
+
}
|
|
919
|
+
if (inputStr === 'D') {
|
|
920
|
+
// Delete to end of current line (keep the newline).
|
|
921
|
+
const nl = input.indexOf('\n', cur);
|
|
922
|
+
const end = nl === -1 ? input.length : nl;
|
|
923
|
+
setInput(input.slice(0, cur) + input.slice(end));
|
|
807
924
|
setHistIdx(null);
|
|
925
|
+
vimCountRef.current = 0;
|
|
808
926
|
return;
|
|
809
927
|
}
|
|
810
928
|
if (inputStr === 'j') {
|
|
811
929
|
if (isFullscreen && maxTop > 0)
|
|
812
930
|
commands.lineDown();
|
|
931
|
+
vimCountRef.current = 0;
|
|
813
932
|
return;
|
|
814
933
|
}
|
|
815
934
|
if (inputStr === 'k') {
|
|
816
935
|
if (isFullscreen && maxTop > 0)
|
|
817
936
|
commands.lineUp();
|
|
937
|
+
vimCountRef.current = 0;
|
|
818
938
|
return;
|
|
819
939
|
}
|
|
820
940
|
if (key.backspace || key.delete) {
|
|
821
941
|
move(-1);
|
|
942
|
+
vimCountRef.current = 0;
|
|
822
943
|
return;
|
|
823
944
|
}
|
|
824
|
-
if (key.escape)
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
return;
|
|
945
|
+
if (key.escape) {
|
|
946
|
+
vimCountRef.current = 0;
|
|
947
|
+
vimPendingOp.current = null;
|
|
948
|
+
return;
|
|
949
|
+
} // already normal
|
|
950
|
+
if (inputStr) {
|
|
951
|
+
vimCountRef.current = 0;
|
|
952
|
+
vimPendingOp.current = null;
|
|
953
|
+
return;
|
|
954
|
+
} // swallow everything else printable
|
|
828
955
|
}
|
|
829
956
|
else if (key.backspace || key.delete) {
|
|
830
957
|
move(-1);
|
|
@@ -1066,5 +1193,5 @@ export function App(props) {
|
|
|
1066
1193
|
if (it.kind === 'diff')
|
|
1067
1194
|
return (_jsxs(Box, { flexDirection: "column", paddingLeft: 2, marginBottom: 1, children: [_jsx(Text, { bold: true, color: tokens.colors.soft, children: it.summary ?? 'Diff' }), it.hunks.map((h, i) => (_jsxs(Box, { flexDirection: "column", marginTop: 0, children: [_jsx(Text, { color: tokens.colors.soft, children: h.path }), h.lines.map((l, j) => (_jsxs(Text, { wrap: "wrap", color: l.kind === 'add' ? tokens.colors.ok : l.kind === 'remove' ? tokens.colors.err : tokens.colors.dim, children: [l.kind === 'add' ? '+ ' : l.kind === 'remove' ? '- ' : ' ', redact(l.text)] }, j)))] }, i)))] }, it.id));
|
|
1068
1195
|
return null;
|
|
1069
|
-
}) : null, showThinking && status.status === 'running' && !streamingIdRef.current ? (_jsxs(Box, { paddingLeft: 2, marginBottom: 1, children: [_jsxs(Text, { color: tokens.colors.guide, children: [" ", g('guide'), " "] }), _jsxs(Text, { color: tokens.colors.accent, children: [_jsx(Spinner, { type: "dots" }), " "] }), _jsx(Text, { color: tokens.colors.dim, children: "Thinking... (esc \u00D72 to cancel)" }), _jsxs(Text, { color: tokens.colors.dim, children: [" ", (elapsed / 1000).toFixed(1), "s"] })] })) : null, showPlan && plan.length > 0 ? (_jsxs(Box, { flexDirection: "column", paddingLeft: 2, marginTop: 0, marginBottom: 1, children: [_jsxs(Box, { children: [_jsxs(Text, { color: tokens.colors.guide, children: [" ", g('guide'), " "] }), _jsxs(Text, { bold: true, children: [g('todoPlan'), " Plan ", plan.filter((p) => p.status === 'done').length, "/", plan.length] })] }), plan.slice(0, 8).map((p, i) => (_jsxs(Box, { children: [_jsxs(Text, { color: tokens.colors.guide, children: [" ", g('guide'), " "] }), _jsxs(Text, { color: p.status === 'done' ? tokens.colors.ok : p.status === 'in_progress' ? tokens.colors.accent : tokens.colors.dim, children: [p.status === 'done' ? g('todoDone') : p.status === 'in_progress' ? g('todoActive') : g('todoPending'), " ", i + 1, ". ", redact(p.title)] })] }, p.id)))] })) : null, showQueued && queuedInputs.length > 0 ? (_jsx(Box, { flexDirection: "column", paddingLeft: 2, marginBottom: 1, children: queuedInputs.map((q, i) => (_jsxs(Text, { color: tokens.colors.dim, children: ["queued: ", redact(q.slice(0, 60)), i === 0 ? ' esc to drop' : ''] }, i))) })) : null] }), isFullscreen && !atBottom ? (_jsx(Box, { flexDirection: "column", width: 1, marginLeft: 1, children: Array.from({ length: trackH }).map((_, i) => (_jsx(Text, { color: i === thumbPos ? tokens.colors.accent : tokens.colors.guide, children: i === thumbPos ? '●' : '│' }, i))) })) : null] }), pinned && badgeLabel(atBottom, pendingNew, status.status !== 'running') ? (_jsx(Box, { justifyContent: "center", flexShrink: 0, children: _jsxs(Text, { backgroundColor: tokens.colors.accentSoft, color: tokens.colors.accent, bold: true, children: [' ', badgeLabel(atBottom, pendingNew, status.status !== 'running'), ' '] }) })) : null, slashSuggest.length > 0 ? (_jsxs(Box, { flexDirection: "column", paddingLeft: 2, flexShrink: 0, children: [slashSuggest.map((s, i) => (_jsxs(Text, { color: i === 0 ? tokens.colors.accent : tokens.colors.dim, children: [i === 0 ? '▸' : ' ', " /", s.name, " \u2014 ", s.hint] }, s.name))), _jsx(Text, { color: tokens.colors.dim, children: " tab to complete" })] })) : null, _jsx(ApprovalModal, { bridge: bridge }), _jsxs(Box, { flexDirection: "column", flexShrink: 0, children: [_jsx(Text, { color: tokens.colors.guide, children: g('rule').repeat(Math.max(10, width - 2)) }), _jsxs(Box, { children: [_jsxs(Text, { color: tokens.colors.accent, bold: true, children: [g('prompt'), " "] }), vimMode === 'normal' ? _jsx(Text, { color: tokens.colors.warn, bold: true, children: "--NORMAL-- " }) : null, _jsx(InputWithCursor, { input: input, cursor: vimCursor })] }), _jsx(Text, { color: tokens.colors.guide, children: g('rule').repeat(Math.max(10, width - 2)) })] }), _jsxs(Box, { justifyContent: "space-between", flexShrink: 0, children: [_jsxs(Text, { color: tokens.colors.dim, children: [status.status === 'running' ? (_jsxs(Text, { color: tokens.colors.accent, children: [_jsx(Spinner, { type: "dots" }), " working \u00B7 "] })) : null, baseHints, maxTop > 0 && isFullscreen ? ' · PgUp/Dn scroll' : ''] }), _jsxs(Text, { color: tokens.colors.dim, children: [maxTop > 0 && isFullscreen ? `⇅ ${topRow}/${maxTop} · ` : '', cost > 0 ? `$${cost.toFixed(2)} · ` : '', ctxPct, "% ctx \u00B7 ", status.model, status.status === 'running' ? ' ●' : ''] })] })] }));
|
|
1196
|
+
}) : null, showThinking && status.status === 'running' && !streamingIdRef.current ? (_jsxs(Box, { paddingLeft: 2, marginBottom: 1, children: [_jsxs(Text, { color: tokens.colors.guide, children: [" ", g('guide'), " "] }), _jsxs(Text, { color: tokens.colors.accent, children: [_jsx(Spinner, { type: "dots" }), " "] }), _jsx(Text, { color: tokens.colors.dim, children: "Thinking... (esc \u00D72 to cancel)" }), _jsxs(Text, { color: tokens.colors.dim, children: [" ", (elapsed / 1000).toFixed(1), "s"] })] })) : null, showPlan && plan.length > 0 ? (_jsxs(Box, { flexDirection: "column", paddingLeft: 2, marginTop: 0, marginBottom: 1, children: [_jsxs(Box, { children: [_jsxs(Text, { color: tokens.colors.guide, children: [" ", g('guide'), " "] }), _jsxs(Text, { bold: true, children: [g('todoPlan'), " Plan ", plan.filter((p) => p.status === 'done').length, "/", plan.length] })] }), plan.slice(0, 8).map((p, i) => (_jsxs(Box, { children: [_jsxs(Text, { color: tokens.colors.guide, children: [" ", g('guide'), " "] }), _jsxs(Text, { color: p.status === 'done' ? tokens.colors.ok : p.status === 'in_progress' ? tokens.colors.accent : tokens.colors.dim, children: [p.status === 'done' ? g('todoDone') : p.status === 'in_progress' ? g('todoActive') : g('todoPending'), " ", i + 1, ". ", redact(p.title)] })] }, p.id)))] })) : null, showQueued && queuedInputs.length > 0 ? (_jsx(Box, { flexDirection: "column", paddingLeft: 2, marginBottom: 1, children: queuedInputs.map((q, i) => (_jsxs(Text, { color: tokens.colors.dim, children: ["queued: ", redact(q.slice(0, 60)), i === 0 ? ' esc to drop' : ''] }, i))) })) : null] }), isFullscreen && !atBottom ? (_jsx(Box, { flexDirection: "column", width: 1, marginLeft: 1, children: Array.from({ length: trackH }).map((_, i) => (_jsx(Text, { color: i === thumbPos ? tokens.colors.accent : tokens.colors.guide, children: i === thumbPos ? '●' : '│' }, i))) })) : null] }), pinned && badgeLabel(atBottom, pendingNew, status.status !== 'running') ? (_jsx(Box, { justifyContent: "center", flexShrink: 0, children: _jsxs(Text, { backgroundColor: tokens.colors.accentSoft, color: tokens.colors.accent, bold: true, children: [' ', badgeLabel(atBottom, pendingNew, status.status !== 'running'), ' '] }) })) : null, slashSuggest.length > 0 ? (_jsxs(Box, { flexDirection: "column", paddingLeft: 2, flexShrink: 0, children: [slashSuggest.map((s, i) => (_jsxs(Text, { color: i === 0 ? tokens.colors.accent : tokens.colors.dim, children: [i === 0 ? '▸' : ' ', " /", s.name, " \u2014 ", s.hint] }, s.name))), _jsx(Text, { color: tokens.colors.dim, children: " tab to complete" })] })) : null, slashSuggest.length === 0 && pathSuggest.length > 0 ? (_jsxs(Box, { flexDirection: "column", paddingLeft: 2, flexShrink: 0, children: [pathSuggest.map((s, i) => (_jsxs(Text, { color: i === 0 ? tokens.colors.accent : tokens.colors.dim, children: [i === 0 ? '▸' : ' ', " ", s.name] }, s.name))), _jsx(Text, { color: tokens.colors.dim, children: " tab to complete" })] })) : null, _jsx(ApprovalModal, { bridge: bridge }), _jsxs(Box, { flexDirection: "column", flexShrink: 0, children: [_jsx(Text, { color: tokens.colors.guide, children: g('rule').repeat(Math.max(10, width - 2)) }), _jsxs(Box, { children: [_jsxs(Text, { color: tokens.colors.accent, bold: true, children: [g('prompt'), " "] }), vimMode === 'normal' ? _jsx(Text, { color: tokens.colors.warn, bold: true, children: "--NORMAL-- " }) : null, _jsx(InputWithCursor, { input: input, cursor: vimCursor })] }), _jsx(Text, { color: tokens.colors.guide, children: g('rule').repeat(Math.max(10, width - 2)) })] }), _jsxs(Box, { justifyContent: "space-between", flexShrink: 0, children: [_jsxs(Text, { color: tokens.colors.dim, children: [status.status === 'running' ? (_jsxs(Text, { color: tokens.colors.accent, children: [_jsx(Spinner, { type: "dots" }), " working \u00B7 "] })) : null, baseHints, maxTop > 0 && isFullscreen ? ' · PgUp/Dn scroll' : ''] }), _jsxs(Text, { color: tokens.colors.dim, children: [maxTop > 0 && isFullscreen ? `⇅ ${topRow}/${maxTop} · ` : '', cost > 0 ? `$${cost.toFixed(2)} · ` : '', ctxPct, "% ctx \u00B7 ", status.model, status.status === 'running' ? ' ●' : ''] })] })] }));
|
|
1070
1197
|
}
|
package/dist/tui/app.test.js
CHANGED
|
@@ -540,6 +540,30 @@ describe('App', () => {
|
|
|
540
540
|
const frame = await waitForMatch(lastFrame, /REDACTED/);
|
|
541
541
|
expect(frame).not.toMatch(/hunter2-hunter2/);
|
|
542
542
|
});
|
|
543
|
+
it('vim counts, word motions, and dd edit the buffer', async () => {
|
|
544
|
+
let hooks = null;
|
|
545
|
+
const { stdin, lastFrame } = render(_jsx(App, { initialModel: "m", maxSteps: 10, cwd: "/test", onPrompt: async () => { }, onSlash: async () => { }, isFullscreen: false, onMounted: (h) => {
|
|
546
|
+
hooks = { setVimMode: h.setVimMode };
|
|
547
|
+
} }));
|
|
548
|
+
await new Promise((r) => setTimeout(r, 50));
|
|
549
|
+
stdin.write('abcdef');
|
|
550
|
+
await waitForMatch(lastFrame, /abcdef/);
|
|
551
|
+
hooks.setVimMode('normal');
|
|
552
|
+
await waitForMatch(lastFrame, /--NORMAL--/);
|
|
553
|
+
// 0 then 3x deletes three chars → 'def'
|
|
554
|
+
stdin.write('0');
|
|
555
|
+
await new Promise((r) => setTimeout(r, 30));
|
|
556
|
+
stdin.write('3');
|
|
557
|
+
await new Promise((r) => setTimeout(r, 30));
|
|
558
|
+
stdin.write('x');
|
|
559
|
+
await waitForMatch(lastFrame, /def/);
|
|
560
|
+
expect(lastFrame() ?? '').not.toMatch(/abcdef/);
|
|
561
|
+
// dd clears the single-line buffer
|
|
562
|
+
stdin.write('d');
|
|
563
|
+
await new Promise((r) => setTimeout(r, 30));
|
|
564
|
+
stdin.write('d');
|
|
565
|
+
await waitForMatch(lastFrame, /Message Klyro/);
|
|
566
|
+
});
|
|
543
567
|
it('vim normal mode swallows typing, shows mode, i returns to insert', async () => {
|
|
544
568
|
let hooks = null;
|
|
545
569
|
const { stdin, lastFrame } = render(_jsx(App, { initialModel: "m", maxSteps: 10, cwd: "/test", onPrompt: async () => { }, onSlash: async () => { }, isFullscreen: false, onMounted: (h) => {
|
package/dist/tui/approval.js
CHANGED
|
@@ -74,13 +74,65 @@ export function ApprovalModal({ bridge }) {
|
|
|
74
74
|
}, [bridge]);
|
|
75
75
|
const [explain, setExplain] = useState(false);
|
|
76
76
|
const [full, setFull] = useState(false);
|
|
77
|
+
// Inline edit mode (`e`): buffer holds the JSON input being edited.
|
|
78
|
+
// Enter submits (re-validated + re-prompted by the runtime), Esc cancels
|
|
79
|
+
// back to the choice keys. Reset whenever a new prompt arrives.
|
|
80
|
+
const [editing, setEditing] = useState(null);
|
|
81
|
+
const [editError, setEditError] = useState(null);
|
|
82
|
+
useEffect(() => {
|
|
83
|
+
setEditing(null);
|
|
84
|
+
setEditError(null);
|
|
85
|
+
}, [pending]);
|
|
77
86
|
useInput((inputStr, key) => {
|
|
78
87
|
if (!pending)
|
|
79
88
|
return;
|
|
89
|
+
if (editing !== null) {
|
|
90
|
+
if (key.return) {
|
|
91
|
+
let parsed;
|
|
92
|
+
try {
|
|
93
|
+
parsed = JSON.parse(editing);
|
|
94
|
+
}
|
|
95
|
+
catch (err) {
|
|
96
|
+
setEditError(`invalid JSON: ${err instanceof Error ? err.message : String(err)}`);
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
100
|
+
setEditError('edited input must be a JSON object');
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
bridge.resolve({ kind: 'edit', editedInput: parsed });
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
if (key.escape) {
|
|
107
|
+
setEditing(null);
|
|
108
|
+
setEditError(null);
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
if (key.backspace || key.delete) {
|
|
112
|
+
setEditing((v) => (v ?? '').slice(0, -1));
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
if (!key.ctrl && !key.meta)
|
|
116
|
+
setEditing((v) => (v ?? '') + inputStr.replace(/\r/g, '\n'));
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
80
119
|
if (key.return) {
|
|
81
120
|
bridge.resolve('deny');
|
|
82
121
|
return;
|
|
83
122
|
}
|
|
123
|
+
// `e` enters inline edit mode (real edit-and-retry); the stdin prompt
|
|
124
|
+
// keeps `e` as deny since it has no editor. Case-sensitive mapping for
|
|
125
|
+
// the rest lives in approvalChoiceForKey.
|
|
126
|
+
if (inputStr === 'e') {
|
|
127
|
+
try {
|
|
128
|
+
setEditing(JSON.stringify(pending.input ?? {}, null, 2));
|
|
129
|
+
}
|
|
130
|
+
catch {
|
|
131
|
+
setEditing('{}');
|
|
132
|
+
}
|
|
133
|
+
setEditError(null);
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
84
136
|
// Case-sensitive mapping lives in approvalChoiceForKey: 'A' persists,
|
|
85
137
|
// 'a' is session-only (the old inline toLowerCase-first code collapsed
|
|
86
138
|
// them, silently downgrading persistence).
|
|
@@ -107,5 +159,5 @@ export function ApprovalModal({ bridge }) {
|
|
|
107
159
|
const clean = sanitizeForPrompt(s);
|
|
108
160
|
return !full && clean.length > n ? clean.slice(0, n) + '…' : clean;
|
|
109
161
|
};
|
|
110
|
-
return (_jsxs(Box, { flexDirection: "column", borderStyle: "double", borderColor: "yellow", paddingX: 1, marginY: 1, children: [_jsxs(Text, { color: "yellow", bold: true, children: ["\u26A0 approval needed \u2014 ", pending.toolName] }), _jsxs(Text, { color: "gray", children: [" reason: \"", trunc(pending.reason), "\""] }), pending.summary ? _jsxs(Text, { children: [" \"", trunc(pending.summary), "\""] }) : null, explain ? _jsxs(Text, { color: "cyan", children: [" Explain: This tool will ", pending.toolName, " with the shown args. [y] once, [a] session, [A] always\u2192settings, [n] deny, [e] edit, [f] full text, [?] toggle help."] }) : null, _jsxs(Box, { marginTop: 1, children: [_jsx(Text, { color: "green", children: "[y] once" }), _jsx(Text, { color: "gray", children: " " }), _jsx(Text, { color: "green", children: "[a] session" }), _jsx(Text, { color: "gray", children: " " }), _jsx(Text, { color: "green", children: "[A] always" }), _jsx(Text, { color: "gray", children: " " }), _jsx(Text, { color: "red", children: "[n] deny" }), _jsx(Text, { color: "gray", children: " " }), _jsx(Text, { color: "yellow", children: "[e] edit" }), _jsx(Text, { color: "gray", children: " " }), _jsx(Text, { color: "cyan", children: "[f] full" }), _jsx(Text, { color: "gray", children: " " }), _jsx(Text, { color: "cyan", children: "[?] explain" }), _jsx(Text, { color: "gray", children: " (Enter = deny)" })] })] }));
|
|
162
|
+
return (_jsxs(Box, { flexDirection: "column", borderStyle: "double", borderColor: "yellow", paddingX: 1, marginY: 1, children: [_jsxs(Text, { color: "yellow", bold: true, children: ["\u26A0 approval needed \u2014 ", pending.toolName] }), _jsxs(Text, { color: "gray", children: [" reason: \"", trunc(pending.reason), "\""] }), pending.summary ? _jsxs(Text, { children: [" \"", trunc(pending.summary), "\""] }) : null, editing !== null ? (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsx(Text, { color: "yellow", bold: true, children: " editing input JSON (Enter = submit, Esc = back):" }), _jsxs(Text, { wrap: "wrap", children: [" ", (editing.length > 1500 ? editing.slice(0, 1500) + '…' : editing) + '▌'] }), editError ? _jsxs(Text, { color: "red", children: [" ", editError] }) : null] })) : null, explain ? _jsxs(Text, { color: "cyan", children: [" Explain: This tool will ", pending.toolName, " with the shown args. [y] once, [a] session, [A] always\u2192settings, [n] deny, [e] edit, [f] full text, [?] toggle help."] }) : null, _jsxs(Box, { marginTop: 1, children: [_jsx(Text, { color: "green", children: "[y] once" }), _jsx(Text, { color: "gray", children: " " }), _jsx(Text, { color: "green", children: "[a] session" }), _jsx(Text, { color: "gray", children: " " }), _jsx(Text, { color: "green", children: "[A] always" }), _jsx(Text, { color: "gray", children: " " }), _jsx(Text, { color: "red", children: "[n] deny" }), _jsx(Text, { color: "gray", children: " " }), _jsx(Text, { color: "yellow", children: "[e] edit" }), _jsx(Text, { color: "gray", children: " " }), _jsx(Text, { color: "cyan", children: "[f] full" }), _jsx(Text, { color: "gray", children: " " }), _jsx(Text, { color: "cyan", children: "[?] explain" }), _jsx(Text, { color: "gray", children: " (Enter = deny)" })] })] }));
|
|
111
163
|
}
|