@kevin5251984/guild 0.2.17 → 0.2.19
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 +1 -1
- package/src/catalog/subagents.ts +2 -1
- package/src/chat-parts.ts +6 -1
- package/src/compact.ts +27 -0
- package/src/db.ts +199 -64
- package/src/generate.ts +58 -27
- package/src/handlers.ts +341 -54
- package/src/harness.ts +21 -8
- package/src/image-gen.ts +2 -1
- package/src/llm.ts +400 -58
- package/src/memory.ts +69 -0
- package/src/mention.ts +119 -12
- package/src/oauth.ts +202 -25
- package/src/opencode-free.ts +247 -0
- package/src/public/buddy.js +432 -0
- package/src/public/chat.css +174 -72
- package/src/public/chat.html +212 -43
- package/src/public/i18n.js +41 -6
- package/src/public/index.html +1 -46
- package/src/public/mobile.css +491 -0
- package/src/public/mobile.html +784 -0
- package/src/public/settings.html +188 -27
- 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/router.ts +76 -5
- package/src/store.ts +161 -5
- package/src/subagent.ts +233 -6
- package/src/tools.ts +124 -18
- package/src/trajectory.ts +72 -9
- package/src/usage.ts +10 -0
- package/vendor/protocol/src/index.ts +14 -1
|
@@ -0,0 +1,784 @@
|
|
|
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=away" />
|
|
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-actions">
|
|
43
|
+
<button type="submit" class="send" id="send" data-i18n="send">送出</button>
|
|
44
|
+
<button type="button" class="stop-all" id="stop" hidden data-i18n="live.stop">停止</button>
|
|
45
|
+
</div>
|
|
46
|
+
</div>
|
|
47
|
+
</form>
|
|
48
|
+
</section>
|
|
49
|
+
<div class="flash" id="flash" hidden></div>
|
|
50
|
+
<script>
|
|
51
|
+
(function () {
|
|
52
|
+
const COLORS = ["#7c6af7", "#5b8def", "#2ea887", "#e07a3d", "#d4537e"];
|
|
53
|
+
const state = {
|
|
54
|
+
channels: [],
|
|
55
|
+
bots: [],
|
|
56
|
+
serverLive: [],
|
|
57
|
+
messages: [],
|
|
58
|
+
lives: [],
|
|
59
|
+
kind: "",
|
|
60
|
+
id: "",
|
|
61
|
+
posting: false,
|
|
62
|
+
mentionIndex: 0,
|
|
63
|
+
pollTimer: 0,
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
function hue(key) {
|
|
67
|
+
let n = 0;
|
|
68
|
+
for (const ch of String(key)) n = (n + ch.charCodeAt(0) * 17) % COLORS.length;
|
|
69
|
+
return COLORS[n];
|
|
70
|
+
}
|
|
71
|
+
function escapeHtml(value) {
|
|
72
|
+
return String(value)
|
|
73
|
+
.replace(/&/g, "&")
|
|
74
|
+
.replace(/</g, "<")
|
|
75
|
+
.replace(/>/g, ">");
|
|
76
|
+
}
|
|
77
|
+
function initials(name) {
|
|
78
|
+
const parts = String(name)
|
|
79
|
+
.replace(/[^\p{L}\p{N} ]/gu, " ")
|
|
80
|
+
.trim()
|
|
81
|
+
.split(/\s+/);
|
|
82
|
+
if (parts[0] && /[A-Za-z]/.test(parts[0][0])) {
|
|
83
|
+
return parts[0][0].toUpperCase();
|
|
84
|
+
}
|
|
85
|
+
return Array.from(String(name).replace(/\s+/g, ""))[0] || "?";
|
|
86
|
+
}
|
|
87
|
+
function portraitUrl(bot) {
|
|
88
|
+
const url = bot && typeof bot.portrait === "string" ? bot.portrait.trim() : "";
|
|
89
|
+
return url.indexOf("/generated/") === 0 ? url : "";
|
|
90
|
+
}
|
|
91
|
+
function avatarFace(bot) {
|
|
92
|
+
const src = portraitUrl(bot);
|
|
93
|
+
if (src) return '<img alt="" src="' + escapeHtml(src) + '">';
|
|
94
|
+
return escapeHtml(initials((bot && (bot.name || bot.handle)) || "?"));
|
|
95
|
+
}
|
|
96
|
+
function botById(id) {
|
|
97
|
+
return (state.bots || []).find((bot) => bot.id === id);
|
|
98
|
+
}
|
|
99
|
+
function currentChannel() {
|
|
100
|
+
return (state.channels || []).find((ch) => ch.id === state.id);
|
|
101
|
+
}
|
|
102
|
+
function currentRoomRef() {
|
|
103
|
+
return state.kind ? { kind: state.kind, id: state.id } : null;
|
|
104
|
+
}
|
|
105
|
+
function roomMessagesUrl() {
|
|
106
|
+
return state.kind === "dm"
|
|
107
|
+
? "/dms/" + encodeURIComponent(state.id) + "/messages"
|
|
108
|
+
: "/channels/" + encodeURIComponent(state.id) + "/messages";
|
|
109
|
+
}
|
|
110
|
+
function liveUrl() {
|
|
111
|
+
const r = currentRoomRef();
|
|
112
|
+
if (!r) return "";
|
|
113
|
+
return r.kind === "dm"
|
|
114
|
+
? "/dms/" + encodeURIComponent(r.id) + "/live"
|
|
115
|
+
: "/channels/" + encodeURIComponent(r.id) + "/live";
|
|
116
|
+
}
|
|
117
|
+
function abortUrl() {
|
|
118
|
+
const r = currentRoomRef();
|
|
119
|
+
if (!r) return "";
|
|
120
|
+
return r.kind === "dm"
|
|
121
|
+
? "/dms/" + encodeURIComponent(r.id) + "/abort"
|
|
122
|
+
: "/channels/" + encodeURIComponent(r.id) + "/abort";
|
|
123
|
+
}
|
|
124
|
+
function parseHash() {
|
|
125
|
+
const raw = location.hash.replace(/^#/, "");
|
|
126
|
+
if (raw.startsWith("d/")) {
|
|
127
|
+
state.kind = "dm";
|
|
128
|
+
state.id = decodeURIComponent(raw.slice(2));
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
if (raw.startsWith("c/")) {
|
|
132
|
+
state.kind = "channel";
|
|
133
|
+
state.id = decodeURIComponent(raw.slice(2));
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
state.kind = "";
|
|
137
|
+
state.id = "";
|
|
138
|
+
}
|
|
139
|
+
function showList() {
|
|
140
|
+
location.hash = "";
|
|
141
|
+
}
|
|
142
|
+
function fitShell() {
|
|
143
|
+
const h = window.visualViewport ? window.visualViewport.height : window.innerHeight;
|
|
144
|
+
document.documentElement.style.setProperty("--vvh", h + "px");
|
|
145
|
+
}
|
|
146
|
+
function toast(text) {
|
|
147
|
+
const el = document.getElementById("flash");
|
|
148
|
+
el.hidden = false;
|
|
149
|
+
el.textContent = text;
|
|
150
|
+
el.classList.add("on");
|
|
151
|
+
clearTimeout(toast.timer);
|
|
152
|
+
toast.timer = setTimeout(() => {
|
|
153
|
+
el.classList.remove("on");
|
|
154
|
+
}, 1800);
|
|
155
|
+
}
|
|
156
|
+
function relTime(iso) {
|
|
157
|
+
if (!iso) return t("noActivity");
|
|
158
|
+
const at = Date.parse(iso);
|
|
159
|
+
if (!Number.isFinite(at)) return t("noActivity");
|
|
160
|
+
const d = Date.now() - at;
|
|
161
|
+
if (d < 45000) return t("justNow");
|
|
162
|
+
if (d < 3600000) {
|
|
163
|
+
return t("minutesAgo", { n: Math.max(1, Math.floor(d / 60000)) });
|
|
164
|
+
}
|
|
165
|
+
const when = new Date(at);
|
|
166
|
+
return (
|
|
167
|
+
String(when.getMonth() + 1).padStart(2, "0") +
|
|
168
|
+
"/" +
|
|
169
|
+
String(when.getDate()).padStart(2, "0")
|
|
170
|
+
);
|
|
171
|
+
}
|
|
172
|
+
function previewLine(text) {
|
|
173
|
+
return String(text || "")
|
|
174
|
+
.replace(/```[\s\S]*?```/g, " ")
|
|
175
|
+
.replace(/[*_`#]+/g, "")
|
|
176
|
+
.replace(/\s+/g, " ")
|
|
177
|
+
.trim()
|
|
178
|
+
.slice(0, 88);
|
|
179
|
+
}
|
|
180
|
+
function roomLiveRows(kind, id) {
|
|
181
|
+
return (state.serverLive || []).filter(
|
|
182
|
+
(row) => row.kind === kind && row.id === id,
|
|
183
|
+
);
|
|
184
|
+
}
|
|
185
|
+
function liveTurnsFrom(body) {
|
|
186
|
+
const rows =
|
|
187
|
+
Array.isArray(body && body.bots) && body.bots.length
|
|
188
|
+
? body.bots
|
|
189
|
+
: body && body.botId
|
|
190
|
+
? [body]
|
|
191
|
+
: [];
|
|
192
|
+
return rows
|
|
193
|
+
.filter((row) => row && row.botId)
|
|
194
|
+
.map((row) => ({
|
|
195
|
+
botId: row.botId,
|
|
196
|
+
thinking: row.thinking || "",
|
|
197
|
+
steps: Array.isArray(row.steps) ? row.steps.slice(-5) : [],
|
|
198
|
+
startedAt: row.startedAt || "",
|
|
199
|
+
}));
|
|
200
|
+
}
|
|
201
|
+
function isBusy() {
|
|
202
|
+
return state.posting || (state.lives || []).length > 0;
|
|
203
|
+
}
|
|
204
|
+
function mentionScanText(text) {
|
|
205
|
+
return String(text || "")
|
|
206
|
+
.replace(/```[\s\S]*?```/g, " ")
|
|
207
|
+
.replace(/`[^`]*`/g, " ");
|
|
208
|
+
}
|
|
209
|
+
function summonedBotIds(text) {
|
|
210
|
+
const raw = String(text || "");
|
|
211
|
+
const known = new Map(
|
|
212
|
+
(state.bots || []).map((bot) => [
|
|
213
|
+
String(bot.handle).toLowerCase(),
|
|
214
|
+
bot.id,
|
|
215
|
+
]),
|
|
216
|
+
);
|
|
217
|
+
const out = [];
|
|
218
|
+
const push = (id) => {
|
|
219
|
+
if (id && out.indexOf(id) < 0) out.push(id);
|
|
220
|
+
};
|
|
221
|
+
let fence = false;
|
|
222
|
+
const lines = raw.split(/\r?\n/);
|
|
223
|
+
const lead = /^[ \t]*((?:@[A-Za-z0-9_-]+[\s,、]*)+)/;
|
|
224
|
+
for (let i = 0; i < lines.length; i++) {
|
|
225
|
+
const line = lines[i];
|
|
226
|
+
const trimmed = line.trim();
|
|
227
|
+
if (trimmed.indexOf("```") === 0) {
|
|
228
|
+
fence = !fence;
|
|
229
|
+
continue;
|
|
230
|
+
}
|
|
231
|
+
if (fence) continue;
|
|
232
|
+
const m = line.match(lead);
|
|
233
|
+
if (!m) continue;
|
|
234
|
+
const names = m[1].match(/@([A-Za-z0-9_-]+)/g) || [];
|
|
235
|
+
for (let n = 0; n < names.length; n++) {
|
|
236
|
+
push(known.get(names[n].slice(1).toLowerCase()));
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
if (out.length) return out;
|
|
240
|
+
const scan = mentionScanText(raw);
|
|
241
|
+
const re = /(?:^|\s)@([A-Za-z0-9_-]+)\b/g;
|
|
242
|
+
let row;
|
|
243
|
+
while ((row = re.exec(scan))) {
|
|
244
|
+
push(known.get(String(row[1]).toLowerCase()));
|
|
245
|
+
}
|
|
246
|
+
return out;
|
|
247
|
+
}
|
|
248
|
+
function lastBotAuthor() {
|
|
249
|
+
const msgs = state.messages || [];
|
|
250
|
+
for (let i = msgs.length - 1; i >= 0; i--) {
|
|
251
|
+
const author = msgs[i].author;
|
|
252
|
+
if (author && author !== "you" && botById(author)) return author;
|
|
253
|
+
}
|
|
254
|
+
return "";
|
|
255
|
+
}
|
|
256
|
+
function botsFromSend(text) {
|
|
257
|
+
if (state.kind === "dm") return [state.id];
|
|
258
|
+
const scan = mentionScanText(text);
|
|
259
|
+
if (/(^|\s)@(channel|quest|here|all)\b/i.test(scan)) {
|
|
260
|
+
const ch = currentChannel();
|
|
261
|
+
return (ch && ch.memberIds) || [];
|
|
262
|
+
}
|
|
263
|
+
const ids = summonedBotIds(text);
|
|
264
|
+
if (ids.length) return ids;
|
|
265
|
+
const last = lastBotAuthor();
|
|
266
|
+
return last ? [last] : [];
|
|
267
|
+
}
|
|
268
|
+
function visibleAssistantText(text) {
|
|
269
|
+
return String(text || "")
|
|
270
|
+
.replace(/<skill_content\b[\s\S]*?<\/skill_content>/gi, "")
|
|
271
|
+
.replace(/<system-reminder>[\s\S]*?<\/system-reminder>/gi, "")
|
|
272
|
+
.replace(/<available_skills>[\s\S]*?<\/available_skills>/gi, "")
|
|
273
|
+
.trim();
|
|
274
|
+
}
|
|
275
|
+
function formatBody(text) {
|
|
276
|
+
const cleaned = visibleAssistantText(text);
|
|
277
|
+
if (cleaned.length > 8000) return escapeHtml(cleaned.slice(-8000));
|
|
278
|
+
return typeof renderMarkdown === "function"
|
|
279
|
+
? renderMarkdown(cleaned)
|
|
280
|
+
: escapeHtml(cleaned);
|
|
281
|
+
}
|
|
282
|
+
function liveStepLabel(name) {
|
|
283
|
+
if (name === "think") return t("think");
|
|
284
|
+
if (name === "skill") return t("skill");
|
|
285
|
+
if (name === "image_gen") return t("imageGen");
|
|
286
|
+
if (name === "spawn") return t("spawn");
|
|
287
|
+
if (name === "run") return t("bash");
|
|
288
|
+
if (name === "read") return t("read");
|
|
289
|
+
if (name === "write") return t("write");
|
|
290
|
+
if (name === "list") return t("list");
|
|
291
|
+
return name || "";
|
|
292
|
+
}
|
|
293
|
+
function liveStepsHtml(steps) {
|
|
294
|
+
const rows = (steps || []).slice(-5);
|
|
295
|
+
if (!rows.length) return "";
|
|
296
|
+
return (
|
|
297
|
+
'<ul class="live-steps">' +
|
|
298
|
+
rows
|
|
299
|
+
.map((step) => {
|
|
300
|
+
const running = step.running ? " running" : "";
|
|
301
|
+
const detail = String(step.detail || "").trim();
|
|
302
|
+
return (
|
|
303
|
+
'<li class="live-step' +
|
|
304
|
+
running +
|
|
305
|
+
'"><span class="live-name">' +
|
|
306
|
+
escapeHtml(liveStepLabel(step.name)) +
|
|
307
|
+
"</span>" +
|
|
308
|
+
(detail
|
|
309
|
+
? '<span class="live-detail">' + escapeHtml(detail) + "</span>"
|
|
310
|
+
: "") +
|
|
311
|
+
"</li>"
|
|
312
|
+
);
|
|
313
|
+
})
|
|
314
|
+
.join("") +
|
|
315
|
+
"</ul>"
|
|
316
|
+
);
|
|
317
|
+
}
|
|
318
|
+
function turnStatusHtml() {
|
|
319
|
+
if (!isBusy() || !state.lives.length) return "";
|
|
320
|
+
return state.lives
|
|
321
|
+
.map((live) => {
|
|
322
|
+
const bot = botById(live.botId);
|
|
323
|
+
if (!bot) return "";
|
|
324
|
+
const started = live.startedAt ? Date.parse(live.startedAt) : NaN;
|
|
325
|
+
const elapsed = Number.isFinite(started)
|
|
326
|
+
? Date.now() - started
|
|
327
|
+
: 0;
|
|
328
|
+
const clock =
|
|
329
|
+
elapsed >= 15000
|
|
330
|
+
? '<span class="clock">' + Math.floor(elapsed / 1000) + "s</span>"
|
|
331
|
+
: "";
|
|
332
|
+
return (
|
|
333
|
+
'<article class="msg bot live" data-bot-id="' +
|
|
334
|
+
escapeHtml(bot.id) +
|
|
335
|
+
'"><span class="avatar" style="background:' +
|
|
336
|
+
hue(bot.handle) +
|
|
337
|
+
'">' +
|
|
338
|
+
avatarFace(bot) +
|
|
339
|
+
'</span><div class="msg-main"><div class="msg-head"><span class="name">' +
|
|
340
|
+
escapeHtml(bot.name) +
|
|
341
|
+
'</span><span class="handle">@' +
|
|
342
|
+
escapeHtml(bot.handle) +
|
|
343
|
+
'</span></div><div class="turn-live"><div class="turn-status">' +
|
|
344
|
+
t("deepDiving") +
|
|
345
|
+
clock +
|
|
346
|
+
"</div>" +
|
|
347
|
+
liveStepsHtml(live.steps) +
|
|
348
|
+
'<div class="turn-actions"><button type="button" class="turn-stop" data-live-stop="' +
|
|
349
|
+
escapeHtml(bot.id) +
|
|
350
|
+
'">' +
|
|
351
|
+
t("live.stop") +
|
|
352
|
+
"</button></div></div></div></article>"
|
|
353
|
+
);
|
|
354
|
+
})
|
|
355
|
+
.join("");
|
|
356
|
+
}
|
|
357
|
+
function msgHtml(msg) {
|
|
358
|
+
const you = msg.author === "you";
|
|
359
|
+
const bot = you ? null : botById(msg.author);
|
|
360
|
+
const name = you ? t("you") : bot ? bot.name : msg.author;
|
|
361
|
+
const handle = bot ? bot.handle : "";
|
|
362
|
+
const avStyle = bot ? "background:" + hue(bot.handle) : "";
|
|
363
|
+
const face = you
|
|
364
|
+
? escapeHtml(initials(t("you")))
|
|
365
|
+
: bot
|
|
366
|
+
? avatarFace(bot)
|
|
367
|
+
: "?";
|
|
368
|
+
const body = you
|
|
369
|
+
? '<p class="bubble">' + escapeHtml(msg.body || "") + "</p>"
|
|
370
|
+
: '<div class="bubble"><div class="assistant-text md">' +
|
|
371
|
+
formatBody(msg.body || "") +
|
|
372
|
+
"</div></div>";
|
|
373
|
+
return (
|
|
374
|
+
'<article class="msg' +
|
|
375
|
+
(you ? " you" : " bot") +
|
|
376
|
+
'"><span class="avatar" style="' +
|
|
377
|
+
avStyle +
|
|
378
|
+
'">' +
|
|
379
|
+
face +
|
|
380
|
+
'</span><div class="msg-main"><div class="msg-head"><span class="name">' +
|
|
381
|
+
escapeHtml(name) +
|
|
382
|
+
"</span>" +
|
|
383
|
+
(handle
|
|
384
|
+
? '<span class="handle">@' + escapeHtml(handle) + "</span>"
|
|
385
|
+
: "") +
|
|
386
|
+
'<span class="clock">' +
|
|
387
|
+
escapeHtml(relTime(msg.createdAt)) +
|
|
388
|
+
"</span></div>" +
|
|
389
|
+
body +
|
|
390
|
+
"</div></article>"
|
|
391
|
+
);
|
|
392
|
+
}
|
|
393
|
+
function renderThread(opts) {
|
|
394
|
+
const root = document.getElementById("thread");
|
|
395
|
+
const pin =
|
|
396
|
+
Boolean(opts && opts.pinBottom) ||
|
|
397
|
+
root.scrollHeight - root.scrollTop - root.clientHeight < 80;
|
|
398
|
+
const all = state.messages || [];
|
|
399
|
+
const cap = 30;
|
|
400
|
+
const truncated = all.length > cap;
|
|
401
|
+
const rows = truncated ? all.slice(-cap) : all;
|
|
402
|
+
if (!rows.length && !isBusy()) {
|
|
403
|
+
root.innerHTML =
|
|
404
|
+
'<div class="empty-thread">' + escapeHtml(t("noMessages")) + "</div>";
|
|
405
|
+
return;
|
|
406
|
+
}
|
|
407
|
+
const notice = truncated
|
|
408
|
+
? '<div class="empty-thread">' +
|
|
409
|
+
escapeHtml(t("mobile.recent", { n: cap })) +
|
|
410
|
+
"</div>"
|
|
411
|
+
: "";
|
|
412
|
+
root.innerHTML = notice + rows.map(msgHtml).join("") + turnStatusHtml();
|
|
413
|
+
if (pin) root.scrollTop = root.scrollHeight;
|
|
414
|
+
}
|
|
415
|
+
function syncComposer() {
|
|
416
|
+
const busy = isBusy();
|
|
417
|
+
document.getElementById("send").hidden = busy;
|
|
418
|
+
document.getElementById("stop").hidden = !busy;
|
|
419
|
+
}
|
|
420
|
+
function navRow(opts) {
|
|
421
|
+
const live = opts.busy
|
|
422
|
+
? '<span class="live-dot">' + escapeHtml(t("busy")) + "</span>"
|
|
423
|
+
: "";
|
|
424
|
+
return (
|
|
425
|
+
'<li><a class="' +
|
|
426
|
+
(opts.branch ? "branch" : "") +
|
|
427
|
+
'" href="' +
|
|
428
|
+
escapeHtml(opts.href) +
|
|
429
|
+
'">' +
|
|
430
|
+
opts.av +
|
|
431
|
+
'<span class="nav-copy"><strong>' +
|
|
432
|
+
escapeHtml(opts.name) +
|
|
433
|
+
live +
|
|
434
|
+
"</strong><em>" +
|
|
435
|
+
escapeHtml(opts.preview || t("noActivity")) +
|
|
436
|
+
"</em></span></a></li>"
|
|
437
|
+
);
|
|
438
|
+
}
|
|
439
|
+
function renderList() {
|
|
440
|
+
const all = state.channels || [];
|
|
441
|
+
const byId = new Map(all.map((ch) => [ch.id, ch]));
|
|
442
|
+
const kidsOf = (id) => all.filter((ch) => ch.parentId === id);
|
|
443
|
+
const roots = all.filter((ch) => !ch.parentId);
|
|
444
|
+
const seen = new Set();
|
|
445
|
+
const rows = [];
|
|
446
|
+
function walk(ch, depth) {
|
|
447
|
+
if (seen.has(ch.id)) return;
|
|
448
|
+
seen.add(ch.id);
|
|
449
|
+
const last = ch.lastMessage;
|
|
450
|
+
rows.push(
|
|
451
|
+
navRow({
|
|
452
|
+
href: "#c/" + encodeURIComponent(ch.id),
|
|
453
|
+
branch: depth > 0,
|
|
454
|
+
name: (depth ? "↳ " : "#") + ch.name,
|
|
455
|
+
preview: last
|
|
456
|
+
? previewLine(last.body) + " · " + relTime(last.createdAt)
|
|
457
|
+
: relTime(ch.updatedAt),
|
|
458
|
+
busy: roomLiveRows("channel", ch.id).length > 0,
|
|
459
|
+
av: '<span class="avatar">#</span>',
|
|
460
|
+
}),
|
|
461
|
+
);
|
|
462
|
+
kidsOf(ch.id).forEach((kid) => walk(kid, depth + 1));
|
|
463
|
+
}
|
|
464
|
+
roots.forEach((ch) => walk(ch, 0));
|
|
465
|
+
all.forEach((ch) => {
|
|
466
|
+
if (!seen.has(ch.id) && (!ch.parentId || !byId.has(ch.parentId))) {
|
|
467
|
+
walk(ch, 0);
|
|
468
|
+
}
|
|
469
|
+
});
|
|
470
|
+
document.getElementById("channels").innerHTML = rows.length
|
|
471
|
+
? rows.join("")
|
|
472
|
+
: '<li class="empty-list">' + escapeHtml(t("noActivity")) + "</li>";
|
|
473
|
+
document.getElementById("dms").innerHTML = (state.bots || [])
|
|
474
|
+
.map((bot) => {
|
|
475
|
+
const last = bot.lastMessage;
|
|
476
|
+
return navRow({
|
|
477
|
+
href: "#d/" + encodeURIComponent(bot.id),
|
|
478
|
+
name: bot.name,
|
|
479
|
+
preview: last
|
|
480
|
+
? previewLine(last.body) + " · " + relTime(last.createdAt)
|
|
481
|
+
: "@" + bot.handle,
|
|
482
|
+
busy: roomLiveRows("dm", bot.id).length > 0,
|
|
483
|
+
av:
|
|
484
|
+
'<span class="avatar" style="background:' +
|
|
485
|
+
hue(bot.handle) +
|
|
486
|
+
'">' +
|
|
487
|
+
avatarFace(bot) +
|
|
488
|
+
"</span>",
|
|
489
|
+
});
|
|
490
|
+
})
|
|
491
|
+
.join("");
|
|
492
|
+
}
|
|
493
|
+
function renderHead() {
|
|
494
|
+
const title = document.getElementById("title");
|
|
495
|
+
const sub = document.getElementById("subtitle");
|
|
496
|
+
if (state.kind === "dm") {
|
|
497
|
+
const bot = botById(state.id);
|
|
498
|
+
title.textContent = bot ? bot.name : state.id;
|
|
499
|
+
sub.textContent = bot ? "@" + bot.handle : "";
|
|
500
|
+
return;
|
|
501
|
+
}
|
|
502
|
+
const ch = currentChannel();
|
|
503
|
+
title.textContent = ch ? "#" + ch.name : state.id;
|
|
504
|
+
const n = ch && ch.members ? ch.members.length : 0;
|
|
505
|
+
sub.textContent = n ? String(n) : "";
|
|
506
|
+
}
|
|
507
|
+
function showScreens() {
|
|
508
|
+
const list = !state.kind;
|
|
509
|
+
document.getElementById("screen-list").hidden = !list;
|
|
510
|
+
document.getElementById("screen-room").hidden = list;
|
|
511
|
+
}
|
|
512
|
+
async function loadWorkspace() {
|
|
513
|
+
const data = await fetch("/workspace").then((r) => r.json());
|
|
514
|
+
state.channels = data.channels || [];
|
|
515
|
+
state.bots = data.bots || [];
|
|
516
|
+
state.serverLive = Array.isArray(data.live) ? data.live : [];
|
|
517
|
+
renderList();
|
|
518
|
+
if (state.kind) renderHead();
|
|
519
|
+
}
|
|
520
|
+
async function loadMessages(opts) {
|
|
521
|
+
if (!state.kind) return;
|
|
522
|
+
const res = await fetch(roomMessagesUrl());
|
|
523
|
+
const body = await res.json().catch(() => []);
|
|
524
|
+
if (!res.ok) throw new Error(body.error || "load failed");
|
|
525
|
+
state.messages = Array.isArray(body) ? body : [];
|
|
526
|
+
renderThread({ pinBottom: !(opts && opts.merge) });
|
|
527
|
+
}
|
|
528
|
+
async function pollLive() {
|
|
529
|
+
if (!state.kind) return;
|
|
530
|
+
const url = liveUrl();
|
|
531
|
+
if (!url) return;
|
|
532
|
+
try {
|
|
533
|
+
const res = await fetch(url);
|
|
534
|
+
const body = await res.json().catch(() => ({}));
|
|
535
|
+
if (!res.ok) return;
|
|
536
|
+
const lives = liveTurnsFrom(body);
|
|
537
|
+
const was = state.lives.map((row) => row.botId).join(",");
|
|
538
|
+
const now = lives.map((row) => row.botId).join(",");
|
|
539
|
+
state.lives = lives;
|
|
540
|
+
if (!lives.length && !state.posting) {
|
|
541
|
+
if (was) await loadMessages({ merge: true }).catch(() => {});
|
|
542
|
+
else renderThread();
|
|
543
|
+
} else {
|
|
544
|
+
if (now !== was) await loadMessages({ merge: true }).catch(() => {});
|
|
545
|
+
else renderThread();
|
|
546
|
+
}
|
|
547
|
+
syncComposer();
|
|
548
|
+
} catch {
|
|
549
|
+
/* poll is best-effort */
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
async function tick() {
|
|
553
|
+
try {
|
|
554
|
+
if (!state.kind) await loadWorkspace();
|
|
555
|
+
else {
|
|
556
|
+
await pollLive();
|
|
557
|
+
if (!isBusy()) await loadMessages({ merge: true }).catch(() => {});
|
|
558
|
+
}
|
|
559
|
+
} finally {
|
|
560
|
+
schedule();
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
function schedule() {
|
|
564
|
+
clearTimeout(state.pollTimer);
|
|
565
|
+
const ms = !state.kind ? 2500 : isBusy() ? 900 : 2200;
|
|
566
|
+
state.pollTimer = setTimeout(tick, ms);
|
|
567
|
+
}
|
|
568
|
+
async function openFromHash() {
|
|
569
|
+
parseHash();
|
|
570
|
+
hideMention();
|
|
571
|
+
showScreens();
|
|
572
|
+
await loadWorkspace();
|
|
573
|
+
if (state.kind) {
|
|
574
|
+
renderHead();
|
|
575
|
+
await loadMessages().catch((err) => toast(err.message || String(err)));
|
|
576
|
+
await pollLive();
|
|
577
|
+
syncComposer();
|
|
578
|
+
const thread = document.getElementById("thread");
|
|
579
|
+
thread.scrollTop = thread.scrollHeight;
|
|
580
|
+
}
|
|
581
|
+
schedule();
|
|
582
|
+
}
|
|
583
|
+
function mentionAtCursor(el) {
|
|
584
|
+
const start = el.selectionStart || 0;
|
|
585
|
+
const before = el.value.slice(0, start);
|
|
586
|
+
const m = before.match(/(^|\s)@([A-Za-z0-9_-]*)$/);
|
|
587
|
+
if (!m) return null;
|
|
588
|
+
return { start: start - m[2].length - 1, query: m[2].toLowerCase() };
|
|
589
|
+
}
|
|
590
|
+
function mentionChoices(query) {
|
|
591
|
+
const needle = String(query || "").toLowerCase();
|
|
592
|
+
return (state.bots || []).filter((bot) => {
|
|
593
|
+
if (!needle) return true;
|
|
594
|
+
return (
|
|
595
|
+
bot.handle.toLowerCase().indexOf(needle) >= 0 ||
|
|
596
|
+
bot.name.toLowerCase().indexOf(needle) >= 0
|
|
597
|
+
);
|
|
598
|
+
});
|
|
599
|
+
}
|
|
600
|
+
function hideMention() {
|
|
601
|
+
document.getElementById("mention-pop").hidden = true;
|
|
602
|
+
}
|
|
603
|
+
function renderMention() {
|
|
604
|
+
const draft = document.getElementById("draft");
|
|
605
|
+
const at = mentionAtCursor(draft);
|
|
606
|
+
const pop = document.getElementById("mention-pop");
|
|
607
|
+
if (!at) {
|
|
608
|
+
hideMention();
|
|
609
|
+
return;
|
|
610
|
+
}
|
|
611
|
+
const rows = mentionChoices(at.query);
|
|
612
|
+
if (!rows.length) {
|
|
613
|
+
hideMention();
|
|
614
|
+
return;
|
|
615
|
+
}
|
|
616
|
+
if (state.mentionIndex >= rows.length) state.mentionIndex = 0;
|
|
617
|
+
if (state.mentionIndex < 0) state.mentionIndex = rows.length - 1;
|
|
618
|
+
document.getElementById("mention-results").innerHTML = rows
|
|
619
|
+
.map((bot, index) => {
|
|
620
|
+
const on = index === state.mentionIndex ? " on" : "";
|
|
621
|
+
return (
|
|
622
|
+
'<button type="button" class="mention-item' +
|
|
623
|
+
on +
|
|
624
|
+
'" data-mention="' +
|
|
625
|
+
index +
|
|
626
|
+
'" data-handle="' +
|
|
627
|
+
escapeHtml(bot.handle) +
|
|
628
|
+
'"><span class="mention-glyph" style="background:' +
|
|
629
|
+
hue(bot.handle) +
|
|
630
|
+
'">' +
|
|
631
|
+
avatarFace(bot) +
|
|
632
|
+
"</span><span><strong>" +
|
|
633
|
+
escapeHtml(bot.name) +
|
|
634
|
+
"</strong><em>@" +
|
|
635
|
+
escapeHtml(bot.handle) +
|
|
636
|
+
"</em></span></button>"
|
|
637
|
+
);
|
|
638
|
+
})
|
|
639
|
+
.join("");
|
|
640
|
+
pop.hidden = false;
|
|
641
|
+
}
|
|
642
|
+
function insertMention(handle) {
|
|
643
|
+
const draft = document.getElementById("draft");
|
|
644
|
+
const at = mentionAtCursor(draft);
|
|
645
|
+
if (!at) return;
|
|
646
|
+
const after = draft.value.slice(draft.selectionStart || 0);
|
|
647
|
+
const next = draft.value.slice(0, at.start) + "@" + handle + " " + after;
|
|
648
|
+
draft.value = next;
|
|
649
|
+
const pos = at.start + handle.length + 2;
|
|
650
|
+
draft.setSelectionRange(pos, pos);
|
|
651
|
+
hideMention();
|
|
652
|
+
fitDraft();
|
|
653
|
+
draft.focus();
|
|
654
|
+
}
|
|
655
|
+
function pickMention(index) {
|
|
656
|
+
const btn = document.querySelector(
|
|
657
|
+
'.mention-item[data-mention="' + index + '"]',
|
|
658
|
+
);
|
|
659
|
+
if (btn) insertMention(btn.getAttribute("data-handle"));
|
|
660
|
+
}
|
|
661
|
+
function fitDraft() {
|
|
662
|
+
const el = document.getElementById("draft");
|
|
663
|
+
el.style.height = "auto";
|
|
664
|
+
el.style.height = Math.min(el.scrollHeight, 144) + "px";
|
|
665
|
+
}
|
|
666
|
+
async function stopTurn(botId) {
|
|
667
|
+
try {
|
|
668
|
+
await fetch(abortUrl(), {
|
|
669
|
+
method: "POST",
|
|
670
|
+
headers: { "content-type": "application/json" },
|
|
671
|
+
body: JSON.stringify(botId ? { botId: botId } : {}),
|
|
672
|
+
});
|
|
673
|
+
} catch {
|
|
674
|
+
/* ignore */
|
|
675
|
+
}
|
|
676
|
+
state.lives = botId
|
|
677
|
+
? state.lives.filter((row) => row.botId !== botId)
|
|
678
|
+
: [];
|
|
679
|
+
state.posting = false;
|
|
680
|
+
syncComposer();
|
|
681
|
+
renderThread();
|
|
682
|
+
await loadMessages().catch(() => {});
|
|
683
|
+
}
|
|
684
|
+
async function sendDraft() {
|
|
685
|
+
const draft = document.getElementById("draft");
|
|
686
|
+
const raw = draft.value.trim();
|
|
687
|
+
if (!raw || !state.kind || isBusy()) return;
|
|
688
|
+
const botIds = botsFromSend(raw);
|
|
689
|
+
const payload = { body: raw };
|
|
690
|
+
if (botIds.length) payload.mentions = botIds;
|
|
691
|
+
if (botIds.length === 1) payload.assigneeId = botIds[0];
|
|
692
|
+
draft.value = "";
|
|
693
|
+
fitDraft();
|
|
694
|
+
hideMention();
|
|
695
|
+
state.messages = state.messages.concat([
|
|
696
|
+
{
|
|
697
|
+
id: "pending-you-" + Date.now(),
|
|
698
|
+
author: "you",
|
|
699
|
+
body: raw,
|
|
700
|
+
createdAt: new Date().toISOString(),
|
|
701
|
+
},
|
|
702
|
+
]);
|
|
703
|
+
state.posting = true;
|
|
704
|
+
syncComposer();
|
|
705
|
+
renderThread();
|
|
706
|
+
try {
|
|
707
|
+
const res = await fetch(roomMessagesUrl(), {
|
|
708
|
+
method: "POST",
|
|
709
|
+
headers: { "content-type": "application/json" },
|
|
710
|
+
body: JSON.stringify(payload),
|
|
711
|
+
});
|
|
712
|
+
const body = await res.json().catch(() => ({}));
|
|
713
|
+
if (!res.ok) throw new Error(body.error || "send failed");
|
|
714
|
+
await loadMessages();
|
|
715
|
+
await pollLive();
|
|
716
|
+
} catch (err) {
|
|
717
|
+
if (err && err.name === "AbortError") return;
|
|
718
|
+
toast(err.message || String(err));
|
|
719
|
+
draft.value = raw;
|
|
720
|
+
fitDraft();
|
|
721
|
+
await loadMessages().catch(() => {});
|
|
722
|
+
} finally {
|
|
723
|
+
state.posting = false;
|
|
724
|
+
syncComposer();
|
|
725
|
+
schedule();
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
document.getElementById("back").addEventListener("click", () => {
|
|
730
|
+
showList();
|
|
731
|
+
});
|
|
732
|
+
document.getElementById("composer").addEventListener("submit", (event) => {
|
|
733
|
+
event.preventDefault();
|
|
734
|
+
sendDraft();
|
|
735
|
+
});
|
|
736
|
+
document.getElementById("stop").addEventListener("click", () => {
|
|
737
|
+
stopTurn();
|
|
738
|
+
});
|
|
739
|
+
document.getElementById("thread").addEventListener("click", (event) => {
|
|
740
|
+
const stop = event.target.closest("[data-live-stop]");
|
|
741
|
+
if (!stop) return;
|
|
742
|
+
event.preventDefault();
|
|
743
|
+
stopTurn(stop.getAttribute("data-live-stop"));
|
|
744
|
+
});
|
|
745
|
+
document.getElementById("draft").addEventListener("input", () => {
|
|
746
|
+
fitDraft();
|
|
747
|
+
renderMention();
|
|
748
|
+
});
|
|
749
|
+
document.getElementById("draft").addEventListener("keydown", (event) => {
|
|
750
|
+
const pop = document.getElementById("mention-pop");
|
|
751
|
+
if (pop.hidden) return;
|
|
752
|
+
if (event.key === "ArrowDown") {
|
|
753
|
+
event.preventDefault();
|
|
754
|
+
state.mentionIndex += 1;
|
|
755
|
+
renderMention();
|
|
756
|
+
} else if (event.key === "ArrowUp") {
|
|
757
|
+
event.preventDefault();
|
|
758
|
+
state.mentionIndex -= 1;
|
|
759
|
+
renderMention();
|
|
760
|
+
} else if (event.key === "Enter" || event.key === "Tab") {
|
|
761
|
+
event.preventDefault();
|
|
762
|
+
pickMention(state.mentionIndex);
|
|
763
|
+
} else if (event.key === "Escape") {
|
|
764
|
+
hideMention();
|
|
765
|
+
}
|
|
766
|
+
});
|
|
767
|
+
document.getElementById("mention-results").addEventListener("click", (event) => {
|
|
768
|
+
const btn = event.target.closest("[data-handle]");
|
|
769
|
+
if (!btn) return;
|
|
770
|
+
insertMention(btn.getAttribute("data-handle"));
|
|
771
|
+
});
|
|
772
|
+
window.addEventListener("hashchange", () => {
|
|
773
|
+
openFromHash().catch((err) => toast(err.message || String(err)));
|
|
774
|
+
});
|
|
775
|
+
window.addEventListener("resize", fitShell);
|
|
776
|
+
if (window.visualViewport) {
|
|
777
|
+
window.visualViewport.addEventListener("resize", fitShell);
|
|
778
|
+
}
|
|
779
|
+
fitShell();
|
|
780
|
+
openFromHash().catch((err) => toast(err.message || String(err)));
|
|
781
|
+
})();
|
|
782
|
+
</script>
|
|
783
|
+
</body>
|
|
784
|
+
</html>
|