@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.
- package/bin/rind.js +5 -5
- package/lib/assistant-renderer.js +179 -265
- package/lib/choice-menu-state.js +46 -46
- package/lib/cli-input-actions.js +548 -0
- package/lib/cli-output-controller.js +460 -0
- package/lib/cli-runtime-controller.js +350 -0
- package/lib/cli-state-store.js +32 -0
- package/lib/cli-state.js +41 -0
- package/lib/command-controller.js +159 -126
- package/lib/compact-context-state.js +22 -22
- package/lib/components/assistant-message.js +169 -0
- package/lib/components/composer-area.js +25 -0
- package/lib/components/dynamic-block.js +20 -0
- package/lib/components/monitor-stack.js +35 -0
- package/lib/components/text-block.js +47 -0
- package/lib/components/tool-block.js +122 -0
- package/lib/composer-terminal.js +224 -203
- package/lib/event-controller.js +243 -242
- package/lib/frontend-cli-implementation.js +656 -1111
- package/lib/input-controller.js +75 -94
- package/lib/input-errors.js +3 -3
- package/lib/interrupt-state.js +9 -9
- package/lib/line-editor.js +541 -541
- package/lib/local-slash-commands.js +217 -0
- package/lib/markdown-lines.js +103 -0
- package/lib/model-menu-state.js +50 -50
- package/lib/one-shot-progress.js +145 -0
- package/lib/one-shot.js +228 -0
- package/lib/question-menu-state.js +61 -0
- package/lib/rendering.js +1309 -1060
- package/lib/runtime-client.js +241 -193
- package/lib/runtime-env.js +21 -21
- package/lib/runtime-protocol.js +122 -15
- package/lib/slash-command-mode.js +0 -11
- package/lib/slash-menu-state.js +59 -59
- package/lib/{background-controller.js → task-monitor-controller.js} +411 -289
- package/lib/terminal-key.js +97 -97
- package/lib/text-width.js +335 -151
- package/lib/theme-menu-state.js +31 -0
- package/lib/theme.js +134 -0
- package/lib/tool-display.js +675 -0
- package/lib/tui/component.js +55 -0
- package/lib/tui/cursor.js +29 -0
- package/lib/tui/input-buffer.js +172 -0
- package/lib/tui/tui.js +591 -0
- package/lib/turn-controller.js +68 -78
- package/package.json +28 -28
- package/lib/assistant-stream-buffer.js +0 -25
- 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
|
+
}
|