Skip to content

Instantly share code, notes, and snippets.

@niquola
Last active April 4, 2026 14:12
Show Gist options
  • Select an option

  • Save niquola/768db722523be5da221faf6dbd9125b7 to your computer and use it in GitHub Desktop.

Select an option

Save niquola/768db722523be5da221faf6dbd9125b7 to your computer and use it in GitHub Desktop.
Wsjet: CGI-style widgets for agentic workspaces

Wsjet: Hypermedia Widgets for Human-Agent Collaboration

The Insight

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?

What is Wsjet?

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

Two-Way Communication Channel

Wsjet creates a bidirectional bridge between human and agent:

Agent → Human (Show)

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."

Human → Agent (Collect)

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.

Interactive Loop (Collaborate)

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>
  `);
}

How It Works Technically

                     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 bun process (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.

Use Cases

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

The Agent's Perspective

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.

Why Not Just Chat?

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.

The CGI Renaissance

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment