feat(sidebar): show reference origin as an icon and record native chat origins (#33049)

Move the "Seen in" chip onto the review's meta line as a terminal icon with
a tooltip. Native chat tabs now record PR links their tools print, so the
icon appears for PRs created from chat too.
This commit is contained in:
Neil
2026-10-23 22:42:57 -07:00
committed by GitHub
parent a75cdc8287
commit 9ed7d9fdb7
10 changed files with 284 additions and 85 deletions
@@ -0,0 +1,44 @@
import { useAppStore } from '@/store'
import { sameStructuredReadTarget } from '@/attention/agent-subject-read-actions'
import { structuredAgentSessionTargetForTab } from '@/runtime/structured-agent-session-owner'
import type { AgentSessionSubscribeEvent } from '../../../../shared/agent-session-wire'
import type { RuntimeClientTarget } from '../../../../shared/runtime-client-target'
import { createTerminalGitHubPRLinkDetector } from '../../../../shared/terminal-github-pr-link-detector'
import { getStructuredAgentSessionTabs } from './structured-agent-session-tabs'
/** Records PRs a chat's tools print (e.g. `gh pr create`) on its workspace, like terminal output. */
export function createStructuredAgentSessionPRLinkObserver(
sessionId: string,
target: RuntimeClientTarget
): (event: AgentSessionSubscribeEvent) => void {
const detect = createTerminalGitHubPRLinkDetector()
return (event) => {
// Why: only live batches; replayed history would re-link PRs the user may have unlinked.
if (event.type !== 'batch') {
return
}
const links = event.batch.items.flatMap((item) =>
item.body.kind === 'tool-call' && item.body.output?.head
? detect(`${item.body.output.head}\n`)
: []
)
if (!links.length) {
return
}
const state = useAppStore.getState()
const tab = getStructuredAgentSessionTabs(state.unifiedTabsByWorktree).find((candidate) => {
const tabTarget =
candidate.entityId === sessionId && structuredAgentSessionTargetForTab(state, candidate)
return Boolean(tabTarget && sameStructuredReadTarget(tabTarget, target))
})
if (!tab) {
return
}
for (const link of links) {
state.observeTerminalGitHubPullRequestLink(tab.worktreeId, link, {
kind: 'agent-session',
tabId: tab.id
})
}
}
}
@@ -19,6 +19,7 @@ import {
readStructuredAgentSessionOlderPage
} from './structured-agent-session-older-page-read'
import { startStructuredAgentSessionReadTransport } from './structured-agent-session-read-transport'
import { createStructuredAgentSessionPRLinkObserver } from './structured-agent-session-pr-link-observer'
import type {
StructuredAgentSessionReadSnapshot,
@@ -61,6 +62,7 @@ function createReadOwner(
let captureActiveHistoryReadGuard = (): (() => boolean) => retiredHistoryRead
const activations = new Set<symbol>()
const listeners = new Set<() => void>()
const observePRLinks = createStructuredAgentSessionPRLinkObserver(sessionId, target)
const emit = (): void => {
for (const listener of listeners) {
@@ -196,7 +198,10 @@ function createReadOwner(
}
const transport = startStructuredAgentSessionReadTransport({
applyRequests: apply,
applyEvent: (event, options) => apply({ type: 'event', event, ...options }),
applyEvent: (event, options) => {
apply({ type: 'event', event, ...options })
observePRLinks(event)
},
applyError: (message, refusal) => {
// Once loaded, a failure with no host refusal attached is treated as lost contact: the
// transport retries it and the chat stays as it is. A refusal is the host's answer: shown.
@@ -1,6 +1,8 @@
import React from 'react'
import { Terminal } from 'lucide-react'
import { useAppStore } from '@/store'
import { Button } from '@/components/ui/button'
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'
import { translate } from '@/i18n/i18n'
import { resolveNotificationTabOwner } from '@/attention/notification-subject-owner'
import { terminalLayoutContainsLeaf } from '../../../../shared/workspace-session-pane-ownership'
@@ -24,12 +26,32 @@ function useReferenceTabs(workspace: Worktree): Tab[] {
})
}
function focusOrigin(workspace: Worktree, tab: Tab, paneKey: string | undefined): void {
const state = useAppStore.getState()
if (!state.setActiveWorktree(workspace.id, workspace.hostId)) {
return
}
state.activateTab(tab.id, { worktreeId: workspace.id })
const pane = paneKey ? parsePaneKey(paneKey) : null
if (
tab.contentType === 'terminal' &&
pane &&
pane.tabId === tab.entityId &&
state.tabsByWorktree[workspace.id]?.some((entry) => entry.id === tab.entityId) &&
terminalLayoutContainsLeaf(state.terminalLayoutsByTabId[tab.entityId]?.root, pane.leafId)
) {
activateTabAndFocusPane(tab.entityId, pane.leafId)
}
}
export function WorkspaceAttachmentOrigins({
item,
workspace
workspace,
compact = false
}: {
item: WorkspaceAttachment
workspace: Worktree
compact?: boolean
}): React.JSX.Element | null {
const tabs = useReferenceTabs(workspace)
const origins = item.origins?.filter((origin) => origin.kind === 'observed') ?? []
@@ -37,7 +59,9 @@ export function WorkspaceAttachmentOrigins({
return null
}
return (
<div className="flex flex-wrap items-center gap-1 pl-5">
<div
className={compact ? 'flex shrink-0 items-center' : 'flex flex-wrap items-center gap-1 pl-5'}
>
{origins.map((origin) => {
const tab = tabs.find(
(candidate) =>
@@ -51,6 +75,35 @@ export function WorkspaceAttachmentOrigins({
origin.agent ||
translate('workspace.links.terminal', 'Terminal')
const description = translate('workspace.links.seenIn', 'Seen in {{label}}', { label })
if (compact) {
const unavailable = translate('workspace.links.unavailableTab', 'Tab unavailable')
return (
<Tooltip key={getWorkspaceAttachmentOriginKey(origin)}>
<TooltipTrigger asChild>
<Button
type="button"
variant="ghost"
size="icon-xs"
className="size-4"
aria-disabled={!tab || undefined}
aria-label={tab ? description : `${description} · ${unavailable}`}
onKeyDown={(event) => event.stopPropagation()}
onClick={(event) => {
event.stopPropagation()
if (tab) {
focusOrigin(workspace, tab, origin.paneKey)
}
}}
>
<Terminal className="size-3 text-muted-foreground" />
</Button>
</TooltipTrigger>
<TooltipContent side="top" sideOffset={4}>
{tab ? description : `${description} · ${unavailable}`}
</TooltipContent>
</Tooltip>
)
}
return tab ? (
<Button
key={getWorkspaceAttachmentOriginKey(origin)}
@@ -65,24 +118,7 @@ export function WorkspaceAttachmentOrigins({
}}
onClick={(event) => {
event.stopPropagation()
const state = useAppStore.getState()
if (!state.setActiveWorktree(workspace.id, workspace.hostId)) {
return
}
state.activateTab(tab.id, { worktreeId: workspace.id })
const pane = origin.paneKey ? parsePaneKey(origin.paneKey) : null
if (
tab.contentType === 'terminal' &&
pane &&
pane.tabId === tab.entityId &&
state.tabsByWorktree[workspace.id]?.some((entry) => entry.id === tab.entityId) &&
terminalLayoutContainsLeaf(
state.terminalLayoutsByTabId[tab.entityId]?.root,
pane.leafId
)
) {
activateTabAndFocusPane(tab.entityId, pane.leafId)
}
focusOrigin(workspace, tab, origin.paneKey)
}}
>
{description}
@@ -113,6 +113,11 @@ export function WorktreeCardLinkedItemsSection({
url={url}
onOpen={(url, event) => open({ ...item, url }, event)}
onManage={onManage ? (event) => open(item, event) : undefined}
origins={
item.origins?.length ? (
<WorkspaceAttachmentOrigins item={item} workspace={workspace} compact />
) : undefined
}
actions={
item.type !== 'issue' ? renderReviewActions?.(item, live) : undefined
}
@@ -139,9 +144,6 @@ export function WorktreeCardLinkedItemsSection({
) : null}
</div>
) : null}
{item.origins?.length ? (
<WorkspaceAttachmentOrigins item={item} workspace={workspace} />
) : null}
</div>
)
})}
@@ -31,6 +31,7 @@ export function WorktreeCardReferenceRow({
url,
onOpen,
onManage,
origins,
actions
}: {
review: WorktreeCardPrDisplay | null
@@ -43,6 +44,7 @@ export function WorktreeCardReferenceRow({
url?: string
onOpen: (url: string, event: MouseEvent) => void
onManage?: (event: MouseEvent) => void
origins?: ReactNode
actions?: ReactNode
}): React.JSX.Element {
return (
@@ -127,6 +129,7 @@ export function WorktreeCardReferenceRow({
{provider === 'GitHub' ? null : `· ${provider}`}
{source ? ` · ${source}` : ''}
</span>
{origins}
{actions}
</div>
</div>
@@ -3,7 +3,10 @@ import type { AppState } from '@/store/types'
import { makeTerminalTab } from '../store/slices/worktrees-slice-test-fixtures'
import { singlePaneLayoutSnapshot } from '../store/slices/terminal-helpers'
import { makePaneKey } from '../../../shared/stable-pane-id'
import { getWorkspaceAttachmentTerminalOrigin } from './workspace-attachment-terminal-origin'
import {
getWorkspaceAttachmentObservedOrigin,
getWorkspaceAttachmentTerminalOrigin
} from './workspace-attachment-observed-origin'
const workspaceId = 'repo::/feature'
const leaf = '11111111-1111-4111-8111-111111111111'
@@ -91,3 +94,43 @@ describe('reference evidence terminal identity', () => {
).toBeUndefined()
})
})
describe('reference evidence native chat identity', () => {
it('records the chat tab and its session without a terminal pane', () => {
const chat = {
...state(),
unifiedTabsByWorktree: {
[workspaceId]: [
{
id: 'chat-tab',
entityId: 'chat-session',
groupId: 'group',
worktreeId: workspaceId,
contentType: 'agent-session' as const,
agentSessionAgent: 'claude' as const,
label: 'Plan refactor',
customLabel: null,
color: null,
sortOrder: 0,
createdAt: 0
}
]
}
}
expect(
getWorkspaceAttachmentObservedOrigin(
chat,
workspaceId,
{ kind: 'agent-session', tabId: 'chat-tab' },
'local'
)
).toEqual({
kind: 'observed',
tabId: 'chat-tab',
hostId: 'local',
label: 'Plan refactor',
agent: 'claude',
sessionId: 'chat-session'
})
})
})
@@ -0,0 +1,112 @@
import type { AppState } from '@/store/types'
import type { ExecutionHostId } from '../../../shared/execution-host'
import type { NotificationWorkspaceOwner } from '../../../shared/notification-source'
import {
resolveNotificationTabOwner,
resolveTerminalNotificationOwner
} from '@/attention/notification-subject-owner'
import type { WorkspaceAttachmentOrigin } from '../../../shared/worktree/types'
import { parsePaneKey } from '../../../shared/stable-pane-id'
import { terminalLayoutContainsLeaf } from '../../../shared/workspace-session-pane-ownership'
import type { Tab } from '../../../shared/tab-types'
export type WorkspaceReferenceTerminalContext = {
kind?: 'terminal'
tabId: string
paneKey: string
ptyId?: string | null
executionHostId?: ExecutionHostId
}
export type WorkspaceReferenceAgentSessionContext = { kind: 'agent-session'; tabId: string }
export type WorkspaceReferenceObservationContext =
| WorkspaceReferenceTerminalContext
| WorkspaceReferenceAgentSessionContext
type ObservationState = Parameters<typeof resolveTerminalNotificationOwner>[0] &
Pick<AppState, 'tabsByWorktree' | 'agentStatusByPaneKey' | 'terminalLayoutsByTabId'>
function findAgentSessionTab(
state: Partial<Pick<AppState, 'unifiedTabsByWorktree'>>,
worktreeId: string,
tabId: string
): Tab | undefined {
return state.unifiedTabsByWorktree?.[worktreeId]?.find(
(tab) => tab.id === tabId && tab.contentType === 'agent-session'
)
}
export function resolveWorkspaceReferenceObservationOwner(
state: ObservationState,
worktreeId: string,
context: WorkspaceReferenceObservationContext
): NotificationWorkspaceOwner | null {
if (context.kind === 'agent-session') {
const tab = findAgentSessionTab(state, worktreeId, context.tabId)
return tab ? resolveNotificationTabOwner(state, tab) : null
}
return resolveTerminalNotificationOwner(state, worktreeId, context)
}
export function getWorkspaceAttachmentObservedOrigin(
state: ObservationState,
worktreeId: string,
context: WorkspaceReferenceObservationContext,
hostId: ExecutionHostId | undefined
): WorkspaceAttachmentOrigin | undefined {
if (context.kind !== 'agent-session') {
return getWorkspaceAttachmentTerminalOrigin(state, worktreeId, context, hostId)
}
const tab = findAgentSessionTab(state, worktreeId, context.tabId)
if (!tab) {
return undefined
}
return {
kind: 'observed',
tabId: tab.id,
...(hostId ? { hostId } : {}),
label: tab.customLabel || tab.label,
...(tab.agentSessionAgent ? { agent: tab.agentSessionAgent } : {}),
sessionId: tab.entityId
}
}
export function getWorkspaceAttachmentTerminalOrigin(
state: Pick<AppState, 'tabsByWorktree' | 'agentStatusByPaneKey' | 'terminalLayoutsByTabId'>,
worktreeId: string,
context: WorkspaceReferenceTerminalContext,
hostId: ExecutionHostId | undefined
): WorkspaceAttachmentOrigin | undefined {
const tab = state.tabsByWorktree?.[worktreeId]?.find(
(candidate) => candidate.id === context.tabId
)
const pane = parsePaneKey(context.paneKey)
const layout = state.terminalLayoutsByTabId?.[context.tabId]
if (!tab || !pane || pane.tabId !== tab.id || !layout) {
return undefined
}
if (!terminalLayoutContainsLeaf(layout.root, pane.leafId)) {
return undefined
}
const ptyId =
layout.ptyIdsByLeafId?.[pane.leafId] ?? (layout.root?.type === 'leaf' ? tab.ptyId : null)
if (!context.ptyId || context.ptyId !== ptyId) {
return undefined
}
const entry = state.agentStatusByPaneKey?.[context.paneKey]
const matchingEntry =
(entry?.worktreeId && entry.worktreeId !== worktreeId) ||
(entry?.terminalHandle && entry.terminalHandle !== ptyId)
? undefined
: entry
return {
kind: 'observed',
tabId: tab.id,
paneKey: context.paneKey,
...(hostId ? { hostId } : {}),
label: tab.customTitle || tab.defaultTitle || tab.title,
...(matchingEntry?.agentType ? { agent: matchingEntry.agentType } : {}),
...(matchingEntry?.providerSession?.id ? { sessionId: matchingEntry.providerSession.id } : {})
}
}
@@ -1,51 +0,0 @@
import type { AppState } from '@/store/types'
import type { ExecutionHostId } from '../../../shared/execution-host'
import type { WorkspaceAttachmentOrigin } from '../../../shared/worktree/types'
import { parsePaneKey } from '../../../shared/stable-pane-id'
import { terminalLayoutContainsLeaf } from '../../../shared/workspace-session-pane-ownership'
export type WorkspaceReferenceTerminalContext = {
tabId: string
paneKey: string
ptyId?: string | null
executionHostId?: ExecutionHostId
}
export function getWorkspaceAttachmentTerminalOrigin(
state: Pick<AppState, 'tabsByWorktree' | 'agentStatusByPaneKey' | 'terminalLayoutsByTabId'>,
worktreeId: string,
context: WorkspaceReferenceTerminalContext,
hostId: ExecutionHostId | undefined
): WorkspaceAttachmentOrigin | undefined {
const tab = state.tabsByWorktree?.[worktreeId]?.find(
(candidate) => candidate.id === context.tabId
)
const pane = parsePaneKey(context.paneKey)
const layout = state.terminalLayoutsByTabId?.[context.tabId]
if (!tab || !pane || pane.tabId !== tab.id || !layout) {
return undefined
}
if (!terminalLayoutContainsLeaf(layout.root, pane.leafId)) {
return undefined
}
const ptyId =
layout.ptyIdsByLeafId?.[pane.leafId] ?? (layout.root?.type === 'leaf' ? tab.ptyId : null)
if (!context.ptyId || context.ptyId !== ptyId) {
return undefined
}
const entry = state.agentStatusByPaneKey?.[context.paneKey]
const matchingEntry =
(entry?.worktreeId && entry.worktreeId !== worktreeId) ||
(entry?.terminalHandle && entry.terminalHandle !== ptyId)
? undefined
: entry
return {
kind: 'observed',
tabId: tab.id,
paneKey: context.paneKey,
...(hostId ? { hostId } : {}),
label: tab.customTitle || tab.defaultTitle || tab.title,
...(matchingEntry?.agentType ? { agent: matchingEntry.agentType } : {}),
...(matchingEntry?.providerSession?.id ? { sessionId: matchingEntry.providerSession.id } : {})
}
}
@@ -1,5 +1,5 @@
import type { WorkspaceAttachmentMutation } from '../../../../shared/workspace-attachment-mutation'
import type { WorkspaceReferenceTerminalContext } from '@/lib/workspace-attachment-terminal-origin'
import type { WorkspaceReferenceObservationContext } from '@/lib/workspace-attachment-observed-origin'
import type { CreateWorktreeCallOptions } from './worktrees/create/worktree-create-payload'
import type { WorktreeCatalogVersion } from '../../../../shared/worktree/catalog-version'
import type { WorkspaceKey } from '../../../../shared/folder-workspace-types'
@@ -288,7 +288,7 @@ export type WorktreeSlice = {
observeTerminalGitHubPullRequestLink: (
worktreeId: string,
link: TerminalGitHubPRLink,
context?: WorkspaceReferenceTerminalContext
context?: WorkspaceReferenceObservationContext
) => void
bumpWorktreeActivity: (worktreeId: string) => void
/**
@@ -8,8 +8,10 @@ import { getRuntimeEnvironmentRevision } from '@/runtime/runtime-environment-rev
import type { WorktreeSlice } from '../../worktree-helpers'
import type { WorktreeSliceGet } from '../listing/worktree-slice-types'
import { branchName } from '@/lib/git-utils'
import { resolveTerminalNotificationOwner } from '@/attention/notification-subject-owner'
import { getWorkspaceAttachmentTerminalOrigin } from '@/lib/workspace-attachment-terminal-origin'
import {
getWorkspaceAttachmentObservedOrigin,
resolveWorkspaceReferenceObservationOwner
} from '@/lib/workspace-attachment-observed-origin'
import { getWorkspaceAttachments } from '../../../../../../shared/workspace-attachments'
import { getWorkspaceAttachmentKey } from '../../../../../../shared/workspace-attachment-normalization'
import {
@@ -46,9 +48,12 @@ export function createObserveTerminalGitHubPullRequestLink(
const confirmations = new PrioritySemaphore(3)
return (worktreeId, link, context) => {
const state = get()
const owner = context ? resolveTerminalNotificationOwner(state, worktreeId, context) : null
const owner = context
? resolveWorkspaceReferenceObservationOwner(state, worktreeId, context)
: null
const hostId = owner?.executionHostId ?? undefined
if (context && (!hostId || (context.executionHostId && context.executionHostId !== hostId))) {
const contextHostId = context?.kind === 'agent-session' ? undefined : context?.executionHostId
if (context && (!hostId || (contextHostId && contextHostId !== hostId))) {
return
}
const worktree = findKnownWorktreeById(state, worktreeId, hostId)
@@ -75,7 +80,7 @@ export function createObserveTerminalGitHubPullRequestLink(
const pushBranch = worktree.pushTarget?.branchName
let confirmedPushBranch: string | undefined
const origin = context
? getWorkspaceAttachmentTerminalOrigin(state, worktreeId, context, hostId)
? getWorkspaceAttachmentObservedOrigin(state, worktreeId, context, hostId)
: undefined
if (context && !origin) {
return
@@ -85,8 +90,8 @@ export function createObserveTerminalGitHubPullRequestLink(
return true
}
const latest = get()
const latestOwner = resolveTerminalNotificationOwner(latest, worktreeId, context)
const latestOrigin = getWorkspaceAttachmentTerminalOrigin(latest, worktreeId, context, hostId)
const latestOwner = resolveWorkspaceReferenceObservationOwner(latest, worktreeId, context)
const latestOrigin = getWorkspaceAttachmentObservedOrigin(latest, worktreeId, context, hostId)
return (
latestOwner?.executionHostId === hostId &&
Boolean(