Skip to content

Commit cbf1f4a

Browse files
lordlinusCopilot
andcommitted
Add the missing chat widget + CSS; add 3 gaps found by a second E2E test
Ran a second independent end-to-end build test (fresh Copilot CLI session, different domain: a "PTO approver" instead of the first test's "expense approver") to verify the skill's snippets/simplification/bug-fixes actually help. Result: 12/12 smoke checks, all 7 browser E2E scenarios passed with screenshots, and verify.sh all green - and per the build's own report, the bridge snippet worked "completely unmodified except for the AGENT_NAME constant", azd ai agent init worked exactly as documented, and verify.sh/smoke.py/browser_e2e.js needed only mechanical domain renames. Total wall clock was roughly half the first test run's ~1 hour. That run also surfaced one real, valuable gap: the skill's frontend snippets only shipped providers.tsx (the <CopilotKit> wrapper + hook registrations) but never the actual chat WIDGET every consumer needs. The builder had to discover CopilotChat by reading the installed package's .d.ts files. Added: - frontend/app/page.tsx - the CopilotChat widget, generalized from this run's real, working, screenshot-verified code. - frontend/app/layout.tsx - minimal root layout wiring globals.css + Providers. - frontend/app/globals.css - plain CSS (no Tailwind dependency) for the .hitl-card/.tool-card class names, so the approval card/tool cards render legibly instead of just being present in the DOM unstyled. Verified these three files by swapping them into the actual test project and confirming `npm run build` succeeds and the page renders correctly in a headless browser (screenshot taken, matches expected UI). Also added 3 troubleshooting.md entries and 1 HITL-section entry for gaps this run's own REVIEW_NOTES.md flagged: in-memory hosted-agent state is process-lifetime (restart between verification passes or approvals leak across runs), a stale process can leave the hosted-agent port bound (Address already in use on the next `azd ai agent run`), an occasional transient DeploymentNotFound 404 on the very first request after startup (retry/restart resolves it), and keeping a renamed gated-tool parameter name in sync between the Python tool and the frontend's parsed-args cast. Verified locally with `npx @microsoft/vally-cli lint`: 3/3 checks pass, 21/21 reference files reachable from SKILL.md. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 17ca853 commit cbf1f4a

5 files changed

Lines changed: 125 additions & 0 deletions

File tree

skills/foundry-hosted-agent-copilotkit/references/snippets/README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,9 @@ adaptation — just don't start there.
3232
| [`backend/bridge_app.py`](backend/bridge_app.py) | `backend/bridge_app.py` | The ENTIRE bridge in one file: FastAPI AG-UI endpoint, the streaming Responses HTTP client (DIRECT local / platform deployed), the Responses→AG-UI translation, HITL forwarding, and an SSE keep-alive. |
3333
| [`backend/requirements.txt`](backend/requirements.txt) | `backend/requirements.txt` | Bridge-only deps (no agent-framework/foundry packages — the bridge runs no model). |
3434
| [CopilotKit catch-all route (`route.ts`)](frontend/app/api/copilotkit/[[...slug]]/route.ts) | `frontend/app/api/copilotkit/[[...slug]]/route.ts` | CopilotKit runtime handler pointed at the bridge. |
35+
| [`frontend/app/layout.tsx`](frontend/app/layout.tsx) | `frontend/app/layout.tsx` | Root layout: imports `globals.css`, wraps children in `Providers`. |
36+
| [`frontend/app/page.tsx`](frontend/app/page.tsx) | `frontend/app/page.tsx` | **The actual chat widget** (`CopilotChat`) — the one piece of UI every consumer needs; easy to miss since `providers.tsx` only wraps it. |
37+
| [`frontend/app/globals.css`](frontend/app/globals.css) | `frontend/app/globals.css` | Plain CSS for the `.hitl-card`/`.tool-card` class names `ApprovalHitl.tsx`/`ToolCards.tsx` use — without this they're functionally correct but visually unstyled. |
3538
| [`frontend/app/providers.tsx`](frontend/app/providers.tsx) | `frontend/app/providers.tsx` | `<CopilotKit>` provider + HITL/tool-card component registration. |
3639
| [`frontend/components/ApprovalHitl.tsx`](frontend/components/ApprovalHitl.tsx) | `frontend/components/` | `useHumanInTheLoop` example for the gated tool. |
3740
| [`frontend/components/ToolCards.tsx`](frontend/components/ToolCards.tsx) | `frontend/components/` | `useRenderTool` examples for both tools. |
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
/* Minimal, framework-agnostic CSS for the .hitl-card / .tool-card class
2+
names used by ApprovalHitl.tsx and ToolCards.tsx. No Tailwind or other
3+
build-time CSS dependency required — plain CSS only. Import this from
4+
your root layout (e.g. `import "./globals.css"` in app/layout.tsx). */
5+
6+
body {
7+
background: #ffffff;
8+
color: #171717;
9+
font-family: Arial, Helvetica, sans-serif;
10+
margin: 0;
11+
}
12+
13+
.hitl-card {
14+
border: 2px solid #d97706;
15+
background: #fffbeb;
16+
border-radius: 8px;
17+
padding: 12px 16px;
18+
margin: 8px 0;
19+
}
20+
21+
.hitl-card--done {
22+
border-color: #16a34a;
23+
background: #f0fdf4;
24+
}
25+
26+
.hitl-card__title {
27+
font-weight: 700;
28+
color: #92400e;
29+
margin-bottom: 6px;
30+
}
31+
32+
.hitl-card__actions {
33+
display: flex;
34+
gap: 8px;
35+
margin-top: 10px;
36+
}
37+
38+
.hitl-card__actions button {
39+
padding: 6px 14px;
40+
border-radius: 6px;
41+
border: 1px solid #999;
42+
cursor: pointer;
43+
font-weight: 600;
44+
}
45+
46+
.hitl-card__actions button[data-testid="hitl-approve-button"] {
47+
background: #16a34a;
48+
color: white;
49+
border-color: #16a34a;
50+
}
51+
52+
.hitl-card__actions button[data-testid="hitl-reject-button"] {
53+
background: #dc2626;
54+
color: white;
55+
border-color: #dc2626;
56+
}
57+
58+
.tool-card {
59+
border: 1px solid #ccc;
60+
background: #f8fafc;
61+
border-radius: 8px;
62+
padding: 10px 14px;
63+
margin: 8px 0;
64+
}
65+
66+
.tool-card__title {
67+
font-weight: 600;
68+
color: #334155;
69+
}
70+
71+
.tool-card__result {
72+
white-space: pre-wrap;
73+
font-size: 13px;
74+
margin-top: 6px;
75+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import type { Metadata } from "next";
2+
import "./globals.css";
3+
import { Providers } from "./providers";
4+
5+
export const metadata: Metadata = {
6+
title: "My Assistant",
7+
description: "Internal chat assistant with human-in-the-loop approval",
8+
};
9+
10+
export default function RootLayout({ children }: Readonly<{ children: React.ReactNode }>) {
11+
return (
12+
<html lang="en">
13+
<body>
14+
<Providers>{children}</Providers>
15+
</body>
16+
</html>
17+
);
18+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
"use client";
2+
3+
import { CopilotChat } from "@copilotkit/react-core/v2";
4+
import { AGENT_NAME } from "../lib/agent";
5+
6+
// The actual chat WIDGET — the one piece of UI every consumer needs that
7+
// isn't just plumbing. `providers.tsx` only wraps <CopilotKit> and registers
8+
// the HITL/tool-card hooks; this page renders the chat window itself.
9+
// `CopilotChat` takes the same `agentId` convention as the hooks (see
10+
// lib/agent.ts for why this is a separate identifier from the Foundry
11+
// hosted agent's own name).
12+
export default function Home() {
13+
return (
14+
<main style={{ maxWidth: 820, margin: "0 auto", padding: "24px" }}>
15+
<h1 style={{ fontSize: 22, fontWeight: 600, marginBottom: 8 }}>My Assistant</h1>
16+
<p style={{ color: "#555", marginBottom: 20 }}>
17+
Ask about pending records, or ask to approve a specific record id — approvals require your
18+
explicit confirmation.
19+
</p>
20+
<div style={{ height: "70vh", border: "1px solid #ddd", borderRadius: 8, overflow: "hidden" }}>
21+
<CopilotChat agentId={AGENT_NAME} />
22+
</div>
23+
</main>
24+
);
25+
}

skills/foundry-hosted-agent-copilotkit/references/troubleshooting.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,9 @@ what you actually have installed before trusting it.
1111
| Symptom | Cause | Fix |
1212
| --- | --- | --- |
1313
| `python3 -m venv` fails (`ensurepip is not available`) or `pip`/`apt-get install python3-venv` needs sudo you don't have | sandboxed/managed dev environment without system Python tooling | check for `uv` (`which uv`) and use `uv venv` / `uv pip install` instead — it doesn't need `ensurepip` or root. |
14+
| `smoke.py`/`browser_e2e.js` fail (or pass for the wrong reason) when run back-to-back, or after an interrupted earlier attempt | the hosted agent's example in-memory store (`_RECORDS`/whatever dict you're using) is **process-lifetime** state — every approve/reject call, from any script, mutates the SAME shared data | restart the hosted agent process (`azd ai agent run` / `python main.py`) between independent verification passes to get back to the seeded starting state. Don't assume a fresh run starts clean if a previous script (or an interrupted tool call) already approved/rejected the same record ids. |
15+
| A new `azd ai agent run` fails with `Address already in use` (often with a confusing hypercorn traceback) | a previous local hosted-agent process from an earlier/interrupted session is still holding the port | find and stop the old process (`ss -ltnp \| grep 8088` or similar) before starting a new one. |
16+
| The very first request to a freshly-started local hosted agent 404s with `DeploymentNotFound`, even though the deployment demonstrably exists (`az cognitiveservices account deployment list` shows it) | occasional local-dev warm-up flake in the hosted runtime/SDK, not a real config problem | retry once, or restart the hosted agent process with the same env vars — this has been observed to resolve itself immediately on retry. |
1417

1518
## HITL / approval
1619

@@ -21,6 +24,7 @@ what you actually have installed before trusting it.
2124
| Clicking Approve does nothing / tool never runs | assumed a specific resolve payload shape is framework-enforced | it isn't (see the CopilotKit bridge section below) — make sure your bridge's parser actually matches what the frontend's `respond(...)` sends. |
2225
| Approve works once, next message 400s with an orphaned tool-call id | stale/replayed approval payload re-sent to the hosted agent | don't replay raw history to the hosted agent; derive the next turn's input from the latest user text or the pending `mcp_approval_response`, chained via `previous_response_id`. |
2326
| Consequential tool runs WITHOUT asking | Tool missing `approval_mode="always_require"` | Decorate the consequential tool; check for at least one in your structural check. |
27+
| Approval card renders but shows the wrong/missing id or field when you renamed your gated tool's parameter (e.g. `record_id` → your own name) | the frontend's `ApprovalHitl.tsx` parses `function_arguments` and casts to a specific field name (`{ record_id?: string }` in the snippet) — this must match whatever parameter name your actual Python tool takes, since the bridge itself just forwards the model's raw arguments JSON verbatim | when you rename a gated tool's parameter, update the corresponding field name in the frontend's parsed-args cast to match — this is a presentation-only detail, not a bridge change. |
2428

2529
## AG-UI rendering (bridge-level)
2630

0 commit comments

Comments
 (0)