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/124-face-human-sync.fixed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Typing in a face-to-face room posts a human line to Bot Social (`FLYBOT-FACE-HUMAN`) and shows it on the other FlyBot instance. Send no longer goes through the local Bot prompt, so it does not bump turns or stay local-only. ([#124](https://github.com/ThinkFlowLab/FlyBot/issues/124))
1 change: 1 addition & 0 deletions changelog.d/125-face-waiting-leave.fixed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Face-to-face waiting is a tile grid (avatar above name and owner) with Leave and Enter chat on one row. Leave after joining has ended still moves the room to Past rooms. Past rows are unique with Open only; live rows in Friends' Bots are click-to-open with Leave, not Open/Start/Leave stacked. Join copy uses the owner and Bot name. ([#125](https://github.com/ThinkFlowLab/FlyBot/issues/125))
1 change: 1 addition & 0 deletions changelog.d/126-social-usability.changed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
The sidebar + menu is a short list of actions (not a New chat composer). Add friend has one-click Copy invite link. Friends' Bots keeps the N to approve badge and shows holds in that chat with readable names, not raw bot ids. ([#126](https://github.com/ThinkFlowLab/FlyBot/issues/126))
37 changes: 29 additions & 8 deletions core/client.js
Original file line number Diff line number Diff line change
Expand Up @@ -431,6 +431,7 @@ var ZH = {
// The DSH Agent's own row and Session list. "DSH Agent" is a product name and
// stays in English.
"Add DSH Agent": "添加 DSH Agent",
"Add Agent": "添加 Agent",
"Remove DSH Agent": "移除 DSH Agent",
"New session": "新会话",
Archive: "归档",
Expand Down Expand Up @@ -2826,6 +2827,7 @@ button.bt-q-opt:active{background:var(--bt-active)}
@media (prefers-reduced-motion:reduce){.bt-q{animation:bt-fade .12s ease-out backwards}.bt-q-settled{animation:none}.bt-q-submit{animation:none}}

.bt-newchat{flex:1;display:flex;flex-direction:column;min-width:0;position:relative;animation:bt-veil-in .18s ease-out}
.bt-newchat[data-menu] .bt-drop{position:relative;top:0;left:0;width:auto;max-height:none;margin:8px 12px;box-shadow:none}
.bt-to{display:flex;align-items:center;flex-wrap:wrap;gap:6px;min-height:48px;padding:6px 12px;border-bottom:1px solid var(--bt-line);font-size:14px;box-sizing:border-box}
.bt-to-label{color:var(--bt-ink-2)}
.bt-to input{flex:1;min-width:120px;border:0;outline:none;background:none;color:var(--bt-ink);font-size:14px;height:26px}
Expand Down Expand Up @@ -9160,6 +9162,7 @@ function NewChat({ mode, roster, actions }) {
const [query, setQuery] = (0, import_react21.useState)("");
const [picked, setPicked] = (0, import_react21.useState)([]);
const [group, setGroup] = (0, import_react21.useState)(mode === "group");
const [compose, setCompose] = (0, import_react21.useState)(mode !== "new");
const [admin, setAdmin] = (0, import_react21.useState)(null);
const [message, setMessage] = (0, import_react21.useState)("");
const [busy, setBusy] = (0, import_react21.useState)(false);
Expand Down Expand Up @@ -9262,11 +9265,20 @@ function NewChat({ mode, roster, actions }) {
actions.openSession(room2.id);
close();
});
const menuOnly = mode === "new" && !compose && !group && picked.length === 0 && wanted === "";
const options = [];
if (!group && picked.length === 0 && mode !== "search" && wanted === "") {
if (menuOnly) {
options.push({ key: "chat", icon: (0, import_react21.createElement)(PlusIcon), label: t("New chat"), run: () => {
setCompose(true);
setCursor(0);
setTimeout(() => inputRef.current?.focus());
} });
}
options.push({ key: "new", icon: (0, import_react21.createElement)(PlusIcon), label: t("Create new Bot"), run: createBot });
options.push({ key: "group", icon: (0, import_react21.createElement)(PeopleIcon), label: t("Create group chat"), run: () => {
setGroup(true);
setCompose(true);
setCursor(0);
inputRef.current?.focus();
} });
Expand All @@ -9280,7 +9292,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 DSH Agent"), run: () => run(async () => {
options.push({ key: "agent", icon: (0, import_react21.createElement)(import_dsh_client_ui_primitives4.FishLogo, { size: 16 }), label: t("Add Agent"), run: () => run(async () => {
await actions.addAgent();
close();
}) });
Expand Down Expand Up @@ -9327,8 +9339,13 @@ ${listed}`, { axis: "y", ...QUICK_EDGES });
{ className: "bt-pane", style: { left: paneLeft } },
(0, import_react21.createElement)(
"div",
{ className: "bt-newchat", role: "dialog", "aria-label": mode === "search" ? t("Search") : t("New chat") },
(0, import_react21.createElement)(
{ className: "bt-newchat", role: "dialog", "aria-label": mode === "search" ? t("Search") : t("New chat"), "data-menu": menuOnly || void 0 },
menuOnly ? (0, import_react21.createElement)(
"div",
{ className: "bt-to" },
(0, import_react21.createElement)("span", { className: "bt-to-label" }, t("New…")),
(0, import_react21.createElement)("button", { type: "button", className: "bt-x", "aria-label": t("Close new chat"), onClick: close }, "×")
) : (0, import_react21.createElement)(
"div",
{ className: "bt-to" },
(0, import_react21.createElement)("span", { className: "bt-to-label" }, t("To:")),
Expand Down Expand Up @@ -9402,9 +9419,9 @@ ${listed}`, { axis: "y", ...QUICK_EDGES });
option.hint && index === active ? (0, import_react21.createElement)("span", { className: "bt-option-hint" }, option.hint) : null
))
)),
(0, import_react21.createElement)("div", { className: "bt-nc-spacer" }),
menuOnly ? null : (0, import_react21.createElement)("div", { className: "bt-nc-spacer" }),
error ? (0, import_react21.createElement)("div", { className: "bt-error" }, error) : null,
(0, import_react21.createElement)(
menuOnly ? null : (0, import_react21.createElement)(
"div",
{ className: "bt-nc-compose" },
(0, import_react21.createElement)(
Expand Down Expand Up @@ -9780,6 +9797,10 @@ var ROOM_MODES = [
];
var shownMode = (room2, roster) => roster.byId[room2.admin] ? room2.mode : "everyone";
function roomSubtitle(room2, roster) {
if (room2.origin === "face-to-face") {
const live = (room2.presentedParticipants ?? room2.participants ?? []).filter((p) => p.leftSeq === void 0);
return t("{count} members", { count: live.length });
}
const admin = roster.byId[room2.admin];
return [t("{count} members", { count: room2.members.length }), admin ? t("Admin {name}", { name: admin.name }) : t("No admin")].join(" · ");
}
Expand Down Expand Up @@ -12356,8 +12377,8 @@ function GroupOverview({ room: room2, roster, actions, usage, useWork, openSetti
room2.sharedFrom.invitedAt ? (0, import_react32.createElement)("div", { className: "bt-set-hint" }, t("Invited {when}", { when: new Date(room2.sharedFrom.invitedAt).toLocaleString() })) : null,
(0, import_react32.createElement)("button", { type: "button", className: "bt-more", onClick: leaveMine }, t("Remove my Bot"))
) : face ? null : room2.notice ? (0, import_react32.createElement)(About, { key: "notice", title: "Notice", text: room2.notice }) : (0, import_react32.createElement)("button", { key: "notice", type: "button", className: "bt-more", onClick: openSettings }, t("Add a notice in group settings")),
(0, import_react32.createElement)(SharedRoomApprovals, { key: "approvals", room: room2 }),
(0, import_react32.createElement)(
face ? null : (0, import_react32.createElement)(SharedRoomApprovals, { key: "approvals", room: room2 }),
face && (room2.face?.status === "candidate" || room2.face?.status === "joining") ? null : (0, import_react32.createElement)(
"div",
{ key: "members" },
(0, import_react32.createElement)("div", { className: "bt-section-title" }, t("Members · {count}", { count: members.length + humans.length + remotes.length })),
Expand Down Expand Up @@ -12409,7 +12430,7 @@ function GroupOverview({ room: room2, roster, actions, usage, useWork, openSetti
})
])
),
(0, import_react32.createElement)(
face ? null : (0, import_react32.createElement)(
"div",
{ key: "mode" },
(0, import_react32.createElement)("div", { className: "bt-section-title" }, t("Who answers")),
Expand Down
8 changes: 5 additions & 3 deletions core/src/client/details-drawer.js
Original file line number Diff line number Diff line change
Expand Up @@ -176,8 +176,10 @@ function GroupOverview({ room, roster, actions, usage, useWork, openSettings })
: room.notice
? h(About, { key: 'notice', title: 'Notice', text: room.notice })
: h('button', { key: 'notice', type: 'button', className: 'bt-more', onClick: openSettings }, t('Add a notice in group settings')),
h(SharedRoomApprovals, { key: 'approvals', room }),
h('div', { key: 'members' },
face ? null : h(SharedRoomApprovals, { key: 'approvals', room }),
face && (room.face?.status === 'candidate' || room.face?.status === 'joining')
? null
: h('div', { key: 'members' },
h('div', { className: 'bt-section-title' }, t('Members · {count}', { count: members.length + humans.length + remotes.length })),
h('div', { className: 'bt-members', role: 'list' }, [
...humans.map(person => h('div', { key: person.id, role: 'listitem' },
Expand All @@ -201,7 +203,7 @@ function GroupOverview({ room, roster, actions, usage, useWork, openSettings })
room.admin === member.id ? h('span', { className: 'bt-chip bt-chip-admin' }, t('Admin')) : null),
entry ? h(MemberWork, { entry, bot: member, room, roster, actions, now }) : null)
})])),
h('div', { key: 'mode' }, h('div', { className: 'bt-section-title' }, t('Who answers')),
face ? null : h('div', { key: 'mode' }, h('div', { className: 'bt-section-title' }, t('Who answers')),
h('div', { className: 'bt-kv' }, h('span', null, t(ROOM_MODES.find(([id]) => id === shownMode(room, roster))?.[1] ?? 'Everyone')))),
usage ? h('div', { key: 'usage' }, h('div', { className: 'bt-section-title' }, t('Usage')),
h('div', { className: 'bt-kv' }, h('span', null, room.name), h('span', null, usage))) : null,
Expand Down
4 changes: 4 additions & 0 deletions core/src/client/group-settings.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@ export const ROOM_MODES = [
export const shownMode = (room, roster) => (roster.byId[room.admin] ? room.mode : 'everyone')

export function roomSubtitle(room, roster) {
if (room.origin === 'face-to-face') {
const live = (room.presentedParticipants ?? room.participants ?? []).filter(p => p.leftSeq === undefined)
return t('{count} members', { count: live.length })
}
const admin = roster.byId[room.admin]
return [t('{count} members', { count: room.members.length }), admin ? t('Admin {name}', { name: admin.name }) : t('No admin')].join(' · ')
}
Expand Down
1 change: 1 addition & 0 deletions core/src/client/i18n.js
Original file line number Diff line number Diff line change
Expand Up @@ -370,6 +370,7 @@ const ZH = {
// The DSH Agent's own row and Session list. "DSH Agent" is a product name and
// stays in English.
'Add DSH Agent': '添加 DSH Agent',
'Add Agent': '添加 Agent',
'Remove DSH Agent': '移除 DSH Agent',
'New session': '新会话',
Archive: '归档',
Expand Down
23 changes: 16 additions & 7 deletions core/src/client/new-chat.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ export function NewChat({ mode, roster, actions }) {
const [query, setQuery] = useState('')
const [picked, setPicked] = useState([])
const [group, setGroup] = useState(mode === 'group')
// Sidebar + starts as a short action menu; the To:/composer opens after New chat.
const [compose, setCompose] = useState(mode !== 'new')
// A new group chat picks its admin first, then the other members.
const [admin, setAdmin] = useState(null)
const [message, setMessage] = useState('')
Expand Down Expand Up @@ -106,10 +108,14 @@ export function NewChat({ mode, roster, actions }) {
actions.openSession(room.id)
close()
})
const menuOnly = mode === 'new' && !compose && !group && picked.length === 0 && wanted === ''

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 Exclude Bot matches from the initial action menu

On the initial mode === 'new' render, wanted is empty, so matches contains every listed Bot. Although menuOnly hides the recipient field/composer and adds a New chat action, the unconditional loop at lines 133-135 still appends the whole roster, meaning the sidebar + opens a potentially long recipient picker rather than the intended short action-only menu (and the new unbounded menu styling can run past the pane for large teams). Suppress Bot matches until New chat switches compose on.

Useful? React with 👍 / 👎.

const options = []
if (!group && picked.length === 0 && mode !== 'search' && wanted === '') {
if (menuOnly) {
options.push({ key: 'chat', icon: h(PlusIcon), label: t('New chat'), run: () => { setCompose(true); setCursor(0); setTimeout(() => inputRef.current?.focus()) } })
}
options.push({ key: 'new', icon: h(PlusIcon), label: t('Create new Bot'), run: createBot })
options.push({ key: 'group', icon: h(PeopleIcon), label: t('Create group chat'), run: () => { setGroup(true); setCursor(0); inputRef.current?.focus() } })
options.push({ key: 'group', icon: h(PeopleIcon), label: t('Create group chat'), run: () => { setGroup(true); setCompose(true); setCursor(0); inputRef.current?.focus() } })
for (const action of newChatActionRows({ mode, group, picked, query, roster })) {
options.push({
key: action.key,
Expand All @@ -119,9 +125,9 @@ export function NewChat({ mode, roster, actions }) {
run: () => action.run({ close, actions, roster }),
})
}
// A team holds at most one DSH Agent; the entry exists only while there is none.
// 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 DSH Agent'), run: () => run(async () => { await actions.addAgent(); close() }) })
options.push({ key: 'agent', icon: h(FishLogo, { size: 16 }), label: t('Add Agent'), run: () => run(async () => { await actions.addAgent(); close() }) })
}
}
for (const bot of matches) {
Expand Down Expand Up @@ -150,8 +156,11 @@ export function NewChat({ mode, roster, actions }) {
else if (admin !== null) setAdmin(null)
}
return h('div', { className: 'bt-pane', style: { left: paneLeft } },
h('div', { className: 'bt-newchat', role: 'dialog', 'aria-label': mode === 'search' ? t('Search') : t('New chat') },
h('div', { className: 'bt-to' },
h('div', { className: 'bt-newchat', role: 'dialog', 'aria-label': mode === 'search' ? t('Search') : t('New chat'), 'data-menu': menuOnly || undefined },
menuOnly ? h('div', { className: 'bt-to' },
h('span', { className: 'bt-to-label' }, t('New…')),
h('button', { type: 'button', className: 'bt-x', 'aria-label': t('Close new chat'), onClick: close }, '×'))
: h('div', { className: 'bt-to' },
h('span', { className: 'bt-to-label' }, t('To:')),
adminBot ? h('span', { key: adminBot.id, className: 'bt-token bt-token-admin', title: t('Admin') },
h(BotMark, { bot: adminBot, size: 16 }), adminBot.name, h(AdminStar),
Expand Down Expand Up @@ -187,9 +196,9 @@ export function NewChat({ mode, roster, actions }) {
option.bot ? h(BotMark, { bot: option.bot, size: 22 }) : h('span', { className: 'bt-option-icon' }, option.icon),
h('span', { className: 'bt-option-label' }, option.label),
option.hint && index === active ? h('span', { className: 'bt-option-hint' }, option.hint) : null)))),
h('div', { className: 'bt-nc-spacer' }),
menuOnly ? null : h('div', { className: 'bt-nc-spacer' }),
error ? h('div', { className: 'bt-error' }, error) : null,
h('div', { className: 'bt-nc-compose' },
menuOnly ? null : h('div', { className: 'bt-nc-compose' },
h('div', { className: 'bt-nc-card' },
h('span', { className: 'bt-nc-plus', 'aria-hidden': true }, h(PlusIcon)),
h('textarea', {
Expand Down
1 change: 1 addition & 0 deletions core/src/client/styles.js
Original file line number Diff line number Diff line change
Expand Up @@ -284,6 +284,7 @@ button.bt-q-opt:active{background:var(--bt-active)}
@media (prefers-reduced-motion:reduce){.bt-q{animation:bt-fade .12s ease-out backwards}.bt-q-settled{animation:none}.bt-q-submit{animation:none}}

.bt-newchat{flex:1;display:flex;flex-direction:column;min-width:0;position:relative;animation:bt-veil-in .18s ease-out}
.bt-newchat[data-menu] .bt-drop{position:relative;top:0;left:0;width:auto;max-height:none;margin:8px 12px;box-shadow:none}
.bt-to{display:flex;align-items:center;flex-wrap:wrap;gap:6px;min-height:48px;padding:6px 12px;border-bottom:1px solid var(--bt-line);font-size:14px;box-sizing:border-box}
.bt-to-label{color:var(--bt-ink-2)}
.bt-to input{flex:1;min-width:120px;border:0;outline:none;background:none;color:var(--bt-ink);font-size:14px;height:26px}
Expand Down
17 changes: 16 additions & 1 deletion core/src/host/channel.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import { randomUUID } from 'node:crypto'
import { createUserMessage } from '@deepseek-ai/dsh-llm'
import { questionReply } from './parts.js'
import { COLORS, EXTRA_COLORS, ROUTE } from './constants.js'
import { isFaceRoom } from './room-membership.js'
import { mentionSpans } from '../shared/mentions.js'

const LANGUAGES = { zh: 'Chinese (简体中文)', en: 'English' }

Expand Down Expand Up @@ -356,7 +358,20 @@ export function install(rt) {
const past = rt.pastOwners.get(body.sessionId)
const sessionId = rt.botOf(body.sessionId) ? rt.chatOf(body.sessionId) : past && past.roomId === undefined ? rt.chatOf(past.botId) : body.sessionId
const room = rt.roomOf(sessionId)
if (room && actor) {
if (room && isFaceRoom(room)) {
if (actor && rt.authzEnforced() && !rt.roomMemberOf(actor, room.id)) return fail('You are not a member of that room', 'bot/forbidden')
// Face rooms must not go through sessionController.prompt: that starts a local
// bot round (turns/steps) and never fans the line to the other FlyBot instance.
const line = await rt.postRoomMessage(room.id, { text, displayName: actor?.displayName }, actor)
const who = line?.who || actor?.displayName || 'You'
if (typeof rt.postToRoom === 'function') {
await rt.postToRoom(room, { id: 'face-notice', name: who }, `${who}: ${text}`, { quiet: true, notice: true })
.catch(error => rt.warn?.('ds-bot: face-room line not shown locally: %s', error))
}
const bots = (room.members ?? []).map(id => rt.botOf(id)).filter(bot => bot?.name)
const named = mentionSpans(text, bots).some(span => span.bot)
if (!named) return ok(true)
Comment on lines +372 to +373

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 Treat @ALL as a local Bot mention

When a face-room owner sends @all (or another word from ALL_WORDS), mentionSpans returns a span with all: true, not bot, so named stays false and this branch returns without starting the local round. The line is displayed and fanned out, but none of the owner's local Bots answer even though group-chat mention semantics define @all as calling everyone; include span.all in this decision.

Useful? React with 👍 / 👎.

} else if (room && actor) {
if (rt.authzEnforced() && !rt.roomMemberOf(actor, room.id)) return fail('You are not a member of that room', 'bot/forbidden')
await rt.postRoomMessage(room.id, { text, displayName: actor.displayName }, actor)
const agent = await rt.resolveAgent(sessionId)
Expand Down
Loading
Loading