Files
hermes-mobile/apps/mobile/src/screens/FilesScreen/FilesScreen.tsx
T
2026-07-09 04:43:00 +00:00

119 lines
3.8 KiB
TypeScript

import { useEffect, useState } from 'react';
import type { FileEntry, FileReadResponse } from '@hermes-mobile/shared';
import { useCompanion } from '../../api/CompanionContext.js';
function parentPath(path: string): string {
if (path === '.') return '.';
const parts = path.split('/').filter(Boolean);
parts.pop();
return parts.length ? parts.join('/') : '.';
}
export function FilesScreen() {
const { client } = useCompanion();
const [path, setPath] = useState('.');
const [entries, setEntries] = useState<FileEntry[]>([]);
const [activeFile, setActiveFile] = useState<FileReadResponse | null>(null);
const [draft, setDraft] = useState('');
const [status, setStatus] = useState('');
const [busy, setBusy] = useState(false);
async function loadDirectory(nextPath = path): Promise<void> {
setBusy(true);
setStatus('');
try {
const response = await client.listFiles(nextPath);
setPath(response.path);
setEntries(response.entries);
setActiveFile(null);
setDraft('');
} catch (caughtError) {
setStatus(caughtError instanceof Error ? caughtError.message : 'Unable to list files');
} finally {
setBusy(false);
}
}
async function openEntry(entry: FileEntry): Promise<void> {
if (entry.type === 'directory') {
await loadDirectory(entry.path);
return;
}
setBusy(true);
setStatus('');
try {
const response = await client.readFile(entry.path);
setActiveFile(response);
setDraft(response.content);
} catch (caughtError) {
setStatus(caughtError instanceof Error ? caughtError.message : 'Unable to read file');
} finally {
setBusy(false);
}
}
async function saveFile(): Promise<void> {
if (!activeFile) return;
setBusy(true);
try {
const response = await client.writeFile(activeFile.path, draft);
setStatus(`Saved ${response.bytesWritten} bytes to ${response.path}`);
await openEntry({ name: activeFile.path, path: activeFile.path, type: 'file', size: 0, modifiedAt: new Date().toISOString() });
} catch (caughtError) {
setStatus(caughtError instanceof Error ? caughtError.message : 'Unable to save file');
} finally {
setBusy(false);
}
}
useEffect(() => {
void loadDirectory('.');
}, [client]);
return (
<section className="screen stack-screen">
<div className="screen-heading">
<div>
<p className="eyebrow">Workspace</p>
<h2>Files</h2>
</div>
<button className="small-button" onClick={() => void loadDirectory()} type="button">Refresh</button>
</div>
<article className="panel-card">
<div className="path-bar">
<button disabled={path === '.' || busy} onClick={() => void loadDirectory(parentPath(path))} type="button">Up</button>
<span>{path}</span>
</div>
<div className="file-list">
{entries.map((entry) => (
<button className="file-row" key={entry.path} onClick={() => void openEntry(entry)} type="button">
<span>{entry.type === 'directory' ? '📁' : '📄'} {entry.name}</span>
<small>{entry.type === 'file' ? `${entry.size} B` : 'dir'}</small>
</button>
))}
</div>
</article>
{activeFile ? (
<article className="panel-card editor-card">
<div className="screen-heading">
<div>
<p className="eyebrow">Editing</p>
<h3>{activeFile.path}</h3>
</div>
<button className="small-button" disabled={busy} onClick={() => void saveFile()} type="button">Save</button>
</div>
<textarea aria-label="File content" onChange={(event) => setDraft(event.target.value)} value={draft} />
</article>
) : null}
{status ? <p className="error-card">{status}</p> : null}
</section>
);
}