Instrument console footer strip with Request Logs panel rows

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 footerPanels contribution titled Request Logs (that title is the footer toggle label)
  • A small in-memory store of recent log entries, updated from the same onAfterSend handler
  • A RequestLogsPanel React 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.

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" }]
}
IDE open on request-logger manifest.json showing footerPanels Request Logs contribution
manifest.json with the Request Logs footer panel contribution.

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([]);
}
IDE open on src/logStore.ts with createExternalStore LogEntry helpers
logStore.ts for the in-memory request log list.

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);
}
renderer.tsx onAfterSend calling pushLogEntry and appendLogLine
Push into the store and still append the JSONL file.

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>
  );
}
IDE open on RequestLogsPanel.tsx with Clear button and log list UI
RequestLogsPanel.tsx for the slide-up footer content.

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
  1. Confirm the unpacked Request Logger plugin is enabled.
  2. Click Request Logs in the window footer (beside Console, Variables, and other panels).
  3. Send a few requests. New rows should appear at the top of the panel, and the JSONL file should still grow.
  4. Use Clear to empty the in-memory list (the file on disk is unchanged).
HarborClient with Request Logs footer panel listing POST and GET 200 responses
The Request Logs footer panel after sending a few requests.

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:

Leave a Reply

Trending

Discover more from HarborClient Blog

Subscribe now to keep reading and get access to the full archive.

Continue reading