Commit Graph
818 Commits
Author SHA1 Message Date
AnanovoandGitHub f48a6696cf fix(web): reveal directory tails and support touch scrolling (#2237) 2026-10-29 21:22:31 +08:00
AnanovoandGitHub bb51684339 fix(web): hide reorder handles for single composer attachments (#2235) 2026-10-29 12:49:12 +08:00
weishu 01dabad204 fix(sessions): stabilize ordering by latest user input
Persist and backfill the last user-message timestamp, and use it for deterministic session ordering across web and native clients while preserving manual pins.

Remove the fixed-order setting and guard ordering against transcript echoes, replayed messages, and stale REST/SSE updates. Add regression coverage and regenerate the SSE fixture.
2026-10-29 12:47:52 +08:00
Junmo KimandGitHub ba7494c5b5 fix(web): show Codex cached input percentage in response metadata (#2230) 2026-10-29 08:07:11 +08:00
e0b4aac78c fix(web): do not claim Agents are missing when no machine is connected (#1978)
New Session treated a disabled availability query (empty agents, not
loading) as "this machine has no CLIs." That painted the same string as
a real empty probe, next to "No machines available."

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-28 12:18:57 +08:00
ShinnandGitHub d730996e7b fix(web): dismiss composer settings before Escape aborts a turn (#1957)
* fix(web): dismiss composer settings before Escape aborts a turn

* fix(web): preserve IME Escape in composer settings
2026-10-28 12:18:40 +08:00
AnanovoandGitHub 8f1318ed8e fix(web): allow directory collapse during session search (#1797)
* fix(web): allow directory collapse during session search

* fix(web): preserve directory collapse after filtering
2026-10-28 12:17:59 +08:00
Junmo KimandGitHub 2a94782412 fix(web): make composer session mention pills ellipsize (#1678) 2026-10-28 12:17:41 +08:00
a23d78f416 fix(web): avoid session misclicks during live reordering (#1656)
Co-authored-by: NPUlrk <21106497+NPUlrk@users.noreply.github.com>
2026-10-28 12:17:28 +08:00
Junmo KimandGitHub a4a5779829 fix: render opencode edit/write/read tool calls through the shared views (#1644)
* feat(cli): add diff-shape tool input canonicalization helper

* fix(cli): canonicalize ACP diff-shaped tool inputs for the Edit/Write views

* fix(opencode): normalize local-mode edit/write tool parts to the diff view shapes

* fix(opencode): preserve replaceAll and fix id-less hook pairing

- canonicalizeDiffToolInput now carries OpenCode's optional replaceAll through
  to the canonical Edit input as replace_all, so persisted history keeps
  whether an edit replaced one occurrence or all.
- opencodeLocalLauncher computes the execute-hook pairing signature from the
  raw name/input BEFORE canonicalizing, so an id-less 'before' with empty or
  partial args shares a queue key with the full 'after'.

* fix(opencode): preserve snake_case replace_all in edit canonicalization

canonicalizeDiffToolInput accepts already-canonical snake_case edit inputs
(file_path/old_string/new_string), but only read camelCase replaceAll. Read
replace_all too so the execution argument survives every call site.

* fix(opencode): upgrade partial local edit/write calls when full args arrive

A partial native before ({filePath}) was emitted as-is and recorded as sent,
so the later canonical full input was deduped and the web Edit/Write view never
received the complete arguments. Track the emitted input per callId and, when
a partial native edit/write call is superseded by a canonical full input, emit
a replacement tool-call (same callId) before the result. Applies to both the
message.part.updated and tool.execute.* local event paths.

* fix(opencode): release completed tool inputs from upgrade tracking

emittedToolInputs retained the full emitted Write content and Edit old/new
strings for the whole launcher lifetime. Once a call is completed
(sentToolResults), later upgrades are already blocked, so drop the stored
payload in both result paths. Add a lifecycle test that a stale part for a
completed callId neither re-emits nor emits a duplicate result.

* style(opencode): restore 4-space indentation on local launcher tracking decls

The per-session tracking declarations (sentTextParts, sentToolCalls,
sentToolResults, emittedToolInputs) were flush-left inside opencodeLocalLauncher,
violating the repo's 4-space indentation rule.

* fix(opencode): gate diff canonicalization on edit/write semantics

canonicalizeDiffToolInput now takes the tool's semantic name/kind and only
normalizes known edit/write aliases (edit, write, write_file, replace, modify,
file_edit). The Edit shape (path + both old/new) is unambiguous on its own, but
Write is just {path, content} — a shape many non-edit MCP tools also accept —
so without the gate those tools were renamed to Write and their extra args
dropped. OpenCode maps both edit and write to kind 'edit', which stays in the
allow-list, so the native-shape cases keep working. ACP callers pass the update
kind (falling back to the derived name); local callers pass the tool name.

* fix(opencode): re-emit canonical edit/write when the input changes

shouldEmit only handled the null->canonical transition, so a shape-complete
Write/Edit that already canonicalizes (e.g. {content: ""}) was suppressed
when the final content arrived, leaving the web diff with stale input. Emit on
first usable input and re-emit whenever the canonical input value changes,
across both message.part.updated and tool.execute.* paths.

* test(opencode): cover changed-input re-emit on the message.part.updated path

The execute-hook path already had a regression for canonical inputs changing
from empty to final content; add the matching case for message.part.updated so
both local emit paths are covered.

* fix(web): render opencode native read output as a file content code block

OpenCode's lowercase read tool fell through to the generic markdown
result view, collapsing the whole file into a soft-wrapped paragraph
and dropping the <path>/<type>/<content> markup. Register the read
alias, unwrap the opencode payload (including the trailing
"(End of file ...)" trailer), parse its 1-indexed line numbering into
the code block gutter, and resolve the camelCase filePath key in the
card title.

* fix(web): only unwrap complete opencode read envelopes

Anchor the envelope match to the full <path>/<type>file/<content>
structure so plain Read results whose file text contains a literal
<content> element are left untouched, capture the body greedily so an
embedded </content> survives, and stop trimming real trailing
whitespace.
2026-10-28 12:17:14 +08:00
AnanovoandGitHub 8d94a745b5 fix(web): prevent session navigation on right-click (#1636) 2026-10-28 12:16:47 +08:00
AnanovoandGitHub 21b0ff30f3 fix(web): add exact PWA notification icon sizes (#1625) 2026-10-28 12:16:25 +08:00
AnanovoandGitHub aa0172e6aa fix(web): align fork and rewind confirmations (#1603) 2026-10-28 12:16:12 +08:00
AnanovoandGitHub 7765fd3378 fix(web): make long file errors expandable (#1422)
* fix(web): make long file errors expandable

* fix(web): count graphemes in expandable errors

* fix(web): reset expanded errors when messages change

* fix(web): reset expansion when error text changes

* fix(web): avoid duplicate file error alerts
2026-10-28 12:15:58 +08:00
AnanovoandGitHub f6bc583471 feat(web): open a session in a separate window (#2192)
* feat(web): open a session in a separate window

* test(web): cover session sidebar restore control

* fix(web): preserve hidden sidebar across session navigation

* fix(web): retain hidden sidebar on session routes
2026-10-28 12:15:22 +08:00
AnanovoandGitHub 009e6d7799 feat(codex): allow historical forks while a turn is running (#2185)
* feat(codex): allow historical forks while a turn is running

* fix(web): fork user-only completed turns during generation
2026-10-28 12:15:11 +08:00
AnanovoandGitHub 577ab7546b feat(sessions): add explicit per-session title locks (#2072)
* feat(sessions): add explicit per-session title locks

* fix(sessions): preserve title locks across rename entry points
2026-10-28 12:14:48 +08:00
Junmo KimandGitHub 328ddb0ee3 feat(cli): show Claude compaction as a summary card with token delta (#1691)
* feat(cli): report claude compact completion with token delta

Replace the plain 'Compaction completed' line with a pi-style
'📦 Compacted (<before> → <after> tokens)' fallback built from the
system/compact_boundary metadata, and prefix the start notice with 📦.
The SDK stream carries no summary body (measured), so the delta line is
the terminal state of the completion event.

* fix(cli): hide local-command tag user messages from chat

Claude Code echoes slash-command bookkeeping back over the SDK as plain
user-role messages wrapped in <local-command-*> tags. Flag them isMeta so
the existing downstream filters drop them instead of leaking raw stdout.

* feat(web): collapse compact summary card by default

Keep the header (title, token delta, timestamp) always visible and hide
the summary body behind a chevron toggle. Toggle state is component-local
and non-persistent.

* feat(cli): promote claude compact summary from the local transcript

* fix(cli): don't double-render the manual /compact boundary as a status line

* fix(cli): detect later-turn /compact and ignore stale transcript summaries

* fix(cli): establish the compact transcript baseline before the result can race it

* fix(cli): narrow the local-command meta filter to the compact stdout echo

* fix(cli): range-read only the transcript tail when polling for the compact summary

* fix(cli): complete the transcript tail read before parsing the compact summary

* fix(cli): refresh the context bar with post-compaction tokens after /compact

* fix(cli): scope the Compacted stdout suppression to the active compact turn

* fix(cli): make compact lifecycle ownership race-safe

* fix(cli): publish compact outcome before accepting the next turn

* fix(cli): cancel compact completion with its failed stream attempt

* fix(cli): propagate deferred compact completion failures

* refactor(cli): coordinate compact completion in the response loop

* fix(cli): correlate compact completion with its stream signal

* fix(cli): commit compact ownership when its result arrives

* refactor(cli): name the manual compact stream suppression predicates

* fix(cli): suppress the compact echo even when hook output is appended

* fix(cli): carry the resolved context window into the compact usage refresh
2026-10-28 12:14:34 +08:00
Junmo KimandGitHub ae147e85db feat(opencode): import local session history (#1677)
* feat(cli): list local OpenCode sessions over machine RPC

* feat(hub): import OpenCode sessions into HAPI

* feat(web): import OpenCode history from new-session picker

* fix(cli): derive OpenCode imported message ids from part ids

Random ids made every scan differ, so delta classification flagged
unchanged transcripts as diverged on re-import.

* fix(cli): surface unreadable OpenCode db as rpc error

A locked or unopenable opencode.db now fails the listing instead of
reporting an empty success; summaries normalize timestamps and the web
import toast counts only created or actually-appended sessions.

* fix(hub): fail closed on opencode re-import past live HAPI continuation

Imported entries must remain a strict prefix of the native transcript.
Insertions ahead of the imported boundary or new native messages after
the session continued in HAPI now fail closed instead of appending
duplicated or reordered history.

* fix(web,hub): apply selected launch config when importing OpenCode history

Pass the model, reasoning effort, and permission mode chosen in the
new-session picker through the import request and persist them on the
imported HAPI session via applySessionConfig, so reopen launches with
the visible selection instead of stale or default values.

* fix(hub,cli): preserve explicit launch resets and skip streaming parts on import

- Launch config keys are applied by presence, so explicit null/default
  selections from the picker must reach applySessionConfig to clear
  stale model, effort, or permission values on re-import. Requests that
  omit the fields entirely still leave persisted config untouched.
- Assistant text parts are mutable while a response streams. Mirror the
  live scanner gate and import only finished (time.end set), synthetic,
  or user text so an in-progress row can never be stored and later
  finalized as transcript_diverged.

* test(hub): fix launch-config reset assertions to satisfy strict types

* fix(cli): fail opencode transcript reads on query errors instead of partial snapshots

Swallowed SQLite errors could return a truncated message list that the
hub later rejects as transcript_diverged with no repair path. Full
transcript reads now propagate query failures; summary lastUserMessage
extraction stays resilient per message.

* test(cli): cover unreadable-part-table transcript read stays non-partial

* fix(hub): scope launch-config failures to the per-session import result

A thrown applySessionConfig call used to escape the bulk handler as an
unstructured 500 even though transcripts were already persisted. Catch
it per session and report it as a config_failed result so remaining
sessions in a bulk request still import.

Also cover the mid-transcript part-query failure with an injected
opener that rejects instead of returning a holey transcript.

* fix(cli): read opencode transcripts inside a deferred snapshot transaction

Messages and their parts were read in separate statements, so a
concurrent OpenCode write could leave a hole in the imported sequence
that positional re-import checks then reject forever. Wrap each full
transcript read in BEGIN DEFERRED/COMMIT with rollback on failure.

* fix(cli): follow machine path policy for OpenCode imports

* fix(web): use unified permission mode for OpenCode imports

* fix(opencode): harden session import consistency

* fix(hub): preserve metadata on rejected OpenCode imports

* fix(hub): refresh OpenCode import state under lock

* fix(hub): resolve OpenCode imports after locking

* fix(opencode): bind imported resume to source machine
2026-10-28 12:14:18 +08:00
SSU-WEI HUANGandGitHub 3f4bd9afea feat(web): show project in session header (#1635)
* feat(web): show project in session header

* fix(web): hide unknown worktree project paths
2026-10-28 12:14:04 +08:00
8b5cde7e44 feat(web): persist and send voice input message across session navigation (#1435) (#1436)
* feat(web): persist and send voice input message across session navigation (#1435)

* fix(web): address bot review findings for voice send across session switch (#1435)

* fix(web): resolve teardown, inactive session, send error recovery and mention serialization findings (#1435)

* fix(web): preserve draft initialText on error and handle async cross-session permission lookup (#1435)

* fix(web): preserve session draft on background failure and send via composer delivery policy (#1435)

* fix(web): send dictation directly via API and fix standard dictation language and recording support (#1435)

* trigger CI rerun for voice dictation session switch fixes (#1435)

* fix(web): prevent failed voice sends from clobbering replacement composer text (#1435)

* fix(web): prevent zero-byte recording failure from clobbering replacement composer draft (#1435)

* fix(web): prevent unmount voice transcription, clear draft on send success, and clear partial transcript (#1435)

* fix(web): stop button transcribes into composer without auto-sending; only send button triggers stopAndSend (#1435)

* ci: rerun CI for flaky claudeRemote test timeout (#1435)

* ci: retry flaky claudeRemote test (#1435)

* fix(web): guard dictation send path against attachments/schedule/connecting and protect newer drafts from late overwrite (#1435)

* fix(web): preserve Pi steer delivery mode for background dictation sends (#1435)

* fix(web): include active state in handleSend dependency array (#1435)

* fix(voice): surface MediaRecorder failures instead of transcribing partial audio

A MediaRecorder error can still be followed by dataavailable and stop
with partial bytes, so the previous onstop-only flow could transcribe
and auto-send corrupt audio. Track recorder.onerror and short-circuit
the stop handler: restore the draft and surface an error instead of
transcribing. Add a regression test for error -> dataavailable -> stop.

* fix(web): resume inactive sessions for voice dictation direct-sends

A voice dictation direct-send (mic -> send) posts the transcribed message
straight to the session, bypassing the text pipeline's inactive-session
resume. The hub rejects message POSTs to inactive sessions with 409
session_inactive, so sending to an archived session always failed (while
reopening it first made the same send succeed).

- stopAndSend now accepts { resolveSessionId, onSessionResolved } and
  resolves the target session after transcription, before the POST
- HappyComposer allows direct voice sends on inactive sessions when a
  resolver is provided (instead of downgrading to a plain stop), and
  threads SessionPage's canonical resolveSessionId + handleSessionResolved
  (the same ones the text send path uses) into the dictation hooks
- after the message is delivered to the resumed session, the UI
  navigates/seeds the live session; failed sends keep the existing
  draft-restore + error surfacing
- realtime dictation sends apply the same resolution

* fix(web): satisfy HappyChatContextValue in markdown-a test helper

Upstream 51ae260a added the required showSessionSummaryInChat field to
HappyChatContextValue but missed the markdown-a test helper, so web
typecheck (part of CI) fails at the upstream tip. Provide the field like
the sibling test helpers do.

* fix(web): expose a Send action during connected dictation

The visible composer button becomes Stop-only while dictation is
connected, so mouse/touch users could never reach the session-bound
stopAndSend path in handleSend (keyboard Enter was the only entry
point). Add an explicit Send button next to Stop when dictation is
connected; it routes through onSend('default') so the transcript is
captured to the target session and survives navigation (per #1435),
while Stop keeps its stop-without-sending semantics.

Addresses HAPI bot review on #1436.

* fix(web): hide dictation Send button when direct send is not eligible

The Send button shown during connected dictation only performs a real
send when handleSend's direct-send path runs; with attachments, a
pending schedule, or scratchlist mode it silently falls back to
dictation.toggle() (stop + transcribe). Gate the button on the same
eligibility conditions as handleSend so a visible Send control always
actually submits.

Addresses HAPI bot review (Minor) on #1436.

* fix(web): drop superseded markdown-a test context fix

Upstream merged its own showSessionSummaryInChat fix for the markdown-a
test helper; the duplicate property added for the pre-merge CI break is
no longer needed.

* fix(web): gate dictation direct-send on full attachment state and guard late navigation

- dictationDirectSendEligible now uses blocksScheduling (visible + hidden
  persisted + still-hydrating attachment drafts) instead of only the
  visible attachments array. An inactive composer keeps persisted
  attachment blobs out of the visible array, so the previous check could
  expose direct Send, resume the session, and POST only the voice text,
  stranding the blobs on the obsolete source session. handleSend's
  canDirectSend and the Send-button gate share one helper so they can
  never disagree.
- The voice send survives unmount by design (#1435); a send that finishes
  after the operator left the source session no longer navigates them
  back. handleVoiceSessionResolved only navigates when still on the
  source session's page.

Addresses HAPI bot review (Major + Minor) on #1436.

* fix(web): read live route state when a background voice send completes

The previous guard captured the render-time pathname; once the source
SessionPage unmounts, that captured value still points at the source
route, so a voice send that finishes after the operator left would still
navigate them back. Read router.state.location.pathname at callback time
instead (the router instance is stable; its state is always current).

Addresses HAPI bot review (Minor) on #1436.

* fix(web): recover post-resume send failures under the resumed session id

When a voice dictation send fails after the inactive session was already
resumed, the catch path persisted the retryable transcript under the
archived source session id — which the hub has superseded — so the
recovery was invisible on the live session. Save the draft under the
resumed id, and fire onSessionResolved so the operator is taken to the
live session where the retryable text lives. Applies to both standard
and realtime dictation.

Addresses HAPI bot review (Major) on #1436.

* fix(web): snapshot resumed-session draft before the post-resume send

The previous recovery guard read getDraft(targetSessionId) inside the
catch — after the send already rejected — so it compared the current
draft to itself and always overwrote text the operator typed into the
resumed composer while the request was in flight. Snapshot the resumed
session's draft at resolve time instead, so a newer draft is never
clobbered. Applies to both standard and realtime dictation.

Addresses HAPI bot review (Major) on #1436.

* fix(web): preserve both drafts when a post-resume send fails

When the operator typed a newer draft into the resumed composer while
the voice send was in flight and the send then failed, the recovery
guard correctly refused to overwrite the newer draft — but that dropped
the failed voice text entirely. Merge instead: append the failed message
after the newer draft so both remain recoverable. Applies to both
standard and realtime dictation.

Addresses HAPI bot review (Major) on #1436.

* fix(web): merge live composer text into failed voice-send recovery

Failure recovery previously read only the persisted draft store, but
while the composer is mounted its edits live in memory (session storage
is written only on unmount). If the operator typed replacement text
while transcription or delivery was pending, recovery either skipped the
composer restore (persisting only the voice text, which the later unmount
then overwrote) or clobbered the replacement. Recovery now reads the live
composer text when mounted and appends the failed voice text after it,
reseating the composer to the merged draft; the unmounted case merges a
persisted draft written since the snapshot. Applies to recording,
zero-byte, transcription, send, and realtime failure paths in both
dictation implementations.

Addresses HAPI bot review (Major) on #1436.

* fix(web): preserve dictation audio constraints

* fix(web): transfer follow-up drafts after voice resume

* fix(web): merge source drafts on failed voice resume

* Preserve resumed voice drafts and replace recovered composer text

Sample destination/source draft merges at persistence time; use a replacement callback for realtime recovery. Six regressions fail before the fix; 66 focused tests, all five package suites, typecheck and 16 Playwright checks pass.

* Recover delayed voice failures into the current composer mount

Resolve live recovery targets by session identity and update the draft ref synchronously before later unmount. Both standard/realtime remount regressions fail before the fix. 85 focused tests, full package suites, typecheck and 16 Playwright checks pass.

* fix: preserve live composer drafts after background voice success

Use shared transfer sampling for standard and realtime sends; update the mounted destination before navigation and unmount. Verified focused voice/transfer tests, typecheck, all five full package suites (CLI rerun serially), and 16 browser checks.

* fix: retain resumed voice follow-ups and destination attachments

Verified focused regressions, root typecheck, all five complete package suites (environment-sensitive failures rerun serially), and 16 browser checks.

* fix: preserve independent destination drafts during voice recovery

Cross-session recovery always retains destination text and independently merges source follow-ups. Remove destination baseline replacement from both hooks. Verified 72 focused regressions, root typecheck, all five full package suites (Hub rerun serially), and 16 browser checks.

* fix: preserve voice recovery drafts before composer hydration

Expose persisted draft text until hydration and update the text ref synchronously when restoring. Cover both voice modes with a held animation frame and intentional empty text after hydration. Verified 94 focused checks, full package suites, root typecheck, and 16 browser checks.

* fix(web): retain ElevenLabs finalization across navigation

* fix(web): preserve new same-text drafts across pending voice sends

* fix(web): refresh voice send callback after draft hydration

* fix(web): resolve voice destinations from current session activity

* fix(web): give voice direct-sends a localId so agent echoes dedupe

Voice dictation, realtime dictation and the cross-session voice tool sent
through api.sendMessage with localId null. Shared Codex then minted a
random clientUserMessageId for the turn and echoed the user message back
with it; the hub dedupes only on (session, localId), so every voice
message was stored and shown twice.

Route all voice sends through sendVoiceMessage, which mints the same
local-* id useSendMessage uses.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 12:13:53 +08:00
ffd950a220 fix(codex): drop safe-yolo from Codex permission modes (#2183)
* fix(codex): drop safe-yolo from Codex permission modes

Every Codex session runs on the native shared runtime (runCodex is
runSharedCodex), which rejects safe-yolo. The mode stayed in
CODEX_PERMISSION_MODES and six filters hid it again in the web picker,
composer, hub permission-mode route, hapi set-mode and both native
apps. API callers that validate against the flavor catalog (e.g. a
spawn with permissionMode: 'safe-yolo') still passed it through and
only failed inside the runner.

Remove it from the Codex catalog (shared, iOS, Android, fixture) and
delete the filters. autoReview is the native replacement. Safe Yolo
stays for Gemini, Kimi and Copilot.

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

* test(ios): Codex option list now has four modes

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 12:12:55 +08:00
fe2fbb881d fix: list Claude background tasks from the snapshot instead of guessing (#2177)
* fix: list Claude background tasks from the snapshot instead of guessing

The status panel only had a count from `background_tasks_changed` and
reconstructed commands by regex-matching Bash results in loaded chat
history, so tasks whose start message was unloaded, auto-backgrounded
("moved to the background (ID: …)"), or non-Bash showed
"N running · exact command unavailable".

The hub now keeps the snapshot's tasks (id, type, description) on the
session alongside the count. The web panel lists them directly, using
the exact task ID to look up the Bash command when loaded and the
snapshot description otherwise.

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

* fix(web): only attribute a Bash command from its background launch result

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

* fix(native): accept backgroundTasks in strict session patch decoders

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 12:12:41 +08:00
AnanovoandGitHub 116756e833 fix(web,cli): clarify archive cleanup progress and bound shutdown waits (#2061)
* fix(web,cli): clarify archive cleanup progress and bound shutdown waits

* fix(web): keep archive dismissal and delete waiting states consistent

* fix(web): scope archive subscriptions to active consumers
2026-10-28 12:11:52 +08:00
AnanovoandGitHub 78472e1c78 docs(web): document session list and search behavior (#1984)
* docs(web): document session list and search behavior

* chore: retrigger PR checks

* docs(web): clarify searched group pagination
2026-10-28 12:11:27 +08:00
Matvey RudoyandGitHub 2f72043c5e fix(web): keep tail following through layout-driven scroll (#1960)
Content growth and mobile-keyboard resizes increase the distance from the
bottom without any user gesture. handleScroll treated that as the user
leaving the tail and disabled auto-follow, so the session feed stopped
following new messages until it was scrolled down manually.

While tail-follow is active, treat a non-upward scroll as layout churn
and re-pin to the newest message instead of dropping out of tail mode.
Add regression tests for transcript growth and viewport shrink.
2026-10-28 12:11:06 +08:00
Junmo KimandGitHub cd0b8e3589 fix(claude): stop locking Claude sessions to three hard-coded model aliases (#1948)
* feat(shared): derive Claude model choices from the CLI catalog

The choices are the rows of the claude CLI's own /model picker, in its
order and with its display names. The stored value is what
claude --model receives: an alias is resolved again each time the
session's claude process starts, a full id stays on its model, so
following and staying need no new field.

Effort support is read per model so a model switch can drop an effort
level the next model does not offer.

* feat(cli): discover Claude models from the CLI's initialize response

A machine RPC starts claude in stream-json mode, sends only an
initialize control request and reads the models it reports, so no turn
runs and no transcript is written.

claude --print runs a directory's project hooks and .mcp.json servers
without a trust prompt, so the probe starts from the home directory
with user settings only, no MCP servers and hooks disabled.

Successful catalogs are cached per claude executable for five minutes
and concurrent requests share one probe; failures are not cached.

* feat(hub): expose the Claude model catalog per machine

GET /machines/:id/claude-models forwards to the runner's
listClaudeModels RPC. A runner that predates the handler answers with
the stable rpc_target_missing code so clients can keep their built-in
list.

* feat(web): offer the machine's Claude models in the model pickers

The composer and the create-session form list the rows the machine's
claude CLI shows in its own /model picker, as it shows them. Built-in
presets the catalog does not list remain selectable after it.

Until the catalog arrives, or on a runner without discovery, the
built-in presets are shown as before. A saved Claude model is no longer
reset to Default when the built-in list lacks it; the create-session
form keeps it and marks it as no longer listed when the catalog
dropped it.

* feat(hub,web): keep Claude effort within what the selected model offers

For Claude sessions the model endpoint accepts an optional effort, so a
model switch and the effort it keeps reach the session as one config
change. Other flavors are refused: Pi, for one, commits the model
before its thinking level. When the machine's catalog says the next
Claude model does not offer the current effort, the composer clears it
in that same request. The create-session form drops such an effort
however the model was set, including a restored preference or draft.

Effort choices in both places narrow to the selected model's levels;
without a catalog every level stays available as before.

* fix(web): retry Claude model discovery after a failed probe

The machine answers a failed probe with success:false over HTTP 200.
The query cached that as an answer for its stale time, so a user who
signed in and reopened a picker within a minute still saw only the
built-in models. A failed probe is now a query error, which is not
kept, and the next picker to mount asks the machine again.

* fix(web): list only the Claude CLI's models once its catalog is known

The built-in presets were kept after the catalog so that a partial list
could not hide Opus. The catalog is the CLI's own /model list, though,
not a partial one, and the presets added entries the CLI does not show
(Sonnet 1M, Opus 1M, Fable 1M). With a catalog the list is now exactly
its rows; a saved model it does not list stays as the current model,
and New Session marks it as no longer listed. Without a catalog the
built-in presets are shown as before.

* fix(shared,web): list earlier Claude versions under Other models

Claude Code's model sheet shows each family's newest model first and
every earlier version under an "Other models" heading. The CLI's
catalog lists models in that order but does not say where the split
is, so the first row of each family is taken as its newest and later
rows of the same family as earlier versions.

The composer's model list and New Session's model select now show the
same two groups, keeping the CLI order within each. Nothing is folded:
every row stays one click away, as it does in Claude Code.

* test(web): pin that Claude 1M variants come from the CLI catalog

Claude Code 2.1.105, 2.1.200, 2.1.240 and 2.1.274 list a 1M choice as
its own catalog row (opus[1m], claude-fable-5[1m]); 2.1.303 and 2.1.310
list none, and on 2.1.310 opus and sonnet run with a 1M context window.
The catalog-only list therefore offers a 1M variant exactly when the CLI
does. Pin that and record why the built-in [1m] presets are not appended.
2026-10-28 12:10:36 +08:00
SSU-WEI HUANGandGitHub 3e68e29110 fix(codex): reuse live model catalogs and extend discovery cache (#1938)
* fix(codex): reuse live model catalogs and extend discovery cache

Closes #1937

* fix(codex): track relative model catalog changes

* fix(hub): expose missing Codex session model RPC code
2026-10-28 12:10:08 +08:00
8ae7f0aa7b fix(cursor): in-place relaunch when switching live session to Auto (#1909)
* fix(cursor): in-place relaunch when switching live session to Auto

When ACP does not advertise literal auto, switch to Auto by respawning
the same hub session under --model auto + session/load instead of
rejecting. Keeps #1819 CLI Auto pin semantics. Fixes #1908.

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

* fix(cursor): widen in-place relaunch spawn model typing

Allow null/undefined rollback spawn models from resolveCursorSpawnModel.

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

* fix(cursor): re-pin ACP session id after in-place relaunch load

If session/load returns a different agent id, update hub metadata the
same way as a fresh session so resume stays recoverable.

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

* fix(cursor): coordinate message loop with in-place Auto relaunch

Await relaunch completion before prompting, re-read the live backend
after model apply, and publish this.backend only after session/load.
Addresses tip bot Majors on #1909.

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

* fix(cursor): harden Auto relaunch against prompt races and cleanup

Hold promptInFlight before mode apply, end the loop if backend restore
fails instead of requeue-spinning, and discard late replacement backends
when session teardown has started. Addresses tip bot Majors on #1909.

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

* fix(cursor): avoid continue-in-try circular typing; finish relaunch harden

Refactor mismatch requeue off continue-inside-try so typecheck passes,
keeping promptInFlight hold and teardown discard for #1909 tip bot Majors.

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

* fix(cursor): preserve queued turns and Auto-review across relaunch failure

Requeue dequeued batches before ending when restore fails, and still
record Auto-review (slash after publish) when mode flips while backend
is null mid-relaunch. Addresses tip bot Majors on #1909 @ 9e5f241f1.

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

* fix(cursor): defer hub ack until Auto relaunch has a live backend

Avoid acknowledging queued turns before a usable ACP process exists, and
reset Auto-review slash bookkeeping on in-place process replacement.

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

* fix(cursor): recompute Auto-review spawn flag for replacement ACP

Slash bookkeeping after in-place relaunch must use the replacement
process's --auto-review arg, not the prior process's sticky flag.

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

* fix(cursor): drop queued Auto-review slash when replacement has --auto-review

Cursor treats /auto-review as a toggle; leave no leftover slash for a
spawn that already enabled Auto-review via the CLI flag.

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

* fix(cursor): discard held Auto-review toggle after --auto-review relaunch

Mark internal toggles so queue reconcile and post-relaunch dispatch drop
only HAPI-generated /auto-review slashes, preserving user-sent commands.

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

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-28 12:09:00 +08:00
2fb408e60b fix(web): synthesize answerable card for pending permissions dropped by the tracer (#1903)
A pending permission whose sidechain tool_use never makes it into the
rendered block tree leaves the session blocked with no answerable UI: the
attention indicator shows a pending request, but no card renders anywhere.

Root cause: the tracer drops sidechain messages it cannot attribute to a
parent Task/Agent call — no parentToolUseId on the message and no
prompt-holding sidechain root in the window (observed with background
subagents whose transcripts live in separate subagents/*.jsonl files and
whose initial prompt message never reaches the hub). The orphan-card
fallback in reduceChatBlocks then checked *raw transcript* tool ids, so
the dropped message still suppressed synthesis.

Check the final rendered tree instead (recursively, including blocks
nested under Task/Agent cards): any pending permission that is not
rendered anywhere gets exactly one synthesized, answerable card. This is
the approach suggested in #1073 itself.

Verified against a live session where 48 of 69 messages were dropped by
the tracer, including the pending WebFetch tool_use; with this change the
request renders as an orphan card and can be approved from the chat.

Refs #1073, refs #587.

Co-authored-by: zx-shi <shi@users.noreply.github.com>
Co-authored-by: Claude Code <noreply@anthropic.com>
2026-10-28 12:08:42 +08:00
AnanovoandGitHub ea12ff98c6 fix(web): align file preview content around scrollbars (#1726)
* fix(web): align file preview around scrollbars

* fix(web): smooth file preview gutter compensation

* fix(web): preserve file preview image viewer positioning
2026-10-28 12:07:02 +08:00
AnanovoandGitHub 35205ddb53 fix(web): keep mobile code gutters clear across renderers (#1720) 2026-10-28 12:06:50 +08:00
AnanovoandGitHub 1436c9d6a7 fix(web): improve fullscreen image preview toolbar layout (#1665)
* fix(web): improve fullscreen image preview toolbar layout

* chore: retrigger PR review

* fix(web): keep mobile fullscreen images within viewport

* fix(web): keep mobile fullscreen images within viewport
2026-10-28 12:06:37 +08:00
34c6832119 fix(web): keep machine health visible for single-machine layouts (#1664)
* fix(web): keep machine health visible for single-machine layouts

The sidebar redesign gated the machine filter bar behind >= 2 machines,
so a single-machine setup lost its only surface for machine CPU/RAM
health and session counts (#1259).

With exactly one machine there is nothing to filter, so render a compact
non-interactive summary row in the bar's slot instead: machine label,
session count, and inline CPU/RAM metrics — visible on touch layouts too,
since no hover is needed. Pointer users additionally get the same
capacity popup as the filter chips.

Fixes #1259

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

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

* fix(web): show machine summary when the sole machine has no sessions

Address the review finding on #1664: the summary row was derived solely
from session groups, so a single-machine setup with zero visible
sessions (fresh install, or "active only" filtering) still hid machine
health — the exact regression #1259 reports.

Fall back to the sole machine from the machines list in that case; the
row then shows its label, a zero count, and inline CPU/RAM metrics.

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

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

---------

Co-authored-by: yellowdog01 <yellowdog01@users.noreply.github.com>
Co-authored-by: HAPI <noreply@hapi.run>
2026-10-28 12:06:22 +08:00
AnanovoandGitHub 7a320d793a fix(web): consolidate tool card display settings (#1599)
* feat(web): add configurable tool grouping mode

* fix(web): default tool grouping to grouped

* fix(web): sync tool grouping mode in the same window

* fix(web): reset tool group ids when mode changes

* fix(web): consolidate tool card display settings

* fix(web): preserve tool chronology in grouped mode

* fix(web): preserve permission details across tool grouping

* fix(web): honor command aliases in detailed tool cards

* fix(web): preserve tool mode details and unseen state

* fix(web): refresh fixtures after tool group merge

* fix(web): keep protocol fixtures in grouped mode

* fix(web): preserve grouped tool defaults

* fix(web): prioritize ACP tool kinds

* chore: retrigger PR checks
2026-10-28 12:05:49 +08:00
AnanovoandGitHub b4f46f58bb fix(web): preserve shared turn layout and generated file downloads (#1543)
* fix(web): preserve wrapped inline code in shared images

* test(web): snapshot wrapped inline code export

* ci: cover shared turn export on Linux

* ci: cover shared turn export on Linux

* fix(web): preserve generated file controls in shared previews

* fix(web): ignore late shared file responses

* fix(web): keep shared file links keyboard accessible

* chore: retrigger PR checks

* chore: retrigger PR checks

* fix(ci): correct E2E workflow indentation

* fix(web): hide table scrollbars in shared image exports
2026-10-28 12:05:20 +08:00
AnanovoandGitHub e32eac84f5 fix(web): exclude in-progress sessions from unread counts (#2203) 2026-10-28 11:02:19 +08:00
eb49ccd62b fix(web): resolve relative Markdown links and images in file preview (#2211)
* fix(web): resolve relative Markdown links and images in file preview

The standalone Markdown preview rendered without the previewed file's path,
so relative .md links became inert and relative images resolved against the
web page URL. Pass the session file to the renderer and resolve relative
hrefs/srcs against its directory: links open the sibling file preview,
images load through the session file-read API. HTTPS images are unchanged
(direct browser load); paths that escape the root stay inert.

Fixes #1917

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

* fix(web): bound preview image reads and check workspace containment

Only read supported image types whose directory-listed size is at most
10 MiB, and require absolute resolved paths to stay inside the session
workspace so escapes remain inert.

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

* fix(web): keep session links and browsing context in Markdown preview

Resolve session references before relative file paths, and carry the
preview's origin/tab/query into relative file links so Back returns to
where the user entered from.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 10:31:28 +08:00
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