Skip to content

feat(cockpit): write the review's own comment yourself, in the send panel - #50

Merged
jtomaszewski merged 11 commits into
mainfrom
jtomaszewski/edit-general-comment
Sep 4, 2026
Merged

feat(cockpit): write the review's own comment yourself, in the send panel#50
jtomaszewski merged 11 commits into
mainfrom
jtomaszewski/edit-general-comment

Conversation

@jtomaszewski

@jtomaszewski jtomaszewski commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

The problem

Cerber drafts a review, you read it, you send it. Everything on the page is yours to change on the way — every comment can be edited, re-graded or dropped — with one exception: the review's own comment. The paragraph GitHub shows at the top of a review, the one attached to no line, was composed and only composed (summary + walkthrough + the comments that can't post inline + footer). If it said the wrong thing, the only way to change a word of it was to ask the reviewer to rewrite the summary and wait several minutes for the answer.

That is the one part of the payload a human most often wants to write themselves. "Talked this through with the author, the quotes are approved now" is not something an AI can draft, and it is exactly what belongs in that box.

The fix

see what gets postedwrite it yourself. The editor opens seeded with the composed body, grows to the text, previews the markdown as it will read, saves with ⌘↵. What you save is exactly what posts — footer and all, and verbatim: only the composed body is trimmed, because a hand-written one that opens on an indented line is a markdown code block and trimming would silently repaint it as a paragraph.

No half-honouring: a hand-written body is not re-decorated with a footer you deleted or notes you cut. That would post something nobody wrote.

It changes the body alone:

  • inline comments still post, and the event still follows the verdict;
  • the draft underneath is untouched — build it from the review again is one click;
  • a re-review clears it, the same way it drops the pre-chat snapshot (it described a body for a draft that no longer exists);
  • a chat turn never writes one, so writing or clearing one while a turn runs is yours and stands.

Because this is the one place what GitHub gets stops being derived from what the cockpit shows, the panel says so wherever it describes the payload: the strip reads body you wrote rather than claiming 1 folded into the body over a body those notes may have been cut from, the note under the box says it no longer follows the summary or the comments, and a body you wrote opens shown rather than behind the see what gets posted click — including one that arrives from another tab or a poll tick, since it is the one fact about the payload nothing else on the page carries.

The box holds the only copy of what you typed. The summary and comments it replaces are still the review's; these words are nowhere else. So the editor stays open until the write actually lands: the button reads saving… while it is in flight, a failure leaves the draft alone with the reason under the box, and neither Escape nor a second ⌘↵ can take it away mid-save.

Two things leave the panel in the same pass, both saying twice what is already said once:

  • the "send as… ▾ / follows the verdict" switch. The verdict buttons sit directly above the button, so a second control over the same decision was two ways to say one thing — the way to post an approve is to say the review approves. (cerber send -e keeps its override; a terminal has no verdict buttons above it.)
  • the verdict bar's "what you send follows this unless you say otherwise", which existed to explain that switch.

Technical details

  • bodyOverride: string | null on the artifact (src/core/artifact.ts), defaulted null so every artifact already on disk still parses — schema evolution stays additive, no version bump.
  • buildReviewPayload returns it in place of the composed body (src/core/send.ts), untrimmed. The composition still runs: folded is what the cockpit uses to say which comments have no line to land on.
  • Written through PATCH /api/reviews/:key. null hands the body back to the composition; anything that is neither string nor null gets a 400 rather than being coerced — String({}) is "[object Object]", and this field reaches GitHub verbatim.
  • mergeConcurrentEdits takes bodyOverride from the current artifact — a turn never writes one.
  • mergeRunResult doesn't carry it, so a re-review drops it along with the draft it described.
  • The send preview clears before refetching and keys on a signature of what the body is made of (override, summary, chapter prose, comment bodies/grades/status) rather than on the artifact object, which a poll replaces wholesale every three seconds while a run or chat turn is in flight. An open editor is exempt from the refetch — it owns the box.
  • History records the body to post was written by hand / …follows the review again.
  • SPEC.md (§4.4, §8.5, §12.5, §14.4, §16.2, §17.6), docs/lifecycle.md and CLAUDE.md updated in the same change.

