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
+130 -6
View File
@@ -885,6 +885,16 @@ export const api = {
method: "PATCH",
body: JSON.stringify({ ...body, currentPassword }),
}),
/**
* DELETE /api/users/:id — delete the user for good: logins, their
* workspaces' records and (one Linux user per account) their Linux
* account and whole home. Never yourself or the last admin.
*/
remove: (id: number, currentPassword: string) =>
request<{ user: AuthUser }>(`/users/${id}`, {
method: "DELETE",
body: JSON.stringify({ currentPassword }),
}),
/** POST /api/users/:id/password — a new password; that user's logins end. */
setPassword: (id: number, password: string, currentPassword: string) =>
request<{ user: AuthUser }>(`/users/${id}/password`, {
@@ -2545,8 +2555,7 @@ export const api = {
method: "POST",
body: JSON.stringify({ paths }),
}),
trash: (t: FsPlace) =>
request<{ items: FsTrashItem[]; keepDays?: number }>(fsUrl(t, "/trash")),
trash: (t: FsPlace) => request<{ items: FsTrashItem[]; keepDays?: number }>(fsUrl(t, "/trash")),
restore: (t: FsPlace, id: string) =>
request<{ path: string; laneId: number | null }>(fsUrl(t, "/trash/restore"), {
method: "POST",
@@ -2613,11 +2622,14 @@ export const api = {
/** The signed-in user's own git identity (`/api/me/git`, `server/routes/git-identity.js`). */
gitIdentity: {
get: () => request<GitIdentity>("/me/git"),
/** Make the SSH key; `replace` swaps an existing one for a new pair. */
generateKey: (replace = false) =>
/**
* Make the SSH key; `replace` swaps an existing one for a new pair.
* `type`: ed25519 (default) or rsa (RSA 4096 — AWS CodeCommit takes only RSA).
*/
generateKey: (replace = false, type: "ed25519" | "rsa" = "ed25519") =>
request<GitIdentity>("/me/git/ssh-key", {
method: "POST",
body: JSON.stringify({ replace }),
body: JSON.stringify({ replace, type }),
}),
removeKey: () => request<GitIdentity>("/me/git/ssh-key", { method: "DELETE" }),
saveHttps: (body: { host: string; username: string; password: string }) =>
@@ -2646,6 +2658,58 @@ export const api = {
),
remove: (id: number) => request<{ ok: true }>(`/snapshots/${id}`, { method: "DELETE" }),
},
/** The Artifacts hub (`/api/artifacts`, `server/lib/artifacts.js`). */
artifacts: {
list: (params: { scope?: "mine" | "hub" | "all"; kind?: ArtifactKind; q?: string } = {}) => {
const qs = new URLSearchParams();
if (params.scope) qs.set("scope", params.scope);
if (params.kind) qs.set("kind", params.kind);
if (params.q) qs.set("q", params.q);
const s = qs.toString();
return request<{ items: Artifact[] }>(`/artifacts${s ? `?${s}` : ""}`);
},
settings: () => request<ArtifactSettings>("/artifacts/settings"),
setInstallsEnabled: (installsEnabled: boolean) =>
request<{ installsEnabled: boolean }>("/artifacts/settings", {
method: "PUT",
body: JSON.stringify({ installsEnabled }),
}),
/** Documents a workspace's sessions wrote, to pin. */
candidates: (laneId: number) =>
request<{ items: ArtifactCandidate[] }>(`/artifacts/candidates?laneId=${laneId}`),
/** Skill folders the caller may share (their own, and the workspace's). */
skills: (laneId?: number) =>
request<{ items: { name: string; from: "user" | "workspace" }[] }>(
`/artifacts/skills${laneId ? `?laneId=${laneId}` : ""}`
),
addPage: (body: { url: string; title?: string; description?: string }) =>
request<{ item: Artifact }>("/artifacts/page", { method: "POST", body: JSON.stringify(body) }),
pinFile: (body: { laneId: number; path: string; title?: string; sessionId?: string }) =>
request<{ item: Artifact }>("/artifacts/file", { method: "POST", body: JSON.stringify(body) }),
addSkill: (body: { name: string; laneId?: number; title?: string; description?: string }) =>
request<{ item: Artifact }>("/artifacts/skill", { method: "POST", body: JSON.stringify(body) }),
addPlugin: (body: { key: string; market: string; title?: string; description?: string }) =>
request<{ item: Artifact }>("/artifacts/plugin", {
method: "POST",
body: JSON.stringify(body),
}),
update: (id: number, body: { title?: string; description?: string; shared?: boolean }) =>
request<{ item: Artifact }>(`/artifacts/${id}`, { method: "PATCH", body: JSON.stringify(body) }),
remove: (id: number) => request<{ ok: true }>(`/artifacts/${id}`, { method: "DELETE" }),
install: (id: number, replace = false) =>
request<{ installed: string; replaced?: boolean }>(`/artifacts/${id}/install`, {
method: "POST",
body: JSON.stringify({ replace }),
}),
/** One of the snapshot's own files, served sandboxed. */
rawUrl: (id: number, path?: string, download = false) => {
const qs = new URLSearchParams();
if (path) qs.set("path", path);
if (download) qs.set("download", "1");
const s = qs.toString();
return `${BASE}/artifacts/${id}/raw${s ? `?${s}` : ""}`;
},
},
/** The mascot's custom pets (`/api/pets`, `server/lib/pets.js`). */
pets: {
list: () => request<PetList>("/pets"),
@@ -3640,6 +3704,21 @@ export const api = {
method: "PUT",
body: JSON.stringify({ instructions }),
}),
/** POST /api/lanes/:id/sources/clone — clone a related repo BESIDE the
* workspace folder and add it as an extra folder (409 ECREDENTIALS /
* ESSHAUTH like {@link lanes.clone}). */
cloneFolder: (
id: number,
body: {
url: string;
name?: string;
credentials?: { username: string; password: string; remember?: boolean };
}
) =>
request<LaneSourcesView & { path: string }>(`/lanes/${id}/sources/clone`, {
method: "POST",
body: JSON.stringify(body),
}),
/** POST /api/lanes/:id/sources/folder — an extra folder the agent may use. */
addFolder: (id: number, path: string) =>
request<LaneSourcesView>(`/lanes/${id}/sources/folder`, {
@@ -3820,6 +3899,51 @@ function requestCodexBackupsHelper(params?: {
export type CcArtifactType =
"skills" | "agents" | "commands" | "output-styles" | "memory" | "auto-memory";
export type ArtifactKind = "page" | "file" | "skill" | "plugin";
/** One row of the Artifacts hub (`server/lib/artifacts.js` `view`). */
export interface Artifact {
id: number;
kind: ArtifactKind;
title: string;
description: string;
ownerId: number | null;
owner: string | null;
mine: boolean;
canChange: boolean;
laneId: number | null;
sessionId: string | null;
/** page: the file it was published from; file: its workspace path;
* skill: its folder name; plugin: `name@marketplace`. */
source: string | null;
/** plugin: the marketplace source (owner/repo or a git URL). */
market: string | null;
/** page: the claude.ai link. */
url: string | null;
files: { path: string; size: number }[];
size: number | null;
shared: boolean;
sharedAt: string | null;
installs: number;
createdAt: string;
updatedAt: string;
}
export interface ArtifactCandidate {
path: string;
sessionId: string;
at: string;
exists: boolean;
pinned: boolean;
}
export interface ArtifactSettings {
serverMode: boolean;
canShare: boolean;
installsEnabled: boolean;
canManage: boolean;
}
/** Body for PUT /api/cc-config/file - create or overwrite one artifact. */
export interface CcWriteArgs {
// "auto-memory" targets a per-project memory file and requires `project`.
@@ -3950,7 +4074,7 @@ export interface GitIdentity {
account: "member" | "host";
/** False for the host's identity seen by a non-admin. */
writable: boolean;
ssh: { publicKey: string; fingerprint: string } | null;
ssh: { type?: "ed25519" | "rsa"; publicKey: string; fingerprint: string } | null;
/** The account's commit author (`git config --global`); null fields = unset. */
author: { name: string | null; email: string | null };
https: {