From 21d27dba9785b31a1e90f272d8c8114f7d0c0b0c Mon Sep 17 00:00:00 2001 From: DocTator Date: Sun, 27 Sep 2026 11:10:50 -0400 Subject: [PATCH 1/3] WIP: scaffold HTTP Client plugin (untested, incomplete) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit hermes-plugin.json (id doctatortot.http-client, permissions network+ storage+clipboard.write — no engines/minAppVersion yet since this depends on api.network.request, which is hermes-ide#384, not yet merged/released), activate.ts, and HttpClientPanel.tsx (method/url/header rows/body form, status+headers+body response view with JSON pretty-print, copy-to- clipboard, last-20 request history via storage). NOT YET DONE: npm install, npm run build, tsc check, any actual testing against a running app. Pausing here — Doc is picking this up from another session. Co-Authored-By: Claude Sonnet 5 --- plugins/http-client/.gitignore | 3 + plugins/http-client/hermes-plugin.json | 33 ++ plugins/http-client/package.json | 18 ++ plugins/http-client/src/HttpClientPanel.tsx | 335 ++++++++++++++++++++ plugins/http-client/src/activate.ts | 64 ++++ plugins/http-client/tsconfig.json | 17 + plugins/http-client/vite.config.ts | 26 ++ 7 files changed, 496 insertions(+) create mode 100644 plugins/http-client/.gitignore create mode 100644 plugins/http-client/hermes-plugin.json create mode 100644 plugins/http-client/package.json create mode 100644 plugins/http-client/src/HttpClientPanel.tsx create mode 100644 plugins/http-client/src/activate.ts create mode 100644 plugins/http-client/tsconfig.json create mode 100644 plugins/http-client/vite.config.ts diff --git a/plugins/http-client/.gitignore b/plugins/http-client/.gitignore new file mode 100644 index 0000000..1ab415f --- /dev/null +++ b/plugins/http-client/.gitignore @@ -0,0 +1,3 @@ +node_modules/ +dist/ +*.tgz diff --git a/plugins/http-client/hermes-plugin.json b/plugins/http-client/hermes-plugin.json new file mode 100644 index 0000000..5ca48d9 --- /dev/null +++ b/plugins/http-client/hermes-plugin.json @@ -0,0 +1,33 @@ +{ + "id": "doctatortot.http-client", + "name": "HTTP Client", + "version": "0.1.0", + "description": "Lightweight REST client for quick API testing — send GET/POST/PUT/PATCH/DELETE requests, inspect the response, and recall your last few requests.", + "author": "doctatortot", + "main": "dist/index.js", + "activationEvents": [ + { "type": "onStartup" } + ], + "contributes": { + "commands": [ + { + "command": "http-client.open", + "title": "Open HTTP Client", + "category": "HTTP Client" + } + ], + "panels": [ + { + "id": "http-client-panel", + "name": "HTTP Client", + "side": "left", + "icon": "" + } + ] + }, + "permissions": [ + "network", + "storage", + "clipboard.write" + ] +} diff --git a/plugins/http-client/package.json b/plugins/http-client/package.json new file mode 100644 index 0000000..910e07c --- /dev/null +++ b/plugins/http-client/package.json @@ -0,0 +1,18 @@ +{ + "name": "@hermes-hq/plugin-http-client", + "version": "0.1.0", + "private": true, + "type": "module", + "scripts": { + "build": "vite build", + "dev": "vite build --watch" + }, + "devDependencies": { + "@types/react": "^19.2.14", + "typescript": "^5.4.0", + "vite": "^8.0.0" + }, + "peerDependencies": { + "react": "^18.0.0" + } +} diff --git a/plugins/http-client/src/HttpClientPanel.tsx b/plugins/http-client/src/HttpClientPanel.tsx new file mode 100644 index 0000000..9178ee6 --- /dev/null +++ b/plugins/http-client/src/HttpClientPanel.tsx @@ -0,0 +1,335 @@ +import { useEffect, useState } from "react"; +import { getAPI } from "./activate"; + +const METHODS = ["GET", "POST", "PUT", "PATCH", "DELETE"] as const; +type Method = (typeof METHODS)[number]; + +const HAS_BODY: Record = { + GET: false, + POST: true, + PUT: true, + PATCH: true, + DELETE: false, +}; + +interface HeaderRow { + key: string; + value: string; +} + +interface HistoryEntry { + id: string; + method: Method; + url: string; + headers: HeaderRow[]; + body: string; +} + +interface ResponseState { + status: number; + headers: Record; + body: string; +} + +const HISTORY_KEY = "requestHistory"; +const MAX_HISTORY = 20; + +function prettyBody(body: string): string { + try { + return JSON.stringify(JSON.parse(body), null, 2); + } catch { + return body; + } +} + +function headersToRecord(rows: HeaderRow[]): Record { + const out: Record = {}; + for (const row of rows) { + const key = row.key.trim(); + if (key) out[key] = row.value; + } + return out; +} + +function statusColor(status: number): string { + if (status >= 200 && status < 300) return "var(--success, #3fb950)"; + if (status >= 400) return "var(--error, #f85149)"; + return "var(--text-2)"; +} + +export function HttpClientPanel() { + const api = getAPI(); + + const [method, setMethod] = useState("GET"); + const [url, setUrl] = useState(""); + const [headers, setHeaders] = useState([{ key: "", value: "" }]); + const [body, setBody] = useState(""); + const [sending, setSending] = useState(false); + const [response, setResponse] = useState(null); + const [error, setError] = useState(null); + const [history, setHistory] = useState([]); + + useEffect(() => { + api.storage.get(HISTORY_KEY).then((raw) => { + if (!raw) return; + try { + setHistory(JSON.parse(raw)); + } catch { + // Corrupt/old-format history — start fresh rather than crash the panel. + } + }); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + function updateHeader(index: number, field: "key" | "value", value: string) { + setHeaders((prev) => { + const next = [...prev]; + next[index] = { ...next[index], [field]: value }; + // Always keep one trailing empty row so there's always room to add another. + if (index === next.length - 1 && (next[index].key || next[index].value)) { + next.push({ key: "", value: "" }); + } + return next; + }); + } + + function removeHeader(index: number) { + setHeaders((prev) => prev.filter((_, i) => i !== index)); + } + + async function send() { + if (!url.trim()) { + api.ui.showToast("Enter a URL first", { type: "warning" }); + return; + } + setSending(true); + setError(null); + setResponse(null); + try { + const result = await api.network.request(method, url.trim(), { + headers: headersToRecord(headers), + body: HAS_BODY[method] && body.trim() ? body : undefined, + }); + setResponse(result); + + const entry: HistoryEntry = { + id: `${Date.now()}`, + method, + url: url.trim(), + headers, + body, + }; + const nextHistory = [entry, ...history.filter((h) => !(h.method === method && h.url === entry.url))].slice(0, MAX_HISTORY); + setHistory(nextHistory); + await api.storage.set(HISTORY_KEY, JSON.stringify(nextHistory)); + } catch (err) { + setError(String(err)); + api.ui.showToast("Request failed", { type: "error" }); + } finally { + setSending(false); + } + } + + function loadFromHistory(entry: HistoryEntry) { + setMethod(entry.method); + setUrl(entry.url); + setHeaders(entry.headers.length > 0 ? [...entry.headers, { key: "", value: "" }] : [{ key: "", value: "" }]); + setBody(entry.body); + setResponse(null); + setError(null); + } + + async function copyResponseBody() { + if (!response) return; + await api.clipboard.writeText(response.body); + api.ui.showToast("Response body copied", { type: "success" }); + } + + const inputStyle: React.CSSProperties = { + background: "var(--bg-2)", + color: "var(--text-1)", + border: "1px solid var(--border)", + borderRadius: "var(--radius-sm)", + padding: "5px 8px", + fontSize: "var(--text-sm)", + fontFamily: "var(--font-mono)", + }; + + const labelStyle: React.CSSProperties = { + fontSize: "var(--text-xs)", + fontWeight: 600, + textTransform: "uppercase", + color: "var(--text-2)", + letterSpacing: "0.05em", + }; + + return ( +
+

HTTP CLIENT

+ +
+ + setUrl(e.target.value)} + placeholder="https://api.example.com/resource" + style={{ ...inputStyle, flex: 1 }} + /> +
+ +
+ Headers + {headers.map((row, i) => ( +
+ updateHeader(i, "key", e.target.value)} + placeholder="Header" + style={{ ...inputStyle, flex: 1 }} + /> + updateHeader(i, "value", e.target.value)} + placeholder="Value" + style={{ ...inputStyle, flex: 1 }} + /> + {i < headers.length - 1 && ( + + )} +
+ ))} +
+ + {HAS_BODY[method] && ( +
+ Body +