Commit Graph
1221 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 effc505a07 fix(mcp): clarify display_image user-output semantics (#1568) 2026-08-15 11:15:27 +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
weishu fea42212ea Release version 0.27.3 2026-08-12 10:26:06 +08: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
c69a88afae fix(cursor): close ACP list-models race and false exit 143 window (#1518)
* fix(cursor): close ACP list-models race and false exit 143 window

Register the agent-acp-active guard before spawn, hold it until stdio
close (not bare exit), record the ACP child PID, and align lock/cache
home with resolveHapiHomeDir so runner and session children agree.
Richer exit attribution distinguishes live-PID transport disruption
from confirmed child death. Fixes residual #1472 after #835.

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

* test(cursor): isolate ACP guard teardown from ~/.hapi

Reset afterEach under the temp HAPI_HOME only, and restore the
isolated home before teardown in the unset-HAPI_HOME case, so tests
cannot wipe a live agent-acp-active lock. Use distinct child PIDs in
registration tests.

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

* fix(cursor): publish ACP lock pid before count

Fail-closed reservation order: write pids/<hostPid> before count so
concurrent reconcile cannot treat a mid-register lock as stale and
clear it for list-models. Keep a short mtime grace only when pids/ is
missing (mkdir gap). Regression covers mid-publish readers.

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

* fix(cursor): keep empty pids/ ACP reservation fail-closed

Between mkdir(pids) and the host pid writeFile, reconcile could see
liveCount=0 and clear the lock. Keep that window when count is still
absent and the lock is fresh; re-scan for pids published mid-reconcile.
Regression hooks the mkdir/write gap.

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

* fix(cursor): keep ACP lock across last-unregister publish race

Write a short-lived registering marker before pids/count so empty pids
with leftover count cannot erase a concurrent mid-addLockPid reservation.

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

* fix(cursor): use per-pid ACP registering markers

Crash/reboot must not pin list-models forever on a bare registering
file; prune dead owners and only keep live registrar PIDs.

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

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-12 09:55:26 +08:00
7c4b3ab17b fix(cursor): stop session-list spinner flicker from ACP state_update (#1503)
* fix(cursor): stop session-list spinner flicker from ACP state_update

#1487 mapped Cursor ACP state_update running/idle onto hub thinking.
Cursor chatters those states while HAPI is queue-idle, so keepAlive flipped
thinking every ~1-2s and the session list spinner danced. Ignore
state_update for thinking; only bump on real activity, clear via prompt
finally/abort.

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

* fix(cursor): clear thinking on ACP idle without bumping on running

Refine #1502 hotfix: ignore state_update running/requires_action (Cursor
chatter caused spinner flicker) but still clear on idle so mid-idle harness
wakes do not stick thinking=true forever.

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

* fix(cursor): stop ACP background updates from flickering thinking

Ignore tool/content session updates for hub thinking (ACP allows them while
idle). Drive thinking from state_update only, debounce running 750ms, and skip
idle clears during an in-flight HAPI prompt so #1502 residual flicker dies.

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-12 09:54:37 +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 291e7bc40b fix(test): stop runner integration suite from leaking detached process trees (#1515) (#1521)
* fix(test): stop runner integration suite from leaking detached process trees (#1515)

The default CLI test run included runner.integration.test.ts, which spawns
real detached runner/session process trees. A failing, timed-out, or
interrupted test (or a plain runner stop) left those trees alive under
PID 1 — on the Mac this accumulated ~600 Node/Bun/agent processes and
several GiB of RSS over repeated runs.

Test harness changes only; production runner session-preservation
semantics are untouched:

- Exclude runner.integration.test.ts from the default parallel unit-test
  suite; move it into a dedicated serial integration project
  (vitest.integration.config.ts, 'bun run test:integration'). The
  20-session stress test is opt-in via HAPI_RUN_STRESS_TESTS=true.
- Add a test-owned process/session registry (processRegistry.ts): every
  runner, runner-spawned session, and terminal-style child is registered
  immediately after spawn; afterEach/afterAll run two-stage cleanup
  (logical stopRunnerSession first, then bounded process-tree kill),
  followed by a marker sweep for agent grandchildren reparented to PID 1.
- Add a per-run HAPI_TEST_MARKER env stamp + identity/secret env
  neutralization for test children (integrationEnv.ts) so outer HAPI/pi
  session variables never leak into test processes and the final audit
  can recognize test-owned processes by env alone.
- Final suite audit in globalSetup teardown: reap anything still
  carrying the run marker and fail with PID/command diagnostics if
  anything cannot be reaped, before removing the temp home.
- Regression coverage: a deliberately failing test registers a detached
  child and the follow-up audit must find zero test-owned processes.
- CI: replace the dead .env.integration-test step with a dedicated
  integration job running the serial project.

* refactor(test): drop unused killByChildProcess import and child field from registry

* chore(test): raise integration hookTimeout to 60s for slow teardown hosts

* fix(test): fail loudly when the process-table audit cannot scan; assert regression child death

Bot review #1521 findings:
- A failed `ps` scan (unsupported flags, buffer exhaustion, permissions)
  previously returned [] and silently disabled both teardown audit layers.
  It now throws; globalSetup teardown catches the scan error into the
  audit error (temp home is still removed) so the run fails visibly.
- The regression audit test cleaned the leak with the reaper before
  asserting, and force-killed the fresh marked runner. The failing
  test's direct child PID is now asserted dead in afterEach right after
  registry cleanup (before the marker sweep), and the audit test stops
  its own runner gracefully before reaping.

* fix(test): bound the logical cleanup phase so a hung runner cannot stall the hook

Bot review #1521: stopRunnerSession carries the worker's 60s HTTP timeout
(setup.ts raises HAPI_RUNNER_HTTP_TIMEOUT for the stress test), and the
integration hook timeout is also 60s — N sequential stops could exhaust
the hook budget before the process-tree fallback and marker sweep ran,
recreating the very leak this change prevents.

Logical shutdown is now parallel (Promise.allSettled over all tracked
sessions) and the whole phase (stops + PID resolution) races against a
15s budget, so stage-2 tree-kill and the marker sweep always get their
share of the hook window.

* fix(test): bound graceful runner stop in hooks; keep credentials out of audit diagnostics

Bot review #1521 (follow-up):
- stopRunner()'s HTTP stop can burn the worker-wide 60s timeout on a
  hung-but-live runner, starving the marker sweep within the hook budget.
  afterEach/afterAll now race the graceful stop against a 10s bound; a
  runner that does not stop in time is force-reaped by the sweep (it
  carries the run marker) and the next beforeEach's alive-PID guard
  ignores any stale state file.
- The env-bearing ps scan (ps eww) was also used for diagnostics, so the
  first 500 chars of a short-command process could print inherited
  credentials (CLI_API_TOKEN etc.) into teardown error logs. The scan now
  only identifies marked PIDs; command lines are fetched separately
  without 'e', falling back to '(command unavailable)' instead of the
  env dump.

* fix(test): reap runner model-probe orphans before the zero-survivor inspection

Bot review #1521 (Minor): inspect-before-reap. Applying it exposed a real
race: each test's runner legitimately spawns marker-carrying children at
startup (agent acp + agent --list-models model-catalog probes). Stopping
the runner orphans them (ppid 1) with the run marker, so the audit test's
OWN runner polluted the pure inspection with fresh probes spawned after
the failing test's sweep window.

- reapTestOwnedProcesses now re-kills every re-scan iteration instead of
  killing once and only re-scanning, so a process that survived its first
  SIGKILL (mid-exec) or spawned mid-kill is not given a free pass.
- The regression audit test stops its runner, reaps (clearing its own
  legitimate orphan probes), then inspects: anything still marked is a
  genuine survivor the bounded reaper could not remove and fails the
  suite. Killable leaks from the failing test are already asserted dead
  in afterEach before the sweep runs.

* fix(test): strictly bound the marker reaper; make per-test sweep unconditional and verified

Bot review #1521 (follow-up):
- The 10s reap deadline did not bound the awaited per-tree kills: each
  killProcessTreeByPid can wait up to 2s per PID, so several stuck
  processes could still exceed the 60s hook budget. Every process in a
  test-owned tree carries the marker (env is inherited), so tree-walking
  is unnecessary: the reaper now SIGKILLs every marked PID found by each
  scan, fire-and-forget, and re-scans every 250ms — the deadline strictly
  bounds the function.
- The per-test sweep was skipped when the direct-child assertion failed
  first, and its survivors were ignored. afterEach now snapshots the
  regression-child state BEFORE the unconditional sweep, then verifies
  both the registry result and the sweep leftovers.

* fix(test): replace it.fails regression with a direct assertion test

Bot review #1521 (Minor): Vitest applies the it.fails expected-failure
inversion after afterEach, so a broken registry assertion inside the hook
would be masked as an expected failure, and the marker sweep would erase
the evidence before the follow-up audit ran.

The regression is now a normal test that registers a detached child at
spawn time, deliberately performs NO per-test teardown, runs only the
spawn-time registered cleanup, and asserts the child PID is dead. The
afterEach no longer carries the registry-leak assertion (moved into the
test body where it cannot be inverted); the per-test sweep assertion and
the final audit test are unchanged.

* fix(test): bound registry stage-2 tree-kills; require live regression fixture

Bot review #1521 (follow-up):
- Stage-2 killProcessTreeByPid awaits per descendant serially and can
  consume the whole 60s hook for a large/stuck tree. Signals are all
  delivered synchronously (children first) before any waiting, so racing
  the awaits against a 5s budget bounds the phase without skipping any
  kill; waitForAllDead still verifies the outcome.
- The regression test could pass vacuously if its fixture exited during
  the startup delay (the registry exit listener would remove it before
  cleanup). It now asserts the child is alive before running cleanup.

* fix(test): kill registered roots with bare synchronous SIGKILL, no pgrep walk

Bot review #1521 (follow-up): racing the mapped killProcessTreeByPid
calls against a timer does not bound the phase — evaluating the map
invokes each call immediately, and each runs the recursive synchronous
pgrep walk before its first await, which can consume the hook before the
timer, runner stop, or marker sweep run.

Stage-2 now SIGKILLs registered roots directly (fire-and-forget, no
tree walk, no per-PID waits) and waits a bounded 5s for death.
Descendants are reaped by the unconditional marker sweep immediately
afterward — every descendant inherits the run marker, so tree-walking is
unnecessary.

* fix(test): drop duplicate process-death wait in registry cleanup

Bot review #1521 (Minor): the duplicated waitForAllDead delayed the
authoritative marker sweep by another 5s under the exact stuck-process
condition the harness must handle. Keep the single bounded wait; the
afterEach marker sweep remains the guarantee.
2026-08-12 09:29:59 +08:00
11964b4b0d fix(a2a): stamp causing inbound on work_ad notify ingest (#1510)
* fix(a2a): stamp causing inbound on work_ad notify ingest

Resolve the turn cause from the full session messages table at insert so
consumers do not guess from a truncated transcript. Stamp causeMessageId,
causeText, causeKind, and related_event_id, and link follows to the
previous work_ad.

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

* fix(a2a): ignore scheduled and client-posted work_ads in cause chain

Future-scheduled inbounds are not this turn's cause. Only AGENT_NOTIFY_SUMMARY
rows chain related_event_id / sticky cause, so HTTP-posted work_ads cannot
steer hub-derived attribution.

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

* fix(a2a): skip transcript echoes and reserve notify provenance

Claude jsonl echoes remote prompts as extra role=user CLI rows; mark them
and ignore them when choosing a work_ad cause. HTTP event writes cannot
claim AGENT_NOTIFY_SUMMARY provenance.

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

* fix(a2a): stamp transcript echo only for hub-delivered prompts

Local Claude TTY prompts share isExternalUserMessage; matching pending
web/telegram text keeps those rows as work_ad causes.

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

* fix(a2a): match Claude queue text and bound cause message scan

Register pending transcript echoes at the formatted queue boundary
(and drop them on cancel). Later work_ad notifies scan after causeSeq
instead of decoding the full session transcript.

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

* fix(a2a): note delivered Claude batch and skip uninvoked causes

Register transcript-echo text at SDK delivery after batch join and
skill expansion. Keep the marker when cancel misses the queue.
Cause candidates require invokedAt so queued localId rows wait.

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

* fix(a2a): note parked Claude batches and chain work_ads by insert order

Stamp echo markers on the pending mode-switch delivery path. Advance
causeSeq past every invoked inbound in the same batch. List session
work_ads by rowid so backdated transcript timestamps cannot rewind the chain.

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

* fix(a2a): pick latest invoked inbound after history hydration

Fork/merge copies leave old user rows without prior work_ads; choose
the newest invoked inbound before the assistant. Echo markers now keep
batch localIds so cancel can drop a restored-then-cancelled prompt.

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

* fix(a2a): drop echo markers when a Claude batch is abandoned

The three-failure launcher cap discarded the in-flight prompt without
clearing pending transcript-echo text, so a later identical local
prompt could be misclassified.

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

* fix(a2a): replace echo markers when a restored prompt is rebatched

Recoverable launch failure keeps the original marker; a later same-mode
prompt joins into new delivered text. Drop overlapping localId markers
so a later identical local prompt is not stamped isTranscriptEcho.

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

* fix(a2a): keep notify cause chain across session merge

Re-key AGENT_NOTIFY_SUMMARY work_ads onto the surviving session and
bound later scans by causeCursorMessageId so merge seq-shift cannot
revive an already-consumed batch inbound as the sticky cause.

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

* fix(a2a): keep notify rows on live source during history merge

mergeSessionHistory leaves the source socket alive. Only re-key
AGENT_NOTIFY_SUMMARY work_ads when mergeSessions deletes that id.

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

* fix(a2a): drop id-less echo markers on rebatch and launch drop

API callers may omit localId. Replace the nameless in-flight marker
when a new id-less delivery is noted, and discard by delivered text
when the three-failure path abandons the batch. Do not mint hub
localIds (that would change invokedAt ack semantics).

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

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-11 18:08:02 +01: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
24e0c76717 feat(cursor): bump hub thinking on ACP harness wake (#1487)
* feat(cursor): bump hub thinking on ACP harness wake

When Cursor resumes after idle (notify_on_output / mid-idle ACP activity
or a permission request), flip thinking via the existing session-alive
keepalive so the hub list matches reality. Fixes #1470.

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

* fix(cursor): emit thinking true/false edges for ACP harness wake

Address Codex Major on #1487: activity listener now reports idle as
false, and the launcher only keepalives on actual thinking transitions
so streamed chunks do not spam session-alive.

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

* fix(cursor): reattach activity thinking listener after session/new remap

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

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-11 10:03:42 +08:00
SSU-WEI HUANGandGitHub 2548eaf3ed fix(cli): raise flaky claudeRemote first-test timeout to 15s under CI load (#1493)
The first test in claudeRemote.test.ts imports the full remote-module
graph and intermittently exceeds vitest's default 5s timeout on loaded
CI runners, failing PRs that touch no cli/ files. Give that one test a
15s per-test timeout; verified: typecheck exit 0, claudeRemote suite
6/6 pass (~2s cold), full suite 223/224 files pass (runner.integration
fails identically on pristine base in this sandbox).

Fixes #1491
2026-08-11 10:03:27 +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
SSU-WEI HUANGandGitHub b572c35e34 fix(pi): surface mid-turn LLM errors in web chat (#1479)
Pi finalizes a failed assistant message with stopReason 'error' and
errorMessage on both message_end and turn_end, but PiMessageAccumulator
only handled text/thinking deltas, so the error was silently dropped and
the web showed nothing (spinner stop + partial text at most).

Emit an AgentMessage {type:'error'} once per failed message (message_end
wins, turn_end is the safety net for Pi builds that skip message_end);
the web already renders AGENT_MESSAGE_PAYLOAD_TYPE error payloads as
system messages. User-initiated aborts stay silent.

Closes #1478
2026-08-10 11:47:56 +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
ad12eeb5d6 feat(a2a): P3 AGENT_NOTIFY_SUMMARY → work-graph status ingest (#1467)
* feat(a2a): P3 AGENT_NOTIFY_SUMMARY → work-graph status ingest

Land A0 events/event_links substrate (aligned with #1374 contracts) plus
thin P3 ingest so opt-in notify footers become durable work_ad rows.

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

* fix(a2a): address #1467 cold pass-1 Majors on notify ingest

Populate expires_at from message ts+TTL, persist message timestamps on
insert, pin append-only ledger survival, and defer project column to P4.

* test(hub): cover notify ingest via syncEngine message-received

Pins the display-never-gates-capture invariant on the real handleRealtimeEvent
wiring path (cold pass-1 minor 12).

* fix(a2a): address #1467 Sol cold Majors on notify ingest

Support AGY agy_message text extraction, bind audit principal to session id,
map stalled→blocked (never self-stale), and cap work-graph POST bodies.

* fix(a2a): validate work-graph creates at store insert (Sol S4)

Notify ingest built WorkGraphEventCreate and inserted without schema
bounds; oversized footer summaries could land past HTTP Zod limits.
Validate WorkGraphEventCreateSchema in insertWorkGraphEvent, clamp
untrusted notify strings at elevation, and regress 2049-char summaries.

* fix(a2a): reject fractional work-graph list limit

Bot Minor: limit=1.5 passed Number.isFinite and hit SQLite LIMIT as a
non-integer, surfacing as 500. Require Number.isInteger before query.

* fix(a2a): keep notify elevation inside payload budget

Drop duplicated notify_summary nesting that could exceed the 32 KiB
payload_json cap after per-field clamps, and measure the cap in UTF-8
bytes via TextEncoder instead of UTF-16 string.length.

* fix(a2a): clamp notify footer fields by UTF-8 bytes

Per-field string.length clamps still let CJK footers exceed the UTF-8
payload_json budget and silent-drop elevation. Truncate action/project/
agent/summary (and tags) to the ledger byte caps before insert.

* fix(a2a): clamp notify payload fields by JSON-escaped UTF-8

Raw UTF-8 clamps still let backslash-heavy footers expand past the
payload_json budget after JSON.stringify. Budget the escaped form so
elevation inserts instead of silent-dropping.

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-10 10:49:52 +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
SSU-WEI HUANGandGitHub 427ac1ff95 fix(pi): sync native session name (#1454)
* fix(pi): sync native session name

* fix(pi): sync live native renames via session_info_changed

Pi emits session_info_changed on /name and set_session_name. Share the
native-title sync callback between get_state startup/resume and the live
rename event so HAPI metadata.summary.text mirrors Pi's authoritative
session name without a change_title flow. Explicit HAPI/web renames
(metadata.name) keep precedence per sessionTitle.ts.
2026-08-10 10:46:16 +08:00
SSU-WEI HUANGandGitHub 2a98b4425f fix(agy): sync native Anti-Gravity conversation titles (#1476)
* fix(agy): sync native Anti-Gravity conversation titles (#1439)

Read the Anti-Gravity CLI's conversation_summaries.db title for the
active brain UUID and mirror it into HAPI session metadata.summary,
reusing the existing native-title normalizer so placeholder/empty
titles are ignored and a user-defined metadata.name stays preferred
by the web title logic.

The existing AGY scanner polls every 5s while a brain is known, so the
title follows native generation/rename without PTY parsing. Lookup is
best-effort: missing/locked DB or schema drift degrades to no-op.

* test(agy): cover delayed/renamed native title polling and cleanup stop

Addresses HAPI Bot review suggestion on #1476: scanner re-reads the
native title on later scans (null -> generated -> renamed) and stops
reading after cleanup.
2026-08-10 10:46:04 +08:00
SSU-WEI HUANGandGitHub bb40a4e8d0 fix(agy): preserve PTY running status (#1456)
* fix(agy): preserve PTY running status

* fix(agent): let a trailing idle marker win over a same-chunk busy marker

runAgentPty treated any busy marker in a chunk as authoritative, so a
chunk carrying both 'Generating' and the idle footer discarded the idle
marker. With the silence watchdog disabled (agy), nothing re-armed
inputReady and the session stayed 'running' forever. Compare marker
positions: the last one in the chunk reflects the final repaint.

Adds lastMarkerIndex for string and non-global RegExp markers and a
null-watchdog regression with both markers in one ANSI-decorated chunk.

* fix(agent): scan the full chunk and keep busy-run completion bookkeeping

- Scan promptBuffer + data before truncating to PROMPT_BUFFER_SIZE, so a
  busy marker followed by more than 4096 bytes in one callback is still
  seen; otherwise the run never completes and a pending web delivery can
  block subsequent messages.
- When an idle marker wins over a same-chunk busy marker, keep the busy
  confirmation so completeAgentRun fires for the run that just ended.
- lastMarkerIndex now clones non-global RegExps with the g flag and scans
  the original string, preserving anchors and terminating on zero-width
  matches (previously /$/ looped forever).
2026-08-10 10:45:52 +08:00
SSU-WEI HUANGandGitHub e6e229c068 fix(codex): preserve plan approval in yolo (#1420) 2026-08-10 10:44:41 +08:00
SSU-WEI HUANGandGitHub b744414241 fix(cli): skip invalid set_mode for interactive Copilot sessions (#1463)
* fix(cli): skip invalid set_mode for interactive Copilot sessions

applyInitialAgentMode() unconditionally sent session/set_mode with
modeId 'interactive', which the Copilot ACP server rejects (Invalid
mode 'interactive' - supported: agent/plan/autopilot), killing every
default-mode Copilot session at startup.

- Short-circuit applyAgentMode('interactive') as a no-op success,
  mirroring buildCopilotAcpArgs which already omits --mode at spawn
- Classify 'Invalid mode' responses as unsupported runtime switching so
  server-side mode rejections degrade gracefully (restart to apply)

Verified: bun typecheck + bun run test (full suite green).

* fix(cli): map Copilot interactive mode to ACP agent mode

Address review findings: the previous no-op for interactive left the
backend in Plan/Autopilot after a runtime switch while HAPI reported
Interactive, and classifying 'Invalid mode' responses permanently
disabled switching for valid modes too.

- Map interactive -> the ACP 'agent' mode id (the spawn default) so
  session/set_mode always receives a valid mode and runtime switches
  back to Interactive stay in sync with the backend
- Keep 'Invalid mode' out of the capability-loss classifier: it is a
  per-mode rejection, not evidence that set_mode is unavailable
2026-08-10 10:44:24 +08:00
weishu c2cc73159c fix(codex): preserve transcript final answer ordering 2026-08-10 10:36:34 +08:00
SSU-WEI HUANGandGitHub 190293ce9e fix(voice): preserve compatible dictation locale (#1455) 2026-08-10 08:49:49 +08:00
fe4d51043c fix(cli): remap bracketed Cursor wires onto bare/SKU catalogs (#1430)
* fix(cli): remap bracketed Cursor wires onto bare/SKU catalogs

#1271 only handled the grok-4.5 legacy alias family. Persisted wires like
gpt-5.3-codex[fast=false] still failed agent --model on resume against
today's bare ACP catalogs. Remap non-legacy wires to bare bases / CLI
SKUs (and nearest configOption wires), and prefer spawn-safe ids in
session state.

Fixes #1428

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

* fix(cli): harden Cursor stale-model remap against silent wrong picks

Address Codex Major review on #1430: prefer spawn-safe ids even on exact
bracket cache hits, require explicit wire params when ranking configOption
candidates, and stop downgrading unavailable CLI SKU variants.

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

* fix(cli): preserve desired Cursor variant across spawn-safe remap

Do not overwrite session.model with the bare/SKU spawn id before ACP
apply, and keep spawn-safe requested ids in wireIdForCursorSessionState
so apply does not re-poison hub state with bracket wires.

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

* fix(cli): remap-retry Cursor session/new on stale model reject

Initialize and session/load already retried once after Cannot use this
model; session/new had the same failure mode with an empty shared cache.
Mirror the one-shot remap+respawn path and cover it with a launcher test.

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

* fix(cli): resolve Cursor apply against ACP option values first

Spawn-safe remap prefers bare/SKU ids, but set_config_option may only
accept full bracket wires. Resolve against the model option list before
falling back to metadata so apply does not pick a rejected bare base.

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

* fix(cli): fail Cursor launch when remapped spawn cannot restore model

When --model was remapped for spawn, restoring the original desired
variant via ACP is mandatory. Soft-failing left sessions running on the
wrong model. Cover success and hard-fail paths in launcher tests.

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

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-09 20:49:42 +01: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