callwalkietalkie 0.8.0 → 0.9.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/callwalkietalkie.js +1 -1
- package/package.json +2 -2
- package/runtime/agent_ui.html +364 -29
- package/runtime/cmux-adapter/cmux_adapter.py +96 -32
- package/runtime/winproxy.py +59 -5
package/bin/callwalkietalkie.js
CHANGED
|
@@ -193,7 +193,7 @@ async function ensureMachineKey(site) {
|
|
|
193
193
|
if (/^cwt_[a-f0-9]{48,128}$/i.test(existing)) return existing;
|
|
194
194
|
}
|
|
195
195
|
|
|
196
|
-
process.stdout.write("
|
|
196
|
+
process.stdout.write(" Starting server… ");
|
|
197
197
|
let res;
|
|
198
198
|
try {
|
|
199
199
|
res = await fetch(`${site}/v1/keys`, {
|
package/package.json
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "callwalkietalkie",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.1",
|
|
4
4
|
"description": "Chat into cmux from your phone. Run npx callwalkietalkie.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
7
7
|
"type": "git",
|
|
8
|
-
"url": "git+https://github.com/nbened/
|
|
8
|
+
"url": "git+https://github.com/nbened/callwalkietalkie.git",
|
|
9
9
|
"directory": "cli"
|
|
10
10
|
},
|
|
11
11
|
"homepage": "https://callwalkietalkie.com",
|
package/runtime/agent_ui.html
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
<meta charset="utf-8">
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, viewport-fit=cover">
|
|
6
6
|
<meta name="apple-mobile-web-app-capable" content="yes">
|
|
7
|
-
<title>Sessions —
|
|
7
|
+
<title>Sessions — WalkieTalkie</title>
|
|
8
8
|
<style>
|
|
9
9
|
:root {
|
|
10
10
|
--bg: #ffffff;
|
|
@@ -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;
|
|
@@ -124,6 +135,9 @@
|
|
|
124
135
|
color: inherit;
|
|
125
136
|
cursor: pointer;
|
|
126
137
|
}
|
|
138
|
+
.session-icon { width: 24px; height: 24px; flex: none; margin-top: 3px; color: var(--muted); }
|
|
139
|
+
.sess.active .session-icon { color: var(--sent); }
|
|
140
|
+
.session-preview { display: block; margin-top: 4px; color: var(--muted); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
127
141
|
.sess.active { background: #f7fbff; }
|
|
128
142
|
.sess:active { background: var(--ground); }
|
|
129
143
|
.sess .dot {
|
|
@@ -135,6 +149,112 @@
|
|
|
135
149
|
background: transparent;
|
|
136
150
|
}
|
|
137
151
|
.sess.active .dot { background: var(--live); }
|
|
152
|
+
.sess.browser .dot { background: #94a3b8; }
|
|
153
|
+
.sess.browser.active .dot { background: var(--sent); }
|
|
154
|
+
.sess .kind {
|
|
155
|
+
flex: none;
|
|
156
|
+
font-size: 10px;
|
|
157
|
+
font-weight: 700;
|
|
158
|
+
letter-spacing: 0.06em;
|
|
159
|
+
text-transform: uppercase;
|
|
160
|
+
color: var(--muted);
|
|
161
|
+
border: 1px solid var(--line);
|
|
162
|
+
border-radius: 6px;
|
|
163
|
+
padding: 2px 6px;
|
|
164
|
+
align-self: flex-start;
|
|
165
|
+
margin-top: 2px;
|
|
166
|
+
}
|
|
167
|
+
.sess.browser .kind {
|
|
168
|
+
color: var(--sent);
|
|
169
|
+
border-color: color-mix(in srgb, var(--sent) 35%, var(--line));
|
|
170
|
+
background: color-mix(in srgb, var(--sent) 8%, transparent);
|
|
171
|
+
}
|
|
172
|
+
#browser {
|
|
173
|
+
display: flex;
|
|
174
|
+
flex-direction: column;
|
|
175
|
+
height: 100%;
|
|
176
|
+
background: #0b0d10;
|
|
177
|
+
}
|
|
178
|
+
#browser[hidden] { display: none !important; }
|
|
179
|
+
#btop {
|
|
180
|
+
flex: none;
|
|
181
|
+
display: flex;
|
|
182
|
+
align-items: center;
|
|
183
|
+
gap: 10px;
|
|
184
|
+
padding: 8px 14px 10px;
|
|
185
|
+
border-bottom: 1px solid rgba(255,255,255,.08);
|
|
186
|
+
background: rgba(12,14,18,.96);
|
|
187
|
+
color: #f4f4f5;
|
|
188
|
+
}
|
|
189
|
+
#bback {
|
|
190
|
+
flex: none;
|
|
191
|
+
width: 36px;
|
|
192
|
+
height: 36px;
|
|
193
|
+
border: none;
|
|
194
|
+
background: transparent;
|
|
195
|
+
color: #7dd3fc;
|
|
196
|
+
font-size: 28px;
|
|
197
|
+
line-height: 1;
|
|
198
|
+
padding: 0;
|
|
199
|
+
cursor: pointer;
|
|
200
|
+
}
|
|
201
|
+
#btitlewrap { flex: 1; min-width: 0; }
|
|
202
|
+
#btitle {
|
|
203
|
+
font-size: 16px;
|
|
204
|
+
font-weight: 600;
|
|
205
|
+
display: flex;
|
|
206
|
+
align-items: center;
|
|
207
|
+
gap: 7px;
|
|
208
|
+
white-space: nowrap;
|
|
209
|
+
overflow: hidden;
|
|
210
|
+
text-overflow: ellipsis;
|
|
211
|
+
}
|
|
212
|
+
#btitle .live {
|
|
213
|
+
width: 7px;
|
|
214
|
+
height: 7px;
|
|
215
|
+
border-radius: 4px;
|
|
216
|
+
background: var(--live);
|
|
217
|
+
flex: none;
|
|
218
|
+
}
|
|
219
|
+
#bstatus {
|
|
220
|
+
font-size: 12px;
|
|
221
|
+
color: #a1a1aa;
|
|
222
|
+
font-family: var(--mono);
|
|
223
|
+
white-space: nowrap;
|
|
224
|
+
overflow: hidden;
|
|
225
|
+
text-overflow: ellipsis;
|
|
226
|
+
}
|
|
227
|
+
#bstage {
|
|
228
|
+
flex: 1;
|
|
229
|
+
min-height: 0;
|
|
230
|
+
position: relative;
|
|
231
|
+
display: flex;
|
|
232
|
+
align-items: center;
|
|
233
|
+
justify-content: center;
|
|
234
|
+
background: #0b0d10;
|
|
235
|
+
overflow: hidden;
|
|
236
|
+
}
|
|
237
|
+
#bframe {
|
|
238
|
+
max-width: 100%;
|
|
239
|
+
max-height: 100%;
|
|
240
|
+
object-fit: contain;
|
|
241
|
+
display: block;
|
|
242
|
+
background: #111;
|
|
243
|
+
}
|
|
244
|
+
#bframe[hidden] { display: none; }
|
|
245
|
+
#bempty {
|
|
246
|
+
position: absolute;
|
|
247
|
+
inset: 0;
|
|
248
|
+
display: flex;
|
|
249
|
+
align-items: center;
|
|
250
|
+
justify-content: center;
|
|
251
|
+
color: #a1a1aa;
|
|
252
|
+
font-size: 14px;
|
|
253
|
+
padding: 24px;
|
|
254
|
+
text-align: center;
|
|
255
|
+
}
|
|
256
|
+
#bempty[hidden] { display: none; }
|
|
257
|
+
#bempty.err { color: #fda4af; }
|
|
138
258
|
.sess .body { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 2px; }
|
|
139
259
|
.sess .top { display: flex; justify-content: space-between; gap: 8px; }
|
|
140
260
|
.sess .name {
|
|
@@ -459,7 +579,7 @@
|
|
|
459
579
|
</div>
|
|
460
580
|
<div id="sess-list"></div>
|
|
461
581
|
<div id="sessions-foot">
|
|
462
|
-
<span class="hint">
|
|
582
|
+
<span class="hint">Terminals & agent chats · live web previews · <button id="restore-sessions" type="button">Restore</button></span>
|
|
463
583
|
<button id="leave" type="button">Disconnect</button>
|
|
464
584
|
</div>
|
|
465
585
|
</div>
|
|
@@ -482,6 +602,20 @@
|
|
|
482
602
|
</div>
|
|
483
603
|
</div>
|
|
484
604
|
|
|
605
|
+
<div id="browser" hidden>
|
|
606
|
+
<div id="btop">
|
|
607
|
+
<button id="bback" type="button" aria-label="Back to sessions">‹</button>
|
|
608
|
+
<div id="btitlewrap">
|
|
609
|
+
<div id="btitle"><span class="live" id="blivedot" hidden></span><span id="btitletext">Browser</span></div>
|
|
610
|
+
<div id="bstatus">…</div>
|
|
611
|
+
</div>
|
|
612
|
+
</div>
|
|
613
|
+
<div id="bstage">
|
|
614
|
+
<img id="bframe" alt="Live browser view" hidden />
|
|
615
|
+
<div id="bempty">Starting live view…</div>
|
|
616
|
+
</div>
|
|
617
|
+
</div>
|
|
618
|
+
|
|
485
619
|
<script>
|
|
486
620
|
(function() {
|
|
487
621
|
var TOKEN = __TOKEN_JSON__;
|
|
@@ -489,6 +623,7 @@
|
|
|
489
623
|
var API = __API_BASE_JSON__;
|
|
490
624
|
var sessionsView = document.getElementById("sessions");
|
|
491
625
|
var threadView = document.getElementById("thread");
|
|
626
|
+
var browserView = document.getElementById("browser");
|
|
492
627
|
var sessList = document.getElementById("sess-list");
|
|
493
628
|
var sessionsMeta = document.getElementById("sessions-meta");
|
|
494
629
|
var log = document.getElementById("log");
|
|
@@ -501,6 +636,12 @@
|
|
|
501
636
|
var backBtn = document.getElementById("back");
|
|
502
637
|
var leaveBtn = document.getElementById("leave");
|
|
503
638
|
var restoreBtn = document.getElementById("restore-sessions");
|
|
639
|
+
var bbackBtn = document.getElementById("bback");
|
|
640
|
+
var btitleText = document.getElementById("btitletext");
|
|
641
|
+
var bstatusEl = document.getElementById("bstatus");
|
|
642
|
+
var bliveDot = document.getElementById("blivedot");
|
|
643
|
+
var bframe = document.getElementById("bframe");
|
|
644
|
+
var bempty = document.getElementById("bempty");
|
|
504
645
|
|
|
505
646
|
var misses = 0;
|
|
506
647
|
var lastSig = "";
|
|
@@ -511,6 +652,8 @@
|
|
|
511
652
|
var holdStop = false;
|
|
512
653
|
var sawBusy = false;
|
|
513
654
|
var sendAt = 0;
|
|
655
|
+
var clearUntil = 0;
|
|
656
|
+
var clearTimers = [];
|
|
514
657
|
var poller = null;
|
|
515
658
|
var leaving = false;
|
|
516
659
|
var showCmds = true;
|
|
@@ -520,6 +663,13 @@
|
|
|
520
663
|
var lastSessions = [];
|
|
521
664
|
var hiddenSessions = loadHidden();
|
|
522
665
|
var openSwipeRow = null;
|
|
666
|
+
var activeType = "terminal";
|
|
667
|
+
var activeUrl = "";
|
|
668
|
+
var activeSurface = null;
|
|
669
|
+
var activeWorkspace = null;
|
|
670
|
+
var inBrowser = false;
|
|
671
|
+
var previewTimer = null;
|
|
672
|
+
var previewBusy = false;
|
|
523
673
|
|
|
524
674
|
function loadHidden() {
|
|
525
675
|
try {
|
|
@@ -555,15 +705,42 @@
|
|
|
555
705
|
});
|
|
556
706
|
}
|
|
557
707
|
|
|
558
|
-
function
|
|
559
|
-
msg.value = "";
|
|
708
|
+
function wipeComposer() {
|
|
709
|
+
if (msg.value !== "") msg.value = "";
|
|
560
710
|
msg.style.height = "auto";
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
function clearComposer() {
|
|
714
|
+
// Soft keyboards (esp. iOS enterkeyhint=send) often re-commit the draft
|
|
715
|
+
// after we clear. Wipe now and again for a short window.
|
|
716
|
+
clearTimers.forEach(function(id) { clearTimeout(id); });
|
|
717
|
+
clearTimers = [];
|
|
718
|
+
clearUntil = Date.now() + 400;
|
|
719
|
+
wipeComposer();
|
|
720
|
+
try { msg.setAttribute("readonly", "readonly"); } catch (e) {}
|
|
721
|
+
function finishUnlock() {
|
|
722
|
+
try { msg.removeAttribute("readonly"); } catch (e) {}
|
|
723
|
+
wipeComposer();
|
|
724
|
+
}
|
|
725
|
+
[0, 16, 50, 120, 250].forEach(function(ms) {
|
|
726
|
+
clearTimers.push(setTimeout(function() {
|
|
727
|
+
wipeComposer();
|
|
728
|
+
if (ms >= 250) finishUnlock();
|
|
729
|
+
}, ms));
|
|
730
|
+
});
|
|
561
731
|
requestAnimationFrame(function() {
|
|
562
|
-
|
|
563
|
-
|
|
732
|
+
wipeComposer();
|
|
733
|
+
requestAnimationFrame(wipeComposer);
|
|
564
734
|
});
|
|
565
735
|
}
|
|
566
736
|
|
|
737
|
+
function isReinjectedDraft() {
|
|
738
|
+
if (!pendingUser) return false;
|
|
739
|
+
if (!(sending || Date.now() < clearUntil)) return false;
|
|
740
|
+
var cur = normText((msg.value || "").replace(/\r?\n/g, ""));
|
|
741
|
+
return !!cur && cur === normText(pendingUser);
|
|
742
|
+
}
|
|
743
|
+
|
|
567
744
|
function esc(s) {
|
|
568
745
|
return String(s).replace(/[&<>"']/g, function(c) {
|
|
569
746
|
return ({"&":"&","<":"<",">":">","\"":""","'":"'"})[c];
|
|
@@ -575,16 +752,92 @@
|
|
|
575
752
|
return String(p).replace(/^\/Users\/[^\/]+/, "~");
|
|
576
753
|
}
|
|
577
754
|
|
|
755
|
+
function shortUrl(u) {
|
|
756
|
+
if (!u) return "";
|
|
757
|
+
try {
|
|
758
|
+
var x = new URL(u);
|
|
759
|
+
var path = x.pathname === "/" ? "" : x.pathname;
|
|
760
|
+
return x.host + path + (x.search || "");
|
|
761
|
+
} catch (e) {
|
|
762
|
+
return String(u);
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
function stopBrowserStream() {
|
|
767
|
+
if (previewTimer) {
|
|
768
|
+
clearTimeout(previewTimer);
|
|
769
|
+
previewTimer = null;
|
|
770
|
+
}
|
|
771
|
+
previewBusy = false;
|
|
772
|
+
if (bframe) {
|
|
773
|
+
bframe.removeAttribute("src");
|
|
774
|
+
bframe.hidden = true;
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
|
|
578
778
|
function showSessions() {
|
|
579
779
|
inThread = false;
|
|
780
|
+
inBrowser = false;
|
|
781
|
+
stopBrowserStream();
|
|
580
782
|
sessionsView.hidden = false;
|
|
581
783
|
threadView.hidden = true;
|
|
784
|
+
if (browserView) browserView.hidden = true;
|
|
582
785
|
}
|
|
583
786
|
|
|
584
787
|
function showThread() {
|
|
585
788
|
inThread = true;
|
|
789
|
+
inBrowser = false;
|
|
790
|
+
stopBrowserStream();
|
|
586
791
|
sessionsView.hidden = true;
|
|
587
792
|
threadView.hidden = false;
|
|
793
|
+
if (browserView) browserView.hidden = true;
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
function showBrowserLive() {
|
|
797
|
+
inThread = false;
|
|
798
|
+
inBrowser = true;
|
|
799
|
+
sessionsView.hidden = true;
|
|
800
|
+
threadView.hidden = true;
|
|
801
|
+
if (browserView) browserView.hidden = false;
|
|
802
|
+
if (btitleText) btitleText.textContent = titleText.textContent || "Browser";
|
|
803
|
+
if (bstatusEl) bstatusEl.textContent = activeUrl ? shortUrl(activeUrl) : "live view";
|
|
804
|
+
if (bempty) {
|
|
805
|
+
bempty.hidden = false;
|
|
806
|
+
bempty.classList.remove("err");
|
|
807
|
+
bempty.textContent = "Starting live view…";
|
|
808
|
+
}
|
|
809
|
+
if (bliveDot) bliveDot.hidden = true;
|
|
810
|
+
stopBrowserStream();
|
|
811
|
+
pullBrowserFrame();
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
function pullBrowserFrame() {
|
|
815
|
+
if (!inBrowser || previewBusy) return;
|
|
816
|
+
previewBusy = true;
|
|
817
|
+
var url = API + "/preview?t=" + encodeURIComponent(TOKEN) + "&_=" + Date.now();
|
|
818
|
+
if (activeSurface) url += "&surface=" + encodeURIComponent(activeSurface);
|
|
819
|
+
if (activeWorkspace) url += "&workspace=" + encodeURIComponent(activeWorkspace);
|
|
820
|
+
var img = new Image();
|
|
821
|
+
img.onload = function() {
|
|
822
|
+
if (!inBrowser) { previewBusy = false; return; }
|
|
823
|
+
bframe.src = img.src;
|
|
824
|
+
bframe.hidden = false;
|
|
825
|
+
if (bempty) bempty.hidden = true;
|
|
826
|
+
if (bliveDot) bliveDot.hidden = false;
|
|
827
|
+
previewBusy = false;
|
|
828
|
+
previewTimer = setTimeout(pullBrowserFrame, 800);
|
|
829
|
+
};
|
|
830
|
+
img.onerror = function() {
|
|
831
|
+
previewBusy = false;
|
|
832
|
+
if (!inBrowser) return;
|
|
833
|
+
if (bframe.hidden && bempty) {
|
|
834
|
+
bempty.hidden = false;
|
|
835
|
+
bempty.classList.add("err");
|
|
836
|
+
bempty.textContent = "Can't stream this browser tab yet";
|
|
837
|
+
}
|
|
838
|
+
previewTimer = setTimeout(pullBrowserFrame, 1600);
|
|
839
|
+
};
|
|
840
|
+
img.src = url;
|
|
588
841
|
}
|
|
589
842
|
|
|
590
843
|
function inlineMd(s) {
|
|
@@ -769,7 +1022,7 @@
|
|
|
769
1022
|
var html = "";
|
|
770
1023
|
var expanded = expandMessages(known);
|
|
771
1024
|
if (!expanded.length && !pendingUser) {
|
|
772
|
-
html = '<div class="turn system"><div class="bubble">
|
|
1025
|
+
html = '<div class="turn system"><div class="bubble">' + (agentName(titleText.textContent) ? 'Start a conversation. Send your first message below.' : 'Your terminal is open. Send a command or text below.') + '</div></div>';
|
|
773
1026
|
}
|
|
774
1027
|
expanded.forEach(function(m) {
|
|
775
1028
|
var role = m.role;
|
|
@@ -801,6 +1054,19 @@
|
|
|
801
1054
|
sessionsMeta.className = cls || "";
|
|
802
1055
|
}
|
|
803
1056
|
|
|
1057
|
+
var sessionActivity = {};
|
|
1058
|
+
function agentName(title) {
|
|
1059
|
+
var match = String(title || "").match(/\b(claude|codex|aider|opencode|gemini)\b/i);
|
|
1060
|
+
return match ? match[1] : "";
|
|
1061
|
+
}
|
|
1062
|
+
function sessionIcon(kind) {
|
|
1063
|
+
var paths = kind === "browser"
|
|
1064
|
+
? '<circle cx="12" cy="12" r="9"/><ellipse cx="12" cy="12" rx="4" ry="9"/><path d="M3 12h18"/>'
|
|
1065
|
+
: kind === "chat"
|
|
1066
|
+
? '<path d="M21 11a8 8 0 0 1-8 8H6l-4 3V11a9 9 0 0 1 19 0Z"/><path d="M7 10h10M7 14h6"/>'
|
|
1067
|
+
: '<rect x="2" y="4" width="20" height="16" rx="3"/><path d="m6 9 3 3-3 3m7 0h5"/>';
|
|
1068
|
+
return '<svg class="session-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">' + paths + '</svg>';
|
|
1069
|
+
}
|
|
804
1070
|
function renderPicker(sessions, currentSurface) {
|
|
805
1071
|
lastSessions = sessions || [];
|
|
806
1072
|
activeKey = currentSurface;
|
|
@@ -809,38 +1075,65 @@
|
|
|
809
1075
|
return hiddenSessions.indexOf(sessionKey(s)) === -1;
|
|
810
1076
|
});
|
|
811
1077
|
if (!visible.length) {
|
|
812
|
-
var
|
|
1078
|
+
var emptyMsg = lastSessions.length
|
|
813
1079
|
? "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">' +
|
|
1080
|
+
: "No cmux terminals or browsers open.";
|
|
1081
|
+
sessList.innerHTML = '<div class="turn system" style="padding:24px 16px"><div class="bubble">' + emptyMsg + "</div></div>";
|
|
816
1082
|
return;
|
|
817
1083
|
}
|
|
818
|
-
|
|
1084
|
+
function renderSession(s) {
|
|
819
1085
|
var surface = s.surface || s.surface_id || "";
|
|
820
1086
|
var workspace = s.workspace || s.workspace_id || "";
|
|
821
|
-
var
|
|
822
|
-
var
|
|
1087
|
+
var kind = s.type || "terminal";
|
|
1088
|
+
var isBrowser = kind === "browser";
|
|
1089
|
+
var agent = !isBrowser && agentName(s.title);
|
|
1090
|
+
var activity = sessionActivity[surface];
|
|
1091
|
+
var title = esc(s.title || surface || (isBrowser ? "browser" : "session"));
|
|
1092
|
+
var path = isBrowser
|
|
1093
|
+
? esc(shortUrl(s.url || ""))
|
|
1094
|
+
: esc(shortPath(s.cwd || s.current_directory || ""));
|
|
823
1095
|
var key = esc(sessionKey(s));
|
|
824
1096
|
var active = surface === currentSurface;
|
|
825
|
-
var
|
|
1097
|
+
var state = isBrowser ? (s.url ? "Live preview available" : "No page open")
|
|
1098
|
+
: agent ? (!activity ? "Open to check conversation" : activity.started ? "Chat started" : "Ready to chat")
|
|
1099
|
+
: "Open terminal";
|
|
1100
|
+
if (agent && activity && Date.now() - activity.at < 4000) {
|
|
1101
|
+
state = activity.busy ? "Working" : activity.started ? "Ready for your next message" : "Ready to chat";
|
|
1102
|
+
}
|
|
1103
|
+
var snip = esc(state);
|
|
1104
|
+
if (agent && activity && activity.preview) snip += '<span class="session-preview">' + esc(activity.preview) + '</span>';
|
|
826
1105
|
return '<div class="sess-row" data-key="' + key + '">' +
|
|
827
1106
|
'<button type="button" class="sess-delete">Delete</button>' +
|
|
828
|
-
'<button type="button" class="sess' + (active ? " active" : "") + '"' +
|
|
1107
|
+
'<button type="button" class="sess' + (active ? " active" : "") + (isBrowser ? " browser" : "") + '"' +
|
|
829
1108
|
' data-surface="' + esc(surface) + '"' +
|
|
830
1109
|
' data-workspace="' + esc(workspace) + '"' +
|
|
831
1110
|
' data-window="' + esc(s.window || "") + '"' +
|
|
832
|
-
' data-title="' + title + '"
|
|
833
|
-
'
|
|
1111
|
+
' data-title="' + title + '"' +
|
|
1112
|
+
' data-type="' + esc(kind) + '"' +
|
|
1113
|
+
' data-url="' + esc(s.url || "") + '">' +
|
|
1114
|
+
sessionIcon(isBrowser ? 'browser' : agent ? 'chat' : 'terminal') +
|
|
834
1115
|
'<span class="body">' +
|
|
835
|
-
'<span class="top"><span class="name">' + title + '</span
|
|
1116
|
+
'<span class="top"><span class="name">' + title + '</span><span class="kind">' + (isBrowser ? "HTTP / Web" : agent ? "Agent chat" : "Terminal") + "</span></span>" +
|
|
836
1117
|
'<span class="snip">' + snip + "</span>" +
|
|
837
|
-
(
|
|
1118
|
+
(path ? '<span class="path">' + path + "</span>" : "") +
|
|
838
1119
|
"</span></button></div>";
|
|
1120
|
+
}
|
|
1121
|
+
sessList.innerHTML = [
|
|
1122
|
+
{ type: "browser", title: "Web pages" },
|
|
1123
|
+
{ type: "terminal", title: "Terminals" }
|
|
1124
|
+
].map(function(group) {
|
|
1125
|
+
var items = visible.filter(function(s) {
|
|
1126
|
+
return (s.type || "terminal") === group.type;
|
|
1127
|
+
});
|
|
1128
|
+
if (!items.length) return "";
|
|
1129
|
+
return '<section aria-labelledby="sess-' + group.type + '-heading">' +
|
|
1130
|
+
'<h2 class="sess-section-title" id="sess-' + group.type + '-heading">' + group.title + '</h2>' +
|
|
1131
|
+
items.map(renderSession).join("") + '</section>';
|
|
839
1132
|
}).join("");
|
|
840
1133
|
bindSwipeRows();
|
|
841
1134
|
}
|
|
842
1135
|
|
|
843
|
-
async function selectSession(surface, workspace, windowRef, title) {
|
|
1136
|
+
async function selectSession(surface, workspace, windowRef, title, typeHint, urlHint) {
|
|
844
1137
|
try {
|
|
845
1138
|
var r = await fetch(API + "/select?t=" + encodeURIComponent(TOKEN), {
|
|
846
1139
|
method: "POST",
|
|
@@ -853,9 +1146,12 @@
|
|
|
853
1146
|
return;
|
|
854
1147
|
}
|
|
855
1148
|
activeKey = j.surface || surface;
|
|
1149
|
+
activeSurface = j.surface || surface || null;
|
|
1150
|
+
activeWorkspace = j.workspace || workspace || null;
|
|
1151
|
+
activeType = j.type || typeHint || "terminal";
|
|
1152
|
+
activeUrl = j.url || urlHint || "";
|
|
856
1153
|
titleText.textContent = title || j.title || "cmux";
|
|
857
1154
|
liveDot.hidden = false;
|
|
858
|
-
// Hard-reset thread so the previous surface's messages can't linger.
|
|
859
1155
|
known = [];
|
|
860
1156
|
pendingUser = null;
|
|
861
1157
|
lastSig = "";
|
|
@@ -864,9 +1160,13 @@
|
|
|
864
1160
|
agentBusy = false;
|
|
865
1161
|
log.innerHTML = "";
|
|
866
1162
|
updateComposerMode();
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
1163
|
+
if (activeType === "browser") {
|
|
1164
|
+
showBrowserLive();
|
|
1165
|
+
} else {
|
|
1166
|
+
showThread();
|
|
1167
|
+
await refreshStatus();
|
|
1168
|
+
await refreshTranscript();
|
|
1169
|
+
}
|
|
870
1170
|
} catch (err) {
|
|
871
1171
|
setSessionsMeta("switch failed", "err");
|
|
872
1172
|
}
|
|
@@ -884,7 +1184,9 @@
|
|
|
884
1184
|
btn.getAttribute("data-surface"),
|
|
885
1185
|
btn.getAttribute("data-workspace"),
|
|
886
1186
|
btn.getAttribute("data-window"),
|
|
887
|
-
btn.getAttribute("data-title")
|
|
1187
|
+
btn.getAttribute("data-title"),
|
|
1188
|
+
btn.getAttribute("data-type"),
|
|
1189
|
+
btn.getAttribute("data-url")
|
|
888
1190
|
);
|
|
889
1191
|
});
|
|
890
1192
|
|
|
@@ -892,11 +1194,18 @@
|
|
|
892
1194
|
showSessions();
|
|
893
1195
|
refreshStatus();
|
|
894
1196
|
});
|
|
1197
|
+
if (bbackBtn) {
|
|
1198
|
+
bbackBtn.addEventListener("click", function() {
|
|
1199
|
+
showSessions();
|
|
1200
|
+
refreshStatus();
|
|
1201
|
+
});
|
|
1202
|
+
}
|
|
895
1203
|
|
|
896
1204
|
function disconnect() {
|
|
897
1205
|
if (leaving) return;
|
|
898
1206
|
leaving = true;
|
|
899
1207
|
clearInterval(poller);
|
|
1208
|
+
stopBrowserStream();
|
|
900
1209
|
fetch(API + "/shutdown?t=" + encodeURIComponent(TOKEN), {
|
|
901
1210
|
method: "POST",
|
|
902
1211
|
keepalive: true,
|
|
@@ -924,10 +1233,19 @@
|
|
|
924
1233
|
setSessionsMeta("localhost · live", "ok");
|
|
925
1234
|
titleText.textContent = j.title || "cmux";
|
|
926
1235
|
liveDot.hidden = false;
|
|
1236
|
+
activeType = j.type || activeType || "terminal";
|
|
1237
|
+
activeUrl = j.url || activeUrl || "";
|
|
1238
|
+
if (j.surface) activeSurface = j.surface;
|
|
1239
|
+
if (j.workspace) activeWorkspace = j.workspace;
|
|
927
1240
|
var bits = [];
|
|
928
|
-
if (j.
|
|
1241
|
+
if (j.type === "browser" && j.url) bits.push(shortUrl(j.url));
|
|
1242
|
+
else if (j.cwd) bits.push(shortPath(j.cwd));
|
|
929
1243
|
if (j.surface) bits.push(j.surface);
|
|
930
1244
|
setStatus(bits.join(" · ") || "live", "ok");
|
|
1245
|
+
if (inBrowser) {
|
|
1246
|
+
if (btitleText) btitleText.textContent = j.title || "Browser";
|
|
1247
|
+
if (bstatusEl) bstatusEl.textContent = j.url ? shortUrl(j.url) : "live view";
|
|
1248
|
+
}
|
|
931
1249
|
renderPicker(j.sessions || j.workspaces || [], j.surface);
|
|
932
1250
|
return true;
|
|
933
1251
|
} catch (e) {
|
|
@@ -940,7 +1258,7 @@
|
|
|
940
1258
|
}
|
|
941
1259
|
|
|
942
1260
|
async function refreshTranscript() {
|
|
943
|
-
if (!inThread) return;
|
|
1261
|
+
if (!inThread || inBrowser) return;
|
|
944
1262
|
try {
|
|
945
1263
|
var r = await fetch(API + "/transcript?t=" + encodeURIComponent(TOKEN));
|
|
946
1264
|
var j = await r.json();
|
|
@@ -956,8 +1274,11 @@
|
|
|
956
1274
|
}
|
|
957
1275
|
updateComposerMode();
|
|
958
1276
|
var msgs = j.messages || [];
|
|
1277
|
+
var chatMessages = msgs.filter(function(m) { return m.kind !== "command" && (m.role === "user" || m.role === "assistant"); });
|
|
1278
|
+
sessionActivity[activeSurface] = { started: chatMessages.length > 0, busy: !!j.busy, at: Date.now(), preview: chatMessages.length ? String(chatMessages[chatMessages.length - 1].text || "").slice(0, 120) : "" };
|
|
1279
|
+
msg.placeholder = agentName(titleText.textContent) ? "Message the agent…" : "Send a command or text…";
|
|
959
1280
|
var sig = msgs.map(function(m) { return (m.kind||"") + ":" + m.role + ":" + m.text; }).join("\x1e");
|
|
960
|
-
if (sig !== lastSig || pendingUser || agentBusy) {
|
|
1281
|
+
if (sig !== lastSig || pendingUser || agentBusy || !log.innerHTML) {
|
|
961
1282
|
lastSig = sig;
|
|
962
1283
|
render(msgs);
|
|
963
1284
|
}
|
|
@@ -974,7 +1295,12 @@
|
|
|
974
1295
|
async function send() {
|
|
975
1296
|
var text = (msg.value || "").trim();
|
|
976
1297
|
// Allow queueing a follow-up while the agent is busy; only block on in-flight send/stop.
|
|
977
|
-
if (!text ||
|
|
1298
|
+
if (!text || interrupting) return;
|
|
1299
|
+
if (sending) {
|
|
1300
|
+
// Keyboard re-inserted the just-sent draft; don't start another send — just clear.
|
|
1301
|
+
if (isReinjectedDraft()) clearComposer();
|
|
1302
|
+
return;
|
|
1303
|
+
}
|
|
978
1304
|
sending = true;
|
|
979
1305
|
sendBtn.disabled = true;
|
|
980
1306
|
pendingUser = text;
|
|
@@ -995,6 +1321,8 @@
|
|
|
995
1321
|
setStatus(j.error || "send failed", "err");
|
|
996
1322
|
pendingUser = null;
|
|
997
1323
|
holdStop = false;
|
|
1324
|
+
clearUntil = 0;
|
|
1325
|
+
try { msg.removeAttribute("readonly"); } catch (e) {}
|
|
998
1326
|
msg.value = text;
|
|
999
1327
|
render(known);
|
|
1000
1328
|
} else {
|
|
@@ -1005,6 +1333,8 @@
|
|
|
1005
1333
|
setStatus("send failed", "err");
|
|
1006
1334
|
pendingUser = null;
|
|
1007
1335
|
holdStop = false;
|
|
1336
|
+
clearUntil = 0;
|
|
1337
|
+
try { msg.removeAttribute("readonly"); } catch (e2) {}
|
|
1008
1338
|
msg.value = text;
|
|
1009
1339
|
render(known);
|
|
1010
1340
|
}
|
|
@@ -1089,6 +1419,11 @@
|
|
|
1089
1419
|
send();
|
|
1090
1420
|
});
|
|
1091
1421
|
msg.addEventListener("input", function(e) {
|
|
1422
|
+
if (isReinjectedDraft()) {
|
|
1423
|
+
wipeComposer();
|
|
1424
|
+
updateComposerMode();
|
|
1425
|
+
return;
|
|
1426
|
+
}
|
|
1092
1427
|
msg.style.height = "auto";
|
|
1093
1428
|
msg.style.height = Math.min(msg.scrollHeight, 120) + "px";
|
|
1094
1429
|
updateComposerMode();
|
|
@@ -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,
|
|
@@ -410,13 +411,13 @@ _LASSO_SVG = """<svg class="lasso" width="16" height="16" viewBox="0 0 24 24" fi
|
|
|
410
411
|
<path d="M16.2 13.8c1.6 1.4 3.4 3.8 4.6 6.4" stroke="currentColor" stroke-width="1.85" stroke-linecap="round"/>
|
|
411
412
|
</svg>"""
|
|
412
413
|
|
|
413
|
-
_BRAND = f'<span class="brand">{_LASSO_SVG}
|
|
414
|
+
_BRAND = f'<span class="brand">{_LASSO_SVG}WalkieTalkie</span>'
|
|
414
415
|
|
|
415
416
|
SETUP_HTML = """<!doctype html>
|
|
416
417
|
<html lang="en"><head>
|
|
417
418
|
<meta charset="utf-8">
|
|
418
419
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
419
|
-
<title>Link your phone —
|
|
420
|
+
<title>Link your phone — WalkieTalkie</title>
|
|
420
421
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
421
422
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
422
423
|
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
|
|
@@ -526,7 +527,7 @@ HANDOFF_HTML = """<!doctype html>
|
|
|
526
527
|
<html lang="en"><head>
|
|
527
528
|
<meta charset="utf-8">
|
|
528
529
|
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
|
|
529
|
-
<title>Phone linked —
|
|
530
|
+
<title>Phone linked — WalkieTalkie</title>
|
|
530
531
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
531
532
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
532
533
|
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
|
|
@@ -547,7 +548,7 @@ STALE_HTML = """<!doctype html>
|
|
|
547
548
|
<html lang="en"><head>
|
|
548
549
|
<meta charset="utf-8">
|
|
549
550
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
550
|
-
<title>Session expired —
|
|
551
|
+
<title>Session expired — WalkieTalkie</title>
|
|
551
552
|
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
|
|
552
553
|
<style>__CSS__</style>
|
|
553
554
|
</head><body>
|
|
@@ -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:
|