GIP — Grab Issues in Parallel

Grab a batch of open issues and work them at the same time — one subagent per issue, each in its own git worktree, each running the full grab-issue flow (claim → check history → open draft PR → implement → mark ready → ARDI to clean). Then assemble one combined report.

This is the parallel counterpart to gii, which is deliberately serial. The whole job of this skill is to safely lift that serialization — and that is only safe for issues that are provably independent. Read The independence gate before fanning anything out; it is the load-bearing part.

When this fires

  • “gip”, “grab issues in parallel”, “do these issues concurrently”
  • “work several issues at once”, “parallelize the backlog”, “fan out the issue queue”, “spin up a worker per issue”

It does not fire for a single issue (use gi) or when the issues are interdependent / stacked (use gii, which stacks).

Why gii is serial — and when parallel is safe

gii runs one issue at a time on purpose, for three reasons:

  1. Base-branch stacking — a later issue’s branch may depend on a prior MR that hasn’t merged yet, so it must branch from that MR’s tip, not main.
  2. Same-file conflicts — two issues that edit the same files produce guaranteed merge conflicts if worked in parallel.
  3. Shared working tree — a single checkout can’t hold two in-progress branches at once.

Parallel is safe only when (1) and (2) don’t apply and (3) is removed by worktree isolation. So this skill fans out only the independent subset and sends everything else back to the serial path.

Procedure

0. Establish context (orchestrator, once)

Detect the forge (GitHub gh / GitLab glab) from git remote get-url origin and note the default branch. Resolve <owner>/<repo> once so you can pass it to every subagent. In a remote/web session, gh may be absent — use the GitHub MCP tools instead (e.g. mcp__github__add_issue_comment, mcp__github__create_pull_request, mcp__github__search_pull_requests).

1. Enumerate and triage (orchestrator)

List the open issues and prioritize them exactly as gi does. Decide which issues are in scope for this batch (respect any the user named).

2. The independence gate (orchestrator — do NOT skip)

Partition the in-scope issues into an independent set (safe to parallelize) and a dependent remainder (must stay serial). An issue belongs in the independent set only if all hold:

  • No stacking dependency — it can branch straight from origin/main; it doesn’t need another in-flight issue’s unmerged branch as its base.
  • No file overlap — its likely touched files don’t intersect any other in-batch issue’s likely files. When in doubt, read the issues and sketch each one’s probable file footprint; if two plausibly collide, treat them as dependent.
  • Not blocked — no “blocked by #N” / “depends on #N” marker pointing at another open item.

Anything failing a check drops to the dependent remainder. If two issues overlap only with each other, keep one in the independent set and defer the other to the remainder — don’t fan out both.

When unsure, serialize. A false “independent” call costs a merge conflict and a confused review; a false “dependent” call only costs some wall-clock time. Bias toward the remainder.

If the independent set has 0 or 1 issues, there’s nothing to parallelize — hand off to gii (serial) and stop here.

3. Pick the concurrency cap (orchestrator)

Fan out at most N at once (default N = 3). Bound it because every subagent pushes commits, triggers a review workflow, and polls for the result — too many at once swamps shared CI runners and the review bot (the same runner-contention reason ardia processes PRs one at a time). If the independent set is larger than N, run it in waves of N.

4. Fan out — one worktree-isolated subagent per issue (concurrent)

Spawn the wave in a single message with multiple Agent calls so they run at once. Give every subagent isolation: "worktree" — that hands each one its own working directory over the shared .git, so concurrent edits, branches, and commits never collide on one checkout. (This is the subagent form of the same isolation session-lock sets up for independent top-level sessions.)

A subagent starts fresh — it sees only the prompt you hand it, not this skill file — so inline the entire per-issue procedure. Don’t point it at gi/ardi; restate the steps. Fill in <N>, <title>, <owner>, <repo>, and the default branch for each issue:

