@beunec/rivine 0.2.1 → 0.3.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/README.md CHANGED
@@ -60,11 +60,11 @@ no router — no keyword matcher, no classifier, no extra model turn that reads
60
60
  your request and picks a lane. A wrong guess sends a database migration to the
61
61
  frontend agent and you never see the decision that was made for you.
62
62
 
63
- ### Mode — which agent runs
63
+ ### Mode — what Rivine Code works on
64
64
 
65
- Each Mode is one standalone agent that owns the task end to end: it searches the
66
- codebase, reads what it needs, writes across as many files as the change
67
- requires, runs the build and tests, and fixes what the terminal reports.
65
+ Each Mode owns the task end to end: it searches the codebase, reads what it
66
+ needs, writes across as many files as the change requires, runs the build and
67
+ tests, and fixes what the terminal reports.
68
68
 
69
69
  | Mode | For | How to choose |
70
70
  |---|---|---|
@@ -93,9 +93,14 @@ even having said "just do it": credentials, anything outside the project
93
93
  directory, and commands that are hard to undo (`rm -rf`, `git push --force`, a
94
94
  deploy).
95
95
 
96
- The first interactive session asks for Mode, then Rate. `-m …` and `--rate …`
97
- skip the questions because you already answered them. Shift+Tab toggles the
98
- Rate.
96
+ The first interactive session asks for Mode, then Rate, before the prompt
97
+ opens — including for a one-shot `rivine "..."`, which used to assume Full
98
+ Stack. `-m …` and `--rate …` skip the question you already answered on the
99
+ command line. Shift+Tab toggles the Rate. `/mode` and `/rate` reopen either
100
+ picker later.
101
+
102
+ Without a terminal to ask on — a pipe, CI, another agent driving this — `-m`
103
+ is required and the run stops with the list rather than guessing.
99
104
 
100
105
  What stays gated:
101
106
 
@@ -138,10 +143,11 @@ rivine mcp login <name> sign in to a remote MCP server
138
143
  rivine --output-format json one JSON object on stdout, for CI
