mirror of
https://github.com/wu736139669/hapi.git
synced 2026-11-07 01:21:37 +00:00
7031d60eb4a72ddd76037a459902f2f3d13b7632
299
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
7031d60eb4 |
fix(opencode): expose model-specific reasoning effort options (#1716)
* fix(cli): discover opencode thought_level via set_config_option on model switch * fix(cli): apply and refresh opencode model switches so thought_level stays discoverable * fix(web): track opencode effort options across model switches * feat(cli,hub,web): dynamic opencode effort options in new-session form * test(cli): avoid platform-specific process event narrowing * fix(opencode): address variant discovery review findings * fix(opencode): synchronize effort options with model targets * fix(opencode): roll back rejected model targets * fix(cli): guard opencode variant probe workspace paths * fix(web): clear stale opencode effort on model switch * fix(cli): clear stale opencode effort metadata * fix(web): reset stale effort options on model switch * fix(web): reset opencode effort poll budget * test(web): enforce opencode effort poll budget |
||
|
|
f27e58741b |
feat(web,ios,android): let Claude pick a permission mode when creating a session (#1751)
* refactor(web): route create-form permission control through one native-select predicate Extract usesNativePermissionSelect(flavor) (grok || codex-family, matching the existing iOS/Android predicate of the same name) and route PermissionField's select-vs-toggle gate through it instead of an inline condition. Rename the codex-family-only state codexFamilyPermissionMode to nativePermissionMode since it now backs a shared predicate, not just the codex family. Behavior is unchanged: any stale sessionStorage draft written under the old codexFamilyPermissionMode key has no value under the new key and falls back to 'default', which only matters within a single browser tab's lifetime. * feat(web): let Claude pick a permission mode when creating a session Claude was the only create-form flavor still on the global HAPI YOLO toggle while grok and the codex family got the native permission select, so there was no way to start a session in Plan Mode without creating it first and switching the mode from the composer. usesNativePermissionSelect now gates the control for claude as well, and the spawn body carries permissionMode (including 'default') instead of yolo, which is the shape the other native-select flavors already send. The stored hapi:newSession:yolo preference is bridged into the select rather than dropped, but only for the flavors that have actually moved onto it (LEGACY_YOLO_BRIDGE_AGENTS = codex, claude). copilot, gemini, kimi and opencode moved earlier and settled on 'default'; re-enabling Yolo for them now would widen permissions rather than migrate a preference. This narrows the sessionStorage draft bridge too, which until now fired for the whole codex family with no allow-list, so their draft restores yield 'default' instead of 'yolo' — same-tab-lifetime state only. Claude and the codex family share one nativePermissionMode state and their mode sets do not overlap, so the existing agent-change reset plus the flavor filters in the draft loader and the stored launch settings are what keep a codex mode out of a Claude spawn. Adds the regression test that pins it: pick a mode under codex, switch to Claude, create, assert the payload carries 'default'. * feat(ios): let Claude pick a permission mode when creating a session Extend usesNativePermissionSelect to include claude alongside grok and the codex family, matching the web change. buildSpawnRequest now derives both yolo and permissionMode from that single predicate instead of two separate local flags, so claude sends permissionMode (including 'default') and no longer sends yolo. Unlike web, iOS carries no persistent YOLO preference across sessions to migrate — the toggle only lives in the in-memory form or a draft deleted on success — so there is no bridging logic to add here. * feat(android): let Claude pick a permission mode when creating a session Extend usesNativePermissionSelect to include claude alongside grok and the codex family, matching the web and iOS changes. buildSpawnRequest derives both yolo and permissionMode from that single predicate, so claude sends permissionMode (including 'default') and no longer sends yolo. Unlike web, Android has no persistent YOLO preference to migrate: the toggle only lives in the form draft, which is deleted once a session is created. The agent-switch test asserted claude renders the YOLO toggle; it now checks the native select for claude and keeps the toggle assertion on cursor, which still carries it. |
||
|
|
33f51fd0ab | feat(web): add configurable PWA taskbar badge (#1759) | ||
|
|
4dac00ccd9 |
feat(web): add bulk mark-all-read session action (#1773)
* feat(web): add bulk mark-all-read session action * fix(web): mark duplicate sessions as read * fix(web): count hidden duplicate unread sessions * fix(web): exclude empty session stubs from bulk read |
||
|
|
8357da0a9d |
feat(codex): support backend-authorized Luna Reserve fallback (#1780)
* feat(codex): reconcile Luna Reserve fallback and conditional usage * fix(codex): preserve queued settings and reconcile Reserve sessions |
||
|
|
52a76a0bc4 |
fix(web): clarify session summary setting copy (#1760)
* fix(web): clarify session summary settings * fix(web): clarify summary emission copy |
||
|
|
17ee052d9a |
fix(web): preserve the visible chat window during rewind (#1766)
* fix(web): preserve chat window during rewind * fix(web): scope rewind invalidation preservation * fix(web): clear unknown rewind boundaries * fix(web): deduplicate rewind invalidations * fix(web): retain rewind dedupe history |
||
|
|
aa0c1dc808 |
fix(codex): fail closed on ambiguous Web Rewind boundaries (#1707)
* fix(codex): fail closed on ambiguous Web Rewind boundaries * fix(codex): reject malformed native rewind history * fix(codex): reject duplicate rewind identifiers * feat(codex): offer Fork fallback for ambiguous rewind * fix(codex): gate rewind Fork fallback on exact native boundary * fix(codex): reject unresolved rewind boundaries safely * chore: trigger PR checks * fix(codex): gate safe rewind fallback on fork support * fix(codex): require complete user ids for rewind fallback |
||
|
|
e5a8212f4a | feat(session): validate agents and browse workspace directories | ||
|
|
a9d09cb3ff |
feat(web): support composer attachment drag reordering (#1682)
* feat(web): support composer attachment drag reordering * fix(web): handle forward attachment reordering * fix(web): enlarge file attachment touch targets * fix(web): preserve staged attachment order |
||
|
|
be1ef2a2e4 |
feat(dsh): integrate DeepSeek Harness through ACP (#1632)
* feat(dsh): add DeepSeek Harness ACP flavor * fix(dsh): update mobile flavor catalogs * fix(dsh): keep mobile spawn policy managed * fix(dsh): keep managed policy and prompt retry * fix(dsh): suppress unsupported runner policy flags * fix(dsh): align native managed-policy UX |
||
|
|
661e9b4eb7 | feat(web): show Claude round usage metadata (#1655) | ||
|
|
dd2e978fe3 |
feat(web): add mark unread session action (#1649)
* feat(web): add mark unread session action * fix(web): keep explicit unread state on selected sessions * fix(web): address mark unread review feedback * fix(web): preserve newer manual unread state |
||
|
|
fdf4fdaa38 |
feat(web,hub): make large conversation export warning-only (#1648)
* feat(export): warn before large session downloads * fix(export): surface server size errors |
||
|
|
0aebf39c78 |
fix(opencode): keep one stored message per reasoning stream (#1643)
* fix(acp): carry the live reasoning marker on the wire payload ACP agents stream thoughts a token at a time, so the handler coalesces them into a buffer and re-sends the whole buffer under a stable stream id every 250ms. The converter dropped the marker that says a payload is one of those throttled snapshots, leaving the hub unable to tell a replaceable snapshot from the settled message that closes the stream. Mirrors how the text variant already forwards streamSnapshot. * fix(hub): keep one stored message per reasoning stream OpenCode reasoning arrives as a series of growing snapshots sharing one stream id, and every snapshot was persisted as its own message. A 26h session reached 48,844 rows and 63MB, and because the web budgets a fixed number of messages, its 400-message window covered barely three minutes of conversation — scrolling up walked through duplicate snapshots instead of history. Retire a stream's earlier live snapshots once their replacement is stored. Sweeping only after the insert matters: the two statements are separate transactions, so clearing first would leave a window where a crash takes the whole stream. Only rows marked live are eligible and the replacement is spared, so a stream always keeps at least one row and the settled message that closes it is never removed. Live rendering is unchanged: the web still receives every snapshot and already folds them by stream id. * fix(web): spend the message window on conversation, not repeated snapshots The window budgets raw messages, but a reasoning stream renders as a single folded block no matter how many snapshots it arrived in. On sessions recorded before the hub started retiring them, those snapshots fill the window on their own: in one 26h session the newest 400 messages covered 202 seconds, so scrolling up paged through duplicates instead of history. Collapse each stream to its newest snapshot before trimming. Rendering is unchanged — the timeline already folds them by stream id — and rows without a stream id are never touched. * fix(ios,android): port reasoning-snapshot compaction to the native windows The window logic in HapiProtocol and :core:protocol is a one-to-one port of the web store, so collapsing superseded reasoning snapshots only on the web left the native windows budgeting raw snapshot rows. The hub stores one row per stream now, but a client that already holds the older snapshots still spends its window on them. Add the same stream-id reader and compaction to both ports, in the shape each already uses for agent-run rows, and pin the behaviour with a pagination fixture. Both fixture suites enumerate shared/fixtures/pagination from disk, so the ports cannot drift from the web again without CI saying so. |
||
|
|
f0e5ba9c0f |
feat(codex): mid-turn Steer via app-server turn/steer (#888) (#1606)
* feat(shared): steer capability gates and live steered signal schemas - STEERING_SUPPORTED_FLAVORS / isSteeringSupportedForSession gate which agents can deliver queued messages into the active turn (pi, codex, cursor ACP; legacy stream-json cursor excluded) - AgentState.steeringActive, DecryptedMessage.steered and messages-consumed live signal (never persisted by the hub) * feat(cli): queue reservations and steered messages-consumed option - MessageQueue2 gains takeByLocalId/restoreReservation/ beginReservationDispatch/commitReservation so an async steer can reserve a queued row without racing the main loop's turn/start drain - emitMessagesConsumed accepts steered: true to mark mid-turn delivery * feat(codex): mid-turn steer via app-server turn/steer (#888) - CodexAppServerClient.steerTurn + TurnSteerParams/Response types - CodexRemoteLauncher registers the steer-queued-message RPC handler: reserves the queued row, validates it against the active turn (no control commands, matching mode hash), injects via turn/steer with an epoch guard that invalidates in-flight steers on abort/cleanup - steeringActive agent state tracks the active-turn window - hub syncEngine gate opens to codex; messages-consumed relays steered * feat(web): Steered badge and steer gating for codex sessions - HappyUserMessage shows a ↳ Steered badge fed by the live messages-consumed steered signal, preserved across server echoes and refetches (mergeMessages carries the optimistic marker) - SessionChat gates canSteer via isSteeringSupportedForSession instead of the pi-only check - clearStaleQueuedStatus normalizes a queued status on an invoked message - fix(web): drop duplicate showSessionSummaryInChat in markdown test (upstream typecheck breakage) * fix(codex,shared): address bot findings on steer gate and ambiguous turn/steer - STEERING_SUPPORTED_FLAVORS / isSteeringSupportedForSession advertise codex and pi only; cursor joins when its soft-steer handler lands (#1609) - turn/steer now splits dispatch (stdin accepted) from completion (turn finished): the hub RPC acks once dispatch succeeds — never on the concurrent turn's completion, which can exceed the 30s RPC window - queue row commits only after the turn settles; a rejected/aborted steer restores the row so the message still delivers via turn/start, and a dispatched steer is never restored (no duplicate delivery) - steer carries clientUserMessageId (echoed as userMessage.clientId) so ambiguous transport failures can reconcile the thread later - client tests cover dispatch/complete split and stdin-write failure * fix(codex): reconcile dispatched steers before restoring; align error copy - A dispatched turn/steer whose completion fails (disconnect / protocol error) is now reconciled via thread/read by clientUserMessageId before the queued row is restored — the instruction is only re-delivered by turn/start when the thread never received it - Reconcile targets the pinned steer thread, not whichever turn is current when completion fails - syncEngine unsupported-flavor error now matches the capability gate (Pi and Codex only until the cursor handler lands) - launcher tests cover steer success (ack on dispatch), reconcile-accepted and reconcile-rejected outcomes * fix(codex): consume the row at dispatch; drop background reconcile - The hub RPC acks and the queue row is consumed as soon as stdin accepts turn/steer; completion is background-only logging. A dispatched steer is never restored, so the same localId cannot be re-delivered via turn/start after the caller was told the steer succeeded - Dispatch failure (stdin write error) still restores the row and reports failure - steer.completed rejection is always handled (no unhandled rejection on the dispatch-failure path) - tests updated: completion failure after dispatch keeps the row consumed; dispatch failure restores it * fix(codex): distinguish definite rejection from indeterminate completion - Transport-level failures (timeout, abort, disconnect, spawn, protocol) carry an indeterminate marker; explicit JSON-RPC error responses do not - After a dispatched steer, turn completion resolves → commit + consumed; a definite app-server rejection restores the row (instruction was never accepted, so turn/start cannot duplicate it); an indeterminate outcome leaves the row reserved so it can never be delivered twice - Completion handling registers before awaiting dispatch so the dispatch-failure path cannot leak an unhandled rejection - client/launcher tests cover explicit rejection (restore), indeterminate outcome (row stays reserved) and dispatch failure * fix(codex): reconcile indeterminate steers instead of a permanent reservation - After an indeterminate completion (disconnect/protocol), reconcile the thread by clientUserMessageId immediately: accepted → commit + consumed, provably rejected → restore, still unreadable → keep the reservation and retry from the main-loop top on later passes (post-reconnect) - A row never sits in dispatching forever: the hub cannot stamp it invoked while the instruction may never have been accepted - tests: indeterminate keeps reserved while thread unreadable; accepted reconciliation consumes; rejected path restores * fix(codex): accept all thread item shapes; retry reconcile; ack through abort - Reconcile matcher accepts userMessage/user_message with clientId/ client_id, matching the shapes the thread parser supports — an accepted steer can no longer be misclassified as rejected - A pending reconciliation schedules a wakeLoop retry, so a temporary app-server outage cannot strand the reservation behind waitForTurnOrRecovery - The success-path ACK no longer checks the steer epoch: the hub already reported steered on dispatch, so commit + messages-consumed must reach it even when an abort resets the queue in between * fix(codex): reinit reconnected app-server; keep reconcile retries alive - thread/read after a disconnect auto-connects a fresh app-server, which must be initialized before any request — reconcile now ensures connect + initialize (isConnected getter added to the client) - every still-unknown loop-top reconciliation schedules the next retry, so recovery without external traffic is eventually observed - launcher mock gains isConnected * fix(codex): timer-driven reconciliation; init tracking; abort-safe ACK - Reconciliation runs on a self-rescheduling 1s timer independent of the main loop (wakes it too), so idle loops and waitForTurnOrRecovery still observe app-server recovery; abort clears nothing implicitly — the ACK path commits and consumes even when the reservation was cancelled - Absence of a durable client id is ambiguous: unmatched reads stay 'unknown' and keep retrying instead of restoring the row - CodexAppServerClient tracks initialized state (reset on disconnect/exit) so ensureAppServerInitialized re-initializes a fresh process before thread/read; initialize failures leave the flag false for the next retry - tests: accepted reconciliation via scheduled timer, indeterminate keeps reserved, explicit rejection restores * fix(codex): bind reconciliation to the launcher lifecycle - runSteerReconciliation clears any armed retry timer on entry and never installs a second one, so loop-top and timer-driven passes cannot multiply - shuttingDown is set when the main loop ends: timers are cleared and the pending map is dropped, so an unresolved steer can never respawn an app-server after cleanup (remote-to-local switch included) * fix(codex): report steered only after app-server acceptance - The handler now awaits steer.completed (the inject-acceptance response): an explicit JSON-RPC rejection surfaces as failed and restores the row for the normal turn/start path instead of a false steered - Transport failure after dispatch reports 'Steer outcome is being reconciled' and keeps the row reserved while the timer-driven thread reconciliation runs - dispatch-failure path also swallows the paired completion rejection * fix(steer): tri-state cancel, clear-safe reservations, bounded acceptance wait - MessageQueue2.cancelByLocalId returns 'in-flight' for a dispatching steer reservation: the hub neither deletes the row nor stamps invoked_at (new CancelMessageResponse 'busy' status; web restores the optimistic row); pushIsolateAndClear and reset/close share cancelReservations so /clear-style commands cannot have a rejected steer resurrect a discarded prompt - turn/steer acceptance wait bounded at 25s (< hub 30s RPC timeout): a lost response is indeterminate and funnels into thread reconciliation instead of stranding the reservation - tests updated for the tri-state cancel contract * fix(codex,web): busy-aware edit flow; bound reconciliation reads - QueuedMessagesBar edit flow treats a 'busy' cancel as unsuccessful: it never prefills the composer when the row is inside an async steer, so a second client cannot send a duplicate - reconcileSteerByClientId bounds thread/read with a 5s timeout so a connected-but-silent app-server cannot hold the reservation in-flight indefinitely * fix(steer): inFlight-dominated cancel acks; bounded reconciliation - hub cancel-queued-message acks check inFlight before removed: a stale duplicate socket reporting removed can no longer delete the durable row while another socket is dispatching the steer - reconciliation entries expire after 60s and mark delivered: after the rejection window, a dispatched steer that the app-server never proved (client ids dropped on restart) is committed instead of polling thread/read forever - pre-dispatch failures (abort before write included) never enter reconciliation — they restore the row and report failure * fix(steer): persist indeterminate outcomes without replay * fix(steer): make ambiguous delivery restart-safe * fix(steer): recover crash-held rows and preserve retry dedup * fix(steer): ack retries and bound stdin dispatch * fix(steer): reconcile indeterminate dispatches and serialize retries * fix(codex): classify stdin callback failures as indeterminate * fix(steer): recheck indeterminate cancels after ACK * fix(steer): close retry and abort races * fix(steer): serialize live retries and abort admission * fix(steer): distinguish live dispatching from unknown * fix(steer): keep ACK failures held and reconcile busy cancel * fix(steer): distinguish held cancel from removal * fix(store): combine schema v24 migrations * fix(store): reserve schema v25 for steer delivery state * fix(steer): keep held cancel state and notify requeue * fix(steer): release explicitly cancelled unknown reservations * fix(codex): reject cancelled reservations before native steer * fix(codex): make reservation restore atomic with state * fix(codex): terminate abandoned transport writes * fix(steer): own abandoned app-server lifecycle and consume races * fix(codex): confirm dispatch and recover abandoned turns * test(codex): mock abandoned transport callback * fix(codex): clear visible turn state on transport loss * fix(steer): claim retries and cover native delivery state * fix(native): preserve indeterminate state on Android hydration * fix(steer): make retry claims single-winner * fix(steer): serialize concurrent retry claims * fix(socket): tolerate missing steer-state ACK callbacks * fix(native): serialize retry operations * docs(web): document unknown steer delivery and retry controls * fix(steer): handle retry failures and abort-before-connect * fix(steer): reinitialize after transport loss and finish iOS retry errors * fix(steer): preserve indeterminate rows across reconnect gaps * test(web): mock indeterminate queued recovery state * fix(steer): recover consumed ACK tombstones * fix(steer): expose consumed cancel tombstones |
||
|
|
e1bfdd6265 |
feat(fixtures): SSE patch + pagination scenario fixtures (K7)
Two new golden-fixture suites, expectations machine-generated from the web
implementation (same source-of-truth principle as the chat suite):
- shared/fixtures/sse/ (12 cases): session-updated versioned-patch
application via applySessionDetailPatch — strict version gates for
metadata/agentState/todos/teamState, out-of-order arrival, teamState:null
clear, max-monotonic updatedAt, flat last-write-wins fields, sub-minute
activeAt keep-alive drop, scratchlistUpdatedAt trigger, and the pinned
actual behavior that activeTurnStartedAt is NOT applied by the patch path.
Inputs are stored schema-normalized and validated against SessionSchema /
strict SessionPatchSchema at generation time; per-patch applied/unchanged
verdicts are part of the contract.
- shared/fixtures/pagination/ (11 cases): op scripts driving the real
message-window store with a scripted ApiClient — latest page + SSE ingest,
before-cursor older pages, epoch-mismatch reset (window discard + recorded
internal latest request), reset:true replace preserving optimistic rows,
localId echo reconciliation, messages-consumed invokedAt stamping (no
cursor advance), message-cancelled removal, cancel-too-late invoked-row
ingest, hidden-row cursor advance, 400-row trim preserving queued rows,
and queued-state gap recovery. Documents pin the exact requests the store
issued, older-load outcomes, reconcile candidates, and a minimal window
projection (ids/order, queued/optimistic flags, hasMore, epoch, viewMode,
compound cursors).
Generator: web/scripts/fixtures/{sse,pagination}/ extend the K5 framework
(canonical serialization reused; suites pruned of stale files). Web
self-conformance tests replay every fixture against the real implementation
in bun run test:web. README documents schemas, projections, replay
contracts, and native consumption for both suites.
No time injection needed: the store's only Date.now() reads gate
notification throttling, which never reaches persisted or projected state.
|
||
|
|
9479f667b5 | feat(fixtures): golden chat fixtures generated from web pipeline (K4+K5) | ||
|
|
a6feb6e8ba |
feat: unified agent configuration descriptors (session config consolidation) (#1469)
* feat(config): add agent config descriptor protocol and advertise via runner capability Introduce shared agent configuration descriptors covering model, effort, permission, and secondary settings per agent flavor, plus the canonical HAPI YOLO -> native permission mode mapping. Runners advertise the builtin descriptors through the runner-state capability so hubs and web can render configuration without hardcoded flavor branches. Migrate the OpenCode create-session model picker from a bespoke radio list to the shared SelectControl combobox. * feat(web): render create-session permission from agent config descriptor Replace the flavor-branched Grok/Codex-family/YOLO permission block with a descriptor-driven PermissionField. Pi now reports permission as managed instead of silently ignoring the YOLO toggle, and YOLO-only flavors show the native permission mode the preference maps to. Removes the superseded GrokPermissionModeSelector and CodexFamilyPermissionModeSelector components. * ci: retry flaky claudeRemote 5s-timeout failure * fix(web): persist explicit OpenCode Default selection instead of restoring a concrete model The parent initialization effect treated every null selected model as 'uninitialized' and auto-picked a concrete advertised model, clobbering the user's explicit Default choice (and a restored Default preference). null now means explicit Default and is preserved; only undefined (no choice made yet) triggers probe-based initialization. Add parent-level regression tests for Default persistence and remembered-model restore. * fix(web): accept undefined selected model in OpencodeModelSelector props * feat(web+cli+hub): unify create-session model/effort fields and add Pi model/effort support Pi's agent config descriptor now advertises model (machine) and effort (static thinking levels) for create AND session availability: - cli: ListPiModelsForMachine RPC runs 'pi --list-models' (cached, inflight deduped) and parses the provider/model table; startup model match accepts provider-qualified ids - hub: GET /api/machines/:id/pi-models route + rpcGateway/syncEngine passthrough - web: NewSession renders Pi models grouped by provider through the generic ModelSelector and a new descriptor-driven EffortField (replaces the per-flavor LaunchEffortSelector/ReasoningEffortSelector pair); launch payload forwards Pi model + thinking-level effort (runner already supported --model/ --effort for pi) * fix(web): render Pi provider groups in ModelSelector and scope Grok availability warning - ModelSelector now renders grouped options as <optgroup> (Pi models are provider-grouped; identical modelIds from different providers stay distinct) - PermissionField only receives autoPermissionModeSupported for Grok — a cached Grok probe result no longer leaks the Grok warning onto other agents Addresses HAPI Bot Minor findings on #1469. * fix(web): drop Object.groupBy from ModelSelector; revalidate restored Pi models against the catalog - ModelSelector buckets options with a reduce instead of Object.groupBy (Safari < 17.4 has no polyfill — New Session would throw on those clients) - Pi restored model/effort are cleared when the value is absent from the live machine catalog, and Create waits for the catalog while a non-default Pi choice is being validated (mirrors Codex/Grok/Copilot handling) Addresses HAPI Bot findings on #1469. * fix(pi+web): serialize startup model before thinking level; hide Pi launch controls during history import - PiSession gains startupModelSettled; the startup set_thinking_level waits for the requested model's set_model attempt to settle first, so a level the default model rejects is not lost before the requested model is confirmed (set_model and set_thinking_level were already serialized by the runtime mutation lock; this pins the model-first ordering) - Create Session hides Pi model/effort controls while a Pi history import is selected — the import reopens the native session as-is and would silently ignore launch-only model/effort values Addresses HAPI Bot findings on #1469. * fix(pi): settle startup-model gate when model discovery fails or returns no models A failed or empty get_available_models response would leave the startupModelSettled gate unresolved, stranding a requested startup effort indefinitely. Resolve the gate on the error path and the empty-models path; adds regression tests for both. |
||
|
|
79ffe9aa0e |
feat(web): composer model/effort value buttons and first-class permission (part of #1438) (#1475)
* feat(web): composer model/effort value buttons and settings order Wide composers now show [model] and [effort] value buttons for non-Pi flavors (labels from the current session values), opening the settings sheet on click. Narrow viewports collapse to the settings button only via a new useNarrowViewport hook. The settings sheet reorders to Model -> Effort -> Permission -> other settings (Fast mode, collaboration, Copilot agent mode) so permission is first-class. Toolbar customization gains 'model'/'effort' items with settings labels. Pi keeps its dedicated model/thinking panels unchanged (unified descriptor-driven sheet is a follow-up). * fix(web): satisfy strict types in composer value-button test harness * fix(web): address review findings on composer model/effort value buttons - Normalize null/'auto'/'default' model wire values onto the value:null option so default-model sessions keep a localized label button (Major) - Exempt model/effort value buttons from the settings outside-click dismissal so a second click closes the sheet instead of reopening it - Read matchMedia synchronously in useNarrowViewport so narrow first paints never flash the wide toolbar - Add regression tests: model=null/'auto' labels, toggle-close behavior, and initial narrow-viewport render * feat(web): fold Pi into the generic composer model/effort value buttons Pi sessions previously exposed model/effort twice: dedicated 'Pi model' / 'Pi thinking level' toolbar buttons (PiModelPanel/PiThinkingLevelPanel) AND the settings sheet's generic Model/Effort sections. Consolidate so Pi looks exactly like every other flavor: - Pi now uses the generic model/effort value buttons; labels resolve from the provider-qualified piModels catalog (name -> modelId -> session id). - The settings sheet's Model section already renders provider-grouped Pi rows and Effort renders Pi thinking levels, so the dedicated panels and their toolbar slots are deleted. - Keep Pi's mid-turn control affordance (#1442): Pi turns hold thread.isDisabled for minutes, so Pi model/effort controls stay enabled while a turn is running (configurationControlsDisabled instead of controlsDisabled), including the sheet rows. - Drop 'piModel'/'piThinking' toolbar layout items; persisted layouts normalize them away automatically. Tests: pi value-button label/sheet tests, mid-turn model selection via the unified sheet, toolbar layout defaults. * fix(web): keep a session-settings trigger on narrow viewports and mid-turn Pi Address the HAPI review bot's two Minor findings on the Pi consolidation: - Narrow viewports collapse the model/effort value buttons into the settings sheet, so a persisted toolbar layout hiding the gear left no session-settings trigger at all. ComposerButtons now forces the gear back into the rendered layout on narrow viewports (wide layouts keep honoring the user's hidden choice). - The Pi mid-turn live-control rule only reached the value buttons and sheet rows; with those buttons gone on narrow, the gear was still disabled by controlsDisabled for the whole (minutes-long) Pi turn. HappyComposer now passes settingsDisabled={modelEffortControlsDisabled} so the gear stays clickable mid-turn for Pi exactly like the buttons. Tests: narrow + hidden-gear layout keeps Settings; narrow Pi mid-turn gear stays enabled and opens the provider-grouped sheet. * fix(web): address HAPI Bot Pi settings-sheet findings Three Minor findings from the review bot on the unified Pi sheet: - Provider-qualified selection: rows compared only modelId, so duplicate model IDs across providers all looked selected. Compare against piSelectedModel's provider+modelId when available. - Thinking-level reset: the removed Pi panel toggled the current level back to null; the unified effort rows only submitted concrete values. Re-clicking the selected effort row now clears it for Pi. - Memo staleness: the settings-sheet memo did not depend on modelEffortControlsDisabled, so a Pi disabled-state transition while the sheet was open left rows enabled from the prior render. Tests: colliding model IDs highlight only the matching provider row, re-clicking the selected effort row sends null, and a rerender with active=false disables the open sheet's rows. * fix(web): include piSelectedModel in settings-sheet memo deps The overlays memo reads piSelectedModel for provider-qualified row highlighting but only declared the derived selectedPiModel. When piSelectedModel hydrates from absent to a qualifier that resolves to the same catalog object, the memo is reused and duplicate model IDs stay highlighted across providers. Add the raw prop to the dep array. * fix(web): gate Pi model rows on catalog; reset drill-down via value button Address the HAPI Bot review on the unified composer settings sheet: - Pi no longer falls back to the generic synthesized modelOptions rows when its provider catalog is empty/loading. Selecting one of those would post a bare model id that runPi cannot resolve to a provider (first cached match or 409). The Model section now only renders for Pi when piModelGroups exists, and renders grouped rows exclusively. - Closing the sheet through the model/effort value button now goes through handleSettingsToggle, so a Cursor variant drill-down resets to the base model list on reopen (previously only the gear and outside-click paths cleared it). Tests: empty Pi catalog hides Model section; value-button close resets Cursor drill-down. * fix(web): hide Pi effort controls until the selected model resolves Address the HAPI Bot review: with the catalog still loading/failed there is no selectedPiModel to derive a capability map from, but the unified effort control stayed enabled (mid-turn Pi controls are intentionally live). Selecting a level would send set_thinking_level for a model that may not support reasoning, and the RPC can be rejected after the sheet closed. The old dedicated panel guarded this state. showEffortSettings now requires a resolved, reasoning-capable Pi model; the effort value button hides the same way. With an empty catalog Pi exposes no settings trigger at all, matching the old control states. Tests: unresolved Pi catalog mid-turn exposes no effort action. * fix(web): hide the Pi model trigger until the catalog resolves Address the HAPI Bot Minor finding: with an empty/loading Pi catalog the model value button fell back to the bare session model id and rendered an enabled trigger that opens no Model section. Show the button only once the provider-qualified catalog entry resolves. |
||
|
|
8dc4a50fee |
fix(web): prevent replaying historical assistant output (#1572)
* fix(web): prevent replaying historical assistant output * fix(web): handle history pagination during typing handoff * fix(web): preserve streaming handoff across history windows * fix(web): distinguish hydrated active runs from new output * fix(web): preserve handoff across tail hydration * fix(web): preserve first output in user-only turns |
||
|
|
ec19699418 |
fix(web): preserve session sidebar scroll during navigation (#1552)
* fix(web): preserve session sidebar scroll during navigation * fix(web): preserve nested session pane scroll * fix(web): avoid repeated file scroll restoration * fix(web): preserve scroll when leaving chat file previews * fix(web): preserve scroll after session resolution |
||
|
|
5c81ed69e2 |
fix(runner) preserve native instructions (#1557)
* fix(codex): preserve native instructions Keep HAPI guidance in the developer layer so app-server retains Codex persistence rules. Align long-running goal states with the Codex 0.147 protocol. * fix(codex): filter new goal statuses |
||
|
|
e32334fbb2 |
fix(web): prioritize latest tail on session re-entry (#1560)
* fix(web): prioritize latest tail on session re-entry * fix(web): start latest sync immediately on re-entry |
||
|
|
767e40a71f |
fix(web): preserve current Fork action while reading history (#1573)
* fix(web): preserve current Fork action while reading history * fix(web): invalidate stale Fork boundary during history updates * fix(web): invalidate Fork boundary after message consumption |
||
|
|
30504252b9 |
feat(web): keep quiet active sessions in the pinned top section — Running / Active / Inactive tiers (#1590)
* feat(web): keep quiet active sessions in the pinned top section (#1589) When pin-in-progress is on, any connected session now floats above the project folders instead of dropping down the moment it finishes executing. The sidebar reads as three tiers: 1. In progress - running and pending work (unchanged) 2. Active sessions - quiet but connected (finished executing, operator usually continues the conversation) 3. Inactive - everything else, in directory groups Also remove the duplicate showSessionSummaryInChat fixture property in markdown-a.test.tsx that fails web typecheck on upstream main. * test(web): cover Active section collapse, keyboard toggle, and search expansion * fix(web): keep action-only project headers when every row floated A directory whose sessions all floated to the pinned sections lost its header entirely, taking copy-path and new-session-in-directory with it. Render an action-only header for fully-floated projects so the directory actions stay available. |
||
|
|
901f17d0ca |
feat(pi): support Pi slash commands from HAPI web (compact/session/model/help) (#1570)
* feat(pi): support Pi slash commands from HAPI web (compact/session/model/help)
Pi runs as 'pi --mode rpc' over piped stdio, so TUI slash commands typed in
web chat previously fell through to the LLM as plain text and silently did
nothing (notably /compact).
- shared: add Pi builtin slash command list (help/compact/session/model) so
the web / menu exposes them; web test updated to match
- cli: intercept Pi builtin commands in runPi's user-message path
* /compact [instructions] -> Pi compact RPC (120s timeout, works while
streaming; summary + token delta reported back as chat messages)
* /session -> get_session_stats formatted stats
* /model [modelId] -> list/switch via set_model
* /help -> supported-commands list
* other Pi TUI builtins (/tree, /export, /reload, ...) -> explicit
terminal-only notice instead of silent LLM pass-through
* unknown slash text still passes through (extension commands, skills,
templates keep working)
- gate the prompt pump with piCompactInFlight so queued prompts are not
rejected by Pi mid-compaction; buffer commands until ready like prompts
- ListSlashCommands RPC merges HAPI builtins with Pi extension commands
- tests: parser unit tests + runPi integration tests (compact execution,
streaming steer interception, failure reporting, FIFO blocking, model
switch, unsupported commands, slash list merge)
- docs: document Pi slash command support in docs/guide/agents.md
* fix(pi): address review findings on slash command lifecycle
- compact timeout: fail the session (indeterminate outcome, runtime lease
poisoned) instead of reopening the prompt FIFO into a possibly-compacting
Pi; pump only when cleanup has not been initiated
- special commands: release the cancellation reservation before executing so
a cancel landing mid-command is not acknowledged (hub would delete the
queued row while the command still runs)
- tests: drop the duplicated slash-command describe block; add focused tests
for compaction timeout with a queued prompt and cancellation during an
in-flight special command
* fix(pi): route slash commands through the prompt FIFO and reject ambiguous models
- slash commands now share the prompt FIFO with ordinary messages: a
/compact or /model typed after a queued prompt dispatches only after it
(and after the active turn settles), instead of jumping the queue from
the preparation chain
- the pump dispatches special entries out-of-band while piSpecialCommandInFlight
keeps the FIFO blocked; steer promotion refuses slash commands
- /model <id> prefers an exact provider/modelId match and reports bare IDs
shared by multiple providers as ambiguous instead of picking the first
- tests: FIFO ordering (queued prompt before /compact), steer-delivered
/compact queued until settle, ambiguous/qualified model selection
* fix(pi): keep /compact interruptible, honor extension precedence, require token boundary
- head-of-line /compact dispatches even while Pi is streaming (Pi's
compact() aborts the active generation itself); every other queued item
still waits for the stream to settle, preserving FIFO order
- discovered extension commands / prompt templates override same-name
builtins at message time, matching the slash-list merge precedence
- parsePiSpecialCommand requires a command-token boundary, so path-like
text such as /compact.md or /model/config stays an ordinary prompt
- tests: interrupt rule, extension collision, reserved-name path prefixes,
non-compact commands waiting for stream settle
* fix(pi): honor cancellation acknowledged during slash-command discovery
A cancel arriving while the chain awaits get_commands (cold cache) was
acknowledged via the preparing reservation but never re-checked, so a
canceled /compact could still execute. Re-check the cancellation marker
after discovery and drop the message before dispatch.
* fix(pi): qualify /model selectors and report failed slash RPCs once
- /model lists provider-qualified selectors (openai/gpt-5.2) so duplicate
bare IDs remain usable and copy-pasteable; current model is qualified too
- compact/set_model failures are owned by the awaited slash/config handlers:
the common response handler no longer emits the raw Pi error a second time
- tests: qualified listing with duplicate providers, single-message failure
reporting for rejected /compact and /model
* fix(pi): consume slash-command queue row at dispatch
Special commands (/compact, /session, /model, /help) are executed by HAPI
itself and never delivered to Pi as prompts. Consumption was deferred until
the command finished, so a /compact run — an LLM summarization pass that can
take minutes — left the row stuck in the web queued bar for its whole
duration, then surfaced as a sent message. Consume the row the moment
dispatch starts; failures still surface via the explicit event message.
* fix(pi): guard special-command dispatch against unexpected rejections
* ci: retry Codex PR Review after infra failure (proxy 503)
* fix(pi): keep session queued-thinking grace during /compact dispatch
The queued-thinking grace is session-scoped, so clearing it while
acknowledging a dispatch-time /compact row also drops the grace for any
prompt queued behind it. /compact keeps running for minutes without
toggling Pi thinking state, which would leave the web session looking idle
while compaction and the following prompt are still pending. Only the
fast, synchronous commands (/session, /model, /help) clear the grace.
* fix(pi): render compaction summary as a dedicated chat block
The manual /compact RPC result was reported as two plain message
events ("📦 Compaction completed (tokens: …)" + "📦 Compaction
summary: …"), which the web chat renders as tiny centered status
lines — unusable for a real summary payload. Emit a structured
compact-summary event instead (summary + token delta) and render
it as an independent block: header with the delta and the summary
markdown in a scrollable panel.
Also emit the same structured event when importing Pi session
files (compaction entries), and queue the event lossless like
other user-visible messages so a disconnect cannot drop it.
Verified: bun typecheck clean; bun run test exit 0 (cli 2481
passed, web 2451 passed, hub/shared clean); runPi/loop/apiSession/
piSessions/presentation suites green.
* fix(pi): address HAPI Bot findings on compact dispatch and import
- Track compaction as thinking for its whole duration: /compact runs for
minutes without a Pi streaming event, so the 15s queued-thinking grace
alone left the web session looking idle while compaction and any queued
prompts were still pending (updateThinkingState around the compact RPC).
- Imported Pi compaction summaries must use the event envelope
(content.type: 'event') like the live wrapper's compact RPC result; the
codex payload envelope is dropped by the web normalizer. Extend
CodexImportedMessageSchema with the event variant.
* fix(pi): /model retries discovery when the model cache is empty
Startup model discovery can be late or fail once; using only the cached
catalog made /model report valid models as unknown. getPiModels() falls
back to the get_available_models RPC on an empty cache, used for both
listing and switching.
* fix(pi): interrupt in-flight /compact on Abort; surface startup model rejection
- The Abort action no longer waits on the runtime-mutation lease when a
manual /compact is in flight (compaction can hold it for up to 120s,
blowing the 25s abort deadline and failing closed). It sends the abort
RPC directly so Pi cancels its compaction AbortController; the compact
RPC's 'Compaction cancelled' error is not double-reported as a failure
since Pi already emits the compaction_end(aborted) lifecycle event.
- A rejected detached startup set_model now emits a visible ⚠️ event into
chat instead of only a debug log, restoring the pre-existing behavior.
* fix(pi): close the Abort race when /compact is queued on the mutation lock
Abort previously assumed an in-flight /compact always had its RPC issued;
the command is marked active at queue dispatch, but the compact RPC is sent
only after the runtime-mutation lock is acquired. An Abort landing in that
gap acknowledged success while the compact RPC still ran afterwards.
Track the compact's rpcStarted/cancelled state: Abort cancels a not-yet-
started compact in place (the queued callback skips it), and interrupts a
started one via the abort RPC as before.
* fix(pi): persist provider-qualified selection after /model switch
The success path updated currentModel/currentProvider and keepalive with a
bare model ID, leaving metadata.piSelectedModel on the previous provider.
The web picker prefers that metadata for selection, context-window
resolution, and effort options, so a switch like openai/gpt-5.2 ->
azure/gpt-5.2 was invisible. Persist piSelectedModel with the full
provider/modelId pair on every confirmed switch.
* fix(pi): retire pending extension UI requests when /compact interrupts a turn
The streaming-interrupt path sent the compact RPC without cancelling
pending extension UI requests first, unlike the Abort path. Editor
requests have no timeout, so the web could stay stuck on a stale
input/permission card and a later answer could be routed to the aborted
turn. Cancel all pending requests (with a response) before compacting.
* fix(pi): fail closed when the direct compact-abort RPC times out
The in-flight /compact abort branch awaited the abort RPC without the
ordinary Abort path's timeout handling: an unanswered abort left the
compaction outcome indeterminate (the compact RPC keeps the mutation
lease for up to 120s) while the wrapper still looked live. Fail the
session on PiRpcTimeoutError, mirroring the standard abort fail-closed
path.
---------
Co-authored-by: swear01 <swear01@users.noreply.github.com>
|
||
|
|
44703d0153 |
fix(web): localize Fork and Rewind labels in Simplified Chinese (#1546)
* fix(web): localize Fork and Rewind labels in Chinese * test(web): cover localized history confirmation dialogs |
||
|
|
b2ae6b90a3 |
fix(web): keep share actions visible during generation (#1550)
* fix(web): keep share actions visible during generation * test(web): cover sharing during generation |
||
|
|
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). |
||
|
|
235d6dd6fd | fix(web): clarify displayed media labels (#1565) | ||
|
|
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 |
||
|
|
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 |
||
|
|
febbf8ff58 |
fix(web): improve session search UI (#1545)
* fix(web): improve session search UI * fix(web): reserve space for search clear control |
||
|
|
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> |
||
|
|
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> |
||
|
|
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 |
||
|
|
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). |
||
|
|
eb7a762984 | fix(web): clarify session list status hints (#1504) | ||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
f63d4bb83b | fix(web): remove trailing ellipses from search placeholders (#1449) | ||
|
|
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> |
||
|
|
00b6e44869 |
fix(web): keep project-pin groups above In progress (#1432)
Durable project pins were losing to unpinned In progress floaters. Render pin-containing directory groups between the global pinned band and the In progress section; leave project-pinned rows inside groups. Closes #1431 Co-authored-by: Cursor <cursoragent@cursor.com> |
||
|
|
cde0507bad | fix(codex): harden model discovery fallback | ||
|
|
3da9f7780a |
feat(web): support project and global pinned sessions (#1115)
* feat(web): support persistent pinned sessions * fix(web): preserve project hierarchy for pinned sessions * fix(web): preserve pins during session deduplication * fix(hub): preserve pins when merging sessions * fix(hub): migrate session pins from schema v15 * chore: retrigger PR review * chore: retrigger PR review * chore: retrigger PR review * chore: retrigger PR review * fix: align pinning with schema and sidebar filters * fix(web): expose pinning in session header menu * test(hub): expect schema v17 after pin migration * fix(web): report pin action failures * chore: retrigger PR review * chore: retrigger PR review * chore: retrigger PR review * chore: retrigger PR review * fix: address pinning review regressions * fix(web): scope pin error toast to session rows * test(hub): expect schema v20 after migration * fix(hub): preserve latest source pin during merge * test(hub): expect schema v22 after pin migration * feat(web): add project and global session pin modes Support mutually exclusive project vs global pins, surface both in the session action menu, and render globally pinned sessions in a top-level sidebar group with project path labels. * fix(web): polish pinned section icon and divider alignment * fix(web): tighten pinned section title icon alignment * fix(web): restore original pinned section pin icon shape * fix(web): rename pinned section to pinned sessions * fix(hub): prefer stronger pin mode when merging sessions Keep global pins over project pins during consolidation so a project-pinned source cannot downgrade an already or concurrently global-pinned target. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(web): keep inactive project-pinned groups expanded Pinned sessions should stay easy to find after reload; do not hide them behind the default inactive-group collapse. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(web): keep project pins in directory groups --------- Co-authored-by: Cursor <cursoragent@cursor.com> |
||
|
|
bdf8da1d5a |
fix(web): preserve drafts across inactive session resume (#1378)
* fix(web): preserve drafts across inactive session resume * fix(web): address resumed draft review findings * fix(web): preserve drafts during hydration resume * fix(web): reset resume cache when session lifecycle changes Clear resolvedSessionRef when session id or active flag changes so a same-id resume that later archives again cannot skip a required new resume. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(web): gate inactive attachments on resumability Skip the attachment adapter and disable drops when an inactive session cannot resume, so file picks do not become stuck error attachments. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(web): avoid source-session 404 during reopen handoff When reopen merges into a different id, skip invalidating the source session detail so draft transfer can finish before navigation without flashing Session unavailable. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(web): include in-flight attachments in inactive resume handoff Pass the selected file into draft transfer and coalesce concurrent multi-file drops so resume navigation cannot drop attachments that were never published to the live snapshot. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(web): harden inactive draft handoff edge cases Re-check attachment cancellation after resume, always copy from the source draft instead of a stale target snapshot, and force a fresh getSession after optimistic seeding. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(web): prevent passive resume and preserve staggered handoffs Restore attachments only on active sessions so draft hydration cannot resume an archived session, and remember completed handoff targets so slower multi-file adds append instead of overwriting. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(web): avoid empty inactive snapshots shadowing attachment drafts When attachment restoration is disabled, persist composer text without publishing an empty live snapshot so reopen can still transfer IndexedDB attachments. Co-authored-by: Cursor <cursoragent@cursor.com> * test(web): cover inactive draft archive-switch-reopen lifecycle Add a component-level harness that exercises HappyComposer snapshot rules with useComposerDraft and transferComposerDraft across archive, session switch, and reopen to a new id. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(web): resolve SessionChat conflict markers for draft handoff Keep upstream send-message types and the inactive-resume handoffComposerDraft import so typecheck no longer dies on committed merge markers. Co-authored-by: Cursor <cursoragent@cursor.com> * chore: retrigger CI after conflict-marker fix Co-authored-by: Cursor <cursoragent@cursor.com> * chore: retry CI after Actions outage (Meta daily) Co-authored-by: Cursor <cursoragent@cursor.com> * chore: retrigger CI and PR review on current tip Re-queue Test and Codex PR Review after the conflict-marker fix and Actions outage. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(web): do not overwrite stored attachments from inactive composer Skip attachment draft persistence on unmount when restoration is disabled, so a failed inactive file pick cannot replace hidden IndexedDB attachments with a partial visible list. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(web): merge inactive picks into stored attachment drafts When restoration is disabled, persist visible pending attachments by merging them with IndexedDB instead of clearing the live snapshot alone, so a failed resume still keeps newly selected files for the next reopen. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(web): harden inactive draft persist for failed picks Keep pathless attachment ids stable, serialize inactive merge/writes, drop removed visible picks from IndexedDB, and await pending persist before transfer/reopen. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(web): navigate after cancel during inactive resume handoff If resume already merged into a new session id, still call onSessionResolved without the cancelled file so draft transfer + navigation leave the deleted source route. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(web): harden inactive draft handoff cancel and persist Carry a live isCancelled predicate through resume handoff so mid-transfer removes drop the file from the source snapshot, persist inactive blobs only on attachment revision (not keystrokes), and keep upload metadata on same-target staggered appends. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(web): await durable draft move and tombstone source Cross-session transfer now awaits an atomic IndexedDB put+delete via moveDraftAttachments, clears the source text draft, and skips unmount re-persist once the source id is marked handed off. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(web): re-sample cancel after draft drain; fail closed on move moveDraftAttachments resolves attachments only after awaiting queued writes, and rolls cache back + throws when a real IndexedDB transaction fails so transfer cannot clear the source or mark a handoff on loss. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(web): re-sample composer text after awaited draft move Keystrokes during the IndexedDB drain keep updating the source draft; re-read live/persisted text after the move resolves before clearing the source and writing the target snapshot. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(web): navigate after resume even if draft move fails Hub reopen can delete the source row before local IndexedDB finishes. Catch transfer failures, still copy text best-effort, and always navigate via transferComposerDraftThenNavigate / handoff onNavigable. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(web): harden draft move prune and in-flight persist Delete the source row before put/prune so a full retention store cannot evict an unrelated draft, and buffer inactive persist edits during an in-flight transfer onto the target instead of recreating the source. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(web): close inactive draft remount and transfer races Reset visibility bookkeeping on inactive remount, install the transfer barrier before the first await, and await a durable corrective target write when late edits land during an in-flight move. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(web): unbreak draft transfer typecheck and remaining races Re-read pendingTransfers.latest after awaits (TS2339), propagate real IndexedDB write failures on corrective same-target moves, and restore only missing stored attachment ids when same-id resume already has a visible pick. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(web): stabilize late draft edits and multi-file resume Repeat corrective target writes until pending.latest is unchanged across the await, sync keystrokes into the transfer barrier, and keep one resume promise per inactive adapter instance so staggered files join the same handoff after navigation. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(web): keep IDB attachments when typing during transfer Track inactive keystrokes on pending.latestText instead of inventing pending.latest with an empty attachment list that would overwrite hidden stored files during a cross-session move. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(web): fail closed when draft attachment reads fail Cross-session transfer now requests throwOnError for IndexedDB reads so a transient empty result cannot delete the source draft during the durable move. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(web): fail closed on inactive merge reads and keep target text Strict-read every inactive attachment RMW merge so a transient IDB error cannot publish a partial live snapshot, and copy composer text to the resumed id before aborting when attachment transfer reads fail. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(web): keep resumed session active after stale detail fetch Reapply optimistic active after background getSession settles so a lagging inactive REST snapshot cannot flip the target composer back, and catch strict inactive persist rejections at fire-and-forget call sites. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(web): prune attachments cancelled during durable draft move Always rewrite the target after the cross-session move so isCancelled flips during the IndexedDB commit window still drop the file, and prune again in handoff before navigation when the batch reports cancellation. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(web): stabilize multi-file cancel across draft corrections Treat the pending cancellation set as part of post-move stability so a sibling remove mid-corrective rewrite is re-sampled before navigation, and loop handoff same-target rewrites until that set stops changing. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(web): transfer inactive drafts before text-only resume send Await onSessionResolved so composer draft handoff completes before the send mutation, and route text-only auto-resume through transferComposerDraftThenNavigate like upload/reopen paths. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(web): transfer Files reopen drafts and respect SSE active Wire the Files-page reopen path through transferComposerDraftThenNavigate, and refresh resumed session detail only when the cached active flag still matches so a mid-flight inactive SSE transition is not resurrected. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(web): defer send until hidden inactive drafts hydrate After text-only resume transfers stored attachments, skip the mutation when the target draft still has files the inactive composer never exposed, so clearDraftsAfterSend cannot wipe them after a text-only POST. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(web): defer any attachment resume and expose stored drafts Always defer cross-session send when visible or stored attachments exist so source-scoped upload paths are not POSTed, and surface IndexedDB attachment presence to disable scheduling on inactive composers. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(web): resume same-id drafts and block pending schedule races Return resumed independently of session id so same-row PTY/Pi resumes still defer for hidden files, route attachment-only Send through an explicit resume callback, and treat incomplete inactive hydration as schedule-blocking. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(web): preserve send text during deferred draft handoff Pass the immutable submitted text into draft transfer so assistant-ui clearing the composer while resume is in flight cannot drop text from deferred hidden-attachment sends. Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: Cursor <cursoragent@cursor.com> Co-authored-by: HeavyGee <133152184+heavygee@users.noreply.github.com> |
||
|
|
b7f52f58ca |
feat(media): add audio and file display (#1405)
* feat(cli): cross-flavor inline image display via MCP and ACP Share display_image prompt across MCP-bridge flavors (Cursor, Gemini, Kimi, Codex, Claude, OpenCode), auto-approve the tool in buildHapiMcpBridge, handle ACP image content blocks, and harden generated-image registration with content sniffing. Closes tiann/hapi#956 Co-authored-by: Cursor <cursoragent@cursor.com> * fix(web): render generated-image cards reliably in chat Keep object URLs stable across refetch, upscale tiny inline images, fetch generated-image bytes with cache no-store (avoid empty 304 bodies), and load hapiMcpUrl from per-session API in hapi-display-image tooling. Co-authored-by: Cursor <cursoragent@cursor.com> * feat(cli+web): display_video MCP for inline mp4/webm (#956) Add display_video alongside display_image, video MIME sniffing with avif guard, web GeneratedImageCard video player, and hapi-display-image auto-routing. Co-authored-by: Cursor <cursoragent@cursor.com> * feat(cli+web): cross-flavor display_video parity with images (#956) Share display_video prompts across MCP-bridge flavors, auto-approve the tool, register mp4/webm via path sniffing, render inline video in web on the existing generated-image RPC path, and restore robust media card fetch. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(cli): ACP image ordering and inline media source provenance Flush buffered assistant text before async generated_image emit from ACP image blocks (PR #958 review Major). Add optional source metadata on generated-image wire messages (ingress, flavor, toolCallId, toolName) for MCP, ACP, and Codex tool-result paths. Seeds artifact-event follow-up #966. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(cli+web): address PR #958 review Majors on media order and stale blobs Queue ACP session updates and await async image registration before later events; clear GeneratedImageCard blob state when imageId changes. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(cli): await ACP queue after late-drain before turn_complete Straggler session/update during drainLateBuffers can queue async image registration; re-await sessionUpdateQueue so generated_image is not emitted after turn_complete. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(scripts): route AVIF ftyp brands to display_image in helper Match server-side detectImageMimeType so .avif files are not sent to display_video and rejected as unsupported video. Co-authored-by: Cursor <cursoragent@cursor.com> * feat(#956): agent inline-media doctor and discovery fixes - hapi doctor inline-media: probe bridges, print per-session inline commands - Expose hapiMcpUrl on session list summaries (stops false "no MCP" scans) - Helper script: match cursorSessionId prefixes; HAPI_SESSION_ID path-only mode - ACP bridge prompt: shell fallback + HAPI session id vs agent id rule Co-authored-by: Cursor <cursoragent@cursor.com> * fix(web): allow immutable cache for generated media blobs Drop cache: no-store on generated-image fetch so browser can reuse hub immutable responses; on 304 re-read via force-cache (#927, PR review). Co-authored-by: Cursor <cursoragent@cursor.com> * fix(#956): Cursor native MCP overlay; drop user-turn bridge prepend Cursor ACP ignores session/new mcpServers. Write .cursor/mcp.json and run agent mcp enable hapi instead. Remove HAPI_MCP_BRIDGE_PROMPT from user turns on ACP remotes; enrich MCP tool descriptions for discovery. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(cli): preserve non-HAPI mcp.json keys on Cursor overlay cleanup Cleanup only removes or restores the hapi MCP entry instead of rewriting the full pre-session snapshot, so concurrent edits to other servers survive. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(cli): handle generated_image in Grok ACP launcher switch Upstream Grok launcher exhaustiveness broke after AgentMessage gained generated_image for cross-flavor inline media. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(cli): rebase fallout for display_video + OpenCode skill lookup Gate display_video in the STDIO bridge, restore OpenCode first-prompt TITLE_INSTRUCTION (skill_lookup), and update tool-list test expectations. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(cli): leave user-owned hapi MCP entry alone on overlay cleanup Only undo mcpServers.hapi when it still matches the exact entry this session installed; concurrent Cursor/user edits of that key survive. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(cli): exact-match auto-approve for display_image and display_video Move media tools off substring name/id hints onto the exact-name set so forged lookalike tools are not approved in default permission mode. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(cli): drop dead Cursor bridge prompt; put guidance in MCP descriptions Cursor must not get a user-turn media prepend (prompt-taint). Remove unused HAPI_MCP_BRIDGE_PROMPT_CURSOR and embed DISPLAY_*_PROMPT_CURSOR in the display_image/display_video MCP tool descriptions instead. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(cli): require user approval for display_image and display_video Those tools read arbitrary local paths into chat; keep them on MCP approval_mode prompt and out of default-mode auto-approve exact names. Co-authored-by: Cursor <cursoragent@cursor.com> * chore: re-trigger Codex PR review after provider 503 Co-authored-by: Cursor <cursoragent@cursor.com> * fix(cli): ignore URI-only ACP image blocks that read local disk Passive ACP agentMessageChunk handling must not load file:// or bare paths; local media goes through prompt-gated display_image/display_video. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(cli): allowlist MP4 ftyp brands for video sniffing Reject HEIC/HEIF and other non-video ISO-BMFF containers instead of treating every non-AVIF ftyp as video/mp4. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(cli): keep Cursor ACP startup if MCP overlay fails Wrap installCursorMcpOverlay so a malformed project .cursor/mcp.json cannot abort the session; continue without inline media tools. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(cli): fail doctor inline-media when checks fail Exit non-zero whenever required checks fail, even if an active hapiMcpUrl bridge is present. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(cli): expect display_video when change_title is disabled Native ACP title mode still exposes display_image and display_video; update startHappyServer test after rebase onto 0.23.4. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(cli): keep ACP title sync synchronous outside media queue session_info_update title forwarding (#1028) must not wait on the async message-handler queue used for inline media ordering. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(cli): sniff media headers only; advertise OpenCode display_video Read 16 bytes for detectMediaTool instead of the whole file, and include hapi_display_video in OPENCODE_NATIVE_TOOL_INSTRUCTION for remote ACP. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(cli): show Cursor generate_image inline in HAPI chat cursor/generate_image only emitted a tool card; register filePath or base64 imageData into generatedImages and emit generated_image so the web chat card renders (issue #956 / swear01 report). Co-authored-by: Cursor <cursoragent@cursor.com> * fix(cli): ignore path-only Cursor generate_image reads Path-only filePath registration bypassed permission-gated display_image / display_video MCP tools. Keep base64 imageData only; local paths must go through MCP approval. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(cli): gate inline media base64 length before decode Reject oversized ACP/Cursor base64 payloads by character count so the CLI never allocates past the 25 MB generated-image cap. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(cli): require EBML DocType webm for inline video sniff Bare EBML magic matches Matroska/MKV too; only accept DocType webm. Also restore annotated Playwright cursor in annotatedVideoUseOption. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(scripts): read 128-byte header for WebM DocType sniff detectMediaTool only loaded 16 bytes, so EBML DocType webm was often missing and valid WebM files fell through to display_image. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(web): compare generated-image source by value in reconcile Wire normalization allocates a fresh source object each pass; reference equality forced media-card recomputation on every reload/SSE refresh. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(cli): inject Cursor MCP enable for overlay unit tests installCursorMcpOverlay always spawned `agent mcp enable`; tests now pass a noop so the suite never shells out to a real Cursor binary. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(cli): shell-quote doctor inline-media helper command Paths and session prefixes with spaces/metacharacters broke the copied snippet; JSON.stringify each interpolated argument. Co-authored-by: Cursor <cursoragent@cursor.com> * test(cli): fix doctor inline-media quote path expectation Repo root from scriptPath is three levels up (cli/), not the parent of cli. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(cli/web): per-session Cursor MCP overlay id and bound tiny-image scale Co-authored-by: Cursor <cursoragent@cursor.com> * fix(cli): redact generate_image base64 from logs and fix doctor MCP ids Co-authored-by: Cursor <cursoragent@cursor.com> * fix(cli): harden inline-media doctor for packaged installs and hub headers Co-authored-by: Cursor <cursoragent@cursor.com> * fix(cli): lock Cursor mcp.json updates and bound ACP media filenames Co-authored-by: Cursor <cursoragent@cursor.com> * fix(cli): preserve mcp.json mode and token-scoped overlay locks Co-authored-by: Cursor <cursoragent@cursor.com> * fix(cli): publish Cursor MCP lock owners via link(2) and treat EPERM as alive Co-authored-by: Cursor <cursoragent@cursor.com> * fix(cli): fail closed on stale MCP locks; keep concurrent mcp.json top-level keys Co-authored-by: Cursor <cursoragent@cursor.com> * fix(cli): roll back Cursor MCP overlay when agent mcp enable fails Co-authored-by: Cursor <cursoragent@cursor.com> * fix(cli): await ACP session queue in suppressUpdatesDuring tests #958 queues handleUpdate for media registration; upstream compact tests assumed sync delivery after restore. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(cli): capture ACP handler at enqueue; keep display_video manual Close two Major review findings on #958: suppress queue leak after restore, and Claude --allowedTools auto-approving local-path video. Co-authored-by: Cursor <cursoragent@cursor.com> * fix: write through symlinked mcp.json; lazy-load inline video Preserve user Cursor MCP symlinks on atomic overlay writes, and require explicit Load video before fetching large generated-video blobs. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(web): use valid TerminalToolDisplayMode in media card test Co-authored-by: Cursor <cursoragent@cursor.com> * fix(scripts): require unique session prefix in display helper Reject ambiguous prefix matches so images/videos cannot land in the wrong HAPI chat when multiple agent session ids share a prefix. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(cli): always cleanup Cursor MCP overlay on teardown Run overlay cleanup in finally so cancelAll/disconnect failures cannot leave a dead hapi-<sessionId> entry in .cursor/mcp.json. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(cli): handle Copilot generated_image; refuse MCP symlinks Unblock typecheck after Antigravity/Copilot merge, and fail closed when .cursor/mcp.json or .cursor is a project-controlled symlink. Co-authored-by: Cursor <cursoragent@cursor.com> * fix: abort restore deliveryMode; prune dead Cursor MCP overlays Unblock web typecheck after steer merge, and recover orphaned hapi-* mcp.json entries via HAPI_MCP_OVERLAY_PID ownership stamps. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(cli): recover dead-PID Cursor MCP overlay locks Token-matched unlock so a crash mid-lock no longer permanently disables inline media; keep live-owner waits identity-safe. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(cli): serialize Cursor MCP stale-lock recovery Acquire an exclusive recovery lock before token-matched unlink so two recoverers cannot remove a successor's live mcp.json lock. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(cli): fail closed on stale Cursor MCP overlay locks Withdraw racy auto-recovery: pathname check-then-unlink/rename can steal a successor lock. Stale locks throw with an explicit rm hint instead. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(web): drop duplicate deliveryMode on abort restore Merge left both steer and queue; keep queue so retries after abort do not re-bind to a later Pi turn. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(cli): bound inline media reads on open fd Close TOCTOU between pathname size check and readFile for display_image / display_video and registerGeneratedImageFromPath. Also preserve non-PID env edits on Cursor MCP overlay cleanup. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(cli): repair happyMcpStdioBridge test syntax after merge Co-authored-by: Cursor <cursoragent@cursor.com> * fix(cli): write Cursor MCP overlay to ~/.cursor, not the project Keep ephemeral hapi-<sessionId> bridges out of the checked-out tree so agents cannot git-add a live loopback URL. Tests inject mcpConfigDir. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(cli): point Cursor MCP diagnostics at ~/.cursor/mcp.json Co-authored-by: Cursor <cursoragent@cursor.com> * feat(media): add audio and file display --------- Co-authored-by: HeavyGee <133152184+heavygee@users.noreply.github.com> Co-authored-by: Cursor <cursoragent@cursor.com> Co-authored-by: Debian <heavygee@oos-linux.in.lockhouse> |