refactor: Clean up root-level files in ui-new folder (#2079)

## Summary

Successfully cleaned up root level files in `frontend/src/components/ui-new/`:

### Deleted (unused files):
- `MockConversationList.tsx` - not imported anywhere
- `MockDisplayConversationEntry.tsx` - not imported anywhere

### Moved to `containers/`:
- `ConversationList.tsx` → `containers/ConversationListContainer.tsx`
- `NewDisplayConversationEntry.tsx` → `containers/NewDisplayConversationEntry.tsx`
- `VirtualizedProcessLogs.tsx` → `containers/VirtualizedProcessLogs.tsx`

### Updated imports in:
- `views/WorkspacesMain.tsx`
- `views/PreviewControls.tsx`
- `containers/LogsContentContainer.tsx`
- `components/dialogs/scripts/ScriptFixerDialog.tsx`
- `containers/ConversationListContainer.tsx`
- `containers/NewDisplayConversationEntry.tsx`

The root level of `ui-new/` is now clean with no files remaining. All components that use state or side effects were correctly placed in `containers/` to comply with ESLint rules. Both type check (`pnpm run check`) and lint (`pnpm run lint`) pass successfully.
This commit is contained in:
Louis Knight-Webb
2026-01-15 14:53:54 +00:00
committed by GitHub
parent 8ba3a50d0b
commit 75beab35d9
9 changed files with 6 additions and 546 deletions

View File

@@ -20,7 +20,7 @@ import {
SelectValue, SelectValue,
} from '@/components/ui/select'; } from '@/components/ui/select';
import { AutoExpandingTextarea } from '@/components/ui/auto-expanding-textarea'; import { AutoExpandingTextarea } from '@/components/ui/auto-expanding-textarea';
import { VirtualizedProcessLogs } from '@/components/ui-new/VirtualizedProcessLogs'; import { VirtualizedProcessLogs } from '@/components/ui-new/containers/VirtualizedProcessLogs';
import { RunningDots } from '@/components/ui-new/primitives/RunningDots'; import { RunningDots } from '@/components/ui-new/primitives/RunningDots';
import { defineModal } from '@/lib/modals'; import { defineModal } from '@/lib/modals';
import { repoApi, attemptsApi } from '@/lib/api'; import { repoApi, attemptsApi } from '@/lib/api';

View File

@@ -1,79 +0,0 @@
import {
VirtuosoMessageList,
VirtuosoMessageListLicense,
VirtuosoMessageListProps,
} from '@virtuoso.dev/message-list';
import { useMemo } from 'react';
import NewDisplayConversationEntry from './NewDisplayConversationEntry';
import { ApprovalFormProvider } from '@/contexts/ApprovalFormContext';
import { ExecutionProcessesProvider } from '@/contexts/ExecutionProcessesContext';
import { RetryUiProvider } from '@/contexts/RetryUiContext';
import type { NormalizedEntry } from 'shared/types';
// Type for mock data entries
export type MockPatchEntry = {
type: 'NORMALIZED_ENTRY';
content: NormalizedEntry;
patchKey: string;
executionProcessId: string;
};
interface MockConversationListProps {
entries: MockPatchEntry[];
attemptId?: string;
}
const INITIAL_TOP_ITEM = { index: 'LAST' as const, align: 'end' as const };
const ItemContent: VirtuosoMessageListProps<
MockPatchEntry,
undefined
>['ItemContent'] = ({ data }) => {
if (data.type === 'NORMALIZED_ENTRY') {
return (
<NewDisplayConversationEntry
expansionKey={data.patchKey}
entry={data.content}
executionProcessId={data.executionProcessId}
/>
);
}
return null;
};
const computeItemKey: VirtuosoMessageListProps<
MockPatchEntry,
undefined
>['computeItemKey'] = ({ data }) => `mock-${data.patchKey}`;
export function MockConversationList({
entries,
attemptId,
}: MockConversationListProps) {
const channelData = useMemo(() => ({ data: entries }), [entries]);
return (
<ExecutionProcessesProvider attemptId={attemptId}>
<RetryUiProvider attemptId={attemptId}>
<ApprovalFormProvider>
<VirtuosoMessageListLicense
licenseKey={import.meta.env.VITE_PUBLIC_REACT_VIRTUOSO_LICENSE_KEY}
>
<VirtuosoMessageList<MockPatchEntry, undefined>
className="h-full scrollbar-none"
data={channelData}
initialLocation={INITIAL_TOP_ITEM}
computeItemKey={computeItemKey}
ItemContent={ItemContent}
Header={() => <div className="h-2" />}
Footer={() => <div className="h-2" />}
/>
</VirtuosoMessageListLicense>
</ApprovalFormProvider>
</RetryUiProvider>
</ExecutionProcessesProvider>
);
}
export default MockConversationList;

