shadok-ai 0.15.53 → 0.15.54
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 +168 -4
package/package.json
CHANGED
package/public/index.html
CHANGED
|
@@ -2306,6 +2306,71 @@
|
|
|
2306
2306
|
#composer { background: transparent; padding: 8px 20px calc(16px + env(safe-area-inset-bottom, 0px)); }
|
|
2307
2307
|
#composer textarea { background: var(--bg-raised); border: 1px solid var(--line); border-bottom: 2px solid var(--amber); border-radius: 6px; }
|
|
2308
2308
|
#composer textarea:focus-visible { box-shadow: none; border-color: var(--line); border-bottom-color: var(--amber); }
|
|
2309
|
+
|
|
2310
|
+
/* ── Motion: switching views, and what loading looks like ──────────────
|
|
2311
|
+
Short on purpose (≤ 220ms, staggers capped): a transition that makes you
|
|
2312
|
+
wait for the list you just asked for is worse than none. Everything here
|
|
2313
|
+
is decoration — the content is in place from the first frame, the
|
|
2314
|
+
animation only eases it in — and all of it is off under reduced motion. */
|
|
2315
|
+
:root { --ease-out: cubic-bezier(.2, .8, .2, 1); }
|
|
2316
|
+
@keyframes view-in-down { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: none; } }
|
|
2317
|
+
@keyframes view-in-up { from { opacity: 0; transform: translateY(-8px); } to { opacity: 1; transform: none; } }
|
|
2318
|
+
@keyframes fade-in { from { opacity: 0; } to { opacity: 1; } }
|
|
2319
|
+
@keyframes chat-in { from { opacity: 0; transform: translateY(4px); } to { opacity: 1; transform: none; } }
|
|
2320
|
+
/* Picking a group: its agents slide in from the side the group sits on in
|
|
2321
|
+
the rail (below → up, above → down), one after the other. */
|
|
2322
|
+
#tabbar.view-in-down :is(#ungrouped, #groups) .tab { animation: view-in-down 200ms var(--ease-out) both; animation-delay: calc(var(--i, 0) * 22ms); }
|
|
2323
|
+
#tabbar.view-in-up :is(#ungrouped, #groups) .tab { animation: view-in-up 200ms var(--ease-out) both; animation-delay: calc(var(--i, 0) * 22ms); }
|
|
2324
|
+
#tabbar.view-in-down .side-label, #tabbar.view-in-up .side-label,
|
|
2325
|
+
#tabbar.view-in-down #viewEmpty, #tabbar.view-in-up #viewEmpty { animation: fade-in 160ms ease-out both; }
|
|
2326
|
+
/* Picking an agent: its conversation eases in instead of snapping. */
|
|
2327
|
+
.transcript.chat-in { animation: chat-in 170ms var(--ease-out) both; }
|
|
2328
|
+
/* The rail's selection is ONE pill that glides to the picked group, instead
|
|
2329
|
+
of a highlight that blinks from one row to the next. */
|
|
2330
|
+
#railList { position: relative; }
|
|
2331
|
+
#railList .rail-item { z-index: 1; transition: color 160ms ease; }
|
|
2332
|
+
#railList.has-pill .rail-item.active { background: transparent; border-color: transparent; }
|
|
2333
|
+
.rail-pill {
|
|
2334
|
+
position: absolute; left: 0; right: 0; top: 0; z-index: 0; pointer-events: none;
|
|
2335
|
+
background: var(--bg-raised); border: 1px solid var(--line); border-radius: 6px;
|
|
2336
|
+
box-shadow: 0 1px 3px rgba(0, 0, 0, .12);
|
|
2337
|
+
transition: transform 200ms var(--ease-out), height 200ms var(--ease-out);
|
|
2338
|
+
}
|
|
2339
|
+
.rail-pill.no-anim { transition: none; }
|
|
2340
|
+
|
|
2341
|
+
/* Skeletons: the SHAPE of what is coming, shimmering, in place of a bare
|
|
2342
|
+
"loading…" — the layout does not jump when the real content lands. */
|
|
2343
|
+
@keyframes skel-shimmer { from { background-position: 100% 0; } to { background-position: 0 0; } }
|
|
2344
|
+
.skel {
|
|
2345
|
+
display: block; border-radius: 6px; height: 12px;
|
|
2346
|
+
/* Fallback first: a browser without color-mix() drops the gradient below
|
|
2347
|
+
and would otherwise draw no skeleton at all. */
|
|
2348
|
+
background: linear-gradient(90deg, rgba(128,128,128,.13) 25%, rgba(128,128,128,.26) 50%, rgba(128,128,128,.13) 75%);
|
|
2349
|
+
background: linear-gradient(90deg,
|
|
2350
|
+
color-mix(in srgb, var(--text-dim) 12%, transparent) 25%,
|
|
2351
|
+
color-mix(in srgb, var(--text-dim) 24%, transparent) 50%,
|
|
2352
|
+
color-mix(in srgb, var(--text-dim) 12%, transparent) 75%);
|
|
2353
|
+
background-size: 300% 100%;
|
|
2354
|
+
animation: skel-shimmer 1.3s linear infinite;
|
|
2355
|
+
}
|
|
2356
|
+
.skel-chat { display: flex; flex-direction: column; gap: 18px; padding-top: 4px; animation: fade-in 200ms ease-out both; }
|
|
2357
|
+
.skel-turn { display: flex; flex-direction: column; gap: 7px; max-width: 70%; }
|
|
2358
|
+
.skel-turn.mine { align-self: flex-end; align-items: flex-end; width: 42%; }
|
|
2359
|
+
.skel-turn .skel-label { width: 64px; height: 9px; }
|
|
2360
|
+
.skel-turn .skel-bubble { width: 100%; height: auto; padding: 12px 14px; display: flex; flex-direction: column; gap: 8px; background: rgba(128,128,128,.07); background: color-mix(in srgb, var(--text-dim) 7%, transparent); animation: none; border-radius: 10px; }
|
|
2361
|
+
.skel-turn .skel-bubble .skel { height: 10px; }
|
|
2362
|
+
.skel-row { display: flex; flex-direction: column; gap: 7px; padding: 10px 8px; border-bottom: 1px solid var(--line); }
|
|
2363
|
+
.skel-row:last-child { border-bottom: none; }
|
|
2364
|
+
.skel-card { height: 92px; border-radius: 8px; }
|
|
2365
|
+
.skel-diff { display: flex; flex-direction: column; gap: 8px; padding: 4px 0; }
|
|
2366
|
+
.skel-diff .skel { height: 10px; }
|
|
2367
|
+
|
|
2368
|
+
@media (prefers-reduced-motion: reduce) {
|
|
2369
|
+
#tabbar.view-in-down .tab, #tabbar.view-in-up .tab, #tabbar .side-label, #tabbar #viewEmpty,
|
|
2370
|
+
.transcript.chat-in, .skel-chat { animation: none !important; }
|
|
2371
|
+
.rail-pill { transition: none; }
|
|
2372
|
+
.skel { animation: none; }
|
|
2373
|
+
}
|
|
2309
2374
|
</style>
|
|
2310
2375
|
<!-- Light/dark mode is a per-browser choice (localStorage). Set it BEFORE the
|
|
2311
2376
|
body paints — here in <head> — so a light-preferring viewer never sees a
|
|
@@ -2548,7 +2613,7 @@
|
|
|
2548
2613
|
<span class="label">Which agent?</span>
|
|
2549
2614
|
<button type="button" class="linkish" id="editProfilesLink" title="Create or edit agent profiles">✎ edit profiles</button>
|
|
2550
2615
|
</div>
|
|
2551
|
-
<div id="profileGrid" role="radiogroup" aria-label="Agent profile"></div>
|
|
2616
|
+
<div id="profileGrid" role="radiogroup" aria-label="Agent profile" aria-busy="true"><span class="skel skel-card"></span><span class="skel skel-card"></span><span class="skel skel-card"></span><span class="skel skel-card"></span></div>
|
|
2552
2617
|
<span class="check-hint" id="profileNaNote" hidden>Applies to new sessions only.</span>
|
|
2553
2618
|
</div>
|
|
2554
2619
|
<!-- The model this ONE agent runs on. Its default sends nothing at all,
|
|
@@ -3387,6 +3452,37 @@
|
|
|
3387
3452
|
if (label.textContent !== text) label.textContent = text;
|
|
3388
3453
|
}
|
|
3389
3454
|
|
|
3455
|
+
// ── Skeletons: the shape of what is loading ─────────────
|
|
3456
|
+
// Static markup only (no user data), so innerHTML is safe here.
|
|
3457
|
+
const skelLine = (w) => `<span class="skel" style="width:${w}%"></span>`;
|
|
3458
|
+
function skeletonHTML(kind, n = 3) {
|
|
3459
|
+
if (kind === "chat") {
|
|
3460
|
+
const turn = (mine, widths) =>
|
|
3461
|
+
`<div class="skel-turn${mine ? " mine" : ""}"><span class="skel skel-label"></span>` +
|
|
3462
|
+
`<div class="skel skel-bubble">${widths.map(skelLine).join("")}</div></div>`;
|
|
3463
|
+
return `<div class="skel-chat" aria-busy="true" aria-label="Loading the conversation">` +
|
|
3464
|
+
turn(true, [80]) + turn(false, [92, 78, 54]) + turn(true, [64]) + turn(false, [88, 60]) + `</div>`;
|
|
3465
|
+
}
|
|
3466
|
+
if (kind === "cards") return Array.from({ length: n }, () => `<span class="skel skel-card"></span>`).join("");
|
|
3467
|
+
if (kind === "diff") {
|
|
3468
|
+
const w = [38, 72, 64, 85, 30, 58, 77, 46, 69, 52];
|
|
3469
|
+
return `<div class="skel-diff" aria-busy="true">${w.map(skelLine).join("")}</div>`;
|
|
3470
|
+
}
|
|
3471
|
+
// "rows": a list of two-line entries (sessions, ledger).
|
|
3472
|
+
return Array.from({ length: n }, (_, i) =>
|
|
3473
|
+
`<div class="skel-row" aria-busy="true">${skelLine(30 + (i * 17) % 25)}${skelLine(60 + (i * 23) % 35)}</div>`).join("");
|
|
3474
|
+
}
|
|
3475
|
+
/** A tab waiting for its history shows a chat skeleton — only when it has
|
|
3476
|
+
* nothing at all yet (a reconnect keeps what is already on screen). */
|
|
3477
|
+
function showChatSkeleton(t) {
|
|
3478
|
+
if (t.transcriptEl.childElementCount) return;
|
|
3479
|
+
t.transcriptEl.innerHTML = skeletonHTML("chat");
|
|
3480
|
+
}
|
|
3481
|
+
function clearChatSkeleton(t) {
|
|
3482
|
+
const s = t.transcriptEl.querySelector(":scope > .skel-chat");
|
|
3483
|
+
if (s) s.remove();
|
|
3484
|
+
}
|
|
3485
|
+
|
|
3390
3486
|
function createTab(groupId, { focus = true } = {}) {
|
|
3391
3487
|
seq++;
|
|
3392
3488
|
const el = document.createElement("div");
|
|
@@ -4594,10 +4690,29 @@
|
|
|
4594
4690
|
const tabsInView = (v) =>
|
|
4595
4691
|
tabs.filter((t) => !t.btnEl.closest("#tweakSlot") && viewOfTab(t) === v);
|
|
4596
4692
|
|
|
4693
|
+
/** Eases the newly shown agents in, from the side the picked group sits on
|
|
4694
|
+
* in the rail. Purely cosmetic: the list is already correct underneath. */
|
|
4695
|
+
let viewAnimTimer = 0;
|
|
4696
|
+
function animateViewSwitch(from, to) {
|
|
4697
|
+
if (from === to || !railShown()) return;
|
|
4698
|
+
const order = [...railList.querySelectorAll(".rail-item")].map((i) => i.dataset.view);
|
|
4699
|
+
const dir = order.indexOf(to) >= order.indexOf(from) ? "view-in-down" : "view-in-up";
|
|
4700
|
+
const shown = [...tabbar.querySelectorAll("#ungrouped .tab, #groups .group.in-view .tab")];
|
|
4701
|
+
// Cap the stagger: past ~8 agents the tail would only make you wait.
|
|
4702
|
+
shown.forEach((el, i) => el.style.setProperty("--i", Math.min(i, 8)));
|
|
4703
|
+
tabbar.classList.remove("view-in-down", "view-in-up");
|
|
4704
|
+
void tabbar.offsetWidth; // restart the animation on a quick re-click
|
|
4705
|
+
tabbar.classList.add(dir);
|
|
4706
|
+
clearTimeout(viewAnimTimer);
|
|
4707
|
+
viewAnimTimer = setTimeout(() => tabbar.classList.remove("view-in-down", "view-in-up"), 420);
|
|
4708
|
+
}
|
|
4709
|
+
|
|
4597
4710
|
function setView(v, { follow = true } = {}) {
|
|
4711
|
+
const from = currentView();
|
|
4598
4712
|
groupView = String(v);
|
|
4599
4713
|
localStorage.setItem(VIEW_KEY(), groupView);
|
|
4600
4714
|
applyView();
|
|
4715
|
+
animateViewSwitch(from, currentView());
|
|
4601
4716
|
// Picking a group shows one of ITS agents: staying on an agent that is no
|
|
4602
4717
|
// longer in the list would leave the chat and the list disagreeing.
|
|
4603
4718
|
if (follow && active && !active.btnEl.closest("#tweakSlot") && viewOfTab(active) !== currentView()) {
|
|
@@ -4753,6 +4868,33 @@
|
|
|
4753
4868
|
return item;
|
|
4754
4869
|
}
|
|
4755
4870
|
|
|
4871
|
+
// The selection pill: one element that GLIDES to the active group (the rows
|
|
4872
|
+
// themselves are rebuilt on every change, so they cannot transition).
|
|
4873
|
+
const railPill = document.createElement("div");
|
|
4874
|
+
railPill.className = "rail-pill";
|
|
4875
|
+
railPill.setAttribute("aria-hidden", "true");
|
|
4876
|
+
let pillView = null;
|
|
4877
|
+
function placeRailPill({ animate = true } = {}) {
|
|
4878
|
+
const item = railList.querySelector(".rail-item.active");
|
|
4879
|
+
if (!item || !item.offsetHeight) {
|
|
4880
|
+
railList.classList.remove("has-pill");
|
|
4881
|
+
railPill.hidden = true;
|
|
4882
|
+
pillView = null;
|
|
4883
|
+
return;
|
|
4884
|
+
}
|
|
4885
|
+
// First placement (or a layout change) jumps; only a change of group glides.
|
|
4886
|
+
const glide = animate && pillView !== null && pillView !== item.dataset.view;
|
|
4887
|
+
railPill.classList.toggle("no-anim", !glide);
|
|
4888
|
+
railPill.hidden = false;
|
|
4889
|
+
railPill.style.height = item.offsetHeight + "px";
|
|
4890
|
+
railPill.style.transform = `translateY(${item.offsetTop}px)`;
|
|
4891
|
+
railList.classList.add("has-pill");
|
|
4892
|
+
pillView = item.dataset.view;
|
|
4893
|
+
}
|
|
4894
|
+
// Collapsing the rail, resizing the window, or a group renamed to a longer
|
|
4895
|
+
// name moves the rows: follow without gliding.
|
|
4896
|
+
if (window.ResizeObserver) new ResizeObserver(() => placeRailPill({ animate: false })).observe(railList);
|
|
4897
|
+
|
|
4756
4898
|
let railFrame = 0;
|
|
4757
4899
|
let railSig = "";
|
|
4758
4900
|
function renderRail() {
|
|
@@ -4781,8 +4923,9 @@
|
|
|
4781
4923
|
return;
|
|
4782
4924
|
}
|
|
4783
4925
|
railSig = sig;
|
|
4784
|
-
railList.replaceChildren(...entries.map(railItem));
|
|
4926
|
+
railList.replaceChildren(...entries.map(railItem), railPill);
|
|
4785
4927
|
}
|
|
4928
|
+
placeRailPill();
|
|
4786
4929
|
if (groupIconsMinted && !restoring) {
|
|
4787
4930
|
groupIconsMinted = false;
|
|
4788
4931
|
persistGroups();
|
|
@@ -5121,6 +5264,7 @@
|
|
|
5121
5264
|
}
|
|
5122
5265
|
|
|
5123
5266
|
function activate(tab) {
|
|
5267
|
+
const prev = active;
|
|
5124
5268
|
if (active && active !== tab) {
|
|
5125
5269
|
active.draft = promptInput.value;
|
|
5126
5270
|
persistDraft(active);
|
|
@@ -5132,10 +5276,19 @@
|
|
|
5132
5276
|
if (!restoring && tab.sessionId) localStorage.setItem("cp.activeSession", tab.sessionId);
|
|
5133
5277
|
// Viewing the channel clears its "unread response" highlight.
|
|
5134
5278
|
if (tab.btnEl.classList.contains("unread")) setTabMood(tab, null);
|
|
5279
|
+
const switched = !!prev && prev !== tab && !restoring;
|
|
5135
5280
|
tabs.forEach((t) => {
|
|
5136
5281
|
t.btnEl.classList.toggle("active", t === tab);
|
|
5137
5282
|
t.transcriptEl.hidden = t !== tab;
|
|
5138
5283
|
});
|
|
5284
|
+
// Ease the new conversation in rather than snapping (only on a real switch).
|
|
5285
|
+
if (switched) {
|
|
5286
|
+
const el = tab.transcriptEl;
|
|
5287
|
+
el.classList.remove("chat-in");
|
|
5288
|
+
void el.offsetWidth;
|
|
5289
|
+
el.classList.add("chat-in");
|
|
5290
|
+
el.addEventListener("animationend", () => el.classList.remove("chat-in"), { once: true });
|
|
5291
|
+
}
|
|
5139
5292
|
promptInput.value = tab.draft;
|
|
5140
5293
|
autosizePrompt();
|
|
5141
5294
|
renderAttachments(tab); // each tab keeps its own pending chips
|
|
@@ -5993,6 +6146,7 @@
|
|
|
5993
6146
|
window.addEventListener("shadok-bridge-ready", resideAllPrompts);
|
|
5994
6147
|
|
|
5995
6148
|
function addTurn(tab, role, who, text, extraClass, at) {
|
|
6149
|
+
clearChatSkeleton(tab);
|
|
5996
6150
|
// Time of the last message in the channel list, relative ("3m ago"). Every
|
|
5997
6151
|
// message (replayed history or live) carries `at`; the last one wins.
|
|
5998
6152
|
if (Number.isFinite(at)) {
|
|
@@ -6782,6 +6936,7 @@
|
|
|
6782
6936
|
// Reattach to the SAME session once we know its id; a brand-new tab still
|
|
6783
6937
|
// starts fresh (no sessionId yet).
|
|
6784
6938
|
const start = t.sessionId ? { ...t.launchMsg, resume: t.sessionId } : t.launchMsg;
|
|
6939
|
+
if (t.sessionId) showChatSkeleton(t); // a resume has history on its way
|
|
6785
6940
|
ws.send(JSON.stringify({ type: "start", origin: "web", ...start }));
|
|
6786
6941
|
});
|
|
6787
6942
|
ws.addEventListener("close", () => {
|
|
@@ -7465,6 +7620,9 @@
|
|
|
7465
7620
|
}
|
|
7466
7621
|
|
|
7467
7622
|
function handleMessage(t, msg) {
|
|
7623
|
+
// `history` comes BEFORE `ready` when there is one, so by `ready` (or any
|
|
7624
|
+
// ending) the skeleton has nothing left to stand in for.
|
|
7625
|
+
if (["ready", "error", "exited", "gone", "stopped"].includes(msg.type)) clearChatSkeleton(t);
|
|
7468
7626
|
switch (msg.type) {
|
|
7469
7627
|
case "version":
|
|
7470
7628
|
handleVersion(msg);
|
|
@@ -7899,7 +8057,7 @@
|
|
|
7899
8057
|
/** Lists the directory's resumable sessions; a click fills the id field. */
|
|
7900
8058
|
async function refreshSessionList() {
|
|
7901
8059
|
const listEl = $("sessionList");
|
|
7902
|
-
listEl.innerHTML =
|
|
8060
|
+
listEl.innerHTML = skeletonHTML("rows", 4);
|
|
7903
8061
|
let sessions = [];
|
|
7904
8062
|
try {
|
|
7905
8063
|
const cwd = $("cwdInput").value.trim();
|
|
@@ -8605,7 +8763,7 @@
|
|
|
8605
8763
|
if (!t || !t.sessionId) return;
|
|
8606
8764
|
$("diffBranch").textContent = t.branch || "working tree";
|
|
8607
8765
|
$("diffStatus").textContent = "loading…";
|
|
8608
|
-
$("diffBody").innerHTML = "";
|
|
8766
|
+
$("diffBody").innerHTML = skeletonHTML("diff");
|
|
8609
8767
|
try {
|
|
8610
8768
|
const d = await (await fetch("/diff?session=" + encodeURIComponent(t.sessionId))).json();
|
|
8611
8769
|
$("diffStatus").textContent = d.status || (d.diff ? "" : "no changes");
|
|
@@ -8961,6 +9119,11 @@
|
|
|
8961
9119
|
applyLedgerFilters();
|
|
8962
9120
|
});
|
|
8963
9121
|
$("ledgerBtn").addEventListener("click", async () => {
|
|
9122
|
+
// Open at once on a skeleton: the table can be hundreds of rows, and a
|
|
9123
|
+
// click that does nothing until it lands reads as a dead button.
|
|
9124
|
+
$("ledgerList").innerHTML = skeletonHTML("rows", 6);
|
|
9125
|
+
$("ledgerCount").textContent = "";
|
|
9126
|
+
$("ledgerOverlay").hidden = false;
|
|
8964
9127
|
try { ledgerAll = (await (await fetch("/ledger")).json()).entries || []; } catch { ledgerAll = []; }
|
|
8965
9128
|
// The option lists come from the module, so the control and the filter it
|
|
8966
9129
|
// drives cannot drift apart.
|
|
@@ -9237,6 +9400,7 @@
|
|
|
9237
9400
|
* keyboard) lives here only, in a single copy. */
|
|
9238
9401
|
function renderProfileCards(container, selected, onPick, failed) {
|
|
9239
9402
|
container.innerHTML = "";
|
|
9403
|
+
container.removeAttribute("aria-busy"); // the boot skeleton, if still there
|
|
9240
9404
|
if (failed) {
|
|
9241
9405
|
const note = document.createElement("div");
|
|
9242
9406
|
note.className = "grid-note";
|