Skip to content

feat(ui-rewrite): POC Code tab + Preview for prompt details (#5448)#5452

Open
a-effort wants to merge 4 commits into
epic/ui-rewritefrom
5448-preview-prompt
Open

feat(ui-rewrite): POC Code tab + Preview for prompt details (#5448)#5452
a-effort wants to merge 4 commits into
epic/ui-rewritefrom
5448-preview-prompt

Conversation

@a-effort

@a-effort a-effort commented Jun 30, 2026

Copy link
Copy Markdown
Collaborator

Summary

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:

  • Four-tab snippet view (curl, JSON-RPC, Python, TypeScript), each pre-filled with the prompt name and current argument values, each with a copy button. Paste-and-go for the most common call paths.
  • Render-only Preview action that hits POST /prompts/{id}. Runs the plugin pipeline, returns the templated MCP messages array, 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.
  • Controlled args form driven by the prompt's declared arguments schema, 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 a TabsContent slot.

Composition under the seam:

  • PromptArgsForm (controlled, full-record emission on every change; null-safe over the orval (PromptArgument | null)[] array shape)
  • PromptSnippetTabs with an actions slot for the trailing Preview button
  • usePromptPreview(promptId, args) owns request lifecycle, performance.now() timing, ApiError.body.detail unwrap, sonner error toast
  • PromptPreviewButton and PromptPreviewResult consume the hook independently so the trigger can live on the tab row while status and response render below

Snippet 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_URL and \$MCPGATEWAY_BEARER_TOKEN literals.

API surface: new src/api/prompts.ts with promptsApi.render(id, args) going through the local api client rather than the orval-generated fetcher. Preserves cookie auth, CSRF header forwarding, and the 401-to-login redirect. ID validation regex mirrors validateToolId in tools.ts. Local RenderedPrompt type narrows the orval unknown response.

Shared primitives:

  • src/components/ui/tabs.tsx is a new radix Tabs wrapper, styled to match the plain-text tab row pattern used in MCPServerDetailsPanel.tsx (gap-6, text-foreground active, text-muted-foreground hover:text-foreground inactive). Both this branch and [UI-REWRITE]: Add prompt details drawer #5323's Code|Definition row use it.
  • src/components/ui/code-block.tsx wraps prism-react-renderer (added dependency, vsDark theme) for bash, json, python, tsx. Always-dark in both light and dark UI modes. Optional Copy affordance.

Token-level styling changes (intentional global):

  • Button size=\"sm\" updated to p-2 gap-1.5 rounded-sm to match the design spec. Affects ~31 existing call sites; sharper corners and slightly tighter padding. Height held at h-8 so vertical row alignment in forms and drawers does not shift.
  • Button variant=\"outline\" no longer renders bg-background, shadow-xs, or dark:bg-input/30 in the idle state. Border-only until hover. aria-expanded fill retained for dropdown triggers.

i18n under prompts.details.code.* and prompts.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.1 adds about 85 KB raw / 57 KB gzipped to the vendor chunk.

Tests: 1674 client tests pass, tsc -b clean, 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 as X-CSRF-Token. Same-origin guaranteed by vite.config.ts shipping 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)

  • Plugin-pass count in the status row: pending a backend response field. Layout scaffold in place, wiring is one line.
  • Preview invocation tagging (header vs /preview sibling route): open question on the issue; isolated to promptsApi.render so the swap is a one-file change later.
  • RBAC gate on the Preview button: deferred until a usePermission hook lands in the rewrite.
  • Syntax-highlighting fidelity polish (currently vsDark; per-token color overrides could be added as a next step).

Related

@a-effort a-effort added the ui-rewrite Tasks for the isolated ui rewrite feature branch label Jun 30, 2026
@a-effort

Copy link
Copy Markdown
Collaborator Author

This is a POC for prompt details drawer "preview" functionality (temp location: won't be an in page option in Prompts)

preview.mp4

@marekdano marekdano left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 shown

If 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" — a ui/ component shouldn't depend on a gateways/ utility.
  • The copy toast hardcodes "prompts.details.code.copySuccess" — if CodeBlock is 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 buildersSnippetInput.args is typed Record<string, string> (non-nullable). The ?? {} fallbacks in buildCurl, buildPython, and buildTypescript can't fire. Remove them.

Comment thread client/src/components/ui/code-block.tsx Outdated
Comment thread client/src/components/prompts/snippets/builders.test.ts
@a-effort

a-effort commented Jul 1, 2026

Copy link
Copy Markdown
Collaborator Author

Thanks for the review @marekdano! Feedback addressed in two commits:

  • 8e5961527 fix(ui-rewrite): address review on prompt Code tab
  • 75bbcf594 fix(ui-rewrite): unify prompt Preview and snippets on prompt.name

Stale preview state on prompt change: fixed in 8e59615. Reset moved into usePromptPreview via a useEffect keyed on the identifier, colocating it with the state that owns it. key={prompt.id} remounting was avoided since it would drop memoization in PromptArgsForm/PromptSnippetTabs during mid-typing edits. Regression test added.

CodeBlock coupling: fixed in 8e59615. copyToClipboard moved to lib/clipboard.ts (six call sites repointed). CodeBlock no longer imports sonner/react-intl or references prompt-domain i18n; it now exposes onCopy?: (code: string) => void, and PromptSnippetTabs owns the toast.

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 SecurityValidator.NAME_PATTERN) is wider than the old validatePromptId. encodeURIComponent(promptName) added in buildCurl/buildPython/buildTypescript; JSON-RPC already escapes correctly as a field value. Tests added in builders.test.ts.

Snippets use prompt.name, Preview uses prompt.id: resolved in 75bbcf5 by unifying on prompt.name. Good catch: the endpoint accepts either (_find_prompt_by_name_or_id), so it was never a 404 risk, but the two call sites should agree. Went with name because:

  1. MCP clients call prompts/get(name). There's no prompt ID at the MCP layer, so name-based snippets teach the correct pattern.
  2. The [MCP RC blog post](https://blog.modelcontextprotocol.io/posts/2026-07-28-release-candidate/) adds an Mcp-Name header as a wire field; staying name-shaped keeps today's URL and tomorrow's header the same reference.
  3. The ambiguity path (422 when a name collides across scopes) surfaces through the existing toast, same as a copy-paste integrator would hit.

Also in 75bbcf5: usePromptPreview(promptName, args) param rename, validatePromptId renamed to validatePromptName with the widened regex, and JSDoc on promptsApi.render covering the acceptance semantics and ambiguity case. Routing Preview/snippets through the server-scoped MCP transport is a larger change (needs server context on the Prompts page); will track separately.

args ?? {} dead code: removed across all four builders (8e59615). SnippetInput.args is non-nullable, so the fallback was unreachable.

void _lineKey;: removed (8e59615); added _-prefix ignore pattern to client/eslint.config.js so the convention is enforced without the workaround.

extractBracedLiteral \" edge case: left as-is, with an inline comment noting the limitation and the two-state-lexer upgrade path for anyone extending TRICKY_ARGS.

@marekdano marekdano left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All findings from the previous review are addressed correctly!

LGTM 🚀 after fixing DCO check

a-effort added 4 commits July 2, 2026 11:16
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>
@gcgoncalves gcgoncalves force-pushed the 5448-preview-prompt branch from 7881258 to 9f4d0aa Compare July 2, 2026 10:18
@a-effort a-effort marked this pull request as ready for review July 2, 2026 15:36
@marekdano

Copy link
Copy Markdown
Collaborator

The PR can be merged when the conflicts are resolved, and all CI checks pass.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ui-rewrite Tasks for the isolated ui rewrite feature branch

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants