From 35daceb4f7a6fbbe6cbd0997f2073c0d63f41cad Mon Sep 17 00:00:00 2001 From: Josh Mabry Date: Sun, 28 Jun 2026 02:57:40 -0700 Subject: [PATCH] =?UTF-8?q?fix(board):=20boot=20once=20=E2=80=94=20stop=20?= =?UTF-8?q?the=20issue-list=20flicker/thrash=20on=20mount=20(v0.1.3,=20#13?= =?UTF-8?q?)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Both view pages called `boot()` twice: `kit.initPluginView(boot)` (the kit invokes it after the theme/auth handshake) AND a direct `boot()` right after. Two overlapping boot sequences each fetched config and called `load()`, which sets the list to "Loading…" then replaces it with data — so the board cycled Loading→data→Loading→data on mount. The direct call also fired before the auth handshake. - Drop the redundant direct `boot()` on both PAGE and NEW_ISSUE_PAGE — boot only via `kit.initPluginView` (the fallback kit still invokes it immediately when the DS kit can't load, so offline/degraded mode is unaffected). - Harden `load()` with a monotonic token so a slow in-flight fetch that resolves after a newer load() started (rapid tab/filter clicks) drops its stale result instead of clobbering the fresh list. - Regression test: each page boots via the kit exactly once (no direct `boot();`). Closes #13. Co-Authored-By: Claude Opus 4.8 (1M context) --- protoagent.plugin.yaml | 2 +- pyproject.toml | 2 +- tests/test_board_view.py | 10 ++++++++++ view.py | 14 +++++++++++--- 4 files changed, 23 insertions(+), 5 deletions(-) diff --git a/protoagent.plugin.yaml b/protoagent.plugin.yaml index 4f07141..7c03672 100644 --- a/protoagent.plugin.yaml +++ b/protoagent.plugin.yaml @@ -2,7 +2,7 @@ # Keep `version` in lockstep with pyproject.toml (tests/test_version.py asserts it). id: github name: GitHub (read/write tools) -version: 0.1.2 +version: 0.1.3 description: >- Read AND write GitHub tools over the `gh` CLI, with PER-AGENT write gating. The read tools (PRs, issues, diffs, CI, repo files/contents) are always on; the write diff --git a/pyproject.toml b/pyproject.toml index 2699f81..cf2fc98 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "github-plugin" -version = "0.1.2" +version = "0.1.3" description = "Read/write GitHub tools for protoAgent over the gh CLI, with per-agent write gating." requires-python = ">=3.11" diff --git a/tests/test_board_view.py b/tests/test_board_view.py index 3a4e732..8440efa 100644 --- a/tests/test_board_view.py +++ b/tests/test_board_view.py @@ -89,6 +89,16 @@ def test_board_page_is_read_only(): assert "/api/plugins/github/issues" in PAGE and "/api/plugins/github/prs" in PAGE # reads only +def test_pages_boot_once(): + """#13 regression — each page boots via the kit ONCE. A second direct ``boot();`` ran two + overlapping config+load sequences → the list flicker/thrash on mount.""" + from ghplugin.view import NEW_ISSUE_PAGE, PAGE + + for page in (PAGE, NEW_ISSUE_PAGE): + assert "kit.initPluginView(boot)" in page # booted via the handshake callback + assert "boot();" not in page # …and NOT also called directly + + def test_config_route_returns_repos_and_default(): c = TestClient(_app()) body = c.get("/api/plugins/github/config").json() diff --git a/view.py b/view.py index 9b2cdb5..31ff2c7 100644 --- a/view.py +++ b/view.py @@ -101,7 +101,12 @@ + ' '+labelPills(it.labels)+''; } + // A monotonic token so an in-flight load() whose fetch resolves AFTER a newer load() + // started (rapid tab/filter clicks, or any double-trigger) drops its stale result instead + // of clobbering the fresh list — no thrash. + let loadSeq = 0; async function load(){ + const my = ++loadSeq; const repo = $("repo").value, list = $("list"); if(!repo){ list.innerHTML = '
No repositories configured. Add some under Settings ▸ GitHub (github.repos).
'; return; } list.innerHTML = '
Loading…
'; @@ -109,11 +114,12 @@ + "?repo="+encodeURIComponent(repo)+"&state="+encodeURIComponent($("state").value); try { const data = await kit.apiFetch(path).then(r => r.json()); + if(my !== loadSeq) return; // a newer load() superseded this one — drop the stale result if(data.error){ list.innerHTML = '
'+esc(data.error)+'
'; return; } const items = data.items||[]; if(!items.length){ list.innerHTML = '
No '+tab+' for this filter.
'; return; } list.innerHTML = items.map(tab==="issues"?issueRow:prRow).join(""); - } catch(e){ list.innerHTML = '
Failed to load — is the agent reachable?
'; } + } catch(e){ if(my===loadSeq) list.innerHTML = '
Failed to load — is the agent reachable?
'; } } async function boot(){ @@ -128,8 +134,10 @@ $("t-issues").onclick = () => setTab("issues"); $("t-prs").onclick = () => setTab("prs"); $("repo").onchange = load; $("state").onchange = load; $("refresh").onclick = load; + // Boot ONCE — via the kit so it runs after the theme/auth handshake (the fallback kit calls + // it immediately). A second direct boot() here caused two overlapping config+load sequences + // → the list flicker/thrash on mount (#13). kit.initPluginView(boot); - boot(); """ @@ -195,6 +203,6 @@ } catch(e){ $("res").textContent = "Request failed."; } } $("submit").onclick = submit; + // Boot ONCE via the kit (after the theme/auth handshake) — not also directly (#13). kit.initPluginView(boot); - boot(); """