Skip to content

refactor(desktop): let the import page name the conversation it lost track of - #3075

Merged
Astro-Han merged 5 commits into
mainfrom
refactor/import-tasks-page-polish
Aug 16, 2026
Merged

refactor(desktop): let the import page name the conversation it lost track of#3075
Astro-Han merged 5 commits into
mainfrom
refactor/import-tasks-page-polish

Conversation

@Astro-Han

@Astro-Han Astro-Han commented Aug 15, 2026

Copy link
Copy Markdown
Contributor

Summary

设置 · 活动 · 导入任务 shipped in #3033 as a relocation, not a design, and never got a pass over how it reads. The import behaviour itself does not change here.

The page has one action, and taking it ends the page: a successful import calls closeSettings() + openSessionInChat() (app-shell.tsx:3408), so the page unmounts. Everything below follows from that.

One thing runs through most of this: the page kept source ids where it needed records. A bare id is a pointer into a list the user is free to replace — filter it, switch source, retry a failed page — so everything built on one either went mute or had to be protected by freezing the list. Both places that held ids now hold ImportAttempt ({ adapterId, sourceSessionId, name }), because "which conversation, in which source" is the same question whether an import is running or came back unconfirmed.

  • The warning banner now names the conversation whose outcome is unknown. When Desktop Main can neither confirm nor fail an import, that row stays disabled for the rest of the page's life — but the banner could only say 查找这个对话, with no way to say which one. It kept the source id and nothing else, and pointing at the row was not an option: by the time the banner renders, that row may be gone. Carrying the record, it names every unconfirmed import and stays true without the row.
  • The in-flight import is named above the list, and no control is frozen. importingId was the same bare id, and it fed the only thing that said which import was running: the row's own 正在导入…. Keeping that readable meant keeping the row alive, so the archived filter was frozen for it in refactor(desktop): rebuild the task rail on one shape and one vocabulary #3033, the source switch in 552156f, and 重试 and 加载更多 in 861fdcc — four controls carrying one rule, added a review round at a time. activeImport carries the record, so the page says 正在导入「X」 in the banner slot, where no catalog change can take it away, and says why every 导入 is disabled. The catalog is then free to move underneath an import and all four freezes are gone, along with the id collision the source-switch freeze was closing: the row predicate compares adapter and id together. (The freezes were raised by CodeRabbit across two rounds; this deletes them rather than completing the set.)
  • 加载更多 is a full-width secondary button, not a centred ghost label that read as a caption under the list. width="100%" is the shape Astryx documents for this (Button.tsx:332-335, :543). No "N remaining" is offered: the adapter returns a cursor, not a total.
  • The page-level import lock engages at all now. Astryx runs clickAction inside a React 19 async transition, and React holds a transition's state updates until the action settles — so the state committed only after the import was over, and nothing on the page could tell one was running. Plain onClick runs outside the transition and commits before the next event can be dispatched. 加载更多 moves back for the same reason: inside the transition loadingMore could neither disable it nor say 正在加载…. Reported by @M4n5ter; the defect predates this PR, but every claim this PR makes about in-flight state was inert without it. Single-flight itself stays, for the one reason that survives: a success calls onImported, which closes Settings, so a second import would be orphaned on a page the user can no longer see.
  • The no-adapter empty state no longer repeats its own title. The title already says nothing was detected; the description now says what to do about it, and keeps the read-only promise that earns the permission to look at another app's files. It still names Codex — the renderer only ever learns which sources were detected, so nothing but a copy string can tell someone with none what to install.

Deliberately not fixed

「再次导入同一个对话会创建一个独立的任务」 stays where it is. It only matters to someone asking "have I already imported this one", and the page unmounts on success, so it can never know the answer. Until an imported session persists its external origin, that sentence cannot become per-row information — and no other position beats a single visible line above the list. See #3081.

Concurrent imports are still not offered. A rework was tried and dropped early. Single-flight is not a limitation of the importer — Desktop Main can take two — it follows from the page ending on the first success, and lifting it would mean deciding what happens to the imports that were still running when Settings closed.

Appending a page still moves nothing in the list. Skeleton placeholder rows were tried and reverted: List does not forward aria-busy (it destructures a closed prop list with no rest spread, List.tsx:143-165), so the ARIA half was dead on arrival, and Skeleton has a built-in 1000ms animation delay against a local directory that answers in tens of milliseconds.

