From db89bf71dedc42d45265c74ddfef6ecc86a13161 Mon Sep 17 00:00:00 2001 From: Ammar Date: Sun, 12 Oct 2025 21:55:14 -0500 Subject: [PATCH 1/2] =?UTF-8?q?=F0=9F=A4=96=20Hover=20AIView=20preview=20i?= =?UTF-8?q?n=20sidebar:=20show=20recent=20messages=20on=20workspace=20hove?= =?UTF-8?q?r=20without=20altering=20active=20view\n\n-=20Add=20AIViewPrevi?= =?UTF-8?q?ew=20(read-only,=20last=20N=20messages,=20ChatProvider=20for=20?= =?UTF-8?q?parity)\n-=20Integrate=20preview=20as=20Tooltip=20content=20aro?= =?UTF-8?q?und=20WorkspaceItem\n-=20Keep=20non-interactive/pointer-events:?= =?UTF-8?q?none=20to=20avoid=20stealing=20focus\n-=20Uses=20existing=20Mes?= =?UTF-8?q?sageRenderer=20for=20full=20parity\n\nGenerated=20with?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/AIViewPreview.tsx | 137 ++++++++++++++++ src/components/ProjectSidebar.tsx | 255 +++++++++++++++++++----------- 2 files changed, 303 insertions(+), 89 deletions(-) create mode 100644 src/components/AIViewPreview.tsx diff --git a/src/components/AIViewPreview.tsx b/src/components/AIViewPreview.tsx new file mode 100644 index 000000000..f15c3c0e2 --- /dev/null +++ b/src/components/AIViewPreview.tsx @@ -0,0 +1,137 @@ +import React, { useMemo } from "react"; +import styled from "@emotion/styled"; +import type { WorkspaceState } from "@/hooks/useWorkspaceAggregators"; +import { MessageRenderer } from "./Messages/MessageRenderer"; +import { ChatProvider } from "@/contexts/ChatContext"; +import { mergeConsecutiveStreamErrors } from "@/utils/messages/messageUtils"; + +interface AIViewPreviewProps { + workspaceId: string; + projectName: string; + branch: string; + workspacePath: string; + workspaceState: WorkspaceState; + maxMessages?: number; + className?: string; +} + +const PreviewContainer = styled.div` + width: 300px; /* match Tooltip width=\"wide\" max-width */ + max-width: min(300px, 80vw); + max-height: 340px; + display: flex; + flex-direction: column; + background: #1f1f1f; + border: 1px solid #3a3a3a; + border-radius: 8px; + box-shadow: 0 10px 30px rgba(0, 0, 0, 0.5); + color: #d4d4d4; + overflow: hidden; + pointer-events: none; /* Keep non-interactive to avoid stealing hover */ +`; + +const PreviewHeader = styled.div` + padding: 6px 10px; + background: #252526; + border-bottom: 1px solid #3e3e42; + font-size: 12px; + font-weight: 600; + color: #cccccc; + display: flex; + justify-content: space-between; + align-items: center; +`; + +const HeaderPath = styled.span` + font-family: var(--font-monospace); + color: #888; + font-weight: 400; + font-size: 11px; +`; + +const PreviewContent = styled.div` + padding: 10px; + overflow: hidden; +`; + +const MessagesScroll = styled.div` + overflow: hidden; /* non-interactive */ +`; + +const FadeBottom = styled.div` + position: absolute; + bottom: 0; + left: 0; + right: 0; + height: 36px; + background: linear-gradient(to bottom, rgba(31, 31, 31, 0), rgba(31, 31, 31, 1)); + pointer-events: none; +`; + +const ContentWrapper = styled.div` + position: relative; +`; + +/** + * Lightweight read-only view of recent messages for hover previews. + * Uses the same MessageRenderer components to ensure visual parity with AIView. + */ +export const AIViewPreview: React.FC = ({ + workspaceId, + projectName, + branch, + workspacePath, + workspaceState, + maxMessages = 6, + className, +}) => { + const merged = useMemo(() => mergeConsecutiveStreamErrors(workspaceState.messages), [ + workspaceState.messages, + ]); + + // Select only the last N messages for brevity + const messages = useMemo(() => merged.slice(Math.max(0, merged.length - maxMessages)), [ + merged, + maxMessages, + ]); + + return ( + + + + + {projectName} / {branch} + + {workspacePath} + + + + + {messages.length === 0 ? ( +
+ No messages yet +
+ ) : ( + messages.map((msg) => ( +
+ +
+ )) + )} +
+ +
+
+
+
+ ); +}; + diff --git a/src/components/ProjectSidebar.tsx b/src/components/ProjectSidebar.tsx index 3c59a0a21..3ff3e04c7 100644 --- a/src/components/ProjectSidebar.tsx +++ b/src/components/ProjectSidebar.tsx @@ -21,6 +21,29 @@ import type { WorkspaceState } from "@/hooks/useWorkspaceAggregators"; import SecretsModal from "./SecretsModal"; import type { Secret } from "@/types/secrets"; import { ForceDeleteModal } from "./ForceDeleteModal"; +import { AIViewPreview } from "./AIViewPreview"; + +// HoverPreviewRenderer isolates preview mount logic to avoid re-render storms and keeps +// the ProjectSidebar lean. It renders a Tooltip portal whose contents are the AIViewPreview. +const HoverPreviewRenderer: React.FC<{ + workspaceId: string; + projectName: string; + branch: string; + workspacePath: string; + workspaceState: WorkspaceState; +}> = ({ workspaceId, projectName, branch, workspacePath, workspaceState }) => { + return ( + + + + ); +}; // Styled Components const SidebarContent = styled.div` @@ -982,100 +1005,159 @@ const ProjectSidebar: React.FC = ({ const isSelected = selectedWorkspace?.workspacePath === workspace.path; + // Compute preview props early to avoid re-computation in hover handlers + const previewProps = { + workspaceId, + projectName, + branch: displayName, + workspacePath: workspace.path, + workspaceState, + } as const; + return ( - - onSelectWorkspace({ - projectPath, - projectName, - workspacePath: workspace.path, - workspaceId, - }) - } - onKeyDown={(e) => { - if (e.key === "Enter" || e.key === " ") { - e.preventDefault(); + + onSelectWorkspace({ projectPath, projectName, workspacePath: workspace.path, workspaceId, - }); + }) } - }} - role="button" - tabIndex={0} - aria-current={isSelected ? "true" : undefined} - data-workspace-path={workspace.path} - data-workspace-id={workspaceId} - > - - { - e.stopPropagation(); - void handleRemoveWorkspace(workspaceId, e.currentTarget); - }} - aria-label={`Remove workspace ${displayName}`} - data-workspace-id={workspaceId} - > - × - - - Remove workspace + onKeyDown={(e) => { + if (e.key === "Enter" || e.key === " ") { + e.preventDefault(); + onSelectWorkspace({ + projectPath, + projectName, + workspacePath: workspace.path, + workspaceId, + }); + } + }} + role="button" + tabIndex={0} + aria-current={isSelected ? "true" : undefined} + data-workspace-path={workspace.path} + data-workspace-id={workspaceId} + > + + {/* Lazy import to avoid bundle growth in main path */} + {/* We avoid dynamic imports per repo rules; component is small */} + {/* Render compact AIViewPreview inside tooltip for hover glance */} + {/* eslint-disable-next-line @typescript-eslint/ban-ts-comment */} + {/* @ts-ignore - imported at top-level */} + {(() => { + // Inline require to avoid circulars; still static import at file top not allowed per size? + // We already created component at src/components/AIViewPreview.tsx + // Importing statically: + return null; + })()} - - - {isEditing ? ( - setEditingName(e.target.value)} - onKeyDown={(e) => handleRenameKeyDown(e, workspaceId)} - onBlur={() => void confirmRename(workspaceId)} - autoFocus - onClick={(e) => e.stopPropagation()} - aria-label={`Rename workspace ${displayName}`} - data-workspace-id={workspaceId} + + { + e.stopPropagation(); + void handleRemoveWorkspace(workspaceId, e.currentTarget); + }} + aria-label={`Remove workspace ${displayName}`} + data-workspace-id={workspaceId} + > + × + + + Remove workspace + + + - ) : ( - { - e.stopPropagation(); - startRenaming(workspaceId, displayName); - }} - title="Double-click to rename" - > - {displayName} - - )} - _onToggleUnread(workspaceId)} - title={ - isStreaming && streamingModel ? ( - - {" "} - is responding - - ) : isStreaming ? ( - "Assistant is responding" - ) : isUnread ? ( - "Unread messages" - ) : ( - "Idle" - ) - } - /> - + /> + {isEditing ? ( + setEditingName(e.target.value)} + onKeyDown={(e) => handleRenameKeyDown(e, workspaceId)} + onBlur={() => void confirmRename(workspaceId)} + autoFocus + onClick={(e) => e.stopPropagation()} + aria-label={`Rename workspace ${displayName}`} + data-workspace-id={workspaceId} + /> + ) : ( + { + e.stopPropagation(); + startRenaming(workspaceId, displayName); + }} + title="Double-click to rename" + > + {displayName} + + )} + _onToggleUnread(workspaceId)} + title={ + isStreaming && streamingModel ? ( + + {" "} + is responding + + ) : isStreaming ? ( + "Assistant is responding" + ) : isUnread ? ( + "Unread messages" + ) : ( + "Idle" + ) + } + /> + + {/* Hover preview portal */} + + + onClick={(e) => e.stopPropagation()} + aria-label={`Rename workspace ${displayName}`} + data-workspace-id={workspaceId} + /> + ) : ( + { + e.stopPropagation(); + startRenaming(workspaceId, displayName); + }} + title="Double-click to rename" + > + {displayName} + + )} + _onToggleUnread(workspaceId)} + title={ + isStreaming + ? "Assistant is responding" + : isUnread + ? "Unread messages" + : "Idle" + } + /> + + {/* Hover preview portal */} + + +>>>>>>> e2cecb3d (🤖 Hover AIView preview in sidebar: show recent messages on workspace hover without altering active view\n\n- Add AIViewPreview (read-only, last N messages, ChatProvider for parity)\n- Integrate preview as Tooltip content around WorkspaceItem\n- Keep non-interactive/pointer-events:none to avoid stealing focus\n- Uses existing MessageRenderer for full parity\n\nGenerated with) {renameError && editingWorkspaceId === workspaceId && ( {renameError} )} @@ -1083,11 +1165,6 @@ const ProjectSidebar: React.FC = ({ ); } )} - - )} - - ); - }) )} From 676946cdf4b0155d44f952a13b1eec6966161d53 Mon Sep 17 00:00:00 2001 From: Ammar Date: Mon, 13 Oct 2025 11:06:54 -0500 Subject: [PATCH 2/2] =?UTF-8?q?=F0=9F=A4=96=20Merge=20main:=20resolve=20co?= =?UTF-8?q?nflicts=20and=20integrate=20ModelDisplay?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/AIViewPreview.tsx | 16 ++++----- src/components/ProjectSidebar.tsx | 56 ++++++------------------------- 2 files changed, 19 insertions(+), 53 deletions(-) diff --git a/src/components/AIViewPreview.tsx b/src/components/AIViewPreview.tsx index f15c3c0e2..c1454e1c3 100644 --- a/src/components/AIViewPreview.tsx +++ b/src/components/AIViewPreview.tsx @@ -85,15 +85,16 @@ export const AIViewPreview: React.FC = ({ maxMessages = 6, className, }) => { - const merged = useMemo(() => mergeConsecutiveStreamErrors(workspaceState.messages), [ - workspaceState.messages, - ]); + const merged = useMemo( + () => mergeConsecutiveStreamErrors(workspaceState.messages), + [workspaceState.messages] + ); // Select only the last N messages for brevity - const messages = useMemo(() => merged.slice(Math.max(0, merged.length - maxMessages)), [ - merged, - maxMessages, - ]); + const messages = useMemo( + () => merged.slice(Math.max(0, merged.length - maxMessages)), + [merged, maxMessages] + ); return ( = ({ ); }; - diff --git a/src/components/ProjectSidebar.tsx b/src/components/ProjectSidebar.tsx index 3ff3e04c7..5400d63f2 100644 --- a/src/components/ProjectSidebar.tsx +++ b/src/components/ProjectSidebar.tsx @@ -1044,24 +1044,18 @@ const ProjectSidebar: React.FC = ({ data-workspace-path={workspace.path} data-workspace-id={workspaceId} > + {/* Hover preview tooltip (does not steal hover) */} - {/* Lazy import to avoid bundle growth in main path */} - {/* We avoid dynamic imports per repo rules; component is small */} - {/* Render compact AIViewPreview inside tooltip for hover glance */} - {/* eslint-disable-next-line @typescript-eslint/ban-ts-comment */} - {/* @ts-ignore - imported at top-level */} - {(() => { - // Inline require to avoid circulars; still static import at file top not allowed per size? - // We already created component at src/components/AIViewPreview.tsx - // Importing statically: - return null; - })()} + { e.stopPropagation(); - void handleRemoveWorkspace(workspaceId, e.currentTarget); + void handleRemoveWorkspace( + workspaceId, + e.currentTarget + ); }} aria-label={`Remove workspace ${displayName}`} data-workspace-id={workspaceId} @@ -1077,7 +1071,6 @@ const ProjectSidebar: React.FC = ({ workspaceId={workspaceId} tooltipPosition="right" /> - /> {isEditing ? ( = ({ {/* Hover preview portal */} - onClick={(e) => e.stopPropagation()} - aria-label={`Rename workspace ${displayName}`} - data-workspace-id={workspaceId} - /> - ) : ( - { - e.stopPropagation(); - startRenaming(workspaceId, displayName); - }} - title="Double-click to rename" - > - {displayName} - - )} - _onToggleUnread(workspaceId)} - title={ - isStreaming - ? "Assistant is responding" - : isUnread - ? "Unread messages" - : "Idle" - } - /> - - {/* Hover preview portal */} - - ->>>>>>> e2cecb3d (🤖 Hover AIView preview in sidebar: show recent messages on workspace hover without altering active view\n\n- Add AIViewPreview (read-only, last N messages, ChatProvider for parity)\n- Integrate preview as Tooltip content around WorkspaceItem\n- Keep non-interactive/pointer-events:none to avoid stealing focus\n- Uses existing MessageRenderer for full parity\n\nGenerated with) {renameError && editingWorkspaceId === workspaceId && ( {renameError} )} @@ -1165,6 +1126,11 @@ const ProjectSidebar: React.FC = ({ ); } )} + + )} + + ); + }) )}