From f452805709e2a6d960122e10054f85a24e83f0fd Mon Sep 17 00:00:00 2001 From: flamboh Date: Mon, 2 Nov 2026 07:05:48 +0000 Subject: [PATCH 1/4] fix(web): Agents popover rows match Lineage, with provider icons and readable models Co-authored-by: Claude Opus 5.5 (Claude Code) --- .../chat/ThreadRelationshipsControl.tsx | 652 ++++++++++-------- 1 file changed, 351 insertions(+), 301 deletions(-) diff --git a/apps/web/src/components/chat/ThreadRelationshipsControl.tsx b/apps/web/src/components/chat/ThreadRelationshipsControl.tsx index 300ea8860c3d7..ee8bbddb60fd2 100644 --- a/apps/web/src/components/chat/ThreadRelationshipsControl.tsx +++ b/apps/web/src/components/chat/ThreadRelationshipsControl.tsx @@ -74,6 +74,7 @@ import { applyProviderInstanceSettings, deriveProviderInstanceEntries, shouldShowInstanceBadge, + type ProviderInstanceEntry, } from "../../providerInstances"; import { threadEnvironment } from "../../state/threads"; import { orchestrationEnvironment } from "../../state/orchestration"; @@ -98,6 +99,7 @@ import { Tooltip, TooltipPopup, TooltipTrigger } from "../ui/tooltip"; import { toastManager } from "../ui/toast"; import { Spinner } from "../ui/spinner"; import { ProviderInstanceIcon } from "./ProviderInstanceIcon"; +import { getTriggerDisplayModelName } from "./providerIconUtils"; import { THREAD_DETAILS_PANEL_ACTION_GROUP_CLASS, THREAD_DETAILS_PANEL_META_CLASS, @@ -656,43 +658,333 @@ function useThreadLineage(props: { }; } -export function ThreadRelationshipsPanel(props: { - readonly environmentId: EnvironmentId; - readonly threadId: ThreadId; - readonly showLineage?: boolean; - readonly showSideChats?: boolean; +type ThreadLineage = ReturnType; + +function lineageModelLabel(model: string, providerEntry: ProviderInstanceEntry | undefined) { + const selectedModel = providerEntry?.models.find((entry) => entry.slug === model); + return selectedModel + ? getTriggerDisplayModelName(selectedModel, providerEntry?.driverKind) + : formatModelSlugName(model); +} + +function ThreadLineageRow(props: { + readonly lineage: ThreadLineage; + readonly currentThreadId: ThreadId; + readonly row: ThreadRelationshipWalkRow; + readonly menu: boolean; }) { const { - ref, parentRef, parentNativeStatus, graph, projects, providers, providerEntries, - providersByEnvironment, - remoteSubagents, subagentsByThreadId, currentThread, currentProject, - groups, - sideChats, - unlinkedRunningCount, mergeTargetThreadId, latestMergeBackRun, canMerge, - canDetach, busyAction, parentTitle, openThread, merge, - detach, stoppingId, workflowGroupsById, canOperateWorkflow, stopWorkflow, stopSubagent, - } = useThreadLineage(props); + } = props.lineage; + const { threadId, edge } = props.row; + const node = graph.nodes.get(threadId); + const isSubagent = edge.kind === "subagent"; + const isMergeTarget = threadId === mergeTargetThreadId; + const isParent = isParentThreadRelationship(edge, props.currentThreadId); + const rowStatus = + (isParent && threadId === parentRef?.threadId + ? (node?.thread?.activityRunStatus ?? parentNativeStatus) + : undefined) ?? threadRelationshipRowStatus(graph, { threadId, edge }); + const RelationshipIcon = isParent + ? CornerLeftUpIcon + : isSubagent + ? BotIcon + : node?.thread?.lineage.sideChat === true + ? MessagesSquareIcon + : GitForkIcon; + const relationship = + node?.thread?.lineage.sideChat === true && !isParent + ? "Side chat" + : relationshipLabel(edge, props.currentThreadId); + const projectedAgent = isSubagent && !isParent ? subagentsByThreadId.get(threadId) : undefined; + const workflowGroup = + projectedAgent === undefined ? undefined : workflowGroupsById.get(projectedAgent.id); + const agent = workflowGroup ? projectedAgent : currentSubagent(projectedAgent, node?.thread); + const failed = rowStatus === "failed" || rowStatus === "error"; + const canStop = + !props.menu && + agent && + (agent.origin === "app_owned" || + (agent.origin === "provider_native" && + agent.driver === "claudeAgent" && + agent.runId != null && + agent.nativeTaskRef?.strength === "strong" && + agent.nativeTaskRef.nativeId !== null && + ["pending", "running", "waiting"].includes(agent.nativeStatus))) && + agent.startedAt && + ["pending", "running", "waiting"].includes(agent.status); + const threadTitle = relationshipThreadTitle({ + title: workflowGroup?.workflow.workflowName ?? node?.thread?.title ?? agent?.title ?? threadId, + isSubagent, + }); + const provider = providers?.find( + (entry) => entry.instanceId === (agent?.providerInstanceId ?? node?.thread?.providerInstanceId), + ); + const providerDriver = agent?.driver ?? provider?.driver; + const providerEntry = providerEntries.find( + (entry) => entry.instanceId === (agent?.providerInstanceId ?? node?.thread?.providerInstanceId), + ); + const canStopWorkflow = + !props.menu && + canOperateWorkflow && + workflowGroup?.workflow.status === "running" && + agent?.origin === "provider_native" && + providerDriver === "claudeAgent"; + const trailingVisibilityClass = (workflowGroup ? canStopWorkflow : canStop) + ? "group-hover:opacity-0 group-focus-within:opacity-0 pointer-coarse:opacity-0 [@media(hover:none)]:opacity-0" + : ""; + const project = projects.find((project) => project.id === node?.thread?.projectId); + const relationshipHint = `Open ${relationship.toLowerCase()} in this chat`; + const RelationshipPopup = agent ? ThreadHoverCardPopup : TooltipPopup; + const relationshipTooltip = agent ? ( + } + status={agent.status} + result={agent.result} + progress={agent.progress} + parentThread={currentThread ?? undefined} + childThread={node?.thread ?? undefined} + parentProject={currentProject} + childProject={project} + /> + ) : ( + + ); + const relationshipContent = ( + <> + {providerDriver ? ( + + ) : ( + + )} + + {threadTitle} + + {relationship} + {node?.thread?.modelSelection.model + ? ` · ${lineageModelLabel(node.thread.modelSelection.model, providerEntry)}` + : ""} + + + + {isParent ? ( + "Parent" + ) : !isSubagent ? ( + edge.kind === "transfer" ? ( + relationship + ) : ( + "Fork" + ) + ) : failed ? ( + Failed + ) : ( + <> + {agent && deriveSubagentElapsedMs(agent, 0) !== null ? ( + + ) : ( + threadRelationshipStatusLabel(rowStatus) + )} + {rowStatus === "running" || rowStatus === "in_progress" ? ( + + ) : null} + + )} + + + ); + // A workflow pairs its menu row with a disclosure; other rows render directly. + const relationshipLink = ( + + : undefined} + onClick={() => openThread(threadId)} + /> + } + > + {relationshipContent} + + {relationshipTooltip} + + ); + if (workflowGroup && agent) { + return ( + openThread(memberThreadId as ThreadId)} + onStop={canStopWorkflow ? () => void stopWorkflow(agent.id) : undefined} + stopping={stoppingId === agent.id} + stopDisabled={stoppingId !== null} + header={relationshipLink} + /> + ); + } + return ( +
  • + {isMergeTarget ? ( +
    + + openThread(threadId)} + /> + } + > + {relationshipContent} + + {relationshipTooltip} + + + void merge()} + > + {busyAction === "merge" ? ( + + ) : ( + + )} + + } + /> + + {latestMergeBackRun === null + ? "Complete a run in this fork before merging it back" + : parentTitle + ? `Merge this conversation back into ${parentTitle}` + : "Merge this conversation back into its source"} + + + + {threadRelationshipStatusLabel(rowStatus)} + +
    + ) : ( + relationshipLink + )} + {canStop && agent ? ( +
    + + void stopSubagent(threadId, agent)} + /> + } + > + {stoppingId === threadId ? ( + + ) : ( + + )} + + Stop subagent + +
    + ) : null} +
  • + ); +} + +export function ThreadRelationshipsPanel(props: { + readonly environmentId: EnvironmentId; + readonly threadId: ThreadId; + readonly showLineage?: boolean; + readonly showSideChats?: boolean; +}) { + const lineage = useThreadLineage(props); + const { + ref, + providerEntries, + providersByEnvironment, + remoteSubagents, + currentThread, + groups, + sideChats, + unlinkedRunningCount, + canDetach, + busyAction, + detach, + } = lineage; const delegatedFrom = currentThread?.delegatedFrom; @@ -704,281 +996,15 @@ export function ThreadRelationshipsPanel(props: { return null; } - const renderRelationshipRow = ({ threadId, edge }: ThreadRelationshipWalkRow) => { - const node = graph.nodes.get(threadId); - const isSubagent = edge.kind === "subagent"; - const isMergeTarget = threadId === mergeTargetThreadId; - const isParent = isParentThreadRelationship(edge, props.threadId); - const rowStatus = - (isParent && threadId === parentRef?.threadId - ? (node?.thread?.activityRunStatus ?? parentNativeStatus) - : undefined) ?? threadRelationshipRowStatus(graph, { threadId, edge }); - const RelationshipIcon = isParent - ? CornerLeftUpIcon - : isSubagent - ? BotIcon - : node?.thread?.lineage.sideChat === true - ? MessagesSquareIcon - : GitForkIcon; - const relationship = - node?.thread?.lineage.sideChat === true && !isParent - ? "Side chat" - : relationshipLabel(edge, props.threadId); - const projectedAgent = isSubagent && !isParent ? subagentsByThreadId.get(threadId) : undefined; - const workflowGroup = - projectedAgent === undefined ? undefined : workflowGroupsById.get(projectedAgent.id); - const agent = workflowGroup ? projectedAgent : currentSubagent(projectedAgent, node?.thread); - const failed = rowStatus === "failed" || rowStatus === "error"; - const canStop = - agent && - (agent.origin === "app_owned" || - (agent.origin === "provider_native" && - agent.driver === "claudeAgent" && - agent.runId != null && - agent.nativeTaskRef?.strength === "strong" && - agent.nativeTaskRef.nativeId !== null && - ["pending", "running", "waiting"].includes(agent.nativeStatus))) && - agent.startedAt && - ["pending", "running", "waiting"].includes(agent.status); - const threadTitle = relationshipThreadTitle({ - title: - workflowGroup?.workflow.workflowName ?? node?.thread?.title ?? agent?.title ?? threadId, - isSubagent, - }); - const provider = providers?.find( - (entry) => - entry.instanceId === (agent?.providerInstanceId ?? node?.thread?.providerInstanceId), - ); - const providerDriver = agent?.driver ?? provider?.driver; - const providerEntry = providerEntries.find( - (entry) => - entry.instanceId === (agent?.providerInstanceId ?? node?.thread?.providerInstanceId), - ); - const canStopWorkflow = - canOperateWorkflow && - workflowGroup?.workflow.status === "running" && - agent?.origin === "provider_native" && - providerDriver === "claudeAgent"; - const trailingVisibilityClass = (workflowGroup ? canStopWorkflow : canStop) - ? "group-hover:opacity-0 group-focus-within:opacity-0 pointer-coarse:opacity-0 [@media(hover:none)]:opacity-0" - : ""; - const project = projects.find((project) => project.id === node?.thread?.projectId); - const relationshipHint = `Open ${relationship.toLowerCase()} in this chat`; - const RelationshipPopup = agent ? ThreadHoverCardPopup : TooltipPopup; - const relationshipTooltip = agent ? ( - } - status={agent.status} - result={agent.result} - progress={agent.progress} - parentThread={currentThread ?? undefined} - childThread={node?.thread ?? undefined} - parentProject={currentProject} - childProject={project} - /> - ) : ( - - ); - const relationshipContent = ( - <> - {providerDriver ? ( - - ) : ( - - )} - - {threadTitle} - - {relationship} - {node?.thread?.modelSelection.model - ? ` · ${formatModelSlugName(node.thread.modelSelection.model)}` - : ""} - - - - {isParent ? ( - "Parent" - ) : !isSubagent ? ( - edge.kind === "transfer" ? ( - relationship - ) : ( - "Fork" - ) - ) : failed ? ( - Failed - ) : ( - <> - {agent && deriveSubagentElapsedMs(agent, 0) !== null ? ( - - ) : ( - threadRelationshipStatusLabel(rowStatus) - )} - {rowStatus === "running" || rowStatus === "in_progress" ? ( - - ) : null} - - )} - - - ); - // A workflow pairs its menu row with a disclosure; other rows render directly. - const relationshipLink = ( - - group.id === "previous") - ?.rows.some((row) => row.threadId === threadId) ? ( - - ) : undefined - } - onClick={() => openThread(threadId)} - /> - } - > - {relationshipContent} - - {relationshipTooltip} - - ); - if (workflowGroup && agent) { - return ( - openThread(memberThreadId as ThreadId)} - onStop={canStopWorkflow ? () => void stopWorkflow(agent.id) : undefined} - stopping={stoppingId === agent.id} - stopDisabled={stoppingId !== null} - header={relationshipLink} - /> - ); - } - return ( -
  • - {isMergeTarget ? ( -
    - - openThread(threadId)} - /> - } - > - {relationshipContent} - - {relationshipTooltip} - - - void merge()} - > - {busyAction === "merge" ? ( - - ) : ( - - )} - - } - /> - - {latestMergeBackRun === null - ? "Complete a run in this fork before merging it back" - : parentTitle - ? `Merge this conversation back into ${parentTitle}` - : "Merge this conversation back into its source"} - - - - {threadRelationshipStatusLabel(rowStatus)} - -
    - ) : ( - relationshipLink - )} - {canStop && agent ? ( -
    - - void stopSubagent(threadId, agent)} - /> - } - > - {stoppingId === threadId ? ( - - ) : ( - - )} - - Stop subagent - -
    - ) : null} -
  • - ); - }; + const renderRelationshipRow = (row: ThreadRelationshipWalkRow, menu = false) => ( + + ); return ( <> @@ -1023,7 +1049,7 @@ export function ThreadRelationshipsPanel(props: { .filter((group) => group.id !== "previous") .map((group) => ( - {(visibleRows) => visibleRows.map(renderRelationshipRow)} + {(visibleRows) => visibleRows.map((row) => renderRelationshipRow(row))} ))} {currentThread ? ( @@ -1035,7 +1061,13 @@ export function ThreadRelationshipsPanel(props: { {currentThread.title} - This thread · {formatModelSlugName(currentThread.modelSelection.model)} + This thread ·{" "} + {lineageModelLabel( + currentThread.modelSelection.model, + providerEntries.find( + (entry) => entry.instanceId === currentThread.modelSelection.instanceId, + ), + )} @@ -1044,7 +1076,7 @@ export function ThreadRelationshipsPanel(props: { .filter((group) => group.id === "previous") .map((group) => ( - {(visibleRows) => visibleRows.map(renderRelationshipRow)} + {(visibleRows) => visibleRows.map((row) => renderRelationshipRow(row, true))} ))} {remoteSubagents.length > 0 ? ( @@ -1082,7 +1114,7 @@ export function ThreadRelationshipsPanel(props: { rows={sideChats} expanded > - {(visibleRows) => visibleRows.map(renderRelationshipRow)} + {(visibleRows) => visibleRows.map((row) => renderRelationshipRow(row))} ) : null} @@ -1116,12 +1148,12 @@ export function ThreadLineageHeaderControl(props: { }) { const nowMinute = useNowMinute(); const nowMs = Date.parse(`${nowMinute}:00.000Z`); + const lineage = useThreadLineage(props); const { graph, projects, providersByEnvironment, remoteSubagents, - subagentsByThreadId, relationshipRows, currentThread, groups, @@ -1133,7 +1165,7 @@ export function ThreadLineageHeaderControl(props: { parentTitle, openThread, merge, - } = useThreadLineage(props); + } = lineage; const parents = relationshipRows.filter(({ edge }) => isParentThreadRelationship(edge, props.threadId), ); @@ -1174,21 +1206,19 @@ export function ThreadLineageHeaderControl(props: { ] .filter((part) => part !== null) .join(" · ") || null; - const agentMeta = (threadId: ThreadId) => subagentsByThreadId.get(threadId)?.model ?? null; const currentTitle = graph.nodes.get(props.threadId)?.thread?.title ?? "This thread"; const renderRow = (row: ThreadRelationshipWalkRow, meta: string | null) => { const node = graph.nodes.get(row.threadId); const isSubagent = row.edge.kind === "subagent"; const isParent = isParentThreadRelationship(row.edge, props.threadId); - const agent = isSubagent && !isParent ? subagentsByThreadId.get(row.threadId) : undefined; const title = relationshipThreadTitle({ - title: node?.thread?.title ?? agent?.title ?? row.threadId, + title: node?.thread?.title ?? row.threadId, isSubagent, }); const status = isParent ? null : threadRelationshipRowStatus(graph, row); const badgeVariant = status ? RELATIONSHIP_BADGE_VARIANTS[status] : undefined; - const Icon = isParent ? MessagesSquareIcon : isSubagent ? BotIcon : GitForkIcon; + const Icon = isParent ? MessagesSquareIcon : GitForkIcon; return ( 0 ? ( Agents - {activeAgents.map((row) => renderRow(row, agentMeta(row.threadId)))} +
      + {activeAgents.map((row) => ( + + ))} +
    ) : null} {previousAgents.length > 0 ? ( Previous agents - {previousAgents.map((row) => renderRow(row, agentMeta(row.threadId)))} +
      + {previousAgents.map((row) => ( + + ))} +
    ) : null} {remoteSubagents.length > 0 ? ( From e048f8028e04c23be47657097ff896036f310453 Mon Sep 17 00:00:00 2001 From: flamboh Date: Mon, 2 Nov 2026 08:45:02 +0000 Subject: [PATCH 2/4] fix(web): Agents popover keeps its row shape, borrowing Lineage's provider icon and model name --- .../chat/ThreadRelationshipsControl.tsx | 673 +++++++++--------- 1 file changed, 335 insertions(+), 338 deletions(-) diff --git a/apps/web/src/components/chat/ThreadRelationshipsControl.tsx b/apps/web/src/components/chat/ThreadRelationshipsControl.tsx index ee8bbddb60fd2..22a5cb6451ed5 100644 --- a/apps/web/src/components/chat/ThreadRelationshipsControl.tsx +++ b/apps/web/src/components/chat/ThreadRelationshipsControl.tsx @@ -37,6 +37,7 @@ import { type OrchestrationV2RemoteTaskChild, type OrchestrationV2Subagent, type OrchestrationV2ThreadShell, + type ProviderDriverKind, type ServerProvider, type ThreadId, } from "@t3tools/contracts"; @@ -658,8 +659,6 @@ function useThreadLineage(props: { }; } -type ThreadLineage = ReturnType; - function lineageModelLabel(model: string, providerEntry: ProviderInstanceEntry | undefined) { const selectedModel = providerEntry?.models.find((entry) => entry.slug === model); return selectedModel @@ -667,301 +666,27 @@ function lineageModelLabel(model: string, providerEntry: ProviderInstanceEntry | : formatModelSlugName(model); } -function ThreadLineageRow(props: { - readonly lineage: ThreadLineage; - readonly currentThreadId: ThreadId; - readonly row: ThreadRelationshipWalkRow; - readonly menu: boolean; +function LineageProviderIcon(props: { + readonly driver: ProviderDriverKind; + readonly provider: ServerProvider | undefined; + readonly providerEntry: ProviderInstanceEntry | undefined; + readonly providerEntries: ReadonlyArray; }) { - const { - parentRef, - parentNativeStatus, - graph, - projects, - providers, - providerEntries, - subagentsByThreadId, - currentThread, - currentProject, - mergeTargetThreadId, - latestMergeBackRun, - canMerge, - busyAction, - parentTitle, - openThread, - merge, - stoppingId, - workflowGroupsById, - canOperateWorkflow, - stopWorkflow, - stopSubagent, - } = props.lineage; - const { threadId, edge } = props.row; - const node = graph.nodes.get(threadId); - const isSubagent = edge.kind === "subagent"; - const isMergeTarget = threadId === mergeTargetThreadId; - const isParent = isParentThreadRelationship(edge, props.currentThreadId); - const rowStatus = - (isParent && threadId === parentRef?.threadId - ? (node?.thread?.activityRunStatus ?? parentNativeStatus) - : undefined) ?? threadRelationshipRowStatus(graph, { threadId, edge }); - const RelationshipIcon = isParent - ? CornerLeftUpIcon - : isSubagent - ? BotIcon - : node?.thread?.lineage.sideChat === true - ? MessagesSquareIcon - : GitForkIcon; - const relationship = - node?.thread?.lineage.sideChat === true && !isParent - ? "Side chat" - : relationshipLabel(edge, props.currentThreadId); - const projectedAgent = isSubagent && !isParent ? subagentsByThreadId.get(threadId) : undefined; - const workflowGroup = - projectedAgent === undefined ? undefined : workflowGroupsById.get(projectedAgent.id); - const agent = workflowGroup ? projectedAgent : currentSubagent(projectedAgent, node?.thread); - const failed = rowStatus === "failed" || rowStatus === "error"; - const canStop = - !props.menu && - agent && - (agent.origin === "app_owned" || - (agent.origin === "provider_native" && - agent.driver === "claudeAgent" && - agent.runId != null && - agent.nativeTaskRef?.strength === "strong" && - agent.nativeTaskRef.nativeId !== null && - ["pending", "running", "waiting"].includes(agent.nativeStatus))) && - agent.startedAt && - ["pending", "running", "waiting"].includes(agent.status); - const threadTitle = relationshipThreadTitle({ - title: workflowGroup?.workflow.workflowName ?? node?.thread?.title ?? agent?.title ?? threadId, - isSubagent, - }); - const provider = providers?.find( - (entry) => entry.instanceId === (agent?.providerInstanceId ?? node?.thread?.providerInstanceId), - ); - const providerDriver = agent?.driver ?? provider?.driver; - const providerEntry = providerEntries.find( - (entry) => entry.instanceId === (agent?.providerInstanceId ?? node?.thread?.providerInstanceId), - ); - const canStopWorkflow = - !props.menu && - canOperateWorkflow && - workflowGroup?.workflow.status === "running" && - agent?.origin === "provider_native" && - providerDriver === "claudeAgent"; - const trailingVisibilityClass = (workflowGroup ? canStopWorkflow : canStop) - ? "group-hover:opacity-0 group-focus-within:opacity-0 pointer-coarse:opacity-0 [@media(hover:none)]:opacity-0" - : ""; - const project = projects.find((project) => project.id === node?.thread?.projectId); - const relationshipHint = `Open ${relationship.toLowerCase()} in this chat`; - const RelationshipPopup = agent ? ThreadHoverCardPopup : TooltipPopup; - const relationshipTooltip = agent ? ( - } - status={agent.status} - result={agent.result} - progress={agent.progress} - parentThread={currentThread ?? undefined} - childThread={node?.thread ?? undefined} - parentProject={currentProject} - childProject={project} - /> - ) : ( - - ); - const relationshipContent = ( - <> - {providerDriver ? ( - - ) : ( - - )} - - {threadTitle} - - {relationship} - {node?.thread?.modelSelection.model - ? ` · ${lineageModelLabel(node.thread.modelSelection.model, providerEntry)}` - : ""} - - - - {isParent ? ( - "Parent" - ) : !isSubagent ? ( - edge.kind === "transfer" ? ( - relationship - ) : ( - "Fork" - ) - ) : failed ? ( - Failed - ) : ( - <> - {agent && deriveSubagentElapsedMs(agent, 0) !== null ? ( - - ) : ( - threadRelationshipStatusLabel(rowStatus) - )} - {rowStatus === "running" || rowStatus === "in_progress" ? ( - - ) : null} - - )} - - - ); - // A workflow pairs its menu row with a disclosure; other rows render directly. - const relationshipLink = ( - - : undefined} - onClick={() => openThread(threadId)} - /> - } - > - {relationshipContent} - - {relationshipTooltip} - - ); - if (workflowGroup && agent) { - return ( - openThread(memberThreadId as ThreadId)} - onStop={canStopWorkflow ? () => void stopWorkflow(agent.id) : undefined} - stopping={stoppingId === agent.id} - stopDisabled={stoppingId !== null} - header={relationshipLink} - /> - ); - } + const { providerEntry } = props; return ( -
  • - {isMergeTarget ? ( -
    - - openThread(threadId)} - /> - } - > - {relationshipContent} - - {relationshipTooltip} - - - void merge()} - > - {busyAction === "merge" ? ( - - ) : ( - - )} - - } - /> - - {latestMergeBackRun === null - ? "Complete a run in this fork before merging it back" - : parentTitle - ? `Merge this conversation back into ${parentTitle}` - : "Merge this conversation back into its source"} - - - - {threadRelationshipStatusLabel(rowStatus)} - -
    - ) : ( - relationshipLink - )} - {canStop && agent ? ( -
    - - void stopSubagent(threadId, agent)} - /> - } - > - {stoppingId === threadId ? ( - - ) : ( - - )} - - Stop subagent - -
    - ) : null} -
  • + ); } @@ -971,20 +696,37 @@ export function ThreadRelationshipsPanel(props: { readonly showLineage?: boolean; readonly showSideChats?: boolean; }) { - const lineage = useThreadLineage(props); const { ref, + parentRef, + parentNativeStatus, + graph, + projects, + providers, providerEntries, providersByEnvironment, remoteSubagents, + subagentsByThreadId, currentThread, + currentProject, groups, sideChats, unlinkedRunningCount, + mergeTargetThreadId, + latestMergeBackRun, + canMerge, canDetach, busyAction, + parentTitle, + openThread, + merge, detach, - } = lineage; + stoppingId, + workflowGroupsById, + canOperateWorkflow, + stopWorkflow, + stopSubagent, + } = useThreadLineage(props); const delegatedFrom = currentThread?.delegatedFrom; @@ -996,15 +738,274 @@ export function ThreadRelationshipsPanel(props: { return null; } - const renderRelationshipRow = (row: ThreadRelationshipWalkRow, menu = false) => ( - - ); + const renderRelationshipRow = ({ threadId, edge }: ThreadRelationshipWalkRow) => { + const node = graph.nodes.get(threadId); + const isSubagent = edge.kind === "subagent"; + const isMergeTarget = threadId === mergeTargetThreadId; + const isParent = isParentThreadRelationship(edge, props.threadId); + const rowStatus = + (isParent && threadId === parentRef?.threadId + ? (node?.thread?.activityRunStatus ?? parentNativeStatus) + : undefined) ?? threadRelationshipRowStatus(graph, { threadId, edge }); + const RelationshipIcon = isParent + ? CornerLeftUpIcon + : isSubagent + ? BotIcon + : node?.thread?.lineage.sideChat === true + ? MessagesSquareIcon + : GitForkIcon; + const relationship = + node?.thread?.lineage.sideChat === true && !isParent + ? "Side chat" + : relationshipLabel(edge, props.threadId); + const projectedAgent = isSubagent && !isParent ? subagentsByThreadId.get(threadId) : undefined; + const workflowGroup = + projectedAgent === undefined ? undefined : workflowGroupsById.get(projectedAgent.id); + const agent = workflowGroup ? projectedAgent : currentSubagent(projectedAgent, node?.thread); + const failed = rowStatus === "failed" || rowStatus === "error"; + const canStop = + agent && + (agent.origin === "app_owned" || + (agent.origin === "provider_native" && + agent.driver === "claudeAgent" && + agent.runId != null && + agent.nativeTaskRef?.strength === "strong" && + agent.nativeTaskRef.nativeId !== null && + ["pending", "running", "waiting"].includes(agent.nativeStatus))) && + agent.startedAt && + ["pending", "running", "waiting"].includes(agent.status); + const threadTitle = relationshipThreadTitle({ + title: + workflowGroup?.workflow.workflowName ?? node?.thread?.title ?? agent?.title ?? threadId, + isSubagent, + }); + const provider = providers?.find( + (entry) => + entry.instanceId === (agent?.providerInstanceId ?? node?.thread?.providerInstanceId), + ); + const providerDriver = agent?.driver ?? provider?.driver; + const providerEntry = providerEntries.find( + (entry) => + entry.instanceId === (agent?.providerInstanceId ?? node?.thread?.providerInstanceId), + ); + const canStopWorkflow = + canOperateWorkflow && + workflowGroup?.workflow.status === "running" && + agent?.origin === "provider_native" && + providerDriver === "claudeAgent"; + const trailingVisibilityClass = (workflowGroup ? canStopWorkflow : canStop) + ? "group-hover:opacity-0 group-focus-within:opacity-0 pointer-coarse:opacity-0 [@media(hover:none)]:opacity-0" + : ""; + const project = projects.find((project) => project.id === node?.thread?.projectId); + const relationshipHint = `Open ${relationship.toLowerCase()} in this chat`; + const RelationshipPopup = agent ? ThreadHoverCardPopup : TooltipPopup; + const relationshipTooltip = agent ? ( + } + status={agent.status} + result={agent.result} + progress={agent.progress} + parentThread={currentThread ?? undefined} + childThread={node?.thread ?? undefined} + parentProject={currentProject} + childProject={project} + /> + ) : ( + + ); + const relationshipContent = ( + <> + {providerDriver ? ( + + ) : ( + + )} + + {threadTitle} + + {relationship} + {node?.thread?.modelSelection.model + ? ` · ${lineageModelLabel(node.thread.modelSelection.model, providerEntry)}` + : ""} + + + + {isParent ? ( + "Parent" + ) : !isSubagent ? ( + edge.kind === "transfer" ? ( + relationship + ) : ( + "Fork" + ) + ) : failed ? ( + Failed + ) : ( + <> + {agent && deriveSubagentElapsedMs(agent, 0) !== null ? ( + + ) : ( + threadRelationshipStatusLabel(rowStatus) + )} + {rowStatus === "running" || rowStatus === "in_progress" ? ( + + ) : null} + + )} + + + ); + // A workflow pairs its menu row with a disclosure; other rows render directly. + const relationshipLink = ( + + group.id === "previous") + ?.rows.some((row) => row.threadId === threadId) ? ( + + ) : undefined + } + onClick={() => openThread(threadId)} + /> + } + > + {relationshipContent} + + {relationshipTooltip} + + ); + if (workflowGroup && agent) { + return ( + openThread(memberThreadId as ThreadId)} + onStop={canStopWorkflow ? () => void stopWorkflow(agent.id) : undefined} + stopping={stoppingId === agent.id} + stopDisabled={stoppingId !== null} + header={relationshipLink} + /> + ); + } + return ( +
  • + {isMergeTarget ? ( +
    + + openThread(threadId)} + /> + } + > + {relationshipContent} + + {relationshipTooltip} + + + void merge()} + > + {busyAction === "merge" ? ( + + ) : ( + + )} + + } + /> + + {latestMergeBackRun === null + ? "Complete a run in this fork before merging it back" + : parentTitle + ? `Merge this conversation back into ${parentTitle}` + : "Merge this conversation back into its source"} + + + + {threadRelationshipStatusLabel(rowStatus)} + +
    + ) : ( + relationshipLink + )} + {canStop && agent ? ( +
    + + void stopSubagent(threadId, agent)} + /> + } + > + {stoppingId === threadId ? ( + + ) : ( + + )} + + Stop subagent + +
    + ) : null} +
  • + ); + }; return ( <> @@ -1049,7 +1050,7 @@ export function ThreadRelationshipsPanel(props: { .filter((group) => group.id !== "previous") .map((group) => ( - {(visibleRows) => visibleRows.map((row) => renderRelationshipRow(row))} + {(visibleRows) => visibleRows.map(renderRelationshipRow)} ))} {currentThread ? ( @@ -1076,7 +1077,7 @@ export function ThreadRelationshipsPanel(props: { .filter((group) => group.id === "previous") .map((group) => ( - {(visibleRows) => visibleRows.map((row) => renderRelationshipRow(row, true))} + {(visibleRows) => visibleRows.map(renderRelationshipRow)} ))} {remoteSubagents.length > 0 ? ( @@ -1114,7 +1115,7 @@ export function ThreadRelationshipsPanel(props: { rows={sideChats} expanded > - {(visibleRows) => visibleRows.map((row) => renderRelationshipRow(row))} + {(visibleRows) => visibleRows.map(renderRelationshipRow)} ) : null} @@ -1148,12 +1149,14 @@ export function ThreadLineageHeaderControl(props: { }) { const nowMinute = useNowMinute(); const nowMs = Date.parse(`${nowMinute}:00.000Z`); - const lineage = useThreadLineage(props); const { graph, projects, + providers, + providerEntries, providersByEnvironment, remoteSubagents, + subagentsByThreadId, relationshipRows, currentThread, groups, @@ -1165,7 +1168,7 @@ export function ThreadLineageHeaderControl(props: { parentTitle, openThread, merge, - } = lineage; + } = useThreadLineage(props); const parents = relationshipRows.filter(({ edge }) => isParentThreadRelationship(edge, props.threadId), ); @@ -1212,23 +1215,37 @@ export function ThreadLineageHeaderControl(props: { const node = graph.nodes.get(row.threadId); const isSubagent = row.edge.kind === "subagent"; const isParent = isParentThreadRelationship(row.edge, props.threadId); + const agent = isSubagent && !isParent ? subagentsByThreadId.get(row.threadId) : undefined; + const providerEntry = agent + ? providerEntries.find((entry) => entry.instanceId === agent.providerInstanceId) + : undefined; + const rowMeta = agent?.model ? lineageModelLabel(agent.model, providerEntry) : meta; const title = relationshipThreadTitle({ - title: node?.thread?.title ?? row.threadId, + title: node?.thread?.title ?? agent?.title ?? row.threadId, isSubagent, }); const status = isParent ? null : threadRelationshipRowStatus(graph, row); const badgeVariant = status ? RELATIONSHIP_BADGE_VARIANTS[status] : undefined; - const Icon = isParent ? MessagesSquareIcon : GitForkIcon; + const Icon = isParent ? MessagesSquareIcon : isSubagent ? BotIcon : GitForkIcon; return ( openThread(row.threadId)} > - + {agent ? ( + entry.instanceId === agent.providerInstanceId)} + providerEntry={providerEntry} + providerEntries={providerEntries} + /> + ) : ( + + )} {title} - {meta ? ( - {meta} + {rowMeta ? ( + {rowMeta} ) : null} {status && badgeVariant ? ( @@ -1296,33 +1313,13 @@ export function ThreadLineageHeaderControl(props: { {activeAgents.length > 0 ? ( Agents -
      - {activeAgents.map((row) => ( - - ))} -
    + {activeAgents.map((row) => renderRow(row, null))}
    ) : null} {previousAgents.length > 0 ? ( Previous agents -
      - {previousAgents.map((row) => ( - - ))} -
    + {previousAgents.map((row) => renderRow(row, null))}
    ) : null} {remoteSubagents.length > 0 ? ( From 15f0ebe75b1462a82099b21bb588c8173e504061 Mon Sep 17 00:00:00 2001 From: flamboh Date: Mon, 2 Nov 2026 08:55:11 +0000 Subject: [PATCH 3/4] fix(web): Agents popover shows the full model name and truncates the title first --- apps/web/src/components/chat/ThreadRelationshipsControl.tsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/apps/web/src/components/chat/ThreadRelationshipsControl.tsx b/apps/web/src/components/chat/ThreadRelationshipsControl.tsx index 22a5cb6451ed5..01aa15a2a8663 100644 --- a/apps/web/src/components/chat/ThreadRelationshipsControl.tsx +++ b/apps/web/src/components/chat/ThreadRelationshipsControl.tsx @@ -1245,7 +1245,11 @@ export function ThreadLineageHeaderControl(props: { )} {title} {rowMeta ? ( - {rowMeta} + + {rowMeta} + ) : null} {status && badgeVariant ? ( From fde77d1244beca7192cb469d8855f5a2c26a34de Mon Sep 17 00:00:00 2001 From: flamboh Date: Mon, 2 Nov 2026 20:51:26 +0000 Subject: [PATCH 4/4] fix(web): keep #29138 to Lineage model names; the Agents menu is going away Co-Authored-By: Claude Opus 5.5 --- .../chat/ThreadRelationshipsControl.tsx | 72 +++++-------------- 1 file changed, 18 insertions(+), 54 deletions(-) diff --git a/apps/web/src/components/chat/ThreadRelationshipsControl.tsx b/apps/web/src/components/chat/ThreadRelationshipsControl.tsx index 01aa15a2a8663..cca8496ce2143 100644 --- a/apps/web/src/components/chat/ThreadRelationshipsControl.tsx +++ b/apps/web/src/components/chat/ThreadRelationshipsControl.tsx @@ -37,7 +37,6 @@ import { type OrchestrationV2RemoteTaskChild, type OrchestrationV2Subagent, type OrchestrationV2ThreadShell, - type ProviderDriverKind, type ServerProvider, type ThreadId, } from "@t3tools/contracts"; @@ -666,30 +665,6 @@ function lineageModelLabel(model: string, providerEntry: ProviderInstanceEntry | : formatModelSlugName(model); } -function LineageProviderIcon(props: { - readonly driver: ProviderDriverKind; - readonly provider: ServerProvider | undefined; - readonly providerEntry: ProviderInstanceEntry | undefined; - readonly providerEntries: ReadonlyArray; -}) { - const { providerEntry } = props; - return ( - - ); -} - export function ThreadRelationshipsPanel(props: { readonly environmentId: EnvironmentId; readonly threadId: ThreadId; @@ -827,11 +802,18 @@ export function ThreadRelationshipsPanel(props: { const relationshipContent = ( <> {providerDriver ? ( - ) : ( @@ -1152,8 +1134,6 @@ export function ThreadLineageHeaderControl(props: { const { graph, projects, - providers, - providerEntries, providersByEnvironment, remoteSubagents, subagentsByThreadId, @@ -1209,6 +1189,7 @@ export function ThreadLineageHeaderControl(props: { ] .filter((part) => part !== null) .join(" · ") || null; + const agentMeta = (threadId: ThreadId) => subagentsByThreadId.get(threadId)?.model ?? null; const currentTitle = graph.nodes.get(props.threadId)?.thread?.title ?? "This thread"; const renderRow = (row: ThreadRelationshipWalkRow, meta: string | null) => { @@ -1216,10 +1197,6 @@ export function ThreadLineageHeaderControl(props: { const isSubagent = row.edge.kind === "subagent"; const isParent = isParentThreadRelationship(row.edge, props.threadId); const agent = isSubagent && !isParent ? subagentsByThreadId.get(row.threadId) : undefined; - const providerEntry = agent - ? providerEntries.find((entry) => entry.instanceId === agent.providerInstanceId) - : undefined; - const rowMeta = agent?.model ? lineageModelLabel(agent.model, providerEntry) : meta; const title = relationshipThreadTitle({ title: node?.thread?.title ?? agent?.title ?? row.threadId, isSubagent, @@ -1233,23 +1210,10 @@ export function ThreadLineageHeaderControl(props: { disabled={node?.missing === true} onClick={() => openThread(row.threadId)} > - {agent ? ( - entry.instanceId === agent.providerInstanceId)} - providerEntry={providerEntry} - providerEntries={providerEntries} - /> - ) : ( - - )} + {title} - {rowMeta ? ( - - {rowMeta} - + {meta ? ( + {meta} ) : null} {status && badgeVariant ? ( @@ -1317,13 +1281,13 @@ export function ThreadLineageHeaderControl(props: { {activeAgents.length > 0 ? ( Agents - {activeAgents.map((row) => renderRow(row, null))} + {activeAgents.map((row) => renderRow(row, agentMeta(row.threadId)))} ) : null} {previousAgents.length > 0 ? ( Previous agents - {previousAgents.map((row) => renderRow(row, null))} + {previousAgents.map((row) => renderRow(row, agentMeta(row.threadId)))} ) : null} {remoteSubagents.length > 0 ? (