callwalkietalkie 0.8.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.
@@ -0,0 +1,1118 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, viewport-fit=cover">
6
+ <meta name="apple-mobile-web-app-capable" content="yes">
7
+ <title>Sessions — Call Walkie Talkie</title>
8
+ <style>
9
+ :root {
10
+ --bg: #ffffff;
11
+ --ground: #f4f4f6;
12
+ --ink: #1c1c1e;
13
+ --muted: #8a8a8e;
14
+ --dim: #48484a;
15
+ --line: #ececef;
16
+ --line-strong: #dcdce0;
17
+ --sent: #0a7cff;
18
+ --received: #e9e9eb;
19
+ --live: #34c759;
20
+ --fail: #ff3b30;
21
+ --ok-bg: #e8f4e9;
22
+ --ok-ink: #1c7a32;
23
+ --fail-bg: #fdeceb;
24
+ --fail-ink: #b3261e;
25
+ --mono: ui-monospace, "SF Mono", Menlo, Consolas, monospace;
26
+ --sans: ui-sans-serif, system-ui, -apple-system, "Helvetica Neue", sans-serif;
27
+ }
28
+ * { box-sizing: border-box; -webkit-tap-highlight-color: transparent; }
29
+ html, body { margin: 0; height: 100%; }
30
+ body {
31
+ background: var(--bg);
32
+ color: var(--ink);
33
+ font-family: var(--sans);
34
+ font-size: 15px;
35
+ line-height: 1.35;
36
+ -webkit-font-smoothing: antialiased;
37
+ display: flex;
38
+ flex-direction: column;
39
+ padding-top: env(safe-area-inset-top);
40
+ }
41
+ [hidden] { display: none !important; }
42
+
43
+ /* —— Sessions list —— */
44
+ #sessions {
45
+ flex: 1;
46
+ display: flex;
47
+ flex-direction: column;
48
+ min-height: 0;
49
+ }
50
+ #sessions-head {
51
+ flex: none;
52
+ padding: 10px 16px 12px;
53
+ display: flex;
54
+ flex-direction: column;
55
+ gap: 12px;
56
+ }
57
+ #sessions-head .row {
58
+ display: flex;
59
+ align-items: center;
60
+ justify-content: space-between;
61
+ gap: 12px;
62
+ }
63
+ #sessions-head h1 {
64
+ margin: 0;
65
+ font-size: 34px;
66
+ font-weight: 700;
67
+ letter-spacing: -0.02em;
68
+ line-height: 1.05;
69
+ }
70
+ #sessions-meta {
71
+ font-size: 13px;
72
+ color: var(--muted);
73
+ font-weight: 500;
74
+ }
75
+ #sessions-meta.ok { color: var(--ok-ink); }
76
+ #sessions-meta.err { color: var(--fail-ink); }
77
+ #sess-list {
78
+ flex: 1;
79
+ overflow-y: auto;
80
+ overflow-x: hidden;
81
+ -webkit-overflow-scrolling: touch;
82
+ border-top: 1px solid var(--line);
83
+ }
84
+ .sess-row {
85
+ position: relative;
86
+ overflow: hidden;
87
+ border-bottom: 1px solid var(--line);
88
+ }
89
+ .sess-delete {
90
+ position: absolute;
91
+ inset: 0 0 0 auto;
92
+ width: 88px;
93
+ display: flex;
94
+ align-items: center;
95
+ justify-content: center;
96
+ background: var(--fail);
97
+ color: #fff;
98
+ font: inherit;
99
+ font-size: 15px;
100
+ font-weight: 600;
101
+ border: none;
102
+ cursor: pointer;
103
+ z-index: 0;
104
+ }
105
+ .sess-row .sess {
106
+ position: relative;
107
+ z-index: 1;
108
+ border-bottom: none;
109
+ transition: transform 180ms ease;
110
+ touch-action: pan-y;
111
+ will-change: transform;
112
+ }
113
+ .sess-row.open .sess { transform: translateX(-88px); }
114
+ .sess {
115
+ width: 100%;
116
+ display: flex;
117
+ gap: 12px;
118
+ padding: 12px 16px;
119
+ border: none;
120
+ border-bottom: 1px solid var(--line);
121
+ background: var(--bg);
122
+ text-align: left;
123
+ font: inherit;
124
+ color: inherit;
125
+ cursor: pointer;
126
+ }
127
+ .sess.active { background: #f7fbff; }
128
+ .sess:active { background: var(--ground); }
129
+ .sess .dot {
130
+ width: 8px;
131
+ height: 8px;
132
+ border-radius: 5px;
133
+ margin-top: 8px;
134
+ flex: none;
135
+ background: transparent;
136
+ }
137
+ .sess.active .dot { background: var(--live); }
138
+ .sess .body { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 2px; }
139
+ .sess .top { display: flex; justify-content: space-between; gap: 8px; }
140
+ .sess .name {
141
+ font-size: 17px;
142
+ font-weight: 600;
143
+ white-space: nowrap;
144
+ overflow: hidden;
145
+ text-overflow: ellipsis;
146
+ }
147
+ .sess .when { font-size: 13px; color: var(--muted); flex: none; }
148
+ .sess .snip {
149
+ font-size: 15px;
150
+ color: var(--dim);
151
+ white-space: nowrap;
152
+ overflow: hidden;
153
+ text-overflow: ellipsis;
154
+ }
155
+ .sess .path {
156
+ font-size: 12px;
157
+ color: var(--muted);
158
+ font-family: var(--mono);
159
+ white-space: nowrap;
160
+ overflow: hidden;
161
+ text-overflow: ellipsis;
162
+ }
163
+ #sessions-foot {
164
+ flex: none;
165
+ display: flex;
166
+ align-items: center;
167
+ justify-content: space-between;
168
+ gap: 12px;
169
+ padding: 10px 16px calc(12px + env(safe-area-inset-bottom));
170
+ border-top: 1px solid var(--line);
171
+ background: var(--bg);
172
+ }
173
+ #sessions-foot .hint { font-size: 12px; color: var(--muted); }
174
+ #sessions-foot .hint button {
175
+ border: none;
176
+ background: none;
177
+ padding: 0;
178
+ font: inherit;
179
+ color: var(--sent);
180
+ cursor: pointer;
181
+ }
182
+ #leave {
183
+ border: none;
184
+ background: var(--ground);
185
+ color: var(--dim);
186
+ font: inherit;
187
+ font-size: 13px;
188
+ font-weight: 600;
189
+ padding: 8px 12px;
190
+ border-radius: 16px;
191
+ cursor: pointer;
192
+ }
193
+
194
+ /* —— Thread —— */
195
+ #thread {
196
+ flex: 1;
197
+ display: flex;
198
+ flex-direction: column;
199
+ min-height: 0;
200
+ }
201
+ #top {
202
+ flex: none;
203
+ display: flex;
204
+ align-items: center;
205
+ gap: 10px;
206
+ padding: 8px 14px 10px;
207
+ border-bottom: 1px solid var(--line);
208
+ background: rgba(255,255,255,.94);
209
+ }
210
+ #back {
211
+ flex: none;
212
+ width: 36px;
213
+ height: 36px;
214
+ border: none;
215
+ background: transparent;
216
+ color: var(--sent);
217
+ font-size: 28px;
218
+ line-height: 1;
219
+ padding: 0;
220
+ cursor: pointer;
221
+ }
222
+ #titlewrap { flex: 1; min-width: 0; display: flex; flex-direction: column; }
223
+ #title {
224
+ font-size: 16px;
225
+ font-weight: 600;
226
+ display: flex;
227
+ align-items: center;
228
+ gap: 7px;
229
+ white-space: nowrap;
230
+ overflow: hidden;
231
+ text-overflow: ellipsis;
232
+ }
233
+ #title .live {
234
+ width: 7px;
235
+ height: 7px;
236
+ border-radius: 4px;
237
+ background: var(--live);
238
+ flex: none;
239
+ }
240
+ #status {
241
+ font-size: 12px;
242
+ color: var(--muted);
243
+ font-family: var(--mono);
244
+ white-space: nowrap;
245
+ overflow: hidden;
246
+ text-overflow: ellipsis;
247
+ }
248
+ #status.ok { color: var(--muted); }
249
+ #status.err { color: var(--fail-ink); }
250
+ #ctog {
251
+ flex: none;
252
+ width: 36px;
253
+ height: 36px;
254
+ border: none;
255
+ border-radius: 18px;
256
+ background: var(--ground);
257
+ font-family: var(--mono);
258
+ font-size: 13px;
259
+ font-weight: 600;
260
+ color: var(--dim);
261
+ cursor: pointer;
262
+ }
263
+ #ctog.on { background: var(--sent); color: #fff; }
264
+
265
+ #log {
266
+ flex: 1;
267
+ overflow-y: auto;
268
+ padding: 14px 16px;
269
+ display: flex;
270
+ flex-direction: column;
271
+ gap: 2px;
272
+ -webkit-overflow-scrolling: touch;
273
+ }
274
+ .turn {
275
+ display: flex;
276
+ flex-direction: column;
277
+ max-width: 100%;
278
+ }
279
+ .turn + .turn.user { margin-top: 10px; }
280
+ .turn.user + .turn.assistant,
281
+ .turn.user + .turn.command,
282
+ .turn.assistant + .turn.user,
283
+ .turn.command + .turn.user { margin-top: 10px; }
284
+ .turn.system { align-items: center; margin: 8px 0; }
285
+ .turn.user { align-items: flex-end; }
286
+ .turn.assistant, .turn.command { align-items: flex-start; }
287
+
288
+ .bubble {
289
+ max-width: 76%;
290
+ font-size: 17px;
291
+ line-height: 1.3;
292
+ word-break: break-word;
293
+ padding: 9px 14px;
294
+ }
295
+ .turn.assistant .bubble {
296
+ background: var(--received);
297
+ color: var(--ink);
298
+ border-radius: 20px 20px 20px 6px;
299
+ }
300
+ .turn.user .bubble {
301
+ background: var(--sent);
302
+ color: #fff;
303
+ border-radius: 20px 20px 6px 20px;
304
+ white-space: pre-wrap;
305
+ }
306
+ .turn.system .bubble {
307
+ max-width: none;
308
+ padding: 0;
309
+ background: none;
310
+ color: var(--muted);
311
+ font-size: 12px;
312
+ text-align: center;
313
+ }
314
+ .turn.command { width: 100%; }
315
+ .turn.command .bubble {
316
+ max-width: 100%;
317
+ padding: 0;
318
+ background: none;
319
+ }
320
+ body.hide-cmds .turn.command { display: none; }
321
+
322
+ .bubble .md > :first-child { margin-top: 0; }
323
+ .bubble .md > :last-child { margin-bottom: 0; }
324
+ .bubble .md p { margin: 0 0 0.55em; }
325
+ .bubble .md p:last-child { margin-bottom: 0; }
326
+ .bubble .md code {
327
+ font-family: var(--mono);
328
+ font-size: 15px;
329
+ background: rgba(0,0,0,.06);
330
+ border-radius: 4px;
331
+ padding: 0.05em 0.3em;
332
+ }
333
+ .turn.user .bubble .md code { background: rgba(255,255,255,.2); }
334
+ .bubble .md pre {
335
+ margin: 0.45em 0;
336
+ padding: 8px 10px;
337
+ background: rgba(0,0,0,.06);
338
+ border-radius: 10px;
339
+ overflow-x: auto;
340
+ }
341
+ .bubble .md pre code { background: none; padding: 0; font-size: 13px; }
342
+ .bubble .md a { color: inherit; text-decoration: underline; }
343
+ .bubble .md ul, .bubble .md ol { margin: 0.35em 0 0.55em; padding-left: 1.25em; }
344
+
345
+ .tool {
346
+ border: 1px solid var(--line-strong);
347
+ border-radius: 14px;
348
+ overflow: hidden;
349
+ background: var(--bg);
350
+ }
351
+ .tool > summary {
352
+ list-style: none;
353
+ cursor: pointer;
354
+ display: flex;
355
+ align-items: center;
356
+ gap: 10px;
357
+ padding: 10px 12px;
358
+ user-select: none;
359
+ }
360
+ .tool > summary::-webkit-details-marker { display: none; }
361
+ .tool .ico {
362
+ width: 26px;
363
+ height: 26px;
364
+ flex: none;
365
+ border-radius: 8px;
366
+ background: #eef4ff;
367
+ color: var(--sent);
368
+ display: flex;
369
+ align-items: center;
370
+ justify-content: center;
371
+ font-size: 12px;
372
+ font-family: var(--mono);
373
+ }
374
+ .tool .meta { flex: 1; min-width: 0; }
375
+ .tool .meta .lab { font-size: 13.5px; font-weight: 600; }
376
+ .tool .meta .cmd {
377
+ font-size: 12.5px;
378
+ font-family: var(--mono);
379
+ color: var(--muted);
380
+ white-space: nowrap;
381
+ overflow: hidden;
382
+ text-overflow: ellipsis;
383
+ }
384
+ .tool .chev { color: #c7c7cc; font-size: 15px; flex: none; }
385
+ .tool[open] .chev { transform: rotate(90deg); }
386
+ .tool .tool-body {
387
+ background: #1c1c1e;
388
+ color: #e5e5ea;
389
+ font-family: var(--mono);
390
+ font-size: 12px;
391
+ line-height: 1.7;
392
+ padding: 10px 12px;
393
+ white-space: pre-wrap;
394
+ max-height: 240px;
395
+ overflow: auto;
396
+ }
397
+
398
+ #bar {
399
+ flex: none;
400
+ padding: 10px 16px calc(12px + env(safe-area-inset-bottom));
401
+ border-top: 1px solid var(--line);
402
+ background: var(--bg);
403
+ }
404
+ #composer {
405
+ display: flex;
406
+ align-items: flex-end;
407
+ gap: 8px;
408
+ background: var(--bg);
409
+ border: 1px solid #d1d1d6;
410
+ border-radius: 22px;
411
+ padding: 6px 6px 6px 14px;
412
+ }
413
+ #msg {
414
+ flex: 1;
415
+ min-width: 0;
416
+ resize: none;
417
+ outline: none;
418
+ border: none;
419
+ background: transparent;
420
+ font-family: inherit;
421
+ font-size: 17px;
422
+ line-height: 1.3;
423
+ color: var(--ink);
424
+ padding: 9px 0;
425
+ max-height: 120px;
426
+ min-height: 22px;
427
+ }
428
+ #msg::placeholder { color: var(--muted); }
429
+ #send {
430
+ flex: none;
431
+ width: 36px;
432
+ height: 36px;
433
+ border: none;
434
+ border-radius: 18px;
435
+ background: var(--sent);
436
+ color: #fff;
437
+ font-size: 17px;
438
+ font-weight: 600;
439
+ cursor: pointer;
440
+ display: flex;
441
+ align-items: center;
442
+ justify-content: center;
443
+ }
444
+ #send:disabled { opacity: 0.4; }
445
+ #send.stop {
446
+ background: var(--fail);
447
+ font-size: 13px;
448
+ letter-spacing: 0;
449
+ }
450
+ </style>
451
+ </head>
452
+ <body>
453
+ <div id="sessions">
454
+ <div id="sessions-head">
455
+ <div class="row">
456
+ <h1>Sessions</h1>
457
+ <div id="sessions-meta">connecting…</div>
458
+ </div>
459
+ </div>
460
+ <div id="sess-list"></div>
461
+ <div id="sessions-foot">
462
+ <span class="hint">Swipe left to hide · <button id="restore-sessions" type="button">Restore hidden</button></span>
463
+ <button id="leave" type="button">Disconnect</button>
464
+ </div>
465
+ </div>
466
+
467
+ <div id="thread" hidden>
468
+ <div id="top">
469
+ <button id="back" type="button" aria-label="Back to sessions">‹</button>
470
+ <div id="titlewrap">
471
+ <div id="title"><span class="live" id="livedot" hidden></span><span id="titletext">cmux</span></div>
472
+ <div id="status">…</div>
473
+ </div>
474
+ <button id="ctog" type="button" title="Show tools" aria-pressed="true">C</button>
475
+ </div>
476
+ <div id="log"></div>
477
+ <div id="bar">
478
+ <div id="composer">
479
+ <textarea id="msg" rows="1" placeholder="Message the agent…" enterkeyhint="send"></textarea>
480
+ <button id="send" type="button" aria-label="Send">↑</button>
481
+ </div>
482
+ </div>
483
+ </div>
484
+
485
+ <script>
486
+ (function() {
487
+ var TOKEN = __TOKEN_JSON__;
488
+ var LANDING = __LANDING_JSON__;
489
+ var API = __API_BASE_JSON__;
490
+ var sessionsView = document.getElementById("sessions");
491
+ var threadView = document.getElementById("thread");
492
+ var sessList = document.getElementById("sess-list");
493
+ var sessionsMeta = document.getElementById("sessions-meta");
494
+ var log = document.getElementById("log");
495
+ var msg = document.getElementById("msg");
496
+ var sendBtn = document.getElementById("send");
497
+ var statusEl = document.getElementById("status");
498
+ var titleText = document.getElementById("titletext");
499
+ var liveDot = document.getElementById("livedot");
500
+ var ctog = document.getElementById("ctog");
501
+ var backBtn = document.getElementById("back");
502
+ var leaveBtn = document.getElementById("leave");
503
+ var restoreBtn = document.getElementById("restore-sessions");
504
+
505
+ var misses = 0;
506
+ var lastSig = "";
507
+ var sending = false;
508
+ var pendingUser = null;
509
+ var agentBusy = false;
510
+ var interrupting = false;
511
+ var holdStop = false;
512
+ var sawBusy = false;
513
+ var sendAt = 0;
514
+ var poller = null;
515
+ var leaving = false;
516
+ var showCmds = true;
517
+ var known = [];
518
+ var activeKey = null;
519
+ var inThread = false;
520
+ var lastSessions = [];
521
+ var hiddenSessions = loadHidden();
522
+ var openSwipeRow = null;
523
+
524
+ function loadHidden() {
525
+ try {
526
+ var raw = localStorage.getItem("longleash.hiddenSessions");
527
+ var arr = raw ? JSON.parse(raw) : [];
528
+ return Array.isArray(arr) ? arr : [];
529
+ } catch (e) {
530
+ return [];
531
+ }
532
+ }
533
+
534
+ function saveHidden() {
535
+ try {
536
+ localStorage.setItem("longleash.hiddenSessions", JSON.stringify(hiddenSessions));
537
+ } catch (e) {}
538
+ }
539
+
540
+ function sessionKey(s) {
541
+ return String(s.surface || s.surface_id || s.title || "");
542
+ }
543
+
544
+ function normText(text) {
545
+ return String(text || "").replace(/\s+/g, " ").trim();
546
+ }
547
+
548
+ function pendingSeen(messages, text) {
549
+ var needle = normText(text);
550
+ if (!needle) return true;
551
+ return (messages || []).some(function(m) {
552
+ if (m.role !== "user") return false;
553
+ var got = normText(m.text);
554
+ return got === needle || got.indexOf(needle) !== -1 || needle.indexOf(got) !== -1;
555
+ });
556
+ }
557
+
558
+ function clearComposer() {
559
+ msg.value = "";
560
+ msg.style.height = "auto";
561
+ requestAnimationFrame(function() {
562
+ msg.value = "";
563
+ msg.dispatchEvent(new Event("input", { bubbles: true }));
564
+ });
565
+ }
566
+
567
+ function esc(s) {
568
+ return String(s).replace(/[&<>"']/g, function(c) {
569
+ return ({"&":"&amp;","<":"&lt;",">":"&gt;","\"":"&quot;","'":"&#39;"})[c];
570
+ });
571
+ }
572
+
573
+ function shortPath(p) {
574
+ if (!p) return "";
575
+ return String(p).replace(/^\/Users\/[^\/]+/, "~");
576
+ }
577
+
578
+ function showSessions() {
579
+ inThread = false;
580
+ sessionsView.hidden = false;
581
+ threadView.hidden = true;
582
+ }
583
+
584
+ function showThread() {
585
+ inThread = true;
586
+ sessionsView.hidden = true;
587
+ threadView.hidden = false;
588
+ }
589
+
590
+ function inlineMd(s) {
591
+ s = esc(s);
592
+ s = s.replace(/`([^`]+)`/g, "<code>$1</code>");
593
+ s = s.replace(/\*\*([^*]+)\*\*/g, "<strong>$1</strong>");
594
+ s = s.replace(/(^|[^*])\*([^*]+)\*(?!\*)/g, "$1<em>$2</em>");
595
+ s = s.replace(/\[([^\]]+)\]\((https?:\/\/[^)\s]+)\)/g, '<a href="$2" target="_blank" rel="noopener">$1</a>');
596
+ return s;
597
+ }
598
+
599
+ function renderMd(src) {
600
+ var text = String(src || "").replace(/\r\n/g, "\n");
601
+ var lines = text.split("\n");
602
+ var html = [];
603
+ var i = 0;
604
+ function flushPara(buf) {
605
+ if (!buf.length) return;
606
+ html.push("<p>" + inlineMd(buf.join(" ")) + "</p>");
607
+ buf.length = 0;
608
+ }
609
+ while (i < lines.length) {
610
+ var line = lines[i];
611
+ var fence = line.match(/^```([\w-]*)\s*$/);
612
+ if (fence) {
613
+ flushPara([]);
614
+ var code = [];
615
+ i++;
616
+ while (i < lines.length && !/^```\s*$/.test(lines[i])) {
617
+ code.push(lines[i]); i++;
618
+ }
619
+ html.push("<pre><code>" + esc(code.join("\n")) + "</code></pre>");
620
+ i++; continue;
621
+ }
622
+ if (!line.trim()) { flushPara([]); i++; continue; }
623
+ var para = [];
624
+ while (i < lines.length && lines[i].trim() && !/^```/.test(lines[i])) {
625
+ para.push(lines[i]); i++;
626
+ }
627
+ flushPara(para);
628
+ }
629
+ return '<div class="md">' + (html.join("") || ("<p>" + inlineMd(text) + "</p>")) + "</div>";
630
+ }
631
+
632
+ function expandMessages(messages) {
633
+ var out = [];
634
+ (messages || []).forEach(function(m) {
635
+ var kind = m.kind || "message";
636
+ var role = m.role === "user" ? "user" : (kind === "command" ? "command" : "assistant");
637
+ if (role === "assistant" && kind === "message") {
638
+ var chunks = String(m.text || "").split(/\n{2,}/).map(function(s) { return s.trim(); }).filter(Boolean);
639
+ if (chunks.length > 1) {
640
+ chunks.forEach(function(chunk) {
641
+ out.push({ role: "assistant", kind: "message", text: chunk });
642
+ });
643
+ return;
644
+ }
645
+ }
646
+ out.push({ role: role, kind: kind, text: m.text || "" });
647
+ });
648
+ return out;
649
+ }
650
+
651
+ function closeSwipeRow() {
652
+ if (openSwipeRow) {
653
+ openSwipeRow.classList.remove("open");
654
+ openSwipeRow = null;
655
+ }
656
+ }
657
+
658
+ function bindSwipeRows() {
659
+ sessList.querySelectorAll(".sess-row").forEach(function(row) {
660
+ var btn = row.querySelector(".sess");
661
+ var del = row.querySelector(".sess-delete");
662
+ var startX = 0;
663
+ var startY = 0;
664
+ var baseX = 0;
665
+ var dragging = false;
666
+
667
+ function setX(x) {
668
+ btn.style.transform = "translateX(" + Math.max(-88, Math.min(0, x)) + "px)";
669
+ }
670
+
671
+ btn.addEventListener("touchstart", function(e) {
672
+ if (e.touches.length !== 1) return;
673
+ closeSwipeRow();
674
+ startX = e.touches[0].clientX;
675
+ startY = e.touches[0].clientY;
676
+ baseX = row.classList.contains("open") ? -88 : 0;
677
+ dragging = true;
678
+ }, { passive: true });
679
+
680
+ btn.addEventListener("touchmove", function(e) {
681
+ if (!dragging || e.touches.length !== 1) return;
682
+ var dx = e.touches[0].clientX - startX;
683
+ var dy = e.touches[0].clientY - startY;
684
+ if (Math.abs(dy) > Math.abs(dx) && Math.abs(dx) < 12) return;
685
+ if (Math.abs(dx) > 8) e.preventDefault();
686
+ setX(baseX + dx);
687
+ }, { passive: false });
688
+
689
+ btn.addEventListener("touchend", function(e) {
690
+ if (!dragging) return;
691
+ dragging = false;
692
+ btn.style.transform = "";
693
+ var endX = (e.changedTouches[0] || {}).clientX || startX;
694
+ var dx = endX - startX;
695
+ if (baseX === 0 && dx < -36) {
696
+ row.classList.add("open");
697
+ openSwipeRow = row;
698
+ } else if (baseX === -88 && dx > 24) {
699
+ row.classList.remove("open");
700
+ openSwipeRow = null;
701
+ } else if (baseX === 0) {
702
+ row.classList.remove("open");
703
+ } else {
704
+ row.classList.add("open");
705
+ openSwipeRow = row;
706
+ }
707
+ });
708
+
709
+ del.addEventListener("click", function(e) {
710
+ e.stopPropagation();
711
+ var key = row.getAttribute("data-key");
712
+ if (key && hiddenSessions.indexOf(key) === -1) hiddenSessions.push(key);
713
+ saveHidden();
714
+ closeSwipeRow();
715
+ renderPicker(lastSessions, activeKey);
716
+ });
717
+ });
718
+ }
719
+
720
+ function toolLabel(firstLine) {
721
+ var s = String(firstLine || "").replace(/^\$\s*/, "").trim();
722
+ if (/^Read,?\b/i.test(s)) return "Read files";
723
+ if (/^Read\b/i.test(s)) return "Read file";
724
+ if (/^Grepped\b/i.test(s)) return "Searched";
725
+ if (/^Edited\b/i.test(s)) return "Edited file";
726
+ if (/^Monitored\b/i.test(s)) return "Monitored task";
727
+ if (/^Waited\b/i.test(s)) return "Waited";
728
+ if (/^Finished\b/i.test(s)) return "Finished";
729
+ if (/^Ran\b/i.test(s)) return "Ran";
730
+ return "Ran command";
731
+ }
732
+
733
+ function splitTool(text) {
734
+ var raw = String(text || "").replace(/\r\n/g, "\n").trim();
735
+ var lines = raw.split("\n");
736
+ var first = (lines[0] || "").replace(/^\$\s*/, "").trim();
737
+ var rest = lines.slice(1).join("\n").replace(/^\n+/, "");
738
+ var dur = "";
739
+ var dm = first.match(/\s+(\d+(?:\.\d+)?m?s)\s*$/);
740
+ if (dm) {
741
+ dur = dm[1];
742
+ first = first.slice(0, -dm[0].length).trim();
743
+ }
744
+ return { cmd: first || "command", out: rest, dur: dur };
745
+ }
746
+
747
+ function renderTool(text) {
748
+ var t = splitTool(text);
749
+ var lab = toolLabel(t.cmd);
750
+ return '<details class="tool">' +
751
+ "<summary>" +
752
+ '<span class="ico">›_</span>' +
753
+ '<span class="meta"><div class="lab">' + esc(lab) + '</div><div class="cmd">' + esc(t.cmd) + "</div></span>" +
754
+ '<span class="chev">›</span>' +
755
+ "</summary>" +
756
+ (t.out ? '<div class="tool-body">' + esc(t.out) + "</div>" : '<div class="tool-body">' + esc(t.cmd) + "</div>") +
757
+ "</details>";
758
+ }
759
+
760
+ function setCmds(on) {
761
+ showCmds = !!on;
762
+ document.body.classList.toggle("hide-cmds", !showCmds);
763
+ ctog.classList.toggle("on", showCmds);
764
+ ctog.setAttribute("aria-pressed", showCmds ? "true" : "false");
765
+ }
766
+
767
+ function render(messages) {
768
+ known = messages || known;
769
+ var html = "";
770
+ var expanded = expandMessages(known);
771
+ if (!expanded.length && !pendingUser) {
772
+ html = '<div class="turn system"><div class="bubble">Say something to the agent.</div></div>';
773
+ }
774
+ expanded.forEach(function(m) {
775
+ var role = m.role;
776
+ var body;
777
+ if (role === "command") body = renderTool(m.text);
778
+ else if (role === "user") body = esc(m.text);
779
+ else body = renderMd(m.text);
780
+ html += '<div class="turn ' + role + '"><div class="bubble">' + body + "</div></div>";
781
+ });
782
+ if (pendingUser) {
783
+ if (pendingSeen(known, pendingUser)) {
784
+ pendingUser = null;
785
+ updateComposerMode();
786
+ } else {
787
+ html += '<div class="turn user"><div class="bubble">' + esc(pendingUser) + "</div></div>";
788
+ }
789
+ }
790
+ log.innerHTML = html;
791
+ log.scrollTop = log.scrollHeight;
792
+ }
793
+
794
+ function setStatus(text, cls) {
795
+ statusEl.textContent = text;
796
+ statusEl.className = cls || "";
797
+ }
798
+
799
+ function setSessionsMeta(text, cls) {
800
+ sessionsMeta.textContent = text;
801
+ sessionsMeta.className = cls || "";
802
+ }
803
+
804
+ function renderPicker(sessions, currentSurface) {
805
+ lastSessions = sessions || [];
806
+ activeKey = currentSurface;
807
+ closeSwipeRow();
808
+ var visible = lastSessions.filter(function(s) {
809
+ return hiddenSessions.indexOf(sessionKey(s)) === -1;
810
+ });
811
+ if (!visible.length) {
812
+ var msg = lastSessions.length
813
+ ? "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">' + msg + "</div></div>";
816
+ return;
817
+ }
818
+ sessList.innerHTML = visible.map(function(s) {
819
+ var surface = s.surface || s.surface_id || "";
820
+ var workspace = s.workspace || s.workspace_id || "";
821
+ var title = esc(s.title || surface || "session");
822
+ var cwd = esc(shortPath(s.cwd || s.current_directory || ""));
823
+ var key = esc(sessionKey(s));
824
+ var active = surface === currentSurface;
825
+ var snip = active ? "Open in this chat" : "Tap to open";
826
+ return '<div class="sess-row" data-key="' + key + '">' +
827
+ '<button type="button" class="sess-delete">Delete</button>' +
828
+ '<button type="button" class="sess' + (active ? " active" : "") + '"' +
829
+ ' data-surface="' + esc(surface) + '"' +
830
+ ' data-workspace="' + esc(workspace) + '"' +
831
+ ' data-window="' + esc(s.window || "") + '"' +
832
+ ' data-title="' + title + '">' +
833
+ '<span class="dot"></span>' +
834
+ '<span class="body">' +
835
+ '<span class="top"><span class="name">' + title + '</span></span>' +
836
+ '<span class="snip">' + snip + "</span>" +
837
+ (cwd ? '<span class="path">' + cwd + "</span>" : "") +
838
+ "</span></button></div>";
839
+ }).join("");
840
+ bindSwipeRows();
841
+ }
842
+
843
+ async function selectSession(surface, workspace, windowRef, title) {
844
+ try {
845
+ var r = await fetch(API + "/select?t=" + encodeURIComponent(TOKEN), {
846
+ method: "POST",
847
+ headers: { "Content-Type": "application/json" },
848
+ body: JSON.stringify({ surface: surface, workspace: workspace, window: windowRef || null })
849
+ });
850
+ var j = await r.json();
851
+ if (!j.ok) {
852
+ setSessionsMeta(j.error || "switch failed", "err");
853
+ return;
854
+ }
855
+ activeKey = j.surface || surface;
856
+ titleText.textContent = title || j.title || "cmux";
857
+ liveDot.hidden = false;
858
+ // Hard-reset thread so the previous surface's messages can't linger.
859
+ known = [];
860
+ pendingUser = null;
861
+ lastSig = "";
862
+ holdStop = false;
863
+ sawBusy = false;
864
+ agentBusy = false;
865
+ log.innerHTML = "";
866
+ updateComposerMode();
867
+ showThread();
868
+ await refreshStatus();
869
+ await refreshTranscript();
870
+ } catch (err) {
871
+ setSessionsMeta("switch failed", "err");
872
+ }
873
+ }
874
+
875
+ sessList.addEventListener("click", function(e) {
876
+ if (e.target.closest(".sess-delete")) return;
877
+ var btn = e.target.closest(".sess");
878
+ if (!btn) return;
879
+ if (btn.closest(".sess-row") && btn.closest(".sess-row").classList.contains("open")) {
880
+ closeSwipeRow();
881
+ return;
882
+ }
883
+ selectSession(
884
+ btn.getAttribute("data-surface"),
885
+ btn.getAttribute("data-workspace"),
886
+ btn.getAttribute("data-window"),
887
+ btn.getAttribute("data-title")
888
+ );
889
+ });
890
+
891
+ backBtn.addEventListener("click", function() {
892
+ showSessions();
893
+ refreshStatus();
894
+ });
895
+
896
+ function disconnect() {
897
+ if (leaving) return;
898
+ leaving = true;
899
+ clearInterval(poller);
900
+ fetch(API + "/shutdown?t=" + encodeURIComponent(TOKEN), {
901
+ method: "POST",
902
+ keepalive: true,
903
+ }).catch(function() {});
904
+ window.location.replace(LANDING || "https://callwalkietalkie.com");
905
+ }
906
+ leaveBtn.addEventListener("click", disconnect);
907
+ restoreBtn.addEventListener("click", function() {
908
+ hiddenSessions = [];
909
+ saveHidden();
910
+ renderPicker(lastSessions, activeKey);
911
+ });
912
+
913
+ async function refreshStatus() {
914
+ try {
915
+ var r = await fetch(API + "/status?t=" + encodeURIComponent(TOKEN));
916
+ var j = await r.json();
917
+ misses = 0;
918
+ if (!j.ok) {
919
+ setSessionsMeta(j.error || "cmux not ready", "err");
920
+ setStatus(j.error || "cmux not ready", "err");
921
+ liveDot.hidden = true;
922
+ return false;
923
+ }
924
+ setSessionsMeta("localhost · live", "ok");
925
+ titleText.textContent = j.title || "cmux";
926
+ liveDot.hidden = false;
927
+ var bits = [];
928
+ if (j.cwd) bits.push(shortPath(j.cwd));
929
+ if (j.surface) bits.push(j.surface);
930
+ setStatus(bits.join(" · ") || "live", "ok");
931
+ renderPicker(j.sessions || j.workspaces || [], j.surface);
932
+ return true;
933
+ } catch (e) {
934
+ if (++misses >= 2) {
935
+ setSessionsMeta("can't reach your Mac", "err");
936
+ setStatus("can't reach your Mac", "err");
937
+ }
938
+ return false;
939
+ }
940
+ }
941
+
942
+ async function refreshTranscript() {
943
+ if (!inThread) return;
944
+ try {
945
+ var r = await fetch(API + "/transcript?t=" + encodeURIComponent(TOKEN));
946
+ var j = await r.json();
947
+ if (!j.ok) {
948
+ if (j.error) setStatus(j.error, "err");
949
+ return;
950
+ }
951
+ agentBusy = !!j.busy;
952
+ if (agentBusy) sawBusy = true;
953
+ if (holdStop && !agentBusy && !pendingUser && (sawBusy || (sendAt && Date.now() - sendAt > 4000))) {
954
+ holdStop = false;
955
+ sawBusy = false;
956
+ }
957
+ updateComposerMode();
958
+ var msgs = j.messages || [];
959
+ var sig = msgs.map(function(m) { return (m.kind||"") + ":" + m.role + ":" + m.text; }).join("\x1e");
960
+ if (sig !== lastSig || pendingUser || agentBusy) {
961
+ lastSig = sig;
962
+ render(msgs);
963
+ }
964
+ } catch (e) {}
965
+ }
966
+
967
+ function isStopMode() {
968
+ // If the user is composing a follow-up, always Send — never steal the
969
+ // button for Stop mid-keystroke (that was eating / breaking messages).
970
+ if ((msg.value || "").trim()) return false;
971
+ return !!(agentBusy || pendingUser || interrupting || holdStop);
972
+ }
973
+
974
+ async function send() {
975
+ var text = (msg.value || "").trim();
976
+ // Allow queueing a follow-up while the agent is busy; only block on in-flight send/stop.
977
+ if (!text || sending || interrupting) return;
978
+ sending = true;
979
+ sendBtn.disabled = true;
980
+ pendingUser = text;
981
+ holdStop = true;
982
+ sawBusy = false;
983
+ sendAt = Date.now();
984
+ clearComposer();
985
+ updateComposerMode();
986
+ render(known);
987
+ try {
988
+ var r = await fetch(API + "/send?t=" + encodeURIComponent(TOKEN), {
989
+ method: "POST",
990
+ headers: { "Content-Type": "application/json" },
991
+ body: JSON.stringify({ text: text })
992
+ });
993
+ var j = await r.json();
994
+ if (!j.ok) {
995
+ setStatus(j.error || "send failed", "err");
996
+ pendingUser = null;
997
+ holdStop = false;
998
+ msg.value = text;
999
+ render(known);
1000
+ } else {
1001
+ await refreshTranscript();
1002
+ render(known);
1003
+ }
1004
+ } catch (e) {
1005
+ setStatus("send failed", "err");
1006
+ pendingUser = null;
1007
+ holdStop = false;
1008
+ msg.value = text;
1009
+ render(known);
1010
+ }
1011
+ sending = false;
1012
+ updateComposerMode();
1013
+ await refreshTranscript();
1014
+ }
1015
+
1016
+ async function interrupt() {
1017
+ if (interrupting) return;
1018
+ interrupting = true;
1019
+ updateComposerMode();
1020
+ try {
1021
+ var r = await fetch(API + "/interrupt?t=" + encodeURIComponent(TOKEN), {
1022
+ method: "POST",
1023
+ signal: AbortSignal.timeout(12000)
1024
+ });
1025
+ var j = await r.json();
1026
+ if (!j.ok) setStatus(j.error || "stop failed", "err");
1027
+ else {
1028
+ holdStop = false;
1029
+ pendingUser = null;
1030
+ setStatus("stopping…");
1031
+ }
1032
+ await refreshTranscript();
1033
+ } catch (e) {
1034
+ setStatus("stop failed", "err");
1035
+ }
1036
+ interrupting = false;
1037
+ updateComposerMode();
1038
+ }
1039
+
1040
+ function updateComposerMode() {
1041
+ var stop = isStopMode();
1042
+ sendBtn.classList.toggle("stop", stop);
1043
+ sendBtn.setAttribute("aria-label", stop ? "Stop" : "Send");
1044
+ sendBtn.textContent = stop ? "■" : "↑";
1045
+ sendBtn.disabled = sending || interrupting;
1046
+ }
1047
+
1048
+ sendBtn.addEventListener("click", function(e) {
1049
+ e.preventDefault();
1050
+ if (isStopMode()) interrupt();
1051
+ else send();
1052
+ });
1053
+ var shiftHeld = false;
1054
+ function shouldSendKey(e) {
1055
+ if (e.isComposing || e.keyCode === 229) return false;
1056
+ if (e.shiftKey || (e.getModifierState && e.getModifierState("Shift"))) return false;
1057
+ return e.key === "Enter" || e.code === "Enter" || e.keyCode === 13 || e.which === 13;
1058
+ }
1059
+ function isSendLineBreak(e) {
1060
+ if (e.isComposing) return false;
1061
+ if (e.shiftKey || shiftHeld || (e.getModifierState && e.getModifierState("Shift"))) return false;
1062
+ if (e.inputType === "insertLineBreak" || e.inputType === "insertParagraph") return true;
1063
+ // Some mobile / soft keyboards insert a literal newline (or odd space) as insertText.
1064
+ if (e.inputType === "insertText" && e.data != null && /[\r\n]/.test(e.data)) return true;
1065
+ return false;
1066
+ }
1067
+ function stripSneakyBreaksAndSend() {
1068
+ if (shiftHeld || msg.value.indexOf("\n") < 0) return;
1069
+ msg.value = msg.value.replace(/\r?\n/g, "");
1070
+ if ((msg.value || "").trim()) send();
1071
+ }
1072
+ msg.addEventListener("keydown", function(e) {
1073
+ if (e.key === "Shift") shiftHeld = true;
1074
+ if (!shouldSendKey(e)) return;
1075
+ e.preventDefault();
1076
+ e.stopPropagation();
1077
+ send();
1078
+ });
1079
+ msg.addEventListener("keyup", function(e) {
1080
+ if (e.key === "Shift") shiftHeld = false;
1081
+ // Soft-keyboard Send/return: keydown often never fires; a newline sneaks in instead.
1082
+ if (!shouldSendKey(e)) return;
1083
+ stripSneakyBreaksAndSend();
1084
+ });
1085
+ // iOS / enterkeyhint="send" often uses beforeinput instead of a normal Enter keydown.
1086
+ msg.addEventListener("beforeinput", function(e) {
1087
+ if (!isSendLineBreak(e)) return;
1088
+ e.preventDefault();
1089
+ send();
1090
+ });
1091
+ msg.addEventListener("input", function(e) {
1092
+ msg.style.height = "auto";
1093
+ msg.style.height = Math.min(msg.scrollHeight, 120) + "px";
1094
+ updateComposerMode();
1095
+ // Last-resort for keyboards that insert a break after beforeinput failed.
1096
+ // Skip paste / undo so multiline paste isn't treated as Send.
1097
+ var t = e && e.inputType;
1098
+ if (t === "insertFromPaste" || t === "historyUndo" || t === "historyRedo" || t === "insertFromDrop") return;
1099
+ if (t === "insertLineBreak" || t === "insertParagraph" ||
1100
+ (t === "insertText" && msg.value.indexOf("\n") >= 0)) {
1101
+ stripSneakyBreaksAndSend();
1102
+ }
1103
+ });
1104
+ msg.addEventListener("blur", function() { shiftHeld = false; });
1105
+ ctog.addEventListener("click", function() { setCmds(!showCmds); });
1106
+
1107
+ setCmds(true);
1108
+ showSessions();
1109
+ refreshStatus();
1110
+ poller = setInterval(function() {
1111
+ if (leaving) return;
1112
+ refreshStatus();
1113
+ refreshTranscript();
1114
+ }, 900);
1115
+ })();
1116
+ </script>
1117
+ </body>
1118
+ </html>