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.
- package/README.md +44 -4
- package/SKILLS/ufoo/SKILL.md +17 -2
- package/SKILLS/uinit/SKILL.md +8 -3
- package/bin/ucode-core.js +15 -0
- package/bin/ucode.js +125 -0
- package/bin/ufoo-assistant-agent.js +5 -0
- package/bin/ufoo-engine.js +25 -0
- package/bin/ufoo.js +4 -0
- package/modules/AGENTS.template.md +14 -4
- package/modules/bus/README.md +8 -5
- package/modules/bus/SKILLS/ubus/SKILL.md +5 -4
- package/modules/context/SKILLS/uctx/SKILL.md +3 -1
- package/modules/online/SKILLS/ufoo-online/SKILL.md +144 -0
- package/package.json +12 -3
- package/scripts/import-pi-mono.js +124 -0
- package/scripts/postinstall.js +20 -49
- package/scripts/sync-claude-skills.sh +21 -0
- package/src/agent/cliRunner.js +524 -31
- package/src/agent/internalRunner.js +76 -9
- package/src/agent/launcher.js +97 -45
- package/src/agent/normalizeOutput.js +1 -1
- package/src/agent/notifier.js +144 -4
- package/src/agent/ptyRunner.js +480 -10
- package/src/agent/ptyWrapper.js +28 -3
- package/src/agent/readyDetector.js +16 -0
- package/src/agent/ucode.js +443 -0
- package/src/agent/ucodeBootstrap.js +113 -0
- package/src/agent/ucodeBuild.js +67 -0
- package/src/agent/ucodeDoctor.js +184 -0
- package/src/agent/ucodeRuntimeConfig.js +129 -0
- package/src/agent/ufooAgent.js +11 -2
- package/src/assistant/agent.js +260 -0
- package/src/assistant/bridge.js +172 -0
- package/src/assistant/engine.js +252 -0
- package/src/assistant/stdio.js +58 -0
- package/src/assistant/ufooEngineCli.js +306 -0
- package/src/bus/activate.js +27 -11
- package/src/bus/daemon.js +133 -5
- package/src/bus/index.js +137 -80
- package/src/bus/inject.js +47 -17
- package/src/bus/message.js +145 -17
- package/src/bus/nickname.js +3 -1
- package/src/bus/queue.js +6 -1
- package/src/bus/store.js +189 -0
- package/src/bus/subscriber.js +20 -4
- package/src/bus/utils.js +9 -3
- package/src/chat/agentBar.js +117 -0
- package/src/chat/agentDirectory.js +88 -0
- package/src/chat/agentSockets.js +225 -0
- package/src/chat/agentViewController.js +298 -0
- package/src/chat/chatLogController.js +115 -0
- package/src/chat/commandExecutor.js +700 -0
- package/src/chat/commands.js +132 -0
- package/src/chat/completionController.js +414 -0
- package/src/chat/cronScheduler.js +160 -0
- package/src/chat/daemonConnection.js +166 -0
- package/src/chat/daemonCoordinator.js +64 -0
- package/src/chat/daemonMessageRouter.js +257 -0
- package/src/chat/daemonReconnect.js +41 -0
- package/src/chat/daemonTransport.js +36 -0
- package/src/chat/daemonTransportDefaults.js +10 -0
- package/src/chat/dashboardKeyController.js +480 -0
- package/src/chat/dashboardView.js +154 -0
- package/src/chat/index.js +935 -2909
- package/src/chat/inputHistoryController.js +105 -0
- package/src/chat/inputListenerController.js +304 -0
- package/src/chat/inputMath.js +104 -0
- package/src/chat/inputSubmitHandler.js +171 -0
- package/src/chat/layout.js +165 -0
- package/src/chat/pasteController.js +81 -0
- package/src/chat/rawKeyMap.js +42 -0
- package/src/chat/settingsController.js +132 -0
- package/src/chat/statusLineController.js +177 -0
- package/src/chat/streamTracker.js +138 -0
- package/src/chat/text.js +70 -0
- package/src/chat/transport.js +61 -0
- package/src/cli/busCoreCommands.js +59 -0
- package/src/cli/ctxCoreCommands.js +199 -0
- package/src/cli/onlineCoreCommands.js +379 -0
- package/src/cli.js +741 -238
- package/src/code/README.md +29 -0
- package/src/code/UCODE_PROMPT.md +32 -0
- package/src/code/agent.js +1651 -0
- package/src/code/cli.js +158 -0
- package/src/code/config +0 -0
- package/src/code/dispatch.js +42 -0
- package/src/code/index.js +70 -0
- package/src/code/nativeRunner.js +1213 -0
- package/src/code/runtime.js +154 -0
- package/src/code/sessionStore.js +162 -0
- package/src/code/taskDecomposer.js +269 -0
- package/src/code/tools/bash.js +53 -0
- package/src/code/tools/common.js +42 -0
- package/src/code/tools/edit.js +70 -0
- package/src/code/tools/read.js +44 -0
- package/src/code/tools/write.js +35 -0
- package/src/code/tui.js +1580 -0
- package/src/config.js +47 -1
- package/src/context/decisions.js +12 -2
- package/src/context/index.js +18 -1
- package/src/context/sync.js +127 -0
- package/src/daemon/agentProcessManager.js +74 -0
- package/src/daemon/cronOps.js +241 -0
- package/src/daemon/index.js +661 -488
- package/src/daemon/ipcServer.js +99 -0
- package/src/daemon/ops.js +417 -179
- package/src/daemon/promptLoop.js +319 -0
- package/src/daemon/promptRequest.js +101 -0
- package/src/daemon/providerSessions.js +32 -17
- package/src/daemon/reporting.js +90 -0
- package/src/daemon/run.js +2 -5
- package/src/daemon/status.js +24 -1
- package/src/init/index.js +68 -14
- package/src/online/bridge.js +663 -0
- package/src/online/client.js +245 -0
- package/src/online/runner.js +253 -0
- package/src/online/server.js +992 -0
- package/src/online/tokens.js +103 -0
- package/src/report/store.js +331 -0
- package/src/shared/eventContract.js +35 -0
- package/src/shared/ptySocketContract.js +21 -0
- package/src/status/index.js +50 -17
- package/src/terminal/adapterContract.js +87 -0
- package/src/terminal/adapterRouter.js +84 -0
- package/src/terminal/adapters/externalAdapter.js +14 -0
- package/src/terminal/adapters/internalAdapter.js +13 -0
- package/src/terminal/adapters/internalPtyAdapter.js +42 -0
- package/src/terminal/adapters/internalQueueAdapter.js +37 -0
- package/src/terminal/adapters/terminalAdapter.js +31 -0
- package/src/terminal/adapters/tmuxAdapter.js +30 -0
- package/src/ufoo/agentsStore.js +69 -3
- package/src/utils/banner.js +5 -2
- package/scripts/.archived/bash-to-js-migration/README.md +0 -46
- package/scripts/.archived/bash-to-js-migration/banner.sh +0 -89
- package/scripts/.archived/bash-to-js-migration/bus-alert.sh +0 -6
- package/scripts/.archived/bash-to-js-migration/bus-autotrigger.sh +0 -6
- package/scripts/.archived/bash-to-js-migration/bus-daemon.sh +0 -231
- package/scripts/.archived/bash-to-js-migration/bus-inject.sh +0 -176
- package/scripts/.archived/bash-to-js-migration/bus-listen.sh +0 -6
- package/scripts/.archived/bash-to-js-migration/bus.sh +0 -986
- package/scripts/.archived/bash-to-js-migration/context-decisions.sh +0 -167
- package/scripts/.archived/bash-to-js-migration/context-doctor.sh +0 -72
- package/scripts/.archived/bash-to-js-migration/context-lint.sh +0 -110
- package/scripts/.archived/bash-to-js-migration/doctor.sh +0 -22
- package/scripts/.archived/bash-to-js-migration/init.sh +0 -247
- package/scripts/.archived/bash-to-js-migration/skills.sh +0 -113
- package/scripts/.archived/bash-to-js-migration/status.sh +0 -125
- package/scripts/banner.sh +0 -2
- 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
|
+
};
|