From ba2e67f93cc3aa9326cc7388acaef9f3f8908b18 Mon Sep 17 00:00:00 2001 From: AstroHan Date: Sun, 16 Aug 2026 00:19:58 +0800 Subject: [PATCH 1/5] refactor(desktop): let the import page name the conversation it lost track of MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 设置 · 活动 · 导入任务 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 --- .../locales/external-session-import-copy.ts | 25 +++++-- .../settings/import-tasks-settings-page.tsx | 71 +++++++++++++------ 2 files changed, 67 insertions(+), 29 deletions(-) diff --git a/apps/desktop/src/renderer/locales/external-session-import-copy.ts b/apps/desktop/src/renderer/locales/external-session-import-copy.ts index b5497a97a8..45c2ea936d 100644 --- a/apps/desktop/src/renderer/locales/external-session-import-copy.ts +++ b/apps/desktop/src/renderer/locales/external-session-import-copy.ts @@ -31,7 +31,12 @@ type ExternalSessionImportCopy = { importFailedTitle: string; importFailedFallback: string; importOutcomeUnknownTitle: string; - importOutcomeUnknownDescription: string; + /** + * Takes the conversation names because this is the only place that can say + * which ones to go look for — the rows they came from may have been filtered + * or paged away by the time it renders. + */ + importOutcomeUnknownDescription: (names: readonly string[]) => string; }; const COPY = { @@ -44,7 +49,12 @@ const COPY = { emptyTitle: '没有可导入的对话', emptyDescription: '当前来源中没有找到符合条件的根对话。', unavailableTitle: '没有检测到支持的 Agent', - unavailableDescription: 'Maka 会在本机读取 Codex 的对话目录,不会修改其中的文件。', + // The title already says nothing was detected, so this says what to do + // about it instead of saying it again. It names Codex because the renderer + // only ever learns which sources *were* detected — nothing but a copy + // string can tell someone with none what to go install. The second half is + // the promise that earns the permission to read another app's files. + unavailableDescription: '在本机使用过 Codex 后,它的对话会出现在这里。Maka 只读取这些文件,不会修改。', loadFailedTitle: '无法读取外部对话', loadFailedFallback: '外部对话目录暂时无法读取,请重试。', retry: '重试', @@ -58,8 +68,8 @@ const COPY = { importFailedTitle: '导入失败', importFailedFallback: '该对话无法转换或保存。请检查来源后重试。', importOutcomeUnknownTitle: '需要确认导入结果', - importOutcomeUnknownDescription: - '导入结果暂时无法确认。请先在任务列表中查找这个对话;如果它已经出现,请不要再次导入。', + importOutcomeUnknownDescription: (names) => + `以下对话的导入结果无法确认:${names.map((name) => `「${name}」`).join('、')}。请先在任务列表中查找,已经出现的不要再次导入。`, }, en: { sourceLabel: 'Source', @@ -70,7 +80,8 @@ const COPY = { emptyTitle: 'No conversations to import', emptyDescription: 'No matching root conversations were found in this source.', unavailableTitle: 'No supported Agent detected', - unavailableDescription: "Maka reads Codex's local session directory without modifying its files.", + unavailableDescription: + 'Once Codex has been used on this machine, its conversations appear here. Maka only reads those files and never modifies them.', loadFailedTitle: 'Could not read external conversations', loadFailedFallback: 'The external session directory is temporarily unavailable. Try again.', retry: 'Retry', @@ -84,8 +95,8 @@ const COPY = { importFailedTitle: 'Import failed', importFailedFallback: 'This conversation could not be converted or saved. Check the source and try again.', importOutcomeUnknownTitle: 'Check the import result', - importOutcomeUnknownDescription: - 'Maka could not confirm whether the import completed. Look for this conversation in the task list first; if it is already there, do not import it again.', + importOutcomeUnknownDescription: (names) => + `Maka could not confirm the outcome of these imports: ${names.map((name) => `“${name}”`).join(', ')}. Look in the task list first, and do not import again anything that is already there.`, }, } satisfies UiCatalog; diff --git a/apps/desktop/src/renderer/settings/import-tasks-settings-page.tsx b/apps/desktop/src/renderer/settings/import-tasks-settings-page.tsx index bedcf00594..3381cdffcb 100644 --- a/apps/desktop/src/renderer/settings/import-tasks-settings-page.tsx +++ b/apps/desktop/src/renderer/settings/import-tasks-settings-page.tsx @@ -22,6 +22,21 @@ type CatalogState = { const EMPTY_CATALOG: CatalogState = { sessions: [], nextCursor: null }; +/** + * An import Desktop Main could neither confirm nor fail. + * + * The name is carried, not just the id, because the only thing that can tell + * the user which conversation to go look for is this record: by the time the + * banner renders, the row it came from may have been filtered or paged away. + * The adapter is carried because a source-native id is unique only within its + * own source. + */ +type UncertainImport = { + adapterId: string; + sourceSessionId: string; + name: string; +}; + /** * Settings · 活动 · 导入任务 — bring another local agent's conversations in as * Maka tasks. @@ -64,12 +79,11 @@ export function ImportTasksSettingsPage(props: { const [catalogError, setCatalogError] = useState(null); const [importError, setImportError] = useState(null); /** - * Conversations whose import neither succeeded nor failed — Desktop Main - * could not confirm the outcome. Re-importing one is how you end up with two - * copies of the same conversation, so those rows stay disabled for the rest - * of this page's lifetime and the banner says where to look instead. + * Re-importing a conversation whose outcome is unknown is how you end up with + * two copies of it, so its row stays disabled for the rest of this page's + * lifetime and the banner names it as the one to go look for. */ - const [uncertainIds, setUncertainIds] = useState>(new Set()); + const [uncertainImports, setUncertainImports] = useState([]); // Only the newest list request may write. Switching source or toggling the // archived filter while a page is in flight would otherwise land the old // source's rows under the new source's label. @@ -155,14 +169,14 @@ export function ImportTasksSettingsPage(props: { ); const importConversation = useCallback( - async (sourceSessionId: string) => { + async (session: ExternalSessionSummary) => { if (adapterId === null || importingId !== null) return; - setImportingId(sourceSessionId); + setImportingId(session.id); setImportError(null); try { const outcome = await window.maka.externalSessions.import({ adapterId, - sourceSessionId, + sourceSessionId: session.id, }); // Navigating away from Settings unmounts this page while the import is // still in Desktop Main's hands. The conversion itself completes and is @@ -170,7 +184,10 @@ export function ImportTasksSettingsPage(props: { // the user has left steering the shell somewhere they did not ask for. if (!mountedRef.current) return; if (!outcome.ok) { - setUncertainIds((current) => new Set(current).add(sourceSessionId)); + setUncertainImports((current) => [ + ...current, + { adapterId, sourceSessionId: session.id, name: session.name }, + ]); return; } props.onImported(outcome.session); @@ -288,11 +305,13 @@ export function ImportTasksSettingsPage(props: { )} - {uncertainIds.size > 0 && ( + {uncertainImports.length > 0 && ( entry.name), + )} /> )} @@ -336,13 +355,20 @@ export function ImportTasksSettingsPage(props: { variant="secondary" size="sm" isLoading={importingId === session.id} - isDisabled={importingId !== null || uncertainIds.has(session.id)} + isDisabled={ + importingId !== null || + uncertainImports.some( + (entry) => + entry.adapterId === adapterId && + entry.sourceSessionId === session.id, + ) + } // Returned, not discarded: Astryx's Button awaits a // promise-returning `clickAction` and drops repeat // clicks until it settles. `void`-ing it gave that // guarantee nothing to await, leaving double-submit to // the `importingId` state alone -- one render behind. - clickAction={() => importConversation(session.id)} + clickAction={() => importConversation(session)} label={importingId === session.id ? copy.importing : copy.import} // Every row's button reads 导入; only the accessible // name can say which conversation it imports. @@ -356,15 +382,16 @@ export function ImportTasksSettingsPage(props: { )} {catalog.nextCursor !== null && adapterId !== null && ( - -