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
@@ -0,0 +1,339 @@
1
+ "use strict";
2
+
3
+ /**
4
+ * Minimal Node host for ufoo-ui/1 handshake / event fanout.
5
+ *
6
+ * Security:
7
+ * - Unix socket chmod 0o600 after listen
8
+ * - Shared auth_token required in hello payload (timing-safe compare)
9
+ * - Commands rejected until the client authenticates
10
+ * - Soft frame-size cap to avoid unbounded buffering
11
+ */
12
+
13
+ const net = require("net");
14
+ const fs = require("fs");
15
+ const crypto = require("crypto");
16
+ const {
17
+ PROTOCOL,
18
+ createEnvelope,
19
+ encodeMessage,
20
+ decodeMessage,
21
+ createSeqCounter,
22
+ } = require("../runtime/contracts/uiProtocol");
23
+
24
+ function extractClientCapabilities(env) {
25
+ const payload = env && env.payload && typeof env.payload === "object" ? env.payload : {};
26
+ const raw = payload.capabilities;
27
+ if (!Array.isArray(raw)) return [];
28
+ const out = [];
29
+ for (const item of raw) {
30
+ if (typeof item === "string" && item) out.push(item);
31
+ }
32
+ return out;
33
+ }
34
+
35
+ const MAX_BUFFER_BYTES = 1024 * 1024;
36
+ const SOCKET_MODE = 0o600;
37
+
38
+ function createAuthToken() {
39
+ return crypto.randomBytes(32).toString("hex");
40
+ }
41
+
42
+ function tokensEqual(expected, actual) {
43
+ const a = Buffer.from(String(expected || ""), "utf8");
44
+ const b = Buffer.from(String(actual || ""), "utf8");
45
+ if (a.length === 0 || a.length !== b.length) return false;
46
+ return crypto.timingSafeEqual(a, b);
47
+ }
48
+
49
+ function createUiHostServer({
50
+ socketPath,
51
+ authToken = createAuthToken(),
52
+ packageVersion = require("../../package.json").version,
53
+ capabilities = ["chat"],
54
+ onCommand = null,
55
+ onClientReady = null,
56
+ maxBufferBytes = MAX_BUFFER_BYTES,
57
+ } = {}) {
58
+ if (!socketPath) throw new Error("socketPath required");
59
+ if (!authToken) throw new Error("authToken required");
60
+ try {
61
+ fs.unlinkSync(socketPath);
62
+ } catch {
63
+ // ignore
64
+ }
65
+
66
+ const seq = createSeqCounter();
67
+ const clients = new Set();
68
+ // Latest authenticated client's advertised capabilities. Multi-window
69
+ // gating reads this to decide whether the Rust TUI can render frames.
70
+ let clientCapabilities = [];
71
+ // Lossy backpressure: when a client socket is saturated (write returns
72
+ // false), keep only the latest envelope per key until drain.
73
+ const lossyPendingByClient = new WeakMap();
74
+
75
+ function isLossyEnvelope(envelope) {
76
+ if (!envelope || envelope.kind !== "event") return false;
77
+ // createLossyEvent omits seq; ordered events always have a number seq.
78
+ return envelope.seq == null;
79
+ }
80
+
81
+ function lossyKey(envelope) {
82
+ if (envelope && envelope.name === "multi.pane.frame") {
83
+ const id = envelope.payload && envelope.payload.agent_id;
84
+ return `frame:${id || ""}`;
85
+ }
86
+ return `event:${(envelope && envelope.name) || ""}`;
87
+ }
88
+
89
+ function flushLossyPending(client) {
90
+ const pending = lossyPendingByClient.get(client);
91
+ if (!pending || pending.size === 0) {
92
+ client.__ufooBackpressure = false;
93
+ return;
94
+ }
95
+ const entries = [...pending.entries()];
96
+ pending.clear();
97
+ for (let i = 0; i < entries.length; i += 1) {
98
+ const [key, env] = entries[i];
99
+ try {
100
+ const ok = client.write(encodeMessage(env));
101
+ if (ok === false) {
102
+ pending.set(key, env);
103
+ for (let j = i + 1; j < entries.length; j += 1) {
104
+ pending.set(entries[j][0], entries[j][1]);
105
+ }
106
+ client.__ufooBackpressure = true;
107
+ return;
108
+ }
109
+ } catch {
110
+ clients.delete(client);
111
+ return;
112
+ }
113
+ }
114
+ client.__ufooBackpressure = false;
115
+ }
116
+
117
+ function ensureDrainHook(client) {
118
+ if (client.__ufooDrainHooked) return;
119
+ client.__ufooDrainHooked = true;
120
+ client.on("drain", () => {
121
+ client.__ufooBackpressure = false;
122
+ flushLossyPending(client);
123
+ });
124
+ }
125
+
126
+ function broadcast(envelope) {
127
+ const line = encodeMessage(envelope);
128
+ const lossy = isLossyEnvelope(envelope);
129
+ for (const client of clients) {
130
+ try {
131
+ if (lossy && client.__ufooBackpressure) {
132
+ let pending = lossyPendingByClient.get(client);
133
+ if (!pending) {
134
+ pending = new Map();
135
+ lossyPendingByClient.set(client, pending);
136
+ }
137
+ pending.set(lossyKey(envelope), envelope);
138
+ continue;
139
+ }
140
+ const ok = client.write(line);
141
+ if (ok === false && lossy) {
142
+ client.__ufooBackpressure = true;
143
+ ensureDrainHook(client);
144
+ let pending = lossyPendingByClient.get(client);
145
+ if (!pending) {
146
+ pending = new Map();
147
+ lossyPendingByClient.set(client, pending);
148
+ }
149
+ pending.set(lossyKey(envelope), envelope);
150
+ }
151
+ } catch {
152
+ clients.delete(client);
153
+ }
154
+ }
155
+ }
156
+
157
+ function send(client, envelope) {
158
+ try {
159
+ client.write(encodeMessage(envelope));
160
+ } catch {
161
+ // ignore write failures on closing sockets
162
+ }
163
+ }
164
+
165
+ function rejectAndClose(socket, message) {
166
+ send(socket, createEnvelope({
167
+ kind: "error",
168
+ name: "protocol",
169
+ seq: seq.next(),
170
+ payload: { ok: false, error: message },
171
+ }));
172
+ try {
173
+ socket.destroy();
174
+ } catch {
175
+ // ignore
176
+ }
177
+ }
178
+
179
+ const server = net.createServer((socket) => {
180
+ clients.add(socket);
181
+ let authenticated = false;
182
+ let buffer = Buffer.alloc(0);
183
+
184
+ socket.on("data", (chunk) => {
185
+ buffer = Buffer.concat([buffer, chunk]);
186
+ if (buffer.length > maxBufferBytes) {
187
+ rejectAndClose(socket, "frame buffer exceeded");
188
+ clients.delete(socket);
189
+ return;
190
+ }
191
+
192
+ let newline;
193
+ while ((newline = buffer.indexOf(0x0a)) >= 0) {
194
+ const lineBuf = buffer.subarray(0, newline);
195
+ buffer = buffer.subarray(newline + 1);
196
+ const line = lineBuf.toString("utf8");
197
+ if (!line.trim()) continue;
198
+
199
+ const decoded = decodeMessage(line);
200
+ if (!decoded.ok) {
201
+ send(socket, createEnvelope({
202
+ kind: "error",
203
+ name: "protocol",
204
+ seq: seq.next(),
205
+ payload: { ok: false, errors: decoded.errors },
206
+ }));
207
+ continue;
208
+ }
209
+ const env = decoded.envelope;
210
+
211
+ if (env.kind === "hello") {
212
+ const token = env.payload && env.payload.auth_token;
213
+ if (!tokensEqual(authToken, token)) {
214
+ rejectAndClose(socket, "unauthorized");
215
+ clients.delete(socket);
216
+ return;
217
+ }
218
+ authenticated = true;
219
+ clientCapabilities = extractClientCapabilities(env);
220
+ send(socket, createEnvelope({
221
+ kind: "welcome",
222
+ seq: seq.next(),
223
+ payload: {
224
+ selected_protocol: PROTOCOL,
225
+ package_version: packageVersion,
226
+ capabilities,
227
+ },
228
+ }));
229
+ if (typeof onClientReady === "function") {
230
+ Promise.resolve(onClientReady(socket, env)).catch(() => {});
231
+ }
232
+ continue;
233
+ }
234
+
235
+ if (!authenticated) {
236
+ rejectAndClose(socket, "hello required");
237
+ clients.delete(socket);
238
+ return;
239
+ }
240
+
241
+ if (env.kind === "command" && typeof onCommand === "function") {
242
+ Promise.resolve(onCommand(env))
243
+ .then((result) => {
244
+ send(socket, createEnvelope({
245
+ kind: "result",
246
+ name: env.name,
247
+ requestId: env.request_id,
248
+ seq: seq.next(),
249
+ payload: result && typeof result === "object" ? result : { ok: true },
250
+ }));
251
+ })
252
+ .catch((err) => {
253
+ send(socket, createEnvelope({
254
+ kind: "error",
255
+ name: env.name,
256
+ requestId: env.request_id,
257
+ seq: seq.next(),
258
+ payload: { ok: false, error: err && err.message ? err.message : String(err) },
259
+ }));
260
+ });
261
+ }
262
+ }
263
+ });
264
+ socket.on("close", () => clients.delete(socket));
265
+ socket.on("error", () => clients.delete(socket));
266
+ });
267
+
268
+ return {
269
+ PROTOCOL,
270
+ socketPath,
271
+ authToken,
272
+ listen() {
273
+ return new Promise((resolve, reject) => {
274
+ server.once("error", reject);
275
+ server.listen(socketPath, () => {
276
+ try {
277
+ fs.chmodSync(socketPath, SOCKET_MODE);
278
+ } catch (err) {
279
+ server.close();
280
+ reject(err);
281
+ return;
282
+ }
283
+ resolve(socketPath);
284
+ });
285
+ });
286
+ },
287
+ close() {
288
+ return new Promise((resolve) => {
289
+ for (const client of clients) {
290
+ try {
291
+ client.destroy();
292
+ } catch {
293
+ // ignore
294
+ }
295
+ }
296
+ clients.clear();
297
+ server.close(() => {
298
+ try {
299
+ fs.unlinkSync(socketPath);
300
+ } catch {
301
+ // ignore
302
+ }
303
+ resolve();
304
+ });
305
+ });
306
+ },
307
+ broadcast,
308
+ nextSeq: () => seq.next(),
309
+ createEvent(name, payload, scope = null) {
310
+ return createEnvelope({
311
+ kind: "event",
312
+ name,
313
+ seq: seq.next(),
314
+ scope,
315
+ payload,
316
+ });
317
+ },
318
+ // Lossy events (e.g. multi.pane.frame) omit the ordered `seq` so they
319
+ // cannot trigger the seq-gap resync path when frames are coalesced.
320
+ createLossyEvent(name, payload, scope = null) {
321
+ return createEnvelope({
322
+ kind: "event",
323
+ name,
324
+ scope,
325
+ payload,
326
+ });
327
+ },
328
+ getClientCapabilities() {
329
+ return clientCapabilities.slice();
330
+ },
331
+ };
332
+ }
333
+
334
+ module.exports = {
335
+ createUiHostServer,
336
+ createAuthToken,
337
+ SOCKET_MODE,
338
+ MAX_BUFFER_BYTES,
339
+ };
@@ -1,334 +0,0 @@
1
- # Ink TUI Migration Plan
2
-
3
- Status: Ink is the only TUI for chat and ucode. The legacy blessed renderer
4
- has been removed after the parity close-out.
5
-
6
- ## Why
7
-
8
- The removed legacy chat, ucode and internal-agent TUIs used blessed. Blessed
9
- is an unmaintained imperative widget tree with no modern equivalent of
10
- React's component model, and it was awkward to extend (manual layout math,
11
- manual redraws, no useful test harness).
12
-
13
- ink (the React-for-terminals library, what Claude Code, Codex CLI fronts
14
- and the Gemini CLI all use) gives us declarative components, flexbox
15
- layout, hooks, and proper isolation of pure logic from rendering.
16
-
17
- ## Approach
18
-
19
- - Ink is the renderer for both chat and ucode.
20
- - Pure helpers live in `src/ui/format/`, so behaviour parity is enforced by
21
- test rather than copy/paste.
22
- - Components live in `src/ui/ink/`, written in plain JS via
23
- `React.createElement` (no JSX, no build step) so jest stays vanilla.
24
- - ink is loaded through `src/ui/runInk.js`, a thin CJS→ESM bridge so the
25
- rest of the codebase stays CommonJS.
26
-
27
- ## Progress
28
-
29
- - **P0** ✅ ink + react deps, runtime bridge, `<InkDemo>` smoke harness,
30
- pure helpers extracted to `src/ui/format/`.
31
- - **P1** ✅ ucode TUI ported to ink.
32
- - **P2** ✅ folded into P3.6 (internal agent view).
33
- - **P3** ✅ chat TUI ported to ink. Daemon
34
- connection, dashboard (5 views), tool-merge, status spinner, history,
35
- agent selection, raw-PTY mirror and internal bus agent view are wired.
36
- - **P4** ✅ parity close-out complete: full `commandExecutor` dispatch,
37
- `daemonMessageRouter` callback coverage, persisted history, BUS streams,
38
- transient agent state, loop summary, project rail switching, cron/settings
39
- dashboard actions, completion popup, and default Ink entrypoints.
40
-
41
- ## P1 ucode TUI — what's wired
42
-
43
- | Feature | Status |
44
- |---|---|
45
- | Banner + version + session id header | ✅ |
46
- | Scrolling `<Static>` log (1000 line cap) | ✅ |
47
- | Multiline input (cursor math, Ctrl+A/E/B/F/D/H/K/U/W, Meta+B/F/D, `\\\n` continuation, Alt+Enter newline, CJK wrap) | ✅ |
48
- | Up/Down history walk + agent-selection mode | ✅ |
49
- | Ctrl+C exit, Ctrl+O expand last tool group | ✅ |
50
- | Tool merge/freeze/expand state machine | ✅ |
51
- | Spinner + phase status line (request/thinking/text/tool labels) | ✅ |
52
- | Esc abort with `AbortController` and "Cancelling..." status | ✅ |
53
- | Agents footer with single-line truncation + `+N more` hint | ✅ |
54
- | `runSingleCommand` empty/exit/probe/help/error/tool/nl/ubus/resume/nl_bg kinds | ✅ |
55
- | Background tasks ("BG x/y/z" suffix) | ✅ |
56
- | ubus / resume / nl_bg branches | ✅ |
57
- | autoBus polling | ✅ |
58
-
59
- ## Real-TTY checklist
60
-
61
- ```sh
62
- ./bin/ucode.js
63
- ```
64
-
65
- ### Editor
66
-
67
- - [ ] Type, see characters appear with the cursor on the next cell.
68
- - [ ] Backspace deletes one cell back; cursor stays correct on CJK.
69
- - [ ] Left/Right arrows move the cursor; resetting preferred col.
70
- - [ ] Up/Down on a single line walks input history.
71
- - [ ] Up/Down on a multiline value moves between visual rows.
72
- - [ ] `\` followed by Enter inserts a newline; Enter alone submits.
73
- - [ ] Alt+Enter inserts a newline.
74
- - [ ] Ctrl+A / Ctrl+E jump to row start / end.
75
- - [ ] Ctrl+W deletes the previous word (also Meta+Backspace).
76
- - [ ] Long pasted text doesn't lock up the renderer.
77
- - [ ] Resize the terminal — input frame and footer span the new width.
78
-
79
- ### Status line
80
-
81
- - [ ] Shows `UCODE · Ready` while idle.
82
- - [ ] Shows a spinning indicator + phase ("Waiting for model...",
83
- "Thinking...", "Generating response...", "Calling X...") during
84
- `runNaturalLanguageTask`.
85
- - [ ] Appends `(<elapsed> s, esc cancel)` when a task is in flight.
86
- - [ ] Esc on a running task flips to "Cancelling..." then back to Ready.
87
-
88
- ### Tool calls
89
-
90
- - [ ] Single tool call renders one line (`· tool · detail`).
91
- - [ ] Two+ consecutive tool calls collapse to one row + `(Ctrl+O expand)`.
92
- - [ ] Ctrl+O expands the most recent group with `│`/`└` branch markers
93
- and only fires once per group.
94
- - [ ] When text arrives between tool calls, the previous group freezes
95
- into the log and a fresh group starts on the next tool call.
96
-
97
- ### Agents footer
98
-
99
- - [ ] Shows `Agents: none │ No target agents` when nothing's online.
100
- - [ ] Shows `Agents: @x @y ... │ ↓ select target · ←/→ switch` otherwise.
101
- - [ ] At narrow widths, the row stays single-line, drops trailing chips
102
- and emits ` +N more`.
103
- - [ ] Down enters selection mode (first chip inverse). Left/Right cycle.
104
- Up exits. Prompt prefix changes to `›@<name> ` when locked.
105
-
106
- ### Smoke
107
-
108
- - [ ] `npx jest --runTestsByPath test/unit/ui/UcodeApp.test.js test/unit/code/ucodeTui.test.js --runInBand` passes.
109
- - [ ] `npx jest --silent` passes; every ink suite stays green.
110
-
111
- ## Decision log
112
-
113
- - **Don't fork ink.** Claude-code-fixed inlines a customised ink under
114
- `src/ink/`; we don't need React 19 / ConcurrentRoot / IDE bridging,
115
- so depending on the public `ink@5` keeps maintenance cost low.
116
- - **Don't add JSX.** `React.createElement` keeps jest CJS happy and
117
- avoids a build step. We can revisit if any single component grows
118
- past ~600 lines and readability suffers.
119
- - **Don't enable `--experimental-vm-modules` for jest.** The risk of
120
- surprise ESM behaviour across the existing test suite is too high.
121
- Component logic is exercised by pure-function tests; real TTY render
122
- coverage remains a manual smoke step.
123
- - **Codex isn't a useful reference.** Its TUI is a Rust ratatui app
124
- (`codex-rs/tui`), not React-based. The architectural principle worth
125
- borrowing is its hard split between TUI and core protocol.
126
-
127
- ## P2 dropped, folded into P3
128
-
129
- The internal-agent view in chat is not an independent program. It is now
130
- an Ink chat sub-mode, so the earlier plan to mount a separate subtree was
131
- folded into P3.6.
132
-
133
- ## P3 audit (chat TUI surface)
134
-
135
- Source: the current Ink host in `src/ui/ink/ChatApp.js`, with
136
- shared daemon and command helpers under `src/app/chat/`. Highlights:
137
-
138
- ### Lifecycle
139
- - Public entrypoint `runChat(projectRoot, { globalMode })` from
140
- `src/app/chat/index.js`; it delegates directly to the Ink ChatApp.
141
- - Runners injected via closures: `daemonCoordinator.send`,
142
- `executeCommand`, `inputSubmitHandler.handleSubmit`,
143
- `daemonMessageRouter.handleMessage`.
144
-
145
- ### View state machine
146
- - `dashboardView` ∈ `projects | agents | mode | provider | cron`
147
- - `focusMode` ∈ `input | dashboard` — toggled by Tab and arrow keys
148
- - `globalMode` (boolean) + `globalScope` ∈ `controller | project` —
149
- `globalMode=true` enables a multi-project rail; Esc/Enter walk the
150
- scope ladder.
151
- - `enterDashboardMode()` / `exitDashboardMode()` are the two transition
152
- points; `setGlobalScope()` runs an async, debounced project switch.
153
-
154
- ### Input
155
- - Submit accepts `@mention`, `@target`, `/command`, plain text and
156
- numeric disambiguation (when `pending.disambiguate` is set).
157
- - Editor keys cover Ctrl+A/E/B/F/D/H/K/U/W, Meta+B/F/D, arrows
158
- (cursor + history when empty), Esc (3-layer: clear @target →
159
- exit project scope → cancel input), bracketed paste, Tab (toggle
160
- dashboard), PgUp/PgDn (scroll log).
161
- - Completion fires on `/` and `@` with sources from command registry,
162
- group templates, solo profiles and agent mentions; Up arrow jumps to
163
- the latest suggestion.
164
- - History is per-project, persisted to `input-history.jsonl` with
165
- draft restoration on project switch.
166
-
167
- ### Daemon stack
168
- - `daemonTransport` owns the socket path and retry policy.
169
- - `daemonConnection` owns the queue + lifecycle (`connect`, `send`,
170
- `requestStatus`, `close`, `markExit`, `switchConnection`,
171
- `getState`).
172
- - `daemonCoordinator` orchestrates project switches with a serialised
173
- Promise chain.
174
- - `daemonMessageRouter.handleMessage(msg)` is a stateless dispatcher
175
- that turns daemon responses into log appends, dashboard updates, PTY
176
- writes and transient agent state changes.
177
- - `daemonReconnect.restartDaemonFlow()` provides a per-project lock for
178
- daemon restarts.
179
-
180
- ### Shared helpers
181
- - `cronScheduler` — `/cron start|stop|list` + the cron dashboard view.
182
- - `settingsController` — launch mode / agent provider,
183
- with daemon restart on mode/provider change. `autoResume` stays config/command-driven.
184
- - `transientAgentState` — TTL-bounded `working / waiting_input /
185
- blocked` markers per agent.
186
- - `projectCloseController` — `requestCloseProject(index)` runs daemon
187
- stop + project switch.
188
- - `agentDirectory` — agent label resolution + window clamping (pure).
189
- - `internalAgentLogHistory` — bus log replay for internal agents.
190
-
191
- ### Internal-agent sub-view
192
- - Ink owns the agent sub-view inside ChatApp.
193
- - PTY mirror mode uses `agentSockets.connectOutput/Input` and
194
- `requestSnapshot`.
195
- - Embedded bus mode keeps its own input value, cursor, log and status.
196
- - `agentSockets.createAgentSockets` owns the PTY/bus socket
197
- lifecycle.
198
-
199
- ### Layout (Ink)
200
- - `ChatApp` owns the chat surface as a React tree under
201
- `src/ui/ink/`.
202
- - Dashboard, log, status, completion, internal-agent panes and input are
203
- rendered from React state with ink flex layout. No blessed widget geometry
204
- or controller layer remains.
205
-
206
- ### Commands
207
- `/bus`, `/ctx`, `/daemon`, `/doctor`, `/cron`, `/group`, `/init`,
208
- `/open`, `/launch`, `/project`, `/role`, `/solo`, `/settings`,
209
- `/help`, plus nested subcommands (`/bus activate|list|rename|send|status`,
210
- `/cron start|list|stop`, etc.). `commandExecutor.executeCommand(text)`
211
- is the single dispatch point. `parseCommand(text)`, `parseAtTarget(text)`
212
- and `shouldEchoCommandInChat(text)` are pure.
213
-
214
- ### Cross-cutting
215
- - `text.js`: `escapeBlessed`, `stripBlessedTags`, `stripAnsi`,
216
- `truncateAnsi`, `decodeEscapedNewlines`. The blessed-tag helpers are now
217
- compatibility shims for older daemon/router log strings; ink call sites
218
- strip or normalize those tags before rendering. The ANSI helpers stay
219
- relevant.
220
- - `rawKeyMap.keyToRaw(ch, key)`: converts ink-style key events to
221
- PTY bytes for the agent view. Stays as-is.
222
- - `transport.js`: `startDaemon`, `stopDaemon`, `connectWithRetry`.
223
- Framework-agnostic, no migration needed.
224
-
225
- ### Removal notes
226
- 1. **Entry**: `src/app/chat/index.js` delegates directly to `runChatInk()`.
227
- 2. **ucode**: `src/code/tui.js` is a compatibility export wrapper around
228
- `src/ui/format/` and `runUcodeInkTui()`.
229
- 3. **Controllers**: the blessed widget controllers and their tests were
230
- removed with the fallback path.
231
- 4. **Markup**: old brace-tag helpers remain only where shared daemon/chat
232
- helpers still emit or sanitize legacy log markup.
233
-
234
- ### P3 phase plan
235
-
236
- | Step | Goal | Status |
237
- |---|---|---|
238
- | P3.1 | This audit | ✅ |
239
- | P3.2 | Ink-only `runChat()` entrypoint | ✅ |
240
- | P3.3 | ChatApp shell (banner + log + input + status) | ✅ |
241
- | P3.4 | Five dashboard views as React components | ✅ |
242
- | P3.5 | Daemon connection + PROMPT/BUS_SEND wiring | ✅ |
243
- | P3.6 | Raw-PTY internal agent view as a ChatApp mode | ✅ |
244
- | P3.7 | Real-TTY checklist | ✅ |
245
-
246
- ## P3 chat TUI — what's wired
247
-
248
- | Feature | Status |
249
- |---|---|
250
- | Banner header (project + global mode + scope) | ✅ |
251
- | Scrolling `<Static>` log (1000 line cap) | ✅ |
252
- | Multiline input (P1 MultilineInput component) | ✅ |
253
- | 5 dashboard views (projects/agents/mode/provider/cron) | ✅ |
254
- | Tab toggles input/dashboard focus | ✅ |
255
- | Up/Down history walk + agent selection mode | ✅ |
256
- | Left/Right cycle agents while selected | ✅ |
257
- | Spinner + phase status line | ✅ |
258
- | Tool-merge state machine + Ctrl+O expand | ✅ |
259
- | Daemon connect / send / status poll | ✅ |
260
- | `PROMPT` for free text, `BUS_SEND` for `@target` | ✅ |
261
- | `BUS_SEND_OK` / `RESPONSE` / `ERROR` / `STATUS` / `BUS` envelopes | ✅ |
262
- | Raw PTY agent mirror (Enter on selected agent, Esc to leave) | ✅ |
263
- | `daemonMessageRouter` (markdown streams, transient state, bus subview) | ✅ |
264
- | `commandExecutor` full slash-command dispatch (`/cron`, `/group`, `/role`, `/settings` …) | ✅ |
265
- | Slash + `@` autocomplete | ✅ |
266
- | Input history persisted file load/save | ✅ |
267
- | Cron dashboard actions | ✅ |
268
- | Settings dashboard actions (launch mode, provider) | ✅ |
269
-
270
- ## Real-TTY checklist for chat
271
-
272
- ```sh
273
- ./bin/ufoo.js chat # project mode
274
- ./bin/ufoo.js chat --global # global controller mode
275
- ```
276
-
277
- ### Layout
278
- - [ ] Banner shows the active project + global/project tag.
279
- - [ ] `Agents:` footer, status line above input, log fills the rest.
280
- - [ ] Resize the terminal — input frame and footer stay single-line.
281
-
282
- ### Input + history
283
- - [ ] Type, Enter sends a `PROMPT`. Backspace, arrows, Ctrl+A/E etc.
284
- behave the same as the ucode editor.
285
- - [ ] Up/Down on an empty draft walks the in-memory history.
286
- - [ ] `\` + Enter inserts a newline.
287
- - [ ] Esc clears any active agent selection.
288
-
289
- ### Daemon
290
- - [ ] On launch the daemon spawns automatically (look for the socket
291
- under `~/.ufoo` or your project's `.ufoo`).
292
- - [ ] Send a free-text message — daemon answers, status flips to
293
- `Working on task...` and back to Ready.
294
- - [ ] Type `@<agent> hi` (or select with arrow keys) — message is
295
- sent via `BUS_SEND`, ack arrives as `✓ Message delivered`.
296
-
297
- ### Agents footer
298
- - [ ] Tab into the dashboard, ↓ enters agent selection (first item
299
- inverse), ←/→ cycles, ↑ exits.
300
- - [ ] Enter on a selected agent attaches to its PTY (cleared screen +
301
- scroll region + bottom hint bar). Esc returns to chat without
302
- losing the previous draft or log.
303
-
304
- ### Tool-merge
305
- - [ ] Daemon-driven tool calls collapse and `(Ctrl+O expand)` works
306
- the same as ucode.
307
-
308
- ### Smoke
309
- - [ ] `npx jest --runTestsByPath test/unit/ui/ChatApp.test.js test/unit/ui/chatReducer.test.js --runInBand` passes.
310
- - [ ] `npx jest --runTestsByPath test/unit/ui/UcodeApp.test.js test/unit/code/ucodeTui.test.js --runInBand` passes.
311
- - [ ] `npx jest --silent` passes; every ink suite stays green.
312
-
313
- ## P4 close-out
314
-
315
- - **STATUS handler fix** — chat now reads `msg.data.active` /
316
- `msg.data.active_meta` / `msg.data.cron.tasks` so the agents and cron
317
- counts in the footer actually update.
318
- - **Slash command dispatch** — `createCommandExecutor` is wired in Ink
319
- with daemon stop/start/restart, cron IPC, project switching and agent
320
- activation callbacks.
321
- - **Input history persistence** — `<projectRoot>/.ufoo/chat/input-history.jsonl`
322
- is loaded on mount and appended on every submit.
323
- - **Daemon message routing** — Ink routes daemon envelopes through
324
- `daemonMessageRouter`, including BUS phase status, transient states,
325
- pending delivery markers, streams, close/launch refreshes and loop
326
- summary dashboard display.
327
- - **Inline completion popup** — `/<prefix>` matches commands from
328
- `COMMAND_REGISTRY`; `@<prefix>` matches the live agents list. Tab
329
- accepts the top suggestion. Pure helper `buildCompletions` lives in
330
- `src/ui/format` with full jest coverage.
331
- - **Exit hygiene** — Ctrl+C now flushes `\x1b[2J\x1b[H` so the shell
332
- prompt comes back to a clean screen instead of sitting under the
333
- final ink frame; `runUcodeInkTui` returns `{ code: 0 }` so
334
- `agent.js`'s `process.exit(res.code)` no longer crashes.