1 // @ts-check
2 (function () {
3 const vscode = acquireVsCodeApi();
4
5 const messagesEl = document.getElementById("messages");
6 const statusEl = document.getElementById("status");
7 const formEl = document.getElementById("composer");
8 const inputEl = /** @type {HTMLTextAreaElement} */ (document.getElementById("input"));
9 const sendEl = /** @type {HTMLButtonElement} */ (document.getElementById("send"));
10
11 // The assistant streams in chunks; keep a handle to the current bubble so we
12 // can append to it rather than creating a new one per chunk.
13 let currentAssistant = null;
14 let busy = false;
15
16 // Tool calls arrive as tool_call -> tool_call_update sharing one toolCallId.
17 // Track each row by id so updates mutate it in place instead of stacking.
18 const toolEls = new Map();
19
20 function scrollToBottom() {
21 messagesEl.scrollTop = messagesEl.scrollHeight;
22 }
23
24 function addMessage(role, text) {
25 const el = document.createElement("div");
26 el.className = "message message-" + role;
27 const body = document.createElement("div");
28 body.className = "bubble";
29 body.textContent = text;
30 el.appendChild(body);
31 messagesEl.appendChild(el);
32 scrollToBottom();
33 return body;
34 }
35
36 function appendAssistant(text) {
37 if (!currentAssistant) {
38 currentAssistant = addMessage("assistant", "");
39 }
40 currentAssistant.textContent += text;
41 scrollToBottom();
42 }
43
44 function endAssistant() {
45 currentAssistant = null;
46 }
47
48 function parsePercent(title) {
49 const m = /\((\d+)%\)/.exec(title || "");
50 if (!m) {
51 return null;
52 }
53 const n = parseInt(m[1], 10);
54 return isNaN(n) ? null : Math.max(0, Math.min(100, n));
55 }
56
57 function stripPercent(title) {
58 return (title || "").replace(/\s*\(\d+%\)/, "").trim();
59 }
60
61 // Render (or update in place) a tool-call row keyed by toolCallId. A title
62 // like "Downloading … (42%)" is shown as a label plus a progress bar.
63 function renderTool(message) {
64 const key = message.toolCallId || null;
65 let entry = key ? toolEls.get(key) : null;
66
67 if (!entry) {
68 const el = document.createElement("div");
69 el.className = "message message-tool";
70 const bubble = document.createElement("div");
71 bubble.className = "bubble";
72 const labelEl = document.createElement("div");
73 labelEl.className = "tool-label";
74 const progress = document.createElement("div");
75 progress.className = "tool-progress";
76 progress.hidden = true;
77 const bar = document.createElement("div");
78 bar.className = "tool-progress-bar";
79 progress.appendChild(bar);
80 bubble.appendChild(labelEl);
81 bubble.appendChild(progress);
82 el.appendChild(bubble);
83 messagesEl.appendChild(el);
84 entry = { el, labelEl, progress, bar, title: "", status: "" };
85 if (key) {
86 toolEls.set(key, entry);
87 }
88 }
89
90 // Updates may omit fields; only overwrite what we were given.
91 if (typeof message.title === "string" && message.title) {
92 entry.title = message.title;
93 }
94 if (typeof message.status === "string" && message.status) {
95 entry.status = message.status;
96 }
97
98 const pct = parsePercent(entry.title);
99 const text = (pct === null ? entry.title : stripPercent(entry.title)) || "tool";
100 entry.labelEl.textContent = text + (entry.status ? "" + entry.status : "");
101 entry.el.className = "message message-tool" + (entry.status ? " tool-" + entry.status : "");
102
103 if (pct === null) {
104 entry.progress.hidden = true;
105 } else {
106 entry.progress.hidden = false;
107 entry.bar.style.width = pct + "%";
108 }
109 scrollToBottom();
110 }
111
112 function setStatus(text) {
113 statusEl.textContent = text;
114 }
115
116 function setBusy(value) {
117 busy = value;
118 sendEl.disabled = value;
119 sendEl.textContent = value ? "" : "Send";
120 }
121
122 function send() {
123 const text = inputEl.value.trim();
124 if (!text || busy) {
125 return;
126 }
127 endAssistant();
128 vscode.postMessage({ type: "prompt", text });
129 inputEl.value = "";
130 }
131
132 formEl.addEventListener("submit", (e) => {
133 e.preventDefault();
134 send();
135 });
136
137 inputEl.addEventListener("keydown", (e) => {
138 // Enter sends; Shift+Enter inserts a newline.
139 if (e.key === "Enter" && !e.shiftKey) {
140 e.preventDefault();
141 send();
142 }
143 });
144
145 window.addEventListener("message", (event) => {
146 const message = event.data;
147 switch (message.type) {
148 case "user":
149 endAssistant();
150 addMessage("user", message.text);
151 break;
152 case "assistant":
153 appendAssistant(message.text || "");
154 break;
155 case "thought":
156 addMessage("thought", message.text || "");
157 break;
158 case "tool":
159 renderTool(message);
160 break;
161 case "status":
162 setStatus(message.text || "");
163 break;
164 case "error":
165 endAssistant();
166 addMessage("error", message.text || "Error");
167 break;
168 case "log":
169 // Agent stderr — surface quietly in the status line.
170 if (message.text) {
171 setStatus(String(message.text).trim().split("\n").pop() || "");
172 }
173 break;
174 case "busy":
175 setBusy(!!message.busy);
176 break;
177 case "turnEnd":
178 endAssistant();
179 setBusy(false);
180 break;
181 case "clear":
182 messagesEl.innerHTML = "";
183 toolEls.clear();
184 endAssistant();
185 break;
186 default:
187 break;
188 }
189 });
190
191 vscode.postMessage({ type: "ready" });
192 })();