@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
@@ -1,126 +1,159 @@
1
- import {
2
- isReadonlySlashCommand,
3
- parseGoalCommand,
4
- steeringCommandText,
5
- } from "./slash-command-mode.js";
6
- import {
7
- helpText,
8
- slashResultText,
9
- } from "./rendering.js";
10
-
11
- export function createCommandController({
12
- request,
13
- turn,
14
- input = {},
15
- state = {},
16
- output = {},
17
- }) {
18
- async function handle(text) {
19
- if (text === "?") {
20
- output.log?.(helpText(state.slashCommands || []));
21
- return true;
22
- }
23
- if (!String(text || "").startsWith("/")) {
24
- return false;
25
- }
26
- const goal = parseGoalCommand(text);
27
- if (goal) {
28
- await input.runGoalCommand?.(goal);
29
- return true;
30
- }
31
- const steering = steeringCommandText(text);
32
- if (steering !== null) {
33
- turn.submitSteering(steering, text);
34
- return true;
35
- }
36
- await runSlashCommand(text);
37
- return true;
38
- }
39
-
40
- async function runSlashCommand(text) {
41
- if (isBareModelCommand(text) && input.isTerminal) {
42
- await input.runModelSelector?.();
43
- return;
44
- }
45
- if (isCompactCommand(text) && input.isTerminal) {
46
- input.startCompactCommand?.();
47
- return;
48
- }
49
- if (isBareSessionsCommand(text) && input.isTerminal) {
50
- await input.runSessionsSelector?.();
51
- return;
52
- }
53
- if (isReadonlySlashCommand(text) && input.isTerminal) {
54
- input.startReadonlySlashCommand?.(text);
55
- return;
56
- }
57
- const result = await request("slash.execute", { input: text });
58
- await applyResult(result);
59
- }
60
-
61
- async function applyResult(result = {}) {
62
- if (result.clear_screen) {
63
- output.clearScreen?.();
64
- }
65
- const text = slashResultText(result, state.slashCommands || []);
66
- if (text) {
67
- output.log?.(text);
68
- }
69
- if (result.context_usage_reset) {
70
- output.resetContextUsage?.();
71
- }
72
- if (result.input_prefill) {
73
- output.setInputPrefill?.(result.input_prefill);
74
- }
75
- if (result.run_turn_input) {
76
- turn.submit(result.run_turn_input, {
77
- transient_system_messages: result.transient_system_messages,
78
- });
79
- }
80
- if (result.should_exit) {
81
- await output.shutdown?.();
82
- output.exit?.();
83
- }
84
- }
85
-
86
- function normalizeCommands(commands) {
87
- if (!Array.isArray(commands)) {
88
- return [];
89
- }
90
- const items = [];
91
- for (const command of commands) {
92
- const name = singleWord(command?.name);
93
- if (!name) {
94
- continue;
95
- }
96
- const description = String(command.description || "").trim();
97
- items.push({ name, description });
98
- for (const alias of command.aliases || []) {
99
- const aliasName = singleWord(alias);
100
- if (aliasName) {
101
- items.push({ name: aliasName, description: `alias for /${name}` });
102
- }
103
- }
104
- }
105
- return items.sort((left, right) => left.name.localeCompare(right.name));
106
- }
107
-
108
- return { handle, normalizeCommands, applyResult };
109
- }
110
-
111
- function singleWord(value) {
112
- const text = String(value || "").trim().toLowerCase();
113
- return text && !/\s/.test(text) ? text : "";
114
- }
115
-
116
- function isBareModelCommand(value) {
117
- return String(value || "").trim().toLowerCase() === "/model";
118
- }
119
-
120
- function isBareSessionsCommand(value) {
121
- return String(value || "").trim().toLowerCase() === "/sessions";
122
- }
123
-
124
- function isCompactCommand(value) {
125
- return String(value || "").trim().toLowerCase() === "/compact";
126
- }
1
+ import { parseGoalCommand } from "./slash-command-mode.js";
2
+ import {
3
+ helpText,
4
+ slashResultText,
5
+ } from "./rendering.js";
6
+ import { LOCAL_SLASH_COMMANDS } from "./local-slash-commands.js";
7
+ import { runtimeMethods } from "./runtime-protocol.js";
8
+
9
+ const LOCAL_COMMANDS = Object.freeze([
10
+ { name: "exit", description: "Exit Rind", aliases: ["quit"] },
11
+ ]);
12
+
13
+ export function createCommandController({
14
+ request,
15
+ turn,
16
+ input = {},
17
+ state = {},
18
+ output = {},
19
+ }) {
20
+ async function handle(text) {
21
+ if (text === "?") {
22
+ output.log?.(helpText(state.slashCommands || []));
23
+ return true;
24
+ }
25
+ if (!String(text || "").startsWith("/")) {
26
+ return false;
27
+ }
28
+ if (isBareThemeCommand(text) && input.isTerminal && input.runThemeSelector) {
29
+ await input.runThemeSelector();
30
+ return true;
31
+ }
32
+ const localResult = await input.runLocalCommand?.(text);
33
+ if (localResult) {
34
+ await applyResult(localResult);
35
+ return true;
36
+ }
37
+ const goal = parseGoalCommand(text);
38
+ if (goal) {
39
+ await input.runGoalCommand?.(goal);
40
+ return true;
41
+ }
42
+ if (isLocalCommand(text, "exit") || isLocalCommand(text, "quit")) {
43
+ output.log?.("Goodbye.");
44
+ await output.shutdown?.();
45
+ output.exit?.();
46
+ return true;
47
+ }
48
+ await runSlashCommand(text);
49
+ return true;
50
+ }
51
+
52
+ async function runSlashCommand(text) {
53
+ if (isBareModelCommand(text) && input.isTerminal && input.runModelSelector) {
54
+ await input.runModelSelector();
55
+ return;
56
+ }
57
+ if (isEffortCommand(text)) {
58
+ await input.runEffortCommand?.(effortArgument(text));
59
+ return;
60
+ }
61
+ if (isCompactCommand(text) && input.isTerminal) {
62
+ input.startCompactCommand?.();
63
+ return;
64
+ }
65
+ if (isBareSessionsCommand(text) && input.isTerminal) {
66
+ await input.runSessionsSelector?.();
67
+ return;
68
+ }
69
+ const result = await request(runtimeMethods.commandExecute, { input: text });
70
+ await applyResult(result);
71
+ }
72
+
73
+ async function applyResult(result = {}) {
74
+ const text = () => slashResultText(result, state.slashCommands || []);
75
+ output.log?.(text);
76
+ if (result.display?.type === "team_blueprints" && input.askTeamBlueprint) {
77
+ const blueprints = Array.isArray(result.display.blueprints) ? result.display.blueprints : [];
78
+ const selected = await input.askTeamBlueprint(blueprints);
79
+ if (selected?.id) {
80
+ await runSlashCommand(`/team blueprint ${selected.id}`);
81
+ }
82
+ return;
83
+ }
84
+ if (result.prompt_prefill) {
85
+ output.setInputPrefill?.(result.prompt_prefill);
86
+ }
87
+ const nextPromptInput = result.next_prompt?.input;
88
+ if (typeof nextPromptInput === "string" && nextPromptInput.trim()) {
89
+ turn.submit(nextPromptInput, {
90
+ transient_system_messages: result.next_prompt.transient_system_messages,
91
+ });
92
+ }
93
+ }
94
+
95
+ function normalizeCommands(commands) {
96
+ if (!Array.isArray(commands)) {
97
+ return [];
98
+ }
99
+ const items = [];
100
+ for (const command of commands) {
101
+ const name = singleWord(command?.name);
102
+ if (!name) {
103
+ continue;
104
+ }
105
+ const description = String(command.description || "").trim();
106
+ items.push({ name, description });
107
+ for (const alias of command.aliases || []) {
108
+ const aliasName = singleWord(alias);
109
+ if (aliasName) {
110
+ items.push({ name: aliasName, description: `alias for /${name}` });
111
+ }
112
+ }
113
+ }
114
+ return items.sort((left, right) => left.name.localeCompare(right.name));
115
+ }
116
+
117
+ return {
118
+ handle,
119
+ normalizeCommands,
120
+ localCommands: () => [
121
+ ...LOCAL_COMMANDS,
122
+ ...LOCAL_SLASH_COMMANDS,
123
+ ].flatMap((command) => normalizeCommands([command])),
124
+ applyResult,
125
+ };
126
+ }
127
+
128
+ function singleWord(value) {
129
+ const text = String(value || "").trim().toLowerCase();
130
+ return text && !/\s/.test(text) ? text : "";
131
+ }
132
+
133
+ function isBareModelCommand(value) {
134
+ return String(value || "").trim().toLowerCase() === "/model";
135
+ }
136
+
137
+ function isEffortCommand(value) {
138
+ return String(value || "").trim().toLowerCase().startsWith("/effort");
139
+ }
140
+
141
+ function effortArgument(value) {
142
+ return String(value || "").trim().replace(/^\/effort\s*/i, "").trim();
143
+ }
144
+
145
+ function isBareThemeCommand(value) {
146
+ return String(value || "").trim().toLowerCase() === "/theme";
147
+ }
148
+
149
+ function isLocalCommand(value, name) {
150
+ return String(value || "").trim().toLowerCase() === `/${name}`;
151
+ }
152
+
153
+ function isBareSessionsCommand(value) {
154
+ return String(value || "").trim().toLowerCase() === "/sessions";
155
+ }
156
+
157
+ function isCompactCommand(value) {
158
+ return String(value || "").trim().toLowerCase() === "/compact";
159
+ }
@@ -1,22 +1,22 @@
1
- export function createCompactContextState() {
2
- let awaitingPostCompactContext = false;
3
- return {
4
- handleContextBuilt(event = {}) {
5
- const decisions = event.decisions && typeof event.decisions === "object" ? event.decisions : {};
6
- if (awaitingPostCompactContext) {
7
- awaitingPostCompactContext = false;
8
- return true;
9
- }
10
- if (decisions.auto_compact_token_limit_reached || decisions.compact_required) {
11
- awaitingPostCompactContext = true;
12
- }
13
- return false;
14
- },
15
- clear() {
16
- awaitingPostCompactContext = false;
17
- },
18
- pending() {
19
- return awaitingPostCompactContext;
20
- },
21
- };
22
- }
1
+ export function createCompactContextState() {
2
+ let awaitingPostCompactContext = false;
3
+ return {
4
+ handleContextBuilt(event = {}) {
5
+ const decisions = event.decisions && typeof event.decisions === "object" ? event.decisions : {};
6
+ if (awaitingPostCompactContext) {
7
+ awaitingPostCompactContext = false;
8
+ return true;
9
+ }
10
+ if (decisions.auto_compact_token_limit_reached || decisions.compact_required) {
11
+ awaitingPostCompactContext = true;
12
+ }
13
+ return false;
14
+ },
15
+ clear() {
16
+ awaitingPostCompactContext = false;
17
+ },
18
+ pending() {
19
+ return awaitingPostCompactContext;
20
+ },
21
+ };
22
+ }
@@ -0,0 +1,169 @@
1
+ import { wrapTextWithAnsi } from "../text-width.js";
2
+ import {
3
+ codeOpenLabel,
4
+ dim,
5
+ isPlainLine,
6
+ isTableLine,
7
+ parseTableRow,
8
+ renderInline,
9
+ renderMarkdownishLine,
10
+ styled,
11
+ } from "../markdown-lines.js";
12
+
13
+ const CONTENT_PREFIX = " ";
14
+
15
+ export class AssistantMessage {
16
+ constructor(options = {}) {
17
+ this.color = Boolean(options.color);
18
+ this.finalized = [];
19
+ this.pending = "";
20
+ this.inCodeBlock = false;
21
+ this.cacheWidth = -1;
22
+ this.cacheItems = [];
23
+ this.cacheLines = null;
24
+ }
25
+
26
+ append(delta) {
27
+ this.pending += String(delta || "");
28
+ for (;;) {
29
+ const newlineIndex = this.pending.indexOf("\n");
30
+ if (newlineIndex === -1) {
31
+ break;
32
+ }
33
+ const line = this.pending.slice(0, newlineIndex);
34
+ this.pending = this.pending.slice(newlineIndex + 1);
35
+ this.classifyFinalize(line);
36
+ }
37
+ this.cacheLines = null;
38
+ }
39
+
40
+ finish() {
41
+ if (this.pending) {
42
+ const line = this.pending;
43
+ this.pending = "";
44
+ this.classifyFinalize(line);
45
+ }
46
+ this.cacheLines = null;
47
+ }
48
+
49
+ get isEmpty() {
50
+ return !this.finalized.length && !this.pending;
51
+ }
52
+
53
+ classifyFinalize(line) {
54
+ if (isTableLine(line, this.inCodeBlock)) {
55
+ this.finalizeTable(line);
56
+ return;
57
+ }
58
+ if (line.trim().startsWith("```")) {
59
+ this.finalizeCodeFence(line);
60
+ return;
61
+ }
62
+ if (this.inCodeBlock) {
63
+ this.pushItem({ kind: "code", raw: line });
64
+ return;
65
+ }
66
+ if (isPlainLine(line)) {
67
+ if (line) {
68
+ this.pushItem({ kind: "plain", raw: line });
69
+ } else {
70
+ this.finalized.push("");
71
+ }
72
+ return;
73
+ }
74
+ this.pushItem({ kind: "markdown", raw: line });
75
+ }
76
+
77
+ finalizeTable(line) {
78
+ const cells = parseTableRow(line);
79
+ if (!cells.length || cells.every((cell) => /^:?-{3,}:?$/.test(cell.trim()))) {
80
+ return;
81
+ }
82
+ this.pushItem({ kind: "table", raw: line });
83
+ }
84
+
85
+ finalizeCodeFence(line) {
86
+ const opening = !this.inCodeBlock;
87
+ this.inCodeBlock = opening;
88
+ const label = opening ? line.trim().slice(3).trim().slice(0, 32) : "";
89
+ this.pushItem({ kind: "fence", label });
90
+ }
91
+
92
+ // Items keep the raw source; styling happens per render so theme switches
93
+ // recolor history without rebuilding blocks.
94
+ pushItem(item) {
95
+ this.finalized.push(item);
96
+ }
97
+
98
+ styleItem(item) {
99
+ switch (item.kind) {
100
+ case "table": {
101
+ const cells = parseTableRow(item.raw);
102
+ const rendered = cells.map((cell, index) =>
103
+ renderInline(cell, this.color, index === 0 ? "tableHeader" : "")
104
+ );
105
+ return rendered.join(dim(" | ", this.color));
106
+ }
107
+ case "fence":
108
+ return dim(item.label ? codeOpenLabel(item.label) : "└ end", this.color);
109
+ case "code":
110
+ return styled(item.raw, this.color, "codeBlock");
111
+ case "plain":
112
+ return item.raw;
113
+ default:
114
+ return renderMarkdownishLine(item.raw, this.color);
115
+ }
116
+ }
117
+
118
+ render(width) {
119
+ if (this.cacheLines && this.cacheWidth === width && !this.pending) {
120
+ return this.cacheLines;
121
+ }
122
+ if (this.cacheWidth !== width) {
123
+ this.cacheItems.length = 0;
124
+ this.cacheWidth = width;
125
+ }
126
+ while (this.cacheItems.length < this.finalized.length) {
127
+ const item = this.finalized[this.cacheItems.length];
128
+ const logical = typeof item === "string" ? item : this.styleItem(item);
129
+ this.cacheItems.push(this.wrapLogical(logical, width));
130
+ }
131
+ const lines = [];
132
+ for (const item of this.cacheItems) {
133
+ lines.push(...item);
134
+ }
135
+ if (this.pending) {
136
+ lines.push(...this.wrapLogical(previewPending(this.pending, this.inCodeBlock, this.color), width));
137
+ }
138
+ this.cacheLines = lines;
139
+ return lines;
140
+ }
141
+
142
+ wrapLogical(logical, width) {
143
+ if (!logical) {
144
+ return [""];
145
+ }
146
+ const contentWidth = Math.max(1, width - CONTENT_PREFIX.length);
147
+ return wrapTextWithAnsi(logical, contentWidth, contentWidth)
148
+ .map((line) => `${CONTENT_PREFIX}${line}`);
149
+ }
150
+
151
+ invalidate() {
152
+ this.cacheWidth = -1;
153
+ this.cacheItems.length = 0;
154
+ this.cacheLines = null;
155
+ }
156
+ }
157
+
158
+ function previewPending(text, inCodeBlock, color) {
159
+ if (inCodeBlock) {
160
+ return text;
161
+ }
162
+ if (text.trim().startsWith("```")) {
163
+ return "";
164
+ }
165
+ if (isPlainLine(text)) {
166
+ return text;
167
+ }
168
+ return renderMarkdownishLine(text, color);
169
+ }
@@ -0,0 +1,25 @@
1
+ import { prepareComposerFrame } from "../composer-terminal.js";
2
+ import { insertCursorMarker } from "../tui/cursor.js";
3
+
4
+ export class ComposerArea {
5
+ constructor(getFrame) {
6
+ this.getFrame = getFrame;
7
+ }
8
+
9
+ render(width) {
10
+ const params = this.getFrame(width);
11
+ if (!params) {
12
+ return [];
13
+ }
14
+ const frame = prepareComposerFrame(params, width);
15
+ const lines = frame.lines.slice();
16
+ if (params.showCaret === false) {
17
+ return lines;
18
+ }
19
+ const cursorRow = Math.min(frame.cursorRow, lines.length - 1);
20
+ if (cursorRow >= 0 && lines.length) {
21
+ lines[cursorRow] = insertCursorMarker(lines[cursorRow] ?? "", frame.cursorColumn);
22
+ }
23
+ return lines;
24
+ }
25
+ }
@@ -0,0 +1,20 @@
1
+ export class DynamicBlock {
2
+ constructor(build) {
3
+ this.build = build;
4
+ this.cacheWidth = -1;
5
+ this.cacheLines = null;
6
+ }
7
+
8
+ render(width) {
9
+ if (this.cacheWidth !== width || this.cacheLines === null) {
10
+ const built = this.build(width);
11
+ this.cacheLines = Array.isArray(built) ? built : [];
12
+ this.cacheWidth = width;
13
+ }
14
+ return this.cacheLines;
15
+ }
16
+
17
+ invalidate() {
18
+ this.cacheLines = null;
19
+ }
20
+ }
@@ -0,0 +1,35 @@
1
+ export class MonitorStack {
2
+ constructor({ composer, monitor, rows }) {
3
+ this.composer = composer;
4
+ this.monitor = monitor;
5
+ this.rows = rows;
6
+ }
7
+
8
+ render(width) {
9
+ const composerLines = this.composer.render(width);
10
+ const monitorFrame = typeof this.monitor?.frame === "function" && this.monitor.isMonitoring()
11
+ ? this.monitor.frame(width)
12
+ : null;
13
+ const monitorLines = Array.isArray(monitorFrame?.lines) ? monitorFrame.lines : [];
14
+ if (!monitorLines.length) {
15
+ return composerLines;
16
+ }
17
+ const totalHeight = Number(this.rows?.()) || 24;
18
+ if (composerLines.length + monitorLines.length <= totalHeight) {
19
+ return [...composerLines, ...monitorLines];
20
+ }
21
+ const available = Math.max(1, totalHeight - Math.min(composerLines.length, Math.max(1, totalHeight - 1)));
22
+ const focusRow = clampIndex(monitorFrame.focusRow, monitorLines.length);
23
+ let start = Math.min(Math.max(0, focusRow - available + 1), monitorLines.length - available);
24
+ start = Math.max(0, start);
25
+ return [...composerLines, ...monitorLines.slice(start, start + available)];
26
+ }
27
+ }
28
+
29
+ function clampIndex(value, length) {
30
+ const index = Math.floor(Number(value) || 0);
31
+ if (!Number.isFinite(index)) {
32
+ return 0;
33
+ }
34
+ return Math.max(0, Math.min(length - 1, index));
35
+ }
@@ -0,0 +1,47 @@
1
+ import { wrapTextWithAnsi } from "../text-width.js";
2
+
3
+ export class TextBlock {
4
+ constructor(text, options = {}) {
5
+ this.text = String(text ?? "");
6
+ this.leading = Boolean(options.leading);
7
+ this.cacheWidth = 0;
8
+ this.cacheLines = null;
9
+ }
10
+
11
+ setText(text) {
12
+ const next = String(text ?? "");
13
+ if (next === this.text) {
14
+ return;
15
+ }
16
+ this.text = next;
17
+ this.cacheLines = null;
18
+ }
19
+
20
+ render(width) {
21
+ if (this.cacheLines && this.cacheWidth === width) {
22
+ return this.cacheLines;
23
+ }
24
+ const source = this.text.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
25
+ const lines = [];
26
+ if (this.leading && source.trim()) {
27
+ lines.push("");
28
+ }
29
+ for (const logicalLine of source.split("\n")) {
30
+ if (!logicalLine) {
31
+ lines.push("");
32
+ continue;
33
+ }
34
+ lines.push(...wrapTextWithAnsi(logicalLine, width));
35
+ }
36
+ while (lines.length > 1 && lines.at(-1) === "") {
37
+ lines.pop();
38
+ }
39
+ this.cacheWidth = width;
40
+ this.cacheLines = lines;
41
+ return lines;
42
+ }
43
+
44
+ invalidate() {
45
+ this.cacheLines = null;
46
+ }
47
+ }