@rind-ai/cli 0.4.1 → 0.6.0

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 (49) hide show
  1. package/bin/rind.js +5 -5
  2. package/lib/assistant-renderer.js +179 -265
  3. package/lib/choice-menu-state.js +46 -46
  4. package/lib/cli-input-actions.js +548 -0
  5. package/lib/cli-output-controller.js +460 -0
  6. package/lib/cli-runtime-controller.js +350 -0
  7. package/lib/cli-state-store.js +32 -0
  8. package/lib/cli-state.js +41 -0
  9. package/lib/command-controller.js +159 -126
  10. package/lib/compact-context-state.js +22 -22
  11. package/lib/components/assistant-message.js +169 -0
  12. package/lib/components/composer-area.js +25 -0
  13. package/lib/components/dynamic-block.js +20 -0
  14. package/lib/components/monitor-stack.js +35 -0
  15. package/lib/components/text-block.js +47 -0
  16. package/lib/components/tool-block.js +122 -0
  17. package/lib/composer-terminal.js +224 -203
  18. package/lib/event-controller.js +243 -242
  19. package/lib/frontend-cli-implementation.js +656 -1111
  20. package/lib/input-controller.js +75 -94
  21. package/lib/input-errors.js +3 -3
  22. package/lib/interrupt-state.js +9 -9
  23. package/lib/line-editor.js +541 -541
  24. package/lib/local-slash-commands.js +217 -0
  25. package/lib/markdown-lines.js +103 -0
  26. package/lib/model-menu-state.js +50 -50
  27. package/lib/one-shot-progress.js +145 -0
  28. package/lib/one-shot.js +228 -0
  29. package/lib/question-menu-state.js +61 -0
  30. package/lib/rendering.js +1309 -1060
  31. package/lib/runtime-client.js +241 -193
  32. package/lib/runtime-env.js +21 -21
  33. package/lib/runtime-protocol.js +122 -15
  34. package/lib/slash-command-mode.js +0 -11
  35. package/lib/slash-menu-state.js +59 -59
  36. package/lib/{background-controller.js → task-monitor-controller.js} +411 -289
  37. package/lib/terminal-key.js +97 -97
  38. package/lib/text-width.js +335 -151
  39. package/lib/theme-menu-state.js +31 -0
  40. package/lib/theme.js +134 -0
  41. package/lib/tool-display.js +675 -0
  42. package/lib/tui/component.js +55 -0
  43. package/lib/tui/cursor.js +29 -0
  44. package/lib/tui/input-buffer.js +172 -0
  45. package/lib/tui/tui.js +591 -0
  46. package/lib/turn-controller.js +68 -78
  47. package/package.json +28 -28
  48. package/lib/assistant-stream-buffer.js +0 -25
  49. package/lib/terminal-ui.js +0 -581
