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/645-silent-creation.changed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Making a Bot or a group chat no longer starts a turn: a new Bot is sent no greeting and waits in the sidebar until you write to it or a parent Bot messages it, a new group posts no opening round, and picking a Bot's model applies it without speaking to the Bot. Text you type in the composer before creating a Bot is sent to it as your first message, so it answers you, and it is never saved as the Bot's instructions; making a team costs no model turn ([#645](https://github.com/ThinkFlowLab/FlyBot/issues/645)).
20 changes: 10 additions & 10 deletions core/client.js
Original file line number Diff line number Diff line change
Expand Up @@ -5806,7 +5806,7 @@ function TriggerCell({ node, sessionId, useRoster, useSessions, useTurnClock, us
}
if (source.kind === "bot") {
if (source.handoff === true) return roster.roomsById[sessionId] ? null : (0, import_react8.createElement)(EarlierParts, { sessionId, roster, actions });
if (source.role === "kickoff" || source.role === "room") return null;
if (source.role === "room") return null;
if (source.role === "mention") return (0, import_react8.createElement)("div", { className: "bt-event" }, (0, import_react8.createElement)("span", null, contentText(data.content)));
if (source.role === "secret" || deferred) return null;
if (source.role === "post") {
Expand Down Expand Up @@ -11306,11 +11306,10 @@ function startModel(roster) {
const main = roster.byId?.[roster.mainBotId] ?? (roster.bots ?? []).find((bot) => bot.id === roster.mainBotId);
return (served(main?.model) ?? served(main?.appliedModel) ?? served(roster.defaultModel) ?? models[0])?.ref ?? "";
}
function createPayload({ name, look, image, model, effort, brief }) {
function createPayload({ name, look, image, model, effort }) {
const payload = { name, color: look.color, avatar: image ? { shape: look.shape, image } : { shape: look.shape } };
if (model) payload.model = model;
if (effort) payload.effort = effort;
if (brief) payload.brief = brief;
return payload;
}

Expand All @@ -11322,7 +11321,7 @@ var randomLook = (avoid = {}) => {
const colors = DEFAULT_COLORS.filter((color) => color !== avoid.color);
return { shape: pick(shapes), color: pick(colors) };
};
function CreateBot({ roster, actions, start, brief = "", onBack, canGoBack, onCreated, inline = false }) {
function CreateBot({ roster, actions, start, onBack, canGoBack, onCreated, inline = false }) {
const [look, setLook] = (0, import_react26.useState)(() => randomLook());
const [image, setImage] = (0, import_react26.useState)(null);
const [name, setName] = (0, import_react26.useState)(start?.name ?? "");
Expand All @@ -11345,8 +11344,8 @@ function CreateBot({ roster, actions, start, brief = "", onBack, canGoBack, onCr
setError("");
setBusy(true);
try {
const bot = await actions.createBot(createPayload({ name: finalName, look, image, model, effort, brief }));
onCreated(bot);
const bot = await actions.createBot(createPayload({ name: finalName, look, image, model, effort }));
await onCreated(bot);
} catch (failure) {
setError(failure?.message ?? String(failure));
setBusy(false);
Expand Down Expand Up @@ -11561,7 +11560,9 @@ function NewChat({ mode, roster, actions }) {
setError("");
setDraft({ name: query.trim() !== "" && !exact ? query.trim() : "", fromList: true });
};
const created = (bot) => {
const created = async (bot) => {
const text = message.trim();
if (text) await actions.send(bot.id, text);
close();
actions.openSession(bot.id);
actions.showDetails(bot.id, "settings");
Expand All @@ -11574,7 +11575,7 @@ function NewChat({ mode, roster, actions }) {
...members.map((id) => ({ kind: "bot", id, role: id === admin ? "admin" : "member" })),
...remotes
];
const room2 = await actions.createRoom(members, { admin, ...participants ? { participants } : {}, ...text ? {} : { greet: true } });
const room2 = await actions.createRoom(members, { admin, ...participants ? { participants } : {} });
if (text) await actions.send(room2.id, text);
actions.openSession(room2.id);
close();
Expand Down Expand Up @@ -11662,7 +11663,6 @@ ${listed}`, { axis: "y", ...QUICK_EDGES });
roster,
actions,
start: draft,
brief: message.trim(),
canGoBack: draft.fromList,
onBack: draft.fromList ? () => {
setDraft(null);
Expand Down Expand Up @@ -12466,7 +12466,7 @@ function NewGroupForm({ roster, actions, run, onCreated, onCancel }) {
if (busy || admin === null || members.length === 0) return;
setBusy(true);
let room2;
const done = await run(actions.createRoom([admin, ...members], { admin, greet: true, ...name.trim() ? { name: name.trim() } : {} }).then((value) => {
const done = await run(actions.createRoom([admin, ...members], { admin, ...name.trim() ? { name: name.trim() } : {} }).then((value) => {
room2 = value;
}));
if (done && room2) onCreated(room2);
Expand Down
10 changes: 4 additions & 6 deletions core/dev/smoke.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -169,18 +169,16 @@ await step('bootstrap: Chief waits for a model', () => api('state'), (value, cal
const bots = Object.values(current.bots)
models = value.models ?? []
return expect(bots.length === 1 && bots[0].name === 'Chief' && current.mainBotIds[0] === bots[0].id, 'expected one Main Bot named Chief')
?? expect(bots[0].model === undefined && bots[0].kickoff !== undefined, 'Chief should hold its greeting until a model is picked')
?? expect(value.bots[0].waiting === true, 'the view should show Chief as waiting')
?? expect(bots[0].model === undefined, 'Chief should have no model until one is picked')
?? expect(calls.length === 0, `Chief called the model ${calls.length} times before it had one`)
?? expect(models.length > 0, 'no models listed for the model card')
})

await step('user picks a model; Chief greets and asks in its reply', () => api('update-bot', { id: chief().id, model: models.at(-1).ref }), (_value, calls) => {
await step('user picks a model; nothing is sent', () => api('update-bot', { id: chief().id, model: models.at(-1).ref }), (_value, calls) => {
const current = state()
return expect(chief().model === models.at(-1).ref, `Chief runs on ${chief().model}`)
?? expect(chief().kickoff === undefined, 'the greeting is still held')
?? expect(!current.questions?.[chief().sessionId ?? chief().id], 'the greeting should not leave a question card')
?? expect(saidText(calls, /先让团队帮你做哪块/), 'expected Chief to ask in its reply')
?? expect(!current.questions?.[chief().sessionId ?? chief().id], 'picking a model should leave no question card')
?? expect(calls.length === 0, `Chief called the model ${calls.length} times when its model was picked`)
})

await step('Chief creates Writer, who replies', () => send(chief().id, '创建一个叫Writer的bot'), () => {
Expand Down
7 changes: 4 additions & 3 deletions core/src/client/bot-draft.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,12 @@ export function startModel(roster) {
return (served(main?.model) ?? served(main?.appliedModel) ?? served(roster.defaultModel) ?? models[0])?.ref ?? ''
}

// The model is always sent: the host fixes it on the Bot at creation.
export function createPayload({ name, look, image, model, effort, brief }) {
// The model is always sent: the host fixes it on the Bot at creation. The card never sends a
// brief: a line typed in the composer is the Bot's first request, and the host takes a brief
// as instructions only from a Bot that creates another (#645).
export function createPayload({ name, look, image, model, effort }) {
const payload = { name, color: look.color, avatar: image ? { shape: look.shape, image } : { shape: look.shape } }
if (model) payload.model = model
if (effort) payload.effort = effort
if (brief) payload.brief = brief
return payload
}
2 changes: 1 addition & 1 deletion core/src/client/cells.js
Original file line number Diff line number Diff line change
Expand Up @@ -568,7 +568,7 @@ export function TriggerCell({ node, sessionId, useRoster, useSessions, useTurnCl
// A part opens with its handoff package, so the earlier parts go right above it.
if (source.handoff === true) return roster.roomsById[sessionId] ? null : h(EarlierParts, { sessionId, roster, actions })
// Group turns belong to the group chat; the member's own chat stays one-to-one.
if (source.role === 'kickoff' || source.role === 'room') return null
if (source.role === 'room') return null
// The host's receipt for the user's @ of a friend's Bot in this group (#80):
// where the message went, or why it did not leave.
if (source.role === 'mention') return h('div', { className: 'bt-event' }, h('span', null, contentText(data.content)))
Expand Down
6 changes: 3 additions & 3 deletions core/src/client/create-bot.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ const randomLook = (avoid = {}) => {

// The new-Bot form: look first, then name and model. Nothing exists until Create.
// `inline` drops the pane's header for a form shown inside Settings.
export function CreateBot({ roster, actions, start, brief = '', onBack, canGoBack, onCreated, inline = false }) {
export function CreateBot({ roster, actions, start, onBack, canGoBack, onCreated, inline = false }) {
const [look, setLook] = useState(() => randomLook())
const [image, setImage] = useState(null)
const [name, setName] = useState(start?.name ?? '')
Expand All @@ -40,8 +40,8 @@ export function CreateBot({ roster, actions, start, brief = '', onBack, canGoBac
setError('')
setBusy(true)
try {
const bot = await actions.createBot(createPayload({ name: finalName, look, image, model, effort, brief }))
onCreated(bot)
const bot = await actions.createBot(createPayload({ name: finalName, look, image, model, effort }))
await onCreated(bot)
} catch (failure) {
setError(failure?.message ?? String(failure))
setBusy(false)
Expand Down
2 changes: 1 addition & 1 deletion core/src/client/group-settings.js
Original file line number Diff line number Diff line change
Expand Up @@ -235,7 +235,7 @@ export function NewGroupForm({ roster, actions, run, onCreated, onCancel }) {
if (busy || admin === null || members.length === 0) return
setBusy(true)
let room
const done = await run(actions.createRoom([admin, ...members], { admin, greet: true, ...(name.trim() ? { name: name.trim() } : {}) }).then((value) => { room = value }))
const done = await run(actions.createRoom([admin, ...members], { admin, ...(name.trim() ? { name: name.trim() } : {}) }).then((value) => { room = value }))
if (done && room) onCreated(room)
else setBusy(false)
}
Expand Down
2 changes: 1 addition & 1 deletion core/src/client/model-menu.js
Original file line number Diff line number Diff line change
Expand Up @@ -184,7 +184,7 @@ export function ModelMenu({ bot, roster, actions, run, wide = false, onPick, onP
}

// The card a Bot's chat opens on while the Bot has no model, or has one that is no
// longer served. The first Main Bot greets the user only after a pick.
// longer served. The first Main Bot starts no turn until a model is picked.
export function ModelChoiceDock({ sessionId, useRoster, actions }) {
const roster = useRoster(value => value)
const bot = roster.byId[sessionId]
Expand Down
12 changes: 8 additions & 4 deletions core/src/client/new-chat.js
Original file line number Diff line number Diff line change
Expand Up @@ -110,9 +110,13 @@ export function NewChat({ mode, roster, actions }) {
setError('')
setDraft({ name: query.trim() !== '' && !exact ? query.trim() : '', fromList: true })
}
// Close first: closing the new-chat pane keeps the details panel, while closing
// A line typed in the composer was a request, so it is the new Bot's first turn, not its
// instructions (#645): the card is handed no brief, and the line is sent once the Bot
// exists. Close first: closing the new-chat pane keeps the details panel, while closing
// nothing would clear it.
const created = (bot) => {
const created = async (bot) => {
const text = message.trim()
if (text) await actions.send(bot.id, text)
close()
actions.openSession(bot.id)
actions.showDetails(bot.id, 'settings')
Expand All @@ -127,7 +131,7 @@ export function NewChat({ mode, roster, actions }) {
...members.map(id => ({ kind: 'bot', id, role: id === admin ? 'admin' : 'member' })),
...remotes,
]
const room = await actions.createRoom(members, { admin, ...(participants ? { participants } : {}), ...(text ? {} : { greet: true }) })
const room = await actions.createRoom(members, { admin, ...(participants ? { participants } : {}) })
if (text) await actions.send(room.id, text)
actions.openSession(room.id)
close()
Expand Down Expand Up @@ -197,7 +201,7 @@ export function NewChat({ mode, roster, actions }) {
if (draft) {
return h('div', { className: 'bt-pane', style: { left: paneLeft } },
h(CreateBot, {
roster, actions, start: draft, brief: message.trim(), canGoBack: draft.fromList,
roster, actions, start: draft, canGoBack: draft.fromList,
onBack: draft.fromList ? () => { setDraft(null); setTimeout(() => inputRef.current?.focus()) } : close,
onCreated: created,
}))
Expand Down
2 changes: 1 addition & 1 deletion core/src/client/text.js
Original file line number Diff line number Diff line change
Expand Up @@ -178,7 +178,7 @@ export function toolArgs(root) {
}

// A relay context is a Bot-to-Bot request, reply or stop notice injected into the
// model's history. ds-bot's own package sources (kickoff, room, secret, handoff
// model's history. ds-bot's own package sources (room, secret, handoff
// parts) are not events in the chat, nor is a request handed back after its line was
// already shown: deferred, resumed after a restart, or retried after a tool call
// written as text.
Expand Down
5 changes: 1 addition & 4 deletions core/src/host/channel.js
Original file line number Diff line number Diff line change
Expand Up @@ -124,8 +124,7 @@ export function install(rt) {
mainBotId: state.mainBotIds[0] ?? null,
mainBotIds: state.mainBotIds,
// `sessionId` is the Bot's current own-chat part; `parts` its earlier ones.
// `waiting`: the Bot holds its greeting until the user picks its model.
bots: Object.values(state.bots).map(({ segments, kickoff, ...bot }) => {
bots: Object.values(state.bots).map(({ segments, ...bot }) => {
// A friend's chat, and a local Bot's own held sends (#371): the provider says which.
const pending = rt.listChatApprovals?.(bot, actor) ?? []
// A local Bot's held replies to friends (#418), for its owner only: drawn on the turns they answer,
Expand All @@ -141,7 +140,6 @@ export function install(rt) {
parts: (segments ?? []).map(segment => segment.sessionId),
pendingCount: pending.length + waitingReplies,
...(held.length ? { heldReplies: held } : {}),
...(kickoff ? { waiting: true } : {}),
}
}),
// `parts`: the members' hidden group Sessions and their earlier segments, for usage.
Expand Down Expand Up @@ -348,7 +346,6 @@ export function install(rt) {
...(typeof body.admin === 'string' ? { admin: body.admin } : {}),
...(Array.isArray(body.participants) ? { participants: body.participants } : {}),
})
if (body.greet === true) await rt.greetRoom(room.id).catch(error => rt.warn('ds-bot: greeting in %s failed: %s', room.name, error))
return ok(room)
}
case 'update-room': {
Expand Down
1 change: 0 additions & 1 deletion core/src/host/delivery.js
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,6 @@ export function install(rt) {
if (role === 'stopped') return `[Stopped by ${fromName}]`
if (role === 'room') return `[Group chat "${roomName}" · ${note}]`
if (role === 'report') return `[Group chat "${roomName}" · replies to your post]`
if (role === 'kickoff') return '[Bot team setup]'
if (role === 'secret') return '[Secret card]'
if (audience === 'user') return `[Message from ${fromName} · the user sees your reply here]`
return `[Message from ${fromName}]`
Expand Down
Loading
Loading