Skip to content

feat(mobile): add the notification history screen - #8871

Open
iscekic wants to merge 7 commits into
mainfrom
drain/mobile-notification-history
Open

iscekic wants to merge 7 commits into
mainfrom
drain/mobile-notification-history

Conversation

@iscekic

@iscekic iscekic commented Oct 24, 2026 •

Copy link
Copy Markdown
Collaborator

Evidence

Evidence: demonstrated — on the Android emulator against the local dev stack, the screen listed the account's recorded notifications with copy rendered from the stored catalog key and params, filtered them by a tapped category chip, opened the destination a push tap opens, showed the push-off banner with the list still loading, and stated the retention window when the account had no rows.

What the first run observed (head 43f31a2654), dev stack on this VM, owned fake account seeded into the local PostgreSQL, five rows written into user_notification_history in the exact column shape the notifications worker's recorder writes — categories balanceAlerts twice, spendAlerts, securityFindings, and an agent_briefing row):

  • user.getNotificationHistory over the local tRPC endpoint returned all five rows newest first, with the stored category, push type, catalog key and title.
  • Preferences hub shows a Notification history row directly under Notifications, with the subtitle "Notifications this account received".
  • Opening the screen listed five rows: "Low balance alert / Acme balance fell below $5" with "17 minutes ago", "Spend alert / Personal spend crossed $25" with "1 hour ago", "New security finding (high) / Hardcoded secret in acme/widgets" with "yesterday", the row whose stored catalog key no longer exists reading its stored fallback copy with "3 days ago", and the briefing row reading its stored copy with "last month". A row's time is the device's relative format, not a stored string.
  • The chip row carried All plus the recorded categories, and the balanceAlerts chip was absent on this account because the server marks that category unavailable without an organization. Tapping Spend alerts narrowed the list on the device to the single spend row; the mounted test pins the query input the chip produces.
  • Tapping that row opened the account's Spend alerts screen — the destination notificationPathForData produces for a spend_alert push, without the via=push marker.
  • With the OS notification permission revoked, the screen showed the push-off banner and its Notification settings button above the full list; the button opened the Notifications settings screen.
  • With the rows deleted, the screen showed "No notifications yet" and "Notifications this account receives appear here. They are kept for 90 days." — the retention number comes from the shared constant.

Screenshots attached below (this VM has no image input; each assertion above is quoted from a uiautomator dump taken in the same command as its capture, and every PNG carries only IHDR/sRGB/sBIT/IDAT/IEND).

Re-verified on head d2ed97ef03 (after the review follow-ups, with a restarted Metro bundle):

  • The list still renders every row with its rendered copy and relative time.
  • A pull to refresh, after one more row was recorded, fetched page one and put that row at the top with "Just now" while the rows below it stayed — the collapse-and-reload path, not a re-read of the old page windows. The server log shows the page-one request the pull made.
  • Selecting a category with no rows says "No notifications in this category"; clearing the filter returns the full list and the empty state's retention line.

The three newest rows in that run came from the notifications worker itself, not from a fixture: a local worker with a temporary config that omits the Secrets Store block (its secret then comes from .dev.vars) accepted three POST /internal/v1/dispatch calls through the push sink and recorded one row each — low_balance → balanceAlerts, spend_alert → spendAlerts, security_finding → securityFindings — and a replay of the first idempotency key answered duplicate and added no row. The config change and a two-line local-only relaxation of the internal-secret check were reverted before this body was written; the tree is clean at the head. The app then pulled and rendered those rows with their catalog copy and params.

