shadok-ai 0.8.18 → 0.8.19

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.8.18",
3
+ "version": "0.8.19",
4
4
  "main": "dist/session.js",
5
5
  "scripts": {
6
6
  "test": "node --import tsx --test test/*.ts context/*/test/*.test.mjs",
package/public/index.html CHANGED
@@ -9,7 +9,9 @@
9
9
  fields (see the coarse-pointer query) fix it at the source instead. -->
10
10
  <meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
11
11
  <title>shadok-ai — cockpit</title>
12
- <link rel="icon" id="favicon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'%3E%3Crect width='32' height='32' rx='7' fill='%2314161d'/%3E%3Ctext x='16' y='25' font-family='monospace' font-size='26' font-weight='700' fill='%23f0a848' text-anchor='middle'%3E%E2%80%BA%3C/text%3E%3C/svg%3E">
12
+ <!-- The idle favicon: a resting Shadok. paint() swaps in the pumping / "!" / "?"
13
+ variants once the JS runs; this is only the pre-script icon. -->
14
+ <link rel="icon" id="favicon" href="data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%2032%2032%22%3E%3Crect%20width%3D%2232%22%20height%3D%2232%22%20rx%3D%227%22%20fill%3D%22%2314161d%22%2F%3E%3Cg%20fill%3D%22%23f0a848%22%3E%3Crect%20x%3D%2211.4%22%20y%3D%2221%22%20width%3D%221.7%22%20height%3D%229%22%20rx%3D%220.85%22%2F%3E%3Crect%20x%3D%2217.4%22%20y%3D%2221%22%20width%3D%221.7%22%20height%3D%229%22%20rx%3D%220.85%22%2F%3E%3Crect%20x%3D%229.2%22%20y%3D%2228.7%22%20width%3D%224.6%22%20height%3D%221.7%22%20rx%3D%220.85%22%2F%3E%3Crect%20x%3D%2216.4%22%20y%3D%2228.7%22%20width%3D%224.6%22%20height%3D%221.7%22%20rx%3D%220.85%22%2F%3E%3C%2Fg%3E%3Cg%20fill%3D%22%23f0a848%22%3E%3Cellipse%20cx%3D%2215%22%20cy%3D%2216.8%22%20rx%3D%227.4%22%20ry%3D%227.6%22%2F%3E%3Cg%20transform%3D%22rotate(6%2019.6%2015.6)%22%3E%3Crect%20x%3D%2218.6%22%20y%3D%2215%22%20width%3D%222.1%22%20height%3D%228.4%22%20rx%3D%221.05%22%2F%3E%3C%2Fg%3E%3Cpath%20d%3D%22M14.6%2010%20C%2014.6%206.6%2C%2016.6%204.8%2C%2018.9%204.8%22%20stroke%3D%22%23f0a848%22%20stroke-width%3D%223%22%20fill%3D%22none%22%20stroke-linecap%3D%22round%22%2F%3E%3Ccircle%20cx%3D%2219.5%22%20cy%3D%224.7%22%20r%3D%223%22%2F%3E%3Cpath%20d%3D%22M22.1%203.5%20L26.4%205%20L22.1%206.5%20Z%22%2F%3E%3Ccircle%20cx%3D%2220.2%22%20cy%3D%224.2%22%20r%3D%220.95%22%20fill%3D%22%2314161d%22%2F%3E%3C%2Fg%3E%3C%2Fsvg%3E">
13
15
  <style>
14
16
  :root {
15
17
  --bg: #14161d;
@@ -1005,7 +1007,7 @@
1005
1007
  .freetext-row input { flex: 1; }
1006
1008
  #thinking { display: none; align-self: flex-start; color: var(--text-dim); font-family: var(--mono); font-size: 12.5px; padding: 0 22px 6px; }
1007
1009
  #thinking.visible { display: flex; align-items: center; gap: 6px; }
1008
- /* Shimmering text (an amber sweep) + three bouncing dots. */
1010
+ /* Shimmering text (an amber sweep) + a little Shadok pumping alongside. */
1009
1011
  #thinking .tw-text {
1010
1012
  background: linear-gradient(90deg, var(--text-dim) 30%, var(--phosphor) 50%, var(--text-dim) 70%);
1011
1013
  background-size: 220% 100%;
@@ -1014,17 +1016,20 @@
1014
1016
  animation: tw-shimmer 1.9s linear infinite;
1015
1017
  }
1016
1018
  @keyframes tw-shimmer { from { background-position: 200% 0; } to { background-position: -200% 0; } }
1017
- #thinking .tw-dots { display: inline-flex; align-items: flex-end; gap: 3px; }
1018
- #thinking .tw-dots i {
1019
- width: 3px; height: 3px; border-radius: 50%; background: var(--amber);
1020
- opacity: 0.3; animation: tw-dot 1.2s ease-in-out infinite;
1021
- }
1022
- #thinking .tw-dots i:nth-child(2) { animation-delay: 0.18s; }
1023
- #thinking .tw-dots i:nth-child(3) { animation-delay: 0.36s; }
1024
- @keyframes tw-dot { 0%, 100% { opacity: 0.25; transform: translateY(0); } 40% { opacity: 1; transform: translateY(-3px); } }
1019
+ /* The pumping Shadok: `color` feeds its `currentColor` fill so it follows the
1020
+ palette. The body bobs and the arm swings — the pump — on the same beat. */
1021
+ #thinking .tw-shadok { display: inline-flex; color: var(--amber); }
1022
+ #thinking .tw-shadok svg { width: 20px; height: 20px; overflow: visible; }
1023
+ /* Body bobs with translate (no transform-origin needed — that unit is a
1024
+ cross-browser trap on SVG). The arm swings from its own top via fill-box, so
1025
+ it reads as working a pump handle. */
1026
+ #thinking .tw-shadok .sh-body { animation: tw-pump 0.7s ease-in-out infinite; }
1027
+ #thinking .tw-shadok .sh-arm { animation: tw-arm 0.7s ease-in-out infinite; transform-box: fill-box; transform-origin: top center; }
1028
+ @keyframes tw-pump { 0%, 100% { transform: translateY(0); } 50% { transform: translateY(1.5px); } }
1029
+ @keyframes tw-arm { 0%, 100% { transform: rotate(-10deg); } 50% { transform: rotate(28deg); } }
1025
1030
  @media (prefers-reduced-motion: reduce) {
1026
1031
  #thinking .tw-text { animation: none; color: var(--phosphor); -webkit-text-fill-color: currentColor; }
1027
- #thinking .tw-dots i { animation: none; opacity: 0.6; }
1032
+ #thinking .tw-shadok .sh-body, #thinking .tw-shadok .sh-arm { animation: none; }
1028
1033
  }
