Themes and storage
Theme packages are plugins. Ship the same .hcp layout, require the ui permission, declare slots in contributes.themes, and set "categories": ["themes"] when the package should appear under File → Themes. Users pick an active theme from View → Theme or Settings → General → Appearance.
See Theme plugins for manifest fields and Marketplace → Theme listings for catalog publishing.
hc.themes
Custom appearance themes extend the built-in Light, Dark, System, and High contrast options in Settings → General. Plugin themes appear in the same dropdown once registered.
HarborClient styles the app with --mac-* CSS custom properties defined in src/renderer/src/styles.css. When a plugin theme is active, the host sets data-theme="plugin-<pluginId>-<themeId>" on <html> and applies your token overrides or injected stylesheet. Built-in light/dark/system behavior is unchanged when a builtin theme is selected.
Themes can be registered two ways:
- JavaScript — call
registerTheme(hc, theme)orhc.themes.register(theme)fromactivate()(documented below). - JSON import — point the manifest contribution at a Theme Designer export file (see JSON theme import). No
activate()call is required for those entries.
Requires the ui permission. For JavaScript registration, call registerTheme(hc, theme) or hc.themes.register(theme) from activate() — registration disposables are tracked automatically.
JSON theme import
Declare an import path on the contribution to ship a palette without JavaScript:
{
"contributes": {
"themes": [
{
"id": "solarized",
"title": "Solarized Dark",
"type": "dark",
"import": "exported.json"
}
]
}
}The file must be a harborclientExport: "theme" envelope — the same shape as File → Themes → Designer export:
{
"harborclientVersion": 1,
"harborclientExport": "theme",
"title": "Solarized Dark",
"type": "dark",
"theme": {
"surface": "#002b36",
"accent": "#268bd2"
},
"stylesheet": "styles.css"
}| Field | Description |
|---|---|
harborclientVersion | Always 1 |
harborclientExport | Always "theme" |
theme | Token overrides without the --mac- prefix |
title / type | Present in the export; manifest id / title / type remain authoritative |
stylesheet | Optional plugin-relative CSS filename, or inlined CSS after first read |
On first read, if stylesheet points at an existing CSS file inside the plugin directory, HarborClient inlines the CSS text into the JSON on disk. Later reads treat the value as already-inlined CSS (idempotent). Theme-only packages can omit renderer and main entirely.
See the Solarized theme example and Theme plugins.
registerTheme(hc, theme)
Signature: (hc: PluginContext, theme: ThemeContribution) => Disposable
Convenience wrapper around hc.themes.register. Prefer this for single-theme plugins.
import { registerTheme } from '@harborclient/sdk';
registerTheme(hc, {
id: 'solarized',
title: 'Solarized Dark',
type: 'dark',
colors: { surface: '#002b36' }
});Use defineTheme(theme) when you want to define the theme object in a separate module with full ThemeContribution typing.
hc.themes.register(theme)
Signature: (theme: ThemeContribution) => Disposable
Manifest: contributes.themes
| Parameter | Type | Description |
|---|---|---|
id | string | Theme id unique within your plugin |
title | string | Label in the appearance dropdown |
type | 'light' | 'dark' | Sets color-scheme and Electron native chrome base |
colors | Partial<Record<ThemeColorToken, string>> | Optional color token overrides |
metrics | Partial<Record<ThemeMetricToken, string>> | Optional typography/geometry overrides (CSS strings such as 14px) |
stylesheet | string | Optional plugin-relative CSS file for complex themes |
Provide colors, metrics, a stylesheet, or a combination. Use colors / metrics for token swaps; use stylesheet when you need selectors beyond :root (for example plugin-specific tweaks under [data-theme='plugin-…']).
hc.themes.register({
id: 'solarized',
title: 'Solarized Dark',
type: 'dark',
colors: {
surface: '#002b36',
sidebar: '#073642',
control: '#073642',
text: '#839496',
'text-secondary': '#93a1a1',
accent: '#268bd2',
selection: 'rgba(38, 139, 210, 0.25)'
},
metrics: {
'layout-font-size': '14px',
'scrollbar-width': '10px'
}
});When the user selects your theme, the persisted value is plugin:<pluginId>:<themeId>. If the plugin is disabled or uninstalled while its theme is active, HarborClient falls back to System.
hc.themes.getActive()
Signature: () => Promise<ActiveTheme>
Returns the currently active theme — either a built-in id or a plugin theme reference.
const active = await hc.themes.getActive();
if (active.source === 'plugin') {
console.log(active.pluginId, active.themeId);
}hc.themes.onDidChange(listener)
Signature: (listener: (theme: ActiveTheme) => void) => Disposable
Fires when the user changes the appearance theme in Settings or when the host resets theme after plugin deactivation.
hc.themes.onDidChange((theme) => {
if (theme.source === 'plugin' && theme.themeId === 'solarized') {
hc.ui.showToast('Solarized theme active');
}
});Theme color tokens
Override any of these keys in colors. Each maps to --mac-<token> on the document root.
| Token | Used for |
|---|---|
surface | Main content background |
sidebar | Left sidebar background |
sidebar-toolbar | Sidebar/footer toolbar strip background |
sidebar-section | Sidebar section headers |
sidebar-section-text | Sidebar section header labels and chevrons |
footer | Footer status bar background |
footer-text | Footer primary text |
footer-muted | Footer de-emphasized text |
footer-icon-active | Active footer icon toggle color |
toolbar-action-active | Pressed sidebar toolbar action icon color |
breadcrumb-background | Request editor breadcrumb bar track |
breadcrumb-segment | Breadcrumb chevron segment fill |
git-staged | Git-backed request names staged for commit |
git-uncommitted | Git-backed request names with tracked unstaged changes |
git-unstaged | Git-backed request names not yet added to the repository |
control | Panels, inputs, footer bar |
field | Input field fill |
separator | Borders and dividers |
text | Primary text |
text-secondary | Secondary labels |
muted | De-emphasized text |
accent | Links, focus rings, primary actions |
selection | Selected row / highlight fill |
tab-underline | Active request tab underline |
resize-handle | Borders, resize grips, high-contrast chrome accents |
variable-token | {{variable}} syntax highlight in editors |
danger, danger-light, warning, success, info | Status colors |
method-get, method-post, … | HTTP method badge colors |
See the Solarized theme example for a complete theme plugin.
hc.commands
Command handlers tie together menus, toolbar actions, and context menu items.
hc.commands.register(id, handler)
Signature: (id: string, handler: (...args: unknown[]) => void | Promise<void>) => Disposable
Manifest: matching contributes.commands entry
Registers a command handler. The id must match a command declared in the manifest and referenced by menu, toolbar, or context menu contributions.
hc.commands.execute(id, ...args)
Signature: (id: string, ...args: unknown[]) => Promise<void>
Runs a registered command programmatically — for example to open a main view from another part of your plugin.
hc.commands.register('myPlugin.openDashboard', () => {
void hc.commands.execute('myPlugin.navigateToView', 'myPlugin.view');
});hc.storage
Plugin-scoped persistent storage. Keys are namespaced by plugin id in the main process. Requires the storage permission.
hc.storage.get(key)
Signature: <T>(key: string) => Promise<T | undefined>
Returns the stored value, or undefined if the key has never been set.
const enabled = await hc.storage.get<boolean>('enabled');hc.storage.set(key, value)
Signature: <T>(key: string, value: T) => Promise<void>
Persists a JSON-serializable value.
await hc.storage.set('enabled', true);Storage-backed store (cross-webview sync)
Separate plugin webviews do not share memory. When one surface writes hc.storage and another needs to react (for example a sidebar and a modal overlay), reload from storage on focus/visibility instead of duplicating read/diff/notify logic in every plugin.
@harborclient/sdk/store provides:
createStorageStore<T>({ storage, key, parse, equals?, keepCurrentWhenMissing? })— returns{ subscribe, getSnapshot, useValue, reloadFromStorage, set }. Hydrates from storage asynchronously on creation; synchronousgetSnapshot()may showparse(undefined)until hydration completes.parsevalidates raw storage into a typed snapshot;setupdates memory and persists (write-through). Default equality usesJSON.stringify; pass a customequalsfor cheaper comparisons. SetkeepCurrentWhenMissing: truewhen an absent storage key should not reset in-memory state. AwaitreloadFromStorage()when you need the persisted value before a synchronous read.syncOnWindowFocus(stores, { intervalMs? })— wiresfocus,visibilitychange, and optional polling toreloadFromStorageon one or more stores. Returns aDisposable; dispose fromdeactivate()or a React effect cleanup.
import type { Disposable, PluginContext } from '@harborclient/sdk';
import { createStorageStore, syncOnWindowFocus } from '@harborclient/sdk/store';
let schemasStore: ReturnType<typeof createStorageStore<unknown[]>>;
let focusSync: Disposable | undefined;
export function activate(hc: PluginContext) {
schemasStore = createStorageStore({
storage: hc.storage,
key: 'schemas',
parse: (raw) => (Array.isArray(raw) ? raw : [])
});
focusSync = syncOnWindowFocus(schemasStore);
}
export function deactivate() {
focusSync?.dispose();
}
// In a component:
const schemas = schemasStore.useValue();
await schemasStore.set([...schemas, newEntry]);Use createExternalStore from the same module for in-webview-only state that does not need persistence.
hc.database
Plugin-scoped SQLite database. Each plugin id gets its own file under HarborClient userData (plugin-databases/{pluginId}.sqlite). Requires the database permission.
Use hc.database when you need indexed queries, relational data, or large structured stores. Keep small settings in hc.storage; the two APIs share no tables and neither can access HarborClient collections or other plugins' data.
get, all, and run accept single-statement parameterized SQL (? placeholders). Use exec for migration scripts (multi-statement DDL). Use transaction for atomic multi-step writes.
hc.database.get(sql, params?)
Signature: <T = Record<string, unknown>>(sql: string, params?: unknown[]) => Promise<T | undefined>
Returns the first row, or undefined when no row matches.
const row = await hc.database.get<{ count: number }>(
'SELECT COUNT(*) AS count FROM events WHERE request_id = ?',
[requestId]
);hc.database.all(sql, params?)
Signature: <T = Record<string, unknown>>(sql: string, params?: unknown[]) => Promise<T[]>
Returns all matching rows.
hc.database.run(sql, params?)
Signature: (sql: string, params?: unknown[]) => Promise<PluginRunResult>
Runs an INSERT, UPDATE, or DELETE statement. Returns { changes, lastInsertRowid }.
hc.database.exec(sql)
Signature: (sql: string) => Promise<void>
Executes a multi-statement SQL script (typically migrations). Rejects scripts containing ATTACH, DETACH, or load_extension.
await hc.database.exec(`
CREATE TABLE IF NOT EXISTS events (
id INTEGER PRIMARY KEY AUTOINCREMENT,
request_id INTEGER NOT NULL,
status INTEGER NOT NULL,
created_at TEXT NOT NULL DEFAULT (datetime('now'))
);
CREATE INDEX IF NOT EXISTS idx_events_request_id ON events(request_id);
`);hc.database.transaction(fn)
Signature: <T>(fn: (tx: PluginDatabaseTx) => Promise<T>) => Promise<T>
Runs fn inside an exclusive transaction. The tx object exposes get, all, and run bound to the same transaction.
await hc.database.transaction(async (tx) => {
await tx.run('INSERT INTO outbox (payload) VALUES (?)', [JSON.stringify(body)]);
await tx.run('UPDATE counters SET value = value + 1 WHERE name = ?', ['sent']);
});Plugin database files are included in HarborClient .hcb backups and removed when the plugin is uninstalled.
hc.fs
Plugin-scoped filesystem access backed by main-process permission checks and a per-plugin path allowlist. Requires filesystem:pick for open/save dialogs, filesystem:read for readFile, and filesystem:write for writeFile. User-selected paths from pick/save dialogs are added to the allowlist automatically; the plugin package directory is allowlisted on load. User-granted paths persist across app restarts and are restored when the plugin loads again.
hc.fs.pickFile(options?)
Signature: (options?: PluginFsPickFileOptions) => Promise<string[]>
Opens a native file picker. Returns absolute paths for the selected files, or an empty array when the dialog is canceled. Requires the filesystem:pick permission.
const paths = await hc.fs.pickFile({
title: 'Choose a schema',
filters: [{ name: 'JSON', extensions: ['json'] }]
});hc.fs.pickDirectory(defaultPath?)
Signature: (defaultPath?: string) => Promise<string | null>
Opens a native directory picker. Returns the selected directory path, or null when canceled. Requires the filesystem:pick permission.
hc.fs.saveFile(content, options?)
Signature: (content: string, options?: PluginFsSaveFileOptions) => Promise<string | null>
Opens a native save dialog and writes content to the chosen path. Returns the saved path, or null when canceled. Requires the filesystem:pick and filesystem:write permissions.
hc.fs.readFile(path)
Signature: (path: string) => Promise<string>
Reads a UTF-8 text file from an allowlisted path. Requires the filesystem:read permission.
hc.fs.writeFile(path, content)
Signature: (path: string, content: string) => Promise<void>
Writes UTF-8 text to an allowlisted path. Requires the filesystem:write permission.
hc.http
Renderer-side HTTP lifecycle events. See Renderer API for full documentation.
Requires the http permission. Use hc.http.onAfterSend when you only need to react to completed sends in the UI — no main entry or polling required.
hc.ipc
Renderer-side RPC into the plugin main entry. See Renderer API.
Requires the ipc permission. Call hc.ipc.invoke(channel, ...args) instead of window.api.invokePluginMain.
hc.host
Typed wrappers for built-in request editor commands. See Renderer API.
Requires the ui permission. Use hc.host.openRequestDraft, hc.host.applyRequestDraft, hc.host.loadRequest, hc.host.sendRequest, hc.host.createCollection, library read/write helpers (listLibraryTree, createFolder, createRequest, …), hc.host.onLibraryChanged, and hc.host.openImageView instead of hc.commands.execute('harborclient:…').
Request creation and update choices
Pick the host API based on the user-facing result you want:
| Goal | API | Result |
|---|---|---|
| Create a new editable request tab | hc.host.openRequestDraft | Opens an unsaved tab seeded with the supplied request fields |
| Update the active request tab in place | hc.host.applyRequestDraft | Replaces supplied fields on the active draft and marks the tab dirty |
| Open an existing saved request by database id | hc.host.loadRequest | Focuses an already-open tab or loads the saved request from a collection |
| Bulk-create saved requests in a new collection | hc.host.createCollection | Persists a collection, optional folders, and saved requests |
| Mutate library tree entities | hc.host.createFolder / … | Create/rename/delete/reorder/move/archive collections, folders, requests |
| List collections / build a custom tree | hc.host.listLibraryTree | Returns summaries for collections, folders, requests, and documents |
| React when the library changes | hc.host.onLibraryChanged | Coarse invalidation so plugins refetch without polling |
| Open an image in a dedicated viewer tab | hc.host.openImageView | Opens or focuses a session-only image-view tab for a path, URL, or bytes |
Use openRequestDraft for history/recent-request style workflows where the plugin should not disturb the current tab. Use applyRequestDraft when the user is intentionally transforming the active request, such as a cURL/import preview tab with an Update button. Use createCollection for importers that create saved requests rather than editing the current tab. Use openImageView for screenshots, logos, generated charts, or import previews that belong in a dedicated image tab.
hc.host.openRequestDraft(payload)
Signature: (payload: OpenRequestDraftPayload) => Promise<void>
Opens a new unsaved request tab seeded with request metadata. Omitted fields use HarborClient defaults (GET, no body, empty headers/params). headers is a flat map; params is an array of enabled query parameter rows.
await hc.host.openRequestDraft({
name: 'Create pet',
method: 'POST',
url: 'https://api.example.com/pets',
headers: { 'Content-Type': 'application/json' },
params: [{ key: 'trace', value: 'true' }],
body: JSON.stringify({ name: 'Fluffy' }),
bodyType: 'json'
});hc.host.applyRequestDraft(payload)
Signature: (payload: ApplyRequestDraftPayload) => Promise<void>
Updates the active request editor tab in place. Provided fields replace the corresponding draft values; when headers or params are supplied, those tables are replaced entirely. The tab becomes dirty, so the user still decides whether to save the changed request to its collection.
function parseExternalFormat(source: string): ApplyRequestDraftPayload {
return {
method: 'PUT',
url: 'https://api.example.com/pets/123',
headers: { 'Content-Type': 'application/json' },
body: source,
bodyType: 'json'
};
}
await hc.host.applyRequestDraft(parseExternalFormat(editorText));
hc.ui.showToast('Request updated');applyRequestDraft throws when there is no active request tab or when a field is invalid. Show parse/update failures inline in your plugin UI when the user needs to fix input.
hc.host.createCollection(payload)
Signature: (payload: CreateCollectionPayload) => Promise<CreateCollectionResult>
Bulk-creates a collection with folders and saved requests. Requests sharing the same folder string are grouped into one folder; requests without folder are created at the collection root.
const { collectionId } = await hc.host.createCollection({
name: 'Petstore API',
requests: [
{
name: 'List pets',
method: 'GET',
url: 'https://api.example.com/pets',
folder: 'pets'
},
{
name: 'Create pet',
method: 'POST',
url: 'https://api.example.com/pets',
folder: 'pets',
body: '{"name":"Fluffy"}',
bodyType: 'json'
}
]
});Library read APIs
Custom collections sidebars need to discover collection ids and subscribe to changes. These APIs require the ui permission and return serializable summaries (ids, names, parent ids, method, sort order, marker) — not full request bodies or document markdown.
| Method | Returns |
|---|---|
hc.host.listCollections(options?) | CollectionSummary[] |
hc.host.listFolders(collectionId) | FolderSummary[] |
hc.host.listRequests(collectionId) | SavedRequestSummary[] |
hc.host.listDocuments(collectionId) | DocumentSummary[] |
hc.host.listLibraryTree(options?) | LibraryTreeSnapshot (collections + nested contents + warnings) |
hc.host.onLibraryChanged(listener) | Disposable |
options.includeArchived defaults to false (active collections only), matching the built-in Collections tree.
Related existing helpers:
hc.host.listCollectionRequests(collectionId, folderId?)— full saved-request rows in sidebar run order (includes body/auth). PreferlistRequests/listLibraryTreefor tree UI.hc.host.getCollectionMetadata(collectionId)— full collection settings row when you already know the id.
async function refreshTree() {
const tree = await hc.host.listLibraryTree();
renderSidebar(tree.collections);
}
const stop = hc.host.onLibraryChanged((event) => {
// event.reason: 'collections' | 'folders' | 'requests' | 'documents'
// event.collectionId is set for per-collection reasons
void refreshTree();
});
await refreshTree();
// Later, when the panel deactivates:
stop.dispose();Granular lists are available when a full tree fetch is too heavy:
const collections = await hc.host.listCollections();
const folders = await hc.host.listFolders(collections[0].id);
const requests = await hc.host.listRequests(collections[0].id);
const documents = await hc.host.listDocuments(collections[0].id);Workflow CRUD
Local workflow registry APIs require the ui permission. Destructive methods are silent — confirm with hc.ui modals before calling deleteWorkflow.
| Method | Returns |
|---|---|
hc.host.listWorkflows() | HostWorkflow[] |
hc.host.getWorkflow(workflowId) | HostWorkflow | null |
hc.host.createWorkflow(input) | HostWorkflow |
hc.host.updateWorkflow(input) | HostWorkflow |
hc.host.renameWorkflow(workflowId, name) | HostWorkflow |
hc.host.deleteWorkflow(workflowId) | void |
hc.host.onWorkflowsChanged(listener) | Disposable |
updateWorkflow replaces actions and durationMs only; name/variables are preserved. Use renameWorkflow to change the display name.
const stop = hc.host.onWorkflowsChanged((event) => {
// event.reason: 'created' | 'updated' | 'renamed' | 'deleted' | 'refreshed'
void hc.host.listWorkflows().then(renderWorkflowList);
});
const workflows = await hc.host.listWorkflows();
const created = await hc.host.createWorkflow({
name: 'Smoke path',
durationMs: 0,
actions: []
});
await hc.host.renameWorkflow(created.id, 'Smoke path (renamed)');
stop.dispose();Navigation and open APIs
Replacement sidebars open host editors and modals through typed hc.host methods (all require ui). These wrap the same Redux paths as the built-in tree — they do not show confirmation dialogs.
| Method | Behavior |
|---|---|
hc.host.loadRequest(requestId) | Opens/focuses the request tab and updates sidebar selection |
hc.host.loadDocument(documentId) | Opens/focuses the markdown tab and updates sidebar selection |
hc.host.openCollectionSettings(collectionId) | Opens the collection settings page tab |
hc.host.openCollectionRunner(collectionId) | Opens the collection runner for the whole collection |
hc.host.openShareModal(collectionId) | Opens the share-collection modal |
hc.host.showEntityContextMenu(input) | Opens the host-built entity context menu (see below) |
loadRequest / loadDocument require the parent collection contents to be cached (call listLibraryTree / expand the collection first), matching loadRequest today.
hc.host.showEntityContextMenu(input)
Signature: (input: ShowEntityContextMenuInput) => Promise<void>
Opens the same collection / folder / request context menu the built-in Collections tree would show — including plugin registerContextMenuItem contributions — positioned in the host window. Fire-and-forget; does not wait for the user to dismiss the menu.
| Field | Type | Description |
|---|---|---|
target | EntityContextMenuTarget | { type: 'collection', collectionId } | folder | request |
x, y | number | Coordinates in the plugin webview viewport |
pluginId | string | Your plugin manifest id (for HostedSurface lookup and focus return) |
contributionId | string | Sidebar panel contribution id mounted in the surface |
The host offsets x/y by the HostedSurface bounding rect. When the surface cannot be found, coordinates are treated as host viewport coordinates.
Limitations
- Document targets are not supported (v1).
- Submenu flyouts and focus return to the webview may be imperfect across the webview boundary.
- Menu actions dispatch host thunks and work even when the built-in Collections tree is unmounted (replacement mode).
row.addEventListener('contextmenu', (event) => {
event.preventDefault();
void hc.host.showEntityContextMenu({
target: { type: 'request', requestId },
x: event.clientX,
y: event.clientY,
pluginId: 'com.example.tree',
contributionId: 'collections'
});
});See the sidebar replacement tree example for a full pattern including reorder/move.
await hc.host.loadRequest(requestId);
await hc.host.loadDocument(documentId);
await hc.host.openCollectionSettings(collectionId);
await hc.host.openCollectionRunner(collectionId);Sidebar selection bridge
Plugins that replace the Collections sidebar stay in sync with host “reveal in sidebar” / breadcrumb / tab focus via a serializable SidebarSelection union:
type SidebarSelection =
| { kind: 'collection'; collectionId: number }
| { kind: 'folder'; collectionId: number; folderId: number }
| {
kind: 'request';
collectionId: number;
folderId: number | null;
requestId: number;
}
| {
kind: 'document';
collectionId: number;
folderId: number | null;
documentId: number;
};| Method | Returns / effect |
|---|---|
hc.host.getSidebarSelection() | SidebarSelection | null |
hc.host.setSidebarSelection(selection) | Updates host Redux; opens request/document tabs |
hc.host.onSidebarSelectionChanged(listener) | Disposable — fires on host and plugin-driven changes |
Selection is derived from Redux collection/folder highlight plus the active request or document tab (not the built-in tree’s local multi-select set).
const current = await hc.host.getSidebarSelection();
const stop = hc.host.onSidebarSelectionChanged((selection) => {
highlightRow(selection);
});
await hc.host.setSidebarSelection({
kind: 'request',
collectionId: 1,
folderId: null,
requestId: 42
});Sidebar panel view context
When a sidebarPanels contribution is mounted (including replaces: "collections"), HostedSurface pushes:
interface SidebarPanelViewContext {
sidebarSelection: SidebarSelection | null;
}Read it on mount with hc.view.getContext() (same pattern as request-tab surfaces). Live updates use onSidebarSelectionChanged.
Replacement-panel keyboard shortcuts
When a panel with replaces: "collections" is registered:
- Focus collections sidebar (
focus-first-collection) reveals the primary surface and focuses the plugin webview (not a hidden built-in row). - Focus sidebar search (
focus-sidebar-search) does the same — plugins own internal search UI; the built-in#sidebar-searchinput is not mounted. - Focus environments remains a no-op while the built-in Environments section is hidden by a collections replacement.
Library write APIs
Custom collections sidebars need the same day-to-day mutations as the built-in tree (create, rename, delete, reorder, move, archive) without accessing Redux. These APIs require the ui permission and wrap the host store thunks used by the Collections sidebar.
Destructive methods are silent. Host methods do not show confirmation dialogs or toasts. Plugins must confirm with hc.ui modals (or equivalent) before calling delete*, setCollectionArchived, and similar.
Created entities return summaries (same shapes as the list APIs) so plugins can update UI optimistically and reconcile via onLibraryChanged.
| Method | Returns |
|---|---|
hc.host.updateCollection({ id, name }) | CollectionSummary |
hc.host.deleteCollection(collectionId) | void |
hc.host.reorderCollections(orderedIds) | void |
hc.host.setCollectionArchived({ collectionId, archived }) | void |
hc.host.duplicateCollection(collectionId) | CollectionSummary |
hc.host.createFolder({ collectionId, name, parentFolderId? }) | FolderSummary |
hc.host.renameFolder({ folderId, collectionId, name }) | FolderSummary |
hc.host.deleteFolder({ folderId, collectionId }) | void |
hc.host.moveFolder({ collectionId, folderId, parentFolderId, sortOrder? }) | FolderSummary |
hc.host.reorderFolders({ collectionId, parentFolderId, orderedFolderIds }) | void |
hc.host.createRequest({ collectionId, folderId?, name?, method?, url? }) | SavedRequestSummary |
hc.host.deleteRequest(requestId) | void |
hc.host.duplicateRequest(requestId) | SavedRequestSummary |
hc.host.moveRequest({ collectionId, requestId, folderId, index? }) | void |
hc.host.reorderRequests({ collectionId, folderId, orderedRequestIds }) | void |
hc.host.createDocument({ collectionId, folderId?, name, content? }) | DocumentSummary |
hc.host.renameDocument({ id, collectionId, name }) | DocumentSummary |
hc.host.deleteDocument({ id, collectionId }) | void |
hc.host.moveDocument({ collectionId, documentId, folderId, index? }) | void |
hc.host.reorderDocuments({ collectionId, folderId, orderedDocumentIds }) | void |
hc.host.reorderContainerItems({ collectionId, folderId, items }) | void |
Bulk create remains available as hc.host.createCollection(payload).
Collections
| Method | Parameters | Notes |
|---|---|---|
updateCollection | id: number, name: string | Renames only; other settings are preserved |
deleteCollection | collectionId: number | Silent; moves to trash when supported |
reorderCollections | orderedIds: number[] | Full top-level collection order |
setCollectionArchived | collectionId: number, archived: boolean | Silent |
duplicateCollection | collectionId: number | Places the copy below the original |
Folders
| Method | Parameters | Notes |
|---|---|---|
createFolder | collectionId, name, optional parentFolderId | Returns created folder summary |
renameFolder | folderId, collectionId, name | |
deleteFolder | folderId, collectionId | Deletes subtree; silent |
moveFolder | collectionId, folderId, parentFolderId, optional sortOrder | |
reorderFolders | collectionId, parentFolderId, orderedFolderIds | Sibling order under one parent |
Requests
| Method | Parameters | Notes |
|---|---|---|
createRequest | collectionId, optional folderId / name / method / url | Defaults: Untitled Request, GET, empty URL; opens a tab |
deleteRequest | requestId | Silent |
duplicateRequest | requestId | Opens the copy in a tab |
moveRequest | collectionId, requestId, folderId, optional index | Omitting index appends |
reorderRequests | collectionId, folderId, orderedRequestIds |
Documents
| Method | Parameters | Notes |
|---|---|---|
createDocument | collectionId, name, optional folderId / content | Does not open a tab |
renameDocument | id, collectionId, name | Body unchanged |
deleteDocument | id, collectionId | Silent |
moveDocument | collectionId, documentId, folderId, optional index | Omitting index appends |
reorderDocuments | collectionId, folderId, orderedDocumentIds |
Mixed containers
| Method | Parameters | Notes |
|---|---|---|
reorderContainerItems | collectionId, folderId, items: { kind, id }[] | Interleaved request + document order; prefer over separate reorder APIs |
Reorder / move pattern
Replacement trees commit drag-end or “Move up/down” actions through the host APIs above, then refresh via onLibraryChanged (or an explicit relist). Prefer reorderContainerItems when a folder interleaves requests and documents.
Do not expect the host to ship a DnD library into the plugin webview. Plugins may use SortableSidebarItem / buildReorderMenuGroup from @harborclient/sdk/components (dnd-kit is an SDK dependency) or implement their own pointer DnD and call move* / reorder* on drop.
See the sidebar replacement tree example.
const { collectionId } = await hc.host.createCollection({
name: 'Auth API',
requests: []
});
await hc.host.createFolder({ collectionId, name: 'Auth' });
const folder = await hc.host.createFolder({
collectionId,
name: 'Tokens',
parentFolderId: /* parent id */
});
const request = await hc.host.createRequest({
collectionId,
folderId: folder.id,
name: 'Login',
method: 'POST'
});
await hc.host.moveRequest({
collectionId,
requestId: request.id,
folderId: null
});
await hc.host.reorderRequests({
collectionId,
folderId: null,
orderedRequestIds: [request.id]
});
await hc.host.setCollectionArchived({ collectionId, archived: true });
await hc.host.deleteRequest(request.id);hc.host.openImageView(payload)
Signature: (payload: OpenImageViewPayload) => Promise<void>
Opens or focuses an image-view page tab. Use this to display screenshots, logos, generated charts, or import previews in a dedicated tab with Copy location and Download actions. Prefer this typed API over hc.commands.execute('harborclient:openImageView', payload).
See also Renderer API → hc.host.
Payload rules
- Provide exactly one source:
path,url,dataUrl, orbase64withcontentType. fileNameis optional forpathandurl(derived from the basename or last URL path segment). It is required for inlinedataUrlandbase64payloads.- Inline
dataUrl/base64payloads are capped by the same IPC body-size limit as large request bodies.
Tab behavior
| Aspect | Behavior |
|---|---|
| Tab label | Shortened filename (middle ellipsis, extension preserved) |
| Page header | Full filename |
| Deduping | Reopening the same source focuses the existing tab |
| Persistence | Session-only — image tabs are not restored after restart |
| In-tab actions | Copy location (path, URL, or data URL) and Download via a save dialog |
// From a menu action or command handler
await hc.host.openImageView({
url: 'https://harborclient.com/images/logo.png'
});
// After the user picks a file with hc.fs.pickFile
await hc.host.openImageView({ path: selectedPath });
// Inline bytes from a plugin-generated PNG
await hc.host.openImageView({
fileName: 'preview.png',
base64: pngBase64,
contentType: 'image/png'
});Global variables
HarborClient stores app-wide variables in Settings → Globals. They use the same Variable shape as collection and environment variables (key, value, defaultValue, share) and participate in {{key}} substitution with the lowest precedence in the static chain:
globals → collection → environment
Request scripts can mutate globals with hc.globals.get / hc.globals.set; values persist after the send completes. See Request scripts — hc.globals.
Reading globals from plugins
Request tab components receive the merged runtime map on RequestTabContext.variables. Global values are included automatically; collection and environment variables override globals when they define the same key:
function AuditTab({ context }: { context: RequestTabContext }) {
const baseUrl = context.variables.baseUrl;
const token = context.variables.token;
// ...
}This snapshot reflects the editor state before send. It does not include ephemeral values from hc.request.variables.set during an in-flight send.
Updating globals from plugins
Replace all global variables with a new list via the built-in host command (requires the ui permission):
await hc.commands.execute('harborclient:updateGlobalVariables', [
{ key: 'baseUrl', value: 'https://api.example.com', defaultValue: '', share: true },
{ key: 'apiKey', value: '', defaultValue: 'dev-key', share: false }
]);Each row uses PluginVariableInput: key, value, optional defaultValue, optional share.
To create or update environment variables instead, use hc.host.createEnvironmentWithVariables and hc.host.updateEnvironmentVariables.
hc.imports
Register handlers for File → Import so plugins can participate in the unified import flow instead of adding separate File menu items.
Requires the ui permission. Call registerImportHandler(hc, extensions, handler) or hc.imports.registerHandler(extensions, handler) — registration disposables are tracked automatically.
Built-in HarborClient formats (HarborClient exports, Postman, Bruno, HAR, OpenCollection, and OpenAPI) are detected first. Plugin handlers run only when the selected file is not recognized as a built-in format and its extension matches a registered handler.
Handlers run in registration order. The first handler whose canImport returns true receives the file. Throw an Error from import to surface a blocking failure in the host.
Common patterns
Direct import — parse file.contents inside import and create HarborClient data immediately (for example with hc.host.createCollection). Use when the user does not need a preview step.
Preview UI — stash the selected ImportFile in plugin state, then open a registered main view with hc.commands.execute('harborclient:openMainView', hc.pluginId, viewId). The preview component reads the stashed file, lets the user confirm selections, and calls host APIs when ready.
See the Import handler example for a complete walkthrough. OpenAPI 3.x and OpenCollection import are built into HarborClient (File → Import); use import handlers for additional custom formats.
registerImportHandler(hc, extensions, handler)
Signature: (hc: PluginContext, extensions: string | string[], handler: ImportHandler) => Disposable
Convenience wrapper around hc.imports.registerHandler.
import { registerImportHandler } from '@harborclient/sdk';
registerImportHandler(hc, '.json', {
canImport: (file) => {
try {
const parsed = JSON.parse(file.contents) as { bundleFormat?: unknown; version?: unknown };
return parsed.bundleFormat === 'request-bundle' && parsed.version === 1;
} catch {
return false;
}
},
import: async (file) => {
// Direct import: create a collection immediately, or open a preview main view.
await hc.host.createCollection({
name: 'Imported bundle',
requests: [{ name: 'Example', method: 'GET', url: 'https://example.com' }]
});
}
});hc.imports.registerHandler(extensions, handler)
Signature: (extensions: string | string[], handler: ImportHandler) => Disposable
| Callback | Type | Description |
|---|---|---|
canImport | (file: ImportFile) => boolean | Promise<boolean> | Returns whether this handler should import the file |
import | (file: ImportFile) => void | Promise<void> | Performs the import workflow |
ImportFile includes name, path, extension (dot-prefixed, lowercase), and UTF-8 contents.
Extensions may be passed with or without a leading dot (json and .json are equivalent). Register multiple extensions in one call: ['.json', '.yaml', '.yml'].
hc.mcp
Register remote MCP client servers so Harbor's chat agent can discover and call tools from external MCP endpoints over Streamable HTTP or legacy SSE.
Requires the mcp permission. Registrations are activation-scoped: Harbor connects while the plugin is enabled and removes them when you dispose the returned handle or the plugin unloads. Plugin-owned servers appear as read-only rows in Settings → AI & MCP with plugin attribution; they are not copied into user MCP settings.
hc.mcp.registerServer(config)
Signature: (config: PluginMcpServerConfig) => Disposable
| Field | Type | Description |
|---|---|---|
name | string | Display name in Settings → AI & MCP |
serverURL | string | Absolute HTTP or HTTPS MCP endpoint URL |
enabled | boolean (optional) | When false, Harbor skips connecting. Defaults to true |
headers | PluginMcpHeader[] | Optional HTTP headers sent with MCP client requests |
icon | string (optional) | Optional square icon as a data:image/...;base64,... URI for settings rows |
hc.mcp.registerServer({
name: 'WordPress',
icon: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg==',
serverURL: 'https://public-api.wordpress.com/wpcom/v2/mcp/v1',
enabled: true,
headers: [{ key: 'Authorization', value: 'Bearer token' }]
});Discovered tools are prefixed with mcp__ in the chat agent tool list, using the same naming scheme as user-configured MCP client servers.
Not extensible
These built-in surfaces are not open to plugin contributions:
- Open request tab strip — tabs for unsaved/saved requests in the editor workspace.
- AI sidebar — the built-in assistant panel.
- Native window chrome — title bar and window controls (menu contributions use the application menu only).
