diff --git a/apps/desktop/electron/main.ts b/apps/desktop/electron/main.ts index 21f690ccd0..5e2180a152 100644 --- a/apps/desktop/electron/main.ts +++ b/apps/desktop/electron/main.ts @@ -2992,9 +2992,7 @@ async function processStartMarker(pid) { if (IS_WINDOWS) { const electronMarker = - pid === process.pid - ? electronProcessStartMarker(pid, process.pid, process.getCreationTime?.()) - : null + pid === process.pid ? electronProcessStartMarker(pid, process.pid, process.getCreationTime?.()) : null if (electronMarker) { return electronMarker diff --git a/apps/desktop/electron/parent-process-identity.test.ts b/apps/desktop/electron/parent-process-identity.test.ts index 107f1825a4..fd46025c7d 100644 --- a/apps/desktop/electron/parent-process-identity.test.ts +++ b/apps/desktop/electron/parent-process-identity.test.ts @@ -23,10 +23,7 @@ test('parent marker resolver shares and caches a successful probe', async () => const load = vi.fn(async () => 'winms:1723456789123') const resolve = createParentStartMarkerResolver({ load }) - assert.deepEqual(await Promise.all([resolve(), resolve()]), [ - 'winms:1723456789123', - 'winms:1723456789123' - ]) + assert.deepEqual(await Promise.all([resolve(), resolve()]), ['winms:1723456789123', 'winms:1723456789123']) assert.equal(await resolve(), 'winms:1723456789123') assert.equal(load.mock.calls.length, 1) }) @@ -34,7 +31,8 @@ test('parent marker resolver shares and caches a successful probe', async () => test('parent marker resolver degrades a failed probe and retries later', async () => { const failure = new Error('powershell timed out') - const load = vi.fn<() => Promise>() + const load = vi + .fn<() => Promise>() .mockRejectedValueOnce(failure) .mockResolvedValueOnce('win:638908765432100000') diff --git a/apps/desktop/electron/parent-process-identity.ts b/apps/desktop/electron/parent-process-identity.ts index 954a890120..0aa8956aca 100644 --- a/apps/desktop/electron/parent-process-identity.ts +++ b/apps/desktop/electron/parent-process-identity.ts @@ -14,11 +14,7 @@ export interface ParentStartMarkerResolverOptions { * an OS helper. Electron reports milliseconds since the Unix epoch; the Python * watchdog converts its exact Windows FILETIME to the same representation. */ -export function electronProcessStartMarker( - pid: number, - ownPid: number, - creationTime: unknown -): string | null { +export function electronProcessStartMarker(pid: number, ownPid: number, creationTime: unknown): string | null { if (pid !== ownPid || typeof creationTime !== 'number' || !Number.isFinite(creationTime)) { return null } diff --git a/apps/desktop/electron/venv-blocker-scan.test.ts b/apps/desktop/electron/venv-blocker-scan.test.ts index a74a309696..9eaa22c26c 100644 --- a/apps/desktop/electron/venv-blocker-scan.test.ts +++ b/apps/desktop/electron/venv-blocker-scan.test.ts @@ -57,9 +57,7 @@ describe('formatBlockerMessage', () => { it('includes PID, name, cmdline, remote-client warning, and retry suggestion', () => { const msg = formatBlockerMessage({ blocked: true, - processes: [ - { pid: 101, name: 'python.exe', cmdline: 'serve --host 10.0.0.1', kind: 'other', safeToStop: false } - ] + processes: [{ pid: 101, name: 'python.exe', cmdline: 'serve --host 10.0.0.1', kind: 'other', safeToStop: false }] }) assert.ok(msg.includes('PID 101')) @@ -340,13 +338,7 @@ describe('stopSafeVenvBlockers', () => { assert.deepEqual(calls, [ { command: 'C:\\Hermes\\venv\\Scripts\\python.exe', - args: [ - '-m', - 'hermes_cli._scan_venv_blockers', - '--terminate-safe', - '47484', - '1722798000.25' - ] + args: ['-m', 'hermes_cli._scan_venv_blockers', '--terminate-safe', '47484', '1722798000.25'] } ]) assert.deepEqual(outcome, { stopped: [47484], failed: [] }) diff --git a/apps/desktop/electron/venv-blocker-scan.ts b/apps/desktop/electron/venv-blocker-scan.ts index aaea9bec73..0b687d3531 100644 --- a/apps/desktop/electron/venv-blocker-scan.ts +++ b/apps/desktop/electron/venv-blocker-scan.ts @@ -127,13 +127,7 @@ export async function stopSafeVenvBlockers( try { await execFn( pythonPath, - [ - '-m', - 'hermes_cli._scan_venv_blockers', - '--terminate-safe', - String(process.pid), - String(process.createTime) - ], + ['-m', 'hermes_cli._scan_venv_blockers', '--terminate-safe', String(process.pid), String(process.createTime)], { cwd: updateRoot, windowsHide: true, timeout: 10_000, maxBuffer: 256 * 1024 } ) stopped.push(process.pid) diff --git a/apps/desktop/src/app/chat/index.tsx b/apps/desktop/src/app/chat/index.tsx index 1a8f60f74d..13ed225186 100644 --- a/apps/desktop/src/app/chat/index.tsx +++ b/apps/desktop/src/app/chat/index.tsx @@ -67,7 +67,11 @@ import { ScrollToBottomButton } from './scroll-to-bottom-button' import { useSessionView } from './session-view' import { SessionActionsMenu } from './sidebar/session-actions-menu' import { threadLoadingState } from './thread-loading' -import { backfillOlderTranscriptPage, mergeOlderTranscriptPage, transcriptBackfillAvailable } from './transcript-backfill' +import { + backfillOlderTranscriptPage, + mergeOlderTranscriptPage, + transcriptBackfillAvailable +} from './transcript-backfill' import { advanceTranscriptWindow, type TranscriptWindowState } from './transcript-window' interface ChatViewProps extends Omit, 'onSubmit'> { diff --git a/apps/desktop/src/app/chat/transcript-backfill.test.ts b/apps/desktop/src/app/chat/transcript-backfill.test.ts index 2864c27192..fd393d06d3 100644 --- a/apps/desktop/src/app/chat/transcript-backfill.test.ts +++ b/apps/desktop/src/app/chat/transcript-backfill.test.ts @@ -1,11 +1,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import type { ChatMessage } from '@/lib/chat-messages' -import { - $transcriptTailBySessionId, - recordTranscriptTail, - transcriptTailState -} from '@/store/transcript-tail' +import { $transcriptTailBySessionId, recordTranscriptTail, transcriptTailState } from '@/store/transcript-tail' import { _resetTranscriptBackfillForTests, @@ -238,8 +234,16 @@ describe('backfillOlderTranscriptPage', () => { }) as never ) - const first = backfillOlderTranscriptPage({ storedSessionId: 'stored-1', isCurrent: () => true, applyOlderPage: vi.fn() }) - const second = backfillOlderTranscriptPage({ storedSessionId: 'stored-1', isCurrent: () => true, applyOlderPage: vi.fn() }) + const first = backfillOlderTranscriptPage({ + storedSessionId: 'stored-1', + isCurrent: () => true, + applyOlderPage: vi.fn() + }) + const second = backfillOlderTranscriptPage({ + storedSessionId: 'stored-1', + isCurrent: () => true, + applyOlderPage: vi.fn() + }) expect(second).toBe(first) expect(getOlderSessionMessages).toHaveBeenCalledTimes(1) diff --git a/apps/desktop/src/app/chat/transcript-backfill.ts b/apps/desktop/src/app/chat/transcript-backfill.ts index 82205b61af..fb6ff3d3d2 100644 --- a/apps/desktop/src/app/chat/transcript-backfill.ts +++ b/apps/desktop/src/app/chat/transcript-backfill.ts @@ -75,6 +75,7 @@ export function graftRefreshedTailOntoBackfill(refreshedTail: ChatMessage[], pre } const first = refreshedTail[0] + const anchor = previous.findIndex( message => (first.rowId !== undefined && message.rowId !== undefined && message.rowId === first.rowId) || diff --git a/apps/desktop/src/app/contrib/mcp-install-deeplink-dialog.tsx b/apps/desktop/src/app/contrib/mcp-install-deeplink-dialog.tsx index b7c3e2389d..aa4b1b3cab 100644 --- a/apps/desktop/src/app/contrib/mcp-install-deeplink-dialog.tsx +++ b/apps/desktop/src/app/contrib/mcp-install-deeplink-dialog.tsx @@ -154,7 +154,9 @@ export function McpInstallDeepLinkDialog() { {!nameValid &&

{m.deepLinkNameInvalid}

} - {nameValid && nameConflict &&

{m.deepLinkNameConflict(trimmedName)}

} + {nameValid && nameConflict && ( +

{m.deepLinkNameConflict(trimmedName)}

+ )}
{m.serverJson} diff --git a/apps/desktop/src/app/session/hooks/use-message-stream/utils.test.ts b/apps/desktop/src/app/session/hooks/use-message-stream/utils.test.ts index da9a2856cc..e56561a9b0 100644 --- a/apps/desktop/src/app/session/hooks/use-message-stream/utils.test.ts +++ b/apps/desktop/src/app/session/hooks/use-message-stream/utils.test.ts @@ -66,7 +66,7 @@ describe('delegateTaskPayloads', () => { it.each(['timeout', 'error', 'failed', 'failure', 'TIMEOUT'])( 'maps completion with result.status=%s to a failed subagent.complete', - (resultStatus) => { + resultStatus => { const [spec] = delegateTaskPayloads( payload({ name: 'delegate_task', result: { status: resultStatus, summary: 'timed out' } }), 'complete' diff --git a/apps/desktop/src/app/session/hooks/use-prompt-actions/index.test.tsx b/apps/desktop/src/app/session/hooks/use-prompt-actions/index.test.tsx index 25afbea6da..246d486648 100644 --- a/apps/desktop/src/app/session/hooks/use-prompt-actions/index.test.tsx +++ b/apps/desktop/src/app/session/hooks/use-prompt-actions/index.test.tsx @@ -5184,6 +5184,7 @@ describe('usePromptActions editMessage stale-target recovery (#82462)', () => { it('surfaces a compressed-away notice instead of plain-resubmitting without an ordinal', async () => { let handle: HarnessHandle | undefined + const requestGateway = vi.fn(async (method: string) => { if (method === 'prompt.submit') { throw new JsonRpcGatewayError('target user message is no longer in session history', { @@ -5204,6 +5205,7 @@ describe('usePromptActions editMessage stale-target recovery (#82462)', () => { { id: 'u1', parts: [textPart('pre-compress')], role: 'user' as const, timestamp: 0 }, { id: 'a1', parts: [textPart('reply')], role: 'assistant' as const, timestamp: 1 } ] + setMessages(seed) await actRender( @@ -5233,6 +5235,7 @@ describe('usePromptActions editMessage stale-target recovery (#82462)', () => { const submitCalls = (requestGateway as unknown as { mock: { calls: unknown[][] } }).mock.calls.filter( ([method]) => method === 'prompt.submit' ) + // First attempt only — no plain resubmit that drops truncate_before_user_ordinal. expect(submitCalls).toHaveLength(1) expect(submitCalls[0]?.[1]).toMatchObject({ diff --git a/apps/desktop/src/app/session/hooks/use-prompt-actions/index.ts b/apps/desktop/src/app/session/hooks/use-prompt-actions/index.ts index c9695dfa69..e41d1a9f01 100644 --- a/apps/desktop/src/app/session/hooks/use-prompt-actions/index.ts +++ b/apps/desktop/src/app/session/hooks/use-prompt-actions/index.ts @@ -1013,11 +1013,13 @@ export function usePromptActions({ } const data = err.data + if (!data || typeof data !== 'object') { return false } const segmentOrdinal = (data as { segment_ordinal?: unknown }).segment_ordinal + return typeof segmentOrdinal === 'number' && segmentOrdinal < 0 } @@ -1046,12 +1048,14 @@ export function usePromptActions({ if (!plan.isFailedTurn && !unavailable && isStaleTargetError(err)) { try { const storedId = selectedStoredSessionIdRef.current + if (storedId) { await resumeStoredSession(storedId) } const refreshed = $messages.get() const retryPlan = planEdit(refreshed, edited) + if (retryPlan && !retryPlan.isFailedTurn) { const survivorRowIds = await submitRewindPrompt( sessionId, @@ -1064,6 +1068,7 @@ export function usePromptActions({ ) applySurvivorRowIds(sessionId, survivorRowIds) + return } diff --git a/apps/desktop/src/app/session/hooks/use-session-actions/index.ts b/apps/desktop/src/app/session/hooks/use-session-actions/index.ts index ccdadffcad..eb3c94ba4a 100644 --- a/apps/desktop/src/app/session/hooks/use-session-actions/index.ts +++ b/apps/desktop/src/app/session/hooks/use-session-actions/index.ts @@ -897,6 +897,7 @@ export function useSessionActions({ toChatMessages(persisted.messages), cachedViewState.messages ) + const runtimeMessages = toChatMessages(activated.messages) const previousMessages = removeRepresentedLocalLiveProjection(cachedViewState.messages, activated) diff --git a/apps/desktop/src/i18n/ar.ts b/apps/desktop/src/i18n/ar.ts index 8bcc30d032..e9c8575b8d 100644 --- a/apps/desktop/src/i18n/ar.ts +++ b/apps/desktop/src/i18n/ar.ts @@ -763,8 +763,10 @@ export const ar = defineLocale({ remove: 'إزالة', saveServer: 'حفظ الخادم', deepLinkTitle: 'إضافة خادم MCP؟', - deepLinkDescription: 'طلب رابط إضافة خادم MCP هذا إلى Hermes. راجع الإعدادات الكاملة أدناه — فهي قادمة من الرابط وليست من Hermes.', - deepLinkStdioWarning: 'سيشغّل هذا الخادم عملية محلية على جهازك بالأمر الموضح أدناه. لا تتابع إلا إذا كنت تثق بمصدره.', + deepLinkDescription: + 'طلب رابط إضافة خادم MCP هذا إلى Hermes. راجع الإعدادات الكاملة أدناه — فهي قادمة من الرابط وليست من Hermes.', + deepLinkStdioWarning: + 'سيشغّل هذا الخادم عملية محلية على جهازك بالأمر الموضح أدناه. لا تتابع إلا إذا كنت تثق بمصدره.', deepLinkConfirm: 'إضافة الخادم', deepLinkNameInvalid: 'الأسماء من 1-64 حرفا أو رقما أو نقطة أو شرطة أو شرطة سفلية.', deepLinkNameConflict: name => `يوجد خادم باسم ${name} بالفعل — اختر اسما مختلفا أو ألغِ العملية.`, diff --git a/apps/desktop/src/i18n/ja.ts b/apps/desktop/src/i18n/ja.ts index 639f2c78bd..cff785fb9a 100644 --- a/apps/desktop/src/i18n/ja.ts +++ b/apps/desktop/src/i18n/ja.ts @@ -887,11 +887,13 @@ export const ja = defineLocale({ 'このサーバーは下記のコマンドでローカルプロセスを実行します。提供元を信頼できる場合のみ続行してください。', deepLinkConfirm: 'サーバーを追加', deepLinkNameInvalid: '名前は 1〜64 文字の英数字、ドット、ハイフン、アンダースコアです。', - deepLinkNameConflict: name => `${name} という名前のサーバーは既に存在します。別の名前にするかキャンセルしてください。`, + deepLinkNameConflict: name => + `${name} という名前のサーバーは既に存在します。別の名前にするかキャンセルしてください。`, deepLinkErrorTitle: 'MCP インストールリンクを拒否しました', deepLinkErrorName: 'リンクのサーバー名が欠落しているか無効です。', deepLinkErrorConfig: 'リンクの設定が有効な base64 エンコード JSON ではありません。', - deepLinkErrorShape: '設定は文字列の `url` または `command` フィールドを持つ JSON オブジェクトである必要があります。', + deepLinkErrorShape: + '設定は文字列の `url` または `command` フィールドを持つ JSON オブジェクトである必要があります。', deepLinkErrorUrl: 'サーバー URL は http:// と https:// のみ許可されます。', deepLinkErrorTooLarge: '設定ペイロードが 32KB の上限を超えています。' }, diff --git a/apps/desktop/src/i18n/zh.ts b/apps/desktop/src/i18n/zh.ts index 04ef547fcf..b9df61a6cc 100644 --- a/apps/desktop/src/i18n/zh.ts +++ b/apps/desktop/src/i18n/zh.ts @@ -1101,7 +1101,8 @@ export const zh: Translations = { disableTool: tool => `禁用 ${tool}`, noOutput: '暂无输出。', deepLinkTitle: '添加 MCP 服务器?', - deepLinkDescription: '一个链接请求将此 MCP 服务器添加到 Hermes。请检查下方的完整配置——它来自该链接,而非 Hermes。', + deepLinkDescription: + '一个链接请求将此 MCP 服务器添加到 Hermes。请检查下方的完整配置——它来自该链接,而非 Hermes。', deepLinkStdioWarning: '此服务器会使用下方所示命令在你的电脑上运行本地进程。仅在信任其来源时继续。', deepLinkConfirm: '添加服务器', deepLinkNameInvalid: '名称须为 1-64 个字母、数字、点、连字符或下划线。', diff --git a/apps/desktop/src/lib/json-rpc-gateway-url-guard.test.ts b/apps/desktop/src/lib/json-rpc-gateway-url-guard.test.ts index 563a1a148b..ca978ed3d1 100644 --- a/apps/desktop/src/lib/json-rpc-gateway-url-guard.test.ts +++ b/apps/desktop/src/lib/json-rpc-gateway-url-guard.test.ts @@ -104,11 +104,13 @@ describe('JsonRpcGatewayClient structured errors', () => { } vi.stubGlobal('WebSocket', RespondingSocket) + try { const client = new JsonRpcGatewayClient({ requestTimeoutMs: 5_000, socketFactory: () => new RespondingSocket() as unknown as WebSocket }) + await client.connect('ws://127.0.0.1:1234/api/ws?token=t') await expect(client.request('prompt.submit', { session_id: 's' })).rejects.toEqual( diff --git a/apps/desktop/src/lib/mcp-deeplink.ts b/apps/desktop/src/lib/mcp-deeplink.ts index 21e13a8545..a78e1cbd22 100644 --- a/apps/desktop/src/lib/mcp-deeplink.ts +++ b/apps/desktop/src/lib/mcp-deeplink.ts @@ -36,8 +36,7 @@ export interface McpInstallRequest { } export type McpDeepLinkParseResult = - | { ok: false; error: McpDeepLinkErrorCode } - | { ok: true; request: McpInstallRequest } + { ok: false; error: McpDeepLinkErrorCode } | { ok: true; request: McpInstallRequest } /** i18n key (under `settings.mcp`) for each rejection, used by the toast. */ export const MCP_DEEPLINK_ERROR_KEYS: Record = {