Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changelog.d/384-friend-row-one-line.fixed.md
Original file line number Diff line number Diff line change
@@ -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))
24 changes: 17 additions & 7 deletions core/client.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)}
Expand Down Expand Up @@ -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)(
Expand Down Expand Up @@ -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
},
Expand All @@ -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
);
Expand Down
22 changes: 16 additions & 6 deletions core/src/client/sidebar.js
Original file line number Diff line number Diff line change
Expand Up @@ -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'

// -------------------------------------------------------------------------
Expand Down Expand Up @@ -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', {
Expand All @@ -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')
Expand All @@ -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 },
Expand Down
5 changes: 2 additions & 3 deletions core/src/client/styles.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)}
Expand Down
26 changes: 21 additions & 5 deletions core/test/flybot-client.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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')
})
2 changes: 1 addition & 1 deletion docs/subsystems/core-client.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
Loading
Loading