Skip to content

feat(chats): chat row states and honest unread markers - #280

Merged
sambitcreate merged 9 commits into
mainfrom
feature/chat-row-states
Sep 30, 2026
Merged

sambitcreate merged 9 commits into
mainfrom
feature/chat-row-states

Conversation

@sambitcreate

Copy link
Copy Markdown
Owner

Summary

Chat list rows now show a richer state and an honest unread marker, inspired by Hermex's chat list.

  • Row state: Needs approval > Needs input > Working > Idle. A chat that is blocked on the user outranks one that is only working.
  • Unread: shown only when new assistant output arrived after the user last viewed the chat. A durable install baseline means existing history never shows as unread. Read markers only move forward and are shared by every surface, so viewing a chat on the phone also clears it on the desktop.

Per-surface changes

Desktop (main and renderer)

  • ChatActivityRegistry now tracks attention prompts by prompt and stream.
    • Tool approvals raise approval attention.
    • ask-user-question prompts raise input attention through a new withdraw callback on the coordinator.
    • When a stream settles, its prompts are cleared.
  • New ChatReadMarkerStore (userData/chat-read-markers.json, mode 0600, bounded):
    • It lives outside the chat index, so marking a chat read never bumps a summary revision.
    • ChatMeta.lastAssistantAt is computed in metaOf.
    • New IPC chats:readMarkers and chats:markRead, plus the broadcast chats:read-markers-changed.
  • Sidebar:
    • ChatRowStatus shows soft-fill Approve/Reply pills with no borders, the Working spinner, and an unread dot, each with accessible labels.
    • aria-busy is set only for Working.
    • The open chat never shows the unread dot.
    • useMarkChatRead reports reads only while the window is visible.

Remote (contract revision 16)

  • /chat-summaries rows gain rowState and unread. Both are optional in the parser; the Mac always sends them.
  • New POST /chats/{chatId}/read:
    • Body is {} or {throughMessageId}; returns 204.
    • Requires chat:read plus readable chat access.
    • Advertised as chat-read-state-v1.
  • Updated OpenAPI, the shared fixture and its Android copy, and docs/aiden-remote-api-v1.md.

iOS and Android

  • Summary decoding is tolerant: missing fields default, and an unknown future rowState becomes nil/null instead of rejecting the page.
  • displayRowState uses local activity as the authority for idleness, so a stale server attention state cannot linger. Older Macs fall back to Working/Idle.
  • Rows show 12% soft semantic fills (warning for Approve, accent for Reply), the spinner for Working, and an accent unread dot.
  • Opening a chat clears unread locally and reports the read once per chat revision when the feature is advertised.

⚠️ Contract revision

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 to test): 13 pass
  • npm run test:aiden-remote: 19 + 508 pass
  • npm run test:sidebar: 7 pass
  • npm run test:ask-user-question: 44 pass
  • chat-activity (renderer and core) and chat-store-core: 55 pass
  • aiden-remote-protocol, operation-contract, coordinator and sidebar: 81 pass
  • npm run test:cli (build and bundle): 59 pass
  • npx tsc --noEmit and npm run lint: clean
  • iOS XCTest on the iPhone 17 Pro simulator (AidenRemoteClientTests, AidenRemotePhase0Tests, AidenChatTests): passed, including the new row-state and read-report tests
  • Android ./gradlew :app:testDebugUnitTest: passed, including the new AidenChatRowStatusTest and additions to the summary and contract tests

Follow-ups

  • Bot chats and Bot contact rows are excluded for now.
  • Physical-device visual acceptance of the mobile pills and dot.
  • Renumber the revision if another wave PR merges 16 first.

🤖 Generated with Claude Code

…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>

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 ChatActivityRegistry now 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 rowState and unread fields, and revision 16 adds the feature-gated POST /chats/{chatId}/read route.
  • 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.

Pullfrog  | Fix all ➔ | Fix 👍s ➔ | View workflow run | Using GPT Luna | 𝕏

Comment thread renderer/shared/chat-row-state.ts Outdated

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 metaOf adds lastAssistantSequence, markers persist readThroughSequence, 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, and chat-store-core.ts under the repo's current oxfmt output, reflowing large untouched regions of those files.
  • Verification test:chat-row-states (15), test:aiden-remote-chat-summaries (13), the full test:aiden-remote chain, 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.

Pullfrog  | Fix all ➔ | Fix 👍s ➔ | View workflow run | Using GLM Flash | 𝕏

Comment thread renderer/components/chat-sidebar.tsx Outdated

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ 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), and chat-sidebar.tsx are 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.tsx that e5bdbd5 broke pass again — re-verified npx tsx --test renderer/components/chat-sidebar.test.tsx (29/29), the focused test:chat-row-states suite (15/15), and tsc --noEmit on head. No CI format gate exists, so reverting the reflow does not violate the repo formatter, whose drift already exists on main.
  • 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 main revision.

Pullfrog  | View workflow run | Using GLM Flash | 𝕏

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ 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 reportChatViewed now requires a new isChatForegrounded flag, so a background reconcile/refresh can no longer report a chat read the device never showed. AidenChatDetailScreen drives the flag from Lifecycle ON_RESUME/ON_PAUSE (plus initial state and dispose), and setChatForegrounded(true) re-reports the current _chat snapshot, 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.backgroundChatRefreshReportsReadOnlyAfterReturningToChat verifies the background refresh stays silent, resume reports the boundary at reply-two, and duplicate foreground events do not re-report. Ran it isolated: :app:testDebugUnitTest --tests sbtbiswas.AidenOnTheGo.AidenChatTest passes 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 main revision.

ℹ️ Nitpicks

  • When AidenChatTest shares one gradle unit-test fork with AidenChatSummaryTest, two pre-existing tests (failedSendRestoresDurableDraftAfterRestart, failedSendPersistsSubmittedTextAndNewerEdits, unchanged since main) intermittently fail with an IllegalStateException from Dispatchers.resetMain() in the shared helper's finally (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.

Pullfrog  | View workflow run | Using GLM Flash | 𝕏

@very-hermes-bot

very-hermes-bot commented Sep 27, 2026 •

Copy link
Copy Markdown
Collaborator

Hermes Review Bot

Confidence: 5

Engine: agy/gemini-3.8-flash-high
Review mode: full
Head: 38552b42a32eebd2d705560c82d9e5fc40e68699
Generated: 2026-09-30T22:27:31+00:00
Reviews: 1

Summary

Dynamic chat list row states (needs_approval > needs_input > working > idle) and durable unread status tracking are established across Electron desktop, the Aiden Remote protocol, Android, and iOS. Durable read boundaries are persisted to chat-read-markers.json using atomic replacement and file mode 0600, using monotonic timestamp and assistant message sequence tie-breaking to avoid race conditions and regressions. The Remote protocol cleanly claims revision 18 under the chat-read-state-v1 feature flag, backed by synchronized OpenAPI contracts and JSON fixtures across native platforms. Android and iOS clients reliably guard read receipts against background noise by requiring active foreground and scene states before dispatching read receipts to POST /chats/{chatId}/read. Maintainers should only verify upon final merge that revision 18 remains the next incremental protocol revision relative to main if concurrent branches land first.

Confidence Score: 5/5

Complete 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
  • main/services/chat-read-markers.ts: Implements file-backed read marker persistence with 10,000-entry capacity bounding and timestamp/sequence tie-breaking.
  • main/services/chat-activity-core.ts: Adds priority-based attention tracking (approval > input), stream lifecycle tracking, and event emission.
  • main/services/ask-user-question-coordinator.ts: Connects question prompts and user submissions to the chat activity attention lifecycle.
  • main/services/aiden-remote-chats.ts & main/services/aiden-remote-router.ts: Exposes POST /chats/{chatId}/read, enriches chat summaries with activity and unread metadata, and handles protocol errors.
  • protocol/aiden-remote/v1/fixtures/contract.json & protocol/aiden-remote/v1/openapi.yaml: Defines revision 18 specifications, summary schemas, and fixture data.
  • renderer/components/chat-row-status.tsx: Provides the shared desktop status component with accessible tooltips, badge indicators, and semantic token styling.
  • renderer/hooks/use-mark-chat-read.ts: Synchronizes active desktop chat views with the main-process read marker store.
  • android/app/src/main/java/com/aiden/agent/ui/chat/AidenChatViewModel.kt: Implements Android row status resolution, cache updates, and foreground-gated read reporting.
  • ios/Aiden/Features/Chat/AidenChatFeature.swift: Implements iOS row status priority resolution, cache storage, and scene-active read reporting.
  • tests/chat-read-markers.test.ts & tests/chat-activity-core.test.ts: Adds rigorous test coverage for edge cases, tie-breakers, and stream cancellation.

Findings

No findings.

Sequence Diagram

sequenceDiagram
  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
Loading
[]

Last reviewed commit: 38552b42a32e
Reviews (1) · Comment /hermes review to trigger a new review · /hermes review full for full re-review

@pullfrog

pullfrog Bot commented Sep 27, 2026

Copy link
Copy Markdown
Contributor

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
timestamp=2026-09-27T17:51:08.493Z level=ERROR run=065ac47d message="stream error" providerID=opencode-go modelID=glm-5.3-flash session.id=ses_f1c02117effeNIAUHRcsUmNHLs small=false agent=build mode=primary error.error="AI_APICallError: Go usage limit exceeded"

Pullfrog  | Rerun failed job ➔ | View workflow run | via Pullfrog | Using GLM Flash | 𝕏

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Pullfrog  | Fix all ➔ | Fix 👍s ➔ | View workflow run | Using GPT Luna | 𝕏

}

private func acceptSelectedChatUpdate(_ updated: AidenChat) {
homeModel.accept(updated)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ℹ️ 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:e2e and npm run test:e2e:list pass. Electron runtime tests were not run on this Linux runner.

Pullfrog  | Fix it ➔ | View workflow run | Using GPT Luna | 𝕏

sambitcreate and others added 2 commits September 29, 2026 21:39
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

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ℹ️ 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 preserving chat-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.

Pullfrog  | Fix it ➔ | View workflow run | Using GPT Luna | 𝕏

@sambitcreate
sambitcreate merged commit fe3888c into main Sep 30, 2026
24 checks passed
@sambitcreate
sambitcreate deleted the feature/chat-row-states branch September 30, 2026 22:55
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