Skip to content

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:

typescript
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

ParameterTypeDescription
idstringTab id
titlestringTab label
ComponentReact.ComponentType<{ context: CollectionSettingsTabContext }>Tab content
ordernumberSort 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.

typescript
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

ParameterTypeDescription
idstringMenu item id
titlestringMenu label
commandstringCommand id; handler receives target context as args
when'collection' | 'folder' | 'request'` or arraySidebar row types
groupstringMenu group
ordernumberSort 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.

typescript
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

ParameterTypeDescription
idstringPanel id
titlestringToggle label in the footer bar
ComponentReact.ComponentTypeSlide-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:

typescript
hc.ui.registerFooterPanel({
  id: 'myPlugin.footer',
  title: 'My Log',
  Component: PluginLogPanel
});
tsx
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

ParameterTypeDescription
idstringAction id
titlestringButton accessible name / tooltip
commandstringCommand id to run on click
iconstringOptional 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.

typescript
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

ParameterTypeDescription
idstringView id
titlestringDisplay name for the page tab
ComponentReact.ComponentTypeFull main-area content
iconstring` (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.

typescript
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

ParameterTypeDescription
menu'file' | 'edit' | 'view' | 'help'Target application menu
commandstringCommand id to run on click
labelstringMenu label override
groupstringMenu group for separators
ordernumberSort 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.

typescript
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

ParameterTypeDescription
idstringModal id
titlestringAccessible title for the modal surface
ComponentReact.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.

typescript
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

ParameterTypeDescription
idstringTab id
titlestringTab label
ComponentReact.ComponentType<{ context: RequestTabContext }>Tab content
ordernumberSort 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).

typescript
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

ParameterTypeDescription
idstringAction id
titlestringButton label or tooltip
commandstringCommand id to run on click
iconstringOptional icon name
ordernumberSort order near the Send button

Adds a button to the request URL bar toolbar.

typescript
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

ParameterTypeDescription
idstringTab id
titlestringTab label
ComponentReact.ComponentType<{ context: ResponseTabContext }>Tab content
ordernumberSort 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).

typescript
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

ParameterTypeDescription
idstringAction id
titlestringButton label or tooltip
commandstringCommand id to run on click
iconstringOptional icon name
ordernumberSort order within the row action group
phasesScriptPhase[]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.

typescript
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

ParameterTypeDescription
idstringSettings section id
titlestringLabel in the Settings sidebar
ComponentReact.ComponentTypePanel content

Registers a React component as a Settings panel alongside built-in sections (General, Storage, and so on).

typescript
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

ParameterTypeDescription
idstringPanel id
titlestringLabel when switching sidebar mode
iconstringOptional icon name
ComponentReact.ComponentTypeFull sidebar content
ordernumberSort 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 === null means 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 (lowest order, then pluginId, then contribution id) and logs a warning.

For activity-rail icons that open a sidebar while keeping the rail visible, use hc.ui.registerSidebarRailItem instead.

typescript
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

ParameterTypeDescription
idstringRail item id
titlestringLabel / accessible name on the activity rail
iconstringRequired curated icon name (server, database, globe, code, robot, puzzle-piece, bolt, flask)
ComponentReact.ComponentTypeFull sidebar content
ordernumberSort 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.

typescript
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

ParameterTypeDescription
idstringSection id
titlestringCollapsible section heading
ComponentReact.ComponentTypeSection body
headerActionsReact.ComponentTypeOptional controls in the section header row
ordernumberSort order below Collections / Environments

Adds a collapsible block inside the scrollable sidebar, using the same pattern as the built-in Collections and Environments sections.

typescript
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

ParameterTypeDescription
idstringItem id
ComponentReact.ComponentTypeStatus content
alignment'left' | 'right'Footer side. Default right.
ordernumberSort order on that side

Adds a custom status indicator to the footer bar.

typescript
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

ParameterTypeDescription
idstringContribution id
titlestringDisplay label
ComponentComponentReceives `{ context: WorkflowActionBlockContext }
actionTypesstring[]Optional filter — omit to show on every timeline action
ordernumberSort 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.

typescript
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

ParameterTypeDescription
idstringAction id
titlestringButton label or tooltip
commandstringCommand id to run on click
iconstringOptional icon name
ordernumberSort 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.

typescript
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

ParameterTypeDescription
panelIdstringManifest footerPanels id
stateFooterPanelIndicatorState | nullIndicator 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:

typescript
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.

typescript
hc.ui.showToast('Settings saved', { duration: 3000 });