shadok-ai 0.15.50 → 0.15.52

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "shadok-ai",
3
- "version": "0.15.50",
3
+ "version": "0.15.52",
4
4
  "main": "dist/session.js",
5
5
  "scripts": {
6
6
  "test": "node --import tsx --test --test-timeout=120000 test/*.ts context/*/test/*.test.mjs",
@@ -24,3 +24,27 @@ export function echoAuthor(msg) {
24
24
  // from another of your tabs; a single cockpit has one human.
25
25
  return place || "you";
26
26
  }
27
+
28
+ /**
29
+ * Is this prompt the viewer's OWN? Decides the side of the bubble: yours on the
30
+ * right, anyone else's — a teammate on the web, a Telegram sender, a cron, the
31
+ * auto-retry — on the left, still tinted as a human message (only the agent is
32
+ * white).
33
+ *
34
+ * Only a web prompt can be yours: another platform is another person, or nobody.
35
+ * Without a name it is a prompt from this browser or a cockpit with no accounts
36
+ * — yours. While `me` is not known yet (`/me` still in flight), a named web
37
+ * prompt is taken as yours too: guessing "someone else" would flash your own
38
+ * history to the left on every reload.
39
+ *
40
+ * @param {{from?: string, origin?: string, auto?: boolean}} msg
41
+ * @param {string | null | undefined} me the signed-in account's name, if known
42
+ */
43
+ export function isOwnPrompt(msg, me) {
44
+ if (msg?.auto) return false;
45
+ const origin = (msg?.origin ?? "").trim();
46
+ if (origin && origin !== "web") return false;
47
+ const from = (msg?.from ?? "").trim();
48
+ if (!from || !me) return true;
49
+ return from === me;
50
+ }
package/public/index.html CHANGED
@@ -187,7 +187,36 @@
187
187
  background: var(--amber); color: var(--on-accent);
188
188
  display: grid; place-items: center; margin-right: 4px;
189
189
  }
190
- header .brand-logo svg { width: 19px; height: 19px; }
190
+ header .brand-logo svg { width: 22px; height: 22px; overflow: visible; }
191
+ /* The mark is the Shadok itself (same anatomy as the favicon's `shadokInner`),
192
+ alive: it bobs, its antennae sway, it blinks and glances around, and waves
193
+ now and then. Pure CSS on static markup, so it costs no JS timer and needs
194
+ no bridge (invariant 10). Hover makes it hop. Every part rotates/scales
195
+ about its OWN box (`transform-box: fill-box`), never the SVG's origin. */
196
+ header .brand-logo svg * { transform-box: fill-box; }
197
+ header .brand-logo .sk-pupils { fill: var(--amber); animation: sk-glance 7s ease-in-out infinite; }
198
+ header .brand-logo .sk-bob { animation: sk-bob 2.4s ease-in-out infinite; }
199
+ header .brand-logo .sk-ant { animation: sk-sway 1.6s ease-in-out infinite alternate; }
200
+ header .brand-logo .sk-ant-l { transform-origin: 100% 100%; }
201
+ header .brand-logo .sk-ant-r { transform-origin: 0% 100%; animation-delay: -0.8s; }
202
+ header .brand-logo .sk-eyes { transform-origin: 50% 50%; animation: sk-blink 4.6s infinite; }
203
+ header .brand-logo .sk-arm { transform-origin: 0% 0%; animation: sk-wave 9s ease-in-out infinite; }
204
+ header .brand:hover .brand-logo svg { animation: sk-hop 0.6s ease-out; }
205
+ @keyframes sk-bob { 0%, 100% { transform: translateY(0); } 50% { transform: translateY(0.9px); } }
206
+ @keyframes sk-sway { from { transform: rotate(-14deg); } to { transform: rotate(12deg); } }
207
+ @keyframes sk-blink { 0%, 90%, 96%, 100% { transform: scaleY(1); } 93% { transform: scaleY(0.12); } }
208
+ @keyframes sk-glance {
209
+ 0%, 20% { transform: translate(0, 0); } 28%, 45% { transform: translate(-0.8px, 0.2px); }
210
+ 53%, 70% { transform: translate(0.6px, -0.3px); } 78%, 100% { transform: translate(0, 0); }
211
+ }
212
+ @keyframes sk-wave {
213
+ 0%, 70%, 100% { transform: rotate(0); } 74% { transform: rotate(-70deg); }
214
+ 78% { transform: rotate(-35deg); } 82% { transform: rotate(-70deg); } 86% { transform: rotate(0); }
215
+ }
216
+ @keyframes sk-hop { 0%, 100% { transform: translateY(0); } 40% { transform: translateY(-4px); } 70% { transform: translateY(0.5px); } }
217
+ @media (prefers-reduced-motion: reduce) {
218
+ header .brand-logo svg, header .brand-logo svg * { animation: none !important; }
219
+ }
191
220
  /* The brand name doubles as the cockpit's editable title: click to rename,
192
221
  Enter/blur commits, Escape cancels. A dashed outline + pencil on hover make
193
222
  the affordance obvious; the outline turns solid amber while editing. */
