Skip to content

Offer the reply suggestion of a ticket to the clerk - #1291

Open
TaprootFreak wants to merge 22 commits into
developfrom
feat/support-reply-suggestions
Open

Offer the reply suggestion of a ticket to the clerk#1291
TaprootFreak wants to merge 22 commits into
developfrom
feat/support-reply-suggestions

Conversation

@TaprootFreak

@TaprootFreak TaprootFreak commented Aug 8, 2026

Copy link
Copy Markdown
Contributor

Consumes the reply suggestions added in DFXswiss/api#4761 — merge that one first.

What the clerk sees

Above the composer on the support issue screen, whenever a suggestion is waiting:

  • the proposed answer, links included,
  • a warning when the answered message is no longer the newest one of the conversation,
  • Accept — the text lands in the composer, where it is edited and sent as the clerk's own
    message, exactly like a template,
  • Discard — the decision is recorded and the panel goes away.

Only the newest suggestion awaiting a decision is offered; earlier ones stay on record on the server
and are never shown. A suggestion is addressed by the id of the message it answers, which is where the
API stores it. There is no control for writing a suggestion, by design: they enter through the
API alone. A suggestion that arrives while the ticket is open is picked up by the existing 15-second
poll, so a clerk sitting on the screen does not have to reload.

Thirteen defects in the screen this uncovered

  1. The ticket loaded twice. loadIssue read the clerk list to pick the default message author,
    which made it depend on that list. The list arrives a moment after the ticket, so every ticket
    that was opened was loaded twice, and the second round put the screen back behind its loading
    spinner — a wasted request and a visible flash. The author is now resolved in its own effect.
  2. A picked author was overwritten. With the author resolved in an effect, a run of that effect
    already scheduled when the clerk picks an author carries the state of the render it was scheduled
    in — so it reset the selection back to the default. A state flag cannot fix that (it is exactly
    the stale value that run reads); a ref is set synchronously and holds.
  3. Answers the screen had moved past were applied anyway. Skipping the poll while a decision
    runs only stops a new fetch; one already in flight still answered with the state from before
    the decision and put the decided suggestion back on screen. The same held for a fetch belonging
    to the ticket that was left, and for the decision's own continuation — which wrote the accepted
    text into the composer of the ticket the clerk had moved to. Fetch and decision now carry a
    token that is raised when a decision starts and when the ticket changes.
  4. The busy flag was not tied to the ticket, so opening another one while a decision ran left
    its Accept and Discard disabled until the abandoned request settled — in the worst case never.
  5. The poll was rebuilt on every decision. The interval took the suggestion's busy flag as a
    dependency, so each decision tore it down and set it up again — which also restarted the clock
    for the message poll the same interval carries, and could postpone new customer messages by up
    to another 15 seconds. It reads a ref now.
  6. The handbook dropped the platform badge of any baseline not taken on macOS, because it
    recognised only -chromium-darwin. The snapshot path template is {projectName}-{platform}, so
    the platform is now read from the name.

Review found a seventh, and it turned out to be the entry to a class rather than a single case: the
ticket load itself wrote whatever came back, while everything around it was guarded — and this
branch had moved the update write from the route parameter onto that loaded ticket. Walking every
state and every await of the screen for the same shape turned up six more:

  1. The ticket load was unguarded, so a slow answer for the ticket that was left replaced the one
    on screen, silently — the spinner is long gone by then. The three writes take the id from the
    route again, so a write never depends on a load having been guarded correctly.
  2. The reload after an update ran from a stale closure, fetching the ticket it belonged to and
    putting it back on screen, form fields included — the next update would have carried those values.
  3. The message thread and its poll had no guard at all, and the thread was not dropped with the
    ticket either: one customer's conversation sat under another customer's ticket until the new
    thread answered, which only starts once the ticket has brought its uid.
  4. A send answering after a ticket change cleared the composer of the ticket the clerk had
    moved to, taking a draft written for someone else.
  5. The file preview kept one customer's document on a panel that stays until it is closed by
    hand, and its own fetch could put it there after the switch.
  6. The template picker was never closed, so it reopened itself over the next ticket, and its
    account lookup filled templates from the customer whose ticket had been left.
  7. A failed ticket load was never cleared, and this screen renders nothing else while one
    stands: a single failure blocked every ticket opened afterwards until the page was reloaded.