Refs #2984

Before / after

image

Verification

  • npm run format:check, npm run lint — pass
  • npm --workspace @maka/desktop run typecheck — pass
  • npm --workspace @maka/desktop run build-storybook + smoke:storybook — pass, 130 stories
  • Manual pass in Storybook over product-settings-pages--import-tasks: the unconfirmed banner naming one conversation, then two, then surviving an archived-filter toggle with both names intact and both rows still disabled; and --import-tasks-no-source for the rewritten empty state
  • Deferred-import pass with import stubbed to never settle. With clickAction: the source switch, the archived filter and every sibling row stay interactive and the row still reads 导入. With onClick: the banner reads 正在导入「Draft the release notes for 0.9.0」, that row spins, every other 导入 is disabled
  • Then the case the freezes existed for: with that import still in flight, turning the archived filter off reloads the catalog and takes its row away. The banner still names it and every remaining row is still disabled — screenshot below
  • Unconfirmed path unchanged against the fixture's failing import: one name in the banner, then two, both rows disabled, and the lock released between the two clicks

Not run: repository-wide tests, e2e.

Review focus

No test fails without this change. Every difference is visual or in what a control says, and the unconfirmed state is only reachable by clicking — scripts/storybook-visual-smoke.mjs disables every play, so a story for it would be skipped in CI. The existing ImportTasks fixture already returns { ok: false }, which puts that state one click away in Storybook; adding a play on top of that buys nothing CI can see. The evidence I have is the manual pass above.

This branch was reviewed adversarially by Codex and by independent Claude subagents across two rounds. That review removed a concurrent-import rework, skeleton rows, a per-row Badge, a per-row tooltip and the archived-filter unfreeze from earlier versions of this PR, and produced the state collapse this revision is built on. It is AI review and does not count as independent human review.

AI use

Select exactly one:

  • No generative tool made a substantive contribution
  • Generative tooling made a substantive contribution

Tool(s) and scope: Claude Code — wrote the implementation and copy changes and ran the verification above, under direction from the human contributor, who chose each option against rendered Storybook comparisons. Codex and Claude subagents performed the adversarial review described above. The commit carries a Generated-by trailer.

Checklist

  • Tests cover the change and fail without it — see Review focus above
  • Lint, format, typecheck and the affected suites pass locally

Does this PR entail a change in behavior?

  • Yes — described under Summary above
  • No

@coderabbitai

coderabbitai Bot commented Aug 15, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: bdd3b123-d2fc-4662-8df7-fa44461fe8d2

📥 Commits

Reviewing files that changed from the base of the PR and between 861fdcc and 0f28ea0.

📒 Files selected for processing (2)
  • apps/desktop/src/renderer/locales/external-session-import-copy.ts
  • apps/desktop/src/renderer/settings/import-tasks-settings-page.tsx

Included review availability: Your plan includes up to 3 reviews per rolling hour; 0 remain after this review.


📝 Walkthrough

Summary

This PR refactors the Desktop import tasks page without changing import behavior.

  • Preserves adapter, source session ID, and conversation name for uncertain imports.
  • Identifies affected conversations when catalog rows change or disappear.
  • Updates import progress and outcome copy in Chinese and English.
  • Changes “加载更多” to a full-width secondary button.
  • Improves no-adapter empty-state guidance.
  • Keeps rows and catalog controls locked during an active import.
  • Uses adapter-aware matching to prevent source-ID collisions.
  • Moves page-owned import state outside Astryx’s async transition.

The PR extends the existing import task state and action flow. It does not create a parallel import path or change the import source of truth.

The solution is the smallest coherent change. Conversation metadata and adapter-aware matching are necessary for accurate import tracking. Page-owned state is necessary because React 19 transitions delayed the lock when using Astryx’s clickAction. No deletion is identified without weakening behavior or regression coverage.

Reported validation includes formatting, linting, Desktop type checking, Storybook build, and Storybook smoke checks. Storybook states were also verified manually. Repository-wide tests and end-to-end tests were not run. Deferred-import interaction tests are unavailable because the repository lacks jsdom and React Testing Library, the fake DOM does not mount Astryx, and visual smoke tests disable Storybook play functions. Required-check status remains unverified without direct check evidence.

