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
+18 -5
View File
@@ -5,6 +5,8 @@
* the selected lane's pipeline map, and run configuration/console/history below.
* Runs are tied to lanes: starting a run posts to POST /api/lanes/:id/start.
* When a cwd isn't owned by any lane, calls POST /api/lanes/ensure first.
* Polls GET /api/sessions?status=active alongside the run list so Claude Code
* sessions started outside the dashboard also appear in the active-runs list.
*
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
*/
@@ -42,7 +44,7 @@ import { useTranslation } from "react-i18next";
import { Plus } from "lucide-react";
import { api } from "../lib/api";
import type { CwdSuggestion, RunListResponse } from "../lib/api";
import type { Lane, LaneFeature, LaneCounts, ProofFeature, WSMessage } from "../lib/types";
import type { Lane, LaneFeature, LaneCounts, ProofFeature, Session, WSMessage } from "../lib/types";
import { eventBus } from "../lib/eventBus";
import { LaneConsolePane } from "../components/run/LaneConsolePane";
import PipelineMap from "../components/lanes/PipelineMap";
@@ -93,6 +95,7 @@ function ConsoleArea({
binaryStatus,
cwdSuggestions,
activeRuns,
externalSessions,
wsConnected,
defaultCwd,
onHasActiveRunChange,
@@ -108,6 +111,7 @@ function ConsoleArea({
binaryStatus: { found: boolean; path: string | null } | null;
cwdSuggestions: CwdSuggestion[];
activeRuns: RunListResponse | null;
externalSessions: Session[];
wsConnected: boolean;
defaultCwd: string;
onHasActiveRunChange: (val: boolean) => void;
@@ -128,6 +132,7 @@ function ConsoleArea({
binaryStatus={binaryStatus}
cwdSuggestions={cwdSuggestions}
activeRuns={activeRuns}
externalSessions={externalSessions}
wsConnected={wsConnected}
defaultCwd={defaultCwd}
onHasActiveRunChange={onHasActiveRunChange}
@@ -149,6 +154,7 @@ function ConsoleArea({
binaryStatus={binaryStatus}
cwdSuggestions={cwdSuggestions}
activeRuns={activeRuns}
externalSessions={externalSessions}
wsConnected={wsConnected}
defaultCwd={defaultCwd}
/>
@@ -180,6 +186,10 @@ export function Workspace() {
// Run state kept at page level: shared across every pane, or drives the
// lane strip itself rather than any one pane's form.
const [activeRuns, setActiveRuns] = useState<RunListResponse | null>(null);
// Claude Code sessions running outside the dashboard (a `claude` the user
// started in a terminal tab). They are not dashboard runs, so the run list
// would otherwise show nothing while two agents are working.
const [externalSessions, setExternalSessions] = useState<Session[]>([]);
const [binaryStatus, setBinaryStatus] = useState<{ found: boolean; path: string | null } | null>(
null
);
@@ -236,10 +246,6 @@ export function Workspace() {
// `claude` is missing from PATH — leave the probe unresolved rather than
// showing a misleading "claude missing" banner for an unrelated fault.
.catch(() => undefined);
api.run
.list()
.then(setActiveRuns)
.catch(() => undefined);
api.lanes
.pipelines()
.then((r) => setPipelineTemplates(r.pipelines))
@@ -320,6 +326,10 @@ export function Workspace() {
.list()
.then(setActiveRuns)
.catch(() => undefined);
api.sessions
.list({ status: "active", limit: 50, sort_by: "updated_at", sort_desc: true })
.then((r) => setExternalSessions(r.sessions))
.catch(() => undefined);
}, []);
// Background poll so the run list and history reflect external changes
@@ -327,6 +337,7 @@ export function Workspace() {
// no WS event fires. Lighter than typical WS gaps; aggressive enough that
// status flips appear within seconds without needing a manual refresh.
useEffect(() => {
refreshList();
const tick = setInterval(() => {
refreshList();
}, 5000);
@@ -706,6 +717,7 @@ export function Workspace() {
binaryStatus={binaryStatus}
cwdSuggestions={cwdSuggestions}
activeRuns={activeRuns}
externalSessions={externalSessions}
wsConnected={wsConnected}
defaultCwd={defaultCwd}
onHasActiveRunChange={setPaneHasActiveRun}
@@ -730,6 +742,7 @@ export function Workspace() {
binaryStatus={binaryStatus}
cwdSuggestions={cwdSuggestions}
activeRuns={activeRuns}
externalSessions={externalSessions}
wsConnected={wsConnected}
defaultCwd={defaultCwd}
onHasActiveRunChange={setPaneHasActiveRun}