shadok-ai 0.12.9 → 0.12.10

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.12.9",
3
+ "version": "0.12.10",
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
@@ -275,7 +275,8 @@
275
275
  .led.busy { background: var(--amber); box-shadow: 0 0 8px var(--amber); animation: breathe 1.2s ease-in-out infinite; }
276
276
  .led.off { background: var(--err); }
277
277
  @keyframes breathe { 50% { opacity: 0.35; } }
278
- @media (prefers-reduced-motion: reduce) { .led.busy { animation: none; } }
278
+ @keyframes sheet-up { from { transform: translateY(100%); } to { transform: translateY(0); } }
279
+ @media (prefers-reduced-motion: reduce) { .led.busy { animation: none; } body.picker-open #tabbar { animation: none; } }
279
280
  header .spacer { flex: 1; }
280
281
  /* Quota dials (5h / 7d subscription usage).
281
282
  A 240° speedometer whose CENTRE is the ideal pace and whose RIGHT END is
@@ -1178,17 +1179,43 @@
1178
1179
  #chanBar .gauge .label { font-size: 10px; }
1179
1180
  #chanBar #cwdView { display: inline-block; max-width: 200px; overflow: hidden; text-overflow: ellipsis; vertical-align: bottom; }
1180
1181
  /* Channel selector: hidden on desktop (the sidebar is the navigation). */
1181
- #chanSelect {
1182
- display: none;
1183
- font-family: var(--mono); font-size: 13px;
1182
+ /* Mobile agents trigger: hidden on desktop (the sidebar IS the navigation),
1183
+ shown as a compact "● name ▾" pill on a phone. */
1184
+ #chanPicker {
1185
+ display: none; align-items: center; gap: 6px;
1186
+ font-family: var(--mono); font-size: 13px; cursor: pointer;
1184
1187
  background: var(--bg-inset); color: var(--text);
1185
1188
  border: 1px solid var(--line); border-radius: 6px; padding: 5px 8px;
1186
1189
  max-width: 55vw;
1187
1190
  }
1191
+ #chanPicker #pickerName { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
1192
+ #chanPicker .pk-caret { color: var(--text-dim); font-size: 11px; flex: none; transition: transform 0.15s; }
1193
+ body.picker-open #chanPicker { border-color: var(--amber); }
1194
+ body.picker-open #chanPicker .pk-caret { transform: rotate(180deg); }
1188
1195
  /* ── Mobile: sidebar hidden, navigation via the channel header's selector ── */
1189
1196
  @media (max-width: 640px) {
1190
1197
  #tabbar { display: none; }
1191
- #chanSelect { display: inline-block; min-width: 0; }
1198
+ #chanPicker { display: inline-flex; min-width: 0; }
1199
+ /* The agents column, on demand, as a bottom sheet — the SAME rich tabs, not a
1200
+ flattened list. Slides up over the chat; a backdrop dims and closes it. */
1201
+ body.picker-open #tabbar {
1202
+ display: flex;
1203
+ position: fixed; left: 0; right: 0; bottom: 0; top: auto;
1204
+ width: auto; max-height: 78vh; z-index: 55;
1205
+ border-right: none; border-top: 1px solid var(--line);
1206
+ border-radius: 14px 14px 0 0;
1207
+ box-shadow: 0 -12px 40px rgba(0, 0, 0, 0.5);
1208
+ padding: 8px 8px calc(12px + env(safe-area-inset-bottom, 0px));
1209
+ animation: sheet-up 0.18s ease-out;
1210
+ }
1211
+ /* A grab handle, so the panel reads as a sheet you can dismiss. */
1212
+ body.picker-open #tabbar::before {
1213
+ content: ""; align-self: center; flex: none;
1214
+ width: 36px; height: 4px; margin: 2px 0 8px;
1215
+ border-radius: 2px; background: var(--line);
1216
+ }
1217
+ #pickerBackdrop { position: fixed; inset: 0; z-index: 54; background: rgba(8, 9, 13, 0.5); }
1218
+ #pickerBackdrop[hidden] { display: none; }
1192
1219
  #chanBar #chanBarName { display: none; }
1193
1220
  /* Condensed channel bar: keep Status + Session, hide the rest. */
1194
1221
  #chanBar #dirGauge, #chanBar #branchGauge { display: none !important; }
