Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 18 additions & 7 deletions apps/desktop/src/renderer/locales/external-session-import-copy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand All @@ -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: '重试',
Expand All @@ -58,8 +68,8 @@ const COPY = {
importFailedTitle: '导入失败',
importFailedFallback: '该对话无法转换或保存。请检查来源后重试。',
importOutcomeUnknownTitle: '需要确认导入结果',
importOutcomeUnknownDescription:
'导入结果暂时无法确认。请先在任务列表中查找这个对话;如果它已经出现,请不要再次导入。',
importOutcomeUnknownDescription: (names) =>
`以下对话的导入结果无法确认:${names.map((name) => `「${name}」`).join('、')}。请先在任务列表中查找,已经出现的不要再次导入。`,
},
en: {
sourceLabel: 'Source',
Expand All @@ -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',
Expand All @@ -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<ExternalSessionImportCopy>;

Expand Down
71 changes: 49 additions & 22 deletions apps/desktop/src/renderer/settings/import-tasks-settings-page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -64,12 +79,11 @@ export function ImportTasksSettingsPage(props: {
const [catalogError, setCatalogError] = useState<string | null>(null);
const [importError, setImportError] = useState<string | null>(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<ReadonlySet<string>>(new Set());
const [uncertainImports, setUncertainImports] = useState<readonly UncertainImport[]>([]);
// 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.
Expand Down Expand Up @@ -155,22 +169,25 @@ 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
// stored either way; what must not happen is a completion from a page
// 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);
Expand Down Expand Up @@ -288,11 +305,13 @@ export function ImportTasksSettingsPage(props: {
<Banner status="error" title={copy.importFailedTitle} description={importError} />
)}

{uncertainIds.size > 0 && (
{uncertainImports.length > 0 && (
<Banner
status="warning"
title={copy.importOutcomeUnknownTitle}
description={copy.importOutcomeUnknownDescription}
description={copy.importOutcomeUnknownDescription(
uncertainImports.map((entry) => entry.name),
)}
/>
)}

Expand Down Expand Up @@ -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)}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
label={importingId === session.id ? copy.importing : copy.import}
// Every row's button reads 导入; only the accessible
// name can say which conversation it imports.
Expand All @@ -356,15 +382,16 @@ export function ImportTasksSettingsPage(props: {
)}

{catalog.nextCursor !== null && adapterId !== null && (
<HStack hAlign="center">
<Button
variant="ghost"
size="sm"
label={loadingMore ? copy.loadingMore : copy.loadMore}
isDisabled={loadingMore}
onClick={() => void loadCatalog(adapterId, catalog.nextCursor ?? undefined)}
/>
</HStack>
/* Full width and `secondary`: as a centred ghost label this read as
a caption under the list rather than the control that extends it. */
<Button
variant="secondary"
size="sm"
width="100%"
label={loadingMore ? copy.loadingMore : copy.loadMore}
isDisabled={loadingMore}
clickAction={() => loadCatalog(adapterId, catalog.nextCursor ?? undefined)}
/>
)}
</VStack>
</SettingsSection>
Expand Down
Loading