* fix(chat): warn before "regenerate" permanently deletes later messages
Both "regenerate" entry points — the per-message footer's ↻ "Regenerate
from here" button, and the vision editor's "Regenerate message" button —
call POST /api/session/{id}/truncate, which does a real, permanent SQL
delete of every message after the point clicked, then generates a new
reply. Clicking either on anything but the very last exchange did this with
zero warning: the button's own label doesn't say it also erases the rest
of the conversation, and neither button visually signals it's destructive
differently from any other message-footer action.
Caught live: a mis-click destroyed a real conversation with no way to
recover it (no backup covered the affected window). This adds a
confirmation prompt — via the existing uiModule.styledConfirm — whenever
the action would delete more than just the message(s) being regenerated,
stating the exact count of messages that would be lost. Regenerating the
most recent exchange (the everyday case, nothing exists after it) is
unaffected and still proceeds with zero friction.
New test: tests/test_regenerate_truncate_warning_js.py — drives the real
regenerateFrom/resendUserMessage functions under Node with a fake message
list DOM, confirming: the last-exchange case never prompts and truncates
directly; an earlier case prompts with the correct count; cancelling
aborts before any network call; confirming proceeds with the correct
keep_count. Also fixed a pre-existing, unrelated gap this surfaced while
verifying the full suite: test_pr6020_browser_review_regressions.py's
Node harness never stubbed `navigator` at all (sessions.js reads
navigator.platform at module load) — a gap on origin/dev itself, not
caused by this branch's diff, using the same Object.defineProperty fix
already applied on #6019/#6148 for the sibling issue there (Node 21+'s own
getter-only navigator global, not relevant to this specific failure but
the same defensive pattern).
Verified: full pytest suite in-container, 5798 passed / 11 pre-existing
unrelated failures (README/docs/agent-loop baseline) / 0 new.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
* test(chat): cover plain resend after dev's replace-by-default change
dev made plain resend (no options) replace from the clicked turn by
default, with append-only resend opt-in via { append: true } (see
test_resend_message_nondestructive.py). The footer "Resend message"
button calls resendUserMessage(el) with no options, so it now truncates
everything after the clicked turn and goes through this PR's warning.
Replace the old "plain resend never prompts" test, which assumed the
previous append-by-default behavior, with one asserting that plain
resend on an earlier pair prompts with the right count, and one
asserting that explicit append-only resend never truncates or prompts.
Also resolve ui.js/sessions.js through the same cache-busting query
string chat.js imports them with. ES modules cache by full URL, so the
harness's unversioned import of ui.js was a second module instance and
its uiMod.styledConfirm stub never reached the instance chat.js calls.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(chat): warn before an edit deletes later messages; offer to fork instead
Sending an edited user message truncates the chat at that message, the
same unrecoverable server-side delete as regenerate and resend, but did
it with no prompt. It now goes through the same confirmation, worded for
the action ("Sending this edit will also permanently delete the N
messages after it"). Cancelling leaves the editor open with the draft.
The confirmation for all three actions (regenerate, resend, edit) also
offers "Fork from here": it forks the chat just before the user turn,
switches to the new chat, and replays the action there (the same message,
or the edited text), leaving the original chat untouched. forkFrom now
shares the same fork helper.
Tests drive the real editUserMessage Send handler under Node and cover
the edit prompt (count, cancel, last-message no-prompt) and the fork
choice for each action (fork keep_count, no truncate, replayed text).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(chat): cut and fork a chat by message id, not loaded-page position
Regenerate and edit sent keep_count, the clicked message's position among
the messages loaded in the page. A long chat loads only its newest messages
until the user scrolls up, so that position is smaller than the real one and
the truncate deleted real messages above the clicked one (regenerating the
latest reply in a ~50-message chat removed about a dozen earlier messages).
Resend already sent before_msg_id; regenerate and edit now do the same.
The fork options this PR adds had the same flaw. Fork now accepts
before_msg_id (fork before that message) and through_msg_id (include it),
resolved against the stored messages; keep_count stays as the fallback for a
message that has no id yet.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(chat): keep a regenerated turn's message id current; stop on a refused cut
Regenerate keeps the original user bubble on screen and resubmits its text
without drawing a new bubble. The resubmitted message is a new DB row, but
the bubble kept the deleted row's id, so the next regenerate's cut was
refused (404) and the page sent the turn anyway, stacking a second reply.
The stream now reports the saved user message's id (user_message_saved),
which the page attaches to the user bubble, or to the bubble a regenerate
kept. Regenerate and edit stop with an error when the server refuses the
cut instead of sending.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* test(chat): name the buttons in words instead of symbols
CONTRIBUTING asks for no Unicode emoji in UI or code; the test comments
used the regenerate and pencil glyphs to name buttons.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* refactor(chat): share the fork-and-send step across edit, resend and regenerate
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(chat): pass the regenerate fork its attachment ids explicitly
Resend already hands its collected file ids to _forkAndSend; regenerate
relied on the earlier _pendingRegenAttachments assignment. Pass _regenIds
too so both fork paths read the same, and check the hand-off in the
regenerate tests.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* test(chat): assert fork choices keep the user message's attachments
Run regenerate and resend's "Fork from here" against a user message with
an uploaded file and check that the send fired in the fork carries its
file id, replacing the source-text check. chat.js exposes a read-only
_pendingRegenAttachmentIds() for this, like cookbook-hwfit.js's test hooks.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(chat): preserve attachments across edit and regeneration
---------
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
Co-authored-by: Alexandre Teixeira <general@alexandreteixeira.dev>
* feat: add Notes and Todos as agent chat context
- Send selected Note/Todo IDs from the Notes UI
- Load selected notes with owner scoping in chat routes
- Format note content and checklist items for the model
- Inject selected notes as untrusted chat context
- Add a selected-context indicator beside Shell Access
- Keep selected context associated with the active chat session
* Improved selected notes context handling and discouragin redundant note lookup and permission requests
* Fix selected notes UI when switching chats
* fix(notes): harden selected context lifecycle and validation
---------
Co-authored-by: Alexandre Teixeira <111787685+alteixeira20@users.noreply.github.com>
Snapshot current maintainer-preview application changes and regression fixtures for integration into lab. Excludes local runtime data, evaluation outputs and source backups. Focused Python regression selection: 140 passed; full suite not certified.
* fix(agent): allow remaining actions for an approved task
* fix(agent): make approval continuation control-only
* fix(ci): preserve approval taint and cache-buster contract
* fix(ui): keep tool approvals in current chat
* fix(ui): route tool approvals through chat submit
* test(ui): pin approval submit routing
* fix(agent): complete approval denial flow
* fix(ui): avoid duplicate ask-user close icon
* fix(agent): retain approved tool in continuation set
* revert(ui): keep PR 6113 scoped to approval continuation
* fix(agent): add task and chat approval scopes
* fix(ui): prevent duplicate ask-user close icon
* feat(ui): add ask-user option shortcuts
* fix(compare): route ask-user choices per pane
* fix(agent): keep skill-test approvals to a single action
The chat card now reuses the wire value `approve` to mean chat-session
scope, and `consume()` returned `allow_remaining_actions=True` for it
unconditionally. The skill-test approval route was never updated: it still
sends `approve` meaning "once", and its button still reads "Allow once",
but the grant it got back set `approval_gate_bypassed` for the rest of the
resumed run. That surface wraps the skill body and every transcript byte
as untrusted context, so it is the last place where one click should
ungate everything that follows.
Give `consume()` an explicit `allow_continuation` flag. Callers that own a
resumable chat keep the scope the user picked; callers that do not — the
skill tester, unattended audits — get SINGLE_ACTION and the gate re-arms
behind the sealed action, which is what their label promises.
* fix(ui): cache-bust every module the approval click depends on
chatStream.js, compare/index.js and compare/stream.js all changed
behaviour but kept their old `?v=`, while chat.js and chatRenderer.js were
bumped. A returning browser therefore serves the new chat.js — which now
deliberately leaves the composer empty and clicks the send button — next to
the cached chatStream.js that has no interceptor. With an empty composer
that button sits at `data-mode="newchat"`, so the click opens a new chat
and the approval is dropped.
Bump the three, and version compare/stream.js's chatRenderer import to
match everyone else's so the ask_user keydown listener binds to one module
instance instead of two.
* fix(ui): keep the digit shortcuts off tool approval cards
With an approval card on screen and focus anywhere outside an input, a bare
`1` fired `approve_task` — the widest of the three grants — with no
modifier and no confirmation. That card is the one control whose entire
purpose is deliberate consent after untrusted context influenced the run,
and Deny sits at 3.
Label the card with its kind and skip the shortcut for approvals. Ordinary
ask_user questions keep 1-3.
* fix(compare): restore a pane's ask_user card instead of dropping the choice
renderAskUserCard removes the card as soon as onSubmit accepts, but the
resume loop gave up silently after 10s if the originating stream still owned
the pane. The user saw the click land, the card vanish, and nothing happen,
with no way to get it back.
Re-render the card on that deadline and say why. The reroll case still
returns without sending — that choice belongs to a stream that no longer
exists.
* refactor(chat): drop the unreachable deny branch
`if decision != "deny"` is always true — the deny path returns a
StreamingResponse a few lines above. It reads as if deny still falls
through to the toggle restore.
---------
Co-authored-by: Léo <leograndcontact@gmail.com>
galleryEditor.js and its js/editor/ graph are 54 modules / 576 KB, and
gallery.js imported them statically. Every page load paid for the whole
image editor even though most sessions never open the Edit tab: 54 of the
173 JS files on a cold load, and 576 KB of the decoded JS, were for a
panel that was never displayed.
Add a small panel-loader registry (static/js/panels.js) that imports a
panel's module on first use and memoises the promise, so a double-click
cannot start two loads and a failed load can still be retried. Convert
the image editor to it, and route the two existing dynamic imports in
chat.js and chatRenderer.js through the same entry so all three call
sites share one module instance instead of two.
closeEditor() and isEditorOpen() stay synchronous: if the module was
never loaded there is no edit session to close and none can be open.
The service worker keeps precaching the editor, in a separate
PANEL_PRECACHE list, so the panel stays available offline even though
index.html no longer loads it. The two lists now serve different
purposes and the header comment says so.
* refactor(model-routing): centralize explicit foreground fallback policy
Make foreground fallback an explicit per-user, availability-only policy shared by streaming Chat, non-stream Chat, and Agent runs.
Preserve strict defaults, owner/model and credential boundaries, pinned Agent routes, and truthful per-round provenance/accounting. Carry provider-reported model identifiers through native streaming adapters, non-stream responses, and caches, and keep legacy default_model_fallbacks as tombstoned raw storage that generic settings APIs and agent tools cannot expose or mutate.
* fix(agent-loop): restore rebase-dropped qwen routing, workspace prompt, and temperature clamp
* fix(model-routing): thread selected endpoint identity, fix cost classification and fallback eligibility
* fix(chat): restore stream helpers and harden run stop lifecycle
* fix(model-routing): let numeric provider codes win over symbolic rate-limit statuses
* fix(agent-loop): apply qwen temperature and notes-tool clamps per fallback candidate
* fix(chat): honor queued stop across resend and reload canonical terminal on EOF
* fix(chat): track stop queue and cleanup ownership by per-send generation
* fix(agent-loop): preserve requested temperature for non-qwen fallback candidates
* fix(chat): reserve send ownership before any await and scope stop to the current send
* fix(chat): clear the previous run identity at send reservation
---------
Co-authored-by: RaresKeY <158580472+RaresKeY@users.noreply.github.com>
Co-authored-by: StressTestor <212606152+StressTestor@users.noreply.github.com>
* perf(chat): batch live thinking DOM updates
* test(chat): cover live thinking scheduler lifecycle
* fix(chat): guard background stop-state, restore live thinking text, drop source-text tests
- _closeOpenThinkingMarkup no longer overwrites currentAccumulated for
backgrounded streams. It now mirrors the guard the delta path already uses
(`if (!_isBg) currentAccumulated = accumulated`). Without it a backgrounded
stream's text is written into the foreground session's stop-state, which
abortCurrentRequest and detachCurrentStream then put in the wrong bubble.
- Split _extractLiveThinkingText into _liveThinkingText (strip every think tag)
and _closedThinkingText (via extractThinkingBlocks). Slicing from the first
<think> to the first </think> pinned the live box to "The" for the rest of the
stream on the `<think>The</think>` + untagged-thinking pattern that the
hasUnclosedThink detection deliberately keeps streaming through.
- The background transition now flushes with rich:true, so a stream that
backgrounds mid-thinking isn't left as pre-wrap plain text permanently.
- Move the throttle to static/js/liveThinkingThrottle.js and import it. The
.mjs suite imports the module instead of slicing it out of chat.js with
vm.runInNewContext and marker comments.
- Replace the source-text assertions in tests/test_live_thinking_scheduler_js.py
with behavioral coverage, per tests/TESTING_STANDARD.md. The .mjs suite grows
from 3 to 6 cases.
- Collapse the duplicated tool_start/agent_step finalizers into one
_endLiveThinkingSection().
* fix(chat): hoist thinking teardown out of the try block so catch can reach it
In an ES module a function declared inside `try { }` is scoped to that block,
and `catch` is a sibling scope rather than a nested one. _closeOpenThinkingMarkup
was declared inside the try and called from catch, so the call threw
ReferenceError and killed the rest of the error path: the stream never
finalized and the thinking block was never torn down.
Declare _closeOpenThinkingMarkup and a new _endThinkingOnTerminalPath next to
the existing _flushLiveThinking / _cancelLiveThinkingWork outer lets and assign
them inside the try, which is the pattern those two already use for exactly
this reason.
Verified against a live stream in a browser: before, clicking stop mid-thinking
logged "_closeOpenThinkingMarkup is not defined" and left no finalized thinking
section; after, the block collapses to "View thinking process" correctly.
* perf(chat): extract live thinking at commit cadence
* fix(chat): bound live thinking work
* test(chat): update stream invariant assertions
---------
Co-authored-by: Léo <leograndcontact@gmail.com>
* Fix: restore session URL hash writes (removed in cf4e240a)
Restores history.replaceState() calls in selectSession() and
materializePendingSession() that were dropped during the July 23 merge.
Without these, chat URLs never update the address bar hash, making
sessions unshareable and causing bare-URL reloads to land on the
welcome screen instead of restoring the last active chat.
Root cause: selectSession() had its hash-write deliberately removed;
materializePendingSession() lost its during a larger refactor that
added the stale-response and incognito guards.
Fixes#5870 (upstream)
* fix: session URL hash lost when sending message mid-stream
Two independent bugs caused the session hash to disappear from the URL:
Bug 1 — ReferenceError in catch block silently killed error recovery
In handleChatSubmit, two const variables (streamingTTS at line 1922 and
abortCtrl at line 1741) were declared inside the try block but referenced
in the catch block. Since const is block-scoped in JavaScript, they were
undefined in catch, causing a ReferenceError that silently aborted the
error handler. This prevented materializePendingSession() from ever being
called, so no hash was written to the URL.
Fix: Hoisted both as let declarations before the try { block.
Bug 2 — Dual sessions.js ES module instances with mismatched state
app.js imported sessions.js with a version query string
(?v=20260722ctxheader4) while every other module imported ./sessions.js
without one. The browser treated them as different URLs, creating two
separate module instances with independent _pendingChat and
currentSessionId state. createDirectChat() set pending on one instance
while handleChatSubmit() checked hasPendingChat() on the other — so the
pending session never materialized.
Fix: Removed the version query string from the sessions.js import in
app.js and from the modulepreload + script tags in index.html. All
modules now share a single sessions.js instance.
Bonus guard: _adoptOpenedSessionBeforeAutoCreate() now checks
hasPendingChat() before adopting a stale DOM-active session, preventing
the send path from landing in the wrong session when a New Chat is pending.
---------
Co-authored-by: samy <samy@users.noreply.github.com>