Review-relevant risks

No protected-area effect was identified in the current diff.

The person performing the merge reviews the final diff. A maintainer makes the final determination.

Walkthrough

The import page now tracks active and uncertain imports by adapter, source session, and conversation name. It disables matching rows and catalog controls, displays affected conversation names in English and Chinese, and clarifies Codex source-file behavior.

Changes

External import outcomes

Layer / File(s) Summary
Track import state and row availability
apps/desktop/src/renderer/settings/import-tasks-settings-page.tsx
The page stores detailed import records, passes complete sessions to import actions, blocks concurrent imports, disables matching rows, updates catalog controls, and uses asynchronous click handlers.
Show localized import details
apps/desktop/src/renderer/locales/external-session-import-copy.ts, apps/desktop/src/renderer/settings/import-tasks-settings-page.tsx
Import messages accept and display conversation names in English and Chinese. Source descriptions explain when Codex conversations appear and that Maka does not modify the files.

Estimated code review effort: 2 (Simple) | ~10 minutes

Merge Risk: ⚪ Minimal · up to 0f28e

This localized import-page update improves status messaging and disables conflicting controls while imports are active. The reported interaction risks are addressed, and no actionable merge-blocking risk remains after normal checks and review.

Possibly related issues

  • maka-agent/maka-agent issue 3081 — Both changes track adapter and source-session data for external imports.
  • maka-agent/maka-agent issue 2984 — Both changes modify the import-tasks settings page and its import copy.

Possibly related PRs

Suggested reviewers: m4n5ter

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Ai Use Disclosure ✅ Passed The PR selects substantive generative use, names Claude Code and its scope, and all five introduced commits contain the valid Generated-by: Claude Code trailer.
Title check ✅ Passed The title clearly describes a primary change: naming conversations whose imports lose confirmed outcomes.
Description check ✅ Passed The description covers the problem, implementation, verification results, AI use, checklist status, and tests that were not run.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch refactor/import-tasks-page-polish

Comment @coderabbitai help to get the list of available commands.

@Astro-Han
Astro-Han force-pushed the refactor/import-tasks-page-polish branch from 605f1a8 to b8254eb Compare August 15, 2026 15:17
@Astro-Han Astro-Han changed the title refactor(desktop): let the import page's rows and paging speak for themselves refactor(desktop): let the import page's locked row say why it is locked Aug 15, 2026
…track of

设置 · 活动 · 导入任务 has one action, and taking it ends the page: a successful
import closes Settings and navigates to the new task. The one state that
outlives that action is an import Desktop Main could neither confirm nor fail,
and the page could not say which conversation it belonged to. It kept only the
source id, so the warning banner had to speak in the abstract — 查找这个对话,
with no way to say which one. Pointing at the row was not an option either: by
the time the banner renders, the archived filter or a new page may have taken
that row away.

Carry the record instead of the id. `uncertainImports` holds the conversation's
name and its adapter, so the banner names every import whose outcome is
unknown, and it stays true when the row is gone. The adapter is part of the
record because a source-native id is unique only within its own source;
matching on a bare id would eventually mark an unrelated row in another source.

加载更多 was a centred ghost label under the list, which read as a caption
rather than the control that extends it. It is now a full-width secondary
button — the shape Astryx documents `width="100%"` for — and it uses
`clickAction` like the row buttons, so repeat clicks are dropped by the promise
rather than by state that lands one render later.

The no-source empty state repeated its own title. It now says what to do about
it instead, and keeps the read-only promise that earns the permission to look
at another app's files.

Not fixed, deliberately: the page still cannot tell you a conversation has
already been imported. A Maka session records nothing about the external
conversation it came from, and a successful import unmounts this page, so no
page-local memory survives to accumulate. The list keeps the one honest
sentence it can — importing the same conversation again creates an independent
task — and #3081 tracks the provenance change a per-row answer would need.

Generated-by: Claude Code
@Astro-Han
Astro-Han force-pushed the refactor/import-tasks-page-polish branch from b8254eb to ba2e67f Compare August 15, 2026 16:20
@Astro-Han Astro-Han changed the title refactor(desktop): let the import page's locked row say why it is locked refactor(desktop): let the import page name the conversation it lost track of Aug 15, 2026
@Astro-Han
Astro-Han marked this pull request as ready for review August 15, 2026 16:30

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1


ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 91484343-88cf-4454-b7d6-c815c77de55c

