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/173-social-ui-polish.changed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Sidebar friend rows use hover-only settings gear, two-line titles with the Friend's Bot marker visible, and Past rooms in sentence case. The + menu merges Add friend and Accept invite, uses Add Bot consistently, and Escape closes Add friend and Face-to-face dialogs. Bot Sharing settings get a section header; friend empty chats show a one-line hint; the tab title follows the active chat; Chief greets in English when the UI is English. ([#173](https://github.com/ThinkFlowLab/FlyBot/issues/173))
57 changes: 53 additions & 4 deletions core/client.js
Original file line number Diff line number Diff line change
Expand Up @@ -316,6 +316,7 @@ var ZH = {
"Kept above the other chats.": "排在其他对话上面。",
"Search still finds it.": "仍然可以搜索到。",
Manage: "管理",
"Send a message. Your friend approves unless Auto-send is on.": "发送消息。除非已开启自动发送,否则好友需要批准。",
// Group chat settings
"Leave it empty to name the group after its members.": "留空就按成员命名。",
"Every member reads it before speaking in this group.": "每位成员在这个群里发言前都会读到。",
Expand Down Expand Up @@ -441,6 +442,7 @@ var ZH = {
// stays in English.
"Add DSH Agent": "添加 DSH Agent",
"Add Agent": "添加 Agent",
"Add Bot": "添加 Bot",
"Remove DSH Agent": "移除 DSH Agent",
"New session": "新会话",
Archive: "归档",
Expand Down Expand Up @@ -2715,6 +2717,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-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}
Expand Down Expand Up @@ -3410,6 +3414,7 @@ header[class*="_header"] .bt-head{flex:1 1 auto;width:100%}
the shell's new-session hero: no headline, no workspace or mode picker, and the
composer stays docked at the bottom. */
[data-content-phase=hero] [class*="_scrollBody"]{justify-content:flex-end!important}
body[data-bt-remote-chat=empty][data-content-phase=hero] [class*="_scrollBody"]::before{content:var(--bt-remote-empty);display:block;text-align:center;color:var(--bt-ink-3);font-size:13px;line-height:18px;padding:16px 12px 8px;max-width:var(--dsh-chat-content-width,min(960px,100%));margin:0 auto;white-space:pre-wrap}
[data-content-phase=hero] [class*="_composerHero"]{align-self:stretch!important;width:auto!important;padding-bottom:0!important}
[data-content-phase=hero] [class*="_composerHero"]>[class$="_root"],[data-content-phase=hero] [class*="_heroWorkspaceRow"]{display:none!important}
[data-content-phase=hero] [class*="_composerHero"] [class*="_input"]{min-height:0!important}
Expand Down Expand Up @@ -3643,6 +3648,21 @@ function brandTitle(title) {
if (/\bDS Bot\b/.test(text)) return text.replace(/\bDS Bot\b/g, "FlyBot");
return text;
}
function sessionTabLabel(roster, sessionId) {
if (!roster || !sessionId) return "";
const bot = roster.byId?.[sessionId];
const room2 = roster.roomsById?.[sessionId];
if (bot?.remote) {
return remoteBotTitle({
displayName: bot.name,
ownerName: bot.remote.homeName || bot.remote.owner,
handle: bot.remote.handle,
isChief: bot.remote.isChief
});
}
if (room2) return room2.displayName || (room2.sharedFrom ? sharedRoomTitle(room2.sharedFrom.inviterName) : room2.name) || "";
return bot?.name || "";
}
function installBrandChrome() {
if (typeof document === "undefined") return () => {
};
Expand Down Expand Up @@ -6354,8 +6374,9 @@ var SIDEBAR_CSS = `

.bt-row-wrap{display:flex;align-items:center;width:100%;min-width:0}
.bt-row-wrap>.bt-row{flex:1 1 auto;min-width:0}
.bt-row-settings{flex:none;display:inline-flex;align-items:center;justify-content:center;width:28px;height:28px;margin:-2px 8px -2px 2px;padding:0;border:0;border-radius:8px;background:transparent;color:var(--bt-ink-3);cursor:pointer;opacity:.85;transition:background-color .12s ease,color .12s ease,opacity .12s ease}
.bt-row-settings:hover,.bt-row-wrap:focus-within .bt-row-settings{opacity:1;color:var(--bt-ink-2);background:var(--bt-hover)}
.bt-row-settings{flex:none;display:inline-flex;align-items:center;justify-content:center;width:28px;height:28px;margin:-2px 8px -2px 2px;padding:0;border:0;border-radius:8px;background:transparent;color:var(--bt-ink-3);cursor:pointer;opacity:0;pointer-events:none;transition:background-color .12s ease,color .12s ease,opacity .12s ease}
.bt-row-wrap:hover .bt-row-settings,.bt-row-wrap:focus-within .bt-row-settings{opacity:1;pointer-events:auto;color:var(--bt-ink-2);background:var(--bt-hover)}
.bt-row-settings:hover{opacity:1;color:var(--bt-ink-2);background:var(--bt-hover)}
.bt-row-settings svg{width:15px;height:15px}
`;

Expand Down Expand Up @@ -6420,6 +6441,21 @@ function HeaderPill({ sessionId, useRoster, useSessions, useSessionStatus, useSe
const notice = useMemoryNotice(memoryIds, useNews);
const changed = useNews((map) => latestMemoryChange(map, memoryIds));
const composerLabel = name ? t("Message {name}", { name }) : t("Message");
const blank3 = useSessions((list) => list.byId[sessionId]?.blank === true);
const remoteEmptyHint = t("Send a message. Your friend approves unless Auto-send is on.");
(0, import_react10.useEffect)(() => {
if (bot?.remote && blank3) {
document.body.dataset.btRemoteChat = "empty";
document.body.style.setProperty("--bt-remote-empty", JSON.stringify(remoteEmptyHint));
} else {
delete document.body.dataset.btRemoteChat;
document.body.style.removeProperty("--bt-remote-empty");
}
return () => {
delete document.body.dataset.btRemoteChat;
document.body.style.removeProperty("--bt-remote-empty");
};
}, [bot?.remote, blank3, remoteEmptyHint]);
(0, import_react10.useEffect)(() => {
document.body.style.setProperty("--bt-placeholder", JSON.stringify(composerLabel));
const input = document.querySelector("[data-composer-input]");
Expand Down Expand Up @@ -9042,7 +9078,7 @@ function BotSettingsForm({ bot, roster, actions, run }) {
(0, import_react19.createElement)(FlyBotDetails, { bot, roster, actions }),
(0, import_react19.createElement)(
Card,
{ title: t("Manage") },
{ title: t("More") },
(0, import_react19.createElement)(
"button",
{ type: "button", className: "bt-set-row bt-bs-row", onClick: duplicate },
Expand Down Expand Up @@ -9403,7 +9439,7 @@ function NewChat({ mode, roster, actions }) {
});
}
if ((roster.agents ?? []).length === 0) {
options.push({ key: "agent", icon: (0, import_react21.createElement)(import_dsh_client_ui_primitives4.FishLogo, { size: 16 }), label: t("Add Agent"), run: () => run(async () => {
options.push({ key: "agent", icon: (0, import_react21.createElement)(import_dsh_client_ui_primitives4.FishLogo, { size: 16 }), label: t("Add Bot"), run: () => run(async () => {
await actions.addAgent();
close();
}) });
Expand Down Expand Up @@ -14496,6 +14532,19 @@ function apply(ctx) {
}
ctx.effect(() => installLocale(ctx.get("locale")), "ds-bot: interface copy");
ctx.effect(() => installBrandChrome(), "ds-bot: brand chrome");
ctx.effect(() => {
const sessions = ctx.sessions.list;
const sync = () => {
if (typeof document === "undefined") return;
const row = Object.values(sessions.getSnapshot().byId ?? {}).find((session) => (session?.retainedBy?.mainView ?? 0) > 0);
const label = sessionTabLabel(roster.getSnapshot(), row?.id);
if (!label) return;
const next = brandTitle(`${label} — FlyBot`);
if (document.title !== next) document.title = next;
};
sync();
return sessions.subscribe(sync);
}, "ds-bot: tab title");
const roster = createSource(EMPTY_ROSTER);
migrateStorage();
const UNREAD_KEY = STORAGE.unread;
Expand Down
2 changes: 1 addition & 1 deletion core/src/client/bot-settings.js
Original file line number Diff line number Diff line change
Expand Up @@ -249,7 +249,7 @@ export function BotSettingsForm({ bot, roster, actions, run }) {
h(SwitchRow, { label: t('Pin'), hint: t('Kept above the other chats.'), on: bot.pinned === true, onChange: pinned => run(actions.setFlags(bot.id, { pinned })) }),
isMainBot ? null : h(SwitchRow, { label: t('Hide from sidebar'), hint: t('Search still finds it.'), on: bot.hidden === true, onChange: hidden => run(actions.setFlags(bot.id, { hidden })) })),
h(FlyBotDetails, { bot, roster, actions }),
h(Card, { title: t('Manage') },
h(Card, { title: t('More') },
h('button', { type: 'button', className: 'bt-set-row bt-bs-row', onClick: duplicate },
h('span', { className: 'bt-set-copy' }, h('span', { className: 'bt-bs-label' }, t('Duplicate')), h('span', { className: 'bt-set-hint' }, t('A copy with the same job and model, and fresh memory.'))),
h('span', { className: 'bt-chevron' }, h(ChevronRightIcon))),
Expand Down
18 changes: 18 additions & 0 deletions core/src/client/brand.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
// FlyBot product chrome: the mark from docs/images/logo.svg, the tab title, and the favicon.
// Paths stay here so tests can import them without React.

import { remoteBotTitle, sharedRoomTitle } from '../shared/remote-label.js'

export const FLYBOT_WING = 'M150 238 C 96 200, 40 196, 22 214 C 8 230, 40 262, 150 300 Z'
export const FLYBOT_WING_LOW = 'M150 318 C 104 300, 62 306, 54 322 C 48 338, 84 352, 150 352 Z'
export const FLYBOT_BODY = 'M196 132 H316 A80 80 0 0 1 396 212 V300 A80 80 0 0 1 316 380 H250 L188 432 V374 A80 80 0 0 1 116 300 V212 A80 80 0 0 1 196 132 Z'
Expand All @@ -24,6 +26,22 @@ export function brandTitle(title) {
return text
}

export function sessionTabLabel(roster, sessionId) {
if (!roster || !sessionId) return ''
const bot = roster.byId?.[sessionId]
const room = roster.roomsById?.[sessionId]
if (bot?.remote) {
return remoteBotTitle({
displayName: bot.name,
ownerName: bot.remote.homeName || bot.remote.owner,
handle: bot.remote.handle,
isChief: bot.remote.isChief,
})
}
if (room) return room.displayName || (room.sharedFrom ? sharedRoomTitle(room.sharedFrom.inviterName) : room.name) || ''
return bot?.name || ''
}

export function installBrandChrome() {
if (typeof document === 'undefined') return () => {}
const href = `data:image/svg+xml,${encodeURIComponent(flybotMarkSvg())}`
Expand Down
15 changes: 15 additions & 0 deletions core/src/client/header.js
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,21 @@ export function HeaderPill({ sessionId, useRoster, useSessions, useSessionStatus
const changed = useNews(map => latestMemoryChange(map, memoryIds))
// The composer placeholder is not localizable per Session, so CSS reads it from here.
const composerLabel = name ? t('Message {name}', { name }) : t('Message')
const blank = useSessions(list => list.byId[sessionId]?.blank === true)
const remoteEmptyHint = t("Send a message. Your friend approves unless Auto-send is on.")
useEffect(() => {
if (bot?.remote && blank) {
document.body.dataset.btRemoteChat = 'empty'
document.body.style.setProperty('--bt-remote-empty', JSON.stringify(remoteEmptyHint))
} else {
delete document.body.dataset.btRemoteChat
document.body.style.removeProperty('--bt-remote-empty')
}
return () => {
delete document.body.dataset.btRemoteChat
document.body.style.removeProperty('--bt-remote-empty')
}
}, [bot?.remote, blank, remoteEmptyHint])
useEffect(() => {
document.body.style.setProperty('--bt-placeholder', JSON.stringify(composerLabel))
const input = document.querySelector('[data-composer-input]')
Expand Down
2 changes: 2 additions & 0 deletions core/src/client/i18n.js
Original file line number Diff line number Diff line change
Expand Up @@ -284,6 +284,7 @@ const ZH = {
'Kept above the other chats.': '排在其他对话上面。',
'Search still finds it.': '仍然可以搜索到。',
Manage: '管理',
"Send a message. Your friend approves unless Auto-send is on.": '发送消息。除非已开启自动发送,否则好友需要批准。',

// Group chat settings
'Leave it empty to name the group after its members.': '留空就按成员命名。',
Expand Down Expand Up @@ -380,6 +381,7 @@ const ZH = {
// stays in English.
'Add DSH Agent': '添加 DSH Agent',
'Add Agent': '添加 Agent',
'Add Bot': '添加 Bot',
'Remove DSH Agent': '移除 DSH Agent',
'New session': '新会话',
Archive: '归档',
Expand Down
15 changes: 14 additions & 1 deletion core/src/client/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { countCards, createSource, EMPTY_ROSTER, indexRoster, isAgentSession } f
import { themeCss, shellTokens, registry, registerShape, registerTheme, allThemes } from './themes.js'
import { MENTION_ORIGIN, currentPart } from './text.js'
import { SHELL_CSS, CONVERSATION_CSS, installStyles, installCompiledStyles } from './styles.js'
import { installBrandChrome } from './brand.js'
import { brandTitle, installBrandChrome, sessionTabLabel } from './brand.js'
import { lookOf } from './mark.js'
import { TeamSidebar, ConnectPlugins, YouAvatar, AccountLauncher, FooterAccount, BrandMark, BrandName, BotReturn, SIDEBAR_CSS, RETURN_CSS, holdShellShortcuts } from './sidebar.js'
import { HeaderPill } from './header.js'
Expand Down Expand Up @@ -50,6 +50,19 @@ export function apply(ctx) {
} catch {}
ctx.effect(() => installLocale(ctx.get('locale')), 'ds-bot: interface copy')
ctx.effect(() => installBrandChrome(), 'ds-bot: brand chrome')
ctx.effect(() => {
const sessions = ctx.sessions.list
const sync = () => {
if (typeof document === 'undefined') return
const row = Object.values(sessions.getSnapshot().byId ?? {}).find(session => (session?.retainedBy?.mainView ?? 0) > 0)
const label = sessionTabLabel(roster.getSnapshot(), row?.id)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Declare the roster before registering the tab-title effect

Under the synchronous ctx.effect contract used by the client plugin, this effect immediately calls sync(), which reads roster even though that const is not initialized until line 66. Normal FlyBot client startup therefore throws a TDZ ReferenceError here and aborts the remainder of apply(); create the roster before registering this effect.

Useful? React with 👍 / 👎.

if (!label) return
const next = brandTitle(`${label} — FlyBot`)
if (document.title !== next) document.title = next
}
sync()
return sessions.subscribe(sync)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Subscribe the tab title to roster changes

The tab label is derived from both the session list and roster, but this cleanup subscribes only to sessions. When pull() refreshes an active remote Bot's name/owner or shared-room display metadata without a navigation/session-list event, the document title keeps the old label until some later session change; subscribe sync to both sources and dispose both subscriptions.

Useful? React with 👍 / 👎.

}, 'ds-bot: tab title')
const roster = createSource(EMPTY_ROSTER)
migrateStorage()
const UNREAD_KEY = STORAGE.unread
Expand Down
2 changes: 1 addition & 1 deletion core/src/client/new-chat.js
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,7 @@ export function NewChat({ mode, roster, actions }) {
}
// A team holds at most one Agent; the entry exists only while there is none.
if ((roster.agents ?? []).length === 0) {
options.push({ key: 'agent', icon: h(FishLogo, { size: 16 }), label: t('Add Agent'), run: () => run(async () => { await actions.addAgent(); close() }) })
options.push({ key: 'agent', icon: h(FishLogo, { size: 16 }), label: t('Add Bot'), run: () => run(async () => { await actions.addAgent(); close() }) })
}
}
// The + menu is a short action list. Individual Bots (Chief and the rest) belong on
Expand Down
5 changes: 3 additions & 2 deletions core/src/client/sidebar.js
Original file line number Diff line number Diff line change
Expand Up @@ -712,7 +712,8 @@ export const SIDEBAR_CSS = `

.bt-row-wrap{display:flex;align-items:center;width:100%;min-width:0}
.bt-row-wrap>.bt-row{flex:1 1 auto;min-width:0}
.bt-row-settings{flex:none;display:inline-flex;align-items:center;justify-content:center;width:28px;height:28px;margin:-2px 8px -2px 2px;padding:0;border:0;border-radius:8px;background:transparent;color:var(--bt-ink-3);cursor:pointer;opacity:.85;transition:background-color .12s ease,color .12s ease,opacity .12s ease}
.bt-row-settings:hover,.bt-row-wrap:focus-within .bt-row-settings{opacity:1;color:var(--bt-ink-2);background:var(--bt-hover)}
.bt-row-settings{flex:none;display:inline-flex;align-items:center;justify-content:center;width:28px;height:28px;margin:-2px 8px -2px 2px;padding:0;border:0;border-radius:8px;background:transparent;color:var(--bt-ink-3);cursor:pointer;opacity:0;pointer-events:none;transition:background-color .12s ease,color .12s ease,opacity .12s ease}
.bt-row-wrap:hover .bt-row-settings,.bt-row-wrap:focus-within .bt-row-settings{opacity:1;pointer-events:auto;color:var(--bt-ink-2);background:var(--bt-hover)}
.bt-row-settings:hover{opacity:1;color:var(--bt-ink-2);background:var(--bt-hover)}
.bt-row-settings svg{width:15px;height:15px}
`
3 changes: 3 additions & 0 deletions core/src/client/styles.js
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +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-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}
Expand Down Expand Up @@ -844,6 +846,7 @@ header[class*="_header"] .bt-head{flex:1 1 auto;width:100%}
the shell's new-session hero: no headline, no workspace or mode picker, and the
composer stays docked at the bottom. */
[data-content-phase=hero] [class*="_scrollBody"]{justify-content:flex-end!important}
body[data-bt-remote-chat=empty][data-content-phase=hero] [class*="_scrollBody"]::before{content:var(--bt-remote-empty);display:block;text-align:center;color:var(--bt-ink-3);font-size:13px;line-height:18px;padding:16px 12px 8px;max-width:var(--dsh-chat-content-width,min(960px,100%));margin:0 auto;white-space:pre-wrap}
[data-content-phase=hero] [class*="_composerHero"]{align-self:stretch!important;width:auto!important;padding-bottom:0!important}
[data-content-phase=hero] [class*="_composerHero"]>[class$="_root"],[data-content-phase=hero] [class*="_heroWorkspaceRow"]{display:none!important}
[data-content-phase=hero] [class*="_composerHero"] [class*="_input"]{min-height:0!important}
Expand Down
2 changes: 1 addition & 1 deletion core/src/host/channel.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ export function install(rt) {

// The DSH interface language of the last browser that asked for the team.
let uiLocale
rt.uiLanguage = () => (uiLocale === undefined ? undefined : LANGUAGES[uiLocale.split('-')[0].toLowerCase()] ?? uiLocale)
rt.uiLanguage = () => (uiLocale === undefined ? 'English' : LANGUAGES[uiLocale.split('-')[0].toLowerCase()] ?? uiLocale)

const ok = value => ({ ok: true, value })
const fail = (message, code = 'bot/error') => ({ ok: false, error: { code, message, details: {} } })
Expand Down
2 changes: 1 addition & 1 deletion core/src/host/roster.js
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ export function install(rt) {
// The kickoff arrives as a user message, so it outranks the Bot's instructions
// unless it gives way to them, and it names no tool those instructions may rule out.
const kickoff = main
? `You were just set up as the user's Main Bot. Greet the user in one or two short messages: say you will run their Bot team and that they can come to you for anything that needs a decision. Then ask in one short question what they want the team to help with first. Write in ${language ?? 'the user\'s language if known; otherwise Chinese'}.`
? `You were just set up as the user's Main Bot. Greet the user in one or two short messages: say you will run their Bot team and that they can come to you for anything that needs a decision. Then ask in one short question what they want the team to help with first. Write in ${language ?? 'English'}.`
: original
? `The user just made you as a copy of ${original.name}: same job, fresh memory. Greet the user in one short message, then ask in one short question what this copy should focus on.`
: creator
Expand Down
Loading
Loading