View File

@@ -1,461 +0,0 @@
import { useTranslation } from 'react-i18next';
import WYSIWYGEditor from '@/components/ui/wysiwyg';
import {
ActionType,
NormalizedEntry,
type NormalizedEntryType,
} from 'shared/types.ts';
import FileChangeRenderer from '@/components/NormalizedConversation/FileChangeRenderer';
import { useExpandable } from '@/stores/useExpandableStore';
import {
WarningCircleIcon,
RobotIcon,
CheckCircleIcon,
CheckSquareIcon,
CaretDownIcon,
PencilSimpleIcon,
EyeIcon,
GlobeIcon,
HammerIcon,
PlusIcon,
MagnifyingGlassIcon,
GearIcon,
TerminalIcon,
UserIcon,
} from '@phosphor-icons/react';
import RawLogText from '@/components/common/RawLogText';
import { cn } from '@/lib/utils';
type Props = {
entry: NormalizedEntry;
expansionKey: string;
};
type FileEditAction = Extract<ActionType, { action: 'file_edit' }>;
const getEntryIcon = (entryType: NormalizedEntryType) => {
const iconClassName = 'size-icon-xs';
if (entryType.type === 'user_message' || entryType.type === 'user_feedback') {
return <UserIcon className={iconClassName} />;
}
if (entryType.type === 'assistant_message') {
return <RobotIcon className={iconClassName} />;
}
if (entryType.type === 'system_message') {
return <GearIcon className={iconClassName} />;
}
if (entryType.type === 'error_message') {
return <WarningCircleIcon className={iconClassName} />;
}
if (entryType.type === 'tool_use') {
const { action_type, tool_name } = entryType;
if (
action_type.action === 'todo_management' ||
(tool_name &&
['todowrite', 'todoread', 'todo_write', 'todo_read', 'todo'].includes(
tool_name.toLowerCase()
))
) {
return <CheckSquareIcon className={iconClassName} />;
}
if (action_type.action === 'file_read') {
return <EyeIcon className={iconClassName} />;
} else if (action_type.action === 'file_edit') {
return <PencilSimpleIcon className={iconClassName} />;
} else if (action_type.action === 'command_run') {
return <TerminalIcon className={iconClassName} />;
} else if (action_type.action === 'search') {
return <MagnifyingGlassIcon className={iconClassName} />;
} else if (action_type.action === 'web_fetch') {
return <GlobeIcon className={iconClassName} />;
} else if (action_type.action === 'task_create') {
return <PlusIcon className={iconClassName} />;
} else if (action_type.action === 'plan_presentation') {
return <CheckSquareIcon className={iconClassName} />;
} else if (action_type.action === 'tool') {
return <HammerIcon className={iconClassName} />;
}
return <GearIcon className={iconClassName} />;
}
return <GearIcon className={iconClassName} />;
};
const shouldRenderMarkdown = (entryType: NormalizedEntryType) =>
entryType.type === 'assistant_message' ||
entryType.type === 'system_message' ||
entryType.type === 'tool_use';
const getContentClassName = (entryType: NormalizedEntryType) => {
const base = ' whitespace-pre-wrap break-words';
if (
entryType.type === 'tool_use' &&
entryType.action_type.action === 'command_run'
)
return `${base} font-mono`;
if (entryType.type === 'error_message')
return `${base} font-mono text-destructive`;
return base;
};
type CardVariant = 'system' | 'error';
const MessageCard: React.FC<{
children: React.ReactNode;
variant: CardVariant;
expanded?: boolean;
onToggle?: () => void;
}> = ({ children, variant, expanded, onToggle }) => {
const frameBase =
'border px-3 py-2 w-full cursor-pointer bg-[hsl(var(--card))] border-[hsl(var(--border))]';
const systemTheme = 'border-400/40 text-zinc-500';
const errorTheme =
'border-red-400/40 bg-red-50 dark:bg-[hsl(var(--card))] text-[hsl(var(--foreground))]';
return (
<div
className={`${frameBase} ${
variant === 'system' ? systemTheme : errorTheme
}`}
onClick={onToggle}
>
<div className="flex items-center gap-1.5">
<div className="min-w-0 flex-1">{children}</div>
{onToggle && (
<ExpandChevron
expanded={!!expanded}
onClick={onToggle}
variant={variant}
/>
)}
</div>
</div>
);
};
type CollapsibleVariant = 'system' | 'error';
const ExpandChevron: React.FC<{
expanded: boolean;
onClick: () => void;
variant: CollapsibleVariant;
}> = ({ expanded, onClick, variant }) => {
const color =
variant === 'system'
? 'text-700 dark:text-300'
: 'text-red-700 dark:text-red-300';
return (
<CaretDownIcon
onClick={onClick}
className={`size-icon-base cursor-pointer transition-transform ${color} ${
expanded ? '' : '-rotate-90'
}`}
/>
);
};
const CollapsibleEntry: React.FC<{
content: string;
markdown: boolean;
expansionKey: string;
variant: CollapsibleVariant;
contentClassName: string;
}> = ({ content, markdown, expansionKey, variant, contentClassName }) => {
const multiline = content.includes('\n');
const [expanded, toggle] = useExpandable(`entry:${expansionKey}`, false);
const Inner = (
<div className={contentClassName}>
{markdown ? (
<WYSIWYGEditor
value={content}
disabled
className="whitespace-pre-wrap break-words"
/>
) : (
content
)}
</div>
);
const firstLine = content.split('\n')[0];
const PreviewInner = (
<div className={contentClassName}>
{markdown ? (
<WYSIWYGEditor
value={firstLine}
disabled
className="whitespace-pre-wrap break-words"
/>
) : (
firstLine
)}
</div>
);
if (!multiline) {
return <MessageCard variant={variant}>{Inner}</MessageCard>;
}
return expanded ? (
<MessageCard variant={variant} expanded={expanded} onToggle={toggle}>
{Inner}
</MessageCard>
) : (
<MessageCard variant={variant} expanded={expanded} onToggle={toggle}>
{PreviewInner}
</MessageCard>
);
};
const ToolCallCard: React.FC<{
entry: NormalizedEntry;
expansionKey: string;
}> = ({ entry, expansionKey }) => {
const { t } = useTranslation('common');
const entryType =
entry.entry_type.type === 'tool_use' ? entry.entry_type : undefined;
const linkifyUrls = entryType?.tool_name === 'Tool Install Script';
const defaultExpanded = linkifyUrls;
const [expanded, toggle] = useExpandable(
`tool-entry:${expansionKey}`,
defaultExpanded
);
const actionType = entryType?.action_type;
const isCommand = actionType?.action === 'command_run';
const isTool = actionType?.action === 'tool';
const label = isCommand
? t('conversation.ran')
: entryType?.tool_name || t('conversation.tool');
const inlineText = entry.content.trim();
const isSingleLine = inlineText !== '' && !/\r?\n/.test(inlineText);
const showInlineSummary = isSingleLine;
const commandResult = isCommand ? actionType.result : null;
const output = commandResult?.output ?? null;
let argsText: string | null = null;
if (isCommand) {
const fromArgs =
typeof actionType.command === 'string' ? actionType.command : '';
const fallback = inlineText;
argsText = (fromArgs || fallback).trim();
}
const hasArgs = isTool && !!actionType.arguments;
const hasResult = isTool && !!actionType.result;
const hasExpandableDetails = isCommand
? Boolean(argsText) || Boolean(output)
: hasArgs || hasResult;
const HeaderWrapper: React.ElementType = hasExpandableDetails
? 'button'
: 'div';
const headerProps = hasExpandableDetails
? {
onClick: (e: React.MouseEvent) => {
e.preventDefault();
toggle();
},
title: expanded
? t('conversation.toolDetailsToggle.hide')
: t('conversation.toolDetailsToggle.show'),
}
: {};
const headerClassName = cn(
'w-full flex items-center gap-1.5 text-left text-secondary-foreground'
);
const renderJson = (v: unknown) => (
<pre className="whitespace-pre-wrap">{JSON.stringify(v, null, 2)}</pre>
);
return (
<div className="inline-block w-full flex flex-col gap-4">
<HeaderWrapper {...headerProps} className={headerClassName}>
<span className="min-w-0 flex items-center gap-1.5">
<span>{entryType && getEntryIcon(entryType)}</span>
{showInlineSummary ? (
<span className="font-mono">{inlineText}</span>
) : (
<span className="font-mono">{label}</span>
)}
</span>
</HeaderWrapper>
{expanded && (
<div className="max-h-[200px] overflow-y-auto border">
{isCommand ? (
<>
{argsText && (
<>
<div className="font-normal uppercase bg-background border-b border-dashed px-2 py-1">
{t('conversation.args')}
</div>
<div className="px-2 py-1">{argsText}</div>
</>
)}
{output && (
<>
<div className="font-normal uppercase bg-background border-y border-dashed px-2 py-1">
{t('conversation.output')}
</div>
<div className="px-2 py-1">
<RawLogText content={output} linkifyUrls={linkifyUrls} />
</div>
</>
)}
</>
) : (
<>
{isTool && actionType && (
<>
<div className="font-normal uppercase bg-background border-b border-dashed px-2 py-1">
{t('conversation.args')}
</div>
<div className="px-2 py-1">
{renderJson(actionType.arguments)}
</div>
<div className="font-normal uppercase bg-background border-y border-dashed px-2 py-1">
{t('conversation.result')}
</div>
<div className="px-2 py-1">
{actionType.result?.type.type === 'markdown' &&
actionType.result.value && (
<WYSIWYGEditor
value={actionType.result.value?.toString()}
disabled
/>
)}
{actionType.result?.type.type === 'json' &&
renderJson(actionType.result.value)}
</div>
</>
)}
</>
)}
</div>
)}
</div>
);
};
function MockDisplayConversationEntry({ entry, expansionKey }: Props) {
const { t } = useTranslation('common');
const entryType = entry.entry_type;
const isSystem = entryType.type === 'system_message';
const isError = entryType.type === 'error_message';
const isToolUse = entryType.type === 'tool_use';
const isUserMessage = entryType.type === 'user_message';
const isFileEdit = (a: ActionType): a is FileEditAction =>
a.action === 'file_edit';
// User message - simple rendering
if (isUserMessage) {
return (
<div className="px-4 py-2 text-base">
<div className="flex items-start gap-2">
<UserIcon className="size-icon-base mt-0.5 text-muted-foreground" />
<div className="flex-1">
<WYSIWYGEditor
value={entry.content}
disabled
className="whitespace-pre-wrap break-words"
/>
</div>
</div>
</div>
);
}
// Tool use rendering
if (isToolUse) {
const toolEntry = entryType;
// File edit - use FileChangeRenderer
if (isFileEdit(toolEntry.action_type)) {
const fileEditAction = toolEntry.action_type as FileEditAction;
return (
<div className="px-4 py-2 text-base space-y-3">
{fileEditAction.changes.map((change, idx) => (
<FileChangeRenderer
key={idx}
path={fileEditAction.path}
change={change}
expansionKey={`edit:${expansionKey}:${idx}`}
defaultExpanded={false}
/>
))}
</div>
);
}
// Other tool uses
return (
<div className="px-4 py-2 text-base space-y-3">
<ToolCallCard entry={entry} expansionKey={expansionKey} />
</div>
);
}
// System or error messages - collapsible
if (isSystem || isError) {
return (
<div className="px-4 py-2 text-base">
<CollapsibleEntry
content={entry.content}
markdown={shouldRenderMarkdown(entryType)}
expansionKey={expansionKey}
variant={isSystem ? 'system' : 'error'}
contentClassName={getContentClassName(entryType)}
/>
</div>
);
}
// Next action - simple completion indicator
if (entry.entry_type.type === 'next_action') {
return (
<div className="px-4 py-2 text-base">
<div className="flex items-center gap-2 text-green-600 dark:text-green-400">
<CheckCircleIcon className="size-icon-base" />
<span>{t('conversation.taskCompleted')}</span>
</div>
</div>
);
}
// Default: assistant message or other types
return (
<div className="px-4 py-2 text-base">
<div className="flex items-start gap-2">
<RobotIcon className="size-icon-base mt-0.5 text-muted-foreground" />
<div className={cn('flex-1', getContentClassName(entryType))}>
{shouldRenderMarkdown(entryType) ? (
<WYSIWYGEditor
value={entry.content}
disabled
className="whitespace-pre-wrap break-words flex flex-col gap-1 font-light"
/>
) : (
entry.content
)}
</div>
</div>
</div>
);
}
export default MockDisplayConversationEntry;

