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
+23 -4
View File
@@ -288,6 +288,17 @@ const userExitStmt = db.prepare(`
LIMIT 1
`);
const anyEndStmt = db.prepare(`
SELECT 1 FROM events WHERE session_id = ? AND event_type = 'SessionEnd' LIMIT 1
`);
// Whether the pane's tmux server was already up when the run started (and is
// still): then the pane ended on its own, not with its server.
function tmuxOutlived(row, serverStartedAt) {
const since = serverStartedAt(row.id);
const started = Date.parse(row.started_at || "");
return Number.isFinite(since) && Number.isFinite(started) && since <= started;
}
// A shell has no session, so its row only tracks the open tab: once closed it
// is dropped — the sidebar keeps no list of closed Terminal tabs.
const pruneShellsStmt = db.prepare(`
@@ -303,12 +314,16 @@ const pruneShellsStmt = db.prepare(`
* A pane AgentHub closed itself (Stop, close tab, Quit) is already `killed`.
* Of the rest, an agent session the user did not exit is `interrupted` — a
* reboot, a killed tmux, a crash — and offered for resume
* (`listInterrupted`); everything else is `abandoned`. Only Claude reports
* how it ended, so any other tool's vanished pane counts as interrupted.
* (`listInterrupted`); everything else is `abandoned`. A pane whose tmux
* server kept running since before it started (`serverStartedAt(id)`, epoch
* ms or null) ended by itself — the user quit Codex, `exit`ed, closed it —
* so it is not offered either: for Claude only when it said it ended (any
* SessionEnd; none at all is a crash), for any other tool, which says
* nothing, always. A server that cannot be asked counts as gone.
*
* Returns the number of rows updated.
*/
function reconcileOrphans(isAlive = () => false) {
function reconcileOrphans(isAlive = () => false, serverStartedAt = () => null) {
try {
let changes = 0;
const { unseenMemberFolder } = require("./linux-accounts");
@@ -317,7 +332,11 @@ function reconcileOrphans(isAlive = () => false) {
const interrupted =
row.tool !== "shell" &&
!!row.session_id &&
!(row.tool === "claude" && userExitStmt.get(row.session_id));
!(row.tool === "claude" && userExitStmt.get(row.session_id)) &&
!(
tmuxOutlived(row, serverStartedAt) &&
(row.tool !== "claude" || anyEndStmt.get(row.session_id))
);
changes += endStmt.run({
id: row.id,
status: interrupted ? "interrupted" : "abandoned",