@@ -1128,6 +1157,11 @@
1128
1157
  between this bubble and the one before, so it keeps its own label — a
1129
1158
  wordless boundary the reader would otherwise never see. */
1130
1159
  .turn.user { align-self: flex-end; align-items: flex-end; }
1160
+ /* Someone else's prompt (a teammate, a Telegram sender, a cron): on the
1161
+ agent's side, still a human bubble — same tint as yours, only the agent is
1162
+ white. The side and the name say who; a colour per person read as noise. */
1163
+ .turn.user.other { align-self: flex-start; align-items: flex-start; }
1164
+ .turn.user + .turn.user.new-speaker > .label { display: block; }
1131
1165
  .turn.claude { align-self: flex-start; }
1132
1166
  .bubble {
1133
1167
  padding: 9px 14px;
@@ -2283,7 +2317,7 @@
2283
2317
  <body>
2284
2318
 
2285
2319
  <header>
2286
- <span class="brand"><span class="brand-logo" aria-hidden="true"><svg viewBox="0 0 40 40"><g fill="none" stroke="currentColor" stroke-width="2.8" stroke-linecap="round" stroke-linejoin="round"><ellipse cx="17" cy="17" rx="10" ry="8.5"/><path d="M25 12.5c3-3 7.5-3 10-1.2-2.8.4-5 1.9-6.6 4"/><path d="M14 25.3l-2.4 10M20 25.3l1.6 10"/></g></svg></span><span class="brand-name" id="brandName" title="Rename this cockpit">shadok-ai</span><span class="brand-edit" id="brandEdit" title="Rename this cockpit" aria-hidden="true">✎</span> <span class="ver" id="verView" title="Running version"></span><span class="ver-chan" id="verChan" aria-hidden="true"></span></span>
2320
+ <span class="brand"><span class="brand-logo" aria-hidden="true"><svg viewBox="0 0 32 32"><g fill="currentColor" stroke="currentColor" stroke-width="1.3" stroke-linecap="round"><g class="sk-leg sk-leg-l"><line x1="13" y1="23" x2="12.6" y2="30.6"/><line x1="11" y1="30.7" x2="14.1" y2="30.7"/></g><g class="sk-leg sk-leg-r"><line x1="18.6" y1="23" x2="19" y2="30.6"/><line x1="17.6" y1="30.7" x2="20.7" y2="30.7"/></g><g class="sk-bob"><line class="sk-arm" x1="21.6" y1="17.2" x2="26" y2="20.4"/><g class="sk-ant sk-ant-l" stroke-width="0.9"><line x1="13.8" y1="5.4" x2="11.8" y2="2.8"/><circle cx="11.8" cy="2.8" r="1" stroke="none"/></g><g class="sk-ant sk-ant-r" stroke-width="0.9"><line x1="18.2" y1="5.4" x2="20.2" y2="2.8"/><circle cx="20.2" cy="2.8" r="1" stroke="none"/></g><circle cx="16" cy="16.2" r="8.4" stroke="none"/><path d="M14.4 18.2 L17.6 18.2 L16 21.6 Z" stroke="none"/><g class="sk-eyes" stroke="none"><circle cx="13.1" cy="7.8" r="2.6" fill="#fff"/><circle cx="18.9" cy="7.8" r="2.6" fill="#fff"/><g class="sk-pupils"><circle cx="13.4" cy="8.1" r="1.3"/><circle cx="19.2" cy="8.1" r="1.3"/></g></g></g></g></svg></span><span class="brand-name" id="brandName" title="Rename this cockpit">shadok-ai</span><span class="brand-edit" id="brandEdit" title="Rename this cockpit" aria-hidden="true">✎</span> <span class="ver" id="verView" title="Running version"></span><span class="ver-chan" id="verChan" aria-hidden="true"></span></span>
2287
2321
  <span class="spacer"></span>
2288
2322
  <div class="gauge quota" id="quota5h" data-window="5h" title="5h rolling limit">
2289
2323
  <svg class="dial" viewBox="0 0 56 44" role="img" aria-label="5h rolling limit">
@@ -3047,7 +3081,7 @@
3047
3081
  hasReadonlyPreset,
3048
3082
  applyReadonlyPreset, isManagedProfile, profileSaveBody, secretUsers } from "/profile-card.js?v=__ASSET_V__";
