Skip to content
This repository was archived by the owner on Aug 1, 2026. It is now read-only.
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
5 changes: 5 additions & 0 deletions internal/web/frontend/src/components/Markdown.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,11 @@ export function Markdown({ text }: { text: string }) {
if (href?.startsWith('ref:')) {
return <Ref token={href.slice('ref:'.length)} />
}
// An in-app route (the hash router's `#/…`) is navigation, not an
// outbound link: it stays in this tab.
if (href?.startsWith('#')) {
return <a href={href}>{children}</a>
}
return (
<a href={href} target="_blank" rel="noreferrer">
{children}
Expand Down
22 changes: 20 additions & 2 deletions internal/web/frontend/src/components/Rail.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -146,18 +146,18 @@ function PlansRail({ version, current }: { version: number; current: string | nu
function WikiRail({ version, current }: { version: number; current: string | null }) {
const [pages, setPages] = useState<WikiPage[]>([])
const [categories, setCategories] = useState<string[]>([])
const [hasIndex, setHasIndex] = useState(false)
useEffect(() => {
api
.wiki()
.then((w) => {
setPages(w?.pages ?? [])
setCategories(w?.categories ?? [])
setHasIndex(!!w?.has_index)
})
.catch(() => setPages([]))
Comment on lines 150 to 158

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Clear hasIndex when the overview refresh fails.

After a prior successful load, this handler clears pages but retains hasIndex; the rail can show an obsolete Index row after the index is removed or the request fails.

Proposed fix
-      .catch(() => setPages([]))
+      .catch(() => {
+        setPages([])
+        setCategories([])
+        setHasIndex(false)
+      })
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
useEffect(() => {
api
.wiki()
.then((w) => {
setPages(w?.pages ?? [])
setCategories(w?.categories ?? [])
setHasIndex(!!w?.has_index)
})
.catch(() => setPages([]))
useEffect(() => {
api
.wiki()
.then((w) => {
setPages(w?.pages ?? [])
setCategories(w?.categories ?? [])
setHasIndex(!!w?.has_index)
})
.catch(() => {
setPages([])
setCategories([])
setHasIndex(false)
})
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/web/frontend/src/components/Rail.tsx` around lines 150 - 158, Update
the wiki overview error handler in the useEffect callback to reset hasIndex to
false alongside clearing pages when api.wiki() fails, preventing stale index
state after an unsuccessful refresh.

}, [version])

if (pages.length === 0) return <div className="side-body muted small">no pages yet</div>

// Grouped by the index.md catalog, in the order the catalog lists them; pages
// absent from it fall into a trailing group (the read model sorts them last).
const groups = [...categories, '\0'].map((c) => ({
Expand All @@ -167,6 +167,24 @@ function WikiRail({ version, current }: { version: number; current: string | nul

return (
<>
{/* The index is the wiki's home page, so it is a row of its own above the
catalog it produced — `#/wiki` with no slug lands there. */}
{hasIndex && (
<div className="side-body">
<div className="resource-list">
<a
className={`rail-row resource-row ${pages.some((p) => p.slug === current) ? '' : 'active'}`}
href={href('wiki')}
title="docs/wiki/index.md"
>
<div className="resource-row-head">
<span className="resource-name">Index</span>
</div>
</a>
</div>
</div>
)}
{pages.length === 0 && <div className="side-body muted small">no pages yet</div>}
{groups
.filter((g) => g.pages.length > 0)
.map((g) => (
Expand Down
99 changes: 97 additions & 2 deletions internal/web/frontend/src/components/WikiView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,17 +25,112 @@ export function WikiView({ slug, version }: { slug: string | null; version: numb
if (!ov.present) return <NoWiki />
if (ov.pages.length === 0) return <EmptyWiki hasIndex={ov.has_index} />

const current = (slug && ov.pages.find((p) => p.slug === slug)) || ov.pages[0]
// A route with no slug — or a slug that no longer exists — lands on the wiki's
// own front door: docs/wiki/index.md. The catalog is authored, so it is the
// page that says what this knowledge base is; opening whichever page happened
// to sort first never did.
const current = slug ? ov.pages.find((p) => p.slug === slug) : undefined

return (
<div className="wiki-view">
<section className="wiki-page">
<WikiPagePane page={current} all={ov} version={version} />
{current ? (
<WikiPagePane page={current} all={ov} version={version} />
) : ov.has_index ? (
<WikiIndexPane all={ov} version={version} />
) : (
<WikiPagePane page={ov.pages[0]} all={ov} version={version} />
)}
</section>
</div>
)
}

const WIKI_INDEX_PATH = 'docs/wiki/index.md'

// WikiIndexPane renders docs/wiki/index.md as the wiki's home page: the author's
// own prose and categories, not a landing page synthesised from the read model.
// Its entries point at files on disk, so they are rewritten into app routes
// before rendering — the catalog has to navigate.
function WikiIndexPane({ all, version }: { all: WikiOverview; version: number }) {
const [text, setText] = useState<string | null>(null)
const [err, setErr] = useState<string | null>(null)

useEffect(() => {
setText(null)
setErr(null)
api
.file(WIKI_INDEX_PATH)
.then((f) => setText(f.text ?? ''))
.catch((e) => setErr(String(e)))
}, [version])

const known = useMemo(() => new Set(all.pages.map((p) => p.slug)), [all])
const { title, body } = useMemo(() => splitLeadingHeading(text ?? ''), [text])
const rendered = useMemo(() => rewriteIndexEntries(rewriteRefTokens(body), known), [body, known])
const unlisted = all.pages.filter((p) => !p.in_index)

return (
<div className="wiki-page-inner">
<header className="wiki-page-head">
<h1>{title || 'Wiki'}</h1>
<div className="wiki-page-meta">
<span className="badge muted">
{all.pages.length} page{all.pages.length === 1 ? '' : 's'}
</span>
{unlisted.length > 0 && <span className="badge attention">{unlisted.length} not in index.md</span>}
</div>
</header>

<div className="wiki-body">
{err ? (
<div className="banner error">{err}</div>
) : text == null ? (
<div className="empty">Loading…</div>
) : (
<Markdown text={rendered} />
)}
</div>

<footer className="wiki-page-foot">
{unlisted.length > 0 && (
<div className="wiki-foot-block">
<div className="wiki-foot-title">Not in the index</div>
{unlisted.map((p) => (
<a key={p.slug} className="chip-link" href={href('wiki', p.slug)}>
{p.title}
</a>
))}
</div>
)}
<div className="wiki-foot-block">
<span className="muted small">{WIKI_INDEX_PATH}</span>
</div>
</footer>
</div>
)
}

// The pane header carries the title, so a leading `# Heading` is lifted out of
// the body rather than rendered a second time under it.
function splitLeadingHeading(text: string): { title: string; body: string } {
const m = /^\s*#\s+(.+?)[ \t]*(?:\n|$)/.exec(text)
if (!m) return { title: '', body: text }
return { title: m[1], body: text.slice(m[0].length) }
}

// index.md links a page as `[Title](pages/<slug>.md)` because it is a real file
// beside them. In the dashboard those have to be routes: a slug that exists
// becomes an in-app link; one that does not becomes a citation token, so a stale
// entry renders as the broken reference it is instead of a dead file link.
const INDEX_ENTRY_RE = /\]\(\s*(?:\.\/)?(?:[^)\s]*\/)?pages\/([^)\s/]+)\.md\s*\)/g

function rewriteIndexEntries(markdown: string, known: Set<string>): string {
return markdown.replace(INDEX_ENTRY_RE, (_m, slug: string) =>
known.has(slug) ? `](${href('wiki', slug)})` : `](ref:[[${slug}]])`,
)
Comment on lines +126 to +131

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Restrict rewriting to actual local Markdown links.

The arbitrary optional prefix matches external URLs such as https://example.com/pages/foo.md, incorrectly hijacking them into wiki routes. The global replacement also rewrites examples inside inline/fenced code. Only process pages/... or ./pages/... links outside code regions.

Proposed fix
-const INDEX_ENTRY_RE = /\]\(\s*(?:\.\/)?(?:[^)\s]*\/)?pages\/([^)\s/]+)\.md\s*\)/g
+const INDEX_ENTRY_RE = /\]\(\s*(?:\.\/)?pages\/([^)\s/]+)\.md\s*\)/g

 function rewriteIndexEntries(markdown: string, known: Set<string>): string {
-  return markdown.replace(INDEX_ENTRY_RE, (_m, slug: string) =>
-    known.has(slug) ? `](${href('wiki', slug)})` : `](ref:[[${slug}]])`,
-  )
+  const parts = markdown.split(/(```[\s\S]*?```|`[^`\n]*`)/g)
+  return parts
+    .map((part, i) =>
+      i % 2 === 1
+        ? part
+        : part.replace(INDEX_ENTRY_RE, (_m, slug: string) =>
+            known.has(slug) ? `](${href('wiki', slug)})` : `](ref:[[${slug}]])`,
+          ),
+    )
+    .join('')
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const INDEX_ENTRY_RE = /\]\(\s*(?:\.\/)?(?:[^)\s]*\/)?pages\/([^)\s/]+)\.md\s*\)/g
function rewriteIndexEntries(markdown: string, known: Set<string>): string {
return markdown.replace(INDEX_ENTRY_RE, (_m, slug: string) =>
known.has(slug) ? `](${href('wiki', slug)})` : `](ref:[[${slug}]])`,
)
const INDEX_ENTRY_RE = /\]\(\s*(?:\.\/)?pages\/([^)\s/]+)\.md\s*\)/g
function rewriteIndexEntries(markdown: string, known: Set<string>): string {
const parts = markdown.split(/(
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/web/frontend/src/components/WikiView.tsx` around lines 126 - 131,
Restrict INDEX_ENTRY_RE in rewriteIndexEntries to match only local pages links
beginning with pages/ or ./pages/, removing the arbitrary prefix that captures
external URLs. Split the markdown into code and non-code regions, leave inline
and fenced code regions unchanged, and apply the existing replacement logic only
to non-code parts before joining them back together.

}

function WikiPagePane({ page, all, version }: { page: WikiPage; all: WikiOverview; version: number }) {
const [text, setText] = useState<string | null>(null)
const [err, setErr] = useState<string | null>(null)
Expand Down
Loading