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
+24 -5
View File
@@ -2670,7 +2670,12 @@ export const api = {
method: "POST",
body: JSON.stringify({ replace, type }),
}),
removeKey: () => request<GitIdentity>("/me/git/ssh-key", { method: "DELETE" }),
/** DELETE /api/me/git/ssh-key — the key of `type`, or every key. */
removeKey: (type?: "ed25519" | "rsa") =>
request<GitIdentity>(`/me/git/ssh-key${type ? `?type=${type}` : ""}`, { method: "DELETE" }),
/** PUT /api/me/git/ssh-terminal — use the key for git typed in the account's terminal. */
setTerminalSsh: (on: boolean) =>
request<GitIdentity>("/me/git/ssh-terminal", { method: "PUT", body: JSON.stringify({ on }) }),
saveHttps: (body: { host: string; username: string; password: string }) =>
request<GitIdentity>("/me/git/https", { method: "POST", body: JSON.stringify(body) }),
removeHttps: (host: string) =>
@@ -2992,9 +2997,13 @@ export const api = {
method: "POST",
body: JSON.stringify({ mode }),
}),
/** DELETE /api/run/:id - kill the tmux session. */
kill: (id: string) =>
request<{ ok: true }>(`/run/${encodeURIComponent(id)}`, { method: "DELETE" }),
/** DELETE /api/run/:id - kill the tmux session. `archive` (the UI's Stop)
* also archives the session it held; Sleep leaves it listed to resume. */
kill: (id: string, opts?: { archive?: boolean }) =>
request<{ ok: true; archived?: string | null }>(
`/run/${encodeURIComponent(id)}${opts?.archive ? "?archive=1" : ""}`,
{ method: "DELETE" }
),
/** GET /api/run/interrupted - sessions whose pane died under them (reboot,
* killed tmux, crash), newest first; the caller's own workspaces only. */
interrupted: () => request<{ items: InterruptedRun[] }>("/run/interrupted"),
@@ -4190,7 +4199,17 @@ export interface GitIdentity {
account: "member" | "host";
/** False for the host's identity seen by a non-admin. */
writable: boolean;
ssh: { type?: "ed25519" | "rsa"; publicKey: string; fingerprint: string } | null;
ssh: {
type?: "ed25519" | "rsa";
publicKey: string;
fingerprint: string;
/** Whether a plain `git` in the account's terminal uses the key (`~/.gitconfig` core.sshCommand); `foreign` = the user set their own. */
terminal?: "on" | "off" | "foreign";
} | null;
/** Every key the account holds (ED25519 first); absent from an older server. */
sshKeys?: { type?: "ed25519" | "rsa"; publicKey: string; fingerprint: string }[];
/** The terminal switch (shared by both keys); null without a key. */
sshTerminal?: "on" | "off" | "foreign" | null;
/** The account's commit author (`git config --global`); null fields = unset. */
author: { name: string | null; email: string | null };
https: {
+57
View File
@@ -0,0 +1,57 @@
/**
* @file stopSession — the one way the UI ends live panes: **Stop** kills the
* pane and ARCHIVES the session it held (`DELETE /api/run/:id?archive=1`), so
* a session the user closed leaves its workspace's lists; **Sleep** kills the
* pane and KEEPS the session listed, to be resumed later by sending it a
* message. Both ask first when any pane holds an AI agent (it may be
* mid-turn); a plain shell holds no session, so it neither asks nor sleeps.
* Used by the tab bar (`ConsoleTabs`), the split panes (`Workspace`) and a
* session's right-click menu (`SessionMenu`).
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
*/
import type { TFunction } from "i18next";
import { api, type RunHandle } from "./api";
import { confirmDialog } from "./confirm";
import { toast } from "./toast";
/** Whether a pane can sleep: an AI pane (a shell has no session to keep). */
export const canSleep = (run: RunHandle | null | undefined): boolean =>
!!run && run.tool !== "shell";
/**
* Stop (archive) or sleep (keep) `runs` after a confirm, and return the ids
* that were ended. `name` titles a one-pane confirm; `t` is the `lanes`
* namespace.
*/
export async function endRuns(
runs: RunHandle[],
{ sleep = false, name, t }: { sleep?: boolean; name: string; t: TFunction }
): Promise<string[]> {
const list = sleep ? runs.filter(canSleep) : runs;
if (!list.length) return [];
const kind = sleep ? "sleep" : "stop";
if (
list.some((r) => r.tool !== "shell") &&
!(await confirmDialog({
title:
list.length > 1
? t(`tabs.${kind}GroupTitle`, { count: list.length })
: t(`tabs.${kind}Title`, { name }),
message: t(`tabs.${kind}Body`),
confirmLabel: t(`tabs.${kind}Confirm`),
destructive: !sleep,
}))
)
return [];
const ended: string[] = [];
for (const r of list) {
try {
await api.run.kill(r.id, { archive: !sleep });
ended.push(r.id);
} catch (err) {
toast.error(err);
}
}
return ended;
}