Skip to content

[Plugins] Readable, viewport-safe HITL approval prompt (compact args + spill file)#6

Open
swarup-padhi-glean wants to merge 10 commits into
mainfrom
pretty-print-elicitation-args
Open

[Plugins] Readable, viewport-safe HITL approval prompt (compact args + spill file)#6
swarup-padhi-glean wants to merge 10 commits into
mainfrom
pretty-print-elicitation-args

Conversation

@swarup-padhi-glean

@swarup-padhi-glean swarup-padhi-glean commented Jun 15, 2026

Copy link
Copy Markdown
Contributor

Problem

When ENABLE_HITL is on, run_tool shows a requires_approval action's arguments in an elicitInput prompt. Two issues with the original rendering:

  1. Unreadable args — the whole object was rendered with JSON.stringify(args, null, 2), which escapes newlines inside string values, so a Markdown body/table collapsed into one line of literal \n.
  2. Viewport overflow — once arguments were expanded for readability, a large argument (long body, table, etc.) could make the prompt taller than Claude Code's viewport and push the Accept/Decline buttons off-screen, so the user couldn't respond.

Fix

The inline approval message is now compact and line-capped so the buttons always stay visible, with full detail spilled to a file when needed.

Inline message (Claude Code / Codex):

  • One line per top-level argument; UPPERCASE keys, indented under Arguments: — distinguishes structure from values in plain text (the elicitation surface can't style fonts).
  • Each value on a single line: multi-line strings collapsed; values past ~120 chars cut and suffixed (truncated); nested objects shown as compact JSON.
  • Capped to a small argument-section budget (≈8 lines — up to 7 args + 1 file-path line) so Accept/Decline stay in view.

Spill file (only when something is truncated or omitted):

  • Full arguments are written to a single fixed Markdown file in the plugin's data dir (PLUGIN_DATA_DIR), overwritten on each approval; the prompt shows the path.
  • Rendered for readability when opened: string values verbatim (so Markdown — tables, headings — renders), nested values as pretty JSON.
  • The write is wrapped in try/catch — if a write is blocked (e.g. a restrictive sandbox), it degrades to a short note instead of breaking the approval gate.

Cursor keeps its one-line prompt (it shows tool + args in its own UI).

Example inline prompt (fabricated values):

Action: send_message
Arguments:
  CHANNEL: C123
  BODY: # Notes | Feature | Status | |---|---| | Search | done | ... (truncated)
  Full arguments: <PLUGIN_DATA_DIR>/glean-approval-args.md

Scope & non-goals

  • Plain text — the elicitation surface doesn't render Markdown; the inline ceiling is readable text, while the spill file is where rich content renders.
  • Affects only the Claude Code / Codex message; Cursor unchanged.
  • Complements file_args, which keeps huge inputs out of arguments entirely.

Code

  • Argument-formatting helpers extracted into src/tools/approval-args.ts (buildCompactArgs, formatArgumentsForFile, writeApprovalArgsFile); run-tool.ts builds the message and gates the file write.

Tests

  • Unit tests for the compact formatter (scalars inline, multi-line collapse, over-width truncation with (truncated), nested compaction, line-budget cap) and the file formatter; the HITL handler test asserts the prompt stays within the line budget and the spill file holds the full content. Build + typecheck + tests all green.

Verification

  • Verified in Claude Code, including /sandbox mode: the full-args file writes successfully to the plugin's managed data dir (the same location the plugin already uses for its own state), so the sandbox permits it; the try/catch covers the edge case where a write is blocked.

The approval prompt ran the whole arguments object through
JSON.stringify(_, null, 2), which escapes every newline inside a string
value. A Markdown body or table therefore collapsed into one unreadable
line of literal "\n". Render each top-level arg by shape instead:
scalars inline, long/multi-line strings as a de-escaped block with real
newlines, nested values as compact JSON — joined by a blank line so a
multi-line value cannot blur into the next key. Values past a 2 KB cap
are truncated with a nudge to pass large content via file_args.

Stays plain text (no Markdown rendering on the elicitation surface) and
only affects the Claude Code / Codex message; Cursor keeps its one-liner.

formatArguments is now exported and unit-tested.
@eshwar-sundar-glean eshwar-sundar-glean deleted the pretty-print-elicitation-args branch June 17, 2026 07:32
@swarup-padhi-glean swarup-padhi-glean restored the pretty-print-elicitation-args branch June 17, 2026 10:44
Adopt the glean-vnext rename from #7 in the three plugin manifests and
bump version to 0.2.22 (above #5's 0.2.21 / main's 0.2.20). The
formatArguments pretty-print change and dist are unaffected by the rename.
#5 landed on main first (readOnlyHint, v0.2.21). Integrate it: keep both
formatArguments (#6) and runToolAnnotations (#5) — they live in separate
parts of run-tool.ts and tests. Resolve the 3 manifests to glean-vnext
v0.2.22, keep both test suites, rebuild dist.
swarup-padhi-glean added a commit that referenced this pull request Jun 19, 2026
Sync onto current main (0.2.24): accept removal of packages/codex (codex
dist mirror dropped on main), keep both #6 formatArguments and main's
updated Cursor approval message in buildApprovalMessage, resolve the 3
plugin manifests to glean-vnext v0.2.26, rebuild dist.
@swarup-padhi-glean swarup-padhi-glean changed the title Pretty-print elicitation arguments as readable plain text [Plugins] Pretty-print elicitation arguments as readable plain text Jun 19, 2026
Sync onto current main (0.2.24): accept removal of packages/codex (codex
dist mirror dropped on main), keep both #6 formatArguments and main's
updated Cursor approval message in buildApprovalMessage, resolve the 3
plugin manifests to glean-vnext v0.2.26, rebuild dist.
@swarup-padhi-glean swarup-padhi-glean force-pushed the pretty-print-elicitation-args branch from 5e62e11 to 45d61e2 Compare June 19, 2026 06:21
The block pretty-print could grow tall enough that Claude Code pushed the
Accept/Decline buttons out of view. Now the inline prompt shows at most a
few one-line-per-argument entries (multi-line/long values collapsed and
truncated, never expanded). Whenever anything is truncated or an argument
is omitted, the full arguments are written to a Markdown file under the
OS temp dir (rendered nicely: string values verbatim so Markdown like
tables renders, nested values as JSON), and the prompt shows the path.

Keeps the elicitation block within ~7 lines so the approval buttons stay
visible. Cursor's one-line prompt is unchanged.
… styling

Feedback from live testing:
- Raise per-argument inline width 80 -> 120 chars (ellipsis was appearing
  too early).
- Allow up to 8 argument-section lines (7 args + 1 file-path line when
  spilling) instead of 4.
- Write the spill file to a single fixed path under PLUGIN_DATA_DIR
  (CLAUDE_PLUGIN_DATA, via start.sh), overwritten each approval, instead
  of a unique file under the OS temp dir.
- Distinguish structure from values in plain text: UPPERCASE keys and
  indent the argument lines under the Arguments: label.
Sync onto main after #13 (promote remote tools + all-manifest version
check). Resolve the 3 manifests to 0.2.26 (above main's 0.2.25); rebuild
dist so it carries both the promoted tools and the approval formatting.
Comment thread .claude/settings.json Outdated
"enabledPlugins": {
"glean-vnext@glean-plugins-vnext": true
}
"enabledPlugins": {}

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.

Why? We should probably add extraKnownMarketplaces if you are seeing an issue with this

Comment thread src/tools/run-tool.ts Outdated
// Per-argument inline width before truncating with an ellipsis.
const maxApprovalArgChars = 120;

function safeJson(value: unknown): string {

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.

#suggest
add a utils file and move all the utils there

Comment thread src/tools/run-tool.ts Outdated
}
}

function isEmptyArgs(args: unknown): boolean {

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.

same as above, utils file

Comment thread src/tools/run-tool.ts Outdated
} else if (value !== null && typeof value === "object") {
const json = safeJson(value);
if (json.length > maxApprovalArgChars) {
rendered = `${json.slice(0, maxApprovalArgChars)}…`;

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.

maybe suffix with (truncated)?

Comment thread src/tools/run-tool.ts Outdated
...lines.map((line) => ` ${line}`),
];
if (needsFile) {
const filePath = await writeApprovalArgsFile(toolName, args);

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.

#blocking
Do test this out.
We should try to run this in /sandbox mode in claude, we are writing files outside project dir

…ings revert

- Move the approval-argument formatting helpers into a dedicated
  src/tools/approval-args.ts (Eshwar: extract utils).
- Suffix truncated inline values with (truncated) (Eshwar).
- Wrap the spill-file write in try/catch so a blocked write (e.g. Claude
  /sandbox restricting writes outside the project dir) degrades to a note
  instead of breaking the approval gate (Eshwar #blocking — also to be
  verified live in /sandbox).
- Revert the stray .claude/settings.json enabledPlugins emptying back to
  main (Eshwar).
…itation-args

Resolve the rebuilt-dist conflict by rebuilding from merged source (keeps
both #14's loopback auth and the approval formatting); bump manifests to
0.2.27 (above main's 0.2.26).
@swarup-padhi-glean

Copy link
Copy Markdown
Contributor Author

Tested in claude code CLI:
Screenshot 2026-06-20 at 10 29 25 AM

Tested in claude code CLI sandbox
Screenshot 2026-06-20 at 10 34 54 AM

Arguments:
Screenshot 2026-06-20 at 10 36 30 AM

cc: @eshwar-sundar-glean

@swarup-padhi-glean swarup-padhi-glean changed the title [Plugins] Pretty-print elicitation arguments as readable plain text [Plugins] Readable, viewport-safe HITL approval prompt (compact args + spill file) Jun 20, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants