Compare commits

...

10 Commits

Author SHA1 Message Date
Michal
90c49bcb22 refactor(prime-agent): drop the widget fallback, keep the tray status
With prime-agent-extension-status.patch in place the tray renders
ctx.ui.setStatus() next to the model name, which is what a status line should
be. The widget was a workaround for its absence and was never a substitute:
widgetContainerBelow sits in the fullscreen *scroll* list, not the dock, so it
scrolled away with the transcript, and with both set the project name appeared
twice.

The startup retries stay: resetExtensionUI() clears extension statuses just as
it cleared widgets, so the value set during session_start is still wiped before
it can be seen.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017BMXdb2qZbPSh8Q7XpTyjB
2026-08-08 19:59:24 +01:00
Michal
0a29c2fd7f fix(prime-agent): re-publish the indicator after prime-agent clears extension widgets
Verified with a probe extension rather than by reading the bundle: session_start
fires with hasUI=true, ctx.ui.setWidget exists and the call returns without
throwing — and the widget still never appeared.

Cause is prime-agent wiping it immediately afterwards. resetExtensionUI() ->
clearExtensionWidgets() runs from onBeforeSessionInvalidate and from the
connection-state-snapshot handler, both of which land after session_start, so
the indicator was set and cleared before it could be seen. Nothing re-set it
until a turn, which is why a fresh session with no messages showed nothing.

Re-publishes at 1s/3s/6s after session_start to land past that reset.
setWidget is idempotent, so a redundant retry costs one re-render.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017BMXdb2qZbPSh8Q7XpTyjB
2026-08-08 19:52:07 +01:00
Michal
3061435399 Merge 'fix(prime-agent): project indicator on turn_start' into main
Some checks failed
CI/CD / lint (push) Successful in 1m3s
CI/CD / typecheck (push) Successful in 1m4s
CI/CD / test (push) Successful in 3m26s
CI/CD / smoke (push) Failing after 3m23s
CI/CD / build (push) Successful in 2m11s
CI/CD / publish (push) Has been skipped
2026-08-08 18:38:53 +01:00
Michal
a1893a7578 fix(prime-agent): publish the project indicator on turn_start, not only session_start
prime-agent emits session_start from exactly one place — reload() — and never
at startup. A session_start-only handler therefore never ran on a fresh
session, and the '/mcpctl -> already on X' path returns without reloading, so
the indicator stayed blank exactly when it was most wanted.

Also guard against the no-op UI context: the runtime hands extensions
noOpUIContext until the TUI binds the real one, and every setter on it
silently discards. Publishing under it would cache a label that never
rendered.

Now published on session_start (reload), turn_start (earliest reliable point
on a fresh session), and on entering /mcpctl. Repeat calls are a no-op unless
the label changed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017BMXdb2qZbPSh8Q7XpTyjB
2026-08-08 18:38:52 +01:00
Michal
64503e8924 Merge pull request 'feat(prime-agent): filterable project picker + visible active-project indicator' (#97) from feat/prime-agent-switcher-ux into main
Some checks failed
CI/CD / lint (push) Successful in 1m4s
CI/CD / typecheck (push) Successful in 2m21s
CI/CD / test (push) Successful in 1m20s
CI/CD / smoke (push) Has started running
CI/CD / build (push) Has started running
CI/CD / publish (push) Has been cancelled
2026-08-08 18:33:30 +01:00
Michal
ba05b2865c 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
2026-08-08 18:33:23 +01:00
Michal
3f93f0798f Merge pull request 'feat(cli): show the active mcpctl project in prime-agent's footer' (#96) from feat/mcpctl-switcher-status into main
Some checks failed
CI/CD / lint (push) Successful in 1m4s
CI/CD / typecheck (push) Successful in 2m18s
CI/CD / test (push) Successful in 1m18s
CI/CD / build (push) Successful in 2m13s
CI/CD / smoke (push) Failing after 3m24s
CI/CD / publish (push) Has been skipped
2026-08-08 12:18:22 +01:00
Michal
ce7df10e06 feat(cli): show the active mcpctl project in prime-agent's footer
`/mcpctl` could switch projects but there was no way to see which one was
active without running a command. prime-agent exposes the same status-bar
channel the model name uses (`ctx.ui.setStatus`), so the switcher now
publishes `mcpctl:<project>` there.

Wired to `session_start`, which fires on startup *and* on every reload —
including the reload the switch itself triggers — so the footer tracks
settings.json without extra bookkeeping. Cleared when no project is mounted.

Also fixes `notify(..., 'success')`: the API only accepts info|warning|error.
Not a live bug (prime-agent falls through to the same showStatus path as
'info') but it fails a typecheck of the extension against the real
ExtensionAPI, which is how it was found.

The extension ships as a JSON-escaped string and is never compiled by our
build, so it was typechecked out-of-tree against
@earendil-works/pi-coding-agent's published types.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017BMXdb2qZbPSh8Q7XpTyjB
2026-08-08 12:18:21 +01:00
Michal
25c1bffdd0 Merge pull request 'fix(cli): harden config prime-agent sync, auth + /mcpctl switcher' (#95) from feat/config-prime-agent into main
Some checks failed
CI/CD / typecheck (push) Successful in 1m5s
CI/CD / lint (push) Successful in 2m11s
CI/CD / test (push) Successful in 1m20s
CI/CD / smoke (push) Failing after 1m53s
CI/CD / build (push) Successful in 4m7s
CI/CD / publish (push) Has been skipped
2026-08-08 12:02:08 +01:00
d56a074733 Merge pull request 'feat(cli): add mcpctl config prime-agent — proxy MCP + skills sync for prime-agent' (#93) from feat/config-prime-agent into main
Some checks failed
CI/CD / lint (push) Successful in 1m6s
CI/CD / typecheck (push) Successful in 2m9s
CI/CD / test (push) Successful in 1m21s
CI/CD / smoke (push) Failing after 1m53s
CI/CD / build (push) Successful in 4m16s
CI/CD / publish (push) Has been skipped
Reviewed-on: #93
2026-08-08 09:19:32 +00:00
2 changed files with 54 additions and 1 deletions

File diff suppressed because one or more lines are too long

View File

@@ -510,6 +510,59 @@ describe('config prime-agent', () => {
expect(exceptionSafeRead(join(tmpDir, '.mcpctl-project'))).toBeNull();
});
it('the installed switcher extension publishes the active project to the footer', 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');
// Footer status, refreshed on startup and on every reload (which is what
// the switch itself triggers) — the mcpctl equivalent of the model name.
expect(ext).toContain("pi.on('session_start'");
expect(ext).toContain('ctx.ui.setStatus(STATUS_KEY');
expect(ext).toContain('`mcpctl:${active}`');
// notify() only accepts info|warning|error — 'success' is not a valid type.
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');
// Both hosts render statuses next to the model name (pi in its footer,
// prime-agent in the tray — the latter via prime-agent-extension-status.patch).
expect(ext).toContain('ctx.ui.setStatus(STATUS_KEY');
// The widget was a workaround for the unpatched tray; it scrolled away with
// the transcript, so it is not a status line and must not come back.
expect(ext).not.toContain('setWidget');
// prime-agent emits session_start only from reload(), never at startup, so
// a session_start-only indicator stays blank until the first switch.
expect(ext).toContain("pi.on('turn_start'");
});
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(