@rind-ai/cli 0.4.1 → 0.6.1

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 +1295 -1037
  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 +16 -27
  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 +680 -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
package/bin/rind.js CHANGED
@@ -1,5 +1,5 @@
1
- #!/usr/bin/env node
2
-
3
- import { runFrontendCli } from "../lib/frontend-cli.js";
4
-
5
- await runFrontendCli(process.argv.slice(2));
1
+ #!/usr/bin/env node
2
+
3
+ import { runFrontendCli } from "../lib/frontend-cli.js";
4
+
5
+ await runFrontendCli(process.argv.slice(2));
@@ -1,265 +1,179 @@
1
- import { graphemes, textWidth } from "./text-width.js";
2
-
3
- const INLINE_TOKEN_RE = /(\[[^\]]+\]\([^)]+\)|`[^`]+`|\*\*[^*]+\*\*)/g;
4
- const PLAIN_TEXT_RE = /[`*#>|\[]/;
5
- const CONTENT_PREFIX = " ";
6
- const ANSI_SEQUENCE = /\x1b\[[0-?]*[ -/]*[@-~]/g;
7
-
8
- export class AssistantRenderer {
9
- constructor(write, options = {}) {
10
- this.write = write;
11
- this.color = options.color ?? (Boolean(process.stdout.isTTY) && !process.env.NO_COLOR);
12
- this.pending = "";
13
- this.inCodeBlock = false;
14
- this.lineOpen = false;
15
- this.atLineStart = true;
16
- this.visibleColumn = 0;
17
- this.columns = options.columns;
18
- }
19
-
20
- append(text) {
21
- this.pending += String(text || "");
22
- while (true) {
23
- const newlineIndex = this.pending.indexOf("\n");
24
- if (newlineIndex === -1) {
25
- break;
26
- }
27
- const line = this.pending.slice(0, newlineIndex);
28
- this.pending = this.pending.slice(newlineIndex + 1);
29
- this.renderLine(line, true);
30
- }
31
- this.flushPlainPending();
32
- }
33
-
34
- finish() {
35
- if (this.pending) {
36
- this.renderLine(this.pending, false);
37
- this.pending = "";
38
- }
39
- if (this.lineOpen) {
40
- this.writeText("\n");
41
- this.lineOpen = false;
42
- }
43
- }
44
-
45
- flushPlainPending() {
46
- if (!this.pending || this.inCodeBlock || !isPlainLine(this.pending)) {
47
- return;
48
- }
49
- this.writePlain(this.pending, false);
50
- this.pending = "";
51
- }
52
-
53
- renderLine(line, newline) {
54
- if (isTableLine(line, this.inCodeBlock)) {
55
- this.renderTableLine(line, newline);
56
- return;
57
- }
58
- if (line.trim().startsWith("```")) {
59
- this.renderCodeFence(line, newline);
60
- return;
61
- }
62
- if (this.inCodeBlock) {
63
- this.writeStyled(styled(line, this.color, "codeBlock"), newline);
64
- return;
65
- }
66
- if (isPlainLine(line)) {
67
- this.writePlain(line, newline);
68
- return;
69
- }
70
- this.writeStyled(renderMarkdownishLine(line, this.color), newline);
71
- }
72
-
73
- renderTableLine(line, newline) {
74
- const cells = parseTableRow(line);
75
- if (!cells.length || cells.every((cell) => /^:?-{3,}:?$/.test(cell.trim()))) {
76
- return;
77
- }
78
- const rendered = cells.map((cell, index) =>
79
- renderInline(cell, this.color, index === 0 ? "tableHeader" : "")
80
- );
81
- this.writeStyled(rendered.join(dim(" | ", this.color)), newline);
82
- }
83
-
84
- renderCodeFence(line, newline) {
85
- const opening = !this.inCodeBlock;
86
- this.inCodeBlock = opening;
87
- const label = opening ? line.trim().slice(3).trim().slice(0, 32) : "";
88
- this.writeStyled(dim(opening ? codeOpenLabel(label) : "└ end", this.color), newline);
89
- }
90
-
91
- writePlain(text, newline) {
92
- this.writeText(text + (newline ? "\n" : ""));
93
- this.lineOpen = Boolean(text) && !newline;
94
- }
95
-
96
- writeStyled(text, newline) {
97
- this.writeText(text + (newline ? "\n" : ""));
98
- this.lineOpen = Boolean(text) && !newline;
99
- }
100
-
101
- writeText(text) {
102
- const parts = String(text || "").split(/(\r\n|\r|\n)/);
103
- const maxWidth = Math.max(1, Math.floor(Number(this.columns ?? process.stdout.columns ?? 80) || 80));
104
- const prefixWidth = textWidth(CONTENT_PREFIX);
105
- let output = "";
106
- for (const part of parts) {
107
- if (!part) {
108
- continue;
109
- }
110
- if (part === "\r\n" || part === "\r" || part === "\n") {
111
- if (this.atLineStart) {
112
- output += CONTENT_PREFIX;
113
- }
114
- output += part;
115
- this.atLineStart = true;
116
- this.visibleColumn = 0;
117
- continue;
118
- }
119
- for (const segment of ansiSegments(part)) {
120
- if (segment.ansi) {
121
- if (this.atLineStart) {
122
- output += CONTENT_PREFIX;
123
- this.atLineStart = false;
124
- this.visibleColumn = prefixWidth;
125
- }
126
- output += segment.text;
127
- continue;
128
- }
129
- for (const grapheme of graphemes(segment.text)) {
130
- const segmentWidth = textWidth(grapheme);
131
- if (this.atLineStart) {
132
- output += CONTENT_PREFIX;
133
- this.atLineStart = false;
134
- this.visibleColumn = prefixWidth;
135
- }
136
- if (
137
- segmentWidth > 0
138
- && this.visibleColumn > prefixWidth
139
- && this.visibleColumn + segmentWidth > maxWidth
140
- ) {
141
- output += `\n${CONTENT_PREFIX}`;
142
- this.visibleColumn = prefixWidth;
143
- }
144
- output += grapheme;
145
- this.visibleColumn += segmentWidth;
146
- }
147
- }
148
- }
149
- if (output) {
150
- this.write(output);
151
- }
152
- }
153
- }
154
-
155
- function ansiSegments(value) {
156
- const segments = [];
157
- let position = 0;
158
- for (const match of value.matchAll(ANSI_SEQUENCE)) {
159
- if (match.index > position) {
160
- segments.push({ text: value.slice(position, match.index), ansi: false });
161
- }
162
- segments.push({ text: match[0], ansi: true });
163
- position = match.index + match[0].length;
164
- }
165
- if (position < value.length) {
166
- segments.push({ text: value.slice(position), ansi: false });
167
- }
168
- return segments;
169
- }
170
-
171
- function renderMarkdownishLine(line, color) {
172
- const heading = line.match(/^(#{1,6})\s+(.+?)\s*$/);
173
- if (heading) {
174
- return renderInline(heading[2], color, "heading");
175
- }
176
-
177
- const quote = line.match(/^(\s*)>\s?(.*)$/);
178
- if (quote) {
179
- return `${quote[1]}${dim("│ ", color)}${renderInline(quote[2], color)}`;
180
- }
181
-
182
- const list = line.match(/^(\s*)([-*+]|\d+\.)\s+(.*)$/);
183
- if (list) {
184
- const marker = /^\d+\.$/.test(list[2]) ? list[2] : "•";
185
- return `${list[1]}${dim(`${marker} `, color)}${renderInline(list[3], color)}`;
186
- }
187
-
188
- return renderInline(line, color);
189
- }
190
-
191
- function renderInline(text, color, baseStyle = "") {
192
- const source = String(text || "");
193
- let output = "";
194
- let index = 0;
195
- for (const match of source.matchAll(INLINE_TOKEN_RE)) {
196
- output += styled(source.slice(index, match.index), color, baseStyle);
197
- output += renderInlineToken(match[0], color, baseStyle);
198
- index = match.index + match[0].length;
199
- }
200
- return output + styled(source.slice(index), color, baseStyle);
201
- }
202
-
203
- function renderInlineToken(token, color, baseStyle) {
204
- const link = token.match(/^\[([^\]]+)\]\(([^)]+)\)$/);
205
- if (link) {
206
- return `${renderInline(link[1], color, baseStyle)} ${dim(`(${link[2]})`, color)}`;
207
- }
208
- if (token.startsWith("`") && token.endsWith("`")) {
209
- return styled(token.slice(1, -1), color, "inlineCode");
210
- }
211
- if (token.startsWith("**") && token.endsWith("**")) {
212
- return styled(token.slice(2, -2), color, baseStyle || "emphasis");
213
- }
214
- return token;
215
- }
216
-
217
- function isPlainLine(line) {
218
- if (!line) {
219
- return true;
220
- }
221
- if (PLAIN_TEXT_RE.test(line)) {
222
- return false;
223
- }
224
- const stripped = line.trimStart();
225
- return !stripped.match(/^([-*+]|\d+\.)\s+/);
226
- }
227
-
228
- function isTableLine(line, inCodeBlock) {
229
- const stripped = line.trim();
230
- return !inCodeBlock && stripped.includes("|") && stripped.split("|").length > 2;
231
- }
232
-
233
- function parseTableRow(line) {
234
- let stripped = line.trim();
235
- if (stripped.startsWith("|")) {
236
- stripped = stripped.slice(1);
237
- }
238
- if (stripped.endsWith("|")) {
239
- stripped = stripped.slice(0, -1);
240
- }
241
- return stripped.split("|").map((cell) => cell.trim());
242
- }
243
-
244
- function codeOpenLabel(label) {
245
- return label ? `┌ code ${label}` : "┌ code";
246
- }
247
-
248
- function styled(text, color, style) {
249
- if (!text || !color || !style) {
250
- return text;
251
- }
252
- const codes = {
253
- codeBlock: "38;5;110",
254
- emphasis: "1;38;5;221",
255
- heading: "1;38;5;81",
256
- inlineCode: "38;5;215",
257
- tableHeader: "1;38;5;81",
258
- };
259
- const code = codes[style] || codes.emphasis;
260
- return `\x1b[${code}m${text}\x1b[0m`;
261
- }
262
-
263
- function dim(text, color) {
264
- return color ? `\x1b[2m${text}\x1b[0m` : text;
265
- }
1
+ import { graphemes, textWidth } 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
+ const ANSI_SEQUENCE = /\x1b\[[0-?]*[ -/]*[@-~]/g;
15
+
16
+ export { CONTENT_PREFIX };
17
+
18
+ export class AssistantRenderer {
19
+ constructor(write, options = {}) {
20
+ this.write = write;
21
+ this.color = options.color ?? (Boolean(process.stdout.isTTY) && !process.env.NO_COLOR);
22
+ this.pending = "";
23
+ this.inCodeBlock = false;
24
+ this.lineOpen = false;
25
+ this.atLineStart = true;
26
+ this.visibleColumn = 0;
27
+ this.columns = options.columns;
28
+ }
29
+
30
+ append(text) {
31
+ this.pending += String(text || "");
32
+ while (true) {
33
+ const newlineIndex = this.pending.indexOf("\n");
34
+ if (newlineIndex === -1) {
35
+ break;
36
+ }
37
+ const line = this.pending.slice(0, newlineIndex);
38
+ this.pending = this.pending.slice(newlineIndex + 1);
39
+ this.renderLine(line, true);
40
+ }
41
+ this.flushPlainPending();
42
+ }
43
+
44
+ finish() {
45
+ if (this.pending) {
46
+ this.renderLine(this.pending, false);
47
+ this.pending = "";
48
+ }
49
+ if (this.lineOpen) {
50
+ this.writeText("\n");
51
+ this.lineOpen = false;
52
+ }
53
+ }
54
+
55
+ flushPlainPending() {
56
+ if (!this.pending || this.inCodeBlock || !isPlainLine(this.pending)) {
57
+ return;
58
+ }
59
+ this.writePlain(this.pending, false);
60
+ this.pending = "";
61
+ }
62
+
63
+ renderLine(line, newline) {
64
+ if (isTableLine(line, this.inCodeBlock)) {
65
+ this.renderTableLine(line, newline);
66
+ return;
67
+ }
68
+ if (line.trim().startsWith("```")) {
69
+ this.renderCodeFence(line, newline);
70
+ return;
71
+ }
72
+ if (this.inCodeBlock) {
73
+ this.writeStyled(styled(line, this.color, "codeBlock"), newline);
74
+ return;
75
+ }
76
+ if (isPlainLine(line)) {
77
+ this.writePlain(line, newline);
78
+ return;
79
+ }
80
+ this.writeStyled(renderMarkdownishLine(line, this.color), newline);
81
+ }
82
+
83
+ renderTableLine(line, newline) {
84
+ const cells = parseTableRow(line);
85
+ if (!cells.length || cells.every((cell) => /^:?-{3,}:?$/.test(cell.trim()))) {
86
+ return;
87
+ }
88
+ const rendered = cells.map((cell, index) =>
89
+ renderInline(cell, this.color, index === 0 ? "tableHeader" : "")
90
+ );
91
+ this.writeStyled(rendered.join(dim(" | ", this.color)), newline);
92
+ }
93
+
94
+ renderCodeFence(line, newline) {
95
+ const opening = !this.inCodeBlock;
96
+ this.inCodeBlock = opening;
97
+ const label = opening ? line.trim().slice(3).trim().slice(0, 32) : "";
98
+ this.writeStyled(dim(opening ? codeOpenLabel(label) : "└ end", this.color), newline);
99
+ }
100
+
101
+ writePlain(text, newline) {
102
+ this.writeText(text + (newline ? "\n" : ""));
103
+ this.lineOpen = Boolean(text) && !newline;
104
+ }
105
+
106
+ writeStyled(text, newline) {
107
+ this.writeText(text + (newline ? "\n" : ""));
108
+ this.lineOpen = Boolean(text) && !newline;
109
+ }
110
+
111
+ writeText(text) {
112
+ const parts = String(text || "").split(/(\r\n|\r|\n)/);
113
+ const maxWidth = Math.max(1, Math.floor(Number(this.columns ?? process.stdout.columns ?? 80) || 80));
114
+ const prefixWidth = textWidth(CONTENT_PREFIX);
115
+ let output = "";
116
+ for (const part of parts) {
117
+ if (!part) {
118
+ continue;
119
+ }
120
+ if (part === "\r\n" || part === "\r" || part === "\n") {
121
+ if (this.atLineStart) {
122
+ output += CONTENT_PREFIX;
123
+ }
124
+ output += part;
125
+ this.atLineStart = true;
126
+ this.visibleColumn = 0;
127
+ continue;
128
+ }
129
+ for (const segment of ansiSegments(part)) {
130
+ if (segment.ansi) {
131
+ if (this.atLineStart) {
132
+ output += CONTENT_PREFIX;
133
+ this.atLineStart = false;
134
+ this.visibleColumn = prefixWidth;
135
+ }
136
+ output += segment.text;
137
+ continue;
138
+ }
139
+ for (const grapheme of graphemes(segment.text)) {
140
+ const segmentWidth = textWidth(grapheme);
141
+ if (this.atLineStart) {
142
+ output += CONTENT_PREFIX;
143
+ this.atLineStart = false;
144
+ this.visibleColumn = prefixWidth;
145
+ }
146
+ if (
147
+ segmentWidth > 0
148
+ && this.visibleColumn > prefixWidth
149
+ && this.visibleColumn + segmentWidth > maxWidth
150
+ ) {
151
+ output += `\n${CONTENT_PREFIX}`;
152
+ this.visibleColumn = prefixWidth;
153
+ }
154
+ output += grapheme;
155
+ this.visibleColumn += segmentWidth;
156
+ }
157
+ }
158
+ }
159
+ if (output) {
160
+ this.write(output);
161
+ }
162
+ }
163
+ }
164
+
165
+ function ansiSegments(value) {
166
+ const segments = [];
167
+ let position = 0;
168
+ for (const match of value.matchAll(ANSI_SEQUENCE)) {
169
+ if (match.index > position) {
170
+ segments.push({ text: value.slice(position, match.index), ansi: false });
171
+ }
172
+ segments.push({ text: match[0], ansi: true });
173
+ position = match.index + match[0].length;
174
+ }
175
+ if (position < value.length) {
176
+ segments.push({ text: value.slice(position), ansi: false });
177
+ }
178
+ return segments;
179
+ }
@@ -1,46 +1,46 @@
1
- export function createChoiceMenuState(options, recommended = "") {
2
- const items = normalizeOptions(options);
3
- let selected = items.indexOf(recommended);
4
- if (selected < 0) {
5
- selected = 0;
6
- }
7
- return {
8
- options() {
9
- return items;
10
- },
11
- selectedIndex() {
12
- return selected;
13
- },
14
- selectedOption() {
15
- return items[selected] || "";
16
- },
17
- handleKey(key = {}) {
18
- if (!items.length) {
19
- return false;
20
- }
21
- if (key.name === "up" || key.text === "k") {
22
- selected = selected <= 0 ? items.length - 1 : selected - 1;
23
- return true;
24
- }
25
- if (key.name === "down" || key.text === "j") {
26
- selected = selected >= items.length - 1 ? 0 : selected + 1;
27
- return true;
28
- }
29
- return false;
30
- },
31
- };
32
- }
33
-
34
- function normalizeOptions(options) {
35
- const seen = new Set();
36
- const items = [];
37
- for (const option of Array.isArray(options) ? options : []) {
38
- const value = String(option || "").trim();
39
- if (!value || seen.has(value)) {
40
- continue;
41
- }
42
- seen.add(value);
43
- items.push(value);
44
- }
45
- return items;
46
- }
1
+ export function createChoiceMenuState(options, selectedValue = "") {
2
+ const items = normalizeOptions(options);
3
+ let selected = items.indexOf(selectedValue);
4
+ if (selected < 0) {
5
+ selected = 0;
6
+ }
7
+ return {
8
+ options() {
9
+ return items;
10
+ },
11
+ selectedIndex() {
12
+ return selected;
13
+ },
14
+ selectedOption() {
15
+ return items[selected] || "";
16
+ },
17
+ handleKey(key = {}) {
18
+ if (!items.length) {
19
+ return false;
20
+ }
21
+ if (key.name === "up" || key.text === "k") {
22
+ selected = selected <= 0 ? items.length - 1 : selected - 1;
23
+ return true;
24
+ }
25
+ if (key.name === "down" || key.text === "j") {
26
+ selected = selected >= items.length - 1 ? 0 : selected + 1;
27
+ return true;
28
+ }
29
+ return false;
30
+ },
31
+ };
32
+ }
33
+
34
+ function normalizeOptions(options) {
35
+ const seen = new Set();
36
+ const items = [];
37
+ for (const option of Array.isArray(options) ? options : []) {
38
+ const value = String(option || "").trim();
39
+ if (!value || seen.has(value)) {
40
+ continue;
41
+ }
42
+ seen.add(value);
43
+ items.push(value);
44
+ }
45
+ return items;
46
+ }