shadok-ai 0.12.5 → 0.12.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/public/index.html +91 -2
package/package.json
CHANGED
package/public/index.html
CHANGED
|
@@ -897,6 +897,22 @@
|
|
|
897
897
|
overflow-x: auto;
|
|
898
898
|
}
|
|
899
899
|
.bubble.md pre code { background: none; padding: 0; }
|
|
900
|
+
/* Copy on a fenced code block. The button sits on a WRAPPER around the <pre>,
|
|
901
|
+
not inside it: the <pre> scrolls sideways, and a button within would scroll
|
|
902
|
+
away. Always visible (a phone has no hover), small, top-right — the <pre>'s
|
|
903
|
+
own top padding keeps the first line clear of it. */
|
|
904
|
+
.bubble.md .md-code { position: relative; }
|
|
905
|
+
.bubble.md .md-code-copy {
|
|
906
|
+
position: absolute; top: 6px; right: 6px;
|
|
907
|
+
padding: 2px 7px; font-family: var(--mono); font-size: 10px; line-height: 1.4;
|
|
908
|
+
cursor: pointer; background: var(--bg-inset); border: 1px solid var(--line);
|
|
909
|
+
border-radius: 6px; color: var(--text-dim); opacity: 0.75;
|
|
910
|
+
transition: color 0.12s, border-color 0.12s, opacity 0.12s;
|
|
911
|
+
}
|
|
912
|
+
.bubble.md .md-code-copy:hover, .bubble.md .md-code-copy:focus-visible {
|
|
913
|
+
opacity: 1; color: var(--amber); border-color: var(--amber);
|
|
914
|
+
}
|
|
915
|
+
.bubble.md .md-code-copy.done { opacity: 1; color: var(--ok, #7ac77a); border-color: currentColor; }
|
|
900
916
|
/* The horizontal scroll for a wide table lives on this wrapper (added in JS
|
|
901
917
|
after sanitising), NOT on the <table>: `display:block` on a table breaks its
|
|
902
918
|
own layout — thead and tbody stop sharing column widths, so the header row
|
|
@@ -2259,7 +2275,12 @@
|
|
|
2259
2275
|
// every upload settles. `updateSendDisabled` re-derives the button state.
|
|
2260
2276
|
let pendingUploads = 0;
|
|
2261
2277
|
function updateSendDisabled() {
|
|
2262
|
-
|
|
2278
|
+
// Sending is also allowed while a turn is RUNNING (`busy`): the message then
|
|
2279
|
+
// interrupts the turn and is sent the moment it settles, mirroring Telegram
|
|
2280
|
+
// (see preemptTurn). A pending dialog still blocks — you answer it, you don't
|
|
2281
|
+
// free-type over it — and so does an in-flight upload.
|
|
2282
|
+
const canType = active && (active.status === "ready" || active.status === "busy");
|
|
2283
|
+
sendBtn.disabled = !canType || !!active.pendingChoices || pendingUploads > 0;
|
|
2263
2284
|
}
|
|
2264
2285
|
const machine = $("machine"), screenEl = $("screen");
|
|
2265
2286
|
|
|
@@ -4139,6 +4160,30 @@
|
|
|
4139
4160
|
t.replaceWith(wrap);
|
|
4140
4161
|
wrap.appendChild(t);
|
|
4141
4162
|
}
|
|
4163
|
+
// A copy button on every fenced code block — the thing you most often want
|
|
4164
|
+
// to lift out of an answer. It goes on a wrapper AROUND the <pre> (the
|
|
4165
|
+
// <pre> scrolls; a button inside would scroll off), and copies the code's
|
|
4166
|
+
// own text, not the whole message.
|
|
4167
|
+
for (const pre of bubble.querySelectorAll("pre")) {
|
|
4168
|
+
const code = pre.querySelector("code") || pre;
|
|
4169
|
+
const wrap = document.createElement("div");
|
|
4170
|
+
wrap.className = "md-code";
|
|
4171
|
+
pre.replaceWith(wrap);
|
|
4172
|
+
wrap.appendChild(pre);
|
|
4173
|
+
const btn = document.createElement("button");
|
|
4174
|
+
btn.type = "button";
|
|
4175
|
+
btn.className = "md-code-copy";
|
|
4176
|
+
btn.textContent = "⧉ copy";
|
|
4177
|
+
btn.title = "Copy this code";
|
|
4178
|
+
btn.addEventListener("click", async (e) => {
|
|
4179
|
+
e.stopPropagation();
|
|
4180
|
+
const ok = await copyText(code.textContent.replace(/\n$/, ""));
|
|
4181
|
+
btn.textContent = ok ? "✓ copied" : "⚠ failed";
|
|
4182
|
+
btn.classList.toggle("done", ok);
|
|
4183
|
+
setTimeout(() => { btn.textContent = "⧉ copy"; btn.classList.remove("done"); }, 1500);
|
|
4184
|
+
});
|
|
4185
|
+
wrap.appendChild(btn);
|
|
4186
|
+
}
|
|
4142
4187
|
} else {
|
|
4143
4188
|
bubble.textContent = text;
|
|
4144
4189
|
}
|
|
@@ -5194,6 +5239,16 @@
|
|
|
5194
5239
|
persistChannels();
|
|
5195
5240
|
}
|
|
5196
5241
|
stopTurnTimer(t);
|
|
5242
|
+
// A message was typed during this turn (a takeover): the turn has just
|
|
5243
|
+
// settled, so send it now. Mirrors the Telegram bridge's preempt.
|
|
5244
|
+
if (t.preempting) {
|
|
5245
|
+
const queued = t.preempting;
|
|
5246
|
+
t.preempting = null;
|
|
5247
|
+
clearTimeout(t.preemptTimer);
|
|
5248
|
+
setTabState(t, "ready", "ready");
|
|
5249
|
+
submitPrompt(t, queued);
|
|
5250
|
+
break;
|
|
5251
|
+
}
|
|
5197
5252
|
// "unread" only when the turn produced visible text. An empty turn (e.g.
|
|
5198
5253
|
// a cron concluding NOTHING TO SHOW) asks for no reading at all, neither
|
|
5199
5254
|
// on the tab nor on the favicon.
|
|
@@ -5406,6 +5461,36 @@
|
|
|
5406
5461
|
if (!$("setupOverlay").hidden) refreshLiveList();
|
|
5407
5462
|
}, 5000);
|
|
5408
5463
|
|
|
5464
|
+
// If the interrupt never lands (a wedged agent whose turn won't settle), don't
|
|
5465
|
+
// lose the message: put it back in the box and say so, rather than loop.
|
|
5466
|
+
const PREEMPT_TIMEOUT_MS = 8000;
|
|
5467
|
+
/**
|
|
5468
|
+
* Send a message WHILE a turn is running: interrupt the turn (Escape, exactly
|
|
5469
|
+
* the on-screen ⎋ button), stash the text, and submit it from the `turn-done`
|
|
5470
|
+
* that follows. This mirrors the Telegram bridge, which does the same on a
|
|
5471
|
+
* `busy` refusal — the web used to just disable Send instead.
|
|
5472
|
+
*
|
|
5473
|
+
* Nothing is rendered optimistically: the queued message appears as an
|
|
5474
|
+
* ordinary prompt once the interrupted turn has settled, so the transcript
|
|
5475
|
+
* stays in order (interrupted output, then your message, then its answer).
|
|
5476
|
+
*/
|
|
5477
|
+
function preemptTurn(t, text) {
|
|
5478
|
+
// Socket down: leave the text in the box, like submitPrompt's own guard.
|
|
5479
|
+
if (!sendTo(t, { type: "key", key: "escape" })) return;
|
|
5480
|
+
t.preempting = text;
|
|
5481
|
+
t.transcriptEl._stickBottom = true;
|
|
5482
|
+
if (t === active) { promptInput.value = ""; promptInput.style.height = "auto"; }
|
|
5483
|
+
dropDraft(t);
|
|
5484
|
+
setTabState(t, "busy", "interrupting…");
|
|
5485
|
+
clearTimeout(t.preemptTimer);
|
|
5486
|
+
t.preemptTimer = setTimeout(() => {
|
|
5487
|
+
if (t.preempting !== text) return; // already sent, or replaced by a newer one
|
|
5488
|
+
t.preempting = null;
|
|
5489
|
+
if (t === active && !promptInput.value) { promptInput.value = text; active.draft = text; }
|
|
5490
|
+
addTurn(t, "system", "", "Couldn't interrupt the running turn — your message is back in the box. Try again, or use ⎋.");
|
|
5491
|
+
}, PREEMPT_TIMEOUT_MS);
|
|
5492
|
+
}
|
|
5493
|
+
|
|
5409
5494
|
function submitPrompt(t, text, force) {
|
|
5410
5495
|
// Send BEFORE touching the UI. Everything below is optimistic — it shows the
|
|
5411
5496
|
// turn, marks the tab busy, clears the composer and drops the draft — so on
|
|
@@ -5505,7 +5590,11 @@
|
|
|
5505
5590
|
e.preventDefault();
|
|
5506
5591
|
const t = active;
|
|
5507
5592
|
const text = promptInput.value.trim();
|
|
5508
|
-
if (!text || t
|
|
5593
|
+
if (!text || !t || t.pendingChoices || pendingUploads > 0 || !t.ws) return;
|
|
5594
|
+
// Sending during a running turn is a takeover: interrupt it, then send the
|
|
5595
|
+
// message once the turn settles. Same behaviour as Telegram.
|
|
5596
|
+
if (t.status === "busy") return preemptTurn(t, text);
|
|
5597
|
+
if (t.status !== "ready") return;
|
|
5509
5598
|
// Checked here so nothing is optimistically rendered before the server
|
|
5510
5599
|
// refuses. The server re-checks anyway — it is the authority.
|
|
5511
5600
|
if (paceBlocked) return renderPaceConfirm(t, text, paceReason);
|