Work GitHub issue # (“ <p>“) in <code><owner>/<repo></code> end to end, on your own branch, in this worktree. You are one of several parallel workers — stay entirely within this worktree and touch only files relevant to this issue.</p> <ol type="1"> <li><strong>Claim it</strong> so no one else double-works it: post a brief “Working on this — paws off until I’m done.” comment on the issue (<code>gh issue comment <N> --body "..."</code>, or the MCP <code>mcp__github__add_issue_comment</code> equivalent in a remote session).</li> <li><strong>Check history</strong> — before writing code, scan merged/closed PRs that touched the same area so you don’t undo past work or reintroduce a fixed bug (<code>gh pr list --state all --search "<keywords>"</code>). If a past PR already solved this, stop and report that instead of re-doing it.</li> <li><strong>Branch from current <code><default-branch></code></strong>: <code>git fetch origin <default-branch> -q && git checkout -b <slug> origin/<default-branch></code>. Use a descriptive <code><slug></code>.</li> <li><strong>Open the draft PR now</strong> — before implementing, so this worktree’s work is visible to the other parallel workers and no one double-grabs the issue (see <a href="../../shared/workflow/pr-on-claim.md"><code>pr-on-claim</code></a>). Give the branch a diff with an empty commit, push, and open a <strong>draft</strong> PR into <code><default-branch></code> referencing <code>Closes #<N></code>: <code>git commit --allow-empty -m "start: <title> (closes #<N>)"</code>, then <code>git push -u origin HEAD</code> (retry with backoff on a network error), then <code>gh pr create --draft …</code> (or <code>mcp__github__create_pull_request</code> with <code>draft: true</code>). A draft doesn’t trigger the review bot on an empty diff.</li> <li><strong>Implement</strong> the change. Keep the diff focused on this issue only — do <strong>not</strong> touch files another issue owns. Follow the repo’s conventions (its <code>CLAUDE.md</code> / lab manual). Run the repo’s pre-commit checks (render / lint / spell / tests) and fix what they flag.</li> <li><strong>Commit and push</strong> the implementation onto the draft PR with a clear message referencing the issue (<code>Closes #<N></code> so the PR auto-closes it), then <strong>mark the PR ready for review</strong> — <code>gh pr ready <N></code> (or <code>mcp__github__update_pull_request</code> with <code>draft: false</code>). Marking it ready is what kicks off review.</li> <li><strong>ARDI to clean</strong> — drive the PR to a clean review verdict: read the LATEST review, Address every finding / Rebut what’s wrong / Defer out-of-scope items to a tracked issue, push, re-request review, repeat until zero findings and CI is green. Don’t stop at “review-clean, needs approval.”</li> </ol> <p>Return: the issue number, the PR number + URL, how many ARDI rounds it took, the final status (clean / blocked / needs-input), and a one-line summary of what you changed. If you hit something ambiguous or architecturally significant, stop and report it instead of guessing.</p> </blockquote> <p>Keep the cheap, once-per-run setup (enumerate, triage, the independence gate) in the orchestrator — never have each subagent re-do it.</p> </section> <section id="work-the-dependent-remainder-serially-orchestrator" class="level3"> <h3>5. Work the dependent remainder serially (orchestrator)</h3> <p>After the parallel wave, run the <strong>dependent remainder</strong> through the normal serial <a href="../gii/SKILL.md"><code>gii</code></a> loop — it stacks branches and handles same-file ordering correctly. Don’t try to parallelize it. GII keeps going through that remainder without pausing for merges — a clean-but-unmerged base is stacked on, not waited on (see <a href="../../shared/workflow/stack-dont-pause.md"><code>stack-dont-pause</code></a>).</p> </section> <section id="combined-report-orchestrator" class="level3"> <h3>6. Combined report (orchestrator)</h3> <p>Collect each subagent’s returned row and print one summary:</p> <pre><code>## GIP Session Summary — <timestamp> ### Parallel wave (independent issues) | # | Issue | PR | Rounds | Status | |---|-------|----|--------|--------| | 1 | [#12](url) | [#30](url) | 2 | ✅ Clean | | 2 | [#15](url) | [#31](url) | 1 | ✅ Clean | ### Serial remainder (dependent / overlapping issues) | # | Issue | PR | Rounds | Status | |---|-------|----|--------|--------| | 3 | [#18](url) | [#32](url) | 3 | ✅ Clean (stacked on #30) |</code></pre> <p>Link every PR (<code>[#N](url)</code>, never bare <code>#N</code>). Call out anything a worker left blocked or flagged for input, and note any stack/merge order from the serial remainder.</p> </section> </section> <section id="graceful-degradation-to-serial" class="level2"> <h2>Graceful degradation to serial</h2> <p>If the <code>Agent</code> tool isn’t available in the session, you can’t fan out — fall back to <a href="../gii/SKILL.md"><code>gii</code></a> and work the whole in-scope set serially. The per-issue work and the final report are the same; only the concurrency is lost.</p> </section> <section id="orchestration" class="level2"> <h2>Orchestration</h2> <p>GIP already fans out — it is the manual form of this pattern, one worktree-isolated subagent per provably-independent issue. When the harness supports it, prefer driving that fan-out through a <strong>Workflow</strong> (per <code>shared/workflow/when-to-orchestrate.md</code>): the deterministic pipeline gives each issue the same implement — open-PR — ARDI chain plus worktree isolation, rather than ad-hoc subagents. The independence gate and the concurrency cap stay unchanged — only provably-independent issues run at once, and still capped (the shared-runner limit the fragment describes). Launch directly when an opt-in signal is present; otherwise propose with a cost estimate first.</p> </section> <section id="relationship-to-other-skills" class="level2"> <h2>Relationship to other skills</h2> <ul> <li><strong><code>gii</code></strong> / <strong><code>gis</code></strong> — the <strong>serial</strong> counterpart and the safe fallback. GIP is GII with the independent subset lifted out and run concurrently; everything GIP can’t prove independent goes back through GII. (gii : gip :: the write loop stays series, the safe subset fans out.)</li> <li><strong><code>gi</code></strong> / <strong><code>grab-issue</code></strong> — the per-issue flow each subagent runs (claim → history → open draft PR → implement → mark ready → ARDI). GIP restates it inline because subagents start fresh.</li> <li><strong><code>pr-on-claim</code></strong> — the rule behind each subagent’s step 4: open the draft PR up front so parallel workers see the in-flight issue before implementing.</li> <li><strong><code>gia</code></strong> — clears the whole queue (clean open PRs, then work issues); compose GIP into its issue phase when that phase’s issues are independent.</li> <li><strong><code>ardi</code></strong> — each subagent ARDIs its own PR to clean.</li> <li><strong><code>ardia</code></strong> — the whole-queue <em>PR</em> write loop; it stays <strong>series</strong> for the same runner-contention reason GIP caps its concurrency. Contrast, not overlap.</li> <li><strong><code>pr-status-all</code></strong> — the read-only fan-out exemplar (one subagent per PR). GIP is the <em>write</em> fan-out: same one-subagent-per-unit shape, but it needs worktree isolation and an independence gate because its units mutate state.</li> <li><strong><code>check-history</code></strong> — each subagent runs it before implementing.</li> <li><strong><code>session-lock</code></strong> — the worktree isolation GIP gives each subagent is the subagent form of the isolation session-lock sets up for top-level sessions.</li> <li><strong><code>defer-issue</code></strong> / <strong><code>split-concerns</code></strong> — used inside a subagent when its issue spawns sub-tasks or grows too large.</li> </ul> </section> <section id="anti-patterns" class="level2"> <h2>Anti-patterns</h2> <ul> <li>❌ Fanning out issues that touch the same files — guaranteed merge conflicts. Run them serially (gii) instead.</li> <li>❌ Fanning out a stacked issue whose base is another unmerged in-batch branch. It belongs in the serial remainder.</li> <li>❌ Skipping <code>isolation: "worktree"</code> — parallel subagents in one checkout clobber each other’s edits and branches.</li> <li>❌ Skipping the independence gate and parallelizing the whole backlog blindly — the failure mode <code>gii</code> exists to avoid.</li> <li>❌ Unbounded fan-out — swamps CI runners and the review bot. Cap at ~3 and run in waves.</li> <li>❌ Writing the subagent prompt as if it inherits this skill’s text — it doesn’t; restate the full per-issue procedure inline.</li> <li>❌ Re-running the triage / independence gate inside each subagent — that’s once-per-run orchestrator work.</li> </ul> <div id="quarto-navigation-envelope" class="hidden"> <p><span class="hidden quarto-markdown-envelope-contents" data-render-id="cXVhcnRvLWludC1zaWRlYmFyLXRpdGxl">ai-config</span></p> <p><span class="hidden quarto-markdown-envelope-contents" data-render-id="cXVhcnRvLWludC1uYXZiYXItdGl0bGU=">ai-config</span></p> <p><span class="hidden quarto-markdown-envelope-contents" data-render-id="cXVhcnRvLWludC1uYXZiYXI6SG9tZQ==">Home</span></p> <p><span class="hidden quarto-markdown-envelope-contents" data-render-id="cXVhcnRvLWludC1uYXZiYXI6L2luZGV4Lmh0bWw=">/index.html</span></p> <p><span class="hidden quarto-markdown-envelope-contents" data-render-id="cXVhcnRvLWludC1uYXZiYXI6U2V0dXA=">Setup</span></p> <p><span class="hidden quarto-markdown-envelope-contents" data-render-id="cXVhcnRvLWludC1uYXZiYXI6L3NldHVwLmh0bWw=">/setup.html</span></p> <p><span class="hidden quarto-markdown-envelope-contents" data-render-id="cXVhcnRvLWludC1uYXZiYXI6U2tpbGxz">Skills</span></p> <p><span class="hidden quarto-markdown-envelope-contents" data-render-id="cXVhcnRvLWludC1uYXZiYXI6L3NraWxscy5odG1s">/skills.html</span></p> <p><span class="hidden quarto-markdown-envelope-contents" data-render-id="cXVhcnRvLWludC1uYXZiYXI6QWdlbnRz">Agents</span></p> <p><span class="hidden quarto-markdown-envelope-contents" data-render-id="cXVhcnRvLWludC1uYXZiYXI6L2FnZW50cy5odG1s">/agents.html</span></p> <p><span class="hidden quarto-markdown-envelope-contents" data-render-id="cXVhcnRvLWludC1uYXZiYXI6V29ya2Zsb3c=">Workflow</span></p> <p><span class="hidden quarto-markdown-envelope-contents" data-render-id="cXVhcnRvLWludC1uYXZiYXI6L3dvcmtmbG93Lmh0bWw=">/workflow.html</span></p> <p><span class="hidden quarto-markdown-envelope-contents" data-render-id="cXVhcnRvLWludC1uYXZiYXI6aHR0cHM6Ly9naXRodWIuY29tL2QtbW9ycmlzb24vYWktY29uZmln">https://github.com/d-morrison/ai-config</span></p> <div class="hidden quarto-markdown-envelope-contents" data-render-id="Zm9vdGVyLWNlbnRlcg=="> <p>Built with <a href="https://quarto.org/">Quarto</a></p> </div> </div> <div id="quarto-meta-markdown" class="hidden"> <p><span class="hidden quarto-markdown-envelope-contents" data-render-id="cXVhcnRvLW1ldGF0aXRsZQ==">ai-config</span></p> <p><span class="hidden quarto-markdown-envelope-contents" data-render-id="cXVhcnRvLXR3aXR0ZXJjYXJkdGl0bGU=">ai-config</span></p> <p><span class="hidden quarto-markdown-envelope-contents" data-render-id="cXVhcnRvLW9nY2FyZHRpdGxl">ai-config</span></p> <p><span class="hidden quarto-markdown-envelope-contents" data-render-id="cXVhcnRvLW1ldGFzaXRlbmFtZQ==">ai-config</span></p> <p><span class="hidden quarto-markdown-envelope-contents" data-render-id="cXVhcnRvLXR3aXR0ZXJjYXJkZGVzYw==">Grab Issues in Parallel: grab several provably-independent open issues and work them concurrently — one worktree-isolated subagent per issue, each implementing its issue, opening an MR/PR, and ARDI-ing it to clean. The parallel counterpart to the deliberately-serial <code>gii</code>. Use when asked to ‘gip’, ‘grab issues in parallel’, ‘work several issues at once’, ‘parallelize the backlog’, ‘do these issues concurrently’, or ‘fan out the issue queue’.</span></p> <p><span class="hidden quarto-markdown-envelope-contents" data-render-id="cXVhcnRvLW9nY2FyZGRkZXNj">Grab Issues in Parallel: grab several provably-independent open issues and work them concurrently — one worktree-isolated subagent per issue, each implementing its issue, opening an MR/PR, and ARDI-ing it to clean. The parallel counterpart to the deliberately-serial <code>gii</code>. Use when asked to ‘gip’, ‘grab issues in parallel’, ‘work several issues at once’, ‘parallelize the backlog’, ‘do these issues concurrently’, or ‘fan out the issue queue’.</span></p> <p><span class="hidden quarto-markdown-envelope-contents" data-render-id="cXVhcnRvLW1ldGFzaXRlZGVzYw==">Portable AI agent config — skills, memories, and commands synced across machines via git</span></p> </div> </section> </section> </main> <!-- /main --> <script id = "quarto-html-after-body" type="application/javascript"> window.document.addEventListener("DOMContentLoaded", function (event) { // Ensure there is a toggle, if there isn't float one in the top right if (window.document.querySelector('.quarto-color-scheme-toggle') === null) { const a = window.document.createElement('a'); a.classList.add('top-right'); a.classList.add('quarto-color-scheme-toggle'); a.href = ""; a.onclick = function() { try { window.quartoToggleColorScheme(); } catch {} return false; }; const i = window.document.createElement("i"); i.classList.add('bi'); a.appendChild(i); window.document.body.appendChild(a); } setColorSchemeToggle(hasAlternateSentinel()) const icon = ""; const anchorJS = new window.AnchorJS(); anchorJS.options = { placement: 'right', icon: icon }; anchorJS.add('.anchored'); const isCodeAnnotation = (el) => { for (const clz of el.classList) { if (clz.startsWith('code-annotation-')) { return true; } } return false; } const onCopySuccess = function(e) { // button target const button = e.trigger; // don't keep focus button.blur(); // flash "checked" button.classList.add('code-copy-button-checked'); var currentTitle = button.getAttribute("title"); button.setAttribute("title", "Copied!"); let tooltip; if (window.bootstrap) { button.setAttribute("data-bs-toggle", "tooltip"); button.setAttribute("data-bs-placement", "left"); button.setAttribute("data-bs-title", "Copied!"); tooltip = new bootstrap.Tooltip(button, { trigger: "manual", customClass: "code-copy-button-tooltip", offset: [0, -8]}); tooltip.show(); } setTimeout(function() { if (tooltip) { tooltip.hide(); button.removeAttribute("data-bs-title"); button.removeAttribute("data-bs-toggle"); button.removeAttribute("data-bs-placement"); } button.setAttribute("title", currentTitle); button.classList.remove('code-copy-button-checked'); }, 1000); // clear code selection e.clearSelection(); } const getTextToCopy = function(trigger) { const outerScaffold = trigger.parentElement.cloneNode(true); const codeEl = outerScaffold.querySelector('code'); for (const childEl of codeEl.children) { if (isCodeAnnotation(childEl)) { childEl.remove(); } } return codeEl.innerText; } const clipboard = new window.ClipboardJS('.code-copy-button:not([data-in-quarto-modal])', { text: getTextToCopy }); clipboard.on('success', onCopySuccess); if (window.document.getElementById('quarto-embedded-source-code-modal')) { const clipboardModal = new window.ClipboardJS('.code-copy-button[data-in-quarto-modal]', { text: getTextToCopy, container: window.document.getElementById('quarto-embedded-source-code-modal') }); clipboardModal.on('success', onCopySuccess); } var localhostRegex = new RegExp(/^(?:http|https):\/\/localhost\:?[0-9]*\//); var mailtoRegex = new RegExp(/^mailto:/); var filterRegex = new RegExp("https:\/\/d-morrison\.github\.io\/ai-config\/"); var isInternal = (href) => { return filterRegex.test(href) || localhostRegex.test(href) || mailtoRegex.test(href); } // Inspect non-navigation links and adorn them if external var links = window.document.querySelectorAll('a[href]:not(.nav-link):not(.navbar-brand):not(.toc-action):not(.sidebar-link):not(.sidebar-item-toggle):not(.pagination-link):not(.no-external):not([aria-hidden]):not(.dropdown-item):not(.quarto-navigation-tool):not(.about-link)'); for (var i=0; i<links.length; i++) { const link = links[i]; if (!isInternal(link.href)) { // undo the damage that might have been done by quarto-nav.js in the case of // links that we want to consider external if (link.dataset.originalHref !== undefined) { link.href = link.dataset.originalHref; } // default icon link.classList.add("external"); } } function tippyHover(el, contentFn, onTriggerFn, onUntriggerFn) { const config = { allowHTML: true, maxWidth: 500, delay: 100, arrow: false, appendTo: function(el) { return el.parentElement; }, interactive: true, interactiveBorder: 10, theme: 'quarto', placement: 'bottom-start', }; if (contentFn) { config.content = contentFn; } if (onTriggerFn) { config.onTrigger = onTriggerFn; } if (onUntriggerFn) { config.onUntrigger = onUntriggerFn; } window.tippy(el, config); } const noterefs = window.document.querySelectorAll('a[role="doc-noteref"]'); for (var i=0; i<noterefs.length; i++) { const ref = noterefs[i]; tippyHover(ref, function() { // use id or data attribute instead here let href = ref.getAttribute('data-footnote-href') || ref.getAttribute('href'); try { href = new URL(href).hash; } catch {} const id = href.replace(/^#\/?/, ""); const note = window.document.getElementById(id); if (note) { return note.innerHTML; } else { return ""; } }); } const xrefs = window.document.querySelectorAll('a.quarto-xref'); const processXRef = (id, note) => { // Strip column container classes const stripColumnClz = (el) => { el.classList.remove("page-full", "page-columns"); if (el.children) { for (const child of el.children) { stripColumnClz(child); } } } stripColumnClz(note) if (id === null || id.startsWith('sec-')) { // Special case sections, only their first couple elements const container = document.createElement("div"); if (note.children && note.children.length > 2) { container.appendChild(note.children[0].cloneNode(true)); for (let i = 1; i < note.children.length; i++) { const child = note.children[i]; if (child.tagName === "P" && child.innerText === "") { continue; } else { container.appendChild(child.cloneNode(true)); break; } } if (window.Quarto?.typesetMath) { window.Quarto.typesetMath(container); } return container.innerHTML } else { if (window.Quarto?.typesetMath) { window.Quarto.typesetMath(note); } return note.innerHTML; } } else { // Remove any anchor links if they are present const anchorLink = note.querySelector('a.anchorjs-link'); if (anchorLink) { anchorLink.remove(); } if (window.Quarto?.typesetMath) { window.Quarto.typesetMath(note); } if (note.classList.contains("callout")) { return note.outerHTML; } else { return note.innerHTML; } } } for (var i=0; i<xrefs.length; i++) { const xref = xrefs[i]; tippyHover(xref, undefined, function(instance) { instance.disable(); let url = xref.getAttribute('href'); let hash = undefined; if (url.startsWith('#')) { hash = url; } else { try { hash = new URL(url).hash; } catch {} } if (hash) { const id = hash.replace(/^#\/?/, ""); const note = window.document.getElementById(id); if (note !== null) { try { const html = processXRef(id, note.cloneNode(true)); instance.setContent(html); } finally { instance.enable(); instance.show(); } } else { // See if we can fetch this fetch(url.split('#')[0]) .then(res => res.text()) .then(html => { const parser = new DOMParser(); const htmlDoc = parser.parseFromString(html, "text/html"); const note = htmlDoc.getElementById(id); if (note !== null) { const html = processXRef(id, note); instance.setContent(html); } }).finally(() => { instance.enable(); instance.show(); }); } } else { // See if we can fetch a full url (with no hash to target) // This is a special case and we should probably do some content thinning / targeting fetch(url) .then(res => res.text()) .then(html => { const parser = new DOMParser(); const htmlDoc = parser.parseFromString(html, "text/html"); const note = htmlDoc.querySelector('main.content'); if (note !== null) { // This should only happen for chapter cross references // (since there is no id in the URL) // remove the first header if (note.children.length > 0 && note.children[0].tagName === "HEADER") { note.children[0].remove(); } const html = processXRef(null, note); instance.setContent(html); } }).finally(() => { instance.enable(); instance.show(); }); } }, function(instance) { }); } let selectedAnnoteEl; const selectorForAnnotation = ( cell, annotation) => { let cellAttr = 'data-code-cell="' + cell + '"'; let lineAttr = 'data-code-annotation="' + annotation + '"'; const selector = 'span[' + cellAttr + '][' + lineAttr + ']'; return selector; } const selectCodeLines = (annoteEl) => { const doc = window.document; const targetCell = annoteEl.getAttribute("data-target-cell"); const targetAnnotation = annoteEl.getAttribute("data-target-annotation"); const annoteSpan = window.document.querySelector(selectorForAnnotation(targetCell, targetAnnotation)); const lines = annoteSpan.getAttribute("data-code-lines").split(","); const lineIds = lines.map((line) => { return targetCell + "-" + line; }) let top = null; let height = null; let parent = null; if (lineIds.length > 0) { //compute the position of the single el (top and bottom and make a div) const el = window.document.getElementById(lineIds[0]); top = el.offsetTop; height = el.offsetHeight; parent = el.parentElement.parentElement; if (lineIds.length > 1) { const lastEl = window.document.getElementById(lineIds[lineIds.length - 1]); const bottom = lastEl.offsetTop + lastEl.offsetHeight; height = bottom - top; } if (top !== null && height !== null && parent !== null) { // cook up a div (if necessary) and position it let div = window.document.getElementById("code-annotation-line-highlight"); if (div === null) { div = window.document.createElement("div"); div.setAttribute("id", "code-annotation-line-highlight"); div.style.position = 'absolute'; parent.appendChild(div); } div.style.top = top - 2 + "px"; div.style.height = height + 4 + "px"; div.style.left = 0; let gutterDiv = window.document.getElementById("code-annotation-line-highlight-gutter"); if (gutterDiv === null) { gutterDiv = window.document.createElement("div"); gutterDiv.setAttribute("id", "code-annotation-line-highlight-gutter"); gutterDiv.style.position = 'absolute'; const codeCell = window.document.getElementById(targetCell); const gutter = codeCell.querySelector('.code-annotation-gutter'); gutter.appendChild(gutterDiv); } gutterDiv.style.top = top - 2 + "px"; gutterDiv.style.height = height + 4 + "px"; } selectedAnnoteEl = annoteEl; } }; const unselectCodeLines = () => { const elementsIds = ["code-annotation-line-highlight", "code-annotation-line-highlight-gutter"]; elementsIds.forEach((elId) => { const div = window.document.getElementById(elId); if (div) { div.remove(); } }); selectedAnnoteEl = undefined; }; // Handle positioning of the toggle window.addEventListener( "resize", throttle(() => { elRect = undefined; if (selectedAnnoteEl) { selectCodeLines(selectedAnnoteEl); } }, 10) ); function throttle(fn, ms) { let throttle = false; let timer; return (...args) => { if(!throttle) { // first call gets through fn.apply(this, args); throttle = true; } else { // all the others get throttled if(timer) clearTimeout(timer); // cancel #2 timer = setTimeout(() => { fn.apply(this, args); timer = throttle = false; }, ms); } }; } // Attach click handler to the DT const annoteDls = window.document.querySelectorAll('dt[data-target-cell]'); for (const annoteDlNode of annoteDls) { annoteDlNode.addEventListener('click', (event) => { const clickedEl = event.target; if (clickedEl !== selectedAnnoteEl) { unselectCodeLines(); const activeEl = window.document.querySelector('dt[data-target-cell].code-annotation-active'); if (activeEl) { activeEl.classList.remove('code-annotation-active'); } selectCodeLines(clickedEl); clickedEl.classList.add('code-annotation-active'); } else { // Unselect the line unselectCodeLines(); clickedEl.classList.remove('code-annotation-active'); } }); } const findCites = (el) => { const parentEl = el.parentElement; if (parentEl) { const cites = parentEl.dataset.cites; if (cites) { return { el, cites: cites.split(' ') }; } else { return findCites(el.parentElement) } } else { return undefined; } }; var bibliorefs = window.document.querySelectorAll('a[role="doc-biblioref"]'); for (var i=0; i<bibliorefs.length; i++) { const ref = bibliorefs[i]; const citeInfo = findCites(ref); if (citeInfo) { tippyHover(citeInfo.el, function() { var popup = window.document.createElement('div'); citeInfo.cites.forEach(function(cite) { var citeDiv = window.document.createElement('div'); citeDiv.classList.add('hanging-indent'); citeDiv.classList.add('csl-entry'); var biblioDiv = window.document.getElementById('ref-' + cite); if (biblioDiv) { citeDiv.innerHTML = biblioDiv.innerHTML; } popup.appendChild(citeDiv); }); return popup.innerHTML; }); } } }); </script> </div> <!-- /content --> <footer class="footer"> <div class="nav-footer"> <div class="nav-footer-left">   </div> <div class="nav-footer-center"> <div class='footer-contents'>Built with [Quarto](https://quarto.org/)</div> </div> <div class="nav-footer-right">   </div> </div> </footer> </body> </html>
Back to top