* fix(frontend): copy share links on the current origin
The sidebar Share action rewrote the origin to the hosted demo
(https://deer-flow-v2.vercel.app) whenever the page ran on localhost or
127.0.0.1, which are the default make dev and Docker entry points. The
copied link never opened the thread, because the thread only exists on
the local deployment, and opening it sent the thread id (and a custom
agent name) to a host the operator does not control.
Always build the link from window.location.origin and pathOfThread().
* docs(changelog): reference #8126 for the share link origin fix
* fix(capabilities): tidy the extension detail layout and narrow-screen overflow
The extension detail view led with a page-level reload button and sat the
back button beside it 3px off: the two flowed inline and aligned by
baseline, which the back button takes from its icon. It also kept a
directory search that filters nothing there, a heading close to the page
title, and status copy that read like body text. On narrow screens the tab
strip widened the whole page, leaving a horizontal scrollbar.
Lead with the back button and move reload to the opposite end, drop the
search from the detail view, and give the detail an icon, package name and
a status notice holding the installation guide. On the directory, reload
now sits beside the hint that explains it. The tab strip scrolls inside
itself and keeps the active tab in view, and reload collapses to an icon
below `sm` with its accessible name intact.
No copy changes.
* fix(capabilities): realign the tab strip on resize without moving the page
Review follow-up. The active tab was only realigned when the tab changed, so
a rotation or window resize could leave it clipped. Realign whenever the strip
resizes, and leave manual swipes alone.
Scroll only the strip. scrollIntoView would also move the page, pulling it back
up when a resize arrives after the user has scrolled past the strip.
Also hide the strip's scrollbar in browsers without scrollbar-width support,
and find the page scroller in the E2E test by computed style rather than by a
class name.
A chat that observes an active run again (remount, chat switch, rejoin)
started its Working… timer from the browser's current time, dropping the
time the run had already spent. useThreadStream now exposes the active
run's created_at from the runs list, and MessageList moves its start back
to it. It never moves the start forward, so a run submitted in this view
keeps its local start.
Fixes#7886
Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
* fix(harness): keep a cancelled batch cancelled when retrying a failed item
retry_item only kept a paused batch as it was, so retrying a failed item
of a cancelled batch reset the batch to queued and the worker claimed and
ran the item. A cancelled batch now rejects the retry (409), re-checked
under the write lock because SQLite ignores FOR UPDATE, and the workspace
no longer offers Retry there.
* docs(changelog): reference #7792 for the cancelled batch retry fix
* feat(runtime): add Queue and Steer for active Web runs
* style(frontend): format Queue and Steer controls
* fix(runtime): retain user Steer controls and order active inputs
* docs: add Chinese changelog entry for Queue and Steer
---------
Co-authored-by: Totoro-qaq <279883115+Totoro-qaq@users.noreply.github.com>
Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
`ArtifactFileDetail` and `loadArtifactContentFromToolCall` re-parsed
`write-file:` URLs with `new URL` + `decodeURIComponent` after
`parseWriteFileArtifact` had already rejected malformed values. The panel
restores its selection from session storage and only type-checks it as a
string, so a stale or hand-edited `write-file:%E0%A4%A` (or
`write-file://[`) threw `URIError`/`TypeError` during render.
Export the guarded parser and read `path` / `tool_call_id` from one parsed
result in both places; a rejected URL degrades to the raw string or
`undefined` instead of taking the panel down.
* fix(channels): keep IM binding codes on screen and report goal state in replies
Connecting Slack, Discord, Feishu, DingTalk, WeCom, Buzz or QQ now opens a
dialog that keeps the /connect command on screen with a copy button, its
expiry time and the binding status, instead of a toast that disappears.
The bots' /connect replies and the IM /goal replies now use the owner's
web UI language (else channel_connections.notification_locale), and /goal
reports the goal's state as the web goal bar does: in progress, why it
stopped, or that it was met.
* docs(changelog): note the IM binding dialog and localized IM replies (#7591)
* test(channels): check IM reply tables against every notification locale
The connect-reply check compared a fixed en-US/zh-CN pair, and the /goal
reply table had none. Both now match NOTIFICATION_LOCALES with the same
keys in each, like the existing _GOAL_REASON_TEXT check, so a new locale
fails here instead of raising KeyError at reply time.
---------
Co-authored-by: Totoro-qaq <279883115+Totoro-qaq@users.noreply.github.com>
* fix(frontend): count code points when previewing table cells and memory facts
artifact-table-preview cut a cell at 120 UTF-16 code units and
memory-settings-page's truncateFactPreview measured and cut fact previews
at 140, so an emoji spanning two units could be split at the cut — the
lone half renders as U+FFFD — and a preview that already fit the
documented code-point budget was still shortened. Both previews now share
truncateToCodePoints in core/utils/text, which measures and cuts by code
points, the same rule briefReason in core/scheduled-tasks/format.ts
already follows.
Add unit tests for the helper and a DOM regression test covering both the
fits-exactly and the cut-inside-an-emoji cases.
* fix(frontend): count code points in the artifact-cell expand gate too
The expandable gate still measured value.length (UTF-16 units), so a
120-code-point cell ending in an astral character opened the expand
control even though the inline preview already showed the whole cell —
the same units-vs-code-points mismatch the preview cut fixed.
The gate now counts Array.from(value), and the boundary test asserts
the fits cell renders as a plain cell with no expand control while the
overflows cell keeps its button and whole-emoji cut.
Suggested-by: gdut4140 (port of the #7509 expandable gate)
* fix(frontend): allow non-ASCII local parts in auth password forms (#7488)
The sign-in, register, admin-setup and change-password forms relied on the
browser's type="email" constraint validation, which rejects any non-ASCII
local part before the submit handler runs. The backend identity schema
deliberately accepts such addresses (see emailSchema in @/core/auth/types),
so an account created via the API, an administrator or OIDC could never sign
in through the password form.
Forms are now noValidate, and the permissive isValidEmail helper (one @,
non-empty local part and domain, no whitespace) runs in the handlers instead,
staying strictly weaker than what the backend accepts.
* fix(auth): normalize emails and restore form password checks
---------
Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
* fix(frontend): make docs search work on desktop and mobile
Docs search on deerflow.tech was broken twice over: no build step ever
generated the Pagefind index that Nextra's <Search> loads, and the custom
docs header that replaces Nextra's navbar never rendered a search box.
- `pnpm build` now runs `search:index` (pagefind 1.5.2, pinned) to write
public/_pagefind/ (gitignored).
- The landing Header and its mobile sheet take an optional `search` slot;
only the docs layout fills it, with localized labels and an aria-label.
- Each docs page carries a Pagefind `lang` filter and queries are scoped to
it, since the root <html lang> is fixed and would mix en/zh results.
- globals.css restyles the results panel with app tokens because Nextra's
<Head> variables are unset, lifts it above the mobile sheet, and keeps it
tappable; the sheet no longer closes before a result navigates and closes
after navigation.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* docs(frontend): add DESIGN.md describing the shipped design system
Records the existing tokens, typography, components, docs (Nextra)
overrides, motion, accessibility and copy rules, plus the landing-only
decorative exceptions, so UI changes have something to be checked
against. Linked from frontend/AGENTS.md.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(frontend): address Codex review of docs search
- Chinese search: the root <html lang> is fixed, so Pagefind indexed zh
pages with the English tokenizer and multi-character words (e.g. 偏好)
never matched. scripts/build-search-index.mjs now sets each page's lang
from its route before indexing, producing separate en-US and zh-CN
indexes, and DocsHtmlLang syncs <html lang> at runtime (resetting
Pagefind on a language switch) so queries hit the right index.
- Mobile menu: reset the open state when the route changes instead of
comparing against the saved path, so Back no longer reopens the sheet.
- Shortcut: stop mounting a second <Search> in Nextra's unused mobile menu,
which captured Cmd/Ctrl+K and focused an offscreen input at 640-767px.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* docs(frontend): keep guidance within size budget
* fix(frontend): address docs search review feedback
Keep Nextra results within the mobile sheet scroll boundary and dismiss same-hash selections by pointer or keyboard. Rebuild Pagefind output without stale fragments and replace UI source-string checks with behavioral coverage.
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
* fix(frontend): resolve relative links in messages to thread artifacts
An assistant message that links a file it wrote with a relative
destination, e.g. [Report](report.md), rendered a plain relative href.
The browser resolved it against the chat route to
/workspace/chats/report.md, so clicking it opened a "thread" whose id is
the file name and the Gateway rejected it (422). Only /mnt/ links were
rewritten, and the clarification and present_files message text did not
get the thread-aware link renderer at all.
- A relative link (no scheme, no leading /, no .. segment) that
suffix-matches exactly one thread artifact now opens that artifact in
a new tab, using the rule message images already use. When zero or
several artifacts match it renders as text instead of a link to a
broken chat URL. Links outside the message list are unchanged.
- The artifact list reaches the link renderer through a context:
Streamdown memoizes its root without comparing `components`, so a
renderer built before the list arrives would keep the stale list.
- /mnt/ artifact links get the normal link styling (they rendered as
unstyled text before).
- A reopened thread rebuilds its values from the POST /history head,
which carries messages but not `artifacts`. The message list and the
artifact panel now add the /mnt/ paths from present_files calls in the
messages, so links resolve and the panel lists the files after reload.
Co-Authored-By: Claude Code <noreply@anthropic.com>
* fix(frontend): address artifact link review
---------
Co-authored-by: huangzz <110116@nd.com>
Co-authored-by: Claude Code <noreply@anthropic.com>
* fix(frontend): keep collapsed sidebar navigation visible on mobile
* fix(frontend): address mobile sidebar review feedback
* fix(ci): keep frontend agent guidance within size budget
* fix(frontend): align mobile sidebar focus and trigger state
* fix(frontend): keep Agents header actions within mobile viewport
---------
Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
* fix(browser): preserve Live keyboard modifiers
* test(browser): cover Meta modifier on named keys
---------
Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
* feat(channels): generalize IM group targets and add task/goal run notifications
- Generalize proactive group notification capability (proactive_group_notifications), binding validation, and scheduled task group selection across Feishu, QQ, and WeCom while keeping WeChat personal-only.
- Add QQ group active notification delivery via POST /v2/groups/{group_openid}/messages (msg_type=2) and WeCom group active notification delivery via WS Client.sendMessage(chatid, body) with bot_id:group:chatid workspace isolation.
- Generalize notification outbox sources via migration 0043_runtime_im_notices (source_kind, source_key, run_id, nullable task_id/task_run_id) with backward-compatible SQLite/PostgreSQL upgrades.
- Add opt-in ordinary task & goal run IM notifications (run_completed, run_failed, human_input_requested) on the shared outbox, delivery worker, renderer, and settings UI.
* fix(notifications): address review feedback and group/runtime notification edge cases
* test(migrations): update CURRENT_HEAD to 0043_runtime_im_notices across migration chain tests
* fix(channels): preserve group binding names and legacy authorization
Carry owner-chosen group names through existing connect state, keep private bindings out of legacy group fallback, and restore completion metadata retries. Cover real binding and worker finalization paths and synchronize notification docs.
---------
Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
* feat(channels): control IM notifications and add WeChat/QQ delivery
* fix(scheduler): skip notification preference locks without recipients
---------
Co-authored-by: Totoro-qaq <279883115+Totoro-qaq@users.noreply.github.com>
Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
* fix(artifacts): preserve account-scoped drafts across navigation
* docs(artifacts): keep draft lifecycle guidance within its budget
* fix(artifacts): retain drafts through unknown auth and isolate typing updates
* fix(artifacts): preserve drafts when visiting public showcases
* fix(artifacts): preserve edits after pending save
* fix(frontend): keep the thread list offset past hidden side chats
Primary thread lists hide sidecar threads on the client, so a page of 50
visible chats can consume more server rows. fetchInfiniteThreadsPage kept
that server offset in a Symbol property on the page array, but TanStack
Query's structural sharing (replaceEqualDeep) rebuilds a changed array
without symbol keys, and the list cache writers (rename, unread, pin,
delete, upsert) rebuild pages with map/filter. After the first refetch or
cache write, getInfiniteThreadsNextPageParam fell back to the sum of the
visible page lengths, so the next page started at 50 instead of 53 and
re-fetched rows already loaded. The same fallback treated a full page
that lost a row to a local delete as the end of the list.
Each cached page is now { threads, hiddenCount, hasMore }. The next offset
is the sum of visible and hidden rows over the loaded pages, so local
deletes and inserts still move it with the server list, and the end of the
list comes from the server response rather than the page length.
useInfiniteThreads projects the pages back to thread arrays with select,
so list components are unchanged.
* test(frontend): pin the sidebar's next-page offset past side chats in E2E
Three side chats in the first 50 server rows push the sidebar's second
page to offset 53. After a rename through the sidebar menu, scrolling to
the sentinel must request offset 53, not 50. On main this test fails with
the request at offset 50.
* docs(changelog): reference #6702
---------
Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
* fix(frontend): static-website gate and deep-link fallback for user management (#6641 follow-ups)
Close the two leftovers from the #6641 review:
- user-settings-page now gates on isStaticWebsiteOnly() like model management
(the static demo user is nominally an admin, but there is no Gateway to
query), and the users adminOnly copy gains the same 'unavailable in demos'
sentence the models copy already carries.
- the settings dialog derives the active section from the nav actually offered
to the current user, so a ?settings=users deep link (or a stale dialog
store) for a non-admin falls back to the first section instead of rendering
an orphan panel with no highlighted entry.
Tests: static demo loads nothing (mutation-verified red), non-admin deep link
falls back to models, admin deep link lands on user management; full unit
suite green.
* test(frontend): pin the static-on x admin matrix cell at the dialog level
Dialog-level coverage for the real demo path (review follow-up on #6691):
admin + isStaticWebsiteOnly() + ?settings=users keeps the Users nav entry
(the splice keys on the role only) while the page-level gate shows the
admin-only note and issues no query. Mutation-verified: adding the mode
flag to the nav splice turns this test red and only this test.
* fix(frontend): show a date-only run_at at midnight in the task's zone
A chat-created one-time task can store run_at as a bare date
("2026-12-07"). The server reads it with datetime.fromisoformat as
midnight in the task's timezone. onceRunAtInstant instead returned it
unchanged: its offset check matched the trailing "-07" as a UTC offset,
and its local-time pattern needed a time anyway. new Date() then read it
as UTC midnight, so the schedule line, chat card and edit form showed
08:00 in Asia/Shanghai and the previous evening west of UTC.
A bare date is now matched first and converted as midnight in the task's
zone, the same way a time-bearing local run_at is.
* docs(changelog): reference #6692
* test(frontend): pin the clock in the date-only run_at E2E
The detail page formats the schedule line against the real clock: it
drops the year inside the current year and switches to relative labels
in the final week. The exact-text assertion therefore failed throughout
2027 ("Once, Dec 7 00:00 ...") and again as "Once, Tue 00:00 ..." in the
week before the run. Fix "now" at 2026-10-01, the same instant the unit
test uses, so the assertion stays deterministic.
---------
Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
* 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>
* feat(authz): admin user-management UI and disabled-account copy (#4063 gap 3)
Frontend slice of the account lifecycle backend (#6566):
- Settings gains an admin-only Users page (core/auth/admin-users.ts ->
GET/PATCH /api/v1/admin/users): email, read-only role, disabled state,
and a suspend/restore toggle per row; the Gateway's rejection detail
(last-active-admin 409) renders verbatim. The nav entry is hidden for
non-admins and the page guards independently; ?settings=users joins the
deep-link whitelist (now an exhaustive Record over SettingsSection).
- A session that turns out disabled maps account_disabled to a login
redirect that states the reason on all three surfaces: the SSR
AuthResult tag (consumed exhaustively by the workspace/artifacts/auth
layouts), the shared fetcher's 401 interception, and
AuthProvider.refreshUser. No next target is preserved - a disabled
account cannot sign back in. Login failures surface the localized
account_disabled copy when the code is present.
- userSchema stays disabled-optional (absent = not disabled); zh/en
locales and docs updated in the same change.
* fix(authz): address review on admin user UI and disabled copy (#4063 gap 3)
Review follow-ups on PR #6641:
- Direct /login visits with a suspended session now surface the SSR
account_disabled verdict through AuthDisabledContext instead of
discarding it (willem-bd): the auth layout wraps pages with the
provider and the login form seeds its reason from it — no redirect,
so no loop. Regression tests cover both verdict states.
- admin-users.ts parses both endpoints against userSchema /
z.array(userSchema) (zhfeng): malformed payloads fail the load
instead of rendering phantom rows; toggle error copy is scoped to
GatewayApiError with a localized fallback for anything else.
- The localized account_disabled copy is login-only (zhfeng): sign-up
keeps the backend message verbatim so a suspended account's email
cannot be probed through the register surface.
- Settings page folds the load banner and toggle error into a single
alert region (zhfeng); adds coded-envelope 409, malformed-row, and
load-failure tests; fetcher.ts documents the 401 body-consumption
contract.
- prettier across touched files (CI format gate).
The admin-users query key keeps user?.id, matching the
model-settings-page precedent (converge later per review).