Forge
javascript3407750f
1(function () {
2 "use strict";
3
4 var domainCatalog = [];
5 var formatCatalog = [
6 { id: "h1", path: "/h1", help: "Heading 1", category: "Format", insert: "# " },
7 { id: "h2", path: "/h2", help: "Heading 2", category: "Format", insert: "## " },
8 { id: "bul", path: "/bul", help: "Bullet list", category: "Format", insert: "- " },
9 { id: "bold", path: "/bold", help: "Bold selection", category: "Format", wrap: ["**", "**"] },
10 { id: "code", path: "/code", help: "Inline code", category: "Format", wrap: ["`", "`"] },
11 { id: "quote", path: "/quote", help: "Block quote", category: "Format", insert: "> " },
12 ];
13
14 var tierOrder = { core: 0, extended: 1, zoo: 2 };
15 var zooCollapsed = true;
16
17 var modalOpen = false;
18 var selectedIndex = 0;
19 var filterText = "";
20 var activeSurface = "command-bar";
21 var activeComposer = null;
22 var activeContext = {};
23
24 function parseContext(raw) {
25 if (!raw) return {};
26 try {
27 return JSON.parse(raw);
28 } catch (_) {
29 return {};
30 }
31 }
32
33 function mergedContext() {
34 var body = document.body;
35 var base = {};
36 var repo = body && body.getAttribute("data-forge-repo");
37 if (repo) base.repo = repo;
38 return Object.assign(base, activeContext);
39 }
40
41 function normalizeCategory(cmd) {
42 var tier = cmd.tier || "core";
43 if (tier === "zoo") return "Extensions";
44 return cmd.category || "Forge";
45 }
46
47 function sortDomainCatalog(items) {
48 items.sort(function (a, b) {
49 var ta = tierOrder[a.tier] ?? 1;
50 var tb = tierOrder[b.tier] ?? 1;
51 if (ta !== tb) return ta - tb;
52 return (a.path || "").localeCompare(b.path || "");
53 });
54 return items;
55 }
56
57 async function loadDomainCatalog(surface) {
58 try {
59 var res = await fetch("/api/v1/capabilities?surface=" + encodeURIComponent(surface));
60 if (!res.ok) return [];
61 var data = await res.json();
62 var items = (data.commands || []).map(function (cmd) {
63 var tier = cmd.tier || "core";
64 return {
65 kind: "domain",
66 path: cmd.path,
67 help: cmd.help || "",
68 category: normalizeCategory({ tier: tier, category: cmd.category }),
69 tier: tier,
70 };
71 });
72 return sortDomainCatalog(items);
73 } catch (_) {
74 return [];
75 }
76 }
77
78 function allItems() {
79 var includeFormat = activeSurface === "mr-comment" || activeSurface === "issue-comment";
80 var formats = includeFormat
81 ? formatCatalog.map(function (f) {
82 return { kind: "format", path: f.path, help: f.help, category: f.category, format: f, tier: "core" };
83 })
84 : [];
85 var merged = formats.concat(domainCatalog);
86 var q = filterText.trim().toLowerCase();
87 if (!q) return merged;
88 return merged.filter(function (item) {
89 var path = (item.path || "").toLowerCase();
90 var help = (item.help || "").toLowerCase();
91 var cat = (item.category || "").toLowerCase();
92 return path.indexOf(q) >= 0 || help.indexOf(q) >= 0 || cat.indexOf(q) >= 0;
93 });
94 }
95
96 function isZooCategory(category) {
97 return category === "Extensions";
98 }
99
100 function renderList() {
101 var list = document.getElementById("forge-slash-list");
102 if (!list) return;
103 var items = allItems();
104 selectedIndex = Math.min(selectedIndex, Math.max(0, items.length - 1));
105 list.innerHTML = "";
106
107 var lastCategory = null;
108 items.forEach(function (item, index) {
109 if (item.category && item.category !== lastCategory) {
110 lastCategory = item.category;
111 var heading = document.createElement("li");
112 heading.className = "forge-slash-category";
113 if (isZooCategory(item.category)) {
114 heading.className += " forge-slash-category-zoo" + (zooCollapsed ? " collapsed" : "");
115 heading.textContent = item.category + (zooCollapsed ? " ▸" : " ▾");
116 heading.addEventListener("mousedown", function (e) {
117 e.preventDefault();
118 zooCollapsed = !zooCollapsed;
119 renderList();
120 });
121 } else {
122 heading.textContent = item.category;
123 }
124 list.appendChild(heading);
125 }
126
127 if (isZooCategory(item.category) && zooCollapsed) return;
128
129 var li = document.createElement("li");
130 li.className = "forge-slash-item" + (item.kind === "format" ? " format" : "") + (index === selectedIndex ? " active" : "");
131 li.setAttribute("role", "option");
132 li.dataset.index = String(index);
133 li.innerHTML =
134 "<span class=\"forge-slash-path\">/" + escapeHtml((item.path || "").replace(/^\//, "")) + "</span>" +
135 (item.help ? "<span class=\"forge-slash-help\">" + escapeHtml(item.help) + "</span>" : "");
136 li.addEventListener("mousedown", function (e) {
137 e.preventDefault();
138 pickItem(item, "");
139 });
140 list.appendChild(li);
141 });
142
143 if (items.length === 0) {
144 var empty = document.createElement("li");
145 empty.className = "forge-slash-empty";
146 empty.textContent = "No matching commands";
147 list.appendChild(empty);
148 }
149 }
150
151 function escapeHtml(value) {
152 return String(value)
153 .replace(/&/g, "&amp;")
154 .replace(/</g, "&lt;")
155 .replace(/>/g, "&gt;")
156 .replace(/"/g, "&quot;");
157 }
158
159 async function openModal(initial, surface, composer, context) {
160 var modal = document.getElementById("forge-slash-modal");
161 var input = document.getElementById("forge-slash-input");
162 if (!modal || !input) return;
163 activeSurface = surface || "command-bar";
164 activeComposer = composer || null;
165 activeContext = context || {};
166 zooCollapsed = true;
167 domainCatalog = await loadDomainCatalog(activeSurface);
168 modalOpen = true;
169 modal.hidden = false;
170 filterText = initial || "";
171 input.value = filterText;
172 selectedIndex = 0;
173 renderList();
174 input.focus();
175 }
176
177 function closeModal() {
178 var modal = document.getElementById("forge-slash-modal");
179 if (!modal) return;
180 modalOpen = false;
181 modal.hidden = true;
182 filterText = "";
183 selectedIndex = 0;
184 activeComposer = null;
185 activeContext = {};
186 }
187
188 function applyFormat(formatDef) {
189 var ta = activeComposer;
190 if (!ta || !formatDef) return;
191 var start = ta.selectionStart;
192 var end = ta.selectionEnd;
193 var text = ta.value;
194 if (formatDef.wrap) {
195 var selected = text.slice(start, end) || "text";
196 var wrapped = formatDef.wrap[0] + selected + formatDef.wrap[1];
197 ta.value = text.slice(0, start) + wrapped + text.slice(end);
198 ta.focus();
199 ta.selectionStart = start + formatDef.wrap[0].length;
200 ta.selectionEnd = start + formatDef.wrap[0].length + selected.length;
201 return;
202 }
203 if (formatDef.insert) {
204 ta.value = text.slice(0, start) + formatDef.insert + text.slice(end);
205 ta.focus();
206 ta.selectionStart = ta.selectionEnd = start + formatDef.insert.length;
207 }
208 }
209
210 async function runDomainCommand(path, argsTail) {
211 closeModal();
212 var payload = {
213 path: path,
214 args: argsTail || "",
215 context: mergedContext(),
216 };
217 try {
218 var res = await fetch("/api/v1/commands/execute", {
219 method: "POST",
220 headers: {
221 "Content-Type": "application/json",
222 Accept: "application/json",
223 "X-Forge-Command-Client": "slash",
224 },
225 body: JSON.stringify(payload),
226 });
227 var data = await res.json();
228 if (data.kind === "redirect" && data.redirectUrl) {
229 window.location.href = data.redirectUrl;
230 return;
231 }
232 if (!res.ok || data.kind === "error") {
233 window.alert(data.error || "Command failed.");
234 }
235 } catch (err) {
236 window.alert(String(err));
237 }
238 }
239
240 function pickItem(item, tail) {
241 if (!item) return;
242 if (item.kind === "format") {
243 applyFormat(item.format);
244 closeModal();
245 if (activeComposer) activeComposer.focus();
246 return;
247 }
248 runDomainCommand(item.path, tail);
249 }
250
251 function onInputKeyDown(event) {
252 var items = allItems().filter(function (item) {
253 return !(isZooCategory(item.category) && zooCollapsed);
254 });
255 if (event.key === "Escape") {
256 event.preventDefault();
257 closeModal();
258 return;
259 }
260 if (event.key === "ArrowDown") {
261 event.preventDefault();
262 selectedIndex = Math.min(selectedIndex + 1, Math.max(0, items.length - 1));
263 renderList();
264 return;
265 }
266 if (event.key === "ArrowUp") {
267 event.preventDefault();
268 selectedIndex = Math.max(selectedIndex - 1, 0);
269 renderList();
270 return;
271 }
272 if (event.key === "Enter") {
273 event.preventDefault();
274 var text = event.target.value || "";
275 var space = text.indexOf(" ");
276 var tail = space >= 0 ? text.slice(space + 1).trim() : "";
277 var match = items[selectedIndex];
278 if (match) {
279 pickItem(match, tail);
280 return;
281 }
282 }
283 }
284
285 function bindComposers() {
286 document.querySelectorAll("textarea.forge-composer").forEach(function (ta) {
287 ta.addEventListener("keydown", function (e) {
288 if (e.key !== "/" || e.ctrlKey || e.metaKey || e.altKey) return;
289 var pos = ta.selectionStart;
290 var before = ta.value.slice(0, pos);
291 if (pos > 0 && before.length > 0 && !/\s$/.test(before)) return;
292 e.preventDefault();
293 var ctx = parseContext(ta.getAttribute("data-forge-context"));
294 openModal("", ta.getAttribute("data-forge-surface") || "mr-comment", ta, ctx);
295 });
296 });
297 }
298
299 function bindUi() {
300 var trigger = document.getElementById("forge-command-bar-trigger");
301 var input = document.getElementById("forge-slash-input");
302 var modal = document.getElementById("forge-slash-modal");
303 if (trigger) {
304 trigger.addEventListener("click", function () {
305 openModal("", "command-bar", null, {});
306 });
307 }
308 if (input) {
309 input.addEventListener("input", function (e) {
310 filterText = e.target.value || "";
311 selectedIndex = 0;
312 renderList();
313 });
314 input.addEventListener("keydown", onInputKeyDown);
315 }
316 if (modal) {
317 modal.addEventListener("click", function (e) {
318 if (e.target === modal) closeModal();
319 });
320 }
321 document.addEventListener("keydown", function (e) {
322 if ((e.ctrlKey || e.metaKey) && e.key.toLowerCase() === "k") {
323 e.preventDefault();
324 if (modalOpen) closeModal();
325 else openModal("", "command-bar", null, {});
326 }
327 });
328 bindComposers();
329 }
330
331 if (document.readyState === "loading") {
332 document.addEventListener("DOMContentLoaded", bindUi);
333 } else {
334 bindUi();
335 }
336})();
337
View only · write via MCP/CIDE