Everything tied to the ticket now takes one token where the work starts and asks before it writes,
and the ticket change drops the author, the thread, the preview, the picker, the errors and both
busy flags along with the draft. Numbers 9 to 13 predate this pull request; they sit in the code it
rewrites, and are fixed here rather than deferred.

A refused decision is reconciled rather than left standing: the server refuses a decision it has
already taken — by another clerk, or in another tab — and the panel would otherwise keep offering
that same decision into that same refusal. Accept and Discard stay disabled until what the server
actually holds is on screen, and the refusal is the message the clerk reads.

One case is deliberately left to the server: leaving a ticket and returning to it before the server
has answered a decision shows the suggestion again with usable buttons, so a second decision can be
sent. "A decision is taken once" is enforced there — the write is addressed to a suggestion that is
still pending — and a second attempt is answered with a conflict — rebuilding that in the client would duplicate state the server holds, for
a window the size of one round trip.

Tests

Every touched file is at 100 % on all four metrics:

File Stmts Branch Funcs Lines
src/screens/support-dashboard-issue.screen.tsx 100 % 100 % 100 % 100 %
src/hooks/support-dashboard.hook.ts 100 % 100 % 100 % 100 %
src/components/support/reply-suggestion-panel.tsx 100 % 100 % 100 % 100 %
src/components/support/info-panel.tsx 100 % 100 % 100 % 100 %

The screen and the hook were at 0 % before this pull request.

Reaching 100 % on the screen meant removing six guards rather than testing them: they were
unreachable, not untested, and CONTRIBUTING asks for exactly that. The account id, the ticket id and
the attachment now come from the render that has them; clearing the file selection remounts the
input instead of writing to a ref that is never null; and the message list hands its file handler a
message whose fileName is settled; and the template button's account check was the remnant of an
optional chain this branch unwrapped, on a field the API always sends. info-panel.tsx is covered by
a spec of its own — a thread without ids, without a file, without a handler.

Handbook

The screen had no baseline at all, so this creates the spec for one:
e2e/support-dashboard-issue.spec.ts drives it through the five visual states this change
introduces — a suggestion waiting for a decision, one the conversation has moved past, none at all,
a decision in flight with both buttons closed, and an accepted suggestion sitting in the composer —
plus the metadata.json entry that titles the flow.

Unlike the older staff specs, this one mocks everything: the session token is synthetic (the
frontend only decodes a JWT, it never verifies it) and every API call is answered from fixtures. The
baselines are deterministic, carry no production data, and need neither a running api nor a seeded
database.

Declared deviation: no baselines are committed

Per Deviating from these guidelines, this pull request departs from Handbook — "a committed
Playwright baseline under e2e/screenshots/baseline/, covering each visual variant the change
introduces". The spec and the metadata entry are here; the five .png files are not.

The reason is that they belong on macOS, where the other 148 baselines in that directory were taken,
and no macOS machine was available to produce them. Playwright only ever compares against the
baseline of the platform it runs on, so Linux captures would be read by nobody on the platform the
rest of the directory targets — and renaming one to -darwin would be a claim about its origin that
the file cannot keep, since font rendering differs between the two.

What closes it: npx playwright test e2e/support-dashboard-issue.spec.ts --update-snapshots on a
Mac writes all five, and committing them here removes the deviation. The reviewer has already run
the spec on macOS and confirmed it passes against the baselines that run wrote.

@TaprootFreak

Copy link
Copy Markdown
Contributor Author

Five review passes were needed before both review dimensions came back clean. What they found and what was changed:

  1. The handbook recognised only baselines taken on macOS, so the three added here reached the page without a project or platform badge. Both are now read from the file name, which is where the snapshot template puts them.
  2. The 15-second poll refetched the suggestion while an accept or discard was in flight, and the state it answered with could put the decided suggestion back on screen.
  3. Neither the offered suggestion nor the "author was picked by hand" marker were tied to the ticket, so both survived opening another one.
  4. The screen tests resolved the clerk list through a real timer while waiting on a fixed number of microtask ticks — the file failed roughly one run in eight, in tests unrelated to whatever had actually raced.
  5. Skipping the poll only stops a new fetch. One already in flight still answered, and its answer was applied — the same for a fetch belonging to the ticket that was left. Both are now qualified by a token.
  6. The decision itself was not qualified: opening another ticket while it ran put the accepted text into the composer of the new one, cleared the suggestion loaded there, and reported the old failure on the new screen.
  7. The flag that disables the buttons during a decision was not tied to the ticket either, so the new ticket's Accept and Discard stayed disabled — in the worst case indefinitely — until the abandoned request settled.

Two points were raised and deliberately not changed:

  • A second decision on a suggestion that is still being decided, reachable by leaving a ticket and returning to it before the server answers. The invariant "a decision is taken once" lives on the server, under a row lock, and answers a second attempt with a conflict. Rebuilding it in the client would duplicate state the server already holds, for a window the size of one round trip whose worst outcome is an error message.
  • The three Playwright baselines carry -chromium-linux, not -chromium-darwin. They were taken on Linux; the screen had no baseline at all before. Playwright compares against the baseline of the platform it runs on, so a macOS run writes its own alongside these — committing those is how the macOS variant is added. Renaming a Linux capture would be a claim about its origin that the file cannot keep. This is the one point that needs a decision here: either accept the Linux baselines with the macOS variant to follow, or ask for them to be regenerated on macOS before merge.

Every touched file is at 100 % statements, branches, functions and lines; the screen and the hook were at 0 % before. The suite runs green repeatedly (918 tests, six consecutive runs).

@TaprootFreak
TaprootFreak marked this pull request as ready for review August 8, 2026 09:03
@TaprootFreak
TaprootFreak marked this pull request as draft August 8, 2026 10:51
@TaprootFreak

Copy link
Copy Markdown
Contributor Author

Four review passes were needed before both a conformance review and a correctness review came back
without findings. What they changed:

Pass 1 — a decision the server refuses, because it has already been taken by another clerk or in
another tab, left the panel standing with its buttons live. The same decision could then be sent
into the same refusal until the poll happened to reconcile it, up to 15 seconds later. The error now
comes with what the server actually holds.

Pass 2

  • Those buttons came back before the reconciliation was in, which left one round trip in which the
    refused decision could be sent again. They stay disabled until the panel is right.
  • The reconciliation reports its own failures the same way, so it overwrote the refusal the clerk
    needed to read. The decision's error is set after it.
  • The poll took the busy flag as a dependency, so every decision tore the interval down and set it
    up again — postponing the message poll the same interval carries. It reads a ref now.

Pass 3 — the test around the refused decision asserted the state after the reconciliation, which
an implementation that frees the buttons right away would also pass. It now holds the reload open
and looks at the buttons while it is running.

Pass 4 — no findings.

Two reported points were declined, with reasons:

  • A request that never answers keeps the buttons disabled forever. True, and true of this screen
    since before this pull request: updating, sending and the decision call itself behave the same
    way, and there is no timeout anywhere in the API layer. Introducing one here would be a change to
    that layer, not to this feature.
  • Two hardcoded German strings on the template button (Vorlage einfügen, Lade Userdaten...) in
    a screen that is otherwise hardcoded English. Real, pre-existing, and untouched by this diff —
    reporting it here rather than fixing it, because routing them through translate() as they stand
    would make German the default value, and changing what the button says is a product decision.

Also worth a reviewer's decision: the three Playwright baselines are -chromium-linux, because they
were taken on Linux. CONTRIBUTING.md asks for the macOS variant, which cannot be produced here —
committing a macOS run's own snapshots next to these adds it. Please either grant that exception
here or ask for the macOS baselines before merge.

Side note from the test work: the screen's tests emitted 45 "not wrapped in act" warnings, from
actions whose reloads landed after the test that caused them. 8 remain, all in tests whose fetch
fails and settles between two awaits.

All checks are green. Coverage on every touched file is 100 % on all four metrics.

@TaprootFreak
TaprootFreak marked this pull request as ready for review August 8, 2026 11:55
@TaprootFreak

Copy link
Copy Markdown
Contributor Author

@mara-steiner please check

@TaprootFreak

