u-foo 1.0.6 → 1.1.9

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 (149) hide show
  1. package/README.md +44 -4
  2. package/SKILLS/ufoo/SKILL.md +17 -2
  3. package/SKILLS/uinit/SKILL.md +8 -3
  4. package/bin/ucode-core.js +15 -0
  5. package/bin/ucode.js +125 -0
  6. package/bin/ufoo-assistant-agent.js +5 -0
  7. package/bin/ufoo-engine.js +25 -0
  8. package/bin/ufoo.js +4 -0
  9. package/modules/AGENTS.template.md +14 -4
  10. package/modules/bus/README.md +8 -5
  11. package/modules/bus/SKILLS/ubus/SKILL.md +5 -4
  12. package/modules/context/SKILLS/uctx/SKILL.md +3 -1
  13. package/modules/online/SKILLS/ufoo-online/SKILL.md +144 -0
  14. package/package.json +12 -3
  15. package/scripts/import-pi-mono.js +124 -0
  16. package/scripts/postinstall.js +20 -49
  17. package/scripts/sync-claude-skills.sh +21 -0
  18. package/src/agent/cliRunner.js +524 -31
  19. package/src/agent/internalRunner.js +76 -9
  20. package/src/agent/launcher.js +97 -45
  21. package/src/agent/normalizeOutput.js +1 -1
  22. package/src/agent/notifier.js +144 -4
  23. package/src/agent/ptyRunner.js +480 -10
  24. package/src/agent/ptyWrapper.js +28 -3
  25. package/src/agent/readyDetector.js +16 -0
  26. package/src/agent/ucode.js +443 -0
  27. package/src/agent/ucodeBootstrap.js +113 -0
  28. package/src/agent/ucodeBuild.js +67 -0
  29. package/src/agent/ucodeDoctor.js +184 -0
  30. package/src/agent/ucodeRuntimeConfig.js +129 -0
  31. package/src/agent/ufooAgent.js +11 -2
  32. package/src/assistant/agent.js +260 -0
  33. package/src/assistant/bridge.js +172 -0
  34. package/src/assistant/engine.js +252 -0
  35. package/src/assistant/stdio.js +58 -0
  36. package/src/assistant/ufooEngineCli.js +306 -0
  37. package/src/bus/activate.js +27 -11
  38. package/src/bus/daemon.js +133 -5
  39. package/src/bus/index.js +137 -80
  40. package/src/bus/inject.js +47 -17
  41. package/src/bus/message.js +145 -17
  42. package/src/bus/nickname.js +3 -1
  43. package/src/bus/queue.js +6 -1
  44. package/src/bus/store.js +189 -0
  45. package/src/bus/subscriber.js +20 -4
  46. package/src/bus/utils.js +9 -3
  47. package/src/chat/agentBar.js +117 -0
  48. package/src/chat/agentDirectory.js +88 -0
  49. package/src/chat/agentSockets.js +225 -0
  50. package/src/chat/agentViewController.js +298 -0
  51. package/src/chat/chatLogController.js +115 -0
  52. package/src/chat/commandExecutor.js +700 -0
  53. package/src/chat/commands.js +132 -0
  54. package/src/chat/completionController.js +414 -0
  55. package/src/chat/cronScheduler.js +160 -0
  56. package/src/chat/daemonConnection.js +166 -0
  57. package/src/chat/daemonCoordinator.js +64 -0
  58. package/src/chat/daemonMessageRouter.js +257 -0
  59. package/src/chat/daemonReconnect.js +41 -0
  60. package/src/chat/daemonTransport.js +36 -0
  61. package/src/chat/daemonTransportDefaults.js +10 -0
  62. package/src/chat/dashboardKeyController.js +480 -0
  63. package/src/chat/dashboardView.js +154 -0
  64. package/src/chat/index.js +935 -2909
  65. package/src/chat/inputHistoryController.js +105 -0
  66. package/src/chat/inputListenerController.js +304 -0
  67. package/src/chat/inputMath.js +104 -0
  68. package/src/chat/inputSubmitHandler.js +171 -0
  69. package/src/chat/layout.js +165 -0
  70. package/src/chat/pasteController.js +81 -0
  71. package/src/chat/rawKeyMap.js +42 -0
  72. package/src/chat/settingsController.js +132 -0
  73. package/src/chat/statusLineController.js +177 -0
  74. package/src/chat/streamTracker.js +138 -0
  75. package/src/chat/text.js +70 -0
  76. package/src/chat/transport.js +61 -0
  77. package/src/cli/busCoreCommands.js +59 -0
  78. package/src/cli/ctxCoreCommands.js +199 -0
  79. package/src/cli/onlineCoreCommands.js +379 -0
  80. package/src/cli.js +741 -238
  81. package/src/code/README.md +29 -0
  82. package/src/code/UCODE_PROMPT.md +32 -0
  83. package/src/code/agent.js +1651 -0
  84. package/src/code/cli.js +158 -0
  85. package/src/code/config +0 -0
  86. package/src/code/dispatch.js +42 -0
  87. package/src/code/index.js +70 -0
  88. package/src/code/nativeRunner.js +1213 -0
  89. package/src/code/runtime.js +154 -0
  90. package/src/code/sessionStore.js +162 -0
  91. package/src/code/taskDecomposer.js +269 -0
  92. package/src/code/tools/bash.js +53 -0
  93. package/src/code/tools/common.js +42 -0
  94. package/src/code/tools/edit.js +70 -0
  95. package/src/code/tools/read.js +44 -0
  96. package/src/code/tools/write.js +35 -0
  97. package/src/code/tui.js +1580 -0
  98. package/src/config.js +47 -1
  99. package/src/context/decisions.js +12 -2
  100. package/src/context/index.js +18 -1
  101. package/src/context/sync.js +127 -0
  102. package/src/daemon/agentProcessManager.js +74 -0
  103. package/src/daemon/cronOps.js +241 -0
  104. package/src/daemon/index.js +661 -488
  105. package/src/daemon/ipcServer.js +99 -0
  106. package/src/daemon/ops.js +417 -179
  107. package/src/daemon/promptLoop.js +319 -0
  108. package/src/daemon/promptRequest.js +101 -0
  109. package/src/daemon/providerSessions.js +32 -17
  110. package/src/daemon/reporting.js +90 -0
  111. package/src/daemon/run.js +2 -5
  112. package/src/daemon/status.js +24 -1
  113. package/src/init/index.js +68 -14
  114. package/src/online/bridge.js +663 -0
  115. package/src/online/client.js +245 -0
  116. package/src/online/runner.js +253 -0
  117. package/src/online/server.js +992 -0
  118. package/src/online/tokens.js +103 -0
  119. package/src/report/store.js +331 -0
  120. package/src/shared/eventContract.js +35 -0
  121. package/src/shared/ptySocketContract.js +21 -0
  122. package/src/status/index.js +50 -17
  123. package/src/terminal/adapterContract.js +87 -0
  124. package/src/terminal/adapterRouter.js +84 -0
  125. package/src/terminal/adapters/externalAdapter.js +14 -0
  126. package/src/terminal/adapters/internalAdapter.js +13 -0
  127. package/src/terminal/adapters/internalPtyAdapter.js +42 -0
  128. package/src/terminal/adapters/internalQueueAdapter.js +37 -0
  129. package/src/terminal/adapters/terminalAdapter.js +31 -0
  130. package/src/terminal/adapters/tmuxAdapter.js +30 -0
  131. package/src/ufoo/agentsStore.js +69 -3
  132. package/src/utils/banner.js +5 -2
  133. package/scripts/.archived/bash-to-js-migration/README.md +0 -46
  134. package/scripts/.archived/bash-to-js-migration/banner.sh +0 -89
  135. package/scripts/.archived/bash-to-js-migration/bus-alert.sh +0 -6
  136. package/scripts/.archived/bash-to-js-migration/bus-autotrigger.sh +0 -6
  137. package/scripts/.archived/bash-to-js-migration/bus-daemon.sh +0 -231
  138. package/scripts/.archived/bash-to-js-migration/bus-inject.sh +0 -176
  139. package/scripts/.archived/bash-to-js-migration/bus-listen.sh +0 -6
  140. package/scripts/.archived/bash-to-js-migration/bus.sh +0 -986
  141. package/scripts/.archived/bash-to-js-migration/context-decisions.sh +0 -167
  142. package/scripts/.archived/bash-to-js-migration/context-doctor.sh +0 -72
  143. package/scripts/.archived/bash-to-js-migration/context-lint.sh +0 -110
  144. package/scripts/.archived/bash-to-js-migration/doctor.sh +0 -22
  145. package/scripts/.archived/bash-to-js-migration/init.sh +0 -247
  146. package/scripts/.archived/bash-to-js-migration/skills.sh +0 -113
  147. package/scripts/.archived/bash-to-js-migration/status.sh +0 -125
  148. package/scripts/banner.sh +0 -2
  149. package/src/bus/API_DESIGN.md +0 -204
