feat(prime-agent): filterable project picker + a visible active-project indicator

Two gaps the pi switcher had already closed.

**The indicator was invisible.** `ctx.ui.setStatus` was the wrong channel:
prime-agent stores extension statuses (`FooterDataProvider.setExtensionStatus`)
but nothing ever reads them back — there is no `getExtensionStatuses()` call
site anywhere in the app, so the value was recorded and never rendered. pi's
`footer.js` does render them, which is why the same code showed a status line
there and nothing here. The active project is now published as a *widget*
(`extensionWidgetsBelow` → `renderWidgets()`), which prime-agent does render.
`setStatus` is still called so pi keeps its footer entry.

**No search.** With 356 projects the picker was a wall of
`smoke-mcptoken-*`. Above 20 projects it now asks for a filter first;
space-separated terms must all match, case-insensitively, against name or
description, so `home auto` finds `homeautomation`. Active project sorts
first, then alphabetical.

Deliberately no client-side truncation here, unlike the pi picker: pi's
`ExtensionSelectorComponent.updateList()` renders every option, so a long list
fills the screen and has to be capped. prime-agent's selector windows the list
itself and shows a true `(20/356)` counter — capping would replace an accurate
total with a misleading one.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017BMXdb2qZbPSh8Q7XpTyjB
This commit is contained in:
Michal
2026-08-08 18:33:23 +01:00
parent 3f93f0798f
commit ba05b2865c
2 changed files with 33 additions and 1 deletions

File diff suppressed because one or more lines are too long

View File

@@ -528,6 +528,38 @@ describe('config prime-agent', () => {
expect(ext).not.toContain("'success'");
});
it('the installed switcher shows the active project via a widget, not just setStatus', async () => {
const settingsPath = join(tmpDir, 'settings.json');
const cmd = createConfigCommand(
{ configDeps: { configDir: tmpDir }, log },
{ client, credentialsDeps: { configDir: tmpDir }, log },
);
await cmd.parseAsync(['prime-agent', '--project', 'ha', '-o', settingsPath, '--skip-skills', '--token', 'mcpctl_pat_x'], { from: 'user' });
const ext = readFileSync(join(tmpDir, 'extensions', 'mcpctl-switch.ts'), 'utf-8');
// prime-agent stores extension statuses but never renders them, so a
// setStatus-only indicator is invisible there. Widgets are rendered.
expect(ext).toContain('ctx.ui.setWidget(STATUS_KEY');
expect(ext).toContain("placement: 'belowEditor'");
// Still set the status: pi's footer does render it.
expect(ext).toContain('ctx.ui.setStatus(STATUS_KEY');
});
it('the installed switcher filters the project list before showing it', async () => {
const settingsPath = join(tmpDir, 'settings.json');
const cmd = createConfigCommand(
{ configDeps: { configDir: tmpDir }, log },
{ client, credentialsDeps: { configDir: tmpDir }, log },
);
await cmd.parseAsync(['prime-agent', '--project', 'ha', '-o', settingsPath, '--skip-skills', '--token', 'mcpctl_pat_x'], { from: 'user' });
const ext = readFileSync(join(tmpDir, 'extensions', 'mcpctl-switch.ts'), 'utf-8');
// Hundreds of projects in a plain arrow-key list is unusable.
expect(ext).toContain('export function filterProjects');
expect(ext).toContain('ctx.ui.input(');
expect(ext).toContain('FILTER_THRESHOLD');
});
it('the installed switcher extension passes --skip-marker', async () => {
const settingsPath = join(tmpDir, 'settings.json');
const cmd = createConfigCommand(