@zhongqian97-code/ecode 0.1.1 → 0.2.1

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 +167 -65
  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, useEffect as useEffect3 } from "react";
99
- import { Box as Box4, useInput as useInput2 } from "ink";
98
+ import { useState as useState3, useCallback, useRef as useRef2, useEffect as useEffect3 } from "react";
99
+ import { Box as Box5, useInput as useInput2 } from "ink";
100
100
 
101
101
  // src/llm.ts
102
102
  import OpenAI from "openai";
@@ -631,14 +631,27 @@ function ConversationHistory({
631
631
  }
632
632
 
633
633
  // src/ui/Input.tsx
634
- import { useState as useState2, useEffect as useEffect2 } from "react";
634
+ import { useState as useState2, useEffect as useEffect2, useRef, forwardRef, useImperativeHandle } from "react";
635
635
  import { Box as Box3, Text as Text3, useInput } from "ink";
636
636
  import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
637
637
  var CURSOR_CHAR = "\u258C";
638
638
  var BLINK_INTERVAL_MS = 530;
639
- function Input({ isActive, onSubmit, placeholder }) {
639
+ var Input = forwardRef(function Input2({ isActive, onSubmit, onChange, placeholder }, ref) {
640
640
  const [lines, setLines] = useState2([""]);
641
641
  const [cursorVisible, setCursorVisible] = useState2(true);
642
+ const linesRef = useRef(lines);
643
+ linesRef.current = lines;
644
+ const onChangeRef = useRef(onChange);
645
+ onChangeRef.current = onChange;
646
+ const onSubmitRef = useRef(onSubmit);
647
+ onSubmitRef.current = onSubmit;
648
+ useImperativeHandle(ref, () => ({
649
+ fill(text) {
650
+ const newLines = text ? text.split("\n") : [""];
651
+ setLines(newLines);
652
+ onChangeRef.current?.(text);
653
+ }
654
+ }));
642
655
  useEffect2(() => {
643
656
  if (!isActive) {
644
657
  setCursorVisible(true);
@@ -653,44 +666,44 @@ function Input({ isActive, onSubmit, placeholder }) {
653
666
  }, [isActive]);
654
667
  useInput(
655
668
  (input, key) => {
669
+ const currentLines = linesRef.current;
656
670
  if (key.return && key.shift) {
657
- setLines((prev) => [...prev, ""]);
671
+ const newLines = [...currentLines, ""];
672
+ setLines(newLines);
673
+ onChangeRef.current?.(newLines.join("\n"));
658
674
  return;
659
675
  }
660
676
  if (key.return) {
661
- const text = lines.join("\n");
662
- onSubmit(text);
677
+ const text = currentLines.join("\n");
678
+ onSubmitRef.current(text);
663
679
  setLines([""]);
680
+ onChangeRef.current?.("");
664
681
  return;
665
682
  }
666
683
  if (key.backspace || key.delete) {
667
- setLines((prev) => {
668
- const next = [...prev];
669
- const lastIdx = next.length - 1;
670
- const lastLine = next[lastIdx];
671
- if (lastLine.length > 0) {
672
- next[lastIdx] = lastLine.slice(0, -1);
673
- return next;
674
- }
675
- if (next.length > 1) {
676
- return next.slice(0, -1);
677
- }
678
- return next;
679
- });
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 {
692
+ return;
693
+ }
694
+ setLines(newLines);
695
+ onChangeRef.current?.(newLines.join("\n"));
680
696
  return;
681
697
  }
682
698
  if (key.ctrl || key.escape || key.upArrow || key.downArrow || key.leftArrow || key.rightArrow || key.tab || key.pageUp || key.pageDown) {
683
699
  return;
684
700
  }
685
701
  if (input.length > 0) {
686
- setLines((prev) => {
687
- const next = [...prev];
688
- next[next.length - 1] = next[next.length - 1] + input;
689
- return next;
690
- });
702
+ const newLines = [...currentLines.slice(0, -1), currentLines[currentLines.length - 1] + input];
703
+ setLines(newLines);
704
+ onChangeRef.current?.(newLines.join("\n"));
691
705
  }
692
706
  },
693
- // 第二个参数:仅在 isActive=true 时注册键盘监听
694
707
  { isActive }
695
708
  );
696
709
  const isEmpty = lines.every((line) => line === "");
@@ -712,20 +725,69 @@ function Input({ isActive, onSubmit, placeholder }) {
712
725
  ] }, idx);
713
726
  }) });
714
727
  };
