Skip to content

fix(chat): show the agent chip loading instead of ignoring taps - #15

Merged
doublepi123 merged 2 commits into
mainfrom
fix/mode-chip-touch
Oct 17, 2026
Merged

doublepi123 merged 2 commits into
mainfrom
fix/mode-chip-touch

Conversation

@doublepi123

Copy link
Copy Markdown
Owner

Follow-up to #13: after dp5 the user still reported "the build button often doesn't respond".

Cause. A local OpenCode with plugins takes ~25s on its first GET /agent after a start. During that window the mode chip had no agents, and a tap only fired a silent background refresh. The chip looked dead on every app start or runtime restart.

Reproduced on an emulator with a proxy delaying the first /agent by 25s. Before the fix, 7 taps over the first 26s showed nothing. Against a healthy server every tap works (30/30 across the chip's icon, text, arrow and edges), so the touch target is not the problem.

Fix

  • RuntimeCatalogState.agentsLoading (surfaced as SettingsUiState.agentsLoading) is true while agents are being read and none is on screen. It goes false once they arrive or the retries give up.
  • The chip swaps its shield icon for a small spinner while loading. The spinner has a TalkBack description.
  • A tap always opens the menu. While loading it shows a disabled "Loading agents…". Once retries are over it shows "Couldn't load agents. Tap to retry.". Agents that arrive while the menu is open replace the placeholder.
  • Strings for both are added in all 8 locales.
  • Per review:
    • load()'s final write reads the agents inside the update, so a retry that lands between the fetch and the write is not overwritten (it also kept the spinner running).
    • A retry tap on a never-connected runtime marks loading synchronously, so repeated taps can't queue several reconnects.

Verified

  • Local: detekt spotlessCheck :app:testGithubDebugUnitTest :app:assembleGithubDebug, 1406 unit tests passing.
  • Emulator, 25s first /agent: every tap at 8–23s showed "Loading agents…", and from 28s the menu listed orchestrator/build/plan.
  • Emulator, 12 consecutive failures: the menu went Loading → "Couldn't load…". Tapping retry went back to Loading → the 3 agents once the server recovered.
  • Screenshots: spinner and disabled state as intended, stable chip size, nothing clipped.

Pre-PR review

Reviewed at bf92cd2: APPROVE. Follow-up commit 08bec3e applies its suggestions:

  • atomic final write
  • synchronous loading on retry
  • spinner semantics
  • zh-CN 智能体
  • test comment

It is verified locally (1406 tests) and on the emulator. Report:

判定: APPROVE

ブロッカー

なし

提案(非ブロッキング)

  • RuntimeCatalogRepository.kt:269-286 と :337-346:ごくまれな競合で、スピナーが回り続けることがあります。同じランタイムで refresh() している最中に、前から走っていた再試行 job が成功するケースです。
    • 順序:
      1. load() が :267 で mutableState.value を読む。
      2. job が withAgents(list) を書き込む。
      3. load() が :269 で state 全体を上書きし、agents が空、agentsLoading = true になる。
      4. job はまだ isActive なので、:286 の refreshAgents() が :329 で何も起動しない。
    • 結果:次の操作があるまでスピナーが回ったままになります。チップをタップすれば新しい job が起動して回復するので、固着は一時的です。
    • 上書き自体は前からある競合ですが、agentsLoading が加わったことで、見た目が「ずっと読み込み中」になりました。
    • 修正案::269 を mutableState.update { } にして、更新の時点で it.agentsFor(target) を見直す。または :286 の判定を job の結果とは別に行う。
  • RuntimeCatalogRepository.kt:318-322:接続に失敗した状態で失敗表示の項目やチップを連打すると、full refresh() がその回数だけ順番に積まれます。
    • 経緯:refresh() が起動した load() が mutex を取って agentsLoading = true にするまで、メニューは「Couldn't load…」のまま押せる状態です。この間に連打した分の load() が mutex の後ろに並び、それぞれが接続の再試行(約 15 秒)を最初から行います。
    • 修正案:refresh() を呼ぶ前に、同期的に mutableState.update { it.copy(agentsLoading = true) } をしておく。これで項目がすぐ無効になり、二重のタップも防げます。または full refresh の job を 1 本に絞る。
  • ChatHomeScreen.kt:2053-2058:スピナーを表示している間は、盾アイコンの contentDescription(cd_agent_mode)が消え、TalkBack はラベルの「build」しか読み上げません。Modifier.semantics { contentDescription = … } に既存の chat_agents_loading を割り当てるのが簡単です。
  • values-zh-rCN/strings.xml:481-482:「agent」が英語のまま残っています。同じファイルの既存の訳では「智能体」が使われているので(:449 の drawer_agents_title、:1040 など)、そちらに揃えることを推奨します。例:「正在加载智能体…」「无法加载智能体,点按重试。」(句点の有無も他の言語に揃える)。
  • RuntimeCatalogRepositoryTest.kt:415-417:runCurrent() では、load() の最初の /agent 読み込みと最後の書き込みまで終わっています。このとき true になっているのは「再試行が控えている」ことによるもので、コメントの「Before the first read lands」とは意味が違います。assert 自体は正しいですが、コメントを直すか、listAgents を suspend させて本当に読み込み前の状態を検証するとよいです。ランタイム切り替えや接続失敗で agentsLoading が false に戻ることを確かめるテストもあると、固着の退行を防げます。

チェック済み項目

  • shell を使えないため、git diff は実行していません。agentsLoading、chat_agents_* を grep し、該当箇所を直接読みました。detekt、spotless、テスト、エミュレータの結果は報告されたものに基づいています。
  • agentsLoading が固着する経路(見てほしい点 1):
    • ランタイムの切り替え:collectLatest(:93)が先に RuntimeCatalogState(runtime = target) で state を作り直すので、false に戻ります。切り替え前のランタイムの load() が cancel されたり、:178/:235 の early return で抜けたりしても、その後の state は新しいランタイムのものです。
    • 別ランタイムの job の cancel(:330):その job は書き込みに届かず、届いても :339/:342 の id 確認で弾かれます。
    • 接続失敗(:212-217):既定値の false に戻ります。
    • 再試行を使い切った場合::344 で false になります。
    • A→B→A と戻した場合:A 用の古い job が :329 でそのまま使われ、最後に書き込むので、固着しません。
    • 残る経路は、上の提案 1 の競合だけです(タップで回復します)。
  • enabled = !loading と onClick = onRefresh(見てほしい点 2):
    • 読み込み中は項目が無効で、onClick は呼ばれません。
    • 失敗表示の項目をタップすると、refreshAgents() が :332-334 で同期的に loading にします。メニューは開いたまま Loading 表示に変わり、agents が入れば同じメニューが 3 項目になります(expanded は閉じません)。
    • 接続失敗のときだけは、refresh() 経由なので Loading 表示になるのが遅れます(提案 2)。
  • i18n(見てほしい点 3):chat_agents_loading と chat_agents_load_failed が全 8 ロケール(values、ar、es、fr、ja、pt-rBR、ru、zh-rCN)にあることを grep で確認しました。英語の Couldn\'t はエスケープ済みで、他のロケールには未エスケープのアポストロフィはありません。Kotlin 側は stringResource を使っていて、ハードコードした文字列はありません。"build" のフォールバックは既存のコードです。
  • 受け渡し:AndCodeApp.kt:1075 → ChatHomeScreen.kt:186(既定値 false)→ :667 → ChatComposer:1302(既定値 false)→ ModeChip:1671/2016。SettingsUiState.agentsLoading(SettingsViewModel.kt:55/222)も既定値付きです。ViewModel の依存は増えていないので、DI の変更は不要です。CircularProgressIndicator は import 済みです(:79)。
  • Claude Code と Antigravity は PermissionModeChip を使うので影響を受けません。Codex は固定で agents を返すので、スピナーは出ません。
  • 前回指摘した点の解消を確認しました:agentsFailure で改行を空白に置き換えている(:361-364)、health == null かつ読み込み中なら再試行を起動しない(:318-322)。

A local OpenCode with plugins takes ~25s on its first GET /agent after a
start. During that time the mode chip had no agents and a tap only fired
a silent background refresh, so the chip looked dead on every app start or
runtime restart. Reproduced on an emulator with a proxy delaying the first
/agent by 25s: the first 7 taps (26s) showed nothing.

- RuntimeCatalogState.agentsLoading: true while agents are being read and
  none is on screen (load or the background retry), false once they arrive
  or the retries give up
- the chip swaps its shield for a small spinner while loading
- a tap always opens the menu: a disabled "Loading agents…" while loading,
  "Couldn't load agents. Tap to retry." once retries are over; agents that
  arrive while it is open replace the placeholder
- strings in all 8 locales
- load()'s final write reads the agents inside the update, so a retry that
  succeeded between its fetch and write is no longer overwritten with an
  empty list (which also left the spinner running)
- a retry tap on a never-connected runtime marks loading synchronously, so
  repeated taps cannot queue one full reconnect each
- the spinner carries the loading content description for TalkBack
- zh-CN uses 智能体 like the rest of the locale
@doublepi123
doublepi123 merged commit 8f0b8f9 into main Oct 17, 2026
6 checks passed
@doublepi123
doublepi123 deleted the fix/mode-chip-touch branch October 17, 2026 12:11
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant