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
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,12 @@

All notable changes to Context are documented here.

## 0.7.63 - 2026-10-13

- Section landing pages of a knowledge website show each first-level group as a card, two per row and one on narrow screens, instead of one long list.
- A `site.nav` link with `position: "start"` appears before the knowledge sections, for example a documentation home link.
- More menu targets and other pages outside every section no longer show the first section's sidebar or highlight it in the top bar; the More menu is highlighted instead.

## 0.7.62 - 2026-10-13

- Approved articles targeted by `site.more` no longer need a knowledge map entry: package builds, knowledge map adjustments and structure approval accept them, and review reports no longer list them as unplaced.
Expand Down
30 changes: 15 additions & 15 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "context",
"version": "0.7.62",
"version": "0.7.63",
"packageManager": "bun@1.3.9",
"repository": {
"type": "git",
Expand Down
2 changes: 1 addition & 1 deletion packages/context-cli/context-workflow/provider.yaml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
schema: agent-graph.provider.v1
id: c4a/context
version: 0.7.62
version: 0.7.63
name: Context workflow
description: Internal work contract for Context knowledge workspaces.
graphs:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -143,9 +143,16 @@ a section shows only its descendants in the left sidebar; opening an article
directly selects its section. Skills and their Markdown references remain
reachable as linked read-only pages, but have no automatically added navigation
section. A reader-facing usage guide can link them where appropriate.
Section landing pages have stable URLs separate from article URLs.

`site.nav` appends links after the reading directories, and `site.more` adds
Section landing pages have stable URLs separate from article URLs. They show
each first-level group of the section as a card, two per row and one on
narrow screens; articles directly under the section share one card, or get one
card each when the section has no groups. Pages outside every section, such as
More menu targets, LLM Docs and Changelog, show no sidebar and highlight no
section.

`site.nav` appends links after the reading directories; a link with
`position: "start"` goes before them instead, for example a documentation home
link. `site.more` adds
items to the More menu before LLM Docs and Changelog. A More item uses either
`link` or `target: { artifact_ref }`; a target is an approved article kept
outside the knowledge map (for example release notes or a contribution guide),
Expand Down
2 changes: 1 addition & 1 deletion packages/context-cli/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "@c4a/context-cli",
"description": "Local runtime and Agent integration for traceable knowledge production",
"version": "0.7.62",
"version": "0.7.63",
"type": "module",
"license": "MIT",
"engines": {
Expand Down
17 changes: 15 additions & 2 deletions packages/context-cli/src/__tests__/packageSite.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ const pkg = kbPackage({ name: "sample", template: "src/templates", site: { title
title: "Sample knowledge", slogan: "Answers for the work ahead", description: "A connected knowledge map.",
resources: [{ title: "Project workspace", description: "Review the source workspace.",
href: "https://example.com/workspace", action: "Open workspace", command: "context status", featured: true }],
} } });
}, nav: [{ text: "Overview", link: "/docs/", position: "start" }, { text: "Blog", link: "https://example.com/blog" }] } });
const selected = [{ relPath: "codeindex/app/start.md", absPath: "/unused", content: '---\ntitle: Start\nartifact_ref: article:start\n---\n# Start\n<!-- context:section id="entry" -->\n## Entry\n' },
{ relPath: "codeindex/app/next.md", absPath: "/unused", content: '---\ntitle: Next\nartifact_ref: article:next\n---\n# Next\n' }].map(file => ({ ...file,
article: { article_id: file.relPath.includes("start") ? "article:start" : "article:next", path: file.relPath,
Expand Down Expand Up @@ -183,8 +183,21 @@ test("VitePress builds an independent site with search, safe prose, anchors and
for (const section of map.sections) {
expect(await readFile(join(root, packageSiteOutputDir(pkg), section.href.slice(1)), "utf8")).toContain(section.title);
}
// A group becomes its own card; loose articles share one card.
const business = await readFile(join(root, packageSiteOutputDir(pkg), map.sections[0].href.slice(1)), "utf8");
expect(business).toContain('class="context-section-grid"');
expect(business.match(/class="context-section-card"/gu)).toHaveLength(2);
expect(business).toMatch(/class="context-section-card"><h2[^>]*>Folder[\s\S]*?Nested next/u);
const navMenu = article.match(/<nav\b[^>]*class="VPNavBarMenu[\s\S]*?<\/nav>/u)![0];
expect(navMenu.indexOf("Overview")).toBeLessThan(navMenu.indexOf("Business"));
expect(navMenu.indexOf("Business")).toBeLessThan(navMenu.indexOf("Blog"));
expect(article).not.toContain("context-empty-sidebar");
const skill = map.pages.find((page: { package_path: string }) => page.package_path === "skills/search/SKILL.md");
expect(await readFile(join(root, packageSiteOutputDir(pkg), skill.site_path), "utf8")).toContain("Search guide");
const skillPage = await readFile(join(root, packageSiteOutputDir(pkg), skill.site_path), "utf8");
expect(skillPage).toContain("Search guide");
// Pages outside every section do not borrow the first section's sidebar.
expect(skillPage).toContain("context-empty-sidebar");
expect(skillPage).not.toMatch(/class="VPSidebarItem level-0/u);
expect(map.warnings).toHaveLength(2);
// A failed additional channel must not replace the prior package.
await expect(withStagedPackageOutput(root, pkg, async staged => {
Expand Down
2 changes: 1 addition & 1 deletion packages/context-cli/src/project/indexerBaseContracts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import { bundledMarkdownReaderQuestionContracts } from
"./indexerBaseMarkdownAuthoringCatalog.js";

const BASE_CONTRACT_VERSION = "1.1.0";
export const BUNDLED_INDEXER_PARSER_PACKAGE_VERSION = "0.7.62";
export const BUNDLED_INDEXER_PARSER_PACKAGE_VERSION = "0.7.63";
const BUNDLED_PARSER_REQUIREMENTS = buildIndexerParserCapabilityRequirements(
BUNDLED_INDEXER_PARSER_PACKAGE_VERSION,
);
Expand Down
20 changes: 18 additions & 2 deletions packages/context-cli/src/project/packageSite.ts
Original file line number Diff line number Diff line change
Expand Up @@ -318,11 +318,27 @@ export async function writePackageSite(input: {
lines.push(`${" ".repeat(level)}- ${entry.href ? `[${title}](${entry.href})` : title}`);
menu(lines, entry.children, level + 1, tag);
});
// Each first-level group is one card; loose articles share a final card.
// Blank lines around the wrappers keep the card contents parsed as Markdown.
const card = (lines: string[], heading: string | undefined, entries: ProjectedKnowledgeMapEntry[], tag: string) => {
lines.push('<div class="context-section-card">', "");
if (heading !== undefined) lines.push(`## ${heading}`, "");
menu(lines, entries, 0, tag);
lines.push("", "</div>", "");
};
await mkdir(join(temporary, "sections"), { recursive: true });
for (const section of sections) {
await writeFile(join(temporary, section.href.slice(1).replace(/\.html$/u, ".md")), localizer.variants(tag => {
const lines = [`# ${mdLabel(localizer.entryTitle(tag, section.entry))}`, ""];
menu(lines, section.entries, 0, tag);
const lines = [`# ${mdLabel(localizer.entryTitle(tag, section.entry))}`, "", '<div class="context-section-grid">', ""];
const groups = section.entries.filter(entry => entry.children.length > 0);
const articles = section.entries.filter(entry => entry.children.length === 0);
for (const group of groups) {
card(lines, group.href ? `[${mdLabel(localizer.entryTitle(tag, group))}](${group.href})` : mdLabel(localizer.entryTitle(tag, group)),
group.children, tag);
}
if (groups.length === 0) for (const article of articles) card(lines, undefined, [article], tag);
else if (articles.length) card(lines, undefined, articles, tag);
lines.push("</div>");
return lines.join("\n") + "\n";
}));
}
Expand Down
5 changes: 3 additions & 2 deletions packages/context-cli/src/project/packageSiteLocales.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { readTranslatedArticle, readTranslatedMapTitles } from "./siteTranslatio
import type { SitePage } from "./packageSite.js";

type KbSite = NonNullable<Extract<PackageDefinition, { kind: "package.kb" }>["site"]>;
export interface SiteNavItem { text: string; link: string; origin?: true; locales?: Record<string, string> }
export interface SiteNavItem { text: string; link: string; origin?: true; position?: "start"; locales?: Record<string, string> }
export interface LocalizedSiteSummary { tag: string; translated: number; stale: number; missing: number }
export interface SiteReaderLocales {
primary: string; tags: string[]; translated: boolean;
Expand Down Expand Up @@ -75,7 +75,8 @@ export function siteNavigationConfig(site: KbSite, pages: readonly SitePage[]) {
return { text: item.text, link: item.link!, ...(item.link!.startsWith("/") ? { origin: true as const } : {}), ...(item.locales ? { locales: item.locales } : {}) };
});
const nav: SiteNavItem[] = (site.nav ?? []).map(item => ({ text: item.text, link: item.link,
...(item.link.startsWith("/") ? { origin: true as const } : {}), ...(item.locales ? { locales: item.locales } : {}) }));
...(item.link.startsWith("/") ? { origin: true as const } : {}), ...(item.position === "start" ? { position: "start" as const } : {}),
...(item.locales ? { locales: item.locales } : {}) }));
return { nav, more };
}

Expand Down
49 changes: 41 additions & 8 deletions packages/context-cli/src/project/packageSiteTheme.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,17 @@ export const siteMarkdownConfig = String.raw`
return true;
}, { alt: ['paragraph'] });
md.renderer.rules.context_anchor = (tokens, index) => '<a id="' + md.utils.escapeHtml(tokens[index].content) + '"></a>\n';
// Only the compiler writes section landing pages; accept exactly its card wrappers there.
const sectionWrappers = new Set(['<div class="context-section-grid">', '<div class="context-section-card">', '</div>']);
md.block.ruler.before('paragraph', 'context_section_card', (state, line, end, silent) => {
if (!/(?:^|\/)sections\/[0-9a-f]+\.md$/.test(state.env.relativePath || '')) return false;
const raw = state.src.slice(state.bMarks[line] + state.tShift[line], state.eMarks[line]);
if (!sectionWrappers.has(raw)) return false;
if (!silent) { const token = state.push('context_section_card', '', 0); token.content = raw; }
state.line = line + 1;
return true;
}, { alt: ['paragraph'] });
md.renderer.rules.context_section_card = (tokens, index) => tokens[index].content + '\n';
// The generated website config declares contextLocaleMarker; null when the site has no translations.
const contextLocale = typeof contextLocaleMarker === 'string' ? contextLocaleMarker : null;
md.block.ruler.before('paragraph', 'context_locale', (state, line, end, silent) => {
Expand Down Expand Up @@ -286,10 +297,13 @@ export default {
return part('year') + '-' + part('month') + '-' + part('day') + ' ' +
part('hour') + ':' + part('minute') + ':' + part('second') + ' ' + part('timeZoneName');
});
const sitePath = () => {
const base = data.site.value.base;
return route.path.startsWith(base) ? '/' + route.path.slice(base.length) : route.path;
};
watch(() => route.path, () => {
allSources.value = false;
const base = data.site.value.base;
const path = route.path.startsWith(base) ? '/' + route.path.slice(base.length) : route.path;
const path = sitePath();
const direct = sections.value.find(section => section.href === path);
const current = sections.value.find(section => section.key === selected.value);
selected.value = (direct || (current?.pages.includes(path) ? current :
Expand All @@ -299,6 +313,12 @@ export default {
const inLlms = computed(() => route.path.startsWith(data.site.value.base + 'llms/'));
const inHistory = computed(() => route.path === data.site.value.base + 'changelog.html');
const inHome = computed(() => data.frontmatter.value.layout === 'home');
// Articles outside the knowledge map, such as More menu targets, belong to no section.
const inMore = computed(() => (data.theme.value.contextMore || []).some(item => !item.origin && item.link === sitePath()));
const outsideSections = computed(() => inLlms.value || inHistory.value || inMore.value || (!inHome.value &&
!sections.value.some(section => section.href === sitePath() || section.pages.includes(sitePath()))));
const userNav = position => (data.theme.value.contextNav || [])
.filter(item => (item.position === 'start') === (position === 'start')).map(navLink);
// Descendants, including site extensions, read the reader language as lang.
// The frontmatter carries it too so the content refreshes its outline on a switch.
provide(dataSymbol, { ...data, lang: computed(() => locale.value), frontmatter: computed(() => {
Expand All @@ -307,15 +327,15 @@ export default {
return { ...value, contextLocale: locale.value, ...(actions ? { hero: { ...value.hero, actions } } : {}) };
}), theme: computed(() => ({ ...data.theme.value,
...data.theme.value.contextUiLabels?.[chinese.value ? 'zh' : 'en'],
sidebar: inLlms.value || inHistory.value ? [] : active.value?.items || [],
nav: [...sections.value.map(section => ({ text: section.label, link: section.href,
activeMatch: !inLlms.value && !inHistory.value && section.key === selected.value ? '^' : '(?!)' })),
...(data.theme.value.contextNav || []).map(navLink),
sidebar: outsideSections.value ? [] : active.value?.items || [],
nav: [...userNav('start'), ...sections.value.map(section => ({ text: section.label, link: section.href,
activeMatch: !outsideSections.value && section.key === selected.value ? '^' : '(?!)' })),
...userNav('end'),
{ text: bothHtml('更多', 'More'), items: [
...(data.theme.value.contextMore || []).map(navLink),
{ text: 'LLM Docs', link: '/llms/index.html' },
{ text: 'Changelog', link: '/changelog.html' }],
activeMatch: inLlms.value || inHistory.value ? '^' : '(?!)' }],
activeMatch: inLlms.value || inHistory.value || inMore.value ? '^' : '(?!)' }],
})) });
const slots = {
'nav-bar-title-before': () => extensions.brand ? h('span', { class: 'context-brand' }, custom('brand')) : null,
Expand Down Expand Up @@ -383,7 +403,7 @@ export default {
};
// History belongs between the page heading and footer metadata.
return () => h(DefaultTheme.Layout, { class: inHome.value ? 'context-home' : data.page.value.isNotFound ? 'context-missing' :
inLlms.value || inHistory.value ? 'context-empty-sidebar' : undefined }, { ...slots, 'doc-after': undefined,
outsideSections.value ? 'context-empty-sidebar' : undefined }, { ...slots, 'doc-after': undefined,
'doc-footer-before': () => [slots['doc-after'](), slots['doc-footer-before']()] });
}
},
Expand Down Expand Up @@ -663,6 +683,19 @@ ${readerContentStyles(".vp-doc")}
.context-sources li + li { margin-top: 6px; }
.context-sources a { color: inherit; text-decoration: none; border-bottom: 1px solid transparent; }
.context-sources a:hover { color: var(--vp-c-brand-1); border-color: currentColor; }
/* Section landing pages: two cards per row, one on narrow screens. */
.context-section-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 14px; margin-top: 24px; }
.vp-doc .context-section-card { min-width: 0; padding: 18px 20px; border: 1px solid var(--vp-c-divider); border-radius: 12px;
background: color-mix(in srgb, var(--vp-c-bg) 88%, var(--vp-c-bg-soft)); }
.vp-doc .context-section-card:hover { border-color: color-mix(in srgb, var(--vp-c-brand-1) 42%, var(--vp-c-divider)); }
.vp-doc .context-section-card h2 { margin: 0 0 10px; padding: 0 0 10px; border-top: 0; border-bottom: 1px solid var(--vp-c-divider); font-size: 17px; line-height: 1.45; letter-spacing: 0; }
.vp-doc .context-section-card h2 a { color: var(--vp-c-text-1); text-decoration: none; }
.vp-doc .context-section-card > ul { margin: 0; padding: 0; list-style: none; }
.vp-doc .context-section-card li { margin: 6px 0; font-size: 14px; line-height: 1.55; }
.vp-doc .context-section-card li a { color: var(--vp-c-text-2); text-decoration: none; }
.vp-doc .context-section-card li a:hover { color: var(--vp-c-brand-1); }
.vp-doc .context-section-card ul ul { margin: 4px 0 8px; padding-left: 14px; border-left: 1px solid var(--vp-c-divider); list-style: none; }
@media (max-width: 719px) { .context-section-grid { grid-template-columns: 1fr; } }
${DIAGRAM_STYLES}
`;

Expand Down
Loading
Loading