u-foo 2.3.31 → 2.4.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 (236) hide show
  1. package/README.md +157 -213
  2. package/README.zh-CN.md +151 -197
  3. package/SKILLS/ufoo/SKILL.md +8 -8
  4. package/bin/uagy.js +69 -0
  5. package/bin/uclaude.js +2 -2
  6. package/bin/ucode.js +4 -4
  7. package/bin/ucodex.js +2 -2
  8. package/bin/ufoo.js +5 -23
  9. package/modules/AGENTS.template.md +1 -1
  10. package/modules/bus/SKILLS/ubus/SKILL.md +35 -10
  11. package/package.json +9 -5
  12. package/scripts/chat-app-smoke.js +30 -0
  13. package/scripts/global-chat-switch-benchmark.js +5 -5
  14. package/scripts/ink-demo.js +23 -0
  15. package/scripts/ink-smoke.js +30 -0
  16. package/scripts/ucode-app-smoke.js +36 -0
  17. package/src/{agent → agents/activity}/activityDetector.js +39 -2
  18. package/src/{agent → agents/activity}/activityStatePublisher.js +1 -1
  19. package/src/{agent → agents/activity}/activityStateWriter.js +2 -2
  20. package/src/{agent → agents/activity}/activityTracker.js +1 -1
  21. package/src/agents/activity/index.js +8 -0
  22. package/src/{agent → agents/controller}/controllerToolExecutor.js +4 -4
  23. package/src/agents/controller/index.js +8 -0
  24. package/src/{agent → agents/controller}/loopObservability.js +2 -2
  25. package/src/{agent → agents/controller}/loopRuntime.js +1 -1
  26. package/src/{agent → agents/controller}/ufooAgent.js +9 -9
  27. package/src/agents/index.js +10 -0
  28. package/src/agents/internal/index.js +3 -0
  29. package/src/{agent → agents/internal}/internalRunner.js +45 -22
  30. package/src/agents/launch/agyConversation.js +159 -0
  31. package/src/agents/launch/index.js +12 -0
  32. package/src/{agent → agents/launch}/launchEnvironment.js +2 -3
  33. package/src/{agent → agents/launch}/launcher.js +64 -21
  34. package/src/{agent → agents/launch}/notifier.js +23 -12
  35. package/src/{agent → agents/launch}/ptyRunner.js +44 -12
  36. package/src/{agent → agents/launch}/ptyWrapper.js +2 -2
  37. package/src/{agent → agents/launch}/publisherRouting.js +1 -1
  38. package/src/{agent → agents/launch}/readyDetector.js +23 -0
  39. package/src/{agent → agents/prompts}/defaultBootstrap.js +63 -4
  40. package/src/{group/bootstrap.js → agents/prompts/groupBootstrap.js} +41 -6
  41. package/src/agents/prompts/index.js +8 -0
  42. package/src/{code/prompts → agents/prompts/native}/index.js +1 -1
  43. package/src/{agent → agents/providers}/claudeThreadProvider.js +1 -1
  44. package/src/{agent → agents/providers}/codexThreadProvider.js +1 -1
  45. package/src/{agent → agents/providers}/directAuthStatus.js +184 -1
  46. package/src/agents/providers/index.js +13 -0
  47. package/src/{agent → agents/providers}/upstreamTransport.js +2 -2
  48. package/src/{chat → app/chat}/agentSockets.js +1 -1
  49. package/src/{chat → app/chat}/commandExecutor.js +56 -28
  50. package/src/{chat → app/chat}/commands.js +119 -5
  51. package/src/{chat → app/chat}/daemonConnection.js +1 -1
  52. package/src/{chat → app/chat}/daemonMessageRouter.js +54 -4
  53. package/src/{chat → app/chat}/daemonTransport.js +2 -1
  54. package/src/{chat → app/chat}/dashboardView.js +2 -21
  55. package/src/app/chat/index.js +6 -0
  56. package/src/{chat → app/chat}/inputSubmitHandler.js +38 -13
  57. package/src/{chat → app/chat}/internalAgentLogHistory.js +1 -1
  58. package/src/app/chat/multiWindow/index.js +268 -0
  59. package/src/app/chat/multiWindow/paneLayout.js +84 -0
  60. package/src/app/chat/multiWindow/paneManager.js +299 -0
  61. package/src/app/chat/multiWindow/renderer.js +384 -0
  62. package/src/app/chat/multiWindow/virtualTerminal.js +327 -0
  63. package/src/{chat → app/chat}/projectCloseController.js +1 -1
  64. package/src/app/chat/shellCommand.js +42 -0
  65. package/src/{chat → app/chat}/transport.js +16 -3
  66. package/src/{cli → app/cli}/ctxCoreCommands.js +3 -3
  67. package/src/{doctor/index.js → app/cli/features/doctor.js} +1 -1
  68. package/src/{init/index.js → app/cli/features/init.js} +14 -32
  69. package/src/{cli → app/cli}/groupCoreCommands.js +2 -2
  70. package/src/app/cli/index.js +9 -0
  71. package/src/{cli → app/cli}/onlineCoreCommands.js +5 -5
  72. package/src/{cli.js → app/cli/run.js} +62 -59
  73. package/src/app/index.js +6 -0
  74. package/src/code/agent.js +10 -9
  75. package/src/code/index.js +2 -0
  76. package/src/code/launcher/index.js +9 -0
  77. package/src/{agent → code/launcher}/ucode.js +7 -8
  78. package/src/{agent → code/launcher}/ucodeBootstrap.js +3 -3
  79. package/src/{agent → code/launcher}/ucodeBuild.js +2 -2
  80. package/src/{agent → code/launcher}/ucodeDoctor.js +2 -2
  81. package/src/{agent → code/launcher}/ucodeRuntimeConfig.js +1 -2
  82. package/src/code/nativeRunner.js +4 -4
  83. package/src/code/taskDecomposer.js +5 -4
  84. package/src/code/tui.js +39 -1997
  85. package/src/config.js +15 -2
  86. package/src/{bus → coordination/bus}/activate.js +2 -2
  87. package/src/{bus → coordination/bus}/daemon.js +15 -5
  88. package/src/coordination/bus/envelope.js +173 -0
  89. package/src/{bus → coordination/bus}/index.js +7 -3
  90. package/src/{bus → coordination/bus}/inject.js +11 -3
  91. package/src/{bus → coordination/bus}/message.js +1 -1
  92. package/src/coordination/bus/messageMeta.js +130 -0
  93. package/src/coordination/bus/promptEnvelope.js +65 -0
  94. package/src/{bus → coordination/bus}/shake.js +1 -1
  95. package/src/{bus → coordination/bus}/store.js +3 -3
  96. package/src/{bus → coordination/bus}/subscriber.js +2 -2
  97. package/src/{bus → coordination/bus}/utils.js +2 -2
  98. package/src/{history → coordination/history}/inputTimeline.js +5 -5
  99. package/src/coordination/index.js +10 -0
  100. package/src/{memory → coordination/memory}/historySearch.js +1 -1
  101. package/src/{memory → coordination/memory}/index.js +3 -3
  102. package/src/{report → coordination/report}/store.js +2 -2
  103. package/src/{ufoo → coordination/state}/agentRegistryDiagnostics.js +43 -0
  104. package/src/{status → coordination/status}/index.js +3 -3
  105. package/src/online/bridge.js +2 -2
  106. package/src/{controller → orchestration/controller}/flags.js +1 -1
  107. package/src/{controller → orchestration/controller}/gateRouter.js +1 -1
  108. package/src/orchestration/controller/index.js +10 -0
  109. package/src/{controller → orchestration/controller}/shadowGuard.js +1 -1
  110. package/src/orchestration/groups/bootstrap.js +3 -0
  111. package/src/orchestration/groups/index.js +10 -0
  112. package/src/orchestration/groups/promptProfiles.js +3 -0
  113. package/src/{group → orchestration/groups}/templates.js +1 -1
  114. package/src/{group → orchestration/groups}/validateTemplate.js +1 -1
  115. package/src/orchestration/index.js +7 -0
  116. package/src/orchestration/solo/index.js +3 -0
  117. package/src/{daemon → runtime/daemon}/agentProcessManager.js +1 -1
  118. package/src/{daemon → runtime/daemon}/cronOps.js +3 -2
  119. package/src/{daemon → runtime/daemon}/groupOrchestrator.js +26 -9
  120. package/src/{daemon → runtime/daemon}/index.js +273 -79
  121. package/src/{daemon → runtime/daemon}/ipcServer.js +24 -2
  122. package/src/{daemon → runtime/daemon}/nicknameScope.js +6 -3
  123. package/src/{daemon → runtime/daemon}/ops.js +48 -61
  124. package/src/{daemon → runtime/daemon}/promptLoop.js +1 -1
  125. package/src/{daemon → runtime/daemon}/promptRequest.js +13 -8
  126. package/src/runtime/daemon/providerSessions.js +230 -0
  127. package/src/{daemon → runtime/daemon}/reporting.js +4 -4
  128. package/src/{daemon → runtime/daemon}/run.js +12 -5
  129. package/src/{daemon → runtime/daemon}/soloBootstrap.js +7 -7
  130. package/src/{daemon → runtime/daemon}/status.js +5 -5
  131. package/src/runtime/index.js +10 -0
  132. package/src/runtime/process/nodeExecutable.js +26 -0
  133. package/src/{projects → runtime/projects}/registry.js +1 -1
  134. package/src/{projects → runtime/projects}/runtimes.js +1 -1
  135. package/src/{terminal → runtime/terminal}/adapterRouter.js +0 -10
  136. package/src/{terminal → runtime/terminal}/adapters/internalAdapter.js +0 -4
  137. package/src/tools/handlers/common.js +1 -1
  138. package/src/tools/handlers/listAgents.js +1 -1
  139. package/src/tools/handlers/memory.js +3 -3
  140. package/src/tools/handlers/readBusSummary.js +1 -1
  141. package/src/tools/handlers/readOpenDecisions.js +1 -1
  142. package/src/tools/handlers/readProjectRegistry.js +1 -1
  143. package/src/tools/handlers/readPromptHistory.js +2 -2
  144. package/src/tools/schemaFixtures.js +1 -1
  145. package/src/ui/MIGRATION.md +336 -0
  146. package/src/ui/format/index.js +974 -0
  147. package/src/ui/index.js +9 -0
  148. package/src/ui/ink/ChatApp.js +3674 -0
  149. package/src/ui/ink/DashboardBar.js +685 -0
  150. package/src/ui/ink/InkDemo.js +96 -0
  151. package/src/ui/ink/MultilineInput.js +612 -0
  152. package/src/ui/ink/UcodeApp.js +822 -0
  153. package/src/ui/ink/agentMirror.js +730 -0
  154. package/src/ui/ink/chatReducer.js +359 -0
  155. package/src/ui/runInk.js +57 -0
  156. package/src/bus/messageMeta.js +0 -52
  157. package/src/chat/agentViewController.js +0 -1072
  158. package/src/chat/chatLogController.js +0 -138
  159. package/src/chat/completionController.js +0 -533
  160. package/src/chat/dashboardKeyController.js +0 -573
  161. package/src/chat/index.js +0 -2214
  162. package/src/chat/inputHistoryController.js +0 -135
  163. package/src/chat/inputListenerController.js +0 -470
  164. package/src/chat/layout.js +0 -186
  165. package/src/chat/pasteController.js +0 -81
  166. package/src/chat/statusLineController.js +0 -223
  167. package/src/chat/streamTracker.js +0 -156
  168. package/src/code/config +0 -0
  169. package/src/daemon/providerSessions.js +0 -488
  170. package/src/terminal/adapters/internalPtyAdapter.js +0 -42
  171. /package/src/{code/prompts → agents/prompts/native}/actions.js +0 -0
  172. /package/src/{code/prompts → agents/prompts/native}/efficiency.js +0 -0
  173. /package/src/{code/prompts → agents/prompts/native}/environment.js +0 -0
  174. /package/src/{code/prompts → agents/prompts/native}/identity.js +0 -0
  175. /package/src/{code/prompts → agents/prompts/native}/safety.js +0 -0
  176. /package/src/{code/prompts → agents/prompts/native}/sections.js +0 -0
  177. /package/src/{code/prompts → agents/prompts/native}/system.js +0 -0
  178. /package/src/{code/prompts → agents/prompts/native}/tasks.js +0 -0
  179. /package/src/{code/prompts → agents/prompts/native}/toolDescriptions/bash.js +0 -0
  180. /package/src/{code/prompts → agents/prompts/native}/toolDescriptions/edit.js +0 -0
  181. /package/src/{code/prompts → agents/prompts/native}/toolDescriptions/read.js +0 -0
  182. /package/src/{code/prompts → agents/prompts/native}/toolDescriptions/write.js +0 -0
  183. /package/src/{code/prompts → agents/prompts/native}/ufoo.js +0 -0
  184. /package/src/{group → agents/prompts}/promptProfiles.js +0 -0
  185. /package/src/{agent → agents/providers}/claudeEventTranslator.js +0 -0
  186. /package/src/{agent → agents/providers}/claudeOauthTokenReader.js +0 -0
  187. /package/src/{agent → agents/providers}/claudeSessionFiles.js +0 -0
  188. /package/src/{agent → agents/providers}/codexEventTranslator.js +0 -0
  189. /package/src/{agent → agents/providers}/credentials/claude.js +0 -0
  190. /package/src/{agent → agents/providers}/credentials/codex.js +0 -0
  191. /package/src/{agent → agents/providers}/credentials/index.js +0 -0
  192. /package/src/{chat → app/chat}/agentBar.js +0 -0
  193. /package/src/{chat → app/chat}/agentDirectory.js +0 -0
  194. /package/src/{chat → app/chat}/cronScheduler.js +0 -0
  195. /package/src/{chat → app/chat}/daemonCoordinator.js +0 -0
  196. /package/src/{chat → app/chat}/daemonReconnect.js +0 -0
  197. /package/src/{chat → app/chat}/daemonTransportDefaults.js +0 -0
  198. /package/src/{chat → app/chat}/inputMath.js +0 -0
  199. /package/src/{chat → app/chat}/rawKeyMap.js +0 -0
  200. /package/src/{chat → app/chat}/settingsController.js +0 -0
  201. /package/src/{chat → app/chat}/text.js +0 -0
  202. /package/src/{chat → app/chat}/transientAgentState.js +0 -0
  203. /package/src/{cli → app/cli}/busCoreCommands.js +0 -0
  204. /package/src/{skills/index.js → app/cli/features/skills.js} +0 -0
  205. /package/src/{bus → coordination/bus}/nickname.js +0 -0
  206. /package/src/{bus → coordination/bus}/queue.js +0 -0
  207. /package/src/{context → coordination/context}/decisions.js +0 -0
  208. /package/src/{context → coordination/context}/doctor.js +0 -0
  209. /package/src/{context → coordination/context}/index.js +0 -0
  210. /package/src/{context → coordination/context}/sync.js +0 -0
  211. /package/src/{ufoo → coordination/state}/agentsStore.js +0 -0
  212. /package/src/{ufoo → coordination/state}/paths.js +0 -0
  213. /package/src/{controller → orchestration/controller}/launchRouting.js +0 -0
  214. /package/src/{controller → orchestration/controller}/routerFastPath.js +0 -0
  215. /package/src/{controller → orchestration/controller}/routerFinalize.js +0 -0
  216. /package/src/{group → orchestration/groups}/diagram.js +0 -0
  217. /package/src/{group → orchestration/groups}/templateValidation.js +0 -0
  218. /package/src/{solo → orchestration/solo}/commands.js +0 -0
  219. /package/src/{shared → runtime/contracts}/eventContract.js +0 -0
  220. /package/src/{shared → runtime/contracts}/ptySocketContract.js +0 -0
  221. /package/src/{providerapi → runtime/privacy}/redactor.js +0 -0
  222. /package/src/{providerapi → runtime/privacy}/shadowDiff.js +0 -0
  223. /package/src/{projects → runtime/projects}/identity.js +0 -0
  224. /package/src/{projects → runtime/projects}/index.js +0 -0
  225. /package/src/{projects → runtime/projects}/projectId.js +0 -0
  226. /package/src/{terminal → runtime/terminal}/adapterContract.js +0 -0
  227. /package/src/{terminal → runtime/terminal}/adapters/externalAdapter.js +0 -0
  228. /package/src/{terminal → runtime/terminal}/adapters/hostAdapter.js +0 -0
  229. /package/src/{terminal → runtime/terminal}/adapters/internalQueueAdapter.js +0 -0
  230. /package/src/{terminal → runtime/terminal}/adapters/terminalAdapter.js +0 -0
  231. /package/src/{terminal → runtime/terminal}/adapters/tmuxAdapter.js +0 -0
  232. /package/src/{terminal → runtime/terminal}/detect.js +0 -0
  233. /package/src/{terminal → runtime/terminal}/index.js +0 -0
  234. /package/src/{terminal → runtime/terminal}/iterm2.js +0 -0
  235. /package/src/{utils → ui/format}/banner.js +0 -0
  236. /package/src/{shared → ui/format}/markdownRenderer.js +0 -0