View File

@@ -4,7 +4,7 @@ import { cn } from '@/lib/utils';
import { import {
VirtualizedProcessLogs, VirtualizedProcessLogs,
type LogEntry, type LogEntry,
} from '../VirtualizedProcessLogs'; } from './VirtualizedProcessLogs';
import { useLogStream } from '@/hooks/useLogStream'; import { useLogStream } from '@/hooks/useLogStream';
import { useLogsPanel } from '@/contexts/LogsPanelContext'; import { useLogsPanel } from '@/contexts/LogsPanelContext';

View File

@@ -36,8 +36,8 @@ import {
ChatThinkingMessage, ChatThinkingMessage,
ChatErrorMessage, ChatErrorMessage,
ChatScriptEntry, ChatScriptEntry,
} from './primitives/conversation'; } from '../primitives/conversation';
import type { DiffInput } from './primitives/conversation/DiffViewCard'; import type { DiffInput } from '../primitives/conversation/DiffViewCard';
type Props = { type Props = {
entry: NormalizedEntry; entry: NormalizedEntry;

View File

@@ -2,7 +2,7 @@ import { ArrowSquareOutIcon, SpinnerIcon } from '@phosphor-icons/react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
import { CollapsibleSectionHeader } from '../primitives/CollapsibleSectionHeader'; import { CollapsibleSectionHeader } from '../primitives/CollapsibleSectionHeader';
import { VirtualizedProcessLogs } from '../VirtualizedProcessLogs'; import { VirtualizedProcessLogs } from '../containers/VirtualizedProcessLogs';
import { PERSIST_KEYS } from '@/stores/useUiPreferencesStore'; import { PERSIST_KEYS } from '@/stores/useUiPreferencesStore';
import { getDevServerWorkingDir } from '@/lib/devServerUtils'; import { getDevServerWorkingDir } from '@/lib/devServerUtils';
import type { ExecutionProcess, PatchType } from 'shared/types'; import type { ExecutionProcess, PatchType } from 'shared/types';

View File

@@ -4,7 +4,7 @@ import type { Session } from 'shared/types';
import type { WorkspaceWithSession } from '@/types/attempt'; import type { WorkspaceWithSession } from '@/types/attempt';
import { SessionChatBoxContainer } from '@/components/ui-new/containers/SessionChatBoxContainer'; import { SessionChatBoxContainer } from '@/components/ui-new/containers/SessionChatBoxContainer';
import { ContextBarContainer } from '@/components/ui-new/containers/ContextBarContainer'; import { ContextBarContainer } from '@/components/ui-new/containers/ContextBarContainer';
import { ConversationList } from '../ConversationList'; import { ConversationList } from '../containers/ConversationListContainer';
import { EntriesProvider } from '@/contexts/EntriesContext'; import { EntriesProvider } from '@/contexts/EntriesContext';
import { MessageEditProvider } from '@/contexts/MessageEditContext'; import { MessageEditProvider } from '@/contexts/MessageEditContext';
import { RetryUiProvider } from '@/contexts/RetryUiContext'; import { RetryUiProvider } from '@/contexts/RetryUiContext';