1029
1034
 
1030
1035
  /* Composer */
@@ -1900,7 +1905,9 @@
1900
1905
  </div>
1901
1906
 
1902
1907
  <div id="transcripts"></div>
1903
- <div id="thinking"><span class="tw-text">Shadok is working</span><span class="tw-dots"><i></i><i></i><i></i></span></div>
1908
+ <!-- The Shadok is injected into .tw-shadok by the script (one drawing shared
1909
+ with the favicon, `shadokInner`); it pumps via CSS. -->
1910
+ <div id="thinking"><span class="tw-text">Shadok is pumping</span><span class="tw-shadok" aria-hidden="true"></span></div>
1904
1911
 
1905
1912
  <form id="composer" hidden>
1906
1913
  <textarea id="promptInput" rows="1"
@@ -3160,33 +3167,67 @@
3160
3167
  const v = getComputedStyle(document.documentElement).getPropertyValue(name).trim();
3161
3168
  return v || fallback;
3162
3169
  }
3163
- /** SVG data-URI favicon: the base mark, plus a coloured corner pip if given. */
3164
- /** The indicator fills the WHOLE favicon: at rest, a dark background + an
3165
- * accent chevron; for a state (red = to do, amber = to read) the whole square
3166
- * takes the colour, dark chevron on top. Far more visible than a small pip. */
3167
- function faviconSVG(bg) {
3170
+ /** The Shadok itself — the cockpit's mascot, which famously PUMPS. One drawing
3171
+ * serves every use: the favicon states here and the in-page "pumping"
3172
+ * indicator. `dx` shifts it left to clear room for a "!" / "?"; `bob`/`armRot`
3173
+ * pose a single frame of the pump (the favicon animates by re-generating with
3174
+ * new values, like the old spinner did); `anim` instead tags the body and arm
3175
+ * groups so CSS can drive them (used only in the DOM indicator, never a data
3176
+ * URI — CSS animation inside a favicon is unreliable). Legs stay planted. */
3177
+ function shadokInner(fill, { dx = 0, bob = 0, armRot = 6, anim = false } = {}) {
3168
3178
  const base = themeColor("--bg", "#14161d");
3169
- const accent = themeColor("--amber", "#f0a848");
3170
- const svg =
3171
- `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">` +
3172
- `<rect width="32" height="32" rx="7" fill="${bg || base}"/>` +
3173
- `<text x="16" y="25" font-family="monospace" font-size="26" font-weight="700" fill="${bg ? base : accent}" text-anchor="middle">›</text>` +
3174
- `</svg>`;
3175
- return "data:image/svg+xml," + encodeURIComponent(svg);
3176
- }
3177
- /** The "working" favicon: one BIG centred spinner filling the favicon (a faint
3178
- * track + a rotating accent arc). */
3179
- function faviconWorkingSVG(angle) {
3179
+ const bodyCls = anim ? ' class="sh-body"' : "";
3180
+ const armCls = anim ? ' class="sh-arm"' : "";
3181
+ const bodyTf = anim ? "" : ` transform="translate(0 ${bob})"`;
3182
+ const armTf = anim ? "" : ` transform="rotate(${armRot} 19.6 15.6)"`;
3183
+ return (
3184
+ `<g transform="translate(${dx} 0)">` +
3185
+ // legs + feet (planted, not part of the bob)
3186
+ `<g fill="${fill}">` +
3187
+ `<rect x="11.4" y="21" width="1.7" height="9" rx="0.85"/><rect x="17.4" y="21" width="1.7" height="9" rx="0.85"/>` +
3188
+ `<rect x="9.2" y="28.7" width="4.6" height="1.7" rx="0.85"/><rect x="16.4" y="28.7" width="4.6" height="1.7" rx="0.85"/>` +
3189
+ `</g>` +
3190
+ // body + pumping arm + long neck + head + beak + eye
3191
+ `<g${bodyCls} fill="${fill}"${bodyTf}>` +
3192
+ `<ellipse cx="15" cy="16.8" rx="7.4" ry="7.6"/>` +
3193
+ `<g${armCls}${armTf}><rect x="18.6" y="15" width="2.1" height="8.4" rx="1.05"/></g>` +
3194
+ `<path d="M14.6 10 C 14.6 6.6, 16.6 4.8, 18.9 4.8" stroke="${fill}" stroke-width="3" fill="none" stroke-linecap="round"/>` +
3195
+ `<circle cx="19.5" cy="4.7" r="3"/><path d="M22.1 3.5 L26.4 5 L22.1 6.5 Z"/>` +
3196
+ `<circle cx="20.2" cy="4.2" r="0.95" fill="${base}"/>` +
3197
+ `</g></g>`
3198
+ );
3199
+ }
3200
+ function faviconWrap(inner) {
3180
3201
  const base = themeColor("--bg", "#14161d");
3202
+ return (
3203
+ "data:image/svg+xml," +
3204
+ encodeURIComponent(
3205
+ `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">` +
3206
+ `<rect width="32" height="32" rx="7" fill="${base}"/>${inner}</svg>`,
3207
+ )
3208
+ );
3209
+ }
3210
+ /** The favicon: an accent Shadok on the dark ground. A waiting state adds a
3211
+ * bold "!" (something to answer) or "?" (something to read) in its colour, and
3212
+ * the Shadok slides left to make room — the coloured glyph is what the user
3213
+ * asked to distinguish the two. No symbol → the idle Shadok. */
3214
+ function faviconSVG(symbol, symColor) {
3181
3215
  const accent = themeColor("--amber", "#f0a848");
3182
- const track = accent + "38"; // ~0.22 alpha on a 6-digit-hex accent
3183
- const svg =
3184
- `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">` +
3185
- `<rect width="32" height="32" rx="7" fill="${base}"/>` +
3186
- `<circle cx="16" cy="16" r="11" fill="none" stroke="${track}" stroke-width="4"/>` +
3187
- `<g transform="rotate(${angle} 16 16)"><circle cx="16" cy="16" r="11" fill="none" stroke="${accent}" stroke-width="4" stroke-linecap="round" stroke-dasharray="34 100"/></g>` +
3188
- `</svg>`;
3189
- return "data:image/svg+xml," + encodeURIComponent(svg);
3216
+ const shadok = shadokInner(accent, symbol ? { dx: -3.6 } : {});
3217
+ const sym = symbol
3218
+ ? `<text x="25.8" y="13" font-family="monospace" font-size="15" font-weight="800" fill="${symColor || accent}" text-anchor="middle">${symbol}</text>`
3219
+ : "";
3220
+ return faviconWrap(shadok + sym);
3221
+ }
3222
+ /** The "working" favicon: the Shadok pumping. `phase` (0..PUMP_FRAMES-1) rides
3223
+ * a cycle — the body bobs and the arm swings, re-generated each tick. */
3224
+ const PUMP_FRAMES = 8;
3225
+ function faviconPumpingSVG(phase) {
3226
+ const accent = themeColor("--amber", "#f0a848");
3227
+ const p = (Math.sin((phase / PUMP_FRAMES) * 2 * Math.PI - Math.PI / 2) + 1) / 2; // 0..1..0
3228
+ const bob = +(p * 1.8).toFixed(2);
3229
+ const armRot = Math.round(-8 + p * 32); // -8 (up) .. +24 (down)
3230
+ return faviconWrap(shadokInner(accent, { bob, armRot }));
3190
3231
  }
3191
3232
  /** What each channel is asking for, as the pure module wants it. */
3192
3233
  function attentionInput() {
@@ -3202,8 +3243,8 @@
3202
3243
  // costs no timer at all.
3203
3244
  let blinkTimer = null;
3204
3245
  let blinkPhase = 0;
3205
- let workTimer = null; // animates the "working" favicon (spinner)
3206
- let workAngle = 0;
3246
+ let workTimer = null; // animates the "working" favicon (the pumping Shadok)
3247
+ let workPhase = 0;
3207
3248
  function stopWork() { if (workTimer) { clearInterval(workTimer); workTimer = null; } }
3208
3249
  function paint() {
3209
3250
  // The ESM bridge lands after the document is parsed (gotcha #10): until it
@@ -3223,20 +3264,24 @@
3223
3264
  phase: blinkPhase,
3224
3265
  });
3225
3266
  document.title = s.badge + titleBase;
3226
- // "working" = animated favicon (spinner). The module only returns that mode
3267
+ // "working" = the animated pumping Shadok. The module only returns that mode
3227
3268
  // when NOTHING of higher priority (blocked / unread) is waiting — priority is
3228
3269
  // therefore handled upstream: a waiting agent always beats a busy one.
3229
3270
  if (s.mode === "working") {
3230
3271
  if (blinkTimer) { clearInterval(blinkTimer); blinkTimer = null; blinkPhase = 0; }
3231
3272
  if (!workTimer) {
3232
- const tick = () => { workAngle = (workAngle + 30) % 360; favicon.href = faviconWorkingSVG(workAngle); };
3273
+ const tick = () => { workPhase = (workPhase + 1) % PUMP_FRAMES; favicon.href = faviconPumpingSVG(workPhase); };
3233
3274
  tick();
3234
- workTimer = setInterval(tick, 120);
3275
+ workTimer = setInterval(tick, 110);
3235
3276
  }
3236
3277
  return;
3237
3278
  }
3238
3279
  stopWork();
3239
- favicon.href = faviconSVG(s.color);
3280
+ // A waiting Shadok carries the glyph the user asked for: "!" when it needs an
3281
+ // answer, "?" when something's there to read. The colour is notifyState's
3282
+ // (RED / RED_DIM on the away-blink, AMBER for unread); idle → no glyph.
3283
+ const symbol = s.mode === "blocked" ? "!" : s.mode === "unread" ? "?" : null;
3284
+ favicon.href = faviconSVG(symbol, s.color);
3240
3285
  if (s.blink && !blinkTimer) {
3241
3286
  blinkTimer = setInterval(() => { blinkPhase ^= 1; paint(); }, window.BLINK_MS || 900);
3242
3287
  } else if (!s.blink && blinkTimer) {
@@ -3246,6 +3291,16 @@
3246
3291
  }
3247
3292
  }
