shadok-ai 0.2.46 → 0.2.47

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/README.md CHANGED
@@ -75,6 +75,12 @@ Enter to skip; you can add it later from the web UI).
75
75
  the chat can't express. With tmux there is also an **experimental real
76
76
  terminal** (xterm.js over the pane's byte stream) when a snapshot isn't enough.
77
77
  - **Diff panel** — what an agent actually changed, against its base.
78
+ - **Phone layout** — the cockpit is a fixed chassis that never pans sideways,
79
+ sized on the *visual* viewport so the composer stays clear of the browser's
80
+ own bar and, when the keyboard opens, sits right above it (the instrument bar
81
+ folds away while you type). Fields are 16px, which is what stops Safari from
82
+ zooming in on focus. The agents column is replaced by a channel selector in
83
+ the channel bar.
78
84
  - **Tweak Shadok-AI** — a card pinned at the bottom of the agents column. One
79
85
  click clones shadok-ai's own source into `~/.shadok-ai/self/shadok-ai` and
80
86
  starts an agent on it in its own worktree; that agent delivers its change as a
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "shadok-ai",
3
- "version": "0.2.46",
3
+ "version": "0.2.47",
4
4
  "main": "dist/session.js",
5
5
  "scripts": {
6
6
  "test": "node --import tsx --test test/*.ts .claude/skills/shadok-ai-agents/test/*.test.mjs",
package/public/index.html CHANGED
@@ -2,7 +2,12 @@
2
2
  <html lang="en">
3
3
  <head>
4
4
  <meta charset="utf-8">
5
- <meta name="viewport" content="width=device-width, initial-scale=1">
5
+ <!-- viewport-fit=cover: the page reaches under the notch and the home indicator;
6
+ the `env(safe-area-inset-*)` rules below put the CONTENT back in the clear.
7
+ No `maximum-scale` / `user-scalable=no`: banning zoom to avoid Safari's
8
+ focus zoom would take pinch-zoom from the people who need it — the 16px
9
+ fields (see the coarse-pointer query) fix it at the source instead. -->
10
+ <meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
6
11
  <title>shadok-ai — cockpit</title>
7
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">
8
13
  <style>
@@ -24,6 +29,7 @@
24
29
  * { margin: 0; padding: 0; box-sizing: border-box; }
25
30
  [hidden] { display: none !important; }
26
31
  html, body { height: 100%; }
32
+ html { overflow: hidden; }
27
33
  body {
28
34
  background: var(--bg);
29
35
  color: var(--text);
@@ -32,9 +38,31 @@
32
38
  display: flex;
33
39
  flex-direction: column;
34
40
  /* Châssis fixe : la page ne scrolle jamais — seuls les panneaux internes
35
- (liste des canaux, transcript, pickers) ont leur propre overflow. */
41
+ (liste des canaux, transcript, pickers) ont leur propre overflow.
42
+ `position: fixed` pins the chassis TO the viewport: on a phone
43
+ `overflow: hidden` alone does not stop the browser from dragging the page
44
+ around — that is what let the whole interface slide left and right as soon
45
+ as a zoom (even an accidental one) made the layout viewport wider than the
46
+ screen. `overscroll-behavior` kills the rubber-band and pull-to-refresh. */
47
+ position: fixed;
48
+ top: 0;
49
+ left: 0;
50
+ width: 100%;
36
51
  overflow: hidden;
37
- }
52
+ overscroll-behavior: none;
53
+ /* Landscape: the notch eats one edge. The content steps aside, the background
54
+ does not. */
55
+ padding-left: env(safe-area-inset-left, 0px);
56
+ padding-right: env(safe-area-inset-right, 0px);
57
+ }
58
+ /* Chassis height — three levels, coarsest first:
59
+ 1. `100%`: the LAYOUT viewport. On a phone it assumes the URL bar retracted,
60
+ so the bottom of the page (the composer) ended up UNDER the browser bar.
61
+ 2. `100dvh`: follows the URL bar as it retracts and comes back.
62
+ 3. `--app-h`: additionally follows the on-screen KEYBOARD, which no CSS unit
63
+ reports. Set from `visualViewport` (see `syncViewport`), touch only. */
64
+ body { height: 100dvh; }
65
+ body.vv-sized { height: var(--app-h); }
38
66
  .label {
39
67
  font-family: var(--mono);
40
68
  font-size: 10px;
@@ -66,6 +94,9 @@
66
94
  }
67
95
  /* Keep the toolbar buttons together so they never split across lines. */
68
96
  header button { flex: 0 0 auto; }
97
+ /* A box that is no box: the buttons stay flex items of `header`. It exists only
98
+ so the mobile query can give them a line of their own. */
99
+ .hdr-tools { display: contents; }
69
100
  header .brand {
70
101
  font-family: var(--mono);
71
102
  font-size: 14px;
@@ -870,12 +901,17 @@
870
901
  #composer {
871
902
  display: flex;
872
903
  gap: 10px;
873
- padding: 14px 22px 18px;
904
+ /* The bottom padding absorbs the home indicator (viewport-fit=cover): without
905
+ it the field slides under it and half the text becomes unreadable. */
906
+ padding: 14px 22px calc(18px + env(safe-area-inset-bottom, 0px));
874
907
  border-top: 1px solid var(--line);
875
908
  background: var(--bg-raised);
876
909
  }
877
910
  #composer textarea {
878
911
  flex: 1;
912
+ /* Without it a textarea never shrinks below its intrinsic width (~20 columns)
913
+ and pushes the composer off the screen on a small phone. */
914
+ min-width: 0;
879
915
  font-family: var(--sans);
880
916
  font-size: 14px;
881
917
  background: var(--bg-inset);
@@ -955,13 +991,43 @@
955
991
  /* ── Mobile : sidebar cachée, navigation par le sélecteur du header canal ── */
956
992
  @media (max-width: 640px) {
957
993
  #tabbar { display: none; }
958
- #chanSelect { display: inline-block; }
994
+ #chanSelect { display: inline-block; min-width: 0; }
959
995
  #chanBar #chanBarName { display: none; }
960
996
  /* Bandeau canal condensé : on garde Status + Session, on cache le reste. */
961
997
  #chanBar #dirGauge, #chanBar #branchGauge { display: none !important; }
962
998
  #chanBar .gauge .label { display: none; } /* valeurs seules, plus compact */
963
999
  #chanBar { gap: 6px; padding: 6px 10px; }
964
- header { flex-wrap: wrap; row-gap: 4px; }
1000
+ header { flex-wrap: wrap; row-gap: 6px; gap: 8px; padding: 8px 12px; }
1001
+ /* Row 1: brand + dials. Row 2: every icon, left-aligned. They used to spread
1002
+ across the two rows as space allowed, and the first of them (🔑) ended up
1003
+ flush against the right edge. */
1004
+ .hdr-tools { display: flex; flex: 1 0 100%; align-items: center; gap: 6px; }
1005
+ header button.icon { padding: 5px 7px; }
1006
+ .quota { min-width: 46px; }
1007
+ .dial { width: 46px; height: 36px; }
1008
+ /* A bit more room for a thumb, a bit less margin wasted on a narrow screen. */
1009
+ .transcript { padding: 14px 14px 6px; }
1010
+ #composer { padding: 10px 14px calc(12px + env(safe-area-inset-bottom, 0px)); gap: 8px; }
1011
+ #thinking { padding: 0 14px 6px; }
1012
+ .turn, .turn.activity { max-width: 92%; }
1013
+ /* Keyboard up: the instrument bar steps aside. It is of no use while typing
1014
+ and it costs a good third of the conversation still visible; it comes back
1015
+ the moment the keyboard closes. */
1016
+ body.kb-up header { display: none; }
1017
+ }
1018
+ /* iOS Safari ZOOMS INTO any focused field whose font is under 16px. That zoom is
1019
+ the single cause of two symptoms that look unrelated: the layout viewport
1020
+ becomes wider than the screen, so the interface starts sliding left and
1021
+ right, and the page jumps up on focus. We do not ban zoom in the viewport tag
1022
+ (that would take it from whoever needs it) — the fields get 16px. Keyed on
1023
+ the POINTER, not on width: a large touch tablet behaves exactly the same.
1024
+ Every selector here has to match or beat the specificity of the rule it
1025
+ corrects (`#composer textarea`, `input[type="text"]`) — otherwise the
1026
+ original wins and the zoom is back, in silence. */
1027
+ @media (pointer: coarse) {
1028
+ textarea, select,
1029
+ input[type="text"], input[type="password"], input[type="number"], input[type="search"],
1030
+ #composer textarea, #chanSelect { font-size: 16px; }
965
1031
  }
966
1032
  #xtermHost { flex: 1; min-height: 0; overflow: auto; padding: 8px 10px; }
967
1033
  #xtermHost .xterm { height: 100%; }
@@ -1092,6 +1158,11 @@
1092
1158
  portant `.overlay`, il n'y a plus de liste à ne pas oublier. */
1093
1159
  .overlay {
1094
1160
  position: fixed; inset: 0; z-index: 50;
1161
+ /* Same trap as the chassis: `inset: 0` spans the LAYOUT viewport, so with the
1162
+ keyboard up a popin centres itself over a height that includes the keyboard
1163
+ — and its field ends up underneath. `--app-h` brings it back to what is
1164
+ visible; unset (desktop), `auto` lets `inset: 0` stretch as before. */
1165
+ height: var(--app-h, auto);
1095
1166
  display: flex; align-items: center; justify-content: center;
1096
1167
  padding: 24px;
1097
1168
  background: rgba(8, 9, 13, 0.62);
@@ -1114,6 +1185,15 @@
1114
1185
  border-radius: 6px; padding: 14px 16px;
1115
1186
  box-shadow: 0 18px 50px rgba(0, 0, 0, 0.55);
1116
1187
  }
1188
+ /* Popins on a phone: near full screen, and bounded by the POPIN (hence by
1189
+ `--app-h`, keyboard deducted) instead of `80vh`, which counts the keyboard in
1190
+ and leaves the field under it. This query has to stay AFTER the rules it
1191
+ corrects — the main mobile query sits higher in the sheet, and at equal
1192
+ specificity order decides: both rules were mute when written up there. */
1193
+ @media (max-width: 640px) {
1194
+ .overlay { padding: 10px; }
1195
+ #secretsPanel, .sec-panel { width: 100%; max-height: 100%; }
1196
+ }
1117
1197
  /* Squelette entête / corps / pied. Seul le CORPS défile, donc l'action
1118
1198
  principale reste atteignable : le formulaire de création dépasse l'écran sur
1119
1199
  un portable, et son bouton « Start » se trouvait en bas d'une page à
@@ -1208,6 +1288,12 @@
1208
1288
  <text class="unit" x="28" y="41" text-anchor="middle">7d</text>
1209
1289
  </svg>
1210
1290
  </div>
1291
+ <!-- The global actions, in ONE container. `display: contents` on desktop: they
1292
+ stay direct items of the bar and the layout does not move a pixel. On a
1293
+ phone this container becomes a ROW OF ITS OWN, so no icon gets pushed
1294
+ against the right edge (the 🔑 was there, 16px from it — out of sight at
1295
+ the slightest zoom). -->
1296
+ <div class="hdr-tools">
1211
1297
  <button id="secretsBtn" class="icon" title="Repo secrets (injected as env)">🔑</button>
1212
1298
  <button id="profilesBtn" class="icon" title="Agent profiles (role + guardrails + secrets)">👤</button>
1213
1299
  <button id="cronBtn" class="icon" title="Scheduled prompts for this agent (monitoring / reporting)">⏰</button>
@@ -1219,6 +1305,7 @@
1219
1305
  <button id="toggleDiff">Diff</button>
1220
1306
  </div>
1221
1307
  </div>
1308
+ </div>
1222
1309
  </header>
1223
1310
 
1224
1311
  <div id="verMenu" hidden>
@@ -4122,6 +4209,45 @@
4122
4209
  persistDraft(active);
4123
4210
  });
4124
4211
 
4212
+ /* ── The on-screen keyboard is not a CSS unit ─────────────────────────────
4213
+ When it opens, the LAYOUT viewport keeps its full height: the chassis still
4214
+ believes it owns the screen, the composer ends up under the keyboard, and the
4215
+ browser then scrolls the document to reveal the field — that is the
4216
+ "everything jumps up", the header leaving by the top. `visualViewport` is the
4217
+ only thing that reports the keyboard, so the chassis is sized from it and the
4218
+ browser's rescue scroll has nothing left to do. Coarse pointers only: on a
4219
+ desktop `visualViewport` also tracks pinch-zoom, and resizing the page on
4220
+ every pinch would be a regression for nobody's benefit. */
4221
+ if (matchMedia("(pointer: coarse)").matches) {
4222
+ // The shortcut reminder needs two lines and gets clipped by a one-line field
4223
+ // — and it does not address a phone anyway, where there is neither
4224
+ // Shift+Enter nor a physical keyboard.
4225
+ promptInput.placeholder = "Write your prompt";
4226
+ }
4227
+ if (window.visualViewport && matchMedia("(pointer: coarse)").matches) {
4228
+ const vv = window.visualViewport;
4229
+ let pending = 0;
4230
+ const syncViewport = () => {
4231
+ pending = 0;
4232
+ document.documentElement.style.setProperty("--app-h", vv.height + "px");
4233
+ document.body.classList.add("vv-sized");
4234
+ // `innerHeight` is the LAYOUT viewport: it does not move when the keyboard
4235
+ // opens, so the gap between the two IS the keyboard. The threshold keeps a
4236
+ // retracting URL bar from being read as one.
4237
+ document.body.classList.toggle("kb-up", window.innerHeight - vv.height > 120);
4238
+ // The chassis now fits above the keyboard: if the browser already dragged
4239
+ // the document, that offset hides nothing but the header.
4240
+ if (window.scrollY || window.scrollX) window.scrollTo(0, 0);
4241
+ // The transcript just shrank: the last message must stay the last message
4242
+ // visible (pinBottom leaves a reader who scrolled up alone).
4243
+ if (active) pinBottom(active);
4244
+ };
4245
+ const schedule = () => { if (!pending) pending = requestAnimationFrame(syncViewport); };
4246
+ vv.addEventListener("resize", schedule);
4247
+ vv.addEventListener("scroll", schedule);
4248
+ syncViewport();
4249
+ }
4250
+
4125
4251
  // ── Interactive terminal (xterm.js over the raw tmux pipe), per channel ──
4126
4252
  // The ⌨️ button toggles it for the ACTIVE channel; it fills the main area
4127
4253
  // (.chat-pane gets .terminal-mode → chat hidden, terminal shown). Raw pane