@tstax/coding-tab 0.1.2 → 0.2.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 +60 -7
- package/dist/browser.js +81 -39
- package/dist/browser.js.map +1 -1
- package/dist/server.cjs +642 -125
- package/dist/server.cjs.map +1 -1
- package/dist/server.d.cts +150 -22
- package/dist/server.d.ts +150 -22
- package/dist/server.js +641 -123
- package/dist/server.js.map +1 -1
- package/dist/style.css +310 -17
- package/package.json +2 -2
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,
|
|
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` —
|
|
103
|
-
- `POST /agent/send` — SSE stream;
|
|
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
|
|
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/
|
|
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,102 @@
|
|
|
1
|
-
"use strict";var CodingTab=(()=>{var
|
|
1
|
+
"use strict";var CodingTab=(()=>{var O=Object.defineProperty;var ct=Object.getOwnPropertyDescriptor;var ut=Object.getOwnPropertyNames;var ht=Object.prototype.hasOwnProperty;var gt=(d,r)=>{for(var l in r)O(d,l,{get:r[l],enumerable:!0})},pt=(d,r,l,o)=>{if(r&&typeof r=="object"||typeof r=="function")for(let u of ut(r))!ht.call(d,u)&&u!==l&&O(d,u,{get:()=>r[u],enumerable:!(o=ct(r,u))||o.enumerable});return d};var mt=d=>pt(O({},"__esModule",{value:!0}),d);var wt={};gt(wt,{mountCodingTab:()=>F});function g(d){return d.replace(/[&<>"']/g,r=>({"&":"&","<":"<",">":">",'"':""","'":"'"})[r])}function P(d){let r=[],l=/`([^`\n]+)`/g,o=0,u;for(;(u=l.exec(d))!==null;)r.push(J(d.slice(o,u.index))),r.push(`<code>${u[1]}</code>`),o=u.index+u[0].length;return r.push(J(d.slice(o))),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,o)=>`<a href="${/^(https?:|mailto:|\/|#)/i.test(o)?o:"#"}" 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
|
+
`),o=[],u=[],p=[],a=!1,b="",m=[],w=()=>{G(u,o),W(p,o)};for(let k=0;k<l.length;k++){let v=l[k],M=v.match(/^\s*```(\w*)\s*$/);if(M){if(a){let y=b?` class="lang-${b}"`:"";o.push(`<pre><code${y}>${m.join(`
|
|
4
|
+
`)}</code></pre>`),m.length=0,b="",a=!1}else w(),a=!0,b=M[1]??"";continue}if(a){m.push(v);continue}if(!v.trim()){w();continue}if(/^\s*(-{3,}|\*{3,}|_{3,})\s*$/.test(v)){w(),o.push("<hr />");continue}let T=v.match(/^(#{1,6})\s+(.*)$/);if(T){w();let y=Math.min(6,T[1].length);o.push(`<h${y}>${P(T[2].trim())}</h${y}>`);continue}let $=v.match(/^(\s*)[-*]\s+(.*)$/),c=v.match(/^(\s*)(\d+)\.\s+(.*)$/);if($||c){G(u,o);let y=($?$[1]:c[1]).length,I=$?"ul":"ol",L=$?$[2]:c[3];for(;p.length>0&&p[p.length-1].indent>y;){let C=p.pop(),_=p[p.length-1]?.items,A=`<${C.type}>${C.items.join("")}</${C.type}>`;_&&_.length>0?_[_.length-1]=_[_.length-1].replace(/<\/li>$/,`${A}</li>`):o.push(A)}let S=p[p.length-1];!S||S.indent<y||S.type!==I?p.push({type:I,indent:y,items:[`<li>${P(L)}</li>`]}):S.items.push(`<li>${P(L)}</li>`);continue}p.length>0&&W(p,o),u.push(v.trim())}return a?o.push(`<pre><code>${m.join(`
|
|
5
|
+
`)}</code></pre>`):w(),o.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>',yt='<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 E(d){return g(d)}function $t(d){let r=d.match(/github\.com[/:]([^/]+)\/([^/]+?)(?:\.git)?\/?$/);return r?`${r[1]}/${r[2]}`:d}function f(){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(/\/$/,""),o=document.createElement("div");o.className="coding-tab",d.innerHTML="",d.appendChild(o);let u=!1;function p(){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)}p();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};function b(){return a.chats.find(t=>t.meta.id===a.activeChatId)??null}async function m(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 m("/chats"),n=new Map(a.chats.map(e=>[e.meta.id,e]));a.chats=t.map(e=>{let i=n.get(e.id);return i?{...i,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 k(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 m(`/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 v(){try{let{chat:t}=await m("/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 k(t),c()}async function T(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 m(`/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 $(t){let n=a.chats.find(s=>s.meta.id===t);if(!n)return;let e=prompt("Rename chat",n.meta.title);if(e===null)return;let i=e.trim();if(!(!i||i===n.meta.title))try{let{chat:s}=await m(`/chats/${t}`,{method:"PATCH",body:JSON.stringify({title:i})});n.meta={...n.meta,title:s.title,updatedAt:s.updatedAt},c()}catch(s){console.error(`[coding-tab] rename chat ${t} failed`,s)}}function c(){if(!a.me){o.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="${
|
|
10
|
+
<a href="${l}/auth/login">${K}<span>Sign in with GitHub</span></a>
|
|
6
11
|
</div>
|
|
7
|
-
`;return}let
|
|
12
|
+
`;return}let t=`
|
|
8
13
|
<div class="coding-tab__header">
|
|
9
|
-
<
|
|
10
|
-
|
|
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="${
|
|
14
|
-
<button data-mode="agent" class="${
|
|
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
|
-
${
|
|
22
|
+
${a.repoLocked&&a.repoUrl?`<a class="coding-tab__repo-locked" href="${E(a.repoUrl)}" target="_blank" rel="noreferrer" title="Locked to this app's repo (set by the server)">${K}<span>${g($t(a.repoUrl))}</span></a>`:`<input data-role="repo" placeholder="https://github.com/org/repo" value="${E(a.repoUrl)}" style="flex:1;min-width:200px" />`}
|
|
17
23
|
<div class="coding-tab__user">
|
|
18
|
-
${
|
|
19
|
-
<span>@${
|
|
24
|
+
${a.me.avatarUrl?`<img src="${a.me.avatarUrl}" alt="" />`:""}
|
|
25
|
+
<span>@${g(a.me.githubLogin)}</span>
|
|
20
26
|
<button data-role="logout">Sign out</button>
|
|
21
27
|
</div>
|
|
22
28
|
</div>
|
|
23
|
-
`,n
|
|
24
|
-
${
|
|
25
|
-
|
|
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=>y(h)).join("")}
|
|
34
|
+
</div>
|
|
35
|
+
</aside>
|
|
36
|
+
`,e=b(),i=`
|
|
37
|
+
<main class="coding-tab__pane">
|
|
38
|
+
${e?L(e):I()}
|
|
39
|
+
</main>
|
|
40
|
+
`;o.innerHTML=`
|
|
41
|
+
${t}
|
|
42
|
+
<div class="coding-tab__body">
|
|
43
|
+
${n}
|
|
44
|
+
${i}
|
|
45
|
+
</div>
|
|
46
|
+
`;let s=o.querySelector("[data-role=thread]");s&&(s.scrollTop=s.scrollHeight),tt()}function y(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 I(){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 L(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(s=>S(s)).join(""):'<div class="coding-tab__notice info">Loading\u2026</div>',i=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="${
|
|
67
|
+
<textarea data-role="prompt" placeholder="${E(i)}" 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
|
-
|
|
32
|
-
<div class="coding-tab__msg-role">You \xB7 ${
|
|
33
|
-
<div class="coding-tab__msg-body">${
|
|
34
|
-
</div
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
<div class="coding-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
71
|
+
`}function S(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=>C(h,t.isPlan)).join(""),e=t.status&&t.status!=="finished"?`<div class="coding-tab__status">${g(t.status)}</div>`:"",i=t.showExecute&&!b()?.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>`:"",s=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}${i}${s}</div>
|
|
79
|
+
</div>`}function C(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),i=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
|
+
${yt}
|
|
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
|
+
${i}
|
|
88
|
+
</div>
|
|
89
|
+
`}function Y(t,n){if(!n||typeof n!="object")return"";let e=n,i=t.toLowerCase();if(i.includes("grep")){let s=typeof e.pattern=="string"?e.pattern:typeof e.query=="string"?e.query:"";return s?`"${s}"`:""}if(i.includes("read")&&typeof e.path=="string"){let s=typeof e.offset=="number"||typeof e.limit=="number"?`:${e.offset??""}-${Number(e.offset??0)+Number(e.limit??0)||""}`:"";return`${e.path}${s}`}return(i.includes("glob")||i.includes("file_search"))&&typeof e.glob_pattern=="string"?e.glob_pattern:i==="shell"&&typeof e.command=="string"?e.command.slice(0,80):i==="task"&&typeof e.description=="string"?e.description:i.includes("write")&&typeof e.path=="string"||i.includes("edit")&&typeof e.path=="string"||i.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){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>`:""}
|
|
46
92
|
<div style="display:flex;gap:8px;flex-wrap:wrap">
|
|
47
|
-
<a class="coding-tab__btn" href="${
|
|
48
|
-
<button class="coding-tab__btn primary" data-role="merge" data-pr="${
|
|
93
|
+
<a class="coding-tab__btn" href="${E(t.url)}" target="_blank" rel="noreferrer">Open in GitHub</a>
|
|
94
|
+
<button class="coding-tab__btn primary" data-role="merge" data-pr="${E(t.url)}">Merge & Redeploy</button>
|
|
49
95
|
</div>
|
|
50
|
-
</div>`}function
|
|
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(`
|
|
96
|
+
</div>`}function Z(t){return t.events.filter(n=>n.kind==="text").map(n=>n.text).join(`
|
|
55
97
|
|
|
56
|
-
`);f=
|
|
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+=`
|
|
98
|
+
`)}function tt(){o.querySelector('[data-role="model"]')?.addEventListener("change",t=>{a.model=t.target.value}),o.querySelectorAll("[data-mode]").forEach(t=>{t.addEventListener("click",()=>{a.mode=t.dataset.mode,c()})}),o.querySelector('[data-role="repo"]')?.addEventListener("change",t=>{a.repoUrl=t.target.value.trim()}),o.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()}),o.querySelector('[data-role="toggle-sidebar"]')?.addEventListener("click",()=>{a.sidebarOpen=!a.sidebarOpen,c()}),o.querySelectorAll('[data-role="new-chat"]').forEach(t=>t.addEventListener("click",async()=>{await v()&&window.innerWidth<720&&(a.sidebarOpen=!1),c()})),o.querySelectorAll('[data-role="select-chat"]').forEach(t=>t.addEventListener("click",()=>{let n=t.dataset.chatId;M(n)})),o.querySelectorAll('[data-role="rename-chat"]').forEach(t=>t.addEventListener("click",n=>{n.stopPropagation(),$(t.dataset.chatId)})),o.querySelectorAll('[data-role="delete-chat"]').forEach(t=>t.addEventListener("click",n=>{n.stopPropagation(),T(t.dataset.chatId)})),o.querySelector('[data-role="send"]')?.addEventListener("click",()=>q()),o.querySelector('[data-role="prompt"]')?.addEventListener("keydown",t=>{let n=t;n.key==="Enter"&&(n.metaKey||n.ctrlKey)&&(n.preventDefault(),q())}),o.querySelector('[data-role="cancel"]')?.addEventListener("click",()=>nt()),o.querySelectorAll('[data-role="execute"]').forEach(t=>t.addEventListener("click",()=>et())),o.querySelectorAll('[data-role="merge"]').forEach(t=>{t.addEventListener("click",()=>at(t.dataset.pr))}),o.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=b();if(!t&&(t=await v(),!t)||t.isStreaming)return;let n=o.querySelector('[data-role="prompt"]'),e=n?.value.trim()??"";if(!e)return;n&&(n.value=""),t.turns=t.turns??[];let i={id:f(),chatId:t.meta.id,seq:t.turns.length,role:"user",isPlan:a.mode==="plan",status:"finished",events:[{kind:"text",id:f(),text:e}],prompt:e,createdAt:Date.now()};t.turns.push(i);let s={id:f(),chatId:t.meta.id,seq:t.turns.length,role:"assistant",isPlan:a.mode==="plan",status:"running",events:[],createdAt:Date.now()};t.turns.push(s),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,s)}catch(h){h.name!=="AbortError"&&N(s,h),s.status="error"}finally{t.isStreaming=!1,t.activeRunId=null,t.abort=null,c()}}async function et(){let t=b();if(!t||t.isStreaming)return;t.turns=t.turns??[];let n={id:f(),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=b();if(t){if(t.activeRunId)try{await m("/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=b();if(n){n.turns=n.turns??[];try{let e=await m("/pr/merge",{method:"POST",body:JSON.stringify({prUrl:t,mergeMethod:"squash"})}),i=e.merged?`Merged commit \`${e.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.";n.turns.push({id:f(),chatId:n.meta.id,seq:n.turns.length,role:"assistant",isPlan:!1,status:e.merged?"finished":"error",events:[{kind:"text",id:f(),text:i}],createdAt:Date.now()})}catch(e){let i=e instanceof Error?e.message:String(e);n.turns.push({id:f(),chatId:n.meta.id,seq:n.turns.length,role:"assistant",isPlan:!1,status:"error",events:[{kind:"text",id:f(),text:`[merge failed] ${i}`}],createdAt:Date.now()})}finally{c()}}}function N(t,n){let e=n instanceof Error?n.message:String(n);t.events.push({kind:"text",id:f(),text:`[error] ${e}`})}async function j(t,n,e,i){e.abort=new AbortController;let s=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(!s.ok||!s.body)throw new Error(`${s.status} ${s.statusText}`);let h=s.body.getReader(),st=new TextDecoder,R="",U=!1;for(;;){let{value:it,done:dt}=await h.read();if(dt)break;R+=st.decode(it,{stream:!0});let D=R.split(`
|
|
58
99
|
|
|
59
|
-
|
|
100
|
+
`);R=D.pop()??"";for(let lt of D){let B=lt.split(`
|
|
101
|
+
`).find(x=>x.startsWith("data: "));if(B)try{let x=JSON.parse(B.slice(6));U=ot(x,e,i,U),c()}catch(x){console.warn("[coding-tab] bad sse event",x)}}}}function ot(t,n,e,i){switch(t.kind){case"ready":return n.activeRunId=t.runId,!1;case"text":{let s=e.events[e.events.length-1];return i&&s&&s.kind==="text"?s.text+=t.text:e.events.push({kind:"text",id:f(),text:t.text}),!0}case"thinking":return i;case"tool":{let s=e.events.find(h=>h.kind==="tool"&&h.callId===t.callId);return s?(s.status=t.status,t.args!==void 0&&(s.args=t.args),t.result!==void 0&&(s.result=t.result)):e.events.push({kind:"tool",id:f(),callId:t.callId,name:t.name,status:t.status,args:t.args,result:t.result}),!1}case"status":return i;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:f(),text:`[error] ${t.message}`}),e.status="error",n.activeRunId=null,!1}}async function rt(){try{let t=await m("/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 m("/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,k(a.activeChatId).then(()=>c())),c()}return rt(),{destroy(){for(let t of a.chats)t.abort?.abort();d.removeChild(o)}}}typeof window<"u"&&(window.CodingTab={mountCodingTab:F});return mt(wt);})();
|
|
60
102
|
//# sourceMappingURL=browser.js.map
|
package/dist/browser.js.map
CHANGED
|
@@ -1 +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) => ({ \"&\": \"&\", \"<\": \"<\", \">\": \">\", '\"': \""\", \"'\": \"'\" }[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 repoLocked: boolean;\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 repoLocked: false,\n sessionId: null,\n activeRunId: null,\n turns: [],\n isStreaming: false,\n };\n\n function repoSlug(url: string): string {\n const m = url.match(/github\\.com[/:]([^/]+)\\/([^/]+?)(?:\\.git)?\\/?$/);\n return m ? `${m[1]}/${m[2]}` : url;\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 ${state.repoLocked && state.repoUrl\n ? `<a class=\"coding-tab__repo-locked\" href=\"${escapeHtml(state.repoUrl)}\" target=\"_blank\" rel=\"noreferrer\" title=\"Locked to this app's repo (set by the server)\">${ICON_GITHUB}<span>${escapeHtml(repoSlug(state.repoUrl))}</span></a>`\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 if (me.defaultRepoUrl) {\n state.repoUrl = me.defaultRepoUrl;\n state.repoLocked = true;\n }\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","repoSlug","url","m","activeAbort","api","path","init","resp","render","headerHtml","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"]}
|
|
1
|
+
{"version":3,"sources":["../src/client/tab.ts","../src/client/markdown.ts"],"sourcesContent":["import { escapeHtml, renderMarkdown } from \"./markdown.js\";\nimport type {\n ChatListItem,\n ChatMode,\n FullChat,\n MeResponse,\n ModelChoice,\n ModelOption,\n PrInfo,\n StoredChat,\n StoredTurn,\n StreamEvent,\n TimelineEvent,\n TurnStatus,\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\n/** Client-side ephemeral wrapper around a `StoredChat`. */\ninterface ChatThread {\n meta: ChatListItem;\n /** Loaded turns (after `/chats/:id` fetch). Undefined means not yet hydrated. */\n turns?: StoredTurn[];\n loaded: boolean;\n isStreaming: boolean;\n activeRunId: string | null;\n abort: AbortController | null;\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\nconst ICON_PLUS = `<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>`;\nconst ICON_TRASH = `<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>`;\nconst ICON_PENCIL = `<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>`;\nconst ICON_MENU = `<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>`;\nconst ICON_CHEVRON = `<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>`;\n\nfunction escapeAttr(s: string): string {\n return escapeHtml(s);\n}\n\nfunction repoSlug(url: string): string {\n const m = url.match(/github\\.com[/:]([^/]+)\\/([^/]+?)(?:\\.git)?\\/?$/);\n return m ? `${m[1]}/${m[2]}` : url;\n}\n\nfunction cryptoRandomId(): string {\n return typeof crypto !== \"undefined\" && \"randomUUID\" in crypto\n ? crypto.randomUUID()\n : Math.random().toString(36).slice(2);\n}\n\nfunction relativeTime(ts: number): string {\n const diff = Date.now() - ts;\n if (diff < 60_000) return \"just now\";\n if (diff < 3_600_000) return `${Math.round(diff / 60_000)}m`;\n if (diff < 86_400_000) return `${Math.round(diff / 3_600_000)}h`;\n if (diff < 604_800_000) return `${Math.round(diff / 86_400_000)}d`;\n return new Date(ts).toLocaleDateString();\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 repoLocked: boolean;\n chats: ChatThread[];\n activeChatId: string | null;\n sidebarOpen: boolean;\n /** Tracks expanded tool events across re-renders (keyed by `event.id`). */\n expandedTools: Set<string>;\n } = {\n me: null,\n models: [],\n mode: options.defaultMode ?? \"plan\",\n model: options.defaultModel ?? \"sonnet\",\n repoUrl: options.defaultRepo ?? \"\",\n repoLocked: false,\n chats: [],\n activeChatId: null,\n sidebarOpen: window.innerWidth >= 720,\n expandedTools: new Set(),\n };\n\n function activeChat(): ChatThread | null {\n return state.chats.find((c) => c.meta.id === state.activeChatId) ?? null;\n }\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)\n throw Object.assign(new Error(`${resp.status} ${resp.statusText}`), { status: resp.status });\n return (await resp.json()) as T;\n }\n\n // ───────── chat lifecycle ─────────\n\n async function loadChatList(): Promise<void> {\n try {\n const { chats } = await api<{ chats: ChatListItem[] }>(\"/chats\");\n // Preserve runtime state for chats we already had.\n const existing = new Map(state.chats.map((c) => [c.meta.id, c]));\n state.chats = chats.map((meta) => {\n const prev = existing.get(meta.id);\n return prev\n ? { ...prev, meta }\n : {\n meta,\n loaded: false,\n isStreaming: false,\n activeRunId: null,\n abort: null,\n };\n });\n } catch (err) {\n console.error(\"[coding-tab] /chats list failed\", err);\n state.chats = [];\n }\n }\n\n async function ensureChatLoaded(chatId: string): Promise<ChatThread | null> {\n const t = state.chats.find((c) => c.meta.id === chatId);\n if (!t) return null;\n if (t.loaded) return t;\n try {\n const { chat } = await api<{ chat: FullChat }>(`/chats/${chatId}`);\n t.turns = chat.turns;\n t.loaded = true;\n t.meta = {\n id: chat.id,\n title: chat.title,\n mode: chat.mode,\n model: chat.model,\n createdAt: chat.createdAt,\n updatedAt: chat.updatedAt,\n };\n // Sync composer state to whatever this chat last used.\n state.mode = chat.mode;\n state.model = chat.model;\n return t;\n } catch (err) {\n console.error(`[coding-tab] /chats/${chatId} load failed`, err);\n return null;\n }\n }\n\n async function createChat(): Promise<ChatThread | null> {\n try {\n const { chat } = await api<{ chat: StoredChat }>(\"/chats\", {\n method: \"POST\",\n body: JSON.stringify({\n mode: state.mode,\n model: state.model,\n repoUrl: state.repoLocked ? undefined : state.repoUrl || undefined,\n }),\n });\n const thread: ChatThread = {\n meta: {\n id: chat.id,\n title: chat.title,\n mode: chat.mode,\n model: chat.model,\n createdAt: chat.createdAt,\n updatedAt: chat.updatedAt,\n },\n turns: [],\n loaded: true,\n isStreaming: false,\n activeRunId: null,\n abort: null,\n };\n state.chats.unshift(thread);\n state.activeChatId = thread.meta.id;\n return thread;\n } catch (err) {\n console.error(\"[coding-tab] create chat failed\", err);\n return null;\n }\n }\n\n async function switchChat(chatId: string): Promise<void> {\n state.activeChatId = chatId;\n if (window.innerWidth < 720) state.sidebarOpen = false;\n render();\n await ensureChatLoaded(chatId);\n render();\n }\n\n async function deleteChat(chatId: string): Promise<void> {\n const thread = state.chats.find((c) => c.meta.id === chatId);\n if (!thread) return;\n if (!confirm(`Delete chat \"${thread.meta.title}\"? This cannot be undone.`)) return;\n thread.abort?.abort();\n try {\n await api(`/chats/${chatId}`, { method: \"DELETE\" });\n } catch (err) {\n console.error(`[coding-tab] delete chat ${chatId} failed`, err);\n return;\n }\n state.chats = state.chats.filter((c) => c.meta.id !== chatId);\n if (state.activeChatId === chatId) {\n state.activeChatId = state.chats[0]?.meta.id ?? null;\n }\n render();\n }\n\n async function renameChat(chatId: string): Promise<void> {\n const thread = state.chats.find((c) => c.meta.id === chatId);\n if (!thread) return;\n const next = prompt(\"Rename chat\", thread.meta.title);\n if (next === null) return;\n const trimmed = next.trim();\n if (!trimmed || trimmed === thread.meta.title) return;\n try {\n const { chat } = await api<{ chat: StoredChat }>(`/chats/${chatId}`, {\n method: \"PATCH\",\n body: JSON.stringify({ title: trimmed }),\n });\n thread.meta = { ...thread.meta, title: chat.title, updatedAt: chat.updatedAt };\n render();\n } catch (err) {\n console.error(`[coding-tab] rename chat ${chatId} failed`, err);\n }\n }\n\n // ───────── render ─────────\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 <button class=\"coding-tab__hamburger\" data-role=\"toggle-sidebar\" aria-label=\"Toggle chat list\">${ICON_MENU}</button>\n <select data-role=\"model\" title=\"Model\">\n ${state.models\n .map(\n (m) =>\n `<option value=\"${m.choice}\" ${state.model === m.choice ? \"selected\" : \"\"}>${escapeHtml(m.displayName)}</option>`,\n )\n .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 ${\n state.repoLocked && state.repoUrl\n ? `<a class=\"coding-tab__repo-locked\" href=\"${escapeAttr(state.repoUrl)}\" target=\"_blank\" rel=\"noreferrer\" title=\"Locked to this app's repo (set by the server)\">${ICON_GITHUB}<span>${escapeHtml(repoSlug(state.repoUrl))}</span></a>`\n : `<input data-role=\"repo\" placeholder=\"https://github.com/org/repo\" value=\"${escapeAttr(state.repoUrl)}\" style=\"flex:1;min-width:200px\" />`\n }\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 sidebarHtml = `\n <aside class=\"coding-tab__sidebar ${state.sidebarOpen ? \"is-open\" : \"\"}\">\n <button class=\"coding-tab__btn primary coding-tab__new-chat\" data-role=\"new-chat\">${ICON_PLUS}<span>New chat</span></button>\n <div class=\"coding-tab__chat-list\">\n ${\n state.chats.length === 0\n ? `<div class=\"coding-tab__chat-empty\">No chats yet — start one with the button above.</div>`\n : state.chats.map((c) => renderChatRow(c)).join(\"\")\n }\n </div>\n </aside>\n `;\n\n const chat = activeChat();\n const paneHtml = `\n <main class=\"coding-tab__pane\">\n ${chat ? renderChatPane(chat) : renderEmptyPane()}\n </main>\n `;\n\n root.innerHTML = `\n ${headerHtml}\n <div class=\"coding-tab__body\">\n ${sidebarHtml}\n ${paneHtml}\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 renderChatRow(c: ChatThread): string {\n const active = c.meta.id === state.activeChatId ? \" is-active\" : \"\";\n const streaming = c.isStreaming ? `<span class=\"coding-tab__chat-row-dot\" title=\"Streaming\"></span>` : \"\";\n return `\n <div class=\"coding-tab__chat-row${active}\" data-chat-id=\"${c.meta.id}\">\n <button class=\"coding-tab__chat-row-main\" data-role=\"select-chat\" data-chat-id=\"${c.meta.id}\">\n ${streaming}\n <span class=\"coding-tab__chat-row-title\">${escapeHtml(c.meta.title)}</span>\n <span class=\"coding-tab__chat-row-meta\">${c.meta.mode === \"plan\" ? \"Plan\" : \"Agent\"} · ${relativeTime(c.meta.updatedAt)}</span>\n </button>\n <div class=\"coding-tab__chat-row-actions\">\n <button data-role=\"rename-chat\" data-chat-id=\"${c.meta.id}\" title=\"Rename\">${ICON_PENCIL}</button>\n <button data-role=\"delete-chat\" data-chat-id=\"${c.meta.id}\" title=\"Delete\">${ICON_TRASH}</button>\n </div>\n </div>\n `;\n }\n\n function renderEmptyPane(): string {\n return `\n <div class=\"coding-tab__pane-empty\">\n <h3>Start a new chat</h3>\n <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>\n <button class=\"coding-tab__btn primary\" data-role=\"new-chat\">${ICON_PLUS}<span>New chat</span></button>\n </div>\n `;\n }\n\n function renderChatPane(chat: ChatThread): string {\n const turns = chat.turns ?? [];\n const threadHtml =\n !chat.loaded\n ? `<div class=\"coding-tab__notice info\">Loading…</div>`\n : turns.length === 0\n ? `<div class=\"coding-tab__notice info\">Type a message below to kick off this chat.</div>`\n : turns.map((t) => renderTurn(t)).join(\"\");\n\n const placeholder =\n turns.length === 0\n ? `Ask anything (e.g. \"add dark mode that follows the OS setting\")`\n : state.mode === \"plan\"\n ? \"Refine the plan, or ask another planning question\"\n : \"Send another instruction\";\n\n return `\n <div class=\"coding-tab__thread\" data-role=\"thread\">${threadHtml}</div>\n <div class=\"coding-tab__composer\">\n <textarea data-role=\"prompt\" placeholder=\"${escapeAttr(placeholder)}\" rows=\"2\"></textarea>\n <button class=\"coding-tab__btn primary\" data-role=\"send\" ${chat.isStreaming ? \"disabled\" : \"\"}>${chat.isStreaming ? \"Working…\" : \"Send\"}</button>\n ${chat.isStreaming ? `<button class=\"coding-tab__btn danger\" data-role=\"cancel\">Stop</button>` : \"\"}\n </div>\n `;\n }\n\n function renderTurn(turn: StoredTurn): string {\n if (turn.role === \"user\") {\n const text = turn.prompt ?? extractText(turn);\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(text)}</div>\n </div>`;\n }\n\n const eventsHtml =\n turn.events.length === 0 && turn.status === \"running\"\n ? `<div class=\"coding-tab__msg-pending\">Working…</div>`\n : turn.events.map((evt) => renderEvent(evt, turn.isPlan)).join(\"\");\n\n const statusBadge = turn.status && turn.status !== \"finished\"\n ? `<div class=\"coding-tab__status\">${escapeHtml(turn.status)}</div>`\n : \"\";\n\n const planActions =\n turn.showExecute && !activeChat()?.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 : \"\";\n\n const prHtml = turn.pr ? renderPr(turn.pr) : \"\";\n\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\">${eventsHtml}${planActions}${prHtml}</div>\n </div>`;\n }\n\n function renderEvent(evt: TimelineEvent, isPlan: boolean): string {\n if (evt.kind === \"text\") {\n const text = evt.text.trim();\n if (!text) return \"\";\n const html = isPlan ? renderMarkdown(text) : renderInlineText(text);\n return `<div class=\"coding-tab__md\">${html}</div>`;\n }\n return renderToolEvent(evt);\n }\n\n function renderInlineText(text: string): string {\n // Lightweight rendering for Agent-mode messages: keep paragraph breaks,\n // inline `code` and **bold**, but skip headings/lists so the model's casual\n // streaming output doesn't get over-formatted.\n return escapeHtml(text)\n .split(/\\n\\s*\\n/)\n .map((para) =>\n para\n .replace(/`([^`\\n]+)`/g, \"<code>$1</code>\")\n .replace(/\\*\\*([^*\\n]+)\\*\\*/g, \"<strong>$1</strong>\")\n .replace(/\\n/g, \"<br />\"),\n )\n .map((p) => `<p>${p}</p>`)\n .join(\"\");\n }\n\n function renderToolEvent(\n evt: Extract<TimelineEvent, { kind: \"tool\" }>,\n ): string {\n const summary = summarizeTool(evt.name, evt.args);\n const expanded = state.expandedTools.has(evt.id);\n const detailHtml = expanded ? renderToolDetail(evt) : \"\";\n return `\n <div class=\"coding-tab__tool\" data-status=\"${evt.status}\" data-tool-id=\"${evt.id}\">\n <button class=\"coding-tab__tool-header\" data-role=\"toggle-tool\" data-tool-id=\"${evt.id}\" aria-expanded=\"${expanded}\">\n <span class=\"coding-tab__tool-dot\"></span>\n ${ICON_CHEVRON}\n <span class=\"coding-tab__tool-name\">${escapeHtml(evt.name)}</span>\n ${summary ? `<span class=\"coding-tab__tool-summary\">${escapeHtml(summary)}</span>` : \"\"}\n </button>\n ${detailHtml}\n </div>\n `;\n }\n\n function summarizeTool(name: string, args: unknown): string {\n if (!args || typeof args !== \"object\") return \"\";\n const a = args as Record<string, unknown>;\n const lower = name.toLowerCase();\n if (lower.includes(\"grep\")) {\n const pattern = typeof a.pattern === \"string\" ? a.pattern : typeof a.query === \"string\" ? a.query : \"\";\n return pattern ? `\"${pattern}\"` : \"\";\n }\n if (lower.includes(\"read\") && typeof a.path === \"string\") {\n const lines =\n typeof a.offset === \"number\" || typeof a.limit === \"number\"\n ? `:${a.offset ?? \"\"}-${(Number(a.offset ?? 0) + Number(a.limit ?? 0)) || \"\"}`\n : \"\";\n return `${a.path}${lines}`;\n }\n if ((lower.includes(\"glob\") || lower.includes(\"file_search\")) && typeof a.glob_pattern === \"string\") {\n return a.glob_pattern as string;\n }\n if (lower === \"shell\" && typeof a.command === \"string\") {\n return (a.command as string).slice(0, 80);\n }\n if (lower === \"task\" && typeof a.description === \"string\") {\n return a.description as string;\n }\n if (lower.includes(\"write\") && typeof a.path === \"string\") {\n return a.path as string;\n }\n if (lower.includes(\"edit\") && typeof a.path === \"string\") {\n return a.path as string;\n }\n if (lower.includes(\"delete\") && typeof a.path === \"string\") {\n return a.path as string;\n }\n return \"\";\n }\n\n function renderToolDetail(evt: Extract<TimelineEvent, { kind: \"tool\" }>): string {\n const argsBlock =\n evt.args !== undefined && evt.args !== null\n ? `<div class=\"coding-tab__tool-section\"><div class=\"coding-tab__tool-label\">Args</div><pre>${escapeHtml(formatBlob(evt.args))}</pre></div>`\n : \"\";\n const resultBlock =\n evt.result !== undefined && evt.result !== null\n ? `<div class=\"coding-tab__tool-section\"><div class=\"coding-tab__tool-label\">Result</div><pre>${escapeHtml(formatBlob(evt.result))}</pre></div>`\n : evt.status === \"running\"\n ? `<div class=\"coding-tab__tool-section\"><div class=\"coding-tab__tool-label\">Result</div><pre class=\"coding-tab__tool-pending\">…running</pre></div>`\n : \"\";\n return `<div class=\"coding-tab__tool-detail\">${argsBlock}${resultBlock}</div>`;\n }\n\n function formatBlob(value: unknown): string {\n if (typeof value === \"string\") return value;\n try {\n return JSON.stringify(value, null, 2);\n } catch {\n return String(value);\n }\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=\"${escapeAttr(pr.url)}\" target=\"_blank\" rel=\"noreferrer\">Open in GitHub</a>\n <button class=\"coding-tab__btn primary\" data-role=\"merge\" data-pr=\"${escapeAttr(pr.url)}\">Merge & Redeploy</button>\n </div>\n </div>`;\n }\n\n function extractText(turn: StoredTurn): string {\n return turn.events\n .filter((e): e is Extract<TimelineEvent, { kind: \"text\" }> => e.kind === \"text\")\n .map((e) => e.text)\n .join(\"\\n\\n\");\n }\n\n // ───────── events ─────────\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 for (const c of state.chats) c.abort?.abort();\n state.me = null;\n state.chats = [];\n state.activeChatId = null;\n render();\n });\n root.querySelector(`[data-role=\"toggle-sidebar\"]`)?.addEventListener(\"click\", () => {\n state.sidebarOpen = !state.sidebarOpen;\n render();\n });\n\n root.querySelectorAll(`[data-role=\"new-chat\"]`).forEach((b) =>\n b.addEventListener(\"click\", async () => {\n const t = await createChat();\n if (t && window.innerWidth < 720) state.sidebarOpen = false;\n render();\n }),\n );\n root.querySelectorAll(`[data-role=\"select-chat\"]`).forEach((b) =>\n b.addEventListener(\"click\", () => {\n const id = (b as HTMLElement).dataset.chatId!;\n switchChat(id);\n }),\n );\n root.querySelectorAll(`[data-role=\"rename-chat\"]`).forEach((b) =>\n b.addEventListener(\"click\", (e) => {\n e.stopPropagation();\n renameChat((b as HTMLElement).dataset.chatId!);\n }),\n );\n root.querySelectorAll(`[data-role=\"delete-chat\"]`).forEach((b) =>\n b.addEventListener(\"click\", (e) => {\n e.stopPropagation();\n deleteChat((b as HTMLElement).dataset.chatId!);\n }),\n );\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 root.querySelectorAll(`[data-role=\"toggle-tool\"]`).forEach((b) =>\n b.addEventListener(\"click\", () => {\n const id = (b as HTMLElement).dataset.toolId!;\n if (state.expandedTools.has(id)) state.expandedTools.delete(id);\n else state.expandedTools.add(id);\n render();\n }),\n );\n }\n\n // ───────── send / execute / cancel / merge ─────────\n\n async function onSend() {\n let chat = activeChat();\n if (!chat) {\n chat = await createChat();\n if (!chat) return;\n }\n if (chat.isStreaming) return;\n\n const promptEl = root.querySelector(`[data-role=\"prompt\"]`) as HTMLTextAreaElement | null;\n const prompt = promptEl?.value.trim() ?? \"\";\n if (!prompt) return;\n if (promptEl) promptEl.value = \"\";\n\n chat.turns = chat.turns ?? [];\n const userTurn: StoredTurn = {\n id: cryptoRandomId(),\n chatId: chat.meta.id,\n seq: chat.turns.length,\n role: \"user\",\n isPlan: state.mode === \"plan\",\n status: \"finished\",\n events: [{ kind: \"text\", id: cryptoRandomId(), text: prompt }],\n prompt,\n createdAt: Date.now(),\n };\n chat.turns.push(userTurn);\n\n const assistantTurn: StoredTurn = {\n id: cryptoRandomId(),\n chatId: chat.meta.id,\n seq: chat.turns.length,\n role: \"assistant\",\n isPlan: state.mode === \"plan\",\n status: \"running\",\n events: [],\n createdAt: Date.now(),\n };\n chat.turns.push(assistantTurn);\n chat.isStreaming = true;\n if (chat.meta.title === \"New chat\") {\n chat.meta = { ...chat.meta, title: prompt.length > 60 ? `${prompt.slice(0, 57)}…` : prompt };\n }\n chat.meta = { ...chat.meta, mode: state.mode, model: state.model, updatedAt: Date.now() };\n render();\n\n try {\n await streamSse(\n \"/agent/send\",\n {\n chatId: chat.meta.id,\n prompt,\n mode: state.mode,\n },\n chat,\n assistantTurn,\n );\n } catch (err) {\n if ((err as Error).name !== \"AbortError\") {\n appendErrorToTurn(assistantTurn, err);\n }\n assistantTurn.status = \"error\";\n } finally {\n chat.isStreaming = false;\n chat.activeRunId = null;\n chat.abort = null;\n render();\n }\n }\n\n async function onExecute() {\n const chat = activeChat();\n if (!chat || chat.isStreaming) return;\n chat.turns = chat.turns ?? [];\n const turn: StoredTurn = {\n id: cryptoRandomId(),\n chatId: chat.meta.id,\n seq: chat.turns.length,\n role: \"assistant\",\n isPlan: false,\n status: \"running\",\n events: [],\n createdAt: Date.now(),\n };\n chat.turns.push(turn);\n chat.isStreaming = true;\n render();\n try {\n await streamSse(\"/agent/execute\", { chatId: chat.meta.id }, chat, turn);\n } catch (err) {\n if ((err as Error).name !== \"AbortError\") {\n appendErrorToTurn(turn, err);\n }\n turn.status = \"error\";\n } finally {\n chat.isStreaming = false;\n chat.activeRunId = null;\n chat.abort = null;\n render();\n }\n }\n\n async function onCancel() {\n const chat = activeChat();\n if (!chat) return;\n if (chat.activeRunId) {\n try {\n await api(\"/agent/cancel\", {\n method: \"POST\",\n body: JSON.stringify({ chatId: chat.meta.id, runId: chat.activeRunId }),\n });\n } catch (err) {\n console.error(\"[coding-tab] cancel failed\", err);\n }\n }\n chat.abort?.abort();\n }\n\n async function onMerge(prUrl: string) {\n const chat = activeChat();\n if (!chat) return;\n chat.turns = chat.turns ?? [];\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 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 chat.turns.push({\n id: cryptoRandomId(),\n chatId: chat.meta.id,\n seq: chat.turns.length,\n role: \"assistant\",\n isPlan: false,\n status: result.merged ? \"finished\" : \"error\",\n events: [{ kind: \"text\", id: cryptoRandomId(), text }],\n createdAt: Date.now(),\n });\n } catch (err) {\n const message = err instanceof Error ? err.message : String(err);\n chat.turns.push({\n id: cryptoRandomId(),\n chatId: chat.meta.id,\n seq: chat.turns.length,\n role: \"assistant\",\n isPlan: false,\n status: \"error\",\n events: [{ kind: \"text\", id: cryptoRandomId(), text: `[merge failed] ${message}` }],\n createdAt: Date.now(),\n });\n } finally {\n render();\n }\n }\n\n function appendErrorToTurn(turn: StoredTurn, err: unknown): void {\n const message = err instanceof Error ? err.message : String(err);\n turn.events.push({\n kind: \"text\",\n id: cryptoRandomId(),\n text: `[error] ${message}`,\n });\n }\n\n // ───────── streaming ─────────\n\n async function streamSse(\n path: string,\n body: unknown,\n chat: ChatThread,\n turn: StoredTurn,\n ): Promise<void> {\n chat.abort = 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: chat.abort.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 /** Track whether the last applied event was a `text` event so we know\n * whether to merge subsequent text deltas into the same paragraph. */\n let lastWasText = false;\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 raw of events) {\n const dataLine = raw.split(\"\\n\").find((l) => l.startsWith(\"data: \"));\n if (!dataLine) continue;\n try {\n const evt = JSON.parse(dataLine.slice(6)) as StreamEvent;\n lastWasText = applyStreamEvent(evt, chat, turn, lastWasText);\n render();\n } catch (e) {\n console.warn(\"[coding-tab] bad sse event\", e);\n }\n }\n }\n }\n\n /**\n * Apply an SSE event into the active turn's timeline. Returns whether the\n * last applied event was a streaming text event (so the next text chunk can\n * be merged with it).\n */\n function applyStreamEvent(\n evt: StreamEvent,\n chat: ChatThread,\n turn: StoredTurn,\n lastWasText: boolean,\n ): boolean {\n switch (evt.kind) {\n case \"ready\":\n chat.activeRunId = evt.runId;\n return false;\n case \"text\": {\n // Each `text` SSE corresponds to one logical block from the model.\n // Within a single render burst the server sends one event per block,\n // so we always start a new paragraph here. (If the SDK ever streams\n // sub-block deltas, the server-side TurnBuffer will have already\n // coalesced them into one event for us.)\n const last = turn.events[turn.events.length - 1];\n if (lastWasText && last && last.kind === \"text\") {\n // Same delta train — append.\n last.text += evt.text;\n } else {\n turn.events.push({\n kind: \"text\",\n id: cryptoRandomId(),\n text: evt.text,\n });\n }\n return true;\n }\n case \"thinking\":\n return lastWasText;\n case \"tool\": {\n const existing = turn.events.find(\n (e): e is Extract<TimelineEvent, { kind: \"tool\" }> =>\n e.kind === \"tool\" && e.callId === evt.callId,\n );\n if (existing) {\n existing.status = evt.status;\n if (evt.args !== undefined) existing.args = evt.args;\n if (evt.result !== undefined) existing.result = evt.result;\n } else {\n turn.events.push({\n kind: \"tool\",\n id: cryptoRandomId(),\n callId: evt.callId,\n name: evt.name,\n status: evt.status,\n args: evt.args,\n result: evt.result,\n });\n }\n return false;\n }\n case \"status\":\n return lastWasText;\n case \"result\":\n turn.status = evt.status as TurnStatus;\n if (evt.pr) turn.pr = evt.pr;\n if (turn.isPlan && evt.status === \"finished\") turn.showExecute = true;\n chat.activeRunId = null;\n return false;\n case \"error\":\n turn.events.push({\n kind: \"text\",\n id: cryptoRandomId(),\n text: `[error] ${evt.message}`,\n });\n turn.status = \"error\";\n chat.activeRunId = null;\n return false;\n }\n }\n\n // ───────── bootstrap ─────────\n\n async function bootstrap() {\n try {\n const me = await api<MeResponse>(\"/auth/me\");\n state.me = me;\n if (me.defaultRepoUrl) {\n state.repoUrl = me.defaultRepoUrl;\n state.repoLocked = true;\n }\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 await loadChatList();\n if (state.chats.length > 0 && !state.activeChatId) {\n state.activeChatId = state.chats[0]!.meta.id;\n ensureChatLoaded(state.activeChatId).then(() => render());\n }\n render();\n }\n\n bootstrap();\n\n return {\n destroy() {\n for (const c of state.chats) c.abort?.abort();\n el.removeChild(root);\n },\n };\n}\n\nif (typeof window !== \"undefined\") {\n (window as unknown as { CodingTab?: { mountCodingTab: typeof mountCodingTab } }).CodingTab = {\n mountCodingTab,\n };\n}\n","/**\n * Tiny zero-dependency markdown renderer used for Plan-mode assistant turns.\n *\n * Supports headings (`#`/`##`/`###`/`####`), unordered (`-`/`*`) and ordered\n * (`1.`) lists, fenced code blocks, inline `code`, `**bold**`, `*italic*`,\n * `[link](url)`, horizontal rules (`---`), paragraphs, and hard breaks.\n *\n * All text is HTML-escaped before any markdown substitutions are applied so\n * the output is safe to inject via `innerHTML` even when the upstream model\n * dumps unsanitised user input back to us.\n */\n\nexport function escapeHtml(s: string): string {\n return s.replace(/[&<>\"']/g, (c) =>\n (\n {\n \"&\": \"&\",\n \"<\": \"<\",\n \">\": \">\",\n '\"': \""\",\n \"'\": \"'\",\n } as Record<string, string>\n )[c]!,\n );\n}\n\nfunction applyInline(text: string): string {\n // Inline code first — anything inside backticks should be inert.\n const segments: string[] = [];\n const codeRe = /`([^`\\n]+)`/g;\n let lastIndex = 0;\n let m: RegExpExecArray | null;\n while ((m = codeRe.exec(text)) !== null) {\n segments.push(applyInlineRest(text.slice(lastIndex, m.index)));\n segments.push(`<code>${m[1]!}</code>`);\n lastIndex = m.index + m[0].length;\n }\n segments.push(applyInlineRest(text.slice(lastIndex)));\n return segments.join(\"\");\n}\n\nfunction applyInlineRest(text: string): string {\n return text\n .replace(/\\*\\*([^*\\n]+)\\*\\*/g, \"<strong>$1</strong>\")\n .replace(/(^|[^*])\\*([^*\\n]+)\\*(?!\\*)/g, \"$1<em>$2</em>\")\n .replace(/\\[([^\\]\\n]+)\\]\\(([^)\\s]+)\\)/g, (_match, label: string, url: string) => {\n const safeUrl = /^(https?:|mailto:|\\/|#)/i.test(url) ? url : \"#\";\n return `<a href=\"${safeUrl}\" target=\"_blank\" rel=\"noreferrer noopener\">${label}</a>`;\n });\n}\n\ninterface ListContext {\n type: \"ul\" | \"ol\";\n indent: number;\n items: string[];\n}\n\nfunction flushParagraph(buf: string[], out: string[]) {\n if (buf.length === 0) return;\n const joined = buf.join(\" \").trim();\n buf.length = 0;\n if (!joined) return;\n out.push(`<p>${applyInline(joined)}</p>`);\n}\n\nfunction flushList(stack: ListContext[], out: string[]) {\n while (stack.length > 0) {\n const ctx = stack.pop()!;\n out.push(`<${ctx.type}>${ctx.items.join(\"\")}</${ctx.type}>`);\n }\n}\n\nexport function renderMarkdown(input: string): string {\n // Escape first so list markers / fences are still recognised but any HTML in\n // the source becomes inert.\n const escaped = escapeHtml(input).replace(/\\r\\n/g, \"\\n\");\n const lines = escaped.split(\"\\n\");\n\n const out: string[] = [];\n const paragraph: string[] = [];\n const listStack: ListContext[] = [];\n let inCode = false;\n let codeLang = \"\";\n const codeBuf: string[] = [];\n\n const closeOpenBlocks = () => {\n flushParagraph(paragraph, out);\n flushList(listStack, out);\n };\n\n for (let i = 0; i < lines.length; i++) {\n const line = lines[i]!;\n\n // Fenced code blocks\n const fence = line.match(/^\\s*```(\\w*)\\s*$/);\n if (fence) {\n if (inCode) {\n const cls = codeLang ? ` class=\"lang-${codeLang}\"` : \"\";\n out.push(`<pre><code${cls}>${codeBuf.join(\"\\n\")}</code></pre>`);\n codeBuf.length = 0;\n codeLang = \"\";\n inCode = false;\n } else {\n closeOpenBlocks();\n inCode = true;\n codeLang = fence[1] ?? \"\";\n }\n continue;\n }\n if (inCode) {\n codeBuf.push(line);\n continue;\n }\n\n // Blank line: paragraph break, list break.\n if (!line.trim()) {\n closeOpenBlocks();\n continue;\n }\n\n // Horizontal rule.\n if (/^\\s*(-{3,}|\\*{3,}|_{3,})\\s*$/.test(line)) {\n closeOpenBlocks();\n out.push(\"<hr />\");\n continue;\n }\n\n // Headings.\n const h = line.match(/^(#{1,6})\\s+(.*)$/);\n if (h) {\n closeOpenBlocks();\n const level = Math.min(6, h[1]!.length);\n out.push(`<h${level}>${applyInline(h[2]!.trim())}</h${level}>`);\n continue;\n }\n\n // List items.\n const ul = line.match(/^(\\s*)[-*]\\s+(.*)$/);\n const ol = line.match(/^(\\s*)(\\d+)\\.\\s+(.*)$/);\n if (ul || ol) {\n flushParagraph(paragraph, out);\n const indent = (ul ? ul[1] : ol![1])!.length;\n const type: \"ul\" | \"ol\" = ul ? \"ul\" : \"ol\";\n const content = (ul ? ul[2] : ol![3])!;\n // Pop deeper lists.\n while (listStack.length > 0 && listStack[listStack.length - 1]!.indent > indent) {\n const ctx = listStack.pop()!;\n const prev = listStack[listStack.length - 1]?.items;\n const html = `<${ctx.type}>${ctx.items.join(\"\")}</${ctx.type}>`;\n if (prev && prev.length > 0) {\n prev[prev.length - 1] = prev[prev.length - 1]!.replace(\n /<\\/li>$/,\n `${html}</li>`,\n );\n } else {\n out.push(html);\n }\n }\n const top = listStack[listStack.length - 1];\n if (!top || top.indent < indent || top.type !== type) {\n listStack.push({ type, indent, items: [`<li>${applyInline(content)}</li>`] });\n } else {\n top.items.push(`<li>${applyInline(content)}</li>`);\n }\n continue;\n }\n\n // If we were in a list and hit a non-list line, close the list.\n if (listStack.length > 0) {\n flushList(listStack, out);\n }\n paragraph.push(line.trim());\n }\n\n if (inCode) {\n out.push(`<pre><code>${codeBuf.join(\"\\n\")}</code></pre>`);\n } else {\n closeOpenBlocks();\n }\n\n return out.join(\"\\n\");\n}\n"],"mappings":"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","names":["tab_exports","__export","mountCodingTab","escapeHtml","s","c","applyInline","text","segments","codeRe","lastIndex","m","applyInlineRest","_match","label","url","flushParagraph","buf","out","joined","flushList","stack","ctx","renderMarkdown","input","lines","paragraph","listStack","inCode","codeLang","codeBuf","closeOpenBlocks","i","line","fence","cls","h","level","ul","ol","indent","type","content","prev","html","top","ICON_GITHUB","ICON_PLUS","ICON_TRASH","ICON_PENCIL","ICON_MENU","ICON_CHEVRON","escapeAttr","s","escapeHtml","repoSlug","url","m","cryptoRandomId","relativeTime","ts","diff","mountCodingTab","el","options","apiBase","root","stylesheetInjected","injectStylesheet","link","state","activeChat","c","api","path","init","resp","loadChatList","chats","existing","meta","prev","err","ensureChatLoaded","chatId","t","chat","createChat","thread","switchChat","render","deleteChat","renameChat","next","trimmed","headerHtml","sidebarHtml","renderChatRow","paneHtml","renderChatPane","renderEmptyPane","bindEvents","active","streaming","turns","threadHtml","renderTurn","placeholder","turn","text","extractText","eventsHtml","evt","renderEvent","statusBadge","planActions","prHtml","renderPr","isPlan","renderMarkdown","renderInlineText","renderToolEvent","para","p","summary","summarizeTool","expanded","detailHtml","renderToolDetail","name","args","a","lower","pattern","lines","argsBlock","formatBlob","resultBlock","value","pr","e","b","id","onSend","ev","onCancel","onExecute","onMerge","promptEl","prompt","userTurn","assistantTurn","streamSse","appendErrorToTurn","prUrl","result","message","body","reader","decoder","buffer","lastWasText","done","events","raw","dataLine","l","applyStreamEvent","last","bootstrap","me","status","models"]}
|