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(); """