Show automation source context in details

Co-authored-by: Orca <help@stably.ai>
This commit is contained in:
Jinwoo-H
2026-06-13 01:16:14 -07:00
co-authored by Orca
parent 19a9d8e91c
commit 1bc715dcb5
4 changed files with 138 additions and 2 deletions
@@ -14,6 +14,7 @@ import {
summarizeAutomationRunUsage
} from './automation-usage-model'
import type { AutomationTargetAvailability } from './automation-target-availability'
import { getAutomationSourceDisplay } from './automation-source-display'
import { translate } from '@/i18n/i18n'
type AutomationDetailProps = {
@@ -30,11 +31,21 @@ type AutomationDetailProps = {
onDelete: (automation: Automation) => void
}
function DetailMetric({ label, value }: { label: string; value: string }): React.JSX.Element {
function DetailMetric({
label,
value,
title
}: {
label: string
value: string
title?: string
}): React.JSX.Element {
return (
<div className="min-w-0">
<div className="text-[11px] font-medium uppercase text-muted-foreground">{label}</div>
<div className="mt-1 break-words text-sm font-medium">{value}</div>
<div className="mt-1 break-words text-sm font-medium" title={title}>
{value}
</div>
</div>
)
}
@@ -118,6 +129,7 @@ export function AutomationDetail({
automation.workspaceMode === 'new_per_run'
? (automation.baseBranch ?? projectDefaultBaseRef ?? 'Project default')
: workspaceName
const sourceDisplay = getAutomationSourceDisplay(automation.sourceContext)
const runNowDisabled = runNowAvailability?.canRunNow === false
return (
@@ -235,6 +247,13 @@ export function AutomationDetail({
label={translate('auto.components.automations.AutomationDetail.15ea446b93', 'Session')}
value={automation.reuseSession ? 'Reuse live session' : 'Fresh each run'}
/>
{sourceDisplay ? (
<DetailMetric
label={translate('auto.components.automations.AutomationDetail.29baf8f4c2', 'Source')}
value={sourceDisplay.label}
title={sourceDisplay.title}
/>
) : null}
<DetailMetric
label={translate('auto.components.automations.AutomationDetail.620b22145e', 'Grace')}
value={formatGrace(automation.missedRunGraceMinutes)}
@@ -0,0 +1,54 @@
import { describe, expect, it } from 'vitest'
import type { TaskSourceContext } from '../../../../shared/task-source-context'
import { getAutomationSourceDisplay } from './automation-source-display'
describe('automation source display', () => {
it('summarizes repo-backed source context separately from run location', () => {
const sourceContext: TaskSourceContext = {
kind: 'task-source',
provider: 'github',
hostId: 'ssh:devbox',
projectId: 'github:stablyai/orca',
projectHostSetupId: 'setup-devbox',
repoId: 'repo-devbox',
accountLabel: 'dev@example.com',
providerIdentity: {
provider: 'github',
owner: 'stablyai',
repo: 'orca'
}
}
expect(getAutomationSourceDisplay(sourceContext)).toEqual({
label: 'GitHub · devbox · stablyai/orca',
title: 'GitHub source · Host: devbox · Account: dev@example.com · Source: stablyai/orca'
})
})
it('uses account identity for Linear sources', () => {
const sourceContext: TaskSourceContext = {
kind: 'task-source',
provider: 'linear',
hostId: 'local',
projectId: 'repo-1',
projectHostSetupId: 'setup-local',
repoId: 'repo-1',
accountLabel: 'Linear API key',
providerIdentity: {
provider: 'linear',
workspaceId: 'legacy',
workspaceName: 'Saved Linear workspace'
}
}
expect(getAutomationSourceDisplay(sourceContext)).toEqual({
label: 'Linear · Local Mac · Saved Linear workspace',
title:
'Linear source · Host: Local Mac · Account: Linear API key · Source: Saved Linear workspace'
})
})
it('returns null when no source context is saved', () => {
expect(getAutomationSourceDisplay(null)).toBeNull()
})
})
@@ -0,0 +1,62 @@
import { getExecutionHostLabel } from '../../../../shared/execution-host'
import type { TaskSourceContext } from '../../../../shared/task-source-context'
export type AutomationSourceDisplay = {
label: string
title: string
}
export function getAutomationSourceDisplay(
sourceContext: TaskSourceContext | null | undefined
): AutomationSourceDisplay | null {
if (!sourceContext) {
return null
}
const providerLabel = getProviderLabel(sourceContext.provider)
const hostLabel = getExecutionHostLabel(sourceContext.hostId)
const identityLabel = getSourceIdentityLabel(sourceContext)
const label = [providerLabel, hostLabel, identityLabel]
.filter((part): part is string => Boolean(part))
.join(' · ')
const title = [
`${providerLabel} source`,
`Host: ${hostLabel}`,
sourceContext.accountLabel ? `Account: ${sourceContext.accountLabel}` : null,
identityLabel ? `Source: ${identityLabel}` : null
]
.filter((part): part is string => Boolean(part))
.join(' · ')
return { label, title }
}
function getProviderLabel(provider: TaskSourceContext['provider']): string {
switch (provider) {
case 'github':
return 'GitHub'
case 'gitlab':
return 'GitLab'
case 'linear':
return 'Linear'
case 'jira':
return 'Jira'
}
}
function getSourceIdentityLabel(sourceContext: TaskSourceContext): string | null {
const identity = sourceContext.providerIdentity
if (identity) {
switch (identity.provider) {
case 'github':
return `${identity.owner}/${identity.repo}`
case 'gitlab':
return identity.namespace && identity.project
? `${identity.namespace}/${identity.project}`
: (identity.projectId ?? null)
case 'linear':
return identity.workspaceName ?? identity.workspaceId ?? null
case 'jira':
return identity.siteUrl ?? identity.siteId ?? null
}
}
return sourceContext.accountLabel ?? sourceContext.repoId ?? null
}