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:
2026-08-18 09:49:03 +07:00
parent 2c29504c75
commit 174c650624
7 changed files with 181 additions and 21 deletions
@@ -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();