Copy link
Copy Markdown
Contributor Author

Thank you — Blocker 1 was right, and it turned out to be the entry point to a whole class rather than
a single case. Point by point, then what the follow-up passes found.

Blocker 1 — the ticket on screen and the ticket being written to

Fixed both ways you offered, because they answer different halves of it. loadIssue now carries the
same token as everything else, so a late answer for the ticket that was left cannot replace the one
on screen; and the three writes take the id from the route again, so a write never depends on a load
having been guarded correctly.

Your reading of how this PR made it worse was accurate: handleUpdate had been moved from the route
parameter onto the loaded ticket, and the two decisions were added beside it.

Blocker 2 — baselines

Done as asked: the three -chromium-linux files are removed. No macOS machine is available here, so
the spec now documents what has to be run and where — npx playwright test e2e/support-dashboard-issue.spec.ts --update-snapshots on a Mac, then commit the five
-chromium-darwin.png files it writes. Five, because of your second point below.

Major — a failed reconciliation re-enabling the buttons

Fixed. loadSuggestion now reports whether the server was actually read, and when the reload fails
alongside the decision the suggestion goes off screen rather than standing there with live buttons.

Major — two polls sharing one epoch

Fixed. Every suggestion fetch takes a number, and only the newest one may write. The epoch stays for
what it is good at — decisions and ticket changes — and the number orders two fetches of the same
ticket against each other, which it could not.

The rest of your list

  • Composer not cleared on a ticket change — fixed, along with the attachment. You were right that
    this PR changes the exposure: the composer now holds text the screen puts there on one click.
  • Two missing visual variants — added to the spec: the decision in flight with both buttons
    closed, and an accepted suggestion sitting in the composer. Five variants in total.
  • Author default with the same clerk on both tickets — fixed, and it needed more than the
    dependency: the author itself is reset with the ticket, because it was also the fallback the
    default fell back to.
  • Prettier — both spots fixed, plus two test files this PR adds. scripts/handbook/build.js is
    left alone: it is not Prettier-clean on develop either, and formatting it would touch lines this
    PR has nothing to do with.
  • act() warnings — down from 45 to 7 by waiting for the reloads the actions trigger. The rest
    are in tests whose fetch fails and settles between two awaits.
  • parseBadges comment — narrowed to what the code does; the project half is still pinned.
  • Test file length — noted, left as it is for now.
  • Hardcoded strings — thank you for the deferral; a separate ticket for the screen's i18n.

What the follow-up passes found

Blocker 1 was one instance of "work started on one ticket writing to the screen showing another", so
the whole screen was walked for the same shape — every state, every await. Five more instances, all
reproduced before they were fixed:

  • the reload after an update ran from a stale closure, so an update answering after the switch
    fetched its own ticket and put it back on screen, form fields included — and the next update would
    have carried those values;
  • the message thread and its poll had no guard at all;
  • a send answering after the switch cleared the composer of the ticket the clerk had moved to;
  • the file preview showed one customer's document over another customer's ticket;
  • the template picker was never closed, so it reopened itself over the next ticket, and its account
    lookup filled templates from the customer whose ticket had been left.

And two that no guard can help with, because the state was already on screen: the thread of the
previous ticket stayed until the new one's own fetch came back — which only starts once the ticket
has brought its uid — and a failed ticket load was never cleared, so one failure blocked every ticket
opened afterwards until the page was reloaded. Both pre-existing, both in the reset this change is
built around.

It is one helper now: take the token where the work starts, ask before writing. The ticket change
drops the author, the thread, the preview, the picker, the errors and the busy flags along with the
draft.

Coverage stays at 100 % on all four touched files, and the screen's own tests went from 83 to 107 —
the 24 new ones pin exactly these paths, each of them failing against the version before its fix.
943 tests pass in total, all checks are green.

@TaprootFreak

Copy link
Copy Markdown
Contributor Author

@mara-steiner please check

The support issue screen shows the newest suggestion still awaiting a
decision above the composer, together with a warning when the conversation
has moved past it. Accepting copies the text into the composer, where it is
edited and sent as the clerk's own message; discarding records the decision.
Suggestions are never written here — they enter through the API alone.