📥 Commits

Reviewing files that changed from the base of the PR and between 62cded2 and ba2e67f.

📒 Files selected for processing (2)
  • apps/desktop/src/renderer/locales/external-session-import-copy.ts
  • apps/desktop/src/renderer/settings/import-tasks-settings-page.tsx

Comment thread apps/desktop/src/renderer/settings/import-tasks-settings-page.tsx Outdated
…light

The archived filter is already frozen during an import because either control
replaces the catalog, which takes away the row the in-flight import belongs to
while every remaining row is still disabled by `importingId !== null` — a page
of dead buttons with the one spinner gone. The source switch does exactly the
same thing and was not frozen.

It also removes the collision `importingId` is open to: it holds a bare source
id, and those are unique only within their own source, so a second adapter
carrying the same id could show 正在导入… on an unrelated row. Freezing the
switch closes that at the source rather than widening the key, which is the
smaller change and the one that also keeps the row visible.

No user-visible change today: the switch only renders when more than one
adapter is detected, and Codex is the only one.

Generated-by: Claude Code
@Astro-Han
Astro-Han requested a review from M4n5ter August 15, 2026 16:45

@M4n5ter M4n5ter left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

English

Blocking: the page-level import lock never becomes active

apps/desktop/src/renderer/settings/import-tasks-settings-page.tsx:275

The new source-selector guard depends on importingId !== null, but imports are started through Astryx Button.clickAction. Button executes that callback inside a React 19 asynchronous transition, so the initial setImportingId(session.id) at line 174 is deferred until the import Promise settles. Before that transition commits, the finally block queues setImportingId(null) at line 198.

Consequently, the page never observes a non-null importingId while the import is pending. I reproduced this with the repository's actual Astryx Button and React 19.2.8:

importingId while pending: null
action button aria-busy: true
source control disabled: false

The clicked button protects itself through its internal pending state, but the source selector, archived filter, and other import rows remain interactive. A user can therefore replace the catalog during an import or start another import—the exact state inconsistency this revision is intended to prevent. This also means the previously resolved cross-adapter review thread is not actually addressed.

Please move ownership of the page-level import lock outside the clickAction transition. One small approach is to use a synchronous ref as the mutual-exclusion guard and start the import from a normal onClick, allowing setImportingId to commit immediately while retaining same-tick duplicate protection.

Please also add a deferred-import interaction test that keeps the import Promise unresolved and verifies that the source selector, archived filter, and every other import row remain unavailable until the Promise settles.

简体中文

Blocking:页面级导入锁从未真正进入 active 状态

apps/desktop/src/renderer/settings/import-tasks-settings-page.tsx:275

新增的来源选择器禁用逻辑依赖 importingId !== null,但导入操作是通过 Astryx 的 Button.clickAction 启动的。Button 会在 React 19 的异步 transition 中执行该回调,因此第 174 行的 setImportingId(session.id) 会被延迟到导入 Promise 结束后才提交;在该 transition 提交前,第 198 行的 finally 又会排入 setImportingId(null)

因此,在导入处于 pending 状态期间,页面从未观察到非空的 importingId。我使用仓库实际的 Astryx Button 和 React 19.2.8 进行了复现:

pending 期间的 importingId:null
操作按钮 aria-busy:true
来源控件 disabled:false

被点击的按钮会通过自身的 pending 状态保护自己,但来源选择器、已归档筛选器以及其他导入行仍然可以操作。用户因此可以在导入期间替换 catalog,或者启动另一项导入——这正是本次修改声称要消除的状态不一致。这也意味着之前已 resolve 的跨 adapter review thread 实际上尚未解决。

请将页面级导入锁的所有权移出 clickAction transition。一个较小的方案是:使用同步 ref 作为互斥 guard,并从普通 onClick 启动导入,使 setImportingId 能立即提交,同时保留对同一 tick 内重复点击的防护。

还请增加一条 deferred-import 交互测试:让导入 Promise 保持 unresolved,并验证在 Promise settle 之前,来源选择器、已归档筛选器和其他所有导入行均不可操作。

