feat(android): wire the dead code, render the git panel, make the token gate real
Four agents' worth of :app wiring. Everything here existed as tested code with
zero production call sites, or as a screen rendering a shape the server stopped
sending months ago.
Dead code that shipped as features. All three were fully implemented and
unit-tested while being reachable from nothing:
- Session-list preview thumbnails. SessionsHome called SessionListScreen
without the `thumbnails` argument, so the `= null` default won and every
row rendered a placeholder forever — on the home session chooser, whose
entire purpose is seeing what each session is doing.
- Quick-reply chips and their palette editor.
- PointerContextMenu (large-screen secondary click).
ThumbnailPipeline's formal cross-review had been explicitly skipped when it was
written — PROGRESS_ANDROID.md said so in as many words, and it is concurrency
code, so nothing had ever reviewed OR executed it. It has now been reviewed and
kept: the LruCache key, the fair Semaphore(2), the Mutex-guarded in-flight
dedup and the NonCancellable in-flight release are all sound, and a retained
map entry would have poisoned a key forever.
The v0.6 project-detail git panel. Android rendered the pre-w6 shape: one bare
dirty dot, a bare hash+subject commit row, and a worktree heading that renamed
itself to "Branch" at n=1 — the exact behaviour G5 removed. Now the sync band,
unpushed marking with a single upstream boundary, and "Worktrees (n)". The rule
that drives it is enforced, not documented: exactly one state may render green,
and no-upstream is not that state.
Remote approvals are no longer blind. status.preview reaches GateBanner and
PlanGateSheet, so the user can see the command or diff they are approving
instead of a tool name. Absent preview stays an ordinary gate, not an error.
The WEBTERM_TOKEN gate is no longer inert. NetworkModule was calling
OkHttpClientFactory.create(identityProvider) with no cookieJar, so the shared
client ran on CookieJar.NO_COOKIES and none of the cookie plumbing did
anything. It now installs the real AuthCookieJar — one client, so the cookie
rides the WS upgrade too — backed by the Tink-sealed store, and binds
InMemoryAuthCookieStore if a cipher cannot be constructed rather than ever
persisting a shell credential in the clear. The persister runs on an IO scope
because OkHttp calls a CookieJar synchronously and must never block on
DataStore or Tink.
And the user-facing half that made the gate unusable: a token-gated host
answers the pairing probe's unauthenticated GET with 401, which the frozen
taxonomy could only report as "this port is running something else" — so such
a host was impossible to pair AND the copy was misleading. Pairing now routes
to a token prompt and resumes through the same cert-gate choke point. The
token is a parameter, never a field and never part of any UI state.
Docs corrected rather than left aspirational: the plan asserted a cleartext
allowlist "permitted only for private LAN/Tailscale CIDRs", which the platform
cannot express at all — the network-security-config format has no netmask or
prefix syntax. PROGRESS_ANDROID.md now opens with what "COMPLETE" actually
meant, and PROGRESS_LOG.md records the whole pass including what is still
undone: A34/A35 have no code, and device QA remains ~0%.
Verified: ./gradlew test :app:assembleDebug :app:testDebugUnitTest koverVerify
-> BUILD SUCCESSFUL, 893 JVM tests, 0 failures (baseline was 617). The :app
run was re-measured with outputs deleted and --no-build-cache, and the APK
re-packaged, so Hilt graph validity is an observation rather than an
up-to-date claim.
This commit is contained in:
@@ -24,6 +24,32 @@
|
||||
|
||||
> 新会话读到的第一块。保持准确,只描述"此刻"。
|
||||
|
||||
### 🤖 [~] Android 客户端"能装但不能用"修复 — blocker 已清,设备验证未做(2026-07-30,worktree `android-blocker-fixes`)
|
||||
|
||||
- **起因**: 例行问"android 客户端完成状况如何"。`android/PROGRESS_ANDROID.md` 写着 **"✅ ANDROID CLIENT COMPLETE — all 36 plan tasks landed"**,617 个 JVM 测试全绿,APK 也出得来。**这个"完成"只在编译层面成立** —— 从来没在任何真机或模拟器上跑过,而一旦跑,第一个动作就崩。审计(45 agent,含逐条对抗验证)+ 修复(多波 agent)已把 blocker 清掉。
|
||||
- **为什么 JVM 测试全绿也漏掉了**: 一个结构性原因 —— **没有任何 JVM 测试会实例化 Android `View`**。`:terminal-view` 那 17 个测试测的是纯逻辑,渲染/输入/触摸整条路径无人触碰。
|
||||
- **三个让 App 在真机上不可用的缺陷**(每一条都反编译 `terminal-view-v0.118.0.aar` 逐偏移确认):
|
||||
1. **按任何键必崩**。`RemoteTerminalView` 只绑了 emulator,从没调用 `setTerminalViewClient` —— KDoc 写着"A17 会装",A17 实际只交付了 `HardwareKeyRouter` 和 KeyBar(而 KeyBar 之所以能用,恰恰因为它绕过了 view)。stock `TerminalView` 在 `onKeyDown` offset 82、`onCreateInputConnection` offset 0、`onKeyUp`、`onKeyPreIme` 无判空解引用 `mClient`。**装个 client 还不够**:返回 false 会继续走到 offset 150 的 `mTermSession.write()`,而 `mTermSession` 永远是 null(`TerminalSession` 是 final,而 fork 进程正是本 App 绝不能做的事,§6.1)。修法:client 自己消费按键,且 `KeyRouting` **刻意没有"交还 stock"这个分支** —— 崩溃变成不可表达,而不只是被绕开。键表仍由 Termux `KeyHandler` 决定,DECCKM 照旧发 `ESC O A`。
|
||||
2. **resize 从来不发**。`RemoteTerminalSession.updateSize` 零调用点,stock 兜底也是死的(`TerminalView.updateSize` 无 session 时 offset 18-25 直接 return),PTY 一直停在服务端的 80×24,所有全屏 TUI 都排版进错误的框。`TerminalResizeDriver` 现在从真实字距驱动 —— 字距走**公开** accessor `TerminalRenderer.getFontWidth()`/`getFontLineSpacing()`(agent 自己 `javap` 发现它们是 public),**不用反射**(R8 下会静默失效)也**不重测 Paint**(会与渲染器实际所用漂移,正是 §R5 风险)。
|
||||
3. **局域网连不上**。`network_security_config.xml` 是自己标着 STUB 的全局禁明文,而 `HostEndpoint` 接受 `http` 并派生 `ws://`。它注释里承诺的 CIDR 白名单**平台根本不支持**(NSC 格式没有任何 netmask/prefix 属性,`<domain>` 只装一个主机名或一个 IP 字面量)。改为 base-config 放开 + `*.terminal.yaojia.wang` 显式禁明文 + 信任锚钉死 system + `OkHttpClientFactory` 在传输层拒绝向非私有地址发起明文 —— 平台表达不了的收窄,放到 HTTP 客户端里做。plan §6.9/§8 的错误描述已一并更正。
|
||||
- **对抗评审又挖出三个,其中一条评审自己判错了**:
|
||||
- **在 alternate-screen TUI 里滑动滚屏仍会崩**。触摸路径完全绕过 `TerminalViewClient`:`doScroll` 在 alternate buffer 激活时把滚动转成 `handleKeyCode`,而那里解引用 `mTermSession`。**Claude Code 本身就是 alternate-screen TUI**,所以这是主界面正常使用中的崩溃。`TerminalScrollGesture` 抢占纵向拖动并复现 `doScroll` 的三个分支,连 fling runnable 和 `onGenericMotionEvent` 一并封死。
|
||||
- **`autofill()` 无判空解引用 `mTermSession`**,而该 view 自称可自动填充 → 任何密码管理器一填就崩。整个子树已排除出 autofill 结构。
|
||||
- 评审要求**恢复** stock 文字选择(此前被 focus 改造弄成不可达)。**这条它错了**:ActionMode 自己的 Copy/Paste 处理器也解引用空 session(`TextSelectionCursorController$1.onActionItemClicked` offsets 89-100 / 126-136),恢复可达性只会把崩溃搬到用户点 Copy 那一刻 —— 比没有选择功能更糟。**长按保持被消费,复制功能如实记为功能缺口**(真正的修法是基于 `TerminalBuffer` + `ClipboardManager` 自建选择层)。
|
||||
- **被静默丢弃的服务端数据,现已解码**: W2 `queue` 帧在 `ServerMessageType` 里没有成员,`decodeServer` 返回 null,`SessionEngine` 当"无法解码"丢掉 —— "N queued"徽标在 Android 上**从来不可能出现**;W1 `status.preview` 完全没建模,导致远程审批是**盲批**(只看到工具名,看不到要执行的命令/diff)。两者均已解码。一条规矩用测试钉住:**preview 坏掉绝不能连帧一起丢** —— `pending` 才是让 gate 出现的信号。
|
||||
- **当作功能发布的死代码**: `ThumbnailPipeline`(会话列表预览)、`QuickReply`(芯片+调色板)、`PointerContextMenu` 三者实现完整、单测齐全、**生产代码零调用点**。均已接线。`ThumbnailPipeline` 当年被**明确跳过**的交叉评审(AW3 条目自己承认)这次补做了。
|
||||
- **凭据处理**: `WEBTERM_TOKEN` cookie 现在在 REST 和 WS upgrade 上都携带,用 Tink AEAD + AndroidKeyStore 封装,**fail-closed**(封装失败就不持久化,绝不退回明文);`allowBackup=false` 阻止这个 30 天 shell 凭据经 Google Drive / D2D 外泄。
|
||||
- **v0.6 git 面板数据层**: 服务端 8fe1f52 之后的字段 Android 全没解码。已补 `SyncState`/`dirtyCount`/`cwd`/`unpushed`/`upstream` + `GET /projects/worktree/state` + `POST /projects/git/fetch`。两条易错语义**编码成行为**而非靠调用点自觉:`SyncState.isFullySynced(now)` 是唯一被认可的"能否显示绿色"判断(任何未知即 false,**无 upstream ≠ 0**);`GitLogResult.upstreamBoundaryIndex` 找**最后一个**未推送提交(取前 N 行会朝危险方向出错)。
|
||||
- **验证**: `./gradlew test :app:assembleDebug :app:testDebugUnitTest koverVerify` —— blocker 修复提交时 **757 个 JVM 测试全绿**(基线 617);`:wire-protocol`/`:session-core`/`:api-client` 三处 koverVerify 均过。commit `3e5cfdc`、`3fe719f`、`35d32f4`。
|
||||
- **方法与其代价(如实记录)**: 分波多 agent(TDD builder → 对抗评审 → 修复)。**API 端持续 529 过载,13 个 agent 里 8 个、11 个里 9 个被打掉**;协议层和 REST 层两轮全灭后由 orchestrator 亲自补写。这也是为什么会话成本冲到 ~$360。对抗评审确实值 —— 滑动滚屏那个 CRITICAL 崩溃是 builder 漏掉、reviewer 抓到的。
|
||||
- **仍未完成(不要把上面读成已完成)**:
|
||||
- **A34 / A35 完全没有代码** —— `:app` 连 androidTest 源集都没有,也没有 benchmark 模块。AW6 条目把它们标成 `[x]` 是因为它们被转成了清单条目,不是实现。
|
||||
- **设备 QA 仍约 0%** —— 仓库里唯一的真机证据是一张配对页的模拟器截图。上面每一条修复都只有 JVM 级验证。
|
||||
- release 签名/版本号/minify;S2 FCM 真机投递;复制功能。
|
||||
- `android/PROGRESS_ANDROID.md` 顶部已加"CORRECTION"段落说明上述全部。
|
||||
- **下一步**: 模拟器(AVD `webterm`,已启动)上跑 instrumented 回归 —— 尤其是这次修掉的三个崩溃,那正是 JVM 测试抓不到、也正是本次修复存在的理由。
|
||||
|
||||
|
||||
### 🌿 [x] w6 项目详情 Git 面板 — G1–G7 全部完成(2026-07-29,worktree `project-detail-git-mockup`)
|
||||
|
||||
- **动机**: 详情页只有一个光秃秃的 `●`,回答不了"有没有 commit 没 push / 现在在哪个 worktree"。设计稿 `docs/mockups/project-detail-git.html`,计划 `docs/plans/w6-project-git-panel.md`。
|
||||
|
||||
Reference in New Issue
Block a user