@zhongqian97-code/ecode 0.2.3 → 0.2.5

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.
Files changed (2) hide show
  1. package/dist/index.js +340 -165
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -95,8 +95,8 @@ function loadConfig() {
95
95
  }
96
96
 
97
97
  // src/ui/App.tsx
98
- import { useState as useState3, useCallback, useRef as useRef2, useEffect as useEffect3 } from "react";
99
- import { Box as Box5, useInput as useInput2 } from "ink";
98
+ import { useState as useState3, useCallback, useRef as useRef2, useEffect as useEffect3, useMemo } from "react";
99
+ import { Box as Box5, useInput as useInput2, useStdout } from "ink";
100
100
 
101
101
  // src/llm.ts
102
102
  import OpenAI from "openai";
@@ -472,162 +472,123 @@ function StatusBar({
472
472
 
473
473
  // src/ui/ConversationHistory.tsx
474
474
  import { Box as Box2, Text as Text2 } from "ink";
475
- import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
475
+
476
+ // src/ui/messageLines.ts
476
477
  var TOOL_RESULT_MAX_LINES = 3;
477
- function truncateLines(text, maxLines) {
478
- const lines = text.split("\n");
479
- if (lines.length <= maxLines) return text;
480
- return lines.slice(0, maxLines).join("\n") + "\n\u2026";
481
- }
482
- function UserMessage({
483
- content
484
- }) {
485
- return (
486
- // marginBottom={0} 避免 Ink 默认添加的底部间距,使历史区域更紧凑
487
- /* @__PURE__ */ jsx2(Box2, { flexDirection: "row", marginBottom: 0, children: /* @__PURE__ */ jsxs2(Text2, { color: "white", children: [
488
- "> ",
489
- content
490
- ] }) })
491
- );
492
- }
493
- function AssistantMessage({
494
- content,
495
- tool_calls,
496
- reasoning_content,
497
- expandTools
498
- }) {
499
- return /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", marginBottom: 0, children: [
500
- reasoning_content && reasoning_content.length > 0 && (expandTools ? (
501
- // 展开:显示 <thinking> 标题和完整推理内容
502
- /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", children: [
503
- /* @__PURE__ */ jsx2(Text2, { color: "gray", children: "<thinking>" }),
504
- /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: reasoning_content })
505
- ] })
506
- ) : (
507
- // 折叠:一行简短占位符,告知用户有隐藏的思考内容
508
- /* @__PURE__ */ jsx2(Text2, { color: "gray", children: "[+ thinking]" })
509
- )),
510
- content && content.trim().length > 0 && /* @__PURE__ */ jsx2(Text2, { color: "cyan", children: content }),
511
- tool_calls && tool_calls.length > 0 && (expandTools ? (
512
- // 展开:逐条渲染每个工具调用
513
- tool_calls.map((tc, idx) => {
514
- let argsDisplay = tc.function.arguments;
515
- try {
516
- const parsed = JSON.parse(tc.function.arguments);
517
- if (typeof parsed === "object" && parsed !== null) {
518
- argsDisplay = Object.values(parsed).map(String).join(", ");
519
- }
520
- } catch {
521
- }
522
- return (
523
- // 黄色 ⚙ 图标 + 工具名 + 格式化参数
524
- /* @__PURE__ */ jsxs2(Text2, { color: "yellow", children: [
525
- "\u2699 \u8C03\u7528\u5DE5\u5177: ",
526
- tc.function.name,
527
- "(",
528
- argsDisplay,
529
- ")"
530
- ] }, idx)
531
- );
532
- })
533
- ) : (
534
- // 折叠:用计数占位符替代,避免占用多行
535
- /* @__PURE__ */ jsxs2(Text2, { color: "gray", children: [
536
- "[+ ",
537
- tool_calls.length,
538
- " \u4E2A\u5DE5\u5177\u8C03\u7528]"
539
- ] })
540
- ))
541
- ] });
542
- }
543
- function ToolMessage({
544
- content,
545
- expandTools
546
- }) {
547
- if (!expandTools) {
548
- return /* @__PURE__ */ jsx2(Box2, { flexDirection: "row", marginBottom: 0, children: /* @__PURE__ */ jsx2(Text2, { color: "gray", children: "[+ \u5DE5\u5177\u7ED3\u679C]" }) });
478
+ function wrapText(text, terminalWidth) {
479
+ const logicalLines = text.split("\n");
480
+ if (terminalWidth <= 0) return logicalLines;
481
+ const result = [];
482
+ for (const line of logicalLines) {
483
+ if (line.length === 0) {
484
+ result.push("");
485
+ continue;
486
+ }
487
+ for (let i = 0; i < line.length; i += terminalWidth) {
488
+ result.push(line.slice(i, i + terminalWidth));
489
+ }
549
490
  }
550
- const truncated = truncateLines(content, TOOL_RESULT_MAX_LINES);
551
- return /* @__PURE__ */ jsx2(Box2, { flexDirection: "row", marginBottom: 0, children: /* @__PURE__ */ jsxs2(Text2, { color: "gray", children: [
552
- "[\u5DE5\u5177\u7ED3\u679C] ",
553
- truncated
554
- ] }) });
491
+ return result;
555
492
  }
556
- function estimateLines(msg, expandTools) {
493
+ function messageToLines(msg, expandTools, terminalWidth) {
557
494
  if (msg.role === "user") {
558
- return Math.max(1, msg.content.split("\n").length);
495
+ return wrapText(`> ${msg.content}`, terminalWidth).map((text) => ({
496
+ text,
497
+ color: "white"
498
+ }));
559
499
  }
560
500
  if (msg.role === "assistant") {
561
- const contentLines = msg.content ? msg.content.split("\n").length : 0;
562
- const assistantMsg = msg;
563
- let reasoningLines = 0;
564
- if (assistantMsg.reasoning_content && assistantMsg.reasoning_content.length > 0) {
501
+ const lines = [];
502
+ const aMsg = msg;
503
+ if (aMsg.reasoning_content && aMsg.reasoning_content.length > 0) {
565
504
  if (expandTools) {
566
- reasoningLines = 1 + assistantMsg.reasoning_content.split("\n").length;
505
+ lines.push({ text: "<thinking>", color: "gray" });
506
+ for (const t of wrapText(aMsg.reasoning_content, terminalWidth)) {
507
+ lines.push({ text: t, dimColor: true });
508
+ }
567
509
  } else {
568
- reasoningLines = 1;
510
+ lines.push({ text: "[+ thinking]", color: "gray" });
511
+ }
512
+ }
513
+ if (aMsg.content && aMsg.content.trim().length > 0) {
514
+ for (const t of wrapText(aMsg.content, terminalWidth)) {
515
+ lines.push({ text: t, color: "cyan" });
569
516
  }
570
517
  }
571
- let toolLines = 0;
572
- if (assistantMsg.tool_calls && assistantMsg.tool_calls.length > 0) {
518
+ if (aMsg.tool_calls && aMsg.tool_calls.length > 0) {
573
519
  if (expandTools) {
574
- toolLines = assistantMsg.tool_calls.length;
520
+ for (const tc of aMsg.tool_calls) {
521
+ let argsDisplay = tc.function.arguments;
522
+ try {
523
+ const parsed = JSON.parse(tc.function.arguments);
524
+ if (typeof parsed === "object" && parsed !== null) {
525
+ argsDisplay = Object.values(parsed).map(String).join(", ");
526
+ }
527
+ } catch {
528
+ }
529
+ const lineText = `\u2699 \u8C03\u7528\u5DE5\u5177: ${tc.function.name}(${argsDisplay})`;
530
+ for (const t of wrapText(lineText, terminalWidth)) {
531
+ lines.push({ text: t, color: "yellow" });
532
+ }
533
+ }
575
534
  } else {
576
- toolLines = 1;
535
+ lines.push({
536
+ text: `[+ ${aMsg.tool_calls.length} \u4E2A\u5DE5\u5177\u8C03\u7528]`,
537
+ color: "gray"
538
+ });
577
539
  }
578
540
  }
579
- return Math.max(1, contentLines + reasoningLines + toolLines);
541
+ return lines.length > 0 ? lines : [{ text: "" }];
580
542
  }
581
543
  if (msg.role === "tool") {
582
544
  if (!expandTools) {
583
- return 1;
545
+ return [{ text: "[+ \u5DE5\u5177\u7ED3\u679C]", color: "gray" }];
584
546
  }
585
- return Math.min(TOOL_RESULT_MAX_LINES, msg.content.split("\n").length) + 1;
547
+ const rawLines = msg.content.split("\n");
548
+ const truncated = rawLines.length > TOOL_RESULT_MAX_LINES ? rawLines.slice(0, TOOL_RESULT_MAX_LINES - 1).join("\n") + "\n\u2026" : msg.content;
549
+ return [
550
+ { text: "[\u5DE5\u5177\u7ED3\u679C]", color: "gray" },
551
+ ...wrapText(truncated, terminalWidth).map((text) => ({
552
+ text,
553
+ color: "gray"
554
+ }))
555
+ ];
586
556
  }
587
- return 1;
557
+ return [];
588
558
  }
559
+
560
+ // src/ui/ConversationHistory.tsx
561
+ import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
589
562
  function ConversationHistory({
590
563
  messages,
591
564
  maxHeight = 20,
592
- expandTools = false
565
+ expandTools = false,
566
+ terminalWidth = 0,
567
+ scrollOffset = 0
593
568
  }) {
594
569
  const visible = messages.filter(
595
570
  (m) => m.role !== "system"
596
571
  );
597
- let totalLines = 0;
598
- let startIdx = visible.length;
599
- for (let i = visible.length - 1; i >= 0; i--) {
600
- const lines = estimateLines(visible[i], expandTools);
601
- if (totalLines + lines > maxHeight) break;
602
- totalLines += lines;
603
- startIdx = i;
604
- }
605
- const displayMessages = visible.slice(startIdx);
606
- return (
607
- // 纵向堆叠所有消息
608
- /* @__PURE__ */ jsx2(Box2, { flexDirection: "column", children: displayMessages.map((msg, idx) => {
609
- if (msg.role === "user") {
610
- return /* @__PURE__ */ jsx2(UserMessage, { content: msg.content }, idx);
611
- }
612
- if (msg.role === "assistant") {
613
- const assistantMsg = msg;
614
- return /* @__PURE__ */ jsx2(
615
- AssistantMessage,
616
- {
617
- content: assistantMsg.content,
618
- tool_calls: assistantMsg.tool_calls,
619
- reasoning_content: assistantMsg.reasoning_content,
620
- expandTools
621
- },
622
- idx
623
- );
624
- }
625
- if (msg.role === "tool") {
626
- return /* @__PURE__ */ jsx2(ToolMessage, { content: msg.content, expandTools }, idx);
627
- }
628
- return null;
629
- }) })
572
+ const allLines = visible.flatMap(
573
+ (msg) => messageToLines(msg, expandTools, terminalWidth)
630
574
  );
575
+ const totalLines = allLines.length;
576
+ const end = Math.max(0, Math.min(totalLines, totalLines - scrollOffset));
577
+ let start = Math.max(0, end - maxHeight);
578
+ let linesAbove = start;
579
+ if (linesAbove > 0 && maxHeight > 1) {
580
+ start = Math.max(0, end - (maxHeight - 1));
581
+ linesAbove = start;
582
+ }
583
+ const visibleLines = allLines.slice(start, end);
584
+ return /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", children: [
585
+ linesAbove > 0 && /* @__PURE__ */ jsxs2(Text2, { color: "gray", dimColor: true, children: [
586
+ "\u2191 ",
587
+ linesAbove,
588
+ " \u884C \xB7 PageUp/Down \u6EDA\u52A8"
589
+ ] }),
590
+ visibleLines.map((line, idx) => /* @__PURE__ */ jsx2(Text2, { color: line.color, dimColor: line.dimColor, children: line.text }, idx))
591
+ ] });
631
592
  }
632
593
 
633
594
  // src/ui/Input.tsx
@@ -636,19 +597,45 @@ import { Box as Box3, Text as Text3, useInput } from "ink";
636
597
  import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
637
598
  var CURSOR_CHAR = "\u258C";
638
599
  var BLINK_INTERVAL_MS = 530;
600
+ function wordBackward(s, pos) {
601
+ let i = pos;
602
+ while (i > 0 && s[i - 1] === " ") {
603
+ i--;
604
+ }
605
+ while (i > 0 && s[i - 1] !== " ") {
606
+ i--;
607
+ }
608
+ return i;
609
+ }
610
+ function wordForward(s, pos) {
611
+ let i = pos;
612
+ const len = s.length;
613
+ while (i < len && s[i] !== " ") {
614
+ i++;
615
+ }
616
+ while (i < len && s[i] === " ") {
617
+ i++;
618
+ }
619
+ return i;
620
+ }
639
621
  var Input = forwardRef(function Input2({ isActive, onSubmit, onChange, placeholder }, ref) {
640
- const [lines, setLines] = useState2([""]);
622
+ const [value, setValue] = useState2("");
623
+ const [cursorPos, setCursorPos] = useState2(0);
641
624
  const [cursorVisible, setCursorVisible] = useState2(true);
642
- const linesRef = useRef(lines);
643
- linesRef.current = lines;
625
+ const valueRef = useRef(value);
626
+ valueRef.current = value;
627
+ const cursorPosRef = useRef(cursorPos);
628
+ cursorPosRef.current = cursorPos;
644
629
  const onChangeRef = useRef(onChange);
645
630
  onChangeRef.current = onChange;
646
631
  const onSubmitRef = useRef(onSubmit);
647
632
  onSubmitRef.current = onSubmit;
648
633
  useImperativeHandle(ref, () => ({
649
634
  fill(text) {
650
- const newLines = text ? text.split("\n") : [""];
651
- setLines(newLines);
635
+ valueRef.current = text;
636
+ cursorPosRef.current = text.length;
637
+ setValue(text);
638
+ setCursorPos(text.length);
652
639
  onChangeRef.current?.(text);
653
640
  }
654
641
  }));
@@ -664,49 +651,124 @@ var Input = forwardRef(function Input2({ isActive, onSubmit, onChange, placehold
664
651
  clearInterval(timer);
665
652
  };
666
653
  }, [isActive]);
654
+ function setValueSync(newValue) {
655
+ valueRef.current = newValue;
656
+ setValue(newValue);
657
+ }
658
+ function setCursorPosSync(newPos) {
659
+ cursorPosRef.current = newPos;
660
+ setCursorPos(newPos);
661
+ }
667
662
  useInput(
668
663
  (input, key) => {
669
- const currentLines = linesRef.current;
664
+ const v = valueRef.current;
665
+ const pos = cursorPosRef.current;
670
666
  if (key.return && key.shift) {
671
- const newLines = [...currentLines, ""];
672
- setLines(newLines);
673
- onChangeRef.current?.(newLines.join("\n"));
667
+ const newValue = v.slice(0, pos) + "\n" + v.slice(pos);
668
+ setValueSync(newValue);
669
+ setCursorPosSync(pos + 1);
670
+ onChangeRef.current?.(newValue);
674
671
  return;
675
672
  }
676
673
  if (key.return) {
677
- const text = currentLines.join("\n");
678
- onSubmitRef.current(text);
679
- setLines([""]);
674
+ onSubmitRef.current(v);
675
+ setValueSync("");
676
+ setCursorPosSync(0);
680
677
  onChangeRef.current?.("");
681
678
  return;
682
679
  }
683
680
  if (key.backspace || key.delete) {
684
- const lastIdx = currentLines.length - 1;
685
- const lastLine = currentLines[lastIdx];
686
- let newLines;
687
- if (lastLine.length > 0) {
688
- newLines = [...currentLines.slice(0, lastIdx), lastLine.slice(0, -1)];
689
- } else if (currentLines.length > 1) {
690
- newLines = currentLines.slice(0, -1);
691
- } else {
681
+ if (pos === 0) return;
682
+ const newValue = v.slice(0, pos - 1) + v.slice(pos);
683
+ setValueSync(newValue);
684
+ setCursorPosSync(pos - 1);
685
+ onChangeRef.current?.(newValue);
686
+ return;
687
+ }
688
+ if (key.ctrl) {
689
+ switch (input) {
690
+ case "a": {
691
+ setCursorPosSync(0);
692
+ return;
693
+ }
694
+ case "e": {
695
+ setCursorPosSync(v.length);
696
+ return;
697
+ }
698
+ case "b": {
699
+ setCursorPosSync(Math.max(0, pos - 1));
700
+ return;
701
+ }
702
+ case "f": {
703
+ setCursorPosSync(Math.min(v.length, pos + 1));
704
+ return;
705
+ }
706
+ case "k": {
707
+ const nextNl = v.indexOf("\n", pos);
708
+ const lineEnd = nextNl === -1 ? v.length : nextNl;
709
+ const newValue = v.slice(0, pos) + v.slice(lineEnd);
710
+ setValueSync(newValue);
711
+ onChangeRef.current?.(newValue);
712
+ return;
713
+ }
714
+ case "u": {
715
+ const newValue = v.slice(pos);
716
+ setValueSync(newValue);
717
+ setCursorPosSync(0);
718
+ onChangeRef.current?.(newValue);
719
+ return;
720
+ }
721
+ case "w": {
722
+ const newPos = wordBackward(v, pos);
723
+ const newValue = v.slice(0, newPos) + v.slice(pos);
724
+ setValueSync(newValue);
725
+ setCursorPosSync(newPos);
726
+ onChangeRef.current?.(newValue);
727
+ return;
728
+ }
729
+ case "d": {
730
+ if (pos >= v.length) return;
731
+ const newValue = v.slice(0, pos) + v.slice(pos + 1);
732
+ setValueSync(newValue);
733
+ onChangeRef.current?.(newValue);
734
+ return;
735
+ }
736
+ default:
737
+ return;
738
+ }
739
+ }
740
+ if (key.meta) {
741
+ if (input === "b") {
742
+ setCursorPosSync(wordBackward(v, pos));
743
+ return;
744
+ }
745
+ if (input === "f") {
746
+ setCursorPosSync(wordForward(v, pos));
692
747
  return;
693
748
  }
694
- setLines(newLines);
695
- onChangeRef.current?.(newLines.join("\n"));
696
749
  return;
697
750
  }
698
- if (key.ctrl || key.escape || key.upArrow || key.downArrow || key.leftArrow || key.rightArrow || key.tab || key.pageUp || key.pageDown) {
751
+ if (key.leftArrow) {
752
+ setCursorPosSync(Math.max(0, pos - 1));
753
+ return;
754
+ }
755
+ if (key.rightArrow) {
756
+ setCursorPosSync(Math.min(v.length, pos + 1));
757
+ return;
758
+ }
759
+ if (key.escape || key.upArrow || key.downArrow || key.tab || key.pageUp || key.pageDown) {
699
760
  return;
700
761
  }
701
762
  if (input.length > 0) {
702
- const newLines = [...currentLines.slice(0, -1), currentLines[currentLines.length - 1] + input];
703
- setLines(newLines);
704
- onChangeRef.current?.(newLines.join("\n"));
763
+ const newValue = v.slice(0, pos) + input + v.slice(pos);
764
+ setValueSync(newValue);
765
+ setCursorPosSync(pos + input.length);
766
+ onChangeRef.current?.(newValue);
705
767
  }
706
768
  },
707
769
  { isActive }
708
770
  );
709
- const isEmpty = lines.every((line) => line === "");
771
+ const isEmpty = value === "";
710
772
  const renderLines = () => {
711
773
  if (isEmpty && placeholder) {
712
774
  return /* @__PURE__ */ jsxs3(Box3, { children: [
@@ -715,19 +777,41 @@ var Input = forwardRef(function Input2({ isActive, onSubmit, onChange, placehold
715
777
  isActive && cursorVisible && /* @__PURE__ */ jsx3(Text3, { color: "cyan", children: CURSOR_CHAR })
716
778
  ] });
717
779
  }
780
+ const lines = value.split("\n");
781
+ let remaining = cursorPos;
782
+ let cursorLine = 0;
783
+ let cursorCol = 0;
784
+ for (let i = 0; i < lines.length; i++) {
785
+ const lineLen = lines[i].length;
786
+ if (remaining <= lineLen) {
787
+ cursorLine = i;
788
+ cursorCol = remaining;
789
+ break;
790
+ }
791
+ remaining -= lineLen + 1;
792
+ }
718
793
  return /* @__PURE__ */ jsx3(Box3, { flexDirection: "column", children: lines.map((line, idx) => {
719
- const isLastLine = idx === lines.length - 1;
720
794
  const prefix = idx === 0 ? "> " : " ";
795
+ const showCursor = isActive && cursorVisible && idx === cursorLine;
796
+ if (!showCursor) {
797
+ return /* @__PURE__ */ jsxs3(Box3, { children: [
798
+ /* @__PURE__ */ jsx3(Text3, { color: "cyan", children: prefix }),
799
+ /* @__PURE__ */ jsx3(Text3, { children: line })
800
+ ] }, idx);
801
+ }
802
+ const before = line.slice(0, cursorCol);
803
+ const after = line.slice(cursorCol);
721
804
  return /* @__PURE__ */ jsxs3(Box3, { children: [
722
805
  /* @__PURE__ */ jsx3(Text3, { color: "cyan", children: prefix }),
723
- /* @__PURE__ */ jsx3(Text3, { children: line }),
724
- isActive && isLastLine && cursorVisible && /* @__PURE__ */ jsx3(Text3, { color: "cyan", children: CURSOR_CHAR })
806
+ /* @__PURE__ */ jsx3(Text3, { children: before }),
807
+ /* @__PURE__ */ jsx3(Text3, { color: "cyan", children: CURSOR_CHAR }),
808
+ /* @__PURE__ */ jsx3(Text3, { children: after })
725
809
  ] }, idx);
726
810
  }) });
727
811
  };
728
812
  return /* @__PURE__ */ jsx3(Box3, { children: isActive ? renderLines() : /* @__PURE__ */ jsxs3(Box3, { children: [
729
813
  /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: "> " }),
730
- /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: isEmpty ? placeholder ?? "" : lines.join(" ") })
814
+ /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: isEmpty ? placeholder ?? "" : value })
731
815
  ] }) });
732
816
  });
733
817
  var Input_default = Input;
@@ -788,7 +872,9 @@ function dismiss(state) {
788
872
 
789
873
  // src/ui/App.tsx
790
874
  import { jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
791
- function App({ config: config2, version: version2, autoMode: autoMode2 = false, registry: registry2 }) {
875
+ function App({ config: config2, version: version2, autoMode: autoMode2 = false, registry: registry2, llmClient }) {
876
+ const { stdout } = useStdout();
877
+ const historyMaxHeight = Math.max(5, (stdout?.rows ?? 24) - 4);
792
878
  const [messages, setMessages] = useState3([]);
793
879
  const [status, setStatus] = useState3("idle");
794
880
  const contextLimit = getContextLimit(config2.model, config2.contextLimit);
@@ -800,8 +886,21 @@ function App({ config: config2, version: version2, autoMode: autoMode2 = false,
800
886
  const [toolName, setToolName] = useState3(void 0);
801
887
  const [confirmPrompt, setConfirmPrompt] = useState3(void 0);
802
888
  const [expandTools, setExpandTools] = useState3(false);
889
+ const [scrollOffset, setScrollOffset] = useState3(0);
890
+ const [inputHistory, setInputHistory] = useState3([]);
891
+ const inputHistoryRef = useRef2([]);
892
+ inputHistoryRef.current = inputHistory;
893
+ const historyIndexRef = useRef2(-1);
894
+ const isNavigatingHistoryRef = useRef2(false);
895
+ const totalLines = useMemo(() => {
896
+ const visible = messages.filter((m) => m.role !== "system");
897
+ return visible.reduce(
898
+ (acc, msg) => acc + messageToLines(msg, expandTools, stdout?.columns ?? 0).length,
899
+ 0
900
+ );
901
+ }, [messages, expandTools, stdout?.columns]);
803
902
  const pendingConfirmRef = useRef2(null);
804
- const llmRef = useRef2(createLLMClient(config2));
903
+ const llmRef = useRef2(llmClient ?? createLLMClient(config2));
805
904
  const inputRef = useRef2(null);
806
905
  const [acState, setAcState] = useState3(getInitialState());
807
906
  const loggerRef = useRef2(null);
@@ -828,7 +927,7 @@ function App({ config: config2, version: version2, autoMode: autoMode2 = false,
828
927
  }
829
928
  loggedCountRef.current = messages.length;
830
929
  }, [messages]);
831
- useInput2((_input, key) => {
930
+ useInput2((input, key) => {
832
931
  const skillList = registry2?.list() ?? [];
833
932
  const suggestions = computeSuggestions(skillList, acState);
834
933
  const open = isOpen(acState, suggestions);
@@ -855,6 +954,48 @@ function App({ config: config2, version: version2, autoMode: autoMode2 = false,
855
954
  }
856
955
  if (key.tab) {
857
956
  setExpandTools((prev) => !prev);
957
+ return;
958
+ }
959
+ const scrollStep = Math.max(1, Math.floor(historyMaxHeight / 2));
960
+ if (key.pageUp) {
961
+ setScrollOffset((prev) => Math.min(prev + scrollStep, Math.max(0, totalLines - 1)));
962
+ return;
963
+ }
964
+ if (key.pageDown) {
965
+ setScrollOffset((prev) => Math.max(0, prev - scrollStep));
966
+ return;
967
+ }
968
+ if (key.ctrl && input === "v") {
969
+ setScrollOffset((prev) => Math.max(0, prev - scrollStep));
970
+ return;
971
+ }
972
+ if (key.meta && input === "v") {
973
+ setScrollOffset((prev) => Math.min(prev + scrollStep, Math.max(0, totalLines - 1)));
974
+ return;
975
+ }
976
+ if (key.ctrl && input === "p") {
977
+ const history = inputHistoryRef.current;
978
+ const newIndex = Math.min(historyIndexRef.current + 1, history.length - 1);
979
+ if (newIndex >= 0 && newIndex < history.length) {
980
+ historyIndexRef.current = newIndex;
981
+ isNavigatingHistoryRef.current = true;
982
+ inputRef.current?.fill(history[newIndex]);
983
+ }
984
+ return;
985
+ }
986
+ if (key.ctrl && input === "n") {
987
+ const history = inputHistoryRef.current;
988
+ if (historyIndexRef.current > 0) {
989
+ const newIndex = historyIndexRef.current - 1;
990
+ historyIndexRef.current = newIndex;
991
+ isNavigatingHistoryRef.current = true;
992
+ inputRef.current?.fill(history[newIndex]);
993
+ } else if (historyIndexRef.current === 0) {
994
+ historyIndexRef.current = -1;
995
+ isNavigatingHistoryRef.current = true;
996
+ inputRef.current?.fill("");
997
+ }
998
+ return;
858
999
  }
859
1000
  });
860
1001
  const confirm = useCallback((prompt) => {
@@ -999,6 +1140,9 @@ function App({ config: config2, version: version2, autoMode: autoMode2 = false,
999
1140
  return;
1000
1141
  }
1001
1142
  if (!trimmed) return;
1143
+ setInputHistory((prev) => [trimmed, ...prev.slice(0, 99)]);
1144
+ historyIndexRef.current = -1;
1145
+ setScrollOffset(0);
1002
1146
  if (registry2) {
1003
1147
  const skillResult = handleSkillInput(trimmed, registry2);
1004
1148
  if (skillResult.type === "error") {
@@ -1039,6 +1183,11 @@ function App({ config: config2, version: version2, autoMode: autoMode2 = false,
1039
1183
  );
1040
1184
  const isInputActive = status === "idle" || status === "awaiting_confirm";
1041
1185
  const handleInputTextChange = useCallback((text) => {
1186
+ if (isNavigatingHistoryRef.current) {
1187
+ isNavigatingHistoryRef.current = false;
1188
+ } else {
1189
+ historyIndexRef.current = -1;
1190
+ }
1042
1191
  if (status !== "awaiting_confirm") {
1043
1192
  setAcState((prev) => handleInputChange(prev, text));
1044
1193
  }
@@ -1046,7 +1195,16 @@ function App({ config: config2, version: version2, autoMode: autoMode2 = false,
1046
1195
  const skillSuggestions = registry2 ? computeSuggestions(registry2.list(), acState) : [];
1047
1196
  const acOpen = isOpen(acState, skillSuggestions);
1048
1197
  return /* @__PURE__ */ jsxs5(Box5, { flexDirection: "column", height: "100%", children: [
1049
- /* @__PURE__ */ jsx5(ConversationHistory, { messages, expandTools }),
1198
+ /* @__PURE__ */ jsx5(
1199
+ ConversationHistory,
1200
+ {
1201
+ messages,
1202
+ expandTools,
1203
+ maxHeight: historyMaxHeight,
1204
+ terminalWidth: stdout?.columns,
1205
+ scrollOffset
1206
+ }
1207
+ ),
1050
1208
  /* @__PURE__ */ jsx5(
1051
1209
  StatusBar,
1052
1210
  {
@@ -1205,4 +1363,21 @@ for (const dir of [builtinSkillsDir, userSkillsDir, projectSkillsDir]) {
1205
1363
  const skills = await loadSkillsFromDir(dir);
1206
1364
  for (const skill of skills) registry.register(skill);
1207
1365
  }
1366
+ if (process.stdout.isTTY) {
1367
+ process.stdout.write("\x1B[?1049h");
1368
+ const exitAltScreen = () => process.stdout.write("\x1B[?1049l");
1369
+ process.on("exit", exitAltScreen);
1370
+ process.on("SIGINT", () => {
1371
+ exitAltScreen();
1372
+ process.exit(0);
1373
+ });
1374
+ process.on("SIGTERM", () => {
1375
+ exitAltScreen();
1376
+ process.exit(0);
1377
+ });
1378
+ process.on("SIGHUP", () => {
1379
+ exitAltScreen();
1380
+ process.exit(0);
1381
+ });
1382
+ }
1208
1383
  render(React4.createElement(App, { config: finalConfig, version: VERSION, autoMode, registry }));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zhongqian97-code/ecode",
3
- "version": "0.2.3",
3
+ "version": "0.2.5",
4
4
  "description": "A minimal Claude Code clone with REPL interface and bash tool calling",
5
5
  "type": "module",
6
6
  "author": "zhongqian97-code",