Uh oh!
There was an error while loading. Please reload this page.
feat(desktop): redesign the models settings pages - #4682
Conversation
The 设置 → 模型 surface had four levels that each spoke a different dialect: a list with a bare count header, a catalog of 59 flat rows behind a category picker, an add form that sprang a second step on the user after they thought they were done, and a detail page laid out on a bespoke two-column grid with the enabled models hidden inside a MultiSelector and every capability declaration expanded into a wall of controls. Rebuild all four on the settings kit the other pages already use, following Astryx's own settings and settings-sidebar templates: - List: a SettingsSection with the add action in its header; an empty list shows the recommended providers as rows so a first run is one click from a provider's form. - Catalog: one search field over everything, and the providers below it as labeled groups (推荐 / 模型计划 / API / 聚合 / 本地). The category picker is gone because scrolling past a heading is the filter; typing collapses the groups into one flat result list. - Add (managed API-key route): a Stepper names the two steps up front (密钥 → 选择模型). The model step is a CheckboxList with 全选 / 清空, a filter past eight models, and a 默认模型 selector limited to the ticked models; the chosen default is placed first in the saved list, which is where the Host reads it from. - Detail: SettingsSection rows throughout. 连接 holds the name / key / endpoint rows plus a 连接状态 row (StatusDot + last test + 测试连接). 模型 lists every chat-capable model as a row with an enable Switch and fact tokens (context, 视觉, 思考); models Maka cannot describe carry a 声明能力 expandable editor on their own row, and the bulk thinking menu lives in the section's action cluster and saves on tick. 高级请求设置 and 删除连接 follow as sections. The hook gains `resetDraftProfile` (per-row Cancel) and lets `saveRelayProfiles` take an explicit table so a bulk tick applies and saves in one gesture. `SettingsExpandableRow` accepts a `beforeAction` node so a row can carry its Switch beside the built-in trigger, and a ReactNode label so the default badge survives on a declaring row. The MultiSelector-based EnabledModelManager, the DetailSection grid, and the copy those relied on are removed. Generated-by: Claude Code
7f185a4 to
6e35bf4Compare- 推荐 shortlist: only the providers a new user can finish without leaving Maka — the account sign-ins plus OpenCode Free / Go. The keyed API providers drop their `recommendedOrder`; they stay in their groups. - Model rows: the facts (id, context, 视觉, 思考) are one dotted supporting line instead of a token per fact, so a default badge and a context pill no longer stack under one name. - 声明能力 → 补充规格 (en: Add specs), and the undescribed hint reads 「Maka 没有这个模型的规格,请补充。」 - Renderer architecture ledger regenerated for the removed EnabledModelManager module. Generated-by: Claude Code
Wording a native reader would write rather than a translation: 配置参数 for the per-model editor (with 缺少该模型的参数信息,请手动配置。 as the hint), 订阅计划 for the plans group, 取消全选 beside 全选, 未测试, 未找到 匹配的…, and shorter helper sentences on the list, empty state, and default model picker. English strings follow the same meaning. Generated-by: Claude Code
…bt ratchet CI's renderer architecture check holds the four legacy settings files this branch rewrites to main's dependency and Hook counts. Shape the new pieces to fit that budget instead of growing it: the catalog groups become List headers, the recommended shortlist becomes a mode of the catalog component so the panel calls no extra hook, the panel writes SettingsSection's header out with the Astryx primitives it already imports, the model-step filter lives in the step's state, the capability editor takes its copy from its parent, and the 添加模型 dialog is one more value of the page's single "what is open" state. The Search icon import goes with it. While here, the 连接状态 row drops its StatusDot: the status is the row's supporting text, and only a failure wears a red Token. An EmptyProviders story covers the first-run shortlist. Generated-by: Claude Code
The inventory still listed provider-enabled-model-manager.tsx, which this branch deleted, and the component lists of the rewritten settings files. Generated-by: Claude Code
The catalog category picker is gone, so nothing reads categoryLabel or the category copy any more; Knip flags the export. Generated-by: Claude Code
jackwener
left a comment
There was a problem hiding this comment.
I reviewed exact head a6958eb2c04921196ce3a04dc64aa7783d6c2c37. One P1 and one P2, both accessibility. Not approving while the P1 stands; this is a feature, so the merge decision is a human's either way.
The rebuild itself is coherent — four levels that spoke four dialects now use the settings kit the rest of Settings already uses, and the two self-imposed rules hold: Badge appears only as the default marker (list row, detail title, default model), a red Token only for a failed connection, and capability facts are ordinary prose rather than a wall of Tokens.
P1 — focus is dropped when the managed key wizard advances to model selection
provider-add-form.tsx:491-610 swaps the whole tree for a different return once managedPhase becomes models, unmounting the key field and the submit button. The level focus hook (providers-panel.tsx:302-333) only runs when level or route changes, and this step leaves level at setup, so it does not re-run.
A keyboard user who presses 验证并选择模型 therefore lands on document.body and has to Tab from the top of the settings page to reach step two. The Stepper does expose aria-current="step" and a label, but the user's focus is not on it, so nothing announces.
What makes this worth fixing rather than noting is that this PR already solves the same problem elsewhere.settings-expandable-row.tsx:41-44,96-108 deliberately moves focus into the editor on expand and back to the trigger on collapse — the same unmount-and-replace situation, handled. The wizard step transition is the one path that did not get it.
Moving focus to the first control of the model step (the filter field or the checkbox list), and back to the key field when returning, is enough.
P2 — catalog search changes results without announcing them
provider-catalog-page.tsx:126-164 replaces the category picker with a single search field. Typing collapses the labelled groups into one flat list, which is a good simplification visually, but the field carries only a hidden label — there is no live region and no result count. An empty search renders the noMatch empty state, which a screen reader user whose focus is still in the input does not hear.
The comparison is inside this PR: the managed wizard announces its selection count with role="status" (provider-add-form.tsx:522) and its busy state with aria-live="polite" (:589,640). The same treatment is missing from catalog search, and from the model filters in the detail page (provider-connection-detail.tsx:712-734) and the wizard (:542-558). Keyboard users can still Tab into the list, which is why this is a P2 rather than a P1.
A polite live region beside the search field, reporting the match count or "no matches", closes it.
Deliberately not filed
SettingsExpandableRow has no aria-expanded / aria-controls, and that is correct here rather than an omission. This is a mode replacement, not a disclosure: the trigger itself unmounts, so there is no element left to carry aria-expanded="true". Moving focus into the editor and back to the button is the right contract for that pattern, and adding the attributes would state something the DOM does not support.
Evidence boundary: this is a static review against the source and the Astryx Stepper implementation. No browser or Storybook run, so the focus behaviour above is derived from the mount/unmount structure and the hook's dependencies rather than observed. The inventory, ratchet, provider-registry and Storybook coverage are being reviewed separately and are not covered here. test was still running when I posted.
简体中文
我审的是 a6958eb2c04921196ce3a04dc64aa7783d6c2c37。一条 P1、一条 P2,都是可及性问题。 P1 未修之前我不批;这是 feature,合并与否本来也该由人决定。
重建本身是自洽的——原先四层各说各的方言,现在统一到设置页其余部分已经在用的那套组件上;而且作者自定的两条规则守住了:Badge 只用作默认标记(列表行、详情标题、默认模型),红色 Token 只用于连接失败,能力事实是普通句子而不是一堵 Token 墙。
P1:托管密钥向导推进到「选择模型」时,焦点被丢掉。
provider-add-form.tsx:491-610 在 managedPhase 变为 models 时,把整棵树换成了另一份 return,密钥输入框和提交按钮被卸载。而层级焦点钩子(providers-panel.tsx:302-333)只在 level 或 route 变化时运行,这一步 level 仍然是 setup,所以它不会重跑。
于是键盘用户按下「验证并选择模型」之后,焦点落到 document.body,必须从设置页顶部重新 Tab 才能到达第二步。Stepper 确实带了 aria-current="step" 和标签,但用户的焦点不在它上面,所以什么都不会被播报。
让这条值得修而不只是记一笔的原因是:这个 PR 自己在别处已经解决过同样的问题。settings-expandable-row.tsx:41-44,96-108 特意在展开时把焦点送进编辑器、收起时送回触发器——同样是卸载替换的情形,那里处理了。向导的步骤切换是唯一没接上的一条路径。
把焦点移到模型步的第一个控件(筛选框或复选列表),返回时再回到密钥框,就够了。
P2:目录搜索改变了结果,却不播报。
provider-catalog-page.tsx:126-164 用一个搜索框取代了分类选择器。输入时带标题的分组会塌成一个扁平列表,视觉上是个不错的简化,但输入框只有一个隐藏标签——没有 live region,也没有结果计数。搜不到时渲染的是 noMatch 空状态,而焦点仍在输入框里的读屏用户听不到它。
对照就在这个 PR 内部:托管向导用 role="status" 播报选中计数(provider-add-form.tsx:522),用 aria-live="polite" 播报忙状态(:589,640)。同样的处理在目录搜索里没有,详情页的模型筛选(provider-connection-detail.tsx:712-734)和向导里的模型筛选(:542-558)也没有。键盘用户仍然可以 Tab 进列表,所以这是 P2 而不是 P1。
在搜索框旁加一个 polite 的 live region,报出匹配数或「未找到」,即可关闭。
特意没有报的一条
SettingsExpandableRow 没有 aria-expanded / aria-controls,这在这里是正确的,不是遗漏。它是模式替换而非 disclosure:触发器本身会被卸载,因此没有元素还能承载 aria-expanded="true"。把焦点移进编辑器再移回按钮,才是这种模式该有的契约;硬加那两个属性,等于声明一件 DOM 支撑不了的事。
证据边界:这是针对源码与 Astryx Stepper 实现的静态审查。没有跑浏览器或 Storybook,所以上面关于焦点的结论是从挂载/卸载结构和钩子依赖推出来的,不是观测到的。清单文件、架构棘轮、provider registry 与 Storybook 覆盖由另一席单独审查,不在本条范围内。我发布时 test 仍在运行。
Automated review notice: This comment was posted by an automated review agent operated by jackwener. It is not an independent human review and does not replace one.
…ounts Advancing the managed key wizard to model selection replaces the whole form while the settings route stays on `setup`, so the page's level focus never re-runs and the button the user pressed takes focus to `document.body`. A keyboard user then tabs from the top of Settings to reach step two. The step that arrives is the only thing that knows it arrived, so it owns the move, on mount: `OnboardingStepForm` focuses its own labelled region. The key step already had this through its field's `hasAutoFocus`. The three search fields rewrite their lists without moving focus and said nothing while doing it. Each now carries a live region reporting the match count, mounted before the text so the update is announced. Generated-by: Claude Code
Astro-Han
commented
Sep 3, 2026
Both are fixed in Focus on step advance — real, and reproducedConfirmed in the browser rather than from the structure: with focus on 验证并选择模型, clicking it leaves Two corrections to the account, neither of which changes that it needed fixing: It is pre-existing, not introduced here. The same reproduction runs identically against Severity is P2 on our scale. The reach is a normal keyboard path, but the consequence is bounded and recoverable, nothing is committed or externally visible, and the flow completes by tabbing. We reserve P1 for merge-blocking consequences. It is fixed here anyway because this PR is rebuilding the surface it lives on. Your root cause is right and worth stating precisely: focus-after-mode-swap has exactly two owners in this surface, one keyed on the route level and one on the row's The fix could not go where you suggested. The renderer debt ratchet forbids So the move belongs to the step that arrives, on mount, which is exactly when it arrives:
Filtered lists — real, fixed at all three sitesAll three confirmed: no live region tied to a result count. The We grade this P3 rather than P2: the list stays reachable by Tab, the no-match state carries a visible clear action, and nothing is persisted. Catalog search is also pre-existing, Each of the three now carries a polite live region reporting the match count, using the repository's existing
|
hqhq1025
left a comment
There was a problem hiding this comment.
Reviewed exact head 0fbfa9574f2e83df5ca988ac99abf2d916bfcfc4. I found one P2 and one P3.
This change rebuilds the four Models settings levels, changes managed onboarding selection/default behavior, replaces the detail editor layout, and makes bulk thinking-level choices save immediately. The earlier focus-loss and filtered-result announcement issues are fixed on this head.
Validation covered a clean install, Desktop production build and typecheck, renderer architecture (71/71), changed-file formatting/header/diff gates, Storybook build plus all 284 theme renders, 35 focused provider/profile tests, and real browser failure-path probes. The full Desktop suite completed with 2052 passes, 0 failures, and the same 8 cancellations in the untouched MCP OAuth deadline group. A conflict-free synthetic merge onto current main74a20f60c9a7 passed clean install, Desktop build/typecheck, renderer architecture, and the 35 focused tests. Hosted test was still running when this review was posted.
This is a feature change, so the final product and merge decision remains with a human reviewer.
Automated review notice: This comment was posted by an automated review agent operated by hqhq1025. It is not an independent human review and does not replace one.
| setRelayProfilesDirty(true); | ||
| setRelayProfileDrafts((current) => applyBulkThinkingLevel(modelIds, current, level, checked)); | ||
| setRelayProfileDrafts(next); | ||
| return saveRelayProfiles(next); |
There was a problem hiding this comment.
P2 — restore the visible bulk state when the immediate save fails. This commits next to relayProfileDrafts before awaiting persistence, while saveRelayProfiles only reports the rejection and returns false. The bulk menu derives its checkmarks from that draft, so a rejected bridge.update leaves the selected level visibly checked even after reopening the menu. I reproduced this in the rendered relay-detail story by forcing bridge.update to reject: the error toast appeared, but aria-checked remained true. The user is then shown an unpersisted setting as if it were saved, and cannot retry the same add gesture without first unchecking it. Roll the draft/dirty state back on rejection, or defer the visible commit until the write succeeds.
| <EmptyState | ||
| isCompact | ||
| title={copy.onboardingNoModelsMatch} | ||
| actions={<Button variant="ghost" size="sm" label={copy.onboardingClearAll} onClick={() => setFilter('')} />} |
There was a problem hiding this comment.
P3 — label this no-results action as clearing the search. In this branch the button only calls setFilter(''), but its visible and accessible name is onboardingClearAll (取消全选 / Deselect all). With a 9-model onboarding fixture and a no-match query, I observed the button saying 取消全选; activating it only cleared the query and did not change the selection. Use the clear-search/filter copy here so the announced command matches its effect.
jackwener
left a comment
There was a problem hiding this comment.
Re-reviewed at exact head 0fbfa9574f2e83df5ca988ac99abf2d916bfcfc4. The focus P1 is closed. One P2 introduced by the fix itself, and the earlier P3 is still open.
The wizard now keeps focus. Advancing from the key step to model selection no longer drops focus to document.body, so a keyboard user reaches step two without tabbing from the top of the settings page. That was the blocking item and it is genuinely fixed.
P2 — clearing a filter empties the live regions instead of announcing the restored count
The live regions added for the search and model filters are the right fix for the original gap: typing now announces how many results matched. But clearing the filter takes the opposite path — the three regions are emptied rather than updated, so a screen-reader user who deletes their query hears nothing at all, exactly at the moment the list grows back to its full size.
This is a smaller version of the same problem the P2 was raised about: a result set changes under the user and nothing says so. Announcing the restored total on clear, the same way a match count is announced on input, closes it.
Worth saying plainly because it is easy to miss in review: this was introduced by the fix. Adding a live region and then not driving it on one of its two transitions leaves a gap that did not exist before the region was there, and only shows up if you check the clear path as well as the type path.
P3 — the two OpenCode entries still appear twice
opencode-free and opencode-go carry both recommendedOrder and catalogGroup: 'plans', so the unfiltered catalog renders each once under Recommended and once under Plans. Unchanged from the previous head.
Evidence boundary: this is a re-review of the three findings and the delta, not a fresh pass over the whole change. test state and the merge gate are unchanged from what the PR page reports.
简体中文
在 0fbfa9574f2e83df5ca988ac99abf2d916bfcfc4 上复核。焦点那条 P1 已关闭。修复本身引入一条 P2,先前那条 P3 仍未解决。
向导现在保住了焦点。 从密钥步推进到选择模型步时,焦点不再掉到 document.body,键盘用户无需从设置页顶部重新 Tab 就能到达第二步。那是唯一的阻塞项,现在确实修好了。
P2:清空筛选时,live region 被清空,而不是播报恢复后的结果数
为搜索和模型筛选新增的 live region,对原来那个缺口是正确的修法:现在输入时会播报匹配了多少条。但清空筛选走的是相反的路径——那三个区域被清空而不是被更新,于是删掉查询词的读屏用户什么也听不到,而这恰恰是列表重新涨回完整规模的那一刻。
这是原先那条 P2 的一个更小版本:结果集在用户脚下变了,却没有任何东西说出来。 在清空时播报恢复后的总数,和输入时播报匹配数用同一套做法,就能关闭它。
这一点值得明说,因为在评审里很容易漏掉:它是被修复本身引入的。 加了一个 live region、却没有在它两个转换里的其中一个上驱动它,留下的缺口在这个区域存在之前并不存在;而且只有在你把「清空路径」和「输入路径」一起核的时候才会显形。
P3:两个 OpenCode 条目仍然出现两次
opencode-free 与 opencode-go 同时带有 recommendedOrder 和 catalogGroup: 'plans',所以未经筛选的目录会让它们各自在「推荐」下渲染一次、在「订阅计划」下再渲染一次。与上一个 head 相比没有变化。
证据边界:这是针对那三条发现与增量的复核,不是对整个改动重新过一遍。test 状态与合并门禁以 PR 页面所示为准。
Automated review notice: This comment was posted by an automated review agent operated by jackwener. It is not an independent human review and does not replace one.
jackwener
left a comment
There was a problem hiding this comment.
Approving at exact head 0fbfa9574f2e83df5ca988ac99abf2d916bfcfc4. No P0 or P1 — the blocking focus issue is fixed. Two non-blocking items stay open, both worth a follow-up.
The wizard keeps focus now. Advancing from the key step to model selection no longer drops focus to document.body, so a keyboard user reaches step two without tabbing from the top of the settings page. That was the one thing holding this up.
The rebuild behind it is also sound. Four levels that each spoke a different dialect now sit on the settings kit the rest of Settings uses, and the two self-imposed rules hold: Badge appears only as the default marker, a red Token only for a failed connection, and capability facts read as ordinary prose rather than a wall of Tokens. SettingsExpandableRow correctly has no aria-expanded — it is a mode replacement, not a disclosure, and the trigger itself unmounts, so there is no element left to carry the state. Moving focus into the editor and back to the button is the right contract there.
Open, not blocking
Clearing a filter empties the live regions instead of announcing the restored count. The regions added for search and the model filters do the right thing on input — they announce how many results matched. On clear they take the opposite path: the three regions are emptied rather than updated, so deleting a query announces nothing at exactly the moment the list grows back to full size.
This one is worth naming precisely because it was introduced by the fix. Adding a live region and then driving only one of its two transitions leaves a gap that did not exist before the region was there, and it only surfaces if the clear path is checked alongside the type path. Announcing the restored total on clear, the same way a match count is announced on input, closes it.
opencode-free and opencode-go still render twice in the unfiltered catalog, once under Recommended and once under Plans, because they carry both recommendedOrder and catalogGroup: 'plans'. Unchanged from the previous head.
This is a feature, so the merge decision is a human's regardless of this approval.
简体中文
在 0fbfa9574f2e83df5ca988ac99abf2d916bfcfc4 上批准。没有 P0/P1——那条阻塞性的焦点问题已修复。两条不阻塞的仍然开着,都值得后续处理。
向导现在保住了焦点。 从密钥步推进到选择模型步时,焦点不再掉到 document.body,键盘用户无需从设置页顶部重新 Tab 就能到达第二步。那是唯一卡住这个 PR 的东西。
它背后的重建本身也是稳的。原先四层各说各的方言,现在统一到设置页其余部分已在使用的那套组件上;而且作者自定的两条规则守住了:Badge 只作默认标记,红色 Token 只用于连接失败,能力事实读起来是普通句子而不是一堵 Token 墙。SettingsExpandableRow正确地没有aria-expanded——它是模式替换而非 disclosure,触发器本身会被卸载,没有元素还能承载那个状态;把焦点移进编辑器再移回按钮,才是这种模式该有的契约。
仍然开着、但不阻塞
清空筛选时,live region 被清空,而不是播报恢复后的结果数。 为搜索和模型筛选新增的那些区域,在输入时做的是对的——它们会播报匹配了多少条。但在清空时走的是相反的路径:三个区域被清空而不是被更新,于是删除查询词时什么也不会播报,而那恰恰是列表重新涨回完整规模的一刻。
这一条值得说准确,因为它是被修复本身引入的。加了一个 live region、却只驱动了它两个转换中的一个,留下的缺口在这个区域存在之前并不存在;而且只有在把「清空路径」和「输入路径」一起核查时才会显形。在清空时播报恢复后的总数,和输入时播报匹配数用同一套做法,就能关闭它。
opencode-free 与 opencode-go 在未筛选的目录里仍然渲染两次,一次在「推荐」下、一次在「订阅计划」下,因为它们同时带着 recommendedOrder 和 catalogGroup: 'plans'。与上一个 head 相比没有变化。
这是一个 feature,所以无论本批准如何,合并与否仍由人决定。
Automated review notice: This comment was posted by an automated review agent operated by jackwener. It is not an independent human review and does not replace one.
Uh oh!
There was an error while loading. Please reload this page.
Summary
设置 → 模型 was four levels that each spoke a different dialect: a list with a bare count header, a catalog of 59 flat rows behind a category picker, an add form that sprang a second step on the user after they thought they were done, and a detail page on a bespoke two-column grid with the enabled models hidden inside a MultiSelector and every capability declaration expanded into a wall of controls.
This rebuilds all four levels on the settings kit the other pages already use (
SettingsSection/SettingsRow/SettingsExpandableRow), following Astryx's ownsettingsandsettings-sidebartemplates and its component guidance (rows for dense data, Stepper for multi-step flows, Badge only for the default marker, a Token only where something failed).SettingsSectionwith 添加连接 in its header. An empty list shows the recommended providers as rows, so a first run is one click from a provider's form.Listunder its own heading. The category picker is gone: scrolling past a heading is the filter, and typing collapses the groups into one flat result list. 推荐 is now only what a new user can finish without leaving Maka: the account sign-ins (Codex, Copilot, xAI Grok) and OpenCode Free / Go; the keyed API providers lose theirrecommendedOrderin the registry and stay in their own groups.Steppernames both steps up front (密钥 → 选择模型). The model step is aCheckboxListwith 全选 / 清空, a filter past eight models, and a 默认模型 selector limited to the ticked models. The chosen default is placed first in the saved list, which is where the Host reads it from.Tokenon failure — followed by the last test message and time, with 测试连接 as the row's action. 模型 lists every chat-capable model as a row with an enableSwitchand one supporting line of facts (id, context window, 视觉, 思考); models Maka cannot describe carry a 配置参数 expandable editor on their own row (Save commits the table, Cancel resets only that row; the feat(desktop): show the model's reported context window beside the declared setting #4575 "该模型声明的窗口为 N tokens · 填入" hint lives inside it), and 批量设置思考档位 sits in the section's action cluster and saves on tick. 高级请求设置 and 删除连接 follow as sections.Removed with it:
EnabledModelManager(MultiSelector), theDetailSectiongrid, the catalog category filter and its copy, and the capability copy the old layout needed. The hook gainsresetDraftProfileand letssaveRelayProfilestake an explicit table;SettingsExpandableRowgainsbeforeActionand aReactNodelabel.The renderer debt ratchet holds the four legacy files this touches (
providers-panel,provider-catalog-page,provider-add-form,provider-connection-detail) tomain's dependency and Hook counts, so the new pieces are shaped to fit it rather than grow it: the catalog groups areListheaders, the list's section header is written out in the panel, the recommended shortlist is a mode of the catalog component, the model-step filter lives in the step's state, and the capability editor takes its copy from its parent.Focus and announcements follow the same rule as the rest of the surface. The wizard's second step replaces the whole form while the route stays on
setup, so the page's level focus never re-runs; the step takes focus itself on mount, throughOnboardingStepForm, and lands on its labelled region. Each of the three search fields carries a live region reporting its match count.Story DOM contracts (
data-maka-contract,data-connection-slug,data-provider,data-card-id, row class names) and the copy the story play functions assert on are unchanged. A newEmptyProvidersstory covers the first-run shortlist.Verification
npm run format,npm run lint,check:architecture(ledger regenerated with--write, check passes againstorigin/main) — clean.tsc -p tsconfig.renderer.jsonandtsc -p tsconfig.storybook.jsoninapps/desktop— clean.Product/Settings/Providersstories rebuilt withbuild-storybookand checked in the browser: list, empty list (shortlist play passes), detail, relay detail (bulk menu play passes; 配置参数 opens the row editor), catalog, and the managed onboarding stories (验证并选择模型 → model step → 添加连接, focus lands on the step). Filter announcements checked live: empty with no filter, a count matching the rendered rows, and the no-match sentence.BEFORE is
main'sstorybook-static, AFTER is this branch's, same stories and viewport.AI use
Select exactly one:
Tool(s) and scope: Claude Code (Claude Fable 5.1) designed the layout against the Astryx CLI docs and templates, wrote the component changes, the copy, the story updates and the before/after captures; the author reviewed and directed the work.
Checklist
Does this PR entail a change in behavior?