Skip to content

Act on the desktop customer-feedback pass (#411-#418) - #423

Merged
vishaltandale00 merged 9 commits into
mainfrom
claude/customer-feedback-issues-f07a5a
Sep 4, 2026
Merged

Act on the desktop customer-feedback pass (#411-#418)#423
vishaltandale00 merged 9 commits into
mainfrom
claude/customer-feedback-issues-f07a5a

Conversation

@vishaltandale00

@vishaltandale00 vishaltandale00 commented Sep 2, 2026

Copy link
Copy Markdown
Owner

Acts on eight pieces of desktop customer feedback, filed as #411-#418.

They ship together because five of them land in the same minified styles.css
lines, which cannot be split into separately reviewable hunks.

Behavior

Issue Problem Fix
#411 The updater checked once per launch (window.mjs, 5s after load). A session left open past a release never learned it was behind. createDesktopUpdater owns a recurring check beside the startup one. Both are guarded by backgroundCheck, so neither disturbs a phase the user is already watching. autoDownload stays false, development never polls, the timer is disposed on shutdown.
#413 Sign-in opened the browser without asking for it to come forward. The customer had to click Chrome in the Dock. All five openExternal seams pass { activate: true } explicitly instead of relying on an unstated platform default.
#415 A settled callback left the user in a spent browser tab reading 127.0.0.1:…/auth/callback, while the app had already moved on behind them. The callback handler presents the Relayer window for every outcome it decides — verified, cancelled, or malformed. Ownership is established before any mutation, so a superseded or foreign callback neither settles nor presents the attempt that owns the listener.

Presentation

Issue Fix
#412 Onboarding actions become plain labels — Connect, Back, Continue, Refresh models, Repair — in both the first-run screen and the settings dialog, each carrying an accessible name that states the operation and the exact provider. Nested boxes go: the harness summary and member rows lose their enclosures, the custom-family group becomes one left rule, and the connection field is outlined rather than filled.
#414 The light-theme lavender gradient becomes var(--bg) — the same #fafafa BrowserWindow paints before first render, so there is no colour shift on launch.
#416 .workspace-layout gains top padding at all three breakpoints. The header row and Environment panel no longer start flush against the window chrome, and the top inset matches the right one.
#417 The model picker takes its surface from theme tokens instead of #1b1e21/#fff, the family <select> stops outweighing the models it filters, supporting text rises 9px → 11px, and spacing lands on one 4px rhythm.
#418 The account control moves from a fixed bottom-right pill into the sidebar footer beside Settings, both drawn as stroked marks. The :has() rule that made .node-context-dock-actions reserve 112px for that pill goes with it.

Connection lifecycle

Review found that #413 and #415 rest on a distinction the service did not make:
which failures of a browser leg are over, and which leave the attempt live. That
brought the pending-connection lifecycle into scope, recorded as BRW-001 to
BRW-005 in the PRD.

  • A pending attempt is owned in the main process by the renderer that began it,
    and is cancelled when that renderer's contents are destroyed. Before this, a
    closed window held the provider name — and its runtime — until the app
    restarted.
  • A failed browser handoff cancels the attempt it created.
  • Both shipping managed adapters collapse every runtime, process, protocol and
    parse failure into one unavailable result, so a check that cannot reach a
    verdict keeps the attempt pending only for a bounded run, then settles and
    frees the name. An aborted check settles at once. Only disconnected means
    the login is still open.
  • Only a failure that settled the attempt returns to Relayer. A transient one
    leaves the user in the browser flow they are still completing.
  • Provider change notifications are best-effort at all six send sites. Sending
    into destroyed contents was turning a settled connection into a rejection.
  • Pending cleanup runs its stages independently, so a rejecting close() no
    longer skips runtime-state and credential removal.

Two things reviewers should look at

#418 reverses a recorded decision. ADR 0008 specified the account control
"anchored to the bottom-right of the application viewport rather than
participating in sidebar layout". The customer asked for the opposite. This
implements the customer's request and updates ADR 0008, docs/architecture.md,
PRD ACC-008, and the evidence-capture script that asserted the corner geometry,
keeping the reason in the ADR.

#415's in-app fallback is not here. The activation and return fixes are, but
the fallback that re-opens the sign-in page while signing-in needs a new IPC
channel, preload surface, and control. The original failure also still wants
reproduction against a packaged macOS build with Chrome cold and already
running. #413 stays open for that remainder.

Verification

npm run check and npm run build pass on the final tree — 2061 deterministic
Vitest cases plus the process-bound Chromium evidence capture.

Layout and accessible names are computed against real Chromium, not asserted
from source, because jsdom has no layout engine and this repository has no DOM
runtime for Vitest. The footer audit measures line boxes from Range rects at
960, 981, 1280 and 1420px and rejects truncation. The update indicator is driven
through the renderer's own subscription across all four visible phases, reading
the rendered accessible name and proving activation opens the details rather
than performing the nested action. Two new first-run variants render both
recovery branches through auth.js.

Every checkpoint added in the last two rounds was verified to fail when the
behavior it claims is removed.

Follow-ups

@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.
To continue using code reviews, add credits to your account and enable them for code reviews in your settings.

@vishaltandale00 vishaltandale00 left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Review of the exact PR head. I found one P1 and four P2 issues; details are attached inline.

Comment thread desktop/main/services/desktop-account-service.mjs
Comment thread desktop/main/ipc/register-ipc.mjs Outdated
Comment thread desktop/main/window.mjs
Comment thread desktop/renderer/index.html
Comment thread desktop/renderer/src/provider-ui.js
@vishaltandale00

Copy link
Copy Markdown
Owner Author

All five findings are addressed in 2391711e, replied to inline. Ready for the next pass.

Finding Fix
P1 stale callback A foreign state is refused without ending or presenting the attempt that owns the listener
P2 provider focus provider-connect-complete presents on a terminal connected result, covering connect and reconnect
P2 launch check Both scheduled discoveries share one guarded backgroundCheck; the launch check fires at most once per process
P2 footer overflow Indicator no longer shrinks; the footer drops its labels below 980px. 960px re-measured at 191/191 with the indicator back to 31px
P2 label context Short visible label, accessible name carrying the operation and the exact provider, PRD updated to record the split

Two things to look at rather than take on trust

The P1 fix changes what a state mismatch does. It no longer ends the
attempt, which contradicted fails closed on state mismatch and rejects the late valid callback, so that test is rewritten rather than worked around. My reading
is that the property is unchanged and slightly stronger — no token exchange
either way, and a stray loopback request can no longer cancel a legitimate
sign-in — but it is a deliberate change to a security-adjacent assertion and
should be judged as one, not skimmed.

The PRD had drifted. It still promised Refresh models and set up defaults
after the earlier pass shortened the button, so product copy and its authority
disagreed. It now records the label/accessible-name split. If you would rather
the visible label return to the full phrase, that is a one-line revert.

Evidence

npm run check and npm run build pass on the head commit: 2088 passed, 2 skipped.

The two account-session tests were confirmed failing against the previous
callback handler before the fix, so they are regression tests rather than
companions to it. New coverage: same-port replacement, foreign state leaving the
attempt live, provider completion presenting only when connected, launch check
guarded and not repeated on window recreation, and the footer contract at the
narrow breakpoint.

Footer numbers are measured in the running app over CDP at a 960px viewport, not
derived.

Not addressed here

Two earlier observations are still open by choice: the footer pairs a glyph gear
with a drawn person mark, and the optional-account step keeps its warm radial
scrim while #414 flattened the two surfaces that carried the lavender gradient.
Both are small if you want them in this PR.

@vishaltandale00 vishaltandale00 left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Review of the latest iteration at 2391711. The prior updater and provider-card findings are resolved. I found one P1 and four P2 issues; details are attached inline.

Comment thread desktop/main/services/desktop-account-service.mjs Outdated
Comment thread desktop/main/ipc/register-ipc.mjs Outdated
Comment thread desktop/renderer/styles.css Outdated
Comment thread desktop/renderer/index.html
Comment thread desktop/renderer/src/auth.js
@vishaltandale00

Copy link
Copy Markdown
Owner Author

All five addressed in 68d7498b, replied inline. npm run check and npm run build pass: 2089 passed, 2 skipped.

The pattern in this round

Four of the five were siblings of first-round fixes — one callback branch, one
breakpoint, one code path per requirement — so this time each fix was checked
against its neighbours before being called done. That sweep caught a regression
in my own change: moving the gear glyph into a span to hide it from assistive
technology would have made both label-hiding rules hide the glyph itself,
trading a naming defect for a blank button. Both rules now spare it.

The sharpest finding is that the test I added last round asserted the P1 bug.
It sent /favicon.ico and expected the attempt to error and present, which is
exactly the behaviour you reported. I fixed the state-mismatch branch and then
wrote a test that locked in the same defect one branch earlier.

Measured, not asserted

Footer geometry re-measured in the running app over CDP:

Viewport Label height Wrapped Footer overflow Settings name
960 label hidden no no Settings (was )
981 / 1000 / 1420 15px (was 30px) no no Settings

PRD

One checkpoint added, naming which outcomes own presentation: every terminal
outcome of a browser leg returns to Relayer — account sign-in, cancellation and
failure, and a provider connection or reconnection that succeeds or fails —
while a pending attempt and a request that is not the attempt's own callback
never present.

Still open by choice

The footer pairs a glyph gear with a drawn person mark, and the optional-account
step keeps its warm radial scrim. Both small if you want them here.

vishaltandale00 and others added 5 commits September 2, 2026 23:47
Eight reported problems, filed as #411-#418 and fixed together because five of
them land in the same minified stylesheet lines and cannot be split into
reviewable hunks.

Behavior:

- #411 The updater checked once per launch, so a session left open past a
  release never learned it was behind. createDesktopUpdater now owns a
  recurring check beside the startup one. A poll is skipped in every in-flight
  phase, so it never resets progress the user is watching; autoDownload stays
  false; development never polls; the timer is disposed on shutdown.
- #413 Sign-in opened the browser without asking for it to come forward, and
  the user was left staring at an unchanged Relayer window. All five
  openExternal seams now pass { activate: true } explicitly.
- #415 A settled callback left the user in a spent browser tab. The callback
  handler now presents the Relayer window for every outcome it decides —
  verified, cancelled, or malformed — reusing the existing presentation path. A
  superseded callback and a plain timeout present nothing, and a window that
  cannot be presented never fails a sign-in.

Presentation:

- #412 Onboarding actions become plain labels (Connect, Back, Continue,
  Refresh models) in both the first-run screen and the settings dialog, and the
  nested boxes go: the harness summary and member rows lose their enclosures,
  the custom-family group becomes one left rule, and the connection field is
  outlined instead of filled.
- #414 The light-theme lavender gradient on the first-run and new-thread
  surfaces becomes var(--bg), the same #fafafa BrowserWindow paints before
  first render, so there is no colour shift on launch.
- #416 .workspace-layout gained top padding at all three breakpoints. The
  header row and Environment panel no longer start flush against the window
  chrome, and the top inset matches the right one.
- #417 The model picker takes its surface from the theme tokens instead of
  #1b1e21 and #fff, the family select stops outweighing the models it filters,
  supporting text rises from 9px to 11px, and spacing lands on one 4px rhythm.
  Drops a stylesheet line that repeated its predecessor verbatim minus one
  rule, which would otherwise have needed every picker edit applied twice.
- #418 The account control moves from a fixed bottom-right pill into the
  sidebar footer beside Settings, and the :has() rule that made
  .node-context-dock-actions reserve 112px for that pill goes with it. The
  collapsed 58px rail stacks its footer, because two controls do not fit side
  by side.

#418 reverses the placement recorded in ADR 0008; the ADR, architecture doc,
PRD ACC-008, and the evidence-capture script that asserted the corner geometry
are updated to match, with the reason kept in the ADR.

Not covered: #413 also asks for an in-flight fallback that re-opens the sign-in
page from inside the app. That needs a new IPC channel, preload surface, and
control, and the original failure still wants reproduction against a packaged
macOS build with Chrome both cold and already running. #413 stays open for it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Two pieces of follow-up feedback on the desktop pass.

The recovery button read "Repair execution configurations", and its sibling
"Refresh models and set up defaults". Both are now "Repair" and "Refresh
models". The earlier pass shortened the refresh button in index.html, but
auth.js overwrites that text on every render:

    recoveryButton.textContent = recovery?.label ?? "Refresh models and set up defaults";

so the markup change never reached the screen. The labels now change at the
sources that actually produce them, in provider-onboarding-model.js and
provider-ui.js, and in that fallback. The two blocking-reason strings stay full
sentences; they explain why setup is blocked rather than labelling an action, and
"Repair." alone would explain nothing.

The account control's mark was the glyph U+25CE, which reads as a bare circle
next to real iconography. It becomes a drawn person in the sidebar's existing
stroked-SVG idiom, matching the collapse and new-thread marks.

Carrying an inline SVG meant the label could no longer be the button's whole
text, because the controller assigns textContent and would erase it. The label
moves into its own span, which the controller writes when present and falls back
to the button when absent, so callers passing a bare element still work. That
also lets the collapsed rail reuse `body.sidebar-collapsed .footer-button span`,
the rule every other footer control already uses, so the font-size:0 workaround
added with the corner-pill move is gone. `#desktopAccountButton.textContent`
still resolves to the label alone, so the provider-ux evidence script's
"Signing in…" assertion is unaffected.

Verified in the running app against a copy of a real profile: the mark renders,
the labels are short, and the model picker shows real members at the intended
13px/11px scale.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Five findings, one P1 and four P2.

P1. A stale callback cancelled the login that replaced it. When a login is
replaced on the same loopback port, the predecessor's callback is served by the
replacement's listener, and the state check treated that as a malformed callback
of the active attempt: it settled the replacement as error and presented the
window. State is what identifies which attempt a callback belongs to, so a
foreign state is now answered 400 and otherwise ignored, leaving the owning
attempt in flight and presenting nothing. A callback that does carry this
attempt's state and is still malformed keeps failing closed as before.

This changes what a state mismatch does: it no longer ends the attempt. The
security property is unchanged and slightly stronger — no token exchange, and a
stray loopback request can no longer cancel a legitimate sign-in.

P2. Provider completions never restored focus, though #415 required it. Connect
and reconnect both settle in relayer:provider-connect-complete, which had no
presentation authority. It now presents on a terminal connected result only.

P2. The launch check bypassed the phase guard the poll uses, and re-fired on
every window creation, so recreating a window while an update was downloading or
staged could reset it to checking. Both scheduled discoveries now run through
one guarded backgroundCheck, and the launch check is scheduled at most once per
process. window.mjs no longer schedules its own.

P2. The footer overflowed the 210px rail the 980px breakpoint switches to. The
measured content box was 191px against 196px of content, and the circular update
indicator was being flex-shrunk from 31px to 24.53px. The indicator no longer
shrinks, and the footer drops its labels below 980px exactly as the collapsed
rail does. Re-measured at 960px: 191px against 191px, indicator back to 31px.

P2. The shortened recovery labels left several provider cards exposing the same
bare accessible name, and the PRD still promised the long wording. The visible
label stays short; the accessible name now carries the full operation and the
exact provider, and the PRD records that split.

Tests. The two account-session tests were confirmed to fail against the prior
callback handler before the fix. New coverage coming out of the review: a
same-port replacement whose predecessor's callback must not cancel or present
it; a foreign state that leaves the attempt live and lets its own callback still
complete; provider completion presenting only when connected; the launch check
guarded and not repeated on window recreation; and the footer contract at the
narrow breakpoint.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Second review round. Four of the five findings were siblings of fixes from the
first round that were applied to one instance and not swept, so each fix here
was checked against its neighbours.

P1. Ownership is now established before anything may touch the attempt. The
previous round fixed the state-mismatch branch but left the method/host/path
branch settling and presenting, so the browser's own /favicon.ico probe of the
callback page cancelled a live sign-in and pulled the window forward. Both
branches now answer 400 and return without settling. Only a request carrying
this attempt's state may settle it.

The test added last round asserted the favicon behaviour, so it encoded the bug
as intent. It now asserts the opposite, alongside the wrong path, method and
host cases the pre-existing test covered, and proves the listener survives all
of them by completing the real callback afterwards. The malformed-shape case
keeps its own coverage with a callback that does carry the attempt's state.

P2. completeConnection() can throw for terminal post-browser failures such as
catalog discovery or runtime registration. The await preceded presentWindow(),
so those left the browser in front of an error the renderer reported behind it.
Every terminal outcome now returns to Relayer; only a still-pending attempt
presents nothing. The PRD records which outcomes own presentation.

P2. Hiding the footer labels below 980px left the Settings button named by its
gear glyph. The glyph is now decorative and the button carries an explicit name,
as does the update indicator, which was named by its arrow for the same reason.
Both label-hiding rules were then corrected to spare the glyph: moving it into a
span would otherwise have made the collapsed rail hide it too, trading a naming
defect for a blank button.

P2. Above the breakpoint the 244px sidebar left the account label 68.1px for
text that needs 69.8px, so "Signing in…" wrapped to two lines inside a 34px
control. Footer labels no longer wrap, and slightly tighter padding keeps the
common case from ellipsizing. Measured at 960, 981, 1000 and 1420px: no wrap, no
overflow, the indicator at its full diameter, and Settings named "Settings".

P2. The PRD requirement added last round for a contextual accessible name was
applied to the provider cards and missed first-run. auth.js now sets the same
name from the onboarding provider.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The ready-phase guards landed in #443 explaining themselves in terms of "the
window opens a periodic check five seconds after launch". This branch moved that
check into the updater and put it behind backgroundCheck, so the sentence no
longer describes the code around it.

The guards still matter, for a different caller: scheduled discovery is stopped
earlier by backgroundCheck, so what these guards now protect is the explicit
check from Settings, which stays unguarded because the user asked for it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@vishaltandale00
vishaltandale00 force-pushed the claude/customer-feedback-issues-f07a5a branch from 68d7498 to 94bebfa Compare September 3, 2026 03:53

@vishaltandale00 vishaltandale00 left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Review of exact head 94bebfa. I found two P2 issues; details are attached inline.

Required proof gaps:

  • The new browser-outcome promise in docs/prd/index.html has no named checkpoint or real-service IPC test distinguishing terminal, pending, stale, and unowned outcomes.
  • test/desktop-account-ui.test.mjs verifies raw strings rather than computed footer geometry and accessibility.
  • test/provider-ui.test.mjs searches source text instead of executing both recovery-name branches.

Comment thread desktop/main/ipc/register-ipc.mjs
Comment thread desktop/renderer/index.html Outdated
…phase

Third review round: two findings and three proof gaps.

P2. The catch added last round presented for every completeConnection()
rejection. Those rejections are not equivalent. completeConnection cancels the
pending connection for a reconnect account failure, an unavailable reconnect
login, and any catalog, readiness or runtime-registration failure. It does not
cancel for an unknown connection, or for a connect whose account check failed
transiently, which leaves the attempt live and cancellable. Presenting there
pulls focus out of a browser flow the user is still completing, which is the
boundary this branch put in the PRD one round earlier.

Terminality is now recorded where the attempt is actually settled, on the error,
under a registry symbol shared with the IPC layer. Presentation asks that
question instead of treating any throw as the end.

P2. The update indicator carried a fixed name of "Application update available"
while the button is visible for available, downloading, ready and failed. It
announced an update after a failed check and never announced a staged one. The
name now follows the phase.

Proof gaps.

The browser-outcome promise had no named checkpoints. It is now BRW-001 through
BRW-003, splitting terminal from transient, unknown and pending, and the IPC
test covers those four outcomes rather than success and failure alone.

The recovery-name assertions read auth.js source text and searched rendered
markup for a prefix. Both recovery branches are now executed and their rendered
accessible names compared exactly, including that two cards on one screen do not
collide.

The footer geometry assertions checked CSS strings. jsdom has no layout engine,
so a unit test cannot answer whether the footer fits or a label wraps. Those
questions moved to capture-provider-ux-video.mjs, which measures the rendered
footer in Chromium: overflow, the indicator's diameter, whether a busy label
wraps, and the Settings and indicator accessible names. The remaining CSS
assertions say only that the rules they depend on exist, and say so.

updateIndicatorName moved into its own module because updates.js cannot be
imported without a DOM, so the naming can be exercised directly across every
phase. The sealed telemetry inventory records the new module.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@vishaltandale00

Copy link
Copy Markdown
Owner Author

Both findings and all three proof gaps addressed in 82d8a5e9, replied inline.
npm run check and npm run build pass: 2099 passed, 2 skipped.

Proof gaps

Named checkpoints. The browser-outcome promise is now BRW-001 to BRW-003,
split so the boundary is stated rather than implied: terminal outcomes return;
a transient failure that leaves the attempt live does not; an unknown or stale
connection, a pending attempt, and a request that is not the attempt's own
callback do not. The IPC test covers those four outcomes rather than success and
failure alone.

Recovery names are executed, not searched. Both branches now render and their
accessible names are compared exactly, including that two cards on one screen do
not collide. The auth.js source grep is gone.

Footer geometry moved to where layout exists. One caveat worth stating
plainly rather than papering over: jsdom has no layout engine, so a unit test in
this suite cannot answer whether the footer fits or a label wraps —
getBoundingClientRect returns zeros. Those questions moved into
capture-provider-ux-video.mjs, which measures the rendered footer in Chromium:
overflow, the indicator's diameter, whether a busy label wraps, and the Settings
and indicator accessible names, failing the capture if any regress. The CSS
assertions that remain now claim only that the rules they depend on exist, and
say so in a comment.

One structural change

updateIndicatorName moved into its own module. updates.js cannot be imported
outside a DOM — I verified that rather than assuming it — so the naming could
not be exercised where it lived. The sealed telemetry inventory records the new
module.

Judgement call to check

For the terminal-failure boundary you offered two options. I took the typed
result rather than "settle every thrown failure", because settling every throw
would make a transient account check cancel a live connection — trading a focus
bug for data loss. If you intended the other reading, it is a small change.

Still open by choice

The footer pairs a glyph gear with a drawn person mark, and the optional-account
step keeps its warm radial scrim.

The two presentation items left open through the review rounds.

The footer paired a glyph gear with a drawn person mark, so the two controls read
at different weights beside each other. Settings is now drawn in the same
stroked idiom, and the sizing rule covers both marks rather than the account one
alone. The gear was the only remaining decorative glyph in the footer, so the
label-hiding rules keep their aria-hidden guard for whatever is added next.

The optional-account step was the last surface still carrying a coloured wash:
a warm radial in light, a cool one in dark, while the first-run and new-thread
surfaces had already been flattened. It now uses the themed ground, which makes
the light override unnecessary rather than merely different. Verified in the
running app: backgroundImage "none", rgb(250,250,250) in light and rgb(11,12,13)
in dark.

The rendered footer audit added in the previous commit reported a wrap here, and
it was the audit that was wrong. It compared the label's natural text width
against its box, which flags an ellipsis as a wrap. With nowrap the label
truncates instead, and a truncated label is not a wrapped one. The audit now
counts line boxes, which is the question it was meant to ask.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

@vishaltandale00 vishaltandale00 left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Reviewed exact head c0466c5. In addition to the inline findings, BRW-001 through BRW-003 still appear only in the browser-behavior narrative; they are absent from the product-requirements list and the verification table. No deterministic checkpoint currently crosses the real service → IPC classification boundary or verifies renderer ownership after a transient completion rejection.

Verification on this head: the focused provider/UI portfolio passed 141 tests; npm run build and full PR git diff --check passed. Hosted deterministic Vitest, quick checks, TypeScript builds, and PRD readability are green. Local npm run check stopped at the known harness failure because Cargo did not provide CARGO_BIN_EXE_relayer-app-server; all preceding Rust tests passed.

Comment thread desktop/main/providers/provider-definition-service.mjs Outdated
Comment thread desktop/main/providers/provider-definition-service.mjs Outdated
Comment thread desktop/main/ipc/register-ipc.mjs Outdated
Comment thread desktop/renderer/src/update-indicator-model.js Outdated
Comment thread desktop/renderer/index.html
Comment thread scripts/capture-provider-ux-video.mjs Outdated
Comment thread test/provider-ui.test.mjs Outdated
…kpoints

Fourth review round: one P1 and six P2.

P1. A connect whose account check failed rejected while the backend kept the
pending attempt. completeConnection is the caller's poll, and its loop in
provider-settings-connection.js continues only on "pending"; a rejection exits
it and the finally releases ownership. The attempt then kept owning the provider
name with nobody polling it, so the retry was refused and the setup was
invisible. A connect that has not succeeded yet now stays pending, carrying why,
so the loop keeps polling and the attempt stays cancellable. Reconnect still
settles and reports terminally, because there the service cancels the attempt.

Terminality is now a type this module owns, constructed before any cleanup runs.
Stamping a property onto the provider's error threw an unmarked TypeError on a
frozen error, and marking after cleanup let a cleanup rejection replace the
failure being reported. The original error is preserved as the cause.

connect() and reconnect() create the pending attempt before handing the URL to
the browser, and a rejected handoff left it owning the provider name with
nothing downstream aware of it. The handoff now cancels what it created before
propagating.

The update indicator named actions its button does not perform. Clicking it
opens the popover; restart, retry and download belong to the nested control. The
names now state the phase and then the operation this button actually performs.

The drawn settings mark is wider than the glyph it replaced, and at the standard
244px footer with the update indicator visible it truncated both labels to
"Settin…" and "Signing i…". Measured before and after: 50/49 and 70/69 truncated,
now 50/50 and 70/70.

The rendered footer audit was unsound twice over. getClientRects().length cannot
prove a single line for a blockified flex item, it ran only at 1280px, and it
accepted truncation, which is how it missed the regression above. It now counts
line boxes from range rects, rejects truncation, and runs at 960, 981, 1280 and
1420. Confirmed against the reverted footer: it fails with "The account label is
truncated at 981px", the breakpoint boundary the old probe never visited.

The first-run recovery checkpoint rendered Settings provider-card markup, not
auth.js. The naming decision moved into provider-onboarding-model beside the
action it pairs with, and both first-run branches are exercised there against
the function auth.js calls.

BRW-001 to BRW-003 existed only in prose. They are now numbered requirements
with verification-table rows, joined by BRW-004 for the browser handoff, and
each row names the deterministic proof that carries it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@vishaltandale00

Copy link
Copy Markdown
Owner Author

All seven addressed in 3b88325b, replied inline. npm run check and
npm run build pass: 2103 passed, 2 skipped.

The P1 settles a question I left open

Last round I argued against "cancel before rejecting" because it would kill a
live attempt. That was right, and it was the wrong conclusion: the answer was
not to reject at all. provider-settings-connection.js polls on pending and
releases ownership in a finally, so rejecting ends the poll and strands the
attempt. A connect that has not succeeded yet now stays pending, carrying why.

Named checkpoints

BRW-001 to BRW-004 are now numbered requirements with verification-table rows,
each naming the deterministic proof that carries it. BRW-004 is new, for the
browser handoff that could orphan an attempt.

The service to IPC classification boundary is now crossed by tests against the
real ProviderDefinitionService: a transient connect stays pending and
cancellable, and a reconnect settles before reporting terminally.

I verified the audit fails before trusting it

The footer audit was unsound twice. This time I reverted the footer fix and
confirmed it reports The account label is truncated at 981px — the breakpoint
boundary the old probe never visited. It now counts line boxes from range rects,
rejects truncation, and runs at 960, 981, 1280 and 1420.

Three of these were mine to begin with

The wider drawn gear truncated both footer labels, the indicator names promised
work the button does not do, and the first-run checkpoint tested Settings cards
rather than auth.js. Each came from a fix in an earlier round, which is the
pattern this review keeps surfacing: I have been fixing the reported instance
and not the shape of the mistake.

@vishaltandale00 vishaltandale00 left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Reviewed exact head 3b88325b. The prior update-name, footer-width, wrapping-audit, terminal-wrapper, transient-ownership, and browser-handoff fixes are present; the rendered footer and accessibility states now pass at 960/981/1280/1420. I found one P1 and seven P2 issues, attached inline.

Required verification: the affected provider/UI portfolio passed 157 tests, including the Chromium evidence capture; npm run build and full-PR git diff --check passed. Hosted deterministic Vitest, quick checks, TypeScript builds, and PRD readability are green. Local npm run check again stopped only at the known harness problem where Cargo did not provide CARGO_BIN_EXE_relayer-app-server, after the preceding Rust tests passed. Adversarial reviews are non-certifying.

Remaining mapping gaps include no checkpoint for renderer destruction/window recreation, notification failure, or teardown rejection. The new terminal wrapper also lacks committed regression cases for a frozen cause and rejecting cleanup.

Comment thread desktop/main/providers/provider-definition-service.mjs Outdated
Comment thread desktop/main/providers/provider-definition-service.mjs Outdated
Comment thread desktop/main/providers/provider-definition-service.mjs
Comment thread desktop/main/ipc/register-ipc.mjs
Comment thread test/provider-adapters.test.mjs Outdated
Comment thread test/provider-onboarding-ipc.test.mjs
Comment thread test/provider-onboarding-model.test.mjs
Comment thread test/desktop-account-ui.test.mjs
A pending managed connection reserves the provider name, but the poll that
settles it and the ownership that cancels it lived only in renderer memory.
Round 4 moved the leak's trigger from "a transient failure rejects" to "the
renderer stops polling"; it did not close the leak.

Give the attempt an owner in main. The renderer that begins it owns it, and its
contents being destroyed cancels it, so closing a window frees the name instead
of holding it until the app restarts. Both shipping managed adapters collapse
every runtime, process, protocol and parse failure into one `unavailable`
result, so a check that cannot reach a verdict is transient only for a bounded
run; after that the attempt settles and readmits the name. An aborted check
settles at once. Only `disconnected` means the login is still open.

Pending cleanup now runs its stages independently: sequentially, a rejecting
close() skipped runtime-state and credential removal while the runtime was
already gone from both collections that could revisit it. Provider change
notifications become best-effort at all six send sites; sending into destroyed
contents was turning a settled connection into a rejection, before the browser
return it precedes.

The four mapped checkpoints move to the seams that ship. BRW-002 and BRW-004
drive the production poll loop and a real definition service, and assert the
name is readmitted rather than that cancelConnection was called. The first-run
recovery name and the update indicator are proven in the Chromium capture
against rendered DOM, since this repository has no DOM runtime for Vitest: the
capture drives real update phases through the renderer's own subscription
instead of posing the control, and two new first-run variants render both
recovery branches. Every new checkpoint was verified to fail when the behavior
it claims is removed.

Issue #448 tracks the two ways an attempt can still outlive its owner: an
abandoned sign-in whose account stays disconnected, and a reload that replaces
the JavaScript context but keeps the same contents.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@vishaltandale00

Copy link
Copy Markdown
Owner Author

Round 5 addressed at 8164e7fa. npm run check and npm run build pass; 2061 Vitest cases plus the process-bound Chromium capture.

The P1 and one P2 were the same defect

pendingConnections reserves the provider name, and nothing in main ever released it. Round 4 moved the leak's trigger — from "a transient failure rejects" to "the renderer stops polling" — without closing it. Both of your findings on that file are that one leak from two angles.

The attempt now has an owner in main: the renderer that begins it, cancelled when its contents are destroyed. And a check that cannot reach a verdict is transient for a bounded run rather than forever, because claude-subscription.mjs:64 collapses every failure into unavailable.

Decisions taken, with the alternatives named

Bounded, not typed. Three consecutive verdict-less checks, counter reset by any disconnected. Typed retryability is the better end state but changes the adapter contract and both shipping implementations — more than a customer-feedback PR should carry.

Bind now, file the rest. Two ways to outlive an owner remain, and both need a timeout duration that is a product decision with no evidence yet: an abandoned sign-in whose account stays disconnected while the renderer lives, and ⌘R, which replaces the JS context but keeps the same webContents so destroyed never fires — reachable because no setApplicationMenu exists anywhere in the repo. Filed as #448 and referenced from BRW-005 rather than left silent.

Every new checkpoint was verified to fail

Mutation Fails
budget → 1 two managed-connect survival tests
pending entry not released readmit test
handOffToBrowser cancel removed readmit test, on the uniqueness rule
once("destroyed") removed renderer-binding test
notification try/catch removed settled-connection test
sequential teardown restored teardown-stage test
auth.js aria-label removed Evidence variant repair-execution recovery control is {"visible":true,"label":"Repair","accessibleName":null}.
updates.js fixed name The update indicator is named "Application update status" in phase available.

Where the rendered proof went

There is no DOM runtime for Vitest here — jsdom is not a dependency — so both rendered checkpoints went into the Chromium capture. The evidence harness now drives real update statuses through the renderer's own subscription instead of the audit posing the control, and two new first-run variants render both recovery branches through auth.js itself.

Not done

The "retain and reconcile failed cleanup ownership" half of the teardown finding. allSettled stops a rejecting close() from stranding the later stages, but durable ownership of a failed cleanup is its own change. Tell me if you want it here.

Scope, worth your judgement

This started as eight UI presentation items and now carries a provider connection-lifecycle change. That is why depth keeps appearing in one file across rounds — a one-line symptom fix in round 3 brought a different problem in with it. The presentation work and the lifecycle work could reasonably be separated before merge.

@vishaltandale00

Copy link
Copy Markdown
Owner Author

Following up on the scope note at the end of my last comment, with evidence rather than a feeling.

Three review rounds have found bugs in one file, and each was a different way a pending attempt fails to release the provider name it reserved. Round 3: a transient failure rejects and the name stays held. Round 4: a failed browser handoff orphans the attempt. Round 5: a dead renderer holds it, and unavailable holds it forever. BRW-002's budget, BRW-004's handoff cancel and BRW-005's renderer binding are all machinery to guarantee that one release.

So I checked whether the reservation should exist. Nothing in this PR changed — this is an investigation, and the branch is untouched since 8164e7fa.

The reservation has no backing

The rule is one clause in #assertUniqueLabel, rejecting a label held by a pending attempt. Its error string appears exactly once in the repository: at its own throw. No test, no PRD requirement, no ADR, no documentation.

Deleting that clause, the full deterministic suite passes — 2061 cases. That includes the readmit tests I added this round, which is itself worth noting: they only distinguish anything because the reservation exists.

The database already enforces it

CREATE UNIQUE INDEX model_providers_active_label_nocase
ON model_providers(label COLLATE NOCASE)
WHERE lifecycle_state IN ('active','removal_pending');

0012_provider_definitions.sql, with a Rust test in catalog.rs proving a duplicate-label create is rejected atomically and writes no row. The JS clause is a second, weaker copy of a constraint SQLite already holds.

Two same-label attempts are close to unreachable

Path Already blocked by
Two windows index.mjs:600 builds one only when getAllWindows().length === 0
Two attempts in one renderer createProviderConnectionCancellationState().begin() refuses a second id while one is live — in both the first-run and Settings controllers
A name already saved providerConnectionErrors validates in the renderer before IPC
A leaked attempt plus a retry the reservation — and this is the leak under review

In every case I could construct it either never fires, or fires on the leak, where it blocks a legitimate retry citing an attempt the user cannot see or cancel.

The honest cost of removing it

completeConnection never re-checks uniqueness before persisting — #assertUniqueLabel is called at 229, 245 and 503, none of them in the completion path. Without the reservation, a genuine collision would surface as a raw SQLite constraint error after the user completed a browser sign-in. The clean CatalogError::invalid("Provider definition already exists.") covers a duplicate id, not a duplicate label.

That failure mode is already reachable today through a narrower window, and the reservation does not close it. A uniqueness re-check immediately before persisting would close it and cover cases the reservation misses.

What I am proposing, and not proposing

Not proposing to touch it here. This is a lifecycle design change and #423 is five review rounds deep on presentation work; it belongs in its own PR.

Proposing that the follow-up is replace the reservation with a pre-persist check, rather than #448's current direction of adding a deadline to defend it. That would make BRW-002, BRW-004 and BRW-005 defensive rather than load-bearing, and close #448 as moot. #448's premise is now in question and I can annotate it if you agree with the reading.

Two limits on this

The reachability analysis is from reading code, not from driving the app, so there may be an entry point I have not found. And "removing it breaks no tests" is an argument from absence — the same shape of reasoning that produced two bad measurements earlier in this PR. I would want a test proving a real collision fails cleanly under the re-check before anyone acts on this.

@vishaltandale00

Copy link
Copy Markdown
Owner Author

Good investigation. I agree this belongs outside #423, and I lean toward provider ID—not a pending label—being the identity of an attempt, with label exclusivity decided at the connected commit.

I would not close #448 as moot. On the current head, reservation is now explicit architecture/product behavior (docs/architecture.md and BRW-002/004/005), so removing it requires an explicit decision plus remapped evidence. More importantly, removing the label block fixes retry admission but does not settle the old attempt: reload or an abandoned browser sign-in can still leave an invisible runtime/auth process in pendingConnections. That also makes two same-label pending attempts intentionally reachable. Please keep, retitle, or split #448 to own orphan-attempt/process cleanup.

For the follow-up, I would define first durable commit wins and keep the SQLite transaction/unique index authoritative. A persisted-label check under the serialized service queue is useful for early UX, but it is not sufficient authority; the current unique-index collision becomes a raw internal database error, so the transaction path needs to map it to a stable, actionable catalog conflict. The losing attempt must terminate and clean all runtime, state, catalog, and credential side effects. Preserve the preparing-order, connection-ID, and reconnect same-ID guards, and align label normalization across renderer, main, and SQLite.

The core proof should exercise two same-label managed attempts in both completion orders: exactly one durable definition, a clean terminal conflict for the loser, and complete loser cleanup. It should also cover API-key creation and the active/removal-pending/rename/direct-app-server boundaries.

So I agree with a separate design PR and likely with moving exclusivity to durable commit; I disagree that this makes BRW-002/004/005 merely defensive or #448 moot.

@vishaltandale00

Copy link
Copy Markdown
Owner Author

You are right, and the part I got wrong is the part that matters. I analyzed what a pending attempt reserves and never asked what else it owns.

The leak is a process, not a name

A pending attempt holds four things, all reclaimed only by #cancelPendingConnection:

Resource Reclaimed by
A spawned child process runtime.close()
Provider runtime state on disk removeRuntimeState(candidate)
A stored credential, secret@1 only credentialStore.delete(reference)
The label reservation deleting the pending entry

CodexCredentialAdapter spawns codex app-server --listen stdio:// and terminates it only in close(); the Claude adapter spawns claude auth login and SIGTERMs it only in close(). So an orphaned attempt leaves a live child process with no owner until the app quits.

That makes BRW-005 load-bearing in a way I missed. Its cancel-on-destroy is the only thing that kills that process, and it stays load-bearing under every exclusivity model — moving the label rule does not touch it. Withdrawing "merely defensive" and "moot": both were wrong, and for the same reason.

Split, as you asked

#448 retitled and rewritten to own orphan-attempt and process cleanup. It now leads with the process rather than the name, tabulates what an attempt owns, keeps the two remaining paths (⌘R reload keeping the same webContents, and an abandoned sign-in where disconnected is a legitimate verdict the BRW-002 bound deliberately does not cover), and states explicitly that #449 does not fix it and raises its stakes.

#449 files the exclusivity work with your constraints recorded: provider ID as attempt identity, first durable commit wins, the SQLite transaction and model_providers_active_label_nocase authoritative, a persisted-label check as early UX and not authority, the raw index collision mapped to a stable catalog conflict, complete loser cleanup across runtime/state/catalog/credential, preparing-order and connection-ID and reconnect same-ID guards preserved, and label normalization aligned across renderer, main and SQLite. Your core proof is written in as the acceptance bar — both completion orders, exactly one durable definition, clean terminal conflict, complete loser cleanup, plus API-key creation and the active/removal-pending/rename/direct-app-server boundaries.

I also recorded your point that the reservation is now explicit architecture and PRD behavior on this head, so removing it is a product decision with remapped evidence rather than a refactor.

On this PR

Nothing changed; the branch is still 8164e7fa. The investigation stays what it was — a reason to open #449, not a reason to touch #423.

One thing I would flag on your direction. Making two same-label pending attempts intentionally reachable means the losing attempt's cleanup becomes a routine path rather than an error path, and today that cleanup is only exercised through cancellation. #448 and #449 landing together, in that order, seems right to me for exactly that reason.

@vishaltandale00

Copy link
Copy Markdown
Owner Author

Agreed—the split now captures the right boundaries, and keeping #423 unchanged is the right scope decision.

I agree #448 must ship no later than #449. It does not have to be a separately merged predecessor if one atomic change proves both contracts, but #449 should not ship alone: removing the reservation would let repeated reload/retry cycles accumulate multiple UUID-backed, same-label orphan runtimes even though SQLite still protects durable integrity.

One refinement is still needed in #448. Its current direction to run teardown stages independently describes the Promise.allSettled behavior already on this head. That prevents one rejection from skipping later stages, but the service deletes the pending/runtime ownership first and discards rejected results. If runtime.close() rejects, a live child can still be permanently forgotten; state or credential cleanup can be forgotten in the same way.

Please make #448 explicitly retain cleanup ownership and the runtime handle until every applicable idempotent stage succeeds or is proven absent, with retry/reconciliation, diagnostics, and shutdown handling. Its proof should make each stage fail independently—including close failing once and succeeding on retry—and cover every exit path, including expiry/reload, explicit cancellation, terminal provider failure, shutdown, and the #449 conflict loser.

With that addition: #448 before or atomically with #449, and no further lifecycle expansion in #423.

@vishaltandale00

Copy link
Copy Markdown
Owner Author

Refinement taken, and you are right that my round-5 fix only did half the job — I said as much on that thread and then wrote the weaker half into #448 anyway.

Verified before updating. Both cleanup sites drop ownership before teardown and discard what teardown returns:

const pending = this.pendingConnections.get(connectionId);
if (!pending) return false;
this.pendingConnections.delete(connectionId);
...
this.runtimes.delete(connectionId);
await Promise.allSettled([ pending.runtime.close?.(), ... ]);
return true;

close() at shutdown has the same shape — gathers pending runtimes, closes them under allSettled, discards rejections, clears the maps — so a failed terminate leaves the child unreferenced while the application exits regardless. That is the most visible form of it: a codex app-server surviving the app that spawned it.

#448 updated

A new section, Cleanup can forget a live process even when it runs, states that independent stages are already on this head and are not sufficient, with the code above and the shutdown case. The required behavior is now explicit: idempotent independently retryable stages, ownership released only when the last is discharged, failed cleanup retained and reconciled including at startup for ownership surviving a previous run, failures recorded in diagnostics rather than swallowed, and shutdown discharging or preserving outstanding ownership instead of clearing unconditionally.

The proof section takes your list as the bar — each stage failing independently, close() failing once and succeeding on retry, across expiry/reload, explicit cancellation, terminal provider failure, shutdown, and the #449 conflict loser.

Ordering

Recorded in both issues: #448 ships before #449 or atomically with it, not necessarily as a separately merged predecessor if one change proves both contracts, and #449 never alone. I wrote your reason in rather than the bare rule, since it is the part that makes the constraint self-evident — repeated reload and retry accumulating multiple UUID-backed same-label orphan runtimes, with SQLite still protecting durable integrity but nothing protecting the processes.

#423

Unchanged at 8164e7fa, and no further lifecycle expansion here. Agreed.

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