Skip to content

Commit 0fc8bd1

Browse files
Bill LeoutsakosBill Leoutsakos
authored andcommitted
feat(pi): add update branch mode
1 parent dc94879 commit 0fc8bd1

11 files changed

Lines changed: 558 additions & 81 deletions

File tree

apps/docs/content/docs/en/workflows/blocks/pi.mdx

Lines changed: 45 additions & 20 deletions
Large diffs are not rendered by default.

apps/sim/blocks/blocks/pi.test.ts

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ import { PI_SEARCH_PROVIDERS } from '@/executor/handlers/pi/keys'
1414

1515
const searchProviderField = PiBlock.subBlocks.find((subBlock) => subBlock.id === 'searchProvider')
1616
const searchApiKeyField = PiBlock.subBlocks.find((subBlock) => subBlock.id === 'searchApiKey')
17+
const targetBranchField = PiBlock.subBlocks.find((subBlock) => subBlock.id === 'targetBranch')
18+
const modeField = PiBlock.subBlocks.find((subBlock) => subBlock.id === 'mode')
1719

1820
function searchKeyVisible(values: Record<string, unknown>): boolean {
1921
return evaluateSubBlockCondition(searchApiKeyField?.condition, values)
@@ -75,3 +77,73 @@ describe('Pi block search fields', () => {
7577
expect(PiBlock.inputs.searchApiKey).toBeDefined()
7678
})
7779
})
80+
81+
describe('Pi Update Branch fields', () => {
82+
it('offers Update Branch with the other cloud modes when E2B is enabled', () => {
83+
const original = process.env.NEXT_PUBLIC_E2B_ENABLED
84+
process.env.NEXT_PUBLIC_E2B_ENABLED = 'true'
85+
try {
86+
const options =
87+
typeof modeField?.options === 'function' ? modeField.options() : modeField?.options
88+
expect(options).toContainEqual({
89+
label: 'Update Branch',
90+
id: 'cloud_branch',
91+
description: 'Checks out an existing branch, makes changes, and pushes commits back',
92+
})
93+
} finally {
94+
if (original === undefined) Reflect.deleteProperty(process.env, 'NEXT_PUBLIC_E2B_ENABLED')
95+
else process.env.NEXT_PUBLIC_E2B_ENABLED = original
96+
}
97+
})
98+
99+
it('requires the target branch only in Update Branch mode', () => {
100+
expect(targetBranchField?.type).toBe('short-input')
101+
expect(targetBranchField?.required).toBe(true)
102+
expect(evaluateSubBlockCondition(targetBranchField?.condition, { mode: 'cloud_branch' })).toBe(
103+
true
104+
)
105+
expect(evaluateSubBlockCondition(targetBranchField?.condition, { mode: 'cloud' })).toBe(false)
106+
expect(evaluateSubBlockCondition(targetBranchField?.condition, { mode: 'cloud_review' })).toBe(
107+
false
108+
)
109+
expect(evaluateSubBlockCondition(targetBranchField?.condition, { mode: 'local' })).toBe(false)
110+
})
111+
112+
it('declares the target branch input and branch output for cloud authoring modes', () => {
113+
expect(PiBlock.inputs.targetBranch).toBeDefined()
114+
expect(
115+
evaluateSubBlockCondition(PiBlock.outputs.branch.condition, { mode: 'cloud_branch' })
116+
).toBe(true)
117+
expect(evaluateSubBlockCondition(PiBlock.outputs.branch.condition, { mode: 'cloud' })).toBe(
118+
true
119+
)
120+
expect(
121+
evaluateSubBlockCondition(PiBlock.outputs.branch.condition, { mode: 'cloud_review' })
122+
).toBe(false)
123+
})
124+
125+
it('reuses skills and memory fields, including their dependent controls', () => {
126+
for (const id of [
127+
'skills',
128+
'memoryType',
129+
'conversationId',
130+
'slidingWindowSize',
131+
'slidingWindowTokens',
132+
]) {
133+
const field = PiBlock.subBlocks.find((subBlock) => subBlock.id === id)
134+
expect(
135+
evaluateSubBlockCondition(field?.condition, {
136+
mode: 'cloud_branch',
137+
memoryType: id === 'slidingWindowTokens' ? 'sliding_window_tokens' : 'sliding_window',
138+
})
139+
).toBe(true)
140+
}
141+
})
142+
143+
it('hides Create PR and Review Code-specific fields', () => {
144+
for (const id of ['baseBranch', 'branchName', 'draft', 'prTitle', 'prBody', 'pullNumber']) {
145+
const field = PiBlock.subBlocks.find((subBlock) => subBlock.id === id)
146+
expect(evaluateSubBlockCondition(field?.condition, { mode: 'cloud_branch' })).toBe(false)
147+
}
148+
})
149+
})

