feat(app): show assistant response timer in the composer #336

Merged
rwachters merged 3 commits from feat/assistant-response-timer into master 2026-10-01 18:42:12 +00:00
rwachters commented 2026-10-01 18:40:40 +00:00 (Migrated from github.com)

Summary

Adds a non-interactive mm:ss elapsed-time indicator to the chat composer action row, immediately left of the send, pause and stop button, in both composer instances. The measurement starts when a turn enters RUNNING, keeps counting through PAUSING and STOPPING, freezes at the final value on IDLE, and stays visible until the next message is sent. The change is client-only: no server, worker, shared-model, protocol or database change.

Changes

  • Added AssistantResponseTimerState (hidden, running, frozen) and derived it solely from turn-state transitions inside ChatStateImpl.setTurnExecutionState, so the measurement can never disagree with the turn state machine.
  • Cleared the measurement in resetState so a reused session slot cannot display another session's value.
  • Exposed the flow on ChatState and ChatViewModel.
  • Added formatAssistantResponseDuration, rendering a turn duration as a zero-padded mm:ss value with unbounded minutes and a zero clamp for negative durations.
  • Added the AssistantResponseTimer composable, which ticks once per second from the turn start instant while a turn is active and renders the final value afterwards, dimmed to read as inactive.
  • Placed the timer left of the action button in both InputArea instances and threaded the state through ChatScreen, ChatAreaState, ChatArea and MessageList.

Implementation notes

  • The timer is a sealed state rather than a timestamp plus a boolean, so live and frozen values are mutually exclusive and observers never have to decide precedence.
  • The live value is recomputed from the start instant on every tick instead of being incremented, so a delayed or coalesced tick cannot accumulate drift.
  • Repeated RUNNING writes keep the original start instant, and repeated IDLE writes cannot overwrite an already frozen value.

Validation

  • ./gradlew :app:desktopTest --offline — PASS, 0 failures, 25 new tests across 4 new test classes.
  • ./gradlew :app:desktopMainClasses --offline — PASS.
  • ./gradlew :app:wasmJsMainClasses --offline — PASS.
  • ./gradlew :app:compileAndroidMain cannot run in this environment (missing Android SDK path in local.properties); pre-existing and unrelated.

Risks and follow-up

  • Timer values are in-memory per session and are intentionally not persisted or restored; reopening a session shows no timer until the next turn starts.
  • The two low-priority review findings that were deferred by explicit decision are out of scope for this pull request.
## Summary Adds a non-interactive `mm:ss` elapsed-time indicator to the chat composer action row, immediately left of the send, pause and stop button, in both composer instances. The measurement starts when a turn enters `RUNNING`, keeps counting through `PAUSING` and `STOPPING`, freezes at the final value on `IDLE`, and stays visible until the next message is sent. The change is client-only: no server, worker, shared-model, protocol or database change. ## Changes - Added `AssistantResponseTimerState` (hidden, running, frozen) and derived it solely from turn-state transitions inside `ChatStateImpl.setTurnExecutionState`, so the measurement can never disagree with the turn state machine. - Cleared the measurement in `resetState` so a reused session slot cannot display another session's value. - Exposed the flow on `ChatState` and `ChatViewModel`. - Added `formatAssistantResponseDuration`, rendering a turn duration as a zero-padded `mm:ss` value with unbounded minutes and a zero clamp for negative durations. - Added the `AssistantResponseTimer` composable, which ticks once per second from the turn start instant while a turn is active and renders the final value afterwards, dimmed to read as inactive. - Placed the timer left of the action button in both `InputArea` instances and threaded the state through `ChatScreen`, `ChatAreaState`, `ChatArea` and `MessageList`. ## Implementation notes - The timer is a sealed state rather than a timestamp plus a boolean, so live and frozen values are mutually exclusive and observers never have to decide precedence. - The live value is recomputed from the start instant on every tick instead of being incremented, so a delayed or coalesced tick cannot accumulate drift. - Repeated `RUNNING` writes keep the original start instant, and repeated `IDLE` writes cannot overwrite an already frozen value. ## Validation - `./gradlew :app:desktopTest --offline` — PASS, 0 failures, 25 new tests across 4 new test classes. - `./gradlew :app:desktopMainClasses --offline` — PASS. - `./gradlew :app:wasmJsMainClasses --offline` — PASS. - `./gradlew :app:compileAndroidMain` cannot run in this environment (missing Android SDK path in `local.properties`); pre-existing and unrelated. ## Risks and follow-up - Timer values are in-memory per session and are intentionally not persisted or restored; reopening a session shows no timer until the next turn starts. - The two low-priority review findings that were deferred by explicit decision are out of scope for this pull request.
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
Torvian.eu/chatbot!336
No description provided.