@bivy/bivy 0.17.3-staging.9 → 0.18.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/bin/bivy.mjs +157 -12
- package/dist/apps/annotate.js +94 -0
- package/dist/apps/display-viewer.js +80 -7
- package/dist/apps/display.js +11 -1
- package/dist/apps/gateway.js +106 -14
- package/dist/apps/input.js +96 -0
- package/dist/apps/inspector.js +84 -29
- package/dist/apps/macos-display.js +170 -0
- package/dist/apps/macos-display.swift +873 -0
- package/dist/apps/menu.js +76 -0
- package/dist/apps/preview-shell.js +232 -16
- package/dist/apps/preview-stream.js +17 -3
- package/dist/apps/registry.js +76 -5
- package/dist/apps/review.js +111 -0
- package/dist/apps/rfb.js +69 -20
- package/dist/apps/screenshot.js +8 -2
- package/dist/apps/service.js +343 -13
- package/dist/apps/styles.css +162 -10
- package/dist/apps/tokens.css +6 -0
- package/dist/apps/types.js +17 -0
- package/dist/attach.js +37 -4
- package/dist/controllers/app-commands.js +16 -4
- package/dist/controllers/artifact-commands.js +42 -0
- package/dist/controllers/presence-commands.js +19 -0
- package/dist/protocol/client-command-routes.js +3 -1
- package/dist/protocol/client-command-schemas.js +19 -3
- package/dist/server.js +200 -21
- package/dist/session/attachment-store.js +15 -0
- package/dist/session/event-log.js +25 -3
- package/dist/session/presence.js +82 -0
- package/dist/session/run-terminal.js +113 -69
- package/dist/session/session-new-dedupe.js +1 -1
- package/dist/terminal.js +23 -0
- package/dist/worktree.js +40 -3
- package/package.json +1 -1
package/dist/apps/styles.css
CHANGED
|
@@ -116,6 +116,10 @@ input, textarea, select { font-family: inherit; font-size: 16px; }
|
|
|
116
116
|
}
|
|
117
117
|
.menu-item:hover, .menu-item[aria-checked="true"], .menu-item.active { background: var(--surface-2); }
|
|
118
118
|
.menu-item.danger { color: var(--danger); }
|
|
119
|
+
.menu-item-label { flex: 1; min-width: 0; }
|
|
120
|
+
.menu-item.separated { margin-top: var(--space-1); border-top: 1px solid var(--line); border-radius: 0 0 var(--radius-sm) var(--radius-sm); }
|
|
121
|
+
/* A section title inside a menu (e.g. "Preview cards" above its choices). */
|
|
122
|
+
.menu-heading { padding: var(--space-2) var(--space-3) var(--space-1); color: var(--muted); font-size: var(--text-xs); font-weight: var(--weight-semibold); letter-spacing: 0.04em; text-transform: uppercase; }
|
|
119
123
|
|
|
120
124
|
/* ---- Feedback ---- */
|
|
121
125
|
.toast {
|
|
@@ -588,6 +592,12 @@ input, textarea, select { font-family: inherit; font-size: 16px; }
|
|
|
588
592
|
discoverability pattern as .row-menu-btn — and a touch-friendly baseline
|
|
589
593
|
opacity on devices with no hover. */
|
|
590
594
|
.assistant-row { align-self: stretch; width: 100%; max-width: 680px; }
|
|
595
|
+
/* The message "Show in chat" landed on (Artifacts / Apps pages). */
|
|
596
|
+
.is-jump-target { border-radius: var(--radius-md); animation: jump-target 2.4s var(--ease-standard); }
|
|
597
|
+
@keyframes jump-target {
|
|
598
|
+
0%, 50% { background: color-mix(in srgb, var(--accent) 10%, transparent); box-shadow: 0 0 0 var(--space-2) color-mix(in srgb, var(--accent) 10%, transparent); }
|
|
599
|
+
100% { background: transparent; box-shadow: 0 0 0 var(--space-2) transparent; }
|
|
600
|
+
}
|
|
591
601
|
.msg-actions { display: flex; align-items: center; gap: 2px; margin-top: var(--space-1); margin-left: -6px; }
|
|
592
602
|
.msg-copy-btn, .msg-speak-btn {
|
|
593
603
|
border: 0; background: transparent; color: var(--muted);
|
|
@@ -1271,6 +1281,7 @@ button.sheet-action:last-child { border-bottom: 0; }
|
|
|
1271
1281
|
.fork-select-field select { width: 100%; padding: 10px 12px; border: 1px solid var(--line-strong); border-radius: var(--radius-md); background: var(--surface); color: var(--ink); }
|
|
1272
1282
|
.fork-toggle { display: flex; align-items: center; gap: 8px; padding: 10px 12px; font-size: 13px; color: var(--ink); }
|
|
1273
1283
|
.fork-toggle input { width: 16px; height: 16px; }
|
|
1284
|
+
.fork-note { margin: 0; padding: var(--space-2) var(--space-3); color: var(--muted); font-size: var(--text-sm); line-height: var(--lh-sm); }
|
|
1274
1285
|
.fork-error { margin: 4px 12px; padding: 8px 10px; border-radius: var(--radius-md); background: var(--surface-2); color: var(--danger); font-size: 12px; }
|
|
1275
1286
|
.fork-warn { margin: 4px 12px; padding: 8px 10px; border-radius: var(--radius-md); background: var(--surface-2); color: var(--muted); font-size: 12px; }
|
|
1276
1287
|
.fork-submit { width: calc(100% - 24px); margin: var(--space-2) var(--space-3); }
|
|
@@ -1395,7 +1406,12 @@ button.sheet-action:last-child { border-bottom: 0; }
|
|
|
1395
1406
|
font-weight: var(--weight-semibold); cursor: pointer; white-space: nowrap;
|
|
1396
1407
|
}
|
|
1397
1408
|
.seg-btn:hover { color: var(--ink); }
|
|
1398
|
-
.seg-btn[aria-selected="true"] { background: var(--surface); color: var(--accent); box-shadow: var(--shadow-sm); }
|
|
1409
|
+
.seg-btn[aria-selected="true"], .seg-btn[aria-checked="true"] { background: var(--surface); color: var(--accent); box-shadow: var(--shadow-sm); }
|
|
1410
|
+
/* Chat | Terminal in the session topbar (SessionViewToggle). */
|
|
1411
|
+
.topbar-subline { display: flex; align-items: center; gap: var(--space-3); min-width: 0; margin-top: 2px; }
|
|
1412
|
+
.topbar-subline .node-switcher { flex: 0 1 auto; }
|
|
1413
|
+
.session-view-toggle { flex: none; padding: 2px; }
|
|
1414
|
+
.session-view-toggle .seg-btn { display: inline-flex; align-items: center; gap: var(--space-1); padding: 2px var(--space-2); line-height: 1.25; }
|
|
1399
1415
|
.automations-tabs {
|
|
1400
1416
|
flex: none; align-self: center; max-width: calc(100% - var(--space-6));
|
|
1401
1417
|
margin: var(--space-2) auto 0; overflow-x: auto;
|
|
@@ -1411,6 +1427,51 @@ button.sheet-action:last-child { border-bottom: 0; }
|
|
|
1411
1427
|
.automations-view-head-actions > .btn.primary { padding: 9px 18px; }
|
|
1412
1428
|
.automations-view-body { padding: 8px 24px max(48px, env(safe-area-inset-bottom)); }
|
|
1413
1429
|
}
|
|
1430
|
+
/* Artifacts / Apps pages (LibraryView.tsx) share the Automations page shell:
|
|
1431
|
+
a quiet image grid for files, a plain list for apps. */
|
|
1432
|
+
.library-view:focus { outline: none; }
|
|
1433
|
+
.library-view .automations-view-head { width: 100%; max-width: 1080px; margin: 0 auto; box-sizing: border-box; }
|
|
1434
|
+
.library-body { max-width: 1080px; }
|
|
1435
|
+
.library-toolbar { display: flex; margin: var(--space-2) 0 var(--space-4); }
|
|
1436
|
+
.library-loading { display: flex; justify-content: center; padding: var(--space-8) 0; }
|
|
1437
|
+
.library-body > .banner { margin: var(--space-2) 0; }
|
|
1438
|
+
.library-note { margin: var(--space-2) 0; font-size: var(--text-sm); color: var(--muted); }
|
|
1439
|
+
.library-grid {
|
|
1440
|
+
list-style: none; margin: var(--space-2) 0 0; padding: 0;
|
|
1441
|
+
display: grid; gap: var(--space-5) var(--space-3);
|
|
1442
|
+
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
|
|
1443
|
+
}
|
|
1444
|
+
.library-tile { display: flex; flex-direction: column; gap: var(--space-2); min-width: 0; }
|
|
1445
|
+
.library-tile-preview {
|
|
1446
|
+
width: 100%; aspect-ratio: 4 / 3; padding: 0; overflow: hidden;
|
|
1447
|
+
display: grid; place-items: center;
|
|
1448
|
+
border: 1px solid var(--line); border-radius: var(--radius-lg);
|
|
1449
|
+
background: var(--surface-2); color: var(--muted); font: inherit; cursor: pointer;
|
|
1450
|
+
transition: border-color var(--dur-fast) var(--ease-standard);
|
|
1451
|
+
}
|
|
1452
|
+
.library-tile-preview:hover:not(:disabled) { border-color: var(--line-strong); }
|
|
1453
|
+
.library-tile-preview:disabled { cursor: progress; }
|
|
1454
|
+
.library-tile-preview img { display: block; width: 100%; height: 100%; object-fit: cover; }
|
|
1455
|
+
.library-tile-file { display: flex; flex-direction: column; align-items: center; gap: var(--space-1); }
|
|
1456
|
+
.library-tile-ext { font-size: var(--text-xs); font-weight: var(--weight-semibold); letter-spacing: 0.04em; }
|
|
1457
|
+
.library-tile-caption { display: flex; flex-direction: column; gap: 2px; min-width: 0; padding: 0 2px; }
|
|
1458
|
+
.library-tile-name {
|
|
1459
|
+
overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
|
|
1460
|
+
font-size: var(--text-sm); font-weight: var(--weight-medium); color: var(--ink);
|
|
1461
|
+
}
|
|
1462
|
+
.library-tile-note { font-size: var(--text-xs); color: var(--danger); }
|
|
1463
|
+
.library-source {
|
|
1464
|
+
align-self: flex-start; display: inline-flex; align-items: center; gap: var(--space-1);
|
|
1465
|
+
max-width: 100%; min-width: 0; min-height: 28px; padding: 0; border: 0; background: transparent;
|
|
1466
|
+
color: var(--muted); font: inherit; font-size: var(--text-xs); text-align: left; cursor: pointer;
|
|
1467
|
+
}
|
|
1468
|
+
.library-source:hover { color: var(--ink); }
|
|
1469
|
+
.library-source svg, .library-source-when { flex: none; }
|
|
1470
|
+
.library-source-name { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
1471
|
+
@media (min-width: 640px) {
|
|
1472
|
+
.library-grid { grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); gap: var(--space-6) var(--space-4); }
|
|
1473
|
+
}
|
|
1474
|
+
|
|
1414
1475
|
.autom-section { margin-top: 26px; }
|
|
1415
1476
|
.autom-section-head {
|
|
1416
1477
|
display: flex; align-items: center; justify-content: space-between; gap: 10px;
|
|
@@ -2445,6 +2506,12 @@ textarea.picker-search { resize: vertical; font-family: ui-monospace, monospace;
|
|
|
2445
2506
|
height: var(--app-h, 100dvh); z-index: var(--z-sheet); display: flex; flex-direction: column;
|
|
2446
2507
|
background: var(--bg); padding-top: env(safe-area-inset-top);
|
|
2447
2508
|
}
|
|
2509
|
+
/* The same terminal inside the session pane (Chat | Terminal toggle): in the
|
|
2510
|
+
main column's flow under the topbar, not a fixed full-screen layer. */
|
|
2511
|
+
.term-overlay.term-embedded {
|
|
2512
|
+
position: relative; top: auto; left: auto; width: auto; height: auto;
|
|
2513
|
+
flex: 1 1 auto; min-height: 0; z-index: auto; padding-top: 0;
|
|
2514
|
+
}
|
|
2448
2515
|
.term-head { display: flex; align-items: center; gap: 10px; padding: 10px 14px; border-bottom: 1px solid var(--line); }
|
|
2449
2516
|
.term-title { font-weight: 700; flex: 0 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
2450
2517
|
.term-status { color: var(--muted); font-weight: 400; margin-left: 8px; font-size: 12px; }
|
|
@@ -2492,6 +2559,10 @@ textarea.picker-search { resize: vertical; font-family: ui-monospace, monospace;
|
|
|
2492
2559
|
}
|
|
2493
2560
|
.term-out .xterm { height: 100%; }
|
|
2494
2561
|
.term-out .xterm-viewport { overflow-y: auto; overscroll-behavior: contain; }
|
|
2562
|
+
/* xterm.css paints the viewport #000; the GPU renderers cover it with the theme
|
|
2563
|
+
background, the DOM fallback does not — a light-theme sliver below the last
|
|
2564
|
+
row. Match readTheme()'s background. */
|
|
2565
|
+
.term-out .xterm .xterm-viewport { background-color: var(--bg); }
|
|
2495
2566
|
/* Smooth momentum scrolling for the terminal viewport on touch devices. */
|
|
2496
2567
|
.term-out .xterm-viewport { -webkit-overflow-scrolling: touch; scrollbar-width: thin; }
|
|
2497
2568
|
|
|
@@ -2834,6 +2905,8 @@ select.picker-search { appearance: auto; }
|
|
|
2834
2905
|
.setup-card p { color: var(--ink); margin: 8px 0; }
|
|
2835
2906
|
.setup-card .muted { color: var(--muted); font-size: 13px; }
|
|
2836
2907
|
.setup-foot { margin-top: 18px; }
|
|
2908
|
+
.setup-foot a { color: var(--accent); }
|
|
2909
|
+
.setup-foot a:hover { color: var(--accent-hover); }
|
|
2837
2910
|
.btn.block { display: block; width: 100%; text-align: center; text-decoration: none; margin-top: 12px; padding: 11px; }
|
|
2838
2911
|
.setup-or { color: var(--muted); font-size: 13px; margin: 16px 0 8px; }
|
|
2839
2912
|
.setup-email { display: flex; flex-direction: column; gap: 8px; }
|
|
@@ -2842,6 +2915,9 @@ select.picker-search { appearance: auto; }
|
|
|
2842
2915
|
.setup-email .btn { width: 100%; white-space: nowrap; min-height: var(--space-7); }
|
|
2843
2916
|
.setup-note { margin-top: 10px; word-break: break-word; }
|
|
2844
2917
|
.setup-note .btn { min-height: var(--space-7); }
|
|
2918
|
+
/* Inline text actions flow with the sentence: negative block margins keep the
|
|
2919
|
+
44px touch target from stretching the paragraph's line box. */
|
|
2920
|
+
p.setup-note .btn.link { min-height: 44px; margin-block: calc((1lh - 44px) / 2); padding-block: 0; font: inherit; vertical-align: middle; }
|
|
2845
2921
|
.setup-error { margin: 8px 0 4px; padding: 10px 12px; border: 1px solid var(--danger); border-left-width: 3px; border-radius: var(--radius-md); background: var(--danger-soft, transparent); color: var(--ink); font-size: 13px; line-height: 1.4; text-align: left; }
|
|
2846
2922
|
/* Setup / connect surfaces prefer a little vertical rhythm on inline banners. */
|
|
2847
2923
|
.setup-card .banner.inline,
|
|
@@ -2852,6 +2928,9 @@ select.picker-search { appearance: auto; }
|
|
|
2852
2928
|
.scrim { position: fixed; inset: 0; background: var(--overlay); z-index: var(--z-dropdown); }
|
|
2853
2929
|
@media (max-width: 900px) {
|
|
2854
2930
|
.app { grid-template-columns: 1fr; }
|
|
2931
|
+
/* Share the subtitle line with the machine name: icons only (the accessible
|
|
2932
|
+
name stays via aria-label). */
|
|
2933
|
+
.session-view-label { display: none; }
|
|
2855
2934
|
.only-mobile { display: inline-flex; }
|
|
2856
2935
|
.sidebar {
|
|
2857
2936
|
position: fixed; top: var(--app-top, 0px); left: 0; height: var(--app-h, 100dvh);
|
|
@@ -2953,18 +3032,86 @@ select.picker-search { appearance: auto; }
|
|
|
2953
3032
|
|
|
2954
3033
|
/* Artifacts sheet (components/ArtifactsSheet.tsx) — agent-sent attachments,
|
|
2955
3034
|
user uploads, and resolved inline images grouped by kind. */
|
|
2956
|
-
|
|
2957
|
-
|
|
2958
|
-
.app
|
|
2959
|
-
.
|
|
2960
|
-
.
|
|
2961
|
-
.app-
|
|
2962
|
-
.app-
|
|
3035
|
+
/* An app, a view of it, or a server that could become one (AppRow.tsx): the
|
|
3036
|
+
same tile, type and trailing action in the chat card, the Apps page and the
|
|
3037
|
+
Apps sheet. Views nest inside their app's card as small rows. */
|
|
3038
|
+
.apps-card { border: 1px solid var(--line); border-radius: var(--radius-md); background: var(--surface); overflow-wrap: anywhere; }
|
|
3039
|
+
.apps-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: var(--space-2); }
|
|
3040
|
+
.app-row { display: flex; align-items: center; gap: var(--space-3); min-width: 0; padding: var(--space-3); }
|
|
3041
|
+
.app-tile { flex: none; display: grid; place-items: center; inline-size: 40px; block-size: 40px; border: 1px solid var(--line); border-radius: var(--radius-md); background: var(--surface-2); color: var(--ink); font-weight: var(--weight-semibold); }
|
|
3042
|
+
.app-row.small .app-tile { inline-size: 36px; block-size: 36px; border-radius: var(--radius-sm); color: var(--muted); }
|
|
3043
|
+
.app-tile-port { font-family: var(--font-mono); font-size: var(--text-xs); color: var(--ok); }
|
|
3044
|
+
.app-row-text { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 2px; }
|
|
3045
|
+
.app-row-name { font-size: var(--text-base); font-weight: var(--weight-semibold); }
|
|
3046
|
+
.app-row.small .app-row-name { font-size: var(--text-sm); }
|
|
3047
|
+
.app-row-meta { color: var(--muted); font-size: var(--text-xs); line-height: var(--lh-xs); }
|
|
3048
|
+
.app-row-action { flex: none; display: flex; align-items: center; gap: var(--space-1); }
|
|
3049
|
+
.app-message { margin-block: var(--space-2); }
|
|
3050
|
+
/* Review card (ReviewCard.tsx): the app at a moment worth judging. The
|
|
3051
|
+
screenshot is shown whole, phone-shaped, on a quiet stage; Before/Now is the
|
|
3052
|
+
canonical .segmented switch; one primary action. */
|
|
3053
|
+
.review-card { margin-block: var(--space-2); max-inline-size: 440px; }
|
|
3054
|
+
.review-stage { display: flex; flex-direction: column; align-items: center; gap: var(--space-2); margin: 0 var(--space-3); padding: var(--space-3); border-radius: var(--radius-md); background: var(--surface-2); }
|
|
3055
|
+
.review-shot { display: grid; place-items: center; padding: 0; border: 1px solid var(--line); border-radius: var(--radius-md); background: var(--bg); overflow: hidden; cursor: zoom-in; box-shadow: var(--shadow-sm); }
|
|
3056
|
+
.review-shot img { display: block; inline-size: auto; block-size: auto; max-inline-size: 100%; max-block-size: min(300px, 40vh); }
|
|
3057
|
+
.review-loading { display: grid; place-items: center; inline-size: 139px; block-size: min(300px, 40vh); max-inline-size: 100%; }
|
|
3058
|
+
.review-gone { margin: 0; padding: var(--space-6) var(--space-3); color: var(--muted); font-size: var(--text-sm); text-align: center; }
|
|
3059
|
+
.review-side .seg-btn { min-block-size: 44px; min-inline-size: 72px; }
|
|
3060
|
+
.review-banner { margin: 0 var(--space-3); }
|
|
3061
|
+
.review-note { margin: var(--space-3) var(--space-3) 0; padding-inline-start: var(--space-3); border-inline-start: 2px solid var(--line-strong); color: var(--ink); font-size: var(--text-sm); white-space: pre-wrap; }
|
|
3062
|
+
.review-status { margin: var(--space-2) var(--space-3) 0; color: var(--muted); font-size: var(--text-sm); }
|
|
3063
|
+
.review-actions { display: flex; padding: var(--space-3); }
|
|
3064
|
+
.review-actions .btn { flex: 1; min-block-size: 44px; }
|
|
3065
|
+
|
|
3066
|
+
/* Apps sheet (AppsSheet.tsx): one card per app, its views nested, one primary
|
|
3067
|
+
action per view; secondary tools as quiet icon buttons; rare or destructive
|
|
3068
|
+
actions behind ⋯ (MoreMenu, the canonical .menu). Status uses .banner. */
|
|
3069
|
+
.apps-sheet { display: flex; flex-direction: column; gap: var(--space-4); padding: var(--space-2) var(--space-2) var(--space-4); }
|
|
3070
|
+
.apps-state { display: flex; align-items: center; justify-content: center; gap: var(--space-2); padding: var(--space-6) 0; color: var(--muted); font-size: var(--text-sm); }
|
|
3071
|
+
.apps-group { display: flex; flex-direction: column; gap: var(--space-2); }
|
|
3072
|
+
.apps-group-title { margin: 0; padding: 0 var(--space-1); color: var(--muted); font-size: var(--text-xs); font-weight: var(--weight-semibold); letter-spacing: 0.04em; text-transform: uppercase; }
|
|
3073
|
+
.apps-view { display: flex; flex-direction: column; border-top: 1px solid var(--line); }
|
|
3074
|
+
.apps-cmd { display: block; margin: 0 var(--space-3) var(--space-3); padding: var(--space-2) var(--space-3); border-radius: var(--radius-sm); background: var(--surface-2); color: var(--ink); font-family: var(--font-mono); font-size: var(--text-xs); white-space: pre-wrap; overflow-wrap: anywhere; }
|
|
3075
|
+
.apps-view-tools { display: flex; flex-wrap: wrap; align-items: center; gap: 0; padding: 0 var(--space-1) var(--space-1); }
|
|
3076
|
+
.apps-view-tools .btn { gap: 6px; padding-inline: var(--space-2); color: var(--muted); }
|
|
3077
|
+
.apps-view-tools .btn:hover:not(:disabled) { color: var(--ink); }
|
|
3078
|
+
.apps-more { position: relative; }
|
|
3079
|
+
.apps-view-tools .apps-more { margin-inline-start: auto; }
|
|
3080
|
+
.apps-more-menu { position: absolute; right: 0; top: calc(100% + var(--space-1)); z-index: var(--z-dropdown); }
|
|
3081
|
+
.apps-notice { display: flex; align-items: flex-start; gap: var(--space-2); margin: 0 var(--space-3) var(--space-3); padding: var(--space-2) var(--space-3); border-radius: var(--radius-sm); background: color-mix(in srgb, var(--ok) 12%, transparent); font-size: var(--text-sm); }
|
|
3082
|
+
.apps-notice > svg { flex: none; margin-top: 1px; color: var(--ok); }
|
|
3083
|
+
.apps-notes { display: flex; flex-direction: column; gap: var(--space-2); margin: 0 var(--space-3) var(--space-3); padding-top: var(--space-3); border-top: 1px dashed var(--line); }
|
|
3084
|
+
.apps-notes-head { display: flex; align-items: center; gap: var(--space-2); color: var(--muted); font-size: var(--text-xs); font-weight: var(--weight-semibold); letter-spacing: 0.04em; text-transform: uppercase; }
|
|
3085
|
+
.apps-count { min-inline-size: 18px; padding: 0 6px; border-radius: var(--radius-full); background: var(--surface-3); color: var(--ink); text-align: center; letter-spacing: 0; }
|
|
3086
|
+
.apps-notes ul { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: var(--space-2); }
|
|
3087
|
+
.apps-note { display: flex; flex-direction: column; gap: 2px; padding-inline-start: var(--space-3); border-inline-start: 2px solid var(--line-strong); }
|
|
3088
|
+
.apps-note-text { color: var(--ink); font-size: var(--text-sm); }
|
|
3089
|
+
.apps-notes-actions { display: flex; gap: var(--space-2); }
|
|
3090
|
+
.apps-empty { display: flex; flex-direction: column; align-items: center; gap: var(--space-2); padding: var(--space-6) var(--space-4); color: var(--muted); font-size: var(--text-sm); text-align: center; }
|
|
3091
|
+
.apps-empty strong { color: var(--ink); font-size: var(--text-base); }
|
|
3092
|
+
.apps-empty p { margin: 0; max-inline-size: 34ch; }
|
|
3093
|
+
.apps-foot { margin: 0; padding: 0 var(--space-1); color: var(--muted); font-size: var(--text-xs); line-height: var(--lh-xs); }
|
|
2963
3094
|
/* Peek: an app preview framed in a drawer over the chat (PreviewPeek.tsx). */
|
|
2964
3095
|
.preview-peek { display: block; inline-size: 100%; block-size: 100%; border: 0; border-radius: var(--radius-md); background: var(--bg); }
|
|
3096
|
+
/* Share-sheet landing (ShareDestinationSheet.tsx): what was shared, then one
|
|
3097
|
+
row per destination — AppRow, with the session's app preview when it has one. */
|
|
3098
|
+
.share-sheet { display: flex; flex-direction: column; gap: var(--space-3); }
|
|
3099
|
+
.share-shared { display: flex; flex-direction: column; gap: var(--space-2); margin: 0; }
|
|
3100
|
+
.share-thumbs { display: flex; gap: var(--space-2); overflow-x: auto; }
|
|
3101
|
+
.share-thumb { flex: none; block-size: 96px; inline-size: auto; max-inline-size: 160px; object-fit: cover; border: 1px solid var(--line); border-radius: var(--radius-md); background: var(--surface-2); }
|
|
3102
|
+
.share-dests { display: flex; flex-direction: column; gap: var(--space-1); }
|
|
3103
|
+
.share-dest { display: block; inline-size: 100%; padding: 0; border: 1px solid var(--line); border-radius: var(--radius-md); background: var(--surface); color: inherit; font: inherit; text-align: start; cursor: pointer; }
|
|
3104
|
+
.share-dest:hover { background: var(--surface-2); }
|
|
3105
|
+
.share-dest .app-row { min-block-size: 44px; }
|
|
3106
|
+
.preview-peek-stage { position: relative; block-size: 100%; }
|
|
3107
|
+
/* Point and speak: the recording bar floats over the top of the preview while
|
|
3108
|
+
the finger stays on the shell's mic, so nothing under it moves. */
|
|
3109
|
+
/* Draw: making the marked-up picture, or the choice when screenshots are off. */
|
|
3110
|
+
.preview-marking { position: absolute; inset-inline: var(--space-2); bottom: var(--space-2); z-index: var(--z-sticky); box-shadow: var(--shadow-lg); border-radius: var(--radius-md); background: var(--surface); }
|
|
3111
|
+
.preview-marking .banner { margin: 0; }
|
|
3112
|
+
.preview-marking .banner-actions { flex-wrap: wrap; }
|
|
3113
|
+
.preview-voice { position: absolute; inset-inline: var(--space-2); top: var(--space-2); z-index: var(--z-sticky); padding: var(--space-1) var(--space-2); border: 1px solid var(--line-strong); border-radius: var(--radius-full); background: var(--surface); box-shadow: var(--shadow-lg); }
|
|
2965
3114
|
.app-terminal-status { padding: var(--space-2) var(--space-4); margin: 0; color: var(--muted); font-size: var(--text-sm); }
|
|
2966
|
-
.app-view-command { overflow-wrap: anywhere; white-space: pre-wrap; font-size: var(--text-xs); color: var(--muted); }
|
|
2967
|
-
.app-view-row .artifact-main { flex-basis: 60%; }
|
|
2968
3115
|
.artifacts-group { display: flex; flex-direction: column; gap: 8px; margin-bottom: 16px; }
|
|
2969
3116
|
.artifacts-group:last-child { margin-bottom: 0; }
|
|
2970
3117
|
.artifacts-group-title { font-size: 12px; font-weight: 600; opacity: 0.6; text-transform: uppercase; letter-spacing: 0.04em; }
|
|
@@ -3201,3 +3348,8 @@ select.picker-search { appearance: auto; }
|
|
|
3201
3348
|
.automation-history-view .autom-section-head { align-items: flex-start; gap: var(--space-3); }
|
|
3202
3349
|
.automation-history-view .autom-section-head > strong { min-width: 0; line-height: var(--lh-lg); }
|
|
3203
3350
|
}
|
|
3351
|
+
|
|
3352
|
+
/* Handoff notes above the composer (HandoffBanner, StandbyNotice): shaped like
|
|
3353
|
+
the composer itself, with one line of another device's draft. */
|
|
3354
|
+
.handoff-banner { border-radius: var(--radius-md); }
|
|
3355
|
+
.handoff-preview { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
package/dist/apps/tokens.css
CHANGED
|
@@ -62,6 +62,12 @@
|
|
|
62
62
|
--s-github: #24292f; --s-slack: #7c2d92; --s-linear: #5e6ad2; --s-schedule: #0e7490;
|
|
63
63
|
--s-webhook: #6d28d9; --s-manual: #b45309; --s-cli: #3f6212; --s-app: #64748b;
|
|
64
64
|
|
|
65
|
+
/* ---- Annotation: marks drawn over an app preview. The same in both themes
|
|
66
|
+
(the app underneath has its own), vivid and rare in real UIs, with a white
|
|
67
|
+
halo so it reads on light and dark pages. The node composites it too. ---- */
|
|
68
|
+
--annotate: #ff2d78;
|
|
69
|
+
--annotate-halo: #ffffff;
|
|
70
|
+
|
|
65
71
|
/* ---- Terminal text selection ---- */
|
|
66
72
|
--term-selection: rgba(63, 99, 119, 0.30);
|
|
67
73
|
--term-selection-inactive: rgba(63, 99, 119, 0.20);
|
package/dist/apps/types.js
CHANGED
|
@@ -1,5 +1,22 @@
|
|
|
1
1
|
// SPDX-License-Identifier: AGPL-3.0-only
|
|
2
2
|
// Copyright (c) 2026 Petter André Sjulstad
|
|
3
|
+
export const REVIEW_CARD_MODES = ["ready", "every", "off"];
|
|
4
|
+
export const APP_REVIEW_BLOCK = "bivy_app_review";
|
|
5
|
+
const isShot = (value) => {
|
|
6
|
+
const shot = value;
|
|
7
|
+
return !!shot && typeof shot.hash === "string" && /^[0-9a-f]{64}$/.test(shot.hash) && [shot.size, shot.width, shot.height].every((n) => typeof n === "number" && n >= 0);
|
|
8
|
+
};
|
|
9
|
+
export function isAppReview(value) {
|
|
10
|
+
if (!value || typeof value !== "object")
|
|
11
|
+
return false;
|
|
12
|
+
const review = value;
|
|
13
|
+
return typeof review.id === "string" && typeof review.sessionId === "string" && typeof review.appId === "string" && /^[a-f0-9]{32}$/.test(review.appId)
|
|
14
|
+
&& typeof review.viewId === "string" && typeof review.name === "string" && typeof review.view === "string" && typeof review.path === "string"
|
|
15
|
+
&& (review.trigger === "present" || review.trigger === "run" || review.trigger === "asked" || review.trigger === "notes") && typeof review.at === "number"
|
|
16
|
+
&& (review.shot === undefined || isShot(review.shot)) && (review.before === undefined || isShot(review.before))
|
|
17
|
+
&& (review.note === undefined || typeof review.note === "string")
|
|
18
|
+
&& (review.notes === undefined || (Number.isInteger(review.notes) && review.notes >= 0));
|
|
19
|
+
}
|
|
3
20
|
export const APP_PUBLICATION_BLOCK = "bivy_app";
|
|
4
21
|
export function isAppReference(value) {
|
|
5
22
|
if (!value || typeof value !== "object")
|
package/dist/attach.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// SPDX-License-Identifier: AGPL-3.0-only
|
|
2
2
|
// Copyright (c) 2026 Petter André Sjulstad
|
|
3
|
+
import { hostname } from "node:os";
|
|
3
4
|
import { StringDecoder } from "node:string_decoder";
|
|
4
5
|
import { WebSocket } from "ws";
|
|
5
6
|
/**
|
|
@@ -50,6 +51,9 @@ const TTY_MODE_RESET = "\x1b[?1049l" + // leave the alternate screen
|
|
|
50
51
|
"\x1b[?1l\x1b>" + // cursor keys + keypad back to normal mode
|
|
51
52
|
"\x1b[?25h" + // cursor visible
|
|
52
53
|
"\x1b[0m"; // reset colors/attributes
|
|
54
|
+
/** How this terminal names itself to the node, so other devices can say where a
|
|
55
|
+
* session was last driven ("Terminal on my-laptop"). */
|
|
56
|
+
const DEVICE = { id: `terminal:${hostname()}`, label: `Terminal on ${hostname()}` };
|
|
53
57
|
function parseArgs(argv) {
|
|
54
58
|
let url = process.env.BIVY_URL || `http://localhost:${process.env.PORT || "4317"}`;
|
|
55
59
|
let token = process.env.BIVY_DEVICE_TOKEN || undefined;
|
|
@@ -102,6 +106,9 @@ function bridge(args, open) {
|
|
|
102
106
|
let bound = false;
|
|
103
107
|
let detachArmed = 0;
|
|
104
108
|
let settled = false;
|
|
109
|
+
// Set while the session this terminal showed is continuing as a chat in the
|
|
110
|
+
// app: the next key decides whether to take it back here or leave.
|
|
111
|
+
let handedToChat;
|
|
105
112
|
const inputDecoder = new StringDecoder("utf8");
|
|
106
113
|
const stdin = process.stdin;
|
|
107
114
|
const wasRaw = stdin.isTTY ? stdin.isRaw : false;
|
|
@@ -144,6 +151,21 @@ function bridge(args, open) {
|
|
|
144
151
|
socket.send(JSON.stringify(payload));
|
|
145
152
|
};
|
|
146
153
|
const onInput = (data) => {
|
|
154
|
+
if (handedToChat) {
|
|
155
|
+
const sessionId = handedToChat;
|
|
156
|
+
handedToChat = undefined;
|
|
157
|
+
if (data[0] !== 0x0d && data[0] !== 0x0a) {
|
|
158
|
+
finish(0);
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
// Reopen the chat's own TUI, resuming the same conversation; the node
|
|
162
|
+
// locks the chat again and every device sees the handoff.
|
|
163
|
+
process.stdout.write(c.dim("Taking it back…\r\n"));
|
|
164
|
+
modesReset = false;
|
|
165
|
+
const { cols, rows } = termSize();
|
|
166
|
+
send({ kind: "terminal.open.tui", sessionId, cols, rows });
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
147
169
|
// Ctrl-\ (0x1c) twice in a row detaches, leaving the daemon session alive.
|
|
148
170
|
if (data.length === 1 && data[0] === 0x1c) {
|
|
149
171
|
const now = Date.now();
|
|
@@ -212,6 +234,17 @@ function bridge(args, open) {
|
|
|
212
234
|
if (typeof msg.data === "string")
|
|
213
235
|
process.stdout.write(msg.data);
|
|
214
236
|
}
|
|
237
|
+
else if (type === "terminal.closed" && msg.reason === "chat" && msg.termId === termId) {
|
|
238
|
+
resetModes();
|
|
239
|
+
termId = undefined;
|
|
240
|
+
process.stdout.write(c.dim("\r\n[continued as chat in the Bivy app]\r\n"));
|
|
241
|
+
if (typeof msg.sessionId !== "string" || !msg.sessionId) {
|
|
242
|
+
finish(0);
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
handedToChat = msg.sessionId;
|
|
246
|
+
process.stdout.write(`Press ${c.bold("Enter")} to take it back here, or any other key to leave.\r\n`);
|
|
247
|
+
}
|
|
215
248
|
else if ((type === "terminal.exit" || type === "terminal.closed") && msg.termId === termId) {
|
|
216
249
|
const code = typeof msg.code === "number" ? msg.code : 0;
|
|
217
250
|
resetModes();
|
|
@@ -220,7 +253,7 @@ function bridge(args, open) {
|
|
|
220
253
|
}
|
|
221
254
|
else if (type === "terminal.error") {
|
|
222
255
|
process.stderr.write(c.red(`\r\n${String(msg.error || "Terminal error")}\r\n`));
|
|
223
|
-
if (!bound)
|
|
256
|
+
if (!bound || !termId)
|
|
224
257
|
finish(1);
|
|
225
258
|
}
|
|
226
259
|
});
|
|
@@ -247,7 +280,7 @@ function openDetached(args, spec, cols, rows) {
|
|
|
247
280
|
catch { }
|
|
248
281
|
resolve(code);
|
|
249
282
|
};
|
|
250
|
-
socket.on("open", () => socket.send(JSON.stringify({ kind: "terminal.open.run", agent: spec.agent, label: spec.label, name: spec.name, model: spec.model, command: spec.command, args: spec.args ?? [], workspace: spec.workspace, sessionId: spec.sessionId, cols, rows })));
|
|
283
|
+
socket.on("open", () => socket.send(JSON.stringify({ kind: "terminal.open.run", agent: spec.agent, label: spec.label, name: spec.name, model: spec.model, command: spec.command, args: spec.args ?? [], workspace: spec.workspace, sessionId: spec.sessionId, cols, rows, device: DEVICE })));
|
|
251
284
|
socket.on("error", (err) => {
|
|
252
285
|
process.stderr.write(c.red(`Connection error: ${err instanceof Error ? err.message : String(err)}\n`));
|
|
253
286
|
finish(1);
|
|
@@ -283,7 +316,7 @@ async function main() {
|
|
|
283
316
|
process.exit(await openDetached(args, spec, cols, rows));
|
|
284
317
|
}
|
|
285
318
|
process.stdout.write(c.dim(`Starting ${c.cyan(spec.name || spec.label || spec.agent || spec.command)} — Ctrl-\\ Ctrl-\\ to detach\r\n`));
|
|
286
|
-
const code = await bridge(args, (ws) => ws.send(JSON.stringify({ kind: "terminal.open.run", agent: spec.agent, label: spec.label, name: spec.name, model: spec.model, command: spec.command, args: spec.args ?? [], workspace: spec.workspace, sessionId: spec.sessionId, cols, rows })));
|
|
319
|
+
const code = await bridge(args, (ws) => ws.send(JSON.stringify({ kind: "terminal.open.run", agent: spec.agent, label: spec.label, name: spec.name, model: spec.model, command: spec.command, args: spec.args ?? [], workspace: spec.workspace, sessionId: spec.sessionId, cols, rows, device: DEVICE })));
|
|
287
320
|
process.exit(code);
|
|
288
321
|
}
|
|
289
322
|
// attach
|
|
@@ -297,7 +330,7 @@ async function main() {
|
|
|
297
330
|
async function attachById(args, termId) {
|
|
298
331
|
const { cols, rows } = termSize();
|
|
299
332
|
process.stdout.write(c.dim(`Attaching to ${c.cyan(termId)} — Ctrl-\\ Ctrl-\\ to detach\r\n`));
|
|
300
|
-
const code = await bridge({ ...args, termId }, (ws) => ws.send(JSON.stringify({ kind: "terminal.attach", termId, cols, rows })));
|
|
333
|
+
const code = await bridge({ ...args, termId }, (ws) => ws.send(JSON.stringify({ kind: "terminal.attach", termId, cols, rows, device: DEVICE })));
|
|
301
334
|
process.exit(code);
|
|
302
335
|
}
|
|
303
336
|
main().catch((error) => {
|
|
@@ -6,7 +6,7 @@ export function createAppCommands(service, workspaceFor, published = () => { })
|
|
|
6
6
|
return workspace;
|
|
7
7
|
};
|
|
8
8
|
const operations = {
|
|
9
|
-
"apps.list": (msg) => service.list(
|
|
9
|
+
"apps.list": (msg) => service.list(typeof msg.sessionId === "string" ? msg.sessionId : undefined),
|
|
10
10
|
"apps.publish": (msg) => {
|
|
11
11
|
const sessionId = String(msg.sessionId);
|
|
12
12
|
const app = service.publish(sessionId, workspaceOf(sessionId), msg.manifest);
|
|
@@ -20,18 +20,30 @@ export function createAppCommands(service, workspaceFor, published = () => { })
|
|
|
20
20
|
published(app);
|
|
21
21
|
return { app };
|
|
22
22
|
},
|
|
23
|
-
"apps.open": (msg) => service.open(String(msg.sessionId), String(msg.appId), String(msg.viewId), typeof msg.returnTo === "string" ? msg.returnTo : undefined, msg.direct === true, typeof msg.scale === "number" ? msg.scale : undefined),
|
|
23
|
+
"apps.open": (msg) => service.open(String(msg.sessionId), String(msg.appId), String(msg.viewId), typeof msg.returnTo === "string" ? msg.returnTo : undefined, msg.direct === true, typeof msg.scale === "number" ? msg.scale : undefined, typeof msg.path === "string" ? msg.path : undefined),
|
|
24
24
|
"apps.shot": (msg) => service.shot(String(msg.sessionId), typeof msg.appId === "string" ? msg.appId : undefined, { widths: msg.widths, themes: msg.themes, path: typeof msg.path === "string" ? msg.path : undefined }),
|
|
25
|
+
"apps.input": (msg) => service.act(String(msg.sessionId), typeof msg.target === "string" ? msg.target : undefined, msg.action),
|
|
26
|
+
"apps.menu": (msg) => service.menu(String(msg.sessionId), typeof msg.target === "string" ? msg.target : undefined, msg.path),
|
|
27
|
+
"apps.present": (msg) => service.present(String(msg.sessionId), { target: typeof msg.target === "string" ? msg.target : undefined, path: typeof msg.path === "string" ? msg.path : undefined, note: typeof msg.note === "string" ? msg.note : undefined }),
|
|
28
|
+
"apps.showMe": (msg) => service.present(String(msg.sessionId), { target: typeof msg.appId === "string" ? msg.appId : undefined, trigger: "asked" }),
|
|
29
|
+
"apps.mute": (msg) => service.mute(String(msg.sessionId)),
|
|
30
|
+
"apps.reviewMode": (msg) => service.setReviewMode(String(msg.sessionId), String(msg.appId), msg.mode),
|
|
31
|
+
"apps.annotate": (msg) => service.annotate(String(msg.sessionId), msg),
|
|
25
32
|
"apps.clearNotes": (msg) => service.clearNotes(String(msg.sessionId), String(msg.appId), String(msg.viewId)),
|
|
33
|
+
"apps.agentNotes": (msg) => service.setAgentNotes(String(msg.sessionId), String(msg.appId), msg.enabled === true),
|
|
34
|
+
"apps.notes": (msg) => service.notes(String(msg.sessionId), { app: typeof msg.appId === "string" ? msg.appId : undefined, view: typeof msg.view === "string" ? msg.view : undefined, since: typeof msg.since === "number" ? msg.since : undefined }),
|
|
26
35
|
"apps.logs": (msg) => service.logs(String(msg.sessionId), String(msg.appId), String(msg.viewId)),
|
|
27
|
-
|
|
36
|
+
// The app UI passes exact IDs; `bivy app share` passes an app and/or view by ID or name.
|
|
37
|
+
"apps.share": (msg) => typeof msg.viewId === "string" && typeof msg.appId === "string"
|
|
38
|
+
? service.share(String(msg.sessionId), msg.appId, msg.viewId)
|
|
39
|
+
: service.shareView(String(msg.sessionId), { app: typeof msg.appId === "string" ? msg.appId : undefined, view: typeof msg.view === "string" ? msg.view : undefined }),
|
|
28
40
|
"apps.revoke": (msg) => service.revoke(String(msg.sessionId), String(msg.appId), String(msg.viewId)),
|
|
29
41
|
"apps.remove": (msg) => { service.remove(String(msg.sessionId), String(msg.appId)); return { ok: true }; },
|
|
30
42
|
};
|
|
31
43
|
return Object.fromEntries(Object.entries(operations).map(([kind, execute]) => [kind, async (msg, ctx) => {
|
|
32
44
|
try {
|
|
33
45
|
ctx.reply({ type: `${kind}.ok`, requestId: msg.requestId, ...await execute(msg) });
|
|
34
|
-
if (kind === "apps.publish" || kind === "apps.adopt" || kind === "apps.remove")
|
|
46
|
+
if (kind === "apps.publish" || kind === "apps.adopt" || kind === "apps.remove" || kind === "apps.reviewMode" || kind === "apps.agentNotes")
|
|
35
47
|
ctx.broadcast({ type: "apps.changed", sessionId: msg.sessionId });
|
|
36
48
|
}
|
|
37
49
|
catch (error) {
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
// SPDX-License-Identifier: AGPL-3.0-only
|
|
2
|
+
// Copyright (c) 2026 Petter André Sjulstad
|
|
3
|
+
//
|
|
4
|
+
// The machine-wide Artifacts index behind the sidebar's Artifacts page: every
|
|
5
|
+
// file an agent sent into a chat (`bivy attach` / `attach_to_chat`), across all
|
|
6
|
+
// of this machine's sessions. Built from the same event-log records attachment
|
|
7
|
+
// GC already sweeps, so there is no second index to keep in sync. Only what can
|
|
8
|
+
// still be opened is listed: the session exists and the blob is still stored.
|
|
9
|
+
/** Newest first, bounded like the per-session projection. */
|
|
10
|
+
export const MAX_MACHINE_ARTIFACTS = 500;
|
|
11
|
+
/** One entry per content hash — the newest send wins, so the same report sent
|
|
12
|
+
* to two sessions links to where it was sent last. */
|
|
13
|
+
export function listMachineArtifacts(deps) {
|
|
14
|
+
const byHash = new Map();
|
|
15
|
+
for (const sessionId of deps.sessionIds()) {
|
|
16
|
+
for (const record of deps.scan(sessionId)) {
|
|
17
|
+
if (record.bivyKind !== "outbound-attachment")
|
|
18
|
+
continue;
|
|
19
|
+
const { ref } = record;
|
|
20
|
+
const existing = byHash.get(ref.hash);
|
|
21
|
+
if (existing && existing.createdAt >= record.createdAt)
|
|
22
|
+
continue;
|
|
23
|
+
byHash.set(ref.hash, {
|
|
24
|
+
sessionId, hash: ref.hash, name: ref.name, mimeType: ref.mimeType, kind: ref.kind, size: ref.size,
|
|
25
|
+
...(record.caption ? { caption: record.caption } : {}),
|
|
26
|
+
createdAt: record.createdAt,
|
|
27
|
+
artifact: Boolean(record.artifact || existing?.artifact),
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return [...byHash.values()]
|
|
32
|
+
.filter((item) => deps.stored(item.hash))
|
|
33
|
+
.sort((a, b) => b.createdAt - a.createdAt)
|
|
34
|
+
.slice(0, MAX_MACHINE_ARTIFACTS);
|
|
35
|
+
}
|
|
36
|
+
export function createArtifactCommands(deps) {
|
|
37
|
+
return {
|
|
38
|
+
"artifacts.list"(msg, ctx) {
|
|
39
|
+
ctx.reply({ type: "artifacts.list.ok", requestId: msg.requestId, artifacts: listMachineArtifacts(deps) });
|
|
40
|
+
},
|
|
41
|
+
};
|
|
42
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { deviceFrom } from "../session/presence.js";
|
|
2
|
+
/** Device handoff presence, shared by relay and HTTP: read a session's driver
|
|
3
|
+
* and carried-over draft, and publish this device's unsent draft. */
|
|
4
|
+
export function createPresenceCommands(book, publish) {
|
|
5
|
+
return {
|
|
6
|
+
"session.presence.get"(input, ctx) {
|
|
7
|
+
const requestId = typeof input.requestId === "string" ? input.requestId : undefined;
|
|
8
|
+
ctx.reply({ type: "session.presence.get.ok", requestId, presence: book.get(String(input.sessionId)) });
|
|
9
|
+
},
|
|
10
|
+
"session.presence.draft"(input, ctx) {
|
|
11
|
+
const requestId = typeof input.requestId === "string" ? input.requestId : undefined;
|
|
12
|
+
const device = deviceFrom(input.device);
|
|
13
|
+
if (!device)
|
|
14
|
+
return ctx.reply({ type: "session.presence.draft.error", httpStatus: 400, requestId, error: "Missing device" });
|
|
15
|
+
publish(book.draft(String(input.sessionId), device, String(input.text ?? "")));
|
|
16
|
+
ctx.reply({ type: "session.presence.draft.ok", requestId });
|
|
17
|
+
},
|
|
18
|
+
};
|
|
19
|
+
}
|
|
@@ -3,10 +3,12 @@
|
|
|
3
3
|
/** Direct-mode adapters for operations whose canonical implementation is the
|
|
4
4
|
* client command registry. Relay and HTTP now differ only in framing. */
|
|
5
5
|
export const CLIENT_COMMAND_ROUTES = [
|
|
6
|
-
...["list", "publish", "offers", "adopt", "open", "logs", "shot", "clearNotes", "share", "revoke", "remove"].map((action) => ({ method: "post", path: `/api/apps/${action}`, kind: `apps.${action}` })),
|
|
6
|
+
...["list", "publish", "offers", "adopt", "open", "logs", "shot", "input", "menu", "present", "showMe", "mute", "reviewMode", "annotate", "clearNotes", "agentNotes", "notes", "share", "revoke", "remove"].map((action) => ({ method: "post", path: `/api/apps/${action}`, kind: `apps.${action}` })),
|
|
7
|
+
{ method: "post", path: "/api/artifacts/list", kind: "artifacts.list" },
|
|
7
8
|
{ method: "post", path: "/api/auth/credentials/native-preview", kind: "credentials.native.preview" },
|
|
8
9
|
{ method: "post", path: "/api/auth/credentials/native-import", kind: "credentials.native.import" },
|
|
9
10
|
{ method: "post", path: "/api/session/pause", kind: "session.pause" },
|
|
11
|
+
...["get", "draft"].map((action) => ({ method: "post", path: `/api/session/presence/${action}`, kind: `session.presence.${action}` })),
|
|
10
12
|
{ method: "post", path: "/api/session/resume", kind: "session.resume" },
|
|
11
13
|
{ method: "post", path: "/api/session/question/answer", kind: "session.question.answer" },
|
|
12
14
|
];
|
|
@@ -8,15 +8,29 @@ const optionalSession = { sessionId: Type.Optional(Type.String()) };
|
|
|
8
8
|
* this table remains migration-compatible; adding validation is one data row. */
|
|
9
9
|
export const CLIENT_COMMAND_SCHEMAS = {
|
|
10
10
|
ping: Type.Object(request),
|
|
11
|
-
"apps.list": Type.Object({ ...request, ...
|
|
11
|
+
"apps.list": Type.Object({ ...request, ...optionalSession }),
|
|
12
|
+
"artifacts.list": Type.Object(request),
|
|
12
13
|
"apps.publish": Type.Object({ ...request, ...session, manifest: Type.Unknown() }),
|
|
13
14
|
"apps.offers": Type.Object({ ...request, ...session }),
|
|
14
15
|
"apps.adopt": Type.Object({ ...request, ...session, port: Type.Integer({ minimum: 1024, maximum: 65535 }) }),
|
|
15
|
-
"apps.open": Type.Object({ ...request, ...session, appId: Type.String(), viewId: Type.String(), returnTo: Type.Optional(Type.String({ maxLength: 2048 })), direct: Type.Optional(Type.Boolean()), scale: Type.Optional(Type.Integer({ minimum: 1, maximum: 2 })) }),
|
|
16
|
+
"apps.open": Type.Object({ ...request, ...session, appId: Type.String(), viewId: Type.String(), returnTo: Type.Optional(Type.String({ maxLength: 2048 })), direct: Type.Optional(Type.Boolean()), scale: Type.Optional(Type.Integer({ minimum: 1, maximum: 2 })), path: Type.Optional(Type.String({ maxLength: 2048 })) }),
|
|
16
17
|
"apps.shot": Type.Object({ ...request, ...session, appId: Type.Optional(Type.String()), widths: Type.Optional(Type.Array(Type.Integer(), { maxItems: 4 })), themes: Type.Optional(Type.Array(Type.Union([Type.Literal("light"), Type.Literal("dark")]), { maxItems: 2 })), path: Type.Optional(Type.String({ maxLength: 2048 })) }),
|
|
18
|
+
"apps.input": Type.Object({ ...request, ...session, target: Type.Optional(Type.String({ maxLength: 200 })), action: Type.Object({ kind: Type.String({ maxLength: 20 }) }) }),
|
|
19
|
+
"apps.menu": Type.Object({ ...request, ...session, target: Type.Optional(Type.String({ maxLength: 200 })), path: Type.Optional(Type.Array(Type.String({ minLength: 1, maxLength: 200 }), { minItems: 1, maxItems: 8 })) }),
|
|
20
|
+
"apps.present": Type.Object({ ...request, ...session, target: Type.Optional(Type.String({ maxLength: 200 })), path: Type.Optional(Type.String({ maxLength: 2048 })), note: Type.Optional(Type.String({ maxLength: 2000 })) }),
|
|
21
|
+
"apps.showMe": Type.Object({ ...request, ...session, appId: Type.Optional(Type.String()) }),
|
|
22
|
+
"apps.mute": Type.Object({ ...request, ...session }),
|
|
23
|
+
"apps.reviewMode": Type.Object({ ...request, ...session, appId: Type.String(), mode: Type.Union([Type.Literal("ready"), Type.Literal("every"), Type.Literal("off")]) }),
|
|
24
|
+
"apps.annotate": Type.Object({ ...request, ...session, appId: Type.String(), viewId: Type.String(), path: Type.Optional(Type.String({ maxLength: 2048 })),
|
|
25
|
+
viewport: Type.Object({ width: Type.Number(), height: Type.Number() }), scroll: Type.Optional(Type.Object({ x: Type.Number(), y: Type.Number() })),
|
|
26
|
+
dpr: Type.Optional(Type.Number()), theme: Type.Optional(Type.Union([Type.Literal("light"), Type.Literal("dark")])), compare: Type.Optional(Type.Integer({ minimum: 0 })),
|
|
27
|
+
strokes: Type.Array(Type.Object({ tool: Type.Union([Type.Literal("pen"), Type.Literal("box")]), points: Type.Array(Type.Array(Type.Number(), { minItems: 2, maxItems: 2 }), { maxItems: 10000 }) }), { minItems: 1, maxItems: 50 }),
|
|
28
|
+
signals: Type.Optional(Type.Record(Type.String(), Type.Boolean())) }),
|
|
17
29
|
"apps.clearNotes": Type.Object({ ...request, ...session, appId: Type.String(), viewId: Type.String() }),
|
|
30
|
+
"apps.agentNotes": Type.Object({ ...request, ...session, appId: Type.String(), enabled: Type.Boolean() }),
|
|
31
|
+
"apps.notes": Type.Object({ ...request, ...session, appId: Type.Optional(Type.String({ maxLength: 200 })), view: Type.Optional(Type.String({ maxLength: 200 })), since: Type.Optional(Type.Number()) }),
|
|
18
32
|
"apps.logs": Type.Object({ ...request, ...session, appId: Type.String(), viewId: Type.String() }),
|
|
19
|
-
"apps.share": Type.Object({ ...request, ...session, appId: Type.String(), viewId: Type.String() }),
|
|
33
|
+
"apps.share": Type.Object({ ...request, ...session, appId: Type.Optional(Type.String({ maxLength: 200 })), viewId: Type.Optional(Type.String()), view: Type.Optional(Type.String({ maxLength: 200 })) }),
|
|
20
34
|
"apps.revoke": Type.Object({ ...request, ...session, appId: Type.String(), viewId: Type.String() }),
|
|
21
35
|
"apps.remove": Type.Object({ ...request, ...session, appId: Type.String() }),
|
|
22
36
|
"credentials.native.preview": Type.Object({ ...request, label: Type.Optional(Type.String({ maxLength: 100 })) }),
|
|
@@ -28,6 +42,8 @@ export const CLIENT_COMMAND_SCHEMAS = {
|
|
|
28
42
|
}),
|
|
29
43
|
"attachment.fetch": Type.Object({ ...request, hash: Type.String() }),
|
|
30
44
|
"session.pause": Type.Object(session),
|
|
45
|
+
"session.presence.get": Type.Object({ ...request, ...session }),
|
|
46
|
+
"session.presence.draft": Type.Object({ ...request, ...session, device: Type.Object({ id: Type.String({ maxLength: 100 }), label: Type.Optional(Type.String({ maxLength: 200 })) }), text: Type.String({ maxLength: 20_000 }) }),
|
|
31
47
|
"session.resume": Type.Object(session),
|
|
32
48
|
"session.question.answer": Type.Object({ ...optionalSession, requestId: Type.String(), answers: Type.Optional(Type.Record(Type.String(), Type.String())) }),
|
|
33
49
|
"session.replay": Type.Object({ ...session, afterSeq: Type.Optional(Type.Number()) }),
|