Re-verified on head 611d4a5fbb (the chip-row offline fix; Metro restarted with a cleared cache, owned fake account, four rows seeded into the local PostgreSQL in the worker's column shape):

  • Cold start with the device offline (airplane mode, the API unreachable) landed on the screen: the chip row rendered every offered chip, All through Balance alerts, and the list settled into "Something went wrong / Could not load notification history." with Retry — no skeleton held the screen.
  • Tapping Retry after connectivity returned loaded the four rows.
  • With connectivity, the same head rendered each row's copy and relative time, the Chat messages chip narrowed the list to its one row, a chip with no rows said "No notifications in this category", and tapping the low-balance row opened the Credit activity screen, the destination that push type opens.
  • The never-settling branch itself is pinned by the mounted suite: with the fix removed the new test leaves the chip skeletons on screen until its wait gives up; at this head it passes.

Limits of this proof: iOS was not exercised, because no iOS-only code or mobile line under Platform.OS/Platform.select changed. Real Expo delivery is not part of it: the sink answers for an accepted send, so the rows prove recording, querying and rendering, not device delivery. Pagination past the first page was not driven on the device — the mounted suite covers the second page, the retry footer and the retention bound.

Change

  • Add a Notification history row under Notifications in the Preferences hub and a route in the profile stack (/(app)/(tabs)/(3_profile)/notification-history).
  • notification-history-screen.tsx: the list. An infinite query over user.getNotificationHistory bounded by the shared retention helper so a newest-first list refuses its next page instead of letting maxPages drop the newest one; pull to refresh; skeleton rows; QueryError with retry for a failed first page; a retry footer for a failed next page; an empty state carrying the retention window.
  • lib/notification-history.ts: the row contract. Copy is rendered on the device from the stored catalog key and params through the shared push catalog in the app language, falling back to the stored title and body when the key is gone (never the content-free preview copy, which is a per-device send-time setting and is not what was recorded). A row's destination is notificationPathForData, with the via=push analytics marker removed, and the stored payload parsed first: a payload this app cannot parse has no destination, shows a toast and stays on the list.
  • The page bound refuses its next page at 10 pages rather than letting maxPages drop the newest, and the footer states when the bound — not the server — ends the list.
  • The chip row carries All plus the categories NOTIFICATION_HISTORY_CATEGORIES names — the categories the recorder can actually produce — and drops a category the server marks unavailable for the account.
  • A row tap fills the same pending-deep-link slot a notification response fills, so the root layout owns the organization switch and the navigation, exactly as a push tap does.
  • use-notification-master-gate.ts: the settings screen's master-gate computation as a hook (OS permission plus a token registered on the account), so the banner and the master switch cannot disagree.
  • Copy: eight keys under notifications.history.* in en.json, translated into all 86 other catalogs; chip labels reuse the settings screen's existing category copy.
  • sessionCardStaleAtSeconds loses its unused nowSeconds parameter. The extension typecheck reports it as an unused parameter, and it now runs because this change touches the tRPC package, so the pre-existing error blocked the typecheck group. Behaviour is identical: the quiet window still counts from the card's last activity.
  • packages/trpc now declares @kilocode/notifications. Its bundled mobile declarations already import NotificationHistoryCategory from that package, and without the dependency the import is unresolvable from the bundle, so every mobile consumer saw user.getNotificationHistory items as an untyped index signature. With it declared, the row type is the real wire type.

Verification

  • pnpm --filter kilo-app run typecheck, oxlint over the changed files, check:classes, check:plurals, check:i18n (87 catalogs match en.json), catalog-parity and the Notifications settings suite.
  • New tests: lib/notification-history.test.ts (copy rendering and fallback, in-language catalog copy, destination mapping with the marker stripped, unparseable payload, chip set and gating) and notification-history-screen.mounted.test.tsx (rendered rows and relative time, chip filtering, row tap through the deep-link slot, dead-target toast, empty state with the retention window, retry state, banner with the list loaded, no banner when push is enabled).
  • The existing Preferences hub test was updated for the new row and its route.
  • The chip row's preferences read now settles while the device is offline instead of pausing in pending, and the mounted suite pins it: a first load with the online manager offline and a failing preferences read renders every chip rather than its skeletons (the test fails on the previous head).

Production prerequisites

None.

Proxy answers

The owner's proxy gave these answers in the owner's place.

  1. Question: Chip set: The merged backend records only 6 of the 8 settings categories: NOTIFICATION_HISTORY_CATEGORIES = chatMessages, agentAttention, sessionStatus, balanceAlerts, spendAlerts, securityFindings. two of the settings keys have no recorded push kind, so a chip for either can only ever answer an empty page (and one of those two belongs to an excluded feature). Which chips should the filter row carry?
    Answer: The 6 recorded categories
    Confidence: 0.85 · Precedent: Product scope: excluded features stay untouched (session 2f71af3a787e); Trust code over documents — session 269375d6e466: "only trust code"
  2. Question: Chip gating: balanceAlerts and securityFindings are unavailable for some accounts (the server reports capabilities[key].available === false when the account has no organization / no security agent). What should the chip do then?
    Answer: Hide the chip
    Confidence: 0.75 · Precedent: UX: keep similar elements consistent across surfaces — session a247f3c19b1e: thought row should match the tool call row
  3. Question: Banner source: The brief: a banner above the list (with a 'Notification settings' button) when push is off or OS permission is denied. Which inputs decide 'push is off'?
    Answer: Full master gate
    Confidence: 0.80 · Precedent: UX: keep similar elements consistent across surfaces — session a247f3c19b1e; fix the class — session 00eb2a1d3e4b: "fix the class"
  4. Question: Dead target: A row must open the push destination and show a toast when the target is dead. What counts as a dead target that the row can detect without an extra fetch?
    Answer: Unparseable stored payload
    Confidence: 0.60 · Precedent: Speed: choose the simplest design that reaches the goal — session a30dc8dc131a: "Simplest please"
  5. Question: Analytics marker: notificationPathForData appends ?via=push, which marks the rendered screen's session_viewed event as push-originated. A history row is not a push tap. Keep the marker?
    Answer: Strip it
    Confidence: 0.50 · Precedent: Engineering: instrument telemetry that shows waste and makes issues easy to debug — session 55c9c80332a7: owner asked for telemetry that shows waste
  6. Question: Retention note: The screen states a 90-day retention. Where does the retention note appear?
    Answer: Empty state only
    Confidence: 0.80 · Precedent: Speed and least work: simplest design that reaches the goal (session a30dc8dc131a, "Simplest please"); the brief places the retention note in the empty state
  7. Question: Settings entry: Where is the history screen reachable from?
    Answer: Preferences hub only
    Confidence: 0.80 · Precedent: Speed and least work: do the least work possible with no drop in quality (session 7f3470e0ba6f, "do the least amount of work possible"); the brief names one entry point

A signed-in user could not review the notifications an account received: the
app had one notification settings screen and no list of past notifications.

Add a Notification history row under Notifications in the Preferences hub and
a screen that lists the recorded notifications newest first. Each row is
rendered on the device from the stored catalog key and its params in the app
language, falls back to the stored copy when that key is gone, and opens the
same destination a push tap opens, including the organization switch. The
list filters by the recorded categories, pages with a retention bound, pulls
to refresh, and covers loading, empty with the retention window, error and
retry states. A device whose pushes are off still lists what arrived and
offers a link to the notification settings.

The rows are typed from `user.getNotificationHistory`, and that output type
only resolves once the tRPC bundle can resolve the notifications package it
names: without it in the bundle's dependencies every mobile consumer saw the
item type as an untyped index signature.
@iscekic iscekic added drain Work item from the findings drain manual Work item from a manually filed finding labels Oct 24, 2026
@iscekic

iscekic commented Oct 24, 2026

Copy link
Copy Markdown
Collaborator Author

bot: Device captures from the Android emulator against the local dev stack, for the notification history screen at head 43f31a2654.

Each assertion below was read from a uiautomator dump taken in the same command as its capture. Fixtures are an owned fake account on the local stack; organization, repository and amounts are fabricated.

  1. The Preferences hub, with the new row directly under Notifications.
  2. The list: five rows, copy rendered from the stored catalog key and params, relative times from the device, and the row whose catalog key no longer exists reading its stored fallback copy.
  3. Filtered by the Spend alerts chip: the list narrows to the single spend row.
  4. The row tap destination: the account's Spend alerts screen.
  5. Push off for the device: the banner and its Notification settings button above the full list.
  6. No rows for the account: the empty state with the 90-day retention line.

Preferences hub with the notification history row

Recorded notifications with rendered copy and relative times

List filtered by the Spend alerts chip

Destination opened by tapping a row

Push-off banner above the loaded list

Empty state with the retention window

… a11y gallery

The Preferences hub gained a row, so the gallery that mounts that screen needs
the row's icon in its icon mock.
The extension typecheck reports the second parameter of
`sessionCardStaleAtSeconds` as unused, because the stale moment is derived from
the card's own last activity and the caller's default window. A change to the
tRPC package's dependencies now marks the extension affected, so that
pre-existing error blocks the typecheck group; remove the parameter and its
argument. Behaviour is identical: the quiet window still counts from the last
activity the server saw.
Comment thread apps/mobile/src/components/notification-history-screen.tsx Outdated
Comment thread apps/mobile/src/components/notification-history-screen.tsx
Comment thread apps/mobile/src/components/notification-history-screen.tsx Outdated
Comment thread apps/mobile/src/components/notification-history-screen.tsx Outdated
Comment thread apps/mobile/src/components/notification-history-screen.tsx Outdated
Comment thread apps/mobile/src/components/notification-history-screen.tsx
@kilo-code-bot

kilo-code-bot Bot commented Oct 24, 2026 •

Copy link
Copy Markdown
Contributor

Code Review Summary

Status: No Issues Found | Recommendation: Merge

Executive Summary

The incremental change gives the chip row's preferences query networkMode: 'always' plus an explicit refetchOnReconnect, so an offline first load settles into the all-chips fallback instead of holding three skeletons forever, and the new mounted test pins that path.

Assumptions: [INFERENCE] Typecheck, lint and the mounted suite were not executed in this read-only environment. The previous SUGGESTION on this query is resolved at this head, and the six earlier inline findings were re-verified as fixed in the current code (pathKey reset, exact infiniteQueryKey refetch and failure read, 10-page bound with capped footer, footer clearance, role="tab", filtered empty state). Minor edge case noted, not filed: after reconnect the preferences refetch can hide a chip the user selected while offline, leaving the filter applied with no active chip until All is tapped; notificationHistoryFilters documents keeping chips when availability is unknown, so the fallback is intentional.

Files Reviewed (2 files)
  • apps/mobile/src/components/notification-history-screen.tsx
  • apps/mobile/src/components/notification-history-screen.mounted.test.tsx
Previous Review Summaries (4 snapshots, latest commit d2ed97e)

Current summary above is authoritative. Previous snapshots are kept for context only.

Previous review (commit d2ed97e)

Status: 1 Issue Found | Recommendation: Address before merge

Executive Summary

The refresh-key fixes are correct — pathKey() resets every cached variant of the read and the exact infiniteQueryKey drives both the page-one refetch and the failure toast — but the chip row's preferences query still never settles while offline, so its three skeletons can stay on screen indefinitely.

Assumptions: [INFERENCE] Typecheck, lint and the mounted suite were not executed in this read-only environment. The carried SUGGESTION on the preferences query was re-verified against current HEAD (line 248); it lies outside the incremental diff, so it is reported here without a new inline comment. The updated mock's key shapes match tRPC 11.19's nested-path form, corroborated by agent-briefing-cache.test.ts:43-52 and use-create-briefing.ts:184, so the new mounted test now exercises the real key shape and would fail on the old prefix bug.

Overview

Severity Count
CRITICAL 0
WARNING 0
SUGGESTION 1
Issue Details (click to expand)

SUGGESTION

File Line Issue
apps/mobile/src/components/notification-history-screen.tsx 248 The preferences query keeps the default networkMode: 'online', so a first load while offline stays isPending/paused and the chip row renders its skeletons forever instead of falling back to NOTIFICATION_HISTORY_FILTERS. Give that query networkMode: 'always' (as the history query now has) or an offline guard.
Files Reviewed (2 files changed in this revision)
  • apps/mobile/src/components/notification-history-screen.tsx - 1 issue (carried, re-verified)
  • apps/mobile/src/components/notification-history-screen.mounted.test.tsx - no issues

Fix these issues in Kilo Cloud

Previous review (commit b6928da)

Status: 3 Issues Found | Recommendation: Address before merge

Executive Summary

Pull-to-refresh on the notification history screen still matches no cached query, because the refresh prefix is a { type: 'query' } key while the screen caches the { type: 'infinite' } key, so the new FlashList refresh props only spin and stop. The rest of the incremental change is correct.

Assumptions: [INFERENCE] Typecheck, lint and the test suites were not run in this read-only environment. The incremental diff (e258d8e13..b6928da59) changes only the FlashList refresh wiring (refreshing/onRefresh) and its mock; both warnings below were re-verified against current HEAD and are unchanged by that commit. The mounted refresh test still cannot catch the key-shape bug because its mock returns a flat ['user','getNotificationHistory'] key rather than tRPC's nested [path, { type, input }] shape.

Overview

Severity Count
CRITICAL 0
WARNING 2
SUGGESTION 1
Issue Details (click to expand)

WARNING

File Line Issue
apps/mobile/src/components/notification-history-screen.tsx 167 The refresh prefix comes from queryKey(), a { type: 'query' } key, while the screen caches the { type: 'infinite', input } key, so reconcileFirstPage and refetchQueries match nothing and pull-to-refresh never refetches. Switching the list to FlashList's refreshing/onRefresh pair makes the control behave correctly but does not fix the no-op refresh. Use pathFilter().queryKey or infiniteQueryKey(...).
apps/mobile/src/components/notification-history-screen.tsx 175 queryClient.getQueryState(prefix) is an exact-key hash lookup, so a prefix like the { type: 'query' } key above returns undefined; the refresh-failure toast never fires. Read the failure from the hook's query.isError after the await or from getQueryCache().find({ queryKey: prefix, type: 'infinite' }).

SUGGESTION

File Line Issue
apps/mobile/src/components/notification-history-screen.tsx 240 The chip row's preferences query keeps the default networkMode: 'online', so offline it stays isPending/paused and renders the three chip skeletons indefinitely instead of falling back to NOTIFICATION_HISTORY_FILTERS. The list half of the earlier offline finding is fixed by networkMode: 'always' on the history query; the preferences query still needs the same treatment or an offline guard.
Files Reviewed (2 files changed in this revision)
  • apps/mobile/src/components/notification-history-screen.tsx - 3 issues (carried, re-verified)
  • apps/mobile/src/components/notification-history-screen.mounted.test.tsx - no new issues

Fix these issues in Kilo Cloud

Previous review (commit e258d8e)

Status: 3 Issues Found | Recommendation: Address before merge

Overview

Severity Count
CRITICAL 0
WARNING 2
SUGGESTION 1
Issue Details (click to expand)

WARNING

File Line Issue
apps/mobile/src/components/notification-history-screen.tsx 167 Pull-to-refresh builds its prefix from queryKey(), a { type: 'query' } key, while the screen caches the { type: 'infinite', input } key, so reconcileFirstPage and refetchQueries match nothing and the refresh no-ops
apps/mobile/src/components/notification-history-screen.tsx 175 queryClient.getQueryState(prefix) is an exact-key lookup, so it returns undefined and the refresh-failure toast never fires

SUGGESTION

File Line Issue
apps/mobile/src/components/notification-history-screen.tsx 240 Carried from the previous review: the chip row's preferences query keeps the default networkMode: 'online', so it stays pending and shows skeletons while offline. The list half of that earlier finding is fixed by networkMode: 'always' on the history query.
Files Reviewed (90 files)
  • apps/mobile/src/components/notification-history-row.tsx
  • apps/mobile/src/components/notification-history-screen.tsx - 2 issues
  • apps/mobile/src/components/notification-history-screen.mounted.test.tsx
  • apps/mobile/src/i18n/locales/en.json
  • apps/mobile/src/i18n/locales/*.json (86 further catalogs; emptyCategoryTitle and capped present in all 87)

Notes: the previous WARNINGs on the 150-row cap, the footer's tab-bar clearance, the chip role, and the chip-agnostic empty state are fixed in this revision. [INFERENCE] Typecheck and the mounted suites were not run in this read-only environment; the new mounted refresh test cannot catch the key-shape bug because its mock returns a flat ['user','getNotificationHistory'] key rather than tRPC's nested [path, { type }] shape.

Fix these issues in Kilo Cloud

Previous review (commit ac8b67b)

Status: 6 Issues Found | Recommendation: Address before merge

Overview

Severity Count
CRITICAL 0
WARNING 3
SUGGESTION 3
Issue Details (click to expand)

WARNING

File Line Issue
apps/mobile/src/components/notification-history-screen.tsx 138 Paging silently stops at 150 rows while the UI promises a 90-day window
apps/mobile/src/components/notification-history-screen.tsx 150 First load while offline stays on the skeleton forever with no retry
apps/mobile/src/components/notification-history-screen.tsx 176 Pull-to-refresh re-fetches retained pages at stale cursors and drops rows

SUGGESTION

File Line Issue
apps/mobile/src/components/notification-history-screen.tsx 191 Footer replaces the tab-bar clearance, so the retry control can sit under the bar
apps/mobile/src/components/notification-history-screen.tsx 93 accessibilityRole='tab' is discarded by Button's fixed role='button'
apps/mobile/src/components/notification-history-screen.tsx 278 The empty state ignores the selected chip
Files Reviewed (97 files)
  • apps/mobile/src/app/(app)/(tabs)/(3_profile)/notification-history.tsx
  • apps/mobile/src/components/notification-history-screen.tsx - 6 issues
  • apps/mobile/src/components/notification-history-screen.mounted.test.tsx
  • apps/mobile/src/components/preferences-screen.tsx
  • apps/mobile/src/components/preferences-screen.mounted.test.tsx
  • apps/mobile/src/components/settings-screens-a11y.mounted.test.tsx
  • apps/mobile/src/lib/hooks/use-notification-master-gate.ts
  • apps/mobile/src/lib/notification-history.ts
  • apps/mobile/src/lib/notification-history.test.ts
  • apps/mobile/src/i18n/locales/*.json (87 catalogs, keys and {{days}} placeholder verified)
  • packages/trpc/package.json
  • pnpm-lock.yaml
  • packages/notifications/src/session-live-activity.ts
  • services/notifications/src/lib/session-card-delivery.ts

Notes: no security issue found; the server query is scoped to the authenticated user and the row payload is parsed with the shared schema. The sessionCardStaleAtSeconds parameter removal has no other callers and is behavior-preserving. useNowTicker cleans up its shared interval on unmount, so the new screen introduces no listener or timer leak. Assumption: query.refetch() re-fetches every retained page at its cached cursor, as documented by the repository's own infinite-retention test; typecheck and the mounted suites were not run in this read-only environment.


Reviewed by deepseek-flash-latest · Input: 52.7K · Output: 7.1K · Cached: 425.6K

Review guidance: REVIEW.md from base branch main

…empty filter

Review follow-ups on the notification history screen:

- A refresh now collapses the retained pages and reloads from the newest row.
  Each retained page is a keyset window that only stays contiguous with the
  page above it while page one's newest row is unchanged; re-requesting the
  old windows would skip every row between the old and the new page-one tail.
  The rows the refresh started from stay on screen until page one delivers.
- The page bound says so in the footer instead of ending the list silently,
  and rises to 10 pages.
- A first load while offline settles into the error state and its retry
  instead of pausing in `pending` and holding the skeleton.
- The chip carries `role="tab"`, which `Button` keeps (`role` outranks
  `accessibilityRole`).
- The next-page retry footer keeps the tab-bar clearance.
- An empty category says so, and the retention window stays on the unfiltered
  empty state it describes.

Two new catalog keys, translated into every catalog.
Comment thread apps/mobile/src/components/notification-history-screen.tsx Outdated
Comment thread apps/mobile/src/components/notification-history-screen.tsx Outdated
FlashList takes `onRefresh`/`refreshing`; it has no `refreshControl` prop, so
the pull gesture never reached the handler on the device. The element stays on
the state scrollers, which do accept one.
A tRPC query key carries `{}` as its second element, so `queryKey()` as a
prefix matches no infinite entry: the reset emptied nothing and the refetch
requested nothing, which is why a pull fetched no page. Reset through
`pathKey()` and refetch the entry's own `infiniteQueryKey`, and read the
failure from that exact key — `getQueryState` answers only an exact one.

The mounted fixture now builds tRPC's real key shapes, so the refresh case
fails if either key is wrong again, and it covers the refresh-failure toast.
@iscekic

iscekic commented Oct 24, 2026

Copy link
Copy Markdown
Collaborator Author

bot: Two more captures from the Android emulator at head d2ed97ef03, on the local dev stack with an owned fake account and fabricated fixtures.

  1. A pull to refresh after a row was recorded: the new row sits at the top with "Just now", the rows recorded before it are still below, and the server log shows the page-one request the pull made.
  2. Selecting a category with no rows: the list says "No notifications in this category" instead of the account-wide empty copy.

List after a pull to refresh, newest row first

Category with no rows

@iscekic

iscekic commented Oct 24, 2026

Copy link
Copy Markdown
Collaborator Author

bot: The rows in this capture came from the notifications worker itself, not from fixtures.

Three POST /internal/v1/dispatch calls through the worker's local push sink produced one history row each — a low-balance alert, a spend alert and a security finding — and a replay of the first idempotency key answered duplicate and added no row. The app then pulled and rendered them with the catalog copy and params the worker stored.

Fabricated fixtures only: the organization, repository and amounts are invented, and the account is an owned fake account on the local stack.

History rows recorded by the notifications worker push sink

@iscekic iscekic added merge:blocked:kilobot-not-ready Review bot has not recommended merge at this head merge:bot Bot-owned review; merge when ready once readiness passes and a review approves. labels Oct 24, 2026
The chip row's preferences read kept the default network mode, so a first
load without connectivity paused in pending and left its skeletons on
screen for as long as the device stayed offline. Give the read the same
mode the list uses, so it answers or fails into the full chip set.
@iscekic iscekic added merge:blocked:kilobot-not-ready Review bot has not recommended merge at this head merge:human Substantive functional risk requires human review. and removed merge:bot Bot-owned review; merge when ready once readiness passes and a review approves. merge:blocked:kilobot-not-ready Review bot has not recommended merge at this head labels Oct 24, 2026
@iscekic

iscekic commented Oct 24, 2026

Copy link
Copy Markdown
Collaborator Author

bot: Device captures from the Android emulator at head 611d4a5fbb, on the local dev stack with an owned fake account and fabricated fixtures. Each assertion was read from a uiautomator dump taken in the same command as its capture.

  1. The list with connectivity: four rows, copy rendered from the stored catalog key and params, relative times from the device, and the row whose catalog key no longer exists reading its stored fallback copy.
  2. The Chat messages chip selected: the list narrows to its one row.
  3. A cold start with the device offline and the API unreachable: every offered chip still renders and the list settles into its retry state instead of holding skeletons.
  4. Retry after connectivity returned: the rows load again.

History list with rendered copy and relative times

List narrowed by the chat messages chip

Offline cold start showing chips and the retry state

Rows reloaded after retry following reconnect

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

drain Work item from the findings drain manual Work item from a manually filed finding merge:human Substantive functional risk requires human review.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant