u-foo 2.3.32 → 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 (235) 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 +5 -5
  12. package/scripts/chat-app-smoke.js +1 -1
  13. package/scripts/global-chat-switch-benchmark.js +5 -5
  14. package/scripts/ink-demo.js +1 -1
  15. package/scripts/ink-smoke.js +1 -1
  16. package/scripts/ucode-app-smoke.js +1 -1
  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 +50 -26
  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 +45 -3
  53. package/src/{chat → app/chat}/dashboardView.js +2 -1
  54. package/src/app/chat/index.js +6 -0
  55. package/src/{chat → app/chat}/inputSubmitHandler.js +4 -13
  56. package/src/{chat → app/chat}/internalAgentLogHistory.js +1 -1
  57. package/src/app/chat/multiWindow/index.js +268 -0
  58. package/src/app/chat/multiWindow/paneLayout.js +84 -0
  59. package/src/app/chat/multiWindow/paneManager.js +299 -0
  60. package/src/app/chat/multiWindow/renderer.js +384 -0
  61. package/src/app/chat/multiWindow/virtualTerminal.js +327 -0
  62. package/src/{chat → app/chat}/transport.js +1 -1
  63. package/src/{cli → app/cli}/ctxCoreCommands.js +3 -3
  64. package/src/{doctor/index.js → app/cli/features/doctor.js} +1 -1
  65. package/src/{init/index.js → app/cli/features/init.js} +14 -32
  66. package/src/{cli → app/cli}/groupCoreCommands.js +2 -2
  67. package/src/app/cli/index.js +9 -0
  68. package/src/{cli → app/cli}/onlineCoreCommands.js +5 -5
  69. package/src/{cli.js → app/cli/run.js} +59 -57
  70. package/src/app/index.js +6 -0
  71. package/src/code/agent.js +10 -9
  72. package/src/code/index.js +2 -0
  73. package/src/code/launcher/index.js +9 -0
  74. package/src/{agent → code/launcher}/ucode.js +7 -8
  75. package/src/{agent → code/launcher}/ucodeBootstrap.js +3 -3
  76. package/src/{agent → code/launcher}/ucodeBuild.js +2 -2
  77. package/src/{agent → code/launcher}/ucodeDoctor.js +2 -2
  78. package/src/{agent → code/launcher}/ucodeRuntimeConfig.js +1 -2
  79. package/src/code/nativeRunner.js +4 -4
  80. package/src/code/tui.js +3 -1454
  81. package/src/config.js +15 -2
  82. package/src/{bus → coordination/bus}/activate.js +2 -2
  83. package/src/{bus → coordination/bus}/daemon.js +15 -5
  84. package/src/coordination/bus/envelope.js +173 -0
  85. package/src/{bus → coordination/bus}/index.js +7 -3
  86. package/src/{bus → coordination/bus}/inject.js +11 -3
  87. package/src/{bus → coordination/bus}/message.js +1 -1
  88. package/src/coordination/bus/messageMeta.js +130 -0
  89. package/src/coordination/bus/promptEnvelope.js +65 -0
  90. package/src/{bus → coordination/bus}/shake.js +1 -1
  91. package/src/{bus → coordination/bus}/store.js +3 -3
  92. package/src/{bus → coordination/bus}/subscriber.js +2 -2
  93. package/src/{bus → coordination/bus}/utils.js +2 -2
  94. package/src/{history → coordination/history}/inputTimeline.js +5 -5
  95. package/src/coordination/index.js +10 -0
  96. package/src/{memory → coordination/memory}/historySearch.js +1 -1
  97. package/src/{memory → coordination/memory}/index.js +3 -3
  98. package/src/{report → coordination/report}/store.js +2 -2
  99. package/src/{status → coordination/status}/index.js +3 -3
  100. package/src/online/bridge.js +2 -2
  101. package/src/{controller → orchestration/controller}/flags.js +1 -1
  102. package/src/{controller → orchestration/controller}/gateRouter.js +1 -1
  103. package/src/orchestration/controller/index.js +10 -0
  104. package/src/{controller → orchestration/controller}/shadowGuard.js +1 -1
  105. package/src/orchestration/groups/bootstrap.js +3 -0
  106. package/src/orchestration/groups/index.js +10 -0
  107. package/src/orchestration/groups/promptProfiles.js +3 -0
  108. package/src/{group → orchestration/groups}/templates.js +1 -1
  109. package/src/{group → orchestration/groups}/validateTemplate.js +1 -1
  110. package/src/orchestration/index.js +7 -0
  111. package/src/orchestration/solo/index.js +3 -0
  112. package/src/{daemon → runtime/daemon}/agentProcessManager.js +1 -1
  113. package/src/{daemon → runtime/daemon}/cronOps.js +3 -2
  114. package/src/{daemon → runtime/daemon}/groupOrchestrator.js +26 -9
  115. package/src/{daemon → runtime/daemon}/index.js +105 -53
  116. package/src/{daemon → runtime/daemon}/ipcServer.js +1 -1
  117. package/src/{daemon → runtime/daemon}/nicknameScope.js +6 -3
  118. package/src/{daemon → runtime/daemon}/ops.js +48 -61
  119. package/src/{daemon → runtime/daemon}/promptLoop.js +1 -1
  120. package/src/{daemon → runtime/daemon}/promptRequest.js +7 -7
  121. package/src/runtime/daemon/providerSessions.js +230 -0
  122. package/src/{daemon → runtime/daemon}/reporting.js +4 -4
  123. package/src/{daemon → runtime/daemon}/run.js +4 -4
  124. package/src/{daemon → runtime/daemon}/soloBootstrap.js +7 -7
  125. package/src/{daemon → runtime/daemon}/status.js +5 -5
  126. package/src/runtime/index.js +10 -0
  127. package/src/{projects → runtime/projects}/registry.js +1 -1
  128. package/src/{terminal → runtime/terminal}/adapterRouter.js +0 -10
  129. package/src/{terminal → runtime/terminal}/adapters/internalAdapter.js +0 -4
  130. package/src/tools/handlers/common.js +1 -1
  131. package/src/tools/handlers/listAgents.js +1 -1
  132. package/src/tools/handlers/memory.js +3 -3
  133. package/src/tools/handlers/readBusSummary.js +1 -1
  134. package/src/tools/handlers/readOpenDecisions.js +1 -1
  135. package/src/tools/handlers/readProjectRegistry.js +1 -1
  136. package/src/tools/handlers/readPromptHistory.js +2 -2
  137. package/src/tools/schemaFixtures.js +1 -1
  138. package/src/ui/MIGRATION.md +42 -88
  139. package/src/ui/format/index.js +5 -28
  140. package/src/ui/index.js +1 -1
  141. package/src/ui/{components → ink}/ChatApp.js +812 -88
  142. package/src/ui/ink/DashboardBar.js +685 -0
  143. package/src/ui/{components → ink}/MultilineInput.js +230 -5
  144. package/src/ui/{components → ink}/UcodeApp.js +16 -7
  145. package/src/ui/{components → ink}/agentMirror.js +24 -19
  146. package/src/ui/{components → ink}/chatReducer.js +29 -7
  147. package/src/bus/messageMeta.js +0 -52
  148. package/src/chat/agentViewController.js +0 -1072
  149. package/src/chat/chatLogController.js +0 -138
  150. package/src/chat/completionController.js +0 -533
  151. package/src/chat/dashboardKeyController.js +0 -533
  152. package/src/chat/index.js +0 -2222
  153. package/src/chat/inputHistoryController.js +0 -135
  154. package/src/chat/inputListenerController.js +0 -470
  155. package/src/chat/layout.js +0 -186
  156. package/src/chat/pasteController.js +0 -81
  157. package/src/chat/statusLineController.js +0 -223
  158. package/src/chat/streamTracker.js +0 -156
  159. package/src/code/config +0 -0
  160. package/src/daemon/providerSessions.js +0 -488
  161. package/src/terminal/adapters/internalPtyAdapter.js +0 -42
  162. package/src/ui/components/DashboardBar.js +0 -417
  163. /package/src/{code/prompts → agents/prompts/native}/actions.js +0 -0
  164. /package/src/{code/prompts → agents/prompts/native}/efficiency.js +0 -0
  165. /package/src/{code/prompts → agents/prompts/native}/environment.js +0 -0
  166. /package/src/{code/prompts → agents/prompts/native}/identity.js +0 -0
  167. /package/src/{code/prompts → agents/prompts/native}/safety.js +0 -0
  168. /package/src/{code/prompts → agents/prompts/native}/sections.js +0 -0
  169. /package/src/{code/prompts → agents/prompts/native}/system.js +0 -0
  170. /package/src/{code/prompts → agents/prompts/native}/tasks.js +0 -0
  171. /package/src/{code/prompts → agents/prompts/native}/toolDescriptions/bash.js +0 -0
  172. /package/src/{code/prompts → agents/prompts/native}/toolDescriptions/edit.js +0 -0
  173. /package/src/{code/prompts → agents/prompts/native}/toolDescriptions/read.js +0 -0
  174. /package/src/{code/prompts → agents/prompts/native}/toolDescriptions/write.js +0 -0
  175. /package/src/{code/prompts → agents/prompts/native}/ufoo.js +0 -0
  176. /package/src/{group → agents/prompts}/promptProfiles.js +0 -0
  177. /package/src/{agent → agents/providers}/claudeEventTranslator.js +0 -0
  178. /package/src/{agent → agents/providers}/claudeOauthTokenReader.js +0 -0
  179. /package/src/{agent → agents/providers}/claudeSessionFiles.js +0 -0
  180. /package/src/{agent → agents/providers}/codexEventTranslator.js +0 -0
  181. /package/src/{agent → agents/providers}/credentials/claude.js +0 -0
  182. /package/src/{agent → agents/providers}/credentials/codex.js +0 -0
  183. /package/src/{agent → agents/providers}/credentials/index.js +0 -0
  184. /package/src/{chat → app/chat}/agentBar.js +0 -0
  185. /package/src/{chat → app/chat}/agentDirectory.js +0 -0
  186. /package/src/{chat → app/chat}/cronScheduler.js +0 -0
  187. /package/src/{chat → app/chat}/daemonCoordinator.js +0 -0
  188. /package/src/{chat → app/chat}/daemonReconnect.js +0 -0
  189. /package/src/{chat → app/chat}/daemonTransport.js +0 -0
  190. /package/src/{chat → app/chat}/daemonTransportDefaults.js +0 -0
  191. /package/src/{chat → app/chat}/inputMath.js +0 -0
  192. /package/src/{chat → app/chat}/projectCloseController.js +0 -0
  193. /package/src/{chat → app/chat}/rawKeyMap.js +0 -0
  194. /package/src/{chat → app/chat}/settingsController.js +0 -0
  195. /package/src/{chat → app/chat}/shellCommand.js +0 -0
  196. /package/src/{chat → app/chat}/text.js +0 -0
  197. /package/src/{chat → app/chat}/transientAgentState.js +0 -0
  198. /package/src/{cli → app/cli}/busCoreCommands.js +0 -0
  199. /package/src/{skills/index.js → app/cli/features/skills.js} +0 -0
  200. /package/src/{bus → coordination/bus}/nickname.js +0 -0
  201. /package/src/{bus → coordination/bus}/queue.js +0 -0
  202. /package/src/{context → coordination/context}/decisions.js +0 -0
  203. /package/src/{context → coordination/context}/doctor.js +0 -0
  204. /package/src/{context → coordination/context}/index.js +0 -0
  205. /package/src/{context → coordination/context}/sync.js +0 -0
  206. /package/src/{ufoo → coordination/state}/agentRegistryDiagnostics.js +0 -0
  207. /package/src/{ufoo → coordination/state}/agentsStore.js +0 -0
  208. /package/src/{ufoo → coordination/state}/paths.js +0 -0
  209. /package/src/{controller → orchestration/controller}/launchRouting.js +0 -0
  210. /package/src/{controller → orchestration/controller}/routerFastPath.js +0 -0
  211. /package/src/{controller → orchestration/controller}/routerFinalize.js +0 -0
  212. /package/src/{group → orchestration/groups}/diagram.js +0 -0
  213. /package/src/{group → orchestration/groups}/templateValidation.js +0 -0
  214. /package/src/{solo → orchestration/solo}/commands.js +0 -0
  215. /package/src/{shared → runtime/contracts}/eventContract.js +0 -0
  216. /package/src/{shared → runtime/contracts}/ptySocketContract.js +0 -0
  217. /package/src/{providerapi → runtime/privacy}/redactor.js +0 -0
  218. /package/src/{providerapi → runtime/privacy}/shadowDiff.js +0 -0
  219. /package/src/{utils → runtime/process}/nodeExecutable.js +0 -0
  220. /package/src/{projects → runtime/projects}/identity.js +0 -0
  221. /package/src/{projects → runtime/projects}/index.js +0 -0
  222. /package/src/{projects → runtime/projects}/projectId.js +0 -0
  223. /package/src/{projects → runtime/projects}/runtimes.js +0 -0
  224. /package/src/{terminal → runtime/terminal}/adapterContract.js +0 -0
  225. /package/src/{terminal → runtime/terminal}/adapters/externalAdapter.js +0 -0
  226. /package/src/{terminal → runtime/terminal}/adapters/hostAdapter.js +0 -0
  227. /package/src/{terminal → runtime/terminal}/adapters/internalQueueAdapter.js +0 -0
  228. /package/src/{terminal → runtime/terminal}/adapters/terminalAdapter.js +0 -0
  229. /package/src/{terminal → runtime/terminal}/adapters/tmuxAdapter.js +0 -0
  230. /package/src/{terminal → runtime/terminal}/detect.js +0 -0
  231. /package/src/{terminal → runtime/terminal}/index.js +0 -0
  232. /package/src/{terminal → runtime/terminal}/iterm2.js +0 -0
  233. /package/src/{utils → ui/format}/banner.js +0 -0
  234. /package/src/{shared → ui/format}/markdownRenderer.js +0 -0
  235. /package/src/ui/{components → ink}/InkDemo.js +0 -0
@@ -3,10 +3,8 @@
3
3
  /**
4
4
  * Multiline text input for the ink-based ucode TUI.
5
5
  *
6
- * Mirrors the behaviour of the blessed `_listener` in src/code/tui.js, but
7
- * built on ink's useInput. Cursor math is delegated to src/ui/format so the
8
- * legacy and ink editors stay in sync (and so the existing jest coverage of
9
- * those helpers protects this component too).
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.
10
8
  *
11
9
  * Props:
12
10
  * value (string) text contents (controlled)
@@ -39,9 +37,108 @@
39
37
 
40
38
  const fmt = require("../format");
41
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
+
42
139
  function createMultilineInput({ React, ink }) {
43
140
  const { useState, useCallback, useMemo, useEffect } = React;
44
- const { Box, Text, useInput } = ink;
141
+ const { Box, Text, useInput, useStdout } = ink;
45
142
  const h = React.createElement;
46
143
 
47
144
  return function MultilineInput({
@@ -61,6 +158,12 @@ function createMultilineInput({ React, ink }) {
61
158
  promptPrefix = "› ",
62
159
  promptColor = "magenta",
63
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,
64
167
  }) {
65
168
  // Cursor is owned by this component. preferredCol tracks the visual
66
169
  // column we want to keep when bouncing across lines of different widths
@@ -300,6 +403,128 @@ function createMultilineInput({ React, ink }) {
300
403
  [value, wrapWidth, cursorPos]
301
404
  );
302
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
+
303
528
  return h(Box, {
304
529
  borderStyle: "single",
305
530
  borderTop: true,
@@ -1,11 +1,9 @@
1
1
  "use strict";
2
2
 
3
3
  /**
4
- * Ink-based ucode TUI. Behaviourally equivalent to runUcodeBlessedTui in
5
- * src/code/tui.js but rendered via React + ink.
4
+ * Ink-based ucode TUI rendered via React + ink.
6
5
  *
7
- * Activation: Ink is the default ucode TUI. Set UFOO_TUI=blessed to use
8
- * the legacy blessed renderer while it remains available as a fallback.
6
+ * Activation: this is the only ucode TUI.
9
7
  *
10
8
  * Coverage today: banner, scrolling log via <Static>, tool-call merge with
11
9
  * Ctrl+O expand, multiline editor (see MultilineInput.js), spinner+phase
@@ -22,7 +20,7 @@ const { createMultilineInput } = require("./MultilineInput");
22
20
 
23
21
  function createUcodeApp({ React, ink, props, interactive = true }) {
24
22
  const { useEffect, useState, useCallback, useRef } = React;
25
- const { Box, Text, Static, useInput, useApp, useStdout } = ink;
23
+ const { Box, Text, useInput, useApp, useStdout } = ink;
26
24
  const h = React.createElement;
27
25
  const MultilineInput = createMultilineInput({ React, ink });
28
26
 
@@ -38,6 +36,7 @@ function createUcodeApp({ React, ink, props, interactive = true }) {
38
36
  banner.concat([""]).map((line, idx) => ({ id: `b-${idx}`, text: line }))
39
37
  );
40
38
  const [draft, setDraft] = useState("");
39
+ const [draftVersion, setDraftVersion] = useState(0);
41
40
  // status: idle when message === "". `type` picks a STATUS_INDICATORS
42
41
  // bucket; `showTimer` and `startedAt` reproduce the blessed spinner
43
42
  // controls. The BG suffix is computed from backgroundTasksRef and
@@ -181,6 +180,7 @@ function createUcodeApp({ React, ink, props, interactive = true }) {
181
180
  if (transition.moved) {
182
181
  setHistoryIndex(transition.nextHistoryIndex);
183
182
  setDraft(transition.nextValue);
183
+ setDraftVersion((v) => v + 1);
184
184
  return;
185
185
  }
186
186
  }
@@ -204,6 +204,7 @@ function createUcodeApp({ React, ink, props, interactive = true }) {
204
204
  if (nextIndex !== historyIndex || draft !== inputHistory[nextIndex]) {
205
205
  setHistoryIndex(nextIndex);
206
206
  setDraft(inputHistory[nextIndex] || "");
207
+ setDraftVersion((v) => v + 1);
207
208
  return;
208
209
  }
209
210
  }
@@ -628,6 +629,7 @@ function createUcodeApp({ React, ink, props, interactive = true }) {
628
629
  const trimmed = value.trim();
629
630
  if (!trimmed) return;
630
631
  setDraft("");
632
+ setDraftVersion((v) => v + 1);
631
633
  setInputHistory((prev) => {
632
634
  const next = prev.concat([trimmed]).slice(-200);
633
635
  setHistoryIndex(next.length);
@@ -668,8 +670,10 @@ function createUcodeApp({ React, ink, props, interactive = true }) {
668
670
  }, { isActive: interactive });
669
671
 
670
672
  return h(Box, { flexDirection: "column", width: "100%" },
671
- h(Static, { items: logLines }, (item) =>
672
- h(Text, { key: item.id }, item.text || " ")
673
+ h(Box, { flexDirection: "column", width: "100%" },
674
+ ...logLines.map((item) =>
675
+ h(Text, { key: item.id }, item.text || " ")
676
+ )
673
677
  ),
674
678
  activeMerge ? h(Box, null,
675
679
  h(Text, { color: activeMerge.entries.some((e) => e.isError) ? "red" : "cyan" },
@@ -684,6 +688,7 @@ function createUcodeApp({ React, ink, props, interactive = true }) {
684
688
  h(Box, { width: "100%" },
685
689
  h(MultilineInput, {
686
690
  value: draft,
691
+ valueVersion: draftVersion,
687
692
  onChange: (next) => setDraft(next),
688
693
  onSubmit: (value) => submit(value),
689
694
  onCancel: () => {
@@ -715,6 +720,10 @@ function createUcodeApp({ React, ink, props, interactive = true }) {
715
720
  interactive,
716
721
  placeholder: "",
717
722
  promptPrefix: targetAgent ? `›@${getAgentLabel(targetAgent)} ` : "› ",
723
+ // The agents footer is rendered below the input. Matching chat's
724
+ // IME parking contract keeps the hardware cursor aligned with the
725
+ // inverse caret instead of drifting to the bottom of the frame.
726
+ linesBelowInput: 1,
718
727
  }),
719
728
  ),
720
729
  h(Box, { width: "100%" },
@@ -15,10 +15,10 @@
15
15
  * Returns a stop() function that the caller invokes on exit.
16
16
  */
17
17
 
18
- const { createAgentSockets } = require("../../chat/agentSockets");
19
- const { loadInternalAgentLogHistory } = require("../../chat/internalAgentLogHistory");
20
- const { IPC_REQUEST_TYPES, IPC_RESPONSE_TYPES } = require("../../shared/eventContract");
21
- const { getUfooPaths } = require("../../ufoo/paths");
18
+ const { createAgentSockets } = require("../../app/chat/agentSockets");
19
+ const { loadInternalAgentLogHistory } = require("../../app/chat/internalAgentLogHistory");
20
+ const { IPC_REQUEST_TYPES, IPC_RESPONSE_TYPES } = require("../../runtime/contracts/eventContract");
21
+ const { getUfooPaths } = require("../../coordination/state/paths");
22
22
  const os = require("os");
23
23
  const path = require("path");
24
24
  const readline = require("readline");
@@ -217,10 +217,8 @@ function startAgentMirror({
217
217
  const cols = stdout.columns || 80;
218
218
  const rows = stdout.rows || 24;
219
219
 
220
- // Mirror the lookup that runChatBlessed uses:
221
- // <bus-queues-dir>/<safeName>/inject.sock. We sanitise the agent id the
222
- // same way so a daemon launched by either TUI is reachable from the
223
- // other.
220
+ // Mirror the daemon socket lookup:
221
+ // <bus-queues-dir>/<safeName>/inject.sock.
224
222
  const safeName = String(agentId || "").replace(/[^A-Za-z0-9_-]/g, "_");
225
223
  const sockPath = path.join(
226
224
  getUfooPaths(projectRoot || process.cwd()).busQueuesDir,
@@ -247,7 +245,7 @@ function startAgentMirror({
247
245
  // Clear screen + reserve a 1-line bar at the bottom for our exit hint.
248
246
  writeOut("\x1b[2J\x1b[H");
249
247
  writeOut(`\x1b[1;${Math.max(1, rows - 1)}r`);
250
- writeOut(`\x1b[${rows};1H\x1b[7m esc \x1b[0m return to chat · attached to ${agentId}`);
248
+ writeOut(`\x1b[${rows};1H\x1b[7m esc esc \x1b[0m return to chat · attached to ${agentId}`);
251
249
  writeOut("\x1b[H");
252
250
 
253
251
  sockets.connectOutput(sockPath);
@@ -259,23 +257,30 @@ function startAgentMirror({
259
257
  if (typeof stdin.setRawMode === "function") stdin.setRawMode(true);
260
258
  stdin.resume();
261
259
 
260
+ let escCount = 0;
261
+ let escTimer = null;
262
+
262
263
  const onData = (chunk) => {
263
264
  if (stopped) return;
264
- // Esc on its own (single 0x1b byte, no follow-up) exits the mirror.
265
- // We can't perfectly distinguish a bare Esc from the start of an
266
- // arrow-key sequence; the convention here is "Esc + nothing within
267
- // 50ms means leave". Anything else gets forwarded as-is.
268
265
  if (chunk.length === 1 && chunk[0] === 0x1b) {
269
- setTimeout(() => {
270
- if (!stopped && pendingEsc === chunk) stop();
271
- }, 50);
272
- pendingEsc = chunk;
266
+ escCount += 1;
267
+ if (escCount >= 2) {
268
+ clearTimeout(escTimer);
269
+ escCount = 0;
270
+ stop();
271
+ return;
272
+ }
273
+ escTimer = setTimeout(() => { escCount = 0; }, 300);
274
+ return;
275
+ }
276
+ if (escCount > 0) {
277
+ clearTimeout(escTimer);
278
+ escCount = 0;
279
+ sockets.sendRaw(Buffer.concat([Buffer.from([0x1b]), chunk]));
273
280
  return;
274
281
  }
275
- pendingEsc = null;
276
282
  sockets.sendRaw(chunk);
277
283
  };
278
- let pendingEsc = null;
279
284
 
280
285
  const onResize = () => {
281
286
  if (stopped) return;
@@ -42,6 +42,7 @@ const DASHBOARD_VIEWS = ["projects", "agents", "mode", "provider", "cron"];
42
42
  const DEFAULT_PROVIDER_OPTIONS = [
43
43
  { label: "codex", value: "codex-cli" },
44
44
  { label: "claude", value: "claude-cli" },
45
+ { label: "agy", value: "agy-cli" },
45
46
  ];
46
47
  function projectRootOf(row = {}) {
47
48
  return String((row && (row.root || row.project_root || row.projectRoot)) || "");
@@ -68,9 +69,10 @@ function createInitialState({ banner = [], globalMode = false, globalScope = "co
68
69
  selectedProjectIndex: -1,
69
70
  selectedProjectRoot: "",
70
71
  projectListWindowStart: 0,
72
+ emptyProjectsDownArmed: false,
71
73
  activeProjectRoot: "",
72
- modeOptions: ["auto", "host", "terminal", "tmux", "internal-pty", "internal"],
73
- selectedModeIndex: Math.max(0, ["auto", "host", "terminal", "tmux", "internal-pty", "internal"].indexOf(initialLaunchMode)),
74
+ modeOptions: ["auto", "host", "terminal", "tmux", "internal"],
75
+ selectedModeIndex: Math.max(0, ["auto", "host", "terminal", "tmux", "internal"].indexOf(initialLaunchMode)),
74
76
  providerOptions: DEFAULT_PROVIDER_OPTIONS,
75
77
  selectedProviderIndex,
76
78
  cronTasks: [],
@@ -132,11 +134,27 @@ function reducer(state, action) {
132
134
  case "draft/clear":
133
135
  return { ...state, draft: "" };
134
136
  case "focus/toggle":
135
- return { ...state, focusMode: state.focusMode === "input" ? "dashboard" : "input" };
137
+ return {
138
+ ...state,
139
+ focusMode: state.focusMode === "input" ? "dashboard" : "input",
140
+ emptyProjectsDownArmed: state.focusMode === "input" ? state.emptyProjectsDownArmed : false,
141
+ };
136
142
  case "focus/set":
137
- return { ...state, focusMode: action.mode === "dashboard" ? "dashboard" : "input" };
138
- case "view/set":
139
- return { ...state, dashboardView: action.view };
143
+ return {
144
+ ...state,
145
+ focusMode: action.mode === "dashboard" ? "dashboard" : "input",
146
+ emptyProjectsDownArmed: action.mode === "dashboard" ? state.emptyProjectsDownArmed : false,
147
+ };
148
+ case "view/set": {
149
+ const view = action.view;
150
+ const inAgentsView = view === "agents";
151
+ return {
152
+ ...state,
153
+ dashboardView: view,
154
+ agentSelectionMode: inAgentsView && state.focusMode === "dashboard" && state.selectedAgentIndex >= 0,
155
+ emptyProjectsDownArmed: view === "projects" ? state.emptyProjectsDownArmed : false,
156
+ };
157
+ }
140
158
  case "view/cycle": {
141
159
  const i = DASHBOARD_VIEWS.indexOf(state.dashboardView);
142
160
  const direction = action.direction === "left" ? -1 : 1;
@@ -204,6 +222,7 @@ function reducer(state, action) {
204
222
  selectedProjectRoot: selectedIndex >= 0 ? selectedRoot : "",
205
223
  selectedProjectIndex: selectedIndex,
206
224
  activeProjectRoot: action.activeProjectRoot || state.activeProjectRoot,
225
+ emptyProjectsDownArmed: list.length === 0 ? state.emptyProjectsDownArmed : false,
207
226
  };
208
227
  }
209
228
  case "projects/select":
@@ -211,9 +230,12 @@ function reducer(state, action) {
211
230
  ...state,
212
231
  selectedProjectIndex: action.index,
213
232
  selectedProjectRoot: String(action.projectRoot || projectRootOf(state.projects[action.index]) || ""),
233
+ emptyProjectsDownArmed: false,
214
234
  };
215
235
  case "projects/clearSelection":
216
- return { ...state, selectedProjectIndex: -1, selectedProjectRoot: "" };
236
+ return { ...state, selectedProjectIndex: -1, selectedProjectRoot: "", emptyProjectsDownArmed: false };
237
+ case "projects/armEmptyDown":
238
+ return { ...state, emptyProjectsDownArmed: true };
217
239
  case "projects/window":
218
240
  return { ...state, projectListWindowStart: Math.max(0, action.windowStart | 0) };
219
241
  case "scope/set":
@@ -1,52 +0,0 @@
1
- "use strict";
2
-
3
- const INJECTION_MODES = {
4
- IMMEDIATE: "immediate",
5
- QUEUED: "queued",
6
- };
7
-
8
- function normalizeInjectionMode(value, fallback = INJECTION_MODES.IMMEDIATE) {
9
- const raw = String(value || "").trim().toLowerCase();
10
- if (raw === INJECTION_MODES.QUEUED) return INJECTION_MODES.QUEUED;
11
- if (raw === INJECTION_MODES.IMMEDIATE) return INJECTION_MODES.IMMEDIATE;
12
- return fallback;
13
- }
14
-
15
- function normalizeMessageSource(value) {
16
- const raw = String(value || "").trim();
17
- return raw || "";
18
- }
19
-
20
- function buildMessageData(message, options = {}) {
21
- const base = options && typeof options.data === "object" && options.data
22
- ? { ...options.data }
23
- : {};
24
- const data = { ...base, message };
25
- data.injection_mode = normalizeInjectionMode(
26
- options.injectionMode || data.injection_mode,
27
- INJECTION_MODES.IMMEDIATE,
28
- );
29
- const source = normalizeMessageSource(options.source || data.source);
30
- if (source) {
31
- data.source = source;
32
- } else {
33
- delete data.source;
34
- }
35
- return data;
36
- }
37
-
38
- function getInjectionModeFromEvent(evt, fallback = INJECTION_MODES.IMMEDIATE) {
39
- const data = evt && typeof evt.data === "object" && evt.data ? evt.data : {};
40
- return normalizeInjectionMode(
41
- data.injection_mode || evt?.injection_mode,
42
- fallback,
43
- );
44
- }
45
-
46
- module.exports = {
47
- INJECTION_MODES,
48
- normalizeInjectionMode,
49
- normalizeMessageSource,
50
- buildMessageData,
51
- getInjectionModeFromEvent,
52
- };