coder(pending): new-file-from-RightRail create endpoint + modal
POST /api/sessions/:sessionId/pending/create queues a pending_changes create via queueCreate (WriteGuardError -> 422 with the guard message). RightRail gains a 'New file from pasted text' modal (path + content) wired through api.coder.createPendingFile; sessionId is threaded down from App.tsx. The staged change shows in the CoderPane DiffPanel for explicit apply. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { ChevronRight, ChevronDown, FileText, Folder, PanelRightClose, PanelRightOpen } from 'lucide-react';
|
||||
import { api } from '@/api/client';
|
||||
import { ChevronRight, ChevronDown, FilePlus, FileText, Folder, PanelRightClose, PanelRightOpen } from 'lucide-react';
|
||||
import { api, ApiError } from '@/api/client';
|
||||
import type { FileEntry } from '@/api/types';
|
||||
import { inferLanguage } from '@/lib/attachments';
|
||||
import { sessionEvents } from '@/hooks/sessionEvents';
|
||||
@@ -8,10 +8,22 @@ import { useRightRailDrawer } from '@/hooks/useRightRailDrawer';
|
||||
import { useViewport } from '@/hooks/useViewport';
|
||||
import { FileViewerOverlay } from '@/components/FileViewerOverlay';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Textarea } from '@/components/ui/textarea';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from '@/components/ui/dialog';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
interface Props {
|
||||
projectId: string;
|
||||
sessionId: string;
|
||||
}
|
||||
|
||||
const STORAGE_KEY = 'boocode.rightrail';
|
||||
@@ -27,7 +39,7 @@ function joinPath(parent: string, name: string): string {
|
||||
return `${parent}/${name}`;
|
||||
}
|
||||
|
||||
export function RightRail({ projectId }: Props) {
|
||||
export function RightRail({ projectId, sessionId }: Props) {
|
||||
const { isMobile } = useViewport();
|
||||
const { open: drawerOpen, setOpen: setDrawerOpen } = useRightRailDrawer();
|
||||
const [open, setOpen] = useState(() => {
|
||||
@@ -39,6 +51,39 @@ export function RightRail({ projectId }: Props) {
|
||||
const [fullFileList, setFullFileList] = useState<string[] | null>(null);
|
||||
const [viewerFile, setViewerFile] = useState<{ path: string; content: string } | null>(null);
|
||||
|
||||
// New-file-from-pasted-text modal. Queues a pending_changes create via
|
||||
// BooCoder; it then shows in the CoderPane DiffPanel for explicit apply.
|
||||
const [newFileOpen, setNewFileOpen] = useState(false);
|
||||
const [newFilePath, setNewFilePath] = useState('');
|
||||
const [newFileContent, setNewFileContent] = useState('');
|
||||
const [creating, setCreating] = useState(false);
|
||||
const [createError, setCreateError] = useState<string | null>(null);
|
||||
|
||||
const openNewFile = useCallback(() => {
|
||||
setNewFilePath('');
|
||||
setNewFileContent('');
|
||||
setCreateError(null);
|
||||
setNewFileOpen(true);
|
||||
}, []);
|
||||
|
||||
const submitNewFile = useCallback(async () => {
|
||||
const path = newFilePath.trim();
|
||||
if (!path || creating) return;
|
||||
setCreating(true);
|
||||
setCreateError(null);
|
||||
try {
|
||||
await api.coder.createPendingFile(sessionId, path, newFileContent);
|
||||
setNewFileOpen(false);
|
||||
setNewFilePath('');
|
||||
setNewFileContent('');
|
||||
} catch (err) {
|
||||
// 422 WriteGuardError surfaces via ApiError.message (the route's { error }).
|
||||
setCreateError(err instanceof ApiError ? err.message : err instanceof Error ? err.message : 'Failed to create file');
|
||||
} finally {
|
||||
setCreating(false);
|
||||
}
|
||||
}, [sessionId, newFilePath, newFileContent, creating]);
|
||||
|
||||
// Combined open state: on mobile use the global drawer state (toggled by
|
||||
// the Session header's FolderTree button); on desktop use the persistent
|
||||
// internal state.
|
||||
@@ -163,6 +208,15 @@ export function RightRail({ projectId }: Props) {
|
||||
<aside className={asideCls}>
|
||||
<div className="flex items-center gap-2 px-3 py-2 border-b shrink-0">
|
||||
<span className="text-xs font-medium flex-1">Files</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={openNewFile}
|
||||
className="p-1 rounded hover:bg-muted text-muted-foreground max-md:min-h-[44px] max-md:min-w-[44px]"
|
||||
aria-label="New file from pasted text"
|
||||
title="New file"
|
||||
>
|
||||
<FilePlus size={14} />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={closeRail}
|
||||
@@ -225,6 +279,48 @@ export function RightRail({ projectId }: Props) {
|
||||
onNavigate={(path) => void openFile(path)}
|
||||
/>
|
||||
)}
|
||||
|
||||
<Dialog open={newFileOpen} onOpenChange={setNewFileOpen}>
|
||||
<DialogContent className="sm:max-w-lg">
|
||||
<DialogHeader>
|
||||
<DialogTitle>New file from pasted text</DialogTitle>
|
||||
<DialogDescription>
|
||||
Queues a new file as a pending change. Review and apply it from the Coder pane.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="space-y-3">
|
||||
<div className="space-y-1.5">
|
||||
<Label htmlFor="new-file-path" className="text-xs">Path (relative to project root)</Label>
|
||||
<Input
|
||||
id="new-file-path"
|
||||
value={newFilePath}
|
||||
onChange={(e) => setNewFilePath(e.target.value)}
|
||||
placeholder="src/example.ts"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-1.5">
|
||||
<Label htmlFor="new-file-content" className="text-xs">Content</Label>
|
||||
<Textarea
|
||||
id="new-file-content"
|
||||
value={newFileContent}
|
||||
onChange={(e) => setNewFileContent(e.target.value)}
|
||||
placeholder="Paste file contents here…"
|
||||
autoFocus
|
||||
className="min-h-[180px] font-mono text-xs"
|
||||
/>
|
||||
</div>
|
||||
{createError && <p className="text-xs text-destructive">{createError}</p>}
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button variant="ghost" onClick={() => setNewFileOpen(false)} disabled={creating}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button onClick={() => void submitNewFile()} disabled={creating || !newFilePath.trim()}>
|
||||
{creating ? 'Creating…' : 'Create'}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user