169 Commits

Author SHA1 Message Date
Ryker_Feng
98b8e4657e
feat(chats): add archive and restore (#5236)
* feat(chats): add archive and restore

* test(chats): observe archive search requests in pagination e2e

* docs(gateway): move thread lifecycle details out of inherited guidance

* docs(chats): add concise archive and restore RFC

* docs(chats): move archive RFC discussion to issue 5237
2026-09-06 22:30:26 +08:00
PeaceMaker-best
e3df6ea4a8
feat(channels): select custom agents per conversation (#5168)
* feat(channels): select custom agents per conversation

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

* fix(channels): reserve agent slash command across clients

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

* fix(tui): hide reserved slash commands from skills

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

* fix(channels): preserve selected agent across clients

---------

Signed-off-by: PeaceMaker-best <221849497+PeaceMaker-best@users.noreply.github.com>
Co-authored-by: PeaceMaker-best <221849497+PeaceMaker-best@users.noreply.github.com>
2026-09-06 16:46:51 +08:00
Aari
0f7d8709d3
feat(sandbox): add controlled egress with approvals (#5152)
* feat(sandbox): add controlled egress approvals

* Apply batched suggestions from code review

* fix(sandbox): harden restricted network policy

* fix(sandbox): harden denied egress handling

* fix(sandbox): isolate network proxy sidecar

* chore: retry sandbox image smoke

* fix(sandbox): close remaining network policy gaps

* fix(sandbox): harden relay token rejection

* fix(sandbox): fence incompatible policy replacement

* fix(sandbox): replace containers across network modes

* fix(sandbox): close remaining lifecycle gaps

---------

Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
2026-09-04 23:46:57 +08:00
PeaceMaker-best
6022bdf5ae
perf(frontend): avoid redundant chat state snapshots (#5159)
* perf(frontend): avoid redundant chat state snapshots

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

* fix(streaming): preserve incremental chat semantics

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

---------

Signed-off-by: PeaceMaker-best <221849497+PeaceMaker-best@users.noreply.github.com>
Co-authored-by: PeaceMaker-best <221849497+PeaceMaker-best@users.noreply.github.com>
2026-09-04 08:20:12 +08:00
Wu Shuwen
5860423eb6
docs: align custom agent naming with API (#4944)
* docs: align custom agent naming with API

* docs: document custom agent API gate

* docs: correct custom agent storage guidance

* docs: correct custom agent config path

* docs: clarify custom agent name scope

* docs: align agent storage placeholder

* docs: clarify custom agent file updates

* docs: qualify custom agent storage

* docs: clarify agent database storage

* Fix formatting in docs-links.test.ts

---------

Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
2026-09-02 10:14:45 +08:00
Aari
340bff1107
feat(mcp): manage servers from Settings (#5022)
* feat(mcp): manage servers from settings

* fix(mcp): make settings updates targeted

* fix(mcp): reject ambiguous masked array edits

* fix(mcp): honor targeted server field deletions

* fix(mcp): preserve OAuth extension secrets

* fix(mcp): validate config before persistence

* fix(mcp): preserve environment placeholders

* fix(mcp): harden targeted configuration routes

* docs: keep gateway guidance within budget

* fix(mcp): protect per-tool override secrets

* fix(mcp): keep disabled edits structurally safe
2026-09-01 23:24:49 +08:00
jiaqiang0000
91c7ed4cf5
fix(frontend): keep renamed thread titles in sync (#5045)
* fix(frontend): 同步会话重命名后的标题状态

* chore: 重新触发 PR 自动分流检查

* fix(frontend): address thread title sync review feedback

Signed-off-by: 橘猫 <2622045569@qq.com>

* fix(frontend): fall back when canonical thread title is empty

Signed-off-by: 橘猫 <2622045569@qq.com>

* fix(frontend): fence stale metadata after thread rename

Signed-off-by: 橘猫 <2622045569@qq.com>

* fix(frontend): fence stale thread list responses after rename

Signed-off-by: 橘猫 <2622045569@qq.com>

---------

Signed-off-by: 橘猫 <2622045569@qq.com>
2026-09-01 23:10:54 +08:00
Zheng Feng
ddd9aec558
fix(frontend): default to Webpack over Turbopack in dev to avoid PostCSS worker leak on macOS (#5133)
* fix(frontend): default to Webpack over Turbopack in dev to avoid PostCSS worker leak on macOS

On macOS arm64, Turbopack + Next.js 16.2.11 + Tailwind CSS v4 causes an
unbounded spawn of PostCSS evaluator processes that consume high CPU and
memory and never return a response. Webpack is unaffected.

Change the no-override default in getDevBundler() from platform-dependent
Turbopack (all non-Windows) to Webpack. DEER_FLOW_DEV_BUNDLER=turbo
continues to work as an explicit opt-in for local diagnosis.

Fixes #5132

* docs(frontend): address webpack default review feedback

* docs(frontend): clarify webpack default rationale
2026-09-01 22:56:23 +08:00
rayhpeng
cd35363a05
fix(history): early user messages vanish or jump mid-run when pagination and context compaction overlap (#4696)
* fix(history): stop dropping user messages that fall outside the loaded page window

Two independent paths made a user's own message disappear from a long thread
(#4666, #4508, #4363). Both are reproduced by a real two-round run: once the
thread passes the 50-row `/messages/page` window AND context compaction fires,
the two sources of truth stop overlapping at the head.

1. Middleware-answered tool results never reached the event store. A middleware
   that short-circuits a tool call (e.g. ReadBeforeWriteMiddleware's blocked
   write) returns a user-visible ToolMessage, but LangChain never emits
   `on_tool_end`, so RunJournal never persisted it — the user saw it during the
   run and it vanished on reload. RunJournal already reconciles final-output
   tool messages, but only for an `ask_clarification` allowlist. The allowlist
   is removed; scope stays bounded by the three conditions that actually matter
   (visible, this run's lead agent, not already persisted), so subagent results
   still stay in their own step feed.

2. mergeMessages discarded the checkpoint prefix before the first shared anchor.
   #4065 correctly established that a summarization-rescued early message must
   not be appended to the tail, and suppressed it instead. That suppression is
   what deletes the message when the first history page no longer reaches back
   to it. It is now woven in before the first shared anchor — the one position
   both the checkpoint and seq-sorted history agree on — so #4065's invariant
   (never the tail) still holds. A collapsed unloaded gap is recoverable by
   paging; a dropped message is not.

Verified against real captured payloads from the reproducing run: the first user
message returns to the transcript. Its exact position is still approximate —
after compaction the live window carries too few anchors to place it precisely,
which only seq-based ordering can close.

Backend: 10809 passed (baseline 10808; same 15 pre-existing failures in
browser/crawler community tools). Frontend: 986 passed, typecheck + eslint clean.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* feat(events): look up a persisted message's seq by identity

Groundwork for placing checkpoint messages in the seq-ordered thread feed
(#4666). A checkpoint carries no seq of its own and loses messages to
summarization, so once the feed's 50-row page window no longer reaches back to a
surviving old message, a client has nothing to place it by. The seq already
exists in run_events keyed by the message id — this exposes it without paging
the whole feed.

`message_identity` is the backend half of the identity rule the frontend applies
in `hooks.ts::messageIdentity`: a ToolMessage is keyed by `tool_call_id`, and
DynamicContextMiddleware's `X` / `X__user` human copies collapse to one identity.
The two halves must stay in sync — a mismatch is silent, degrading placement
rather than raising.

`get_message_seqs` is implemented for all three stores. Misses are absent from
the result rather than an error, so callers degrade to their own placement rule;
the earliest seq wins when one identity resolves to several rows, so a
re-persisted message keeps the position it first occupied. The DB store decodes
rows in Python because `content` is a TEXT column holding a JSON string, not a
JSON column — the identity fields cannot be projected in SQL.

Nothing consumes this yet; no behavior change.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* feat(runtime): carry each persisted message's feed seq on values frames

Attaches `additional_kwargs.deerflow_seq` to messages in a root `values` frame
that the thread feed already holds, so a client can place a message the
checkpoint kept but its loaded history page window no longer reaches (#4666).
Nothing is written back to the checkpoint: the seq is added when the frame is
serialized and belongs to that frame only.

Cost is bounded to frames introducing identities the run has not resolved yet.
Messages this run produces are not in the feed while streaming, so they are
looked up once, recorded as misses, and never retried — in a real run the only
frame that pays for a query is the one where compaction brings older messages
back into view. Measured on a reproducing two-round run: 1 lookup across 25
values frames.

The stamper is built once per run rather than per `_stream_once`, or a goal
continuation would discard the resolved seqs. Subgraph frames are not stamped:
a subagent's snapshot is not part of this thread's feed ordering. A lookup
failure logs and leaves the frame unstamped rather than failing it — placement
is an enhancement and clients fall back to their own ordering rule.

Frontend does not read the field yet; no behavior change.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(gateway): strip the server-owned message seq from untrusted input

`deerflow_seq` is display metadata the Gateway attaches when it serializes a
values frame. A client replaying messages (regenerate / edit-and-rerun) would
otherwise write it into the checkpoint, where it becomes wrong the moment the
thread is forked — a branch re-seeds its feed and reassigns seq (#4380).

Joins the existing server-owned key set, so it follows the same trusted-internal
rule as the dynamic-context and view-image markers.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(frontend): place a checkpoint message by its feed seq, not its nearest anchor

Completes #4666. Weaving a compaction-rescued message before the first shared
anchor keeps it in the transcript, but not in the right place: after compaction
the live window carries too few anchors, and the nearest one can sit deep inside
the loaded page window — measured at row 25 of 50 on a reproducing run, which is
why the first user turn rendered mid-transcript instead of at the head.

Both sides now carry the backend's thread-global seq. `buildVisibleHistoryMessages`
copies each row's `seq` onto the message (same shape as the existing `run_id`),
and the Gateway stamps it onto `values` frame messages it has already persisted.
A live message whose seq is below the loaded window's lower bound is placed ahead
of everything on screen rather than before the nearest anchor. A message with no
seq — still streaming, so not in the feed yet — keeps the weaving path, since the
tail is already its correct position.

Verified against the captured payloads of the reproducing run: the first user
message goes from absent, to #13 (behind the second question), to #0.

Frontend: 988 passed, typecheck + eslint clean.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(frontend): place a pre-window checkpoint message even when no anchor is shared

Also #4666. Placing a compaction-rescued message by its feed seq was gated on
reaching a shared anchor, because the split ran inside the anchor walk. When the
loaded page and the live checkpoint share no identity at all, that walk never
runs and the message fell through to `[...canonical, ...live]` — appended after
the entire window, the one arrangement #4065 proved wrong, with its seq known
the whole time.

That is not a corner case. Open an old, already-summarized conversation and send
a message: the page on screen is the newest rows from before that turn, while
the checkpoint holds the rescued first user turn plus steps of the new run that
are not in the feed yet. On a reproducing run the two sides shared zero anchors
and the user's own first question rendered at row 50 of 50 — the reported
"first message jumps to the bottom".

Split `beforeWindow` out of `live` before walking anchors, walk `liveInWindow`,
and use it for the no-anchor branch as well, so a message routed ahead of the
window is not re-appended at the tail by dedup.

Measured on captured payloads of a reproducing run (real gateway, real
compaction), first user message position:

  no shared anchor:  row 50 -> row 0, seq order monotonic again
  shared anchors:    row 0 -> row 0 (unchanged)
  paged to the top:  row 0 -> row 0 (unchanged)

Regression test verified red-green: reverting the fix fails it with the message
rendered after the window.

Frontend: 989 passed, eslint + tsc clean.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(gateway): stamp the message feed seq on checkpoint reads, not only on stream frames

Completes #4666. `_MessageSeqStamper` sits on the streaming publish path, so a
client that joins a live run learns where a summarization-rescued turn belongs
while a client that merely opens the conversation does not — and opening is the
common case. `GET /threads/{id}/state` and `POST /threads/{id}/history` returned
the checkpoint with no seq at all, so the merge fell back to the nearest shared
anchor, which after summarization sits deep inside the loaded page.

Reproduced in a browser against a real gateway, on a thread that had already
compacted: the user's first question rendered at row 320 of 389, behind the
newest question instead of at the head. Both reads showed 0 of 13 messages
carrying a seq. That is the reported symptom, still present after the streaming
fix.

Add `stamp_messages_with_seq`, the request-scoped counterpart of the stamper:
everything a checkpoint still holds is already persisted, so one batched lookup
resolves the whole list and there is nothing to retry later. Resolve the store
through `_optional_run_event_store` rather than `get_run_event_store`, because
seq is placement metadata — a deployment without a feed must still be able to
read a thread.

After the fix, on the same thread in the same browser: 13 of 13 messages carry a
seq and the first question renders at the head, ahead of the newest one.

Backend: ruff clean, 326 passed across the touched suites.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* refactor(harness): move the injected-user-id suffix helpers to utils.messages to break an import cycle

message_identity imported strip_injected_user_message_id_suffix from the
dynamic-context middleware, closing a cycle (middleware -> deerflow.runtime
-> worker -> events -> middleware) that only stayed hidden while an earlier
import happened to break it. Define INJECTED_USER_MESSAGE_ID_SUFFIX and the
strip helper in deerflow.utils.messages and re-export them from the
middleware so existing importers keep working.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(docs): improve formatting and clarity in AGENTS.md and message-merge.test.ts

* perf(events): stop the seq scan once every wanted identity is resolved

Rows past the last wanted seq can only be re-persisted copies that
already lose the earliest-seq-wins tiebreak, so all three stores now
break out of the scan (and the db store out of its per-row JSON
decoding) once found covers wanted. Matters most for /state and
/history reads of long threads, where this lookup runs with no run
cache and a typically tiny wanted set.

Raised by review on #4696.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* refactor(events): share the seq-stamping expression between the two stampers

The walrus-plus-merge expression was duplicated verbatim between
stamp_messages_with_seq and _MessageSeqStamper.stamp — two counterparts
of one rule where silent divergence is the likely failure mode if only
one side is edited. Both now call attach_message_seq next to
MESSAGE_SEQ_KEY in message_identity.py. The trailing
isinstance(message, Mapping) guard was unreachable (a non-Mapping entry
already got identity = None) and is gone with the extraction.

Raised by review on #4696.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(events): seq stamping survives launch paths without user context

The db store's get_message_seqs defaults to user_id=AUTO, which raises
when no user is in the contextvar — the first strict-AUTO read ever
called from the worker context. On a launch path that never inherits
the auth context (e.g. a null-owner scheduled task), stamp()'s except
clause swallowed that into a per-frame warning and silently disabled
seq stamping for exactly the background runs that need it.

The stamper now soft-resolves the user id once at build time — the
same rule as the worker's write paths beside it (unset -> no filter)
— and passes it explicitly. jsonl/memory stores gain the same
user_id kwarg the base list_messages contract already carries.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* perf(events): SQL-prefilter the message seq lookup's candidate rows

get_message_seqs scanned and JSON-decoded every message row of the
thread: the early exit never fires when a wanted identity is absent
from the feed (a message still streaming, or checkpoint-only), and
/state / /history reads want the newest messages, so the ascending
scan traversed essentially the whole feed — with the content column
carrying full tool outputs, that is heavy I/O plus N JSON parses on
exactly the long threads this lookup exists for.

A LIKE prefilter now keeps that cost in SQL: only rows containing a
wanted raw id as a substring are fetched and decoded. False positives
are re-checked by message_identity; LIKE wildcards are escaped; an id
json.dumps would escape (breaking the verbatim-substring guarantee)
falls the whole set back to the full scan rather than silently
missing.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* docs(agents): sink runtime mechanism docs below the gateway guidance budget

Merging main pushed backend/app/gateway/AGENTS.md past its 40KB soft
budget (main had left 81 bytes of headroom). Per the nearest-file rule,
move the mechanism detail of the message-seq stamping and run-delivery
receipt sections — both owned by runtime/ code — into
packages/harness/deerflow/runtime/AGENTS.md, leaving the gateway file
the REST-surface summary and a pointer. The seq section also documents
the stamper's build-time soft user-id resolution and the db store's SQL
prefilter from the review follow-ups.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* docs(agents): sink durable-MCP task detail below the backend guidance budget

Merging main pushed backend/AGENTS.md past its 24KB module soft budget
(main itself is at 24762 after #4848 — this branch adds zero net bytes
to the file). Per the nearest-file rule, move the two durable-MCP task
runtime bullets' mechanism detail into
packages/harness/deerflow/mcp/AGENTS.md, leaving summaries and
pointers; this also restores ~2KB of headroom so the next merge does
not trip the same wire.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(events): re-ask a message-seq miss once the feed advances

The run-scoped stamper cached lookup misses for the whole run. A message
this run produces reaches a values frame before RunJournal flushes it, so
its first lookup legitimately misses — and the journal persists it moments
later, giving it a feed seq the stamper never asks for again. A long run
that afterwards rolls past the history page and compacts then carries that
message unstamped, back to the approximate anchor placement this stamper
exists to replace (#4666). A transient store error had the same permanent
effect, since the except clause degrades to an empty result.

A miss is now provisional while a hit stays final: RunJournal counts its
successful event-store writes as `feed_generation`, and the stamper re-asks
a missed identity only once that counter moves. Retrying is therefore
bounded by feed writes rather than by frames — the per-frame query the
run-scoped cache was built to avoid — and a failed lookup costs one
generation instead of the run.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-09-01 22:04:17 +08:00
Ryker_Feng
8d8ca506ba
feat(artifacts): download run files as zip (#5117)
* feat(artifacts): download run files as zip

* fix(artifacts): address archive review feedback

* fix(artifacts): gate unavailable archive downloads

* fix(artifacts): verify archive availability

* fix(artifacts): harden archive consistency

* fix(artifacts): reject archive path aliases
2026-09-01 22:01:21 +08:00
betterkite
b552b5015c
fix(messages): drop legacy <uploaded_files> tag handling (#4826)
* fix(messages): drop legacy <uploaded_files> tag handling (#4212)

PR #4174 unified upload-context injection on <current_uploads> (IM and web
both flow through UploadsMiddleware), and #4632 documented the current
path. This removes the remaining backward-compat parsing of the
pre-#4174 <uploaded_files> tag, the final cleanup item tracked by the
issue:

- deermem: only <current_uploads> is stripped from human turns before
  memory persistence, and the upload-sentence scrubber drops the legacy
  tag alternative.
- mem0: the mirrored message filter recognises only <current_uploads>.
- InputSanitizationMiddleware: remove the legacy tag from the blocked-tag
  denylist (it existed only because deermem parsed the old tag).
- frontend: stripUploadedFilesTag / stripInternalMarkers /
  parseUploadedFiles and the message-list fallback parse only
  <current_uploads>; demo thread fixtures are migrated to the current tag.

Scope decision: a <uploaded_files> block in pre-#4174 history is now
treated as ordinary user content (pinned by tests in both layers) instead
of being silently dropped or stripped.

* style: apply prettier formatting to stripUploadedFilesTag

* fix(uploads): keep legacy <uploaded_files> stripping for display/export only

Addresses review feedback on #4826: removing the legacy tag from the
frontend display layer made pre-#4174 threads render raw <uploaded_files>
XML (with server-side upload paths) in chat, copy data, and JSON exports.

The backend cleanup stands — memory pipelines and the sanitization denylist
treat only <current_uploads> as an internal marker. The frontend keeps the
legacy spelling in its display/export-only utilities
(stripUploadedFilesTag / INTERNAL_MARKER_TAGS / parseUploadedFiles and the
message-list fallback) so old history renders cleanly without leaking
internal paths, while the memory/sanitization scope-decision tests remain
unchanged.

Frontend tests now pin both spellings: <current_uploads> and legacy
<uploaded_files> are stripped from copy data, markdown leak-stripping, and
JSON exports.

* docs(ui): record accepted display-spoof tradeoff for legacy upload tag

Review note (willem-bd): since <uploaded_files> is off the sanitization
denylist, a live user can type the legacy spelling and fabricate file
chips / hide their own message text in display. Display-only and
self-inflicted with no backend semantics, so it is accepted for now;
documented at both the message-list fallback and stripUploadedFilesTag.
Age-gating the legacy spelling remains a possible follow-up.

---------

Co-authored-by: betterkite <313258397+betterkite@users.noreply.github.com>
2026-09-01 15:26:33 +08:00
Aari
cdc886ae85
fix(sandbox): make tool descriptions optional (#4878)
* fix(sandbox): make tool descriptions optional

* fix(sandbox): address optional description review

* test(sandbox): pin optional description contracts
2026-09-01 14:15:26 +08:00
PeaceMaker-best
72ba661b84
feat(skills): install local skill archives (#5039)
* feat(skills): install local skill archives

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

* fix(skills): enforce upload limits before parsing

* fix(nginx): scope skill upload limit to upload route

* fix(nginx): harden skill upload proxy handling

* fix(skills): improve archive upload feedback

* fix(skills): address upload review polish

---------

Signed-off-by: PeaceMaker-best <221849497+PeaceMaker-best@users.noreply.github.com>
Co-authored-by: PeaceMaker-best <221849497+PeaceMaker-best@users.noreply.github.com>
2026-08-31 15:22:45 +08:00
Stellar鱼
11e6cdd7e7
fix(frontend): preserve interrupted uniform-run order (#4834) 2026-08-30 22:19:52 +08:00
hataa
56454af931
perf(frontend): cache settled copy-data derivation across streaming chunks (#5095)
* perf(frontend): cache settled copy-data derivation across streaming chunks

Every SSE values chunk re-renders MessageList, and the re-render re-derived
copy/toolbar text for every settled row: getAssistantTurnCopyData re-ran the
O(turn bytes) content extraction per settled group, and MessageListItem's
toolbar recomputed getMessageCopyData per message. Settled group arrays keep
their identity across chunks (deriveStableMessageGroups), so both derivations
now cache on that stable reference: a WeakMap keyed on the messages array for
turn copy data, and a useMemo on message identity for the toolbar copy text.

Fixes #5094

* fix(frontend): gate row copy-data memo and correct cache win claim

Address review: derive one memoized copy value only when
isHuman || (!isLoading && showCopyButton) and reuse it for both editing
and the toolbar, so settled assistant rows (whose toolbar never renders)
skip the derivation and human rows derive once, not twice; correct the
assistantTurnCopyDataCache comment — the regex/trim split is already
cached per message, the cache's win is the traversal/allocations for
string turns and the uncached O(bytes) map/join/trim for array-content
turns (benchmarked: 5.1x / 17.2x per settled history sweep).

* style(frontend): expand single-line messages array for Prettier
2026-08-30 14:28:43 +08:00
Stellar鱼
468eab4b5d
fix(frontend): stabilize model load error feedback (#5021) 2026-08-30 10:56:55 +08:00
Nefelibata
c6f6a01f56
fix(runtime): prevent IndexError in MemoryStreamBridge._make_gap on empty events buffer (#5047)
* fix(runtime): prevent IndexError in MemoryStreamBridge._make_gap on empty events buffer

* fix(runtime): handle empty stream replay gap bounds across backend and frontend

- Clamp MemoryStreamBridge queue_maxsize at 1 and validate StreamBridgeConfig.queue_maxsize >= 1
- Update StreamGap docstring to clarify None retained bounds
- Allow StreamReplayGapData and parseStreamReplayGap in frontend to accept string | null bounds, safely resuming when bounds are null
- Add backend and frontend regression unit tests for queue clamping and null bounds replay gap

* docs(stream-bridge): bump config_version and document empty buffer replay gap behavior

* docs: document nullable gap bounds and sync helm config_version to 37
2026-08-29 17:21:29 +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
nonoge
0cb356858b
fix(frontend): truncate selected model names (#5050)
* fix(frontend): truncate selected model names

* test(frontend): scope model selector overflow guard
2026-08-29 08:03:29 +08:00
Syt3s
6e5a41fd9a
feat(frontend): add conversation outline navigation for long chats (#5025)
* feat(frontend): add conversation outline navigation for long chats

* fix(frontend): escape bottom lock before outline navigation
2026-08-29 08:00:55 +08:00
Jholly
2d0568a14f
fix(frontend): format structured upload error details (#5071)
* fix(frontend): format structured upload error details

* fix(frontend): preserve generic message error details
2026-08-28 10:40:29 +08:00
georgelichen
bdd68469c1
fix(frontend): stabilize Windows development startup (#5036)
* Stabilize Windows frontend development startup

Next.js Turbopack can infer a user-level workspace root and hit an internal Rust task panic on Windows. Keep the frontend root explicit and select Webpack on Windows while preserving Turbopack elsewhere.

Constraint: The existing non-Windows development workflow should keep Turbopack.

Rejected: Disable Turbopack on every platform | unnecessary performance regression.

Confidence: high

Scope-risk: narrow

Reversibility: clean

Directive: Revisit the Windows Webpack fallback after the upstream Turbopack panic is resolved.

Tested: Prettier, ESLint, TypeScript, 139 frontend test files with 1060 tests, and Windows pnpm dev smoke test on port 3010.

Not-tested: Production build and browser E2E suite.

Related: #4957

* Make the frontend bundler fallback diagnosable

Keep the Windows Webpack fallback while allowing local diagnosis to select either supported Next.js bundler. Resolve the Turbopack root through the Node 20-compatible URL API so the explicit workspace boundary remains effective across the declared Next.js engine range.

Constraint: Windows defaults to Webpack because of the observed Turbopack panic, while non-Windows defaults to Turbopack.
Rejected: Accept arbitrary bundler values | Next.js only supports the two explicit flags and invalid values should fail early.
Confidence: high
Scope-risk: narrow
Reversibility: clean
Directive: Revisit the default Windows fallback when the upstream TaskGuard issue is resolved; the override is intentionally retained for diagnosis.
Tested: Frontend format, ESLint, TypeScript, 139-file/1062-test Rstest suite, target launcher tests, and next.config.js absolute-root load check.
Not-tested: Production build and browser E2E suite.
2026-08-27 16:07:16 +08:00
Xinmin Zeng
943d148e5e
feat(threads): distinguish branched conversations (#4983)
* feat(threads): number branched conversation titles

* feat(frontend): show branch lineage in recent chats

* fix(threads): allocate unique branch suffixes

* fix(threads): preserve suffix and filter semantics
2026-08-25 08:29:18 +08:00
Aari
ff0a6768c2
feat(subagents): add unified capacity and durable batch execution (#4998)
* feat(subagents): add capacity controls and durable batches

* fix(helm): sync subagent config schema version

* fix(subagents): preserve batch history without worker

* fix(subagents): support explicit factory runtimes

* fix: address durable batch review findings
2026-08-25 07:49:38 +08:00
陈志谦
8989173c8d
fix(frontend): restore sanitization in custom streamdown rehype chains (#4987)
* fix(frontend): restore sanitization in custom streamdown rehype chains

Streamdown 2.5 replaces its entire default rehype chain
[rehype-raw, rehype-sanitize, rehype-harden] with whatever array the
caller passes via the rehypePlugins prop. Every custom chain in this
repo therefore rendered LLM/stored markdown without any sanitization:

- Artifact markdown previews (markdown-preview-plugins.ts +
  artifact-file-detail.tsx) parse raw HTML via rehypeRaw, so a
  generated .md artifact could inject <style>/<iframe>/on* handlers
  into the workspace DOM (stored XSS; only javascript: anchors were
  blocked by the ArtifactLink component).
- The memory settings summary (memory-settings-page.tsx) spread the
  shared preset without component overrides, so a hostile
  <a href="javascript:..."> in stored memory content rendered as a
  clickable anchor.

Fix strategy:

- Add rehype-sanitize (already resolved in the lockfile via streamdown)
  as a direct dependency and re-insert a [rehypeSanitize, schema] step
  in the shared preset (core/streamdown/plugins.ts). It runs after
  rehypeRaw (raw HTML must be parsed into hast before it can be
  cleaned) and before rehypeKatex/rehypeSlug (their output is trusted
  and would otherwise be filtered or clobbered) - the same
  raw -> sanitize -> math ordering streamdown itself uses.
- The schema extends rehype-sanitize's GitHub-style defaultSchema (the
  base of streamdown's own sanitize schema) so legitimate authored
  artifact HTML (tables, details, images, alignment/size attributes)
  keeps working while script/iframe/style, on* handlers and
  non-allow-listed URL schemes (javascript:, data:, ...) are dropped.
  The only extensions are tel: hrefs and the math-inline/math-display
  class markers remark-math emits and rehype-katex detects.
- Position rehypeSlug after the sanitize step in the artifact chain so
  sanitize's id clobbering (id="x" -> id="user-content-x") cannot break
  the heading anchors it creates.
- Pass a: createMarkdownLinkComponent() on the memory settings page as
  defense in depth, matching the chat rendering path.

Unit tests feed a hostile payload (<a href="javascript:...">,
<img onerror>, <script>, <iframe>, <style>, ontoggle) through both
render paths and assert no executable/clickable equivalent survives,
plus regression guards for heading anchors, legitimate HTML and KaTeX
math rendering.

* fix(frontend): keep the sanitize clobber prefix on heading anchors; minimal lockfile

Review follow-ups on the sanitization change:

- Heading anchors: rehypeScopedSlug replaces rehype-slug in the artifact
  chain. It runs after the sanitize step (so raw-HTML headings are also
  anchored) but keeps rehype-sanitize's user-content- id prefix — an
  untrusted heading like "## current" cannot mint an unprefixed
  id="current" (the DOM-clobbering shape the sanitizer guards against).
  In-page fragment links are translated to the prefixed anchors so they
  still resolve; external URLs, bare "#", already-prefixed fragments and
  sanitize-prefixed raw-HTML ids are left untouched.
- Lockfile: regenerated as a minimal diff — only the two direct-dependency
  importer entries (rehype-sanitize, github-slugger for the scoped slug)
  are added; the libc platform selectors on the 64 native package records
  are preserved byte-for-byte instead of being dropped by lockfile
  normalization.

Full frontend suite: 1034 tests passing; tsc and prettier clean.

* style: reorder github-slugger import ahead of the hast type import

* test(e2e): expect the clobber-prefixed heading anchor in artifact preview

The scoped slug plugin gives generated heading ids rehype-sanitize's
user-content- prefix and translates fragment links to match, so the
anchor-scroll test must locate the prefixed id.

* fix(frontend): reset the scoped slugger per tree; keep footnote anchors single-prefixed

Review follow-ups:

- The scoped slug attacher holds one GithubSlugger, but streamdown
  caches the unified processor by plugin name, so the instance survived
  across parses and repeated renders of the same heading grew -1/-2
  suffixes (the artifact-anchor e2e could not find the id on re-render).
  The transformer now resets the slugger per tree, as rehype-slug does;
  a regression test renders identical artifact markdown twice.
- remark-rehype emits GFM footnote anchors already clobber-prefixed
  (user-content-fn-1); the sanitize step prefixed those ids again while
  their hrefs stayed single-prefixed, breaking footnote navigation in
  every chain built on the shared preset. A new rehypeClobberFragments
  step runs right after sanitize: double-prefixed ids are normalized
  back to one prefix, and unprefixed fragment hrefs are translated to
  the prefixed form (already-prefixed and external links untouched).
  The artifact slug now inserts after this step; covered by a footnote
  regression test on the shared render path.

Unit suite 1036 passing; artifact-preview e2e verified locally
(9/9, including the heading-anchor scroll test).
2026-08-24 22:20:24 +08:00
Aari
1aa813ddb3
feat: add managed subagents and delegation scopes (#4887)
* feat: manage and scope subagents

* fix: address subagent review feedback

* fix: address managed subagent review feedback

* fix: harden subagent settings semantics

* fix: harden managed subagent cache invalidation

* fix: reuse assembled lead agent inputs

* fix: migrate managed subagent definitions

---------

Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
2026-08-24 11:04:23 +08:00
陈志谦
9232e1e6a9
fix: DeerFlow brand casing in disclaimer, What's New title, setup-sandbox .PHONY (#4970)
* fix: DeerFlow brand casing in disclaimer, 'What's New' title, setup-sandbox .PHONY

- en-US disclaimer said 'Deerflow'; every other user-facing string
  brands it 'DeerFlow' (rendered under the chat composer)
- landing section title 'Whats New in DeerFlow 2.0' -> 'What's New'
- setup-sandbox was the only Makefile target missing from .PHONY

* test: align disclaimer assertions with DeerFlow brand casing

The unit and e2e suites still expected the old "Deerflow" spelling in
inputBox.disclaimer; update both to the corrected locale string so the
i18n load test and the chat footer assertion pass again.

---------

Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
2026-08-24 10:58:48 +08:00
wutongyuonce
6688d01c8f
fix(frontend): avoid recreating browser stream after reconnect (#4951)
* fix(frontend): avoid recreating browser stream after reconnect

* test(frontend): cover reconnect delay reset
2026-08-24 10:29:57 +08:00
PiedPiper911
582fa20001
fix(artifacts): serve SHA-256 via ETag so preview/edit work on non-secure contexts (#4865)
* fix(artifacts): serve SHA-256 via ETag so preview/edit work on non-secure contexts

crypto.subtle is only available in secure contexts (HTTPS or localhost). The frontend fell back to it to compute an artifact's SHA-256 when the Gateway did not return one, which threw on http://<lan-ip>:<port> and broke both artifact preview and inline editing (issue #4864).

- Gateway now returns the real SHA-256 as an ETag header for inline text and active-content artifact responses (and skill-archive members).
- Frontend prefers the ETag and only computes a hash as a last resort, falling back gracefully (FNV-1a) instead of throwing when crypto.subtle is missing.

* fix(artifacts): address PR review feedback for #4864

- Cache SHA-256 digests by (path, mtime_ns, size) so the many small Range
  requests a browser issues while scrubbing/paginating a preview do not each
  re-hash a potentially huge artifact from scratch (performance).
- Gate inline editing on a real 64-hex revision: hasRevision requires
  sha256.length === 64, so the FNV-1a fallback on non-secure origins keeps
  preview working but no longer 422s on save (contract).
- Anchor and lowercase the ETag regex and accept the weak W/ prefix gzip
  emits, so uppercase hex and longer digests (sha-384/512) can't masquerade
  as sha-256.
- Cover the forced-download ETag on the backend and add frontend tests for
  weak-ETag parsing and the non-secure-context FNV fallback.

Feedback from reviewer willem-bd on PR #4865.

* style: fix ruff format and prettier issues

- test_artifacts_router.py: collapse two over-split client.get() calls to
  satisfy ruff format (line-length 240)
- loader.ts / artifact-file-detail.tsx / loader.test.ts: apply prettier
  formatting and restore LF line endings

* style: fix ruff format and prettier issues

- test_artifacts_router.py: collapse two over-split client.get() calls to
  satisfy ruff format (line-length 240)
- loader.ts / artifact-file-detail.tsx / loader.test.ts: apply prettier
  formatting and restore LF line endings

* style: fix ruff format and prettier issues

- test_artifacts_router.py: collapse two over-split client.get() calls to
  satisfy ruff format (line-length 240)
- loader.ts / artifact-file-detail.tsx / loader.test.ts: apply prettier
  formatting and restore LF line endings

* style: fix ruff format and prettier issues

- test_artifacts_router.py: collapse two over-split client.get() calls to
  satisfy ruff format (line-length 240)
- loader.ts / artifact-file-detail.tsx / loader.test.ts: apply prettier
  formatting and restore LF line endings

* style: reformat artifact-file-detail.tsx for prettier with tailwind class ordering

* fix: invalidate SHA-256 cache after artifact edit

Clear the LRU cache after os.replace() so the next preview request
computes the new digest. Edits are rare, so clearing the whole
256-entry cache costs nothing (addressing PR review comment #5).

* fix(artifacts): skip ETag for oversized files + CRLF->LF + cache invalidation (#4865)

* fix(loader): use real empty-content SHA-256 for empty 416 range (#4865)

* test(artifacts): assert oversized artifacts carry no SHA-256 ETag (#4865)

* style(frontend): format long sha256 constant (prettier)

* test(backend): fix oversized-artifact ETag assertions and formatting (ruff)

* test(backend): keep oversized-payload line within ruff 240-col config
2026-08-24 07:45:30 +08:00
Aari
5ffc2d3e27
feat(mcp): complete durable task notifications and chat UI (#4833)
* feat(mcp): add reliable task notifications and cancellation

* feat(mcp): add background task chat UI

* fix(mcp): hide and sanitize task notification prompts

* fix(mcp): sanitize projected task names

* fix(mcp): harden task notifications and details

* fix(mcp): harden task lifecycle recovery

* fix(mcp): gate task UI and isolate cancellations

* test: scope plain-text response locator

* fix(mcp): align task notification boundaries

* fix(mcp): bound task delivery retries

* fix background task notification races
2026-08-22 16:53:32 +08:00
starslittle
7e4996eef3
fix(frontend): surface model loading failures (#4840)
* fix(frontend): surface model loading failures

* refactor(frontend): reuse model error UI primitives

* fix(frontend): address model banner review feedback

* refactor(frontend): remove unused model fetch state
2026-08-17 08:23:07 +08:00
Aari
16ecf7b006
fix(frontend): preserve completed message actions during streaming (#4844)
* fix(frontend): preserve completed message actions during streaming

* fix(frontend): address streaming action review feedback

* fix(frontend): reuse settled stream snapshots
2026-08-16 23:53:22 +08:00
starslittle
b341120a4a
fix(frontend): authenticate remaining gateway reads (#4827)
* fix(frontend): authenticate remaining gateway reads

* fix(frontend): authenticate artifact reads

* fix(frontend): include status in model errors
2026-08-16 12:03:13 +08:00
Ryker_Feng
1e8cedb9f4
feat(channels): polish Buzz frontend (#4727)
* feat(channels): complete Buzz frontend copy

* feat(channels): add official Buzz provider icon

* test(i18n): clarify translation suite scope
2026-08-12 08:33:35 +08:00
Ryker_Feng
e401ae2d7b
feat(integrations): support switching Lark app credentials (#4703)
* feat(integrations): support switching Lark app credentials

* fix(integrations): harden Lark app switching

* refactor(integrations): simplify Lark switch flow

* fix(integrations): reject superseded Lark flows

* test(integrations): pass Lark flow generation

* fix(integrations): preserve pending Lark flows

---------

Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
2026-08-10 07:19:19 +08:00
Daoyuan Li
480a3757ed
fix(frontend): add public case study routes (#4635) 2026-08-05 08:49:14 +08:00
Vanzeren
bec6277930
fix(frontend): restore user message above same-run steps after mid-run reload (#4660)
* fix(frontend): restore user message above same-run steps after mid-run reload

After a mid-run page reload, replayed messages-tuple steps can reach the
merged list before the turn's human message (the retained replay buffer
may have dropped it), and the live-only human is woven in before the next
shared history anchor. The steps panel then renders above the user
message it belongs to and the per-turn token row splits one run in two.

restoreLocalTurnMessageOrder only covers the locally submitted turn, so
add restoreReconnectedTurnMessageOrder for the reconnected path: a
visible AI/tool step above the last visible human is provably misplaced
when the same run also appears below the human (canonical history is
seq-sorted), or when the step is run_id-less (live-only) and a
run_id-less step follows the human. Pagination orphans from older turns
(#4399) and resent turns after an interrupted run fail both checks and
keep their position.

* fix(frontend): never move a completed turn's terminal answer below the next human

Branch-seeded threads and mocked feeds can share one run_id across turns,
so the same-run sandwich check alone misfired on completed multi-turn
history: a previous turn's final answer was pulled below the next user
message, stripping the turn of its branchable assistant group (e2e
branch-thread.spec.ts).

Only steps after the last terminal assistant answer (visible content, no
tool calls) in the segment are now candidates for reordering. A
still-streaming text step can look like a terminal answer until its tool
call arrives (#4304); it then stays above the human until canonical
history heals the order — an accepted transient, far safer than moving a
completed answer.
2026-08-04 15:07:59 +08:00
Baldwinzc
91d7f4e4fa
fix(frontend): keep inline-code <think> pairs in content and restore copy for reasoning-only turns (#4647)
Two related fixes in the reasoning extraction path of
core/messages/utils.ts:

1. splitInlineReasoning's first pass stripped every closed
   <think>...</think> pair unconditionally, so a message that discusses
   the tag literally in markdown inline code (e.g. "Wrap your reasoning
   in `<think>...</think>`") had its code span hollowed out and the
   inner text shipped to the Reasoning panel. The streaming pass already
   guards backtick-adjacent openers; apply the same guard to the
   closed-pair pass so both passes agree on what counts as literal tag
   talk.

2. getAssistantTurnCopyData fell back to reasoning via
   `content ?? reasoning`, but extractContentFromMessage never returns
   null, so the fallback was dead code and a reasoning-only turn (e.g.
   stopped mid-thinking) rendered no copy button at all - inconsistent
   with getMessageCopyData, which does copy reasoning in that case. Use
   the same empty-string check it uses.
2026-08-03 08:27:23 +08:00
Aari
da282811b8
feat(frontend): reopen the skill list after a skill is selected (#4639)
* feat(frontend): reopen the skill list after a skill is selected

Selecting a skill closed the composer's skill list for good: `/` no longer
reopened it, so a skill could not be looked up or swapped without deleting
the chip first.

The list now reopens from the editable text beside the chip, and picking an
entry swaps the chip rather than stacking a second activation, since the wire
format carries exactly one leading /skill. Builtin commands are withheld in
that state because they own the whole composer line, and Enter navigates the
list before submitting except while an IME is composing.

The trigger is unchanged: a slash still opens the list only at the start of
the input.

* fix(frontend): keep builtin names reserved in the reopened skill list

Withholding the builtin list from getMatchingSkillSuggestions in chip mode
also disabled the reserved-name filter it drives, so a custom skill named
after a builtin command became selectable there. Nothing rejects such a name
at install time, and submitting the resulting chip runs the command instead
of the skill.

Pass the builtin list as before and drop the builtin entries from the result
instead. The new regression covers both sides of the reservation, and the
reopen test now waits for the list before pressing Enter.

* fix(frontend): hide skills the slash parsers refuse from the picker

The composer picker reserved only the two builtin command names, while both
slash parsers refuse the seven names in the shared contract. A skill named
bootstrap, help, memory, models, new or status was therefore offered, could
be selected into a chip, and submitted — and then activated nothing, because
parse_slash_skill_reference drops the name on the way in. The turn reached
the model as literal text with no skill loaded and no error anywhere.

Reserve the contract names alongside the builtin ones, so the picker cannot
offer what the parsers will not honour.
2026-08-02 22:58:15 +08:00
Ryker_Feng
78f7a5f7c0
fix(frontend): sort chats page by pinned state (#4643) 2026-08-02 22:56:18 +08:00
Kushida
7025ccee40
fix(artifacts): scope full previews to their thread (#4634) 2026-08-02 10:07:34 +08:00
DanielWalnut
459dd78707
perf(frontend): bound delivery, bundles, and long-running UI work (#4622)
* docs: design frontend performance remediation

* docs: plan frontend performance remediation

* test(frontend): add route asset performance budgets

* perf(nginx): compress textual responses safely

* perf(frontend): lazy load case study media

* perf(frontend): bound static demo file tracing

* perf(frontend): restore static locale boundaries

* perf(frontend): defer closed workspace panels

* perf(frontend): split editors and deduplicate highlighting

* perf(frontend): index incremental message derivation

* perf(frontend): stabilize paged history cache policy

* perf(frontend): bound streaming markdown renders

* perf(frontend): virtualize message history

* perf(frontend): bound and virtualize chat lists

* perf(frontend): suspend inactive decorative animation

* perf(browser): stream latest frames as binary

* perf(artifacts): stream bounded text previews

* docs: finalize performance runtime boundaries

* style(backend): apply test formatting

* fix(frontend): keep translation functions client-side

* perf(frontend): defer decorative animation bundles

* test(frontend): lock optimized route budgets

* fix: harden frontend performance boundaries

* test(frontend): update i18n provider fixture

* fix(frontend): preserve sidebar pagination position

* style(backend): format artifact range test
2026-08-01 22:19:59 +08:00
Baldwinzc
2f6cadb8b0
fix(frontend): stop matching <header> as <head> in HTML preview base injection (#4625)
appendHtmlPreviewBaseHref detected the head tag with /<head[^>]*>/i,
which also matches <header ...>. For a fragment with no <head> that
opens with <header> - a common shape in agent-generated report pages -
the <base> element was injected after the <header> opening tag instead
of being prepended, so relative assets appearing before that point
(e.g. a leading <img>) resolved without the base and failed to load in
the sandboxed iframe.

Use the word-boundary-safe /<head(?:\s[^>]*)?>/i that the sibling
appendHtmlPreviewScrollRestoration already uses, keeping the two
injectors consistent.
2026-08-01 21:59:27 +08:00
qin-chenghan
8234370a6a
feat(artifacts): inline editing for text artifacts in the panel (#4596)
* feat(artifacts): inline editing for text artifacts in the panel

Add a PUT /api/threads/{id}/artifacts/{path} endpoint that atomically
replaces an existing UTF-8 text file under /mnt/user-data/outputs after
verifying its SHA-256 revision. Active runs conflict (409); binary,
symlink, oversized, and non-output paths are rejected.

Frontend: edit/save/discard buttons, draft state with conflict detection,
CodeEditor onChange/onSave, loader SHA-256 from ETag, i18n, beforeunload guard.

Backend: PUT endpoint with thread reservation, atomic temp-file replacement,
sandbox sync for non-mounted providers, rollback on failure, ETag on GET.

Tests: 8 backend + 1 blocking-IO + 3 frontend test files.

* fix(artifacts): scope replacement permissions and release sandboxes

---------

Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
2026-08-01 09:15:21 +08:00
Huixin615
5b7ada0cac
fix(frontend): dedupe injected user messages during long runs (#4620) 2026-07-31 22:50:11 +08:00
Amorend
85c3909c2e
feat: show real-time context window usage (#3125) (#3183)
* feat: show real-time context window usage in chat UI (#3125)

Adds a `context_usage` block to `GET /api/threads/{id}/token-usage`
(token count from the live checkpoint, the thread model's
`context_window`, and a percentage), introduces a new
`ModelConfig.context_window` distinct from the per-call `max_tokens`
output cap, and surfaces the percentage in the chat header — inside
`TokenUsageIndicator` when token-usage tracking is on, or as a
standalone badge when it's off so context capacity stays visible
independent of cost tracking.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* feat: per-category breakdown for context window usage

Replace the single-number context_usage payload with a Claude-Code-style
breakdown — messages, system prompt, skills, system/MCP tools (active +
deferred), custom agents, memory injection, autocompact buffer, and free
space — and surface it in the chat UI with a segmented progress bar and
per-row table.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* docs(config): document context_window across model examples

Add `context_window` to every example model in config.example.yaml so the
new chat-UI "% context used" indicator works out of the box for whichever
example a user adopts. Each value is the published default at the time of
writing; users are pointed at the official model spec to verify. Bumps
config_version to 11 so `make config-upgrade` flags outdated user configs.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* style: ruff format (line-length 240)

No behavior change — collapses two multi-line expressions that fit on
one line under the project's 240-char limit. Picked up by `make format`.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* review: address Copilot bot comments on #3183

- token-usage-indicator: switch `{contextPercentage && (...)}` to an
  explicit `!= null` check. (The string `"0"` is actually truthy in JS so
  the original code wasn't buggy, but the explicit check is clearer.)
- context-usage-breakdown: drop the `useMemo` around segments/totals — the
  computation is O(n) over a handful of rows and the previous memo deps
  omitted `t.contextUsage.categories`, so the bar's tooltips/aria-labels
  could stay in the old language after a locale switch.
- context_usage._split_tools: snapshot MCP names from
  `get_cached_mcp_tools()` directly instead of re-reading
  `extensions_config.json` after `get_available_tools()` already loaded
  it. Removes redundant file I/O on every `/token-usage` poll.
  (`get_available_tools()` still emits its own INFO logs — silencing
  those is out of scope here.)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* style(frontend): prettier --write context-usage-breakdown

CI's `pnpm format` (prettier --check) caught two lines previously
formatted by hand. Collapses one comma to fit on one line; no behavior
change.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* fix(gateway): correct context-usage breakdown + add exact token counting

The context-usage indicator shipped two bugs that silently zeroed whole
breakdown rows (both caught by try/except, so the feature looked alive but
produced wrong numbers):

1. _count_system_prompt passed app_config= to get_deferred_tools_prompt_section,
   which only accepts deferred_names -> TypeError swallowed -> system_prompt
   row always 0, and used_tokens/percentage undercounted by the full prompt.
   Also subtracted the deferred section twice (the rendered prompt already
   excluded it). Fix: derive deferred names deterministically and pass them to
   apply_prompt_template; drop the redundant subtraction.

2. _split_tools imported a non-existent get_deferred_registry -> ImportError
   swallowed -> all four tool-category rows always 0. Fix: classify via the
   public is_mcp_tool predicate + tool_search.enabled (mirrors
   build_deferred_tool_setup); the MCP tag is set by get_available_tools.

Added token_usage.counting (approximate|exact). 'exact' routes text/schema/
message counting through the model tokenizer (tiktoken cl100k_base) via the
existing memory-module machinery (lazy load + cache + cooldown + CJK-aware
fallback), so CJK-heavy threads stop being undercounted by chars//4.

Regression + e2e tests added; 6621 backend tests pass.

* fix(gateway): harden context usage accounting

* fix(gateway): count promoted MCP tools as active in context usage

Promoted tools (deferred MCP tools the thread has fetched via tool_search)
have their full schema bound on every subsequent turn by
DeferredToolFilterMiddleware, so they consume context like any active tool.
The breakdown previously left them in the reserved *_deferred rows, under-
counting the thread's used_tokens.

Classification now treats a tool as deferred only when tool_search is enabled,
it is MCP-sourced, AND it has not been promoted. The promoted set is read from
the checkpoint's channel_values and scoped by catalog hash — matching the
runtime middleware, so a stale promotion from MCP-config drift cannot inflate
the active count.

The static system prompt still lists all deferred tool names (promotions only
affect schema binding, not the prompt), so _count_system_prompt's deferred
rendering is intentionally left unchanged.

8 new tests cover classification, catalog-hash scoping (match / drift /
compute-failure / malformed), and checkpoint extraction.

* fix(context): address review feedback

* fix(context): count structured message payloads

* fix(context): harden usage accounting

* fix(config): bump schema for context usage fields

* refactor: narrow context usage to core indicator

---------

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
2026-07-31 21:57:22 +08:00
Baldwinzc
17461ee52e
fix(frontend): parse uploaded filenames containing parentheses (#4608)
parseUploadedFiles stopped the filename capture at the first "("
([^\n(]+), so an entry like "- photo (1).png (12.3 KB)" failed to
match and the file silently disappeared from the message's file chips.
Browsers produce such names for duplicate downloads, making this a
common real-world shape.

Anchor the size group on the "(<number> <unit>)" pair the backend
emits (uploads_middleware formats sizes as "%.1f KB"/"%.1f MB") and
let the filename match greedily up to it, so parenthesized filenames
parse correctly.
2026-07-31 18:08:34 +08:00
Huixin615
133a82c6c2
fix: isolate MCP server toggles from invalid peer configs (#4577)
* fix: isolate MCP server toggle updates

* fix: write extensions config atomically

* fix: normalize MCP transport aliases
2026-07-31 08:32:39 +08:00
Ryker_Feng
150f7740c7
security: harden auth next-path validation (#4587)
* security: harden auth next-path validation

* chore: address auth review nits
2026-07-30 23:53:52 +08:00
Aari
c066819f69
fix(frontend): keep streaming reasoning above the answer text (#4578)
A reasoning model's turn showed its thinking below its answer while
streaming, then flipped to thinking-above-answer once the turn settled.
One message is rendered by two components with opposite ordering rules:
while streaming, an AI message with content and reasoning but no tool
calls yet is deliberately held out of the terminal bubble (#4304) and
rendered by MessageGroup's chain-of-thought panel, which pinned the
trailing reasoning disclosure to the bottom; the settled bubble paints
its <Reasoning> disclosure above the content.

Render the trailing reasoning disclosure before the assistant text that
follows it, and emit a message's reasoning step before its content step
in convertToSteps -- the step list was content-first, so ordering by
step position alone could not fix it. Assistant text emitted before that
reasoning keeps its earlier position.

This also covers two cases the report does not mention: tool-using turns
reversed the same way, and expanding "N more steps" showed a message's
answer above its own thinking.
2026-07-30 13:55:08 +08:00