The row import button ran `importConversation` through Astryx's
`clickAction`, which Astryx executes inside a React 19 async transition.
React holds a transition's state updates until the action settles, so
`setImportingId` committed only after the import was already over.

For the whole duration of an import the page therefore behaved as if
none were running: the row kept reading 导入 instead of 正在导入…, the
other rows stayed clickable, and both catalog controls stayed live --
including the archived filter, frozen for that purpose since #3033, and
the source switch frozen in 552156f. A second import could be started,
or the catalog replaced out from under the in-flight one.

`clickAction` earns the clicked button its own pending state and nothing
more. This lock is a page fact, so the page owns it: plain `onClick`
runs outside the transition and `importingId` commits before the next
event can be dispatched. `加载更多` moves back for the same reason --
inside the transition `loadingMore` could neither disable it nor say
正在加载….

Verified in Storybook against a never-settling import: with `onClick`
the source switch, the archived filter and every other row are inert
until it settles, and a second click on any of them is dropped.

Generated-by: Claude Code

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
apps/desktop/src/renderer/settings/import-tasks-settings-page.tsx (1)

399-408: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Disable all catalog controls during an active import.

isDisabled={loadingMore} leaves “加载更多” active while importingId !== null. The catalog retry button at Line 300 through Line 304 has the same gap. If a load-more request failed, retry calls loadCatalog without a cursor. Line 125 then clears catalog, so the active import row can disappear before the import settles.

Add importingId !== null to both controls.

Proposed fix
                   <Button
                     variant="ghost"
                     size="sm"
                     label={copy.retry}
+                    isDisabled={importingId !== null}
                     onClick={() => void loadCatalog(adapterId)}
                   />
-              isDisabled={loadingMore}
+              isDisabled={loadingMore || importingId !== null}

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: d854fdb2-ee42-4596-bc81-a085a6197793

📥 Commits

Reviewing files that changed from the base of the PR and between 552156f and 771e69b.

📒 Files selected for processing (1)
  • apps/desktop/src/renderer/settings/import-tasks-settings-page.tsx

Included review availability: Your plan includes up to 3 reviews per rolling hour; 1 remains after this review.

@Astro-Han

Astro-Han commented Aug 16, 2026

Copy link
Copy Markdown
Contributor Author
English

Confirmed. Reproduced against the repository's own Astryx Button and React 19.2.8, with a page fixture whose import never settles and two adapters detected:

clickAction:  source switch live | archived filter live | sibling row live | row label 导入
onClick:      aria-disabled      | disabled             | disabled         | row label 正在导入…

Two corrections to the framing. The defect predates this PR: clickAction on the row button came in with #3033, and this PR only changed the argument. And it was never confined to the source switch — 正在导入… has never rendered, and the archived filter has been frozen in name only since #3033. What this PR did add is 加载更多 moving from onClick to clickAction, which made loadingMore inert too.

Fixed in 771e69b: clickAction earns the clicked button its own pending state and nothing else, so plain onClick runs outside the transition and the page state commits before the next event can be dispatched.

Then the page changed shape, and it is worth saying why, because what you reviewed is no longer what is there.

Making the lock work exposed what it was for. importingId was a bare source id — a pointer into a list this page lets the user replace — and the only thing that said which import was running was the row's own 正在导入…. Keeping that readable meant keeping the row alive, which is why the archived filter was frozen for it in #3033, the source switch in 552156f, and 重试 and 加载更多 in 861fdcc after CodeRabbit found the same hole again. Four controls, one rule, added a review round at a time.

This PR had already made the opposite choice one state over: uncertainImports carries { adapterId, sourceSessionId, name } precisely because the row may be gone by the time the banner renders. 0f28ea0 makes it once more. activeImport carries the same record, the page says 正在导入「X」 in the banner slot where no catalog change can take it away, and all four freezes are deleted — along with the cross-adapter id collision, since the row predicate now compares adapter and id together. That thread of yours and CodeRabbit's is closed by removal rather than by a fifth guard.

The case those freezes existed for now passes without them: with an import still in flight, turning the archived filter off reloads the catalog and takes its row away, and the banner still names it while every remaining row stays disabled.

