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>
This commit is contained in:
2026-10-14 16:04:03 +07:00
parent e94038481e
commit 22de5009ff
79 changed files with 4617 additions and 231 deletions
+13 -3
View File
@@ -270,9 +270,13 @@ export function FileBrowser({
const folderInput = useRef<HTMLInputElement>(null);
const ctx = useContextMenu<FsEntry | null>();
// A new caller target (another workspace's Files tab, the page's URL).
// A new caller target (another workspace's Files tab, the page's URL) —
// not the echo of our own `go()` through the page's URL, which would wipe
// Back's history on every folder opened.
useEffect(() => {
setTarget(placeOf());
const next = placeOf();
if (samePlace(next, target) && initialPath === path) return;
setTarget(next);
setPathState(initialPath);
setHistory({ back: [], fwd: [] });
setSelected(new Set());
@@ -1145,7 +1149,13 @@ export function FileBrowser({
{listing && !entries.length && !creating && !error && (
<div className="flex flex-col items-center justify-center gap-2 py-16 text-fg-muted">
<Folder className="h-16 w-16 opacity-40" />
<p className="text-sm">{query ? t("fx.noMatch") : t("fx.empty")}</p>
<p className="max-w-md text-center text-sm">
{query
? t("fx.noMatch")
: isHomePlace(target) && !path
? t("fx.homeEmpty")
: t("fx.empty")}
</p>
</div>
)}
{loading && !listing && <p className="p-6 text-sm text-fg-muted">{t("fx.loading")}</p>}