Verification

  • pnpm typecheck, pnpm test (566), pnpm build all green. New tests: the override replacing the composed body while inline/folded still compute, verbatim preservation including a body opening on an indented code block, composition returning when it's cleared, the PATCH round-trip with null, the 400 on object/array/boolean/number, the chat-merge rule, the payload strip's wording, and both history lines.
  • Driven in a real browser against throwaway CERBER_HOME copies of a live draft: wrote a body and confirmed the JSON on disk and the /send-preview payload both returned exactly that text with the inline comments intact; cleared it and watched the composed body come back; killed the server mid-edit and confirmed the draft stayed put with Failed to fetch under the box and a retry writing it through once the server was back; started the cockpit against a state file already carrying a body and confirmed the panel draws with it open.
  • Nothing was sent to GitHub at any point in that testing.

…anel

The body GitHub gets was composed and only composed — summary, walkthrough,
folded notes, footer — so the one comment on a review that is not attached to
a line was the one thing the user could not write. Now the send preview has an
editor: it opens seeded with the composed body, and what you save is exactly
what posts, footer and all. Half-honouring a hand-written body — keeping a
footer you deleted, re-appending notes you cut — would post something nobody
wrote.

It changes the body alone. Inline comments still post, the event still
applies, and the draft underneath is untouched: `build it from the review
again` is one click, and a re-review clears the override the same way it drops
the pre-chat snapshot. A chat turn never writes one, so writing or clearing
one while a turn runs is the user's and stands.

Because this is the one place what GitHub gets stops being derived from what
the cockpit shows, the panel says so wherever it describes the payload — the
strip reads "body you wrote" instead of claiming folded comments the body may
no longer carry, the note under it says it no longer follows the summary, and
a body you wrote opens shown rather than behind "see what gets posted".

Two things go from the panel while it is being honest about what posts, both
of them saying twice what is already said once: the "send as…" switch and its
"follows the verdict" line, since the verdict buttons sit directly above the
button and the way to post an approve is to say the review approves; and the
verdict bar's "what you send follows this unless you say otherwise", which
explained the switch. `cerber send -e` keeps its override — a terminal has no
verdict buttons above it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@jtomaszewski
jtomaszewski force-pushed the jtomaszewski/edit-general-comment branch from 1254000 to c969a92 Compare September 4, 2026 07:38

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

The server PATCH currently coerces non-string bodyOverride inputs and the send payload trims user-authored override text despite UI/PR claims of posting it “exactly,” so API/input handling and payload fidelity should be tightened before approval.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Pull request overview

This PR adds support for a user-authored “review body to post” in the send panel, allowing the top-level GitHub review comment (not tied to any line) to be edited and saved independently of the drafted summary/walkthrough/comments.

Changes:

  • Adds bodyOverride: string | null to the artifact schema and wires it through API PATCH + payload building so it replaces the composed review body at send time.
  • Updates the send panel UX to preview, edit (with a grow-to-fit textarea), and toggle between composed vs user-written body text.
  • Extends tests and docs/specs to cover the new override semantics (including merge/re-review behavior and history logging).
File summaries
File Description
web/src/types.ts Adds bodyOverride to the web Artifact type.
web/src/styles.css Adds styling for body editor/actions and removes “send as…” menu styles.
web/src/review.ts Adjusts payload summary string when a user-written body is present.
web/src/review.test.ts Adds coverage for payload summary behavior with bodyOverride.
web/src/Detail.tsx Implements the body editor UI and removes the “send as…” override UI/logic.
web/src/api.ts Extends PATCH payload typing to include bodyOverride.
src/server/index.ts Initializes bodyOverride and supports PATCH updates for it.
src/server/guards.test.ts Adds PATCH route tests covering bodyOverride string/null behavior.
src/server/daemon.ts Ensures stub artifacts default bodyOverride to null.
src/server/chat.test.ts Updates chat test fixtures for the new artifact field.
src/runner/review.ts Ensures re-review resets bodyOverride to null.
src/runner/review.test.ts Updates review runner fixtures for the new artifact field.
src/runner/review-merge.test.ts Updates merge fixtures for the new artifact field.
src/runner/chat.test.ts Updates runner chat fixtures for the new artifact field.
src/core/state.test.ts Updates state fixtures for the new artifact field.
src/core/send.ts Makes send payload body come from bodyOverride when set.
src/core/send.test.ts Adds tests asserting override body replaces composed body while inline/folded still compute.
src/core/revise.ts Ensures chat merge keeps the current artifact’s bodyOverride.
src/core/revise.test.ts Adds coverage for bodyOverride surviving concurrent chat turns.
src/core/refresh.test.ts Updates refresh fixtures for the new artifact field.
src/core/history.ts Records history lines when bodyOverride changes.
src/core/history.test.ts Adds coverage for history lines on override set/cleared.
src/core/autosend.test.ts Updates autosend fixtures for the new artifact field.
src/core/artifact.ts Adds bodyOverride to the Zod schema with default null and extended docs.
SPEC.md Documents schema + lifecycle semantics for bodyOverride and send behavior.
docs/lifecycle.md Documents persistence/reset rules for a user-written send body.
CLAUDE.md Updates contributor rules around send behavior and body override semantics.
Review details
  • Files reviewed: 27/27 changed files
  • Comments generated: 3
  • Review effort level: Lite

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread src/core/send.ts
Comment thread src/server/index.ts
Comment thread web/src/types.ts Outdated
…'t a string

