@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.
@@ -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>
@@ -167,12 +167,7 @@
167
167
  <a class="model-choice" href="/settings" data-i18n="model.settings">狀態欄</a>
168
168
  </div>
169
169
  </div>
170
- <select id="model-reasoning" hidden>
171
- <option value="minimal" data-i18n="reason.min">最低</option>
172
- <option value="low" data-i18n="reason.low">低</option>
173
- <option value="medium" data-i18n="reason.mid">中</option>
174
- <option value="high" data-i18n="reason.high">高</option>
175
- </select>
170
+ <select id="model-reasoning" hidden></select>
176
171
  <input type="checkbox" id="model-fast" hidden />
177
172
  </div>
178
173
  </div>
@@ -266,6 +261,13 @@
266
261
  </div>
267
262
  <script>
268
263
  const COLORS = ["#7c6af7", "#5b8def", "#2ea887", "#e07a3d", "#d4537e"];
264
+ const SEAT_HUE = {
265
+ infra: "#5b8def",
266
+ pm: "#2ea887",
267
+ rd: "#e07a3d",
268
+ design: "#7c6af7",
269
+ marketing: "#d4537e",
270
+ };
269
271
  const CHANNEL_ROSTER_CAP = 6;
270
272
  function isGeneralChannel(ch) {
271
273
  return Boolean(ch && (ch.id === "channel-general" || ch.name === "general"));
@@ -321,6 +323,8 @@
321
323
  };
322
324
 
323
325
  function hue(key) {
326
+ const seat = SEAT_HUE[String(key).toLowerCase()];
327
+ if (seat) return seat;
324
328
  let n = 0;
325
329
  for (const ch of String(key)) n = (n + ch.charCodeAt(0) * 17) % COLORS.length;
326
330
  return COLORS[n];
@@ -341,6 +345,15 @@
341
345
  const glyph = Array.from(String(name).replace(/\s+/g, ""))[0] || "?";
342
346
  return glyph;
343
347
  }
348
+ function portraitUrl(bot) {
349
+ const url = bot && typeof bot.portrait === "string" ? bot.portrait.trim() : "";
350
+ return url.indexOf("/generated/") === 0 ? url : "";
351
+ }
352
+ function avatarFace(bot) {
353
+ const src = portraitUrl(bot);
354
+ if (src) return '<img alt="" src="' + escapeHtml(src) + '">';
355
+ return escapeHtml(initials((bot && (bot.name || bot.handle)) || "?"));
356
+ }
344
357
  function escapeHtml(value) {
345
358
  return String(value)
346
359
  .replace(/&/g, "&amp;")
@@ -1027,14 +1040,48 @@
1027
1040
  }
1028
1041
  return out;
1029
1042
  }
