Commit Graph
754 Commits
Author SHA1 Message Date
ad9fa5d711 feat(web): To agent control for terminal scrollback or selection into composer (#2012)
* feat(web): send terminal buffer or selection to agent composer

Add a To agent control on the session terminal page that appends the
xterm buffer (or current selection) to the session composer draft and
navigates back to chat. Button label flips when a selection is active.

Fixes tiann/hapi#2011

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

* feat(web): FUE on composer Terminal for send-to-agent

Advertise To agent / Selection to agent on the composer Terminal icon
with feature id composer-terminal-to-agent. First click shows the
explainer; Got it acknowledges and navigates (avoids unmounting the
callout mid-read).

Part of tiann/hapi#2011

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

* fix(web): wrap-aware terminal extract and safe markdown fences

Preserve logical lines across xterm isWrapped rows (keep wrap-boundary
spaces) and size the excerpt fence longer than any backtick run in the
body so cat'd Markdown cannot close the block early.

Addresses review on tiann/hapi#2012.

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

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-26 10:28:09 +08:00
8121d0df6e feat(web): fixed session order display setting (#2033)
* feat(web): fixed session order display setting

Add a per-device "Fixed session order" toggle (default off, Settings →
Display → Sessions). When on, the session list freezes: sessions sort
by creation time only (newest first) instead of re-sorting on
activity, workspace groups stop jumping (ordered by the newest session
created inside them), and the pin-in-progress running buckets are
paused. Manual pins still float to the top; search-relevance ordering
is unchanged.

- shared: SessionSummary gains an optional createdAt (toSessionSummary
  copies it). Older hubs simply omit it and the web falls back to
  updatedAt, degrading to the legacy activity order.
- web: sessionOrder lib (stable key + comparator), useFixedSessionOrder
  localStorage hook, useSSE cache sort via ref (no EventSource teardown
  on toggle), SessionList in-group/group/machine sorts plus bucket
  gating, settings switch, en/zh-CN/ru strings.
- tests: sessionOrder unit, useFixedSessionOrder hook, useSSE
  fixed-order cache cases, SessionList grouping cases, settings page
  switch, shared createdAt passthrough.

Co-Authored-By: Claude Code <noreply@anthropic.com>
via [HAPI](https://hapi.run)
Co-Authored-By: HAPI <noreply@hapi.run>

* fix(web): tie-break equal order keys on session id

Review follow-up: sessions created in the same instant compared as equal,
so their relative order came from the incoming array order — which the
hub may derive from activity, making tied sessions visibly swap after
every refetch. Tie-break on session id to make the comparator a total
order. Two tests cover the comparator and the refetch-stability view.

Co-Authored-By: Claude Code <noreply@anthropic.com>

* fix(web): tie-break equal group creation instants on the group key

Review follow-up: the directory- and machine-level sorts compared only
the groups' latest creation instants, so groups created in the same
instant followed the incoming array order and could shuffle after every
refetch. Route both fixed-order group sorts through a shared
compareDescWithKeyTiebreak helper that falls back to the group's stable
key (directory groups: machine::path key; machine groups: machineId).
Test covers both incoming orders of two tied groups.

Co-Authored-By: Claude Code <noreply@anthropic.com>

---------

Co-authored-by: Claude Code <noreply@anthropic.com>
Co-authored-by: HAPI <noreply@hapi.run>
2026-10-26 10:27:52 +08:00
AnanovoandGitHub 1eacdd6ff1 fix(web): contain image preview labels to prevent chat layout shifts (#2116)
* fix(web): contain image preview labels to prevent chat layout shifts

* test(web): run history layout regression in CI
2026-10-26 10:27:06 +08:00
SSU-WEI HUANGandGitHub c64051eba6 fix(codex): persist Round duration across session reloads (#2025)
* fix(codex): persist Round duration across session reloads

* fix(codex): preserve completed plan summaries between turns

* test(ios): make plan transcript recycling deterministic

* fix(ios): preserve reading anchors with stale sizing attributes
2026-10-26 10:26:55 +08:00
17ca2b245b perf(web): stop hidden and unchanged sidebar rows from re-rendering (#2165)
* perf(web): stop hidden and unchanged sidebar rows from re-rendering

Collapsed directory groups only hid their rows with CSS, so every row stayed
mounted, and SessionItem re-rendered whenever SessionList did. Machine
heartbeats replace the machines query every ~10-20s per runner, which
re-rendered every row (each with ~15 mutation observers) even though nothing
in the row changed.

- Unmount collapsed panel contents after the close transition ends; expand
  still animates because rows mount before data-open flips the grid.
- Memoize SessionItem and pass a stable onSelect from the sessions route so
  unchanged rows skip list re-renders.

With 1226 sessions / 47 collapsed groups against a live hub, idle main-thread
long tasks over 60s went from 7 (~390ms) to 0 and the sidebar DOM from ~4.9k
to ~1.5k nodes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(web): keep memoized session rows' time labels current

Memoized rows no longer re-render on every list render, so the relative
time and the Codex import marker label went stale. The row summary now
ticks every minute and subscribes to import markers itself; the list-level
subscription that only forced a full re-render is gone.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(web): keep a collapsing row mounted while its dialog is open

A group can collapse on its own when its last active session disconnects.
Unmounting its rows then discarded an open Rename (unsaved name) or Export
dialog. Rows with an open menu or dialog now hold the panel mounted until
it closes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-26 10:25:35 +08:00
927b4971b9 feat(dsh): model selection for DeepSeek Harness (ACP config options) (#2030)
* feat(acp): flatten Zed-style grouped configOptions choices

DeepSeek Harness 0.2.x (and Zed-style agents generally) nest select
choices one level under group entries that carry no `value` of their
own:

    options: [{ group, name, options: [{ value, name }] }]

Both the per-session config-option descriptor and the model-metadata
merge previously read only flat `{ value, name }` entries, so grouped
pickers surfaced zero choices. Flatten recursively in a shared helper;
flat options are unaffected.

Co-Authored-By: Claude Code <noreply@anthropic.com>

* feat(dsh): runtime model switching through ACP config options

dsh advertises its model catalog as a Zed-style grouped config option
(provider groups with JSON-encoded provider+model tuple values) instead
of session/set_model, so wire it through the generic config-option rail:

- shared: ListDshModels/ListDshModelsForMachine RPC methods, response
  types, and grant dsh the ModelChange flavor capability
- cli: DshMode becomes {model} and rides the queued-batch mode like
  other flavors; the launcher applies model switches per-turn via
  session/set_config_option, discovers the live current model from the
  config option after newSession, rolls back (and warns) when a switch
  is rejected, and serves ListDshModels from session metadata. A
  machine-scoped probe (60s single-flight cache) backs the new-session
  catalog; --model is now accepted by the dsh command
- hub: dsh-models passthrough routes (machine + session scope)

Co-Authored-By: Claude Code <noreply@anthropic.com>

* feat(web): dsh model selector for new sessions and in-chat switching

New-session form gets a Default-plus-catalog picker fed by the machine
probe (opaque tuple values verbatim, friendly names as labels, stale
draft picks dropped when the catalog rotates). Running sessions get the
same catalog over the session-scoped hook; switching rides the existing
generic model-change path now that dsh advertises ModelChange.

Co-Authored-By: Claude Code <noreply@anthropic.com>

* fix(dsh): honor explicit Default reset and cover pending catalog discovery

The launcher now captures the server default before applying any spawn
override, and a queued `model: null` (the picker's "Default" after an
explicit switch) switches back to it instead of silently keeping the
last applied model; DshMode keeps null distinct from undefined all the
way from the config RPC through the queue (runDsh no longer collapses
it). The web model-option helpers get explicit dsh branches mirroring
opencode: pending, failed, or empty ACP discovery renders no Claude
presets and the Ctrl/Cmd+M cycler keeps the current model. NewSession
restores dshSelectedModel from preferred launch settings alongside
agy/opencode (the existing catalog-validation effect drops stale tuples)
with remount, forward-on-create, and fallback tests.

Co-Authored-By: Claude Code <noreply@anthropic.com>

* fix(dsh): keep unset model preferences distinct from Default resets

Two review follow-ups:

- currentModel collapses "no preference yet" into null, so a session
  started without --model on a server that reports no current model
  queued model:null on every ordinary turn — the launcher read each one
  as an explicit "Default" reset and emitted 'Cannot reset the DSH
  model' every turn. The preference is now three-state (undefined =
  nothing requested, null = picker's explicit Default, string = model),
  encoded through dshModeForPreference() so preference-less turns carry
  no model field at all.

- ListDshModels was registered only after initialize/session/new and
  discovery finished; a picker opened during startup hit an
  unregistered-method rejection with no retry path and stayed empty
  until remounted. The handler is now registered before initialization
  and parks on a readiness promise resolved once the session exists and
  discovery has settled (rejected with the startup error otherwise).

Co-Authored-By: Claude Code <noreply@anthropic.com>

* docs: DSH advertises model selection over ACP

The capability blurbs still said HAPI exposes no model pickers for DSH and
that the ACP server has no model switching. Update them to the wire reality:
models are listed via ListDshModels and applied per turn with
session/set_config_option. Resume remains out of scope on this branch.

via [HAPI](https://hapi.run)

Co-Authored-By: HAPI <noreply@hapi.run>
Co-Authored-By: Claude Code <noreply@anthropic.com>

---------

Co-authored-by: Claude Code <noreply@anthropic.com>
Co-authored-by: HAPI <noreply@hapi.run>
2026-10-26 10:02:51 +08:00
Junmo KimandGitHub 5a626616a2 fix(web): explain redirected hub requests instead of "Failed to fetch" (#2065)
* feat(web): detect auth-proxy redirects on hub requests

* feat(web): show a redirect notice with an open-hub action on the login screen
2026-10-26 10:01:41 +08:00
AnanovoandGitHub f198ab29a4 fix(web): preserve composer image previews across updates (#2070) 2026-10-26 10:01:08 +08:00
AnanovoandGitHub e3d42503b6 fix(web): contain transient scrollbar overflow during resize (#2077) 2026-10-26 10:00:49 +08:00
d3473f9ed4 fix(cli): forward Claude API error records with null service_tier (#1857) (#2087)
Claude Code writes failed API calls ("Prompt is too long", usage limits,
logged out) as synthetic assistant records whose usage carries
`service_tier: null`. The schema only accepted undefined, so the local
session scanner dropped every one and the web session just looked stalled.

- Accept null for the usage fields the Messages API types as nullable.
- Keep the top-level `error`/`isApiErrorMessage` on assistant records in
  local mode, and `error` in the SDK converter for remote mode.
- Render assistant records carrying `error` as the existing error event,
  so the all-zero synthetic usage no longer resets the context gauge.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-26 10:00:23 +08:00
878ca2058c feat(codex): expose Approve for me as a permission mode (#2016) (#2090)
* feat(codex): expose Approve for me as a permission mode (#2016)

Add autoReview to the Codex permission catalog. It keeps the
workspace-write sandbox with on-request approvals and routes escalations
to Codex's native reviewer (approvals_reviewer = "auto_review"), the same
config as `hapi codex --approve-for-me`. It works at launch and mid-session
via thread/settings/update; explicit modes now reset the reviewer to user,
and sessions report autoReview from the native reviewer setting.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test(native): expect Auto-review in the Codex permission lists (#2016)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* feat(modes): label autoReview as Auto, matching Claude's auto mode (#2016)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-26 09:59:47 +08:00
4233bf8759 fix(web): give the rich composer its own undo history (#2097)
* fix(web): give the rich composer its own undo history (#2085)

Plain-text paste, selected-text deletion, cut, Enter, and mention or
suggestion insertion rebuild the contenteditable DOM, which the browser's
native undo stack cannot follow, so Ctrl+Z silently did nothing for them.

The composer now records a snapshot (serialized value + selection) before
every edit, groups consecutive native typing into one step, and handles
Ctrl/Cmd+Z, Ctrl/Cmd+Shift+Z, Ctrl+Y and beforeinput historyUndo/Redo
itself. Undo restores the prior selection, including ranges. An external
value change (send, draft restore) starts a fresh history.

Fixes #2085

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(web): keep composer undo snapshots as segments

Reparsing a serialized snapshot turned literal `[title](/sessions/<id>)`
text into a mention chip (and normalized its title) on undo. Store the
segments themselves so undo restores the exact draft.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 15:35:35 +08:00
8657c11754 fix(web): load one older page when the cold tail is tool-only (#2098)
* fix(web): load one older page when the cold tail is tool-only

A cold session open fetches the latest 20 raw messages. When they reduce
to only ordinary tool activity, the thread showed a single collapsed tool
group until viewport-coverage loading ran after the 1.8s initial scroll
settling window.

Before committing the cold page, check its reduced/grouped blocks; when
they are tool-only and older history exists, fetch one 200-message older
page (bounded by a 500ms wait) and commit both together, keeping the
existing skeleton meanwhile. Timeout, failure, or an epoch change falls
back to the tail page. Cached re-entry and reset paths are unchanged.

Fixes #2045

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(web): limit tool-only context fetch to cold windows and ignore queued prompts

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 15:35:25 +08:00
e040d23371 fix(web): ignore pending requests on archived sessions in the sidebar (#2101)
Archived sessions keep their stale pending requests in agentState. The
sidebar still treated them as live: the collapsed folder preview forced
them visible and trimmed active sessions to make room, so a folder could
look like it had no active sessions. They also kept the permission/input
attention dot and the "Pending N" badge.

Only count pending requests on active sessions for preview pinning,
attention, and the badge. The state itself is unchanged.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 15:35:15 +08:00
4254942aeb fix: show Claude sessions waiting on background tasks (#2100) (#2102)
* fix: show Claude sessions waiting on background tasks (#2100)

backgroundTaskCount was always 0 for Claude: the hub parsed the
"Command running in background" tool result only from tool_result /
assistant records (Claude sends it as a user record), and waited for a
<task-notification> user record that current Claude Code never emits.

Set the count from Claude's system/background_tasks_changed snapshot,
which the CLI already forwards and which lists every running background
task (empty once the last one finishes).

In the session list, waiting on background work now shows a blue
spinner instead of a static blue dot (too close to the "needs input"
dot) or the green "running" pulse used for selected and pinned rows.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(cli): forward background_tasks_changed snapshots to the hub

OutgoingMessageQueue only sends chat-visible records, so Claude's
background task snapshots never left the CLI. Forward that one hidden
subtype, and have the hub apply it as session state without storing a
hidden message row.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(web): draw the background spinner with the thinking spinner code

Both row spinners now render through one SessionSpinner; background work
only switches the color to blue and no longer adds a separate Running label.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 15:35:02 +08:00
AnanovoandGitHub cb2ff05c2a fix(codex): distinguish compaction waits from unknown delivery (#2115) 2026-10-25 15:34:52 +08:00
AnanovoandGitHub 2c1361ba5d fix(web): stop composer image previews from dragging after release (#2124) 2026-10-25 15:33:46 +08:00
AnanovoandGitHub 097a693348 fix(web): remove composer attachments without waiting for cleanup (#2128)
* fix(web): remove composer attachments without waiting for cleanup

* test(web): run attachment removal regression in CI
2026-10-25 15:33:37 +08:00
5d8deb2011 fix(web): allow pasting non-image files into the composer (#2026)
Paste only accepted image/* files, silently dropping PDFs, archives,
etc. copied from the file manager, while drag-and-drop and the attach
button accept any file. Remove the image-only filter from both paste
handlers so paste matches drag-and-drop. The scheduled-send guard is
unchanged.

via [HAPI](https://hapi.run)

Co-authored-by: HAPI <noreply@hapi.run>
2026-10-20 07:22:14 +08:00
f13a1aaeab fix(web): shed session toolbar icons by keep rank instead of overlapping (#2040)
* fix(web): stop session toolbar search/clear overlapping neighbors

Collapsed search used min-w-0 and crushed under shrink-0 icons, so clear
painted over filter/BlockedJumpPill on narrow widths. Keep search chrome
shrink-0 and wrap the header actions group instead (#2039).

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

* fix(web): size session toolbar actions to content so they wrap

flex-1 min-w-0 kept the action group beside a long search chip in a 280px
sidebar, so Browse/Settings overflowed onto the calendar. Use w-max shrink-0
and keep the router header-action cluster from shrinking (#2040).

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

* fix(web): keep session toolbar icons in one slab when the row wraps

max-w-full plus inner flex-wrap let the action group split, so some icons
stayed beside search and the rest dropped underneath. The group is nowrap
and content-sized, so a tight row moves every icon together.

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

* fix(web): shed secondary session toolbar icons instead of wrapping

Quest was burning a full toolbar line when the icon slab did not fit
beside collapsed search. Keep one nowrap row; hide browse/settings (and
similar headerActions) below a container-query width, with a higher bar
when the query chip is present (#2039).

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

* fix(web): satisfy typecheck for session header-row test

CI failed because optional chaining left headerRow possibly null when
reading className. Use getByTestId instead.

* fix(web): put shed session toolbar actions in a More menu

Hiding headerActions on narrow headers removed Settings with no
alternative. Keep one toolbar row, but surface secondary actions in an
accessible ⋯ popover below the container-query threshold (#2039).

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

* fix(web): fold primary toolbar into More when query chip is active

At ~320px the collapsed chip plus unread/filter/New still overflowed past
More/Settings. When searchQuery is non-empty, put primary controls in the
same overflow menu so the row is [chip][⋯] (#2039).

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

* fix(web): pack session toolbar actions beside search

ml-auto shoved the action slab to the right edge and left a Quest-width
gap between the search icon and BlockedJumpPill (#2039).

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

* fix(web): widen machine menu clamp and width-gate toolbar More

MachineFilterMenu near the left edge (inside More) got a tiny right-only
maxWidth. Clamp to the wider horizontal pocket and left-align when that
side wins. Fold primaries into More only when a query chip is present and
the header is under 32rem (#2039 / #2040).

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

* fix(web): fold toolbar into More on narrow headers without a query

Empty query still forced toolbarCrowded=false, so at 320px / 130% font
with machines+unread the More control went offscreen. Crowd from header
width in both query states (#2039 / #2040).

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

* fix(web): stop Quest-width empty query collapsing to search+More

A flat 32rem crowd gate folded every primary into More on typical Quest
sidebars (~400px), leaving [search][⋯] and a desert of empty space. Empty
query now crowds only below ~22rem (still covers 320px/130% font); query
chip keeps the higher 32rem bar (#2039 / #2040).

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

* fix(web): pin toolbar More to the trailing edge on Quest widths

Left-packing every action left a dead zone after New while Settings hid
in More. Primaries stay beside search; secondary/More get ml-auto on a
flex-1 actions row so the chrome reads [search][primaries] … [⋯]
(#2039 / #2040).

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

* fix(web): shed session toolbar icons by keep rank

A single width gate either hid the whole row or left a hole before More.
Drop the lowest-priority controls first (browse, mark-all, settings) and
keep search, new, and the unread lens on the row while they fit (#2039).

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

* fix(web): size toolbar shed from actions box, not a search guess

Active query reserved 200px for a max-w-[9rem] chip and collapsed the
row to search+More. Measure the actions flex box and each control;
keep daily icons until width is known (#2039).

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

* fix(web): remeasure toolbar shed after search collapse

The actions box unmounts while search is expanded, so a one-shot observer
left the next layout on a stale width. Fit math now reattaches on the
new node, subtracts a visible machine menu, and uses the measured More
width and column gap.

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

* fix(web): let query chip yield so toolbar More stays on-screen

At 320px and 130% type a long chip plus calendar left too little room
for the machine menu and More. An active query now uses min-w-0 on
search and shrink-0 on actions so only the chip text truncates.

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

* fix(web): stop desktop toolbar using an infinite shed budget

Empty-query first paint treated the actions gutter as infinite, so every
shrink-0 icon stayed inline and painted past the split. Use a finite
fallback, cap the budget by the real row gutter, and give the sidebar
min-w-0 so --sidebar-w actually constrains the flex item.

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

* fix(web): use one-gap slack instead of leaving an icon-sized hole

The shed refused the next icon when the gutter was only a column-gap
short, so the row kept an empty slot the size of that icon.

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

* fix(web): fill the toolbar gutter before opening More

The shed was budgeting a guessed remainder, so More stayed open while
a wide empty stretch sat before the split. The budget is now the pixels
from the actions' left edge to the row's right edge. If every icon fits,
overflow is empty.

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

* fix(web): size the toolbar budget from the flex leftover, not the icons

shrink-0 made the actions box as wide as whatever was already showing, so
search lost its clear and calendar and a collapsed query could hide every
action. The actions box is flex-1 again, and a query chip cannot shrink
below its icon, clear, and calendar.

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

* fix(web): reserve machine menu and More before search width

A long 9rem query chip at 320px / 130% left the actions box under 50px
while machine menu + More need ~94px, so More slid off-screen. Cap the
collapsed search group to the row minus those trailing controls.

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

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-18 14:12:16 +01:00
317e39a0bb fix(web): 48px session search clear with calendar left of it (#2034)
* fix(web): 48px session search clear with calendar left of it (#2019)

Quest/touch: expanded search uses a min 48x48 clear end-cap as the
furthest-right control, with the calendar embedded immediately to its
left inside the field. Collapsed search stays shrink-0 with a larger
clear chip so it does not steal the toolbar row.

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

* fix(web): anchor date picker to search group; let collapsed chip shrink (#2019)

HAPI Bot: mount the calendar popup on the search control (not the 48px
button) so left/right align stays on-screen at narrow widths. Drop
shrink-0 on the collapsed group so the query chip can yield space while
calendar/clear stay fixed hit targets.

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

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-15 19:15:50 +01:00
7b00df458c fix(voice): configurable ElevenLabs max conversation duration (#2017)
* feat(voice): configurable ElevenLabs max conversation duration

Auto-created ConvAI agents never set conversation.max_duration_seconds, so
ElevenLabs applied its 600s default and cut voice conversations off.

Add ELEVENLABS_MAX_DURATION_SECONDS (default 1800, validated 60-7200). The cap
is applied when the hub creates an agent and reconciled onto existing
hub-managed agents (found by name) once per process. Operator-supplied agents
(ELEVENLABS_AGENT_ID / voice map / customAgentId) keep their own cap. Invalid
values fail the token request with a clear message instead of an API 422.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

* feat(voice): Settings UI for ElevenLabs max conversation duration

Extend the hub-owner credentials panel (Settings -> Voice -> manage
providers) with a numeric field for ELEVENLABS_MAX_DURATION_SECONDS,
reusing the same env-wins-over-Settings overlay that already backs the
ElevenLabs API key field. A Settings save takes effect on the very next
token request in the same process; the env var still requires a restart.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

* fix(voice): reconcile max duration on cached agents, not just once per process

Codex review on heavygee/hapi#230 caught this: getOrCreateAgentIdForVoice's
agentIdCache short-circuited before ever reaching ensureAgentOverrides, and
ensureAgentOverrides's overridesEnsuredAgents gate was a one-time-forever
marker that didn't account for the configured duration changing after the
first reconciliation. Together, any voice already used once in the hub
process would keep its old cap until a restart, contradicting the Settings
UI's 'takes effect immediately' claim.

Track the last-applied max duration per agent and compare it against the
live setting on every call, including agentIdCache hits, so a Settings
change reaches an already-cached agent on its very next token request.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

* fix(voice): address tiann/hapi#2017 review findings

- Fix TS2769 typecheck failure: test assertions casting the credentials
  response narrowed the type to omit 'editable', which upstream CI's
  bun typecheck catches and this fork's (currently broken) CI did not.
- An invalid ELEVENLABS_MAX_DURATION_SECONDS env value threw out of
  buildMaxDurationStatus(), which is shared by every provider's
  GET/PUT credentials status — breaking visibility into OpenAI/Deepgram/
  Groq settings too, not just ElevenLabs. The token-issuance hot path
  (getConfiguredElevenLabsMaxDurationSeconds) still throws there by
  design; only the status/update endpoints now degrade gracefully.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

* fix(voice): interpolate duration range in the Settings hint

Nit from tiann/hapi#2017 follow-up review: the hint under the max-duration
field rendered the literal '{min}-{max}' placeholders instead of '60-7200'.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-15 15:42:01 +01:00
eca9e99331 feat(web): Enlarge button for dictation and text submission (#2006) (#2007)
* feat(web): larger empty-composer voice/dictate hit target

When the primary UnifiedButton is idle voice/dictate (empty draft),
use h-12 w-12 so Quest and touch clients get a usable target. Compact
h-8 w-8 remains for Send, stop, and disabled states.

Closes #2006

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

* feat(web): full-height composer primary (voice ↔ send)

Park UnifiedButton beside the composer card so it stretches the
full input+toolbar height. Empty shows giant voice/dictate; draft
swaps the same giant control to Send (icon only).

Refs #2006

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

* test(web): fix fill-height primary goldens + UnifiedButton mocks (#2006)

Fill-height tip regressed no-arg onSend assertions and empty-voice helper
expect; HappyComposer omitPrimaryButton needs UnifiedButton in vi.mocks.

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

* test(web): assert onSend('default') + fill-height helpers (#2006)

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

* fix(web): flush fill-height composer primary to card edge (#2006)

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

* fix(web): keep draft voice companion beside fill-height Send (#2006)

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

* fix(web): keep draft voice companion beside fill-height Send (#2006)

Revert accidental soup dump of expandEnter + fat ComposerButtons. Empty
stays giant voice; draft shows companion waveform left of giant Send.

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

* fix(web): draft voice as toolbar circle, not beside giant Send (#2006)

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

* fix(web): hide draft voice when scratchlist owns Send (#2006)

Codex P2: toolbar DictationButton and Ctrl/Cmd+Shift+D must not start
voice while routesToScratchlist — same suppress as UnifiedButton.

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

* fix(web): compact primary when composer is expanded (#2006)

Fill-height Send/voice is for the collapsed Quest card. Expanded mode
stays a compact control so we do not paint a full-shell action strip.

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

* chore: re-trigger upstream pr-review after Sol distributor 503

Empty commit — test/integration already green on tip; pr-review failed
twice with "No available channel for model gpt-6.1-sol".

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

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-12 20:15:15 +08:00
efc9004fda feat(web): add Russian locale and detect initial language (#1958)
* feat(web): add Russian locale

- Add web/src/lib/locales/ru.ts
- Register 'ru' in the locale registry, i18n context, language switcher and settings
- Format outline timestamps with the day.month convention for 'ru'

* feat(web): detect initial locale from Telegram and browser

The Telegram Mini App runs in its own web view, so a language chosen in
the browser does not carry over and the app always opened in English.
Pick the first session locale from an explicit stored choice, then the
Telegram user's language_code, then navigator.language, then English.

* feat(web): translate the newly localized strings

Russian values for the keys added by the hardcoded-string fix (see the
separate web i18n PR). Keeping them on this branch so the locale PR is
complete when both land.

* feat(web): add Russian for the newly localized strings

* fix(web): recheck the Telegram language after a slow SDK load

* fix(web): keep a manual language choice over late Telegram detection

---------

Co-authored-by: mrudy <mrudy@local>
2026-10-05 17:13:53 +08:00
Junmo KimandGitHub 0ef5f976c0 fix(web): render Codex goal responses as cards (#1954) 2026-10-05 17:13:14 +08:00
Dave RapinandGitHub c2d2cfe6f8 Add 130% app font size (#1975) 2026-10-05 17:09:07 +08:00
97cf69bb59 feat(web): dictate keyboard shortcut (Ctrl/Cmd+Shift+D) (#1902)
* feat(web): add Ctrl/Cmd+Shift+D dictate keyboard shortcut

Toggle composer dictation or voice assistant via the same path as the
mic buttons, with dialog/input guards and tooltip documentation.

Fixes tiann/hapi#1900

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

* fix(web): address dictate hotkey bot findings on PR 1902

Ignore key repeat on the session listener and align assistant-mode
invoke guards with UnifiedButton send mode (canSend).

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

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-24 07:25:17 +08:00
tiborsekeraandGitHub b6849c1d35 fix(codex): reject history sync for active sessions (#1895)
* fix(codex): guard transcript sync for active sessions

* test(codex): exercise divergent transcript prefix
2026-09-24 07:24:42 +08:00
a259ad3144 fix(cli): change_title updates metadata.name for visible renames (#1896)
* fix(cli): change_title updates metadata.name for visible renames

Agent change_title was writing metadata.summary only, so spawn --name
sessions kept showing the old title. Route explicit renames through
metadata.name (web rename semantics) across MCP, Codex, and Pi.

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

* fix(web): voice context prefers metadata.name for session title

Voice bootstrap/full formatters only read summary.text, so after
change_title sets name the spoken context kept a stale title. Use the
same name→summary precedence as getSessionTitle.

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

* chore: polish title rename tests after cold review

Rename the Codex root title test to match last-writer-wins behavior,
and skip the voice full-context summary header when no title signal
exists (avoid literal undefined).

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

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-21 13:49:53 +01:00
MiroandGitHub b6a27bdc68 fix(kimi): discover configured models dynamically (#1873)
* fix(kimi): discover configured models dynamically

* fix(kimi): address dynamic model review findings

Probe settlement (Major): parse `kimi provider list --json` on the child's
'close' event instead of 'exit', so stdio is fully drained first, and run the
parse inside a guarded settle() helper. A parser throw previously escaped the
promise from an async event callback and reached the runner's uncaughtException
shutdown handler instead of listKimiModelsForCwd's catch. The helper also
settles the promise exactly once across timeout, spawn error and close, names
the terminating signal when there is no exit code, and tolerates a kill() that
throws.

Running-session discovery (Minor): add a session-scoped Kimi catalog. The
shared RPC method `listKimiModels`, a CLI handler registered on the session
connection with the probe cwd taken from the session metadata (never from RPC
arguments), hub rpcGateway/syncEngine pass-throughs, and
GET /api/sessions/:id/kimi-models gated on an active session of flavor `kimi`.
SessionChat now reads the catalog from that endpoint, so an active session
advertises its models even when no background runner holds the machine socket.
New Session keeps the machine-scoped discovery unchanged, and model switching
still goes through the existing ACP set_model path.

* fix(kimi): register session model discovery before the local/remote loop

The session catalog handler was registered inside the remote launcher, after
ACP initialization. A session started with `hapi kimi` in local mode therefore
answered the web picker's request with an unregistered-handler error, and the
failed query was not retried or invalidated when remote control was taken over.

The catalog is backend independent, so it is now registered in runKimi session
setup alongside the other session-level handlers, using the session's working
directory, and the late registration in the remote launcher is removed. This
covers local, remote and resumed sessions on the same session connection.

Adds cli/src/kimi/runKimi.test.ts asserting the handler is registered after
bootstrap and before the loop, and that the probe cwd is the session's working
directory. The launcher wiring assertion moves out of
kimiRemoteLauncher.test.ts with the registration.
2026-09-21 13:58:59 +08:00
SSU-WEI HUANGandGitHub c258a2279e fix(web): synchronize assistant runtime and history loading (#1893) 2026-09-21 13:56:14 +08:00
2f72a8012e fix(web): save composer draft on visibilitychange, not only on unmount (#1883)
Links in a session open via window.open(_blank). For an installed
standalone-display PWA, navigating to a URL outside the manifest's scope
is a platform behavior (Chromium/Edge show it as a minimal overlay with a
close affordance), not custom code here — but closing it can mean the
underlying page had a real top-level teardown, not just an in-app remount.

useComposerDraft only ever persisted the draft in its effect's cleanup —
reliable for a React-managed unmount (switching sessions), but browsers
don't guarantee that cleanup runs to completion before a genuine
top-level navigation completes, so the save could simply never happen.

Extracts the existing save logic into persistNow() and calls it both from
the unmount cleanup (unchanged) and a new visibilitychange listener (when
document.visibilityState becomes 'hidden'), mirroring the pattern already
used elsewhere in this codebase (useSSE.ts, useAuth.ts, useSyncingState.ts,
usePwaUpdate.ts, useVisibilityReporter.ts) for the same class of problem.

Fixes #1882

Co-authored-by: Ian Stevenson <is@lockhouse.systems>
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-19 09:47:04 +08:00
773af2cf65 feat(web): setting to open external links in a new tab (#1884)
* feat(web): setting to open external links in a new tab

Left-clicking an external http(s) link renders it with no explicit
target, defaulting to _self. In an installed/standalone PWA this gets
intercepted by the browser and shown as a captive in-app overlay
instead of leaving the app; in a normal tab it navigates away from
HAPI entirely. Right-click already opens in a new tab, but that's not
a fair ask for every link.

Add an opt-in Settings > Display > Links toggle that forces
target="_blank" rel="noopener noreferrer" on genuine external http(s)
links only (relative/SPA hrefs and other schemes like mailto are left
untouched). Defaults to off, preserving today's behaviour, since the
overlay may be intentional/liked by some users.

Vibe Coding disclosure: implemented with Claude Sonnet 5 (Claude Code).

* fix(web): simplify link-toggle setting description per review

The description mentioned the target attribute (an implementation
detail, not user-facing) and asserted the in-app overlay is what every
installed PWA shows, which was only actually confirmed on Chromium/
Edge. Describe the user-visible behaviour only, without promising a
specific rendering across all installed-app environments.

* fix(web): stop promising specific behaviour when installed as an app

Per follow-up review: describing the setting as "always" opening links
in a new tab "even when installed as an app" still guarantees a
specific outcome across every browser's PWA handling, which isn't
something this change can verify or promise. State only what the
setting does (open in a separate tab), not how every installed-app
environment will render that.

---------

Co-authored-by: Ian Stevenson <is@lockhouse.systems>
2026-09-19 09:46:38 +08:00
AnanovoandGitHub a7ad631691 feat(web): auto-hide primary session scrollbars (#1730)
* feat(web): auto-hide primary session scrollbars

* test(web): run scrollbar auto-hide E2E in CI
2026-09-16 08:15:02 +08:00
ca1fd80afd fix(cursor): pin Auto to CLI --model auto instead of account default (#1819)
* fix(cursor): pin Auto to CLI --model auto instead of account default

Selecting Auto was treated as unset/ACP default[] after the Default→Auto rename. Persist and spawn `auto` so the picker matches `agent --list-models`.

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

* fix(cursor): keep live ACP model until Auto actually applies

Pin session.model to auto for the next `--model auto` spawn, but do not claim the live backend switched when ACP only advertises default[].

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

* fix(cursor): reject live Auto unless ACP auto or spawn confirmed it

Do not persist Auto when the running catalog has no auto option. Clear the spawn-auto flag after a concrete model apply so Auto → concrete → Auto cannot fake a live switch.

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

* fix(cursor): keep ACP wire ids spawn-safe and apply effort

Cursor's parameterized picker (`clientCapabilities._meta.parameterizedModelPicker`)
advertises bare model bases plus separate `fast` / `thought_level` config options.
`buildCursorModelsSnapshotFromAcp` synthesized every base as
`${base}[fast=${fast}]`, and Cursor rejects any bracket id whose parameter set is
not that model's complete set, so 74 of the 76 published picker ids failed at ACP
`session/new` with `Cannot use this model` (#1818).

- Stop synthesizing wire ids: keep the advertised values verbatim (bare bases are
  accepted by `--model`, defaults applied). Parameter sets differ per model, so a
  synthesized id cannot be validated from the options alone.
- Flag such catalogs as `parameterized` so variant CLI skus still attach to their
  base rows — the ACP apply path expresses them through config options.
- Apply effort in `applyParameterizedCursorModel` (ids `effort`, `reasoning`,
  `reasoning_effort`), applied against the config options Cursor re-advertises for
  the newly selected model, and skipped on models without that axis.
- Match CLI sku bases across Cursor's legacy `cursor-` family prefix
  (`cursor-grok-4.6-high` → ACP base `grok-4.6`) so those variant rows are not
  dropped.
- Version the shared on-disk catalog cache (v2) so runners that already cached the
  rejected synthesized ids re-probe instead of serving them.

Verified with live `cursor-agent 2026.09.10-fd3934a` ACP runs: the catalog no
longer contains bracket ids, every advertised base and sampled variant sku spawns,
and in-session `claude-opus-4-8-low` / `cursor-grok-4.6-low` set the matching
`effort`/`reasoning` option.

* test(cursor): cover aliased effort ids and thinking coexistence

Covers the branches the HAPI bot review called out for #1818: the
`xhigh` → `extra-high` alias on `gpt-5.5` (`reasoning`), the
`reasoning_effort` id used by `gemini-3.8-flash`, and switching effort on a
model that also advertises Claude's separate `thought_level` `thinking`
toggle.

* test(cursor): cover legacy sku base and post-switch effort option

Adds the two apply-path cases the HAPI bot review called out for #1818:
resolving `cursor-grok-4.6-high` onto an advertised `grok-4.6` base, and
reading the effort option Cursor advertises for the newly selected model
(`reasoning` for gpt-5.5 → `effort` for claude-opus-4-8) instead of the
previous model's options.

* test(cursor): create the cache dir in the migration test

The version-envelope test wrote $HAPI_HOME/cache/cursor-models.json without
creating the directory, so running it alone (or filtering with -t) failed with
ENOENT before the assertion; it only passed because sibling tests in the same
file had already created the path. Reported in the HAPI bot review of #1822.

Verified: `vitest run ... -t 'ignores pre-versioned'` → 1 passed, whole file → 4 passed,
and every other changed test file passes when run standalone.

* Clarify Cursor Auto live-switch limitation in session pickers

* Reconcile partial Cursor model changes before acknowledging Auto

* Serialize Cursor model changes across clients with the existing async lock

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-16 08:12:16 +08:00
8f9d6bd7f8 fix(hub): keepalive-only sessions reconcile to an idle lifecycle (#1820) (#1821)
* fix(hub): reconcile keepalive-only sessions to an idle lifecycle

`sessionCache.expireInactive` only expires a session when `activeAt` goes
stale, and `session-alive` refreshes `activeAt` every ~2s for as long as the
CLI socket lives. A session whose agent has done nothing for days therefore
stays `active: true` / `lifecycleState: running` forever — socket pinging was
being read as agent health.

Add the missing second signal. `active` keeps its meaning (the CLI is
reachable); `metadata.lifecycleState` moves `running` -> `idle` once the hub
has seen nothing but keepalives for `HAPI_SESSION_IDLE_TIMEOUT_MS` (default
12h, `0` disables), and back to `running` on the next real progress.

`active` is deliberately not flipped: the socket really is up, and a false
`active: false` would let `resumeSession` spawn a second agent against a live
process and would unlock dedup-merge / delete on it — the #1705 class of bug.

- `sessionCache.recordAgentProgress` tracks a progress clock that keepalives
  can never touch: messages in either direction, queued prompts, background
  tasks. Seeded from `updatedAt` so a hub restart re-derives it from disk.
- `reconcileKeepaliveIdle` runs on the existing 5s inactivity tick. Sessions
  that are thinking, running background tasks or holding a pending permission
  request are never marked; `metadata.idleReconcileExempt` opts out entirely.
- Only real progress clears `idle`, never `thinking` — Cursor ACP emits
  ambient idle/running churn (#1553) that would otherwise flap the lifecycle
  every tick.
- `isLiveLifecycleState` replaces the `=== 'running'` literals so an `idle`
  row does not read as dead to the archive route or the Cursor migrator.
- Web splits keepalive-only sessions into their own session-list bucket.

Refs #1820. Distinct from #842 (`active=false` with a stale `running` badge);
this is the inverse.

via [HAPI](https://hapi.run)

Co-Authored-By: HAPI <noreply@hapi.run>

* fix(hub): seed the idle clock from stored messages across hub restart

PR review on #1821: a cold `SessionCache` had no observed progress and fell
back to `updatedAt` alone. Assistant messages deliberately do not move
`updatedAt`, so a session that was streaming output a minute before a hub
restart read as hours idle and got marked on the very next tick — a false
`idle` that persisted until the next progress event.

Seed the clock once from `getNewestMessagePosition`, which is the durable
record of that output. `updatedAt` stays a floor on top of the seed: todos /
teamState / agentState writes bump it without routing through
`recordAgentProgress`.

Regression test recreates a SessionCache over the same store after recent
assistant output and confirms keepalives no longer cause premature idle
classification — and that the same session is still marked once the output
itself goes stale.

via [HAPI](https://hapi.run)

Co-Authored-By: HAPI <noreply@hapi.run>

* test: cover the socket wake-up path and the idle session-list bucket

Follow-up review on #1821 reported no findings but flagged two uncovered
paths. Both have bad failure modes, so close them.

- Hub: an assistant message arriving through `registerSessionHandlers` must
  wake an idle session. Assistant messages never bump `updatedAt`, so the
  `onAgentProgress` hook is the only thing that can clear the mark — a
  refactor dropping that wiring would silently false-idle every actively
  working session, and nothing would have caught it.
- Web: extract the inline bucketing from the `runningSessions` useMemo into a
  pure `bucketRunningSessions`, matching how the rest of SessionList's logic
  is exported and tested. Covers idle vs quiet-but-ready separation, no
  double-bucketing, the transition back out of idle on thinking/background
  task/pending request, and the disconnected/pinned skips.

No behaviour change.

via [HAPI](https://hapi.run)

Co-Authored-By: HAPI <noreply@hapi.run>

* test: cover the idle lifecycle at both isLiveLifecycleState call sites

Third review round reported no findings but named the two guards this PR
changed as uncovered. Both protect against reading a keepalive-idle row as a
dead one:

- Cursor migrator: an idle-marked row still has a live CLI behind it, so
  preflight must refuse it without `forceArchiveRunning`. A regression here
  would let the migrator transplant the ACP session directory out from under
  a running Cursor runner.
- Archive route: once a keepalive-idle session finally loses its socket it
  must stay archivable, rather than being stranded behind a 409.

Both tests were confirmed non-vacuous by reverting each guard to the
`=== 'running'` literal; each then fails.

No behaviour change.

via [HAPI](https://hapi.run)

Co-Authored-By: HAPI <noreply@hapi.run>

* test: stop the restart regression leaking its temp DB on failure

The file-backed store's `rmSync` was a trailing call, so any failing
assertion above it skipped cleanup and left a temp DB behind — one per failed
run. Noticed while verifying the test was non-vacuous, which deliberately
fails it.

Wrap in try/finally. Verified: a forced failure now leaves zero
`/tmp/hapi-1820-*` directories.

Test-only.

via [HAPI](https://hapi.run)

Co-Authored-By: HAPI <noreply@hapi.run>

---------

Co-authored-by: HAPI <noreply@hapi.run>
2026-09-16 08:11:54 +08:00
AnanovoandGitHub 7b6c575514 fix(web): refresh cached session tails with a small page (#1847) 2026-09-16 07:26:18 +08:00
IanMStevensonandGitHub 02bd43ab14 fix(web): filter unread badge count through sidebar eligibility (#1850) 2026-09-14 21:42:27 +08:00
IanMStevensonandGitHub ed0051749f fix(web): persist AskUserQuestion draft answers across session switches (#1853) 2026-09-14 21:38:57 +08:00
weishu 5280a3b165 fix(chat): dismiss Codex plan actions when continuing planning 2026-09-14 14:30:08 +08:00
AnanovoandGitHub a815230886 fix(web): support bracket-delimited LaTeX in Markdown and share exports (#1846)
* fix(web): support bracket-delimited LaTeX in Markdown and share exports

* fix(web): preserve Markdown containers around LaTeX

* fix(web): handle list fences and math comparisons

* fix(web): cover Markdown metadata edge cases

* fix(web): protect Markdown metadata around LaTeX

* fix(web): preserve reference metadata around LaTeX

* fix(web): preserve quoted formulas after prose

* fix(web): normalize nested Markdown containers

* fix(web): preserve existing dollar math

* fix(web): stop LaTeX matching at protected ranges
2026-09-14 14:24:33 +08:00
AnanovoandGitHub b41b9293e9 fix(web): render cold sessions from a small initial page (#1809)
* fix(web): speed up cold session first paint

* test(web): update pagination fixtures for cold page

* test(web): wait for cold page before loading history

* feat(native): match cold session pagination

* test(native): cover cached re-entry page size
2026-09-14 09:07:14 +08:00
de6c9dee20 fix(web): keep pin dividers coherent under search relevance sort (#1848)
* fix(web): keep pin dividers coherent under search relevance sort

Address tiann's #1842 review: preserve project-pinned sections when
ranking, drop ineffective single-term IDF, add title phrase bonus.

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

* test(web): add component regression for search pin dividers

Render tiann's four-session repro under query "home" and assert the
session list draws at most one project pin divider.

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

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-13 17:24:11 +01:00
b6d0fa563c feat(web): rank session list search by field weight and IDF (#1842)
* feat(web): rank session list search by field weight and IDF

Boolean match + recency buried distinctive title hits under ubiquitous
path segments like /home/. Keep paths searchable; score title higher and
down-weight terms that match most of the corpus. Boundary match so home
does not hit homelab.

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

* fix(web): use boundary match as search score bonus, not a gate

sessionMatchesQuery is shared with @-mentions and the share picker; keeping
substring inclusion restores as-you-type. Boundary affinity multiplies the
field×IDF score instead. Path parts stay separate for wildcards; match set
is explicit so filtering never reads score > 0.

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

* fix(web): pick search field hit by post-bonus contribution

Raw field weight could beat a lighter boundary hit that scores higher
after the ×1.75 multiplier. Compare contribution after the bonus.

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

* test(web): drop invalid updatedAt from summary fixture

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

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-13 16:09:10 +01:00
weishu 0a9f15a9a7 fix: align request_user_input other answers across clients 2026-09-13 19:48:14 +08:00
weishu e0db9c6815 fix(notifications): distinguish user questions from permission requests 2026-09-13 12:41:38 +08:00
weishu 917baf1201 fix(codex): sync shared plan proposals and web actions 2026-09-13 12:09:39 +08:00
092a2259c8 fix(web): force-dismiss indeterminate queued rows on cancel/edit (#1840)
* fix(web): force-dismiss indeterminate queued rows on cancel/edit

When cancel returns busy for an already-indeterminate QueuedMessagesBar
row, keep the optimistic removal instead of re-sticking the row so Edit/X
cannot leave the UI dead. Upgrade busy to invoked when queued-state
reconcile shows the steer already landed, so Edit toasts instead of
prefilling a duplicate.

Fixes #1839

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

* fix(web): never prefill Edit when queued cancel returns busy

Indeterminate rows can still be a live dispatch. Prefilling on busy
risked a duplicate send if the original steer later landed. Keep
force-dismiss for Cancel; toast on Edit+busy without restoring the draft.

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

* fix(web): keep force-dismissed indeterminate rows for late consume

Hide abandoned outcome-unknown rows from the queued bar via
queueDismissed so Cancel/Edit still clear the stuck chip, while leaving
the message in the window for a later messages-consumed SSE.

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

* fix(web): close queueDismissed races on cancel busy and requeue

Restore the hidden hold before getQueuedState so concurrent consume
SSE can land, skip overwriting it in onSuccess, and clear dismissal
when markMessagesRequeued confirms FIFO again.

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

* fix(web): undismiss force-held row when cancel busy finds FIFO

If getQueuedState reports the localId is queued again after a failed
steer, clear queueDismissed so Edit/Cancel return without prefilling.

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

* fix(web): preserve queueDismissed across refresh and merges

Force-dismiss of indeterminate rows survived Cancel, but latest-page
refetch and same-ID merges dropped the client-only flag. Carry it while
the authoritative row remains uninvoked and indeterminate.

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

* fix(web): clear queueDismissed on reconnect FIFO reconcile

Reconnect queued-state reconciliation now markMessagesRequeued for
confirmed queuedLocalIds so force-dismissed rows regain Edit/Cancel
after a missed messages-requeued SSE.

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

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-12 21:02:32 +01:00
weishu 26fddff038 fix(chat): restore grouping for shared Codex commands
Group ordinary Codex commands with default tools across web, iOS and Android while preserving exploration and user-shell boundaries.

Add regression tests, generated protocol fixtures and shared-command coverage in the iOS transcript UI suite.
2026-09-12 19:14:46 +08:00
weishu d18c01b4b6 revert(codex): remove Luna Reserve fallback (#1780)
Revert 8357da0a9d and its later shared-runtime integration.

Remove automatic model fallback, account usage polling, and the related UI, protocol fields, tests, and documentation without adding replacement quota handling.

Keep generic model pagination and method probing required by the current shared-session architecture. Cover idle sessions staying online without usage polling or agent-state churn.
2026-09-12 14:36:45 +08:00