u-foo 2.3.30 → 2.3.32
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/package.json +5 -1
- package/scripts/chat-app-smoke.js +30 -0
- package/scripts/ink-demo.js +23 -0
- package/scripts/ink-smoke.js +30 -0
- package/scripts/ucode-app-smoke.js +36 -0
- package/src/chat/commandExecutor.js +6 -2
- package/src/chat/daemonMessageRouter.js +9 -1
- package/src/chat/daemonTransport.js +2 -1
- package/src/chat/dashboardKeyController.js +0 -40
- package/src/chat/dashboardView.js +0 -20
- package/src/chat/index.js +9 -1
- package/src/chat/inputSubmitHandler.js +34 -0
- package/src/chat/projectCloseController.js +1 -1
- package/src/chat/shellCommand.js +42 -0
- package/src/chat/transport.js +16 -3
- package/src/cli.js +4 -3
- package/src/code/agent.js +4 -0
- package/src/code/nativeRunner.js +74 -0
- package/src/code/taskDecomposer.js +5 -4
- package/src/code/tui.js +73 -561
- package/src/daemon/index.js +169 -27
- package/src/daemon/ipcServer.js +23 -1
- package/src/daemon/promptRequest.js +6 -1
- package/src/daemon/run.js +11 -4
- package/src/projects/runtimes.js +1 -1
- package/src/ufoo/agentRegistryDiagnostics.js +43 -0
- package/src/ui/MIGRATION.md +382 -0
- package/src/ui/components/ChatApp.js +2950 -0
- package/src/ui/components/DashboardBar.js +417 -0
- package/src/ui/components/InkDemo.js +96 -0
- package/src/ui/components/MultilineInput.js +387 -0
- package/src/ui/components/UcodeApp.js +813 -0
- package/src/ui/components/agentMirror.js +725 -0
- package/src/ui/components/chatReducer.js +337 -0
- package/src/ui/format/index.js +997 -0
- package/src/ui/index.js +9 -0
- package/src/ui/runInk.js +57 -0
- package/src/utils/nodeExecutable.js +26 -0
|
@@ -0,0 +1,337 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Chat reducer — the entire bag of UI state needed to render ChatApp,
|
|
5
|
+
* isolated as a pure function so jest can drive it through transitions
|
|
6
|
+
* without mounting ink.
|
|
7
|
+
*
|
|
8
|
+
* Action types (kept simple and explicit; we don't mint constants because
|
|
9
|
+
* the reducer is the only consumer):
|
|
10
|
+
* { type: "log/append", text } append a log line
|
|
11
|
+
* { type: "log/appendMany", lines } append multiple lines at once
|
|
12
|
+
* { type: "log/clear" } reset log to banner
|
|
13
|
+
* { type: "draft/set", value }
|
|
14
|
+
* { type: "draft/clear" }
|
|
15
|
+
* { type: "focus/toggle" } Tab between input/dashboard
|
|
16
|
+
* { type: "focus/set", mode } "input" | "dashboard"
|
|
17
|
+
* { type: "view/set", view } projects|agents|mode|provider|cron
|
|
18
|
+
* { type: "view/cycle", direction } "left" | "right"
|
|
19
|
+
* { type: "agents/set", list } list of {fullId, type, id, nickname, …}
|
|
20
|
+
* { type: "agents/select", index }
|
|
21
|
+
* { type: "agents/cycle", direction }
|
|
22
|
+
* { type: "agents/clearTarget" }
|
|
23
|
+
* { type: "agents/window", windowStart }
|
|
24
|
+
* { type: "projects/set", list, activeProjectRoot }
|
|
25
|
+
* { type: "projects/select", index, projectRoot }
|
|
26
|
+
* { type: "scope/set", scope } "controller" | "project"
|
|
27
|
+
* { type: "status/set", payload } { message, type, showTimer, startedAt }
|
|
28
|
+
* { type: "status/idle" }
|
|
29
|
+
* { type: "history/push", value }
|
|
30
|
+
* { type: "history/setIndex", index }
|
|
31
|
+
* { type: "merge/append", entry } tool merge add
|
|
32
|
+
* { type: "merge/flush" } freeze active group into log
|
|
33
|
+
* { type: "merge/expand" } Ctrl+O
|
|
34
|
+
* { type: "settings/set", patch } merge launch mode / provider / autoResume
|
|
35
|
+
*/
|
|
36
|
+
|
|
37
|
+
const fmt = require("../format");
|
|
38
|
+
|
|
39
|
+
const LOG_CAP = 1000;
|
|
40
|
+
const HISTORY_CAP = 200;
|
|
41
|
+
const DASHBOARD_VIEWS = ["projects", "agents", "mode", "provider", "cron"];
|
|
42
|
+
const DEFAULT_PROVIDER_OPTIONS = [
|
|
43
|
+
{ label: "codex", value: "codex-cli" },
|
|
44
|
+
{ label: "claude", value: "claude-cli" },
|
|
45
|
+
];
|
|
46
|
+
function projectRootOf(row = {}) {
|
|
47
|
+
return String((row && (row.root || row.project_root || row.projectRoot)) || "");
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function createInitialState({ banner = [], globalMode = false, globalScope = "controller", settings = {} } = {}) {
|
|
51
|
+
const initialLaunchMode = settings.launchMode || "auto";
|
|
52
|
+
const initialAgentProvider = settings.agentProvider || "codex-cli";
|
|
53
|
+
const selectedProviderIndex = Math.max(0, DEFAULT_PROVIDER_OPTIONS.findIndex((opt) => opt.value === initialAgentProvider));
|
|
54
|
+
return {
|
|
55
|
+
logLines: banner.concat([""]).map((line, idx) => ({ id: `b-${idx}`, text: line })),
|
|
56
|
+
lineSeq: banner.length + 1,
|
|
57
|
+
draft: "",
|
|
58
|
+
focusMode: "input",
|
|
59
|
+
dashboardView: globalMode ? "projects" : "agents",
|
|
60
|
+
globalMode,
|
|
61
|
+
globalScope,
|
|
62
|
+
agents: [],
|
|
63
|
+
activeAgentMeta: new Map(),
|
|
64
|
+
selectedAgentIndex: -1,
|
|
65
|
+
agentSelectionMode: false,
|
|
66
|
+
agentListWindowStart: 0,
|
|
67
|
+
projects: [],
|
|
68
|
+
selectedProjectIndex: -1,
|
|
69
|
+
selectedProjectRoot: "",
|
|
70
|
+
projectListWindowStart: 0,
|
|
71
|
+
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
|
+
providerOptions: DEFAULT_PROVIDER_OPTIONS,
|
|
75
|
+
selectedProviderIndex,
|
|
76
|
+
cronTasks: [],
|
|
77
|
+
selectedCronIndex: -1,
|
|
78
|
+
loopSummary: null,
|
|
79
|
+
viewingAgentId: null,
|
|
80
|
+
// activeStream is the in-flight chunk-by-chunk publisher message (set
|
|
81
|
+
// while the daemon is streaming). Rendered live below <Static>;
|
|
82
|
+
// promoted to <Static> when the stream finishes the same way the
|
|
83
|
+
// tool-merge group is.
|
|
84
|
+
activeStream: null,
|
|
85
|
+
inputHistory: [],
|
|
86
|
+
historyIndex: 0,
|
|
87
|
+
activeMerge: null,
|
|
88
|
+
lastMerge: null,
|
|
89
|
+
mergeId: 0,
|
|
90
|
+
status: { message: "", type: "thinking", showTimer: false, startedAt: 0 },
|
|
91
|
+
settings: { launchMode: initialLaunchMode, agentProvider: initialAgentProvider, autoResume: settings.autoResume === true },
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function appendLog(state, lines) {
|
|
96
|
+
const incoming = Array.isArray(lines) ? lines : [lines];
|
|
97
|
+
let seq = state.lineSeq;
|
|
98
|
+
const out = state.logLines.concat(incoming.map((text) => {
|
|
99
|
+
const id = `l-${seq}`;
|
|
100
|
+
seq += 1;
|
|
101
|
+
return { id, text: String(text == null ? "" : text) };
|
|
102
|
+
}));
|
|
103
|
+
return {
|
|
104
|
+
...state,
|
|
105
|
+
logLines: out.length > LOG_CAP ? out.slice(-LOG_CAP) : out,
|
|
106
|
+
lineSeq: seq,
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function freezeMergeIntoLog(state) {
|
|
111
|
+
if (!state.activeMerge) return state;
|
|
112
|
+
const summary = fmt.buildToolMergeRowText(state.activeMerge.entries);
|
|
113
|
+
return appendLog({ ...state, activeMerge: null }, summary);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function reducer(state, action) {
|
|
117
|
+
if (!action || !action.type) return state;
|
|
118
|
+
switch (action.type) {
|
|
119
|
+
case "log/append":
|
|
120
|
+
return appendLog(freezeMergeIntoLog(state), action.text);
|
|
121
|
+
case "log/appendMany":
|
|
122
|
+
return appendLog(freezeMergeIntoLog(state), action.lines);
|
|
123
|
+
case "log/clear":
|
|
124
|
+
return {
|
|
125
|
+
...state,
|
|
126
|
+
logLines: [],
|
|
127
|
+
lineSeq: 0,
|
|
128
|
+
activeMerge: null,
|
|
129
|
+
};
|
|
130
|
+
case "draft/set":
|
|
131
|
+
return { ...state, draft: String(action.value || "") };
|
|
132
|
+
case "draft/clear":
|
|
133
|
+
return { ...state, draft: "" };
|
|
134
|
+
case "focus/toggle":
|
|
135
|
+
return { ...state, focusMode: state.focusMode === "input" ? "dashboard" : "input" };
|
|
136
|
+
case "focus/set":
|
|
137
|
+
return { ...state, focusMode: action.mode === "dashboard" ? "dashboard" : "input" };
|
|
138
|
+
case "view/set":
|
|
139
|
+
return { ...state, dashboardView: action.view };
|
|
140
|
+
case "view/cycle": {
|
|
141
|
+
const i = DASHBOARD_VIEWS.indexOf(state.dashboardView);
|
|
142
|
+
const direction = action.direction === "left" ? -1 : 1;
|
|
143
|
+
const start = i < 0 ? 0 : i;
|
|
144
|
+
const next = Math.max(0, Math.min(DASHBOARD_VIEWS.length - 1, start + direction));
|
|
145
|
+
return { ...state, dashboardView: DASHBOARD_VIEWS[next] };
|
|
146
|
+
}
|
|
147
|
+
case "agents/set": {
|
|
148
|
+
const list = Array.isArray(action.list) ? action.list : [];
|
|
149
|
+
const ids = list.map((a) => a.fullId || `${a.type}:${a.id}`);
|
|
150
|
+
const meta = new Map(list.map((a) => [a.fullId || `${a.type}:${a.id}`, a]));
|
|
151
|
+
let nextIdx = state.selectedAgentIndex;
|
|
152
|
+
let nextMode = state.agentSelectionMode;
|
|
153
|
+
if (ids.length === 0) {
|
|
154
|
+
nextIdx = -1;
|
|
155
|
+
nextMode = false;
|
|
156
|
+
} else if (nextIdx >= ids.length) {
|
|
157
|
+
nextIdx = ids.length - 1;
|
|
158
|
+
}
|
|
159
|
+
return {
|
|
160
|
+
...state,
|
|
161
|
+
agents: ids,
|
|
162
|
+
activeAgentMeta: meta,
|
|
163
|
+
selectedAgentIndex: nextIdx,
|
|
164
|
+
agentSelectionMode: nextMode,
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
case "agents/patchMeta": {
|
|
168
|
+
const agentId = String(action.agentId || "").trim();
|
|
169
|
+
if (!agentId) return state;
|
|
170
|
+
const meta = new Map(state.activeAgentMeta instanceof Map ? state.activeAgentMeta : []);
|
|
171
|
+
const current = meta.get(agentId) || {};
|
|
172
|
+
meta.set(agentId, { ...current, ...(action.patch || {}) });
|
|
173
|
+
return { ...state, activeAgentMeta: meta };
|
|
174
|
+
}
|
|
175
|
+
case "agents/select":
|
|
176
|
+
return {
|
|
177
|
+
...state,
|
|
178
|
+
selectedAgentIndex: action.index,
|
|
179
|
+
agentSelectionMode: action.index >= 0,
|
|
180
|
+
};
|
|
181
|
+
case "agents/cycle": {
|
|
182
|
+
if (state.agents.length === 0) return state;
|
|
183
|
+
const cur = state.selectedAgentIndex < 0 ? 0 : state.selectedAgentIndex;
|
|
184
|
+
const next = action.direction === "left"
|
|
185
|
+
? Math.max(0, cur - 1)
|
|
186
|
+
: Math.min(state.agents.length - 1, cur + 1);
|
|
187
|
+
return { ...state, selectedAgentIndex: next, agentSelectionMode: true };
|
|
188
|
+
}
|
|
189
|
+
case "agents/clearTarget":
|
|
190
|
+
return { ...state, selectedAgentIndex: -1, agentSelectionMode: false };
|
|
191
|
+
case "agents/window":
|
|
192
|
+
return { ...state, agentListWindowStart: action.windowStart };
|
|
193
|
+
case "projects/set": {
|
|
194
|
+
const list = Array.isArray(action.list) ? action.list : [];
|
|
195
|
+
const previousSelectedRoot = state.selectedProjectRoot
|
|
196
|
+
|| projectRootOf(state.projects[state.selectedProjectIndex]);
|
|
197
|
+
const selectedRoot = String(action.selectedProjectRoot || previousSelectedRoot || "");
|
|
198
|
+
const selectedIndex = selectedRoot
|
|
199
|
+
? list.findIndex((row) => projectRootOf(row) === selectedRoot)
|
|
200
|
+
: -1;
|
|
201
|
+
return {
|
|
202
|
+
...state,
|
|
203
|
+
projects: list,
|
|
204
|
+
selectedProjectRoot: selectedIndex >= 0 ? selectedRoot : "",
|
|
205
|
+
selectedProjectIndex: selectedIndex,
|
|
206
|
+
activeProjectRoot: action.activeProjectRoot || state.activeProjectRoot,
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
case "projects/select":
|
|
210
|
+
return {
|
|
211
|
+
...state,
|
|
212
|
+
selectedProjectIndex: action.index,
|
|
213
|
+
selectedProjectRoot: String(action.projectRoot || projectRootOf(state.projects[action.index]) || ""),
|
|
214
|
+
};
|
|
215
|
+
case "projects/clearSelection":
|
|
216
|
+
return { ...state, selectedProjectIndex: -1, selectedProjectRoot: "" };
|
|
217
|
+
case "projects/window":
|
|
218
|
+
return { ...state, projectListWindowStart: Math.max(0, action.windowStart | 0) };
|
|
219
|
+
case "scope/set":
|
|
220
|
+
return { ...state, globalScope: action.scope === "project" ? "project" : "controller" };
|
|
221
|
+
case "status/set":
|
|
222
|
+
return { ...state, status: { ...state.status, ...action.payload } };
|
|
223
|
+
case "status/idle":
|
|
224
|
+
return { ...state, status: { message: "", type: "thinking", showTimer: false, startedAt: 0 } };
|
|
225
|
+
case "history/push": {
|
|
226
|
+
const value = String(action.value || "").trim();
|
|
227
|
+
if (!value) return state;
|
|
228
|
+
const next = state.inputHistory.concat([value]).slice(-HISTORY_CAP);
|
|
229
|
+
return { ...state, inputHistory: next, historyIndex: next.length };
|
|
230
|
+
}
|
|
231
|
+
case "history/load": {
|
|
232
|
+
const list = Array.isArray(action.list) ? action.list : [];
|
|
233
|
+
const next = list.slice(-HISTORY_CAP);
|
|
234
|
+
return { ...state, inputHistory: next, historyIndex: next.length };
|
|
235
|
+
}
|
|
236
|
+
case "history/setIndex":
|
|
237
|
+
return { ...state, historyIndex: Math.max(0, Math.min(state.inputHistory.length, action.index)) };
|
|
238
|
+
case "merge/append": {
|
|
239
|
+
const entry = fmt.normalizeToolMergeEntry(action.entry || {});
|
|
240
|
+
let next;
|
|
241
|
+
if (state.activeMerge) {
|
|
242
|
+
next = { ...state.activeMerge, entries: state.activeMerge.entries.concat([entry]) };
|
|
243
|
+
} else {
|
|
244
|
+
next = { id: state.mergeId + 1, entries: [entry], expanded: false };
|
|
245
|
+
}
|
|
246
|
+
const lastMerge = next.entries.length >= 2 ? next : state.lastMerge;
|
|
247
|
+
return {
|
|
248
|
+
...state,
|
|
249
|
+
activeMerge: next,
|
|
250
|
+
lastMerge,
|
|
251
|
+
mergeId: state.activeMerge ? state.mergeId : state.mergeId + 1,
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
case "merge/flush":
|
|
255
|
+
return freezeMergeIntoLog(state);
|
|
256
|
+
case "merge/expand": {
|
|
257
|
+
const candidate = (state.activeMerge && !state.activeMerge.expanded && state.activeMerge.entries.length >= 2)
|
|
258
|
+
? state.activeMerge
|
|
259
|
+
: (state.lastMerge && !state.lastMerge.expanded && state.lastMerge.entries.length >= 2
|
|
260
|
+
? state.lastMerge
|
|
261
|
+
: null);
|
|
262
|
+
if (!candidate) return state;
|
|
263
|
+
const lines = fmt.buildMergedToolExpandedLines(candidate.entries).map((line, i, arr) =>
|
|
264
|
+
`${i === arr.length - 1 ? "└" : "│"} ${line}`
|
|
265
|
+
);
|
|
266
|
+
const after = appendLog(state, lines);
|
|
267
|
+
return {
|
|
268
|
+
...after,
|
|
269
|
+
activeMerge: state.activeMerge && state.activeMerge.id === candidate.id ? null : state.activeMerge,
|
|
270
|
+
lastMerge: state.lastMerge && state.lastMerge.id === candidate.id
|
|
271
|
+
? { ...state.lastMerge, expanded: true }
|
|
272
|
+
: state.lastMerge,
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
case "settings/set":
|
|
276
|
+
return { ...state, settings: { ...state.settings, ...(action.patch || {}) } };
|
|
277
|
+
case "settings/applyMode": {
|
|
278
|
+
const mode = state.modeOptions[state.selectedModeIndex] || state.settings.launchMode;
|
|
279
|
+
return { ...state, settings: { ...state.settings, launchMode: mode } };
|
|
280
|
+
}
|
|
281
|
+
case "settings/applyProvider": {
|
|
282
|
+
const selected = state.providerOptions[state.selectedProviderIndex];
|
|
283
|
+
const agentProvider = selected && selected.value ? selected.value : state.settings.agentProvider;
|
|
284
|
+
return { ...state, settings: { ...state.settings, agentProvider } };
|
|
285
|
+
}
|
|
286
|
+
case "modeIndex/set":
|
|
287
|
+
return { ...state, selectedModeIndex: Math.max(0, action.index | 0) };
|
|
288
|
+
case "providerIndex/set":
|
|
289
|
+
return { ...state, selectedProviderIndex: Math.max(0, action.index | 0) };
|
|
290
|
+
case "cronIndex/set":
|
|
291
|
+
return { ...state, selectedCronIndex: Math.max(-1, action.index | 0) };
|
|
292
|
+
case "cron/set":
|
|
293
|
+
return { ...state, cronTasks: Array.isArray(action.list) ? action.list : [] };
|
|
294
|
+
case "loop/set":
|
|
295
|
+
return { ...state, loopSummary: action.summary && typeof action.summary === "object" ? action.summary : null };
|
|
296
|
+
case "stream/begin":
|
|
297
|
+
return {
|
|
298
|
+
...state,
|
|
299
|
+
activeStream: { publisher: action.publisher || "", text: "" },
|
|
300
|
+
};
|
|
301
|
+
case "stream/delta": {
|
|
302
|
+
if (!state.activeStream) {
|
|
303
|
+
return {
|
|
304
|
+
...state,
|
|
305
|
+
activeStream: { publisher: action.publisher || "", text: String(action.delta || "") },
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
return {
|
|
309
|
+
...state,
|
|
310
|
+
activeStream: {
|
|
311
|
+
...state.activeStream,
|
|
312
|
+
text: state.activeStream.text + String(action.delta || ""),
|
|
313
|
+
},
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
case "stream/end": {
|
|
317
|
+
if (!state.activeStream) return state;
|
|
318
|
+
const lines = String(state.activeStream.text || "").split(/\r?\n/);
|
|
319
|
+
const prefix = state.activeStream.publisher
|
|
320
|
+
? `${state.activeStream.publisher}: `
|
|
321
|
+
: "";
|
|
322
|
+
const annotated = prefix && lines.length > 0
|
|
323
|
+
? [`${prefix}${lines[0]}`, ...lines.slice(1).map((l) => ` ${l}`)]
|
|
324
|
+
: lines;
|
|
325
|
+
const next = appendLog(state, annotated);
|
|
326
|
+
return { ...next, activeStream: null };
|
|
327
|
+
}
|
|
328
|
+
case "agentView/enter":
|
|
329
|
+
return { ...state, viewingAgentId: action.agentId || null };
|
|
330
|
+
case "agentView/exit":
|
|
331
|
+
return { ...state, viewingAgentId: null };
|
|
332
|
+
default:
|
|
333
|
+
return state;
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
module.exports = { reducer, createInitialState, DASHBOARD_VIEWS };
|