feat(opencode): leader keybind, explicit unmount on switch, non-wrapping indicator
Three improvements taken from reading the sibling opencode branches (feat/opencode-extension-abhishek in particular): - `<leader>m` opens the project picker. Switching is the repeated action and typing `/mcpctl` every time is friction; the other two commands stay palette-only. - A switch disconnects before re-adding. `mcp.add` under the same name does re-point the tools on its own, but leaves it to opencode whether the previous client is closed — and an abandoned one keeps its `mcp-session-id` alive on mcplocal, which is exactly what holds a gated project open. Best-effort, so a first mount still works. - The footer label renders `wrapMode="none" truncate`. The home prompt row is narrow enough that the default wrap broke `mcpctl:homeautomation` across two lines mid-word; clipping the tail of a long name reads far better. Verified against opencode 1.18.15: ctrl-x m opens the picker, the home footer is now one line, and a disconnect-then-add switch still lands — the model called `mcpctl_begin_session` and listed the new project's tools. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BVwuCjuMoA13gmzYEfcrNP
This commit is contained in:
File diff suppressed because one or more lines are too long
@@ -67,6 +67,22 @@ describe('embedded opencode plugins', () => {
|
||||
expect(OPENCODE_TUI_PLUGIN_SOURCE).toContain("'--agent', 'opencode'");
|
||||
});
|
||||
|
||||
it('bind the switcher to a chord as well as a slash command', () => {
|
||||
// Switching is the repeated action; typing /mcpctl every time is friction.
|
||||
expect(OPENCODE_TUI_PLUGIN_SOURCE).toContain("slashName: 'mcpctl'");
|
||||
expect(OPENCODE_TUI_PLUGIN_SOURCE).toContain("key: '<leader>m'");
|
||||
});
|
||||
|
||||
it('tear the outgoing mount down before re-pointing it', () => {
|
||||
// An abandoned client keeps its mcp-session-id — and a gated project's
|
||||
// unlocked state — alive on mcplocal.
|
||||
expect(OPENCODE_TUI_PLUGIN_SOURCE).toContain('mcp.disconnect({ name: SERVER_NAME })');
|
||||
});
|
||||
|
||||
it('clip rather than wrap the footer label on the narrow home prompt', () => {
|
||||
expect(OPENCODE_TUI_PLUGIN_SOURCE).toContain('wrapMode="none"');
|
||||
});
|
||||
|
||||
it('switch without rewriting the plugin file opencode has already loaded', () => {
|
||||
expect(OPENCODE_TUI_PLUGIN_SOURCE).toContain("'--skip-plugin'");
|
||||
expect(OPENCODE_TUI_PLUGIN_SOURCE).toContain("'--skip-marker'");
|
||||
|
||||
@@ -134,6 +134,16 @@ const tui = async (api: TuiPluginApi): Promise<void> => {
|
||||
const token = state.tokens?.[project] ?? '';
|
||||
const headers: Record<string, string> = {};
|
||||
if (token !== '') headers['Authorization'] = `Bearer ${token}`;
|
||||
// Tear the outgoing mount down explicitly. `mcp.add` under the same name
|
||||
// does re-point the tools, but leaves it to opencode whether the previous
|
||||
// client is closed — and an abandoned one keeps its `mcp-session-id` alive
|
||||
// on mcplocal, which is what holds a gated project open. Best-effort: on a
|
||||
// first mount there is nothing to disconnect.
|
||||
try {
|
||||
await api.client.mcp.disconnect({ name: SERVER_NAME });
|
||||
} catch {
|
||||
/* not mounted yet */
|
||||
}
|
||||
await api.client.mcp.add({
|
||||
name: SERVER_NAME,
|
||||
config: {
|
||||
@@ -266,6 +276,11 @@ const tui = async (api: TuiPluginApi): Promise<void> => {
|
||||
},
|
||||
},
|
||||
],
|
||||
// Switching is the thing you do repeatedly, so it gets a chord as well as
|
||||
// `/mcpctl`. The other two commands stay palette-only — they are occasional.
|
||||
bindings: [
|
||||
{ key: '<leader>m', group: 'mcpctl', desc: 'switch mcpctl project', cmd: 'mcpctl.switch' },
|
||||
],
|
||||
});
|
||||
|
||||
// The indicator. `session_prompt_right` and `home_prompt_right` are the only
|
||||
@@ -274,8 +289,13 @@ const tui = async (api: TuiPluginApi): Promise<void> => {
|
||||
// above the token counter. (`home_footer` would sit on the counter's line but
|
||||
// *replaces* the cwd/version footer rather than adding to it, and
|
||||
// `app_bottom` costs a whole extra terminal row.)
|
||||
// wrapMode="none" + truncate: the home prompt row is narrow, and the default
|
||||
// wrap breaks "mcpctl:homeautomation" across two lines mid-word. Clipping the
|
||||
// tail of a long name reads far better than a two-line footer.
|
||||
const Indicator = (): JSX.Element => (
|
||||
<text fg={api.theme.current.textMuted}>{api.kv.get(KV_LABEL, indicatorLabel(null))}</text>
|
||||
<text fg={api.theme.current.textMuted} wrapMode="none" truncate>
|
||||
{api.kv.get(KV_LABEL, indicatorLabel(null))}
|
||||
</text>
|
||||
);
|
||||
api.slots.register({
|
||||
order: 100,
|
||||
|
||||
Reference in New Issue
Block a user