feat(pi): filter the project picker instead of scrolling hundreds of rows
Some checks failed
CI/CD / typecheck (pull_request) Successful in 1m10s
CI/CD / test (pull_request) Successful in 1m22s
CI/CD / lint (pull_request) Successful in 2m45s
CI/CD / smoke (pull_request) Failing after 3m35s
CI/CD / build (pull_request) Successful in 2m7s
CI/CD / publish (pull_request) Has been skipped
Some checks failed
CI/CD / typecheck (pull_request) Successful in 1m10s
CI/CD / test (pull_request) Successful in 1m22s
CI/CD / lint (pull_request) Successful in 2m45s
CI/CD / smoke (pull_request) Failing after 3m35s
CI/CD / build (pull_request) Successful in 2m7s
CI/CD / publish (pull_request) Has been skipped
pi's selector is a plain arrow-key list: `ExtensionUIDialogOptions` has no search field and `ExtensionSelectorComponent` ignores typed characters, so filtering has to happen before the list is handed over. With 356 projects — most of them `smoke-proj-none-*` leftovers — arrowing to the one you want is hopeless. Above 20 projects the picker now asks for a filter first. Terms are space-separated and all must match as case-insensitive substrings, so `home auto` finds `homeautomation`. Blank shows everything, Esc cancels. The active project sorts first (most likely pick), then alphabetical. A result set over 50 is capped, and the title says what was dropped — a silently truncated list reads as "that's all of them". The ordering and matching are extracted into an exported `filterProjects` so they are unit-tested rather than eyeballed through a TUI. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017BMXdb2qZbPSh8Q7XpTyjB
This commit is contained in:
73
src/cli/tests/config/pi-project-filter.test.ts
Normal file
73
src/cli/tests/config/pi-project-filter.test.ts
Normal file
@@ -0,0 +1,73 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { filterProjects } from '../../../pi-ext/mcpctl-pi.js';
|
||||
|
||||
/**
|
||||
* pi's selector is a plain arrow-key list — no search — so the project picker
|
||||
* filters before handing the list over. Real installs run to hundreds of
|
||||
* projects, where scrolling is hopeless.
|
||||
*/
|
||||
const PROJECTS = [
|
||||
'smoke-proj-none-mohimh46',
|
||||
'homeautomation',
|
||||
'docmost',
|
||||
'copy-homeautomation',
|
||||
'labctl',
|
||||
'mcpctl-development',
|
||||
'sre',
|
||||
];
|
||||
|
||||
describe('filterProjects', () => {
|
||||
it('puts the active project first, then sorts alphabetically', () => {
|
||||
expect(filterProjects(PROJECTS, '', 'labctl')).toEqual([
|
||||
'labctl',
|
||||
'copy-homeautomation',
|
||||
'docmost',
|
||||
'homeautomation',
|
||||
'mcpctl-development',
|
||||
'smoke-proj-none-mohimh46',
|
||||
'sre',
|
||||
]);
|
||||
});
|
||||
|
||||
it('sorts alphabetically when nothing is active', () => {
|
||||
expect(filterProjects(PROJECTS, '', null)[0]).toBe('copy-homeautomation');
|
||||
});
|
||||
|
||||
it('keeps everything for a blank or whitespace query', () => {
|
||||
expect(filterProjects(PROJECTS, '', null)).toHaveLength(PROJECTS.length);
|
||||
expect(filterProjects(PROJECTS, ' ', null)).toHaveLength(PROJECTS.length);
|
||||
});
|
||||
|
||||
it('matches case-insensitive substrings', () => {
|
||||
expect(filterProjects(PROJECTS, 'HOMEAUTO', null)).toEqual([
|
||||
'copy-homeautomation',
|
||||
'homeautomation',
|
||||
]);
|
||||
});
|
||||
|
||||
it('requires every space-separated term to match', () => {
|
||||
// "home auto" finds homeautomation even though the terms aren't adjacent...
|
||||
expect(filterProjects(PROJECTS, 'home auto', null)).toEqual([
|
||||
'copy-homeautomation',
|
||||
'homeautomation',
|
||||
]);
|
||||
// ...and a term that matches nothing eliminates the row.
|
||||
expect(filterProjects(PROJECTS, 'home zzz', null)).toEqual([]);
|
||||
});
|
||||
|
||||
it('narrows the smoke-test noise that motivated this', () => {
|
||||
const many = [...PROJECTS, ...Array.from({ length: 300 }, (_, i) => `smoke-proj-none-x${String(i)}`)];
|
||||
expect(filterProjects(many, 'smoke', null)).toHaveLength(301);
|
||||
expect(filterProjects(many, 'ctl', null)).toEqual(['labctl', 'mcpctl-development']);
|
||||
});
|
||||
|
||||
it('leaves the caller with an empty list rather than a bad match', () => {
|
||||
expect(filterProjects(PROJECTS, 'nosuchproject', null)).toEqual([]);
|
||||
});
|
||||
|
||||
it('does not mutate the input', () => {
|
||||
const input = [...PROJECTS];
|
||||
filterProjects(input, 'home', 'sre');
|
||||
expect(input).toEqual(PROJECTS);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user