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/1087-remote-approval-wait.fixed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
A friend's direct chat says "Waiting for their approval" (等对方批准) while you wait for their reply, instead of a generic thinking line such as "Sorting out the details…", so a reply their home is holding for approval is not presented as this side still working it out (#1087).
14 changes: 12 additions & 2 deletions core/client.js
Original file line number Diff line number Diff line change
Expand Up @@ -791,6 +791,7 @@ var ZH = {
"{first} and {second} are waiting for your approval": "{first}、{second} 等你批准",
"{first}, {second} and {count} more are waiting for your approval": "{first}、{second} 等 {total} 个等你批准",
"Waiting for your approval": "等你批准",
"Waiting for their approval": "等对方批准",
"Your Bots post in rooms without asking each time. Use Auto-send in the room header to turn it off.": "你的 Bot 会在房间里直接发送,不再每次询问。要关掉,用房间标题里的自动发送。",
"Turn on auto-send": "打开自动发送",
"Approve and always auto-send in this room": "批准并在此房间始终自动发送",
Expand Down Expand Up @@ -6578,6 +6579,14 @@ function awaitingReply(roster, sessionId, now = Date.now()) {
return now - at < AWAIT_REPLY_MS;
}

// src/client/remote-approval-wait.js
var REMOTE_APPROVAL_LABEL = "Waiting for their approval";
function remoteApprovalPhrase(remote, { tool, inputAt = 0 } = {}) {
if (!remote) return null;
if (tool && inputAt <= (tool.at ?? 0)) return null;
return { key: "remote-approval", label: REMOTE_APPROVAL_LABEL, state: "thinking" };
}

// src/client/activity-row.js
var import_react9 = require("react");
var import_react_dom2 = require("react-dom");
Expand Down Expand Up @@ -6748,7 +6757,8 @@ function ActivityIndicator({ sessionId, useRoster, useSessionStatus, useLive, us
const inputRef = (0, import_react9.useRef)(0);
const inputAt = Math.max(inputRef.current, pendingInput);
inputRef.current = inputAt;
const want = !visible ? null : inputAt > (tool?.at ?? 0) ? { key: `input:${inputAt}`, pool: PHRASES.thinking, state: "thinking" } : tool ? { key: tool.label ? `tool:${tool.label}` : "working", label: tool.label, pool: PHRASES.working, state: tool.state ?? "working" } : { key: "thinking", pool: PHRASES.thinking, state: "thinking" };
const phrase = visible ? remoteApprovalPhrase(bot?.remote !== void 0, { tool, inputAt }) : null;
const want = !visible ? null : phrase ? { ...phrase, label: t(phrase.label) } : inputAt > (tool?.at ?? 0) ? { key: `input:${inputAt}`, pool: PHRASES.thinking, state: "thinking" } : tool ? { key: tool.label ? `tool:${tool.label}` : "working", label: tool.label, pool: PHRASES.working, state: tool.state ?? "working" } : { key: "thinking", pool: PHRASES.thinking, state: "thinking" };
const [shown, setShown] = (0, import_react9.useState)(null);
const wantKey = want?.key ?? null;
const markState = (0, import_react9.useRef)("thinking");
Expand Down Expand Up @@ -6818,7 +6828,7 @@ function ActivityIndicator({ sessionId, useRoster, useSessionStatus, useLive, us
const elapsed = now - shown.at >= 6e4 ? elapsedLabel(now - shown.at) : null;
return (0, import_react_dom2.createPortal)((0, import_react9.createElement)(
"div",
{ className: "bt-typing", "data-exiting": phase === "out" ? "" : void 0, "data-carry": carry && phase !== "out" ? "" : void 0, "data-remote": remote ? "" : void 0, "aria-hidden": true },
{ className: "bt-typing", "data-exiting": phase === "out" ? "" : void 0, "data-carry": carry && phase !== "out" ? "" : void 0, "data-remote": remote ? "" : void 0, "data-flybot": shown.key === "remote-approval" ? "remote-approval" : void 0, "aria-hidden": true },
bot ? (0, import_react9.createElement)(BotMark, { bot, size: 28, state: markState.current, live: true, pokeable: true }) : (0, import_react9.createElement)(RoomAvatar, { room: room2, roster, size: 28 }),
(0, import_react9.createElement)(
"span",
Expand Down
6 changes: 5 additions & 1 deletion core/src/client/activity-row.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { Shimmer, PHRASES } from './cells.js'
import { reducedMotion } from './motion.js'
import { t } from './i18n.js'
import { AWAIT_REPLY_MS, awaitingReply } from './awaiting.js'
import { remoteApprovalPhrase } from './remote-approval-wait.js'

// -------------------------------------------------------------------------
// The activity row: the Bot's mark and a shimmering phrase at the transcript
Expand Down Expand Up @@ -178,7 +179,10 @@ export function ActivityIndicator({ sessionId, useRoster, useSessionStatus, useL
// the newest activity, and the phrase and the clock restart from the send.
const inputAt = Math.max(inputRef.current, pendingInput)
inputRef.current = inputAt
// Their home may be holding the reply. Say so, unless a local tool is newer (#1087).
const phrase = visible ? remoteApprovalPhrase(bot?.remote !== undefined, { tool, inputAt }) : null
const want = !visible ? null
: phrase ? { ...phrase, label: t(phrase.label) }
: inputAt > (tool?.at ?? 0)
? { key: `input:${inputAt}`, pool: PHRASES.thinking, state: 'thinking' }
: tool ? { key: tool.label ? `tool:${tool.label}` : 'working', label: tool.label, pool: PHRASES.working, state: tool.state ?? 'working' }
Expand Down Expand Up @@ -245,7 +249,7 @@ export function ActivityIndicator({ sessionId, useRoster, useSessionStatus, useL
useEffect(() => { drawn.current = Boolean(host && phase !== null && shown) }, [host, phase, shown])
if (!host || phase === null || !shown) return null
const elapsed = now - shown.at >= 60000 ? elapsedLabel(now - shown.at) : null
return createPortal(h('div', { className: 'bt-typing', 'data-exiting': phase === 'out' ? '' : undefined, 'data-carry': carry && phase !== 'out' ? '' : undefined, 'data-remote': remote ? '' : undefined, 'aria-hidden': true },
return createPortal(h('div', { className: 'bt-typing', 'data-exiting': phase === 'out' ? '' : undefined, 'data-carry': carry && phase !== 'out' ? '' : undefined, 'data-remote': remote ? '' : undefined, 'data-flybot': shown.key === 'remote-approval' ? 'remote-approval' : undefined, 'aria-hidden': true },
bot ? h(BotMark, { bot, size: 28, state: markState.current, live: true, pokeable: true }) : h(RoomAvatar, { room, roster, size: 28 }),
h('span', { className: 'bt-typing-text' },
h('span', { key: `${shown.key}:${shown.at}`, className: 'bt-typing-label' },
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 @@ -735,6 +735,7 @@ const ZH = {
'{first} and {second} are waiting for your approval': '{first}、{second} 等你批准',
'{first}, {second} and {count} more are waiting for your approval': '{first}、{second} 等 {total} 个等你批准',
'Waiting for your approval': '等你批准',
'Waiting for their approval': '等对方批准',
'Your Bots post in rooms without asking each time. Use Auto-send in the room header to turn it off.': '你的 Bot 会在房间里直接发送,不再每次询问。要关掉,用房间标题里的自动发送。',
'Turn on auto-send': '打开自动发送',
'Approve and always auto-send in this room': '批准并在此房间始终自动发送',
Expand Down
14 changes: 14 additions & 0 deletions core/src/client/remote-approval-wait.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
// A friend's 1:1 chat (#1087). The activity row stays up from my send until their
// reply, and that wait is often their home holding the reply for approval. This
// home cannot see that card, so the row says what the wait is for instead of
// drawing the generic thinking pool ("Sorting out the details").

export const REMOTE_APPROVAL_LABEL = 'Waiting for their approval'

// The phrase for that wait, or null when a local tool is the newest activity
// (its own line stays) or the chat is not a friend's.
export function remoteApprovalPhrase(remote, { tool, inputAt = 0 } = {}) {
if (!remote) return null
if (tool && inputAt <= (tool.at ?? 0)) return null
return { key: 'remote-approval', label: REMOTE_APPROVAL_LABEL, state: 'thinking' }
}
28 changes: 28 additions & 0 deletions core/test/remote-approval-wait.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import assert from 'node:assert/strict'
import { readFileSync } from 'node:fs'
import { test } from 'node:test'
import { DICTIONARY } from '../src/client/i18n.js'
import { REMOTE_APPROVAL_LABEL, remoteApprovalPhrase } from '../src/client/remote-approval-wait.js'

const row = readFileSync(new URL('../src/client/activity-row.js', import.meta.url), 'utf8')
const cells = readFileSync(new URL('../src/client/cells.js', import.meta.url), 'utf8')

test('#1087: a friend DM wait says their approval is pending, not the thinking pool', () => {
const phrase = remoteApprovalPhrase(true, { inputAt: 50 })
assert.deepEqual(phrase, { key: 'remote-approval', label: 'Waiting for their approval', state: 'thinking' })
assert.equal(phrase.label, REMOTE_APPROVAL_LABEL)
// The line people were reading for the whole approval wait.
assert.match(cells, /thinking: \[[^\]]*Sorting out the details/)
assert.equal(cells.includes(`'${REMOTE_APPROVAL_LABEL}'`), false)
assert.equal(DICTIONARY.zh[REMOTE_APPROVAL_LABEL], '等对方批准')
// Fresh input after an older tool is still that wait.
assert.equal(remoteApprovalPhrase(true, { inputAt: 30, tool: { at: 10, label: 'Searching' } }).key, 'remote-approval')
// A local tool that is the newest activity keeps its own line.
assert.equal(remoteApprovalPhrase(true, { inputAt: 10, tool: { at: 20, label: 'Searching' } }), null)
// A local chat never uses it.
assert.equal(remoteApprovalPhrase(false, { inputAt: 50 }), null)
// The row actually asks, and marks the line so the wait is not the thinking pool.
assert.match(row, /remoteApprovalPhrase\(bot\?\.remote !== undefined, \{ tool, inputAt \}\)/)
assert.match(row, /phrase \? \{ \.\.\.phrase, label: t\(phrase\.label\) \}/)
assert.match(row, /shown\.key === 'remote-approval' \? 'remote-approval'/)
})
2 changes: 1 addition & 1 deletion docs/subsystems/core-client.md
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ The overlay (`flybot-slots.js` `FlyBotApprovals`) is the fallback, not a second
- In a face-to-face room the bubble matches a mention by every alias the room host routes on (`faceAliases` in `shared/remote-label.js`, the same list `face-mentions.js` uses), so `@SelfA's Chief` typed on B is A's own Chief on A. The chip shows `@` and the seat label (`faceSeatLabel`, `SelfA · Chief`) on both homes, a bare name means the writer's own Bot (my line tries my seats first, a friend's line the friend's, whatever order the room lists its seats in, as `faceCandidates` does), a friend's Bot outside the room is not matched, and one space typed after a picked `@Name ` (the menu inserts its own) is dropped from the drawing (#305).
- A remote Bot's generated look is seeded by its handle (`remoteSeed` in `src/client/characters.js`), never by its session id. When Bot Social sends `avatar` / `color` / `avatar_updated_at`, `flybot.remote.publish` stores them on the remote-chat record (cached by `avatar_updated_at`). `lookBotForRemote` overlays a person payload's look (face-room `members[]` is the source for room avatars). A stored `color`/`avatar` still wins over the seed; missing look stays handle-seeded (#137).
- A shared display name does not restyle a local Bot (#963). The `@Name` file chip (`mentionCss` in `src/client/mention-style.js`) uses the local Bot's look when a friend has the same name; that friend's own mention anchor keeps the friend's color, and a name only a friend has keeps that friend's look. `indexRoster` does not let a remote chat replace a local Bot's id or session keys, and `rosterBot` resolves a member id to the local Bot. `@Name` in a bubble (`linkMentions`) names the local Bot when both are listed. A group reply that is not remote resolves through `rosterBot` before the index. The friend's own row keeps the friend's look, unless that drawn look is the same picture as the local Bot of the same display name, in which case the row steps its shape and the local Bot stays as stored (#1069).
- A friend's 1:1 chat (#443): `AssistantCell` gives the reply bubbles a `bt-lead` `BotMark` from the chat record (`friendLead`; none on a `Not sent:` note). The activity row (`activity-row.js`) stays up while `awaitingReply` (`awaiting.js`, `roster.remoteAwaiting`, 10 min cap) holds. Between the send turn and the echo turn it sits in the last turn's tail, with a 1.5 s grace and `data-carry` so there is no second entrance, which keeps one row from send to reply. The social `PeerThinking` line is for face rooms only. A friend chat shows the row for a run that started from my submission and, between runs, while `awaitingReply` holds. The friend's reply run (the echo, which has no submission) hides it at once, and only the end of my send run gets the grace. The turn-tail hiding rules in `styles.js` spare `.bt-typing` and its tail item, or the row between runs would be drawn with `display:none`.
- A friend's 1:1 chat (#443): `AssistantCell` gives the reply bubbles a `bt-lead` `BotMark` from the chat record (`friendLead`; none on a `Not sent:` note). The activity row (`activity-row.js`) stays up while `awaitingReply` (`awaiting.js`, `roster.remoteAwaiting`, 10 min cap) holds. Between the send turn and the echo turn it sits in the last turn's tail, with a 1.5 s grace and `data-carry` so there is no second entrance, which keeps one row from send to reply. The social `PeerThinking` line is for face rooms only. While the row is up in a friend chat and no local tool is the newest activity, its phrase is **Waiting for their approval** (等对方批准) instead of the thinking pool (`Sorting out the details` and the rest): that wait is the other home holding the reply, and this home cannot see the card (#1087). A local tool line still wins. A friend chat shows the row for a run that started from my submission and, between runs, while `awaitingReply` holds. The friend's reply run (the echo, which has no submission) hides it at once, and only the end of my send run gets the grace. The turn-tail hiding rules in `styles.js` spare `.bt-typing` and its tail item, or the row between runs would be drawn with `display:none`.
- A friend's Bot the owner hid, a remote chat marked `gone`/`hidden`, and a home-level handle (`@friend.home`, not `@bot.friend.home`) must not reappear in the sidebar, the **发给 / 和谁聊…** picker, `@` mentions, or `list_bots` friends lines (`isListedRemoteChat`). `flybot.remote.publish` skips those handles so remote-chat sync marks the persisted session gone/hidden instead of opening it again; history is kept. Handle comparison is case-insensitive (`normRemoteHandle`).
- The "@" reader is shared with the host (`core/src/shared/mentions.js`, bundled into `core/client.js`). #287 added `unmatchedMentions` there (the `@` words that name nobody, used by the host for the face-room hint); `mentionSpans` and what the bubbles draw are unchanged.
- A friend's Bot settings surface is not the local editor (#160): no rename, avatar editor, model, folder, instructions, Main Bot, Hide, Duplicate, or Delete. The details gear and context menu say **Bot settings**. The menu is Mark as Unread, Bot settings, and Copy conversation ID. Per-friend **Auto-send** and **Remove this Bot** stay on that page (`flybot.bot.details`); Remove closes the overlay so a stale local editor cannot appear. The Bots settings list is local Bots only.
Expand Down
3 changes: 2 additions & 1 deletion patches/ds-bot/0001-core-fixes.patch
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
FlyBot patch 0001-core-fixes on Fei2411/DeepSeek-Bot@2b10c70 (ds-bot/). Generated by scripts/ds-bot-patches.mjs; edits to upstream files only (files FlyBot adds are in files/).

diff --git a/README.md b/README.md
Expand Down Expand Up @@ -922,7 +922,7 @@
"{name} didn't answer in time": '{name} 没有及时回应',
'{name} paused for your approval': '{name} 停下来等你批准',
'{name} started on it': '{name} 开始做了',
@@ -450,12 +450,29 @@ const ZH = {
@@ -450,12 +450,30 @@ const ZH = {
'Asked to make the tool call it wrote as text': '工具调用被写成了文字,已让它重新调用',
'A Bot': '一个 Bot',
'{name} is waiting for your approval': '{name} 等你批准',
Expand All @@ -937,6 +937,7 @@
'{first}, {second} and {count} more are waiting for your approval': '{first}、{second} 等 {total} 个等你批准',
'Waiting for your approval': '等你批准',
- 'in its own chat': '在自己的对话里',
+ 'Waiting for their approval': '等对方批准',
+ 'Your Bots post in rooms without asking each time. Use Auto-send in the room header to turn it off.': '你的 Bot 会在房间里直接发送,不再每次询问。要关掉,用房间标题里的自动发送。',
+ 'Turn on auto-send': '打开自动发送',
+ 'Approve and always auto-send in this room': '批准并在此房间始终自动发送',
Expand Down
Loading
Loading