@tstax/coding-tab 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,119 @@
1
+ # @tstax/coding-tab
2
+
3
+ Drop a Cursor-style coding agent tab into any Express app. Sign in with GitHub, give the agent a prompt in **Plan** or **Agent** mode, review the output, click **Execute** to make code changes, then **Merge & Redeploy** to land a PR — and let your existing GitHub-to-Railway (or Vercel, or Render) auto-deploy ship the change.
4
+
5
+ Powered by [`@cursor/sdk`](https://www.npmjs.com/package/@cursor/sdk) cloud runtime, so the agent behaves the same as Cursor's own chat: real plan/agent loops, real file editing, real PRs.
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ npm install @tstax/coding-tab
11
+ ```
12
+
13
+ ## What you need
14
+
15
+ 1. **Cursor API key** — from <https://cursor.com/dashboard/cloud-agents>.
16
+ 2. **A GitHub OAuth App** for each app that embeds this tab (one-time, ~2 minutes per app):
17
+ - <https://github.com/settings/developers> → New OAuth App
18
+ - Authorization callback URL: `https://<your-app>/coding-tab/auth/callback`
19
+ - Copy the Client ID, generate a Client Secret.
20
+ 3. **Node 20+** running an Express server.
21
+
22
+ ## Mount it on your server
23
+
24
+ ```ts
25
+ import express from "express";
26
+ import { mountCodingTab } from "@tstax/coding-tab/server";
27
+
28
+ const app = express();
29
+
30
+ mountCodingTab(app, {
31
+ cursorApiKey: process.env.CURSOR_API_KEY!,
32
+ githubOAuth: {
33
+ clientId: process.env.GITHUB_CLIENT_ID!,
34
+ clientSecret: process.env.GITHUB_CLIENT_SECRET!,
35
+ callbackUrl: `${process.env.PUBLIC_BASE_URL}/coding-tab/auth/callback`,
36
+ allowedLogins: (process.env.ALLOWED_GITHUB_LOGIN ?? "").split(",").filter(Boolean),
37
+ },
38
+ sessionPassword: process.env.SESSION_SECRET!, // 32+ chars: openssl rand -hex 32
39
+ defaultRepo: { url: "https://github.com/youruser/yourrepo" },
40
+ });
41
+
42
+ app.listen(3000);
43
+ ```
44
+
45
+ That's it on the server. Visit `https://<your-app>/coding-tab/` and the tab is fully self-rendering. Or embed it inside your dashboard:
46
+
47
+ ```html
48
+ <div id="ct"></div>
49
+ <link rel="stylesheet" href="/coding-tab/style.css" />
50
+ <script src="/coding-tab/browser.js"></script>
51
+ <script>
52
+ CodingTab.mountCodingTab(document.getElementById("ct"), { apiBase: "/coding-tab" });
53
+ </script>
54
+ ```
55
+
56
+ ## Required env vars
57
+
58
+ | Var | What it is |
59
+ | --- | --- |
60
+ | `CURSOR_API_KEY` | API key from cursor.com/dashboard/cloud-agents |
61
+ | `GITHUB_CLIENT_ID` | From your OAuth App |
62
+ | `GITHUB_CLIENT_SECRET` | From your OAuth App |
63
+ | `SESSION_SECRET` | 32+ char random string (`openssl rand -hex 32`) |
64
+ | `ALLOWED_GITHUB_LOGIN` | Comma-separated GitHub usernames allowed to sign in |
65
+ | `PUBLIC_BASE_URL` | Your app's external URL, used to build the OAuth callback |
66
+
67
+ ## Security model
68
+
69
+ - **GitHub OAuth, single sign-in.** The user signs in with GitHub once. The OAuth access token authenticates the session AND authorizes clone/push/PR/merge — no separate PAT to manage.
70
+ - **Allowlist.** Set `allowedLogins` to your GitHub username (and anyone else you trust). Anyone outside the list gets a 403 even with a valid GitHub login.
71
+ - **HTTP-only signed encrypted cookie.** Session is stored in an `iron-session` cookie (`coding_tab_session`): JS on the page can't read the access token. Server decrypts on every request.
72
+ - **CSRF-protected handshake.** OAuth uses a per-request `state` cookie that expires in 10 minutes.
73
+ - **Cookie hardening.** `HttpOnly`, `SameSite=Lax`, `Secure` in production.
74
+
75
+ ## Plan vs Agent mode
76
+
77
+ - **Plan** — agent runs read-only, produces a markdown plan ending in `PLAN READY`. Click **Execute plan** to follow up with code changes in the same conversation.
78
+ - **Agent** — agent goes straight to making changes and opens a PR (`autoCreatePR: true`).
79
+
80
+ The conversation context is preserved across plan and execute turns, just like Cursor chat.
81
+
82
+ ## Models
83
+
84
+ The picker shows **Sonnet** and **Opus**, mapped to whatever Claude variants your Cursor account exposes via `Cursor.models.list()`. If your account stops offering Sonnet/Opus directly (Cursor occasionally changes what's available), the dropdown will only show what's accessible.
85
+
86
+ ## API surface (mounted at `basePath`, default `/coding-tab`)
87
+
88
+ Public:
89
+ - `GET /` — host HTML
90
+ - `GET /style.css`, `GET /browser.js` — assets
91
+ - `GET /auth/login` — start OAuth
92
+ - `GET /auth/callback` — OAuth return
93
+ - `POST /auth/logout`
94
+ - `GET /auth/me` — 401 if not signed in, otherwise `{ githubLogin, avatarUrl? }`
95
+
96
+ Authenticated (require valid session):
97
+ - `GET /models` — discovered Sonnet/Opus IDs
98
+ - `POST /agent/start` — SSE stream; creates a new agent + first run
99
+ - `POST /agent/send` — SSE stream; follow-up turn on existing session
100
+ - `POST /agent/execute` — SSE stream; turns the most recent plan into changes
101
+ - `POST /agent/cancel` — cancel an in-flight run
102
+ - `POST /agent/dispose` — release an agent session
103
+ - `GET /pr/status?prUrl=...` — PR metadata + mergeability
104
+ - `POST /pr/merge` — merge a PR
105
+
106
+ ## Development
107
+
108
+ ```bash
109
+ git clone https://github.com/tylerackerman/coding-tab
110
+ cd coding-tab
111
+ npm install
112
+ npm run dev # tsup --watch
113
+ npm run lint # tsc --noEmit
114
+ npm test
115
+ ```
116
+
117
+ ## License
118
+
119
+ MIT
@@ -0,0 +1,60 @@
1
+ "use strict";var CodingTab=(()=>{var v=Object.defineProperty;var A=Object.getOwnPropertyDescriptor;var U=Object.getOwnPropertyNames;var j=Object.prototype.hasOwnProperty;var N=(i,o)=>{for(var c in o)v(i,c,{get:o[c],enumerable:!0})},B=(i,o,c,a)=>{if(o&&typeof o=="object"||typeof o=="function")for(let m of U(o))!j.call(i,m)&&m!==c&&v(i,m,{get:()=>o[m],enumerable:!(a=A(o,m))||a.enumerable});return i};var G=i=>B(v({},"__esModule",{value:!0}),i);var K={};N(K,{mountCodingTab:()=>w});var D='<svg width="18" height="18" viewBox="0 0 16 16" fill="currentColor"><path d="M8 0C3.58 0 0 3.58 0 8a8 8 0 005.47 7.59c.4.07.55-.17.55-.38v-1.34c-2.23.48-2.7-1.07-2.7-1.07-.36-.92-.89-1.16-.89-1.16-.73-.5.05-.49.05-.49.81.06 1.23.83 1.23.83.72 1.23 1.88.87 2.34.66.07-.52.28-.87.5-1.07-1.78-.2-3.65-.89-3.65-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.13 0 0 .67-.21 2.2.82a7.65 7.65 0 014 0c1.53-1.04 2.2-.82 2.2-.82.44 1.11.16 1.93.08 2.13.51.56.82 1.28.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.74.54 1.49v2.21c0 .21.15.46.55.38A8 8 0 0016 8c0-4.42-3.58-8-8-8z"/></svg>';function g(i){return i.replace(/[&<>"']/g,o=>({"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;","'":"&#39;"})[o])}function J(i){return g(i).replace(/`([^`\n]+)`/g,'<code style="background:rgba(255,255,255,0.06);padding:1px 4px;border-radius:3px;font-family:ui-monospace,monospace;font-size:0.9em">$1</code>').replace(/\*\*([^*\n]+)\*\*/g,"<strong>$1</strong>")}function w(i,o){let c=o.apiBase.replace(/\/$/,""),a=document.createElement("div");a.className="coding-tab",i.innerHTML="",i.appendChild(a);let m=!1;function M(){if(m||(m=!0,document.querySelector('link[data-coding-tab="style"]')))return;let e=document.createElement("link");e.rel="stylesheet",e.href=`${c}/style.css`,e.dataset.codingTab="style",document.head.appendChild(e)}M();let t={me:null,models:[],mode:o.defaultMode??"plan",model:o.defaultModel??"sonnet",repoUrl:o.defaultRepo??"",sessionId:null,activeRunId:null,turns:[],isStreaming:!1},p=null;async function y(e,n){let s=await fetch(`${c}${e}`,{credentials:"include",headers:{"Content-Type":"application/json",...n?.headers??{}},...n});if(!s.ok)throw Object.assign(new Error(`${s.status} ${s.statusText}`),{status:s.status});return await s.json()}function l(){if(!t.me){a.innerHTML=`
2
+ <div class="coding-tab__signin">
3
+ <h2>Coding Tab</h2>
4
+ <p>Sign in with GitHub to start a coding session against your repo. Your token is used to clone, push, and open pull requests on your behalf.</p>
5
+ <a href="${c}/auth/login">${D}<span>Sign in with GitHub</span></a>
6
+ </div>
7
+ `;return}let e=`
8
+ <div class="coding-tab__header">
9
+ <select data-role="model">
10
+ ${t.models.map(u=>`<option value="${u.choice}" ${t.model===u.choice?"selected":""}>${g(u.displayName)}</option>`).join("")}
11
+ </select>
12
+ <div class="coding-tab__mode">
13
+ <button data-mode="plan" class="${t.mode==="plan"?"is-active":""}">Plan</button>
14
+ <button data-mode="agent" class="${t.mode==="agent"?"is-active":""}">Agent</button>
15
+ </div>
16
+ <input data-role="repo" placeholder="https://github.com/org/repo" value="${g(t.repoUrl)}" style="flex:1;min-width:200px" />
17
+ <div class="coding-tab__user">
18
+ ${t.me.avatarUrl?`<img src="${t.me.avatarUrl}" alt="" />`:""}
19
+ <span>@${g(t.me.githubLogin)}</span>
20
+ <button data-role="logout">Sign out</button>
21
+ </div>
22
+ </div>
23
+ `,n=t.turns.length===0?'<div class="coding-tab__notice info">Pick a mode (Plan or Agent), choose a model, and type a request below to get started.</div>':t.turns.map(x).join(""),s=t.mode==="agent",r=t.turns.length===0?'Ask anything (e.g. "add dark mode that follows the OS setting")':t.mode==="plan"?"Refine the plan, or ask another planning question":"Send another instruction";a.innerHTML=`
24
+ ${e}
25
+ <div class="coding-tab__thread" data-role="thread">${n}</div>
26
+ <div class="coding-tab__composer">
27
+ <textarea data-role="prompt" placeholder="${r}" rows="2"></textarea>
28
+ <button class="coding-tab__btn primary" data-role="send" ${t.isStreaming?"disabled":""}>${t.isStreaming?"Working\u2026":"Send"}</button>
29
+ ${t.isStreaming?'<button class="coding-tab__btn danger" data-role="cancel">Stop</button>':""}
30
+ </div>
31
+ `;let d=a.querySelector("[data-role=thread]");d&&(d.scrollTop=d.scrollHeight),E()}function x(e){if(e.role==="user")return`<div class="coding-tab__msg user">
32
+ <div class="coding-tab__msg-role">You \xB7 ${e.isPlan?"Plan":"Agent"}</div>
33
+ <div class="coding-tab__msg-body">${g(e.text)}</div>
34
+ </div>`;let n=e.tools.length===0?"":`
35
+ <div class="coding-tab__msg-tools">
36
+ ${e.tools.map(u=>`<div class="coding-tab__tool" data-status="${u.status}"><span class="dot"></span><span>${g(u.name)}</span></div>`).join("")}
37
+ </div>`,s=e.status&&e.status!=="finished"?`<div class="coding-tab__status">${g(e.status)}</div>`:"",r=e.showExecute&&!t.isStreaming?`
38
+ <div class="coding-tab__plan-actions">
39
+ <button class="coding-tab__btn primary" data-role="execute" data-turn="${e.id}">Execute plan</button>
40
+ </div>`:"",d=e.pr?k(e.pr):"";return`<div class="coding-tab__msg assistant">
41
+ <div class="coding-tab__msg-role">Coding Tab \xB7 ${e.isPlan?"Plan":"Agent"} ${s}</div>
42
+ <div class="coding-tab__msg-body">${J(e.text||(e.tools.length>0?"":"\u2026"))}${n}${r}${d}</div>
43
+ </div>`}function k(e){return`<div class="coding-tab__pr">
44
+ <div class="coding-tab__pr-title">PR #${e.number} opened in ${g(e.owner)}/${g(e.repo)}</div>
45
+ ${e.title?`<div>${g(e.title)}</div>`:""}
46
+ <div style="display:flex;gap:8px;flex-wrap:wrap">
47
+ <a class="coding-tab__btn" href="${e.url}" target="_blank" rel="noreferrer">Open in GitHub</a>
48
+ <button class="coding-tab__btn primary" data-role="merge" data-pr="${e.url}">Merge & Redeploy</button>
49
+ </div>
50
+ </div>`}function E(){a.querySelector('[data-role="model"]')?.addEventListener("change",e=>{t.model=e.target.value}),a.querySelectorAll("[data-mode]").forEach(e=>{e.addEventListener("click",()=>{t.mode=e.dataset.mode,l()})}),a.querySelector('[data-role="repo"]')?.addEventListener("change",e=>{t.repoUrl=e.target.value.trim()}),a.querySelector('[data-role="logout"]')?.addEventListener("click",async()=>{await fetch(`${c}/auth/logout`,{method:"POST",credentials:"include"}).catch(()=>{}),t.me=null,t.sessionId=null,t.turns=[],l()}),a.querySelector('[data-role="send"]')?.addEventListener("click",()=>$()),a.querySelector('[data-role="prompt"]')?.addEventListener("keydown",e=>{let n=e;n.key==="Enter"&&(n.metaKey||n.ctrlKey)&&(n.preventDefault(),$())}),a.querySelector('[data-role="cancel"]')?.addEventListener("click",()=>C()),a.querySelectorAll('[data-role="execute"]').forEach(e=>e.addEventListener("click",()=>I())),a.querySelectorAll('[data-role="merge"]').forEach(e=>{e.addEventListener("click",()=>P(e.dataset.pr))})}async function $(){let e=a.querySelector('[data-role="prompt"]'),n=e?.value.trim()??"";if(!n||t.isStreaming)return;e&&(e.value="");let s={id:h(),role:"user",text:n,tools:[],isPlan:t.mode==="plan"};t.turns.push(s);let r={id:h(),role:"assistant",text:"",tools:[],isPlan:t.mode==="plan",status:"running"};t.turns.push(r),t.isStreaming=!0,l();try{let d=!t.sessionId,u=d?"/agent/start":"/agent/send",f=d?{prompt:n,mode:t.mode,model:t.model,repoUrl:t.repoUrl||void 0,startingRef:o.defaultRef}:{prompt:n,mode:t.mode,sessionId:t.sessionId};await S(u,f,r)}catch(d){r.text+=`
51
+
52
+ [error] ${d instanceof Error?d.message:String(d)}`,r.status="error"}finally{t.isStreaming=!1,p=null,l()}}async function I(){if(!t.sessionId||t.isStreaming)return;let e={id:h(),role:"assistant",text:"",tools:[],isPlan:!1,status:"running"};t.turns.push(e),t.isStreaming=!0,l();try{await S("/agent/execute",{sessionId:t.sessionId},e)}catch(n){e.text+=`
53
+
54
+ [error] ${n instanceof Error?n.message:String(n)}`,e.status="error"}finally{t.isStreaming=!1,p=null,l()}}async function C(){if(!t.sessionId||!t.activeRunId){p?.abort();return}try{await y("/agent/cancel",{method:"POST",body:JSON.stringify({sessionId:t.sessionId,runId:t.activeRunId})})}catch(e){console.error("[coding-tab] cancel failed",e)}p?.abort()}async function P(e){try{let n=await y("/pr/merge",{method:"POST",body:JSON.stringify({prUrl:e,mergeMethod:"squash"})}),s={id:h(),role:"assistant",text:n.merged?`Merged commit \`${n.sha.slice(0,7)}\` into the default branch. Railway should redeploy on the next push hook.`:"Merge call returned, but `merged=false`. Check the PR on GitHub.",tools:[],isPlan:!1,status:n.merged?"finished":"error"};t.turns.push(s)}catch(n){let s={id:h(),role:"assistant",text:`[merge failed] ${n instanceof Error?n.message:String(n)}`,tools:[],isPlan:!1,status:"error"};t.turns.push(s)}finally{l()}}async function S(e,n,s){p=new AbortController;let r=await fetch(`${c}${e}`,{method:"POST",credentials:"include",headers:{"Content-Type":"application/json",Accept:"text/event-stream"},body:JSON.stringify(n),signal:p.signal});if(!r.ok||!r.body)throw new Error(`${r.status} ${r.statusText}`);let d=r.body.getReader(),u=new TextDecoder,f="";for(;;){let{value:L,done:q}=await d.read();if(q)break;f+=u.decode(L,{stream:!0});let _=f.split(`
55
+
56
+ `);f=_.pop()??"";for(let O of _){let T=O.split(`
57
+ `).find(b=>b.startsWith("data: "));if(T)try{let b=JSON.parse(T.slice(6));H(b,s),l()}catch(b){console.warn("[coding-tab] bad sse event",b)}}}}function H(e,n){switch(e.kind){case"ready":e.sessionId&&(t.sessionId=e.sessionId),t.activeRunId=e.runId;break;case"text":n.text+=e.text;break;case"thinking":break;case"tool":{let s=n.tools.find(r=>r.id===e.callId);s?s.status=e.status:n.tools.push({id:e.callId,name:e.name,status:e.status});break}case"status":break;case"result":n.status=e.status,e.pr&&(n.pr=e.pr),n.isPlan&&e.status==="finished"&&(n.showExecute=!0),t.activeRunId=null;break;case"error":n.text+=`
58
+
59
+ [error] ${e.message}`,n.status="error",t.activeRunId=null;break}}function h(){return typeof crypto<"u"&&"randomUUID"in crypto?crypto.randomUUID():Math.random().toString(36).slice(2)}async function R(){try{let e=await y("/auth/me");t.me=e}catch(e){let n=e.status;if(n===401||n===403){t.me=null,l();return}console.error("[coding-tab] /auth/me failed",e),t.me=null,l();return}try{let{models:e}=await y("/models");t.models=e,e.length>0&&!e.find(n=>n.choice===t.model)&&(t.model=e[0].choice)}catch(e){console.error("[coding-tab] /models failed",e),t.models=[{choice:"sonnet",cursorModelId:"auto",displayName:"Sonnet (fallback)"},{choice:"opus",cursorModelId:"auto",displayName:"Opus (fallback)"}]}l()}return R(),{destroy(){p?.abort(),i.removeChild(a)}}}typeof window<"u"&&(window.CodingTab={mountCodingTab:w});return G(K);})();
60
+ //# sourceMappingURL=browser.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/client/tab.ts"],"sourcesContent":["import type {\n ChatMode,\n MeResponse,\n ModelChoice,\n ModelOption,\n PrInfo,\n StreamEvent,\n} from \"../shared/types.js\";\n\ninterface MountOptions {\n apiBase: string;\n defaultRepo?: string;\n defaultRef?: string;\n defaultMode?: ChatMode;\n defaultModel?: ModelChoice;\n}\n\ninterface MountHandle {\n destroy(): void;\n}\n\ninterface ChatTurn {\n id: string;\n role: \"user\" | \"assistant\";\n text: string;\n tools: Array<{ id: string; name: string; status: string }>;\n pr?: PrInfo;\n isPlan: boolean;\n status?: \"running\" | \"finished\" | \"error\" | \"cancelled\";\n showExecute?: boolean;\n}\n\nconst ICON_GITHUB = `<svg width=\"18\" height=\"18\" viewBox=\"0 0 16 16\" fill=\"currentColor\"><path d=\"M8 0C3.58 0 0 3.58 0 8a8 8 0 005.47 7.59c.4.07.55-.17.55-.38v-1.34c-2.23.48-2.7-1.07-2.7-1.07-.36-.92-.89-1.16-.89-1.16-.73-.5.05-.49.05-.49.81.06 1.23.83 1.23.83.72 1.23 1.88.87 2.34.66.07-.52.28-.87.5-1.07-1.78-.2-3.65-.89-3.65-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.13 0 0 .67-.21 2.2.82a7.65 7.65 0 014 0c1.53-1.04 2.2-.82 2.2-.82.44 1.11.16 1.93.08 2.13.51.56.82 1.28.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.74.54 1.49v2.21c0 .21.15.46.55.38A8 8 0 0016 8c0-4.42-3.58-8-8-8z\"/></svg>`;\n\nfunction escapeHtml(s: string): string {\n return s.replace(/[&<>\"']/g, (c) => ({ \"&\": \"&amp;\", \"<\": \"&lt;\", \">\": \"&gt;\", '\"': \"&quot;\", \"'\": \"&#39;\" }[c] as string));\n}\n\nfunction renderMarkdownLite(text: string): string {\n const escaped = escapeHtml(text);\n return escaped\n .replace(/`([^`\\n]+)`/g, '<code style=\"background:rgba(255,255,255,0.06);padding:1px 4px;border-radius:3px;font-family:ui-monospace,monospace;font-size:0.9em\">$1</code>')\n .replace(/\\*\\*([^*\\n]+)\\*\\*/g, \"<strong>$1</strong>\");\n}\n\nexport function mountCodingTab(el: HTMLElement, options: MountOptions): MountHandle {\n const apiBase = options.apiBase.replace(/\\/$/, \"\");\n const root = document.createElement(\"div\");\n root.className = \"coding-tab\";\n el.innerHTML = \"\";\n el.appendChild(root);\n\n let stylesheetInjected = false;\n function injectStylesheet() {\n if (stylesheetInjected) return;\n stylesheetInjected = true;\n if (document.querySelector(`link[data-coding-tab=\"style\"]`)) return;\n const link = document.createElement(\"link\");\n link.rel = \"stylesheet\";\n link.href = `${apiBase}/style.css`;\n link.dataset.codingTab = \"style\";\n document.head.appendChild(link);\n }\n injectStylesheet();\n\n const state: {\n me: MeResponse | null;\n models: ModelOption[];\n mode: ChatMode;\n model: ModelChoice;\n repoUrl: string;\n sessionId: string | null;\n activeRunId: string | null;\n turns: ChatTurn[];\n isStreaming: boolean;\n } = {\n me: null,\n models: [],\n mode: options.defaultMode ?? \"plan\",\n model: options.defaultModel ?? \"sonnet\",\n repoUrl: options.defaultRepo ?? \"\",\n sessionId: null,\n activeRunId: null,\n turns: [],\n isStreaming: false,\n };\n\n let activeAbort: AbortController | null = null;\n\n async function api<T = unknown>(path: string, init?: RequestInit): Promise<T> {\n const resp = await fetch(`${apiBase}${path}`, {\n credentials: \"include\",\n headers: { \"Content-Type\": \"application/json\", ...(init?.headers ?? {}) },\n ...init,\n });\n if (!resp.ok) throw Object.assign(new Error(`${resp.status} ${resp.statusText}`), { status: resp.status });\n return (await resp.json()) as T;\n }\n\n function render() {\n if (!state.me) {\n root.innerHTML = `\n <div class=\"coding-tab__signin\">\n <h2>Coding Tab</h2>\n <p>Sign in with GitHub to start a coding session against your repo. Your token is used to clone, push, and open pull requests on your behalf.</p>\n <a href=\"${apiBase}/auth/login\">${ICON_GITHUB}<span>Sign in with GitHub</span></a>\n </div>\n `;\n return;\n }\n\n const headerHtml = `\n <div class=\"coding-tab__header\">\n <select data-role=\"model\">\n ${state.models.map((m) => `<option value=\"${m.choice}\" ${state.model === m.choice ? \"selected\" : \"\"}>${escapeHtml(m.displayName)}</option>`).join(\"\")}\n </select>\n <div class=\"coding-tab__mode\">\n <button data-mode=\"plan\" class=\"${state.mode === \"plan\" ? \"is-active\" : \"\"}\">Plan</button>\n <button data-mode=\"agent\" class=\"${state.mode === \"agent\" ? \"is-active\" : \"\"}\">Agent</button>\n </div>\n <input data-role=\"repo\" placeholder=\"https://github.com/org/repo\" value=\"${escapeHtml(state.repoUrl)}\" style=\"flex:1;min-width:200px\" />\n <div class=\"coding-tab__user\">\n ${state.me.avatarUrl ? `<img src=\"${state.me.avatarUrl}\" alt=\"\" />` : \"\"}\n <span>@${escapeHtml(state.me.githubLogin)}</span>\n <button data-role=\"logout\">Sign out</button>\n </div>\n </div>\n `;\n\n const threadHtml = state.turns.length === 0\n ? `<div class=\"coding-tab__notice info\">Pick a mode (Plan or Agent), choose a model, and type a request below to get started.</div>`\n : state.turns.map(renderTurn).join(\"\");\n\n const isAgentMode = state.mode === \"agent\";\n const placeholder = state.turns.length === 0\n ? `Ask anything (e.g. \"add dark mode that follows the OS setting\")`\n : state.mode === \"plan\" ? \"Refine the plan, or ask another planning question\" : \"Send another instruction\";\n\n root.innerHTML = `\n ${headerHtml}\n <div class=\"coding-tab__thread\" data-role=\"thread\">${threadHtml}</div>\n <div class=\"coding-tab__composer\">\n <textarea data-role=\"prompt\" placeholder=\"${placeholder}\" rows=\"2\"></textarea>\n <button class=\"coding-tab__btn primary\" data-role=\"send\" ${state.isStreaming ? \"disabled\" : \"\"}>${state.isStreaming ? \"Working…\" : \"Send\"}</button>\n ${state.isStreaming ? `<button class=\"coding-tab__btn danger\" data-role=\"cancel\">Stop</button>` : \"\"}\n </div>\n `;\n\n const thread = root.querySelector(\"[data-role=thread]\") as HTMLElement | null;\n if (thread) thread.scrollTop = thread.scrollHeight;\n\n bindEvents();\n }\n\n function renderTurn(turn: ChatTurn): string {\n if (turn.role === \"user\") {\n return `<div class=\"coding-tab__msg user\">\n <div class=\"coding-tab__msg-role\">You · ${turn.isPlan ? \"Plan\" : \"Agent\"}</div>\n <div class=\"coding-tab__msg-body\">${escapeHtml(turn.text)}</div>\n </div>`;\n }\n const toolsHtml = turn.tools.length === 0 ? \"\" : `\n <div class=\"coding-tab__msg-tools\">\n ${turn.tools.map((t) => `<div class=\"coding-tab__tool\" data-status=\"${t.status}\"><span class=\"dot\"></span><span>${escapeHtml(t.name)}</span></div>`).join(\"\")}\n </div>`;\n const statusBadge = turn.status && turn.status !== \"finished\"\n ? `<div class=\"coding-tab__status\">${escapeHtml(turn.status)}</div>` : \"\";\n const planActions = turn.showExecute && !state.isStreaming ? `\n <div class=\"coding-tab__plan-actions\">\n <button class=\"coding-tab__btn primary\" data-role=\"execute\" data-turn=\"${turn.id}\">Execute plan</button>\n </div>` : \"\";\n const prHtml = turn.pr ? renderPr(turn.pr) : \"\";\n return `<div class=\"coding-tab__msg assistant\">\n <div class=\"coding-tab__msg-role\">Coding Tab · ${turn.isPlan ? \"Plan\" : \"Agent\"} ${statusBadge}</div>\n <div class=\"coding-tab__msg-body\">${renderMarkdownLite(turn.text || (turn.tools.length > 0 ? \"\" : \"…\"))}${toolsHtml}${planActions}${prHtml}</div>\n </div>`;\n }\n\n function renderPr(pr: PrInfo): string {\n return `<div class=\"coding-tab__pr\">\n <div class=\"coding-tab__pr-title\">PR #${pr.number} opened in ${escapeHtml(pr.owner)}/${escapeHtml(pr.repo)}</div>\n ${pr.title ? `<div>${escapeHtml(pr.title)}</div>` : \"\"}\n <div style=\"display:flex;gap:8px;flex-wrap:wrap\">\n <a class=\"coding-tab__btn\" href=\"${pr.url}\" target=\"_blank\" rel=\"noreferrer\">Open in GitHub</a>\n <button class=\"coding-tab__btn primary\" data-role=\"merge\" data-pr=\"${pr.url}\">Merge & Redeploy</button>\n </div>\n </div>`;\n }\n\n function bindEvents() {\n root.querySelector(`[data-role=\"model\"]`)?.addEventListener(\"change\", (e) => {\n state.model = (e.target as HTMLSelectElement).value as ModelChoice;\n });\n root.querySelectorAll(`[data-mode]`).forEach((b) => {\n b.addEventListener(\"click\", () => {\n state.mode = (b as HTMLElement).dataset.mode as ChatMode;\n render();\n });\n });\n root.querySelector(`[data-role=\"repo\"]`)?.addEventListener(\"change\", (e) => {\n state.repoUrl = (e.target as HTMLInputElement).value.trim();\n });\n root.querySelector(`[data-role=\"logout\"]`)?.addEventListener(\"click\", async () => {\n await fetch(`${apiBase}/auth/logout`, { method: \"POST\", credentials: \"include\" }).catch(() => {});\n state.me = null;\n state.sessionId = null;\n state.turns = [];\n render();\n });\n root.querySelector(`[data-role=\"send\"]`)?.addEventListener(\"click\", () => onSend());\n root.querySelector(`[data-role=\"prompt\"]`)?.addEventListener(\"keydown\", (e: Event) => {\n const ev = e as KeyboardEvent;\n if (ev.key === \"Enter\" && (ev.metaKey || ev.ctrlKey)) {\n ev.preventDefault();\n onSend();\n }\n });\n root.querySelector(`[data-role=\"cancel\"]`)?.addEventListener(\"click\", () => onCancel());\n root.querySelectorAll(`[data-role=\"execute\"]`).forEach((b) =>\n b.addEventListener(\"click\", () => onExecute()),\n );\n root.querySelectorAll(`[data-role=\"merge\"]`).forEach((b) => {\n b.addEventListener(\"click\", () => onMerge((b as HTMLElement).dataset.pr!));\n });\n }\n\n async function onSend() {\n const promptEl = root.querySelector(`[data-role=\"prompt\"]`) as HTMLTextAreaElement | null;\n const prompt = promptEl?.value.trim() ?? \"\";\n if (!prompt || state.isStreaming) return;\n if (promptEl) promptEl.value = \"\";\n\n const userTurn: ChatTurn = {\n id: cryptoRandomId(),\n role: \"user\",\n text: prompt,\n tools: [],\n isPlan: state.mode === \"plan\",\n };\n state.turns.push(userTurn);\n\n const assistantTurn: ChatTurn = {\n id: cryptoRandomId(),\n role: \"assistant\",\n text: \"\",\n tools: [],\n isPlan: state.mode === \"plan\",\n status: \"running\",\n };\n state.turns.push(assistantTurn);\n state.isStreaming = true;\n render();\n\n try {\n const isFirst = !state.sessionId;\n const path = isFirst ? \"/agent/start\" : \"/agent/send\";\n const body = isFirst\n ? { prompt, mode: state.mode, model: state.model, repoUrl: state.repoUrl || undefined, startingRef: options.defaultRef }\n : { prompt, mode: state.mode, sessionId: state.sessionId };\n await streamSse(path, body, assistantTurn);\n } catch (err) {\n assistantTurn.text += `\\n\\n[error] ${err instanceof Error ? err.message : String(err)}`;\n assistantTurn.status = \"error\";\n } finally {\n state.isStreaming = false;\n activeAbort = null;\n render();\n }\n }\n\n async function onExecute() {\n if (!state.sessionId || state.isStreaming) return;\n const turn: ChatTurn = {\n id: cryptoRandomId(),\n role: \"assistant\",\n text: \"\",\n tools: [],\n isPlan: false,\n status: \"running\",\n };\n state.turns.push(turn);\n state.isStreaming = true;\n render();\n try {\n await streamSse(\"/agent/execute\", { sessionId: state.sessionId }, turn);\n } catch (err) {\n turn.text += `\\n\\n[error] ${err instanceof Error ? err.message : String(err)}`;\n turn.status = \"error\";\n } finally {\n state.isStreaming = false;\n activeAbort = null;\n render();\n }\n }\n\n async function onCancel() {\n if (!state.sessionId || !state.activeRunId) {\n activeAbort?.abort();\n return;\n }\n try {\n await api(\"/agent/cancel\", {\n method: \"POST\",\n body: JSON.stringify({ sessionId: state.sessionId, runId: state.activeRunId }),\n });\n } catch (err) {\n console.error(\"[coding-tab] cancel failed\", err);\n }\n activeAbort?.abort();\n }\n\n async function onMerge(prUrl: string) {\n try {\n const result = await api<{ sha: string; merged: boolean }>(\"/pr/merge\", {\n method: \"POST\",\n body: JSON.stringify({ prUrl, mergeMethod: \"squash\" }),\n });\n const turn: ChatTurn = {\n id: cryptoRandomId(),\n role: \"assistant\",\n text: result.merged\n ? `Merged commit \\`${result.sha.slice(0, 7)}\\` into the default branch. Railway should redeploy on the next push hook.`\n : `Merge call returned, but \\`merged=false\\`. Check the PR on GitHub.`,\n tools: [],\n isPlan: false,\n status: result.merged ? \"finished\" : \"error\",\n };\n state.turns.push(turn);\n } catch (err) {\n const turn: ChatTurn = {\n id: cryptoRandomId(),\n role: \"assistant\",\n text: `[merge failed] ${err instanceof Error ? err.message : String(err)}`,\n tools: [],\n isPlan: false,\n status: \"error\",\n };\n state.turns.push(turn);\n } finally {\n render();\n }\n }\n\n async function streamSse(path: string, body: unknown, turn: ChatTurn): Promise<void> {\n activeAbort = new AbortController();\n const resp = await fetch(`${apiBase}${path}`, {\n method: \"POST\",\n credentials: \"include\",\n headers: { \"Content-Type\": \"application/json\", Accept: \"text/event-stream\" },\n body: JSON.stringify(body),\n signal: activeAbort.signal,\n });\n if (!resp.ok || !resp.body) {\n throw new Error(`${resp.status} ${resp.statusText}`);\n }\n const reader = resp.body.getReader();\n const decoder = new TextDecoder();\n let buffer = \"\";\n\n while (true) {\n const { value, done } = await reader.read();\n if (done) break;\n buffer += decoder.decode(value, { stream: true });\n const events = buffer.split(\"\\n\\n\");\n buffer = events.pop() ?? \"\";\n for (const ev of events) {\n const dataLine = ev.split(\"\\n\").find((l) => l.startsWith(\"data: \"));\n if (!dataLine) continue;\n try {\n const evt = JSON.parse(dataLine.slice(6)) as StreamEvent;\n handleStream(evt, turn);\n render();\n } catch (e) {\n console.warn(\"[coding-tab] bad sse event\", e);\n }\n }\n }\n }\n\n function handleStream(evt: StreamEvent, turn: ChatTurn) {\n switch (evt.kind) {\n case \"ready\":\n if (evt.sessionId) state.sessionId = evt.sessionId;\n state.activeRunId = evt.runId;\n break;\n case \"text\":\n turn.text += evt.text;\n break;\n case \"thinking\":\n break;\n case \"tool\": {\n const existing = turn.tools.find((t) => t.id === evt.callId);\n if (existing) {\n existing.status = evt.status;\n } else {\n turn.tools.push({ id: evt.callId, name: evt.name, status: evt.status });\n }\n break;\n }\n case \"status\":\n break;\n case \"result\":\n turn.status = evt.status;\n if (evt.pr) turn.pr = evt.pr;\n if (turn.isPlan && evt.status === \"finished\") turn.showExecute = true;\n state.activeRunId = null;\n break;\n case \"error\":\n turn.text += `\\n\\n[error] ${evt.message}`;\n turn.status = \"error\";\n state.activeRunId = null;\n break;\n }\n }\n\n function cryptoRandomId(): string {\n return (typeof crypto !== \"undefined\" && \"randomUUID\" in crypto)\n ? crypto.randomUUID()\n : Math.random().toString(36).slice(2);\n }\n\n async function bootstrap() {\n try {\n const me = await api<MeResponse>(\"/auth/me\");\n state.me = me;\n } catch (err) {\n const status = (err as { status?: number }).status;\n if (status === 401 || status === 403) {\n state.me = null;\n render();\n return;\n }\n console.error(\"[coding-tab] /auth/me failed\", err);\n state.me = null;\n render();\n return;\n }\n try {\n const { models } = await api<{ models: ModelOption[] }>(\"/models\");\n state.models = models;\n if (models.length > 0 && !models.find((m) => m.choice === state.model)) {\n state.model = models[0]!.choice;\n }\n } catch (err) {\n console.error(\"[coding-tab] /models failed\", err);\n state.models = [\n { choice: \"sonnet\", cursorModelId: \"auto\", displayName: \"Sonnet (fallback)\" },\n { choice: \"opus\", cursorModelId: \"auto\", displayName: \"Opus (fallback)\" },\n ];\n }\n render();\n }\n\n bootstrap();\n\n return {\n destroy() {\n activeAbort?.abort();\n el.removeChild(root);\n },\n };\n}\n\nif (typeof window !== \"undefined\") {\n (window as unknown as { CodingTab?: { mountCodingTab: typeof mountCodingTab } }).CodingTab = { mountCodingTab };\n}\n"],"mappings":"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","names":["tab_exports","__export","mountCodingTab","ICON_GITHUB","escapeHtml","s","c","renderMarkdownLite","text","el","options","apiBase","root","stylesheetInjected","injectStylesheet","link","state","activeAbort","api","path","init","resp","render","headerHtml","m","threadHtml","renderTurn","isAgentMode","placeholder","thread","bindEvents","turn","toolsHtml","t","statusBadge","planActions","prHtml","renderPr","pr","b","onSend","ev","onCancel","onExecute","onMerge","promptEl","prompt","userTurn","cryptoRandomId","assistantTurn","isFirst","body","streamSse","err","prUrl","result","reader","decoder","buffer","value","done","events","dataLine","l","evt","handleStream","e","existing","bootstrap","me","status","models"]}