@tstax/coding-tab 0.1.2 → 0.2.1

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 CHANGED
@@ -1,9 +1,17 @@
1
1
  # @tstax/coding-tab
2
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.
3
+ Drop a Cursor-style coding agent tab into any Express app. Sign in with GitHub, run **multiple persistent chats** in **Plan** or **Agent** mode, inspect every tool call inline, then **Merge & Redeploy** to land a PR — and let your existing GitHub-to-Railway (or Vercel, or Render) auto-deploy ship the change.
4
4
 
5
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
6
 
7
+ ## What's new in 0.2
8
+
9
+ - **Multiple chats at once.** A left sidebar lists all your chats, each with its own conversation context. Switch between them at any time, even while one is streaming.
10
+ - **Full persistence.** Chat metadata, every turn, and every tool call (with args + result) is stored on disk. Refresh the tab and your history is right where you left it. Plug in your own database via the `storage` option.
11
+ - **Plan rendered as preview.** Plan-mode answers render as proper markdown — headings, lists, code blocks, links — instead of raw text.
12
+ - **Chronological timeline.** Agent text and tool invocations are interleaved in the order they actually happened, with a clear paragraph break between each thought.
13
+ - **Click into any tool call.** Every `grep_search`, `read_file`, `task`, `shell`, etc. is a collapsible row showing the full args and the full result.
14
+
7
15
  ## Install
8
16
 
9
17
  ```bash
@@ -18,6 +26,7 @@ npm install @tstax/coding-tab
18
26
  - Authorization callback URL: `https://<your-app>/coding-tab/auth/callback`
19
27
  - Copy the Client ID, generate a Client Secret.
20
28
  3. **Node 20+** running an Express server.
