Commit Graph
664 Commits
Author SHA1 Message Date
SSU-WEI HUANGandGitHub a794be0811 fix(web): recover silently-dead SSE connections quickly, stop banner noise (#1562)
A suspended mobile tab can lose its SSE connection without the browser
ever noticing (no FIN/RST arrives). On resume the client used to wait
for the 90s heartbeat watchdog or lean on the native EventSource retry,
which can hang on a dead pooled socket - leaving a persistent
"Reconnecting... (stream error)" banner while the UI keeps working.

- useSSE: on visibility resume, distrust the connection after one missed
  heartbeat interval (45s) instead of 90s
- useSSE: abandon connection attempts that don't open within 10s (hung
  on a dead pooled socket after resume) instead of waiting for the
  watchdog; force bypasses the one-shot reconnect guard for the new
  attempt cycle
- useSSE: first reconnect attempt is immediate; exponential backoff
  starts from the second attempt
- ReconnectingBanner: localize connect-timeout and transport-error
  reasons (transport-error is already emitted by current code but fell
  through unlabeled)

Fixes #1559. Reimplements the reconnect half of #989 (closed unmerged)
on top of the current scheduler (hidden-tab deferral, slow backoff,
replay cursor).
2026-08-15 11:16:19 +08:00
AnanovoandGitHub 235d6dd6fd fix(web): clarify displayed media labels (#1565) 2026-08-15 11:15:42 +08:00
AnanovoandGitHub 7be4babfbd fix(web): shorten Codex sync label and size action menu to content (#1569)
* fix(web): refine Codex sync action menu

* test(web): cover action menu trigger anchoring
2026-08-15 11:14:45 +08:00
AnanovoandGitHub e314522982 fix(web): lower context warning thresholds (#1576) 2026-08-15 11:14:31 +08:00
AnanovoandGitHub e0354b09c0 fix(web): align new-session and settings controls (#1578) 2026-08-15 11:14:12 +08:00
AnanovoandGitHub ad72229923 feat(sessions): add on-demand AI title suggestions (#1577)
* feat(sessions): add on-demand AI title suggestions

* fix(sessions): address title suggestion review feedback

* fix(web): ignore stale title generation results
2026-08-15 11:13:49 +08:00
AnanovoandGitHub febbf8ff58 fix(web): improve session search UI (#1545)
* fix(web): improve session search UI

* fix(web): reserve space for search clear control
2026-08-13 10:14:55 +08:00
df1a56e1db fix(cursor): exclusive agent spawn lease for list-models vs ACP (#1529)
* fix(cursor): exclusive agent spawn lease for list-models vs ACP (#1520)

Add a proper-lockfile spawn lease beside agent-acp-active so model probes
and ACP transport acquire mutual exclusion atomically before spawning
agent children, closing the post-#1518 check-then-act overlap window.

Fixes #1520

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

* chore(web): fix markdown-a test HappyChatContext mock for typecheck

Adds showSessionSummaryInChat to chatContext() so CI typecheck passes on
the PR branch (pre-existing main breakage unrelated to #1520).

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

* Revert "chore(web): fix markdown-a test HappyChatContext mock for typecheck"

This reverts commit 4973f321a31fda772e8990ea6cda20517ca906aa.

* fix(cursor): scope spawn lease to agent spawn window only (#1520)

Hold agent-cli.spawn only around spawn('agent') in AcpStdioTransport, not
for the full ACP session. Restores N concurrent cursor sessions per host;
list-models probe lease unchanged.

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

* fix(cursor): tighten spawn lease lifecycle for babysit (#1520)

Acquire spawn lease before ACP marker publish; unregister on spawn failure.
Hold list-models probe lease until child exit on timeout. Add missing
showSessionSummaryInChat to markdown-a test mock (unblocks CI typecheck).

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

* fix(cursor): re-check ACP marker after spawn lease acquire (#1520)

Close check-then-act window where ACP could publish its marker between
the inactive guard read and list-models spawn. Add regression test.

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

* test(cursor): fix ACP-after-acquire mock call order (#1520)

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

* fix(cursor): async spawn lease + force-kill probe timeout (#1520)

Add acquireAgentCliSpawnLease (setTimeout yields) for ACP create path;
AcpStdioTransport.create() async factory. Probe timeout uses
killProcessByChildProcess(force) while holding lease until child exit.

Addresses Bugbot Majors: session-lifetime mutex (fe07b708d), probe lease
release, post-acquire re-check (137baa779), sync loop starvation, timeout
escalation.

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

* fix(acp): coalesce concurrent initialize() transport spawns (#1520)

Await shared bootstrapTransport promise so overlapping initialize calls
do not spawn duplicate ACP children while create() is in flight.

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

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-12 18:42:43 +01:00
SSU-WEI HUANGandGitHub b9d1abed1d fix(web): restore scroll chaining from reasoning panel to chat viewport (#1501)
The reasoning panel carried overscroll-y-contain, which disables native
scroll chaining: after scrolling the panel to its bottom, the outer chat
viewport could not keep scrolling. #1264 removed the containment for
exactly this reason; #1398 re-added it while adding nested follow-tail
coordination, silently reverting #1264.

The coordination (onNestedScrollFollowChange pauses the outer auto-follow
while the user scrolls inside the panel) already resolves the #1397
fighting; containment is unnecessary. Drop it and add a regression test
guarding the class list.

Fixes #1500
2026-08-12 10:10:48 +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
c1ceb83ec2 fix(web): fail-closed scheme-less markdown file links (#1519)
* fix(web): fail-closed scheme-less markdown file links

Never paint a blue SPA dead-end for path-like hrefs after #1142.
Route workspace file targets (relative, abs, ~/ when expandable) through
FilePathAnchor; keep real app routes navigable; render everything else
path-like as inert text. Defense in <A> plus expanded remark rewrite.

Refs #1452

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

* test(web): fixture for fail-closed markdown file-link dogfood

Visual cases for #1452: preview blues vs inert dead paths vs SPA routes.

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

* fix(web): harden fail-closed markdown href policy for review findings

Stop rewriting POSIX abs in remark so <A> can workspace-check; tighten SPA allowlist; resolve .. before containment.

* fix(web): fail-closed Windows absolute markdown file links

Drive paths looked scheme-bearing and skipped containment; leave them for <A> with workspace checks.

* fix(web): autolink Windows paths as raw hrefs for containment

Bare/inline Windows abs become anchors without hapi-file rewrite so <A> can classify; compare containment case-insensitively.

* fix(web): encode Windows file links as hapi-file-candidate

Backslash paths were URI-normalized to %5C before <A>; candidate encoding preserves the path for workspace classification.

* fix(web): satisfy InertMarkdownHref href type for candidate paths

* fix(web): resolve ~/ against /root workspaces in markdown hrefs

* fix(web): reject non-Windows hapi-file-candidate payloads

* fix(web): decode percent-encoded markdown paths before containment

* fix(web): fail-closed empty hapi-file-candidate hrefs

* fix(web): honor Vite BASE_URL and normalize candidate scheme detection

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-12 09:30:37 +08:00
SSU-WEI HUANGandGitHub d396e9d6d4 feat(voice): curate dictation credential presets to ElevenLabs, OpenAI, Groq (#1474)
* feat(voice): curate dictation credential presets to ElevenLabs, OpenAI, Groq

Groq transcription was already wired end-to-end (GROQ_API_KEY,
whisper-large-v3, standard mode), but the credential onboarding panel
listed five providers with no hint that Groq is supported, so mobile
users could not discover it.

- Curate Settings > Voice > Dictation credential presets to ElevenLabs,
  OpenAI, and Groq (Deepgram / OpenAI-compatible remain fully supported
  via env and stay listed when configured)
- Name the three presets in the empty-state and manage hints (en + zh-CN)
- Lock the curated list in with a web preset test, a hub route test for
  the Groq whisper-large-v3 proxy, and shared provider-listing coverage
- Note the presets and no-restart save behavior in voice-assistant.md

Verified: bun typecheck (cli+web+hub) and targeted suites pass; full
test gate green except pre-existing load-sensitive runner stress tests.

* fix(voice): keep legacy dictation providers manageable when configured

HAPI Bot review finding (Major): curating the onboard panel to the three
presets made settings-managed Deepgram / OpenAI-compatible credentials
impossible to rotate or clear from the UI.

- Re-add deepgram / openai-compatible to the onboard provider list
  conditionally when credentials exist, restoring update/clear controls
- Fall back to the first preset if the selected provider leaves the list
- Cover the conditional list in the preset test

* fix(voice): surface partial OpenAI-compatible credentials in onboard panel

HAPI Bot follow-up finding (Major): hub marks openaiCompatible.configured
only when both base URL and model exist, so api-key-only or endpoint-only
stored settings lost the UI path to rotate or clear them.

- Gate the openai-compatible onboard entry on any stored field (base URL,
  model, or API key) via hasOpenAICompatibleCredentials()
- Cover api-key-only / base-url-only / model-only cases in tests
2026-08-11 22:27:44 +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 eb7a762984 fix(web): clarify session list status hints (#1504) 2026-08-11 22:26:07 +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
1cd4d1137a feat(hub,cli,web): fleet runner version governance (skew, self-upgrade, soft-fail reopen) (#1108)
* fix(hub): govern runner capabilities so Cursor reopen soft-fails on skew

Hub↔runner protocol drift was reported as missing Cursor chat data when
cursor-chat-store-status was unregistered. Soft-fail reopen on probe errors,
advertise required machine capabilities, surface an unmissable upgrade banner,
and stop-runner when a newer CLI binary is already on disk.

Fixes #1084

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

* fix(web,hub): make runner skew banner dismissible; gate auto-upgrade

Compact the out-of-date banner (minimize + 1h snooze + per-host Restart)
so it no longer blocks the session list. Auto stop-runner on skew stays
opt-in via HAPI_AUTO_UPGRADE_RUNNERS / autoUpgradeRunners (default off).

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

* fix(web): tolerate full sessionStorage on skew banner minimize

QuotaExceededError from setItem aborted minimize before React state
updated, leaving the banner stuck over the session list. Persist to
memory when storage fails; only enable Restart when a newer CLI is
already on disk; clarify opt-in is stop-runner only, not package push.

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

* fix(hub): drop redundant autoUpgradeRunners; runners already self-restart

CLI version handoff already reloads the runner when the on-disk binary
mtime changes. Hub-driven stop-runner on skew duplicated that. Keep the
skew banner and manual Restart only as a stuck/disabled-handoff escape.

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

* fix(cli,hub,web): runner-only caps ads; gate Restart on supervisor

Address #1108 bot Majors on the thin tip: terminal/lazy bootstraps no
longer merge CURRENT_MACHINE_CAPABILITIES into the machine row (only
asRunner registration does). Banner Restart refuses unsupervised hosts
so stop-runner cannot leave a detached laptop offline; supervised
runners advertise supervisedRestart via HAPI_RUNNER_SUPERVISED=1.

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

* fix(hub,cli,web): clear sticky runner ads; docs SUPERVISED; i18n skew label

Omit-means-clear on runner registration so rollback cannot leave
supervisedRestart/capabilities sticky; always advertise boolean
supervisedRestart from asRunner. Document HAPI_RUNNER_SUPERVISED=1
and localize MachineSelector UPDATE REQUIRED.

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

---------

Co-authored-by: Debian <heavygee@oos-linux.in.lockhouse>
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-11 22:24:39 +08:00
0d12035674 feat(web): searchable session picker on Android share target (#986)
* feat(web): searchable session picker on share target (#980)

Add search to the Android share-target picker so operators can attach
shared content to inactive or older sessions, not just recent actives.
Reuses sidebar search helpers; caps default active list with a search hint.

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

* fix(web): defer share pending consume until session is active

ShareSeedConsumer was consuming the sessionStorage transfer id on first
mount even when the target session was inactive. Reopening into a new
session id remounted the chat with the key already gone, dropping the
shared payload. Consume and seed only after sessionActive is true.

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

* fix(web): bind share pending transfer to target session id

Deferring consume until active left a global pending slot that the next
unrelated active SessionChat could claim. Bind the pending transfer to
the picked session id, retarget on reopen/spawn id-swap, and only
consume when the mounting session matches.

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

* fix(web): wire SessionListSearch date-range props on share picker

Upstream added required customStart/customEnd/onDateRangeChange to
SessionListSearch; share /share must pass them and honor the range when
filtering sessions after rebase onto v0.23.1.

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

* fix(web): pass sessionActivityDates to share SessionListSearch

Upstream SessionListSearch now requires activity dates for the date
picker; without them CI typecheck fails and the PR chip stays needs_work.

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

* fix(web): use cached machine labels in share picker search

Share picker now uses useMachineLabels like SessionList so stale
machine ids still match search by display name.

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

* fix(web): align share picker with sidebar session prep and preview limit

Snapshot via prepareSidebarSessions and pass useSessionPreviewLimit
so search targets and fold cap match the main session list.

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

* fix(web): wire SessionListSearch expanded props on share picker

Upstream search now requires expanded/onExpandedChange; default open
on the share picker so the field is available immediately.

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

* fix(web): unblock typecheck after SessionSummary and RawSendError fields

Fill new required SessionSummary watermarks in the share picker test
helper, and set deliveryMode on abort-restore send errors.

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

* fix(web): mock machines hooks in share page missing-state test

SharePage now calls useMachines for picker search labels; the missing-
share unit test needs those hooks stubbed without a QueryClient.

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

* fix(web): retarget pending share on automatic session supersession

When SessionDetailRoute follows supersededBySessionId A→B, rewrite the
share pending target so ShareSeedConsumer on B can still claim it.

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

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Debian <heavygee@oos-linux.in.lockhouse>
2026-08-11 14:25:00 +01:00
5d8cd9b8fa fix(web): exclude path-only husks from @ session mentions (#1507)
* fix(web): exclude path-only husks from @ session mentions

Mention autocomplete required a real title signal (metadata.name or
summary text) so sidebar-hidden stubs and path-basename husks cannot
win @ queries over live named sessions (#1506).

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

* fix(web): mention @ pool uses sidebar dedup before title filter

Titled stale duplicates hidden by prepareSidebarSessions were still
@-able and could outrank the live row. Align mention candidates with
the visible list, then keep the #1506 title-signal exclusion.

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

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-11 12:38:46 +01:00
a0621194bb feat(web): native/deep-link ingest for /share (GET url, text, title) (#1413)
* feat(web): ingest GET /share?url=&text=&title= deep links

Native companions that cannot POST via Web Share Target can open the
same session picker by synthesizing the IndexedDB transfer client-side.
When id is present, the existing SW path still wins.

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

* fix(web): preserve share deep-link whitespace; scrub content beside id

Keep GET content strings verbatim when non-empty (match POST form-data).
When id is present with leftover url/text/title, replace to ?id= only so
payload does not linger in the address bar. Query-param contract stays —
fragments would break shipped native companions.

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

* fix(web): ingest /share deep links via URL fragment, not query

Shared url/text/title must not appear on the HTTP request line — hub
Hono logger (and any access log) records path+query. Native companions
open /share#url=&text=&title=; the client reads the fragment, scrubs it,
and continues with the existing ?id= picker path. Query validateSearch
keeps only id/error (Web Share Target redirect).

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

* fix(web): keep /share hash ingest across StrictMode remount

Capture the fragment in useState and reuse a single putShareTransfer
promise so the first effect's scrub + cleanup cancel does not lose the
deep-link under React.StrictMode.

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

* feat(web): fetch companion fileUrl into /share transfer files

Native shares cannot put binaries in the hash fragment. Companions host a
one-shot CORS URL and pass fileUrl/fileName/fileType; the share page fetches
bytes into the same IndexedDB files[] as Web Share Target POST.

* fix(web): cap share fileUrl fetch at the composer upload ceiling

Stream fileUrl downloads with Content-Length and body size checks matching
MAX_UPLOAD_BYTES so a crafted deep link cannot buffer unbounded bytes into
IndexedDB. Align native deep-link docs on the fileUrl hand-off vs POST.

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

* fix(web): cast streamed fileUrl chunks to BlobPart for tsc

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

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-11 12:36:58 +01:00
SSU-WEI HUANGandGitHub d147ae00aa fix(web): rename storageUsagePie helper to storageUsageSlices to avoid macOS case-collision with StorageUsagePie.tsx (#1483)
Since #1383 renamed the helper to storageUsagePie.ts, the same directory
contained both StorageUsagePie.tsx and storageUsagePie.ts. On case-insensitive
filesystems (macOS), extensionless imports of the component resolve to the
helper because '.ts' is tried before '.tsx' and 'StorageUsagePie.ts' matches
'storageUsagePie.ts' case-insensitively, breaking typecheck and component
tests on macOS. Linux CI is unaffected.

Renames the helper (and its test) to storageUsageSlices.ts, which cannot
collide, and updates the two importers. Fixes #1482.
2026-08-11 10:04:18 +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 a36df44221 fix(web): left-align confirmation dialog descriptions (#1434) 2026-08-10 10:50:36 +08:00
Junmo KimandGitHub 495fa53465 fix(opencode): surface upstream errors and retries (#1433)
* fix(opencode): report why a prompt failed instead of pointing at logs

The provider's own explanation already reaches this process: the ACP
transport rejects session/prompt with the JSON-RPC error message
verbatim. The launcher caught it, logged it, and handed the user a fixed
"OpenCode prompt failed. Check logs for details." — a remote user is by
definition not at the machine holding those logs, so a rate-limited
session simply stopped with no stated reason.

Only the message is used; that channel carries no response headers,
cookies or body. It is unbounded though (a 20KB provider body produced a
20,210-character message), so it is capped at the same 200 characters
the compaction bridge already applies to provider text, and the JSON-RPC
"Internal error: " wrapper is stripped. A failure with nothing readable
to say still renders the sentence it always did.

* feat(opencode): surface upstream retries from the agent event stream

A provider rate limit leaves OpenCode retrying indefinitely, and it
announces that on exactly one channel: its own server event stream.
Measured against a provider stubbed to answer 429 — 40 minutes, 85
retries, zero ACP notifications, zero stderr bytes, session/prompt never
settling. HAPI showed a session that looked like it was thinking and
said nothing else.

Subscribes to that stream once the ACP session id is known and reports
retries as the same api_error system message Claude sessions already
use, so the web timeline folds a run of them into one block whose
attempt count climbs. The reason rides along in the payload, and the
presentation now appends it to "Retrying..." when an agent supplies one;
sessions that supply nothing render exactly as before.

Not every attempt is announced. The backoff tops out at 30 seconds and
OpenCode does not give up, so a session held against a daily quota would
otherwise persist two messages a minute for as long as it is left
running. The first few attempts are reported, then only attempt numbers
that are powers of two, which needs no clock to decide.

The subscription must be scoped with ?directory=: without it the
endpoint delivers heartbeats and no session events at all, with neither
an error nor a 404 to notice. Its session.error event is deliberately
not read — it carries the Authorization header, cookies and the full
response body verbatim, and the same failure already reaches the user
stripped to a message through the ACP prompt error.

Delegated turns are not covered: OpenCode's subagent tool runs them in a
child session with its own id, and this follows only the one it was
opened for.

No countdown is rendered, though the payload offers one: a timeline
block outlives the turn it describes. Turn state is left alone; a
retrying session really is busy.

* fix(opencode): only relay prompt failures OpenCode itself reported

AcpStdioTransport flattens a JSON-RPC error response to
new Error(response.error.message), so a rejected session/prompt is
indistinguishable by shape from an error the transport built locally.
Its process-close error appends up to 4KB of raw subprocess stderr to
the message, which the formatter would then have published to the hub
and every connected client.

Requiring the "Internal error: " wrapper OpenCode puts on every service
failure turns this into an allowlist: an unrecognised rejection renders
the sentence this path rendered before rather than whatever it happened
to contain. Retry text is collapsed to one line before it is judged,
since a whitespace-only message is truthy and embedded newlines break
the one-line contract the helper states.
2026-08-10 10:48:48 +08:00
AnanovoandGitHub ffd0c752ca fix(web): align and remember Codex-family permission mode (#1410)
* fix(web): align and remember Codex-family permission mode

* fix(web): restore family permission preferences for all agents

* fix(web): scope legacy yolo migration to Codex
2026-08-10 10:48:26 +08:00
AnanovoandGitHub 044d72fdc0 fix(web): show file metadata in preview header (#1450) 2026-08-10 10:47:58 +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
AnanovoandGitHub ac5e959045 fix(web): return chat file previews to conversation (#1415)
* fix(web): return chat file previews to conversation

* chore: retrigger pull request checks
2026-08-10 10:47:34 +08:00
SSU-WEI HUANGandGitHub 1dd7a49f42 fix(web): keep Pi controls available during message send (#1442) 2026-08-10 10:47:19 +08:00
06e2507ad8 fix(web): add Ctrl+X / Ctrl+S to mobile terminal Control pad (#1460)
Dedicated C0 chords for Jed Emacs save (C-x C-s); sticky Ctrl+softkey is unreliable on mobile IME. Keeps Ctrl+L.

Fixes #1459

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-09 15:34:18 +01:00
b8a765d4ef fix(web): keep project pin inside groups below In progress (#1458)
* fix(web): keep project pin inside groups below In progress

Revert #1432's section lift: Pin in project stays first inside its
folder and among directory groups, but must not promote whole folders
above In progress. Restore Settings copy to match.

Fixes #1457

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

* fix(web): align pin-in-progress Settings copy with exclusions

Describe unpinned floaters only, keep global pins above In progress,
and avoid implying every working session or project-pin folders float.

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

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-09 12:45:01 +01:00
00b6e44869 fix(web): keep project-pin groups above In progress (#1432)
Durable project pins were losing to unpinned In progress floaters.
Render pin-containing directory groups between the global pinned band
and the In progress section; leave project-pinned rows inside groups.

Closes #1431

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-09 09:50:54 +01:00
weishu cde0507bad fix(codex): harden model discovery fallback 2026-08-09 09:04:28 +08:00
3da9f7780a feat(web): support project and global pinned sessions (#1115)
* feat(web): support persistent pinned sessions

* fix(web): preserve project hierarchy for pinned sessions

* fix(web): preserve pins during session deduplication

* fix(hub): preserve pins when merging sessions

* fix(hub): migrate session pins from schema v15

* chore: retrigger PR review

* chore: retrigger PR review

* chore: retrigger PR review

* chore: retrigger PR review

* fix: align pinning with schema and sidebar filters

* fix(web): expose pinning in session header menu

* test(hub): expect schema v17 after pin migration

* fix(web): report pin action failures

* chore: retrigger PR review

* chore: retrigger PR review

* chore: retrigger PR review

* chore: retrigger PR review

* fix: address pinning review regressions

* fix(web): scope pin error toast to session rows

* test(hub): expect schema v20 after migration

* fix(hub): preserve latest source pin during merge

* test(hub): expect schema v22 after pin migration

* feat(web): add project and global session pin modes

Support mutually exclusive project vs global pins, surface both in the
session action menu, and render globally pinned sessions in a top-level
sidebar group with project path labels.

* fix(web): polish pinned section icon and divider alignment

* fix(web): tighten pinned section title icon alignment

* fix(web): restore original pinned section pin icon shape

* fix(web): rename pinned section to pinned sessions

* fix(hub): prefer stronger pin mode when merging sessions

Keep global pins over project pins during consolidation so a project-pinned source cannot downgrade an already or concurrently global-pinned target.

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

* fix(web): keep inactive project-pinned groups expanded

Pinned sessions should stay easy to find after reload; do not hide them behind the default inactive-group collapse.

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

* fix(web): keep project pins in directory groups

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-08 16:07:21 +01: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
a13b54ccc7 feat(web): improve mobile terminal input (#1407)
* feat(web): improve mobile terminal input

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

* fix(web): preserve terminal IME and key activation

---------

Co-authored-by: Codex <noreply@anthropic.com>
2026-08-08 13:46:19 +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
28df974edd feat(settings): onboard hub provider credentials for dictation and voice (#1392)
* feat(settings): onboard hub transcription provider credentials in UI

Env-only keys made dictation invisible; Settings can now add/edit/clear
hub-side credentials (masked), with env still winning as override.
Refs tiann/hapi#1384.

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

* feat(settings): onboard voice-assistant backends alongside dictation

Same Settings credential surface now covers ElevenLabs, Gemini Live, and
Qwen Realtime (alias env pairs), not only transcription providers.

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

* fix(settings): address PR #1392 Major credential onboard findings

Alias env locks, non-destructive Save (omit empty fields), and
owner-only settings.json permissions for hub-stored provider secrets.

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

* fix(settings): harden credential onboard for second-pass Majors

Owner-namespace gate, stage-then-sync env after persist, and
per-field OpenAI-compatible editability under mixed env locks.

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

* fix(settings): serialize settings RMW and clear partial compatible creds

Per-file settings lock for concurrent credential PUTs, and Clear shown
for partial OpenAI-compatible entries (key/url/model alone).

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

* fix(settings): serialize all settings writers via updateSettings

Route credentials, relay auth, generators, server settings, and CLI
token persistence through a locked RMW helper; reset Clear form state.

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

* fix(settings): share cross-process settings lock with CLI

Extract withSettingsFileLock for hub+CLI, keep owner-only 0o600
rewrites, and race hub credential updates against CLI-style writers.

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

* fix(settings): keep UI secrets out of process.env; PID-own settings locks

Settings-backed provider credentials now live in an in-memory overlay
(getProviderEnvironment) so tunnel/ACP/Codex children do not inherit them.
Settings file locks record pid+token and only reclaim dead or legacy locks.

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

* fix(settings): never reclaim ownerless settings lock sidecars

wx creates the lock path before the owner JSON is visible; unlinking
null owners let a waiter steal a live acquisition and collide on
settings.json.tmp (CI ENOENT). Only reclaim parsed owners with dead PIDs.

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

* fix(settings): reclaim dead locks via rename; clean up failed publishes

Stale reclaim renames the sidecar to a unique break path and re-verifies
the expected dead owner before deleting it, so a loser cannot unlink a
successor's live lock. Failed owner writes unlink the wx sidecar.
Reclaim uses a sync owner read so contenders do not all observe one
dead owner across an await and race the exclusive create.

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

* fix(settings): reclaim dead locks under exclusive reaper sidecar

Stale reclaim now takes a fixed settings.json.lock.reap lock, re-validates
pid+token, then unlinks — so a delayed contender cannot move a successor's
live lock aside. Also document providerCredentials in settings.schema.json.

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

* fix(settings): fail closed on corrupt CLI settings; backoff busy reaper

CLI updateSettings now uses a strict read that rejects invalid JSON
instead of treating errors as {}, which could wipe providerCredentials.
Settings lock reclaim sleeps when another process holds .reap so retries
are not burned synchronously.

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

* fix(settings): publish locks via candidate+link; fix CLI vitest hoist

Acquire settings locks by writing a complete candidate then linkSync to
the fixed path so a crash cannot leave an empty live sidecar. Fix the
CLI persistence regression test to create its temp dir inside vi.hoisted.

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

* fix(settings): replace bespoke lock with proper-lockfile; hide tenant creds UI

Codex kept finding crash windows in hand-rolled lock sidecars. Switch the
shared settings lock to proper-lockfile's mkdir + mtime lease. Hide the
owner-only credentials editor from non-default namespaces on the voice page.

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

* fix(settings): adapt sessionSummaryContract to outcome updateSettings

Rebase onto main brought #1376 unique tmp + outcome-shaped writers;
wire sessionSummaryContract and the write-failure credential test to match.

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

* chore: retrigger CI after rebase onto upstream/main

Empty commit — Meta reported no checks on da0c6c258 after tip-forward rebase.

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

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-07 13:20:03 +08:00
a020c73629 feat(web): interactive storage usage pie in Settings (#1383)
* feat(web): interactive storage usage pie in Settings

Show relative DB/WAL/SHM share beside absolute sizes on the
Storage settings page, with hover/legend selection and no new
chart dependency.

Fixes #1382

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

* fix(web): lead Settings Storage with relative-share donut

Put the interactive chart above exact byte rows so mobile users
see proportions first and scroll for drill-down sizes.

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

* fix(web): drop duplicate Exact sizes table on Storage

Keep one interactive donut + legend (bytes/%) beside or below the
chart; total and path stay as non-redundant footer lines.

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

* fix(web): storage pie legend roving tabindex + empty footers

Address Codex review: one Tab stop via roving tabindex, total outside
the listbox, and keep total/path when all slices are zero.

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

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-07 09:48:44 +08:00
KorenKritaandGitHub 0a05fada00 fix(web): coordinate reasoning panel scrolling (#1398) 2026-08-07 09:48:25 +08:00
KorenKritaandGitHub feb4099731 fix(web): stabilize fork boundary while streaming (#1399) 2026-08-07 09:47:55 +08:00
f399aa0222 feat(web): transient Unread-only session list filter (#1400)
* feat(web): opt-in needs-attention session list filter

Add a default-off sidebar filter (and Settings → Display toggle) that
keeps permission/input/unread rows while excluding background-only busy
work, mirroring the active-only preference pattern from #903.

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

* fix(web): make needs-attention filter a transient sidebar toggle

Drop the Settings → Display preference and localStorage persistence.
This is an inbox lens: toggle in the session list, rows fall away as
they're seen, and a reload clears the filter.

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

* fix(web): unread-only sidebar lens, not needs-attention

Rename the transient toggle to Unread only and filter solely on
updatedAt > lastSeenAt. Permission/input attention stays on row dots;
this is not Overseer review inbox.

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

* fix(web): use unread-dot shape for unread filter toggle

Match the session-row unread indicator instead of an inbox tray so
Overseer can own inbox chrome later without competing metaphors.

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

* fix(web): apply unread filter after machine scope

Keep machineFilters derived from the unfiltered sidebar set so a
persisted machine selection is not cleared when that machine has no
unread rows. Unread intersects with the selected machine instead.

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

* fix(web): snapshot last-seen once for unread filter pass

Avoid N localStorage read/parse calls while the unread lens is on by
loading the store once per memoized filter pass.

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

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-07 09:47:39 +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
988f8f183f feat: settings toggle for AGENT_NOTIFY_SUMMARY contract injection (#1376)
* feat: settings toggle for AGENT_NOTIFY_SUMMARY contract injection

Add a hub-persisted, default-off Settings control so operators can opt agents into emitting the trailing AGENT_NOTIFY_SUMMARY line. Propagate the resolved flag on CLI session bootstrap and inject at call time for Claude, Codex, OpenCode, and Grok (Cursor still unsupported).

Closes #1375

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

* fix: lock hub settings RMW and restore abort deliveryMode

Serialize settings.json updates with the shared .lock protocol and unique
temp files so the new hub toggle cannot clobber CLI/relay fields under
concurrency. Also supply deliveryMode on abort send-error restore so web
typecheck (and CI) pass.

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

* fix(web): wrap SettingsGeneralPage tests with QueryClientProvider

The hub-settings toggle uses TanStack Query; the settings page suite
was rendering without a QueryClient and blew up CI.

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

* fix(web): defer summary-contract toggle until settings load

Avoid rendering an interactive false switch while the hub GET is still
in flight, which could overwrite an enabled preference on early click.

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

* fix(web): clarify Grok coverage for summary-contract toggle

Local Grok has no instruction inject path; settings copy now matches
remote-only Grok support (and still excludes Cursor).

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

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-06 18:51:35 +08:00
KorenKritaandGitHub 5e7e930713 fix(web): fit composer placeholder to available width (#1388) 2026-08-06 18:51:02 +08:00
6ffc125b1a fix(web): show truncated query on collapsed session search (#1391)
* fix(web): show truncated query on collapsed session search

Collapsed SessionListSearch only showed an indicator dot for active
filters, hiding the terms operators need. Render a compact truncated
query chip when text filter is set (Fixes tiann/hapi#1356).

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

* fix(web): use chat-user chip colors for active search chip

--app-link/15 washes to near-gray in default light mode (link is
near-black). Use --app-chat-user-chip-* so the collapsed query chip
stays a visible blue highlight when text truncates away.

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

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-06 18:50:27 +08:00
KorenKritaandGitHub e532ff259b feat(web): add setting to keep reasoning collapsed by default (#1385) 2026-08-05 23:02:45 +08:00