shadok-ai 0.15.41 → 0.15.43

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.
Files changed (3) hide show
  1. package/README.md +17 -16
  2. package/package.json +1 -1
  3. package/public/index.html +345 -51
package/README.md CHANGED
@@ -76,22 +76,20 @@ Enter to skip; you can add it later from the web UI).
76
76
  - **Channels** — one `claude` process each, running in parallel. Renaming,
77
77
  grouping, and closing all sync live between the browser and Telegram: it's
78
78
  one list, server-owned, not two copies.
79
- - **Groups column** — once you create a group, on a desktop the groups get a
80
- column of their own, left of the agents (with no group there is no column, just
81
- the single agents list): pick a group and the agents column shows only its
82
- agents (*Ungrouped* holds the rest, the home agent included unless you file
83
- it). Each
84
- group has an icon — drawn at random, double-click it to pick another: choose
85
- from the grid, type any emoji, or **type a word** (`rocket`, `bug`, `money`…)
86
- to search for a matching one — and a **+** that creates the new agent straight
87
- into that group.
88
- A single click on a group (its icon or its row) selects it; this matters once
89
- **«** folds the column to its icons, where the icon is the whole row.
90
- Dragging an agent onto a group still files it there, and dragging groups
91
- reorders them. Double-click a name to rename it. A group you are not looking at
92
- carries the same signal its agents do — the row lights up (and breathes) when
93
- one is working, waiting for you, or has an unread answer, the way a channel
94
- does. On a phone nothing changes: the agents sheet still lists every group.
79
+ - **Groups** — on a desktop the cockpit is laid out like Microsoft Teams: an
80
+ app bar on the left (Activity — a click opens the next agent waiting for you —
81
+ then Profiles, Ledger, Secrets, Users, Security, Telegram, Sound), a search box
82
+ in the title bar that narrows the agents list as you type (Enter opens the first
83
+ match), and an agents pane where **every group is a collapsible section**, each
84
+ agent a round avatar with a presence dot (green ready, red working, hollow
85
+ offline). A group's icon opens the icon picker: choose from the grid, type any
86
+ emoji, or **type a word** (`rocket`, `bug`, `money`…) to search for one; its
87
+ **+** creates the new agent straight into that group. Dragging an agent onto a
88
+ group files it there, dragging groups reorders them, and double-clicking a name
89
+ renames it. The agent's header carries Chat / Changes / Schedule / Terminal as
90
+ tabs, and a question the agent asks shows as an *Approval requested* card. On a
91
+ phone nothing changes: the tools sit in the ⋯ menu and the agents sheet lists
92
+ every group.
95
93
  - **Worktree isolation** — spawned agents get their own git worktree and branch
96
94
  by default, so parallel agents never collide. Work is never auto-discarded:
97
95
  an empty worktree is reclaimed on close, anything with changes or commits
@@ -848,6 +846,9 @@ cockpit's name, per launch directory: click the header brand to rename it, so
848
846
  several cockpits stay apart in the tab bar; an empty PUT reverts to the default),
849
847
  `/theme` (GET/PUT — the cockpit's colour palette, per launch directory: an accent
850
848
  key picked from the ⋯ menu, e.g. `emerald`; unknown/`amber` reverts to default.
849
+ The default palette (stored key `amber`, labelled *Shadok*) is the Teams purple in
850
+ light mode and an indigo in dark mode; light mode follows Microsoft Teams, dark
851
+ mode a neutral near-black.
851
852
  Light/dark **mode** is an orthogonal, **per-browser** choice in the same menu
852
853
  (default dark; stored in `localStorage`, applied before first paint) and needs no
853
854
  endpoint — the accent is the instance's shared identity, the mode is personal),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "shadok-ai",
3
- "version": "0.15.41",
3
+ "version": "0.15.43",
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",
package/public/index.html CHANGED
@@ -16,25 +16,30 @@
16
16
  :root {
17
17
  /* The page the panels sit on: a shade darker than --bg, so the nav and the
18
18
  conversation read as cards (the "sober" pass, 2026-10-28). */
19
- --page: #0d0f14;
20
- --radius: 12px;
21
- --bg: #14161d;
22
- --bg-raised: #1b1e27;
23
- --bg-inset: #0e1015;
24
- --line: #2a2e3b;
25
- --text: #d8dae2;
26
- --text-dim: #7d8293;
27
- --amber: #f0a848;
19
+ --page: #0c0c0e;
20
+ --radius: 8px;
21
+ --bg: #131315;
22
+ --bg-raised: #1c1c1f;
23
+ --bg-inset: #0e0e10;
24
+ --line: #2a2a2e;
25
+ --text: #ececef;
26
+ --text-dim: #a1a1aa;
27
+ /* The token keeps its historical name; the default accent is the Shadok
28
+ indigo now, not amber (see the light block for its Teams purple twin). */
29
+ --amber: #7b93ff;
28
30
  /* Accent tints derive from --amber via color-mix, so a palette only has to
29
31
  redefine --amber (and --phosphor) and every tinted surface follows. */
30
32
  --amber-soft: color-mix(in srgb, var(--amber) 12%, transparent);
31
- --phosphor: #ffb45c;
32
- --ok: #7fc97f;
33
- --err: #e07a6a;
33
+ --phosphor: #9aadff;
34
+ /* Text set ON an accent fill (primary button, banners). Dark on the pale
35
+ dark-mode accents, white on the deep light-mode purple. */
36
+ --on-accent: #0b0b0d;
37
+ --ok: #4ade80;
38
+ --err: #f87171;
34
39
  /* Floating menus (⋯, version, tab, palette) sit a touch darker than a raised
35
40
  panel for depth; kept as tokens so light mode can flip them. */
36
- --menu-bg: #14140f;
37
- --menu-hover: #26261c;
41
+ --menu-bg: #18181b;
42
+ --menu-hover: #26262a;
38
43
  --mono: ui-monospace, "SF Mono", Menlo, Consolas, monospace;
39
44
  /* One sans for the whole interface; the mono is kept for what IS code —
40
45
  code blocks, session ids, the terminal. */
@@ -48,24 +53,33 @@
48
53
  a pale amber on white. Accent-tinted surfaces (--amber-soft) derive from
49
54
  --amber and follow on their own. */
50
55
  :root[data-mode="light"] {
51
- --page: #eef0f4;
52
- --bg: #f7f8fa;
56
+ /* Light mode follows Microsoft Teams: grey chrome, white cards, Segoe UI,
57
+ tighter corners, and the Teams purple as the default accent. */
58
+ --page: #ebebeb;
59
+ --bg: #f5f5f5;
53
60
  --bg-raised: #ffffff;
54
- --bg-inset: #e5e8ee;
55
- --line: #d2d7e0;
56
- --text: #1b2030;
57
- --text-dim: #5c6577;
58
- --ok: #2f9e44;
59
- --err: #cf4436;
60
- --phosphor: #c77800;
61
+ --bg-inset: #f0f0f0;
62
+ --line: #e0e0e0;
63
+ --text: #242424;
64
+ --text-dim: #616161;
65
+ --ok: #13a10e;
66
+ --err: #c4314b;
67
+ /* Darkened from the accent itself, so it reads on white whatever palette. */
68
+ --phosphor: color-mix(in srgb, var(--amber) 75%, #000);
69
+ --on-accent: #ffffff;
61
70
  --menu-bg: #ffffff;
62
- --menu-hover: #eceff5;
71
+ --menu-hover: #f0f0f0;
72
+ --radius: 6px;
73
+ --sans: "Segoe UI", "Segoe UI Web (West European)", -apple-system, BlinkMacSystemFont, "Inter", system-ui, Roboto, "Helvetica Neue", Arial, sans-serif;
63
74
  }
75
+ :root[data-mode="light"]:not([data-theme]) { --amber: #5b5fc7; }
64
76
  /* Colour palettes, chosen per instance from the ⋯ menu and stored server-side
65
77
  (like the instance name). Only the accent family changes; the dark base
66
78
  stays constant for readability, and every tinted surface follows because it
67
79
  derives from --amber (color-mix). The default (amber) needs no attribute.
68
80
  Accents are 6-digit hex on purpose — the favicon appends an alpha byte. */
81
+ /* A palette's pale accent would vanish under white text: keep dark text on it. */
82
+ :root[data-mode="light"][data-theme] { --on-accent: #0b0b0d; }
69
83
  :root[data-theme="emerald"] { --amber: #45c58a; --phosphor: #6fe3ab; }
70
84
  :root[data-theme="azure"] { --amber: #5aa9f0; --phosphor: #86c4ff; }
71
85
  :root[data-theme="violet"] { --amber: #b98cf0; --phosphor: #d0b0ff; }
@@ -166,8 +180,14 @@
166
180
  font-weight: 700;
167
181
  color: var(--text);
168
182
  }
169
- header .brand::before { color: var(--amber); }
170
- header .brand::before { content: "◆ "; }
183
+ /* The mark: the Shadok's line drawing on an accent tile. */
184
+ header .brand { display: inline-flex; align-items: center; gap: 4px; }
185
+ header .brand-logo {
186
+ width: 26px; height: 26px; flex: none; border-radius: 6px;
187
+ background: var(--amber); color: var(--on-accent);
188
+ display: grid; place-items: center; margin-right: 4px;
189
+ }
190
+ header .brand-logo svg { width: 19px; height: 19px; }
171
191
  /* The brand name doubles as the cockpit's editable title: click to rename,
172
192
  Enter/blur commits, Escape cancels. A dashed outline + pencil on hover make
173
193
  the affordance obvious; the outline turns solid amber while editing. */
@@ -303,7 +323,7 @@
303
323
  }
304
324
  #updateBanner {
305
325
  position: fixed; top: 0; left: 0; right: 0; z-index: 1000;
306
- background: var(--amber); color: #1a1200;
326
+ background: var(--amber); color: var(--on-accent);
307
327
  font-family: var(--sans); font-size: 12px; text-align: center;
308
328
  padding: 6px 12px;
309
329
  }
@@ -311,7 +331,7 @@
311
331
  update banner; its own id so the two never share styling by accident. */
312
332
  #expiryBanner {
313
333
  position: fixed; top: 0; left: 0; right: 0; z-index: 1000;
314
- background: var(--amber); color: #1a1200;
334
+ background: var(--amber); color: var(--on-accent);
315
335
  font-family: var(--sans); font-size: 12px;
316
336
  padding: 6px 12px;
317
337
  display: flex; gap: 10px; align-items: center; justify-content: center; flex-wrap: wrap;
@@ -460,8 +480,8 @@
460
480
  #moreMenu .mm-mode button.active { border-color: var(--amber); color: var(--amber); background: color-mix(in srgb, var(--amber) 12%, transparent); }
461
481
  button:hover { background: var(--bg-inset); border-color: var(--line); color: var(--text); }
462
482
  button:focus-visible { outline: 2px solid var(--amber); outline-offset: 2px; }
463
- button.primary { background: var(--amber); border-color: var(--amber); color: #1a1205; font-weight: 600; }
464
- button.primary:hover { background: var(--amber); border-color: var(--amber); color: #1a1205; }
483
+ button.primary { background: var(--amber); border-color: var(--amber); color: var(--on-accent); font-weight: 600; }
484
+ button.primary:hover { background: var(--amber); border-color: var(--amber); color: var(--on-accent); }
465
485
  button.primary:hover { filter: brightness(1.1); }
466
486
  button:disabled { opacity: 0.4; cursor: default; }
467
487
 
@@ -2100,6 +2120,165 @@
2100
2120
  .sec-reload span { flex: 1; }
2101
2121
  .sec-reload button { flex: none; padding: 5px 11px; border-radius: 6px; border: 1px solid var(--amber); background: none; color: var(--amber); cursor: pointer; font: inherit; font-size: 12px; }
2102
2122
  .sec-reload button:hover { background: color-mix(in srgb, var(--amber) 15%, transparent); }
2123
+
2124
+ /* ══ Teams layer ═════════════════════════════════════════════════════════
2125
+ The cockpit's shape follows Microsoft Teams: a title bar with search, an app
2126
+ bar on the left, every group as a collapsible section of the agents pane,
2127
+ round avatars with a presence dot, a channel header with tabs, bubbles, and
2128
+ dialogs as approval cards. Colours stay tokens, so light mode reads as Teams
2129
+ and dark mode as the neutral near-black. Placed LAST so it wins on equal
2130
+ specificity; the phone layout (≤640px) keeps its sheet and its ⋯ menu. */
2131
+ :root { --mine: color-mix(in srgb, var(--amber) 20%, var(--bg-raised)); --pane-main: color-mix(in srgb, var(--bg) 55%, var(--bg-raised)); }
2132
+ :root[data-mode="light"] { --mine: color-mix(in srgb, var(--amber) 13%, #fff); }
2133
+
2134
+ /* Title bar */
2135
+ header { padding: 8px 14px; gap: 14px; }
2136
+ #agentSearch {
2137
+ flex: 0 1 520px; margin: 0 auto; display: flex; align-items: center; gap: 8px;
2138
+ height: 32px; padding: 0 12px; border-radius: 6px; cursor: text;
2139
+ background: var(--bg-raised); color: var(--text-dim); border: 1px solid var(--line);
2140
+ }
2141
+ #agentSearch:focus-within { border-color: var(--amber); }
2142
+ #agentSearch input { flex: 1; min-width: 0; border: 0; outline: none; background: none; color: var(--text); font: inherit; font-size: 13.5px; padding: 0; }
2143
+ #agentSearch input::placeholder { color: var(--text-dim); }
2144
+ #agentSearch + .spacer { display: none; }
2145
+ @media (max-width: 640px) { #agentSearch { display: none; } #agentSearch + .spacer { display: block; } }
2146
+ @media (min-width: 641px) { header { flex-wrap: nowrap; } }
2147
+ /* The star pill is the widest tool; under 1100px it lives in the ⋯ menu. */
2148
+ @media (min-width: 641px) and (max-width: 1100px) { #starBtn { display: none; } #moreMenu #starMenuItem { display: block; } }
2149
+ /* Close buttons appear on hover, as in Teams' lists. */
2150
+ .tab .close, .group-head .gclose, .group-head .gadd { visibility: hidden; }
2151
+ .tab:hover .close, .group-head:hover .gclose, .group-head:hover .gadd { visibility: visible; }
2152
+ .group-head .gadd { flex: none; font-size: 14px; padding: 0 4px; line-height: 1; }
2153
+ .group-head .gadd:hover { color: var(--amber); }
2154
+ .group-head .gicon { cursor: pointer; }
2155
+ .group-head .caret { font-size: 10px; width: 10px; color: var(--text-dim); }
2156
+
2157
+ /* Presence: Teams' four dots. Working = busy (red), ready = available (green),
2158
+ dead = offline (hollow). */
2159
+ .led.on { background: var(--ok); box-shadow: none; }
2160
+ .led.busy { background: var(--err); box-shadow: none; }
2161
+ .led.off { background: transparent; box-shadow: inset 0 0 0 2px var(--text-dim); }
2162
+ .tav {
2163
+ position: relative; flex: none; width: 32px; height: 32px; border-radius: 50%;
2164
+ display: grid; place-items: center; background: var(--ac, var(--amber));
2165
+ color: #fff; font-size: 12px; font-weight: 600; letter-spacing: 0;
2166
+ }
2167
+ .tav .led { position: absolute; right: -2px; bottom: -2px; width: 12px; height: 12px; margin: 0; border: 2px solid var(--bg); }
2168
+ .ab-txt, .ct-txt { display: none; }
2169
+ #appBar { display: none; }
2170
+ @media (max-width: 640px) {
2171
+ /* A phone's single header row has no room for the mark beside the name,
2172
+ the version and the dials; the favicon carries it there. */
2173
+ header .brand-logo { display: none; }
2174
+ }
2175
+
2176
+ /* Agents list: avatar on the left, two rows of text beside it. */
2177
+ .tab {
2178
+ display: grid; grid-template-columns: 32px minmax(0, 1fr); column-gap: 10px; row-gap: 1px;
2179
+ align-items: center; padding: 7px 8px 9px; border-left: 0; border-radius: 6px; color: var(--text);
2180
+ }
2181
+ .tab > .tav { grid-row: 1 / 3; }
2182
+ .tab-row1, .tab-row2 { grid-column: 2; min-width: 0; }
2183
+ .tab .ctxbar { left: 50px; right: 8px; }
2184
+ .tab .lastat, .tab .ctxnum { font-size: 11px; color: var(--text-dim); }
2185
+ .tab.active { border-left: 0; border-radius: 6px; box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); background: var(--bg-raised); font-weight: 600; }
2186
+ :root:not([data-mode="light"]) .tab.active { background: var(--bg-inset); box-shadow: inset 0 0 0 1px var(--line); }
2187
+ .tab.active .tav .led { border-color: var(--bg-raised); }
2188
+ .tab.unread { border-left: 0; border-radius: 6px; font-weight: 700; }
2189
+ .tab.filtered-out { display: none !important; }
2190
+ body.filtering .group.collapsed .group-body { display: flex !important; }
2191
+ .group-head { gap: 10px; padding: 8px; font-size: 14px; color: var(--text); border-radius: 6px; }
2192
+ .group-head .gicon {
2193
+ flex: none; width: 30px; height: 30px; border-radius: 6px; display: grid; place-items: center;
2194
+ background: color-mix(in srgb, var(--amber) 16%, var(--bg-raised)); font-size: 16px; line-height: 1;
2195
+ }
2196
+ .group-head .gicon:empty { display: none; }
2197
+ .group-body { padding-left: 20px; }
2198
+
2199
+ @media (min-width: 641px) {
2200
+ main { padding: 0; gap: 0; }
2201
+ /* App bar */
2202
+ #appBar { width: 68px; flex: none; display: flex; flex-direction: column; align-items: center; gap: 2px; padding: 4px 0 12px; background: var(--page); overflow-y: auto; }
2203
+ #appTools { display: contents; }
2204
+ #appBar button {
2205
+ position: relative; width: 62px; padding: 8px 0 6px; display: flex; flex-direction: column; align-items: center; gap: 3px;
2206
+ border: 0; border-radius: 6px; background: none; color: var(--text-dim); font-size: 10.5px; line-height: 1.1;
2207
+ }
2208
+ #appBar button:hover { color: var(--amber); background: none; }
2209
+ #appBar button.on { color: var(--amber); font-weight: 600; }
2210
+ #appBar button.on::before { content: ""; position: absolute; left: -2px; top: 10px; bottom: 10px; width: 3px; border-radius: 2px; background: var(--amber); }
2211
+ #appBar button[hidden] { display: none; }
2212
+ #appBar svg { width: 20px !important; height: 20px !important; vertical-align: 0 !important; }
2213
+ #appBar .ab-txt { display: block; }
2214
+ #appBar .ab-n, #appBar .sec-badge {
2215
+ position: absolute; top: 3px; right: 12px; min-width: 16px; height: 16px; padding: 0 4px; border-radius: 8px;
2216
+ background: var(--err); color: #fff; font-size: 10px; font-weight: 600; line-height: 16px; text-align: center;
2217
+ }
2218
+ #appBar .ab-n[hidden] { display: none; }
2219
+
2220
+ /* Every group is a section of the agents pane; the rail goes. */
2221
+ body.has-groups #groupRail { display: none; }
2222
+ #tabbar, body.has-groups #tabbar {
2223
+ width: 300px; border: 0; border-radius: 8px 0 0 0; background: var(--bg); padding: 12px 8px;
2224
+ }
2225
+ #tabbar .side-label { font-size: 18px; font-weight: 700; padding: 4px 8px 12px; }
2226
+ body.has-groups #tabbar .side-label .sl-all { display: inline; }
2227
+ #tabbar .side-label .sl-view, #viewEmpty { display: none !important; }
2228
+ #tabbar .group-head { display: flex; }
2229
+ #tabbar .group { margin-top: 6px; }
2230
+ #tabbar .group-body { padding-left: 20px; }
2231
+ #tabbar .group.collapsed .group-body { display: none; }
2232
+ #tabbar:not([data-view="ungrouped"]) #ungrouped,
2233
+ #tabbar[data-view="ungrouped"] #groups { display: flex; flex-direction: column; }
2234
+ #tabbar .group:not(.in-view) { display: flex; }
2235
+ #newGroupBare, body.has-groups #newGroupBare { display: block; }
2236
+
2237
+ /* Conversation */
2238
+ .chat-pane { border: 0; border-radius: 0; background: var(--pane-main); }
2239
+ #machine, #diffpanel { border-radius: 0; border-top: 0; border-bottom: 0; border-right: 0; }
2240
+ }
2241
+
2242
+ /* Channel header: avatar, name, status, then tabs. */
2243
+ #chanBar { gap: 12px; padding: 0 20px; min-height: 58px; background: var(--bg-raised); border-bottom: 1px solid var(--line); }
2244
+ #chanBar .tav { width: 36px; height: 36px; font-size: 13px; }
2245
+ #chanBar #chanBarName { font-size: 16px; font-weight: 700; }
2246
+ #chanBar .gauge .label { display: none; }
2247
+ #chanBar .gauge .value { color: var(--text-dim); font-size: 12px; }
2248
+ #chanBar .gauge:nth-of-type(3) { display: none; } /* the session id: in the tooltip of the menu */
2249
+ .chan-tabs { display: flex; align-self: stretch; gap: 2px; margin-left: 10px; }
2250
+ .chan-tabs button, #chanBar .chan-actions #chanCron, #chanBar .chan-actions #chanTerm {
2251
+ position: relative; align-self: stretch; display: inline-flex; align-items: center; gap: 6px;
2252
+ border: 0; border-radius: 0; background: none; padding: 0 10px; color: var(--text-dim); font-size: 14px;
2253
+ }
2254
+ .chan-tabs button:hover, #chanBar .chan-actions #chanCron:hover, #chanBar .chan-actions #chanTerm:hover { color: var(--text); background: none; }
2255
+ .chan-tabs button.on, #chanBar .chan-actions #chanTerm.on { color: var(--text); font-weight: 600; background: none; border: 0; }
2256
+ .chan-tabs button.on::after, #chanBar .chan-actions #chanTerm.on::after {
2257
+ content: ""; position: absolute; left: 10px; right: 10px; bottom: 0; height: 3px; border-radius: 3px 3px 0 0; background: var(--amber);
2258
+ }
2259
+ #chanBar .chan-actions { align-self: stretch; }
2260
+ #chanBar .chan-actions .ct-txt { display: inline; }
2261
+ #chanBar .chan-actions #chanStop, #chanBar .chan-actions #chanMenu { align-self: center; }
2262
+ @media (max-width: 640px) {
2263
+ #chanBar { min-height: 0; padding: 8px 12px; }
2264
+ #chanBar .tav, .chan-tabs, #chanBar .chan-actions .ct-txt { display: none; }
2265
+ }
2266
+
2267
+ /* Messages: Teams bubbles. */
2268
+ .bubble { border-radius: 6px; }
2269
+ .turn.user .bubble { background: var(--mine); border: 0; }
2270
+ .turn.claude .bubble { background: var(--bg-raised); border: 0; box-shadow: 0 1px 2px rgba(0, 0, 0, 0.12); }
2271
+ .turn.dialog .bubble {
2272
+ background: var(--bg-raised); border: 0; border-top: 4px solid var(--amber); border-radius: 6px;
2273
+ box-shadow: 0 0 0 1px var(--line), 0 2px 6px rgba(0, 0, 0, 0.08); padding-top: 12px;
2274
+ }
2275
+ .turn.dialog .bubble::before { content: "Approval requested"; font-size: 12px; font-weight: 600; color: var(--phosphor); margin-bottom: -6px; }
2276
+ .turn.dialog.spent .bubble { border-top-color: var(--line); }
2277
+
2278
+ /* Composer: a card with the accent underline. */
2279
+ #composer { background: transparent; padding: 8px 20px calc(16px + env(safe-area-inset-bottom, 0px)); }
2280
+ #composer textarea { background: var(--bg-raised); border: 1px solid var(--line); border-bottom: 2px solid var(--amber); border-radius: 6px; }
2281
+ #composer textarea:focus-visible { box-shadow: none; border-color: var(--line); border-bottom-color: var(--amber); }
2103
2282
  </style>
2104
2283
  <!-- Light/dark mode is a per-browser choice (localStorage). Set it BEFORE the
2105
2284
  body paints — here in <head> — so a light-preferring viewer never sees a
@@ -2111,7 +2290,9 @@
2111
2290
  <body>
2112
2291
 
2113
2292
  <header>
2114
- <span class="brand"><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>
2293
+ <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>
2294
+ <!-- Teams-style search: filters the agents list as you type (desktop). -->
2295
+ <label id="agentSearch"><svg viewBox="0 0 24 24" width="14" height="14" aria-hidden="true"><circle cx="10.5" cy="10.5" r="6" fill="none" stroke="currentColor" stroke-width="1.8"/><path d="M15 15l5 5" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"/></svg><input id="agentFilter" type="search" placeholder="Search agents" autocomplete="off" spellcheck="false" aria-label="Search agents"></label>
2115
2296
  <span class="spacer"></span>
2116
2297
  <div class="gauge quota" id="quota5h" data-window="5h" title="5h rolling limit">
2117
2298
  <svg class="dial" viewBox="0 0 56 44" role="img" aria-label="5h rolling limit">
@@ -2137,15 +2318,15 @@
2137
2318
  against the right edge (the 🔑 was there, 16px from it — out of sight at
2138
2319
  the slightest zoom). -->
2139
2320
  <div class="hdr-tools">
2140
- <button id="secretsBtn" class="icon" title="Repo secrets (injected as env)" aria-label="Secrets"><svg viewBox="0 0 24 24" width="15" height="15" style="vertical-align:-2px" aria-hidden="true"><circle cx="8.5" cy="8.5" r="3.5" fill="none" stroke="currentColor" stroke-width="1.8"/><path d="M11 11l7.5 7.5M15.5 15.5l1.8-1.8M17.8 17.8l1.7-1.7" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"/></svg></button>
2141
- <button id="ledgerBtn" class="icon" title="Shared ledger — what agents have resolved / decided" aria-label="Ledger"><svg viewBox="0 0 24 24" width="15" height="15" style="vertical-align:-2px" aria-hidden="true"><rect x="5" y="3.5" width="14" height="17" rx="2" fill="none" stroke="currentColor" stroke-width="1.8"/><path d="M8.5 8h7M8.5 12h7M8.5 16h4" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"/></svg></button>
2142
- <button id="profilesBtn" class="icon" title="Agent profiles (role + guardrails + secrets)" aria-label="Profiles"><svg viewBox="0 0 24 24" width="15" height="15" style="vertical-align:-2px" aria-hidden="true"><circle cx="12" cy="8.5" r="3.6" fill="none" stroke="currentColor" stroke-width="1.8"/><path d="M5.5 19.5a6.5 6.5 0 0 1 13 0" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"/></svg></button>
2321
+ <button id="secretsBtn" class="icon" title="Repo secrets (injected as env)" aria-label="Secrets"><svg viewBox="0 0 24 24" width="15" height="15" style="vertical-align:-2px" aria-hidden="true"><circle cx="8.5" cy="8.5" r="3.5" fill="none" stroke="currentColor" stroke-width="1.8"/><path d="M11 11l7.5 7.5M15.5 15.5l1.8-1.8M17.8 17.8l1.7-1.7" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"/></svg><span class="ab-txt">Secrets</span></button>
2322
+ <button id="ledgerBtn" class="icon" title="Shared ledger — what agents have resolved / decided" aria-label="Ledger"><svg viewBox="0 0 24 24" width="15" height="15" style="vertical-align:-2px" aria-hidden="true"><rect x="5" y="3.5" width="14" height="17" rx="2" fill="none" stroke="currentColor" stroke-width="1.8"/><path d="M8.5 8h7M8.5 12h7M8.5 16h4" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"/></svg><span class="ab-txt">Ledger</span></button>
2323
+ <button id="profilesBtn" class="icon" title="Agent profiles (role + guardrails + secrets)" aria-label="Profiles"><svg viewBox="0 0 24 24" width="15" height="15" style="vertical-align:-2px" aria-hidden="true"><circle cx="12" cy="8.5" r="3.6" fill="none" stroke="currentColor" stroke-width="1.8"/><path d="M5.5 19.5a6.5 6.5 0 0 1 13 0" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"/></svg><span class="ab-txt">Profiles</span></button>
2143
2324
  <!-- Security: what was done to accounts and second factors, and "this wasn't
2144
2325
  me". Kept out of the ⋯ overflow on a phone: its badge is the point. -->
2145
- <button id="securityBtn" class="icon" title="Security — what happened to your account" aria-label="Security" hidden><svg viewBox="0 0 24 24" width="15" height="15" style="vertical-align:-2px" aria-hidden="true"><path d="M12 3l7 3v5c0 4.5-3 8.2-7 10-4-1.8-7-5.5-7-10V6l7-3z" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linejoin="round"/></svg><span id="securityBadge" class="sec-badge" hidden></span></button>
2146
- <button id="usersBtn" class="icon" title="Users — accounts for this instance" aria-label="Users" hidden><svg viewBox="0 0 24 24" width="15" height="15" style="vertical-align:-2px" aria-hidden="true"><circle cx="9" cy="8.5" r="3.2" fill="none" stroke="currentColor" stroke-width="1.8"/><path d="M3 19a6 6 0 0 1 12 0" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"/><path d="M16 6.2a3.2 3.2 0 0 1 0 6.1M17.5 19a6 6 0 0 0-2.2-4.6" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"/></svg></button>
2147
- <button id="telegramBtn" class="icon" title="Configure the Telegram bot (token, on/off, allowed chats)"><svg viewBox="0 0 24 24" width="15" height="15" style="vertical-align:-2px" aria-hidden="true"><path fill="#26A5E4" d="M11.944 0A12 12 0 0 0 0 12a12 12 0 0 0 12 12 12 12 0 0 0 12-12A12 12 0 0 0 12 0a12 12 0 0 0-.056 0zm4.962 7.224c.1-.002.321.023.465.14a.506.506 0 0 1 .171.325c.016.093.036.306.02.472-.18 1.898-.962 6.502-1.36 8.627-.168.9-.499 1.201-.82 1.23-.696.065-1.225-.46-1.9-.902-1.056-.693-1.653-1.124-2.678-1.8-1.185-.78-.417-1.21.258-1.91.177-.184 3.247-2.977 3.307-3.23.007-.032.014-.15-.056-.212s-.174-.041-.249-.024c-.106.024-1.793 1.14-5.061 3.345-.48.33-.913.49-1.302.48-.428-.008-1.252-.241-1.865-.44-.752-.245-1.349-.374-1.297-.789.027-.216.325-.437.893-.663 3.498-1.524 5.83-2.529 6.998-3.014 3.332-1.386 4.025-1.627 4.476-1.635z"/></svg></button>
2148
- <button id="muteNotif" class="icon" title="Mute the notification sound" aria-label="Notification sound"><svg viewBox="0 0 24 24" width="15" height="15" style="vertical-align:-2px" aria-hidden="true"><path d="M6 16.5h12c-1.3-1.1-2-2.7-2-4.5v-1.2a4 4 0 0 0-8 0v1.2c0 1.8-.7 3.4-2 4.5z" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linejoin="round"/><path d="M10.4 19a1.7 1.7 0 0 0 3.2 0" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"/><line class="mute-slash" x1="4.5" y1="4.5" x2="19.5" y2="19.5" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"/></svg></button>
2326
+ <button id="securityBtn" class="icon" title="Security — what happened to your account" aria-label="Security" hidden><svg viewBox="0 0 24 24" width="15" height="15" style="vertical-align:-2px" aria-hidden="true"><path d="M12 3l7 3v5c0 4.5-3 8.2-7 10-4-1.8-7-5.5-7-10V6l7-3z" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linejoin="round"/></svg><span id="securityBadge" class="sec-badge" hidden></span><span class="ab-txt">Security</span></button>
2327
+ <button id="usersBtn" class="icon" title="Users — accounts for this instance" aria-label="Users" hidden><svg viewBox="0 0 24 24" width="15" height="15" style="vertical-align:-2px" aria-hidden="true"><circle cx="9" cy="8.5" r="3.2" fill="none" stroke="currentColor" stroke-width="1.8"/><path d="M3 19a6 6 0 0 1 12 0" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"/><path d="M16 6.2a3.2 3.2 0 0 1 0 6.1M17.5 19a6 6 0 0 0-2.2-4.6" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"/></svg><span class="ab-txt">Users</span></button>
2328
+ <button id="telegramBtn" class="icon" title="Configure the Telegram bot (token, on/off, allowed chats)"><svg viewBox="0 0 24 24" width="15" height="15" style="vertical-align:-2px" aria-hidden="true"><path fill="#26A5E4" d="M11.944 0A12 12 0 0 0 0 12a12 12 0 0 0 12 12 12 12 0 0 0 12-12A12 12 0 0 0 12 0a12 12 0 0 0-.056 0zm4.962 7.224c.1-.002.321.023.465.14a.506.506 0 0 1 .171.325c.016.093.036.306.02.472-.18 1.898-.962 6.502-1.36 8.627-.168.9-.499 1.201-.82 1.23-.696.065-1.225-.46-1.9-.902-1.056-.693-1.653-1.124-2.678-1.8-1.185-.78-.417-1.21.258-1.91.177-.184 3.247-2.977 3.307-3.23.007-.032.014-.15-.056-.212s-.174-.041-.249-.024c-.106.024-1.793 1.14-5.061 3.345-.48.33-.913.49-1.302.48-.428-.008-1.252-.241-1.865-.44-.752-.245-1.349-.374-1.297-.789.027-.216.325-.437.893-.663 3.498-1.524 5.83-2.529 6.998-3.014 3.332-1.386 4.025-1.627 4.476-1.635z"/></svg><span class="ab-txt">Telegram</span></button>
2329
+ <button id="muteNotif" class="icon" title="Mute the notification sound" aria-label="Notification sound"><svg viewBox="0 0 24 24" width="15" height="15" style="vertical-align:-2px" aria-hidden="true"><path d="M6 16.5h12c-1.3-1.1-2-2.7-2-4.5v-1.2a4 4 0 0 0-8 0v1.2c0 1.8-.7 3.4-2 4.5z" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linejoin="round"/><path d="M10.4 19a1.7 1.7 0 0 0 3.2 0" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"/><line class="mute-slash" x1="4.5" y1="4.5" x2="19.5" y2="19.5" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"/></svg><span class="ab-txt">Sound</span></button>
2149
2330
  <div class="more-wrap">
2150
2331
  <button id="moreBtn" class="icon" title="More">⋯</button>
2151
2332
  <div id="moreMenu" hidden>
@@ -2643,6 +2824,14 @@
2643
2824
  </div>
2644
2825
 
2645
2826
  <main>
2827
+ <!-- The app bar (desktop): Teams' left column. Activity jumps to an agent that
2828
+ waits for you; the global tools are MOVED here by reflowHeaderTools (the
2829
+ real buttons, handlers included) and back to the header/⋯ on a phone. -->
2830
+ <nav id="appBar" aria-label="Apps">
2831
+ <button id="abActivity" type="button" title="Jump to an agent waiting for you"><svg viewBox="0 0 24 24" width="20" height="20" aria-hidden="true"><path fill="none" stroke="currentColor" stroke-width="1.6" stroke-linejoin="round" d="M6 16V11a6 6 0 1 1 12 0v5l1.5 2h-15zM10 20.5a2 2 0 0 0 4 0"/></svg><span class="ab-txt">Activity</span><span id="abActivityN" class="ab-n" hidden></span></button>
2832
+ <button id="abAgents" type="button" class="on" title="Agents"><svg viewBox="0 0 24 24" width="20" height="20" aria-hidden="true"><g fill="none" stroke="currentColor" stroke-width="1.6"><circle cx="9" cy="9" r="3.2"/><circle cx="17" cy="10" r="2.4"/><path d="M3.5 19c.6-3 2.8-4.6 5.5-4.6s4.9 1.6 5.5 4.6M14.5 15c2.6-.6 5 .6 5.8 3.4"/></g></svg><span class="ab-txt">Agents</span></button>
2833
+ <div id="appTools"></div>
2834
+ </nav>
2646
2835
  <!-- The groups column (desktop only): a VIEW over the agents column beside it,
2647
2836
  which still holds the groups, their order and their members — that is what
2648
2837
  is persisted. Picking a group narrows the agents column to it. On a phone
@@ -2688,6 +2877,7 @@
2688
2877
  modes — the terminal is just another view of the same channel, not a
2689
2878
  separate place, so its actions have no reason to appear with it. -->
2690
2879
  <div id="chanBar" hidden>
2880
+ <span class="tav" id="chanAvatar" aria-hidden="true"></span>
2691
2881
  <span class="label" id="chanBarName"></span>
2692
2882
  <!-- Mobile: the sidebar is hidden → this button opens the very same rich
2693
2883
  agent tabs (LED, state, background-work hatch, context bar) as a bottom
@@ -2711,11 +2901,15 @@
2711
2901
  <span class="label">Branch</span>
2712
2902
  <span class="value" id="branchView">—</span>
2713
2903
  </div>
2904
+ <span class="chan-tabs" role="tablist">
2905
+ <button id="chanChat" type="button" role="tab">Chat</button>
2906
+ <button id="chanDiff" type="button" role="tab">Changes</button>
2907
+ </span>
2714
2908
  <span class="chan-actions">
2715
2909
  <button id="chanUpdate" class="icon" title="Claude Code updated — reload this agent to apply it" aria-label="Reload to apply update" hidden>↻ update</button>
2716
- <button id="chanCron" class="icon" title="Scheduled prompts for this agent (monitoring / reporting)" aria-label="Schedule"><svg viewBox="0 0 24 24" width="15" height="15" style="vertical-align:-2px" aria-hidden="true"><circle cx="12" cy="13" r="6.3" fill="none" stroke="currentColor" stroke-width="1.8"/><path d="M12 9.8V13l2.2 1.4" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"/><path d="M4.8 6.6 7.2 4.6M19.2 6.6 16.8 4.6" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"/></svg></button>
2910
+ <button id="chanCron" class="icon" title="Scheduled prompts for this agent (monitoring / reporting)" aria-label="Schedule"><svg viewBox="0 0 24 24" width="15" height="15" style="vertical-align:-2px" aria-hidden="true"><circle cx="12" cy="13" r="6.3" fill="none" stroke="currentColor" stroke-width="1.8"/><path d="M12 9.8V13l2.2 1.4" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"/><path d="M4.8 6.6 7.2 4.6M19.2 6.6 16.8 4.6" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"/></svg><span class="ct-txt">Schedule</span></button>
2717
2911
  <button id="chanStop" class="icon stop" title="End session" aria-label="End session"><svg viewBox="0 0 24 24" width="15" height="15" style="vertical-align:-2px" aria-hidden="true"><path d="M12 4v7" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"/><path d="M8.5 6.8a6 6 0 1 0 7 0" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"/></svg></button>
2718
- <button id="chanTerm" class="icon" title="Terminal — the raw TUI for this agent" aria-label="Terminal"><svg viewBox="0 0 24 24" width="15" height="15" style="vertical-align:-2px" aria-hidden="true"><rect x="2.5" y="4" width="19" height="16" rx="2.5" fill="none" stroke="currentColor" stroke-width="1.8"/><path d="M6 9.5l3 2.5-3 2.5" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"/><path d="M12.5 15h5" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"/></svg></button>
2912
+ <button id="chanTerm" class="icon" title="Terminal — the raw TUI for this agent" aria-label="Terminal"><svg viewBox="0 0 24 24" width="15" height="15" style="vertical-align:-2px" aria-hidden="true"><rect x="2.5" y="4" width="19" height="16" rx="2.5" fill="none" stroke="currentColor" stroke-width="1.8"/><path d="M6 9.5l3 2.5-3 2.5" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"/><path d="M12.5 15h5" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"/></svg><span class="ct-txt">Terminal</span></button>
2719
2913
  <!-- The per-agent menu (rename, profile, mirror, report to, close). It's
2720
2914
  the same menu a right-click / long-press on a tab opens — but with the
2721
2915
  sidebar hidden on a phone there was no way to reach it, so it lives
@@ -3124,6 +3318,23 @@
3124
3318
  * Delegating is this product's central action; it must not cost the user
3125
3319
  * their place in the conversation they are having.
3126
3320
  */
3321
+ // ── Teams layer: avatars ───────────────────────────────────────────────────
3322
+ // Avatar colours: the Teams persona palette. Picked from the NAME, so an agent
3323
+ // keeps its colour across reloads and devices without storing anything.
3324
+ const AVATAR_COLORS = ["#5b5fc7", "#0078d4", "#038387", "#498205", "#c4314b", "#ca5010", "#8764b8", "#986f0b", "#00727c", "#b146c2"];
3325
+ function avatarInitials(name) {
3326
+ const words = String(name || "").replace(/[^\p{L}\p{N}]+/gu, " ").trim().split(" ").filter(Boolean);
3327
+ if (!words.length) return "?";
3328
+ return (words.length > 1 ? words[0][0] + words[1][0] : words[0].slice(0, 2)).toUpperCase();
3329
+ }
3330
+ function paintTabAvatar(avatar, label, name) {
3331
+ let h = 0;
3332
+ for (const c of String(name || "")) h = (h * 31 + c.codePointAt(0)) >>> 0;
3333
+ avatar.style.setProperty("--ac", AVATAR_COLORS[h % AVATAR_COLORS.length]);
3334
+ const text = avatarInitials(name);
3335
+ if (label.textContent !== text) label.textContent = text;
3336
+ }
3337
+
3127
3338
  function createTab(groupId, { focus = true } = {}) {
3128
3339
  seq++;
3129
3340
  const el = document.createElement("div");
@@ -3206,13 +3417,22 @@
3206
3417
  // Name + time stacked (the time UNDER the title, so as not to truncate it).
3207
3418
  // Two rows: row 1 = led + name + icons; row 2 FULL WIDTH = "xxx ago" on the
3208
3419
  // left, context % hard right (no longer hidden behind the icons).
3420
+ // Teams-style avatar: initials on a colour derived from the name, with the
3421
+ // state LED riding its corner as a presence dot. Follows renames.
3422
+ const avatar = document.createElement("span");
3423
+ avatar.className = "tav";
3424
+ const initials = document.createElement("span");
3425
+ avatar.append(initials, led);
3426
+ const paintAvatar = () => paintTabAvatar(avatar, initials, name.textContent);
3427
+ new MutationObserver(paintAvatar).observe(name, { childList: true, characterData: true, subtree: true });
3428
+ paintAvatar();
3209
3429
  const row1 = document.createElement("div");
3210
3430
  row1.className = "tab-row1";
3211
- row1.append(led, name, timer, mute, cron, tg, shells, monitors, update, close);
3431
+ row1.append(name, timer, mute, cron, tg, shells, monitors, update, close);
3212
3432
  const row2 = document.createElement("div");
3213
3433
  row2.className = "tab-row2";
3214
3434
  row2.append(lastAt, ctxNum);
3215
- btn.append(row1, row2);
3435
+ btn.append(avatar, row1, row2);
3216
3436
  // Context-window usage: a thin progress bar at the bottom of the tab.
3217
3437
  const ctxBar = document.createElement("div");
3218
3438
  ctxBar.className = "ctxbar";
@@ -4165,7 +4385,14 @@
4165
4385
  gclose.className = "gclose";
4166
4386
  gclose.textContent = "×";
4167
4387
  gclose.title = "Ungroup (tabs are kept)";
4168
- head.append(caret, gname, gclose);
4388
+ const gicon = document.createElement("span");
4389
+ gicon.className = "gicon";
4390
+ gicon.title = "Change the group's icon";
4391
+ const gadd = document.createElement("span");
4392
+ gadd.className = "gadd";
4393
+ gadd.textContent = "+";
4394
+ gadd.title = "New agent in this group";
4395
+ head.append(caret, gicon, gname, gadd, gclose);
4169
4396
  const body = document.createElement("div");
4170
4397
  body.className = "group-body";
4171
4398
  el.append(head, body);
@@ -4183,6 +4410,8 @@
4183
4410
  paintCaret();
4184
4411
  head.addEventListener("click", (e) => {
4185
4412
  if (e.target === gclose) return dissolveGroup(group);
4413
+ if (e.target === gicon) { e.stopPropagation(); return openIconPicker(group, gicon); }
4414
+ if (e.target === gadd) { e.stopPropagation(); return openSetup(group.id); }
4186
4415
  el.classList.toggle("collapsed");
4187
4416
  paintCaret();
4188
4417
  persistGroups();
@@ -4285,7 +4514,11 @@
4285
4514
  return window.resolveGroupView(groupView, groups.map((g) => g.id));
4286
4515
  }
4287
4516
  /** The group a new agent goes into: the one on screen, none on a phone. */
4517
+ // The Teams layout has no rail: a group's own + names the group the next
4518
+ // agent goes into, and every other opening of the setup popin clears it.
4519
+ let setupGroup = null;
4288
4520
  function viewGroupId() {
4521
+ if (setupGroup != null) return setupGroup;
4289
4522
  if (!railShown()) return null;
4290
4523
  const v = currentView();
4291
4524
  return v === "ungrouped" ? null : Number(v);
@@ -4320,7 +4553,11 @@
4320
4553
  // No group → no column (invariant: the rail earns its space, see the CSS).
4321
4554
  document.body.classList.toggle("has-groups", groups.length > 0);
4322
4555
  tabbar.dataset.view = v;
4323
- for (const g of groups) g.el.classList.toggle("in-view", String(g.id) === v);
4556
+ for (const g of groups) {
4557
+ g.el.classList.toggle("in-view", String(g.id) === v);
4558
+ const gi = g.headEl.querySelector(".gicon");
4559
+ if (gi && gi.textContent !== g.icon) gi.textContent = g.icon;
4560
+ }
4324
4561
  const g = groups.find((x) => String(x.id) === v);
4325
4562
  $("viewLabel").textContent = g ? `${g.icon} ${g.nameEl.textContent}` : "Ungrouped";
4326
4563
  $("viewEmpty").hidden = tabsInView(v).length > 0 || v === "ungrouped";
@@ -4891,7 +5128,7 @@
4891
5128
  // accent family and everything tinted follows. "amber" is the default and
4892
5129
  // carries NO attribute, so it falls back to :root.
4893
5130
  const THEMES = [
4894
- { key: "amber", label: "Amber", color: "#f0a848" },
5131
+ { key: "amber", label: "Shadok", color: "#7b93ff" }, // the default key keeps its stored name
4895
5132
  { key: "emerald", label: "Emerald", color: "#45c58a" },
4896
5133
  { key: "azure", label: "Azure", color: "#5aa9f0" },
4897
5134
  { key: "violet", label: "Violet", color: "#b98cf0" },
@@ -5349,6 +5586,7 @@
5349
5586
  $("chanBar").hidden = false;
5350
5587
  $("chanBarName").textContent = t.nameEl ? t.nameEl.textContent : "";
5351
5588
  $("chanBarName").title = t.cwd || "";
5589
+ paintTabAvatar($("chanAvatar"), $("chanAvatar"), t.nameEl ? t.nameEl.textContent : "");
5352
5590
  updateSendDisabled(); // t === active here; also honours a pending upload
5353
5591
  thinking.classList.toggle("visible", t.status === "busy");
5354
5592
  // Showing/hiding "Shadok is pumping" changes the transcript area's height:
@@ -6585,7 +6823,8 @@
6585
6823
  * than a stillborn tab — it became the active channel, with its form in place
6586
6824
  * of the conversation you were watching.
6587
6825
  */
6588
- function openSetup() {
6826
+ function openSetup(groupId = null) {
6827
+ setupGroup = groupId;
6589
6828
  renderModelRow();
6590
6829
  // Close the mobile agents sheet first: the setup overlay sits BELOW it in
6591
6830
  // the stack, so leaving the sheet open would hide the form behind it.
@@ -7592,7 +7831,7 @@
7592
7831
  $("setupClose").addEventListener("click", closeSetup);
7593
7832
  $("setupCancel").addEventListener("click", closeSetup);
7594
7833
  $("setupOverlay").addEventListener("click", (e) => { if (e.target === $("setupOverlay")) closeSetup(); });
7595
- $("emptyNewBtn").addEventListener("click", openSetup);
7834
+ $("emptyNewBtn").addEventListener("click", () => openSetup());
7596
7835
  document.querySelectorAll('input[name="mode"]').forEach((r) =>
7597
7836
  r.addEventListener("change", () => {
7598
7837
  const mode = document.querySelector('input[name="mode"]:checked').value;
@@ -8193,17 +8432,21 @@
8193
8432
  // the real button elements, so their click handlers travel with them.
8194
8433
  // Recomputed on load, on resize, and when the instance name changes.
8195
8434
  const OVERFLOW_TOOLS = ["secretsBtn", "ledgerBtn", "profilesBtn", "telegramBtn", "muteNotif"];
8435
+ // Every tool the desktop app bar carries, in its order.
8436
+ const APP_TOOLS = ["profilesBtn", "ledgerBtn", "secretsBtn", "usersBtn", "securityBtn", "telegramBtn", "muteNotif"];
8196
8437
  function reflowHeaderTools() {
8197
8438
  const hdrTools = document.querySelector(".hdr-tools");
8198
8439
  const moreWrap = document.querySelector(".more-wrap");
8199
8440
  const overflow = $("overflowTools");
8200
8441
  if (!hdrTools || !moreWrap || !overflow) return;
8201
8442
  const mobile = matchMedia("(max-width: 640px)").matches;
8202
- for (const id of OVERFLOW_TOOLS) {
8443
+ const appTools = $("appTools");
8444
+ for (const id of APP_TOOLS) {
8203
8445
  const el = $(id);
8204
8446
  if (!el) continue;
8205
- if (mobile) overflow.appendChild(el); // all into the ⋯ menu
8206
- else hdrTools.insertBefore(el, moreWrap); // back in the header, in order
8447
+ if (!mobile && appTools) appTools.appendChild(el); // desktop: the app bar
8448
+ else if (mobile && OVERFLOW_TOOLS.includes(id)) overflow.appendChild(el); // phone: the ⋯ menu
8449
+ else hdrTools.insertBefore(el, moreWrap); // phone: stays in the header
8207
8450
  }
8208
8451
  }
8209
8452
  let reflowPending = false;
@@ -8216,6 +8459,57 @@
8216
8459
  document.addEventListener("DOMContentLoaded", reflowHeaderTools);
8217
8460
  reflowHeaderTools();
8218
8461
 
8462
+ // ── Teams layer: app bar, search, channel tabs ─────────────────────────────
8463
+ // Activity: how many agents wait for an answer; a click opens the next one.
8464
+ function paintActivity() {
8465
+ const n = tabs.filter((t) => t.btnEl.classList.contains("needs-answer")).length;
8466
+ const badge = $("abActivityN");
8467
+ badge.hidden = !n;
8468
+ badge.textContent = n || "";
8469
+ }
8470
+ new MutationObserver(paintActivity).observe($("tabbar"), { subtree: true, attributes: true, attributeFilter: ["class"], childList: true });
8471
+ $("abActivity").addEventListener("click", () => {
8472
+ const waiting = tabs.filter((t) => t.btnEl.classList.contains("needs-answer"));
8473
+ if (!waiting.length) return;
8474
+ activate(waiting[(waiting.indexOf(active) + 1) % waiting.length]);
8475
+ });
8476
+ $("abAgents").addEventListener("click", () => $("agentFilter").focus());
8477
+
8478
+ // Search: narrows the agents list to the names that match; groups unfold
8479
+ // while a query is typed so nothing matching hides in a collapsed one.
8480
+ function applyAgentFilter() {
8481
+ const q = $("agentFilter").value.trim().toLowerCase();
8482
+ document.body.classList.toggle("filtering", !!q);
8483
+ for (const t of tabs) {
8484
+ const hit = !q || (t.nameEl ? t.nameEl.textContent : "").toLowerCase().includes(q);
8485
+ t.btnEl.classList.toggle("filtered-out", !hit);
8486
+ }
8487
+ }
8488
+ $("agentFilter").addEventListener("input", applyAgentFilter);
8489
+ $("agentFilter").addEventListener("keydown", (e) => {
8490
+ if (e.key === "Escape") { e.target.value = ""; applyAgentFilter(); e.target.blur(); }
8491
+ if (e.key === "Enter") {
8492
+ const first = tabs.find((t) => !t.btnEl.classList.contains("filtered-out"));
8493
+ if (first) { activate(first); e.target.value = ""; applyAgentFilter(); }
8494
+ }
8495
+ });
8496
+
8497
+ // Channel tabs: Chat leaves the terminal and the diff; Changes toggles the diff.
8498
+ $("chanChat").addEventListener("click", () => {
8499
+ if (chatPane.classList.contains("terminal-mode")) $("chanTerm").click();
8500
+ if ($("diffpanel").classList.contains("open")) $("toggleDiff").click();
8501
+ });
8502
+ $("chanDiff").addEventListener("click", () => $("toggleDiff").click());
8503
+ const paintChanTabs = () => {
8504
+ const term = chatPane.classList.contains("terminal-mode");
8505
+ const diff = $("diffpanel").classList.contains("open");
8506
+ $("chanChat").classList.toggle("on", !term && !diff);
8507
+ $("chanDiff").classList.toggle("on", diff);
8508
+ };
8509
+ new MutationObserver(paintChanTabs).observe(chatPane, { attributes: true, attributeFilter: ["class"] });
8510
+ new MutationObserver(paintChanTabs).observe($("diffpanel"), { attributes: true, attributeFilter: ["class"] });
8511
+ paintChanTabs();
8512
+
8219
8513
  // ── Diff panel ──────────────────────────────────────────
8220
8514
  function colorDiff(text) {
8221
8515
  const esc = (s) => s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
@@ -10010,7 +10304,7 @@
10010
10304
  el.value = "";
10011
10305
  });
10012
10306
 
10013
- newTabBtn.addEventListener("click", openSetup);
10307
+ newTabBtn.addEventListener("click", () => openSetup());
10014
10308
 
10015
10309
  // One click starts an agent on shadok-ai's own source: the directory, the
10016
10310
  // worktree and the role are all decided already, so a form would only ask you