3049
3083
  import { notifyState, BLINK_MS } from "/notify.js?v=__ASSET_V__";
3050
- import { echoAuthor } from "/echo-author.js?v=__ASSET_V__";
3084
+ import { echoAuthor, isOwnPrompt } from "/echo-author.js?v=__ASSET_V__";
3051
3085
  import { dialPos, dialColor, arcSegments, dialTitle, SWEEP_DEG } from "/gauge-dial.js?v=__ASSET_V__";
3052
3086
  import { TOUR_STEPS, bubblePlacement, unionRect, visibleSteps } from "/tour-steps.js?v=__ASSET_V__";
3053
3087
  import { pickChannelSource, dirKey, serverSwapped } from "/channel-store.js?v=__ASSET_V__";
@@ -3105,6 +3139,7 @@
3105
3139
  window.hasReadonlyPreset = hasReadonlyPreset;
3106
3140
  window.applyReadonlyPreset = applyReadonlyPreset;
3107
3141
  window.echoAuthor = echoAuthor;
3142
+ window.isOwnPrompt = isOwnPrompt;
3108
3143
  window.notifyState = notifyState;
3109
3144
  window.BLINK_MS = BLINK_MS;
3110
3145
  window.dialPos = dialPos;
@@ -3154,6 +3189,7 @@
3154
3189
  extractLiveText: () => "",
3155
3190
  livePreviewDecision: () => null,
3156
3191
  echoAuthor: () => "",
3192
+ isOwnPrompt: () => true,
3157
3193
  notifyState: () => ({ color: null, badge: "", blink: false }),
3158
3194
  dialPos: () => 0,
3159
3195
  dialColor: () => "",
@@ -5920,6 +5956,32 @@
5920
5956
  return window.echoAuthor ? window.echoAuthor(msg) : "you";
5921
5957
  }
5922
5958
 
