callwalkietalkie 0.8.0 → 0.9.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/package.json +1 -1
- package/runtime/agent_ui.html +286 -20
- package/runtime/cmux-adapter/cmux_adapter.py +96 -32
- package/runtime/winproxy.py +55 -1
package/package.json
CHANGED
package/runtime/agent_ui.html
CHANGED
|
@@ -81,6 +81,17 @@
|
|
|
81
81
|
-webkit-overflow-scrolling: touch;
|
|
82
82
|
border-top: 1px solid var(--line);
|
|
83
83
|
}
|
|
84
|
+
.sess-section-title {
|
|
85
|
+
margin: 0;
|
|
86
|
+
padding: 12px 16px;
|
|
87
|
+
background: var(--ground);
|
|
88
|
+
border-bottom: 1px solid var(--line);
|
|
89
|
+
color: var(--muted);
|
|
90
|
+
font-size: 12px;
|
|
91
|
+
font-weight: 600;
|
|
92
|
+
letter-spacing: 0.06em;
|
|
93
|
+
text-transform: uppercase;
|
|
94
|
+
}
|
|
84
95
|
.sess-row {
|
|
85
96
|
position: relative;
|
|
86
97
|
overflow: hidden;
|
|
@@ -135,6 +146,112 @@
|
|
|
135
146
|
background: transparent;
|
|
136
147
|
}
|
|
137
148
|
.sess.active .dot { background: var(--live); }
|
|
149
|
+
.sess.browser .dot { background: #94a3b8; }
|
|
150
|
+
.sess.browser.active .dot { background: var(--sent); }
|
|
151
|
+
.sess .kind {
|
|
152
|
+
flex: none;
|
|
153
|
+
font-size: 10px;
|
|
154
|
+
font-weight: 700;
|
|
155
|
+
letter-spacing: 0.06em;
|
|
156
|
+
text-transform: uppercase;
|
|
157
|
+
color: var(--muted);
|
|
158
|
+
border: 1px solid var(--line);
|
|
159
|
+
border-radius: 6px;
|
|
160
|
+
padding: 2px 6px;
|
|
161
|
+
align-self: flex-start;
|
|
162
|
+
margin-top: 2px;
|
|
163
|
+
}
|
|
164
|
+
.sess.browser .kind {
|
|
165
|
+
color: var(--sent);
|
|
166
|
+
border-color: color-mix(in srgb, var(--sent) 35%, var(--line));
|
|
167
|
+
background: color-mix(in srgb, var(--sent) 8%, transparent);
|
|
168
|
+
}
|
|
169
|
+
#browser {
|
|
170
|
+
display: flex;
|
|
171
|
+
flex-direction: column;
|
|
172
|
+
height: 100%;
|
|
173
|
+
background: #0b0d10;
|
|
174
|
+
}
|
|
175
|
+
#browser[hidden] { display: none !important; }
|
|
176
|
+
#btop {
|
|
177
|
+
flex: none;
|
|
178
|
+
display: flex;
|
|
179
|
+
align-items: center;
|
|
180
|
+
gap: 10px;
|
|
181
|
+
padding: 8px 14px 10px;
|
|
182
|
+
border-bottom: 1px solid rgba(255,255,255,.08);
|
|
183
|
+
background: rgba(12,14,18,.96);
|
|
184
|
+
color: #f4f4f5;
|
|
185
|
+
}
|
|
186
|
+
#bback {
|
|
187
|
+
flex: none;
|
|
188
|
+
width: 36px;
|
|
189
|
+
height: 36px;
|
|
190
|
+
border: none;
|
|
191
|
+
background: transparent;
|
|
192
|
+
color: #7dd3fc;
|
|
193
|
+
font-size: 28px;
|
|
194
|
+
line-height: 1;
|
|
195
|
+
padding: 0;
|
|
196
|
+
cursor: pointer;
|
|
197
|
+
}
|
|
198
|
+
#btitlewrap { flex: 1; min-width: 0; }
|
|
199
|
+
#btitle {
|
|
200
|
+
font-size: 16px;
|
|
201
|
+
font-weight: 600;
|
|
202
|
+
display: flex;
|
|
203
|
+
align-items: center;
|
|
204
|
+
gap: 7px;
|
|
205
|
+
white-space: nowrap;
|
|
206
|
+
overflow: hidden;
|
|
207
|
+
text-overflow: ellipsis;
|
|
208
|
+
}
|
|
209
|
+
#btitle .live {
|
|
210
|
+
width: 7px;
|
|
211
|
+
height: 7px;
|
|
212
|
+
border-radius: 4px;
|
|
213
|
+
background: var(--live);
|
|
214
|
+
flex: none;
|
|
215
|
+
}
|
|
216
|
+
#bstatus {
|
|
217
|
+
font-size: 12px;
|
|
218
|
+
color: #a1a1aa;
|
|
219
|
+
font-family: var(--mono);
|
|
220
|
+
white-space: nowrap;
|
|
221
|
+
overflow: hidden;
|
|
222
|
+
text-overflow: ellipsis;
|
|
223
|
+
}
|
|
224
|
+
#bstage {
|
|
225
|
+
flex: 1;
|
|
226
|
+
min-height: 0;
|
|
227
|
+
position: relative;
|
|
228
|
+
display: flex;
|
|
229
|
+
align-items: center;
|
|
230
|
+
justify-content: center;
|
|
231
|
+
background: #0b0d10;
|
|
232
|
+
overflow: hidden;
|
|
233
|
+
}
|
|
234
|
+
#bframe {
|
|
235
|
+
max-width: 100%;
|
|
236
|
+
max-height: 100%;
|
|
237
|
+
object-fit: contain;
|
|
238
|
+
display: block;
|
|
239
|
+
background: #111;
|
|
240
|
+
}
|
|
241
|
+
#bframe[hidden] { display: none; }
|
|
242
|
+
#bempty {
|
|
243
|
+
position: absolute;
|
|
244
|
+
inset: 0;
|
|
245
|
+
display: flex;
|
|
246
|
+
align-items: center;
|
|
247
|
+
justify-content: center;
|
|
248
|
+
color: #a1a1aa;
|
|
249
|
+
font-size: 14px;
|
|
250
|
+
padding: 24px;
|
|
251
|
+
text-align: center;
|
|
252
|
+
}
|
|
253
|
+
#bempty[hidden] { display: none; }
|
|
254
|
+
#bempty.err { color: #fda4af; }
|
|
138
255
|
.sess .body { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 2px; }
|
|
139
256
|
.sess .top { display: flex; justify-content: space-between; gap: 8px; }
|
|
140
257
|
.sess .name {
|
|
@@ -459,7 +576,7 @@
|
|
|
459
576
|
</div>
|
|
460
577
|
<div id="sess-list"></div>
|
|
461
578
|
<div id="sessions-foot">
|
|
462
|
-
<span class="hint">
|
|
579
|
+
<span class="hint">Terminals chat · browsers live-stream · <button id="restore-sessions" type="button">Restore</button></span>
|
|
463
580
|
<button id="leave" type="button">Disconnect</button>
|
|
464
581
|
</div>
|
|
465
582
|
</div>
|
|
@@ -482,6 +599,20 @@
|
|
|
482
599
|
</div>
|
|
483
600
|
</div>
|
|
484
601
|
|
|
602
|
+
<div id="browser" hidden>
|
|
603
|
+
<div id="btop">
|
|
604
|
+
<button id="bback" type="button" aria-label="Back to sessions">‹</button>
|
|
605
|
+
<div id="btitlewrap">
|
|
606
|
+
<div id="btitle"><span class="live" id="blivedot" hidden></span><span id="btitletext">Browser</span></div>
|
|
607
|
+
<div id="bstatus">…</div>
|
|
608
|
+
</div>
|
|
609
|
+
</div>
|
|
610
|
+
<div id="bstage">
|
|
611
|
+
<img id="bframe" alt="Live browser view" hidden />
|
|
612
|
+
<div id="bempty">Starting live view…</div>
|
|
613
|
+
</div>
|
|
614
|
+
</div>
|
|
615
|
+
|
|
485
616
|
<script>
|
|
486
617
|
(function() {
|
|
487
618
|
var TOKEN = __TOKEN_JSON__;
|
|
@@ -489,6 +620,7 @@
|
|
|
489
620
|
var API = __API_BASE_JSON__;
|
|
490
621
|
var sessionsView = document.getElementById("sessions");
|
|
491
622
|
var threadView = document.getElementById("thread");
|
|
623
|
+
var browserView = document.getElementById("browser");
|
|
492
624
|
var sessList = document.getElementById("sess-list");
|
|
493
625
|
var sessionsMeta = document.getElementById("sessions-meta");
|
|
494
626
|
var log = document.getElementById("log");
|
|
@@ -501,6 +633,12 @@
|
|
|
501
633
|
var backBtn = document.getElementById("back");
|
|
502
634
|
var leaveBtn = document.getElementById("leave");
|
|
503
635
|
var restoreBtn = document.getElementById("restore-sessions");
|
|
636
|
+
var bbackBtn = document.getElementById("bback");
|
|
637
|
+
var btitleText = document.getElementById("btitletext");
|
|
638
|
+
var bstatusEl = document.getElementById("bstatus");
|
|
639
|
+
var bliveDot = document.getElementById("blivedot");
|
|
640
|
+
var bframe = document.getElementById("bframe");
|
|
641
|
+
var bempty = document.getElementById("bempty");
|
|
504
642
|
|
|
505
643
|
var misses = 0;
|
|
506
644
|
var lastSig = "";
|
|
@@ -520,6 +658,13 @@
|
|
|
520
658
|
var lastSessions = [];
|
|
521
659
|
var hiddenSessions = loadHidden();
|
|
522
660
|
var openSwipeRow = null;
|
|
661
|
+
var activeType = "terminal";
|
|
662
|
+
var activeUrl = "";
|
|
663
|
+
var activeSurface = null;
|
|
664
|
+
var activeWorkspace = null;
|
|
665
|
+
var inBrowser = false;
|
|
666
|
+
var previewTimer = null;
|
|
667
|
+
var previewBusy = false;
|
|
523
668
|
|
|
524
669
|
function loadHidden() {
|
|
525
670
|
try {
|
|
@@ -575,16 +720,92 @@
|
|
|
575
720
|
return String(p).replace(/^\/Users\/[^\/]+/, "~");
|
|
576
721
|
}
|
|
577
722
|
|
|
723
|
+
function shortUrl(u) {
|
|
724
|
+
if (!u) return "";
|
|
725
|
+
try {
|
|
726
|
+
var x = new URL(u);
|
|
727
|
+
var path = x.pathname === "/" ? "" : x.pathname;
|
|
728
|
+
return x.host + path + (x.search || "");
|
|
729
|
+
} catch (e) {
|
|
730
|
+
return String(u);
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
function stopBrowserStream() {
|
|
735
|
+
if (previewTimer) {
|
|
736
|
+
clearTimeout(previewTimer);
|
|
737
|
+
previewTimer = null;
|
|
738
|
+
}
|
|
739
|
+
previewBusy = false;
|
|
740
|
+
if (bframe) {
|
|
741
|
+
bframe.removeAttribute("src");
|
|
742
|
+
bframe.hidden = true;
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
|
|
578
746
|
function showSessions() {
|
|
579
747
|
inThread = false;
|
|
748
|
+
inBrowser = false;
|
|
749
|
+
stopBrowserStream();
|
|
580
750
|
sessionsView.hidden = false;
|
|
581
751
|
threadView.hidden = true;
|
|
752
|
+
if (browserView) browserView.hidden = true;
|
|
582
753
|
}
|
|
583
754
|
|
|
584
755
|
function showThread() {
|
|
585
756
|
inThread = true;
|
|
757
|
+
inBrowser = false;
|
|
758
|
+
stopBrowserStream();
|
|
586
759
|
sessionsView.hidden = true;
|
|
587
760
|
threadView.hidden = false;
|
|
761
|
+
if (browserView) browserView.hidden = true;
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
function showBrowserLive() {
|
|
765
|
+
inThread = false;
|
|
766
|
+
inBrowser = true;
|
|
767
|
+
sessionsView.hidden = true;
|
|
768
|
+
threadView.hidden = true;
|
|
769
|
+
if (browserView) browserView.hidden = false;
|
|
770
|
+
if (btitleText) btitleText.textContent = titleText.textContent || "Browser";
|
|
771
|
+
if (bstatusEl) bstatusEl.textContent = activeUrl ? shortUrl(activeUrl) : "live view";
|
|
772
|
+
if (bempty) {
|
|
773
|
+
bempty.hidden = false;
|
|
774
|
+
bempty.classList.remove("err");
|
|
775
|
+
bempty.textContent = "Starting live view…";
|
|
776
|
+
}
|
|
777
|
+
if (bliveDot) bliveDot.hidden = true;
|
|
778
|
+
stopBrowserStream();
|
|
779
|
+
pullBrowserFrame();
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
function pullBrowserFrame() {
|
|
783
|
+
if (!inBrowser || previewBusy) return;
|
|
784
|
+
previewBusy = true;
|
|
785
|
+
var url = API + "/preview?t=" + encodeURIComponent(TOKEN) + "&_=" + Date.now();
|
|
786
|
+
if (activeSurface) url += "&surface=" + encodeURIComponent(activeSurface);
|
|
787
|
+
if (activeWorkspace) url += "&workspace=" + encodeURIComponent(activeWorkspace);
|
|
788
|
+
var img = new Image();
|
|
789
|
+
img.onload = function() {
|
|
790
|
+
if (!inBrowser) { previewBusy = false; return; }
|
|
791
|
+
bframe.src = img.src;
|
|
792
|
+
bframe.hidden = false;
|
|
793
|
+
if (bempty) bempty.hidden = true;
|
|
794
|
+
if (bliveDot) bliveDot.hidden = false;
|
|
795
|
+
previewBusy = false;
|
|
796
|
+
previewTimer = setTimeout(pullBrowserFrame, 800);
|
|
797
|
+
};
|
|
798
|
+
img.onerror = function() {
|
|
799
|
+
previewBusy = false;
|
|
800
|
+
if (!inBrowser) return;
|
|
801
|
+
if (bframe.hidden && bempty) {
|
|
802
|
+
bempty.hidden = false;
|
|
803
|
+
bempty.classList.add("err");
|
|
804
|
+
bempty.textContent = "Can't stream this browser tab yet";
|
|
805
|
+
}
|
|
806
|
+
previewTimer = setTimeout(pullBrowserFrame, 1600);
|
|
807
|
+
};
|
|
808
|
+
img.src = url;
|
|
588
809
|
}
|
|
589
810
|
|
|
590
811
|
function inlineMd(s) {
|
|
@@ -809,38 +1030,58 @@
|
|
|
809
1030
|
return hiddenSessions.indexOf(sessionKey(s)) === -1;
|
|
810
1031
|
});
|
|
811
1032
|
if (!visible.length) {
|
|
812
|
-
var
|
|
1033
|
+
var emptyMsg = lastSessions.length
|
|
813
1034
|
? "All sessions hidden. Swipe deleted rows are stored on this phone."
|
|
814
|
-
: "No cmux terminals open.";
|
|
815
|
-
sessList.innerHTML = '<div class="turn system" style="padding:24px 16px"><div class="bubble">' +
|
|
1035
|
+
: "No cmux terminals or browsers open.";
|
|
1036
|
+
sessList.innerHTML = '<div class="turn system" style="padding:24px 16px"><div class="bubble">' + emptyMsg + "</div></div>";
|
|
816
1037
|
return;
|
|
817
1038
|
}
|
|
818
|
-
|
|
1039
|
+
function renderSession(s) {
|
|
819
1040
|
var surface = s.surface || s.surface_id || "";
|
|
820
1041
|
var workspace = s.workspace || s.workspace_id || "";
|
|
821
|
-
var
|
|
822
|
-
var
|
|
1042
|
+
var kind = s.type || "terminal";
|
|
1043
|
+
var isBrowser = kind === "browser";
|
|
1044
|
+
var title = esc(s.title || surface || (isBrowser ? "browser" : "session"));
|
|
1045
|
+
var path = isBrowser
|
|
1046
|
+
? esc(shortUrl(s.url || ""))
|
|
1047
|
+
: esc(shortPath(s.cwd || s.current_directory || ""));
|
|
823
1048
|
var key = esc(sessionKey(s));
|
|
824
1049
|
var active = surface === currentSurface;
|
|
825
|
-
var snip =
|
|
1050
|
+
var snip = isBrowser
|
|
1051
|
+
? (active ? "Live stream" : "Browser · tap for live view")
|
|
1052
|
+
: (active ? "Open in this chat" : "Terminal · tap to chat");
|
|
826
1053
|
return '<div class="sess-row" data-key="' + key + '">' +
|
|
827
1054
|
'<button type="button" class="sess-delete">Delete</button>' +
|
|
828
|
-
'<button type="button" class="sess' + (active ? " active" : "") + '"' +
|
|
1055
|
+
'<button type="button" class="sess' + (active ? " active" : "") + (isBrowser ? " browser" : "") + '"' +
|
|
829
1056
|
' data-surface="' + esc(surface) + '"' +
|
|
830
1057
|
' data-workspace="' + esc(workspace) + '"' +
|
|
831
1058
|
' data-window="' + esc(s.window || "") + '"' +
|
|
832
|
-
' data-title="' + title + '"
|
|
1059
|
+
' data-title="' + title + '"' +
|
|
1060
|
+
' data-type="' + esc(kind) + '"' +
|
|
1061
|
+
' data-url="' + esc(s.url || "") + '">' +
|
|
833
1062
|
'<span class="dot"></span>' +
|
|
834
1063
|
'<span class="body">' +
|
|
835
|
-
'<span class="top"><span class="name">' + title + '</span
|
|
1064
|
+
'<span class="top"><span class="name">' + title + '</span><span class="kind">' + (isBrowser ? "Browser" : "Terminal") + "</span></span>" +
|
|
836
1065
|
'<span class="snip">' + snip + "</span>" +
|
|
837
|
-
(
|
|
1066
|
+
(path ? '<span class="path">' + path + "</span>" : "") +
|
|
838
1067
|
"</span></button></div>";
|
|
1068
|
+
}
|
|
1069
|
+
sessList.innerHTML = [
|
|
1070
|
+
{ type: "browser", title: "Browsers" },
|
|
1071
|
+
{ type: "terminal", title: "Terminals" }
|
|
1072
|
+
].map(function(group) {
|
|
1073
|
+
var items = visible.filter(function(s) {
|
|
1074
|
+
return (s.type || "terminal") === group.type;
|
|
1075
|
+
});
|
|
1076
|
+
if (!items.length) return "";
|
|
1077
|
+
return '<section aria-labelledby="sess-' + group.type + '-heading">' +
|
|
1078
|
+
'<h2 class="sess-section-title" id="sess-' + group.type + '-heading">' + group.title + '</h2>' +
|
|
1079
|
+
items.map(renderSession).join("") + '</section>';
|
|
839
1080
|
}).join("");
|
|
840
1081
|
bindSwipeRows();
|
|
841
1082
|
}
|
|
842
1083
|
|
|
843
|
-
async function selectSession(surface, workspace, windowRef, title) {
|
|
1084
|
+
async function selectSession(surface, workspace, windowRef, title, typeHint, urlHint) {
|
|
844
1085
|
try {
|
|
845
1086
|
var r = await fetch(API + "/select?t=" + encodeURIComponent(TOKEN), {
|
|
846
1087
|
method: "POST",
|
|
@@ -853,9 +1094,12 @@
|
|
|
853
1094
|
return;
|
|
854
1095
|
}
|
|
855
1096
|
activeKey = j.surface || surface;
|
|
1097
|
+
activeSurface = j.surface || surface || null;
|
|
1098
|
+
activeWorkspace = j.workspace || workspace || null;
|
|
1099
|
+
activeType = j.type || typeHint || "terminal";
|
|
1100
|
+
activeUrl = j.url || urlHint || "";
|
|
856
1101
|
titleText.textContent = title || j.title || "cmux";
|
|
857
1102
|
liveDot.hidden = false;
|
|
858
|
-
// Hard-reset thread so the previous surface's messages can't linger.
|
|
859
1103
|
known = [];
|
|
860
1104
|
pendingUser = null;
|
|
861
1105
|
lastSig = "";
|
|
@@ -864,9 +1108,13 @@
|
|
|
864
1108
|
agentBusy = false;
|
|
865
1109
|
log.innerHTML = "";
|
|
866
1110
|
updateComposerMode();
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
1111
|
+
if (activeType === "browser") {
|
|
1112
|
+
showBrowserLive();
|
|
1113
|
+
} else {
|
|
1114
|
+
showThread();
|
|
1115
|
+
await refreshStatus();
|
|
1116
|
+
await refreshTranscript();
|
|
1117
|
+
}
|
|
870
1118
|
} catch (err) {
|
|
871
1119
|
setSessionsMeta("switch failed", "err");
|
|
872
1120
|
}
|
|
@@ -884,7 +1132,9 @@
|
|
|
884
1132
|
btn.getAttribute("data-surface"),
|
|
885
1133
|
btn.getAttribute("data-workspace"),
|
|
886
1134
|
btn.getAttribute("data-window"),
|
|
887
|
-
btn.getAttribute("data-title")
|
|
1135
|
+
btn.getAttribute("data-title"),
|
|
1136
|
+
btn.getAttribute("data-type"),
|
|
1137
|
+
btn.getAttribute("data-url")
|
|
888
1138
|
);
|
|
889
1139
|
});
|
|
890
1140
|
|
|
@@ -892,11 +1142,18 @@
|
|
|
892
1142
|
showSessions();
|
|
893
1143
|
refreshStatus();
|
|
894
1144
|
});
|
|
1145
|
+
if (bbackBtn) {
|
|
1146
|
+
bbackBtn.addEventListener("click", function() {
|
|
1147
|
+
showSessions();
|
|
1148
|
+
refreshStatus();
|
|
1149
|
+
});
|
|
1150
|
+
}
|
|
895
1151
|
|
|
896
1152
|
function disconnect() {
|
|
897
1153
|
if (leaving) return;
|
|
898
1154
|
leaving = true;
|
|
899
1155
|
clearInterval(poller);
|
|
1156
|
+
stopBrowserStream();
|
|
900
1157
|
fetch(API + "/shutdown?t=" + encodeURIComponent(TOKEN), {
|
|
901
1158
|
method: "POST",
|
|
902
1159
|
keepalive: true,
|
|
@@ -924,10 +1181,19 @@
|
|
|
924
1181
|
setSessionsMeta("localhost · live", "ok");
|
|
925
1182
|
titleText.textContent = j.title || "cmux";
|
|
926
1183
|
liveDot.hidden = false;
|
|
1184
|
+
activeType = j.type || activeType || "terminal";
|
|
1185
|
+
activeUrl = j.url || activeUrl || "";
|
|
1186
|
+
if (j.surface) activeSurface = j.surface;
|
|
1187
|
+
if (j.workspace) activeWorkspace = j.workspace;
|
|
927
1188
|
var bits = [];
|
|
928
|
-
if (j.
|
|
1189
|
+
if (j.type === "browser" && j.url) bits.push(shortUrl(j.url));
|
|
1190
|
+
else if (j.cwd) bits.push(shortPath(j.cwd));
|
|
929
1191
|
if (j.surface) bits.push(j.surface);
|
|
930
1192
|
setStatus(bits.join(" · ") || "live", "ok");
|
|
1193
|
+
if (inBrowser) {
|
|
1194
|
+
if (btitleText) btitleText.textContent = j.title || "Browser";
|
|
1195
|
+
if (bstatusEl) bstatusEl.textContent = j.url ? shortUrl(j.url) : "live view";
|
|
1196
|
+
}
|
|
931
1197
|
renderPicker(j.sessions || j.workspaces || [], j.surface);
|
|
932
1198
|
return true;
|
|
933
1199
|
} catch (e) {
|
|
@@ -940,7 +1206,7 @@
|
|
|
940
1206
|
}
|
|
941
1207
|
|
|
942
1208
|
async function refreshTranscript() {
|
|
943
|
-
if (!inThread) return;
|
|
1209
|
+
if (!inThread || inBrowser) return;
|
|
944
1210
|
try {
|
|
945
1211
|
var r = await fetch(API + "/transcript?t=" + encodeURIComponent(TOKEN));
|
|
946
1212
|
var j = await r.json();
|
|
@@ -207,6 +207,12 @@ class Target:
|
|
|
207
207
|
window_ref: Optional[str] = None
|
|
208
208
|
title: str = "cmux"
|
|
209
209
|
cwd: Optional[str] = None
|
|
210
|
+
surface_type: str = "terminal"
|
|
211
|
+
url: Optional[str] = None
|
|
212
|
+
|
|
213
|
+
@property
|
|
214
|
+
def is_browser(self) -> bool:
|
|
215
|
+
return (self.surface_type or "terminal") == "browser"
|
|
210
216
|
|
|
211
217
|
|
|
212
218
|
@dataclass
|
|
@@ -243,6 +249,10 @@ class Session:
|
|
|
243
249
|
self.target.cwd = item["cwd"]
|
|
244
250
|
if item.get("window"):
|
|
245
251
|
self.target.window_ref = item.get("window")
|
|
252
|
+
if item.get("type"):
|
|
253
|
+
self.target.surface_type = item["type"]
|
|
254
|
+
if "url" in item:
|
|
255
|
+
self.target.url = item.get("url")
|
|
246
256
|
return
|
|
247
257
|
except CmuxError:
|
|
248
258
|
pass
|
|
@@ -259,6 +269,8 @@ class Session:
|
|
|
259
269
|
break
|
|
260
270
|
|
|
261
271
|
def read_text(self, *, lines: int = 800, scrollback: bool = True) -> str:
|
|
272
|
+
if self.target.is_browser:
|
|
273
|
+
return ""
|
|
262
274
|
result = rpc(
|
|
263
275
|
"surface.read_text",
|
|
264
276
|
{
|
|
@@ -280,6 +292,8 @@ class Session:
|
|
|
280
292
|
Agents disagree on the cancel key (Ctrl+C vs Escape), and some cmux
|
|
281
293
|
builds only honor one of send_key / send_text — so we fire all of them.
|
|
282
294
|
"""
|
|
295
|
+
if self.target.is_browser:
|
|
296
|
+
raise CmuxError("Can't interrupt a browser tab")
|
|
283
297
|
self.focus()
|
|
284
298
|
base = {
|
|
285
299
|
"workspace_id": self.target.workspace_ref,
|
|
@@ -316,6 +330,8 @@ class Session:
|
|
|
316
330
|
pass
|
|
317
331
|
|
|
318
332
|
def send(self, text: str) -> None:
|
|
333
|
+
if self.target.is_browser:
|
|
334
|
+
raise CmuxError("This is a browser live view — open a terminal to chat")
|
|
319
335
|
text = text.rstrip("\n")
|
|
320
336
|
if not text:
|
|
321
337
|
raise CmuxError("empty message")
|
|
@@ -334,6 +350,8 @@ class Session:
|
|
|
334
350
|
self._sent.append(text)
|
|
335
351
|
|
|
336
352
|
def transcript(self) -> list[ChatMessage]:
|
|
353
|
+
if self.target.is_browser:
|
|
354
|
+
return []
|
|
337
355
|
screen = self.read_text()
|
|
338
356
|
self._last_text = screen
|
|
339
357
|
parsed = parse_screen(screen)
|
|
@@ -341,6 +359,8 @@ class Session:
|
|
|
341
359
|
return merge_sent(parsed, self._sent)
|
|
342
360
|
|
|
343
361
|
def busy(self) -> bool:
|
|
362
|
+
if self.target.is_browser:
|
|
363
|
+
return False
|
|
344
364
|
return is_busy(self._last_text or self.read_text())
|
|
345
365
|
|
|
346
366
|
def notify_linked(self) -> None:
|
|
@@ -371,6 +391,29 @@ class Session:
|
|
|
371
391
|
pass
|
|
372
392
|
|
|
373
393
|
|
|
394
|
+
def browser_screenshot(
|
|
395
|
+
*,
|
|
396
|
+
surface: Optional[str] = None,
|
|
397
|
+
workspace: Optional[str] = None,
|
|
398
|
+
socket_path: Optional[str] = None,
|
|
399
|
+
) -> bytes:
|
|
400
|
+
"""Capture a cmux browser surface as PNG bytes. Does not focus/steal foreground."""
|
|
401
|
+
path = socket_path or find_socket()
|
|
402
|
+
params: dict[str, Any] = {}
|
|
403
|
+
if surface:
|
|
404
|
+
params["surface_id"] = surface
|
|
405
|
+
if workspace:
|
|
406
|
+
params["workspace_id"] = workspace
|
|
407
|
+
result = rpc("browser.screenshot", params, socket_path=path) or {}
|
|
408
|
+
b64 = result.get("png_base64") or result.get("base64")
|
|
409
|
+
if b64:
|
|
410
|
+
return base64.b64decode(b64)
|
|
411
|
+
file_path = result.get("path")
|
|
412
|
+
if file_path and Path(file_path).is_file():
|
|
413
|
+
return Path(file_path).read_bytes()
|
|
414
|
+
raise CmuxError("browser screenshot returned no image")
|
|
415
|
+
|
|
416
|
+
|
|
374
417
|
# ---------------------------------------------------------------- discovery
|
|
375
418
|
|
|
376
419
|
|
|
@@ -399,7 +442,7 @@ def list_surfaces(workspace_ref: str, socket_path: Optional[str] = None) -> list
|
|
|
399
442
|
|
|
400
443
|
|
|
401
444
|
def inventory(socket_path: Optional[str] = None) -> list[dict]:
|
|
402
|
-
"""Flat list of pickable terminal sessions across windows/workspaces."""
|
|
445
|
+
"""Flat list of pickable terminal + browser sessions across windows/workspaces."""
|
|
403
446
|
path = socket_path or find_socket()
|
|
404
447
|
items: list[dict] = []
|
|
405
448
|
tree = None
|
|
@@ -411,6 +454,25 @@ def inventory(socket_path: Optional[str] = None) -> list[dict]:
|
|
|
411
454
|
except CmuxError:
|
|
412
455
|
tree = None
|
|
413
456
|
|
|
457
|
+
def _append(win_ref, ws_ref, ws_id, ws_title, ws_cwd, surf):
|
|
458
|
+
kind = surf.get("type") or "terminal"
|
|
459
|
+
if kind not in ("terminal", "browser"):
|
|
460
|
+
return
|
|
461
|
+
items.append(
|
|
462
|
+
{
|
|
463
|
+
"window": win_ref,
|
|
464
|
+
"workspace": ws_ref or ws_id,
|
|
465
|
+
"workspace_id": ws_id,
|
|
466
|
+
"surface": surf.get("ref") or surf.get("id"),
|
|
467
|
+
"surface_id": surf.get("id"),
|
|
468
|
+
"title": surf.get("title") or ws_title or kind,
|
|
469
|
+
"cwd": ws_cwd if kind == "terminal" else None,
|
|
470
|
+
"url": surf.get("url") if kind == "browser" else None,
|
|
471
|
+
"type": kind,
|
|
472
|
+
"active": bool(surf.get("active") or surf.get("focused")),
|
|
473
|
+
}
|
|
474
|
+
)
|
|
475
|
+
|
|
414
476
|
if isinstance(tree, dict) and tree.get("windows"):
|
|
415
477
|
for win in tree.get("windows") or []:
|
|
416
478
|
win_ref = win.get("ref") or win.get("window_ref")
|
|
@@ -421,20 +483,7 @@ def inventory(socket_path: Optional[str] = None) -> list[dict]:
|
|
|
421
483
|
ws_cwd = ws.get("current_directory")
|
|
422
484
|
for pane in ws.get("panes") or []:
|
|
423
485
|
for surf in pane.get("surfaces") or []:
|
|
424
|
-
|
|
425
|
-
continue
|
|
426
|
-
items.append(
|
|
427
|
-
{
|
|
428
|
-
"window": win_ref,
|
|
429
|
-
"workspace": ws_ref or ws_id,
|
|
430
|
-
"workspace_id": ws_id,
|
|
431
|
-
"surface": surf.get("ref") or surf.get("id"),
|
|
432
|
-
"surface_id": surf.get("id"),
|
|
433
|
-
"title": surf.get("title") or ws_title or "terminal",
|
|
434
|
-
"cwd": ws_cwd,
|
|
435
|
-
"active": bool(surf.get("active") or surf.get("focused")),
|
|
436
|
-
}
|
|
437
|
-
)
|
|
486
|
+
_append(win_ref, ws_ref, ws_id, ws_title, ws_cwd, surf)
|
|
438
487
|
if items:
|
|
439
488
|
return items
|
|
440
489
|
|
|
@@ -442,22 +491,13 @@ def inventory(socket_path: Optional[str] = None) -> list[dict]:
|
|
|
442
491
|
for ws in list_workspaces(socket_path=path):
|
|
443
492
|
ws_ref = ws.get("ref") or ws.get("id")
|
|
444
493
|
for surf in list_surfaces(ws_ref, socket_path=path):
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
"surface": surf.get("ref") or surf.get("id"),
|
|
453
|
-
"surface_id": surf.get("id"),
|
|
454
|
-
"title": surf.get("title")
|
|
455
|
-
or ws.get("custom_title")
|
|
456
|
-
or ws.get("title")
|
|
457
|
-
or "terminal",
|
|
458
|
-
"cwd": ws.get("current_directory"),
|
|
459
|
-
"active": bool(surf.get("focused") or surf.get("selected_in_pane")),
|
|
460
|
-
}
|
|
494
|
+
_append(
|
|
495
|
+
None,
|
|
496
|
+
ws_ref,
|
|
497
|
+
ws.get("id"),
|
|
498
|
+
ws.get("custom_title") or ws.get("title") or ws_ref,
|
|
499
|
+
ws.get("current_directory"),
|
|
500
|
+
surf,
|
|
461
501
|
)
|
|
462
502
|
return items
|
|
463
503
|
|
|
@@ -491,11 +531,21 @@ def resolve_target(
|
|
|
491
531
|
continue
|
|
492
532
|
pick = item
|
|
493
533
|
break
|
|
534
|
+
if pick is None:
|
|
535
|
+
for item in inv:
|
|
536
|
+
if item.get("active") and (item.get("type") or "terminal") == "terminal":
|
|
537
|
+
pick = item
|
|
538
|
+
break
|
|
494
539
|
if pick is None:
|
|
495
540
|
for item in inv:
|
|
496
541
|
if item.get("active"):
|
|
497
542
|
pick = item
|
|
498
543
|
break
|
|
544
|
+
if pick is None:
|
|
545
|
+
for item in inv:
|
|
546
|
+
if (item.get("type") or "terminal") == "terminal":
|
|
547
|
+
pick = item
|
|
548
|
+
break
|
|
499
549
|
if pick is None and inv:
|
|
500
550
|
pick = inv[0]
|
|
501
551
|
if pick is None:
|
|
@@ -510,10 +560,14 @@ def resolve_target(
|
|
|
510
560
|
"window": focused.get("window_ref"),
|
|
511
561
|
"title": "cmux",
|
|
512
562
|
"cwd": None,
|
|
563
|
+
"type": focused.get("surface_type") or "terminal",
|
|
564
|
+
"url": None,
|
|
513
565
|
}
|
|
514
566
|
|
|
515
567
|
# Enrich title/cwd from workspace.list when tree omitted them
|
|
516
|
-
if
|
|
568
|
+
if (pick.get("type") or "terminal") == "terminal" and (
|
|
569
|
+
not pick.get("cwd") or pick.get("title") in (None, "terminal", "cmux")
|
|
570
|
+
):
|
|
517
571
|
for ws in list_workspaces(socket_path=path):
|
|
518
572
|
if pick.get("workspace") in (ws.get("ref"), ws.get("id")):
|
|
519
573
|
pick["cwd"] = pick.get("cwd") or ws.get("current_directory")
|
|
@@ -530,6 +584,8 @@ def resolve_target(
|
|
|
530
584
|
window_ref=pick.get("window") or focused.get("window_ref"),
|
|
531
585
|
title=pick.get("title") or "cmux",
|
|
532
586
|
cwd=pick.get("cwd"),
|
|
587
|
+
surface_type=pick.get("type") or "terminal",
|
|
588
|
+
url=pick.get("url"),
|
|
533
589
|
)
|
|
534
590
|
|
|
535
591
|
|
|
@@ -595,6 +651,14 @@ def _is_chrome(line: str) -> bool:
|
|
|
595
651
|
s = line.strip()
|
|
596
652
|
if not s:
|
|
597
653
|
return True
|
|
654
|
+
label = re.sub(r"^[→›>►▶•·▸▹➤➜\-\s]+", "", s).strip().lower()
|
|
655
|
+
if label in {
|
|
656
|
+
"add a follow-up",
|
|
657
|
+
"add follow-up",
|
|
658
|
+
"follow-up",
|
|
659
|
+
"follow up",
|
|
660
|
+
} or label.startswith("add a follow-up"):
|
|
661
|
+
return True
|
|
598
662
|
if _BOXY.search(s) and len(_BOXY.findall(s)) >= 3:
|
|
599
663
|
return True
|
|
600
664
|
if s.startswith(("→ ", "→")) and ("ctrl+" in s.lower() or "stop" in s.lower()):
|
package/runtime/winproxy.py
CHANGED
|
@@ -35,6 +35,7 @@ if _ADAPTER_DIR not in sys.path:
|
|
|
35
35
|
sys.path.insert(0, _ADAPTER_DIR)
|
|
36
36
|
from cmux_adapter import ( # noqa: E402
|
|
37
37
|
CmuxError,
|
|
38
|
+
browser_screenshot,
|
|
38
39
|
inventory,
|
|
39
40
|
open_session,
|
|
40
41
|
ping as cmux_ping,
|
|
@@ -703,6 +704,8 @@ def agent_status():
|
|
|
703
704
|
"app": "cmux",
|
|
704
705
|
"title": t.title,
|
|
705
706
|
"cwd": t.cwd,
|
|
707
|
+
"url": t.url,
|
|
708
|
+
"type": t.surface_type,
|
|
706
709
|
"workspace": t.workspace_ref,
|
|
707
710
|
"surface": t.surface_ref,
|
|
708
711
|
"window": t.window_ref,
|
|
@@ -715,9 +718,11 @@ def agent_status():
|
|
|
715
718
|
"ref": s.get("workspace"),
|
|
716
719
|
"title": s.get("title"),
|
|
717
720
|
"custom_title": None,
|
|
718
|
-
"current_directory": s.get("cwd"),
|
|
721
|
+
"current_directory": s.get("cwd") or s.get("url"),
|
|
719
722
|
"selected": s.get("active"),
|
|
720
723
|
"surface": s.get("surface"),
|
|
724
|
+
"type": s.get("type"),
|
|
725
|
+
"url": s.get("url"),
|
|
721
726
|
}
|
|
722
727
|
for s in sessions
|
|
723
728
|
],
|
|
@@ -741,6 +746,16 @@ def agent_select():
|
|
|
741
746
|
workspace=workspace, surface=surface, window=window, force=True
|
|
742
747
|
)
|
|
743
748
|
session.focus()
|
|
749
|
+
# Bring cmux to the foreground so the selected tab is actually visible.
|
|
750
|
+
try:
|
|
751
|
+
subprocess.run(
|
|
752
|
+
["/usr/bin/open", "-a", "cmux"],
|
|
753
|
+
check=False,
|
|
754
|
+
capture_output=True,
|
|
755
|
+
timeout=5,
|
|
756
|
+
)
|
|
757
|
+
except Exception:
|
|
758
|
+
pass
|
|
744
759
|
t = session.target
|
|
745
760
|
return jsonify(
|
|
746
761
|
{
|
|
@@ -750,6 +765,8 @@ def agent_select():
|
|
|
750
765
|
"window": t.window_ref,
|
|
751
766
|
"title": t.title,
|
|
752
767
|
"cwd": t.cwd,
|
|
768
|
+
"url": t.url,
|
|
769
|
+
"type": t.surface_type,
|
|
753
770
|
}
|
|
754
771
|
)
|
|
755
772
|
except KeyboardInterrupt:
|
|
@@ -758,6 +775,43 @@ def agent_select():
|
|
|
758
775
|
return jsonify({"ok": False, "error": _agent_error_message(e)}), 503
|
|
759
776
|
|
|
760
777
|
|
|
778
|
+
@app.route("/agent/preview")
|
|
779
|
+
def agent_preview():
|
|
780
|
+
"""Live PNG frame of a cmux browser tab. Runs outside the agent lock."""
|
|
781
|
+
surface = (request.args.get("surface") or "").strip() or None
|
|
782
|
+
workspace = (request.args.get("workspace") or "").strip() or None
|
|
783
|
+
try:
|
|
784
|
+
with _agent_lock:
|
|
785
|
+
session = _get_session()
|
|
786
|
+
path = session.socket_path
|
|
787
|
+
t = session.target
|
|
788
|
+
if not surface:
|
|
789
|
+
if not t.is_browser:
|
|
790
|
+
return jsonify(
|
|
791
|
+
{"ok": False, "error": "select a browser tab first"}
|
|
792
|
+
), 400
|
|
793
|
+
surface = t.surface_ref
|
|
794
|
+
workspace = t.workspace_ref
|
|
795
|
+
elif not workspace:
|
|
796
|
+
workspace = t.workspace_ref
|
|
797
|
+
# Screenshot outside the lock so chat/select stay responsive.
|
|
798
|
+
png = browser_screenshot(
|
|
799
|
+
surface=surface, workspace=workspace, socket_path=path
|
|
800
|
+
)
|
|
801
|
+
return Response(
|
|
802
|
+
png,
|
|
803
|
+
mimetype="image/png",
|
|
804
|
+
headers={
|
|
805
|
+
"Cache-Control": "no-store, no-cache, must-revalidate, max-age=0",
|
|
806
|
+
"Pragma": "no-cache",
|
|
807
|
+
},
|
|
808
|
+
)
|
|
809
|
+
except KeyboardInterrupt:
|
|
810
|
+
raise
|
|
811
|
+
except BaseException as e:
|
|
812
|
+
return jsonify({"ok": False, "error": _agent_error_message(e)}), 503
|
|
813
|
+
|
|
814
|
+
|
|
761
815
|
@app.route("/agent/transcript")
|
|
762
816
|
def agent_transcript():
|
|
763
817
|
try:
|