Commit Graph
757 Commits
Author SHA1 Message Date
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 9f54e12c4f docs: streamline agent guidance and scope verification 2026-09-12 22:19:32 +08:00
weishu 1de9613df6 docs: align documentation with current implementation 2026-09-12 22:19:32 +08: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 d29713aeda fix(native): render plan proposals inline
Render ExitPlanMode and exit_plan_mode Markdown from input.plan in iOS and Android conversations. Preserve approvals, raw source and diagnostics while hiding empty output placeholders and prewarming plan documents.

Add generated protocol fixtures and native regression coverage for long plans, live updates, recycling, themes and typography.
2026-09-12 16:06:16 +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
weishu 0c4abcb3d1 feat(codex): share sessions across terminal and web
Use one native app-server for terminal, Web and phone clients while retaining the existing CLI and Runner lifecycle.

Synchronize native queues, permissions, question history and steering state; preserve explicit permission precedence and per-turn usage models. Resume inactive clear commands through Runner and reject independent child cold resumes.

Add shared-runtime regression tests, generated protocol fixtures and lifecycle documentation.
2026-09-12 10:59:17 +08:00
weishu a729456682 fix(claude): answer local permission prompts from web
Bridge main-session PermissionRequest hooks without suppressing the native
terminal dialog. Reconcile replies against native results and clean up on
timeout, cancellation, mode switches, and session changes.

Keep reply IDs distinct from native tool IDs across web and native clients;
add protocol fixtures and regression tests.

