3 Commits

Author SHA1 Message Date
Hyeonsang Cho
3f71b2e13f
fix(frontend): return to the opened workspace page after signing in (#6681)
* fix(frontend): return to the opened workspace page after signing in

Opening a workspace page without a valid session sent the user to a bare
/login: the workspace layout called redirect("/login"), and a layout
cannot read the request URL, so the path and query were both lost. A
scheduled-task notification link (/workspace/scheduled-tasks?task_id=X)
opened after the session expired therefore landed on the default
workspace after sign-in.

For an unauthenticated verdict the layout now renders
WorkspaceLoginRedirect, which replaces the page with
/login?next=<path+query> from the browser. It renders nothing else, so
the workspace pages under it never mount.

AuthProvider.refreshUser and useRequireAuth kept the path but dropped
the query. They now share currentReturnPath() with the fetcher, which
already kept both.

* docs(changelog): reference #6681

* fix(frontend): keep the fragment in the sign-in return path

currentReturnPath() kept the path and query but not location.hash, so
an anchored deep link (/workspace/chats/x#section) lost its anchor across
the login round trip. It is now the single source for all four bounce
sites, so include the fragment. The server never receives the fragment,
so only the browser-side bounce can keep it. validateAuthNextPath, the
backend's validate_next_param and its quote() into /auth/callback all
carry a '#' through unchanged.

The models API test stubbed window.location without hash; a real
Location always has one, so the stub now carries it.

---------

Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
2026-10-11 19:05:03 +08:00
Nan Gao
73e3699347
feat(frontend): render markdown artifacts in the "open in new window" view (#5056)
* feat(frontend): render markdown artifacts in the new window

The artifacts panel's "open in new window" action handed the browser the
raw Gateway response. For markdown that is a `text/markdown` body the
browser can only show as source, so the new window was a text dump rather
than a reader.

Route markdown artifacts to a new `/artifacts/view` page that renders them
with the same components the panel uses (SafeStreamdown + the artifact
rehype chain + citation links/panel), including the truncated-preview
banner and its "load full file" action. Everything else keeps the raw
Gateway URL — notably HTML/SVG, which the Gateway deliberately serves as a
download so active content never executes in the application origin.

- `core/artifacts/viewer.ts` centralizes which stored artifacts are
  markdown (`.skill` archives included, since they hold a SKILL.md), so
  the panel and the viewer route cannot drift.
- `ArtifactFilePreview` and its siblings move out of
  `artifact-file-detail.tsx` into `artifact-file-preview.tsx`; otherwise
  the standalone route would pull the CodeMirror editor into its bundle.
- The window title comes from the route's `generateMetadata`, not
  `document.title`, which the App Router overwrites after hydration.
- The viewer reads content through `useStandaloneArtifactContent`, which
  shares `useArtifactContent`'s query key but not its `useThread`
  dependency, since a detached window has no thread context.

Claude-Session: https://claude.ai/code/session_013AiCrC5SBc3HdFYNxsp1EC

* fix(frontend): keep the artifact target across re-authentication

Review found the standalone viewer unrecoverable from an expired session.
The window's target lives entirely in `?path=...&thread_id=...`, and both
auth paths dropped it:

- The layout guard redirected to `/login` with no `next` at all. A layout
  cannot read `searchParams`, so the guard moves into the page, which can
  — and rebuilds the full viewer address for `next`. The layout loses its
  AuthProvider along the way: nothing under this route reads `useAuth`,
  and the guard now makes a single `getServerSideUser` call per request.
- The shared fetch wrapper built `next` from `window.location.pathname`,
  which silently truncated the query string. It now carries `search` too,
  so any route holding state in the query survives a 401, not just this
  one. `validateAuthNextPath` already accepts a query string.

`buildArtifactViewerURL` is split out of `resolveArtifactOpenURL`: the
guard needs the route itself, never the Gateway fallback that the latter
takes for non-markdown targets.

Tests: the login round trip (unit — the rebuilt URL survives
`validateAuthNextPath` and parses back to the same target), the fetch
wrapper preserving the query on 401 (unit), and the expired-session
window reaching `/login` with the artifact intact (E2E). The E2E asserts
on the popup's navigation *requests*, since `(auth)/layout` answers
`/login` with a server redirect under DEER_FLOW_AUTH_DISABLED and no
navigation commits.

`tests/unit/core/models/api.test.ts` stubbed `window.location` without
`search`; a real Location always has it.

Claude-Session: https://claude.ai/code/session_013AiCrC5SBc3HdFYNxsp1EC

* fix(frontend): keep public showcase artifacts out of the auth gate

Review found that the viewer's access check regressed `/showcase`. Those
pages render with `isMock`, their artifacts are served by the
unauthenticated demo route, and the raw artifact URL this window replaced
stayed public — so gating the window unconditionally bounced every
logged-out showcase visitor to /login for a document that is already
public.

`requiresAuthenticatedViewer` exempts a mock target only when
`resolveStaticDemoArtifact` would actually serve it. The allowlist is the
authority rather than the flag: `mock=true` is caller-supplied, so a
target the demo route answers with 404 — a non-allowlisted path, or a
thread that is not a demo thread — still needs a session.

Covered in `tests/e2e-auth/`, since the default E2E config disables auth
and cannot see this: a public showcase artifact renders without a
session, while a non-allowlisted path and a missing mock flag both land
on /login. Verified the positive case goes red without the exemption.

Claude-Session: https://claude.ai/code/session_013AiCrC5SBc3HdFYNxsp1EC
2026-08-29 11:07:16 +08:00
urzeye
f881996e1a
fix(auth): recover from setup status timeouts (#4371)
* fix(auth): recover from setup status timeouts

* test(auth): cover setup status recovery flows

---------

Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
2026-07-26 10:05:05 +08:00