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:
@@ -81,8 +81,9 @@ import type {
|
||||
} from "../lib/api";
|
||||
import type { Lane, LaneFeature, ProofFeature, Session } from "../lib/types";
|
||||
import { toast } from "../lib/toast";
|
||||
import { endRuns } from "../lib/stopSession";
|
||||
import { ContextMenu, MenuItem, useContextMenu } from "../components/ContextMenu";
|
||||
import { confirmDialog, confirmDiscard } from "../lib/confirm";
|
||||
import { confirmDiscard } from "../lib/confirm";
|
||||
import { keepIfSame } from "../lib/keepIfSame";
|
||||
import { useWorkLanes } from "../hooks/useLanes";
|
||||
import { LaneConsolePane } from "../components/run/LaneConsolePane";
|
||||
@@ -423,27 +424,12 @@ function ConsoleArea({
|
||||
else onNewSession?.();
|
||||
};
|
||||
|
||||
// A pane header's X: stops that one session, as a one-pane tab's X does —
|
||||
// an AI session may be mid-turn, so it asks first; a shell holds no agent.
|
||||
const stopPane = async (slot: number) => {
|
||||
// A pane header's X stops (and archives) that one session, its moon sleeps
|
||||
// it — as a one-pane tab's buttons do (`endRuns` asks first for an AI pane).
|
||||
const stopPane = async (slot: number, sleep = false) => {
|
||||
const run = runOf(slot);
|
||||
if (!run) return;
|
||||
if (
|
||||
run.tool !== "shell" &&
|
||||
!(await confirmDialog({
|
||||
title: t("tabs.stopTitle", { name: label(run) }),
|
||||
message: t("tabs.stopBody"),
|
||||
confirmLabel: t("tabs.stopConfirm"),
|
||||
destructive: true,
|
||||
}))
|
||||
)
|
||||
return;
|
||||
try {
|
||||
await api.run.kill(run.id);
|
||||
ended(slot);
|
||||
} catch (err) {
|
||||
toast.error(err);
|
||||
}
|
||||
if ((await endRuns([run], { sleep, name: label(run), t })).length) ended(slot);
|
||||
};
|
||||
|
||||
// A split menu's "New terminal" or AI tool (bare, as the tab bar's `+`
|
||||
@@ -632,6 +618,7 @@ function ConsoleArea({
|
||||
onNewSplit={(target, edge, tool) => void newSplit(target, edge, tool)}
|
||||
stateOf={stateOf}
|
||||
onStop={(slot) => void stopPane(slot)}
|
||||
onSleep={(slot) => void stopPane(slot, true)}
|
||||
// Keyed by workspace too, so a kept pane coming back is the same one.
|
||||
keyOf={allTabs ? undefined : (slot) => paneKey(selectedLaneId ?? 0, slot)}
|
||||
kept={kept}
|
||||
|
||||
@@ -766,11 +766,21 @@ describe("console tabs", () => {
|
||||
expect(screen.getByTestId("console-tab-10")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("a tab's moon sleeps its session: stopped but not archived", async () => {
|
||||
withRuns([live(0), live(1)]);
|
||||
await open("/workspace?lane=1&run=agenthub-lane-1");
|
||||
await userEvent.setup().click(screen.getByTestId("console-tab-sleep-0"));
|
||||
expect(api.run.kill).toHaveBeenCalledWith("agenthub-lane-1", { archive: false });
|
||||
await waitFor(() =>
|
||||
expect(screen.getByTestId("console-tab-1")).toHaveAttribute("aria-selected", "true")
|
||||
);
|
||||
});
|
||||
|
||||
it("closing the active tab stops its session and opens the next one", async () => {
|
||||
withRuns([live(0), live(1)]);
|
||||
await open("/workspace?lane=1&run=agenthub-lane-1");
|
||||
await userEvent.setup().click(screen.getByTestId("console-tab-close-0"));
|
||||
expect(api.run.kill).toHaveBeenCalledWith("agenthub-lane-1");
|
||||
expect(api.run.kill).toHaveBeenCalledWith("agenthub-lane-1", { archive: true });
|
||||
await waitFor(() =>
|
||||
expect(screen.getByTestId("console-tab-1")).toHaveAttribute("aria-selected", "true")
|
||||
);
|
||||
@@ -916,8 +926,8 @@ describe("split panes", () => {
|
||||
withRuns([live(1)]);
|
||||
await userEvent.setup().click(screen.getByTestId("console-tab-close-0-2"));
|
||||
await settle();
|
||||
expect(api.run.kill).toHaveBeenCalledWith("agenthub-lane-1");
|
||||
expect(api.run.kill).toHaveBeenCalledWith("agenthub-lane-1-2");
|
||||
expect(api.run.kill).toHaveBeenCalledWith("agenthub-lane-1", { archive: true });
|
||||
expect(api.run.kill).toHaveBeenCalledWith("agenthub-lane-1-2", { archive: true });
|
||||
expect(shownPanes()).toEqual(["pane-1"]);
|
||||
expect(screen.getByTestId("run-param").textContent).toBe("agenthub-lane-1-1");
|
||||
});
|
||||
@@ -932,7 +942,7 @@ describe("split panes", () => {
|
||||
await userEvent.setup().click(screen.getByTestId("pane-stop-1"));
|
||||
await settle();
|
||||
expect(api.run.kill).toHaveBeenCalledTimes(1);
|
||||
expect(api.run.kill).toHaveBeenCalledWith("agenthub-lane-1-1");
|
||||
expect(api.run.kill).toHaveBeenCalledWith("agenthub-lane-1-1", { archive: true });
|
||||
expect(shownPanes()).toEqual(["pane-0", "pane-2"]);
|
||||
expect(screen.queryByTestId("console-tab-1")).toBeNull();
|
||||
});
|
||||
@@ -1017,9 +1027,11 @@ describe("split panes", () => {
|
||||
|
||||
await right(screen.getByTestId("console-tab-2"));
|
||||
await user.click(
|
||||
within(screen.getByTestId("console-tab-menu-2")).getByText("Close tab (stops its session)")
|
||||
within(screen.getByTestId("console-tab-menu-2")).getByText(
|
||||
"Close tab (stops and archives its session)"
|
||||
)
|
||||
);
|
||||
expect(api.run.kill).toHaveBeenCalledWith("agenthub-lane-1-2");
|
||||
expect(api.run.kill).toHaveBeenCalledWith("agenthub-lane-1-2", { archive: true });
|
||||
});
|
||||
|
||||
it("a split menu starts a new terminal in the first free slot", async () => {
|
||||
|
||||
Reference in New Issue
Block a user