| ... | @@ -5,7 +5,10 @@ import { CollapsibleSectionHeader, SectionHeaderStatus } from "../ui/collapsible | ... | @@ -5,7 +5,10 @@ import { CollapsibleSectionHeader, SectionHeaderStatus } from "../ui/collapsible |
| 5 | import { SubagentTooltipContent } from "./SubagentTooltipContent"; | 5 | import { SubagentTooltipContent } from "./SubagentTooltipContent"; |
| 6 | import { PullRequestGlyph } from "../pullRequest/pullRequestIcons"; | 6 | import { PullRequestGlyph } from "../pullRequest/pullRequestIcons"; |
| 7 | import { scopedThreadKey, scopeThreadRef } from "@t3tools/client-runtime/environment"; | 7 | import { scopedThreadKey, scopeThreadRef } from "@t3tools/client-runtime/environment"; |
| 8 | import { projectedSubagentsToRuntime } from "@t3tools/client-runtime/state/subagentRuntime"; | 8 | import { |
| | 9 | projectedSubagentsToRuntime, |
| | 10 | type RuntimeSubagent, |
| | 11 | } from "@t3tools/client-runtime/state/subagentRuntime"; |
| 9 | import { formatSubagentDisplayTitle } from "@t3tools/client-runtime/state/subagent-display"; | 12 | import { formatSubagentDisplayTitle } from "@t3tools/client-runtime/state/subagent-display"; |
| 10 | import { | 13 | import { |
| 11 | deriveThreadRelationshipGraph, | 14 | deriveThreadRelationshipGraph, |
| ... | @@ -23,6 +26,7 @@ import { | ... | @@ -23,6 +26,7 @@ import { |
| 23 | } from "@t3tools/client-runtime/state/thread-workflows"; | 26 | } from "@t3tools/client-runtime/state/thread-workflows"; |
| 24 | import type { EnvironmentId, OrchestrationV2ThreadShell, ThreadId } from "@t3tools/contracts"; | 27 | import type { EnvironmentId, OrchestrationV2ThreadShell, ThreadId } from "@t3tools/contracts"; |
| 25 | import { groupBy } from "effect/Array"; | 28 | import { groupBy } from "effect/Array"; |
| | 29 | import * as DateTime from "effect/DateTime"; |
| 26 | import { useNavigate } from "@tanstack/react-router"; | 30 | import { useNavigate } from "@tanstack/react-router"; |
| 27 | import { | 31 | import { |
| 28 | ArrowRightIcon, | 32 | ArrowRightIcon, |
| ... | @@ -162,6 +166,30 @@ function relationshipThreadTitle(input: { | ... | @@ -162,6 +166,30 @@ function relationshipThreadTitle(input: { |
| 162 | return formatSubagentDisplayTitle(input.title); | 166 | return formatSubagentDisplayTitle(input.title); |
| 163 | } | 167 | } |
| 164 | | 168 | |
| | 169 | /** |
| | 170 | * A delegated task settles with its first run, but the parent can keep sending |
| | 171 | * the child follow-ups. While the child thread has a live run, the row's timer |
| | 172 | * and hover card follow that run instead of the settled task. |
| | 173 | */ |
| | 174 | function liveSubagent<Agent extends RuntimeSubagent>( |
| | 175 | agent: Agent | undefined, |
| | 176 | childThread: OrchestrationV2ThreadShell | null | undefined, |
| | 177 | ): Agent | undefined { |
| | 178 | const liveStatus = childThread?.activityRunStatus; |
| | 179 | if (!agent || !liveStatus) return agent; |
| | 180 | const startedAt = childThread.activityRunStartedAt; |
| | 181 | return { |
| | 182 | ...agent, |
| | 183 | status: liveStatus === "running" || liveStatus === "waiting" ? liveStatus : "pending", |
| | 184 | startedAt: startedAt ? DateTime.formatIso(startedAt) : null, |
| | 185 | completedAt: null, |
| | 186 | // The settled task's output belongs to its first run, not this one. |
| | 187 | progress: null, |
| | 188 | result: null, |
| | 189 | error: null, |
| | 190 | }; |
| | 191 | } |
| | 192 | |
| 165 | export function ThreadRelationshipsPanel(props: { | 193 | export function ThreadRelationshipsPanel(props: { |
| 166 | readonly environmentId: EnvironmentId; | 194 | readonly environmentId: EnvironmentId; |
| 167 | readonly threadId: ThreadId; | 195 | readonly threadId: ThreadId; |
| ... | @@ -239,9 +267,11 @@ export function ThreadRelationshipsPanel(props: { | ... | @@ -239,9 +267,11 @@ export function ThreadRelationshipsPanel(props: { |
| 239 | { id: "active", label: null, rows: active, expanded: true }, | 267 | { id: "active", label: null, rows: active, expanded: true }, |
| 240 | { id: "previous", label: "Previous agents", rows: previous, expanded: false }, | 268 | { id: "previous", label: "Previous agents", rows: previous, expanded: false }, |
| 241 | ]; | 269 | ]; |
| | 270 | // Subagents without a child thread yet have no row, so count them separately. |
| 242 | const runningCount = | 271 | const runningCount = |
| 243 | projection?.subagents.filter((agent) => agent.status === "running").length ?? | 272 | (projection?.subagents.filter( |
| 244 | active.filter(({ edge }) => edge.status === "running").length; | 273 | (agent) => agent.childThreadId === null && agent.status === "running", |
| | 274 | ).length ?? 0) + active.filter(({ edge }) => edge.status === "running").length; |
| 245 | | 275 | |
| 246 | if (relationshipRows.length === 0 && runningCount === 0) { | 276 | if (relationshipRows.length === 0 && runningCount === 0) { |
| 247 | return null; | 277 | return null; |
| ... | @@ -330,7 +360,10 @@ export function ThreadRelationshipsPanel(props: { | ... | @@ -330,7 +360,10 @@ export function ThreadRelationshipsPanel(props: { |
| 330 | ? BotIcon | 360 | ? BotIcon |
| 331 | : GitForkIcon; | 361 | : GitForkIcon; |
| 332 | const relationship = relationshipLabel(edge, props.threadId); | 362 | const relationship = relationshipLabel(edge, props.threadId); |
| 333 | const agent = isSubagent && !isParent ? subagentsByThreadId.get(threadId) : undefined; | 363 | const agent = liveSubagent( |
| | 364 | isSubagent && !isParent ? subagentsByThreadId.get(threadId) : undefined, |
| | 365 | node?.thread, |
| | 366 | ); |
| 334 | const threadTitle = relationshipThreadTitle({ | 367 | const threadTitle = relationshipThreadTitle({ |
| 335 | title: node?.thread?.title ?? agent?.title ?? threadId, | 368 | title: node?.thread?.title ?? agent?.title ?? threadId, |
| 336 | isSubagent, | 369 | isSubagent, |