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/458-face-room-lifecycle.fixed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Face-to-face rooms: a number nobody is using no longer silently opens a room. Join says "Nobody is using 111111 yet." with Cancel and Start a room, and a room nobody else ever joined (left, or run out) leaves no Past rooms entry. A room stays open while two or more people are in it; when you are the last one, it reads "Room closed." with one Close button, and Leave (or Close) takes you back to the chat you were in without opening Bot settings. Both homes count down to Bot Social's one deadline, even when a device clock is off or a tab was in the background. A room's turns no longer take over its Bots' sidebar preview, the room row leads with its number ("482915 · Alice, Bob"), Enter chat steps aside while "Leave this room?" asks, Join waits for all 6 digits, and a click outside the dialog closes it like Escape ([#458](https://github.com/ThinkFlowLab/FlyBot/issues/458)).
14 changes: 12 additions & 2 deletions core/client.js
Original file line number Diff line number Diff line change
Expand Up @@ -2629,6 +2629,15 @@ function usageOf(list, ids) {
const hit = input > 0 ? Math.round(cacheRead / input * 100) : 0;
return t("{tokens} tok · Cache hit {hit}%", { tokens: formatTokens(input + output), hit });
}
function faceRowTitle(title) {
const text = String(title ?? "");
const named = text.match(/^Face-to-face · (.+) · (\d{6})$/);
if (named) return `${named[2]} · ${named[1]}`;
return text.match(/^Face-to-face · (\d{6})$/)?.[1] ?? text;
}
function awayGroups(groups, roomsById) {
return (groups ?? []).filter((entry) => roomsById?.[entry.id]?.origin !== "face-to-face");
}