@@ -0,0 +1,460 @@
1
+ import { AssistantRenderer } from "./assistant-renderer.js";
2
+ import {
3
+ assistantHeaderText,
4
+ outputBlockText,
5
+ promptText,
6
+ startupText,
7
+ toolRequestedLine,
8
+ toolResultLine,
9
+ toolStartedLine,
10
+ userInputText,
11
+ questionAnswerText,
12
+ questionText,
13
+ } from "./rendering.js";
14
+ import { TextBlock } from "./components/text-block.js";
15
+ import { DynamicBlock } from "./components/dynamic-block.js";
16
+ import { AssistantMessage } from "./components/assistant-message.js";
17
+ import { ToolBlock } from "./components/tool-block.js";
18
+ import { argsFromResult } from "./tool-display.js";
19
+
20
+ export function createCliOutputController({ state, terminalUi, transcript }) {
21
+ const streamBuffer = createLegacyStreamBuffer();
22
+ const legacyRenderer = new AssistantRenderer((text) => writeOutput(text));
23
+ let assistantMessage = null;
24
+ let blockCount = 0;
25
+ const toolBlocks = new Map();
26
+ const legacyBegunTools = new Set();
27
+ let questionBlock = null;
28
+
29
+ function suspendPrompt(action, options = {}) {
30
+ return action();
31
+ }
32
+
33
+ function redraw(force = false) {
34
+ if (!terminalUi || state.runtime.status === "closing") {
35
+ return;
36
+ }
37
+ terminalUi.requestRender(force);
38
+ }
39
+
40
+ function inputState() {
41
+ const running = state.turn.active || state.display.activeCompact;
42
+ return {
43
+ running,
44
+ label: state.display.activeCompact
45
+ ? "Compacting"
46
+ : state.display.goalChasing
47
+ ? "Goal-Chasing"
48
+ : "Working",
49
+ frame: state.display.activityFrame,
50
+ elapsedMs: running ? Date.now() - state.display.activityStartedAt : 0,
51
+ pendingInputs: state.input.pending,
52
+ };
53
+ }
54
+
55
+ function mainPromptText(frameWidth) {
56
+ return promptText(state.session.info, state.display.stats, inputState(), frameWidth);
57
+ }
58
+
59
+ function refreshInputState() {
60
+ updateActivityTimer();
61
+ redraw();
62
+ }
63
+
64
+ function updateActivityTimer() {
65
+ if (state.turn.active || state.display.activeCompact) {
66
+ if (!state.display.activityStartedAt) {
67
+ state.display.activityStartedAt = Date.now();
68
+ }
69
+ if (state.display.activityTimer) {
70
+ return;
71
+ }
72
+ state.display.activityTimer = setInterval(() => {
73
+ state.display.activityFrame += 1;
74
+ redraw();
75
+ }, 300);
76
+ state.display.activityTimer.unref?.();
77
+ return;
78
+ }
79
+ clearActivityTimer();
80
+ }
81
+
82
+ function clearActivityTimer() {
83
+ if (!state.display.activityTimer) {
84
+ return;
85
+ }
86
+ clearInterval(state.display.activityTimer);
87
+ state.display.activityTimer = null;
88
+ state.display.activityFrame = 0;
89
+ state.display.activityStartedAt = 0;
90
+ }
91
+
92
+ function appendBlock(component) {
93
+ transcript.addChild(component);
94
+ blockCount += 1;
95
+ redraw();
96
+ }
97
+
98
+ function lazyLines(buildText, leading) {
99
+ return new DynamicBlock(() => {
100
+ const value = String(buildText() ?? "");
101
+ const body = outputBlockText(value);
102
+ if (!body.trim()) {
103
+ return [];
104
+ }
105
+ const lines = body.split("\n");
106
+ while (lines.length > 1 && lines.at(-1) === "") {
107
+ lines.pop();
108
+ }
109
+ return leading ? ["", ...lines] : lines;
110
+ });
111
+ }
112
+
113
+ // Accepts a string or a thunk so themed content restyles on full repaints.
114
+ function log(text) {
115
+ const build = typeof text === "function" ? text : () => String(text ?? "");
116
+ if (!terminalUi) {
117
+ flushAssistantText(streamBuffer.flush());
118
+ const value = String(build() ?? "");
119
+ if (!value.trim()) {
120
+ return;
121
+ }
122
+ process.stdout.write(outputBlockText(value, state.display.outputStarted));
123
+ state.display.outputStarted = true;
124
+ return;
125
+ }
126
+ const leading = blockCount > 0;
127
+ appendBlock(lazyLines(build, leading));
128
+ }
129
+
130
+ function writeOutput(text) {
131
+ if (!terminalUi) {
132
+ flushAssistantText(streamBuffer.push(text));
133
+ }
134
+ }
135
+
136
+ function flushAssistantText(text = "") {
137
+ const output = String(text || "");
138
+ if (!output) {
139
+ return;
140
+ }
141
+ process.stdout.write(output);
142
+ }
143
+
144
+ function writeUserInput(text) {
145
+ const value = String(text ?? "");
146
+ if (!value.trim()) {
147
+ return;
148
+ }
149
+ if (!terminalUi) {
150
+ flushAssistantText(streamBuffer.flush());
151
+ const line = userInputText(value);
152
+ if (!line) {
153
+ return;
154
+ }
155
+ process.stdout.write(outputBlockText(line, state.display.outputStarted));
156
+ state.display.outputStarted = true;
157
+ return;
158
+ }
159
+ const leading = blockCount > 0;
160
+ appendBlock(new DynamicBlock((width) => {
161
+ const rendered = userInputText(value, width);
162
+ const lines = rendered ? rendered.split("\n") : [];
163
+ if (leading && lines.length) {
164
+ lines.unshift("");
165
+ }
166
+ while (lines.length > 1 && lines.at(-1) === "") {
167
+ lines.pop();
168
+ }
169
+ return lines;
170
+ }));
171
+ }
172
+
173
+ function writeError(text) {
174
+ const value = String(text ?? "");
175
+ if (!terminalUi || state.runtime.status === "closing") {
176
+ process.stderr.write(value);
177
+ return;
178
+ }
179
+ appendBlock(new TextBlock(outputBlockText(value), { leading: blockCount > 0 }));
180
+ }
181
+
182
+ function closeOpenAssistantOutputLine() {}
183
+
184
+ function ensureAssistantBlocks() {
185
+ if (!assistantMessage) {
186
+ const leading = blockCount > 0;
187
+ transcript.addChild(lazyLines(() => assistantHeaderText(), leading));
188
+ blockCount += 1;
189
+ assistantMessage = new AssistantMessage({ color: true });
190
+ transcript.addChild(assistantMessage);
191
+ blockCount += 1;
192
+ }
193
+ return assistantMessage;
194
+ }
195
+
196
+ function closeAssistant() {
197
+ if (terminalUi) {
198
+ if (assistantMessage) {
199
+ assistantMessage.finish();
200
+ assistantMessage = null;
201
+ }
202
+ state.display.assistantHeaderShown = false;
203
+ return;
204
+ }
205
+ legacyRenderer.finish();
206
+ flushAssistantText(streamBuffer.flush());
207
+ state.display.assistantHeaderShown = false;
208
+ }
209
+
210
+ function beginQuestion(event) {
211
+ if (!terminalUi) {
212
+ log(() => questionText(event));
213
+ return;
214
+ }
215
+ const state = { event, answer: null };
216
+ const block = new DynamicBlock(() => {
217
+ const text = state.answer === null
218
+ ? questionText(state.event)
219
+ : questionAnswerText(state.event, state.answer);
220
+ return text.split("\n");
221
+ });
222
+ questionBlock = { state, block };
223
+ appendBlock(block);
224
+ }
225
+
226
+ function finishQuestion(event, answer) {
227
+ if (!terminalUi || !questionBlock) {
228
+ log(() => questionAnswerText(event, answer));
229
+ return;
230
+ }
231
+ questionBlock.state.event = event;
232
+ questionBlock.state.answer = answer;
233
+ questionBlock.block.invalidate();
234
+ questionBlock = null;
235
+ redraw();
236
+ }
237
+
238
+ function assistantAppend(text) {
239
+ if (!terminalUi) {
240
+ legacyRenderer.append(text);
241
+ return;
242
+ }
243
+ const message = ensureAssistantBlocks();
244
+ message.append(text);
245
+ redraw();
246
+ }
247
+
248
+ function clearAssistantLineForInput() {
249
+ if (!terminalUi && state.display.assistantOutputLineOpen) {
250
+ process.stdout.write("\n");
251
+ state.display.assistantOutputLineOpen = false;
252
+ }
253
+ }
254
+
255
+ function showStartup(info) {
256
+ if (!terminalUi) {
257
+ return;
258
+ }
259
+ const source = { ...info };
260
+ appendBlock(new DynamicBlock((width) => {
261
+ const rendered = startupText(source, width);
262
+ return rendered ? rendered.split("\n") : [];
263
+ }));
264
+ }
265
+
266
+ function beginTool(event) {
267
+ const callId = String(event?.tool_call_id || "");
268
+ if (!terminalUi) {
269
+ if (!callId) {
270
+ log(toolStartedLine(event));
271
+ return;
272
+ }
273
+ if (legacyBegunTools.has(callId)) {
274
+ return;
275
+ }
276
+ legacyBegunTools.add(callId);
277
+ log(event?.args_preview ? toolRequestedLine(event) : toolStartedLine(event));
278
+ return;
279
+ }
280
+ if (!callId) {
281
+ return;
282
+ }
283
+ const existing = toolBlocks.get(callId);
284
+ if (existing) {
285
+ existing.enrichArgs(event);
286
+ return;
287
+ }
288
+ const block = new ToolBlock({
289
+ event,
290
+ onRequestRender: () => redraw(),
291
+ leading: blockCount > 0,
292
+ });
293
+ toolBlocks.set(callId, block);
294
+ appendBlock(block);
295
+ }
296
+
297
+ function updateToolProgress(callId, message) {
298
+ if (!terminalUi) {
299
+ return;
300
+ }
301
+ toolBlocks.get(String(callId || ""))?.setProgress(message);
302
+ }
303
+
304
+ function finishTool(event, fileChange) {
305
+ if (!terminalUi) {
306
+ log(toolResultLine(event, fileChange));
307
+ return;
308
+ }
309
+ const callId = String(event?.tool_call_id || "");
310
+ let block = toolBlocks.get(callId);
311
+ if (!block) {
312
+ if (!callId) {
313
+ return;
314
+ }
315
+ block = new ToolBlock({
316
+ event,
317
+ onRequestRender: () => redraw(),
318
+ leading: blockCount > 0,
319
+ });
320
+ toolBlocks.set(callId, block);
321
+ appendBlock(block);
322
+ }
323
+ block.enrichArgs({ arguments: argsFromResult(event?.tool_name, event?.result) });
324
+ block.finish(event, fileChange);
325
+ }
326
+
327
+ function setToolsExpanded(expanded) {
328
+ if (!terminalUi) {
329
+ return;
330
+ }
331
+ for (const block of toolBlocks.values()) {
332
+ block.setExpanded(expanded);
333
+ }
334
+ redraw();
335
+ }
336
+
337
+ function renderHistory(messages) {
338
+ const pendingTools = new Map();
339
+ const flushPendingTools = () => {
340
+ for (const [toolCallId, tool] of pendingTools.entries()) {
341
+ beginTool({
342
+ tool_call_id: toolCallId,
343
+ tool_name: tool.name,
344
+ args_preview: tool.arguments,
345
+ });
346
+ finishTool({
347
+ tool_call_id: toolCallId,
348
+ tool_name: tool.name,
349
+ status: "completed",
350
+ result: "",
351
+ });
352
+ }
353
+ pendingTools.clear();
354
+ };
355
+ for (const message of Array.isArray(messages) ? messages : []) {
356
+ const role = String(message?.role || "");
357
+ if (role === "user") {
358
+ flushPendingTools();
359
+ closeAssistant();
360
+ writeUserInput(messageText(message?.content));
361
+ continue;
362
+ }
363
+ if (role === "assistant") {
364
+ flushPendingTools();
365
+ const content = messageText(message?.content);
366
+ if (content) {
367
+ assistantAppend(content);
368
+ closeAssistant();
369
+ }
370
+ for (const call of Array.isArray(message?.tool_calls) ? message.tool_calls : []) {
371
+ const toolCallId = String(call?.id || "");
372
+ const toolName = String(call?.function?.name || "tool");
373
+ if (toolCallId) {
374
+ pendingTools.set(toolCallId, {
375
+ name: toolName,
376
+ arguments: String(call?.function?.arguments || ""),
377
+ });
378
+ }
379
+ }
380
+ continue;
381
+ }
382
+ if (role === "tool") {
383
+ const toolCallId = String(message?.tool_call_id || "");
384
+ const tool = pendingTools.get(toolCallId) || { name: "tool", arguments: "" };
385
+ pendingTools.delete(toolCallId);
386
+ beginTool({
387
+ tool_call_id: toolCallId,
388
+ tool_name: tool.name,
389
+ args_preview: tool.arguments,
390
+ });
391
+ finishTool({
392
+ tool_call_id: toolCallId,
393
+ tool_name: tool.name,
394
+ status: "completed",
395
+ result: messageText(message?.content),
396
+ });
397
+ }
398
+ }
399
+ flushPendingTools();
400
+ closeAssistant();
401
+ redraw(true);
402
+ }
403
+
404
+ return {
405
+ terminalUi: Boolean(terminalUi),
406
+ suspendPrompt,
407
+ redraw,
408
+ refreshInputState,
409
+ clearActivityTimer,
410
+ mainPromptText,
411
+ log,
412
+ writeUserInput,
413
+ writeError,
414
+ closeAssistant,
415
+ clearAssistantLineForInput,
416
+ assistantAppend,
417
+ beginTool,
418
+ updateToolProgress,
419
+ finishTool,
420
+ beginQuestion,
421
+ finishQuestion,
422
+ setToolsExpanded,
423
+ renderHistory,
424
+ showStartup,
425
+ replayAll: () => terminalUi?.replayAll?.(),
426
+ };
427
+ }
428
+
429
+ function createLegacyStreamBuffer() {
430
+ let pending = "";
431
+ return {
432
+ push(text) {
433
+ pending += String(text || "");
434
+ const flushed = pending;
435
+ pending = "";
436
+ return flushed;
437
+ },
438
+ flush() {
439
+ const flushed = pending;
440
+ pending = "";
441
+ return flushed;
442
+ },
443
+ };
444
+ }
445
+
446
+ function messageText(content) {
447
+ if (typeof content === "string") {
448
+ return content;
449
+ }
450
+ if (!Array.isArray(content)) {
451
+ return "";
452
+ }
453
+ return content
454
+ .map((part) => {
455
+ if (typeof part === "string") return part;
456
+ if (part && typeof part === "object" && typeof part.text === "string") return part.text;
457
+ return "";
458
+ })
459
+ .join("");
460
+ }