Repository navigation
feat(webui): device-authorization login, a sign-out that signs out, and account switching (N) - #29
Merged
Merged
Conversation
…nd account switching
Roadmap N (登录与账号): the WebUI had no login at all — it served
whatever OAuth credential 'mcode login' (or the desktop) had already
written, the usage panel said 登录后查看用量 with no way to log in, and
退出登录 did not actually sign out: it ran the host's invalidateAuth,
which clears projections, then the next lease refresh re-authenticated
from the credential that was never removed. Signing in and switching
accounts were both impossible without leaving the browser.
Server — the assembly already builds one MCodeOAuthCore for quota
leases; the account session now wraps it:
* account-login.ts: one session per service answering begin/status/
cancel. begin starts (or re-attaches to — the core dedupes login()
on its own promise) a device-authorization login and resolves with
the prompt as soon as the core emits it; status folds the settled
outcome, the in-flight prompt, and — with no attempt this process
started — the persisted store, so a terminal-client login counts;
cancel aborts without recording the core's cancellation rejection as
a failure.
* signOut now prefers signOutAccount (logout {revoke}) — revoke and
remove the credential — falling back to the historical invalidation
for older hosts. 切换账号 is the same surface from the other side:
sign out, then begin a fresh device flow.
* Operations beginAccountLogin / getAccountLoginStatus /
cancelAccountLogin; oauth-core.d.ts shim grows the login surface.
Client — AccountLoginDialog (portal dialog + pure AccountLoginPanel):
the device code, the authorization link (complete URI when the server
sends one), a 2s poll, cancel, expired retry, and an authenticated
state whose 切换账号 runs the sign-out-then-begin sequence. Entries:
the user menu's 账号与登录 item, and a 登录 button in the usage
panel's signed-out block — whose mousedown no longer steals focus,
because focusin re-triggered the usage anchor's onFocus → loadUsage,
whose loading skeleton replaced the pressed node mid-click and the
first real click silently never dispatched (reproduced with an event
trace; the node that received mousedown was disconnected by mouseup).
The browser fixture answers the three operations and gains
setAccountLoginState / setUsageQuotaResult so the dialog's phases can
be driven deterministically.
Tests: account-login.test.ts (7 — prompt-on-begin, re-attach without a
second login call, settled outcome, cancellation-is-not-failure,
persisted-store status, phase derivation) and
account-login.spec.mjs (4 browser cases — the full device flow with
authorization completing, the switch path issuing sign-out before the
new flow, the usage-panel entry, cancel). The port stubs in
webui-service / host-shape-invariant grow the four members.
antianqi
added a commit
that referenced
this pull request
Oct 7, 2026
…nect
Every dev-mode upgrade and API call was rejected with 403 Forbidden Origin,
and the shell sat on "WebUI connection failed". Sessions and account state came
back empty because nothing had ever connected: the websocket handshake never
completed.
`isAllowedOrigin` requires the origin's port to equal the bound port, which is
correct for the packaged server — ADR 0004 has it serve the built client from
the same listener, so same-origin is the right rule there. The Vite dev server
necessarily listens on a different port (5173 against 8787), so every request
the browser made carried an origin the check rejected.
Measured, before this change:
ws://127.0.0.1:8787/ws Origin http://127.0.0.1:8787 -> OPEN
ws://127.0.0.1:5173/ws Origin http://127.0.0.1:5173 -> ERROR
After: both OPEN.
The port check is skipped only in dev, and only after the checks above it have
already held — the origin must still be http or https and its host must still be
`127.0.0.1` or `localhost`. A page on another machine still cannot reach the
service.
This is a port of another contributor's uncommitted work on this file, which I
had to drop while syncing the base: their patch no longer applies, because
PR #29 added 153 lines to `service.ts` and PR #32 moved the other two files in
the same change set. The reasoning and the shape are theirs, carried over
rather than rewritten; if they would rather land it themselves, this commit
should be dropped in favour of theirs.
Validation
- pnpm typecheck:webui-full exit 0
- run-vitest-suite.mjs webui 10 failed | 1917 passed | 4 skipped
The 10 failures are the unchanged pre-existing Windows baseline. The browser
suite's own harness does not exercise the dev proxy path, so it neither caught
this nor proves the fix; the handshake measurement above is what does.
Not covered here: `vite.config.ts` and `build-webui-styles.mjs` carried related
edits in the same dropped patch. Those address dev-server packaging rather than
the handshake, and neither was needed to restore the connection.
antianqi
added a commit
that referenced
this pull request
Oct 9, 2026
…nect
Every dev-mode upgrade and API call was rejected with 403 Forbidden Origin,
and the shell sat on "WebUI connection failed". Sessions and account state came
back empty because nothing had ever connected: the websocket handshake never
completed.
`isAllowedOrigin` requires the origin's port to equal the bound port, which is
correct for the packaged server — ADR 0004 has it serve the built client from
the same listener, so same-origin is the right rule there. The Vite dev server
necessarily listens on a different port (5173 against 8787), so every request
the browser made carried an origin the check rejected.
Measured, before this change:
ws://127.0.0.1:8787/ws Origin http://127.0.0.1:8787 -> OPEN
ws://127.0.0.1:5173/ws Origin http://127.0.0.1:5173 -> ERROR
After: both OPEN.
The port check is skipped only in dev, and only after the checks above it have
already held — the origin must still be http or https and its host must still be
`127.0.0.1` or `localhost`. A page on another machine still cannot reach the
service.
This is a port of another contributor's uncommitted work on this file, which I
had to drop while syncing the base: their patch no longer applies, because
PR #29 added 153 lines to `service.ts` and PR #32 moved the other two files in
the same change set. The reasoning and the shape are theirs, carried over
rather than rewritten; if they would rather land it themselves, this commit
should be dropped in favour of theirs.
Validation
- pnpm typecheck:webui-full exit 0
- run-vitest-suite.mjs webui 10 failed | 1917 passed | 4 skipped
The 10 failures are the unchanged pre-existing Windows baseline. The browser
suite's own harness does not exercise the dev proxy path, so it neither caught
this nor proves the fix; the handshake measurement above is what does.
Not covered here: `vite.config.ts` and `build-webui-styles.mjs` carried related
edits in the same dropped patch. Those address dev-server packaging rather than
the handshake, and neither was needed to restore the connection.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Change
Roadmap N(登录与账号): the two 🟡 rows — 登录方式 and 登出·账号切换(身份展示 ✅ untouched). Baseline:
webui @ e35940c(verified against the fetchedorigin/webuiHEAD at start of work, matching the number read at 22:33).What was actually wrong (both reproduced/read before any change):
mcode login(or the desktop) had already written into the shared store. The usage panel literally said 「登录后查看用量」 with no way to log in; after a sign-out the only way back was leaving the browser.invalidateAuth, which clears the cli-auth projection and the in-process context — the OAuth credential itself survived, and the next lease refresh (refreshOAuthAuthContext) re-authenticated immediately. Neither row could work while the credential was undeletable from the UI.The fix — one surface, server-first:
account-login.ts+ assembly): the assembly already builds oneMCodeOAuthCorefor quota leases; the account session wraps the same core — one credential store, one watch.beginAccountLoginstarts (or re-attaches to — the core dedupeslogin()on its own promise) a device-authorization login, the same flow the terminal client uses, and resolves with the prompt (user code + verification URL) without waiting for the human.getAccountLoginStatusfolds the settled outcome / in-flight prompt / persisted store (a terminal-client login counts).cancelAccountLoginaborts without recording the cancellation as a failure.signOutAccount→quotaOauthCore.logout({revoke: true})(revoke server-side + remove the credential) followed by the existing invalidation; hosts assembled before this change fall back to the historical behaviour instead of failing.AccountLoginDialog(portal dialog; the pureAccountLoginPanelis separately testable) — device code, authorization link (complete URI when provided), 2s poll, cancel, expired retry. Two entries: the user menu's 「账号与登录」 item, and a 登录 button in the usage panel's signed-out block.onFocus → loadUsage, whose loading skeleton replaced the pressed node mid-click, and Chromium does not dispatch click when the mousedown node is disconnected (event-trace reproduced). The button now prevents mousedown default; the comment inUserMenu.tsxexplains the chain.Validation
pnpm test:webui— 91 files / 1750 tests passed, including newaccount-login.test.ts(7: prompt-on-begin, re-attach without a second login call, settled outcome, cancellation-is-not-failure, persisted-store status, phase derivation).npx playwright test— 93/93 passed, including newaccount-login.spec.mjs(4, harness-importedtest): the full device flow with authorization completing via the poll; 切换账号 issuingsignOutbefore the new device flow; the usage-panel entry (which also covers the first-click fix); cancel closing and cancelling.pnpm typecheck:webui-full,pnpm check:source(inventory regenerated),pnpm verify— passed, 20 gates on darwin.webui-service.test.ts/webui-host-shape-invariant.test.tsgrow the four members (the invariant class exists precisely to make a forgotten member a compile error).NOT RUN / boundaries
MCodeOAuthCorecontract (login/cancelLogin/logout/getStatus, source-read). No live-service acceptance claimed.Publication and contribution checks
release/public-source.json; new tests are declared intest/vitest-suites.jsonwhere applicable.Maintainer handoff
Publication scope or license changes (if any): none.
Shared-source port: not needed.