feat(ui-rewrite): POC Code tab + Preview for prompt details (#5448)#5452
feat(ui-rewrite): POC Code tab + Preview for prompt details (#5448)#5452a-effort wants to merge 4 commits into
Conversation
|
This is a POC for prompt details drawer "preview" functionality (temp location: won't be an in page option in Prompts) preview.mp4 |
marekdano
left a comment
There was a problem hiding this comment.
Clean POC for the prompt Code tab that will slot into the #5323 drawer. I'm happy with the flow and implementation.
Just a few findings:
Stale preview state when prompt changes (bug)
usePromptPreview never resets result/error/hasRun when promptId changes. PromptCodeTab resets args on prompt.id change via useEffect, but the preview hook holds its own state independently:
// PromptCodeTab.tsx — args are reset, but preview result is not
useEffect(() => {
setArgs(seedArgs(prompt));
}, [prompt.id]); // preview.result from the old prompt is still shownIf a user previews prompt A then switches to prompt B, they'll see prompt A's result until they click Preview again. Add a reset effect in the hook, or force remount via <PromptCodeTab key={prompt.id} prompt={prompt} />.
Inconsistency: snippets use prompt.name, Preview API uses prompt.id
PromptSnippetTabs receives prompt.name and generates URLs like /prompts/greet_user, while usePromptPreview(prompt.id, args) calls /prompts/p1. If the backend only accepts one form (ID vs slug), users copying a snippet could get 404s. Verify the backend accepts both, or unify the identifier used across snippets and the API call.
CodeBlock couples a generic UI component to prompt-specific i18n and gateway internals
src/components/ui/code-block.tsx imports from two places it shouldn't:
import { copyToClipboard } from "@/components/gateways/utils"— aui/component shouldn't depend on agateways/utility.- The copy toast hardcodes
"prompts.details.code.copySuccess"— ifCodeBlockis reused for tool or resource details, the toast will say "Copied curl snippet" incorrectly.
The copy callback and toast message should be caller-supplied (e.g. onCopy?: () => void) or the i18n key should be a prop.
promptName in snippets is not validated
validatePromptId guards the API call, but promptName (from prompt.name) flows directly into snippet strings without validation. A prompt whose name contains backticks would produce a malformed TypeScript snippet. Assert promptName matches the same [a-zA-Z0-9_-]+ pattern before passing it to builders.
Suggestions
args ?? {} is dead code in snippet builders — SnippetInput.args is typed Record<string, string> (non-nullable). The ?? {} fallbacks in buildCurl, buildPython, and buildTypescript can't fire. Remove them.
0420601 to
2f5fc4a
Compare
|
Thanks for the review @marekdano! Feedback addressed in two commits:
Stale preview state on prompt change: fixed in 8e59615. Reset moved into CodeBlock coupling: fixed in 8e59615. promptName not validated: fixed in 8e59615 and 75bbcf5. URL encoding used instead of regex validation, since the backend's name pattern (space/dot/dash allowed via Snippets use
Also in 75bbcf5:
|
marekdano
left a comment
There was a problem hiding this comment.
All findings from the previous review are addressed correctly!
LGTM 🚀 after fixing DCO check
Prototype for #5448 — the Code tab and render-only Preview action that will slot into the prompt details drawer being built in #5323. Single public seam: `<PromptCodeTab prompt={selected} />`. Functionality ------------- - Args form, schema-driven from `PromptRead.arguments`; controlled, owns no state of its own. Required/optional badge per arg. - Snippet tabs: curl, JSON-RPC, Python, TypeScript. All four rebuild live from the args form via `useMemo`. Per-snippet Copy button with sonner toast feedback. Endpoint + auth footer shows the literal env vars (`$MCPGATEWAY_URL`, `$MCPGATEWAY_BEARER_TOKEN`). - Preview panel hits `POST /prompts/{id}` (render-only, no LLM), shows status pill (200 OK / Render failed), client-measured render time, and the rendered MCP messages via `JsonHighlighter`. Button toggles Preview → Re-run after first run; in-flight state disables the button. Failures also surface a sonner error toast. - Temporary `src/pages/Prompts.tsx` mount with a Select picker so the prototype can be exercised against a live gateway; replaced when #5323 lands the drawer. Dev notes --------- - Snippet builders are pure functions of `{ promptName, args }` so the args form / drawer state machine and the snippet shape are independently testable. 19-test escape-safety matrix covers `"`, `\`, `\n`, `${VAR}`, and `'` round-trip through each language's parser. - Preview goes through a new `src/api/prompts.ts` thin wrapper (not the raw orval fetcher). This keeps the cookie-auth + CSRF + 401→login redirect of the local `api` client, and isolates the Preview-tagging open question from the issue (header vs `/preview` route) to a single function. - `client/src/components/ui/tabs.tsx` is a new shadcn-style radix Tabs wrapper — shared infra both this PR and #5323's Code|Definition tabs will use. No new deps; the `radix-ui` umbrella package was already installed. - i18n keys land in `prompts.details.code.*` and `prompts.details.preview.*` across en-US/es-ES/pt-BR; native-speaker review of es/pt strings still pending. - All-strings args model. `PromptArgument` has no type info; values flow as `Record<string, string>` to match the gateway's `Dict[str, str]` body. Inline validation deferred. - Plugin-passed count omitted from the status row — orval response is typed `unknown` and there is no plugin-trace field yet. Layout scaffold is in place; wiring is one line when the backend grows the field. Marked with `TODO(#5448 followup)`. - `vi.spyOn(navigator.clipboard, "writeText")` in `PromptSnippetTabs.test.tsx` — jsdom defines `navigator.clipboard` via a getter, so `Object.defineProperty(navigator, "clipboard", { value })` silently no-ops. Documented inline. Tests ----- - 49 new tests across the POC files; all 1664 client tests pass. - `tsc -b` clean. - Not yet exercised: e2e Playwright (skip until the drawer ships). Files ----- - `client/src/components/ui/tabs.tsx` (+ test) — radix wrapper - `client/src/api/prompts.ts` (+ test) — `promptsApi.render` + ID guard - `client/src/components/prompts/snippets/` — pure builders + matrix - `client/src/components/prompts/PromptArgsForm.tsx` (+ test) - `client/src/components/prompts/PromptSnippetTabs.tsx` (+ test) - `client/src/components/prompts/PromptPreviewPanel.tsx` (+ test) - `client/src/components/prompts/PromptCodeTab.tsx` (+ test) — public seam - `client/src/components/prompts/index.ts` — barrel - `client/src/i18n/locales/{en-US,es-ES,pt-BR}/prompts.json` + index updates - `client/src/pages/Prompts.tsx` — temporary mount Signed-off-by: a-effort <anna.effort@ibm.com>
…ighter (#5448) Style and structural follow-up to commit 9abeb13. Brings the prototype's Code tab visual to match the design screenshots and adds prism-react-renderer for in-snippet syntax coloring. Visual changes -------------- - Tab row matches the existing drawer pattern (see `MCPServerDetailsPanel.tsx`): plain text triggers (gap-6, active = `text-foreground`, inactive = `text-muted-foreground hover:text-foreground`), no pill background, no per-trigger shadow. Updated `ui/tabs.tsx` so the primitive itself enforces this — both this PR and #5323's Code|Definition row will inherit the right look. - Preview/Re-run button hoisted onto the tab row (right-aligned via a new `actions` slot on `PromptSnippetTabs`). - `Button` `size="sm"` refreshed to the spec: `p-2` (8px uniform), `gap-1.5` (6px), `rounded-sm` (4px). Height retained at `h-8` so row alignments in the ~31 existing `size="sm"` call sites don't shift. - `Button` `variant="outline"` no longer renders a default background or shadow — fully transparent until hover (`hover:bg-muted` light / `dark:hover:bg-input/50` dark). Affects all outline buttons app-wide. - Status row simplified per design: `✓ 200 OK · Render N ms` (icon stays emerald; "200 OK" goes plain `text-foreground`, "Render N ms" stays `text-muted-foreground`). - Dropped: the `Endpoint / Auth` footer, the "Rendered messages" heading, the empty-state paragraph. None of them are in the design and the layout reads cleaner without them. Syntax highlighting ------------------- - Adds `prism-react-renderer ^2.4.1` (production bundle: +85 KB / +57 KB gzipped). Pure tokenization, no runtime themes loaded from disk. - New `ui/code-block.tsx` primitive — always-dark `<pre><code>` with Prism's `vsDark` theme, optional top-right Copy button driven by the existing `copyToClipboard` helper. Languages: `bash`, `json`, `python`, `tsx`/`typescript`. - All four sub-tab snippets and the rendered MCP response use it; the response box now also has a Copy affordance. Structural changes ------------------ - Split the monolithic `PromptPreviewPanel` into: - `usePromptPreview(promptId, args)` — owns request, timing, error parsing, toast. - `PromptPreviewButton` — the trigger only; sits on the tab row. - `PromptPreviewResult` — status row + response box; rendered below. - `PromptCodeTab` orchestrates them in a single layout block. - `PromptPreviewPanel.tsx` removed (only `PromptCodeTab` consumed it). - Public seam unchanged: `<PromptCodeTab prompt={selected} />`. i18n ---- - Renamed `preview.renderTimeMs` → `preview.renderMs` so the message template is the full "Render {ms} ms" string (matches the design's bare label). - Removed now-unused keys: `code.endpoint`, `code.auth`, `code.auth.bearer`, `preview.renderTime`, `preview.pluginsPassed`, `preview.messagesHeading`, `preview.empty`. - Three locales updated (en-US / es-ES / pt-BR). Tests ----- - All 1674 client tests pass; type check clean; production build clean. - `PromptPreviewPanel.test.tsx` replaced by `usePromptPreview.test.tsx`, `PromptPreviewButton.test.tsx`, `PromptPreviewResult.test.tsx`. - `PromptSnippetTabs.test.tsx` and `PromptCodeTab.test.tsx` updated to read snippet contents via the rendered `<pre>` element's textContent (prism-react-renderer breaks the string across token spans, so `getByText` against the whole snippet no longer matches). Files ----- - `package.json`, `package-lock.json` — prism-react-renderer - `src/components/ui/button.tsx` — sm + outline variant refresh - `src/components/ui/tabs.tsx` — text-only tab row - `src/components/ui/code-block.tsx` (+ test) — new - `src/components/prompts/usePromptPreview.ts` (+ test) — new - `src/components/prompts/PromptPreviewButton.tsx` (+ test) — new - `src/components/prompts/PromptPreviewResult.tsx` (+ test) — new - `src/components/prompts/PromptCodeTab.tsx` (+ test) — reorganized - `src/components/prompts/PromptSnippetTabs.tsx` (+ test) — reorganized - `src/components/prompts/PromptPreviewPanel.tsx` (+ test) — removed - `src/components/prompts/index.ts` — barrel refresh - `src/i18n/locales/{en-US,es-ES,pt-BR}/prompts.json` — key churn Signed-off-by: a-effort <anna.effort@ibm.com>
- usePromptPreview: reset result/error when promptId changes so switching
prompts inside a shared drawer instance no longer shows the previous
prompt's rendered messages.
- CodeBlock: drop internal i18n, toast, and clipboard dependencies; expose
an onCopy callback so callers own the domain-specific success message.
Prompt snippet tabs now hold the copy handler and toast.
- Move copyToClipboard from components/gateways/utils.ts to lib/clipboard.ts
and repoint call sites; the util is not gateway-specific.
- Snippet builders: encode promptName with encodeURIComponent in the URL
path of curl/python/typescript so names containing spaces (allowed by
the backend name regex) do not break the copy-paste. Drop dead
`?? {}` fallbacks on the non-nullable args parameter.
- ESLint: allow `_`-prefixed unused locals via argsIgnorePattern so the
code-block destructure can rely on the naming convention instead of
`void _key;` no-op lines.
- prompts render API: document that the endpoint accepts either name or
id and why we pass id here.
- Snippet builder test parser: note the `\"` edge case in
extractBracedLiteral for anyone extending the escape matrix.
Signed-off-by: a-effort <anna.effort@ibm.com>
) Address the review's follow-up: the previous fix-up documented the name-vs-id inconsistency but did not resolve it. Both call sites now address the prompt by name, matching the identifier the Code-tab snippets display and what MCP-spec clients use on the wire. - usePromptPreview: rename `promptId` param to `promptName`; the reset effect and useCallback deps track the new identifier. - PromptCodeTab: pass `prompt.name` to the hook so the Preview call and the snippet URLs address the same string. - api/prompts.ts: rename `validatePromptId` to `validatePromptName`, widen the regex to the backend's `SecurityValidator.NAME_PATTERN` (space, dot, dash allowed), and URL-encode the identifier in the path. - JSDoc on `promptsApi.render` documents: the endpoint accepts either name or id (backend does the resolution); the ambiguity failure mode and how the hook surfaces it; alignment with MCP 2026-07-28 RC's `Mcp-Name` header direction; and a "server-scoped MCP transport" follow-up as a future option. - Tests: update error-message assertions, add coverage for backend-legal names with spaces and dots, rename the rerender test to track the new identifier. Signed-off-by: a-effort <anna.effort@ibm.com>
7881258 to
9f4d0aa
Compare
|
The PR can be merged when the conflicts are resolved, and all CI checks pass. |
Summary
<PromptCodeTab prompt={selected} />; single prop, no callbacks, no drawer knowledge.src/pages/Prompts.tsx) is temporary so the prototype can be exercised against the live gateway; remove it when [UI-REWRITE]: Add prompt details drawer #5323's drawer lands.What this does for users
Serves developers wiring a gateway prompt into agents, MCP clients, or scripts. Today they reverse-engineer the wire format from the Template and Arguments blocks. This branch ships:
POST /prompts/{id}. Runs the plugin pipeline, returns the templated MCPmessagesarray, and does not invoke an LLM. Status row reports HTTP code and client-measured render time. Re-run replays with whatever args are currently in the form.argumentsschema, with required/optional badges and inline descriptions. The same args object feeds both the snippets and the Preview request, so what the user sees in the snippet is what gets sent.The existing LLM-execution Test modal is unchanged. Preview complements it for the "what bytes do I send" question.
Technical notes
Public seam:
<PromptCodeTab prompt={NonNullable<PromptRead>} />. The #5323 engineer drops it into aTabsContentslot.Composition under the seam:
PromptArgsForm(controlled, full-record emission on every change; null-safe over the orval(PromptArgument | null)[]array shape)PromptSnippetTabswith anactionsslot for the trailing Preview buttonusePromptPreview(promptId, args)owns request lifecycle,performance.now()timing,ApiError.body.detailunwrap, sonner error toastPromptPreviewButtonandPromptPreviewResultconsume the hook independently so the trigger can live on the tab row while status and response render belowSnippet builders are pure functions of
{ promptName, args }exported individually (buildCurl,buildJsonRpc,buildPython,buildTypescript). 19-test escape-safety matrix covers double quotes, backslashes, newlines, dollar-sign literals (verified not shell-expanded inside single-quoted curl bodies), and apostrophes (bash'\''escape). URL and bearer token are referenced as\$MCPGATEWAY_URLand\$MCPGATEWAY_BEARER_TOKENliterals.API surface: new
src/api/prompts.tswithpromptsApi.render(id, args)going through the localapiclient rather than the orval-generated fetcher. Preserves cookie auth, CSRF header forwarding, and the 401-to-login redirect. ID validation regex mirrorsvalidateToolIdintools.ts. LocalRenderedPrompttype narrows the orvalunknownresponse.Shared primitives:
src/components/ui/tabs.tsxis a new radix Tabs wrapper, styled to match the plain-text tab row pattern used inMCPServerDetailsPanel.tsx(gap-6,text-foregroundactive,text-muted-foreground hover:text-foregroundinactive). Both this branch and [UI-REWRITE]: Add prompt details drawer #5323's Code|Definition row use it.src/components/ui/code-block.tsxwrapsprism-react-renderer(added dependency,vsDarktheme) forbash,json,python,tsx. Always-dark in both light and dark UI modes. Optional Copy affordance.Token-level styling changes (intentional global):
Buttonsize=\"sm\"updated top-2 gap-1.5 rounded-smto match the design spec. Affects ~31 existing call sites; sharper corners and slightly tighter padding. Height held ath-8so vertical row alignment in forms and drawers does not shift.Buttonvariant=\"outline\"no longer rendersbg-background,shadow-xs, ordark:bg-input/30in the idle state. Border-only until hover.aria-expandedfill retained for dropdown triggers.i18n under
prompts.details.code.*andprompts.details.preview.*across en-US, es-ES, pt-BR. Spanish and Portuguese translations included; native review pending before ship.Bundle impact:
prism-react-renderer ^2.4.1adds about 85 KB raw / 57 KB gzipped to the vendor chunk.Tests: 1674 client tests pass,
tsc -bclean, production build clean. New coverage includes 19 snippet-builder cases, hook lifecycle, button state transitions, result rendering for success and failure, and integration tests confirming arg-form keystrokes flow into the active snippet via prism's tokenized DOM.Production CSRF posture: verified end-to-end. Login at
/app/auth/login(routers/app.py:135) sets both auth and CSRF cookies on path/. The api client (client.ts:106) reads the cookie on every mutating request and forwards it asX-CSRF-Token. Same-origin guaranteed byvite.config.tsshipping the bundle under/static/app/on the gateway itself. 401 path triggers redirect-to-login. No additional code needed.Deliberately out of scope (carried as inline TODOs)
/previewsibling route): open question on the issue; isolated topromptsApi.renderso the swap is a one-file change later.usePermissionhook lands in the rewrite.vsDark; per-token color overrides could be added as a next step).Related