From 8ad18f858798daedad1e28b1688bd1748f7048f8 Mon Sep 17 00:00:00 2001 From: Simos Mikelatos Date: Wed, 24 Jun 2026 20:49:24 +0000 Subject: [PATCH] fix: improve desktop chat performance --- electron/assets/logo-windows.ico | Bin 0 -> 15024 bytes electron/desktopWindow.js | 14 ++++- electron/main.js | 5 ++ electron/scripts/generate-windows-icon.js | 57 ++++++++++++++++++ package.json | 10 ++- scripts/release/prepare-desktop-app.js | 1 + .../chat/hooks/useChatComposerState.ts | 40 ++++++++---- src/components/chat/view/ChatInterface.tsx | 2 +- .../chat/view/subcomponents/ChatComposer.tsx | 20 +++--- .../view/subcomponents/ChatMessagesPane.tsx | 8 ++- src/index.css | 24 ++++++++ 11 files changed, 153 insertions(+), 28 deletions(-) create mode 100644 electron/assets/logo-windows.ico create mode 100644 electron/scripts/generate-windows-icon.js diff --git a/electron/assets/logo-windows.ico b/electron/assets/logo-windows.ico new file mode 100644 index 0000000000000000000000000000000000000000..8ce6a9110d776cda967d51b1102fdea515fa3599 GIT binary patch literal 15024 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 { + const isAllowedPermission = (webContents, permission) => { const sourceUrl = webContents.getURL(); - const allowedPermissions = new Set(['clipboard-read', 'media']); - callback(isAllowedPermissionOrigin(sourceUrl, this.getCloudState().controlPlaneUrl) && allowedPermissions.has(permission)); + const allowedPermissions = new Set(['clipboard-read', 'media', 'notifications']); + return isAllowedPermissionOrigin(sourceUrl, this.getCloudState().controlPlaneUrl) && allowedPermissions.has(permission); + }; + + session.defaultSession.setPermissionRequestHandler((webContents, permission, callback) => { + callback(isAllowedPermission(webContents, permission)); + }); + session.defaultSession.setPermissionCheckHandler((webContents, permission) => { + if (!webContents) return false; + return isAllowedPermission(webContents, permission); }); } diff --git a/electron/main.js b/electron/main.js index 665b8c96..f2808faf 100644 --- a/electron/main.js +++ b/electron/main.js @@ -12,6 +12,7 @@ import { TabsController } from './tabs.js'; const __dirname = path.dirname(fileURLToPath(import.meta.url)); const APP_NAME = 'CloudCLI'; +const APP_USER_MODEL_ID = 'ai.cloudcli.desktop'; const CALLBACK_PROTOCOL = 'cloudcli'; const CALLBACK_URL = `${CALLBACK_PROTOCOL}://auth/callback`; const CLOUDCLI_CONTROL_PLANE_URL = process.env.CLOUDCLI_CONTROL_PLANE_URL || 'https://cloudcli.ai'; @@ -20,6 +21,10 @@ const AUTH_CALLBACK_TTL_MS = 10 * 60 * 1000; const tabs = new TabsController(); +if (process.platform === 'win32') { + app.setAppUserModelId(APP_USER_MODEL_ID); +} + let activeTarget = { kind: 'launcher', name: APP_NAME, url: null }; let desktopWindow = null; let localServer = null; diff --git a/electron/scripts/generate-windows-icon.js b/electron/scripts/generate-windows-icon.js new file mode 100644 index 00000000..7d6ebdef --- /dev/null +++ b/electron/scripts/generate-windows-icon.js @@ -0,0 +1,57 @@ +#!/usr/bin/env node +import fs from 'node:fs/promises'; +import path from 'node:path'; +import sharp from 'sharp'; + +const assetsDir = 'electron/assets'; +const sourcePath = 'public/logo-512.png'; +const icoPath = path.join(assetsDir, 'logo-windows.ico'); +const sizes = [16, 24, 32, 48, 64, 128, 256]; + +function writeDirectoryEntry(buffer, image, offset) { + buffer.writeUInt8(image.size === 256 ? 0 : image.size, offset); + buffer.writeUInt8(image.size === 256 ? 0 : image.size, offset + 1); + buffer.writeUInt8(0, offset + 2); + buffer.writeUInt8(0, offset + 3); + buffer.writeUInt16LE(1, offset + 4); + buffer.writeUInt16LE(32, offset + 6); + buffer.writeUInt32LE(image.buffer.length, offset + 8); + buffer.writeUInt32LE(image.offset, offset + 12); +} + +async function renderPng(size) { + return sharp(sourcePath) + .resize(size, size, { fit: 'contain' }) + .png() + .toBuffer(); +} + +await fs.mkdir(assetsDir, { recursive: true }); + +const images = await Promise.all( + sizes.map(async (size) => ({ + size, + buffer: await renderPng(size), + offset: 0, + })), +); + +const headerSize = 6 + images.length * 16; +let cursor = headerSize; +for (const image of images) { + image.offset = cursor; + cursor += image.buffer.length; +} + +const ico = Buffer.alloc(cursor); +ico.writeUInt16LE(0, 0); +ico.writeUInt16LE(1, 2); +ico.writeUInt16LE(images.length, 4); + +images.forEach((image, index) => { + writeDirectoryEntry(ico, image, 6 + index * 16); + image.buffer.copy(ico, image.offset); +}); + +await fs.writeFile(icoPath, ico); +console.log(`Wrote ${icoPath}`); diff --git a/package.json b/package.json index 2d995d68..6ce8266b 100644 --- a/package.json +++ b/package.json @@ -43,6 +43,7 @@ "desktop:dist:win": "npm run build && npm run desktop:stage && electron-builder --projectDir .desktop-build/desktop-app --win nsis", "server:bundle": "npm run build && node scripts/release/build-server-bundle.js", "desktop:icon:mac": "node electron/scripts/generate-macos-icon.js", + "desktop:icon:win": "node electron/scripts/generate-windows-icon.js", "build": "npm run build:semantics && npm run build:client && npm run build:server", "build:client": "vite build", "build:semantics": "node scripts/build-computer-semantics.mjs", @@ -110,9 +111,16 @@ } }, "win": { + "icon": "electron/assets/logo-windows.ico", "target": [ "nsis" - ] + ], + "publisherName": "CloudCLI", + "verifyUpdateCodeSignature": false + }, + "nsis": { + "installerIcon": "electron/assets/logo-windows.ico", + "uninstallerIcon": "electron/assets/logo-windows.ico" } }, "keywords": [ diff --git a/scripts/release/prepare-desktop-app.js b/scripts/release/prepare-desktop-app.js index ebf0f3d0..50edf895 100644 --- a/scripts/release/prepare-desktop-app.js +++ b/scripts/release/prepare-desktop-app.js @@ -101,6 +101,7 @@ function buildDesktopPackageJson(copiedOptionalDependencies) { protocols: packageJson.build.protocols, mac: packageJson.build.mac, win: packageJson.build.win, + nsis: packageJson.build.nsis, }, }; } diff --git a/src/components/chat/hooks/useChatComposerState.ts b/src/components/chat/hooks/useChatComposerState.ts index c1f86f2d..443313bf 100644 --- a/src/components/chat/hooks/useChatComposerState.ts +++ b/src/components/chat/hooks/useChatComposerState.ts @@ -204,6 +204,8 @@ export function useChatComposerState({ const textareaRef = useRef(null); const inputHighlightRef = useRef(null); + const textareaLineHeightRef = useRef(null); + const lastAutosizedInputRef = useRef(null); const handleSubmitRef = useRef< ((event: FormEvent | MouseEvent | TouchEvent | KeyboardEvent) => Promise) | null >(null); @@ -457,6 +459,22 @@ export function useChatComposerState({ inputHighlightRef.current.scrollLeft = target.scrollLeft; }, []); + const resizeTextarea = useCallback((target: HTMLTextAreaElement) => { + target.style.height = 'auto'; + const nextHeight = Math.max(22, target.scrollHeight); + target.style.height = `${nextHeight}px`; + + let lineHeight = textareaLineHeightRef.current; + if (!lineHeight) { + lineHeight = parseInt(window.getComputedStyle(target).lineHeight); + textareaLineHeightRef.current = Number.isFinite(lineHeight) ? lineHeight : 24; + } + + const expanded = nextHeight > (textareaLineHeightRef.current || 24) * 2; + setIsTextareaExpanded((previous) => previous === expanded ? previous : expanded); + lastAutosizedInputRef.current = target.value; + }, []); + const handleImageFiles = useCallback((files: File[]) => { const validFiles = files.filter((file) => { try { @@ -806,13 +824,13 @@ export function useChatComposerState({ if (!textareaRef.current) { return; } - // Re-run when input changes so restored drafts get the same autosize behavior as typed text. - textareaRef.current.style.height = 'auto'; - textareaRef.current.style.height = `${Math.max(22, textareaRef.current.scrollHeight)}px`; - const lineHeight = parseInt(window.getComputedStyle(textareaRef.current).lineHeight); - const expanded = textareaRef.current.scrollHeight > lineHeight * 2; - setIsTextareaExpanded(expanded); - }, [input]); + if (lastAutosizedInputRef.current === input) { + return; + } + // Re-run for restored drafts and programmatic input changes. User typing is + // already resized in onInput, so this avoids doing the same forced layout twice. + resizeTextarea(textareaRef.current); + }, [input, resizeTextarea]); useEffect(() => { if (!textareaRef.current || input.trim()) { @@ -894,15 +912,11 @@ export function useChatComposerState({ const handleTextareaInput = useCallback( (event: FormEvent) => { const target = event.currentTarget; - target.style.height = 'auto'; - target.style.height = `${Math.max(22, target.scrollHeight)}px`; + resizeTextarea(target); setCursorPosition(target.selectionStart); syncInputOverlayScroll(target); - - const lineHeight = parseInt(window.getComputedStyle(target).lineHeight); - setIsTextareaExpanded(target.scrollHeight > lineHeight * 2); }, - [setCursorPosition, syncInputOverlayScroll], + [resizeTextarea, setCursorPosition, syncInputOverlayScroll], ); const handleClearInput = useCallback(() => { diff --git a/src/components/chat/view/ChatInterface.tsx b/src/components/chat/view/ChatInterface.tsx index 5efe6af4..7a86b4e6 100644 --- a/src/components/chat/view/ChatInterface.tsx +++ b/src/components/chat/view/ChatInterface.tsx @@ -309,7 +309,7 @@ function ChatInterface({ return ( -
+
{ + if (!isCommandMenuOpen) { + return { top: 0, left: 16, bottom: 90 }; + } + const textareaRect = textareaRef.current?.getBoundingClientRect(); + return { + top: textareaRect ? Math.max(16, textareaRect.top - 316) : 0, + left: textareaRect ? textareaRect.left : 16, + bottom: textareaRect ? window.innerHeight - textareaRect.top + 8 : 90, + }; + }, [input, isCommandMenuOpen, textareaRef]); // Detect if the AskUserQuestion interactive panel is active const hasQuestionPanel = pendingPermissionRequests.some( @@ -170,7 +176,7 @@ export default function ChatComposer({ const hasPendingPermissions = pendingPermissionRequests.length > 0; return ( -
+
{!hasPendingPermissions && ( )} diff --git a/src/components/chat/view/subcomponents/ChatMessagesPane.tsx b/src/components/chat/view/subcomponents/ChatMessagesPane.tsx index d97c944f..bb61096a 100644 --- a/src/components/chat/view/subcomponents/ChatMessagesPane.tsx +++ b/src/components/chat/view/subcomponents/ChatMessagesPane.tsx @@ -1,5 +1,5 @@ import { useTranslation } from 'react-i18next'; -import { useCallback, useMemo, useRef } from 'react'; +import { memo, useCallback, useMemo, useRef } from 'react'; import type { Dispatch, RefObject, SetStateAction } from 'react'; import type { ChatMessage } from '../../types/types'; @@ -67,7 +67,7 @@ interface ChatMessagesPaneProps { selectedProject: Project; } -export default function ChatMessagesPane({ +function ChatMessagesPane({ scrollContainerRef, onWheel, onTouchMove, @@ -151,7 +151,7 @@ export default function ChatMessagesPane({ ref={scrollContainerRef} onWheel={onWheel} onTouchMove={onTouchMove} - className="relative flex-1 space-y-3 overflow-y-auto overflow-x-hidden px-0 py-3 sm:space-y-4 sm:p-4" + className="chat-messages-pane relative min-h-0 flex-1 space-y-3 overflow-y-auto overflow-x-hidden px-0 py-3 sm:space-y-4 sm:p-4" > {(isLoadingSessionMessages || isProcessing) && chatMessages.length === 0 ? (
@@ -308,3 +308,5 @@ export default function ChatMessagesPane({
); } + +export default memo(ChatMessagesPane); diff --git a/src/index.css b/src/index.css index 06028a03..f99ce614 100644 --- a/src/index.css +++ b/src/index.css @@ -557,6 +557,30 @@ /* Mobile optimizations and components */ @layer components { + .chat-messages-pane { + contain: layout style paint; + } + + .chat-composer-shell { + contain: layout style paint; + } + + .chat-message { + contain: layout style paint; + content-visibility: auto; + contain-intrinsic-size: auto 180px; + } + + .chat-message.assistant { + contain-intrinsic-size: auto 240px; + } + + .chat-message.user, + .chat-message.tool, + .chat-message.error { + contain-intrinsic-size: auto 96px; + } + /* Mobile touch optimization and safe areas */ @media (max-width: 768px) { * {