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
+47 -1
View File
@@ -14,6 +14,7 @@
import { useEffect, useMemo, useRef, useState, type KeyboardEvent, type ReactNode } from "react";
import { useTranslation } from "react-i18next";
import { Copy, Eye, EyeOff, Plus, Sparkles, X } from "lucide-react";
import { confirmDialog } from "../../lib/confirm";
import { api, type AuthUser, type ServerUserUsage, type UserPatch } from "../../lib/api";
import type { Lane } from "../../lib/types";
import { fmtCost, formatDateTime } from "../../lib/format";
@@ -131,6 +132,38 @@ export function UserEditDialog({
}
};
/** Delete the user for good — asked first, with what goes, in red. */
const remove = async () => {
if (self || busy) return;
if (!current) {
setError(t("delete.needsPassword"));
return;
}
const name = user.displayName || user.username;
const ok = await confirmDialog({
title: t("delete.title", { name }),
message: t("delete.body", { name, count: owned?.length ?? 0, user: user.username }),
confirmLabel: t("delete.confirm"),
destructive: true,
});
if (!ok) return;
setBusy(true);
setError(null);
try {
await api.users.remove(user.id, current);
onDone(t("done.deleted", { name }));
} catch (e) {
const err = e as Error & { code?: string };
setError(
err.code && t(`errors.${err.code}`, { defaultValue: "" })
? t(`errors.${err.code}`)
: err.message
);
} finally {
setBusy(false);
}
};
const onKeyDown = (e: KeyboardEvent<HTMLInputElement>) => {
if (isSubmitEnter(e)) {
e.preventDefault();
@@ -459,8 +492,21 @@ export function UserEditDialog({
</p>
)}
<div className="flex items-center justify-end gap-2">
{!self && (
<button
type="button"
onClick={() => void remove()}
disabled={busy}
data-testid="user-delete"
className="mr-auto rounded-md border border-status-danger/50 px-3 py-1.5 text-sm text-status-danger hover:bg-status-danger/10 disabled:opacity-50"
>
{t("delete.button")}
</button>
)}
{!changed && (
<span className="mr-auto text-xs text-fg-muted">{t("edit.noChanges")}</span>
<span className={`text-xs text-fg-muted ${self ? "mr-auto" : ""}`}>
{t("edit.noChanges")}
</span>
)}
<button type="button" onClick={onClose} className="btn-ghost">
{t("common:cancel")}