Refs #1796
2026-09-11 18:54:47 +08:00
SSU-WEI HUANGandGitHub 68422ed6d3 fix(web): use conversation content for session reference eligibility (#1800)
* fix(web): base session reference eligibility on conversation content

* fix(hub): refresh replacement content after clear abort

* fix(hub): refresh content on idempotent clear aborts
2026-09-11 14:22:16 +08:00
AnanovoandGitHub de4af23482 fix(web): simplify session summary setting copy (#1803) 2026-09-11 14:20:58 +08:00
AnanovoandGitHub 2eb11a5d2c fix(web): clarify round usage metadata labels (#1805)
* fix(web): clarify round usage metadata labels

* test(web): cover duration boundary rounding
2026-09-11 14:20:38 +08:00
DebinandGitHub d124c106a4 feat(web): file context menu with copy path / absolute path / add to composer (#1808)
Add a right-click (desktop) / long-press (mobile) menu to file entries on
the Files page: git change rows, file search results, and the directory
tree. Actions:
- Copy path (workspace-relative)
- Copy absolute path (resolved against session.metadata.path)
- Add to composer (append a backticked reference to the session composer
  draft and navigate back to the chat)

Closes #1807
2026-09-11 14:20:22 +08:00
Junmo KimandGitHub fd2822bab5 fix(agy): allow switching existing sessions to newly available models (#1814)
* fix(agy): say what the model picker is actually waiting on

The spinner in the New Session AGY picker read "Checking Antigravity
authentication…", but nothing at that point checks authentication — the
machine is running `agy models`, and the sign-in prompt is a separate
branch below it, shown only when agy reports the failure.

Name the wait after the work: "Fetching available models…", the same
words agy prints while it fetches.

* refactor(agy): describe a probe by its outcome, not by its response

The probe function returned a finished `AgyModelsResponse`, so "agy could
not be reached" and "agy listed no models" both arrived as a successful
response carrying the hardcoded mirror, and the caller could no longer
tell which had happened. Every policy decision about that answer has to
live inside the probe as a result.

Hand back what the probe observed — a live catalog, an auth failure, or
nothing usable — and let the caller turn it into a response. Same
behaviour: the mirror still stands in for both failure modes, and the
60s cache still holds whatever came out.

* fix(agy): serve the model catalog stale-while-revalidate

The `agy models` probe is a whole agy invocation — around 3s on a good
day, 15s when it times out — and the 60s window meant the New Session
picker paid that again a minute after the last look.

Keep the last listing agy actually returned and answer from it: fresh for
ten minutes, then still answered while a probe refreshes behind it, until
the entry is a day old and stops standing in for the machine at all. A
probe that times out or loses auth leaves that entry alone, so a blip no
longer empties a working picker, and the hardcoded mirror is no longer
recorded as if the machine had reported it.

Three things fall out of that and are handled here. A machine whose
sign-in has actually gone bad would otherwise look healthy for a day, so
an auth failure rides along with the catalog it can still serve — and,
because nothing else would re-probe a catalog that is still fresh, a
warning riding on the answer is itself a reason to look again. A failed
probe is not repeated on the very next request either, or a machine where
agy hangs would spawn it once per poll.

An explicit refresh always costs a probe, and never rides one that was
already running when it was asked for.

* fix(agy): let Retry force a fresh model catalog probe

With the catalog held for ten minutes, Retry would otherwise hand back the
answer it was pressed to replace, so the intent travels to the machine:
`?refresh=true` on the machine route, an optional RPC param, and a
one-shot flag on the query so ordinary mount and focus refetches stay
cheap. Every hop is optional, so a hub and a runner on different versions
still talk — the older side ignores it and answers from its cache.

Retry also has to be reachable, and honest, in the state that needs it.
The machine now answers with both a usable catalog and the sign-in failure
behind it, so the picker keeps the list and says why it may be out of
date, with the button right there rather than only once there is nothing
left to show. Pressing it runs agy, which can take tens of seconds, so the
button says so while it does.

The client contract covers both: `agy-models` is the one catalog route
that can carry an `error` on a successful response, and the one that
takes a refresh parameter.

* fix(agy): use the machine catalog in the in-session model picker

New Session already asks the machine what `agy models` lists, but a
session that is already open offered the built-in list in
`shared/src/models.ts`. That list is a hand-maintained mirror, so a model
agy started offering after the last release could be picked for a new
session and not for the one already running.

Point the composer at the same machine catalog. The mirror stays as the
fallback for the moment before the machine answers, and a model the
session is already on is kept selectable — and readable, when it is one
of the known presets — even after the catalog moves on without it.

* fix(agy): announce a model catalog re-check that changed the answer

Serving the last known catalog answers the picker instantly, but a picker
that was already open kept showing that answer until the user closed and
reopened it — the machine had no way to say it had found something newer.

Say it on the stream that already carries machine changes. The machine
daemon — the only process that answers `<machineId>:listAgyModels` —
emits it, and the hub forwards it as `machine-agy-models-updated` with
nothing but the machineId. Namespace resolution, per-machine delivery and
reconnect replay all come from the existing path.

What counts as a change is what the route would answer, not what sits in
the cache. That distinction carries the cases: a sign-in that lapsed or
came back changes no models yet changes what the user is told; a machine
whose agy was signed out has been answering from the hardcoded mirror, and
its first real listing is the largest change there is, for every client
except the one awaiting it.

* fix(agy): re-read the announced machine's model catalog

On `machine-agy-models-updated`, cancel and refetch that one machine's
catalog query — the app's global connection is always subscribed, so an
open picker redraws wherever it is.

Cancelling first is what makes it correct rather than merely likely.
query-core cancels an in-flight fetch only when the query already holds
data, so a picker opening for the first time would otherwise join the
request already on its way and settle on the listing the announcement
replaced. The refetch is answered from the machine's cache, so it starts
no probe and cannot bounce another announcement back.

A reconnect the hub could not replay takes the resync path, which clears
the agy catalogs the same way — that path has no announcement to fall back
on, so it is the one that can least afford to join a stale request.

* fix(agy): keep a model the user picked when the catalog moves under them

The catalog can now change while the New Session form is open, and the
form dropped any selection the machine no longer advertised — including
one the user had just made.

Keep that one, and list it as no longer listed so the form does not imply
agy is still offering it. A model restored from a draft or a saved
preference is still dropped: it may never have been runnable here.

* fix(agy): announce uncached authentication changes
2026-09-11 14:19:37 +08:00
AnanovoandGitHub 2b402d24d9 fix(web): show Codex round usage metadata (#1685)
* fix(web): show Codex round usage metadata

* fix(web): skip imported Codex round duration
2026-09-09 09:32:43 +08:00
AnanovoandGitHub ceb9314350 feat(web): add scroll-to-bottom button (#1694)
* feat(web): add scroll-to-bottom button

* fix(web): type counted scroll button props

* fix(web): retarget smooth tail scroll

* chore: retrigger pull request checks
2026-09-09 09:32:09 +08:00
Junmo KimandGitHub 7031d60eb4 fix(opencode): expose model-specific reasoning effort options (#1716)
* fix(cli): discover opencode thought_level via set_config_option on model switch

* fix(cli): apply and refresh opencode model switches so thought_level stays discoverable

* fix(web): track opencode effort options across model switches

* feat(cli,hub,web): dynamic opencode effort options in new-session form

* test(cli): avoid platform-specific process event narrowing

* fix(opencode): address variant discovery review findings

* fix(opencode): synchronize effort options with model targets

* fix(opencode): roll back rejected model targets

* fix(cli): guard opencode variant probe workspace paths

* fix(web): clear stale opencode effort on model switch

* fix(cli): clear stale opencode effort metadata

* fix(web): reset stale effort options on model switch

* fix(web): reset opencode effort poll budget

* test(web): enforce opencode effort poll budget
2026-09-09 09:31:30 +08:00
Junmo KimandGitHub f27e58741b feat(web,ios,android): let Claude pick a permission mode when creating a session (#1751)
* refactor(web): route create-form permission control through one native-select predicate

Extract usesNativePermissionSelect(flavor) (grok || codex-family, matching
the existing iOS/Android predicate of the same name) and route
PermissionField's select-vs-toggle gate through it instead of an inline
condition. Rename the codex-family-only state codexFamilyPermissionMode to
nativePermissionMode since it now backs a shared predicate, not just the
codex family. Behavior is unchanged: any stale sessionStorage draft written
under the old codexFamilyPermissionMode key has no value under the new key
and falls back to 'default', which only matters within a single browser
tab's lifetime.

* feat(web): let Claude pick a permission mode when creating a session

Claude was the only create-form flavor still on the global HAPI YOLO toggle
while grok and the codex family got the native permission select, so there was
no way to start a session in Plan Mode without creating it first and switching
the mode from the composer. usesNativePermissionSelect now gates the control
for claude as well, and the spawn body carries permissionMode (including
'default') instead of yolo, which is the shape the other native-select flavors
already send.

The stored hapi:newSession:yolo preference is bridged into the select rather
than dropped, but only for the flavors that have actually moved onto it
(LEGACY_YOLO_BRIDGE_AGENTS = codex, claude). copilot, gemini, kimi and opencode
moved earlier and settled on 'default'; re-enabling Yolo for them now would
widen permissions rather than migrate a preference. This narrows the
sessionStorage draft bridge too, which until now fired for the whole codex
family with no allow-list, so their draft restores yield 'default' instead of
'yolo' — same-tab-lifetime state only.

Claude and the codex family share one nativePermissionMode state and their mode
sets do not overlap, so the existing agent-change reset plus the flavor filters
in the draft loader and the stored launch settings are what keep a codex mode
out of a Claude spawn. Adds the regression test that pins it: pick a mode under
codex, switch to Claude, create, assert the payload carries 'default'.

* feat(ios): let Claude pick a permission mode when creating a session

Extend usesNativePermissionSelect to include claude alongside grok and the
codex family, matching the web change. buildSpawnRequest now derives both
yolo and permissionMode from that single predicate instead of two separate
local flags, so claude sends permissionMode (including 'default') and no
longer sends yolo. Unlike web, iOS carries no persistent YOLO preference
across sessions to migrate — the toggle only lives in the in-memory form or
a draft deleted on success — so there is no bridging logic to add here.

* feat(android): let Claude pick a permission mode when creating a session

Extend usesNativePermissionSelect to include claude alongside grok and the
codex family, matching the web and iOS changes. buildSpawnRequest derives
both yolo and permissionMode from that single predicate, so claude sends
permissionMode (including 'default') and no longer sends yolo. Unlike web,
Android has no persistent YOLO preference to migrate: the toggle only lives
in the form draft, which is deleted once a session is created.

The agent-switch test asserted claude renders the YOLO toggle; it now checks
the native select for claude and keeps the toggle assertion on cursor, which
still carries it.
2026-09-09 09:30:42 +08:00
AnanovoandGitHub 33f51fd0ab feat(web): add configurable PWA taskbar badge (#1759) 2026-09-09 09:28:57 +08:00
AnanovoandGitHub 4dac00ccd9 feat(web): add bulk mark-all-read session action (#1773)
* feat(web): add bulk mark-all-read session action

* fix(web): mark duplicate sessions as read

* fix(web): count hidden duplicate unread sessions

* fix(web): exclude empty session stubs from bulk read
2026-09-09 09:28:08 +08:00
SSU-WEI HUANGandGitHub 8357da0a9d feat(codex): support backend-authorized Luna Reserve fallback (#1780)
* feat(codex): reconcile Luna Reserve fallback and conditional usage

* fix(codex): preserve queued settings and reconcile Reserve sessions
2026-09-09 09:27:40 +08:00
AnanovoandGitHub 66ea35963e feat(web): add word wrap to file source previews (#1754) 2026-09-09 09:24:26 +08:00
Junmo KimandGitHub b14bb0fae5 fix(web): let the installed Android status bar follow the system (#1742)
An installed WebAPK reads manifest theme_color once at install time and uses
it as a fixed toolbar color; runtime document theme-color updates never
repaint it, only the icon tint follows. With theme_color set to white the
status bar stayed white while dark app themes turned the icons white too,
hiding the clock, signal and battery.

Chrome falls back to white when no theme color is present and to black when
the system is dark and no dark color is declared, so dropping theme_color
gives a status bar that follows the Android system theme with icons that stay
readable in both. The dark manifest override that could have supplied a
per-scheme color is not an option: Chrome removed dark color parsing from the
manifest parser in 128 and the mojom fields are marked obsolete.

The document theme-color meta keeps driving every non-installed surface, so
browser tabs are unchanged.
2026-09-09 09:24:16 +08:00
AnanovoandGitHub e26312b51f fix(web): remember file browser tab preference (#1761) 2026-09-09 09:24:06 +08:00
AnanovoandGitHub c191e073b2 fix(web): compact recent path labels (#1757)
* fix(web): compact recent path labels

* fix(web): expose full recent path to assistive tech
2026-09-09 09:23:58 +08:00
AnanovoandGitHub 52a76a0bc4 fix(web): clarify session summary setting copy (#1760)
* fix(web): clarify session summary settings

* fix(web): clarify summary emission copy
2026-09-09 09:23:43 +08:00
AnanovoandGitHub 17ee052d9a fix(web): preserve the visible chat window during rewind (#1766)
* fix(web): preserve chat window during rewind

* fix(web): scope rewind invalidation preservation

* fix(web): clear unknown rewind boundaries

* fix(web): deduplicate rewind invalidations

* fix(web): retain rewind dedupe history
2026-09-09 09:23:31 +08:00