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
@@ -219,6 +219,7 @@ Done means: reviewers and future agents can understand the vision, implementatio
- [x] 2026-06-12: Tightened the project-first design doc so host-aware Add Project is the current path for browse/import, clone, and create, while bulk setup and cloud VM provisioning are future host-onboarding work. Re-verified shared host-model tests covering local/SSH/runtime host kinds, runtime capability gating, SSH reconnect/disconnect actions, host settings overrides, and task source host scopes.
- [x] 2026-06-12: Tightened runtime project-host setup availability so remote servers must advertise both `project-host-setup.v1` and `workspace-run-context.v1` before users can choose them for project setup/workspace creation; verified with focused setup option tests, targeted oxlint, `git diff --check`, and full `pnpm run typecheck`.
- [x] 2026-06-13: Verified Linear Tasks in the live Electron app with the connected legacy Linear API-key workspace: the visible Linear list rendered 36 issues, source-scoped cache keys included both the legacy local account key and an explicit `linear:local:...` task-source cache key, and screenshot evidence was added to the dark verification report. Jira remains unverified because this profile has no connected Jira account.
- [x] 2026-06-13: Added Automations detail source-context display so saved automations show their provider source/account/host separately from run location; verified with focused source-display tests, targeted oxlint, `git diff --check`, and full `pnpm run typecheck`.
- [x] 2026-06-12: Live-verified Add Project disconnected-host UX in Electron by simulating the SSH target as disconnected in renderer state: the Host picker defaulted to Local Mac and showed the SSH host as a disabled muted row; restored SSH to connected and added screenshot evidence to `notes/multihost-remote-verification-report.html`.
- [x] 2026-06-12: Audited Settings ownership for Source Control AI defaults, repository Source Control AI overrides, agent launch defaults, quick commands, workspace directories, and provider accounts; added a tested settings-ownership helper and surfaced the ownership language in Settings while preserving the existing Windows/WSL account and agent-location pattern. Verified with focused settings tests and web typecheck.
- [x] 2026-06-12: Improved remote-server Settings version-skew diagnostics by showing the exact compatibility explanation plus named runtime capabilities instead of only a capability count; verified with `RuntimeEnvironmentsPane` tests and web typecheck.
@@ -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
}