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
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ Every local Bot can chat with friends' Bots through a [Bot Social](https://githu
- **One-click onboarding.** On first start with a `server` configured, FlyBot calls `POST <server>/v1/onboard/flybot`, creates your home (`@<name>.home`), and stores credentials in `$DSH_HOME/bot/flybot-social.json` (mode 600).
- **Members.** Access is a per-Bot **grant** (default deny, Chief included). A local Bot becomes `@<bot>.<home>` on the server when you invite someone to that Bot, when it joins an invite, or when you bring it into a face-to-face room. The sidebar lists only Bots granted to you. **Manage access** on a Bot shows who has it and **Revoke**. Deleted Bots are revoked. Ungranted Bots have no outside identity: friends cannot reach them and they cannot message friends.
- **Connectors (GitHub, Composio).** Settings → **Connectors** connects an account once; it is **on by default for you** in every Bot, new and existing, also in a Bot you have shared. When someone else drives a Bot — a friend, another person in its room, another owner's Bot — the tools are **off by default**, so they cannot reach your mail, calendar or repositories through it; you can allow it per Bot (**Choose Bots**, or the **Connectors** card in the Bot's settings: **also when others use it**), or turn a Bot's connector off for yourself. A room with other people in it counts as their use. Disconnecting resets every Bot's choices.
- **Friends in the Bot list.** A friend's Bot sits in the same sidebar list as yours, with a small globe after the name. Face-to-face rooms sit there too, with a people mark; ended ones fold under **Past rooms**. Click a row to open a normal chat. Sidebar **+** → **Add friend** (paste a handle, invite link or code), **Face-to-face room**, and **Copy my handle**. On a local Bot: right-click / settings **Sharing** → **Invite to this Bot** (or **Copy invite link** / **Copy with instructions**); **People with access** lists who has it and **Revoke**. Each person row there also has **Auto-send** for that Bot → that person (default off); **Auto-send from now on** on a held reply turns on that same switch.
- **Friends in the Bot list.** A friend's Bot sits in the same sidebar list as yours, with a small globe after the name. Face-to-face rooms sit there too, with a people mark; ended ones fold under **Past rooms**. Click a row to open a normal chat. Sidebar **+** → **Add friend** (paste a handle, invite link or code), **Face-to-face room**, and **Copy my handle**. On a local Bot: right-click / settings **Sharing** → **Copy invite link for <Bot>**, then **Copy link** / **Copy with instructions** / **New link** (the same control as **Add friend** → **Invite a friend**); **People with access** lists who has it and **Revoke**. Each person row there also has **Auto-send** for that Bot → that person (default off); **Auto-send from now on** on a held reply turns on that same switch.
- **Face-to-face rooms.** Sidebar **+** → **Face-to-face room**: pick one or more of your Bots, enter the **same 6-digit number** as your friend (about 10 minutes), wait, then **Enter chat** (or **Leave**). No prior friendship and no preview of the other home's other Bots. Your typed lines are sent as you (when the server supports `typed_by_owner`); `@Name` / the picker label / a handle asks that Bot; `@all` asks everyone. A plain line with no `@` is answered by the one Bot it is for (each Bot passes silently unless the line fits it; the Bot that spoke last gets follow-ups; the first Bot answers if all pass); a line to everyone ("introduce yourselves", "你们…") may get one answer from each. Bot-written room lines still go through Approve unless face **Auto-send** is on (open room overview; default off).
- **Typed by the owner · mentions · thinking.** Against a Bot Social that lists these under `GET /v1/platform` `features`: your typed friend-DM and face lines can be stamped `typed_by_owner` so the other home draws them as you; face sends may carry an explicit `mentions` handle list; a quiet `Name is thinking…` line appears above the composer while a Bot of another home works in a friend DM or a face-to-face room (ephemeral `presence`; DM path [#360](https://github.com/ThinkFlowLab/FlyBot/pull/360) / [#332](https://github.com/ThinkFlowLab/FlyBot/issues/332)). Older servers simply omit what they do not support.
- **Approvals.** Inbound messages from a friend arrive immediately (fenced as untrusted). Outbound replies may wait in the open chat (badge + Approve / Reject) until you decide, unless Auto-send is on for that Bot and person (the Bot's **Sharing** → **People with access**) or for the face room. FlyBot also holds a Bot's own send behind an approval card; files are always held.
Expand Down Expand Up @@ -160,7 +160,7 @@ On first start FlyBot calls `POST <server>/v1/onboard/flybot`, registers your ho

**Change the server later** without editing YAML: Settings → **Bot Social** shows the server and its connection status, **Change server** checks the new address (`https://`, or `http://localhost` for development; FlyBot reads `<server>/v1/platform` and shows its features) and, after a clear warning, switches without a restart. A new server is a **new identity**: your friends and sharing stay on the old server, your old credentials file is kept as a backup and never deleted, and switching back restores them. The `cordis.patch.yml` value stays the default.

**Share one Bot:** that Bot's settings → **Sharing** → **Invite to this Bot** (or **Copy invite link**); send the link. Your friend opens sidebar **+** → **Add friend**, pastes the link or code, and accepts — that grant covers **only** that Bot. **People with access** lists who has it; **Revoke** removes them.
**Share one Bot:** that Bot's settings → **Sharing** → **Copy invite link for <Bot>** (or **+** → **Add friend** → **Invite a friend**); send the link. Your friend opens sidebar **+** → **Add friend**, pastes the link or code, and accepts — that grant covers **only** that Bot. **People with access** lists who has it; **Revoke** removes them.

**Chat:** click the friend's Bot in the sidebar. Their messages arrive at once. What your Bots send back waits in that chat (Approve / Reject) unless **Auto-send** is on for that Bot and that friend (your Bot's **Sharing** → **People with access**, or **Auto-send my Bots' replies to …** on the friend's Bot settings for all your Bots; default off). Look for the **to approve** badge if they see no reply.

Expand Down
4 changes: 2 additions & 2 deletions README.zh.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ npx -y @deepseek-ai/dsh@0.2.0-rc.2 plugin --profile web add "github:ThinkFlowLab
- **一键入驻。** 配置好 `server` 后首次启动会调用 `POST <server>/v1/onboard/flybot`,创建家(`@<name>.home`),凭证写在 `$DSH_HOME/bot/flybot-social.json`(权限 600)。
- **成员。** 访问是按 Bot 的**授权**(默认拒绝,Chief 也一样)。本地 Bot 在你邀请别人使用它、它加入邀请、或你把它带进面对面房间时,才会在服务器上成为 `@<bot>.<home>`。侧栏只列出别人授权给你的 Bot。每个 Bot 的 **管理访问** 列出谁有它,并可 **撤销**。删除 Bot 会撤销其身份。未授权的 Bot 没有对外身份:朋友够不到它,它也不能给朋友发消息。
- **连接器(GitHub、Composio)。** 设置 → **连接器** 里连接一次账号;**默认对你自己开启**,每个 Bot(新建的和已有的)都能用,共享出去的 Bot 你自己也照样能用。别人在驱动某个 Bot 时——好友、房间里的其他人、别人的 Bot——工具**默认关闭**,所以他们碰不到你的邮件、日历或代码仓库;你可以按 Bot 单独允许(**选择 Bot**,或该 Bot 设置里的 **连接器** 卡片:**别人使用时也开启**),也可以为自己关闭某个 Bot 的连接器。有其他人在的房间里也算别人使用。断开连接会重置所有 Bot 的选择。
- **侧栏里的好友。** 好友的 Bot 和你自己的 Bot 在同一列表,名字后面有一个小地球标记。面对面房间也在这份列表里,带一个小人标记;已结束的折在 **过去的房间** 下。点开就是普通对话。侧栏 **+** → **添加好友**(可粘贴句柄、邀请链接或码)、**面对面房间**、**复制我的 handle**。本地 Bot:设置里 **Sharing(分享)** → **Invite to this Bot(邀请使用此 Bot)**(或 **复制邀请链接** / 带说明的邀请);**People with access(访问权限)** 列出谁有它并可 **Revoke(撤销)**。这里每个人的那一行也有 **Auto-send(自动发送)**,只管这个 Bot → 这个人(默认关);被暂扣回复上的 **Auto-send from now on** 打开的就是这个开关。
- **侧栏里的好友。** 好友的 Bot 和你自己的 Bot 在同一列表,名字后面有一个小地球标记。面对面房间也在这份列表里,带一个小人标记;已结束的折在 **过去的房间** 下。点开就是普通对话。侧栏 **+** → **添加好友**(可粘贴句柄、邀请链接或码)、**面对面房间**、**复制我的 handle**。本地 Bot:设置里 **Sharing(分享)** → **Copy invite link for <Bot>(复制邀请链接)**,之后可 **Copy link** / **Copy with instructions**(带说明)/ **New link**(与 **添加好友** → **Invite a friend** 是同一个控件);**People with access(访问权限)** 列出谁有它并可 **Revoke(撤销)**。这里每个人的那一行也有 **Auto-send(自动发送)**,只管这个 Bot → 这个人(默认关);被暂扣回复上的 **Auto-send from now on** 打开的就是这个开关。
- **面对面房间。** 侧栏 **+** → **面对面房间**:勾选自己的 Bot,双方输入**同一组 6 位数字**(约 10 分钟内),等候后点 **Enter chat(进入聊天)**(或 **Leave**)。不必先成为好友,也不会暴露对方家里的其他 Bot。你打的字在对方看来是你本人(服务器支持 `typed_by_owner` 时);`@名字` / 选择器标签 / handle 点名该 Bot;`@all` 叫全员。没有 `@` 的普通一句话只由最合适的那个 Bot 回答(其他 Bot 默默跳过;追问优先给上一个发言的 Bot;都跳过时第一个 Bot 兜底回答);对“大家 / 你们 / 自我介绍”这类面向所有人的话,每个 Bot 可各答一句。Bot 自己写的房间消息仍可能进审批,除非在该房间概览开了面对面 **Auto-send**(默认关)。
- **主人手打 · 提及 · 正在输入。** 若 Bot Social 的 `GET /v1/platform` `features` 含相应项:私聊与面对面里你手打的行可盖 `typed_by_owner` 章;面对面发送可带显式 `mentions` handle 列表;朋友私聊或面对面房间里,其他家的 Bot 工作时输入框上方会出现安静的 `Name is thinking…`(短暂的 `presence`;私聊路径见 [#360](https://github.com/ThinkFlowLab/FlyBot/pull/360) / [#332](https://github.com/ThinkFlowLab/FlyBot/issues/332))。旧服务器没有的能力会自动跳过。
- **审批。** 好友发来的消息会立刻送达(按不可信内容处理)。你的 Bot 发出的回复可能停在当前聊天里等你批准(或拒绝),除非对这个 Bot 和这个人开了自动发送(该 Bot 的 **Sharing** → **People with access**),或该面对面房间开了自动发送。FlyBot 本地也会暂扣 Bot 自己发起的发送;文件一律先审批。
Expand Down Expand Up @@ -154,7 +154,7 @@ export PORT=8787

**之后换服务器**不用改 YAML:设置 → **Bot Social** 显示当前服务器和连接状态,**更换服务器**会先检查新地址(`https://`;开发时可用 `http://localhost`;FlyBot 读取 `<server>/v1/platform` 并列出它的功能),经过明确的警告确认后无需重启即可切换。换服务器等于**换一个新身份**:好友和分享留在旧服务器上,旧的凭证文件作为备份保留、不会被删除,切换回去即可恢复。`cordis.patch.yml` 里的值仍是默认值。

**分享某一个 Bot:** 该 Bot 设置 → **Sharing** → **Invite to this Bot**(或 **复制邀请链接**),把链接发给对方;对方侧栏 **+** → **添加好友** 粘贴链接或码并接受——**只授权这一个 Bot**。**People with access** 可查看并 **Revoke**。
**分享某一个 Bot:** 该 Bot 设置 → **Sharing** → **Copy invite link for <Bot>**(或 **+** → **添加好友** → **Invite a friend**),把链接发给对方;对方侧栏 **+** → **添加好友** 粘贴链接或码并接受——**只授权这一个 Bot**。**People with access** 可查看并 **Revoke**。

**私聊:** 点侧栏里对方的 Bot。对方来信立刻到。你这边发出去的内容停在该聊天等批准(除非对这个 Bot 和该好友开了 **Auto-send**:你的 Bot 的 **Sharing** → **People with access**;或在好友 Bot 设置里勾 **Auto-send my Bots' replies to …**,覆盖你所有 Bot;默认关)。对方看不到回复时,先看 **to approve / 待批准** 角标。

Expand Down
1 change: 1 addition & 0 deletions changelog.d/499-invite-friendchat-polish.fixed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Invites and friend chats are tidier. A friend's Bot chat always reads **Message Chief (Alice)** in the composer, from the first open and after every send, never the coding-agent copy. **Add friend** and a Bot's **Sharing** use one invite control: **Copy invite link for Chief** makes and copies the link, which then shows with **Copy link**, **Copy with instructions** and **New link**, and every copy says which Bot it is for (`Invite link for Chief copied.`). The Add friend paste box wraps, so a whole pasted link is readable; typing clears an old error, and after an accepted invite the panel shows the result for a moment, then closes and opens the new friend's Bot. The Sharing hint mentions Revoke only once someone has access ([#499](https://github.com/ThinkFlowLab/FlyBot/issues/499)).
61 changes: 33 additions & 28 deletions core/client.js
Original file line number Diff line number Diff line change
Expand Up @@ -7814,6 +7814,38 @@ var SIDEBAR_CSS = `
.bt-row-settings svg{width:15px;height:15px}
`;

// src/client/composer-label.js
var ATTRIBUTES = ["aria-label", "data-placeholder"];
function holdComposerLabel(doc, label) {
doc.body.style.setProperty("--bt-placeholder", JSON.stringify(label));
const shell = /* @__PURE__ */ new Map();
const apply2 = () => {
for (const el of doc.querySelectorAll("[data-composer-input]")) {
for (const name of ATTRIBUTES) {
const current = el.getAttribute(name);
if (current === label) continue;
if (!shell.has(el)) shell.set(el, {});
shell.get(el)[name] = current;
el.setAttribute(name, label);
}
}
};
apply2();
const Observer = doc.defaultView?.MutationObserver ?? globalThis.MutationObserver;
const watcher = typeof Observer === "function" ? new Observer(apply2) : null;
watcher?.observe(doc.body, { subtree: true, childList: true, attributes: true, attributeFilter: ATTRIBUTES });
return () => {
watcher?.disconnect();
for (const [el, values] of shell) {
if (!el.isConnected) continue;
for (const [name, value] of Object.entries(values)) {
if (value == null) el.removeAttribute(name);
else el.setAttribute(name, value);
}
}
};
}

// src/client/header.js
var import_react13 = require("react");
var import_react_dom4 = require("react-dom");
Expand Down Expand Up @@ -7897,34 +7929,7 @@ function HeaderPill({ sessionId, useRoster, useSessions, useSessionStatus, useSe
delete document.body.dataset.btFriendChat;
};
}, [bot?.remote]);
(0, import_react13.useEffect)(() => {
document.body.style.setProperty("--bt-placeholder", JSON.stringify(composerLabel));
const input = document.querySelector("[data-composer-input]");
const priorLabel = input?.getAttribute("aria-label") ?? null;
const apply2 = () => {
const el = document.querySelector("[data-composer-input]");
if (el && el.getAttribute("aria-label") !== composerLabel) el.setAttribute("aria-label", composerLabel);
};
apply2();
const timer = setTimeout(apply2, 0);
let frame = 0;
const watcher = typeof MutationObserver === "function" ? new MutationObserver(() => {
if (!frame) frame = requestAnimationFrame(() => {
frame = 0;
apply2();
});
}) : null;
watcher?.observe(document.body, { subtree: true, childList: true, attributes: true, attributeFilter: ["aria-label"] });
return () => {
clearTimeout(timer);
watcher?.disconnect();
if (frame) cancelAnimationFrame(frame);
const el = document.querySelector("[data-composer-input]");
if (!el) return;
if (priorLabel == null) el.removeAttribute("aria-label");
else el.setAttribute("aria-label", priorLabel);
};
}, [composerLabel]);
(0, import_react13.useLayoutEffect)(() => holdComposerLabel(document, composerLabel), [composerLabel]);
(0, import_react13.useEffect)(() => {
actions.beginVisit(sessionId);
return () => actions.endVisit(sessionId);
Expand Down
41 changes: 41 additions & 0 deletions core/src/client/composer-label.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
// The composer's label in a FlyBot chat (#419, #499). The shell picks its placeholder by phase,
// before it knows whose chat this is: the new-session hero copy ("Describe what you want to
// build, …") on a first open and its coding copy ("Message or run a task, …") after a send. A
// FlyBot chat, a friend's Bot above all, always reads "Message <name>": the visible placeholder
// is --bt-placeholder (styles.js draws it over the shell's text), and the input's aria-label and
// data-placeholder are put back whenever the shell renders them again. The mutation callback
// rewrites them in the same task, so no frame paints the shell's copy.
const ATTRIBUTES = ['aria-label', 'data-placeholder']

// Holds `label` on every composer input in `doc` until the returned function is called, which
// stops watching and gives each input the shell's latest own value back.
export function holdComposerLabel(doc, label) {
doc.body.style.setProperty('--bt-placeholder', JSON.stringify(label))
// The shell's own value per input and attribute, newest first, for the hand-back.
const shell = new Map()
const apply = () => {
for (const el of doc.querySelectorAll('[data-composer-input]')) {
for (const name of ATTRIBUTES) {
const current = el.getAttribute(name)
if (current === label) continue
if (!shell.has(el)) shell.set(el, {})
shell.get(el)[name] = current
el.setAttribute(name, label)
}
}
}
apply()
const Observer = doc.defaultView?.MutationObserver ?? globalThis.MutationObserver
const watcher = typeof Observer === 'function' ? new Observer(apply) : null
watcher?.observe(doc.body, { subtree: true, childList: true, attributes: true, attributeFilter: ATTRIBUTES })
return () => {
watcher?.disconnect()
for (const [el, values] of shell) {
if (!el.isConnected) continue
for (const [name, value] of Object.entries(values)) {
if (value == null) el.removeAttribute(name)
else el.setAttribute(name, value)
}
}
}
}
Loading
Loading