Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 23 additions & 2 deletions src/commands/integration/connect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = [
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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);
Expand Down
4 changes: 3 additions & 1 deletion src/utils/prompt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,8 @@ export async function promptSelectOrBack<T extends string>(
ctx: PromptContext,
message: string,
options: Array<{ value: T; label: string; hint?: string }>,
backLabel = '← Back'
backLabel = '← Back',
initialValue?: T
): Promise<T | typeof BACK> {
ensureInteractive(ctx, message);
type NavOption =
Expand All @@ -93,6 +94,7 @@ export async function promptSelectOrBack<T extends string>(
const result = await p.select<NavOption[], string | typeof BACK>({
message,
options: [...options, { value: BACK, label: backLabel }],
...(initialValue !== undefined ? { initialValue } : {}),
});
if (result === BACK || p.isCancel(result)) return BACK;
return result as T;
Expand Down
39 changes: 39 additions & 0 deletions test/integration-connect-priority.test.ts
Original file line number Diff line number Diff line change
@@ -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);
Comment thread
claude[bot] marked this conversation as resolved.
assert.deepEqual(options, all);
assert.equal(initialValue, undefined);
}
});
});
Loading