Skip to content

fix: deliver chat events, end runs on time, and show real chat state#83

Merged
yuga-hashimoto merged 5 commits into
mainfrom
claude/chat-realtime-and-drawer-status
Jul 25, 2026
Merged

fix: deliver chat events, end runs on time, and show real chat state#83
yuga-hashimoto merged 5 commits into
mainfrom
claude/chat-realtime-and-drawer-status

Conversation

@yuga-hashimoto

@yuga-hashimoto yuga-hashimoto commented Jul 25, 2026

Copy link
Copy Markdown
Owner

Why

The previous round (#80, shipped in v0.3.7/v0.3.8) did not fix the reported symptoms. This time the
diagnosis started from the running app rather than from reading the code.

In a screenshot of a finished turn, the reply had arrived, yet "処理中…", "考え中…" and the stop
button were all still showing — and every chat in the drawer was on the grey idle dot, including the
one that was running.

isThinking is only ever cleared by updateStreamingMessage, which only runs on a
message.part.updated event. It was still set, so no stream event had reached the app at all;
the visible reply had come from the 3-second fallback poll. That pinned the real problem, and
pulling on it turned up four separate causes.

Root causes

1. The event stream never opened

RuntimeActivityRepository only opened the stream once the runtime reported
RuntimeState.Connected. But every REST call — sending a prompt, loading a transcript — works
regardless of that flag. A runtime whose state never reached Connected therefore looked
completely functional while silently delivering no events at all: no live reply, no drawer
activity, no idle notification.

The stream now follows the selected runtime and leans on its own retry/backoff for reconnection,
which is simpler and strictly more robust than gating on a second, separately-maintained flag.

2. Runs never ended

The response poll asked session(id).time.completed whether the turn was done. Sessions do not
carry a completion time — only assistant messages do — so it was always null, completion was
never detected, and the composer sat on the stop button showing "考え中" until the 120s timeout
expired. That is exactly the screenshot.

Completion is now read off the transcript: the newest message being a finished assistant turn,
requiring a reply that did not exist before this prompt so the previous turn's completion cannot
end the new one instantly.

3. Drawer state was event-only

activeSessionIds was populated purely from stream events, so with no events every chat stayed on
the idle dot. A chat now reports its own run state, and unread markers are persisted, so a chat
that finished while the app was closed is still unread on the next launch.

4. The markers were not what was asked for

State Before Now
対応中 static green dot loading spinner
対応完了(未読) hollow blue ring filled blue dot
対応完了(既読) grey dot grey dot (unchanged)

Also fixed — pre-existing on main, both blocking CI

  • The chat test suite could not run. ChatViewModel.init started an unbounded 30-second
    connection probe (while (isActive) { probe(); delay(30s) }, added in 212afdf). Under a virtual
    test clock that loop never ends, so advanceUntilIdle() spun forever and every chat test hung
    — verified by running ChatViewModelQuestionTest on clean main, where it is killed at a 420s
    timeout. This is a large part of why chat regressions kept shipping unnoticed. The probe is now
    opt-in and enabled by the app; the suite completes in ~15s again.
  • The quick input widget could not render: its layout declared an EditText, which
    RemoteViews cannot inflate (RemoteViewLayout lint error on main). It is a tap target that
    opens the input activity, so it is now a TextView.
  • Six locales were missing three strings (MissingTranslation lint error on main).

Testing

./gradlew :app:testDebugUnitTest :app:lintDebug245 tests pass, lint clean.

Before this branch, that same command fails on main: the test task hangs, and lint reports the two
errors above.

New coverage: the stream opens without a connected state; runtime-state churn never restarts it;
losing the runtime clears activity but keeps unread markers; a chat's own run drives the drawer;
unread markers survive a restart; and the wire-format decoding the live path depends on
(sessionID/messageID/partID, tool state, and assistant time.completed) is pinned against a
mock server.

Not verified on a device — no emulator in this environment.

claude added 2 commits July 25, 2026 07:18
The previous round of fixes shipped in v0.3.7/v0.3.8 and the symptoms
stayed. Digging into what actually happens at runtime turned up four
different causes, none of which the earlier work touched.

Events never reached the app. The stream only opened once a runtime
reported Connected, but every REST call — sending a prompt, loading a
transcript — works regardless of that flag. A runtime whose state never
reached Connected therefore looked completely functional while silently
delivering no events at all: no live reply, no drawer activity, no idle
notification. The stream now follows the selected runtime and relies on
its own retry, which is both simpler and strictly more robust.

Runs never ended. The response poll asked the session object for a
completion time, but sessions do not carry one — only assistant messages
do — so completion was never detected and the composer sat on the stop
button, still showing "thinking", until the two minute timeout expired.
Completion is now read off the transcript.

Drawer state was event-only. Active chats came purely from stream
events, so with no events every chat sat on the grey idle dot even while
plainly working. A chat now reports its own run, and unread markers are
persisted so a chat that finished while the app was closed is still
unread on the next launch.

Markers did not match what was asked for: working now shows a spinner
rather than a static green dot, and finished-but-unread shows a filled
blue dot rather than a hollow ring.

Also fixed, both pre-existing on main and both blocking CI:

- ChatViewModel started an unbounded 30s connection probe from init.
  Under a virtual test clock that loop never ends, so every chat test
  hung and the suite could not run — which is a large part of why chat
  regressions kept shipping. The probe is now opt-in and enabled by the
  app; the suite runs in seconds again.
- The quick input widget declared an EditText, which RemoteViews cannot
  inflate, so the widget could not render. It is a tap target that opens
  the input activity, so it is now a TextView.
- Six locales were missing three strings.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RLvEkDwiWT8R4S23gdPsSB
spotlessCheck flagged kotlinx.coroutines.Job as unused after the stream
lifecycle stopped tracking a job handle.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RLvEkDwiWT8R4S23gdPsSB
@github-actions

Copy link
Copy Markdown
Contributor

API 34 emulator screenshot failure

Diagnostic logs were uploaded as the emulator-ui-diagnostics-30149195111 workflow artifact.

> Task :app:writeDebugSigningConfigVersions
> Task :app:preDebugAndroidTestBuild SKIPPED

> Task :app:stripDebugDebugSymbols
Unable to strip the following libraries, packaging them as they are: libandroid-shmem.so, libandroidx.graphics.path.so, libopencode_android_proot.so, libopencode_android_proot_loader.so, libopencode_android_proot_loader32.so, libtalloc.so, libtensorflowlite_jni.so.

> Task :app:processDebugAndroidTestManifest
> Task :app:generateDebugAndroidTestBuildConfig
> Task :app:checkDebugAndroidTestAarMetadata
> Task :app:generateDebugAndroidTestResValues
> Task :app:mapDebugAndroidTestSourceSetPaths
> Task :app:generateDebugAndroidTestResources
> Task :app:javaPreCompileDebugAndroidTest
> Task :app:mergeDebugAndroidTestShaders
> Task :app:compileDebugAndroidTestShaders NO-SOURCE
> Task :app:generateDebugAndroidTestAssets UP-TO-DATE
> Task :app:mergeDebugAndroidTestAssets
> Task :app:compressDebugAndroidTestAssets
> Task :app:desugarDebugAndroidTestFileDependencies
> Task :app:mergeDebugAndroidTestResources
> Task :app:checkDebugAndroidTestDuplicateClasses
> Task :app:processDebugAndroidTestResources
> Task :app:mergeExtDexDebugAndroidTest
> Task :app:mergeLibDexDebugAndroidTest
> Task :app:mergeDebugAndroidTestJniLibFolders
> Task :app:mergeDebugAndroidTestNativeLibs NO-SOURCE
> Task :app:stripDebugAndroidTestDebugSymbols NO-SOURCE
> Task :app:validateSigningDebugAndroidTest
> Task :app:writeDebugAndroidTestSigningConfigVersions
> Task :app:compileDebugKotlin
w: file:///home/runner/work/opencode-android/opencode-android/app/src/main/java/com/opencode/android/feature/chat/LiveTranscriptOverlay.kt:107:44 'val Icons.Filled.Send: ImageVector' is deprecated. Use the AutoMirrored version at Icons.AutoMirrored.Filled.Send.
w: file:///home/runner/work/opencode-android/opencode-android/app/src/main/java/com/opencode/android/feature/chat/OpenCodeChatScreen.kt:678:5 'fun ClickableText(text: AnnotatedString, modifier: Modifier = ..., style: TextStyle = ..., softWrap: Boolean = ..., overflow: TextOverflow = ..., maxLines: Int = ..., onTextLayout: (TextLayoutResult) -> Unit = ..., onClick: (Int) -> Unit): Unit' is deprecated. Use Text or BasicText and pass an AnnotatedString that contains a LinkAnnotation.
w: file:///home/runner/work/opencode-android/opencode-android/app/src/main/java/com/opencode/android/feature/search/CommandPalette.kt:103:46 'val Icons.Filled.Chat: ImageVector' is deprecated. Use the AutoMirrored version at Icons.AutoMirrored.Filled.Chat.
w: file:///home/runner/work/opencode-android/opencode-android/app/src/main/java/com/opencode/android/feature/settings/SettingsScreen.kt:132:34 'fun Modifier.menuAnchor(): Modifier' is deprecated. Use overload that takes MenuAnchorType and enabled parameters.
w: file:///home/runner/work/opencode-android/opencode-android/app/src/main/java/com/opencode/android/feature/settings/SettingsScreenV2.kt:191:42 'val Icons.Filled.Chat: ImageVector' is deprecated. Use the AutoMirrored version at Icons.AutoMirrored.Filled.Chat.
w: file:///home/runner/work/opencode-android/opencode-android/app/src/main/java/com/opencode/android/feature/settings/SettingsScreenV2.kt:207:42 'val Icons.Filled.Chat: ImageVector' is deprecated. Use the AutoMirrored version at Icons.AutoMirrored.Filled.Chat.
w: file:///home/runner/work/opencode-android/opencode-android/app/src/main/java/com/opencode/android/feature/settings/SettingsScreenV2.kt:214:42 'val Icons.Filled.Chat: ImageVector' is deprecated. Use the AutoMirrored version at Icons.AutoMirrored.Filled.Chat.
w: file:///home/runner/work/opencode-android/opencode-android/app/src/main/java/com/opencode/android/ui/components/FileTypeIcon.kt:82:52 'val Icons.Filled.InsertDriveFile: ImageVector' is deprecated. Use the AutoMirrored version at Icons.AutoMirrored.Filled.InsertDriveFile.
w: file:///home/runner/work/opencode-android/opencode-android/app/src/main/java/com/opencode/android/ui/theme/Theme.kt:145:20 'var statusBarColor: Int' is deprecated. Deprecated in Java.
w: file:///home/runner/work/opencode-android/opencode-android/app/src/main/java/com/opencode/android/ui/theme/Theme.kt:146:20 'var navigationBarColor: Int' is deprecated. Deprecated in Java.

> Task :app:compileDebugJavaWithJavac
> Task :app:dexBuilderDebug
> Task :app:mergeDebugGlobalSynthetics
> Task :app:processDebugJavaRes
> Task :app:bundleDebugClassesToCompileJar
> Task :app:mergeProjectDexDebug
> Task :app:mergeDebugJavaResource
> Task :app:kspDebugAndroidTestKotlin
> Task :app:packageDebug
> Task :app:createDebugApkListingFileRedirect
> Task :app:assembleDebug

> Task :app:compileDebugAndroidTestKotlin FAILED
e: file:///home/runner/work/opencode-android/opencode-android/app/src/androidTest/java/com/opencode/android/ui/UiScreenshotInstrumentedTest.kt:139:29 No value passed for parameter 'workspaces'.
e: file:///home/runner/work/opencode-android/opencode-android/app/src/androidTest/java/com/opencode/android/ui/UiScreenshotInstrumentedTest.kt:139:29 No value passed for parameter 'selectedWorkspacePath'.
e: file:///home/runner/work/opencode-android/opencode-android/app/src/androidTest/java/com/opencode/android/ui/UiScreenshotInstrumentedTest.kt:139:29 No value passed for parameter 'onSelectProject'.
e: file:///home/runner/work/opencode-android/opencode-android/app/src/androidTest/java/com/opencode/android/ui/UiScreenshotInstrumentedTest.kt:276:17 No value passed for parameter 'wakeWordEnabled'.
e: file:///home/runner/work/opencode-android/opencode-android/app/src/androidTest/java/com/opencode/android/ui/UiScreenshotInstrumentedTest.kt:276:17 No value passed for parameter 'onWakeWordChange'.
gradle/actions: Writing build results to /home/runner/work/_temp/.gradle-actions/build-results/emulator-1784964315195.json

FAILURE: Build failed with an exception.

* What went wrong:
Execution failed for task ':app:compileDebugAndroidTestKotlin'.
> A failure occurred while executing org.jetbrains.kotlin.compilerRunner.GradleCompilerRunnerWithWorkers$GradleKotlinCompilerWorkAction
   > Compilation error. See log for more details

* Try:
> Run with --stacktrace option to get the stack trace.
> Run with --info or --debug option to get more log output.
> Run with --scan to get full insights.
> Get more help at https://help.gradle.org.

BUILD FAILED in 16m 43s
63 actionable tasks: 63 executed
OK: killing emulator, bye bye

OK

claude added 3 commits July 25, 2026 07:43
…ures

The androidTest source set is only compiled by the emulator capture job,
so it silently rotted when AppDrawerContent gained project selection and
VoiceSettingsScreen gained the wake-word toggle. The capture job has been
failing to compile on main since; this restores it and exercises the new
running and unread drawer markers while it is there.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RLvEkDwiWT8R4S23gdPsSB
The jacoco plugin only attaches a report task to the JVM `test` task,
which an Android module does not have, so `:app:jacocoTestReport` never
existed and CI's coverage step failed on every run. Registers a report
over the debug unit tests, writing where the workflow expects it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RLvEkDwiWT8R4S23gdPsSB
Scanning the whole build directory for exec files made Gradle treat every
other task's output as an undeclared input, so the report failed as soon
as a release variant was assembled in the same invocation — which is
exactly what CI does.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RLvEkDwiWT8R4S23gdPsSB
@yuga-hashimoto
yuga-hashimoto merged commit 75f4628 into main Jul 25, 2026
3 of 4 checks passed
@github-actions

Copy link
Copy Markdown
Contributor

API 34 emulator screenshot failure

Diagnostic logs were uploaded as the emulator-ui-diagnostics-30150200854 workflow artifact.

INSTRUMENTATION_STATUS: class=com.opencode.android.ui.UiScreenshotInstrumentedTest
INSTRUMENTATION_STATUS: current=1
INSTRUMENTATION_STATUS: id=AndroidJUnitRunner
INSTRUMENTATION_STATUS: numtests=1
INSTRUMENTATION_STATUS: stream=
com.opencode.android.ui.UiScreenshotInstrumentedTest:
INSTRUMENTATION_STATUS: test=captureReviewedScreens
INSTRUMENTATION_STATUS_CODE: 1
INSTRUMENTATION_STATUS: class=com.opencode.android.ui.UiScreenshotInstrumentedTest
INSTRUMENTATION_STATUS: current=1
INSTRUMENTATION_STATUS: id=AndroidJUnitRunner
INSTRUMENTATION_STATUS: numtests=1
INSTRUMENTATION_STATUS: stack=java.lang.AssertionError: Assert failed: The component is not displayed!
	at androidx.compose.ui.test.AssertionsKt.assertIsDisplayed(Assertions.kt:34)
	at com.opencode.android.ui.UiScreenshotInstrumentedTest.captureReviewedScreens$lambda$0(UiScreenshotInstrumentedTest.kt:103)
	at com.opencode.android.ui.UiScreenshotInstrumentedTest.$r8$lambda$Rtp34o0V-2Q6qCYNPZSXovrWpP8(Unknown Source:0)
	at com.opencode.android.ui.UiScreenshotInstrumentedTest$$ExternalSyntheticLambda7.invoke(D8$$SyntheticClass:0)
	at com.opencode.android.ui.UiScreenshotInstrumentedTest.capture(UiScreenshotInstrumentedTest.kt:366)
	at com.opencode.android.ui.UiScreenshotInstrumentedTest.captureReviewedScreens(UiScreenshotInstrumentedTest.kt:100)

INSTRUMENTATION_STATUS: stream=
Error in captureReviewedScreens(com.opencode.android.ui.UiScreenshotInstrumentedTest):
java.lang.AssertionError: Assert failed: The component is not displayed!
	at androidx.compose.ui.test.AssertionsKt.assertIsDisplayed(Assertions.kt:34)
	at com.opencode.android.ui.UiScreenshotInstrumentedTest.captureReviewedScreens$lambda$0(UiScreenshotInstrumentedTest.kt:103)
	at com.opencode.android.ui.UiScreenshotInstrumentedTest.$r8$lambda$Rtp34o0V-2Q6qCYNPZSXovrWpP8(Unknown Source:0)
	at com.opencode.android.ui.UiScreenshotInstrumentedTest$$ExternalSyntheticLambda7.invoke(D8$$SyntheticClass:0)
	at com.opencode.android.ui.UiScreenshotInstrumentedTest.capture(UiScreenshotInstrumentedTest.kt:366)
	at com.opencode.android.ui.UiScreenshotInstrumentedTest.captureReviewedScreens(UiScreenshotInstrumentedTest.kt:100)

INSTRUMENTATION_STATUS: test=captureReviewedScreens
INSTRUMENTATION_STATUS_CODE: -2
INSTRUMENTATION_RESULT: stream=

Time: 3.289
There was 1 failure:
1) captureReviewedScreens(com.opencode.android.ui.UiScreenshotInstrumentedTest)
java.lang.AssertionError: Assert failed: The component is not displayed!
	at androidx.compose.ui.test.AssertionsKt.assertIsDisplayed(Assertions.kt:34)
	at com.opencode.android.ui.UiScreenshotInstrumentedTest.captureReviewedScreens$lambda$0(UiScreenshotInstrumentedTest.kt:103)
	at com.opencode.android.ui.UiScreenshotInstrumentedTest.$r8$lambda$Rtp34o0V-2Q6qCYNPZSXovrWpP8(Unknown Source:0)
	at com.opencode.android.ui.UiScreenshotInstrumentedTest$$ExternalSyntheticLambda7.invoke(D8$$SyntheticClass:0)
	at com.opencode.android.ui.UiScreenshotInstrumentedTest.capture(UiScreenshotInstrumentedTest.kt:366)
	at com.opencode.android.ui.UiScreenshotInstrumentedTest.captureReviewedScreens(UiScreenshotInstrumentedTest.kt:100)

FAILURES!!!
Tests run: 1,  Failures: 1


INSTRUMENTATION_CODE: -1

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.

2 participants