@zhongqian97-code/ecode 0.2.2 → 0.2.4

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 +141 -133
  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" });
569
511
  }
570
512
  }
571
- let toolLines = 0;
572
- if (assistantMsg.tool_calls && assistantMsg.tool_calls.length > 0) {
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" });
516
+ }
517
+ }
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
@@ -788,7 +749,9 @@ function dismiss(state) {
788
749
 
789
750
  // src/ui/App.tsx
790
751
  import { jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
791
- function App({ config: config2, version: version2, autoMode: autoMode2 = false, registry: registry2 }) {
752
+ function App({ config: config2, version: version2, autoMode: autoMode2 = false, registry: registry2, llmClient }) {
753
+ const { stdout } = useStdout();
754
+ const historyMaxHeight = Math.max(5, (stdout?.rows ?? 24) - 4);
792
755
  const [messages, setMessages] = useState3([]);
793
756
  const [status, setStatus] = useState3("idle");
794
757
  const contextLimit = getContextLimit(config2.model, config2.contextLimit);
@@ -800,8 +763,16 @@ function App({ config: config2, version: version2, autoMode: autoMode2 = false,
800
763
  const [toolName, setToolName] = useState3(void 0);
801
764
  const [confirmPrompt, setConfirmPrompt] = useState3(void 0);
802
765
  const [expandTools, setExpandTools] = useState3(false);
766
+ const [scrollOffset, setScrollOffset] = useState3(0);
767
+ const totalLines = useMemo(() => {
768
+ const visible = messages.filter((m) => m.role !== "system");
769
+ return visible.reduce(
770
+ (acc, msg) => acc + messageToLines(msg, expandTools, stdout?.columns ?? 0).length,
771
+ 0
772
+ );
773
+ }, [messages, expandTools, stdout?.columns]);
803
774
  const pendingConfirmRef = useRef2(null);
804
- const llmRef = useRef2(createLLMClient(config2));
775
+ const llmRef = useRef2(llmClient ?? createLLMClient(config2));
805
776
  const inputRef = useRef2(null);
806
777
  const [acState, setAcState] = useState3(getInitialState());
807
778
  const loggerRef = useRef2(null);
@@ -855,6 +826,16 @@ function App({ config: config2, version: version2, autoMode: autoMode2 = false,
855
826
  }
856
827
  if (key.tab) {
857
828
  setExpandTools((prev) => !prev);
829
+ return;
830
+ }
831
+ const scrollStep = Math.max(1, Math.floor(historyMaxHeight / 2));
832
+ if (key.pageUp) {
833
+ setScrollOffset((prev) => Math.min(prev + scrollStep, Math.max(0, totalLines - 1)));
834
+ return;
835
+ }
836
+ if (key.pageDown) {
837
+ setScrollOffset((prev) => Math.max(0, prev - scrollStep));
838
+ return;
858
839
  }
859
840
  });
860
841
  const confirm = useCallback((prompt) => {
@@ -999,6 +980,7 @@ function App({ config: config2, version: version2, autoMode: autoMode2 = false,
999
980
  return;
1000
981
  }
1001
982
  if (!trimmed) return;
983
+ setScrollOffset(0);
1002
984
  if (registry2) {
1003
985
  const skillResult = handleSkillInput(trimmed, registry2);
1004
986
  if (skillResult.type === "error") {
@@ -1046,7 +1028,16 @@ function App({ config: config2, version: version2, autoMode: autoMode2 = false,
1046
1028
  const skillSuggestions = registry2 ? computeSuggestions(registry2.list(), acState) : [];
1047
1029
  const acOpen = isOpen(acState, skillSuggestions);
1048
1030
  return /* @__PURE__ */ jsxs5(Box5, { flexDirection: "column", height: "100%", children: [
1049
- /* @__PURE__ */ jsx5(ConversationHistory, { messages, expandTools }),
1031
+ /* @__PURE__ */ jsx5(
1032
+ ConversationHistory,
1033
+ {
1034
+ messages,
1035
+ expandTools,
1036
+ maxHeight: historyMaxHeight,
1037
+ terminalWidth: stdout?.columns,
1038
+ scrollOffset
1039
+ }
1040
+ ),
1050
1041
  /* @__PURE__ */ jsx5(
1051
1042
  StatusBar,
1052
1043
  {
@@ -1205,4 +1196,21 @@ for (const dir of [builtinSkillsDir, userSkillsDir, projectSkillsDir]) {
1205
1196
  const skills = await loadSkillsFromDir(dir);
1206
1197
  for (const skill of skills) registry.register(skill);
1207
1198
  }
1199
+ if (process.stdout.isTTY) {
1200
+ process.stdout.write("\x1B[?1049h");
1201
+ const exitAltScreen = () => process.stdout.write("\x1B[?1049l");
1202
+ process.on("exit", exitAltScreen);
1203
+ process.on("SIGINT", () => {
1204
+ exitAltScreen();
1205
+ process.exit(0);
1206
+ });
1207
+ process.on("SIGTERM", () => {
1208
+ exitAltScreen();
1209
+ process.exit(0);
1210
+ });
1211
+ process.on("SIGHUP", () => {
1212
+ exitAltScreen();
1213
+ process.exit(0);
1214
+ });
1215
+ }
1208
1216
  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.2",
3
+ "version": "0.2.4",
4
4
  "description": "A minimal Claude Code clone with REPL interface and bash tool calling",
5
5
  "type": "module",
6
6
  "author": "zhongqian97-code",