In agentic workflows, communication between human and AI is mostly text — chat messages back and forth. But many interactions are better served by structured UI: a form to fill, a table to review, a dashboard to monitor, a diff to approve.
What if the agent could create UI as easily as it creates files?
Wsjet (workspace jet) is a CGI-style convention where a .wsjet.ts file in the workspace becomes an interactive widget. The agent writes a TypeScript file, and the platform immediately renders it as HTML in the workspace UI — no server, no build step, no deployment.
Agent writes tasks.wsjet.ts → ⚡ tasks appears in sidebar → user clicks → interactive UI
Wsjet creates a bidirectional bridge between human and agent:
The agent creates a wsjet to present something:
// report.wsjet.ts — Agent creates this to show analysis results
const data = await Bun.file(".analysis.json").json();
console.log(`
<h2>Code Analysis Report</h2>
<table>
<tr><th>Metric</th><th>Value</th><th>Status</th></tr>
${data.metrics.map(m => `
<tr>
<td>${m.name}</td>
<td>${m.value}</td>
<td>${m.ok ? '✅' : '❌'}</td>
</tr>
`).join("")}
</table>
<p>Generated at ${new Date().toISOString()}</p>
`);The agent says in chat: "I've analyzed the codebase. Check ⚡ report for details."
The agent creates a wsjet to collect input from the user:
// config.wsjet.ts — Agent needs user decisions before proceeding
const path = process.env.PATH_INFO || "/";
const method = process.env.REQUEST_METHOD || "GET";
const wsDir = process.env.WORKSPACE_DIR!;
if (method === "GET") {
console.log(`
<div class="p-6 max-w-lg">
<h2>Configuration Needed</h2>
<p class="text-sm text-gray-500 mb-4">
I need a few decisions before setting up the database schema.
</p>
<form hx-post="/jet/myws/config/submit" hx-target="#file-content" hx-swap="innerHTML">
<label>Database name</label>
<input name="dbName" value="myapp" required class="w-full border rounded px-3 py-2 mb-3" />
<label>Enable audit logging?</label>
<select name="audit" class="w-full border rounded px-3 py-2 mb-3">
<option value="yes">Yes — log all changes</option>
<option value="no">No — skip logging</option>
</select>
<label>Tables to create</label>
<div class="space-y-1 mb-4">
<label><input type="checkbox" name="tables" value="users" checked /> users</label>
<label><input type="checkbox" name="tables" value="posts" checked /> posts</label>
<label><input type="checkbox" name="tables" value="comments" /> comments</label>
<label><input type="checkbox" name="tables" value="tags" /> tags</label>
</div>
<button type="submit" class="bg-blue-600 text-white px-4 py-2 rounded">
Apply Configuration
</button>
</form>
</div>
`);
}
if (path === "/submit" && method === "POST") {
const form = new URLSearchParams(await Bun.stdin.text());
const config = {
dbName: form.get("dbName"),
audit: form.get("audit") === "yes",
tables: form.getAll("tables"),
submittedAt: new Date().toISOString(),
};
await Bun.write(`${wsDir}/.user-config.json`, JSON.stringify(config, null, 2));
console.log(`
<div class="p-6">
<h2>✅ Configuration saved</h2>
<p>The agent will pick up your choices and proceed.</p>
<pre class="bg-gray-50 p-4 rounded mt-4 text-sm">${JSON.stringify(config, null, 2)}</pre>
</div>
`);
}The agent says: "I need your input on the database setup. Please fill out ⚡ config."
After the user submits, the agent reads .user-config.json and continues.
The agent creates a wsjet for ongoing collaboration:
// review.wsjet.ts — Code review interface
const path = process.env.PATH_INFO || "/";
const wsDir = process.env.WORKSPACE_DIR!;
const changes = await Bun.file(`${wsDir}/.pending-changes.json`).json().catch(() => []);
if (path === "/") {
console.log(`
<div class="p-4">
<h2>Pending Changes (${changes.length})</h2>
${changes.map((c, i) => `
<div class="border rounded p-3 mb-2">
<div class="font-medium">${c.file}</div>
<pre class="bg-gray-50 p-2 mt-2 text-xs">${c.diff}</pre>
<div class="flex gap-2 mt-2">
<button hx-post="/jet/ws/review/approve?idx=${i}"
hx-target="#file-content" hx-swap="innerHTML"
class="bg-green-600 text-white px-3 py-1 rounded text-sm">Approve</button>
<button hx-post="/jet/ws/review/reject?idx=${i}"
hx-target="#file-content" hx-swap="innerHTML"
class="bg-red-500 text-white px-3 py-1 rounded text-sm">Reject</button>
</div>
</div>
`).join("")}
</div>
`);
} Workspace UI (browser)
|
htmx GET/POST /jet/{ws}/{name}/{path}
|
wsmanager.ts (proxy)
|
wmlet.ts
|
bun run {name}.wsjet.ts
env: REQUEST_METHOD, PATH_INFO,
QUERY_STRING, WORKSPACE_DIR
stdin: POST body
|
stdout → HTML fragment
|
htmx swaps into #file-content div
Key properties:
- Stateless — each request is a fresh
bunprocess (like PHP/CGI) - No iframe — HTML injected directly into workspace DOM
- htmx-native — forms and links work via htmx attributes
- Shared styles — wsjet HTML uses workspace's Tailwind CSS
- Filesystem state — read/write JSON files in workspace directory
- Full Bun runtime — access to Bun.sql, fetch, file I/O, crypto, etc.
| Pattern | Agent Creates | User Interacts |
|---|---|---|
| Show results | Analysis report, test summary, data visualization | View, export |
| Collect input | Configuration form, parameter selection | Fill form, submit |
| Review changes | Diff viewer, approval UI | Approve/reject each change |
| Dashboard | Live metrics, database tables, API status | Monitor, drill down |
| Wizard | Multi-step setup with validation | Step through, configure |
| Data entry | CRUD forms for database records | Create, edit, delete |
| Approval gate | "I want to do X, Y, Z — approve?" | Checkbox + confirm |
From the agent's CLAUDE.md:
## Wsjet — Interactive Widgets
Create `*.wsjet.ts` files to show UI to the user.
The file receives HTTP requests via env vars and writes HTML to stdout.
Available env vars:
- REQUEST_METHOD — GET, POST, etc.
- PATH_INFO — path after the wsjet name
- QUERY_STRING — URL query parameters
- WORKSPACE_DIR — workspace root directory
- CONTENT_TYPE — request content type
- stdin — POST/PUT body
The HTML is rendered inside the workspace UI with htmx and Tailwind CSS.
Use hx-get/hx-post attributes for interactivity.
Example: create a form that saves user input to a JSON file,
then read that file to continue your work.Chat is great for conversation. But some interactions need structure:
- "Pick 3 tables from this list of 20" → checkboxes are better than typing names
- "Here are 5 proposed changes, approve each" → approve/reject buttons per item
- "Configure these 8 parameters" → a form with defaults and validation
- "Here's the data I found" → a sortable table, not a wall of text
- "Monitor this process" → a live dashboard that auto-refreshes
Wsjet lets the agent create the right UI for each interaction, while keeping everything inside the workspace — no external tools, no context switching.
| Era | Technology | Script → HTML |
|---|---|---|
| 1993 | CGI | Perl script → full page |
| 2004 | PHP | .php file → full page |
| 2015 | Serverless | Lambda → JSON API |
| 2026 | Wsjet | .wsjet.ts → HTML fragment via htmx |
The wheel turns. Sometimes the simplest approach wins.
Try it: github.com/agentic-workspaces/reference-implementation