mirror of
https://github.com/wu736139669/hapi.git
synced 2026-08-05 06:24:37 +00:00
0725fabe84e7a047e44bd98c10ed10da95826c27
124
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
0725fabe84 |
feat(web): pin running sessions in an in-progress section with state badges (#1315)
* feat(web): pin running sessions in an 'in progress' section with a live badge * feat(web): show project name on pinned running session rows * feat(web): make the pinned 'in progress' section collapsible * fix(web): don't auto-expand directory groups when opening pinned running sessions * fix(web): keep running section open while searching; clear auto-expand guard when selection leaves a group * feat(web): show machine label on pinned running session rows * fix(web): make running-section toggle keyboard-accessible with correct filtered state * feat(web): split pinned running section into working/pending/idle groups with distinct badges |
||
|
|
9d07857570 | Add provider-backed dictation mode (#1327) | ||
|
|
5b91504263 |
Add read-only session status panel (#1301)
* feat(web): add session status panel * fix(web): include nested session activity * fix(web): preserve incomplete status details * fix(web): handle anonymous terminal completion * fix(web): classify pending status accurately * fix(web): retain ambiguous terminal candidates |
||
|
|
68299631f9 | feat(web): customize composer toolbar visibility (#1298) | ||
|
|
0537ddf84a |
feat(web): make session header metadata configurable (#1267)
* feat(web): make session header metadata configurable * fix(web): align mobile header metadata priority |
||
|
|
fd20e584d6 | feat(web): refine composer status bar (#1281) | ||
|
|
b6897ee736 |
feat(web): replace session list refresh button with pull-to-refresh
Remove the refresh icon button from the session list toolbar and make the list itself the refresh affordance via a touch pull gesture. Gesture (SessionList): - Touch listeners on the scrollable list container; pull engages only at scrollTop 0, with 16px feedback / 64px trigger thresholds, and fires on release past the trigger. Mirrors the established pull-to-load-older pattern in HappyThread. - Touch-only by design: desktop has no overscroll bounce, so a wheel pull feels broken; desktop keeps relying on SSE live updates and query focus refetch. - onRefresh widened to () => Promise<unknown> | void so the indicator tracks the in-flight refetch and ignores re-entrant pulls. Feedback: - Status pill over the list (role=status, aria-live) shows pull/release/refreshing states with a spinner while refreshing; it also covers the initial useSessions load (isLoading), which lost its only busy indication when the toolbar button was removed. - The success toast is dropped (the pill is the feedback); the failure toast is kept. handleRefresh now returns its promise. Empty states (review P2s): - SessionsEmptyState and the no-results message move from the shrink-0 header container into the scroll container, so the gesture works on the visible empty state (retry path after a failed initial fetch) and short viewports scroll instead of crushing the gesture area. - SessionsEmptyState is gated on !isLoading so a slow initial request no longer flashes the final empty state with active actions. i18n: add sessions.refresh.pull/release/refreshing (en + zh-CN), remove now-unused button.refresh and sessions.refresh.success.*. Desktop wheel pull was implemented and then reverted after review. |
||
|
|
da921821ee |
fix(web): show Auto instead of Default for Cursor model picker (#1248)
* fix(web): show Auto instead of Default for Cursor model picker Cursor CLI uses `auto` for automatic model selection; labeling it "Default" in HAPI was confusing and inconsistent with `agent --list-models`. Fixes #1247 Co-authored-by: Cursor <cursoragent@cursor.com> * fix(web): align Cursor unavailable copy with Auto label Co-authored-by: Cursor <cursoragent@cursor.com> * chore: re-trigger Codex PR review Co-authored-by: Cursor <cursoragent@cursor.com> * docs(web): update Cursor catalog JSDoc for Auto label Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: Cursor <cursoragent@cursor.com> |
||
|
|
3f73a5f6ef |
feat(web): collapse session search into the sidebar toolbar row
Move the session-list search out of its dedicated full-width row and into the sidebar toolbar, so the sidebar gets one compact header row instead of two stacked rows. Behavior: - Collapsed by default: a search icon sits at the left of the toolbar row, on the same line as the existing tool icons (codex import, refresh, browse, settings, new session). - Clicking the icon expands the input to fill the entire row width (tool icons hide) and focuses the input; when focus leaves the search container the input collapses back to the icon. - Query and date range stay applied while collapsed; the icon shows an indicator dot whenever any filter is active. - The date-range picker stays inside the expanded input. Implementation: - SessionList gains a headerActions prop and renders a single header row: [collapsible search] [spacer] [optional renderHeader plus] [headerActions]. - SessionsPage drops its own toolbar wrapper and passes its five buttons through headerActions instead. - The safe-area top inset moves to the router container wrapping both the error banner and the list, so iOS PWA error text no longer sits beneath the status bar/notch. Focus handling (review follow-ups): - Controls that unmount themselves on click (clear-query X, picker backdrop, range-end selection, picker footer Clear) silently moved focus to <body>, leaving the search expanded but no longer collapsible via blur. Each now returns focus to the input; the picker footer Clear is wired through a dedicated onClear prop. - Header actions can no longer be suppressed by a stale expanded state: they render whenever the search control itself is absent (e.g. the session list empties via SSE), and the expansion resets when the list becomes empty. i18n: add sessions.search.open (en / zh-CN). Tests: adapt existing search/date-filter tests to expand the search before interacting; add regressions for collapse-on-blur query persistence, focus restore after clearing the query and after clearing the date range, and header actions surviving an emptied session list. |
||
|
|
0db7d68b37 |
feat(web): add stepwise session preview controls (#1266)
* feat(web): add stepwise session preview controls * fix(web): avoid no-op session preview collapse * fix(web): expand previews from rendered count |
||
|
|
584d1647d0 | fix(web): harden older history loading | ||
|
|
a742fdf1a8 |
feat(hub+web): include scratchlist in session export (#1235) (#1237)
Bump export schema to v2 with scratchlist text and attachment metadata so operators keep notes when they export-then-delete. Markdown gets a Scratchlist section; attachment bytes stay out of the JSON. Co-authored-by: Cursor <cursoragent@cursor.com> |
||
|
|
f8934d81ee |
feat(web): show machine + last-active in session detail header (#1244)
* feat(web): show machine and last-active in session header Multi-machine estates lose the machine signal after leaving list filter chips; surface machine label + relative age in SessionHeader meta row. Fixes #1241. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(web): keep session-header age fresh under keep-alive Treat detail-cache activeAt keep-alives as render-relevant now that the header reads them, and tick relative age every minute so labels advance without a session prop change. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(web): skip sub-minute activeAt keep-alives in detail cache Relative age only changes at 60s boundaries; accepting every ~10s heartbeat replaced the Session object and re-rendered the chat tree for no visible header change. Co-authored-by: Cursor <cursoragent@cursor.com> * chore: retrigger Codex PR review after stream disconnect Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: Cursor <cursoragent@cursor.com> |
||
|
|
46ab828daa | feat(web): show Hub SQLite storage usage in Settings (#1225) | ||
|
|
4c203f17cb |
feat(web,hub): scratchlist v2.2 hub attachment storage (#921) (#1205)
* feat(hub,shared): scratchlist v2.2 hub attachment storage foundation (#921) Hub stores scratchlist attachment bytes on filesystem; SQLite holds AttachmentMetadata[] JSON via session_scratchlist.attachments (v11→v12). Upstream ladder: v10→v11 text-only scratchlist table (#896), v11→v12 attachments column. Configurable limits via HAPI_SCRATCHLIST_* env vars. Upload, serve, and limits REST routes; delete entry cleans hub files. Web promote/rehydrate still TODO. Soup renumber branch follows. Co-authored-by: Cursor <cursoragent@cursor.com> * feat(web): scratchlist v2.2 attachment UX (#921) Route scratchlist-mode composer submits with attachments to hub storage, show image thumbnails in the drawer, and rehydrate attachments on promote to composer or queue (hub fetch → CLI upload for send). Co-authored-by: Cursor <cursoragent@cursor.com> * fix(web): scratchlist attach submit, float thumbs, copy tooltip (#921) Hub upload adapter now sets path on ready attachments so the composer send button unlocks in scratchlist mode; routing label matches attachments too. Entry thumbnails float left with text wrap; copy tooltip clarifies text-only. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(hub): adapt scratchlist update tests to patch API (#921) update() now takes { text?, attachments? }; v12 CRUD tests still passed a string. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(hub,web): harden scratchlist attachment ownership and orphan cleanup Resolve claimed hub paths against the current session before persist, count on-disk session bytes for upload caps, delete blobs dropped on entry update, and DELETE pending uploads when composer remove runs. Co-authored-by: Cursor <cursoragent@cursor.com> * chore: drop accidental .cursor files from attachment PR Co-authored-by: Cursor <cursoragent@cursor.com> * fix(web): exit scratchlist mode before rehydrate; delete raced uploads Promote-to-composer flushes mode exit so attachments use the chat adapter. Cancel-during-upload deletes the hub blob once upload returns. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(hub,web): exact UUID delete match; stage hub paths on chat send Reject partial attachment ids on disk delete, and restage scratchlist hub attachments through uploadFile when sending after leaving scratchlist mode. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(hub): skip text-only PUT resolve; cleanup session attachment dirs Text-only edits keep existing attachment metadata after session-id transfer. Require full UUID on resolve. Delete scratchlist attachment files when a session is deleted. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(hub,web): scratchlist attach route, PUT bytes, orphan deletes Park only hub-resident attachments; subtract removed blobs from the PUT session cap; delete attachment files only when no other entry still references them. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(hub): canonicalize scratchlist attachment filenames Resolve stores the on-disk sanitized name (not claimed.filename) and hardens Content-Disposition against CR/LF/quote injection. Co-authored-by: Cursor <cursoragent@cursor.com> * test(hub): cover toxic filename canonicalize on resolve Co-authored-by: Cursor <cursoragent@cursor.com> * fix(hub,web): serialize scratchlist uploads; drop hub blobs after chat stage Per-session upload lock keeps disk byte caps honest under concurrency. After a successful toggle-off chat send, delete the staged hub copies so they no longer count against the session attachment budget. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(shared,web): allow clearing scratchlist attachments; cleanup staged uploads PUT may send attachments:[] without a text change. Staging to chat rolls back partial normal-upload copies on failure. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(hub): re-key scratchlist attachment files on session merge Move hub blobs when scratchlist rows transfer between session ids so quota and path ownership stay correct. Reject PUT that would leave an empty textless entry after clearing attachments. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(web): reuse restored scratchlist hub attachments without re-upload Composer draft remount was re-uploading blobs that already had a hapi-hub:scratchlist path, orphaning the originals against session quota. Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: Cursor <cursoragent@cursor.com> |
||
|
|
8d1f84e20b |
feat: name your machines from web settings (#1214)
Machines are labelled by hostname with no way to give them a friendlier
name. `MachineMetadataSchema` has declared `displayName` all along and the
whole read path already honours it (`displayName → host → id`), but nothing
could ever write it: the CLI never sends the field, the hub exposed no route
that sets it, and the web UI had no editor.
Add the missing write path:
- `PATCH /api/machines/:id` with `{ displayName }`, guarded by the existing
`requireMachine`. An empty value removes the key so the label falls back to
the hostname; the empty string is never stored.
- `machineCache.renameMachine` merges that one key into the stored metadata
and lets `refreshMachine` publish `machine-updated`, which `useSSE` already
invalidates on — so every connected client relabels without new plumbing.
- A `/settings/machines` page listing online machines with inline rename,
placed between Voice and About so the existing preference pages keep their
order. Each row keeps the hostname visible, so a renamed machine is still
identifiable.
The merge reads the raw stored metadata rather than the cached `Machine`
view. That view is narrowed by `MachineMetadataSchema`, which strips unknown
keys and yields `null` for a row that fails validation — reachable, since the
CLI's `machine-update-metadata` handler accepts `z.unknown()`. Merging
against it would have written those fields out of existence.
The row's save is guarded by a ref rather than `isPending`: disabling the
focused input forces a blur, so Enter otherwise reaches `save` twice and
fires two PATCHes, the second of which can lose the version race and report
a failure for a rename that succeeded.
`mergeMachineMetadata` already preserves hub-side fields on CLI
re-registration, so a reconnect does not clobber the name.
Closes #1210
|
||
|
|
5f367dcb6a |
fix(web): polish shared turn image UX (#1208)
* feat(web): include session title in share image filename * fix(web): polish shared turn image UX |
||
|
|
fcb56989d9 |
feat(web): load older history by scrolling to top
Remove the redundant "Load older" button; the existing top sentinel already auto-loads older pages when approaching the top. Loading state moves to a floating pill overlay so prepends no longer shift layout. |
||
|
|
2235b924a7 |
feat(web,hub): scratchlist v2 - hub sync via typed table + session-updated piggyback (#896)
* feat(web,hub): scratchlist v2 - hub sync via typed table + session-updated piggyback (#893) Promotes scratchlist persistence from per-device localStorage to a hub- backed typed table so entries follow the operator across devices. v1 panel UI / FUE / shortcut / styling are deliberately unchanged - this is a backend + sync-layer feature. Hub side - New `session_scratchlist` typed table (sessionId, entryId, text, createdAt, updatedAt) with composite PK and FK ON DELETE CASCADE from sessions. Schema bumped V9 -> V10; idempotent migration added to the legacy + step ladders. - REST CRUD under `/api/sessions/:id/scratchlist[/:entryId]`, all routed through the existing `requireSessionFromParam` guard so namespace / ownership enforcement is identical to other session-scoped routes. - Per-session 200-entry cap enforced on POST. Duplicate entryId reported idempotently (200) so the migration retry path is safe. - `SessionPatchSchema` extended with `scratchlistUpdatedAt?: number`; every successful mutation emits a `session-updated` SSE patch with the token. (Following operator's piggyback decision; aligns with the parallel #884 patch-shape extension.) Web side - Hub becomes source of truth via TanStack Query (`queryKeys.scratchlist(sessionId)`); localStorage demoted to offline cache. Add / delete / update mutations are optimistic with rollback on error. - Silent first-load migration: existing localStorage entries are pushed to the hub preserving id + createdAt, and a one-time banner (mirroring `CursorMigrationBanner`) tells the operator their notes are now in the hub. Banner dismissal is per-session and persistent. - SSE handler queues a `scratchlist` invalidation when the patch carries `scratchlistUpdatedAt`, so cross-device + cross-tab updates land within an SSE round-trip. - Delete-session confirm copy now includes a count of scratchlist entries that will be cascade-deleted. Out of scope (separate tracking issue #894): "delete with summarize-and- migrate" UX flow. Tests - Hub: V9->V10 migration (fresh + multi-hop legacy + idempotent reopen + cascade-delete), `ScratchlistStore` CRUD + ordering, REST routes (happy path + 400/403/404/409), SyncEngine SSE emission. - Web: hook covers initial fetch, optimistic add/delete/update with rollback, localStorage migration + banner, cap enforcement, local-only reorder. Banner component renders only on `'completed'`. - Existing Playwright e2e (10 tests, panel UI regression) all pass unchanged. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(scratchlist): address HAPI Bot Major findings on PR #896 Two real data-correctness paths the bot caught on the initial review. 1. Migration partial-failure data loss The migration loop swallowed each failed POST and still wrote the `migrated` flag, while the offline-cache effect mirrored the (partial) hub state back into `hapi.scratchlist.v1.<sessionId>` - so a transient error or cap rejection could leave entries neither on the hub nor in localStorage. Fix: - Track failed entries during migration and persist them back to localStorage; do NOT advance the flag if any entry failed, so a future mount retries. - Gate the offline-cache effect on the migration flag. Pre- migration, localStorage holds the v1 entries the migration reads; mirroring an empty hub fetch over them was the wipe. - Drop the "skip migration when hub is non-empty" gate. Combined with the duplicate-idempotent POST short-circuit (below), a retry against a session that another device already populated is a safe union. 2. Duplicate POST returned 409 at cap The route checked `count >= SCRATCHLIST_MAX_ENTRIES` BEFORE asking the store whether the supplied `entryId` already existed, so an idempotent migration retry against a 200-row session returned 409 instead of 200. Fix: check duplicate first via a new `SyncEngine.getScratchlistEntry`, return the existing row with 200, and only run the cap check for genuinely new ids. Tests added: - hub/routes: at-cap + duplicate entryId returns 200 (not 409); at-cap + new entryId still 409. - web/hook: partial-failure persists the failed entries back to localStorage and leaves the flag unset; offline-cache effect does not wipe pre-migration localStorage. Co-authored-by: Cursor <cursoragent@cursor.com> * feat(web/scratchlist): per-entry age indicator (clock icon + tooltip) Surfaces the smart-relative time the entry was last saved on every scratchlist row, mirroring the bucketing used in the session list: just-now -> Nm -> Nh -> Nd -> absolute date. Implementation: - Extract the existing `formatRelativeTime` helper out of SessionList into `web/src/lib/relative-time.ts` so the panel can reuse the same buckets and i18n keys (no copy-paste drift between surfaces). Also add `formatAbsoluteDateTime` for the precise-stamp tooltip line. - Add `updatedAt?: number` to the local `ScratchlistEntry` shape. v1-only callers stay valid (the field is optional and `isEntry` now accepts rows that omit it). The hub hook forwards the hub's `updatedAt` so the indicator reflects edits, not just creation. - New `EntryAgeIndicator` component: clock SVG in the same style as the existing action icons, rendered inside both panel surfaces (the older `ScratchlistList` and the drawer variant). Falls back to `createdAt` when `updatedAt` is missing (legacy v1 rows during the migration window) and renders nothing if neither timestamp is usable. - Tooltip carries the relative bucket plus the absolute timestamp on a second line; aria-label carries the relative bucket only so screen readers stay terse. - Mirror `updatedAt` into the localStorage offline cache so an offline reload still has accurate ages. Tests: - `relative-time.test.ts`: bucket math, seconds-vs-ms detection, non-finite guard. - `ScratchlistPanel.test.tsx`: indicator renders with the right smart-relative bucket, falls back to `createdAt` when `updatedAt` is absent, and renders nothing when both timestamps are zero. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(scratchlist): bound client-supplied entryId length (HAPI Bot, PR #896) The POST /api/sessions/:id/scratchlist body validator left `entryId` unbounded (`z.string().min(1)`), but that string is persisted as part of the SQLite primary key. An authenticated/direct client could grow the table and its index well beyond the intended scratchlist limits by submitting oversized keys. Adds `SCRATCHLIST_MAX_ENTRY_ID_LENGTH = 128` (comfortably fits a UUID's 36 chars plus any prefix scheme we might layer on later) and applies `.max(...)` to the optional `entryId` in `ScratchlistEntryCreateRequestSchema`. Anything longer is rejected with 400 before the row hits SQLite. Test pins the new behavior: a 129-char id returns 400 and never reaches the engine. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(scratchlist): banner state machine - 'completed' is sticky until dismissed (HAPI Bot, PR #896) The previous state machine swallowed the migration banner if the operator reloaded the page before clicking dismiss: the migration flag was set on success, and on remount the init logic mapped a flag-set/dismiss-not-set session to 'pre-migrated', a state the banner explicitly refuses to render. Net effect: a migrated session never prompted for affirmative dismissal. Fixes: - Drop the 'pre-migrated' state. The dismissal flag is now the only signal that suppresses the banner; the migration flag alone means 'banner shows until dismissed' (now or after a reload). - Sessions that had nothing to migrate (no v1 entries in localStorage) pre-emptively write BOTH flags - migrated AND dismissed - so the bot's banner-stickiness fix doesn't surface a banner that has nothing to announce on freshly-created v2 sessions. Tests: - New `reload-before-dismiss leaves the banner visible` test pins the fix end-to-end: mount #1 migrates -> 'completed', unmount, mount #2 on the same session reads the localStorage flags and stays 'completed'. - New `opts fresh sessions out of the banner pre-emptively` test pins the no-v1-entries shortcut. - Existing `does not re-migrate on a mount where the migrated flag is already set` updated to assert 'completed' (not the dropped 'pre-migrated'). - Existing `skips migration when localStorage is empty` updated to assert the new 'dismissed' status + the banner-dismissed flag. - Banner test for the 'pre-migrated -> nothing' case removed (the state no longer exists). Co-authored-by: Cursor <cursoragent@cursor.com> * fix(scratchlist): transfer rows during session merge so cascade-delete does not strand them (closes #920 for v2.0) `mergeSessionData` in `sessionCache.ts` ends every merge codepath with `deleteSession(oldSessionId)`, which fires `ON DELETE CASCADE` on every FK-tied table. `session_scratchlist.session_id` is FK'd with cascade, so without an explicit transfer step every dedup (#448 agent-id collision) and every resume-of-inactive (`syncEngine.resumeSession` -> mergeSessions) silently destroys the operator's per-session notes. This is the gap upstream-discovery agent flagged on #920 against PR #896. With the 2026-06-15 hub-restart cascade incident as evidence (23 sessions auto-archived in a single bounce, 4 confirmed HAPI-id rotations across 2 bounces), unmitigated this would violate v2.0's "survives reloads / second laptop / clear-site-data" promise the first time the operator hits a hub bounce. Fix: - New `transferScratchlistEntries(db, fromSessionId, toSessionId)` in `hub/src/store/scratchlist.ts`. Atomic via BEGIN/COMMIT. Uses `UPDATE OR IGNORE` so rows that would collide on PRIMARY KEY (session_id, entry_id) simply do not move - the dedup target's copy wins, matching the operator's mental model that the consolidated session is authoritative. Cleans up any collision-loser rows so the no-delete codepath (`mergeSessionHistory`) is symmetric with the delete path. - Wired into `mergeSessionData` BEFORE the `deleteSession()` call, alongside the existing message-merge step. Both `mergeSessions` (deleteOld=true) and `mergeSessionHistory` (deleteOld=false) get coverage because both can rotate the visible session id. - Emits `session-updated{scratchlistUpdatedAt}` on the new session so any web client looking at the consolidated id invalidates and refetches; for the keep-old codepath the emit also fires on the old id since it stays alive but is now empty of scratchlist. Tests (`sessionCache-merge-scratchlist.test.ts`, 7 cases): - mergeSessions (deleteOld=true): rows move, old is gone, no stranded rows. - mergeSessions PK collision: dedup target wins, unique-to-old rows still come across. - mergeSessions SSE: exactly one scratchlist patch on the new id. - mergeSessions no-op: zero rows -> zero emits. - mergeSessionHistory (deleteOld=false): rows move, old session stays alive but empty of scratchlist. - mergeSessionHistory SSE: emits on BOTH old and new ids. - Cascade-delete safety smoke: post-merge, an explicit operator delete of the new session DOES cascade-delete its scratchlist (i.e. the FK cascade we want is intact; the bug was triggering it on the wrong id). Web layer note: v1 localStorage is keyed by HAPI session id; on rotation the old key is orphaned but no longer represents data loss because the hub now holds the canonical state and the offline-cache mirror re-populates `hapi.scratchlist.v1.<newId>` on first read of the consolidated session. Documented as a known limitation; not a blocker for v2.0 because the hub is the source of truth. #894 (v2.1 migrate-on-delete) inherits a related concern about operator-Delete vs merge-Delete consent flow - flagged in the upstream-discovery handoff, separate scope. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(scratchlist): rebase onto upstream/main - scratchlist migration is V10→V11 Upstream landed V9→V10 as sessions.service_tier (#898/#904). Scratchlist v2 moves to V10→V11 so both migrations coexist without clobbering each other. - mergeSessionData conflict resolved: keep upstream migrateFromV9ToV10 (service_tier) and add migrateFromV10ToV11 (session_scratchlist) - SCHEMA_VERSION bumped 10 → 11 - Rename migration-v10.test.ts → migration-v11.test.ts with updated multi-hop coverage (V9→V10→V11) - Add serviceTier: null to scratchlist route test session fixture (required by upstream Session type after #898) Co-authored-by: Cursor <cursoragent@cursor.com> * fix(scratchlist): emit SSE on all-collision merge when old session stays alive (HAPI Bot, PR #896) When mergeSessionHistory deletes every old scratchlist row via PK collision (moved=0, collided>0) the still-alive old session kept showing stale cached entries until an unrelated refetch. Emit scratchlistUpdatedAt on the old id whenever collided>0 on the keep-old codepath, not only when moved>0. New-session emit stays gated on moved>0 since the target row is unchanged on full collision. Test pins the all-collision mergeSessionHistory case. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(scratchlist): stabilize migration queryKey to stop POST retry loop (HAPI Bot, PR #896) useMemo on queryKeys.scratchlist(sessionId) so the migration effect does not re-fire every render after a failed POST clears migrationAttemptedRef. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(scratchlist): dedupe optimistic add when SSE refetch wins race (HAPI Bot, PR #896) onSuccess now drops both the temporary optimistic id and any existing row with the canonical entryId so a fast SSE invalidation cannot leave twins. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(scratchlist): treat 404 on update/delete as stale cache, not rollback (HAPI Bot, PR #896) When another client already removed an entry, keep it gone locally and invalidate instead of restoring previousData from optimistic rollback. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(scratchlist): drop optimistic add ghost when previousData missing (HAPI Bot, PR #896) onError now filters by optimisticEntryId if the initial fetch never populated cache, so a rejected POST cannot leave an unsaved note. Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: Cursor <cursoragent@cursor.com> Co-authored-by: Debian <heavygee@oos-linux.in.lockhouse> |
||
|
|
07db10f86d |
fix(web): expose Codex Fast and Plan on Create Session (#1017)
* fix(web): expose Codex Fast and Plan on Create Session Wire serviceTier and collaborationMode through spawn so Create can set the same Codex options chat Settings already supports (#1015). Co-authored-by: Cursor <cursoragent@cursor.com> * fix(cli): forward collaborationMode through machine spawn RPC Create Session Plan was accepted by the hub but dropped in apiMachine before buildCliArgs; also preserve collaborationMode on resume spawn. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(cli): correct stopSession mock type in spawn RPC test Co-authored-by: Cursor <cursoragent@cursor.com> * fix(web): keep Fast mode across Create draft restore while models load Co-authored-by: Cursor <cursoragent@cursor.com> * fix(web): preserve pending Fast selection * fix: apply Fast and Plan to imported Codex sessions * test: narrow imported Codex session id * fix: forward explicit Standard service tier * fix: integrate create-session controls with current main * test: close Codex RPC suite * fix: preserve existing session spawn field * fix(web): integrate Codex controls with current New Session form * fix(web): reconcile draft types and submit state * fix(hub): integrate spawn arguments with current resume flow * test(cli): isolate spawn RPC suite --------- Co-authored-by: Cursor <cursoragent@cursor.com> |
||
|
|
adb9e2094e | fix(web): stabilize session list alignment and scrolling (#1196) | ||
|
|
226b2d066a |
feat(hub): native companion (FCM) push channel + device registry + pairing QR (#803)
* feat(hub): native companion (FCM) push channel + device registry
Adds opt-in FCM HTTP v1 notification delivery so a companion mobile/wearable
app can receive permission, ready, and task notifications end-to-end. The
channel is gated entirely on FCM_SERVICE_ACCOUNT_PATH + FCM_PROJECT_ID being
set; operators not running a companion see zero behavior change.
What lands:
- POST/DELETE /api/devices/register — JWT-authed FCM token registry,
upsert on (namespace, deviceId, platform), platforms `phone` | `wear`.
- Sqlite v9 → v10 migration adds `fcm_devices` (idx on namespace + token).
- FcmService — minimal HTTP v1 client, RS256 service-account JWT via
jose (dep already in tree), 5-minute access-token cache, 401 retry.
- FcmNotificationChannel — implements NotificationChannel, sends data-only
FCM (so companion can route to phone+watch surfaces). Body composition
parses an optional trailing `AGENT_NOTIFY_SUMMARY {json}` line for richer
ready summaries; truncates plain assistant text to 280 chars otherwise.
Tags each payload with `severity` (info/warning/success/error) so clients
can color/categorise the notification.
- PushNotificationChannel gains a NativeFallbackProbe — when a namespace
has at least one registered FCM device, web-push and SSE in-page toast
are skipped so the operator does not double-notify on phone+browser.
Probe is no-op when no FCM device is registered; PWA-only setups
unchanged. Branch trace gated on HAPI_NOTIFY_DEBUG=1.
- shared/src/messages.ts — `extractAssistantPlainText` (codex + Claude SDK
shapes) and `extractNotifySummary` (strict end-anchored line parser).
- hub/src/notifications/toolArgs.ts — tool-arg formatters lifted out of
telegram/sessionView (kept duplicated there in this PR; refactor of
Telegram is a follow-up).
- docs/api/native-companion-contract.md — payload + endpoints + env vars,
versioned at contract v1.
Test coverage:
- 260 hub tests pass (incl. 23 new across FCM channel, push dedup,
v10 migration, devices route).
- 60 shared tests pass (messages parsers).
Notes for reviewers:
- Reference companion implementation lives in a separate Android repo
(Kotlin, phone APK + Wear OS APK) — this PR is hub-side only.
- No new runtime deps (`jose` and `zod` already declared in hub).
Co-authored-by: Cursor <cursoragent@cursor.com>
* docs(contract): clarify scope - companion is remote-hub client, not hub-on-phone
Adds a Scope section to the native-companion contract so anyone
implementing it knows the audience: operators running the hub on a
server who want phone/watch as a notification surface, not users
expecting a Termux-bundled hub. Mirrors the framing now in
heavygee/hapi-companion README.
Co-authored-by: Cursor <cursoragent@cursor.com>
* docs(contract): correct Scope section - hub topology is unchanged
Removes the prior framing that referenced a non-existent 'Termux
hub-on-phone' alternative. This contract describes a native client to
the same hub the PWA talks to; it does not change where the hub runs.
Co-authored-by: Cursor <cursoragent@cursor.com>
* feat(web): companion app pairing QR in Settings
Companion section in Settings renders a QR code encoding the deeplink
hapicompanion://bind?hub=<base>&code=<token>. Scanning it from the HAPI
companion app (Android phone or Wear OS) auto-fills the bind form and
authenticates against this hub - no manual URL/token paste.
QR is gated behind a Show button so the access token doesn't sit visible
on screen by default; a Copy link affordance and the textual deeplink
are also exposed for manual onboarding.
Adds qrcode + @types/qrcode to web/ (already a hub dep, no new resolved
package - just a workspace declaration).
Co-authored-by: Cursor <cursoragent@cursor.com>
* feat(hub): terminal QR for companion app pairing alongside PWA QR
After the existing PWA access QR is rendered on tunnel start, also print
the hapicompanion://bind?hub=...&code=... deeplink and a matching QR.
Same tunnel + token, different scheme: phones with the companion app
installed pick up the deeplink via the manifest intent filter; phones
without it ignore it and fall back to the PWA QR above.
QR rendering failure is non-fatal in both cases - the textual deeplink
above the QR is sufficient for manual paste.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(fcm): address HAPI Bot review on PR #803
Two bugs surfaced by the upstream review bot:
1) Web Push silently dropped when FCM is not actually configured.
The native-fallback probe only checked the device registry; it did
not check whether resolveFcmConfig() actually succeeded. So an
operator who previously enabled FCM, registered a phone, then later
started the hub WITHOUT FCM_SERVICE_ACCOUNT_PATH would see the probe
return true (devices still in DB) -> Web Push suppressed -> no FCM
channel registered -> notifications go to /dev/null.
Fix: extracted the probe construction into buildNativeFallbackProbe()
which short-circuits to () => false when fcmConfig is missing. Probe
never even consults the device store in the no-config branch, so
stale rows can never matter.
2) Transient FCM failures permanently unregistered devices.
sendToToken() returned a single boolean and sendToNamespace() removed
any device whose send returned false. A 429 (rate limit), 503
(server error), 401 (auth glitch), or even an ECONNREFUSED would
delete the device row, after which the user would need to re-pair to
get notifications again. The bot caught it; the fix is the obvious
one.
Fix: sendToToken() now returns 'sent' | 'invalid' | 'failed'.
- 'invalid' is reserved for the responses that genuinely indicate a
dead token: HTTP 404 with UNREGISTERED/NOT_FOUND, and HTTP 400
with INVALID_ARGUMENT explicitly referencing the token field.
- Everything else (429, 5xx, 401, 403, network errors) is 'failed'
and counts toward the failed tally without removing the device.
sendToNamespace() only calls removeDeviceByToken() on 'invalid'.
Tests: 11 new tests across two new files. fcmService.test.ts covers
all six branches (200, 404 unregistered, 429, 503, 401, network error)
plus a mixed-batch case that proves invalid tokens get removed in the
same call where transient-failure tokens survive. nativeFallbackProbe
.test.ts covers both no-config and configured branches plus the
explicit "no-config never touches the store" guarantee.
Hub test count: 273 -> 284 (all passing).
Co-authored-by: Cursor <cursoragent@cursor.com>
* docs(contract): correct FCM visibility rule and remove unsupported event type
HAPI Bot review on PR #803 caught two contract-doc accuracy gaps:
1) Visibility rule was wrong. Doc said "FCM fires when Web Push would
fire AND client not visible via SSE", but FcmNotificationChannel
ALWAYS fires regardless of PWA visibility (deliberately - native
companion is the canonical wrist-first surface, and there is a
passing test asserting this). Companion app implementers reading
the contract would have built foreground-suppression logic and
then dropped notifications when the PWA tab was open.
2) Documented `session-completed` event doesn't exist. NotificationHub
never calls into a 'session-completed' channel method on
FcmNotificationChannel; the type would never reach a native client.
Removed from the documented enum, leaving only the three actual
events: ready, permission-request, task-notification.
Co-authored-by: Cursor <cursoragent@cursor.com>
* docs(contract): drop trailing whitespace, use blank line for paragraph break
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(web): persist CLI access token after Telegram bind so pairing QR works
The Settings -> Companion pairing QR reads the original CLI access token
from localStorage (hapi_access_token::<baseUrl>) so it can be encoded into
the hapicompanion://bind deeplink. For browser/CLI logins useAuthSource
already persists the token via setAccessToken, but the Telegram Mini App
bind path went through useAuth.bind() which exchanged the typed CLI token
for a JWT and never persisted it. Telegram users therefore always saw the
"signed in via Telegram..." fallback and got no usable QR.
After a successful client.bind() we now mirror useAuthSource's behavior
and write the same accessToken to the same localStorage key, restoring
parity between the two auth paths. No change for browser/CLI users.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(fcm): gate native-fallback probe on rolling FCM health
The native-fallback probe previously returned true whenever FCM was
configured AND devices were registered, which suppressed web-push for
the namespace. The HAPI Bot correctly pointed out the gap: if the FCM
pipeline silently breaks (expired service-account key, sustained 5xx,
OAuth token-fetch failure, network blackhole) the operator gets nothing
on either channel until they manually intervene.
Approach (deliberate, not the bot's exact suggested fix):
- FcmService now keeps a small rolling window (last 8 outcomes) of send
attempts and exposes `isHealthy()`. The threshold is 5+/8 failures =
unhealthy; the buffer starts empty so a freshly-booted hub is
optimistic ("innocent until proven guilty") and does not double-fire
on event #1.
- Token-fetch failure (`getFcmAccessToken` throws) now records exactly
one health-failure (not one per device), short-circuits the send
loop, and returns a result so `sendToNamespace` no longer leaks the
exception.
- `invalid` token responses are explicitly excluded from the health
buffer because they are per-device facts (rotated/uninstalled token),
not pipeline failures - FCM was reachable, it just rejected one
stale token.
- `buildNativeFallbackProbe` now optionally accepts the FcmService and
short-circuits to "let web-push fire" when health is bad, before it
even queries the device registry. The single-arg call shape is still
supported for back-compat.
Why not the bot's exact suggestion ("invert: call FCM first, fall back
on result.sent === 0"):
- Couples PushNotificationChannel to FcmService and FcmSendPayload,
reversing the clean parallel-channel architecture established earlier
in this PR.
- Treats every transient single-event failure as fallback-worthy, which
re-opens the duplicate-notification race that the suppression logic
was added to close (FCM HTTP timeout that delivers later + the web
push we sent in the meantime = two pings).
- A rolling health window only flips on sustained breakage, which is
the actual operational scenario the bot is worried about.
The wrist-first design intent ("FCM fires unconditionally, web-push is
suppressed for the same namespace") documented in
docs/api/native-companion-contract.md is preserved on the happy path.
The probe only re-enables web-push when there is concrete evidence the
native pipeline is not delivering.
Tests:
- New FcmService.isHealthy suite covers empty-buffer, threshold flip,
recovery as failures age out of the window, invalid-token exclusion,
and network-error path.
- nativeFallbackProbe gains coverage for the unhealthy-but-registered,
healthy-and-registered, and absent-fcmService (back-compat) cases.
- All 292 hub tests still pass; typecheck clean.
Co-authored-by: Cursor <cursoragent@cursor.com>
* refactor(telegram): drop duplicate tool-args formatter, use shared module
The Telegram session view had its own copy of formatToolArgumentsDetailed
identical to the one in hub/src/notifications/toolArgs.ts (already used by
the FCM channel). Replace the local copy with an import.
Removes ~70 lines of duplication, plus the now-unused MAX_TOOL_ARGS_LENGTH
constant and `truncate` import. The shared signature accepts an optional
opts arg whose default maxArgLength is 150 - matching the prior constant -
so the call site is unchanged.
Two benign upgrades come along for the ride from the shared module:
?? instead of || on field fallbacks (no real-world difference; permission
arguments never carry empty-string fields), and String(...) wrapping plus
a typeof object guard that makes non-string values render gracefully
instead of throwing into the catch block.
Hub tests: 311 pass / 0 fail. Telegram subset: 5 pass / 0 fail. typecheck
green.
Cold-reviewed by an out-of-context Claude Opus peer before push.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(fcm): require positive evidence in health window before suppressing web-push
Addresses HAPI Bot Major review on PR #803.
The previous health gate treated an empty outcome buffer as healthy
("innocent until proven guilty"). That created a silent-blackhole window
on cold start with broken FCM credentials: the push channel suppressed
SSE/Web Push for the first ~5 events while the FCM channel attempted
each delivery and recorded failures, until enough stacked to flip the
threshold. Every notification in that gap was silently lost.
New invariant: isHealthy() requires at least one successful FCM send in
the recent window (HEALTH_WINDOW=8) AND failures below threshold
(HEALTH_FAILURE_THRESHOLD=5). Both conditions are necessary; either
alone is insufficient evidence to safely suppress web-push fallback.
Trade-off: one duplicated notification per hub restart per namespace.
On the first event after restart, web-push fires alongside FCM (because
the gate has no positive evidence yet). Once FCM records that first
success, the gate engages and subsequent events are FCM-only. Worth it
for guaranteed delivery during cold-start outages.
Tests reworked to match new semantics:
- "starts UNHEALTHY with empty buffer" (was: healthy)
- "flips to healthy after first successful send" (new)
- "stays unhealthy across failures-only run" (new, exercises the exact
blackhole scenario the bot flagged)
- "flips back to unhealthy after threshold breach with prior successes"
(renamed, establishes successes first)
- "invalid tokens don't count against health" (reworked: send a mixed
batch first to establish health, then verify invalids don't flip it)
- "network errors count as failures" (reworked: establish health first)
Hub tests: 313 pass / 0 fail. typecheck green.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(hub): bump FCM migration to V10→V11 after upstream service_tier V9→V10
Upstream/main landed sessions.service_tier at schema v10. The companion
FCM device registry now migrates at v11 so both changes compose cleanly
after the courtesy rebase onto current upstream/main.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(hub): per-dispatch native gate instead of stale FCM probe
FCM runs before web-push; PushNotificationChannel skips web/SSE only
when the same notify() dispatch already delivered via FCM. Removes the
isHealthy()+device-row probe that could suppress web-push after warm
FCM outages.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(hub,web): cap notifySummary for FCM limits; fix PWA test cast
Rebase follow-up: truncate AGENT_NOTIFY_SUMMARY summary/action before
FCM data payload (bot Major). Fix usePwaUpdate.test.ts setTimeout mock
cast so bun typecheck passes on current main.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(hub): cap all FCM notifySummary fields and task bodies
Whitelist and truncate AGENT_NOTIFY_SUMMARY auxiliary fields before
JSON serialization; cap task-notification summaries to glance limit.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(hub): FCM fetch timeouts and cap Grep/Glob permission args
10s AbortSignal.timeout on OAuth + FCM send so sequential web-push
fallback is not blocked on hung Google endpoints; truncate Grep/Glob
pattern in permission detail formatter.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(hub): bind FCM token to one namespace on re-pair
Delete stale fcm_devices rows sharing the same token when a native
install registers under a different namespace.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(web): localize Companion settings and pairing copy
Add en/zh-CN keys for the Companion section title and CompanionPairing
strings; matches locale-driven Settings pattern (bot Minor on #803).
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(hub): tighten FCM token-invalid detection and truncation edge cases
Parse FCM error JSON: only UNREGISTERED or token-field INVALID_ARGUMENT
unregister devices; generic NOT_FOUND stays transient. Guard limit<=3
in truncateReadyText so tiny action budgets cannot blow the glance cap.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(hub): parse FcmError details.errorCode for UNREGISTERED tokens
FCM v1 often returns HTTP 404 with root NOT_FOUND plus
details[].errorCode UNREGISTERED; prune those tokens while keeping
generic project/resource NOT_FOUND transient.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(web): mock AppContext for About Companion pairing in settings tests
Settings About now mounts CompanionPairing via useAppContext after the
#1027 hub redesign rebase; wrap the About route test with AppContext and
Companion mocks so the suite stays green.
Co-authored-by: Cursor <cursoragent@cursor.com>
* docs(contract): point companion auth at POST /api/auth, not /api/bind
Pairing QR carries the CLI access token as `code`. /api/bind requires
Telegram initData; native companions must use /api/auth with accessToken.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(web): mount Companion pairing under Settings General
About is version/links only after the settings hub redesign; pairing is
setup, so keep Companion with language prefs and update the route tests.
Co-authored-by: Cursor <cursoragent@cursor.com>
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Debian <heavygee@oos-linux.in.lockhouse>
|
||
|
|
b28088bfe3 | feat(web): show timestamps in conversation outline (#1111) | ||
|
|
4a63418103 | feat(web): share conversation turns as images (#1047) | ||
|
|
e52443f4cf |
feat(web): global word-wrap toggle for code and diff views (#985)
* refactor(web): add per-line shiki line-splitting helper * feat(web): add global word-wrap toggle for code, markdown, and diff views * feat(web): add a shaded gutter background behind line numbers * fix(web): make DiffView compact rows follow the global wrap setting |
||
|
|
2e54d9fdff |
feat(web): replace machine tree level with filter chips in session list
Single-machine users no longer expand a redundant machine layer; with multiple machines a chip filter bar (persisted, with hover health popup) replaces the collapsible machine headers. Directory groups now render top-level with machine-name suffixes when unfiltered. Also removes the redundant session/project count header text. |
||
|
|
84323496c6 | fix(web): clarify settings heading hierarchy (#1177) | ||
|
|
d90bde0b88 | feat(web): show tool execution timing (#1140) | ||
|
|
224ae07438 |
feat(web): customize composer toolbar layout (#1101)
* feat(web): customize composer toolbar layout * fix(web): reorder toolbar across split boundary * fix(web): match toolbar focus and visual order * fix(web): support accessible toolbar reordering |
||
|
|
33015b67db |
feat(web): add conversation outline search (#1102)
* feat(web): add conversation outline search * fix(web): keep outline close action accessible |
||
|
|
0834dc098c |
feat(web): indicate session activity in date picker (#1103)
* feat(web): indicate session activity in date picker * fix(web): expose session activity to assistive tech |
||
|
|
b7f1f4390f |
feat(web): copy session reference from context menu (#1144)
* feat(web): add Copy reference to session context menu Refs tiann/hapi#950 Adds a More actions item that copies a cross-session citation (see session "title" (/sessions/id) for context) instead of a bare share URL. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(web): sanitize session titles in copy-reference text JSON-escape titles and collapse whitespace so arbitrary session names cannot inject prompt text into cross-session citations. Addresses Codex review on tiann/hapi#951. Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: Cursor <cursoragent@cursor.com> |
||
|
|
aa5beb3af2 | feat(codex): preserve native exploration actions (#1139) | ||
|
|
e69ca0782f | feat(web): make grouped tool summaries specific (#1134) | ||
|
|
2623a51b0b |
feat(web): filter sessions by last activity (#1083)
* feat(web): filter sessions by last activity * test(web): make session date filter tests deterministic |
||
|
|
64834467e3 |
feat(codex): import and resume sessions from runners (#1088)
* fix codex import resume flow * fix hub restart session active state * fix codex transcript workspace scoping * Address Codex import review findings * Fix Codex import machine selection * Update Codex sessions error test * Address Codex import review findings * Preserve forked Codex session id on sync * Make Codex duplicate cleanup source-aware * Handle Codex archive failures * Limit existing session flag to Codex * Preserve Codex import machine binding * fix: rebase runner Codex import onto current main * fix: preserve runner-scoped Codex import behavior --------- Co-authored-by: syy <815728149@qq.com> |
||
|
|
77f94ef738 |
fix(web): keep machine names visible and health tooltips touchable (#1049)
* fix(web): improve machine health sidebar UX Keep machine names visible, align health metrics, and make nested health tooltips usable with touch and keyboard input. via [HAPI](https://hapi.run) Co-Authored-By: HAPI <noreply@hapi.run> * fix(web): preserve tooltip focus reveal groups Keep the unnamed group used by existing focus reveal classes while retaining named hover groups for nested machine health tooltips. via [HAPI](https://hapi.run) Co-Authored-By: HAPI <noreply@hapi.run> * fix(web): add exec* timestamps to ToolCard test fixture Unblocks typecheck after #1036 made execStartedAt/execCompletedAt required on ChatToolCall; fixture was missing both fields. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(web): align machine health status with meters Right-align the capacity status with the utilization meter edge to balance the tooltip header without shortening the bars. via [HAPI](https://hapi.run) Co-Authored-By: HAPI <noreply@hapi.run> * fix(web): restore machine health disclosure semantics Expose the machine group's expanded state on its toggle and describe the health trigger with the tooltip body for assistive technologies. via [HAPI](https://hapi.run) Co-Authored-By: HAPI <noreply@hapi.run> --------- Co-authored-by: HAPI <noreply@hapi.run> Co-authored-by: Debian <heavygee@oos-linux.in.lockhouse> Co-authored-by: Cursor <cursoragent@cursor.com> |
||
|
|
058a375e6d |
feat(web): enhance directory browser with metadata and sorting (#1055)
* feat(web): enhance directory browser with metadata and sorting * test(web): update ToolCard fixture timestamps |
||
|
|
520c3f511a |
fix: verify Cursor chat store before reopen (#1037)
* test: reproduce issue #841 * test: cover Cursor chat store discovery * fix: verify Cursor chat store before resume (closes #841) * test: preserve non-Cursor resume behavior * test: cover conservative Cursor resume gating * fix: gate Cursor reopen until store verification * test: cover legacy Cursor drawer fallback * fix: scan unique legacy Cursor store drawer * test: preserve raw Cursor workspace path hashing * fix: hash raw Cursor workspace path * test: pin Cursor probe owner and machine * fix: probe Cursor store on recorded owner * test: normalize Cursor probe owner home * fix: normalize Cursor probe owner home |
||
|
|
f8657dae3a |
feat(web): add color theme presets (#1040)
* feat(web): add color theme presets * fix(web): preserve presets with custom colors * fix(web): sync color theme across tabs * fix(web): clear boot theme background * feat(web): integrate palette presets with display settings |
||
|
|
2ce6d3ef3a |
feat(web): show tool call duration in the detail dialog (#1036)
* refactor(web): export formatDuration for reuse * feat(web): show tool call duration in the detail dialog Show a completed tool's execution duration at the top of its detail dialog. The value is derived from the Claude entry's own timestamps (the execution machine's wall clock) rather than the hub's message-receive time, and is used only when both the tool_use and tool_result entries carry a real timestamp — otherwise it falls back to the hub receive times on both sides, so the two clocks are never mixed. Running/pending tools show nothing, the running-state live timer is unchanged, and clock skew is guarded against. Reuses the existing formatDuration formatter. No schema changes. * fix(web): backfill hub startedAt on reorder so duration isn't 0.0s When a tool_result entry is reduced before its tool_use, the tool block is created from the result, so the hub startedAt is the result receive time. The tool_use path only lowered the exec start, not the hub startedAt, so a timestamp-less pair (no exec duration available) fell back to startedAt === completedAt and the detail dialog showed 0.0s. Lower the hub startedAt to the earlier tool_use receive time as well. |
||
|
|
f6ad345339 | feat(web): redesign responsive settings navigation (#1027) | ||
|
|
b9eed7c071 |
feat: add Grok Build support (#1030)
* test: define Grok Build integration behavior * feat: add Grok Build agent integration * test: cover Grok permissions and resume paths * docs: add Grok Build setup guide * fix: scope Grok ACP discovery to session cwd * fix: align Grok permission UI semantics * docs: clarify Grok runner setup * test: require Grok create model and effort options * feat: add Grok create model and effort pickers * test: define Grok runtime parity behavior * feat: add Grok runtime ACP controls and discovery * fix: tighten Grok runtime controls * fix: suppress nonfatal Grok title quota errors * feat: support Grok Auto permission mode * feat: forward ACP native session titles for Grok * fix: guard Grok Windows shell arguments |
||
|
|
d97b270ba8 |
fix(codex): bridge MCP elicitation through user input (#1008)
* fix(codex): bridge MCP elicitation through user input * fix(codex): allow MCP elicitation in yolo mode * fix(codex): preserve MCP form semantics * fix(codex): accept implicit MCP form mode * fix(codex): harden MCP elicitation prompts * fix(codex): require valid MCP choice answers * fix(codex): round-trip MCP array elicitation * fix(web): require explicit MCP URL confirmation * fix(codex): preserve MCP array item types * fix(codex): support multi-select MCP elicitation * fix(codex): allow MCP elicitation in read-only mode * fix(codex): route MCP tool approvals through permissions |
||
|
|
73584e925a |
feat(cursor): multitask slash, autoReview mode, native worktree/add-dir (#1014)
* feat(cursor): multitask slash, autoReview mode, native worktree/add-dir Close the highest-value Cursor Agent gaps for remote HAPI: expand ACP-safe slash pass-through (/multitask, worktree, add-dir, …), add autoReview permission mode (--auto-review spawn + mid-session slash), and route Cursor New Session worktrees through agent --worktree instead of HAPI sibling trees. Fixes #1013 Co-authored-by: Cursor <cursoragent@cursor.com> * fix(cli): accept --mode autoReview for hapi cursor Align --mode parsing with CURSOR_PERMISSION_MODES so documented `hapi cursor --mode autoReview` enables Smart Auto instead of silently falling back to default. Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: Cursor <cursoragent@cursor.com> |
||
|
|
4c76668a6c | refine message actions and metadata | ||
|
|
65e1708c78 |
feat(web): mermaid diagram lightbox on click (#741)
* feat(web): mermaid diagram lightbox on click Click rendered mermaid blocks in chat to open a zoomable full-screen viewer. Re-renders from source in the modal with the current theme. Closes #737. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(web): fit mermaid lightbox to viewport on open Auto-scale diagrams to fill the viewer instead of opening at intrinsic mermaid size. Reset returns to fit; zoom label is relative to fit (100%). Co-authored-by: Cursor <cursoragent@cursor.com> * fix(web): fit mermaid lightbox to device screen not inner panel Use visualViewport for fit scale, full-screen pan layer, and a floating toolbar so the diagram can use the whole display. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(web): show mermaid lightbox by reusing inline SVG Second mermaid.render on open often left a 0×0 SVG while fit scale was computed from the loading placeholder. Reuse the inline SVG in the modal and measure viewBox with retried fit-to-screen. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(web): uniquify mermaid SVG ids in lightbox clone Inlining the same mermaid markup twice duplicates element ids and breaks url(#ref) resolution in the modal copy. Prefix ids and hrefs for lightbox only. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(web): give mermaid lightbox SVG explicit dimensions Mermaid emits width="100%" with max-width in px; that collapses to 0×0 inside the centered lightbox layer. Derive width/height from viewBox for the uniquified lightbox clone. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(web): render mermaid lightbox via isolated SVG data URL String id rewrites broke mermaid's embedded CSS so only labels appeared zoomed. Rasterize the inline SVG to a data-URL img instead of duplicating markup in the DOM. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(web): lightbox re-renders SVG for sequence diagrams Data-URL images drop or blank some mermaid diagram types (sequence). Re-render with a modal-specific id into inline SVG on a code-bg panel, and add sequence theme variables for dark/light. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(web): mermaid lightbox uses inline SVG in shadow DOM Reuse the inline render in an isolated shadow root so sequence CSS stays intact, and fit the viewport from viewBox dimensions instead of the loading placeholder or width="100%" layout. Co-authored-by: Cursor <cursoragent@cursor.com> * test(web): Playwright lightbox coverage per mermaid diagram type Add e2e harness and a script that opens the lightbox for each diagram kind (flowchart through kanban). Fit uses inline getBBox() so compact charts like gitGraph fill the viewport. Co-authored-by: Cursor <cursoragent@cursor.com> * test(web): bounded Playwright via webServer, fix gantt fit sizing Playwright owns Vite lifecycle (no agent-spawned dev server). Fit uses viewBox unless viewBox padding is excessive (gitGraph); wide charts use width-based coverage in e2e. Co-authored-by: Cursor <cursoragent@cursor.com> * chore(web): gitignore Playwright test-results Co-authored-by: Cursor <cursoragent@cursor.com> * fix(web): address PR 741 bot feedback (typecheck, fit floor, gitignore) Guard lightbox open when svg is null; allow fit scale down to 0.01 while keeping 0.25 minimum for manual zoom; ignore Playwright test-results/ correctly. Co-authored-by: Cursor <cursoragent@cursor.com> * test(web): Playwright asserts click expands diagram vs inline Measure inline vs lightbox bounding box after click; require visible growth (area ratio or max dimension) plus dialog + shadow SVG content. Co-authored-by: Cursor <cursoragent@cursor.com> * test(web): Playwright against live HAPI session for mermaid lightbox Add seed script for a dedicated chat session, live hub Playwright suite (HAPI_LIVE=1), and dogfood doc. Live tests fail until driver serves shadow-DOM lightbox (catches gray-box regression on stale bundles). Co-authored-by: Cursor <cursoragent@cursor.com> * fix(web): undo wrapper transform in lightbox fit; carry fit floor in zoom Resolves PR #741 review threads (HAPI Bot Major): 1. measureSvgIntrinsicSize / measureContentSize prefer intrinsic dimensions (viewBox -> width/height attrs -> img.naturalSize) before getBoundingClientRect. When the rect is the only signal, divide by scaleRef.current so the 50/200ms refit retries stop compounding with the wrapper's scale(...) transform. Large diagrams no longer jump tiny or oversize after async render completes. 2. Interactive zoom (wheel/keys/buttons/pinch) now clamps with Math.min(MIN_SCALE, baseScaleRef.current). A diagram fitted below the normal 25% floor stays reachable instead of snapping back to 25% and clipping. Zoom-out button disabled threshold uses the same min. 3. Add Vitest coverage for both helpers (intrinsic precedence, scale-aware rect fallback, divide-by-zero guard) so regressions surface without needing the full Playwright stack. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(scripts): mermaid seed refuses to wipe non-fixture sessions HAPI Bot Major (PR #741): SESSION_ID is documented as overridable, and the script unconditionally deletes every message for the target session before seeding fixtures. If pointed at a real session id, that's silent data loss. Refuse to proceed when an existing session id has a tag other than 'mermaid-lightbox-e2e'. New ids and the canonical fixture session still seed normally; real sessions throw before any DELETE runs. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(web): normalize mermaid svg for lightbox shadow root Mermaid emits width="100%" on every diagram. Inside a shadow root whose host has no explicit size, that collapses to zero in Chromium for most diagram types - only ones that ship pixel attrs (e.g. journey) happen to render. Operator confirmed on the live driver: every diagram except journey opened to a grey rounded square. MermaidLightboxSvg now runs normalizeMermaidSvgForStandaloneDisplay before injecting (strips width/height="100%", bakes viewBox dims as pixels) and sets :host{display:inline-block} so the host sizes to the SVG. Inline svg in chat is unchanged - only the lightbox copy is normalized. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(web): keep mermaid lightbox content below the toolbar Operator screenshot showed the diagram top (e.g. pie 'Pets' title) clipped behind the toolbar bar. Two causes: 1. getScreenFitSize used the full viewport height, so the fit scale sized the diagram to fill an area the toolbar overlapped. 2. The viewport (drag/zoom area) was inset-0; content centered on the full viewport center, not the visible region's center, pushing the top behind the toolbar. Measure the toolbar with a ResizeObserver, subtract its height from the fit calculation (clamped at zero), and start the viewport region below the toolbar (top: toolbarHeight). Fit scale recomputes whenever toolbar height changes. Adds Vitest coverage for getScreenFitSize reserved-top math. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(web): guard ResizeObserver before constructing it HAPI Bot Major (PR #741): Vitest jsdom does not polyfill ResizeObserver, so the toolbar measure effect throws ReferenceError when the existing mermaid-diagram React tests open the lightbox. Same code path is also brittle in any browser/webview without the API. Fall back to plain window 'resize' listener when ResizeObserver is absent. Toolbar height won't auto-update on element resize without it, but the lightbox still renders and the resize listener catches the common viewport-rotation case. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(scripts): live mermaid playwright wrapper runs from repo root HAPI Bot Minor (PR #741): the wrapper sets cwd to scripts/, but the test:mermaid-lightbox:live npm script lives in the repo-root package.json, so spawning npm there exited before Playwright started. Switch cwd to the repo root and drop the unused WEB_DIR constant. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(web): accept signed viewBox values in mermaid lightbox normalize HAPI Bot Minor (PR #741): the viewBox regex only matched digits, dots, and spaces, so a valid viewBox with negative origin (e.g. '-8 -8 640 480') returned null. normalizeMermaidSvgForStandaloneDisplay then became a no-op and left width='100%', re-introducing the zero-sized lightbox render this PR is meant to fix for the affected diagrams. Switch to the bot's suggested regex (signed numbers, single or double quotes, comma or space separators) and reject NaN parts. Adds Vitest coverage for signed origins, single quotes, comma separators, the malformed/no-viewBox null paths, and an end-to-end normalize test that fails against the old regex. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(web): align @playwright/test on 1.60.0 across workspaces HAPI Bot Major (PR #741): web/package.json pinned @playwright/test at 1.49.1 while the root workspace and bun.lock were on 1.60.0. The mismatch surfaced after rebasing onto upstream/main, where the root had already moved to 1.60.0 while my web devDependency lagged from an older commit. A frozen install would reject the lockfile and the new web e2e script could resolve a different Playwright than root scripts. Bump the web devDependency to 1.60.0 and regenerate bun.lock so all workspaces share one Playwright version. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(web): move mermaid playwright fixtures out of public HAPI Bot Minor (PR #741): the e2e and smoke fixtures lived under web/public, so Vite copied them verbatim into web/dist and the hub asset generator embedded them in production bundles. Both pages import Vite dev-only paths (/@react-refresh and /src/dev/...), so the production /mermaid-lightbox-{e2e,smoke}.html routes would 404 on those imports. Move both fixtures to web/e2e-fixtures/ to match the existing scratchlist-fixture pattern (relative ../src/dev import, served by Vite at /e2e-fixtures/...) and update the Playwright spec to hit the new path. Build now ships 112 PWA precache entries instead of 114 (both fixtures excluded from dist). Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: Cursor <cursoragent@cursor.com> |
||
|
|
a82dd49049 |
feat(web): markdown Source | Preview toggle in session file pane (#957)
* feat(web): markdown Source | Preview toggle in session file pane Add Source | Preview toggle for .md/.mdx files in the session file route, defaulting to preview with localStorage persistence. Reuse chat markdown pipeline via MarkdownRenderer standalone mode (no assistant-ui thread). Includes unit tests, Playwright smoke, and e2e fixture. Closes tiann/hapi#954 Co-authored-by: Cursor <cursoragent@cursor.com> * fix(web): cast standalone MarkdownRenderer components for react-markdown Soup verify gate: defaultComponents merge type is wider than react-markdown Components; standalone file-pane path needs explicit cast. * fix(web): route file-pane markdown fences through SyntaxHighlighter Standalone file preview now mirrors chat code-block rendering: fenced blocks use SyntaxHighlighter and MARKDOWN_COMPONENTS_BY_LANGUAGE (mermaid included) without requiring ThreadPrimitive context. Addresses HAPI Bot Major on tiann/hapi#957. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(web): detect fenced vs inline code in standalone markdown preview Move block detection to the pre override (react-markdown v10 does not pass inline to custom code components). Add inline-code regression test. Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: Cursor <cursoragent@cursor.com> |
||
|
|
26a24bb6ce |
feat(web,hub,cli): show machine health in session sidebar (#962)
* feat(web,hub,cli): show machine load in session sidebar Runners attach OS health snapshots to machine-alive heartbeats; the hub caches them and the web session list renders load or CPU between the machine label and session count. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(web,cli): show CPU and RAM pressure in machine health badge Sidebar label now combines CPU and RAM percentages for overload signaling; load stays in the tooltip on Unix. Prime CPU sampling so the first heartbeat includes usage, not just memory. Co-authored-by: Cursor <cursoragent@cursor.com> * feat(web): visual machine health meters with tooltip Replace bare CPU/RAM text with labeled mini bar gauges, chip border tint by severity, and a HoverTooltip explaining capacity and overload guidance. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(web): widen machine health tooltip with horizontal layout Allow a generous popover width and lay CPU/RAM/load out side by side so the capacity tooltip reads wider and less tall than the chip. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(web): anchor machine health tooltip to row left edge Wide tooltip was align=end on the chip, so it grew left off-screen. Use row-span positioning on the machine tile button instead. Co-authored-by: Cursor <cursoragent@cursor.com> * feat(web): machine host card with OS label and inline health Turn the session sidebar machine row into a bordered host panel with OS metadata and side-by-side CPU/RAM meters embedded in the tile instead of a flat label line matching project rows. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(web): keep machine host tile single-row height Collapse the machine header back to one py-1.5 row with OS and compact inline health beside the name, and restore the original project indent without the extra nested rail or second header line. Co-authored-by: Cursor <cursoragent@cursor.com> * feat(web): show CPU core count in machine health tooltip When the runner reports cpuCount, the tooltip reads "CPU across all 6 cores" instead of the generic all-cores label. Co-authored-by: Cursor <cursoragent@cursor.com> * docs: add machine health sidebar screenshots Dogfood captures for the session sidebar machine tile and capacity tooltip, for upstream PR review. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(cli): clear machine-alive priming timeout on disconnect Track the 50ms CPU priming setTimeout and clear it in stopKeepAlive so disconnect/shutdown during the delay cannot leave a stray interval alive. Co-authored-by: Cursor <cursoragent@cursor.com> * chore: drop dogfood screenshots from upstream PR diff Review evidence lives in the PR discussion only; no need to ship PNGs in the repo long-term. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(web): truncate long machine OS/host metadata in sidebar row Bound the metadata span so a long hostname cannot push the health chip or session count off-screen in narrow sidebars. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(web): reveal machine health tooltip on keyboard row focus Wire MACHINE_ROW_TOOLTIP_FOCUS_CLASS and aria-describedby on the machine header button so keyboard users can read the health tooltip like session rows. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(cli): use MemAvailable for Linux RAM pressure on Bun Bun's os.freemem() reflects MemFree (~1% on cache-heavy hosts), which made sidebar RAM read ~99% while btop showed ~40% used. Parse /proc/meminfo MemAvailable instead so used percent matches operator tools. Co-authored-by: Cursor <cursoragent@cursor.com> * feat(web,cli): show machine uptime in sidebar tiles and tooltip Collect os.uptime() as uptimeSeconds on keepalive and render compact up 1h 54m in the machine meta row plus an Uptime line in the health tooltip. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(web): anchor machine health tooltip to chip not row align=row positioned the tooltip below the full machine header button, so the collapsible project panel painted over it on hover. Use align=end with a min-width panel so mouse and keyboard tooltips stay visible. Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: Cursor <cursoragent@cursor.com> |
||
|
|
a0259b531e |
feat(web): drag-and-drop files onto chat panel to add as attachments (#936)
* feat(web): drag-and-drop files onto chat panel to add as attachments Closes #935 Adds a `useDragOver` hook that detects when a file is being dragged over the browser window and suppresses the browser's default file-open behaviour for drops outside the accept zone. A new `DragDropZone` component wraps the inner `AssistantRuntimeProvider` content in `SessionChat`. It shows a semi-transparent overlay (dashed border + "Drop to attach" label) on the right-side chat panel as soon as any file drag is detected — regardless of where the pointer is on the page. Dropping on the right panel adds the files as composer attachments via the existing `api.composer().addAttachment()` path. Drops on the left sidebar are suppressed (no navigation, no attachment). via [HAPI](https://hapi.run) Co-Authored-By: HAPI <noreply@hapi.run> * fix(web): disable drag-drop zone when pendingSchedule is active The backend rejects requests with both scheduledAt and attachments. DragDropZone now respects pendingSchedule the same way paste and the attach button do — disabled=true suppresses the overlay, sets dropEffect='none', and skips addAttachment on drop. via [HAPI](https://hapi.run) Co-Authored-By: HAPI <noreply@hapi.run> * fix(web): harden drag-drop default-action handling Address HAPI Bot review on #936: - useDragOver: cancel the browser's default file-open/navigation on the document-level `drop` event for file payloads, not only on `dragover`. Preventing default on `dragover` alone still lets the browser open a file dropped outside any zone (e.g. the sidebar), which could unload the app. - DragDropZone: only preventDefault when the drop payload actually contains files, so non-file drops (e.g. dragging selected text into the composer) keep their default browser behaviour. Add regression tests for both. via [HAPI](https://hapi.run) Co-Authored-By: HAPI <noreply@hapi.run> Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(web): use Simplified Chinese for composer.dropToAttach in zh-CN Address HAPI Bot review on #936: the new zh-CN string used Traditional Chinese forms (放開以附加檔案) in the Simplified Chinese locale, which is inconsistent with neighbouring keys (e.g. composer.attach = 添加文件). Use 松开以添加文件 to match. via [HAPI](https://hapi.run) Co-Authored-By: HAPI <noreply@hapi.run> Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: HAPI <noreply@hapi.run> Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> |