@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 CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- import { runFrontendCli } from "../lib/frontend-cli.js";
3
+ import { runFrontendCliApp } from "../lib/frontend-cli-implementation.js";
4
4
 
5
- await runFrontendCli(process.argv.slice(2));
5
+ await runFrontendCliApp(process.argv.slice(2));
@@ -1,17 +1,18 @@
1
+ import { ANSI_SEQUENCE } from "./text-width.js";
1
2
  import { graphemes, textWidth } from "./text-width.js";
2
3
  import {
3
4
  codeOpenLabel,
5
+ consumeTableLine,
6
+ createTableState,
4
7
  dim,
8
+ finishTableState,
5
9
  isPlainLine,
6
- isTableLine,
7
- parseTableRow,
8
- renderInline,
10
+ renderTableBlock,
9
11
  renderMarkdownishLine,
10
12
  styled,
11
13
  } from "./markdown-lines.js";
12
14
 
13
15
  const CONTENT_PREFIX = " ";
14
- const ANSI_SEQUENCE = /\x1b\[[0-?]*[ -/]*[@-~]/g;
15
16
 
16
17
  export { CONTENT_PREFIX };
17
18
 
@@ -21,6 +22,7 @@ export class AssistantRenderer {
21
22
  this.color = options.color ?? (Boolean(process.stdout.isTTY) && !process.env.NO_COLOR);
22
23
  this.pending = "";
23
24
  this.inCodeBlock = false;
25
+ this.tableState = createTableState();
24
26
  this.lineOpen = false;
25
27
  this.atLineStart = true;
26
28
  this.visibleColumn = 0;
@@ -46,6 +48,12 @@ export class AssistantRenderer {
46
48
  this.renderLine(this.pending, false);
47
49
  this.pending = "";
48
50
  }
51
+ const table = finishTableState(this.tableState);
52
+ if (table?.type === "flush") {
53
+ this.writeTable(table.rows);
54
+ } else if (table?.type === "flush_candidate") {
55
+ table.lines.forEach((line) => this.renderNormalLine(line, true));
56
+ }
49
57
  if (this.lineOpen) {
50
58
  this.writeText("\n");
51
59
  this.lineOpen = false;
@@ -53,7 +61,7 @@ export class AssistantRenderer {
53
61
  }
54
62
 
55
63
  flushPlainPending() {
56
- if (!this.pending || this.inCodeBlock || !isPlainLine(this.pending)) {
64
+ if (!this.pending || this.inCodeBlock || this.tableState.candidate.length || this.tableState.rows || !isPlainLine(this.pending)) {
57
65
  return;
58
66
  }
59
67
  this.writePlain(this.pending, false);
@@ -61,10 +69,29 @@ export class AssistantRenderer {
61
69
  }
62
70
 
63
71
  renderLine(line, newline) {
64
- if (isTableLine(line, this.inCodeBlock)) {
65
- this.renderTableLine(line, newline);
72
+ const result = consumeTableLine(this.tableState, line, this.inCodeBlock);
73
+ if (result.type === "hold" || result.type === "append") {
74
+ return;
75
+ }
76
+ if (result.type === "start") {
77
+ result.lines?.forEach((candidate) => this.renderNormalLine(candidate, true));
78
+ return;
79
+ }
80
+ if (result.type === "flush") {
81
+ this.writeTable(result.rows);
82
+ if (result.line === undefined) return;
83
+ this.renderNormalLine(result.line, newline);
84
+ return;
85
+ }
86
+ if (result.type === "flush_candidate") {
87
+ result.lines.forEach((candidate) => this.renderNormalLine(candidate, true));
88
+ if (result.line !== undefined) this.renderNormalLine(result.line, newline);
66
89
  return;
67
90
  }
91
+ this.renderNormalLine(line, newline);
92
+ }
93
+
94
+ renderNormalLine(line, newline) {
68
95
  if (line.trim().startsWith("```")) {
69
96
  this.renderCodeFence(line, newline);
70
97
  return;
@@ -80,15 +107,15 @@ export class AssistantRenderer {
80
107
  this.writeStyled(renderMarkdownishLine(line, this.color), newline);
81
108
  }
82
109
 
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" : "")
110
+ writeTable(rows) {
111
+ const table = renderTableBlock(
112
+ rows,
113
+ this.color,
114
+ Math.max(1, Math.floor(Number(this.columns ?? process.stdout.columns ?? 80) || 80) - CONTENT_PREFIX.length),
90
115
  );
91
- this.writeStyled(rendered.join(dim(" | ", this.color)), newline);
116
+ if (table) {
117
+ this.writeStyled(table, true);
118
+ }
92
119
  }
93
120
 
94
121
  renderCodeFence(line, newline) {
@@ -18,14 +18,17 @@ export function createCliInputActions({
18
18
  request,
19
19
  output,
20
20
  getTurnController,
21
+ getCommandController,
21
22
  getTaskMonitor,
22
23
  getLineInput,
23
24
  pausePrompt,
24
25
  resumePrompt,
25
26
  handleSigint,
27
+ promptHistory = [],
28
+ onPromptHistory = () => {},
26
29
  }) {
27
30
  let cancelActiveInput = null;
28
- const promptEditor = createLineEditor();
31
+ const promptEditor = createLineEditor("", { history: promptHistory });
29
32
 
30
33
  function restoreInputText(text) {
31
34
  state.input.prefill = String(text || "");
@@ -51,7 +54,7 @@ export function createCliInputActions({
51
54
  return;
52
55
  }
53
56
  state.input.pending.splice(index, 1);
54
- output.suspendPrompt(() => output.writeUserInput(input));
57
+ output.writeUserInput(input);
55
58
  }
56
59
 
57
60
  async function retrievePendingInput(mode, session) {
@@ -90,6 +93,28 @@ export function createCliInputActions({
90
93
  }
91
94
  }
92
95
 
96
+ async function promotePendingFollowUp(session) {
97
+ const entry = [...state.input.pending].reverse().find((item) => item.mode === "follow_up");
98
+ if (!entry || state.input.retrievingModes.has("follow_up")) {
99
+ return;
100
+ }
101
+ state.input.retrievingModes.add("follow_up");
102
+ try {
103
+ const result = await request(runtimeMethods.sessionPromoteFollowUp, { input_id: entry.inputId });
104
+ if (result?.accepted !== true || result?.mode !== "steering" || result?.input_id !== entry.inputId) {
105
+ throw new Error("Runtime did not confirm the follow-up promotion.");
106
+ }
107
+ entry.mode = "steering";
108
+ output.redraw();
109
+ } catch (error) {
110
+ if (state.runtime.status !== "closing") {
111
+ output.writeError(`${error instanceof Error ? error.message : String(error)}\n`);
112
+ }
113
+ } finally {
114
+ state.input.retrievingModes.delete("follow_up");
115
+ }
116
+ }
117
+
93
118
  function clearPendingInputs() {
94
119
  if (!state.input.pending.length) {
95
120
  return;
@@ -178,7 +203,6 @@ export function createCliInputActions({
178
203
 
179
204
  function askTtyInput(prompt, placeholder) {
180
205
  return new Promise((resolve) => {
181
- output.clearAssistantLineForInput();
182
206
  const mode = placeholder && placeholder !== answerPlaceholderText() ? "prompt" : "line";
183
207
  const initialInput = state.input.prefill;
184
208
  const editor = mode === "prompt" ? promptEditor : createLineEditor(initialInput);
@@ -246,6 +270,14 @@ export function createCliInputActions({
246
270
  handleTeamBlueprintInput(session, event);
247
271
  return;
248
272
  }
273
+ if (session.mode === "fork") {
274
+ handleForkInput(session, event);
275
+ return;
276
+ }
277
+ if (session.mode === "context-board") {
278
+ handleContextBoardInput(session, event);
279
+ return;
280
+ }
249
281
  const key = event;
250
282
  const matches = session.menuState ? syncSlashMenu(session) : [];
251
283
  const menuKey = !key.ctrl && !key.alt && !key.shift && ["escape", "up", "down"].includes(key.name);
@@ -265,6 +297,10 @@ export function createCliInputActions({
265
297
  void retrievePendingInput("steering", session);
266
298
  return;
267
299
  }
300
+ if (session.mode === "prompt" && key.alt && !key.ctrl && !key.shift && key.name === "right" && state.input.pending.some((item) => item.mode === "follow_up")) {
301
+ void promotePendingFollowUp(session);
302
+ return;
303
+ }
268
304
  const result = session.editor.handleInput(key);
269
305
  if (result === "submit") {
270
306
  submitTtyInput(session);
@@ -281,7 +317,7 @@ export function createCliInputActions({
281
317
  return;
282
318
  }
283
319
  const session = state.input.session;
284
- if (!session || session.mode === "model" || session.mode === "theme" || session.mode === "sessions") {
320
+ if (!session || session.mode === "model" || session.mode === "theme" || session.mode === "sessions" || session.mode === "fork" || session.mode === "context-board") {
285
321
  return;
286
322
  }
287
323
  if (session.mode === "question" && !session.questionState.isEditing()) {
@@ -307,7 +343,6 @@ export function createCliInputActions({
307
343
 
308
344
  function askThemeMenu() {
309
345
  return new Promise((resolve) => {
310
- output.clearAssistantLineForInput();
311
346
  const themeState = createThemeMenuState();
312
347
  if (!themeState.items().length) {
313
348
  resolve("");
@@ -339,18 +374,29 @@ export function createCliInputActions({
339
374
  if (session.menuState) syncSlashMenu(session);
340
375
  const command = session.menuState?.selectedCommand();
341
376
  const value = command ? `/${command.name}` : session.editor.input();
342
- if (session.mode === "prompt") session.editor.addToHistory(value);
377
+ if (session.mode === "prompt") recordPromptHistory(session.editor, value);
343
378
  completeTtyInput(session, value, session.mode === "prompt" && !state.turn.active, session.mode === "line" ? "\n" : "");
344
379
  }
345
380
 
346
381
  function queueTtyInput(session) {
347
382
  const value = session.editor.input();
348
383
  if (!value.trim()) return;
349
- session.editor.addToHistory(value);
384
+ recordPromptHistory(session.editor, value);
350
385
  completeTtyInput(session, "", false);
351
386
  getTurnController().submitFollowUp(value);
352
387
  }
353
388
 
389
+ function recordPromptHistory(editor, value) {
390
+ const text = String(value || "").trim();
391
+ if (!text || text.startsWith("/")) {
392
+ editor.addToHistory("");
393
+ return;
394
+ }
395
+ if (editor.addToHistory(text)) {
396
+ onPromptHistory(editor.getHistory());
397
+ }
398
+ }
399
+
354
400
  function completeTtyInput(session, value, writeUser, lineText = "", displayValue = value) {
355
401
  if (state.input.session !== session) return;
356
402
  state.input.session = null;
@@ -366,7 +412,6 @@ export function createCliInputActions({
366
412
 
367
413
  function askModelMenu(models, currentModel) {
368
414
  return new Promise((resolve) => {
369
- output.clearAssistantLineForInput();
370
415
  const modelState = createModelMenuState(models, currentModel);
371
416
  if (!modelState.items().length) {
372
417
  resolve("");
@@ -382,7 +427,6 @@ export function createCliInputActions({
382
427
 
383
428
  function askEffortMenu(currentEffort) {
384
429
  return new Promise((resolve) => {
385
- output.clearAssistantLineForInput();
386
430
  const modelState = createModelMenuState(REASONING_EFFORTS.slice(), currentEffort);
387
431
  const session = { mode: "model", inputText: "/effort", modelState, resolve };
388
432
  state.input.session = session;
@@ -406,16 +450,17 @@ export function createCliInputActions({
406
450
 
407
451
  function askQuestionMenu(event) {
408
452
  return new Promise((resolve) => {
409
- output.clearAssistantLineForInput();
410
453
  const options = (Array.isArray(event.options) ? event.options : [])
411
454
  .map((option) => ({ label: String(option?.label || "").trim(), description: String(option?.description || "").trim() }))
412
455
  .filter((option) => option.label);
413
456
  const questionState = createQuestionMenuState(options);
457
+ const editor = createLineEditor();
458
+ editor.setViewportWidth(process.stdout.columns || 80);
414
459
  state.input.session = {
415
460
  mode: "question",
416
461
  question: String(event.question || "Input required"),
417
462
  questionState,
418
- editor: null,
463
+ editor,
419
464
  resolve,
420
465
  };
421
466
  state.input.active = true;
@@ -426,7 +471,6 @@ export function createCliInputActions({
426
471
 
427
472
  function askSessionMenu(options, sessions, currentIndex) {
428
473
  return new Promise((resolve) => {
429
- output.clearAssistantLineForInput();
430
474
  const choiceState = createChoiceMenuState(options, options[currentIndex] || "");
431
475
  const session = { mode: "sessions", inputText: "/sessions", choiceState, sessions, resolve };
432
476
  state.input.session = session;
@@ -438,7 +482,6 @@ export function createCliInputActions({
438
482
 
439
483
  function askTeamBlueprint(blueprints) {
440
484
  return new Promise((resolve) => {
441
- output.clearAssistantLineForInput();
442
485
  const items = (Array.isArray(blueprints) ? blueprints : []).map((item) => ({
443
486
  id: String(item?.id || ""),
444
487
  label: [item?.id, item?.name, item?.description].filter(Boolean).join(" · "),
@@ -459,9 +502,14 @@ export function createCliInputActions({
459
502
  function handleQuestionInput(session, key) {
460
503
  const modified = key.ctrl || key.alt || key.shift;
461
504
  if (session.questionState.isEditing()) {
462
- if (!modified && key.name === "escape") return completeTtyInput(session, "", false);
463
- if (!modified && session.questionState.handleNavigation(key)) {
464
- session.editor = null;
505
+ if (!modified && (key.name === "up" || key.name === "down")) {
506
+ session.questionState.leaveEditing();
507
+ session.editor.setInput("");
508
+ if (session.questionState.handleNavigation(key)) output.redraw();
509
+ return;
510
+ }
511
+ if (!modified && key.name === "escape") {
512
+ session.questionState.leaveEditing();
465
513
  output.redraw();
466
514
  return;
467
515
  }
@@ -477,8 +525,6 @@ export function createCliInputActions({
477
525
  }
478
526
  if (!modified && (key.name === "enter" || key.name === "return" || key.name === "tab")) {
479
527
  if (session.questionState.enterEditing()) {
480
- session.editor = createLineEditor();
481
- session.editor.setViewportWidth(process.stdout.columns || 80);
482
528
  output.redraw();
483
529
  return;
484
530
  }
@@ -488,7 +534,6 @@ export function createCliInputActions({
488
534
  }
489
535
  if (!modified && key.name === "escape") return completeTtyInput(session, "", false);
490
536
  if (!modified && session.questionState.handleNavigation(key)) {
491
- session.editor = null;
492
537
  output.redraw();
493
538
  }
494
539
  }
@@ -513,6 +558,49 @@ export function createCliInputActions({
513
558
  if (!modified && session.choiceState.handleKey(key)) output.redraw();
514
559
  }
515
560
 
561
+ function handleForkInput(session, key) {
562
+ const modified = key.ctrl || key.alt || key.shift;
563
+ if (!modified && (key.name === "enter" || key.name === "return")) {
564
+ completeTtyInput(session, session.items[session.choiceState.selectedIndex()] || null, false);
565
+ return;
566
+ }
567
+ if (!modified && key.name === "escape") return completeTtyInput(session, null, false);
568
+ if (!modified && session.choiceState.handleKey(key)) output.redraw();
569
+ }
570
+
571
+ function askContextBoard(board) {
572
+ return new Promise((resolve) => {
573
+ const session = { mode: "context-board", inputText: "/context", board, pageIndex: 0, resolve };
574
+ state.input.session = session;
575
+ state.input.active = true;
576
+ cancelActiveInput = () => completeTtyInput(session, "", false);
577
+ output.redraw(true);
578
+ });
579
+ }
580
+
581
+ function handleContextBoardInput(session, key) {
582
+ const modified = key.ctrl || key.alt || key.shift;
583
+ if (!modified && key.name === "escape") {
584
+ completeTtyInput(session, "", false);
585
+ return;
586
+ }
587
+ if (!modified && (key.name === "tab" || key.name === "left" || key.name === "right")) {
588
+ session.pageIndex = (session.pageIndex + 1) % 2;
589
+ output.redraw();
590
+ }
591
+ }
592
+
593
+ function askForkPointMenu(items) {
594
+ return new Promise((resolve) => {
595
+ const choiceState = createChoiceMenuState(items.map((item) => item.label), items[0].label);
596
+ const session = { mode: "fork", inputText: "/fork", choiceState, items, resolve };
597
+ state.input.session = session;
598
+ state.input.active = true;
599
+ cancelActiveInput = () => completeTtyInput(session, null, false);
600
+ output.redraw(true);
601
+ });
602
+ }
603
+
516
604
  function syncSlashMenu(session) {
517
605
  session.menuState.setInput(session.editor.input());
518
606
  return session.menuState.matches();
@@ -529,6 +617,29 @@ export function createCliInputActions({
529
617
  return typeof prompt === "function" ? prompt() : prompt;
530
618
  }
531
619
 
620
+ // External input (rind send) enters the same path as a typed submit: echo
621
+ // only when a typed enter would echo, then command-first dispatch.
622
+ function dispatchExternal(text) {
623
+ const value = String(text || "");
624
+ if (!value.trim()) {
625
+ return;
626
+ }
627
+ if (!state.turn.active && !value.startsWith("/")) {
628
+ output.writeUserInput(value, "send");
629
+ }
630
+ void getCommandController().handle(value)
631
+ .then((handled) => {
632
+ if (!handled) {
633
+ getTurnController().submit(value);
634
+ }
635
+ })
636
+ .catch((error) => {
637
+ if (state.runtime.status !== "closing") {
638
+ output.writeError(`${error instanceof Error ? error.message : String(error)}\n`);
639
+ }
640
+ });
641
+ }
642
+
532
643
  return {
533
644
  ask,
534
645
  answerQuestion,
@@ -538,11 +649,14 @@ export function createCliInputActions({
538
649
  clearPendingInputs,
539
650
  handleTerminalInput,
540
651
  handleTerminalPaste,
652
+ dispatchExternal,
541
653
  askModelMenu,
542
654
  askEffortMenu,
543
655
  askThemeMenu,
544
656
  askSessionMenu,
545
657
  askTeamBlueprint,
658
+ askForkPointMenu,
659
+ askContextBoard,
546
660
  cancel: () => cancelActiveInput?.(),
547
661
  };
548
662
  }