Fix default tab command setup policy

Co-authored-by: Orca <help@stably.ai>
This commit is contained in:
Jinwoo-H
2026-05-31 04:46:21 -07:00
committed by Neil
co-authored by Orca
parent b4128ba998
commit 429f5e5d1b
5 changed files with 142 additions and 13 deletions
+17 -1
View File
@@ -882,7 +882,10 @@ describe('shouldRunSetupForCreate', () => {
})
describe('getDefaultTabsLaunch', () => {
const makeRepo = (setupRunPolicy?: 'ask' | 'run-by-default' | 'skip-by-default') =>
const makeRepo = (
setupRunPolicy?: 'ask' | 'run-by-default' | 'skip-by-default',
commandSourcePolicy?: 'local-only' | 'run-both' | 'shared-only'
) =>
({
id: 'test-id',
path: '/test/repo',
@@ -892,6 +895,7 @@ describe('getDefaultTabsLaunch', () => {
hookSettings: {
mode: 'auto',
setupRunPolicy,
commandSourcePolicy,
scripts: { setup: '', archive: '' }
}
}) as unknown as Repo
@@ -923,4 +927,16 @@ describe('getDefaultTabsLaunch', () => {
runCommands: false
})
})
it('does not run shared default tab commands when command source is local-only', () => {
const hooks = {
scripts: {},
defaultTabs: [{ title: 'Server', command: 'pnpm dev' }]
}
expect(getDefaultTabsLaunch(hooks, makeRepo('run-by-default', 'local-only'))).toEqual({
tabs: hooks.defaultTabs,
runCommands: false
})
})
})
+11 -1
View File
@@ -357,7 +357,17 @@ export function getDefaultTabsLaunch(
return undefined
}
const hasCommands = tabs.some((tab) => Boolean(tab.command?.trim()))
const runCommands = hasCommands ? shouldRunSetupForCreate(repo, decision) : false
const sharedCommandPolicy = resolveHookCommandSourcePolicy(
repo.hookSettings?.commandSourcePolicy,
{
hasLocalScript: Boolean(repo.hookSettings?.scripts.setup?.trim())
}
)
// Why: default tab commands come from committed `orca.yaml`; a repo set to
// local-only may still use shared titles/colors, but must not execute them.
const canRunSharedCommands = sharedCommandPolicy !== 'local-only'
const runCommands =
hasCommands && canRunSharedCommands ? shouldRunSetupForCreate(repo, decision) : false
return { tabs, runCommands }
}
@@ -267,6 +267,31 @@ export default function NewWorkspaceComposerCard({
selectedRepoSshStatus === 'disconnected' || selectedRepoSshStatus === null
? 'Connect'
: 'Reconnect'
const setupConfigLabel =
setupConfig?.kind === 'default-tabs'
? 'Default tab commands'
: setupConfig?.kind === 'setup-and-default-tabs'
? 'Setup and default tab commands'
: 'Setup script'
const setupRunLabel =
setupConfig?.kind === 'default-tabs'
? 'Run default tab commands'
: setupConfig?.kind === 'setup-and-default-tabs'
? 'Run setup and default tab commands'
: 'Run setup command'
const setupAskLabel =
setupConfig?.kind === 'default-tabs'
? 'Run default tab commands now?'
: setupConfig?.kind === 'setup-and-default-tabs'
? 'Run setup and default tab commands now?'
: 'Run setup now?'
const setupRunButtonLabel =
setupConfig?.kind === 'default-tabs'
? 'Run commands now'
: setupConfig?.kind === 'setup-and-default-tabs'
? 'Run commands now'
: 'Run setup now'
const setupSkipButtonLabel = setupConfig?.kind === 'setup' ? 'Skip for now' : 'Skip commands'
const handleSetDefaultAgent = React.useCallback(
(next: TuiAgent | 'blank' | null) => {
@@ -558,7 +583,7 @@ export default function NewWorkspaceComposerCard({
<div className="space-y-2">
<div className="flex flex-wrap items-center justify-between gap-2">
<label className="text-xs font-medium text-muted-foreground">
Setup script
{setupConfigLabel}
</label>
<span className="rounded-full border border-border/70 bg-muted/45 px-2 py-0.5 text-[10px] font-medium uppercase tracking-[0.14em] text-foreground/70">
{setupConfig.source === 'yaml'
@@ -600,7 +625,7 @@ export default function NewWorkspaceComposerCard({
}
className="sr-only"
/>
<span>Run setup command</span>
<span>{setupRunLabel}</span>
</label>
)
}
@@ -609,7 +634,7 @@ export default function NewWorkspaceComposerCard({
{requiresExplicitSetupChoice ? (
<div className="space-y-2">
<div className="text-[11px] font-medium text-muted-foreground">
Run setup now?
{setupAskLabel}
</div>
<div className="flex flex-wrap items-center gap-2">
<Button
@@ -618,7 +643,7 @@ export default function NewWorkspaceComposerCard({
variant={setupDecision === 'run' ? 'default' : 'outline'}
size="sm"
>
Run setup now
{setupRunButtonLabel}
</Button>
<Button
type="button"
@@ -626,7 +651,7 @@ export default function NewWorkspaceComposerCard({
variant={setupDecision === 'skip' ? 'secondary' : 'outline'}
size="sm"
>
Skip for now
{setupSkipButtonLabel}
</Button>
</div>
{!setupDecision ? (
@@ -40,6 +40,7 @@ vi.mock('@/lib/telemetry', () => ({
import {
ensureAgentStartupInTerminal,
getSetupConfig,
getWorkspaceSeedName,
isGitLabIssueUrl
} from './new-workspace'
@@ -252,3 +253,39 @@ describe('ensureAgentStartupInTerminal prompt delivery', () => {
expect(mockTrack).not.toHaveBeenCalledWith('agent_prompt_sent', expect.anything())
})
})
describe('getSetupConfig', () => {
it('treats default tab commands as setup-decision commands', () => {
expect(
getSetupConfig(undefined, {
scripts: {},
defaultTabs: [
{ title: 'Server', command: 'pnpm dev' },
{ title: 'Notes' },
{ command: 'codex' }
]
})
).toEqual({
source: 'yaml',
kind: 'default-tabs',
command: '# defaultTabs[1] Server\npnpm dev\n\n# defaultTabs[3]\ncodex'
})
})
it('ignores shared default tab commands when command source is local-only', () => {
expect(
getSetupConfig(
{
hookSettings: {
commandSourcePolicy: 'local-only',
scripts: {}
}
},
{
scripts: {},
defaultTabs: [{ title: 'Server', command: 'pnpm dev' }]
}
)
).toBeNull()
})
})
+47 -6
View File
@@ -78,7 +78,38 @@ export function isGitLabIssueUrl(url: string): boolean {
// is the minimum viable instruction that always produces a coherent agent task.
export const DEFAULT_ISSUE_COMMAND_TEMPLATE = 'Complete {{artifact_url}}'
export type SetupConfig = { source: 'yaml' | 'local' | 'both'; command: string }
export type SetupConfig = {
source: 'yaml' | 'local' | 'both'
command: string
kind: 'setup' | 'default-tabs' | 'setup-and-default-tabs'
}
function getDefaultTabCommandPreview(yamlHooks: OrcaHooks | null): string {
return (yamlHooks?.defaultTabs ?? [])
.map((tab, index) => {
const command = tab.command?.trim()
if (!command) {
return null
}
const label = tab.title ? ` ${tab.title}` : ''
return `# defaultTabs[${index + 1}]${label}\n${command}`
})
.filter((entry): entry is string => entry !== null)
.join('\n\n')
}
function getSetupConfigKind(
hasSetup: boolean,
hasDefaultTabCommands: boolean
): SetupConfig['kind'] {
if (hasSetup && hasDefaultTabCommands) {
return 'setup-and-default-tabs'
}
if (hasDefaultTabCommands) {
return 'default-tabs'
}
return 'setup'
}
/**
* Substitute the issue-command template variables. Prefers `{{artifact_url}}`
@@ -149,21 +180,31 @@ export function getSetupConfig(
yamlHooks: OrcaHooks | null
): SetupConfig | null {
const yamlSetup = yamlHooks?.scripts?.setup?.trim()
const yamlDefaultTabCommands = getDefaultTabCommandPreview(yamlHooks)
const localSetup = repo?.hookSettings?.scripts?.setup?.trim()
const sourcePolicy = resolveHookCommandSourcePolicy(repo?.hookSettings?.commandSourcePolicy, {
hasLocalScript: Boolean(localSetup)
})
if (sourcePolicy === 'local-only') {
return localSetup ? { source: 'local', command: localSetup } : null
return localSetup ? { source: 'local', command: localSetup, kind: 'setup' } : null
}
if (sourcePolicy === 'run-both' && yamlSetup && localSetup) {
return { source: 'both', command: `${yamlSetup}\n${localSetup}` }
const yamlCommand = [yamlSetup, yamlDefaultTabCommands].filter(Boolean).join('\n\n')
if (sourcePolicy === 'run-both' && yamlCommand && localSetup) {
return {
source: 'both',
command: `${yamlCommand}\n\n${localSetup}`,
kind: getSetupConfigKind(true, Boolean(yamlDefaultTabCommands))
}
}
if (yamlSetup) {
return { source: 'yaml', command: yamlSetup }
if (yamlCommand) {
return {
source: 'yaml',
command: yamlCommand,
kind: getSetupConfigKind(Boolean(yamlSetup), Boolean(yamlDefaultTabCommands))
}
}
return null
}