feat(desktop): import several conversations in one pass - #4437
Merged
Conversation
Settings › 导入任务 imported one conversation per press. A Claude Code
directory here lists 1128 of them, so bringing over a project's worth meant
finding a row, pressing 导入, waiting for the page to navigate away, coming
back, and finding the next one.
Every row now carries a checkbox and a master row sits above the list:
[▬] 已选 15 / 16 [ 导入所选 ]
[ ] cyberpet ~/CyberPet · 19:13 [ 导入 ]
[✓] cyberpet ~/CyberPet · 18:57 [ 导入 ]
No mode to enter: this page exists to pick conversations out of a
directory, so the boxes are the page. The per-row 导入 button is untouched
and still navigates to what it imported.
**A batch stays here and reports.** A single import ends by calling
`onImported`, which closes Settings and opens the new task — there is no
sensible task to open after importing twelve, and leaving would strand the
rows that did not land. The summary counts four outcomes apart:
- `imported` — landed.
- `duplicated` — landed, and that conversation now exists twice.
Re-importing is how one is refreshed, so those rows are selectable on
purpose, but a user who marked twelve and reads "imported 12" deserves
to know which kind they were.
- `failed` — the Host rejected it. One rejection is not the batch's answer
for every row after it, so the run continues.
- `unknown` — the call did not answer. Only a catalog read settles whether
the conversion landed, and folding this into failures is what would
invite the retry that makes a second copy. These reach the existing
unconfirmed banner, which names every one of them and resolves them a
press at a time.
**Sequential, and not because the Host cannot take two.**
`external-session-coordinator.ts` dedupes per (adapter, source id) and
converts different conversations concurrently without complaint. The
reasons are on this page: recovery re-reads the whole catalog window an
attempt came from, so overlapping attempts would race that read; a progress
count is only true while one thing is happening; and the page has no useful
answer to "which of these five failed" if they fail together.
**"All" means the rows on screen.** The catalog is paged behind a cursor
and narrowed by a search term and the archived filter, so a box that
reached past the rows beneath it would start an import whose size the user
never saw.
## Paying for it in a file that may not grow
`renderer-architecture.json` ratchets this page's debt against `main`: no
new stateful hooks, no new bridge call sites, no new module specifiers.
Three consolidations paid for the feature, and each is worth having on its
own terms:
- `sourceLoading` + `sourceResolved` become one `sourceProbe`. They were
always written together and `loading && resolved` was never a state this
page could be in, with nothing enforcing it.
- `activeImport` plus the batch's progress and summary become one
`importRun`. A single import and a batch are one activity and cannot
overlap; three states invited combinations that cannot happen.
- The marked set is INTERSECTED at read time instead of pruned in an
effect. That is not only a hook saved: an effect leaves a render where a
row the catalog just dropped is still counted, still named by the
confirm, and still importable. A derived read has no such window.
Both import paths now go through one `requestImport`, which the ledger also
required — and which is what two callers of the same bridge method wanted
anyway.
The page's `hookCalls` (31), `bridgePaths` (4) and `dependencyPaths` (19)
are unchanged from `main`.
The selection itself is `packages/ui/src/listed-selection.ts` — a marked
subset of whatever a surface is listing, with no opinion about what the ids
identify. Every function returns its input unchanged when nothing moved,
because identity is the render boundary for a list of rows.
## Self-review
Reviewing the diff before opening this turned up three defects, all fixed:
1. A stale doc comment left stacked above its replacement, describing an
`activeImport` that no longer exists. Its one load-bearing fact — that
the single-import limit is about navigation and not about the Host — is
merged into the new comment.
2. Every selected row spun during a batch, including rows the run had not
reached and rows it had already finished. A spinner claims something is
happening now, so the batch state carries the id actually converting and
only that row reads as busy. A test drives a parked first conversion and
asserts exactly one row is spinning.
3. The batch records an unanswered import without running recovery, unlike
the single path — deliberate, because recovery re-reads the whole
catalog window and doing that between conversions would interleave N
full reads with the writes the batch is making. It was undocumented,
which is how a deliberate divergence becomes a bug report.
## Verification
packages/ui 304 tests, 304 pass
apps/desktop 1851 tests, 1849 pass, 2 fail (environmental)
check:renderer-architecture --base pass, all three metrics level
astryx inventory + tests, astryx:theme --check pass
knip (desktop, ui), lint, format:check, typecheck pass
check:app-shell-hooks, check:asf-headers pass
Driven in the running app over a real Claude Code directory of 16
conversations: master box to 16/16, untick one to 15/16 with the master
reading indeterminate, and back.
The two failing `app-update-attestation` cases (TUF/Sigstore in the
packaged Electron runtime) fail on this machine with and without these
changes.
Reverting each behaviour fails a test: the duplicate count, treating a
rejection as an import, a two-state master box, and the queued-row spinner.
Generated-by: Claude Opus 5 via Claude Code
abhinav-phi
pushed a commit
to abhinav-phi/maka
that referenced
this pull request
Sep 1, 2026
Settings › 导入任务 imported one conversation per press. A Claude Code
directory here lists 1128 of them, so bringing over a project's worth meant
finding a row, pressing 导入, waiting for the page to navigate away, coming
back, and finding the next one.
Every row now carries a checkbox and a master row sits above the list:
[▬] 已选 15 / 16 [ 导入所选 ]
[ ] cyberpet ~/CyberPet · 19:13 [ 导入 ]
[✓] cyberpet ~/CyberPet · 18:57 [ 导入 ]
No mode to enter: this page exists to pick conversations out of a
directory, so the boxes are the page. The per-row 导入 button is untouched
and still navigates to what it imported.
**A batch stays here and reports.** A single import ends by calling
`onImported`, which closes Settings and opens the new task — there is no
sensible task to open after importing twelve, and leaving would strand the
rows that did not land. The summary counts four outcomes apart:
- `imported` — landed.
- `duplicated` — landed, and that conversation now exists twice.
Re-importing is how one is refreshed, so those rows are selectable on
purpose, but a user who marked twelve and reads "imported 12" deserves
to know which kind they were.
- `failed` — the Host rejected it. One rejection is not the batch's answer
for every row after it, so the run continues.
- `unknown` — the call did not answer. Only a catalog read settles whether
the conversion landed, and folding this into failures is what would
invite the retry that makes a second copy. These reach the existing
unconfirmed banner, which names every one of them and resolves them a
press at a time.
**Sequential, and not because the Host cannot take two.**
`external-session-coordinator.ts` dedupes per (adapter, source id) and
converts different conversations concurrently without complaint. The
reasons are on this page: recovery re-reads the whole catalog window an
attempt came from, so overlapping attempts would race that read; a progress
count is only true while one thing is happening; and the page has no useful
answer to "which of these five failed" if they fail together.
**"All" means the rows on screen.** The catalog is paged behind a cursor
and narrowed by a search term and the archived filter, so a box that
reached past the rows beneath it would start an import whose size the user
never saw.
## Paying for it in a file that may not grow
`renderer-architecture.json` ratchets this page's debt against `main`: no
new stateful hooks, no new bridge call sites, no new module specifiers.
Three consolidations paid for the feature, and each is worth having on its
own terms:
- `sourceLoading` + `sourceResolved` become one `sourceProbe`. They were
always written together and `loading && resolved` was never a state this
page could be in, with nothing enforcing it.
- `activeImport` plus the batch's progress and summary become one
`importRun`. A single import and a batch are one activity and cannot
overlap; three states invited combinations that cannot happen.
- The marked set is INTERSECTED at read time instead of pruned in an
effect. That is not only a hook saved: an effect leaves a render where a
row the catalog just dropped is still counted, still named by the
confirm, and still importable. A derived read has no such window.
Both import paths now go through one `requestImport`, which the ledger also
required — and which is what two callers of the same bridge method wanted
anyway.
The page's `hookCalls` (31), `bridgePaths` (4) and `dependencyPaths` (19)
are unchanged from `main`.
The selection itself is `packages/ui/src/listed-selection.ts` — a marked
subset of whatever a surface is listing, with no opinion about what the ids
identify. Every function returns its input unchanged when nothing moved,
because identity is the render boundary for a list of rows.
## Self-review
Reviewing the diff before opening this turned up three defects, all fixed:
1. A stale doc comment left stacked above its replacement, describing an
`activeImport` that no longer exists. Its one load-bearing fact — that
the single-import limit is about navigation and not about the Host — is
merged into the new comment.
2. Every selected row spun during a batch, including rows the run had not
reached and rows it had already finished. A spinner claims something is
happening now, so the batch state carries the id actually converting and
only that row reads as busy. A test drives a parked first conversion and
asserts exactly one row is spinning.
3. The batch records an unanswered import without running recovery, unlike
the single path — deliberate, because recovery re-reads the whole
catalog window and doing that between conversions would interleave N
full reads with the writes the batch is making. It was undocumented,
which is how a deliberate divergence becomes a bug report.
## Verification
packages/ui 304 tests, 304 pass
apps/desktop 1851 tests, 1849 pass, 2 fail (environmental)
check:renderer-architecture --base pass, all three metrics level
astryx inventory + tests, astryx:theme --check pass
knip (desktop, ui), lint, format:check, typecheck pass
check:app-shell-hooks, check:asf-headers pass
Driven in the running app over a real Claude Code directory of 16
conversations: master box to 16/16, untick one to 15/16 with the master
reading indeterminate, and back.
The two failing `app-update-attestation` cases (TUF/Sigstore in the
packaged Electron runtime) fail on this machine with and without these
changes.
Reverting each behaviour fails a test: the duplicate count, treating a
rejection as an import, a two-state master box, and the queued-row spinner.
Generated-by: Claude Opus 5 via Claude Code
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
Settings › 导入任务 imported one conversation per press. A Claude Code directory here lists 1128 of them, so bringing over a project's worth meant finding a row, pressing 导入, waiting for the page to navigate away, coming back, and finding the next one.
Every row now carries a checkbox and a master row sits above the list:
There is no mode to enter: this page exists to pick conversations out of a directory, so the boxes are the page. The per-row 导入 button is untouched and still navigates to what it imported.
A batch stays here and reports. A single import ends by calling
onImported, which closes Settings and opens the new task. There is no sensible task to open after importing twelve, and leaving would strand the rows that did not land. The summary counts four outcomes apart:Sequential, and not because the Host cannot take two.
external-session-coordinator.tsdedupes per(adapterId, sourceSessionId)and converts different conversations concurrently without complaint. The reasons are on this page: recovery re-reads the whole catalog window an attempt came from, so overlapping attempts would race that read; a progress count is only true while one thing is happening; and the page has no useful answer to "which of these five failed" if they fail together."All" means the rows on screen. The catalog is paged behind a cursor and narrowed by a search term and the archived filter, so a box that reached past the rows beneath it would start an import whose size the user never saw.
Paying for it in a file that may not grow
renderer-architecture.jsonratchets this page's debt againstmain: no new stateful hooks, no new bridge call sites, no new module specifiers. Three consolidations paid for the feature, and each is worth having on its own terms.sourceLoading+sourceResolvedbecome onesourceProbe. They were always written together, andloading && resolvedwas never a state this page could be in with nothing enforcing it.activeImportplus the batch's progress and summary become oneimportRun. A single import and a batch are one activity and cannot overlap; three states invited combinations that cannot happen.Both import paths now go through one
requestImport, which the ledger also required — and which is what two callers of the same bridge method wanted anyway.hookCallsbridgePathsdependencyPathsThe selection itself is
packages/ui/src/listed-selection.ts— a marked subset of whatever a surface is listing, with no opinion about what the ids identify. Every function returns its input unchanged when nothing moved, because identity is the render boundary for a list of rows.Self-review
Reviewing the diff before opening this turned up three defects, all fixed here.
activeImportthat no longer exists. Its one load-bearing fact — that the single-import limit is about navigation, not about the Host — is merged into the new comment.Verification
Driven in the running app over a real Claude Code directory of 16 conversations: master box to 16/16, untick one row to 15/16 with the master reading
indeterminate, and back.The two failing
app-update-attestationcases (TUF/Sigstore in the packaged Electron runtime) fail on this machine with and without these changes.Each behaviour fails a test when reverted:
duplicatedindeterminatestateRelationship to #4365
No file overlaps that PR. It adds multi-select to the Session rail; this adds it to the import catalog. They share the checkbox-and-master-box shape and nothing else — the rail acts on Maka's own sessions through
SessionNavigationServices, this acts on foreign conversations throughwindow.maka.externalSessions. Either can land first; whichever lands second will needrenderer-architecture.jsonand the Astryx inventory regenerated, which is mechanical.AI use
Select exactly one:
Tool(s) and scope: Claude Opus 5 via Claude Code — design, implementation, tests, and this description. Reviewed and verified locally by me; the commit carries a
Generated-bytrailer.Checklist
Does this PR entail a change in behavior?