// src/client/sources.js
function createSource(initial) {
Expand Down Expand Up @@ -6399,7 +6408,8 @@ function BotRow({ bot, roster, actions, useSessions, useSessionStatus, useSessio
const groups = workKeys(useGroupWork(useWork, (rooms) => Object.entries(rooms).flatMap(([roomId, entries2]) => entries2.filter((entry) => entry.botId === id).map((entry) => `${roomId}|${entry.tool ?? ""}`))));
const away = !running && groups.length > 0;
const state = botState(running || away, doing, alert);
const awayLine = away ? workLine(roster.roomsById[groups[0].id]?.name ?? t("a group chat"), groups[0].tool, groups.length - 1) : null;
const shownAway = away ? awayGroups(groups, roster.roomsById) : [];
const awayLine = shownAway.length > 0 ? workLine(roster.roomsById[shownAway[0].id]?.name ?? t("a group chat"), shownAway[0].tool, shownAway.length - 1) : null;
if (!isListedRemoteChat(bot) && !active && !main) return null;
const onContextMenu = (event) => {
event.preventDefault();
Expand Down Expand Up @@ -6557,7 +6567,7 @@ function RoomRow({ room: room2, roster, actions, useSessions, useSessionStatus,
wide ? (0, import_react10.createElement)(
"span",
{ className: "bt-row-body" },
(0, import_react10.createElement)("span", { className: "bt-row-name" }, (0, import_react10.createElement)("span", { className: "bt-row-title" }, title), face ? (0, import_react10.createElement)(FaceMark) : room2.sharedFrom ? (0, import_react10.createElement)("span", { className: "bt-tag" }, t("Shared")) : null, pinned ? (0, import_react10.createElement)(PinMark) : null),
(0, import_react10.createElement)("span", { className: "bt-row-name" }, (0, import_react10.createElement)("span", { className: "bt-row-title" }, face ? faceRowTitle(title) : title), face ? (0, import_react10.createElement)(FaceMark) : room2.sharedFrom ? (0, import_react10.createElement)("span", { className: "bt-tag" }, t("Shared")) : null, pinned ? (0, import_react10.createElement)(PinMark) : null),
workingLine ? (0, import_react10.createElement)("span", { className: "bt-row-preview" }, workingLine) : preview ? (0, import_react10.createElement)("span", { className: "bt-row-preview" }, preview) : null
) : null,
wide && waiting && !active ? (0, import_react10.createElement)("span", { className: "bt-unread bt-unread-wait", "aria-hidden": true }) : wide && unread && !active ? (0, import_react10.createElement)("span", { className: "bt-unread", "aria-hidden": true }) : null
Expand Down
7 changes: 4 additions & 3 deletions core/src/client/sidebar.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { Component, createElement as h, Fragment, useEffect, useLayoutEffect, us
import { createPortal } from 'react-dom'
import { FishLogo } from '@deepseek-ai/dsh-client-ui-primitives'
import { isMainOf } from './sources.js'
import { currentPart, nameMentions, previewOf, relativeShort, unsentReplyTurns } from './text.js'
import { awayGroups, currentPart, faceRowTitle, nameMentions, previewOf, relativeShort, unsentReplyTurns } from './text.js'
import { PlusIcon, SearchIcon, GaugeIcon, PlugIcon, ClockIcon, GearIcon, PinMarkIcon, ConnectorIcon, SwitchIcon, CloudDownloadIcon, FlyBotMark } from './icons.js'
import { MAIN_LOOK } from './characters.js'
import { botState, BotMark, BotAvatar, RoomAvatar } from './mark.js'
Expand Down Expand Up @@ -57,7 +57,8 @@ function BotRow({ bot, roster, actions, useSessions, useSessionStatus, useSessio
const groups = workKeys(useGroupWork(useWork, rooms => Object.entries(rooms).flatMap(([roomId, entries]) => entries.filter(entry => entry.botId === id).map(entry => `${roomId}|${entry.tool ?? ''}`))))
const away = !running && groups.length > 0
const state = botState(running || away, doing, alert)
const awayLine = away ? workLine(roster.roomsById[groups[0].id]?.name ?? t('a group chat'), groups[0].tool, groups.length - 1) : null
const shownAway = away ? awayGroups(groups, roster.roomsById) : []
const awayLine = shownAway.length > 0 ? workLine(roster.roomsById[shownAway[0].id]?.name ?? t('a group chat'), shownAway[0].tool, shownAway.length - 1) : null
if (!isListedRemoteChat(bot) && !active && !main) return null
const onContextMenu = (event) => {
event.preventDefault()
Expand Down Expand Up @@ -173,7 +174,7 @@ function RoomRow({ room, roster, actions, useSessions, useSessionStatus, useSess
h(RoomAvatar, { room, roster, size: 36 }),
pinned && !wide ? h(PinMark) : null,
wide ? h('span', { className: 'bt-row-body' },
h('span', { className: 'bt-row-name' }, h('span', { className: 'bt-row-title' }, title), face ? h(FaceMark) : room.sharedFrom ? h('span', { className: 'bt-tag' }, t('Shared')) : null, pinned ? h(PinMark) : null),
h('span', { className: 'bt-row-name' }, h('span', { className: 'bt-row-title' }, face ? faceRowTitle(title) : title), face ? h(FaceMark) : room.sharedFrom ? h('span', { className: 'bt-tag' }, t('Shared')) : null, pinned ? h(PinMark) : null),
workingLine ? h('span', { className: 'bt-row-preview' }, workingLine) : preview ? h('span', { className: 'bt-row-preview' }, preview) : null) : null,
wide && waiting && !active ? h('span', { className: 'bt-unread bt-unread-wait', 'aria-hidden': true })
: wide && unread && !active ? h('span', { className: 'bt-unread', 'aria-hidden': true }) : null)
Expand Down
16 changes: 16 additions & 0 deletions core/src/client/text.js
Original file line number Diff line number Diff line change
Expand Up @@ -295,3 +295,19 @@ export function usageOf(list, ids) {
const hit = input > 0 ? Math.round((cacheRead / input) * 100) : 0
return t('{tokens} tok · Cache hit {hit}%', { tokens: formatTokens(input + output), hit })
}

// A face room's sidebar row leads with its number, so a title cut to one line never hides it
// (#458): "Face-to-face · Alice, Bob · 482915" reads "482915 · Alice, Bob". The face mark says
// what the row is; the full title stays on hover. Any other title is kept as it is.
export function faceRowTitle(title) {
const text = String(title ?? '')
const named = text.match(/^Face-to-face · (.+) · (\d{6})$/)
if (named) return `${named[2]} · ${named[1]}`
return text.match(/^Face-to-face · (\d{6})$/)?.[1] ?? text
}

// The group chats whose work shows on a Bot's row as "<group> · Thinking…". A face room has its
// own row, so its turns never take over the Bot's preview (#458).
export function awayGroups(groups, roomsById) {
return (groups ?? []).filter(entry => roomsById?.[entry.id]?.origin !== 'face-to-face')
}
24 changes: 24 additions & 0 deletions core/test/face-row.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import test from 'node:test'
import assert from 'node:assert/strict'
import { readFileSync } from 'node:fs'
import { awayGroups, faceRowTitle } from '../src/client/text.js'

test('#458: a face-room sidebar row leads with its number, so a cut title never hides it', () => {
assert.equal(faceRowTitle('Face-to-face · SelfA0056, SelfB0056 · 482915'), '482915 · SelfA0056, SelfB0056')
assert.equal(faceRowTitle('Face-to-face · 482915'), '482915')
// A title without a number (an older room, the date form) or any other name stays as it is.
assert.equal(faceRowTitle('Face-to-face · Alice, Bob · Oct 18 16:27'), 'Face-to-face · Alice, Bob · Oct 18 16:27')
assert.equal(faceRowTitle('Kitchen'), 'Kitchen')
const sidebar = readFileSync(new URL('../src/client/sidebar.js', import.meta.url), 'utf8')
const row = sidebar.slice(sidebar.indexOf('function RoomRow('), sidebar.indexOf('function AgentSessionRow('))
assert.match(row, /className: 'bt-row-title' \}, face \? faceRowTitle\(title\) : title\)/)
})

test("#458: a face room's turns never take over its Bots' sidebar preview; other groups still show", () => {
const roomsById = { g1: { id: 'g1', origin: 'face-to-face', name: 'Face-to-face · A, B · 482915' }, g2: { id: 'g2', name: 'Team' } }
assert.deepEqual(awayGroups([{ id: 'g1', tool: 'think' }], roomsById), [])
assert.deepEqual(awayGroups([{ id: 'g1' }, { id: 'g2', tool: 'read' }], roomsById), [{ id: 'g2', tool: 'read' }])
const sidebar = readFileSync(new URL('../src/client/sidebar.js', import.meta.url), 'utf8')
const row = sidebar.slice(sidebar.indexOf('function BotRow('), sidebar.indexOf('function RemoteMark('))
assert.match(row, /const shownAway = away \? awayGroups\(groups, roster\.roomsById\) : \[\]/)
})
2 changes: 1 addition & 1 deletion docs/subsystems/core-client.md
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,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`). 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)") is its hover text and label. The row wrap's gear is absolutely positioned so the hover/selected band stays full width (#384). A friend's Bot fallback look (`lookAvoidingLocals`) steps its seeded shape (then color) when it would match one of your own Bots exactly; a named ink and its published hex count as one color. `indexRoster` registers the own Bots (`setLocalBots`) and `BotMark` applies the avoidance to every remote Bot, so the sidebar, header, palette, details drawer and transcript agree; a published image/emoji/shape is left alone. 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). The structure entry may set `include(botId) === false` to omit a Bot from the default list (tree-archived and leftover `归档-*` / `Archive-*` names, #452); those Bots stay findable in Search when not also `bot.hidden`.
- `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)") is its hover text and label. The row wrap's gear is absolutely positioned so the hover/selected band stays full width (#384). A friend's Bot fallback look (`lookAvoidingLocals`) steps its seeded shape (then color) when it would match one of your own Bots exactly; a named ink and its published hex count as one color. `indexRoster` registers the own Bots (`setLocalBots`) and `BotMark` applies the avoidance to every remote Bot, so the sidebar, header, palette, details drawer and transcript agree; a published image/emoji/shape is left alone. A face-room row stays on one line with an ellipsis, and every room row has its full title as hover text (#304). The face-room row leads with its number (`faceRowTitle` in `text.js`: `Face-to-face · A, B · 482915` reads `482915 · A, B`), so the cut never hides it; the hover text and label keep the full title (#458). A Bot row's group-work line (`<group> · Thinking…`) skips face rooms (`awayGroups` in `text.js`): a face room has its own row, so its turns never take over the Bot's preview (#458). 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). The structure entry may set `include(botId) === false` to omit a Bot from the default list (tree-archived and leftover `归档-*` / `Archive-*` names, #452); those Bots stay findable in Search when not also `bot.hidden`.

### Approval wait badge on chat rows

Expand Down
Loading
Loading