In our last post, Logging Requests with a HarborClient Plugin, we added logging: a Settings path for a JSONL file, and a renderer hc.http.onAfterSend hook that appends each completed exchange. That is useful for offline review — but you still have to leave HarborClient to read the file.
This tutorial continues that plugin and gives the logs a real UI: a footer button labeled Request Logs that opens a slide-up panel listing recent method, URL, and status as requests finish. File logging stays in place.
What you will add
- A
footerPanelscontribution titled Request Logs (that title is the footer toggle label) - A small in-memory store of recent log entries, updated from the same
onAfterSendhandler - A
RequestLogsPanelReact component with a clear action and empty state - Registration via
hc.ui.registerFooterPanel
Prerequisites: finish the previous tutorial (or keep an equivalent request-logger plugin loaded unpacked). You still need the ui and http permissions you already declared.
Declare the footer panel in the manifest
Open manifest.json and add a footerPanels entry next to your settings section. The id must match what you pass to registerFooterPanel:
"contributes": {
"settingsSections": [{ "id": "settings", "title": "Request Logger" }],
"footerPanels": [{ "id": "request-logs", "title": "Request Logs" }]
}

HarborClient wraps your panel component in a resizable shell (drag handle, height persistence, host close button). You only supply the content.
Share log entries with a small store
The footer panel and the after-send hook need a shared list in the renderer webview. Use createExternalStore from @harborclient/sdk/store for in-memory state that does not need persistence across reloads — the file on disk remains the durable record.
Create src/logStore.ts:
import { createExternalStore } from '@harborclient/sdk/store';
export type LogEntry = {
at: string;
method: string;
url: string;
status: number;
statusText: string;
sourceRequestName: string | null;
};
const MAX_ENTRIES = 200;
export const logStore = createExternalStore<LogEntry[]>([]);
export function pushLogEntry(entry: LogEntry): void {
const next = [entry, ...logStore.getSnapshot()].slice(0, MAX_ENTRIES);
logStore.setState(next);
}
export function clearLogEntries(): void {
logStore.setState([]);
}

In src/renderer.tsx, keep writing the JSONL file, and also push into the store. Inside your existing onAfterSend handler, after you build entry:
import { pushLogEntry } from './logStore';
// inside onAfterSend, after building `entry`:
pushLogEntry({
at: entry.at,
method: entry.method,
url: entry.url,
status: entry.status,
statusText: entry.statusText,
sourceRequestName: entry.sourceRequestName
});
try {
await appendLogLine(hc, JSON.stringify(entry));
} catch (error) {
console.error('Failed to write request log', error);
}

If you want a deeper refresher on onAfterSend and other lifecycle callbacks, see SDK Hooks.
Build the Request Logs panel
Add src/components/RequestLogsPanel.tsx. Follow the footer layout contract: fill the shell with flex h-full min-h-0 flex-col, put the list in a flex-1 overflow-auto child, and leave roughly 32px of right padding on the header so controls do not sit under the host close button.
import { useSyncExternalStore } from '@harborclient/sdk/react';
import { clearLogEntries, logStore } from '../logStore';
export function RequestLogsPanel() {
const entries = useSyncExternalStore(
logStore.subscribe,
logStore.getSnapshot,
logStore.getSnapshot
);
return (
<div className="flex h-full min-h-0 flex-col bg-control">
<div className="flex shrink-0 items-center justify-between gap-3 border-b border-separator px-3 py-2 pr-8">
<h3 className="text-[14px] font-medium text-text">Request Logs</h3>
<button type="button" onClick={() => clearLogEntries()}>
Clear
</button>
</div>
<div className="min-h-0 flex-1 overflow-auto">
{entries.length === 0 ? (
<p className="p-3 text-muted">No requests logged yet. Send a request to see it here.</p>
) : (
<ul className="divide-y divide-separator">
{entries.map((entry) => (
<li key={entry.at + entry.url + entry.status} className="px-3 py-2">
<div className="flex flex-wrap items-baseline gap-x-2 gap-y-1">
<span className="font-medium">{entry.method}</span>
<span className="break-all">{entry.url}</span>
<span className="text-muted">
{entry.status} {entry.statusText}
</span>
</div>
<p className="text-muted">
{entry.sourceRequestName ?? 'Untitled'} · {entry.at}
</p>
</li>
))}
</ul>
)}
</div>
</div>
);
}

Register the panel in activate
Still in src/renderer.tsx, register the footer panel next to your settings section. Optionally set a footer indicator when the list is non-empty so the toggle shows activity:
import { RequestLogsPanel } from './components/RequestLogsPanel';
import { logStore } from './logStore';
export function activate(hc: PluginContext): void {
// ... existing SettingsPanelHost + registerSettingsSection ...
hc.ui.registerFooterPanel({
id: 'request-logs',
title: 'Request Logs',
Component: RequestLogsPanel
});
const syncIndicator = () => {
const count = logStore.getSnapshot().length;
hc.ui.setFooterPanelIndicator(
'request-logs',
count === 0
? null
: { status: 'info', label: `${count} request log${count === 1 ? '' : 's'}` }
);
};
logStore.subscribe(syncIndicator);
syncIndicator();
hc.http.onAfterSend(async (request, response) => {
// build entry, pushLogEntry, appendLogLine, then:
syncIndicator();
});
}
The host tracks the Disposable from registerFooterPanel. The store subscription is module-level; clearing entries or unloading the plugin resets what the panel shows. For a fuller cleanup story, see SDK Hooks.
Rebuild and try it
pnpm build # or, while iterating: pnpm dev
- Confirm the unpacked Request Logger plugin is enabled.
- Click Request Logs in the window footer (beside Console, Variables, and other panels).
- Send a few requests. New rows should appear at the top of the panel, and the JSONL file should still grow.
- Use Clear to empty the in-memory list (the file on disk is unchanged).

If the toggle is missing, check that contributes.footerPanels matches the registration id, rebuild so dist/ is current, and reload the unpacked plugin.
Next steps
From here you can filter the list by status, open the matching request with hc.host.loadRequest when sourceRequestId is present, or persist a capped recent list with createCappedList if you want the panel to survive reloads. When you are ready to share the plugin, package a built dist/ and follow the marketplace steps in How to Write a HarborClient Plugin.
More resources:
- Logging Requests with a HarborClient Plugin
- SDK Hooks
- Plugin development guide —
registerFooterPanel - Plugin skeleton







Leave a Reply