@beunec/rivine 0.1.4 → 0.2.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 +78 -38
- package/action.yml +25 -8
- package/dist/api/workspace.js +3 -0
- package/dist/index.js +34 -11
- package/dist/protocol/permissions.js +56 -4
- package/dist/protocol/runner.js +23 -0
- package/dist/runHeadless.js +1 -0
- package/dist/ui/App.js +60 -9
- package/dist/ui/components.js +91 -12
- package/dist/ui/render.js +4 -0
- package/dist/ui/theme.js +12 -0
- package/package.json +2 -2
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)
|
|
@@ -105,8 +139,9 @@ rivine --output-format json one JSON object on stdout, for CI
|
|
|
105
139
|
```
|
|
106
140
|
|
|
107
141
|
In a session: type `/` and press Tab. `/help` `/mode` `/plan` `/accept`
|
|
108
|
-
`/status` `/diff` `/pr` `/mcp` `/credits` `/clear` `/exit`.
|
|
109
|
-
permission
|
|
142
|
+
`/status` `/diff` `/pr` `/mcp` `/credits` `/clear` `/exit`. `/mode` opens the
|
|
143
|
+
permission picker. Shift+Tab cycles plan / default / accept edits. Ctrl+C
|
|
144
|
+
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(' | ')}`, 'manual')
|
|
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,10 +247,24 @@ 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';
|
|
256
|
+
}
|
|
257
|
+
export function modeName(mode) {
|
|
258
|
+
switch (mode) {
|
|
259
|
+
case 'plan':
|
|
260
|
+
return 'plan';
|
|
261
|
+
case 'default':
|
|
262
|
+
return 'default';
|
|
263
|
+
case 'acceptEdits':
|
|
264
|
+
return 'accept edits';
|
|
265
|
+
case 'bypass':
|
|
266
|
+
return 'bypass';
|
|
267
|
+
}
|
|
216
268
|
}
|
|
217
269
|
export function describeMode(mode) {
|
|
218
270
|
switch (mode) {
|
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,28 +16,31 @@ 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, ModePicker, WORDMARK_MIN_COLUMNS, ChoicePrompt, Completions, HelpPanel, StatusBar, TranscriptLine, ShellPane, SHELL_PANE_LINES, } from './components.js';
|
|
23
|
+
import { Activity, ApprovalPrompt, Banner, CodeModePicker, 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;
|
|
36
37
|
return `mcp:${mcpServerOf(String(request.args?.tool ?? '')) ?? 'unknown'}`;
|
|
37
38
|
}
|
|
38
|
-
export function App({ cwd, initialMode, askForMode, oneShot }) {
|
|
39
|
+
export function App({ cwd, initialMode, askForMode, initialCodeMode, oneShot }) {
|
|
39
40
|
const { exit } = useApp();
|
|
40
|
-
const [
|
|
41
|
+
const [codeMode, setCodeMode] = useState(initialCodeMode);
|
|
42
|
+
const codeModeChosenRef = useRef(Boolean(initialCodeMode));
|
|
43
|
+
const [phase, setPhase] = useState(initialCodeMode ? (askForMode ? 'mode' : 'starting') : 'codeMode');
|
|
41
44
|
const sessionReadyRef = useRef(false);
|
|
42
45
|
const modeChosenRef = useRef(!askForMode);
|
|
43
46
|
const [lines, setLines] = useState([]);
|
|
@@ -48,6 +51,14 @@ export function App({ cwd, initialMode, askForMode, oneShot }) {
|
|
|
48
51
|
const [tool, setTool] = useState('');
|
|
49
52
|
const [mode, setMode] = useState(initialMode);
|
|
50
53
|
const [input, setInput] = useState('');
|
|
54
|
+
const [inputSeed, setInputSeed] = useState('');
|
|
55
|
+
const [inputNonce, setInputNonce] = useState(0);
|
|
56
|
+
const [screenGeneration, setScreenGeneration] = useState(0);
|
|
57
|
+
const setPromptText = useCallback((text) => {
|
|
58
|
+
setInput(text);
|
|
59
|
+
setInputSeed(text);
|
|
60
|
+
setInputNonce((n) => n + 1);
|
|
61
|
+
}, []);
|
|
51
62
|
const [showHelp, setShowHelp] = useState(false);
|
|
52
63
|
const [approval, setApproval] = useState(null);
|
|
53
64
|
const [question, setQuestion] = useState(null);
|
|
@@ -103,6 +114,18 @@ export function App({ cwd, initialMode, askForMode, oneShot }) {
|
|
|
103
114
|
catch {
|
|
104
115
|
}
|
|
105
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
|
+
}, []);
|
|
106
129
|
const pickMode = useCallback((chosen) => {
|
|
107
130
|
setMode(chosen);
|
|
108
131
|
modeChosenRef.current = true;
|
|
@@ -114,6 +137,9 @@ export function App({ cwd, initialMode, askForMode, oneShot }) {
|
|
|
114
137
|
}
|
|
115
138
|
push('system', 'Ask for anything. /help for commands.');
|
|
116
139
|
}, [push]);
|
|
140
|
+
const cancelModePick = useCallback(() => {
|
|
141
|
+
setPhase(sessionReadyRef.current ? 'ready' : 'starting');
|
|
142
|
+
}, []);
|
|
117
143
|
const quit = useCallback(() => {
|
|
118
144
|
void runnerRef.current?.shutdown().finally(() => exit());
|
|
119
145
|
}, [exit]);
|
|
@@ -137,10 +163,13 @@ export function App({ cwd, initialMode, askForMode, oneShot }) {
|
|
|
137
163
|
quit();
|
|
138
164
|
return true;
|
|
139
165
|
case '/clear':
|
|
166
|
+
process.stdout.write(ESC + '[2J' + ESC + '[3J' + ESC + '[H');
|
|
140
167
|
setLines([]);
|
|
168
|
+
setShowHelp(false);
|
|
169
|
+
setScreenGeneration((n) => n + 1);
|
|
141
170
|
return true;
|
|
142
171
|
case '/mode':
|
|
143
|
-
|
|
172
|
+
setPhase('mode');
|
|
144
173
|
return true;
|
|
145
174
|
case '/plan':
|
|
146
175
|
setMode('plan');
|
|
@@ -201,7 +230,7 @@ export function App({ cwd, initialMode, askForMode, oneShot }) {
|
|
|
201
230
|
}, [cwd, cycleMode, mode, push, quit, refreshBalance, status]);
|
|
202
231
|
const submit = useCallback(async (message) => {
|
|
203
232
|
const text = message.trim();
|
|
204
|
-
|
|
233
|
+
setPromptText('');
|
|
205
234
|
if (!runnerRef.current)
|
|
206
235
|
return;
|
|
207
236
|
if (runnerRef.current.interactiveShell) {
|
|
@@ -309,6 +338,22 @@ export function App({ cwd, initialMode, askForMode, oneShot }) {
|
|
|
309
338
|
push('file', path.replace(/^\/workspace\//, ''));
|
|
310
339
|
}
|
|
311
340
|
},
|
|
341
|
+
onFileDiff: (diff) => {
|
|
342
|
+
settleLive();
|
|
343
|
+
if (diff.binary) {
|
|
344
|
+
push('file', `${diff.path} (binary)`);
|
|
345
|
+
return;
|
|
346
|
+
}
|
|
347
|
+
push('file', diff.stat || diff.path);
|
|
348
|
+
for (const line of diff.patch.split('\n')) {
|
|
349
|
+
if (!line || line.startsWith('---') || line.startsWith('+++'))
|
|
350
|
+
continue;
|
|
351
|
+
push(line.startsWith('+') ? 'diff-add'
|
|
352
|
+
: line.startsWith('-') ? 'diff-del'
|
|
353
|
+
: line.startsWith('@@') ? 'diff-hunk'
|
|
354
|
+
: 'diff-ctx', line);
|
|
355
|
+
}
|
|
356
|
+
},
|
|
312
357
|
onNotice: (text) => push('notice', text),
|
|
313
358
|
onOutput: (text) => {
|
|
314
359
|
shellBuf.current = trimToPane(shellBuf.current + sanitizeForModel(text));
|
|
@@ -418,6 +463,12 @@ export function App({ cwd, initialMode, askForMode, oneShot }) {
|
|
|
418
463
|
cycleMode();
|
|
419
464
|
return;
|
|
420
465
|
}
|
|
466
|
+
if (key.tab) {
|
|
467
|
+
const completed = completeInput(input);
|
|
468
|
+
if (completed !== input)
|
|
469
|
+
setPromptText(completed);
|
|
470
|
+
return;
|
|
471
|
+
}
|
|
421
472
|
if (key.escape && runnerRef.current?.killShell()) {
|
|
422
473
|
push('notice', 'Stopped the running command.');
|
|
423
474
|
return;
|
|
@@ -466,9 +517,9 @@ export function App({ cwd, initialMode, askForMode, oneShot }) {
|
|
|
466
517
|
const compactBanner = terminalWidth() < WORDMARK_MIN_COLUMNS || Boolean(oneShot);
|
|
467
518
|
const matches = completionsFor(input);
|
|
468
519
|
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
|
|
520
|
+
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(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
521
|
? { 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: {
|
|
522
|
+
: { 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
523
|
mode,
|
|
473
524
|
project: status.project,
|
|
474
525
|
cloudhandle: status.cloudhandle,
|
package/dist/ui/components.js
CHANGED
|
@@ -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,24 +40,59 @@ 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
|
|
43
|
+
export function CodeModePicker({ onPick, current, onCancel, }) {
|
|
43
44
|
const width = terminalWidth();
|
|
44
45
|
const options = [
|
|
45
|
-
{ key: '1', mode: '
|
|
46
|
-
{ key: '2', mode: '
|
|
47
|
-
{ key: '3', mode: '
|
|
48
|
-
{ 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' },
|
|
49
51
|
];
|
|
50
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 ModePicker({ onPick, current, onCancel, }) {
|
|
71
|
+
const width = terminalWidth();
|
|
72
|
+
const options = [
|
|
73
|
+
{ key: '1', mode: 'default', label: 'Manual', detail: 'ask for approval before each edit' },
|
|
74
|
+
{ key: '2', mode: 'bypass', label: 'Bypass', detail: 'edit files in this project without asking' },
|
|
75
|
+
];
|
|
76
|
+
useInput((input, key) => {
|
|
77
|
+
if (key.escape && onCancel) {
|
|
78
|
+
onCancel();
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
51
81
|
if (key.return) {
|
|
52
|
-
onPick('
|
|
82
|
+
onPick(current ?? 'default');
|
|
53
83
|
return;
|
|
54
84
|
}
|
|
55
85
|
const found = options.find((o) => o.key === input);
|
|
56
86
|
if (found)
|
|
57
87
|
onPick(found.mode);
|
|
58
88
|
});
|
|
59
|
-
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) => {
|
|
90
|
+
const active = o.mode === current;
|
|
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(13) }), _jsx(Text, { color: color('muted'), children: fit(o.detail, Math.max(10, width - 26)) })] }, o.key));
|
|
92
|
+
}) }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { color: color('faint'), children: fit(current
|
|
93
|
+
?
|
|
94
|
+
`press a number · enter keeps ${options.find((o) => o.mode === current)?.label ?? 'this'} · esc cancels`
|
|
95
|
+
: 'press a number · enter for accept edits · shift+tab changes it later', width - 4) }) })] }));
|
|
60
96
|
}
|
|
61
97
|
export const SHELL_PANE_LINES = 12;
|
|
62
98
|
export function ShellPane({ text, live }) {
|
|
@@ -104,8 +140,51 @@ export function HelpPanel() {
|
|
|
104
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 change permission mode" }), _jsx(Text, { color: color('faint'), children: " esc stop the current turn" }), _jsx(Text, { color: color('faint'), children: " ctrl+c quit" })] })] }));
|
|
105
141
|
}
|
|
106
142
|
export function StatusBar({ info }) {
|
|
107
|
-
const width =
|
|
108
|
-
const narrow = width < 76;
|
|
143
|
+
const width = terminalColumns();
|
|
109
144
|
const dot = info.busy ? color('warn') : color('success');
|
|
110
|
-
|
|
145
|
+
const segments = [];
|
|
146
|
+
let used = 2;
|
|
147
|
+
let full = true;
|
|
148
|
+
const add = (text, node) => {
|
|
149
|
+
if (!full)
|
|
150
|
+
return false;
|
|
151
|
+
const cost = (segments.length > 0 ? 3 : 0) + text.length;
|
|
152
|
+
if (used + cost > width - 2) {
|
|
153
|
+
full = false;
|
|
154
|
+
return false;
|
|
155
|
+
}
|
|
156
|
+
used += cost;
|
|
157
|
+
segments.push({ node, text });
|
|
158
|
+
return true;
|
|
159
|
+
};
|
|
160
|
+
const modeText = describeMode(info.mode);
|
|
161
|
+
const modeShort = modeName(info.mode);
|
|
162
|
+
const brand = 'Rivine';
|
|
163
|
+
const fitsBrandAndFullMode = 2 + brand.length + 3 + modeText.length <= width - 2;
|
|
164
|
+
if (2 + brand.length + 3 + modeShort.length <= width - 2) {
|
|
165
|
+
add(brand, (_jsx(Text, { color: color('brand'), bold: true, children: brand })));
|
|
166
|
+
}
|
|
167
|
+
const shownMode = fitsBrandAndFullMode ? modeText : modeShort;
|
|
168
|
+
used += (segments.length > 0 ? 3 : 0) + shownMode.length;
|
|
169
|
+
segments.push({
|
|
170
|
+
node: _jsx(Text, { color: color('muted'), children: shownMode }),
|
|
171
|
+
text: shownMode,
|
|
172
|
+
});
|
|
173
|
+
if (info.balance !== null) {
|
|
174
|
+
const money = `$${info.balance.toFixed(2)}`;
|
|
175
|
+
add(money, _jsx(Text, { color: info.balance < 10 ? color('warn') : color('muted'), children: money }));
|
|
176
|
+
}
|
|
177
|
+
if (info.project) {
|
|
178
|
+
const project = fit(info.project, 18, true);
|
|
179
|
+
add(project, _jsx(Text, { color: color('muted'), children: project }));
|
|
180
|
+
}
|
|
181
|
+
if (info.cloudhandle) {
|
|
182
|
+
const handle = fit(info.cloudhandle, 22);
|
|
183
|
+
const masked = info.bcidMasked ? ` ${info.bcidMasked}` : '';
|
|
184
|
+
add(handle + masked, _jsxs(_Fragment, { children: [_jsx(Text, { color: color('brand'), children: handle }), masked ? _jsx(Text, { color: color('faint'), children: masked }) : null] }));
|
|
185
|
+
}
|
|
186
|
+
if (used + 8 <= width - 2) {
|
|
187
|
+
segments.push({ node: _jsx(Text, { color: color('faint'), children: ' /help' }), text: ' /help' });
|
|
188
|
+
}
|
|
189
|
+
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
190
|
}
|
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/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,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@beunec/rivine",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Rivine Code in your terminal
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "Rivine Code in your terminal \u2014 Beunec's software engineering runtime constructs, working on the files you already have open.",
|
|
5
5
|
"license": "UNLICENSED",
|
|
6
6
|
"author": "Beunec Technologies",
|
|
7
7
|
"homepage": "https://aselius.beunec.com",
|