feat: v2.7.7 — File Explorer on workspaces and the user's home, clone with per-user git identity
- File Explorer (Files in the sidebar, /files; also from a workspace's Files tab, portalled so no panel clips it), laid out like GNOME Files: places (Home, workspaces, Trash), Back/Forward/Up, path bar, search, icon grid with image thumbnails or list, labelled New/Upload, selection action bar, hidden files behind Ctrl+H, right-click menus and the usual keys. Make, rename, move, copy (within a workspace, between workspaces, home ↔ workspace), delete into a per-account trash restorable for 7 days, streamed upload of files and whole folders (≤ 500 MB a batch, Replace / Keep both / Skip on a taken name), download a file or a zip of anything. server/lib/file-explorer.js, /api/lanes/:id/fs/*, /api/homes/:who/fs/*. - The user's home: every dot entry at its top stays hidden and refused (also via a symlink, download, zip or as a copy source); an admin may view other users' homes read-only, audited. - write-as learns move/copy (re-run `sudo agenthub server install-system`). - Hardening: a write to a workspace re-checks its owner by the live role; S3 keeps its server.conf if the file stops reading and a tmux call never falls back to the host's server; the AI Proxy gateway marks what it relays and the dashboard refuses it; a NUL in a pane's argv/env is refused. - OpenAPI for the new routes (server/openapi-extra/files.js); docs (API, SERVER-MODE, CLAUDE.md) updated. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -102,7 +102,7 @@ import {
|
||||
Sparkles,
|
||||
Trash2,
|
||||
UnfoldVertical,
|
||||
Upload,
|
||||
FolderOpen,
|
||||
} from "lucide-react";
|
||||
import { api, gitTarget } from "../../lib/api";
|
||||
import { pasteToLane } from "../../lib/panePaste";
|
||||
@@ -123,6 +123,7 @@ import {
|
||||
type LaneDiffChange,
|
||||
} from "./LaneDiffView";
|
||||
import { LaneConflictView } from "./LaneConflictView";
|
||||
import { FileExplorer } from "./FileExplorer";
|
||||
import { ContextMenu, MenuItem, MenuSeparator, useContextMenu } from "../ContextMenu";
|
||||
import { MarkdownPreview, isMarkdown } from "../MarkdownPreview";
|
||||
import { LaneGitHeader, useLaneGitFacts } from "./LaneGitHeader";
|
||||
@@ -1639,6 +1640,14 @@ export function LaneChangesPanel({
|
||||
{t("changes.uploadHere")}
|
||||
</MenuItem>
|
||||
)}
|
||||
{treeCtx.menu.item.dir && (
|
||||
<MenuItem
|
||||
testId="lane-changes-menu-explorer"
|
||||
onSelect={pick(treeCtx.close, () => setExplorer({ path: treeCtx.menu!.item.path }))}
|
||||
>
|
||||
{t("changes.openInExplorer")}
|
||||
</MenuItem>
|
||||
)}
|
||||
<MenuItem onSelect={pick(treeCtx.close, () => copy(treeCtx.menu!.item.path))}>
|
||||
{t("changes.copyPath")}
|
||||
</MenuItem>
|
||||
@@ -1888,7 +1897,11 @@ export function LaneChangesPanel({
|
||||
// upload only creates, into the folder it was asked for ("" = the root).
|
||||
const uploadInput = useRef<HTMLInputElement>(null);
|
||||
const uploadDir = useRef("");
|
||||
const [uploading, setUploading] = useState(false);
|
||||
const [, setUploading] = useState(false);
|
||||
// The File Explorer (the toolbar button where Upload was); the tree is
|
||||
// re-read when it closes after changing anything.
|
||||
const [explorer, setExplorer] = useState<{ path: string } | null>(null);
|
||||
const explorerChanged = useRef(false);
|
||||
const download = (path: string) => {
|
||||
const a = document.createElement("a");
|
||||
a.href = api.lanes.gitDownloadUrl(target, path);
|
||||
@@ -2121,9 +2134,24 @@ export function LaneChangesPanel({
|
||||
aria-label={t("changes.quickOpen")}
|
||||
className="w-full rounded border border-border bg-surface-1 px-2 py-1 font-mono text-[11px] text-fg-primary"
|
||||
/>
|
||||
<IconButton label={t("changes.upload")} disabled={uploading} onClick={() => pickUpload("")}>
|
||||
<Upload className="h-3.5 w-3.5" />
|
||||
<IconButton label={t("changes.openExplorer")} onClick={() => setExplorer({ path: "" })}>
|
||||
<FolderOpen className="h-3.5 w-3.5" />
|
||||
</IconButton>
|
||||
<FileExplorer
|
||||
open={!!explorer}
|
||||
laneId={laneId}
|
||||
session={session}
|
||||
source={source}
|
||||
initialPath={explorer?.path ?? ""}
|
||||
onChanged={() => (explorerChanged.current = true)}
|
||||
onClose={() => {
|
||||
setExplorer(null);
|
||||
if (explorerChanged.current) {
|
||||
explorerChanged.current = false;
|
||||
void rereadAll();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<input
|
||||
ref={uploadInput}
|
||||
type="file"
|
||||
|
||||
Reference in New Issue
Block a user