1043
+ function deferredHandles(text) {
1044
+ const raw = String(text || "");
1045
+ const out = [];
1046
+ const add = (name) => {
1047
+ const key = String(name || "").toLowerCase();
1048
+ if (key && out.indexOf(key) < 0) out.push(key);
1049
+ };
1050
+ const scan = raw.replace(/```[\s\S]*?```/g, " ");
1051
+ const cue =
1052
+ /(?:完成後|通過後|最後(?:才|由)?|才需要|才叫|才交)[^@\n]{0,24}@([A-Za-z0-9_-]+)/gi;
1053
+ const bare =
1054
+ /(?:完成後|才需要|才叫)\s*(?:call|ask|由)\s*`?@?([A-Za-z0-9_-]+)`?/gi;
1055
+ let row;
1056
+ while ((row = cue.exec(scan))) add(row[1]);
1057
+ while ((row = bare.exec(scan))) add(row[1]);
1058
+ const later =
1059
+ /^[ \t]*(?:\d+[.)、]\s*|[-*•]\s+)(?:通過後|最後|然後|之後|完成後)\s*`?@([A-Za-z0-9_-]+)`?/i;
1060
+ const lines = raw.split(/\r?\n/);
1061
+ for (let i = 0; i < lines.length; i++) {
1062
+ const hit = lines[i].match(later);
1063
+ if (hit) add(hit[1]);
1064
+ }
1065
+ return out;
1066
+ }
1030
1067
  function summonedBotIds(text) {
1031
1068
  const raw = String(text || "");
1069
+ const deferred = new Set(deferredHandles(raw));
1070
+ const members = new Set(
1071
+ state.kind === "channel"
1072
+ ? ((currentChannel() && currentChannel().memberIds) || [])
1073
+ : state.kind === "dm"
1074
+ ? [state.id]
1075
+ : [],
1076
+ );
1032
1077
  const known = new Map(
1033
- (state.bots || []).map((bot) => [String(bot.handle).toLowerCase(), bot.id]),
1078
+ (state.bots || [])
1079
+ .filter((bot) => !members.size || members.has(bot.id))
1080
+ .map((bot) => [String(bot.handle).toLowerCase(), bot.id]),
1034
1081
  );
1035
1082
  const out = [];
1036
- const push = (id) => {
1037
- if (id && !out.includes(id)) out.push(id);
1083
+ const push = (id, handle) => {
1084
+ if (id && !deferred.has(handle) && !out.includes(id)) out.push(id);
1038
1085
  };
1039
1086
  let fence = false;
1040
1087
  const lines = raw.split(/\r?\n/);
@@ -1051,11 +1098,21 @@
1051
1098
  if (!m) continue;
1052
1099
  const names = m[1].match(/@([A-Za-z0-9_-]+)/g) || [];
1053
1100
  for (let n = 0; n < names.length; n++) {
1054
- push(known.get(names[n].slice(1).toLowerCase()));
1101
+ const handle = names[n].slice(1).toLowerCase();
1102
+ push(known.get(handle), handle);
1055
1103
  }
1056
1104
  }
1057
1105
  if (out.length) return out;
1058
- return mentionedBotIds(raw).slice(0, 1);
1106
+ return mentionedBotIds(raw)
1107
+ .filter((id) => {
1108
+ const bot = (state.bots || []).find((row) => row.id === id);
1109
+ return (
1110
+ bot &&
1111
+ (!members.size || members.has(id)) &&
1112
+ !deferred.has(String(bot.handle).toLowerCase())
1113
+ );
1114
+ })
1115
+ .slice(0, 1);
1059
1116
  }
1060
1117
  function lastBotAuthor() {
1061
1118
  const msgs = state.messages || [];
@@ -1128,6 +1185,12 @@
1128
1185
  hour12: true,
1129
1186
  });
1130
1187
  }
1188
+ function formatMsgWhen(iso) {
1189
+ const day = formatMsgDay(iso);
1190
+ const clock = formatMsgClock(iso);
1191
+ if (day && clock) return day + " " + clock;
1192
+ return clock || day || "";
1193
+ }
1131
1194
  function formatMsgDay(iso) {
1132
1195
  if (!iso) return "";
1133
1196
  const when = new Date(iso);
@@ -1229,7 +1292,20 @@
1229
1292
  }
1230
1293
  function botModelMeta(bot) {
1231
1294
  const model = botModelLabel(bot) || "—";
1232
- const reason = reasonLabel((modelCfg && modelCfg.reasoning) || "medium");
1295
+ const spec = (() => {
1296
+ const ref = botModelRef(bot);
1297
+ if (!ref) return null;
1298
+ const hit = allModels().find(
1299
+ (row) => row.provider === ref.provider && row.model === ref.model,
1300
+ );
1301
+ return (hit && hit.reasoning) || null;
1302
+ })();
1303
+ const reasonVal = clampEffort(
1304
+ modelCfg && modelCfg.reasoning,
1305
+ spec,
1306
+ Boolean(modelCfg && modelCfg.fast),
1307
+ );
1308
+ const reason = reasonVal ? reasonLabel(reasonVal) : "—";
1233
1309
  const provider = botProviderLabel(bot) || "—";
1234
1310
  return model + " · " + reason + " · " + provider;
1235
1311
  }
@@ -1266,11 +1342,15 @@
1266
1342
  const foot = opts.party
1267
1343
  ? opts.party
1268
1344
  : "<em>" + escapeHtml(opts.preview || "") + "</em>";