@@ -0,0 +1,612 @@
1
+ "use strict";
2
+
3
+ /**
4
+ * Multiline text input for the ink-based ucode TUI.
5
+ *
6
+ * Built on ink's useInput. Cursor math is delegated to src/ui/format so
7
+ * jest can cover the editor behaviour without mounting ink.
8
+ *
9
+ * Props:
10
+ * value (string) text contents (controlled)
11
+ * onChange(nextValue) called when value changes
12
+ * onSubmit(value) Enter pressed without modifiers, no trailing `\`
13
+ * onCancel() Esc pressed; the parent decides what to do
14
+ * (e.g. abort an in-flight task). The component
15
+ * does NOT mutate `value` on cancel.
16
+ * onArrowUpAtTop(value) cursor on the first visual row, Up pressed
17
+ * onArrowDownAtBottom(value) cursor on the last visual row, Down pressed
18
+ * onArrowLeftAtEmpty(value) Left pressed while value is empty
19
+ * onArrowRightAtEmpty(value) Right pressed while value is empty
20
+ * width (number) wrap width in display cells
21
+ * interactive (boolean) gates useInput; pass false for non-TTY mounts
22
+ * interceptArrowsAndEnter (boolean)
23
+ * when true, Up/Down/Left/Right/Return are
24
+ * suppressed inside the editor so a parent
25
+ * component (e.g. completion popup) can
26
+ * handle them. Plain editing keys still work.
27
+ * placeholder (string) rendered in gray when value is empty
28
+ *
29
+ * Newlines: Enter submits. Use Alt+Enter (delivered as meta+Return) or end the
30
+ * line with `\` (the legacy continuation trick) to insert a literal newline.
31
+ * We do NOT rely on Shift+Enter — many terminals don't distinguish it from
32
+ * plain Enter, so it would silently submit.
33
+ *
34
+ * Bracketed paste arrives as a multi-byte `input` chunk in useInput; we route
35
+ * it through insertText, so multi-line paste already works without extra code.
36
+ */
37
+
38
+ const fmt = require("../format");
39
+
40
+ // IME cursor parking interacts with ink's frame rendering. Two facts about
41
+ // ink make the naive "move cursor up in useEffect" approach insufficient:
42
+ //
43
+ // 1. ink throttles onRender to 32ms (ink.js:39), so frame writes happen
44
+ // AFTER our useEffect, not before — anything we wrote in useEffect ends
45
+ // up getting overwritten by ink's parking cursor at the bottom of the
46
+ // next frame, which is what the user sees as "光标被结尾抢走".
47
+ //
48
+ // 2. ink's log-update emits ansi-escapes.eraseLines(N) before each frame:
49
+ // a sequence of `eraseLine + cursorUp` pairs starting from "wherever
50
+ // the cursor currently is". If our IME hack left the cursor mid-frame,
51
+ // ink's relative cursorUp walks past the top of the frame and tramples
52
+ // lines above it.
53
+ //
54
+ // Fix: wrap stdout.write once. Before any frame-shaped write (starts with
55
+ // ESC[2K from eraseLines, or ESC[2J from full-screen rerender), push the
56
+ // cursor back DOWN to the parking row so ink's math is restored. AFTER the
57
+ // frame write, if the IME park target is active, re-emit the cursor-up +
58
+ // CHA so the hardware cursor follows the inverse caret again. This way the
59
+ // caret stays parked at the IME-visible row even when ink's throttled write
60
+ // fires long after React commit.
61
+ const __imeStdoutState = new WeakSet();
62
+ const __imeCursor = {
63
+ active: false,
64
+ // Where to park the cursor: rowsUp above ink's "row after last frame line"
65
+ // anchor, and 0-based terminal column.
66
+ parkRowsUp: 0,
67
+ parkCol: 0,
68
+ // How many rows up we last actually moved the cursor — used to undo the
69
+ // move before ink runs its relative eraseLines. ALWAYS matches the move
70
+ // we last wrote, regardless of whether that was through the patched
71
+ // stdout write or the useEffect path.
72
+ movedUpRows: 0,
73
+ // Tracks whether the LAST frame ink wrote ended with '\n' (the log-update
74
+ // path) or not (the full-screen path). The anchor row is one row higher
75
+ // when there's no trailing newline, which shifts subsequent restore-down
76
+ // math by one. Without this flag, useEffect after a full-screen frame
77
+ // restores down too far and overshoots upward → a "ghost caret" sits one
78
+ // or more rows above the real caret.
79
+ lastFrameHadNewline: true,
80
+ };
81
+
82
+ function isFrameWrite(chunk) {
83
+ if (typeof chunk !== "string" && !(chunk instanceof String)) return false;
84
+ const str = String(chunk);
85
+ // eraseLines(N>0) starts with ESC[2K; full-screen clear starts with ESC[2J.
86
+ return str.startsWith("\x1b[2K") || str.startsWith("\x1b[2J");
87
+ }
88
+
89
+ // Compute "rows up from the current anchor to the caret". The anchor sits
90
+ // one row below the last frame line when the frame ended with '\n', and AT
91
+ // the last frame line otherwise — so a frame with no trailing newline needs
92
+ // one fewer row up to land on the caret.
93
+ function rowsUpFromAnchor() {
94
+ const base = __imeCursor.parkRowsUp;
95
+ return __imeCursor.lastFrameHadNewline ? base : Math.max(0, base - 1);
96
+ }
97
+
98
+ function applyParkSequence(parkRowsUp) {
99
+ if (!__imeCursor.active) return "";
100
+ const up = parkRowsUp > 0 ? `\x1b[${parkRowsUp}A` : "";
101
+ const col = `\x1b[${__imeCursor.parkCol + 1}G`; // CHA is 1-based
102
+ __imeCursor.movedUpRows = parkRowsUp;
103
+ return `\x1b[?25h${up}${col}`;
104
+ }
105
+
106
+ function patchStdoutForIME(out) {
107
+ if (!out || typeof out.write !== "function" || __imeStdoutState.has(out)) {
108
+ return;
109
+ }
110
+ __imeStdoutState.add(out);
111
+ const originalWrite = out.write.bind(out);
112
+ out.write = function patchedWrite(chunk, encoding, callback) {
113
+ if (!isFrameWrite(chunk) || (typeof chunk !== "string" && !(chunk instanceof String))) {
114
+ return originalWrite(chunk, encoding, callback);
115
+ }
116
+ // Combine "hide cursor + restore-to-anchor + ink's frame + reposition +
117
+ // show cursor" into a SINGLE write so the terminal processes the whole
118
+ // transition atomically. With ink's eraseLines walking the cursor up
119
+ // through the frame mid-write, even one stray byte between escape
120
+ // sequences can leave the hardware cursor visible on an intermediate
121
+ // row for a frame — exactly the "faint cursor above the real one"
122
+ // ghost the user reports.
123
+ const str = String(chunk);
124
+ let prefix = "\x1b[?25l"; // hide cursor for the whole transition
125
+ if (__imeCursor.movedUpRows > 0) {
126
+ // Push the cursor back down to ink's "after last frame line" anchor
127
+ // so the relative cursorUp inside eraseLines walks the right rows.
128
+ prefix += `\x1b[${__imeCursor.movedUpRows}B`;
129
+ __imeCursor.movedUpRows = 0;
130
+ }
131
+ // Record which ink path this frame took so subsequent restores-down know
132
+ // where the anchor actually sits.
133
+ __imeCursor.lastFrameHadNewline = str.endsWith("\n");
134
+ const suffix = applyParkSequence(rowsUpFromAnchor()) || "\x1b[?25l";
135
+ return originalWrite(prefix + str + suffix, encoding, callback);
136
+ };
137
+ }
138
+
139
+ function createMultilineInput({ React, ink }) {
140
+ const { useState, useCallback, useMemo, useEffect } = React;
141
+ const { Box, Text, useInput, useStdout } = ink;
142
+ const h = React.createElement;
143
+
144
+ return function MultilineInput({
145
+ value = "",
146
+ valueVersion = 0,
147
+ onChange = () => {},
148
+ onSubmit = () => {},
149
+ onCancel = () => {},
150
+ onArrowUpAtTop,
151
+ onArrowDownAtBottom,
152
+ onArrowLeftAtEmpty,
153
+ onArrowRightAtEmpty,
154
+ width = 80,
155
+ interactive = true,
156
+ interceptArrowsAndEnter = false,
157
+ placeholder = "",
158
+ promptPrefix = "› ",
159
+ promptColor = "magenta",
160
+ borderColor = "gray",
161
+ // How many terminal rows of UI sit *below* the bottom of this input box
162
+ // (status line, dashboard rows, etc.). The component uses this to compute
163
+ // how far up the hardware cursor needs to be moved after each render so
164
+ // the IME composition window pops up at the visible (inverse) cursor
165
+ // instead of at the bottom of the screen.
166
+ linesBelowInput = 0,
167
+ }) {
168
+ // Cursor is owned by this component. preferredCol tracks the visual
169
+ // column we want to keep when bouncing across lines of different widths
170
+ // via Up/Down.
171
+ const [cursorState, setCursorState] = useState(() => String(value || "").length);
172
+ const [preferredCol, setPreferredCol] = useState(null);
173
+ const cursorPos = fmt.clampCursorPos(cursorState, value);
174
+
175
+ // When the parent forces a new value via valueVersion (e.g. accepting
176
+ // a completion), park the cursor at the end of the freshly inserted
177
+ // text so the user can keep typing without arrow-keying back to the
178
+ // tail.
179
+ useEffect(() => {
180
+ setCursorState(String(value || "").length);
181
+ setPreferredCol(null);
182
+ // eslint-disable-next-line react-hooks/exhaustive-deps
183
+ }, [valueVersion]);
184
+
185
+ const wrapWidth = Math.max(1, Math.floor(Number(width) || 80));
186
+
187
+ const setCursor = useCallback((next) => {
188
+ setCursorState(fmt.clampCursorPos(next, value));
189
+ }, [value]);
190
+ const resetPreferredCol = useCallback(() => setPreferredCol(null), []);
191
+
192
+ const change = useCallback((nextValue, nextCursor) => {
193
+ const clamped = fmt.clampCursorPos(nextCursor, nextValue);
194
+ setCursorState(clamped);
195
+ onChange(nextValue);
196
+ }, [onChange]);
197
+
198
+ const insertText = useCallback((text) => {
199
+ const before = value.slice(0, cursorPos);
200
+ const after = value.slice(cursorPos);
201
+ change(`${before}${text}${after}`, cursorPos + text.length);
202
+ setPreferredCol(null);
203
+ }, [value, cursorPos, change]);
204
+
205
+ const replaceRange = useCallback((start, end, text) => {
206
+ const safeStart = Math.max(0, Math.min(value.length, start));
207
+ const safeEnd = Math.max(safeStart, Math.min(value.length, end));
208
+ const next = `${value.slice(0, safeStart)}${text}${value.slice(safeEnd)}`;
209
+ change(next, safeStart + text.length);
210
+ setPreferredCol(null);
211
+ }, [value, change]);
212
+
213
+ const deleteBefore = useCallback(() => {
214
+ if (cursorPos <= 0) return;
215
+ replaceRange(cursorPos - 1, cursorPos, "");
216
+ }, [cursorPos, replaceRange]);
217
+
218
+ const deleteAt = useCallback(() => {
219
+ if (cursorPos >= value.length) return;
220
+ replaceRange(cursorPos, cursorPos + 1, "");
221
+ }, [cursorPos, value.length, replaceRange]);
222
+
223
+ const deleteToBoundary = useCallback((boundary) => {
224
+ const target = fmt.moveCursorToVisualLineBoundary({
225
+ cursorPos,
226
+ inputValue: value,
227
+ width: wrapWidth,
228
+ boundary,
229
+ });
230
+ const start = Math.min(cursorPos, target);
231
+ const end = Math.max(cursorPos, target);
232
+ if (start === end) return;
233
+ replaceRange(start, end, "");
234
+ }, [cursorPos, value, wrapWidth, replaceRange]);
235
+
236
+ const deleteWordBefore = useCallback(() => {
237
+ const next = fmt.deleteWordBeforeCursor(value, cursorPos);
238
+ change(next.value, next.cursorPos);
239
+ setPreferredCol(null);
240
+ }, [value, cursorPos, change]);
241
+
242
+ const deleteWordAfter = useCallback(() => {
243
+ const end = fmt.moveCursorByWord(value, cursorPos, "forward");
244
+ replaceRange(cursorPos, end, "");
245
+ }, [value, cursorPos, replaceRange]);
246
+
247
+ useInput((input, key) => {
248
+ // Let the parent absorb arrow keys + Enter when a popup (e.g. the
249
+ // completion list) is open. We still process plain text input so
250
+ // typing continues to filter the popup live.
251
+ if (interceptArrowsAndEnter && (
252
+ key.upArrow || key.downArrow || key.leftArrow || key.rightArrow || key.return
253
+ )) {
254
+ return;
255
+ }
256
+ // Submit: Enter without modifiers, except after a trailing backslash
257
+ // (which is the legacy "\\\n" continuation trick). Alt+Enter inserts
258
+ // a literal newline. Shift+Enter is intentionally NOT used: many
259
+ // terminals don't distinguish it from plain Enter.
260
+ if (key.return) {
261
+ if (key.meta) {
262
+ insertText("\n");
263
+ return;
264
+ }
265
+ if (cursorPos > 0 && value[cursorPos - 1] === "\\") {
266
+ replaceRange(cursorPos - 1, cursorPos, "\n");
267
+ return;
268
+ }
269
+ onSubmit(value);
270
+ return;
271
+ }
272
+ if (key.escape) {
273
+ onCancel();
274
+ return;
275
+ }
276
+ if (key.ctrl) {
277
+ if (input === "a") {
278
+ setCursor(fmt.moveCursorToVisualLineBoundary({
279
+ cursorPos, inputValue: value, width: wrapWidth, boundary: "start",
280
+ }));
281
+ resetPreferredCol();
282
+ return;
283
+ }
284
+ if (input === "e") {
285
+ setCursor(fmt.moveCursorToVisualLineBoundary({
286
+ cursorPos, inputValue: value, width: wrapWidth, boundary: "end",
287
+ }));
288
+ resetPreferredCol();
289
+ return;
290
+ }
291
+ if (input === "b") {
292
+ setCursor(fmt.moveCursorHorizontally(cursorPos, value, "left"));
293
+ resetPreferredCol();
294
+ return;
295
+ }
296
+ if (input === "f") {
297
+ setCursor(fmt.moveCursorHorizontally(cursorPos, value, "right"));
298
+ resetPreferredCol();
299
+ return;
300
+ }
301
+ if (input === "d") { deleteAt(); return; }
302
+ if (input === "h") { deleteBefore(); return; }
303
+ if (input === "k") { deleteToBoundary("end"); return; }
304
+ if (input === "u") { deleteToBoundary("start"); return; }
305
+ if (input === "w") { deleteWordBefore(); return; }
306
+ // Ctrl+C is parent's responsibility (typically exits the app).
307
+ return;
308
+ }
309
+ if (key.meta) {
310
+ if (input === "b") {
311
+ setCursor(fmt.moveCursorByWord(value, cursorPos, "backward"));
312
+ resetPreferredCol();
313
+ return;
314
+ }
315
+ if (input === "f") {
316
+ setCursor(fmt.moveCursorByWord(value, cursorPos, "forward"));
317
+ resetPreferredCol();
318
+ return;
319
+ }
320
+ if (input === "d") { deleteWordAfter(); return; }
321
+ }
322
+ if (key.backspace) {
323
+ if (key.meta || key.ctrl) deleteWordBefore();
324
+ else deleteBefore();
325
+ return;
326
+ }
327
+ if (key.delete) {
328
+ // ink reports key.delete for the 0x7F byte that most terminals send
329
+ // when the user presses the top-left Delete key (a.k.a. Backspace on
330
+ // non-Mac keyboards). Treat it as "delete the character before the
331
+ // cursor" by default. Real forward-delete (Fn+Delete on macOS) sends
332
+ // an escape sequence and ink also sets key.delete with no leading
333
+ // input — we can't reliably tell them apart, so favour the much
334
+ // more common backspace semantics. Meta+Delete still maps to
335
+ // delete-to-line-end as before.
336
+ if (key.meta) deleteToBoundary("end");
337
+ else deleteBefore();
338
+ return;
339
+ }
340
+ if (key.leftArrow) {
341
+ if (!value && typeof onArrowLeftAtEmpty === "function") {
342
+ onArrowLeftAtEmpty(value);
343
+ return;
344
+ }
345
+ setCursor(fmt.moveCursorHorizontally(cursorPos, value, "left"));
346
+ resetPreferredCol();
347
+ return;
348
+ }
349
+ if (key.rightArrow) {
350
+ if (!value && typeof onArrowRightAtEmpty === "function") {
351
+ onArrowRightAtEmpty(value);
352
+ return;
353
+ }
354
+ setCursor(fmt.moveCursorHorizontally(cursorPos, value, "right"));
355
+ resetPreferredCol();
356
+ return;
357
+ }
358
+ if (key.upArrow) {
359
+ if (value) {
360
+ const move = fmt.moveCursorVertically({
361
+ cursorPos, inputValue: value, width: wrapWidth,
362
+ direction: "up", preferredCol,
363
+ });
364
+ setPreferredCol(move.preferredCol);
365
+ if (move.moved) {
366
+ setCursor(move.nextCursorPos);
367
+ return;
368
+ }
369
+ }
370
+ if (typeof onArrowUpAtTop === "function") onArrowUpAtTop(value);
371
+ return;
372
+ }
373
+ if (key.downArrow) {
374
+ if (value) {
375
+ const move = fmt.moveCursorVertically({
376
+ cursorPos, inputValue: value, width: wrapWidth,
377
+ direction: "down", preferredCol,
378
+ });
379
+ setPreferredCol(move.preferredCol);
380
+ if (move.moved) {
381
+ setCursor(move.nextCursorPos);
382
+ return;
383
+ }
384
+ }
385
+ if (typeof onArrowDownAtBottom === "function") onArrowDownAtBottom(value);
386
+ return;
387
+ }
388
+
389
+ // Plain character / paste. Filter control bytes.
390
+ if (input && !key.ctrl && !key.meta) {
391
+ const filtered = input.replace(/[\x00-\x08\x0b-\x0c\x0e-\x1f\x7f]/g, "");
392
+ if (filtered) insertText(filtered);
393
+ }
394
+ }, { isActive: interactive });
395
+
396
+ // Render: split into logical lines, then split each into visual rows by
397
+ // wrap width. Highlight one cell at the cursor location. With a
398
+ // placeholder, we still draw the cursor (visible at offset 0) and append
399
+ // the placeholder text in gray after it.
400
+ const showPlaceholder = !value && !!placeholder;
401
+ const visualRows = useMemo(
402
+ () => layoutRows(value, wrapWidth, cursorPos),
403
+ [value, wrapWidth, cursorPos]
404
+ );
405
+
406
+ // Hardware-cursor parking for IME support. ink hides the terminal cursor
407
+ // by default and parks it after the last frame line; macOS/Linux IMEs
408
+ // (Pinyin, kkc, etc.) anchor the candidate window to the *hardware*
409
+ // cursor, so without this hack Chinese input pops up at the bottom-right
410
+ // instead of next to the inverse-block caret. We compute the row offset
411
+ // of the inverse caret from the bottom of ink's rendered frame and emit
412
+ // ANSI cursor-position escapes after every render.
413
+ //
414
+ // ink frame layout (top→bottom)
415
+ // ... chat log ...
416
+ // ┌── input border top ──┐ <- visualRows[0]
417
+ // │ › row 0 │
418
+ // │ row 1 │
419
+ // └── input border bot ──┘
420
+ // status line <- linesBelowInput rows
421
+ // dashboard row(s)
422
+ // <ink parks cursor here>
423
+ const { stdout } = useStdout() || {};
424
+ // Find the visual (row, col) of the cursor inside the wrapped layout.
425
+ let cursorVisualRow = 0;
426
+ let cursorVisualCol = 0;
427
+ {
428
+ let placed = false;
429
+ for (let r = 0; r < visualRows.length && !placed; r += 1) {
430
+ const row = visualRows[r];
431
+ let col = 0;
432
+ for (const seg of row.segments) {
433
+ if (seg.cursor) {
434
+ cursorVisualRow = r;
435
+ cursorVisualCol = col;
436
+ placed = true;
437
+ break;
438
+ }
439
+ col += fmt.displayCellWidth(seg.text);
440
+ }
441
+ }
442
+ if (!placed) {
443
+ // Cursor at end-of-input on a fresh row.
444
+ cursorVisualRow = Math.max(0, visualRows.length - 1);
445
+ cursorVisualCol = 0;
446
+ const lastRow = visualRows[cursorVisualRow];
447
+ if (lastRow) {
448
+ for (const seg of lastRow.segments) {
449
+ if (seg.cursor) break;
450
+ cursorVisualCol += fmt.displayCellWidth(seg.text);
451
+ }
452
+ }
453
+ }
454
+ }
455
+ const promptCols = cursorVisualRow === 0
456
+ ? fmt.displayCellWidth(promptPrefix)
457
+ : 2; // " " indent on continuation rows
458
+ const cursorTermCol = promptCols + cursorVisualCol; // 0-based column
459
+
460
+ // Distance from the cursor's row to the parking row that ink will leave
461
+ // behind: bottom border (1) + linesBelowInput + the trailing newline ink
462
+ // appends to its frame string (1, see ink/log-update.js).
463
+ const rowsBelowCursor = (visualRows.length - 1 - cursorVisualRow)
464
+ + 1 // bottom border row of the input box
465
+ + Math.max(0, Math.floor(Number(linesBelowInput) || 0))
466
+ + 1; // ink appends "\n" after the frame, so the cursor sits one extra
467
+ // line below the last printed row
468
+
469
+ useEffect(() => {
470
+ const out = stdout || process.stdout;
471
+ if (!out || typeof out.write !== "function" || !out.isTTY) {
472
+ __imeCursor.active = false;
473
+ return undefined;
474
+ }
475
+ if (!interactive) {
476
+ // Hand the cursor back to ink and stop chasing the caret.
477
+ if (__imeCursor.movedUpRows > 0) {
478
+ out.write(`\x1b[${__imeCursor.movedUpRows}B`);
479
+ __imeCursor.movedUpRows = 0;
480
+ }
481
+ __imeCursor.active = false;
482
+ return undefined;
483
+ }
484
+ patchStdoutForIME(out);
485
+ // Publish the desired park target so the stdout monkey-patch can
486
+ // re-park after every throttled ink frame write.
487
+ __imeCursor.active = true;
488
+ __imeCursor.parkRowsUp = rowsBelowCursor;
489
+ __imeCursor.parkCol = cursorTermCol;
490
+ // Park immediately — covers cases where ink has nothing to render
491
+ // (output unchanged) and won't fire a frame write at all, and keeps
492
+ // the caret visible between frames. Combine hide + restore + park +
493
+ // show into a single write so the terminal never sees the cursor at
494
+ // an intermediate row.
495
+ //
496
+ // CRITICAL: the move-up amount must match the anchor that movedUpRows
497
+ // was measured against. If the last frame ended without '\n' (the
498
+ // full-screen path), the anchor is one row higher than the log-update
499
+ // case, so we use rowsUpFromAnchor() rather than parkRowsUp directly.
500
+ // Otherwise restoring down by movedUpRows then moving up parkRowsUp
501
+ // overshoots by one and leaves the hardware cursor one row above the
502
+ // inverse caret — the residual "ghost cursor" symptom.
503
+ let combined = "\x1b[?25l";
504
+ if (__imeCursor.movedUpRows > 0) {
505
+ combined += `\x1b[${__imeCursor.movedUpRows}B`;
506
+ __imeCursor.movedUpRows = 0;
507
+ }
508
+ combined += applyParkSequence(rowsUpFromAnchor());
509
+ out.write(combined);
510
+ return undefined;
511
+ });
512
+
513
+ // On unmount, return the cursor to ink's expected parking row (so the
514
+ // next frame ink renders after us doesn't trample lines above its frame)
515
+ // and re-hide it so the rest of ink's lifetime behaves as before.
516
+ useEffect(() => () => {
517
+ const out = stdout || process.stdout;
518
+ __imeCursor.active = false;
519
+ if (out && typeof out.write === "function" && out.isTTY) {
520
+ const restore = __imeCursor.movedUpRows > 0
521
+ ? `\x1b[${__imeCursor.movedUpRows}B`
522
+ : "";
523
+ __imeCursor.movedUpRows = 0;
524
+ out.write(`${restore}\x1b[?25l`);
525
+ }
526
+ }, [stdout]);
527
+
528
+ return h(Box, {
529
+ borderStyle: "single",
530
+ borderTop: true,
531
+ borderBottom: true,
532
+ borderLeft: false,
533
+ borderRight: false,
534
+ borderColor,
535
+ flexDirection: "column",
536
+ width: "100%",
537
+ },
538
+ ...visualRows.map((row, idx) =>
539
+ h(Box, { key: `row-${idx}` },
540
+ idx === 0 ? h(Text, { color: promptColor }, promptPrefix) : h(Text, null, " "),
541
+ ...row.segments.map((seg, segIdx) =>
542
+ h(Text, {
543
+ key: `s-${segIdx}`,
544
+ inverse: seg.cursor,
545
+ color: showPlaceholder && idx === 0 && segIdx === 0 ? "gray" : undefined,
546
+ }, seg.text)
547
+ ),
548
+ showPlaceholder && idx === 0
549
+ ? h(Text, { color: "gray" }, placeholder)
550
+ : null,
551
+ )
552
+ ),
553
+ );
554
+ };
555
+ }
556
+
557
+ /**
558
+ * Lay out `value` into visual rows respecting `width`, and mark the cell at
559
+ * `cursor` so the renderer can invert it. Returns:
560
+ * [{ segments: [{ text, cursor }] }, ...]
561
+ *
562
+ * Cursor at end-of-input is rendered as an inverted space appended to the
563
+ * final row. Newlines split rows but never appear in segments. Pass
564
+ * `cursor < 0` to suppress the cursor entirely (used in placeholder mode).
565
+ */
566
+ function layoutRows(value, width, cursor) {
567
+ const text = String(value == null ? "" : value);
568
+ const safeWidth = Math.max(1, Math.floor(Number(width) || 1));
569
+ const rawCursor = Number(cursor);
570
+ const showCursor = Number.isFinite(rawCursor) && rawCursor >= 0;
571
+ const cursorIdx = showCursor
572
+ ? Math.min(text.length, Math.floor(rawCursor))
573
+ : -1;
574
+
575
+ const rows = [];
576
+ let row = { segments: [], cellsUsed: 0, cursorPlaced: false };
577
+ const pushRow = () => {
578
+ rows.push(row);
579
+ row = { segments: [], cellsUsed: 0, cursorPlaced: false };
580
+ };
581
+
582
+ for (let i = 0; i < text.length; i += 1) {
583
+ const ch = text[i];
584
+ if (ch === "\n") {
585
+ if (cursorIdx === i && !row.cursorPlaced) {
586
+ row.segments.push({ text: " ", cursor: true });
587
+ row.cursorPlaced = true;
588
+ }
589
+ pushRow();
590
+ continue;
591
+ }
592
+ const w = fmt.displayCellWidth(ch);
593
+ if (row.cellsUsed + w > safeWidth) pushRow();
594
+ if (cursorIdx === i) {
595
+ row.segments.push({ text: ch, cursor: true });
596
+ row.cursorPlaced = true;
597
+ } else {
598
+ row.segments.push({ text: ch, cursor: false });
599
+ }
600
+ row.cellsUsed += w;
601
+ }
602
+ if (cursorIdx === text.length && !row.cursorPlaced) {
603
+ row.segments.push({ text: " ", cursor: true });
604
+ }
605
+ rows.push(row);
606
+ return rows;
607
+ }
608
+
609
+ module.exports = {
610
+ createMultilineInput,
611
+ layoutRows,
612
+ };