Files
AgentHub/client/src/pages/FilesPage.tsx
T
nntrivi2001 22de5009ff feat: v2.7.8 — Artifacts hub, clone related repos as extra folders, quieter resume prompt
- 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>
2026-10-14 16:06:54 +07:00

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>
);
}