arc402-cli 0.9.4 → 0.9.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.
@@ -1,10 +1,11 @@
1
1
  import React from "react";
2
- import { Box, Text, useStdout } from "ink";
2
+ import { Box, Text } from "ink";
3
3
 
4
4
  interface ViewportProps {
5
5
  lines: string[];
6
6
  scrollOffset: number;
7
7
  isAutoScroll: boolean;
8
+ height: number;
8
9
  }
9
10
 
10
11
  /**
@@ -13,49 +14,37 @@ interface ViewportProps {
13
14
  * scrollOffset=0 means pinned to bottom (auto-scroll).
14
15
  * Positive scrollOffset means scrolled up by that many lines.
15
16
  */
16
- export function Viewport({ lines, scrollOffset, isAutoScroll }: ViewportProps) {
17
- const { stdout } = useStdout();
18
- const termRows = stdout?.rows ?? 24;
19
-
20
- // Approximate viewport height for scroll slicing.
21
- // The actual flex layout handles visual sizing; this is for computing
22
- // which lines to show in the scroll window.
23
- const viewportHeight = Math.max(1, termRows - 18);
17
+ export function Viewport({ lines, scrollOffset, isAutoScroll, height }: ViewportProps) {
18
+ const viewportHeight = Math.max(1, height);
24
19
 
25
20
  // Compute the window slice
26
- // scrollOffset=0 → show last viewportHeight lines
27
- // scrollOffset=N → show lines ending viewportHeight+N from end
28
21
  const totalLines = lines.length;
29
22
  let endIdx: number;
30
23
  let startIdx: number;
31
24
 
32
25
  if (scrollOffset === 0) {
33
- // Auto-scroll: pinned to bottom
34
26
  endIdx = totalLines;
35
27
  startIdx = Math.max(0, endIdx - viewportHeight);
36
28
  } else {
37
- // Scrolled up: scrollOffset lines from bottom
38
29
  endIdx = Math.max(0, totalLines - scrollOffset);
39
30
  startIdx = Math.max(0, endIdx - viewportHeight);
40
31
  }
41
32
 
42
33
  const visibleLines = lines.slice(startIdx, endIdx);
43
34
 
35
+ const canScrollUp = startIdx > 0;
44
36
  const canScrollDown = scrollOffset > 0;
45
37
 
46
38
  return (
47
39
  <Box flexDirection="column" flexGrow={1}>
48
- <Box flexDirection="column">
49
- {visibleLines.map((line, i) => (
50
- <Text key={i}>{line}</Text>
51
- ))}
52
- </Box>
53
- {/* spacer — pushes content up, scroll indicator down */}
54
- <Box flexGrow={1} />
40
+ {canScrollUp && (
41
+ <Text dimColor>{" \u2191 more (Shift+Up)"}</Text>
42
+ )}
43
+ {visibleLines.map((line, i) => (
44
+ <Text key={startIdx + i}>{line}</Text>
45
+ ))}
55
46
  {canScrollDown && !isAutoScroll && (
56
- <Box justifyContent="flex-end">
57
- <Text dimColor>↓ more</Text>
58
- </Box>
47
+ <Text dimColor>{" \u2193 more (Shift+Down / Esc)"}</Text>
59
48
  )}
60
49
  </Box>
61
50
  );
@@ -0,0 +1,127 @@
1
+ import React, { useState, useEffect } from "react";
2
+ import { Text, useInput } from "ink";
3
+
4
+ interface CustomTextInputProps {
5
+ value: string;
6
+ onChange: (value: string) => void;
7
+ onSubmit?: (value: string) => void;
8
+ focus?: boolean;
9
+ }
10
+
11
+ /**
12
+ * Minimal text input that does NOT intercept Tab, Up, Down, Escape, or Ctrl+C,
13
+ * allowing parent useInput handlers to receive those keys.
14
+ */
15
+ export function CustomTextInput({
16
+ value,
17
+ onChange,
18
+ onSubmit,
19
+ focus = true,
20
+ }: CustomTextInputProps) {
21
+ const [cursorPos, setCursorPos] = useState(value.length);
22
+ const [cursorVisible, setCursorVisible] = useState(true);
23
+
24
+ // Keep cursor within bounds when value changes externally
25
+ useEffect(() => {
26
+ setCursorPos((pos) => Math.min(pos, value.length));
27
+ }, [value]);
28
+
29
+ // Blink cursor
30
+ useEffect(() => {
31
+ if (!focus) return;
32
+ const timer = setInterval(() => {
33
+ setCursorVisible((v) => !v);
34
+ }, 530);
35
+ return () => clearInterval(timer);
36
+ }, [focus]);
37
+
38
+ useInput(
39
+ (input, key) => {
40
+ // Keys we explicitly do NOT handle — let parent see them:
41
+ // Tab, Up, Down, Escape, Ctrl+C
42
+ if (key.tab || key.upArrow || key.downArrow || key.escape) return;
43
+ if (input === "\x03") return; // Ctrl+C
44
+
45
+ // Enter/Return — submit
46
+ if (key.return) {
47
+ onSubmit?.(value);
48
+ return;
49
+ }
50
+
51
+ // Backspace
52
+ if (key.backspace || key.delete) {
53
+ if (cursorPos > 0) {
54
+ const next = value.slice(0, cursorPos - 1) + value.slice(cursorPos);
55
+ setCursorPos(cursorPos - 1);
56
+ onChange(next);
57
+ }
58
+ return;
59
+ }
60
+
61
+ // Left arrow
62
+ if (key.leftArrow) {
63
+ setCursorPos((p) => Math.max(0, p - 1));
64
+ return;
65
+ }
66
+
67
+ // Right arrow
68
+ if (key.rightArrow) {
69
+ setCursorPos((p) => Math.min(value.length, p + 1));
70
+ return;
71
+ }
72
+
73
+ // Home (Ctrl+A)
74
+ if (input === "\x01") {
75
+ setCursorPos(0);
76
+ return;
77
+ }
78
+
79
+ // End (Ctrl+E)
80
+ if (input === "\x05") {
81
+ setCursorPos(value.length);
82
+ return;
83
+ }
84
+
85
+ // Ctrl+U — clear line
86
+ if (input === "\x15") {
87
+ setCursorPos(0);
88
+ onChange("");
89
+ return;
90
+ }
91
+
92
+ // Ctrl+K — kill to end of line
93
+ if (input === "\x0B") {
94
+ onChange(value.slice(0, cursorPos));
95
+ return;
96
+ }
97
+
98
+ // Ignore other control characters
99
+ if (input.length > 0 && input.charCodeAt(0) < 32) return;
100
+
101
+ // Regular character input (including paste)
102
+ if (input.length > 0) {
103
+ const next =
104
+ value.slice(0, cursorPos) + input + value.slice(cursorPos);
105
+ setCursorPos(cursorPos + input.length);
106
+ onChange(next);
107
+ }
108
+ },
109
+ { isActive: focus }
110
+ );
111
+
112
+ if (!focus) {
113
+ return <Text dimColor>{value}</Text>;
114
+ }
115
+
116
+ const before = value.slice(0, cursorPos);
117
+ const cursorChar = cursorPos < value.length ? value[cursorPos] : " ";
118
+ const after = value.slice(cursorPos + 1);
119
+
120
+ return (
121
+ <Text>
122
+ {before}
123
+ <Text inverse={cursorVisible}>{cursorChar}</Text>
124
+ {after}
125
+ </Text>
126
+ );
127
+ }
@@ -68,14 +68,14 @@ export function useCommand(): UseCommandResult {
68
68
  stdoutRemainder += chunk.toString("utf8");
69
69
  const lines = stdoutRemainder.split("\n");
70
70
  stdoutRemainder = lines.pop() ?? "";
71
- for (const line of lines) onLine(line);
71
+ for (const line of lines) onLine(line.replace(/\r$/, ""));
72
72
  });
73
73
 
74
74
  child.stderr?.on("data", (chunk: Buffer) => {
75
75
  stderrRemainder += chunk.toString("utf8");
76
76
  const lines = stderrRemainder.split("\n");
77
77
  stderrRemainder = lines.pop() ?? "";
78
- for (const line of lines) onLine(line);
78
+ for (const line of lines) onLine(line.replace(/\r$/, ""));
79
79
  });
80
80
 
81
81
  child.on("close", (code) => {
@@ -50,6 +50,10 @@ export function useScroll(viewportHeight: number): UseScrollResult {
50
50
  scrollUp(viewportHeight);
51
51
  } else if (key.pageDown) {
52
52
  scrollDown(viewportHeight);
53
+ } else if (key.shift && key.upArrow) {
54
+ scrollUp(1);
55
+ } else if (key.shift && key.downArrow) {
56
+ scrollDown(1);
53
57
  }
54
58
  });
55
59