1269
- return (
1270
- '<li><a class="nav-row' +
1345
+ const cls =
1346
+ "nav-row" +
1271
1347
  (opts.on ? " on" : "") +
1272
1348
  (opts.unread ? " unread" : "") +
1273
1349
  (opts.notice ? " notice" : "") +
1350
+ (opts.branch ? " nav-branch" : "");
1351
+ const inner =
1352
+ '<a class="' +
1353
+ cls +
1274
1354
  '" href="' +
1275
1355
  opts.href +
1276
1356
  '">' +
@@ -1282,8 +1362,9 @@
1282
1362
  right +
1283
1363
  "</span>" +
1284
1364
  foot +
1285
- "</span></a></li>"
1286
- );
1365
+ "</span></a>";
1366
+ if (opts.bare) return inner;
1367
+ return "<li>" + inner + "</li>";
1287
1368
  }
1288
1369
  function partyStackHtml(members) {
1289
1370
  const bots = Array.isArray(members) ? members : [];
@@ -1293,7 +1374,7 @@
1293
1374
  '<span class="avatar" style="background:' +
1294
1375
  hue(bot.handle) +
1295
1376
  '">' +
1296
- escapeHtml(initials(bot.name)) +
1377
+ avatarFace(bot) +
1297
1378
  "</span>",
1298
1379
  )
1299
1380
  .join("");
@@ -1343,31 +1424,93 @@
1343
1424
  }