5959
+ /** Which side a human prompt sits on: yours on the right, anyone else's on
5960
+ * the left — still a tinted human bubble, only the agent is white. The author
5961
+ * is kept on the turn so the side can be re-decided once `/me` answers
5962
+ * (`resideAllPrompts`). */
5963
+ function markPromptSide(bubble, msg) {
5964
+ const turn = bubble.parentElement;
5965
+ turn.dataset.from = msg?.from ?? "";
5966
+ turn.dataset.origin = msg?.origin ?? "";
5967
+ if (msg?.auto) turn.dataset.auto = "1";
5968
+ // A run of prompts shows one label — but only while it is ONE speaker.
5969
+ const prev = turn.previousElementSibling;
5970
+ const who = turn.querySelector(":scope > .label")?.firstChild?.textContent ?? "";
5971
+ const prevWho = prev?.classList.contains("user") ? prev.querySelector(":scope > .label")?.firstChild?.textContent ?? "" : who;
5972
+ turn.classList.toggle("new-speaker", who !== prevWho);
5973
+ applyPromptSide(turn);
5974
+ }
5975
+ function applyPromptSide(turn) {
5976
+ const msg = { from: turn.dataset.from, origin: turn.dataset.origin, auto: turn.dataset.auto === "1" };
5977
+ const own = window.isOwnPrompt ? window.isOwnPrompt(msg, me?.name) : true;
5978
+ turn.classList.toggle("other", !own);
5979
+ }
5980
+ function resideAllPrompts() {
5981
+ for (const turn of document.querySelectorAll(".turn.user[data-origin]")) applyPromptSide(turn);
5982
+ }
5983
+ window.addEventListener("shadok-bridge-ready", resideAllPrompts);
5984
+
5923
5985
  function addTurn(tab, role, who, text, extraClass, at) {
5924
5986
  // Time of the last message in the channel list, relative ("3m ago"). Every
5925
5987
  // message (replayed history or live) carries `at`; the last one wins.
@@ -7423,6 +7485,7 @@
7423
7485
  // SAME echoAuthor the live echo uses: "Ada · telegram"
7424
7486
  // for a Telegram message, "you" for your own web prompt.
7425
7487
  const b = addTurn(t, "user", echoAuthorLabel({ from: h.from, origin: h.origin }), h.text, "hist", h.at);
7488
+ markPromptSide(b, { from: h.from, origin: h.origin });
7426
7489
  stampMsgIds(t, b.parentElement, h.ids);
7427
7490
  } else {
7428
7491
  const b = addTurn(t, "claude", "claude", h.text, "hist", h.at);
@@ -7537,6 +7600,7 @@
7537
7600
  closeActivity(t);
7538
7601
  {
7539
7602
  const b = addTurn(t, "user", echoAuthorLabel(msg), msg.text, null, Date.now());
7603
+ markPromptSide(b, msg);
7540
7604
  awaitPromptId(b, msg.text);
7541
7605
  markUnreadBoundary(t, b.parentElement);
7542
7606
  }
@@ -7993,7 +8057,11 @@
7993
8057
  // Sending = going back to the bottom, whatever happens (even after scrolling
7994
8058
  // up): you want to see your message and the answer that follows.
7995
8059
  t.transcriptEl._stickBottom = true;
7996
- awaitPromptId(addTurn(t, "user", "you", text, null, Date.now()), text);
8060
+ {
8061
+ const b = addTurn(t, "user", "you", text, null, Date.now());
8062
+ markPromptSide(b, {});
8063
+ awaitPromptId(b, text);
8064
+ }
7997
8065
  startTurnTimer(t);
7998
8066
  setTabMood(t, "working");
7999
8067
  // Remember this channel's last sent prompt, so Escape (an interruption) can
@@ -9532,6 +9600,8 @@
9532
9600
  $("signOut").hidden = !named;
9533
9601
  $("whoSep").hidden = !named;
9534
9602
  if (named) $("whoAmI").textContent = "Signed in as " + me.name + " · " + me.role;
9603
+ // Prompts drawn before we knew who you are get their side now.
9604
+ resideAllPrompts();
9535
9605
  // Security: for a signed-in person (or the lone admin of a no-password
9536
9606
  // instance, who is everybody). The server answers 403 otherwise.
9537
9607
  $("securityBtn").hidden = !(me?.name || me?.role === "admin");