ClickUI Email Live Session
Trigger Conditions
Use this skill when the user wants email handled in an AgentClick live session instead of chat.
Common triggers:
- "write the email and let me review first"
- "before sending, let me review"
- "draft it and I will approve"
- "review my inbox in UI"
- "reply in live session"
- "email live session"
If the task is email work plus live session handling in UI, use this skill over generic chat drafting.
Core Rules
- Always create an AgentClick live session with
type: "email_review". - Treat the AgentClick page as the active email client for this task.
- The same agent that creates the session must monitor and update it.
- Do not start a helper process, fake monitor, server-side monitor, or detached subagent monitor.
- If a true subagent exists in the caller environment and the user explicitly wants one, it may help fetch or draft, but the main responsibility still stays with the agent handling the session.
- Show full email content in the payload.
previewis only for the sidebar. - Do not pre-generate reply drafts unless the user explicitly asked for that before opening the page.
- Reply generation is lazy: generate only after the user clicks
Reply. - The reply text comes from the agent's own generation unless the user wired some other drafting system explicitly.
Preferences
Before opening a session, fetch user style preferences and apply any rules under ## Email Reply Style to every reply draft in this session:
curl -s "$AGENTCLICK_BASE/api/preferences/style"
Session Style Rules
When the poll result includes userIntention, treat it as a session-level style rule — apply it to every subsequent reply draft in this session, not just the current one. The server saves it to preferences automatically unless a conflict is detected.
Example: if userIntention is "say Hiiii instead of Hi", every reply generated after that point must use "Hiiii".
Preference Conflict Resolution
When the poll result includes preferenceConflict, the server found an existing rule that overlaps with the new one and did not auto-save. Resolve it before continuing.
{
"preferenceConflict": {
"existing": "the old rule already saved",
"incoming": "the new rule that was not saved"
}
}
Resolution steps:
Decide which action best fits. If the rules clearly conflict (e.g. opposite greetings, contradictory tones), guess replace. If they seem additive (e.g. greeting style vs. length style), guess keep both.
Tell the user your guess and ask for confirmation:
"Existing preference: [existing]. New preference: [incoming]. I'll [replace / keep both] — is that right? Or should I delete the old one instead?"
Based on the response:
Replace (new overrides old):
curl -s -X PUT "$AGENTCLICK_BASE/api/preferences/style" \ -H "Content-Type: application/json" \ -d "{\"oldRule\":\"EXISTING\",\"newRule\":\"INCOMING\",\"scope\":\"email\"}"Keep both (they coexist):
curl -s -X POST "$AGENTCLICK_BASE/api/preferences/style" \ -H "Content-Type: application/json" \ -d "{\"rule\":\"INCOMING\",\"scope\":\"email\"}"Delete old (remove the existing rule, do not save incoming):
curl -s -X DELETE "$AGENTCLICK_BASE/api/preferences/style" \ -H "Content-Type: application/json" \ -d "{\"rule\":\"EXISTING\",\"scope\":\"email\"}"
Resume polling after resolving.
Step 1: Ensure AgentClick is running
if curl -s --max-time 1 http://localhost:38173/api/health > /dev/null 2>&1; then
AGENTCLICK_BASE="http://localhost:38173"
else
AGENTCLICK_BASE="http://host.docker.internal:38173"
fi
if ! curl -s --max-time 1 "$AGENTCLICK_BASE/api/health" > /dev/null 2>&1; then
npm start >/tmp/agentclick.log 2>&1 &
for _ in $(seq 1 30); do
if curl -s --max-time 1 "$AGENTCLICK_BASE/api/health" > /dev/null 2>&1; then
break
fi
sleep 1
done
fi
curl -s --max-time 1 "$AGENTCLICK_BASE/api/health"
If health still fails, stop and fix the server problem before creating a session.
Step 2: Fetch Gmail data with the parallel fetch script
Use the bundled parallel fetch script as the default inbox-loading path. It uses gog underneath and is faster than serial message fetches.
Script:
skills/clickui-email/scripts/fetch_gmail_inbox_parallel.mjs
Example for 10 recent inbox emails:
node skills/clickui-email/scripts/fetch_gmail_inbox_parallel.mjs \
--query 'in:inbox' \
--max 10 \
--out /tmp/clickui_inbox.json
Example scoped to unread updates with explicit account and concurrency:
node skills/clickui-email/scripts/fetch_gmail_inbox_parallel.mjs \
--query 'category:updates is:unread' \
--max 10 \
--account you@gmail.com \
--concurrency 5 \
--out /tmp/clickui_inbox.json
Guidelines:
- Prefer 10 recent emails unless the user asked for a different count.
- Use this script first when loading multiple emails because it fetches in parallel.
- The script should produce inbox JSON for the live session payload. Load that file instead of rebuilding the inbox array inline when possible.
- Normalize categories to Gmail-style values when possible:
Primary,Social,Promotions,Updates,Forums. - Keep full email text in
bodyand only usepreviewfor the sidebar. - If you need one-off message detail beyond the script output, use
gog gmail get <message_id>.
Suggested inbox item shape:
{
"id": "gmail-message-id",
"category": "Updates",
"from": "Sender <sender@example.com>",
"to": ["me@example.com"],
"cc": [],
"bcc": [],
"subject": "Email subject",
"preview": "Short sidebar preview",
"body": "Full email body shown in the main panel",
"headers": {
"date": "Sat, 7 Mar 2026 10:30:00 -0500"
},
"read": false,
"gmailThreadId": "thread-id-from-gmail",
"gmailMessageId": "message-id-from-gmail"
}
Required fields for sending:
gmailThreadIdandgmailMessageIdare required for the server-side email send endpoint (/api/sessions/:id/email-send) to create a Gmail draft reply and send it. The parallel fetch script produces these automatically.
Step 3: Create the live session
Write the payload to disk before POSTing.
cat > /tmp/clickui_email_review.json <<'JSON'
{
"type": "email_review",
"sessionKey": "SESSION_KEY",
"payload": {
"inbox": [],
"draft": {
"replyTo": "",
"to": "",
"subject": "",
"paragraphs": []
}
}
}
JSON
RESPONSE=$(curl -s -X POST "$AGENTCLICK_BASE/api/review" \
-H "Content-Type: application/json" \
-d @/tmp/clickui_email_review.json)
SESSION_ID=$(echo "$RESPONSE" | grep -o '"sessionId":"[^"]*"' | cut -d'"' -f4)
echo "$SESSION_ID"
Payload rules:
- Include real inbox data in
payload.inbox. - Leave
payload.draft.paragraphsempty unless the user asked for an initial draft before page open. - The page should open with full emails available, not only previews.
Step 4: Monitor the session as the agent
After creating the session, the same agent must stay attached to it.
Environment detection: GOG_ACCOUNT is set in Docker (via docker-compose.yml) and absent elsewhere.
- In Docker (
GOG_ACCOUNTset): run ONE curl per exec call, check result in your context, then poll again. Do NOT use a bash while loop. You are the loop controller. - In non-Docker: the
/waitcall blocks until state changes, then process the result.
# Detect environment: GOG_ACCOUNT is set in Docker (docker-compose.yml), absent elsewhere
if [ -n "$GOG_ACCOUNT" ]; then
# Docker: use short-poll (one curl per exec, you are the loop controller)
curl -s "$AGENTCLICK_BASE/api/sessions/${SESSION_ID}"
else
# Non-Docker: use blocking /wait
curl -s --max-time 310 "$AGENTCLICK_BASE/api/sessions/${SESSION_ID}/wait"
fi
After each poll result:
- If
pageStatus.stopMonitoringistrue→ stop immediately (the/waitendpoint unblocks on this) - If
statusis"completed"→ stop - If
statusis"rewriting"→ handle the request (see Rewrite / Update Rules below), then poll again - Otherwise → wait 1 second (
sleep 1as a separate exec), then poll again
Rewrite / Update Rules
When the session returns status: "rewriting", inspect result and apply the minimum needed update.
The UI is not only a final approval screen. The user may:
- read emails
- mark emails as read
- click
Replyon one email while browsing others - request
Read More - edit reply paragraphs directly
- ask for a paragraph rewrite
- stop monitoring from the page
Reply requests
When the result has requestReplyDraft: true, follow this exact two-PUT sequence:
PUT 1 — mark as loading (do this immediately so the UI shows a spinner):
curl -s -X PUT "$AGENTCLICK_BASE/api/sessions/${SESSION_ID}/payload" \
-H "Content-Type: application/json" \
-d "{\"payload\":{\"inbox\":[{\"id\":\"${EMAIL_ID}\",\"replyState\":\"loading\"}]}}"
Generate the reply — use the email body from the poll response payload. Do not re-fetch from Gmail.
PUT 2 — deliver the draft (replyState: "ready" with the full draft):
cat > /tmp/reply_put.json <<JSON
{
"payload": {
"inbox": [
{
"id": "TARGET_EMAIL_ID",
"replyState": "ready",
"replyUnread": true,
"replyDraft": {
"replyTo": "TARGET_EMAIL_ID",
"to": "sender@example.com",
"subject": "Re: Original subject",
"paragraphs": [
{"id": "p1", "content": "First paragraph text"},
{"id": "p2", "content": "Second paragraph text"}
]
}
}
]
}
}
JSON
curl -s -X PUT "$AGENTCLICK_BASE/api/sessions/${SESSION_ID}/payload" \
-H "Content-Type: application/json" \
-d @/tmp/reply_put.json
CRITICAL — paragraph format:
paragraphsmust be an array of{"id": "p1", "content": "..."}objects. Plain strings will break the UI. Theidcan be any unique string (e.g."p1","p2").
CRITICAL — placement: The draft goes inside the email item in
inbox, NOT inpayload.draft. The UI readsreplyStateandreplyDraftfrom the individual email object.
Valid replyState values: "idle" (default), "loading" (generating), "ready" (draft available).
Why two PUTs: The server keeps the session in rewriting state as long as any email has replyState: "loading". PUT 1 sets loading to lock the rewriting state open; PUT 2 delivers the finished draft. Without PUT 1, the session drops back to pending between calls and the second PUT is rejected.
Read more requests
If result.readMore is true:
- fetch more Gmail emails with
gog - keep the request scoped to the current category filter if the result includes categories
- merge new emails into the current inbox payload instead of replacing the whole list unless replacement is explicitly intended
Read state changes
If the user marks emails as read:
- record which message ids changed
- if the task includes Gmail sync, update Gmail through
gog - reflect the new read state in the session payload
Draft edits
If the user edits draft paragraphs in the page:
- preserve their edits
- only regenerate paragraphs the user explicitly asked to rewrite
- keep
replyTo,to, andsubjectstable unless the UI explicitly changed supported fields - CC and BCC additions from the page should be preserved and returned
PUT payload updates
Always write the updated payload to a temp file before PUT.
When updating after a reply request, place the draft on the email item (not in payload.draft).
Only include the target email in the inbox array — the server merges it with existing emails by id, so other emails are preserved:
cat > /tmp/clickui_email_payload.json <<'JSON'
{
"payload": {
"inbox": [
{
"id": "target-email-id",
"replyState": "ready",
"replyUnread": false,
"replyDraft": {
"replyTo": "sender@example.com",
"to": "sender@example.com",
"subject": "Re: Original subject",
"paragraphs": [
{"id": "p1", "content": "Paragraph 1"},
{"id": "p2", "content": "Paragraph 2"}
]
}
}
]
}
}
JSON
curl -s -X PUT "$AGENTCLICK_BASE/api/sessions/${SESSION_ID}/payload" \
-H "Content-Type: application/json" \
-d @/tmp/clickui_email_payload.json
Rules:
- Reuse the same
SESSION_IDfor the full interaction. - For reply drafts, always use the two-PUT sequence described in the Reply requests section above.
- If PUT fails with
"Session is not in rewriting state", the loading email item was not set — re-trigger by calling/completewithregenerate: true, then repeat the two-PUT sequence. - If PUT fails for any other reason, fix it before continuing.
Completion Rules
When the user clicks Confirm & Send in the UI:
- the frontend calls
POST /api/sessions/:id/email-sendwhich creates a Gmail draft and sends it server-side viagog - it also syncs read state for marked-as-read emails
- the agent does not need to send the email — the server handles it
- the agent should treat the session as approved work and continue monitoring for further actions
- do not ask the user again if they already confirmed in UI
- for the send to work, inbox emails must include
gmailThreadIdandgmailMessageId(produced by the parallel fetch script)
When the user stops monitoring from the page:
- stop immediately
- do not keep polling in the background
- do not leave any detached monitor running
UI Expectations
Assume the page behaves like this and update payloads accordingly:
- Full email content is shown in the main panel.
- Sidebar uses short preview text only.
- Category filters may be folded by default.
Read Moreappears at the bottom of the email list.- Reply draft is folded by default.
- Paragraphs can be edited directly and also individually rewritten.
- When a draft becomes ready after a reply request, the corresponding email row may show a ready state and an unread marker until opened.
- Clicking stop or back may set
pageStatus.stopMonitoring = true.
Practical Notes
- Keep the monitor logic in the current agent turn when feasible.
- If the environment cannot keep a long blocking wait, poll the session every 10 seconds instead.
- Do not claim a reply came from Gmail or from a background process if the agent generated it.
- Prefer the bundled parallel fetch script for inbox loading, and use direct
gogcalls for one-off follow-up detail when needed. - The
/waitand short-poll responses include the full session payload. Use this data to generate reply drafts — do not re-fetch emails from Gmail unless the body is missing.