@zhongqian97-code/ecode 0.1.1 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/index.js +160 -63
  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,23 @@ 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 onChangeRef = useRef(onChange);
643
+ onChangeRef.current = onChange;
644
+ useImperativeHandle(ref, () => ({
645
+ fill(text) {
646
+ const newLines = text ? text.split("\n") : [""];
647
+ setLines(newLines);
648
+ onChangeRef.current?.(text);
649
+ }
650
+ }));
642
651
  useEffect2(() => {
643
652
  if (!isActive) {
644
653
  setCursorVisible(true);
@@ -654,43 +663,42 @@ function Input({ isActive, onSubmit, placeholder }) {
654
663
  useInput(
655
664
  (input, key) => {
656
665
  if (key.return && key.shift) {
657
- setLines((prev) => [...prev, ""]);
666
+ const newLines = [...lines, ""];
667
+ setLines(newLines);
668
+ onChange?.(newLines.join("\n"));
658
669
  return;
659
670
  }
660
671
  if (key.return) {
661
672
  const text = lines.join("\n");
662
673
  onSubmit(text);
663
674
  setLines([""]);
675
+ onChange?.("");
664
676
  return;
665
677
  }
666
678
  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
- });
679
+ const lastIdx = lines.length - 1;
680
+ const lastLine = lines[lastIdx];
681
+ let newLines;
682
+ if (lastLine.length > 0) {
683
+ newLines = [...lines.slice(0, lastIdx), lastLine.slice(0, -1)];
684
+ } else if (lines.length > 1) {
685
+ newLines = lines.slice(0, -1);
686
+ } else {
687
+ return;
688
+ }
689
+ setLines(newLines);
690
+ onChange?.(newLines.join("\n"));
680
691
  return;
681
692
  }
682
693
  if (key.ctrl || key.escape || key.upArrow || key.downArrow || key.leftArrow || key.rightArrow || key.tab || key.pageUp || key.pageDown) {
683
694
  return;
684
695
  }
685
696
  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
- });
697
+ const newLines = [...lines.slice(0, -1), lines[lines.length - 1] + input];
698
+ setLines(newLines);
699
+ onChange?.(newLines.join("\n"));
691
700
  }
692
701
  },
693
- // 第二个参数:仅在 isActive=true 时注册键盘监听
694
702
  { isActive }
695
703
  );
696
704
  const isEmpty = lines.every((line) => line === "");
@@ -712,20 +720,69 @@ function Input({ isActive, onSubmit, placeholder }) {
712
720
  ] }, idx);
713
721
  }) });
714
722
  };
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
- ) });
723
+ return /* @__PURE__ */ jsx3(Box3, { children: isActive ? renderLines() : /* @__PURE__ */ jsxs3(Box3, { children: [
724
+ /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: "> " }),
725
+ /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: isEmpty ? placeholder ?? "" : lines.join(" ") })
726
+ ] }) });
727
+ });
728
+ var Input_default = Input;
729
+
730
+ // src/ui/SkillAutocomplete.tsx
731
+ import { Box as Box4, Text as Text4 } from "ink";
732
+ import { Fragment as Fragment2, jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
733
+ function SkillAutocomplete({
734
+ suggestions,
735
+ selectedIndex,
736
+ isOpen: isOpen2
737
+ }) {
738
+ if (!isOpen2 || suggestions.length === 0) {
739
+ return /* @__PURE__ */ jsx4(Fragment2, {});
740
+ }
741
+ return /* @__PURE__ */ jsx4(Box4, { flexDirection: "column", borderStyle: "round", borderColor: "cyan", children: suggestions.map((skill, idx) => {
742
+ const selected = idx === selectedIndex;
743
+ return /* @__PURE__ */ jsxs4(Box4, { children: [
744
+ /* @__PURE__ */ jsx4(Text4, { color: selected ? "cyan" : void 0, bold: selected, children: selected ? "> " : " " }),
745
+ /* @__PURE__ */ jsxs4(Text4, { color: selected ? "cyan" : void 0, bold: selected, children: [
746
+ "/",
747
+ skill.name
748
+ ] }),
749
+ /* @__PURE__ */ jsxs4(Text4, { dimColor: true, children: [
750
+ " \u2014 ",
751
+ skill.description
752
+ ] })
753
+ ] }, skill.name);
754
+ }) });
755
+ }
756
+
757
+ // src/ui/autocompleteLogic.ts
758
+ function getInitialState() {
759
+ return { query: "", selectedIndex: 0, dismissed: false };
760
+ }
761
+ function computeSuggestions(skills, state, maxSuggestions = 5) {
762
+ const { query } = state;
763
+ if (!query.startsWith("/")) return [];
764
+ const term = query.slice(1).toLowerCase();
765
+ if (term.includes(" ")) return [];
766
+ return skills.filter((s) => s.name.toLowerCase().startsWith(term)).slice(0, maxSuggestions);
767
+ }
768
+ function isOpen(state, suggestions) {
769
+ return state.query.startsWith("/") && suggestions.length > 0 && !state.dismissed;
770
+ }
771
+ function handleInputChange(_state, newQuery) {
772
+ return { query: newQuery, selectedIndex: 0, dismissed: false };
773
+ }
774
+ function moveUp(state, count) {
775
+ return { ...state, selectedIndex: (state.selectedIndex - 1 + count) % count };
776
+ }
777
+ function moveDown(state, count) {
778
+ return { ...state, selectedIndex: (state.selectedIndex + 1) % count };
779
+ }
780
+ function dismiss(state) {
781
+ return { ...state, dismissed: true };
725
782
  }
726
783
 
727
784
  // src/ui/App.tsx
728
- import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
785
+ import { jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
729
786
  function App({ config: config2, version: version2, autoMode: autoMode2 = false, registry: registry2 }) {
730
787
  const [messages, setMessages] = useState3([]);
731
788
  const [status, setStatus] = useState3("idle");
@@ -738,10 +795,12 @@ function App({ config: config2, version: version2, autoMode: autoMode2 = false,
738
795
  const [toolName, setToolName] = useState3(void 0);
739
796
  const [confirmPrompt, setConfirmPrompt] = useState3(void 0);
740
797
  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);
798
+ const pendingConfirmRef = useRef2(null);
799
+ const llmRef = useRef2(createLLMClient(config2));
800
+ const inputRef = useRef2(null);
801
+ const [acState, setAcState] = useState3(getInitialState());
802
+ const loggerRef = useRef2(null);
803
+ const loggedCountRef = useRef2(0);
745
804
  useEffect3(() => {
746
805
  if (config2.logDir) {
747
806
  loggerRef.current = createLogger(config2.logDir, /* @__PURE__ */ new Date());
@@ -765,6 +824,30 @@ function App({ config: config2, version: version2, autoMode: autoMode2 = false,
765
824
  loggedCountRef.current = messages.length;
766
825
  }, [messages]);
767
826
  useInput2((_input, key) => {
827
+ const skillList = registry2?.list() ?? [];
828
+ const suggestions = computeSuggestions(skillList, acState);
829
+ const open = isOpen(acState, suggestions);
830
+ if (open) {
831
+ if (key.upArrow) {
832
+ setAcState((prev) => moveUp(prev, suggestions.length));
833
+ return;
834
+ }
835
+ if (key.downArrow) {
836
+ setAcState((prev) => moveDown(prev, suggestions.length));
837
+ return;
838
+ }
839
+ if (key.escape) {
840
+ setAcState((prev) => dismiss(prev));
841
+ return;
842
+ }
843
+ if (key.tab) {
844
+ const selected = suggestions[acState.selectedIndex];
845
+ if (selected) {
846
+ inputRef.current?.fill(`/${selected.name} `);
847
+ }
848
+ return;
849
+ }
850
+ }
768
851
  if (key.tab) {
769
852
  setExpandTools((prev) => !prev);
770
853
  }
@@ -950,30 +1033,44 @@ function App({ config: config2, version: version2, autoMode: autoMode2 = false,
950
1033
  [status, messages, runLlmLoop]
951
1034
  );
952
1035
  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
- );
1036
+ const handleInputTextChange = useCallback((text) => {
1037
+ if (status !== "awaiting_confirm") {
1038
+ setAcState((prev) => handleInputChange(prev, text));
1039
+ }
1040
+ }, [status]);
1041
+ const skillSuggestions = registry2 ? computeSuggestions(registry2.list(), acState) : [];
1042
+ const acOpen = isOpen(acState, skillSuggestions);
1043
+ return /* @__PURE__ */ jsxs5(Box5, { flexDirection: "column", height: "100%", children: [
1044
+ /* @__PURE__ */ jsx5(ConversationHistory, { messages, expandTools }),
1045
+ /* @__PURE__ */ jsx5(
1046
+ StatusBar,
1047
+ {
1048
+ status,
1049
+ toolName,
1050
+ confirmPrompt,
1051
+ version: version2,
1052
+ tokenUsage
1053
+ }
1054
+ ),
1055
+ /* @__PURE__ */ jsx5(
1056
+ SkillAutocomplete,
1057
+ {
1058
+ suggestions: skillSuggestions,
1059
+ selectedIndex: acState.selectedIndex,
1060
+ isOpen: acOpen
1061
+ }
1062
+ ),
1063
+ /* @__PURE__ */ jsx5(
1064
+ Input_default,
1065
+ {
1066
+ ref: inputRef,
1067
+ isActive: isInputActive,
1068
+ onSubmit: handleSubmit,
1069
+ onChange: handleInputTextChange,
1070
+ placeholder: status === "awaiting_confirm" ? "y / n" : void 0
1071
+ }
1072
+ )
1073
+ ] });
977
1074
  }
978
1075
 
979
1076
  // 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.0",
4
4
  "description": "A minimal Claude Code clone with REPL interface and bash tool calling",
5
5
  "type": "module",
6
6
  "author": "zhongqian97-code",