You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@maka.apache.org by wy...@apache.org on 2026/08/25 14:22:14 UTC
(maka) branch fix/nonblocking-previous-session-recovery created (now 61a87a209)
This is an automated email from the ASF dual-hosted git repository.
M4n5ter pushed a change to branch fix/nonblocking-previous-session-recovery
in repository https://gitbox.apache.org/repos/asf/maka.git
at 61a87a209 fix(desktop): make recovery notice non-blocking
This branch includes the following new commits:
new 61a87a209 fix(desktop): make recovery notice non-blocking
The 1 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails. The revisions
listed as "add" were already present in the repository and have only
been added to this reference.
---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@maka.apache.org
For additional commands, e-mail: commits-help@maka.apache.org
(maka) 01/01: fix(desktop): make recovery notice non-blocking
Posted by wy...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
M4n5ter pushed a commit to branch fix/nonblocking-previous-session-recovery
in repository https://gitbox.apache.org/repos/asf/maka.git
commit 61a87a209c1c74c9e7a102578b8267ad64a07e99
Author: Wang <m4...@gmail.com>
AuthorDate: Tue Aug 25 22:21:58 2026 +0800
fix(desktop): make recovery notice non-blocking
Start Runtime Host and the Renderer before presenting prior-run recovery evidence. Acknowledge that evidence when the live Renderer receives a compact toast, while keeping its bounded diagnostics copyable for the rest of the process.
Generated-by: Codex
---
.../__tests__/main-process-diagnostics.test.ts | 61 +++++++++++++++++++
.../main-process-recovery-journal.test.ts | 55 -----------------
.../__tests__/native-diagnostic-dialog.test.ts | 58 ------------------
.../src/main/desktop-diagnostics-ipc-main.ts | 26 +++++++++
.../src/main/main-process-recovery-journal.ts | 15 -----
apps/desktop/src/main/main.ts | 68 +++++++---------------
apps/desktop/src/main/native-diagnostic-dialog.ts | 34 -----------
apps/desktop/src/preload/bridge-contract.d.ts | 2 +
apps/desktop/src/preload/preload.ts | 10 ++++
apps/desktop/src/renderer/app-shell.tsx | 32 ++++++++++
.../src/renderer/locales/shell-remaining-copy.ts | 10 ++++
apps/desktop/src/renderer/use-shell-appearance.ts | 3 +
12 files changed, 166 insertions(+), 208 deletions(-)
diff --git a/apps/desktop/src/main/__tests__/main-process-diagnostics.test.ts b/apps/desktop/src/main/__tests__/main-process-diagnostics.test.ts
index e3ed15452..ec6b42eba 100644
--- a/apps/desktop/src/main/__tests__/main-process-diagnostics.test.ts
+++ b/apps/desktop/src/main/__tests__/main-process-diagnostics.test.ts
@@ -22,6 +22,7 @@ import { test } from 'node:test';
import type { IpcMain } from 'electron';
import {
registerDesktopDiagnosticsIpc,
+ registerPreviousMainProcessDiagnosticsIpc,
type DesktopDiagnosticsIpcDeps,
} from '../desktop-diagnostics-ipc-main.js';
import {
@@ -275,6 +276,66 @@ test('copies Desktop diagnostics while Runtime Host is unavailable', async () =>
assert.match(clipboard, /Diagnostics unavailable: Runtime Host disconnected/);
});
+test('acknowledges one previous-run notice while keeping its diagnostics copyable', async () => {
+ type IpcHandler = Parameters<Pick<IpcMain, 'handle'>['handle']>[1];
+ const handlers = new Map<string, IpcHandler>();
+ let acknowledgements = 0;
+ let clipboard = '';
+ registerPreviousMainProcessDiagnosticsIpc({
+ ipcMain: {
+ handle(channel, handler) {
+ handlers.set(channel, handler);
+ },
+ },
+ evidence: {
+ run: {
+ startedAt: '2026-08-20T00:00:00.000Z',
+ appVersion: '0.1.10',
+ buildMode: 'packaged',
+ buildCommit: 'b'.repeat(40),
+ electronVersion: '43.2.0',
+ nodeVersion: '24.0.0',
+ chromeVersion: '144.0.0',
+ platform: 'win32',
+ arch: 'x64',
+ osRelease: '10.0.26100',
+ },
+ snapshotAt: '2026-08-20T00:10:00.000Z',
+ logs: ['previous main log with api_key=very-secret-token'],
+ },
+ acknowledge: () => {
+ acknowledgements += 1;
+ },
+ environment: () => environment,
+ mainLogs: () => ['current main log'],
+ runtimeHostProcessLogs: () => ['current Runtime Host exit'],
+ resolveActiveRuntimeHost: () => {
+ throw new Error('Previous-run diagnostics must remain Desktop-only');
+ },
+ resolveRuntimeHost: () => {
+ throw new Error('Previous-run diagnostics must not resolve a task Host');
+ },
+ writeClipboard: (value) => {
+ clipboard = value;
+ },
+ });
+
+ const take = handlers.get('diagnostics:takePreviousMainProcessInterruption');
+ const copy = handlers.get('diagnostics:copyPreviousMainProcessInterruption');
+ assert.ok(take);
+ assert.ok(copy);
+ assert.equal(await take({} as never), true);
+ assert.equal(await take({} as never), false);
+ assert.equal(acknowledgements, 1);
+
+ await copy({} as never);
+ assert.match(clipboard, /Surface: previous_main_process_interruption/);
+ assert.match(clipboard, /clean shutdown was not observed/);
+ assert.match(clipboard, /Maka: 0\.1\.10/);
+ assert.match(clipboard, /Recent previous main-process logs \(1\)/);
+ assert.doesNotMatch(clipboard, /current main log|current Runtime Host exit|very-secret-token/);
+});
+
test('copies Desktop diagnostics while the scoped Host is reconnecting', async () => {
type IpcHandler = Parameters<Pick<IpcMain, 'handle'>['handle']>[1];
const handlers = new Map<string, IpcHandler>();
diff --git a/apps/desktop/src/main/__tests__/main-process-recovery-journal.test.ts b/apps/desktop/src/main/__tests__/main-process-recovery-journal.test.ts
index 4c8fd6853..f677c6b19 100644
--- a/apps/desktop/src/main/__tests__/main-process-recovery-journal.test.ts
+++ b/apps/desktop/src/main/__tests__/main-process-recovery-journal.test.ts
@@ -31,7 +31,6 @@ import {
MAIN_PROCESS_RECOVERY_FLUSH_INTERVAL_MS,
MAIN_PROCESS_RECOVERY_LOG_MAX_BYTES,
MAIN_PROCESS_RECOVERY_MAX_AGE_MS,
- presentPendingMainProcessRecovery,
} from '../main-process-recovery-journal.js';
test('recovers one bounded redacted snapshot after an unclean exit', async () => {
@@ -222,60 +221,6 @@ test('ignores corrupt pending records without amplifying invalid log arrays', as
}
});
-test('discards pending evidence only after its recovery presentation succeeds', async () => {
- const directory = await mkdtemp(join(tmpdir(), 'maka-main-recovery-presentation-'));
- const errors: unknown[] = [];
- try {
- const interrupted = createJournal(
- directory,
- new DiagnosticLogBuffer(),
- new Date('2026-08-20T00:00:00Z'),
- );
- interrupted.markDirty();
- interrupted.flushNow();
-
- const failedPresentation = createJournal(
- directory,
- new DiagnosticLogBuffer(),
- new Date('2026-08-20T01:00:00Z'),
- );
- await presentPendingMainProcessRecovery(
- failedPresentation,
- async () => {
- throw new Error('dialog unavailable');
- },
- (error) => errors.push(error),
- );
- assert.equal(errors.length, 1);
- failedPresentation.markClean();
-
- const retriedPresentation = createJournal(
- directory,
- new DiagnosticLogBuffer(),
- new Date('2026-08-20T02:00:00Z'),
- );
- await presentPendingMainProcessRecovery(
- retriedPresentation,
- async (evidence) => {
- assert.equal(evidence.run.startedAt, '2026-08-20T00:00:00.000Z');
- },
- (error) => errors.push(error),
- );
- retriedPresentation.markClean();
-
- const afterSuccess = createJournal(
- directory,
- new DiagnosticLogBuffer(),
- new Date('2026-08-20T03:00:00Z'),
- );
- assert.equal(afterSuccess.pending, undefined);
- assert.equal(errors.length, 1);
- afterSuccess.markClean();
- } finally {
- await rm(directory, { recursive: true, force: true });
- }
-});
-
test('refuses to read a symlinked recovery record', async (context) => {
if (process.platform === 'win32') context.skip('Creating symlinks requires optional privileges on Windows');
const directory = await mkdtemp(join(tmpdir(), 'maka-main-recovery-symlink-'));
diff --git a/apps/desktop/src/main/__tests__/native-diagnostic-dialog.test.ts b/apps/desktop/src/main/__tests__/native-diagnostic-dialog.test.ts
index a52aa9635..f2db20c36 100644
--- a/apps/desktop/src/main/__tests__/native-diagnostic-dialog.test.ts
+++ b/apps/desktop/src/main/__tests__/native-diagnostic-dialog.test.ts
@@ -23,13 +23,11 @@ import type { MessageBoxOptions, MessageBoxReturnValue } from 'electron';
import {
copyDesktopDiagnosticReport,
createDesktopMainRendererDiagnosticInput,
- createDesktopPreviousMainProcessDiagnosticInput,
} from '../main-process-diagnostics.js';
import {
showFatalStartupError,
showMainRendererProcessGoneDialog,
showMessageBoxWithDiagnostics,
- showPreviousMainProcessInterruptionDialog,
} from '../native-diagnostic-dialog.js';
const diagnosticEnvironment = () => ({
@@ -151,59 +149,3 @@ test('main Renderer loss keeps Copy Diagnostics auxiliary to recovery', async ()
assert.match(clipboard, /Recent main-process logs \(1\)/);
assert.doesNotMatch(clipboard, /very-secret-token/);
});
-
-test('previous main-process evidence remains copyable before a Renderer exists', async () => {
- const shown: MessageBoxOptions[] = [];
- const responses = [1, 0];
- let clipboard = '';
- const input = createDesktopPreviousMainProcessDiagnosticInput({
- run: {
- startedAt: '2026-08-20T00:00:00.000Z',
- appVersion: '0.1.10',
- buildMode: 'packaged',
- buildCommit: 'b'.repeat(40),
- electronVersion: '43.2.0',
- nodeVersion: '24.0.0',
- chromeVersion: '144.0.0',
- platform: 'win32',
- arch: 'x64',
- osRelease: '10.0.26100',
- },
- snapshotAt: '2026-08-20T00:10:00.000Z',
- logs: ['previous main log with api_key=very-secret-token'],
- });
-
- await showPreviousMainProcessInterruptionDialog({
- locale: 'en',
- copyDiagnostics: () =>
- copyDesktopDiagnosticReport(
- {
- environment: diagnosticEnvironment,
- mainLogs: () => ['current main log'],
- runtimeHostProcessLogs: () => ['current Runtime Host exit'],
- resolveActiveRuntimeHost: () => {
- throw new Error('Previous-run diagnostics must remain Desktop-only');
- },
- resolveRuntimeHost: () => {
- throw new Error('Previous-run diagnostics must not resolve a task Host');
- },
- writeClipboard: (value) => {
- clipboard = value;
- },
- },
- input,
- ),
- showMessageBox: async (options): Promise<MessageBoxReturnValue> => {
- shown.push(options);
- return { response: responses.shift() ?? 0, checkboxChecked: false };
- },
- });
-
- assert.deepEqual(shown[0]?.buttons, ['Continue', 'Copy Diagnostics']);
- assert.deepEqual(shown[1]?.buttons, ['Continue', 'Copy Again']);
- assert.match(clipboard, /Surface: previous_main_process_interruption/);
- assert.match(clipboard, /clean shutdown was not observed/);
- assert.match(clipboard, /Maka: 0\.1\.10/);
- assert.match(clipboard, /Recent previous main-process logs \(1\)/);
- assert.doesNotMatch(clipboard, /current main log|current Runtime Host exit|very-secret-token/);
-});
diff --git a/apps/desktop/src/main/desktop-diagnostics-ipc-main.ts b/apps/desktop/src/main/desktop-diagnostics-ipc-main.ts
index 048f033a7..9e5241804 100644
--- a/apps/desktop/src/main/desktop-diagnostics-ipc-main.ts
+++ b/apps/desktop/src/main/desktop-diagnostics-ipc-main.ts
@@ -20,9 +20,11 @@
import type { IpcMain } from 'electron';
import {
copyDesktopDiagnosticReport,
+ createDesktopPreviousMainProcessDiagnosticInput,
parseDesktopDiagnosticInput,
type DesktopDiagnosticsDeps,
} from './main-process-diagnostics.js';
+import type { MainProcessRecoveryEvidence } from './main-process-recovery-journal.js';
export interface DesktopDiagnosticsIpcDeps extends DesktopDiagnosticsDeps {
readonly ipcMain: Pick<IpcMain, 'handle'>;
@@ -37,3 +39,27 @@ export function registerDesktopDiagnosticsIpc(deps: DesktopDiagnosticsIpcDeps):
},
);
}
+
+export interface PreviousMainProcessDiagnosticsIpcDeps extends DesktopDiagnosticsIpcDeps {
+ readonly evidence: MainProcessRecoveryEvidence | undefined;
+ readonly acknowledge: () => void;
+}
+
+export function registerPreviousMainProcessDiagnosticsIpc(
+ deps: PreviousMainProcessDiagnosticsIpcDeps,
+): void {
+ let noticeAvailable = deps.evidence !== undefined;
+ deps.ipcMain.handle('diagnostics:takePreviousMainProcessInterruption', (): boolean => {
+ if (!noticeAvailable) return false;
+ deps.acknowledge();
+ noticeAvailable = false;
+ return true;
+ });
+ deps.ipcMain.handle('diagnostics:copyPreviousMainProcessInterruption', async (): Promise<void> => {
+ if (!deps.evidence) throw new Error('Previous-session diagnostics are unavailable');
+ await copyDesktopDiagnosticReport(
+ deps,
+ createDesktopPreviousMainProcessDiagnosticInput(deps.evidence),
+ );
+ });
+}
diff --git a/apps/desktop/src/main/main-process-recovery-journal.ts b/apps/desktop/src/main/main-process-recovery-journal.ts
index 9c27ab4cf..bfe91e627 100644
--- a/apps/desktop/src/main/main-process-recovery-journal.ts
+++ b/apps/desktop/src/main/main-process-recovery-journal.ts
@@ -239,21 +239,6 @@ export function createMainProcessRecoveryJournal(
};
}
-export async function presentPendingMainProcessRecovery(
- journal: Pick<MainProcessRecoveryJournal, 'pending' | 'discardPending'>,
- present: (evidence: MainProcessRecoveryEvidence) => Promise<void>,
- onError: (error: unknown) => void,
-): Promise<void> {
- const evidence = journal.pending;
- if (!evidence) return;
- try {
- await present(evidence);
- journal.discardPending();
- } catch (error) {
- onError(error);
- }
-}
-
export function appendUncaughtMainProcessError(
logs: { append(level: 'error', message: string, capturedAt?: Date): void },
journal: Pick<MainProcessRecoveryJournal, 'markDirty' | 'flushNow'>,
diff --git a/apps/desktop/src/main/main.ts b/apps/desktop/src/main/main.ts
index 3068460b4..6b0405cd5 100644
--- a/apps/desktop/src/main/main.ts
+++ b/apps/desktop/src/main/main.ts
@@ -23,7 +23,7 @@ import {
developmentLaunchResultFile,
shouldShowLoserDialog,
} from '@maka/core/dev-single-instance';
-import { app, clipboard, dialog } from 'electron';
+import { app, clipboard, dialog, ipcMain } from 'electron';
import { join } from 'node:path';
import { resolveBuildInfo } from './build-info.js';
import {
@@ -36,15 +36,12 @@ import {
import {
appendUncaughtMainProcessError,
createMainProcessRecoveryJournal,
- presentPendingMainProcessRecovery,
type MainProcessRecoveryJournal,
} from './main-process-recovery-journal.js';
-import {
- showFatalStartupError,
- showPreviousMainProcessInterruptionDialog,
-} from './native-diagnostic-dialog.js';
+import { showFatalStartupError } from './native-diagnostic-dialog.js';
import { isIsolatedE2e } from './startup-context.js';
import { reportDevelopmentLaunchResult } from './dev-single-instance-result.js';
+import { registerPreviousMainProcessDiagnosticsIpc } from './desktop-diagnostics-ipc-main.js';
let recoveryJournal: MainProcessRecoveryJournal | undefined;
installMainProcessLogCapture(mainProcessLogBuffer, () => recoveryJournal?.markDirty());
@@ -125,6 +122,24 @@ if (!app.requestSingleInstanceLock()) {
} catch (error) {
console.error('[diagnostics] main-process recovery unavailable:', error);
}
+ if (isIsolatedE2e) recoveryJournal?.discardPending();
+ registerPreviousMainProcessDiagnosticsIpc({
+ ipcMain,
+ evidence: isIsolatedE2e ? undefined : recoveryJournal?.pending,
+ acknowledge: () => recoveryJournal?.discardPending(),
+ environment: () =>
+ captureDesktopDiagnosticEnvironment({
+ appVersion: app.getVersion(),
+ buildMode: buildInfo.mode,
+ buildCommit: buildInfo.commit,
+ locale: app.getLocale(),
+ workspacePath: join(app.getPath('userData'), 'workspaces', 'default'),
+ }),
+ mainLogs: () => mainProcessLogBuffer.snapshot(),
+ resolveActiveRuntimeHost: () => undefined,
+ resolveRuntimeHost: () => undefined,
+ writeClipboard: (report) => clipboard.writeText(report),
+ });
// The full boot must not run in the top-level module-evaluation chain:
// Electron ESM emits `ready` only after the entry module finishes
// evaluating, so a top-level `await app.whenReady()` (which the
@@ -134,47 +149,8 @@ if (!app.requestSingleInstanceLock()) {
// store/db write".
app
.whenReady()
- .then(async () => {
+ .then(() => {
console.log('[startup] app ready');
- const journal = recoveryJournal;
- if (journal?.pending) {
- if (isIsolatedE2e) {
- journal.discardPending();
- } else {
- await presentPendingMainProcessRecovery(
- journal,
- (pending) =>
- showPreviousMainProcessInterruptionDialog({
- locale: resolveSystemUiLocale(app.getPreferredSystemLanguages()),
- copyDiagnostics: () =>
- copyDesktopDiagnosticReport(
- {
- environment: () =>
- captureDesktopDiagnosticEnvironment({
- appVersion: app.getVersion(),
- buildMode: buildInfo.mode,
- buildCommit: buildInfo.commit,
- locale: app.getLocale(),
- workspacePath: join(
- app.getPath('userData'),
- 'workspaces',
- 'default',
- ),
- }),
- mainLogs: () => mainProcessLogBuffer.snapshot(),
- resolveActiveRuntimeHost: () => undefined,
- resolveRuntimeHost: () => undefined,
- writeClipboard: (report) => clipboard.writeText(report),
- },
- createDesktopPreviousMainProcessDiagnosticInput(pending),
- ),
- showMessageBox: (options) => dialog.showMessageBox(options),
- }),
- (error) =>
- console.error('[diagnostics] previous-session recovery dialog failed:', error),
- );
- }
- }
return import('./runtime-host-boot.js');
})
.catch(async (error: unknown) => {
diff --git a/apps/desktop/src/main/native-diagnostic-dialog.ts b/apps/desktop/src/main/native-diagnostic-dialog.ts
index 249132673..d7d2fb467 100644
--- a/apps/desktop/src/main/native-diagnostic-dialog.ts
+++ b/apps/desktop/src/main/native-diagnostic-dialog.ts
@@ -114,25 +114,6 @@ export async function showMainRendererProcessGoneDialog(
return result.response === 0 ? 'relaunch' : 'exit';
}
-export async function showPreviousMainProcessInterruptionDialog(
- deps: DiagnosticDialogDeps,
-): Promise<void> {
- const copy = PREVIOUS_MAIN_PROCESS_INTERRUPTION_COPY[deps.locale];
- await showMessageBoxWithDiagnostics(
- {
- type: 'warning',
- title: copy.title,
- message: copy.message,
- detail: copy.detail,
- buttons: [copy.continue],
- defaultId: 0,
- cancelId: 0,
- noLink: true,
- },
- deps,
- );
-}
-
async function copyDiagnostics(
copy: () => void | Promise<void>,
locale: UiLocale,
@@ -214,18 +195,3 @@ const MAIN_RENDERER_GONE_COPY = {
exit: '退出',
},
} as const;
-
-const PREVIOUS_MAIN_PROCESS_INTERRUPTION_COPY = {
- en: {
- title: 'Previous session ended unexpectedly',
- message: 'Maka did not finish shutting down during its previous session.',
- detail: 'You can copy the available diagnostics before continuing.',
- continue: 'Continue',
- },
- zh: {
- title: '上一次会话意外结束',
- message: 'Maka 上一次运行时未能完成退出流程。',
- detail: '继续前,你可以复制当前可用的诊断信息。',
- continue: '继续',
- },
-} as const;
diff --git a/apps/desktop/src/preload/bridge-contract.d.ts b/apps/desktop/src/preload/bridge-contract.d.ts
index 877736d21..a989e94ff 100644
--- a/apps/desktop/src/preload/bridge-contract.d.ts
+++ b/apps/desktop/src/preload/bridge-contract.d.ts
@@ -1425,6 +1425,8 @@ export interface MakaBridge {
};
diagnostics: {
copyReport(input: DesktopDiagnosticInput): Promise<void>;
+ takePreviousMainProcessInterruption(): Promise<boolean>;
+ copyPreviousMainProcessInterruption(): Promise<void>;
};
workspace: {
searchFiles(
diff --git a/apps/desktop/src/preload/preload.ts b/apps/desktop/src/preload/preload.ts
index 8d39ee7f2..9824c551d 100644
--- a/apps/desktop/src/preload/preload.ts
+++ b/apps/desktop/src/preload/preload.ts
@@ -242,6 +242,7 @@ const runtimeHostMetadata = new Map<
{ readonly profileId: string; readonly profileName: string; readonly profileKind: 'local' | 'remote' }
>();
const newTaskChangeListeners = new Set<() => void>();
+let previousMainProcessInterruptionRead: Promise<boolean> | undefined;
type RuntimeHostProfileWireEvent = DesktopRuntimeHostProfileChangedEvent;
@@ -2989,6 +2990,15 @@ const makaBridge = {
},
},
diagnostics: {
+ takePreviousMainProcessInterruption(): Promise<boolean> {
+ previousMainProcessInterruptionRead ??= ipcRenderer.invoke(
+ 'diagnostics:takePreviousMainProcessInterruption',
+ ) as Promise<boolean>;
+ return previousMainProcessInterruptionRead;
+ },
+ copyPreviousMainProcessInterruption(): Promise<void> {
+ return ipcRenderer.invoke('diagnostics:copyPreviousMainProcessInterruption');
+ },
async copyReport(input: DesktopDiagnosticInput): Promise<void> {
const rendererContext = {
rendererUserAgent: navigator.userAgent,
diff --git a/apps/desktop/src/renderer/app-shell.tsx b/apps/desktop/src/renderer/app-shell.tsx
index e98b20e93..41db40427 100644
--- a/apps/desktop/src/renderer/app-shell.tsx
+++ b/apps/desktop/src/renderer/app-shell.tsx
@@ -128,6 +128,7 @@ import { scopeWorkHubSessionsToCoordinationHost } from './workhub-coordination-h
import { createDesktopWorkHubSessionPort } from './workhub-session-port.js';
import { WorkHubCoordinationStatus, WorkHubSurface } from './workhub-surface.js';
import { getShellCopy, localizedShellErrorMessage } from './locales/shell-copy';
+import { getShellRemainingCopy } from './locales/shell-remaining-copy.js';
import { getDesktopConversationCopy } from './locales/conversation-copy';
import { ErrorBoundary } from './error-boundary';
import { useShellAppearance } from './use-shell-appearance';
@@ -345,6 +346,7 @@ function AppShellContent({
const [appUpdateStatus, setAppUpdateStatus] = useState<AppUpdateStatus | null>(null);
const updateInstallInFlightRef = useRef(false);
const notifiedInstallErrorRef = useRef<string | null>(null);
+ const previousInterruptionShownRef = useRef(false);
const {
sessions,
catalogRevision,
@@ -610,6 +612,7 @@ function AppShellContent({
themePalette,
setThemePalette,
uiLocaleUpdateGate,
+ appearanceHydrated,
userLabel,
setUserLabel,
@@ -622,6 +625,8 @@ function AppShellContent({
setUiLocalePreference,
});
const shellCopy = getShellCopy(uiLocale).app;
+ const previousInterruptionCopy =
+ getShellRemainingCopy(uiLocale).previousMainProcessInterruption;
const projectActionsCopy = getShellCopy(uiLocale).projectActions;
const desktopConversationCopy = getDesktopConversationCopy(uiLocale);
/**
@@ -635,6 +640,33 @@ function AppShellContent({
const newTaskPermissionMode =
newTaskPermissionChoice ?? newTask.selectedHost?.chatDefaults.permissionMode ?? 'ask';
const setNewTaskPermissionMode = setNewTaskPermissionChoice;
+ useEffect(() => {
+ if (!appearanceHydrated) return;
+ let cancelled = false;
+ void window.maka.diagnostics
+ .takePreviousMainProcessInterruption()
+ .then((interrupted) => {
+ if (cancelled || !interrupted || previousInterruptionShownRef.current) return;
+ previousInterruptionShownRef.current = true;
+ toastApi.toast({
+ variant: 'warning',
+ title: previousInterruptionCopy.title,
+ description: previousInterruptionCopy.description,
+ duration: 10_000,
+ action: {
+ label: previousInterruptionCopy.copyDiagnostics,
+ onClick: () =>
+ window.maka.diagnostics.copyPreviousMainProcessInterruption(),
+ },
+ });
+ })
+ .catch((error) =>
+ console.error('[diagnostics] previous-session notice failed:', error),
+ );
+ return () => {
+ cancelled = true;
+ };
+ }, [appearanceHydrated, previousInterruptionCopy, toastApi]);
useEffect(() => {
if (!isAppUpdateInstallFailure(appUpdateStatus)) {
notifiedInstallErrorRef.current = null;
diff --git a/apps/desktop/src/renderer/locales/shell-remaining-copy.ts b/apps/desktop/src/renderer/locales/shell-remaining-copy.ts
index d93bea4aa..75ace87c6 100644
--- a/apps/desktop/src/renderer/locales/shell-remaining-copy.ts
+++ b/apps/desktop/src/renderer/locales/shell-remaining-copy.ts
@@ -85,6 +85,11 @@ const zhCopy = {
scheduledTask: "定时任务",
viewScheduledTasks: "查看定时任务",
},
+ previousMainProcessInterruption: {
+ title: "Maka 已恢复",
+ description: "上次退出未完成。",
+ copyDiagnostics: "复制报告",
+ },
conversationExport: {
exported: (date: string) => `由 Maka 于 ${date} 导出。`,
you: "你",
@@ -161,6 +166,11 @@ const enCopy: ShellRemainingCopy = {
scheduledTask: "Scheduled task",
viewScheduledTasks: "View scheduled tasks",
},
+ previousMainProcessInterruption: {
+ title: "Maka recovered",
+ description: "The previous shutdown was incomplete.",
+ copyDiagnostics: "Copy report",
+ },
conversationExport: {
exported: (date) => `Exported ${date} from Maka.`,
you: "You",
diff --git a/apps/desktop/src/renderer/use-shell-appearance.ts b/apps/desktop/src/renderer/use-shell-appearance.ts
index 460e2581f..d52166de4 100644
--- a/apps/desktop/src/renderer/use-shell-appearance.ts
+++ b/apps/desktop/src/renderer/use-shell-appearance.ts
@@ -59,6 +59,7 @@ export function useShellAppearance({
const [themePalette, setThemePalette] = useState<ThemePalette>('default');
const [uiLocaleUpdateGate] = useState(createUiLocaleUpdateGate);
const [userLabel, setUserLabel] = useState<string>('');
+ const [appearanceHydrated, setAppearanceHydrated] = useState(false);
// undefined = the user expressed no preference, so each model uses its own.
const [defaultThinkingLevel, setDefaultThinkingLevel] = useState<ThinkingLevel | undefined>(undefined);
@@ -102,6 +103,7 @@ export function useShellAppearance({
),
);
}
+ setAppearanceHydrated(true);
const runtimeHostResult = await runtimeHostHydration;
if (runtimeHostResult.ok) {
@@ -117,6 +119,7 @@ export function useShellAppearance({
themePalette,
setThemePalette,
uiLocaleUpdateGate,
+ appearanceHydrated,
userLabel,
setUserLabel,
defaultThinkingLevel,
---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@maka.apache.org
For additional commands, e-mail: commits-help@maka.apache.org