| index 5a6e6c6..55e4211 100644 |
| --- a/web/static/chat.js |
| +++ b/web/static/chat.js |
| @@ -663,8 +663,11 @@ function agentTag(muted, kind) { |
| t.title = ai |
| ? "An AI helper — what you send it is sent to an AI provider to answer" |
| : "An automated helper (bot), not a person — it runs on this server"; |
| + // The chip is centered by its flex parent (each name line is a flex row with |
| + // align-items:center — see the call sites), so it needs no baseline nudge; |
| + // flex:none keeps it from shrinking, line-height:1 keeps the box tight. |
| t.style.cssText = |
| - "margin-left:6px;font-size:0.7em;font-weight:600;letter-spacing:0.03em;padding:1px 5px;border-radius:6px;vertical-align:middle;transform:translateY(-1px);" + |
| + "flex:none;margin-left:6px;font-size:0.7em;line-height:1;font-weight:600;letter-spacing:0.03em;padding:2px 5px;border-radius:6px;vertical-align:middle;" + |
| (muted ? "background:var(--border,#ddd);color:var(--muted,#666);" : "background:var(--accent,#888);color:#fff;"); |
| return t; |
| } |
| @@ -673,15 +676,19 @@ function agentTag(muted, kind) { |
| // one-line description (the Helpers catalog blurb — the "what am I adding?" hint). |
| function pickerName(e) { |
| const nm = document.createElement("span"); |
| - nm.textContent = e.label; |
| - if (e.is_agent) { |
| - nm.appendChild(agentTag(false, e.kind)); |
| - if (e.description) { |
| - const d = document.createElement("span"); |
| - d.className = "helper-desc"; |
| - d.textContent = e.description; |
| - nm.appendChild(d); |
| - } |
| + // Name + badge on a centered flex line; the description (if any) sits below. |
| + const line = document.createElement("span"); |
| + line.style.cssText = "display:flex;align-items:center;"; |
| + const txt = document.createElement("span"); |
| + txt.textContent = e.label; |
| + line.appendChild(txt); |
| + if (e.is_agent) line.appendChild(agentTag(false, e.kind)); |
| + nm.appendChild(line); |
| + if (e.is_agent && e.description) { |
| + const d = document.createElement("span"); |
| + d.className = "helper-desc"; |
| + d.textContent = e.description; |
| + nm.appendChild(d); |
| } |
| return nm; |
| } |
| @@ -1427,7 +1434,19 @@ function renderThreadList() { |
| body.className = "thread-row-body"; |
| const name = document.createElement("span"); |
| name.className = "thread-row-name"; |
| - name.textContent = threadName[t.id] || "Chat"; |
| + // Text in its own span so it keeps ellipsis-truncating while the row is a |
| + // flex line (which vertically centers the name with any badge). |
| + const nameText = document.createElement("span"); |
| + nameText.className = "thread-row-name-text"; |
| + nameText.textContent = threadName[t.id] || "Chat"; |
| + name.appendChild(nameText); |
| + // Badge a DM whose other member is a helper, so the chat index shows AI/BOT |
| + // like the picker and message senders do (muted — it repeats per row). |
| + if (t.kind === "dm") { |
| + const other = (t.members || []).find((id) => id !== myUserId); |
| + const e = roster.find((x) => x.user_id === other); |
| + if (e && e.is_agent) name.appendChild(agentTag(true, e.kind)); |
| + } |
| body.appendChild(name); |
| if (threadPreview[t.id]) { |
| const prev = document.createElement("span"); |
| @@ -1682,14 +1701,19 @@ function paintThreadHeader(id) { |
| const isGroup = t && t.kind === "group"; |
| document.getElementById("thread-title").textContent = threadName[id] || "Chat"; |
| const sub = document.getElementById("thread-sub"); |
| - // Count people and bots separately (roster is_agent), so the subtitle reads |
| - // "2 members, 3 bots" rather than lumping bots in as members. |
| + // Count people, deterministic bots, and AI agents separately (roster is_agent |
| + // + kind), so the subtitle reads "2 members, 2 bots, 1 AI agent" — mirroring |
| + // the BOT/AI badges rather than lumping helpers in as members. |
| const members = t ? t.members || [] : []; |
| - const bots = members.filter((id) => { |
| + let bots = 0; |
| + let agents = 0; |
| + for (const id of members) { |
| const e = roster.find((x) => x.user_id === id); |
| - return e && e.is_agent; |
| - }).length; |
| - sub.textContent = isGroup ? memberCountLabel(members.length - bots, bots) : ""; |
| + if (!e || !e.is_agent) continue; |
| + if (e.kind === "ai") agents++; |
| + else bots++; |
| + } |
| + sub.textContent = isGroup ? memberCountLabel(members.length - bots - agents, bots, agents) : ""; |
| sub.onclick = isGroup ? () => showMembers() : null; // tap "N members" → member list |
| sub.style.cursor = isGroup ? "pointer" : ""; |
| // The kebab is always available (every chat can be closed); the group-only |
| @@ -1914,6 +1938,8 @@ async function appendMessage(threadId, m, scroll = true, container = null, cache |
| if (!mine && t && t.kind === "group") { |
| const nm = document.createElement("div"); |
| nm.className = "name"; |
| + nm.style.display = "flex"; // center the name text with its AI/BOT badge |
| + nm.style.alignItems = "center"; |
| nm.textContent = data.name || ""; |
| // Badge bot senders on the name label itself, so "who's a bot" is visible |
| // on every message, not just in the picker. Sender names aren't signed, so |
| @@ -4084,7 +4110,12 @@ function showMembers() { |
| main.className = "member-main"; |
| main.appendChild(avatarEl(name, () => avatarFor(uid))); |
| const lbl = document.createElement("span"); |
| - lbl.textContent = me ? `${name} (you)` : name; |
| + lbl.style.display = "flex"; // center the name with its badge |
| + lbl.style.alignItems = "center"; |
| + const lblText = document.createElement("span"); |
| + lblText.style.minWidth = "0"; // keep ellipsis truncation inside the flex line |
| + lblText.textContent = me ? `${name} (you)` : name; |
| + lbl.appendChild(lblText); |
| // Transparency: a bot in the room stays visibly a bot, not just in the picker. |
| const entry = roster.find((x) => x.user_id === uid); |
| if (entry && entry.is_agent) lbl.appendChild(agentTag(false, entry.kind)); |