Skip to content

Commit e80494c

Browse files
davindicodeclaude
andcommitted
Add file upload button and coding CLI slash commands
- File explorer: upload button in toolbar, supports multiple files, uploads to current directory with progress spinner - Code tab: slash commands grouped by CLI (claude, codex, opencode) for quick access to /clear, /compact, /model, /exit, etc. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 259a944 commit e80494c

2 files changed

Lines changed: 104 additions & 1 deletion

File tree

app/components/files/FilesPage.tsx

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,8 @@ function FileSessionView({ session }: { session: FileSession }) {
108108
const { cwd, entries, showHidden, selectedFile, fileContent, loading } = session;
109109
const [fileLoading, setFileLoading] = useState(false);
110110
const [fileError, setFileError] = useState<string | null>(null);
111+
const [uploading, setUploading] = useState(false);
112+
const uploadInputRef = useRef<HTMLInputElement>(null);
111113

112114
const fullPath = (name: string) => (cwd === "/" ? `/${name}` : `${cwd}/${name}`);
113115

@@ -221,6 +223,26 @@ function FileSessionView({ session }: { session: FileSession }) {
221223

222224
const handleInfo = (entry: FileEntry) => setDialog({ type: "info", entry });
223225

226+
const handleUpload = async (files: FileList | null) => {
227+
if (!files || files.length === 0) return;
228+
setUploading(true);
229+
try {
230+
for (const file of Array.from(files)) {
231+
const formData = new FormData();
232+
formData.append("file", file);
233+
formData.append("dir", cwd);
234+
const res = await fetch("/api/files/upload", { method: "POST", body: formData });
235+
const data = await res.json();
236+
if (data.error) patch({ error: `Upload failed: ${data.error}` });
237+
}
238+
await loadDirectory(cwd);
239+
} catch (err: any) {
240+
patch({ error: `Upload failed: ${err.message}` });
241+
}
242+
setUploading(false);
243+
if (uploadInputRef.current) uploadInputRef.current.value = "";
244+
};
245+
224246
if (selectedFile) {
225247
if (fileLoading) {
226248
return (
@@ -285,6 +307,25 @@ function FileSessionView({ session }: { session: FileSession }) {
285307
<path strokeLinecap="round" strokeLinejoin="round" d="M12 11v6M9 14h6" />
286308
</svg>
287309
</button>
310+
<button onClick={() => uploadInputRef.current?.click()} disabled={uploading} className="p-1.5 text-gray-400 hover:text-white disabled:text-gray-600 transition-colors" title="Upload files">
311+
{uploading ? (
312+
<svg className="w-4 h-4 animate-spin" fill="none" viewBox="0 0 24 24">
313+
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
314+
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z" />
315+
</svg>
316+
) : (
317+
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" strokeWidth={2}>
318+
<path strokeLinecap="round" strokeLinejoin="round" d="M4 16v2a2 2 0 002 2h12a2 2 0 002-2v-2M7 10l5-5 5 5M12 15V3" />
319+
</svg>
320+
)}
321+
</button>
322+
<input
323+
ref={uploadInputRef}
324+
type="file"
325+
multiple
326+
className="hidden"
327+
onChange={(e) => handleUpload(e.target.files)}
328+
/>
288329
</div>
289330
<label className="flex items-center gap-2 text-sm text-gray-400 cursor-pointer">
290331
<input type="checkbox" checked={showHidden} onChange={(e) => toggleHidden(e.target.checked)} className="accent-blue-500" />

app/components/terminal/InputBox.tsx

Lines changed: 63 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,49 @@ const GIT_QUICK_CMDS: { label: string; title: string; command: string }[] = [
143143
{ label: "branch", title: "git branch (list branches)", command: "git branch\n" },
144144
];
145145

146+
// Slash commands grouped by CLI
147+
interface SlashCmdGroup {
148+
cli: string;
149+
cmds: { label: string; title: string; command: string }[];
150+
}
151+
152+
const SLASH_CMD_GROUPS: SlashCmdGroup[] = [
153+
{
154+
cli: "claude",
155+
cmds: [
156+
{ label: "/clear", title: "Clear conversation context", command: "/clear\n" },
157+
{ label: "/compact", title: "Compact conversation to save context", command: "/compact\n" },
158+
{ label: "/cost", title: "Show token usage and cost", command: "/cost\n" },
159+
{ label: "/help", title: "Show available commands", command: "/help\n" },
160+
{ label: "/model", title: "Show or switch model", command: "/model\n" },
161+
{ label: "/exit", title: "Exit Claude Code", command: "/exit\n" },
162+
{ label: "/config", title: "Open config", command: "/config\n" },
163+
{ label: "/memory", title: "Edit CLAUDE.md memory", command: "/memory\n" },
164+
{ label: "/review", title: "Review a PR", command: "/review\n" },
165+
{ label: "/vim", title: "Toggle vim mode", command: "/vim\n" },
166+
],
167+
},
168+
{
169+
cli: "codex",
170+
cmds: [
171+
{ label: "/help", title: "Show available commands", command: "/help\n" },
172+
{ label: "/model", title: "Show or switch model", command: "/model\n" },
173+
{ label: "/exit", title: "Exit Codex", command: "/exit\n" },
174+
{ label: "/clear", title: "Clear conversation", command: "/clear\n" },
175+
{ label: "/approval", title: "Change approval mode", command: "/approval\n" },
176+
],
177+
},
178+
{
179+
cli: "opencode",
180+
cmds: [
181+
{ label: "/help", title: "Show available commands", command: "/help\n" },
182+
{ label: "/exit", title: "Exit OpenCode", command: "/exit\n" },
183+
{ label: "/clear", title: "Clear conversation", command: "/clear\n" },
184+
{ label: "/compact", title: "Compact context", command: "/compact\n" },
185+
],
186+
},
187+
];
188+
146189
const CODE_LAUNCH_CMDS: CodeLaunchCmd[] = [
147190
// Claude Code
148191
{ cli: "claude", label: "claude", title: "Claude Code — interactive mode", command: "claude\n" },
@@ -762,7 +805,7 @@ export default function InputBox() {
762805

763806
{/* Code tab: coding CLI launchers + common keys */}
764807
{activeGroup === CODE_TAB && (
765-
<div className="border-b border-gray-700/50 bg-[#12122a] px-2 py-1.5">
808+
<div className="border-b border-gray-700/50 bg-[#12122a] px-2 py-1.5 max-h-64 overflow-y-auto">
766809
{/* Coding CLI key combos */}
767810
<div className="flex flex-wrap gap-1 mb-1.5">
768811
<span className="text-[10px] text-gray-600 w-full">cli keys</span>
@@ -772,6 +815,25 @@ export default function InputBox() {
772815
</button>
773816
))}
774817
</div>
818+
{/* Slash commands grouped by CLI */}
819+
{SLASH_CMD_GROUPS.map((group) => (
820+
<div key={group.cli} className="border-t border-gray-700/50 pt-1.5 mb-1.5">
821+
<span className="text-[10px] text-gray-600">{group.cli} /commands</span>
822+
<div className="flex flex-wrap gap-1 mt-0.5">
823+
{group.cmds.map((cmd) => (
824+
<button
825+
key={`${group.cli}-${cmd.label}`}
826+
onClick={() => { if (activeSessionId) sendInput(activeSessionId, cmd.command); }}
827+
disabled={!activeSessionId}
828+
title={cmd.title}
829+
className="px-2 py-0.5 text-[11px] bg-[#1a1a2e] text-cyan-400 hover:text-cyan-200 hover:bg-[#2a2a4a] disabled:text-gray-600 rounded border border-cyan-800/60 whitespace-nowrap transition-colors select-none"
830+
>
831+
{cmd.label}
832+
</button>
833+
))}
834+
</div>
835+
</div>
836+
))}
775837
{/* CLI launch shortcuts */}
776838
<div className="border-t border-gray-700/50 pt-1.5">
777839
<span className="text-[10px] text-gray-600">launch</span>

0 commit comments

Comments
 (0)