diff --git a/packages/electron/main.mjs b/packages/electron/main.mjs index 9cff1514..15b8aaff 100644 --- a/packages/electron/main.mjs +++ b/packages/electron/main.mjs @@ -707,6 +707,8 @@ const loadShellEnv = () => { }; // Merge the user's login-shell env (PATH, etc.) into this process before we +import { pathLooksUserConfigured, mergePathValues } from '@openchamber/web/server/lib/opencode/path-utils.js'; + // import/start the server in-process. The server and its children (opencode // CLI, git, etc.) inherit process.env directly now — there is no sidecar // subprocess to hand a custom env to. @@ -716,13 +718,7 @@ const inheritUserShellEnv = () => { const homeDir = os.homedir(); const currentPath = process.env.PATH || ''; - const currentPathLooksUserConfigured = currentPath.split(':').some((segment) => ( - segment.startsWith(`${homeDir}${path.sep}`) - || segment === homeDir - || segment.startsWith('/opt/homebrew/') - || segment.startsWith('/opt/pkg/') - || segment.startsWith('/opt/pmk/') - )); + const currentPathLooksUserConfigured = pathLooksUserConfigured(currentPath, homeDir, ':'); for (const [key, value] of Object.entries(shellEnv)) { if (key === 'PATH') continue; @@ -730,8 +726,10 @@ const inheritUserShellEnv = () => { process.env[key] = value; } } - if (!currentPathLooksUserConfigured && typeof shellEnv.PATH === 'string' && shellEnv.PATH.length > 0) { - process.env.PATH = shellEnv.PATH; + + const shellPath = typeof shellEnv.PATH === 'string' ? shellEnv.PATH : ''; + if (!currentPathLooksUserConfigured && shellPath) { + process.env.PATH = mergePathValues(shellPath, currentPath, ':'); } }; diff --git a/packages/ui/src/App.tsx b/packages/ui/src/App.tsx index 388d0b88..967223a6 100644 --- a/packages/ui/src/App.tsx +++ b/packages/ui/src/App.tsx @@ -5,6 +5,7 @@ import { AgentManagerView } from '@/components/views/agent-manager'; import { ChatView } from '@/components/views'; import { FireworksProvider } from '@/contexts/FireworksContext'; import { Toaster } from '@/components/ui/sonner'; +import { Button } from '@/components/ui/button'; import { MemoryDebugPanel } from '@/components/ui/MemoryDebugPanel'; import { setStreamPerfEnabled } from '@/stores/utils/streamDebug'; import { ErrorBoundary } from '@/components/ui/ErrorBoundary'; @@ -56,6 +57,7 @@ import { QuickOpenDialog } from '@/components/ui/QuickOpenDialog'; import { McpOAuthCallbackPage } from '@/components/sections/mcp/McpOAuthCallbackPage'; import { MCP_OAUTH_CALLBACK_PATH } from '@/components/sections/mcp/mcpOAuth'; import { lazyWithChunkRecovery } from '@/lib/chunkLoadRecovery'; +import { useI18n } from '@/lib/i18n'; // Lazy-loaded heavy views — loaded on demand to reduce initial bundle size. const OnboardingScreen = lazyWithChunkRecovery(() => @@ -73,6 +75,27 @@ const AboutDialogWrapper: React.FC = () => { ); }; +const StartupInitializationRecovery: React.FC<{ + onRetry: () => void; + isRetrying: boolean; +}> = ({ onRetry, isRetrying }) => { + const { t } = useI18n(); + + return ( +
{t('startup.initRecovery.description')}
++{this.state.error.toString()}diff --git a/packages/ui/src/components/chat/ChatInput.tsx b/packages/ui/src/components/chat/ChatInput.tsx index cefd2f2b..80de5dd6 100644 --- a/packages/ui/src/components/chat/ChatInput.tsx +++ b/packages/ui/src/components/chat/ChatInput.tsx @@ -814,11 +814,17 @@ const ChatInputComponent: React.FC= ({ onOpenSettings, scrollTo const sendableAttachedFiles = attachedFiles; + const knownAgentNames = React.useMemo( + () => new Set(agents.map((agent) => agent.name.toLowerCase())), + [agents] + ); + const knownAgentNamesRef = React.useRef(knownAgentNames); + knownAgentNamesRef.current = knownAgentNames; + const hasInlineMentionForHighlight = React.useMemo(() => { if (!message || !message.includes('@') || inputMode === 'shell') { return false; } - const knownAgentNames = new Set(agents.map((agent) => agent.name.toLowerCase())); const mentionRegex = /@([^\s]+)/g; let match: RegExpExecArray | null; while ((match = mentionRegex.exec(message)) !== null) { @@ -839,7 +845,7 @@ const ChatInputComponent: React.FC = ({ onOpenSettings, scrollTo } } return false; - }, [agents, inputMode, message]); + }, [inputMode, message, knownAgentNames]); const highlightedComposerContent = React.useMemo(() => { if (!hasInlineMentionForHighlight) { @@ -847,7 +853,6 @@ const ChatInputComponent: React.FC = ({ onOpenSettings, scrollTo } const parts: Array<{ text: string; mentionKind: 'none' | 'file' | 'agent' }> = []; - const knownAgentNames = new Set(agents.map((agent) => agent.name.toLowerCase())); const mentionRegex = /@([^\s]+)/g; let lastIndex = 0; let match: RegExpExecArray | null; @@ -880,7 +885,7 @@ const ChatInputComponent: React.FC = ({ onOpenSettings, scrollTo } return parts; - }, [agents, hasInlineMentionForHighlight, message]); + }, [hasInlineMentionForHighlight, message, knownAgentNames]); const sanitizeAttachmentsForSend = React.useCallback( (files: AttachedFile[] | undefined): AttachedFile[] => (files ?? []) @@ -900,7 +905,6 @@ const ChatInputComponent: React.FC = ({ onOpenSettings, scrollTo const clientDirectory = opencodeClient.getDirectory() || ''; const root = (chatSearchDirectory || clientDirectory).replace(/\\/g, '/').replace(/\/+$/, ''); - const knownAgentNames = new Set(agents.map((agent) => agent.name.toLowerCase())); const seenPaths = new Set (); const attachments: AttachedFile[] = []; @@ -923,7 +927,7 @@ const ChatInputComponent: React.FC = ({ onOpenSettings, scrollTo continue; } - if (knownAgentNames.has(mentionPath.toLowerCase())) { + if (knownAgentNamesRef.current.has(mentionPath.toLowerCase())) { continue; } @@ -970,7 +974,7 @@ const ChatInputComponent: React.FC = ({ onOpenSettings, scrollTo sanitizedText: rawText, attachments, }; - }, [agents, chatSearchDirectory]); + }, [chatSearchDirectory]); const [autocompleteOverlayPosition, setAutocompleteOverlayPosition] = React.useState (null); const abortTimeoutRef = React.useRef | null>(null); const prevWasAbortedRef = React.useRef(false); @@ -1670,7 +1674,6 @@ const ChatInputComponent: React.FC = ({ onOpenSettings, scrollTo const selectionStart = textarea?.selectionStart ?? message.length; const selectionEnd = textarea?.selectionEnd ?? message.length; const hasCollapsedSelection = selectionStart === selectionEnd; - const knownAgentNames = new Set(agents.map((agent) => agent.name.toLowerCase())); if (hasCollapsedSelection) { const probeIndex = e.key === 'Backspace' ? selectionStart - 1 : selectionStart; @@ -1688,7 +1691,7 @@ const ChatInputComponent: React.FC = ({ onOpenSettings, scrollTo const token = message.slice(tokenStart, tokenEnd); const mentionContent = token.slice(1); const looksLikeFileMention = FILE_MENTION_TOKEN.test(token) - && !knownAgentNames.has(mentionContent.toLowerCase()) + && !knownAgentNamesRef.current.has(mentionContent.toLowerCase()) && isConfirmedFilePath(mentionContent); if (looksLikeFileMention) { diff --git a/packages/ui/src/components/chat/CommandAutocomplete.tsx b/packages/ui/src/components/chat/CommandAutocomplete.tsx index 52d20ee4..12a6bbf4 100644 --- a/packages/ui/src/components/chat/CommandAutocomplete.tsx +++ b/packages/ui/src/components/chat/CommandAutocomplete.tsx @@ -56,8 +56,10 @@ export const CommandAutocomplete = React.forwardRef ([]); const [loading, setLoading] = React.useState(false); - const { commands: commandsWithMetadata, loadCommands: refreshCommands } = useCommandsStore(); - const { skills, loadSkills: refreshSkills } = useSkillsStore(); + const commandsWithMetadata = useCommandsStore((s) => s.commands); + const refreshCommands = useCommandsStore((s) => s.loadCommands); + const skills = useSkillsStore((s) => s.skills); + const refreshSkills = useSkillsStore((s) => s.loadSkills); const [selectedIndex, setSelectedIndex] = React.useState(0); const itemRefs = React.useRef<(HTMLDivElement | null)[]>([]); const containerRef = React.useRef (null); diff --git a/packages/ui/src/components/chat/DiffPreview.tsx b/packages/ui/src/components/chat/DiffPreview.tsx index 7c144f63..4700fba9 100644 --- a/packages/ui/src/components/chat/DiffPreview.tsx +++ b/packages/ui/src/components/chat/DiffPreview.tsx @@ -4,6 +4,23 @@ import { cn } from '@/lib/utils'; import { getLanguageFromExtension } from '@/lib/toolHelpers'; import { parseDiffToUnified } from './message/toolRenderers'; +const DIFF_CUSTOM_STYLE: React.CSSProperties = { + margin: 0, + padding: 0, + fontSize: 'inherit', + background: 'transparent', + backgroundColor: 'transparent', + borderRadius: 0, + overflow: 'visible', + whiteSpace: 'pre-wrap', + wordBreak: 'break-all', + overflowWrap: 'anywhere', +}; + +const DIFF_CODE_TAG_PROPS = { + style: { background: 'transparent', backgroundColor: 'transparent', fontSize: 'inherit' } as React.CSSProperties, +}; + interface DiffPreviewProps { diff: string; syntaxTheme: { [key: string]: React.CSSProperties }; @@ -46,21 +63,8 @@ export const DiffPreview: React.FC = ({ diff, syntaxTheme, fil PreTag="div" wrapLines wrapLongLines - customStyle={{ - margin: 0, - padding: 0, - fontSize: 'inherit', - background: 'transparent', - backgroundColor: 'transparent', - borderRadius: 0, - overflow: 'visible', - whiteSpace: 'pre-wrap', - wordBreak: 'break-all', - overflowWrap: 'anywhere', - }} - codeTagProps={{ - style: { background: 'transparent', backgroundColor: 'transparent', fontSize: 'inherit' }, - }} + customStyle={DIFF_CUSTOM_STYLE} + codeTagProps={DIFF_CODE_TAG_PROPS} > {line.content} @@ -104,21 +108,8 @@ export const WritePreview: React.FC = ({ content, syntaxTheme PreTag="div" wrapLines wrapLongLines - customStyle={{ - margin: 0, - padding: 0, - fontSize: 'inherit', - background: 'transparent', - backgroundColor: 'transparent', - borderRadius: 0, - overflow: 'visible', - whiteSpace: 'pre-wrap', - wordBreak: 'break-all', - overflowWrap: 'anywhere', - }} - codeTagProps={{ - style: { background: 'transparent', backgroundColor: 'transparent', fontSize: 'inherit' }, - }} + customStyle={DIFF_CUSTOM_STYLE} + codeTagProps={DIFF_CODE_TAG_PROPS} > {line || ' '} diff --git a/packages/ui/src/components/chat/FileAttachment.tsx b/packages/ui/src/components/chat/FileAttachment.tsx index d874c2e0..1725bf96 100644 --- a/packages/ui/src/components/chat/FileAttachment.tsx +++ b/packages/ui/src/components/chat/FileAttachment.tsx @@ -522,7 +522,7 @@ export const MessageFilesDisplay = memo(({ files, onShowPopup, compact = false } if (isImage && file.url) { return ( +