@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
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { wrapTextWithAnsi } from "../text-width.js";
|
|
2
2
|
import {
|
|
3
3
|
codeOpenLabel,
|
|
4
|
+
consumeTableLine,
|
|
5
|
+
createTableState,
|
|
6
|
+
finishTableState,
|
|
4
7
|
dim,
|
|
5
8
|
isPlainLine,
|
|
6
|
-
|
|
7
|
-
parseTableRow,
|
|
8
|
-
renderInline,
|
|
9
|
+
renderTableBlock,
|
|
9
10
|
renderMarkdownishLine,
|
|
10
11
|
styled,
|
|
11
12
|
} from "../markdown-lines.js";
|
|
@@ -18,6 +19,7 @@ export class AssistantMessage {
|
|
|
18
19
|
this.finalized = [];
|
|
19
20
|
this.pending = "";
|
|
20
21
|
this.inCodeBlock = false;
|
|
22
|
+
this.tableState = createTableState();
|
|
21
23
|
this.cacheWidth = -1;
|
|
22
24
|
this.cacheItems = [];
|
|
23
25
|
this.cacheLines = null;
|
|
@@ -32,7 +34,9 @@ export class AssistantMessage {
|
|
|
32
34
|
}
|
|
33
35
|
const line = this.pending.slice(0, newlineIndex);
|
|
34
36
|
this.pending = this.pending.slice(newlineIndex + 1);
|
|
35
|
-
this.classifyFinalize(line)
|
|
37
|
+
if (this.classifyFinalize(line)) {
|
|
38
|
+
this.cacheItems.length = 0;
|
|
39
|
+
}
|
|
36
40
|
}
|
|
37
41
|
this.cacheLines = null;
|
|
38
42
|
}
|
|
@@ -43,18 +47,48 @@ export class AssistantMessage {
|
|
|
43
47
|
this.pending = "";
|
|
44
48
|
this.classifyFinalize(line);
|
|
45
49
|
}
|
|
50
|
+
this.flushTableState();
|
|
51
|
+
this.cacheItems.length = 0;
|
|
46
52
|
this.cacheLines = null;
|
|
47
53
|
}
|
|
48
54
|
|
|
49
55
|
get isEmpty() {
|
|
50
|
-
return !this.finalized.length && !this.pending;
|
|
56
|
+
return !this.finalized.length && !this.pending && !this.tableState.candidate.length && !this.tableState.rows;
|
|
51
57
|
}
|
|
52
58
|
|
|
53
59
|
classifyFinalize(line) {
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
return;
|
|
60
|
+
const result = consumeTableLine(this.tableState, line, this.inCodeBlock);
|
|
61
|
+
if (result.type === "hold") {
|
|
62
|
+
return false;
|
|
63
|
+
}
|
|
64
|
+
if (result.type === "start") {
|
|
65
|
+
for (const line of result.lines || []) {
|
|
66
|
+
this.classifyNonTable(line);
|
|
67
|
+
}
|
|
68
|
+
this.pushItem({ kind: "table", rows: this.tableState.rows });
|
|
69
|
+
return true;
|
|
70
|
+
}
|
|
71
|
+
if (result.type === "append") {
|
|
72
|
+
return true;
|
|
73
|
+
}
|
|
74
|
+
if (result.type === "flush") {
|
|
75
|
+
this.classifyNonTable(result.line);
|
|
76
|
+
return true;
|
|
57
77
|
}
|
|
78
|
+
if (result.type === "flush_candidate") {
|
|
79
|
+
for (const candidate of result.lines) {
|
|
80
|
+
this.classifyNonTable(candidate);
|
|
81
|
+
}
|
|
82
|
+
if (result.line !== undefined) {
|
|
83
|
+
this.classifyNonTable(result.line);
|
|
84
|
+
}
|
|
85
|
+
return false;
|
|
86
|
+
}
|
|
87
|
+
this.classifyNonTable(line);
|
|
88
|
+
return false;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
classifyNonTable(line) {
|
|
58
92
|
if (line.trim().startsWith("```")) {
|
|
59
93
|
this.finalizeCodeFence(line);
|
|
60
94
|
return;
|
|
@@ -74,12 +108,16 @@ export class AssistantMessage {
|
|
|
74
108
|
this.pushItem({ kind: "markdown", raw: line });
|
|
75
109
|
}
|
|
76
110
|
|
|
77
|
-
|
|
78
|
-
const
|
|
79
|
-
if (!
|
|
111
|
+
flushTableState() {
|
|
112
|
+
const result = finishTableState(this.tableState);
|
|
113
|
+
if (!result) {
|
|
80
114
|
return;
|
|
81
115
|
}
|
|
82
|
-
|
|
116
|
+
if (result.type === "flush_candidate") {
|
|
117
|
+
for (const line of result.lines) {
|
|
118
|
+
this.classifyNonTable(line);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
83
121
|
}
|
|
84
122
|
|
|
85
123
|
finalizeCodeFence(line) {
|
|
@@ -95,15 +133,10 @@ export class AssistantMessage {
|
|
|
95
133
|
this.finalized.push(item);
|
|
96
134
|
}
|
|
97
135
|
|
|
98
|
-
styleItem(item) {
|
|
136
|
+
styleItem(item, width) {
|
|
99
137
|
switch (item.kind) {
|
|
100
|
-
case "table":
|
|
101
|
-
|
|
102
|
-
const rendered = cells.map((cell, index) =>
|
|
103
|
-
renderInline(cell, this.color, index === 0 ? "tableHeader" : "")
|
|
104
|
-
);
|
|
105
|
-
return rendered.join(dim(" | ", this.color));
|
|
106
|
-
}
|
|
138
|
+
case "table":
|
|
139
|
+
return renderTableBlock(item.rows, this.color, Math.max(1, width - CONTENT_PREFIX.length));
|
|
107
140
|
case "fence":
|
|
108
141
|
return dim(item.label ? codeOpenLabel(item.label) : "└ end", this.color);
|
|
109
142
|
case "code":
|
|
@@ -125,7 +158,7 @@ export class AssistantMessage {
|
|
|
125
158
|
}
|
|
126
159
|
while (this.cacheItems.length < this.finalized.length) {
|
|
127
160
|
const item = this.finalized[this.cacheItems.length];
|
|
128
|
-
const logical = typeof item === "string" ? item : this.styleItem(item);
|
|
161
|
+
const logical = typeof item === "string" ? item : this.styleItem(item, width);
|
|
129
162
|
this.cacheItems.push(this.wrapLogical(logical, width));
|
|
130
163
|
}
|
|
131
164
|
const lines = [];
|
package/lib/composer-terminal.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
+
import { ANSI_SEQUENCE } from "./text-width.js";
|
|
1
2
|
import { graphemes, stripAnsi, textWidth, wrapTextCells } from "./text-width.js";
|
|
2
3
|
|
|
3
4
|
const DEFAULT_COLUMNS = 80;
|
|
4
5
|
const INPUT_MARKER = "\n ▷ ";
|
|
5
|
-
const ANSI_SEQUENCE = /\x1b\[[0-?]*[ -/]*[@-~]/g;
|
|
6
6
|
const SGR_SEQUENCE = /^\x1b\[([0-9;]*)m$/;
|
|
7
7
|
|
|
8
8
|
export function prepareComposerFrame(frame = {}, columns = DEFAULT_COLUMNS) {
|
package/lib/event-controller.js
CHANGED
|
@@ -3,7 +3,6 @@ import {
|
|
|
3
3
|
cancelledText,
|
|
4
4
|
contextBuiltLine,
|
|
5
5
|
errorLine,
|
|
6
|
-
goalContinuedLine,
|
|
7
6
|
planUpdatedLine,
|
|
8
7
|
turnCompletedLine,
|
|
9
8
|
} from "./rendering.js";
|
|
@@ -32,8 +31,14 @@ export function createEventController({
|
|
|
32
31
|
}
|
|
33
32
|
switch (eventType) {
|
|
34
33
|
case "assistant_delta":
|
|
34
|
+
output.setActivityLabel?.("Working");
|
|
35
35
|
output.assistantAppend?.(event.text || "");
|
|
36
36
|
return;
|
|
37
|
+
case "turn_step_retry": {
|
|
38
|
+
const attempt = Number(event.attempt);
|
|
39
|
+
output.setActivityLabel?.(attempt > 0 ? `Retrying ${attempt}` : "Retrying");
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
37
42
|
case "context_built": {
|
|
38
43
|
if (output.handleContextBuilt?.(event)) {
|
|
39
44
|
output.resetContextUsage?.();
|
|
@@ -113,19 +118,12 @@ export function createEventController({
|
|
|
113
118
|
await input.answerQuestion?.(event);
|
|
114
119
|
return;
|
|
115
120
|
case "queued_input_delivered":
|
|
116
|
-
output.setGoalChasing?.(false);
|
|
117
121
|
output.deliverQueuedInput?.(event.input || "", event.mode || "steering", event.input_id || "");
|
|
118
122
|
return;
|
|
119
|
-
case "goal_continued":
|
|
120
|
-
output.closeAssistant?.();
|
|
121
|
-
output.setGoalChasing?.(true);
|
|
122
|
-
output.log?.(() => goalContinuedLine(event.round));
|
|
123
|
-
return;
|
|
124
123
|
case "turn_failed":
|
|
125
124
|
output.clearQueuedInputs?.();
|
|
126
125
|
output.clearCompactContext?.();
|
|
127
126
|
output.closeAssistant?.();
|
|
128
|
-
output.setGoalChasing?.(false);
|
|
129
127
|
output.log?.(() => errorLine(event.error));
|
|
130
128
|
resetTurnState();
|
|
131
129
|
return;
|
|
@@ -133,7 +131,6 @@ export function createEventController({
|
|
|
133
131
|
output.clearQueuedInputs?.();
|
|
134
132
|
output.clearCompactContext?.();
|
|
135
133
|
output.closeAssistant?.();
|
|
136
|
-
output.setGoalChasing?.(false);
|
|
137
134
|
output.log?.(() => cancelledText());
|
|
138
135
|
resetTurnState();
|
|
139
136
|
return;
|
|
@@ -141,8 +138,9 @@ export function createEventController({
|
|
|
141
138
|
output.clearQueuedInputs?.();
|
|
142
139
|
output.clearCompactContext?.();
|
|
143
140
|
output.closeAssistant?.();
|
|
144
|
-
|
|
145
|
-
|
|
141
|
+
if (state.activeGoal?.status !== "active") {
|
|
142
|
+
output.log?.(turnCompletedLine(event, toolStats));
|
|
143
|
+
}
|
|
146
144
|
resetTurnState();
|
|
147
145
|
return;
|
|
148
146
|
default:
|
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
} from "./runtime-protocol.js";
|
|
17
17
|
import { executeLocalSlashCommand, loadLocalSettings } from "./local-slash-commands.js";
|
|
18
18
|
import { loadCliState, saveCliState } from "./cli-state-store.js";
|
|
19
|
+
import { loadPromptHistory, savePromptHistory } from "./prompt-history-store.js";
|
|
19
20
|
import { setTheme } from "./theme.js";
|
|
20
21
|
import { createTurnController } from "./turn-controller.js";
|
|
21
22
|
import { createCommandController } from "./command-controller.js";
|
|
@@ -30,6 +31,8 @@ import { createCliRuntimeController } from "./cli-runtime-controller.js";
|
|
|
30
31
|
import { createCliOutputController } from "./cli-output-controller.js";
|
|
31
32
|
import { createCliInputActions } from "./cli-input-actions.js";
|
|
32
33
|
import { cliHelp, oneShotHelp, runOneShot } from "./one-shot.js";
|
|
34
|
+
import { runSend, sendHelp } from "./send.js";
|
|
35
|
+
import { listenIpc } from "./ipc.js";
|
|
33
36
|
import { createTui } from "./tui/tui.js";
|
|
34
37
|
import { Container } from "./tui/component.js";
|
|
35
38
|
import { ComposerArea } from "./components/composer-area.js";
|
|
@@ -72,6 +75,19 @@ if (cliArgs[0] === "run" && cliArgs.some((arg) => arg === "--help" || arg === "-
|
|
|
72
75
|
process.stdout.write(`${oneShotHelp}\n`);
|
|
73
76
|
return;
|
|
74
77
|
}
|
|
78
|
+
if (cliArgs[0] === "send" && cliArgs.some((arg) => arg === "--help" || arg === "-h")) {
|
|
79
|
+
process.stdout.write(`${sendHelp}\n`);
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
if (cliArgs[0] === "send") {
|
|
83
|
+
try {
|
|
84
|
+
process.exitCode = await runSend({ args: cliArgs });
|
|
85
|
+
} catch (error) {
|
|
86
|
+
process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
|
|
87
|
+
process.exitCode = 2;
|
|
88
|
+
}
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
75
91
|
if (cliArgs.some((arg) => arg === "--version" || arg === "--help" || arg === "-h")) {
|
|
76
92
|
if (cliArgs.includes("--help") || cliArgs.includes("-h")) {
|
|
77
93
|
process.stdout.write(`${cliHelp}\n\n`);
|
|
@@ -102,7 +118,9 @@ const sessionState = cliState.session;
|
|
|
102
118
|
const turnStateData = cliState.turn;
|
|
103
119
|
const inputStateData = cliState.input;
|
|
104
120
|
const displayState = cliState.display;
|
|
121
|
+
const promptHistory = loadPromptHistory();
|
|
105
122
|
let input = null;
|
|
123
|
+
let ipcServer = null;
|
|
106
124
|
const compactContextState = createCompactContextState();
|
|
107
125
|
const isTty = Boolean(process.stdin.isTTY && process.stdout.isTTY);
|
|
108
126
|
const tui = isTty
|
|
@@ -143,6 +161,7 @@ const {
|
|
|
143
161
|
writeError: writeErrorOutput,
|
|
144
162
|
closeAssistant,
|
|
145
163
|
renderHistory,
|
|
164
|
+
setTurnContext,
|
|
146
165
|
} = outputController;
|
|
147
166
|
|
|
148
167
|
const runtimeClient = createRuntimeClient({
|
|
@@ -168,6 +187,10 @@ const runtimeClient = createRuntimeClient({
|
|
|
168
187
|
displayState.lastEventSequence = 0;
|
|
169
188
|
turnStateData.active = false;
|
|
170
189
|
turnStateData.interruptRequested = false;
|
|
190
|
+
displayState.activityLabel = "";
|
|
191
|
+
displayState.lastTurnId = "";
|
|
192
|
+
setTurnContext("");
|
|
193
|
+
clearActivityTimer();
|
|
171
194
|
inputActions?.clearPendingInputs();
|
|
172
195
|
if (!wasClosing) {
|
|
173
196
|
runtimeState.failure = error;
|
|
@@ -206,15 +229,17 @@ const runtimeController = createCliRuntimeController({
|
|
|
206
229
|
requireInitialization: requireRuntimeInitialization,
|
|
207
230
|
state: cliState,
|
|
208
231
|
getCommands: () => commandController,
|
|
209
|
-
getTurnController: () => turnController,
|
|
210
232
|
getTaskMonitor: () => taskMonitorController,
|
|
211
233
|
getCompactContextState: () => compactContextState,
|
|
212
234
|
askModelMenu: (...args) => inputActions.askModelMenu(...args),
|
|
213
235
|
askEffortMenu: (...args) => inputActions.askEffortMenu(...args),
|
|
214
236
|
askSessionMenu: (...args) => inputActions.askSessionMenu(...args),
|
|
237
|
+
askForkPointMenu: (...args) => inputActions.askForkPointMenu(...args),
|
|
215
238
|
askTeamBlueprint: (...args) => inputActions.askTeamBlueprint(...args),
|
|
239
|
+
askContextBoard: (...args) => inputActions.askContextBoard(...args),
|
|
216
240
|
restoreLiveTurn,
|
|
217
241
|
renderHistory,
|
|
242
|
+
onSessionRestored: rebindSendEndpoint,
|
|
218
243
|
clearPendingInputs: (...args) => inputActions.clearPendingInputs(...args),
|
|
219
244
|
closeAssistant,
|
|
220
245
|
refreshInputState,
|
|
@@ -227,7 +252,6 @@ const request = runtimeController.request;
|
|
|
227
252
|
turnController = createTurnController({
|
|
228
253
|
request,
|
|
229
254
|
state: turnState,
|
|
230
|
-
refreshGoalState: runtimeController.refreshGoalState,
|
|
231
255
|
onTurnStart: () => {
|
|
232
256
|
displayState.assistantHeaderShown = false;
|
|
233
257
|
},
|
|
@@ -257,6 +281,9 @@ commandController = createCommandController({
|
|
|
257
281
|
},
|
|
258
282
|
startCompactCommand: runtimeController.startCompactCommand,
|
|
259
283
|
runSessionsSelector: runtimeController.runSessionsSelector,
|
|
284
|
+
runForkSelector: runtimeController.runForkSelector,
|
|
285
|
+
runContextBoard: runtimeController.runContextBoard,
|
|
286
|
+
printContextReport: runtimeController.printContextReport,
|
|
260
287
|
runLocalCommand: async (text) => {
|
|
261
288
|
if (!Object.keys(sessionState.settings).length) {
|
|
262
289
|
sessionState.settings = await loadLocalSettings(undefined, sessionState.info.workspace_root || sessionState.info.cwd || process.cwd());
|
|
@@ -322,6 +349,9 @@ const eventController = createEventController({
|
|
|
322
349
|
get activeTurn() {
|
|
323
350
|
return turnStateData.active;
|
|
324
351
|
},
|
|
352
|
+
get activeGoal() {
|
|
353
|
+
return sessionState.info.goal;
|
|
354
|
+
},
|
|
325
355
|
debug: cliArgs.includes("--debug"),
|
|
326
356
|
},
|
|
327
357
|
input: { answerQuestion: (...args) => inputActions.answerQuestion(...args) },
|
|
@@ -337,12 +367,10 @@ const eventController = createEventController({
|
|
|
337
367
|
log: logOutput,
|
|
338
368
|
debug: (text) => writeErrorOutput(`${text}\n`),
|
|
339
369
|
updateGoal: updateGoalState,
|
|
340
|
-
setGoalChasing: (enabled) => {
|
|
341
|
-
displayState.goalChasing = Boolean(enabled);
|
|
342
|
-
},
|
|
343
370
|
setStats: (stats) => {
|
|
344
371
|
displayState.stats = stats;
|
|
345
372
|
},
|
|
373
|
+
setActivityLabel: outputController.setActivityLabel,
|
|
346
374
|
redraw: redrawInput,
|
|
347
375
|
clearCompactContext: () => compactContextState.clear(),
|
|
348
376
|
deliverQueuedInput: (...args) => inputActions.deliverQueuedInput(...args),
|
|
@@ -353,7 +381,10 @@ inputActions = createCliInputActions({
|
|
|
353
381
|
state: cliState,
|
|
354
382
|
request,
|
|
355
383
|
output: outputController,
|
|
384
|
+
promptHistory,
|
|
385
|
+
onPromptHistory: (history) => savePromptHistory(history),
|
|
356
386
|
getTurnController: () => turnController,
|
|
387
|
+
getCommandController: () => commandController,
|
|
357
388
|
getTaskMonitor: () => taskMonitorController,
|
|
358
389
|
getLineInput: () => input,
|
|
359
390
|
pausePrompt: () => inputController.pause(),
|
|
@@ -427,6 +458,26 @@ function updateGoalState(goal) {
|
|
|
427
458
|
sessionState.info = { ...sessionState.info, goal: goal && typeof goal === "object" ? goal : null };
|
|
428
459
|
redrawInput();
|
|
429
460
|
}
|
|
461
|
+
|
|
462
|
+
async function rebindSendEndpoint() {
|
|
463
|
+
try {
|
|
464
|
+
await ipcServer?.close();
|
|
465
|
+
ipcServer = null;
|
|
466
|
+
const sessionId = String(sessionState.info.session_id || "");
|
|
467
|
+
if (!sessionId) {
|
|
468
|
+
return;
|
|
469
|
+
}
|
|
470
|
+
ipcServer = await listenIpc({
|
|
471
|
+
sessionId,
|
|
472
|
+
getSessionId: () => sessionState.info.session_id,
|
|
473
|
+
dispatch: (text) => inputActions.dispatchExternal(text),
|
|
474
|
+
onUnavailable: () => logOutput(`Send endpoint unavailable: another rind process owns session ${sessionId}.`),
|
|
475
|
+
});
|
|
476
|
+
} catch (error) {
|
|
477
|
+
ipcServer = null;
|
|
478
|
+
writeErrorOutput(`${error instanceof Error ? error.message : String(error)}\n`);
|
|
479
|
+
}
|
|
480
|
+
}
|
|
430
481
|
function resetContextUsage() {
|
|
431
482
|
displayState.stats = { context_usage_percent: 0 };
|
|
432
483
|
redrawInput();
|
|
@@ -442,25 +493,57 @@ async function renderEvent(message) {
|
|
|
442
493
|
return;
|
|
443
494
|
}
|
|
444
495
|
if (message?.event?.type === "turn_started") {
|
|
445
|
-
|
|
496
|
+
const nextTurnId = String(message.turn_id || "");
|
|
497
|
+
if (!nextTurnId || (!turnStateData.id && displayState.lastTurnId === nextTurnId)
|
|
498
|
+
|| (turnStateData.id && nextTurnId !== turnStateData.id)) {
|
|
446
499
|
return;
|
|
447
500
|
}
|
|
448
|
-
|
|
501
|
+
clearActivityTimer();
|
|
502
|
+
turnStateData.id = nextTurnId;
|
|
503
|
+
displayState.lastTurnId = nextTurnId;
|
|
504
|
+
setTurnContext(nextTurnId);
|
|
449
505
|
turnStateData.active = Boolean(turnStateData.id);
|
|
506
|
+
turnStateData.interruptRequested = false;
|
|
507
|
+
displayState.activityLabel = "Working";
|
|
508
|
+
displayState.assistantHeaderShown = false;
|
|
509
|
+
refreshInputState();
|
|
450
510
|
}
|
|
451
511
|
const result = await eventController.handle(message);
|
|
452
512
|
if (["turn_completed", "turn_failed", "turn_cancelled"].includes(message?.event?.type)) {
|
|
453
513
|
turnStateData.id = "";
|
|
514
|
+
setTurnContext("");
|
|
515
|
+
displayState.activityLabel = "";
|
|
516
|
+
await runtimeController.refreshGoalState();
|
|
517
|
+
const goalActive = sessionState.info.goal?.status === "active";
|
|
518
|
+
turnStateData.active = goalActive;
|
|
519
|
+
turnStateData.interruptRequested = false;
|
|
520
|
+
if (!goalActive) {
|
|
521
|
+
clearActivityTimer();
|
|
522
|
+
}
|
|
523
|
+
refreshInputState();
|
|
454
524
|
}
|
|
455
525
|
return result;
|
|
456
526
|
}
|
|
457
527
|
|
|
458
528
|
function restoreLiveTurn(value) {
|
|
459
|
-
if (!value || typeof value !== "object")
|
|
529
|
+
if (!value || typeof value !== "object" || String(value.status || "") !== "running") {
|
|
530
|
+
displayState.lastTurnId = "";
|
|
531
|
+
setTurnContext("");
|
|
532
|
+
return;
|
|
533
|
+
}
|
|
460
534
|
const turnId = String(value.turn_id || "");
|
|
461
|
-
if (!turnId)
|
|
535
|
+
if (!turnId) {
|
|
536
|
+
displayState.lastTurnId = "";
|
|
537
|
+
setTurnContext("");
|
|
538
|
+
return;
|
|
539
|
+
}
|
|
540
|
+
clearActivityTimer();
|
|
462
541
|
turnStateData.id = turnId;
|
|
463
542
|
turnStateData.active = true;
|
|
543
|
+
turnStateData.interruptRequested = false;
|
|
544
|
+
displayState.activityLabel = "Working";
|
|
545
|
+
displayState.lastTurnId = turnId;
|
|
546
|
+
setTurnContext(turnId);
|
|
464
547
|
displayState.assistantHeaderShown = false;
|
|
465
548
|
const text = String(value.assistant_text || "");
|
|
466
549
|
if (text) outputController.assistantAppend(text);
|
|
@@ -471,7 +554,7 @@ function composeFrame(width = process.stdout.columns || 80) {
|
|
|
471
554
|
if (!session) {
|
|
472
555
|
return null;
|
|
473
556
|
}
|
|
474
|
-
const choiceMenu = ["model", "theme", "sessions", "team-blueprints"].includes(session.mode);
|
|
557
|
+
const choiceMenu = ["model", "theme", "sessions", "team-blueprints", "fork"].includes(session.mode);
|
|
475
558
|
if (session.mode === "prompt" && session.menuState) {
|
|
476
559
|
session.menuState.setInput(session.editor.input());
|
|
477
560
|
}
|
|
@@ -500,6 +583,8 @@ function composeFrame(width = process.stdout.columns || 80) {
|
|
|
500
583
|
}
|
|
501
584
|
if (session.mode === "question") {
|
|
502
585
|
const editing = session.questionState.isEditing();
|
|
586
|
+
session.editor.setViewportWidth(width);
|
|
587
|
+
const editorCursor = editing ? session.editor.cursorPosition() : null;
|
|
503
588
|
const menu = questionMenuFrame(
|
|
504
589
|
session.questionState.options(),
|
|
505
590
|
session.questionState.selectedIndex(),
|
|
@@ -507,17 +592,18 @@ function composeFrame(width = process.stdout.columns || 80) {
|
|
|
507
592
|
editing,
|
|
508
593
|
CUSTOM_ANSWER_LABEL,
|
|
509
594
|
width,
|
|
595
|
+
editorCursor,
|
|
510
596
|
);
|
|
511
597
|
return {
|
|
512
598
|
showCaret,
|
|
513
599
|
prompt: mainPromptText(width),
|
|
514
600
|
inputText: session.question,
|
|
515
|
-
cursor:
|
|
601
|
+
cursor: editorCursor ?? { line: 0, column: session.question.length },
|
|
516
602
|
menuText: menu.text.trimEnd(),
|
|
517
603
|
menuCursor: editing ? menu.cursor : null,
|
|
518
604
|
};
|
|
519
605
|
}
|
|
520
|
-
if (session.mode === "sessions") {
|
|
606
|
+
if (session.mode === "sessions" || session.mode === "team-blueprints" || session.mode === "fork") {
|
|
521
607
|
return {
|
|
522
608
|
showCaret,
|
|
523
609
|
prompt: mainPromptText(width),
|
|
@@ -526,13 +612,15 @@ function composeFrame(width = process.stdout.columns || 80) {
|
|
|
526
612
|
menuText: sessionMenuText(session.choiceState.options(), session.choiceState.selectedIndex()).trimEnd(),
|
|
527
613
|
};
|
|
528
614
|
}
|
|
529
|
-
if (session.mode === "
|
|
615
|
+
if (session.mode === "context-board") {
|
|
530
616
|
return {
|
|
531
|
-
showCaret,
|
|
532
|
-
prompt:
|
|
533
|
-
inputText:
|
|
534
|
-
cursor: { line: 0, column:
|
|
535
|
-
menuText:
|
|
617
|
+
showCaret: false,
|
|
618
|
+
prompt: "",
|
|
619
|
+
inputText: "",
|
|
620
|
+
cursor: { line: 0, column: 0 },
|
|
621
|
+
menuText: typeof session.board?.render === "function"
|
|
622
|
+
? session.board.render(session.pageIndex, width)
|
|
623
|
+
: "",
|
|
536
624
|
};
|
|
537
625
|
}
|
|
538
626
|
const matches = session.menuState
|
|
@@ -590,6 +678,7 @@ function closeRuntime() {
|
|
|
590
678
|
runtimeState.status = "closing";
|
|
591
679
|
clearActivityTimer();
|
|
592
680
|
taskMonitorController.stop();
|
|
681
|
+
void ipcServer?.close();
|
|
593
682
|
void runtimeClient.shutdown();
|
|
594
683
|
closeInput();
|
|
595
684
|
}
|
|
@@ -598,6 +687,7 @@ function forceCloseRuntime() {
|
|
|
598
687
|
runtimeState.status = "closing";
|
|
599
688
|
clearActivityTimer();
|
|
600
689
|
taskMonitorController.stop();
|
|
690
|
+
void ipcServer?.close();
|
|
601
691
|
closeInput();
|
|
602
692
|
runtimeClient.forceShutdown();
|
|
603
693
|
}
|
package/lib/ipc.js
ADDED
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import { mkdir, unlink } from "node:fs/promises";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import net from "node:net";
|
|
5
|
+
|
|
6
|
+
const CONNECT_TIMEOUT_MS = 2000;
|
|
7
|
+
|
|
8
|
+
export function ipcEndpointName(sessionId) {
|
|
9
|
+
const id = String(sessionId || "");
|
|
10
|
+
if (!/^[A-Za-z0-9_-]+$/.test(id)) {
|
|
11
|
+
throw new Error("Invalid session id.");
|
|
12
|
+
}
|
|
13
|
+
if (process.platform === "win32") {
|
|
14
|
+
return `\\\\.\\pipe\\rind-${id}`;
|
|
15
|
+
}
|
|
16
|
+
return path.join(process.env.RIND_HOME || path.join(homedir(), ".rind"), "ipc", `${id}.sock`);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export async function listenIpc({ sessionId, getSessionId, dispatch, net: netImpl = net, onUnavailable = () => {} }) {
|
|
20
|
+
const endpoint = ipcEndpointName(sessionId);
|
|
21
|
+
const server = netImpl.createServer();
|
|
22
|
+
let listening = false;
|
|
23
|
+
let closing = false;
|
|
24
|
+
let settled = false;
|
|
25
|
+
let recovering = false;
|
|
26
|
+
|
|
27
|
+
server.on("connection", (socket) => {
|
|
28
|
+
socket.setEncoding("utf8");
|
|
29
|
+
let buffer = "";
|
|
30
|
+
socket.on("data", (chunk) => {
|
|
31
|
+
buffer += chunk;
|
|
32
|
+
const newlineIndex = buffer.indexOf("\n");
|
|
33
|
+
if (newlineIndex === -1) {
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
socket.removeAllListeners("data");
|
|
37
|
+
respond(socket, buffer.slice(0, newlineIndex));
|
|
38
|
+
});
|
|
39
|
+
socket.on("error", () => {});
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
const ready = new Promise((resolve) => {
|
|
43
|
+
const settle = () => {
|
|
44
|
+
if (settled) {
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
settled = true;
|
|
48
|
+
resolve();
|
|
49
|
+
};
|
|
50
|
+
server.on("listening", () => {
|
|
51
|
+
listening = true;
|
|
52
|
+
settle();
|
|
53
|
+
});
|
|
54
|
+
server.on("error", () => {
|
|
55
|
+
if (settled || recovering) {
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
recovering = true;
|
|
59
|
+
void recover().then(settle, settle).finally(() => {
|
|
60
|
+
recovering = false;
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
function respond(socket, line) {
|
|
66
|
+
let input = "";
|
|
67
|
+
try {
|
|
68
|
+
const message = JSON.parse(line);
|
|
69
|
+
if (typeof message.input === "string") {
|
|
70
|
+
input = message.input;
|
|
71
|
+
}
|
|
72
|
+
} catch {
|
|
73
|
+
return socket.end(`${JSON.stringify({ ok: false, message: "Invalid request." })}\n`);
|
|
74
|
+
}
|
|
75
|
+
const payload = closing
|
|
76
|
+
? { ok: false, message: "Session is shutting down." }
|
|
77
|
+
: input.trim()
|
|
78
|
+
? { ok: true, session_id: String(getSessionId() || "") }
|
|
79
|
+
: { ok: false, message: "Empty input." };
|
|
80
|
+
socket.end(`${JSON.stringify(payload)}\n`);
|
|
81
|
+
if (payload.ok) {
|
|
82
|
+
dispatch(input);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
async function recover() {
|
|
87
|
+
if (process.platform === "win32") {
|
|
88
|
+
onUnavailable();
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
if (await probeEndpoint(netImpl, endpoint)) {
|
|
92
|
+
onUnavailable();
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
await unlink(endpoint).catch(() => {});
|
|
96
|
+
await new Promise((resolve) => {
|
|
97
|
+
const onRetryError = () => {
|
|
98
|
+
onUnavailable();
|
|
99
|
+
resolve();
|
|
100
|
+
};
|
|
101
|
+
server.once("error", onRetryError);
|
|
102
|
+
server.listen(endpoint, () => {
|
|
103
|
+
server.removeListener("error", onRetryError);
|
|
104
|
+
listening = true;
|
|
105
|
+
resolve();
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (process.platform !== "win32") {
|
|
111
|
+
await mkdir(path.dirname(endpoint), { recursive: true }).catch(() => {});
|
|
112
|
+
}
|
|
113
|
+
server.listen(endpoint);
|
|
114
|
+
await ready;
|
|
115
|
+
return {
|
|
116
|
+
close() {
|
|
117
|
+
closing = true;
|
|
118
|
+
return new Promise((resolve) => {
|
|
119
|
+
server.close(() => {
|
|
120
|
+
if (process.platform !== "win32" && listening) {
|
|
121
|
+
void unlink(endpoint).catch(() => {});
|
|
122
|
+
}
|
|
123
|
+
resolve();
|
|
124
|
+
});
|
|
125
|
+
});
|
|
126
|
+
},
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function probeEndpoint(netImpl, endpoint) {
|
|
131
|
+
return new Promise((resolve) => {
|
|
132
|
+
const probe = netImpl.connect(endpoint);
|
|
133
|
+
probe.once("connect", () => {
|
|
134
|
+
probe.destroy();
|
|
135
|
+
resolve(true);
|
|
136
|
+
});
|
|
137
|
+
probe.once("error", () => resolve(false));
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export function sendIpc({ session, input, timeoutMs = CONNECT_TIMEOUT_MS, net: netImpl = net }) {
|
|
142
|
+
const endpoint = ipcEndpointName(session);
|
|
143
|
+
return new Promise((resolve) => {
|
|
144
|
+
const socket = netImpl.connect(endpoint);
|
|
145
|
+
let timer = null;
|
|
146
|
+
let settled = false;
|
|
147
|
+
let buffer = "";
|
|
148
|
+
const finish = (result) => {
|
|
149
|
+
if (settled) {
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
settled = true;
|
|
153
|
+
if (timer) {
|
|
154
|
+
clearTimeout(timer);
|
|
155
|
+
}
|
|
156
|
+
socket.destroy();
|
|
157
|
+
resolve(result);
|
|
158
|
+
};
|
|
159
|
+
socket.setEncoding("utf8");
|
|
160
|
+
socket.on("connect", () => {
|
|
161
|
+
socket.write(`${JSON.stringify({ input: String(input || "") })}\n`);
|
|
162
|
+
});
|
|
163
|
+
socket.on("data", (chunk) => {
|
|
164
|
+
buffer += chunk;
|
|
165
|
+
const newlineIndex = buffer.indexOf("\n");
|
|
166
|
+
if (newlineIndex === -1) {
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
let payload = null;
|
|
170
|
+
try {
|
|
171
|
+
payload = JSON.parse(buffer.slice(0, newlineIndex));
|
|
172
|
+
} catch {
|
|
173
|
+
payload = null;
|
|
174
|
+
}
|
|
175
|
+
finish(payload && typeof payload === "object" ? payload : { ok: false, message: "Invalid response from the rind session." });
|
|
176
|
+
});
|
|
177
|
+
socket.on("error", () => finish({ ok: false, message: notRunningMessage(session) }));
|
|
178
|
+
timer = setTimeout(() => {
|
|
179
|
+
finish({ ok: false, message: notRunningMessage(session) });
|
|
180
|
+
}, timeoutMs);
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function notRunningMessage(sessionId) {
|
|
185
|
+
return `rind session ${sessionId} is not running — the id is shown in the target session's banner and /status.`;
|
|
186
|
+
}
|