715
- return /* @__PURE__ */ jsx3(Box3, { children: isActive ? (
716
- // 激活状态:调用完整的多行渲染逻辑
717
- renderLines()
718
- ) : (
719
- // 禁用状态:暗色单行显示,不响应键盘
720
- /* @__PURE__ */ jsxs3(Box3, { children: [
721
- /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: "> " }),
722
- /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: isEmpty ? placeholder ?? "" : lines.join(" ") })
723
- ] })
724
- ) });
728
+ return /* @__PURE__ */ jsx3(Box3, { children: isActive ? renderLines() : /* @__PURE__ */ jsxs3(Box3, { children: [
729
+ /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: "> " }),
730
+ /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: isEmpty ? placeholder ?? "" : lines.join(" ") })
731
+ ] }) });
732
+ });
733
+ var Input_default = Input;
734
+
735
+ // src/ui/SkillAutocomplete.tsx
736
+ import { Box as Box4, Text as Text4 } from "ink";
737
+ import { Fragment as Fragment2, jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
738
+ function SkillAutocomplete({
739
+ suggestions,
740
+ selectedIndex,
741
+ isOpen: isOpen2
742
+ }) {
743
+ if (!isOpen2 || suggestions.length === 0) {
744
+ return /* @__PURE__ */ jsx4(Fragment2, {});
745
+ }
746
+ return /* @__PURE__ */ jsx4(Box4, { flexDirection: "column", borderStyle: "round", borderColor: "cyan", children: suggestions.map((skill, idx) => {
747
+ const selected = idx === selectedIndex;
748
+ return /* @__PURE__ */ jsxs4(Box4, { children: [
749
+ /* @__PURE__ */ jsx4(Text4, { color: selected ? "cyan" : void 0, bold: selected, children: selected ? "> " : " " }),
750
+ /* @__PURE__ */ jsxs4(Text4, { color: selected ? "cyan" : void 0, bold: selected, children: [
751
+ "/",
752
+ skill.name
753
+ ] }),
754
+ /* @__PURE__ */ jsxs4(Text4, { dimColor: true, children: [
755
+ " \u2014 ",
756
+ skill.description
757
+ ] })
758
+ ] }, skill.name);
759
+ }) });
760
+ }
761
+
762
+ // src/ui/autocompleteLogic.ts
763
+ function getInitialState() {
764
+ return { query: "", selectedIndex: 0, dismissed: false };
765
+ }
766
+ function computeSuggestions(skills, state, maxSuggestions = 5) {
767
+ const { query } = state;
768
+ if (!query.startsWith("/")) return [];
769
+ const term = query.slice(1).toLowerCase();
770
+ if (term.includes(" ")) return [];
771
+ return skills.filter((s) => s.name.toLowerCase().startsWith(term)).slice(0, maxSuggestions);
772
+ }
773
+ function isOpen(state, suggestions) {
774
+ return state.query.startsWith("/") && suggestions.length > 0 && !state.dismissed;
775
+ }
776
+ function handleInputChange(_state, newQuery) {
777
+ return { query: newQuery, selectedIndex: 0, dismissed: false };
778
+ }
779
+ function moveUp(state, count) {
780
+ return { ...state, selectedIndex: (state.selectedIndex - 1 + count) % count };
781
+ }
782
+ function moveDown(state, count) {
783
+ return { ...state, selectedIndex: (state.selectedIndex + 1) % count };
784
+ }
785
+ function dismiss(state) {
786
+ return { ...state, dismissed: true };
725
787
  }
726
788
 
727
789
  // src/ui/App.tsx
728
- import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
790
+ import { jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
729
791
  function App({ config: config2, version: version2, autoMode: autoMode2 = false, registry: registry2 }) {
730
792
  const [messages, setMessages] = useState3([]);
731
793
  const [status, setStatus] = useState3("idle");
@@ -738,10 +800,12 @@ function App({ config: config2, version: version2, autoMode: autoMode2 = false,
738
800
  const [toolName, setToolName] = useState3(void 0);
739
801
  const [confirmPrompt, setConfirmPrompt] = useState3(void 0);
740
802
  const [expandTools, setExpandTools] = useState3(false);
741
- const pendingConfirmRef = useRef(null);
742
- const llmRef = useRef(createLLMClient(config2));
743
- const loggerRef = useRef(null);
744
- const loggedCountRef = useRef(0);
803
+ const pendingConfirmRef = useRef2(null);
804
+ const llmRef = useRef2(createLLMClient(config2));
805
+ const inputRef = useRef2(null);
806
+ const [acState, setAcState] = useState3(getInitialState());
807
+ const loggerRef = useRef2(null);
808
+ const loggedCountRef = useRef2(0);
745
809
  useEffect3(() => {
746
810
  if (config2.logDir) {
747
811
  loggerRef.current = createLogger(config2.logDir, /* @__PURE__ */ new Date());
@@ -765,6 +829,30 @@ function App({ config: config2, version: version2, autoMode: autoMode2 = false,
765
829
  loggedCountRef.current = messages.length;
766
830
  }, [messages]);
767
831
  useInput2((_input, key) => {
832
+ const skillList = registry2?.list() ?? [];
833
+ const suggestions = computeSuggestions(skillList, acState);
834
+ const open = isOpen(acState, suggestions);
835
+ if (open) {
836
+ if (key.upArrow) {
837
+ setAcState((prev) => moveUp(prev, suggestions.length));
838
+ return;
839
+ }
840
+ if (key.downArrow) {
841
+ setAcState((prev) => moveDown(prev, suggestions.length));
842
+ return;
843
+ }
844
+ if (key.escape) {
845
+ setAcState((prev) => dismiss(prev));
846
+ return;
847
+ }
848
+ if (key.tab) {
849
+ const selected = suggestions[acState.selectedIndex];
850
+ if (selected) {
851
+ inputRef.current?.fill(`/${selected.name} `);
852
+ }
853
+ return;
854
+ }
855
+ }
768
856
  if (key.tab) {
769
857
  setExpandTools((prev) => !prev);
770
858
  }
@@ -950,30 +1038,44 @@ function App({ config: config2, version: version2, autoMode: autoMode2 = false,
950
1038
  [status, messages, runLlmLoop]
951
1039
  );
952
1040
  const isInputActive = status === "idle" || status === "awaiting_confirm";
953
- return (
954
- // 顶层容器:纵向堆叠,撑满终端高度(height="100%"
955
- /* @__PURE__ */ jsxs4(Box4, { flexDirection: "column", height: "100%", children: [
956
- /* @__PURE__ */ jsx4(ConversationHistory, { messages, expandTools }),
957
- /* @__PURE__ */ jsx4(
958
- StatusBar,
959
- {
960
- status,
961
- toolName,
962
- confirmPrompt,
963
- version: version2,
964
- tokenUsage
965
- }
966
- ),
967
- /* @__PURE__ */ jsx4(
968
- Input,
969
- {
970
- isActive: isInputActive,
971
- onSubmit: handleSubmit,
972
- placeholder: status === "awaiting_confirm" ? "y / n" : void 0
973
- }
974
- )
975
- ] })
976
- );
1041
+ const handleInputTextChange = useCallback((text) => {
1042
+ if (status !== "awaiting_confirm") {
1043
+ setAcState((prev) => handleInputChange(prev, text));
1044
+ }
1045
+ }, [status]);
1046
+ const skillSuggestions = registry2 ? computeSuggestions(registry2.list(), acState) : [];
1047
+ const acOpen = isOpen(acState, skillSuggestions);
1048
+ return /* @__PURE__ */ jsxs5(Box5, { flexDirection: "column", height: "100%", children: [
1049
+ /* @__PURE__ */ jsx5(ConversationHistory, { messages, expandTools }),
1050
+ /* @__PURE__ */ jsx5(
1051
+ StatusBar,
1052
+ {
1053
+ status,
1054
+ toolName,
1055
+ confirmPrompt,
1056
+ version: version2,
1057
+ tokenUsage
1058
+ }
1059
+ ),
1060
+ /* @__PURE__ */ jsx5(
1061
+ SkillAutocomplete,
1062
+ {
1063
+ suggestions: skillSuggestions,
1064
+ selectedIndex: acState.selectedIndex,
1065
+ isOpen: acOpen
1066
+ }
1067
+ ),
1068
+ /* @__PURE__ */ jsx5(
1069
+ Input_default,
1070
+ {
1071
+ ref: inputRef,
1072
+ isActive: isInputActive,
1073
+ onSubmit: handleSubmit,
1074
+ onChange: handleInputTextChange,
1075
+ placeholder: status === "awaiting_confirm" ? "y / n" : void 0
1076
+ }
1077
+ )
1078
+ ] });
977
1079
  }
978
1080
 
979
1081
  // src/skills/registry.ts
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zhongqian97-code/ecode",
3
- "version": "0.1.1",
3
+ "version": "0.2.1",
4
4
  "description": "A minimal Claude Code clone with REPL interface and bash tool calling",
5
5
  "type": "module",
6
6
  "author": "zhongqian97-code",