From e003c53b06d8a922ae17401f8696c8d9281509c8 Mon Sep 17 00:00:00 2001 From: brooklyn! Date: Thu, 4 Jun 2026 09:10:38 -0500 Subject: [PATCH] chore(desktop): zero eslint/typecheck debt + prettier pass (#39100) - eslint --fix across src/ and electron/ (unused imports, import/prop sort, padding) - flatten empty catch blocks in electron CJS; drop unused applyUpdatesPosixInApp arg - add setMutableRef helper for imperative ref writes (react-compiler clean) - move sidebar cookie persistence into an effect; extract scrollElementToBottom helper --- apps/desktop/electron/backend-probes.test.cjs | 4 +- apps/desktop/electron/bootstrap-platform.cjs | 4 +- .../electron/bootstrap-platform.test.cjs | 10 +- apps/desktop/electron/bootstrap-runner.cjs | 90 ++++++++--- .../electron/connection-config.test.cjs | 31 +--- apps/desktop/electron/main.cjs | 37 +++-- .../src/app/chat/composer/context-menu.tsx | 26 +--- apps/desktop/src/app/chat/composer/index.tsx | 10 +- .../composer/slash-nav-dom-repro.test.tsx | 5 +- apps/desktop/src/app/chat/perf-probe.tsx | 94 +++++++++--- .../src/app/chat/right-rail/preview.tsx | 9 +- .../src/app/chat/sidebar/session-row.tsx | 7 +- apps/desktop/src/app/command-center/index.tsx | 16 +- .../desktop/src/app/command-palette/index.tsx | 19 ++- apps/desktop/src/app/cron/index.tsx | 28 ++-- .../src/app/gateway/hooks/use-gateway-boot.ts | 2 + .../src/app/messaging/platform-icon.tsx | 8 +- apps/desktop/src/app/page-search-shell.tsx | 8 +- apps/desktop/src/app/profiles/index.tsx | 4 +- .../src/app/right-sidebar/terminal/index.tsx | 1 + .../app/right-sidebar/terminal/persistent.tsx | 22 ++- .../terminal/use-terminal-session.ts | 92 +++++++++--- apps/desktop/src/app/routes.ts | 8 +- .../app/session/hooks/use-message-stream.ts | 5 +- .../app/session/hooks/use-prompt-actions.ts | 11 +- .../app/session/hooks/use-session-actions.ts | 131 ++++++++-------- .../session/hooks/use-session-state-cache.ts | 5 +- .../src/app/settings/gateway-settings.tsx | 1 + .../src/app/settings/model-settings.tsx | 18 +-- .../src/app/settings/providers-settings.tsx | 15 +- .../src/app/settings/sessions-settings.tsx | 8 +- .../src/app/settings/toolset-config-panel.tsx | 12 +- .../app/shell/hooks/use-statusbar-items.tsx | 19 ++- .../src/app/shell/model-edit-submenu.tsx | 18 +-- .../src/app/shell/model-menu-panel.tsx | 5 +- apps/desktop/src/app/shell/titlebar.ts | 3 +- apps/desktop/src/app/skills/index.tsx | 13 +- apps/desktop/src/app/updates-overlay.tsx | 8 +- .../assistant-ui/thread-virtualizer.tsx | 37 ++++- .../components/assistant-ui/tool-approval.tsx | 7 +- .../src/components/chat/disclosure-row.tsx | 4 +- .../components/desktop-install-overlay.tsx | 141 ++++++++++++++---- .../desktop-onboarding-overlay.test.tsx | 3 +- .../components/desktop-onboarding-overlay.tsx | 8 +- .../desktop/src/components/error-boundary.tsx | 5 +- apps/desktop/src/components/model-picker.tsx | 1 + .../components/model-visibility-dialog.tsx | 23 ++- apps/desktop/src/components/notifications.tsx | 4 +- .../src/components/prompt-overlays.tsx | 9 +- apps/desktop/src/components/ui/button.tsx | 3 +- apps/desktop/src/components/ui/input.tsx | 7 +- apps/desktop/src/components/ui/sidebar.tsx | 7 +- apps/desktop/src/components/ui/textarea.tsx | 8 +- apps/desktop/src/lib/ansi.ts | 16 +- apps/desktop/src/lib/mutable-ref.ts | 6 + apps/desktop/src/lib/yolo-session.ts | 5 +- apps/desktop/src/store/onboarding.test.ts | 1 + apps/desktop/src/store/onboarding.ts | 10 ++ 58 files changed, 685 insertions(+), 427 deletions(-) create mode 100644 apps/desktop/src/lib/mutable-ref.ts diff --git a/apps/desktop/electron/backend-probes.test.cjs b/apps/desktop/electron/backend-probes.test.cjs index db39ebcee..13d30a286 100644 --- a/apps/desktop/electron/backend-probes.test.cjs +++ b/apps/desktop/electron/backend-probes.test.cjs @@ -67,7 +67,9 @@ test('verifyHermesCli returns true when --version exits 0', () => { } finally { try { fs.unlinkSync(scriptPath) - } catch {} + } catch { + void 0 + } } }) diff --git a/apps/desktop/electron/bootstrap-platform.cjs b/apps/desktop/electron/bootstrap-platform.cjs index e217a4c6f..e385eca1c 100644 --- a/apps/desktop/electron/bootstrap-platform.cjs +++ b/apps/desktop/electron/bootstrap-platform.cjs @@ -52,7 +52,9 @@ function detectRemoteDisplay(options = {}) { const env = options.env ?? process.env const platform = options.platform ?? process.platform - const override = String(env.HERMES_DESKTOP_DISABLE_GPU || '').trim().toLowerCase() + const override = String(env.HERMES_DESKTOP_DISABLE_GPU || '') + .trim() + .toLowerCase() if (GPU_OVERRIDE_ON.has(override)) return 'override (HERMES_DESKTOP_DISABLE_GPU)' if (GPU_OVERRIDE_OFF.has(override)) return null diff --git a/apps/desktop/electron/bootstrap-platform.test.cjs b/apps/desktop/electron/bootstrap-platform.test.cjs index 2d3ba3763..9833d90ce 100644 --- a/apps/desktop/electron/bootstrap-platform.test.cjs +++ b/apps/desktop/electron/bootstrap-platform.test.cjs @@ -45,11 +45,17 @@ test('detectRemoteDisplay does not treat WSLg as remote', () => { // WSLg renders locally via vGPU and doesn't show the flicker, so a WSL // session with a local DISPLAY keeps hardware acceleration on. assert.equal(detectRemoteDisplay({ env: { WSL_DISTRO_NAME: 'Ubuntu', DISPLAY: ':0' }, platform: 'linux' }), null) - assert.equal(detectRemoteDisplay({ env: { WSL_INTEROP: '/run/WSL/1_interop', DISPLAY: ':0' }, platform: 'linux' }), null) + assert.equal( + detectRemoteDisplay({ env: { WSL_INTEROP: '/run/WSL/1_interop', DISPLAY: ':0' }, platform: 'linux' }), + null + ) }) test('detectRemoteDisplay flags SSH sessions on any platform', () => { - assert.equal(detectRemoteDisplay({ env: { SSH_CONNECTION: '1.2.3.4 5 6.7.8.9 22' }, platform: 'linux' }), 'ssh-session') + assert.equal( + detectRemoteDisplay({ env: { SSH_CONNECTION: '1.2.3.4 5 6.7.8.9 22' }, platform: 'linux' }), + 'ssh-session' + ) assert.equal(detectRemoteDisplay({ env: { SSH_CLIENT: '1.2.3.4 5 22' }, platform: 'darwin' }), 'ssh-session') assert.equal(detectRemoteDisplay({ env: { SSH_TTY: '/dev/pts/0' }, platform: 'win32' }), 'ssh-session') }) diff --git a/apps/desktop/electron/bootstrap-runner.cjs b/apps/desktop/electron/bootstrap-runner.cjs index fa7a47ee2..871ba2ec7 100644 --- a/apps/desktop/electron/bootstrap-runner.cjs +++ b/apps/desktop/electron/bootstrap-runner.cjs @@ -101,7 +101,9 @@ function downloadInstallScript(commit, destPath) { .get(res.headers.location, res2 => { if (res2.statusCode !== 200) { reject( - new Error(`Failed to download ${scriptName}: HTTP ${res2.statusCode} from redirect ${res.headers.location}`) + new Error( + `Failed to download ${scriptName}: HTTP ${res2.statusCode} from redirect ${res.headers.location}` + ) ) return } @@ -121,7 +123,9 @@ function downloadInstallScript(commit, destPath) { out.close() try { fs.unlinkSync(tmpPath) - } catch {} + } catch { + void 0 + } reject(new Error(`Failed to download ${scriptName}: HTTP ${res.statusCode} from ${url}`)) return } @@ -134,14 +138,18 @@ function downloadInstallScript(commit, destPath) { out.on('error', err => { try { fs.unlinkSync(tmpPath) - } catch {} + } catch { + void 0 + } reject(err) }) }) .on('error', err => { try { fs.unlinkSync(tmpPath) - } catch {} + } catch { + void 0 + } reject(err) }) }) @@ -168,13 +176,19 @@ async function resolveInstallScript({ installStamp, sourceRepoRoot, hermesHome, const cached = cachedScriptPath(hermesHome, installStamp.commit) try { await fsp.access(cached, fs.constants.R_OK) - emit({ type: 'log', line: `[bootstrap] using cached ${installScriptName()} for ${installStamp.commit.slice(0, 12)}` }) + emit({ + type: 'log', + line: `[bootstrap] using cached ${installScriptName()} for ${installStamp.commit.slice(0, 12)}` + }) return { path: cached, source: 'cache', commit: installStamp.commit, kind: installScriptKind() } } catch { // not cached; download } - emit({ type: 'log', line: `[bootstrap] fetching ${installScriptName()} for ${installStamp.commit.slice(0, 12)} from GitHub` }) + emit({ + type: 'log', + line: `[bootstrap] fetching ${installScriptName()} for ${installStamp.commit.slice(0, 12)} from GitHub` + }) await downloadInstallScript(installStamp.commit, cached) emit({ type: 'log', line: `[bootstrap] saved to ${cached}` }) return { path: cached, source: 'download', commit: installStamp.commit, kind: installScriptKind() } @@ -207,7 +221,9 @@ function spawnPowerShell(scriptPath, args, { emit, stageName, abortSignal, herme killed = true try { child.kill('SIGTERM') - } catch {} + } catch { + void 0 + } } if (abortSignal) { if (abortSignal.aborted) { @@ -278,7 +294,9 @@ function spawnBash(scriptPath, args, { emit, stageName, abortSignal, hermesHome killed = true try { child.kill('SIGTERM') - } catch {} + } catch { + void 0 + } } if (abortSignal) { if (abortSignal.aborted) { @@ -369,7 +387,9 @@ async function fetchManifest({ scriptPath, installerKind, emit, hermesHome, acti hermesHome }) if (result.code !== 0) { - throw new Error(`${isPosix ? 'install.sh --manifest' : 'install.ps1 -Manifest'} failed: exit ${result.code}\n${result.stderr || result.stdout}`) + throw new Error( + `${isPosix ? 'install.sh --manifest' : 'install.ps1 -Manifest'} failed: exit ${result.code}\n${result.stderr || result.stdout}` + ) } // The manifest is the LAST JSON line on stdout (install.ps1 may print // banner / info lines first depending on Console.OutputEncoding effects). @@ -381,9 +401,13 @@ async function fetchManifest({ scriptPath, installerKind, emit, hermesHome, acti if (parsed && Array.isArray(parsed.stages)) { return parsed } - } catch {} + } catch { + void 0 + } } - throw new Error(`${isPosix ? 'install.sh --manifest' : 'install.ps1 -Manifest'} produced no parseable JSON payload\n${result.stdout}`) + throw new Error( + `${isPosix ? 'install.sh --manifest' : 'install.ps1 -Manifest'} produced no parseable JSON payload\n${result.stdout}` + ) } // Parse the JSON result frame from a stage run. The protocol guarantees @@ -397,7 +421,9 @@ function parseStageResult(stdout) { if (parsed && typeof parsed.ok === 'boolean' && typeof parsed.stage === 'string') { return parsed } - } catch {} + } catch { + void 0 + } } return null } @@ -408,13 +434,20 @@ async function runStage({ scriptPath, installerKind, stage, emit, hermesHome, ac const isPosix = installerKind === 'posix' const args = isPosix - ? ['--stage', stage.name, '--non-interactive', '--json', ...buildPosixPinArgs({ installStamp, activeRoot, hermesHome })] + ? [ + '--stage', + stage.name, + '--non-interactive', + '--json', + ...buildPosixPinArgs({ installStamp, activeRoot, hermesHome }) + ] : ['-Stage', stage.name, '-NonInteractive', '-Json', ...buildPinArgs(installStamp)] - const result = await (isPosix ? spawnBash : spawnPowerShell)( - scriptPath, - args, - { emit, stageName: stage.name, abortSignal, hermesHome } - ) + const result = await (isPosix ? spawnBash : spawnPowerShell)(scriptPath, args, { + emit, + stageName: stage.name, + abortSignal, + hermesHome + }) const durationMs = Date.now() - startedAt @@ -449,7 +482,14 @@ async function runStage({ scriptPath, installerKind, stage, emit, hermesHome, ac emit(ev) return ev } - const ev = { type: 'stage', name: stage.name, state: 'failed', durationMs, json, error: json.reason || `exit code ${result.code}` } + const ev = { + type: 'stage', + name: stage.name, + state: 'failed', + durationMs, + json, + error: json.reason || `exit code ${result.code}` + } emit(ev) return ev } @@ -489,7 +529,9 @@ async function runBootstrap(opts) { if (typeof onEvent === 'function') { try { onEvent({ type: 'failed', error: 'bootstrap cancelled by user' }) - } catch {} + } catch { + void 0 + } } return { ok: false, cancelled: true } } @@ -501,7 +543,9 @@ async function runBootstrap(opts) { const emit = ev => { try { runLog.stream.write(JSON.stringify(ev) + '\n') - } catch {} + } catch { + void 0 + } try { if (typeof onEvent === 'function') onEvent(ev) } catch (err) { @@ -578,7 +622,9 @@ async function runBootstrap(opts) { } finally { try { runLog.stream.end() - } catch {} + } catch { + void 0 + } } } diff --git a/apps/desktop/electron/connection-config.test.cjs b/apps/desktop/electron/connection-config.test.cjs index 754e9f700..3182bd2ff 100644 --- a/apps/desktop/electron/connection-config.test.cjs +++ b/apps/desktop/electron/connection-config.test.cjs @@ -53,31 +53,19 @@ test('normalizeRemoteBaseUrl rejects garbage', () => { // --- buildGatewayWsUrl (token) --- test('buildGatewayWsUrl uses wss for https and bakes the token', () => { - assert.equal( - buildGatewayWsUrl('https://gw.example.com', 'tok123'), - 'wss://gw.example.com/api/ws?token=tok123' - ) + assert.equal(buildGatewayWsUrl('https://gw.example.com', 'tok123'), 'wss://gw.example.com/api/ws?token=tok123') }) test('buildGatewayWsUrl uses ws for http', () => { - assert.equal( - buildGatewayWsUrl('http://127.0.0.1:9119', 'abc'), - 'ws://127.0.0.1:9119/api/ws?token=abc' - ) + assert.equal(buildGatewayWsUrl('http://127.0.0.1:9119', 'abc'), 'ws://127.0.0.1:9119/api/ws?token=abc') }) test('buildGatewayWsUrl honors a path prefix', () => { - assert.equal( - buildGatewayWsUrl('https://host/hermes', 't'), - 'wss://host/hermes/api/ws?token=t' - ) + assert.equal(buildGatewayWsUrl('https://host/hermes', 't'), 'wss://host/hermes/api/ws?token=t') }) test('buildGatewayWsUrl url-encodes the token', () => { - assert.equal( - buildGatewayWsUrl('https://host', 'a/b c+d'), - 'wss://host/api/ws?token=a%2Fb%20c%2Bd' - ) + assert.equal(buildGatewayWsUrl('https://host', 'a/b c+d'), 'wss://host/api/ws?token=a%2Fb%20c%2Bd') }) // --- buildGatewayWsUrlWithTicket (oauth) --- @@ -89,10 +77,7 @@ test('buildGatewayWsUrlWithTicket uses ?ticket= not ?token=', () => { }) test('buildGatewayWsUrlWithTicket url-encodes the ticket', () => { - assert.equal( - buildGatewayWsUrlWithTicket('https://host', 'a+b/c'), - 'wss://host/api/ws?ticket=a%2Bb%2Fc' - ) + assert.equal(buildGatewayWsUrlWithTicket('https://host', 'a+b/c'), 'wss://host/api/ws?ticket=a%2Bb%2Fc') }) // --- authModeFromStatus --- @@ -157,11 +142,7 @@ test('cookiesHaveSession handles non-arrays', () => { }) test('AT_COOKIE_VARIANTS covers all three deploy shapes', () => { - assert.deepEqual(AT_COOKIE_VARIANTS, [ - '__Host-hermes_session_at', - '__Secure-hermes_session_at', - 'hermes_session_at' - ]) + assert.deepEqual(AT_COOKIE_VARIANTS, ['__Host-hermes_session_at', '__Secure-hermes_session_at', 'hermes_session_at']) }) // --- tokenPreview --- diff --git a/apps/desktop/electron/main.cjs b/apps/desktop/electron/main.cjs index 093b90ade..254e4a70b 100644 --- a/apps/desktop/electron/main.cjs +++ b/apps/desktop/electron/main.cjs @@ -1352,9 +1352,7 @@ async function applyUpdates(opts = {}) { env: { ...process.env, HERMES_HOME, - PATH: [path.join(HERMES_HOME, 'node', 'bin'), venvBin, process.env.PATH] - .filter(Boolean) - .join(path.delimiter) + PATH: [path.join(HERMES_HOME, 'node', 'bin'), venvBin, process.env.PATH].filter(Boolean).join(path.delimiter) }, detached: true, stdio: 'ignore', @@ -1428,7 +1426,7 @@ function shellQuote(value) { // (`hermes desktop --build-only`), then atomically swap the running .app bundle // with the freshly built one and relaunch. Degrades to "backend updated, // restart to load the new GUI" if the swap can't be performed. -async function applyUpdatesPosixInApp(opts = {}) { +async function applyUpdatesPosixInApp() { const updateRoot = resolveUpdateRoot() const hermes = resolveHermesCliBinary(updateRoot) if (!hermes) { @@ -1901,7 +1899,9 @@ async function ensureRuntime(backend) { stages: [], protocolVersion: null }) - } catch {} + } catch { + void 0 + } bootstrapAbortController = new AbortController() @@ -1919,10 +1919,14 @@ async function ensureRuntime(backend) { // bootstrap and a log-write failure doesn't suppress the UI signal. try { rememberLog(`[bootstrap] ${JSON.stringify(ev)}`) - } catch {} + } catch { + void 0 + } try { broadcastBootstrapEvent(ev) - } catch {} + } catch { + void 0 + } }, writeMarker: writeBootstrapMarker }) @@ -2848,7 +2852,9 @@ function buildApplicationMenu() { { label: 'Actual Size', accelerator: 'CommandOrControl+0', - click: () => { if (mainWindow && !mainWindow.isDestroyed()) mainWindow.webContents.setZoomLevel(0) } + click: () => { + if (mainWindow && !mainWindow.isDestroyed()) mainWindow.webContents.setZoomLevel(0) + } }, { label: 'Zoom In', @@ -3191,7 +3197,7 @@ function openOauthLoginWindow(baseUrl) { let win = null let pollTimer = null - const finish = (err) => { + const finish = err => { if (settled) return settled = true if (pollTimer) clearInterval(pollTimer) @@ -3314,7 +3320,7 @@ function fetchJsonViaOauthSession(url, options = {}) { return } const looksHtml = /^\s*<(?:!doctype|html)/i.test(text) - const contentType = String((res.headers['content-type'] || res.headers['Content-Type'] || '')) + const contentType = String(res.headers['content-type'] || res.headers['Content-Type'] || '') if (looksHtml || contentType.includes('text/html')) { reject(new Error(`Expected JSON from ${url} but got HTML (status ${statusCode}).`)) return @@ -3553,8 +3559,7 @@ async function resolveRemoteBackend() { ticket = await mintGatewayWsTicket(baseUrl) } catch (error) { const err = new Error( - 'Your remote gateway session has expired. ' + - 'Open Settings → Gateway and click "Sign in" again.' + 'Your remote gateway session has expired. ' + 'Open Settings → Gateway and click "Sign in" again.' ) err.needsOauthLogin = true err.cause = error @@ -4036,7 +4041,9 @@ ipcMain.handle('hermes:bootstrap:cancel', async () => { if (bootstrapAbortController) { try { bootstrapAbortController.abort() - } catch {} + } catch { + void 0 + } return { ok: true, cancelled: true } } return { ok: false, cancelled: false } @@ -4614,7 +4621,9 @@ app.on('before-quit', () => { if (bootstrapAbortController) { try { bootstrapAbortController.abort() - } catch {} + } catch { + void 0 + } } if (desktopLogFlushTimer) { diff --git a/apps/desktop/src/app/chat/composer/context-menu.tsx b/apps/desktop/src/app/chat/composer/context-menu.tsx index b7ebef7d8..4db5553cf 100644 --- a/apps/desktop/src/app/chat/composer/context-menu.tsx +++ b/apps/desktop/src/app/chat/composer/context-menu.tsx @@ -2,13 +2,7 @@ import { useState } from 'react' import { Button } from '@/components/ui/button' import { Codicon } from '@/components/ui/codicon' -import { - Dialog, - DialogContent, - DialogDescription, - DialogHeader, - DialogTitle -} from '@/components/ui/dialog' +import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from '@/components/ui/dialog' import { DropdownMenu, DropdownMenuContent, @@ -104,8 +98,8 @@ export function ContextMenu({
- Tip: type @ to reference files - inline. + Tip: type @ to reference + files inline.
@@ -120,12 +114,7 @@ export function ContextMenu({ ) } -function PromptSnippetsDialog({ - onInsertText, - onOpenChange, - open, - snippets -}: PromptSnippetsDialogProps) { +function PromptSnippetsDialog({ onInsertText, onOpenChange, open, snippets }: PromptSnippetsDialogProps) { return ( @@ -160,12 +149,7 @@ function PromptSnippetsDialog({ ) } -export function ContextMenuItem({ - children, - disabled, - icon: Icon, - onSelect -}: ContextMenuItemProps) { +export function ContextMenuItem({ children, disabled, icon: Icon, onSelect }: ContextMenuItemProps) { return ( diff --git a/apps/desktop/src/app/chat/composer/index.tsx b/apps/desktop/src/app/chat/composer/index.tsx index e39c79356..97e7d78a2 100644 --- a/apps/desktop/src/app/chat/composer/index.tsx +++ b/apps/desktop/src/app/chat/composer/index.tsx @@ -21,11 +21,7 @@ import { chatMessageText } from '@/lib/chat-messages' import { DATA_IMAGE_URL_RE } from '@/lib/embedded-images' import { triggerHaptic } from '@/lib/haptics' import { cn } from '@/lib/utils' -import { - $composerAttachments, - clearComposerAttachments, - type ComposerAttachment -} from '@/store/composer' +import { $composerAttachments, clearComposerAttachments, type ComposerAttachment } from '@/store/composer' import { $queuedPromptsBySession, enqueueQueuedPrompt, @@ -172,7 +168,7 @@ export function ChatBar({ const [queueEdit, setQueueEdit] = useState(null) const [focusRequestId, setFocusRequestId] = useState(0) const dragDepthRef = useRef(0) - const composingRef = useRef(false) // true during IME composition (CJK input) + const composingRef = useRef(false) // true during IME composition (CJK input) const lastSpokenIdRef = useRef(null) const narrow = useMediaQuery('(max-width: 30rem)') @@ -1253,9 +1249,11 @@ export function ChatBar({ onDrop={handleDrop} onSubmit={e => { e.preventDefault() + if (composingRef.current) { return } + submitDraft() }} ref={composerRef} diff --git a/apps/desktop/src/app/chat/composer/slash-nav-dom-repro.test.tsx b/apps/desktop/src/app/chat/composer/slash-nav-dom-repro.test.tsx index 8835d4275..d2f7f8fef 100644 --- a/apps/desktop/src/app/chat/composer/slash-nav-dom-repro.test.tsx +++ b/apps/desktop/src/app/chat/composer/slash-nav-dom-repro.test.tsx @@ -37,7 +37,10 @@ function Harness({ const refreshTrigger = useCallback(() => { const editor = editorRef.current - if (!editor) {return} + if (!editor) { + return + } + const raw = editor.textContent ?? '' if (!raw.includes('@') && !raw.includes('/')) { diff --git a/apps/desktop/src/app/chat/perf-probe.tsx b/apps/desktop/src/app/chat/perf-probe.tsx index f128c9cb3..383e4bcba 100644 --- a/apps/desktop/src/app/chat/perf-probe.tsx +++ b/apps/desktop/src/app/chat/perf-probe.tsx @@ -1,6 +1,6 @@ import { Profiler, type ProfilerOnRenderCallback, type ReactNode } from 'react' -import { $messages, setMessages, setBusy } from '@/store/session' +import { $messages, setBusy, setMessages } from '@/store/session' type Sample = { id: string @@ -40,13 +40,16 @@ if (typeof window !== 'undefined' && !window.__PERF_PROBE__) { }, summary: () => { const byId = new Map() + for (const s of samples) { const k = `${s.id}:${s.phase}` const arr = byId.get(k) ?? [] arr.push(s.actualDuration) byId.set(k, arr) } + const out: Record = {} + for (const [k, arr] of byId) { arr.sort((a, b) => a - b) const total = arr.reduce((a, b) => a + b, 0) @@ -55,19 +58,27 @@ if (typeof window !== 'undefined' && !window.__PERF_PROBE__) { total: Math.round(total * 100) / 100, max: Math.round(arr[arr.length - 1] * 100) / 100, p50: Math.round(arr[Math.floor(arr.length * 0.5)] * 100) / 100, - p95: Math.round(arr[Math.floor(arr.length * 0.95)] * 100) / 100, + p95: Math.round(arr[Math.floor(arr.length * 0.95)] * 100) / 100 } } + return out - }, + } } } const onRender: ProfilerOnRenderCallback = (id, phase, actualDuration, baseDuration, startTime, commitTime) => { const probe = typeof window !== 'undefined' ? window.__PERF_PROBE__ : undefined - if (!probe || !probe.enabled) return + + if (!probe || !probe.enabled) { + return + } + probe.samples.push({ id, phase, actualDuration, baseDuration, startTime, commitTime }) - if (probe.samples.length > 5000) probe.samples.splice(0, probe.samples.length - 5000) + + if (probe.samples.length > 5000) { + probe.samples.splice(0, probe.samples.length - 5000) + } } if (typeof window !== 'undefined' && !window.__PERF_DRIVE__) { @@ -86,7 +97,11 @@ if (typeof window !== 'undefined' && !window.__PERF_DRIVE__) { snapshotMsgs: () => $messages.get().length, reset: () => { activeHandle?.stop() - if (baseline) setMessages(baseline) + + if (baseline) { + setMessages(baseline) + } + baseline = null setBusy(false) }, @@ -104,7 +119,11 @@ if (typeof window !== 'undefined' && !window.__PERF_DRIVE__) { }: { chunk?: string; intervalMs?: number; totalTokens?: number; flushMinMs?: number } = {}) => { activeHandle?.stop() const current = $messages.get() - if (!baseline) baseline = current + + if (!baseline) { + baseline = current + } + const msgId = `synthetic-${Date.now()}` // Seed an empty assistant message — assistant-ui will see it grow. setMessages([ @@ -126,13 +145,20 @@ if (typeof window !== 'undefined' && !window.__PERF_DRIVE__) { let flushHandle: number | null = null const applyDelta = (delta: string) => { - if (!delta) return + if (!delta) { + return + } + setMessages(prev => prev.map(m => { - if (m.id !== msgId) return m + if (m.id !== msgId) { + return m + } + const head = m.parts.slice(0, -1) const last = m.parts.at(-1) const lastText = last && last.type === 'text' ? last.text : '' + return { ...m, parts: [...head, { type: 'text', text: lastText + delta }] @@ -150,8 +176,16 @@ if (typeof window !== 'undefined' && !window.__PERF_DRIVE__) { } const scheduleFlush = () => { - if (flushHandle !== null) return - if (flushMinMs <= 0) { flushNow(); return } + if (flushHandle !== null) { + return + } + + if (flushMinMs <= 0) { + flushNow() + + return + } + const since = performance.now() - lastFlushAt const wait = Math.max(0, flushMinMs - since) flushHandle = @@ -162,48 +196,62 @@ if (typeof window !== 'undefined' && !window.__PERF_DRIVE__) { const handle: SyntheticDriverHandle = { stop: () => { - if (timer) clearTimeout(timer) + if (timer) { + clearTimeout(timer) + } + timer = null + if (flushHandle !== null) { clearTimeout(flushHandle) cancelAnimationFrame?.(flushHandle) } + flushHandle = null + if (pendingDelta) { applyDelta(pendingDelta) pendingDelta = '' } + activeHandle = null // Mark message finalized. - setMessages(prev => - prev.map(m => - m.id === msgId - ? { ...m, pending: false } - : m - ) - ) + setMessages(prev => prev.map(m => (m.id === msgId ? { ...m, pending: false } : m))) setBusy(false) } } + activeHandle = handle const tick = () => { - if (activeHandle !== handle) return - if (pushed >= totalTokens) { - if (pendingDelta) flushNow() - handle.stop() + if (activeHandle !== handle) { return } + + if (pushed >= totalTokens) { + if (pendingDelta) { + flushNow() + } + + handle.stop() + + return + } + pushed += 1 + if (flushMinMs > 0) { pendingDelta += chunk scheduleFlush() } else { applyDelta(chunk) } + timer = setTimeout(tick, intervalMs) } + timer = setTimeout(tick, intervalMs) + return handle } } diff --git a/apps/desktop/src/app/chat/right-rail/preview.tsx b/apps/desktop/src/app/chat/right-rail/preview.tsx index 215991808..b53acc955 100644 --- a/apps/desktop/src/app/chat/right-rail/preview.tsx +++ b/apps/desktop/src/app/chat/right-rail/preview.tsx @@ -101,12 +101,17 @@ export function ChatPreviewRail({ onRestartServer, setTitlebarToolGroup }: ChatP // memory. `onMouseDown` swallows the middle-button press so // Chromium doesn't switch into autoscroll mode. onAuxClick={event => { - if (event.button !== 1) return + if (event.button !== 1) { + return + } + event.preventDefault() closeRightRailTab(tab.id) }} onMouseDown={event => { - if (event.button === 1) event.preventDefault() + if (event.button === 1) { + event.preventDefault() + } }} > {active && ( diff --git a/apps/desktop/src/app/chat/sidebar/session-row.tsx b/apps/desktop/src/app/chat/sidebar/session-row.tsx index 1b4fe6604..545d30e72 100644 --- a/apps/desktop/src/app/chat/sidebar/session-row.tsx +++ b/apps/desktop/src/app/chat/sidebar/session-row.tsx @@ -146,7 +146,12 @@ export function SidebarSessionRow({ /> ) : ( - + )} diff --git a/apps/desktop/src/app/command-center/index.tsx b/apps/desktop/src/app/command-center/index.tsx index 3789cbf45..f4420b3ed 100644 --- a/apps/desktop/src/app/command-center/index.tsx +++ b/apps/desktop/src/app/command-center/index.tsx @@ -6,14 +6,7 @@ import { PageLoader } from '@/components/page-loader' import { Button } from '@/components/ui/button' import { SearchField } from '@/components/ui/search-field' import { SegmentedControl } from '@/components/ui/segmented-control' -import { - getActionStatus, - getLogs, - getStatus, - getUsageAnalytics, - restartGateway, - updateHermes -} from '@/hermes' +import { getActionStatus, getLogs, getStatus, getUsageAnalytics, restartGateway, updateHermes } from '@/hermes' import type { ActionStatusResponse, AnalyticsResponse, StatusResponse } from '@/hermes' import { sessionTitle } from '@/lib/chat-runtime' import { Activity, AlertCircle, BarChart3, type IconComponent, Pin } from '@/lib/icons' @@ -128,12 +121,7 @@ function EmptyPanel({ action, description, title }: { action?: ReactNode; descri ) } -export function CommandCenterView({ - initialSection, - onClose, - onDeleteSession, - onOpenSession -}: CommandCenterViewProps) { +export function CommandCenterView({ initialSection, onClose, onDeleteSession, onOpenSession }: CommandCenterViewProps) { const sessions = useStore($sessions) const pinnedSessionIds = useStore($pinnedSessionIds) diff --git a/apps/desktop/src/app/command-palette/index.tsx b/apps/desktop/src/app/command-palette/index.tsx index 31a034417..5875f1eb3 100644 --- a/apps/desktop/src/app/command-palette/index.tsx +++ b/apps/desktop/src/app/command-palette/index.tsx @@ -4,14 +4,7 @@ import { Dialog as DialogPrimitive } from 'radix-ui' import { useCallback, useEffect, useMemo, useState } from 'react' import { useNavigate } from 'react-router-dom' -import { - Command, - CommandEmpty, - CommandGroup, - CommandInput, - CommandItem, - CommandList -} from '@/components/ui/command' +import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from '@/components/ui/command' import { getHermesConfigRecord, listSessions } from '@/hermes' import { sessionTitle } from '@/lib/chat-runtime' import { @@ -137,7 +130,11 @@ export function CommandPalette() { // Server-backed sources for the type-to-search groups, fetched lazily while // the palette is open. react-query handles caching/dedup/staleness. - const configQuery = useQuery({ queryKey: ['command-palette', 'config'], queryFn: getHermesConfigRecord, enabled: open }) + const configQuery = useQuery({ + queryKey: ['command-palette', 'config'], + queryFn: getHermesConfigRecord, + enabled: open + }) const sessionsQuery = useQuery({ queryKey: ['command-palette', 'sessions'], @@ -154,7 +151,9 @@ export function CommandPalette() { const mcpServers = useMemo(() => { const raw = configQuery.data?.mcp_servers - return raw && typeof raw === 'object' && !Array.isArray(raw) ? Object.keys(raw as Record).sort() : [] + return raw && typeof raw === 'object' && !Array.isArray(raw) + ? Object.keys(raw as Record).sort() + : [] }, [configQuery.data]) const sessions = useMemo(() => (sessionsQuery.data?.sessions ?? []).map(toSessionEntry), [sessionsQuery.data]) diff --git a/apps/desktop/src/app/cron/index.tsx b/apps/desktop/src/app/cron/index.tsx index fe5f108be..fe5ef0d5c 100644 --- a/apps/desktop/src/app/cron/index.tsx +++ b/apps/desktop/src/app/cron/index.tsx @@ -432,20 +432,20 @@ export function CronView({ onClose }: CronViewProps) { {!jobs ? ( ) : visibleJobs.length === 0 ? ( - // Empty state owns the primary "create" CTA — we used to also have - // one in the filters bar but it was redundant. Only show the button - // when there are zero jobs total; the search-empty case ("No - // matches") just asks the user to broaden their query. - setEditor({ mode: 'create' }) : undefined} - title={totalCount === 0 ? 'No scheduled jobs yet' : 'No matches'} - /> + // Empty state owns the primary "create" CTA — we used to also have + // one in the filters bar but it was redundant. Only show the button + // when there are zero jobs total; the search-empty case ("No + // matches") just asks the user to broaden their query. + setEditor({ mode: 'create' }) : undefined} + title={totalCount === 0 ? 'No scheduled jobs yet' : 'No matches'} + /> ) : (
{/* Inline header replaces the old top-bar "New cron" button. We diff --git a/apps/desktop/src/app/gateway/hooks/use-gateway-boot.ts b/apps/desktop/src/app/gateway/hooks/use-gateway-boot.ts index 14b229c2b..f1d31a8cd 100644 --- a/apps/desktop/src/app/gateway/hooks/use-gateway-boot.ts +++ b/apps/desktop/src/app/gateway/hooks/use-gateway-boot.ts @@ -194,6 +194,7 @@ export function useGatewayBoot({ scheduleReconnect() } }) + const offEvent = gateway.onEvent(event => callbacksRef.current.handleGatewayEvent(event)) // Wake signals: power resume (macOS/Windows), network coming back, and the @@ -201,6 +202,7 @@ export function useGatewayBoot({ const offPowerResume = desktop.onPowerResume?.(() => reconnectNow()) const onOnline = () => reconnectNow() + const onVisible = () => { if (document.visibilityState === 'visible') { reconnectNow() diff --git a/apps/desktop/src/app/messaging/platform-icon.tsx b/apps/desktop/src/app/messaging/platform-icon.tsx index 2212a5806..6a0b32a7a 100644 --- a/apps/desktop/src/app/messaging/platform-icon.tsx +++ b/apps/desktop/src/app/messaging/platform-icon.tsx @@ -1,5 +1,3 @@ -import type { ComponentType, SVGProps } from 'react' - import { SiApple, SiBilibili, @@ -14,6 +12,7 @@ import { SiWechat, SiWhatsapp } from '@icons-pack/react-simple-icons' +import type { ComponentType, SVGProps } from 'react' import { Globe, Link as LinkIcon, MessageSquareText } from '@/lib/icons' import { cn } from '@/lib/utils' @@ -69,10 +68,7 @@ export function PlatformAvatar({ className, platformId, platformName }: Platform if (!spec) { return ( -