Summary
src/tui/dialog.tsx launches the Supabase OAuth flow as fire-and-forget work with no in-flight guard, no cancellation, and no timeout around the authorize() / callback() network calls.
This allows:
- overlapping auth attempts in a single session
- background auth work to continue after the dialog is dismissed
- retries that start a second flow while the first is still running
- dialogs that can hang forever if the callback path never resolves
Evidence
Fire-and-forget auth start
src/tui/dialog.tsx
178-186: startOAuth = () => runAuthFlow(...)
195: idle state onConfirm: startOAuth
No in-flight state is tracked outside the dialog UI state itself.
Dismiss only closes UI, not the network/auth flow
src/tui/dialog.tsx
139-145: closeDialog() sets lifecycle.closed = true and calls props.onClose()
147-175: setState() stops UI updates after lifecycle.closed
This prevents stale UI changes, but it does not cancel the underlying runAuthFlow() promise or the host OAuth calls it is awaiting.
Retry can overlap a still-running attempt
src/tui/dialog.tsx
224-234: error dialog onConfirm does await startOAuth() again
If a previous attempt is still running in the background, retry starts another flow.
No timeout around host OAuth calls
src/tui/dialog.tsx
65-68: waits on provider.oauth.authorize(...)
97-100: waits on provider.oauth.callback(...)
Neither call has a timeout or abort path.
Reproduction
Overlap case
- Run
/supabase.
- Confirm the dialog and let it reach "Waiting for authorization...".
- Dismiss the dialog before completing the browser callback.
- Re-open
/supabase and start auth again.
- Complete one or both browser flows.
Expected
At most one Supabase auth flow should be active per dialog/session/store, and retry should either cancel or await the existing attempt.
Actual
Multiple auth flows can overlap. The first one can continue running after the dialog is gone.
Hang case
- Run
/supabase.
- Start auth.
- Make
authorize() or callback() stall indefinitely (e.g. broken server path, unreachable host endpoint, unresolved callback).
Expected
User gets a bounded timeout and a recoverable retry path.
Actual
The dialog can remain stuck waiting indefinitely.
Impact
- Confusing UX with multiple browser tabs or competing flows.
- Last-finishing auth flow wins, even if it is not the one the user intended.
- Hanging auth requests have no bounded recovery path.
Suggested Fix
Recommended approach:
- Track a single in-flight auth promise for the dialog lifecycle.
- Block re-entry while one flow is active, or convert retry into "cancel then restart".
- Add timeout wrappers around
authorize() and callback().
- If possible, thread cancellation/abort through the flow so dismiss actually cancels the work instead of only hiding the UI.
Acceptance Criteria
- User cannot start a second Supabase auth flow while one is already active.
- Dismiss/retry semantics are explicit and deterministic.
authorize() and callback() fail with a clear timeout instead of hanging forever.
- Regression coverage exists for dismiss-then-retry overlap.
Notes
This is mostly a reliability/UX issue, but it can indirectly affect correctness because multiple flows can race to update the same local auth store.
Summary
src/tui/dialog.tsxlaunches the Supabase OAuth flow as fire-and-forget work with no in-flight guard, no cancellation, and no timeout around theauthorize()/callback()network calls.This allows:
Evidence
Fire-and-forget auth start
src/tui/dialog.tsx178-186:startOAuth = () => runAuthFlow(...)195: idle stateonConfirm: startOAuthNo in-flight state is tracked outside the dialog UI state itself.
Dismiss only closes UI, not the network/auth flow
src/tui/dialog.tsx139-145:closeDialog()setslifecycle.closed = trueand callsprops.onClose()147-175:setState()stops UI updates afterlifecycle.closedThis prevents stale UI changes, but it does not cancel the underlying
runAuthFlow()promise or the host OAuth calls it is awaiting.Retry can overlap a still-running attempt
src/tui/dialog.tsx224-234: error dialogonConfirmdoesawait startOAuth()againIf a previous attempt is still running in the background, retry starts another flow.
No timeout around host OAuth calls
src/tui/dialog.tsx65-68: waits onprovider.oauth.authorize(...)97-100: waits onprovider.oauth.callback(...)Neither call has a timeout or abort path.
Reproduction
Overlap case
/supabase./supabaseand start auth again.Expected
At most one Supabase auth flow should be active per dialog/session/store, and retry should either cancel or await the existing attempt.
Actual
Multiple auth flows can overlap. The first one can continue running after the dialog is gone.
Hang case
/supabase.authorize()orcallback()stall indefinitely (e.g. broken server path, unreachable host endpoint, unresolved callback).Expected
User gets a bounded timeout and a recoverable retry path.
Actual
The dialog can remain stuck waiting indefinitely.
Impact
Suggested Fix
Recommended approach:
authorize()andcallback().Acceptance Criteria
authorize()andcallback()fail with a clear timeout instead of hanging forever.Notes
This is mostly a reliability/UX issue, but it can indirectly affect correctness because multiple flows can race to update the same local auth store.