From 14c9df626a5075c71cc84cd78f4ce40f59cfdf21 Mon Sep 17 00:00:00 2001 From: Louis Knight-Webb Date: Sat, 17 Jan 2026 13:14:08 +0000 Subject: [PATCH] Add ESLint rule to ban optional props in container components (Vibe Kanban) (#2115) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Done. The ESLint rule is now in place, banning optional props in all ui-new components. **Summary:** - Added the rule to `frontend/.eslintrc.cjs` targeting `TSPropertySignature[optional=true]` - The rule applies to all files under `src/components/ui-new/**/*.{ts,tsx}` **Impact Assessment - 100 violations across:** | Category | Files | Violations | |----------|-------|------------| | **primitives/** | 11 files | ~40 violations | | **views/** | 5 files | ~15 violations | | **containers/** | 5 files | ~10 violations | | **dialogs/** | 7 files | ~15 violations | | **Root-level** | 5 files | ~15 violations | | **types/** | 1 file | 5 violations | The most affected files are: - `primitives/Dropdown.tsx` (14 violations) - `primitives/RepoCard.tsx` (12 violations) - `NewDisplayConversationEntry.tsx` (11 violations) - `primitives/CommandBar.tsx` (5 violations) Would you like me to start refactoring these components to comply with the new rule? * Done. The rule now only applies to `containers/`. **Summary:** - Removed the optional props ban from the general `ui-new/**` override - Added a new override specifically for `src/components/ui-new/containers/**/*.{ts,tsx}` **Impact reduced from 100 to 48 violations**, all now in container components: | File | Violations | |------|------------| | SessionChatBoxContainer.tsx | 11 | | DiffViewCardWithComments.tsx | 9 | | SearchableDropdownContainer.tsx | 6 | | FileTreeContainer.tsx | 3 | | PreviewControlsContainer.tsx | 3 | | ChangesPanelContainer.tsx | 2 | | LogsContentContainer.tsx | 2 | | PreviewBrowserContainer.tsx | 2 | | WorkspacesLayout.tsx | 2 | | WorkspacesMainContainer.tsx | 2 | | + 6 more files | 6 | * All ESLint issues in `SessionChatBoxContainer.tsx` have been fixed. Here's a summary of the changes: 1. **Removed dead code**: - Removed `taskId` prop (never passed by call site) - Removed `workspaceId` aliasing and fallback (`propWorkspaceId ?? session?.workspace_id`) 2. **Updated interface** - Changed all optional props (`?`) to explicit types: - Props that can be undefined use `Type | undefined` instead of `Type?` - Props with defaults (`sessions`, `isNewSessionMode`) are now required in the interface 3. **Added explicit defaults** in destructuring for all props: - `session = undefined`, `onSelectSession = undefined`, `projectId = undefined`, `onStartNewSession = undefined`, `workspaceId = undefined` - `filesChanged = 0`, `linesAdded = 0`, `linesRemoved = 0` - `sessions = []`, `isNewSessionMode = false` * Done. I've refactored `SessionChatBoxContainer.tsx` to use a discriminated union for props. Here's a summary: **Changes made:** 1. **Defined discriminated union type** with three modes: - `existing-session`: Has a `session` (required), `onSelectSession`, `onStartNewSession` - `new-session`: Has a `workspaceId` (required), `onSelectSession` - `placeholder`: Has `workspaceId` (optional), no callbacks 2. **Updated component** to extract mode-specific values from props based on the discriminant 3. **Updated call site** (`WorkspacesMain.tsx`) to pass the correct mode and props based on state **Benefits:** - No more redundant `= undefined` defaults for props that are required in their respective modes - Type safety ensures each mode has exactly the props it needs - Self-documenting code with explicit mode discriminant - ESLint and TypeScript checks pass * Cleanup script changes for workspace f4ec6f6e-a524-4760-889a-7ec2ebbce4c6 * Both lint and type check pass. I've removed `workspaceId` from `PlaceholderProps` since it's not needed when the editor is disabled in placeholder mode. * All tasks are complete. Here's a summary of the changes made: ## Summary I've refactored `DiffViewCardWithComments.tsx` and its related files to fix 11 ESLint errors (9 in DiffViewCardWithComments, 1 in CommentWidgetLine, 1 in ReviewCommentRenderer). ### Changes Made: 1. **`DiffViewCardWithComments.tsx`** - Converted to use discriminated unions: - `DiffInput` type: Changed optional props (`oldPath?`, `changeKind?`, `hasLineNumbers?`) to explicit union types (`oldPath: string | undefined`) - `DiffViewCardWithCommentsProps`: Split into two modes using a discriminated union: - `CollapsibleProps` (mode: 'collapsible') - requires `expanded` and `onToggle` - `StaticProps` (mode: 'static') - always expanded, no toggle - Base props (`status`, `className`, `projectId`, `attemptId`) now use `T | undefined` instead of `T?` 2. **`CommentWidgetLine.tsx`** - Changed `projectId?: string` to `projectId: string | undefined` 3. **`ReviewCommentRenderer.tsx`** - Changed `projectId?: string` to `projectId: string | undefined` 4. **`ChangesPanel.tsx`** (callsite) - Updated to pass the new required props: - Added `mode="collapsible"` - Added `status={undefined}` - Added `className=""` ### Verification: - ✅ ESLint passes for all modified files - ✅ TypeScript type check passes * Cleanup script changes for workspace f4ec6f6e-a524-4760-889a-7ec2ebbce4c6 * Done. Here's a summary of all changes: ## Summary Refactored `DiffViewCardWithComments.tsx` and its callsites to fix ESLint issues: ### Changes Made: 1. **`DiffViewCardWithComments.tsx`**: - Removed unused `status` prop and `ToolStatusDot` rendering - Made `projectId` and `attemptId` required strings (no longer `undefined`) - Removed conditional check for `attemptId` on the Open in IDE button (always shown now) 2. **`ChangesPanel.tsx`**: - Made `projectId` and `attemptId` required strings - Removed `status={undefined}` from the component call 3. **`CommentWidgetLine.tsx`**: - Changed `projectId: string | undefined` to `projectId: string` 4. **`ReviewCommentRenderer.tsx`**: - Changed `projectId: string | undefined` to `projectId: string` 5. **`ChangesPanelContainer.tsx`**: - Made `className` and `attemptId` required props - Added guard: if `projectId` is unavailable, renders empty panel 6. **`WorkspacesLayout.tsx`**: - Added guard: only renders `ChangesPanelContainer` when `selectedWorkspace?.id` exists - Passes required `className=""` prop ### Verification: - ✅ TypeScript type check passes - ✅ ESLint passes for all modified files * Done. Changed `changeKind` from `DiffChangeKind | undefined` to `DiffChangeKind` in the `DiffInput` type. Both TypeScript and ESLint pass. * Done. I've successfully refactored `SearchableDropdownContainer.tsx` to fix the 6 ESLint errors: **Changes made:** 1. **`SearchableDropdownContainer.tsx`** (interface): - `selectedValue: string | null` (removed `?`) - `filterItem: ((item: T, query: string) => boolean) | null` (removed `?`, added `| null`) - `contentClassName: string` (removed `?`) - `placeholder: string` (removed `?`) - `emptyMessage: string` (removed `?`) - `getItemBadge: ((item: T) => string | undefined) | null` (removed `?`, added `| null`) 2. **`SearchableDropdownContainer.tsx`** (implementation): - Removed default values from destructuring (`placeholder = 'Search'`, `emptyMessage = 'No items found'`) - Changed `if (filterItem)` to `if (filterItem !== null)` - Changed `getItemBadge={getItemBadge}` to `getItemBadge={getItemBadge ?? undefined}` 3. **`RepoCardSimple.tsx`** (callsite): - Added `filterItem={null}` - Changed `selectedValue={selectedBranch}` to `selectedValue={selectedBranch ?? null}` to handle the case when `selectedBranch` is `undefined` All ESLint errors for `SearchableDropdownContainer.tsx` are now resolved, and TypeScript compiles without errors. * Refactor FileTreeContainer to fix ESLint errors for optional props - Make all props required in FileTreeContainerProps interface - Add guard in RightSidebar to only render Changes section when selectedWorkspace exists, ensuring workspaceId is always defined - Remove redundant null check for onSelectFile callback Co-Authored-By: Claude Opus 4.5 * Refactor NewDisplayConversationEntry to fix ESLint errors for optional props - Remove dead `task` prop from NewDisplayConversationEntry and ConversationListContainer (was only passed to legacy DisplayConversationEntry for entry types that don't use it) - Make `executionProcessId` and `taskAttempt` required props - Convert internal helper component optional props from `prop?: T` to `prop: T | undefined` to satisfy ESLint while preserving the same runtime behavior Co-Authored-By: Claude Opus 4.5 * Refactor VirtualizedProcessLogs to fix ESLint errors for optional props Made search-related props required instead of optional: - searchQuery: string (was optional) - matchIndices: number[] (was optional) - currentMatchIndex: number (was optional) Updated callsites to provide explicit "no search" values where search functionality is not used. Co-Authored-By: Claude Opus 4.5 * Cleanup script changes for workspace f4ec6f6e-a524-4760-889a-7ec2ebbce4c6 * Refactor container components to fix ESLint errors for optional props Made the following props required (parents always provide them): - BrowseRepoButtonContainer: disabled - CopyButton: disabled - WorkspacesMainContainer: isNewSessionMode, onStartNewSession - LogsContentContainer: className - PreviewBrowserContainer: attemptId, className - PreviewControlsContainer: attemptId, className Changed to union types (legitimately optional values): - LogsContentContainer: command: string | undefined - ProjectSelectorContainer: selectedProjectName: string | undefined Added guards in parent components to only render children when required data is available (RightSidebar, WorkspacesLayout). Co-Authored-By: Claude Opus 4.5 * Cleanup script changes for workspace f4ec6f6e-a524-4760-889a-7ec2ebbce4c6 * Refactor WorkspacesLayout to fix ESLint errors for optional props Extract create mode sections into dedicated container components: - CreateModeProjectSectionContainer: project selector with useCreateMode - CreateModeReposSectionContainer: repos list with branch auto-selection - CreateModeAddReposSectionContainer: add repos section This removes the ModeProviderProps interface with optional props that violated the ESLint rule. Now CreateModeProvider is only rendered when in create mode, and each container manages its own context access. Co-Authored-By: Claude Opus 4.5 * Cleanup script changes for workspace f4ec6f6e-a524-4760-889a-7ec2ebbce4c6 --------- Co-authored-by: Claude Opus 4.5 --- frontend/.eslintrc.cjs | 14 ++ .../dialogs/scripts/ScriptFixerDialog.tsx | 8 +- .../containers/BrowseRepoButtonContainer.tsx | 2 +- .../containers/ChangesPanelContainer.tsx | 20 +- .../ui-new/containers/CommentWidgetLine.tsx | 2 +- .../containers/ConversationListContainer.tsx | 14 +- .../ui-new/containers/CopyButton.tsx | 2 +- .../CreateModeAddReposSectionContainer.tsx | 29 +++ .../CreateModeProjectSectionContainer.tsx | 32 +++ .../CreateModeReposSectionContainer.tsx | 50 ++++ .../containers/DiffViewCardWithComments.tsx | 76 +++--- .../ui-new/containers/FileTreeContainer.tsx | 8 +- .../containers/LogsContentContainer.tsx | 9 +- .../NewDisplayConversationEntry.tsx | 25 +- .../containers/PreviewBrowserContainer.tsx | 4 +- .../containers/PreviewControlsContainer.tsx | 4 +- .../containers/ProjectSelectorContainer.tsx | 2 +- .../containers/ReviewCommentRenderer.tsx | 2 +- .../ui-new/containers/RightSidebar.tsx | 172 ++++---------- .../SearchableDropdownContainer.tsx | 24 +- .../containers/SessionChatBoxContainer.tsx | 101 ++++---- .../containers/VirtualizedProcessLogs.tsx | 22 +- .../ui-new/containers/WorkspacesLayout.tsx | 216 ++++++++---------- .../containers/WorkspacesMainContainer.tsx | 4 +- .../ui-new/primitives/RepoCardSimple.tsx | 3 +- .../components/ui-new/views/ChangesPanel.tsx | 10 +- .../ui-new/views/PreviewControls.tsx | 8 +- .../ui-new/views/WorkspacesMain.tsx | 27 ++- 28 files changed, 474 insertions(+), 416 deletions(-) create mode 100644 frontend/src/components/ui-new/containers/CreateModeAddReposSectionContainer.tsx create mode 100644 frontend/src/components/ui-new/containers/CreateModeProjectSectionContainer.tsx create mode 100644 frontend/src/components/ui-new/containers/CreateModeReposSectionContainer.tsx diff --git a/frontend/.eslintrc.cjs b/frontend/.eslintrc.cjs index e9b291a4..7b5c5386 100644 --- a/frontend/.eslintrc.cjs +++ b/frontend/.eslintrc.cjs @@ -227,6 +227,20 @@ module.exports = { ], }, }, + { + // Container components should not have optional props + files: ['src/components/ui-new/containers/**/*.{ts,tsx}'], + rules: { + 'no-restricted-syntax': [ + 'error', + { + selector: 'TSPropertySignature[optional=true]', + message: + 'Optional props are not allowed in container components. Make the prop required or provide a default value.', + }, + ], + }, + }, { // Logic hooks in ui-new/hooks/ - no JSX allowed files: ['src/components/ui-new/hooks/**/*.{ts,tsx}'], diff --git a/frontend/src/components/dialogs/scripts/ScriptFixerDialog.tsx b/frontend/src/components/dialogs/scripts/ScriptFixerDialog.tsx index 9c13d66e..627351e2 100644 --- a/frontend/src/components/dialogs/scripts/ScriptFixerDialog.tsx +++ b/frontend/src/components/dialogs/scripts/ScriptFixerDialog.tsx @@ -351,7 +351,13 @@ const ScriptFixerDialogImpl = NiceModal.create(
{latestProcess ? ( - + ) : (
{t('scriptFixer.noLogs')} diff --git a/frontend/src/components/ui-new/containers/BrowseRepoButtonContainer.tsx b/frontend/src/components/ui-new/containers/BrowseRepoButtonContainer.tsx index 44f69707..2b61ecc4 100644 --- a/frontend/src/components/ui-new/containers/BrowseRepoButtonContainer.tsx +++ b/frontend/src/components/ui-new/containers/BrowseRepoButtonContainer.tsx @@ -7,7 +7,7 @@ import { IconListItem } from '@/components/ui-new/primitives/IconListItem'; import type { Repo } from 'shared/types'; interface BrowseRepoButtonContainerProps { - disabled?: boolean; + disabled: boolean; onRepoRegistered: (repo: Repo) => void; } diff --git a/frontend/src/components/ui-new/containers/ChangesPanelContainer.tsx b/frontend/src/components/ui-new/containers/ChangesPanelContainer.tsx index 7ab43c13..427b6de7 100644 --- a/frontend/src/components/ui-new/containers/ChangesPanelContainer.tsx +++ b/frontend/src/components/ui-new/containers/ChangesPanelContainer.tsx @@ -131,9 +131,9 @@ function useInViewObserver( } interface ChangesPanelContainerProps { - className?: string; + className: string; /** Attempt ID for opening files in IDE */ - attemptId?: string; + attemptId: string; } export function ChangesPanelContainer({ @@ -201,13 +201,27 @@ export function ChangesPanelContainer({ }); }, [diffs, processedPaths]); + // Guard: Don't render diffs until we have required data + const projectId = task?.project_id; + if (!projectId) { + return ( + + ); + } + return ( ); diff --git a/frontend/src/components/ui-new/containers/CommentWidgetLine.tsx b/frontend/src/components/ui-new/containers/CommentWidgetLine.tsx index d8e24e7d..f5b6d5d7 100644 --- a/frontend/src/components/ui-new/containers/CommentWidgetLine.tsx +++ b/frontend/src/components/ui-new/containers/CommentWidgetLine.tsx @@ -10,7 +10,7 @@ interface CommentWidgetLineProps { widgetKey: string; onSave: () => void; onCancel: () => void; - projectId?: string; + projectId: string; } export function CommentWidgetLine({ diff --git a/frontend/src/components/ui-new/containers/ConversationListContainer.tsx b/frontend/src/components/ui-new/containers/ConversationListContainer.tsx index d699dd88..1cea11c9 100644 --- a/frontend/src/components/ui-new/containers/ConversationListContainer.tsx +++ b/frontend/src/components/ui-new/containers/ConversationListContainer.tsx @@ -17,17 +17,14 @@ import { PatchTypeWithKey, useConversationHistory, } from '@/hooks/useConversationHistory'; -import type { TaskWithAttemptStatus } from 'shared/types'; import type { WorkspaceWithSession } from '@/types/attempt'; interface ConversationListProps { attempt: WorkspaceWithSession; - task?: TaskWithAttemptStatus; } interface MessageListContext { attempt: WorkspaceWithSession; - task?: TaskWithAttemptStatus; } const INITIAL_TOP_ITEM = { index: 'LAST' as const, align: 'end' as const }; @@ -56,7 +53,6 @@ const ItemContent: VirtuosoMessageListProps< MessageListContext >['ItemContent'] = ({ data, context }) => { const attempt = context?.attempt; - const task = context?.task; if (data.type === 'STDOUT') { return

{data.content}

; @@ -64,14 +60,13 @@ const ItemContent: VirtuosoMessageListProps< if (data.type === 'STDERR') { return

{data.content}

; } - if (data.type === 'NORMALIZED_ENTRY') { + if (data.type === 'NORMALIZED_ENTRY' && attempt) { return ( ); } @@ -84,7 +79,7 @@ const computeItemKey: VirtuosoMessageListProps< MessageListContext >['computeItemKey'] = ({ data }) => `conv-${data.patchKey}`; -export function ConversationList({ attempt, task }: ConversationListProps) { +export function ConversationList({ attempt }: ConversationListProps) { const [channelData, setChannelData] = useState | null>(null); const [loading, setLoading] = useState(true); @@ -149,10 +144,7 @@ export function ConversationList({ attempt, task }: ConversationListProps) { useConversationHistory({ attempt, onEntriesUpdated }); const messageListRef = useRef(null); - const messageListContext = useMemo( - () => ({ attempt, task }), - [attempt, task] - ); + const messageListContext = useMemo(() => ({ attempt }), [attempt]); // Determine if content is ready to show (has data or finished loading) const hasContent = !loading || (channelData?.data?.length ?? 0) > 0; diff --git a/frontend/src/components/ui-new/containers/CopyButton.tsx b/frontend/src/components/ui-new/containers/CopyButton.tsx index 6a6644cc..e0a65928 100644 --- a/frontend/src/components/ui-new/containers/CopyButton.tsx +++ b/frontend/src/components/ui-new/containers/CopyButton.tsx @@ -6,7 +6,7 @@ import { Tooltip } from '../primitives/Tooltip'; interface CopyButtonProps { onCopy: () => void; - disabled?: boolean; + disabled: boolean; } /** diff --git a/frontend/src/components/ui-new/containers/CreateModeAddReposSectionContainer.tsx b/frontend/src/components/ui-new/containers/CreateModeAddReposSectionContainer.tsx new file mode 100644 index 00000000..8e2b96ef --- /dev/null +++ b/frontend/src/components/ui-new/containers/CreateModeAddReposSectionContainer.tsx @@ -0,0 +1,29 @@ +import { useMemo } from 'react'; +import { useTranslation } from 'react-i18next'; +import { useCreateMode } from '@/contexts/CreateModeContext'; +import { RecentReposListContainer } from './RecentReposListContainer'; +import { BrowseRepoButtonContainer } from './BrowseRepoButtonContainer'; +import { CreateRepoButtonContainer } from './CreateRepoButtonContainer'; + +export function CreateModeAddReposSectionContainer() { + const { t } = useTranslation(['common']); + const { repos, addRepo } = useCreateMode(); + const registeredRepoPaths = useMemo(() => repos.map((r) => r.path), [repos]); + + return ( +
+

+ {t('common:sections.recent')} +

+ +

+ {t('common:sections.other')} +

+ + +
+ ); +} diff --git a/frontend/src/components/ui-new/containers/CreateModeProjectSectionContainer.tsx b/frontend/src/components/ui-new/containers/CreateModeProjectSectionContainer.tsx new file mode 100644 index 00000000..21ad9003 --- /dev/null +++ b/frontend/src/components/ui-new/containers/CreateModeProjectSectionContainer.tsx @@ -0,0 +1,32 @@ +import { useCallback } from 'react'; +import { useCreateMode } from '@/contexts/CreateModeContext'; +import { useProjects } from '@/hooks/useProjects'; +import { ProjectSelectorContainer } from './ProjectSelectorContainer'; +import { CreateProjectDialog } from '@/components/ui-new/dialogs/CreateProjectDialog'; + +export function CreateModeProjectSectionContainer() { + const { selectedProjectId, setSelectedProjectId, clearRepos } = + useCreateMode(); + const { projects } = useProjects(); + const selectedProject = projects.find((p) => p.id === selectedProjectId); + + const handleCreateProject = useCallback(async () => { + const result = await CreateProjectDialog.show({}); + if (result.status === 'saved') { + setSelectedProjectId(result.project.id); + clearRepos(); + } + }, [setSelectedProjectId, clearRepos]); + + return ( +
+ setSelectedProjectId(p.id)} + onCreateProject={handleCreateProject} + /> +
+ ); +} diff --git a/frontend/src/components/ui-new/containers/CreateModeReposSectionContainer.tsx b/frontend/src/components/ui-new/containers/CreateModeReposSectionContainer.tsx new file mode 100644 index 00000000..70980ece --- /dev/null +++ b/frontend/src/components/ui-new/containers/CreateModeReposSectionContainer.tsx @@ -0,0 +1,50 @@ +import { useMemo, useEffect } from 'react'; +import { useTranslation } from 'react-i18next'; +import { WarningIcon } from '@phosphor-icons/react'; +import { useCreateMode } from '@/contexts/CreateModeContext'; +import { useMultiRepoBranches } from '@/hooks/useRepoBranches'; +import { SelectedReposList } from '@/components/ui-new/primitives/SelectedReposList'; + +export function CreateModeReposSectionContainer() { + const { t } = useTranslation(['tasks']); + const { repos, removeRepo, targetBranches, setTargetBranch } = + useCreateMode(); + + const repoIds = useMemo(() => repos.map((r) => r.id), [repos]); + const { branchesByRepo } = useMultiRepoBranches(repoIds); + + useEffect(() => { + repos.forEach((repo) => { + const branches = branchesByRepo[repo.id]; + if (branches && !targetBranches[repo.id]) { + const currentBranch = branches.find((b) => b.is_current); + if (currentBranch) { + setTargetBranch(repo.id, currentBranch.name); + } + } + }); + }, [repos, branchesByRepo, targetBranches, setTargetBranch]); + + if (repos.length === 0) { + return ( +
+
+ +

+ {t('gitPanel.create.warnings.noReposSelected')} +

+
+
+ ); + } + + return ( + + ); +} diff --git a/frontend/src/components/ui-new/containers/DiffViewCardWithComments.tsx b/frontend/src/components/ui-new/containers/DiffViewCardWithComments.tsx index ab38d739..956bf796 100644 --- a/frontend/src/components/ui-new/containers/DiffViewCardWithComments.tsx +++ b/frontend/src/components/ui-new/containers/DiffViewCardWithComments.tsx @@ -26,8 +26,7 @@ import { import { CommentWidgetLine } from './CommentWidgetLine'; import { ReviewCommentRenderer } from './ReviewCommentRenderer'; import { GitHubCommentRenderer } from './GitHubCommentRenderer'; -import type { ToolStatus, DiffChangeKind } from 'shared/types'; -import { ToolStatusDot } from '../primitives/conversation/ToolStatusDot'; +import type { DiffChangeKind } from 'shared/types'; import { OpenInIdeButton } from '@/components/ide/OpenInIdeButton'; import { useOpenInEditor } from '@/hooks/useOpenInEditor'; import '@/styles/diff-style-overrides.css'; @@ -44,34 +43,45 @@ export type DiffInput = type: 'content'; oldContent: string; newContent: string; - oldPath?: string; + oldPath: string | undefined; newPath: string; - changeKind?: DiffChangeKind; + changeKind: DiffChangeKind; } | { type: 'unified'; path: string; unifiedDiff: string; - hasLineNumbers?: boolean; + hasLineNumbers: boolean; }; -interface DiffViewCardWithCommentsProps { +/** Base props shared across all modes */ +interface BaseProps { /** Diff data - either raw content or unified diff string */ input: DiffInput; - /** Expansion state */ - expanded?: boolean; - /** Toggle expansion callback */ - onToggle?: () => void; - /** Optional status indicator */ - status?: ToolStatus; /** Additional className */ - className?: string; + className: string; /** Project ID for @ mentions in comments */ - projectId?: string; + projectId: string; /** Attempt ID for opening files in IDE */ - attemptId?: string; + attemptId: string; } +/** Props for collapsible mode (with expand/collapse) */ +interface CollapsibleProps extends BaseProps { + mode: 'collapsible'; + /** Expansion state */ + expanded: boolean; + /** Toggle expansion callback */ + onToggle: () => void; +} + +/** Props for static mode (always expanded, no toggle) */ +interface StaticProps extends BaseProps { + mode: 'static'; +} + +type DiffViewCardWithCommentsProps = CollapsibleProps | StaticProps; + interface DiffData { diffFile: DiffFile | null; additions: number; @@ -168,15 +178,13 @@ function useDiffData(input: DiffInput): DiffData { }, [input]); } -export function DiffViewCardWithComments({ - input, - expanded = false, - onToggle, - status, - className, - projectId, - attemptId, -}: DiffViewCardWithCommentsProps) { +export function DiffViewCardWithComments(props: DiffViewCardWithCommentsProps) { + const { input, className, projectId, attemptId, mode } = props; + + // Extract mode-specific values + const expanded = mode === 'collapsible' ? props.expanded : true; + const onToggle = mode === 'collapsible' ? props.onToggle : undefined; + const { theme } = useTheme(); const actualTheme = getActualTheme(theme); const globalMode = useDiffViewMode(); @@ -349,12 +357,6 @@ export function DiffViewCardWithComments({ > - {status && ( - - )} {changeLabel && ( )}
- {attemptId && ( - e.stopPropagation()}> - - - )} + e.stopPropagation()}> + + {onToggle && ( void; - className?: string; + onSelectFile: (path: string, diff: Diff) => void; + className: string; } export function FileTreeContainer({ @@ -118,7 +118,7 @@ export function FileTreeContainer({ setSelectedPath(path); // Find the diff for this path const diff = diffs.find((d) => d.newPath === path || d.oldPath === path); - if (diff && onSelectFile) { + if (diff) { onSelectFile(path, diff); } }, diff --git a/frontend/src/components/ui-new/containers/LogsContentContainer.tsx b/frontend/src/components/ui-new/containers/LogsContentContainer.tsx index bb8b384e..99735d3e 100644 --- a/frontend/src/components/ui-new/containers/LogsContentContainer.tsx +++ b/frontend/src/components/ui-new/containers/LogsContentContainer.tsx @@ -10,10 +10,15 @@ import { useLogsPanel } from '@/contexts/LogsPanelContext'; export type LogsPanelContent = | { type: 'process'; processId: string } - | { type: 'tool'; toolName: string; content: string; command?: string }; + | { + type: 'tool'; + toolName: string; + content: string; + command: string | undefined; + }; interface LogsContentContainerProps { - className?: string; + className: string; } export function LogsContentContainer({ className }: LogsContentContainerProps) { diff --git a/frontend/src/components/ui-new/containers/NewDisplayConversationEntry.tsx b/frontend/src/components/ui-new/containers/NewDisplayConversationEntry.tsx index 62454a22..597c309a 100644 --- a/frontend/src/components/ui-new/containers/NewDisplayConversationEntry.tsx +++ b/frontend/src/components/ui-new/containers/NewDisplayConversationEntry.tsx @@ -6,7 +6,6 @@ import { NormalizedEntry, ToolStatus, TodoItem, - type TaskWithAttemptStatus, type RepoWithTargetBranch, } from 'shared/types'; import type { WorkspaceWithSession } from '@/types/attempt'; @@ -42,9 +41,8 @@ import type { DiffInput } from '../primitives/conversation/DiffViewCard'; type Props = { entry: NormalizedEntry; expansionKey: string; - executionProcessId?: string; - taskAttempt?: WorkspaceWithSession; - task?: TaskWithAttemptStatus; + executionProcessId: string; + taskAttempt: WorkspaceWithSession; }; type FileEditAction = Extract; @@ -260,7 +258,7 @@ function renderToolUseEntry( function NewDisplayConversationEntry(props: Props) { const { t } = useTranslation('common'); - const { entry, expansionKey, executionProcessId, taskAttempt, task } = props; + const { entry, expansionKey, executionProcessId, taskAttempt } = props; const entryType = entry.entry_type; switch (entryType.type) { @@ -326,7 +324,6 @@ function NewDisplayConversationEntry(props: Props) { expansionKey={expansionKey} executionProcessId={executionProcessId} taskAttempt={taskAttempt} - task={task} /> ); @@ -426,7 +423,7 @@ function PlanEntry({ }: { plan: string; expansionKey: string; - workspaceId?: string; + workspaceId: string | undefined; status: ToolStatus; }) { const { t } = useTranslation('common'); @@ -470,7 +467,7 @@ function GenericToolApprovalEntry({ toolName: string; content: string; expansionKey: string; - workspaceId?: string; + workspaceId: string | undefined; status: ToolStatus; }) { const [expanded, toggle] = usePersistedExpanded( @@ -501,8 +498,8 @@ function UserMessageEntry({ }: { content: string; expansionKey: string; - workspaceId?: string; - executionProcessId?: string; + workspaceId: string | undefined; + executionProcessId: string | undefined; }) { const [expanded, toggle] = usePersistedExpanded(`user:${expansionKey}`, true); const { startEdit, isEntryGreyed, isInEditMode } = useMessageEditContext(); @@ -538,7 +535,7 @@ function AssistantMessageEntry({ workspaceId, }: { content: string; - workspaceId?: string; + workspaceId: string | undefined; }) { return ; } @@ -559,7 +556,7 @@ function ToolSummaryEntry({ status: ToolStatus; content: string; toolName: string; - command?: string; + command: string | undefined; }) { const [expanded, toggle] = usePersistedExpanded( `tool:${expansionKey}`, @@ -654,8 +651,8 @@ function ScriptEntryWithFix({ processId: string; exitCode: number | null; status: ToolStatus; - workspaceId?: string; - sessionId?: string; + workspaceId: string | undefined; + sessionId: string | undefined; }) { // Try to get repos from workspace context - may not be available in all contexts let repos: RepoWithTargetBranch[] = []; diff --git a/frontend/src/components/ui-new/containers/PreviewBrowserContainer.tsx b/frontend/src/components/ui-new/containers/PreviewBrowserContainer.tsx index 8f0ca66a..912b4033 100644 --- a/frontend/src/components/ui-new/containers/PreviewBrowserContainer.tsx +++ b/frontend/src/components/ui-new/containers/PreviewBrowserContainer.tsx @@ -27,8 +27,8 @@ const MIN_RESPONSIVE_WIDTH = 320; const MIN_RESPONSIVE_HEIGHT = 480; interface PreviewBrowserContainerProps { - attemptId?: string; - className?: string; + attemptId: string; + className: string; } export function PreviewBrowserContainer({ diff --git a/frontend/src/components/ui-new/containers/PreviewControlsContainer.tsx b/frontend/src/components/ui-new/containers/PreviewControlsContainer.tsx index 45ebbc62..06a2457d 100644 --- a/frontend/src/components/ui-new/containers/PreviewControlsContainer.tsx +++ b/frontend/src/components/ui-new/containers/PreviewControlsContainer.tsx @@ -10,8 +10,8 @@ import { useWorkspaceContext } from '@/contexts/WorkspaceContext'; import { useLogsPanel } from '@/contexts/LogsPanelContext'; interface PreviewControlsContainerProps { - attemptId?: string; - className?: string; + attemptId: string; + className: string; } export function PreviewControlsContainer({ diff --git a/frontend/src/components/ui-new/containers/ProjectSelectorContainer.tsx b/frontend/src/components/ui-new/containers/ProjectSelectorContainer.tsx index d778b64d..eabe2ac8 100644 --- a/frontend/src/components/ui-new/containers/ProjectSelectorContainer.tsx +++ b/frontend/src/components/ui-new/containers/ProjectSelectorContainer.tsx @@ -17,7 +17,7 @@ import type { Project } from 'shared/types'; interface ProjectSelectorContainerProps { projects: Project[]; selectedProjectId: string | null; - selectedProjectName?: string; + selectedProjectName: string | undefined; onProjectSelect: (project: Project) => void; onCreateProject: () => void; } diff --git a/frontend/src/components/ui-new/containers/ReviewCommentRenderer.tsx b/frontend/src/components/ui-new/containers/ReviewCommentRenderer.tsx index 98cead43..927ed3e4 100644 --- a/frontend/src/components/ui-new/containers/ReviewCommentRenderer.tsx +++ b/frontend/src/components/ui-new/containers/ReviewCommentRenderer.tsx @@ -7,7 +7,7 @@ import { useReview, type ReviewComment } from '@/contexts/ReviewProvider'; interface ReviewCommentRendererProps { comment: ReviewComment; - projectId?: string; + projectId: string; } export function ReviewCommentRenderer({ diff --git a/frontend/src/components/ui-new/containers/RightSidebar.tsx b/frontend/src/components/ui-new/containers/RightSidebar.tsx index 03fad86e..5719d9fe 100644 --- a/frontend/src/components/ui-new/containers/RightSidebar.tsx +++ b/frontend/src/components/ui-new/containers/RightSidebar.tsx @@ -1,22 +1,14 @@ -import { useMemo, useEffect, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; import { FileTreeContainer } from '@/components/ui-new/containers/FileTreeContainer'; import { ProcessListContainer } from '@/components/ui-new/containers/ProcessListContainer'; import { PreviewControlsContainer } from '@/components/ui-new/containers/PreviewControlsContainer'; import { GitPanelContainer } from '@/components/ui-new/containers/GitPanelContainer'; import { TerminalPanelContainer } from '@/components/ui-new/containers/TerminalPanelContainer'; -import { ProjectSelectorContainer } from '@/components/ui-new/containers/ProjectSelectorContainer'; -import { RecentReposListContainer } from '@/components/ui-new/containers/RecentReposListContainer'; -import { BrowseRepoButtonContainer } from '@/components/ui-new/containers/BrowseRepoButtonContainer'; -import { CreateRepoButtonContainer } from '@/components/ui-new/containers/CreateRepoButtonContainer'; +import { CreateModeProjectSectionContainer } from '@/components/ui-new/containers/CreateModeProjectSectionContainer'; +import { CreateModeReposSectionContainer } from '@/components/ui-new/containers/CreateModeReposSectionContainer'; +import { CreateModeAddReposSectionContainer } from '@/components/ui-new/containers/CreateModeAddReposSectionContainer'; import { useChangesView } from '@/contexts/ChangesViewContext'; import { useWorkspaceContext } from '@/contexts/WorkspaceContext'; -import { useCreateMode } from '@/contexts/CreateModeContext'; -import { useMultiRepoBranches } from '@/hooks/useRepoBranches'; -import { useProjects } from '@/hooks/useProjects'; -import { CreateProjectDialog } from '@/components/ui-new/dialogs/CreateProjectDialog'; -import { SelectedReposList } from '@/components/ui-new/primitives/SelectedReposList'; -import { WarningIcon } from '@phosphor-icons/react'; import type { Workspace, RepoWithTargetBranch } from 'shared/types'; import { RIGHT_MAIN_PANEL_MODES, @@ -56,40 +48,6 @@ export function RightSidebar({ const { setExpanded } = useExpandedAll(); const isTerminalVisible = useUiPreferencesStore((s) => s.isTerminalVisible); - const { - repos: createRepos, - addRepo, - removeRepo, - clearRepos, - targetBranches, - setTargetBranch, - selectedProjectId, - setSelectedProjectId, - } = useCreateMode(); - const { projects } = useProjects(); - - const repoIds = useMemo(() => createRepos.map((r) => r.id), [createRepos]); - const { branchesByRepo } = useMultiRepoBranches(repoIds); - - useEffect(() => { - if (!isCreateMode) return; - createRepos.forEach((repo) => { - const branches = branchesByRepo[repo.id]; - if (branches && !targetBranches[repo.id]) { - const currentBranch = branches.find((b) => b.is_current); - if (currentBranch) { - setTargetBranch(repo.id, currentBranch.name); - } - } - }); - }, [ - isCreateMode, - createRepos, - branchesByRepo, - targetBranches, - setTargetBranch, - ]); - const [changesExpanded] = usePersistedExpanded( PERSIST_KEYS.changesSection, true @@ -111,22 +69,6 @@ export function RightSidebar({ true ); - const selectedProject = projects.find((p) => p.id === selectedProjectId); - const registeredRepoPaths = useMemo( - () => createRepos.map((r) => r.path), - [createRepos] - ); - - const handleCreateProject = useCallback(async () => { - const result = await CreateProjectDialog.show({}); - if (result.status === 'saved') { - setSelectedProjectId(result.project.id); - clearRepos(); - } - }, [setSelectedProjectId, clearRepos]); - - const hasNoRepos = createRepos.length === 0; - const hasUpperContent = rightMainPanelMode === RIGHT_MAIN_PANEL_MODES.CHANGES || rightMainPanelMode === RIGHT_MAIN_PANEL_MODES.LOGS || @@ -151,63 +93,21 @@ export function RightSidebar({ persistKey: PERSIST_KEYS.gitPanelProject, visible: true, expanded: true, - content: ( -
- setSelectedProjectId(p.id)} - onCreateProject={handleCreateProject} - /> -
- ), + content: , }, { title: t('common:sections.repositories'), persistKey: PERSIST_KEYS.gitPanelRepositories, visible: true, expanded: true, - content: hasNoRepos ? ( -
-
- -

- {t('gitPanel.create.warnings.noReposSelected')} -

-
-
- ) : ( - - ), + content: , }, { title: t('common:sections.addRepositories'), persistKey: PERSIST_KEYS.gitPanelAddRepositories, visible: true, expanded: true, - content: ( -
-

- {t('common:sections.recent')} -

- -

- {t('common:sections.other')} -

- - -
- ), + content: , }, ] : buildWorkspaceSections(); @@ -238,23 +138,26 @@ export function RightSidebar({ switch (rightMainPanelMode) { case RIGHT_MAIN_PANEL_MODES.CHANGES: - result.unshift({ - title: 'Changes', - persistKey: PERSIST_KEYS.changesSection, - visible: hasUpperContent, - expanded: upperExpanded, - content: ( - { - selectFile(path); - setExpanded(`diff:${path}`, true); - }} - /> - ), - }); + if (selectedWorkspace) { + result.unshift({ + title: 'Changes', + persistKey: PERSIST_KEYS.changesSection, + visible: hasUpperContent, + expanded: upperExpanded, + content: ( + { + selectFile(path); + setExpanded(`diff:${path}`, true); + }} + className="" + /> + ), + }); + } break; case RIGHT_MAIN_PANEL_MODES.LOGS: result.unshift({ @@ -266,15 +169,20 @@ export function RightSidebar({ }); break; case RIGHT_MAIN_PANEL_MODES.PREVIEW: - result.unshift({ - title: 'Preview', - persistKey: PERSIST_KEYS.rightPanelPreview, - visible: hasUpperContent, - expanded: upperExpanded, - content: ( - - ), - }); + if (selectedWorkspace) { + result.unshift({ + title: 'Preview', + persistKey: PERSIST_KEYS.rightPanelPreview, + visible: hasUpperContent, + expanded: upperExpanded, + content: ( + + ), + }); + } break; case null: break; diff --git a/frontend/src/components/ui-new/containers/SearchableDropdownContainer.tsx b/frontend/src/components/ui-new/containers/SearchableDropdownContainer.tsx index 0b971324..7d12332c 100644 --- a/frontend/src/components/ui-new/containers/SearchableDropdownContainer.tsx +++ b/frontend/src/components/ui-new/containers/SearchableDropdownContainer.tsx @@ -6,14 +6,14 @@ interface SearchableDropdownContainerProps { /** Array of items to display */ items: T[]; /** Currently selected value (matched against getItemKey) */ - selectedValue?: string | null; + selectedValue: string | null; /** Extract unique key from item */ getItemKey: (item: T) => string; /** Extract display label from item */ getItemLabel: (item: T) => string; - /** Custom filter function (defaults to label.includes(query)) */ - filterItem?: (item: T, query: string) => boolean; + /** Custom filter function (null = default label.includes(query)) */ + filterItem: ((item: T, query: string) => boolean) | null; /** Called when an item is selected */ onSelect: (item: T) => void; @@ -22,14 +22,14 @@ interface SearchableDropdownContainerProps { trigger: React.ReactNode; /** Class name for dropdown content */ - contentClassName?: string; + contentClassName: string; /** Placeholder text for search input */ - placeholder?: string; + placeholder: string; /** Message shown when no items match */ - emptyMessage?: string; + emptyMessage: string; - /** Optional badge text for each item */ - getItemBadge?: (item: T) => string | undefined; + /** Badge text for each item (null = no badges) */ + getItemBadge: ((item: T) => string | undefined) | null; } export function SearchableDropdownContainer({ @@ -41,8 +41,8 @@ export function SearchableDropdownContainer({ onSelect, trigger, contentClassName, - placeholder = 'Search', - emptyMessage = 'No items found', + placeholder, + emptyMessage, getItemBadge, }: SearchableDropdownContainerProps) { const [searchTerm, setSearchTerm] = useState(''); @@ -53,7 +53,7 @@ export function SearchableDropdownContainer({ const filteredItems = useMemo(() => { if (!searchTerm.trim()) return items; const query = searchTerm.toLowerCase(); - if (filterItem) { + if (filterItem !== null) { return items.filter((item) => filterItem(item, query)); } return items.filter((item) => @@ -160,7 +160,7 @@ export function SearchableDropdownContainer({ contentClassName={contentClassName} placeholder={placeholder} emptyMessage={emptyMessage} - getItemBadge={getItemBadge} + getItemBadge={getItemBadge ?? undefined} /> ); } diff --git a/frontend/src/components/ui-new/containers/SessionChatBoxContainer.tsx b/frontend/src/components/ui-new/containers/SessionChatBoxContainer.tsx index 5589507b..dca1a8a7 100644 --- a/frontend/src/components/ui-new/containers/SessionChatBoxContainer.tsx +++ b/frontend/src/components/ui-new/containers/SessionChatBoxContainer.tsx @@ -58,45 +58,68 @@ function computeExecutionStatus(params: { return 'idle'; } -interface SessionChatBoxContainerProps { - /** The current session */ - session?: Session; - /** Task ID for execution tracking */ - taskId?: string; - /** Number of files changed in current session */ - filesChanged?: number; - /** Number of lines added */ - linesAdded?: number; - /** Number of lines removed */ - linesRemoved?: number; +/** Shared props across all modes */ +interface SharedProps { /** Available sessions for this workspace */ - sessions?: Session[]; - /** Called when a session is selected */ - onSelectSession?: (sessionId: string) => void; + sessions: Session[]; /** Project ID for file search in typeahead */ - projectId?: string; - /** Whether user is creating a new session */ - isNewSessionMode?: boolean; - /** Callback to start new session mode */ - onStartNewSession?: () => void; - /** Workspace ID for creating new sessions */ - workspaceId?: string; + projectId: string | undefined; + /** Number of files changed in current session */ + filesChanged: number; + /** Number of lines added */ + linesAdded: number; + /** Number of lines removed */ + linesRemoved: number; } -export function SessionChatBoxContainer({ - session, - taskId, - filesChanged, - linesAdded, - linesRemoved, - sessions = [], - onSelectSession, - projectId, - isNewSessionMode = false, - onStartNewSession, - workspaceId: propWorkspaceId, -}: SessionChatBoxContainerProps) { - const workspaceId = propWorkspaceId ?? session?.workspace_id; +/** Props for existing session mode */ +interface ExistingSessionProps extends SharedProps { + mode: 'existing-session'; + /** The current session */ + session: Session; + /** Called when a session is selected */ + onSelectSession: (sessionId: string) => void; + /** Callback to start new session mode */ + onStartNewSession: (() => void) | undefined; +} + +/** Props for new session mode */ +interface NewSessionProps extends SharedProps { + mode: 'new-session'; + /** Workspace ID for creating new sessions */ + workspaceId: string; + /** Called when a session is selected */ + onSelectSession: (sessionId: string) => void; +} + +/** Props for placeholder mode (no workspace selected) */ +interface PlaceholderProps extends SharedProps { + mode: 'placeholder'; +} + +type SessionChatBoxContainerProps = + | ExistingSessionProps + | NewSessionProps + | PlaceholderProps; + +export function SessionChatBoxContainer(props: SessionChatBoxContainerProps) { + const { mode, sessions, projectId, filesChanged, linesAdded, linesRemoved } = + props; + + // Extract mode-specific values + const session = mode === 'existing-session' ? props.session : undefined; + const workspaceId = + mode === 'existing-session' + ? props.session.workspace_id + : mode === 'new-session' + ? props.workspaceId + : undefined; + const isNewSessionMode = mode === 'new-session'; + const onSelectSession = + mode === 'placeholder' ? undefined : props.onSelectSession; + const onStartNewSession = + mode === 'existing-session' ? props.onStartNewSession : undefined; + const sessionId = session?.id; const queryClient = useQueryClient(); @@ -151,7 +174,7 @@ export function SessionChatBoxContainer({ // Execution state const { isAttemptRunning, stopExecution, isStopping, processes } = - useAttemptExecution(workspaceId, taskId); + useAttemptExecution(workspaceId); // Approval feedback context const feedbackContext = useApprovalFeedbackOptional(); @@ -557,12 +580,8 @@ export function SessionChatBoxContainer({ localMessage, ]); - // Render placeholder state if no session and not in new session mode - // This maintains the visual structure during workspace transitions - const isPlaceholderMode = !session && !isNewSessionMode; - // In placeholder mode, render a disabled version to maintain visual structure - if (isPlaceholderMode) { + if (mode === 'placeholder') { return ( ['ItemContent'] = ({ data, context }) => { - const isMatch = context?.matchIndices?.includes(data.originalIndex) ?? false; + const isMatch = context.matchIndices.includes(data.originalIndex); const isCurrentMatch = - context?.matchIndices?.[context?.currentMatchIndex ?? -1] === - data.originalIndex; + context.matchIndices[context.currentMatchIndex] === data.originalIndex; return ( ); @@ -121,9 +120,8 @@ export function VirtualizedProcessLogs({ // Scroll to current match when it changes useEffect(() => { if ( - matchIndices && matchIndices.length > 0 && - currentMatchIndex !== undefined && + currentMatchIndex >= 0 && currentMatchIndex !== prevCurrentMatchRef.current ) { const logIndex = matchIndices[currentMatchIndex]; diff --git a/frontend/src/components/ui-new/containers/WorkspacesLayout.tsx b/frontend/src/components/ui-new/containers/WorkspacesLayout.tsx index 1fb1f122..aa47cd1d 100644 --- a/frontend/src/components/ui-new/containers/WorkspacesLayout.tsx +++ b/frontend/src/components/ui-new/containers/WorkspacesLayout.tsx @@ -1,4 +1,4 @@ -import { useEffect, type ReactNode } from 'react'; +import { useEffect } from 'react'; import { Group, Layout, Panel, Separator } from 'react-resizable-panels'; import { useWorkspaceContext } from '@/contexts/WorkspaceContext'; import { ExecutionProcessesProvider } from '@/contexts/ExecutionProcessesContext'; @@ -29,37 +29,6 @@ import { useCommandBarShortcut } from '@/hooks/useCommandBarShortcut'; const WORKSPACES_GUIDE_ID = 'workspaces-guide'; -interface ModeProviderProps { - isCreateMode: boolean; - executionProps: { - key: string; - attemptId?: string; - sessionId?: string; - }; - children: ReactNode; -} - -function ModeProvider({ - isCreateMode, - executionProps, - children, -}: ModeProviderProps) { - if (isCreateMode) { - return {children}; - } - return ( - - - {children} - - - ); -} - export function WorkspacesLayout() { const { workspaceId, @@ -135,6 +104,89 @@ export function WorkspacesLayout() { setRightMainPanelSize(layout['right-main']); }; + const mainContent = ( + + + +
+ + {isLeftMainPanelVisible && ( + + {isCreateMode ? ( + + ) : ( + + )} + + )} + + {isLeftMainPanelVisible && rightMainPanelMode !== null && ( + + )} + + {rightMainPanelMode !== null && ( + + {rightMainPanelMode === RIGHT_MAIN_PANEL_MODES.CHANGES && + selectedWorkspace?.id && ( + + )} + {rightMainPanelMode === RIGHT_MAIN_PANEL_MODES.LOGS && ( + + )} + {rightMainPanelMode === RIGHT_MAIN_PANEL_MODES.PREVIEW && + selectedWorkspace?.id && ( + + )} + + )} + + + {isRightSidebarVisible && ( +
+ +
+ )} +
+
+
+
+ ); + return (
@@ -146,95 +198,17 @@ export function WorkspacesLayout() { )}
- - - - -
- - {isLeftMainPanelVisible && ( - - {isCreateMode ? ( - - ) : ( - - )} - - )} - - {isLeftMainPanelVisible && - rightMainPanelMode !== null && ( - - )} - - {rightMainPanelMode !== null && ( - - {rightMainPanelMode === - RIGHT_MAIN_PANEL_MODES.CHANGES && ( - - )} - {rightMainPanelMode === - RIGHT_MAIN_PANEL_MODES.LOGS && ( - - )} - {rightMainPanelMode === - RIGHT_MAIN_PANEL_MODES.PREVIEW && ( - - )} - - )} - - - {isRightSidebarVisible && ( -
- -
- )} -
-
-
-
-
+ {isCreateMode ? ( + {mainContent} + ) : ( + + {mainContent} + + )}
diff --git a/frontend/src/components/ui-new/containers/WorkspacesMainContainer.tsx b/frontend/src/components/ui-new/containers/WorkspacesMainContainer.tsx index cfcd8988..3a12f03c 100644 --- a/frontend/src/components/ui-new/containers/WorkspacesMainContainer.tsx +++ b/frontend/src/components/ui-new/containers/WorkspacesMainContainer.tsx @@ -12,9 +12,9 @@ interface WorkspacesMainContainerProps { onSelectSession: (sessionId: string) => void; isLoading: boolean; /** Whether user is creating a new session */ - isNewSessionMode?: boolean; + isNewSessionMode: boolean; /** Callback to start new session mode */ - onStartNewSession?: () => void; + onStartNewSession: () => void; } export function WorkspacesMainContainer({ diff --git a/frontend/src/components/ui-new/primitives/RepoCardSimple.tsx b/frontend/src/components/ui-new/primitives/RepoCardSimple.tsx index 0b83f4b2..6fc979c8 100644 --- a/frontend/src/components/ui-new/primitives/RepoCardSimple.tsx +++ b/frontend/src/components/ui-new/primitives/RepoCardSimple.tsx @@ -46,9 +46,10 @@ export function RepoCardSimple({ {branches && onBranchChange && ( b.name} getItemLabel={(b) => b.name} + filterItem={null} getItemBadge={(b) => (b.is_current ? 'Current' : undefined)} onSelect={(b) => onBranchChange(b.name)} placeholder="Search" diff --git a/frontend/src/components/ui-new/views/ChangesPanel.tsx b/frontend/src/components/ui-new/views/ChangesPanel.tsx index 91dca199..6bd3cc99 100644 --- a/frontend/src/components/ui-new/views/ChangesPanel.tsx +++ b/frontend/src/components/ui-new/views/ChangesPanel.tsx @@ -16,9 +16,9 @@ interface ChangesPanelProps { diffItems: DiffItemData[]; onDiffRef?: (path: string, el: HTMLDivElement | null) => void; /** Project ID for @ mentions in comments */ - projectId?: string; + projectId: string; /** Attempt ID for opening files in IDE */ - attemptId?: string; + attemptId: string; } // Memoized DiffItem - only re-renders when its specific diff reference changes @@ -32,8 +32,8 @@ const DiffItem = memo(function DiffItem({ diff: Diff; initialExpanded?: boolean; onRef?: (path: string, el: HTMLDivElement | null) => void; - projectId?: string; - attemptId?: string; + projectId: string; + attemptId: string; }) { const path = diff.newPath || diff.oldPath || ''; const [expanded, toggle] = usePersistedExpanded( @@ -55,9 +55,11 @@ const DiffItem = memo(function DiffItem({ return (
onRef?.(path, el)}> diff --git a/frontend/src/components/ui-new/views/PreviewControls.tsx b/frontend/src/components/ui-new/views/PreviewControls.tsx index c302d028..247ae4b6 100644 --- a/frontend/src/components/ui-new/views/PreviewControls.tsx +++ b/frontend/src/components/ui-new/views/PreviewControls.tsx @@ -81,7 +81,13 @@ export function PreviewControls({
) : devServerProcesses.length > 0 ? ( - + ) : null}
diff --git a/frontend/src/components/ui-new/views/WorkspacesMain.tsx b/frontend/src/components/ui-new/views/WorkspacesMain.tsx index 753a66ee..473d5460 100644 --- a/frontend/src/components/ui-new/views/WorkspacesMain.tsx +++ b/frontend/src/components/ui-new/views/WorkspacesMain.tsx @@ -83,16 +83,27 @@ export function WorkspacesMain({ {/* Chat box - always rendered to prevent flash during workspace switch */}