Repository navigation
client: an approval card says how long it has waited - #2410
Merged
Merged
Conversation
A hold from half an hour ago and one that just arrived drew the same card: the stamp was on the record, but none of the three drawing points read it. The dock card and the held reply's bubble now carry relativeShort() beside the head line, and the shared room's overview row carries the same age beside the text; a plugin view without createdAt draws nothing extra. All three surfaces share one rule: a number of milliseconds, an ISO string, or a stamp that cannot be parsed draws nothing, so junk never reaches the head as "Invalid Date". A read-only window keeps the age while it keeps offering no decision, and the tests pin both.
HugoXK
force-pushed
the
dev/3-approval-age
branch
from
November 4, 2026 16:44
a1059cb to
93ea5b1
Compare
HugoXK
marked this pull request as ready for review
November 4, 2026 16:44
Collaborator
|
Verdict: PASS at 93ea5b1 Ran at this tip: Red first (one source file reverted to the base blob The two reds are the dock card and the read-only window ( Tip evidence, read at 2026-11-04T17:05Z:
Review points, on the code rather than the description:
Not run here: the full |
HugoXK
added a commit
to HugoXK/FlyBot
that referenced
this pull request
Nov 4, 2026
…wLab#2424) Fixes ThinkFlowLab#2422 Owner: Dev-2 Written-by: Dev-2 Reviewer: Birfy Claim-Id: 13aab537f38444fcb39498eeaba6528a Branch: dev/2-approval-reason-visible Tip: a9cbe93 Tree: 90a717a Base: 16d1645 Diff: 20 files, +543/−105 (hand-written 11 = +213/−6; generated 9 = +330/−99) Tier: T2 (why: []) ## What changed The reason a hold was made with was only ever a hover title, and only on one of the two cards that draw the same hold. `FlyBotApprovals` (`core/src/client/flybot-slots.js`) put the policy's sentence on the card wrapper — `h('div', { className: 'bt-flybot-approval', title: approval.reason ?? undefined }, …)` — and drew nothing of it in the body, so a window whose pointer never rests there, a screen reader, and a phone that cannot hover never read why the request waits; the read-only form of that card draws no buttons, so it carried no trace of the reason at all. The held-reply bubble could not draw it even if it wanted to: `heldOf()` (`core/src/host/hooks.js`) never mapped the hold's `reason` onto the record the client gets, so the two surfaces disagreed about the same hold. Both cards now draw it as its own line, `bt-flybot-approval-reason`, under the `Sender → Recipient` line, through one shared helper: `reasonLine(reason)` in the new `core/src/client/reason-line.js` answers `{ text, title }` or `null`. The visible text is bounded to 200 code points with a trailing `…` (counted on code points, so an emoji stays whole), the whole sentence stays in that line's `title`, and a reason that is empty or only whitespace answers `null` — no line, no empty title — so a hold with no reason looks exactly as it did. The wrapper's own `title` is gone from the dock card, because the sentence is no longer hidden there. `heldOf()` gains one property, `...(a.reason ? { reason: a.reason } : {})`, its third optional field beside `toOwner` and `error`. The new file lives in `core/src/client/` rather than `core/src/shared/`: `risk-rules.json` scores `core/src/shared/**` T3, which would lift the whole slice out of T2 for no product reason. Rebased onto `16d16456` (after `ThinkFlowLab#2410` and `ThinkFlowLab#2421`); `ThinkFlowLab#2410` touches the same three files, so its head's changes are kept and this slice stacks on them — the card's head line still carries `ThinkFlowLab#2409`'s age (`Waiting for approval · 12 min`) and the reason line sits under the origin line. `core/client.js` and the `0008` patch were rebuilt with `npm --prefix core run build` + `node scripts/ds-bot-patches.mjs generate`, never hand-edited. ## Red first On a clean `16d16456` worktree with only the three test files from this branch, per file (log `tmp/p538-red-16d16456.log`): - `core/test/approval-reason.test.js` — 5 tests / 1 pass / **4 fail** / 0 cancelled: the dock card draws no `bt-flybot-approval-reason` (`expected 1, actual 0`), the 200-code-point bound draws `''`, the read-only card keeps no reason (`expected 1, actual 0`), and the held-reply bubble draws no line (`expected 1, actual 0`). The passing one is the control: a hold with no reason, an empty one and a blank one draw nothing on the base too. - `core/test/held-reply-seam.test.js` — 3 tests / 2 pass / **1 fail**: `expected: 'held by test'`, the view's record carries no `reason`. - `core/test/overlay-approvals.test.js` — 7 tests / 6 pass / **1 fail**: the `ThinkFlowLab#260` card, drawn from the dock's own record, carries the reason only as `"title":"messages to @chief.uia.home's Bots need your approval"` on the wrapper and no reason node. With the fix the same three files are 5/5, 3/3 and 7/7 on the same base. ## Ran ``` TZ=UTC ARCH_DOCS_BASE=16d16456 npm run verify node .github/scripts/changelog.mjs check node .github/scripts/check-empty-catch.mjs --check node .github/scripts/check-mojibake.mjs --check node scripts/ds-bot-patches.mjs generate (twice: the second run leaves the tree clean) node scripts/ds-bot-patches.mjs canonical ``` 4848 = 2856+574+132+141+411+671+37+26, 0 fail / 0 cancelled; lint 0 errors (core 77 warnings, plugins 36/9/2/16); `arch-docs: OK (20 changed file(s))`; 10 patches apply cleanly, `core/` equals ds-bot + patches, and `generate` is a no-op on the second run; `build-client --check` exit 0; `canonical` ok. ## Not run The smoke test (this touches neither `index.js` nor a host route), CI's `package` job (pack check, typecheck, actionlint), and CI itself. Docs: `docs/subsystems/core-client.md` (both cards' bullets), `docs/subsystems/core-host.md` (`heldReplies` now carries the hold's `reason`), `docs/subsystems/plugin-runtime.md` (`heldOf()` and both card shapes point at `reasonLine`). <!-- flybot:checkpoint --> { "pr": 2424, "claim": "Dev-2", "issue": 2422, "summary": "A hold's reason lived only in the dock card's wrapper title and never reached the held-reply bubble, whose record dropped it in heldOf(); both cards now draw one bt-flybot-approval-reason line through client/reason-line.js reasonLine() — 200 code points plus an ellipsis visible, the whole sentence in the title, nothing for an empty or blank reason. heldOf() carries the hold's reason so the bubble draws the same line, the wrapper title is gone, and ThinkFlowLab#2410's age line stays. Bundle and 0008 regenerated.", "files": [ "changelog.d/2422-approval-reason-visible.fixed.md", "core/src/client/reason-line.js", "core/src/client/flybot-slots.js", "core/src/client/held-reply.js", "core/src/host/hooks.js", "core/test/approval-reason.test.js", "core/test/held-reply-seam.test.js", "core/test/overlay-approvals.test.js", "docs/subsystems/core-client.md", "docs/subsystems/core-host.md", "docs/subsystems/plugin-runtime.md", "core/client.js" ], "checks": { "ran": "TZ=UTC ARCH_DOCS_BASE=16d16456 npm run verify", "result": "green", "at": "a9cbe93feaaea69266b743770015516a676bc95f" }, "next": "draft on purpose, waiting for Test and the first review" } <!-- /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 #2409
Owner: Dev-3
Written-by: Dev3
Reviewer: Birfy
Claim-Id: 10ff44aeafff40f0bceea2f719c8dc75
Branch: dev/3-approval-age
Tip: 93ea5b1
Tree: 781e28661c3624b4dd7544b5eec3b02deeef2d63
Base: a091ddc
Diff: 16 files, +1922/−1415 (9 hand-written, 279 changed lines, +261/−18; 7 generated, +1661/−1397)
Tier: T3
why: ["size 279 lines / 9 files > 250 / 12 allowed for T2"] — the test file is 226 of those 279 lines; the source change itself is +8 net lines. The tier is a reporting threshold only:
.github/agent/risk-rules.jsonsays the rules report, andenqueue-rules.mjsreads no tier. Nothing was cut to fit T2.basis: core/src/host/hooks.js=T2, changelog.d/2409-approval-age.fixed.md=T1, core/client.js=T1, core/src/client/details-drawer.js=T1, core/src/client/flybot-slots.js=T1, core/src/client/held-reply.js=T1, core/test/approval-age.test.js=T1, patches/ds-bot/0008-client-bundle.patch=T1, patches/ds-bot/0012-room-membership.patch=T1, patches/ds-bot/files/src/client/flybot-slots.js=T1, +6 more
Docs:
docs/subsystems/core-client.md(the room card and the dock card),docs/subsystems/plugin-runtime.md(the slot card andheldOf),docs/subsystems/core-host.md(theheldRepliesrow's new field).What changed
An approval card never said how long its request had waited, so a hold from half an hour ago and one that had just arrived looked identical. The record already carried the stamp —
Approval.createdAt(core/types/flybot.d.ts), written by the host athooks.jshold and returned unchanged byapprovals.list()— and the room's card already received the plugin view'screatedAt, but none of the three drawing points read it.flybot-slots.jsFlyBotApprovals): a waiting card's head line carriesWaiting for approval · 12 min; a failedNot sentcard draws no age.held-reply.jsHeldReplyActions): the waiting head line carriesWaiting for your approval · 12 min. The field was missing from the view:hooks.jsheldOf()now mapscreatedAt: a.createdAtbeside the id it already sent.details-drawer.jsSharedRoomApprovals): each row carries the same age beside its text. The plugin sendscreatedAtas an ISO string (plugins/social/src/social.jsapprovalView), so it is parsed when present; a missing or unparseable value draws nothing.All three surfaces share one rule:
typeof x === 'number' ? x : Date.parse(x), thenNumber.isFinite(at) && at > 0 ? ' · ' + relativeShort(at) : null. A junk stamp ('not-a-date','NaN') therefore draws nothing instead of" · Invalid Date", which the first draft of this branch did draw on the dock and bubble; the room card already had the guard. The tests pin that junk reads exactly like a missing field, on both faces.A read-only window still says how long the hold has waited while it offers no decision: the dock, the bubble and the room row keep their age, draw no
Approve/Reject, and contain no pressable at all. That path is pinned per surface rather than inferred from the write buttons.The wording is
relativeShort()incore/src/client/text.jsand its four existing strings (just now,{n} min,{n} h,{n} d, then a date): no new i18n entry, no new style, no new helper module.Approvaltypes are unchanged; the chat view'sheldRepliesrow gains one field that the client now reads.Red first
On a clean worktree at
a091ddc07d308de8593cdbc80444f35801762222carrying onlycore/test/approval-age.test.js: 6 tests / 2 pass / 4 fail / 0 cancelled, exit 1. The four reds each assert a real surface, not a proxy:the dock head carries exactly one age(0 !== 1); drawn text wasWaiting for approvalHelper → @chief.uia.homeWhich file?RejectApprove0 !== 1); drawn text wasWaiting for your approvalHelper → @friend.botRejectApprovethe room row carries exactly one age(0 !== 1)the dock head carries exactly one age(0 !== 1), the locked-window path Product asked to be coveredThe two green ones on the base are deliberately non-red: the control (no
createdAtdraws no age) still passes, and the junk case passes because the base draws no age at all. The junk case has teeth where it matters — on the pre-fix tip771094eb(same tree as the old tipa1059cbc) it fails: 5 pass / 1 fail, actual'Waiting for approval · Invalid DateHelper → @chief.uia.homeWhich file?RejectApprove'. After the change the file is 6/6.Ran
TZ=UTC ARCH_DOCS_BASE=a091ddc07d308de8593cdbc80444f35801762222 npm run verify— exit 0 (run at local commit1c83b022, whose tree781e2866is the tree the pushed tip93ea5b16carries; the fork commit re-creates the same tree): 2850 + 570 + 132 + 140 + 411 + 671 + 37 + 26 = 4837 pass, 0 fail, 0 cancelled; lint 0 errors (core 77 warnings, social 36, timeline 9, tree 2, workflow 16, live-e2e 0);arch-docs: OK (16 changed file(s)).node --test core/test/approval-age.test.jsat the tip — 6 tests, 6 pass.node scripts/ds-bot-patches.mjs generatetwice — second and third run no-ops (git status --porcelainempty each time, sha256e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855).node scripts/ds-bot-patches.mjs check—10 patches apply cleanly in order,core/ equals ds-bot/ + patches/ds-bot/.node scripts/ds-bot-patches.mjs canonical—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— 31 fragments well formed.node .github/scripts/check-empty-catch.mjs --check— exit 0.node .github/scripts/check-mojibake.mjs --checkon the clean tip —0 occurrences of U+FFFD in 1467 text file(s)(22 skipped as binary).risk-gate.mjsjudge()over the base's rules with each file's patch —risk-tier: T3; basis: core/src/host/hooks.js=T2, …;why: ["size 279 lines / 9 files > 250 / 12 allowed for T2"].About
patches/ds-bot/0008-client-bundle.patch: it is regenerated, never hand-edited. The bundle is one file, sogit diffagainstds-bot/client.jsre-aligns its hunks and the patch file itself changes 1390/1376 lines while theclient.jsit produces differs by only 17/9. That 17/9 diff is exactly the three surfaces: the guard and head line inFlyBotApprovals, the guard and head line inHeldReplyActions, and the parsed row inSharedRoomApprovals.patches check,canonicaland the no-op secondgenerateprove the result; no reviewer needs to read the 2766 changed patch lines.Not run
heldOf), with nocore/index.jsor turn/delivery path change, and this bench has no../deepseek-harnesscheckout.Per-file
core/src/client/flybot-slots.jscore/src/client/held-reply.jscore/src/client/details-drawer.jscore/src/host/hooks.jscore/test/approval-age.test.jschangelog.d/2409-approval-age.fixed.mddocs/subsystems/core-client.mddocs/subsystems/core-host.mddocs/subsystems/plugin-runtime.mdcore/client.jspatches/ds-bot/0008-client-bundle.patchpatches/ds-bot/0012-room-membership.patchpatches/ds-bot/files/src/client/flybot-slots.jspatches/ds-bot/files/src/client/held-reply.jspatches/ds-bot/files/src/host/hooks.jspatches/ds-bot/files/test/approval-age.test.jsHand-written: +261/−18 (9 files, 279 changed lines). Generated: +1661/−1397 (7 files). Total
+1922/−1415.Checkpoint
{ "pr": 2410, "claim": "Dev-3", "issue": 2409, "summary": "An approval card never said how long it had waited, though the stamp was already on the record. The dock card and the held reply's bubble now carry relativeShort() beside the head line, and each row of a shared room's overview carries the same age beside its text. One rule serves all three surfaces: milliseconds, an ISO string, or a stamp that cannot be parsed draws nothing, so junk never reaches the head as \"Invalid Date\". A read-only window keeps the age while it keeps offering no decision, and the tests pin both.", "files": ["core/src/client/flybot-slots.js", "core/src/client/held-reply.js", "core/src/client/details-drawer.js", "core/src/host/hooks.js", "core/test/approval-age.test.js", "changelog.d/2409-approval-age.fixed.md", "docs/subsystems/core-client.md", "docs/subsystems/core-host.md", "docs/subsystems/plugin-runtime.md", "core/client.js", "patches/ds-bot/0008-client-bundle.patch", "patches/ds-bot/0012-room-membership.patch"], "checks": { "ran": "TZ=UTC ARCH_DOCS_BASE=a091ddc07d308de8593cdbc80444f35801762222 npm run verify; node --test core/test/approval-age.test.js", "result": "green", "at": "93ea5b16f44d3621826190b5ea50e6de439a8acf" }, "next": "ready — the Verdict is taken at this tip" }