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 */}