Skip to content
Open
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
5 changes: 5 additions & 0 deletions .changeset/loud-plums-invite.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@nanocollective/nanocoder": patch
---

Setup wizard's config location picker now shows the resolved path next to each option instead of a bare label.
4 changes: 2 additions & 2 deletions source/app/components/app-container.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
getGitStatusSummarySync,
} from '@/tools/git/utils';
import {DEVELOPMENT_MODE_LABELS, type DevelopmentMode} from '@/types/core';
import {homeRelative} from '@/utils/path';

/**
* Format a {@link GitStatusSummary} for inline display next to the
Expand Down Expand Up @@ -54,8 +55,7 @@ function BootSummary({
const {colors} = useTheme();
const {isNarrow} = useResponsiveTerminal();
const configPath = getClosestConfigFile('agents.config.json');
const homedir = process.env.HOME || process.env.USERPROFILE || '';
const shortConfig = homedir ? configPath.replace(homedir, '~') : configPath;
const shortConfig = homeRelative(configPath);
const modeLabel = mode ? DEVELOPMENT_MODE_LABELS[mode] : undefined;
const gitStatus = getGitStatusSummarySync();
const gitLabel = gitStatus ? formatBootSummaryGitLabel(gitStatus) : undefined;
Expand Down
44 changes: 44 additions & 0 deletions source/utils/path.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import test from 'ava';
import {resolve, sep} from 'node:path';
import {homeRelative, truncateMiddle} from './path.js';

const HOME = resolve('/Users/will');

test('homeRelative shortens a path inside home to a tilde form', t => {
const input = resolve('/Users/will/projects/app');
t.is(homeRelative(input, HOME), `~${sep}projects${sep}app`);
});

test('homeRelative returns a bare tilde for the home directory itself', t => {
t.is(homeRelative(resolve('/Users/will'), HOME), '~');
});

test('homeRelative does not mangle a sibling directory that shares a prefix', t => {
const input = resolve('/Users/willy/projects/app');
t.is(homeRelative(input, HOME), input);
});

test('homeRelative leaves unrelated paths untouched', t => {
const input = resolve('/etc/config');
t.is(homeRelative(input, HOME), input);
});

test('homeRelative leaves paths untouched when home is the filesystem root', t => {
const root = resolve('/');
const child = resolve('/foo');
t.is(homeRelative(child, root), child);
t.is(homeRelative(root, root), root);
});

test('truncateMiddle leaves short strings untouched', t => {
t.is(truncateMiddle('/short/path', 40), '/short/path');
});

test('truncateMiddle keeps both the root and the leaf segment', t => {
const long = '/Users/will/projects/some-really-long-monorepo-name/src/index.ts';
const result = truncateMiddle(long, 30);
t.is(result.length, 30);
t.true(result.startsWith('/Users/wi'));
t.true(result.endsWith('index.ts'));
t.true(result.includes('...'));
});
37 changes: 37 additions & 0 deletions source/utils/path.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import {homedir} from 'node:os';
import {resolve, sep} from 'node:path';

export function homeRelative(path: string, home: string = homedir()): string {
const resolved = resolve(path);
const resolvedHome = resolve(home);

if (resolvedHome === sep || /^[A-Za-z]:\\$/.test(resolvedHome)) {
return resolved;
}

if (resolved === resolvedHome) {
return '~';
}

if (resolved.startsWith(resolvedHome + sep)) {
return `~${resolved.slice(resolvedHome.length)}`;
}

return resolved;
}

export function truncateMiddle(str: string, maxLength: number): string {
if (str.length <= maxLength) {
return str;
}

const ellipsis = '...';
if (maxLength <= ellipsis.length) {
return str.slice(0, Math.max(0, maxLength));
}

const keepStart = Math.ceil((maxLength - ellipsis.length) / 2);
const keepEnd = Math.floor((maxLength - ellipsis.length) / 2);

return str.slice(0, keepStart) + ellipsis + str.slice(str.length - keepEnd);
}
16 changes: 16 additions & 0 deletions source/wizards/steps/location-step.spec.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import test from 'ava';
import {renderWithTheme as render} from '@/test-utils/render-with-theme';
import React from 'react';
import {homeRelative} from '@/utils/path';
import {LocationStep} from './location-step.js';

// ============================================================================
Expand Down Expand Up @@ -37,6 +38,21 @@ test('LocationStep shows global config option', t => {
t.regex(output!, /Global user config/);
});

test('LocationStep shows the resolved project path next to the option', t => {
const {lastFrame} = render(
<LocationStep onComplete={() => {}} projectDir="/test/project" />,
);

const output = lastFrame();
t.truthy(output);
const lines = output!.split('\n');
const stemIndex = lines.findIndex(line =>
line.includes('Current project directory'),
);
t.true(stemIndex !== -1, 'expected to find the project directory stem');
t.is(lines[stemIndex + 1]?.trim(), homeRelative('/test/project'));
});

test('LocationStep shows tip about config types', t => {
const {lastFrame} = render(
<LocationStep onComplete={() => {}} projectDir="/test/project" />,
Expand Down
27 changes: 23 additions & 4 deletions source/wizards/steps/location-step.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {StyledSelectInput} from '@/components/ui/styled-select-input';
import {getColors} from '@/config';
import {getConfigPath} from '@/config/paths';
import {useResponsiveTerminal} from '@/hooks/useTerminalWidth';
import {homeRelative, truncateMiddle} from '@/utils/path';

export type ConfigLocation = 'project' | 'global';

Expand All @@ -21,14 +22,16 @@ interface LocationOption {
value: ConfigLocation;
}

const LABEL_PATH_SEPARATOR = '\u0000';

export function LocationStep({
onComplete,
onBack,
projectDir,
configFileName = 'agents.config.json',
}: LocationStepProps) {
const colors = getColors();
const {isNarrow, truncatePath} = useResponsiveTerminal();
const {actualWidth, isNarrow} = useResponsiveTerminal();
const projectPath = join(projectDir, configFileName);
const globalPath = join(getConfigPath(), configFileName);

Expand All @@ -51,11 +54,11 @@ export function LocationStep({

const locationOptions: LocationOption[] = [
{
label: `Global user config`,
label: `Global user config${LABEL_PATH_SEPARATOR}${homeRelative(getConfigPath())}`,
value: 'global',
},
{
label: `Current project directory`,
label: `Current project directory${LABEL_PATH_SEPARATOR}${homeRelative(projectDir)}`,
value: 'project',
},
];
Expand Down Expand Up @@ -101,7 +104,7 @@ export function LocationStep({
Configuration found at:{' '}
</Text>
<Text color={colors.secondary}>
{isNarrow ? truncatePath(existingPath, 40) : existingPath}
{isNarrow ? truncateMiddle(existingPath, 40) : existingPath}
</Text>
</Box>
<StyledSelectInput
Expand Down Expand Up @@ -134,6 +137,22 @@ export function LocationStep({
<StyledSelectInput
items={locationOptions}
onSelect={(item: LocationOption) => handleLocationSelect(item)}
itemComponent={({isSelected, label}) => {
const [stem, path] = label.split(LABEL_PATH_SEPARATOR);
const color = isSelected ? colors.primary : colors.text;
const pathBudget = Math.max(10, Math.min(76, actualWidth - 4));
return (
<Box flexDirection="column">
<Text color={color} wrap="truncate-end">
{stem}
</Text>
<Text color={colors.secondary} wrap="truncate-end">
{' '}
{truncateMiddle(path, pathBudget)}
</Text>
</Box>
);
}}
/>
{!isNarrow && (
<Box marginTop={1}>
Expand Down
Loading