Pre-launch — Gluecron is in final validation. Public signups and git hosting for non-owner users open after launch review.
Commit621081e

feat(repo-nav): clean primary bar + ✨AI/More dropdowns, one consistent nav everywhere

feat(repo-nav): clean primary bar + ✨AI/More dropdowns, one consistent nav everywhere

The repo nav was two problems at once: (1) 26 tabs crammed into one
horizontally-scrolling row (GitHub shows ~9), and (2) three DIFFERENT
navs on the same repo — web.tsx pages rendered the full 26-item
RepoNav, while pulls.tsx (PrNav, 4 tabs) and issues.tsx (IssueNav, 3
tabs, also imported by ai-explain/ai-tests/compare/nl-search/
semantic-search/migration-assistant/ai-changelog) rendered their own
stripped bars — a deliberate workaround from when RepoNav was treated
as locked. So clicking "Commits" gave 26 tabs and "Pull Requests" gave
4, with no way to reach Settings/Actions/etc from the PR page.

Owner-directed redesign (chosen: clean primary + dropdowns):
- Primary bar (~7): Code, Issues, Pull Requests, Actions, Security,
  Insights, Settings.
- "✨ AI ▾" dropdown: Explain, Ask AI, Workspace, Spec to PR, Tests,
  NL Search, Debt Map, Archaeology.
- "More ▾" dropdown: Commits, Discussions, Wiki, Projects, Releases,
  Contributors, Pulse, Gates, Deployments, Pipeline, Agents, Traffic
  (owner-only — existing currentUser===repoOwner gate preserved).

Dropdowns use native <details>/<summary> (no framework, accessible,
mobile-friendly) with a positioned panel; a small idempotent
outside-click script closes an open menu. When the active item lives
in a dropdown, its trigger is marked active so you can see which
section you're in.

Consistency: PrNav and IssueNav are now thin wrappers over RepoNav, so
all ~10 pages that used them render the ONE canonical nav with zero
call-site churn. RepoNav's `active` prop union is unchanged (a
superset), so every existing caller keeps compiling.

RepoNav is a §4.7 locked view contract; this restructure is
owner-directed this session (the required sign-off) and
contract-preserving (same props, every destination retained, only
relocated). typecheck 0 errors; bun test 3107 pass, same 4 pre-existing
unrelated fails.
ccantynz-alt committed on July 22, 2026Parent: 9b776da
4 files changed+180188621081ed57b30d085205bb75e922a860d82bdee6
4 changed files+180−188
Modifiedsrc/routes/issues.tsx+7−9View fileUnifiedSplit
23712371);
23722372
23732373// Shared nav component with issues tab
2374// Thin wrapper over the shared RepoNav so this page — and the ~7 other
2375// route files that import IssueNav (ai-explain, ai-tests, compare,
2376// nl-search, semantic-search, migration-assistant, ai-changelog) — all
2377// render the ONE canonical repo nav instead of a stripped 3-tab bar.
2378// Previously a deliberate minimal nav because RepoNav was treated as
2379// locked; owner-directed nav unification this session reverses that.
23742380const IssueNav = ({
23752381 owner,
23762382 repo,
23792385 owner: string;
23802386 repo: string;
23812387 active: "code" | "commits" | "issues";
2382}) => (
2383 <TabNav
2384 tabs={[
2385 { label: "Code", href: `/${owner}/${repo}`, active: active === "code" },
2386 { label: "Issues", href: `/${owner}/${repo}/issues`, active: active === "issues" },
2387 { label: "Commits", href: `/${owner}/${repo}/commits`, active: active === "commits" },
2388 ]}
2389 />
2390);
2388}) => <RepoNav owner={owner} repo={repo} active={active} />;
23912389
23922390// Re-run AI triage on demand (e.g. after the issue body has been edited).
23932391// Bypasses ISSUE_TRIAGE_MARKER via { force: true }. Write-access only.
Modifiedsrc/routes/pulls.tsx+5−11View fileUnifiedSplit
3030 pendingReviewComments,
3131} from "../db/schema";
3232import { Layout } from "../views/layout";
33import { RepoHeader } from "../views/components";
33import { RepoHeader, RepoNav } from "../views/components";
3434import { PendingCommentsBanner } from "../views/pending-comments-banner";
3535import { DiffView, type InlineDiffComment } from "../views/diff-view";
3636import { ReactionsBar } from "../views/reactions";
21892189}
21902190
21912191// PR Nav helper
2192// Thin wrapper over the shared RepoNav so the PR pages render the ONE
2193// canonical repo nav instead of a stripped 4-tab bar. Owner-directed nav
2194// unification this session.
21922195const PrNav = ({
21932196 owner,
21942197 repo,
21972200 owner: string;
21982201 repo: string;
21992202 active: "code" | "issues" | "pulls" | "commits";
2200}) => (
2201 <TabNav
2202 tabs={[
2203 { label: "Code", href: `/${owner}/${repo}`, active: active === "code" },
2204 { label: "Issues", href: `/${owner}/${repo}/issues`, active: active === "issues" },
2205 { label: "Pull Requests", href: `/${owner}/${repo}/pulls`, active: active === "pulls" },
2206 { label: "Commits", href: `/${owner}/${repo}/commits`, active: active === "commits" },
2207 ]}
2208 />
2209);
2203}) => <RepoNav owner={owner} repo={repo} active={active} />;
22102204
22112205/**
22122206 * Block M3 — pre-merge risk score card. Pure presentational helper.
Modifiedsrc/views/components.tsx+90−168View fileUnifiedSplit
185185 currentUser?: string | null;
186186 /** Repo owner username — used for owner-only tab gating. */
187187 repoOwner?: string;
188}> = ({ owner, repo, active, currentUser, repoOwner }) => (
189 <div class="repo-nav">
190 <a href={`/${owner}/${repo}`} class={active === "code" ? "active" : ""}>
191 Code
192 </a>
193 <a
194 href={`/${owner}/${repo}/issues`}
195 class={active === "issues" ? "active" : ""}
196 >
197 Issues
198 </a>
199 <a
200 href={`/${owner}/${repo}/discussions`}
201 class={active === "discussions" ? "active" : ""}
202 >
203 Discussions
204 </a>
205 <a
206 href={`/${owner}/${repo}/wiki`}
207 class={active === "wiki" ? "active" : ""}
208 >
209 Wiki
210 </a>
211 <a
212 href={`/${owner}/${repo}/pulls`}
213 class={active === "pulls" ? "active" : ""}
214 >
215 Pull Requests
216 </a>
217 <a
218 href={`/${owner}/${repo}/projects`}
219 class={active === "projects" ? "active" : ""}
220 >
221 Projects
222 </a>
223 <a
224 href={`/${owner}/${repo}/commits`}
225 class={active === "commits" ? "active" : ""}
226 >
227 Commits
228 </a>
229 <a
230 href={`/${owner}/${repo}/actions`}
231 class={active === "actions" ? "active" : ""}
232 >
233 Actions
234 </a>
235 <a
236 href={`/${owner}/${repo}/releases`}
237 class={active === "releases" ? "active" : ""}
238 >
239 Releases
240 </a>
241 <a
242 href={`/${owner}/${repo}/contributors`}
243 class={active === "contributors" ? "active" : ""}
244 >
245 Contributors
246 </a>
247 <a
248 href={`/${owner}/${repo}/pulse`}
249 class={active === "pulse" ? "active" : ""}
250 >
251 Pulse
252 </a>
253 {currentUser && repoOwner && currentUser === repoOwner && (
254 <a
255 href={`/${owner}/${repo}/traffic`}
256 class={active === "traffic" ? "active" : ""}
188}> = ({ owner, repo, active, currentUser, repoOwner }) => {
189 const base = `/${owner}/${repo}`;
190 const isOwner = !!(currentUser && repoOwner && currentUser === repoOwner);
191
192 // key typed as `string` so comparisons against the (narrower) `active`
193 // union never trip TS2367 \u2014 every key below is a legitimate destination.
194 type NavItem = { key: string; href: string; label: string };
195
196 const primary: NavItem[] = [
197 { key: "code", href: base, label: "Code" },
198 { key: "issues", href: `${base}/issues`, label: "Issues" },
199 { key: "pulls", href: `${base}/pulls`, label: "Pull Requests" },
200 { key: "actions", href: `${base}/actions`, label: "Actions" },
201 { key: "security", href: `${base}/security/vulnerabilities`, label: "Security" },
202 { key: "insights", href: `${base}/insights`, label: "Insights" },
203 { key: "settings", href: `${base}/settings`, label: "Settings" },
204 ];
205
206 const aiItems: NavItem[] = [
207 { key: "explain", href: `${base}/explain`, label: "Explain" },
208 { key: "ask", href: `${base}/ask`, label: "Ask AI" },
209 { key: "workspace", href: `${base}/workspace`, label: "Workspace" },
210 { key: "spec", href: `${base}/spec`, label: "Spec to PR" },
211 { key: "tests", href: `${base}/ai/tests`, label: "Tests" },
212 { key: "nl-search", href: `${base}/search/nl`, label: "NL Search" },
213 { key: "debt-map", href: `${base}/debt-map`, label: "Debt Map" },
214 { key: "archaeology", href: `${base}/archaeology`, label: "Archaeology" },
215 ];
216
217 const moreItems: NavItem[] = [
218 { key: "commits", href: `${base}/commits`, label: "Commits" },
219 { key: "discussions", href: `${base}/discussions`, label: "Discussions" },
220 { key: "wiki", href: `${base}/wiki`, label: "Wiki" },
221 { key: "projects", href: `${base}/projects`, label: "Projects" },
222 { key: "releases", href: `${base}/releases`, label: "Releases" },
223 { key: "contributors", href: `${base}/contributors`, label: "Contributors" },
224 { key: "pulse", href: `${base}/pulse`, label: "Pulse" },
225 { key: "gates", href: `${base}/gates`, label: "Gates" },
226 { key: "deployments", href: `${base}/cloud-deployments`, label: "Deployments" },
227 { key: "pipeline", href: `${base}/pipeline`, label: "Pipeline" },
228 { key: "agents", href: `${base}/agents`, label: "Agents" },
229 ...(isOwner ? [{ key: "traffic", href: `${base}/traffic`, label: "Traffic" }] : []),
230 ];
231
232 const aiActive = aiItems.some((i) => i.key === active);
233 const moreActive = moreItems.some((i) => i.key === active);
234
235 return (
236 <div class="repo-nav">
237 {primary.map((it) => (
238 <a href={it.href} class={active === it.key ? "active" : ""}>
239 {it.label}
240 </a>
241 ))}
242 <details
243 class={`repo-nav-menu repo-nav-ai-menu${aiActive ? " is-active" : ""}`}
244 style="margin-left:auto"
257245 >
258 Traffic
259 </a>
260 )}
261 <a
262 href={`/${owner}/${repo}/gates`}
263 class={active === "gates" ? "active" : ""}
264 >
265 {"\u25CF"} Gates
266 </a>
267 <a
268 href={`/${owner}/${repo}/security/vulnerabilities`}
269 class={active === "security" ? "active" : ""}
270 >
271 Security
272 </a>
273 <a
274 href={`/${owner}/${repo}/settings`}
275 class={active === "settings" ? "active" : ""}
276 >
277 Settings
278 </a>
279 <a
280 href={`/${owner}/${repo}/cloud-deployments`}
281 class={active === "deployments" ? "active" : ""}
282 >
283 Deployments
284 </a>
285 <a
286 href={`/${owner}/${repo}/pipeline`}
287 class={active === "pipeline" ? "active" : ""}
288 >
289 Pipeline
290 </a>
291 <a
292 href={`/${owner}/${repo}/insights`}
293 class={active === "insights" ? "active" : ""}
294 >
295 Insights
296 </a>
297 <a
298 href={`/${owner}/${repo}/agents`}
299 class={active === "agents" ? "active" : ""}
300 >
301 Agents
302 </a>
303 <a
304 href={`/${owner}/${repo}/explain`}
305 class={`repo-nav-ai${active === "explain" ? " active" : ""}`}
306 style="margin-left: auto"
307 >
308 {"\u2728"} Explain
309 </a>
310 <a href={`/${owner}/${repo}/ask`} class="repo-nav-ai">
311 {"\u2728"} Ask AI
312 </a>
313 <a
314 href={`/${owner}/${repo}/workspace`}
315 class={`repo-nav-ai${active === "workspace" ? " active" : ""}`}
316 title="AI Workspace — spec-to-PR, fix issues with AI, web editor"
317 >
318 {"✨"} Workspace
319 </a>
320 <a
321 href={`/${owner}/${repo}/spec`}
322 class="repo-nav-ai"
323 title="Spec to PR — paste a feature spec, AI opens a draft PR"
324 >
325 {"\u2728"} Spec
326 </a>
327 <a
328 href={`/${owner}/${repo}/ai/tests`}
329 class="repo-nav-ai"
330 title="AI Tests \u2014 generate failing test stubs from a source file"
331 >
332 {"\u2728"} Tests
333 </a>
334 <a
335 href={`/${owner}/${repo}/debt-map`}
336 class={`repo-nav-ai${active === "debt-map" ? " active" : ""}`}
337 title="AI Debt Map \u2014 visual technical debt graph with Claude analysis"
338 >
339 {"\u2593"} Debt Map
340 </a>
341 <a
342 href={`/${owner}/${repo}/search/nl`}
343 class={`repo-nav-ai${active === "nl-search" ? " active" : ""}`}
344 title="Natural Language Search \u2014 search by intent, not keywords"
345 >
346 {"\u2728"} NL Search
347 </a>
348 <a
349 href={`/${owner}/${repo}/archaeology`}
350 class={`repo-nav-ai${active === "archaeology" ? " active" : ""}`}
351 title="AI Archaeology \u2014 excavate why any file exists using git history, PRs, and issues"
352 >
353 {"\ud83c\udfdb"} Archaeology
354 </a>
355 </div>
356);
246 <summary class="repo-nav-ai" title="AI-native features \u2014 review, chat, spec-to-PR, and more">
247 {"\u2728"} AI
248 </summary>
249 <div class="repo-nav-menu-panel repo-nav-menu-panel-right">
250 {aiItems.map((it) => (
251 <a href={it.href} class={active === it.key ? "active" : ""}>
252 {it.label}
253 </a>
254 ))}
255 </div>
256 </details>
257 <details class={`repo-nav-menu${moreActive ? " is-active" : ""}`}>
258 <summary>More</summary>
259 <div class="repo-nav-menu-panel repo-nav-menu-panel-right">
260 {moreItems.map((it) => (
261 <a href={it.href} class={active === it.key ? "active" : ""}>
262 {it.label}
263 </a>
264 ))}
265 </div>
266 </details>
267 <script
268 dangerouslySetInnerHTML={{
269 __html:
270 "if(!window.__repoNavMenuInit){window.__repoNavMenuInit=1;" +
271 "document.addEventListener('click',function(e){" +
272 "document.querySelectorAll('details.repo-nav-menu[open]').forEach(function(d){" +
273 "if(!d.contains(e.target))d.removeAttribute('open');});});}",
274 }}
275 />
276 </div>
277 );
278};
357279
358280export const BranchSwitcher: FC<{
359281 owner: string;
Modifiedsrc/views/layout.tsx+78−0View fileUnifiedSplit
25262526 font-weight: 600;
25272527 }
25282528
2529 /* Repo-nav dropdown menus (✨ AI / More) — native <details> disclosure */
2530 .repo-nav-menu {
2531 position: relative;
2532 display: inline-flex;
2533 align-items: stretch;
2534 }
2535 .repo-nav-menu > summary {
2536 list-style: none;
2537 cursor: pointer;
2538 padding: 11px 14px;
2539 color: var(--text-muted);
2540 font-size: var(--t-sm);
2541 font-weight: 500;
2542 white-space: nowrap;
2543 border-bottom: 2px solid transparent;
2544 margin-bottom: -1px;
2545 user-select: none;
2546 display: inline-flex;
2547 align-items: center;
2548 gap: 5px;
2549 transition: color var(--t-fast) var(--ease), background var(--t-fast) var(--ease);
2550 }
2551 .repo-nav-menu > summary::-webkit-details-marker { display: none; }
2552 .repo-nav-menu > summary::after {
2553 content: "\\25BE";
2554 font-size: 9px;
2555 opacity: 0.55;
2556 }
2557 .repo-nav-menu > summary:hover {
2558 color: var(--text-strong);
2559 background: var(--bg-hover);
2560 }
2561 .repo-nav-menu.is-active > summary { color: var(--text-strong); font-weight: 600; }
2562 .repo-nav-menu[open] > summary { color: var(--text-strong); background: var(--bg-hover); }
2563 .repo-nav-ai-menu > summary { color: var(--accent) !important; }
2564 .repo-nav-ai-menu > summary:hover {
2565 color: var(--accent-hover) !important;
2566 background: var(--accent-gradient-faint) !important;
2567 }
2568
2569 .repo-nav-menu-panel {
2570 position: absolute;
2571 top: 100%;
2572 left: 0;
2573 margin-top: 6px;
2574 min-width: 200px;
2575 background: var(--bg-elevated);
2576 border: 1px solid var(--border);
2577 border-radius: var(--radius);
2578 box-shadow: 0 10px 30px rgba(0,0,0,0.22);
2579 padding: 6px;
2580 z-index: 60;
2581 display: flex;
2582 flex-direction: column;
2583 gap: 1px;
2584 }
2585 .repo-nav-menu-panel-right { left: auto; right: 0; }
2586 /* Override the inherited .repo-nav a tab styling inside the dropdown panel */
2587 .repo-nav-menu-panel a {
2588 position: static;
2589 display: block;
2590 padding: 7px 10px;
2591 border-radius: 7px;
2592 border-bottom: none;
2593 margin-bottom: 0;
2594 color: var(--text-muted);
2595 font-size: var(--t-sm);
2596 font-weight: 500;
2597 white-space: nowrap;
2598 }
2599 .repo-nav-menu-panel a::after { display: none !important; }
2600 .repo-nav-menu-panel a:hover { color: var(--text-strong); background: var(--bg-hover); }
2601 .repo-nav-menu-panel a.active {
2602 color: var(--text-strong);
2603 font-weight: 600;
2604 background: rgba(91,110,232,0.14);
2605 }
2606
25292607 .breadcrumb {
25302608 display: flex;
25312609 gap: 6px;
25322610