Feature: Tools transcript cards + HITL interactions
Status: Implemented (alpha)
Surfaces: panel() / render_transcript() / render_approvals() (primary); legacy CCv2 chat() (parity where practical)
Related: doc/prd.md FR-C3, FR-C6, FR-C7, FR-S3 (superseded), FR-S6; doc/roadmap.md
Test prompts: examples/testdata/prompts.json categories display_*
- No raw JSON expanders in default UX. Tool activity is shown as meaningful cards (query, path, command, options, results).
- Specialized interactions for SDK-routed / high-stakes tools (
AskUserQuestion, approvals, SQL, file/shell mutations, plan exit). - Debug escape hatch. CoCo debug mode may reveal raw tool payloads in a collapsed expander.
- Stable dispatch. Tool name matching is case- and separator-insensitive (
SQL,sql_execute,SqlExecute→ same family).
Non-goals (this revision): full Monaco diffs, custom MCP tool UIs beyond the generic meaningful card, headless programmatic UI.
(Copy-to-clipboard shipped separately via show_copy= / FR-S7.)
| Term | Meaning |
|---|---|
| Transcript tool card | UI for a transcript item with kind == "tool" |
| Interaction surface | UI while permission_manager has an active pending request (AWAITING_USER) |
| Debug mode | is_debug_mode() — env STREAMLIT_COCO_DEBUG / COCO_DEBUG, or st.session_state["coco_debug"] |
| Family | Logical tool group used for rendering (sql, read, write, edit, bash, glob, grep, ask_user, exit_plan, generic) |
normalize(name) = lowercase(name) with non-alphanumeric characters removed
Examples: AskUserQuestion → askuserquestion, sql_execute → sqlexecute, ExitPlanMode → exitplanmode.
| Context | Allowed? |
|---|---|
| Transcript tool card (primary surface) | Yes — compact st.expander / <details>, collapsed by default |
| Tool payload / results inside the card | Shown only when the card expander is opened (still meaningful content, not raw JSON) |
| Debug mode | Optional nested collapsed expander labeled Raw tool payload |
| Structured output / unrelated UI | Out of scope (may still use expanders) |
- Tool cards use a collapsed expander as the primary surface (dense transcript / Copilot rail).
- Expander label:
**{FamilyLabel}** · {StatusLabel}plus optional short meta (path, row count, pattern, match count). - Auto-expand only when status is
errorso failures are not missed. - Status labels:
Running|Completed|Failed(map fromrunning/completed/error). - While a tool is
running, show a short progress caption inside the expander (e.g. “Searching content…”, “Reading…”). - When the tool is
completed/error, or the turnresultarrives, never keep the progress caption — show matches / result body / error instead (header status must leaveRunning).
When debug mode is on, after the meaningful card content, optionally:
▾ Raw tool payload (collapsed)
{ input, result }
show_tool_details=False suppresses large result bodies where specified per family; debug still may show raw payload.
- Active pending requests are rendered by
render_approvals()(or AskUser / plan variants). - Transcript should not duplicate the interactive form for the active request (
hide_active_approvalfor approval items; AskUser running card stays a short “Waiting…” notice).
While any pending permission request is active, fragment auto-polling pauses so buttons / radios remain clickable (existing panel() behavior).
Names: AskUserQuestion, ask_user_question, …
Transcript card
| Status | Display |
|---|---|
running |
Info: “Waiting for your answer — {headers}” |
completed |
Caption: “Answered — {headers}” (no JSON) |
error |
Error: “Question cancelled or failed — {headers}” |
Interaction (always; never auto-allow / never “Always allow”)
- Info banner: CoCo needs input.
- Per question: header + question text.
- Options → radio (single) or multiselect (multi).
- Always append Other… unless an Other/free-form option already exists.
- Free-form / Something else / Other choices are always shown last (even if the agent listed them mid-list).
- Selecting Other… reveals a required text field.
- Actions: Submit answers (allow with
updated_input={questions, answers}) · Cancel (deny, rightmost). - Submit disabled until every question has a valid answer.
Permission routing: Always create_request in can_use_tool before any allowlist check.
Names: SQL, sql_execute, SqlExecute, sql_query, …
Transcript card
- Header:
SQL · {status}+ row count when completed. - Body:
st.code(sql, language="sql")fromquery/command/sql/statement/text(nestedinput/arguments/paramsallowed). - Completed: parse result into rows →
st.dataframe; else plain text (truncate > 4k chars); empty → “Query returned no rows.” - Failed:
st.errorwith message. - Running: “Executing query…”
Interaction (when approval required)
- Warning: wants to run SQL.
- Show SQL code block.
- Approve once / Always allow / Deny (SQL preview shown).
Names: Read
Transcript card
- Header:
Read · {status}·`{path}` - Running: “Reading file…”
- Completed: show content as
st.codeif looks like code/text (truncate long), or caption “Read N chars” - Failed: error message from result
Path keys: path, file_path, filePath, filename
Interaction: show path (and optional preview of intent); standard approval buttons.
Names: Write
Transcript card
- Header:
Write · {status}· path - Body: content preview in
st.code(language guessed from extension; defaulttext), truncated - Running: “Writing file…”
- Failed: error
Content keys: content, new_str, newString, text
Interaction: path + content preview + Approve once / Always allow / Deny.
Names: Edit
Transcript card
- Header:
Edit · {status}· path - Body: show Before / After (or old_string / new_string) as two code blocks — not a raw expander
- Running: “Applying edit…”
- Failed: error
Keys: path as above; old_string/oldString, new_string/newString
Interaction: path + before/after or unified diff preview + approval buttons.
Names: Bash
Transcript card
- Header:
Bash · {status} - Body:
st.code(command, language="bash") - Completed: stdout/stderr / result text in
st.text(truncated) - Running: “Running command…”
- Failed: error
Command keys: command, cmd
Interaction: command code block + approval buttons.
Names: Glob
Transcript card
- Header:
Glob · {status}· pattern - Completed: compact caption (
N files); path list only in debug mode - Running: “Searching files…”
Keys: pattern, glob_pattern, glob
Interaction: show pattern; approval if required.
Names: Grep
Transcript card
- Header:
Grep · {status}· pattern · optional path - Completed: compact caption (
N matches) — no full match dump in the card (preview only in debug mode) - Running: “Searching content…”
Keys: pattern, regex; path keys as Read; optional path/glob
Interaction: pattern (+ path) + approval if required.
Names: ExitPlanMode, exit_plan_mode, …
Transcript card
- Header:
Plan · {status} - Body: plan markdown/text in bordered card
Interaction (always route through pending when can_use_tool is installed)
- Title: Approve plan to leave plan mode?
- Show
plan(and optionalquestion) as markdown/text - Actions: Approve plan (allow; optional
updated_input.message) · Reject (deny, optional reason text, rightmost) - No “Always allow”
Permission routing: Always create_request (like AskUserQuestion). Never add to always_allowed_tools.
Transcript card
- Header:
{ToolName} · {status} - Body: human summary of up to 6 scalar input fields (key → short value); skip huge blobs
- Result: short text/caption if scalar/string; if large structured data, show “Result received (N keys)” and only dump JSON in debug
- Never default to an open JSON expander
Interaction: warning with tool name + meaningful input summary + standard approval buttons; raw JSON only in debug.
For mutable / gated tools (not AskUser / ExitPlan):
| Button | Behavior |
|---|---|
| Approve once | PermissionResultAllow (optional updated_input) |
| Always allow {Tool} | Allow + session memory for that tool name (not AskUser / ExitPlan) |
| Deny | PermissionResultDeny (rightmost) |
Left → right order in the UI: Approve once · Always allow · Deny.
SQL / Write / Edit / Bash / generic follow this pattern when pending.
| API | Behavior |
|---|---|
panel(..., show_tool_details=True) |
Show result bodies for specialized cards (tables, content previews) |
show_tool_details=False |
Headers + primary input (SQL/command/path) only; skip large results |
render_approvals(..., show_tool_input=None) |
None → follow debug mode for raw payload expander |
is_debug_mode() |
Gates raw payload expanders |
Legacy component should:
- Detect AskUser / SQL families (already partially done)
- Prefer compact cards over
<details>JSON for Read/Write/Edit/Bash/Glob/Grep/generic - Full interactive AskUser remains native
panel()path (CCv2 may show “answer in host UI” notice if pending)
- Default demo path never opens a tool JSON expander for SQL, AskUser, Read, Write, Edit, Bash, Glob, Grep.
- AskUser always shows radio/Other/Submit; never Approve once for that tool.
- SQL shows code + table/text result.
- Write/Edit/Bash approval shows path/content/command, not only the tool name.
- Debug checkbox reveals raw payloads without changing the primary card.
- Unit tests cover name normalization, SQL parse, family dispatch, AskUser/ExitPlan forced pending.
- Manual checklist under
doc/features/tools-display/test-checklist.mdpasses on a live session.
| Module | Responsibility |
|---|---|
streamlit_coco/tool_names.py |
normalize + family detection |
streamlit_coco/ask_user.py |
AskUser helpers (existing) |
streamlit_coco/sql_tool.py |
SQL helpers (existing) |
streamlit_coco/tool_cards.py |
Transcript card renderers per family |
streamlit_coco/ui.py |
Interaction surfaces (approvals, AskUser, ExitPlan) |
streamlit_coco/permissions.py |
Forced pending for AskUser + ExitPlan |
streamlit_coco/display.py |
Dispatch transcript items → tool_cards |
streamlit_coco/frontend/main.js |
CCv2 card parity |
| Date | Change |
|---|---|
| 2026-07-23 | Initial full spec (no default tool expanders; family catalog + HITL) |
| 2026-07-31 | Compact transcript: tool cards are collapsed expanders; auto-open on error |
| 2026-07-23 | Implemented in library; display_* prompt pack + Plan mode toggle in chat demo |