@beunec/rivine 0.1.4 → 0.1.5

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.
@@ -214,6 +214,18 @@ export function nextMode(current) {
214
214
  return 'default';
215
215
  return CYCLE_MODES[(index + 1) % CYCLE_MODES.length];
216
216
  }
217
+ export function modeName(mode) {
218
+ switch (mode) {
219
+ case 'plan':
220
+ return 'plan';
221
+ case 'default':
222
+ return 'default';
223
+ case 'acceptEdits':
224
+ return 'accept edits';
225
+ case 'bypass':
226
+ return 'bypass';
227
+ }
228
+ }
217
229
  export function describeMode(mode) {
218
230
  switch (mode) {
219
231
  case 'plan':
package/dist/ui/App.js CHANGED
@@ -23,13 +23,14 @@ import { diff as gitDiff, gitBranchLine, prCreate } from '../protocol/git.js';
23
23
  import { Activity, ApprovalPrompt, Banner, ModePicker, WORDMARK_MIN_COLUMNS, ChoicePrompt, Completions, HelpPanel, StatusBar, TranscriptLine, ShellPane, SHELL_PANE_LINES, } from './components.js';
24
24
  import { TokenBuffer, terminalWidth } from './render.js';
25
25
  import { color } from './theme.js';
26
- import { completionsFor } from './slashCommands.js';
26
+ import { completeInput, completionsFor } from './slashCommands.js';
27
27
  function trimToPane(text) {
28
28
  const lines = text.split('\n');
29
29
  return lines.length > SHELL_PANE_LINES * 2
30
30
  ? lines.slice(-SHELL_PANE_LINES * 2).join('\n')
31
31
  : text;
32
32
  }
33
+ const ESC = String.fromCharCode(27);
33
34
  function grantKey(category, request) {
34
35
  if (category !== 'mcp')
35
36
  return category;
@@ -48,6 +49,14 @@ export function App({ cwd, initialMode, askForMode, oneShot }) {
48
49
  const [tool, setTool] = useState('');
49
50
  const [mode, setMode] = useState(initialMode);
50
51
  const [input, setInput] = useState('');
52
+ const [inputSeed, setInputSeed] = useState('');
53
+ const [inputNonce, setInputNonce] = useState(0);
54
+ const [screenGeneration, setScreenGeneration] = useState(0);
55
+ const setPromptText = useCallback((text) => {
56
+ setInput(text);
57
+ setInputSeed(text);
58
+ setInputNonce((n) => n + 1);
59
+ }, []);
51
60
  const [showHelp, setShowHelp] = useState(false);
52
61
  const [approval, setApproval] = useState(null);
53
62
  const [question, setQuestion] = useState(null);
@@ -114,6 +123,9 @@ export function App({ cwd, initialMode, askForMode, oneShot }) {
114
123
  }
115
124
  push('system', 'Ask for anything. /help for commands.');
116
125
  }, [push]);
126
+ const cancelModePick = useCallback(() => {
127
+ setPhase(sessionReadyRef.current ? 'ready' : 'starting');
128
+ }, []);
117
129
  const quit = useCallback(() => {
118
130
  void runnerRef.current?.shutdown().finally(() => exit());
119
131
  }, [exit]);
@@ -137,10 +149,13 @@ export function App({ cwd, initialMode, askForMode, oneShot }) {
137
149
  quit();
138
150
  return true;
139
151
  case '/clear':
152
+ process.stdout.write(ESC + '[2J' + ESC + '[3J' + ESC + '[H');
140
153
  setLines([]);
154
+ setShowHelp(false);
155
+ setScreenGeneration((n) => n + 1);
141
156
  return true;
142
157
  case '/mode':
143
- cycleMode();
158
+ setPhase('mode');
144
159
  return true;
145
160
  case '/plan':
146
161
  setMode('plan');
@@ -201,7 +216,7 @@ export function App({ cwd, initialMode, askForMode, oneShot }) {
201
216
  }, [cwd, cycleMode, mode, push, quit, refreshBalance, status]);
202
217
  const submit = useCallback(async (message) => {
203
218
  const text = message.trim();
204
- setInput('');
219
+ setPromptText('');
205
220
  if (!runnerRef.current)
206
221
  return;
207
222
  if (runnerRef.current.interactiveShell) {
@@ -418,6 +433,12 @@ export function App({ cwd, initialMode, askForMode, oneShot }) {
418
433
  cycleMode();
419
434
  return;
420
435
  }
436
+ if (key.tab) {
437
+ const completed = completeInput(input);
438
+ if (completed !== input)
439
+ setPromptText(completed);
440
+ return;
441
+ }
421
442
  if (key.escape && runnerRef.current?.killShell()) {
422
443
  push('notice', 'Stopped the running command.');
423
444
  return;
@@ -466,9 +487,9 @@ export function App({ cwd, initialMode, askForMode, oneShot }) {
466
487
  const compactBanner = terminalWidth() < WORDMARK_MIN_COLUMNS || Boolean(oneShot);
467
488
  const matches = completionsFor(input);
468
489
  const busy = phase !== 'ready';
469
- return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Static, { items: staticItems, children: (item) => item.kind === 'banner' ? (_jsx(Banner, { compact: compactBanner }, "banner")) : (_jsx(TranscriptLine, { line: item.line }, item.line.id)) }), live ? (_jsx(Box, { paddingX: 1, children: _jsx(Text, { color: color('text'), children: live }) })) : null, shellView ? _jsx(ShellPane, { text: shellView, live: shellActive }) : null, phase === 'running' && stage ? _jsx(Activity, { stage: stage, tool: tool }) : null, phase === 'mode' ? _jsx(ModePicker, { onPick: pickMode }) : null, phase === 'approval' && approval ? (_jsx(ApprovalPrompt, { approval: approval, allowAlways: isBatchable(approval.category), onChoice: answerApproval })) : null, phase === 'question' && question ? (question.options?.length ? (_jsx(ChoicePrompt, { prompt: question.prompt, options: question.options, onPick: answerQuestion })) : (_jsxs(Box, { flexDirection: "column", paddingX: 1, paddingY: 1, children: [_jsx(Text, { color: color('accent'), bold: true, children: question.prompt }), _jsxs(Box, { marginTop: 1, children: [_jsx(Text, { color: color('user'), children: '› ' }), _jsx(TextInput, { placeholder: "your answer", onSubmit: answerQuestion })] })] }))) : null, phase === 'hitl' && hitl ? (_jsx(ChoicePrompt, { prompt: hitl.tool ? `${hitl.tool} — ${hitl.message}` : hitl.message, options: hitl.options?.length ? hitl.options : ['Approve', 'Decline'], onPick: (value) => answerHitl(hitl.options?.length
490
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Static, { items: staticItems, children: (item) => item.kind === 'banner' ? (_jsx(Banner, { compact: compactBanner }, "banner")) : (_jsx(TranscriptLine, { line: item.line }, item.line.id)) }, screenGeneration), live ? (_jsx(Box, { paddingX: 1, children: _jsx(Text, { color: color('text'), children: live }) })) : null, shellView ? _jsx(ShellPane, { text: shellView, live: shellActive }) : null, phase === 'running' && stage ? _jsx(Activity, { stage: stage, tool: tool }) : null, phase === 'mode' ? (_jsx(ModePicker, { onPick: pickMode, current: modeChosenRef.current ? mode : undefined, onCancel: modeChosenRef.current ? cancelModePick : undefined })) : null, phase === 'approval' && approval ? (_jsx(ApprovalPrompt, { approval: approval, allowAlways: isBatchable(approval.category), onChoice: answerApproval })) : null, phase === 'question' && question ? (question.options?.length ? (_jsx(ChoicePrompt, { prompt: question.prompt, options: question.options, onPick: answerQuestion })) : (_jsxs(Box, { flexDirection: "column", paddingX: 1, paddingY: 1, children: [_jsx(Text, { color: color('accent'), bold: true, children: question.prompt }), _jsxs(Box, { marginTop: 1, children: [_jsx(Text, { color: color('user'), children: '› ' }), _jsx(TextInput, { placeholder: "your answer", onSubmit: answerQuestion })] })] }))) : null, phase === 'hitl' && hitl ? (_jsx(ChoicePrompt, { prompt: hitl.tool ? `${hitl.tool} — ${hitl.message}` : hitl.message, options: hitl.options?.length ? hitl.options : ['Approve', 'Decline'], onPick: (value) => answerHitl(hitl.options?.length
470
491
  ? { approved: true, feedback: value }
471
- : { approved: value === 'Approve' }) })) : null, showHelp && phase === 'ready' ? _jsx(HelpPanel, {}) : null, phase === 'ready' && !oneShot ? (_jsxs(_Fragment, { children: [_jsxs(Box, { paddingX: 1, children: [_jsx(Text, { color: color('user'), children: '› ' }), _jsx(TextInput, { placeholder: "What should Rivine build?", onChange: setInput, onSubmit: (value) => void submit(value) })] }), _jsx(Completions, { matches: matches })] })) : null, phase === 'starting' ? _jsx(Activity, { stage: "Starting\u2026", tool: "" }) : null, _jsx(StatusBar, { info: {
492
+ : { approved: value === 'Approve' }) })) : null, showHelp && phase === 'ready' ? _jsx(HelpPanel, {}) : null, phase === 'ready' && !oneShot ? (_jsxs(_Fragment, { children: [_jsxs(Box, { paddingX: 1, children: [_jsx(Text, { color: color('user'), children: '› ' }), _jsx(TextInput, { defaultValue: inputSeed, placeholder: "What should Rivine build?", onChange: setInput, onSubmit: (value) => void submit(value) }, inputNonce)] }), _jsx(Completions, { matches: matches })] })) : null, phase === 'starting' ? _jsx(Activity, { stage: "Starting\u2026", tool: "" }) : null, _jsx(StatusBar, { info: {
472
493
  mode,
473
494
  project: status.project,
474
495
  cloudhandle: status.cloudhandle,
@@ -9,12 +9,13 @@
9
9
  * redistribution or derivative works.
10
10
  */
11
11
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
12
+ import React from 'react';
12
13
  import { Box, Text, useInput } from 'ink';
13
14
  import { Spinner } from '@inkjs/ui';
14
15
  import { ASCII_ONLY, BOX, color, glyph, lineColor } from './theme.js';
15
- import { fit, terminalWidth, wrap } from './render.js';
16
+ import { fit, terminalWidth, wrap, terminalColumns } from './render.js';
16
17
  import { SLASH_COMMANDS } from './slashCommands.js';
17
- import { describeMode } from '../protocol/permissions.js';
18
+ import { describeMode, modeName } from '../protocol/permissions.js';
18
19
  const WORDMARK = [
19
20
  '█▀▄ ▀█▀ █ █ ▀█▀ █▄ █ █▀▀ █▀▀ █▀█ █▀▄ █▀▀',
20
21
  '█▀▄ █ ▀▄ ▄▀ █ █ ▀█ █▀ █ █ █ █ █ █▀ ',
@@ -39,7 +40,7 @@ export function Activity({ stage, tool }) {
39
40
  const label = tool ? `${stage} · ${tool}` : stage;
40
41
  return (_jsx(Box, { paddingX: 1, children: _jsx(Spinner, { label: fit(label, width - 6) }) }));
41
42
  }
42
- export function ModePicker({ onPick }) {
43
+ export function ModePicker({ onPick, current, onCancel, }) {
43
44
  const width = terminalWidth();
44
45
  const options = [
45
46
  { key: '1', mode: 'plan', label: 'Plan', detail: 'reads and answers, changes nothing' },
@@ -48,15 +49,25 @@ export function ModePicker({ onPick }) {
48
49
  { key: '4', mode: 'bypass', label: 'Full speed', detail: 'no prompts inside this project' },
49
50
  ];
50
51
  useInput((input, key) => {
52
+ if (key.escape && onCancel) {
53
+ onCancel();
54
+ return;
55
+ }
51
56
  if (key.return) {
52
- onPick('acceptEdits');
57
+ onPick(current ?? 'acceptEdits');
53
58
  return;
54
59
  }
55
60
  const found = options.find((o) => o.key === input);
56
61
  if (found)
57
62
  onPick(found.mode);
58
63
  });
59
- return (_jsxs(Box, { flexDirection: "column", paddingX: 1, paddingY: 1, children: [_jsx(Text, { color: color('text'), bold: true, children: "How should Rivine work in this project?" }), _jsx(Box, { flexDirection: "column", marginTop: 1, children: options.map((o) => (_jsxs(Box, { children: [_jsx(Text, { color: color('accent'), children: ` ${o.key}` }), _jsx(Text, { color: color('faint'), children: " \u00B7 " }), _jsx(Text, { color: color('text'), children: o.label.padEnd(13) }), _jsx(Text, { color: color('muted'), children: fit(o.detail, Math.max(10, width - 24)) })] }, o.key))) }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { color: color('faint'), children: fit('press a number · enter for accept edits · shift+tab changes it later', width - 4) }) })] }));
64
+ return (_jsxs(Box, { flexDirection: "column", paddingX: 1, paddingY: 1, children: [_jsx(Text, { color: color('text'), bold: true, children: "How should Rivine work in this project?" }), _jsx(Box, { flexDirection: "column", marginTop: 1, children: options.map((o) => {
65
+ const active = o.mode === current;
66
+ return (_jsxs(Box, { children: [_jsx(Text, { color: active ? color('success') : color('faint'), children: active ? ' ›' : ' ' }), _jsx(Text, { color: color('accent'), children: o.key }), _jsx(Text, { color: color('faint'), children: " \u00B7 " }), _jsx(Text, { color: active ? color('text') : color('muted'), bold: active, children: o.label.padEnd(13) }), _jsx(Text, { color: color('muted'), children: fit(o.detail, Math.max(10, width - 26)) })] }, o.key));
67
+ }) }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { color: color('faint'), children: fit(current
68
+ ?
69
+ `press a number · enter keeps ${options.find((o) => o.mode === current)?.label ?? 'this'} · esc cancels`
70
+ : 'press a number · enter for accept edits · shift+tab changes it later', width - 4) }) })] }));
60
71
  }
61
72
  export const SHELL_PANE_LINES = 12;
62
73
  export function ShellPane({ text, live }) {
@@ -104,8 +115,51 @@ export function HelpPanel() {
104
115
  return (_jsxs(Box, { flexDirection: "column", paddingX: 1, paddingY: 1, children: [_jsx(Text, { color: color('text'), bold: true, children: "Commands" }), SLASH_COMMANDS.map((c) => (_jsxs(Box, { children: [_jsx(Text, { color: color('accent'), children: ` ${c.name}`.padEnd(12) }), _jsx(Text, { color: color('muted'), children: c.summary })] }, c.name))), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(Text, { color: color('faint'), children: " shift+tab change permission mode" }), _jsx(Text, { color: color('faint'), children: " esc stop the current turn" }), _jsx(Text, { color: color('faint'), children: " ctrl+c quit" })] })] }));
105
116
  }
106
117
  export function StatusBar({ info }) {
107
- const width = terminalWidth();
108
- const narrow = width < 76;
118
+ const width = terminalColumns();
109
119
  const dot = info.busy ? color('warn') : color('success');
110
- return (_jsxs(Box, { paddingX: 1, children: [_jsxs(Text, { color: dot, children: [BOX.dot, " "] }), !narrow ? (_jsxs(_Fragment, { children: [_jsx(Text, { color: color('brand'), bold: true, children: "Rivine" }), _jsx(Text, { color: color('faint'), children: " \u00B7 " })] })) : null, _jsx(Text, { color: color('muted'), children: describeMode(info.mode) }), info.project ? (_jsxs(_Fragment, { children: [_jsx(Text, { color: color('faint'), children: " \u00B7 " }), _jsx(Text, { color: color('muted'), children: fit(info.project, 18, true) })] })) : null, !narrow && info.cloudhandle ? (_jsxs(_Fragment, { children: [_jsx(Text, { color: color('faint'), children: " \u00B7 " }), _jsx(Text, { color: color('brand'), children: fit(info.cloudhandle, 22) }), info.bcidMasked ? _jsx(Text, { color: color('faint'), children: ` ${info.bcidMasked}` }) : null] })) : null, info.balance !== null ? (_jsxs(_Fragment, { children: [_jsx(Text, { color: color('faint'), children: " \u00B7 " }), _jsxs(Text, { color: info.balance < 10 ? color('warn') : color('muted'), children: ["$", info.balance.toFixed(2)] })] })) : null, !narrow ? _jsx(Text, { color: color('faint'), children: ' /help' }) : null] }));
120
+ const segments = [];
121
+ let used = 2;
122
+ let full = true;
123
+ const add = (text, node) => {
124
+ if (!full)
125
+ return false;
126
+ const cost = (segments.length > 0 ? 3 : 0) + text.length;
127
+ if (used + cost > width - 2) {
128
+ full = false;
129
+ return false;
130
+ }
131
+ used += cost;
132
+ segments.push({ node, text });
133
+ return true;
134
+ };
135
+ const modeText = describeMode(info.mode);
136
+ const modeShort = modeName(info.mode);
137
+ const brand = 'Rivine';
138
+ const fitsBrandAndFullMode = 2 + brand.length + 3 + modeText.length <= width - 2;
139
+ if (2 + brand.length + 3 + modeShort.length <= width - 2) {
140
+ add(brand, (_jsx(Text, { color: color('brand'), bold: true, children: brand })));
141
+ }
142
+ const shownMode = fitsBrandAndFullMode ? modeText : modeShort;
143
+ used += (segments.length > 0 ? 3 : 0) + shownMode.length;
144
+ segments.push({
145
+ node: _jsx(Text, { color: color('muted'), children: shownMode }),
146
+ text: shownMode,
147
+ });
148
+ if (info.balance !== null) {
149
+ const money = `$${info.balance.toFixed(2)}`;
150
+ add(money, _jsx(Text, { color: info.balance < 10 ? color('warn') : color('muted'), children: money }));
151
+ }
152
+ if (info.project) {
153
+ const project = fit(info.project, 18, true);
154
+ add(project, _jsx(Text, { color: color('muted'), children: project }));
155
+ }
156
+ if (info.cloudhandle) {
157
+ const handle = fit(info.cloudhandle, 22);
158
+ const masked = info.bcidMasked ? ` ${info.bcidMasked}` : '';
159
+ add(handle + masked, _jsxs(_Fragment, { children: [_jsx(Text, { color: color('brand'), children: handle }), masked ? _jsx(Text, { color: color('faint'), children: masked }) : null] }));
160
+ }
161
+ if (used + 8 <= width - 2) {
162
+ segments.push({ node: _jsx(Text, { color: color('faint'), children: ' /help' }), text: ' /help' });
163
+ }
164
+ return (_jsxs(Box, { paddingX: 1, children: [_jsxs(Text, { color: dot, children: [BOX.dot, " "] }), segments.map((segment, i) => (_jsxs(React.Fragment, { children: [i > 0 && segment.text !== ' /help' ? (_jsx(Text, { color: color('faint'), children: " \u00B7 " })) : null, segment.node] }, i)))] }));
111
165
  }
package/dist/ui/render.js CHANGED
@@ -49,6 +49,10 @@ export class TokenBuffer {
49
49
  this.pending = '';
50
50
  }
51
51
  }
52
+ export function terminalColumns() {
53
+ const columns = process.stdout.columns;
54
+ return Math.max(20, columns && columns > 0 ? columns : 80);
55
+ }
52
56
  export function terminalWidth() {
53
57
  const columns = process.stdout.columns;
54
58
  return Math.max(48, Math.min(columns && columns > 0 ? columns : 80, 120));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@beunec/rivine",
3
- "version": "0.1.4",
3
+ "version": "0.1.5",
4
4
  "description": "Rivine Code in your terminal — Beunec's software engineering runtime constructs, working on the files you already have open.",
5
5
  "license": "UNLICENSED",
6
6
  "author": "Beunec Technologies",