u-foo 3.0.9 → 3.0.11

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 (65) hide show
  1. package/README.md +25 -9
  2. package/README.zh-CN.md +22 -9
  3. package/dist/tui/darwin-arm64/ufoo-tui +0 -0
  4. package/dist/tui/darwin-x64/ufoo-tui +0 -0
  5. package/dist/tui/linux-arm64/ufoo-tui +0 -0
  6. package/dist/tui/linux-x64/ufoo-tui +0 -0
  7. package/package.json +12 -4
  8. package/scripts/pack-tui.js +112 -0
  9. package/scripts/postinstall.js +11 -0
  10. package/src/agents/activity/activityReconcile.js +106 -0
  11. package/src/agents/activity/activityStatePublisher.js +31 -2
  12. package/src/agents/activity/index.js +1 -0
  13. package/src/agents/launch/launcher.js +19 -0
  14. package/src/agents/launch/ptyRunner.js +20 -1
  15. package/src/app/chat/ChatController.js +433 -0
  16. package/src/app/chat/agentDirectory.js +63 -0
  17. package/src/app/chat/agentEnter.js +70 -0
  18. package/src/app/chat/agentIdentity.js +50 -0
  19. package/src/app/chat/bootstrap.js +66 -0
  20. package/src/app/chat/commandExecutor.js +108 -0
  21. package/src/app/chat/commands.js +38 -1
  22. package/src/app/chat/dashboardView.js +6 -2
  23. package/src/app/chat/historyStore.js +181 -0
  24. package/src/app/chat/index.js +14 -2
  25. package/src/app/chat/inputSubmitHandler.js +21 -7
  26. package/src/app/chat/ipcBuilders.js +52 -0
  27. package/src/app/chat/multiWindow/paneManager.js +10 -1
  28. package/src/app/chat/multiWindow/renderer.js +1 -1
  29. package/src/app/chat/multiWindow/vtFrame.js +93 -0
  30. package/src/app/chat/streamState.js +182 -0
  31. package/src/app/cli/features/doctor.js +22 -0
  32. package/src/code/UcodeController.js +156 -0
  33. package/src/code/context/planGraphService.js +4 -0
  34. package/src/code/repl.js +4 -3
  35. package/src/code/runtime/taskLoop.js +46 -50
  36. package/src/code/tui.js +13 -2
  37. package/src/code/ucodeSlashDispatch.js +241 -0
  38. package/src/coordination/bus/activate.js +3 -0
  39. package/src/runtime/contracts/schemas/ufoo-ui-v1/envelope.json +28 -0
  40. package/src/runtime/contracts/uiProtocol.js +190 -0
  41. package/src/ui/{ink/chatLogModel.js → chatLogModel.js} +2 -2
  42. package/src/ui/dashboardBridge.js +81 -0
  43. package/src/ui/format/index.js +2 -2
  44. package/src/ui/index.js +8 -4
  45. package/src/ui/multiPaneBusMirror.js +137 -0
  46. package/src/ui/multiWindowHandoff.js +232 -0
  47. package/src/ui/ptyHandoff.js +23 -0
  48. package/src/ui/rustChatHost.js +1520 -0
  49. package/src/ui/rustMultiSession.js +497 -0
  50. package/src/ui/rustUcodeHost.js +999 -0
  51. package/src/ui/scrollbackReplay.js +82 -0
  52. package/src/ui/settingsBridge.js +49 -0
  53. package/src/ui/toolMergeBridge.js +66 -0
  54. package/src/ui/tuiLauncher.js +105 -0
  55. package/src/ui/ucodeStatusLine.js +74 -0
  56. package/src/ui/uiHostServer.js +339 -0
  57. package/src/ui/MIGRATION.md +0 -334
  58. package/src/ui/ink/ChatApp.js +0 -4152
  59. package/src/ui/ink/DashboardBar.js +0 -691
  60. package/src/ui/ink/InkDemo.js +0 -96
  61. package/src/ui/ink/MultilineInput.js +0 -662
  62. package/src/ui/ink/UcodeApp.js +0 -1675
  63. package/src/ui/ink/agentMirror.js +0 -730
  64. package/src/ui/ink/chatReducer.js +0 -473
  65. 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 };