Skip to content

fix(chat): make agent-prompted modals actionable#76

Merged
xaspx merged 1 commit into
xaspx:mainfrom
tomekpanek:fix/chat-modal-buttons
Jun 5, 2026
Merged

fix(chat): make agent-prompted modals actionable#76
xaspx merged 1 commit into
xaspx:mainfrom
tomekpanek:fix/chat-modal-buttons

Conversation

@tomekpanek

Copy link
Copy Markdown
Contributor

Symptom

When the agent asks for approval (or any of the four modal prompts — Approval / Clarification / Sudo / Secret), HCI shows a modal with the title text but no buttons. Clicking outside dismisses the modal but the chat locks: Send button is red STOP state, Stop button does nothing, typing works but nothing posts. Only a refresh recovers it, and the original tool call never completes.

After buttons work, picking a choice still produces ⚠️ session not found (twice) and the agent prompts the user again as if nothing was selected.

Root causes

Two layered bugs that compound:

1. Modal API mismatch (src/js/chat/gateway.js)

The four agent-prompted modal helpers were written against an older showModal contract:

showModal({ body, confirmText, cancelText, onConfirm, onCancel })

The current src/js/components/modal.js takes:

showModal({ message, inputs = [], buttons = [] }) -> Promise<result>

Only title is shared. body/confirmText/cancelText/onConfirm/onCancel are silently dropped, so the modal renders a header with an empty modal-actions div (= no buttons). Click-outside resolves the Promise to null but the old onCancel callback never fires, so the agent stays blocked on a response that the UI cannot send. The chat lock never clears because chat.done/chat.error doesn't arrive until the agent gets a response.

2. Wrong param shape on the bridge → gateway hop (lib/tui-gateway-bridge.js)

  • approval.respond was sent without session_id. The gateway's handler calls _sess(params, rid) which looks up params[\"session_id\"] in _sessions; missing key returns the 4001 "session not found" error, surfaced as the amber ⚠️ in chat.
  • Bridge sent { approve: true|false, command }; gateway reads { choice: \"approve\"|\"deny\", command }.
  • clarify.respond sent { request_id, text, choice }; gateway's _respond reads params[\"answer\"], so until now the agent always saw an empty answer and followed up with "looks like you haven't picked yet."

(sudo.respond and secret.respond already used the keys the gateway expects.)

Fix

  1. Port all four modals (showApprovalModal, showClarifyModal, showSudoModal, showSecretModal) to the modern { message, inputs, buttons } shape, await the Promise, and translate a null result (click-outside / Cancel) into a negative response so the agent unblocks naturally.
  2. Multiple-choice clarify now uses buttons: choices.map(c => ({ text: c, value: c })) instead of injecting <button> markup into the body.
  3. Bridge tracks _internalSid (updated on every prompt.submit in both chatStart and chatSend).
  4. respondApproval now sends { session_id: _internalSid, choice: approve ? 'approve' : 'deny', command }.
  5. respondClarify sends { request_id, answer: choice ?? text ?? '' }.

Test plan

  • node --test test/chat-approval-modal.test.js — 2/2 pass (new regression test, fails on `main`)
  • node --test test/*.test.js — no new regressions
  • npm run build — clean
  • Manual: trigger a tool that needs approval. Modal shows ✅ Approve / ❌ Deny. Click Approve → tool runs, chat continues.
  • Manual: same modal, click outside instead. Tool fails cleanly, chat unlocks (no permanent STOP state).
  • Manual: trigger a clarify with choices. Pick one. Agent sees the choice (no "looks like you haven't picked yet").

The Approval / Clarification / Sudo / Secret modals rendered a header
with no buttons, then click-outside closed them silently and the chat
locked: STOP button red, Send dead, recovered only by refresh.

Two layered bugs:

1. src/js/chat/gateway.js called showModal with {body, confirmText,
   cancelText, onConfirm, onCancel} — an older shape. The current
   modal.js takes {message, inputs, buttons} and returns a Promise.
   Only ``title`` survived the mismatch, so buttons[] stayed empty and
   neither callback fired on close. Ported the four helpers to await
   the Promise; null result (click-outside / Cancel) sends a negative
   response so the agent unblocks via chat.done/chat.error.

2. lib/tui-gateway-bridge.js sent ``approval.respond`` without
   session_id; the gateway's _sess() rejected it with 4001 "session
   not found" (surfaced as ⚠️ in chat). It also sent {approve, command}
   where the gateway reads {choice: "approve"|"deny"}, and
   ``clarify.respond`` sent {text, choice} where the gateway reads
   {answer}. Bridge now tracks _internalSid (set on every prompt.submit)
   and translates each respond into the keys the gateway expects.

Regression-covered by test/chat-approval-modal.test.js.
@xaspx xaspx merged commit bb0cbde into xaspx:main Jun 5, 2026
4 checks passed
@xaspx

xaspx commented Jun 5, 2026

Copy link
Copy Markdown
Owner

✅ Tested & Merged

Deploy ke staging:

Test results:

  • ✅ Approval modal → ❌ Deny / ✅ Approve buttons muncul, click bekerja
  • ✅ Clarify modal → multiple-choice buttons atau text input + Submit
  • ✅ Sudo/Secret modals → input + Submit
  • ✅ Click-outside → modal dismiss, chat gak lock
  • ✅ No regressions on normal chat

Merge: Merged to main at bb0cbde. Dev branch synced.

Thanks @tomekpanek! 🚀

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