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.
- package/README.md +17 -16
- package/package.json +1 -1
- 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
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
into that group.
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
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
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: #
|
|
20
|
-
--radius:
|
|
21
|
-
--bg: #
|
|
22
|
-
--bg-raised: #
|
|
23
|
-
--bg-inset: #
|
|
24
|
-
--line: #
|
|
25
|
-
--text: #
|
|
26
|
-
--text-dim: #
|
|
27
|
-
|
|
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: #
|
|
32
|
-
|
|
33
|
-
|
|
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: #
|
|
37
|
-
--menu-hover: #
|
|
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
|
-
|
|
52
|
-
|
|
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: #
|
|
55
|
-
--line: #
|
|
56
|
-
--text: #
|
|
57
|
-
--text-dim: #
|
|
58
|
-
--ok: #
|
|
59
|
-
--err: #
|
|
60
|
-
|
|
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: #
|
|
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
|
-
|
|
170
|
-
header .brand
|
|
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:
|
|
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:
|
|
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:
|
|
464
|
-
button.primary:hover { background: var(--amber); border-color: var(--amber); color:
|
|
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(
|
|
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
|
-
|
|
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)
|
|
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: "
|
|
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
|
-
|
|
8443
|
+
const appTools = $("appTools");
|
|
8444
|
+
for (const id of APP_TOOLS) {
|
|
8203
8445
|
const el = $(id);
|
|
8204
8446
|
if (!el) continue;
|
|
8205
|
-
if (mobile)
|
|
8206
|
-
else
|
|
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, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
@@ -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
|