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/1182-task-lead.changed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
A group picks one task lead to divide the board. That Bot, or the admin when none is picked, assigns each part; the card is claimed and the assignee starts it in the group without waiting for a confirmation. A question stays in the group ([#1182](https://github.com/ThinkFlowLab/FlyBot/issues/1182)).
10 changes: 9 additions & 1 deletion core/client.js
Original file line number Diff line number Diff line change
Expand Up @@ -393,6 +393,10 @@ var ZH = {
"Leave the group without an admin": "让群聊不设管理员",
Unset: "取消",
"Make admin": "设为管理员",
"Task lead": "总负责人",
"Make task lead": "设为总负责人",
"Use the admin": "改由管理员负责",
"The task lead divides the board. When unset, the admin does.": "总负责人拆分看板任务。未指定时由管理员负责。",
// Face room lead (#476)
"Room lead": "总负责人",
Lead: "总负责人",
Expand Down Expand Up @@ -12589,6 +12593,8 @@ function GroupSettingsForm({ room: room2, roster, actions, run, onDeleted }) {
)),
members.map((member) => {
const leads = room2.admin === member.id;
const boardHere = room2.origin !== "face-to-face" && room2.origin !== "inbound-share";
const taskLead = room2.taskLead === member.id;
return (0, import_react27.createElement)(
"div",
{ key: member.id, className: "bt-set-row bt-bs-row bt-gs-member" },
Expand All @@ -12598,9 +12604,11 @@ function GroupSettingsForm({ room: room2, roster, actions, run, onDeleted }) {
(0, import_react27.createElement)(BotAvatar, { bot: member, size: 26, main: isMainOf(roster, member.id), admin: leads, live: false }),
(0, import_react27.createElement)("span", { className: "bt-row-title" }, member.name),
leads ? (0, import_react27.createElement)("span", { className: "bt-tag" }, t("Admin")) : null,
taskLead ? (0, import_react27.createElement)("span", { className: "bt-tag" }, t("Task lead")) : null,
(roster.humans ?? []).some((person) => person.designatedChiefId === member.id) ? (0, import_react27.createElement)("span", { className: "bt-tag" }, t("Chief")) : null
),
leads ? (0, import_react27.createElement)("button", { type: "button", className: "bt-mini", title: t("Leave the group without an admin"), onClick: () => update({ admin: null }) }, t("Unset")) : (0, import_react27.createElement)("button", { type: "button", className: "bt-mini", onClick: () => update({ admin: member.id }) }, t("Make admin")),
boardHere ? taskLead ? (0, import_react27.createElement)("button", { type: "button", className: "bt-mini", onClick: () => update({ taskLead: null }) }, t("Use the admin")) : (0, import_react27.createElement)("button", { type: "button", className: "bt-mini", onClick: () => update({ taskLead: member.id }) }, t("Make task lead")) : null,
members.length > 2 ? (0, import_react27.createElement)("button", { type: "button", className: "bt-x", "aria-label": t("Remove {name} from the group", { name: member.name }), title: t("Remove from group"), onClick: () => update({ remove: [member.id] }) }, "×") : null
);
}),
Expand Down Expand Up @@ -12655,7 +12663,7 @@ function GroupSettingsForm({ room: room2, roster, actions, run, onDeleted }) {
"span",
{ className: "bt-set-hint" },
// The host checks for two members on every change, even a rename.
members.length + humans.length + remotes.length < 2 ? t("Add a Bot first: a group chat needs at least two.") : admin ? t("{name} leads this group: it can call members with @Name, change the group, and post here.", { name: admin.name }) : t("No admin yet. An admin can call members with @Name, change the group, and post here.")
members.length + humans.length + remotes.length < 2 ? t("Add a Bot first: a group chat needs at least two.") : `${admin ? t("{name} leads this group: it can call members with @Name, change the group, and post here.", { name: admin.name }) : t("No admin yet. An admin can call members with @Name, change the group, and post here.")}${room2.origin === "face-to-face" || room2.origin === "inbound-share" ? "" : ` ${t("The task lead divides the board. When unset, the admin does.")}`}`
)),
// #876: a plugin's rows for this room (Invite by link), only in a group that lives here. Last, after
// the note on who leads, so that note stays with the members it is about (#892).
Expand Down
10 changes: 8 additions & 2 deletions core/src/client/group-settings.js
Original file line number Diff line number Diff line change
Expand Up @@ -223,15 +223,21 @@ export function GroupSettingsForm({ room, roster, actions, run, onDeleted }) {
: null)),
members.map((member) => {
const leads = room.admin === member.id
const boardHere = room.origin !== 'face-to-face' && room.origin !== 'inbound-share'
const taskLead = room.taskLead === member.id
return h('div', { key: member.id, className: 'bt-set-row bt-bs-row bt-gs-member' },
h('button', { type: 'button', className: 'bt-gs-who', title: t("Open {name}'s chat", { name: member.name }), onClick: () => openBot(member.id) },
h(BotAvatar, { bot: member, size: 26, main: isMainOf(roster, member.id), admin: leads, live: false }),
h('span', { className: 'bt-row-title' }, member.name),
leads ? h('span', { className: 'bt-tag' }, t('Admin')) : null,
taskLead ? h('span', { className: 'bt-tag' }, t('Task lead')) : null,
(roster.humans ?? []).some(person => person.designatedChiefId === member.id) ? h('span', { className: 'bt-tag' }, t('Chief')) : null),
leads
? h('button', { type: 'button', className: 'bt-mini', title: t('Leave the group without an admin'), onClick: () => update({ admin: null }) }, t('Unset'))
: h('button', { type: 'button', className: 'bt-mini', onClick: () => update({ admin: member.id }) }, t('Make admin')),
boardHere ? (taskLead
? h('button', { type: 'button', className: 'bt-mini', onClick: () => update({ taskLead: null }) }, t('Use the admin'))
: h('button', { type: 'button', className: 'bt-mini', onClick: () => update({ taskLead: member.id }) }, t('Make task lead'))) : null,
members.length > 2 ? h('button', { type: 'button', className: 'bt-x', 'aria-label': t('Remove {name} from the group', { name: member.name }), title: t('Remove from group'), onClick: () => update({ remove: [member.id] }) }, '×') : null)
}),
remotes.map((person) => h('div', { key: person.handle || person.id, className: 'bt-set-row bt-bs-row bt-gs-member' },
Expand Down Expand Up @@ -267,8 +273,8 @@ export function GroupSettingsForm({ room, roster, actions, run, onDeleted }) {
h('div', { className: 'bt-set-row bt-gs-note' }, h('span', { className: 'bt-set-hint' },
// The host checks for two members on every change, even a rename.
members.length + humans.length + remotes.length < 2 ? t('Add a Bot first: a group chat needs at least two.')
: admin ? t('{name} leads this group: it can call members with @Name, change the group, and post here.', { name: admin.name })
: t('No admin yet. An admin can call members with @Name, change the group, and post here.'))),
: `${admin ? t('{name} leads this group: it can call members with @Name, change the group, and post here.', { name: admin.name })
: t('No admin yet. An admin can call members with @Name, change the group, and post here.')}${room.origin === 'face-to-face' || room.origin === 'inbound-share' ? '' : ` ${t('The task lead divides the board. When unset, the admin does.')}`}`)),
// #876: a plugin's rows for this room (Invite by link), only in a group that lives here. Last, after
// the note on who leads, so that note stays with the members it is about (#892).
room.origin === 'face-to-face' || room.origin === 'inbound-share' ? null : h(FlyBotRoomMembers, { room, roster, actions })),
Expand Down
4 changes: 4 additions & 0 deletions core/src/client/i18n.js
Original file line number Diff line number Diff line change
Expand Up @@ -360,6 +360,10 @@ const ZH = {
'Leave the group without an admin': '让群聊不设管理员',
Unset: '取消',
'Make admin': '设为管理员',
'Task lead': '总负责人',
'Make task lead': '设为总负责人',
'Use the admin': '改由管理员负责',
'The task lead divides the board. When unset, the admin does.': '总负责人拆分看板任务。未指定时由管理员负责。',
// Face room lead (#476)
'Room lead': '总负责人',
Lead: '总负责人',
Expand Down
1 change: 1 addition & 0 deletions core/src/host/channel.js
Original file line number Diff line number Diff line change
Expand Up @@ -356,6 +356,7 @@ export function install(rt) {
notice: body.notice,
mode: body.mode,
admin: body.admin,
taskLead: body.taskLead,
members: Array.isArray(body.members) ? body.members : undefined,
add: Array.isArray(body.add) ? body.add : undefined,
remove: Array.isArray(body.remove) ? body.remove : undefined,
Expand Down
4 changes: 3 additions & 1 deletion core/src/host/delivery.js
Original file line number Diff line number Diff line change
Expand Up @@ -165,7 +165,7 @@ export function install(rt) {
// group turn (groups.js). `fromRoomId`: the group whose Session the sender wrote
// from; the reply goes back to that Session (turns.js). `answeredIn`: the group where the user
// answered the Bot's question card about work handed over from there (tools.js, #550).
async function deliver({ fromId, toId, sessionId, text, role, hop = 0, replyTo, roomId, audience, note, exchangeId = randomUUID(), steer = false, deferred = false, resumed = false, retried = false, resent = false, askedBy, fromRoomId, answeredIn, fromName: givenName, untrusted = false, remote, attachments, origin: givenOrigin }) {
async function deliver({ fromId, toId, sessionId, text, role, hop = 0, replyTo, roomId, audience, note, exchangeId = randomUUID(), steer = false, deferred = false, resumed = false, retried = false, resent = false, askedBy, fromRoomId, answeredIn, fromName: givenName, untrusted = false, remote, attachments, origin: givenOrigin, taskDrive = false }) {
const from = rt.botOf(fromId)
const to = rt.botOf(toId)
if (to === undefined) throw new Error(`Unknown Bot ${toId}`)
Expand Down Expand Up @@ -200,6 +200,8 @@ export function install(rt) {
...(resumed ? { resumed: true } : {}),
...(retried ? { retried: true } : {}),
...(askedBy ? { askedBy } : {}),
// A board wake delivered as a room turn (#1182). A round's own room turn does not set this.
...(taskDrive ? { taskDrive: true } : {}),
...(fromRoomId ? { senderRoomId: fromRoomId } : {}),
// A remote member's message (FlyBot): the chat draws it as that member's bubble.
// `typedBy`: the plugin has the server's attestation that this friend typed the line themselves.
Expand Down
15 changes: 14 additions & 1 deletion core/src/host/groups.js
Original file line number Diff line number Diff line change
Expand Up @@ -180,7 +180,6 @@ const adminNote = room => isFaceRoom(room) ? FACE_LEAD_NOTE : [
'You are this group\'s admin.',
room.mode === 'admin' ? 'Plain messages come to you first: answer them, or call the members who should answer with @Name.' : '',
'"@all" calls every member. When the user asks, change the group with update_group.',
'When the group is given a job, record one overall task with track_tasks and a subtask for each Bot that has a part.',
].filter(Boolean).join(' ')

const ROOM_READ_MAX = 30
Expand Down Expand Up @@ -277,6 +276,19 @@ export function install(rt) {
].filter(Boolean).join(' ')
}

// Who divides the board (#1182). One Bot: the picked task lead, or the admin when
// none is picked. Everyone else does the card it was given and does not re-plan.
function taskBoardNote(room, member) {
if (isFaceRoom(room)) return ''
const leadId = typeof rt.taskLeadOf === 'function' ? rt.taskLeadOf(room) : undefined
if (!leadId) return 'This room has no task lead on the board. Do not divide the work; the user or a Main Bot picks one task lead.'
if (leadId === member.id) {
return 'You are this room\'s task lead (总负责人) for the board. When the group is given a job, record one overall task with track_tasks and assign each part to the Bot it fits (assignee is that Bot\'s name). Nobody else plans. A card you assign is claimed: that Bot starts the work and does not wait for a confirmation. A question stays in this group.'
}
const name = String(rt.botOf(leadId)?.name ?? 'The task lead').replace(/[\r\n]+/g, ' ').slice(0, 80)
return `${name} is this room's task lead (总负责人) for the board: it writes the one plan and assigns the parts. Do the card it assigns you, or link your own subtask to that overall task. Do not record a new plan or assign work. Start the work; do not wait for a confirmation. A question about your card goes in your reply here, not in your own chat.`
}

// What holds for every turn of a member in this group (#894): who is here, how calls work, the
// notice, the member's own role here and the reply rule. It is a section of the system prompt, so
// the model reads it on every call and a checkpoint or a new part never drops it, while the
Expand All @@ -294,6 +306,7 @@ export function install(rt) {
members,
room.notice ? `Group notice (shared instructions for every member):\n${room.notice}` : '',
leads ? adminNote(room) : faceMemberNote(room, member, rt.botOf),
taskBoardNote(room, member),
routeRules(room, member),
ROOM_REPLY,
].filter(Boolean).join('\n\n')
Expand Down
5 changes: 3 additions & 2 deletions core/src/host/prompt.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

14 changes: 14 additions & 0 deletions core/src/host/roster.js
Original file line number Diff line number Diff line change
Expand Up @@ -376,6 +376,14 @@ export function install(rt) {
admin = null
}
if (patch.mode !== undefined && !ROOM_MODES.includes(patch.mode)) throw new Error(`Reply mode must be one of ${ROOM_MODES.join(', ')}`)
// The one Bot that divides this room's board (#1182). Empty clears it, and the admin plans.
let taskLead = room.taskLead ?? null
if (patch.taskLead !== undefined) {
if (room.origin === 'face-to-face') throw new Error('A face-to-face room has no group task board')
taskLead = patch.taskLead || null
if (taskLead !== null && !members.includes(taskLead)) throw new Error(`${rt.botOf(taskLead)?.name ?? 'That Bot'} must be a member to lead the board`)
}
if (taskLead !== null && !members.includes(taskLead)) taskLead = null
const name = patch.name === undefined ? undefined : cleanRoomName(patch.name, room.id)
const notice = patch.notice === undefined ? undefined : cleanNotice(patch.notice)
const moving = patch.workDir !== undefined
Expand All @@ -401,6 +409,11 @@ export function install(rt) {
}
if (admin !== (room.admin ?? null)) changes.push(admin ? `made ${rt.botOf(admin).name} the admin` : 'removed the admin')
room.admin = admin
if (taskLead !== (room.taskLead ?? null)) {
if (taskLead) room.taskLead = taskLead
else delete room.taskLead
changes.push(taskLead ? `made ${rt.botOf(taskLead).name} the task lead` : 'the admin leads the board again')
}
if (patch.mode !== undefined && patch.mode !== room.mode) {
room.mode = patch.mode
changes.push(patch.mode === 'admin' ? 'plain messages now go to the admin first' : 'every member now replies in turn')
Expand Down Expand Up @@ -513,6 +526,7 @@ export function install(rt) {
if (!room.members.includes(id)) continue
room.members = room.members.filter(member => member !== id)
if (room.admin === id) room.admin = null
if (room.taskLead === id) delete room.taskLead
rt.dropGroupSession(room, id)
touched.push(room)
}
Expand Down
Loading
Loading