29
+ 4. **A writable directory** for the default file store, OR your own `ChatStorage` adapter (see [Persistence](#persistence)).
21
30
 
22
31
  ## Mount it on your server
23
32
 
@@ -37,6 +46,8 @@ mountCodingTab(app, {
37
46
  },
38
47
  sessionPassword: process.env.SESSION_SECRET!, // 32+ chars: openssl rand -hex 32
39
48
  defaultRepo: { url: "https://github.com/youruser/yourrepo" },
49
+ // Optional — defaults to `<cwd>/.coding-tab-data`
50
+ dataDir: process.env.CODING_TAB_DATA_DIR,
40
51
  });
41
52
 
42
53
  app.listen(3000);
@@ -64,6 +75,42 @@ That's it on the server. Visit `https://<your-app>/coding-tab/` and the tab is f
64
75
  | `ALLOWED_GITHUB_LOGIN` | Comma-separated GitHub usernames allowed to sign in |
65
76
  | `PUBLIC_BASE_URL` | Your app's external URL, used to build the OAuth callback |
66
77
 
78
+ ## Persistence
79
+
80
+ Each chat is keyed to a GitHub login and contains a chronological list of turns; each assistant turn is a chronological list of `TimelineEvent`s (text paragraphs and tool invocations).
81
+
82
+ By default, this is stored as JSON files under `dataDir` (`<cwd>/.coding-tab-data` if you don't set it):
83
+
84
+ ```
85
+ <dataDir>/
86
+ chats/<chatId>.json # full chat: metadata + every turn + every tool call
87
+ index/<login>.json # cached listing per user (rebuilt on demand if missing)
88
+ ```
89
+
90
+ ### Pluggable storage
91
+
92
+ The default file store is fine for a single-process app on a host with a real (or mounted) filesystem. For ephemeral hosts (Railway free tier, Vercel, etc.) or multi-instance deployments, plug in your own:
93
+
94
+ ```ts
95
+ import type { ChatStorage } from "@tstax/coding-tab/server";
96
+
97
+ const supabaseStorage: ChatStorage = {
98
+ async listChats(login) { /* SELECT id, title, ... FROM chats WHERE login = $1 */ },
99
+ async loadChat(id, login) { /* … */ },
100
+ async createChat(chat) { /* … */ },
101
+ async patchChat(id, login, patch) { /* … */ },
102
+ async appendTurn(turn) { /* … */ },
103
+ async patchTurn(chatId, turnId, patch) { /* … */ },
104
+ async deleteChat(id, login) { /* … */ },
105
+ };
106
+
107
+ mountCodingTab(app, { ...config, storage: supabaseStorage });
108
+ ```
109
+
110
+ ### Resuming agents across restarts
111
+
112
+ Each chat persists the `agentId` of the cloud agent it last spoke to. When the user sends another message in that chat, the server calls `Agent.resume(agentId)` so the model picks up the same conversation. If the cloud agent has been garbage-collected, we fall back to `Agent.create()`; the persisted turn history still displays.
113
+
67
114
  ## Repo selection
68
115
 
69
116
  The repo the agent works against is whatever you pass in `defaultRepo.url` server-side. The client widget reads this from `/auth/me` and locks the URL field to that repo (rendered as a clickable `org/repo` pill in the header), so users can't accidentally point the agent at a different codebase. To change the repo, change the server config — there's intentionally no UI override.
@@ -72,13 +119,14 @@ The repo the agent works against is whatever you pass in `defaultRepo.url` serve
72
119
 
73
120
  - **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.
74
121
  - **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.
122
+ - **Per-user chat scoping.** Every persistence call is scoped to the signed-in user's GitHub login; one user can never read or mutate another user's chats.
75
123
  - **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.
76
124
  - **CSRF-protected handshake.** OAuth uses a per-request `state` cookie that expires in 10 minutes.
77
125
  - **Cookie hardening.** `HttpOnly`, `SameSite=Lax`, `Secure` in production.
78
126
 
79
127
  ## Plan vs Agent mode
80
128
 
81
- - **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.
129
+ - **Plan** — agent runs read-only, produces a markdown plan ending in `PLAN READY`. The plan renders as a proper preview (headings, lists, code, links). Click **Execute plan** to follow up with code changes in the same conversation.
82
130
  - **Agent** — agent goes straight to making changes and opens a PR (`autoCreatePR: true`).
83
131
 
84
132
  The conversation context is preserved across plan and execute turns, just like Cursor chat.
@@ -98,19 +146,24 @@ Public:
98
146
  - `GET /auth/me` — 401 if not signed in, otherwise `{ githubLogin, avatarUrl?, defaultRepoUrl?, defaultRepoRef? }`
99
147
 
100
148
  Authenticated (require valid session):
149
+ - `GET /chats` — list current user's chats
150
+ - `POST /chats` — create a new chat (returns `{ chat }`)
151
+ - `GET /chats/:id` — full chat including all turns
152
+ - `PATCH /chats/:id` — rename / change mode or model
153
+ - `DELETE /chats/:id` — delete chat + dispose any in-memory agent
101
154
  - `GET /models` — discovered Sonnet/Opus IDs
102
- - `POST /agent/start` — SSE stream; creates a new agent + first run
103
- - `POST /agent/send` — SSE stream; follow-up turn on existing session
155
+ - `POST /agent/start` — alias of `/agent/send`
156
+ - `POST /agent/send` — SSE stream; sends a turn into the given chat (creates or resumes the cloud agent)
104
157
  - `POST /agent/execute` — SSE stream; turns the most recent plan into changes
105
- - `POST /agent/cancel` — cancel an in-flight run
106
- - `POST /agent/dispose` — release an agent session
158
+ - `POST /agent/cancel` — cancel an in-flight run for a chat
159
+ - `POST /agent/dispose` — release the in-memory agent for a chat (does not delete history)
107
160
  - `GET /pr/status?prUrl=...` — PR metadata + mergeability
108
161
  - `POST /pr/merge` — merge a PR
109
162
 
110
163
  ## Development
111
164
 
112
165
  ```bash
113
- git clone https://github.com/tylerackerman/coding-tab
166
+ git clone https://github.com/tstax/coding-tab
114
167
  cd coding-tab
115
168
  npm install
116
169
  npm run dev # tsup --watch
package/dist/browser.js CHANGED
@@ -1,60 +1,103 @@
1
- "use strict";var CodingTab=(()=>{var v=Object.defineProperty;var j=Object.getOwnPropertyDescriptor;var N=Object.getOwnPropertyNames;var B=Object.prototype.hasOwnProperty;var G=(i,o)=>{for(var u in o)v(i,u,{get:o[u],enumerable:!0})},D=(i,o,u,a)=>{if(o&&typeof o=="object"||typeof o=="function")for(let p of N(o))!B.call(i,p)&&p!==u&&v(i,p,{get:()=>o[p],enumerable:!(a=j(o,p))||a.enumerable});return i};var J=i=>D(v({},"__esModule",{value:!0}),i);var z={};G(z,{mountCodingTab:()=>w});var T='<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 d(i){return i.replace(/[&<>"']/g,o=>({"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;","'":"&#39;"})[o])}function K(i){return d(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 u=o.apiBase.replace(/\/$/,""),a=document.createElement("div");a.className="coding-tab",i.innerHTML="",i.appendChild(a);let p=!1;function M(){if(p||(p=!0,document.querySelector('link[data-coding-tab="style"]')))return;let e=document.createElement("link");e.rel="stylesheet",e.href=`${u}/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??"",repoLocked:!1,sessionId:null,activeRunId:null,turns:[],isStreaming:!1};function x(e){let n=e.match(/github\.com[/:]([^/]+)\/([^/]+?)(?:\.git)?\/?$/);return n?`${n[1]}/${n[2]}`:e}let m=null;async function y(e,n){let s=await fetch(`${u}${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 c(){if(!t.me){a.innerHTML=`
1
+ "use strict";var CodingTab=(()=>{var O=Object.defineProperty;var ct=Object.getOwnPropertyDescriptor;var ut=Object.getOwnPropertyNames;var gt=Object.prototype.hasOwnProperty;var ht=(d,r)=>{for(var l in r)O(d,l,{get:r[l],enumerable:!0})},mt=(d,r,l,s)=>{if(r&&typeof r=="object"||typeof r=="function")for(let u of ut(r))!gt.call(d,u)&&u!==l&&O(d,u,{get:()=>r[u],enumerable:!(s=ct(r,u))||s.enumerable});return d};var pt=d=>mt(O({},"__esModule",{value:!0}),d);var wt={};ht(wt,{mountCodingTab:()=>F});function g(d){return d.replace(/[&<>"']/g,r=>({"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;","'":"&#39;"})[r])}function P(d){let r=[],l=/`([^`\n]+)`/g,s=0,u;for(;(u=l.exec(d))!==null;)r.push(J(d.slice(s,u.index))),r.push(`<code>${u[1]}</code>`),s=u.index+u[0].length;return r.push(J(d.slice(s))),r.join("")}function J(d){return d.replace(/\*\*([^*\n]+)\*\*/g,"<strong>$1</strong>").replace(/(^|[^*])\*([^*\n]+)\*(?!\*)/g,"$1<em>$2</em>").replace(/\[([^\]\n]+)\]\(([^)\s]+)\)/g,(r,l,s)=>`<a href="${/^(https?:|mailto:|\/|#)/i.test(s)?s:"#"}" target="_blank" rel="noreferrer noopener">${l}</a>`)}function G(d,r){if(d.length===0)return;let l=d.join(" ").trim();d.length=0,l&&r.push(`<p>${P(l)}</p>`)}function W(d,r){for(;d.length>0;){let l=d.pop();r.push(`<${l.type}>${l.items.join("")}</${l.type}>`)}}function z(d){let l=g(d).replace(/\r\n/g,`
2
+ `).split(`
3
+ `),s=[],u=[],m=[],a=!1,f="",p=[],w=()=>{G(u,s),W(m,s)};for(let S=0;S<l.length;S++){let b=l[S],M=b.match(/^\s*```(\w*)\s*$/);if(M){if(a){let v=f?` class="lang-${f}"`:"";s.push(`<pre><code${v}>${p.join(`
4
+ `)}</code></pre>`),p.length=0,f="",a=!1}else w(),a=!0,f=M[1]??"";continue}if(a){p.push(b);continue}if(!b.trim()){w();continue}if(/^\s*(-{3,}|\*{3,}|_{3,})\s*$/.test(b)){w(),s.push("<hr />");continue}let C=b.match(/^(#{1,6})\s+(.*)$/);if(C){w();let v=Math.min(6,C[1].length);s.push(`<h${v}>${P(C[2].trim())}</h${v}>`);continue}let y=b.match(/^(\s*)[-*]\s+(.*)$/),c=b.match(/^(\s*)(\d+)\.\s+(.*)$/);if(y||c){G(u,s);let v=(y?y[1]:c[1]).length,L=y?"ul":"ol",I=y?y[2]:c[3];for(;m.length>0&&m[m.length-1].indent>v;){let E=m.pop(),$=m[m.length-1]?.items,A=`<${E.type}>${E.items.join("")}</${E.type}>`;$&&$.length>0?$[$.length-1]=$[$.length-1].replace(/<\/li>$/,`${A}</li>`):s.push(A)}let k=m[m.length-1];!k||k.indent<v||k.type!==L?m.push({type:L,indent:v,items:[`<li>${P(I)}</li>`]}):k.items.push(`<li>${P(I)}</li>`);continue}m.length>0&&W(m,s),u.push(b.trim())}return a?s.push(`<pre><code>${p.join(`
5
+ `)}</code></pre>`):w(),s.join(`
6
+ `)}var K='<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>',V='<svg width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"><path d="M8 3v10M3 8h10"/></svg>',ft='<svg width="13" height="13" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linecap="round"><path d="M3 4h10M6 4V2.5a.5.5 0 01.5-.5h3a.5.5 0 01.5.5V4M5 4l.7 9.1a.9.9 0 00.9.9h2.8a.9.9 0 00.9-.9L11 4"/></svg>',bt='<svg width="13" height="13" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round"><path d="M11 2.5l2.5 2.5L6 12.5 3 13l.5-3z"/></svg>',vt='<svg width="18" height="18" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"><path d="M2.5 4h11M2.5 8h11M2.5 12h11"/></svg>',_t='<svg class="ct-chevron" width="10" height="10" viewBox="0 0 10 10" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><path d="M3 2l4 3-4 3"/></svg>';function T(d){return g(d)}function yt(d){let r=d.match(/github\.com[/:]([^/]+)\/([^/]+?)(?:\.git)?\/?$/);return r?`${r[1]}/${r[2]}`:d}function _(){return typeof crypto<"u"&&"randomUUID"in crypto?crypto.randomUUID():Math.random().toString(36).slice(2)}function $t(d){let r=Date.now()-d;return r<6e4?"just now":r<36e5?`${Math.round(r/6e4)}m`:r<864e5?`${Math.round(r/36e5)}h`:r<6048e5?`${Math.round(r/864e5)}d`:new Date(d).toLocaleDateString()}function F(d,r){let l=r.apiBase.replace(/\/$/,""),s=document.createElement("div");s.className="coding-tab",d.innerHTML="",d.appendChild(s);let u=!1;function m(){if(u||(u=!0,document.querySelector('link[data-coding-tab="style"]')))return;let t=document.createElement("link");t.rel="stylesheet",t.href=`${l}/style.css`,t.dataset.codingTab="style",document.head.appendChild(t)}m();let a={me:null,models:[],mode:r.defaultMode??"plan",model:r.defaultModel??"sonnet",repoUrl:r.defaultRepo??"",repoLocked:!1,chats:[],activeChatId:null,sidebarOpen:window.innerWidth>=720,expandedTools:new Set,mergeStates:new Map};function f(){return a.chats.find(t=>t.meta.id===a.activeChatId)??null}async function p(t,n){let e=await fetch(`${l}${t}`,{credentials:"include",headers:{"Content-Type":"application/json",...n?.headers??{}},...n});if(!e.ok)throw Object.assign(new Error(`${e.status} ${e.statusText}`),{status:e.status});return await e.json()}async function w(){try{let{chats:t}=await p("/chats"),n=new Map(a.chats.map(e=>[e.meta.id,e]));a.chats=t.map(e=>{let o=n.get(e.id);return o?{...o,meta:e}:{meta:e,loaded:!1,isStreaming:!1,activeRunId:null,abort:null}})}catch(t){console.error("[coding-tab] /chats list failed",t),a.chats=[]}}async function S(t){let n=a.chats.find(e=>e.meta.id===t);if(!n)return null;if(n.loaded)return n;try{let{chat:e}=await p(`/chats/${t}`);return n.turns=e.turns,n.loaded=!0,n.meta={id:e.id,title:e.title,mode:e.mode,model:e.model,createdAt:e.createdAt,updatedAt:e.updatedAt},a.mode=e.mode,a.model=e.model,n}catch(e){return console.error(`[coding-tab] /chats/${t} load failed`,e),null}}async function b(){try{let{chat:t}=await p("/chats",{method:"POST",body:JSON.stringify({mode:a.mode,model:a.model,repoUrl:a.repoLocked?void 0:a.repoUrl||void 0})}),n={meta:{id:t.id,title:t.title,mode:t.mode,model:t.model,createdAt:t.createdAt,updatedAt:t.updatedAt},turns:[],loaded:!0,isStreaming:!1,activeRunId:null,abort:null};return a.chats.unshift(n),a.activeChatId=n.meta.id,n}catch(t){return console.error("[coding-tab] create chat failed",t),null}}async function M(t){a.activeChatId=t,window.innerWidth<720&&(a.sidebarOpen=!1),c(),await S(t),c()}async function C(t){let n=a.chats.find(e=>e.meta.id===t);if(n&&confirm(`Delete chat "${n.meta.title}"? This cannot be undone.`)){n.abort?.abort();try{await p(`/chats/${t}`,{method:"DELETE"})}catch(e){console.error(`[coding-tab] delete chat ${t} failed`,e);return}a.chats=a.chats.filter(e=>e.meta.id!==t),a.activeChatId===t&&(a.activeChatId=a.chats[0]?.meta.id??null),c()}}async function y(t){let n=a.chats.find(i=>i.meta.id===t);if(!n)return;let e=prompt("Rename chat",n.meta.title);if(e===null)return;let o=e.trim();if(!(!o||o===n.meta.title))try{let{chat:i}=await p(`/chats/${t}`,{method:"PATCH",body:JSON.stringify({title:o})});n.meta={...n.meta,title:i.title,updatedAt:i.updatedAt},c()}catch(i){console.error(`[coding-tab] rename chat ${t} failed`,i)}}function c(){if(!a.me){s.innerHTML=`
2
7
  <div class="coding-tab__signin">
3
8
  <h2>Coding Tab</h2>
4
9
  <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="${u}/auth/login">${T}<span>Sign in with GitHub</span></a>
10
+ <a href="${l}/auth/login">${K}<span>Sign in with GitHub</span></a>
6
11
  </div>
7
- `;return}let e=`
12
+ `;return}let t=`
8
13
  <div class="coding-tab__header">
9
- <select data-role="model">
10
- ${t.models.map(g=>`<option value="${g.choice}" ${t.model===g.choice?"selected":""}>${d(g.displayName)}</option>`).join("")}
14
+ <button class="coding-tab__hamburger" data-role="toggle-sidebar" aria-label="Toggle chat list">${vt}</button>
15
+ <select data-role="model" title="Model">
16
+ ${a.models.map(h=>`<option value="${h.choice}" ${a.model===h.choice?"selected":""}>${g(h.displayName)}</option>`).join("")}
11
17
  </select>
12
18
  <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>
19
+ <button data-mode="plan" class="${a.mode==="plan"?"is-active":""}">Plan</button>
20
+ <button data-mode="agent" class="${a.mode==="agent"?"is-active":""}">Agent</button>
15
21
  </div>
16
- ${t.repoLocked&&t.repoUrl?`<a class="coding-tab__repo-locked" href="${d(t.repoUrl)}" target="_blank" rel="noreferrer" title="Locked to this app's repo (set by the server)">${T}<span>${d(x(t.repoUrl))}</span></a>`:`<input data-role="repo" placeholder="https://github.com/org/repo" value="${d(t.repoUrl)}" style="flex:1;min-width:200px" />`}
22
+ ${a.repoLocked&&a.repoUrl?`<a class="coding-tab__repo-locked" href="${T(a.repoUrl)}" target="_blank" rel="noreferrer" title="Locked to this app's repo (set by the server)">${K}<span>${g(yt(a.repoUrl))}</span></a>`:`<input data-role="repo" placeholder="https://github.com/org/repo" value="${T(a.repoUrl)}" style="flex:1;min-width:200px" />`}
17
23
  <div class="coding-tab__user">
18
- ${t.me.avatarUrl?`<img src="${t.me.avatarUrl}" alt="" />`:""}
19
- <span>@${d(t.me.githubLogin)}</span>
24
+ ${a.me.avatarUrl?`<img src="${a.me.avatarUrl}" alt="" />`:""}
25
+ <span class="coding-tab__user-name">@${g(a.me.githubLogin)}</span>
20
26
  <button data-role="logout">Sign out</button>
21
27
  </div>
22
28
  </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(E).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>
29
+ `,n=`
30
+ <aside class="coding-tab__sidebar ${a.sidebarOpen?"is-open":""}">
31
+ <button class="coding-tab__btn primary coding-tab__new-chat" data-role="new-chat">${V}<span>New chat</span></button>
32
+ <div class="coding-tab__chat-list">
33
+ ${a.chats.length===0?'<div class="coding-tab__chat-empty">No chats yet \u2014 start one with the button above.</div>':a.chats.map(h=>v(h)).join("")}
34
+ </div>
35
+ </aside>
36
+ `,e=f(),o=`
37
+ <main class="coding-tab__pane">
38
+ ${e?I(e):L()}
39
+ </main>
40
+ `;s.innerHTML=`
41
+ ${t}
42
+ <div class="coding-tab__body">
43
+ ${n}
44
+ ${o}
45
+ </div>
46
+ `;let i=s.querySelector("[data-role=thread]");i&&(i.scrollTop=i.scrollHeight),tt()}function v(t){let n=t.meta.id===a.activeChatId?" is-active":"",e=t.isStreaming?'<span class="coding-tab__chat-row-dot" title="Streaming"></span>':"";return`
47
+ <div class="coding-tab__chat-row${n}" data-chat-id="${t.meta.id}">
48
+ <button class="coding-tab__chat-row-main" data-role="select-chat" data-chat-id="${t.meta.id}">
49
+ ${e}
50
+ <span class="coding-tab__chat-row-title">${g(t.meta.title)}</span>
51
+ <span class="coding-tab__chat-row-meta">${t.meta.mode==="plan"?"Plan":"Agent"} \xB7 ${$t(t.meta.updatedAt)}</span>
52
+ </button>
53
+ <div class="coding-tab__chat-row-actions">
54
+ <button data-role="rename-chat" data-chat-id="${t.meta.id}" title="Rename">${bt}</button>
55
+ <button data-role="delete-chat" data-chat-id="${t.meta.id}" title="Delete">${ft}</button>
56
+ </div>
57
+ </div>
58
+ `}function L(){return`
59
+ <div class="coding-tab__pane-empty">
60
+ <h3>Start a new chat</h3>
61
+ <p>Pick a mode (Plan or Agent), choose a model, and click <strong>New chat</strong> to begin. Each chat keeps its own context.</p>
62
+ <button class="coding-tab__btn primary" data-role="new-chat">${V}<span>New chat</span></button>
63
+ </div>
64
+ `}function I(t){let n=t.turns??[],e=t.loaded?n.length===0?'<div class="coding-tab__notice info">Type a message below to kick off this chat.</div>':n.map(i=>k(i)).join(""):'<div class="coding-tab__notice info">Loading\u2026</div>',o=n.length===0?'Ask anything (e.g. "add dark mode that follows the OS setting")':a.mode==="plan"?"Refine the plan, or ask another planning question":"Send another instruction";return`
65
+ <div class="coding-tab__thread" data-role="thread">${e}</div>
26
66
  <div class="coding-tab__composer">
27
- <textarea data-role="prompt" placeholder="${r}" rows="2"></textarea>
67
+ <textarea data-role="prompt" placeholder="${T(o)}" rows="2"></textarea>
28
68
  <button class="coding-tab__btn primary" data-role="send" ${t.isStreaming?"disabled":""}>${t.isStreaming?"Working\u2026":"Send"}</button>
29
69
  ${t.isStreaming?'<button class="coding-tab__btn danger" data-role="cancel">Stop</button>':""}
30
70
  </div>
31
- `;let l=a.querySelector("[data-role=thread]");l&&(l.scrollTop=l.scrollHeight),C()}function E(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">${d(e.text)}</div>
34
- </div>`;let n=e.tools.length===0?"":`
35
- <div class="coding-tab__msg-tools">
36
- ${e.tools.map(g=>`<div class="coding-tab__tool" data-status="${g.status}"><span class="dot"></span><span>${d(g.name)}</span></div>`).join("")}
37
- </div>`,s=e.status&&e.status!=="finished"?`<div class="coding-tab__status">${d(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>`:"",l=e.pr?I(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">${K(e.text||(e.tools.length>0?"":"\u2026"))}${n}${r}${l}</div>
43
- </div>`}function I(e){return`<div class="coding-tab__pr">
44
- <div class="coding-tab__pr-title">PR #${e.number} opened in ${d(e.owner)}/${d(e.repo)}</div>
45
- ${e.title?`<div>${d(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>
71
+ `}function k(t){if(t.role==="user"){let h=t.prompt??Z(t);return`<div class="coding-tab__msg user">
72
+ <div class="coding-tab__msg-role">You \xB7 ${t.isPlan?"Plan":"Agent"}</div>
73
+ <div class="coding-tab__msg-body">${g(h)}</div>
74
+ </div>`}let n=t.events.length===0&&t.status==="running"?'<div class="coding-tab__msg-pending">Working\u2026</div>':t.events.map(h=>E(h,t.isPlan)).join(""),e=t.status&&t.status!=="finished"?`<div class="coding-tab__status">${g(t.status)}</div>`:"",o=t.showExecute&&!f()?.isStreaming?`<div class="coding-tab__plan-actions">
75
+ <button class="coding-tab__btn primary" data-role="execute" data-turn="${t.id}">Execute plan</button>
76
+ </div>`:"",i=t.pr?X(t.pr):"";return`<div class="coding-tab__msg assistant">
77
+ <div class="coding-tab__msg-role">Coding Tab \xB7 ${t.isPlan?"Plan":"Agent"} ${e}</div>
78
+ <div class="coding-tab__msg-body">${n}${o}${i}</div>
79
+ </div>`}function E(t,n){if(t.kind==="text"){let e=t.text.trim();return e?`<div class="coding-tab__md">${n?z(e):$(e)}</div>`:""}return A(t)}function $(t){return g(t).split(/\n\s*\n/).map(n=>n.replace(/`([^`\n]+)`/g,"<code>$1</code>").replace(/\*\*([^*\n]+)\*\*/g,"<strong>$1</strong>").replace(/\n/g,"<br />")).map(n=>`<p>${n}</p>`).join("")}function A(t){let n=Y(t.name,t.args),e=a.expandedTools.has(t.id),o=e?Q(t):"";return`
80
+ <div class="coding-tab__tool" data-status="${t.status}" data-tool-id="${t.id}">
81
+ <button class="coding-tab__tool-header" data-role="toggle-tool" data-tool-id="${t.id}" aria-expanded="${e}">
82
+ <span class="coding-tab__tool-dot"></span>
83
+ ${_t}
84
+ <span class="coding-tab__tool-name">${g(t.name)}</span>
85
+ ${n?`<span class="coding-tab__tool-summary">${g(n)}</span>`:""}
86
+ </button>
87
+ ${o}
49
88
  </div>
50
- </div>`}function C(){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,c()})}),a.querySelector('[data-role="repo"]')?.addEventListener("change",e=>{t.repoUrl=e.target.value.trim()}),a.querySelector('[data-role="logout"]')?.addEventListener("click",async()=>{await fetch(`${u}/auth/logout`,{method:"POST",credentials:"include"}).catch(()=>{}),t.me=null,t.sessionId=null,t.turns=[],c()}),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",()=>P()),a.querySelectorAll('[data-role="execute"]').forEach(e=>e.addEventListener("click",()=>L())),a.querySelectorAll('[data-role="merge"]').forEach(e=>{e.addEventListener("click",()=>R(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,c();try{let l=!t.sessionId,g=l?"/agent/start":"/agent/send",f=l?{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(g,f,r)}catch(l){r.text+=`
51
-
52
- [error] ${l instanceof Error?l.message:String(l)}`,r.status="error"}finally{t.isStreaming=!1,m=null,c()}}async function L(){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,c();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,m=null,c()}}async function P(){if(!t.sessionId||!t.activeRunId){m?.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)}m?.abort()}async function R(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{c()}}async function S(e,n,s){m=new AbortController;let r=await fetch(`${u}${e}`,{method:"POST",credentials:"include",headers:{"Content-Type":"application/json",Accept:"text/event-stream"},body:JSON.stringify(n),signal:m.signal});if(!r.ok||!r.body)throw new Error(`${r.status} ${r.statusText}`);let l=r.body.getReader(),g=new TextDecoder,f="";for(;;){let{value:q,done:O}=await l.read();if(O)break;f+=g.decode(q,{stream:!0});let _=f.split(`
89
+ `}function Y(t,n){if(!n||typeof n!="object")return"";let e=n,o=t.toLowerCase();if(o.includes("grep")){let i=typeof e.pattern=="string"?e.pattern:typeof e.query=="string"?e.query:"";return i?`"${i}"`:""}if(o.includes("read")&&typeof e.path=="string"){let i=typeof e.offset=="number"||typeof e.limit=="number"?`:${e.offset??""}-${Number(e.offset??0)+Number(e.limit??0)||""}`:"";return`${e.path}${i}`}return(o.includes("glob")||o.includes("file_search"))&&typeof e.glob_pattern=="string"?e.glob_pattern:o==="shell"&&typeof e.command=="string"?e.command.slice(0,80):o==="task"&&typeof e.description=="string"?e.description:o.includes("write")&&typeof e.path=="string"||o.includes("edit")&&typeof e.path=="string"||o.includes("delete")&&typeof e.path=="string"?e.path:""}function Q(t){let n=t.args!==void 0&&t.args!==null?`<div class="coding-tab__tool-section"><div class="coding-tab__tool-label">Args</div><pre>${g(H(t.args))}</pre></div>`:"",e=t.result!==void 0&&t.result!==null?`<div class="coding-tab__tool-section"><div class="coding-tab__tool-label">Result</div><pre>${g(H(t.result))}</pre></div>`:t.status==="running"?'<div class="coding-tab__tool-section"><div class="coding-tab__tool-label">Result</div><pre class="coding-tab__tool-pending">\u2026running</pre></div>':"";return`<div class="coding-tab__tool-detail">${n}${e}</div>`}function H(t){if(typeof t=="string")return t;try{return JSON.stringify(t,null,2)}catch{return String(t)}}function X(t){let n=a.mergeStates.get(t.url)??{state:"idle"},e,o="";switch(n.state){case"loading":e='<button class="coding-tab__btn primary" data-state="loading" disabled>Merging\u2026</button>',o='<div class="coding-tab__pr-status">Merging this PR and triggering Railway redeploy\u2026</div>';break;case"success":e='<button class="coding-tab__btn success" disabled>Merged \u2713</button>',o=`<div class="coding-tab__pr-status is-success">Merged commit ${g(n.sha.slice(0,7))}. Railway should redeploy on the next push hook.</div>`;break;case"error":e=`<button class="coding-tab__btn primary" data-role="merge" data-pr="${T(t.url)}">Try merge again</button>`,o=`<div class="coding-tab__pr-status is-error">Merge failed: ${g(n.message)}</div>`;break;default:e=`<button class="coding-tab__btn primary" data-role="merge" data-pr="${T(t.url)}">Merge & Redeploy</button>`}return`<div class="coding-tab__pr">
90
+ <div class="coding-tab__pr-title">PR #${t.number} opened in ${g(t.owner)}/${g(t.repo)}</div>
91
+ ${t.title?`<div>${g(t.title)}</div>`:""}
92
+ <div class="coding-tab__pr-actions">
93
+ <a class="coding-tab__btn" href="${T(t.url)}" target="_blank" rel="noreferrer">Open in GitHub</a>
94
+ ${e}
95
+ </div>
96
+ ${o}
97
+ </div>`}function Z(t){return t.events.filter(n=>n.kind==="text").map(n=>n.text).join(`
55
98
 
56
- `);f=_.pop()??"";for(let A of _){let k=A.split(`
57
- `).find(b=>b.startsWith("data: "));if(k)try{let b=JSON.parse(k.slice(6));H(b,s),c()}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+=`
99
+ `)}function tt(){s.querySelector('[data-role="model"]')?.addEventListener("change",t=>{a.model=t.target.value}),s.querySelectorAll("[data-mode]").forEach(t=>{t.addEventListener("click",()=>{a.mode=t.dataset.mode,c()})}),s.querySelector('[data-role="repo"]')?.addEventListener("change",t=>{a.repoUrl=t.target.value.trim()}),s.querySelector('[data-role="logout"]')?.addEventListener("click",async()=>{await fetch(`${l}/auth/logout`,{method:"POST",credentials:"include"}).catch(()=>{});for(let t of a.chats)t.abort?.abort();a.me=null,a.chats=[],a.activeChatId=null,c()}),s.querySelector('[data-role="toggle-sidebar"]')?.addEventListener("click",()=>{a.sidebarOpen=!a.sidebarOpen,c()}),s.querySelectorAll('[data-role="new-chat"]').forEach(t=>t.addEventListener("click",async()=>{await b()&&window.innerWidth<720&&(a.sidebarOpen=!1),c()})),s.querySelectorAll('[data-role="select-chat"]').forEach(t=>t.addEventListener("click",()=>{let n=t.dataset.chatId;M(n)})),s.querySelectorAll('[data-role="rename-chat"]').forEach(t=>t.addEventListener("click",n=>{n.stopPropagation(),y(t.dataset.chatId)})),s.querySelectorAll('[data-role="delete-chat"]').forEach(t=>t.addEventListener("click",n=>{n.stopPropagation(),C(t.dataset.chatId)})),s.querySelector('[data-role="send"]')?.addEventListener("click",()=>q()),s.querySelector('[data-role="prompt"]')?.addEventListener("keydown",t=>{let n=t;n.key==="Enter"&&(n.metaKey||n.ctrlKey)&&(n.preventDefault(),q())}),s.querySelector('[data-role="cancel"]')?.addEventListener("click",()=>nt()),s.querySelectorAll('[data-role="execute"]').forEach(t=>t.addEventListener("click",()=>et())),s.querySelectorAll('[data-role="merge"]').forEach(t=>{t.addEventListener("click",()=>at(t.dataset.pr))}),s.querySelectorAll('[data-role="toggle-tool"]').forEach(t=>t.addEventListener("click",()=>{let n=t.dataset.toolId;a.expandedTools.has(n)?a.expandedTools.delete(n):a.expandedTools.add(n),c()}))}async function q(){let t=f();if(!t&&(t=await b(),!t)||t.isStreaming)return;let n=s.querySelector('[data-role="prompt"]'),e=n?.value.trim()??"";if(!e)return;n&&(n.value=""),t.turns=t.turns??[];let o={id:_(),chatId:t.meta.id,seq:t.turns.length,role:"user",isPlan:a.mode==="plan",status:"finished",events:[{kind:"text",id:_(),text:e}],prompt:e,createdAt:Date.now()};t.turns.push(o);let i={id:_(),chatId:t.meta.id,seq:t.turns.length,role:"assistant",isPlan:a.mode==="plan",status:"running",events:[],createdAt:Date.now()};t.turns.push(i),t.isStreaming=!0,t.meta.title==="New chat"&&(t.meta={...t.meta,title:e.length>60?`${e.slice(0,57)}\u2026`:e}),t.meta={...t.meta,mode:a.mode,model:a.model,updatedAt:Date.now()},c();try{await j("/agent/send",{chatId:t.meta.id,prompt:e,mode:a.mode},t,i)}catch(h){h.name!=="AbortError"&&N(i,h),i.status="error"}finally{t.isStreaming=!1,t.activeRunId=null,t.abort=null,c()}}async function et(){let t=f();if(!t||t.isStreaming)return;t.turns=t.turns??[];let n={id:_(),chatId:t.meta.id,seq:t.turns.length,role:"assistant",isPlan:!1,status:"running",events:[],createdAt:Date.now()};t.turns.push(n),t.isStreaming=!0,c();try{await j("/agent/execute",{chatId:t.meta.id},t,n)}catch(e){e.name!=="AbortError"&&N(n,e),n.status="error"}finally{t.isStreaming=!1,t.activeRunId=null,t.abort=null,c()}}async function nt(){let t=f();if(t){if(t.activeRunId)try{await p("/agent/cancel",{method:"POST",body:JSON.stringify({chatId:t.meta.id,runId:t.activeRunId})})}catch(n){console.error("[coding-tab] cancel failed",n)}t.abort?.abort()}}async function at(t){let n=f();if(n&&a.mergeStates.get(t)?.state!=="loading"){a.mergeStates.set(t,{state:"loading"}),c(),n.turns=n.turns??[];try{let e=await p("/pr/merge",{method:"POST",body:JSON.stringify({prUrl:t,mergeMethod:"squash"})});e.merged?(a.mergeStates.set(t,{state:"success",sha:e.sha}),n.turns.push({id:_(),chatId:n.meta.id,seq:n.turns.length,role:"assistant",isPlan:!1,status:"finished",events:[{kind:"text",id:_(),text:`Merged commit \`${e.sha.slice(0,7)}\` into the default branch. Railway should redeploy on the next push hook.`}],createdAt:Date.now()})):a.mergeStates.set(t,{state:"error",message:"GitHub returned merged=false"})}catch(e){let o=e instanceof Error?e.message:String(e);a.mergeStates.set(t,{state:"error",message:o})}finally{c()}}}function N(t,n){let e=n instanceof Error?n.message:String(n);t.events.push({kind:"text",id:_(),text:`[error] ${e}`})}async function j(t,n,e,o){e.abort=new AbortController;let i=await fetch(`${l}${t}`,{method:"POST",credentials:"include",headers:{"Content-Type":"application/json",Accept:"text/event-stream"},body:JSON.stringify(n),signal:e.abort.signal});if(!i.ok||!i.body)throw new Error(`${i.status} ${i.statusText}`);let h=i.body.getReader(),rt=new TextDecoder,R="",U=!1;for(;;){let{value:it,done:dt}=await h.read();if(dt)break;R+=rt.decode(it,{stream:!0});let B=R.split(`
58
100
 
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 U(){try{let e=await y("/auth/me");t.me=e,e.defaultRepoUrl&&(t.repoUrl=e.defaultRepoUrl,t.repoLocked=!0)}catch(e){let n=e.status;if(n===401||n===403){t.me=null,c();return}console.error("[coding-tab] /auth/me failed",e),t.me=null,c();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)"}]}c()}return U(),{destroy(){m?.abort(),i.removeChild(a)}}}typeof window<"u"&&(window.CodingTab={mountCodingTab:w});return J(z);})();
101
+ `);R=B.pop()??"";for(let lt of B){let D=lt.split(`
102
+ `).find(x=>x.startsWith("data: "));if(D)try{let x=JSON.parse(D.slice(6));U=st(x,e,o,U),c()}catch(x){console.warn("[coding-tab] bad sse event",x)}}}}function st(t,n,e,o){switch(t.kind){case"ready":return n.activeRunId=t.runId,!1;case"text":{let i=e.events[e.events.length-1];return o&&i&&i.kind==="text"?i.text+=t.text:e.events.push({kind:"text",id:_(),text:t.text}),!0}case"thinking":return o;case"tool":{let i=e.events.find(h=>h.kind==="tool"&&h.callId===t.callId);return i?(i.status=t.status,t.args!==void 0&&(i.args=t.args),t.result!==void 0&&(i.result=t.result)):e.events.push({kind:"tool",id:_(),callId:t.callId,name:t.name,status:t.status,args:t.args,result:t.result}),!1}case"status":return o;case"result":return e.status=t.status,t.pr&&(e.pr=t.pr),e.isPlan&&t.status==="finished"&&(e.showExecute=!0),n.activeRunId=null,!1;case"error":return e.events.push({kind:"text",id:_(),text:`[error] ${t.message}`}),e.status="error",n.activeRunId=null,!1}}async function ot(){try{let t=await p("/auth/me");a.me=t,t.defaultRepoUrl&&(a.repoUrl=t.defaultRepoUrl,a.repoLocked=!0)}catch(t){let n=t.status;if(n===401||n===403){a.me=null,c();return}console.error("[coding-tab] /auth/me failed",t),a.me=null,c();return}try{let{models:t}=await p("/models");a.models=t,t.length>0&&!t.find(n=>n.choice===a.model)&&(a.model=t[0].choice)}catch(t){console.error("[coding-tab] /models failed",t),a.models=[{choice:"sonnet",cursorModelId:"auto",displayName:"Sonnet (fallback)"},{choice:"opus",cursorModelId:"auto",displayName:"Opus (fallback)"}]}await w(),a.chats.length>0&&!a.activeChatId&&(a.activeChatId=a.chats[0].meta.id,S(a.activeChatId).then(()=>c())),c()}return ot(),{destroy(){for(let t of a.chats)t.abort?.abort();d.removeChild(s)}}}typeof window<"u"&&(window.CodingTab={mountCodingTab:F});return pt(wt);})();
60
103
  //# sourceMappingURL=browser.js.map