Repository navigation
Conversation
Option keys embedded the section and item positions. Search returns typed rows inside the Pages section, so when they merged in after the primary results, every folded-in database got a new key, the highlight kept its old index, and Enter inserted whatever had moved there, or expanded "N more" instead of inserting the highlighted link. - Key each option by its picker section kind plus a section-free navKey: person:<id>, page:<id>, database:<db>:<view|->, row:<db>:<row>, link:<url>, and kind plus title for date and reminder quick picks. Only a real duplicate inside one section gets #2, #3. mentionItemNavKey is exported for the later merge lanes. - Hold the highlight by key. When its key leaves the list, move it to the same object in another section, else to the first option. - Enter resolves the highlighted option by key, and the keydown handler is refreshed before paint. Enter on "N more" still highlights the first page it reveals. - The delivery fixture returns typed rows inside Pages, as Search does. New tests cover a database highlight across a rows merge, "N more", the reset when the item collapses away, a cross-section refresh, the create options and key stability under insertions. Implements MEN-WEB-02 of the recency-aware mention search plan. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011r7tx2DTWWjAV5oYkz3pnQ
…s and current-only caching
Web sent mention searches with no stream id, request time or abort signal. The typed primary and
rows-only requests left in one tick and could cancel each other in Search, superseded keystrokes
were never aborted and had no time limit, the cache-hit refresh wrote the cache before checking
that its answer was still current, and a bare @ asked for database rows that it then dropped.
- SearchMentions takes optional { signal, streamId, requestTime }, sent as x-request-stream-id
and x-request-time with the signal. A request with the headers that fails without a response,
as a CORS preflight rejected by an older self-hosted nginx does, is retried once without them.
If that retry is answered, the tab stops sending them.
- Each opening of the panel is a session with five lanes, each its own stream
web-mention-<session>-<lane>: bare, primary, rows, refresh and refresh-rows. Request times
strictly increase per lane, and issuing a request on a lane aborts the lane's previous one.
- A query change, closing the panel or unmounting it aborts the typed lanes. Bare and refresh
requests run to their deadline, so a complete answer still reaches the cache. Deadlines are
2.5 s typed and 5 s bare. An aborted request is not reported; one past its deadline fails as
a timeout through the existing failure state.
- The foreground and the refresh path cache an answer only while every request it came from is
current on its lane and was not aborted.
- A bare query no longer asks for database_row.
- Tests cover the lanes, the transport through the real axios chain, the fetch and the panel
lifecycle, including an emulated Search that supersedes by stream. A Playwright scenario
checks both headers and the bare include.
Implements MEN-WEB-01 of the recency-aware mention search plan.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011r7tx2DTWWjAV5oYkz3pnQ
…ailed rows retries Follow-up to f8d55c9 from its review. - Header fallback: a same-origin search sends no CORS preflight, so it never retries without the headers. A cross-origin search turns them off only after two separate rejections in a row whose retries without them were answered, and only for 10 minutes. Requests in flight together count once, and any answered search with the headers resets the count. - A failed rows retry no longer leaves the rows ticket it replaced in the answer, so the answer stays current: a deferred search settles instead of loading forever, and a cache refresh shows its answer again. The panel gates caching on lane currency and display on the effect. - Tests: transport cases for same origin, a transient failure, the token refresh, requests in flight together and the off period; panel cases for reopening a mounted panel, a late answer to aborted typed requests and both failed-retry cases; fetch tickets after a failed retry. - Playwright: record only answered mention requests, assert that the bare and typed streams share one session, and make the scenario opt-in (RUN_CLOUD_MENTION_STREAM_HEADERS) until the Cloud nginx that CI runs allows both headers in CORS preflights. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011r7tx2DTWWjAV5oYkz3pnQ
…ons, keep a fresh cache Web showed every failure as "No result": a primary error discarded the rows that answered, failed sections vanished, and a request past its deadline looked like an empty answer. The rows retry and the no-cache decision keyed on `partial`, which load-shed breadcrumb paths alone set. The cache revalidated on every hit, never expired, and outlived workspace events and sign-out. - A search never throws for one of its requests. Each ends as ok, failed, timeout, offline, retry_later or cancelled, and the answer merges what answered, with a state: complete, unavailable, busy (RetryLater on the primary or bare request) or offline. - The rows request is retried once, 500 ms later, only when its answer has a failed section with partial, or has the superseded shape. Path-shed partial answers, a disabled keyword index (failed without partial) and rejected rows requests are not retried. - Only a complete answer is cached: every request answered, no section failed, none superseded (partial without a section of a requested object kind), and every ticket current. - The panel offers "Suggestions unavailable · Retry", or "You're offline · Retry", as a keyboard-selectable option after the results, and counts RetryLater down in a busy row before sending again. "No result" shows only for a complete answer. Sections that answered stay on screen, a failed refresh keeps the cached answer, and Retry sends again at once. - Cache: an entry younger than 10 s is shown without a request. Folder, recent-view, roster and permission events mark entries stale, a revoked view is dropped from them, and sign-out or a panel on another workspace clears them. An answer to a request sent before a clear is dropped, and one sent before a stale mark is stored stale. - English strings for the new rows. Tests cover the outcomes and the retry rule, the cache's freshness, events and stamps, and every panel state, including the plan's cases. Implements MEN-WEB-03 of the recency-aware mention search plan. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011r7tx2DTWWjAV5oYkz3pnQ
…er and filter cache writes Follow-up to the review of ff6b47c..15d779d (MEN-WEB-03). - The rows retry also needs every other request to have answered completely. Behind a failed, stalled, busy or offline primary the answer cannot complete, and the retry kept the skeleton past the 2.5 s deadline: a stalled primary with failed rows turned unavailable at 3.2 s, or 5.6 s when the retry stalled too. It now does at 2.62 s, after two requests. - The superseded shape (partial, without a section of a requested object kind) makes only a rows-only answer unavailable. A primary or bare answer of that shape is almost always a People annotation that degraded over objects that matched nothing, because the panel aborts its own superseded requests, so it is shown without the Retry row. No answer of that shape is cached. - Sections that arrive while the rest of a search is pending are shown only when they have items, so a retry no longer blanks the rows of the previous attempt. - Cache writes carry a send sequence. An answer to a request sent before the stored one is ignored, as when the bare request of an earlier panel opening answers late. - A revoked view is left out of answers to requests sent before the revoke, in the cache and on screen, and an open panel drops it at once. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011r7tx2DTWWjAV5oYkz3pnQ
…fresh cooldowns Follow-up to the review of ff6b47c..e9ca903 (MEN-WEB-03). - A panel that reopens on a stale cached query while the refresh an earlier opening sent is still in flight joins that refresh. It now shows the refresh's outcome as its own: the fresh results, or the Retry, busy or offline row under the cached answer. Before, it only re-read the cache, so a refresh that failed, timed out, went offline or was asked to wait left the stale answer on screen with no row and no further refresh. startMentionSearchRefresh now resolves with the refresh's result for every caller that joins it. - Tests for the RetryLater paths of a cached answer, which no test reached: a refresh asked to wait shows the busy row under the cached answer and refreshes once when the cooldown ends; returning to a query whose cooldown still runs, with or without a cached answer, shows the seconds left and searches when it ends; and a reopened panel shows the answer, the failure or the cooldown of the refresh it joined. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011r7tx2DTWWjAV5oYkz3pnQ
…tline and this tab's edits Web holds the outline, the recent views, the trash and the roster, and knows what this tab created, renamed, deleted and opened, but the mention picker could reach none of it. A page made seconds ago or not yet indexed was missing from its results. - mentionLocalIndex.ts, a module store keyed by workspace. AppBusinessLayer gives it the outline, recent views and trash as references (useMentionLocalIndexSources); usePageOperations records creations, renames, moves to trash and restores; useViewOperations records opens; the roster cache gives every roster it loads. - A snapshot is built on the first read after a change and memoized: 5,000 views take about 4 ms in Jest. It applies the server's exclusions (spaces, a database's own views, embedded and dashboard-owned views, row pages and templates, the trash with its subtree) and carries server times, last_viewed_at, this tab's times and the navKey of the server item for each view. - A creation is listed in the same tick, until the outline lists it. A rename shows until the outline catches up or moves past it, a deletion until the trash list shows it, and an outline refresh keeps the session times. Opening a database's view counts for the database. - The store forgets a workspace on a switch, another account or unmount, and everything when the session ends. useMentionLocalSnapshot reads it without any context, so it works outside an AppProvider. Nothing renders it yet: MEN-WEB-09 merges it into the picker's results. - mentionUtils exports the page and database navKey formats that mentionItemNavKey uses. Implements MEN-WEB-07 of the recency-aware mention search plan. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011r7tx2DTWWjAV5oYkz3pnQ
…btrees in every lane A review of the local mention index (MEN-WEB-07) found that what it lists depended on when a snapshot was read, and that the subtree of a trashed page leaked through the recent and session lanes. - A deletion recorded in this tab was retired only by a read while the outline still listed the page and the trash list showed it. In the usual order the outline drops the page first, so a restore in another tab never showed the page again. The trash list showing the page now clears the deletion, and a deletion is not recorded when the trash list already shows the page. - A creation was marked as listed by the sources only during a read, so one moved to the trash elsewhere stayed listed from the session. Creations are now settled when the sources change or the creation is recorded, and a creation the trash list shows is never listed. - Rename overrides were dropped while a snapshot was built, so the name shown depended on reads. They are now settled when the outline or recent views change. Building a snapshot no longer writes to the session. - The outline walk now marks the whole subtree of a trashed or deleted page, and recent views and creations are checked through the parents that recent views and creations give, not only the direct parent. A deletion in this tab keeps the subtree the outline showed, so stale recent views below the page stay hidden after the outline drops it, until it is restored. - The trash list carries no subtree: a recent view two or more levels below a page trashed elsewhere still shows once the outline has dropped that page, unless the parents of recent views lead to it. The module header lists this limit. Settling walks the outline only while a creation or rename waits for it: a 0.7 ms median for 5,000 views in Jest. The snapshot build stays near 4 ms. Follow-up to MEN-WEB-07 of the recency-aware mention search plan. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011r7tx2DTWWjAV5oYkz3pnQ
…rces out of the local index A second review of the local mention index (MEN-WEB-07) found four problems. - A page deleted for good came back. The recent views and this tab's creations hid a view only while the trash list showed it, and the app reloads recent views only when Search or the Recent section opens. A view the trash list drops is now gone for those lanes, with its subtree, until the outline lists it again, this tab restores it, or reloaded recent views list it or a view below it (the server leaves trashed and deleted views out of them). The outline lane is unchanged, so a restore elsewhere still shows the page. At most 500 such views are kept per workspace. - A workspace switch gave the new workspace the previous one's outline and trash for a commit, because useWorkspaceData resets them in an effect. The sources hook now gives a new workspace none of the references the previous one left until they change. A test drives this through AppBusinessLayer. - deletePage read the subtree from the index's outline after the move resolved, when a folder notification may already have dropped it. It now also passes the view it found before the move. - The outline showing an earlier name this tab sent dropped a newer pending rename. A pending rename now keeps up to five names the sources may show before it: the one it replaced and the earlier ones this tab sent. Settling walks the outline once per change while a rename, creation or gone view waits for it: 0.7 ms median for 5,000 views in Jest. The snapshot build stays near 3.6 ms. Follow-up to MEN-WEB-07 of the recency-aware mention search plan. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011r7tx2DTWWjAV5oYkz3pnQ
appflowy
marked this pull request as ready for review
November 11, 2026 00:03
|
Sorry @appflowy, your pull request is larger than the review limit of 150,000 diff characters |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
These are the four Web tasks from the first batch of the recency-aware @-mention plan. The batch's server tasks are in AppFlowy-Cloud-Premium#1581, which now holds all three server batches (it replaced #1571 there).
None of these changes needs a server change, so the client works with older self-hosted servers as well as the current hosted one.
person:,page:,database:<db>:<view>,row:<db>:<row>,link:<url>. The highlight follows its item when typed rows merge in, and Enter resolves the option by its key, so it never inserts a shifted result.x-request-stream-id, with increasingx-request-timevalues.Deadlines are 2.5 s for typed queries and 5 s for bare
@. Changing the query, closing the panel or unmounting it aborts the typed lanes.An answer is cached only while it is still current. Bare
@no longer asks fordatabase_row.- A failed request keeps the sections the other requests returned.
- The panel shows "Suggestions unavailable · Retry" or "You're offline · Retry", and a countdown when the server says to retry later.
- The rows request is retried once, only when its section failed.
- The cache has a 10 s freshness window, is marked stale on workspace events, and drops revoked views.
- Sources: the outline, recent views, trash, the member roster, and this tab's own page creations, renames, moves to trash and opens.
- It excludes what the server excludes: spaces, a database's own views, embedded and dashboard-owned views, row pages, templates and trashed subtrees.
- 5,000 views take about 3.6 ms per snapshot.
Nothing renders it yet: MEN-WEB-09 will merge it into the picker.
Compatibility
nginx.confpredates the server's CORS fix rejects the two new headers in preflight. The request is then retried once without them, and the tab stops sending them. Same-origin deployments send no preflight.Validation
pnpm type-checkpasses. ESLint is clean with the repo's ignore list.error-catalog:checkpasses.This PR stays a draft until two things are done:
playwright/bdd/features/editor/mention-search.feature. It is written and type-checked but has not run yet, and.github/playwright-groups.jsonhas no group for it yet.document.mention.*: suggestionsUnavailable, offline, retry, retrying, busyRetryingIn.Product questions left open, with the conservative behavior shipped:
🤖 Generated with Claude Code