@@ -0,0 +1,105 @@
1
+ const fs = require("fs");
2
+
3
+ function createInputHistoryController(options = {}) {
4
+ const {
5
+ inputHistoryFile,
6
+ historyDir,
7
+ setInputValue = () => {},
8
+ getInputValue = () => "",
9
+ fsMod = fs,
10
+ } = options;
11
+
12
+ if (!inputHistoryFile || !historyDir) {
13
+ throw new Error("createInputHistoryController requires inputHistoryFile and historyDir");
14
+ }
15
+
16
+ const inputHistory = [];
17
+ let historyIndex = 0;
18
+ let historyDraft = "";
19
+
20
+ function appendInputHistory(text) {
21
+ if (!text) return;
22
+ fsMod.mkdirSync(historyDir, { recursive: true });
23
+ fsMod.appendFileSync(inputHistoryFile, `${JSON.stringify({ text })}\n`);
24
+ }
25
+
26
+ function loadInputHistory(limit = 2000) {
27
+ try {
28
+ const raw = fsMod.readFileSync(inputHistoryFile, "utf8");
29
+ const lines = String(raw || "").trim().split(/\r?\n/).filter(Boolean);
30
+ const items = lines.slice(-limit).map((line) => JSON.parse(line));
31
+ for (const item of items) {
32
+ if (item && typeof item.text === "string" && item.text.trim() !== "") {
33
+ inputHistory.push(item.text);
34
+ }
35
+ }
36
+ } catch {
37
+ // ignore missing/invalid history
38
+ }
39
+ historyIndex = inputHistory.length;
40
+ }
41
+
42
+ function updateDraftFromInput() {
43
+ if (historyIndex === inputHistory.length) {
44
+ historyDraft = getInputValue();
45
+ }
46
+ }
47
+
48
+ function setIndexToEnd() {
49
+ historyIndex = inputHistory.length;
50
+ historyDraft = "";
51
+ }
52
+
53
+ function historyUp() {
54
+ if (inputHistory.length === 0) return false;
55
+ if (historyIndex === inputHistory.length) {
56
+ historyDraft = getInputValue();
57
+ }
58
+ if (historyIndex > 0) {
59
+ historyIndex -= 1;
60
+ setInputValue(inputHistory[historyIndex]);
61
+ return true;
62
+ }
63
+ return true;
64
+ }
65
+
66
+ function historyDown() {
67
+ if (inputHistory.length === 0) return false;
68
+ if (historyIndex < inputHistory.length - 1) {
69
+ historyIndex += 1;
70
+ setInputValue(inputHistory[historyIndex]);
71
+ return true;
72
+ }
73
+ if (historyIndex === inputHistory.length - 1) {
74
+ historyIndex = inputHistory.length;
75
+ setInputValue(historyDraft || "");
76
+ return true;
77
+ }
78
+ return false;
79
+ }
80
+
81
+ function commitSubmittedText(text) {
82
+ if (!text) return;
83
+ inputHistory.push(text);
84
+ appendInputHistory(text);
85
+ setIndexToEnd();
86
+ }
87
+
88
+ return {
89
+ loadInputHistory,
90
+ updateDraftFromInput,
91
+ historyUp,
92
+ historyDown,
93
+ commitSubmittedText,
94
+ setIndexToEnd,
95
+ getState: () => ({
96
+ history: [...inputHistory],
97
+ historyIndex,
98
+ historyDraft,
99
+ }),
100
+ };
101
+ }
102
+
103
+ module.exports = {
104
+ createInputHistoryController,
105
+ };
@@ -0,0 +1,304 @@
1
+ function createInputListenerController(options = {}) {
2
+ const {
3
+ getCurrentView = () => "main",
4
+ exitHandler = () => {},
5
+ getFocusMode = () => "input",
6
+ getDashboardView = () => "agents",
7
+ getSelectedAgentIndex = () => -1,
8
+ getActiveAgents = () => [],
9
+ getTargetAgent = () => null,
10
+ requestCloseAgent = () => {},
11
+ logMessage = () => {},
12
+ isSuppressKeypress = () => false,
13
+ normalizeCommandPrefix = () => {},
14
+ handleDashboardKey = () => false,
15
+ exitDashboardMode = () => {},
16
+ completionController,
17
+ getLogHeight = () => 10,
18
+ scrollLog = () => {},
19
+ insertTextAtCursor = () => {},
20
+ normalizePaste = (text) => text,
21
+ resetPreferredCol = () => {},
22
+ getCursorPos = () => 0,
23
+ setCursorPos = () => {},
24
+ ensureInputCursorVisible = () => {},
25
+ getWrapWidth = () => 0,
26
+ getCursorRowCol = () => ({ row: 0, col: 0 }),
27
+ countLines = () => 1,
28
+ getCursorPosForRowCol = () => 0,
29
+ getPreferredCol = () => null,
30
+ setPreferredCol = () => {},
31
+ historyUp = () => false,
32
+ historyDown = () => false,
33
+ enterDashboardMode = () => {},
34
+ resizeInput = () => {},
35
+ updateDraftFromInput = () => {},
36
+ } = options;
37
+
38
+ if (!completionController) {
39
+ throw new Error("createInputListenerController requires completionController");
40
+ }
41
+
42
+ function shouldShowCompletion(value = "") {
43
+ const text = String(value || "");
44
+ return text.startsWith("/") || text.startsWith("@");
45
+ }
46
+
47
+ function render(textarea) {
48
+ if (textarea && textarea.screen && typeof textarea.screen.render === "function") {
49
+ textarea.screen.render();
50
+ }
51
+ }
52
+
53
+ function updateCursor(textarea) {
54
+ if (textarea && typeof textarea._updateCursor === "function") {
55
+ textarea._updateCursor();
56
+ }
57
+ }
58
+
59
+ function handleKey(ch, key = {}, textarea) {
60
+ const keyName = key && key.name;
61
+
62
+ if (getCurrentView() === "agent") return;
63
+
64
+ if (key && key.ctrl && keyName === "c") {
65
+ exitHandler();
66
+ return;
67
+ }
68
+
69
+ if (key && key.ctrl && keyName === "x") {
70
+ const focusMode = getFocusMode();
71
+ const dashboardView = getDashboardView();
72
+ if (focusMode === "dashboard" && dashboardView !== "agents") {
73
+ handleDashboardKey(key);
74
+ return;
75
+ }
76
+ const selectedAgentIndex = getSelectedAgentIndex();
77
+ const activeAgents = getActiveAgents();
78
+ const targetAgent = getTargetAgent();
79
+ if (
80
+ focusMode === "dashboard" &&
81
+ dashboardView === "agents" &&
82
+ selectedAgentIndex >= 0 &&
83
+ selectedAgentIndex < activeAgents.length
84
+ ) {
85
+ requestCloseAgent(activeAgents[selectedAgentIndex]);
86
+ } else if (targetAgent) {
87
+ requestCloseAgent(targetAgent);
88
+ } else {
89
+ logMessage("error", "{white-fg}✗{/white-fg} No agent selected");
90
+ }
91
+ return;
92
+ }
93
+
94
+ if (isSuppressKeypress()) {
95
+ return;
96
+ }
97
+
98
+ normalizeCommandPrefix();
99
+
100
+ if (getFocusMode() === "dashboard") {
101
+ if (handleDashboardKey(key)) return;
102
+ const dashboardView = getDashboardView();
103
+ if (
104
+ dashboardView === "agents" &&
105
+ ch &&
106
+ ch.length === 1 &&
107
+ !(key && key.ctrl) &&
108
+ !(key && key.meta) &&
109
+ !/^[\x00-\x1f\x7f]$/.test(ch)
110
+ ) {
111
+ exitDashboardMode(true);
112
+ } else {
113
+ return;
114
+ }
115
+ }
116
+
117
+ if (completionController.isActive() && completionController.handleKey(ch, key)) return;
118
+
119
+ if (keyName === "pageup" || keyName === "pagedown") {
120
+ const delta = Math.max(1, Math.floor(getLogHeight() / 2));
121
+ scrollLog(keyName === "pageup" ? -delta : delta);
122
+ return;
123
+ }
124
+
125
+ if (ch && ch.length > 1 && (!keyName || keyName.length !== 1)) {
126
+ insertTextAtCursor(normalizePaste(ch));
127
+ return;
128
+ }
129
+
130
+ if (ch && (ch.includes("\n") || ch.includes("\r")) && (keyName !== "return" && keyName !== "enter")) {
131
+ insertTextAtCursor(normalizePaste(ch));
132
+ return;
133
+ }
134
+
135
+ if (keyName === "return" || keyName === "enter") {
136
+ if (key && key.shift) {
137
+ insertTextAtCursor("\n");
138
+ } else {
139
+ resetPreferredCol();
140
+ if (textarea && typeof textarea._done === "function") {
141
+ textarea._done(null, textarea.value);
142
+ }
143
+ }
144
+ return;
145
+ }
146
+
147
+ if (keyName === "left") {
148
+ const cursorPos = getCursorPos();
149
+ if (cursorPos > 0) setCursorPos(cursorPos - 1);
150
+ resetPreferredCol();
151
+ ensureInputCursorVisible();
152
+ updateCursor(textarea);
153
+ render(textarea);
154
+ return;
155
+ }
156
+
157
+ if (keyName === "right") {
158
+ const cursorPos = getCursorPos();
159
+ if (cursorPos < (textarea && textarea.value ? textarea.value.length : 0)) {
160
+ setCursorPos(cursorPos + 1);
161
+ }
162
+ resetPreferredCol();
163
+ ensureInputCursorVisible();
164
+ updateCursor(textarea);
165
+ render(textarea);
166
+ return;
167
+ }
168
+
169
+ if (keyName === "home") {
170
+ setCursorPos(0);
171
+ resetPreferredCol();
172
+ ensureInputCursorVisible();
173
+ updateCursor(textarea);
174
+ render(textarea);
175
+ return;
176
+ }
177
+
178
+ if (keyName === "end") {
179
+ setCursorPos((textarea && textarea.value ? textarea.value.length : 0));
180
+ resetPreferredCol();
181
+ ensureInputCursorVisible();
182
+ updateCursor(textarea);
183
+ render(textarea);
184
+ return;
185
+ }
186
+
187
+ if (keyName === "up") {
188
+ if (completionController.isActive() && textarea && textarea.value === "/" && getCursorPos() === 1) {
189
+ completionController.jumpToLast();
190
+ return;
191
+ }
192
+ if (historyUp()) {
193
+ completionController.hide();
194
+ return;
195
+ }
196
+ }
197
+
198
+ if (keyName === "down") {
199
+ if (historyDown()) {
200
+ completionController.hide();
201
+ return;
202
+ }
203
+ }
204
+
205
+ if (keyName === "up" || keyName === "down") {
206
+ const innerWidth = getWrapWidth();
207
+ if (innerWidth > 0) {
208
+ const cursorPos = getCursorPos();
209
+ const value = (textarea && textarea.value) || "";
210
+ const { row, col } = getCursorRowCol(value, cursorPos, innerWidth);
211
+ if (getPreferredCol() === null) setPreferredCol(col);
212
+ const totalRows = countLines(value, innerWidth);
213
+
214
+ if (keyName === "down" && row >= totalRows - 1) {
215
+ enterDashboardMode();
216
+ return;
217
+ }
218
+
219
+ const targetRow = keyName === "up"
220
+ ? Math.max(0, row - 1)
221
+ : Math.min(totalRows - 1, row + 1);
222
+ setCursorPos(getCursorPosForRowCol(value, targetRow, getPreferredCol(), innerWidth));
223
+ }
224
+ ensureInputCursorVisible();
225
+ updateCursor(textarea);
226
+ render(textarea);
227
+ return;
228
+ }
229
+
230
+ if (keyName === "escape") {
231
+ if (textarea && typeof textarea._done === "function") {
232
+ textarea._done(null, null);
233
+ }
234
+ return;
235
+ }
236
+
237
+ if (keyName === "backspace") {
238
+ const cursorPos = getCursorPos();
239
+ if (cursorPos > 0 && textarea) {
240
+ textarea.value = textarea.value.slice(0, cursorPos - 1) + textarea.value.slice(cursorPos);
241
+ setCursorPos(cursorPos - 1);
242
+ resetPreferredCol();
243
+ resizeInput();
244
+ ensureInputCursorVisible();
245
+ updateCursor(textarea);
246
+ updateDraftFromInput();
247
+
248
+ if (shouldShowCompletion(textarea.value)) {
249
+ completionController.show(textarea.value);
250
+ } else {
251
+ completionController.hide();
252
+ }
253
+
254
+ render(textarea);
255
+ }
256
+ return;
257
+ }
258
+
259
+ if (keyName === "delete") {
260
+ const cursorPos = getCursorPos();
261
+ if (textarea && cursorPos < textarea.value.length) {
262
+ textarea.value = textarea.value.slice(0, cursorPos) + textarea.value.slice(cursorPos + 1);
263
+ resetPreferredCol();
264
+ resizeInput();
265
+ ensureInputCursorVisible();
266
+ updateCursor(textarea);
267
+ render(textarea);
268
+ updateDraftFromInput();
269
+ }
270
+ return;
271
+ }
272
+
273
+ const insertChar = (ch && ch.length === 1)
274
+ ? ch
275
+ : (keyName && keyName.length === 1 ? keyName : null);
276
+
277
+ if (insertChar && !/^[\x00-\x08\x0b-\x0c\x0e-\x1f\x7f]$/.test(insertChar) && textarea) {
278
+ const cursorPos = getCursorPos();
279
+ textarea.value = textarea.value.slice(0, cursorPos) + insertChar + textarea.value.slice(cursorPos);
280
+ setCursorPos(cursorPos + 1);
281
+ normalizeCommandPrefix();
282
+ resetPreferredCol();
283
+ resizeInput();
284
+ updateCursor(textarea);
285
+ updateDraftFromInput();
286
+
287
+ if (shouldShowCompletion(textarea.value)) {
288
+ completionController.show(textarea.value);
289
+ } else if (completionController.isActive()) {
290
+ completionController.hide();
291
+ }
292
+
293
+ render(textarea);
294
+ }
295
+ }
296
+
297
+ return {
298
+ handleKey,
299
+ };
300
+ }
301
+
302
+ module.exports = {
303
+ createInputListenerController,
304
+ };
@@ -0,0 +1,104 @@
1
+ function safeStrWidth(strWidth, value) {
2
+ if (typeof strWidth === "function") return strWidth(value);
3
+ return Array.from(String(value || "")).length;
4
+ }
5
+
6
+ function getInnerWidth({ input, screen, promptWidth = 2 }) {
7
+ const lpos = input.lpos || input._getCoords();
8
+ if (lpos && Number.isFinite(lpos.xl) && Number.isFinite(lpos.xi)) {
9
+ return Math.max(1, lpos.xl - lpos.xi + 1);
10
+ }
11
+ if (typeof input.width === "number") return Math.max(1, input.width);
12
+ if (typeof input.width === "string") {
13
+ const match = input.width.match(/^100%-([0-9]+)$/);
14
+ if (match && typeof screen.width === "number") {
15
+ return Math.max(1, screen.width - parseInt(match[1], 10));
16
+ }
17
+ }
18
+ if (typeof screen.width === "number") return Math.max(1, screen.width - promptWidth);
19
+ if (typeof screen.cols === "number") return Math.max(1, screen.cols - promptWidth);
20
+ return 1;
21
+ }
22
+
23
+ function getWrapWidth(input, fallbackWidth) {
24
+ if (input._clines && typeof input._clines.width === "number") {
25
+ return Math.max(1, input._clines.width);
26
+ }
27
+ return Math.max(1, fallbackWidth || 1);
28
+ }
29
+
30
+ function countLines(text, width, strWidth) {
31
+ if (width <= 0) return 1;
32
+ const lines = String(text || "").split("\n");
33
+ let total = 0;
34
+ for (const line of lines) {
35
+ const lineWidth = safeStrWidth(strWidth, line);
36
+ total += Math.max(1, Math.ceil(lineWidth / width));
37
+ }
38
+ return total;
39
+ }
40
+
41
+ function getCursorRowCol(text, pos, width, strWidth) {
42
+ if (width <= 0) return { row: 0, col: 0 };
43
+ const before = String(text || "").slice(0, Math.max(0, pos));
44
+ const lines = before.split("\n");
45
+ let row = 0;
46
+ for (let i = 0; i < lines.length - 1; i += 1) {
47
+ const lineWidth = safeStrWidth(strWidth, lines[i]);
48
+ row += Math.max(1, Math.ceil(lineWidth / width));
49
+ }
50
+ const lastLine = lines[lines.length - 1] || "";
51
+ const lastWidth = safeStrWidth(strWidth, lastLine);
52
+ row += Math.floor(lastWidth / width);
53
+ const col = lastWidth % width;
54
+ return { row, col };
55
+ }
56
+
57
+ function getLinePosForCol(line, targetCol, strWidth) {
58
+ if (targetCol <= 0) return 0;
59
+ let col = 0;
60
+ let offset = 0;
61
+ for (const ch of Array.from(String(line || ""))) {
62
+ const w = safeStrWidth(strWidth, ch);
63
+ if (col + w > targetCol) return offset;
64
+ col += w;
65
+ offset += ch.length;
66
+ }
67
+ return offset;
68
+ }
69
+
70
+ function getCursorPosForRowCol(text, targetRow, targetCol, width, strWidth) {
71
+ if (width <= 0) return 0;
72
+ const source = String(text || "");
73
+ const lines = source.split("\n");
74
+ let row = 0;
75
+ let pos = 0;
76
+ for (const line of lines) {
77
+ const lineWidth = safeStrWidth(strWidth, line);
78
+ const wrappedRows = Math.max(1, Math.ceil(lineWidth / width));
79
+ if (targetRow < row + wrappedRows) {
80
+ const rowInLine = targetRow - row;
81
+ const visualCol = rowInLine * width + Math.max(0, targetCol);
82
+ return pos + getLinePosForCol(line, visualCol, strWidth);
83
+ }
84
+ pos += line.length + 1;
85
+ row += wrappedRows;
86
+ }
87
+ return source.length;
88
+ }
89
+
90
+ function normalizePaste(text) {
91
+ if (!text) return "";
92
+ let normalized = String(text).replace(/\x1b\[200~|\x1b\[201~/g, "");
93
+ normalized = normalized.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
94
+ return normalized;
95
+ }
96
+
97
+ module.exports = {
98
+ getInnerWidth,
99
+ getWrapWidth,
100
+ countLines,
101
+ getCursorRowCol,
102
+ getCursorPosForRowCol,
103
+ normalizePaste,
104
+ };
@@ -0,0 +1,171 @@
1
+ const { IPC_REQUEST_TYPES } = require("../shared/eventContract");
2
+
3
+ function createInputSubmitHandler(options = {}) {
4
+ const {
5
+ state,
6
+ parseAtTarget = () => null,
7
+ resolveAgentId = () => null,
8
+ executeCommand = async () => false,
9
+ queueStatusLine = () => {},
10
+ send = () => {},
11
+ logMessage = () => {},
12
+ getAgentLabel = (id) => id,
13
+ escapeBlessed = (value) => String(value || ""),
14
+ markPendingDelivery = () => {},
15
+ clearTargetAgent = () => {},
16
+ setTargetAgent = () => {},
17
+ enterAgentView = () => {},
18
+ getAgentAdapter = () => null,
19
+ activateAgent = async () => {},
20
+ getInjectSockPath = () => "",
21
+ existsSync = () => false,
22
+ commitInputHistory = () => {},
23
+ focusInput = () => {},
24
+ renderScreen = () => {}, // Add renderScreen callback
25
+ } = options;
26
+
27
+ if (!state || typeof state !== "object") {
28
+ throw new Error("createInputSubmitHandler requires a mutable state object");
29
+ }
30
+
31
+ async function tryActivateTargetAgent(agentId) {
32
+ const adapter = getAgentAdapter(agentId);
33
+ const capabilities = adapter && adapter.capabilities ? adapter.capabilities : null;
34
+ const sockPath = getInjectSockPath(agentId);
35
+ const supportsSocket = Boolean(capabilities && capabilities.supportsSocketProtocol);
36
+ const supportsActivate = Boolean(capabilities && capabilities.supportsActivate);
37
+ const supportsInternalQueue = Boolean(capabilities && capabilities.supportsInternalQueueLoop);
38
+
39
+ if (existsSync(sockPath) && supportsSocket) {
40
+ clearTargetAgent();
41
+ enterAgentView(agentId);
42
+ return true;
43
+ }
44
+
45
+ if (supportsActivate) {
46
+ clearTargetAgent();
47
+ try {
48
+ if (adapter && typeof adapter.activate === "function") {
49
+ adapter.activate(agentId);
50
+ } else {
51
+ const pendingActivation = activateAgent(agentId);
52
+ if (pendingActivation && typeof pendingActivation.catch === "function") {
53
+ pendingActivation.catch(() => {});
54
+ }
55
+ }
56
+ } catch {
57
+ // Best-effort activation.
58
+ }
59
+ return true;
60
+ }
61
+
62
+ if (supportsInternalQueue) {
63
+ clearTargetAgent();
64
+ enterAgentView(agentId, { useBus: true });
65
+ return true;
66
+ }
67
+
68
+ return false;
69
+ }
70
+
71
+ async function handleSubmit(value) {
72
+ const text = String(value || "").trim();
73
+
74
+ if (!text) {
75
+ if (state.targetAgent) {
76
+ const handled = await tryActivateTargetAgent(state.targetAgent);
77
+ if (handled) return;
78
+ }
79
+ focusInput();
80
+ return;
81
+ }
82
+
83
+ commitInputHistory(text);
84
+
85
+ if (state.targetAgent) {
86
+ const label = getAgentLabel(state.targetAgent);
87
+ logMessage(
88
+ "user",
89
+ `{cyan-fg}→{/cyan-fg} {magenta-fg}@${escapeBlessed(label)}{/magenta-fg} ${escapeBlessed(text)}`
90
+ );
91
+ renderScreen(); // Immediately render the user message
92
+ markPendingDelivery(state.targetAgent);
93
+ send({ type: IPC_REQUEST_TYPES.BUS_SEND, target: state.targetAgent, message: text });
94
+ clearTargetAgent();
95
+ focusInput();
96
+ return;
97
+ }
98
+
99
+ const atTarget = parseAtTarget(text);
100
+ if (atTarget) {
101
+ if (!atTarget.message) {
102
+ const resolvedTarget = resolveAgentId(atTarget.target) || "";
103
+ if (!resolvedTarget) {
104
+ logMessage("error", "{white-fg}✗{/white-fg} Unknown @target");
105
+ focusInput();
106
+ return;
107
+ }
108
+ setTargetAgent(resolvedTarget);
109
+ logMessage(
110
+ "status",
111
+ `{white-fg}⚙{/white-fg} Target selected: @${escapeBlessed(atTarget.target)}`
112
+ );
113
+ focusInput();
114
+ return;
115
+ }
116
+ const resolvedTarget = resolveAgentId(atTarget.target) || atTarget.target;
117
+ logMessage(
118
+ "user",
119
+ `{cyan-fg}→{/cyan-fg} {magenta-fg}@${escapeBlessed(atTarget.target)}{/magenta-fg} ${escapeBlessed(atTarget.message)}`
120
+ );
121
+ renderScreen(); // Immediately render the user message
122
+ markPendingDelivery(resolvedTarget);
123
+ send({ type: IPC_REQUEST_TYPES.BUS_SEND, target: resolvedTarget, message: atTarget.message });
124
+ focusInput();
125
+ return;
126
+ }
127
+
128
+ if (text.startsWith("/")) {
129
+ logMessage("user", `{white-fg}→{/white-fg} ${escapeBlessed(text)}`);
130
+ renderScreen(); // Render slash command immediately
131
+ try {
132
+ await executeCommand(text);
133
+ } catch (err) {
134
+ logMessage("error", `{white-fg}✗{/white-fg} Command error: ${escapeBlessed(err.message)}`);
135
+ }
136
+ focusInput();
137
+ return;
138
+ }
139
+
140
+ if (state.pending && state.pending.disambiguate) {
141
+ const idx = parseInt(text, 10);
142
+ const choice = state.pending.disambiguate.candidates[idx - 1];
143
+ if (choice) {
144
+ queueStatusLine(`ufoo-agent processing (assigning ${choice.agent_id})`);
145
+ send({
146
+ type: IPC_REQUEST_TYPES.PROMPT,
147
+ text: `Use agent ${choice.agent_id} to handle: ${state.pending.original || "the request"}`,
148
+ });
149
+ state.pending = null;
150
+ } else {
151
+ logMessage("error", "Invalid selection.");
152
+ }
153
+ } else {
154
+ state.pending = { original: text };
155
+ queueStatusLine("ufoo-agent processing");
156
+ send({ type: IPC_REQUEST_TYPES.PROMPT, text });
157
+ logMessage("user", `{white-fg}→{/white-fg} ${escapeBlessed(text)}`);
158
+ renderScreen(); // Render plain text message immediately
159
+ }
160
+
161
+ focusInput();
162
+ }
163
+
164
+ return {
165
+ handleSubmit,
166
+ };
167
+ }
168
+
169
+ module.exports = {
170
+ createInputSubmitHandler,
171
+ };