@kevin5251984/guild 0.2.18 → 0.2.20
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/package.json +2 -2
- package/src/catalog/subagents.ts +2 -1
- package/src/chat-parts.ts +6 -1
- package/src/cli.ts +2 -5
- package/src/compact.ts +27 -0
- package/src/db.ts +236 -68
- package/src/generate.ts +59 -28
- package/src/handlers.ts +355 -84
- package/src/harness.ts +39 -10
- package/src/host-browse.ts +149 -4
- package/src/image-gen.ts +2 -1
- package/src/llm.ts +435 -71
- package/src/mcp.ts +32 -5
- package/src/memory.ts +69 -0
- package/src/mention.ts +189 -14
- package/src/oauth.ts +204 -27
- package/src/opencode-free.ts +248 -0
- package/src/public/buddy.js +432 -0
- package/src/public/chat.css +174 -72
- package/src/public/chat.html +354 -69
- package/src/public/i18n.js +44 -6
- package/src/public/index.html +1 -46
- package/src/public/mobile.css +597 -0
- package/src/public/mobile.html +954 -0
- package/src/public/settings.html +239 -35
- package/src/public/skills-add.html +8 -0
- package/src/public/studio.html +171 -117
- package/src/public/style.css +220 -26
- package/src/public/subagents-add.html +8 -0
- package/src/reasoning-catalog.ts +346 -0
- package/src/router.ts +184 -13
- package/src/store.ts +170 -5
- package/src/subagent.ts +239 -7
- package/src/tools.ts +132 -21
- package/src/trajectory.ts +72 -9
- package/src/usage.ts +10 -0
- package/src/version.ts +25 -0
- package/vendor/protocol/src/index.ts +27 -2
|
@@ -0,0 +1,954 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="zh-Hant">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
|
|
6
|
+
<meta name="theme-color" content="#0B0E12" />
|
|
7
|
+
<link rel="icon" href="/favicon.ico" sizes="32x32" />
|
|
8
|
+
<link rel="icon" href="/favicon.svg" type="image/svg+xml" />
|
|
9
|
+
<link rel="icon" href="/favicon-32.png" type="image/png" sizes="32x32" />
|
|
10
|
+
<link rel="icon" href="/favicon-16.png" type="image/png" sizes="16x16" />
|
|
11
|
+
<title>Guild — 外出</title>
|
|
12
|
+
<link rel="stylesheet" href="/mobile.css?v=enamel" />
|
|
13
|
+
<script src="/i18n.js"></script>
|
|
14
|
+
<script src="/md.js"></script>
|
|
15
|
+
</head>
|
|
16
|
+
<body class="m-away" data-i18n-page="mobile">
|
|
17
|
+
<section class="screen" id="screen-list">
|
|
18
|
+
<header class="list-head">
|
|
19
|
+
<h1>Guild</h1>
|
|
20
|
+
</header>
|
|
21
|
+
<p class="hint" data-i18n="mobile.hint">
|
|
22
|
+
外出時用 Tailscale 連到家裡這台 guildd(埠 7420)。
|
|
23
|
+
</p>
|
|
24
|
+
<div class="section-h" data-i18n="channels">委託</div>
|
|
25
|
+
<ul class="navlist" id="channels"></ul>
|
|
26
|
+
<div class="section-h" data-i18n="dms">密談</div>
|
|
27
|
+
<ul class="navlist" id="dms"></ul>
|
|
28
|
+
</section>
|
|
29
|
+
<section class="screen" id="screen-room" hidden>
|
|
30
|
+
<header class="room-head">
|
|
31
|
+
<button type="button" class="back" id="back" data-i18n-title="mobile.back" data-i18n-aria="mobile.back" title="返回委託" aria-label="返回委託">‹</button>
|
|
32
|
+
<h1 id="title"></h1>
|
|
33
|
+
<div class="meta" id="subtitle"></div>
|
|
34
|
+
</header>
|
|
35
|
+
<div class="thread" id="thread"></div>
|
|
36
|
+
<form class="composer" id="composer">
|
|
37
|
+
<div class="composer-card">
|
|
38
|
+
<div class="mention-pop" id="mention-pop" hidden>
|
|
39
|
+
<div id="mention-results"></div>
|
|
40
|
+
</div>
|
|
41
|
+
<textarea id="draft" rows="1" placeholder="Ask anything" data-i18n-placeholder="askAnything"></textarea>
|
|
42
|
+
<div class="composer-steer" id="composer-steer" hidden></div>
|
|
43
|
+
<div class="composer-actions">
|
|
44
|
+
<button type="submit" class="send" id="send" data-i18n="send">送出</button>
|
|
45
|
+
<button type="button" class="stop-all" id="stop" hidden data-i18n="live.stop">停止</button>
|
|
46
|
+
<button type="button" class="steer" id="steer" hidden data-i18n="steer.insert">插入引導</button>
|
|
47
|
+
</div>
|
|
48
|
+
</div>
|
|
49
|
+
</form>
|
|
50
|
+
</section>
|
|
51
|
+
<div class="flash" id="flash" role="status" aria-live="polite" hidden></div>
|
|
52
|
+
<script>
|
|
53
|
+
(function () {
|
|
54
|
+
const COLORS = ["#7c6af7", "#5b8def", "#2ea887", "#e07a3d", "#d4537e"];
|
|
55
|
+
const SEAT_HUE = {
|
|
56
|
+
infra: "#5b8def",
|
|
57
|
+
pm: "#2ea887",
|
|
58
|
+
rd: "#e07a3d",
|
|
59
|
+
design: "#7c6af7",
|
|
60
|
+
marketing: "#d4537e",
|
|
61
|
+
};
|
|
62
|
+
const state = {
|
|
63
|
+
channels: [],
|
|
64
|
+
bots: [],
|
|
65
|
+
serverLive: [],
|
|
66
|
+
messages: [],
|
|
67
|
+
lives: [],
|
|
68
|
+
kind: "",
|
|
69
|
+
id: "",
|
|
70
|
+
posting: false,
|
|
71
|
+
mentionIndex: 0,
|
|
72
|
+
pollTimer: 0,
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
function hue(key) {
|
|
76
|
+
const seat = SEAT_HUE[String(key).toLowerCase()];
|
|
77
|
+
if (seat) return seat;
|
|
78
|
+
let n = 0;
|
|
79
|
+
for (const ch of String(key)) n = (n + ch.charCodeAt(0) * 17) % COLORS.length;
|
|
80
|
+
return COLORS[n];
|
|
81
|
+
}
|
|
82
|
+
function escapeHtml(value) {
|
|
83
|
+
return String(value)
|
|
84
|
+
.replace(/&/g, "&")
|
|
85
|
+
.replace(/</g, "<")
|
|
86
|
+
.replace(/>/g, ">");
|
|
87
|
+
}
|
|
88
|
+
function initials(name) {
|
|
89
|
+
const parts = String(name)
|
|
90
|
+
.replace(/[^\p{L}\p{N} ]/gu, " ")
|
|
91
|
+
.trim()
|
|
92
|
+
.split(/\s+/);
|
|
93
|
+
if (parts[0] && /[A-Za-z]/.test(parts[0][0])) {
|
|
94
|
+
const letters = parts.slice(0, 2).map((part) => part[0]).filter(Boolean);
|
|
95
|
+
if (
|
|
96
|
+
letters.length === 2 &&
|
|
97
|
+
/[A-Za-z]/.test(letters[0]) &&
|
|
98
|
+
/[A-Za-z]/.test(letters[1])
|
|
99
|
+
) {
|
|
100
|
+
return letters.join("").toUpperCase();
|
|
101
|
+
}
|
|
102
|
+
return letters[0].toUpperCase();
|
|
103
|
+
}
|
|
104
|
+
const glyph = Array.from(String(name).replace(/\s+/g, ""))[0] || "?";
|
|
105
|
+
return glyph;
|
|
106
|
+
}
|
|
107
|
+
function portraitUrl(bot) {
|
|
108
|
+
const url = bot && typeof bot.portrait === "string" ? bot.portrait.trim() : "";
|
|
109
|
+
return url.indexOf("/generated/") === 0 ? url : "";
|
|
110
|
+
}
|
|
111
|
+
function avatarFace(bot) {
|
|
112
|
+
const src = portraitUrl(bot);
|
|
113
|
+
if (src) return '<img alt="" src="' + escapeHtml(src) + '">';
|
|
114
|
+
return escapeHtml(initials((bot && (bot.name || bot.handle)) || "?"));
|
|
115
|
+
}
|
|
116
|
+
function botById(id) {
|
|
117
|
+
return (state.bots || []).find((bot) => bot.id === id);
|
|
118
|
+
}
|
|
119
|
+
function currentChannel() {
|
|
120
|
+
return (state.channels || []).find((ch) => ch.id === state.id);
|
|
121
|
+
}
|
|
122
|
+
function currentRoomRef() {
|
|
123
|
+
return state.kind ? { kind: state.kind, id: state.id } : null;
|
|
124
|
+
}
|
|
125
|
+
function roomMessagesUrl() {
|
|
126
|
+
return state.kind === "dm"
|
|
127
|
+
? "/dms/" + encodeURIComponent(state.id) + "/messages"
|
|
128
|
+
: "/channels/" + encodeURIComponent(state.id) + "/messages";
|
|
129
|
+
}
|
|
130
|
+
function liveUrl() {
|
|
131
|
+
const r = currentRoomRef();
|
|
132
|
+
if (!r) return "";
|
|
133
|
+
return r.kind === "dm"
|
|
134
|
+
? "/dms/" + encodeURIComponent(r.id) + "/live"
|
|
135
|
+
: "/channels/" + encodeURIComponent(r.id) + "/live";
|
|
136
|
+
}
|
|
137
|
+
function abortUrl() {
|
|
138
|
+
const r = currentRoomRef();
|
|
139
|
+
if (!r) return "";
|
|
140
|
+
return r.kind === "dm"
|
|
141
|
+
? "/dms/" + encodeURIComponent(r.id) + "/abort"
|
|
142
|
+
: "/channels/" + encodeURIComponent(r.id) + "/abort";
|
|
143
|
+
}
|
|
144
|
+
function steerUrl() {
|
|
145
|
+
const r = currentRoomRef();
|
|
146
|
+
if (!r) return "";
|
|
147
|
+
return r.kind === "dm"
|
|
148
|
+
? "/dms/" + encodeURIComponent(r.id) + "/steer"
|
|
149
|
+
: "/channels/" + encodeURIComponent(r.id) + "/steer";
|
|
150
|
+
}
|
|
151
|
+
function parseHash() {
|
|
152
|
+
const raw = location.hash.replace(/^#/, "");
|
|
153
|
+
if (raw.startsWith("d/")) {
|
|
154
|
+
state.kind = "dm";
|
|
155
|
+
state.id = decodeURIComponent(raw.slice(2));
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
if (raw.startsWith("c/")) {
|
|
159
|
+
state.kind = "channel";
|
|
160
|
+
state.id = decodeURIComponent(raw.slice(2));
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
state.kind = "";
|
|
164
|
+
state.id = "";
|
|
165
|
+
}
|
|
166
|
+
function showList() {
|
|
167
|
+
location.hash = "";
|
|
168
|
+
}
|
|
169
|
+
function fitShell() {
|
|
170
|
+
const h = window.visualViewport ? window.visualViewport.height : window.innerHeight;
|
|
171
|
+
document.documentElement.style.setProperty("--vvh", h + "px");
|
|
172
|
+
}
|
|
173
|
+
function toast(text, opts) {
|
|
174
|
+
const el = document.getElementById("flash");
|
|
175
|
+
const ok = Boolean(opts && opts.ok);
|
|
176
|
+
el.hidden = false;
|
|
177
|
+
el.textContent = text;
|
|
178
|
+
el.classList.toggle("err", !ok);
|
|
179
|
+
el.classList.add("on");
|
|
180
|
+
clearTimeout(toast.timer);
|
|
181
|
+
toast.timer = setTimeout(() => {
|
|
182
|
+
el.classList.remove("on");
|
|
183
|
+
}, (opts && opts.ms) || 1800);
|
|
184
|
+
}
|
|
185
|
+
function relTime(iso) {
|
|
186
|
+
if (!iso) return t("noActivity");
|
|
187
|
+
const at = Date.parse(iso);
|
|
188
|
+
if (!Number.isFinite(at)) return t("noActivity");
|
|
189
|
+
const d = Date.now() - at;
|
|
190
|
+
if (d < 45000) return t("justNow");
|
|
191
|
+
if (d < 3600000) {
|
|
192
|
+
return t("minutesAgo", { n: Math.max(1, Math.floor(d / 60000)) });
|
|
193
|
+
}
|
|
194
|
+
const when = new Date(at);
|
|
195
|
+
return (
|
|
196
|
+
String(when.getMonth() + 1).padStart(2, "0") +
|
|
197
|
+
"/" +
|
|
198
|
+
String(when.getDate()).padStart(2, "0")
|
|
199
|
+
);
|
|
200
|
+
}
|
|
201
|
+
function previewLine(text) {
|
|
202
|
+
return String(text || "")
|
|
203
|
+
.replace(/```[\s\S]*?```/g, " ")
|
|
204
|
+
.replace(/[*_`#]+/g, "")
|
|
205
|
+
.replace(/\s+/g, " ")
|
|
206
|
+
.trim()
|
|
207
|
+
.slice(0, 88);
|
|
208
|
+
}
|
|
209
|
+
function roomLiveRows(kind, id) {
|
|
210
|
+
return (state.serverLive || []).filter(
|
|
211
|
+
(row) => row.kind === kind && row.id === id,
|
|
212
|
+
);
|
|
213
|
+
}
|
|
214
|
+
function liveTurnsFrom(body) {
|
|
215
|
+
const rows =
|
|
216
|
+
Array.isArray(body && body.bots) && body.bots.length
|
|
217
|
+
? body.bots
|
|
218
|
+
: body && body.botId
|
|
219
|
+
? [body]
|
|
220
|
+
: [];
|
|
221
|
+
return rows
|
|
222
|
+
.filter((row) => row && row.botId)
|
|
223
|
+
.map((row) => ({
|
|
224
|
+
botId: row.botId,
|
|
225
|
+
thinking: row.thinking || "",
|
|
226
|
+
steps: Array.isArray(row.steps) ? row.steps.slice(-5) : [],
|
|
227
|
+
startedAt: row.startedAt || "",
|
|
228
|
+
}));
|
|
229
|
+
}
|
|
230
|
+
function isBusy() {
|
|
231
|
+
return state.posting || (state.lives || []).length > 0;
|
|
232
|
+
}
|
|
233
|
+
function mentionScanText(text) {
|
|
234
|
+
return String(text || "")
|
|
235
|
+
.replace(/```[\s\S]*?```/g, " ")
|
|
236
|
+
.replace(/`[^`]*`/g, " ");
|
|
237
|
+
}
|
|
238
|
+
function deferredHandles(text) {
|
|
239
|
+
const raw = String(text || "");
|
|
240
|
+
const out = [];
|
|
241
|
+
const add = (name) => {
|
|
242
|
+
const key = String(name || "").toLowerCase();
|
|
243
|
+
if (key && out.indexOf(key) < 0) out.push(key);
|
|
244
|
+
};
|
|
245
|
+
const scan = raw.replace(/```[\s\S]*?```/g, " ");
|
|
246
|
+
const cue =
|
|
247
|
+
/(?:完成後|通過後|最後(?:才|由)?|才需要|才叫|才交)[^@\n]{0,24}@([A-Za-z0-9_-]+)/gi;
|
|
248
|
+
const bare =
|
|
249
|
+
/(?:完成後|才需要|才叫)\s*(?:call|ask|由)\s*`?@?([A-Za-z0-9_-]+)`?/gi;
|
|
250
|
+
let row;
|
|
251
|
+
while ((row = cue.exec(scan))) add(row[1]);
|
|
252
|
+
while ((row = bare.exec(scan))) add(row[1]);
|
|
253
|
+
const later =
|
|
254
|
+
/^[ \t]*(?:\d+[.)、]\s*|[-*•]\s+)(?:通過後|最後|然後|之後|完成後)\s*`?@([A-Za-z0-9_-]+)`?/i;
|
|
255
|
+
const lines = raw.split(/\r?\n/);
|
|
256
|
+
for (let i = 0; i < lines.length; i++) {
|
|
257
|
+
const hit = lines[i].match(later);
|
|
258
|
+
if (hit) add(hit[1]);
|
|
259
|
+
}
|
|
260
|
+
return out;
|
|
261
|
+
}
|
|
262
|
+
function summonedBotIds(text) {
|
|
263
|
+
const raw = String(text || "");
|
|
264
|
+
const deferred = {};
|
|
265
|
+
deferredHandles(raw).forEach((handle) => {
|
|
266
|
+
deferred[handle] = true;
|
|
267
|
+
});
|
|
268
|
+
const members = new Set(
|
|
269
|
+
state.kind === "channel"
|
|
270
|
+
? ((currentChannel() && currentChannel().memberIds) || [])
|
|
271
|
+
: state.kind === "dm"
|
|
272
|
+
? [state.id]
|
|
273
|
+
: [],
|
|
274
|
+
);
|
|
275
|
+
const known = new Map(
|
|
276
|
+
(state.bots || [])
|
|
277
|
+
.filter((bot) => !members.size || members.has(bot.id))
|
|
278
|
+
.map((bot) => [
|
|
279
|
+
String(bot.handle).toLowerCase(),
|
|
280
|
+
bot.id,
|
|
281
|
+
]),
|
|
282
|
+
);
|
|
283
|
+
const out = [];
|
|
284
|
+
const push = (id, handle) => {
|
|
285
|
+
if (id && !deferred[handle] && out.indexOf(id) < 0) out.push(id);
|
|
286
|
+
};
|
|
287
|
+
let fence = false;
|
|
288
|
+
const lines = raw.split(/\r?\n/);
|
|
289
|
+
const lead = /^[ \t]*((?:@[A-Za-z0-9_-]+[\s,、]*)+)/;
|
|
290
|
+
for (let i = 0; i < lines.length; i++) {
|
|
291
|
+
const line = lines[i];
|
|
292
|
+
const trimmed = line.trim();
|
|
293
|
+
if (trimmed.indexOf("```") === 0) {
|
|
294
|
+
fence = !fence;
|
|
295
|
+
continue;
|
|
296
|
+
}
|
|
297
|
+
if (fence) continue;
|
|
298
|
+
const m = line.match(lead);
|
|
299
|
+
if (!m) continue;
|
|
300
|
+
const names = m[1].match(/@([A-Za-z0-9_-]+)/g) || [];
|
|
301
|
+
for (let n = 0; n < names.length; n++) {
|
|
302
|
+
const handle = names[n].slice(1).toLowerCase();
|
|
303
|
+
push(known.get(handle), handle);
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
if (out.length) return out;
|
|
307
|
+
const scan = mentionScanText(raw);
|
|
308
|
+
const re = /(?:^|\s)@([A-Za-z0-9_-]+)\b/g;
|
|
309
|
+
let row;
|
|
310
|
+
while ((row = re.exec(scan))) {
|
|
311
|
+
const handle = String(row[1]).toLowerCase();
|
|
312
|
+
push(known.get(handle), handle);
|
|
313
|
+
}
|
|
314
|
+
return out;
|
|
315
|
+
}
|
|
316
|
+
function lastBotAuthor() {
|
|
317
|
+
const msgs = state.messages || [];
|
|
318
|
+
for (let i = msgs.length - 1; i >= 0; i--) {
|
|
319
|
+
const author = msgs[i].author;
|
|
320
|
+
if (author && author !== "you" && botById(author)) return author;
|
|
321
|
+
}
|
|
322
|
+
return "";
|
|
323
|
+
}
|
|
324
|
+
function botsFromSend(text) {
|
|
325
|
+
if (state.kind === "dm") return [state.id];
|
|
326
|
+
const scan = mentionScanText(text);
|
|
327
|
+
if (/(^|\s)@(channel|quest|here|all)\b/i.test(scan)) {
|
|
328
|
+
const ch = currentChannel();
|
|
329
|
+
return (ch && ch.memberIds) || [];
|
|
330
|
+
}
|
|
331
|
+
const ids = summonedBotIds(text);
|
|
332
|
+
if (ids.length) return ids;
|
|
333
|
+
const last = lastBotAuthor();
|
|
334
|
+
return last ? [last] : [];
|
|
335
|
+
}
|
|
336
|
+
function visibleAssistantText(text) {
|
|
337
|
+
return String(text || "")
|
|
338
|
+
.replace(/<skill_content\b[\s\S]*?<\/skill_content>/gi, "")
|
|
339
|
+
.replace(/<system-reminder>[\s\S]*?<\/system-reminder>/gi, "")
|
|
340
|
+
.replace(/<available_skills>[\s\S]*?<\/available_skills>/gi, "")
|
|
341
|
+
.trim();
|
|
342
|
+
}
|
|
343
|
+
function formatBody(text) {
|
|
344
|
+
const cleaned = visibleAssistantText(text);
|
|
345
|
+
if (cleaned.length > 8000) return escapeHtml(cleaned.slice(-8000));
|
|
346
|
+
return typeof renderMarkdown === "function"
|
|
347
|
+
? renderMarkdown(cleaned)
|
|
348
|
+
: escapeHtml(cleaned);
|
|
349
|
+
}
|
|
350
|
+
function liveStepLabel(name) {
|
|
351
|
+
if (name === "think") return t("think");
|
|
352
|
+
if (name === "skill") return t("skill");
|
|
353
|
+
if (name === "image_gen") return t("imageGen");
|
|
354
|
+
if (name === "spawn") return t("spawn");
|
|
355
|
+
if (name === "run") return t("bash");
|
|
356
|
+
if (name === "read") return t("read");
|
|
357
|
+
if (name === "write") return t("write");
|
|
358
|
+
if (name === "list") return t("list");
|
|
359
|
+
return name || "";
|
|
360
|
+
}
|
|
361
|
+
function liveStepsHtml(steps) {
|
|
362
|
+
const rows = (steps || []).slice(-5);
|
|
363
|
+
if (!rows.length) return "";
|
|
364
|
+
return (
|
|
365
|
+
'<ul class="live-steps">' +
|
|
366
|
+
rows
|
|
367
|
+
.map((step) => {
|
|
368
|
+
const running = step.running ? " running" : "";
|
|
369
|
+
const detail = String(step.detail || "").trim();
|
|
370
|
+
return (
|
|
371
|
+
'<li class="live-step' +
|
|
372
|
+
running +
|
|
373
|
+
'"><span class="live-name">' +
|
|
374
|
+
escapeHtml(liveStepLabel(step.name)) +
|
|
375
|
+
"</span>" +
|
|
376
|
+
(detail
|
|
377
|
+
? '<span class="live-detail">' + escapeHtml(detail) + "</span>"
|
|
378
|
+
: "") +
|
|
379
|
+
"</li>"
|
|
380
|
+
);
|
|
381
|
+
})
|
|
382
|
+
.join("") +
|
|
383
|
+
"</ul>"
|
|
384
|
+
);
|
|
385
|
+
}
|
|
386
|
+
function turnStatusHtml() {
|
|
387
|
+
if (!isBusy() || !state.lives.length) return "";
|
|
388
|
+
return state.lives
|
|
389
|
+
.map((live) => {
|
|
390
|
+
const bot = botById(live.botId);
|
|
391
|
+
if (!bot) return "";
|
|
392
|
+
const started = live.startedAt ? Date.parse(live.startedAt) : NaN;
|
|
393
|
+
const elapsed = Number.isFinite(started)
|
|
394
|
+
? Date.now() - started
|
|
395
|
+
: 0;
|
|
396
|
+
const clock =
|
|
397
|
+
elapsed >= 15000
|
|
398
|
+
? '<span class="clock">' + Math.floor(elapsed / 1000) + "s</span>"
|
|
399
|
+
: "";
|
|
400
|
+
return (
|
|
401
|
+
'<article class="msg bot live" data-bot-id="' +
|
|
402
|
+
escapeHtml(bot.id) +
|
|
403
|
+
'"><span class="avatar" style="background:' +
|
|
404
|
+
hue(bot.handle) +
|
|
405
|
+
'">' +
|
|
406
|
+
avatarFace(bot) +
|
|
407
|
+
'</span><div class="msg-main"><div class="msg-head"><span class="name">' +
|
|
408
|
+
escapeHtml(bot.name) +
|
|
409
|
+
'</span><span class="handle">@' +
|
|
410
|
+
escapeHtml(bot.handle) +
|
|
411
|
+
'</span></div><div class="turn-live"><div class="turn-status">' +
|
|
412
|
+
t("deepDiving") +
|
|
413
|
+
clock +
|
|
414
|
+
"</div>" +
|
|
415
|
+
liveStepsHtml(live.steps) +
|
|
416
|
+
'<div class="turn-actions"><button type="button" class="turn-stop" data-live-stop="' +
|
|
417
|
+
escapeHtml(bot.id) +
|
|
418
|
+
'">' +
|
|
419
|
+
t("live.stop") +
|
|
420
|
+
'</button><button type="button" class="turn-steer" data-live-steer="' +
|
|
421
|
+
escapeHtml(bot.id) +
|
|
422
|
+
'">' +
|
|
423
|
+
t("live.steer") +
|
|
424
|
+
"</button></div></div></div></article>"
|
|
425
|
+
);
|
|
426
|
+
})
|
|
427
|
+
.join("");
|
|
428
|
+
}
|
|
429
|
+
function msgHtml(msg) {
|
|
430
|
+
const you = msg.author === "you";
|
|
431
|
+
const bot = you ? null : botById(msg.author);
|
|
432
|
+
const name = you ? t("you") : bot ? bot.name : msg.author;
|
|
433
|
+
const handle = bot ? bot.handle : "";
|
|
434
|
+
const avStyle = bot ? "background:" + hue(bot.handle) : "";
|
|
435
|
+
const face = you
|
|
436
|
+
? escapeHtml(initials(t("you")))
|
|
437
|
+
: bot
|
|
438
|
+
? avatarFace(bot)
|
|
439
|
+
: "?";
|
|
440
|
+
const body = you
|
|
441
|
+
? '<p class="bubble">' + escapeHtml(msg.body || "") + "</p>"
|
|
442
|
+
: '<div class="bubble"><div class="assistant-text md">' +
|
|
443
|
+
formatBody(msg.body || "") +
|
|
444
|
+
"</div></div>";
|
|
445
|
+
return (
|
|
446
|
+
'<article class="msg' +
|
|
447
|
+
(you ? " you" : " bot") +
|
|
448
|
+
'"><span class="avatar" style="' +
|
|
449
|
+
avStyle +
|
|
450
|
+
'">' +
|
|
451
|
+
face +
|
|
452
|
+
'</span><div class="msg-main"><div class="msg-head"><span class="name">' +
|
|
453
|
+
escapeHtml(name) +
|
|
454
|
+
"</span>" +
|
|
455
|
+
(handle
|
|
456
|
+
? '<span class="handle">@' + escapeHtml(handle) + "</span>"
|
|
457
|
+
: "") +
|
|
458
|
+
'<span class="clock">' +
|
|
459
|
+
escapeHtml(relTime(msg.createdAt)) +
|
|
460
|
+
"</span></div>" +
|
|
461
|
+
body +
|
|
462
|
+
"</div></article>"
|
|
463
|
+
);
|
|
464
|
+
}
|
|
465
|
+
function renderThread(opts) {
|
|
466
|
+
const root = document.getElementById("thread");
|
|
467
|
+
const pin =
|
|
468
|
+
Boolean(opts && opts.pinBottom) ||
|
|
469
|
+
root.scrollHeight - root.scrollTop - root.clientHeight < 80;
|
|
470
|
+
const all = state.messages || [];
|
|
471
|
+
const cap = 30;
|
|
472
|
+
const truncated = all.length > cap;
|
|
473
|
+
const rows = truncated ? all.slice(-cap) : all;
|
|
474
|
+
if (!rows.length && !isBusy()) {
|
|
475
|
+
root.innerHTML =
|
|
476
|
+
'<div class="empty-thread">' + escapeHtml(t("noMessages")) + "</div>";
|
|
477
|
+
return;
|
|
478
|
+
}
|
|
479
|
+
const notice = truncated
|
|
480
|
+
? '<div class="empty-thread">' +
|
|
481
|
+
escapeHtml(t("mobile.recent", { n: cap })) +
|
|
482
|
+
"</div>"
|
|
483
|
+
: "";
|
|
484
|
+
root.innerHTML = notice + rows.map(msgHtml).join("") + turnStatusHtml();
|
|
485
|
+
if (pin) root.scrollTop = root.scrollHeight;
|
|
486
|
+
}
|
|
487
|
+
function steerModLabel() {
|
|
488
|
+
return /Mac|iPhone|iPad/.test(navigator.platform || "") ? "\u2318" : "Ctrl+";
|
|
489
|
+
}
|
|
490
|
+
function syncSteerHint() {
|
|
491
|
+
const hint = document.getElementById("composer-steer");
|
|
492
|
+
if (!hint) return;
|
|
493
|
+
if (!isBusy()) {
|
|
494
|
+
hint.hidden = true;
|
|
495
|
+
hint.textContent = "";
|
|
496
|
+
return;
|
|
497
|
+
}
|
|
498
|
+
const hintKey = t("steer.hint", { mod: steerModLabel() });
|
|
499
|
+
hint.hidden = false;
|
|
500
|
+
hint.textContent = hintKey === "steer.hint" ? t("live.steer") : hintKey;
|
|
501
|
+
}
|
|
502
|
+
function syncComposer() {
|
|
503
|
+
const busy = isBusy();
|
|
504
|
+
document.getElementById("send").hidden = busy;
|
|
505
|
+
document.getElementById("stop").hidden = !busy;
|
|
506
|
+
const steer = document.getElementById("steer");
|
|
507
|
+
if (steer) steer.hidden = !busy;
|
|
508
|
+
syncSteerHint();
|
|
509
|
+
}
|
|
510
|
+
function navRow(opts) {
|
|
511
|
+
const live = opts.busy
|
|
512
|
+
? '<span class="live-dot">' + escapeHtml(t("busy")) + "</span>"
|
|
513
|
+
: "";
|
|
514
|
+
return (
|
|
515
|
+
'<li><a class="' +
|
|
516
|
+
(opts.branch ? "branch" : "") +
|
|
517
|
+
'" href="' +
|
|
518
|
+
escapeHtml(opts.href) +
|
|
519
|
+
'">' +
|
|
520
|
+
opts.av +
|
|
521
|
+
'<span class="nav-copy"><strong>' +
|
|
522
|
+
escapeHtml(opts.name) +
|
|
523
|
+
live +
|
|
524
|
+
"</strong><em>" +
|
|
525
|
+
escapeHtml(opts.preview || t("noActivity")) +
|
|
526
|
+
"</em></span></a></li>"
|
|
527
|
+
);
|
|
528
|
+
}
|
|
529
|
+
function renderList() {
|
|
530
|
+
const all = state.channels || [];
|
|
531
|
+
const byId = new Map(all.map((ch) => [ch.id, ch]));
|
|
532
|
+
const kidsOf = (id) => all.filter((ch) => ch.parentId === id);
|
|
533
|
+
const roots = all.filter((ch) => !ch.parentId);
|
|
534
|
+
const seen = new Set();
|
|
535
|
+
const rows = [];
|
|
536
|
+
function walk(ch, depth) {
|
|
537
|
+
if (seen.has(ch.id)) return;
|
|
538
|
+
seen.add(ch.id);
|
|
539
|
+
const last = ch.lastMessage;
|
|
540
|
+
rows.push(
|
|
541
|
+
navRow({
|
|
542
|
+
href: "#c/" + encodeURIComponent(ch.id),
|
|
543
|
+
branch: depth > 0,
|
|
544
|
+
name: (depth ? "↳ " : "#") + ch.name,
|
|
545
|
+
preview: last
|
|
546
|
+
? previewLine(last.body) + " · " + relTime(last.createdAt)
|
|
547
|
+
: relTime(ch.updatedAt),
|
|
548
|
+
busy: roomLiveRows("channel", ch.id).length > 0,
|
|
549
|
+
av: '<span class="avatar">#</span>',
|
|
550
|
+
}),
|
|
551
|
+
);
|
|
552
|
+
kidsOf(ch.id).forEach((kid) => walk(kid, depth + 1));
|
|
553
|
+
}
|
|
554
|
+
roots.forEach((ch) => walk(ch, 0));
|
|
555
|
+
all.forEach((ch) => {
|
|
556
|
+
if (!seen.has(ch.id) && (!ch.parentId || !byId.has(ch.parentId))) {
|
|
557
|
+
walk(ch, 0);
|
|
558
|
+
}
|
|
559
|
+
});
|
|
560
|
+
document.getElementById("channels").innerHTML = rows.length
|
|
561
|
+
? rows.join("")
|
|
562
|
+
: '<li class="empty-list">' + escapeHtml(t("noActivity")) + "</li>";
|
|
563
|
+
document.getElementById("dms").innerHTML = (state.bots || [])
|
|
564
|
+
.map((bot) => {
|
|
565
|
+
const last = bot.lastMessage;
|
|
566
|
+
return navRow({
|
|
567
|
+
href: "#d/" + encodeURIComponent(bot.id),
|
|
568
|
+
name: bot.name,
|
|
569
|
+
preview: last
|
|
570
|
+
? previewLine(last.body) + " · " + relTime(last.createdAt)
|
|
571
|
+
: "@" + bot.handle,
|
|
572
|
+
busy: roomLiveRows("dm", bot.id).length > 0,
|
|
573
|
+
av:
|
|
574
|
+
'<span class="avatar" style="background:' +
|
|
575
|
+
hue(bot.handle) +
|
|
576
|
+
'">' +
|
|
577
|
+
avatarFace(bot) +
|
|
578
|
+
"</span>",
|
|
579
|
+
});
|
|
580
|
+
})
|
|
581
|
+
.join("");
|
|
582
|
+
}
|
|
583
|
+
function renderHead() {
|
|
584
|
+
const title = document.getElementById("title");
|
|
585
|
+
const sub = document.getElementById("subtitle");
|
|
586
|
+
if (state.kind === "dm") {
|
|
587
|
+
const bot = botById(state.id);
|
|
588
|
+
title.textContent = bot ? bot.name : state.id;
|
|
589
|
+
sub.textContent = bot ? "@" + bot.handle : "";
|
|
590
|
+
return;
|
|
591
|
+
}
|
|
592
|
+
const ch = currentChannel();
|
|
593
|
+
title.textContent = ch ? "#" + ch.name : state.id;
|
|
594
|
+
const n = ch && ch.members ? ch.members.length : 0;
|
|
595
|
+
sub.textContent = n ? String(n) : "";
|
|
596
|
+
}
|
|
597
|
+
function showScreens() {
|
|
598
|
+
const list = !state.kind;
|
|
599
|
+
document.getElementById("screen-list").hidden = !list;
|
|
600
|
+
document.getElementById("screen-room").hidden = list;
|
|
601
|
+
}
|
|
602
|
+
async function loadWorkspace() {
|
|
603
|
+
const data = await fetch("/workspace").then((r) => r.json());
|
|
604
|
+
state.channels = data.channels || [];
|
|
605
|
+
state.bots = data.bots || [];
|
|
606
|
+
state.serverLive = Array.isArray(data.live) ? data.live : [];
|
|
607
|
+
renderList();
|
|
608
|
+
if (state.kind) renderHead();
|
|
609
|
+
}
|
|
610
|
+
async function loadMessages(opts) {
|
|
611
|
+
if (!state.kind) return;
|
|
612
|
+
const res = await fetch(roomMessagesUrl());
|
|
613
|
+
const body = await res.json().catch(() => []);
|
|
614
|
+
if (!res.ok) throw new Error(body.error || "load failed");
|
|
615
|
+
state.messages = Array.isArray(body) ? body : [];
|
|
616
|
+
renderThread({ pinBottom: !(opts && opts.merge) });
|
|
617
|
+
}
|
|
618
|
+
async function pollLive() {
|
|
619
|
+
if (!state.kind) return;
|
|
620
|
+
const url = liveUrl();
|
|
621
|
+
if (!url) return;
|
|
622
|
+
try {
|
|
623
|
+
const res = await fetch(url);
|
|
624
|
+
const body = await res.json().catch(() => ({}));
|
|
625
|
+
if (!res.ok) return;
|
|
626
|
+
const lives = liveTurnsFrom(body);
|
|
627
|
+
const was = state.lives.map((row) => row.botId).join(",");
|
|
628
|
+
const now = lives.map((row) => row.botId).join(",");
|
|
629
|
+
state.lives = lives;
|
|
630
|
+
if (!lives.length && !state.posting) {
|
|
631
|
+
if (was) await loadMessages({ merge: true }).catch(() => {});
|
|
632
|
+
else renderThread();
|
|
633
|
+
} else {
|
|
634
|
+
if (now !== was) await loadMessages({ merge: true }).catch(() => {});
|
|
635
|
+
else renderThread();
|
|
636
|
+
}
|
|
637
|
+
syncComposer();
|
|
638
|
+
} catch {
|
|
639
|
+
/* poll is best-effort */
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
async function tick() {
|
|
643
|
+
try {
|
|
644
|
+
if (!state.kind) await loadWorkspace();
|
|
645
|
+
else {
|
|
646
|
+
await pollLive();
|
|
647
|
+
if (!isBusy()) await loadMessages({ merge: true }).catch(() => {});
|
|
648
|
+
}
|
|
649
|
+
} finally {
|
|
650
|
+
schedule();
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
function schedule() {
|
|
654
|
+
clearTimeout(state.pollTimer);
|
|
655
|
+
const ms = !state.kind ? 2500 : isBusy() ? 900 : 2200;
|
|
656
|
+
state.pollTimer = setTimeout(tick, ms);
|
|
657
|
+
}
|
|
658
|
+
async function openFromHash() {
|
|
659
|
+
parseHash();
|
|
660
|
+
hideMention();
|
|
661
|
+
showScreens();
|
|
662
|
+
await loadWorkspace();
|
|
663
|
+
if (state.kind) {
|
|
664
|
+
renderHead();
|
|
665
|
+
await loadMessages().catch((err) => toast(err.message || String(err)));
|
|
666
|
+
await pollLive();
|
|
667
|
+
syncComposer();
|
|
668
|
+
const thread = document.getElementById("thread");
|
|
669
|
+
thread.scrollTop = thread.scrollHeight;
|
|
670
|
+
}
|
|
671
|
+
schedule();
|
|
672
|
+
}
|
|
673
|
+
function mentionAtCursor(el) {
|
|
674
|
+
const start = el.selectionStart || 0;
|
|
675
|
+
const before = el.value.slice(0, start);
|
|
676
|
+
const m = before.match(/(^|\s)@([A-Za-z0-9_-]*)$/);
|
|
677
|
+
if (!m) return null;
|
|
678
|
+
return { start: start - m[2].length - 1, query: m[2].toLowerCase() };
|
|
679
|
+
}
|
|
680
|
+
function mentionChoices(query) {
|
|
681
|
+
const needle = String(query || "").toLowerCase();
|
|
682
|
+
return (state.bots || []).filter((bot) => {
|
|
683
|
+
if (!needle) return true;
|
|
684
|
+
return (
|
|
685
|
+
bot.handle.toLowerCase().indexOf(needle) >= 0 ||
|
|
686
|
+
bot.name.toLowerCase().indexOf(needle) >= 0
|
|
687
|
+
);
|
|
688
|
+
});
|
|
689
|
+
}
|
|
690
|
+
function hideMention() {
|
|
691
|
+
document.getElementById("mention-pop").hidden = true;
|
|
692
|
+
}
|
|
693
|
+
function renderMention() {
|
|
694
|
+
const draft = document.getElementById("draft");
|
|
695
|
+
const at = mentionAtCursor(draft);
|
|
696
|
+
const pop = document.getElementById("mention-pop");
|
|
697
|
+
if (!at) {
|
|
698
|
+
hideMention();
|
|
699
|
+
return;
|
|
700
|
+
}
|
|
701
|
+
const rows = mentionChoices(at.query);
|
|
702
|
+
if (!rows.length) {
|
|
703
|
+
hideMention();
|
|
704
|
+
return;
|
|
705
|
+
}
|
|
706
|
+
if (state.mentionIndex >= rows.length) state.mentionIndex = 0;
|
|
707
|
+
if (state.mentionIndex < 0) state.mentionIndex = rows.length - 1;
|
|
708
|
+
document.getElementById("mention-results").innerHTML = rows
|
|
709
|
+
.map((bot, index) => {
|
|
710
|
+
const on = index === state.mentionIndex ? " on" : "";
|
|
711
|
+
return (
|
|
712
|
+
'<button type="button" class="mention-item' +
|
|
713
|
+
on +
|
|
714
|
+
'" data-mention="' +
|
|
715
|
+
index +
|
|
716
|
+
'" data-handle="' +
|
|
717
|
+
escapeHtml(bot.handle) +
|
|
718
|
+
'"><span class="mention-glyph" style="background:' +
|
|
719
|
+
hue(bot.handle) +
|
|
720
|
+
'">' +
|
|
721
|
+
avatarFace(bot) +
|
|
722
|
+
"</span><span><strong>" +
|
|
723
|
+
escapeHtml(bot.name) +
|
|
724
|
+
"</strong><em>@" +
|
|
725
|
+
escapeHtml(bot.handle) +
|
|
726
|
+
"</em></span></button>"
|
|
727
|
+
);
|
|
728
|
+
})
|
|
729
|
+
.join("");
|
|
730
|
+
pop.hidden = false;
|
|
731
|
+
}
|
|
732
|
+
function insertMention(handle) {
|
|
733
|
+
const draft = document.getElementById("draft");
|
|
734
|
+
const at = mentionAtCursor(draft);
|
|
735
|
+
if (!at) return;
|
|
736
|
+
const after = draft.value.slice(draft.selectionStart || 0);
|
|
737
|
+
const next = draft.value.slice(0, at.start) + "@" + handle + " " + after;
|
|
738
|
+
draft.value = next;
|
|
739
|
+
const pos = at.start + handle.length + 2;
|
|
740
|
+
draft.setSelectionRange(pos, pos);
|
|
741
|
+
hideMention();
|
|
742
|
+
fitDraft();
|
|
743
|
+
draft.focus();
|
|
744
|
+
}
|
|
745
|
+
function pickMention(index) {
|
|
746
|
+
const btn = document.querySelector(
|
|
747
|
+
'.mention-item[data-mention="' + index + '"]',
|
|
748
|
+
);
|
|
749
|
+
if (btn) insertMention(btn.getAttribute("data-handle"));
|
|
750
|
+
}
|
|
751
|
+
function fitDraft() {
|
|
752
|
+
const el = document.getElementById("draft");
|
|
753
|
+
el.style.height = "auto";
|
|
754
|
+
el.style.height = Math.min(el.scrollHeight, 144) + "px";
|
|
755
|
+
}
|
|
756
|
+
async function stopTurn(botId) {
|
|
757
|
+
try {
|
|
758
|
+
await fetch(abortUrl(), {
|
|
759
|
+
method: "POST",
|
|
760
|
+
headers: { "content-type": "application/json" },
|
|
761
|
+
body: JSON.stringify(botId ? { botId: botId } : {}),
|
|
762
|
+
});
|
|
763
|
+
} catch {
|
|
764
|
+
/* ignore */
|
|
765
|
+
}
|
|
766
|
+
state.lives = botId
|
|
767
|
+
? state.lives.filter((row) => row.botId !== botId)
|
|
768
|
+
: [];
|
|
769
|
+
state.posting = false;
|
|
770
|
+
syncComposer();
|
|
771
|
+
renderThread();
|
|
772
|
+
await loadMessages().catch(() => {});
|
|
773
|
+
}
|
|
774
|
+
async function sendSteer(raw, botId) {
|
|
775
|
+
const text = String(raw || "").trim();
|
|
776
|
+
if (!text || !state.kind) return;
|
|
777
|
+
const payload = { body: text };
|
|
778
|
+
if (botId) payload.botId = botId;
|
|
779
|
+
else {
|
|
780
|
+
const ids = botsFromSend(text);
|
|
781
|
+
if (ids.length === 1) payload.botId = ids[0];
|
|
782
|
+
}
|
|
783
|
+
const res = await fetch(steerUrl(), {
|
|
784
|
+
method: "POST",
|
|
785
|
+
headers: { "content-type": "application/json" },
|
|
786
|
+
body: JSON.stringify(payload),
|
|
787
|
+
});
|
|
788
|
+
const body = await res.json().catch(() => ({}));
|
|
789
|
+
if (!res.ok) throw new Error(body.error || "steer failed");
|
|
790
|
+
await loadMessages({ merge: true }).catch(() => {});
|
|
791
|
+
await pollLive();
|
|
792
|
+
const who = body.message && body.message.steerBotId
|
|
793
|
+
? botById(body.message.steerBotId)
|
|
794
|
+
: null;
|
|
795
|
+
toast(
|
|
796
|
+
who ? t("steer.tagTo", { who: who.name || who.handle }) : t("steer.tag"),
|
|
797
|
+
{ ok: true, ms: 2000 },
|
|
798
|
+
);
|
|
799
|
+
}
|
|
800
|
+
async function steerDraft(botId) {
|
|
801
|
+
const draft = document.getElementById("draft");
|
|
802
|
+
const raw = draft.value.trim();
|
|
803
|
+
if (!raw) {
|
|
804
|
+
draft.focus();
|
|
805
|
+
toast(t("live.steer"), { ok: true, ms: 1400 });
|
|
806
|
+
return;
|
|
807
|
+
}
|
|
808
|
+
draft.value = "";
|
|
809
|
+
fitDraft();
|
|
810
|
+
hideMention();
|
|
811
|
+
state.messages = state.messages.concat([
|
|
812
|
+
{
|
|
813
|
+
id: "pending-steer-" + Date.now(),
|
|
814
|
+
author: "you",
|
|
815
|
+
body: raw,
|
|
816
|
+
createdAt: new Date().toISOString(),
|
|
817
|
+
},
|
|
818
|
+
]);
|
|
819
|
+
renderThread();
|
|
820
|
+
try {
|
|
821
|
+
await sendSteer(raw, botId);
|
|
822
|
+
} catch (err) {
|
|
823
|
+
draft.value = raw;
|
|
824
|
+
fitDraft();
|
|
825
|
+
await loadMessages().catch(() => {});
|
|
826
|
+
toast(err.message || String(err));
|
|
827
|
+
}
|
|
828
|
+
syncComposer();
|
|
829
|
+
schedule();
|
|
830
|
+
}
|
|
831
|
+
async function sendDraft() {
|
|
832
|
+
const draft = document.getElementById("draft");
|
|
833
|
+
const raw = draft.value.trim();
|
|
834
|
+
if (!raw || !state.kind || isBusy()) return;
|
|
835
|
+
const botIds = botsFromSend(raw);
|
|
836
|
+
const payload = { body: raw };
|
|
837
|
+
if (botIds.length) payload.mentions = botIds;
|
|
838
|
+
if (botIds.length === 1) payload.assigneeId = botIds[0];
|
|
839
|
+
draft.value = "";
|
|
840
|
+
fitDraft();
|
|
841
|
+
hideMention();
|
|
842
|
+
state.messages = state.messages.concat([
|
|
843
|
+
{
|
|
844
|
+
id: "pending-you-" + Date.now(),
|
|
845
|
+
author: "you",
|
|
846
|
+
body: raw,
|
|
847
|
+
createdAt: new Date().toISOString(),
|
|
848
|
+
},
|
|
849
|
+
]);
|
|
850
|
+
state.posting = true;
|
|
851
|
+
syncComposer();
|
|
852
|
+
renderThread();
|
|
853
|
+
try {
|
|
854
|
+
const res = await fetch(roomMessagesUrl(), {
|
|
855
|
+
method: "POST",
|
|
856
|
+
headers: { "content-type": "application/json" },
|
|
857
|
+
body: JSON.stringify(payload),
|
|
858
|
+
});
|
|
859
|
+
const body = await res.json().catch(() => ({}));
|
|
860
|
+
if (!res.ok) throw new Error(body.error || "send failed");
|
|
861
|
+
await loadMessages();
|
|
862
|
+
await pollLive();
|
|
863
|
+
} catch (err) {
|
|
864
|
+
if (err && err.name === "AbortError") return;
|
|
865
|
+
toast(err.message || String(err));
|
|
866
|
+
draft.value = raw;
|
|
867
|
+
fitDraft();
|
|
868
|
+
await loadMessages().catch(() => {});
|
|
869
|
+
} finally {
|
|
870
|
+
state.posting = false;
|
|
871
|
+
syncComposer();
|
|
872
|
+
schedule();
|
|
873
|
+
}
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
document.getElementById("back").addEventListener("click", () => {
|
|
877
|
+
showList();
|
|
878
|
+
});
|
|
879
|
+
document.getElementById("composer").addEventListener("submit", (event) => {
|
|
880
|
+
event.preventDefault();
|
|
881
|
+
sendDraft();
|
|
882
|
+
});
|
|
883
|
+
document.getElementById("stop").addEventListener("click", () => {
|
|
884
|
+
stopTurn();
|
|
885
|
+
});
|
|
886
|
+
document.getElementById("steer").addEventListener("click", () => {
|
|
887
|
+
steerDraft().catch((err) => toast(err.message || String(err)));
|
|
888
|
+
});
|
|
889
|
+
document.getElementById("thread").addEventListener("click", (event) => {
|
|
890
|
+
const stop = event.target.closest("[data-live-stop]");
|
|
891
|
+
if (stop) {
|
|
892
|
+
event.preventDefault();
|
|
893
|
+
stopTurn(stop.getAttribute("data-live-stop"));
|
|
894
|
+
return;
|
|
895
|
+
}
|
|
896
|
+
const steer = event.target.closest("[data-live-steer]");
|
|
897
|
+
if (!steer) return;
|
|
898
|
+
event.preventDefault();
|
|
899
|
+
steerDraft(steer.getAttribute("data-live-steer")).catch((err) =>
|
|
900
|
+
toast(err.message || String(err)),
|
|
901
|
+
);
|
|
902
|
+
});
|
|
903
|
+
document.getElementById("draft").addEventListener("input", () => {
|
|
904
|
+
fitDraft();
|
|
905
|
+
renderMention();
|
|
906
|
+
});
|
|
907
|
+
document.getElementById("draft").addEventListener("keydown", (event) => {
|
|
908
|
+
const pop = document.getElementById("mention-pop");
|
|
909
|
+
if (
|
|
910
|
+
pop.hidden &&
|
|
911
|
+
event.key === "Enter" &&
|
|
912
|
+
!event.shiftKey &&
|
|
913
|
+
(event.metaKey || event.ctrlKey)
|
|
914
|
+
) {
|
|
915
|
+
if (isBusy()) {
|
|
916
|
+
event.preventDefault();
|
|
917
|
+
steerDraft().catch((err) => toast(err.message || String(err)));
|
|
918
|
+
}
|
|
919
|
+
return;
|
|
920
|
+
}
|
|
921
|
+
if (pop.hidden) return;
|
|
922
|
+
if (event.key === "ArrowDown") {
|
|
923
|
+
event.preventDefault();
|
|
924
|
+
state.mentionIndex += 1;
|
|
925
|
+
renderMention();
|
|
926
|
+
} else if (event.key === "ArrowUp") {
|
|
927
|
+
event.preventDefault();
|
|
928
|
+
state.mentionIndex -= 1;
|
|
929
|
+
renderMention();
|
|
930
|
+
} else if (event.key === "Enter" || event.key === "Tab") {
|
|
931
|
+
event.preventDefault();
|
|
932
|
+
pickMention(state.mentionIndex);
|
|
933
|
+
} else if (event.key === "Escape") {
|
|
934
|
+
hideMention();
|
|
935
|
+
}
|
|
936
|
+
});
|
|
937
|
+
document.getElementById("mention-results").addEventListener("click", (event) => {
|
|
938
|
+
const btn = event.target.closest("[data-handle]");
|
|
939
|
+
if (!btn) return;
|
|
940
|
+
insertMention(btn.getAttribute("data-handle"));
|
|
941
|
+
});
|
|
942
|
+
window.addEventListener("hashchange", () => {
|
|
943
|
+
openFromHash().catch((err) => toast(err.message || String(err)));
|
|
944
|
+
});
|
|
945
|
+
window.addEventListener("resize", fitShell);
|
|
946
|
+
if (window.visualViewport) {
|
|
947
|
+
window.visualViewport.addEventListener("resize", fitShell);
|
|
948
|
+
}
|
|
949
|
+
fitShell();
|
|
950
|
+
openFromHash().catch((err) => toast(err.message || String(err)));
|
|
951
|
+
})();
|
|
952
|
+
</script>
|
|
953
|
+
</body>
|
|
954
|
+
</html>
|