139
144
  ```
140
145
 
141
- In a session: type `/` and press Tab. `/help` `/agent` `/mode` `/plan` `/accept`
146
+ In a session: type `/` and press Tab. `/help` `/mode` `/rate` `/plan` `/accept`
142
147
  `/status` `/diff` `/pr` `/mcp` `/credits` `/clear` `/exit`. Startup asks for
143
- **Mode** (which agent) then **Rate** (Manual or Bypass). `/agent` and `/mode`
144
- reopen those pickers. Shift+Tab toggles Manual ⇄ Bypass. Ctrl+C quits cleanly.
148
+ **Mode** (what Rivine Code works on) then **Rate** (Manual or Bypass); `/mode`
149
+ and `/rate` reopen those pickers. Shift+Tab toggles Manual ⇄ Bypass. Ctrl+C
150
+ quits cleanly.
145
151
 
146
152
  `/diff` shows what changed in the working tree. `/pr` prepares a pull request
147
153
  through your own `gh` CLI. It **never pushes without being asked**: on a
@@ -120,12 +120,20 @@ export async function resumeTurn(args) {
120
120
  decision: args.approved ? 'approved' : 'rejected',
121
121
  feedback: args.feedback ?? '',
122
122
  modified_args: args.modifiedArgs ?? null,
123
+ code_mode: args.codeMode ?? null,
124
+ code_rate: args.codeRate ?? null,
123
125
  cli_session_key: args.sessionKey ?? null,
124
126
  cli_session_id: args.sessionId,
125
127
  }),
126
128
  });
127
129
  if (!res.ok) {
128
- throw new Error(`Could not resume the turn (HTTP ${res.status}).`);
130
+ const detail = await res
131
+ .json()
132
+ .then((b) => String(b?.detail ?? b?.error ?? '').trim())
133
+ .catch(() => '');
134
+ throw new Error(detail
135
+ ? `Could not resume the turn (HTTP ${res.status}): ${detail}`
136
+ : `Could not resume the turn (HTTP ${res.status}).`);
129
137
  }
130
138
  return (await res.json());
131
139
  }
package/dist/index.js CHANGED
@@ -10,6 +10,7 @@
10
10
  * redistribution or derivative works.
11
11
  */
12
12
  import { readFileSync } from 'node:fs';
13
+ import { realpath } from 'node:fs/promises';
13
14
  import { dirname, join, resolve as resolvePath } from 'node:path';
14
15
  import { fileURLToPath } from 'node:url';
15
16
  import { render } from 'ink';
@@ -18,6 +19,7 @@ import { Command } from 'commander';
18
19
  import { App } from './ui/App.js';
19
20
  import { runHeadless } from './runHeadless.js';
20
21
  import { isInteractiveTerminal, installTeardown } from './ui/headless.js';
22
+ import { emitNotice } from './ui/notices.js';
21
23
  import { login, logout, status } from './auth/session.js';
22
24
  import { openBrowser } from './auth/pkce.js';
23
25
  import { apiUrl, siteUrl } from './config.js';
@@ -262,14 +264,15 @@ function collect(value, previous) {
262
264
  program
263
265
  .argument('[prompt...]', 'send one prompt and exit')
264
266
  .option('-p, --print <prompt>', 'send one prompt, print the answer, and exit')
265
- .option('-m, --mode <mode>', `which agent runs: ${CODE_MODES.join(' | ')}`)
267
+ .option('-m, --mode <mode>', `what Rivine Code works on: ${CODE_MODES.join(' | ')}`)
266
268
  .option('--rate <rate>', `how edits apply: ${RATES.join(' | ')}`)
267
269
  .option('--plan', 'shorthand for --mode simple (Plan/Ask Only)')
268
270
  .option('--bypass', 'never ask for approval — for CI only, on a machine you are willing to have changed')
269
271
  .option('-C, --cwd <dir>', 'run against a different directory')
270
272
  .option('--output-format <format>', 'text | json — json puts one object on stdout and everything else on stderr', 'text')
271
273
  .action(async (prompt, options) => {
272
- const cwd = options.cwd ? resolvePath(options.cwd) : process.cwd();
274
+ const requested = options.cwd ? resolvePath(options.cwd) : process.cwd();
275
+ const cwd = await realpath(requested).catch(() => requested);
273
276
  const oneShot = options.print ?? (prompt.length ? prompt.join(' ') : undefined);
274
277
  const { mode, explicit: modeExplicit } = resolveMode(options);
275
278
  const codeMode = resolveCodeMode(options);
@@ -298,19 +301,29 @@ program
298
301
  return;
299
302
  }
300
303
  if (format === 'json' && oneShot) {
301
- process.exitCode = await runHeadless({
302
- cwd, mode, codeMode: codeMode ?? 'fullstack', prompt: oneShot, format,
303
- });
304
+ if (!codeMode) {
305
+ console.error('\n --mode is required with --output-format json.\n' +
306
+ ` Choose one of: ${CODE_MODES.join(', ')}\n` +
307
+ ' e.g. rivine -m backend --output-format json -p "add tests for auth.ts"\n');
308
+ process.exitCode = 2;
309
+ return;
310
+ }
311
+ process.exitCode = await runHeadless({ cwd, mode, codeMode, prompt: oneShot, format });
304
312
  return;
305
313
  }
306
314
  const { waitUntilExit } = render(React.createElement(App, {
307
315
  cwd,
308
316
  initialMode: mode,
309
317
  askForMode: !modeExplicit && !oneShot,
310
- initialCodeMode: codeMode ?? (oneShot ? 'fullstack' : undefined),
318
+ initialCodeMode: codeMode ?? undefined,
311
319
  oneShot,
312
320
  }), { exitOnCtrlC: false });
313
- const removeTeardown = installTeardown(() => { });
321
+ const removeTeardown = installTeardown(() => { }, {
322
+ onRejection: (err) => {
323
+ const message = err?.message ?? String(err);
324
+ emitNotice(`Background task failed: ${message}`);
325
+ },
326
+ });
314
327
  try {
315
328
  await waitUntilExit();
316
329
  }
@@ -47,7 +47,6 @@ export function parseCodeMode(raw) {
47
47
  return table[key] ?? null;
48
48
  }
49
49
  export const PERMISSION_MODES = ['plan', 'default', 'acceptEdits', 'bypass'];
50
- export const CYCLE_MODES = ['plan', 'default', 'acceptEdits'];
51
50
  export function isBatchable(category) {
52
51
  return category === 'write' || category === 'mcp';
53
52
  }
@@ -251,9 +250,6 @@ export function nextMode(current) {
251
250
  ? rateToPermissionMode('manual')
252
251
  : rateToPermissionMode('bypass');
253
252
  }
254
- export function nextRate(current) {
255
- return current === 'bypass' ? 'manual' : 'bypass';
256
- }
257
253
  export function modeName(mode) {
258
254
  switch (mode) {
259
255
  case 'plan':
@@ -16,6 +16,7 @@ import { isSealedEnvelope, SessionKey } from './sessionKey.js';
16
16
  import { permissionModeToRate } from './permissions.js';
17
17
  import { selectRelevantTools } from '../mcp/relevance.js';
18
18
  import { readTurnStream } from './stream.js';
19
+ import { displayStage } from '../ui/render.js';
19
20
  const PENDING_POLL_MS = 1200;
20
21
  export class TurnRunner {
21
22
  ctx;
@@ -72,6 +73,14 @@ export class TurnRunner {
72
73
  this.ctx.mcpTools = tools;
73
74
  }
74
75
  async run(message, snapshot) {
76
+ if (!this.ctx.codeMode) {
77
+ return {
78
+ answer: '',
79
+ cancelled: false,
80
+ error: 'No Mode selected. Pick one with /mode — Frontend Only, Backend Only, ' +
81
+ 'Full Stack, Plan/Ask Only, or Security Scan/Fix.',
82
+ };
83
+ }
75
84
  this.stopped = false;
76
85
  this.startPolling();
77
86
  const selection = selectRelevantTools(message, this.ctx.mcpTools ?? []);
@@ -112,9 +121,12 @@ export class TurnRunner {
112
121
  answer += event.content ?? '';
113
122
  this.cb.onToken(event.content ?? '');
114
123
  break;
115
- case 'stage':
116
- this.cb.onStage(String(event.label ?? ''));
124
+ case 'stage': {
125
+ const label = displayStage(String(event.label ?? ''));
126
+ if (label)
127
+ this.cb.onStage(label);
117
128
  break;
129
+ }
118
130
  case 'tool_call_start': {
119
131
  const e = event;
120
132
  const detail = e.args?.command ?? e.args?.filename ?? '';
@@ -158,6 +170,7 @@ export class TurnRunner {
158
170
  options: extractOptions(e),
159
171
  multi: Boolean(e.ui_schema?.multi ?? e.args?.multi),
160
172
  threadId: String(e.thread_id ?? `${this.ctx.workspaceId}_${this.ctx.chatSpaceId}`),
173
+ kind: hitlKind(e.hitl_type, e.args),
161
174
  };
162
175
  break;
163
176
  }
@@ -184,6 +197,8 @@ export class TurnRunner {
184
197
  let answer = '';
185
198
  for (let round = 0; current && round < 25; round += 1) {
186
199
  const decision = await this.enqueuePrompt(() => this.cb.requestDecision(current));
200
+ if (decision.codeMode)
201
+ this.setCodeMode(decision.codeMode);
187
202
  const body = (await resumeTurn({
188
203
  workspaceId: this.ctx.workspaceId,
189
204
  chatSpaceId: this.ctx.chatSpaceId,
@@ -193,6 +208,8 @@ export class TurnRunner {
193
208
  approved: decision.approved,
194
209
  feedback: decision.feedback,
195
210
  modifiedArgs: decision.modifiedArgs,
211
+ codeMode: decision.codeMode ?? null,
212
+ codeRate: decision.codeMode ? permissionModeToRate(this.ctx.mode) : null,
196
213
  }));
197
214
  answer = String(body.response ?? answer);
198
215
  const meta = body.context_metadata ?? {};
@@ -203,6 +220,7 @@ export class TurnRunner {
203
220
  options: extractOptions({ ui_schema: meta.hitl_ui_schema, args: meta.hitl_args }),
204
221
  multi: Boolean(meta.hitl_ui_schema?.multi),
205
222
  threadId: String(meta.thread_id ?? current.threadId),
223
+ kind: hitlKind(meta.hitl_type, meta.hitl_args),
206
224
  }
207
225
  : null;
208
226
  }
@@ -339,9 +357,26 @@ export class TurnRunner {
339
357
  await closeSession(this.ctx.sessionId);
340
358
  }
341
359
  }
342
- function extractOptions(event) {
360
+ export function hitlKind(hitlType, args) {
361
+ const field = args && typeof args === 'object' ? String(args.field ?? '') : '';
362
+ return String(hitlType ?? '') === 'code_clarification' && field === 'mode'
363
+ ? 'code_mode'
364
+ : 'other';
365
+ }
366
+ export function extractOptions(event) {
367
+ const schemaEnum = (() => {
368
+ const props = event?.ui_schema?.properties;
369
+ if (!props || typeof props !== 'object')
370
+ return undefined;
371
+ for (const value of Object.values(props)) {
372
+ if (Array.isArray(value?.enum) && value.enum.length)
373
+ return value.enum;
374
+ }
375
+ return undefined;
376
+ })();
343
377
  const raw = event?.ui_schema?.options ??
344
378
  event?.ui_schema?.choices ??
379
+ schemaEnum ??
345
380
  event?.args?.options ??
346
381
  event?.options;
347
382
  if (!Array.isArray(raw) || raw.length === 0)
@@ -11,6 +11,7 @@
11
11
  import { InsufficientCredits } from './api/workspace.js';
12
12
  import { siteUrl } from './config.js';
13
13
  import { TurnRunner } from './protocol/runner.js';
14
+ import { permissionModeToRate } from './protocol/permissions.js';
14
15
  import { creditShortfall, startSession } from './session.js';
15
16
  import { HeadlessRenderer, installTeardown } from './ui/headless.js';
16
17
  export async function runHeadless(options) {
@@ -84,7 +85,9 @@ export async function runHeadless(options) {
84
85
  },
85
86
  requestDecision: async (request) => {
86
87
  out.endAnswer();
87
- out.line('notice', `Approval needed but no terminal to ask on: ${request.message}`);
88
+ out.line('notice', request.kind === 'code_mode'
89
+ ? 'No Mode selected. Pass -m frontend|backend|fullstack|simple|security.'
90
+ : `Approval needed but no terminal to ask on: ${request.message}`);
88
91
  return { approved: false, feedback: 'No interactive terminal available.' };
89
92
  },
90
93
  askQuestion: async (question) => {
@@ -111,7 +114,7 @@ export async function runHeadless(options) {
111
114
  if (out.refusalCount > 0) {
112
115
  const advice = options.mode === 'plan'
113
116
  ? 'plan mode makes no changes — re-run without --plan'
114
- : `re-run with -m acceptEdits, or --bypass in CI (mode was ${options.mode})`;
117
+ : `re-run with --rate bypass (Rate was ${permissionModeToRate(options.mode)})`;
115
118
  const message = `${out.refusalCount} operation(s) were skipped because there is no terminal ` +
116
119
  `to approve them: ${advice}.`;
117
120
  out.line('error', message);
package/dist/ui/App.js CHANGED
@@ -22,8 +22,10 @@ import { sanitizeForModel } from '../protocol/shell.js';
22
22
  import { diff as gitDiff, gitBranchLine, prCreate } from '../protocol/git.js';
23
23
  import { Activity, ApprovalPrompt, Banner, CodeModePicker, RatePicker, WORDMARK_MIN_COLUMNS, ChoicePrompt, Completions, HelpPanel, StatusBar, TranscriptLine, ShellPane, SHELL_PANE_LINES, } from './components.js';
24
24
  import { TokenBuffer, terminalWidth } from './render.js';
25
+ import { initialPhase, phaseAfterBootstrap, phaseAfterModePicked, phaseAfterRatePicked, } from './phase.js';
25
26
  import { color } from './theme.js';
26
27
  import { completeInput, completionsFor } from './slashCommands.js';
28
+ import { setNoticeSink } from './notices.js';
27
29
  function trimToPane(text) {
28
30
  const lines = text.split('\n');
29
31
  return lines.length > SHELL_PANE_LINES * 2
@@ -40,7 +42,7 @@ export function App({ cwd, initialMode, askForMode, initialCodeMode, oneShot })
40
42
  const { exit } = useApp();
41
43
  const [codeMode, setCodeMode] = useState(initialCodeMode);
42
44
  const codeModeChosenRef = useRef(Boolean(initialCodeMode));
43
- const [phase, setPhase] = useState(initialCodeMode ? (askForMode ? 'mode' : 'starting') : 'codeMode');
45
+ const [phase, setPhase] = useState(initialPhase(Boolean(initialCodeMode), Boolean(askForMode)));
44
46
  const sessionReadyRef = useRef(false);
45
47
  const modeChosenRef = useRef(!askForMode);
46
48
  const [lines, setLines] = useState([]);
@@ -74,6 +76,8 @@ export function App({ cwd, initialMode, askForMode, initialCodeMode, oneShot })
74
76
  const mcpRef = useRef(null);
75
77
  const snapshotRef = useRef(null);
76
78
  const snapshotSentRef = useRef(false);
79
+ const minCreditsRef = useRef({ rivine_code: 10 });
80
+ const queuedPromptRef = useRef(null);
77
81
  const answerRef = useRef(null);
78
82
  const lineId = useRef(0);
79
83
  const liveText = useRef('');
@@ -119,9 +123,10 @@ export function App({ cwd, initialMode, askForMode, initialCodeMode, oneShot })
119
123
  codeModeChosenRef.current = true;
120
124
  runnerRef.current?.setCodeMode(chosen);
121
125
  push('system', `Mode: ${CODE_MODE_LABELS[chosen]}`);
122
- setPhase(modeChosenRef.current
123
- ? (sessionReadyRef.current ? 'ready' : 'starting')
124
- : 'mode');
126
+ setPhase(phaseAfterModePicked({
127
+ sessionReady: sessionReadyRef.current,
128
+ rateChosen: modeChosenRef.current,
129
+ }));
125
130
  }, [push]);
126
131
  const cancelCodeModePick = useCallback(() => {
127
132
  setPhase(sessionReadyRef.current ? 'ready' : 'starting');
@@ -130,7 +135,10 @@ export function App({ cwd, initialMode, askForMode, initialCodeMode, oneShot })
130
135
  setMode(chosen);
131
136
  modeChosenRef.current = true;
132
137
  runnerRef.current?.setMode(chosen);
133
- setPhase(sessionReadyRef.current ? 'ready' : 'starting');
138
+ setPhase(phaseAfterRatePicked({
139
+ sessionReady: sessionReadyRef.current,
140
+ rateChosen: true,
141
+ }));
134
142
  push('system', `Rate: ${describeMode(chosen)}`);
135
143
  if (chosen === 'bypass') {
136
144
  push('notice', 'Running without prompts. Files outside this project are still refused.');
@@ -140,8 +148,16 @@ export function App({ cwd, initialMode, askForMode, initialCodeMode, oneShot })
140
148
  const cancelModePick = useCallback(() => {
141
149
  setPhase(sessionReadyRef.current ? 'ready' : 'starting');
142
150
  }, []);
151
+ useEffect(() => {
152
+ setNoticeSink((text) => push('notice', text));
153
+ return () => setNoticeSink(null);
154
+ }, [push]);
143
155
  const quit = useCallback(() => {
144
- void runnerRef.current?.shutdown().finally(() => exit());
156
+ void runnerRef.current
157
+ ?.shutdown()
158
+ .catch(() => {
159
+ })
160
+ .finally(() => exit());
145
161
  }, [exit]);
146
162
  const cycleMode = useCallback(() => {
147
163
  setMode((current) => {
@@ -168,10 +184,10 @@ export function App({ cwd, initialMode, askForMode, initialCodeMode, oneShot })
168
184
  setShowHelp(false);
169
185
  setScreenGeneration((n) => n + 1);
170
186
  return true;
171
- case '/agent':
187
+ case '/mode':
172
188
  setPhase('codeMode');
173
189
  return true;
174
- case '/mode':
190
+ case '/rate':
175
191
  setPhase('mode');
176
192
  return true;
177
193
  case '/plan':
@@ -179,6 +195,10 @@ export function App({ cwd, initialMode, askForMode, initialCodeMode, oneShot })
179
195
  codeModeChosenRef.current = true;
180
196
  runnerRef.current?.setCodeMode('simple');
181
197
  push('system', `Mode: ${CODE_MODE_LABELS.simple}`);
198
+ setPhase(phaseAfterModePicked({
199
+ sessionReady: sessionReadyRef.current,
200
+ rateChosen: modeChosenRef.current,
201
+ }));
182
202
  return true;
183
203
  case '/accept':
184
204
  setMode('bypass');
@@ -249,7 +269,13 @@ export function App({ cwd, initialMode, askForMode, initialCodeMode, oneShot })
249
269
  return;
250
270
  if (runCommand(text))
251
271
  return;
252
- const needed = creditShortfall(status.balance, { rivine_code: 10 });
272
+ if (!codeMode) {
273
+ queuedPromptRef.current = text;
274
+ push('notice', 'Pick a Mode first — it decides what Rivine Code works on. Your message is saved.');
275
+ setPhase('codeMode');
276
+ return;
277
+ }
278
+ const needed = creditShortfall(status.balance, minCreditsRef.current);
253
279
  if (needed !== null) {
254
280
  push('error', `Rivine Code needs $${needed.toFixed(2)} in credits to run.`);
255
281
  push('notice', `Top up at ${siteUrl()}/credits`);
@@ -288,7 +314,16 @@ export function App({ cwd, initialMode, askForMode, initialCodeMode, oneShot })
288
314
  if (oneShot)
289
315
  quit();
290
316
  }
291
- }, [oneShot, push, quit, refreshBalance, runCommand, settleLive, status.balance]);
317
+ }, [codeMode, oneShot, push, quit, refreshBalance, runCommand, settleLive, status.balance]);
318
+ useEffect(() => {
319
+ if (phase !== 'ready' || !codeMode)
320
+ return;
321
+ const queued = queuedPromptRef.current;
322
+ if (!queued)
323
+ return;
324
+ queuedPromptRef.current = null;
325
+ void submit(queued);
326
+ }, [phase, codeMode, submit]);
292
327
  useEffect(() => {
293
328
  let cancelled = false;
294
329
  (async () => {
@@ -400,20 +435,29 @@ export function App({ cwd, initialMode, askForMode, initialCodeMode, oneShot })
400
435
  setPhase('question');
401
436
  }),
402
437
  }, session.mcp);
403
- void session.mcpReady.then((reports) => {
438
+ void session.mcpReady
439
+ .then((reports) => {
404
440
  if (cancelled || reports.length === 0)
405
441
  return;
406
442
  const good = reports.filter((r) => r.ok);
407
443
  const tools = session.mcp.listTools().length;
408
- if (good.length)
444
+ if (good.length) {
409
445
  push('system', `MCP: ${good.length} server(s), ${tools} tool(s) available.`);
446
+ }
410
447
  for (const bad of reports.filter((r) => !r.ok)) {
411
448
  push('notice', `MCP server "${bad.name}" did not start: ${bad.error}`);
412
449
  }
413
450
  runnerRef.current?.setMcpTools(session.mcp.listTools());
451
+ })
452
+ .catch((err) => {
453
+ push('notice', `MCP servers could not be listed: ${err.message}`);
414
454
  });
455
+ minCreditsRef.current = session.bootstrap.engine_min_credits ?? minCreditsRef.current;
415
456
  sessionReadyRef.current = true;
416
- setPhase(modeChosenRef.current ? 'ready' : 'mode');
457
+ setPhase((current) => phaseAfterBootstrap(current, {
458
+ sessionReady: true,
459
+ rateChosen: modeChosenRef.current,
460
+ }));
417
461
  runnerRef.current?.setMode(mode);
418
462
  push('system', `${session.project}${session.bootstrap.created ? ' · new session' : ''}`);
419
463
  if (session.continuing) {
@@ -525,9 +569,15 @@ export function App({ cwd, initialMode, askForMode, initialCodeMode, oneShot })
525
569
  const compactBanner = terminalWidth() < WORDMARK_MIN_COLUMNS || Boolean(oneShot);
526
570
  const matches = completionsFor(input);
527
571
  const busy = phase !== 'ready';
528
- 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 === 'codeMode' ? (_jsx(CodeModePicker, { onPick: pickCodeMode, current: codeModeChosenRef.current ? codeMode : undefined, onCancel: codeModeChosenRef.current ? cancelCodeModePick : undefined })) : null, phase === 'mode' ? (_jsx(RatePicker, { 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
572
+ 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 === 'codeMode' ? (_jsx(CodeModePicker, { onPick: pickCodeMode, current: codeModeChosenRef.current ? codeMode : undefined, onCancel: codeModeChosenRef.current ? cancelCodeModePick : undefined })) : null, phase === 'mode' ? (_jsx(RatePicker, { 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 ? (hitl.kind === 'code_mode' ? (_jsx(CodeModePicker, { onPick: (chosen) => {
573
+ setCodeMode(chosen);
574
+ codeModeChosenRef.current = true;
575
+ runnerRef.current?.setCodeMode(chosen);
576
+ push('system', `Mode: ${CODE_MODE_LABELS[chosen]}`);
577
+ answerHitl({ approved: true, feedback: CODE_MODE_LABELS[chosen], codeMode: chosen });
578
+ }, current: codeMode })) : (_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
529
579
  ? { approved: true, feedback: value }
530
- : { 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: {
580
+ : { 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: {
531
581
  mode,
532
582
  codeMode,
533
583
  project: status.project,
@@ -62,7 +62,7 @@ export function CodeModePicker({ onPick, current, onCancel, }) {
62
62
  if (found)
63
63
  onPick(found.mode);
64
64
  });
65
- return (_jsxs(Box, { flexDirection: "column", paddingX: 1, paddingY: 1, children: [_jsx(Text, { color: color('text'), bold: true, children: "Mode \u2014 which agent should handle this?" }), _jsx(Box, { flexDirection: "column", marginTop: 1, children: options.map((o) => {
65
+ return (_jsxs(Box, { flexDirection: "column", paddingX: 1, paddingY: 1, children: [_jsx(Text, { color: color('text'), bold: true, children: "Mode \u2014 what should Rivine Code work on?" }), _jsx(Box, { flexDirection: "column", marginTop: 1, children: options.map((o) => {
66
66
  const active = o.mode === current;
67
67
  return (_jsxs(Box, { children: [_jsx(Text, { color: active ? color('success') : color('faint'), children: active ? ' \u203a' : ' ' }), _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 }), _jsx(Text, { color: color('faint'), children: " \u2014 " }), _jsx(Text, { color: color('faint'), children: fit(o.detail, Math.max(10, width - 34)) })] }, o.key));
68
68
  }) }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { color: color('faint'), children: current ? 'esc keeps the current mode' : 'pick a number to begin' }) })] }));
@@ -93,7 +93,6 @@ export function RatePicker({ onPick, current, onCancel, }) {
93
93
  ? `press a number · enter keeps ${options.find((o) => o.mode === current)?.label ?? 'this'} · esc cancels`
94
94
  : 'press a number · enter for Manual · shift+tab toggles later', width - 4) }) })] }));
95
95
  }
96
- export const ModePicker = RatePicker;
97
96
  export const SHELL_PANE_LINES = 12;
98
97
  export function ShellPane({ text, live }) {
99
98
  if (!text)
@@ -137,7 +136,7 @@ export function Completions({ matches }) {
137
136
  return (_jsxs(Box, { flexDirection: "column", paddingX: 1, children: [matches.slice(0, 6).map((c) => (_jsxs(Box, { children: [_jsx(Text, { color: color('accent'), children: c.name.padEnd(10) }), _jsx(Text, { color: color('faint'), children: fit(c.summary, width - 14) })] }, c.name))), matches.length === 1 ? (_jsx(Text, { color: color('faint'), children: " tab to complete" })) : null] }));
138
137
  }
139
138
  export function HelpPanel() {
140
- 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 toggle Rate (Manual \u21C4 Bypass)" }), _jsx(Text, { color: color('faint'), children: " esc stop the current turn" }), _jsx(Text, { color: color('faint'), children: " ctrl+c quit" })] })] }));
139
+ 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: " Mode what Rivine Code works on \u2014 /mode" }), _jsx(Text, { color: color('faint'), children: " Rate how edits apply \u2014 /rate, or shift+tab" })] }), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(Text, { color: color('faint'), children: " shift+tab toggle Rate (Manual \u21C4 Bypass)" }), _jsx(Text, { color: color('faint'), children: " esc stop the current turn" }), _jsx(Text, { color: color('faint'), children: " ctrl+c quit" })] })] }));
141
140
  }
142
141
  export function StatusBar({ info }) {
143
142
  const width = terminalColumns();
@@ -73,7 +73,7 @@ export class HeadlessRenderer {
73
73
  refuseApproval(approval) {
74
74
  const fix = this.mode === 'plan'
75
75
  ? 'plan mode makes no changes; re-run without --plan'
76
- : 'no terminal to ask on — re-run with -m acceptEdits, or --bypass in CI';
76
+ : 'no terminal to ask on — re-run with --rate bypass';
77
77
  this.refused += 1;
78
78
  this.line('notice', `Skipped: ${approval.summary}`);
79
79
  this.line('notice', ` ${approval.reason} (${fix})`);
@@ -85,7 +85,7 @@ export class HeadlessRenderer {
85
85
  return '';
86
86
  }
87
87
  }
88
- export function installTeardown(onExit) {
88
+ export function installTeardown(onExit, options = {}) {
89
89
  let finished = false;
90
90
  const restore = () => {
91
91
  try {
@@ -116,15 +116,18 @@ export function installTeardown(onExit) {
116
116
  process.stderr.write(`\n ${err?.message ?? String(err)}\n\n`);
117
117
  void finish(1);
118
118
  };
119
+ const onRejection = options.onRejection
120
+ ? (err) => options.onRejection(err)
121
+ : onFatal;
119
122
  process.on('SIGINT', onSigint);
120
123
  process.on('SIGTERM', onSigterm);
121
124
  process.on('uncaughtException', onFatal);
122
- process.on('unhandledRejection', onFatal);
125
+ process.on('unhandledRejection', onRejection);
123
126
  return () => {
124
127
  process.off('SIGINT', onSigint);
125
128
  process.off('SIGTERM', onSigterm);
126
129
  process.off('uncaughtException', onFatal);
127
- process.off('unhandledRejection', onFatal);
130
+ process.off('unhandledRejection', onRejection);
128
131
  restore();
129
132
  };
130
133
  }
@@ -0,0 +1,20 @@
1
+ /*!
2
+ * Rivine Code CLI
3
+ * Licensed and owned by Beunec Technologies, Inc.
4
+ *
5
+ * https://aselius.beunec.com
6
+ *
7
+ * This software is proprietary. Use is governed by the licence agreement
8
+ * accompanying it; it is not open source and is not licensed for
9
+ * redistribution or derivative works.
10
+ */
11
+ let sink = null;
12
+ export function setNoticeSink(next) {
13
+ sink = next;
14
+ }
15
+ export function emitNotice(text) {
16
+ if (!sink || !text)
17
+ return false;
18
+ sink(text);
19
+ return true;
20
+ }
@@ -0,0 +1,28 @@
1
+ /*!
2
+ * Rivine Code CLI
3
+ * Licensed and owned by Beunec Technologies, Inc.
4
+ *
5
+ * https://aselius.beunec.com
6
+ *
7
+ * This software is proprietary. Use is governed by the licence agreement
8
+ * accompanying it; it is not open source and is not licensed for
9
+ * redistribution or derivative works.
10
+ */
11
+ export function phaseAfterBootstrap(current, s) {
12
+ if (current === 'codeMode' || current === 'mode')
13
+ return current;
14
+ return s.rateChosen ? 'ready' : 'mode';
15
+ }
16
+ export function phaseAfterModePicked(s) {
17
+ if (!s.rateChosen)
18
+ return 'mode';
19
+ return s.sessionReady ? 'ready' : 'starting';
20
+ }
21
+ export function phaseAfterRatePicked(s) {
22
+ return s.sessionReady ? 'ready' : 'starting';
23
+ }
24
+ export function initialPhase(hasMode, askRate) {
25
+ if (!hasMode)
26
+ return 'codeMode';
27
+ return askRate ? 'mode' : 'starting';
28
+ }
package/dist/ui/render.js CHANGED
@@ -111,3 +111,6 @@ export function tail(text, max) {
111
111
  return { lines, hidden: 0 };
112
112
  return { lines: lines.slice(-max), hidden: lines.length - max };
113
113
  }
114
+ export function displayStage(label) {
115
+ return /^routed to\b/i.test(label.trim()) ? '' : label;
116
+ }
@@ -11,19 +11,19 @@
11
11
  export const SLASH_COMMANDS = [
12
12
  { name: '/help', summary: 'show these commands' },
13
13
  {
14
- name: '/agent',
15
- summary: 'pick which engine Mode runs',
16
- detail: 'Frontend, Backend, Full Stack, Plan/Ask, or Security.',
14
+ name: '/mode',
15
+ summary: 'pick the Mode — what Rivine Code works on',
16
+ detail: 'Frontend Only, Backend Only, Full Stack, Plan/Ask Only, or Security Scan/Fix.',
17
17
  },
18
18
  {
19
- name: '/mode',
19
+ name: '/rate',
20
20
  summary: 'pick the Rate — Manual or Bypass',
21
- detail: 'Shift+Tab toggles Manual ⇄ Bypass.',
21
+ detail: 'Manual asks before each edit; Bypass applies them. Shift+Tab toggles.',
22
22
  },
23
23
  {
24
24
  name: '/plan',
25
25
  summary: 'switch to Plan/Ask Only Mode',
26
- detail: 'Same as picking Plan/Ask in /agent.',
26
+ detail: 'Same as picking Plan/Ask Only in /mode.',
27
27
  },
28
28
  {
29
29
  name: '/accept',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@beunec/rivine",
3
- "version": "0.2.1",
3
+ "version": "0.3.0",
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",