apps/sim/blocks/blocks/pi.ts

Lines changed: 49 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -42,14 +42,28 @@ const CLOUD_REVIEW: { field: 'mode'; value: 'cloud_review' } = {
4242
field: 'mode',
4343
value: 'cloud_review',
4444
}
45-
const CLOUD_ANY: { field: 'mode'; value: Array<'cloud' | 'cloud_review'> } = {
45+
const CLOUD_BRANCH: { field: 'mode'; value: 'cloud_branch' } = {
4646
field: 'mode',
47-
value: ['cloud', 'cloud_review'],
47+
value: 'cloud_branch',
48+
}
49+
const CLOUD_ANY: {
50+
field: 'mode'
51+
value: Array<'cloud' | 'cloud_branch' | 'cloud_review'>
52+
} = {
53+
field: 'mode',
54+
value: ['cloud', 'cloud_branch', 'cloud_review'],
55+
}
56+
const CLOUD_AUTHORING: { field: 'mode'; value: Array<'cloud' | 'cloud_branch'> } = {
57+
field: 'mode',
58+
value: ['cloud', 'cloud_branch'],
4859
}
4960
const LOCAL: { field: 'mode'; value: 'local' } = { field: 'mode', value: 'local' }
50-
const AUTHORING_MODES: { field: 'mode'; value: Array<'cloud' | 'local'> } = {
61+
const AUTHORING_MODES: {
62+
field: 'mode'
63+
value: Array<'cloud' | 'cloud_branch' | 'local'>
64+
} = {
5165
field: 'mode',
52-
value: ['cloud', 'local'],
66+
value: ['cloud', 'cloud_branch', 'local'],
5367
}
5468
const MEMORY_TYPES = ['conversation', 'sliding_window', 'sliding_window_tokens']
5569

@@ -85,12 +99,13 @@ export const PiBlock: BlockConfig<PiResponse> = {
8599
description: 'Run an autonomous coding agent on a repo',
86100
authMode: AuthMode.ApiKey,
87101
longDescription:
88-
'The Pi Coding Agent runs the Pi harness against a real repository. Create PR spins up an isolated sandbox, clones a GitHub repo, edits with native shell + git, and opens a pull request. Review Code checks out a pinned PR snapshot with read-only tools and posts a structured review with optional inline comments. Local Dev edits files on your own machine over SSH. Create PR and Local Dev can reuse skills and multi-turn memory; Review Code runs without either because PR contents are untrusted. Any mode can optionally get one web_search tool backed by your own Exa, Serper, Parallel AI, or Firecrawl key; the agent writes its own queries, so repository content may reach the provider, and results are untrusted third-party data.',
102+
'The Pi Coding Agent runs the Pi harness against a real repository. Create PR spins up an isolated sandbox, clones a GitHub repo, edits with native shell + git, and opens a pull request. Update Branch checks out an existing branch and pushes the agent’s changes back to it without force-pushing. Review Code checks out a pinned PR snapshot with read-only tools and posts a structured review with optional inline comments. Local Dev edits files on your own machine over SSH. Create PR, Update Branch, and Local Dev can reuse skills and multi-turn memory; Review Code runs without either because PR contents are untrusted. Any mode can optionally get one web_search tool backed by your own Exa, Serper, Parallel AI, or Firecrawl key; the agent writes its own queries, so repository content may reach the provider, and results are untrusted third-party data.',
89103
bestPractices: `
90104
- Use Create PR for hands-off changes against a GitHub repo where a reviewable PR is the deliverable.
105+
- Use Update Branch to continue work on an existing remote branch, including one previously created by Pi.
91106
- Use Review Code to analyze an existing PR and leave summary + inline review comments.
92107
- Use Local Dev to edit a repo on your own machine; expose the machine on a public hostname/tunnel so Sim can reach it over SSH.
93-
- Create PR requires your own provider API key because the model runs in the sandbox. Review Code keeps the model key in Sim and can use either BYOK or a hosted key.
108+
- Create PR and Update Branch require your own provider API key because the model runs in the sandbox. Review Code keeps the model key in Sim and can use either BYOK or a hosted key.
94109
- Internet Search is off by default and always needs your own key for the selected provider, entered on the block. There is no workspace BYOK fallback and no hosted key. Leave it on None unless the task genuinely needs external information.
95110
`,
96111
category: 'blocks',
@@ -102,7 +117,7 @@ export const PiBlock: BlockConfig<PiResponse> = {
102117
id: 'mode',
103118
title: 'Mode',
104119
type: 'dropdown',
105-
/** Create PR and Review Code require E2B and stay hidden when it is disabled. */
120+
/** Cloud modes require E2B and stay hidden when it is disabled. */
106121
value: () => (isTruthy(getEnv('NEXT_PUBLIC_E2B_ENABLED')) ? 'cloud' : 'local'),
107122
options: () => {
108123
const options = [
@@ -119,6 +134,11 @@ export const PiBlock: BlockConfig<PiResponse> = {
119134
id: 'cloud',
120135
description: 'Runs in an isolated sandbox, clones your repo, and opens a PR',
121136
},
137+
{
138+
label: 'Update Branch',
139+
id: 'cloud_branch',
140+
description: 'Checks out an existing branch, makes changes, and pushes commits back',
141+
},
122142
{
123143
label: 'Review Code',
124144
id: 'cloud_review',
@@ -156,7 +176,7 @@ export const PiBlock: BlockConfig<PiResponse> = {
156176
defaultValue: 'none',
157177
options: SEARCH_PROVIDER_OPTIONS,
158178
tooltip:
159-
'Gives the agent a single web_search tool backed by the selected provider. Search always uses your own key for that provider, never a Sim-hosted one, because Create PR places the key inside the coding sandbox.',
179+
'Gives the agent a single web_search tool backed by the selected provider. Search always uses your own key for that provider, never a Sim-hosted one, because cloud authoring places the key inside the coding sandbox.',
160180
},
161181
{
162182
id: 'searchApiKey',
@@ -204,7 +224,7 @@ export const PiBlock: BlockConfig<PiResponse> = {
204224
paramVisibility: 'user-only',
205225
placeholder: 'GitHub personal access token',
206226
tooltip:
207-
'Personal access token used for GitHub access. Create PR needs clone/push/PR permissions; Review Code needs clone + review permissions.',
227+
'Personal access token used for GitHub access. Create PR needs clone/push/PR permissions; Update Branch needs clone/push permissions; Review Code needs clone + review permissions.',
208228
required: true,
209229
condition: CLOUD_ANY,
210230
},
@@ -216,6 +236,16 @@ export const PiBlock: BlockConfig<PiResponse> = {
216236
tooltip: 'The branch the pull request is opened against; the repo is cloned from it too.',
217237
condition: CLOUD,
218238
},
239+
{
240+
id: 'targetBranch',
241+
title: 'Target Branch',
242+
type: 'short-input',
243+
placeholder: 'e.g., feature/add-auth',
244+
tooltip:
245+
'Existing remote branch to update. The run never force-pushes and fails if the branch does not exist or changes while Pi is running.',
246+
required: true,
247+
condition: CLOUD_BRANCH,
248+
},
219249
{
220250
id: 'branchName',
221251
title: 'Branch Name',
@@ -417,12 +447,12 @@ export const PiBlock: BlockConfig<PiResponse> = {
417447
mode: 'advanced',
418448
required: {
419449
field: 'mode',
420-
value: ['cloud', 'local'],
450+
value: ['cloud', 'cloud_branch', 'local'],
421451
and: { field: 'memoryType', value: MEMORY_TYPES },
422452
},
423453
condition: {
424454
field: 'mode',
425-
value: ['cloud', 'local'],
455+
value: ['cloud', 'cloud_branch', 'local'],
426456
and: { field: 'memoryType', value: MEMORY_TYPES },
427457
},
428458
dependsOn: ['memoryType'],
@@ -435,7 +465,7 @@ export const PiBlock: BlockConfig<PiResponse> = {
435465
mode: 'advanced',
436466
condition: {
437467
field: 'mode',
438-
value: ['cloud', 'local'],
468+
value: ['cloud', 'cloud_branch', 'local'],
439469
and: { field: 'memoryType', value: ['sliding_window'] },
440470
},
441471
dependsOn: ['memoryType'],
@@ -448,7 +478,7 @@ export const PiBlock: BlockConfig<PiResponse> = {
448478
mode: 'advanced',
449479
condition: {
450480
field: 'mode',
451-
value: ['cloud', 'local'],
481+
value: ['cloud', 'cloud_branch', 'local'],
452482
and: { field: 'memoryType', value: ['sliding_window_tokens'] },
453483
},
454484
dependsOn: ['memoryType'],
@@ -460,15 +490,16 @@ export const PiBlock: BlockConfig<PiResponse> = {
460490
inputs: {
461491
mode: {
462492
type: 'string',
463-
description: 'Execution mode: Create PR, Review Code, or Local Dev',
493+
description: 'Execution mode: Create PR, Update Branch, Review Code, or Local Dev',
464494
},
465495
task: { type: 'string', description: 'Instruction for the coding agent' },
466496
model: { type: 'string', description: 'AI model to use' },
467-
owner: { type: 'string', description: 'GitHub repository owner (Create PR and Review Code)' },
468-
repo: { type: 'string', description: 'GitHub repository name (Create PR and Review Code)' },
469-
githubToken: { type: 'string', description: 'GitHub token (Create PR and Review Code)' },
497+
owner: { type: 'string', description: 'GitHub repository owner (cloud modes)' },
498+
repo: { type: 'string', description: 'GitHub repository name (cloud modes)' },
499+
githubToken: { type: 'string', description: 'GitHub token (cloud modes)' },
470500
baseBranch: { type: 'string', description: 'Base branch for the PR (Create PR)' },
471501
branchName: { type: 'string', description: 'Branch to create (Create PR)' },
502+
targetBranch: { type: 'string', description: 'Existing branch to update (Update Branch)' },
472503
draft: { type: 'boolean', description: 'Open the PR as a draft (Create PR)' },
473504
prTitle: { type: 'string', description: 'Pull request title (Create PR)' },
474505
prBody: { type: 'string', description: 'Pull request body (Create PR)' },
@@ -512,7 +543,7 @@ export const PiBlock: BlockConfig<PiResponse> = {
512543
branch: {
513544
type: 'string',
514545
description: 'Branch pushed with the changes',
515-
condition: CLOUD,
546+
condition: CLOUD_AUTHORING,
516547
},
517548
reviewUrl: {
518549
type: 'string',

apps/sim/executor/handlers/pi/backend.ts

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,15 @@ export interface PiCloudRunParams extends PiContextualRunParams {
105105
prBody?: string
106106
}
107107

108+
/** Parameters for a cloud (E2B) Pi run that updates an existing branch. */
109+
export interface PiCloudBranchRunParams extends PiContextualRunParams {
110+
mode: 'cloud_branch'
111+
owner: string
112+
repo: string
113+
githubToken: string
114+
targetBranch: string
115+
}
116+
108117
/** Parameters for a cloud (E2B) Pi run that reviews an existing PR. */
109118
export interface PiCloudReviewRunParams extends PiRunBaseParams {
110119
mode: 'cloud_review'
@@ -115,7 +124,11 @@ export interface PiCloudReviewRunParams extends PiRunBaseParams {
115124
reviewEvent: 'COMMENT' | 'REQUEST_CHANGES'
116125
}
117126

118-
export type PiRunParams = PiLocalRunParams | PiCloudRunParams | PiCloudReviewRunParams
127+
export type PiRunParams =
128+
| PiLocalRunParams
129+
| PiCloudRunParams
130+
| PiCloudBranchRunParams
131+
| PiCloudReviewRunParams
119132

120133
/** Progress callbacks and cancellation passed into a backend run. */
121134
export interface PiRunContext {

0 commit comments

Comments
 (0)