Skip to content

Commit 94778eb

Browse files
BillLeoutsakosvl346Bill LeoutsakosBill Leoutsakos
authored
feat(pi): add update PR mode (#6031)
* feat(pi): add update branch mode * feat(pi): manage pull requests in update mode * docs(pi): clarify closed PR behavior * fix(pi): handle update PR finalization races * fix(pi): accept renamed update PR repositories * fix(pi): clarify shared open PR errors * fix(pi): recheck update PR ambiguity * fix(pi): recreate closed update PRs * fix(pi): follow replacement update PRs * fix(pi): preserve update PR BYOK after staging rebase --------- Co-authored-by: Bill Leoutsakos <billleoutsakos@Bills-MacBook-Pro.local> Co-authored-by: Bill Leoutsakos <billleoutsakos@Mac.localdomain>
1 parent 4b239fa commit 94778eb

15 files changed

Lines changed: 1866 additions & 266 deletions

File tree

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

Lines changed: 63 additions & 30 deletions
Large diffs are not rendered by default.

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

Lines changed: 131 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import { PI_SEARCH_PROVIDERS } from '@/executor/handlers/pi/keys'
2222

2323
const searchProviderField = PiBlock.subBlocks.find((subBlock) => subBlock.id === 'searchProvider')
2424
const searchApiKeyField = PiBlock.subBlocks.find((subBlock) => subBlock.id === 'searchApiKey')
25+
const targetBranchField = PiBlock.subBlocks.find((subBlock) => subBlock.id === 'targetBranch')
2526

2627
function searchKeyVisible(values: Record<string, unknown>): boolean {
2728
return evaluateSubBlockCondition(searchApiKeyField?.condition, values)
@@ -84,69 +85,73 @@ describe('Pi block search fields', () => {
8485
})
8586
})
8687

