Skip to content

feat(notifications): dual-stream Notification Center and Status Hint banner - #305

Merged
zjy365 merged 14 commits into
mainfrom
aimeritething/dunning
Aug 28, 2026
Merged

zjy365 merged 14 commits into
mainfrom
aimeritething/dunning

Conversation

@aimeritething

@aimeritething aimeritething commented Aug 27, 2026

Copy link
Copy Markdown
Member

Summary

The Notification Center stops being a dev-mock shell and becomes the user's real inbox, and a Status Hint banner explains a holding billing state at the top of the content area.

  • Dual-stream inbox (AIM-332). Platform Notifications (the debt ladder's fixed-name CRs) are read live from the cluster through a Go read proxy with the caller's own kubeconfig — never copied. Brain-produced Notifications live in Brain's Postgres (sealai_notification: messages with a dedupe key, per-user read receipts, 365-day sweep on write). One merged list, real timestamps, cr:/db: ids. Any role marks read via a receipt; Owners/Managers additionally patch the CR's isRead label best-effort so the desktop bell follows. First producer: quota-exhausted, one entry per resource, edge-triggered, released on recovery.
  • Display layer and remaining producers (AIM-334). The 8 fixed-name debt-ladder CRs render Brain-voiced titles, bodies, and CTAs (display-only; unknown names fall back to upstream text). Gift-only newcomers no longer see the low/critical balance tiers. Two more producers: the one-time gift hint (credit-hint:<user>) and subscription-change receipts (subscription-change:<workspace>:<txid>). The legacy sidebar dev mock is deleted; the billing dev-fixture seam now feeds the real pipeline.
  • Status Hint (AIM-333). Single-slot banner: payment-due > Account Debt > quota-full > trial-expiry; the destructive states cannot be dismissed, the other two dismiss and revive on state re-entry. Deadlines derive client-side (ADR-0063); CTAs record the billing return route.
  • ADR-0067 records the hybrid storage decision, the rejected alternatives, and the no-standing-credentials pivot.

Review follow-ups (last commit)

  • CR ids are now versioned by version = spec.timestamp, else metadata.generation, else creation time. Before, a CR without spec.timestamp fell back to creationTimestamp for its id, which an in-place overwrite keeps — an old receipt would have hidden the revived message. A label patch never changes generation, so marking read does not move the id.
  • Pinned: an upgrade over a still-scheduled downgrade is the next receipt; the superseded downgrade stays as history and its transaction never re-writes.
  • Dedup: shared lib/time, one billing surface tint recipe, MICRO_UNITS_PER_CURRENCY_UNIT, id helpers in notification-ids; one authorize+parse seam in the handlers and one JSON post helper in the client; notificationCRItemSchema pinned to NotificationCRItem; sidebar CTA via AppButton; subscription-summary SWR key via kubeconfigCredentialKey.

Known gaps (deliberate, tracked separately)

  • The gift hint is per-user but stored in the first observing workspace; other workspaces' inboxes do not list it.
  • Gift-hint bodies use the fallback "expires a month after it was granted" — no expiry date is proxied yet.
  • Cancellations without a recorded transaction key their receipt by cancel:<periodEnd>.
  • Free-plan payment-due copy says "Upgrade plan" (a Free plan cannot be renewed); the design doc still says "Renew plan".

Testing

  • bun check, bun typecheck, go vet: clean.
  • go test ./service/notification/... ./route/notification/...: pass (fallback test asserts timestamp → creation time and version → generation).
  • bun test for notifications, status-hint, shell, billing/server/dev-fixtures: 170 pass, 0 fail; packages/api CR hook tests pass. The two billing-plan-surface.interaction failures pre-date this branch (reproduced on clean main).

Database migration

  • The two migrations this branch originally carried (0015_notification_center, 0016_notification_account_scope) are squashed into a single 0015_notification_center regenerated from schema.ts. Nothing outside this branch ever applied them.
  • Merge-order note for feat(projects): adopt external Template Instances as Brain Projects #299 / feat(chat): use current GitHub authorization in Chat Devbox #298: those PRs also add a 0015 migration. Whichever lands later must renumber its migration on rebase (idx, file name, _journal.json tag) and regenerate its when timestamp — drizzle's migrator orders by when > last applied created_at, not by idx, so a rebased migration with an older when than the one already on main is silently skipped on databases that already applied it. Deleting the migration + snapshot + journal entry and re-running bun run db:generate does both.

🤖 Generated with Claude Code

aimeritething and others added 5 commits August 27, 2026 21:59
…ual-stream inbox

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The Notification Center goes from dev-mock shell to a real inbox with
exactly one source of truth per message (ADR-0067):

- Go read proxy `/api/notification/v1alpha1`: lists the namespace's
  upstream Notification CRs with the caller's kubeconfig bearer and
  merge-patches the desktop-compatible `isRead` label; route-level and
  fake-cluster service tests.
- Brain store `sealai_notification`: `notification_messages` with a
  partial-unique live `dedupe_key` (release on recovery keeps history)
  and a 365-day sweep on write; `notification_read_receipts` per user x
  workspace x source-prefixed id, cascading from swept rows and re-keyed
  on account merge (ADR-0059).
- First producer: quota-exhausted, edge-triggered per resource at 100%,
  observed at the chat turn and the sidebar's quota warm-up.
- Merged feed: `cr:<name>:<timestamp>` / `db:<id>` ids, real timestamps,
  unread = label unread AND no receipt; mark-read always writes a
  receipt and best-effort patches the CR unless the role is Developer.
- Model moves from display-string times to epoch timestamps; the dev
  mock becomes a feed override.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Receipts are keyed by user x message key (no workspace): upstream writes
  account-level CRs into every namespace, and a person reads a message
  once; migration 0015 regenerated (unreleased) and the merge re-key
  narrowed to match.
- Route-level Go tests drive list and mark-read through a fake cluster,
  including the 403 (Developer RBAC) and 404 mappings, via package-level
  service seams; plus a status-mapping table test.
- Production store instance moves to `server-store.ts`; unused `isLive`
  removed; `WorkspaceSubscriptionRole` no longer splits a JSDoc from its
  symbol; "message center" wording replaced per CONTEXT.md.
- ADR-0067 records the client-observed quota trade-off.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…res (AIM-334)

The Notification Center speaks the product's language and covers the rest
of the catalog:

- Override table: the 8 fixed-name debt-ladder CRs (`debt-choice-*`,
  `workspace-debt-*`) render Brain-voiced title, body, and CTA (Top up
  balance / Renew plan → Billing Plan); unknown names keep upstream's text;
  display-only, the CR is never touched. Rows gain an optional CTA link
  that records the billing return route before navigating.
- Gift-only filter at the merge seam: a never-topped-up account holding
  nothing but gift credit does not see the low/critical balance tiers; the
  debt ladder is never filtered; unknown account state never hides a
  warning. Top-up history comes from the account's payment list
  (`loadHasToppedUp`), credits from the existing `credits/info` proxy.
- Two producers: the `$1 welcome gift` hint (`credit-hint:<user>`, once per
  user, observed from the inbox's own credits read) and subscription-change
  receipts (`subscription-change:<workspace>:<txid>`, one per settled
  upgrade/downgrade/cancellation, observed at the Plan view's settlement and
  cancel points; the last transaction names the change and keys it).
  Routes `gift-observation` and `subscription-change`; A1 copy aligned to
  the spec table with a View usage CTA.
- Dev fixtures through the real pipeline: the billing Dev Mock's cookie now
  drives the notification routes too — fixture CRs ride the feed as
  `platformItems` and the client merges, overrides, and filters them like
  live ones; three new scenarios (`payg-debt-deletion`, `payg-debt-final`,
  `quota-full`) complete the catalog coverage; the sidebar dev mock is
  deleted and the billing mock's panel entry mounts with the inbox.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014deH6UMJWcCYravZx19nvh
Productionize the status hint surface: one tinted, in-flow banner at the
top of the shell's content column that explains a holding billing state
and offers the fix, evaluated client-side from already-proxied
subscription, account, and quota reads.

- Pure model: payment-due > account-debt > quota-full > trial-expiry,
  single slot, dismiss/revive edge semantics, settled strings from the
  design doc; payment-due stages change copy and dates only.
- Subscription summary carries the Deletion Countdown stage and derived
  deadline (ADR-0063) plus the recovery voice; an expired Free trial
  asks for an upgrade, never a renewal.
- Currency-free balance read and a per-workspace quota read split out of
  the existing loaders; the Usage quota rows learn Pods.
- Dev fixtures gain `free-expiring` so every banner state is simulable.
- CONTEXT.md names the Status Hint.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…mestamp; code-review cleanups

- The Go read proxy exposes `version` (spec.timestamp, else metadata.generation,
  else creationTimestamp) and the client keys `cr:` ids by it: an in-place
  overwrite of a fixed-name CR is a new id no receipt covers, while a label
  patch never changes it. `timestamp` keeps its display/sort fallback.
- Pin the subscription-change rule: an upgrade over a still-scheduled
  downgrade is the next receipt; the superseded downgrade stays as history
  and its transaction never re-writes.
- Share what was duplicated: `lib/time` (DAY_MS/HOUR_MS/MINUTE_MS), the
  billing surface tint recipe, `MICRO_UNITS_PER_CURRENCY_UNIT`, and the
  `cr:`/`db:` id helpers in `notification-ids`.
- One authorize+parse seam for the body-carrying notification handlers; one
  JSON post helper in the client.
- `notificationCRItemSchema` is pinned to `NotificationCRItem`; `StatusHint.cta`
  is a `NotificationCTA`.
- Sidebar CTA renders through `AppButton`; the subscription-summary SWR key
  uses `kubeconfigCredentialKey` like its siblings.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@aimeritething
aimeritething force-pushed the aimeritething/dunning branch from a96db6e to 9fcd315 Compare August 27, 2026 17:09

@aimeritething aimeritething left a comment

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

P1 需求实现审查:以下三处会直接导致验收条件无法成立。

>
{item.title}
</span>
<span className="mt-0.5 block truncate text-muted-foreground text-xs">

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

[P1] 这里没有渲染 item.body,也没有详情视图。AIM-334 要求 8 个固定 CR 名称显示覆盖后的 title、body 和 CTA;当前用户只能看到标题、时间和 CTA,A1、D4、B5 的正文同样不可见。请在列表项或可打开的详情中实际呈现正文,并补充覆盖 UI 渲染的测试。

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Still open on current HEAD. feed-model now fills item.body for the 8 CR names and for A1/D4/B5, but NotificationRow still only renders item.title, then project · time, then the CTA — item.body is never mounted.

That leaves D4 and B5 as title-only (the useful sentence is the body), and the CR overrides’ Brain-voiced bodies unreachable. Please render the body in the row (or in an openable detail) and add a UI test that the override title and body are in the document.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Addressed. The body has rendered since 3a24a0989 (Severity redesign: click expands a clamped item.body under the title). f78062dc8 adds app-sidebar-notifications.interaction.test.tsx: all 8 CR overrides render title and body and CTA with the upstream copy absent, an unknown CR falls back to upstream title/body, and A1/D4/B5 show their body sentence; clicking a row marks it read and un-clamps the body.

for (const target of targets) {
next.add(target.id);
}
return next;

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

[P1] 已读回执失败在这里被吞掉,但前面已经把 ID 写入本地 readIds。结果是 UI 显示已读且用户无法重试,刷新或换设备后又恢复未读,不满足 AIM-332 的 ‘always writes a receipt’ 和跨会话持久化要求。请在失败时回滚或保留可重试状态,并处理 Mark all 超过 200 个 ID 的请求上限。

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Addressed. Failed receipts now remove the optimistic ids, toast, and leave the row clickable; postNotificationReadReceipts splits at NOTIFICATION_READ_BATCH_LIMIT (200) with client tests for 401 ids and a failed second batch.

const produced = await store.produce({
dedupeKey: creditHintDedupeKey(userUid),
kind: "credit-hint",
namespace,

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

[P1] D4 是账户级消息,设计要求在每个 workspace 可见。这里把全局去重消息写入首次观察到的 namespace,而 feed 又只按当前 namespace 查询,因此其他 workspace 永远看不到。请将账户级消息的存储或查询作用域与 workspace 消息分开,或者在各 workspace 的 feed 中投影同一条账户消息。

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Addressed. produce now sets userUid, listMessages is workspace rows (user_uid null) ∪ this person’s account rows, and the handler/store tests list the same hint from ns-b. Receipts stay uid-keyed, so a read in one workspace covers the others.

The remaining merge gap is separate: rekeyPersonalResources still does not move notification_messages.user_uid (comment on that call).

…batching

Review follow-ups on PR #305:

- The gift hint (catalog D4) is account-level but was stored under the
  first observing namespace and the feed only listed that namespace, so
  other workspaces never showed it. `notification_messages` gains a
  nullable `user_uid` (migration 0016); an inbox now lists its workspace's
  rows plus the person's account-scoped rows, and `markRead` attaches
  receipts to either. ADR-0067 records the two scopes.
- A failed read receipt was swallowed after the optimistic `readIds`
  write, leaving the item read for the session and unread after reload
  with no way to retry. The failure now rolls the optimistic ids back and
  says so with a toast.
- "Mark all" with more than 200 unread ids hit the route's request limit
  and failed silently. The client splits receipts into batches of
  `NOTIFICATION_READ_BATCH_LIMIT`, sent in order.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W33Mx9kYjBFoaQux2wKWJz
@norberia

Copy link
Copy Markdown
Contributor

@cursoragent review this

@cursor

cursor Bot commented Aug 28, 2026

Copy link
Copy Markdown

Reviewed #305 and posted findings on the PR.

Still blocking: AIM-334 body copy is computed (item.body for the 8 CR overrides, A1, D4, B5) but NotificationRow only shows title, time, and CTA. Gift-hint and subscription-change receipts are title-only. No UI test covers the override text.

Previous P1s that are fixed: mark-read rolls back and batches at 200; D4 is account-scoped and listed in every workspace.

New P2s: account merge re-keys receipts but not notification_messages.user_uid; a failed gift observation latches for the session; A1 only observes quota at sidebar mount / chat, so Status Hint can show quota-full while the inbox has no A1.

The hybrid CR proxy, CR id versioning, gift-only filter, and Status Hint model look sound.

Open in Web Open in Cursor 

Both migrations were introduced on this branch and never left it: 0015
created the sealai_notification schema and 0016 added user_uid to
notification_messages. Regenerated from schema.ts as a single 0015 so a
fresh database gets the final table shape in one step. The regenerated
`when` is later than the 0015 candidates on the other open PRs, so
drizzle's timestamp ordering stays correct whichever lands first.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KKrT7639VGVh1swM6dfFu8
@aimeritething
aimeritething force-pushed the aimeritething/dunning branch from 20e095f to ccb7807 Compare August 28, 2026 02:26
…verity

Replace the kind axis (and the producer-less deploy-* kinds) with a
Notification Severity derived from the message: the debt ladder and every
workspace-debt stage are critical, low-balance tiers and quota exhaustion
warn, receipts, hints, and announcements are info. The panel now renders
the body (clamped; click expands and marks read), colors icon and CTA chip
by severity with the Status Hint's tint recipe, filters with a SlidingToggle,
offers 'Mark all as read' as a text button, shows short relative time with
an absolute tooltip, and keeps a floor height so an empty inbox and a full
one read as the same surface. CONTEXT.md gains the Severity term.

Prototype variants live on prototype/notification-center-variants.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GStxUVYCv8odwpRXtc3gUy
@cursor

cursor Bot commented Aug 28, 2026

Copy link
Copy Markdown

Review

Two of the three previous P1s are fixed on current HEAD (ccb78075):

  • Mark-read now rolls the optimistic readIds back on a failed receipt, toasts, and splits batches at the 200-id limit.
  • D4 is account-scoped (user_uid set, inbox query is workspace rows ∪ this person's account rows), with store/handler tests that list the hint from a workspace that never observed it.

Still blocking AIM-334: item.body is computed for every CR override and for A1/D4/B5, but NotificationRow still only paints title + project · time + CTA. Gift-hint and subscription-change receipts are title-only without the sentence that carries the fact. There is still no UI test that the override copy is visible.

New gaps from the account-scope follow-up and the observation points are on the diff as [P2]s: identity merge does not re-key notification_messages.user_uid; a failed gift observation latches for the session; A1 only observes quota at sidebar mount / chat, so the Status Hint can show quota-full while the inbox has no A1.

The hybrid CR proxy + Brain store, CR id versioning by spec.timestamp else generation, gift-only filter, and Status Hint severity/dismiss model look sound.

.where(eq(onboardingProfiles.userUid, input.tombstoneUserUid))
.returning({ userUid: onboardingProfiles.userUid });

// Notification read receipts are keyed by (uid, message key): the

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[P2] Account-scoped messages are uid-keyed personal resources now (ADR-0067 / ADR-0059), but the merge sweep only re-keys receipts.

After a merge, a credit-hint row stays on the tombstone user_uid and disappears from the survivor’s inbox. The live dedupe key is credit-hint:<tombstone-uid>, so a later observation writes credit-hint:<survivor-uid> — a second welcome — rather than following the existing row.

Same class: store.produce has no requireCurrentIdentityBinding, so a gift observation that authorized before the merge can commit a tombstone-keyed row after the sweep. markRead already serializes against that; produce should too when userUid is set.

Please re-key (or collapse) notification_messages.user_uid like profiles/receipts, and pin it in identity-fingerprint-core.test.ts (receipt re-key is also untested there).

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Addressed in f78062dc8. rekeyPersonalResources now re-keys notification_messages.user_uid and the uid-bearing dedupe_key (credit-hint:<tombstone>credit-hint:<survivor>) so a later observation finds the existing row; where the survivor already holds a live row under that key the tombstone's is deleted (receipts cascade). store.produce takes account: { legacyWorkspaceActor, userUid } for account-scoped rows and runs requireCurrentIdentityBinding in the same transaction like markRead; the observation routes map a superseded binding to 401. identity-fingerprint-core.test.ts pins receipt re-key/dedup and message re-key + second-merge collapse; store.test.ts pins the produce refusal.

) {
return;
}
giftObservedFor.current = key;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[P2] The ref is set before the request. On failure the catch is empty, so this workspace+credential pair never retries for the rest of the session — D4 is missing until a reload or workspace switch.

Set the latch only after a successful POST (producer dedupe already makes a double-fire safe), or clear it in the catch so the next effect/poll can try again.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Addressed in f78062dc8. The latch clears in the catch, and the credits SWR onSuccess calls the same observeGift on every poll (SWR keeps equal data referentially stable, so an effect dep alone would not retry). The producer dedupes by user, so a repeat is harmless.

// without waiting on the desktop SDK (see project-workspace-layout).
// without waiting on the desktop SDK (see project-workspace-layout), and
// let the quota-exhausted producer observe the same snapshot.
useEffect(() => {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[P2] A1 is only observed here (once per appToken/kubeconfig/namespace) and on each chat turn. Status Hint polls workspace quota every 5 minutes and can show “CPU quota is full” while the inbox still has no A1 — and a recovery in that window never releases the live key, so the next crossing may not write a fresh entry.

The 30s quota cache used by the account popover also does not report observations. Please observe on the same cadence as the Status Hint quota read (or on cache refresh), so the banner and the inbox cannot disagree for minutes while the user never chats.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Addressed in f78062dc8. New observeWorkspaceQuotaForInbox (read snapshot → report → mutate every notifications-feed key) is called from the Status Hint quota SWR onSuccess on its 5-minute cadence, and the sidebar mount observation goes through it too — the banner and the inbox observe on the same poll, recoveries release, and a produced A1 lands immediately rather than on the next feed poll. Unit-tested with injected deps.

aimeritething and others added 5 commits August 28, 2026 11:00
…ing observations, panel UI test

- Account merge now re-keys account-scoped notification_messages (user_uid
  and the uid-bearing dedupe key) to the survivor, collapsing onto a live
  survivor row; produce() re-checks the identity binding for account-scoped
  rows like markRead does, and the observation routes map a superseded
  binding to 401.
- The gift-hint observation latch clears on failure and every credits poll
  is an observation opportunity, so a failed report no longer hides D4 for
  the session.
- Workspace quota is observed on the Status Hint's polling cadence through a
  shared observeWorkspaceQuotaForInbox that also refreshes the inbox, so the
  banner and the inbox see the same snapshot.
- Interaction test pins that the 8 CR overrides and A1/D4/B5 render title,
  body, and CTA in the panel.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LLcM4LbLxbAr6aFFUwNx2n
…v Mock cookie helper (AIM-338, AIM-339)

Switching a billing Mock Scenario used mutate(() => true), refiring every SWR
key on the page; with the cluster unreachable those requests hang and queue the
mock's own refetches behind them. The toggle now revalidates only the keys the
billing fixtures shape.

features/dev-mock/ carries the cookie grammar, the server-side resolver and the
panel source every route-handler Dev Mock shares; the billing mock is the first
consumer, behaviour unchanged. The production gate stays inlined at each route
so the fixture import can still be statically dropped.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012Nid6CzLVaZwacAdF5EhY9
… messages (AIM-339)

A second, independent Dev Mock (cookie sealai-notifications-dev-mock) serves
announcements, deployment outcomes and database events as fixture platform CRs,
layered over whatever answers below — the billing mock's feed or the real
handler — so the two mocks stack. Read receipts for fixture ids stay in memory;
the rest forward. Scenarios: announcement, deployment, db-event, mixed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012Nid6CzLVaZwacAdF5EhY9
… (AIM-336)

Cookie sealai-deploy-task-dev-mock serves one AI-runner fixture task per
scenario (running, blocked, failed, succeeded, cancelled): the task list and
projection stream answer with it so the dock chip appears, and the timeline
routes answer for its id only, leaving real tasks reachable. The SSE routes
send one snapshot frame and then only heartbeats, so the client never enters
its reconnect loop. Toggling reloads the page — the stores hold SSE
connections, not SWR keys.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012Nid6CzLVaZwacAdF5EhY9
Cookie sealai-chat-dev-mock serves the session bootstrap, thread list and
thread messages from three fixture threads (long with tool cards in every
state and a pinned chat context, short, empty). Sending still goes to the real
POST /api/chat, and the session keeps the real handler's Chat Billing Mode when
it answers so the billing mock keeps driving it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012Nid6CzLVaZwacAdF5EhY9
@zjy365
zjy365 merged commit 743c8b4 into main Aug 28, 2026
6 checks passed
@zjy365

zjy365 commented Aug 28, 2026

Copy link
Copy Markdown
Collaborator

补充两个当前实现中的后续问题(PR 已 merged,建议跟进修复):

  1. [P1] Status Hint dismissal 未按 workspace 隔离

statusHintDismissedAtom 使用固定的 sessionStorage key,只保存 quota-full / trial-expiry 等全局 ID。用户在 workspace A 关闭 quota banner 后切换到 workspace B,B 的同类 quota-full 状态也会被隐藏,直到当前 workspace 的状态恢复并重新触发。dismissal 应绑定 workspace(必要时加 user/credential scope)。

代码:status-hint-store.ts

  1. [P2] 并发 mark-read 失败会回滚另一个请求已成功的 optimistic 状态

失败回调无条件删除本次请求的 targetIds。当单条 mark-read 与 mark-all、或快速重复点击产生并发请求时,一个请求成功、另一个失败,失败请求会删掉成功请求写入的 read ID;失败路径也不会重新拉取 feed,通知可能错误显示为 unread,直到下一次轮询。建议按 mutation/version 跟踪 optimistic 更新,或失败后重新拉取 feed。

代码:use-notification-feed.ts

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

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants