perf: stability and performance improvements with some minor UI issues resolved (#172)
* Stability and performance improvements. (#1) ## Changelog ### Performance Improvements - **perf: make terminal creation cwd check async (#9)** Replaced synchronous `fs.existsSync` with `fs.promises.access` in the terminal creation handler to prevent blocking the event loop. Improves throughput under high load. - **perf: optimize fuzzyMatchScore by avoiding redundant lowercasing (#8)** Renamed `fuzzyMatchScore` to `fuzzyMatchScoreNormalized` and updated it to accept a pre-lowercased query, avoiding repeated string allocations. Updated the call site in `searchFilesystemFiles`. ~25% search performance improvement on large datasets. - **perf: use async file read in update-install handler (#7)** Replaced `fs.readFileSync` with `await fs.promises.readFile` to avoid blocking the event loop. **Benchmark (10k ops):** - Sync: ~95ms (blocking) - Async: ~1124ms (non-blocking) Higher per-call overhead, but better server responsiveness. - **perf(server): optimize mkdir endpoint with async fs (#6)** Replaced `fs.mkdirSync` with `await fsPromises.mkdir` in `/api/fs/mkdir`. Prevents event-loop blocking and improves concurrent performance. **Result:** ~2× throughput improvement (100 concurrent requests). - **perf: parallelize fs checks in validateProjectEntries (#4)** Replaced serial `for...of` with `Promise.all + map`. Reduced validation time for 500 projects from ~110ms to ~20ms (~5× speedup). - **perf: cache getLoginShellPath result to avoid blocking event loop (#5)** Cached `getLoginShellPath` result to avoid repeated `spawnSync` calls (~400ms each). Subsequent calls reduced to <1ms. --- ### Server Fixes - **fix(server): use async check for terminal restart endpoint (#3)** Replaced `fs.existsSync` with `fs.promises.stat` in `/api/terminal/:sessionId/restart`. Added directory validation for better robustness. --- ### UI & Accessibility - **feat(ui): add aria-labels to git identities sidebar buttons (#1)** - Added `aria-label="Create new profile"` to the create button - Added `aria-label="Profile actions"` to the dropdown trigger - Added `.Jules/palette.md` for UX/a11y learnings --- ### UI Performance (Bolt) - **⚡ Bolt: Optimize MessageList re-renders by preserving referential equality (#2)** - **⚡ Bolt: Optimize MessageList re-renders by preserving referential equality (#11)** - **⚡ Bolt: Optimize MessageList re-renders by preserving referential equality (#12)** * stability and improvements (#17) * feat: add corner radius setting and update snackbar actions - Add `cornerRadius` to UI store and settings. - Add Corner Radius slider to Visual Settings section. - Apply corner radius to ChatInput component. - Remove default close button from Snackbar (Sonner). - Add "OK" action button to session deletion toasts. - Ensure `cornerRadius` setting is visible in OpenChamberPage. * fix(ui): add missing aria-label to radius slider and verify functionality - Added `aria-label="Corner radius in pixels"` to the desktop version of the corner radius slider for accessibility. - Verified functionality and accessibility compliance via script. * fix(ui): restore input bar offset setting on desktop - Restored the Input Bar Offset setting to be visible on desktop, not just mobile. - Verified both Corner Radius and Input Bar Offset sliders are accessible. * Fix mobile layout for chat input controls (#16) * Fix mobile layout for chat input controls - Reduced horizontal gaps in mobile model controls. - Added max-width constraints to model, variant, and agent labels on mobile to prevent overflow and cramping. - Optimized spacing for mobile view. * feat(git): auto-select gitmoji for generated commit messages When the "Generate commit message" feature is used and gitmoji is enabled, automatically prepend the appropriate gitmoji based on the commit subject keywords. - Added `KEYWORD_MAP` to map commit types to gitmojis. - Added `matchGitmojiFromSubject` helper. - Updated `handleGenerateCommitMessage` to apply the gitmoji. * feat(git): auto-select gitmoji for generated commit messages - Added `KEYWORD_MAP` to map commit types to gitmojis. - Added `matchGitmojiFromSubject` helper. - Updated `handleGenerateCommitMessage` to apply the gitmoji. - Fixed mobile layout for model controls.
This commit is contained in:
@@ -151,7 +151,7 @@ export const ChatInput: React.FC<ChatInputProps> = ({ onOpenSettings, scrollToBo
|
||||
|
||||
const { currentProviderId, currentModelId, currentVariant, currentAgentName, setAgent, getVisibleAgents } = useConfigStore();
|
||||
const agents = getVisibleAgents();
|
||||
const { isMobile, inputBarOffset, isKeyboardOpen, setTimelineDialogOpen } = useUIStore();
|
||||
const { isMobile, inputBarOffset, isKeyboardOpen, setTimelineDialogOpen, cornerRadius } = useUIStore();
|
||||
const { working } = useAssistantStatus();
|
||||
const [showAbortStatus, setShowAbortStatus] = React.useState(false);
|
||||
const abortTimeoutRef = React.useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||
@@ -305,16 +305,21 @@ export const ChatInput: React.FC<ChatInputProps> = ({ onOpenSettings, scrollToBo
|
||||
// Keep border width stable so toggling modes doesn't shift layout.
|
||||
const baseBorderWidth = isVSCodeRuntime() ? 1 : 2;
|
||||
|
||||
const baseStyle: React.CSSProperties = {
|
||||
borderRadius: cornerRadius,
|
||||
};
|
||||
|
||||
if (!chatInputAccent) {
|
||||
return { borderWidth: baseBorderWidth };
|
||||
return { ...baseStyle, borderWidth: baseBorderWidth };
|
||||
}
|
||||
|
||||
const borderColor = chatInputAccent.border ?? chatInputAccent.text;
|
||||
return {
|
||||
...baseStyle,
|
||||
borderColor: softenBorderColor(borderColor),
|
||||
borderWidth: baseBorderWidth,
|
||||
};
|
||||
}, [chatInputAccent, softenBorderColor]);
|
||||
}, [chatInputAccent, softenBorderColor, cornerRadius]);
|
||||
|
||||
const hasContent = message.trim() || attachedFiles.length > 0;
|
||||
const hasQueuedMessages = queuedMessages.length > 0;
|
||||
@@ -1410,7 +1415,7 @@ export const ChatInput: React.FC<ChatInputProps> = ({ onOpenSettings, scrollToBo
|
||||
/>
|
||||
<div
|
||||
className={cn(
|
||||
"rounded-xl border border-border/80 bg-input/10 dark:bg-input/30",
|
||||
"border border-border/80 bg-input/10 dark:bg-input/30",
|
||||
"flex flex-col relative overflow-visible"
|
||||
)}
|
||||
style={chatInputWrapperStyle}
|
||||
@@ -1466,7 +1471,7 @@ export const ChatInput: React.FC<ChatInputProps> = ({ onOpenSettings, scrollToBo
|
||||
disabled={!currentSessionId && !newSessionDraftOpen}
|
||||
|
||||
className={cn(
|
||||
'min-h-[52px] resize-none border-0 px-3 shadow-none rounded-t-xl rounded-b-none appearance-none focus:shadow-none focus-visible:shadow-none focus-visible:border-transparent focus-visible:ring-0 focus-visible:ring-transparent hover:border-transparent bg-transparent',
|
||||
'min-h-[52px] resize-none border-0 px-3 shadow-none rounded-b-none appearance-none focus:shadow-none focus-visible:shadow-none focus-visible:border-transparent focus-visible:ring-0 focus-visible:ring-transparent hover:border-transparent bg-transparent',
|
||||
isMobile ? "py-2.5" : "pt-4 pb-2",
|
||||
"focus-visible:outline-none focus-visible:ring-0"
|
||||
)}
|
||||
@@ -1474,15 +1479,21 @@ export const ChatInput: React.FC<ChatInputProps> = ({ onOpenSettings, scrollToBo
|
||||
flex: 'none',
|
||||
height: textareaSize ? `${textareaSize.height}px` : undefined,
|
||||
maxHeight: textareaSize ? `${textareaSize.maxHeight}px` : undefined,
|
||||
borderTopLeftRadius: cornerRadius,
|
||||
borderTopRightRadius: cornerRadius,
|
||||
}}
|
||||
rows={1}
|
||||
/>
|
||||
<div
|
||||
className={cn(
|
||||
'rounded-b-xl bg-transparent',
|
||||
'bg-transparent',
|
||||
footerPaddingClass,
|
||||
isMobile ? 'flex items-center gap-x-1.5' : cn('flex items-center justify-between', footerGapClass)
|
||||
)}
|
||||
style={{
|
||||
borderBottomLeftRadius: cornerRadius,
|
||||
borderBottomRightRadius: cornerRadius,
|
||||
}}
|
||||
data-chat-input-footer="true"
|
||||
>
|
||||
{isMobile ? (
|
||||
|
||||
@@ -55,10 +55,18 @@ const MessageList: React.FC<MessageListProps> = ({
|
||||
}
|
||||
return true;
|
||||
})
|
||||
.map((message) => ({
|
||||
...message,
|
||||
parts: filterSyntheticParts(message.parts),
|
||||
}));
|
||||
.map((message) => {
|
||||
const filteredParts = filterSyntheticParts(message.parts);
|
||||
// Optimization: If parts haven't changed, return the original message object.
|
||||
// This preserves referential equality and prevents unnecessary re-renders of ChatMessage (which is memoized).
|
||||
if (filteredParts === message.parts) {
|
||||
return message;
|
||||
}
|
||||
return {
|
||||
...message,
|
||||
parts: filteredParts,
|
||||
};
|
||||
});
|
||||
}, [messages]);
|
||||
|
||||
const { getContextForMessage } = useTurnGrouping(displayMessages);
|
||||
|
||||
@@ -464,7 +464,7 @@ export const ModelControls: React.FC<ModelControlsProps> = ({ className }) => {
|
||||
const editToggleIconClass = sizeVariant === 'mobile' ? 'h-5 w-5' : sizeVariant === 'vscode' ? 'h-4 w-4' : 'h-4 w-4';
|
||||
const controlIconSize = sizeVariant === 'mobile' ? 'h-5 w-5' : sizeVariant === 'vscode' ? 'h-4 w-4' : 'h-4 w-4';
|
||||
const controlTextSize = isCompact ? 'typography-micro' : 'typography-meta';
|
||||
const inlineGapClass = sizeVariant === 'mobile' ? 'gap-x-2' : sizeVariant === 'vscode' ? 'gap-x-1' : 'gap-x-3';
|
||||
const inlineGapClass = sizeVariant === 'mobile' ? 'gap-x-1' : sizeVariant === 'vscode' ? 'gap-x-1' : 'gap-x-3';
|
||||
const editPermissionMenuLabel = editModeShortLabels[effectiveEditMode];
|
||||
|
||||
const renderEditModeIcon = React.useCallback((mode: EditPermissionMode, iconClass = editToggleIconClass) => {
|
||||
@@ -2170,7 +2170,7 @@ export const ModelControls: React.FC<ModelControlsProps> = ({ className }) => {
|
||||
<span
|
||||
className={cn(
|
||||
'model-controls__model-label typography-micro font-medium truncate min-w-0',
|
||||
!isMobile && 'max-w-[220px]',
|
||||
isMobile ? 'max-w-[120px]' : 'max-w-[220px]',
|
||||
)}
|
||||
>
|
||||
{getCurrentModelDisplayName()}
|
||||
@@ -2325,7 +2325,13 @@ export const ModelControls: React.FC<ModelControlsProps> = ({ className }) => {
|
||||
)}
|
||||
>
|
||||
<RiBrainAi3Line className={cn(controlIconSize, 'flex-shrink-0', colorClass)} />
|
||||
<span className={cn('model-controls__variant-label', controlTextSize, 'font-medium truncate min-w-0', colorClass)}>
|
||||
<span className={cn(
|
||||
'model-controls__variant-label',
|
||||
controlTextSize,
|
||||
'font-medium truncate min-w-0',
|
||||
isMobile && 'max-w-[60px]',
|
||||
colorClass
|
||||
)}>
|
||||
{displayVariant}
|
||||
</span>
|
||||
</button>
|
||||
@@ -2561,7 +2567,6 @@ export const ModelControls: React.FC<ModelControlsProps> = ({ className }) => {
|
||||
'model-controls__agent-trigger flex items-center gap-1.5 transition-opacity min-w-0 focus:outline-none',
|
||||
buttonHeight,
|
||||
'cursor-pointer hover:opacity-70',
|
||||
isCompact && 'ml-1'
|
||||
)}
|
||||
>
|
||||
<RiAiAgentLine
|
||||
@@ -2573,7 +2578,12 @@ export const ModelControls: React.FC<ModelControlsProps> = ({ className }) => {
|
||||
style={currentAgentName ? { color: `var(${getAgentColor(currentAgentName).var})` } : undefined}
|
||||
/>
|
||||
<span
|
||||
className={cn('model-controls__agent-label', controlTextSize, 'font-medium truncate min-w-0')}
|
||||
className={cn(
|
||||
'model-controls__agent-label',
|
||||
controlTextSize,
|
||||
'font-medium truncate min-w-0',
|
||||
isMobile && 'max-w-[60px]'
|
||||
)}
|
||||
style={currentAgentName ? { color: `var(${getAgentColor(currentAgentName).var})` } : undefined}
|
||||
>
|
||||
{getAgentDisplayName()}
|
||||
|
||||
Reference in New Issue
Block a user