Files
hapi/android/core/data
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
..