@kevin5251984/guild 0.2.20 → 0.2.22

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.
@@ -31,6 +31,8 @@ var I18N_ROWS = [
31
31
  ["search.traj", "搜尋 system / tool / spawn / 子代理 / 結果", "Search system / tool / spawn / subagent / result"],
32
32
  ["channels", "委託", "Channels"],
33
33
  ["dms", "密談", "Whispers"],
34
+ ["cron.navEmpty", "還沒有排程", "No schedules"],
35
+ ["cron.navWhere", "@{handle} · {schedule}", "@{handle} · {schedule}"],
34
36
  ["newChannel", "新委託", "New channel"],
35
37
  ["newChannelName", "委託名稱", "Channel name"],
36
38
  ["create", "建立", "Create"],
@@ -90,6 +92,9 @@ var I18N_ROWS = [
90
92
  ["stop", "停止", "Stop"],
91
93
  ["steer.hint", "Enter 排隊 · {mod}↩ 插入這輪", "Enter to queue · {mod}↩ to steer this turn"],
92
94
  ["live.stop", "停止", "Stop"],
95
+ ["live.pause", "暫停", "Pause"],
96
+ ["live.continue", "繼續", "Continue"],
97
+ ["live.paused", "已暫停", "Paused"],
93
98
  ["live.steer", "插入這輪", "Insert into turn"],
94
99
  ["steer.queue", "排隊", "Queue"],
95
100
  ["steer.insert", "插入引導", "Steer"],
@@ -153,6 +158,7 @@ var I18N_ROWS = [
153
158
  ["attach.rules", "規則", "Rules"],
154
159
  ["attach.term", "終端", "Terminal"],
155
160
  ["attach.tree", "工作區", "Workspace"],
161
+ ["attach.cron", "排程", "Schedule"],
156
162
  ["attach.slash", "指令", "Commands"],
157
163
  ["insert", "插入 {name}", "Insert {name}"],
158
164
  ["remove", "移除", "Remove"],
@@ -221,6 +227,23 @@ var I18N_ROWS = [
221
227
  ["slash.here", "叫所有人", "Ping everyone"],
222
228
  ["slash.hereHint", "通知這個據點上的所有冒險者。通常是錯的:出活仍點名一人。", "Notify every adventurer in this hall. Usually wrong: call one @handle."],
223
229
  ["slash.library", "開工坊", "Open workshop"],
230
+ ["slash.cron", "排程", "Schedule"],
231
+ ["slash.cronHint", "請這席用 cronjob 排", "Ask this seat to cronjob"],
232
+ ["cron.title", "排程", "Schedule"],
233
+ ["cron.help", "新增會請這席呼叫 cronjob。時間用自然語言寫即可,例如「每10分鐘跟我說一次 Hi」。guildd 每 60 秒看一次;到點後這席開一輪新的 @handle,prompt 必須自包含。", "Add asks this seat to call cronjob. Write the time in natural language, e.g. \"every 10 minutes say Hi\". guildd ticks every 60s. Due jobs start a fresh @handle turn with a self-contained prompt."],
234
+ ["cron.seat", "席", "Seat"],
235
+ ["cron.ask", "要排什麼", "What to schedule"],
236
+ ["cron.askPh", "每10分鐘跟我說一次 Hi", "every 10 minutes say Hi"],
237
+ ["cron.askBody", "請用 cronjob 排程:{ask}", "Create a cronjob for this: {ask}"],
238
+ ["cron.needAsk", "先寫要排什麼", "Write what to schedule first"],
239
+ ["cron.needSeat", "這個據點沒有席", "No seat in this hall"],
240
+ ["cron.add", "請他排", "Ask them"],
241
+ ["cron.pause", "暫停", "Pause"],
242
+ ["cron.resume", "繼續", "Resume"],
243
+ ["cron.run", "立刻跑", "Run now"],
244
+ ["cron.remove", "刪除", "Remove"],
245
+ ["cron.empty", "這個據點還沒有排程。", "No schedules in this hall."],
246
+ ["cron.next", "下次 {when}", "Next {when}"],
224
247
  ["slash.sec.skill", "技能", "Skills"],
225
248
  ["slash.sec.agent", "子代理", "Subagents"],
226
249
  ["slash.sec.cmd", "指令", "Commands"],
package/src/public/md.js CHANGED
@@ -19,22 +19,39 @@ function isFileCode(text) {
19
19
  return /[A-Za-z0-9]\/[A-Za-z0-9._@+-]/.test(s);
20
20
  }
21
21
 
22
+ function localImgSrc(href) {
23
+ const raw = String(href || "")
24
+ .trim()
25
+ .replace(/"/g, "");
26
+ if (!raw) return "";
27
+ if (/^\/generated\/[A-Za-z0-9._-]+$/.test(raw)) return raw;
28
+ if (/^https?:\/\//i.test(raw)) return raw;
29
+ let path = raw;
30
+ if (/^file:\/\//i.test(path)) path = path.replace(/^file:\/\//i, "");
31
+ if (!path.startsWith("/") || path.startsWith("//") || path.includes("..")) {
32
+ return "";
33
+ }
34
+ if (!/\.(?:png|jpe?g|gif|webp)$/i.test(path)) return "";
35
+ return "/local?p=" + encodeURIComponent(path);
36
+ }
37
+
22
38
  function inlineMd(text) {
23
39
  let out = escapeMd(text);
24
- out = out.replace(
25
- /!\[([^\]]*)\]\((\/generated\/[A-Za-z0-9._-]+|https?:[^)\s]+)\)/g,
26
- function (_m, alt, href) {
27
- const src = String(href).replace(/"/g, "");
28
- const label = alt || "image";
29
- return (
30
- '<img class="md-img" src="' +
31
- src +
32
- '" alt="' +
33
- label +
34
- '" loading="lazy">'
35
- );
36
- },
37
- );
40
+ out = out.replace(/!\[([^\]]*)\]\(([^)\s]+)\)/g, function (m, alt, href) {
41
+ const src = localImgSrc(href);
42
+ if (!src) return m;
43
+ const label = alt || "image";
44
+ return (
45
+ '<a class="md-img-link" href="' +
46
+ src +
47
+ '" target="_blank" rel="noopener noreferrer">' +
48
+ '<img class="md-img" src="' +
49
+ src +
50
+ '" alt="' +
51
+ label +
52
+ '" loading="lazy"></a>'
53
+ );
54
+ });
38
55
  out = out.replace(/\[([^\]]+)\]\((https?:[^)\s]+)\)/g, function (_m, label, href) {
39
56
  return (
40
57
  '<a href="' +
@@ -327,6 +344,83 @@ function renderMarkdown(raw) {
327
344
  return html.join("");
328
345
  }
329
346
 
347
+ function htmlPreviewSrcdoc(raw, lang) {
348
+ const text = String(raw || "");
349
+ if (/svg/i.test(String(lang || ""))) {
350
+ return (
351
+ '<!doctype html><html><body style="margin:0;background:#fff">' +
352
+ text +
353
+ "</body></html>"
354
+ );
355
+ }
356
+ if (/<!doctype html|<html[\s>]/i.test(text)) return text;
357
+ return (
358
+ '<!doctype html><html><body style="margin:0;background:transparent">' +
359
+ text +
360
+ "</body></html>"
361
+ );
362
+ }
363
+
364
+ function hydrateHtmlPreviews(root) {
365
+ if (!root || typeof root.querySelectorAll !== "function") return;
366
+ root.querySelectorAll(".md-html-preview").forEach((box) => {
367
+ const src = box.querySelector(".md-html-src");
368
+ const frame = box.querySelector(".md-html-frame");
369
+ if (!src || !frame) return;
370
+ const raw = src.value;
371
+ const lang = (box.querySelector(".md-fence-lang") || {}).textContent || "";
372
+ const doc = htmlPreviewSrcdoc(raw, lang);
373
+ if (frame.dataset.ready && frame.srcdoc === doc) return;
374
+ frame.dataset.ready = "1";
375
+ frame.srcdoc = doc;
376
+ });
377
+ }
378
+
379
+ function holdHtmlFrames(root) {
380
+ const frames = [];
381
+ if (!root || typeof root.querySelectorAll !== "function") return frames;
382
+ root.querySelectorAll("article.msg .md-html-preview").forEach((box) => {
383
+ const article = box.closest("article.msg[data-id]");
384
+ const frame = box.querySelector(".md-html-frame");
385
+ const src = box.querySelector(".md-html-src");
386
+ if (!article || !frame || !src || !frame.dataset.ready) return;
387
+ frames.push({
388
+ id: article.getAttribute("data-id"),
389
+ src: src.value,
390
+ view: box.getAttribute("data-view") || "preview",
391
+ frame: frame,
392
+ });
393
+ });
394
+ return frames;
395
+ }
396
+
397
+ function putHtmlFrames(root, frames) {
398
+ if (!root || typeof root.querySelector !== "function") return;
399
+ (frames || []).forEach((item) => {
400
+ const article = root.querySelector(
401
+ 'article.msg[data-id="' + CSS.escape(item.id) + '"]',
402
+ );
403
+ const box = article && article.querySelector(".md-html-preview");
404
+ const src = box && box.querySelector(".md-html-src");
405
+ const fresh = box && box.querySelector(".md-html-frame");
406
+ if (!box || !src || !fresh || src.value !== item.src) return;
407
+ item.frame.dataset.ready = "1";
408
+ fresh.replaceWith(item.frame);
409
+ box.setAttribute("data-view", item.view);
410
+ box.querySelectorAll("[data-html-view]").forEach((tab) => {
411
+ tab.classList.toggle("on", tab.getAttribute("data-html-view") === item.view);
412
+ });
413
+ });
414
+ }
415
+
330
416
  if (typeof module !== "undefined" && module.exports) {
331
- module.exports = { renderMarkdown, inlineMd };
417
+ module.exports = {
418
+ renderMarkdown,
419
+ inlineMd,
420
+ hydrateHtmlPreviews,
421
+ htmlPreviewSrcdoc,
422
+ holdHtmlFrames,
423
+ putHtmlFrames,
424
+ localImgSrc,
425
+ };
332
426
  }
@@ -57,6 +57,8 @@ textarea:focus-visible {
57
57
  .navlist a,
58
58
  .back,
59
59
  .turn-stop,
60
+ .turn-pause,
61
+ .turn-continue,
60
62
  .turn-steer,
61
63
  .steer,
62
64
  .send,
@@ -72,6 +74,8 @@ textarea:focus-visible {
72
74
  .navlist a:active,
73
75
  .back:active,
74
76
  .turn-stop:active:not(:disabled),
77
+ .turn-pause:active:not(:disabled),
78
+ .turn-continue:active:not(:disabled),
75
79
  .turn-steer:active:not(:disabled),
76
80
  .steer:active:not(:disabled),
77
81
  .send:active:not(:disabled),
@@ -83,6 +87,8 @@ textarea:focus-visible {
83
87
  .navlist a,
84
88
  .back,
85
89
  .turn-stop,
90
+ .turn-pause,
91
+ .turn-continue,
86
92
  .turn-steer,
87
93
  .steer,
88
94
  .send,
@@ -97,6 +103,8 @@ textarea:focus-visible {
97
103
  .navlist a:active,
98
104
  .back:active,
99
105
  .turn-stop:active,
106
+ .turn-pause:active,
107
+ .turn-continue:active,
100
108
  .turn-steer:active,
101
109
  .steer:active,
102
110
  .send:active,
@@ -326,6 +334,7 @@ textarea:focus-visible {
326
334
  background: var(--fill);
327
335
  overflow: auto;
328
336
  }
337
+ .assistant-text .md-html-preview { overflow: hidden; }
329
338
  .assistant-text .md-fence-bar {
330
339
  display: flex;
331
340
  align-items: center;
@@ -336,14 +345,114 @@ textarea:focus-visible {
336
345
  color: var(--muted);
337
346
  font-size: 0.75rem;
338
347
  }
348
+ .assistant-text .md-fence-tabs {
349
+ display: inline-flex;
350
+ align-items: center;
351
+ gap: 2px;
352
+ margin-left: auto;
353
+ margin-right: 4px;
354
+ }
355
+ .assistant-text .md-fence-tab {
356
+ margin: 0;
357
+ min-height: var(--tap);
358
+ padding: 0 10px;
359
+ border: 0;
360
+ border-radius: 8px;
361
+ background: transparent;
362
+ color: var(--muted);
363
+ font: inherit;
364
+ font-size: 0.78rem;
365
+ cursor: pointer;
366
+ }
367
+ .assistant-text .md-fence-tab.on,
368
+ .assistant-text .md-fence-tab:hover { background: var(--lift); color: var(--text); }
369
+ .assistant-text .md-fence-acts {
370
+ display: flex;
371
+ align-items: center;
372
+ gap: 2px;
373
+ }
374
+ .assistant-text .md-fence-btn {
375
+ margin: 0;
376
+ width: var(--tap);
377
+ height: var(--tap);
378
+ padding: 0;
379
+ border: 0;
380
+ border-radius: 8px;
381
+ background: transparent;
382
+ color: var(--muted);
383
+ cursor: pointer;
384
+ display: inline-flex;
385
+ align-items: center;
386
+ justify-content: center;
387
+ }
388
+ .assistant-text .md-html-frame {
389
+ display: block;
390
+ width: 100%;
391
+ min-height: 220px;
392
+ height: 52vh;
393
+ border: 0;
394
+ background: transparent;
395
+ color-scheme: dark;
396
+ border-radius: 0 0 12px 12px;
397
+ }
398
+ .assistant-text .md-html-preview[data-view="code"] .md-html-frame { display: none; }
399
+ .assistant-text .md-html-preview[data-view="preview"] .md-pre { display: none; }
339
400
  .assistant-text .md-pre {
340
401
  margin: 0;
341
402
  padding: 0.6rem 0.8rem 0.8rem;
342
403
  overflow: auto;
343
404
  font-size: 0.82rem;
405
+ white-space: pre;
406
+ overflow-wrap: normal;
407
+ word-break: normal;
344
408
  }
345
- .assistant-text img.md-img {
409
+ .html-zoom {
410
+ position: fixed;
411
+ inset: 0;
412
+ z-index: 80;
413
+ display: grid;
414
+ grid-template-rows: auto 1fr;
415
+ padding: max(12px, env(safe-area-inset-top)) 12px max(12px, env(safe-area-inset-bottom));
416
+ background: rgba(0, 0, 0, 0.72);
417
+ }
418
+ .html-zoom[hidden] { display: none !important; }
419
+ .html-zoom-bar {
420
+ display: flex;
421
+ align-items: center;
422
+ justify-content: space-between;
423
+ gap: 12px;
424
+ margin: 0 0 12px;
425
+ color: var(--text);
426
+ }
427
+ .html-zoom-close {
428
+ margin: 0;
429
+ width: var(--tap);
430
+ height: var(--tap);
431
+ border: 0;
432
+ border-radius: 50%;
433
+ background: var(--fill);
434
+ color: var(--text);
435
+ font-size: 1.35rem;
436
+ cursor: pointer;
437
+ }
438
+ .html-zoom-frame {
439
+ width: 100%;
440
+ height: 100%;
441
+ border: 0;
442
+ border-radius: 16px;
443
+ background: #fff;
444
+ }
445
+ .assistant-text .md-img-link {
446
+ display: inline-block;
346
447
  max-width: 100%;
448
+ line-height: 0;
449
+ }
450
+ .assistant-text img.md-img {
451
+ max-width: min(100%, 22rem);
452
+ max-height: min(16rem, 40vh);
453
+ width: auto;
454
+ height: auto;
455
+ object-fit: contain;
347
456
  border-radius: 10px;
348
457
  }
349
458
  .turn-live {
@@ -416,6 +525,8 @@ textarea:focus-visible {
416
525
  margin-top: 4px;
417
526
  }
418
527
  .turn-stop,
528
+ .turn-pause,
529
+ .turn-continue,
419
530
  .turn-steer,
420
531
  .steer,
421
532
  .send,
@@ -447,13 +558,16 @@ textarea:focus-visible {
447
558
  background: var(--lift);
448
559
  border-color: color-mix(in srgb, var(--danger) 45%, var(--line));
449
560
  }
450
- /* Steer rides the steel rail, never a filled danger slab. */
561
+ .turn-pause,
562
+ .turn-continue,
451
563
  .turn-steer,
452
564
  .steer {
453
565
  color: var(--steel);
454
566
  background: var(--fill);
455
567
  border-color: color-mix(in srgb, var(--steel) 38%, var(--line));
456
568
  }
569
+ .turn-pause:hover,
570
+ .turn-continue:hover,
457
571
  .turn-steer:hover,
458
572
  .steer:hover {
459
573
  background: var(--lift);
@@ -9,7 +9,7 @@
9
9
  <link rel="icon" href="/favicon-32.png" type="image/png" sizes="32x32" />
10
10
  <link rel="icon" href="/favicon-16.png" type="image/png" sizes="16x16" />
11
11
  <title>Guild — 外出</title>
12
- <link rel="stylesheet" href="/mobile.css?v=enamel" />
12
+ <link rel="stylesheet" href="/mobile.css?v=html-frame" />
13
13
  <script src="/i18n.js"></script>
14
14
  <script src="/md.js"></script>
15
15
  </head>
@@ -49,6 +49,13 @@
49
49
  </form>
50
50
  </section>
51
51
  <div class="flash" id="flash" role="status" aria-live="polite" hidden></div>
52
+ <div class="html-zoom" id="html-zoom" hidden>
53
+ <div class="html-zoom-bar">
54
+ <strong data-i18n="html.zoom">HTML 預覽</strong>
55
+ <button type="button" class="html-zoom-close" id="html-zoom-close" title="關閉" aria-label="關閉" data-i18n-title="close" data-i18n-aria="close">×</button>
56
+ </div>
57
+ <iframe class="html-zoom-frame" id="html-zoom-frame" sandbox="allow-scripts" title="HTML preview large"></iframe>
58
+ </div>
52
59
  <script>
53
60
  (function () {
54
61
  const COLORS = ["#7c6af7", "#5b8def", "#2ea887", "#e07a3d", "#d4537e"];
@@ -141,6 +148,20 @@
141
148
  ? "/dms/" + encodeURIComponent(r.id) + "/abort"
142
149
  : "/channels/" + encodeURIComponent(r.id) + "/abort";
143
150
  }
151
+ function pauseUrl() {
152
+ const r = currentRoomRef();
153
+ if (!r) return "";
154
+ return r.kind === "dm"
155
+ ? "/dms/" + encodeURIComponent(r.id) + "/pause"
156
+ : "/channels/" + encodeURIComponent(r.id) + "/pause";
157
+ }
158
+ function continueUrl() {
159
+ const r = currentRoomRef();
160
+ if (!r) return "";
161
+ return r.kind === "dm"
162
+ ? "/dms/" + encodeURIComponent(r.id) + "/continue"
163
+ : "/channels/" + encodeURIComponent(r.id) + "/continue";
164
+ }
144
165
  function steerUrl() {
145
166
  const r = currentRoomRef();
146
167
  if (!r) return "";
@@ -225,6 +246,7 @@
225
246
  thinking: row.thinking || "",
226
247
  steps: Array.isArray(row.steps) ? row.steps.slice(-5) : [],
227
248
  startedAt: row.startedAt || "",
249
+ paused: Boolean(row.paused),
228
250
  }));
229
251
  }
230
252
  function isBusy() {
@@ -342,10 +364,11 @@
342
364
  }
343
365
  function formatBody(text) {
344
366
  const cleaned = visibleAssistantText(text);
345
- if (cleaned.length > 8000) return escapeHtml(cleaned.slice(-8000));
367
+ const clipped =
368
+ cleaned.length > 48000 ? cleaned.slice(0, 48000) + "\n…" : cleaned;
346
369
  return typeof renderMarkdown === "function"
347
- ? renderMarkdown(cleaned)
348
- : escapeHtml(cleaned);
370
+ ? renderMarkdown(clipped)
371
+ : escapeHtml(clipped);
349
372
  }
350
373
  function liveStepLabel(name) {
351
374
  if (name === "think") return t("think");
@@ -409,19 +432,35 @@
409
432
  '</span><span class="handle">@' +
410
433
  escapeHtml(bot.handle) +
411
434
  '</span></div><div class="turn-live"><div class="turn-status">' +
412
- t("deepDiving") +
435
+ (live.paused ? t("live.paused") : t("deepDiving")) +
413
436
  clock +
414
437
  "</div>" +
415
438
  liveStepsHtml(live.steps) +
416
- '<div class="turn-actions"><button type="button" class="turn-stop" data-live-stop="' +
439
+ '<div class="turn-actions">' +
440
+ (live.paused
441
+ ? '<button type="button" class="turn-continue" data-live-continue="' +
442
+ escapeHtml(bot.id) +
443
+ '">' +
444
+ t("live.continue") +
445
+ "</button>"
446
+ : '<button type="button" class="turn-pause" data-live-pause="' +
447
+ escapeHtml(bot.id) +
448
+ '">' +
449
+ t("live.pause") +
450
+ "</button>") +
451
+ '<button type="button" class="turn-stop" data-live-stop="' +
417
452
  escapeHtml(bot.id) +
418
453
  '">' +
419
454
  t("live.stop") +
420
- '</button><button type="button" class="turn-steer" data-live-steer="' +
421
- escapeHtml(bot.id) +
422
- '">' +
423
- t("live.steer") +
424
- "</button></div></div></div></article>"
455
+ "</button>" +
456
+ (live.paused
457
+ ? ""
458
+ : '<button type="button" class="turn-steer" data-live-steer="' +
459
+ escapeHtml(bot.id) +
460
+ '">' +
461
+ t("live.steer") +
462
+ "</button>") +
463
+ "</div></div></div></article>"
425
464
  );
426
465
  })
427
466
  .join("");
@@ -445,6 +484,8 @@
445
484
  return (
446
485
  '<article class="msg' +
447
486
  (you ? " you" : " bot") +
487
+ '" data-id="' +
488
+ escapeHtml(msg.id || "") +
448
489
  '"><span class="avatar" style="' +
449
490
  avStyle +
450
491
  '">' +
@@ -481,9 +522,28 @@
481
522
  escapeHtml(t("mobile.recent", { n: cap })) +
482
523
  "</div>"
483
524
  : "";
525
+ const keptHtml =
526
+ typeof holdHtmlFrames === "function" ? holdHtmlFrames(root) : [];
484
527
  root.innerHTML = notice + rows.map(msgHtml).join("") + turnStatusHtml();
528
+ if (typeof putHtmlFrames === "function") putHtmlFrames(root, keptHtml);
529
+ if (typeof hydrateHtmlPreviews === "function") {
530
+ hydrateHtmlPreviews(root);
531
+ }
485
532
  if (pin) root.scrollTop = root.scrollHeight;
486
533
  }
534
+ function openHtmlZoom(raw) {
535
+ const wrap = document.getElementById("html-zoom");
536
+ const frame = document.getElementById("html-zoom-frame");
537
+ if (!wrap || !frame) return;
538
+ frame.srcdoc = raw || "";
539
+ wrap.hidden = false;
540
+ }
541
+ function closeHtmlZoom() {
542
+ const wrap = document.getElementById("html-zoom");
543
+ const frame = document.getElementById("html-zoom-frame");
544
+ if (frame) frame.srcdoc = "";
545
+ if (wrap) wrap.hidden = true;
546
+ }
487
547
  function steerModLabel() {
488
548
  return /Mac|iPhone|iPad/.test(navigator.platform || "") ? "\u2318" : "Ctrl+";
489
549
  }
@@ -607,12 +667,25 @@
607
667
  renderList();
608
668
  if (state.kind) renderHead();
609
669
  }
670
+ function sameMessages(next) {
671
+ const prev = state.messages || [];
672
+ if (prev.length !== next.length) return false;
673
+ return next.every(
674
+ (msg, i) =>
675
+ prev[i].id === msg.id &&
676
+ prev[i].body === msg.body &&
677
+ Boolean(prev[i].finishedAt) === Boolean(msg.finishedAt),
678
+ );
679
+ }
610
680
  async function loadMessages(opts) {
611
681
  if (!state.kind) return;
612
682
  const res = await fetch(roomMessagesUrl());
613
683
  const body = await res.json().catch(() => []);
614
684
  if (!res.ok) throw new Error(body.error || "load failed");
615
- state.messages = Array.isArray(body) ? body : [];
685
+ const incoming = Array.isArray(body) ? body : [];
686
+ const unchanged = sameMessages(incoming);
687
+ state.messages = incoming;
688
+ if (opts && opts.merge && unchanged) return;
616
689
  renderThread({ pinBottom: !(opts && opts.merge) });
617
690
  }
618
691
  async function pollLive() {
@@ -629,10 +702,10 @@
629
702
  state.lives = lives;
630
703
  if (!lives.length && !state.posting) {
631
704
  if (was) await loadMessages({ merge: true }).catch(() => {});
632
- else renderThread();
705
+ } else if (now !== was) {
706
+ await loadMessages({ merge: true }).catch(() => {});
633
707
  } else {
634
- if (now !== was) await loadMessages({ merge: true }).catch(() => {});
635
- else renderThread();
708
+ renderThread();
636
709
  }
637
710
  syncComposer();
638
711
  } catch {
@@ -771,6 +844,48 @@
771
844
  renderThread();
772
845
  await loadMessages().catch(() => {});
773
846
  }
847
+ async function pauseTurn(botId) {
848
+ try {
849
+ await fetch(pauseUrl(), {
850
+ method: "POST",
851
+ headers: { "content-type": "application/json" },
852
+ body: JSON.stringify(botId ? { botId: botId } : {}),
853
+ });
854
+ } catch {
855
+ /* ignore */
856
+ }
857
+ (state.lives || []).forEach((row) => {
858
+ if (!botId || row.botId === botId) row.paused = true;
859
+ });
860
+ renderThread();
861
+ await pollLive().catch(() => {});
862
+ }
863
+ async function continueTurn(botId) {
864
+ (state.lives || []).forEach((row) => {
865
+ if (row.botId === botId) row.paused = false;
866
+ });
867
+ state.posting = true;
868
+ syncComposer();
869
+ renderThread();
870
+ try {
871
+ const res = await fetch(continueUrl(), {
872
+ method: "POST",
873
+ headers: { "content-type": "application/json" },
874
+ body: JSON.stringify({ botId: botId }),
875
+ });
876
+ const body = await res.json().catch(() => ({}));
877
+ if (!res.ok) throw new Error(body.error || "continue failed");
878
+ await loadMessages();
879
+ await pollLive();
880
+ } catch (err) {
881
+ toast(err.message || String(err));
882
+ await pollLive().catch(() => {});
883
+ } finally {
884
+ state.posting = false;
885
+ syncComposer();
886
+ schedule();
887
+ }
888
+ }
774
889
  async function sendSteer(raw, botId) {
775
890
  const text = String(raw || "").trim();
776
891
  if (!text || !state.kind) return;
@@ -886,13 +1001,68 @@
886
1001
  document.getElementById("steer").addEventListener("click", () => {
887
1002
  steerDraft().catch((err) => toast(err.message || String(err)));
888
1003
  });
1004
+ document.getElementById("html-zoom-close").addEventListener("click", () => {
1005
+ closeHtmlZoom();
1006
+ });
1007
+ document.getElementById("html-zoom").addEventListener("click", (event) => {
1008
+ if (event.target.id === "html-zoom") closeHtmlZoom();
1009
+ });
889
1010
  document.getElementById("thread").addEventListener("click", (event) => {
1011
+ const htmlExpand = event.target.closest("[data-html-expand]");
1012
+ if (htmlExpand) {
1013
+ event.preventDefault();
1014
+ const box = htmlExpand.closest(".md-html-preview");
1015
+ const src = box && box.querySelector(".md-html-src");
1016
+ if (src) openHtmlZoom(src.value);
1017
+ return;
1018
+ }
1019
+ const htmlView = event.target.closest("[data-html-view]");
1020
+ if (htmlView) {
1021
+ event.preventDefault();
1022
+ const box = htmlView.closest(".md-html-preview");
1023
+ if (!box) return;
1024
+ const view = htmlView.getAttribute("data-html-view");
1025
+ box.setAttribute("data-view", view);
1026
+ const frame = box.querySelector(".md-html-frame");
1027
+ const pre = box.querySelector(".md-pre");
1028
+ if (frame) frame.hidden = view !== "preview";
1029
+ if (pre) pre.hidden = view !== "code";
1030
+ box.querySelectorAll("[data-html-view]").forEach((tab) => {
1031
+ tab.classList.toggle("on", tab.getAttribute("data-html-view") === view);
1032
+ });
1033
+ return;
1034
+ }
1035
+ const fenceCopy = event.target.closest("[data-fence-copy]");
1036
+ if (fenceCopy) {
1037
+ event.preventDefault();
1038
+ const box = fenceCopy.closest(".md-fence");
1039
+ const src = box && (box.querySelector(".md-html-src") || box.querySelector("code"));
1040
+ const text = src && "value" in src ? src.value : src ? src.textContent : "";
1041
+ if (text && navigator.clipboard) {
1042
+ navigator.clipboard.writeText(text).catch(() => {});
1043
+ }
1044
+ return;
1045
+ }
890
1046
  const stop = event.target.closest("[data-live-stop]");
891
1047
  if (stop) {
892
1048
  event.preventDefault();
893
1049
  stopTurn(stop.getAttribute("data-live-stop"));
894
1050
  return;
895
1051
  }
1052
+ const pause = event.target.closest("[data-live-pause]");
1053
+ if (pause) {
1054
+ event.preventDefault();
1055
+ pauseTurn(pause.getAttribute("data-live-pause"));
1056
+ return;
1057
+ }
1058
+ const cont = event.target.closest("[data-live-continue]");
1059
+ if (cont) {
1060
+ event.preventDefault();
1061
+ continueTurn(cont.getAttribute("data-live-continue")).catch((err) =>
1062
+ toast(err.message || String(err)),
1063
+ );
1064
+ return;
1065
+ }
896
1066
  const steer = event.target.closest("[data-live-steer]");
897
1067
  if (!steer) return;
898
1068
  event.preventDefault();