feat: Stop archives a session, Sleep keeps it; SSH keys per type (#82); update Auto fix

- Stop (a tab's or pane's X, Close tab) now archives the session it held
  (DELETE /api/run/:id?archive=1); the new Sleep (moon left of the X, and in
  the tab, pane and session right-click menus) stops the pane but keeps the
  session listed to resume later. Shared endRuns in client/src/lib/stopSession.ts.
- Git identity (#82): an account keeps an ED25519 AND an RSA key (AWS
  CodeCommit takes only RSA), every ssh git offers both, and a member's
  ~/.gitconfig gets core.sshCommand so a git typed in a terminal uses them.
- Update dialog: Auto fix starts a Claude Code session on the install
  folder with the failed update's error.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-15 11:51:26 +07:00
parent 58be04a4e7
commit 0a1dcfeb7f
28 changed files with 813 additions and 225 deletions
+29 -4
View File
@@ -1,8 +1,9 @@
/**
* @file HTTP surface for a user's OWN git identity (`server/lib/git-identity.js`),
* mounted at `/api/me/git`: the SSH key a clone uses (make, replace, delete —
* mounted at `/api/me/git`: the SSH keys a clone uses — ed25519 and/or rsa (make, replace, delete —
* only the PUBLIC key is ever returned) and the HTTPS logins (save, delete —
* `hasToken` only, never a password), and the commit author (`user.name` /
* `hasToken` only, never a password), whether the account's own terminal
* git uses that key (`core.sshCommand`), and the commit author (`user.name` /
* `user.email` of the account's global git config — a member's commits fail
* without one). The account is the signed-in user's
* own Linux account under S3, else the host's, which only an admin (or local
@@ -27,6 +28,7 @@ const STATUS = {
EBADAUTHOR: 400,
ESTOREUNREADABLE: 409,
EBADKEYTYPE: 400,
EFOREIGNSSH: 409,
};
function fail(res, e) {
@@ -36,10 +38,15 @@ function fail(res, e) {
}
async function view(account) {
const keys = identity.sshKeysInfo(account);
const terminal = keys.length ? await identity.terminalSshState(account) : undefined;
return {
account: account.kind,
writable: account.kind === "member" || account.writable !== false,
ssh: identity.sshKeyInfo(account),
// `ssh`: the first key, as before there could be two; `sshKeys`: all.
ssh: keys[0] ? { ...keys[0], terminal } : null,
sshKeys: keys,
sshTerminal: terminal ?? null,
https: identity.listHttps(account),
author: await identity.getAuthor(account),
};
@@ -81,7 +88,8 @@ router.post("/ssh-key", sameOriginGuard, async (req, res) => {
router.delete("/ssh-key", sameOriginGuard, async (req, res) => {
try {
const account = accountOf(req, { write: true });
if (!identity.removeSshKey(account)) {
const type = req.query.type === undefined ? undefined : String(req.query.type);
if (!(await identity.removeSshKey(account, type))) {
return res.status(404).json({ error: { code: "ENOTFOUND", message: "no SSH key" } });
}
res.json(await view(account));
@@ -90,6 +98,23 @@ router.delete("/ssh-key", sameOriginGuard, async (req, res) => {
}
});
/**
* Whether a plain `git` in the account's own terminal uses the AgentHub key
* (`core.sshCommand` in its `~/.gitconfig`, issue #82). `{on: boolean}`.
*/
router.put("/ssh-terminal", sameOriginGuard, async (req, res) => {
try {
const account = accountOf(req, { write: true });
if (!identity.sshKeyInfo(account) && req.body?.on === true) {
return res.status(404).json({ error: { code: "ENOTFOUND", message: "no SSH key" } });
}
await identity.setTerminalSsh(account, req.body?.on === true);
res.json(await view(account));
} catch (e) {
fail(res, e);
}
});
router.post("/https", sameOriginGuard, async (req, res) => {
try {
const account = accountOf(req, { write: true });