perf: reduce re-renders, fix mobile keyboard handling, add chunk load recovery, and improve PATH management (#1028)

* fix: exclude file content from reverted prompt text

Revert and fork now restore only the user's original prompt, not server-injected file content
Uses existing isSyntheticPart helper for type-safe filtering

* fix: keep scrollbar visible when hovering over thumb

* fix: prevent ESC abort from triggering when terminal is focused

* fix: pass directory to permission/question reply calls so approvals actually resolve

* fix: default model selection not responding after Base UI migration

* fix: prevent modal content from shifting and clipping footer buttons

* fix: improve session switching performance and add sub-agent export with prompt collapse

Defer viewport anchor saving to eliminate ~800ms UI freeze when switching sessions
Add export dialog to include sub-agent tasks recursively in markdown export
Add collapse chevron button for expanded user prompts in sticky header

* fix: resolve sidebar scroll and TDZ crash in session sidebar

* perf: reduce CPU overhead and re-renders across chat, layout, and settings

* fix: position collapse button at top of message and prevent ESC abort in terminal

* fix: position collapse button at top and add padding only when expanded

* refactor: extract shared PATH utilities and mobile keyboard hook

* refactor: import shared path-utils in electron, use module-level style constants

- Electron now imports pathLooksUserConfigured/mergePathValues from
  shared path-utils.js instead of inline duplication
- ToolPart collapsedCustomStyle moved from useMemo([]) to module const

* fix: resolve remaining merge conflicts and type errors

- Remove duplicate variable declarations in SessionNodeItem
- Remove orphaned export callback body from conflict resolution
- Fix HelpDialog description -> descriptionKey (i18n rename)

* fix: resolve type-check and lint errors in session-actions.test.ts

- Added missing bun:test type declarations (beforeEach, mock, mock.module)
- Removed unused State import
- Replaced 'as any' casts with proper OpencodeClient and ChildStoreManager types
- Added eslint-disable for unused _ parameter in mock function

* fix PR 1028 export and PATH edge cases

* fix startup retry exhaustion state

* remove opencode package lock change

* fix sub-session rename cancellation

---------

Co-authored-by: Bohdan Triapitsyn <artmore@protonmail.com>
This commit is contained in:
Islam Nofl
2026-04-26 16:24:07 +03:00
committed by GitHub
co-authored by Bohdan Triapitsyn
parent 632e6cc97b
commit 4523e9c486
87 changed files with 1918 additions and 703 deletions
@@ -5,6 +5,7 @@ import { NumberInput } from '@/components/ui/number-input';
import { Textarea } from '@/components/ui/textarea';
import { toast } from '@/components/ui';
import { useAgentsStore, type AgentConfig, type AgentScope } from '@/stores/useAgentsStore';
import { useShallow } from 'zustand/react/shallow';
import { useDirectorySync } from '@/sync/sync-context';
import { useDirectoryStore } from '@/stores/useDirectoryStore';
import { useDeviceInfo } from '@/lib/device';
@@ -184,7 +185,23 @@ const buildPermissionConfigWithGlobal = (
export const AgentsPage: React.FC = () => {
const { t } = useI18n();
const { isMobile } = useDeviceInfo();
const { selectedAgentName, getAgentByName, createAgent, updateAgent, agents, agentDraft, setAgentDraft } = useAgentsStore();
const {
selectedAgentName,
getAgentByName,
createAgent,
updateAgent,
agents,
agentDraft,
setAgentDraft,
} = useAgentsStore(useShallow((s) => ({
selectedAgentName: s.selectedAgentName,
getAgentByName: s.getAgentByName,
createAgent: s.createAgent,
updateAgent: s.updateAgent,
agents: s.agents,
agentDraft: s.agentDraft,
setAgentDraft: s.setAgentDraft,
})));
const selectedAgent = selectedAgentName ? getAgentByName(selectedAgentName) : null;
const isNewAgent = Boolean(agentDraft && agentDraft.name === selectedAgentName && !selectedAgent);
@@ -19,6 +19,7 @@ import {
} from '@/components/ui/dropdown-menu';
import { RiAddLine, RiAiAgentFill, RiAiAgentLine, RiDeleteBinLine, RiFileCopyLine, RiMore2Line, RiRobot2Line, RiRobotLine, RiRestartLine, RiEditLine } from '@remixicon/react';
import { useAgentsStore, isAgentBuiltIn, isAgentHidden, type AgentScope, type AgentDraft } from '@/stores/useAgentsStore';
import { useShallow } from 'zustand/react/shallow';
import { cn } from '@/lib/utils';
import type { Agent } from '@opencode-ai/sdk/v2';
import { ScrollableOverlay } from '@/components/ui/ScrollableOverlay';
@@ -116,7 +117,15 @@ export const AgentsSidebar: React.FC<AgentsSidebarProps> = ({ onItemSelect }) =>
createAgent,
deleteAgent,
loadAgents,
} = useAgentsStore();
} = useAgentsStore(useShallow((s) => ({
selectedAgentName: s.selectedAgentName,
agents: s.agents,
setSelectedAgent: s.setSelectedAgent,
setAgentDraft: s.setAgentDraft,
createAgent: s.createAgent,
deleteAgent: s.deleteAgent,
loadAgents: s.loadAgents,
})));
React.useEffect(() => {
loadAgents();
@@ -2,6 +2,7 @@ import React from 'react';
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger,
@@ -73,7 +74,7 @@ export const ModelSelector: React.FC<ModelSelectorProps> = ({
const [isDropdownOpen, setIsDropdownOpen] = React.useState(false);
const [searchQuery, setSearchQuery] = React.useState('');
const [selectedIndex, setSelectedIndex] = React.useState(0);
const itemRefs = React.useRef<(HTMLDivElement | null)[]>([]);
const itemRefs = React.useRef<(HTMLElement | null)[]>([]);
const allowedProviderSet = React.useMemo(() => {
if (!Array.isArray(allowedProviderIds) || allowedProviderIds.length === 0) {
@@ -176,14 +177,14 @@ export const ModelSelector: React.FC<ModelSelectorProps> = ({
const showProviderLogo = keyPrefix === 'fav' || keyPrefix === 'recent';
return (
<div
<DropdownMenuItem
key={`${keyPrefix}-${provID}-${modID}`}
ref={(el) => { itemRefs.current[flatIndex] = el; }}
className={cn(
"typography-meta group flex items-center gap-2 px-2 py-1.5 rounded-md cursor-pointer",
isHighlighted ? "bg-interactive-selection" : "hover:bg-interactive-hover/50"
"group flex items-center gap-2",
isHighlighted && "bg-interactive-selection"
)}
onClick={() => handleProviderAndModelChange(provID, modID)}
onSelect={() => handleProviderAndModelChange(provID, modID)}
onMouseEnter={() => setSelectedIndex(flatIndex)}
>
<div className="flex items-center gap-1.5 flex-1 min-w-0">
@@ -223,7 +224,7 @@ export const ModelSelector: React.FC<ModelSelectorProps> = ({
)}
</button>
</div>
</div>
</DropdownMenuItem>
);
};
@@ -613,19 +614,18 @@ export const ModelSelector: React.FC<ModelSelectorProps> = ({
<ScrollableOverlay outerClassName="max-h-[min(400px,calc(100dvh-12rem))] flex-1">
<div className="p-1">
{/* Not selected option */}
<div
<DropdownMenuItem
className={cn(
"typography-meta flex items-center gap-2 px-2 py-1.5 rounded-md cursor-pointer",
"hover:bg-interactive-hover/50"
"flex items-center gap-2",
)}
onClick={() => handleProviderAndModelChange('', '')}
onSelect={() => handleProviderAndModelChange('', '')}
>
<RiCloseLine className="h-3.5 w-3.5 text-muted-foreground" />
<span className="text-muted-foreground">{placeholder || t('settings.agents.modelSelector.notSelected')}</span>
{!providerId && !modelId && (
<RiCheckLine className="h-4 w-4 text-primary ml-auto" />
)}
</div>
</DropdownMenuItem>
<DropdownMenuSeparator />