UI
All hc.ui.register* methods require the ui permission, return a Disposable that unregisters the contribution when called, and require an id that matches an entry in the corresponding manifest.contributes.* array.
Registration disposables are tracked automatically when you call hc.ui.register* methods. Custom disposables (timers, focus sync, etc.) should be disposed in deactivate() or React effect cleanup.
See Manifest for the manifest keys that correspond to each registrar.
PluginContext and contribution types
The renderer entry exports activate(hc) and optionally deactivate(). The hc argument is a PluginContext:
import type { HttpResponse, RequestDraft } from '@harborclient/sdk';
import type * as React from 'react';
export interface Disposable {
dispose(): void;
}
export interface UiContributionBase {
/** Must match an id in the corresponding manifest contributes.* array */
id: string;
title: string;
}
export interface SettingsSectionContribution extends UiContributionBase {
Component: React.ComponentType;
}
export interface SidebarPanelContribution extends UiContributionBase {
icon?: string;
Component: React.ComponentType;
order?: number;
}
export interface SidebarSectionContribution extends UiContributionBase {
Component: React.ComponentType;
headerActions?: React.ComponentType;
order?: number;
}
export interface MainViewContribution extends UiContributionBase {
Component: React.ComponentType;
/** Optional tab-bar icon name (`server`, `database`, `globe`, `code`, `robot`, `puzzle-piece`, `bolt`, `flask`). */
icon?: string;
}
export interface RequestTabContext {
draft: RequestDraft;
response: HttpResponse | null;
readOnly: true;
collectionAuth: AuthConfig;
collectionHeaders: Array<{ key: string; value: string; enabled: boolean }>;
/** Merged global, collection, and environment values for {{key}} substitution. */
variables: Record<string, string>;
}
export interface RequestTabContribution extends UiContributionBase {
Component: React.ComponentType<{ context: RequestTabContext }>;
order?: number;
}
export interface ResponseTabContext {
draft: RequestDraft;
response: HttpResponse | null;
}
export interface ResponseTabContribution extends UiContributionBase {
Component: React.ComponentType<{ context: ResponseTabContext }>;
order?: number;
/** When to show the tab. Default `hasResponse`. */
when?: 'always' | 'hasResponse';
}
export interface CollectionSettingsTabContext {
collectionId: number;
readOnly: boolean;
}
export interface CollectionSettingsTabContribution extends UiContributionBase {
Component: React.ComponentType<{ context: CollectionSettingsTabContext }>;
order?: number;
}
export interface FooterPanelContribution extends UiContributionBase {
Component: React.ComponentType;
}
export type FooterPanelIndicatorStatus =
| 'success'
| 'danger'
| 'muted'
| 'accent'
| 'warning'
| 'info';
export interface FooterPanelIndicatorState {
status: FooterPanelIndicatorStatus;
label?: string;
}
export type AppMenu = 'file' | 'edit' | 'view' | 'help';
export interface MenuItemContribution {
menu: AppMenu;
command: string;
label?: string;
group?: string;
order?: number;
}
export interface RequestToolbarActionContribution {
id: string;
title: string;
command: string;
icon?: string;
order?: number;
}
export interface LivePageChromeActionContext {
tabId: string;
url: string;
title: string;
websiteId?: number | null;
}
export interface LivePageChromeActionContribution {
id: string;
title: string;
command: string;
icon?: string;
}
export type ContextMenuTarget = 'collection' | 'folder' | 'request';
export interface ContextMenuItemContribution {
id: string;
title: string;
command: string;
when: ContextMenuTarget | ContextMenuTarget[];
group?: string;
order?: number;
}
export interface StatusBarItemContribution {
id: string;
Component: React.ComponentType;
alignment?: 'left' | 'right';
order?: number;
}
/**
* HarborClient UI color tokens. Override via `colors` or a bundled stylesheet.
* Maps to `--mac-*` CSS custom properties on `:root`.
*/
export type ThemeColorToken =
| 'surface'
| 'header'
| 'page-header'
| 'page-header-text'
| 'page-header-muted'
| 'sidebar'
| 'sidebar-toolbar'
| 'sidebar-rail'
| 'sidebar-rail-active'
| 'sidebar-rail-text'
| 'sidebar-rail-separator'
| 'sidebar-section'
| 'sidebar-section-text'
| 'footer'
| 'footer-text'
| 'footer-muted'
| 'footer-icon-active'
| 'toolbar-action-active'
| 'git-uncommitted'
| 'control'
| 'field'
| 'separator'
| 'text'
| 'text-secondary'
| 'muted'
| 'accent'
| 'selection'
| 'doc-markdown'
| 'tab-bar'
| 'tab-active'
| 'tab-inactive'
| 'tab-hover'
| 'tab-text'
| 'tab-text-inactive'
| 'tab-unsaved'
| 'tab-underline'
| 'resize-separator'
| 'resize-handle'
| 'variable-token'
| 'danger'
| 'danger-light'
| 'warning'
| 'success'
| 'info'
| 'method-get'
| 'method-post'
| 'method-put'
| 'method-patch'
| 'method-delete'
| 'method-head'
| 'method-options';
/**
* HarborClient UI metric tokens (typography and geometry).
* Override via `metrics` or a bundled stylesheet. Maps to `--mac-*` on `:root`.
*/
export type ThemeMetricToken =
| 'layout-font-family'
| 'layout-font-size'
| 'layout-border-width'
| 'layout-radius'
| 'breadcrumb-font-family'
| 'breadcrumb-font-size'
| 'breadcrumb-border-width'
| 'breadcrumb-radius'
| 'text-font-family'
| 'text-font-family-mono'
| 'text-font-size'
| 'text-font-size-sm'
| 'text-font-size-lg'
| 'interactive-font-family'
| 'interactive-font-size'
| 'interactive-border-width'
| 'interactive-radius'
| 'interactive-focus-ring-width'
| 'chrome-font-family'
| 'chrome-font-size'
| 'chrome-border-width'
| 'chrome-radius'
| 'tab-font-family'
| 'tab-font-size'
| 'tab-border-width'
| 'tab-radius'
| 'status-font-family'
| 'status-font-size'
| 'status-border-width'
| 'status-radius'
| 'method-font-family'
| 'method-font-size'
| 'method-border-width'
| 'method-radius'
| 'script-stage-font-family'
| 'script-stage-font-size'
| 'script-stage-border-width'
| 'script-stage-radius'
| 'git-font-family'
| 'git-font-size'
| 'git-border-width'
| 'git-radius'
| 'scrollbar-width';
export interface ThemeContribution {
/** Must match an id in manifest.contributes.themes */
id: string;
title: string;
/** Base appearance for `color-scheme` and native window chrome */
type: 'light' | 'dark';
/** Color token overrides without the `--mac-` prefix */
colors?: Partial<Record<ThemeColorToken, string>>;
/** Typography/geometry overrides without the `--mac-` prefix (CSS strings) */
metrics?: Partial<Record<ThemeMetricToken, string>>;
/** Plugin-relative CSS path (for example `dist/theme.css`) */
stylesheet?: string;
}
export type BuiltinThemeId = 'light' | 'dark' | 'system' | 'high-contrast';
export type ActiveTheme =
| { source: 'builtin'; id: BuiltinThemeId }
| { source: 'plugin'; pluginId: string; themeId: string };
export interface PluginThemes {
register(theme: ThemeContribution): Disposable;
getActive(): Promise<ActiveTheme>;
onDidChange(listener: (theme: ActiveTheme) => void): Disposable;
}
export interface PluginStorage {
get<T>(key: string): Promise<T | undefined>;
set<T>(key: string, value: T): Promise<void>;
}
export interface PluginCommands {
register(id: string, handler: (...args: unknown[]) => void | Promise<void>): Disposable;
execute(id: string, ...args: unknown[]): Promise<void>;
}
export type ActionHandlerMap = Record<string, (...args: unknown[]) => void | Promise<void>>;
export interface PluginActions {
register(namespace: string, handlers: ActionHandlerMap): Disposable;
}
export interface PluginUi {
registerSettingsSection(section: SettingsSectionContribution): Disposable;
registerSidebarPanel(panel: SidebarPanelContribution): Disposable;
registerSidebarRailItem(item: SidebarRailItemContribution): Disposable;
registerSidebarSection(section: SidebarSectionContribution): Disposable;
registerMainView(view: MainViewContribution): Disposable;
registerRequestTab(tab: RequestTabContribution): Disposable;
registerResponseTab(tab: ResponseTabContribution): Disposable;
registerCollectionSettingsTab(tab: CollectionSettingsTabContribution): Disposable;
registerFooterPanel(panel: FooterPanelContribution): Disposable;
setFooterPanelIndicator(panelId: string, state: FooterPanelIndicatorState | null): void;
registerMenuItem(item: MenuItemContribution): Disposable;
registerRequestToolbarAction(action: RequestToolbarActionContribution): Disposable;
registerLivePageChromeAction(action: LivePageChromeActionContribution): Disposable;
registerScriptEditorAction(action: ScriptEditorActionContribution): Disposable;
registerWorkflowToolbarAction(action: WorkflowToolbarActionContribution): Disposable;
registerWorkflowActionBlock(block: WorkflowActionBlockContribution): Disposable;
registerContextMenuItem(item: ContextMenuItemContribution): Disposable;
registerStatusBarItem(item: StatusBarItemContribution): Disposable;
showToast(message: string, options?: { duration?: number }): void;
}
export interface PluginContext {
pluginId: string;
react: typeof React;
ui: PluginUi;
themes: PluginThemes;
commands: PluginCommands;
actions: PluginActions;
storage: PluginStorage;
fs: PluginFs;
http: PluginRendererHttp;
ipc: PluginIpcInvoker;
host: PluginHost;
imports: PluginImports;
mcp: PluginMcp;
ai: PluginAi;
}
export interface PluginMcpHeader {
key: string;
value: string;
}
export interface PluginMcpServerConfig {
name: string;
serverURL: string;
enabled?: boolean;
headers?: PluginMcpHeader[];
icon?: string;
}
export interface PluginMcp {
registerServer(config: PluginMcpServerConfig): Disposable;
}
export interface PluginImports {
registerHandler(extensions: string | string[], handler: ImportHandler): Disposable;
}
export interface PluginRendererHttp {
onAfterSend(
handler: (request: PluginHttpRequest, response: PluginHttpResponse) => void | Promise<void>
): Disposable;
}
export interface PluginIpcInvoker {
invoke<T>(channel: string, ...args: unknown[]): Promise<T>;
}
export interface OpenRequestDraftParam {
key: string;
value: string;
}
export interface OpenRequestDraftPayload {
name?: string;
method?: string;
url?: string;
headers?: Record<string, string>;
params?: OpenRequestDraftParam[];
body?: string;
bodyType?: BodyType;
}
export interface ApplyRequestDraftPayload {
method?: string;
url?: string;
headers?: Record<string, string>;
params?: OpenRequestDraftParam[];
body?: string;
bodyType?: BodyType;
}
export type OpenImageViewPayload =
| { path: string; fileName?: string }
| { url: string; fileName?: string }
| { dataUrl: string; fileName: string }
| { base64: string; contentType: string; fileName: string };
export interface PluginHost {
openRequestDraft(payload: OpenRequestDraftPayload): Promise<void>;
applyRequestDraft(payload: ApplyRequestDraftPayload): Promise<void>;
loadRequest(requestId: number): Promise<void>;
send(): Promise<void>;
fetch(
input: string | URL | { url: string },
init?: PluginFetchInit
): Promise<PluginFetchResponse>;
createEnvironmentWithVariables(
name: string,
variables: PluginVariableInput[]
): Promise<CreatedEnvironmentResult>;
updateEnvironmentVariables(
environmentId: number,
variables: PluginVariableInput[]
): Promise<void>;
createCollection(payload: CreateCollectionPayload): Promise<CreateCollectionResult>;
listWorkflows(): Promise<HostWorkflow[]>;
getWorkflow(workflowId: number): Promise<HostWorkflow | null>;
createWorkflow(input: CreateWorkflowPayload): Promise<HostWorkflow>;
updateWorkflow(input: UpdateWorkflowPayload): Promise<HostWorkflow>;
renameWorkflow(workflowId: number, name: string): Promise<HostWorkflow>;
deleteWorkflow(workflowId: number): Promise<void>;
onWorkflowsChanged(listener: (event: WorkflowsChangedEvent) => void): Disposable;
openImageView(payload: OpenImageViewPayload): Promise<void>;
}
export interface PluginHttpRequest {
method: string;
url: string;
headers: Record<string, string>;
body: string;
bodyType?: string;
params?: Array<{ key: string; value: string }>;
sourceRequestId?: number;
sourceRequestName?: string;
}
export interface PluginHttpResponse {
status: number;
statusText: string;
headers: Record<string, string>;
body: string;
}Install @harborclient/sdk as a dev dependency in your plugin project for types and the JSX runtime helpers. The package tracks HarborClient releases. Type definitions are maintained in harborclient/sdk. Main entries use MainPluginContext instead — import it from @harborclient/sdk or @harborclient/sdk/main for main-only plugins.
hc.ui.closeModal(modalId?)
Available since v2.0.0
Signature:(modalId?: string) => void
Closes the open plugin modal overlay. When modalId is provided, the overlay closes only if that modal is currently open.
hc.ui.openModal(modalId, context?)
Available since v2.0.0
Signature:(modalId: string, context?: unknown) => void
Manifest:contributes.modals
Opens the registered modal overlay in the host application window. Optional context is passed to the modal component as a context prop.
hc.ui.registerCollectionSettingsTab(tab)
Available since v2.0.0
Signature:(tab: CollectionSettingsTabContribution) => Disposable
Manifest:contributes.collectionSettingsTabs
| Parameter | Type | Description |
|---|---|---|
id | string | Tab id |
title | string | Tab label |
Component | React.ComponentType<{ context: CollectionSettingsTabContext }> | Tab content |
order | number | Sort order among collection settings tabs |
Adds a segmented tab to Collection Settings (alongside General, Variables, Headers, and so on). The component receives context.collectionId and context.readOnly.
hc.ui.registerCollectionSettingsTab({
id: 'myPlugin.collTab',
title: 'Plugin',
Component: CollectionPluginTab
});hc.ui.registerContextMenuItem(item)
Available since v2.0.0
Signature:(item: ContextMenuItemContribution) => Disposable
Manifest:contributes.contextMenus
| Parameter | Type | Description |
|---|---|---|
id | string | Menu item id |
title | string | Menu label |
command | string | Command id; handler receives target context as args |
when | 'collection' | 'folder' | 'request'` or array | Sidebar row types |
group | string | Menu group |
order | number | Sort order within the group |
Adds an action to row context menus in the sidebar for collection, folder, and request targets.
When a plugin replaces the Collections sidebar (replaces: "collections"),
those contributions still appear in menus opened via
hc.host.showEntityContextMenu
— the host builds the same menu model the built-in tree uses. They do not
appear automatically inside a plugin’s own custom RowActionsMenu unless the
plugin calls showEntityContextMenu (or builds an equivalent menu and invokes
the same host commands).
Document targets are not supported. See Host for coordinate mapping and focus-return limitations.
hc.commands.register('myPlugin.requestMenu', (target) => {
hc.ui.showToast(`Action on request ${target.requestId}`);
});
hc.ui.registerContextMenuItem({
id: 'myPlugin.requestMenu',
title: 'Plugin action',
command: 'myPlugin.requestMenu',
when: 'request'
});hc.ui.registerFooterPanel(panel)
Available since v2.0.0
Signature:(panel: FooterPanelContribution) => Disposable
Manifest:contributes.footerPanels
| Parameter | Type | Description |
|---|---|---|
id | string | Panel id |
title | string | Toggle label in the footer bar |
Component | React.ComponentType | Slide-up panel content |
Registers a slide-up footer panel using the same pattern as Console and Variables. The host wraps your component in a resizable shell — you do not implement resize logic yourself. The shell provides:
- A top drag handle (and keyboard resize on the handle)
- Per-panel height persistence in
localStorage(hc.footerPanel.<namespaced-id>) - A close button in the top-right corner
Layout contract: Your Component should fill the resizable area with
flex h-full min-h-0 flex-col and put scrollable content in a
flex-1 overflow-auto child. Leave roughly 32px of right padding on header
rows so controls do not sit under the host close button.
To show a status dot beside the footer toggle (running / stopped / error), use
hc.ui.setFooterPanelIndicator —
do not mount your own indicator UI.
Example panel structure:
hc.ui.registerFooterPanel({
id: 'myPlugin.footer',
title: 'My Log',
Component: PluginLogPanel
});function PluginLogPanel() {
return (
<div className="flex h-full min-h-0 flex-col bg-control">
<div className="flex shrink-0 items-center border-b border-separator px-3 py-2 pr-8">
<h3 className="text-[14px] font-medium text-text">My Log</h3>
</div>
<div className="min-h-0 flex-1 overflow-auto">{/* scrollable body */}</div>
</div>
);
}hc.ui.registerLivePageChromeAction(action)
Available since v2.0.0
Signature:(action: LivePageChromeActionContribution) => Disposable
Manifest:contributes.livePageChromeActions
| Parameter | Type | Description |
|---|---|---|
id | string | Action id |
title | string | Button accessible name / tooltip |
command | string | Command id to run on click |
icon | string | Optional curated icon name (host resolves; puzzle fallback) |
Adds a RoundButton to the embedded browser chrome bar between Downloads and Ask AI. Buttons sort by plugin activation order, then registration order within that plugin — there is no order field.
The command handler receives a single {@link LivePageChromeActionContext} argument with tabId, url, title, and optional websiteId.
hc.commands.register('myPlugin.pageAction', (context: LivePageChromeActionContext) => {
hc.ui.showToast(`Page: ${context.url}`);
});
hc.ui.registerLivePageChromeAction({
id: 'myPlugin.pageAction',
title: 'Page action',
command: 'myPlugin.pageAction',
icon: 'bolt'
});hc.ui.registerMainView(view)
Available since v2.0.0
Signature:(view: MainViewContribution) => Disposable
Manifest:contributes.mainViews
| Parameter | Type | Description |
|---|---|---|
id | string | View id |
title | string | Display name for the page tab |
Component | React.ComponentType | Full main-area content |
icon | string` (optional) | Tab-bar icon name. Supported: server, database, globe, code, robot, puzzle-piece, bolt, flask. Unknown names fall back to puzzle-piece. |
Registers a full main-area overlay, replacing the request editor while open (same pattern as Team Hubs or Sharing Keys). Open the view with hc.commands.execute from a menu item or other trigger. The title and optional icon appear on the page tab.
hc.ui.registerMainView({
id: 'myPlugin.view',
title: 'My Dashboard',
icon: 'server',
Component: DashboardView
});hc.ui.registerMenuItem(item)
Available since v2.0.0
Signature:(item: MenuItemContribution) => Disposable
Manifest:contributes.menus
| Parameter | Type | Description |
|---|---|---|
menu | 'file' | 'edit' | 'view' | 'help' | Target application menu |
command | string | Command id to run on click |
label | string | Menu label override |
group | string | Menu group for separators |
order | number | Sort order within the group |
Adds an item to the application menu. Register the command handler with hc.commands.register separately.
For File → Import workflows, prefer hc.imports.registerHandler instead of adding a separate File menu import item. Built-in HarborClient formats are detected first; plugin handlers receive only unrecognized files whose extensions they registered.
hc.commands.register('myPlugin.run', () => {
hc.ui.showToast('Command ran');
});
hc.ui.registerMenuItem({ menu: 'view', command: 'myPlugin.run', group: 'plugin' });hc.ui.registerModal(modal)
Available since v2.0.0
Signature:(modal: ModalContribution) => Disposable
Manifest:contributes.modals
| Parameter | Type | Description |
|---|---|---|
id | string | Modal id |
title | string | Accessible title for the modal surface |
Component | React.ComponentType<{ context?: unknown }> | Modal body; receives context from `openModal |
Registers a modal rendered in a full-window overlay at the application root. Open it with hc.ui.openModal(modalId, context?) and close it with hc.ui.closeModal(modalId?). Requires the ui permission.
hc.ui.registerModal({
id: 'myPlugin.editor',
title: 'Edit item',
Component: ({ context }) => <EditorModal context={context} />
})
hc.ui.openModal('myPlugin.editor', { itemId: 'abc' });hc.ui.registerRequestTab(tab)
Available since v2.0.0
Signature:(tab: RequestTabContribution) => Disposable
Manifest:contributes.requestTabs
| Parameter | Type | Description |
|---|---|---|
id | string | Tab id |
title | string | Tab label |
Component | React.ComponentType<{ context: RequestTabContext }> | Tab content |
order | number | Sort order among editor tabs |
Adds a segmented tab to the request editor (alongside Params, Headers, Body, and so on). The component receives context.draft for the active request, context.response when a response exists, and context.variables for merged global, collection, and environment substitution values (see Global variables).
hc.ui.registerRequestTab({
id: 'myPlugin.requestTab',
title: 'Audit',
Component: AuditTab
});hc.ui.registerRequestToolbarAction(action)
Available since v2.0.0
Signature:(action: RequestToolbarActionContribution) => Disposable
Manifest:contributes.requestToolbarActions
| Parameter | Type | Description |
|---|---|---|
id | string | Action id |
title | string | Button label or tooltip |
command | string | Command id to run on click |
icon | string | Optional icon name |
order | number | Sort order near the Send button |
Adds a button to the request URL bar toolbar.
hc.commands.register('myPlugin.sendAction', () => {
hc.ui.showToast('Pre-send check passed');
});
hc.ui.registerRequestToolbarAction({
id: 'myPlugin.sendAction',
title: 'Run check',
command: 'myPlugin.sendAction'
});hc.ui.registerResponseTab(tab)
Available since v2.0.0
Signature:(tab: ResponseTabContribution) => Disposable
Manifest:contributes.responseTabs
| Parameter | Type | Description |
|---|---|---|
id | string | Tab id |
title | string | Tab label |
Component | React.ComponentType<{ context: ResponseTabContext }> | Tab content |
order | number | Sort order among response tabs |
when | 'always' | 'hasResponse' | When the tab is visible. Default hasResponse. |
Adds a tab to the response viewer (alongside Body, Headers, Tests).
hc.ui.registerResponseTab({
id: 'myPlugin.responseTab',
title: 'Summary',
when: 'hasResponse',
Component: ResponseSummaryTab
});hc.ui.registerScriptEditorAction(action)
Available since v2.0.0
Signature:(action: ScriptEditorActionContribution) => Disposable
Manifest:contributes.scriptEditorActions
| Parameter | Type | Description |
|---|---|---|
id | string | Action id |
title | string | Button label or tooltip |
command | string | Command id to run on click |
icon | string | Optional icon name |
order | number | Sort order within the row action group |
phases | ScriptPhase[] | Optional filter — show only in pre-request and/or post-request stage tabs |
HarborClient uses request stage for the pre-request and post-request script lists (ScriptPhase: pre | post) and script stage for timing within a list (ScriptStage: before-all, before-each, main, after-each, after-all).
Script rows carry a {@link ScriptStage} value that controls when the script runs within its request stage. Plugin script editor actions are not filtered by script stage today — use phases only.
Adds an icon button to each script row in the pre-request and post-request stage editors. The command handler receives a single {@link ScriptEditorActionContext} argument with phase (request stage), scriptId, and code.
hc.commands.register('myPlugin.convert', (context: ScriptEditorActionContext) => {
hc.ui.openModal('preview', context);
});
hc.ui.registerScriptEditorAction({
id: 'myPlugin.convert',
title: 'Convert',
command: 'myPlugin.convert'
});hc.ui.registerSettingsSection(section)
Available since v2.0.0
Signature:(section: SettingsSectionContribution) => Disposable
Manifest:contributes.settingsSections
| Parameter | Type | Description |
|---|---|---|
id | string | Settings section id |
title | string | Label in the Settings sidebar |
Component | React.ComponentType | Panel content |
Registers a React component as a Settings panel alongside built-in sections (General, Storage, and so on).
hc.ui.registerSettingsSection({
id: 'compactMode',
title: 'Compact Mode',
Component: CompactModePanel
});hc.ui.registerSidebarPanel(panel)
Available since v2.0.0
Signature:(panel: SidebarPanelContribution) => Disposable
Manifest:contributes.sidebarPanels
| Parameter | Type | Description |
|---|---|---|
id | string | Panel id |
title | string | Label when switching sidebar mode |
icon | string | Optional icon name |
Component | React.ComponentType | Full sidebar content |
order | number | Sort order among plugin panels |
Registers a switchable left sidebar destination — a full-height panel the user selects instead of the default collections view. The host mounts the panel with resizeMode="fill" so the plugin surface fills the sidebar body and scrolls inside the webview (plugin-surface-fill). Panel bodies should scroll via Scrollbars from @harborclient/sdk/components (OverlayScrollbars is bundled with the SDK; theme CSS comes from the host stylesheet) so they match the built-in Collections sidebar — do not use native overflow-y-auto for the main list.
Replacing Collections: To make this panel the default left-sidebar body (hiding the built-in Collections tree and "Collections" switcher tab), set replaces: "collections" on the matching manifest entry. That field is not part of the runtime SidebarPanelContribution object; the host copies it from the manifest at registration time.
Semantics:
activeSidebarPanelId === nullmeans the primary collections surface — the replacement panel when one is registered, otherwise the built-in Collections tree.- Non-replacing plugin panels remain switchable destinations alongside the primary surface.
- When multiple panels claim
replaces: "collections", the host picks one winner (lowestorder, thenpluginId, then contributionid) and logs a warning.
For activity-rail icons that open a sidebar while keeping the rail visible, use hc.ui.registerSidebarRailItem instead.
hc.ui.registerSidebarPanel({
id: 'myPlugin.panel',
title: 'My Tools',
icon: 'wrench',
Component: MySidebarPanel
});hc.ui.registerSidebarRailItem(item)
Available since v2.8.8
Signature:(item: SidebarRailItemContribution) => Disposable
Manifest:contributes.sidebarRailItems
| Parameter | Type | Description |
|---|---|---|
id | string | Rail item id |
title | string | Label / accessible name on the activity rail |
icon | string | Required curated icon name (server, database, globe, code, robot, puzzle-piece, bolt, flask) |
Component | React.ComponentType | Full sidebar content |
order | number | Sort order among plugin rail items (appended after built-in modes) |
Registers an activity-rail button. Selecting it mounts the panel with resizeMode="fill" and keeps the activity rail visible (unlike registerSidebarPanel, which uses a horizontal switcher and hides the rail). Panel bodies should scroll via Scrollbars from @harborclient/sdk/components.
The host pushes { sidebarSelection } as surface context (same shape as sidebar panels). Prefer hc.host.onSidebarSelectionChanged for live updates.
hc.ui.registerSidebarRailItem({
id: 'myPlugin.tools',
title: 'My Tools',
icon: 'bolt',
Component: MyToolsSidebar,
order: 10
});hc.ui.registerSidebarSection(section)
Available since v2.0.0
Signature:(section: SidebarSectionContribution) => Disposable
Manifest:contributes.sidebarSections
| Parameter | Type | Description |
|---|---|---|
id | string | Section id |
title | string | Collapsible section heading |
Component | React.ComponentType | Section body |
headerActions | React.ComponentType | Optional controls in the section header row |
order | number | Sort order below Collections / Environments |
Adds a collapsible block inside the scrollable sidebar, using the same pattern as the built-in Collections and Environments sections.
hc.ui.registerSidebarSection({
id: 'myPlugin.section',
title: 'Quick links',
Component: QuickLinksSection,
order: 100
});hc.ui.registerStatusBarItem(item)
Available since v2.0.0
Signature:(item: StatusBarItemContribution) => Disposable
Manifest:contributes.statusBarItems
| Parameter | Type | Description |
|---|---|---|
id | string | Item id |
Component | React.ComponentType | Status content |
alignment | 'left' | 'right' | Footer side. Default right. |
order | number | Sort order on that side |
Adds a custom status indicator to the footer bar.
hc.ui.registerStatusBarItem({
id: 'myPlugin.status',
alignment: 'right',
Component: PluginStatusBadge
});hc.ui.registerWorkflowActionBlock(block)
Available since v2.8.6
Signature:(block: WorkflowActionBlockContribution) => Disposable
Manifest:contributes.workflowActionBlocks
| Parameter | Type | Description |
|---|---|---|
id | string | Contribution id |
title | string | Display label |
Component | Component | Receives `{ context: WorkflowActionBlockContext } |
actionTypes | string[] | Optional filter — omit to show on every timeline action |
order | number | Sort order among stacked surfaces in a block |
Renders a HostedSurface inside matching workflow timeline action blocks (below
the built-in thumbnail). Surfaces are skipped when the block is compact (too
narrow). Prefer a narrow actionTypes list to avoid mounting many webviews.
function ActionBadge({ context }: { context: WorkflowActionBlockContext }) {
const { react: React } = hc;
return React.createElement('span', null, context.action.type);
}
hc.ui.registerWorkflowActionBlock({
id: 'badge',
title: 'Action badge',
actionTypes: ['request.send', 'request.load'],
Component: ActionBadge
});hc.ui.registerWorkflowToolbarAction(action)
Available since v2.8.6
Signature:(action: WorkflowToolbarActionContribution) => Disposable
Manifest:contributes.workflowToolbarActions
| Parameter | Type | Description |
|---|---|---|
id | string | Action id |
title | string | Button label or tooltip |
command | string | Command id to run on click |
icon | string | Optional icon name |
order | number | Sort order to the right of Save |
Adds a button to the right of Save in the workflow play/edit toolbar. The
command handler receives a single WorkflowToolbarActionContext argument with
workflowId, actionIndex, action, and dirty.
hc.commands.register('myPlugin.annotate', (context: WorkflowToolbarActionContext) => {
hc.ui.showToast(`Workflow ${context.workflowId} selected action ${context.actionIndex}`);
});
hc.ui.registerWorkflowToolbarAction({
id: 'myPlugin.annotate',
title: 'Annotate',
command: 'myPlugin.annotate'
});hc.ui.setFooterPanelIndicator(panelId, state)
Available since v2.0.0
Signature:(panelId: string, state: FooterPanelIndicatorState | null) => void
Manifest:contributes.footerPanels
| Parameter | Type | Description |
|---|---|---|
panelId | string | Manifest footerPanels id |
state | FooterPanelIndicatorState | null | Indicator status, or null to hide the dot |
Sets or clears the native status dot beside a footer panel toggle. The host
renders a StatusDot; plugins do not mount an indicator webview.
FooterPanelIndicatorState:
| Field | Type | Description |
|---|---|---|
status |
'success' | 'danger' | 'muted' | 'accent' | 'warning' | 'info' |
Color preset for the status dot |
label |
string |
Optional accessible name for the dot |
Call from the agent (always-on) renderer after registering the panel, and again whenever status changes:
hc.ui.setFooterPanelIndicator('myPlugin.footer', {
status: running ? 'success' : 'muted',
label: running ? 'My server active' : 'My server stopped'
});
// Hide the dot:
hc.ui.setFooterPanelIndicator('myPlugin.footer', null);hc.ui.showToast(message, options?)
Available since v2.0.0
Signature:(message: string, options?: { duration?: number }) => void
Shows a non-blocking toast for success or info feedback. Do not use toasts for errors that require acknowledgment — show those inline in your plugin UI instead.
hc.ui.showToast('Settings saved', { duration: 3000 });