diff --git a/src/commands/integration/connect.ts b/src/commands/integration/connect.ts index ff09776..27a953a 100644 --- a/src/commands/integration/connect.ts +++ b/src/commands/integration/connect.ts @@ -98,6 +98,23 @@ export function resolveTypeOptions(category: string | undefined, typeFromFlag: b return typeFromFlag ? TYPE_OPTIONS : filtered; } +// When the user's primary local agent (config.agent, persisted by `polylane +// setup`) also exists as a cloud code agent, surface it first in its category +// and pre-highlight it in the picker. Exact id match only โ€” the local +// registry and the integration types share ids where they overlap (cursor). +export function prioritizeCodeAgent( + options: typeof TYPE_OPTIONS, + localAgent: string | undefined +): { options: typeof TYPE_OPTIONS; initialValue: ConnectableType | undefined } { + const idx = options.findIndex((o) => o.category === 'code-agent' && o.value === localAgent); + if (idx < 0) return { options, initialValue: undefined }; + const first = options.findIndex((o) => o.category === 'code-agent'); + const reordered = [...options]; + const [own] = reordered.splice(idx, 1); + reordered.splice(first, 0, { ...own!, hint: own!.hint.replace(/coding agent$/, 'your coding agent') }); + return { options: reordered, initialValue: own!.value }; +} + // Same site list the console offers; the flag accepts any value so orgs on // sites not listed here (e.g. newer regions) are not locked out. const DATADOG_SITES = [ @@ -637,7 +654,10 @@ export const integrationConnectCommand: Command = { const typeFromFlag = getArgString(args, 'type') !== undefined; const category = getArgString(args, 'category'); // --type always wins: the category filter only narrows the picker. - const typeOptions = resolveTypeOptions(category, typeFromFlag); + const { options: typeOptions, initialValue } = prioritizeCodeAgent( + resolveTypeOptions(category, typeFromFlag), + config.agent + ); if (shouldOfferCodeAgent(category, typeFromFlag, isInteractive(config.nonInteractive))) { note( @@ -676,7 +696,8 @@ export const integrationConnectCommand: Command = { { nonInteractive: config.nonInteractive }, 'Which integration do you want to connect?', typeOptions, - 'Cancel' + 'Cancel', + initialValue ); if (type === BACK) break; const outcome = await connectType(config, api, args, workspaceId, type, noBrowser); diff --git a/src/utils/prompt.ts b/src/utils/prompt.ts index 0873aed..baa8297 100644 --- a/src/utils/prompt.ts +++ b/src/utils/prompt.ts @@ -84,7 +84,8 @@ export async function promptSelectOrBack( ctx: PromptContext, message: string, options: Array<{ value: T; label: string; hint?: string }>, - backLabel = 'โ† Back' + backLabel = 'โ† Back', + initialValue?: T ): Promise { ensureInteractive(ctx, message); type NavOption = @@ -93,6 +94,7 @@ export async function promptSelectOrBack( const result = await p.select({ message, options: [...options, { value: BACK, label: backLabel }], + ...(initialValue !== undefined ? { initialValue } : {}), }); if (result === BACK || p.isCancel(result)) return BACK; return result as T; diff --git a/test/integration-connect-priority.test.ts b/test/integration-connect-priority.test.ts new file mode 100644 index 0000000..2bc316b --- /dev/null +++ b/test/integration-connect-priority.test.ts @@ -0,0 +1,39 @@ +import { describe, it } from 'node:test'; +import assert from 'node:assert/strict'; +import { prioritizeCodeAgent, typeOptionsForCategory } from '../src/commands/integration/connect'; + +describe('prioritizeCodeAgent', () => { + it('moves the local agent to the front of the code-agent group', () => { + const all = typeOptionsForCategory(undefined); + const before = all.map((o) => ({ ...o })); + const { options, initialValue } = prioritizeCodeAgent(all, 'cursor'); + assert.deepEqual(all, before); + const values = options.map((o) => o.value); + assert.equal(initialValue, 'cursor'); + assert.equal(values.indexOf('cursor'), values.indexOf('devin') - 1); + assert.equal(values[0], 'github'); + assert.equal(options.find((o) => o.value === 'cursor')?.hint, 'API key ยท your coding agent'); + }); + + it('pre-highlights the local agent in a narrowed picker', () => { + const { options, initialValue } = prioritizeCodeAgent(typeOptionsForCategory('code-agent'), 'cursor'); + assert.equal(options[0]?.value, 'cursor'); + assert.equal(initialValue, 'cursor'); + }); + + it('keeps grouping intact', () => { + const { options } = prioritizeCodeAgent(typeOptionsForCategory(undefined), 'cursor'); + const categories = options.map((o) => o.category); + assert.deepEqual([...new Set(categories)], ['git', 'communication', 'observability', 'code-agent', 'protocol']); + assert.equal(options.length, typeOptionsForCategory(undefined).length); + }); + + it('is a no-op when the local agent has no cloud counterpart', () => { + const all = typeOptionsForCategory(undefined); + for (const agent of [undefined, 'claude', 'zed']) { + const { options, initialValue } = prioritizeCodeAgent(all, agent); + assert.deepEqual(options, all); + assert.equal(initialValue, undefined); + } + }); +});