@bivy/bivy 0.17.2 → 0.17.3-staging.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/bivy.mjs +57 -10
- package/dist/agents/claude-code/runtime.js +17 -0
- package/dist/apps/display-viewer.js +81 -0
- package/dist/apps/display.js +135 -0
- package/dist/apps/gateway.js +388 -25
- package/dist/apps/inspector.js +115 -0
- package/dist/apps/listeners.js +115 -0
- package/dist/apps/preview-shell.js +206 -17
- package/dist/apps/registry.js +124 -20
- package/dist/apps/remote-preview.js +10 -2
- package/dist/apps/rfb.js +126 -0
- package/dist/apps/screenshot.js +194 -0
- package/dist/apps/service.js +243 -5
- package/dist/apps/styles.css +5 -0
- package/dist/apps/x11.js +273 -0
- package/dist/attach.js +30 -1
- package/dist/config-cli.js +2 -0
- package/dist/controllers/app-commands.js +19 -6
- package/dist/node-config.js +6 -3
- package/dist/policy/session-limit.js +63 -0
- package/dist/protocol/client-command-routes.js +1 -1
- package/dist/protocol/client-command-schemas.js +7 -1
- package/dist/server.js +99 -7
- package/dist/session/run-terminal.js +114 -70
- package/package.json +2 -1
|
@@ -98,6 +98,44 @@ export function createRunTerminals(deps) {
|
|
|
98
98
|
}
|
|
99
99
|
return false;
|
|
100
100
|
}
|
|
101
|
+
// One conversation, one writer. A run pinned to a session that is also open as
|
|
102
|
+
// a chat owns it while the PTY lives: the chat locks (and says so on every
|
|
103
|
+
// device), and when the PTY ends the chat reloads what the terminal wrote.
|
|
104
|
+
function bindRun(record, termId) {
|
|
105
|
+
record.tuiTermId = termId;
|
|
106
|
+
void deps.sessionTerminalsRecord(record.id, { termId }).catch(() => { });
|
|
107
|
+
deps.broadcastTuiState(record.id, true);
|
|
108
|
+
}
|
|
109
|
+
function releaseRun(record, termId) {
|
|
110
|
+
if (record.tuiTermId !== termId)
|
|
111
|
+
return;
|
|
112
|
+
record.tuiTermId = undefined;
|
|
113
|
+
void deps.sessionTerminalsForget(record.id).catch(() => { });
|
|
114
|
+
record.tuiRefreshing = true;
|
|
115
|
+
deps.broadcastTuiState(record.id, false);
|
|
116
|
+
void deps.refreshRecordAfterTui(record);
|
|
117
|
+
}
|
|
118
|
+
function adoptLiveRun(record) {
|
|
119
|
+
if (record.tuiTermId)
|
|
120
|
+
return;
|
|
121
|
+
for (const id of runTerminals) {
|
|
122
|
+
const pinned = terminals.meta(id)?.sessionId;
|
|
123
|
+
if (pinned && (pinned === record.id || pinned === record.sessionFile)) {
|
|
124
|
+
bindRun(record, id);
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
/** Stop a run because its session is continuing as a chat. Tells every viewer
|
|
130
|
+
* where the session went first, so a terminal attached elsewhere (a laptop's
|
|
131
|
+
* `bivy run`) can offer to take it back instead of just printing "ended". */
|
|
132
|
+
function handRunToChat(termId, sessionId) {
|
|
133
|
+
deps.broadcast({ type: "terminal.closed", termId, reason: "chat", sessionId });
|
|
134
|
+
terminals.close(termId);
|
|
135
|
+
runTerminals.delete(termId);
|
|
136
|
+
runViewers.delete(termId);
|
|
137
|
+
deps.broadcast({ type: "terminal.closed", termId });
|
|
138
|
+
}
|
|
101
139
|
async function runTerminalList() {
|
|
102
140
|
const runs = terminals.list((meta) => meta.kind === "run").filter((t) => runTerminals.has(t.id));
|
|
103
141
|
let saved = [];
|
|
@@ -218,6 +256,18 @@ export function createRunTerminals(deps) {
|
|
|
218
256
|
emit({ type: "terminal.error", error: `Too many run terminals open (${runTerminals.size}/${deps.maxRunTerminals}). Close one (e.g. 'bivy kill') or raise BIVY_MAX_RUN_TERMINALS.` });
|
|
219
257
|
return undefined;
|
|
220
258
|
}
|
|
259
|
+
const owner = spec.sessionId && !spec.mux ? deps.findOpenSession(spec.sessionId) : undefined;
|
|
260
|
+
if (owner?.tuiTermId && runTerminals.has(owner.tuiTermId)) {
|
|
261
|
+
// Already open in a terminal: join it rather than start a second writer.
|
|
262
|
+
if (spec.clientId && (spec.cols !== undefined || spec.rows !== undefined))
|
|
263
|
+
terminals.setClientSize(owner.tuiTermId, spec.clientId, spec.cols || 80, spec.rows || 24);
|
|
264
|
+
emit({ type: "terminal.attached", termId: owner.tuiTermId, data: terminals.snapshot(owner.tuiTermId) ?? "" });
|
|
265
|
+
return owner.tuiTermId;
|
|
266
|
+
}
|
|
267
|
+
if (owner && deps.sessionBusy(owner)) {
|
|
268
|
+
emit({ type: "terminal.error", sessionId: owner.id, error: "This session is mid-turn in chat. Stop or finish the turn, then open it in the terminal." });
|
|
269
|
+
return undefined;
|
|
270
|
+
}
|
|
221
271
|
const active = deps.getActiveSession();
|
|
222
272
|
const workspace = spec.workspace || active?.session?.cwd || active?.worktree?.path || active?.workspace || deps.defaultWorkspace;
|
|
223
273
|
// Agent-owned integrations keep their native login untouched. Only an
|
|
@@ -238,7 +288,7 @@ export function createRunTerminals(deps) {
|
|
|
238
288
|
workspace,
|
|
239
289
|
command: spec.command,
|
|
240
290
|
args: spec.args,
|
|
241
|
-
env: credentialEnv,
|
|
291
|
+
env: { ...credentialEnv, ...spec.env },
|
|
242
292
|
cols: spec.cols,
|
|
243
293
|
rows: spec.rows,
|
|
244
294
|
clientId: spec.clientId,
|
|
@@ -260,6 +310,17 @@ export function createRunTerminals(deps) {
|
|
|
260
310
|
clearRunIdleNotify(id);
|
|
261
311
|
deps.broadcast({ type: "terminal.exit", termId: id, code });
|
|
262
312
|
deps.broadcast({ type: "terminal.closed", termId: id });
|
|
313
|
+
// A run that owned an open chat hands the conversation back to it; the
|
|
314
|
+
// chat keeps its own record, so none of the run bookkeeping below applies.
|
|
315
|
+
const bound = spec.sessionId ? deps.findOpenSession(spec.sessionId) : undefined;
|
|
316
|
+
if (bound?.tuiTermId === id) {
|
|
317
|
+
releaseRun(bound, id);
|
|
318
|
+
try {
|
|
319
|
+
deps.sessionListChanged();
|
|
320
|
+
}
|
|
321
|
+
catch { /* best-effort */ }
|
|
322
|
+
return;
|
|
323
|
+
}
|
|
263
324
|
if (!spec.mux) {
|
|
264
325
|
// The durable record of this run, in order of fidelity: the agent's
|
|
265
326
|
// own session (pinned at launch), the session discovered in the
|
|
@@ -315,7 +376,9 @@ export function createRunTerminals(deps) {
|
|
|
315
376
|
},
|
|
316
377
|
});
|
|
317
378
|
runTerminals.add(id);
|
|
318
|
-
if (
|
|
379
|
+
if (owner)
|
|
380
|
+
bindRun(owner, id);
|
|
381
|
+
if (spec.sessionId && spec.agent && !spec.mux && !owner) {
|
|
319
382
|
const runtimeId = TAKEOVER_RUNTIME_BY_AGENT[spec.agent] ?? spec.agent;
|
|
320
383
|
try {
|
|
321
384
|
deps.upsertSessionMetadata({ id: spec.sessionId, runtimeId, agentName: spec.agent, workspace, name: name || undefined, source: "cli", status: "working" });
|
|
@@ -366,6 +429,13 @@ export function createRunTerminals(deps) {
|
|
|
366
429
|
: undefined;
|
|
367
430
|
if (!entry)
|
|
368
431
|
return { ok: false, status: 404, error: "No matching live run-terminal." };
|
|
432
|
+
// The chat already exists (the terminal was opened from it, or joined it):
|
|
433
|
+
// stopping the PTY is the whole handoff — its exit reloads the chat.
|
|
434
|
+
const owner = entry.meta.sessionId ? deps.findOpenSession(entry.meta.sessionId) : undefined;
|
|
435
|
+
if (owner?.tuiTermId === entry.id) {
|
|
436
|
+
handRunToChat(entry.id, owner.id);
|
|
437
|
+
return { ok: true, sessionId: owner.id, runtimeId: owner.runtimeId ?? "" };
|
|
438
|
+
}
|
|
369
439
|
const agent = entry.meta.agent ?? "";
|
|
370
440
|
const runtimeId = TAKEOVER_RUNTIME_BY_AGENT[agent];
|
|
371
441
|
if (!runtimeId)
|
|
@@ -385,10 +455,7 @@ export function createRunTerminals(deps) {
|
|
|
385
455
|
// second agent process until the next prompt. If preparation fails, the
|
|
386
456
|
// terminal remains live and the user loses nothing.
|
|
387
457
|
const record = await deps.createSession(workspace, pinned, { runtimeId, makeActive: true, source: "takeover" });
|
|
388
|
-
|
|
389
|
-
runTerminals.delete(entry.id);
|
|
390
|
-
runViewers.delete(entry.id);
|
|
391
|
-
deps.broadcast({ type: "terminal.closed", termId: entry.id });
|
|
458
|
+
handRunToChat(entry.id, record.id);
|
|
392
459
|
const resumeCommand = RESUME_CLI_BY_AGENT[agent]?.(pinned);
|
|
393
460
|
return { ok: true, sessionId: record.id, runtimeId, resumeCommand };
|
|
394
461
|
}
|
|
@@ -473,84 +540,61 @@ export function createRunTerminals(deps) {
|
|
|
473
540
|
return true;
|
|
474
541
|
}
|
|
475
542
|
case "terminal.open.tui": {
|
|
543
|
+
// "Open in terminal" for a chat session: its runtime's own interactive
|
|
544
|
+
// TUI, resuming the same conversation, as a daemon-owned run pinned to the
|
|
545
|
+
// session — so it outlives the device that opened it, every device can
|
|
546
|
+
// watch it, and the chat stays locked until it ends (single writer).
|
|
476
547
|
void (async () => {
|
|
477
|
-
const record = deps.
|
|
548
|
+
const record = typeof msg.sessionId === "string" && msg.sessionId ? deps.findOpenSession(msg.sessionId) : undefined;
|
|
478
549
|
if (!record) {
|
|
479
550
|
emit({ type: "terminal.error", error: "Session not found for TUI." });
|
|
480
551
|
return;
|
|
481
552
|
}
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
if (typeof msg.cols !== "undefined" || typeof msg.rows !== "undefined")
|
|
487
|
-
terminals.setClientSize(record.tuiTermId, clientId, Number(msg.cols) || 80, Number(msg.rows) || 24);
|
|
488
|
-
emit({ type: "terminal.attached", termId: record.tuiTermId, data: snapshot });
|
|
489
|
-
return;
|
|
490
|
-
}
|
|
491
|
-
}
|
|
492
|
-
if (deps.sessionBusy(record)) {
|
|
553
|
+
const cols = Number(msg.cols) || undefined;
|
|
554
|
+
const rows = Number(msg.rows) || undefined;
|
|
555
|
+
const live = record.tuiTermId && runTerminals.has(record.tuiTermId) ? record.tuiTermId : undefined;
|
|
556
|
+
if (!live && deps.sessionBusy(record)) {
|
|
493
557
|
emit({ type: "terminal.error", sessionId: record.id, error: "Finish or stop the current turn before opening the TUI." });
|
|
494
558
|
return;
|
|
495
559
|
}
|
|
496
|
-
let spec;
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
try {
|
|
510
|
-
const id = terminals.open({
|
|
511
|
-
workspace,
|
|
512
|
-
command: spec.command,
|
|
513
|
-
args: spec.args,
|
|
514
|
-
env: spec.env,
|
|
515
|
-
cols: Number(msg.cols) || undefined,
|
|
516
|
-
rows: Number(msg.rows) || undefined,
|
|
517
|
-
clientId,
|
|
518
|
-
onData: (data) => emit({ type: "terminal.output", termId: id, data }),
|
|
519
|
-
onExit: (code) => {
|
|
520
|
-
owned.delete(id);
|
|
521
|
-
if (record.tuiTermId === id) {
|
|
522
|
-
record.tuiTermId = undefined;
|
|
523
|
-
void deps.sessionTerminalsForget(record.id).catch(() => { });
|
|
524
|
-
record.tuiRefreshing = true;
|
|
525
|
-
deps.broadcastTuiState(record.id, false);
|
|
526
|
-
void deps.refreshRecordAfterTui(record);
|
|
527
|
-
}
|
|
528
|
-
emit({ type: "terminal.exit", termId: id, code });
|
|
529
|
-
},
|
|
530
|
-
});
|
|
531
|
-
owned.add(id);
|
|
532
|
-
record.tuiTermId = id;
|
|
533
|
-
void deps.sessionTerminalsRecord(record.id, { termId: id }).catch(() => { });
|
|
534
|
-
emit({ type: "terminal.opened", termId: id, workspace, sessionId: record.id, mode: "tui" });
|
|
535
|
-
deps.broadcastTuiState(record.id, true);
|
|
536
|
-
}
|
|
537
|
-
catch (error) {
|
|
538
|
-
emit({ type: "terminal.error", sessionId: record.id, error: error instanceof Error ? error.message : String(error) });
|
|
560
|
+
let spec = null;
|
|
561
|
+
if (!live) {
|
|
562
|
+
try {
|
|
563
|
+
spec = record.session.interactiveTuiCommand ? await record.session.interactiveTuiCommand() : null;
|
|
564
|
+
}
|
|
565
|
+
catch (error) {
|
|
566
|
+
emit({ type: "terminal.error", sessionId: record.id, error: error instanceof Error ? error.message : String(error) });
|
|
567
|
+
return;
|
|
568
|
+
}
|
|
569
|
+
if (!spec) {
|
|
570
|
+
emit({ type: "terminal.error", sessionId: record.id, error: "This runtime has no interactive TUI available on this node." });
|
|
571
|
+
return;
|
|
572
|
+
}
|
|
539
573
|
}
|
|
574
|
+
const id = await openRunTerminal({
|
|
575
|
+
command: spec?.command ?? "",
|
|
576
|
+
args: spec?.args ?? [],
|
|
577
|
+
env: spec?.env,
|
|
578
|
+
name: record.session.getName() || undefined,
|
|
579
|
+
workspace: record.session.cwd || record.worktree?.path || record.workspace,
|
|
580
|
+
sessionId: record.id,
|
|
581
|
+
cols,
|
|
582
|
+
rows,
|
|
583
|
+
clientId,
|
|
584
|
+
}, emit);
|
|
585
|
+
if (id)
|
|
586
|
+
viewRun?.(id);
|
|
540
587
|
})();
|
|
541
588
|
return true;
|
|
542
589
|
}
|
|
543
590
|
case "terminal.close.tui": {
|
|
544
|
-
|
|
591
|
+
// "Use chat": hand the session's terminal back to its chat.
|
|
592
|
+
const record = typeof msg.sessionId === "string" && msg.sessionId ? deps.findOpenSession(msg.sessionId) : undefined;
|
|
545
593
|
const termId = record?.tuiTermId;
|
|
546
|
-
if (termId)
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
clearBellNotify(termId);
|
|
550
|
-
}
|
|
551
|
-
else if (record) {
|
|
594
|
+
if (record && termId && runTerminals.has(termId))
|
|
595
|
+
handRunToChat(termId, record.id);
|
|
596
|
+
else if (record)
|
|
552
597
|
deps.broadcastTuiState(record.id, false);
|
|
553
|
-
}
|
|
554
598
|
return true;
|
|
555
599
|
}
|
|
556
600
|
case "terminal.attach": {
|
|
@@ -607,5 +651,5 @@ export function createRunTerminals(deps) {
|
|
|
607
651
|
return false;
|
|
608
652
|
}
|
|
609
653
|
}
|
|
610
|
-
return { runTerminalList, openRunTerminal, takeoverRunTerminal, handleTerminalMessage, addRunViewer, dropRunViewer, hasRunTerminal, hasLiveRunForSession };
|
|
654
|
+
return { runTerminalList, openRunTerminal, takeoverRunTerminal, handleTerminalMessage, addRunViewer, dropRunViewer, hasRunTerminal, hasLiveRunForSession, adoptLiveRun };
|
|
611
655
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bivy/bivy",
|
|
3
|
-
"version": "0.17.
|
|
3
|
+
"version": "0.17.3-staging.10",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"license": "AGPL-3.0-only",
|
|
6
6
|
"description": "Run coding agents on machines you own. Open-source, self-hostable agent workspace.",
|
|
@@ -39,6 +39,7 @@
|
|
|
39
39
|
},
|
|
40
40
|
"dependencies": {
|
|
41
41
|
"@modelcontextprotocol/sdk": "1.30.0",
|
|
42
|
+
"@novnc/novnc": "1.7.0",
|
|
42
43
|
"brace-expansion": "5.0.12",
|
|
43
44
|
"express": "^5.2.1",
|
|
44
45
|
"express-rate-limit": "^8.7.0",
|