Repository navigation
Group chat details: the room's own scheduled tasks are invisible and unreachable from the room - #2040
Merged
Merged
Conversation
A scheduled task can belong to a group chat: the new-task form offers `Group chats` as a target, Settings groups tasks by room, and the host carries `owner.roomId` end to end. The room's own details panel had no section for them, so "is the 9am task on this room still there?" meant leaving the room for Settings, and nothing in the room led there. `RoomSchedules` draws the room's tasks in the shape the Bot line already uses: three rows, a title carrying the real count above three, and `N more not shown` past the cap. It filters `task.botId === undefined && task.roomId === room.id`, so a Bot's task in the same view and another room's task stay out. Rows and the more line open the settings list with the single argument (`openSettings( 'schedules')`), never a target, which would open the new-task form (ThinkFlowLab#2030). A room with no tasks draws nothing at all — no empty-state sentence and no add button — and a face-to-face room has no such section. The rows and the count are now shared with the Bot line (`SummaryLines`), so the two surfaces cannot describe one list differently. Tests in `core/test/schedules-client.test.js` cover the control cases: a Bot-owned task and another room's task sit in the same view and must not appear, a room with no tasks renders `null`, a face-to-face room renders `null`, an unread view renders `null`, and both the rows and the more line record `openSettings('schedules')` with one argument. Run at this tip: `npm run verify` with `TZ=UTC` (core suite, plugin suites, `.github`, interop, live-e2e, oxlint + arch-docs), `node scripts/ds-bot-patches.mjs generate` twice with the second pass byte-identical, `check`, `canonical`, and `node core/scripts/build-client.mjs --check`. Not run: the smoke test (`dev/smoke.mjs`), which this change does not need — it touches no `index.js` or `src/host/` file. Fixes ThinkFlowLab#2039
Collaborator
|
Verdict: PASS at 4d69748 Ran at this tip: 审查要点
|
This was referenced Nov 1, 2026
Group chat details: the room's own scheduled tasks are invisible and unreachable from the room
#2039
Closed
HugoXK
added a commit
to HugoXK/FlyBot
that referenced
this pull request
Nov 1, 2026
…ount hides it (ThinkFlowLab#2044) Fixes ThinkFlowLab#2042 Written-by: Dev-2 Owner: Dev-2 Reviewer: Birfy Claim-Id: 4f6f3f39cbed481e9d7bde5dfa09b6c2 Branch: dev/2-inbox-done-total Base: dfc4af7 ## What changed The Inbox groups its work into Needs you, In progress and Done, and Done is capped at `DONE_MAX = 20` rows (`core/src/client/inbox.js:25`). The cap was silent: `inboxModel` returned only `needs`, `progress` and `done`, and the Done heading counted the rows it was handed, so a person with 25 finished tasks read `Done · 20` and had no way to know five were not on screen. 1. `inboxModel` now returns the total from before the cap: `core/src/client/inbox.js:361` sorts the settled rows once and returns `done: settled.slice(0, DONE_MAX)` beside `doneTotal: settled.length`. The rows themselves, their order and the cap are unchanged; `needs` and `progress` are computed before that line and never read the total. 2. The Done group draws one line when, and only when, the cap hid something: `InboxDialog` (`:654-668`) computes `hidden = group.id === 'done' ? Math.max(0, (model.doneTotal ?? rows.length) - rows.length) : 0` and appends `{count} more not shown` — the wording `i18n.js:228` already carries and `project-board.js:572` already uses, the same line `ThinkFlowLab#2039` shares with the Bot details summary. No new EN/ZH key. 3. The line is a plain row, not an alert, and sits inside the Done section after the list (`:668`), so it reads with what it counts. `INBOX_CSS` gains `.bt-inbox-more` matching the other secondary lines (`:687`): `--bt-ink-3`, 12.5px, tabular numerals. 4. The heading keeps counting the rows it draws (`Done · 20`), and a short or empty Done list draws no line at all: 20 rows hide nothing, 0 rows keep the existing "Nothing finished in the last 7 days." sentence. Line numbers are this tip (`222b6ec365`), not the base. ## Line by line - `core/src/client/inbox.js` — +21 / −10: `doneTotal` out of `inboxModel`, the block-bodied `GROUPS.map` with the more row, and the `.bt-inbox-more` rule. - `core/test/inbox.test.js` — +84: two cases (the model's total with the Needs you / In progress control, and the dialog's line at 25, at 20 and at 0). - `core/client.js` — +28 / −21: generated by `node core/scripts/build-client.mjs`, never hand-edited. - `patches/ds-bot/0008-client-bundle.patch` — +228 / −311: the same `client.js` edit, re-hunked against ds-bot's copy, generated by `node scripts/ds-bot-patches.mjs generate`, never hand-edited. - `patches/ds-bot/files/src/client/inbox.js` — +21 / −10: the source's generated mirror. - `patches/ds-bot/files/test/inbox.test.js` — +84: the test file's generated mirror. - `docs/subsystems/core-client.md` — +1 / −1: the ownership row for `core/src/client/inbox.js` now says the total survives the cap. - `changelog.d/2042-inbox-done-total.fixed.md` — +1. ## Base The branch was written on `00a3e67a`. `ThinkFlowLab#2040` merged into that same area while this work waited for it: it landed as `dfc4af7a` and changed `core/client.js`, `patches/ds-bot/0008-client-bundle.patch` and the neighbouring row of `docs/subsystems/core-client.md`, so this branch was moved onto `dfc4af7a` rather than left to conflict. The four hand-written paths (`inbox.js`, the test, the doc row, the changelog fragment) were applied to the new base, the one doc conflict was resolved by keeping both rows (`ThinkFlowLab#2040`'s `schedules.js` row and this one's `inbox.js` row), and the two generated artifacts were then rebuilt by `build-client` + `generate` — nothing in `core/client.js` or `patches/` was resolved or edited by hand. Every gate below, and the red-first run itself, was measured on this pull request's real base `dfc4af7a`. ## Red first On a clean checkout of this pull request's base `dfc4af7a807b03ef927fe588065ad1f75df6c9bd` carrying only the new test file (`git status` shows that one path): **30 tests, 28 pass / 2 fail**, and the two failures are the two new cases: - `ThinkFlowLab#2042: the model reports the Done total from before the cap…` fails on `and the total is the count before the cap`: `undefined !== 25` — the model has no total to report. - `ThinkFlowLab#2042: the Done group says how many rows the cap hid…` fails on `exactly one line says how many rows the cap hid`: `expected: 1 / actual: 0` — nothing on screen says five rows are missing. The other 28 are the existing file's, unchanged. At the tip the same file is **30 tests / 30 pass / 0 fail**. ## The tests bite: six mutations, each caught by its own case Each mutation was applied to `inbox.js` alone, the file run with `--test-name-pattern "ThinkFlowLab#2042"`, then restored: | mutation | cases that went red | | --- | --- | | report the capped length as the total | both | | clamp `hidden` at `>= 0` only (drop the difference) | the dialog case | | `hidden + 1` (count the line itself) | the dialog case | | draw the line above the rows instead of below | the dialog case | | `group.id === 'done'` → `true` (line on every group) | the dialog case | | ignore `doneTotal` and use `rows.length` | the dialog case | The first mutation reds both cases because the data is wrong and the drawing follows it; every other mutation changes only what is drawn, so only the dialog case can see it. The control in the model case is a quiet roster (no pending messages, no rooms) over the same tasks: the same failed task with 25 Done rows and with 5 classifies identically into `needs` and `progress`, so the total cannot leak into either group. ## The generated patch: 0008 re-hunks, it does not rewrite `core/client.js` changes by +28/−21, yet `patches/ds-bot/0008-client-bundle.patch` shows +228/−311. Measured on this diff, counting occurrences (multiset): **171 of the 228 added lines are byte-identical to a line the same diff removes**; the remaining 57 carry the new code. On distinct line text the same overlap is **167** (222 distinct added lines, 306 distinct deleted), so the reading does not depend on the counting basis. The bundle text is re-paired around one edit, because `core/client.js` and ds-bot's `client.js` differ almost everywhere. The gates prove the result: `ok 10 patches apply cleanly in order`, `ok core/ equals ds-bot/ + patches/ds-bot/`, `ok patches/ds-bot/ is what generate writes (generate is a no-op)`. Nothing in the patches was hand-edited. ## Verified on the head `TZ=UTC ARCH_DOCS_BASE=dfc4af7a807b03ef927fe588065ad1f75df6c9bd npm run verify` in the clean tree at `222b6ec36571c2a5b6813509e3a500395ffc130e`, exit 0: - core `2479/2479`, social `541` (`540` pass + `1` skip), tree `127` (`126` + `1`), timeline `135` (`134` + `1`), workflow `336/336`, `.github/scripts` `563/563`, interop `37/37`, live-e2e `24/24`. Skip counts are host-dependent and are stated as this run produced them, not as a claim about another machine. - `arch-docs: OK (8 changed file(s))` against base `dfc4af7a`; core lint `73 warnings and 0 errors`, none in a line this diff adds. - `ok 10 patches apply cleanly in order`; `ok core/ equals ds-bot/ + patches/ds-bot/`; `ok patches/ds-bot/ is what generate writes (generate is a no-op)`; `node core/scripts/build-client.mjs --check` exit 0; `node .github/scripts/changelog.mjs check` green. - After `generate` the tree stayed byte-identical (`core/client.js` md5 `0e48b6eba266d6a4328254e156b583d4`, `0001-core-fixes.patch` md5 `a50d2becb9e6e4b460aa8f7559e0ffcb` — untouched by this change, `0008-client-bundle.patch` md5 `26fd0ef3224f0f8d043f92a9ed8087dc`). - Not run: the smoke bench (`dev/bench.sh smoke`) — the diff is an Inbox model field and one rendering line, with no `index.js`, host, turn or store path touched. ## Scope `doneTotal` in `inboxModel`, the Done group's more line, one CSS rule, the two tests, the generated artifacts, one doc row and one changelog fragment. The cap stays 20, the Done heading keeps counting the rows it draws, the Needs you and In progress groups and their counts are untouched, the empty-state sentence is untouched, and no i18n key was added or changed. ## Checkpoint <!-- flybot:checkpoint --> ```json { "pr": 2044, "claim": "Dev-2", "issue": 2042, "summary": "The Inbox Done group is capped at 20 rows and the cap was silent, so 25 finished tasks read as Done 20 with nothing saying five were missing. The model now returns the total from before the cap and the group draws one N more not shown line from it, reusing the existing wording, only when the cap actually hid rows. The heading still counts the rows it draws, twenty and zero hide nothing, and the empty sentence is unchanged. Needs you and In progress never read the total, proven by the same tasks against a short Done list.", "files": [ "core/src/client/inbox.js", "core/test/inbox.test.js", "core/client.js", "patches/ds-bot/0008-client-bundle.patch", "patches/ds-bot/files/src/client/inbox.js", "patches/ds-bot/files/test/inbox.test.js", "docs/subsystems/core-client.md", "changelog.d/2042-inbox-done-total.fixed.md" ], "checks": { "ran": "TZ=UTC ARCH_DOCS_BASE=dfc4af7a807b03ef927fe588065ad1f75df6c9bd npm run verify", "result": "green", "at": "222b6ec36571c2a5b6813509e3a500395ffc130e" }, "next": "none — ready for review" } ``` <!-- /flybot:checkpoint -->
HugoXK
added a commit
to HugoXK/FlyBot
that referenced
this pull request
Nov 1, 2026
Fixes ThinkFlowLab#2041 Owner: Dev-1 Written-by: Dev-1 Reviewer: Birfy Claim-Id: 95933d41dc0c46eca230c105c4174339 ## What changed The board listed a Project's decision files as inert rows — `data-path` and `data-sha256`, no click target, no link — so a person could see that a decision existed and could not open it. A row is a `button` now, and activating it draws that one file's text under the row; activating it again closes it, and another row replaces it. A `button` needs no `tabIndex`, so keyboard focus and activation work. Host (`core/src/host/project-board.js`): - `rt.projectDecision(projectId, name, actor)` answers the reader's own three states for one name: the text with `bytes`/`sha256`/`mtime`, `absent: <path>`, or `too large: <path> is N bytes (bound 200000)`. - The accepted set is the listing the last `projectBoard` read of that Project produced. `decisionsOf` hands `known` back as a map of path → the folder that path was read from, and that map is kept per Project. It is the un-truncated dedupe, so a name past the 200-row cut opens as well, and a decision a meeting archived into a folder of its own is read from *that* folder rather than the Project folder. - A name outside the set answers `not listed: …` **before** any `stat` or `open`. `isDecisionPath` (ThinkFlowLab#2038) is the first check, so `decisions/../outside.md`, an absolute path and a name one level down never reach the disk at all; the set is what refuses a legal-looking name the listing skipped, such as a symlink. - **Who may open one.** The text sits behind the same door as the cards (ThinkFlowLab#1723): the board already answers `tasks: []` for a member who cannot see a Project's cards, and its decisions must not answer instead. The actor travels with the id from `store.js` (`channel.js` untouched), and visibility is worked out per call, in memory, before anything is opened — the per-Project cache holds names and folders only, never one member's view of them, so one reader cannot lend their reach to the next caller and a revoked card takes effect on the next open. A name a decided meeting archived follows that meeting's own cards through `rt.taskVisible`, the rule the board paints them with; a plain record has no card, so the Project's role list decides (`roleOf`: owner, maintainer or member). The chain has three steps, in this order and on purpose: the cards decide when the name has cards; a meeting-linked name whose `taskIds` is empty, or whose cards are all gone, falls to that same Project role list — the cards are the only thing that carried visibility, and a record that lost them must not read as visible to nobody, so the deliberate consequence is that such a name is exactly as open as a plain record and no wider; and anything that cannot be judged at all — no `taskVisible`, no Project record — refuses rather than allows. A caller who may not open a name is answered with exactly the sentence a name the listing never had is answered with, so the refusal cannot be read as a list of what exists. - The byte read is `readListedFile` in `core/src/host/project-state.js`, extracted from `readProjectState`: the 200,000-byte bound and the `lstat` rule (a symlink answers `absent`) are one implementation for both readers, and `project_state`'s own answers are unchanged. Endpoint wiring: `project-decision` in `store.js`, dispatched in `channel.js` beside `project-board`, listed in `authz.js`'s `TASK_ENDPOINTS` beside it. Client (`core/src/client/project-board.js`): `ProjectBoardRecord` keeps one open path, asks `call('project-decision', { id, path })`, and draws a `pre` for the body or the host's own sentence for `absent` / `too large` / `not listed` under a `data-open-decision` block — the answer is text, never markup it contains. An answer that lands after another row was opened is dropped rather than painted under the row now open. Two copy keys with zh entries. ## Red first Base `3eca1e54` sources with only the two new test files added, `cd core && node --import ./test/harness-resolve.mjs --test test/project-board.test.js test/project-board-view.test.js` — **81 tests, 68 pass, 13 fail**, the same three runs in a row: | test | red shape | | --- | --- | | `ThinkFlowLab#2041: activating a decision row…` | `expected: 'button'`, `actual: 'div'` | | `ThinkFlowLab#2041: a second activation collapses…` | `TypeError: rowAt(...).props.onClick is not a function` | | `ThinkFlowLab#2041: each refused answer reads its own sentence…` | same `TypeError` | | `ThinkFlowLab#2041: an answer that lands after another row opened…` | same `TypeError` | | `ThinkFlowLab#2041: the overlay hands the board its own read…` | `the board draws the decision row` — no row exists to click | | `ThinkFlowLab#2041: a decision the board listed opens with its text` | the call answers `{"ok":false,"error":{"code":"bot/not-found","message":"Unknown endpoint project-decision","details":{}}}`, so `false !== true` | | `ThinkFlowLab#2041: a name the board did not list…` | `TypeError: rt.projectDecision is not a function` | | `ThinkFlowLab#2041: a listed decision over the bound…` | same `TypeError` | | `ThinkFlowLab#2041: a decision past the 200-row cut…` | same `TypeError` | | `ThinkFlowLab#2041: a decision deleted after the board listed it…` | same `TypeError` | | `ThinkFlowLab#2041: a decision a meeting archived…` | same `TypeError` | | `ThinkFlowLab#2041: a decision text is answered only to a caller who can see the Project` | the endpoint is unknown, so `false !== true` | | `ThinkFlowLab#2041: revoking a card closes the name a board read already cached` | same | The client case that answers a refusal walks all three the reader has — `too large`, `not listed` and `absent` — and each has to read as the host's sentence, whole, with the row still a row. The four refused names in the second case are `decisions/../outside.md`, an absolute path, `decisions/sub/2026-10-30-deep.md` and a symlink with a legal name; each must answer `not listed: …`, carry no `text`, and the `../` one points at a file over the bound, so `too large` would be the answer if the host had stat'd it — it is `not listed`, which is the proof the disk was never touched. ## Mutations (each applied alone, then reverted) - **A — ignore the listed folder map, read from `folderOf(project)`:** **80/81**, red on `a name the board did not list…` (the symlink answers `absent`): the set, not the name shape, is what refuses a legal-looking name. - **B — keep only the rows the 200-row cut kept (`decisions.files`):** **74/81**, red on seven cases — the listing-to-open path, the bound, the past-cut name, the deleted one, the archived one, and both caller cases. - **C — drop the `MAX_BYTES` check in `readListedFile`:** **83/86** together with `test/project-state.test.js`, red on the board's `over the bound`, on the caller case and on the reader's own `ThinkFlowLab#1702 read one returns its bytes, bounded` — the bound is shared, so breaking it once breaks both. - **D — drop the late-answer guard** (`settle` writing into whichever row is open): **32/33** on `test/project-board-view.test.js`, red on `an answer that lands after another row opened is never drawn`. Before this test existed the same arm ran **33/33 green**, which is why the guard had no evidence behind it. - **E — remove `call: read` from the pane the overlay builds:** **32/33**, red on `the overlay hands the board its own read, so a row opens through it` — without that line the panel's `call` is undefined and an activation returns silently. - **F — no visibility check at all:** **79/81**, red on both caller cases, so the door is what the tests measure and not an unused predicate. ## Verified at the tip - Base `3eca1e54261ea22b211e93884e26ea5d578bacb5`; local commit `c7a3291c50c32f8087e5040a02fd479b27ec6144`, tree `f7139a1c4c1e5c4b276cbcb7fe86b57c29625f98`; the pushed head is rebuilt from that same tree by the push path, so both trees are identical. One commit, 23 files, +1349/−121. - `TZ=UTC ARCH_DOCS_BASE=3eca1e54 npm run verify` → exit 0: core **2492/2492**, social 541, tree 127, timeline 135, workflow 336, `.github` 563, interop 37, live-e2e 24; `arch-docs: OK (23 changed file(s))`; lint 73/36/9/2/16 warnings and 0 errors, plus interop 1 and live-e2e 0; `build-client --check` exit 0. - `node scripts/ds-bot-patches.mjs check` → pin `2b10c70` ok, `10 patches apply cleanly in order`, `core/ equals ds-bot/ + patches/ds-bot/`; `canonical` → `generate is a no-op`; `generate` re-run on this tip changed no path (`git status --porcelain` empty apart from one untracked dependency symlink this worktree links in), and the tree did not move. ## Generated set — what moved and why This branch is rebased on `3eca1e54`, which already carries ThinkFlowLab#2040's and ThinkFlowLab#2044's own regeneration of `client.js` and `0008`, so every number here is this change's delta against that base. `generate` rewrote the `files/` mirrors of the five edited files (`src/host/project-board.js`, `src/host/project-state.js`, `src/host/authz.js`, `src/client/project-board.js` and both test files) and `core/client.js`. Three patches gained exactly one hunk each, and nothing else in them moved — the endpoint has to be dispatched from `store.js` and `channel.js`, which those patches already carry: | patch | inner file | delta | which | | --- | --- | --- | --- | | `0002` | `src/host/store.js` | `@@ -160,13 +160,489 @@` → `@@ -160,13 +160,497 @@` at patch line 1013 | re-aligned hunk header only | | `0002` | `src/host/store.js` | +8 lines at patch line 1444 | **mine** — the `project-decision` case (2 comment + 6 code) | | `0005` | `src/host/channel.js` | `@@ -319,32 +319,135 @@` → `@@ -319,32 +319,136 @@` at line 844 | re-aligned hunk header only | | `0005` | `src/host/channel.js` | +1 line at line 874 | **mine** — `case 'project-decision':` | | `0001` | `src/client/i18n.js` | `@@ -41,22 +41,74 @@` → `@@ -41,22 +41,76 @@` at line 751 | re-aligned hunk header only | | `0001` | `src/client/i18n.js` | +2 lines at line 808 | **mine** — the two zh entries | `0008` is the client bundle, one inner diff (`client.js`), and it moved as a whole: +128/−90. That is representation drift — inserting the new row markup and the open block moves the diff's own anchors — not extra content: what `0008` produces is `core/client.js` byte for byte (`core/ equals ds-bot/ + patches/ds-bot/` above), and that file's own delta against the base is +50/−7, the client change itself. `0003`/`0007`/`0009`/`0012`/`0014`/`0016` are byte-identical to the base. ## What I did not run - The smoke bench (`core/dev/bench.sh smoke`): it needs the `deepseek-harness` checkout, which this worktree does not have. The host change is a read endpoint beside `project-board` and `project-state`; nothing in `index.js` or the turn/agent path moved. - The CI `package` job (pack check, typecheck, actionlint) — `verify` does not run it. ## Deliberately out of scope Editing a decision, more than one open at a time, search, and any change to how decisions are written (the issue's own scope). The endpoint never re-lists the folder: a file created after the last board read answers `not listed` until the board polls again (≤5 s), which is the price of the set being "the listing this board produced" rather than a second, wider read path; closing a row and opening it again re-reads that one file (one `lstat` and one read, no directory re-list), so a file edited in the meantime is shown as it is now. The rows themselves stay as they are: `project-board` sends the decision *listing* to any signed-in actor (ThinkFlowLab#1723 leaves it that way), which is a recorded residual of ThinkFlowLab#2038, not something this change widens — what this change narrows is the text behind a row. ## Checkpoint <!-- flybot:checkpoint --> ```json { "pr": 2046, "issue": 2041, "claim": "Dev-1", "summary": "A decision row on the Project board was inert; it is a button now, and activating it draws that one file's text under the row. The host adds rt.projectDecision, which opens a listed name only when the caller may see it: a meeting's decision follows that meeting's cards, a plain record follows the Project's roles, and anyone else gets the not listed sentence a missing name gets. The byte read is readListedFile, shared with project_state, so the bound and the symlink rule cannot drift. The endpoint is wired through store, channel and authz, and the client draws the body or the refusal.", "files": [ "core/src/host/project-board.js", "core/src/host/project-state.js", "core/src/host/store.js", "core/src/client/project-board.js", "core/test/project-board.test.js", "core/test/project-board-view.test.js", "core/src/host/authz.js", "docs/subsystems/core-host.md" ], "checks": { "ran": "TZ=UTC ARCH_DOCS_BASE=3eca1e54 npm run verify; generate x2 + check + canonical; build-client --check; red first on 3eca1e5 with tests only", "result": "green", "at": "2c7b70a6c76bb926aed1e9a0dd338595220392db" }, "next": "none — ready for review; Test and Security are named by the Lead." } ``` <!-- /flybot:checkpoint -->
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #2039
Written-by: Dev-2
Owner: Dev-2
Reviewer: Birfy
Claim-Id: 796b6872c3d1439795aca747074ae30c
Branch: dev/2-room-schedules
Base: 00a3e67
What changed
A scheduled task can belong to a group chat, end to end: the new-task form offers
Group chatsas a target (core/src/client/schedules.js), the Settings page groups tasks by room, and the host carriesowner.roomIdthroughschedulesView(). The room's own details panel had no section for them —ScheduleSummaryis mounted only on a Bot (details-drawer.js:349), and a group chat goes throughGroupOverview, whose sections were overview, notice, approvals, members,Who answersand Usage. So a person in a group who asked "is the 9am task on this room still there?" had to leave the room for Settings, and nothing in the room led there.RoomSchedules({ room, actions })(core/src/client/schedules.js:459-467) reads the same shared view (useSchedules) and filterstask.botId === undefined && task.roomId === room.id. A task owned by a Bot and a task owned by another room are both in that view and both stay out.N more not shownline are now one component,SummaryLines(:422-435), shared with the Bot line, so the two surfaces cannot describe the same list differently. Three rows,Scheduled tasks · 5above three, and the more row says how many the cap hid.openSettings('schedules')— the#2030/#2032shape. A room id here would open the new-task form instead, which is not what a row means.Add onebutton. An unread view (null) draws nothing either, for the reason#2035fixed on the Bot side.view.available === false(Automation tasks off) draws nothing here, because the rooms' page keeps the only "none" sentence.GroupOverviewmounts it after the Usage section (core/src/client/details-drawer.js:214, import at:13). A face-to-face room draws no such section:RoomSchedulesreturnsnullforroom.origin === 'face-to-face', so the rule lives in one place instead of beside every mount.Line numbers are this tip (
4d697487), not the base.Line by line
core/src/client/schedules.js— +34 / −11:SummaryLinesextracted fromScheduleSummary, andRoomSchedulesadded.core/src/client/details-drawer.js— +2 / −1: the import and the one mount line inGroupOverview.core/test/schedules-client.test.js— +59 / −2: five cases (own tasks only with both controls, the single-argument click, no tasks, face room, unread view), plus a star re-export in the test bundle so a missing name reds one case instead of failing the build.core/client.js— +23 / −7: generated bynpm run buildincore/, never hand-edited.patches/ds-bot/0001-core-fixes.patch— +47 / −24: the sameschedules.jsdiff, re-hunked, generated bynode scripts/ds-bot-patches.mjs generate.patches/ds-bot/0008-client-bundle.patch— +849 / −836: the sameclient.jsedit, re-hunked (see below), generated, never hand-edited.patches/ds-bot/0012-room-membership.patch— +5 / −3: the samedetails-drawer.jsedit, re-hunked, generated.patches/ds-bot/files/test/schedules-client.test.js— +59 / −2: the test file's generated mirror.docs/subsystems/core-client.md— +1 / −1: the ownership row forcore/src/client/schedules.jsnow says what a room's panel lists.changelog.d/2039-room-schedules.fixed.md— +1.Base
The branch was written on
ed032b86, andmainmoved to00a3e67a(#2038) before the pull request was opened. The two commits touch no file in common, andcore/src/client/schedules.js,core/src/client/details-drawer.jsandcore/test/schedules-client.test.jsare byte-identical between the two bases (git diff ed032b86 00a3e67a -- <those three>is empty), so the branch was overlaid on the newmaininstead of rebased. Every gate below, and the red-first run itself, was measured on this pull request's real base00a3e67a.Red first
On a clean checkout of this pull request's base
00a3e67a80e33233c7c819ee39c536c4a32900d7carrying only the new test file (git statusshows that one path): 38 tests, 33 pass / 5 fail, and the five failures are the five new cases, each withRoomSchedules is not a function— the panel does not exist there, which is the defect the issue describes. The 33 that passed are the existing file's, unchanged. The same run oned032b86, where the branch was first written, read 38 / 33 / 5 with the same five failures; both bases carry these sources byte-identically.At the tip the same file is 38 tests / 38 pass / 0 fail.
The tests bite: six mutations, each caught by its own case
Each mutation was applied to
RoomSchedulesalone, the file run with--test-name-pattern "#2039", then restored:roomIdcondition (keepbotId === undefined)own tasks only,no tasksbotIdcondition (keeproomId)own tasks only,no tasksface roomno tasksunread viewopenSettings('schedules', room.id))single-argument clickThe controls are why the first two bite: the same view holds five tasks of the room, one task of the room's Bot carrying that room's id, and one task of another room. A filter written as "tasks without a
botId" counts six and titles the panelScheduled tasks · 6; a filter written as "this room'sroomId" draws the Bot-owned task. Both red.The generated patch: 0008 re-hunks, it does not rewrite
core/client.jschanges by +23/−7, yetpatches/ds-bot/0008-client-bundle.patchshows +849/−836. Measured on this diff, counting occurrences (multiset): 775 of the 849 added lines are byte-identical to a line the same diff removes; the remaining 74 carry the new code. On distinct line text the same overlap is 670 (702 distinct added lines, 685 distinct deleted), so the reading does not depend on the counting basis. The bundle text is re-paired around one edit, becausecore/client.jsand ds-bot'sclient.jsdiffer almost everywhere. The gates prove the result:ok 10 patches apply cleanly in order,ok core/ equals ds-bot/ + patches/ds-bot/,ok patches/ds-bot/ is what generate writes (generate is a no-op). Nothing in the patches was hand-edited.Verified on the head
TZ=UTC ARCH_DOCS_BASE=00a3e67a80e33233c7c819ee39c536c4a32900d7 npm run verifyin the clean tree at4d697487b34deae99302f25796161e176d248d70, exit 0:2477/2477, social541(540pass +1skip), tree127(126+1), timeline135(134+1), workflow336,.github/scripts563/563, interop37/37, live-e2e24/24. Skip counts are host-dependent and are stated as this run produced them, not as a claim about another machine.arch-docs: OK (10 changed file(s))against base00a3e67a; core lint73 warnings and 0 errors, none in a line this diff adds.ok ds-bot/ = Fei2411/DeepSeek-Bot@2b10c70;ok 10 patches apply cleanly in order;ok core/ equals ds-bot/ + patches/ds-bot/;ok patches/ds-bot/ is what generate writes (generate is a no-op);node core/scripts/build-client.mjs --checkexit 0;node .github/scripts/changelog.mjs checkgreen.node scripts/ds-bot-patches.mjs generateran twice; the second pass left the tree byte-identical (core/client.jsmd537cc286b89a2d62e6953b86c652eb024,0001-core-fixes.patchmd5a50d2becb9e6e4b460aa8f7559e0ffcb,0008-client-bundle.patchmd55a0168daf3db7e0b33a5d679ad27e7fa,0012-room-membership.patchmd5d76317531840da312b10dfde2ccc6621).dev/bench.sh smoke) — the diff is client rendering only, with noindex.js, host, turn or store path touched.Scope
RoomSchedulesand theSummaryLinesextraction inschedules.js, one mount line and one import indetails-drawer.js, the tests, the generated artifacts, one doc line and one changelog fragment. The Settings page, the new-task form, the host (owner.roomIdalready arrives), the Bot details line's own behaviour, the three-row cap and#2030's more row are untouched. No new i18n key.Checkpoint
{ "pr": 2040, "claim": "Dev-2", "issue": 2039, "summary": "A group chat's details panel now lists the scheduled tasks that room owns, in the shape the Bot line already uses: three rows, the real count in the title above three, and an N more not shown row. Rows and that row open the settings list with the single argument, never a target that would open the new-task form. The filter keeps tasks that carry this room's id and no botId, so a Bot's task and another room's task in the same view stay out. A room with no tasks draws nothing at all, an unread view draws nothing, and a face-to-face room has no such section.", "files": [ "core/src/client/schedules.js", "core/src/client/details-drawer.js", "core/test/schedules-client.test.js", "core/client.js", "patches/ds-bot/0001-core-fixes.patch", "patches/ds-bot/0008-client-bundle.patch", "patches/ds-bot/0012-room-membership.patch", "patches/ds-bot/files/test/schedules-client.test.js", "docs/subsystems/core-client.md", "changelog.d/2039-room-schedules.fixed.md" ], "checks": { "ran": "TZ=UTC ARCH_DOCS_BASE=00a3e67a80e33233c7c819ee39c536c4a32900d7 npm run verify", "result": "green", "at": "4d697487b34deae99302f25796161e176d248d70" }, "next": "none — ready for review" }