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/1892-pool-no-dossier.changed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Project board: a role-pool entry in the team block reads `@id` beside its rank instead of a Dossier control — `project-team` answers rows keyed by the Project's own Bots, so the old pool click opened a dossier the host never sent; the Chief and my-agent rows keep theirs ([#1892](https://github.com/ThinkFlowLab/FlyBot/issues/1892)).
13 changes: 1 addition & 12 deletions core/client.js
Original file line number Diff line number Diff line change
Expand Up @@ -10966,18 +10966,7 @@ var teamRow = (role, label, botId, onOpenBot, onOpenDossier) => (0, import_react
var POOL_RANKS = [["owner", "owner"], ["maintainer", "maintainers"], ["member", "members"]];
function ProjectTeam({ project, onOpenBot, onOpenDossier }) {
const view = projectTeamView(project);
const poolId = (id) => (0, import_react19.createElement)("button", {
key: id,
type: "button",
className: "bt-project-team-dossier",
"data-dossier-role": "pool",
"data-dossier-bot": id,
"aria-label": t("Open {name}'s dossier", { name: id }),
onClick: (event) => {
event?.stopPropagation();
onOpenDossier?.({ role: "pool", label: t("Role pool"), botId: id });
}
}, `@${id}`);
const poolId = (id) => (0, import_react19.createElement)("span", { key: id }, `@${id}`);
const ranks = view.empty ? (0, import_react19.createElement)("span", { className: "bt-project-missing" }, t("No one in this pool yet.")) : (0, import_react19.createElement)(
"span",
{ className: "bt-project-team-roles" },
Expand Down
10 changes: 4 additions & 6 deletions core/src/client/project-board.js
Original file line number Diff line number Diff line change
Expand Up @@ -614,12 +614,10 @@ const POOL_RANKS = [['owner', 'owner'], ['maintainer', 'maintainers'], ['member'

export function ProjectTeam({ project, onOpenBot, onOpenDossier }) {
const view = projectTeamView(project)
const poolId = (id) => h('button', {
key: id, type: 'button', className: 'bt-project-team-dossier',
'data-dossier-role': 'pool', 'data-dossier-bot': id,
'aria-label': t('Open {name}\'s dossier', { name: id }),
onClick: (event) => { event?.stopPropagation(); onOpenDossier?.({ role: 'pool', label: t('Role pool'), botId: id }) },
}, `@${id}`)
// #1892: a pool entry is a rank, not a Bot this board holds a dossier answer for — `project-team`
// answers rows keyed by the Project's own Bots, so the old control opened a row the host never
// sent. The Chief and my-agent rows above keep theirs.
const poolId = (id) => h('span', { key: id }, `@${id}`)
const ranks = view.empty
? h('span', { className: 'bt-project-missing' }, t('No one in this pool yet.'))
: h('span', { className: 'bt-project-team-roles' },
Expand Down
35 changes: 35 additions & 0 deletions core/test/project-board-panels.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -537,6 +537,41 @@ test('the board reads the dossier from exactly one project-team call', async ()
assert.deepEqual(opened.map(entry => entry.botId), ['bot-chief'])
})

// #1892: the role pool is a rank list, not a Bot the board holds a dossier answer for.
// `project-team` answers rows keyed by the Project's own Bots, so a pool click asked for a row the
// host never sent and drew an empty panel. A pool entry now reads `@id` beside its rank; the Chief
// and my-agent rows keep the Dossier control they have had since #1864.
test('#1892: a role-pool entry reads @id and its rank, not a Dossier control', () => {
resetSlots()
const opened = []
const tree = render(ProjectBoardPane, { project: team(), rows, canManage: false, onOpenDossier: value => opened.push(value) })
const pool = teamRow(tree, 'pool')
assert.match(text(pool), /@bot-maint/, 'the pool lists the maintainer the record stores')
assert.match(text(pool), /@bot-member/, 'and the member')
assert.match(text(pool), /Owner:|Maintainer:|Member:/, 'each entry keeps its rank label')
assert.match(text(pool), /My role/, "and the caller's own rank line stays beside them")

const poolNodes = all(pool)
const dossierish = (node) => {
const className = String(node.props?.className ?? '').split(/\s+/)
return node.props?.['data-dossier-role'] !== undefined
|| node.props?.['data-dossier-bot'] !== undefined
|| className.includes('bt-project-team-dossier')
|| /dossier/i.test(String(node.props?.['aria-label'] ?? ''))
}
assert.deepEqual(poolNodes.filter(dossierish).map(node => text(node)), [], 'no pool entry carries a Dossier control')
for (const node of poolNodes) node.props?.onClick?.()
assert.deepEqual(opened.filter(entry => entry.role === 'pool'), [], 'and no pool id ever asks for a dossier')

const chief = all(tree).find(node => node.props?.['data-dossier-bot'] === 'bot-chief')
const agent = all(tree).find(node => node.props?.['data-dossier-bot'] === 'bot-assistant')
assert.ok(chief, 'the Chief row keeps its dossier control')
assert.ok(agent, 'my agent keeps its dossier control')
chief.props.onClick()
agent.props.onClick()
assert.deepEqual(opened.map(entry => entry.role), ['chief', 'agent'], 'and each still asks for its own row')
})

test('the dossier panel draws Persona, Memory and Skills, and stays empty for a null row', () => {
resetSlots()
const drawn = text(render(ProjectDossierPanel, { row: dossier(), label: 'Project Chief' }))
Expand Down
8 changes: 5 additions & 3 deletions docs/subsystems/core-client.md
Original file line number Diff line number Diff line change
Expand Up @@ -316,14 +316,16 @@ spawn failed — stays drawn and reads *Not set*, so this slice never waits on #
live in `src/client/role-labels.js`, which the sidebar and this block both print. Tests:
`core/test/project-board-panels.test.js`.

The **dossier** (#1864) is the read-only half beside those rows. Each team row carries a `Dossier`
control (`data-dossier-bot`, also for a pool member), and `ProjectDossierPanel` draws three sections
The **dossier** (#1864) is the read-only half beside those rows. Each Bot row carries a `Dossier`
control (`data-dossier-bot`), and `ProjectDossierPanel` draws three sections
from the one `project-team` answer the overlay reads with the board: **Persona** (the Bot's
instructions, role and model), **Memory** (the `MEMORY.md` head and the topic names) and **Skills**
(the attached `skill` capability names). The panel never re-fetches per section, and a `null` row —
the host's answer for a deleted Bot — shows *Not set* with nothing invented. `readProjectBoard`
keeps the dossier on the same read as the rows, so the board and the panel cannot disagree, and the
"my agent" row uses the same panel. The entry in the owner's own Bot chat is
"my agent" row uses the same panel. A role-pool entry draws none: since #1892 it is plain `@id`
beside its rank, because `project-team` answers rows keyed by the Project's own Bots and the old
control asked for a row the host never sent. The entry in the owner's own Bot chat is
`ProjectAgentEntry` in the header: the roster already carries `bot.projectId`, the entry reads
`project-read` once for `assistant.botId` and opens that Session through `actions.openSession`; the
assistant is not a special case of the board, and the Project page keeps its own row. Tests:
Expand Down
54 changes: 22 additions & 32 deletions patches/ds-bot/0008-client-bundle.patch
Original file line number Diff line number Diff line change
Expand Up @@ -7527,22 +7527,11 @@ diff --git a/client.js b/client.js
+ setBusy("");
}
- });
+ };
+ return (0, import_react9.createElement)(
+ "div",
+ { className: "bt-file-chips" },
+ files.map((file) => (0, import_react9.createElement)(
+ "button",
+ { key: file.id, type: "button", className: "bt-file-chip", disabled: busy === file.id, title: `Download ${file.name} (untrusted file from a remote Bot)`, onClick: () => download2(file) },
+ `📎 ${file.name} · ${fileSize(file.size)}`
+ )),
+ error ? (0, import_react9.createElement)("span", { className: "bt-file-error", role: "alert" }, error) : null
+ );
}
-}
-function ScopeSwitch({ value, onChange, bot }) {
- const options = [["all", t("All Bots")], ["bot", t("Only {name}", { name: bot.name })]];
- return (0, import_react5.createElement)(
+function GroupEvent({ prefix, room: room2, name, roster, actions }) {
+ };
+ return (0, import_react9.createElement)(
"div",
- { className: "bt-secret-scope", role: "radiogroup", "aria-label": t("Who may use it") },
Expand All @@ -7555,6 +7544,18 @@ diff --git a/client.js b/client.js
- className: "bt-secret-seg",
- onClick: () => onChange(id)
- }, text))
+ { className: "bt-file-chips" },
+ files.map((file) => (0, import_react9.createElement)(
+ "button",
+ { key: file.id, type: "button", className: "bt-file-chip", disabled: busy === file.id, title: `Download ${file.name} (untrusted file from a remote Bot)`, onClick: () => download2(file) },
+ `📎 ${file.name} · ${fileSize(file.size)}`
+ )),
+ error ? (0, import_react9.createElement)("span", { className: "bt-file-error", role: "alert" }, error) : null
+ );
+}
+function GroupEvent({ prefix, room: room2, name, roster, actions }) {
+ return (0, import_react9.createElement)(
+ "div",
+ { className: "bt-event" },
+ (0, import_react9.createElement)("span", null, prefix),
+ room2 ? (0, import_react9.createElement)("button", { type: "button", onClick: () => actions.openSession(room2.id) }, (0, import_react9.createElement)(RoomAvatar, { room: room2, roster, size: 16 }), room2.name) : name ? (0, import_react9.createElement)("span", null, name) : null
Expand Down Expand Up @@ -9032,10 +9033,6 @@ diff --git a/client.js b/client.js
+ const approvals = askedIn(room2, work, id, doing);
+ return { id, approvals, doing, here: doing !== void 0, state: approvals.length > 0 ? "ask" : doing ? "work" : "idle" };
+ });
+}
+function busyMembers(room2, work, roster) {
+ const busy = memberWork(room2, work).filter((entry) => entry.state !== "idle" && roster.byId[entry.id]);
+ return [...busy.filter((entry) => entry.state === "ask"), ...busy.filter((entry) => entry.state === "work")].map((entry) => ({ ...entry, bot: roster.byId[entry.id] }));
}
-function EventLine({ prefix, bots, joiner = t("and"), onOpen, whole = false }) {
- if (whole && bots.length === 1) {
Expand All @@ -9061,6 +9058,10 @@ diff --git a/client.js b/client.js
- (0, import_react6.createElement)(BotMark, { bot, size: 16 }),
- bot.name
- ));
+function busyMembers(room2, work, roster) {
+ const busy = memberWork(room2, work).filter((entry) => entry.state !== "idle" && roster.byId[entry.id]);
+ return [...busy.filter((entry) => entry.state === "ask"), ...busy.filter((entry) => entry.state === "work")].map((entry) => ({ ...entry, bot: roster.byId[entry.id] }));
+}
+function splitBusy(busy) {
+ const here = busy.filter((entry) => entry.state === "work" && entry.here);
+ const rows = [...here.filter((entry) => entry.doing.speaking === true), ...here.filter((entry) => entry.doing.speaking !== true)].slice(0, ROWS_MAX);
Expand Down Expand Up @@ -9853,7 +9854,7 @@ diff --git a/client.js b/client.js
+ const custom = typeof reply.custom === "string" ? reply.custom.trim() : "";
+ if (selected.length === 0 && custom === "") return void 0;
+ return Promise.resolve(actions.answerQuestion(card.sessionId, card.question.id, { selected, custom }));
+ }
}
+ if (card?.kind === "task" && card.status === "input") {
+ const text2 = typeof choice === "string" ? choice.trim() : "";
+ const questionId = card.task?.inputRequest?.id;
Expand All @@ -9865,7 +9866,7 @@ diff --git a/client.js b/client.js
+ const note = choice !== null && typeof choice === "object" ? String(choice.note ?? "").trim() : "";
+ if (note === "") return void 0;
+ return Promise.resolve(actions.sendBackTask(card.task.id, note));
}
+ }
+ if (card?.kind === "approval" && card.approval?.answerable === true) {
+ return Promise.resolve(actions.answerApproval(card.approval.id, choice));
+ }
Expand Down Expand Up @@ -12247,7 +12248,7 @@ diff --git a/client.js b/client.js
setOpen(false);
};
const onKey = (event) => {
@@ -5097,412 +5097,1340 @@ function UpdateButton({ useUpdate, actions }) {
@@ -5097,412 +5097,1329 @@ function UpdateButton({ useUpdate, actions }) {
window.removeEventListener("keydown", onKey, true);
};
}, [open]);
Expand Down Expand Up @@ -13280,18 +13281,7 @@ diff --git a/client.js b/client.js
+var POOL_RANKS = [["owner", "owner"], ["maintainer", "maintainers"], ["member", "members"]];
+function ProjectTeam({ project, onOpenBot, onOpenDossier }) {
+ const view = projectTeamView(project);
+ const poolId = (id) => (0, import_react19.createElement)("button", {
+ key: id,
+ type: "button",
+ className: "bt-project-team-dossier",
+ "data-dossier-role": "pool",
+ "data-dossier-bot": id,
+ "aria-label": t("Open {name}'s dossier", { name: id }),
+ onClick: (event) => {
+ event?.stopPropagation();
+ onOpenDossier?.({ role: "pool", label: t("Role pool"), botId: id });
+ }
+ }, `@${id}`);
+ const poolId = (id) => (0, import_react19.createElement)("span", { key: id }, `@${id}`);
+ const ranks = view.empty ? (0, import_react19.createElement)("span", { className: "bt-project-missing" }, t("No one in this pool yet.")) : (0, import_react19.createElement)(
+ "span",
+ { className: "bt-project-team-roles" },
Expand Down
10 changes: 4 additions & 6 deletions patches/ds-bot/files/src/client/project-board.js
Original file line number Diff line number Diff line change
Expand Up @@ -614,12 +614,10 @@ const POOL_RANKS = [['owner', 'owner'], ['maintainer', 'maintainers'], ['member'

export function ProjectTeam({ project, onOpenBot, onOpenDossier }) {
const view = projectTeamView(project)
const poolId = (id) => h('button', {
key: id, type: 'button', className: 'bt-project-team-dossier',
'data-dossier-role': 'pool', 'data-dossier-bot': id,
'aria-label': t('Open {name}\'s dossier', { name: id }),
onClick: (event) => { event?.stopPropagation(); onOpenDossier?.({ role: 'pool', label: t('Role pool'), botId: id }) },
}, `@${id}`)
// #1892: a pool entry is a rank, not a Bot this board holds a dossier answer for — `project-team`
// answers rows keyed by the Project's own Bots, so the old control opened a row the host never
// sent. The Chief and my-agent rows above keep theirs.
const poolId = (id) => h('span', { key: id }, `@${id}`)
const ranks = view.empty
? h('span', { className: 'bt-project-missing' }, t('No one in this pool yet.'))
: h('span', { className: 'bt-project-team-roles' },
Expand Down
35 changes: 35 additions & 0 deletions patches/ds-bot/files/test/project-board-panels.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -537,6 +537,41 @@ test('the board reads the dossier from exactly one project-team call', async ()
assert.deepEqual(opened.map(entry => entry.botId), ['bot-chief'])
})

// #1892: the role pool is a rank list, not a Bot the board holds a dossier answer for.
// `project-team` answers rows keyed by the Project's own Bots, so a pool click asked for a row the
// host never sent and drew an empty panel. A pool entry now reads `@id` beside its rank; the Chief
// and my-agent rows keep the Dossier control they have had since #1864.
test('#1892: a role-pool entry reads @id and its rank, not a Dossier control', () => {
resetSlots()
const opened = []
const tree = render(ProjectBoardPane, { project: team(), rows, canManage: false, onOpenDossier: value => opened.push(value) })
const pool = teamRow(tree, 'pool')
assert.match(text(pool), /@bot-maint/, 'the pool lists the maintainer the record stores')
assert.match(text(pool), /@bot-member/, 'and the member')
assert.match(text(pool), /Owner:|Maintainer:|Member:/, 'each entry keeps its rank label')
assert.match(text(pool), /My role/, "and the caller's own rank line stays beside them")

const poolNodes = all(pool)
const dossierish = (node) => {
const className = String(node.props?.className ?? '').split(/\s+/)
return node.props?.['data-dossier-role'] !== undefined
|| node.props?.['data-dossier-bot'] !== undefined
|| className.includes('bt-project-team-dossier')
|| /dossier/i.test(String(node.props?.['aria-label'] ?? ''))
}
assert.deepEqual(poolNodes.filter(dossierish).map(node => text(node)), [], 'no pool entry carries a Dossier control')
for (const node of poolNodes) node.props?.onClick?.()
assert.deepEqual(opened.filter(entry => entry.role === 'pool'), [], 'and no pool id ever asks for a dossier')

const chief = all(tree).find(node => node.props?.['data-dossier-bot'] === 'bot-chief')
const agent = all(tree).find(node => node.props?.['data-dossier-bot'] === 'bot-assistant')
assert.ok(chief, 'the Chief row keeps its dossier control')
assert.ok(agent, 'my agent keeps its dossier control')
chief.props.onClick()
agent.props.onClick()
assert.deepEqual(opened.map(entry => entry.role), ['chief', 'agent'], 'and each still asks for its own row')
})

test('the dossier panel draws Persona, Memory and Skills, and stays empty for a null row', () => {
resetSlots()
const drawn = text(render(ProjectDossierPanel, { row: dossier(), label: 'Project Chief' }))
Expand Down
Loading