Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/cognit/mcp/assets/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;700&display=swap">
<link rel="stylesheet" href="/static/styles.css">
</head>
<body>
<body data-pr-url="__PR_URL_ATTR__" data-branch="__BRANCH_ATTR__">

<!-- decorative topbar: borrows GitHub chrome, branded cognit -->
<header class="topbar">
Expand Down
41 changes: 36 additions & 5 deletions src/cognit/mcp/assets/quiz_mcp.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,17 @@ let view = null; // waiting | answering | results | published
// client-side answer to reveal.)
let examMode = localStorage.getItem("cognit.examMode") === "1";

// GitHub deep-linking for anchors: repo blob base derived from the PR url, plus the
// PR's head branch (both injected into <body> by the server). Empty in tests/demos.
const PR_URL = document.body.dataset.prUrl || "";
const BRANCH = document.body.dataset.branch || "";
const REPO_URL = PR_URL.replace(/\/pull\/\d+.*$/, ""); // → https://github.com/owner/repo
function githubFileUrl(path, startLine, endLine) {
if (!REPO_URL || !BRANCH) return null;
const frag = startLine === endLine ? `#L${startLine}` : `#L${startLine}-L${endLine}`;
return `${REPO_URL}/blob/${BRANCH}/${path}${frag}`;
}

// ── small DOM helper ────────────────────────────────────────────
function el(tag, attrs = {}, children = []) {
const node = document.createElement(tag);
Expand Down Expand Up @@ -359,15 +370,35 @@ function renderAnchor(q) {
if (!a || !a.path) return null;
const range = a.start_line === a.end_line ? `${a.start_line}` : `${a.start_line}–${a.end_line}`;
const body = el("div", { class: "codepanel__body" });
const panel = el("div", { class: "codepanel codepanel--inline" }, [
el("div", { class: "codepanel__summary" }, [
el("span", { class: "codepanel__file", text: a.path }),
// Default to plain text; upgrade to a GitHub link only once we've confirmed the path
// is actually one of the PR's changed files — so a hallucinated/non-diff anchor path
// never produces a broken (404) link. The link targets the file on the PR's branch.
const fileEl = el("span", { class: "codepanel__file", text: a.path });
const href = githubFileUrl(a.path, a.start_line, a.end_line);
if (href) {
ensureChangedFiles().then((files) => {
if (!files.some((f) => fileMatchesAnchor(f, a.path))) return; // not in the diff → no link
const link = el("a", {
class: "codepanel__file", href, target: "_blank", rel: "noopener",
title: `Open ${a.path} on GitHub`, text: a.path,
onclick: (e) => e.stopPropagation(), // follow the link without toggling the panel
});
fileEl.replaceWith(link);
});
}
const details = el("details", { class: "codepanel" }, [ // collapsible, closed by default
el("summary", { class: "codepanel__summary" }, [
el("span", { class: "codepanel__icon", "aria-hidden": "true", text: "▸" }),
fileEl,
el("span", { class: "codepanel__lines", text: `:${range}` }),
]),
body,
]);
loadHunk(a.path, a, body); // fetch + render immediately; inline, no click needed
return panel;
let loaded = false;
details.addEventListener("toggle", () => {
if (details.open && !loaded) { loaded = true; loadHunk(a.path, a, body); } // fetch on first expand
});
return details;
}

const DETERMINISTIC = new Set(["mcq", "tf", "mermaid"]);
Expand Down
12 changes: 10 additions & 2 deletions src/cognit/mcp/assets/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -819,6 +819,7 @@ textarea.open:focus { border-color: var(--blue); outline: none; box-shadow: 0 0
@media (prefers-reduced-motion: reduce) {
.gen__spinner { animation: none; }
.term::after { animation: none; }
.codepanel__icon { transition: none; }
}

/* inline code context (anchors) ──────────────────────────────────── */
Expand All @@ -833,11 +834,18 @@ textarea.open:focus { border-color: var(--blue); outline: none; box-shadow: 0 0
align-items: baseline;
gap: 4px;
padding: 8px 12px;
cursor: pointer;
font-family: var(--mono);
font-size: 12px;
color: var(--fg-mute);
}
.codepanel__file { color: var(--fg); font-weight: 500; }
list-style: none;
user-select: none;
}
.codepanel__summary::-webkit-details-marker { display: none; }
.codepanel__icon { color: var(--fg-faint); transition: transform 0.12s ease; display: inline-block; }
.codepanel[open] > .codepanel__summary .codepanel__icon { transform: rotate(90deg); }
.codepanel__file { color: var(--fg); font-weight: 500; text-decoration: none; }
a.codepanel__file:hover { color: var(--blue); text-decoration: underline; }
.codepanel__lines { color: var(--fg-faint); }
.codepanel__body { border-top: 1px solid var(--border-mute); }
.codepanel pre.diff {
Expand Down
1 change: 1 addition & 0 deletions src/cognit/mcp/launch.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ def build_launch_spec(
"COGNIT_HTTP_PORT": str(port),
"COGNIT_SNAPSHOT_PATH": str(snapshot_path),
"COGNIT_REPO_ROOT": str(repo_root),
"COGNIT_BRANCH": branch, # so the browser can deep-link anchors to files on this branch
}
argv = [
"claude",
Expand Down
12 changes: 10 additions & 2 deletions src/cognit/mcp/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -161,7 +161,13 @@ def file_diff(path: str) -> str:


def _start_web(
state: QuizState, *, llm: LLMClient, pr_url: str, port: int, diffs: _DiffProvider
state: QuizState,
*,
llm: LLMClient,
pr_url: str,
port: int,
diffs: _DiffProvider,
branch: str,
) -> None:
app = build_web_app(
state,
Expand All @@ -170,6 +176,7 @@ def _start_web(
diff_section=lambda path: do_file_diff(path, diffs.sections()),
changed_files=lambda: list(diffs.sections()),
pr_url=pr_url,
branch=branch,
)
url = f"http://127.0.0.1:{port}"

Expand Down Expand Up @@ -213,6 +220,7 @@ def main() -> None:
pr_number = int(os.environ["COGNIT_PR_NUMBER"])
port = int(os.environ["COGNIT_HTTP_PORT"])
snapshot = Path(os.environ["COGNIT_SNAPSHOT_PATH"])
branch = os.environ.get("COGNIT_BRANCH", "")

state = QuizState(pr_number=pr_number, snapshot_path=snapshot)
# Best-effort early collision detection: probe the port so an obvious conflict
Expand All @@ -229,7 +237,7 @@ def main() -> None:
threading.Thread(
target=_start_web,
args=(state,),
kwargs={"llm": llm, "pr_url": pr_url, "port": port, "diffs": diffs},
kwargs={"llm": llm, "pr_url": pr_url, "port": port, "diffs": diffs, "branch": branch},
daemon=True,
).start()
_build_mcp(state, llm, diffs).run()
3 changes: 3 additions & 0 deletions src/cognit/mcp/web.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ def build_web_app(
diff_section: Callable[[str], str] | None = None,
changed_files: Callable[[], list[str]] | None = None,
pr_url: str = "",
branch: str = "",
) -> FastAPI:
"""Browser projection over `state`.

Expand All @@ -56,11 +57,13 @@ def build_web_app(
app = FastAPI()
app.mount("/static", StaticFiles(directory=str(_ASSETS_DIR)), name="static")
pr_url_attr = _html.escape(pr_url, quote=True)
branch_attr = _html.escape(branch, quote=True)
index_html = (
(_ASSETS_DIR / "index.html")
.read_text()
.replace("__PR__", str(state.pr_number))
.replace("__PR_URL_ATTR__", pr_url_attr)
.replace("__BRANCH_ATTR__", branch_attr)
)

@app.get("/state")
Expand Down
1 change: 1 addition & 0 deletions tests/mcp/test_launch.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ def test_launch_spec_has_confined_flags(tmp_path: Path):
assert spec.env["COGNIT_HTTP_PORT"] == "8123"
assert spec.env["COGNIT_SNAPSHOT_PATH"] == str(tmp_path / "s.json")
assert spec.env["COGNIT_REPO_ROOT"] == str(tmp_path)
assert spec.env["COGNIT_BRANCH"] == "feat/x"


def test_mcp_config_points_at_cognit_module(tmp_path: Path):
Expand Down
27 changes: 27 additions & 0 deletions tests/mcp/test_web.py
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,33 @@ def fake_grade() -> Results:
t.join(timeout=2)


def test_index_includes_branch_for_github_links(tmp_path: Path) -> None:
state = QuizState(pr_number=7, snapshot_path=tmp_path / "s.json")
state.set_quiz(
Quiz(
pr_number=7,
questions=[
MCQQuestion(id="q1", prompt="p", options=["A", "B"], answer="A", explanation="x")
],
)
)
app = build_web_app(
state,
post_comment=lambda b: "http://c/1",
branch="feat/cool-thing",
pr_url="https://github.com/o/r/pull/7",
)
port = _free_port()
server, t = _serve(app, port)
try:
html = httpx.get(f"http://127.0.0.1:{port}/").text
assert 'data-branch="feat/cool-thing"' in html
assert "__BRANCH_ATTR__" not in html # placeholder templated out
finally:
server.should_exit = True
t.join(timeout=2)


def test_grade_endpoint_501_when_unavailable(client):
# the default fixture app wires no `grade` callable
c, _state, _ = client
Expand Down
Loading