I did not add the synchronous ref. Measured on the fixed build: one macrotask after the first click — the earliest a second real click can arrive — both row buttons already carry disabled, and further clicks are dropped. Three clicks dispatched inside a single task do get through (["conv-a", "conv-a", "conv-b"]), but no browser delivers user clicks that way. A ref would be a second representation of a fact the rendered disabled already enforces. If you know a path that dispatches two activations in one task, I will take it — that would change the answer.

On the deferred-import test: there is no seam that runs it in CI today. The repository has no jsdom and no @testing-library/react. The only React harness is apps/desktop/src/main/__tests__/fake-dom.ts, which documents that it is deliberately not jsdom, and none of the tests beside it mounts @astryxdesign/core. Storybook play functions are all disabled by scripts/storybook-visual-smoke.mjs, so a play-based interaction test would be skipped. Playwright E2E reaches the settings surface, but holding an import pending needs a new e2e seam in Desktop Main, and e2e/fixtures.ts deliberately keeps test data out of production main.

So the test is not something I can add without first building one of those. My preference is a renderer component-test harness that can mount Astryx, because this bug class — a design-system prop whose pending contract is button-local — will reach other pages. I would rather do that as its own change than fold new test infrastructure into this one, but I will do it here if you want the fix and its test to land together. Your call.

简体中文

已确认。 使用仓库自身的 Astryx Button 与 React 19.2.8 复现:页面 fixture 的 import 永不 settle,检测到两个 adapter。

clickAction:  来源开关可用 | 归档筛选可用 | 同级行可用 | 行标签 导入
onClick:      aria-disabled | disabled    | disabled  | 行标签 正在导入…

有两处需要更正。该缺陷早于本 PR:行按钮上的 clickAction#3033 引入,本 PR 只改了实参。它也从不限于来源开关——正在导入… 从未渲染过,归档筛选自 #3033 起就是名义上的冻结。本 PR 确实新增了一处:加载更多onClick 改为 clickAction,使 loadingMore 同样失效。

已在 771e69b 修复:clickAction 换来的只是被点按钮自身的 pending 状态,因此改用普通 onClick,在 transition 之外执行,页面状态在下一个事件派发前即已提交。

之后页面的形状变了,这里有必要说明原因,因为你评审过的版本已经不是现在的版本。

把锁修好之后,才看清它是为什么存在的。importingId 是一个裸的 source id——指向一个本页允许用户替换的列表——而唯一能说出「哪个导入在跑」的,只有该行自己的 正在导入…。要让它可读,就得让那一行活着,于是归档筛选在 #3033 为它冻结,来源开关在 552156f,重试与加载更多在 861fdcc——在 CodeRabbit 再次发现同一个洞之后。四个控件,一条规则,每轮评审加一个。

而本 PR 在相邻的另一个状态上早已做过相反的选择:uncertainImports 携带 { adapterId, sourceSessionId, name },正是因为横幅渲染时那一行可能已经不在了。0f28ea08 把同一件事再做一次。activeImport 携带同样的记录,页面在横幅位显示 正在导入「X」——目录的任何变化都带不走它——四处冻结随之全部删除;跨 adapter 的 id 碰撞也一并消失,因为行谓词现在同时比较 adapter 与 id。你和 CodeRabbit 的那条线是被移除关闭的,而不是加上第五道 guard。

那些冻结原本要防的场景,现在没有冻结也成立:导入仍在进行时关闭归档筛选,目录重载并带走该行,横幅依然指名该对话,其余每一行仍为禁用。

我没有加同步 ref。 在修复后的构建上实测:第一次点击后的下一个 macrotask(真实第二次点击最早的到达时机),两个行按钮已带 disabled,后续点击被丢弃。在同一个 task 内派发三次点击确实能穿过(["conv-a", "conv-a", "conv-b"]),但浏览器不会这样投递用户点击。ref 会成为「已由渲染出的 disabled 强制」这一事实的第二份表示。如果你知道某条路径能在一个 task 内派发两次激活,请告诉我——那会改变结论。

关于 deferred-import 测试:目前仓库没有能在 CI 中运行它的接缝。 没有 jsdom,也没有 @testing-library/react。唯一的 React harness 是 apps/desktop/src/main/__tests__/fake-dom.ts,其注释写明刻意不使用 jsdom,且同目录下没有任何测试 mount @astryxdesign/core。Storybook 的 playscripts/storybook-visual-smoke.mjs 全部禁用,基于 play 的交互测试会被跳过。Playwright E2E 能到达设置界面,但要让一次导入保持 pending,需要在 Desktop Main 中新增 e2e 接缝,而 e2e/fixtures.ts 刻意将测试数据挡在生产 main 之外。

因此这条测试我无法在不先搭建其中之一的前提下加上。我倾向于建一个能 mount Astryx 的 renderer 组件测试 harness,因为这类缺陷——设计系统某个 prop 的 pending 契约仅限按钮自身——还会出现在其他页面。我更希望把它作为独立改动,而不是把新的测试基建塞进本 PR;但如果你希望修复与测试一起落地,我也可以在这里做。由你决定。

@Astro-Han
Astro-Han requested a review from M4n5ter August 16, 2026 03:28
重试 on the catalog error banner calls `loadCatalog` without a cursor,
which resets the catalog to page one. Clicking it during an import takes
away the row that import belongs to -- the same hole the archived filter
and the source switch are frozen against, at a third control that was
missed because each of them carried the rule separately.

The rule now has a name. `catalogFrozen` says once why an in-flight
import freezes the catalog, and the controls that can replace it read
it. 加载更多 appends and would keep the row, but it is frozen too: a page
where every control but one holds still has to be read control by
control before it can be trusted, and an import settles in well under a
second.

Reported by CodeRabbit. Verified in Storybook with a failing `list` and
a never-settling `import`: 重试 is live while nothing is importing and
disabled once something is, alongside 加载更多, the filter and the switch.

Generated-by: Claude Code
@Astro-Han

Astro-Han commented Aug 16, 2026

Copy link
Copy Markdown
Contributor Author

@coderabbitai Both real, fixed in 861fdcc.

重试 is the one that matters: it calls loadCatalog without a cursor, so it resets the catalog to page one and the in-flight import's row disappears. That is the same hole the archived filter and the source switch are already frozen against — a third control, missed because each of them carried the rule separately.

So the fix names the rule rather than adding a fourth copy of it: catalogFrozen says once why an in-flight import freezes the catalog, and every control that can replace it reads that.

加载更多 I fixed for a different reason than the one given. It appends, so it would keep the row and the invariant holds either way. It is frozen because a page where every control but one holds still has to be read control by control before it can be trusted, and nothing is lost by waiting out an import that takes well under a second.

Verified in Storybook with a failing list (to raise the error banner) and a never-settling import: 重试 is enabled while nothing is importing, and disabled once something is, alongside 加载更多, the filter and the switch.


Superseded by 0f28ea0. Finding a third and fourth control carrying the same rule was the signal that the rule was in the wrong place. importingId was a bare source id — a pointer into a list this page lets the user replace — so the freezes existed only to keep the in-flight import's row alive. activeImport now carries { adapterId, sourceSessionId, name } and the page names the running import above the list, where no catalog change can reach it. catalogFrozen and all four freezes are deleted; 重试 and 加载更多 are live during an import again, and the hole is closed because nothing depends on the row any more.

@coderabbitai

coderabbitai Bot commented Aug 16, 2026

Copy link
Copy Markdown

Tip

For best results, initiate chat on the files or code changes.

@Astro-Han Thanks for the precise verification.

catalogFrozen correctly centralizes the catalog-replacement invariant. It protects 重试, the archived filter, and the source switch while an import is active.

Your 加载更多 rationale is also valid. It does not remove the active row, but disabling it preserves consistent page-level controls during an import.

The Storybook cases verify both required states. The findings are addressed.


✏️ Learnings added
Learnt from: Astro-Han
URL: https://github.com/maka-agent/maka-agent/pull/3075

Timestamp: 2026-08-16T03:35:11.810Z
Learning: In `apps/desktop/src/renderer/settings/import-tasks-settings-page.tsx`, catalog controls that can replace the external-session catalog must be disabled while an import is active. `重试` calls `loadCatalog` without a cursor and resets the catalog to page one, which can remove the in-flight import row. Use the shared `catalogFrozen` state for the source switch, archived filter, retry action, and related catalog-replacement controls instead of duplicating this condition.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.


