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/2273-board-readonly-hint.fixed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
A read-only window's task board now says inside the board itself why Add task, Start, Complete and dragging are gone: one line above the columns carries the host's own read-only sentence, which the full-screen board had covered until now. A writable board draws nothing new. ([#2273](https://github.com/ThinkFlowLab/FlyBot/issues/2273))
3 changes: 3 additions & 0 deletions core/client.js
Original file line number Diff line number Diff line change
Expand Up @@ -9733,6 +9733,9 @@ function BoardView({ roster, tasks, rail, scope, counts: counts2, dragging, erro
(0, import_react16.createElement)("input", { "aria-label": t("Task title"), placeholder: t("New task"), value: draft ?? "", onChange: (event) => onDraft?.(event.target.value) }),
(0, import_react16.createElement)("button", { type: "submit", className: "bt-soft bt-soft-sm" }, t("Add task"))
) : null,
// #2273: the board is a 1080px overlay and covers the sidebar banner and the Settings hint,
// so the lock's own sentence is drawn here too, from the host, not in a second wording.
locked ? (0, import_react16.createElement)("p", { className: "bt-board-hint bt-read-only", role: "status" }, hostText(roster.readOnly)) : null,
(0, import_react16.createElement)(
"div",
{ className: "bt-board-columns" },
Expand Down
5 changes: 4 additions & 1 deletion core/src/client/kanban.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { taskCard } from '../shared/board-card.js'
import { BoardCard as BoardCardFrame, TASK_SKIN } from './board-card.js'
import { CloseIcon } from './icons.js'
import { useEscape } from './overlay-hooks.js'
import { t } from './i18n.js'
import { hostText, t } from './i18n.js'
import { relativeShort } from './text.js'

const POLL_MS = 15_000
Expand Down Expand Up @@ -366,6 +366,9 @@ export function BoardView({ roster, tasks, rail, scope, counts, dragging, error,
scope?.kind === 'bot' && onCreate && !locked ? h('form', { className: 'bt-board-add', onSubmit: (event) => { event.preventDefault(); onCreate(draft) } },
h('input', { 'aria-label': t('Task title'), placeholder: t('New task'), value: draft ?? '', onChange: (event) => onDraft?.(event.target.value) }),
h('button', { type: 'submit', className: 'bt-soft bt-soft-sm' }, t('Add task'))) : null,
// #2273: the board is a 1080px overlay and covers the sidebar banner and the Settings hint,
// so the lock's own sentence is drawn here too, from the host, not in a second wording.
locked ? h('p', { className: 'bt-board-hint bt-read-only', role: 'status' }, hostText(roster.readOnly)) : null,
h('div', { className: 'bt-board-columns' },
['not-started', 'in-progress', 'done'].map(column => h(BoardColumn, {
key: column, column, tasks: columns[column], roster, lanes, dragging, onMove, onDrag, archiveFull, locked,
Expand Down
5 changes: 5 additions & 0 deletions core/test/kanban.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -842,6 +842,9 @@ test('P-455: a read-only window draws no write on the task board', async () => {
assert.match(textOf(tree), /Working card/)
assert.match(textOf(tree), /Done card/)
assert.equal(byClass(tree, 'bt-board-when').length, 1)
// P-474: the 1080px board overlay covers the sidebar banner and the Settings hint, so the lock's
// own sentence is drawn inside the board — one line, the host's text, and still no write beside it.
assert.deepEqual(byClass(tree, 'bt-read-only').map(node => [node.props.role, textOf(node)]), [['status', lockedRoster.readOnly]])
// A drop on a column writes nothing either, even when a drag value is handed to the handler.
board.column('in-progress').props.onDrop({ preventDefault() {}, dataTransfer: { getData: () => open.id } })
assert.deepEqual(writes, [])
Expand All @@ -868,6 +871,8 @@ test('counter-proof: a writable board keeps all four writes', async () => {
assert.equal(byClass(board.tree, 'bt-board-add').length, 1)
const draggable = byClass(board.tree, 'bt-board-card').filter(node => node.props.draggable).map(node => node.props['data-task']).sort()
assert.deepEqual(draggable, ['task-open123456', 'task-working456789'])
// P-474 counter-proof: a writable board draws no read-only line of any kind.
assert.deepEqual(byClass(board.tree, 'bt-read-only').map(textOf), [])
const start = all(board.tree).find(node => node.type === 'button' && textOf(node) === 'Start')
start.props.onClick()
await board.settle()
Expand Down
2 changes: 1 addition & 1 deletion docs/subsystems/core-client.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ The browser side of the patched `ds-bot` core (`core/`): sources in `core/src/cl
| `core/src/shared/preview.js` | What a preview may show of a stored line (#130, #1347): `visibleText(text)` unwraps complete `<untrusted-message>` wraps and then removes leftover marker fragments until neither changes the text — a `from="User">` tail, a `untrusted-message>` whose brackets an older pass already stripped, a nested fragment that would otherwise rebuild a marker — and folds the result to the one line a sidebar row is. `stripFence` is the marker half on its own. It is used where a row is built (`previewOf` in `src/client/text.js`, the group row in `src/client/sidebar.js`, the host's `preview` in `src/host/channel.js`), so no surface has to remember it, and by the details drawer's copy export: `transcriptOf` (`src/client/text.js`) reads each stored turn through the same helper, so a copied conversation carries the words, not an untrusted fence, and `copyBodyOf` drops a leading delegation trace only when its first hop is a Bot's name taken from the roster (`copyNames`; longest first, and no names strips nothing), which keeps a line like `Settings → Models 页面` as written; a copied reply keeps its own line breaks, and only a trace-only line or a fence-marker line is dropped. Tests: `core/test/ui-clickthrough.test.js`, `core/test/details-copy.test.js`. |
| `core/src/client/board-card.js` | The task board's card frame (#1363): `BoardCard` draws the identity line, the title, the note and the actions the board passes in, with `TASK_SKIN`. A card carries `data-source` (`task`) and `data-key` (`task:<id>`). The caller is `kanban.js`. Tests: `core/test/board-card.test.js`, `core/test/kanban.test.js`. |
| `core/src/shared/board-card.js` | Task-card identity (#1363): `taskCardKey(id)` is `task:<id>` and `taskCard(task)` builds the model `BoardCard` draws. Tests: `core/test/board-card.test.js`. |
| `core/src/client/kanban.js` | Per-Bot task board (#1058, #1099): the **Tasks** row under Inbox (`KanbanRow`, `ui.board`, `actions.openBoard` / `closeBoard`) and `KanbanDialog`. `boardRail` lists the viewer's Bots nested by `createdBy` and the groups they administer; `boardColumns` maps task status onto **Not started** / **In progress** / **Done**. Cards show id, priority, title, note, assignee and progress, and are drawn by the shared `board-card.js` component: a task card says `source: 'task'`, keys `task:<id>` and keeps `data-task` / `data-column` for this board's own selectors (#1265 step 1). The dialog body is `BoardView` (#1363): the rail stays per-Bot and the columns are the task columns only. The GitHub metrics panel (Today / 7 days, Retry, Diagnose, the package-default repository header) is not mounted. A task card is the shared `BoardCard` with `TASK_SKIN`, and dragging it or dropping it on a column only reports the request to the dialog's `onMove` (`actions.taskUpdate`), so the view still never writes. In a group scope, `columnTree` nests subtasks under the overall task (`kind: 'group'`) when they share a column, and a subtask in another column says which overall task it is part of. That scope also says assigning, claiming, and progress live on the board (overall task, subtask, assignee, column, progress) and that @ only wakes someone. Drag or **Start** / **Complete** calls `actions.taskUpdate`; only the Bot's owner gets those actions. A group scope loads `tasks { roomId }`. Since #1252 the default read is active work only and the Done column merges a second read of the finished rows — `tasks { ...query, archive: true, limit: ARCHIVE_SHOWN }`, which is room-scoped for a group and names the Bot for a Bot board, because the host cuts the page before the board can filter by Bot — newest-settled first by `updatedAt` (tiebreak `createdAt`), finished rows only, and cut to that limit, so a room with hundreds of finished cards still renders a twenty-card column while the poll carries the slice and not the history. Since #2045 that page is also counted for what it is: when the archive holds a full page (`ARCHIVE_SHOWN`), the Done heading reads `20 newest` and a Bot's rail row reads `20+ done` rather than a fraction that would present the page as the history, and in a group the page is cut across the room, so every Bot's share of it is a lower bound and reads the same way rather than presenting its slice as its own total; a page under the limit keeps `{done}/{total}` exactly as before. P-364: the rail adds up the list the scope actually draws from, and a room board keeps its active cards in a second list of its own (`groupTasks`), so a room rail counts the room's active cards as well as the archive page, reading `1/4` beside four drawn cards and `0/1` for a Bot whose cards are all still active, while a Bot board's numbers are unchanged. #2069: it counts one list and never both — `own` still holds the Bot board's read after the rail switches to a room, and adding it counted the room's active cards twice, so a room board that draws two cards read `1/3` where it now reads `1/2`. P-360: a Bot board's load effect is keyed by the Bot it shows, so switching the rail re-reads the new Bot's active list and archive page — the page it kept was that of the Bot it left, and filtering it by the new Bot emptied its Done column while the poll kept refilling the old one. That read without `limit` stays uncapped on purpose: it is the id-lookup and tooling path (`statusOf`, a Bot reading a settled row), not what the board polls. Finishing a card moves it off the active list at once, before the next poll. A card in the Done column also says when it finished — a relative time beside the assignee (`bt-board-when`, `relativeShort`), on a Bot's board and a group's alike (#2066). The stamp is the **first** terminal entry in the card's `actors` (`accepted`, `cancelled` or `failed`), never `updatedAt`: `record()` bumps `updatedAt` on a plain edit too, so taking it would name the edit instead of the finish and taking the last terminal entry would do the same. A row written before that history existed falls back to `updatedAt`, and a card with neither a finite `at` nor a finite `updatedAt` draws no node at all — `relativeShort(undefined)` would print `Invalid Date` and `0` would print 1970. The other columns keep the footer they had. #2248: a read-only window (`roster.readOnly` a non-empty string, read once in `KanbanDialog`) draws no board write the host would refuse — no **Add task** form, no **Start** / **Complete**, no `draggable` card, and a drop on a column runs no `taskUpdate` — while the readable board (rail, three columns, cards, archive, Done stamp) and a writable window are unchanged. Tests: `core/test/kanban.test.js`. |
| `core/src/client/kanban.js` | Per-Bot task board (#1058, #1099): the **Tasks** row under Inbox (`KanbanRow`, `ui.board`, `actions.openBoard` / `closeBoard`) and `KanbanDialog`. `boardRail` lists the viewer's Bots nested by `createdBy` and the groups they administer; `boardColumns` maps task status onto **Not started** / **In progress** / **Done**. Cards show id, priority, title, note, assignee and progress, and are drawn by the shared `board-card.js` component: a task card says `source: 'task'`, keys `task:<id>` and keeps `data-task` / `data-column` for this board's own selectors (#1265 step 1). The dialog body is `BoardView` (#1363): the rail stays per-Bot and the columns are the task columns only. The GitHub metrics panel (Today / 7 days, Retry, Diagnose, the package-default repository header) is not mounted. A task card is the shared `BoardCard` with `TASK_SKIN`, and dragging it or dropping it on a column only reports the request to the dialog's `onMove` (`actions.taskUpdate`), so the view still never writes. In a group scope, `columnTree` nests subtasks under the overall task (`kind: 'group'`) when they share a column, and a subtask in another column says which overall task it is part of. That scope also says assigning, claiming, and progress live on the board (overall task, subtask, assignee, column, progress) and that @ only wakes someone. Drag or **Start** / **Complete** calls `actions.taskUpdate`; only the Bot's owner gets those actions. A group scope loads `tasks { roomId }`. Since #1252 the default read is active work only and the Done column merges a second read of the finished rows — `tasks { ...query, archive: true, limit: ARCHIVE_SHOWN }`, which is room-scoped for a group and names the Bot for a Bot board, because the host cuts the page before the board can filter by Bot — newest-settled first by `updatedAt` (tiebreak `createdAt`), finished rows only, and cut to that limit, so a room with hundreds of finished cards still renders a twenty-card column while the poll carries the slice and not the history. Since #2045 that page is also counted for what it is: when the archive holds a full page (`ARCHIVE_SHOWN`), the Done heading reads `20 newest` and a Bot's rail row reads `20+ done` rather than a fraction that would present the page as the history, and in a group the page is cut across the room, so every Bot's share of it is a lower bound and reads the same way rather than presenting its slice as its own total; a page under the limit keeps `{done}/{total}` exactly as before. P-364: the rail adds up the list the scope actually draws from, and a room board keeps its active cards in a second list of its own (`groupTasks`), so a room rail counts the room's active cards as well as the archive page, reading `1/4` beside four drawn cards and `0/1` for a Bot whose cards are all still active, while a Bot board's numbers are unchanged. #2069: it counts one list and never both — `own` still holds the Bot board's read after the rail switches to a room, and adding it counted the room's active cards twice, so a room board that draws two cards read `1/3` where it now reads `1/2`. P-360: a Bot board's load effect is keyed by the Bot it shows, so switching the rail re-reads the new Bot's active list and archive page — the page it kept was that of the Bot it left, and filtering it by the new Bot emptied its Done column while the poll kept refilling the old one. That read without `limit` stays uncapped on purpose: it is the id-lookup and tooling path (`statusOf`, a Bot reading a settled row), not what the board polls. Finishing a card moves it off the active list at once, before the next poll. A card in the Done column also says when it finished — a relative time beside the assignee (`bt-board-when`, `relativeShort`), on a Bot's board and a group's alike (#2066). The stamp is the **first** terminal entry in the card's `actors` (`accepted`, `cancelled` or `failed`), never `updatedAt`: `record()` bumps `updatedAt` on a plain edit too, so taking it would name the edit instead of the finish and taking the last terminal entry would do the same. A row written before that history existed falls back to `updatedAt`, and a card with neither a finite `at` nor a finite `updatedAt` draws no node at all — `relativeShort(undefined)` would print `Invalid Date` and `0` would print 1970. The other columns keep the footer they had. #2248: a read-only window (`roster.readOnly` a non-empty string, read once in `KanbanDialog`) draws no board write the host would refuse — no **Add task** form, no **Start** / **Complete**, no `draggable` card, and a drop on a column runs no `taskUpdate` — while the readable board (rail, three columns, cards, archive, Done stamp) and a writable window are unchanged. Since #2273 that board also says inside itself why the writes are gone: because the board is a full-screen overlay that covers the sidebar banner and the Settings hint, `BoardView` draws the host's own read-only sentence as one `role="status"` line above the columns (`bt-board-hint`), the same wording those two surfaces use and never a second one, and a writable window draws no such line. Tests: `core/test/kanban.test.js`. |
| `core/src/client/standing-approvals.js` | Standing tool approvals (#540): `StandingApprovalsCard`, the **Always allowed** card in Bot settings (the Bot's grants) and Group settings (the grants this person gave in the group), one row per kind with **Remove** (`actions.removeStandingApproval`). The approval cards (`member-status.js` `ApprovalCard`, the inbox card) add **Always allow for this Bot** and, for an ask from a group, **Always allow for this group** (`alwaysChoices`; never on a friend's Bot). Host side in [core-host](core-host.md#standing-tool-approvals-540). Tests: `core/test/member-status.test.js`, `core/test/standing-approvals.test.js`. |
| `core/src/client/mobile.js` | Phone-width layout, step 1 (#362): `MOBILE_CSS` under `@media (max-width:640px)` for held-reply and floating approval buttons, the chat pointers and Agent inbox cards. Tests: `core/test/mobile-layout.test.js`. |

Expand Down
5 changes: 4 additions & 1 deletion patches/ds-bot/0008-client-bundle.patch
Original file line number Diff line number Diff line change
Expand Up @@ -5023,7 +5023,7 @@ diff --git a/client.js b/client.js
@keyframes bt-mem-line{from{opacity:0;transform:translateY(4px)}}
.bt-mem-skel{display:flex;flex-direction:column;gap:24px}
.bt-mem-skel-block{display:flex;flex-direction:column;gap:10px}
@@ -2574,2935 +2574,7469 @@ body[data-ds-dark-theme] .bt-theme-chip i{background:var(--da)}
@@ -2574,2935 +2574,7472 @@ body[data-ds-dark-theme] .bt-theme-chip i{background:var(--da)}
@media (max-width:640px){.bt-mem{width:100vw;height:100%;border-radius:0;border:0}.bt-mem-body{padding:18px 18px 20px}.bt-mem-foot{padding:8px 12px 12px}}
@media (prefers-reduced-motion:reduce){.bt-mem,.bt-mem-scrim,.bt-mem-summary>*,.bt-mem-empty,.bt-mem-reply,.bt-mem-skel-block span,.bt-mem-entries,.bt-mem-send>*,.bt-mem-changes li svg{animation:none}}

Expand Down Expand Up @@ -11180,6 +11180,9 @@ diff --git a/client.js b/client.js
+ (0, import_react16.createElement)("input", { "aria-label": t("Task title"), placeholder: t("New task"), value: draft ?? "", onChange: (event) => onDraft?.(event.target.value) }),
+ (0, import_react16.createElement)("button", { type: "submit", className: "bt-soft bt-soft-sm" }, t("Add task"))
+ ) : null,
+ // #2273: the board is a 1080px overlay and covers the sidebar banner and the Settings hint,
+ // so the lock's own sentence is drawn here too, from the host, not in a second wording.
+ locked ? (0, import_react16.createElement)("p", { className: "bt-board-hint bt-read-only", role: "status" }, hostText(roster.readOnly)) : null,
+ (0, import_react16.createElement)(
+ "div",
+ { className: "bt-board-columns" },
Expand Down
5 changes: 4 additions & 1 deletion patches/ds-bot/files/src/client/kanban.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { taskCard } from '../shared/board-card.js'
import { BoardCard as BoardCardFrame, TASK_SKIN } from './board-card.js'
import { CloseIcon } from './icons.js'
import { useEscape } from './overlay-hooks.js'
import { t } from './i18n.js'
import { hostText, t } from './i18n.js'
import { relativeShort } from './text.js'

const POLL_MS = 15_000
Expand Down Expand Up @@ -366,6 +366,9 @@ export function BoardView({ roster, tasks, rail, scope, counts, dragging, error,
scope?.kind === 'bot' && onCreate && !locked ? h('form', { className: 'bt-board-add', onSubmit: (event) => { event.preventDefault(); onCreate(draft) } },
h('input', { 'aria-label': t('Task title'), placeholder: t('New task'), value: draft ?? '', onChange: (event) => onDraft?.(event.target.value) }),
h('button', { type: 'submit', className: 'bt-soft bt-soft-sm' }, t('Add task'))) : null,
// #2273: the board is a 1080px overlay and covers the sidebar banner and the Settings hint,
// so the lock's own sentence is drawn here too, from the host, not in a second wording.
locked ? h('p', { className: 'bt-board-hint bt-read-only', role: 'status' }, hostText(roster.readOnly)) : null,
h('div', { className: 'bt-board-columns' },
['not-started', 'in-progress', 'done'].map(column => h(BoardColumn, {
key: column, column, tasks: columns[column], roster, lanes, dragging, onMove, onDrag, archiveFull, locked,
Expand Down
Loading
Loading