Also splits the default message author out of the ticket load. It read the
clerk list, so the list arriving a moment later reloaded the whole ticket and
put the screen back behind its loading spinner: a second request and a
visible flash on every ticket that was opened.
Closing a preview that is not open, polling a ticket that has no uid, and
unmounting with a preview still open were the paths the screen tests had not
reached yet.
The default author is resolved from the ticket and the clerk list, which
arrive independently. Whichever lands after the clerk picked an author by
hand reset the selection back to the default — visible in CI, where the
clerk list is slower than it is locally.
A change event on a select whose options have not arrived yet is dropped, so
the two tests picking a clerk read the default back. The clerk list now
resolves a tick late in every test, which reproduces the ordering CI runs
into, and the two tests wait for the options they pick from.
The default author is resolved in an effect over the ticket and the clerk
list. A run scheduled before the clerk picks an author carries the state of
the render it was scheduled in, so the state flag guarding it was still false
and the pick was overwritten. A ref is set synchronously and holds.

The screen tests now mock the dashboard hook as one stable object, the way
its useMemo behaves in the app: a fresh object per render changed the
identity of every callback and drove the screen through reload rounds no
production render performs — which is why this only ever failed in CI.
The five guards left uncovered were unreachable rather than untested, so they
are gone instead of pinned: the account id, the ticket id and the attachment
now come from the render that has them, and clearing the file selection
remounts the input rather than writing to a ref that is never null.

The message list hands its file handler a message whose fileName is settled,
which is what removes the last of those guards from the screen; its own
branches — a thread without ids, without a file, without a handler — are
covered by a spec of their own.
The screen had no baseline at all, so this creates one: a spec that drives it
through the three states the suggestion panel has — waiting for a decision,
outdated, absent — plus the metadata entry that titles the flow.

Unlike the older staff specs, everything is mocked: the session token is
synthetic (the frontend only decodes a JWT, it never verifies it) and every
API call is answered from fixtures. The baselines are deterministic, carry no
production data, and need neither a running api nor a seeded database.
Found by review: the handbook recognised only `darwin` baselines, so the three
new ones — taken on Linux — reached the page with neither a project nor a
platform badge, which is precisely the case where that badge carries
information. The snapshot path template is `{projectName}-{platform}`, so both
parts are now read from the name.

The spec says where its baselines came from, and how the macOS variant is
added: Playwright compares against the baseline of the platform it runs on, so
a macOS run writes its own alongside these.
Three findings from review:

The 15-second poll refetched the suggestion even while an accept or discard
was in flight. Its answer carries the state from before that decision, so the
decided suggestion could reappear and invite a second click, which the API
then refuses as a conflict. The poll now skips the suggestion while a decision
is running.

react-router keeps this component mounted when only the route parameter
changes, and neither the offered suggestion nor the "author was picked by
hand" marker were tied to the ticket. Opening another ticket now drops both.

The screen tests resolved the clerk list through a real timer while waiting on
a fixed number of microtask ticks, which is not a wait at all: the file failed
roughly one run in eight, in tests unrelated to whatever had actually raced.
The list now resolves when the test says so, and the waits are on conditions.
Found by review: skipping the poll while a decision runs only stops a new
fetch from starting. One that was already in flight answers with the state
from before the decision — and the busy flag is false again by then, so it
put the decided suggestion back on screen, where the next click earned a
conflict from the API. The same held for a fetch of the ticket that was open
before another one was opened.

Both are now qualified by a token that is raised when a decision starts and
when the ticket changes; an answer whose token no longer matches is dropped,
error included. The reset runs ahead of the loading effects, so raising the
token qualifies the fetch that follows instead of invalidating it.
Found by review: opening another ticket while an accept or discard was still
running left its continuation unguarded — the accepted text landed in the
composer of the ticket the clerk had moved to, the suggestion loaded there was
cleared, and a failure of the old decision was reported on the new screen. The
decision now carries the same token as a fetch does.
Found by review: the flag that blocks the buttons while a decision runs was
not tied to the ticket. Opening another one while a decision was still in
flight left the new ticket's Accept and Discard disabled and its suggestion
poll paused — until the request of the ticket that was left finally settled,
which in the worst case is never.

