22de5009ff
- Artifacts (/artifacts): claude.ai pages sessions publish (captured from the Artifact tool's hook), pinned workspace files, skills and plugins, each kept as a snapshot; shared ones appear in a Hub and a skill or plugin installs into the viewer's own account (server/lib/artifacts.js, /api/artifacts, docs/plans/artifacts/). - Workspace extra folders: clone one or more related repos beside the workspace and add them in one step (POST /api/lanes/:id/sources/clone). - A pane that ended by itself while tmux kept running is no longer offered for resume (tmux.serverStartedAt in reconcileOrphans). - Changes panel: a folder that is not a git repo opens on Files, and its files open from there. - Plus the parallel sessions' work in this tree: File Explorer button in the panel header, user delete, RSA SSH keys, chat/terminal and pet fixes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
86 lines
3.4 KiB
TypeScript
86 lines
3.4 KiB
TypeScript
/**
|
|
* @file FilesPage.tsx
|
|
* @description `/files` — the file manager (`FileBrowser`) as a page of its
|
|
* own, reached from the sidebar's **Files** entry. The location is kept in the
|
|
* URL (`?home=<me|user>&path=` or `?lane=<id>&path=`), so Back and a shared
|
|
* link land in the same folder. With neither it opens the workspace last
|
|
* shown (`agenthub.workspace.selected`) — reached from a workspace, Files is
|
|
* about THAT workspace — else the user's own HOME (their Linux home under S3),
|
|
* else the first workspace.
|
|
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
|
|
*/
|
|
|
|
import { useEffect, useState } from "react";
|
|
import { useSearchParams } from "react-router-dom";
|
|
import { useTranslation } from "react-i18next";
|
|
import { api, isHomePlace, type FsPlace } from "../lib/api";
|
|
import { FileBrowser } from "../components/lanes/FileExplorer";
|
|
|
|
function lastWorkspace(): number | null {
|
|
try {
|
|
const v = Number(localStorage.getItem("agenthub.workspace.selected"));
|
|
return Number.isInteger(v) && v > 0 ? v : null;
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
type Start = { lane: number } | { home: string };
|
|
|
|
export function FilesPage() {
|
|
const { t } = useTranslation(["lanes"]);
|
|
const [params, setParams] = useSearchParams();
|
|
const askedLane = Number(params.get("lane"));
|
|
const askedHome = params.get("home");
|
|
// The URL wins on every render: a place picked in the browser (another
|
|
// user's home) lands in the URL, and a frozen start would send the next
|
|
// folder opened there back to the first place.
|
|
const asked: Start | null = askedHome
|
|
? { home: askedHome }
|
|
: Number.isInteger(askedLane) && askedLane > 0
|
|
? { lane: askedLane }
|
|
: null;
|
|
const [fallback, setStart] = useState<Start | null>(null);
|
|
const start = asked ?? fallback;
|
|
const [none, setNone] = useState(false);
|
|
|
|
useEffect(() => {
|
|
if (start) return;
|
|
void (async () => {
|
|
// The workspace last shown first: Files reached from a workspace opens
|
|
// THAT workspace (from PhuNT → AC_SHOP it opened your own home, empty
|
|
// but for hidden files); the home is the fallback.
|
|
const { lanes } = await api.lanes.list().catch(() => ({ lanes: [] }));
|
|
const last = lastWorkspace();
|
|
const shown = lanes.find((l) => l.id === last);
|
|
if (shown) return setStart({ lane: shown.id });
|
|
const homes = await api.files.homes().catch(() => ({ homes: [] }));
|
|
if (homes.homes.some((h) => h.id === "me")) return setStart({ home: "me" });
|
|
if (lanes[0]) setStart({ lane: lanes[0].id });
|
|
else setNone(true);
|
|
})();
|
|
}, [start]);
|
|
|
|
if (none) return <p className="p-8 text-sm text-fg-muted">{t("fx.noWorkspace")}</p>;
|
|
if (!start) return null;
|
|
return (
|
|
// Cancels Layout's page padding, like the Workspace session view.
|
|
<div className="-m-5 flex h-[calc(100vh-1.5rem)] max-md:h-[calc(100vh-4.25rem)] lg:-m-6">
|
|
<FileBrowser
|
|
laneId={"lane" in start ? start.lane : null}
|
|
initialHome={"home" in start ? start.home : null}
|
|
initialPath={params.get("path") ?? ""}
|
|
onNavigate={(place: FsPlace, path) => {
|
|
const next = new URLSearchParams(
|
|
isHomePlace(place)
|
|
? { home: place.home }
|
|
: { lane: String(typeof place === "number" ? place : place.lane) }
|
|
);
|
|
if (path) next.set("path", path);
|
|
setParams(next, { replace: false });
|
|
}}
|
|
/>
|
|
</div>
|
|
);
|
|
}
|