@rind-ai/cli 0.6.1 → 0.7.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 +2 -2
- package/lib/assistant-renderer.js +42 -15
- package/lib/cli-input-actions.js +133 -19
- package/lib/cli-output-controller.js +82 -94
- package/lib/cli-runtime-controller.js +155 -8
- package/lib/cli-state.js +2 -2
- package/lib/command-controller.js +34 -0
- package/lib/components/assistant-message.js +54 -21
- package/lib/composer-terminal.js +1 -1
- package/lib/event-controller.js +9 -11
- package/lib/frontend-cli-implementation.js +108 -18
- package/lib/ipc.js +186 -0
- package/lib/line-editor.js +29 -4
- package/lib/local-slash-commands.js +2 -0
- package/lib/markdown-lines.js +142 -0
- package/lib/one-shot-progress.js +145 -145
- package/lib/one-shot.js +3 -3
- package/lib/prompt-history-store.js +54 -0
- package/lib/question-menu-state.js +10 -0
- package/lib/rendering.js +466 -102
- package/lib/runtime-client.js +12 -13
- package/lib/runtime-protocol.js +5 -0
- package/lib/send.js +53 -0
- package/lib/task-monitor-controller.js +45 -45
- package/lib/terminal-key.js +47 -3
- package/lib/text-width.js +2 -19
- package/lib/tool-display.js +18 -23
- package/lib/tui/cursor.js +17 -8
- package/lib/tui/input-buffer.js +69 -28
- package/lib/tui/tui.js +128 -30
- package/lib/turn-controller.js +13 -4
- package/package.json +5 -5
- package/lib/frontend-cli.js +0 -5
|
@@ -18,17 +18,13 @@ import { ToolBlock } from "./components/tool-block.js";
|
|
|
18
18
|
import { argsFromResult } from "./tool-display.js";
|
|
19
19
|
|
|
20
20
|
export function createCliOutputController({ state, terminalUi, transcript }) {
|
|
21
|
-
const streamBuffer = createLegacyStreamBuffer();
|
|
22
21
|
const legacyRenderer = new AssistantRenderer((text) => writeOutput(text));
|
|
23
22
|
let assistantMessage = null;
|
|
24
23
|
let blockCount = 0;
|
|
25
24
|
const toolBlocks = new Map();
|
|
26
25
|
const legacyBegunTools = new Set();
|
|
27
26
|
let questionBlock = null;
|
|
28
|
-
|
|
29
|
-
function suspendPrompt(action, options = {}) {
|
|
30
|
-
return action();
|
|
31
|
-
}
|
|
27
|
+
let turnContext = "";
|
|
32
28
|
|
|
33
29
|
function redraw(force = false) {
|
|
34
30
|
if (!terminalUi || state.runtime.status === "closing") {
|
|
@@ -39,19 +35,29 @@ export function createCliOutputController({ state, terminalUi, transcript }) {
|
|
|
39
35
|
|
|
40
36
|
function inputState() {
|
|
41
37
|
const running = state.turn.active || state.display.activeCompact;
|
|
38
|
+
const inputSession = state.input.session;
|
|
42
39
|
return {
|
|
43
40
|
running,
|
|
44
|
-
label: state.display.activeCompact
|
|
41
|
+
label: state.display.activityLabel || (state.display.activeCompact
|
|
45
42
|
? "Compacting"
|
|
46
|
-
:
|
|
47
|
-
? "Goal-Chasing"
|
|
48
|
-
: "Working",
|
|
43
|
+
: "Working"),
|
|
49
44
|
frame: state.display.activityFrame,
|
|
50
45
|
elapsedMs: running ? Date.now() - state.display.activityStartedAt : 0,
|
|
51
46
|
pendingInputs: state.input.pending,
|
|
47
|
+
inputMode: inputSession?.mode || "prompt",
|
|
48
|
+
menuOpen: Boolean(inputSession?.menuState?.matches?.()?.length),
|
|
52
49
|
};
|
|
53
50
|
}
|
|
54
51
|
|
|
52
|
+
function setActivityLabel(label = "") {
|
|
53
|
+
const next = String(label || "");
|
|
54
|
+
if (state.display.activityLabel === next) {
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
state.display.activityLabel = next;
|
|
58
|
+
redraw();
|
|
59
|
+
}
|
|
60
|
+
|
|
55
61
|
function mainPromptText(frameWidth) {
|
|
56
62
|
return promptText(state.session.info, state.display.stats, inputState(), frameWidth);
|
|
57
63
|
}
|
|
@@ -80,10 +86,9 @@ export function createCliOutputController({ state, terminalUi, transcript }) {
|
|
|
80
86
|
}
|
|
81
87
|
|
|
82
88
|
function clearActivityTimer() {
|
|
83
|
-
if (
|
|
84
|
-
|
|
89
|
+
if (state.display.activityTimer) {
|
|
90
|
+
clearInterval(state.display.activityTimer);
|
|
85
91
|
}
|
|
86
|
-
clearInterval(state.display.activityTimer);
|
|
87
92
|
state.display.activityTimer = null;
|
|
88
93
|
state.display.activityFrame = 0;
|
|
89
94
|
state.display.activityStartedAt = 0;
|
|
@@ -114,7 +119,6 @@ export function createCliOutputController({ state, terminalUi, transcript }) {
|
|
|
114
119
|
function log(text) {
|
|
115
120
|
const build = typeof text === "function" ? text : () => String(text ?? "");
|
|
116
121
|
if (!terminalUi) {
|
|
117
|
-
flushAssistantText(streamBuffer.flush());
|
|
118
122
|
const value = String(build() ?? "");
|
|
119
123
|
if (!value.trim()) {
|
|
120
124
|
return;
|
|
@@ -129,7 +133,7 @@ export function createCliOutputController({ state, terminalUi, transcript }) {
|
|
|
129
133
|
|
|
130
134
|
function writeOutput(text) {
|
|
131
135
|
if (!terminalUi) {
|
|
132
|
-
flushAssistantText(
|
|
136
|
+
flushAssistantText(text);
|
|
133
137
|
}
|
|
134
138
|
}
|
|
135
139
|
|
|
@@ -141,14 +145,13 @@ export function createCliOutputController({ state, terminalUi, transcript }) {
|
|
|
141
145
|
process.stdout.write(output);
|
|
142
146
|
}
|
|
143
147
|
|
|
144
|
-
function writeUserInput(text) {
|
|
148
|
+
function writeUserInput(text, source = "") {
|
|
145
149
|
const value = String(text ?? "");
|
|
146
150
|
if (!value.trim()) {
|
|
147
151
|
return;
|
|
148
152
|
}
|
|
149
153
|
if (!terminalUi) {
|
|
150
|
-
|
|
151
|
-
const line = userInputText(value);
|
|
154
|
+
const line = userInputText(value, undefined, source);
|
|
152
155
|
if (!line) {
|
|
153
156
|
return;
|
|
154
157
|
}
|
|
@@ -158,7 +161,7 @@ export function createCliOutputController({ state, terminalUi, transcript }) {
|
|
|
158
161
|
}
|
|
159
162
|
const leading = blockCount > 0;
|
|
160
163
|
appendBlock(new DynamicBlock((width) => {
|
|
161
|
-
const rendered = userInputText(value, width);
|
|
164
|
+
const rendered = userInputText(value, width, source);
|
|
162
165
|
const lines = rendered ? rendered.split("\n") : [];
|
|
163
166
|
if (leading && lines.length) {
|
|
164
167
|
lines.unshift("");
|
|
@@ -179,8 +182,6 @@ export function createCliOutputController({ state, terminalUi, transcript }) {
|
|
|
179
182
|
appendBlock(new TextBlock(outputBlockText(value), { leading: blockCount > 0 }));
|
|
180
183
|
}
|
|
181
184
|
|
|
182
|
-
function closeOpenAssistantOutputLine() {}
|
|
183
|
-
|
|
184
185
|
function ensureAssistantBlocks() {
|
|
185
186
|
if (!assistantMessage) {
|
|
186
187
|
const leading = blockCount > 0;
|
|
@@ -203,7 +204,6 @@ export function createCliOutputController({ state, terminalUi, transcript }) {
|
|
|
203
204
|
return;
|
|
204
205
|
}
|
|
205
206
|
legacyRenderer.finish();
|
|
206
|
-
flushAssistantText(streamBuffer.flush());
|
|
207
207
|
state.display.assistantHeaderShown = false;
|
|
208
208
|
}
|
|
209
209
|
|
|
@@ -245,13 +245,6 @@ export function createCliOutputController({ state, terminalUi, transcript }) {
|
|
|
245
245
|
redraw();
|
|
246
246
|
}
|
|
247
247
|
|
|
248
|
-
function clearAssistantLineForInput() {
|
|
249
|
-
if (!terminalUi && state.display.assistantOutputLineOpen) {
|
|
250
|
-
process.stdout.write("\n");
|
|
251
|
-
state.display.assistantOutputLineOpen = false;
|
|
252
|
-
}
|
|
253
|
-
}
|
|
254
|
-
|
|
255
248
|
function showStartup(info) {
|
|
256
249
|
if (!terminalUi) {
|
|
257
250
|
return;
|
|
@@ -280,7 +273,8 @@ export function createCliOutputController({ state, terminalUi, transcript }) {
|
|
|
280
273
|
if (!callId) {
|
|
281
274
|
return;
|
|
282
275
|
}
|
|
283
|
-
const
|
|
276
|
+
const key = toolBlockKey(callId);
|
|
277
|
+
const existing = toolBlocks.get(key);
|
|
284
278
|
if (existing) {
|
|
285
279
|
existing.enrichArgs(event);
|
|
286
280
|
return;
|
|
@@ -290,7 +284,7 @@ export function createCliOutputController({ state, terminalUi, transcript }) {
|
|
|
290
284
|
onRequestRender: () => redraw(),
|
|
291
285
|
leading: blockCount > 0,
|
|
292
286
|
});
|
|
293
|
-
toolBlocks.set(
|
|
287
|
+
toolBlocks.set(key, block);
|
|
294
288
|
appendBlock(block);
|
|
295
289
|
}
|
|
296
290
|
|
|
@@ -298,7 +292,7 @@ export function createCliOutputController({ state, terminalUi, transcript }) {
|
|
|
298
292
|
if (!terminalUi) {
|
|
299
293
|
return;
|
|
300
294
|
}
|
|
301
|
-
toolBlocks.get(
|
|
295
|
+
toolBlocks.get(toolBlockKey(callId))?.setProgress(message);
|
|
302
296
|
}
|
|
303
297
|
|
|
304
298
|
function finishTool(event, fileChange) {
|
|
@@ -307,7 +301,8 @@ export function createCliOutputController({ state, terminalUi, transcript }) {
|
|
|
307
301
|
return;
|
|
308
302
|
}
|
|
309
303
|
const callId = String(event?.tool_call_id || "");
|
|
310
|
-
|
|
304
|
+
const key = toolBlockKey(callId);
|
|
305
|
+
let block = toolBlocks.get(key);
|
|
311
306
|
if (!block) {
|
|
312
307
|
if (!callId) {
|
|
313
308
|
return;
|
|
@@ -317,7 +312,7 @@ export function createCliOutputController({ state, terminalUi, transcript }) {
|
|
|
317
312
|
onRequestRender: () => redraw(),
|
|
318
313
|
leading: blockCount > 0,
|
|
319
314
|
});
|
|
320
|
-
toolBlocks.set(
|
|
315
|
+
toolBlocks.set(key, block);
|
|
321
316
|
appendBlock(block);
|
|
322
317
|
}
|
|
323
318
|
block.enrichArgs({ arguments: argsFromResult(event?.tool_name, event?.result) });
|
|
@@ -334,21 +329,30 @@ export function createCliOutputController({ state, terminalUi, transcript }) {
|
|
|
334
329
|
redraw();
|
|
335
330
|
}
|
|
336
331
|
|
|
337
|
-
function
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
332
|
+
function setTurnContext(turnId = "") {
|
|
333
|
+
turnContext = String(turnId || "");
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
function toolBlockKey(callId) {
|
|
337
|
+
const value = String(callId || "");
|
|
338
|
+
return value ? `${turnContext}:${value}` : "";
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
function renderHistory(messages) {
|
|
342
|
+
const pendingTools = new Map();
|
|
343
|
+
const flushPendingTools = () => {
|
|
344
|
+
for (const [toolCallId, tool] of pendingTools.entries()) {
|
|
345
|
+
beginTool({
|
|
346
|
+
tool_call_id: toolCallId,
|
|
347
|
+
tool_name: tool.name,
|
|
348
|
+
args_preview: tool.arguments,
|
|
349
|
+
});
|
|
350
|
+
finishTool({
|
|
351
|
+
tool_call_id: toolCallId,
|
|
352
|
+
tool_name: tool.name,
|
|
353
|
+
status: "completed",
|
|
354
|
+
result: "",
|
|
355
|
+
});
|
|
352
356
|
}
|
|
353
357
|
pendingTools.clear();
|
|
354
358
|
};
|
|
@@ -360,39 +364,39 @@ export function createCliOutputController({ state, terminalUi, transcript }) {
|
|
|
360
364
|
writeUserInput(messageText(message?.content));
|
|
361
365
|
continue;
|
|
362
366
|
}
|
|
363
|
-
if (role === "assistant") {
|
|
364
|
-
flushPendingTools();
|
|
367
|
+
if (role === "assistant") {
|
|
368
|
+
flushPendingTools();
|
|
365
369
|
const content = messageText(message?.content);
|
|
366
370
|
if (content) {
|
|
367
371
|
assistantAppend(content);
|
|
368
372
|
closeAssistant();
|
|
369
373
|
}
|
|
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),
|
|
374
|
+
for (const call of Array.isArray(message?.tool_calls) ? message.tool_calls : []) {
|
|
375
|
+
const toolCallId = String(call?.id || "");
|
|
376
|
+
const toolName = String(call?.function?.name || "tool");
|
|
377
|
+
if (toolCallId) {
|
|
378
|
+
pendingTools.set(toolCallId, {
|
|
379
|
+
name: toolName,
|
|
380
|
+
arguments: String(call?.function?.arguments || ""),
|
|
381
|
+
});
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
continue;
|
|
385
|
+
}
|
|
386
|
+
if (role === "tool") {
|
|
387
|
+
const toolCallId = String(message?.tool_call_id || "");
|
|
388
|
+
const tool = pendingTools.get(toolCallId) || { name: "tool", arguments: "" };
|
|
389
|
+
pendingTools.delete(toolCallId);
|
|
390
|
+
beginTool({
|
|
391
|
+
tool_call_id: toolCallId,
|
|
392
|
+
tool_name: tool.name,
|
|
393
|
+
args_preview: tool.arguments,
|
|
394
|
+
});
|
|
395
|
+
finishTool({
|
|
396
|
+
tool_call_id: toolCallId,
|
|
397
|
+
tool_name: tool.name,
|
|
398
|
+
status: "completed",
|
|
399
|
+
result: messageText(message?.content),
|
|
396
400
|
});
|
|
397
401
|
}
|
|
398
402
|
}
|
|
@@ -403,16 +407,15 @@ export function createCliOutputController({ state, terminalUi, transcript }) {
|
|
|
403
407
|
|
|
404
408
|
return {
|
|
405
409
|
terminalUi: Boolean(terminalUi),
|
|
406
|
-
suspendPrompt,
|
|
407
410
|
redraw,
|
|
408
411
|
refreshInputState,
|
|
409
412
|
clearActivityTimer,
|
|
413
|
+
setActivityLabel,
|
|
410
414
|
mainPromptText,
|
|
411
415
|
log,
|
|
412
416
|
writeUserInput,
|
|
413
417
|
writeError,
|
|
414
418
|
closeAssistant,
|
|
415
|
-
clearAssistantLineForInput,
|
|
416
419
|
assistantAppend,
|
|
417
420
|
beginTool,
|
|
418
421
|
updateToolProgress,
|
|
@@ -420,28 +423,13 @@ export function createCliOutputController({ state, terminalUi, transcript }) {
|
|
|
420
423
|
beginQuestion,
|
|
421
424
|
finishQuestion,
|
|
422
425
|
setToolsExpanded,
|
|
426
|
+
setTurnContext,
|
|
423
427
|
renderHistory,
|
|
424
428
|
showStartup,
|
|
425
429
|
replayAll: () => terminalUi?.replayAll?.(),
|
|
426
430
|
};
|
|
427
431
|
}
|
|
428
432
|
|
|
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
433
|
|
|
446
434
|
function messageText(content) {
|
|
447
435
|
if (typeof content === "string") {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { REASONING_EFFORTS } from "./runtime-protocol.js";
|
|
2
|
-
import {
|
|
2
|
+
import { commandResultText, contextBoardText, goalCommandText, modelListErrorText, sessionSwitchedText, usageBoardText } from "./rendering.js";
|
|
3
3
|
|
|
4
4
|
export function createCliRuntimeController({
|
|
5
5
|
client,
|
|
@@ -9,14 +9,16 @@ export function createCliRuntimeController({
|
|
|
9
9
|
requireInitialization,
|
|
10
10
|
state,
|
|
11
11
|
getCommands,
|
|
12
|
-
getTurnController,
|
|
13
12
|
getTaskMonitor,
|
|
14
13
|
getCompactContextState,
|
|
15
14
|
askModelMenu,
|
|
16
15
|
askEffortMenu = null,
|
|
17
16
|
askSessionMenu,
|
|
17
|
+
askForkPointMenu,
|
|
18
|
+
askContextBoard = null,
|
|
18
19
|
restoreLiveTurn,
|
|
19
20
|
renderHistory = () => {},
|
|
21
|
+
onSessionRestored = () => {},
|
|
20
22
|
clearPendingInputs,
|
|
21
23
|
closeAssistant,
|
|
22
24
|
refreshInputState,
|
|
@@ -74,7 +76,6 @@ export function createCliRuntimeController({
|
|
|
74
76
|
}
|
|
75
77
|
|
|
76
78
|
async function runGoalCommand(command) {
|
|
77
|
-
const turnController = getTurnController();
|
|
78
79
|
if (command.action === "set" && state.turn.active) {
|
|
79
80
|
log(() => commandResultText("Goal not started", "pause or finish the active turn first"));
|
|
80
81
|
return;
|
|
@@ -84,7 +85,6 @@ export function createCliRuntimeController({
|
|
|
84
85
|
const result = await request(methods.goalSet, { objective: command.objective });
|
|
85
86
|
updateGoalState(result?.goal);
|
|
86
87
|
log(() => goalCommandText(result?.goal, "set"));
|
|
87
|
-
turnController.submit(command.objective);
|
|
88
88
|
return;
|
|
89
89
|
}
|
|
90
90
|
if (command.action === "clear") {
|
|
@@ -97,9 +97,6 @@ export function createCliRuntimeController({
|
|
|
97
97
|
const result = await request(methods.goalStatus, { status: command.action === "resume" ? "active" : "paused" });
|
|
98
98
|
updateGoalState(result?.goal);
|
|
99
99
|
log(() => goalCommandText(result?.goal, command.action));
|
|
100
|
-
if (command.action === "resume" && !state.turn.active) {
|
|
101
|
-
turnController.submit("", { goal_continuation: true });
|
|
102
|
-
}
|
|
103
100
|
return;
|
|
104
101
|
}
|
|
105
102
|
const result = await request(methods.goalGet);
|
|
@@ -179,6 +176,7 @@ export function createCliRuntimeController({
|
|
|
179
176
|
getCompactContextState().clear();
|
|
180
177
|
refreshInputState();
|
|
181
178
|
redraw();
|
|
179
|
+
await onSessionRestored();
|
|
182
180
|
void getTaskMonitor()?.refresh().catch(() => {});
|
|
183
181
|
return true;
|
|
184
182
|
}
|
|
@@ -219,12 +217,137 @@ export function createCliRuntimeController({
|
|
|
219
217
|
}
|
|
220
218
|
}
|
|
221
219
|
|
|
220
|
+
async function runForkSelector() {
|
|
221
|
+
if (state.turn.active || state.display.activeCompact) {
|
|
222
|
+
log("Cannot fork while a turn is running. Wait for it to finish or stop it first.");
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
if (String(state.session.info.session_type || "") === "delegated_task") {
|
|
226
|
+
log("Delegated task sessions cannot be forked.");
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
let replay;
|
|
230
|
+
try {
|
|
231
|
+
replay = await request(methods.sessionReplay);
|
|
232
|
+
} catch (error) {
|
|
233
|
+
log(`Command failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
const messages = Array.isArray(replay?.messages) ? replay.messages : [];
|
|
237
|
+
const userMessages = messages.filter(isForkableUserMessage).slice(-FORK_MENU_MESSAGE_LIMIT);
|
|
238
|
+
if (!userMessages.length) {
|
|
239
|
+
log("Nothing to fork: this session has no messages yet.");
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
const items = [
|
|
243
|
+
{ id: "", label: FORK_END_LABEL },
|
|
244
|
+
...userMessages.slice().reverse().map((message) => ({
|
|
245
|
+
id: String(message.id || ""),
|
|
246
|
+
label: forkPointLabel(message),
|
|
247
|
+
text: String(message.content || ""),
|
|
248
|
+
})),
|
|
249
|
+
];
|
|
250
|
+
const selected = await askForkPointMenu(items);
|
|
251
|
+
if (!selected || state.runtime.status === "closing") {
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
let fork;
|
|
255
|
+
try {
|
|
256
|
+
fork = await request(methods.sessionFork, selected.id ? { before_message_id: selected.id } : {});
|
|
257
|
+
} catch (error) {
|
|
258
|
+
log(`Fork failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
const newId = String(fork?.session_id || "");
|
|
262
|
+
if (!newId) {
|
|
263
|
+
log("Fork failed: the runtime returned no session id.");
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
try {
|
|
267
|
+
const switched = await restoreSession(newId, {
|
|
268
|
+
switchSession: true,
|
|
269
|
+
announce: (info) => log(() => sessionSwitchedText(info)),
|
|
270
|
+
});
|
|
271
|
+
if (!switched) {
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
} catch (error) {
|
|
275
|
+
log(`Forked to ${newId}, but switching failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
278
|
+
const kept = selected.id
|
|
279
|
+
? Math.max(0, messages.findIndex((message) => String(message.id || "") === selected.id))
|
|
280
|
+
: messages.length;
|
|
281
|
+
log(`Forked ${newId} ← ${fork.forked_from} (${kept === messages.length ? `kept all ${kept}` : `kept the first ${kept} of ${messages.length}`} messages).`);
|
|
282
|
+
if (selected.text) {
|
|
283
|
+
state.input.prefill = selected.text;
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
async function runContextBoard() {
|
|
288
|
+
if (state.turn.active || state.display.activeCompact) {
|
|
289
|
+
log("Cannot open the context board while a turn is running. Wait for it to finish or stop it first.");
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
292
|
+
let data;
|
|
293
|
+
try {
|
|
294
|
+
data = await fetchContextBoardData();
|
|
295
|
+
} catch (error) {
|
|
296
|
+
log(`Context board failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
if (!askContextBoard) {
|
|
300
|
+
printContextPages(data);
|
|
301
|
+
return;
|
|
302
|
+
}
|
|
303
|
+
await askContextBoard({
|
|
304
|
+
render: (pageIndex, width) => contextPageText(data, pageIndex, width),
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
async function printContextReport() {
|
|
309
|
+
let data;
|
|
310
|
+
try {
|
|
311
|
+
data = await fetchContextBoardData();
|
|
312
|
+
} catch (error) {
|
|
313
|
+
log(`Context report failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
314
|
+
return;
|
|
315
|
+
}
|
|
316
|
+
printContextPages(data);
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
async function fetchContextBoardData() {
|
|
320
|
+
const breakdownResult = await request(methods.contextInspect);
|
|
321
|
+
const summaryResult = await request(methods.usageSummary, { days: CONTEXT_BOARD_DAYS });
|
|
322
|
+
return {
|
|
323
|
+
breakdown: breakdownResult?.breakdown && typeof breakdownResult.breakdown === "object" ? breakdownResult.breakdown : null,
|
|
324
|
+
latestUsage: breakdownResult?.latest_usage && typeof breakdownResult.latest_usage === "object" ? breakdownResult.latest_usage : null,
|
|
325
|
+
summary: summaryResult && typeof summaryResult === "object" ? summaryResult : null,
|
|
326
|
+
};
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
function contextPageText(data, pageIndex, width, { plain = false } = {}) {
|
|
330
|
+
return pageIndex === 0
|
|
331
|
+
? contextBoardText({ breakdown: data.breakdown, latest_usage: data.latestUsage, index: 1, count: 2, plain }, width)
|
|
332
|
+
: usageBoardText({ summary: data.summary, index: 2, count: 2, plain }, width);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
function printContextPages(data) {
|
|
336
|
+
for (const pageIndex of [0, 1]) {
|
|
337
|
+
const text = contextPageText(data, pageIndex, process.stdout.columns || 0, { plain: true });
|
|
338
|
+
if (text.trim()) {
|
|
339
|
+
log(text);
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
222
344
|
function startCompactCommand() {
|
|
223
345
|
if (state.display.activeCompact) {
|
|
224
346
|
log("Compact is already running.");
|
|
225
347
|
return;
|
|
226
348
|
}
|
|
227
349
|
state.display.activeCompact = true;
|
|
350
|
+
state.display.activityLabel = "Compacting";
|
|
228
351
|
state.turn.interruptRequested = false;
|
|
229
352
|
refreshInputState();
|
|
230
353
|
void runCompactCommand().catch((error) => {
|
|
@@ -240,6 +363,7 @@ export function createCliRuntimeController({
|
|
|
240
363
|
await getCommands().applyResult(result);
|
|
241
364
|
} finally {
|
|
242
365
|
state.display.activeCompact = false;
|
|
366
|
+
state.display.activityLabel = "";
|
|
243
367
|
state.turn.interruptRequested = false;
|
|
244
368
|
refreshInputState();
|
|
245
369
|
}
|
|
@@ -309,12 +433,17 @@ export function createCliRuntimeController({
|
|
|
309
433
|
runGoalCommand,
|
|
310
434
|
refreshGoalState,
|
|
311
435
|
runSessionsSelector,
|
|
436
|
+
runForkSelector,
|
|
437
|
+
runContextBoard,
|
|
438
|
+
printContextReport,
|
|
312
439
|
startCompactCommand,
|
|
313
440
|
runModelSelector,
|
|
314
441
|
runEffortCommand,
|
|
315
442
|
};
|
|
316
443
|
}
|
|
317
444
|
|
|
445
|
+
const CONTEXT_BOARD_DAYS = 7;
|
|
446
|
+
|
|
318
447
|
function mergeSlashCommands(...groups) {
|
|
319
448
|
const byName = new Map();
|
|
320
449
|
for (const group of groups) {
|
|
@@ -327,6 +456,24 @@ function mergeSlashCommands(...groups) {
|
|
|
327
456
|
return [...byName.values()].sort((left, right) => left.name.localeCompare(right.name));
|
|
328
457
|
}
|
|
329
458
|
|
|
459
|
+
const FORK_MENU_MESSAGE_LIMIT = 50;
|
|
460
|
+
const FORK_END_LABEL = "Fork at current end (keep full history)";
|
|
461
|
+
const FORK_CONTEXT_KINDS = new Set(["skill_snapshot", "skill_catalog", "goal_checkpoint"]);
|
|
462
|
+
|
|
463
|
+
function isForkableUserMessage(message) {
|
|
464
|
+
if (message?.role !== "user" || !String(message?.content || "").trim()) {
|
|
465
|
+
return false;
|
|
466
|
+
}
|
|
467
|
+
const kind = message?.meta?.kind;
|
|
468
|
+
return !kind || !FORK_CONTEXT_KINDS.has(kind);
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
function forkPointLabel(message) {
|
|
472
|
+
const rawTime = String(message.ts || "").slice(11, 16);
|
|
473
|
+
const time = /^\d{2}:\d{2}$/.test(rawTime) ? rawTime : "";
|
|
474
|
+
return [time, singleLineText(message.content).slice(0, 60)].filter(Boolean).join(" · ");
|
|
475
|
+
}
|
|
476
|
+
|
|
330
477
|
function sessionMenuOption(session) {
|
|
331
478
|
const values = [singleLineText(session?.id), singleLineText(session?.title), singleLineText(session?.updated_at)];
|
|
332
479
|
if (session?.current) values.push("current");
|
|
@@ -341,7 +488,7 @@ function modelSetResultText(result, model) {
|
|
|
341
488
|
if (defaultModel) lines.push(`- default model: ${defaultModel} (unchanged)`);
|
|
342
489
|
lines.push(result?.active_updated || result?.runtime || result?.session
|
|
343
490
|
? "- active session: updated"
|
|
344
|
-
: "- active
|
|
491
|
+
: "- active turn: unchanged; the new model applies to the next turn");
|
|
345
492
|
return commandResultText(lines[0], lines.slice(1).join(" · "));
|
|
346
493
|
}
|
|
347
494
|
|
package/lib/cli-state.js
CHANGED
|
@@ -25,13 +25,13 @@ export function createCliState() {
|
|
|
25
25
|
},
|
|
26
26
|
display: {
|
|
27
27
|
activeCompact: false,
|
|
28
|
-
|
|
28
|
+
activityLabel: "",
|
|
29
29
|
stats: {},
|
|
30
30
|
lastEventSequence: 0,
|
|
31
|
+
lastTurnId: "",
|
|
31
32
|
activityFrame: 0,
|
|
32
33
|
activityTimer: null,
|
|
33
34
|
activityStartedAt: 0,
|
|
34
|
-
assistantOutputLineOpen: false,
|
|
35
35
|
assistantHeaderShown: false,
|
|
36
36
|
outputStarted: false,
|
|
37
37
|
toolDetailsExpanded: false,
|
|
@@ -66,6 +66,28 @@ export function createCommandController({
|
|
|
66
66
|
await input.runSessionsSelector?.();
|
|
67
67
|
return;
|
|
68
68
|
}
|
|
69
|
+
if (isBareForkCommand(text)) {
|
|
70
|
+
if (input.isTerminal && input.runForkSelector) {
|
|
71
|
+
await input.runForkSelector();
|
|
72
|
+
} else {
|
|
73
|
+
output.log?.("/fork requires an interactive terminal.");
|
|
74
|
+
}
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
if (isContextCommand(text)) {
|
|
78
|
+
const argument = contextArgument(text);
|
|
79
|
+
if (argument) {
|
|
80
|
+
output.log?.("Custom ranges are not supported yet; showing the last 7 days.");
|
|
81
|
+
}
|
|
82
|
+
if (input.isTerminal && input.runContextBoard) {
|
|
83
|
+
await input.runContextBoard();
|
|
84
|
+
} else if (input.printContextReport) {
|
|
85
|
+
await input.printContextReport();
|
|
86
|
+
} else {
|
|
87
|
+
output.log?.("/context requires a connected runtime.");
|
|
88
|
+
}
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
69
91
|
const result = await request(runtimeMethods.commandExecute, { input: text });
|
|
70
92
|
await applyResult(result);
|
|
71
93
|
}
|
|
@@ -154,6 +176,18 @@ function isBareSessionsCommand(value) {
|
|
|
154
176
|
return String(value || "").trim().toLowerCase() === "/sessions";
|
|
155
177
|
}
|
|
156
178
|
|
|
179
|
+
function isBareForkCommand(value) {
|
|
180
|
+
return String(value || "").trim().toLowerCase() === "/fork";
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function isContextCommand(value) {
|
|
184
|
+
return /^\/context\b/i.test(String(value || "").trim());
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function contextArgument(value) {
|
|
188
|
+
return String(value || "").trim().replace(/^\/context\b/i, "").trim();
|
|
189
|
+
}
|
|
190
|
+
|
|
157
191
|
function isCompactCommand(value) {
|
|
158
192
|
return String(value || "").trim().toLowerCase() === "/compact";
|
|
159
193
|
}
|