🧠 Learnings used
Learnt from: Astro-Han
Repo: maka-agent/maka-agent PR: 3075
File: apps/desktop/src/renderer/settings/import-tasks-settings-page.tsx:365-378
Timestamp: 2026-08-15T16:45:30.608Z
Learning: In `apps/desktop/src/renderer/settings/import-tasks-settings-page.tsx`, the adapter `SegmentedControl` and archived filter must remain disabled while `importingId !== null`. Changing either control reloads or replaces the external-session catalog, which can remove the row for an in-flight import. Source session IDs are unique only within an adapter, so freezing adapter selection also prevents cross-adapter ID collisions in the active import UI.

You are interacting with an AI system.

@M4n5ter M4n5ter left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

LGTM

… catalog

`importingId` was a bare source id — a pointer into a list the page lets
the user replace. Everything it fed said "which import is running", and
all of that lived on the row, so the row had to be kept alive: the
archived filter was frozen for it in #3033, the source switch in
552156f, 重试 and 加载更多 in 861fdcc. Four controls, one rule, added
one review round at a time.

This PR already made the same fix for the unconfirmed banner, whose ids
had the same problem. Making it once more removes the reason for all
four freezes: `activeImport` carries the adapter, the id and the name,
so the page can say 正在导入「X」 above the list, where no catalog
change can take it away, and name why every 导入 is disabled. The
catalog is then free to move underneath an import. `catalogFrozen` and
its four readers are gone, and so is the id collision the source-switch
freeze was closing — the row predicate compares adapter and id together.

`ImportAttempt` is one record type for both slots, since "which
conversation, in which source" is the same question whether the import
is running or came back unconfirmed.

Verified in Storybook: with an import that never settles, filtering its
row away leaves the banner naming it and every remaining row disabled;
with the fixture's failing import, one name then two in the unconfirmed
banner, both rows disabled, and the lock released in between.

Generated-by: Claude Code
@coderabbitai

coderabbitai Bot commented Aug 16, 2026

Copy link
Copy Markdown

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

file="apps/desktop/src/renderer/settings/import-tasks-settings-page.tsx"

ast-grep outline "$file" --items all --view expanded
printf '\n--- active import and catalog-control references ---\n'
rg -n -C 5 '\b(activeImport|catalogFrozen|importingId|loadCatalog|handleImport)\b' "$file"

Length of output: 7067


@Astro-Han Confirmed from the current implementation.

activeImport contains adapterId, sourceSessionId, and name. The page-level status banner identifies the active import even when a catalog reload removes its row.

isSameAttempt compares the adapter and source session ID. All import buttons remain disabled while activeImport exists. 重试 and 加载更多 can remain active because the active-import state does not depend on catalog membership.

The earlier catalogFrozen finding no longer applies. Thanks for the clarification.


⛔ Learnings removed
Learnt from: Astro-Han
Repo: maka-agent/maka-agent PR: 0
File: :0-0
Timestamp: 2026-08-16T03:35:11.810Z
Learning: In `apps/desktop/src/renderer/settings/import-tasks-settings-page.tsx`, catalog controls that can replace the external-session catalog must be disabled while an import is active. `重试` calls `loadCatalog` without a cursor and resets the catalog to page one, which can remove the in-flight import row. Use the shared `catalogFrozen` state for the source switch, archived filter, retry action, and related catalog-replacement controls instead of duplicating this condition.

Learnt from: Astro-Han
Repo: maka-agent/maka-agent PR: 3075
File: apps/desktop/src/renderer/settings/import-tasks-settings-page.tsx:365-378
Timestamp: 2026-08-15T16:45:30.608Z
Learning: In `apps/desktop/src/renderer/settings/import-tasks-settings-page.tsx`, the adapter `SegmentedControl` and archived filter must remain disabled while `importingId !== null`. Changing either control reloads or replaces the external-session catalog, which can remove the row for an in-flight import. Source session IDs are unique only within an adapter, so freezing adapter selection also prevents cross-adapter ID collisions in the active import UI.

You are interacting with an AI system.

@Astro-Han
Astro-Han merged commit eea5cd8 into main Aug 16, 2026
12 checks passed
@Astro-Han
Astro-Han deleted the refactor/import-tasks-page-polish branch August 16, 2026 06:20
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants