* feat(codex): support /personality via in-session override
Intercept /personality in the Codex slash layer, keep the value in CLI
memory only, and forward it on thread/turn start when set. Unset means
omit the field so Codex config/thread defaults apply—no Hub DB or web UI.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(codex): refuse fake /personality clear (sticky thread setting)
turn/start.personality sticks for later turns; omitting the field does not
restore config.toml. Drop default|auto|clear success paths and require an
explicit friendly|pragmatic|none instead.
Co-authored-by: Cursor <cursoragent@cursor.com>
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(web): show Auto instead of Default for Cursor model picker
Cursor CLI uses `auto` for automatic model selection; labeling it
"Default" in HAPI was confusing and inconsistent with `agent --list-models`.
Fixes#1247
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(web): align Cursor unavailable copy with Auto label
Co-authored-by: Cursor <cursoragent@cursor.com>
* chore: re-trigger Codex PR review
Co-authored-by: Cursor <cursoragent@cursor.com>
* docs(web): update Cursor catalog JSDoc for Auto label
Co-authored-by: Cursor <cursoragent@cursor.com>
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(web): expose Codex Fast and Plan on Create Session
Wire serviceTier and collaborationMode through spawn so Create can set
the same Codex options chat Settings already supports (#1015).
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(cli): forward collaborationMode through machine spawn RPC
Create Session Plan was accepted by the hub but dropped in apiMachine
before buildCliArgs; also preserve collaborationMode on resume spawn.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(cli): correct stopSession mock type in spawn RPC test
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(web): keep Fast mode across Create draft restore while models load
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(web): preserve pending Fast selection
* fix: apply Fast and Plan to imported Codex sessions
* test: narrow imported Codex session id
* fix: forward explicit Standard service tier
* fix: integrate create-session controls with current main
* test: close Codex RPC suite
* fix: preserve existing session spawn field
* fix(web): integrate Codex controls with current New Session form
* fix(web): reconcile draft types and submit state
* fix(hub): integrate spawn arguments with current resume flow
* test(cli): isolate spawn RPC suite
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
* test: reproduce issue #786
* fix: load extra headers from settings (closes#786)
* test: cover extra header precedence and redaction
* fix: redact persisted extra headers in diagnostics
* test: cover runner extra header identity
* fix: restart runner when extra headers change
Surface the same reasoning label already shown in the composer StatusBar
in the top SessionHeader for codex/opencode sessions. Also show an
explicit Fast badge only when serviceTier is fast (#1004-aligned).
Closes#1015 (header display portion).
Co-authored-by: Cursor <cursoragent@cursor.com>
* feat(gemini): remove launchable Gemini CLI agent, keep sessions readable
Google sunset the consumer Gemini CLI (Pro/Ultra/free tiers stopped
serving requests 2026-06-18). This removes the ability to launch/create
Gemini CLI sessions while keeping existing stored Gemini sessions fully
readable in the web UI.
Removed (no longer launchable):
- cli/src/gemini/ runtime (runGemini, loop, local/remote launchers,
session, ACP backend, config, scanner) + GeminiDisplay ink view
- `hapi gemini` command + registry entry + usage line
- runner spawn branch & buildCliArgs mapping now reject gemini with a
clear error; resume dispatch throws a clear "no longer supported" error
- gemini dropped from the new-session agent selector via new
CREATABLE_AGENT_FLAVORS, and from preferred-agent defaults
Kept (read path — existing sessions still validate, load, render):
- `gemini` in AGENT_FLAVORS / AgentFlavorSchema, FLAVOR_CAPS / FLAVOR_LABELS
- AgentFlavorIcon badge, model-option labels, ACP message normalization,
metadata.geminiSessionId, hub session dedup/resume-id
Note: the Gemini *Live voice* backend is a separate feature and is
untouched.
Adds read-guarantee tests (stored gemini validates; excluded from
creatable). typecheck + full suite green.
via [HAPI](https://hapi.run)
Co-Authored-By: HAPI <noreply@hapi.run>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(gemini): reject gemini resume before handoff (#953 review)
HAPI Bot [Major]: `hapi resume <active-gemini-session>` called
handoffSessionToLocal() — which tells the running remote agent to exit —
before reaching the gemini-unsupported throw in dispatchLocalResume, so
it could stop the live/readable session and then fail locally.
Move the gemini guard into resumeCommand.run before the handoff, so an
active Gemini session is left running/readable instead of being stopped.
Keep the dispatch-layer guard as defense-in-depth. Adds a regression test
asserting handoffSessionToLocal is not called for an active gemini target.
via [HAPI](https://hapi.run)
Co-Authored-By: HAPI <noreply@hapi.run>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(gemini): harden against stale gemini input (#953 review)
Two [Minor] follow-ups from HAPI Bot:
- newSessionFormDraft: coerce a restored browse draft's agent to a
creatable flavor, so a pre-removal 'gemini' draft cannot submit
agent:'gemini' even though the selector no longer offers it.
- buildCliArgs: reject 'gemini' explicitly instead of silently falling
through to the 'claude' command if the exported helper is reused
outside the guarded spawnSession path.
Updated the buildCliArgs precedence test to a creatable agent and added
a test asserting buildCliArgs('gemini') throws.
via [HAPI](https://hapi.run)
Co-Authored-By: HAPI <noreply@hapi.run>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(gemini): reset dependent draft fields when coercing stale agent (#953 review)
Follow-up [Minor]: coercing a stale gemini draft's agent to claude left
model/base/effort untouched, so a { agent:'gemini', model:'gemini-2.5-pro' }
draft restored as claude *with* a Gemini model, which handleCreate() then
sent to the runner. Now reset model / cursorSelectedBase / effort /
modelReasoningEffort to defaults whenever the agent is coerced.
Adds a regression test.
via [HAPI](https://hapi.run)
Co-Authored-By: HAPI <noreply@hapi.run>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(gemini): tombstone `hapi gemini` so it errors clearly (#953 review)
HAPI Bot [Major]: after removing geminiCommand from the registry,
resolveCommand() treats `gemini` as an unknown subcommand and falls
through to the default Claude command (forwarding "gemini" as an arg),
so `hapi gemini` silently started Claude instead of reporting the sunset.
Add an explicit tombstone `gemini` command that prints the sunset error
and exits 1.
via [HAPI](https://hapi.run)
Co-Authored-By: HAPI <noreply@hapi.run>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* test(web): assert AgentSelector hides the sunset Gemini agent (#953)
Render regression test confirming the new-session AgentSelector offers
exactly CREATABLE_AGENT_FLAVORS and never shows a Gemini radio.
via [HAPI](https://hapi.run)
Co-Authored-By: HAPI <noreply@hapi.run>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
---------
Co-authored-by: HAPI <noreply@hapi.run>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
* feat(web): drag-and-drop files onto chat panel to add as attachments
Closes#935
Adds a `useDragOver` hook that detects when a file is being dragged over
the browser window and suppresses the browser's default file-open
behaviour for drops outside the accept zone.
A new `DragDropZone` component wraps the inner `AssistantRuntimeProvider`
content in `SessionChat`. It shows a semi-transparent overlay (dashed
border + "Drop to attach" label) on the right-side chat panel as soon as
any file drag is detected — regardless of where the pointer is on the
page. Dropping on the right panel adds the files as composer attachments
via the existing `api.composer().addAttachment()` path. Drops on the left
sidebar are suppressed (no navigation, no attachment).
via [HAPI](https://hapi.run)
Co-Authored-By: HAPI <noreply@hapi.run>
* fix(web): disable drag-drop zone when pendingSchedule is active
The backend rejects requests with both scheduledAt and attachments.
DragDropZone now respects pendingSchedule the same way paste and the
attach button do — disabled=true suppresses the overlay, sets
dropEffect='none', and skips addAttachment on drop.
via [HAPI](https://hapi.run)
Co-Authored-By: HAPI <noreply@hapi.run>
* fix(web): harden drag-drop default-action handling
Address HAPI Bot review on #936:
- useDragOver: cancel the browser's default file-open/navigation on the
document-level `drop` event for file payloads, not only on `dragover`.
Preventing default on `dragover` alone still lets the browser open a
file dropped outside any zone (e.g. the sidebar), which could unload
the app.
- DragDropZone: only preventDefault when the drop payload actually
contains files, so non-file drops (e.g. dragging selected text into
the composer) keep their default browser behaviour.
Add regression tests for both.
via [HAPI](https://hapi.run)
Co-Authored-By: HAPI <noreply@hapi.run>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(web): use Simplified Chinese for composer.dropToAttach in zh-CN
Address HAPI Bot review on #936: the new zh-CN string used Traditional
Chinese forms (放開以附加檔案) in the Simplified Chinese locale, which is
inconsistent with neighbouring keys (e.g. composer.attach = 添加文件).
Use 松开以添加文件 to match.
via [HAPI](https://hapi.run)
Co-Authored-By: HAPI <noreply@hapi.run>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
---------
Co-authored-by: HAPI <noreply@hapi.run>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
* test: reproduce issue #864
Assert Cursor error paths emit agent error payloads and web UI renders
them as warning-styled events instead of neutral session messages.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(cursor): surface agent errors with warning styling in web UI (closes#864)
Route Cursor stderr, init, prompt, and legacy exit failures through
sendAgentMessage({ type: 'error' }) and teach the web chat layer to
render error events with a warning icon instead of neutral info text.
Co-authored-by: Cursor <cursoragent@cursor.com>
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
Without an explicit -fast suffix, inferSkuParamHints returned no fast hint, so
matchCliSkuToAcpWireId tied between fast=true and fast=false wires and kept
the first one. For composer-2.5 that meant the picker's "non-fast" sku silently
resolved to composer-2.5[fast=true] — the same wire the fast sku resolves to —
producing the "selected but no response" symptom in #883.
Treat absence of -fast as fast=false so base-only skus pick the slow variant
and round-trip back to the matching radio.
Fixes#883
via [HAPI](https://hapi.run)
Co-authored-by: HAPI <noreply@hapi.run>
* test: reproduce issue #901 (active-only filter + paginated show more)
* fix: active-only session filter + paginated 'Show N more' (closes#901)
Add a persisted 'Active sessions only' toggle in Settings -> Display that
hides inactive sessions in the sidebar while keeping the selected session
visible. Change 'Show N more' to reveal one batch (preview-limit size) per
click instead of expanding every hidden session at once, with 'Show less'
to collapse back to the initial preview.
via [HAPI](https://hapi.run)
Co-Authored-By: HAPI <noreply@hapi.run>
---------
Co-authored-by: HAPI <noreply@hapi.run>
* fix(web): persist file explorer expanded tree and scroll position across navigation
Expanded folder state and scroll position in the Directories tab were
stored only in local React state, so navigating to a file and back
would reset the tree to the root and scroll to top.
Now both are saved to sessionStorage (keyed by sessionId) on every
change and restored on remount, so the explorer resumes exactly where
the user left off.
Closes#910
via [HAPI](https://hapi.run)
Co-Authored-By: HAPI <noreply@hapi.run>
* fix(web): key DirectoryTree by sessionId to prevent stale expanded state across sessions
When navigating between sessions, React can reuse the same DirectoryTree
instance. The useState lazy initializer only runs on first mount, so the
tree would hydrate with the wrong session's expanded set and then
overwrite the new session's storage key.
Adding key={sessionId} forces a fresh mount per session.
via [HAPI](https://hapi.run)
Co-Authored-By: HAPI <noreply@hapi.run>
---------
Co-authored-by: HAPI <noreply@hapi.run>
Adds a download icon button to the file viewer toolbar (next to copy-path).
Clicking it decodes the existing base64 file content into a Blob and triggers
a browser download — no new backend endpoint required. Works for text, binary,
and image files. Button is hidden until the file has loaded successfully.
Closes#924
via [HAPI](https://hapi.run)
Co-authored-by: HAPI <noreply@hapi.run>
* test: reproduce issue #866
* feat(web): OLED Black theme + per-appearance custom colors (closes#866)
Add an explicit OLED Black appearance (true #000 canvas, border-based
elevation) alongside system/dark/light, and a curated "key color"
customizer. Each key color (background, surface, text, hint, accent,
border, user bubble) cascades to its --app-* tokens and is stored per
appearance so a color tuned for light never leaks onto pure black.
via [HAPI](https://hapi.run)
Co-Authored-By: HAPI <noreply@hapi.run>
---------
Co-authored-by: HAPI <noreply@hapi.run>
OutgoingMessageQueue.scheduleProcessing() defers socket.emit() via
setTimeout(fn,0) — a macrotask. The Claude SDK's nextMessage() callback
runs in a microtask chain, which executes before that macrotask fires.
This means messages-consumed for turn N+1 can be sent to the hub before
the queued agent messages from turn N have been emitted. The hub stamps
invokedAt on the N+1 user message at receive time, and then stores the
late-arriving agent messages with created_at > invokedAt_N+1. Since
compareMessages sorts by invokedAt ?? createdAt ascending, those agent
messages sort permanently below the N+1 user message.
Fix: await messageQueue.flush() at the top of nextMessage() so all
pending outgoing agent messages are sent through the socket before
messages-consumed is dispatched.
Closes#908
via [HAPI](https://hapi.run)
Co-authored-by: HAPI <noreply@hapi.run>
* test: reproduce issue #927
* fix(hub): cache generated images + raise socket buffer cap (closes#927)
Generated-image display was slow and could silently fail:
- The /generated-images route sent `Cache-Control: no-store`, so every
card remount (session switch, scroll, reload) re-ran the full HTTP ->
socket.io RPC -> base64 round-trip. The bytes for an imageId are
immutable, so serve them `private, max-age=31536000, immutable` + ETag.
- socket.io / bun-engine `maxHttpBufferSize` was left at the 1 MB default,
while the MCP tool accepts images up to 25 MB. The base64 CLI -> hub ack
frame for anything above ~750 KB raw exceeded the cap and was dropped.
Raise the buffer to comfortably carry the largest allowed image.
via [HAPI](https://hapi.run)
Co-Authored-By: HAPI <noreply@hapi.run>
* fix(hub): short-circuit generated-image revalidation with a 304 (#927)
The imageId is an immutable content fingerprint, so use it as the ETag and
answer If-None-Match with 304 before issuing the readGeneratedImage RPC.
This makes the ETag actually useful: revalidation now skips the CLI socket
round-trip entirely, and still serves correctly even after the image was
evicted from the CLI's in-memory store.
via [HAPI](https://hapi.run)
Co-Authored-By: HAPI <noreply@hapi.run>
* fix(web): stabilize ApiClient identity across token refresh (#927)
The ApiClient was rebuilt whenever the token value changed (useMemo dep),
even though every request already reads the live token via getToken/tokenRef.
On a flaky/remote connection, repeated 401s -> onUnauthorized -> forced
refresh churned `api`'s identity, which remounts everything keyed on it:
VoiceBackendSession ([props.api]) -> Voice re-register spam, and
GeneratedImageCard ([ctx.api]) -> per-image refetch storm, feeding a
render avalanche. Depend on auth presence (hasToken) instead.
Reproduced with a useAuth hook test (red->green); full web suite stays green.
via [HAPI](https://hapi.run)
Co-Authored-By: HAPI <noreply@hapi.run>
---------
Co-authored-by: HAPI <noreply@hapi.run>
* test: reproduce issue #898 (Codex fast mode service tier)
* fix(codex): add Fast mode (service tier) toggle and /fast command (closes#898)
* feat(codex+web): Fast mode UI toggle with full persistence
Wires the Codex Fast mode (service tier) end-to-end so it can be toggled
from the web composer and survives reload/handoff:
- shared: serviceTier on Session/SessionPatch, session-alive payload,
resume target, and a SessionServiceTierRequest schema
- cli: AgentSessionBase carries serviceTier through keepAlive; runCodex
syncs it to the session instance
- hub: service_tier column (schema v10 + migration), store setter,
sessionCache + syncEngine plumbing, POST /sessions/:id/service-tier
- web: api.setServiceTier + mutation, a Fast/Standard toggle in the
composer settings (gated to Codex GPT-5.5/5.4), and StatusBar now
reflects the real tier instead of the effort heuristic
Refs #898
* fix(codex): preserve unset/persisted service tier on startup keepalive
Addresses HAPI Bot [Major] on PR #904: applyCurrentConfigToSession ran
setServiceTier(currentServiceTier ?? null) on wrapper-ready, collapsing the
untouched `undefined` state into explicit Standard. The immediate
setCollaborationMode keepalive then persisted serviceTier: null, silently
downgrading resumed Fast sessions and disabling account-default Fast.
- Seed currentServiceTier from the persisted session (sessionInfo.serviceTier),
so a resumed Fast thread keeps running Fast.
- Only call setServiceTier when the tier is explicit (!== undefined), preserving
the three-state omit semantics at the keepalive boundary.
- Add regression tests: persisted Fast is re-asserted; untouched omits the tier.
* feat(codex+web): gate Fast toggle on catalog-advertised service tier
The Fast toggle was gated on a model-name regex (gpt-5.5/5.4), which still
showed a no-op control to API-key users — Fast credits only apply with
ChatGPT login. Codex's model/list catalog advertises the service tiers
actually available for each model in the current auth/plan context, so gate
on that instead:
- cli: capture serviceTiers (ids) per model in ModelListItem + normalizeModel
- shared: CodexModelSummary.serviceTiers (flows through the existing
getSessionCodexModels pass-through; no hub change needed)
- web: codexModelAdvertisesFastTier(sessionModel, models) replaces the regex;
SessionChat gates the toggle on it (hidden while the catalog is
loading/errored). The toggle now only appears when toggling it will
actually take effect.
Refs #898
* fix(codex): make explicit Standard service tier sticky across resume
Addresses HAPI Bot [Major] (round 2): a single persisted null conflated
"untouched" with "explicit Standard". A user who turned Fast off persisted
null, but startup mapped null -> undefined (untouched) and omitted serviceTier,
so an account/thread-default Fast could silently return after restart/resume.
Introduce a distinct stored representation:
- 'fast' / 'standard' are explicit user choices; null/undefined = untouched.
- Translate 'standard' -> Codex app-server serviceTier: null ONLY when building
thread/turn params (toAppServerServiceTier); untouched omits the field.
- /fast off now stores 'standard'; the web Standard option sends 'standard'.
- Tighten SessionServiceTierRequest to enum(['fast','standard']) so stray tier
strings are never forwarded.
Tests: sticky-Standard-on-resume regression; turn/thread params translate
'standard'->null and omit on untouched; hub route applies fast/standard and
rejects unsupported values + local sessions.
Refs #898
* fix(codex): recognize real Fast tier (id 'priority', name 'Fast') in catalog gate
Live E2E against an authed Codex session revealed the model catalog advertises
the Fast tier with id 'priority' and display name 'Fast' (not id 'fast'), so the
/fast/i gate — which only saw tier ids — wrongly hid the toggle for valid
ChatGPT users on gpt-5.5/gpt-5.4. Capture both the tier id and name as
lowercased tokens so the existing name-based match recognizes 'Fast'. The sent
value stays 'fast' (the documented service_tier value / raw additionalSpeedTiers
request tier). Verified end-to-end: gpt-5.5/gpt-5.4 gate on, gpt-5.4-mini off.
Refs #898
* fix(codex): preserve service tier across session resume
Resuming a Codex session spawns a fresh session (serviceTier null) and merges
the old one in. Unlike model/effort/permissionMode, serviceTier was neither
threaded through the resume spawn nor preserved in mergeSessionData, so a
resumed Fast (or explicit Standard) session silently reverted to the account
default.
Thread serviceTier through the spawn path like its siblings:
- hub: resumeSession passes session.serviceTier to spawnSession; rpcGateway +
syncEngine carry it in the spawn RPC payload; mergeSessionData preserves it
old->new (safety net).
- cli: SpawnSessionOptions.serviceTier; apiMachine forwards it; buildCliArgs
emits --service-tier for codex; the codex command parses it; runCodex seeds
currentServiceTier from the spawn override first (opts.serviceTier ??
sessionInfo.serviceTier), so a resumed thread immediately runs the right tier.
Verified end-to-end: set Fast -> kill process -> reopen -> resumed session (new
id) still runs Fast. Tests: buildCliArgs --service-tier (codex only), runCodex
spawn-override seed, mergeSessionData service-tier preservation.
Refs #898
* fix(codex): send advertised 'priority' tier id for Fast, not 'fast'
The model catalog advertises the Fast tier with request id 'priority' (display
name 'Fast'), and OpenAI docs confirm service_tier='fast' maps to the request
value 'priority'. The app-server serviceTier override is a raw request value
that does not validate unknown strings (a live probe accepted 'bogus-xyz'), so
sending 'fast' risks being silently ignored — no Fast applied.
Translate the stored 'fast' state to app-server 'priority' at the thread/turn
param boundary (toAppServerServiceTier); the stored/UI/command representation
stays 'fast'/'standard'. Verified live: a turn with serviceTier='priority' runs
and consumes the Fast-tier rate budget.
Addresses HAPI Bot [Major]. Refs #898
* fix(codex): validate --service-tier CLI value (fast|standard)
Addresses HAPI Bot [Minor]: the internal --service-tier spawn arg accepted any
non-empty string, unlike the web /service-tier enum, so a malformed value could
be seeded into currentServiceTier and persisted via keepalive. Parse it to
'fast'|'standard' and reject anything else, matching the web endpoint.
Refs #898
* fix(opencode): use ACP-reported reasoning effort options
Expose thought_level options from OpenCode ACP to the web UI via RPC/API
instead of hardcoded presets, and validate effort values before setConfigOption.
Fixes#852
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(opencode): sync hub effort after coerced setConfigOption
When resolveThoughtLevelEffort falls back to a different supported value,
roll back session state after a successful ACP update so keepalive and the
web UI do not keep advertising the rejected effort.
Co-authored-by: Cursor <cursoragent@cursor.com>
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
Pre-write resume token into session metadata before awaiting session/load
so hub and web see cursorSessionId immediately after resume spawn.
Fixes#834
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(web): dedupe sidebar sessions by flavor resume id
Wire deduplicateSessionsByAgentId into SessionList and resolve cursor
threads via cursorSessionId so resume/archive no longer shows duplicate
inactive rows for the same ACP session.
Fixes#833
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(web): use SessionSummary.agentSessionId for sidebar dedup
SessionList only receives SessionSummary from the API; native ids like
cursorSessionId are already mapped into metadata.agentSessionId by
toSessionSummary. Drop resolveAgentSessionIdFromMetadata to fix typecheck.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(web): hide inactive empty session stubs in sidebar
Filter inactive rows with no agentSessionId and no title signal before
grouping sessions, and expose lifecycleState on SessionSummary for future
sidebar rules. Completes the #833 P0 follow-up alongside agent-id dedup.
Fixes#833
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(web): scope sidebar dedup key by flavor
Prevent cross-flavor collisions when flattened agentSessionId retains a
stale native id. Add regression test and relax claudeRemote CI timeout.
Fixes#833
Co-authored-by: Cursor <cursoragent@cursor.com>
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
Fixes incomplete cliModelSkus while agent acp holds the CLI lock (#831) and
replace single-pid ACP lock with cross-process refcount (#832). Web picker
merges machine/session catalogs and waits for SKU readiness before showing
variant labels.
Fixes#831Fixes#832
Co-authored-by: Cursor <cursoragent@cursor.com>
* feat(cli,web,hub): migrate Cursor remote sessions to ACP with model/effort pickers
Move stream-json remote launcher to legacy path and add ACP launcher with
set_config_option model/mode sync, optimistic keepalive on config changes, and
shared catalog caching. Web gets dual base/effort Cursor pickers for session and
new-session flows; hide composer status bar when Cursor sends no usage_update.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(cli,web,shared): Cursor model picker — ACP wires + CLI sku variants
Enrich the web/mobile picker with agent --list-models SKUs grouped under
ACP wire bases, fix session-open base highlight, and keep catalog discovery
safe while the ACP transport holds the CLI lock.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(cursor-acp): apply ACP default model when web resets to Default
Web sends model: null for Default; push session/set_config_option with the
ACP default[] wire so Cursor backend matches hub state. Regression tests
for setModel(null) and applyModelConfig(null).
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(acp): clear stale agent-acp lock when owning process is gone
Check lock pid with signal 0; remove orphaned lock dirs after SIGKILL or
crash so listCursorModels can run cold probes again. Regression tests for
guard and catalog discovery.
Co-authored-by: Cursor <cursoragent@cursor.com>
* test(cursor): use live pid for ACP lock handler tests
Stale-lock cleanup clears dead pids; handler tests must simulate an
active lock with the current process pid to avoid cold probes/timeouts.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(acp): scope agent CLI lock guard to Cursor agent command only
Gemini/OpenCode/Kimi ACP sessions must not register agent-acp-active;
that blocked listCursorModels while unrelated backends were running.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(hub,web): reject Cursor model changes for local sessions
Hub returns 409 when controlledByUser is set, matching Codex. Web hides
model and variant pickers for local Cursor sessions so users do not hit
a dead RPC path. Document pre-push-review in AGENTS.md.
Verified: bun typecheck; bun run test (919 cli + 243 hub + 768 web + 46 shared).
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(web): send stable ids for Cursor ask_question replies
Parse and submit question.id and option.id so ACP receives keys like
{ approach: ['a'] } instead of index/label. Verified: bun typecheck && bun run test.
Co-authored-by: Cursor <cursoragent@cursor.com>
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
`PermissionHandler` stored its own `permissionMode` field and only updated
it inside `handleModeChange`, which is called when a new batch is pulled
from the queue. The `SetSessionConfig` RPC (web dropdown changes) updates
`runClaude.ts`'s `currentPermissionMode` and the session keepalive
metadata, but never reaches the handler — so switching to Yolo mid-turn
left `canCallTool` checking the stale mode and still prompting for
approval. Closes#735.
Drop the stored field and read live from `session.getPermissionMode()`,
mirroring how the OpenCode permission handler already works. Override
`Session.getPermissionMode()` in `claude/session.ts` to return the
Claude-narrow `PermissionMode`, sound because the matching
`setPermissionMode` setter only accepts that subset.
via [HAPI](https://hapi.run)
Co-authored-by: HAPI <noreply@hapi.run>
PR #756 added a mid-turn emit in captureUsageUpdate to surface live
context usage via the web status bar. Testing against OpenCode 1.15.11
on a real session shows OpenCode emits a single usage_update per turn,
within ~1ms of session/prompt resolving — never during streaming.
That makes the mid-turn path dead code for OpenCode (and for any other
ACP agent that follows the same pattern). It also persists a useless
inputTokens:0/outputTokens:0 token_count message that gets immediately
overwritten by the finalize emit, churning the session history.
Drop the mid-stream emit and the activeOnUpdate plumbing it required.
Keep the finalize fallback for agents that don't return a usage block
on session/prompt (slash-handled turns, errored turns). The persistent
"live" counter requires the agent to emit usage_update during streaming;
filed upstream against anomalyco/opencode.
Refs #750
via [HAPI](https://hapi.run)
Co-authored-by: HAPI <noreply@hapi.run>
* fix(acp): flush straggler chunks promptly after session/prompt returns
After session/prompt returns, HAPI drains buffered agentMessageChunk text
and marks the turn complete, but leaves the message handler alive. Models
with long streaming tails (DeepSeek, GPT-5.5) continue to push chunks
after that drain, causing text to accumulate in the buffer and only appear
when the next user prompt triggers the pre-prompt drain — showing up in
the wrong turn with broken markdown.
Start a 50ms interval timer after the post-prompt drain that keeps calling
drainBuffers() on the live handler for up to 6 seconds, so straggler
chunks are emitted within one poll tick instead of waiting for the next
prompt. The timer is cancelled when the next prompt starts (pre-prompt
drain replaces the handler) or on disconnect.
Fixes#609. Also applies to Gemini and Kimi which share the same
AcpSdkBackend code path.
via [HAPI](https://hapi.run)
Co-Authored-By: HAPI <noreply@hapi.run>
* fix(acp): gate next turn's handler swap on previous turn's late drain
Addresses the github-actions review on #730: the fire-and-forget late-flush
timer let `prompt()` resolve while stragglers were still possibly arriving,
so a rapid follow-up prompt could either drop those chunks (during the
old null-handler gap) or leak them into the new turn's onUpdate.
Pre-prompt phase now keeps the previous turn's handler alive across the
quiet wait (bounded by LATE_FLUSH_WINDOW_MS) and swaps in a single phase
immediately before sending the new session/prompt. The post-prompt late
flush timer is unchanged — it still emits idle-window stragglers promptly
without delaying the ready signal or `setModel` / `setConfigOption`.
Adds three regression tests: late-chunk flushing within the window,
pre-prompt straggler attribution to the previous turn's onUpdate, and
disconnect cancelling the timer. Removes now-unused
PRE_PROMPT_UPDATE_DRAIN_TIMEOUT_MS.
via [HAPI](https://hapi.run)
Co-Authored-By: HAPI <noreply@hapi.run>
* fix(acp): await late drain so ready never fires before stragglers emit
Follow-up to bot's repeated MAJOR on #730: even with the pre-prompt gate,
the fire-and-forget late-flush timer let `prompt()` resolve before slow
tails finished, so the launcher's `ready` signal (and any user follow-up
queued against it) raced with text still being emitted to the current
turn's onUpdate.
Replace the setInterval timer with a synchronous `drainLateBuffers()`
awaited in `prompt()`'s finally before turn_complete is sent. It polls
drainBuffers every LATE_FLUSH_INTERVAL_MS so the UI keeps streaming
smoothly during the wait, and exits early once the model has been quiet
for LATE_FLUSH_QUIET_PERIOD_MS (250 ms — adds negligible latency to fast
models like Claude whose tail is typically <100 ms) or the
LATE_FLUSH_WINDOW_MS upper bound (6 s) elapses.
Side effects:
- Restore PRE_PROMPT_UPDATE_DRAIN_TIMEOUT_MS (1200 ms): the pre-prompt
drain is now just a safety net since the post-prompt wait guarantees
the previous turn is quiet by the time the next prompt starts.
- Drop the `lateFlushTimer` field, `startLateFlushTimer`,
`stopLateFlushTimer`, and their disconnect/pre-prompt cleanup calls.
- Update the "emits straggler chunks" test to assert ordering before
turn_complete, and add a fast-path test confirming the drain exits
promptly when the model is quiet.
via [HAPI](https://hapi.run)
Co-Authored-By: HAPI <noreply@hapi.run>
* fix(acp): anchor late-flush quiet window to entry, not stale lastSessionUpdateAt
Bot's third MAJOR on #730: drainLateBuffers() compared elapsed time
against lastSessionUpdateAt, which can already be older than
LATE_FLUSH_QUIET_PERIOD_MS by the time the method starts — e.g. when the
model emits chunks early in the turn, pauses, then sends stopReason. In
that case the first loop iteration sees a stale "quiet" reading and
returns immediately, missing any straggler that arrives just after
session/prompt resolves; the chunk then sits in the buffer until the
next prompt's pre-prompt drain.
Anchor the quiet check to max(lastSessionUpdateAt, entry time) so we
always observe at least one quiet period from method entry regardless of
when the last chunk was. Adds a regression test that fires a chunk
early, awaits a 200ms pause, schedules a post-resolution straggler, and
asserts it lands before turn_complete.
via [HAPI](https://hapi.run)
Co-Authored-By: HAPI <noreply@hapi.run>
* docs(acp): correct LATE_FLUSH_QUIET_PERIOD_MS comment after entry-anchor fix
The previous note claimed the 250ms quiet check "exits early for fast
models, adding negligible latency". That was true before commit 512d6a4
when the check compared against lastSessionUpdateAt; with the entry-time
anchor, drainLateBuffers always observes at least one full quiet period.
Document that this minimum wait is the price of catching post-resolution
stragglers from paused-mid-turn models.
via [HAPI](https://hapi.run)
Co-Authored-By: HAPI <noreply@hapi.run>
---------
Co-authored-by: HAPI <noreply@hapi.run>
* chore(web): upgrade @tanstack/react-router to ^1.170.8
Fixes QuotaExceededError in scroll restoration: upstream @tanstack/react-router
>=1.145.6 wraps sessionStorage.setItem with try-catch, preventing the crash when
scroll restoration cache exceeds quota.
Refs: #683, #716, #721
* fix(web): adapt scrollStorageGuard to @tanstack/router-core >=1.145.6 API
`scrollRestorationCache` was removed from the public exports; replace with
`storageKey` import and simplify `hardResetScrollRestorationPersistedState`
to a plain `removeItem`. Remove the now-stale in-memory cache sync path and
its associated tests. Upstream try-catch (>=1.145.6) covers crash prevention;
this guard continues to proactively prune sessionStorage.
via [HAPI](https://hapi.run)
Co-Authored-By: HAPI <noreply@hapi.run>
---------
Co-authored-by: HAPI <noreply@hapi.run>
* feat(opencode): support plan mode
* feat(opencode): support reasoning effort
* feat(opencode): surface context usage in web
Bridge OpenCode ACP usage updates into the existing token-count pipeline so the web status bar can show live context and cache information without a separate UI path.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(opencode): restrict plan mode to remote, rollback reasoning effort on failure
- Block local OpenCode plan startup (tools not enforced in local path)
- Allow remote OpenCode plan only (ACP permission handler denies tools)
- Guard web /permission-mode endpoint for local OpenCode plan sessions
- Rollback session reasoning effort when OpenCode rejects set_config_option
- Wire rollback callback through opencodeLoop to runOpencode closure
- Add tests: local plan rejected, remote plan allowed, web guard, effort rollback
* fix(web): auto-retry OpenCode models query to populate model selector without refresh
- Retry early failures (RPC may still be registering on new sessions)
- Poll briefly until availableModels is non-empty
- Stop polling once model options are discovered
- Add tests for retry/poll/stop policy
* fix(opencode): cap model discovery polling
---------
Co-authored-by: Cursor <cursoragent@cursor.com>