diff --git a/changelog.d/384-friend-row-one-line.fixed.md b/changelog.d/384-friend-row-one-line.fixed.md new file mode 100644 index 000000000..664d52eac --- /dev/null +++ b/changelog.d/384-friend-row-one-line.fixed.md @@ -0,0 +1 @@ +A friend's Bot in the sidebar reads like your own Bots: its name and its owner on one line (no more two-line wrap), the last message (or *Friend's Bot*) on the second line, a globe you can see, and the full name on hover. ([#384](https://github.com/ThinkFlowLab/FlyBot/issues/384)) diff --git a/core/client.js b/core/client.js index dfe912195..830e36847 100644 --- a/core/client.js +++ b/core/client.js @@ -3010,9 +3010,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: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-row-owner{flex:0 100 auto;min-width:24px;font-size:13px;color:var(--bt-ink-2);white-space:nowrap;overflow:hidden;text-overflow:ellipsis} +.bt-row-remote{flex:none;display:inline-flex;align-items:center;color:var(--bt-ink-2)} .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} .bt-unread{width:8px;height:8px;border-radius:50%;background:#1084fe;flex:none;margin-right:2px;animation:bt-fade .13s cubic-bezier(.22,1,.36,1)} @@ -6056,7 +6055,11 @@ function BotRow({ bot, roster, actions, useSessions, useSessionStatus, useSessio actions.openMenu({ kind: "bot", id, x: event.clientX, y: event.clientY }); }; const pinned = bot.pinned === true && !tile; - const shown = bot.remote ? remoteBotTitle({ displayName: bot.name, ownerName: bot.remote.homeName || bot.remote.owner, handle: bot.remote.handle, isChief: bot.remote.isChief }) : bot.name; + const remoteRecord = bot.remote ? { displayName: bot.name, ownerName: bot.remote.homeName || bot.remote.owner, handle: bot.remote.handle, isChief: bot.remote.isChief } : null; + const shown = remoteRecord ? remoteBotTitle(remoteRecord) : bot.name; + const rowName = remoteRecord ? cleanPeerName(bot.name, shown) : shown; + const owner = remoteRecord ? ownerNameOf(remoteRecord) || cleanPeerName(bot.remote.handle) : ""; + const secondLine = awayLine || preview || (remoteRecord ? t("Friend's Bot") : ""); const label = [shown, main ? t("Main Bot") : "", pinned ? t("Pinned") : "", running || away ? t("Working") : "", waiting ? t("Waiting for your approval") : alert ? t("Waiting for your answer") : "", unread ? t("Unread activity") : ""].filter(Boolean).join(", "); if (tile && wide) { return (0, import_react9.createElement)("div", { className: "bt-tile-wrap" }, (0, import_react9.createElement)( @@ -6085,7 +6088,7 @@ function BotRow({ bot, roster, actions, useSessions, useSessionStatus, useSessio "aria-label": label, "aria-current": active ? "page" : void 0, "data-bot-id": id, - title: wide ? void 0 : bot.name, + title: remoteRecord ? shown : wide ? void 0 : bot.name, onClick: () => actions.openSession(id), onContextMenu }, @@ -6094,8 +6097,15 @@ function BotRow({ bot, roster, actions, useSessions, useSessionStatus, useSessio wide ? (0, import_react9.createElement)( "span", { className: "bt-row-body" }, - (0, import_react9.createElement)("span", { className: "bt-row-name" }, (0, import_react9.createElement)("span", { className: "bt-row-title" }, shown), bot.remote ? (0, import_react9.createElement)(RemoteMark, { remote: bot.remote }) : null, pinned ? (0, import_react9.createElement)(PinMark) : null), - awayLine ? (0, import_react9.createElement)("span", { className: "bt-row-preview" }, awayLine) : preview ? (0, import_react9.createElement)("span", { className: "bt-row-preview" }, preview) : null + (0, import_react9.createElement)( + "span", + { className: "bt-row-name" }, + (0, import_react9.createElement)("span", { className: "bt-row-title" }, rowName), + owner ? (0, import_react9.createElement)("span", { className: "bt-row-owner" }, owner) : null, + bot.remote ? (0, import_react9.createElement)(RemoteMark, { remote: bot.remote }) : null, + pinned ? (0, import_react9.createElement)(PinMark) : null + ), + secondLine ? (0, import_react9.createElement)("span", { className: "bt-row-preview" }, secondLine) : null ) : null, unreadMark ); diff --git a/core/src/client/sidebar.js b/core/src/client/sidebar.js index 4c10e2aed..4679cd31c 100644 --- a/core/src/client/sidebar.js +++ b/core/src/client/sidebar.js @@ -11,7 +11,7 @@ import { reducedMotion } from './activity-row.js' import { Shimmer, workPhrase } from './cells.js' import { springEasing, SPRING_SHAPE } from './motion.js' import { hostText, t } from './i18n.js' -import { remoteBotTitle, sharedRoomTitle, isListedRemoteChat } from '../shared/remote-label.js' +import { cleanPeerName, ownerNameOf, remoteBotTitle, sharedRoomTitle, isListedRemoteChat } from '../shared/remote-label.js' import { showsDshRow } from './dsh-offer.js' // ------------------------------------------------------------------------- @@ -64,7 +64,14 @@ function BotRow({ bot, roster, actions, useSessions, useSessionStatus, useSessio actions.openMenu({ kind: 'bot', id, x: event.clientX, y: event.clientY }) } const pinned = bot.pinned === true && !tile - const shown = bot.remote ? remoteBotTitle({ displayName: bot.name, ownerName: bot.remote.homeName || bot.remote.owner, handle: bot.remote.handle, isChief: bot.remote.isChief }) : bot.name + const remoteRecord = bot.remote ? { displayName: bot.name, ownerName: bot.remote.homeName || bot.remote.owner, handle: bot.remote.handle, isChief: bot.remote.isChief } : null + const shown = remoteRecord ? remoteBotTitle(remoteRecord) : bot.name + // A friend's Bot row (#384): its name, then its owner, on one line like every other row; the + // second line is the preview, or "Friend's Bot" before the first message. The full title is on hover. + const rowName = remoteRecord ? cleanPeerName(bot.name, shown) : shown + // A plain per-Bot identity (@alice.bot) names no owner: its handle tells two "Helper" rows apart. + const owner = remoteRecord ? ownerNameOf(remoteRecord) || cleanPeerName(bot.remote.handle) : '' + const secondLine = awayLine || preview || (remoteRecord ? t("Friend's Bot") : '') const label = [shown, main ? t('Main Bot') : '', pinned ? t('Pinned') : '', running || away ? t('Working') : '', waiting ? t('Waiting for your approval') : alert ? t('Waiting for your answer') : '', unread ? t('Unread activity') : ''].filter(Boolean).join(', ') if (tile && wide) { return h('div', { className: 'bt-tile-wrap' }, h('button', { @@ -79,13 +86,16 @@ function BotRow({ bot, roster, actions, useSessions, useSessionStatus, useSessio : wide && unread && !active ? h('span', { className: 'bt-unread', 'aria-hidden': true }) : null const row = h('button', { type: 'button', className: 'bt-row', 'aria-label': label, 'aria-current': active ? 'page' : undefined, - 'data-bot-id': id, title: wide ? undefined : bot.name, onClick: () => actions.openSession(id), onContextMenu, + 'data-bot-id': id, title: remoteRecord ? shown : wide ? undefined : bot.name, onClick: () => actions.openSession(id), onContextMenu, }, h(BotAvatar, { bot, size: 36, state, main }), pinned && !wide ? h(PinMark) : null, wide ? h('span', { className: 'bt-row-body' }, - h('span', { className: 'bt-row-name' }, h('span', { className: 'bt-row-title' }, shown), bot.remote ? h(RemoteMark, { remote: bot.remote }) : null, pinned ? h(PinMark) : null), - awayLine ? h('span', { className: 'bt-row-preview' }, awayLine) : preview ? h('span', { className: 'bt-row-preview' }, preview) : null) : null, + h('span', { className: 'bt-row-name' }, + h('span', { className: 'bt-row-title' }, rowName), + owner ? h('span', { className: 'bt-row-owner' }, owner) : null, + bot.remote ? h(RemoteMark, { remote: bot.remote }) : null, pinned ? h(PinMark) : null), + secondLine ? h('span', { className: 'bt-row-preview' }, secondLine) : null) : null, unreadMark) // Hover/focus gear on every wide row (#276/#173): own Bots and friends' Bots alike. const settingsLabel = bot.remote ? t('Bot settings') : t('Edit Bot') @@ -97,7 +107,7 @@ function BotRow({ bot, roster, actions, useSessions, useSessionStatus, useSessio return row } -// A friend's Bot: a small globe after the name, its handle on hover. +// A friend's Bot: a globe after its owner's name, its handle on hover. function RemoteMark({ remote }) { return h('span', { className: 'bt-row-remote', title: remote.gone ? `${remote.handle} · ${t('no longer shared')}` : remote.handle, 'aria-label': t('Friend\'s Bot') }, h('svg', { width: 11, height: 11, viewBox: '0 0 24 24', fill: 'none', stroke: 'currentColor', strokeWidth: 2, strokeLinecap: 'round', 'aria-hidden': true }, diff --git a/core/src/client/styles.js b/core/src/client/styles.js index d4a350b3c..78c0afc9a 100644 --- a/core/src/client/styles.js +++ b/core/src/client/styles.js @@ -148,9 +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: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-row-owner{flex:0 100 auto;min-width:24px;font-size:13px;color:var(--bt-ink-2);white-space:nowrap;overflow:hidden;text-overflow:ellipsis} +.bt-row-remote{flex:none;display:inline-flex;align-items:center;color:var(--bt-ink-2)} .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} .bt-unread{width:8px;height:8px;border-radius:50%;background:#1084fe;flex:none;margin-right:2px;animation:bt-fade .13s cubic-bezier(.22,1,.36,1)} diff --git a/core/test/flybot-client.test.js b/core/test/flybot-client.test.js index 9b7e86370..ea4ec1de9 100644 --- a/core/test/flybot-client.test.js +++ b/core/test/flybot-client.test.js @@ -177,10 +177,26 @@ test('a face-room sidebar row stays on one line and shows its full title on hove 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/) + // No sidebar row wraps its title (#384 dropped the friend's Bot two-line clamp too). + assert.equal(styles.split('\n').some(line => line.includes('-webkit-line-clamp:2') && line.includes('.bt-row-')), false) assert.match(styles, /\.bt-row-title\{min-width:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis\}/) }) + +test("a friend's Bot sidebar row is its name and owner on one line, with a second line like own rows (#384)", 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 row = sidebar.slice(sidebar.indexOf('function BotRow('), sidebar.indexOf('function RemoteMark(')) + assert.match(row, /const rowName = remoteRecord \? cleanPeerName\(bot\.name, shown\) : shown/) + assert.match(row, /h\('span', \{ className: 'bt-row-title' \}, rowName\)/) + assert.match(row, /owner \? h\('span', \{ className: 'bt-row-owner' \}, owner\) : null/) + assert.match(row, /const owner = remoteRecord \? ownerNameOf\(remoteRecord\) \|\| cleanPeerName\(bot\.remote\.handle\) : ''/) + // Before the first message the second line still says what the row is. + assert.match(row, /const secondLine = awayLine \|\| preview \|\| \(remoteRecord \? t\("Friend's Bot"\) : ''\)/) + // The full "Helper · Bob's Bot" title is on hover and in the label. + assert.match(row, /title: remoteRecord \? shown :/) + assert.match(styles, /\.bt-row-owner\{flex:0 100 auto;[^}]*white-space:nowrap;overflow:hidden;text-overflow:ellipsis\}/) + assert.match(styles, /\.bt-row-remote\{[^}]*color:var\(--bt-ink-2\)\}/) + const { DICTIONARY } = await import('../src/client/i18n.js') + assert.equal(DICTIONARY.zh["Friend's Bot"], '好友的 Bot') +}) diff --git a/docs/subsystems/core-client.md b/docs/subsystems/core-client.md index bd74a3f4d..bc58de2e6 100644 --- a/docs/subsystems/core-client.md +++ b/docs/subsystems/core-client.md @@ -88,7 +88,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` (`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). +- `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`). No row wraps its title (#384): a friend's Bot row shows the Bot's name (`bt-row-title`), then its owner (`bt-row-owner`, muted, shrinks first) and the globe (at `--bt-ink-2`, readable) on one line; its second line is the preview like an own row, or *Friend's Bot* before the first message, and the full `remoteBotTitle` ("Helper · Bob's Bot") is its hover text and label. A face-room row stays on one line with an ellipsis, 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/patches/ds-bot/0003-flybot-api-client.patch b/patches/ds-bot/0003-flybot-api-client.patch index 1c5600b23..f19bbdc2a 100644 --- a/patches/ds-bot/0003-flybot-api-client.patch +++ b/patches/ds-bot/0003-flybot-api-client.patch @@ -1174,11 +1174,11 @@ diff --git a/src/client/sidebar.js b/src/client/sidebar.js import { Shimmer, workPhrase } from './cells.js' import { springEasing, SPRING_SHAPE } from './motion.js' import { hostText, t } from './i18n.js' -+import { remoteBotTitle, sharedRoomTitle, isListedRemoteChat } from '../shared/remote-label.js' ++import { cleanPeerName, ownerNameOf, remoteBotTitle, sharedRoomTitle, isListedRemoteChat } from '../shared/remote-label.js' import { showsDshRow } from './dsh-offer.js' // ------------------------------------------------------------------------- -@@ -37,56 +37,91 @@ const workKeys = text => (text === '' ? [] : text.split(',').map((key) => { +@@ -37,56 +37,101 @@ const workKeys = text => (text === '' ? [] : text.split(',').map((key) => { })) const workLine = (lead, tool, more) => h(Shimmer, null, `${lead} · ${workPhrase(tool)}…${more > 0 ? ` +${more}` : ''}`) @@ -1213,7 +1213,14 @@ diff --git a/src/client/sidebar.js b/src/client/sidebar.js } const pinned = bot.pinned === true && !tile - const label = [bot.name, main ? t('Main Bot') : '', pinned ? t('Pinned') : '', running || away ? t('Working') : '', approval ? t('Waiting for your approval') : alert ? t('Waiting for your answer') : '', unread ? t('Unread activity') : ''].filter(Boolean).join(', ') -+ const shown = bot.remote ? remoteBotTitle({ displayName: bot.name, ownerName: bot.remote.homeName || bot.remote.owner, handle: bot.remote.handle, isChief: bot.remote.isChief }) : bot.name ++ const remoteRecord = bot.remote ? { displayName: bot.name, ownerName: bot.remote.homeName || bot.remote.owner, handle: bot.remote.handle, isChief: bot.remote.isChief } : null ++ const shown = remoteRecord ? remoteBotTitle(remoteRecord) : bot.name ++ // A friend's Bot row (#384): its name, then its owner, on one line like every other row; the ++ // second line is the preview, or "Friend's Bot" before the first message. The full title is on hover. ++ const rowName = remoteRecord ? cleanPeerName(bot.name, shown) : shown ++ // A plain per-Bot identity (@alice.bot) names no owner: its handle tells two "Helper" rows apart. ++ const owner = remoteRecord ? ownerNameOf(remoteRecord) || cleanPeerName(bot.remote.handle) : '' ++ const secondLine = awayLine || preview || (remoteRecord ? t("Friend's Bot") : '') + const label = [shown, main ? t('Main Bot') : '', pinned ? t('Pinned') : '', running || away ? t('Working') : '', waiting ? t('Waiting for your approval') : alert ? t('Waiting for your answer') : '', unread ? t('Unread activity') : ''].filter(Boolean).join(', ') if (tile && wide) { return h('div', { className: 'bt-tile-wrap' }, h('button', { @@ -1230,15 +1237,20 @@ diff --git a/src/client/sidebar.js b/src/client/sidebar.js + : wide && unread && !active ? h('span', { className: 'bt-unread', 'aria-hidden': true }) : null + const row = h('button', { type: 'button', className: 'bt-row', 'aria-label': label, 'aria-current': active ? 'page' : undefined, - 'data-bot-id': id, title: wide ? undefined : bot.name, onClick: () => actions.openSession(id), onContextMenu, +- 'data-bot-id': id, title: wide ? undefined : bot.name, onClick: () => actions.openSession(id), onContextMenu, ++ 'data-bot-id': id, title: remoteRecord ? shown : wide ? undefined : bot.name, onClick: () => actions.openSession(id), onContextMenu, }, h(BotAvatar, { bot, size: 36, state, main }), pinned && !wide ? h(PinMark) : null, wide ? h('span', { className: 'bt-row-body' }, - h('span', { className: 'bt-row-name' }, h('span', { className: 'bt-row-title' }, bot.name), pinned ? h(PinMark) : null), -+ h('span', { className: 'bt-row-name' }, h('span', { className: 'bt-row-title' }, shown), bot.remote ? h(RemoteMark, { remote: bot.remote }) : null, pinned ? h(PinMark) : null), - awayLine ? h('span', { className: 'bt-row-preview' }, awayLine) : preview ? h('span', { className: 'bt-row-preview' }, preview) : null) : null, +- awayLine ? h('span', { className: 'bt-row-preview' }, awayLine) : preview ? h('span', { className: 'bt-row-preview' }, preview) : null) : null, - wide && unread && !active ? h('span', { className: 'bt-unread', 'aria-hidden': true }) : null) ++ h('span', { className: 'bt-row-name' }, ++ h('span', { className: 'bt-row-title' }, rowName), ++ owner ? h('span', { className: 'bt-row-owner' }, owner) : null, ++ bot.remote ? h(RemoteMark, { remote: bot.remote }) : null, pinned ? h(PinMark) : null), ++ secondLine ? h('span', { className: 'bt-row-preview' }, secondLine) : null) : null, + unreadMark) + // Hover/focus gear on every wide row (#276/#173): own Bots and friends' Bots alike. + const settingsLabel = bot.remote ? t('Bot settings') : t('Edit Bot') @@ -1250,7 +1262,7 @@ diff --git a/src/client/sidebar.js b/src/client/sidebar.js + return row +} + -+// A friend's Bot: a small globe after the name, its handle on hover. ++// A friend's Bot: a globe after its owner's name, its handle on hover. +function RemoteMark({ remote }) { + return h('span', { className: 'bt-row-remote', title: remote.gone ? `${remote.handle} · ${t('no longer shared')}` : remote.handle, 'aria-label': t('Friend\'s Bot') }, + h('svg', { width: 11, height: 11, viewBox: '0 0 24 24', fill: 'none', stroke: 'currentColor', strokeWidth: 2, strokeLinecap: 'round', 'aria-hidden': true }, @@ -1587,13 +1599,12 @@ diff --git a/src/client/styles.js b/src/client/styles.js .bt-round{width:36px;height:36px;border-radius:50%;border:1px solid var(--bt-line);background:var(--bt-main);color:var(--bt-ink);display:inline-flex;align-items:center;justify-content:center;cursor:pointer;padding:0;transition:background-color .12s ease,transform .12s ease} .bt-round:hover{background:var(--bt-hover)} .bt-round:active{transform:scale(.94)} -@@ -132,21 +132,25 @@ body[data-bt-motion=quiet] .bt-mark[data-poke]{cursor:default} +@@ -132,21 +132,24 @@ 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: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-row-owner{flex:0 100 auto;min-width:24px;font-size:13px;color:var(--bt-ink-2);white-space:nowrap;overflow:hidden;text-overflow:ellipsis} ++.bt-row-remote{flex:none;display:inline-flex;align-items:center;color:var(--bt-ink-2)} .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} .bt-unread{width:8px;height:8px;border-radius:50%;background:#1084fe;flex:none;margin-right:2px;animation:bt-fade .13s cubic-bezier(.22,1,.36,1)} diff --git a/patches/ds-bot/0008-client-bundle.patch b/patches/ds-bot/0008-client-bundle.patch index f9b05403b..e564cb983 100644 --- a/patches/ds-bot/0008-client-bundle.patch +++ b/patches/ds-bot/0008-client-bundle.patch @@ -1571,13 +1571,12 @@ diff --git a/client.js b/client.js .bt-round{width:36px;height:36px;border-radius:50%;border:1px solid var(--bt-line);background:var(--bt-main);color:var(--bt-ink);display:inline-flex;align-items:center;justify-content:center;cursor:pointer;padding:0;transition:background-color .12s ease,transform .12s ease} .bt-round:hover{background:var(--bt-hover)} .bt-round:active{transform:scale(.94)} -@@ -1950,21 +1950,25 @@ body[data-bt-motion=quiet] .bt-mark[data-poke]{cursor:default} +@@ -1950,21 +1950,24 @@ 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: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-row-owner{flex:0 100 auto;min-width:24px;font-size:13px;color:var(--bt-ink-2);white-space:nowrap;overflow:hidden;text-overflow:ellipsis} ++.bt-row-remote{flex:none;display:inline-flex;align-items:center;color:var(--bt-ink-2)} .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} .bt-unread{width:8px;height:8px;border-radius:50%;background:#1084fe;flex:none;margin-right:2px;animation:bt-fade .13s cubic-bezier(.22,1,.36,1)} @@ -3522,7 +3521,8 @@ diff --git a/client.js b/client.js + if (reply.kind === "notice" && room2?.origin === "face-to-face" && String(reply.text ?? "").trim() === WAITING_OTHERS_NOTICE) return null; + if (reply.kind === "notice" || reply.kind === "joined" || reply.kind === "left" || reply.kind === "started") { + return (0, import_react7.createElement)("div", { key: `notice-${index}`, className: "bt-event", "data-flybot": "face-notice" }, remoteMessageText(reply.text)); -+ } + } +- return (0, import_react6.createElement)(GroupMessage, { key: `${reply.botId}-${index}`, bot, text: reply.text, room: room2, roster, actions, msgKey: callId ? `${sessionId}:${callId}:${index}` : void 0, useUi }); + const remote = reply.remote || reply.handle && !roster.byId[reply.botId]; + if (remote) { + return (0, import_react7.createElement)(RemoteMessage, { @@ -3535,8 +3535,7 @@ diff --git a/client.js b/client.js + msgKey: callId ? `${sessionId}:${callId}:${index}` : void 0, + useUi + }); - } -- return (0, import_react6.createElement)(GroupMessage, { key: `${reply.botId}-${index}`, bot, text: reply.text, room: room2, roster, actions, msgKey: callId ? `${sessionId}:${callId}:${index}` : void 0, useUi }); ++ } + return (0, import_react7.createElement)(GroupMessage, { key: `${reply.botId}-${index}`, bot, text: reply.text, room: room2, roster, actions, msgKey: callId ? `${sessionId}:${callId}:${index}` : void 0, useUi }); }), pending.map((id) => { @@ -3816,7 +3815,7 @@ diff --git a/client.js b/client.js ) ) ), host); -@@ -4732,48 +4732,50 @@ function showsDshRow(dsh, app) { +@@ -4732,48 +4732,54 @@ function showsDshRow(dsh, app) { } // src/client/sidebar.js @@ -3870,7 +3869,11 @@ diff --git a/client.js b/client.js }; const pinned = bot.pinned === true && !tile; - const label = [bot.name, main ? t("Main Bot") : "", pinned ? t("Pinned") : "", running || away ? t("Working") : "", approval ? t("Waiting for your approval") : alert ? t("Waiting for your answer") : "", unread ? t("Unread activity") : ""].filter(Boolean).join(", "); -+ const shown = bot.remote ? remoteBotTitle({ displayName: bot.name, ownerName: bot.remote.homeName || bot.remote.owner, handle: bot.remote.handle, isChief: bot.remote.isChief }) : bot.name; ++ const remoteRecord = bot.remote ? { displayName: bot.name, ownerName: bot.remote.homeName || bot.remote.owner, handle: bot.remote.handle, isChief: bot.remote.isChief } : null; ++ const shown = remoteRecord ? remoteBotTitle(remoteRecord) : bot.name; ++ const rowName = remoteRecord ? cleanPeerName(bot.name, shown) : shown; ++ const owner = remoteRecord ? ownerNameOf(remoteRecord) || cleanPeerName(bot.remote.handle) : ""; ++ const secondLine = awayLine || preview || (remoteRecord ? t("Friend's Bot") : ""); + const label = [shown, main ? t("Main Bot") : "", pinned ? t("Pinned") : "", running || away ? t("Working") : "", waiting ? t("Waiting for your approval") : alert ? t("Waiting for your answer") : "", unread ? t("Unread activity") : ""].filter(Boolean).join(", "); if (tile && wide) { - return (0, import_react8.createElement)("div", { className: "bt-tile-wrap" }, (0, import_react8.createElement)( @@ -3896,7 +3899,12 @@ diff --git a/client.js b/client.js "button", { type: "button", -@@ -4802,21 +4802,66 @@ function BotRow({ bot, roster, actions, useSessions, useSessionStatus, useSessio +@@ -4798,25 +4798,77 @@ function BotRow({ bot, roster, actions, useSessions, useSessionStatus, useSessio + "aria-label": label, + "aria-current": active ? "page" : void 0, + "data-bot-id": id, +- title: wide ? void 0 : bot.name, ++ title: remoteRecord ? shown : wide ? void 0 : bot.name, onClick: () => actions.openSession(id), onContextMenu }, @@ -3910,8 +3918,15 @@ diff --git a/client.js b/client.js { className: "bt-row-body" }, - (0, import_react8.createElement)("span", { className: "bt-row-name" }, (0, import_react8.createElement)("span", { className: "bt-row-title" }, bot.name), pinned ? (0, import_react8.createElement)(PinMark) : null), - awayLine ? (0, import_react8.createElement)("span", { className: "bt-row-preview" }, awayLine) : preview ? (0, import_react8.createElement)("span", { className: "bt-row-preview" }, preview) : null -+ (0, import_react9.createElement)("span", { className: "bt-row-name" }, (0, import_react9.createElement)("span", { className: "bt-row-title" }, shown), bot.remote ? (0, import_react9.createElement)(RemoteMark, { remote: bot.remote }) : null, pinned ? (0, import_react9.createElement)(PinMark) : null), -+ awayLine ? (0, import_react9.createElement)("span", { className: "bt-row-preview" }, awayLine) : preview ? (0, import_react9.createElement)("span", { className: "bt-row-preview" }, preview) : null ++ (0, import_react9.createElement)( ++ "span", ++ { className: "bt-row-name" }, ++ (0, import_react9.createElement)("span", { className: "bt-row-title" }, rowName), ++ owner ? (0, import_react9.createElement)("span", { className: "bt-row-owner" }, owner) : null, ++ bot.remote ? (0, import_react9.createElement)(RemoteMark, { remote: bot.remote }) : null, ++ pinned ? (0, import_react9.createElement)(PinMark) : null ++ ), ++ secondLine ? (0, import_react9.createElement)("span", { className: "bt-row-preview" }, secondLine) : null ) : null, - wide && unread && !active ? (0, import_react8.createElement)("span", { className: "bt-unread", "aria-hidden": true }) : null + unreadMark diff --git a/patches/ds-bot/files/test/flybot-client.test.js b/patches/ds-bot/files/test/flybot-client.test.js index 9b7e86370..ea4ec1de9 100644 --- a/patches/ds-bot/files/test/flybot-client.test.js +++ b/patches/ds-bot/files/test/flybot-client.test.js @@ -177,10 +177,26 @@ test('a face-room sidebar row stays on one line and shows its full title on hove 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/) + // No sidebar row wraps its title (#384 dropped the friend's Bot two-line clamp too). + assert.equal(styles.split('\n').some(line => line.includes('-webkit-line-clamp:2') && line.includes('.bt-row-')), false) assert.match(styles, /\.bt-row-title\{min-width:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis\}/) }) + +test("a friend's Bot sidebar row is its name and owner on one line, with a second line like own rows (#384)", 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 row = sidebar.slice(sidebar.indexOf('function BotRow('), sidebar.indexOf('function RemoteMark(')) + assert.match(row, /const rowName = remoteRecord \? cleanPeerName\(bot\.name, shown\) : shown/) + assert.match(row, /h\('span', \{ className: 'bt-row-title' \}, rowName\)/) + assert.match(row, /owner \? h\('span', \{ className: 'bt-row-owner' \}, owner\) : null/) + assert.match(row, /const owner = remoteRecord \? ownerNameOf\(remoteRecord\) \|\| cleanPeerName\(bot\.remote\.handle\) : ''/) + // Before the first message the second line still says what the row is. + assert.match(row, /const secondLine = awayLine \|\| preview \|\| \(remoteRecord \? t\("Friend's Bot"\) : ''\)/) + // The full "Helper · Bob's Bot" title is on hover and in the label. + assert.match(row, /title: remoteRecord \? shown :/) + assert.match(styles, /\.bt-row-owner\{flex:0 100 auto;[^}]*white-space:nowrap;overflow:hidden;text-overflow:ellipsis\}/) + assert.match(styles, /\.bt-row-remote\{[^}]*color:var\(--bt-ink-2\)\}/) + const { DICTIONARY } = await import('../src/client/i18n.js') + assert.equal(DICTIONARY.zh["Friend's Bot"], '好友的 Bot') +})