feat(run): show externally started Claude sessions in the active-runs list
The Workspace active-runs list only knew about runs this dashboard spawned, so two `claude` sessions started by hand in terminal tabs showed up nowhere — the list read "no active runs" while two agents were working. Poll GET /api/sessions?status=active alongside the run list and merge those sessions in as live rows, deduped against dashboard runs by session_id and filtered to local sources with a cwd (a remote-source or cwd-less session cannot be resumed on this machine). External rows get no Attach action: the dashboard owns no tmux session for them, so there is no PTY to bridge. They offer Resume, which reuses the existing ensure-lane + start-with-resumeSessionId path to spawn a new tmux-backed `claude --resume` in that folder — a second process on the same transcript, not a view of the original terminal.
This commit is contained in:
@@ -17,6 +17,7 @@ import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import i18n from "i18next";
|
||||
import { ActiveRunsSwitcher, RunsModal, type UnifiedRunRow } from "../RunHistory";
|
||||
import type { DashboardRunHistoryItem, RunListResponse } from "../../../lib/api";
|
||||
import type { Session } from "../../../lib/types";
|
||||
|
||||
const LIVE_ID = "run-live";
|
||||
const PAST_ID = "run-past";
|
||||
@@ -67,6 +68,21 @@ const HEADLESS = historyItem({
|
||||
started_at: new Date(1000).toISOString(),
|
||||
});
|
||||
|
||||
function externalSession(over: Partial<Session> = {}): Session {
|
||||
return {
|
||||
id: "sess-external",
|
||||
name: "the external prompt",
|
||||
status: "active",
|
||||
cwd: "/tmp/external",
|
||||
model: "claude-opus-5",
|
||||
started_at: new Date(3000).toISOString(),
|
||||
ended_at: null,
|
||||
updated_at: new Date(3000).toISOString(),
|
||||
source: "local",
|
||||
...over,
|
||||
} as unknown as Session;
|
||||
}
|
||||
|
||||
function renderSwitcher(overrides: Partial<React.ComponentProps<typeof ActiveRunsSwitcher>> = {}) {
|
||||
const spies = {
|
||||
onAttach: vi.fn(),
|
||||
@@ -188,6 +204,44 @@ describe("ActiveRunsSwitcher", () => {
|
||||
expect(screen.queryByText("stale copy")).toBeNull();
|
||||
});
|
||||
|
||||
it("counts and lists a session started outside the dashboard, resumable not attachable", () => {
|
||||
const { spies } = renderSwitcher({
|
||||
activeRuns: null,
|
||||
runHistory: [],
|
||||
externalSessions: [externalSession()],
|
||||
});
|
||||
fireEvent.click(screen.getByText(i18n.t("run:runs.viewActive_other", { count: 1 })));
|
||||
expect(screen.getByText("the external prompt")).toBeTruthy();
|
||||
expect(screen.getByText(i18n.t("run:runs.externalBadge"))).toBeTruthy();
|
||||
// No tmux session of ours to attach to — Resume is the only action.
|
||||
expect(screen.queryByText(i18n.t("run:runs.attachLabel", "Attach"))).toBeNull();
|
||||
fireEvent.click(screen.getByText(i18n.t("run:resume.resumeOption")));
|
||||
expect(spies.onResumeFromHistory).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
id: "session:sess-external",
|
||||
session_id: "sess-external",
|
||||
cwd: "/tmp/external",
|
||||
status: "running",
|
||||
isLive: true,
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it("skips external sessions already covered by a run, remote ones, and cwd-less ones", () => {
|
||||
renderSwitcher({
|
||||
runHistory: [],
|
||||
externalSessions: [
|
||||
externalSession({ id: "sess-live" }), // same session as the live run
|
||||
externalSession({ id: "sess-remote", source: "remote-1" }),
|
||||
externalSession({ id: "sess-nocwd", cwd: null }),
|
||||
],
|
||||
});
|
||||
fireEvent.click(screen.getByText(i18n.t("run:runs.viewActive_other", { count: 1 })));
|
||||
expect(screen.getByText("the live prompt")).toBeTruthy();
|
||||
expect(screen.queryByText("the external prompt")).toBeNull();
|
||||
expect(screen.queryByText(i18n.t("run:runs.externalBadge"))).toBeNull();
|
||||
});
|
||||
|
||||
it("fires attach with the run id of the row that was clicked", () => {
|
||||
const { spies } = renderSwitcher();
|
||||
openModal();
|
||||
|
||||
Reference in New Issue
Block a user