Three from review:

The composed body is trimmed; a hand-written one is not. "Exactly what posts"
has to mean it — and a body opening on an indented line is a markdown code
block, so trimming would silently repaint it as a paragraph.

`PATCH /api/reviews/:key` answers 400 on a `bodyOverride` that is neither a
string nor null, rather than coercing it. `String({})` is "[object Object]",
and this field reaches GitHub verbatim.

The cockpit's Artifact type carries `bodyOverride` as required: the server
defaults it through zod and stubs it as null, so it is always in the response,
and optional there only hid a field someone forgot to plumb through.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

The Send panel preview can temporarily display a stale body while refetching, creating brief but user-visible inconsistencies between the preview text and the “body source” explanation.

Review details

Suppressed comments (1)

web/src/Detail.tsx:1455

  • In SendPanel, the send-preview body can temporarily go stale when event/artifact changes because the effect refetches without clearing preview first. That can briefly show the previous payload body while the “built from…” / “you wrote this body” source line is already reflecting the new artifact.bodyOverride state, which is misleading.

Clear preview/previewError before refetching (and guard against out-of-order responses) so the UI shows the loading state instead of stale content.

  useEffect(() => {
    if (!showBody) return;
    fetchSendPreview(reviewKey, event)
      .then((p) => {
        setPreview(p);
  • Files reviewed: 27/27 changed files
  • Comments generated: 0 new
  • Review effort level: Lite

The line under the box reads from the artifact, which changes the moment a
body is saved or cleared; the box itself waited for a refetch. For that window
the panel paired "you wrote this body" with the composed body it had just
replaced — the exact confusion this panel exists to prevent. It now clears to
"building the body…" first, and a slower earlier answer no longer lands on top
of a newer one.

Keyed on what the body is actually made of rather than on the artifact object,
which a poll replaces wholesale every three seconds while a run or chat turn is
in flight — on identity, the preview would clear and refetch on every tick of a
body nothing had changed. The open editor is exempt: it owns the box, and the
draft in it has to outlive an artifact that moves underneath.
@jtomaszewski

Copy link
Copy Markdown
Contributor Author

Fixed in cd5c354 — the suppressed comment on Detail.tsx:1455 was a real one.

The line under the box reads from the artifact, which changes the instant a body is saved or cleared, while the box waited on the refetch. For that window the panel paired "you wrote this body" with the composed body it had just replaced — precisely the confusion this panel exists to prevent. It now clears to building the body… first, and a stale flag drops a slower earlier response landing on top of a newer one.

One thing I did differently from the suggestion: the effect is keyed on a signature of what the body is made of (bodyOverride, summary, chapter prose, comment bodies/grades/status) rather than on the artifact object. While a run or a chat turn is in flight the cockpit polls the artifact every three seconds and replaces it wholesale, so clearing on object identity would have flashed building the body… on every tick of a body nothing had changed. The open editor is also exempt from the refetch — it owns the box, and a draft being typed has to outlive an artifact that moves underneath it.

Verified in a browser against a throwaway state dir: saved a body, watched the box clear and come back with the new text and the note in agreement.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

The new BodyEditor ⌘↵/Ctrl↵ save shortcut doesn’t preventDefault(), so it can still insert a newline and behave inconsistently compared to the existing textarea shortcut handling.

Review details

Suppressed comments (1)

Previously missed (1) — in code that hasn't changed since the last review.

web/src/Detail.tsx:1386

  • In BodyEditor, ⌘↵/Ctrl↵ triggers onSave but doesn’t call preventDefault(), so the browser can still insert a newline into the textarea (the same issue you explicitly guard against in LineComposer). That can lead to saving text that differs from what the user briefly saw/typed and makes the shortcut feel flaky.
  • Files reviewed: 27/27 changed files
  • Comments generated: 0 new
  • Review effort level: Lite

… out

Same guard the line composer already has. The save closes the editor, so the
stray newline was discarded rather than posted — but a shortcut whose default
action still fires is one keystroke away from mattering, and the box beside it
gets this right.
@jtomaszewski

Copy link
Copy Markdown
Contributor Author

Fixed in the push above — e.preventDefault() on ⌘↵/Ctrl↵ in BodyEditor, the same guard LineComposer already carries.

For the record on severity: the save closes the editor, so the stray newline was discarded rather than posted. But a shortcut whose default action still fires is one refactor away from mattering, and the box next to it gets this right — so it's worth the line.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

The send-panel body editor currently closes even if saving the overridden body fails, which can discard the user’s draft text and misrepresent persistence.

Review details

Suppressed comments (3)

Previously missed (1) — in code that hasn't changed since the last review.

web/src/Detail.tsx:1443

  • onBody is typed as returning void, so the body editor can’t await persistence. That means the editor can only optimistically close, and on a failed PATCH the user’s typed text can be lost when the component unmounts. Consider making onBody return a Promise so the editor can keep the draft open (and/or show an error) until the save actually succeeds.

This issue also appears in the following locations of the same file:

  • line 1550
  • line 2546

web/src/Detail.tsx:1553

  • The body editor closes immediately after calling onBody(text). If the PATCH fails (network/server error), the editor unmounts and the user can lose the text they just wrote. Close the editor only after the save promise resolves successfully; on failure, keep it open so the draft is preserved.
              onSave={(text) => {
                onBody(text);
                setEditingBody(false);
              }}

web/src/Detail.tsx:2546

  • onBody is currently wired through apply(...), which catches errors and converts them into setError(...) side-effects. If onBody is changed to return a Promise for the editor to await, this implementation should reject on failure (after setting the error) so the editor can keep the draft open and not treat a failed save as success.
              onBody={(text) => apply(patchReview(reviewKey, { bodyOverride: text }))}
  • Files reviewed: 27/27 changed files
  • Comments generated: 0 new
  • Review effort level: Lite

…ide it

The editor closed the moment save was clicked, so a PATCH that failed took the
text with it — and this box is the only copy of what was typed. The summary and
the comments it replaces are still the review's; these words are nowhere else.

It now stays open until the write lands: `onBody` hands back the promise
instead of a caught one, the save button says "saving…" while it is in flight,
and a failure leaves the draft alone with the error under the box. The reset
link reports the same way, where the body is read rather than at the top of the
page.

Verified by killing the server mid-edit: the text stayed, "Failed to fetch"
appeared under it, and clicking save again once the server was back wrote it.
@jtomaszewski

Copy link
Copy Markdown
Contributor Author

Fixed in 458377d — this one mattered more than the last two.

The editor closed the moment save was clicked, so a failed PATCH took the text with it, and that box is the only copy of what was typed: the summary and the comments it replaces are still the review's, but these words are nowhere else.

onBody now hands back the real promise rather than the caught one (apply swallows failures, which is fine for a comment edit that leaves the original in place — not for this). The editor stays open until the write lands, the button reads saving… while it is in flight, and a failure leaves the draft alone with the reason under the box. The build it from the review again link reports the same way, where the body is read rather than at the top of the page.

Verified by killing the server mid-edit: the text stayed put, Failed to fetch appeared under the box, and clicking save again once the server was back wrote it through.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

The new send-panel body editor has a couple of concrete error-handling/duplicate-submit bugs (in web/src/Detail.tsx) that can cause failed saves/resets to be mishandled.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Review details

Suppressed comments (1)

web/src/Detail.tsx:1592

  • This .catch handler can itself throw if the rejection value is null/a string/etc, because it reads e.message without checking the type. That can leave the panel with an unreported failure when clearing the body override.
                        onBody(null).catch((e) => setPreviewError(String(e.message ?? e)))
  • Files reviewed: 27/27 changed files
  • Comments generated: 1
  • Review effort level: Lite

Comment thread web/src/Detail.tsx
…hat can't throw

⌘↵ can be held down and the disabled button is not the only way in, so `save()`
returns early while a write is in flight rather than sending a second one.

And the failure handlers read `e?.message`: a rejection is not guaranteed to be
an Error, and a catch that throws on `null.message` leaves the failure
unreported — the one outcome this path exists to prevent.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

The new body editor can be closed via the Escape key while a save is in flight, which can discard the only copy of the user-authored text despite the intended “stay open until written” behavior.

Review details

Suppressed comments (2)

Previously missed (2) — in code that hasn't changed since the last review.

web/src/Detail.tsx:1412

  • Escape currently calls onCancel() even while a save is in flight, which can close the editor and discard the only copy of the draft despite the comment that it “stays open until the write actually lands”. Since the cancel button is disabled while saving, the keyboard path should follow the same rule (ignore Escape while saving is true).
    web/src/Detail.tsx:1490
  • bodySource is JSON.stringified on every render, even when the preview is hidden (showBody is false) or the editor is open (editingBody is true). With many comments this can become avoidable work during the 3s polling refreshes; compute it lazily with useMemo and gate it behind showBody && !editingBody.
  • Files reviewed: 27/27 changed files
  • Comments generated: 0 new
  • Review effort level: Lite

The cancel button is disabled while a write is in flight, for the reason the
editor exists to serve — the box holds the only copy of what was typed. Escape
was the way around it.

And the body's change signature is only built while the preview is on screen:
with it closed there is nothing to keep in step, and walking every comment on
each three-second poll tick was work for a string nobody reads.
@jtomaszewski

Copy link
Copy Markdown
Contributor Author

Both fixed in the push above.

Escape while saving — you're right, and it contradicted the rule the editor exists for: the cancel button is disabled for exactly that reason and the keyboard path must not be the way around it. Now ignored while a write is in flight.

bodySource on every render — gated behind showBody. With the preview closed there is nothing to keep in step, so the string is not built at all; that is where the 3s-poll cost you're pointing at actually lands. I left useMemo out deliberately: the arrays it reads are new objects on every poll response, so a memo would recompute every time anyway and only add a dependency list to keep true.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

The new send-panel UI has a couple of concrete state/render edge cases (around showBody syncing and previewError hiding the preview/controls) that can block users from seeing or recovering the posting body without extra steps.

Review details

Suppressed comments (2)

Previously missed (2) — in code that hasn't changed since the last review.

web/src/Detail.tsx:1481

  • showBody is initialized from artifact.bodyOverride only on first render; if bodyOverride becomes non-null later (e.g., another tab writes it, or a poll tick brings in a change), the panel can keep the posting body hidden even though the UI copy/spec says a user-written body “opens shown”. Sync showBody when ownBody flips true, without forcing it closed when cleared (so the user’s “hide what gets posted” choice still stands).
    web/src/Detail.tsx:1581
  • When previewError is set (e.g., if “build it from the review again” fails), the current render path shows only the error <p> and hides the existing preview + controls. That prevents immediate retry/edit/copy without first toggling the whole preview closed/open. Prefer showing the error alongside the existing preview when one is available, and only replacing the preview entirely when there is no preview yet.
  • Files reviewed: 27/27 changed files
  • Comments generated: 0 new
  • Review effort level: Lite

…ailed reset keeps the body

`showBody` was seeded from the artifact once, at mount, so a body written in
another tab (or arriving on a poll tick) stayed hidden behind "see what gets
posted" — while the panel's whole claim is that a body you wrote is the one
fact about the payload nothing else on the page carries. It now opens whenever
that flips true, and never closes on its own: clearing a body must not reopen a
preview you closed.

And a preview error renders above the body rather than instead of it. A reset
that failed leaves the body it could not replace still standing, and taking
that away with the buttons made retrying a matter of closing the panel and
opening it again.
@jtomaszewski

Copy link
Copy Markdown
Contributor Author

Both fixed in c45310d, and both verified in a browser.

showBody seeded once — right, and it contradicted the claim the panel makes: a body you wrote is the one fact about the payload nothing else on the page carries, so leaving it behind see what gets posted defeats the point. It now opens whenever ownBody flips true, and only in that direction — clearing a body must not reopen a preview you deliberately closed. Checked by starting the cockpit against a state file already carrying a body: the panel draws with it open.

previewError replacing the preview — now rendered above the body instead of in place of it, and the "building the body…" line is suppressed when the error is already the reason there is nothing to show. Checked by killing the server and clicking build it from the review again: Failed to fetch appears above, the body and both buttons stay, and a retry is one click.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 Approval recommended

The feature is coherently integrated end-to-end with solid tests/docs updates, with only a minor React state-update-after-unmount cleanup recommended.

Review details

Suppressed comments (1)

Previously missed (1) — in code that hasn't changed since the last review.

web/src/Detail.tsx:1393

  • BodyEditor.save() calls setSaving(false) in a .finally() after onSave resolves, but on successful save the parent closes the editor (unmounting this component). That makes the .finally() state update run after unmount, which can trigger React warnings and is easy to trip in dev builds.

Since the editor unmounts on success, only reset saving on the error path (or guard with an isMounted ref).

  • Files reviewed: 27/27 changed files
  • Comments generated: 0 new
  • Review effort level: Lite

A save that lands closes the editor, so resetting `saving` in a `finally` was
writing into an unmounted box. The failure path is the only one that returns to
it — and it is the only one that needs to.
@jtomaszewski

Copy link
Copy Markdown
Contributor Author

Fixed in the push above — setSaving(false) moved into the catch. React 19 makes a post-unmount setState a silent no-op rather than a warning, so nothing was actually breaking, but the finally claimed a path that doesn't exist: a save that lands closes the editor, and the failure path is the only one that comes back.

Thanks for the run — seven rounds, and the two that mattered (a failed save discarding the only copy of the text, and the preview going stale under a line that had already changed) were both worth the trip.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

The new body editor UX has a couple of concrete edge cases (draft loss / sizing behavior) that should be addressed before merging.

Review details

Suppressed comments (2)

Previously missed (2) — in code that hasn't changed since the last review.

web/src/Detail.tsx:1351

  • useGrowToFit only recalculates height when value changes; resizing the window after the textarea has grown can leave it larger than 70% of the new viewport (or unnecessarily small), which contradicts the "up to most of the window" intent. Consider recalculating on resize as well, without re-attaching listeners on every keystroke.
    web/src/Detail.tsx:1593
  • While the body editor is open, the "see/hide what gets posted" toggle can still be clicked to hide the section, which unmounts BodyEditor and discards the unsaved draft (and also allows closing mid-save). Disabling the toggle while editingBody is true prevents accidental data loss and matches the intent that the editor owns the box until save/cancel.
  • Files reviewed: 27/27 changed files
  • Comments generated: 0 new
  • Review effort level: Lite

… re-fits on resize

"Hide what gets posted" unmounted the open editor, taking the draft with it —
the same loss the awaited save exists to prevent, by a different door. The
toggle is disabled while the editor is open, and says why.

And the box's cap is a share of the window, so it is re-taken when the window
changes: one grown in a tall window stayed taller than the short one it then
sat in. Its own effect, so the listener is not torn down and rebuilt on every
keystroke.
@jtomaszewski

Copy link
Copy Markdown
Contributor Author

Both fixed in the push above, both verified in a browser.

The toggle unmounting the editor — this was the same loss the awaited save exists to prevent, by a different door, so thank you for it. Hide what gets posted is now disabled while the editor is open and carries the reason on hover: save or cancel the body you are writing first.

useGrowToFit on resize — the cap is a share of the window, so it is re-taken when the window changes. In its own effect keyed on a stable callback, so the listener isn't torn down and rebuilt on every keystroke, as you noted. Checked live: a box at 700px in a 1000px-tall window re-fits to 420px when the window drops to 600.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 Approval recommended

Changes are well-covered by tests and documentation, with one minor performance tweak suggested but no functional blockers found.

Review details

Suppressed comments (1)

Previously missed (1) — in code that hasn't changed since the last review.

web/src/Detail.tsx:1518

  • bodySource is computed (including mapping all comments + JSON.stringify) even while the body editor is open (editingBody === true). The effect already bails out during editing, so this extra work runs on every poll-driven re-render without any benefit and could get expensive on large reviews.
  • Files reviewed: 27/27 changed files
  • Comments generated: 0 new
  • Review effort level: Lite

The effect bails while the editor is open, so the string it would key on is
never read there — the guard now says the same two conditions the effect acts
on rather than one of them.
@jtomaszewski

Copy link
Copy Markdown
Contributor Author

Applied in 849fb7b — the guard now names the same two conditions the effect acts on (showBody && !editingBody) rather than one of them.

That is the last outstanding item; merging on the 🟢 at 78782f8 once CI is green here. For the record: 849fb7b is your own suggestion applied verbatim, one guard token plus its comment, and it is not itself re-reviewed.

@jtomaszewski
jtomaszewski merged commit 26467bf into main Sep 4, 2026
2 checks passed
@jtomaszewski
jtomaszewski deleted the jtomaszewski/edit-general-comment branch September 4, 2026 08:52
@github-actions

github-actions Bot commented Sep 4, 2026

Copy link
Copy Markdown

🎉 This PR is included in version 0.29.0 🎉

The release is available on:

Your semantic-release bot 📦🚀

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants