Commit Graph
110 Commits
Author SHA1 Message Date
weishu ba2b5c5651 Release version 0.33.5 2026-11-01 13:19:38 +08:00
weishu c94a968b81 fix(android): observe platform IME state in home search tests 2026-11-01 12:56:13 +08:00
weishu b0d01c5307 Release version 0.33.4 2026-11-01 12:26:43 +08:00
AnanovoandGitHub 639f71b6a6 test(android): name the IME phases and upload instrumentation reports (#2256)
* test(android): name the IME phases and upload instrumentation reports

* test(android): report Compose's IME inset height on failure

* test(android): import the WindowInsets.ime extension used by the diagnostic
2026-11-01 09:54:30 +08:00
Junmo KimandGitHub be41e854e0 feat(codex,opencode): show compaction summaries and token counts (#2249)
* fix(chat): render available compaction facts

* feat(opencode): unify native compaction summary cards

* fix(opencode): report actual compaction application lifecycle

* feat(codex): render native compaction checkpoints and metrics

* fix(chat): render compaction summaries and token facts in native clients

* fix(opencode): preserve transcript priming across read failures

* fix(opencode): recover snapshot-only replies after attachment
2026-11-01 09:54:10 +08:00
weishu 5ef4304a4a fix: synchronize read state across web and native clients
Track readable content revisions and durable read cursors in the hub. Acknowledge only rendered foreground tails, persist offline read operations, and preserve newer manual unread choices across devices.

Migrate existing history to a read baseline and cover pagination receipts with shared native conformance fixtures.
2026-11-01 09:52:51 +08:00
weishu 1c861b2f0e feat(android): redesign home toolbar search and filters 2026-10-30 07:15:39 +08:00
weishu d9acc0c31e Release version 0.33.3 2026-10-29 22:29:49 +08:00
3f8686e623 feat(claude): offer the four current Claude models, drop the 1M presets (#2153)
* fix(claude): drop the obsolete 1M model presets

The opus, sonnet and fable aliases now resolve to models with a native 1M
window (Opus 5.5, Sonnet 5.5, Fable 5.1), so Claude Code documents the
"[1m]" suffix as having no effect on them and its own /model picker no
longer lists 1M rows. Remove Sonnet 1M / Opus 1M / Fable 1M from the
shared, Android and iOS catalogs.

Sessions stored with a "[1m]" value keep working: the composer keeps
the current model selectable, and the context-budget fallback treats any
"[1m]" model and the remaining aliases as 1M, so local-mode sessions on
plain opus/sonnet no longer show a 200k budget.

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

* fix(cli): seed a 1M turn-1 context window for bare Claude aliases

With the 1M presets gone, a session on plain fable/opus/sonnet must not
seed 200k before its first result: those aliases now resolve to models
with a native 1M window. Seed 1M when the selected model is a preset
alias; an explicit full id still seeds conservatively until its result
arrives.

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

* feat(claude): offer the four current Claude models as presets

The Claude presets now mirror the aliases Claude Code's /model lists, in
its order: Opus, Fable, Sonnet, Haiku. Each alias resolves to the newest
model of its family, and all four have a native 1M window, so neither
1M variants nor pinned older versions are offered. Haiku was missing.

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

* feat(claude): read the effective context window from get_context_usage

The 1M seed for the aliases is wrong for users who disable 1M
(CLAUDE_CODE_DISABLE_1M_CONTEXT) or cap it with autoCompactWindow. In
remote mode, ask Claude via the get_context_usage control request after
each system/init and use its maxTokens, which is what /context shows.
A result's raw contextWindow no longer overwrites that value. Older CLIs
that reject the request keep the existing seed.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: weishu <twsxtd@gmail.com>
2026-10-29 22:28:59 +08:00
weishu 01cdec1179 feat(android): share files and text to sessions 2026-10-29 21:17:19 +08:00
weishu 730dfcb22e Release version 0.33.2 2026-10-29 13:24:52 +08:00
weishu 4b3f92ea84 fix(android): keep session ordering timestamps consistently Long 2026-10-29 13:01:38 +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
6ccde08548 feat(android): render settings pickers as expressive choice dialogs (#2227)
* feat(android): anchor the Material type scale on the shared reading tokens

MaterialTheme was built with only a colorScheme, leaving HapiTypography's
body/code/caption tokens an island that never influenced Material
typography. Derive the app-wide Material type scale from those tokens so
Material widgets and direct token consumers share one type system: the
body token drives bodyLarge (the chat reading size) and the caption token
drives bodySmall (the metadata size), while every other slot keeps its
Material platform default so existing MaterialTheme.typography call sites
stay pixel-identical. Only size and leading come from the tokens; weight,
tracking and family stay Material's. HapiTheme now accepts a typography
argument (defaulting to the shared scale) and passes it through, and the
monospace code token stays a direct-consumer token because Material has
no code slot.

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

* feat(android): render settings pickers as expressive choice dialogs

The theme and language pickers were the last bare AlertDialog + radio-row
surfaces in the app: plain stacked radio buttons with no visual anchor for
what each option means, and two focus targets per row (the row and the
RadioButton each carried their own click handler). Give small exclusive
pickers a shared expressive form: each option renders as a full-width
choice card whose selected state is a secondaryContainer fill plus a
trailing check, and options can carry a fixed preview swatch.

The theme modes show two-tone palette chips — follow-system spans the
light and dark palettes, OLED is outlined pure black, dark uses the dark
canvas rather than black so the two stay distinguishable at a glance.
Swatch colors are fixed values depicting the mode they name, not derived
from the active scheme, mirroring how the web color-theme cards hardcode
their previews: a "light" chip must stay light even inside a dark dialog.

Rows keep radio semantics through Modifier.selectable with Role.RadioButton
inside a selectableGroup, so TalkBack still announces the standard
"selected, n of m" picker phrasing while each option becomes a single
focus target. Tapping an option still applies it immediately. Language
options skip the swatch slot; the component stays generic over the value.

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

---------

Co-authored-by: Claude Code <noreply@anthropic.com>
Co-authored-by: HAPI <noreply@hapi.run>
2026-10-29 10:57:55 +08:00
weishu a6cf8fd078 Release version 0.33.1 2026-10-29 10:13:33 +08:00
weishu ee273464f4 Release version 0.33.0 2026-10-29 08:51:11 +08:00
64f464190f feat(android): refactor settings into a hub with category subpages (#2218)
Replace the single-column settings screen with a category directory
whose rows come from a declarative catalog (SettingsHubCatalog: one
entry = route + title + summary + icon + visibility lambda), so adding
a category later (Display, Chat, Voice, Machines) is a list element,
not layout surgery. Mirrors the web settings IA at the category level.

- General subpage (settings/general) inherits the uncategorized
  sections: Appearance (theme + Material You), Language, About
  (versions + hub health). Zero behavior change per item.
- The owner-only Insights section dissolves into hub-level rows:
  Usage/Storage render only for the owner (same ns=default gate) and
  route to their existing dashboards, matching the web's owner-only
  nav categories (SettingsNav OWNER_ONLY_CATEGORIES).
- Flat settings/... route pattern with per-hub ViewModelHolder, same
  as the existing usage/storage precedents; back from a subpage lands
  on the hub. The hub's holder passes probeHealth=false so opening the
  hub itself fires no /health request; the General holder keeps it.
- Previews: hub owner/non-owner gating + General body. JVM tests pin
  catalog order, the owner gate, route uniqueness, and row summaries.
- Strings: settings_category_general(+_summary) in en/ru/zh; relocate
  three zh keys (chat_tool_resolved, chat_plan_implement,
  chat_plan_continue) that had drifted to the file tail and broke the
  three-language key order (KEYSYNC) — pure moves, values untouched.


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

Co-authored-by: Claude Code <noreply@anthropic.com>
Co-authored-by: HAPI <noreply@hapi.run>
2026-10-28 15:20:53 +08:00
261cb3bb6b test(android): pin composer attachment encode hop to the test scheduler (#2189)
The ComposerAttachments harness left the constructor default
encodeDispatcher (Dispatchers.Default, a real thread pool) in place.
With two uploads in flight the second-launched one can finish encoding
first, reach FakeUploadApi.uploadFile first, and consume the scripted
results deque out of launch order — the queued failure gets stolen by
the upload meant to succeed, so the robbed chip never settles to its
scripted state and items.first {} hangs until runTest's dispatch
timeout (UncompletedCoroutinesError, intermittent under suite load).

Pin the encode hop to the scope's own dispatcher (the runTest scheduler
in every test), making upload arrival order equal launch order
deterministically; upload still hops dispatchers as in production. Add
a regression-lock test scripting two results over two concurrent adds.

Product code is untouched: upload() keys results by attachment id and
is arrival-order independent — the ordering dependency lives purely in
the test fake's deque.


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

Co-authored-by: Claude Code <noreply@anthropic.com>
Co-authored-by: HAPI <noreply@hapi.run>
2026-10-28 12:13:08 +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
weishu 3a9b5eefd1 docs: point users to the App Store and Google Play apps
The native iOS and Android apps are now published. Replace the
build-from-source install path with store links, mark the native apps as
paid and the Web/PWA as the free client, and add store buttons to the
website hero. Source builds remain documented for developers.
2026-10-28 10:32:21 +08:00
weishu 85ccab1ac6 Release version 0.31.3 2026-10-27 14:23:48 +08:00
weishu 7bf0e5bc35 Release version 0.31.2 2026-10-27 08:32:28 +08:00
65c8dc727a feat(android): session list date-range filter with activity-aware picker (#2141)
* feat(android): add session date-range filter with activity-aware picker

- Pure SessionDateRangeFilter port of the web SessionList.tsx semantics:
  strict YYYY-MM-DD parsing with calendar round-trip validation,
  [start local midnight, end local midnight + 1 day) windows anchored to
  the injected zone, session.updatedAt as the activity field
- M3 DateRangePicker dialog (opens on the selected start's month, Clear
  resets without closing) plus a lightweight recent-activity legend —
  declared divergence: M3 exposes no per-day decoration API, so the web's
  in-cell "has session activity" emphasis surfaces beside the picker
  instead; explicit Apply replaces the web's close-after-second-click
- Wired as a transient, unpersisted lens (web useState parity) applied
  before the machine scope; the activity-day set and machine chips both
  derive from the unfiltered list so filtering never hides alternatives
- Top-bar calendar action with active dot badge, active-range summary
  row with clear, date-specific empty state; strings in en/zh-rCN/ru
  (en-ru key order KEYSYNC clean)
- JVM tests: predicate window edges, DST spring-forward, zone-anchored
  midnights, strict parsing, VM lens combination and pre-filter
  activity-day derivation

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

* fix(android): refresh session date filters after timezone changes

* Deduplicate Android sessions before applying date filters

---------

Co-authored-by: Claude Code <noreply@anthropic.com>
Co-authored-by: HAPI <noreply@hapi.run>
Co-authored-by: weishu <twsxtd@gmail.com>
2026-10-26 15:20:03 +08:00
b8675110b1 feat(android): add scratchlist row move, copy, and delete-confirm actions (#2132)
* feat(android): add scratchlist row move, copy, and delete-confirm actions

Align the scratchlist entry card with the web client's per-row actions
(ScratchlistPanel inventory):

- Move up/down: adjacent swap, disabled at the list edges, local-only
  like web v2 — no protocol request; the next refetch restores hub
  `createdAt DESC` order (pure `ScratchlistEntryOps.moveEntry`).
- Copy: copies only entry.text via ClipboardManager (FullTextAction
  precedent), swaps the icon to a check for 1500 ms on success, single
  per-list copied state, silent no-op on failure.
- Delete confirm: texts strictly longer than 100 characters (web
  SCRATCHLIST_CONFIRM_DELETE_THRESHOLD, untrimmed length) confirm via
  AlertDialog before delete, both from the row and the editor sheet;
  shorter notes delete directly.

New en/ru/zh-rCN strings ship in the same commit.

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

* fix(android): guard saved scratchlist notes and restart copy feedback

* test(android): uniquely select scratchlist confirmation actions

* Fix Android scratchlist editor test targeting

---------

Co-authored-by: Claude Code <noreply@anthropic.com>
Co-authored-by: HAPI <noreply@hapi.run>
Co-authored-by: weishu <twsxtd@gmail.com>
2026-10-26 15:14:17 +08:00
b3795f7aef feat(android): remember new-session launch defaults per machine and agent (#2138)
* feat(android): remember new-session launch defaults per machine and agent

Port of the web launch-preference persistence
(`hapi:newSession:launchSettings:v1:<machine>:<agent>`): model, effort,
reasoning effort and — for shared-permission-mode flavors only — the
permission mode of a successfully spawned session are remembered per
(machineId, agentFlavor) dimension and prefill the next create form for
that dimension (fresh screen, agent switch, machine switch).

- Storage: NewSessionPrefsData gains a nested `launchSettings` map inside
  the existing `hapi_prefs` DataStore blob; legacy blobs without the
  field decode to empty defaults, corrupt shapes keep the pre-existing
  whole-blob default degrade (DirectoryExpandPrefs contract).
- Capture happens at spawn success only (web saves on every
  create-success path); grok renders the native select but its mode is
  deliberately not remembered (web `usesSharedPermissionModeState`).
- Restore: resolvePreferredLaunchSettings validates claude's static model
  and effort lists, drops `xhigh` from opencode's reasoning efforts and
  coerces a removed permission mode to `default` — never a stale toggle;
  codex's dynamic model passes through for the existing settled-catalog
  reconciliation.
- A restored in-progress draft outranks the remembered defaults until the
  user switches agent or machine (web `preserveRestoredDraftRef`).
- Machine switches now also reset effort/reasoning effort/shared mode to
  the new dimension's remembered values (or sentinels), matching the web
  restore effect that re-runs on machineId change.

Divergences from web: no `cursorSelectedBase` (no Android picker yet);
agy's static model presets are not re-validated on Android (no v1 picker,
the stored value is always `auto`).

Tests: prefs round-trip/cold-start, legacy-blob tolerance, corrupt blob
and corrupt-entry fallback (real temp-file DataStore, pure JVM); capture
gating and resolve coercion logic; VM prefill per dimension, machine and
agent switching, spawn-success capture, and draft precedence.

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

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

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

* fix(android): preserve launch selections across machine heartbeats

---------

Co-authored-by: HAPI <noreply@hapi.run>
Co-authored-by: weishu <twsxtd@gmail.com>
2026-10-26 14:34:42 +08:00
5d4dc058a5 feat(android): permission input semantics parity (editor inputType, URL confirm, grok auto gate) (#2127)
* feat(android): settled permission verdict details

Port the web PermissionFooter verdict classification so a settled
permission row explains HOW a request was decided, not just that it was.

Behavior vs web (web/src/components/ToolCard/PermissionFooter.tsx:11-63):

- codex family sessions (flavor family + Codex/Gemini/OpenCode/Copilot/
  Cursor tool prefixes): approved + decision approved_for_session shows
  "Approved for session"; denied + decision abort shows "Aborted" with
  the wire reason appended when present.
- claude sessions: mode acceptEdits shows "Approved: allow all edits"
  (previously the raw mode id was appended); decision approved_for_session
  or an allowedTools hit — plain tool name or the `Bash(command)`
  qualifier built from the tool input command/cmd — shows
  "Approved for session" (previously indistinguishable from a plain
  approval).
- canceled now appends the wire reason (previously dropped).
- resolved/pending/denied unchanged in shape; denied keeps the error tone
  and aborted shares it.

Deliberate divergence: web's own settled footer renders only a minimal
red reason box (PermissionFooter.tsx:207-220) — its formatPermissionSummary
classification is unreachable there. Android renders the classified
verdict line in the existing subdued row style instead, as intended.

Implementation: new pure JVM classifier
feature/chat/permissions/PermissionVerdict.kt (exhaustive status ->
PermissionVerdictLabel mapping, unit-tested in PermissionVerdictTest);
ToolCallBlockView.PermissionStateRow becomes a thin renderer over it.
Strings added in en/zh-rCN/ru (ru mirrors en key order; zh values from
web locales).

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

* feat(android): persistent permission-mode chip + inactive gating

Item 2 — persistent permission-mode chip above the composer, mirroring
the web StatusBar mode span (StatusBar.tsx:253-265, 389-393):
- New PermissionModeChip composable + PermissionModeChipRule pure object:
  resolve the raw wire mode, keep it only when the flavor offers it, and
  hide `default` everywhere except copilot (model=auto sessions still
  surface the chip). Unknown wire ids hide (no raw fallback).
- Tone mapping follows the web PERMISSION_TONE_CLASSES (StatusBar.tsx:
  43-48): neutral=onSurfaceVariant, info=primary, warning=tertiary,
  danger=error. Labels stay en catalog strings — web mode labels are
  hardcoded English, not i18n.
- The chip consults the unfiltered flavor catalog: the concurrent-clients
  Safe-Yolo carve-out only narrows the switcher, never the chip.
- Deliberate divergence: the web span is not clickable (its switcher is
  the composer settings sheet / Shift+Tab). Android has no Shift+Tab, so
  tapping the chip opens the session config sheet that hosts the
  switcher — same surface as the top-bar gear, no new top-bar icon
  (ChatScreen keeps its two-icons-max rule).

Item 4 — permission-mode gating aligned with web (SessionChat.tsx:
2079, 2138-2163; HappyComposer.tsx:536-537, 1544-1549, 1960-1995):
- Inactive sessions stay switchable, matching web: SessionChat passes
  allowSendWhenInactive unconditionally, so configurationControlsDisabled
  never trips for inactive threads and the offline notice stays
  informational only (the send path auto-resumes).
- The permission section now disappears for copilot sessions under local
  terminal control — web withholds onPermissionModeChange there and the
  whole section then hides (HappyComposer.tsx:1595). Other flavors keep
  switching while terminal-controlled.
- ChatViewModel.setPermissionMode gains the matching belt-and-suspenders
  guard; the controlledByUser derivation is extracted into
  terminalControlled() (controlledByUser && !concurrentClients, same as
  SessionChat.tsx:996) and shared with buildConfigUi.

Tests: PermissionModeChipTest (6 cases — default/codex/copilot/pi/unknown/
safe-yolo), ChatViewModelInteractionTest +3 (withheld for terminal
copilot; survives terminal claude + shared-clients copilot; inactive
stays switchable). No new user-visible strings (chip label is catalog
en, contentDescription reuses chat_config_permission_mode).

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

* test(android): derive the dsh chip expectation from the flavor catalog

The hardcoded "dsh offers no modes" assumption breaks on trees where dsh
gained the codex-style mode list — derive the expectation from the
catalog so the test pins the actual contract (the chip mirrors the
flavor's offered list).

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

* feat(android): permission input semantics parity (editor inputType, URL confirm, grok auto gate)

Three behavior alignments with the web app's permission surfaces:

1. Editor inputType (web requestUserInput.ts:152-153, 191-195):
   a required text question with `inputType === 'editor'` is always
   answered and its note is pushed untrimmed (even the literal
   "user_note: "); non-editor notes stay trimmed and optional.
   Android: PermissionInputs.requestUserInputAnswerValues /
   isRequestUserInputAnswered now take the question, gate on the
   literal "editor" inputType; the footer's note field grows to
   minLines = 2 to invite multi-line answers.

2. request_user_input URL confirmation (web requestUserInput.ts:46-70,
   129-141; RequestUserInputFooter.tsx:142-152): the sign-in url only
   survives when it parses as http/https; submitting requires the
   CLI-synthesized __mcp_url_confirmation option (description === url)
   to be selected, and submission opens the url before approving.
   Android: parseRequestUserInput + normalizeRequestUserInputUrl +
   isRequestUserInputUrlConfirmed; the footer enforces the confirm
   guard, then opens the url via ACTION_VIEW. Divergences: the raw
   wire string is compared (the CLI synthesizes the description from
   the same value web would re-normalize, so they are equivalent for
   every CLI-produced request), and the open-failure message is
   platform-adapted (no popup blocker on Android).

3. Grok Auto permission gate (web NewSessionForm index.tsx + grok
   models probe): when the machine reports
   `autoPermissionModeSupported === false` for the chosen cwd, the Auto
   option is disabled with an " (unavailable)" suffix, a hint line is
   shown, and an existing Auto selection resets to Default. The gate is
   grok-only (claude and other agents never gate) and tri-state — null
   (unknown, probe pending or failed) never disables.
   Android: GrokModelsResponse wire type (nullable field),
   HapiApi.getMachineGrokModelsForCwd, NewSessionViewModel probes on
   agent/cwd/machine changes (deduped, cancellable, existence-gated),
   OptionItem gains `enabled`, the permission dropdown honors it and
   renders the hint. In-session switching stays ungated, matching web.

New strings in en / zh-rCN / ru (KEYSYNC: en and ru key order
identical).

Tests: RequestUserInputTest covers editor/untrimmed-note/url
parsing/confirmation semantics; NewSessionViewModelTest covers gate
disable+reset+hint, tri-state never-gate cases, and agent/cwd gating;
HapiApiTest covers the cwd query param and nullable decoding.

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

* fix(android): accept browser-valid sign-in URLs

---------

Co-authored-by: Claude Code <noreply@anthropic.com>
Co-authored-by: HAPI <noreply@hapi.run>
Co-authored-by: weishu <twsxtd@gmail.com>
2026-10-26 14:34:16 +08:00
b5b75f7e41 feat(android): session-list sidebar parity — dedup, stub hiding, relevance search (#2104)
* android/protocol: carry hasConversationContent through the session projection

The hub sends `hasConversationContent` on both `SessionSummary` and the
full `Session`; the sidebar's empty-stub hiding (web `SessionList.tsx`)
depends on it. Decode it on both wire models and project it through
`toSessionSummary` exactly like shared/src/sessionSummary.ts does
(`session.hasConversationContent ?? false`), so SSE upserts land with the
same signal REST lists already carry. Optional-with-default keeps older
hub payloads decodable.

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

* android: session-list sidebar parity — agent-session dedup, stub hiding, relevance search

Port the web sidebar's list body to the Android session list:

- Collapse duplicate views of one agent session (resume/reopen chains)
  into a single row — winner by active → selected → pin → recency —
  keyed on `flavor:agentSessionId` (web `getSessionDedupKey`).
- Hide fresh auto-created "new session" stubs: inactive, conversation-
  less, no title signal and no agent id (web `isSidebarEmptySessionStub`);
  selected/active/pinned rows are never hidden.
- Live search box with the web's relevance ranking (field weights,
  boundary + title-phrase bonuses, wildcard `*`/`?`, AND across terms)
  and a distinct no-results state. Pinned rows stay contiguous while
  searching.

Pure logic lives in `SessionListFilters` / `SessionListSearch` (Kotlin
objects, JVM-tested against the web reference cases); the ViewModel
pipeline is visible → dedup+stub → canonical sort → search filter+rank.

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

* android: ru translations for session search parity

Upstream landed values-ru after this stack's merge base; new strings
need ru entries or :app:lintDebug fails with MissingTranslation.

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

* fix(android): preserve global pins in session search

---------

Co-authored-by: Claude Code <noreply@anthropic.com>
Co-authored-by: HAPI <noreply@hapi.run>
Co-authored-by: weishu <twsxtd@gmail.com>
2026-10-26 14:33:59 +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
431c170f4a feat(android): persistent permission-mode chip + inactive gating (#2125)
* feat(android): settled permission verdict details

Port the web PermissionFooter verdict classification so a settled
permission row explains HOW a request was decided, not just that it was.

Behavior vs web (web/src/components/ToolCard/PermissionFooter.tsx:11-63):

- codex family sessions (flavor family + Codex/Gemini/OpenCode/Copilot/
  Cursor tool prefixes): approved + decision approved_for_session shows
  "Approved for session"; denied + decision abort shows "Aborted" with
  the wire reason appended when present.
- claude sessions: mode acceptEdits shows "Approved: allow all edits"
  (previously the raw mode id was appended); decision approved_for_session
  or an allowedTools hit — plain tool name or the `Bash(command)`
  qualifier built from the tool input command/cmd — shows
  "Approved for session" (previously indistinguishable from a plain
  approval).
- canceled now appends the wire reason (previously dropped).
- resolved/pending/denied unchanged in shape; denied keeps the error tone
  and aborted shares it.

Deliberate divergence: web's own settled footer renders only a minimal
red reason box (PermissionFooter.tsx:207-220) — its formatPermissionSummary
classification is unreachable there. Android renders the classified
verdict line in the existing subdued row style instead, as intended.

Implementation: new pure JVM classifier
feature/chat/permissions/PermissionVerdict.kt (exhaustive status ->
PermissionVerdictLabel mapping, unit-tested in PermissionVerdictTest);
ToolCallBlockView.PermissionStateRow becomes a thin renderer over it.
Strings added in en/zh-rCN/ru (ru mirrors en key order; zh values from
web locales).

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

* feat(android): persistent permission-mode chip + inactive gating

Item 2 — persistent permission-mode chip above the composer, mirroring
the web StatusBar mode span (StatusBar.tsx:253-265, 389-393):
- New PermissionModeChip composable + PermissionModeChipRule pure object:
  resolve the raw wire mode, keep it only when the flavor offers it, and
  hide `default` everywhere except copilot (model=auto sessions still
  surface the chip). Unknown wire ids hide (no raw fallback).
- Tone mapping follows the web PERMISSION_TONE_CLASSES (StatusBar.tsx:
  43-48): neutral=onSurfaceVariant, info=primary, warning=tertiary,
  danger=error. Labels stay en catalog strings — web mode labels are
  hardcoded English, not i18n.
- The chip consults the unfiltered flavor catalog: the concurrent-clients
  Safe-Yolo carve-out only narrows the switcher, never the chip.
- Deliberate divergence: the web span is not clickable (its switcher is
  the composer settings sheet / Shift+Tab). Android has no Shift+Tab, so
  tapping the chip opens the session config sheet that hosts the
  switcher — same surface as the top-bar gear, no new top-bar icon
  (ChatScreen keeps its two-icons-max rule).

Item 4 — permission-mode gating aligned with web (SessionChat.tsx:
2079, 2138-2163; HappyComposer.tsx:536-537, 1544-1549, 1960-1995):
- Inactive sessions stay switchable, matching web: SessionChat passes
  allowSendWhenInactive unconditionally, so configurationControlsDisabled
  never trips for inactive threads and the offline notice stays
  informational only (the send path auto-resumes).
- The permission section now disappears for copilot sessions under local
  terminal control — web withholds onPermissionModeChange there and the
  whole section then hides (HappyComposer.tsx:1595). Other flavors keep
  switching while terminal-controlled.
- ChatViewModel.setPermissionMode gains the matching belt-and-suspenders
  guard; the controlledByUser derivation is extracted into
  terminalControlled() (controlledByUser && !concurrentClients, same as
  SessionChat.tsx:996) and shared with buildConfigUi.

Tests: PermissionModeChipTest (6 cases — default/codex/copilot/pi/unknown/
safe-yolo), ChatViewModelInteractionTest +3 (withheld for terminal
copilot; survives terminal claude + shared-clients copilot; inactive
stays switchable). No new user-visible strings (chip label is catalog
en, contentDescription reuses chat_config_permission_mode).

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

* test(android): derive the dsh chip expectation from the flavor catalog

The hardcoded "dsh offers no modes" assumption breaks on trees where dsh
gained the codex-style mode list — derive the expectation from the
catalog so the test pins the actual contract (the chip mirrors the
flavor's offered list).

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

---------

Co-authored-by: Claude Code <noreply@anthropic.com>
Co-authored-by: HAPI <noreply@hapi.run>
2026-10-25 15:34:11 +08:00
4c87a0f1a2 feat(android): settled permission verdict details (#2123)
Port the web PermissionFooter verdict classification so a settled
permission row explains HOW a request was decided, not just that it was.

Behavior vs web (web/src/components/ToolCard/PermissionFooter.tsx:11-63):

- codex family sessions (flavor family + Codex/Gemini/OpenCode/Copilot/
  Cursor tool prefixes): approved + decision approved_for_session shows
  "Approved for session"; denied + decision abort shows "Aborted" with
  the wire reason appended when present.
- claude sessions: mode acceptEdits shows "Approved: allow all edits"
  (previously the raw mode id was appended); decision approved_for_session
  or an allowedTools hit — plain tool name or the `Bash(command)`
  qualifier built from the tool input command/cmd — shows
  "Approved for session" (previously indistinguishable from a plain
  approval).
- canceled now appends the wire reason (previously dropped).
- resolved/pending/denied unchanged in shape; denied keeps the error tone
  and aborted shares it.

Deliberate divergence: web's own settled footer renders only a minimal
red reason box (PermissionFooter.tsx:207-220) — its formatPermissionSummary
classification is unreachable there. Android renders the classified
verdict line in the existing subdued row style instead, as intended.

Implementation: new pure JVM classifier
feature/chat/permissions/PermissionVerdict.kt (exhaustive status ->
PermissionVerdictLabel mapping, unit-tested in PermissionVerdictTest);
ToolCallBlockView.PermissionStateRow becomes a thin renderer over it.
Strings added in en/zh-rCN/ru (ru mirrors en key order; zh values from
web locales).


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

Co-authored-by: Claude Code <noreply@anthropic.com>
Co-authored-by: HAPI <noreply@hapi.run>
2026-10-25 15:34:01 +08:00
99cd3d4172 feat(android): add Russian locale (#1963)
* feat(android): add Russian locale

Translate all 518 app strings into values-ru/strings.xml, register ru in
locales_config.xml, and add AppLanguage.RUSSIAN to the in-app language
picker.

* fix(android): use count-agnostic Russian forms for dynamic counts

The *_many strings used a genitive-plural noun after the number, which is
wrong for 2-4 (e.g. "2 инструментов"). The house style avoids <plurals>,
so phrase these labels with the noun first ("Инструменты: %1") instead:
tool groups, review findings, queued/new messages, agent steps, questions,
lines, chars, edits, usage requests, scratchlist count and attachment cap.

* fix(android): make the process-steps label count-safe

* fix(android): use a count-safe token label in the usage tooltip

---------

Co-authored-by: mrudy <mrudy@local>
2026-10-05 17:14:11 +08:00
weishu facb66ee03 Release version 0.30.7 2026-09-15 09:08:33 +08:00
weishu 6d44996a59 Release version 0.30.6 2026-09-14 15:05:49 +08:00
weishu 5280a3b165 fix(chat): dismiss Codex plan actions when continuing planning 2026-09-14 14:30:08 +08:00
weishu b7e892b99e fix(release): sync iOS and Android versions 2026-09-14 09:48:19 +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
weishu 0a9f15a9a7 fix: align request_user_input other answers across clients 2026-09-13 19:48:14 +08:00
weishu a6880fe592 fix(native): add Codex plan proposal actions
Add current-proposal decoding, dedicated implementation requests and composer-only continuation on iOS and Android. Keep pending/errors across row recycling, reject stale actions and prevent duplicate submissions.

Include native regression tests and localized plan controls.
2026-09-13 19:48:14 +08:00
AnanovoandGitHub 7e7b16767b fix(android): stabilize tool group inspection on API 36 (#1843) 2026-09-13 18:56:56 +08:00
weishu e0db9c6815 fix(notifications): distinguish user questions from permission requests 2026-09-13 12:41:38 +08:00
weishu 051dbbd7da docs: refresh native app guides and contracts 2026-09-13 10:43:59 +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 7fded0a137 fix(android): align home toolbar and compact code actions 2026-09-12 19:14:46 +08:00
weishu 6f4aacce6b feat(android): enable default push through official relay 2026-09-12 17:52:53 +08:00
weishu 32ca6f543a feat(android): improve native chat reading and navigation 2026-09-12 16:56:41 +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 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 f5e5bcfa10 fix(native): render question tool answers in details 2026-09-11 21:04:48 +08:00