Commit Graph
229 Commits
Author SHA1 Message Date
wushenghua 587705dedb fix(web): bound loaded generated-media memory with a small LRU
Loaded video/audio/file blobs stayed in memory for the whole session
(object URLs were only revoked on unmount), so opening several tens-of-MB
videos in one chat grew memory without bound. Explicitly loaded media now
goes through a shared cache that keeps at most three entries; the least
recently used object URL is revoked on overflow and its card falls back to
the Load button (a sticky eviction flag prevents a slow load from
re-publishing an already-revoked URL). Auto-loaded images keep their
existing per-card lifetime.
2026-10-01 00:13:38 +08:00
wushenghua 8e61772bb4 feat(hub,web): serve chat image originals as cached attachments
Pasted images were stored inline in the message JSON as full-size base64
data URLs. Every conversation load (open, paginate, mobile reconnect) had
to download and parse those megabytes before anything rendered, with no
lazy loading and no per-image caching. Measured on this hub: 115 messages
holding 138MB of base64, the largest single message 18.3MB.

New messages now carry only a small thumbnail (browser-generated, 768px
webp/jpeg data URL); the original is uploaded as before and the hub keeps
a durable copy under ~/.hapi/attachments (table chat_attachments, schema
v27). The chat renders the thumbnail instantly and lazily swaps in the
original from GET /api/sessions/:id/attachments/:id when the viewer opens;
that endpoint answers sha256 ETag + immutable Cache-Control so repeated
opens are free. Sessions scope the URL, deletes remove the copy, and
hub/scripts/attachments.ts covers stats/verify/gc/export.

Old messages keep their inline previews (migration deliberately deferred).

Verified end-to-end against the local hub: upload returns the attachment
id/url, original 200 with ETag, revalidation 304, cross-session 404, delete
removes file+row (404 after). Typecheck clean; suite status matches the
pre-change baseline; fixtures unchanged.
2026-09-30 00:58:09 +08:00
wushenghua e998f05eb2 merge: hapi v0.30.7
Six text conflicts, all resolved:
- cli/apiMachine + runner/run: adopt upstream's MachinePathPolicy; keep the
  local policy that browsing stays disabled until --workspace-root is set
  (banner text + upstream's browse test adapted to assert the local gate).
- cli/codex root.test.ts: keep both the syncHistory split test and upstream's
  change_title/name test; fixture return merges metadata + syncHistory.
- web/SessionList: combine the local personal/global pinned split with
  upstream's search-relevance ranking (deps union).
- web/AskUserQuestionFooter: take upstream's draft-restore effect and thread
  the local skippedByQuestion through the draft store; the local DSH skip test
  moves to AskUserQuestionFooter.skip.test.tsx so upstream's vi.mock setup in
  the original file cannot shadow it.

Also: bun install for the assistant-ui 0.15.21 bump, regenerated
shared/fixtures from the merged pipeline.

Test status vs the pre-merge baseline (d6cf54d7), identical pre-existing
failures only: cli 7, hub 2 (opencodeClear), web 35, shared 2 (DSH); relay
clean; typecheck clean across all packages.
2026-09-29 21:14:39 +08:00
weishu 5280a3b165 fix(chat): dismiss Codex plan actions when continuing planning 2026-09-14 14:30:08 +08:00
AnanovoandGitHub a815230886 fix(web): support bracket-delimited LaTeX in Markdown and share exports (#1846)
* fix(web): support bracket-delimited LaTeX in Markdown and share exports

* fix(web): preserve Markdown containers around LaTeX

* fix(web): handle list fences and math comparisons

* fix(web): cover Markdown metadata edge cases

* fix(web): protect Markdown metadata around LaTeX

* fix(web): preserve reference metadata around LaTeX

* fix(web): preserve quoted formulas after prose

* fix(web): normalize nested Markdown containers

* fix(web): preserve existing dollar math

* fix(web): stop LaTeX matching at protected ranges
2026-09-14 14:24:33 +08:00
wushenghua cb0dbd7d22 chore: rebase local deployment features onto hapi v0.30.3 2026-09-13 15:55:04 +08:00
weishu 917baf1201 fix(codex): sync shared plan proposals and web actions 2026-09-13 12:09:39 +08:00
092a2259c8 fix(web): force-dismiss indeterminate queued rows on cancel/edit (#1840)
* fix(web): force-dismiss indeterminate queued rows on cancel/edit

When cancel returns busy for an already-indeterminate QueuedMessagesBar
row, keep the optimistic removal instead of re-sticking the row so Edit/X
cannot leave the UI dead. Upgrade busy to invoked when queued-state
reconcile shows the steer already landed, so Edit toasts instead of
prefilling a duplicate.

Fixes #1839

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(web): never prefill Edit when queued cancel returns busy

Indeterminate rows can still be a live dispatch. Prefilling on busy
risked a duplicate send if the original steer later landed. Keep
force-dismiss for Cancel; toast on Edit+busy without restoring the draft.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(web): keep force-dismissed indeterminate rows for late consume

Hide abandoned outcome-unknown rows from the queued bar via
queueDismissed so Cancel/Edit still clear the stuck chip, while leaving
the message in the window for a later messages-consumed SSE.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(web): close queueDismissed races on cancel busy and requeue

Restore the hidden hold before getQueuedState so concurrent consume
SSE can land, skip overwriting it in onSuccess, and clear dismissal
when markMessagesRequeued confirms FIFO again.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(web): undismiss force-held row when cancel busy finds FIFO

If getQueuedState reports the localId is queued again after a failed
steer, clear queueDismissed so Edit/Cancel return without prefilling.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(web): preserve queueDismissed across refresh and merges

Force-dismiss of indeterminate rows survived Cancel, but latest-page
refetch and same-ID merges dropped the client-only flag. Carry it while
the authoritative row remains uninvoked and indeterminate.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(web): clear queueDismissed on reconnect FIFO reconcile

Reconnect queued-state reconciliation now markMessagesRequeued for
confirmed queuedLocalIds so force-dismissed rows regain Edit/Cancel
after a missed messages-requeued SSE.

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-12 21:02:32 +01:00
weishu d18c01b4b6 revert(codex): remove Luna Reserve fallback (#1780)
Revert 8357da0a9d and its later shared-runtime integration.

Remove automatic model fallback, account usage polling, and the related UI, protocol fields, tests, and documentation without adding replacement quota handling.

Keep generic model pagination and method probing required by the current shared-session architecture. Cover idle sessions staying online without usage polling or agent-state churn.
2026-09-12 14:36:45 +08:00
weishu 0c4abcb3d1 feat(codex): share sessions across terminal and web
Use one native app-server for terminal, Web and phone clients while retaining the existing CLI and Runner lifecycle.

Synchronize native queues, permissions, question history and steering state; preserve explicit permission precedence and per-turn usage models. Resume inactive clear commands through Runner and reject independent child cold resumes.

Add shared-runtime regression tests, generated protocol fixtures and lifecycle documentation.
2026-09-12 10:59:17 +08:00
AnanovoandGitHub 2eb11a5d2c fix(web): clarify round usage metadata labels (#1805)
* fix(web): clarify round usage metadata labels

* test(web): cover duration boundary rounding
2026-09-11 14:20:38 +08:00
Junmo KimandGitHub fd2822bab5 fix(agy): allow switching existing sessions to newly available models (#1814)
* fix(agy): say what the model picker is actually waiting on

The spinner in the New Session AGY picker read "Checking Antigravity
authentication…", but nothing at that point checks authentication — the
machine is running `agy models`, and the sign-in prompt is a separate
branch below it, shown only when agy reports the failure.

Name the wait after the work: "Fetching available models…", the same
words agy prints while it fetches.

* refactor(agy): describe a probe by its outcome, not by its response

The probe function returned a finished `AgyModelsResponse`, so "agy could
not be reached" and "agy listed no models" both arrived as a successful
response carrying the hardcoded mirror, and the caller could no longer
tell which had happened. Every policy decision about that answer has to
live inside the probe as a result.

Hand back what the probe observed — a live catalog, an auth failure, or
nothing usable — and let the caller turn it into a response. Same
behaviour: the mirror still stands in for both failure modes, and the
60s cache still holds whatever came out.

* fix(agy): serve the model catalog stale-while-revalidate

The `agy models` probe is a whole agy invocation — around 3s on a good
day, 15s when it times out — and the 60s window meant the New Session
picker paid that again a minute after the last look.

Keep the last listing agy actually returned and answer from it: fresh for
ten minutes, then still answered while a probe refreshes behind it, until
the entry is a day old and stops standing in for the machine at all. A
probe that times out or loses auth leaves that entry alone, so a blip no
longer empties a working picker, and the hardcoded mirror is no longer
recorded as if the machine had reported it.

Three things fall out of that and are handled here. A machine whose
sign-in has actually gone bad would otherwise look healthy for a day, so
an auth failure rides along with the catalog it can still serve — and,
because nothing else would re-probe a catalog that is still fresh, a
warning riding on the answer is itself a reason to look again. A failed
probe is not repeated on the very next request either, or a machine where
agy hangs would spawn it once per poll.

An explicit refresh always costs a probe, and never rides one that was
already running when it was asked for.

* fix(agy): let Retry force a fresh model catalog probe

With the catalog held for ten minutes, Retry would otherwise hand back the
answer it was pressed to replace, so the intent travels to the machine:
`?refresh=true` on the machine route, an optional RPC param, and a
one-shot flag on the query so ordinary mount and focus refetches stay
cheap. Every hop is optional, so a hub and a runner on different versions
still talk — the older side ignores it and answers from its cache.

Retry also has to be reachable, and honest, in the state that needs it.
The machine now answers with both a usable catalog and the sign-in failure
behind it, so the picker keeps the list and says why it may be out of
date, with the button right there rather than only once there is nothing
left to show. Pressing it runs agy, which can take tens of seconds, so the
button says so while it does.

The client contract covers both: `agy-models` is the one catalog route
that can carry an `error` on a successful response, and the one that
takes a refresh parameter.

* fix(agy): use the machine catalog in the in-session model picker

New Session already asks the machine what `agy models` lists, but a
session that is already open offered the built-in list in
`shared/src/models.ts`. That list is a hand-maintained mirror, so a model
agy started offering after the last release could be picked for a new
session and not for the one already running.

Point the composer at the same machine catalog. The mirror stays as the
fallback for the moment before the machine answers, and a model the
session is already on is kept selectable — and readable, when it is one
of the known presets — even after the catalog moves on without it.

* fix(agy): announce a model catalog re-check that changed the answer

Serving the last known catalog answers the picker instantly, but a picker
that was already open kept showing that answer until the user closed and
reopened it — the machine had no way to say it had found something newer.

Say it on the stream that already carries machine changes. The machine
daemon — the only process that answers `<machineId>:listAgyModels` —
emits it, and the hub forwards it as `machine-agy-models-updated` with
nothing but the machineId. Namespace resolution, per-machine delivery and
reconnect replay all come from the existing path.

What counts as a change is what the route would answer, not what sits in
the cache. That distinction carries the cases: a sign-in that lapsed or
came back changes no models yet changes what the user is told; a machine
whose agy was signed out has been answering from the hardcoded mirror, and
its first real listing is the largest change there is, for every client
except the one awaiting it.

* fix(agy): re-read the announced machine's model catalog

On `machine-agy-models-updated`, cancel and refetch that one machine's
catalog query — the app's global connection is always subscribed, so an
open picker redraws wherever it is.

Cancelling first is what makes it correct rather than merely likely.
query-core cancels an in-flight fetch only when the query already holds
data, so a picker opening for the first time would otherwise join the
request already on its way and settle on the listing the announcement
replaced. The refetch is answered from the machine's cache, so it starts
no probe and cannot bounce another announcement back.

A reconnect the hub could not replay takes the resync path, which clears
the agy catalogs the same way — that path has no announcement to fall back
on, so it is the one that can least afford to join a stale request.

* fix(agy): keep a model the user picked when the catalog moves under them

The catalog can now change while the New Session form is open, and the
form dropped any selection the machine no longer advertised — including
one the user had just made.

Keep that one, and list it as no longer listed so the form does not imply
agy is still offering it. A model restored from a draft or a saved
preference is still dropped: it may never have been runnable here.

* fix(agy): announce uncached authentication changes
2026-09-11 14:19:37 +08:00
AnanovoandGitHub 2b402d24d9 fix(web): show Codex round usage metadata (#1685)
* fix(web): show Codex round usage metadata

* fix(web): skip imported Codex round duration
2026-09-09 09:32:43 +08:00
AnanovoandGitHub ceb9314350 feat(web): add scroll-to-bottom button (#1694)
* feat(web): add scroll-to-bottom button

* fix(web): type counted scroll button props

* fix(web): retarget smooth tail scroll

* chore: retrigger pull request checks
2026-09-09 09:32:09 +08:00
SSU-WEI HUANGandGitHub 8357da0a9d feat(codex): support backend-authorized Luna Reserve fallback (#1780)
* feat(codex): reconcile Luna Reserve fallback and conditional usage

* fix(codex): preserve queued settings and reconcile Reserve sessions
2026-09-09 09:27:40 +08:00
AnanovoandGitHub a9d09cb3ff feat(web): support composer attachment drag reordering (#1682)
* feat(web): support composer attachment drag reordering

* fix(web): handle forward attachment reordering

* fix(web): enlarge file attachment touch targets

* fix(web): preserve staged attachment order
2026-08-24 14:27:10 +08:00
Junmo KimandGitHub 661e9b4eb7 feat(web): show Claude round usage metadata (#1655) 2026-08-22 12:37:03 +08:00
KorenKritaandGitHub e3f13c4eb6 feat(web): anchor composer settings sheet to the clicked value button's section (#1627) 2026-08-20 18:19:15 +08:00
SSU-WEI HUANGandGitHub 9bdccf0cff fix(web): unify message action buttons (#1645) 2026-08-20 08:54:20 +08:00
SSU-WEI HUANGandGitHub f0e5ba9c0f feat(codex): mid-turn Steer via app-server turn/steer (#888) (#1606)
* feat(shared): steer capability gates and live steered signal schemas

- STEERING_SUPPORTED_FLAVORS / isSteeringSupportedForSession gate which
  agents can deliver queued messages into the active turn (pi, codex,
  cursor ACP; legacy stream-json cursor excluded)
- AgentState.steeringActive, DecryptedMessage.steered and
  messages-consumed  live signal (never persisted by the hub)

* feat(cli): queue reservations and steered messages-consumed option

- MessageQueue2 gains takeByLocalId/restoreReservation/
  beginReservationDispatch/commitReservation so an async steer can reserve
  a queued row without racing the main loop's turn/start drain
- emitMessagesConsumed accepts steered: true to mark mid-turn delivery

* feat(codex): mid-turn steer via app-server turn/steer (#888)

- CodexAppServerClient.steerTurn + TurnSteerParams/Response types
- CodexRemoteLauncher registers the steer-queued-message RPC handler:
  reserves the queued row, validates it against the active turn (no
  control commands, matching mode hash), injects via turn/steer with an
  epoch guard that invalidates in-flight steers on abort/cleanup
- steeringActive agent state tracks the active-turn window
- hub syncEngine gate opens to codex; messages-consumed relays steered

* feat(web): Steered badge and steer gating for codex sessions

- HappyUserMessage shows a ↳ Steered badge fed by the live
  messages-consumed steered signal, preserved across server echoes and
  refetches (mergeMessages carries the optimistic marker)
- SessionChat gates canSteer via isSteeringSupportedForSession instead of
  the pi-only check
- clearStaleQueuedStatus normalizes a queued status on an invoked message
- fix(web): drop duplicate showSessionSummaryInChat in markdown test
  (upstream typecheck breakage)

* fix(codex,shared): address bot findings on steer gate and ambiguous turn/steer

- STEERING_SUPPORTED_FLAVORS / isSteeringSupportedForSession advertise
  codex and pi only; cursor joins when its soft-steer handler lands (#1609)
- turn/steer now splits dispatch (stdin accepted) from completion (turn
  finished): the hub RPC acks once dispatch succeeds — never on the
  concurrent turn's completion, which can exceed the 30s RPC window
- queue row commits only after the turn settles; a rejected/aborted steer
  restores the row so the message still delivers via turn/start, and a
  dispatched steer is never restored (no duplicate delivery)
- steer carries clientUserMessageId (echoed as userMessage.clientId) so
  ambiguous transport failures can reconcile the thread later
- client tests cover dispatch/complete split and stdin-write failure

* fix(codex): reconcile dispatched steers before restoring; align error copy

- A dispatched turn/steer whose completion fails (disconnect / protocol
  error) is now reconciled via thread/read by clientUserMessageId before
  the queued row is restored — the instruction is only re-delivered by
  turn/start when the thread never received it
- Reconcile targets the pinned steer thread, not whichever turn is
  current when completion fails
- syncEngine unsupported-flavor error now matches the capability gate
  (Pi and Codex only until the cursor handler lands)
- launcher tests cover steer success (ack on dispatch), reconcile-accepted
  and reconcile-rejected outcomes

* fix(codex): consume the row at dispatch; drop background reconcile

- The hub RPC acks and the queue row is consumed as soon as stdin accepts
  turn/steer; completion is background-only logging. A dispatched steer is
  never restored, so the same localId cannot be re-delivered via turn/start
  after the caller was told the steer succeeded
- Dispatch failure (stdin write error) still restores the row and reports
  failure
- steer.completed rejection is always handled (no unhandled rejection on
  the dispatch-failure path)
- tests updated: completion failure after dispatch keeps the row consumed;
  dispatch failure restores it

* fix(codex): distinguish definite rejection from indeterminate completion

- Transport-level failures (timeout, abort, disconnect, spawn, protocol)
  carry an indeterminate marker; explicit JSON-RPC error responses do not
- After a dispatched steer, turn completion resolves → commit + consumed;
  a definite app-server rejection restores the row (instruction was never
  accepted, so turn/start cannot duplicate it); an indeterminate outcome
  leaves the row reserved so it can never be delivered twice
- Completion handling registers before awaiting dispatch so the
  dispatch-failure path cannot leak an unhandled rejection
- client/launcher tests cover explicit rejection (restore), indeterminate
  outcome (row stays reserved) and dispatch failure

* fix(codex): reconcile indeterminate steers instead of a permanent reservation

- After an indeterminate completion (disconnect/protocol), reconcile the
  thread by clientUserMessageId immediately: accepted → commit + consumed,
  provably rejected → restore, still unreadable → keep the reservation and
  retry from the main-loop top on later passes (post-reconnect)
- A row never sits in dispatching forever: the hub cannot stamp it invoked
  while the instruction may never have been accepted
- tests: indeterminate keeps reserved while thread unreadable; accepted
  reconciliation consumes; rejected path restores

* fix(codex): accept all thread item shapes; retry reconcile; ack through abort

- Reconcile matcher accepts userMessage/user_message with clientId/
  client_id, matching the shapes the thread parser supports — an accepted
  steer can no longer be misclassified as rejected
- A pending reconciliation schedules a wakeLoop retry, so a temporary
  app-server outage cannot strand the reservation behind waitForTurnOrRecovery
- The success-path ACK no longer checks the steer epoch: the hub already
  reported steered on dispatch, so commit + messages-consumed must reach
  it even when an abort resets the queue in between

* fix(codex): reinit reconnected app-server; keep reconcile retries alive

- thread/read after a disconnect auto-connects a fresh app-server, which
  must be initialized before any request — reconcile now ensures
  connect + initialize (isConnected getter added to the client)
- every still-unknown loop-top reconciliation schedules the next retry,
  so recovery without external traffic is eventually observed
- launcher mock gains isConnected

* fix(codex): timer-driven reconciliation; init tracking; abort-safe ACK

- Reconciliation runs on a self-rescheduling 1s timer independent of the
  main loop (wakes it too), so idle loops and waitForTurnOrRecovery still
  observe app-server recovery; abort clears nothing implicitly — the ACK
  path commits and consumes even when the reservation was cancelled
- Absence of a durable client id is ambiguous: unmatched reads stay
  'unknown' and keep retrying instead of restoring the row
- CodexAppServerClient tracks initialized state (reset on disconnect/exit)
  so ensureAppServerInitialized re-initializes a fresh process before
  thread/read; initialize failures leave the flag false for the next retry
- tests: accepted reconciliation via scheduled timer, indeterminate
  keeps reserved, explicit rejection restores

* fix(codex): bind reconciliation to the launcher lifecycle

- runSteerReconciliation clears any armed retry timer on entry and never
  installs a second one, so loop-top and timer-driven passes cannot
  multiply
- shuttingDown is set when the main loop ends: timers are cleared and the
  pending map is dropped, so an unresolved steer can never respawn an
  app-server after cleanup (remote-to-local switch included)

* fix(codex): report steered only after app-server acceptance

- The handler now awaits steer.completed (the inject-acceptance response):
  an explicit JSON-RPC rejection surfaces as failed and restores the row
  for the normal turn/start path instead of a false steered
- Transport failure after dispatch reports 'Steer outcome is being
  reconciled' and keeps the row reserved while the timer-driven thread
  reconciliation runs
- dispatch-failure path also swallows the paired completion rejection

* fix(steer): tri-state cancel, clear-safe reservations, bounded acceptance wait

- MessageQueue2.cancelByLocalId returns 'in-flight' for a dispatching
  steer reservation: the hub neither deletes the row nor stamps invoked_at
  (new CancelMessageResponse 'busy' status; web restores the optimistic
  row); pushIsolateAndClear and reset/close share cancelReservations so
  /clear-style commands cannot have a rejected steer resurrect a discarded
  prompt
- turn/steer acceptance wait bounded at 25s (< hub 30s RPC timeout): a
  lost response is indeterminate and funnels into thread reconciliation
  instead of stranding the reservation
- tests updated for the tri-state cancel contract

* fix(codex,web): busy-aware edit flow; bound reconciliation reads

- QueuedMessagesBar edit flow treats a 'busy' cancel as unsuccessful: it
  never prefills the composer when the row is inside an async steer, so a
  second client cannot send a duplicate
- reconcileSteerByClientId bounds thread/read with a 5s timeout so a
  connected-but-silent app-server cannot hold the reservation in-flight
  indefinitely

* fix(steer): inFlight-dominated cancel acks; bounded reconciliation

- hub cancel-queued-message acks check inFlight before removed: a stale
  duplicate socket reporting removed can no longer delete the durable row
  while another socket is dispatching the steer
- reconciliation entries expire after 60s and mark delivered: after the
  rejection window, a dispatched steer that the app-server never proved
  (client ids dropped on restart) is committed instead of polling
  thread/read forever
- pre-dispatch failures (abort before write included) never enter
  reconciliation — they restore the row and report failure

* fix(steer): persist indeterminate outcomes without replay

* fix(steer): make ambiguous delivery restart-safe

* fix(steer): recover crash-held rows and preserve retry dedup

* fix(steer): ack retries and bound stdin dispatch

* fix(steer): reconcile indeterminate dispatches and serialize retries

* fix(codex): classify stdin callback failures as indeterminate

* fix(steer): recheck indeterminate cancels after ACK

* fix(steer): close retry and abort races

* fix(steer): serialize live retries and abort admission

* fix(steer): distinguish live dispatching from unknown

* fix(steer): keep ACK failures held and reconcile busy cancel

* fix(steer): distinguish held cancel from removal

* fix(store): combine schema v24 migrations

* fix(store): reserve schema v25 for steer delivery state

* fix(steer): keep held cancel state and notify requeue

* fix(steer): release explicitly cancelled unknown reservations

* fix(codex): reject cancelled reservations before native steer

* fix(codex): make reservation restore atomic with state

* fix(codex): terminate abandoned transport writes

* fix(steer): own abandoned app-server lifecycle and consume races

* fix(codex): confirm dispatch and recover abandoned turns

* test(codex): mock abandoned transport callback

* fix(codex): clear visible turn state on transport loss

* fix(steer): claim retries and cover native delivery state

* fix(native): preserve indeterminate state on Android hydration

* fix(steer): make retry claims single-winner

* fix(steer): serialize concurrent retry claims

* fix(socket): tolerate missing steer-state ACK callbacks

* fix(native): serialize retry operations

* docs(web): document unknown steer delivery and retry controls

* fix(steer): handle retry failures and abort-before-connect

* fix(steer): reinitialize after transport loss and finish iOS retry errors

* fix(steer): preserve indeterminate rows across reconnect gaps

* test(web): mock indeterminate queued recovery state

* fix(steer): recover consumed ACK tombstones

* fix(steer): expose consumed cancel tombstones
2026-08-19 20:07:39 +08:00
SSU-WEI HUANGandGitHub 79ffe9aa0e feat(web): composer model/effort value buttons and first-class permission (part of #1438) (#1475)
* feat(web): composer model/effort value buttons and settings order

Wide composers now show [model] and [effort] value buttons for non-Pi
flavors (labels from the current session values), opening the settings
sheet on click. Narrow viewports collapse to the settings button only
via a new useNarrowViewport hook. The settings sheet reorders to
Model -> Effort -> Permission -> other settings (Fast mode,
collaboration, Copilot agent mode) so permission is first-class.

Toolbar customization gains 'model'/'effort' items with settings labels.
Pi keeps its dedicated model/thinking panels unchanged (unified
descriptor-driven sheet is a follow-up).

* fix(web): satisfy strict types in composer value-button test harness

* fix(web): address review findings on composer model/effort value buttons

- Normalize null/'auto'/'default' model wire values onto the value:null
  option so default-model sessions keep a localized label button (Major)
- Exempt model/effort value buttons from the settings outside-click
  dismissal so a second click closes the sheet instead of reopening it
- Read matchMedia synchronously in useNarrowViewport so narrow first
  paints never flash the wide toolbar
- Add regression tests: model=null/'auto' labels, toggle-close behavior,
  and initial narrow-viewport render

* feat(web): fold Pi into the generic composer model/effort value buttons

Pi sessions previously exposed model/effort twice: dedicated 'Pi model' /
'Pi thinking level' toolbar buttons (PiModelPanel/PiThinkingLevelPanel)
AND the settings sheet's generic Model/Effort sections. Consolidate so Pi
looks exactly like every other flavor:

- Pi now uses the generic model/effort value buttons; labels resolve from
  the provider-qualified piModels catalog (name -> modelId -> session id).
- The settings sheet's Model section already renders provider-grouped Pi
  rows and Effort renders Pi thinking levels, so the dedicated panels and
  their toolbar slots are deleted.
- Keep Pi's mid-turn control affordance (#1442): Pi turns hold
  thread.isDisabled for minutes, so Pi model/effort controls stay enabled
  while a turn is running (configurationControlsDisabled instead of
  controlsDisabled), including the sheet rows.
- Drop 'piModel'/'piThinking' toolbar layout items; persisted layouts
  normalize them away automatically.

Tests: pi value-button label/sheet tests, mid-turn model selection via the
unified sheet, toolbar layout defaults.

* fix(web): keep a session-settings trigger on narrow viewports and mid-turn Pi

Address the HAPI review bot's two Minor findings on the Pi consolidation:

- Narrow viewports collapse the model/effort value buttons into the
  settings sheet, so a persisted toolbar layout hiding the gear left no
  session-settings trigger at all. ComposerButtons now forces the gear
  back into the rendered layout on narrow viewports (wide layouts keep
  honoring the user's hidden choice).
- The Pi mid-turn live-control rule only reached the value buttons and
  sheet rows; with those buttons gone on narrow, the gear was still
  disabled by controlsDisabled for the whole (minutes-long) Pi turn.
  HappyComposer now passes settingsDisabled={modelEffortControlsDisabled}
  so the gear stays clickable mid-turn for Pi exactly like the buttons.

Tests: narrow + hidden-gear layout keeps Settings; narrow Pi mid-turn
gear stays enabled and opens the provider-grouped sheet.

* fix(web): address HAPI Bot Pi settings-sheet findings

Three Minor findings from the review bot on the unified Pi sheet:

- Provider-qualified selection: rows compared only modelId, so duplicate
  model IDs across providers all looked selected. Compare against
  piSelectedModel's provider+modelId when available.
- Thinking-level reset: the removed Pi panel toggled the current level
  back to null; the unified effort rows only submitted concrete values.
  Re-clicking the selected effort row now clears it for Pi.
- Memo staleness: the settings-sheet memo did not depend on
  modelEffortControlsDisabled, so a Pi disabled-state transition while
  the sheet was open left rows enabled from the prior render.

Tests: colliding model IDs highlight only the matching provider row,
re-clicking the selected effort row sends null, and a rerender with
active=false disables the open sheet's rows.

* fix(web): include piSelectedModel in settings-sheet memo deps

The overlays memo reads piSelectedModel for provider-qualified row
highlighting but only declared the derived selectedPiModel. When
piSelectedModel hydrates from absent to a qualifier that resolves to
the same catalog object, the memo is reused and duplicate model IDs
stay highlighted across providers. Add the raw prop to the dep array.

* fix(web): gate Pi model rows on catalog; reset drill-down via value button

Address the HAPI Bot review on the unified composer settings sheet:

- Pi no longer falls back to the generic synthesized modelOptions rows
  when its provider catalog is empty/loading. Selecting one of those
  would post a bare model id that runPi cannot resolve to a provider
  (first cached match or 409). The Model section now only renders for
  Pi when piModelGroups exists, and renders grouped rows exclusively.
- Closing the sheet through the model/effort value button now goes
  through handleSettingsToggle, so a Cursor variant drill-down resets
  to the base model list on reopen (previously only the gear and
  outside-click paths cleared it).

Tests: empty Pi catalog hides Model section; value-button close resets
Cursor drill-down.

* fix(web): hide Pi effort controls until the selected model resolves

Address the HAPI Bot review: with the catalog still loading/failed there
is no selectedPiModel to derive a capability map from, but the unified
effort control stayed enabled (mid-turn Pi controls are intentionally
live). Selecting a level would send set_thinking_level for a model that
may not support reasoning, and the RPC can be rejected after the sheet
closed. The old dedicated panel guarded this state.

showEffortSettings now requires a resolved, reasoning-capable Pi model;
the effort value button hides the same way. With an empty catalog Pi
exposes no settings trigger at all, matching the old control states.

Tests: unresolved Pi catalog mid-turn exposes no effort action.

* fix(web): hide the Pi model trigger until the catalog resolves

Address the HAPI Bot Minor finding: with an empty/loading Pi catalog the
model value button fell back to the bare session model id and rendered
an enabled trigger that opens no Model section. Show the button only
once the provider-qualified catalog entry resolves.
2026-08-16 22:43:10 +08:00
AnanovoandGitHub 7909c46fff feat(search): support wildcard patterns across search fields (#1571)
* feat(search): add wildcard matching to search fields

* fix(search): harden wildcard matching and file globs

* fix(search): align file matching with shared wildcard semantics

* fix(search): bound file wildcard search in runner

* fix(search): normalize outline queries through shared matcher

* fix(web): remove duplicate markdown test context field
2026-08-16 22:41:24 +08:00
AnanovoandGitHub 8dc4a50fee fix(web): prevent replaying historical assistant output (#1572)
* fix(web): prevent replaying historical assistant output

* fix(web): handle history pagination during typing handoff

* fix(web): preserve streaming handoff across history windows

* fix(web): distinguish hydrated active runs from new output

* fix(web): preserve handoff across tail hydration

* fix(web): preserve first output in user-only turns
2026-08-16 22:41:08 +08:00
901f17d0ca feat(pi): support Pi slash commands from HAPI web (compact/session/model/help) (#1570)
* feat(pi): support Pi slash commands from HAPI web (compact/session/model/help)

Pi runs as 'pi --mode rpc' over piped stdio, so TUI slash commands typed in
web chat previously fell through to the LLM as plain text and silently did
nothing (notably /compact).

- shared: add Pi builtin slash command list (help/compact/session/model) so
  the web / menu exposes them; web test updated to match
- cli: intercept Pi builtin commands in runPi's user-message path
  * /compact [instructions] -> Pi compact RPC (120s timeout, works while
    streaming; summary + token delta reported back as chat messages)
  * /session -> get_session_stats formatted stats
  * /model [modelId] -> list/switch via set_model
  * /help -> supported-commands list
  * other Pi TUI builtins (/tree, /export, /reload, ...) -> explicit
    terminal-only notice instead of silent LLM pass-through
  * unknown slash text still passes through (extension commands, skills,
    templates keep working)
- gate the prompt pump with piCompactInFlight so queued prompts are not
  rejected by Pi mid-compaction; buffer commands until ready like prompts
- ListSlashCommands RPC merges HAPI builtins with Pi extension commands
- tests: parser unit tests + runPi integration tests (compact execution,
  streaming steer interception, failure reporting, FIFO blocking, model
  switch, unsupported commands, slash list merge)
- docs: document Pi slash command support in docs/guide/agents.md

* fix(pi): address review findings on slash command lifecycle

- compact timeout: fail the session (indeterminate outcome, runtime lease
  poisoned) instead of reopening the prompt FIFO into a possibly-compacting
  Pi; pump only when cleanup has not been initiated
- special commands: release the cancellation reservation before executing so
  a cancel landing mid-command is not acknowledged (hub would delete the
  queued row while the command still runs)
- tests: drop the duplicated slash-command describe block; add focused tests
  for compaction timeout with a queued prompt and cancellation during an
  in-flight special command

* fix(pi): route slash commands through the prompt FIFO and reject ambiguous models

- slash commands now share the prompt FIFO with ordinary messages: a
  /compact or /model typed after a queued prompt dispatches only after it
  (and after the active turn settles), instead of jumping the queue from
  the preparation chain
- the pump dispatches special entries out-of-band while piSpecialCommandInFlight
  keeps the FIFO blocked; steer promotion refuses slash commands
- /model <id> prefers an exact provider/modelId match and reports bare IDs
  shared by multiple providers as ambiguous instead of picking the first
- tests: FIFO ordering (queued prompt before /compact), steer-delivered
  /compact queued until settle, ambiguous/qualified model selection

* fix(pi): keep /compact interruptible, honor extension precedence, require token boundary

- head-of-line /compact dispatches even while Pi is streaming (Pi's
  compact() aborts the active generation itself); every other queued item
  still waits for the stream to settle, preserving FIFO order
- discovered extension commands / prompt templates override same-name
  builtins at message time, matching the slash-list merge precedence
- parsePiSpecialCommand requires a command-token boundary, so path-like
  text such as /compact.md or /model/config stays an ordinary prompt
- tests: interrupt rule, extension collision, reserved-name path prefixes,
  non-compact commands waiting for stream settle

* fix(pi): honor cancellation acknowledged during slash-command discovery

A cancel arriving while the chain awaits get_commands (cold cache) was
acknowledged via the preparing reservation but never re-checked, so a
canceled /compact could still execute. Re-check the cancellation marker
after discovery and drop the message before dispatch.

* fix(pi): qualify /model selectors and report failed slash RPCs once

- /model lists provider-qualified selectors (openai/gpt-5.2) so duplicate
  bare IDs remain usable and copy-pasteable; current model is qualified too
- compact/set_model failures are owned by the awaited slash/config handlers:
  the common response handler no longer emits the raw Pi error a second time
- tests: qualified listing with duplicate providers, single-message failure
  reporting for rejected /compact and /model

* fix(pi): consume slash-command queue row at dispatch

Special commands (/compact, /session, /model, /help) are executed by HAPI
itself and never delivered to Pi as prompts. Consumption was deferred until
the command finished, so a /compact run — an LLM summarization pass that can
take minutes — left the row stuck in the web queued bar for its whole
duration, then surfaced as a sent message. Consume the row the moment
dispatch starts; failures still surface via the explicit event message.

* fix(pi): guard special-command dispatch against unexpected rejections

* ci: retry Codex PR Review after infra failure (proxy 503)

* fix(pi): keep session queued-thinking grace during /compact dispatch

The queued-thinking grace is session-scoped, so clearing it while
acknowledging a dispatch-time /compact row also drops the grace for any
prompt queued behind it. /compact keeps running for minutes without
toggling Pi thinking state, which would leave the web session looking idle
while compaction and the following prompt are still pending. Only the
fast, synchronous commands (/session, /model, /help) clear the grace.

* fix(pi): render compaction summary as a dedicated chat block

The manual /compact RPC result was reported as two plain message
events ("📦 Compaction completed (tokens: …)" + "📦 Compaction
summary: …"), which the web chat renders as tiny centered status
lines — unusable for a real summary payload. Emit a structured
compact-summary event instead (summary + token delta) and render
it as an independent block: header with the delta and the summary
markdown in a scrollable panel.

Also emit the same structured event when importing Pi session
files (compaction entries), and queue the event lossless like
other user-visible messages so a disconnect cannot drop it.

Verified: bun typecheck clean; bun run test exit 0 (cli 2481
passed, web 2451 passed, hub/shared clean); runPi/loop/apiSession/
piSessions/presentation suites green.

* fix(pi): address HAPI Bot findings on compact dispatch and import

- Track compaction as thinking for its whole duration: /compact runs for
  minutes without a Pi streaming event, so the 15s queued-thinking grace
  alone left the web session looking idle while compaction and any queued
  prompts were still pending (updateThinkingState around the compact RPC).
- Imported Pi compaction summaries must use the event envelope
  (content.type: 'event') like the live wrapper's compact RPC result; the
  codex payload envelope is dropped by the web normalizer. Extend
  CodexImportedMessageSchema with the event variant.

* fix(pi): /model retries discovery when the model cache is empty

Startup model discovery can be late or fail once; using only the cached
catalog made /model report valid models as unknown. getPiModels() falls
back to the get_available_models RPC on an empty cache, used for both
listing and switching.

* fix(pi): interrupt in-flight /compact on Abort; surface startup model rejection

- The Abort action no longer waits on the runtime-mutation lease when a
  manual /compact is in flight (compaction can hold it for up to 120s,
  blowing the 25s abort deadline and failing closed). It sends the abort
  RPC directly so Pi cancels its compaction AbortController; the compact
  RPC's 'Compaction cancelled' error is not double-reported as a failure
  since Pi already emits the compaction_end(aborted) lifecycle event.
- A rejected detached startup set_model now emits a visible ⚠️ event into
  chat instead of only a debug log, restoring the pre-existing behavior.

* fix(pi): close the Abort race when /compact is queued on the mutation lock

Abort previously assumed an in-flight /compact always had its RPC issued;
the command is marked active at queue dispatch, but the compact RPC is sent
only after the runtime-mutation lock is acquired. An Abort landing in that
gap acknowledged success while the compact RPC still ran afterwards.
Track the compact's rpcStarted/cancelled state: Abort cancels a not-yet-
started compact in place (the queued callback skips it), and interrupts a
started one via the abort RPC as before.

* fix(pi): persist provider-qualified selection after /model switch

The success path updated currentModel/currentProvider and keepalive with a
bare model ID, leaving metadata.piSelectedModel on the previous provider.
The web picker prefers that metadata for selection, context-window
resolution, and effort options, so a switch like openai/gpt-5.2 ->
azure/gpt-5.2 was invisible. Persist piSelectedModel with the full
provider/modelId pair on every confirmed switch.

* fix(pi): retire pending extension UI requests when /compact interrupts a turn

The streaming-interrupt path sent the compact RPC without cancelling
pending extension UI requests first, unlike the Abort path. Editor
requests have no timeout, so the web could stay stuck on a stale
input/permission card and a later answer could be routed to the aborted
turn. Cancel all pending requests (with a response) before compacting.

* fix(pi): fail closed when the direct compact-abort RPC times out

The in-flight /compact abort branch awaited the abort RPC without the
ordinary Abort path's timeout handling: an unanswered abort left the
compaction outcome indeterminate (the compact RPC keeps the mutation
lease for up to 120s) while the wrapper still looked live. Fail the
session on PiRpcTimeoutError, mirroring the standard abort fail-closed
path.

---------

Co-authored-by: swear01 <swear01@users.noreply.github.com>
2026-08-15 11:21:45 +08:00
AnanovoandGitHub 386ee4121b fix(web): hide chat viewport focus outline after Home/End (#1495)
* fix(web): hide chat scroll focus outline

* fix(web): keep chat viewport keyboard focus visible

* test(web): cover chat viewport keyboard focus
2026-08-15 11:17:43 +08:00
AnanovoandGitHub 0e60697895 fix(web): prevent Rewind crashes when message history resets (#1530) 2026-08-15 11:17:25 +08:00
AnanovoandGitHub 44703d0153 fix(web): localize Fork and Rewind labels in Simplified Chinese (#1546)
* fix(web): localize Fork and Rewind labels in Chinese

* test(web): cover localized history confirmation dialogs
2026-08-15 11:16:59 +08:00
AnanovoandGitHub b2ae6b90a3 fix(web): keep share actions visible during generation (#1550)
* fix(web): keep share actions visible during generation

* test(web): cover sharing during generation
2026-08-15 11:16:32 +08:00
AnanovoandGitHub 235d6dd6fd fix(web): clarify displayed media labels (#1565) 2026-08-15 11:15:42 +08:00
AnanovoandGitHub e314522982 fix(web): lower context warning thresholds (#1576) 2026-08-15 11:14:31 +08:00
51ae260a3f feat(web): settings for AGENT_NOTIFY_SUMMARY chat display (default hide) (#1477)
* feat(web): render AGENT_NOTIFY_SUMMARY as compact metadata

* fix(web): defer summary rendering until completion

* fix(shared): preserve indentation when splitting summaries

* fix(web): guard unknown summary statuses

* feat(web): settings for AGENT_NOTIFY_SUMMARY chat display (default hide)

Add hub setting sibling to emit (#1376): show compact NotifySummaryText
when on; strip footer from chat/copy when off. Store/parse/FCM unchanged.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(web): address #1477 Majors for display setting

Allow any namespace to GET hub-settings (PUT stays owner-only) so
sessionSummaryInChat applies hub-wide. Reject whitespace-delimited
AGENT_NOTIFY_SUMMARY examples so default-hide does not eat prose.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(shared): allow indented AGENT_NOTIFY_SUMMARY footers

Keep rejecting whitespace-delimited prose examples, but accept a
standalone footer whose only prefix is leading indentation.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(web): poll hub display setting; hide empty notify footers

Refetch sessionSummaryInChat so open clients pick up owner toggles.
When display is on, recognized footers without status/summary/action
still strip raw JSON instead of falling back to MarkdownText.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(web): poll notify display once in chat shell

Move hub-settings refetchInterval off per-message hooks into HappyThread
context. Strip well-formed footers while streaming when display is off.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(web,hub): QueryClient for HappyThread tests; atomic hub-settings

Wrap HappyThread mobile-scroll tests in QueryClientProvider after the
chat-shell hub-settings poll. Read/write both hub setting flags in one
settings.json snapshot under the shared lock.

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: Ananovo <78636812+techotaku39@users.noreply.github.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-12 10:10:36 +08:00
SSU-WEI HUANGandGitHub e6b9fd68e6 feat(pi): queue mid-turn messages by default; steer only via explicit per-message Steer button (#1480)
* feat(pi): queue mid-turn messages by default; steer only via explicit per-message Steer button

Pi (PyAgent) was the only flavor whose ordinary composer submission while
streaming bypassed the queue: the web resolved it to deliveryMode 'steer'
and the CLI dispatched a native steer into the running turn immediately,
with no waiting state. This makes Pi match Codex/Claude behavior (issue
#1466): mid-turn messages wait in the queue by default, and the operator
delivers one into the running turn with the new per-queued-message Steer
button.

- web: resolveMessageDeliveryMode now queues for every flavor; QueuedMessagesBar
  gains a Steer button (pi + thinking + remote-controlled + immediate rows)
  backed by a new useSteerQueuedMessage hook + api.steerMessage.
- hub: POST /sessions/:id/messages/:messageId/steer -> syncEngine.steerQueuedMessage
  (pi-only gate, remote-only, scheduled/absent/invoked rejection) -> RPC.
- cli: pi runner registers 'steer-queued-message'; a queued message is promoted
  into the active turn via the existing PiSteerDispatcher (target generation
  captured at promote time; turn-ended steers fall back to the prompt FIFO).
  Steers requested while the message is still preparing are deferred and
  promoted right after preparation completes.
- Removed the now-dead Alt+Enter / touch-hold queue gesture (its only purpose
  was opting out of the removed automatic steer).

Verified: bun typecheck; cli/hub/web/shared suites (env-dependent runner
integration + kimi wire-locator flakes reproduce on pristine upstream and
are unrelated to this diff).

* fix(pi): preserve queued messages on rejected steers and pin the steering generation

Addresses both Major findings from the HAPI Bot review of PR #1480.

- steerDispatcher: a deterministic native rejection (Pi responded error) now
  degrades the message to the ordinary prompt FIFO instead of emitting
  messages-consumed. A promoted queued message must not be lost just because
  the steer was rejected; the hub row stays queued until the FIFO delivers it.
  The indeterminate-timeout path keeps its fail-closed consume + escalate
  behavior (a duplicate delivery would be worse).
- runPi: the deferred-steer path now captures the streaming generation at RPC
  request time (Map<localId, generation>) instead of reading it after
  preparation completes, so a steer requested against turn G1 can never be
  injected into a turn G2 that started while the message was preparing — the
  dispatcher's generation-mismatch check degrades it to the FIFO.

Regression coverage: negative steer response preserves the entry via the FIFO
(no consume); generation rollover while preparing delivers as a normal prompt
at the next settle (no steer into the new turn).

Verified: bun typecheck; cli pi suites (48 tests), hub 1041, web 2301, shared
240 — all green; only the pre-existing environment-dependent runner
integration test fails locally (reproduces on pristine upstream).

* fix(pi): reject all scheduled steers and always clear deferred-steer bookkeeping

Addresses the two Minor findings from the HAPI Bot follow-up review.

- hub: steerQueuedMessage rejects every scheduled row — mature ones included —
  aligning the endpoint with the web UI (Steer is never offered on scheduled
  rows) and preserving scheduled-FIFO delivery semantics.
- cli: the deferred-steer bookkeeping map is now cleared in a finally on the
  preparation chain, covering the early exits (cancellation before/after
  attachment I/O, empty prepared message, preparation failure) that previously
  could leave a stale generation entry behind for the session lifetime.

Regression coverage: hub steer gate tests (mature scheduled row stays queued,
non-pi flavor rejected) and a runPi test proving cancellation wins over a
deferred steer (no steer/prompt/consume after preparation completes).

Verified: bun typecheck; hub 1043 pass, cli 2421 pass (only the pre-existing
environment-dependent runner integration suite fails locally), web 2301 and
shared 240 unchanged since their green runs.

* fix(web): reconcile stale queued rows when a steer returns invoked

Addresses the remaining Minor finding from the HAPI Bot follow-up review:
when the steer endpoint reports the message was already invoked and the
messages-consumed SSE was missed while the row was still queued, the hook
now marks the row consumed locally (mirroring useCancelQueuedMessage) so
the queued bar cannot keep a stale actionable row until the next sync.

Regression coverage: steer returning status 'invoked' reconciles the row
via markMessagesConsumed and shows no toast.

Verified: bun typecheck; web 2302 pass (hub/cli/shared unchanged since
their green runs).
2026-08-11 22:27:14 +08:00
AnanovoandGitHub 173da49c84 fix(web): remove duplicate queued composer gap (#1505) 2026-08-11 22:25:25 +08:00
AnanovoandGitHub c2aa4bf171 fix(web): align generated share watermark to bottom right (#1513) 2026-08-11 22:25:07 +08:00
AnanovoandGitHub db46b4e08f fix(web): hide unavailable history actions and reorder message actions (#1494)
* fix(web): reorder and hide history actions

* test(web): cover locked history actions
2026-08-11 10:03:09 +08:00
AnanovoandGitHub f63d4bb83b fix(web): remove trailing ellipses from search placeholders (#1449) 2026-08-10 10:50:50 +08:00
AnanovoandGitHub ad7f2a4084 fix(web): keep shared-image titles in sync with renamed sessions (#1411)
* fix(web): use renamed session title in share images

* ci: rerun failed checks
2026-08-10 10:47:46 +08:00
SSU-WEI HUANGandGitHub 1dd7a49f42 fix(web): keep Pi controls available during message send (#1442) 2026-08-10 10:47:19 +08:00
17d1ea0c75 fix(web): close composer settings on outside click (#1354)
* fix(web): close composer settings on outside click

Co-Authored-By: Codex <noreply@anthropic.com>

* fix(web): capture composer outside clicks

Co-Authored-By: Codex <noreply@anthropic.com>

---------

Co-authored-by: Codex <noreply@anthropic.com>
2026-08-08 13:53:48 +08:00
bdf8da1d5a fix(web): preserve drafts across inactive session resume (#1378)
* fix(web): preserve drafts across inactive session resume

* fix(web): address resumed draft review findings

* fix(web): preserve drafts during hydration resume

* fix(web): reset resume cache when session lifecycle changes

Clear resolvedSessionRef when session id or active flag changes so a same-id resume that later archives again cannot skip a required new resume.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(web): gate inactive attachments on resumability

Skip the attachment adapter and disable drops when an inactive session cannot resume, so file picks do not become stuck error attachments.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(web): avoid source-session 404 during reopen handoff

When reopen merges into a different id, skip invalidating the source session detail so draft transfer can finish before navigation without flashing Session unavailable.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(web): include in-flight attachments in inactive resume handoff

Pass the selected file into draft transfer and coalesce concurrent multi-file drops so resume navigation cannot drop attachments that were never published to the live snapshot.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(web): harden inactive draft handoff edge cases

Re-check attachment cancellation after resume, always copy from the source draft instead of a stale target snapshot, and force a fresh getSession after optimistic seeding.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(web): prevent passive resume and preserve staggered handoffs

Restore attachments only on active sessions so draft hydration cannot resume an archived session, and remember completed handoff targets so slower multi-file adds append instead of overwriting.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(web): avoid empty inactive snapshots shadowing attachment drafts

When attachment restoration is disabled, persist composer text without publishing an empty live snapshot so reopen can still transfer IndexedDB attachments.

Co-authored-by: Cursor <cursoragent@cursor.com>

* test(web): cover inactive draft archive-switch-reopen lifecycle

Add a component-level harness that exercises HappyComposer snapshot rules with useComposerDraft and transferComposerDraft across archive, session switch, and reopen to a new id.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(web): resolve SessionChat conflict markers for draft handoff

Keep upstream send-message types and the inactive-resume handoffComposerDraft import so typecheck no longer dies on committed merge markers.

Co-authored-by: Cursor <cursoragent@cursor.com>

* chore: retrigger CI after conflict-marker fix

Co-authored-by: Cursor <cursoragent@cursor.com>

* chore: retry CI after Actions outage (Meta daily)

Co-authored-by: Cursor <cursoragent@cursor.com>

* chore: retrigger CI and PR review on current tip

Re-queue Test and Codex PR Review after the conflict-marker fix and Actions outage.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(web): do not overwrite stored attachments from inactive composer

Skip attachment draft persistence on unmount when restoration is disabled, so a failed inactive file pick cannot replace hidden IndexedDB attachments with a partial visible list.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(web): merge inactive picks into stored attachment drafts

When restoration is disabled, persist visible pending attachments by merging them with IndexedDB instead of clearing the live snapshot alone, so a failed resume still keeps newly selected files for the next reopen.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(web): harden inactive draft persist for failed picks

Keep pathless attachment ids stable, serialize inactive merge/writes, drop removed visible picks from IndexedDB, and await pending persist before transfer/reopen.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(web): navigate after cancel during inactive resume handoff

If resume already merged into a new session id, still call onSessionResolved without the cancelled file so draft transfer + navigation leave the deleted source route.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(web): harden inactive draft handoff cancel and persist

Carry a live isCancelled predicate through resume handoff so mid-transfer
removes drop the file from the source snapshot, persist inactive blobs only
on attachment revision (not keystrokes), and keep upload metadata on
same-target staggered appends.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(web): await durable draft move and tombstone source

Cross-session transfer now awaits an atomic IndexedDB put+delete via
moveDraftAttachments, clears the source text draft, and skips unmount
re-persist once the source id is marked handed off.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(web): re-sample cancel after draft drain; fail closed on move

moveDraftAttachments resolves attachments only after awaiting queued
writes, and rolls cache back + throws when a real IndexedDB transaction
fails so transfer cannot clear the source or mark a handoff on loss.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(web): re-sample composer text after awaited draft move

Keystrokes during the IndexedDB drain keep updating the source draft;
re-read live/persisted text after the move resolves before clearing the
source and writing the target snapshot.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(web): navigate after resume even if draft move fails

Hub reopen can delete the source row before local IndexedDB finishes.
Catch transfer failures, still copy text best-effort, and always navigate
via transferComposerDraftThenNavigate / handoff onNavigable.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(web): harden draft move prune and in-flight persist

Delete the source row before put/prune so a full retention store cannot
evict an unrelated draft, and buffer inactive persist edits during an
in-flight transfer onto the target instead of recreating the source.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(web): close inactive draft remount and transfer races

Reset visibility bookkeeping on inactive remount, install the transfer
barrier before the first await, and await a durable corrective target
write when late edits land during an in-flight move.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(web): unbreak draft transfer typecheck and remaining races

Re-read pendingTransfers.latest after awaits (TS2339), propagate real
IndexedDB write failures on corrective same-target moves, and restore only
missing stored attachment ids when same-id resume already has a visible pick.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(web): stabilize late draft edits and multi-file resume

Repeat corrective target writes until pending.latest is unchanged across
the await, sync keystrokes into the transfer barrier, and keep one resume
promise per inactive adapter instance so staggered files join the same
handoff after navigation.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(web): keep IDB attachments when typing during transfer

Track inactive keystrokes on pending.latestText instead of inventing
pending.latest with an empty attachment list that would overwrite
hidden stored files during a cross-session move.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(web): fail closed when draft attachment reads fail

Cross-session transfer now requests throwOnError for IndexedDB reads so a
transient empty result cannot delete the source draft during the durable move.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(web): fail closed on inactive merge reads and keep target text

Strict-read every inactive attachment RMW merge so a transient IDB error
cannot publish a partial live snapshot, and copy composer text to the
resumed id before aborting when attachment transfer reads fail.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(web): keep resumed session active after stale detail fetch

Reapply optimistic active after background getSession settles so a lagging
inactive REST snapshot cannot flip the target composer back, and catch
strict inactive persist rejections at fire-and-forget call sites.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(web): prune attachments cancelled during durable draft move

Always rewrite the target after the cross-session move so isCancelled
flips during the IndexedDB commit window still drop the file, and prune
again in handoff before navigation when the batch reports cancellation.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(web): stabilize multi-file cancel across draft corrections

Treat the pending cancellation set as part of post-move stability so a
sibling remove mid-corrective rewrite is re-sampled before navigation,
and loop handoff same-target rewrites until that set stops changing.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(web): transfer inactive drafts before text-only resume send

Await onSessionResolved so composer draft handoff completes before the
send mutation, and route text-only auto-resume through
transferComposerDraftThenNavigate like upload/reopen paths.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(web): transfer Files reopen drafts and respect SSE active

Wire the Files-page reopen path through transferComposerDraftThenNavigate,
and refresh resumed session detail only when the cached active flag still
matches so a mid-flight inactive SSE transition is not resurrected.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(web): defer send until hidden inactive drafts hydrate

After text-only resume transfers stored attachments, skip the mutation
when the target draft still has files the inactive composer never
exposed, so clearDraftsAfterSend cannot wipe them after a text-only POST.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(web): defer any attachment resume and expose stored drafts

Always defer cross-session send when visible or stored attachments exist
so source-scoped upload paths are not POSTed, and surface IndexedDB
attachment presence to disable scheduling on inactive composers.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(web): resume same-id drafts and block pending schedule races

Return resumed independently of session id so same-row PTY/Pi resumes
still defer for hidden files, route attachment-only Send through an
explicit resume callback, and treat incomplete inactive hydration as
schedule-blocking.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(web): preserve send text during deferred draft handoff

Pass the immutable submitted text into draft transfer so assistant-ui clearing the composer while resume is in flight cannot drop text from deferred hidden-attachment sends.

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: HeavyGee <133152184+heavygee@users.noreply.github.com>
2026-08-08 13:47:25 +08:00
b7f52f58ca feat(media): add audio and file display (#1405)
* feat(cli): cross-flavor inline image display via MCP and ACP

Share display_image prompt across MCP-bridge flavors (Cursor, Gemini,
Kimi, Codex, Claude, OpenCode), auto-approve the tool in
buildHapiMcpBridge, handle ACP image content blocks, and harden
generated-image registration with content sniffing.

Closes tiann/hapi#956

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(web): render generated-image cards reliably in chat

Keep object URLs stable across refetch, upscale tiny inline images,
fetch generated-image bytes with cache no-store (avoid empty 304 bodies),
and load hapiMcpUrl from per-session API in hapi-display-image tooling.

Co-authored-by: Cursor <cursoragent@cursor.com>

* feat(cli+web): display_video MCP for inline mp4/webm (#956)

Add display_video alongside display_image, video MIME sniffing with avif
guard, web GeneratedImageCard video player, and hapi-display-image auto-routing.

Co-authored-by: Cursor <cursoragent@cursor.com>

* feat(cli+web): cross-flavor display_video parity with images (#956)

Share display_video prompts across MCP-bridge flavors, auto-approve the
tool, register mp4/webm via path sniffing, render inline video in web on
the existing generated-image RPC path, and restore robust media card fetch.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(cli): ACP image ordering and inline media source provenance

Flush buffered assistant text before async generated_image emit from ACP
image blocks (PR #958 review Major). Add optional source metadata on
generated-image wire messages (ingress, flavor, toolCallId, toolName) for
MCP, ACP, and Codex tool-result paths. Seeds artifact-event follow-up #966.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(cli+web): address PR #958 review Majors on media order and stale blobs

Queue ACP session updates and await async image registration before later
events; clear GeneratedImageCard blob state when imageId changes.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(cli): await ACP queue after late-drain before turn_complete

Straggler session/update during drainLateBuffers can queue async image
registration; re-await sessionUpdateQueue so generated_image is not emitted
after turn_complete.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(scripts): route AVIF ftyp brands to display_image in helper

Match server-side detectImageMimeType so .avif files are not sent to
display_video and rejected as unsupported video.

Co-authored-by: Cursor <cursoragent@cursor.com>

* feat(#956): agent inline-media doctor and discovery fixes

- hapi doctor inline-media: probe bridges, print per-session inline commands
- Expose hapiMcpUrl on session list summaries (stops false "no MCP" scans)
- Helper script: match cursorSessionId prefixes; HAPI_SESSION_ID path-only mode
- ACP bridge prompt: shell fallback + HAPI session id vs agent id rule

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(web): allow immutable cache for generated media blobs

Drop cache: no-store on generated-image fetch so browser can reuse hub
immutable responses; on 304 re-read via force-cache (#927, PR review).

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(#956): Cursor native MCP overlay; drop user-turn bridge prepend

Cursor ACP ignores session/new mcpServers. Write .cursor/mcp.json and
run agent mcp enable hapi instead. Remove HAPI_MCP_BRIDGE_PROMPT from
user turns on ACP remotes; enrich MCP tool descriptions for discovery.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(cli): preserve non-HAPI mcp.json keys on Cursor overlay cleanup

Cleanup only removes or restores the hapi MCP entry instead of rewriting
the full pre-session snapshot, so concurrent edits to other servers survive.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(cli): handle generated_image in Grok ACP launcher switch

Upstream Grok launcher exhaustiveness broke after AgentMessage gained
generated_image for cross-flavor inline media.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(cli): rebase fallout for display_video + OpenCode skill lookup

Gate display_video in the STDIO bridge, restore OpenCode first-prompt
TITLE_INSTRUCTION (skill_lookup), and update tool-list test expectations.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(cli): leave user-owned hapi MCP entry alone on overlay cleanup

Only undo mcpServers.hapi when it still matches the exact entry this
session installed; concurrent Cursor/user edits of that key survive.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(cli): exact-match auto-approve for display_image and display_video

Move media tools off substring name/id hints onto the exact-name set so
forged lookalike tools are not approved in default permission mode.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(cli): drop dead Cursor bridge prompt; put guidance in MCP descriptions

Cursor must not get a user-turn media prepend (prompt-taint). Remove unused
HAPI_MCP_BRIDGE_PROMPT_CURSOR and embed DISPLAY_*_PROMPT_CURSOR in the
display_image/display_video MCP tool descriptions instead.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(cli): require user approval for display_image and display_video

Those tools read arbitrary local paths into chat; keep them on MCP
approval_mode prompt and out of default-mode auto-approve exact names.

Co-authored-by: Cursor <cursoragent@cursor.com>

* chore: re-trigger Codex PR review after provider 503

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(cli): ignore URI-only ACP image blocks that read local disk

Passive ACP agentMessageChunk handling must not load file:// or bare
paths; local media goes through prompt-gated display_image/display_video.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(cli): allowlist MP4 ftyp brands for video sniffing

Reject HEIC/HEIF and other non-video ISO-BMFF containers instead of
treating every non-AVIF ftyp as video/mp4.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(cli): keep Cursor ACP startup if MCP overlay fails

Wrap installCursorMcpOverlay so a malformed project .cursor/mcp.json
cannot abort the session; continue without inline media tools.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(cli): fail doctor inline-media when checks fail

Exit non-zero whenever required checks fail, even if an active
hapiMcpUrl bridge is present.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(cli): expect display_video when change_title is disabled

Native ACP title mode still exposes display_image and display_video;
update startHappyServer test after rebase onto 0.23.4.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(cli): keep ACP title sync synchronous outside media queue

session_info_update title forwarding (#1028) must not wait on the
async message-handler queue used for inline media ordering.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(cli): sniff media headers only; advertise OpenCode display_video

Read 16 bytes for detectMediaTool instead of the whole file, and include
hapi_display_video in OPENCODE_NATIVE_TOOL_INSTRUCTION for remote ACP.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(cli): show Cursor generate_image inline in HAPI chat

cursor/generate_image only emitted a tool card; register filePath or
base64 imageData into generatedImages and emit generated_image so the
web chat card renders (issue #956 / swear01 report).

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(cli): ignore path-only Cursor generate_image reads

Path-only filePath registration bypassed permission-gated display_image /
display_video MCP tools. Keep base64 imageData only; local paths must go
through MCP approval.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(cli): gate inline media base64 length before decode

Reject oversized ACP/Cursor base64 payloads by character count so the
CLI never allocates past the 25 MB generated-image cap.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(cli): require EBML DocType webm for inline video sniff

Bare EBML magic matches Matroska/MKV too; only accept DocType webm.
Also restore annotated Playwright cursor in annotatedVideoUseOption.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(scripts): read 128-byte header for WebM DocType sniff

detectMediaTool only loaded 16 bytes, so EBML DocType webm was often
missing and valid WebM files fell through to display_image.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(web): compare generated-image source by value in reconcile

Wire normalization allocates a fresh source object each pass; reference
equality forced media-card recomputation on every reload/SSE refresh.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(cli): inject Cursor MCP enable for overlay unit tests

installCursorMcpOverlay always spawned `agent mcp enable`; tests now pass
a noop so the suite never shells out to a real Cursor binary.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(cli): shell-quote doctor inline-media helper command

Paths and session prefixes with spaces/metacharacters broke the copied
snippet; JSON.stringify each interpolated argument.

Co-authored-by: Cursor <cursoragent@cursor.com>

* test(cli): fix doctor inline-media quote path expectation

Repo root from scriptPath is three levels up (cli/), not the parent of cli.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(cli/web): per-session Cursor MCP overlay id and bound tiny-image scale

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(cli): redact generate_image base64 from logs and fix doctor MCP ids

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(cli): harden inline-media doctor for packaged installs and hub headers

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(cli): lock Cursor mcp.json updates and bound ACP media filenames

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(cli): preserve mcp.json mode and token-scoped overlay locks

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(cli): publish Cursor MCP lock owners via link(2) and treat EPERM as alive

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(cli): fail closed on stale MCP locks; keep concurrent mcp.json top-level keys

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(cli): roll back Cursor MCP overlay when agent mcp enable fails

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(cli): await ACP session queue in suppressUpdatesDuring tests

#958 queues handleUpdate for media registration; upstream compact tests
assumed sync delivery after restore.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(cli): capture ACP handler at enqueue; keep display_video manual

Close two Major review findings on #958: suppress queue leak after
restore, and Claude --allowedTools auto-approving local-path video.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix: write through symlinked mcp.json; lazy-load inline video

Preserve user Cursor MCP symlinks on atomic overlay writes, and require
explicit Load video before fetching large generated-video blobs.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(web): use valid TerminalToolDisplayMode in media card test

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(scripts): require unique session prefix in display helper

Reject ambiguous prefix matches so images/videos cannot land in the
wrong HAPI chat when multiple agent session ids share a prefix.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(cli): always cleanup Cursor MCP overlay on teardown

Run overlay cleanup in finally so cancelAll/disconnect failures cannot
leave a dead hapi-<sessionId> entry in .cursor/mcp.json.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(cli): handle Copilot generated_image; refuse MCP symlinks

Unblock typecheck after Antigravity/Copilot merge, and fail closed when
.cursor/mcp.json or .cursor is a project-controlled symlink.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix: abort restore deliveryMode; prune dead Cursor MCP overlays

Unblock web typecheck after steer merge, and recover orphaned hapi-*
mcp.json entries via HAPI_MCP_OVERLAY_PID ownership stamps.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(cli): recover dead-PID Cursor MCP overlay locks

Token-matched unlock so a crash mid-lock no longer permanently disables
inline media; keep live-owner waits identity-safe.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(cli): serialize Cursor MCP stale-lock recovery

Acquire an exclusive recovery lock before token-matched unlink so two
recoverers cannot remove a successor's live mcp.json lock.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(cli): fail closed on stale Cursor MCP overlay locks

Withdraw racy auto-recovery: pathname check-then-unlink/rename can steal
a successor lock. Stale locks throw with an explicit rm hint instead.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(web): drop duplicate deliveryMode on abort restore

Merge left both steer and queue; keep queue so retries after abort
do not re-bind to a later Pi turn.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(cli): bound inline media reads on open fd

Close TOCTOU between pathname size check and readFile for display_image /
display_video and registerGeneratedImageFromPath. Also preserve non-PID
env edits on Cursor MCP overlay cleanup.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(cli): repair happyMcpStdioBridge test syntax after merge

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(cli): write Cursor MCP overlay to ~/.cursor, not the project

Keep ephemeral hapi-<sessionId> bridges out of the checked-out tree so
agents cannot git-add a live loopback URL. Tests inject mcpConfigDir.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(cli): point Cursor MCP diagnostics at ~/.cursor/mcp.json

Co-authored-by: Cursor <cursoragent@cursor.com>

* feat(media): add audio and file display

---------

Co-authored-by: HeavyGee <133152184+heavygee@users.noreply.github.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Debian <heavygee@oos-linux.in.lockhouse>
2026-08-08 13:46:59 +08:00
TEEKandGitHub cfca9d210d fix(web): prevent near-bottom chat scroll jump (#1358)
* fix(web): prevent near-bottom chat scroll jump

* fix(web): preserve explicit tail scroll intent
2026-08-07 13:20:26 +08:00
KorenKritaandGitHub 0a05fada00 fix(web): coordinate reasoning panel scrolling (#1398) 2026-08-07 09:48:25 +08:00
AnanovoandGitHub b57fb35867 fix(web): collapse expanded composer after successful send (#1368)
* fix(web): collapse expanded composer after send

* fix(web): wait for send result before collapsing composer

* fix(web): match composer collapse to send settlement
2026-08-06 14:37:18 +01:00
KorenKritaandGitHub 5e7e930713 fix(web): fit composer placeholder to available width (#1388) 2026-08-06 18:51:02 +08:00
KorenKritaandGitHub 8c4d4198a8 fix(web): preserve manual scroll during initial settling (#1377)
* fix(web): preserve manual scroll during initial settling

* fix: remove duplicate Windows lockfile entry

* fix(web): capture scrollbar input during settling
2026-08-05 15:04:48 +08:00
9b4b9ec9d4 fix(web): avoid React #185 from MessageActions useAuiState object snapshot (#1381)
#1306 returned a fresh object from useAuiState on every call. useSyncExternalStore
compares snapshots with Object.is, so that looped re-renders and crashed every
session chat with minified React error #185. Split into primitive selectors and
add regression coverage. Fixes #1380.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-05 15:03:45 +08:00
weishu b31c5a8e76 fix(web): skip filler br element in contenteditable composer 2026-08-05 00:01:38 +08:00
f6da005b50 feat(web): FUE + composer hint for session @-mentions (#1274)
* feat(web): FUE + placeholder for rich composer session @-mentions

Discover session @-mentions via composer-grounded FUE and always-on
placeholder copy when rich composer is active (#1273).

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(web): notify onFocus after programmatic rich-composer autofocus

Playwright headless (and some engines) skip the DOM focus event for
element.focus(), so FUE engage never ran. Call the onFocus prop after
autofocus so discovery still works.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(web): engage rich-composer FUE on mount

DOM focus events are unreliable for programmatic autofocus (and in
Playwright). Treat the live rich composer as the affordance and open
the callout when the rich path mounts.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(web): measure FueCallout height; ellipsis rich placeholder

Bot review on #1274: position from real panel height (ResizeObserver)
so multi-line FUE bodies clear the composer, and keep long mention
placeholders on one ellipsized line in the input row.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(web): Escape dismisses rich-composer FUE; keep expand flex chain

Escape while the mention FUE is engaging only dismisses the callout
(no abort/collapse). FUE anchor is a flex container so expanded
RichComposerInput still fills height. Mock resolveComposerPlaceholderKey
in sendError tests.

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-04 11:18:21 +08:00
AnanovoandGitHub a0c676818f fix(web): sync share metadata and active-turn availability (#1306)
* fix(web): align sharing with session state

* fix(web): keep share state in sync

* fix(web): fail closed for trimmed active turns

* fix(web): refresh prepared share images

* fix(web): preserve sharing during queued thinking

* fix: track a stable active turn boundary

* fix: anchor active turns to persisted messages

* fix(web): include Pi reasoning in share metadata

* fix(hub): refresh queued thinking grace on retry

* test(web): isolate mobile thread scroll setup

* fix(hub): advance queued turn boundaries

* fix(hub): guard queued boundary advancement

* perf(web): precompute running turn sharing

* fix(hub): use hub time for turn boundaries

* perf(web): pause closed share metadata timer
2026-08-04 11:18:08 +08:00