* 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
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.
* 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>
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.
* 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>
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>
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>
* 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>
* 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>
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.
* 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>
* 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>
* 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>
* 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>
* 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>
* 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>
* 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>
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>
* 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>
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.
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.
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.
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.