Skip to content
Merged
4 changes: 4 additions & 0 deletions apps/ui/playwright.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,10 @@ export default defineConfig({
ALLOWED_ROOT_DIRECTORY: '',
// Simulate containerized environment to skip sandbox confirmation dialogs
IS_CONTAINERIZED: 'true',
// Increase Node.js memory limit to prevent OOM during tests
NODE_OPTIONS: [process.env.NODE_OPTIONS, '--max-old-space-size=4096']
.filter(Boolean)
.join(' '),
},
},
]),
Expand Down
1 change: 1 addition & 0 deletions apps/ui/src/components/ui/header-actions-panel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@ export function HeaderActionsPanelTrigger({
onClick={onToggle}
className={cn('h-8 w-8 p-0 text-muted-foreground hover:text-foreground lg:hidden', className)}
aria-label={isOpen ? 'Close actions menu' : 'Open actions menu'}
data-testid="header-actions-panel-trigger"
>
{isOpen ? <X className="w-5 h-5" /> : <Menu className="w-5 h-5" />}
</Button>
Expand Down
132 changes: 91 additions & 41 deletions apps/ui/src/components/views/context-view.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,10 @@ import {
FilePlus,
FileUp,
MoreVertical,
ArrowLeft,
} from 'lucide-react';
import { Spinner } from '@/components/ui/spinner';
import { useIsMobile } from '@/hooks/use-media-query';
import {
useKeyboardShortcuts,
useKeyboardShortcutsConfig,
Expand All @@ -42,7 +44,7 @@ import {
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { cn } from '@/lib/utils';
import { sanitizeFilename } from '@/lib/image-utils';
import { sanitizeFilename, isMarkdownFilename, isImageFilename } from '@/lib/image-utils';
import { Markdown } from '../ui/markdown';
import {
DropdownMenu,
Expand All @@ -54,6 +56,16 @@ import { Textarea } from '@/components/ui/textarea';

const logger = createLogger('ContextView');

// Responsive layout classes
const FILE_LIST_BASE_CLASSES = 'border-r border-border flex flex-col overflow-hidden';
const FILE_LIST_DESKTOP_CLASSES = 'w-64';
const FILE_LIST_EXPANDED_CLASSES = 'flex-1';
const FILE_LIST_MOBILE_NO_SELECTION_CLASSES = 'w-full border-r-0';
const FILE_LIST_MOBILE_SELECTION_CLASSES = 'hidden';

const EDITOR_PANEL_BASE_CLASSES = 'flex-1 flex flex-col overflow-hidden';
const EDITOR_PANEL_MOBILE_HIDDEN_CLASSES = 'hidden';

interface ContextFile {
name: string;
type: 'text' | 'image';
Expand Down Expand Up @@ -103,6 +115,9 @@ export function ContextView() {
// File input ref for import
const fileInputRef = useRef<HTMLInputElement>(null);

// Mobile detection
const isMobile = useIsMobile();

// Keyboard shortcuts for this view
const contextShortcuts: KeyboardShortcut[] = useMemo(
() => [
Expand All @@ -122,18 +137,6 @@ export function ContextView() {
return `${currentProject.path}/.automaker/context`;
}, [currentProject]);

const isMarkdownFile = (filename: string): boolean => {
const ext = filename.toLowerCase().substring(filename.lastIndexOf('.'));
return ext === '.md' || ext === '.markdown';
};

// Determine if a file is an image based on extension
const isImageFile = (filename: string): boolean => {
const imageExtensions = ['.png', '.jpg', '.jpeg', '.gif', '.webp', '.svg', '.bmp'];
const ext = filename.toLowerCase().substring(filename.lastIndexOf('.'));
return imageExtensions.includes(ext);
};

// Load context metadata
const loadMetadata = useCallback(async (): Promise<ContextMetadata> => {
const contextPath = getContextPath();
Expand Down Expand Up @@ -195,10 +198,15 @@ export function ContextView() {
const result = await api.readdir(contextPath);
if (result.success && result.entries) {
const files: ContextFile[] = result.entries
.filter((entry) => entry.isFile && entry.name !== 'context-metadata.json')
.filter(
(entry) =>
entry.isFile &&
entry.name !== 'context-metadata.json' &&
(isMarkdownFilename(entry.name) || isImageFilename(entry.name))
)
.map((entry) => ({
name: entry.name,
type: isImageFile(entry.name) ? 'image' : 'text',
type: isImageFilename(entry.name) ? 'image' : 'text',
path: `${contextPath}/${entry.name}`,
description: metadata.files[entry.name]?.description,
}));
Expand Down Expand Up @@ -232,11 +240,10 @@ export function ContextView() {

// Select a file
const handleSelectFile = (file: ContextFile) => {
if (hasChanges) {
// Could add a confirmation dialog here
}
// Note: Unsaved changes warning could be added here in the future
// For now, silently proceed to avoid disrupting mobile UX flow
loadFileContent(file);
setIsPreviewMode(isMarkdownFile(file.name));
setIsPreviewMode(isMarkdownFilename(file.name));
};
Comment on lines 242 to 247
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Potential data loss when switching files with unsaved changes.

The unsaved changes warning was removed to improve mobile UX flow, but this means users could lose edits when selecting a different file. Consider implementing a mobile-friendly confirmation (e.g., a toast with "Discard changes?" or auto-save) rather than silently discarding changes.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/ui/src/components/views/context-view.tsx` around lines 251 - 256, The
current handleSelectFile silently drops unsaved edits by immediately calling
loadFileContent(file) and setIsPreviewMode(...); update handleSelectFile to
detect unsaved edits (use the existing unsaved state or introduce
hasUnsavedChanges) and either (a) trigger a mobile-friendly confirmation flow
(e.g., show a toast/confirm component like showDiscardConfirm that, on user
confirm, calls loadFileContent and setIsPreviewMode) or (b) auto-save the draft
first (call saveDraft/currentSaveFunction) and then proceed to loadFileContent
and setIsPreviewMode; ensure you reference handleSelectFile, loadFileContent,
setIsPreviewMode, isMarkdownFile and the unsaved state or save function when
implementing the change.


// Save current file
Expand Down Expand Up @@ -341,7 +348,7 @@ export function ContextView() {

try {
const api = getElectronAPI();
const isImage = isImageFile(file.name);
const isImage = isImageFilename(file.name);

let filePath: string;
let fileName: string;
Expand Down Expand Up @@ -582,7 +589,7 @@ export function ContextView() {
// Update selected file with new name and path
const renamedFile: ContextFile = {
name: newName,
type: isImageFile(newName) ? 'image' : 'text',
type: isImageFilename(newName) ? 'image' : 'text',
path: newPath,
content: result.content,
description: metadata.files[newName]?.description,
Expand Down Expand Up @@ -790,7 +797,17 @@ export function ContextView() {
)}

{/* Left Panel - File List */}
<div className="w-64 border-r border-border flex flex-col overflow-hidden">
{/* Mobile: Full width, hidden when file is selected (full-screen editor) */}
{/* Desktop: Fixed width w-64, expands to fill space when no file selected */}
<div
className={cn(
FILE_LIST_BASE_CLASSES,
FILE_LIST_DESKTOP_CLASSES,
!selectedFile && FILE_LIST_EXPANDED_CLASSES,
isMobile && !selectedFile && FILE_LIST_MOBILE_NO_SELECTION_CLASSES,
isMobile && selectedFile && FILE_LIST_MOBILE_SELECTION_CLASSES
)}
>
Comment on lines +800 to +810
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Potential CSS conflict between w-64 and flex-1 on desktop when no file is selected.

When no file is selected on desktop, both w-64 (width: 16rem) and flex-1 (flex: 1 1 0%) are applied. These can produce inconsistent behavior: flex-basis: 0% may override or conflict with the explicit width, depending on browser implementation.

Per the comment on line 801, the intent is for the file list to expand when no file is selected. Consider applying w-64 only when a file is selected:

🔧 Proposed fix
         <div
           className={cn(
             FILE_LIST_BASE_CLASSES,
-            FILE_LIST_DESKTOP_CLASSES,
-            !selectedFile && FILE_LIST_EXPANDED_CLASSES,
+            !isMobile && selectedFile && FILE_LIST_DESKTOP_CLASSES,
+            !isMobile && !selectedFile && FILE_LIST_EXPANDED_CLASSES,
             isMobile && !selectedFile && FILE_LIST_MOBILE_NO_SELECTION_CLASSES,
             isMobile && selectedFile && FILE_LIST_MOBILE_SELECTION_CLASSES
           )}
         >
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
{/* Mobile: Full width, hidden when file is selected (full-screen editor) */}
{/* Desktop: Fixed width w-64, expands to fill space when no file selected */}
<div
className={cn(
FILE_LIST_BASE_CLASSES,
FILE_LIST_DESKTOP_CLASSES,
!selectedFile && FILE_LIST_EXPANDED_CLASSES,
isMobile && !selectedFile && FILE_LIST_MOBILE_NO_SELECTION_CLASSES,
isMobile && selectedFile && FILE_LIST_MOBILE_SELECTION_CLASSES
)}
>
{/* Mobile: Full width, hidden when file is selected (full-screen editor) */}
{/* Desktop: Fixed width w-64, expands to fill space when no file selected */}
<div
className={cn(
FILE_LIST_BASE_CLASSES,
!isMobile && selectedFile && FILE_LIST_DESKTOP_CLASSES,
!isMobile && !selectedFile && FILE_LIST_EXPANDED_CLASSES,
isMobile && !selectedFile && FILE_LIST_MOBILE_NO_SELECTION_CLASSES,
isMobile && selectedFile && FILE_LIST_MOBILE_SELECTION_CLASSES
)}
>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/ui/src/components/views/context-view.tsx` around lines 800 - 810, The
desktop width class (w-64) in FILE_LIST_DESKTOP_CLASSES conflicts with
FILE_LIST_EXPANDED_CLASSES (flex-1) when no file is selected; update the
conditional class application so that the fixed width is only applied when a
file is selected—e.g., remove or split w-64 out of FILE_LIST_DESKTOP_CLASSES and
include that narrow-desktop class only when selectedFile is truthy, while
keeping FILE_LIST_EXPANDED_CLASSES (flex-1) when !selectedFile; adjust the class
expression around FILE_LIST_DESKTOP_CLASSES/FILE_LIST_EXPANDED_CLASSES in the
component rendering the div (the code referencing selectedFile, isMobile,
FILE_LIST_BASE_CLASSES, FILE_LIST_DESKTOP_CLASSES, FILE_LIST_EXPANDED_CLASSES,
FILE_LIST_MOBILE_NO_SELECTION_CLASSES, and FILE_LIST_MOBILE_SELECTION_CLASSES).

<div className="p-3 border-b border-border">
<h2 className="text-sm font-semibold text-muted-foreground">
Context Files ({contextFiles.length})
Expand Down Expand Up @@ -881,36 +898,58 @@ export function ContextView() {
</div>

{/* Right Panel - Editor/Preview */}
<div className="flex-1 flex flex-col overflow-hidden">
{/* Mobile: Hidden when no file selected (file list shows full screen) */}
<div
className={cn(
EDITOR_PANEL_BASE_CLASSES,
isMobile && !selectedFile && EDITOR_PANEL_MOBILE_HIDDEN_CLASSES
)}
>
{selectedFile ? (
<>
{/* File toolbar */}
<div className="flex items-center justify-between p-3 border-b border-border bg-card">
<div className="flex items-center gap-2 min-w-0">
{/* Mobile-only: Back button to return to file list */}
{isMobile && (
<Button
variant="ghost"
size="sm"
onClick={() => setSelectedFile(null)}
className="shrink-0 -ml-1"
aria-label="Back"
title="Back"
>
<ArrowLeft className="h-4 w-4" />
</Button>
)}
{selectedFile.type === 'image' ? (
<ImageIcon className="w-4 h-4 text-muted-foreground flex-shrink-0" />
) : (
<FileText className="w-4 h-4 text-muted-foreground flex-shrink-0" />
)}
<span className="text-sm font-medium truncate">{selectedFile.name}</span>
</div>
<div className="flex gap-2">
{selectedFile.type === 'text' && isMarkdownFile(selectedFile.name) && (
<div className={cn('flex gap-2', isMobile && 'gap-1')}>
{/* Mobile: Icon-only buttons with aria-labels for accessibility */}
{selectedFile.type === 'text' && isMarkdownFilename(selectedFile.name) && (
<Button
variant={'outline'}
size="sm"
onClick={() => setIsPreviewMode(!isPreviewMode)}
data-testid="toggle-preview-mode"
aria-label={isPreviewMode ? 'Edit' : 'Preview'}
title={isPreviewMode ? 'Edit' : 'Preview'}
>
{isPreviewMode ? (
<>
<Pencil className="w-4 h-4 mr-2" />
Edit
<Pencil className="w-4 h-4" />
{!isMobile && <span className="ml-2">Edit</span>}
</>
) : (
<>
<Eye className="w-4 h-4 mr-2" />
Preview
<Eye className="w-4 h-4" />
{!isMobile && <span className="ml-2">Preview</span>}
</>
)}
</Button>
Expand All @@ -921,20 +960,31 @@ export function ContextView() {
onClick={saveFile}
disabled={!hasChanges || isSaving}
data-testid="save-context-file"
aria-label="Save"
title="Save"
>
<Save className="w-4 h-4" />
{!isMobile && (
<span className="ml-2">
{isSaving ? 'Saving...' : hasChanges ? 'Save' : 'Saved'}
</span>
)}
</Button>
)}
{/* Desktop-only: Delete button (use dropdown on mobile to save space) */}
{!isMobile && (
<Button
variant="outline"
size="sm"
onClick={() => setIsDeleteDialogOpen(true)}
className="text-red-500 hover:text-red-400 hover:border-red-500/50"
data-testid="delete-context-file"
aria-label="Delete"
title="Delete"
>
<Save className="w-4 h-4 mr-2" />
{isSaving ? 'Saving...' : hasChanges ? 'Save' : 'Saved'}
<Trash2 className="w-4 h-4" />
</Button>
)}
<Button
variant="outline"
size="sm"
onClick={() => setIsDeleteDialogOpen(true)}
className="text-red-500 hover:text-red-400 hover:border-red-500/50"
data-testid="delete-context-file"
>
<Trash2 className="w-4 h-4" />
</Button>
</div>
</div>

Expand Down Expand Up @@ -1072,7 +1122,7 @@ export function ContextView() {
.filter((f): f is globalThis.File => f !== null);
}

const mdFile = files.find((f) => isMarkdownFile(f.name));
const mdFile = files.find((f) => isMarkdownFilename(f.name));
if (mdFile) {
const content = await mdFile.text();
setNewMarkdownContent(content);
Expand Down
Loading