diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 83ed66601..2602a9287 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -48,7 +48,7 @@ jobs: # classifier walk. - run: npm ci - name: Test CI scope classifier - run: node --test scripts/ci-scope.spec.mjs scripts/cockpit-matrix.spec.mjs + run: node --test scripts/ci-scope.spec.mjs scripts/cockpit-matrix.spec.mjs scripts/cockpit-ports.spec.mjs - name: Detect changed CI surfaces id: scope run: | diff --git a/apps/cockpit/cockpit-e2e-wiring.spec.ts b/apps/cockpit/cockpit-e2e-wiring.spec.ts index e464b0c29..d5a71ea46 100644 --- a/apps/cockpit/cockpit-e2e-wiring.spec.ts +++ b/apps/cockpit/cockpit-e2e-wiring.spec.ts @@ -2,6 +2,9 @@ import { existsSync, readdirSync, readFileSync } from 'node:fs'; import { dirname, join, relative, resolve } from 'node:path'; import { fileURLToPath } from 'node:url'; import { capabilities } from './scripts/capability-registry'; +// @ts-expect-error — .mjs ES module without .d.ts; the e2e tsconfig uses +// allowJs:true but this top-level test file doesn't go through that config. +import { portsFor } from '../../cockpit/ports.mjs'; interface E2eWiring { angularPort: number; @@ -84,12 +87,23 @@ function activeCockpitE2eWiring(): E2eWiring[] { const projectRoot = dirname(projectJsonPath); const globalSetupPath = join(projectRoot, 'e2e/global-setup-impl.ts'); const globalSetup = readFileSync(globalSetupPath, 'utf8'); - const proxyPath = join(projectRoot, 'proxy.conf.json'); - const proxy = JSON.parse(readFileSync(proxyPath, 'utf8')) as Record; - const proxyPort = Number(proxy['/api']?.target?.match(/:(\d+)$/)?.[1]); const langgraphCwd = parseStringProperty(globalSetup, 'langgraphCwd'); - const langgraphPort = parseNumberProperty(globalSetup, 'langgraphPort') ?? proxyPort; - const angularPort = parseNumberProperty(globalSetup, 'angularPort'); + + // Post-port-registry migration: ports are imported from + // cockpit/ports.mjs rather than living as literals in + // global-setup-impl.ts. Look them up by project name. + let langgraphPort: number | undefined; + let angularPort: number | undefined; + try { + const ports = portsFor(project.name) as { angular: number; langgraph: number }; + langgraphPort = ports.langgraph; + angularPort = ports.angular; + } catch { + // Cap not in registry (e.g. cockpit-ag-ui-streaming-angular). + // Fall back to parsing literals if global-setup still has them. + langgraphPort = parseNumberProperty(globalSetup, 'langgraphPort'); + angularPort = parseNumberProperty(globalSetup, 'angularPort'); + } if (!project.name || !langgraphCwd || !langgraphPort || !angularPort) { throw new Error(`Unable to parse e2e wiring for ${relative(repoRoot, projectJsonPath)}`); @@ -153,16 +167,29 @@ describe('cockpit e2e wiring', () => { errors.push(`${wiring.project}: registry pythonDir ${capability.pythonDir} != global setup langgraphCwd ${wiring.langgraphCwd}`); } - const proxyPath = join(wiring.projectRoot, 'proxy.conf.json'); - if (!existsSync(proxyPath)) { - errors.push(`${wiring.project}: missing proxy.conf.json`); - } else { - const proxy = JSON.parse(readFileSync(proxyPath, 'utf8')) as Record; + // Post-port-registry: proxy.conf.mjs templates the target from + // cockpit/ports.mjs at runtime. Drift is impossible because the + // value comes from the same registry the test already checks via + // wiring.langgraphPort. So we just assert the .mjs file exists + + // imports portsFor with this cap's name. + const proxyMjs = join(wiring.projectRoot, 'proxy.conf.mjs'); + const proxyJson = join(wiring.projectRoot, 'proxy.conf.json'); + if (existsSync(proxyMjs)) { + const text = readFileSync(proxyMjs, 'utf8'); + if (!text.includes(`portsFor('${wiring.project}')`)) { + errors.push(`${wiring.project}: proxy.conf.mjs does not call portsFor('${wiring.project}')`); + } + } else if (existsSync(proxyJson)) { + // Legacy (allowed for ag-ui exception only); not expected for any + // cap reaching this code path. + const proxy = JSON.parse(readFileSync(proxyJson, 'utf8')) as Record; const target = proxy['/api']?.target; const expectedTarget = `http://localhost:${wiring.langgraphPort}`; if (target !== expectedTarget) { errors.push(`${wiring.project}: proxy target ${target} != ${expectedTarget}`); } + } else { + errors.push(`${wiring.project}: missing proxy.conf (no .mjs or .json)`); } const scriptsDir = join(wiring.projectRoot, 'e2e/scripts'); diff --git a/apps/website/content/docs/chat/api/api-docs.json b/apps/website/content/docs/chat/api/api-docs.json index 1d98a8f1a..44625ad6c 100644 --- a/apps/website/content/docs/chat/api/api-docs.json +++ b/apps/website/content/docs/chat/api/api-docs.json @@ -1456,6 +1456,91 @@ ], "methods": [] }, + { + "name": "ChatApprovalCardComponent", + "kind": "class", + "description": "", + "params": [], + "examples": [], + "properties": [ + { + "name": "action", + "type": "OutputEmitterRef", + "description": "", + "optional": false + }, + { + "name": "agent", + "type": "InputSignal", + "description": "", + "optional": false + }, + { + "name": "bodyTemplate", + "type": "Signal | undefined>", + "description": "", + "optional": false + }, + { + "name": "matchKind", + "type": "InputSignal", + "description": "", + "optional": false + }, + { + "name": "payload", + "type": "Signal", + "description": "", + "optional": false + }, + { + "name": "showEdit", + "type": "InputSignal", + "description": "", + "optional": false + }, + { + "name": "title", + "type": "InputSignal", + "description": "", + "optional": false + } + ], + "methods": [ + { + "name": "emit", + "signature": "emit(action: ChatApprovalAction)", + "description": "", + "params": [ + { + "name": "action", + "type": "ChatApprovalAction", + "description": "", + "optional": false + } + ] + }, + { + "name": "onCancelEvent", + "signature": "onCancelEvent(ev: Event)", + "description": "", + "params": [ + { + "name": "ev", + "type": "Event", + "description": "", + "optional": false + } + ] + }, + { + "name": "onDialogClose", + "signature": "onDialogClose()", + "description": "", + "params": [] + } + ] + }, { "name": "ChatCitationCardTemplateDirective", "kind": "class", @@ -6376,6 +6461,13 @@ "signature": "\"idle\" | \"running\" | \"error\"", "examples": [] }, + { + "name": "ChatApprovalAction", + "kind": "type", + "description": "", + "signature": "\"approve\" | \"edit\" | \"cancel\"", + "examples": [] + }, { "name": "ChatMessageRole", "kind": "type", diff --git a/apps/website/src/app/global.css b/apps/website/src/app/global.css index 76cafccfb..168efc345 100644 --- a/apps/website/src/app/global.css +++ b/apps/website/src/app/global.css @@ -5,6 +5,10 @@ box-sizing: border-box; } +html { + overflow-x: hidden; +} + body { background-color: var(--color-bg); color: var(--color-text-primary); @@ -14,6 +18,14 @@ body { overflow-x: hidden; } +/* Code blocks scroll internally; never expand the layout viewport. */ +pre, code { + max-width: 100%; +} +pre { + overflow-x: auto; +} + /* Long unbreakable tokens (e.g. package names like @threadplane/chat, * file paths like app.config.ts) live inside marketing headings and pull * the layout wider than the viewport on narrow phones. Allow breaking diff --git a/apps/website/src/components/docs/DocsSidebar.tsx b/apps/website/src/components/docs/DocsSidebar.tsx index 587d2d688..05f94393d 100644 --- a/apps/website/src/components/docs/DocsSidebar.tsx +++ b/apps/website/src/components/docs/DocsSidebar.tsx @@ -168,7 +168,7 @@ export function DocsSidebar({ activeLibrary, activeSection, activeSlug }: Props) return (