Opening a ticket now clears the flag, and only the decision the screen is
still on may clear it, so a late answer cannot unblock a decision that has
since started here. The success-path test also asserts what it had only
claimed: the suggestion of the ticket now open survives the late answer.
The API stores a suggestion on the message it answers rather than in a table
of its own, so it no longer has an id separate from that message.
A decision the server has already taken — by another clerk, or in another tab —
came back as an error and left the panel standing with its buttons live, so the
same decision could be sent into the same refusal until the poll happened to
reconcile it up to 15 seconds later. The error now comes with what the server
actually holds.

The tests around it grew the waits the actions they trigger deserve: an update
reloads the ticket, a send reloads the thread, and a test that ended on the
first of those left the rest to land after it. That is what produced most of
this file's "not wrapped in act" warnings, 45 of them; 8 remain, all in tests
whose fetch fails and settles between two awaits.
Three points from review. The busy flag was cleared as soon as the refused
decision returned, which re-enabled Accept and Discard while the suggestion the
server had already decided was still on screen — one round trip in which the
same decision could be sent again. It now waits for the reconciliation.

The reload reports its own failures the same way, so it overwrote the refusal
the clerk actually needed to read. The decision's error is set after it.

The poll effect took the busy flag as a dependency, so every decision tore the
interval down and set it up again — which also postponed the message poll the
same interval carries. It reads a ref now.
The test around the refused decision asserted the state after the
reconciliation, which a version that clears the buttons right away would also
pass. It now holds the reload open and looks at the buttons while it is running.
Review found that the ticket load wrote whatever came back, while everything
around it was guarded. Two loads can be in flight at once — the screen stays
mounted when only the route parameter changes — so a slow answer for the ticket
that was left replaced the one on screen, silently, because the spinner is long
gone by then. This branch had made that worse rather than inherited it: it moved
the update write from the route parameter onto the loaded ticket and added the
two decisions beside it. The load now carries the same token as everything else,
and the three writes take the ticket from the route again.

What else the ticket change now drops: the composer and its attachment. A draft
is written for one customer, an accepted suggestion most of all, and carrying it
into the next ticket is how it reaches someone it was never meant for. The reset
also moved ahead of the default-author effect, which is what lets the default
apply again when two tickets share a clerk — the case where nothing else changes
for that effect to react to.

Two suggestion fetches of one ticket carry the same token, so it could not order
them against each other: a poll outliving the 15-second interval let the next one
start, and the slower answer won. Each fetch takes a number now.

And a reconciliation that fails leaves nothing behind: if the reload after a
refused decision fails too, the suggestion goes off screen instead of standing
there with live buttons.
An adversarial pass over the concurrency work found the same class of defect in
four more places, and a second pass found a fifth. All of them share a shape:
work started on one ticket writing to the screen showing another.

- The reload after an update ran from a stale closure, so an update that
  answered after the switch fetched the ticket it belonged to and put it back on
  screen, form fields included — the next update would then carry those values.
- The thread and its poll had no guard at all: the messages of the ticket that
  was left replaced the ones on screen.
- A send that answered after the switch cleared the composer of the ticket the
  clerk had moved to, taking a draft written for someone else.
- The file preview, sticky by design, kept showing one customer's document over
  another customer's ticket.
- The default author read the ticket on screen while it was still the previous
  one, so its clerk became the default for a ticket that has no clerk of its own.

The guard is one helper now, taken where the work starts and asked before it
writes, and the ticket change drops the author, the preview and both busy flags
along with what it already dropped.
Two further passes over the same question. The guards stop a late answer from
writing to the wrong ticket, but they cannot help with what is already on
screen: the thread of the ticket that was left stayed until the new one's own
fetch came back, and that fetch only starts once the ticket has brought its uid.
For that moment one customer's conversation sat under another customer's ticket.

The same for what the screen reports. A failed ticket load blocks this screen
entirely and was never cleared, so one failure blocked every ticket opened
afterwards until the page was reloaded; an action error stayed as a banner over
a ticket it had nothing to do with. Both are pre-existing, both belong to the
reset this change is built around.

Also dropped the manual revoke of the preview url — the effect watching that
state already does it — and formatted the two new test files.
A third pass walked every state and every await of the screen. Two were still
open, both reproduced before they were fixed:

The file preview wrote whatever came back. An attachment opened on one ticket
and answering after the switch put one customer's document on another
customer's ticket, on a panel that stays until it is closed by hand.

The template picker was worse. Nothing ever closed it, so it reopened itself
over the next ticket as soon as that ticket had loaded — and when its account
lookup answered after the switch, the templates were filled from the account of
the customer whose ticket the clerk had left.

Both take the guard now, and the ticket change closes the picker along with
everything else it drops.
The reset closes it, but nothing held that: removing the line left all 107 tests
green while the picker stayed open over the next ticket, showing the transaction
list of the account that was left.
The account id is required, so the disabled state of the template button could
only ever be decided by the loading flag — the other half was left over from an
optional chain this branch unwrapped, and the test that covered it constructed
an account the API cannot return. Both are gone, like the five unreachable
guards before them.

The suggestion type also loses the two fields the screen never reads.
@TaprootFreak
TaprootFreak force-pushed the feat/support-reply-suggestions branch from d4fa58f to c11fb88 Compare August 10, 2026 10:22
@TaprootFreak

Copy link
Copy Markdown
Contributor Author

Thank you — and a note on timing before the points: this review landed on 0957202e, three commits
before the head at the time. #3 and #4 were being fixed as it was written, which is why the code
already reads differently. Everything is answered below against the current head, now rebased onto
develop.

Blocker 1 — no baseline is committed

Declared as a deviation in the description, under the section develop gained in #1292: the spec and
the metadata entry are here, the five .png files are not, because they belong on macOS and no macOS
machine was available to make them.

You already have them. Your run wrote all five and passed against them on the second pass — if you
commit those files to this branch, the deviation is gone and nothing else about it changes. If you
would rather not carry someone else's baselines, say so and I will find a Mac before this merges;
what I will not do is rename a Linux capture to -darwin.

Blocker 2 — the description described a different branch

Fixed. It now names the five variants, states that no baselines are committed, and declares the
deviation with its reason. The list of defects is also brought up to date — it had grown from six to
thirteen while the description still said six.

Rebased onto develop, so the guideline you refer to is in the branch under review.

Major 3 — the message thread

Fixed in 09d4967e and 0957202e, which is where your review crossed with the work. loadMessages
and pollForNewMessages carry the token, and the [id] effect clears messages and pendingCount
next to the composer reset — the deterministic half you describe, which the token cannot help with.

Both of your reproductions have a test now: the late answer is dropped, and the thread is off screen
the moment the ticket changes.

Major 4 — the file preview

Fixed in the same two commits. openFile carries the token, and the preview is cleared with the
ticket, revoking its object url through the effect that already watches that state.

The same pass found the template picker in the same condition — nothing ever closed it, so it
reopened itself over the next ticket, and its account lookup filled templates from the customer whose
ticket had been left. Also fixed, also tested.

Since these are pre-existing and you offered the exception: they are fixed here rather than deferred.
Along with the comment you quoted, which now describes what the code does.

Minor 5 — act(...) warnings

Seven when you looked, four now. The remaining four are two tests each in the ticket-load and
suggestion-load error paths, where a rejected fetch settles between two of the awaits renderScreen
makes. Wrapping that sequence in a single act scope deadlocks the waitFor inside it, so closing
them properly means restructuring the helper — worth doing, but I would rather not fold it into this
branch. Say the word if you want it here anyway.

Minor 6 — the dead guard

Removed, and the test that covered it with it: it constructed an account without an id, which the
type does not allow and the API does not send. That makes six unreachable guards removed rather than
tested, which is the same reasoning as the five in the description.

Nits

  • state and handled are gone from SupportReplySuggestion — the screen reads neither.
  • openTemplatePicker's early return: I tried putting it back and took it out again. It cannot be
    reached — the button is disabled for the whole time the request runs, and the second click would
    have to land in the same event as the first. It would have been the seventh unreachable guard.
  • parseBadges: agreed, and thank you for checking it against every name in the directory.

Coverage is still 100 % on all four touched files; 943 tests pass; all checks are green.

@TaprootFreak

Copy link
Copy Markdown
Contributor Author

@mara-steiner please check

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant