hi
\n\n', + 't', + 'x.html' + ) + ).toBe('hi
'); + }); + + it('throws naming the file when the region is missing or unterminated', () => { + expect(() => sliceRegion('const a = 1;', 'nope', 'x.ts')).toThrow( + /x\.ts.*nope/ + ); + expect(() => + sliceRegion('// #region open\nconst a = 1;', 'open', 'x.ts') + ).toThrow(/unterminated/); + }); + + it('keeps nested regions intact and ends at the matching endregion', () => { + const source = [ + '// #region outer', + 'const a = 1;', + '// #region inner', + 'const b = 2;', + '// #endregion', + 'const c = 3;', + '// #endregion', + ].join('\n'); + expect(sliceRegion(source, 'outer', 'f.ts')).toBe( + [ + 'const a = 1;', + '// #region inner', + 'const b = 2;', + '// #endregion', + 'const c = 3;', + ].join('\n') + ); + expect(sliceRegion(source, 'inner', 'f.ts')).toBe('const b = 2;'); + }); + + it('counts an unnamed nested region so the outer slice is not cut short', () => { + const source = [ + '// #region outer', + 'const a = 1;', + '// #region', + 'const b = 2;', + '// #endregion', + 'const c = 3;', + '// #endregion', + ].join('\n'); + expect(sliceRegion(source, 'outer', 'f.ts')).toBe( + [ + 'const a = 1;', + '// #region', + 'const b = 2;', + '// #endregion', + 'const c = 3;', + ].join('\n') + ); + }); + + it('accepts an HTML marker with no space before the comment close', () => { + expect( + sliceRegion( + '\nhi
\n\n', + 't', + 'x.html' + ) + ).toBe('hi
'); + }); +}); + +describe('fenceFor', () => { + it('maps the extension to a fence language', () => { + expect(fenceFor('const a = 1;', 'x.ts')).toBe('```ts\nconst a = 1;\n```'); + expect(fenceFor('a = 1', 'x.py')).toBe('```python\na = 1\n```'); + expect(fenceFor('', 'x.html')).toBe('```html\n\n```'); + }); + + it('uses a longer fence than any backtick run inside the code', () => { + expect(fenceFor('const s = `a```b`;', 'x.ts')).toBe( + '````ts\nconst s = `a```b`;\n````' + ); + }); + + it('strips one trailing newline so the fence closes on its own line', () => { + expect(fenceFor('a = 1\n', 'x.py')).toBe('```python\na = 1\n```'); + }); +}); + +describe('exampleTitle', () => { + it('is the basename', () => { + expect( + exampleTitle('cockpit/langgraph/streaming/python/src/graph.py') + ).toBe('graph.py'); + }); +}); diff --git a/apps/website/src/lib/example-code.ts b/apps/website/src/lib/example-code.ts new file mode 100644 index 000000000..9d4afdce3 --- /dev/null +++ b/apps/website/src/lib/example-code.ts @@ -0,0 +1,129 @@ +/** + * Resolution for `hi
\n\n', 't', 'x.html') + ).toBe('hi
'); + }); + + it('throws naming the file when the region is missing or unterminated', () => { + expect(() => sliceRegion('const a = 1;', 'nope', 'x.ts')).toThrow(/x\.ts.*nope/); + expect(() => sliceRegion('// #region open\nconst a = 1;', 'open', 'x.ts')).toThrow( + /unterminated/ + ); + }); +}); + +describe('fenceFor', () => { + it('maps the extension to a fence language', () => { + expect(fenceFor('const a = 1;', 'x.ts')).toBe('```ts\nconst a = 1;\n```'); + expect(fenceFor('a = 1', 'x.py')).toBe('```python\na = 1\n```'); + expect(fenceFor('', 'x.html')).toBe('```html\n\n```'); + }); + + it('uses a longer fence than any backtick run inside the code', () => { + expect(fenceFor('const s = `a```b`;', 'x.ts')).toBe('````ts\nconst s = `a```b`;\n````'); + }); + + it('strips one trailing newline so the fence closes on its own line', () => { + expect(fenceFor('a = 1\n', 'x.py')).toBe('```python\na = 1\n```'); + }); +}); + +describe('exampleTitle', () => { + it('is the basename', () => { + expect(exampleTitle('cockpit/langgraph/streaming/python/src/graph.py')).toBe('graph.py'); + }); +}); +``` + +- [ ] **Step 2: Run to verify failure** + +Run: `cd apps/website && npx vitest run example-code` +Expected: FAIL, `Failed to resolve import "./example-code"`. + +- [ ] **Step 3: Implement `apps/website/src/lib/example-code.ts`** + +```ts +/** + * Resolution for `highlighted'
- );
-
- const md = `# Getting Started\n\nThis is a paragraph.\n\n## Step 1\n\nAnother paragraph.`;
- const result = await renderMarkdown(md);
-
- expect(result.title).toBe('Getting Started');
- expect(result.html).toContain('Getting Started');
- expect(result.html).toContain('This is a paragraph.');
- });
-
- it('highlights fenced code blocks with Shiki', async () => {
- mockCodeToHtml.mockResolvedValue(
- 'const x = 1;'
- );
-
- const md = '# Test\n\n```typescript\nconst x = 1;\n```';
- const result = await renderMarkdown(md);
-
- expect(result.html).toContain('class="shiki"');
- expect(mockCodeToHtml).toHaveBeenCalled();
- });
-
- it('contains Shiki rendering failures with escaped plain code', async () => {
- mockCodeToHtml.mockRejectedValue(new Error('Shiki failed'));
-
- const result = await renderMarkdown(
- '# Test\n\n```typescript\nconst tag = "&entity; const tag = "&entity; <unsafe>";'
- );
- });
-
- it('escapes ampersands and angle brackets when Step highlighting fails', async () => {
- mockCodeToHtml.mockRejectedValue(new Error('Shiki failed'));
-
- const result = await renderMarkdown(
- '# Test\n\nconst tag = "&entity; <unsafe>";'
- );
- });
-
- it('extracts title from first h1', async () => {
- const md = '# My Title\n\nContent here.';
- const result = await renderMarkdown(md);
- expect(result.title).toBe('My Title');
- });
-
- it('returns empty title when no h1 exists', async () => {
- const md = 'Just a paragraph.';
- const result = await renderMarkdown(md);
- expect(result.title).toBe('');
- });
-
- it('extracts a title from a long heading without regex backtracking', async () => {
- const result = await renderMarkdown(`# ${'word '.repeat(20_000)}`);
- expect(result.title.startsWith('word word')).toBe(true);
- });
-
- it('extracts a title from a tab-delimited CommonMark heading', async () => {
- const result = await renderMarkdown('#\tTabbed title');
- expect(result.title).toBe('Tabbed title');
- });
-
- it('renders Summary blocks', async () => {
- const md = '# Test\n\nagent()');
- expect(result.html).toContain('');
- });
-
- it('renders Tip callout blocks', async () => {
- const md = '# Test\n\ncode'
- );
- const md = '# Test\n\n```typescript\n// app.config.ts\nconst x = 1;\n```';
- const result = await renderMarkdown(md);
- expect(result.html).toContain('doc-codeblock__header');
- expect(result.html).toContain('app.config.ts');
- expect(result.html).toContain('data-copy-code');
- });
-});
diff --git a/libs/cockpit-shell/src/lib/render-markdown.ts b/libs/cockpit-shell/src/lib/render-markdown.ts
deleted file mode 100644
index d83cecd7b..000000000
--- a/libs/cockpit-shell/src/lib/render-markdown.ts
+++ /dev/null
@@ -1,288 +0,0 @@
-import { marked } from 'marked';
-import { codeToHtml } from 'shiki';
-
-export interface RenderedMarkdown {
- title: string;
- html: string;
-}
-
-interface ExtractedBlock {
- placeholder: string;
- type: string;
- content: string;
- attrs: Record${escaped}`;
- }
- html = html.replace(
- block.placeholder,
- wrapCodeBlock(highlighted, block.lang, filename)
- );
- }
-
- return html;
-}
-
-function extractFilename(code: string): {
- filename: string | null;
- cleanedCode: string;
-} {
- const firstLine = code.split('\n')[0];
- const tsMatch = firstLine?.match(/^\/\/\s*(.+\.\w+)\s*$/);
- if (tsMatch) {
- return {
- filename: tsMatch[1],
- cleanedCode: code.split('\n').slice(1).join('\n'),
- };
- }
- const pyMatch = firstLine?.match(/^#\s*(.+\.\w+)\s*$/);
- if (pyMatch) {
- return {
- filename: pyMatch[1],
- cleanedCode: code.split('\n').slice(1).join('\n'),
- };
- }
- return { filename: null, cleanedCode: code };
-}
-
-function wrapCodeBlock(
- shikiHtml: string,
- lang: string,
- filename: string | null
-): string {
- const langLabel =
- lang !== 'text' ? `${lang}` : '';
- const fileLabel = filename
- ? `${filename}`
- : '';
- const header =
- fileLabel || langLabel
- ? `${escaped}`;
- }
- html = html.replace(
- block.placeholder,
- wrapCodeBlock(highlighted, block.lang, filename)
- );
- }
-
- for (const block of blocks) {
- if (!html.includes(block.placeholder)) continue;
- let rendered: string;
- switch (block.type) {
- case 'Summary':
- rendered = await renderSummary(block.content);
- break;
- case 'Tip':
- rendered = await renderCallout('tip', block.content);
- break;
- case 'Note':
- rendered = await renderCallout('note', block.content);
- break;
- case 'Warning':
- rendered = await renderCallout('warning', block.content);
- break;
- case 'Steps':
- rendered = await renderSteps(block.content, blocks);
- break;
- case 'Step':
- rendered = '';
- break;
- case 'Prompt':
- rendered = await renderPrompt(block.content);
- break;
- case 'Related':
- rendered = await renderRelated(block.content);
- break;
- case 'ApiTable': {
- const tableHtml = await marked.parse(block.content);
- rendered = renderApiTable(tableHtml);
- break;
- }
- default:
- rendered = block.content;
- }
- html = html.replace(block.placeholder, rendered);
- }
-
- return { title, html };
-}
diff --git a/libs/cockpit-shell/src/lib/workspace-content.spec.ts b/libs/cockpit-shell/src/lib/workspace-content.spec.ts
index 10c00f39b..9bb06fda2 100644
--- a/libs/cockpit-shell/src/lib/workspace-content.spec.ts
+++ b/libs/cockpit-shell/src/lib/workspace-content.spec.ts
@@ -21,13 +21,11 @@ import {
const testEntry = cockpitManifest[0] as CockpitManifestEntry;
// Stable mock function references, hoisted so vi.mock factories can access them
-const { mockExistsSync, mockReadFileSync, mockCodeToHtml, mockRenderMarkdown } =
- vi.hoisted(() => ({
- mockExistsSync: vi.fn(),
- mockReadFileSync: vi.fn(),
- mockCodeToHtml: vi.fn(),
- mockRenderMarkdown: vi.fn(),
- }));
+const { mockExistsSync, mockReadFileSync, mockCodeToHtml } = vi.hoisted(() => ({
+ mockExistsSync: vi.fn(),
+ mockReadFileSync: vi.fn(),
+ mockCodeToHtml: vi.fn(),
+}));
vi.mock('node:fs', async (importOriginal) => {
const actual = await importOriginalconst y = 2;'
);
expect(bundle.docSections).toEqual([]);
- expect(bundle.narrativeDocs).toEqual([]);
});
it('returns empty maps for a docs-only presentation', async () => {
@@ -260,9 +251,9 @@ describe('getContentBundle', () => {
expect(bundle.promptFiles).toEqual({});
expect(bundle.runtimeUrl).toBeNull();
expect(bundle.docSections).toEqual([]);
- expect(bundle.narrativeDocs).toEqual([]);
expect(mockReadFileSync).not.toHaveBeenCalled();
expect(mockCodeToHtml).not.toHaveBeenCalled();
+ expect(bundle.codeSources).toEqual({});
});
it('extracts docSections from code and backend files', async () => {
@@ -286,7 +277,6 @@ describe('getContentBundle', () => {
promptAssetPaths: ['prompts/streaming.md'],
codeAssetPaths: ['src/streaming.component.ts'],
backendAssetPaths: ['src/graph.py'],
- docsAssetPaths: [],
runtimeUrl: undefined,
devPort: undefined,
};
@@ -299,10 +289,9 @@ describe('getContentBundle', () => {
expect(bundle.docSections[0].language).toBe('typescript');
expect(bundle.docSections[1].title).toBe('StreamingGraph');
expect(bundle.docSections[1].language).toBe('python');
- expect(bundle.narrativeDocs).toEqual([]);
});
- it('contains missing prompt and narrative assets', async () => {
+ it('contains missing prompt assets', async () => {
mockReadFileSync.mockImplementation(() => {
throw new Error('ENOENT');
});
@@ -314,7 +303,6 @@ describe('getContentBundle', () => {
promptAssetPaths: ['missing/prompt.md'],
codeAssetPaths: [],
backendAssetPaths: [],
- docsAssetPaths: ['missing/guide.md'],
};
const bundle = await getContentBundle(presentation);
@@ -322,7 +310,6 @@ describe('getContentBundle', () => {
expect(bundle.promptFiles).toEqual({
'missing/prompt.md': 'File not found: missing/prompt.md',
});
- expect(bundle.narrativeDocs).toEqual([]);
});
it('contains absolute and traversal paths without reading outside the workspace', async () => {
@@ -333,7 +320,6 @@ describe('getContentBundle', () => {
promptAssetPaths: ['../outside-prompt.md'],
codeAssetPaths: ['/private/secret.ts', '../outside-code.ts'],
backendAssetPaths: [],
- docsAssetPaths: ['/private/secret.md', '../outside-doc.md'],
};
const bundle = await getContentBundle(presentation);
@@ -345,9 +331,7 @@ describe('getContentBundle', () => {
expect(bundle.promptFiles).toEqual({
'../outside-prompt.md': 'File not found: ../outside-prompt.md',
});
- expect(bundle.narrativeDocs).toEqual([]);
expect(mockReadFileSync).not.toHaveBeenCalled();
- expect(mockRenderMarkdown).not.toHaveBeenCalled();
});
it('loads workspace-only capabilities from the same registry assets', async () => {
@@ -370,10 +354,6 @@ describe('getContentBundle', () => {
return 'export const memory = true;';
});
mockCodeToHtml.mockResolvedValue('code'); - mockRenderMarkdown.mockResolvedValue({ - title: 'Deep Agents Memory', - html: '
Narrative.
', - }); vi.stubEnv('NEXT_PUBLIC_COCKPIT_RUNTIME_BASE_URL', ''); const bundle = await getContentBundle(presentation); @@ -385,47 +365,6 @@ describe('getContentBundle', () => { expect(Object.keys(bundle.promptFiles)).toEqual( descriptor?.promptAssetPaths ?? [] ); - expect(bundle.narrativeDocs.map((doc) => doc.sourceFile)).toEqual( - descriptor?.docsAssetPaths?.map((path) => path.split('/').at(-1)) ?? [] - ); expect(bundle.runtimeUrl).toBe('http://localhost:4313'); }); - - it('skips a narrative rendering failure and continues loading the bundle', async () => { - mockReadFileSync.mockImplementation((filePath: unknown) => { - const path = String(filePath); - if (path.endsWith('code.ts')) return 'export const code = true;'; - if (path.endsWith('prompt.md')) return '# Prompt'; - if (path.endsWith('broken.md')) return '# Broken'; - if (path.endsWith('valid.md')) return '# Valid'; - throw new Error('ENOENT'); - }); - mockCodeToHtml.mockResolvedValue('code'); - mockRenderMarkdown - .mockRejectedValueOnce(new Error('Marked failed')) - .mockResolvedValueOnce({ title: 'Valid', html: '
code' }, - promptFiles: { 'prompt.md': '# Prompt' }, - narrativeDocs: [ - { - title: 'Valid', - html: '
Learn to stream.
', sourceFile: 'guide.md' }, - ]} - /> - ); - expect(html).toContain('Streaming Guide'); - expect(html).toContain('Learn to stream.'); - }); - - it('renders empty state when no docs', () => { - const html = renderToStaticMarkup(const x = 1;No documentation available for this capability.
-source' }, + codeSources: { 'example.ts': 'source' }, promptFiles: {}, runtimeUrl: null, docSections: [], - narrativeDocs: [], }; function Readout() { diff --git a/libs/workspace-react/src/lib/workspace-provider.tsx b/libs/workspace-react/src/lib/workspace-provider.tsx index adc5ed8a6..e4b5ccd1a 100644 --- a/libs/workspace-react/src/lib/workspace-provider.tsx +++ b/libs/workspace-react/src/lib/workspace-provider.tsx @@ -28,7 +28,6 @@ import { import type { RuntimeFrameTelemetry, TrackModeChange, - TrackNarrativeAction, TrackNavigation, TrackRuntimeAction, TrackRuntimeTransition, @@ -73,7 +72,6 @@ export interface WorkspaceProviderProps { readonly getSessionId: WorkspaceSessionIdProvider; readonly runtimeTelemetry?: RuntimeFrameTelemetry; readonly trackNavigation?: TrackNavigation; - readonly trackNarrativeAction?: TrackNarrativeAction; readonly trackModeChange?: TrackModeChange; readonly trackRuntimeAction?: TrackRuntimeAction; readonly trackRuntimeTransition?: TrackRuntimeTransition; @@ -165,7 +163,6 @@ export function WorkspaceProvider({ getSessionId, runtimeTelemetry, trackNavigation, - trackNarrativeAction, trackModeChange, trackRuntimeAction, trackRuntimeTransition, @@ -407,7 +404,6 @@ export function WorkspaceProvider({ getSessionId, runtimeTelemetry, trackNavigation, - trackNarrativeAction, trackModeChange, selectMode, setActiveUtility, @@ -444,7 +440,6 @@ export function WorkspaceProvider({ selectMode, setActiveUtility, trackModeChange, - trackNarrativeAction, trackNavigation, ] ); diff --git a/libs/workspace-react/src/lib/workspace-shell.spec.tsx b/libs/workspace-react/src/lib/workspace-shell.spec.tsx index cd0261ffd..6615fdacc 100644 --- a/libs/workspace-react/src/lib/workspace-shell.spec.tsx +++ b/libs/workspace-react/src/lib/workspace-shell.spec.tsx @@ -54,23 +54,16 @@ const presentation: WorkspacePresentation = { promptAssetPaths: [], codeAssetPaths: ['example.ts'], backendAssetPaths: [], - docsAssetPaths: ['guide.md'], runtimeUrl: 'langgraph/streaming', devPort: 4300, runnable: true, }; const contentBundle: ContentBundle = { codeFiles: { 'example.ts': '
source' },
+ codeSources: { 'example.ts': 'source' },
promptFiles: {},
runtimeUrl: 'https://runtime.example.test/demo',
docSections: [],
- narrativeDocs: [
- {
- title: 'Streaming guide',
- html: '