@beunec/rivine 0.1.5 → 0.2.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.
- package/README.md +79 -39
- package/action.yml +25 -8
- package/dist/api/workspace.js +3 -0
- package/dist/index.js +34 -11
- package/dist/protocol/permissions.js +61 -9
- package/dist/protocol/runner.js +23 -0
- package/dist/runHeadless.js +1 -0
- package/dist/ui/App.js +53 -14
- package/dist/ui/components.js +55 -21
- package/dist/ui/slashCommands.js +18 -5
- package/dist/ui/theme.js +12 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
# Rivine Code, in your terminal
|
|
2
2
|
|
|
3
|
+
Requires Node 20 or newer.
|
|
4
|
+
|
|
3
5
|
```
|
|
4
6
|
npm install -g @beunec/rivine
|
|
5
7
|
cd your-project
|
|
8
|
+
rivine login
|
|
6
9
|
rivine
|
|
7
10
|
```
|
|
8
11
|
|
|
@@ -42,8 +45,7 @@ stops it; the agent sees the non-zero exit and decides what to do about it.
|
|
|
42
45
|
|
|
43
46
|
The terminal comes from `node-pty`, an optional dependency. If it cannot be
|
|
44
47
|
installed or a PTY cannot be allocated, commands still run and still stream —
|
|
45
|
-
just without a terminal. Nothing fails
|
|
46
|
-
you want to know which one you have. `RIVINE_NO_PTY=1` forces the pipe path.
|
|
48
|
+
just without a terminal. Nothing fails. `RIVINE_NO_PTY=1` forces the pipe path.
|
|
47
49
|
|
|
48
50
|
What you see and what the model is sent are deliberately different. Escape codes
|
|
49
51
|
are stripped and redrawn lines are collapsed to their final state before the
|
|
@@ -51,33 +53,64 @@ output leaves your machine, so a progress bar that repainted four hundred times
|
|
|
51
53
|
costs one line rather than four hundred. This is why a real terminal does not
|
|
52
54
|
cost more per turn than not having one.
|
|
53
55
|
|
|
54
|
-
##
|
|
56
|
+
## Mode and Rate
|
|
57
|
+
|
|
58
|
+
Two choices, both required, and neither is inferred from what you type. There is
|
|
59
|
+
no router — no keyword matcher, no classifier, no extra model turn that reads
|
|
60
|
+
your request and picks a lane. A wrong guess sends a database migration to the
|
|
61
|
+
frontend agent and you never see the decision that was made for you.
|
|
62
|
+
|
|
63
|
+
### Mode — which agent runs
|
|
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.
|
|
68
|
+
|
|
69
|
+
| Mode | For | How to choose |
|
|
70
|
+
|---|---|---|
|
|
71
|
+
| `frontend` | UI and client tier | `1`, `-m frontend` |
|
|
72
|
+
| `backend` | Server, API, database | `2`, `-m backend` |
|
|
73
|
+
| `fullstack` | A change spanning every tier | `3`, `-m fullstack` |
|
|
74
|
+
| `simple` | Plan, ask, document, refresh, ship | `4`, `-m simple`, `--plan` |
|
|
75
|
+
| `security` | Find, verify, and patch vulnerabilities | `5`, `-m security` |
|
|
76
|
+
|
|
77
|
+
`simple` is the only Mode with live web access — its work is the work that
|
|
78
|
+
depends on the world outside the repository. The build Modes are measured on
|
|
79
|
+
whether the software runs.
|
|
80
|
+
|
|
81
|
+
### Rate — how edits apply
|
|
55
82
|
|
|
56
83
|
The engine runs on our servers. The decision about what happens to your machine
|
|
57
84
|
is made **here**, on your machine, before anything runs — never on our servers.
|
|
58
85
|
|
|
59
|
-
|
|
|
86
|
+
| Rate | Applies without asking | How to choose |
|
|
60
87
|
|---|---|---|
|
|
61
|
-
| `
|
|
62
|
-
| `
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
- **Anything outside the project.**
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
- **
|
|
80
|
-
|
|
88
|
+
| `manual` | Reads and read-only commands. Asks before every write, edit, or delete | `1`, `--rate manual` (default) |
|
|
89
|
+
| `bypass` | Writes and ordinary commands inside this project | `2`, `--rate bypass`, `--bypass` |
|
|
90
|
+
|
|
91
|
+
Three things ask in **both** Rates, because they are what you would want to see
|
|
92
|
+
even having said "just do it": credentials, anything outside the project
|
|
93
|
+
directory, and commands that are hard to undo (`rm -rf`, `git push --force`, a
|
|
94
|
+
deploy).
|
|
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.
|
|
99
|
+
|
|
100
|
+
What stays gated:
|
|
101
|
+
|
|
102
|
+
- **Anything outside the project.** Refused in `bypass` (set
|
|
103
|
+
`RIVINE_ALLOW_OUTSIDE_PROJECT=1` if you mean it). Other modes ask. The jail
|
|
104
|
+
resolves symlinks first, so a link pointing out of the directory is caught
|
|
105
|
+
rather than followed.
|
|
106
|
+
- **Credentials** — every mode except `bypass`. `.env`, `*.pem`, `~/.ssh`,
|
|
107
|
+
`~/.aws`, service-account JSON. These ask on *read* too — reading one puts
|
|
108
|
+
a live secret into a model prompt.
|
|
109
|
+
- **Commands that are hard to undo** — every mode except `bypass`. `rm -rf`,
|
|
110
|
+
`git push`, `--force`, `git reset --hard`, `sudo`, `npm publish`, any
|
|
111
|
+
deploy, `terraform apply`.
|
|
112
|
+
- **Batch writes** — every mode except `bypass`. Judged by their most
|
|
113
|
+
dangerous entry, so a safe first file cannot clear a whole batch.
|
|
81
114
|
|
|
82
115
|
One exemption: `.rivine/` — the agent's own notebook for this project, seeded at
|
|
83
116
|
session start and appended to at the end of each turn. It writes without asking
|
|
@@ -90,7 +123,8 @@ session start and appended to at the end of each turn. It writes without asking
|
|
|
90
123
|
rivine start a session in the current directory
|
|
91
124
|
rivine "add tests for auth.ts" one prompt, then exit
|
|
92
125
|
rivine -p "..." the same, explicitly
|
|
93
|
-
rivine
|
|
126
|
+
rivine -m frontend start in Frontend Only
|
|
127
|
+
rivine --plan shorthand for -m simple (Plan/Ask Only)
|
|
94
128
|
rivine -C ../other-project run against a different directory
|
|
95
129
|
|
|
96
130
|
rivine login sign in (opens your browser)
|
|
@@ -104,9 +138,10 @@ rivine mcp login <name> sign in to a remote MCP server
|
|
|
104
138
|
rivine --output-format json one JSON object on stdout, for CI
|
|
105
139
|
```
|
|
106
140
|
|
|
107
|
-
In a session: type `/` and press Tab. `/help` `/mode` `/plan` `/accept`
|
|
108
|
-
`/status` `/diff` `/pr` `/mcp` `/credits` `/clear` `/exit`.
|
|
109
|
-
|
|
141
|
+
In a session: type `/` and press Tab. `/help` `/agent` `/mode` `/plan` `/accept`
|
|
142
|
+
`/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.
|
|
110
145
|
|
|
111
146
|
`/diff` shows what changed in the working tree. `/pr` prepares a pull request
|
|
112
147
|
through your own `gh` CLI. It **never pushes without being asked**: on a
|
|
@@ -139,8 +174,8 @@ names the flag that would have allowed it. Granting write access because nobody
|
|
|
139
174
|
was watching is exactly backwards.
|
|
140
175
|
|
|
141
176
|
```
|
|
142
|
-
rivine -p "run the tests and fix what fails"
|
|
143
|
-
rivine -p "..."
|
|
177
|
+
rivine -m backend -p "run the tests and fix what fails"
|
|
178
|
+
rivine -m fullstack --rate bypass -p "..." # CI, on a machine you accept changes to
|
|
144
179
|
```
|
|
145
180
|
|
|
146
181
|
## Sign-in
|
|
@@ -262,11 +297,12 @@ pipe into `jq`:
|
|
|
262
297
|
{ "ok": true, "answer": "…", "error": null, "exit_code": 0, "files_written": ["src/a.ts"] }
|
|
263
298
|
```
|
|
264
299
|
|
|
265
|
-
Exit codes are `0` success, `1` error, `2` out of credits
|
|
266
|
-
|
|
267
|
-
terminal to give it. `3`
|
|
268
|
-
|
|
269
|
-
|
|
300
|
+
Exit codes are `0` success, `1` error, `2` out of credits (also if you run
|
|
301
|
+
`rivine` with no prompt and no terminal), and `3` nothing could be done —
|
|
302
|
+
every operation needed approval and there was no terminal to give it. `3`
|
|
303
|
+
exists because that run still costs a turn: without it the step would go
|
|
304
|
+
green having changed nothing. The result object carries `refused` alongside
|
|
305
|
+
`files_written` so a workflow can tell the two apart.
|
|
270
306
|
|
|
271
307
|
There is a composite action in `action.yml`:
|
|
272
308
|
|
|
@@ -315,10 +351,11 @@ is one, it will be linked here rather than described.
|
|
|
315
351
|
|
|
316
352
|
## Credits
|
|
317
353
|
|
|
318
|
-
The same wallet as the web app, checked by the same gate. Rivine Code needs
|
|
319
|
-
|
|
320
|
-
the top-up page rather than starting work it cannot finish.
|
|
321
|
-
the status bar show your balance.
|
|
354
|
+
The same wallet as the web app, checked by the same gate. Rivine Code needs a
|
|
355
|
+
minimum balance to start a turn (usually **$10.00**); below that the CLI says
|
|
356
|
+
so and links to the top-up page rather than starting work it cannot finish.
|
|
357
|
+
`rivine status` and the status bar show your balance. The bar refreshes after
|
|
358
|
+
each turn.
|
|
322
359
|
|
|
323
360
|
## Configuration
|
|
324
361
|
|
|
@@ -330,6 +367,9 @@ deployments:
|
|
|
330
367
|
| `RIVINE_API_URL` | `https://workspace.beunec.com` |
|
|
331
368
|
| `RIVINE_SITE_URL` | `https://aselius.beunec.com` |
|
|
332
369
|
| `RIVINE_TOKEN` | unset — a pre-issued token for CI |
|
|
370
|
+
| `RIVINE_NO_PTY` | unset — set to `1` to run commands without a PTY |
|
|
371
|
+
| `RIVINE_ALLOW_OUTSIDE_PROJECT` | unset — set to `1` only if `bypass` should be allowed to leave the project directory |
|
|
372
|
+
| `RIVINE_ASCII` | unset — set to `1` for ASCII glyphs (narrow panes, no Unicode) |
|
|
333
373
|
|
|
334
374
|
Auth0 parameters are fetched from `GET /api/cli/config` at sign-in, so this
|
|
335
375
|
package carries no tenant configuration and a client-id change never requires
|
package/action.yml
CHANGED
|
@@ -38,8 +38,16 @@ inputs:
|
|
|
38
38
|
required: true
|
|
39
39
|
mode:
|
|
40
40
|
description: >-
|
|
41
|
-
|
|
42
|
-
|
|
41
|
+
Which standalone agent runs: frontend, backend, fullstack, simple, or
|
|
42
|
+
security. Required — there is no router, and guessing would send a
|
|
43
|
+
database migration to the frontend agent without anyone seeing the
|
|
44
|
+
decision.
|
|
45
|
+
required: true
|
|
46
|
+
rate:
|
|
47
|
+
description: >-
|
|
48
|
+
How edits apply: manual or bypass. `bypass` is the default here because
|
|
49
|
+
there is nobody to approve anything; `manual` refuses every operation
|
|
50
|
+
that would prompt, which in CI means it stops.
|
|
43
51
|
required: false
|
|
44
52
|
default: bypass
|
|
45
53
|
working-directory:
|
|
@@ -98,6 +106,7 @@ runs:
|
|
|
98
106
|
# concerned. They are read as variables and never spliced into it.
|
|
99
107
|
RIVINE_PROMPT: ${{ inputs.prompt }}
|
|
100
108
|
RIVINE_MODE: ${{ inputs.mode }}
|
|
109
|
+
RIVINE_RATE: ${{ inputs.rate }}
|
|
101
110
|
# Read by the CLI in place of an interactive sign-in. Store it as a
|
|
102
111
|
# repository secret; it is an access token for a real account with a
|
|
103
112
|
# real credit balance.
|
|
@@ -115,13 +124,21 @@ runs:
|
|
|
115
124
|
exit 1
|
|
116
125
|
fi
|
|
117
126
|
|
|
118
|
-
# An allowlist, not a quoted interpolation:
|
|
119
|
-
# words, and anything else is a mistake worth stopping on
|
|
120
|
-
# passing to the CLI to reject later.
|
|
127
|
+
# An allowlist, not a quoted interpolation: each of these is one of a
|
|
128
|
+
# closed set of words, and anything else is a mistake worth stopping on
|
|
129
|
+
# rather than passing to the CLI to reject later.
|
|
121
130
|
case "$RIVINE_MODE" in
|
|
122
|
-
|
|
131
|
+
frontend|backend|fullstack|simple|security) ;;
|
|
132
|
+
*)
|
|
133
|
+
echo "::error::Unknown mode '$RIVINE_MODE'. Use frontend, backend, fullstack, simple or security."
|
|
134
|
+
exit 1
|
|
135
|
+
;;
|
|
136
|
+
esac
|
|
137
|
+
|
|
138
|
+
case "$RIVINE_RATE" in
|
|
139
|
+
manual|bypass) ;;
|
|
123
140
|
*)
|
|
124
|
-
echo "::error::Unknown
|
|
141
|
+
echo "::error::Unknown rate '$RIVINE_RATE'. Use manual or bypass."
|
|
125
142
|
exit 1
|
|
126
143
|
;;
|
|
127
144
|
esac
|
|
@@ -129,7 +146,7 @@ runs:
|
|
|
129
146
|
# stdout is exactly one JSON object in this mode; the readable
|
|
130
147
|
# transcript and any command output go to stderr, which is what shows
|
|
131
148
|
# up in the workflow log.
|
|
132
|
-
rivine --output-format json -m "$RIVINE_MODE" -p "$RIVINE_PROMPT" > "$RUNNER_TEMP/rivine.json"
|
|
149
|
+
rivine --output-format json -m "$RIVINE_MODE" --rate "$RIVINE_RATE" -p "$RIVINE_PROMPT" > "$RUNNER_TEMP/rivine.json"
|
|
133
150
|
status=$?
|
|
134
151
|
|
|
135
152
|
if [ ! -s "$RUNNER_TEMP/rivine.json" ]; then
|
package/dist/api/workspace.js
CHANGED
|
@@ -76,6 +76,9 @@ export async function openTurn(req) {
|
|
|
76
76
|
client: 'cli',
|
|
77
77
|
cli_session_id: req.sessionId,
|
|
78
78
|
permission_mode: req.permissionMode,
|
|
79
|
+
code_mode: req.codeMode ?? null,
|
|
80
|
+
code_rate: req.codeRate ?? null,
|
|
81
|
+
specialty: req.codeMode ?? null,
|
|
79
82
|
cwd_snapshot: req.cwdSnapshot ?? null,
|
|
80
83
|
mcp_tools: req.mcpTools ?? null,
|
|
81
84
|
cli_session_key: req.sessionKey ?? null,
|
package/dist/index.js
CHANGED
|
@@ -25,7 +25,7 @@ import { describeServer, loadServers, removeServer, saveServer } from './mcp/con
|
|
|
25
25
|
import { McpManager } from './mcp/manager.js';
|
|
26
26
|
import { loginToServer } from './mcp/oauth.js';
|
|
27
27
|
import { clearSession, mcpAccount } from './auth/keychain.js';
|
|
28
|
-
import {
|
|
28
|
+
import { CODE_MODES, RATES, parseCodeMode, rateToPermissionMode, resolveRate, } from './protocol/permissions.js';
|
|
29
29
|
const program = new Command();
|
|
30
30
|
function readPackageVersion() {
|
|
31
31
|
try {
|
|
@@ -45,11 +45,16 @@ function resolveMode(options) {
|
|
|
45
45
|
if (options.bypass)
|
|
46
46
|
return { mode: 'bypass', explicit: true };
|
|
47
47
|
if (options.plan)
|
|
48
|
-
return { mode: '
|
|
49
|
-
const
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
48
|
+
return { mode: 'default', explicit: true };
|
|
49
|
+
const rate = resolveRate(options.rate);
|
|
50
|
+
return { mode: rateToPermissionMode(rate), explicit: Boolean(options.rate) };
|
|
51
|
+
}
|
|
52
|
+
function resolveCodeMode(options) {
|
|
53
|
+
if (options.mode)
|
|
54
|
+
return parseCodeMode(options.mode);
|
|
55
|
+
if (options.plan)
|
|
56
|
+
return 'simple';
|
|
57
|
+
return null;
|
|
53
58
|
}
|
|
54
59
|
program
|
|
55
60
|
.command('login', { isDefault: false })
|
|
@@ -257,8 +262,9 @@ function collect(value, previous) {
|
|
|
257
262
|
program
|
|
258
263
|
.argument('[prompt...]', 'send one prompt and exit')
|
|
259
264
|
.option('-p, --print <prompt>', 'send one prompt, print the answer, and exit')
|
|
260
|
-
.option('-m, --mode <mode>', `
|
|
261
|
-
.option('--
|
|
265
|
+
.option('-m, --mode <mode>', `which agent runs: ${CODE_MODES.join(' | ')}`)
|
|
266
|
+
.option('--rate <rate>', `how edits apply: ${RATES.join(' | ')}`)
|
|
267
|
+
.option('--plan', 'shorthand for --mode simple (Plan/Ask Only)')
|
|
262
268
|
.option('--bypass', 'never ask for approval — for CI only, on a machine you are willing to have changed')
|
|
263
269
|
.option('-C, --cwd <dir>', 'run against a different directory')
|
|
264
270
|
.option('--output-format <format>', 'text | json — json puts one object on stdout and everything else on stderr', 'text')
|
|
@@ -266,25 +272,42 @@ program
|
|
|
266
272
|
const cwd = options.cwd ? resolvePath(options.cwd) : process.cwd();
|
|
267
273
|
const oneShot = options.print ?? (prompt.length ? prompt.join(' ') : undefined);
|
|
268
274
|
const { mode, explicit: modeExplicit } = resolveMode(options);
|
|
275
|
+
const codeMode = resolveCodeMode(options);
|
|
276
|
+
if (options.mode && !codeMode) {
|
|
277
|
+
console.error(`\n Unknown mode ${JSON.stringify(options.mode)}.\n` +
|
|
278
|
+
` Choose one of: ${CODE_MODES.join(', ')}\n`);
|
|
279
|
+
process.exitCode = 2;
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
269
282
|
const format = options.outputFormat === 'json' ? 'json' : 'text';
|
|
270
283
|
if (!isInteractiveTerminal()) {
|
|
271
284
|
if (!oneShot) {
|
|
272
285
|
console.error('\n No interactive terminal detected.\n' +
|
|
273
|
-
' Pass a prompt to run once: rivine -p "add tests for auth.ts"\n');
|
|
286
|
+
' Pass a prompt to run once: rivine -m backend -p "add tests for auth.ts"\n');
|
|
274
287
|
process.exitCode = 2;
|
|
275
288
|
return;
|
|
276
289
|
}
|
|
277
|
-
|
|
290
|
+
if (!codeMode) {
|
|
291
|
+
console.error('\n --mode is required without a terminal to ask on.\n' +
|
|
292
|
+
` Choose one of: ${CODE_MODES.join(', ')}\n` +
|
|
293
|
+
' e.g. rivine -m backend --rate bypass -p "add tests for auth.ts"\n');
|
|
294
|
+
process.exitCode = 2;
|
|
295
|
+
return;
|
|
296
|
+
}
|
|
297
|
+
process.exitCode = await runHeadless({ cwd, mode, codeMode, prompt: oneShot, format });
|
|
278
298
|
return;
|
|
279
299
|
}
|
|
280
300
|
if (format === 'json' && oneShot) {
|
|
281
|
-
process.exitCode = await runHeadless({
|
|
301
|
+
process.exitCode = await runHeadless({
|
|
302
|
+
cwd, mode, codeMode: codeMode ?? 'fullstack', prompt: oneShot, format,
|
|
303
|
+
});
|
|
282
304
|
return;
|
|
283
305
|
}
|
|
284
306
|
const { waitUntilExit } = render(React.createElement(App, {
|
|
285
307
|
cwd,
|
|
286
308
|
initialMode: mode,
|
|
287
309
|
askForMode: !modeExplicit && !oneShot,
|
|
310
|
+
initialCodeMode: codeMode ?? (oneShot ? 'fullstack' : undefined),
|
|
288
311
|
oneShot,
|
|
289
312
|
}), { exitOnCtrlC: false });
|
|
290
313
|
const removeTeardown = installTeardown(() => { });
|
|
@@ -8,6 +8,44 @@
|
|
|
8
8
|
* accompanying it; it is not open source and is not licensed for
|
|
9
9
|
* redistribution or derivative works.
|
|
10
10
|
*/
|
|
11
|
+
export const RATES = ['manual', 'bypass'];
|
|
12
|
+
export const CYCLE_RATES = ['manual', 'bypass'];
|
|
13
|
+
export function resolveRate(raw) {
|
|
14
|
+
const key = (raw ?? '').trim().toLowerCase().replace(/[\s_\-]+/g, '');
|
|
15
|
+
if (key === 'bypass' || key === 'auto' || key === 'yolo' || key === 'bypasspermissions') {
|
|
16
|
+
return 'bypass';
|
|
17
|
+
}
|
|
18
|
+
return 'manual';
|
|
19
|
+
}
|
|
20
|
+
export function rateToPermissionMode(rate) {
|
|
21
|
+
return rate === 'bypass' ? 'bypass' : 'default';
|
|
22
|
+
}
|
|
23
|
+
export function permissionModeToRate(mode) {
|
|
24
|
+
return mode === 'bypass' ? 'bypass' : 'manual';
|
|
25
|
+
}
|
|
26
|
+
export const CODE_MODES = [
|
|
27
|
+
'frontend', 'backend', 'fullstack', 'simple', 'security',
|
|
28
|
+
];
|
|
29
|
+
export const CODE_MODE_LABELS = {
|
|
30
|
+
frontend: 'Frontend Only',
|
|
31
|
+
backend: 'Backend Only',
|
|
32
|
+
fullstack: 'Full Stack',
|
|
33
|
+
simple: 'Plan/Ask Only',
|
|
34
|
+
security: 'Security Scan/Fix',
|
|
35
|
+
};
|
|
36
|
+
export function parseCodeMode(raw) {
|
|
37
|
+
const key = (raw ?? '').trim().toLowerCase().replace(/[\s_\-\/]+/g, '');
|
|
38
|
+
const table = {
|
|
39
|
+
frontend: 'frontend', frontendonly: 'frontend', fe: 'frontend',
|
|
40
|
+
backend: 'backend', backendonly: 'backend', database: 'backend', be: 'backend',
|
|
41
|
+
backenddatabase: 'backend', backendanddatabase: 'backend', db: 'backend',
|
|
42
|
+
fullstack: 'fullstack', hybrid: 'fullstack', softwareengineer: 'fullstack',
|
|
43
|
+
frontendbackenddatabase: 'fullstack',
|
|
44
|
+
simple: 'simple', plan: 'simple', ask: 'simple', planaskonly: 'simple', docs: 'simple',
|
|
45
|
+
security: 'security', securityscanfix: 'security', audit: 'security', sec: 'security',
|
|
46
|
+
};
|
|
47
|
+
return table[key] ?? null;
|
|
48
|
+
}
|
|
11
49
|
export const PERMISSION_MODES = ['plan', 'default', 'acceptEdits', 'bypass'];
|
|
12
50
|
export const CYCLE_MODES = ['plan', 'default', 'acceptEdits'];
|
|
13
51
|
export function isBatchable(category) {
|
|
@@ -209,19 +247,20 @@ export function decide(input) {
|
|
|
209
247
|
return { verdict: 'ask', category: 'command', reason: 'unrecognised operation' };
|
|
210
248
|
}
|
|
211
249
|
export function nextMode(current) {
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
250
|
+
return permissionModeToRate(current) === 'bypass'
|
|
251
|
+
? rateToPermissionMode('manual')
|
|
252
|
+
: rateToPermissionMode('bypass');
|
|
253
|
+
}
|
|
254
|
+
export function nextRate(current) {
|
|
255
|
+
return current === 'bypass' ? 'manual' : 'bypass';
|
|
216
256
|
}
|
|
217
257
|
export function modeName(mode) {
|
|
218
258
|
switch (mode) {
|
|
219
259
|
case 'plan':
|
|
220
260
|
return 'plan';
|
|
221
261
|
case 'default':
|
|
222
|
-
return 'default';
|
|
223
262
|
case 'acceptEdits':
|
|
224
|
-
return '
|
|
263
|
+
return 'manual';
|
|
225
264
|
case 'bypass':
|
|
226
265
|
return 'bypass';
|
|
227
266
|
}
|
|
@@ -231,10 +270,23 @@ export function describeMode(mode) {
|
|
|
231
270
|
case 'plan':
|
|
232
271
|
return 'plan · reads only';
|
|
233
272
|
case 'default':
|
|
234
|
-
return 'default · asks before changes';
|
|
235
273
|
case 'acceptEdits':
|
|
236
|
-
return '
|
|
274
|
+
return 'manual · asks before edits';
|
|
237
275
|
case 'bypass':
|
|
238
|
-
return 'bypass ·
|
|
276
|
+
return 'bypass · edits without asking';
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
export function codeModeName(mode) {
|
|
280
|
+
switch (mode) {
|
|
281
|
+
case 'frontend':
|
|
282
|
+
return 'Frontend';
|
|
283
|
+
case 'backend':
|
|
284
|
+
return 'Backend';
|
|
285
|
+
case 'fullstack':
|
|
286
|
+
return 'Full Stack';
|
|
287
|
+
case 'simple':
|
|
288
|
+
return 'Plan/Ask';
|
|
289
|
+
case 'security':
|
|
290
|
+
return 'Security';
|
|
239
291
|
}
|
|
240
292
|
}
|
package/dist/protocol/runner.js
CHANGED
|
@@ -13,6 +13,7 @@ import { apiUrl } from '../config.js';
|
|
|
13
13
|
import { accessToken } from '../auth/session.js';
|
|
14
14
|
import { classify, execute } from './fsExecutor.js';
|
|
15
15
|
import { isSealedEnvelope, SessionKey } from './sessionKey.js';
|
|
16
|
+
import { permissionModeToRate } from './permissions.js';
|
|
16
17
|
import { selectRelevantTools } from '../mcp/relevance.js';
|
|
17
18
|
import { readTurnStream } from './stream.js';
|
|
18
19
|
const PENDING_POLL_MS = 1200;
|
|
@@ -36,6 +37,9 @@ export class TurnRunner {
|
|
|
36
37
|
setMode(mode) {
|
|
37
38
|
this.ctx.mode = mode;
|
|
38
39
|
}
|
|
40
|
+
setCodeMode(mode) {
|
|
41
|
+
this.ctx.codeMode = mode;
|
|
42
|
+
}
|
|
39
43
|
writeToShell(data) {
|
|
40
44
|
if (!this.shell)
|
|
41
45
|
return false;
|
|
@@ -82,6 +86,8 @@ export class TurnRunner {
|
|
|
82
86
|
sessionId: this.ctx.sessionId,
|
|
83
87
|
message,
|
|
84
88
|
permissionMode: this.ctx.mode,
|
|
89
|
+
codeMode: this.ctx.codeMode ?? null,
|
|
90
|
+
codeRate: permissionModeToRate(this.ctx.mode),
|
|
85
91
|
cwdSnapshot: snapshot,
|
|
86
92
|
mcpTools: selection.tools.map(({ name, description, input_schema }) => ({
|
|
87
93
|
name,
|
|
@@ -115,6 +121,23 @@ export class TurnRunner {
|
|
|
115
121
|
this.cb.onTool(String(e.name ?? 'tool'), String(detail));
|
|
116
122
|
break;
|
|
117
123
|
}
|
|
124
|
+
case 'file_diff': {
|
|
125
|
+
const e = event;
|
|
126
|
+
const path = String(e.path ?? '').trim();
|
|
127
|
+
if (!path)
|
|
128
|
+
break;
|
|
129
|
+
this.cb.onFileDiff?.({
|
|
130
|
+
path: path.replace(/^\/workspace\/?/, ''),
|
|
131
|
+
stat: String(e.stat ?? ''),
|
|
132
|
+
patch: String(e.patch ?? ''),
|
|
133
|
+
added: Number(e.added ?? 0),
|
|
134
|
+
removed: Number(e.removed ?? 0),
|
|
135
|
+
isNew: Boolean(e.is_new),
|
|
136
|
+
isDelete: Boolean(e.is_delete),
|
|
137
|
+
binary: Boolean(e.binary),
|
|
138
|
+
});
|
|
139
|
+
break;
|
|
140
|
+
}
|
|
118
141
|
case 'code_file': {
|
|
119
142
|
const e = event;
|
|
120
143
|
const path = String(e.path ?? '').trim();
|
package/dist/runHeadless.js
CHANGED
package/dist/ui/App.js
CHANGED
|
@@ -16,11 +16,11 @@ import { fetchCredits, InsufficientCredits } from '../api/workspace.js';
|
|
|
16
16
|
import { openBrowser } from '../auth/pkce.js';
|
|
17
17
|
import { siteUrl } from '../config.js';
|
|
18
18
|
import { TurnRunner } from '../protocol/runner.js';
|
|
19
|
-
import { describeMode, isBatchable, mcpServerOf, nextMode, } from '../protocol/permissions.js';
|
|
19
|
+
import { CODE_MODE_LABELS, describeMode, isBatchable, mcpServerOf, nextMode, } from '../protocol/permissions.js';
|
|
20
20
|
import { creditShortfall, startSession } from '../session.js';
|
|
21
21
|
import { sanitizeForModel } from '../protocol/shell.js';
|
|
22
22
|
import { diff as gitDiff, gitBranchLine, prCreate } from '../protocol/git.js';
|
|
23
|
-
import { Activity, ApprovalPrompt, Banner,
|
|
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
25
|
import { color } from './theme.js';
|
|
26
26
|
import { completeInput, completionsFor } from './slashCommands.js';
|
|
@@ -36,9 +36,11 @@ function grantKey(category, request) {
|
|
|
36
36
|
return category;
|
|
37
37
|
return `mcp:${mcpServerOf(String(request.args?.tool ?? '')) ?? 'unknown'}`;
|
|
38
38
|
}
|
|
39
|
-
export function App({ cwd, initialMode, askForMode, oneShot }) {
|
|
39
|
+
export function App({ cwd, initialMode, askForMode, initialCodeMode, oneShot }) {
|
|
40
40
|
const { exit } = useApp();
|
|
41
|
-
const [
|
|
41
|
+
const [codeMode, setCodeMode] = useState(initialCodeMode);
|
|
42
|
+
const codeModeChosenRef = useRef(Boolean(initialCodeMode));
|
|
43
|
+
const [phase, setPhase] = useState(initialCodeMode ? (askForMode ? 'mode' : 'starting') : 'codeMode');
|
|
42
44
|
const sessionReadyRef = useRef(false);
|
|
43
45
|
const modeChosenRef = useRef(!askForMode);
|
|
44
46
|
const [lines, setLines] = useState([]);
|
|
@@ -112,12 +114,24 @@ export function App({ cwd, initialMode, askForMode, oneShot }) {
|
|
|
112
114
|
catch {
|
|
113
115
|
}
|
|
114
116
|
}, []);
|
|
117
|
+
const pickCodeMode = useCallback((chosen) => {
|
|
118
|
+
setCodeMode(chosen);
|
|
119
|
+
codeModeChosenRef.current = true;
|
|
120
|
+
runnerRef.current?.setCodeMode(chosen);
|
|
121
|
+
push('system', `Mode: ${CODE_MODE_LABELS[chosen]}`);
|
|
122
|
+
setPhase(modeChosenRef.current
|
|
123
|
+
? (sessionReadyRef.current ? 'ready' : 'starting')
|
|
124
|
+
: 'mode');
|
|
125
|
+
}, [push]);
|
|
126
|
+
const cancelCodeModePick = useCallback(() => {
|
|
127
|
+
setPhase(sessionReadyRef.current ? 'ready' : 'starting');
|
|
128
|
+
}, []);
|
|
115
129
|
const pickMode = useCallback((chosen) => {
|
|
116
130
|
setMode(chosen);
|
|
117
131
|
modeChosenRef.current = true;
|
|
118
132
|
runnerRef.current?.setMode(chosen);
|
|
119
133
|
setPhase(sessionReadyRef.current ? 'ready' : 'starting');
|
|
120
|
-
push('system', `
|
|
134
|
+
push('system', `Rate: ${describeMode(chosen)}`);
|
|
121
135
|
if (chosen === 'bypass') {
|
|
122
136
|
push('notice', 'Running without prompts. Files outside this project are still refused.');
|
|
123
137
|
}
|
|
@@ -132,7 +146,7 @@ export function App({ cwd, initialMode, askForMode, oneShot }) {
|
|
|
132
146
|
const cycleMode = useCallback(() => {
|
|
133
147
|
setMode((current) => {
|
|
134
148
|
const next = nextMode(current);
|
|
135
|
-
push('system', `
|
|
149
|
+
push('system', `Rate: ${describeMode(next)}`);
|
|
136
150
|
return next;
|
|
137
151
|
});
|
|
138
152
|
}, [push]);
|
|
@@ -154,16 +168,23 @@ export function App({ cwd, initialMode, askForMode, oneShot }) {
|
|
|
154
168
|
setShowHelp(false);
|
|
155
169
|
setScreenGeneration((n) => n + 1);
|
|
156
170
|
return true;
|
|
171
|
+
case '/agent':
|
|
172
|
+
setPhase('codeMode');
|
|
173
|
+
return true;
|
|
157
174
|
case '/mode':
|
|
158
175
|
setPhase('mode');
|
|
159
176
|
return true;
|
|
160
177
|
case '/plan':
|
|
161
|
-
|
|
162
|
-
|
|
178
|
+
setCodeMode('simple');
|
|
179
|
+
codeModeChosenRef.current = true;
|
|
180
|
+
runnerRef.current?.setCodeMode('simple');
|
|
181
|
+
push('system', `Mode: ${CODE_MODE_LABELS.simple}`);
|
|
163
182
|
return true;
|
|
164
183
|
case '/accept':
|
|
165
|
-
setMode('
|
|
166
|
-
|
|
184
|
+
setMode('bypass');
|
|
185
|
+
runnerRef.current?.setMode('bypass');
|
|
186
|
+
push('system', `Rate: ${describeMode('bypass')}`);
|
|
187
|
+
push('notice', 'Accept-edits is gone — Bypass applies in-project edits without asking.');
|
|
167
188
|
return true;
|
|
168
189
|
case '/credits':
|
|
169
190
|
openBrowser(`${siteUrl()}/credits`);
|
|
@@ -183,7 +204,8 @@ export function App({ cwd, initialMode, askForMode, oneShot }) {
|
|
|
183
204
|
}
|
|
184
205
|
case '/status': {
|
|
185
206
|
push('system', `${status.cloudhandle || 'signed in'} ${status.bcidMasked} · ${status.project} · ` +
|
|
186
|
-
`${status.balance !== null ? `${status.balance.toFixed(2)}` : 'balance unknown'} ·
|
|
207
|
+
`${status.balance !== null ? `${status.balance.toFixed(2)}` : 'balance unknown'} · ` +
|
|
208
|
+
`${codeMode ? CODE_MODE_LABELS[codeMode] : 'Mode unset'} · ${describeMode(mode)}`);
|
|
187
209
|
const facts = sessionFactsRef.current;
|
|
188
210
|
push('system', `${facts.continuing ? "continuing this directory's thread" : 'new thread'} · ` +
|
|
189
211
|
`${facts.hasLocalMemory ? 'project notes on disk' : 'no project notes yet'}`);
|
|
@@ -213,7 +235,7 @@ export function App({ cwd, initialMode, askForMode, oneShot }) {
|
|
|
213
235
|
push('notice', `Unknown command ${cmd} — /help for the list.`);
|
|
214
236
|
return true;
|
|
215
237
|
}
|
|
216
|
-
}, [cwd, cycleMode, mode, push, quit, refreshBalance, status]);
|
|
238
|
+
}, [codeMode, cwd, cycleMode, mode, push, quit, refreshBalance, status]);
|
|
217
239
|
const submit = useCallback(async (message) => {
|
|
218
240
|
const text = message.trim();
|
|
219
241
|
setPromptText('');
|
|
@@ -324,6 +346,22 @@ export function App({ cwd, initialMode, askForMode, oneShot }) {
|
|
|
324
346
|
push('file', path.replace(/^\/workspace\//, ''));
|
|
325
347
|
}
|
|
326
348
|
},
|
|
349
|
+
onFileDiff: (diff) => {
|
|
350
|
+
settleLive();
|
|
351
|
+
if (diff.binary) {
|
|
352
|
+
push('file', `${diff.path} (binary)`);
|
|
353
|
+
return;
|
|
354
|
+
}
|
|
355
|
+
push('file', diff.stat || diff.path);
|
|
356
|
+
for (const line of diff.patch.split('\n')) {
|
|
357
|
+
if (!line || line.startsWith('---') || line.startsWith('+++'))
|
|
358
|
+
continue;
|
|
359
|
+
push(line.startsWith('+') ? 'diff-add'
|
|
360
|
+
: line.startsWith('-') ? 'diff-del'
|
|
361
|
+
: line.startsWith('@@') ? 'diff-hunk'
|
|
362
|
+
: 'diff-ctx', line);
|
|
363
|
+
}
|
|
364
|
+
},
|
|
327
365
|
onNotice: (text) => push('notice', text),
|
|
328
366
|
onOutput: (text) => {
|
|
329
367
|
shellBuf.current = trimToPane(shellBuf.current + sanitizeForModel(text));
|
|
@@ -385,7 +423,7 @@ export function App({ cwd, initialMode, askForMode, oneShot }) {
|
|
|
385
423
|
push('notice', 'Project notes found in .rivine/local_memory.md.');
|
|
386
424
|
}
|
|
387
425
|
if (!oneShot && modeChosenRef.current && !askForMode) {
|
|
388
|
-
push('system', `
|
|
426
|
+
push('system', `Rate: ${describeMode(mode)}`);
|
|
389
427
|
push('system', 'Ask for anything. /help for commands.');
|
|
390
428
|
}
|
|
391
429
|
if (oneShot)
|
|
@@ -487,10 +525,11 @@ export function App({ cwd, initialMode, askForMode, oneShot }) {
|
|
|
487
525
|
const compactBanner = terminalWidth() < WORDMARK_MIN_COLUMNS || Boolean(oneShot);
|
|
488
526
|
const matches = completionsFor(input);
|
|
489
527
|
const busy = phase !== 'ready';
|
|
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(
|
|
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
|
|
491
529
|
? { approved: true, feedback: value }
|
|
492
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: {
|
|
493
531
|
mode,
|
|
532
|
+
codeMode,
|
|
494
533
|
project: status.project,
|
|
495
534
|
cloudhandle: status.cloudhandle,
|
|
496
535
|
bcidMasked: status.bcidMasked,
|
package/dist/ui/components.js
CHANGED
|
@@ -15,7 +15,7 @@ import { Spinner } from '@inkjs/ui';
|
|
|
15
15
|
import { ASCII_ONLY, BOX, color, glyph, lineColor } from './theme.js';
|
|
16
16
|
import { fit, terminalWidth, wrap, terminalColumns } from './render.js';
|
|
17
17
|
import { SLASH_COMMANDS } from './slashCommands.js';
|
|
18
|
-
import { describeMode, modeName } from '../protocol/permissions.js';
|
|
18
|
+
import { codeModeName, describeMode, modeName, } from '../protocol/permissions.js';
|
|
19
19
|
const WORDMARK = [
|
|
20
20
|
'█▀▄ ▀█▀ █ █ ▀█▀ █▄ █ █▀▀ █▀▀ █▀█ █▀▄ █▀▀',
|
|
21
21
|
'█▀▄ █ ▀▄ ▄▀ █ █ ▀█ █▀ █ █ █ █ █ █▀ ',
|
|
@@ -40,13 +40,38 @@ export function Activity({ stage, tool }) {
|
|
|
40
40
|
const label = tool ? `${stage} · ${tool}` : stage;
|
|
41
41
|
return (_jsx(Box, { paddingX: 1, children: _jsx(Spinner, { label: fit(label, width - 6) }) }));
|
|
42
42
|
}
|
|
43
|
-
export function
|
|
43
|
+
export function CodeModePicker({ onPick, current, onCancel, }) {
|
|
44
44
|
const width = terminalWidth();
|
|
45
45
|
const options = [
|
|
46
|
-
{ key: '1', mode: '
|
|
47
|
-
{ key: '2', mode: '
|
|
48
|
-
{ key: '3', mode: '
|
|
49
|
-
{ key: '4', mode: '
|
|
46
|
+
{ key: '1', mode: 'frontend', label: 'Frontend Only', detail: 'UI and client tier' },
|
|
47
|
+
{ key: '2', mode: 'backend', label: 'Backend Only', detail: 'server, API, and database' },
|
|
48
|
+
{ key: '3', mode: 'fullstack', label: 'Full Stack', detail: 'across every tier in one turn' },
|
|
49
|
+
{ key: '4', mode: 'simple', label: 'Plan/Ask Only', detail: 'plan, answer, document, refresh, ship' },
|
|
50
|
+
{ key: '5', mode: 'security', label: 'Security Scan/Fix', detail: 'find, verify, and patch' },
|
|
51
|
+
];
|
|
52
|
+
useInput((input, key) => {
|
|
53
|
+
if (key.escape && onCancel) {
|
|
54
|
+
onCancel();
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
if (key.return && current) {
|
|
58
|
+
onPick(current);
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
const found = options.find((o) => o.key === input);
|
|
62
|
+
if (found)
|
|
63
|
+
onPick(found.mode);
|
|
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) => {
|
|
66
|
+
const active = o.mode === current;
|
|
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
|
+
}) }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { color: color('faint'), children: current ? 'esc keeps the current mode' : 'pick a number to begin' }) })] }));
|
|
69
|
+
}
|
|
70
|
+
export function RatePicker({ onPick, current, onCancel, }) {
|
|
71
|
+
const width = terminalWidth();
|
|
72
|
+
const options = [
|
|
73
|
+
{ key: '1', mode: 'default', label: 'Manual', detail: 'ask before each edit' },
|
|
74
|
+
{ key: '2', mode: 'bypass', label: 'Bypass', detail: 'edit in this project without asking' },
|
|
50
75
|
];
|
|
51
76
|
useInput((input, key) => {
|
|
52
77
|
if (key.escape && onCancel) {
|
|
@@ -54,21 +79,21 @@ export function ModePicker({ onPick, current, onCancel, }) {
|
|
|
54
79
|
return;
|
|
55
80
|
}
|
|
56
81
|
if (key.return) {
|
|
57
|
-
onPick(current ?? '
|
|
82
|
+
onPick(current ?? 'default');
|
|
58
83
|
return;
|
|
59
84
|
}
|
|
60
85
|
const found = options.find((o) => o.key === input);
|
|
61
86
|
if (found)
|
|
62
87
|
onPick(found.mode);
|
|
63
88
|
});
|
|
64
|
-
return (_jsxs(Box, { flexDirection: "column", paddingX: 1, paddingY: 1, children: [_jsx(Text, { color: color('text'), bold: true, children: "
|
|
89
|
+
return (_jsxs(Box, { flexDirection: "column", paddingX: 1, paddingY: 1, children: [_jsx(Text, { color: color('text'), bold: true, children: "Rate \u2014 how should Rivine apply edits?" }), _jsx(Box, { flexDirection: "column", marginTop: 1, children: options.map((o) => {
|
|
65
90
|
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(
|
|
91
|
+
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(8) }), _jsx(Text, { color: color('faint'), children: " \u2014 " }), _jsx(Text, { color: color('muted'), children: fit(o.detail, Math.max(10, width - 24)) })] }, o.key));
|
|
67
92
|
}) }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { color: color('faint'), children: fit(current
|
|
68
|
-
?
|
|
69
|
-
|
|
70
|
-
: 'press a number · enter for accept edits · shift+tab changes it later', width - 4) }) })] }));
|
|
93
|
+
? `press a number · enter keeps ${options.find((o) => o.mode === current)?.label ?? 'this'} · esc cancels`
|
|
94
|
+
: 'press a number · enter for Manual · shift+tab toggles later', width - 4) }) })] }));
|
|
71
95
|
}
|
|
96
|
+
export const ModePicker = RatePicker;
|
|
72
97
|
export const SHELL_PANE_LINES = 12;
|
|
73
98
|
export function ShellPane({ text, live }) {
|
|
74
99
|
if (!text)
|
|
@@ -112,7 +137,7 @@ export function Completions({ matches }) {
|
|
|
112
137
|
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] }));
|
|
113
138
|
}
|
|
114
139
|
export function HelpPanel() {
|
|
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
|
|
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" })] })] }));
|
|
116
141
|
}
|
|
117
142
|
export function StatusBar({ info }) {
|
|
118
143
|
const width = terminalColumns();
|
|
@@ -132,18 +157,27 @@ export function StatusBar({ info }) {
|
|
|
132
157
|
segments.push({ node, text });
|
|
133
158
|
return true;
|
|
134
159
|
};
|
|
135
|
-
const
|
|
136
|
-
const
|
|
160
|
+
const rateText = describeMode(info.mode);
|
|
161
|
+
const rateShort = modeName(info.mode);
|
|
162
|
+
const agentText = info.codeMode ? codeModeName(info.codeMode) : '';
|
|
137
163
|
const brand = 'Rivine';
|
|
138
|
-
const
|
|
139
|
-
|
|
164
|
+
const fitsBrandAndFullRate = 2 +
|
|
165
|
+
brand.length +
|
|
166
|
+
(agentText ? 3 + agentText.length : 0) +
|
|
167
|
+
3 +
|
|
168
|
+
rateText.length <=
|
|
169
|
+
width - 2;
|
|
170
|
+
if (2 + brand.length + 3 + rateShort.length <= width - 2) {
|
|
140
171
|
add(brand, (_jsx(Text, { color: color('brand'), bold: true, children: brand })));
|
|
141
172
|
}
|
|
142
|
-
|
|
143
|
-
|
|
173
|
+
if (agentText) {
|
|
174
|
+
add(agentText, _jsx(Text, { color: color('accent'), children: agentText }));
|
|
175
|
+
}
|
|
176
|
+
const shownRate = fitsBrandAndFullRate ? rateText : rateShort;
|
|
177
|
+
used += (segments.length > 0 ? 3 : 0) + shownRate.length;
|
|
144
178
|
segments.push({
|
|
145
|
-
node: _jsx(Text, { color: color('muted'), children:
|
|
146
|
-
text:
|
|
179
|
+
node: _jsx(Text, { color: color('muted'), children: shownRate }),
|
|
180
|
+
text: shownRate,
|
|
147
181
|
});
|
|
148
182
|
if (info.balance !== null) {
|
|
149
183
|
const money = `$${info.balance.toFixed(2)}`;
|
package/dist/ui/slashCommands.js
CHANGED
|
@@ -10,14 +10,27 @@
|
|
|
10
10
|
*/
|
|
11
11
|
export const SLASH_COMMANDS = [
|
|
12
12
|
{ name: '/help', summary: 'show these commands' },
|
|
13
|
+
{
|
|
14
|
+
name: '/agent',
|
|
15
|
+
summary: 'pick which engine Mode runs',
|
|
16
|
+
detail: 'Frontend, Backend, Full Stack, Plan/Ask, or Security.',
|
|
17
|
+
},
|
|
13
18
|
{
|
|
14
19
|
name: '/mode',
|
|
15
|
-
summary: '
|
|
16
|
-
detail: 'Shift+Tab
|
|
20
|
+
summary: 'pick the Rate — Manual or Bypass',
|
|
21
|
+
detail: 'Shift+Tab toggles Manual ⇄ Bypass.',
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
name: '/plan',
|
|
25
|
+
summary: 'switch to Plan/Ask Only Mode',
|
|
26
|
+
detail: 'Same as picking Plan/Ask in /agent.',
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
name: '/accept',
|
|
30
|
+
summary: 'switch Rate to Bypass',
|
|
31
|
+
detail: 'Accept-edits is gone; Bypass applies in-project edits without asking.',
|
|
17
32
|
},
|
|
18
|
-
{ name: '/
|
|
19
|
-
{ name: '/accept', summary: 'switch to accept-edits mode' },
|
|
20
|
-
{ name: '/status', summary: 'account, project and balance' },
|
|
33
|
+
{ name: '/status', summary: 'account, project, Mode, Rate and balance' },
|
|
21
34
|
{ name: '/diff', summary: 'what changed in the working tree' },
|
|
22
35
|
{
|
|
23
36
|
name: '/pr',
|
package/dist/ui/theme.js
CHANGED
|
@@ -49,6 +49,10 @@ const GLYPHS_UNICODE = {
|
|
|
49
49
|
notice: '!',
|
|
50
50
|
error: '✗',
|
|
51
51
|
system: '◆',
|
|
52
|
+
'diff-add': ' ',
|
|
53
|
+
'diff-del': ' ',
|
|
54
|
+
'diff-hunk': ' ',
|
|
55
|
+
'diff-ctx': ' ',
|
|
52
56
|
};
|
|
53
57
|
const GLYPHS_ASCII = {
|
|
54
58
|
user: '>',
|
|
@@ -59,6 +63,10 @@ const GLYPHS_ASCII = {
|
|
|
59
63
|
notice: '!',
|
|
60
64
|
error: 'x',
|
|
61
65
|
system: '#',
|
|
66
|
+
'diff-add': ' ',
|
|
67
|
+
'diff-del': ' ',
|
|
68
|
+
'diff-hunk': ' ',
|
|
69
|
+
'diff-ctx': ' ',
|
|
62
70
|
};
|
|
63
71
|
export const ASCII_ONLY = process.env.RIVINE_ASCII === '1';
|
|
64
72
|
export function glyph(kind) {
|
|
@@ -73,6 +81,10 @@ const LINE_ROLE = {
|
|
|
73
81
|
notice: 'warn',
|
|
74
82
|
error: 'danger',
|
|
75
83
|
system: 'brand',
|
|
84
|
+
'diff-add': 'success',
|
|
85
|
+
'diff-del': 'danger',
|
|
86
|
+
'diff-hunk': 'brand',
|
|
87
|
+
'diff-ctx': 'faint',
|
|
76
88
|
};
|
|
77
89
|
export function lineColor(kind) {
|
|
78
90
|
return color(LINE_ROLE[kind]);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@beunec/rivine",
|
|
3
|
-
"version": "0.1
|
|
3
|
+
"version": "0.2.1",
|
|
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",
|