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
19 changes: 19 additions & 0 deletions __tests__/ui/screens/InstallPluginsScreen.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,25 @@ describe('InstallPluginsScreen', () => {
});
});

it('sorts detected IDEs above non-detected ones', async () => {
const { detectInstalledPlugins } = await import('../../../src/integrations/skills/plugin.js');
vi.mocked(detectInstalledPlugins).mockResolvedValueOnce([
{ ide: 'claude', via: 'cli' },
{ ide: 'codex', via: 'cli' },
]);

using sut = renderScreen(<InstallPluginsScreen />, { screen: ScreenId.InstallPlugins });

await waitFor(() => {
const frame = sut.lastFrame()!;
const codexPos = frame.indexOf('Codex');
const cursorPos = frame.indexOf('Cursor');
expect(codexPos).toBeGreaterThan(-1);
expect(cursorPos).toBeGreaterThan(-1);
expect(codexPos).toBeLessThan(cursorPos);
});
});

it('shows error and retry option on install failure', async () => {
const { installPlugin } = await import('../../../src/integrations/skills/plugin.js');
vi.mocked(installPlugin).mockRejectedValueOnce(new Error('Installation failed'));
Expand Down
80 changes: 9 additions & 71 deletions src/ui/tui/screens/install-plugins/InstallPluginsScreen.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,11 @@
import { Box, Text } from 'ink';
import { Spinner } from '@inkjs/ui';
import { Colors, Icons } from '../../styles.js';
import { MainLayout } from '../../components/MainLayout.js';
import { TaskList } from '../../components/TaskList.js';
import { buildWizardTasks } from '../../lib/wizard-tasks.js';
import type { IdeId } from '@shared-kernel/types.js';
import { getIntegrations } from '@integrations/index.js';
import { PLUGIN_REPO_URL } from '@lib/constants.js';
import { ScreenId } from '@lib/session.js';
import { MainLayout } from '../../components/MainLayout.js';
import { TaskList } from '../../components/TaskList.js';
import { useAutoAdvance } from '../../hooks/useAutoAdvance.js';
import { useLogger } from '../../hooks/useLog.js';
import { useNavigation } from '../../hooks/useNavigation.js';
import { buildWizardTasks } from '../../lib/wizard-tasks.js';
import { $session, store } from '../../store.js';
import { usePluginInstall } from './usePluginInstall.js';
import { track } from '@lib/telemetry.js';
Expand All @@ -21,15 +16,10 @@ import {
} from './log-messages.js';
import * as te from './telemetry-events.js';
import { type IdeSelectValue, type DetectedSelectValue, type ErrorAction } from './actions.js';
import { BottomPrompt } from './components/index.js';
import { BottomPrompt, MainContent } from './components/index.js';

const ALL_INTEGRATIONS = getIntegrations();

const IDE_LABELS = Object.fromEntries(ALL_INTEGRATIONS.map((i) => [i.id, i.name])) as Record<
IdeId,
string
>;

export function InstallPluginsScreen() {
const navigate = useNavigation(ScreenId.InstallPlugins);
const log = useLogger(ScreenId.InstallPlugins);
Expand All @@ -55,6 +45,7 @@ export function InstallPluginsScreen() {
function handleDetectedSelect(value: DetectedSelectValue) {
if (value === 'continue') {
if (!preferredIndex) return;

store.setIde(preferredIndex);
log(pluginsAlreadyInstalled(detected));
track(te.pluginsAlreadyDetected());
Expand All @@ -68,9 +59,9 @@ export function InstallPluginsScreen() {
if (value === 'exit') {
log(pluginExitedAfterError(error));
track(te.pluginExitedAfterError());
process.exit(1);
return;
return process.exit(1);
}

const ide = $session.get().ide;
if (ide) selectIde(ide);
}
Expand All @@ -84,67 +75,14 @@ export function InstallPluginsScreen() {
: 'active',
);

const main = (
<Box flexDirection="column">
<Box marginBottom={1}>
<Text color={Colors.primary} bold>
Select agent to set up
</Text>
</Box>
<Box marginBottom={1}>
<Text color={Colors.muted}>
Your agent will get Confidence skills for flag management, warehouse setup, migrations,
and onboarding — so it can help without searching the docs.
</Text>
</Box>

{phase === 'detecting' && <Spinner label="Checking for Confidence AI plugins..." />}

{phase === 'already-installed' && (
<>
<Text>Detected Confidence plugins for:</Text>
{detected.map((d) => (
<Box key={d} gap={1}>
<Text color={Colors.success}>{Icons.check}</Text>
<Text>{IDE_LABELS[d] ?? d}</Text>
</Box>
))}
</>
)}

{phase === 'installing' && <Spinner label="Installing Confidence plugin..." />}

{phase === 'installed' && (
<Box>
<Text color={Colors.success}>Plugin installed successfully. Continuing...</Text>
</Box>
)}

{phase === 'error' && (
<Box flexDirection="column">
<Text color={Colors.error}>Failed to install plugin: {error}</Text>
<Box marginTop={1}>
<Text color={Colors.muted}>
You can install manually from: <Text color={Colors.primary}>{PLUGIN_REPO_URL}</Text>
</Text>
</Box>
</Box>
)}
</Box>
);

const preferredLabel = preferredIndex ? IDE_LABELS[preferredIndex] : null;
const otherIntegrations = ALL_INTEGRATIONS.filter((i) => i.id !== preferredIndex);

return (
<MainLayout
main={main}
main={<MainContent phase={phase} detected={detected} error={error} />}
aside={<TaskList tasks={tasks} />}
prompt={
<BottomPrompt
phase={phase}
preferredLabel={preferredLabel}
otherIntegrations={otherIntegrations}
detected={detected}
onIdeSelect={handleIdeSelect}
onDetectedSelect={handleDetectedSelect}
onError={handleError}
Expand Down
27 changes: 16 additions & 11 deletions src/ui/tui/screens/install-plugins/components/BottomPrompt.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { IdeIntegration } from '@integrations/index.js';
import type { IdeId } from '@shared-kernel/types.js';
import { getIntegrations } from '@integrations/index.js';
import { PromptPanel } from '../../../components/PromptPanel.js';
import type { PluginPhase } from '../usePluginInstall.js';
import {
Expand All @@ -9,19 +10,19 @@ import {
type ErrorAction,
} from '../actions.js';

const ALL_INTEGRATIONS = getIntegrations();

type BottomPromptProps = {
phase: PluginPhase;
preferredLabel: string | null;
otherIntegrations: IdeIntegration[];
detected: IdeId[];
onIdeSelect: (value: IdeSelectValue) => void;
onDetectedSelect: (value: DetectedSelectValue) => void;
onError: (value: ErrorAction) => void;
};

export function BottomPrompt({
phase,
preferredLabel,
otherIntegrations,
detected,
onIdeSelect,
onDetectedSelect,
onError,
Expand All @@ -46,17 +47,21 @@ export function BottomPrompt({
/>
);
case 'already-installed': {
const otherOptions = otherIntegrations.map((i) => ({
label: i.name,
value: i.id,
}));
const detectedSet = new Set(detected);
const preferred = ALL_INTEGRATIONS.find((i) => detectedSet.has(i.id));
const otherOptions = ALL_INTEGRATIONS.filter((i) => i.id !== preferred?.id)
.toSorted((a, b) => Number(detectedSet.has(b.id)) - Number(detectedSet.has(a.id)))
.map((i) => ({
label: i.name,
value: i.id,
}));

return (
<PromptPanel
mode="select"
status={`Confidence plugin detected for ${preferredLabel}. Continue with this agent tool?`}
status={`Confidence plugin detected for ${preferred?.name}. Continue with it?`}
options={[
{ label: `Continue with ${preferredLabel}`, value: 'continue' },
{ label: `Continue with ${preferred?.name}`, value: 'continue' },
...otherOptions,
]}
onSelect={onDetectedSelect}
Expand Down
68 changes: 68 additions & 0 deletions src/ui/tui/screens/install-plugins/components/MainContent.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import { Box, Text } from 'ink';
import { Spinner } from '@inkjs/ui';
import type { IdeId } from '@shared-kernel/types.js';
import { getIntegrations } from '@integrations/index.js';
import { PLUGIN_REPO_URL } from '@lib/constants.js';
import { Colors, Icons } from '../../../styles.js';
import type { PluginPhase } from '../usePluginInstall.js';

type IdeLabels = Record<IdeId, string>;

const IDE_LABELS = Object.fromEntries(getIntegrations().map((i) => [i.id, i.name])) as IdeLabels;

type MainContentProps = {
phase: PluginPhase;
detected: IdeId[];
error: string | null;
};

export function MainContent({ phase, detected, error }: MainContentProps) {
return (
<Box flexDirection="column">
<Box marginBottom={1}>
<Text color={Colors.primary} bold>
Select agent to set up
</Text>
</Box>
<Box marginBottom={1}>
<Text color={Colors.muted}>
Your agent will get Confidence skills for flag management, warehouse setup, migrations,
and onboarding — so it can help without searching the docs.
</Text>
</Box>

{phase === 'detecting' && <Spinner label="Checking for Confidence AI plugins..." />}

{phase === 'already-installed' && (
<>
<Text>Detected Confidence plugins for:</Text>
{detected.map((d) => (
<Box key={d} gap={1}>
<Text color={Colors.success}>{Icons.check}</Text>
<Text>{IDE_LABELS[d] ?? d}</Text>
</Box>
))}
</>
)}

{phase === 'installing' && <Spinner label="Installing Confidence plugin..." />}

{phase === 'installed' && (
<Box>
<Text color={Colors.success}>Plugin installed successfully. Continuing...</Text>
</Box>
)}

{phase === 'error' && (
<Box flexDirection="column">
<Text color={Colors.error}>Failed to install plugin: {error}</Text>
<Box marginTop={1}>
<Text color={Colors.muted}>
You can install manually from: <Text color={Colors.primary}>{PLUGIN_REPO_URL}</Text>
</Text>
</Box>
</Box>
)}
</Box>
);
}
1 change: 1 addition & 0 deletions src/ui/tui/screens/install-plugins/components/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export { BottomPrompt } from './BottomPrompt.js';
export { MainContent } from './MainContent.js';