privateboard 0.1.22 → 0.1.24
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/dist/boot.js +140 -113
- package/dist/boot.js.map +1 -1
- package/dist/cli.js +140 -113
- package/dist/cli.js.map +1 -1
- package/dist/server.js +140 -113
- package/dist/server.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/version.js.map +1 -1
- package/package.json +1 -1
- package/public/agent-overlay.js +4 -6
- package/public/agent-profile.js +10 -12
- package/public/app-updater.css +318 -0
- package/public/app-updater.js +247 -0
- package/public/app.js +66 -21
- package/public/home.html +1 -1
- package/public/i18n.js +60 -0
- package/public/index.html +168 -32
- package/public/mention-picker.js +527 -0
- package/public/new-agent.js +10 -5
|
@@ -0,0 +1,527 @@
|
|
|
1
|
+
/* ═══════════════════════════════════════════
|
|
2
|
+
IN-ROOM @ MENTION PICKER
|
|
3
|
+
═══════════════════════════════════════════
|
|
4
|
+
Hooks the room's input bar textarea (`.ib-textarea[data-send-input]`).
|
|
5
|
+
Typing `@` (at start of text or after whitespace) opens a floating
|
|
6
|
+
checkbox menu above the input listing the directors currently in the
|
|
7
|
+
room (chair + active members). Behaviour is "tick = insert /
|
|
8
|
+
untick = remove" · every checkbox toggle rewrites the picker-owned
|
|
9
|
+
text zone immediately, so the user sees the @handle land in the
|
|
10
|
+
textarea as they click. No separate Enter/commit step.
|
|
11
|
+
|
|
12
|
+
Region model
|
|
13
|
+
────────────
|
|
14
|
+
When the picker opens we remember `trigger.start` (the position of
|
|
15
|
+
the `@` glyph) and `zoneEnd` (the end of the picker-managed
|
|
16
|
+
substring). On every check toggle we rebuild that range from the
|
|
17
|
+
current `selectedOrder`, splice it into the textarea, and bump
|
|
18
|
+
`zoneEnd`. When nothing is selected we restore the original
|
|
19
|
+
`@<query>` so the user can keep typing to refine the filter.
|
|
20
|
+
|
|
21
|
+
On close (Esc / click outside / Enter / next room nav) we push the
|
|
22
|
+
final `selectedOrder` onto `pending` · `submitFromComposer()` reads
|
|
23
|
+
it via `window.MentionPicker.consumePendingMentions(text)` and
|
|
24
|
+
filters by handle-still-present-in-body so backspaced-out handles
|
|
25
|
+
drop out cleanly.
|
|
26
|
+
*/
|
|
27
|
+
(function () {
|
|
28
|
+
/* ── State ───────────────────────────────────────────────── */
|
|
29
|
+
const state = {
|
|
30
|
+
open: false,
|
|
31
|
+
trigger: null, // { start, end, query } at open time (start is fixed)
|
|
32
|
+
zoneEnd: -1, // dynamic end of picker-managed region in textarea
|
|
33
|
+
query: "", // current filter; refinable only while selection empty
|
|
34
|
+
selected: new Set(), // agent ids currently checked (multi)
|
|
35
|
+
selectedOrder: [], // [id, ...] in insertion order, for stable text rebuild
|
|
36
|
+
filtered: [],
|
|
37
|
+
activeIdx: -1, // keyboard cursor
|
|
38
|
+
pending: [], // [{ id, handle }] persisted across opens until send
|
|
39
|
+
};
|
|
40
|
+
let pickerEl = null;
|
|
41
|
+
let textarea = null;
|
|
42
|
+
let _suppressClose = false;
|
|
43
|
+
const HIDE_DEBOUNCE_MS = 60;
|
|
44
|
+
|
|
45
|
+
/* ── Styles ──────────────────────────────────────────────── */
|
|
46
|
+
function ensureStyles() {
|
|
47
|
+
if (document.getElementById("mention-picker-styles")) return;
|
|
48
|
+
const css = `
|
|
49
|
+
.mention-picker {
|
|
50
|
+
position: absolute;
|
|
51
|
+
bottom: calc(100% + 6px);
|
|
52
|
+
left: 8px;
|
|
53
|
+
z-index: 40;
|
|
54
|
+
width: 320px;
|
|
55
|
+
max-height: 300px;
|
|
56
|
+
padding: 6px;
|
|
57
|
+
border: 1px solid var(--line-strong);
|
|
58
|
+
border-radius: 12px;
|
|
59
|
+
background: color-mix(in srgb, color-mix(in srgb, var(--panel-3) 78%, var(--bg) 22%) 92%, transparent);
|
|
60
|
+
backdrop-filter: blur(24px) saturate(180%);
|
|
61
|
+
-webkit-backdrop-filter: blur(24px) saturate(180%);
|
|
62
|
+
box-shadow: 0 6px 28px rgba(0,0,0,0.28);
|
|
63
|
+
display: flex;
|
|
64
|
+
flex-direction: column;
|
|
65
|
+
gap: 4px;
|
|
66
|
+
font-family: var(--sans, system-ui), sans-serif;
|
|
67
|
+
font-size: 13px;
|
|
68
|
+
color: var(--text);
|
|
69
|
+
}
|
|
70
|
+
.mention-picker[hidden] { display: none; }
|
|
71
|
+
.mention-picker-head {
|
|
72
|
+
display: flex;
|
|
73
|
+
align-items: baseline;
|
|
74
|
+
justify-content: space-between;
|
|
75
|
+
padding: 4px 8px 2px;
|
|
76
|
+
color: var(--text-soft);
|
|
77
|
+
font-size: 11px;
|
|
78
|
+
letter-spacing: 0.1em;
|
|
79
|
+
text-transform: uppercase;
|
|
80
|
+
}
|
|
81
|
+
.mention-picker-list {
|
|
82
|
+
list-style: none;
|
|
83
|
+
margin: 0;
|
|
84
|
+
padding: 0;
|
|
85
|
+
overflow-y: auto;
|
|
86
|
+
max-height: 220px;
|
|
87
|
+
}
|
|
88
|
+
.mention-row {
|
|
89
|
+
display: flex;
|
|
90
|
+
align-items: center;
|
|
91
|
+
gap: 8px;
|
|
92
|
+
padding: 6px 8px;
|
|
93
|
+
border-radius: 8px;
|
|
94
|
+
cursor: pointer;
|
|
95
|
+
user-select: none;
|
|
96
|
+
}
|
|
97
|
+
.mention-row:hover,
|
|
98
|
+
.mention-row[data-active="true"] {
|
|
99
|
+
background: color-mix(in srgb, var(--lime) 12%, transparent);
|
|
100
|
+
}
|
|
101
|
+
.mention-row-cb {
|
|
102
|
+
flex: 0 0 auto;
|
|
103
|
+
width: 14px;
|
|
104
|
+
height: 14px;
|
|
105
|
+
margin: 0;
|
|
106
|
+
accent-color: var(--lime);
|
|
107
|
+
pointer-events: none;
|
|
108
|
+
}
|
|
109
|
+
.mention-row-av {
|
|
110
|
+
flex: 0 0 auto;
|
|
111
|
+
width: 22px;
|
|
112
|
+
height: 22px;
|
|
113
|
+
border-radius: 50%;
|
|
114
|
+
object-fit: cover;
|
|
115
|
+
background: color-mix(in srgb, var(--panel-2) 60%, transparent);
|
|
116
|
+
}
|
|
117
|
+
.mention-row-name {
|
|
118
|
+
flex: 1 1 auto;
|
|
119
|
+
font-weight: 500;
|
|
120
|
+
color: var(--text);
|
|
121
|
+
overflow: hidden;
|
|
122
|
+
text-overflow: ellipsis;
|
|
123
|
+
white-space: nowrap;
|
|
124
|
+
}
|
|
125
|
+
.mention-row-handle {
|
|
126
|
+
flex: 0 0 auto;
|
|
127
|
+
color: var(--text-soft);
|
|
128
|
+
font-size: 11px;
|
|
129
|
+
font-family: var(--mono, ui-monospace), monospace;
|
|
130
|
+
}
|
|
131
|
+
.mention-row-empty {
|
|
132
|
+
padding: 10px 12px;
|
|
133
|
+
color: var(--text-soft);
|
|
134
|
+
font-size: 12px;
|
|
135
|
+
font-style: italic;
|
|
136
|
+
}
|
|
137
|
+
.mention-picker-foot {
|
|
138
|
+
padding: 4px 8px 2px;
|
|
139
|
+
color: var(--text-soft);
|
|
140
|
+
font-size: 11px;
|
|
141
|
+
letter-spacing: 0.06em;
|
|
142
|
+
display: flex;
|
|
143
|
+
justify-content: space-between;
|
|
144
|
+
}
|
|
145
|
+
.input-bar:has(.mention-picker) { position: relative; }
|
|
146
|
+
`;
|
|
147
|
+
const style = document.createElement("style");
|
|
148
|
+
style.id = "mention-picker-styles";
|
|
149
|
+
style.textContent = css;
|
|
150
|
+
document.head.appendChild(style);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/* ── DOM helpers ─────────────────────────────────────────── */
|
|
154
|
+
function ensurePicker(input) {
|
|
155
|
+
const bar = input.closest(".input-bar");
|
|
156
|
+
if (!bar) return null;
|
|
157
|
+
let el = bar.querySelector("[data-mention-picker]");
|
|
158
|
+
if (el) return el;
|
|
159
|
+
ensureStyles();
|
|
160
|
+
el = document.createElement("div");
|
|
161
|
+
el.className = "mention-picker";
|
|
162
|
+
el.setAttribute("data-mention-picker", "");
|
|
163
|
+
el.setAttribute("role", "listbox");
|
|
164
|
+
el.setAttribute("aria-label", "Mention a director");
|
|
165
|
+
el.hidden = true;
|
|
166
|
+
el.innerHTML = `
|
|
167
|
+
<div class="mention-picker-head">
|
|
168
|
+
<span>@ Mention</span>
|
|
169
|
+
<span data-mention-count></span>
|
|
170
|
+
</div>
|
|
171
|
+
<ul class="mention-picker-list" data-mention-list></ul>
|
|
172
|
+
<div class="mention-picker-foot">
|
|
173
|
+
<span>↑↓ navigate · Space / click toggle</span>
|
|
174
|
+
<span>Enter / Esc close</span>
|
|
175
|
+
</div>
|
|
176
|
+
`;
|
|
177
|
+
bar.appendChild(el);
|
|
178
|
+
return el;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function escapeHtml(s) {
|
|
182
|
+
return String(s).replace(/[&<>"']/g, (c) => ({
|
|
183
|
+
"&": "&", "<": "<", ">": ">", '"': """, "'": "'",
|
|
184
|
+
}[c]));
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/* ── Data ────────────────────────────────────────────────── */
|
|
188
|
+
function getDirectors() {
|
|
189
|
+
const app = window.app;
|
|
190
|
+
if (!app) return [];
|
|
191
|
+
const members = Array.isArray(app.currentMembers) ? app.currentMembers : [];
|
|
192
|
+
const chair = app.currentChair || null;
|
|
193
|
+
const seen = new Set();
|
|
194
|
+
const out = [];
|
|
195
|
+
if (chair && chair.id) {
|
|
196
|
+
seen.add(chair.id);
|
|
197
|
+
out.push(normalizeDirector(chair, true));
|
|
198
|
+
}
|
|
199
|
+
for (const m of members) {
|
|
200
|
+
if (!m || !m.id || seen.has(m.id)) continue;
|
|
201
|
+
seen.add(m.id);
|
|
202
|
+
out.push(normalizeDirector(m, false));
|
|
203
|
+
}
|
|
204
|
+
return out.filter((d) => !!d.handle);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function normalizeDirector(a, isChair) {
|
|
208
|
+
const rawHandle = a.handle || a.slug || a.id || "";
|
|
209
|
+
const handle = String(rawHandle).replace(/^[@/]/, "").trim();
|
|
210
|
+
return {
|
|
211
|
+
id: a.id,
|
|
212
|
+
name: a.name || handle || a.id,
|
|
213
|
+
handle,
|
|
214
|
+
avatar: a.avatarPath || a.avatar || a.avatar_url || "",
|
|
215
|
+
isChair,
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function directorById(id) {
|
|
220
|
+
return getDirectors().find((d) => d.id === id) || null;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/* ── Trigger detection ───────────────────────────────────── */
|
|
224
|
+
function findActiveMention(ta) {
|
|
225
|
+
const value = ta.value;
|
|
226
|
+
const caret = ta.selectionStart;
|
|
227
|
+
if (typeof caret !== "number") return null;
|
|
228
|
+
for (let i = caret - 1; i >= 0; i--) {
|
|
229
|
+
const ch = value[i];
|
|
230
|
+
if (ch === "@") {
|
|
231
|
+
const before = i === 0 ? "" : value[i - 1];
|
|
232
|
+
if (before === "" || /\s/.test(before)) {
|
|
233
|
+
const query = value.substring(i + 1, caret);
|
|
234
|
+
if (/\s/.test(query)) return null;
|
|
235
|
+
return { start: i, end: caret, query };
|
|
236
|
+
}
|
|
237
|
+
return null;
|
|
238
|
+
}
|
|
239
|
+
if (/\s/.test(ch)) return null;
|
|
240
|
+
}
|
|
241
|
+
return null;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/* ── Filter + render ─────────────────────────────────────── */
|
|
245
|
+
function applyFilter() {
|
|
246
|
+
const q = (state.query || "").toLowerCase();
|
|
247
|
+
const all = getDirectors();
|
|
248
|
+
if (!q) {
|
|
249
|
+
state.filtered = all;
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
state.filtered = all.filter((d) =>
|
|
253
|
+
d.handle.toLowerCase().includes(q) ||
|
|
254
|
+
d.name.toLowerCase().includes(q),
|
|
255
|
+
);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function render() {
|
|
259
|
+
if (!pickerEl) return;
|
|
260
|
+
const list = pickerEl.querySelector("[data-mention-list]");
|
|
261
|
+
const count = pickerEl.querySelector("[data-mention-count]");
|
|
262
|
+
const selCount = state.selected.size;
|
|
263
|
+
count.textContent = selCount ? `${selCount} picked` : "";
|
|
264
|
+
if (!state.filtered.length) {
|
|
265
|
+
list.innerHTML = `<li class="mention-row-empty">No matching director</li>`;
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
list.innerHTML = state.filtered.map((d, i) => {
|
|
269
|
+
const checked = state.selected.has(d.id) ? "checked" : "";
|
|
270
|
+
const active = i === state.activeIdx ? "true" : "false";
|
|
271
|
+
const avHTML = d.avatar
|
|
272
|
+
? `<img class="mention-row-av" src="${escapeHtml(d.avatar)}" alt="">`
|
|
273
|
+
: `<span class="mention-row-av"></span>`;
|
|
274
|
+
const handleSuffix = d.isChair ? " · chair" : "";
|
|
275
|
+
return `
|
|
276
|
+
<li class="mention-row" data-mention-row data-agent-id="${escapeHtml(d.id)}"
|
|
277
|
+
data-handle="${escapeHtml(d.handle)}" data-active="${active}" data-idx="${i}">
|
|
278
|
+
<input type="checkbox" class="mention-row-cb" ${checked} tabindex="-1">
|
|
279
|
+
${avHTML}
|
|
280
|
+
<span class="mention-row-name">${escapeHtml(d.name)}</span>
|
|
281
|
+
<span class="mention-row-handle">@${escapeHtml(d.handle)}${handleSuffix}</span>
|
|
282
|
+
</li>
|
|
283
|
+
`;
|
|
284
|
+
}).join("");
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/* ── Textarea region rewrite ──────────────────────────────
|
|
288
|
+
The picker owns the substring `[trigger.start, zoneEnd)`. Every
|
|
289
|
+
check toggle calls this to materialise the current selection as
|
|
290
|
+
`@h1 @h2 ` (with trailing space when non-empty) or the original
|
|
291
|
+
`@<query>` (when empty). Setting textarea.value programmatically
|
|
292
|
+
would normally fire our input handler and reopen / close the
|
|
293
|
+
picker · we sidestep by NOT dispatching the input event and
|
|
294
|
+
instead calling autosize directly. */
|
|
295
|
+
function rebuildZone() {
|
|
296
|
+
if (!textarea || !state.trigger) return;
|
|
297
|
+
const inserts = state.selectedOrder
|
|
298
|
+
.map((id) => directorById(id))
|
|
299
|
+
.filter(Boolean);
|
|
300
|
+
const insertText = inserts.length
|
|
301
|
+
? inserts.map((d) => `@${d.handle}`).join(" ") + " "
|
|
302
|
+
: "@" + (state.query || "");
|
|
303
|
+
const before = textarea.value.substring(0, state.trigger.start);
|
|
304
|
+
const after = textarea.value.substring(state.zoneEnd);
|
|
305
|
+
textarea.value = before + insertText + after;
|
|
306
|
+
state.zoneEnd = state.trigger.start + insertText.length;
|
|
307
|
+
textarea.setSelectionRange(state.zoneEnd, state.zoneEnd);
|
|
308
|
+
// Autosize manually since we skipped dispatching `input`.
|
|
309
|
+
if (window.app && typeof window.app.autosizeRoomInputTextarea === "function") {
|
|
310
|
+
window.app.autosizeRoomInputTextarea();
|
|
311
|
+
}
|
|
312
|
+
textarea.focus();
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/* ── Open / close / toggle ───────────────────────────────── */
|
|
316
|
+
function open(ta, trigger) {
|
|
317
|
+
textarea = ta;
|
|
318
|
+
pickerEl = ensurePicker(ta);
|
|
319
|
+
if (!pickerEl) return;
|
|
320
|
+
state.open = true;
|
|
321
|
+
state.trigger = { start: trigger.start, end: trigger.end, query: trigger.query };
|
|
322
|
+
state.zoneEnd = trigger.end;
|
|
323
|
+
state.query = trigger.query || "";
|
|
324
|
+
state.selected = new Set();
|
|
325
|
+
state.selectedOrder = [];
|
|
326
|
+
state.activeIdx = 0;
|
|
327
|
+
applyFilter();
|
|
328
|
+
pickerEl.hidden = false;
|
|
329
|
+
render();
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
function close() {
|
|
333
|
+
if (!state.open) return;
|
|
334
|
+
// Persist any inserted handles for the next send. consumePendingMentions
|
|
335
|
+
// will dedupe against the textarea body so backspaced-out ones drop.
|
|
336
|
+
for (const id of state.selectedOrder) {
|
|
337
|
+
const d = directorById(id);
|
|
338
|
+
if (d) state.pending.push({ id: d.id, handle: d.handle });
|
|
339
|
+
}
|
|
340
|
+
state.open = false;
|
|
341
|
+
state.trigger = null;
|
|
342
|
+
state.zoneEnd = -1;
|
|
343
|
+
state.query = "";
|
|
344
|
+
state.selected = new Set();
|
|
345
|
+
state.selectedOrder = [];
|
|
346
|
+
state.activeIdx = -1;
|
|
347
|
+
if (pickerEl) pickerEl.hidden = true;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
function toggleSelected(id) {
|
|
351
|
+
if (!state.open) return;
|
|
352
|
+
if (state.selected.has(id)) {
|
|
353
|
+
state.selected.delete(id);
|
|
354
|
+
state.selectedOrder = state.selectedOrder.filter((x) => x !== id);
|
|
355
|
+
} else {
|
|
356
|
+
state.selected.add(id);
|
|
357
|
+
state.selectedOrder.push(id);
|
|
358
|
+
}
|
|
359
|
+
rebuildZone();
|
|
360
|
+
render();
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
function moveActive(delta) {
|
|
364
|
+
if (!state.filtered.length) return;
|
|
365
|
+
const n = state.filtered.length;
|
|
366
|
+
const next = (state.activeIdx + delta + n) % n;
|
|
367
|
+
state.activeIdx = next;
|
|
368
|
+
render();
|
|
369
|
+
const row = pickerEl && pickerEl.querySelector(`.mention-row[data-idx="${next}"]`);
|
|
370
|
+
if (row && row.scrollIntoView) row.scrollIntoView({ block: "nearest" });
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
function toggleActive() {
|
|
374
|
+
if (state.activeIdx < 0 || state.activeIdx >= state.filtered.length) return;
|
|
375
|
+
const d = state.filtered[state.activeIdx];
|
|
376
|
+
if (d) toggleSelected(d.id);
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
/* ── Event wiring ────────────────────────────────────────── */
|
|
380
|
+
// Input · detect new @ trigger, OR refine query while picker open
|
|
381
|
+
// (query refinement only allowed before first selection).
|
|
382
|
+
document.addEventListener("input", (e) => {
|
|
383
|
+
const ta = e.target;
|
|
384
|
+
if (!ta || !ta.matches || !ta.matches(".ib-textarea[data-send-input]")) return;
|
|
385
|
+
if (state.open) {
|
|
386
|
+
// Picker owns the input-bar region. Query refinement is only
|
|
387
|
+
// meaningful before any selection · once the user has checked
|
|
388
|
+
// anyone, the zone holds `@h1 @h2 ` (not `@<query>`) and free
|
|
389
|
+
// typing would corrupt the layout. Lock to selection-empty.
|
|
390
|
+
if (state.selectedOrder.length === 0) {
|
|
391
|
+
const trigger = findActiveMention(ta);
|
|
392
|
+
if (trigger && trigger.start === state.trigger.start) {
|
|
393
|
+
state.trigger = { start: trigger.start, end: trigger.end, query: trigger.query };
|
|
394
|
+
state.zoneEnd = trigger.end;
|
|
395
|
+
state.query = trigger.query || "";
|
|
396
|
+
applyFilter();
|
|
397
|
+
if (state.activeIdx >= state.filtered.length) state.activeIdx = Math.max(0, state.filtered.length - 1);
|
|
398
|
+
render();
|
|
399
|
+
} else {
|
|
400
|
+
close();
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
return;
|
|
404
|
+
}
|
|
405
|
+
const trigger = findActiveMention(ta);
|
|
406
|
+
if (trigger) open(ta, trigger);
|
|
407
|
+
});
|
|
408
|
+
|
|
409
|
+
// Keydown · arrow nav / Space toggle / Enter close / Esc close.
|
|
410
|
+
// Capture phase so we intercept BEFORE the global Enter→submit
|
|
411
|
+
// handler in app.js.
|
|
412
|
+
document.addEventListener("keydown", (e) => {
|
|
413
|
+
if (!state.open) return;
|
|
414
|
+
const ta = e.target;
|
|
415
|
+
if (!ta || !ta.matches || !ta.matches(".ib-textarea[data-send-input]")) return;
|
|
416
|
+
if (e.isComposing) return;
|
|
417
|
+
if (e.key === "Escape") {
|
|
418
|
+
e.preventDefault();
|
|
419
|
+
e.stopPropagation();
|
|
420
|
+
close();
|
|
421
|
+
return;
|
|
422
|
+
}
|
|
423
|
+
if (e.key === "ArrowDown") {
|
|
424
|
+
e.preventDefault();
|
|
425
|
+
moveActive(1);
|
|
426
|
+
return;
|
|
427
|
+
}
|
|
428
|
+
if (e.key === "ArrowUp") {
|
|
429
|
+
e.preventDefault();
|
|
430
|
+
moveActive(-1);
|
|
431
|
+
return;
|
|
432
|
+
}
|
|
433
|
+
if (e.key === "Enter" && !e.shiftKey) {
|
|
434
|
+
// Enter always closes the picker · the host's submit handler
|
|
435
|
+
// gets a clean keystroke right after (we don't preventDefault
|
|
436
|
+
// when there's no selection, so a plain @-then-Enter sends
|
|
437
|
+
// the message as expected).
|
|
438
|
+
if (state.selectedOrder.length > 0) {
|
|
439
|
+
e.preventDefault();
|
|
440
|
+
e.stopPropagation();
|
|
441
|
+
close();
|
|
442
|
+
} else {
|
|
443
|
+
close();
|
|
444
|
+
}
|
|
445
|
+
return;
|
|
446
|
+
}
|
|
447
|
+
if (e.key === " " || e.code === "Space") {
|
|
448
|
+
// Space toggles the active row · keeps multi-pick ergonomic
|
|
449
|
+
// without breaking out of the picker.
|
|
450
|
+
e.preventDefault();
|
|
451
|
+
e.stopPropagation();
|
|
452
|
+
toggleActive();
|
|
453
|
+
return;
|
|
454
|
+
}
|
|
455
|
+
}, true);
|
|
456
|
+
|
|
457
|
+
// Click row · toggle that director.
|
|
458
|
+
document.addEventListener("click", (e) => {
|
|
459
|
+
const row = e.target.closest && e.target.closest(".mention-row[data-agent-id]");
|
|
460
|
+
if (!row) return;
|
|
461
|
+
e.preventDefault();
|
|
462
|
+
e.stopPropagation();
|
|
463
|
+
const id = row.getAttribute("data-agent-id");
|
|
464
|
+
if (id) toggleSelected(id);
|
|
465
|
+
});
|
|
466
|
+
|
|
467
|
+
// Click outside menu · dismiss. Any click landing outside the
|
|
468
|
+
// picker's own DOM closes it · including clicks back into the
|
|
469
|
+
// textarea (user spec: blur from menu area = auto-dismiss). The
|
|
470
|
+
// setTimeout lets row-click handlers run to completion before the
|
|
471
|
+
// picker hides itself.
|
|
472
|
+
document.addEventListener("mousedown", (e) => {
|
|
473
|
+
if (!state.open) return;
|
|
474
|
+
if (_suppressClose) return;
|
|
475
|
+
if (e.target.closest && e.target.closest("[data-mention-picker]")) return;
|
|
476
|
+
setTimeout(close, HIDE_DEBOUNCE_MS);
|
|
477
|
+
});
|
|
478
|
+
|
|
479
|
+
// Keyboard blur · Tab / programmatic focus-shift off the textarea
|
|
480
|
+
// also dismisses the picker. focusout bubbles, so a single document
|
|
481
|
+
// listener works. Defer with a microtask so a row-click's refocus
|
|
482
|
+
// path (toggleSelected → rebuildZone → textarea.focus) wins the
|
|
483
|
+
// race when the click happens to flicker focus.
|
|
484
|
+
document.addEventListener("focusout", (e) => {
|
|
485
|
+
if (!state.open) return;
|
|
486
|
+
if (!textarea || e.target !== textarea) return;
|
|
487
|
+
setTimeout(() => {
|
|
488
|
+
if (!state.open) return;
|
|
489
|
+
// Focus returned to the textarea (likely a row click that ran
|
|
490
|
+
// textarea.focus()) · don't close.
|
|
491
|
+
if (document.activeElement === textarea) return;
|
|
492
|
+
// Focus moved into the picker itself (shouldn't happen since
|
|
493
|
+
// rows aren't focusable, but defensive) · don't close.
|
|
494
|
+
if (document.activeElement && document.activeElement.closest && document.activeElement.closest("[data-mention-picker]")) return;
|
|
495
|
+
close();
|
|
496
|
+
}, HIDE_DEBOUNCE_MS);
|
|
497
|
+
});
|
|
498
|
+
|
|
499
|
+
// Send button · drain selection before host submits.
|
|
500
|
+
document.addEventListener("click", (e) => {
|
|
501
|
+
const btn = e.target.closest && e.target.closest("[data-send-button]");
|
|
502
|
+
if (!btn) return;
|
|
503
|
+
if (state.open) close();
|
|
504
|
+
});
|
|
505
|
+
|
|
506
|
+
/* ── Public API ──────────────────────────────────────────── */
|
|
507
|
+
window.MentionPicker = {
|
|
508
|
+
consumePendingMentions(text) {
|
|
509
|
+
// If the picker is still open (user hit Enter to submit
|
|
510
|
+
// directly), flush its selection into pending first.
|
|
511
|
+
if (state.open) close();
|
|
512
|
+
if (!state.pending.length) return [];
|
|
513
|
+
const body = String(text || "");
|
|
514
|
+
const seen = new Set();
|
|
515
|
+
const out = [];
|
|
516
|
+
for (const p of state.pending) {
|
|
517
|
+
if (seen.has(p.id)) continue;
|
|
518
|
+
if (!body.includes(`@${p.handle}`)) continue;
|
|
519
|
+
seen.add(p.id);
|
|
520
|
+
out.push(p.id);
|
|
521
|
+
}
|
|
522
|
+
state.pending = [];
|
|
523
|
+
return out;
|
|
524
|
+
},
|
|
525
|
+
close() { close(); },
|
|
526
|
+
};
|
|
527
|
+
})();
|
package/public/new-agent.js
CHANGED
|
@@ -10,7 +10,6 @@
|
|
|
10
10
|
{ provider: "anthropic", models: [
|
|
11
11
|
{ v: "sonnet-4-6", name: "Sonnet 4.6", deck: "balanced · default" },
|
|
12
12
|
{ v: "opus-4-7", name: "Opus 4.7", deck: "deep reasoning" },
|
|
13
|
-
{ v: "opus-4-6", name: "Opus 4.6", deck: "prior-gen flagship" },
|
|
14
13
|
{ v: "opus-4-6-fast", name: "Opus 4.6 Fast", deck: "faster 4.6 · same intelligence" },
|
|
15
14
|
{ v: "haiku-4-5", name: "Haiku 4.5", deck: "fast · low-cost" }
|
|
16
15
|
]},
|
|
@@ -24,13 +23,19 @@
|
|
|
24
23
|
{ v: "gemini-3-flash", name: "Gemini 3 Flash", deck: "frontier flash · 1M ctx" },
|
|
25
24
|
{ v: "gemini-3-1-flash", name: "Gemini 3.1 Flash Lite", deck: "fast · 1M ctx" }
|
|
26
25
|
]},
|
|
27
|
-
{ provider: "xai", models: [
|
|
28
|
-
{ v: "grok-4-3", name: "Grok 4.3", deck: "flagship · 1M ctx" },
|
|
29
|
-
{ v: "grok-4-1-fast", name: "Grok 4.1 Fast", deck: "fast · 256k ctx" }
|
|
30
|
-
]},
|
|
31
26
|
{ provider: "deepseek", models: [
|
|
32
27
|
{ v: "deepseek-v4-pro", name: "DeepSeek V4 Pro", deck: "reasoning · open weights" },
|
|
33
28
|
{ v: "deepseek-v4-flash", name: "DeepSeek Lite", deck: "V4 Flash · fast · 1M ctx" }
|
|
29
|
+
]},
|
|
30
|
+
{ provider: "zhipu", models: [
|
|
31
|
+
{ v: "glm-5-1", name: "GLM 5.1", deck: "Zhipu flagship · 200k ctx" }
|
|
32
|
+
]},
|
|
33
|
+
{ provider: "moonshot", models: [
|
|
34
|
+
{ v: "kimi-k2-6", name: "Kimi K2.6", deck: "long-context" }
|
|
35
|
+
]},
|
|
36
|
+
{ provider: "minimax", models: [
|
|
37
|
+
{ v: "minimax-m2-7", name: "MiniMax M2.7", deck: "flagship · long-context" },
|
|
38
|
+
{ v: "minimax-m2-5", name: "MiniMax M2.5", deck: "prior · long-context" }
|
|
34
39
|
]}
|
|
35
40
|
];
|
|
36
41
|
const ALL_MODELS = MODEL_GROUPS.flatMap((g) => g.models);
|