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 work | 1 | // 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-7nm8r9 | 12 | // 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 work | 19 | |
| 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 work | 81 | 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-agents | 109 | // 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-7nm8r9 | 111 | openSettings: function () { |
| 112 | quietly(invoke("open_settings")); | |
| 113 | }, | |
| Merge branch 'main' into rm-agents | 114 | // 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 | }), | |
| The desktop app has developer tools: a Developer tools setting, off unless someone turns it on under Window in the app's settings, lets F12 on the page, Ctrl+Shift+I (⌥⌘I on macOS) and the View menu's new Developer tools row open the web inspector for the site's page, so a page that says something went wrong can be looked into from its console. Release builds have the inspector through Tauri's devtools feature, which on macOS reaches it through WebKit's private API, so the app could not go in the Mac App Store; it ships from g1t.sh. With the setting off none of these open it, and the menu row is greyed out. On Windows the setting takes effect at once, WebView2's own F12, Ctrl+Shift+I and Inspect following it; WebKit on macOS and Linux keeps what the page's web view was made with, so there the settings offer Restart now, as they do for the title bar. The site can open it too, through window.g1tDesktop.devtools.open(), a new open_devtools command that answers only the server's page in the window and only while the setting is on, granted to the server's origin alone. The settings window and the Runner window keep the setting when they save, and the desktop app guide says how to turn it on and open it. | 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-7nm8r9 | 171 | // 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 work | 230 | }; |
| 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-7nm8r9 | 233 | // 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 work | 236 | // 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-7nm8r9 | 237 | 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 work | 243 | 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-7nm8r9 | 259 | if (url.origin !== location.origin || outside(url)) { |
| preparing for cloud work | 260 | 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-7nm8r9 | 275 | if (url.origin === location.origin && !outside(url)) location.assign(url.href); |
| preparing for cloud work | 276 | 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-7nm8r9 | 281 | |
| The desktop app has developer tools: a Developer tools setting, off unless someone turns it on under Window in the app's settings, lets F12 on the page, Ctrl+Shift+I (⌥⌘I on macOS) and the View menu's new Developer tools row open the web inspector for the site's page, so a page that says something went wrong can be looked into from its console. Release builds have the inspector through Tauri's devtools feature, which on macOS reaches it through WebKit's private API, so the app could not go in the Mac App Store; it ships from g1t.sh. With the setting off none of these open it, and the menu row is greyed out. On Windows the setting takes effect at once, WebView2's own F12, Ctrl+Shift+I and Inspect following it; WebKit on macOS and Linux keeps what the page's web view was made with, so there the settings offer Restart now, as they do for the title bar. The site can open it too, through window.g1tDesktop.devtools.open(), a new open_devtools command that answers only the server's page in the window and only while the setting is on, granted to the server's origin alone. The settings window and the Runner window keep the setting when they save, and the desktop app guide says how to turn it on and open it. | 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-7nm8r9 | 295 | // ── 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-agents | 298 | // 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-7nm8r9 | 302 | var search = null; |
| Merge branch 'main' into rm-agents | 303 | var runnerMenu = false; |
| Merge remote-tracking branch 'origin/claude/cool-newton-7nm8r9' into claude/cool-newton-7nm8r9 | 304 | 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-agents | 361 | runnerMenu: runnerMenu, |
| Merge remote-tracking branch 'origin/claude/cool-newton-7nm8r9' into claude/cool-newton-7nm8r9 | 362 | }; |
| 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 work | 396 | })(); |
This file's history is long; its oldest lines are credited to the oldest commit read.