* feat: add cache-aware token usage dashboard
Track normalized Claude, Codex, and ACP usage with incremental SQLite backfill. Exclude imported transcript history, rebuild usage after history rewrites, and expose an owner-only dashboard with cache-aware totals and breakdowns.
via [HAPI](https://hapi.run)
Co-Authored-By: HAPI <noreply@hapi.run>
* fix: preserve usage model and local dates
via [HAPI](https://hapi.run)
Co-Authored-By: HAPI <noreply@hapi.run>
* fix: normalize cached usage and timezone buckets
via [HAPI](https://hapi.run)
Co-Authored-By: HAPI <noreply@hapi.run>
---------
Co-authored-by: HAPI <noreply@hapi.run>
Settings truncated read-only fixtures to 800 chars so scrolling never
revealed the rest. Default preview is now the full document; explicit
caps remain for tests.
Closes#1341
Co-authored-by: Cursor <cursoragent@cursor.com>
* feat(web): show hidden directories in workspace browser
Add optional includeHidden param to the machine list-directory RPC so the
WorkspaceBrowser can toggle hidden (dot-prefixed) entries. Default remains
filtered for backward compatibility; the toggle persists via localStorage.
* fix(web): disable show-hidden toggle while directory loading
Prevent overlapping list-directory requests with opposite includeHidden
values; the toggle is now disabled while a directory load is active.
* fix(web): migrate chat-path attachments on scratchlist park (#1226)
When an image is attached via the normal upload adapter before scratchlist
mode is enabled, toggling mode swapped adapters and send() dropped the
metadata — park stored text-only and cleared chips. Migrate pending
chat-path files into hub scratchlist storage on send, and fail closed if
non-hub paths still reach the park wrapper.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(web): defer chat-path cleanup until scratchlist park succeeds
send() migrates before scratchlist.add; deleting the original upload
there left retries pointing at a missing chat blob when park failed.
Stamp migratedFromPath and clean up only after the park attempt result.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(web): park scratchlist before composer.send clears chips
assistant-ui empties text/attachments before onNew, so return false from
park could not restore retryable state and rejected cleanup deleted the
migrated hub blob. Intercept park from a live snapshot; clear only after
accept; releaseWithoutDelete so clearAttachments keeps parked hubs.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(web): lock composer while scratchlist park is in flight
Disable input/send and hide chip remove during migrate+add so mid-flight
edits are not wiped on success and hub blobs are not deleted out from
under the accepted entry.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(web): do not clear composer if park snapshot changed mid-flight
Compare post-await composer state to the pre-park snapshot before
clearing; disable DragDropZone and scratchlist promote while parking
so parent paths cannot add chips the clear would silently drop.
Addresses Codex Major on #1227 (preserve post-snapshot composer changes).
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(web): defer migrated chat-path cleanup until park snapshot clears
Return ScratchlistParkResult.beforeClear from onParkScratchlist so
finalizeMigratedScratchlistParkCleanup runs only after HappyComposer
confirms the composer was unchanged mid-flight.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(web): flush rich mentions before scratchlist park snapshot
Park snapshots composer.text after flushSerializedText so session
@-mention chips serialize to markdown links before scratchlist add.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(web): validate park snapshot before scratchlist add
Split prepare/commit/abort so mid-flight composer edits abort orphan
hub blobs instead of parking a duplicate. Reuse restored hub paths in
prepareScratchlistParkAttachments so remounted chips do not re-upload.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(web): ignore scratchlist toggle hotkey while park is in flight
Co-authored-by: Cursor <cursoragent@cursor.com>
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
* feat: add message-level conversation fork and rewind
Expose native Codex/Grok/Claude history controls through hub REST+RPC and web ConfirmDialog actions, without file rewind or composed forks. Also reconcile the duplicate hub V14→V15 migration so typecheck can pass.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix: hydrate fork transcript and consume Claude --fork-session
Forked HAPI children now copy the source transcript prefix so navigation is not a blank thread, and Claude drops --fork-session after the first launch so relaunches do not branch again.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix: harden fork/rewind concurrency and durable history points
Skip pending scheduled rows when hydrating fork transcripts, serialize fork/rewind per session, and persist conversation history points/indexes across existing-session bootstrap and Grok relaunches.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix: close remaining fork/rewind races and UI anchoring
Block sends and scheduled maturation while history actions run, order fork prefixes by invocation time, inherit history locators into children, and only offer Fork current on the live tail boundary.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix: address remaining fork/rewind bot findings
Materialize Claude --fork-session before the first child prompt, validate
HAPI history boundaries before native RPC, expose forkCurrent on a latest
user boundary, fully demote unsupported conversationHistory capabilities,
and fix the truncate test setup order.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix: align fork-current ids and Claude fork bootstrap
Compare the latest fork boundary in assistant-ui threadMessageId space,
spawn Claude forks with the persisted session mode, and preserve
forkedFrom across existing-session bootstrap.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix: close fork/rewind consistency holes at the contract layer
Hold the source history lock until Claude child binds a distinct native
id, persist Codex localId→turnId locators, and mark/block diverged
sessions when native rewind outruns HAPI truncate.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix: use Codex stable lastTurnId for historical fork
Map HAPI's exclusive boundary to the previous turn's inclusive
lastTurnId so native fork context matches the hydrated transcript.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix: require exact Grok native resume for fork children
Reject newSession fallback when forkedFrom is set, and keep the hub
history lock until the child binds the forked grokSessionId.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix: kill active fork children before failed-fork cleanup
Bind/readiness failures can leave the child process running; deleteSession
rejects active rows, so terminate first then remove the HAPI session.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix: close remaining fork lock, hydrate, and todos gaps
Reject mode switches during history actions, batch-copy fork
transcripts in one SQLite transaction, and rebuild todos after
fork hydrate / rewind truncate.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix: allow Codex historical fork before the first turn
Use experimental beforeTurnId when there is no previous turn for the
stable inclusive lastTurnId boundary.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix: mark Grok history busy immediately after dequeue
Hub idle checks clear once messages-consumed fires; hold the busy flag
across permission sync and rewind-points lookup before prompt starts.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix: encode copied conversation history content
* fix(web): hide local conversation history actions
* style(codex): remove trailing whitespace
* fix(fork): preserve children when cleanup is unconfirmed
* fix(history): confirm cleanup and guard rewind divergence
* fix(history): probe capabilities before advertising
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
* 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
Keep touch and pen input eligible to cancel initial bottom settling when native panning dispatches pointercancel before scroll. Add interaction coverage for the mobile event sequence and unrelated global cancellations.
- @assistant-ui/react ^0.11.53 -> ^0.14.29, react-markdown ^0.11.9 -> ^0.14.7
- resolves @assistant-ui/tap 0.9.8, which ships the upstream fix for
bulk message prepends (per-scheduler MAX_UPDATE_DEPTH guard, PR
assistant-ui/assistant-ui#5370) that the local patch covered for 0.3.5
- API migration: useAssistantApi -> useAui, useAssistantState -> useAuiState
with s.* selector access; TextMessagePart type-guard for content.find;
portable DefaultComponentsMap annotation for memoizeMarkdownComponents
Verified: tsc clean, 1762 unit tests, history-load e2e 12/12 against the
unpatched upstream scheduler.
Remote sessions get an authoritative context_window injected into usage by
the CLI (from SDK result modelUsage), but local-mode sessions forward raw
transcript JSONL whose usage has no context_window, so the web fell back to
getContextBudgetTokens — which could not tell Fable's 1M window from its
bare id (only the "[1m]" suffix was recognized) and received session.model,
which is usually null for local sessions, defaulting to a 200k budget. A 1M
Fable session with ~256k of context showed 135% used with an over-limit
warning instead of ~26%.
- modelConfig: recognize Fable ids (fable, fable[1m], claude-fable-*) as 1M
- reducer: LatestUsage carries the usage-bearing message's own model
- StatusBar: new contextModel prop feeds the fallback heuristic only;
the model prop's other semantics are unchanged
- HappyComposer forwards it; SessionChat passes
latestUsage.model ?? session.model
Replace the always-visible wrapping chip row below the md breakpoint with a
filter icon button in the session list header (right side, next to the new
session button). The button opens a radio menu with per-machine counts and
an inline health summary, shows an active-filter dot, clamps to the
remaining viewport/safe-area space, and supports Escape/Arrow-key
navigation with focus restore. Desktop keeps the one-tap chip bar.
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.
* 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>
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.
* feat(web): feature-flagged rich composer for inline session @ mentions
Custom segmented contenteditable (not TipTap) inserts caret-local session
atoms from the existing @ picker and serializes to markdown links on send.
Textarea path remains default until flag parity dogfood.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(web): rich composer mention boundary + #1215 refs
Treat U+FFFC mirror atoms as word boundaries so @ after a session
token still opens autocomplete. Point comments at Fixes#1215.
Co-authored-by: Cursor <cursoragent@cursor.com>
* test(web): peer-stack e2e for rich composer session @ mentions (#1215)
Smoke: flag on, @ picker inserts inline session atom chip (not prose dump).
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(web): preserve newlines in rich composer Enter-newline mode
Chromium splits contenteditable on Enter into block divs; serialize those
as \\n and insert <br> when parent leaves Enter unhandled (Shift+Enter /
enter-inserts-newline).
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(web): show @ badge when rich composer mentions flag is on
Dogfood was invisible: flag-off looks like a normal textarea, and flag-on
had no chrome. Surface a small @ badge when enabled.
* fix(web): rich session composer on by default (not a user setting)
The plan dual-path was an engineering kill-switch, not an opt-in. Default
to the segmented composer; only richMentions=0 disables. Drop the flag
badge and record a peer-stack motion proof covering chips + baseline UX.
* fix(web): make rich composer Shift+Enter create a visible newline
Trailing <br>+empty text node was a silent no-op at EOL. Use
insertLineBreak (ZWSP pad fallback), assert real \\n in peer e2e.
* feat(web): hover tooltips on rich composer session chips
Show full title, status, short id, and path on chip hover via a portal
bubble fed by live useSessions lookup (drafts fall back to title + id).
* fix(web): dismiss rich composer chip tooltips on mouse leave
contenteditable pointerout/relatedTarget was flaky so tips stuck after
leaving the chip. Hit-test on pointermove, clear on prose/input/leave.
* fix(web): address cold-review Blocker/Majors on rich composer
Exclude peer e2e from default Playwright; force plain-text paste; restore
newline hard-stop in findActiveWord; fix root-anchored selection mapping
and nested-block serialize; cover with unit tests.
* chore: drop accidental .cursor files from rich-composer tip
* fix(web): close remaining cold-review gaps on rich composer
Drop absolute peer e2e tooling imports, prove chip→markdown send, and
harden paste/EOL/focus/tooltip/Enter edges before Meta rematerialize.
Co-authored-by: Cursor <cursoragent@cursor.com>
* chore: absorb soup playwright.config union for clean remat
Keep fork peer-stack timeouts/annotated-video wiring and add testIgnore
for e2e/peer so the next driver rematerialize does not conflict.
Co-authored-by: Cursor <cursoragent@cursor.com>
* revert: drop fork playwright tooling from upstreamable tip
Peer-stack annotated-video + HAPI_PEER wiring stay on fork main / soup.
Product tip only needs testIgnore for e2e/peer (see docs/tooling/peer-stack.md).
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(web): fix rich composer Shift+Enter double newline and paste space
Prefer manual newline+pad over execCommand insertLineBreak, and stop
applying autocomplete trailing-space on paste/drop paths.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(web): pad EOL Shift+Enter after Range.insertNode split
insertNode always leaves an empty text sibling, so !nextSibling never
saw EOL; detect meaningful trailing content and cover with jsdom tests.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(web): drop custom onDrop from rich composer
Intercepting drop without caretRangeFromPoint landed text at EOF or
no-oped in-editor moves. Native CE drop is enough for #1215; paste
still forces plain text.
Co-authored-by: Cursor <cursoragent@cursor.com>
* feat(web): sidebar-parity tooltips on rich composer session chips
Reuse SessionRowSummary (flavor, thinking/attention, schedule, todos,
relative ago, path) for chip hover so the tip matches the session list.
Co-authored-by: Cursor <cursoragent@cursor.com>
* chore: keep peer-stack e2e off the upstreamable tip
Peer specs and playwright.peer.config stay on fork main per
docs/tooling/peer-stack.md; default config still testIgnore's e2e/peer.
Co-authored-by: Cursor <cursoragent@cursor.com>
* feat: cite sessions with UUID wire + inspect_peer for agent/overseer
Rich composer chips already serialize to [title](/sessions/<id>); flush
before send so the agent prompt never gets title-only chip text. Add
inspect_peer (MCP + hapi inspect-peer) as the read twin of ping_peer so
that same id is immediately usable for overseer/agent peer lookup, with
system-prompt glue from citations to inspect/ping.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(cli): gate inspect_peer behind permission approval
Cross-session history reads need the same prompt path as ping_peer:
keep inspect_peer off Claude --allowedTools and treat it as sensitive
in ACP/OpenCode read-only mode so prompt injection cannot silently
enumerate peer transcripts.
Co-authored-by: Cursor <cursoragent@cursor.com>
* chore: clarify playwright peer testIgnore is upstream-safe
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(web): keep session UUIDs on rich composer copy/cut/paste
Copy/cut write wire markdown so chips do not collapse to @title-only
clipboard text; paste reparses session links back into atoms.
Co-authored-by: Cursor <cursoragent@cursor.com>
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
Remove the top-right Loading messages pill shown while tail reconciliation runs with cached conversation content.
Keep background tail synchronization silent so it does not compete with older-history loading feedback or imply that the visible conversation is blocked. Cold-start skeletons, pull-to-load guidance, older-page loading state, unseen-message navigation, and global reconnect feedback remain unchanged.
* fix(web): count unseen messages by rendered block, not raw message
The "N new messages" pill counted raw DecryptedMessages while the
timeline renders folded blocks, so the two never agreed. A subagent run
is dozens of sidechain messages but a single Task card; a tool_use and
its tool_result are two messages and one card; consecutive tools collapse
into one group. The pill could read "47 new messages" when scrolling down
revealed two new rows.
collectNewUnseenIds never inspected isSidechain, and it could not: the
reducer's grouping is stateful (it needs the Task tool_use before it can
map parentToolUseId), so a per-message predicate in the store cannot
reproduce it. Adding an isSidechain check there would also invert the
error for orphan sidechain messages, which tracer.ts falls back to
emitting at the top level.
Instead, drop the store's unseen bookkeeping entirely and count what the
renderer actually produced. Watermark the visible blocks when the user
scrolls away from the tail, then count the blocks past the last one they
had seen.
The count is anchor-based rather than timestamp-based because the blocks
array is not monotonic in createdAt: messages sort by invokedAt ??
createdAt, so a queued message carries an old createdAt while sitting at
the end. Anchoring also makes prepended history free, since older blocks
land before the anchor.
Known limit, documented at the call site: once the history window fills
up, mergeIntoWindow trims incoming messages off the tail, so the pill
reports 0 instead of a count. Under-reporting is preferable here, and
returning to the tail force-refetches the latest page anyway.
* fix(web): keep unseen watermark stable across optimistic id replacement
The watermark snapshotted only block.id, but that id is not stable for
the user's own messages: mergeMessages replaces an optimistic row with a
stored row that keeps localId under a new server id, and the user block
renders with the message id. Scrolling into history while an own message
was still optimistic meant its echo anchored one block earlier and bumped
the pill by one, with no new rendered row.
Track localId alongside id in the watermark and match on either.
Reported by HAPI Bot on #1255.
* fix(web): count joined assistant cards, not pre-join blocks
visibleBlocks is still not one-to-one with rendered rows: assistant-ui
joins a run of adjacent assistant-role blocks into a single card, so a
response made of reasoning + text + a tool call was reported as three new
messages instead of one, and appending another block to an in-flight
response bumped the pill without adding a row.
Walk the blocks after the anchor and only start a new row where the
assistant run breaks.
Role assignment is the part that would drift, so rather than restating it,
visibleBlockRole moves from assistant-runtime.ts to toolGroups.ts (next to
the VisibleChatBlock definition it describes) and both the runtime and the
counter import the one copy.
Reported by HAPI Bot on #1255.