u-foo 3.0.10 → 3.0.12
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 +25 -9
- package/README.zh-CN.md +22 -9
- package/SKILLS/ubus/SKILL.md +15 -9
- package/SKILLS/ufoo/SKILL.md +20 -6
- package/dist/tui/darwin-arm64/ufoo-tui +0 -0
- package/dist/tui/darwin-x64/ufoo-tui +0 -0
- package/dist/tui/linux-arm64/ufoo-tui +0 -0
- package/dist/tui/linux-x64/ufoo-tui +0 -0
- package/package.json +11 -3
- package/scripts/pack-tui.js +112 -0
- package/scripts/postinstall.js +11 -0
- package/src/agents/activity/activityReconcile.js +106 -0
- package/src/agents/activity/activityStatePublisher.js +31 -2
- package/src/agents/activity/index.js +1 -0
- package/src/agents/launch/launcher.js +19 -0
- package/src/agents/launch/ptyRunner.js +20 -1
- package/src/agents/prompts/groupBootstrap.js +4 -0
- package/src/agents/prompts/native/ufoo.js +2 -0
- package/src/app/chat/ChatController.js +433 -0
- package/src/app/chat/agentDirectory.js +63 -0
- package/src/app/chat/agentEnter.js +70 -0
- package/src/app/chat/agentIdentity.js +50 -0
- package/src/app/chat/bootstrap.js +66 -0
- package/src/app/chat/commandExecutor.js +108 -0
- package/src/app/chat/commands.js +31 -0
- package/src/app/chat/dashboardView.js +6 -2
- package/src/app/chat/historyStore.js +181 -0
- package/src/app/chat/index.js +14 -2
- package/src/app/chat/inputSubmitHandler.js +21 -7
- package/src/app/chat/ipcBuilders.js +52 -0
- package/src/app/chat/multiWindow/paneManager.js +10 -1
- package/src/app/chat/multiWindow/renderer.js +1 -1
- package/src/app/chat/multiWindow/vtFrame.js +93 -0
- package/src/app/chat/streamState.js +182 -0
- package/src/app/cli/features/doctor.js +22 -0
- package/src/code/UCODE_PROMPT.md +2 -0
- package/src/code/UcodeController.js +156 -0
- package/src/code/context/planGraphService.js +4 -0
- package/src/code/repl.js +4 -3
- package/src/code/runtime/taskLoop.js +46 -50
- package/src/code/tui.js +13 -2
- package/src/code/ucodeSlashDispatch.js +241 -0
- package/src/coordination/bus/activate.js +3 -0
- package/src/runtime/contracts/schemas/ufoo-ui-v1/envelope.json +28 -0
- package/src/runtime/contracts/uiProtocol.js +190 -0
- package/src/ui/{ink/chatLogModel.js → chatLogModel.js} +2 -2
- package/src/ui/dashboardBridge.js +81 -0
- package/src/ui/format/index.js +2 -2
- package/src/ui/index.js +8 -4
- package/src/ui/multiPaneBusMirror.js +137 -0
- package/src/ui/multiWindowHandoff.js +232 -0
- package/src/ui/ptyHandoff.js +23 -0
- package/src/ui/rustChatHost.js +1520 -0
- package/src/ui/rustMultiSession.js +497 -0
- package/src/ui/rustUcodeHost.js +999 -0
- package/src/ui/scrollbackReplay.js +82 -0
- package/src/ui/settingsBridge.js +49 -0
- package/src/ui/toolMergeBridge.js +66 -0
- package/src/ui/tuiLauncher.js +105 -0
- package/src/ui/ucodeStatusLine.js +74 -0
- package/src/ui/uiHostServer.js +339 -0
- package/src/ui/MIGRATION.md +0 -334
- package/src/ui/ink/ChatApp.js +0 -4163
- package/src/ui/ink/DashboardBar.js +0 -691
- package/src/ui/ink/InkDemo.js +0 -96
- package/src/ui/ink/MultilineInput.js +0 -662
- package/src/ui/ink/UcodeApp.js +0 -1675
- package/src/ui/ink/agentMirror.js +0 -730
- package/src/ui/ink/chatReducer.js +0 -473
- package/src/ui/runInk.js +0 -66
|
@@ -1,473 +0,0 @@
|
|
|
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
|
-
const { createChatLogEntry } = require("./chatLogModel");
|
|
39
|
-
|
|
40
|
-
const LOG_CAP = 1000;
|
|
41
|
-
const HISTORY_CAP = 200;
|
|
42
|
-
const DASHBOARD_VIEWS = ["projects", "agents", "mode", "provider", "cron"];
|
|
43
|
-
const DEFAULT_PROVIDER_OPTIONS = [
|
|
44
|
-
{ label: "codex", value: "codex-cli" },
|
|
45
|
-
{ label: "claude", value: "claude-cli" },
|
|
46
|
-
{ label: "agy", value: "agy-cli" },
|
|
47
|
-
{ label: "kimi", value: "kimi-cli" },
|
|
48
|
-
];
|
|
49
|
-
function projectRootOf(row = {}) {
|
|
50
|
-
return String((row && (row.root || row.project_root || row.projectRoot)) || "");
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
function stableJson(value) {
|
|
54
|
-
if (value === null || typeof value !== "object") return JSON.stringify(value);
|
|
55
|
-
if (Array.isArray(value)) return `[${value.map(stableJson).join(",")}]`;
|
|
56
|
-
const keys = Object.keys(value).sort();
|
|
57
|
-
return `{${keys.map((key) => `${JSON.stringify(key)}:${stableJson(value[key])}`).join(",")}}`;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
function shallowArrayEqual(left = [], right = []) {
|
|
61
|
-
if (left === right) return true;
|
|
62
|
-
if (!Array.isArray(left) || !Array.isArray(right)) return false;
|
|
63
|
-
if (left.length !== right.length) return false;
|
|
64
|
-
for (let i = 0; i < left.length; i += 1) {
|
|
65
|
-
if (left[i] !== right[i]) return false;
|
|
66
|
-
}
|
|
67
|
-
return true;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
function listPayloadEqual(left = [], right = []) {
|
|
71
|
-
if (left === right) return true;
|
|
72
|
-
if (!Array.isArray(left) || !Array.isArray(right)) return false;
|
|
73
|
-
if (left.length !== right.length) return false;
|
|
74
|
-
for (let i = 0; i < left.length; i += 1) {
|
|
75
|
-
if (stableJson(left[i]) !== stableJson(right[i])) return false;
|
|
76
|
-
}
|
|
77
|
-
return true;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
function mapPayloadEqual(left, right) {
|
|
81
|
-
if (left === right) return true;
|
|
82
|
-
if (!(left instanceof Map) || !(right instanceof Map)) return false;
|
|
83
|
-
if (left.size !== right.size) return false;
|
|
84
|
-
for (const [key, value] of left.entries()) {
|
|
85
|
-
if (!right.has(key)) return false;
|
|
86
|
-
if (stableJson(value) !== stableJson(right.get(key))) return false;
|
|
87
|
-
}
|
|
88
|
-
return true;
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
function statusPayloadEqual(left, right) {
|
|
92
|
-
const normalize = (value = {}) => {
|
|
93
|
-
const normalized = { ...value };
|
|
94
|
-
if (normalized.showTimer !== true) normalized.startedAt = 0;
|
|
95
|
-
return normalized;
|
|
96
|
-
};
|
|
97
|
-
return stableJson(normalize(left)) === stableJson(normalize(right));
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
function createInitialState({ banner = [], globalMode = false, globalScope = "controller", settings = {} } = {}) {
|
|
101
|
-
const initialLaunchMode = settings.launchMode || "auto";
|
|
102
|
-
const initialAgentProvider = settings.agentProvider || "codex-cli";
|
|
103
|
-
const selectedProviderIndex = Math.max(0, DEFAULT_PROVIDER_OPTIONS.findIndex((opt) => opt.value === initialAgentProvider));
|
|
104
|
-
const seed = Array.isArray(banner) ? banner.concat([""]) : [""];
|
|
105
|
-
return {
|
|
106
|
-
logLines: seed.map((line, idx) => {
|
|
107
|
-
// History reload may pass structured `{ text, sourceType }` rows —
|
|
108
|
-
// don't wrap those objects as `text` (that yields "[object Object]").
|
|
109
|
-
if (line && typeof line === "object" && !Array.isArray(line)) {
|
|
110
|
-
return createChatLogEntry(line, `b-${idx}`);
|
|
111
|
-
}
|
|
112
|
-
return createChatLogEntry({
|
|
113
|
-
text: line,
|
|
114
|
-
sourceType: "banner",
|
|
115
|
-
}, `b-${idx}`);
|
|
116
|
-
}),
|
|
117
|
-
lineSeq: seed.length,
|
|
118
|
-
draft: "",
|
|
119
|
-
focusMode: "input",
|
|
120
|
-
dashboardView: globalMode ? "projects" : "agents",
|
|
121
|
-
globalMode,
|
|
122
|
-
globalScope,
|
|
123
|
-
agents: [],
|
|
124
|
-
activeAgentMeta: new Map(),
|
|
125
|
-
selectedAgentIndex: -1,
|
|
126
|
-
agentSelectionMode: false,
|
|
127
|
-
agentListWindowStart: 0,
|
|
128
|
-
projects: [],
|
|
129
|
-
selectedProjectIndex: -1,
|
|
130
|
-
selectedProjectRoot: "",
|
|
131
|
-
projectListWindowStart: 0,
|
|
132
|
-
emptyProjectsDownArmed: false,
|
|
133
|
-
activeProjectRoot: "",
|
|
134
|
-
modeOptions: ["auto", "host", "terminal", "tmux", "internal"],
|
|
135
|
-
selectedModeIndex: Math.max(0, ["auto", "host", "terminal", "tmux", "internal"].indexOf(initialLaunchMode)),
|
|
136
|
-
providerOptions: DEFAULT_PROVIDER_OPTIONS,
|
|
137
|
-
selectedProviderIndex,
|
|
138
|
-
cronTasks: [],
|
|
139
|
-
selectedCronIndex: -1,
|
|
140
|
-
loopSummary: null,
|
|
141
|
-
viewingAgentId: null,
|
|
142
|
-
// activeStream is the in-flight chunk-by-chunk publisher message (set
|
|
143
|
-
// while the daemon is streaming). Rendered live below <Static>;
|
|
144
|
-
// promoted to <Static> when the stream finishes the same way the
|
|
145
|
-
// tool-merge group is. Text accumulates in `chunks` (joined on read via
|
|
146
|
-
// activeStreamText) so each delta dispatch is O(1) instead of O(n)
|
|
147
|
-
// string concatenation.
|
|
148
|
-
activeStream: null,
|
|
149
|
-
inputHistory: [],
|
|
150
|
-
historyIndex: 0,
|
|
151
|
-
activeMerge: null,
|
|
152
|
-
lastMerge: null,
|
|
153
|
-
mergeId: 0,
|
|
154
|
-
status: { message: "", type: "thinking", showTimer: false, startedAt: 0 },
|
|
155
|
-
settings: { launchMode: initialLaunchMode, agentProvider: initialAgentProvider, autoResume: settings.autoResume === true },
|
|
156
|
-
};
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
function appendLog(state, lines) {
|
|
160
|
-
const incoming = Array.isArray(lines) ? lines : [lines];
|
|
161
|
-
let seq = state.lineSeq;
|
|
162
|
-
const out = state.logLines.concat(incoming.map((line) => {
|
|
163
|
-
const id = `l-${seq}`;
|
|
164
|
-
seq += 1;
|
|
165
|
-
return createChatLogEntry(line, id);
|
|
166
|
-
}));
|
|
167
|
-
return {
|
|
168
|
-
...state,
|
|
169
|
-
logLines: out.length > LOG_CAP ? out.slice(-LOG_CAP) : out,
|
|
170
|
-
lineSeq: seq,
|
|
171
|
-
};
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
function freezeMergeIntoLog(state) {
|
|
175
|
-
if (!state.activeMerge) return state;
|
|
176
|
-
const summary = fmt.buildToolMergeRowText(state.activeMerge.entries);
|
|
177
|
-
return appendLog({ ...state, activeMerge: null }, summary);
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
// Joined text of the in-flight stream. Deltas accumulate in `chunks` so the
|
|
181
|
-
// reducer never does per-delta string concatenation (O(n²) over a stream);
|
|
182
|
-
// readers pay a single O(n) join when they actually need the text.
|
|
183
|
-
function activeStreamText(stream) {
|
|
184
|
-
if (!stream || typeof stream !== "object") return "";
|
|
185
|
-
if (Array.isArray(stream.chunks)) return stream.chunks.join("");
|
|
186
|
-
return String(stream.text || "");
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
function reducer(state, action) {
|
|
190
|
-
if (!action || !action.type) return state;
|
|
191
|
-
switch (action.type) {
|
|
192
|
-
case "log/append":
|
|
193
|
-
return appendLog(freezeMergeIntoLog(state), action.text);
|
|
194
|
-
case "log/appendMany":
|
|
195
|
-
return appendLog(freezeMergeIntoLog(state), action.lines);
|
|
196
|
-
case "log/clear":
|
|
197
|
-
return {
|
|
198
|
-
...state,
|
|
199
|
-
logLines: [],
|
|
200
|
-
lineSeq: 0,
|
|
201
|
-
activeMerge: null,
|
|
202
|
-
};
|
|
203
|
-
case "draft/set":
|
|
204
|
-
return { ...state, draft: String(action.value || "") };
|
|
205
|
-
case "draft/clear":
|
|
206
|
-
return { ...state, draft: "" };
|
|
207
|
-
case "focus/toggle":
|
|
208
|
-
return {
|
|
209
|
-
...state,
|
|
210
|
-
focusMode: state.focusMode === "input" ? "dashboard" : "input",
|
|
211
|
-
emptyProjectsDownArmed: state.focusMode === "input" ? state.emptyProjectsDownArmed : false,
|
|
212
|
-
};
|
|
213
|
-
case "focus/set":
|
|
214
|
-
return {
|
|
215
|
-
...state,
|
|
216
|
-
focusMode: action.mode === "dashboard" ? "dashboard" : "input",
|
|
217
|
-
emptyProjectsDownArmed: action.mode === "dashboard" ? state.emptyProjectsDownArmed : false,
|
|
218
|
-
};
|
|
219
|
-
case "view/set": {
|
|
220
|
-
const view = action.view;
|
|
221
|
-
const inAgentsView = view === "agents";
|
|
222
|
-
return {
|
|
223
|
-
...state,
|
|
224
|
-
dashboardView: view,
|
|
225
|
-
agentSelectionMode: inAgentsView && state.focusMode === "dashboard" && state.selectedAgentIndex >= 0,
|
|
226
|
-
emptyProjectsDownArmed: view === "projects" ? state.emptyProjectsDownArmed : false,
|
|
227
|
-
};
|
|
228
|
-
}
|
|
229
|
-
case "view/cycle": {
|
|
230
|
-
const i = DASHBOARD_VIEWS.indexOf(state.dashboardView);
|
|
231
|
-
const direction = action.direction === "left" ? -1 : 1;
|
|
232
|
-
const start = i < 0 ? 0 : i;
|
|
233
|
-
const next = Math.max(0, Math.min(DASHBOARD_VIEWS.length - 1, start + direction));
|
|
234
|
-
return { ...state, dashboardView: DASHBOARD_VIEWS[next] };
|
|
235
|
-
}
|
|
236
|
-
case "agents/set": {
|
|
237
|
-
const list = Array.isArray(action.list) ? action.list : [];
|
|
238
|
-
const ids = list.map((a) => a.fullId || `${a.type}:${a.id}`);
|
|
239
|
-
const meta = new Map(list.map((a) => [a.fullId || `${a.type}:${a.id}`, a]));
|
|
240
|
-
let nextIdx = state.selectedAgentIndex;
|
|
241
|
-
let nextMode = state.agentSelectionMode;
|
|
242
|
-
if (ids.length === 0) {
|
|
243
|
-
nextIdx = -1;
|
|
244
|
-
nextMode = false;
|
|
245
|
-
} else if (nextIdx >= ids.length) {
|
|
246
|
-
nextIdx = ids.length - 1;
|
|
247
|
-
}
|
|
248
|
-
if (
|
|
249
|
-
shallowArrayEqual(state.agents, ids) &&
|
|
250
|
-
mapPayloadEqual(state.activeAgentMeta, meta) &&
|
|
251
|
-
state.selectedAgentIndex === nextIdx &&
|
|
252
|
-
state.agentSelectionMode === nextMode
|
|
253
|
-
) {
|
|
254
|
-
return state;
|
|
255
|
-
}
|
|
256
|
-
return {
|
|
257
|
-
...state,
|
|
258
|
-
agents: ids,
|
|
259
|
-
activeAgentMeta: meta,
|
|
260
|
-
selectedAgentIndex: nextIdx,
|
|
261
|
-
agentSelectionMode: nextMode,
|
|
262
|
-
};
|
|
263
|
-
}
|
|
264
|
-
case "agents/patchMeta": {
|
|
265
|
-
const agentId = String(action.agentId || "").trim();
|
|
266
|
-
if (!agentId) return state;
|
|
267
|
-
const patch = action.patch || {};
|
|
268
|
-
const patchKeys = Object.keys(patch);
|
|
269
|
-
const current = (state.activeAgentMeta instanceof Map ? state.activeAgentMeta.get(agentId) : null) || {};
|
|
270
|
-
// Skip no-op patches: activity updates stream in at a high rate and an
|
|
271
|
-
// unchanged patch would still mint a new Map + state, forcing a full
|
|
272
|
-
// re-render of the Ink tree.
|
|
273
|
-
if (patchKeys.every((key) => stableJson(current[key]) === stableJson(patch[key]))) {
|
|
274
|
-
return state;
|
|
275
|
-
}
|
|
276
|
-
const meta = new Map(state.activeAgentMeta instanceof Map ? state.activeAgentMeta : []);
|
|
277
|
-
meta.set(agentId, { ...current, ...patch });
|
|
278
|
-
return { ...state, activeAgentMeta: meta };
|
|
279
|
-
}
|
|
280
|
-
case "agents/select":
|
|
281
|
-
return {
|
|
282
|
-
...state,
|
|
283
|
-
selectedAgentIndex: action.index,
|
|
284
|
-
agentSelectionMode: action.index >= 0,
|
|
285
|
-
};
|
|
286
|
-
case "agents/cycle": {
|
|
287
|
-
if (state.agents.length === 0) return state;
|
|
288
|
-
const cur = state.selectedAgentIndex < 0 ? 0 : state.selectedAgentIndex;
|
|
289
|
-
const next = action.direction === "left"
|
|
290
|
-
? Math.max(0, cur - 1)
|
|
291
|
-
: Math.min(state.agents.length - 1, cur + 1);
|
|
292
|
-
return { ...state, selectedAgentIndex: next, agentSelectionMode: true };
|
|
293
|
-
}
|
|
294
|
-
case "agents/clearTarget":
|
|
295
|
-
return { ...state, selectedAgentIndex: -1, agentSelectionMode: false };
|
|
296
|
-
case "agents/window":
|
|
297
|
-
return { ...state, agentListWindowStart: action.windowStart };
|
|
298
|
-
case "projects/set": {
|
|
299
|
-
const list = Array.isArray(action.list) ? action.list : [];
|
|
300
|
-
const previousSelectedRoot = state.selectedProjectRoot
|
|
301
|
-
|| projectRootOf(state.projects[state.selectedProjectIndex]);
|
|
302
|
-
const selectedRoot = String(action.selectedProjectRoot || previousSelectedRoot || "");
|
|
303
|
-
const selectedIndex = selectedRoot
|
|
304
|
-
? list.findIndex((row) => projectRootOf(row) === selectedRoot)
|
|
305
|
-
: -1;
|
|
306
|
-
const nextActiveRoot = action.activeProjectRoot || state.activeProjectRoot;
|
|
307
|
-
if (
|
|
308
|
-
listPayloadEqual(state.projects, list) &&
|
|
309
|
-
state.selectedProjectRoot === (selectedIndex >= 0 ? selectedRoot : "") &&
|
|
310
|
-
state.selectedProjectIndex === selectedIndex &&
|
|
311
|
-
state.activeProjectRoot === nextActiveRoot &&
|
|
312
|
-
state.emptyProjectsDownArmed === (list.length === 0 ? state.emptyProjectsDownArmed : false)
|
|
313
|
-
) {
|
|
314
|
-
return state;
|
|
315
|
-
}
|
|
316
|
-
return {
|
|
317
|
-
...state,
|
|
318
|
-
projects: list,
|
|
319
|
-
selectedProjectRoot: selectedIndex >= 0 ? selectedRoot : "",
|
|
320
|
-
selectedProjectIndex: selectedIndex,
|
|
321
|
-
activeProjectRoot: nextActiveRoot,
|
|
322
|
-
emptyProjectsDownArmed: list.length === 0 ? state.emptyProjectsDownArmed : false,
|
|
323
|
-
};
|
|
324
|
-
}
|
|
325
|
-
case "projects/select":
|
|
326
|
-
return {
|
|
327
|
-
...state,
|
|
328
|
-
selectedProjectIndex: action.index,
|
|
329
|
-
selectedProjectRoot: String(action.projectRoot || projectRootOf(state.projects[action.index]) || ""),
|
|
330
|
-
emptyProjectsDownArmed: false,
|
|
331
|
-
};
|
|
332
|
-
case "projects/clearSelection":
|
|
333
|
-
return { ...state, selectedProjectIndex: -1, selectedProjectRoot: "", emptyProjectsDownArmed: false };
|
|
334
|
-
case "projects/armEmptyDown":
|
|
335
|
-
return { ...state, emptyProjectsDownArmed: true };
|
|
336
|
-
case "projects/window":
|
|
337
|
-
return { ...state, projectListWindowStart: Math.max(0, action.windowStart | 0) };
|
|
338
|
-
case "scope/set":
|
|
339
|
-
return { ...state, globalScope: action.scope === "project" ? "project" : "controller" };
|
|
340
|
-
case "status/set": {
|
|
341
|
-
const nextStatus = { ...state.status, ...action.payload };
|
|
342
|
-
if (statusPayloadEqual(state.status, nextStatus)) return state;
|
|
343
|
-
return { ...state, status: nextStatus };
|
|
344
|
-
}
|
|
345
|
-
case "status/idle":
|
|
346
|
-
return { ...state, status: { message: "", type: "thinking", showTimer: false, startedAt: 0 } };
|
|
347
|
-
case "history/push": {
|
|
348
|
-
const value = String(action.value || "").trim();
|
|
349
|
-
if (!value) return state;
|
|
350
|
-
const next = state.inputHistory.concat([value]).slice(-HISTORY_CAP);
|
|
351
|
-
return { ...state, inputHistory: next, historyIndex: next.length };
|
|
352
|
-
}
|
|
353
|
-
case "history/load": {
|
|
354
|
-
const list = Array.isArray(action.list) ? action.list : [];
|
|
355
|
-
const next = list.slice(-HISTORY_CAP);
|
|
356
|
-
return { ...state, inputHistory: next, historyIndex: next.length };
|
|
357
|
-
}
|
|
358
|
-
case "history/setIndex":
|
|
359
|
-
return { ...state, historyIndex: Math.max(0, Math.min(state.inputHistory.length, action.index)) };
|
|
360
|
-
case "merge/append": {
|
|
361
|
-
const entry = fmt.normalizeToolMergeEntry(action.entry || {});
|
|
362
|
-
let next;
|
|
363
|
-
if (state.activeMerge) {
|
|
364
|
-
next = { ...state.activeMerge, entries: state.activeMerge.entries.concat([entry]) };
|
|
365
|
-
} else {
|
|
366
|
-
next = { id: state.mergeId + 1, entries: [entry], expanded: false };
|
|
367
|
-
}
|
|
368
|
-
const lastMerge = next.entries.length >= 2 ? next : state.lastMerge;
|
|
369
|
-
return {
|
|
370
|
-
...state,
|
|
371
|
-
activeMerge: next,
|
|
372
|
-
lastMerge,
|
|
373
|
-
mergeId: state.activeMerge ? state.mergeId : state.mergeId + 1,
|
|
374
|
-
};
|
|
375
|
-
}
|
|
376
|
-
case "merge/flush":
|
|
377
|
-
return freezeMergeIntoLog(state);
|
|
378
|
-
case "merge/expand": {
|
|
379
|
-
const candidate = (state.activeMerge && !state.activeMerge.expanded && state.activeMerge.entries.length >= 2)
|
|
380
|
-
? state.activeMerge
|
|
381
|
-
: (state.lastMerge && !state.lastMerge.expanded && state.lastMerge.entries.length >= 2
|
|
382
|
-
? state.lastMerge
|
|
383
|
-
: null);
|
|
384
|
-
if (!candidate) return state;
|
|
385
|
-
const lines = fmt.buildMergedToolExpandedLines(candidate.entries).map((line, i, arr) =>
|
|
386
|
-
`${i === arr.length - 1 ? "└" : "│"} ${line}`
|
|
387
|
-
);
|
|
388
|
-
const after = appendLog(state, lines);
|
|
389
|
-
return {
|
|
390
|
-
...after,
|
|
391
|
-
activeMerge: state.activeMerge && state.activeMerge.id === candidate.id ? null : state.activeMerge,
|
|
392
|
-
lastMerge: state.lastMerge && state.lastMerge.id === candidate.id
|
|
393
|
-
? { ...state.lastMerge, expanded: true }
|
|
394
|
-
: state.lastMerge,
|
|
395
|
-
};
|
|
396
|
-
}
|
|
397
|
-
case "settings/set":
|
|
398
|
-
return { ...state, settings: { ...state.settings, ...(action.patch || {}) } };
|
|
399
|
-
case "settings/applyMode": {
|
|
400
|
-
const mode = state.modeOptions[state.selectedModeIndex] || state.settings.launchMode;
|
|
401
|
-
return { ...state, settings: { ...state.settings, launchMode: mode } };
|
|
402
|
-
}
|
|
403
|
-
case "settings/applyProvider": {
|
|
404
|
-
const selected = state.providerOptions[state.selectedProviderIndex];
|
|
405
|
-
const agentProvider = selected && selected.value ? selected.value : state.settings.agentProvider;
|
|
406
|
-
return { ...state, settings: { ...state.settings, agentProvider } };
|
|
407
|
-
}
|
|
408
|
-
case "modeIndex/set":
|
|
409
|
-
return { ...state, selectedModeIndex: Math.max(0, action.index | 0) };
|
|
410
|
-
case "providerIndex/set":
|
|
411
|
-
return { ...state, selectedProviderIndex: Math.max(0, action.index | 0) };
|
|
412
|
-
case "cronIndex/set":
|
|
413
|
-
return { ...state, selectedCronIndex: Math.max(-1, action.index | 0) };
|
|
414
|
-
case "cron/set": {
|
|
415
|
-
const list = Array.isArray(action.list) ? action.list : [];
|
|
416
|
-
if (listPayloadEqual(state.cronTasks, list)) return state;
|
|
417
|
-
return { ...state, cronTasks: list };
|
|
418
|
-
}
|
|
419
|
-
case "loop/set": {
|
|
420
|
-
const summary = action.summary && typeof action.summary === "object" ? action.summary : null;
|
|
421
|
-
if (stableJson(state.loopSummary) === stableJson(summary)) return state;
|
|
422
|
-
return { ...state, loopSummary: summary };
|
|
423
|
-
}
|
|
424
|
-
case "stream/begin":
|
|
425
|
-
return {
|
|
426
|
-
...state,
|
|
427
|
-
activeStream: { publisher: action.publisher || "", chunks: [] },
|
|
428
|
-
};
|
|
429
|
-
case "stream/delta": {
|
|
430
|
-
const delta = String(action.delta || "");
|
|
431
|
-
if (!state.activeStream) {
|
|
432
|
-
return {
|
|
433
|
-
...state,
|
|
434
|
-
activeStream: { publisher: action.publisher || "", chunks: [delta] },
|
|
435
|
-
};
|
|
436
|
-
}
|
|
437
|
-
const chunks = Array.isArray(state.activeStream.chunks)
|
|
438
|
-
? state.activeStream.chunks
|
|
439
|
-
: [String(state.activeStream.text || "")];
|
|
440
|
-
return {
|
|
441
|
-
...state,
|
|
442
|
-
activeStream: {
|
|
443
|
-
...state.activeStream,
|
|
444
|
-
chunks: chunks.concat([delta]),
|
|
445
|
-
},
|
|
446
|
-
};
|
|
447
|
-
}
|
|
448
|
-
case "stream/end": {
|
|
449
|
-
if (!state.activeStream) return state;
|
|
450
|
-
const lines = activeStreamText(state.activeStream).split(/\r?\n/);
|
|
451
|
-
const prefix = state.activeStream.publisher
|
|
452
|
-
? `${state.activeStream.publisher}: `
|
|
453
|
-
: "";
|
|
454
|
-
const annotated = prefix && lines.length > 0
|
|
455
|
-
? [`${prefix}${lines[0]}`, ...lines.slice(1).map((l) => ` ${l}`)]
|
|
456
|
-
: lines;
|
|
457
|
-
const next = appendLog(state, annotated.map((text) => ({
|
|
458
|
-
text,
|
|
459
|
-
type: "bus",
|
|
460
|
-
sourceType: "bus",
|
|
461
|
-
})));
|
|
462
|
-
return { ...next, activeStream: null };
|
|
463
|
-
}
|
|
464
|
-
case "agentView/enter":
|
|
465
|
-
return { ...state, viewingAgentId: action.agentId || null };
|
|
466
|
-
case "agentView/exit":
|
|
467
|
-
return { ...state, viewingAgentId: null };
|
|
468
|
-
default:
|
|
469
|
-
return state;
|
|
470
|
-
}
|
|
471
|
-
}
|
|
472
|
-
|
|
473
|
-
module.exports = { reducer, createInitialState, DASHBOARD_VIEWS, activeStreamText };
|
package/src/ui/runInk.js
DELETED
|
@@ -1,66 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Ink runtime launcher for ufoo TUIs.
|
|
5
|
-
*
|
|
6
|
-
* ufoo is CommonJS, ink@5 is ESM-only. We bridge by dynamic-importing ink
|
|
7
|
-
* and exposing a CJS-friendly API that takes a `createRoot(React, ink)`
|
|
8
|
-
* factory instead of a pre-built React element. The factory is invoked with
|
|
9
|
-
* the loaded `React` module and ink namespace so callers don't need to
|
|
10
|
-
* import them themselves.
|
|
11
|
-
*
|
|
12
|
-
* Usage:
|
|
13
|
-
* const { runInk } = require("./runInk");
|
|
14
|
-
* const handle = await runInk(
|
|
15
|
-
* (React, ink) => React.createElement(App, props),
|
|
16
|
-
* { stdin, stdout },
|
|
17
|
-
* );
|
|
18
|
-
* await handle.waitUntilExit();
|
|
19
|
-
*/
|
|
20
|
-
|
|
21
|
-
// Resolve the ink.render options. patchConsole defaults ON so stray
|
|
22
|
-
// console.log/warn output is routed through Ink (rendered above the live
|
|
23
|
-
// frame) instead of punching holes through the repainted frame — direct
|
|
24
|
-
// stdout writes mid-frame are a classic cause of visible flicker/tearing.
|
|
25
|
-
function resolveInkRenderOptions(options = {}) {
|
|
26
|
-
return {
|
|
27
|
-
stdin: options.stdin || process.stdin,
|
|
28
|
-
stdout: options.stdout || process.stdout,
|
|
29
|
-
stderr: options.stderr || process.stderr,
|
|
30
|
-
exitOnCtrlC: options.exitOnCtrlC !== false,
|
|
31
|
-
patchConsole: options.patchConsole !== false,
|
|
32
|
-
};
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
async function runInk(createRoot, options = {}) {
|
|
36
|
-
const ink = await import("ink");
|
|
37
|
-
const React = require("react");
|
|
38
|
-
const element = createRoot(React, ink);
|
|
39
|
-
if (!element) throw new Error("runInk: createRoot returned no element");
|
|
40
|
-
const renderOptions = resolveInkRenderOptions(options);
|
|
41
|
-
const stdout = renderOptions.stdout;
|
|
42
|
-
const inst = ink.render(element, renderOptions);
|
|
43
|
-
// ink keeps the final rendered frame on screen when it unmounts (it writes
|
|
44
|
-
// the frame as plain stdout output), which leaves the TUI lingering after
|
|
45
|
-
// Ctrl+C. Clear and home the cursor on every clean exit so the shell prompt
|
|
46
|
-
// returns to the top of a blank screen.
|
|
47
|
-
const wrappedExit = inst.waitUntilExit().then((value) => {
|
|
48
|
-
try {
|
|
49
|
-
if (stdout && stdout.isTTY && typeof stdout.write === "function") {
|
|
50
|
-
stdout.write("\x1b[2J\x1b[H");
|
|
51
|
-
}
|
|
52
|
-
} catch { /* ignore */ }
|
|
53
|
-
return value;
|
|
54
|
-
});
|
|
55
|
-
return {
|
|
56
|
-
waitUntilExit: () => wrappedExit,
|
|
57
|
-
rerender: (next) => inst.rerender(next),
|
|
58
|
-
unmount: () => inst.unmount(),
|
|
59
|
-
cleanup: () => inst.cleanup(),
|
|
60
|
-
instance: inst,
|
|
61
|
-
React,
|
|
62
|
-
ink,
|
|
63
|
-
};
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
module.exports = { runInk, resolveInkRenderOptions };
|