Skip to content

Commit ad5125d

Browse files
committed
Mark bundled plugin rows with a mountain in listings
1 parent bf79cc0 commit ad5125d

11 files changed

Lines changed: 268 additions & 14 deletions

src/plugins/origin-marker.test.ts

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import { describe, expect, test } from "bun:test";
2+
3+
import {
4+
BUNDLED_PLUGIN_MARKER,
5+
pluginOriginMarker,
6+
withOriginMarker,
7+
} from "./origin-marker";
8+
9+
describe("pluginOriginMarker", () => {
10+
test("bundled repo plugins get the mountain marker", () => {
11+
expect(pluginOriginMarker("repo")).toBe(BUNDLED_PLUGIN_MARKER);
12+
});
13+
14+
test("other origins render as their origin label", () => {
15+
expect(pluginOriginMarker("user")).toBe("[user]");
16+
expect(pluginOriginMarker("project")).toBe("[project]");
17+
expect(pluginOriginMarker("path")).toBe("[path]");
18+
});
19+
20+
test("rows without an origin stay unmarked", () => {
21+
expect(pluginOriginMarker(undefined)).toBe("");
22+
});
23+
});
24+
25+
describe("withOriginMarker", () => {
26+
test("appends the marker after the label", () => {
27+
expect(withOriginMarker("/implement", "repo")).toBe(
28+
`/implement ${BUNDLED_PLUGIN_MARKER}`,
29+
);
30+
expect(withOriginMarker("exa — enabled", "user")).toBe(
31+
"exa — enabled [user]",
32+
);
33+
});
34+
35+
test("leaves unmarked labels alone", () => {
36+
expect(withOriginMarker("/help", undefined)).toBe("/help");
37+
});
38+
});

src/plugins/origin-marker.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import type { PluginOrigin } from "../trust/project-trust.js";
2+
3+
/**
4+
* Inline marker for a bundled (origin "repo") Corbits plugin row. The brand
5+
* mark itself is a multi-cell canvas silhouette (`tui/mark-shape.ts`), not a
6+
* single text glyph, and `●` already means live work in chrome state — so
7+
* rows use the mountain the issue asks for.
8+
*/
9+
export const BUNDLED_PLUGIN_MARKER = "⛰";
10+
11+
/** Short marker naming a plugin row's discovery origin for list display. */
12+
export function pluginOriginMarker(origin: PluginOrigin | undefined): string {
13+
if (origin === undefined) return "";
14+
return origin === "repo" ? BUNDLED_PLUGIN_MARKER : `[${origin}]`;
15+
}
16+
17+
/** Append the origin marker to a row label, leaving unmarked labels alone. */
18+
export function withOriginMarker(
19+
label: string,
20+
origin: PluginOrigin | undefined,
21+
): string {
22+
const marker = pluginOriginMarker(origin);
23+
return marker === "" ? label : `${label} ${marker}`;
24+
}

src/plugins/register.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -76,8 +76,10 @@ export function registerCommandPluginModule(
7676
if (!isCommandPluginModule(mod)) return false;
7777
const commandPlugin = mod.commandPlugin;
7878
if (commandPlugin === undefined) return false;
79-
registerCommandPlugin(commandPlugin, () =>
80-
isPluginModuleEnabled(mod, getConfig()),
79+
registerCommandPlugin(
80+
commandPlugin,
81+
() => isPluginModuleEnabled(mod, getConfig()),
82+
mod.origin,
8183
);
8284
return true;
8385
}

src/tui/command-catalog.test.ts

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,11 @@ import { describe, expect, test } from "bun:test";
22
import {
33
commandItemsFromRegistry,
44
filterPaletteCommands,
5+
formatPaletteRows,
56
paletteLabels,
67
} from "./command-catalog";
8+
import { BUNDLED_PLUGIN_MARKER } from "../plugins/origin-marker.js";
9+
import { stringWidth } from "./view/height.js";
710

811
describe("commandItemsFromRegistry", () => {
912
test("maps listCommands-shaped entries to name-only `/` labels", () => {
@@ -78,3 +81,42 @@ describe("paletteLabels", () => {
7881
expect(paletteLabels(catalog)).toEqual(["/tasks"]);
7982
});
8083
});
84+
85+
describe("command origin markers", () => {
86+
test("bundled repo rows carry the mountain, other origins their label", () => {
87+
const items = commandItemsFromRegistry([
88+
{ name: "implement", description: "Bundled command", origin: "repo" },
89+
{ name: "mine", description: "Marketplace command", origin: "user" },
90+
{ name: "proj", description: "Project command", origin: "project" },
91+
{ name: "local", description: "Path command", origin: "path" },
92+
{ name: "help", description: "Built-in" },
93+
]);
94+
expect(paletteLabels(items)).toEqual([
95+
`/implement ${BUNDLED_PLUGIN_MARKER}`,
96+
"/mine [user]",
97+
"/proj [project]",
98+
"/local [path]",
99+
"/help",
100+
]);
101+
});
102+
103+
test("marked rows still filter by command name", () => {
104+
const catalog = commandItemsFromRegistry([
105+
{ name: "implement", description: "Bundled command", origin: "repo" },
106+
]);
107+
expect(filterPaletteCommands("implem", catalog).map((c) => c.id)).toEqual([
108+
"implement",
109+
]);
110+
});
111+
112+
test("a marked row still formats to exactly the target width", () => {
113+
const catalog = commandItemsFromRegistry([
114+
{ name: "implement", description: "Bundled command", origin: "repo" },
115+
]);
116+
for (const width of [16, 24, 40]) {
117+
const rows = formatPaletteRows(paletteLabels(catalog), width);
118+
expect(rows).toHaveLength(1);
119+
expect(stringWidth(rows[0] ?? "")).toBe(width);
120+
}
121+
});
122+
});

src/tui/command-catalog.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,16 @@
77
* setPaletteCatalog(shell, () => commandItemsFromRegistry(listCommands()))
88
*/
99

10+
import { withOriginMarker } from "../plugins/origin-marker.js";
11+
import type { PluginOrigin } from "../trust/project-trust.js";
1012
import { sliceToWidth, stringWidth } from "./view/height.js";
1113

1214
/** Minimal registry shape — matches `listCommands()` entries without importing them. */
1315
export interface RegistryCommandSource {
1416
readonly name: string;
1517
readonly description: string;
18+
/** Discovery origin of the contributing plugin, when the command has one. */
19+
readonly origin?: PluginOrigin;
1620
}
1721

1822
/** One entry in the `/` command list: registry command name + display label. */
@@ -33,8 +37,9 @@ export function commandItemsFromRegistry(
3337
id: c.name,
3438
// Name-only rows keep the slash popup scannable; description is a
3539
// dedicated field for the overlay zone and stays in keywords so typed
36-
// filter still finds prose matches.
37-
label: `/${c.name}`,
40+
// filter still finds prose matches. Plugin rows carry their origin
41+
// marker (mountain for bundled, origin label otherwise).
42+
label: withOriginMarker(`/${c.name}`, c.origin),
3843
description: c.description,
3944
keywords: [c.name, c.description, "slash", "command"],
4045
}));

src/tui/command-surfaces.test.ts

Lines changed: 54 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import {
2323
import type { KeyEvent } from "@opentui/core";
2424

2525
import { focusOwner } from "./focus/index.js";
26+
import { BUNDLED_PLUGIN_MARKER as BUNDLED_MARKER } from "../plugins/origin-marker.js";
2627
import { withTestRenderer, type Harness } from "./harness";
2728
import { projectPluginsRoot, userPluginsRoot } from "../plugins/uninstall.js";
2829
import { createAppShell } from "./shell/index";
@@ -109,7 +110,7 @@ describe("surface labels", () => {
109110
credentialValues: {},
110111
origin: "project",
111112
};
112-
expect(pluginRowLabel(entry)).toBe("linear — untrusted");
113+
expect(pluginRowLabel(entry)).toBe("linear — untrusted [project]");
113114
expect(
114115
pluginRowLabel({
115116
id: "b",
@@ -119,7 +120,7 @@ describe("surface labels", () => {
119120
credentialValues: {},
120121
origin: "user",
121122
}),
122-
).toBe("exa — enabled");
123+
).toBe("exa — enabled [user]");
123124
});
124125

125126
test("mcp label reports disabled without a tool count", () => {
@@ -141,7 +142,24 @@ describe("surface labels", () => {
141142
'agent a: skill "style" referenced but not found in skill search path',
142143
],
143144
}),
144-
).toBe("agents — enabled — has warnings");
145+
).toBe("agents — enabled — has warnings [user]");
146+
});
147+
148+
test("plugin label marks bundled and non-bundled origins differently", () => {
149+
const entry = (origin: PluginEntry["origin"], name = "repo-plugin") =>
150+
pluginRowLabel({
151+
id: name,
152+
name,
153+
enabled: true,
154+
credentials: [],
155+
credentialValues: {},
156+
origin,
157+
});
158+
expect(entry("repo")).toBe(`repo-plugin — enabled ${BUNDLED_MARKER}`);
159+
expect(entry("user", "market-plugin")).toBe(
160+
"market-plugin — enabled [user]",
161+
);
162+
expect(entry("path", "local-plugin")).toBe("local-plugin — enabled [path]");
145163
});
146164
});
147165

@@ -413,6 +431,39 @@ describe("plugins surface", () => {
413431
expect(shell.overlayItems[0]).toBe("linear — enabled");
414432
});
415433
});
434+
435+
test("marks bundled rows with the mountain and other origins by label", async () => {
436+
await withShell(async (shell) => {
437+
const deps: CommandSurfaceDeps = {
438+
notify: () => undefined,
439+
plugins: {
440+
list: () => [
441+
{
442+
id: "bundled",
443+
name: "bundled",
444+
enabled: true,
445+
credentials: [],
446+
credentialValues: {},
447+
origin: "repo",
448+
},
449+
{
450+
id: "market",
451+
name: "market",
452+
enabled: true,
453+
credentials: [],
454+
credentialValues: {},
455+
origin: "user",
456+
},
457+
],
458+
} as unknown as PluginsSurfaceDeps,
459+
};
460+
openCommandSurface(shell, "plugins", deps);
461+
expect(shell.overlayItems.slice(0, 2)).toEqual([
462+
`bundled — enabled ${BUNDLED_MARKER}`,
463+
"market — enabled [user]",
464+
]);
465+
});
466+
});
416467
});
417468

418469
function key(name: string): KeyEvent {

src/tui/command-surfaces.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010

1111
import { isAbsoluteHTTPURL, validateMCPServerName } from "../mcp/add-server.js";
1212
import { formatPluginWarningsSummary } from "../plugins/diagnostics.js";
13+
import { withOriginMarker } from "../plugins/origin-marker.js";
1314
import type { PluginOrigin } from "../plugins/admin.js";
1415
import {
1516
classifyPluginRemove,
@@ -283,9 +284,12 @@ export function pluginRowLabel(entry: PluginEntry): string {
283284
: pluginHasWarnings(entry)
284285
? "has warnings"
285286
: entry.kind;
286-
return blocker
287-
? `${entry.name}${state}${blocker}`
288-
: `${entry.name}${state}`;
287+
return withOriginMarker(
288+
blocker
289+
? `${entry.name}${state}${blocker}`
290+
: `${entry.name}${state}`,
291+
entry.origin,
292+
);
289293
}
290294

291295
function pluginNeedsDiskConfirm(

src/tui/commands/registry.test.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -274,6 +274,40 @@ describe("registerCommandPlugin", () => {
274274
"built-in",
275275
);
276276
});
277+
278+
it("surfaces the winning candidate's plugin origin via listCommands", () => {
279+
registerCommandPlugin(
280+
{
281+
commands: [
282+
{
283+
name: "origin-marked-cmd",
284+
description: "bundled",
285+
handler: () => ({ type: "noop" }),
286+
},
287+
],
288+
},
289+
() => true,
290+
"repo",
291+
);
292+
registerCommandPlugin({
293+
commands: [
294+
{
295+
name: "unmarked-plugin-cmd",
296+
description: "no origin",
297+
handler: () => ({ type: "noop" }),
298+
},
299+
],
300+
});
301+
302+
expect(
303+
listCommands().find((command) => command.name === "origin-marked-cmd")
304+
?.pluginOrigin,
305+
).toBe("repo");
306+
expect(
307+
listCommands().find((command) => command.name === "unmarked-plugin-cmd")
308+
?.pluginOrigin,
309+
).toBeUndefined();
310+
});
277311
});
278312

279313
describe("setHiddenCommands", () => {

src/tui/commands/registry.ts

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import type { CostSummary } from "../../cost/cost-summary.js";
2+
import type { PluginOrigin } from "../../trust/project-trust.js";
23

34
export interface CommandContext {
45
signalClear: () => void;
@@ -57,6 +58,11 @@ export interface SubcommandDefinition {
5758
export interface CommandDefinition {
5859
name: string;
5960
description: string;
61+
/**
62+
* Discovery origin of the plugin that contributed this command, when the
63+
* command came from a plugin. Built-ins leave it unset and render unmarked.
64+
*/
65+
pluginOrigin?: PluginOrigin;
6066
/**
6167
* Claude Code–compatible free-form arg guidance (frontmatter `argument-hint`).
6268
* Shown greyed next to the command and after `/cmd ` until the operator types.
@@ -78,6 +84,7 @@ export interface CommandPlugin {
7884
interface PluginCommandCandidate {
7985
command: CommandDefinition;
8086
isActive: () => boolean;
87+
origin?: PluginOrigin;
8188
}
8289

8390
const registry = new Map<string, CommandDefinition>();
@@ -94,10 +101,15 @@ export function registerCommand(def: CommandDefinition): void {
94101
export function registerCommandPlugin(
95102
plugin: CommandPlugin,
96103
isActive: () => boolean = () => true,
104+
origin?: PluginOrigin,
97105
): void {
98106
for (const cmd of plugin.commands) {
99107
const candidates = pluginCandidates.get(cmd.name) ?? [];
100-
candidates.push({ command: cmd, isActive });
108+
candidates.push({
109+
command: cmd,
110+
isActive,
111+
...(origin !== undefined ? { origin } : {}),
112+
});
101113
pluginCandidates.set(cmd.name, candidates);
102114
}
103115
}
@@ -118,8 +130,15 @@ export function listCommands(): CommandDefinition[] {
118130
const commands = [...registry.values()];
119131
for (const name of pluginCandidates.keys()) {
120132
if (registry.has(name)) continue;
121-
const command = getCommand(name);
122-
if (command !== undefined) commands.push(command);
133+
const winner = pluginCandidates
134+
.get(name)
135+
?.find((candidate) => candidate.isActive());
136+
if (winner === undefined) continue;
137+
commands.push(
138+
winner.origin === undefined
139+
? winner.command
140+
: { ...winner.command, pluginOrigin: winner.origin },
141+
);
123142
}
124143
return commands
125144
.filter(

0 commit comments

Comments
 (0)