@@ -1241,7 +1268,7 @@
1241
1268
  @media (pointer: coarse) {
1242
1269
  textarea, select,
1243
1270
  input[type="text"], input[type="password"], input[type="number"], input[type="search"],
1244
- #composer textarea, #chanSelect { font-size: 16px; }
1271
+ #composer textarea { font-size: 16px; }
1245
1272
  }
1246
1273
  #xtermHost { flex: 1; min-height: 0; overflow: auto; padding: 8px 10px; }
1247
1274
  #xtermHost .xterm { height: 100%; }
@@ -2022,6 +2049,8 @@
2022
2049
  </button>
2023
2050
  </div>
2024
2051
  </nav>
2052
+ <!-- Dims the chat behind the mobile agents sheet; tap to close. -->
2053
+ <div id="pickerBackdrop" hidden></div>
2025
2054
 
2026
2055
  <section class="chat-pane">
2027
2056
 
@@ -2030,8 +2059,12 @@
2030
2059
  separate place, so its actions have no reason to appear with it. -->
2031
2060
  <div id="chanBar" hidden>
2032
2061
  <span class="label" id="chanBarName"></span>
2033
- <!-- Mobile: sidebar hidden → this selector switches channel / creates one. -->
2034
- <select id="chanSelect" title="Agent"></select>
2062
+ <!-- Mobile: the sidebar is hidden → this button opens the very same rich
2063
+ agent tabs (LED, state, background-work hatch, context bar) as a bottom
2064
+ sheet, instead of flattening them into a native <select>. -->
2065
+ <button id="chanPicker" type="button" aria-haspopup="true" aria-expanded="false" title="Agents">
2066
+ <span id="pickerLed" class="led"></span><span id="pickerName">—</span><span class="pk-caret">▾</span>
2067
+ </button>
2035
2068
  <div class="gauge">
2036
2069
  <span class="label">Status</span>
2037
2070
  <span class="value"><span id="led" class="led"></span><span id="state">—</span></span>
@@ -2787,7 +2820,7 @@
2787
2820
  document.addEventListener("click", () => {
2788
2821
  if (Date.now() - tabMenuOpenedAt > 200) closeTabMenu();
2789
2822
  });
2790
- document.addEventListener("keydown", (e) => { if (e.key === "Escape") closeTabMenu(); });
2823
+ document.addEventListener("keydown", (e) => { if (e.key === "Escape") { closeTabMenu(); closePicker(); } });
2791
2824
  window.addEventListener("blur", () => closeTabMenu());
2792
2825
 
2793
2826
  /** Right-click menu on a tab: the home for per-channel actions. */
@@ -3460,6 +3493,8 @@
3460
3493
  promptInput.value = tab.draft;
3461
3494
  autosizePrompt();
3462
3495
  refreshChrome();
3496
+ // Picking an agent from the mobile sheet dismisses it.
3497
+ closePicker();
3463
3498
  // Experimental terminal follows the active channel (re-seeds on switch).
3464
3499
  if (typeof syncTerm === "function") syncTerm();
3465
3500
  if (tab.status !== "setup") {
@@ -3866,29 +3901,39 @@
3866
3901
  * central panel shows the invitation.
3867
3902
  */
3868
3903
  /** (Re)builds the mobile selector: one option per channel + "+ New agent". */
3904
+ // The mobile trigger just MIRRORS the active agent (its name + LED). The sheet
3905
+ // it opens is the real sidebar (#tabbar), so there is no flattened list to
3906
+ // rebuild here — every rich tab renders itself. Name kept for its callers.
3869
3907
  function rebuildChanSelect() {
3870
- const sel = $("chanSelect");
3871
- if (!sel) return;
3872
- const cur = active ? String(active.id) : "";
3873
- sel.innerHTML = "";
3874
- for (const t of tabs) {
3875
- const o = document.createElement("option");
3876
- o.value = String(t.id);
3877
- o.textContent = t.nameEl ? t.nameEl.textContent : "agent " + t.id;
3878
- sel.appendChild(o);
3879
- }
3880
- const nw = document.createElement("option");
3881
- nw.value = "__new__";
3882
- nw.textContent = "+ New agent";
3883
- sel.appendChild(nw);
3884
- sel.value = cur;
3885
- }
3886
- $("chanSelect").addEventListener("change", () => {
3887
- const v = $("chanSelect").value;
3888
- if (v === "__new__") { createTab(); return; }
3889
- const t = tabs.find((x) => String(x.id) === v);
3890
- if (t) activate(t);
3891
- });
3908
+ const led = $("pickerLed"), name = $("pickerName");
3909
+ if (!led || !name) return;
3910
+ led.className = active && active.ledEl ? active.ledEl.className : "led";
3911
+ name.textContent = active ? (active.nameEl ? active.nameEl.textContent : "agent " + active.id) : "—";
3912
+ }
3913
+ // ── Mobile agents sheet ──────────────────────────────────────────────────
3914
+ // On a phone the sidebar is hidden; this opens it as a bottom sheet so the
3915
+ // channels keep their full rendering (status, background-work hatch, context)
3916
+ // instead of collapsing into a native <select>.
3917
+ function isMobileWidth() { return matchMedia("(max-width: 640px)").matches; }
3918
+ function openPicker() {
3919
+ if (!isMobileWidth()) return;
3920
+ document.body.classList.add("picker-open");
3921
+ $("pickerBackdrop").hidden = false;
3922
+ $("chanPicker").setAttribute("aria-expanded", "true");
3923
+ }
3924
+ function closePicker() {
3925
+ if (!document.body.classList.contains("picker-open")) return;
3926
+ document.body.classList.remove("picker-open");
3927
+ $("pickerBackdrop").hidden = true;
3928
+ $("chanPicker").setAttribute("aria-expanded", "false");
3929
+ }
3930
+ function togglePicker() {
3931
+ document.body.classList.contains("picker-open") ? closePicker() : openPicker();
3932
+ }
3933
+ $("chanPicker").addEventListener("click", togglePicker);
3934
+ $("pickerBackdrop").addEventListener("click", closePicker);
3935
+ // Leaving phone width (rotation, resized window) must not strand an open sheet.
3936
+ addEventListener("resize", () => { if (!isMobileWidth()) closePicker(); });
3892
3937
 
3893
3938
  function refreshChrome() {
3894
3939
  rebuildChanSelect(); // keeps the mobile selector in step (list + active)
@@ -4774,6 +4819,9 @@
4774
4819
  * of the conversation you were watching.
4775
4820
  */
4776
4821
  function openSetup() {
4822
+ // Close the mobile agents sheet first: the setup overlay sits BELOW it in
4823
+ // the stack, so leaving the sheet open would hide the form behind it.
4824
+ closePicker();
4777
4825
  // PREDICTABLE defaults on every opening. The browser restores form-field
4778
4826
  // state (a "continue"/"resume" mode chosen once, an unticked worktree box, a
4779
4827
  // worktree cwd) — which is what silently made "create an agent" fall back
@@ -7252,6 +7300,7 @@
7252
7300
 
7253
7301
  tweakCta.addEventListener("click", async () => {
7254
7302
  if (tweakBusy) return;
7303
+ closePicker(); // dismiss the mobile sheet; the tweak agent takes over the view
7255
7304
  // Belt and braces: the CTA is hidden as soon as a tweak channel exists, but
7256
7305
  // should it reappear we activate the existing one rather than open a second.
7257
7306
  const already = tweakTab();
@@ -38,16 +38,14 @@ export const TOUR_STEPS = [
38
38
  "Every agent gets its own git worktree and branch, so several never collide. " +
39
39
  "Pick one here to follow it, and “+ New agent” starts another. One of them is this " +
40
40
  "cockpit's home agent: the only one that cannot be closed.",
41
- // The column on desktop, the channel picker on a phone — never both at once.
42
- //
43
- // Which is also why this body says neither "at the top" nor "at the bottom",
44
- // and no longer points at "Tweak Shadok-AI": those describe the COLUMN, and
45
- // the phone's `<select>` holds one option per agent plus "+ New agent" and
46
- // nothing else. A tour that names a control the reader cannot find is the
47
- // same failure as a spotlight on empty space, just harder to notice.
48
- // (That Tweak is unreachable on a phone at all is a real gap — in the UI,
49
- // not in the tour.)
50
- target: ["#tabbar", "#chanSelect"],
41
+ // The column on desktop, the agents-picker button on a phone — never both at
42
+ // once. The phone's `#chanPicker` opens the SAME column as a bottom sheet, so
43
+ // "+ New agent" and Tweak are reachable there too now — but the spotlight
44
+ // lands on the CLOSED button, whose contents aren't on screen yet. So this
45
+ // body still says neither "at the top" nor "at the bottom" and still doesn't
46
+ // point at "Tweak Shadok-AI": naming a control the reader cannot see at the
47
+ // moment of the spotlight is the same failure as a spotlight on empty space.
48
+ target: ["#tabbar", "#chanPicker"],
51
49
  },
52
50
  {
53
51
  id: "tab",