3248
3293
  function refreshBadge() { paint(); }
3294
+ // The in-page "pumping" Shadok, drawn once from the SAME `shadokInner` as the
3295
+ // favicon. `currentColor` lets the CSS palette (var(--amber)) tint it, so it
3296
+ // follows a theme change with no redraw; `anim` tags the body/arm for the CSS
3297
+ // pump. The element sits earlier in the document, so it's here to fill now.
3298
+ {
3299
+ const twShadok = document.querySelector("#thinking .tw-shadok");
3300
+ if (twShadok)
3301
+ twShadok.innerHTML =
3302
+ `<svg viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg">${shadokInner("currentColor", { anim: true })}</svg>`;
3303
+ }
3249
3304
  // Coming back must stop the blink at once, not at the next tick. Both signals
3250
3305
  // matter: switching tabs fires visibilitychange, switching application only
3251
3306
  // fires focus/blur.
@@ -3393,7 +3448,7 @@
3393
3448
  $("chanBarName").title = t.cwd || "";
3394
3449
  updateSendDisabled(); // t === active here; also honours a pending upload
3395
3450
  thinking.classList.toggle("visible", t.status === "busy");
3396
- // Showing/hiding "Shadok is working…" changes the transcript area's height:
3451
+ // Showing/hiding "Shadok is pumping" changes the transcript area's height:
3397
3452
  // without re-pinning to the bottom, the last message slips below the fold
3398
3453
  // (typically "cut off" right after sending a prompt).
3399
3454
  pinBottom(t);
package/public/notify.js CHANGED
@@ -65,8 +65,8 @@ export function notifyState(channels, view) {
65
65
  : { color: RED, badge: "● ", blink: true, mode: "blocked" };
66
66
  }
67
67
  if (unread) return { color: AMBER, badge: "● ", blink: false, mode: "unread" };
68
- // "working" has neither pip colour nor badge: the favicon is animated (a
69
- // spinner) by the caller for as long as the mode is "working".
68
+ // "working" has neither pip colour nor badge: the caller animates the favicon
69
+ // (the Shadok pumping) for as long as the mode is "working".
70
70
  if (working) return { color: null, badge: "", blink: false, mode: "working" };
71
71
  return { color: null, badge: "", blink: false, mode: null };
72
72
  }