Skip to content
396 linesCodeBlameRaw

Pick any line to see why it is the way it is: the commit, the pull request and issue it came from, and what the agent was thinking.

preparing for cloud work1// The desktop bridge: what the site finds at `window.g1tDesktop`, run at
2// the start of every page the window loads (bridge.rs puts it there). The
3// site's lib/notify-client.ts describes the contract from its side; when
4// the bridge is there, notifications go to the system, the badge to the
5// icon, links out to the browser, and g1t:// links arrive as `onNavigate`.
6// Each call is a command in bridge.rs, which only answers the server's
7// pages; on any other page the calls fail quietly.
8(function () {
9 if (window.g1tDesktop) return;
10 var VERSION = "__VERSION__";
11 var PLATFORM = "__PLATFORM__";
Merge remote-tracking branch 'origin/claude/cool-newton-7nm8r9' into claude/cool-newton-7nm8r912 // Whether g1t's own title bar is above this page ("custom"), or the
13 // system's ("system"). The bar is a page of its own (src/frame.rs); this
14 // page only tells it what to show (`report`), and is never under it.
15 var FRAME = "__FRAME__";
16 // The site's pages for reading about g1t, not using it, which open in
17 // the browser (navigation.rs); one ending in "/" is every page under it.
18 var OUTSIDE = "__OUTSIDE__";
preparing for cloud work19
20 function invoke(command, args) {
21 var core = window.__TAURI__ && window.__TAURI__.core;
22 if (core && typeof core.invoke === "function") return core.invoke(command, args || {});
23 var internals = window.__TAURI_INTERNALS__;
24 if (internals && typeof internals.invoke === "function") return internals.invoke(command, args || {});
25 return Promise.reject(new Error("g1t desktop: this page cannot reach the app"));
26 }
27
28 function quietly(promise) {
29 promise.catch(function (error) {
30 console.warn("g1t desktop:", error && error.message ? error.message : error);
31 });
32 }
33
34 var listeners = [];
35 var listening = false;
36 function listen() {
37 if (listening) return;
38 listening = true;
39 var events = window.__TAURI__ && window.__TAURI__.event;
40 if (!events || typeof events.listen !== "function") return;
41 events
42 .listen("g1t-navigate", function (event) {
43 var href = event && event.payload;
44 if (typeof href !== "string") return;
45 listeners.slice().forEach(function (listener) {
46 try {
47 listener(href);
48 } catch (error) {
49 console.warn("g1t desktop: a navigate listener failed", error);
50 }
51 });
52 })
53 .catch(function () {});
54 }
55
Merge the desktop app's runner: it starts with the app for the signed-in person, sandboxed by default and running nothing where no sandbox is available yet, registers through the site, keeps itself up to date, and says everywhere what it is doing, why it is not, and how to fix it.56 // An app event, to a listener; returns what stops listening.
57 function on(name, listener) {
58 var events = window.__TAURI__ && window.__TAURI__.event;
59 if (!events || typeof events.listen !== "function") return function () {};
60 var stop = null;
61 var stopped = false;
62 events
63 .listen(name, function (event) {
64 try {
65 listener(event && event.payload);
66 } catch (error) {
67 console.warn("g1t desktop: a " + name + " listener failed", error);
68 }
69 })
70 .then(function (unlisten) {
71 if (stopped) unlisten();
72 else stop = unlisten;
73 })
74 .catch(function () {});
75 return function () {
76 stopped = true;
77 if (stop) stop();
78 };
79 }
80
preparing for cloud work81 var bridge = {
82 version: VERSION,
83 platform: PLATFORM,
84 // A native notification, for a message to the person while the window is behind another.
85 notify: function (notification) {
86 quietly(invoke("notify", { title: String(notification.title || "g1t"), body: String(notification.body || "") }));
87 },
88 // What needs the person, on the icon and the tray; 0 clears it.
89 setBadge: function (count) {
90 quietly(invoke("set_badge", { count: Math.max(0, Math.floor(Number(count) || 0)) }));
91 },
92 // A link to somewhere else, in the browser the system uses.
93 openUrl: function (url) {
94 quietly(invoke("open_url", { url: String(url) }));
95 },
96 // g1t:// links, as site paths such as "/acme/-/chat/dm/chn_1".
97 onNavigate: function (listener) {
98 listeners.push(listener);
99 listen();
100 return function () {
101 var at = listeners.indexOf(listener);
102 if (at >= 0) listeners.splice(at, 1);
103 };
104 },
105 // Looks for an update now: "current", "ready:<version>", or rejects with why.
106 checkForUpdates: function () {
107 return invoke("check_for_updates");
108 },
Merge branch 'main' into rm-agents109 // The app's own settings: the dialog this page draws (`settings.onOpen`),
110 // or their own window when the page can't.
Merge remote-tracking branch 'origin/claude/cool-newton-7nm8r9' into claude/cool-newton-7nm8r9111 openSettings: function () {
112 quietly(invoke("open_settings"));
113 },
Merge branch 'main' into rm-agents114 // The app's settings, for the dialog the site draws over itself, and
115 // the runner's menu under the title bar's dot (src/settings.rs). The
116 // page says it draws them with `ready`; until then they open in windows
117 // of their own.
118 settings: Object.freeze({
119 ready: function (ready) {
120 return invoke("site_ready", { ready: ready !== false });
121 },
122 get: function () {
123 return invoke("get_settings");
124 },
125 // Every setting but opening at sign-in, as `get` has them (camelCase).
126 save: function (settings) {
127 return invoke("save_settings", { settings: settings });
128 },
129 setOpenAtLogin: function (on) {
130 return invoke("set_open_at_login", { on: !!on });
131 },
132 // "current", "ready:<version>", or rejects with why; the dialog says it, so no notification.
133 checkForUpdates: function () {
134 return invoke("check_for_updates", { shown: true });
135 },
136 restart: function () {
137 return invoke("restart");
138 },
139 restartToUpdate: function () {
140 return invoke("restart_to_update");
141 },
142 changeServer: function () {
143 return invoke("change_server");
144 },
145 // Open the dialog: { section: "runner" | "updates" | null, check: boolean }.
146 onOpen: function (listener) {
147 return on("g1t-open-settings", listener);
148 },
149 // The runner menu: { action: "toggle" | "open", at: share of the bar's width | null }.
150 onRunnerMenu: function (listener) {
151 return on("g1t-runner-menu", listener);
152 },
153 // An update finished downloading, with its version.
154 onUpdateReady: function (listener) {
155 return on("g1t-update-ready", listener);
156 },
157 // Whether the runner menu is open, which the title bar's dot shows.
158 setRunnerMenuOpen: function (open) {
159 runnerMenu = !!open;
160 report();
161 },
162 }),
Merge branch 'worktree-agent-a6b141eb3a7e9cf01'163 // The web inspector for this page, while Developer tools is on in the
164 // app's settings (src/devtools.rs); rejects with where to turn it on
165 // while it is off.
166 devtools: Object.freeze({
167 open: function () {
168 return invoke("open_devtools");
169 },
170 }),
Merge remote-tracking branch 'origin/claude/cool-newton-7nm8r9' into claude/cool-newton-7nm8r9171 // The title bar's search, in its middle: the site hands it a label
172 // ("Search Flagon") and what opens its search. Returns what takes it away.
173 setSearch: function (label, open) {
174 var mine = typeof open === "function" ? { label: String(label || "Search"), open: open } : null;
175 search = mine;
176 report();
177 return function () {
178 if (search === mine) {
179 search = null;
180 report();
181 }
182 };
183 },
184 // Whether g1t draws the title bar, and how tall it is: 0 under the system's.
185 // The page is below it, never under it: this says only that the bar is there.
186 titleBarHeight: FRAME === "custom" ? 36 : 0,
Merge the desktop app's runner: it starts with the app for the signed-in person, sandboxed by default and running nothing where no sandbox is available yet, registers through the site, keeps itself up to date, and says everywhere what it is doing, why it is not, and how to fix it.187 // The runner the app keeps on this computer (src/runner/mod.rs).
188 runner: Object.freeze({
189 // What it is doing: its diagnosis (why it isn't running, and the
190 // fix), the workspace, how jobs run, and its last log lines.
191 status: function () {
192 return invoke("runner_status");
193 },
194 // Who is signed in, the workspaces they own, and the one they are in;
195 // resolves to the workspace a registration token is wanted for, or null.
196 offer: function (offer) {
197 return invoke("runner_offer", {
198 signedIn: !!offer.signedIn,
199 workspaces: (offer.workspaces || []).map(function (w) {
200 return { slug: String(w.slug), name: w.name == null ? null : String(w.name) };
201 }),
202 current: offer.current == null ? null : String(offer.current),
203 });
204 },
205 // A registration token for the workspace the app asked for, or why there is none.
206 register: function (answer) {
207 return invoke("runner_register", {
208 workspace: String(answer.workspace),
209 token: answer.token == null ? null : String(answer.token),
210 allowed: answer.allowed !== false,
211 error: answer.error == null ? null : String(answer.error),
212 });
213 },
Merge the desktop runner's settings: a card for each kind of job, Linux and Windows, each with its state, job, cause and fix, and its own Start and Stop; an honest wait for a release's sandbox images, checked again by itself; and the same on the Runners page.214 // Start, stop, or a diagnosis's fix, by its id; Start and Stop of one
215 // kind of job alone (`linux`, `windows`) with its id as `capability`.
216 act: function (action, capability) {
217 var args = { action: String(action) };
218 if (capability != null) args.capability = String(capability);
219 return invoke("runner_control", args);
Merge the desktop app's runner: it starts with the app for the signed-in person, sandboxed by default and running nothing where no sandbox is available yet, registers through the site, keeps itself up to date, and says everywhere what it is doing, why it is not, and how to fix it.220 },
221 // The app wants a registration token for a workspace.
222 onWanted: function (listener) {
223 return on("g1t-runner-wanted", listener);
224 },
225 // The runner's status changed.
226 onStatus: function (listener) {
227 return on("g1t-runner-status", listener);
228 },
229 }),
preparing for cloud work230 };
231 Object.defineProperty(window, "g1tDesktop", { value: Object.freeze(bridge), writable: false, configurable: false });
232
Merge remote-tracking branch 'origin/claude/cool-newton-7nm8r9' into claude/cool-newton-7nm8r9233 // Links out. A link to another site, or to one of the site's pages for
234 // reading about g1t, opens in the system's browser, and the window stays
235 // where it is; a link that asks for a new tab, or a click
preparing for cloud work236 // with Ctrl or Cmd held, goes on in this window, which has no tabs.
Merge remote-tracking branch 'origin/claude/cool-newton-7nm8r9' into claude/cool-newton-7nm8r9237 function outside(url) {
238 var path = url.pathname.length > 1 ? url.pathname.replace(/\/+$/, "") : url.pathname;
239 return OUTSIDE.some(function (page) {
240 return page.charAt(page.length - 1) === "/" ? path.indexOf(page) === 0 : path === page;
241 });
242 }
preparing for cloud work243 function destination(href) {
244 try {
245 var url = new URL(href, location.href);
246 return /^https?:$/.test(url.protocol) ? url : null;
247 } catch (error) {
248 return null;
249 }
250 }
251 document.addEventListener(
252 "click",
253 function (event) {
254 if (event.defaultPrevented || event.button !== 0) return;
255 var anchor = event.target && event.target.closest ? event.target.closest("a[href]") : null;
256 if (!anchor || anchor.hasAttribute("download")) return;
257 var url = destination(anchor.getAttribute("href"));
258 if (!url) return;
Merge remote-tracking branch 'origin/claude/cool-newton-7nm8r9' into claude/cool-newton-7nm8r9259 if (url.origin !== location.origin || outside(url)) {
preparing for cloud work260 event.preventDefault();
261 event.stopImmediatePropagation();
262 bridge.openUrl(url.href);
263 } else if (anchor.target === "_blank" || event.metaKey || event.ctrlKey) {
264 event.preventDefault();
265 event.stopImmediatePropagation();
266 location.assign(url.href);
267 }
268 },
269 true,
270 );
271 var open = window.open;
272 window.open = function (href, target, features) {
273 var url = destination(href);
274 if (url) {
Merge remote-tracking branch 'origin/claude/cool-newton-7nm8r9' into claude/cool-newton-7nm8r9275 if (url.origin === location.origin && !outside(url)) location.assign(url.href);
preparing for cloud work276 else bridge.openUrl(url.href);
277 return null;
278 }
279 return typeof open === "function" ? open.call(window, href, target, features) : null;
280 };
Merge remote-tracking branch 'origin/claude/cool-newton-7nm8r9' into claude/cool-newton-7nm8r9281
Merge branch 'worktree-agent-a6b141eb3a7e9cf01'282 // F12 opens the web inspector, or closes it, while Developer tools is on;
283 // the View menu's row has Ctrl+Shift+I (⌥⌘I on macOS). With it off, or
284 // on a page that isn't the site's, the app says no and the key is the
285 // page's as ever.
286 document.addEventListener(
287 "keydown",
288 function (event) {
289 if (event.key !== "F12" || event.ctrlKey || event.metaKey || event.altKey || event.shiftKey) return;
290 invoke("open_devtools", { toggle: true }).catch(function () {});
291 },
292 true,
293 );
294
Merge remote-tracking branch 'origin/claude/cool-newton-7nm8r9' into claude/cool-newton-7nm8r9295 // ── The title bar ─────────────────────────────────────────────────────
296 // What the bar above the page shows of it: the title, the search the
297 // site handed over (`setSearch`), whether there is a page to go back or
Merge branch 'main' into rm-agents298 // forward to, the page's colours, so the bar is the colour of g1t's
299 // rail, light or dark, or of whatever page is showing, and whether the
300 // runner menu is open under the bar's dot. Sent whenever any of it
301 // changes; the bar's search comes back as an event.
Merge remote-tracking branch 'origin/claude/cool-newton-7nm8r9' into claude/cool-newton-7nm8r9302 var search = null;
Merge branch 'main' into rm-agents303 var runnerMenu = false;
Merge remote-tracking branch 'origin/claude/cool-newton-7nm8r9' into claude/cool-newton-7nm8r9304 var report = function () {};
305 if (FRAME !== "custom") return;
306
307 // A colour the page names, resolved: `var(--color-rail)` is light-dark(…) until it is drawn.
308 var probe = null;
309 function resolve(value) {
310 if (!document.body) return "";
311 if (!probe) {
312 probe = document.createElement("span");
313 probe.setAttribute("aria-hidden", "true");
314 probe.style.cssText = "position:absolute;width:0;height:0;overflow:hidden;pointer-events:none;";
315 }
316 if (probe.parentNode !== document.body) document.body.appendChild(probe);
317 probe.style.color = "";
318 probe.style.color = value;
319 var color = getComputedStyle(probe).color;
320 probe.remove();
321 return color;
322 }
323 function opaque(color) {
324 return !!color && color !== "transparent" && !/rgba\(.*,\s*0\)$/.test(color);
325 }
326 function luminance(color) {
327 var parts = (color.match(/[\d.]+/g) || []).map(Number);
328 return parts.length < 3 ? 0 : (0.2126 * parts[0] + 0.7152 * parts[1] + 0.0722 * parts[2]) / 255;
329 }
330 // g1t's pages name their colours (apps/web/app/app.css); any other page has its own background and text.
331 function colors() {
332 if (!document.body) return null;
333 var root = getComputedStyle(document.documentElement);
334 var ours = root.getPropertyValue("--color-rail").trim() !== "";
335 var body = getComputedStyle(document.body);
336 var background = ours ? resolve("var(--color-rail)") : body.backgroundColor;
337 if (!opaque(background)) background = root.backgroundColor;
338 if (!opaque(background)) return null;
339 var foreground = ours ? resolve("var(--color-fg)") : body.color;
340 return {
341 background: background,
342 foreground: foreground,
343 muted: ours ? resolve("var(--color-muted)") : foreground,
344 accent: ours ? resolve("var(--color-accent)") : foreground,
345 dark: luminance(background) < 0.5,
346 };
347 }
348
349 var sent = "";
350 var waiting = 0;
351 report = function () {
352 clearTimeout(waiting);
353 waiting = setTimeout(function () {
354 var nav = window.navigation;
355 var page = {
356 title: document.title || "g1t",
357 search: search ? search.label : null,
358 canGoBack: nav && typeof nav.canGoBack === "boolean" ? nav.canGoBack : null,
359 canGoForward: nav && typeof nav.canGoForward === "boolean" ? nav.canGoForward : null,
360 colors: colors(),
Merge branch 'main' into rm-agents361 runnerMenu: runnerMenu,
Merge remote-tracking branch 'origin/claude/cool-newton-7nm8r9' into claude/cool-newton-7nm8r9362 };
363 var text = JSON.stringify(page);
364 if (text === sent) return;
365 sent = text;
366 invoke("title_bar_page", { page: page }).catch(function () {});
367 }, 30);
368 };
369
370 document.addEventListener("DOMContentLoaded", function () {
371 report();
372 if (document.head) new MutationObserver(report).observe(document.head, { subtree: true, childList: true, characterData: true });
373 // The site's Light, Dark or Auto (data-theme on <html>), and the system's own.
374 new MutationObserver(report).observe(document.documentElement, { attributes: true, attributeFilter: ["data-theme", "class", "style"] });
375 });
376 window.addEventListener("load", report);
377 window.addEventListener("pageshow", report);
378 window.addEventListener("popstate", report);
379 if (window.matchMedia) {
380 var scheme = window.matchMedia("(prefers-color-scheme: dark)");
381 if (typeof scheme.addEventListener === "function") scheme.addEventListener("change", report);
382 }
383 if (window.navigation && typeof window.navigation.addEventListener === "function") {
384 window.navigation.addEventListener("currententrychange", report);
385 }
386
387 // The bar's search was clicked: the site's search opens.
388 var events = window.__TAURI__ && window.__TAURI__.event;
389 if (events && typeof events.listen === "function") {
390 events
391 .listen("g1t-search", function () {
392 if (search) search.open();
393 })
394 .catch(function () {});
395 }
preparing for cloud work396})();

This file's history is long; its oldest lines are credited to the oldest commit read.