From 28e154eb3a6d8153caf652b06ab155f5c721dadd Mon Sep 17 00:00:00 2001 From: Danyang Chen Date: Mon, 19 Oct 2026 00:14:21 +0800 Subject: [PATCH 1/2] Face-to-face room rows stay on one line with the full title on hover (part of #304) --- changelog.d/304-face-row-one-line.fixed.md | 1 + core/client.js | 8 +++++--- core/src/client/sidebar.js | 5 ++++- core/src/client/styles.js | 4 ++-- core/test/flybot-client.test.js | 16 ++++++++++++++++ patches/ds-bot/0003-flybot-api-client.patch | 16 ++++++++++------ patches/ds-bot/0008-client-bundle.patch | 16 ++++++++++++---- patches/ds-bot/files/test/flybot-client.test.js | 16 ++++++++++++++++ plugins/social/client.js | 3 ++- plugins/social/test/social.test.js | 2 ++ 10 files changed, 70 insertions(+), 17 deletions(-) create mode 100644 changelog.d/304-face-row-one-line.fixed.md diff --git a/changelog.d/304-face-row-one-line.fixed.md b/changelog.d/304-face-row-one-line.fixed.md new file mode 100644 index 000000000..80b542bcf --- /dev/null +++ b/changelog.d/304-face-row-one-line.fixed.md @@ -0,0 +1 @@ +A face-to-face room's sidebar row stays on one line (only a friend's Bot row wraps to two), and the row and its Past rooms entry show the full title on hover. ([#304](https://github.com/ThinkFlowLab/FlyBot/issues/304)) diff --git a/core/client.js b/core/client.js index b07698b57..4512efe4e 100644 --- a/core/client.js +++ b/core/client.js @@ -3004,8 +3004,8 @@ body[data-bt-motion=quiet] .bt-mark[data-poke]{cursor:default} .bt-row-body{display:flex;flex-direction:column;min-width:0;flex:1 1 auto} .bt-row-name{display:flex;align-items:center;gap:6px;min-width:0;font-size:14px;line-height:20px;font-weight:400} .bt-row-title{min-width:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis} -.bt-row:has(.bt-row-remote) .bt-row-title,.bt-row-wrap:has(.bt-row-remote) .bt-row-title{white-space:normal;display:-webkit-box;-webkit-box-orient:vertical;-webkit-line-clamp:2;line-clamp:2;overflow:hidden;text-overflow:ellipsis} -.bt-row:has(.bt-row-remote) .bt-row-name,.bt-row-wrap:has(.bt-row-remote) .bt-row-name{align-items:flex-start} +.bt-row:has(.bt-row-remote:not(.bt-row-face)) .bt-row-title,.bt-row-wrap:has(.bt-row-remote:not(.bt-row-face)) .bt-row-title{white-space:normal;display:-webkit-box;-webkit-box-orient:vertical;-webkit-line-clamp:2;line-clamp:2;overflow:hidden;text-overflow:ellipsis} +.bt-row:has(.bt-row-remote:not(.bt-row-face)) .bt-row-name,.bt-row-wrap:has(.bt-row-remote:not(.bt-row-face)) .bt-row-name{align-items:flex-start} .bt-row-remote{flex:none;display:inline-flex;align-items:center;color:var(--bt-ink-3,rgba(20,20,20,.36))} .bt-tag{flex:none;max-width:96px;font-size:12px;line-height:16px;font-weight:400;padding:1px 6px;border-radius:4px;background:var(--bt-tag-bg);color:var(--bt-tag-ink);white-space:nowrap;overflow:hidden;text-overflow:ellipsis} .bt-row-preview{font-size:13px;line-height:18px;color:var(--bt-ink-2);white-space:nowrap;overflow:hidden;text-overflow:ellipsis} @@ -6129,7 +6129,7 @@ function isPastFaceRoom(room2) { function FaceMark() { return (0, import_react9.createElement)( "span", - { className: "bt-row-remote", title: t("Face-to-face"), "aria-label": t("Face-to-face") }, + { className: "bt-row-remote bt-row-face", title: t("Face-to-face"), "aria-label": t("Face-to-face") }, (0, import_react9.createElement)( "svg", { width: 11, height: 11, viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: 2, strokeLinecap: "round", "aria-hidden": true }, @@ -6163,6 +6163,8 @@ function RoomRow({ room: room2, roster, actions, useSessions, useSessionStatus, className: "bt-row", "aria-label": label, "aria-current": active ? "page" : void 0, + // The full title on hover: a long face-room title is cut to one line in the row (#304, #173). + title, "data-room-id": id, onClick: () => actions.openSession(id), onContextMenu: (event) => { diff --git a/core/src/client/sidebar.js b/core/src/client/sidebar.js index 4367ca778..4c10e2aed 100644 --- a/core/src/client/sidebar.js +++ b/core/src/client/sidebar.js @@ -112,8 +112,9 @@ function isPastFaceRoom(room) { return status === 'candidate' && expires && Number.isFinite(Date.parse(expires)) && Date.parse(expires) <= Date.now() } +// A face room's people mark. It is not a friend's Bot marker, so the row keeps one line (#304). function FaceMark() { - return h('span', { className: 'bt-row-remote', title: t('Face-to-face'), 'aria-label': t('Face-to-face') }, + return h('span', { className: 'bt-row-remote bt-row-face', title: t('Face-to-face'), 'aria-label': t('Face-to-face') }, h('svg', { width: 11, height: 11, viewBox: '0 0 24 24', fill: 'none', stroke: 'currentColor', strokeWidth: 2, strokeLinecap: 'round', 'aria-hidden': true }, h('circle', { cx: 9, cy: 8, r: 3 }), h('circle', { cx: 16, cy: 9, r: 2.5 }), h('path', { d: 'M4 18a5 5 0 0 1 10 0M14 18a4 4 0 0 1 6 0' }))) @@ -138,6 +139,8 @@ function RoomRow({ room, roster, actions, useSessions, useSessionStatus, useSess const label = [title, face ? t('Face-to-face') : '', pinned ? t('Pinned') : '', members.length > 0 ? t('Working') : '', waiting ? t('Waiting for your approval') : ''].filter(Boolean).join(', ') return h('button', { type: 'button', className: 'bt-row', 'aria-label': label, 'aria-current': active ? 'page' : undefined, + // The full title on hover: a long face-room title is cut to one line in the row (#304, #173). + title, 'data-room-id': id, onClick: () => actions.openSession(id), onContextMenu: (event) => { event.preventDefault(); actions.openMenu({ kind: 'room', id, x: event.clientX, y: event.clientY }) }, }, diff --git a/core/src/client/styles.js b/core/src/client/styles.js index 0d530a407..d4a350b3c 100644 --- a/core/src/client/styles.js +++ b/core/src/client/styles.js @@ -148,8 +148,8 @@ body[data-bt-motion=quiet] .bt-mark[data-poke]{cursor:default} .bt-row-body{display:flex;flex-direction:column;min-width:0;flex:1 1 auto} .bt-row-name{display:flex;align-items:center;gap:6px;min-width:0;font-size:14px;line-height:20px;font-weight:400} .bt-row-title{min-width:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis} -.bt-row:has(.bt-row-remote) .bt-row-title,.bt-row-wrap:has(.bt-row-remote) .bt-row-title{white-space:normal;display:-webkit-box;-webkit-box-orient:vertical;-webkit-line-clamp:2;line-clamp:2;overflow:hidden;text-overflow:ellipsis} -.bt-row:has(.bt-row-remote) .bt-row-name,.bt-row-wrap:has(.bt-row-remote) .bt-row-name{align-items:flex-start} +.bt-row:has(.bt-row-remote:not(.bt-row-face)) .bt-row-title,.bt-row-wrap:has(.bt-row-remote:not(.bt-row-face)) .bt-row-title{white-space:normal;display:-webkit-box;-webkit-box-orient:vertical;-webkit-line-clamp:2;line-clamp:2;overflow:hidden;text-overflow:ellipsis} +.bt-row:has(.bt-row-remote:not(.bt-row-face)) .bt-row-name,.bt-row-wrap:has(.bt-row-remote:not(.bt-row-face)) .bt-row-name{align-items:flex-start} .bt-row-remote{flex:none;display:inline-flex;align-items:center;color:var(--bt-ink-3,rgba(20,20,20,.36))} .bt-tag{flex:none;max-width:96px;font-size:12px;line-height:16px;font-weight:400;padding:1px 6px;border-radius:4px;background:var(--bt-tag-bg);color:var(--bt-tag-ink);white-space:nowrap;overflow:hidden;text-overflow:ellipsis} .bt-row-preview{font-size:13px;line-height:18px;color:var(--bt-ink-2);white-space:nowrap;overflow:hidden;text-overflow:ellipsis} diff --git a/core/test/flybot-client.test.js b/core/test/flybot-client.test.js index 464384311..9b7e86370 100644 --- a/core/test/flybot-client.test.js +++ b/core/test/flybot-client.test.js @@ -168,3 +168,19 @@ test('the account menu keeps a Plugins row that opens the shell plugin panel', a assert.match(menu, /\[t\('Connectors'\), ConnectorIcon, \(\) => actions\.openConnectors\(\)\]/) assert.equal(DICTIONARY.zh.Plugins, '插件') }) + +test('a face-room sidebar row stays on one line and shows its full title on hover (#304)', async () => { + const { readFileSync } = await import('node:fs') + const sidebar = readFileSync(new URL('../src/client/sidebar.js', import.meta.url), 'utf8').replace(/\r\n/g, '\n') + const styles = readFileSync(new URL('../src/client/styles.js', import.meta.url), 'utf8').replace(/\r\n/g, '\n') + const mark = sidebar.slice(sidebar.indexOf('function FaceMark('), sidebar.indexOf('\n}\n', sidebar.indexOf('function FaceMark('))) + assert.match(mark, /className: 'bt-row-remote bt-row-face'/) + const row = sidebar.slice(sidebar.indexOf('function RoomRow('), sidebar.indexOf('function AgentSessionRow(')) + assert.match(row, /'aria-current': active \? 'page' : undefined,\n(\s*\/\/[^\n]*\n)?\s*title,\n/) + // The two-line clamp is for a friend's Bot marker only, never the face-room people mark. + const clamp = styles.split('\n').filter(line => line.includes('-webkit-line-clamp:2') && line.includes('.bt-row-remote')) + assert.ok(clamp.length > 0) + for (const line of clamp) assert.doesNotMatch(line, /:has\(\.bt-row-remote\)/) + assert.match(styles, /\.bt-row:has\(\.bt-row-remote:not\(\.bt-row-face\)\) \.bt-row-title/) + assert.match(styles, /\.bt-row-title\{min-width:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis\}/) +}) diff --git a/patches/ds-bot/0003-flybot-api-client.patch b/patches/ds-bot/0003-flybot-api-client.patch index 975745ade..1c5600b23 100644 --- a/patches/ds-bot/0003-flybot-api-client.patch +++ b/patches/ds-bot/0003-flybot-api-client.patch @@ -1178,7 +1178,7 @@ diff --git a/src/client/sidebar.js b/src/client/sidebar.js import { showsDshRow } from './dsh-offer.js' // ------------------------------------------------------------------------- -@@ -37,56 +37,90 @@ const workKeys = text => (text === '' ? [] : text.split(',').map((key) => { +@@ -37,56 +37,91 @@ const workKeys = text => (text === '' ? [] : text.split(',').map((key) => { })) const workLine = (lead, tool, more) => h(Shimmer, null, `${lead} · ${workPhrase(tool)}…${more > 0 ? ` +${more}` : ''}`) @@ -1265,8 +1265,9 @@ diff --git a/src/client/sidebar.js b/src/client/sidebar.js + return status === 'candidate' && expires && Number.isFinite(Date.parse(expires)) && Date.parse(expires) <= Date.now() +} + ++// A face room's people mark. It is not a friend's Bot marker, so the row keeps one line (#304). +function FaceMark() { -+ return h('span', { className: 'bt-row-remote', title: t('Face-to-face'), 'aria-label': t('Face-to-face') }, ++ return h('span', { className: 'bt-row-remote bt-row-face', title: t('Face-to-face'), 'aria-label': t('Face-to-face') }, + h('svg', { width: 11, height: 11, viewBox: '0 0 24 24', fill: 'none', stroke: 'currentColor', strokeWidth: 2, strokeLinecap: 'round', 'aria-hidden': true }, + h('circle', { cx: 9, cy: 8, r: 3 }), h('circle', { cx: 16, cy: 9, r: 2.5 }), + h('path', { d: 'M4 18a5 5 0 0 1 10 0M14 18a4 4 0 0 1 6 0' }))) @@ -1281,7 +1282,7 @@ diff --git a/src/client/sidebar.js b/src/client/sidebar.js const active = useSessionRetainInfo(id, info => (info?.retainedBy?.mainView ?? 0) > 0) const unread = useSessionStatus(map => map.get(id)?.completionUnread === true) || (marked && !active) useMarkedUnread(id, active, marked, actions) -@@ -96,7 +96,10 @@ function RoomRow({ room, roster, actions, useSessions, useSessionStatus, useSess +@@ -96,18 +96,23 @@ function RoomRow({ room, roster, actions, useSessions, useSessionStatus, useSess const workingLine = first ? workLine(first.name, members[0].tool, members.length - 1) : null if (room.hidden && !active) return null const pinned = room.pinned === true @@ -1292,8 +1293,11 @@ diff --git a/src/client/sidebar.js b/src/client/sidebar.js + const label = [title, face ? t('Face-to-face') : '', pinned ? t('Pinned') : '', members.length > 0 ? t('Working') : '', waiting ? t('Waiting for your approval') : ''].filter(Boolean).join(', ') return h('button', { type: 'button', className: 'bt-row', 'aria-label': label, 'aria-current': active ? 'page' : undefined, ++ // The full title on hover: a long face-room title is cut to one line in the row (#304, #173). ++ title, 'data-room-id': id, onClick: () => actions.openSession(id), -@@ -105,9 +105,9 @@ function RoomRow({ room, roster, actions, useSessions, useSessionStatus, useSess + onContextMenu: (event) => { event.preventDefault(); actions.openMenu({ kind: 'room', id, x: event.clientX, y: event.clientY }) }, + }, h(RoomAvatar, { room, roster, size: 36 }), pinned && !wide ? h(PinMark) : null, wide ? h('span', { className: 'bt-row-body' }, @@ -1587,8 +1591,8 @@ diff --git a/src/client/styles.js b/src/client/styles.js .bt-row-body{display:flex;flex-direction:column;min-width:0;flex:1 1 auto} .bt-row-name{display:flex;align-items:center;gap:6px;min-width:0;font-size:14px;line-height:20px;font-weight:400} .bt-row-title{min-width:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis} -+.bt-row:has(.bt-row-remote) .bt-row-title,.bt-row-wrap:has(.bt-row-remote) .bt-row-title{white-space:normal;display:-webkit-box;-webkit-box-orient:vertical;-webkit-line-clamp:2;line-clamp:2;overflow:hidden;text-overflow:ellipsis} -+.bt-row:has(.bt-row-remote) .bt-row-name,.bt-row-wrap:has(.bt-row-remote) .bt-row-name{align-items:flex-start} ++.bt-row:has(.bt-row-remote:not(.bt-row-face)) .bt-row-title,.bt-row-wrap:has(.bt-row-remote:not(.bt-row-face)) .bt-row-title{white-space:normal;display:-webkit-box;-webkit-box-orient:vertical;-webkit-line-clamp:2;line-clamp:2;overflow:hidden;text-overflow:ellipsis} ++.bt-row:has(.bt-row-remote:not(.bt-row-face)) .bt-row-name,.bt-row-wrap:has(.bt-row-remote:not(.bt-row-face)) .bt-row-name{align-items:flex-start} +.bt-row-remote{flex:none;display:inline-flex;align-items:center;color:var(--bt-ink-3,rgba(20,20,20,.36))} .bt-tag{flex:none;max-width:96px;font-size:12px;line-height:16px;font-weight:400;padding:1px 6px;border-radius:4px;background:var(--bt-tag-bg);color:var(--bt-tag-ink);white-space:nowrap;overflow:hidden;text-overflow:ellipsis} .bt-row-preview{font-size:13px;line-height:18px;color:var(--bt-ink-2);white-space:nowrap;overflow:hidden;text-overflow:ellipsis} diff --git a/patches/ds-bot/0008-client-bundle.patch b/patches/ds-bot/0008-client-bundle.patch index 74d43cfe6..da9c52024 100644 --- a/patches/ds-bot/0008-client-bundle.patch +++ b/patches/ds-bot/0008-client-bundle.patch @@ -1551,8 +1551,8 @@ diff --git a/client.js b/client.js .bt-row-body{display:flex;flex-direction:column;min-width:0;flex:1 1 auto} .bt-row-name{display:flex;align-items:center;gap:6px;min-width:0;font-size:14px;line-height:20px;font-weight:400} .bt-row-title{min-width:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis} -+.bt-row:has(.bt-row-remote) .bt-row-title,.bt-row-wrap:has(.bt-row-remote) .bt-row-title{white-space:normal;display:-webkit-box;-webkit-box-orient:vertical;-webkit-line-clamp:2;line-clamp:2;overflow:hidden;text-overflow:ellipsis} -+.bt-row:has(.bt-row-remote) .bt-row-name,.bt-row-wrap:has(.bt-row-remote) .bt-row-name{align-items:flex-start} ++.bt-row:has(.bt-row-remote:not(.bt-row-face)) .bt-row-title,.bt-row-wrap:has(.bt-row-remote:not(.bt-row-face)) .bt-row-title{white-space:normal;display:-webkit-box;-webkit-box-orient:vertical;-webkit-line-clamp:2;line-clamp:2;overflow:hidden;text-overflow:ellipsis} ++.bt-row:has(.bt-row-remote:not(.bt-row-face)) .bt-row-name,.bt-row-wrap:has(.bt-row-remote:not(.bt-row-face)) .bt-row-name{align-items:flex-start} +.bt-row-remote{flex:none;display:inline-flex;align-items:center;color:var(--bt-ink-3,rgba(20,20,20,.36))} .bt-tag{flex:none;max-width:96px;font-size:12px;line-height:16px;font-weight:400;padding:1px 6px;border-radius:4px;background:var(--bt-tag-bg);color:var(--bt-tag-ink);white-space:nowrap;overflow:hidden;text-overflow:ellipsis} .bt-row-preview{font-size:13px;line-height:18px;color:var(--bt-ink-2);white-space:nowrap;overflow:hidden;text-overflow:ellipsis} @@ -3928,7 +3928,7 @@ diff --git a/client.js b/client.js +function FaceMark() { + return (0, import_react9.createElement)( + "span", -+ { className: "bt-row-remote", title: t("Face-to-face"), "aria-label": t("Face-to-face") }, ++ { className: "bt-row-remote bt-row-face", title: t("Face-to-face"), "aria-label": t("Face-to-face") }, + (0, import_react9.createElement)( + "svg", + { width: 11, height: 11, viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: 2, strokeLinecap: "round", "aria-hidden": true }, @@ -3947,7 +3947,7 @@ diff --git a/client.js b/client.js const active = useSessionRetainInfo(id, (info) => (info?.retainedBy?.mainView ?? 0) > 0); const unread = useSessionStatus((map) => map.get(id)?.completionUnread === true) || marked && !active; useMarkedUnread(id, active, marked, actions); -@@ -4826,8 +4826,11 @@ function RoomRow({ room: room2, roster, actions, useSessions, useSessionStatus, +@@ -4826,14 +4826,19 @@ function RoomRow({ room: room2, roster, actions, useSessions, useSessionStatus, const workingLine = first ? workLine(first.name, members[0].tool, members.length - 1) : null; if (room2.hidden && !active) return null; const pinned = room2.pinned === true; @@ -3961,6 +3961,14 @@ diff --git a/client.js b/client.js "button", { type: "button", + className: "bt-row", + "aria-label": label, + "aria-current": active ? "page" : void 0, ++ // The full title on hover: a long face-room title is cut to one line in the row (#304, #173). ++ title, + "data-room-id": id, + onClick: () => actions.openSession(id), + onContextMenu: (event) => { @@ -4841,20 +4841,20 @@ function RoomRow({ room: room2, roster, actions, useSessions, useSessionStatus, actions.openMenu({ kind: "room", id, x: event.clientX, y: event.clientY }); } diff --git a/patches/ds-bot/files/test/flybot-client.test.js b/patches/ds-bot/files/test/flybot-client.test.js index 464384311..9b7e86370 100644 --- a/patches/ds-bot/files/test/flybot-client.test.js +++ b/patches/ds-bot/files/test/flybot-client.test.js @@ -168,3 +168,19 @@ test('the account menu keeps a Plugins row that opens the shell plugin panel', a assert.match(menu, /\[t\('Connectors'\), ConnectorIcon, \(\) => actions\.openConnectors\(\)\]/) assert.equal(DICTIONARY.zh.Plugins, '插件') }) + +test('a face-room sidebar row stays on one line and shows its full title on hover (#304)', async () => { + const { readFileSync } = await import('node:fs') + const sidebar = readFileSync(new URL('../src/client/sidebar.js', import.meta.url), 'utf8').replace(/\r\n/g, '\n') + const styles = readFileSync(new URL('../src/client/styles.js', import.meta.url), 'utf8').replace(/\r\n/g, '\n') + const mark = sidebar.slice(sidebar.indexOf('function FaceMark('), sidebar.indexOf('\n}\n', sidebar.indexOf('function FaceMark('))) + assert.match(mark, /className: 'bt-row-remote bt-row-face'/) + const row = sidebar.slice(sidebar.indexOf('function RoomRow('), sidebar.indexOf('function AgentSessionRow(')) + assert.match(row, /'aria-current': active \? 'page' : undefined,\n(\s*\/\/[^\n]*\n)?\s*title,\n/) + // The two-line clamp is for a friend's Bot marker only, never the face-room people mark. + const clamp = styles.split('\n').filter(line => line.includes('-webkit-line-clamp:2') && line.includes('.bt-row-remote')) + assert.ok(clamp.length > 0) + for (const line of clamp) assert.doesNotMatch(line, /:has\(\.bt-row-remote\)/) + assert.match(styles, /\.bt-row:has\(\.bt-row-remote:not\(\.bt-row-face\)\) \.bt-row-title/) + assert.match(styles, /\.bt-row-title\{min-width:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis\}/) +}) diff --git a/plugins/social/client.js b/plugins/social/client.js index fefc72815..7ba56ddc4 100644 --- a/plugins/social/client.js +++ b/plugins/social/client.js @@ -854,7 +854,8 @@ window.__ModuleLoader__.load({ shown.map(m => h('span', { key: m.handle || m.id, 'data-flybot': 'face-card-member', 'data-handle': m.handle }, h(FaceAvatar, { bot: look(m), size: 20 }))), extra ? h('span', { className: 'fs-stack-more' }, `+${extra}`) : null), - h('span', { className: 'fs-name' }, shownFaceTitle(room)), + // One line; the full title on hover when the narrow sidebar cuts it (#304). + h('span', { className: 'fs-name', title: shownFaceTitle(room) }, shownFaceTitle(room)), h('span', { className: 'fs-chip fs-chip-status', 'data-flybot': 'face-status', 'data-status': faceChip(room) }, faceChipLabel(room)), h('span', { className: 'fs-face-actions', onClick: event => event.stopPropagation() }, past diff --git a/plugins/social/test/social.test.js b/plugins/social/test/social.test.js index 22852dc70..6711801b5 100644 --- a/plugins/social/test/social.test.js +++ b/plugins/social/test/social.test.js @@ -2630,6 +2630,8 @@ test('Face-to-face room card titles include people and a distinguisher, not Face })) const title = tree.find(n => n.props.className === 'fs-name') assert.equal(String(title?.children?.join?.('') ?? ''), 'Face-to-face · Alice, Bob · 482913') + // The row stays on one line; the full title is the hover text when the sidebar cuts it (#304). + assert.equal(title.props.title, 'Face-to-face · Alice, Bob · 482913') assert.equal(tree.some(n => String(n.children?.join?.('') ?? n.children) === 'Face-to-face room'), false) }) From 09c100e7498b9f2f7b4ce7c0f91d9938231cb697 Mon Sep 17 00:00:00 2001 From: Danyang Chen Date: Mon, 19 Oct 2026 00:20:10 +0800 Subject: [PATCH 2/2] Docs: one-line face-room rows and hover titles (#304) --- docs/subsystems/core-client.md | 2 +- docs/subsystems/plugin-runtime.md | 2 +- docs/subsystems/social.md | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/subsystems/core-client.md b/docs/subsystems/core-client.md index b5a110e27..6f3f6d40d 100644 --- a/docs/subsystems/core-client.md +++ b/docs/subsystems/core-client.md @@ -87,7 +87,7 @@ The overlay (`flybot-slots.js` `FlyBotApprovals`) is the fallback, not a second ### One sidebar list -- `TeamSidebar` lists local Bots, listed remote chats (`isListedRemoteChat`), live face-to-face rooms, and shared rooms in one `bt-list`. Own chats sort before foreign ones (a remote Bot, a face room, or `sharedFrom`); pinned and recency still win. A friend's Bot keeps the globe `RemoteMark`; a live face room gets a small people `FaceMark`. Past/ended face rooms stay out of this list (`isPastFaceRoom`) so the social section can fold them under **Past rooms**. With `@flybot/tree` on, only own Bots are tree nodes under Main; a friend's Bot stays in the flat ordered list (not hidden when Main is collapsed). +- `TeamSidebar` lists local Bots, listed remote chats (`isListedRemoteChat`), live face-to-face rooms, and shared rooms in one `bt-list`. Own chats sort before foreign ones (a remote Bot, a face room, or `sharedFrom`); pinned and recency still win. A friend's Bot keeps the globe `RemoteMark`; a live face room gets a small people `FaceMark` (`bt-row-remote bt-row-face`). A friend's Bot row may wrap its title to two lines; a face-room row stays on one line with an ellipsis (the clamp skips `.bt-row-face`), and every room row has its full title as hover text (#304). Past/ended face rooms stay out of this list (`isPastFaceRoom`) so the social section can fold them under **Past rooms**. With `@flybot/tree` on, only own Bots are tree nodes under Main; a friend's Bot stays in the flat ordered list (not hidden when Main is collapsed). ### Approval wait badge on chat rows diff --git a/docs/subsystems/plugin-runtime.md b/docs/subsystems/plugin-runtime.md index 615ae547a..dd60348d4 100644 --- a/docs/subsystems/plugin-runtime.md +++ b/docs/subsystems/plugin-runtime.md @@ -22,7 +22,7 @@ FlyBot's plugin API surface on top of the patched core: host hooks, client slots - `core/src/client/flybot-slots.js` — client slot registry. - `core/src/client/flybot-registry.js` — plugin discovery/registration. - `core/types/flybot.d.ts` — public types. -- Flybot unit tests (`flybot.test.js`, `flybot-client.test.js`, `flybot-feed.test.js`); `flybot-client.test.js` also pins the sidebar footer button (Connect apps opens Connectors) and the account menu's Plugins row. +- Flybot unit tests (`flybot.test.js`, `flybot-client.test.js`, `flybot-feed.test.js`); `flybot-client.test.js` also pins the sidebar footer button (Connect apps opens Connectors), the account menu's Plugins row, and the one-line face-room row with its hover title (#304). ## Does not own diff --git a/docs/subsystems/social.md b/docs/subsystems/social.md index 6fb49bd6f..2bf6441b2 100644 --- a/docs/subsystems/social.md +++ b/docs/subsystems/social.md @@ -75,7 +75,7 @@ pins the list. The authority for the list is Bot Social's README and `docs/FACE_ ### Face-to-face waiting grid, leave, and social + (#124, #125, #126) - Waiting UI (join dialog and `flybot.room.overview` while `candidate`) is a WeChat-like tile grid: the 6-digit room number at the top, avatar on top; a joined member leads with the person (`data-flybot=face-owner`, the tile name) and the Bot they brought on a second line (`data-flybot=face-bot`), so two Chiefs are told apart (#229); picker tiles show the Bot name only. **Leave** and **Enter chat** share one footer row. Join copy uses owner + Bot name (`Chief · BoxA` / `BoxA's Chief`), never `BoxA's BoxA` or generic `BoxA's Bot`. Setup copy says you wait together until someone enters the chat. Ended joining says **Joining has ended**, not “matching window”. The group subtitle is live participant count (no “0 members · No admin”). -- **Leave** after the number is void (`bot_not_found` / room already gone) still drops the room off the active list onto **Past rooms**. Past rows are unique by `faceRoomId` and have one primary action (**Open**); live rows are the main-list RoomRow (people mark) with **Leave** on the open room overview. +- **Leave** after the number is void (`bot_not_found` / room already gone) still drops the room off the active list onto **Past rooms**. Past rows are unique by `faceRoomId`, keep the title on one line with the full title as hover text (#304), and have one primary action (**Open**); live rows are the main-list RoomRow (people mark) with **Leave** on the open room overview. - #192: **Leave** keeps the hosted room, marked `left` (`upsertFace`), so **Open** in Past rooms finds it already hosted and shows the read-only transcript with `You left this room.` (before, `dropFace` removed it and Open built an empty room the browser did not have yet). A past room that falls off the 20 kept entries is dropped then. - #193: the account-menu handle row overrides the menu's full-width button rule so the handle text stays visible next to **Copy**. - #194: a face room title shows one time: Bot Social's `created_at`, else the joining deadline minus the 10-minute window (`faceRoomCreatedAt`), the same before and after Enter chat and on every home.