1344
1425
  function renderNav() {
1345
1426
  const q = document.getElementById("filter").value.toLowerCase();
1346
- const channels = state.channels
1347
- .filter((ch) => !q || ch.name.toLowerCase().includes(q))
1427
+ const all = state.channels || [];
1428
+ const byId = new Map(all.map((ch) => [ch.id, ch]));
1429
+ const kidsOf = (id) =>
1430
+ all.filter((ch) => ch.parentId === id).slice().sort(byUpdatedAtDesc);
1431
+ const visible = new Set();
1432
+ for (const ch of all) {
1433
+ const hit = !q || String(ch.name || "").toLowerCase().includes(q);
1434
+ if (!hit) continue;
1435
+ let walk = ch;
1436
+ while (walk) {
1437
+ visible.add(walk.id);
1438
+ walk = walk.parentId ? byId.get(walk.parentId) : null;
1439
+ }
1440
+ }
1441
+ function channelBusy(ch) {
1442
+ return Boolean(
1443
+ (state.busyRoom &&
1444
+ state.busyRoom.kind === "channel" &&
1445
+ state.busyRoom.id === ch.id) ||
1446
+ (state.serverLive || []).some(
1447
+ (row) => row.kind === "channel" && row.id === ch.id,
1448
+ ),
1449
+ );
1450
+ }
1451
+ function channelUpdated(ch) {
1452
+ let latest = ch.updatedAt ? Date.parse(ch.updatedAt) : 0;
1453
+ for (const kid of kidsOf(ch.id)) {
1454
+ const child = channelUpdated(kid);
1455
+ const ts = child ? Date.parse(child) : 0;
1456
+ if (ts > latest) latest = ts;
1457
+ }
1458
+ return Number.isFinite(latest) && latest > 0
1459
+ ? new Date(latest).toISOString()
1460
+ : ch.updatedAt;
1461
+ }
1462
+ function branchTree(parentId) {
1463
+ return kidsOf(parentId)
1464
+ .filter((ch) => visible.has(ch.id))
1465
+ .map((ch) => {
1466
+ const nested = branchTree(ch.id);
1467
+ const row = navRowHtml({
1468
+ bare: true,
1469
+ branch: true,
1470
+ href: "#c/" + encodeURIComponent(ch.id),
1471
+ on: state.kind === "channel" && state.id === ch.id,
1472
+ notice: true,
1473
+ name: "↳ " + ch.name,
1474
+ updatedAt: ch.updatedAt,
1475
+ party: partyStackHtml(ch.members),
1476
+ busy: channelBusy(ch),
1477
+ unread: state.unread.has(roomKey("channel", ch.id)),
1478
+ });
1479
+ return nested
1480
+ ? "<li>" + row + '<ul class="nav-branches">' + nested + "</ul></li>"
1481
+ : "<li>" + row + "</li>";
1482
+ })
1483
+ .join("");
1484
+ }
1485
+ const roots = all
1486
+ .filter((ch) => !ch.parentId && visible.has(ch.id))
1348
1487
  .slice()
1349
- .sort(byUpdatedAtDesc);
1350
- document.getElementById("channels").innerHTML = channels
1488
+ .sort((a, b) =>
1489
+ byUpdatedAtDesc(
1490
+ { updatedAt: channelUpdated(a) },
1491
+ { updatedAt: channelUpdated(b) },
1492
+ ),
1493
+ );
1494
+ document.getElementById("channels").innerHTML = roots
1351
1495
  .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({
1496
+ const nested = branchTree(ch.id);
1497
+ const row = navRowHtml({
1498
+ bare: true,
1362
1499
  href: "#c/" + encodeURIComponent(ch.id),
1363
1500
  on: state.kind === "channel" && state.id === ch.id,
1364
1501
  notice: true,
1365
1502
  name: ch.name,
1366
- updatedAt: ch.updatedAt,
1503
+ updatedAt: channelUpdated(ch),
1367
1504
  party: partyStackHtml(ch.members),
1368
- busy: busy,
1369
- unread: unread,
1505
+ busy: channelBusy(ch),
1506
+ unread: state.unread.has(roomKey("channel", ch.id)),
1370
1507
  });
1508
+ return (
1509
+ '<li class="nav-quest">' +
1510
+ row +
1511
+ (nested ? '<ul class="nav-branches">' + nested + "</ul>" : "") +
1512
+ "</li>"
1513
+ );
1371
1514
  })
1372
1515
  .join("");
1373
1516
  const bots = state.bots
@@ -1391,7 +1534,7 @@
1391
1534
  '<span class="avatar" style="background:' +
1392
1535
  hue(bot.handle) +
1393
1536
  '">' +
1394
- escapeHtml(initials(bot.name)) +
1537
+ avatarFace(bot) +
1395
1538
  "</span>",
1396
1539
  name: bot.name,
1397
1540
  updatedAt: bot.updatedAt,
@@ -1426,7 +1569,7 @@
1426
1569
  '" style="background:' +
1427
1570
  hue(bot.handle) +
1428
1571
  '">' +
1429
- escapeHtml(initials(bot.name)) +
1572
+ avatarFace(bot) +
1430
1573
  '</button><span class="who"><strong>' +
1431
1574
  escapeHtml(bot.name) +
1432
1575
  "</strong><em>@" +
@@ -1445,9 +1588,9 @@
1445
1588
  document.getElementById("subtitle").textContent = "";
1446
1589
  wrap.hidden = true;
1447
1590
  document.getElementById("members-pop").hidden = true;
1448
- av.textContent = bot ? initials(bot.name) : "?";
1449
1591
  av.style.background = bot ? hue(bot.handle) : "#333";
1450
1592
  av.classList.remove("hash-av");
1593
+ av.innerHTML = bot ? avatarFace(bot) : "?";
1451
1594
  if (bot) {
1452
1595
  av.setAttribute("href", "#");
1453
1596
  av.setAttribute("data-bot-id", bot.id);
@@ -1480,7 +1623,7 @@
1480
1623
  '<span class="avatar" style="background:' +
1481
1624
  hue(bot.handle) +
1482
1625
  '">' +
1483
- escapeHtml(initials(bot.name)) +
1626
+ avatarFace(bot) +
1484
1627
  "</span>",
1485
1628
  )
1486
1629
  .join("");
@@ -1624,7 +1767,7 @@
1624
1767
  '" style="background:' +
1625
1768
  hue(bot.handle) +
1626
1769
  '">' +
1627
- escapeHtml(initials(bot.name)) +
1770
+ avatarFace(bot) +
1628
1771
  "</button>",
1629
1772
  head:
1630
1773
  '<div class="msg-head"><button type="button" class="name"' +
@@ -1848,23 +1991,28 @@
1848
1991
  rail.innerHTML = you
1849
1992
  .map((el) => {
1850
1993
  const id = el.getAttribute("data-id") || "";
1994
+ const when = formatMsgWhen(el.getAttribute("data-at") || "");
1851
1995
  const preview = youPromptPreview(el);
1852
1996
  const top = Math.max(
1853
1997
  6,
1854
1998
  Math.min(track - 6, ((el.offsetTop + el.offsetHeight / 2) / total) * track),
1855
1999
  );
2000
+ const label = [when, preview].filter(Boolean).join(" · ");
1856
2001
  return (
1857
2002
  '<button type="button" class="prompt-tick" data-prompt-jump="' +
1858
2003
  escapeHtml(id) +
1859
2004
  '" style="top:' +
1860
2005
  top +
1861
- 'px"' +
2006
+ 'px" aria-label="' +
2007
+ escapeHtml(t("promptJump") + (label ? " · " + label : "")) +
2008
+ '"><span class="prompt-tip">' +
2009
+ (when
2010
+ ? '<time class="prompt-tip-when">' + escapeHtml(when) + "</time>"
2011
+ : "") +
1862
2012
  (preview
1863
- ? ' data-preview="' + escapeHtml(preview) + '"'
2013
+ ? '<span class="prompt-tip-text">' + escapeHtml(preview) + "</span>"
1864
2014
  : "") +
1865
- ' aria-label="' +
1866
- escapeHtml(t("promptJump") + (preview ? " · " + preview : "")) +
1867
- '"></button>'
2015
+ "</span></button>"
1868
2016
  );
1869
2017
  })
1870
2018
  .join("");
@@ -1988,6 +2136,15 @@
1988
2136
  '">' +
1989
2137
  t("retry") +
1990
2138
  "</button>" +
2139
+ (state.kind === "channel" &&
2140
+ !msg.queued &&
2141
+ String(msg.id || "").indexOf("pending-") !== 0
2142
+ ? '<button type="button" data-branch="' +
2143
+ escapeHtml(msg.id) +
2144
+ '">' +
2145
+ t("branch") +
2146
+ "</button>"
2147
+ : "") +
1991
2148
  (msg.queued
1992
2149
  ? ""
1993
2150
  : '<button type="button" class="danger" data-msg-del="' +
@@ -2003,6 +2160,8 @@
2003
2160
  (msg.queued ? " queued" : msg.steer ? " steer" : "") +
2004
2161
  '" data-id="' +
2005
2162
  escapeHtml(msg.id) +
2163
+ '" data-at="' +
2164
+ escapeHtml(stamp) +
2006
2165
  '">' +
2007
2166
  clock +
2008
2167
  body +
@@ -2021,7 +2180,7 @@
2021
2180
  '" style="background:' +
2022
2181
  hue(handle || name) +
2023
2182
  '">' +
2024
- escapeHtml(initials(name)) +
2183
+ (bot ? avatarFace(bot) : escapeHtml(initials(name))) +
2025
2184
  "</button>";
2026
2185
  const head =
2027
2186
  '<div class="msg-head"><button type="button" class="name"' +
@@ -2122,6 +2281,30 @@
2122
2281
  await loadWorkspace();
2123
2282
  if (state.kind === "channel") await loadMessages();
2124
2283
  }
2284
+ async function closeBranch(id, ch) {
2285
+ if (!id) return;
2286
+ if (!confirm(t("branch.closeConfirm", { name: (ch && ch.name) || id }))) {
2287
+ return;
2288
+ }
2289
+ const merge = confirm(t("branch.closeMerge"));
2290
+ const res = await fetch(
2291
+ "/channels/" + encodeURIComponent(id) + "/close",
2292
+ {
2293
+ method: "POST",
2294
+ headers: { "content-type": "application/json" },
2295
+ body: JSON.stringify({ merge: merge }),
2296
+ },
2297
+ );
2298
+ const body = await res.json().catch(() => ({}));
2299
+ if (!res.ok) throw new Error(body.error || t("branch.closeFailed"));
2300
+ const parentId = (ch && ch.parentId) || body.parentId || "channel-general";
2301
+ if (state.kind === "channel" && state.id === id) {
2302
+ state.id = parentId;
2303
+ setHash();
2304
+ }
2305
+ await loadWorkspace();
2306
+ if (state.kind === "channel") await loadMessages();
2307
+ }
2125
2308
 
2126
2309
 
2127
2310
  function roomMessagesUrl(kind, id, suffix) {
@@ -2471,6 +2654,29 @@
2471
2654
  renderThread();
2472
2655
  renderNav();
2473
2656
  }
2657
+ async function branchFromMessage(id) {
2658
+ if (state.kind !== "channel" || !state.id) return;
2659
+ const msg = state.messages.find((item) => item.id === id);
2660
+ if (!msg) return;
2661
+ const def = String(msg.body || "")
2662
+ .replace(/\s+/g, " ")
2663
+ .trim()
2664
+ .slice(0, 28);
2665
+ const typed = window.prompt(t("branch.name"), def || "");
2666
+ if (typed === null) return;
2667
+ const name = typed.trim() || def;
2668
+ const res = await fetch(
2669
+ "/channels/" + encodeURIComponent(state.id) + "/branches",
2670
+ {
2671
+ method: "POST",
2672
+ headers: { "content-type": "application/json" },
2673
+ body: JSON.stringify({ messageId: id, name: name || undefined }),
2674
+ },
2675
+ );
2676
+ const body = await res.json().catch(() => ({}));
2677
+ if (!res.ok) throw new Error(body.error || "branch failed");
2678
+ location.hash = "#c/" + encodeURIComponent(body.id);
2679
+ }
2474
2680
  async function retryMessage(id, body) {
2475
2681
  if (canStopHere()) {
2476
2682
  stopTurn();
@@ -2499,6 +2705,7 @@
2499
2705
  const payload = {};
2500
2706
  if (typeof body === "string") payload.body = body;
2501
2707
  if (botIds.length === 1) payload.assigneeId = botIds[0];
2708
+ if (botIds.length) payload.mentions = botIds;
2502
2709
  await postChat(
2503
2710
  messagesUrl(id) + "/retry",
2504
2711
  payload,
@@ -2688,6 +2895,12 @@
2688
2895
  await retryMessage(retry.getAttribute("data-retry"));
2689
2896
  return;
2690
2897
  }
2898
+ const branchBtn = event.target.closest("[data-branch]");
2899
+ if (branchBtn) {
2900
+ event.preventDefault();
2901
+ await branchFromMessage(branchBtn.getAttribute("data-branch"));
2902
+ return;
2903
+ }
2691
2904
  const del = event.target.closest("[data-msg-del]");
2692
2905
  if (del) {
2693
2906
  event.preventDefault();
@@ -2807,7 +3020,11 @@
2807
3020
  const nameEl = document.getElementById("channel-md-name");
2808
3021
  nameEl.value = ch ? ch.name : "";
2809
3022
  nameEl.disabled = locked;
2810
- document.getElementById("channel-md-del").hidden = locked;
3023
+ const del = document.getElementById("channel-md-del");
3024
+ const branched = Boolean(ch && ch.parentId);
3025
+ del.hidden = locked;
3026
+ del.setAttribute("data-i18n", branched ? "branch.close" : "channel.delete");
3027
+ del.textContent = t(branched ? "branch.close" : "channel.delete");
2811
3028
  channelMdDialog.showModal();
2812
3029
  }
2813
3030
  const botMemoryDialog = document.getElementById("bot-memory");
@@ -2997,7 +3214,8 @@
2997
3214
  document.getElementById("channel-md-del").addEventListener("click", async () => {
2998
3215
  const ch = currentChannel();
2999
3216
  try {
3000
- await deleteChannel(state.id, ch && ch.name);
3217
+ if (ch && ch.parentId) await closeBranch(state.id, ch);
3218
+ else await deleteChannel(state.id, ch && ch.name);
3001
3219
  channelMdDialog.close();
3002
3220
  } catch (err) {
3003
3221
  alert(err.message);
@@ -4385,11 +4603,13 @@
4385
4603
  if (!pending) return;
4386
4604
  const fromCands = (cands || []).map((bot) => bot.id).filter(Boolean);
4387
4605
  const fromReply = replyAuthorId(pending.replyTo);
4388
- const botIds = fromCands.length
4389
- ? fromCands
4606
+ const botIds = summoned.length
4607
+ ? summoned
4390
4608
  : fromReply
4391
4609
  ? [fromReply]
4392
- : botsFromSend(pending.raw) || [];
4610
+ : fromCands.length
4611
+ ? fromCands
4612
+ : botsFromSend(pending.raw) || [];
4393
4613
  const assigneeId = botIds.length === 1 ? botIds[0] : "";
4394
4614
  const ids = botIds;
4395
4615
  const split = splitSendTargets(ids);
@@ -4432,6 +4652,7 @@
4432
4652
  if (replyTo) payload.replyTo = replyTo;
4433
4653
  if (attachments.length) payload.attachments = attachments;
4434
4654
  if (assigneeId) payload.assigneeId = assigneeId;
4655
+ if (botIds && botIds.length) payload.mentions = botIds;
4435
4656
  if (here) {
4436
4657
  state.messages = state.messages.concat([
4437
4658
  {
@@ -4481,7 +4702,7 @@
4481
4702
  '"><span class="avatar sm" style="background:' +
4482
4703
  hue(bot.handle) +
4483
4704
  '">' +
4484
- escapeHtml(initials(bot.name)) +
4705
+ avatarFace(bot) +
4485
4706
  '</span><span class="assign-copy"><strong>' +
4486
4707
  escapeHtml(bot.name) +
4487
4708
  "</strong><span>@" +
@@ -4547,8 +4768,15 @@
4547
4768
  openAssign(cands, { raw: raw, packed: packed, replyTo: replyTo });
4548
4769
  return;
4549
4770
  }
4550
- const botIds = botsFromSend(raw);
4551
- const assigneeId = cands.length === 1 ? cands[0].id : "";
4771
+ const fromReply = replyAuthorId(replyTo);
4772
+ const botIds = summoned.length
4773
+ ? summoned
4774
+ : fromReply
4775
+ ? [fromReply]
4776
+ : botsFromSend(raw);
4777
+ const assigneeId = summoned.length === 1
4778
+ ? summoned[0]
4779
+ : fromReply || (cands.length === 1 ? cands[0].id : "");
4552
4780
  await flushSend(raw, packed, replyTo, botIds, assigneeId);
4553
4781
  });
4554
4782
  function asLibList(value) {
@@ -5165,6 +5393,7 @@
5165
5393
  group: p.name || p.id,
5166
5394
  ready: Boolean(p.ready),
5167
5395
  kind: p.kind || "key",
5396
+ reasoning: m.reasoning || null,
5168
5397
  });
5169
5398
  }
5170
5399
  }
@@ -5178,10 +5407,45 @@
5178
5407
  return hit ? hit.name : ref.model;
5179
5408
  }
5180
5409
  function reasonLabel(value) {
5410
+ if (value === "none") return t("reason.none");
5181
5411
  if (value === "minimal") return t("reason.min");
5182
5412
  if (value === "low") return t("reason.low");
5413
+ if (value === "medium") return t("reason.mid");
5183
5414
  if (value === "high") return t("reason.high");
5184
- return t("reason.mid");
5415
+ if (value === "xhigh") return t("reason.xhigh");
5416
+ if (value === "max") return t("reason.max");
5417
+ return value || t("reason.mid");
5418
+ }
5419
+ function activeReasoningSpec() {
5420
+ const ref = activeModelRef();
5421
+ if (!ref) return null;
5422
+ const hit = allModels().find(
5423
+ (row) => row.provider === ref.provider && row.model === ref.model,
5424
+ );
5425
+ return (hit && hit.reasoning) || null;
5426
+ }
5427
+ function effortChoices(spec) {
5428
+ const raw = spec && spec.supportedEfforts;
5429
+ if (!raw || !raw.length) return [];
5430
+ return spec.mandatory
5431
+ ? raw.filter((key) => key !== "none")
5432
+ : raw.slice();
5433
+ }
5434
+ function clampEffort(want, spec, fast) {
5435
+ const choices = effortChoices(spec);
5436
+ if (!choices.length) return "";
5437
+ if (fast) {
5438
+ if (choices.indexOf("low") >= 0) return "low";
5439
+ if (choices.indexOf("minimal") >= 0) return "minimal";
5440
+ return choices[choices.length - 1];
5441
+ }
5442
+ if (want && choices.indexOf(want) >= 0) return want;
5443
+ if (spec.defaultEffort && choices.indexOf(spec.defaultEffort) >= 0) {
5444
+ return spec.defaultEffort;
5445
+ }
5446
+ if (choices.indexOf("high") >= 0) return "high";
5447
+ if (choices.indexOf("medium") >= 0) return "medium";
5448
+ return choices[0];
5185
5449
  }
5186
5450
  function speedLabel() {
5187
5451
  return modelCfg.fast ? t("speed.fast") : t("speed.normal");
@@ -5283,25 +5547,44 @@
5283
5547
  }
5284
5548
  document.getElementById("model-results").innerHTML =
5285
5549
  html || '<p class="model-empty">' + t("noModels") + "</p>";
5286
- document.getElementById("model-reasoning").value = modelCfg.reasoning || "medium";
5287
5550
  document.getElementById("model-fast").checked = Boolean(modelCfg.fast);
5288
- const reason = reasonLabel(modelCfg.reasoning || "medium");
5551
+ const spec = activeReasoningSpec();
5552
+ const choices = effortChoices(spec);
5553
+ const reasonVal = clampEffort(
5554
+ modelCfg.reasoning,
5555
+ spec,
5556
+ Boolean(modelCfg.fast),
5557
+ );
5558
+ const reasonSel = document.getElementById("model-reasoning");
5559
+ reasonSel.innerHTML = choices
5560
+ .map(
5561
+ (value) =>
5562
+ '<option value="' +
5563
+ value +
5564
+ '">' +
5565
+ reasonLabel(value) +
5566
+ "</option>",
5567
+ )
5568
+ .join("");
5569
+ if (reasonVal) reasonSel.value = reasonVal;
5570
+ const reasonRow = document.querySelector('.model-nav [data-sheet="reasoning"]');
5571
+ if (reasonRow) reasonRow.hidden = !choices.length;
5572
+ if (!choices.length) {
5573
+ const pane = document.getElementById("model-sheet-reasoning");
5574
+ if (pane && !pane.hidden) showModelSheet("models");
5575
+ }
5576
+ const reason = choices.length ? reasonLabel(reasonVal) : "";
5289
5577
  const speed = speedLabel();
5290
5578
  const name = displayName(current);
5291
5579
  document.getElementById("model-btn-label").textContent = current
5292
- ? name + " " + reason
5580
+ ? name + (reason ? " " + reason : "")
5293
5581
  : name;
5294
5582
  document.getElementById("model-row-model").textContent = name;
5295
- document.getElementById("model-row-reason").textContent = reason;
5583
+ document.getElementById("model-row-reason").textContent = reason || "—";
5296
5584
  document.getElementById("model-row-speed").textContent = speed;
5297
- document.getElementById("model-reason-list").innerHTML = [
5298
- ["minimal", t("reason.min")],
5299
- ["low", t("reason.low")],
5300
- ["medium", t("reason.mid")],
5301
- ["high", t("reason.high")],
5302
- ]
5303
- .map(([value, label]) => {
5304
- const on = (modelCfg.reasoning || "medium") === value ? " on" : "";
5585
+ document.getElementById("model-reason-list").innerHTML = choices
5586
+ .map((value) => {
5587
+ const on = reasonVal === value ? " on" : "";
5305
5588
  const tick = on ? '<span class="tick">✓</span>' : "";
5306
5589
  return (
5307
5590
  '<button type="button" class="model-choice' +
@@ -5309,7 +5592,7 @@
5309
5592
  '" data-reason="' +
5310
5593
  value +
5311
5594
  '">' +
5312
- label +
5595
+ reasonLabel(value) +
5313
5596
  tick +
5314
5597
  "</button>"
5315
5598
  );
@@ -5537,7 +5820,9 @@
5537
5820
  ? '<span class="traj-av" style="background:' +
5538
5821
  hue(bot.handle) +
5539
5822
  '">' +
5540
- escapeHtml(glyph) +
5823
+ (portraitUrl(bot)
5824
+ ? '<img alt="" src="' + escapeHtml(portraitUrl(bot)) + '">'
5825
+ : escapeHtml(glyph)) +
5541
5826
  "</span>"
5542
5827
  : '<span class="traj-av you">' + escapeHtml(glyph) + "</span>";
5543
5828
  return (