141 Commits

Author SHA1 Message Date
Nan Gao
3067b5a5bd
fix(ui): tidy the extension detail layout and narrow-screen overflow (#7936)
* 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.
2026-11-06 14:00:04 +08:00
Xinmin Zeng
728446fe0d
fix(frontend): keep the live run timer on the run's server start (#7888)
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>
2026-11-05 06:51:55 +08:00
Wenchao An
2145c9ab1f
feat(models): discover provider catalogs and search chat models (#7871)
* feat(models): discover provider catalogs and search chat models

* docs(models): scope catalog guidance to owning directories

* test(guidance): register model catalog scoped instructions

* test(memory): pin capacity scenarios to a stable clock

* fix(models): guard catalog imports and align picker interactions

* test(memory): share the fixed clock with updater capacity tests

* fix(models): clear provider filters removed by catalog refresh

* refactor(models): share connection fields across model forms

* fix(models): keep active provider filters clearable

---------

Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
2026-11-05 06:43:01 +08:00
easT-T
3dd9e8841f
fix(frontend): preserve live subtask steps during history backfill (#7864)
* fix(frontend): preserve live subtask steps during history backfill

* test(deermem): keep updater eviction fixtures fresh

---------

Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
2026-11-04 20:16:09 +08:00
Hyeonsang Cho
7fd0c0cb3e
fix(harness): keep a cancelled batch cancelled when retrying a failed item (#7792)
* 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
2026-11-02 10:05:05 +08:00
Totoro
44e372dd7f
feat(runtime): support Queue and Steer during active Web runs (#7729)
* 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>
2026-10-31 11:35:36 +08:00
Lucien
178a9ab458
fix(web): preserve tool-result identity when provider call IDs repeat (#7672)
* fix(web): preserve independent results when tool call IDs repeat

* fix(web): preserve leading results and idempotent result replay
2026-10-31 08:41:07 +08:00
N
4fd2b211cc
fix(artifacts): keep saved revisions ahead of stale reads (#7650)
* fix(artifacts): keep saved revisions ahead of stale reads

* fix(artifacts): abort cancelled content requests

Signed-off-by: Willem Jiang <willem.jiang@gmail.com>

---------

Signed-off-by: Willem Jiang <willem.jiang@gmail.com>
Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
2026-10-29 23:23:08 +08:00
一片一片
f0ba280365
fix(artifacts): parse write-file URLs through the guarded helper (#7609)
`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.
2026-10-28 07:35:01 +08:00
Totoro
ab8cd4e10f
fix(channels): keep IM binding codes on screen and report goal state in replies (#7591)
* 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>
2026-10-27 09:46:58 +08:00
YongxinFu
8337a80ae6
fix(frontend): count code points when previewing table cells and memory facts (#7503)
* 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)
2026-10-26 08:23:49 +08:00
Luck
ce0fa4c114
feat(batch): add browser review for persisted results (#7006)
* feat(batch): add browser review for persisted results

* fix(batch): address browser review feedback

* test(batch): cover persisted result review regressions

Exercise running-to-terminal detail refresh, scoped citation dialogs, omitted evidence, translated page labels, and legacy evidence bounds. Fix component lint and formatting and synchronize the review contract documentation.

* docs(frontend): keep batch review guidance within budget

* fix(batch): refresh retried reviews and account for omitted evidence

* fix(ci): keep batch review guidance within size budget

---------

Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
2026-10-25 23:27:35 +08:00
ShaoLOWu
21fb080530
fix(frontend): allow non-ASCII local parts in auth password forms (#7488) (#7498)
* 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>
2026-10-25 22:09:58 +08:00
Agustin M.
66f00e7b86
fix(frontend): make docs search work on desktop and mobile (#6715)
* 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>
2026-10-25 21:31:11 +08:00
Totoro
a1b89d0432
fix(frontend): reflect tool outcomes in chat steps (#7465)
Co-authored-by: Totoro-qaq <279883115+Totoro-qaq@users.noreply.github.com>
2026-10-25 16:25:29 +08:00
dependabot[bot]
916e647a75
chore(deps-dev): bump happy-dom from 20.11.1 to 21.0.4 in /frontend (#7391)
* chore(deps-dev): bump happy-dom from 20.11.1 to 21.0.4 in /frontend

Bumps [happy-dom](https://github.com/capricorn86/happy-dom) from 20.11.1 to 21.0.4.
- [Release notes](https://github.com/capricorn86/happy-dom/releases)
- [Commits](https://github.com/capricorn86/happy-dom/compare/v20.11.1...v21.0.4)

---
updated-dependencies:
- dependency-name: happy-dom
  dependency-version: 21.0.4
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>

* test(frontend): isolate FlipDisplay animations in DOM suites

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
2026-10-24 10:45:01 +08:00
Tsai Yuan
0530d0c7c6
fix(frontend): resolve relative message links to artifacts (#7356)
* 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>
2026-10-23 22:50:52 +08:00
EastonJiang
0d3a562c5f
fix(frontend): bound Live Browser WebSocket handshakes (#7301)
* fix(frontend): bound Live Browser WebSocket handshakes

* test: register browser-view scoped guidance

* fix(frontend): explain Browser Live connection failures

---------

Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
2026-10-23 15:39:05 +08:00
ShallAbby
c11ba84708
fix(frontend): keep collapsed sidebar navigation visible on mobile (#7198)
* 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>
2026-10-22 15:13:01 +08:00
Lucien
ff07f87cc3
fix(browser): preserve Live keyboard modifiers (#7213)
* fix(browser): preserve Live keyboard modifiers

* test(browser): cover Meta modifier on named keys

---------

Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
2026-10-21 20:57:56 +08:00
Totoro
059758f321
fix(runtime): stop Goal continuations after loop hard stops (#7167)
* fix(runtime): stop Goal continuations after loop hard stops

* chore: remove redundant SVG sources from Goal fix

* test(goal): remove PR-only screenshot generation

---------

Co-authored-by: Totoro-qaq <279883115+Totoro-qaq@users.noreply.github.com>
2026-10-20 16:55:13 +08:00
Lucien
1ae50826b1
fix(frontend): isolate workspace query caches by authenticated owner (#7146)
* fix(frontend): isolate query caches by authenticated owner

* fix(frontend): share auth owner across isolation boundaries

* docs(frontend): keep agent guidance within size budget

---------

Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
2026-10-20 10:26:48 +08:00
EastonJiang
b8bb728368
fix(frontend): stop chat sends after unsuccessful upload responses (#7122)
Co-authored-by: Jaxon1216 <224378083+Jaxon1216@users.noreply.github.com>
2026-10-19 23:10:33 +08:00
WuWenbo
294de7cefd
fix(models): preserve keys after reverted connection edits (#6813) 2026-10-19 16:29:46 +08:00
LingYi
49adb0edec
fix(frontend): ignore message edit shortcuts during IME composition (#6768)
Co-authored-by: LingYi-Liang <270316896+LingYi-Liang@users.noreply.github.com>
Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
2026-10-19 07:11:53 +08:00
Lucien
34316550df
fix(agents): preserve reasoning defaults when the model catalog is unavailable (#6907)
* fix(frontend): preserve untouched custom-agent reasoning defaults

* docs: keep frontend agent guidance within size budget

* test(agents): cover reasoning edits across catalog transitions

* fix(agents): expose stale defaults and validate model switches

---------

Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
2026-10-16 17:22:21 +08:00
王一帆
6bc1020d34
feat(knowledge): decouple scope and read-only catalogs (phase 2) (#6904)
* feat(knowledge): decouple scope and read-only catalogs

* docs(knowledge): keep frontend guidance within repository budget

* fix(knowledge): align execution identity and harden provider boundaries

* docs: keep knowledge guidance within CI size budgets

---------

Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
2026-10-15 15:10:39 +08:00
Totoro
c994826d06
feat(channels): generalize group notifications and add opt-in task/goal IM notices (#6903)
* 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>
2026-10-15 10:30:55 +08:00
Lucien
8008a6195f
fix(artifacts): authenticate isolated HTML preview resources (#6701)
* fix(artifacts): authenticate isolated HTML preview resources

* docs(artifacts): retain precise path guidance within size budget

* fix: preserve HTML preview auth and rendering semantics

* fix(artifacts): quote rewritten CSS URLs

* docs: keep merged artifact guidance within size limit

---------

Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
2026-10-15 10:15:44 +08:00
LingYi
96f930f5be
fix(frontend): align one-time DST conversion with the scheduler (#6756)
Co-authored-by: LingYi-Liang <270316896+LingYi-Liang@users.noreply.github.com>
2026-10-14 22:17:51 +08:00
Xuehao Xu
201b9840cd
feat(knowledge): unify read-only retrieval and source contracts (phase 1) (#6716)
* feat(knowledge): unify read-only retrieval and source contracts

* fix(knowledge): address citation and contract review feedback

* fix(knowledge): resolve citations in delegated task reports

* docs(frontend): keep agent guidance within CI size budget

* docs(backend): preserve merged guidance chain headroom
2026-10-14 14:16:08 +08:00
Totoro
cb90f5dca4
feat(channels): control IM notifications and add WeChat/QQ delivery (#6850)
* 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>
2026-10-14 11:14:48 +08:00
haotianqi666
4210854a9f
fix(frontend): resolve local images in Markdown artifact previews (#6827)
* fix(frontend): resolve local images in Markdown artifact previews

* fix(frontend): key artifact images by resolver context

---------

Co-authored-by: haotianqi666 <250384991+haotianqi666@users.noreply.github.com>
Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
2026-10-14 08:59:53 +08:00
Lucien
c5fd453c37
fix(artifacts): preserve account-scoped drafts across navigation (#6700)
* 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
2026-10-13 15:44:20 +08:00
Eilen Shin
e98179a9d3
feat(models): register managed models with provider presets (#6757)
* feat(models): register managed models with provider presets

* docs: add PR 6757 UI comparison screenshots

* test: fix model management CI checks

* docs: remove PR 6757 screenshots

---------

Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
2026-10-13 08:14:23 +08:00
Totoro
5ae95e6a34
feat: add Feishu group notifications and fix mention caret placement (#6762)
* feat(feishu): deliver scheduled-task notifications to bound users

* feat: add Feishu group notifications and fix mention caret placement

* fix(frontend): preserve group targets while capabilities load

---------

Co-authored-by: Totoro-qaq <279883115+Totoro-qaq@users.noreply.github.com>
2026-10-13 07:17:56 +08:00
haotianqi666
338f8819a4
fix(frontend): preserve schedule when switching to custom cron (#6749)
Co-authored-by: haotianqi666 <250384991+haotianqi666@users.noreply.github.com>
2026-10-12 20:04:48 +08:00
PeaceMaker-best
65fa321ec1
feat(deer-flow): configure agent tool access (#6630)
* feat(deer-flow): configure agent tool access

Signed-off-by: PeaceMaker-best <221849497+PeaceMaker-best@users.noreply.github.com>

* fix(frontend): reconcile knowledge defaults with agent tool access

* docs: reduce frontend agent guidance below size limit

* fix: address agent tool-group catalog review findings

---------

Signed-off-by: PeaceMaker-best <221849497+PeaceMaker-best@users.noreply.github.com>
Co-authored-by: PeaceMaker-best <221849497+PeaceMaker-best@users.noreply.github.com>
Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
2026-10-12 19:00:54 +08:00
Hyeonsang Cho
eb7fc28010
fix(frontend): keep the thread list offset past hidden side chats (#6702)
* 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>
2026-10-12 17:35:27 +08:00
hataa
c89fed0be4
fix(frontend): static-website gate and deep-link fallback for user management (#6641 follow-ups) (#6691)
* 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.
2026-10-12 09:37:15 +08:00
Hyeonsang Cho
91a88e08e6
fix(frontend): show a date-only run_at at midnight in the task's zone (#6692)
* 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>
2026-10-11 20:34:07 +08:00
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
hataa
2f4f4d4b6c
feat(authz): admin user-management UI and disabled-account copy (#4063 gap 3) (#6641)
* 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).
2026-10-11 12:54:51 +08:00
Yeager0204
782fed9135
feat(skills): show YAML load errors for owned custom skills (#6623)
* feat(skills): show YAML load errors for owned custom skills

* docs: capture skill diagnostics with real gateway validation

* fix(skills): clarify YAML diagnostic scope

* docs: remove PR evidence directory

---------

Co-authored-by: kwsYeager <331533331+kwsYegar@users.noreply.github.com>
Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
2026-10-11 12:52:48 +08:00
yang rui
a333ac3189
feat(search): add Webz news search provider and setup integration (#5880)
* feat(search): add Webz news search provider and setup integration

* fix(webz): honor caller result limits and clarify page counts

* fix(frontend): render Webz search source links

* fix(frontend): skip malformed search source entries
2026-10-10 18:49:59 +08:00
stanGSA
f71999d99c
fix(frontend): export complete persisted conversation history (#6530)
* fix(frontend): export complete persisted conversation history

* fix(frontend): preserve canonical export order across runs

---------

Co-authored-by: stanGSA <225717802+stanGSA@users.noreply.github.com>
2026-10-10 16:02:05 +08:00
Totoro
0fe6e63a68
feat(goal): show why a chat goal stopped and when it was met, and redact the goal evaluator and continuation input (#6556)
* feat(goal): record the met goal and keep goal state on the history head

- Add a goal_outcome LastValue channel (GoalOutcomeState). Only the
  guarded clear of a satisfied ordinary goal writes it, in the same
  checkpoint as the clear; the reply anchor is the latest visible AI
  message with text, the reply the visible signature keys on. A goal
  stored without created_at still clears and gets a record.
- write_thread_goal removes the record on every other goal write and
  lists it in channel_versions, new_versions and metadata writes when
  it changes. Threads that never had a record keep their checkpoint
  shape. The sync-only SqliteSaver and CachedHistorySaver, which take
  the #6448 sync fallback, store and drop it the same way.
- goal_outcome is server-owned: POST /state and non-internal run input
  that carry it get 400. A POST /state goal write, or a goal in
  external run input, clears it.
- A branch head written onto its replay base resets the channels the
  head lacks, so a goal or record that write_thread_goal removed does
  not come back. Branching from the met turn keeps the record without
  the met goal.
- The POST /history head now carries an active goal and an achieved
  goal_outcome, also before any message exists, so the post-run
  history refetch keeps them. Older entries stay title/thread_data.
- contracts/thread_goal_contract.json pins the chat stand-down and
  check-failure codes, the record shape and status, the head keys and
  the host-written reasons.

* feat(frontend): show why a goal stopped and when it was met

- The goal bar derives its state from the active goal, the goal_outcome
  record and the messages: set, in progress, continuing, stopped (limit,
  auto-continue off, no progress, token budget, missing evidence, failed
  run, unknown code), waiting for you, waiting, couldn't check the goal,
  paused and goal met. An 8/8 stand-down now reads "Stopped" with the
  next step instead of "Continuing 8/8", and a count-0 stand-down no
  longer looks like a fresh goal.
- "Goal met" shows while its anchor reply is the latest visible message
  (a visible human message, or a visible AI message with text, moves the
  chat on), so a reload agrees with the live view. An active goal wins
  over the record, and this tab's /goal set or clear hides it.
- useActiveGoal drops the optimistic goal when the record of that same
  goal arrives, which fixes the bar that stayed after a goal met on the
  first run (the clear frame has no goal key), also for a goal stored
  without created_at. Server goal values pass through isGoalState; a
  /goal status read uses the backend's own active check, so the bar, the
  toast and the edit lock agree with the server.
- Row 2 gives the reason and what to do next; Details holds the full
  goal, the checker's own note (never host-written text, a check failure
  or a paused goal's older check), the assumption note and an unknown
  code, and starts collapsed in each new state. The bar is a labelled
  section with a live region that announces state changes only (another
  goal's same state counts) and re-reads its text after a language switch.
- The edit pencil on the latest turn is shown disabled with "Editing is
  off while a goal is set" instead of hidden; the message toolbar shows
  on focus-within, so keyboard users can reach it.
- A /goal set or clear rejected with 409 keeps the draft and says a run
  is still going; other failures keep the server's message and the draft.
  A /goal request replaced by a newer one leaves the composer alone, so
  a double submit no longer empties it before the second one fails.
- Copy lives in inputBox.goalBar (en-US, zh-CN); a contract test reads
  contracts/thread_goal_contract.json, and the raw-identifier guard now
  catches stand-down codes and goal field names.

* test(frontend): cover the goal bar on both chat pages

- tests/e2e/goal-status.spec.ts runs every case on the chat page and the
  agent chat page, since each wires the goal bar and the edit lock itself:
  a goal stopped at 8/8, one waiting for details and one waiting on an
  open question card show their reason and next step, also after a
  reload; the locked edit pencil gets keyboard focus, shows its toolbar
  and tooltip, does not open the editor, and hides while a file uploads;
  Goal met after one automatic follow-up (In progress, Continuing 1/8,
  then "Auto-continued once") survives a reload and hides after the next
  visible message; a goal met on the first run of a new chat replaces the
  set bar and frees the pencil, and a new /goal replaces Goal met while
  its own run goes on; a /goal set or clear refused with 409 keeps the
  draft for the retry and shows its own busy toast.
- The mock POST /history head carries only the contract's
  history_head_keys, with goal only while active and goal_outcome only
  when achieved; thread state and run values omit an unset goal instead
  of sending goal: null. PUT and DELETE /goal drop the met record,
  setGoalWritesBusy returns the Gateway's 409, and a repeat create of a
  requested thread id returns the thread as is, like the Gateway.
- The raw-identifier guard also catches checker blockers and the met
  record's field names.
- frontend/src/AGENTS.md: the GoalStatus sentence now names goal,
  goal_outcome and useActiveGoal, reworded in place.

* test(frontend): keep the draft through a failed upload and a busy /goal

#6412 fires onSent only once a send is dispatched, so a failed upload
keeps the text, files and stored draft. A /goal the Gateway refuses with
409 never reaches sendMessage, and the composer rejects it so PromptInput
keeps the text. Drive both through the real sendMessage in one composer:
a failed upload keeps "summarize it" stored, a busy /goal keeps its text
and stored draft with no run, and the retry saves the goal and starts
one run with the objective, not the "/goal" text. After that run the
composer is empty and no stored draft comes back.

* fix(frontend): drop the stop condition's closing period before the pause clause

A stored stop condition often ends with a period, so the schedule card
read "…remain)., pauses itself" (and "…。,满足后自动暂停" in Chinese).
Trim the condition's closing sentence punctuation before the clause that
follows it. The event line, where the condition ends the sentence, is
unchanged.

* fix(frontend): point "evidence missing" at the agent, not a user reply

"Evidence missing" asked the user to reply with what's left, which is
the push a goal is meant to save. It now asks the agent to show the
result or explain what's missing, and offers /goal clear only for a goal that
no longer applies: clearing drops the met record, so work that is right
would never show "Goal met".

zh-CN follows. Copy only: no key, state or mapping changes, and the
scheduled page's reason labels are untouched.

tests/unit/components/workspace/goal-status-next-steps.test.ts checks
the row in en-US and zh-CN, and that no stop row offers /goal clear
without that condition. goal-status.dom.test.tsx and
goal-stream.dom.test.tsx now expect the new copy.

* fix(frontend): say "Couldn't check the goal" once in the goal bar

After a goal check that could not run, the bar's second line borrowed
the scheduled page's whole reason, so it repeated the chip: "Couldn't
check the goal · Couldn't check the goal; the run itself may be fine.
It's checked again after your next message." Chinese had the same
repeat.

The line now gives only why the check failed ("The run itself may be
fine", "No final reply was saved" or "The chat changed during the
check"), then when it is checked again. The screen-reader announcement
follows. The chip and the scheduled page's reason labels are unchanged.

tests/unit/components/workspace/goal-status-unchecked.test.ts checks
every check-failure code in en-US and zh-CN: the label appears once,
and the line still says why the check failed.

* feat(goal): redact the evaluator input when pii_redaction is enabled

The goal evaluator calls its model directly, outside
PiiRedactionMiddleware, so it sent raw user lines, tool arguments, tool
results and Human Input Card answers even with pii_redaction enabled.

With pii_redaction enabled:
- the messages in the evidence window are redacted whole before the
  evidence caps can cut an identifier in two, as TitleMiddleware does.
  This reuses redact_queued_messages, which already covers content,
  call arguments and card answers. Earlier messages are left alone: the
  evaluator never sees them, and the pass is synchronous work on the
  event loop (0.84 s for an 800-message, 4 MB thread, 0.06 s for its
  window);
- the assembled input, the goal objective included, is redacted again.

A redaction error fails the check with evaluator_failed; raw text is
never sent instead. When redaction is off, the input is byte-identical.
Thread state keeps the raw text. README.md, config.example.yaml and the
middleware docstring list the evaluator with the other redacted inputs.

tests/test_goal_pii.py puts an address where the evidence caps cut a
tool argument, a tool result and a card answer, and checks the
objective, the unchanged input when redaction is off, that only the
evidence window reaches the redactor and gives the same evidence as
redacting the whole thread, that a redaction error stands down with
evaluator_failed before the model is called, and the worker path.

* fix(goal): redact the hidden continuation when pii_redaction is enabled

The hidden /goal continuation repeats the goal objective and the
evaluator's reason and evidence summary. PiiRedactionMiddleware skips
it, since it is a framework message and not a user message, so with
pii_redaction enabled each continuation turn sent them raw to the
agent's model, and the message stayed in the thread for later model
calls. The durable-context block and the evaluator input already redact
the objective.

With pii_redaction enabled, make_goal_continuation_message now redacts
the objective, the reason and the evidence summary with redact_text
before the message is built. The thread keeps the redacted message,
which the UI hides; the goal state keeps the raw objective.

The worker builds the message before it counts the continuation. A
redaction error fails the check the way an evaluator redaction error
does: run_failed with stand_down_reason evaluator_failed, the goal stays
active, nothing is sent and the continuation budget is not spent. Only
the exception type is stored. With redaction off, the message is
byte-identical.

README.md, config.example.yaml and the middleware docstring list the
continuation with the other redacted inputs.

tests/test_goal_continuation_pii.py checks the redacted objective,
reason and evidence summary against the tokens redact_text gives, the
unchanged message with redaction off or no config, the worker path
under the run config, and a redaction error.

* docs(changelog): link the goal outcome entries to #6556

Fill the four ([#PR]) placeholders in CHANGELOG.md and CHANGELOG_zh.md with this PR's number and add the matching link references.

* fix(frontend): keep an active goal stored without timestamps visible

POST /threads/{id}/state and run input can store an active goal without
created_at or updated_at. The backend still evaluates it and rejects an
edit with 409 (is_active_goal checks only status "active"), but
isGoalState required both timestamps, so history and live values
dropped it: both chat pages hid the goal bar and unlocked the last-turn
pencil, and only a /goal status read, which used a looser predicate,
brought them back.

isGoalState now accepts any record with a string objective and status
"active", and normalizeGoalStatusRead uses it, so thread values, live
updates and status reads share one rule. Malformed or inactive values
are still dropped.

Tests cover a sparse active goal from the history head and from a live
update before the post-run refetch (bar shown, hasGoal true), and a
malformed goal that stays hidden.

* test(goal): check the outcome write metadata the same way on every saver

PostgresSaver stores checkpoint metadata through
get_serializable_checkpoint_metadata, which drops "writes", so the
[postgres] case of test_goal_outcome_round_trips_through_the_sync_fallback
failed with KeyError: 'writes' when CI ran it against a live database.

The test now records the metadata write_thread_goal passes to put and
asserts the clear's and the next goal's "writes" there, for every saver.
The stored channel values are checked as before, and on sqlite and
cached-sqlite the stored "writes" must still equal what was passed.

* docs(agents): keep goal_outcome out of the agents guidance chain

The middlewares AGENTS.md chain is already over the 98,304-byte AG002
hard limit on main, and the PR-mode check fails any chain a PR touches,
so adding goal_outcome to the ThreadState field list in
deerflow/agents/AGENTS.md failed the agent-guidance job. goal_outcome
stays documented in runtime/AGENTS.md and backend/app/gateway/AGENTS.md,
which are outside that chain.

* refactor(goal): read the evaluator's evidence and redaction from one window

format_visible_conversation recomputed the window start that the redaction
pass takes from _evidence_window. Both now use _evidence_window, so a later
change to the cap or to what counts as visible moves both.

The window test now fills the window with tool-only replies and tool results.
Before, it caught a cap change in one copy but not a change to what counts
as visible.

---------

Co-authored-by: Totoro-qaq <279883115+Totoro-qaq@users.noreply.github.com>
Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
2026-10-10 00:20:45 +08:00
yu1128-L
5ecc1c2ac2
docs(events): document durable thread seq watermark (#6532)
* docs(events): document durable thread seq watermark

RUN_EVENT_STREAM.md described seq as monotonic but did not cover the durable per-thread high-water mark that keeps allocations monotonic after a run is deleted. Record that behavior in the Record Envelope section, including that thread deletion drops the mark only when no events remain.

* test(scheduler): pin clock for scheduled-run timezone assertions

---------

Co-authored-by: yu1128-L <296164533+yu1128-L@users.noreply.github.com>
Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
2026-10-09 08:10:41 +08:00
Hyeonsang Cho
f0476677de
fix(frontend): keep composer context when an attachment upload fails (#6412)
* fix(frontend): keep composer context when an attachment upload fails

sendMessage called onSent right after the in-flight guard, before any
attachment was prepared or uploaded. The composer's onSent drops its
one-time state there: quotes, conversation references, staged project
files and the stored draft. PromptInput keeps the text and files when the
upload rejects, so the user saw the same message and retried it, but the
retry went out without that context.

onSent now fires once the send is dispatched: after the attachments
upload, right before the run is submitted. A failed preparation or upload
rejects first, so every caller (the chat composer and the side chat)
keeps its state for a retry.

Because onSent can now arrive after the composer has moved to another
conversation, InputBox checks which conversation it shows. If it changed,
onSent leaves that conversation's live state alone and retires only the
sending conversation's stored draft and staged files, through a new
clearProjectAttachments helper that does not touch mounted state.

The chat E2E that held an attachment upload asserted the draft was gone
mid-upload, which encoded the old timing. It now asserts the draft stays
stored while the upload is pending and is gone, without being rewritten,
once the send is dispatched.

* docs(changelog): reference #6412

* docs(frontend): note onSent's guarantee ends at dispatch

* fix(frontend): retire only what a detached send carried

A send whose upload finishes after its composer unmounted still took the
live path, because only a conversation switch was detected, and could
delete a draft a new composer saved under the same reusable key. A late
send also cleared the thread's whole staged list, dropping a document
attached during the upload.

Track the composer's lifetime instead of its key, and in the detached
path clear the stored draft only while it still holds the sent text and
retire only the staged entries the send captured.

---------

Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
2026-10-07 17:55:15 +08:00
Hyeonsang Cho
4217b30e3b
fix(frontend): keep the side-chat draft when a send fails (#6407)
* fix(frontend): keep the side-chat draft when a send fails

PromptInput clears the composer only when onSubmit resolves and keeps it
on rejection. The side chat's handleSubmit caught every failure, showed
the toast and resolved, so a failed side-chat creation or attachment
upload lost the typed text and attachments. The first message to a new
side chat was also cleared as soon as it was queued, before the queued
effect sent it, so a failure there could not be retried either.

handleSubmit now rethrows after its toast, and the queued first send
carries the submit's resolve/reject so the composer settles with that
send's outcome. The queued effect no longer toasts on its own, so a
failed first send shows one error instead of two.

* docs(changelog): reference #6407

---------

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