shadok-ai 0.15.44 → 0.15.46
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 +8 -10
- package/package.json +1 -1
- package/public/index.html +29 -134
package/README.md
CHANGED
|
@@ -76,20 +76,18 @@ 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** — on a desktop the
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
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
|
|
79
|
+
- **Groups** — on a desktop the groups live in their own **left column**: pick
|
|
80
|
+
one to narrow the agents pane to it, and the global tools (Profiles, Ledger,
|
|
81
|
+
Secrets, Users, Security, Telegram, Sound) stay in the header at the top. Each
|
|
82
|
+
agent is a round avatar with a presence dot (green ready, red working, hollow
|
|
85
83
|
offline). A group's icon opens the icon picker: choose from the grid, type any
|
|
86
84
|
emoji, or **type a word** (`rocket`, `bug`, `money`…) to search for one; its
|
|
87
85
|
**+** creates the new agent straight into that group. Dragging an agent onto a
|
|
88
86
|
group files it there, dragging groups reorders them, and double-clicking a name
|
|
89
|
-
renames it.
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
every group.
|
|
87
|
+
renames it. A question the agent asks shows as an *Approval requested* card. The
|
|
88
|
+
whole cockpit is skinned like Microsoft Teams (grey light mode / near-black dark
|
|
89
|
+
mode, the Shadok mark in the header). On a phone the tools sit in the ⋯ menu and
|
|
90
|
+
the agents sheet lists every group.
|
|
93
91
|
- **Worktree isolation** — spawned agents get their own git worktree and branch
|
|
94
92
|
by default, so parallel agents never collide. Work is never auto-discarded:
|
|
95
93
|
an empty worktree is reclaimed on close, anything with changes or commits
|
package/package.json
CHANGED
package/public/index.html
CHANGED
|
@@ -2139,27 +2139,18 @@
|
|
|
2139
2139
|
.sec-reload button:hover { background: color-mix(in srgb, var(--amber) 15%, transparent); }
|
|
2140
2140
|
|
|
2141
2141
|
/* ══ Teams layer ═════════════════════════════════════════════════════════
|
|
2142
|
-
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
specificity; the phone layout (≤640px) keeps its
|
|
2142
|
+
A Teams-style SKIN over the cockpit's original layout: round avatars with a
|
|
2143
|
+
presence dot, a channel header, bubbles, and dialogs as approval cards.
|
|
2144
|
+
Colours stay tokens, so light mode reads as Teams and dark mode as the
|
|
2145
|
+
neutral near-black. The layout itself is unchanged — the global tools live
|
|
2146
|
+
in the header, and the groups keep their own left column (#groupRail). Placed
|
|
2147
|
+
LAST so it wins on equal specificity; the phone layout (≤640px) keeps its
|
|
2148
|
+
sheet and its ⋯ menu. */
|
|
2148
2149
|
:root { --mine: color-mix(in srgb, var(--amber) 20%, var(--bg-raised)); --pane-main: color-mix(in srgb, var(--bg) 55%, var(--bg-raised)); }
|
|
2149
2150
|
:root[data-mode="light"] { --mine: color-mix(in srgb, var(--amber) 13%, #fff); }
|
|
2150
2151
|
|
|
2151
2152
|
/* Title bar */
|
|
2152
2153
|
header { padding: 8px 14px; gap: 14px; }
|
|
2153
|
-
#agentSearch {
|
|
2154
|
-
flex: 0 1 520px; margin: 0 auto; display: flex; align-items: center; gap: 8px;
|
|
2155
|
-
height: 32px; padding: 0 12px; border-radius: 6px; cursor: text;
|
|
2156
|
-
background: var(--bg-raised); color: var(--text-dim); border: 1px solid var(--line);
|
|
2157
|
-
}
|
|
2158
|
-
#agentSearch:focus-within { border-color: var(--amber); }
|
|
2159
|
-
#agentSearch input { flex: 1; min-width: 0; border: 0; outline: none; background: none; color: var(--text); font: inherit; font-size: 13.5px; padding: 0; }
|
|
2160
|
-
#agentSearch input::placeholder { color: var(--text-dim); }
|
|
2161
|
-
#agentSearch + .spacer { display: none; }
|
|
2162
|
-
@media (max-width: 640px) { #agentSearch { display: none; } #agentSearch + .spacer { display: block; } }
|
|
2163
2154
|
@media (min-width: 641px) { header { flex-wrap: nowrap; } }
|
|
2164
2155
|
/* The star pill is the widest tool; under 1100px it lives in the ⋯ menu. */
|
|
2165
2156
|
@media (min-width: 641px) and (max-width: 1100px) { #starBtn { display: none; } #moreMenu #starMenuItem { display: block; } }
|
|
@@ -2183,7 +2174,6 @@
|
|
|
2183
2174
|
}
|
|
2184
2175
|
.tav .led { position: absolute; right: -2px; bottom: -2px; width: 12px; height: 12px; margin: 0; border: 2px solid var(--bg); }
|
|
2185
2176
|
.ab-txt, .ct-txt { display: none; }
|
|
2186
|
-
#appBar { display: none; }
|
|
2187
2177
|
@media (max-width: 640px) {
|
|
2188
2178
|
/* A phone's single header row has no room for the mark beside the name,
|
|
2189
2179
|
the version and the dials; the favicon carries it there. */
|
|
@@ -2203,8 +2193,6 @@
|
|
|
2203
2193
|
:root:not([data-mode="light"]) .tab.active { background: var(--bg-inset); box-shadow: inset 0 0 0 1px var(--line); }
|
|
2204
2194
|
.tab.active .tav .led { border-color: var(--bg-raised); }
|
|
2205
2195
|
.tab.unread { border-left: 0; border-radius: 6px; font-weight: 700; }
|
|
2206
|
-
.tab.filtered-out { display: none !important; }
|
|
2207
|
-
body.filtering .group.collapsed .group-body { display: flex !important; }
|
|
2208
2196
|
.group-head { gap: 10px; padding: 8px; font-size: 14px; color: var(--text); border-radius: 6px; }
|
|
2209
2197
|
.group-head .gicon {
|
|
2210
2198
|
flex: none; width: 30px; height: 30px; border-radius: 6px; display: grid; place-items: center;
|
|
@@ -2215,62 +2203,25 @@
|
|
|
2215
2203
|
|
|
2216
2204
|
@media (min-width: 641px) {
|
|
2217
2205
|
main { padding: 0; gap: 0; }
|
|
2218
|
-
/* App bar */
|
|
2219
|
-
#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; }
|
|
2220
|
-
#appTools { display: contents; }
|
|
2221
|
-
#appBar button {
|
|
2222
|
-
position: relative; width: 62px; padding: 8px 0 6px; display: flex; flex-direction: column; align-items: center; gap: 3px;
|
|
2223
|
-
border: 0; border-radius: 6px; background: none; color: var(--text-dim); font-size: 10.5px; line-height: 1.1;
|
|
2224
|
-
}
|
|
2225
|
-
#appBar button:hover { color: var(--amber); background: none; }
|
|
2226
|
-
#appBar button.on { color: var(--amber); font-weight: 600; }
|
|
2227
|
-
#appBar button.on::before { content: ""; position: absolute; left: -2px; top: 10px; bottom: 10px; width: 3px; border-radius: 2px; background: var(--amber); }
|
|
2228
|
-
#appBar button[hidden] { display: none; }
|
|
2229
|
-
#appBar svg { width: 20px !important; height: 20px !important; vertical-align: 0 !important; }
|
|
2230
|
-
#appBar .ab-txt { display: block; }
|
|
2231
|
-
#appBar .ab-n, #appBar .sec-badge {
|
|
2232
|
-
position: absolute; top: 3px; right: 12px; min-width: 16px; height: 16px; padding: 0 4px; border-radius: 8px;
|
|
2233
|
-
background: var(--err); color: #fff; font-size: 10px; font-weight: 600; line-height: 16px; text-align: center;
|
|
2234
|
-
}
|
|
2235
|
-
#appBar .ab-n[hidden] { display: none; }
|
|
2236
|
-
|
|
2237
|
-
/* Every group is a section of the agents pane; the rail goes. */
|
|
2238
|
-
body.has-groups #groupRail { display: none; }
|
|
2239
|
-
#tabbar, body.has-groups #tabbar {
|
|
2240
|
-
width: 300px; border: 0; border-radius: 8px 0 0 0; background: var(--bg); padding: 12px 8px;
|
|
2241
|
-
}
|
|
2242
|
-
#tabbar .side-label { font-size: 18px; font-weight: 700; padding: 4px 8px 12px; }
|
|
2243
|
-
body.has-groups #tabbar .side-label .sl-all { display: inline; }
|
|
2244
|
-
#tabbar .side-label .sl-view, #viewEmpty { display: none !important; }
|
|
2245
|
-
#tabbar .group-head { display: flex; }
|
|
2246
|
-
#tabbar .group { margin-top: 6px; }
|
|
2247
|
-
#tabbar .group-body { padding-left: 20px; }
|
|
2248
|
-
#tabbar .group.collapsed .group-body { display: none; }
|
|
2249
|
-
#tabbar:not([data-view="ungrouped"]) #ungrouped,
|
|
2250
|
-
#tabbar[data-view="ungrouped"] #groups { display: flex; flex-direction: column; }
|
|
2251
|
-
#tabbar .group:not(.in-view) { display: flex; }
|
|
2252
|
-
#newGroupBare, body.has-groups #newGroupBare { display: block; }
|
|
2253
|
-
|
|
2254
2206
|
/* Conversation */
|
|
2255
2207
|
.chat-pane { border: 0; border-radius: 0; background: var(--pane-main); }
|
|
2256
2208
|
#machine, #diffpanel { border-radius: 0; border-top: 0; border-bottom: 0; border-right: 0; }
|
|
2257
2209
|
}
|
|
2258
2210
|
|
|
2259
|
-
/* Channel header: avatar, name, status, then
|
|
2211
|
+
/* Channel header: avatar, name, status, then actions. */
|
|
2260
2212
|
#chanBar { gap: 12px; padding: 0 20px; min-height: 58px; background: var(--bg-raised); border-bottom: 1px solid var(--line); }
|
|
2261
2213
|
#chanBar .tav { width: 36px; height: 36px; font-size: 13px; }
|
|
2262
2214
|
#chanBar #chanBarName { font-size: 16px; font-weight: 700; }
|
|
2263
2215
|
#chanBar .gauge .label { display: none; }
|
|
2264
2216
|
#chanBar .gauge .value { color: var(--text-dim); font-size: 12px; }
|
|
2265
2217
|
#chanBar .gauge:nth-of-type(3) { display: none; } /* the session id: in the tooltip of the menu */
|
|
2266
|
-
.chan-
|
|
2267
|
-
.chan-tabs button, #chanBar .chan-actions #chanCron, #chanBar .chan-actions #chanTerm {
|
|
2218
|
+
#chanBar .chan-actions #chanCron, #chanBar .chan-actions #chanTerm {
|
|
2268
2219
|
position: relative; align-self: stretch; display: inline-flex; align-items: center; gap: 6px;
|
|
2269
2220
|
border: 0; border-radius: 0; background: none; padding: 0 10px; color: var(--text-dim); font-size: 14px;
|
|
2270
2221
|
}
|
|
2271
|
-
|
|
2272
|
-
|
|
2273
|
-
|
|
2222
|
+
#chanBar .chan-actions #chanCron:hover, #chanBar .chan-actions #chanTerm:hover { color: var(--text); background: none; }
|
|
2223
|
+
#chanBar .chan-actions #chanTerm.on { color: var(--text); font-weight: 600; background: none; border: 0; }
|
|
2224
|
+
#chanBar .chan-actions #chanTerm.on::after {
|
|
2274
2225
|
content: ""; position: absolute; left: 10px; right: 10px; bottom: 0; height: 3px; border-radius: 3px 3px 0 0; background: var(--amber);
|
|
2275
2226
|
}
|
|
2276
2227
|
#chanBar .chan-actions { align-self: stretch; }
|
|
@@ -2278,7 +2229,7 @@
|
|
|
2278
2229
|
#chanBar .chan-actions #chanStop, #chanBar .chan-actions #chanMenu { align-self: center; }
|
|
2279
2230
|
@media (max-width: 640px) {
|
|
2280
2231
|
#chanBar { min-height: 0; padding: 8px 12px; }
|
|
2281
|
-
#chanBar .tav,
|
|
2232
|
+
#chanBar .tav, #chanBar .chan-actions .ct-txt { display: none; }
|
|
2282
2233
|
}
|
|
2283
2234
|
|
|
2284
2235
|
/* Messages: Teams bubbles. */
|
|
@@ -2308,8 +2259,6 @@
|
|
|
2308
2259
|
|
|
2309
2260
|
<header>
|
|
2310
2261
|
<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>
|
|
2311
|
-
<!-- Teams-style search: filters the agents list as you type (desktop). -->
|
|
2312
|
-
<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>
|
|
2313
2262
|
<span class="spacer"></span>
|
|
2314
2263
|
<div class="gauge quota" id="quota5h" data-window="5h" title="5h rolling limit">
|
|
2315
2264
|
<svg class="dial" viewBox="0 0 56 44" role="img" aria-label="5h rolling limit">
|
|
@@ -2873,14 +2822,6 @@
|
|
|
2873
2822
|
</div>
|
|
2874
2823
|
|
|
2875
2824
|
<main>
|
|
2876
|
-
<!-- The app bar (desktop): Teams' left column. Activity jumps to an agent that
|
|
2877
|
-
waits for you; the global tools are MOVED here by reflowHeaderTools (the
|
|
2878
|
-
real buttons, handlers included) and back to the header/⋯ on a phone. -->
|
|
2879
|
-
<nav id="appBar" aria-label="Apps">
|
|
2880
|
-
<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>
|
|
2881
|
-
<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>
|
|
2882
|
-
<div id="appTools"></div>
|
|
2883
|
-
</nav>
|
|
2884
2825
|
<!-- The groups column (desktop only): a VIEW over the agents column beside it,
|
|
2885
2826
|
which still holds the groups, their order and their members — that is what
|
|
2886
2827
|
is persisted. Picking a group narrows the agents column to it. On a phone
|
|
@@ -2950,10 +2891,6 @@
|
|
|
2950
2891
|
<span class="label">Branch</span>
|
|
2951
2892
|
<span class="value" id="branchView">—</span>
|
|
2952
2893
|
</div>
|
|
2953
|
-
<span class="chan-tabs" role="tablist">
|
|
2954
|
-
<button id="chanChat" type="button" role="tab">Chat</button>
|
|
2955
|
-
<button id="chanDiff" type="button" role="tab">Changes</button>
|
|
2956
|
-
</span>
|
|
2957
2894
|
<span class="chan-actions">
|
|
2958
2895
|
<button id="chanUpdate" class="icon" title="Claude Code updated — reload this agent to apply it" aria-label="Reload to apply update" hidden>↻ update</button>
|
|
2959
2896
|
<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>
|
|
@@ -4297,8 +4234,20 @@
|
|
|
4297
4234
|
d.scrollIntoView({ behavior: "smooth", block: "start" });
|
|
4298
4235
|
}
|
|
4299
4236
|
|
|
4237
|
+
// One confirmation for ending an agent, shared by the header's End button and
|
|
4238
|
+
// the left-column close (the tab's × and the agent menu's "Close agent"), so
|
|
4239
|
+
// the wording never drifts between them. Ending stops the process and drops the
|
|
4240
|
+
// agent from the list; work it committed or left in its worktree is kept — an
|
|
4241
|
+
// empty worktree is cleaned up (invariant 5). The main channel never reaches
|
|
4242
|
+
// here (it cannot be closed).
|
|
4243
|
+
function confirmEndAgent(tab) {
|
|
4244
|
+
const name = (tab && tab.nameEl ? tab.nameEl.textContent : "") || "this agent";
|
|
4245
|
+
return confirm("End “" + name + "”?\n\nIts session stops and it leaves the list. Any work it committed or left in its worktree is kept; an empty worktree is cleaned up.");
|
|
4246
|
+
}
|
|
4247
|
+
|
|
4300
4248
|
function closeTab(tab) {
|
|
4301
4249
|
if (tab.isMain) return; // the environment's main channel is never closed
|
|
4250
|
+
if (!confirmEndAgent(tab)) return;
|
|
4302
4251
|
// Full close: end the channel everywhere (stop the agent, delete its
|
|
4303
4252
|
// Telegram topic, drop it from the registry). Use the HTTP endpoint so it
|
|
4304
4253
|
// works even when the session is dead (no live WebSocket) — a zombie channel
|
|
@@ -8460,6 +8409,7 @@
|
|
|
8460
8409
|
$("chanUpdate").addEventListener("click", () => { if (active) handleClaudeUpdate(active); });
|
|
8461
8410
|
$("chanCron").addEventListener("click", () => { $("cronOverlay").hidden = false; renderCrons(); });
|
|
8462
8411
|
$("chanStop").addEventListener("click", () => {
|
|
8412
|
+
if (!active || !confirmEndAgent(active)) return;
|
|
8463
8413
|
sendTo(active, { type: "stop" });
|
|
8464
8414
|
});
|
|
8465
8415
|
// The per-agent menu, opened from the channel header — the same one a
|
|
@@ -8493,21 +8443,17 @@
|
|
|
8493
8443
|
// the real button elements, so their click handlers travel with them.
|
|
8494
8444
|
// Recomputed on load, on resize, and when the instance name changes.
|
|
8495
8445
|
const OVERFLOW_TOOLS = ["secretsBtn", "ledgerBtn", "profilesBtn", "telegramBtn", "muteNotif"];
|
|
8496
|
-
// Every tool the desktop app bar carries, in its order.
|
|
8497
|
-
const APP_TOOLS = ["profilesBtn", "ledgerBtn", "secretsBtn", "usersBtn", "securityBtn", "telegramBtn", "muteNotif"];
|
|
8498
8446
|
function reflowHeaderTools() {
|
|
8499
8447
|
const hdrTools = document.querySelector(".hdr-tools");
|
|
8500
8448
|
const moreWrap = document.querySelector(".more-wrap");
|
|
8501
8449
|
const overflow = $("overflowTools");
|
|
8502
8450
|
if (!hdrTools || !moreWrap || !overflow) return;
|
|
8503
8451
|
const mobile = matchMedia("(max-width: 640px)").matches;
|
|
8504
|
-
const
|
|
8505
|
-
for (const id of APP_TOOLS) {
|
|
8452
|
+
for (const id of OVERFLOW_TOOLS) {
|
|
8506
8453
|
const el = $(id);
|
|
8507
8454
|
if (!el) continue;
|
|
8508
|
-
if (
|
|
8509
|
-
else
|
|
8510
|
-
else hdrTools.insertBefore(el, moreWrap); // phone: stays in the header
|
|
8455
|
+
if (mobile) overflow.appendChild(el); // all into the ⋯ menu
|
|
8456
|
+
else hdrTools.insertBefore(el, moreWrap); // back in the header, in order
|
|
8511
8457
|
}
|
|
8512
8458
|
}
|
|
8513
8459
|
let reflowPending = false;
|
|
@@ -8520,57 +8466,6 @@
|
|
|
8520
8466
|
document.addEventListener("DOMContentLoaded", reflowHeaderTools);
|
|
8521
8467
|
reflowHeaderTools();
|
|
8522
8468
|
|
|
8523
|
-
// ── Teams layer: app bar, search, channel tabs ─────────────────────────────
|
|
8524
|
-
// Activity: how many agents wait for an answer; a click opens the next one.
|
|
8525
|
-
function paintActivity() {
|
|
8526
|
-
const n = tabs.filter((t) => t.btnEl.classList.contains("needs-answer")).length;
|
|
8527
|
-
const badge = $("abActivityN");
|
|
8528
|
-
badge.hidden = !n;
|
|
8529
|
-
badge.textContent = n || "";
|
|
8530
|
-
}
|
|
8531
|
-
new MutationObserver(paintActivity).observe($("tabbar"), { subtree: true, attributes: true, attributeFilter: ["class"], childList: true });
|
|
8532
|
-
$("abActivity").addEventListener("click", () => {
|
|
8533
|
-
const waiting = tabs.filter((t) => t.btnEl.classList.contains("needs-answer"));
|
|
8534
|
-
if (!waiting.length) return;
|
|
8535
|
-
activate(waiting[(waiting.indexOf(active) + 1) % waiting.length]);
|
|
8536
|
-
});
|
|
8537
|
-
$("abAgents").addEventListener("click", () => $("agentFilter").focus());
|
|
8538
|
-
|
|
8539
|
-
// Search: narrows the agents list to the names that match; groups unfold
|
|
8540
|
-
// while a query is typed so nothing matching hides in a collapsed one.
|
|
8541
|
-
function applyAgentFilter() {
|
|
8542
|
-
const q = $("agentFilter").value.trim().toLowerCase();
|
|
8543
|
-
document.body.classList.toggle("filtering", !!q);
|
|
8544
|
-
for (const t of tabs) {
|
|
8545
|
-
const hit = !q || (t.nameEl ? t.nameEl.textContent : "").toLowerCase().includes(q);
|
|
8546
|
-
t.btnEl.classList.toggle("filtered-out", !hit);
|
|
8547
|
-
}
|
|
8548
|
-
}
|
|
8549
|
-
$("agentFilter").addEventListener("input", applyAgentFilter);
|
|
8550
|
-
$("agentFilter").addEventListener("keydown", (e) => {
|
|
8551
|
-
if (e.key === "Escape") { e.target.value = ""; applyAgentFilter(); e.target.blur(); }
|
|
8552
|
-
if (e.key === "Enter") {
|
|
8553
|
-
const first = tabs.find((t) => !t.btnEl.classList.contains("filtered-out"));
|
|
8554
|
-
if (first) { activate(first); e.target.value = ""; applyAgentFilter(); }
|
|
8555
|
-
}
|
|
8556
|
-
});
|
|
8557
|
-
|
|
8558
|
-
// Channel tabs: Chat leaves the terminal and the diff; Changes toggles the diff.
|
|
8559
|
-
$("chanChat").addEventListener("click", () => {
|
|
8560
|
-
if (chatPane.classList.contains("terminal-mode")) $("chanTerm").click();
|
|
8561
|
-
if ($("diffpanel").classList.contains("open")) $("toggleDiff").click();
|
|
8562
|
-
});
|
|
8563
|
-
$("chanDiff").addEventListener("click", () => $("toggleDiff").click());
|
|
8564
|
-
const paintChanTabs = () => {
|
|
8565
|
-
const term = chatPane.classList.contains("terminal-mode");
|
|
8566
|
-
const diff = $("diffpanel").classList.contains("open");
|
|
8567
|
-
$("chanChat").classList.toggle("on", !term && !diff);
|
|
8568
|
-
$("chanDiff").classList.toggle("on", diff);
|
|
8569
|
-
};
|
|
8570
|
-
new MutationObserver(paintChanTabs).observe(chatPane, { attributes: true, attributeFilter: ["class"] });
|
|
8571
|
-
new MutationObserver(paintChanTabs).observe($("diffpanel"), { attributes: true, attributeFilter: ["class"] });
|
|
8572
|
-
paintChanTabs();
|
|
8573
|
-
|
|
8574
8469
|
// ── Diff panel ──────────────────────────────────────────
|
|
8575
8470
|
function colorDiff(text) {
|
|
8576
8471
|
const esc = (s) => s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|