feat(chats): chat row states and honest unread markers - #280
Conversation
…ote and mobile
Chat list rows now show Needs approval, Needs input, Working or Idle,
plus an unread dot that appears only when assistant output arrived after
the user last viewed the chat. A durable install baseline keeps history
from lighting up.
- Desktop: attention tracking in ChatActivityRegistry, a main-owned
read-marker store, and a sidebar ChatRowStatus with soft semantic fills.
- Remote (contract revision 16): /chat-summaries rows carry rowState and
unread, and POST /chats/{chatId}/read is advertised as
chat-read-state-v1. OpenAPI, fixtures and API docs are updated.
- iOS/Android: tolerant decoding, row pills and unread dot, and a
read-on-open report.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
There was a problem hiding this comment.
Important
Mobile read reports can clear output that was not in the displayed snapshot, and timestamp-only markers can suppress later output when message times tie.
Reviewed changes Reviewed head d0dff38 for the new chat row-state and shared unread behavior across Electron, Aiden Remote, iOS, and Android.
- Attention states
ChatActivityRegistrynow projects active turns and pending approval/input prompts into prioritized row states, with corresponding desktop and native indicators. - Read tracking A durable installation baseline and per-chat read-through markers drive unread state, while the desktop reports the latest conversational message shown in its visible chat.
- Remote contract Summary rows gain optional
rowStateandunreadfields, and revision 16 adds the feature-gatedPOST /chats/{chatId}/readroute. - Native clients iOS and Android tolerate additive summary fields, cache and render the row status, and report chats as read when opened.
ℹ️ Contract revision 16 is concurrently claimed
PR #275 is also open against revision 15 and claims revision 16. The two PRs cannot both land unchanged; coordinate merge order and advance the later branch from the resulting main revision, keeping the contract fixtures, client assertions, and documentation aligned.
Technical details
# Advance the second Remote contract PR
## Affected sites
- [`protocol/aiden-remote/v1/fixtures/contract.json:2`](https://github.com/sambitcreate/aiden-agent/blob/d0dff38f0d65ed69ef2daf5d62ee5078cd43f12a/protocol/aiden-remote/v1/fixtures/contract.json#L2) — this PR claims revision 16.
- [`android/app/src/test/resources/contract.json:2`](https://github.com/sambitcreate/aiden-agent/blob/d0dff38f0d65ed69ef2daf5d62ee5078cd43f12a/android/app/src/test/resources/contract.json#L2) — mirrored fixture.
- [`main/services/aiden-remote-protocol.test.ts:102`](https://github.com/sambitcreate/aiden-agent/blob/d0dff38f0d65ed69ef2daf5d62ee5078cd43f12a/main/services/aiden-remote-protocol.test.ts#L102), [`main/services/aiden-remote-operation-contract.test.ts:900`](https://github.com/sambitcreate/aiden-agent/blob/d0dff38f0d65ed69ef2daf5d62ee5078cd43f12a/main/services/aiden-remote-operation-contract.test.ts#L900), [`ios/AidenOnTheGoTests/AidenRemotePhase0Tests.swift:139`](https://github.com/sambitcreate/aiden-agent/blob/d0dff38f0d65ed69ef2daf5d62ee5078cd43f12a/ios/AidenOnTheGoTests/AidenRemotePhase0Tests.swift#L139), and [`android/app/src/test/java/sbtbiswas/AidenOnTheGo/AidenBotContractTest.kt:124`](https://github.com/sambitcreate/aiden-agent/blob/d0dff38f0d65ed69ef2daf5d62ee5078cd43f12a/android/app/src/test/java/sbtbiswas/AidenOnTheGo/AidenBotContractTest.kt#L124) — revision assertions.
- [`docs/aiden-remote-api-v1.md:108`](https://github.com/sambitcreate/aiden-agent/blob/d0dff38f0d65ed69ef2daf5d62ee5078cd43f12a/docs/aiden-remote-api-v1.md#L108) — documents the revision-16 summary additions.
## Required outcome
- Whichever PR lands second must increment from the revision on `main` after the first merges and update the shared/Android fixtures, TypeScript and native assertions, OpenAPI, and contract documentation together.GPT Luna | 𝕏
There was a problem hiding this comment.
Important
The follow-up commit's formatting reflow breaks two existing sidebar source-reading contracts, so npm run test:sidebar (and the CI renderer-other lane) fails on head.
Reviewed changes Re-reviewed head e5bdbd5 against the prior Pullfrog review (d0dff38); the delta is the read-boundary fix commit plus formatter reflows.
- Mobile read boundaries Android and iOS read reports now capture and send the newest locally displayed message ID (
lastViewedMessageId), so a reply persisted after the snapshot can no longer be cleared unseen. Verified on Android; both inline threads from the prior review are resolved. - Equal-time tie-breaking
metaOfaddslastAssistantSequence, markers persistreadThroughSequence, and both desktop and Remote read handlers compute ordering consistently (full-array positions, system-role safe). Remote summary rows still never expose positions. - Formatter reflow The same commit brought
chat-sidebar.tsx,aiden-remote-chats.ts, andchat-store-core.tsunder the repo's currentoxfmtoutput, reflowing large untouched regions of those files. - Verification
test:chat-row-states(15),test:aiden-remote-chat-summaries(13), the fulltest:aiden-remotechain,test:ask-user-question, the reflow-adjacent source contracts (subagent-phase3-contract,context-lifecycle-surface-matrix), CI policy,tsc --noEmit, eslint, and filtered Android unit tests all pass; only the sidebar contract suite fails. iOS XCTest was not runnable on this Linux runner.
GLM Flash | 𝕏
There was a problem hiding this comment.
✅ No new issues found.
Reviewed changes Re-reviewed the new head 8b252e0e against my previous review (e5bdbd5); the delta reverts the formatter reflow of unrelated regions so the fix commit stays focused on the changed behavior.
- Reflow reverted
aiden-remote-chats.ts,chat-store-core.ts(and its test), andchat-sidebar.tsxare back on their previous formatting, keeping only the read-boundary and sequence-tie fixes and the feature code itself. - Sidebar contracts green again The two source-reading contracts in
chat-sidebar.test.tsxthate5bdbd5broke pass again — re-verifiednpx tsx --test renderer/components/chat-sidebar.test.tsx(29/29), the focusedtest:chat-row-statessuite (15/15), andtsc --noEmiton head. No CI format gate exists, so reverting the reflow does not violate the repo formatter, whose drift already exists onmain. - Open coordination item (carried, not new) Contract revision 16 is still concurrently claimed by open PR #275; whichever PR merges second must renumber from the resulting
mainrevision.
GLM Flash | 𝕏
There was a problem hiding this comment.
✅ No new issues found.
Reviewed changes Re-reviewed the new head 40b88315 against my previous review (8b252e0e); the delta fixes one residual background-clearing path in Android.
- Foreground-only read reports Android's
reportChatViewednow requires a newisChatForegroundedflag, so a background reconcile/refresh can no longer report a chat read the device never showed.AidenChatDetailScreendrives the flag from LifecycleON_RESUME/ON_PAUSE(plus initial state and dispose), andsetChatForegrounded(true)re-reports the current_chatsnapshot, so output persisted while the screen was covered is only cleared once it is actually resumed and seen. - Regression coverage The new MockWebServer-driven
AidenChatTest.backgroundChatRefreshReportsReadOnlyAfterReturningToChatverifies the background refresh stays silent, resume reports the boundary atreply-two, and duplicate foreground events do not re-report. Ran it isolated::app:testDebugUnitTest --tests sbtbiswas.AidenOnTheGo.AidenChatTestpasses 74/74. - Carried coordination item Contract revision 16 is still concurrently claimed by open PR #275; whichever PR merges second must renumber from the resulting
mainrevision.
ℹ️ Nitpicks
- When
AidenChatTestshares one gradle unit-test fork withAidenChatSummaryTest, two pre-existing tests (failedSendRestoresDurableDraftAfterRestart,failedSendPersistsSubmittedTextAndNewerEdits, unchanged sincemain) intermittently fail with anIllegalStateExceptionfromDispatchers.resetMain()in the shared helper'sfinally(AidenChatTest.kt:624); the class passes 74/74 when run alone. Timing-based cross-suite pollution of the shared process, not introduced by this PR — worth a look in a separate cleanup.
GLM Flash | 𝕏
Hermes Review BotConfidence: 5 Engine: SummaryDynamic chat list row states ( Confidence Score: 5/5Complete behavioral tracing was performed across the main process services, desktop renderer hooks and components, Aiden Remote server routes, OpenAPI schemas, Android Kotlin ViewModels/caches, iOS TCA features/caches, and all unit/contract test suites. 📁 Important Files Changed
FindingsNo findings. Sequence DiagramsequenceDiagram
autonumber
actor User as User (Desktop / Mobile)
participant UI as Chat Row & Pane UI
participant Attention as Chat Activity Core
participant Store as Chat Read Marker Store
participant Remote as Aiden Remote API
participant Agent as Agent Execution
Agent->>Attention: requestAttention(chatId, "approval")
Attention-->>UI: Broadcast activity state ("needs_approval")
UI-->>User: Render badge (Approval required)
User->>UI: Approve tool / submit input
UI->>Attention: resolveAttention(chatId)
Attention-->>UI: Transition state ("working")
Agent->>UI: Stream assistant response completes
Attention-->>UI: Settle stream & clear working state ("idle")
opt Chat not active in foreground
Store-->>UI: Compare lastAssistantAt/Seq > readThrough/Seq -> Unread
UI-->>User: Display unread indicator dot
end
User->>UI: Select and view chat
UI->>Store: markRead(chatId, timestamp, sequence)
opt Mobile Client
UI->>Remote: POST /chats/{chatId}/read
Remote->>Store: Persist read boundary
end
UI-->>User: Clear unread indicator
[]
|
|
Pullfrog stalled The agent stopped emitting events for 120s and was killed by the activity-timeout watchdog. The model produced no output at all before the stall — the request was sent but nothing came back. This is usually transient; re-running often succeeds. Recent agent stderr
|
There was a problem hiding this comment.
Important
A late iOS detail update can still clear a cached unread marker after navigation.
Reviewed changes Re-reviewed the changes since the prior Pullfrog review: iOS foreground/scene read admission and the isolated held-events test.
- Gated iOS read reports. Added visible-detail and active-scene checks and re-reporting of the latest snapshot when the scene resumes.
- Isolated stream recovery. Scoped the held request to the unique stream-consumer endpoint and exercised the progress observer before sending.
GPT Luna | 𝕏
| } | ||
|
|
||
| private func acceptSelectedChatUpdate(_ updated: AidenChat) { | ||
| homeModel.accept(updated) |
There was a problem hiding this comment.
A detail update that completes after navigation still reaches homeModel.accept before the selected-chat guard, and accept(_:) rebuilds the summary with unread: false. This can erase the cached unread dot for output the user has not viewed; move the guard ahead of this mutation.
There was a problem hiding this comment.
ℹ️ No new issues found in this delta. Approval remains withheld while the existing iOS stale-detail review thread is unresolved.
Reviewed changes Re-reviewed a1729c27, the only commit since the prior Pullfrog review, covering navigation-readiness and persisted-state assertions in the chat queue E2E specs.
- Workspace-bar relaunch path. Waits for a usable composer and selected chat, then verifies the persisted message is visible before checking settings after relaunch.
- Detached-response navigation. Waits for New Agent selection to complete before reopening the running chat, then verifies the transcript and Stop control before cancelling.
- Validation.
npm run type-check:e2eandnpm run test:e2e:listpass. Electron runtime tests were not run on this Linux runner.
GPT Luna | 𝕏
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
# Conflicts: # android/app/src/main/java/sbtbiswas/AidenOnTheGo/models/AidenInstallation.kt # android/app/src/main/java/sbtbiswas/AidenOnTheGo/protocol/AidenRemoteProtocol.kt # android/app/src/test/java/sbtbiswas/AidenOnTheGo/AidenBotContractTest.kt # android/app/src/test/java/sbtbiswas/AidenOnTheGo/AidenChatTest.kt # android/app/src/test/resources/contract.json # docs/aiden-remote-api-v1.md # ios/AidenOnTheGo/Networking/AidenRemoteClient.swift # ios/AidenOnTheGoTests/AidenChatTests.swift # ios/AidenOnTheGoTests/AidenRemoteClientTests.swift # ios/AidenOnTheGoTests/AidenRemotePhase0Tests.swift # main/services/aiden-remote-operation-contract.test.ts # main/services/aiden-remote-protocol.test.ts # main/services/aiden-remote-protocol.ts # main/services/ask-user-question-coordinator.test.ts # main/services/ask-user-question-coordinator.ts # package.json # protocol/aiden-remote/v1/fixtures/contract.json # scripts/ci-test-registry.json
There was a problem hiding this comment.
ℹ️ No new issues found in this delta. Approval remains withheld while the existing iOS stale-detail thread remains unresolved.
Reviewed changes Re-reviewed merge commit 38552b4, focusing on the Remote contract reconciliation and prompt-state integration with current main.
- Advanced the Remote contract to revision 18. Updated fixtures, TypeScript/iOS/Android assertions, OpenAPI, and API documentation after revision 17 landed on
main, while preservingchat-agent-interrupt-v1. - Integrated prompt timeout handling. Merged the upstream ask-user timeout/expiration lifecycle with row-attention withdrawal so timed-out and cancelled prompts stop reporting Needs input.
- Validated the merged head.
test:chat-row-states(15),test:aiden-remote(529 passed, one skipped, plus transport tests),test:ask-user-question(19),test:sidebar(44), TypeScript type-check, and focused Android contract/chat suites passed. iOS XCTest was unavailable on this Linux runner.
GPT Luna | 𝕏

Summary
Chat list rows now show a richer state and an honest unread marker, inspired by Hermex's chat list.
Per-surface changes
Desktop (main and renderer)
ChatActivityRegistrynow tracks attention prompts by prompt and stream.withdrawcallback on the coordinator.ChatReadMarkerStore(userData/chat-read-markers.json, mode 0600, bounded):ChatMeta.lastAssistantAtis computed inmetaOf.chats:readMarkersandchats:markRead, plus the broadcastchats:read-markers-changed.ChatRowStatusshows soft-fill Approve/Reply pills with no borders, the Working spinner, and an unread dot, each with accessible labels.aria-busyis set only for Working.useMarkChatReadreports reads only while the window is visible.Remote (contract revision 16)
/chat-summariesrows gainrowStateandunread. Both are optional in the parser; the Mac always sends them.POST /chats/{chatId}/read:{}or{throughMessageId}; returns 204.chat:readplus readable chat access.chat-read-state-v1.docs/aiden-remote-api-v1.md.iOS and Android
rowStatebecomes nil/null instead of rejecting the page.displayRowStateuses local activity as the authority for idleness, so a stale server attention state cannot linger. Older Macs fall back to Working/Idle.This PR claims revision 16 (main is at 15). Other PRs in the same wave may also claim 16. Whichever merges second must renumber. The revision appears in both fixtures, the TS/iOS/Android fixture assertions and the protocol test inventory.
Tests run
npm run test:chat-row-states(new script, appended totest): 13 passnpm run test:aiden-remote: 19 + 508 passnpm run test:sidebar: 7 passnpm run test:ask-user-question: 44 passnpm run test:cli(build and bundle): 59 passnpx tsc --noEmitandnpm run lint: cleanAidenRemoteClientTests,AidenRemotePhase0Tests,AidenChatTests): passed, including the new row-state and read-report tests./gradlew :app:testDebugUnitTest: passed, including the newAidenChatRowStatusTestand additions to the summary and contract testsFollow-ups
🤖 Generated with Claude Code