A prototype workspace where personal AI agents work alongside humans in shared documents and chat β visible, transparent, and collaborating with each other in real time.
By Oliver Newth (n3wth).
Collab explores a new paradigm: AI agents as ambient collaborators, not just assistants. Instead of chatting with a single AI in a sidebar, you work in a shared space where your agents are present β you can see their cursors, watch them think, and observe them edit documents in real time.
The demo centers on two AI personas editing a shared project proposal together with a human team:
| Agent | Avatar | Expertise |
|---|---|---|
| Aiden | Animated dark blob (seed 1) | Technical architecture, specifications, data models |
| Nova | Animated dark blob (seed 2) | Product strategy, UX design, adoption risks, user journeys |
Both agents are powered by Google Gemini 2.5 Flash and coordinate their turns through a shared queue so they never conflict.
- Live agent cursors β Animated avatars move through the document as agents read and write
- Animated blob avatars β Each agent has a unique organic canvas blob that pulses and morphs with their activity state
- Agent hover cards β Hover over any agent avatar (in the header or chat) to see model, persona, tools, owner, and live status
- Reasoning chain β Gemini's thinking steps are surfaced per message and collapsible in the chat
- Thought bubbles β Agents display their reasoning before acting, making AI transparent
- Agent-to-agent collaboration β Agents tag each other and respond to each other's edits
- Structured document actions β Agents can
insert,replace,read, orchat - Conflict-free editing β An editor lock prevents simultaneous document mutations
- Rate-limited API calls β Automatic backoff and retry (7 seconds minimum between calls; exponential on 429)
- Duplicate-heading guard β Agents never insert a section that already exists
- Chat-driven triggers β Natural language commands open/close the doc and direct agent activity
- Reset session β One-click button clears all messages and agent state
- Secure API proxy β Gemini key is never exposed in production (Vercel serverless function)
%%{init: {'theme': 'base', 'themeVariables': { 'primaryColor': '#dbeafe', 'primaryTextColor': '#1a1a2e', 'primaryBorderColor': '#2563eb', 'lineColor': '#64748b' }}}%%
graph TD
subgraph Browser["π Browser β React SPA"]
CP["π¬ Chat Panel<br/>Message bubbles Β· Status chips Β· Input"]
DP["π Document Panel<br/>Tiptap editor + AgentCursors<br/>cursors Β· thoughts Β· selections"]
CP & DP --> App
App["βοΈ App.tsx β State<br/>docOpen Β· aiden / nova Β· messages#91;#93;"]
App --> Orch
Orch["π― orchestrator.ts<br/>Turn queue & coordination<br/>Triggers: doc-opened Β· user-message Β· agent-tagged"]
Orch --> Agent
Orch --> Actions
Agent["π€ agent.ts β askAgent#40;#41;<br/>Prompts Β· Rate limit Β· JSON repair"]
Actions["β‘ agent-actions.ts β executeAction#40;#41;<br/>insert Β· replace Β· read Β· chat Β· editor lock"]
end
Agent -- "π HTTPS" --> Proxy
Proxy --> Gemini
Proxy["π /api/gemini.ts<br/>Vercel serverless proxy β hides API key"]
Gemini["β¨ Gemini 2.5 Flash<br/>Google AI API β LLM reasoning"]
classDef ui fill:#dbeafe,stroke:#2563eb,stroke-width:1.5px,color:#1e3a5f
classDef state fill:#ede9fe,stroke:#7c3aed,stroke-width:1.5px,color:#3b1f6e
classDef orch fill:#fce7f3,stroke:#db2777,stroke-width:1.5px,color:#4a1930
classDef agent fill:#e0f2fe,stroke:#0284c7,stroke-width:1.5px,color:#0c4a6e
classDef actions fill:#ffedd5,stroke:#ea580c,stroke-width:1.5px,color:#6b2f0a
classDef proxy fill:#dcfce7,stroke:#16a34a,stroke-width:1.5px,color:#14532d
classDef gemini fill:#fef9c3,stroke:#ca8a04,stroke-width:1.5px,color:#713f12
class CP,DP ui
class App state
class Orch orch
class Agent agent
class Actions actions
class Proxy proxy
class Gemini gemini
style Browser fill:#f8fafc,stroke:#475569,stroke-width:2px,color:#1e293b
| Module | Lines | Responsibility |
|---|---|---|
src/App.tsx |
610 | Root component: split layout, state, user input handlers |
src/orchestrator.ts |
318 | Agent turn queue, trigger dispatch, autonomous turn cap |
src/agent.ts |
459 | Gemini API calls, prompt building, rate limiting, JSON repair |
src/agent-actions.ts |
467 | Editor mutations: insert/replace/read/chat, cursor animation |
src/agent-cursor.ts |
190 | Custom Tiptap extension: cursor widgets, thought bubbles |
api/gemini.ts |
61 | Vercel serverless proxy β forwards requests, hides key |
%%{init: {'theme': 'base', 'themeVariables': { 'primaryColor': '#dbeafe', 'primaryTextColor': '#1a1a2e', 'primaryBorderColor': '#2563eb', 'lineColor': '#64748b' }}}%%
flowchart TD
A(["π€ User sends message"]):::input --> B["βοΈ App.tsx"]:::state
B --> C["π― Orchestrator"]:::orch
C --> D{"π Detect trigger type"}:::decision
D -- "π doc-opened" --> E["Enqueue both agents<br/>with initial instructions"]:::trigger
D -- "π¬ user-message" --> F["Clear queue, detect @mention<br/>and enqueue relevant agent"]:::trigger
D -- "π·οΈ agent-tagged" --> G["Limited back-and-forth<br/>max 2 tags"]:::trigger
E --> H["βοΈ processQueue#40;#41;"]:::process
F --> H
G --> H
H --> I["π€ askAgent#40;params#41;"]:::agent
I --> J["π Build prompt<br/>β’ Persona injection<br/>β’ Doc text β€ 2 000 chars<br/>β’ Chat history β last 6<br/>β’ Recent changes context"]:::agent
J --> K["π‘ POST /api/gemini"]:::api
K --> L["β¨ Gemini 2.5 Flash"]:::gemini
L --> M["π§ Parse JSON response<br/>with repair on truncation"]:::process
M -- "π insert" --> N["π Acquire lock<br/>Insert at end / heading<br/>char-by-char"]:::action
M -- "βοΈ replace" --> O["π Acquire lock<br/>Find & replace<br/>char-by-char"]:::action
M -- "ποΈ read" --> P["Highlight text<br/>Show thought bubble<br/>3.5 s"]:::action
N --> Q(["β
Turn complete"]):::done
O --> Q
P --> Q
Q --> R{"π Was doc edited?"}:::decision
R -- "Yes" --> S["π¨ Enqueue other<br/>agent to react"]:::trigger
R -- "No" --> T["Continue queue"]:::process
S --> U{"π’ Autonomous turn cap?<br/>max 3 per agent / session"}:::decision
T --> U
U -- "Yes" --> V["π Stop auto turns"]:::stop
U -- "No" --> W["βΆοΈ Next turn"]:::next
classDef input fill:#dbeafe,stroke:#2563eb,stroke-width:2px,color:#1e3a5f
classDef state fill:#ede9fe,stroke:#7c3aed,stroke-width:1.5px,color:#3b1f6e
classDef orch fill:#fce7f3,stroke:#db2777,stroke-width:1.5px,color:#4a1930
classDef decision fill:#fef3c7,stroke:#d97706,stroke-width:2px,color:#78350f
classDef trigger fill:#e0e7ff,stroke:#4f46e5,stroke-width:1.5px,color:#312e81
classDef process fill:#f1f5f9,stroke:#64748b,stroke-width:1.5px,color:#334155
classDef agent fill:#e0f2fe,stroke:#0284c7,stroke-width:1.5px,color:#0c4a6e
classDef api fill:#dcfce7,stroke:#16a34a,stroke-width:1.5px,color:#14532d
classDef gemini fill:#fef9c3,stroke:#ca8a04,stroke-width:1.5px,color:#713f12
classDef action fill:#ffedd5,stroke:#ea580c,stroke-width:1.5px,color:#6b2f0a
classDef done fill:#dcfce7,stroke:#16a34a,stroke-width:2px,color:#14532d
classDef stop fill:#fee2e2,stroke:#dc2626,stroke-width:1.5px,color:#7f1d1d
classDef next fill:#dcfce7,stroke:#16a34a,stroke-width:1.5px,color:#14532d
| Action | Description | Lock needed? |
|---|---|---|
insert |
Appends content blocks at end or after a heading | Yes |
replace |
Finds exact text, deletes it, types replacement | Yes |
read |
Highlights a passage, shows thought bubble for 3.5 seconds | No |
chat |
Sends a chat message only, no editor interaction | No |
%%{init: {'theme': 'base', 'themeVariables': { 'actorBkg': '#f1f5f9', 'actorBorder': '#475569', 'actorTextColor': '#1a1a2e', 'actorLineColor': '#94a3b8', 'signalColor': '#475569', 'signalTextColor': '#1a1a2e', 'noteBkgColor': '#fef9c3', 'noteBorderColor': '#ca8a04', 'noteTextColor': '#713f12', 'activationBkgColor': '#dbeafe', 'activationBorderColor': '#2563eb', 'sequenceNumberColor': '#ffffff' }}}%%
sequenceDiagram
autonumber
participant A as π΅ Aiden Β· Technical
participant O as π― Orchestrator
participant N as π Nova Β· Product
A->>O: Inserts "Technical Architecture" section
activate O
O->>O: Detects insert action
O->>N: Auto-enqueue β React to Aiden's changes
deactivate O
activate N
N->>N: Reads section & decides response
N->>O: Chat: @Aiden should we add onboarding flows?
deactivate N
activate O
O->>O: Detects @Aiden mention (agent-tagged)
O->>A: Enqueue response
deactivate O
activate A
A->>O: Responds
deactivate A
Note over A,N: π Tag limit prevents infinite loops β MAX_AGENT_TAGS = 2
%%{init: {'theme': 'base', 'themeVariables': { 'primaryColor': '#f1f5f9', 'primaryTextColor': '#1a1a2e', 'primaryBorderColor': '#475569', 'lineColor': '#64748b' }}}%%
flowchart LR
subgraph rl["β±οΈ Rate Limiter"]
direction TB
A["π Min interval β 7 000 ms between calls"]:::config
B["π Max retries β 3 attempts per request"]:::config
C["π Backoff on 429:<br/>5 s β 10 s β 20 s β 40 s β 60 s"]:::warn
D["π 3 consecutive errors β 30 s cool-down"]:::error
A ~~~ B ~~~ C ~~~ D
end
classDef config fill:#e0f2fe,stroke:#0284c7,stroke-width:1.5px,color:#0c4a6e
classDef warn fill:#fef3c7,stroke:#d97706,stroke-width:1.5px,color:#78350f
classDef error fill:#fee2e2,stroke:#dc2626,stroke-width:1.5px,color:#7f1d1d
style rl fill:#f8fafc,stroke:#475569,stroke-width:2px,color:#1e293b
The 7-second minimum interval keeps usage safely below the free-tier limit of ~10 RPM.
- Node.js 18 or later
- npm 9 or later
- A Google AI Studio API key (free tier works)
git clone https://github.com/n3wth/markup.git
cd markup
npm installCreate a .env.local file in the project root:
# Used by the server-side proxy (never exposed to the client)
GEMINI_API_KEY=your_gemini_api_key_hereNote: The API key is only read server-side by the
/api/geminiproxy. It is never bundled into the client.
npm run devOpen http://localhost:5173 in your browser.
| What to type | What happens |
|---|---|
Open the doc |
Both agents enter the document and start collaborating |
@Aiden add a technical spec |
Aiden receives the instruction and edits the doc |
@Nova what's the user journey? |
Nova responds with product perspective |
Come back / Stop |
Agents exit the document |
collab/
βββ api/
β βββ gemini.ts # Vercel serverless proxy β hides Gemini key in prod
β
βββ docs/
β βββ diagrams/ # Mermaid diagram sources (.mmd)
β βββ high-level-component-map.mmd
β βββ agent-turn-lifecycle.mmd
β βββ agent-to-agent-collaboration.mmd
β βββ rate-limiting.mmd
β βββ proxy-flow.mmd
β
βββ src/
β βββ main.tsx # React entry point
β βββ App.tsx # Root component: layout, state, user handlers
β βββ App.css # All styling (layout, animations, agent colours)
β βββ index.css # CSS resets & globals
β βββ blob-avatar.tsx # Animated blob avatars (simplex-noise, canvas)
β βββ lib/
β β βββ utils.ts # Shared utilities
β βββ agent.ts # Gemini API calls, prompt building, rate limiting
β βββ agent-actions.ts # Editor mutations and cursor animations
β βββ agent-cursor.ts # Custom Tiptap extension for agent cursors
β βββ orchestrator.ts # Turn queue and agent coordination
β βββ __tests__/ # Vitest unit tests
β
βββ public/
β βββ vite.svg
β
βββ index.html # HTML shell
βββ vite.config.ts # Vite build configuration
βββ components.json # shadcn/ui component config
βββ tsconfig.json # TypeScript root config
βββ tsconfig.app.json # App TypeScript config (strict, ES2022)
βββ tsconfig.node.json # Node TypeScript config
βββ eslint.config.js # ESLint rules
βββ package.json # Dependencies and scripts
"You are Aiden. You have strong opinions about clean architecture and precise specifications."
- Writes technical specifications and data models
- Proposes system architecture and protocols
- Adds implementation-level detail to proposals
- Avatar: Animated dark blob (simplex-noise seed 1)
"You are Nova. You champion the user and are skeptical of complexity."
- Identifies UX gaps and adoption risks
- Adds user scenarios and journey maps
- Questions assumptions with product strategy lens
- Avatar: Animated dark blob (simplex-noise seed 2)
Both agents receive the same document context and recent chat history on every turn, enabling coherent multi-turn collaboration.
| Script | Description |
|---|---|
npm run dev |
Start Vite dev server with HMR at localhost:5173 |
npm run build |
Type-check + bundle for production (dist/) |
npm run typecheck |
Type-check only (no bundle) |
npm run preview |
Preview the production build locally |
npm run lint |
Run ESLint across all source files |
npm run test |
Run Vitest unit tests |
This project is designed to deploy on Vercel using its native serverless function support.
npm i -g vercel
vercel --prodIn the Vercel dashboard β Settings β Environment Variables, add:
GEMINI_API_KEY = your_gemini_api_key_here
The client in production will call /api/gemini (the serverless proxy) instead of the Gemini API directly, keeping your key secure.
%%{init: {'theme': 'base', 'themeVariables': { 'actorBkg': '#f1f5f9', 'actorBorder': '#475569', 'actorTextColor': '#1a1a2e', 'actorLineColor': '#94a3b8', 'signalColor': '#475569', 'signalTextColor': '#1a1a2e', 'noteBkgColor': '#dcfce7', 'noteBorderColor': '#16a34a', 'noteTextColor': '#14532d', 'activationBkgColor': '#e0f2fe', 'activationBorderColor': '#0284c7', 'sequenceNumberColor': '#ffffff' }}}%%
sequenceDiagram
autonumber
participant B as π Browser
participant V as π Vercel Function<br/>(api/gemini.ts)
participant G as β¨ Gemini API
B->>+V: POST /api/gemini
Note over V: π Reads process.env.GEMINI_API_KEY
V->>+G: POST with API key
G-->>-V: Response
V-->>-B: JSON response
Note over B,G: π API key never exposed to the browser
| Layer | Technology | Version |
|---|---|---|
| UI Framework | React | 19.2 |
| Language | TypeScript | 5.9 |
| Build Tool | Vite | 7.3 |
| Rich Text Editor | Tiptap | 3.20 |
| AI Model | Gemini 2.5 Flash | β |
| Serverless Hosting | Vercel | β |
| Agent avatars | BlobAvatar (simplex-noise) | Custom canvas blobs |
| Concern | Development | Production |
|---|---|---|
| Gemini API key | In .env.local (loaded by dev server only) |
In Vercel env var (server-side only) |
| Key exposure | Not in client bundle; proxy reads env | Hidden behind serverless proxy |
| Request validation | None | None (add auth if needed) |
For a production deployment with multiple users, add authentication to
/api/geminito prevent key abuse.
- No persistence β All state is lost on page refresh
- Single document β One hardcoded proposal; no multi-doc support
- Fixed agents β Only Aiden and Nova; personas are hardcoded
- Single session β No real multi-user collaboration (everyone sees the same local state)
- Rate limited β 7 s between API calls; interactions can feel slow
- Autonomous turn cap β Max 3 autonomous turns per agent per session (cost control)
- Mobile unfriendly β Fixed-width layout assumes a desktop viewport
Oliver Newth (n3wth) β prototype, architecture, and implementation.
MIT β see LICENSE for details.