Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 5 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,11 @@ Focused, independently installable plugins for [Herdr](https://herdr.dev/).

## Plugin catalogue

| Plugin | Description | Install |
| -------------------------------------------- | -------------------------------------------------------------- | -------------------------------------------------------------------- |
| [Tab Numbers](plugins/tab-numbers) | Adds each named tab's contiguous position to its label. | `herdr plugin install oullin/herdr-plugins/plugins/tab-numbers` |
| [Tmux Keybindings](plugins/tmux-keybindings) | Applies tmux-style bindings with a toggleable reference panel. | `herdr plugin install oullin/herdr-plugins/plugins/tmux-keybindings` |
| Plugin | Description | Install |
| ------------------------------------------------------ | ---------------------------------------------------------- | ------------------------------------------------------------------------- |
| [Tab Numbers](plugins/tab-numbers) | Adds each named tab's contiguous position to its label. | `herdr plugin install oullin/herdr-plugins/plugins/tab-numbers` |
| [Tmux Keybindings](plugins/tmux-keybindings) | Applies tmux-style bindings with a modal reference dialog. | `herdr plugin install oullin/herdr-plugins/plugins/tmux-keybindings` |
| [Pane Navigation Hints](plugins/pane-navigation-hints) | Shows live pane navigation shortcuts in every pane border. | `herdr plugin install oullin/herdr-plugins/plugins/pane-navigation-hints` |

Herdr installs plugins from repository subdirectories, so this repository has one marketplace listing while each plugin remains independently installable.

Expand Down
10 changes: 9 additions & 1 deletion package/core/src/herdr-cli/client/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { HerdrPaneClient } from '#herdr-plugin-core/herdr-cli/panes';
import { HerdrTabClient } from '#herdr-plugin-core/herdr-cli/tabs';
import { HerdrCliTransport } from '#herdr-plugin-core/herdr-cli/transport';
import { HerdrWorkspaceClient } from '#herdr-plugin-core/herdr-cli/workspaces';
import type { Environment, JsonObject, Pane, PluginPaneOptions, Tab, Workspace } from '#herdr-plugin-core/models';
import type { Environment, JsonObject, Pane, PaneTitleUpdate, PluginPaneOptions, Tab, Workspace } from '#herdr-plugin-core/models';
import type { HerdrClient } from '#herdr-plugin-core/ports';

export class HerdrCliClient implements HerdrClient {
Expand Down Expand Up @@ -54,10 +54,18 @@ export class HerdrCliClient implements HerdrClient {
this.config.reloadConfig();
}

listPanes(workspaceId?: string): readonly Pane[] {
return this.panes.listPanes(workspaceId);
}

getPane(paneId: string): Pane | undefined {
return this.panes.getPane(paneId);
}

reportPaneTitle(paneId: string, update: PaneTitleUpdate): void {
this.panes.reportPaneTitle(paneId, update);
}

openPluginPane(options: PluginPaneOptions): Pane {
return this.panes.openPluginPane(options);
}
Expand Down
29 changes: 24 additions & 5 deletions package/core/src/herdr-cli/panes/index.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { HerdrCommandError } from '#herdr-plugin-core/errors';
import type { HerdrCliTransport } from '#herdr-plugin-core/herdr-cli/transport';
import { isJsonObject } from '#herdr-plugin-core/json';
import type { Pane, PluginPaneOptions } from '#herdr-plugin-core/models';
import type { Pane, PaneTitleUpdate, PluginPaneOptions } from '#herdr-plugin-core/models';
import type { PaneClient } from '#herdr-plugin-core/ports';

export class HerdrPaneClient implements PaneClient {
Expand All @@ -11,15 +11,34 @@ export class HerdrPaneClient implements PaneClient {
this.transport = transport;
}

listPanes(workspaceId?: string): readonly Pane[] {
const panes = this.transport.call(['pane', 'list', ...(workspaceId === undefined ? [] : ['--workspace', workspaceId])])['panes'];

if (!Array.isArray(panes)) {
throw new HerdrCommandError('Herdr did not return a pane list');
}

return panes.map((pane) => this.parse(pane));
}

getPane(paneId: string): Pane | undefined {
const args = ['pane', 'get', paneId];
const result = this.transport.execute(args);

if (result.error || result.status !== 0) {
return undefined;
try {
return this.parse(this.transport.call(args)['pane']);
} catch (error) {
const commandError = error as HerdrCommandError;

if (commandError?.code === 'pane_not_found') {
return undefined;
}

throw error;
}
}

return this.parse(this.transport.decodeResult(result.stdout, args)['pane']);
reportPaneTitle(paneId: string, update: PaneTitleUpdate): void {
this.transport.run(['pane', 'report-metadata', paneId, '--source', update.source, ...('title' in update ? ['--title', update.title] : ['--clear-title'])]);
}

openPluginPane(options: PluginPaneOptions): Pane {
Expand Down
2 changes: 1 addition & 1 deletion package/core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ export { HerdrCommandError, type HerdrErrorOptions, PluginError } from '#herdr-p
export { executePlugin, type PluginExecutionOptions } from '#herdr-plugin-core/execution';
export { type HerdrApiError, HerdrCliClient, HerdrCliTransport, HerdrConfigClient, HerdrPaneClient, HerdrResponseDecoder, HerdrTabClient, HerdrWorkspaceClient } from '#herdr-plugin-core/herdr-cli';
export { isJsonObject } from '#herdr-plugin-core/json';
export type { Environment, JsonObject, Pane, PluginPaneOptions, Tab, Workspace } from '#herdr-plugin-core/models';
export type { Environment, JsonObject, Pane, PaneTitleUpdate, PluginPaneOptions, Tab, Workspace } from '#herdr-plugin-core/models';
export { HerdrConfigPathResolver, PluginStateDirectoryResolver } from '#herdr-plugin-core/path-resolvers';
export { AtomicFileStore, JsonFileStore } from '#herdr-plugin-core/persistence';
export { PluginContext } from '#herdr-plugin-core/plugin-context';
Expand Down
12 changes: 12 additions & 0 deletions package/core/src/models.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,18 @@ export interface Pane {
readonly workspace_id: string;
}

export type PaneTitleUpdate =
| {
readonly source: string;
readonly title: string;
readonly clearTitle?: never;
}
| {
readonly source: string;
readonly title?: never;
readonly clearTitle: true;
};

export interface PluginPaneOptions {
readonly pluginId: string;
readonly entrypoint: string;
Expand Down
4 changes: 3 additions & 1 deletion package/core/src/ports.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { Pane, PluginPaneOptions, Tab, Workspace } from '#herdr-plugin-core/models';
import type { Pane, PaneTitleUpdate, PluginPaneOptions, Tab, Workspace } from '#herdr-plugin-core/models';

export interface WorkspaceClient {
listWorkspaces(): readonly Workspace[];
Expand All @@ -16,7 +16,9 @@ export interface ConfigClient {
}

export interface PaneClient {
listPanes(workspaceId?: string): readonly Pane[];
getPane(paneId: string): Pane | undefined;
reportPaneTitle(paneId: string, update: PaneTitleUpdate): void;
openPluginPane(options: PluginPaneOptions): Pane;
closePluginPane(paneId: string): boolean;
}
Expand Down
116 changes: 116 additions & 0 deletions package/core/tests/herdr-cli-client.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,122 @@ describe('HerdrCliClient', () => {
]);
});

it('lists panes and owns source-scoped pane title metadata', () => {
const runner = new StubCommandRunner(
{
error: undefined,
status: 0,
stdout: JSON.stringify({ result: { panes: [{ pane_id: 'p1', tab_id: 't1', workspace_id: 'w1' }] } }),
stderr: '',
},
{
error: undefined,
status: 0,
stdout: '',
stderr: '',
},
{
error: undefined,
status: 0,
stdout: '',
stderr: '',
},
);

const client = new HerdrCliClient('/mock/herdr', runner);

expect(
client.listPanes('w1'),
).toEqual([{ pane_id: 'p1', tab_id: 't1', workspace_id: 'w1' }]);

client.reportPaneTitle('p1', { source: 'example.hints', title: 'Ctrl+B arrows' });
client.reportPaneTitle('p1', { source: 'example.hints', clearTitle: true });

expect(
runner.calls.map(({ args }) => args),
).toEqual([
['pane', 'list', '--workspace', 'w1'],
['pane', 'report-metadata', 'p1', '--source', 'example.hints', '--title', 'Ctrl+B arrows'],
['pane', 'report-metadata', 'p1', '--source', 'example.hints', '--clear-title'],
]);
});

it('gets a pane through the shared transport', () => {
const runner = new StubCommandRunner({
error: undefined,
status: 0,
stdout: JSON.stringify({ result: { pane: { pane_id: 'p1', tab_id: 't1', workspace_id: 'w1' } } }),
stderr: '',
});

const client = new HerdrCliClient('/mock/herdr', runner);

expect(
client.getPane('p1'),
).toEqual({ pane_id: 'p1', tab_id: 't1', workspace_id: 'w1' });
expect(runner.calls[0]?.args).toEqual(['pane', 'get', 'p1']);
});

it('returns undefined when a pane is not found', () => {
const runner = new StubCommandRunner({
error: undefined,
status: 1,
stdout: '',
stderr: JSON.stringify({ error: { code: 'pane_not_found', message: 'missing pane' } }),
});

const client = new HerdrCliClient('/mock/herdr', runner);

expect(
client.getPane('missing'),
).toBeUndefined();
expect(runner.calls[0]?.args).toEqual(['pane', 'get', 'missing']);
});

it('preserves structured errors from pane lookups', () => {
const runner = new StubCommandRunner({
error: undefined,
status: 1,
stdout: '',
stderr: JSON.stringify({ error: { code: 'server_unavailable', message: 'server unavailable' } }),
});

const client = new HerdrCliClient('/mock/herdr', runner);

try {
client.getPane('p1');
expect.unreachable();
} catch (error) {
expect(error).toBeInstanceOf(HerdrCommandError);
expect((error as HerdrCommandError).code).toBe('server_unavailable');
}

expect(runner.calls[0]?.args).toEqual(['pane', 'get', 'p1']);
});

it('preserves command-runner errors from pane lookups', () => {
const processError = new Error('spawn failed');

const runner = new StubCommandRunner({
error: processError,
status: null,
stdout: '',
stderr: '',
});

const client = new HerdrCliClient('/mock/herdr', runner);

try {
client.getPane('p1');
expect.unreachable();
} catch (error) {
expect(error).toBeInstanceOf(HerdrCommandError);
expect((error as HerdrCommandError).cause).toBe(processError);
}

expect(runner.calls[0]?.args).toEqual(['pane', 'get', 'p1']);
});

it('preserves structured Herdr error codes', () => {
const runner = new StubCommandRunner({
error: undefined,
Expand Down
Loading
Loading