@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.
@@ -8,7 +8,7 @@
8
8
  <link rel="icon" href="/favicon-32.png" type="image/png" sizes="32x32" />
9
9
  <link rel="icon" href="/favicon-16.png" type="image/png" sizes="16x16" />
10
10
  <title>Guild — 大廳</title>
11
- <link rel="stylesheet" href="/chat.css?v=press-enamel" />
11
+ <link rel="stylesheet" href="/chat.css?v=quest-nest" />
12
12
  <script src="/i18n.js"></script>
13
13
  <script src="/md.js"></script>
14
14
  </head>
@@ -341,6 +341,15 @@
341
341
  const glyph = Array.from(String(name).replace(/\s+/g, ""))[0] || "?";
342
342
  return glyph;
343
343
  }
344
+ function portraitUrl(bot) {
345
+ const url = bot && typeof bot.portrait === "string" ? bot.portrait.trim() : "";
346
+ return url.indexOf("/generated/") === 0 ? url : "";
347
+ }
348
+ function avatarFace(bot) {
349
+ const src = portraitUrl(bot);
350
+ if (src) return '<img alt="" src="' + escapeHtml(src) + '">';
351
+ return escapeHtml(initials((bot && (bot.name || bot.handle)) || "?"));
352
+ }
344
353
  function escapeHtml(value) {
345
354
  return String(value)
346
355
  .replace(/&/g, "&amp;")
@@ -1128,6 +1137,12 @@
1128
1137
  hour12: true,
1129
1138
  });
1130
1139
  }
1140
+ function formatMsgWhen(iso) {
1141
+ const day = formatMsgDay(iso);
1142
+ const clock = formatMsgClock(iso);
1143
+ if (day && clock) return day + " " + clock;
1144
+ return clock || day || "";
1145
+ }
1131
1146
  function formatMsgDay(iso) {
1132
1147
  if (!iso) return "";
1133
1148
  const when = new Date(iso);
@@ -1266,11 +1281,15 @@
1266
1281
  const foot = opts.party
1267
1282
  ? opts.party
1268
1283
  : "<em>" + escapeHtml(opts.preview || "") + "</em>";
1269
- return (
1270
- '<li><a class="nav-row' +
1284
+ const cls =
1285
+ "nav-row" +
1271
1286
  (opts.on ? " on" : "") +
1272
1287
  (opts.unread ? " unread" : "") +
1273
1288
  (opts.notice ? " notice" : "") +
1289
+ (opts.branch ? " nav-branch" : "");
1290
+ const inner =
1291
+ '<a class="' +
1292
+ cls +
1274
1293
  '" href="' +
1275
1294
  opts.href +
1276
1295
  '">' +
@@ -1282,8 +1301,9 @@
1282
1301
  right +
1283
1302
  "</span>" +
1284
1303
  foot +
1285
- "</span></a></li>"
1286
- );
1304
+ "</span></a>";
1305
+ if (opts.bare) return inner;
1306
+ return "<li>" + inner + "</li>";
1287
1307
  }
1288
1308
  function partyStackHtml(members) {
1289
1309
  const bots = Array.isArray(members) ? members : [];
@@ -1293,7 +1313,7 @@
1293
1313
  '<span class="avatar" style="background:' +
1294
1314
  hue(bot.handle) +
1295
1315
  '">' +
1296
- escapeHtml(initials(bot.name)) +
1316
+ avatarFace(bot) +
1297
1317
  "</span>",
1298
1318
  )
1299
1319
  .join("");
@@ -1343,31 +1363,93 @@
1343
1363
  }
1344
1364
  function renderNav() {
1345
1365
  const q = document.getElementById("filter").value.toLowerCase();
1346
- const channels = state.channels
1347
- .filter((ch) => !q || ch.name.toLowerCase().includes(q))
1366
+ const all = state.channels || [];
1367
+ const byId = new Map(all.map((ch) => [ch.id, ch]));
1368
+ const kidsOf = (id) =>
1369
+ all.filter((ch) => ch.parentId === id).slice().sort(byUpdatedAtDesc);
1370
+ const visible = new Set();
1371
+ for (const ch of all) {
1372
+ const hit = !q || String(ch.name || "").toLowerCase().includes(q);
1373
+ if (!hit) continue;
1374
+ let walk = ch;
1375
+ while (walk) {
1376
+ visible.add(walk.id);
1377
+ walk = walk.parentId ? byId.get(walk.parentId) : null;
1378
+ }
1379
+ }
1380
+ function channelBusy(ch) {
1381
+ return Boolean(
1382
+ (state.busyRoom &&
1383
+ state.busyRoom.kind === "channel" &&
1384
+ state.busyRoom.id === ch.id) ||
1385
+ (state.serverLive || []).some(
1386
+ (row) => row.kind === "channel" && row.id === ch.id,
1387
+ ),
1388
+ );
1389
+ }
1390
+ function channelUpdated(ch) {
1391
+ let latest = ch.updatedAt ? Date.parse(ch.updatedAt) : 0;
1392
+ for (const kid of kidsOf(ch.id)) {
1393
+ const child = channelUpdated(kid);
1394
+ const ts = child ? Date.parse(child) : 0;
1395
+ if (ts > latest) latest = ts;
1396
+ }
1397
+ return Number.isFinite(latest) && latest > 0
1398
+ ? new Date(latest).toISOString()
1399
+ : ch.updatedAt;
1400
+ }
1401
+ function branchTree(parentId) {
1402
+ return kidsOf(parentId)
1403
+ .filter((ch) => visible.has(ch.id))
1404
+ .map((ch) => {
1405
+ const nested = branchTree(ch.id);
1406
+ const row = navRowHtml({
1407
+ bare: true,
1408
+ branch: true,
1409
+ href: "#c/" + encodeURIComponent(ch.id),
1410
+ on: state.kind === "channel" && state.id === ch.id,
1411
+ notice: true,
1412
+ name: "↳ " + ch.name,
1413
+ updatedAt: ch.updatedAt,
1414
+ party: partyStackHtml(ch.members),
1415
+ busy: channelBusy(ch),
1416
+ unread: state.unread.has(roomKey("channel", ch.id)),
1417
+ });
1418
+ return nested
1419
+ ? "<li>" + row + '<ul class="nav-branches">' + nested + "</ul></li>"
1420
+ : "<li>" + row + "</li>";
1421
+ })
1422
+ .join("");
1423
+ }
1424
+ const roots = all
1425
+ .filter((ch) => !ch.parentId && visible.has(ch.id))
1348
1426
  .slice()
1349
- .sort(byUpdatedAtDesc);
1350
- document.getElementById("channels").innerHTML = channels
1427
+ .sort((a, b) =>
1428
+ byUpdatedAtDesc(
1429
+ { updatedAt: channelUpdated(a) },
1430
+ { updatedAt: channelUpdated(b) },
1431
+ ),
1432
+ );
1433
+ document.getElementById("channels").innerHTML = roots
1351
1434
  .map((ch) => {
1352
- const busy = Boolean(
1353
- (state.busyRoom &&
1354
- state.busyRoom.kind === "channel" &&
1355
- state.busyRoom.id === ch.id) ||
1356
- (state.serverLive || []).some(
1357
- (row) => row.kind === "channel" && row.id === ch.id,
1358
- ),
1359
- );
1360
- const unread = state.unread.has(roomKey("channel", ch.id));
1361
- return navRowHtml({
1435
+ const nested = branchTree(ch.id);
1436
+ const row = navRowHtml({
1437
+ bare: true,
1362
1438
  href: "#c/" + encodeURIComponent(ch.id),
1363
1439
  on: state.kind === "channel" && state.id === ch.id,
1364
1440
  notice: true,
1365
1441
  name: ch.name,
1366
- updatedAt: ch.updatedAt,
1442
+ updatedAt: channelUpdated(ch),
1367
1443
  party: partyStackHtml(ch.members),
1368
- busy: busy,
1369
- unread: unread,
1444
+ busy: channelBusy(ch),
1445
+ unread: state.unread.has(roomKey("channel", ch.id)),
1370
1446
  });
1447
+ return (
1448
+ '<li class="nav-quest">' +
1449
+ row +
1450
+ (nested ? '<ul class="nav-branches">' + nested + "</ul>" : "") +
1451
+ "</li>"
1452
+ );
1371
1453
  })
1372
1454
  .join("");
1373
1455
  const bots = state.bots
@@ -1391,7 +1473,7 @@
1391
1473
  '<span class="avatar" style="background:' +
1392
1474
  hue(bot.handle) +
1393
1475
  '">' +
1394
- escapeHtml(initials(bot.name)) +
1476
+ avatarFace(bot) +
1395
1477
  "</span>",
1396
1478
  name: bot.name,
1397
1479
  updatedAt: bot.updatedAt,
@@ -1426,7 +1508,7 @@
1426
1508
  '" style="background:' +
1427
1509
  hue(bot.handle) +
1428
1510
  '">' +
1429
- escapeHtml(initials(bot.name)) +
1511
+ avatarFace(bot) +
1430
1512
  '</button><span class="who"><strong>' +
1431
1513
  escapeHtml(bot.name) +
1432
1514
  "</strong><em>@" +
@@ -1445,9 +1527,9 @@
1445
1527
  document.getElementById("subtitle").textContent = "";
1446
1528
  wrap.hidden = true;
1447
1529
  document.getElementById("members-pop").hidden = true;
1448
- av.textContent = bot ? initials(bot.name) : "?";
1449
1530
  av.style.background = bot ? hue(bot.handle) : "#333";
1450
1531
  av.classList.remove("hash-av");
1532
+ av.innerHTML = bot ? avatarFace(bot) : "?";
1451
1533
  if (bot) {
1452
1534
  av.setAttribute("href", "#");
1453
1535
  av.setAttribute("data-bot-id", bot.id);
@@ -1480,7 +1562,7 @@
1480
1562
  '<span class="avatar" style="background:' +
1481
1563
  hue(bot.handle) +
1482
1564
  '">' +
1483
- escapeHtml(initials(bot.name)) +
1565
+ avatarFace(bot) +
1484
1566
  "</span>",
1485
1567
  )
1486
1568
  .join("");
@@ -1624,7 +1706,7 @@
1624
1706
  '" style="background:' +
1625
1707
  hue(bot.handle) +
1626
1708
  '">' +
1627
- escapeHtml(initials(bot.name)) +
1709
+ avatarFace(bot) +
1628
1710
  "</button>",
1629
1711
  head:
1630
1712
  '<div class="msg-head"><button type="button" class="name"' +
@@ -1848,23 +1930,28 @@
1848
1930
  rail.innerHTML = you
1849
1931
  .map((el) => {
1850
1932
  const id = el.getAttribute("data-id") || "";
1933
+ const when = formatMsgWhen(el.getAttribute("data-at") || "");
1851
1934
  const preview = youPromptPreview(el);
1852
1935
  const top = Math.max(
1853
1936
  6,
1854
1937
  Math.min(track - 6, ((el.offsetTop + el.offsetHeight / 2) / total) * track),
1855
1938
  );
1939
+ const label = [when, preview].filter(Boolean).join(" · ");
1856
1940
  return (
1857
1941
  '<button type="button" class="prompt-tick" data-prompt-jump="' +
1858
1942
  escapeHtml(id) +
1859
1943
  '" style="top:' +
1860
1944
  top +
1861
- 'px"' +
1945
+ 'px" aria-label="' +
1946
+ escapeHtml(t("promptJump") + (label ? " · " + label : "")) +
1947
+ '"><span class="prompt-tip">' +
1948
+ (when
1949
+ ? '<time class="prompt-tip-when">' + escapeHtml(when) + "</time>"
1950
+ : "") +
1862
1951
  (preview
1863
- ? ' data-preview="' + escapeHtml(preview) + '"'
1952
+ ? '<span class="prompt-tip-text">' + escapeHtml(preview) + "</span>"
1864
1953
  : "") +
1865
- ' aria-label="' +
1866
- escapeHtml(t("promptJump") + (preview ? " · " + preview : "")) +
1867
- '"></button>'
1954
+ "</span></button>"
1868
1955
  );
1869
1956
  })
1870
1957
  .join("");
@@ -1988,6 +2075,15 @@
1988
2075
  '">' +
1989
2076
  t("retry") +
1990
2077
  "</button>" +
2078
+ (state.kind === "channel" &&
2079
+ !msg.queued &&
2080
+ String(msg.id || "").indexOf("pending-") !== 0
2081
+ ? '<button type="button" data-branch="' +
2082
+ escapeHtml(msg.id) +
2083
+ '">' +
2084
+ t("branch") +
2085
+ "</button>"
2086
+ : "") +
1991
2087
  (msg.queued
1992
2088
  ? ""
1993
2089
  : '<button type="button" class="danger" data-msg-del="' +
@@ -2003,6 +2099,8 @@
2003
2099
  (msg.queued ? " queued" : msg.steer ? " steer" : "") +
2004
2100
  '" data-id="' +
2005
2101
  escapeHtml(msg.id) +
2102
+ '" data-at="' +
2103
+ escapeHtml(stamp) +
2006
2104
  '">' +
2007
2105
  clock +
2008
2106
  body +
@@ -2021,7 +2119,7 @@
2021
2119
  '" style="background:' +
2022
2120
  hue(handle || name) +
2023
2121
  '">' +
2024
- escapeHtml(initials(name)) +
2122
+ (bot ? avatarFace(bot) : escapeHtml(initials(name))) +
2025
2123
  "</button>";
2026
2124
  const head =
2027
2125
  '<div class="msg-head"><button type="button" class="name"' +
@@ -2122,6 +2220,30 @@
2122
2220
  await loadWorkspace();
2123
2221
  if (state.kind === "channel") await loadMessages();
2124
2222
  }
2223
+ async function closeBranch(id, ch) {
2224
+ if (!id) return;
2225
+ if (!confirm(t("branch.closeConfirm", { name: (ch && ch.name) || id }))) {
2226
+ return;
2227
+ }
2228
+ const merge = confirm(t("branch.closeMerge"));
2229
+ const res = await fetch(
2230
+ "/channels/" + encodeURIComponent(id) + "/close",
2231
+ {
2232
+ method: "POST",
2233
+ headers: { "content-type": "application/json" },
2234
+ body: JSON.stringify({ merge: merge }),
2235
+ },
2236
+ );
2237
+ const body = await res.json().catch(() => ({}));
2238
+ if (!res.ok) throw new Error(body.error || t("branch.closeFailed"));
2239
+ const parentId = (ch && ch.parentId) || body.parentId || "channel-general";
2240
+ if (state.kind === "channel" && state.id === id) {
2241
+ state.id = parentId;
2242
+ setHash();
2243
+ }
2244
+ await loadWorkspace();
2245
+ if (state.kind === "channel") await loadMessages();
2246
+ }
2125
2247
 
2126
2248
 
2127
2249
  function roomMessagesUrl(kind, id, suffix) {
@@ -2471,6 +2593,29 @@
2471
2593
  renderThread();
2472
2594
  renderNav();
2473
2595
  }
2596
+ async function branchFromMessage(id) {
2597
+ if (state.kind !== "channel" || !state.id) return;
2598
+ const msg = state.messages.find((item) => item.id === id);
2599
+ if (!msg) return;
2600
+ const def = String(msg.body || "")
2601
+ .replace(/\s+/g, " ")
2602
+ .trim()
2603
+ .slice(0, 28);
2604
+ const typed = window.prompt(t("branch.name"), def || "");
2605
+ if (typed === null) return;
2606
+ const name = typed.trim() || def;
2607
+ const res = await fetch(
2608
+ "/channels/" + encodeURIComponent(state.id) + "/branches",
2609
+ {
2610
+ method: "POST",
2611
+ headers: { "content-type": "application/json" },
2612
+ body: JSON.stringify({ messageId: id, name: name || undefined }),
2613
+ },
2614
+ );
2615
+ const body = await res.json().catch(() => ({}));
2616
+ if (!res.ok) throw new Error(body.error || "branch failed");
2617
+ location.hash = "#c/" + encodeURIComponent(body.id);
2618
+ }
2474
2619
  async function retryMessage(id, body) {
2475
2620
  if (canStopHere()) {
2476
2621
  stopTurn();
@@ -2499,6 +2644,7 @@
2499
2644
  const payload = {};
2500
2645
  if (typeof body === "string") payload.body = body;
2501
2646
  if (botIds.length === 1) payload.assigneeId = botIds[0];
2647
+ if (botIds.length) payload.mentions = botIds;
2502
2648
  await postChat(
2503
2649
  messagesUrl(id) + "/retry",
2504
2650
  payload,
@@ -2688,6 +2834,12 @@
2688
2834
  await retryMessage(retry.getAttribute("data-retry"));
2689
2835
  return;
2690
2836
  }
2837
+ const branchBtn = event.target.closest("[data-branch]");
2838
+ if (branchBtn) {
2839
+ event.preventDefault();
2840
+ await branchFromMessage(branchBtn.getAttribute("data-branch"));
2841
+ return;
2842
+ }
2691
2843
  const del = event.target.closest("[data-msg-del]");
2692
2844
  if (del) {
2693
2845
  event.preventDefault();
@@ -2807,7 +2959,11 @@
2807
2959
  const nameEl = document.getElementById("channel-md-name");
2808
2960
  nameEl.value = ch ? ch.name : "";
2809
2961
  nameEl.disabled = locked;
2810
- document.getElementById("channel-md-del").hidden = locked;
2962
+ const del = document.getElementById("channel-md-del");
2963
+ const branched = Boolean(ch && ch.parentId);
2964
+ del.hidden = locked;
2965
+ del.setAttribute("data-i18n", branched ? "branch.close" : "channel.delete");
2966
+ del.textContent = t(branched ? "branch.close" : "channel.delete");
2811
2967
  channelMdDialog.showModal();
2812
2968
  }
2813
2969
  const botMemoryDialog = document.getElementById("bot-memory");
@@ -2997,7 +3153,8 @@
2997
3153
  document.getElementById("channel-md-del").addEventListener("click", async () => {
2998
3154
  const ch = currentChannel();
2999
3155
  try {
3000
- await deleteChannel(state.id, ch && ch.name);
3156
+ if (ch && ch.parentId) await closeBranch(state.id, ch);
3157
+ else await deleteChannel(state.id, ch && ch.name);
3001
3158
  channelMdDialog.close();
3002
3159
  } catch (err) {
3003
3160
  alert(err.message);
@@ -4385,11 +4542,13 @@
4385
4542
  if (!pending) return;
4386
4543
  const fromCands = (cands || []).map((bot) => bot.id).filter(Boolean);
4387
4544
  const fromReply = replyAuthorId(pending.replyTo);
4388
- const botIds = fromCands.length
4389
- ? fromCands
4545
+ const botIds = summoned.length
4546
+ ? summoned
4390
4547
  : fromReply
4391
4548
  ? [fromReply]
4392
- : botsFromSend(pending.raw) || [];
4549
+ : fromCands.length
4550
+ ? fromCands
4551
+ : botsFromSend(pending.raw) || [];
4393
4552
  const assigneeId = botIds.length === 1 ? botIds[0] : "";
4394
4553
  const ids = botIds;
4395
4554
  const split = splitSendTargets(ids);
@@ -4432,6 +4591,7 @@
4432
4591
  if (replyTo) payload.replyTo = replyTo;
4433
4592
  if (attachments.length) payload.attachments = attachments;
4434
4593
  if (assigneeId) payload.assigneeId = assigneeId;
4594
+ if (botIds && botIds.length) payload.mentions = botIds;
4435
4595
  if (here) {
4436
4596
  state.messages = state.messages.concat([
4437
4597
  {
@@ -4481,7 +4641,7 @@
4481
4641
  '"><span class="avatar sm" style="background:' +
4482
4642
  hue(bot.handle) +
4483
4643
  '">' +
4484
- escapeHtml(initials(bot.name)) +
4644
+ avatarFace(bot) +
4485
4645
  '</span><span class="assign-copy"><strong>' +
4486
4646
  escapeHtml(bot.name) +
4487
4647
  "</strong><span>@" +
@@ -4547,8 +4707,15 @@
4547
4707
  openAssign(cands, { raw: raw, packed: packed, replyTo: replyTo });
4548
4708
  return;
4549
4709
  }
4550
- const botIds = botsFromSend(raw);
4551
- const assigneeId = cands.length === 1 ? cands[0].id : "";
4710
+ const fromReply = replyAuthorId(replyTo);
4711
+ const botIds = summoned.length
4712
+ ? summoned
4713
+ : fromReply
4714
+ ? [fromReply]
4715
+ : botsFromSend(raw);
4716
+ const assigneeId = summoned.length === 1
4717
+ ? summoned[0]
4718
+ : fromReply || (cands.length === 1 ? cands[0].id : "");
4552
4719
  await flushSend(raw, packed, replyTo, botIds, assigneeId);
4553
4720
  });
4554
4721
  function asLibList(value) {
@@ -5537,7 +5704,9 @@
5537
5704
  ? '<span class="traj-av" style="background:' +
5538
5705
  hue(bot.handle) +
5539
5706
  '">' +
5540
- escapeHtml(glyph) +
5707
+ (portraitUrl(bot)
5708
+ ? '<img alt="" src="' + escapeHtml(portraitUrl(bot)) + '">'
5709
+ : escapeHtml(glyph)) +
5541
5710
  "</span>"
5542
5711
  : '<span class="traj-av you">' + escapeHtml(glyph) + "</span>";
5543
5712
  return (
@@ -1,6 +1,10 @@
1
1
  /** Guild UI strings. Default follows the browser; packs are zh-Hant and en. */
2
2
  var I18N_ROWS = [
3
3
  ["title.chat", "Guild — 大廳", "Guild — Hall"],
4
+ ["title.mobile", "Guild — 外出", "Guild — Away"],
5
+ ["mobile.hint", "外出時用 Tailscale 連到家裡這台 guildd(埠 7420)。", "Away from the desk: Tailscale onto this machine's guildd (port 7420)."],
6
+ ["mobile.back", "返回委託", "Back to channels"],
7
+ ["mobile.recent", "只顯示最近 {n} 則。完整紀錄在大廳。", "Showing the latest {n}. Full log is in the hall."],
4
8
  ["title.library", "Guild — 技能庫", "Guild — Skills"],
5
9
  ["title.subagents", "Guild — 子代理", "Guild — Subagents"],
6
10
  ["title.subagentsAdd", "Guild — 新增子代理", "Guild — Add subagent"],
@@ -24,7 +28,7 @@ var I18N_ROWS = [
24
28
  ["search.subagents", "搜尋子代理", "Search subagents"],
25
29
  ["search.mcp", "搜尋 MCP", "Search MCP"],
26
30
  ["search.models", "搜尋全部模型", "Search all models"],
27
- ["search.traj", "搜尋 system / tool / spawn / 結果", "Search system / tool / spawn / result"],
31
+ ["search.traj", "搜尋 system / tool / spawn / 子代理 / 結果", "Search system / tool / spawn / subagent / result"],
28
32
  ["channels", "委託", "Channels"],
29
33
  ["dms", "密談", "Whispers"],
30
34
  ["newChannel", "新委託", "New channel"],
@@ -63,6 +67,12 @@ var I18N_ROWS = [
63
67
  ["edit", "編輯", "Edit"],
64
68
  ["reply", "回覆", "Reply"],
65
69
  ["retry", "重問", "Retry"],
70
+ ["branch", "分支", "Branch"],
71
+ ["branch.name", "子委託名稱", "Name this side quest"],
72
+ ["branch.close", "結案", "Close quest"],
73
+ ["branch.closeConfirm", "結案 #{name}?子委託會從側欄拿掉。", "Close #{name}? It leaves the sidebar."],
74
+ ["branch.closeMerge", "要把紀錄整理後寫回主委託 MEMORY.md 嗎?", "Write the notes back into the parent MEMORY.md?"],
75
+ ["branch.closeFailed", "結案失敗", "Couldn't close"],
66
76
  ["stats", "統計", "Stats"],
67
77
  ["stats.title", "回應統計", "Response statistics"],
68
78
  ["stats.tokens", "Token", "Tokens"],
@@ -71,7 +81,7 @@ var I18N_ROWS = [
71
81
  ["stats.rounds", "模型回合", "Agent rounds"],
72
82
  ["stats.cost", "費用", "Cost"],
73
83
  ["stats.estimated", "估算", "Estimated"],
74
- ["stats.inOut", "輸入 {in} · 輸出 {out}", "in {in} · out {out}"],
84
+ ["stats.inOut", "輸入 {in} · 快取命中 {cache} · 輸出 {out}", "in {in} · cache {cache} · out {out}"],
75
85
  ["askAgain", "重新詢問", "Ask again"],
76
86
  ["send", "送出", "Send"],
77
87
  ["jumpBottom", "滾到最新", "Jump to latest"],
@@ -95,6 +105,11 @@ var I18N_ROWS = [
95
105
  ["busy", "忙碌中", "Busy"],
96
106
  ["unread", "未讀", "Unread"],
97
107
  ["noMessages", "還沒有紀錄。寫一句話開始。據點裡記得 @handle。", "No log yet. Say something. In a hall, @handle an adventurer."],
108
+ ["setup.kicker", "第一步", "Step 1"],
109
+ ["setup.needModel", "還沒接模型。Guild 不能想、也不能跑工具。", "No model yet. Guild cannot think or run tools."],
110
+ ["setup.needModelBody", "先連接訂閱或填 API key,套用主模型,再回來點名。", "Connect a subscription or paste an API key, apply a default, then come back to @mention someone."],
111
+ ["setup.subs", "連接訂閱", "Connect a subscription"],
112
+ ["setup.keys", "填 API key", "Add an API key"],
98
113
  ["dm", "密談", "Whisper"],
99
114
  ["justNow", "剛剛", "Just now"],
100
115
  ["minutesAgo", "{n} 分鐘前", "{n} min ago"],
@@ -140,15 +155,16 @@ var I18N_ROWS = [
140
155
  ["remove", "移除", "Remove"],
141
156
  ["cancelReply", "取消回覆", "Cancel reply"],
142
157
  ["replying", "回覆…", "Replying…"],
143
- ["trace", "Trajectory", "Trajectory"],
158
+ ["trace", "軌跡", "Trajectory"],
144
159
  ["trace.steps", "步驟", "Steps"],
145
160
  ["trace.long", "長輸出", "Long output"],
146
161
  ["trace.pick", "選一筆事件。", "Select an event."],
147
162
  ["trace.empty", "沒有事件。送一則訊息後會開始記錄。", "No events yet. Send a message to start the log."],
148
- ["trace.log", "append-only log", "append-only log"],
163
+ ["trace.log", "只追加紀錄", "append-only log"],
149
164
  ["trace.derived", "由訊息還原", "rebuilt from messages"],
150
165
  ["trace.live", "進行中", "in progress"],
151
166
  ["trace.loading", "讀取中…", "Loading…"],
167
+ ["trace.filter.all", "全部", "All"],
152
168
  ["html.preview", "預覽", "Preview"],
153
169
  ["html.code", "原始碼", "Code"],
154
170
  ["html.expand", "放大預覽", "Expand preview"],
@@ -266,7 +282,7 @@ var I18N_ROWS = [
266
282
  ["library.local", "本機", "Host"],
267
283
  ["settings.eyebrow", "Hermes-style", "Hermes-style"],
268
284
  ["settings.title", "模型", "Models"],
269
- ["settings.lede", "訂閱帳號走 OAuthAPI key 可同時掛多家供應商,寫入 ~/.guild/models.json。", "Log in with a subscription. API keys can cover several providers and are written to ~/.guild/models.json."],
285
+ ["settings.lede", "第一次:先連訂閱或填 API key,再套用主模型。沒有模型就不能用。訂閱走 OAuthAPI key 寫入 ~/.guild/models.json。", "First time: connect a subscription or paste an API key, then apply a default. Guild does not run without a model. Subscriptions use OAuth. API keys go in ~/.guild/models.json."],
270
286
  ["settings.accounts", "連接帳號", "Accounts"],
271
287
  ["settings.accountsHint", "用訂閱登入,不必複製金鑰。瀏覽器完成授權後會自動刷新 token。", "Subscription login, no key paste. Tokens refresh after the browser handshake."],
272
288
  ["settings.connect", "連接", "Connect"],
@@ -275,6 +291,18 @@ var I18N_ROWS = [
275
291
  ["settings.pending", "登入中", "Signing in"],
276
292
  ["settings.keys", "API 金鑰供應商", "API key providers"],
277
293
  ["settings.keysHint", "點名稱切換供應商。要加新的,點「+ 新增」。金鑰可用 $ENV 或直接貼上。", "Tap a name to switch. Tap “+ Add” for a new provider. Keys can be $ENV or pasted."],
294
+ ["settings.keyless", "無需金鑰", "No key needed"],
295
+ ["settings.opencodeFreeHint", "不用登入、不用註冊、不用環境變數。走 OpenCode Zen 匿名免費層。", "No login, no account, no env var. OpenCode Zen's anonymous free tier."],
296
+ ["settings.saveKeylessHint", "這家不用金鑰。套用主模型即可。", "No key to save. Apply it as the default."],
297
+ ["settings.sync", "Sync", "Sync"],
298
+ ["settings.syncing", "同步並測試中…", "Syncing and probing…"],
299
+ ["settings.synced", "已同步 {n} 個可用模型", "Synced {n} working models"],
300
+ ["settings.syncedSkip", "已同步 {ok} 個可用 · 略過 {names}", "Synced {ok} working · skipped {names}"],
301
+ ["settings.syncedNone", "目錄有了,但沒有模型測得通", "Catalog ok, but no model answered"],
302
+ ["settings.probeOk", "可用", "Works"],
303
+ ["settings.probeBusy", "限流", "Busy"],
304
+ ["settings.probeFail", "無法連線", "Down"],
305
+ ["settings.syncFailed", "同步失敗", "Couldn't sync"],
278
306
  ["settings.noProviders", "還沒有供應商。點「+ 新增」選一家模板。", "No providers yet. Tap “+ Add” and pick a template."],
279
307
  ["settings.addProvider", "新增供應商", "Add provider"],
280
308
  ["settings.addChip", "+ 新增", "+ Add"],
@@ -286,7 +314,7 @@ var I18N_ROWS = [
286
314
  ["settings.provider", "供應商", "Provider"],
287
315
  ["settings.apply", "套用", "Apply"],
288
316
  ["settings.aux", "輔助模型", "Auxiliary models"],
289
- ["settings.auxHint", "Vision、Web extract、Compression 等每一列都寫出實際模型。跟主模型時仍顯示名稱,不是空白。", "Each row — Vision, web extract, compression, and the rest shows the model in use. Inherited rows still name the default."],
317
+ ["settings.auxHint", "只有 Vision、Web extract、SubAgent 可以另選模型。其餘用途一律跟主模型。", "Only Vision, web extract, and SubAgent can use a different model. Everything else follows the default."],
290
318
  ["settings.auxReset", "全部改回主模型", "Reset all to default"],
291
319
  ["settings.useMain", "改回跟主模型", "Follow default"],
292
320
  ["settings.change", "更改", "Change"],
@@ -297,6 +325,7 @@ var I18N_ROWS = [
297
325
  ["settings.noDefault", "尚未設定主模型", "No default model"],
298
326
  ["settings.currentMain", "目前主模型:{name}", "Default: {name}"],
299
327
  ["settings.delete", "刪除", "Delete"],
328
+ ["settings.deletedProvider", "已刪除 {name}", "Deleted {name}"],
300
329
  ["settings.locale", "介面語言", "Language"],
301
330
  ["settings.localeHint", "寫進這個瀏覽器。大廳、編制、工坊、模型共用。", "Saved in this browser. Shared by Hall, Roster, Workshop, and Models."],
302
331
  ["settings.oauthCode", "在瀏覽器開啟授權頁,輸入代碼:", "Open the auth page and enter this code:"],
@@ -326,6 +355,8 @@ var I18N_ROWS = [
326
355
  ["studio.streetAlt", "Guild 酒館門口", "Guild inn door"],
327
356
  ["studio.leave", "出門", "Leave"],
328
357
  ["studio.inn", "酒館", "Inn"],
358
+ ["studio.idle", "待命", "idle"],
359
+ ["studio.diving", "潛水", "diving"],
329
360
  ["studio.streetLine", "夜晚的酒館還開著。點那扇門進去。", "The inn is still open. Tap the door."],
330
361
  ["studio.tonight", "今晚", "Tonight"],
331
362
  ["studio.hire", "招募冒險者", "Recruit an adventurer"],
@@ -384,6 +415,10 @@ var I18N_ROWS = [
384
415
  ["studio.vacant", "空位", "Vacant"],
385
416
  ["studio.vacantLine", "空座位。要招冒險者進來嗎?", "Empty seat. Recruit someone?"],
386
417
  ["studio.hireChip", "招募", "Hire"],
418
+ ["studio.genLook", "生成形象", "Generate look"],
419
+ ["studio.genLookBusy", "畫臉中…", "Drawing…"],
420
+ ["studio.genLookDone", "新形象好了。", "New look ready."],
421
+ ["studio.genLookFail", "生圖失敗。到模型頁接 Grok 訂閱或填金鑰再試。", "Image failed. Connect a Grok subscription or add a key on the models page."],
387
422
  ["studio.drink", "……今晚在這兒喝酒。", "…drinking here tonight."],
388
423
  ["studio.notFound", "找不到這名冒險者", "Adventurer not found"],
389
424
  ["studio.editing", "正在編輯 @{handle} · 勾選只屬於這名冒險者", "Editing @{handle} · skills are only for this adventurer"],
@@ -15,6 +15,7 @@
15
15
  rel="stylesheet"
16
16
  />
17
17
  <link rel="stylesheet" href="/style.css" />
18
+ <script src="/buddy.js"></script>
18
19
  </head>
19
20
  <body class="office-page">
20
21
  <header class="appbar">
@@ -63,53 +64,7 @@
63
64
  const colors = ["#7c6af7", "#5b8def", "#2ea887", "#e07a3d", "#d4537e"];
64
65
  return colors[seedOf(key) % colors.length];
65
66
  }
66
- function px(ctx, x, y, w, h, color) {
67
- ctx.fillStyle = color;
68
- ctx.fillRect(x, y, w, h);
69
- }
70
- function drawBuddy(canvas, shirt, seed) {
71
- const ctx = canvas.getContext("2d");
72
- canvas.width = 16;
73
- canvas.height = 24;
74
- ctx.imageSmoothingEnabled = false;
75
- const skin = ["#f0c8a0", "#e2b184", "#c68642", "#8d5524"][seed % 4];
76
- const hair = ["#2a1c12", "#c4a35a", "#1a1a1a", "#6b2d3c", "#3c4a6e"][seed % 5];
77
- const outline = "#1b1510";
78
- const pants = "#2a3140";
79
- const style = seed % 3;
80
-
81
- px(ctx, 4, 12, 8, 7, outline);
82
- px(ctx, 5, 13, 6, 5, shirt);
83
- px(ctx, 3, 13, 2, 5, shirt);
84
- px(ctx, 11, 13, 2, 5, shirt);
85
- px(ctx, 3, 18, 2, 1, skin);
86
- px(ctx, 11, 18, 2, 1, skin);
87
-
88
- px(ctx, 4, 4, 8, 8, outline);
89
- px(ctx, 5, 5, 6, 6, skin);
90
- px(ctx, 6, 7, 1, 1, outline);
91
- px(ctx, 9, 7, 1, 1, outline);
92
- px(ctx, 7, 9, 2, 1, "#c07070");
93
67
 
94
- if (style === 0) {
95
- px(ctx, 4, 3, 8, 3, hair);
96
- px(ctx, 4, 5, 2, 3, hair);
97
- px(ctx, 10, 5, 2, 3, hair);
98
- } else if (style === 1) {
99
- px(ctx, 4, 2, 8, 4, hair);
100
- px(ctx, 3, 5, 2, 4, hair);
101
- px(ctx, 11, 5, 2, 4, hair);
102
- } else {
103
- px(ctx, 5, 2, 6, 2, hair);
104
- px(ctx, 4, 4, 8, 2, hair);
105
- px(ctx, 7, 1, 2, 2, hair);
106
- }
107
-
108
- px(ctx, 5, 19, 3, 4, pants);
109
- px(ctx, 8, 19, 3, 4, pants);
110
- px(ctx, 5, 23, 3, 1, outline);
111
- px(ctx, 8, 23, 3, 1, outline);
112
- }
113
68
  function escapeHtml(value) {
114
69
  return String(value)
115
70
  .replace(/&/g, "&amp;")