87-
describe('Pi Create PR Babysit surface', () => {
88-
it('offers exactly Create PR, Review Code, and Local Dev as top-level modes', () => {
88+
describe('Pi cloud authoring surface', () => {
89+
it('offers Create PR, Update PR, Review Code, and Local Dev as top-level modes', () => {
8990
const mode = PiBlock.subBlocks.find((subBlock) => subBlock.id === 'mode')
9091
const options =
9192
typeof mode?.options === 'function'
9293
? mode.options()
9394
: (mode?.options as Array<{ id: string }> | undefined)
9495

95-
expect(options?.map(({ id }) => id)).toEqual(['cloud', 'cloud_review', 'local'])
96+
expect(options?.map(({ id }) => id)).toEqual(['cloud', 'cloud_branch', 'cloud_review', 'local'])
9697
})
9798

98-
it('declares the toggle, required reviewer mentions, advanced rounds, and result outputs', () => {
99-
const toggle = PiBlock.subBlocks.find((subBlock) => subBlock.id === 'babysitMode')
100-
const maxRounds = PiBlock.subBlocks.find((subBlock) => subBlock.id === 'maxRounds')
101-
const mentions = PiBlock.subBlocks.find((subBlock) => subBlock.id === 'reviewMentions')
102-
103-
expect(toggle).toMatchObject({
104-
type: 'switch',
105-
defaultValue: false,
106-
condition: { field: 'mode', value: 'cloud' },
107-
})
108-
expect(maxRounds).toMatchObject({
109-
type: 'short-input',
110-
defaultValue: '3',
111-
mode: 'advanced',
112-
condition: {
113-
field: 'mode',
114-
value: 'cloud',
115-
and: { field: 'babysitMode', value: [true, 'true'] },
116-
},
117-
})
118-
expect(mentions).toMatchObject({
119-
type: 'short-input',
120-
defaultValue: '',
121-
hideDividerBefore: true,
122-
required: {
123-
field: 'mode',
124-
value: 'cloud',
125-
and: { field: 'babysitMode', value: [true, 'true'] },
126-
},
127-
condition: {
128-
field: 'mode',
129-
value: 'cloud',
130-
and: { field: 'babysitMode', value: [true, 'true'] },
131-
},
132-
})
133-
for (const output of [
134-
'rounds',
135-
'threadsClean',
136-
'checksGreen',
137-
'threadsResolved',
138-
'commitsPushed',
139-
'stopReason',
140-
]) {
141-
expect(PiBlock.outputs[output]).toMatchObject({
99+
it.each(['cloud', 'cloud_branch'])(
100+
'declares Babysit controls and outputs for %s',
101+
(authoringMode) => {
102+
const toggle = PiBlock.subBlocks.find((subBlock) => subBlock.id === 'babysitMode')
103+
const maxRounds = PiBlock.subBlocks.find((subBlock) => subBlock.id === 'maxRounds')
104+
const mentions = PiBlock.subBlocks.find((subBlock) => subBlock.id === 'reviewMentions')
105+
106+
expect(toggle).toMatchObject({
107+
type: 'switch',
108+
defaultValue: false,
109+
condition: { field: 'mode', value: ['cloud', 'cloud_branch'] },
110+
})
111+
expect(maxRounds).toMatchObject({
112+
type: 'short-input',
113+
defaultValue: '3',
114+
mode: 'advanced',
115+
condition: {
116+
field: 'mode',
117+
value: ['cloud', 'cloud_branch'],
118+
and: { field: 'babysitMode', value: [true, 'true'] },
119+
},
120+
})
121+
expect(mentions).toMatchObject({
122+
type: 'short-input',
123+
defaultValue: '',
124+
hideDividerBefore: true,
125+
required: {
126+
field: 'mode',
127+
value: ['cloud', 'cloud_branch'],
128+
and: { field: 'babysitMode', value: [true, 'true'] },
129+
},
142130
condition: {
143131
field: 'mode',
144-
value: 'cloud',
132+
value: ['cloud', 'cloud_branch'],
145133
and: { field: 'babysitMode', value: [true, 'true'] },
146134
},
147135
})
136+
for (const output of [
137+
'rounds',
138+
'threadsClean',
139+
'checksGreen',
140+
'threadsResolved',
141+
'commitsPushed',
142+
'stopReason',
143+
]) {
144+
expect(PiBlock.outputs[output]).toMatchObject({
145+
condition: {
146+
field: 'mode',
147+
value: ['cloud', 'cloud_branch'],
148+
and: { field: 'babysitMode', value: [true, 'true'] },
149+
},
150+
})
151+
}
152+
expect(evaluateSubBlockCondition(toggle?.condition, { mode: authoringMode })).toBe(true)
148153
}
149-
})
154+
)
150155

151156
it('requires a task and hides Draft PR while Babysit Mode is enabled', () => {
152157
const task = PiBlock.subBlocks.find((subBlock) => subBlock.id === 'task')
@@ -176,4 +181,81 @@ describe('Pi Create PR Babysit surface', () => {
176181
expect(evaluateSubBlockCondition(pullNumber?.condition, { mode: 'cloud' })).toBe(false)
177182
expect(evaluateSubBlockCondition(pullNumber?.condition, { mode: 'cloud_review' })).toBe(true)
178183
})
184+
185+
it('requires the target branch only in Update PR mode', () => {
186+
expect(targetBranchField?.type).toBe('short-input')
187+
expect(targetBranchField?.required).toBe(true)
188+
expect(evaluateSubBlockCondition(targetBranchField?.condition, { mode: 'cloud_branch' })).toBe(
189+
true
190+
)
191+
expect(evaluateSubBlockCondition(targetBranchField?.condition, { mode: 'cloud' })).toBe(false)
192+
expect(evaluateSubBlockCondition(targetBranchField?.condition, { mode: 'cloud_review' })).toBe(
193+
false
194+
)
195+
expect(evaluateSubBlockCondition(targetBranchField?.condition, { mode: 'local' })).toBe(false)
196+
})
197+
198+
it('declares the target branch input and branch output for cloud authoring modes', () => {
199+
expect(PiBlock.inputs.targetBranch).toBeDefined()
200+
expect(
201+
evaluateSubBlockCondition(PiBlock.outputs.branch.condition, { mode: 'cloud_branch' })
202+
).toBe(true)
203+
expect(evaluateSubBlockCondition(PiBlock.outputs.branch.condition, { mode: 'cloud' })).toBe(
204+
true
205+
)
206+
expect(
207+
evaluateSubBlockCondition(PiBlock.outputs.branch.condition, { mode: 'cloud_review' })
208+
).toBe(false)
209+
})
210+
211+
it('reuses skills and memory fields, including their dependent controls', () => {
212+
for (const id of [
213+
'skills',
214+
'memoryType',
215+
'conversationId',
216+
'slidingWindowSize',
217+
'slidingWindowTokens',
218+
]) {
219+
const field = PiBlock.subBlocks.find((subBlock) => subBlock.id === id)
220+
expect(
221+
evaluateSubBlockCondition(field?.condition, {
222+
mode: 'cloud_branch',
223+
memoryType: id === 'slidingWindowTokens' ? 'sliding_window_tokens' : 'sliding_window',
224+
})
225+
).toBe(true)
226+
}
227+
})
228+
229+
it('shows PR metadata controls and hides Create PR and Review Code-only fields', () => {
230+
for (const id of ['baseBranch', 'prTitle', 'prBody', 'prState']) {
231+
const field = PiBlock.subBlocks.find((subBlock) => subBlock.id === id)
232+
expect(evaluateSubBlockCondition(field?.condition, { mode: 'cloud_branch' })).toBe(true)
233+
}
234+
for (const id of ['branchName', 'draft', 'pullNumber']) {
235+
const field = PiBlock.subBlocks.find((subBlock) => subBlock.id === id)
236+
expect(evaluateSubBlockCondition(field?.condition, { mode: 'cloud_branch' })).toBe(false)
237+
}
238+
const prState = PiBlock.subBlocks.find((subBlock) => subBlock.id === 'prState')
239+
expect(
240+
evaluateSubBlockCondition(prState?.condition, {
241+
mode: 'cloud_branch',
242+
babysitMode: true,
243+
})
244+
).toBe(false)
245+
expect(
246+
evaluateSubBlockCondition(prState?.condition, {
247+
mode: 'cloud_branch',
248+
babysitMode: 'true',
249+
})
250+
).toBe(false)
251+
expect(PiBlock.inputs.prState).toBeDefined()
252+
})
253+
254+
it.each(['cloud', 'cloud_branch'])(
255+
'always shows the model API key for sandbox authoring mode %s',
256+
(mode) => {
257+
const apiKey = PiBlock.subBlocks.find((subBlock) => subBlock.id === 'apiKey')
258+
expect(evaluateSubBlockCondition(apiKey?.condition, { mode })).toBe(true)
259+
}
260+
)
179261
})

0 commit comments

Comments
 (0)