wispy-cli 2.7.24 → 2.7.26

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,30 +1,14 @@
1
1
  /**
2
2
  * cjk-text-input.mjs — CJK-aware text input for Ink TUI
3
3
  *
4
- * Drop-in replacement for `ink-text-input` that handles Korean/CJK IME
5
- * composition properly by using Node's UTF-8 encoded stdin data events
6
- * instead of raw byte-by-byte useInput hooks.
7
- *
8
- * Key insight: When stdin is in "raw" mode and encoding is set to "utf8",
9
- * Node.js emits fully-composed UTF-8 strings (including Korean/CJK characters)
10
- * rather than raw bytes. The terminal emulator handles IME composition before
11
- * sending the final composed character to stdin.
4
+ * Uses Ink's native useInput hook (not raw stdin) so it works alongside
5
+ * other Ink components. Korean/CJK works because useInput already receives
6
+ * decoded UTF-8 characters the real fix is handling them correctly.
12
7
  */
13
8
 
14
- import React, { useState, useEffect, useRef, useCallback } from "react";
15
- import { Box, Text, useStdin } from "ink";
9
+ import React, { useState, useEffect, useRef } from "react";
10
+ import { Box, Text, useInput, useStdout } from "ink";
16
11
 
17
- /**
18
- * CJKTextInput — A text input component that properly handles CJK/Korean input.
19
- *
20
- * @param {object} props
21
- * @param {string} props.value - Controlled value
22
- * @param {function} props.onChange - Called with new value on change
23
- * @param {function} props.onSubmit - Called with value on Enter
24
- * @param {string} [props.placeholder] - Placeholder text when empty
25
- * @param {boolean} [props.focus=true] - Whether this input is focused
26
- * @param {boolean} [props.showCursor=true] - Whether to show cursor block
27
- */
28
12
  function CJKTextInput({
29
13
  value = "",
30
14
  onChange,
@@ -33,118 +17,85 @@ function CJKTextInput({
33
17
  focus = true,
34
18
  showCursor = true,
35
19
  }) {
36
- const { stdin, setRawMode } = useStdin();
37
- const valueRef = useRef(value);
38
-
39
- // Keep ref in sync with controlled value
40
- useEffect(() => {
41
- valueRef.current = value;
42
- }, [value]);
20
+ const { stdout } = useStdout();
43
21
 
22
+ // Show real terminal cursor when focused so IME composition popup
23
+ // appears at the correct position (not bottom-right corner)
44
24
  useEffect(() => {
45
- if (!focus || !stdin) return;
46
-
47
- // Set UTF-8 encoding so Node decodes multi-byte sequences (Korean, CJK)
48
- // before emitting the data event — this is the key fix for CJK input.
49
- const prevEncoding = stdin.readableEncoding;
50
- stdin.setEncoding("utf8");
51
-
52
- const onData = (chunk) => {
53
- // chunk is now a UTF-8 decoded string (not a Buffer)
54
- const str = typeof chunk === "string" ? chunk : chunk.toString("utf8");
55
-
56
- for (let i = 0; i < str.length; ) {
57
- // Check for escape sequences (arrow keys, etc.)
58
- if (str[i] === "\x1b") {
59
- const seq = str.slice(i);
60
- // Arrow keys
61
- if (seq.startsWith("\x1b[D") || seq.startsWith("\x1bOD")) {
62
- // Left arrow — not handled here, skip
63
- i += seq.startsWith("\x1b[D") ? 3 : 3;
64
- continue;
65
- }
66
- if (seq.startsWith("\x1b[C") || seq.startsWith("\x1bOC")) {
67
- // Right arrow — not handled here, skip
68
- i += 3;
69
- continue;
70
- }
71
- // Other escape sequences — skip
72
- i += 1;
73
- continue;
74
- }
75
-
76
- const ch = str[i];
77
- const code = ch.charCodeAt(0);
78
-
79
- if (ch === "\r" || ch === "\n") {
80
- onSubmit?.(valueRef.current);
81
- i++;
82
- continue;
83
- }
84
-
85
- if (ch === "\x7f" || ch === "\b") {
86
- // Backspace
87
- const newVal = valueRef.current.slice(0, -1);
88
- valueRef.current = newVal;
89
- onChange?.(newVal);
90
- i++;
91
- continue;
92
- }
93
-
94
- if (ch === "\x03") {
95
- // Ctrl+C — let Ink handle it
96
- i++;
97
- continue;
98
- }
99
-
100
- if (ch === "\t") {
101
- // Tab — do NOT consume, let Ink's useInput handle tab for view switching
102
- i++;
103
- continue;
104
- }
25
+ if (focus && stdout) {
26
+ stdout.write("\x1b[?25h"); // show cursor
27
+ return () => {
28
+ stdout.write("\x1b[?25l"); // hide cursor on unfocus
29
+ };
30
+ }
31
+ }, [focus, stdout]);
32
+
33
+ useInput(
34
+ (input, key) => {
35
+ // Skip keys handled by parent (Tab, Ctrl+C, arrows for view switching)
36
+ if (
37
+ key.tab ||
38
+ (key.shift && key.tab) ||
39
+ key.upArrow ||
40
+ key.downArrow ||
41
+ (key.ctrl && input === "c")
42
+ ) {
43
+ return;
44
+ }
105
45
 
106
- if (ch === "\x15") {
107
- // Ctrl+U — clear line
108
- valueRef.current = "";
109
- onChange?.("");
110
- i++;
111
- continue;
112
- }
46
+ if (key.return) {
47
+ onSubmit?.(value);
48
+ return;
49
+ }
113
50
 
114
- if (ch === "\x01") {
115
- // Ctrl+A move to beginning (just clear for simplicity)
116
- i++;
117
- continue;
118
- }
51
+ if (key.backspace || key.delete) {
52
+ // For CJK: slice off one character (which may be multi-byte)
53
+ const newVal = [...value].slice(0, -1).join("");
54
+ onChange?.(newVal);
55
+ return;
56
+ }
119
57
 
120
- // Normal printable character (ASCII or multi-byte CJK/Korean)
121
- // code >= 32 for ASCII printable; any non-control Unicode char is fine
122
- if (code >= 32 || code > 127) {
123
- const newVal = valueRef.current + ch;
124
- valueRef.current = newVal;
125
- onChange?.(newVal);
126
- }
58
+ if (key.leftArrow || key.rightArrow) {
59
+ // Cursor movement skip for now (append-only input)
60
+ return;
61
+ }
127
62
 
128
- i++;
63
+ // Ctrl+U — clear line
64
+ if (key.ctrl && input === "u") {
65
+ onChange?.("");
66
+ return;
129
67
  }
130
- };
131
68
 
132
- stdin.on("data", onData);
69
+ // Ctrl+W — delete last word
70
+ if (key.ctrl && input === "w") {
71
+ const trimmed = value.trimEnd();
72
+ const lastSpace = trimmed.lastIndexOf(" ");
73
+ onChange?.(lastSpace >= 0 ? trimmed.slice(0, lastSpace + 1) : "");
74
+ return;
75
+ }
133
76
 
134
- return () => {
135
- stdin.removeListener("data", onData);
136
- // Restore previous encoding
137
- if (prevEncoding !== null) {
138
- stdin.setEncoding(prevEncoding);
77
+ // Normal character input — this includes CJK characters
78
+ // Ink's useInput already decodes UTF-8, so 'input' is a proper string
79
+ if (input && input.length > 0) {
80
+ onChange?.(value + input);
139
81
  }
140
- };
141
- }, [focus, stdin, onChange, onSubmit]);
82
+ },
83
+ { isActive: focus }
84
+ );
142
85
 
143
86
  // Render
144
87
  const displayValue = value;
145
88
  const showPlaceholder = !displayValue && placeholder;
146
89
 
147
90
  if (showPlaceholder) {
91
+ if (showCursor && focus) {
92
+ return React.createElement(
93
+ Box,
94
+ {},
95
+ React.createElement(Text, { inverse: true }, placeholder[0] || " "),
96
+ React.createElement(Text, { dimColor: true }, placeholder.slice(1))
97
+ );
98
+ }
148
99
  return React.createElement(Text, { dimColor: true }, placeholder);
149
100
  }
150
101
 
@@ -153,7 +104,7 @@ function CJKTextInput({
153
104
  Box,
154
105
  {},
155
106
  React.createElement(Text, {}, displayValue),
156
- React.createElement(Text, { inverse: true }, " "),
107
+ React.createElement(Text, { inverse: true }, " ")
157
108
  );
158
109
  }
159
110
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wispy-cli",
3
- "version": "2.7.24",
3
+ "version": "2.7.26",
4
4
  "description": "🌿 Wispy — AI workspace assistant with trustworthy execution (harness, receipts, approvals, diffs)",
5
5
  "license": "MIT",
6
6
  "author": "Minseo & Poropo",