From 9341e16e261ad534277bc4e0e6fc026a05d50528 Mon Sep 17 00:00:00 2001 From: Brian Love Date: Wed, 8 Apr 2026 09:58:28 -0700 Subject: [PATCH 01/48] feat(cockpit): add render/spec-rendering capability Co-Authored-By: Claude Opus 4.6 (1M context) --- .../angular/e2e/spec-rendering.spec.ts | 19 ++++ .../spec-rendering/angular/project.json | 61 ++++++++++++ .../spec-rendering/angular/proxy.conf.json | 9 ++ .../angular/src/app/app.config.ts | 14 +++ .../src/app/spec-rendering.component.ts | 56 +++++++++++ .../environments/environment.development.ts | 5 + .../angular/src/environments/environment.ts | 5 + .../spec-rendering/angular/src/index.html | 13 +++ .../spec-rendering/angular/src/index.ts | 29 ++++++ .../render/spec-rendering/angular/src/main.ts | 6 ++ .../spec-rendering/angular/src/styles.css | 1 + .../spec-rendering/angular/tsconfig.app.json | 9 ++ .../spec-rendering/angular/tsconfig.json | 24 +++++ .../spec-rendering/python/docs/guide.md | 97 +++++++++++++++++++ .../spec-rendering/python/langgraph.json | 8 ++ .../python/prompts/spec-rendering.md | 11 +++ .../spec-rendering/python/pyproject.toml | 21 ++++ .../render/spec-rendering/python/src/graph.py | 40 ++++++++ .../render/spec-rendering/python/src/index.ts | 42 ++++++++ 19 files changed, 470 insertions(+) create mode 100644 cockpit/render/spec-rendering/angular/e2e/spec-rendering.spec.ts create mode 100644 cockpit/render/spec-rendering/angular/project.json create mode 100644 cockpit/render/spec-rendering/angular/proxy.conf.json create mode 100644 cockpit/render/spec-rendering/angular/src/app/app.config.ts create mode 100644 cockpit/render/spec-rendering/angular/src/app/spec-rendering.component.ts create mode 100644 cockpit/render/spec-rendering/angular/src/environments/environment.development.ts create mode 100644 cockpit/render/spec-rendering/angular/src/environments/environment.ts create mode 100644 cockpit/render/spec-rendering/angular/src/index.html create mode 100644 cockpit/render/spec-rendering/angular/src/index.ts create mode 100644 cockpit/render/spec-rendering/angular/src/main.ts create mode 100644 cockpit/render/spec-rendering/angular/src/styles.css create mode 100644 cockpit/render/spec-rendering/angular/tsconfig.app.json create mode 100644 cockpit/render/spec-rendering/angular/tsconfig.json create mode 100644 cockpit/render/spec-rendering/python/docs/guide.md create mode 100644 cockpit/render/spec-rendering/python/langgraph.json create mode 100644 cockpit/render/spec-rendering/python/prompts/spec-rendering.md create mode 100644 cockpit/render/spec-rendering/python/pyproject.toml create mode 100644 cockpit/render/spec-rendering/python/src/graph.py create mode 100644 cockpit/render/spec-rendering/python/src/index.ts diff --git a/cockpit/render/spec-rendering/angular/e2e/spec-rendering.spec.ts b/cockpit/render/spec-rendering/angular/e2e/spec-rendering.spec.ts new file mode 100644 index 000000000..86299c4b0 --- /dev/null +++ b/cockpit/render/spec-rendering/angular/e2e/spec-rendering.spec.ts @@ -0,0 +1,19 @@ +import { expect, test } from '@playwright/test'; + +test.describe('Render Spec Rendering Example', () => { + test.beforeEach(async ({ page }) => { + await page.goto('http://localhost:4401'); + await page.waitForSelector('app-spec-rendering', { state: 'attached' }); + }); + + test('renders the chat interface with render preview sidebar', async ({ page }) => { + await expect(page.locator('chat')).toBeVisible(); + await expect(page.locator('aside')).toBeVisible(); + await expect(page.locator('aside h3')).toHaveText('Live Render Preview'); + }); + + test('displays the JSON spec in the sidebar', async ({ page }) => { + await expect(page.locator('aside pre')).toBeVisible(); + await expect(page.locator('aside pre')).toContainText('container'); + }); +}); diff --git a/cockpit/render/spec-rendering/angular/project.json b/cockpit/render/spec-rendering/angular/project.json new file mode 100644 index 000000000..f53637900 --- /dev/null +++ b/cockpit/render/spec-rendering/angular/project.json @@ -0,0 +1,61 @@ +{ + "name": "cockpit-render-spec-rendering-angular", + "$schema": "../../../../node_modules/nx/schemas/project-schema.json", + "sourceRoot": "cockpit/render/spec-rendering/angular/src", + "projectType": "application", + "targets": { + "build": { + "executor": "@angular/build:application", + "outputs": ["{options.outputPath.base}"], + "options": { + "outputPath": { + "base": "dist/cockpit/render/spec-rendering/angular", + "browser": "" + }, + "browser": "cockpit/render/spec-rendering/angular/src/main.ts", + "tsConfig": "cockpit/render/spec-rendering/angular/tsconfig.app.json", + "styles": ["cockpit/render/spec-rendering/angular/src/styles.css"] + }, + "configurations": { + "production": { + "budgets": [ + { "type": "initial", "maximumWarning": "500kb", "maximumError": "1mb" }, + { "type": "anyComponentStyle", "maximumWarning": "4kb", "maximumError": "8kb" } + ], + "outputHashing": "none" + }, + "development": { + "optimization": false, + "extractLicenses": false, + "sourceMap": true, + "fileReplacements": [ + { + "replace": "cockpit/render/spec-rendering/angular/src/environments/environment.ts", + "with": "cockpit/render/spec-rendering/angular/src/environments/environment.development.ts" + } + ] + } + }, + "defaultConfiguration": "production" + }, + "serve": { + "continuous": true, + "executor": "@angular/build:dev-server", + "configurations": { + "production": { "buildTarget": "cockpit-render-spec-rendering-angular:build:production" }, + "development": { "buildTarget": "cockpit-render-spec-rendering-angular:build:development" } + }, + "defaultConfiguration": "development", + "options": { + "proxyConfig": "cockpit/render/spec-rendering/angular/proxy.conf.json" + } + }, + "smoke": { + "executor": "nx:run-commands", + "options": { + "cwd": "cockpit/render/spec-rendering/angular", + "command": "npx tsx -e \"import { renderSpecRenderingAngularModule } from './src/index.ts'; const module = renderSpecRenderingAngularModule; if (module.id !== 'render-spec-rendering-angular' || module.title !== 'Render Spec Rendering (Angular)') { throw new Error('Unexpected module shape for ' + module.id); }\"" + } + } + } +} diff --git a/cockpit/render/spec-rendering/angular/proxy.conf.json b/cockpit/render/spec-rendering/angular/proxy.conf.json new file mode 100644 index 000000000..8523362d7 --- /dev/null +++ b/cockpit/render/spec-rendering/angular/proxy.conf.json @@ -0,0 +1,9 @@ +{ + "/api": { + "target": "http://localhost:8123", + "secure": false, + "changeOrigin": true, + "pathRewrite": { "^/api": "" }, + "ws": true + } +} diff --git a/cockpit/render/spec-rendering/angular/src/app/app.config.ts b/cockpit/render/spec-rendering/angular/src/app/app.config.ts new file mode 100644 index 000000000..08e9180d0 --- /dev/null +++ b/cockpit/render/spec-rendering/angular/src/app/app.config.ts @@ -0,0 +1,14 @@ +// SPDX-License-Identifier: PolyForm-Noncommercial-1.0.0 +import { ApplicationConfig } from '@angular/core'; +import { provideStreamResource } from '@cacheplane/stream-resource'; +import { provideChat } from '@cacheplane/chat'; +import { provideRender } from '@cacheplane/render'; +import { environment } from '../environments/environment'; + +export const appConfig: ApplicationConfig = { + providers: [ + provideStreamResource({ apiUrl: environment.langGraphApiUrl }), + provideChat({}), + provideRender({}), + ], +}; diff --git a/cockpit/render/spec-rendering/angular/src/app/spec-rendering.component.ts b/cockpit/render/spec-rendering/angular/src/app/spec-rendering.component.ts new file mode 100644 index 000000000..3f5cb7ecc --- /dev/null +++ b/cockpit/render/spec-rendering/angular/src/app/spec-rendering.component.ts @@ -0,0 +1,56 @@ +// SPDX-License-Identifier: PolyForm-Noncommercial-1.0.0 +import { Component, signal } from '@angular/core'; +import { JsonPipe } from '@angular/common'; +import { ChatComponent } from '@cacheplane/chat'; +import { RenderSpecComponent, signalStateStore } from '@cacheplane/render'; +import { streamResource } from '@cacheplane/stream-resource'; +import { environment } from '../environments/environment'; + +/** + * SpecRenderingComponent demonstrates RenderSpecComponent from @cacheplane/render. + * + * Shows how JSON render specs are converted into live Angular components. + * The sidebar displays a live render spec preview using RenderSpecComponent, + * while the chat area communicates with an agent that explains spec rendering. + */ +@Component({ + selector: 'app-spec-rendering', + standalone: true, + imports: [ChatComponent, RenderSpecComponent, JsonPipe], + template: ` +
+ + +
+ `, +}) +export class SpecRenderingComponent { + protected readonly stream = streamResource({ + apiUrl: environment.langGraphApiUrl, + assistantId: environment.streamingAssistantId, + }); + + protected readonly store = signalStateStore({ greeting: 'Hello from RenderSpec!' }); + + protected readonly demoSpec = signal({ + type: 'container', + props: { class: 'space-y-2' }, + children: [ + { type: 'text', props: { content: 'This UI is rendered from a JSON spec' } }, + { type: 'text', props: { bind: '/greeting' } }, + ], + }); +} diff --git a/cockpit/render/spec-rendering/angular/src/environments/environment.development.ts b/cockpit/render/spec-rendering/angular/src/environments/environment.development.ts new file mode 100644 index 000000000..192f0e610 --- /dev/null +++ b/cockpit/render/spec-rendering/angular/src/environments/environment.development.ts @@ -0,0 +1,5 @@ +export const environment = { + production: false, + langGraphApiUrl: 'http://localhost:4401/api', + streamingAssistantId: 'spec-rendering', +}; diff --git a/cockpit/render/spec-rendering/angular/src/environments/environment.ts b/cockpit/render/spec-rendering/angular/src/environments/environment.ts new file mode 100644 index 000000000..81a27a8ff --- /dev/null +++ b/cockpit/render/spec-rendering/angular/src/environments/environment.ts @@ -0,0 +1,5 @@ +export const environment = { + production: true, + langGraphApiUrl: '/api', + streamingAssistantId: 'spec-rendering', +}; diff --git a/cockpit/render/spec-rendering/angular/src/index.html b/cockpit/render/spec-rendering/angular/src/index.html new file mode 100644 index 000000000..fd45fb1eb --- /dev/null +++ b/cockpit/render/spec-rendering/angular/src/index.html @@ -0,0 +1,13 @@ + + + + + Render Spec Rendering — Angular + + + + + + + + diff --git a/cockpit/render/spec-rendering/angular/src/index.ts b/cockpit/render/spec-rendering/angular/src/index.ts new file mode 100644 index 000000000..e7c6087c6 --- /dev/null +++ b/cockpit/render/spec-rendering/angular/src/index.ts @@ -0,0 +1,29 @@ +export interface CockpitCapabilityModule { + id: string; + manifestIdentity: { + product: 'render'; + section: 'core-capabilities'; + topic: 'spec-rendering'; + page: 'overview'; + language: 'angular'; + }; + title: string; + docsPath: string; + promptAssetPaths: string[]; + codeAssetPaths: string[]; +} + +export const renderSpecRenderingAngularModule: CockpitCapabilityModule = { + id: 'render-spec-rendering-angular', + manifestIdentity: { + product: 'render', + section: 'core-capabilities', + topic: 'spec-rendering', + page: 'overview', + language: 'angular', + }, + title: 'Render Spec Rendering (Angular)', + docsPath: '/docs/render/core-capabilities/spec-rendering/overview/angular', + promptAssetPaths: ['cockpit/render/spec-rendering/angular/prompts/spec-rendering.md'], + codeAssetPaths: ['cockpit/render/spec-rendering/angular/src/app/spec-rendering.component.ts'], +}; diff --git a/cockpit/render/spec-rendering/angular/src/main.ts b/cockpit/render/spec-rendering/angular/src/main.ts new file mode 100644 index 000000000..6c4c2a798 --- /dev/null +++ b/cockpit/render/spec-rendering/angular/src/main.ts @@ -0,0 +1,6 @@ +// SPDX-License-Identifier: PolyForm-Noncommercial-1.0.0 +import { bootstrapApplication } from '@angular/platform-browser'; +import { appConfig } from './app/app.config'; +import { SpecRenderingComponent } from './app/spec-rendering.component'; + +bootstrapApplication(SpecRenderingComponent, appConfig).catch(console.error); diff --git a/cockpit/render/spec-rendering/angular/src/styles.css b/cockpit/render/spec-rendering/angular/src/styles.css new file mode 100644 index 000000000..8c26c28ac --- /dev/null +++ b/cockpit/render/spec-rendering/angular/src/styles.css @@ -0,0 +1 @@ +/* Global styles for the spec-rendering capability demo */ diff --git a/cockpit/render/spec-rendering/angular/tsconfig.app.json b/cockpit/render/spec-rendering/angular/tsconfig.app.json new file mode 100644 index 000000000..72c01e364 --- /dev/null +++ b/cockpit/render/spec-rendering/angular/tsconfig.app.json @@ -0,0 +1,9 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../../../dist/out-tsc", + "types": [] + }, + "files": ["src/main.ts"], + "include": ["src/**/*.d.ts", "src/**/*.ts"] +} diff --git a/cockpit/render/spec-rendering/angular/tsconfig.json b/cockpit/render/spec-rendering/angular/tsconfig.json new file mode 100644 index 000000000..3fd970371 --- /dev/null +++ b/cockpit/render/spec-rendering/angular/tsconfig.json @@ -0,0 +1,24 @@ +{ + "extends": "../../../../tsconfig.base.json", + "compilerOptions": { + "noPropertyAccessFromIndexSignature": false, + "experimentalDecorators": true, + "module": "preserve", + "emitDeclarationOnly": false, + "composite": false, + "lib": ["es2022", "dom"], + "skipLibCheck": true, + "strict": false + }, + "angularCompilerOptions": { + "enableI18nLegacyMessageIdFormat": false, + "strictInjectionParameters": false, + "strictInputAccessModifiers": false, + "strictTemplates": false + }, + "files": [], + "include": [], + "references": [ + { "path": "./tsconfig.app.json" } + ] +} diff --git a/cockpit/render/spec-rendering/python/docs/guide.md b/cockpit/render/spec-rendering/python/docs/guide.md new file mode 100644 index 000000000..d836966f0 --- /dev/null +++ b/cockpit/render/spec-rendering/python/docs/guide.md @@ -0,0 +1,97 @@ +# Spec Rendering with @cacheplane/render + + +Render Angular components from JSON specifications using RenderSpecComponent. +The component recursively resolves element types from a registry and renders +them with reactive prop bindings. + + + +Add JSON-driven UI rendering to this Angular component using `RenderSpecComponent` +from `@cacheplane/render`. Define a registry with `defineAngularRegistry()`, create +a state store with `signalStateStore()`, and pass a JSON spec to the template. + + + + + +Set up `provideRender()` in your app config with a component registry: + +```typescript +// app.config.ts +import { ApplicationConfig } from '@angular/core'; +import { provideRender } from '@cacheplane/render'; +import { defineAngularRegistry } from '@cacheplane/render'; + +export const appConfig: ApplicationConfig = { + providers: [ + provideRender({ + registry: defineAngularRegistry({}), + }), + ], +}; +``` + + + + +Create a spec object describing your UI layout. Each element has a `type` that +maps to a registered Angular component: + +```typescript +const spec = { + type: 'container', + props: { class: 'p-4 space-y-2' }, + children: [ + { type: 'heading', props: { text: 'Hello from a JSON spec' } }, + { type: 'text', props: { content: 'Rendered by RenderSpecComponent' } }, + ], +}; +``` + + + + +Use `` in your template to render the JSON spec: + +```html + +``` + +RenderSpecComponent recursively walks the spec tree, resolves each type +from the registry, and creates Angular components with the specified props. + + + + +Use `signalStateStore()` to create a reactive state store that your spec +can bind to: + +```typescript +import { signalStateStore } from '@cacheplane/render'; + +const store = signalStateStore({ count: 0, name: '' }); +store.set('/count', 1); +store.get('/name'); // Signal +``` + + + + +Use `streamResource()` to connect to the agent and display render specs +from the conversation: + +```typescript +protected readonly stream = streamResource({ + apiUrl: environment.langGraphApiUrl, + assistantId: environment.streamingAssistantId, +}); +``` + + + + + +RenderSpecComponent is tree-shakeable — only registered component types are included +in your bundle. + diff --git a/cockpit/render/spec-rendering/python/langgraph.json b/cockpit/render/spec-rendering/python/langgraph.json new file mode 100644 index 000000000..36e0ba15d --- /dev/null +++ b/cockpit/render/spec-rendering/python/langgraph.json @@ -0,0 +1,8 @@ +{ + "graphs": { + "spec-rendering": "./src/graph.py:graph" + }, + "dependencies": ["."], + "python_version": "3.12", + "env": ".env" +} diff --git a/cockpit/render/spec-rendering/python/prompts/spec-rendering.md b/cockpit/render/spec-rendering/python/prompts/spec-rendering.md new file mode 100644 index 000000000..5edec1100 --- /dev/null +++ b/cockpit/render/spec-rendering/python/prompts/spec-rendering.md @@ -0,0 +1,11 @@ +# Spec Rendering Assistant + +You are an assistant that demonstrates the RenderSpecComponent from @cacheplane/render. + +When the user asks you to create a UI, respond with a description of the layout +and components you would use. Include JSON render spec examples when helpful. + +A render spec is a JSON object with `type`, `props`, and optional `children` fields. +For example: `{"type": "card", "props": {"title": "Hello"}, "children": []}`. + +Explain how RenderSpecComponent recursively renders these specs into Angular components. diff --git a/cockpit/render/spec-rendering/python/pyproject.toml b/cockpit/render/spec-rendering/python/pyproject.toml new file mode 100644 index 000000000..62c0dfe83 --- /dev/null +++ b/cockpit/render/spec-rendering/python/pyproject.toml @@ -0,0 +1,21 @@ +[project] +name = "cockpit-render-spec-rendering" +version = "0.1.0" +requires-python = ">=3.12" +dependencies = [ + "langgraph>=0.3", + "langchain-openai>=0.3", + "langsmith>=0.2", +] + +[tool.uv] +dev-dependencies = [ + "langgraph-cli[inmem]>=0.1", +] + +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[tool.hatch.build.targets.wheel] +packages = ["src"] diff --git a/cockpit/render/spec-rendering/python/src/graph.py b/cockpit/render/spec-rendering/python/src/graph.py new file mode 100644 index 000000000..c96c540ba --- /dev/null +++ b/cockpit/render/spec-rendering/python/src/graph.py @@ -0,0 +1,40 @@ +""" +Render Spec Rendering Graph + +A LangGraph StateGraph that returns JSON render specs describing UI layouts. +The Angular frontend uses RenderSpecComponent to render these specs into +live Angular components. +""" + +from pathlib import Path +from langgraph.graph import StateGraph, MessagesState, END +from langchain_openai import ChatOpenAI +from langchain_core.messages import SystemMessage + +PROMPTS_DIR = Path(__file__).parent.parent / "prompts" + + +def build_spec_rendering_graph(): + """ + Constructs a graph that generates JSON render specs. + + The agent responds with JSON UI specifications that the Angular frontend + renders using RenderSpecComponent from @cacheplane/render. + """ + llm = ChatOpenAI(model="gpt-5-mini", streaming=True) + + async def generate(state: MessagesState) -> dict: + system_prompt = (PROMPTS_DIR / "spec-rendering.md").read_text() + messages = [SystemMessage(content=system_prompt)] + state["messages"] + response = await llm.ainvoke(messages) + return {"messages": [response]} + + graph = StateGraph(MessagesState) + graph.add_node("generate", generate) + graph.set_entry_point("generate") + graph.add_edge("generate", END) + + return graph.compile() + + +graph = build_spec_rendering_graph() diff --git a/cockpit/render/spec-rendering/python/src/index.ts b/cockpit/render/spec-rendering/python/src/index.ts new file mode 100644 index 000000000..ccf08aa97 --- /dev/null +++ b/cockpit/render/spec-rendering/python/src/index.ts @@ -0,0 +1,42 @@ +export interface CockpitCapabilityModule { + id: string; + manifestIdentity: { + product: 'render'; + section: 'core-capabilities'; + topic: 'spec-rendering'; + page: 'overview'; + language: 'python'; + }; + title: string; + docsPath: string; + promptAssetPaths: string[]; + codeAssetPaths: string[]; + backendAssetPaths: string[]; + docsAssetPaths: string[]; + runtimeUrl?: string; + devPort?: number; +} + +export const renderSpecRenderingPythonModule: CockpitCapabilityModule = { + id: 'render-spec-rendering-python', + manifestIdentity: { + product: 'render', + section: 'core-capabilities', + topic: 'spec-rendering', + page: 'overview', + language: 'python', + }, + title: 'Render Spec Rendering (Python)', + docsPath: '/docs/render/core-capabilities/spec-rendering/overview/python', + promptAssetPaths: ['cockpit/render/spec-rendering/python/prompts/spec-rendering.md'], + codeAssetPaths: [ + 'cockpit/render/spec-rendering/angular/src/app/spec-rendering.component.ts', + 'cockpit/render/spec-rendering/angular/src/app/app.config.ts', + ], + backendAssetPaths: [ + 'cockpit/render/spec-rendering/python/src/graph.py', + ], + docsAssetPaths: ['cockpit/render/spec-rendering/python/docs/guide.md'], + runtimeUrl: 'render/spec-rendering', + devPort: 4401, +}; From daebd9c38b1dcf564da4d3be8dfc413df5c91c17 Mon Sep 17 00:00:00 2001 From: Brian Love Date: Wed, 8 Apr 2026 10:01:59 -0700 Subject: [PATCH 02/48] feat(cockpit): add render/element-rendering capability Co-Authored-By: Claude Opus 4.6 (1M context) --- .../angular/e2e/element-rendering.spec.ts | 19 ++++ .../element-rendering/angular/project.json | 61 +++++++++++++ .../element-rendering/angular/proxy.conf.json | 9 ++ .../angular/src/app/app.config.ts | 14 +++ .../src/app/element-rendering.component.ts | 74 ++++++++++++++++ .../environments/environment.development.ts | 5 ++ .../angular/src/environments/environment.ts | 5 ++ .../element-rendering/angular/src/index.html | 13 +++ .../element-rendering/angular/src/index.ts | 29 +++++++ .../element-rendering/angular/src/main.ts | 6 ++ .../element-rendering/angular/src/styles.css | 1 + .../angular/tsconfig.app.json | 9 ++ .../element-rendering/angular/tsconfig.json | 24 ++++++ .../element-rendering/python/docs/guide.md | 86 +++++++++++++++++++ .../element-rendering/python/langgraph.json | 8 ++ .../python/prompts/element-rendering.md | 15 ++++ .../element-rendering/python/pyproject.toml | 21 +++++ .../element-rendering/python/src/graph.py | 40 +++++++++ .../element-rendering/python/src/index.ts | 42 +++++++++ 19 files changed, 481 insertions(+) create mode 100644 cockpit/render/element-rendering/angular/e2e/element-rendering.spec.ts create mode 100644 cockpit/render/element-rendering/angular/project.json create mode 100644 cockpit/render/element-rendering/angular/proxy.conf.json create mode 100644 cockpit/render/element-rendering/angular/src/app/app.config.ts create mode 100644 cockpit/render/element-rendering/angular/src/app/element-rendering.component.ts create mode 100644 cockpit/render/element-rendering/angular/src/environments/environment.development.ts create mode 100644 cockpit/render/element-rendering/angular/src/environments/environment.ts create mode 100644 cockpit/render/element-rendering/angular/src/index.html create mode 100644 cockpit/render/element-rendering/angular/src/index.ts create mode 100644 cockpit/render/element-rendering/angular/src/main.ts create mode 100644 cockpit/render/element-rendering/angular/src/styles.css create mode 100644 cockpit/render/element-rendering/angular/tsconfig.app.json create mode 100644 cockpit/render/element-rendering/angular/tsconfig.json create mode 100644 cockpit/render/element-rendering/python/docs/guide.md create mode 100644 cockpit/render/element-rendering/python/langgraph.json create mode 100644 cockpit/render/element-rendering/python/prompts/element-rendering.md create mode 100644 cockpit/render/element-rendering/python/pyproject.toml create mode 100644 cockpit/render/element-rendering/python/src/graph.py create mode 100644 cockpit/render/element-rendering/python/src/index.ts diff --git a/cockpit/render/element-rendering/angular/e2e/element-rendering.spec.ts b/cockpit/render/element-rendering/angular/e2e/element-rendering.spec.ts new file mode 100644 index 000000000..e1ab82385 --- /dev/null +++ b/cockpit/render/element-rendering/angular/e2e/element-rendering.spec.ts @@ -0,0 +1,19 @@ +import { expect, test } from '@playwright/test'; + +test.describe('Render Element Rendering Example', () => { + test.beforeEach(async ({ page }) => { + await page.goto('http://localhost:4402'); + await page.waitForSelector('app-element-rendering', { state: 'attached' }); + }); + + test('renders the chat interface and toggle button', async ({ page }) => { + await expect(page.locator('chat')).toBeVisible(); + await expect(page.locator('aside')).toBeVisible(); + await expect(page.locator('button.toggle-visibility')).toBeVisible(); + }); + + test('displays nested elements in the sidebar', async ({ page }) => { + await expect(page.locator('aside h3')).toHaveText('Nested Elements'); + await expect(page.locator('aside pre')).toContainText('container'); + }); +}); diff --git a/cockpit/render/element-rendering/angular/project.json b/cockpit/render/element-rendering/angular/project.json new file mode 100644 index 000000000..c92192f8a --- /dev/null +++ b/cockpit/render/element-rendering/angular/project.json @@ -0,0 +1,61 @@ +{ + "name": "cockpit-render-element-rendering-angular", + "$schema": "../../../../node_modules/nx/schemas/project-schema.json", + "sourceRoot": "cockpit/render/element-rendering/angular/src", + "projectType": "application", + "targets": { + "build": { + "executor": "@angular/build:application", + "outputs": ["{options.outputPath.base}"], + "options": { + "outputPath": { + "base": "dist/cockpit/render/element-rendering/angular", + "browser": "" + }, + "browser": "cockpit/render/element-rendering/angular/src/main.ts", + "tsConfig": "cockpit/render/element-rendering/angular/tsconfig.app.json", + "styles": ["cockpit/render/element-rendering/angular/src/styles.css"] + }, + "configurations": { + "production": { + "budgets": [ + { "type": "initial", "maximumWarning": "500kb", "maximumError": "1mb" }, + { "type": "anyComponentStyle", "maximumWarning": "4kb", "maximumError": "8kb" } + ], + "outputHashing": "none" + }, + "development": { + "optimization": false, + "extractLicenses": false, + "sourceMap": true, + "fileReplacements": [ + { + "replace": "cockpit/render/element-rendering/angular/src/environments/environment.ts", + "with": "cockpit/render/element-rendering/angular/src/environments/environment.development.ts" + } + ] + } + }, + "defaultConfiguration": "production" + }, + "serve": { + "continuous": true, + "executor": "@angular/build:dev-server", + "configurations": { + "production": { "buildTarget": "cockpit-render-element-rendering-angular:build:production" }, + "development": { "buildTarget": "cockpit-render-element-rendering-angular:build:development" } + }, + "defaultConfiguration": "development", + "options": { + "proxyConfig": "cockpit/render/element-rendering/angular/proxy.conf.json" + } + }, + "smoke": { + "executor": "nx:run-commands", + "options": { + "cwd": "cockpit/render/element-rendering/angular", + "command": "npx tsx -e \"import { renderElementRenderingAngularModule } from './src/index.ts'; const module = renderElementRenderingAngularModule; if (module.id !== 'render-element-rendering-angular' || module.title !== 'Render Element Rendering (Angular)') { throw new Error('Unexpected module shape for ' + module.id); }\"" + } + } + } +} diff --git a/cockpit/render/element-rendering/angular/proxy.conf.json b/cockpit/render/element-rendering/angular/proxy.conf.json new file mode 100644 index 000000000..8523362d7 --- /dev/null +++ b/cockpit/render/element-rendering/angular/proxy.conf.json @@ -0,0 +1,9 @@ +{ + "/api": { + "target": "http://localhost:8123", + "secure": false, + "changeOrigin": true, + "pathRewrite": { "^/api": "" }, + "ws": true + } +} diff --git a/cockpit/render/element-rendering/angular/src/app/app.config.ts b/cockpit/render/element-rendering/angular/src/app/app.config.ts new file mode 100644 index 000000000..08e9180d0 --- /dev/null +++ b/cockpit/render/element-rendering/angular/src/app/app.config.ts @@ -0,0 +1,14 @@ +// SPDX-License-Identifier: PolyForm-Noncommercial-1.0.0 +import { ApplicationConfig } from '@angular/core'; +import { provideStreamResource } from '@cacheplane/stream-resource'; +import { provideChat } from '@cacheplane/chat'; +import { provideRender } from '@cacheplane/render'; +import { environment } from '../environments/environment'; + +export const appConfig: ApplicationConfig = { + providers: [ + provideStreamResource({ apiUrl: environment.langGraphApiUrl }), + provideChat({}), + provideRender({}), + ], +}; diff --git a/cockpit/render/element-rendering/angular/src/app/element-rendering.component.ts b/cockpit/render/element-rendering/angular/src/app/element-rendering.component.ts new file mode 100644 index 000000000..323eae6e3 --- /dev/null +++ b/cockpit/render/element-rendering/angular/src/app/element-rendering.component.ts @@ -0,0 +1,74 @@ +// SPDX-License-Identifier: PolyForm-Noncommercial-1.0.0 +import { Component, signal } from '@angular/core'; +import { JsonPipe } from '@angular/common'; +import { ChatComponent } from '@cacheplane/chat'; +import { RenderSpecComponent, signalStateStore } from '@cacheplane/render'; +import { streamResource } from '@cacheplane/stream-resource'; +import { environment } from '../environments/environment'; + +/** + * ElementRenderingComponent demonstrates RenderElementComponent from @cacheplane/render. + * + * Shows how nested element trees are recursively rendered with visibility conditions. + * The sidebar displays a nested element tree with a toggle button for visibility, + * while the chat area communicates with an agent that explains element rendering. + */ +@Component({ + selector: 'app-element-rendering', + standalone: true, + imports: [ChatComponent, RenderSpecComponent, JsonPipe], + template: ` +
+ + +
+ `, +}) +export class ElementRenderingComponent { + protected readonly stream = streamResource({ + apiUrl: environment.langGraphApiUrl, + assistantId: environment.streamingAssistantId, + }); + + protected readonly store = signalStateStore({ showDetail: true }); + + protected readonly demoSpec = signal({ + type: 'container', + props: { class: 'space-y-2' }, + children: [ + { type: 'text', props: { content: 'Parent Element' } }, + { + type: 'container', + props: { class: 'pl-4 space-y-1' }, + children: [ + { type: 'text', props: { content: 'Child element (always visible)' } }, + { type: 'text', props: { content: 'Detail child (toggleable)', visible: { bind: '/showDetail' } } }, + ], + }, + ], + }); + + toggleVisibility() { + const current = this.store.get('/showDetail')(); + this.store.set('/showDetail', !current); + } +} diff --git a/cockpit/render/element-rendering/angular/src/environments/environment.development.ts b/cockpit/render/element-rendering/angular/src/environments/environment.development.ts new file mode 100644 index 000000000..a9ded1ad6 --- /dev/null +++ b/cockpit/render/element-rendering/angular/src/environments/environment.development.ts @@ -0,0 +1,5 @@ +export const environment = { + production: false, + langGraphApiUrl: 'http://localhost:4402/api', + streamingAssistantId: 'element-rendering', +}; diff --git a/cockpit/render/element-rendering/angular/src/environments/environment.ts b/cockpit/render/element-rendering/angular/src/environments/environment.ts new file mode 100644 index 000000000..8d5c213ab --- /dev/null +++ b/cockpit/render/element-rendering/angular/src/environments/environment.ts @@ -0,0 +1,5 @@ +export const environment = { + production: true, + langGraphApiUrl: '/api', + streamingAssistantId: 'element-rendering', +}; diff --git a/cockpit/render/element-rendering/angular/src/index.html b/cockpit/render/element-rendering/angular/src/index.html new file mode 100644 index 000000000..cfa75c946 --- /dev/null +++ b/cockpit/render/element-rendering/angular/src/index.html @@ -0,0 +1,13 @@ + + + + + Render Element Rendering — Angular + + + + + + + + diff --git a/cockpit/render/element-rendering/angular/src/index.ts b/cockpit/render/element-rendering/angular/src/index.ts new file mode 100644 index 000000000..df11d4e15 --- /dev/null +++ b/cockpit/render/element-rendering/angular/src/index.ts @@ -0,0 +1,29 @@ +export interface CockpitCapabilityModule { + id: string; + manifestIdentity: { + product: 'render'; + section: 'core-capabilities'; + topic: 'element-rendering'; + page: 'overview'; + language: 'angular'; + }; + title: string; + docsPath: string; + promptAssetPaths: string[]; + codeAssetPaths: string[]; +} + +export const renderElementRenderingAngularModule: CockpitCapabilityModule = { + id: 'render-element-rendering-angular', + manifestIdentity: { + product: 'render', + section: 'core-capabilities', + topic: 'element-rendering', + page: 'overview', + language: 'angular', + }, + title: 'Render Element Rendering (Angular)', + docsPath: '/docs/render/core-capabilities/element-rendering/overview/angular', + promptAssetPaths: ['cockpit/render/element-rendering/angular/prompts/element-rendering.md'], + codeAssetPaths: ['cockpit/render/element-rendering/angular/src/app/element-rendering.component.ts'], +}; diff --git a/cockpit/render/element-rendering/angular/src/main.ts b/cockpit/render/element-rendering/angular/src/main.ts new file mode 100644 index 000000000..4606a6ac7 --- /dev/null +++ b/cockpit/render/element-rendering/angular/src/main.ts @@ -0,0 +1,6 @@ +// SPDX-License-Identifier: PolyForm-Noncommercial-1.0.0 +import { bootstrapApplication } from '@angular/platform-browser'; +import { appConfig } from './app/app.config'; +import { ElementRenderingComponent } from './app/element-rendering.component'; + +bootstrapApplication(ElementRenderingComponent, appConfig).catch(console.error); diff --git a/cockpit/render/element-rendering/angular/src/styles.css b/cockpit/render/element-rendering/angular/src/styles.css new file mode 100644 index 000000000..be5412ead --- /dev/null +++ b/cockpit/render/element-rendering/angular/src/styles.css @@ -0,0 +1 @@ +/* Global styles for the element-rendering capability demo */ diff --git a/cockpit/render/element-rendering/angular/tsconfig.app.json b/cockpit/render/element-rendering/angular/tsconfig.app.json new file mode 100644 index 000000000..72c01e364 --- /dev/null +++ b/cockpit/render/element-rendering/angular/tsconfig.app.json @@ -0,0 +1,9 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../../../dist/out-tsc", + "types": [] + }, + "files": ["src/main.ts"], + "include": ["src/**/*.d.ts", "src/**/*.ts"] +} diff --git a/cockpit/render/element-rendering/angular/tsconfig.json b/cockpit/render/element-rendering/angular/tsconfig.json new file mode 100644 index 000000000..3fd970371 --- /dev/null +++ b/cockpit/render/element-rendering/angular/tsconfig.json @@ -0,0 +1,24 @@ +{ + "extends": "../../../../tsconfig.base.json", + "compilerOptions": { + "noPropertyAccessFromIndexSignature": false, + "experimentalDecorators": true, + "module": "preserve", + "emitDeclarationOnly": false, + "composite": false, + "lib": ["es2022", "dom"], + "skipLibCheck": true, + "strict": false + }, + "angularCompilerOptions": { + "enableI18nLegacyMessageIdFormat": false, + "strictInjectionParameters": false, + "strictInputAccessModifiers": false, + "strictTemplates": false + }, + "files": [], + "include": [], + "references": [ + { "path": "./tsconfig.app.json" } + ] +} diff --git a/cockpit/render/element-rendering/python/docs/guide.md b/cockpit/render/element-rendering/python/docs/guide.md new file mode 100644 index 000000000..5cd1decd0 --- /dev/null +++ b/cockpit/render/element-rendering/python/docs/guide.md @@ -0,0 +1,86 @@ +# Element Rendering with @cacheplane/render + + +Recursively render nested element trees using RenderElementComponent. +Each element resolves its type from the registry and supports visibility +conditions bound to a reactive state store. + + + +Add recursive element rendering to this Angular component using +`RenderElementComponent` from `@cacheplane/render`. Define a nested element +spec, create a state store for visibility toggling, and render the tree. + + + + + +Create a spec with nested children forming a recursive tree: + +```typescript +const spec = { + type: 'container', + props: { class: 'space-y-2' }, + children: [ + { type: 'heading', props: { text: 'Parent Element' } }, + { + type: 'container', + props: { class: 'pl-4' }, + children: [ + { type: 'text', props: { content: 'Child element' } }, + { type: 'text', props: { content: 'Another child', visible: { bind: '/showDetail' } } }, + ], + }, + ], +}; +``` + + + + +Use `signalStateStore()` to manage visibility flags: + +```typescript +import { signalStateStore } from '@cacheplane/render'; + +const store = signalStateStore({ showDetail: true }); +``` + + + + +Pass the spec and store to the render component: + +```html + +``` + +RenderElementComponent handles the recursive rendering internally, +walking each level of the tree. + + + + +Each element in the tree is rendered by RenderElementComponent. Children +are resolved recursively, so deeply nested structures render correctly. +Visibility conditions at any level control the entire subtree below. + + + + +Use `streamResource()` to receive element specs from the agent: + +```typescript +protected readonly stream = streamResource({ + apiUrl: environment.langGraphApiUrl, + assistantId: environment.streamingAssistantId, +}); +``` + + + + + +Use JSON Pointer paths like `/showDetail` to bind visibility conditions +to values in the state store. + diff --git a/cockpit/render/element-rendering/python/langgraph.json b/cockpit/render/element-rendering/python/langgraph.json new file mode 100644 index 000000000..4d29646b2 --- /dev/null +++ b/cockpit/render/element-rendering/python/langgraph.json @@ -0,0 +1,8 @@ +{ + "graphs": { + "element-rendering": "./src/graph.py:graph" + }, + "dependencies": ["."], + "python_version": "3.12", + "env": ".env" +} diff --git a/cockpit/render/element-rendering/python/prompts/element-rendering.md b/cockpit/render/element-rendering/python/prompts/element-rendering.md new file mode 100644 index 000000000..4d7e55bd3 --- /dev/null +++ b/cockpit/render/element-rendering/python/prompts/element-rendering.md @@ -0,0 +1,15 @@ +# Element Rendering Assistant + +You are an assistant that demonstrates RenderElementComponent from @cacheplane/render. + +RenderElementComponent recursively renders nested element trees. Each element +in the tree can have children, forming a recursive structure. Visibility +conditions control whether an element and its subtree are rendered. + +When the user asks about element rendering, explain: +- How RenderElementComponent walks the element tree recursively +- How each element resolves its type from the registry +- How visibility conditions (e.g., bound to state store values) can show/hide subtrees +- How nested children inherit context from their parent elements + +Include JSON element spec examples showing nested structures with visibility bindings. diff --git a/cockpit/render/element-rendering/python/pyproject.toml b/cockpit/render/element-rendering/python/pyproject.toml new file mode 100644 index 000000000..4bd11bfb7 --- /dev/null +++ b/cockpit/render/element-rendering/python/pyproject.toml @@ -0,0 +1,21 @@ +[project] +name = "cockpit-render-element-rendering" +version = "0.1.0" +requires-python = ">=3.12" +dependencies = [ + "langgraph>=0.3", + "langchain-openai>=0.3", + "langsmith>=0.2", +] + +[tool.uv] +dev-dependencies = [ + "langgraph-cli[inmem]>=0.1", +] + +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[tool.hatch.build.targets.wheel] +packages = ["src"] diff --git a/cockpit/render/element-rendering/python/src/graph.py b/cockpit/render/element-rendering/python/src/graph.py new file mode 100644 index 000000000..b8dbc9d6c --- /dev/null +++ b/cockpit/render/element-rendering/python/src/graph.py @@ -0,0 +1,40 @@ +""" +Render Element Rendering Graph + +A LangGraph StateGraph that explains recursive RenderElementComponent usage. +The Angular frontend uses RenderElementComponent to recursively render +nested element trees with visibility conditions. +""" + +from pathlib import Path +from langgraph.graph import StateGraph, MessagesState, END +from langchain_openai import ChatOpenAI +from langchain_core.messages import SystemMessage + +PROMPTS_DIR = Path(__file__).parent.parent / "prompts" + + +def build_element_rendering_graph(): + """ + Constructs a graph that explains recursive element rendering. + + The agent responds with guidance on using RenderElementComponent to + recursively render nested element trees with visibility conditions. + """ + llm = ChatOpenAI(model="gpt-5-mini", streaming=True) + + async def generate(state: MessagesState) -> dict: + system_prompt = (PROMPTS_DIR / "element-rendering.md").read_text() + messages = [SystemMessage(content=system_prompt)] + state["messages"] + response = await llm.ainvoke(messages) + return {"messages": [response]} + + graph = StateGraph(MessagesState) + graph.add_node("generate", generate) + graph.set_entry_point("generate") + graph.add_edge("generate", END) + + return graph.compile() + + +graph = build_element_rendering_graph() diff --git a/cockpit/render/element-rendering/python/src/index.ts b/cockpit/render/element-rendering/python/src/index.ts new file mode 100644 index 000000000..0746c6cec --- /dev/null +++ b/cockpit/render/element-rendering/python/src/index.ts @@ -0,0 +1,42 @@ +export interface CockpitCapabilityModule { + id: string; + manifestIdentity: { + product: 'render'; + section: 'core-capabilities'; + topic: 'element-rendering'; + page: 'overview'; + language: 'python'; + }; + title: string; + docsPath: string; + promptAssetPaths: string[]; + codeAssetPaths: string[]; + backendAssetPaths: string[]; + docsAssetPaths: string[]; + runtimeUrl?: string; + devPort?: number; +} + +export const renderElementRenderingPythonModule: CockpitCapabilityModule = { + id: 'render-element-rendering-python', + manifestIdentity: { + product: 'render', + section: 'core-capabilities', + topic: 'element-rendering', + page: 'overview', + language: 'python', + }, + title: 'Render Element Rendering (Python)', + docsPath: '/docs/render/core-capabilities/element-rendering/overview/python', + promptAssetPaths: ['cockpit/render/element-rendering/python/prompts/element-rendering.md'], + codeAssetPaths: [ + 'cockpit/render/element-rendering/angular/src/app/element-rendering.component.ts', + 'cockpit/render/element-rendering/angular/src/app/app.config.ts', + ], + backendAssetPaths: [ + 'cockpit/render/element-rendering/python/src/graph.py', + ], + docsAssetPaths: ['cockpit/render/element-rendering/python/docs/guide.md'], + runtimeUrl: 'render/element-rendering', + devPort: 4402, +}; From c63b6ad130c54379ef42131a0e2c09aaa49c5e92 Mon Sep 17 00:00:00 2001 From: Brian Love Date: Wed, 8 Apr 2026 10:03:40 -0700 Subject: [PATCH 03/48] feat(cockpit): add render/state-management capability Co-Authored-By: Claude Opus 4.6 (1M context) --- .../angular/e2e/state-management.spec.ts | 19 ++++ .../state-management/angular/project.json | 61 +++++++++++++ .../state-management/angular/proxy.conf.json | 9 ++ .../angular/src/app/app.config.ts | 14 +++ .../src/app/state-management.component.ts | 85 ++++++++++++++++++ .../environments/environment.development.ts | 5 ++ .../angular/src/environments/environment.ts | 5 ++ .../state-management/angular/src/index.html | 13 +++ .../state-management/angular/src/index.ts | 29 ++++++ .../state-management/angular/src/main.ts | 6 ++ .../state-management/angular/src/styles.css | 1 + .../angular/tsconfig.app.json | 9 ++ .../state-management/angular/tsconfig.json | 24 +++++ .../state-management/python/docs/guide.md | 90 +++++++++++++++++++ .../state-management/python/langgraph.json | 8 ++ .../python/prompts/state-management.md | 18 ++++ .../state-management/python/pyproject.toml | 21 +++++ .../state-management/python/src/graph.py | 39 ++++++++ .../state-management/python/src/index.ts | 42 +++++++++ 19 files changed, 498 insertions(+) create mode 100644 cockpit/render/state-management/angular/e2e/state-management.spec.ts create mode 100644 cockpit/render/state-management/angular/project.json create mode 100644 cockpit/render/state-management/angular/proxy.conf.json create mode 100644 cockpit/render/state-management/angular/src/app/app.config.ts create mode 100644 cockpit/render/state-management/angular/src/app/state-management.component.ts create mode 100644 cockpit/render/state-management/angular/src/environments/environment.development.ts create mode 100644 cockpit/render/state-management/angular/src/environments/environment.ts create mode 100644 cockpit/render/state-management/angular/src/index.html create mode 100644 cockpit/render/state-management/angular/src/index.ts create mode 100644 cockpit/render/state-management/angular/src/main.ts create mode 100644 cockpit/render/state-management/angular/src/styles.css create mode 100644 cockpit/render/state-management/angular/tsconfig.app.json create mode 100644 cockpit/render/state-management/angular/tsconfig.json create mode 100644 cockpit/render/state-management/python/docs/guide.md create mode 100644 cockpit/render/state-management/python/langgraph.json create mode 100644 cockpit/render/state-management/python/prompts/state-management.md create mode 100644 cockpit/render/state-management/python/pyproject.toml create mode 100644 cockpit/render/state-management/python/src/graph.py create mode 100644 cockpit/render/state-management/python/src/index.ts diff --git a/cockpit/render/state-management/angular/e2e/state-management.spec.ts b/cockpit/render/state-management/angular/e2e/state-management.spec.ts new file mode 100644 index 000000000..000c6fc24 --- /dev/null +++ b/cockpit/render/state-management/angular/e2e/state-management.spec.ts @@ -0,0 +1,19 @@ +import { expect, test } from '@playwright/test'; + +test.describe('Render State Management Example', () => { + test.beforeEach(async ({ page }) => { + await page.goto('http://localhost:4403'); + await page.waitForSelector('app-state-management', { state: 'attached' }); + }); + + test('renders the sidebar with state display', async ({ page }) => { + await expect(page.locator('chat')).toBeVisible(); + await expect(page.locator('aside')).toBeVisible(); + await expect(page.locator('.state-display')).toBeVisible(); + }); + + test('displays current state values', async ({ page }) => { + await expect(page.locator('aside h3')).toHaveText('State Management'); + await expect(page.locator('aside pre')).toContainText('Alice'); + }); +}); diff --git a/cockpit/render/state-management/angular/project.json b/cockpit/render/state-management/angular/project.json new file mode 100644 index 000000000..b44f95870 --- /dev/null +++ b/cockpit/render/state-management/angular/project.json @@ -0,0 +1,61 @@ +{ + "name": "cockpit-render-state-management-angular", + "$schema": "../../../../node_modules/nx/schemas/project-schema.json", + "sourceRoot": "cockpit/render/state-management/angular/src", + "projectType": "application", + "targets": { + "build": { + "executor": "@angular/build:application", + "outputs": ["{options.outputPath.base}"], + "options": { + "outputPath": { + "base": "dist/cockpit/render/state-management/angular", + "browser": "" + }, + "browser": "cockpit/render/state-management/angular/src/main.ts", + "tsConfig": "cockpit/render/state-management/angular/tsconfig.app.json", + "styles": ["cockpit/render/state-management/angular/src/styles.css"] + }, + "configurations": { + "production": { + "budgets": [ + { "type": "initial", "maximumWarning": "500kb", "maximumError": "1mb" }, + { "type": "anyComponentStyle", "maximumWarning": "4kb", "maximumError": "8kb" } + ], + "outputHashing": "none" + }, + "development": { + "optimization": false, + "extractLicenses": false, + "sourceMap": true, + "fileReplacements": [ + { + "replace": "cockpit/render/state-management/angular/src/environments/environment.ts", + "with": "cockpit/render/state-management/angular/src/environments/environment.development.ts" + } + ] + } + }, + "defaultConfiguration": "production" + }, + "serve": { + "continuous": true, + "executor": "@angular/build:dev-server", + "configurations": { + "production": { "buildTarget": "cockpit-render-state-management-angular:build:production" }, + "development": { "buildTarget": "cockpit-render-state-management-angular:build:development" } + }, + "defaultConfiguration": "development", + "options": { + "proxyConfig": "cockpit/render/state-management/angular/proxy.conf.json" + } + }, + "smoke": { + "executor": "nx:run-commands", + "options": { + "cwd": "cockpit/render/state-management/angular", + "command": "npx tsx -e \"import { renderStateManagementAngularModule } from './src/index.ts'; const module = renderStateManagementAngularModule; if (module.id !== 'render-state-management-angular' || module.title !== 'Render State Management (Angular)') { throw new Error('Unexpected module shape for ' + module.id); }\"" + } + } + } +} diff --git a/cockpit/render/state-management/angular/proxy.conf.json b/cockpit/render/state-management/angular/proxy.conf.json new file mode 100644 index 000000000..8523362d7 --- /dev/null +++ b/cockpit/render/state-management/angular/proxy.conf.json @@ -0,0 +1,9 @@ +{ + "/api": { + "target": "http://localhost:8123", + "secure": false, + "changeOrigin": true, + "pathRewrite": { "^/api": "" }, + "ws": true + } +} diff --git a/cockpit/render/state-management/angular/src/app/app.config.ts b/cockpit/render/state-management/angular/src/app/app.config.ts new file mode 100644 index 000000000..08e9180d0 --- /dev/null +++ b/cockpit/render/state-management/angular/src/app/app.config.ts @@ -0,0 +1,14 @@ +// SPDX-License-Identifier: PolyForm-Noncommercial-1.0.0 +import { ApplicationConfig } from '@angular/core'; +import { provideStreamResource } from '@cacheplane/stream-resource'; +import { provideChat } from '@cacheplane/chat'; +import { provideRender } from '@cacheplane/render'; +import { environment } from '../environments/environment'; + +export const appConfig: ApplicationConfig = { + providers: [ + provideStreamResource({ apiUrl: environment.langGraphApiUrl }), + provideChat({}), + provideRender({}), + ], +}; diff --git a/cockpit/render/state-management/angular/src/app/state-management.component.ts b/cockpit/render/state-management/angular/src/app/state-management.component.ts new file mode 100644 index 000000000..605126dd1 --- /dev/null +++ b/cockpit/render/state-management/angular/src/app/state-management.component.ts @@ -0,0 +1,85 @@ +// SPDX-License-Identifier: PolyForm-Noncommercial-1.0.0 +import { Component } from '@angular/core'; +import { JsonPipe } from '@angular/common'; +import { ChatComponent } from '@cacheplane/chat'; +import { RenderSpecComponent, signalStateStore } from '@cacheplane/render'; +import { streamResource } from '@cacheplane/stream-resource'; +import { environment } from '../environments/environment'; + +/** + * StateManagementComponent demonstrates signalStateStore from @cacheplane/render. + * + * Shows how to use get/set/update methods with JSON Pointer paths for + * reactive state management. The sidebar displays an interactive form + * with state store values that update reactively. + */ +@Component({ + selector: 'app-state-management', + standalone: true, + imports: [ChatComponent, RenderSpecComponent, JsonPipe], + template: ` +
+ + +
+ `, +}) +export class StateManagementComponent { + protected readonly stream = streamResource({ + apiUrl: environment.langGraphApiUrl, + assistantId: environment.streamingAssistantId, + }); + + protected readonly store = signalStateStore({ + user: { name: 'Alice', age: 30 }, + settings: { theme: 'dark' }, + }); + + protected currentState() { + return { + user: { + name: this.store.get('/user/name')(), + age: this.store.get('/user/age')(), + }, + settings: { + theme: this.store.get('/settings/theme')(), + }, + }; + } +} diff --git a/cockpit/render/state-management/angular/src/environments/environment.development.ts b/cockpit/render/state-management/angular/src/environments/environment.development.ts new file mode 100644 index 000000000..4e13de3b0 --- /dev/null +++ b/cockpit/render/state-management/angular/src/environments/environment.development.ts @@ -0,0 +1,5 @@ +export const environment = { + production: false, + langGraphApiUrl: 'http://localhost:4403/api', + streamingAssistantId: 'state-management', +}; diff --git a/cockpit/render/state-management/angular/src/environments/environment.ts b/cockpit/render/state-management/angular/src/environments/environment.ts new file mode 100644 index 000000000..d4e4e4363 --- /dev/null +++ b/cockpit/render/state-management/angular/src/environments/environment.ts @@ -0,0 +1,5 @@ +export const environment = { + production: true, + langGraphApiUrl: '/api', + streamingAssistantId: 'state-management', +}; diff --git a/cockpit/render/state-management/angular/src/index.html b/cockpit/render/state-management/angular/src/index.html new file mode 100644 index 000000000..4e02ce74f --- /dev/null +++ b/cockpit/render/state-management/angular/src/index.html @@ -0,0 +1,13 @@ + + + + + Render State Management — Angular + + + + + + + + diff --git a/cockpit/render/state-management/angular/src/index.ts b/cockpit/render/state-management/angular/src/index.ts new file mode 100644 index 000000000..d1f559401 --- /dev/null +++ b/cockpit/render/state-management/angular/src/index.ts @@ -0,0 +1,29 @@ +export interface CockpitCapabilityModule { + id: string; + manifestIdentity: { + product: 'render'; + section: 'core-capabilities'; + topic: 'state-management'; + page: 'overview'; + language: 'angular'; + }; + title: string; + docsPath: string; + promptAssetPaths: string[]; + codeAssetPaths: string[]; +} + +export const renderStateManagementAngularModule: CockpitCapabilityModule = { + id: 'render-state-management-angular', + manifestIdentity: { + product: 'render', + section: 'core-capabilities', + topic: 'state-management', + page: 'overview', + language: 'angular', + }, + title: 'Render State Management (Angular)', + docsPath: '/docs/render/core-capabilities/state-management/overview/angular', + promptAssetPaths: ['cockpit/render/state-management/angular/prompts/state-management.md'], + codeAssetPaths: ['cockpit/render/state-management/angular/src/app/state-management.component.ts'], +}; diff --git a/cockpit/render/state-management/angular/src/main.ts b/cockpit/render/state-management/angular/src/main.ts new file mode 100644 index 000000000..9a62a96f5 --- /dev/null +++ b/cockpit/render/state-management/angular/src/main.ts @@ -0,0 +1,6 @@ +// SPDX-License-Identifier: PolyForm-Noncommercial-1.0.0 +import { bootstrapApplication } from '@angular/platform-browser'; +import { appConfig } from './app/app.config'; +import { StateManagementComponent } from './app/state-management.component'; + +bootstrapApplication(StateManagementComponent, appConfig).catch(console.error); diff --git a/cockpit/render/state-management/angular/src/styles.css b/cockpit/render/state-management/angular/src/styles.css new file mode 100644 index 000000000..d3782ccf2 --- /dev/null +++ b/cockpit/render/state-management/angular/src/styles.css @@ -0,0 +1 @@ +/* Global styles for the state-management capability demo */ diff --git a/cockpit/render/state-management/angular/tsconfig.app.json b/cockpit/render/state-management/angular/tsconfig.app.json new file mode 100644 index 000000000..72c01e364 --- /dev/null +++ b/cockpit/render/state-management/angular/tsconfig.app.json @@ -0,0 +1,9 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../../../dist/out-tsc", + "types": [] + }, + "files": ["src/main.ts"], + "include": ["src/**/*.d.ts", "src/**/*.ts"] +} diff --git a/cockpit/render/state-management/angular/tsconfig.json b/cockpit/render/state-management/angular/tsconfig.json new file mode 100644 index 000000000..3fd970371 --- /dev/null +++ b/cockpit/render/state-management/angular/tsconfig.json @@ -0,0 +1,24 @@ +{ + "extends": "../../../../tsconfig.base.json", + "compilerOptions": { + "noPropertyAccessFromIndexSignature": false, + "experimentalDecorators": true, + "module": "preserve", + "emitDeclarationOnly": false, + "composite": false, + "lib": ["es2022", "dom"], + "skipLibCheck": true, + "strict": false + }, + "angularCompilerOptions": { + "enableI18nLegacyMessageIdFormat": false, + "strictInjectionParameters": false, + "strictInputAccessModifiers": false, + "strictTemplates": false + }, + "files": [], + "include": [], + "references": [ + { "path": "./tsconfig.app.json" } + ] +} diff --git a/cockpit/render/state-management/python/docs/guide.md b/cockpit/render/state-management/python/docs/guide.md new file mode 100644 index 000000000..f1811e796 --- /dev/null +++ b/cockpit/render/state-management/python/docs/guide.md @@ -0,0 +1,90 @@ +# State Management with @cacheplane/render + + +Manage reactive UI state using signalStateStore with JSON Pointer paths. +The store provides get/set/update methods backed by Angular Signals for +automatic UI propagation. + + + +Add reactive state management to this Angular component using +`signalStateStore()` from `@cacheplane/render`. Create a store with +nested state, read values with get(), write with set(), and batch +updates with update(). + + + + + +Initialize a `signalStateStore()` with your initial state shape: + +```typescript +import { signalStateStore } from '@cacheplane/render'; + +const store = signalStateStore({ + user: { name: '', age: 0 }, + settings: { theme: 'dark' }, +}); +``` + + + + +Use JSON Pointer paths to read reactive Signal values: + +```typescript +const name = store.get('/user/name'); // Signal +const theme = store.get('/settings/theme'); // Signal + +// In template: {{ name() }} +``` + + + + +Set individual values at any path: + +```typescript +store.set('/user/name', 'Alice'); +store.set('/user/age', 30); +store.set('/settings/theme', 'light'); +``` + +All Signals referencing these paths update automatically. + + + + +Apply multiple changes atomically: + +```typescript +store.update((draft) => { + draft.user.name = 'Bob'; + draft.user.age = 25; + draft.settings.theme = 'dark'; +}); +``` + + + + +Render specs can bind props to store paths: + +```typescript +const spec = { + type: 'text', + props: { content: { bind: '/user/name' } }, +}; +``` + +```html + +``` + + + + + +JSON Pointer paths follow RFC 6901. Use `/` to separate segments: +`/user/name` points to `state.user.name`. + diff --git a/cockpit/render/state-management/python/langgraph.json b/cockpit/render/state-management/python/langgraph.json new file mode 100644 index 000000000..2c4995a2a --- /dev/null +++ b/cockpit/render/state-management/python/langgraph.json @@ -0,0 +1,8 @@ +{ + "graphs": { + "state-management": "./src/graph.py:graph" + }, + "dependencies": ["."], + "python_version": "3.12", + "env": ".env" +} diff --git a/cockpit/render/state-management/python/prompts/state-management.md b/cockpit/render/state-management/python/prompts/state-management.md new file mode 100644 index 000000000..9e1120299 --- /dev/null +++ b/cockpit/render/state-management/python/prompts/state-management.md @@ -0,0 +1,18 @@ +# State Management Assistant + +You are an assistant that demonstrates signalStateStore from @cacheplane/render. + +signalStateStore provides reactive state management using JSON Pointer paths +(RFC 6901) and Angular Signals. It supports: + +- **get(path)**: Returns a Signal for the value at the given JSON Pointer path +- **set(path, value)**: Sets the value at the given path, triggering reactive updates +- **update(fn)**: Batch updates multiple values at once + +When the user asks about state management, explain: +- How JSON Pointer paths like `/user/name` address nested state +- How get() returns Angular Signals that automatically update the UI +- How set() triggers reactive propagation to all bound components +- How update() enables atomic batch modifications + +Include examples of creating stores, reading/writing values, and binding to render specs. diff --git a/cockpit/render/state-management/python/pyproject.toml b/cockpit/render/state-management/python/pyproject.toml new file mode 100644 index 000000000..fe757312c --- /dev/null +++ b/cockpit/render/state-management/python/pyproject.toml @@ -0,0 +1,21 @@ +[project] +name = "cockpit-render-state-management" +version = "0.1.0" +requires-python = ">=3.12" +dependencies = [ + "langgraph>=0.3", + "langchain-openai>=0.3", + "langsmith>=0.2", +] + +[tool.uv] +dev-dependencies = [ + "langgraph-cli[inmem]>=0.1", +] + +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[tool.hatch.build.targets.wheel] +packages = ["src"] diff --git a/cockpit/render/state-management/python/src/graph.py b/cockpit/render/state-management/python/src/graph.py new file mode 100644 index 000000000..97d07bd10 --- /dev/null +++ b/cockpit/render/state-management/python/src/graph.py @@ -0,0 +1,39 @@ +""" +Render State Management Graph + +A LangGraph StateGraph that explains signalStateStore usage with +JSON Pointer paths for reactive state management in Angular. +""" + +from pathlib import Path +from langgraph.graph import StateGraph, MessagesState, END +from langchain_openai import ChatOpenAI +from langchain_core.messages import SystemMessage + +PROMPTS_DIR = Path(__file__).parent.parent / "prompts" + + +def build_state_management_graph(): + """ + Constructs a graph that explains signalStateStore. + + The agent responds with guidance on using signalStateStore for + reactive state management with JSON Pointer paths. + """ + llm = ChatOpenAI(model="gpt-5-mini", streaming=True) + + async def generate(state: MessagesState) -> dict: + system_prompt = (PROMPTS_DIR / "state-management.md").read_text() + messages = [SystemMessage(content=system_prompt)] + state["messages"] + response = await llm.ainvoke(messages) + return {"messages": [response]} + + graph = StateGraph(MessagesState) + graph.add_node("generate", generate) + graph.set_entry_point("generate") + graph.add_edge("generate", END) + + return graph.compile() + + +graph = build_state_management_graph() diff --git a/cockpit/render/state-management/python/src/index.ts b/cockpit/render/state-management/python/src/index.ts new file mode 100644 index 000000000..210aad4ea --- /dev/null +++ b/cockpit/render/state-management/python/src/index.ts @@ -0,0 +1,42 @@ +export interface CockpitCapabilityModule { + id: string; + manifestIdentity: { + product: 'render'; + section: 'core-capabilities'; + topic: 'state-management'; + page: 'overview'; + language: 'python'; + }; + title: string; + docsPath: string; + promptAssetPaths: string[]; + codeAssetPaths: string[]; + backendAssetPaths: string[]; + docsAssetPaths: string[]; + runtimeUrl?: string; + devPort?: number; +} + +export const renderStateManagementPythonModule: CockpitCapabilityModule = { + id: 'render-state-management-python', + manifestIdentity: { + product: 'render', + section: 'core-capabilities', + topic: 'state-management', + page: 'overview', + language: 'python', + }, + title: 'Render State Management (Python)', + docsPath: '/docs/render/core-capabilities/state-management/overview/python', + promptAssetPaths: ['cockpit/render/state-management/python/prompts/state-management.md'], + codeAssetPaths: [ + 'cockpit/render/state-management/angular/src/app/state-management.component.ts', + 'cockpit/render/state-management/angular/src/app/app.config.ts', + ], + backendAssetPaths: [ + 'cockpit/render/state-management/python/src/graph.py', + ], + docsAssetPaths: ['cockpit/render/state-management/python/docs/guide.md'], + runtimeUrl: 'render/state-management', + devPort: 4403, +}; From ce594a328390bcad2512cf565459c304a6dc6966 Mon Sep 17 00:00:00 2001 From: Brian Love Date: Wed, 8 Apr 2026 10:05:26 -0700 Subject: [PATCH 04/48] feat(cockpit): add render/registry capability Co-Authored-By: Claude Opus 4.6 (1M context) --- .../registry/angular/e2e/registry.spec.ts | 18 ++++ cockpit/render/registry/angular/project.json | 61 ++++++++++++++ .../render/registry/angular/proxy.conf.json | 9 ++ .../registry/angular/src/app/app.config.ts | 14 ++++ .../angular/src/app/registry.component.ts | 61 ++++++++++++++ .../environments/environment.development.ts | 5 ++ .../angular/src/environments/environment.ts | 5 ++ .../render/registry/angular/src/index.html | 13 +++ cockpit/render/registry/angular/src/index.ts | 29 +++++++ cockpit/render/registry/angular/src/main.ts | 6 ++ .../render/registry/angular/src/styles.css | 1 + .../render/registry/angular/tsconfig.app.json | 9 ++ cockpit/render/registry/angular/tsconfig.json | 24 ++++++ cockpit/render/registry/python/docs/guide.md | 83 +++++++++++++++++++ cockpit/render/registry/python/langgraph.json | 8 ++ .../registry/python/prompts/registry.md | 15 ++++ cockpit/render/registry/python/pyproject.toml | 21 +++++ cockpit/render/registry/python/src/graph.py | 39 +++++++++ cockpit/render/registry/python/src/index.ts | 42 ++++++++++ 19 files changed, 463 insertions(+) create mode 100644 cockpit/render/registry/angular/e2e/registry.spec.ts create mode 100644 cockpit/render/registry/angular/project.json create mode 100644 cockpit/render/registry/angular/proxy.conf.json create mode 100644 cockpit/render/registry/angular/src/app/app.config.ts create mode 100644 cockpit/render/registry/angular/src/app/registry.component.ts create mode 100644 cockpit/render/registry/angular/src/environments/environment.development.ts create mode 100644 cockpit/render/registry/angular/src/environments/environment.ts create mode 100644 cockpit/render/registry/angular/src/index.html create mode 100644 cockpit/render/registry/angular/src/index.ts create mode 100644 cockpit/render/registry/angular/src/main.ts create mode 100644 cockpit/render/registry/angular/src/styles.css create mode 100644 cockpit/render/registry/angular/tsconfig.app.json create mode 100644 cockpit/render/registry/angular/tsconfig.json create mode 100644 cockpit/render/registry/python/docs/guide.md create mode 100644 cockpit/render/registry/python/langgraph.json create mode 100644 cockpit/render/registry/python/prompts/registry.md create mode 100644 cockpit/render/registry/python/pyproject.toml create mode 100644 cockpit/render/registry/python/src/graph.py create mode 100644 cockpit/render/registry/python/src/index.ts diff --git a/cockpit/render/registry/angular/e2e/registry.spec.ts b/cockpit/render/registry/angular/e2e/registry.spec.ts new file mode 100644 index 000000000..78aea5bee --- /dev/null +++ b/cockpit/render/registry/angular/e2e/registry.spec.ts @@ -0,0 +1,18 @@ +import { expect, test } from '@playwright/test'; + +test.describe('Render Registry Example', () => { + test.beforeEach(async ({ page }) => { + await page.goto('http://localhost:4404'); + await page.waitForSelector('app-registry', { state: 'attached' }); + }); + + test('renders the sidebar with registry info', async ({ page }) => { + await expect(page.locator('chat')).toBeVisible(); + await expect(page.locator('aside')).toBeVisible(); + await expect(page.locator('.registry-info')).toBeVisible(); + }); + + test('displays the component registry heading', async ({ page }) => { + await expect(page.locator('aside h3')).toHaveText('Component Registry'); + }); +}); diff --git a/cockpit/render/registry/angular/project.json b/cockpit/render/registry/angular/project.json new file mode 100644 index 000000000..c771d1fa5 --- /dev/null +++ b/cockpit/render/registry/angular/project.json @@ -0,0 +1,61 @@ +{ + "name": "cockpit-render-registry-angular", + "$schema": "../../../../node_modules/nx/schemas/project-schema.json", + "sourceRoot": "cockpit/render/registry/angular/src", + "projectType": "application", + "targets": { + "build": { + "executor": "@angular/build:application", + "outputs": ["{options.outputPath.base}"], + "options": { + "outputPath": { + "base": "dist/cockpit/render/registry/angular", + "browser": "" + }, + "browser": "cockpit/render/registry/angular/src/main.ts", + "tsConfig": "cockpit/render/registry/angular/tsconfig.app.json", + "styles": ["cockpit/render/registry/angular/src/styles.css"] + }, + "configurations": { + "production": { + "budgets": [ + { "type": "initial", "maximumWarning": "500kb", "maximumError": "1mb" }, + { "type": "anyComponentStyle", "maximumWarning": "4kb", "maximumError": "8kb" } + ], + "outputHashing": "none" + }, + "development": { + "optimization": false, + "extractLicenses": false, + "sourceMap": true, + "fileReplacements": [ + { + "replace": "cockpit/render/registry/angular/src/environments/environment.ts", + "with": "cockpit/render/registry/angular/src/environments/environment.development.ts" + } + ] + } + }, + "defaultConfiguration": "production" + }, + "serve": { + "continuous": true, + "executor": "@angular/build:dev-server", + "configurations": { + "production": { "buildTarget": "cockpit-render-registry-angular:build:production" }, + "development": { "buildTarget": "cockpit-render-registry-angular:build:development" } + }, + "defaultConfiguration": "development", + "options": { + "proxyConfig": "cockpit/render/registry/angular/proxy.conf.json" + } + }, + "smoke": { + "executor": "nx:run-commands", + "options": { + "cwd": "cockpit/render/registry/angular", + "command": "npx tsx -e \"import { renderRegistryAngularModule } from './src/index.ts'; const module = renderRegistryAngularModule; if (module.id !== 'render-registry-angular' || module.title !== 'Render Registry (Angular)') { throw new Error('Unexpected module shape for ' + module.id); }\"" + } + } + } +} diff --git a/cockpit/render/registry/angular/proxy.conf.json b/cockpit/render/registry/angular/proxy.conf.json new file mode 100644 index 000000000..8523362d7 --- /dev/null +++ b/cockpit/render/registry/angular/proxy.conf.json @@ -0,0 +1,9 @@ +{ + "/api": { + "target": "http://localhost:8123", + "secure": false, + "changeOrigin": true, + "pathRewrite": { "^/api": "" }, + "ws": true + } +} diff --git a/cockpit/render/registry/angular/src/app/app.config.ts b/cockpit/render/registry/angular/src/app/app.config.ts new file mode 100644 index 000000000..08e9180d0 --- /dev/null +++ b/cockpit/render/registry/angular/src/app/app.config.ts @@ -0,0 +1,14 @@ +// SPDX-License-Identifier: PolyForm-Noncommercial-1.0.0 +import { ApplicationConfig } from '@angular/core'; +import { provideStreamResource } from '@cacheplane/stream-resource'; +import { provideChat } from '@cacheplane/chat'; +import { provideRender } from '@cacheplane/render'; +import { environment } from '../environments/environment'; + +export const appConfig: ApplicationConfig = { + providers: [ + provideStreamResource({ apiUrl: environment.langGraphApiUrl }), + provideChat({}), + provideRender({}), + ], +}; diff --git a/cockpit/render/registry/angular/src/app/registry.component.ts b/cockpit/render/registry/angular/src/app/registry.component.ts new file mode 100644 index 000000000..f65a20946 --- /dev/null +++ b/cockpit/render/registry/angular/src/app/registry.component.ts @@ -0,0 +1,61 @@ +// SPDX-License-Identifier: PolyForm-Noncommercial-1.0.0 +import { Component } from '@angular/core'; +import { JsonPipe } from '@angular/common'; +import { ChatComponent } from '@cacheplane/chat'; +import { RenderSpecComponent, defineAngularRegistry } from '@cacheplane/render'; +import { streamResource } from '@cacheplane/stream-resource'; +import { environment } from '../environments/environment'; + +/** + * RegistryComponent demonstrates defineAngularRegistry from @cacheplane/render. + * + * Shows how to create a component registry, list registered types, and + * look up components by type string. The sidebar displays the list of + * registered component types. + */ +@Component({ + selector: 'app-registry', + standalone: true, + imports: [ChatComponent, RenderSpecComponent, JsonPipe], + template: ` +
+ + +
+ `, +}) +export class RegistryComponent { + protected readonly stream = streamResource({ + apiUrl: environment.langGraphApiUrl, + assistantId: environment.streamingAssistantId, + }); + + private readonly registry = defineAngularRegistry({}); + + protected readonly registeredNames = this.registry.names(); +} diff --git a/cockpit/render/registry/angular/src/environments/environment.development.ts b/cockpit/render/registry/angular/src/environments/environment.development.ts new file mode 100644 index 000000000..c56c1d51f --- /dev/null +++ b/cockpit/render/registry/angular/src/environments/environment.development.ts @@ -0,0 +1,5 @@ +export const environment = { + production: false, + langGraphApiUrl: 'http://localhost:4404/api', + streamingAssistantId: 'registry', +}; diff --git a/cockpit/render/registry/angular/src/environments/environment.ts b/cockpit/render/registry/angular/src/environments/environment.ts new file mode 100644 index 000000000..63ce5caff --- /dev/null +++ b/cockpit/render/registry/angular/src/environments/environment.ts @@ -0,0 +1,5 @@ +export const environment = { + production: true, + langGraphApiUrl: '/api', + streamingAssistantId: 'registry', +}; diff --git a/cockpit/render/registry/angular/src/index.html b/cockpit/render/registry/angular/src/index.html new file mode 100644 index 000000000..6ccab583b --- /dev/null +++ b/cockpit/render/registry/angular/src/index.html @@ -0,0 +1,13 @@ + + + + + Render Registry — Angular + + + + + + + + diff --git a/cockpit/render/registry/angular/src/index.ts b/cockpit/render/registry/angular/src/index.ts new file mode 100644 index 000000000..a4c11b4d1 --- /dev/null +++ b/cockpit/render/registry/angular/src/index.ts @@ -0,0 +1,29 @@ +export interface CockpitCapabilityModule { + id: string; + manifestIdentity: { + product: 'render'; + section: 'core-capabilities'; + topic: 'registry'; + page: 'overview'; + language: 'angular'; + }; + title: string; + docsPath: string; + promptAssetPaths: string[]; + codeAssetPaths: string[]; +} + +export const renderRegistryAngularModule: CockpitCapabilityModule = { + id: 'render-registry-angular', + manifestIdentity: { + product: 'render', + section: 'core-capabilities', + topic: 'registry', + page: 'overview', + language: 'angular', + }, + title: 'Render Registry (Angular)', + docsPath: '/docs/render/core-capabilities/registry/overview/angular', + promptAssetPaths: ['cockpit/render/registry/angular/prompts/registry.md'], + codeAssetPaths: ['cockpit/render/registry/angular/src/app/registry.component.ts'], +}; diff --git a/cockpit/render/registry/angular/src/main.ts b/cockpit/render/registry/angular/src/main.ts new file mode 100644 index 000000000..81544a19d --- /dev/null +++ b/cockpit/render/registry/angular/src/main.ts @@ -0,0 +1,6 @@ +// SPDX-License-Identifier: PolyForm-Noncommercial-1.0.0 +import { bootstrapApplication } from '@angular/platform-browser'; +import { appConfig } from './app/app.config'; +import { RegistryComponent } from './app/registry.component'; + +bootstrapApplication(RegistryComponent, appConfig).catch(console.error); diff --git a/cockpit/render/registry/angular/src/styles.css b/cockpit/render/registry/angular/src/styles.css new file mode 100644 index 000000000..cb74ac244 --- /dev/null +++ b/cockpit/render/registry/angular/src/styles.css @@ -0,0 +1 @@ +/* Global styles for the registry capability demo */ diff --git a/cockpit/render/registry/angular/tsconfig.app.json b/cockpit/render/registry/angular/tsconfig.app.json new file mode 100644 index 000000000..72c01e364 --- /dev/null +++ b/cockpit/render/registry/angular/tsconfig.app.json @@ -0,0 +1,9 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../../../dist/out-tsc", + "types": [] + }, + "files": ["src/main.ts"], + "include": ["src/**/*.d.ts", "src/**/*.ts"] +} diff --git a/cockpit/render/registry/angular/tsconfig.json b/cockpit/render/registry/angular/tsconfig.json new file mode 100644 index 000000000..3fd970371 --- /dev/null +++ b/cockpit/render/registry/angular/tsconfig.json @@ -0,0 +1,24 @@ +{ + "extends": "../../../../tsconfig.base.json", + "compilerOptions": { + "noPropertyAccessFromIndexSignature": false, + "experimentalDecorators": true, + "module": "preserve", + "emitDeclarationOnly": false, + "composite": false, + "lib": ["es2022", "dom"], + "skipLibCheck": true, + "strict": false + }, + "angularCompilerOptions": { + "enableI18nLegacyMessageIdFormat": false, + "strictInjectionParameters": false, + "strictInputAccessModifiers": false, + "strictTemplates": false + }, + "files": [], + "include": [], + "references": [ + { "path": "./tsconfig.app.json" } + ] +} diff --git a/cockpit/render/registry/python/docs/guide.md b/cockpit/render/registry/python/docs/guide.md new file mode 100644 index 000000000..3570bdf6b --- /dev/null +++ b/cockpit/render/registry/python/docs/guide.md @@ -0,0 +1,83 @@ +# Registry with @cacheplane/render + + +Map type strings to Angular component classes using defineAngularRegistry. +The registry resolves component types at render time, enabling JSON specs +to reference components by name. + + + +Add a component registry to this Angular application using +`defineAngularRegistry()` from `@cacheplane/render`. Register component +types, look them up with `registry.get()`, and list all types with +`registry.names()`. + + + + + +Create simple Angular components that will be registered: + +```typescript +@Component({ selector: 'app-card', template: '
' }) +export class CardComponent {} + +@Component({ selector: 'app-badge', template: '{{ label }}' }) +export class BadgeComponent { @Input() label = ''; } +``` + +
+ + +Map type strings to component classes: + +```typescript +import { defineAngularRegistry } from '@cacheplane/render'; + +const registry = defineAngularRegistry({ + card: CardComponent, + badge: BadgeComponent, +}); +``` + + + + +Look up a component class by its type string: + +```typescript +const CardClass = registry.get('card'); // CardComponent +const BadgeClass = registry.get('badge'); // BadgeComponent +``` + + + + +Get all registered type strings: + +```typescript +const types = registry.names(); // ['card', 'badge'] +``` + + + + +Pass the registry to provideRender in your app config: + +```typescript +export const appConfig: ApplicationConfig = { + providers: [ + provideRender({ registry }), + ], +}; +``` + +RenderSpecComponent will use this registry to resolve types in JSON specs. + + +
+ + +Keep registry entries focused — each type string should map to exactly one +component. Use descriptive names like 'data-table' or 'stat-card' for clarity. + diff --git a/cockpit/render/registry/python/langgraph.json b/cockpit/render/registry/python/langgraph.json new file mode 100644 index 000000000..0ecff1a5b --- /dev/null +++ b/cockpit/render/registry/python/langgraph.json @@ -0,0 +1,8 @@ +{ + "graphs": { + "registry": "./src/graph.py:graph" + }, + "dependencies": ["."], + "python_version": "3.12", + "env": ".env" +} diff --git a/cockpit/render/registry/python/prompts/registry.md b/cockpit/render/registry/python/prompts/registry.md new file mode 100644 index 000000000..9af148941 --- /dev/null +++ b/cockpit/render/registry/python/prompts/registry.md @@ -0,0 +1,15 @@ +# Registry Assistant + +You are an assistant that demonstrates defineAngularRegistry from @cacheplane/render. + +defineAngularRegistry() creates a component registry that maps type strings +to Angular component classes. This registry is used by RenderSpecComponent +to resolve which component to instantiate for each element in a render spec. + +When the user asks about the registry, explain: +- How defineAngularRegistry() accepts a map of type string to component class +- How registry.get(type) returns the component class for a given type +- How registry.names() returns all registered type strings +- How the registry integrates with provideRender() configuration + +Include examples showing registry creation, lookup, and integration with the render system. diff --git a/cockpit/render/registry/python/pyproject.toml b/cockpit/render/registry/python/pyproject.toml new file mode 100644 index 000000000..16cd7ec36 --- /dev/null +++ b/cockpit/render/registry/python/pyproject.toml @@ -0,0 +1,21 @@ +[project] +name = "cockpit-render-registry" +version = "0.1.0" +requires-python = ">=3.12" +dependencies = [ + "langgraph>=0.3", + "langchain-openai>=0.3", + "langsmith>=0.2", +] + +[tool.uv] +dev-dependencies = [ + "langgraph-cli[inmem]>=0.1", +] + +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[tool.hatch.build.targets.wheel] +packages = ["src"] diff --git a/cockpit/render/registry/python/src/graph.py b/cockpit/render/registry/python/src/graph.py new file mode 100644 index 000000000..fb47e1e05 --- /dev/null +++ b/cockpit/render/registry/python/src/graph.py @@ -0,0 +1,39 @@ +""" +Render Registry Graph + +A LangGraph StateGraph that explains defineAngularRegistry() for mapping +type strings to Angular component classes. +""" + +from pathlib import Path +from langgraph.graph import StateGraph, MessagesState, END +from langchain_openai import ChatOpenAI +from langchain_core.messages import SystemMessage + +PROMPTS_DIR = Path(__file__).parent.parent / "prompts" + + +def build_registry_graph(): + """ + Constructs a graph that explains defineAngularRegistry. + + The agent responds with guidance on creating and using component + registries for render spec type resolution. + """ + llm = ChatOpenAI(model="gpt-5-mini", streaming=True) + + async def generate(state: MessagesState) -> dict: + system_prompt = (PROMPTS_DIR / "registry.md").read_text() + messages = [SystemMessage(content=system_prompt)] + state["messages"] + response = await llm.ainvoke(messages) + return {"messages": [response]} + + graph = StateGraph(MessagesState) + graph.add_node("generate", generate) + graph.set_entry_point("generate") + graph.add_edge("generate", END) + + return graph.compile() + + +graph = build_registry_graph() diff --git a/cockpit/render/registry/python/src/index.ts b/cockpit/render/registry/python/src/index.ts new file mode 100644 index 000000000..21928736c --- /dev/null +++ b/cockpit/render/registry/python/src/index.ts @@ -0,0 +1,42 @@ +export interface CockpitCapabilityModule { + id: string; + manifestIdentity: { + product: 'render'; + section: 'core-capabilities'; + topic: 'registry'; + page: 'overview'; + language: 'python'; + }; + title: string; + docsPath: string; + promptAssetPaths: string[]; + codeAssetPaths: string[]; + backendAssetPaths: string[]; + docsAssetPaths: string[]; + runtimeUrl?: string; + devPort?: number; +} + +export const renderRegistryPythonModule: CockpitCapabilityModule = { + id: 'render-registry-python', + manifestIdentity: { + product: 'render', + section: 'core-capabilities', + topic: 'registry', + page: 'overview', + language: 'python', + }, + title: 'Render Registry (Python)', + docsPath: '/docs/render/core-capabilities/registry/overview/python', + promptAssetPaths: ['cockpit/render/registry/python/prompts/registry.md'], + codeAssetPaths: [ + 'cockpit/render/registry/angular/src/app/registry.component.ts', + 'cockpit/render/registry/angular/src/app/app.config.ts', + ], + backendAssetPaths: [ + 'cockpit/render/registry/python/src/graph.py', + ], + docsAssetPaths: ['cockpit/render/registry/python/docs/guide.md'], + runtimeUrl: 'render/registry', + devPort: 4404, +}; From d0717debeba5c4fdaf9acfe459e095139a73d517 Mon Sep 17 00:00:00 2001 From: Brian Love Date: Wed, 8 Apr 2026 10:07:06 -0700 Subject: [PATCH 05/48] feat(cockpit): add render/repeat-loops capability Co-Authored-By: Claude Opus 4.6 (1M context) --- .../angular/e2e/repeat-loops.spec.ts | 19 ++++ .../render/repeat-loops/angular/project.json | 61 ++++++++++++ .../repeat-loops/angular/proxy.conf.json | 9 ++ .../angular/src/app/app.config.ts | 14 +++ .../angular/src/app/repeat-loops.component.ts | 84 ++++++++++++++++ .../environments/environment.development.ts | 5 + .../angular/src/environments/environment.ts | 5 + .../repeat-loops/angular/src/index.html | 13 +++ .../render/repeat-loops/angular/src/index.ts | 29 ++++++ .../render/repeat-loops/angular/src/main.ts | 6 ++ .../repeat-loops/angular/src/styles.css | 1 + .../repeat-loops/angular/tsconfig.app.json | 9 ++ .../render/repeat-loops/angular/tsconfig.json | 24 +++++ .../render/repeat-loops/python/docs/guide.md | 95 +++++++++++++++++++ .../render/repeat-loops/python/langgraph.json | 8 ++ .../python/prompts/repeat-loops.md | 19 ++++ .../render/repeat-loops/python/pyproject.toml | 21 ++++ .../render/repeat-loops/python/src/graph.py | 39 ++++++++ .../render/repeat-loops/python/src/index.ts | 42 ++++++++ 19 files changed, 503 insertions(+) create mode 100644 cockpit/render/repeat-loops/angular/e2e/repeat-loops.spec.ts create mode 100644 cockpit/render/repeat-loops/angular/project.json create mode 100644 cockpit/render/repeat-loops/angular/proxy.conf.json create mode 100644 cockpit/render/repeat-loops/angular/src/app/app.config.ts create mode 100644 cockpit/render/repeat-loops/angular/src/app/repeat-loops.component.ts create mode 100644 cockpit/render/repeat-loops/angular/src/environments/environment.development.ts create mode 100644 cockpit/render/repeat-loops/angular/src/environments/environment.ts create mode 100644 cockpit/render/repeat-loops/angular/src/index.html create mode 100644 cockpit/render/repeat-loops/angular/src/index.ts create mode 100644 cockpit/render/repeat-loops/angular/src/main.ts create mode 100644 cockpit/render/repeat-loops/angular/src/styles.css create mode 100644 cockpit/render/repeat-loops/angular/tsconfig.app.json create mode 100644 cockpit/render/repeat-loops/angular/tsconfig.json create mode 100644 cockpit/render/repeat-loops/python/docs/guide.md create mode 100644 cockpit/render/repeat-loops/python/langgraph.json create mode 100644 cockpit/render/repeat-loops/python/prompts/repeat-loops.md create mode 100644 cockpit/render/repeat-loops/python/pyproject.toml create mode 100644 cockpit/render/repeat-loops/python/src/graph.py create mode 100644 cockpit/render/repeat-loops/python/src/index.ts diff --git a/cockpit/render/repeat-loops/angular/e2e/repeat-loops.spec.ts b/cockpit/render/repeat-loops/angular/e2e/repeat-loops.spec.ts new file mode 100644 index 000000000..7deb13e31 --- /dev/null +++ b/cockpit/render/repeat-loops/angular/e2e/repeat-loops.spec.ts @@ -0,0 +1,19 @@ +import { expect, test } from '@playwright/test'; + +test.describe('Render Repeat Loops Example', () => { + test.beforeEach(async ({ page }) => { + await page.goto('http://localhost:4405'); + await page.waitForSelector('app-repeat-loops', { state: 'attached' }); + }); + + test('renders the sidebar with list items', async ({ page }) => { + await expect(page.locator('chat')).toBeVisible(); + await expect(page.locator('aside')).toBeVisible(); + await expect(page.locator('.item-list')).toBeVisible(); + }); + + test('displays repeat loop heading and items', async ({ page }) => { + await expect(page.locator('aside h3')).toHaveText('Repeat Loop Items'); + await expect(page.locator('aside pre')).toContainText('Task Alpha'); + }); +}); diff --git a/cockpit/render/repeat-loops/angular/project.json b/cockpit/render/repeat-loops/angular/project.json new file mode 100644 index 000000000..b1127da91 --- /dev/null +++ b/cockpit/render/repeat-loops/angular/project.json @@ -0,0 +1,61 @@ +{ + "name": "cockpit-render-repeat-loops-angular", + "$schema": "../../../../node_modules/nx/schemas/project-schema.json", + "sourceRoot": "cockpit/render/repeat-loops/angular/src", + "projectType": "application", + "targets": { + "build": { + "executor": "@angular/build:application", + "outputs": ["{options.outputPath.base}"], + "options": { + "outputPath": { + "base": "dist/cockpit/render/repeat-loops/angular", + "browser": "" + }, + "browser": "cockpit/render/repeat-loops/angular/src/main.ts", + "tsConfig": "cockpit/render/repeat-loops/angular/tsconfig.app.json", + "styles": ["cockpit/render/repeat-loops/angular/src/styles.css"] + }, + "configurations": { + "production": { + "budgets": [ + { "type": "initial", "maximumWarning": "500kb", "maximumError": "1mb" }, + { "type": "anyComponentStyle", "maximumWarning": "4kb", "maximumError": "8kb" } + ], + "outputHashing": "none" + }, + "development": { + "optimization": false, + "extractLicenses": false, + "sourceMap": true, + "fileReplacements": [ + { + "replace": "cockpit/render/repeat-loops/angular/src/environments/environment.ts", + "with": "cockpit/render/repeat-loops/angular/src/environments/environment.development.ts" + } + ] + } + }, + "defaultConfiguration": "production" + }, + "serve": { + "continuous": true, + "executor": "@angular/build:dev-server", + "configurations": { + "production": { "buildTarget": "cockpit-render-repeat-loops-angular:build:production" }, + "development": { "buildTarget": "cockpit-render-repeat-loops-angular:build:development" } + }, + "defaultConfiguration": "development", + "options": { + "proxyConfig": "cockpit/render/repeat-loops/angular/proxy.conf.json" + } + }, + "smoke": { + "executor": "nx:run-commands", + "options": { + "cwd": "cockpit/render/repeat-loops/angular", + "command": "npx tsx -e \"import { renderRepeatLoopsAngularModule } from './src/index.ts'; const module = renderRepeatLoopsAngularModule; if (module.id !== 'render-repeat-loops-angular' || module.title !== 'Render Repeat Loops (Angular)') { throw new Error('Unexpected module shape for ' + module.id); }\"" + } + } + } +} diff --git a/cockpit/render/repeat-loops/angular/proxy.conf.json b/cockpit/render/repeat-loops/angular/proxy.conf.json new file mode 100644 index 000000000..8523362d7 --- /dev/null +++ b/cockpit/render/repeat-loops/angular/proxy.conf.json @@ -0,0 +1,9 @@ +{ + "/api": { + "target": "http://localhost:8123", + "secure": false, + "changeOrigin": true, + "pathRewrite": { "^/api": "" }, + "ws": true + } +} diff --git a/cockpit/render/repeat-loops/angular/src/app/app.config.ts b/cockpit/render/repeat-loops/angular/src/app/app.config.ts new file mode 100644 index 000000000..08e9180d0 --- /dev/null +++ b/cockpit/render/repeat-loops/angular/src/app/app.config.ts @@ -0,0 +1,14 @@ +// SPDX-License-Identifier: PolyForm-Noncommercial-1.0.0 +import { ApplicationConfig } from '@angular/core'; +import { provideStreamResource } from '@cacheplane/stream-resource'; +import { provideChat } from '@cacheplane/chat'; +import { provideRender } from '@cacheplane/render'; +import { environment } from '../environments/environment'; + +export const appConfig: ApplicationConfig = { + providers: [ + provideStreamResource({ apiUrl: environment.langGraphApiUrl }), + provideChat({}), + provideRender({}), + ], +}; diff --git a/cockpit/render/repeat-loops/angular/src/app/repeat-loops.component.ts b/cockpit/render/repeat-loops/angular/src/app/repeat-loops.component.ts new file mode 100644 index 000000000..5fa470fe8 --- /dev/null +++ b/cockpit/render/repeat-loops/angular/src/app/repeat-loops.component.ts @@ -0,0 +1,84 @@ +// SPDX-License-Identifier: PolyForm-Noncommercial-1.0.0 +import { Component } from '@angular/core'; +import { JsonPipe } from '@angular/common'; +import { ChatComponent } from '@cacheplane/chat'; +import { RenderSpecComponent, signalStateStore } from '@cacheplane/render'; +import { streamResource } from '@cacheplane/stream-resource'; +import { environment } from '../environments/environment'; + +/** + * RepeatLoopsComponent demonstrates repeat rendering from @cacheplane/render. + * + * Shows how to iterate over arrays using repeat specs with RepeatScope. + * The sidebar displays a list of items rendered via repeat with add/remove buttons. + */ +@Component({ + selector: 'app-repeat-loops', + standalone: true, + imports: [ChatComponent, RenderSpecComponent, JsonPipe], + template: ` +
+ + +
+ `, +}) +export class RepeatLoopsComponent { + protected readonly stream = streamResource({ + apiUrl: environment.langGraphApiUrl, + assistantId: environment.streamingAssistantId, + }); + + protected readonly store = signalStateStore({ + items: [ + { name: 'Task Alpha', done: false }, + { name: 'Task Beta', done: true }, + { name: 'Task Gamma', done: false }, + ], + }); + + protected readonly items = this.store.get('/items'); + + private counter = 0; + + addItem() { + this.counter++; + this.store.update((draft: any) => { + draft.items.push({ name: `Task ${this.counter}`, done: false }); + }); + } + + removeItem(index: number) { + this.store.update((draft: any) => { + draft.items.splice(index, 1); + }); + } +} diff --git a/cockpit/render/repeat-loops/angular/src/environments/environment.development.ts b/cockpit/render/repeat-loops/angular/src/environments/environment.development.ts new file mode 100644 index 000000000..0adc49e3e --- /dev/null +++ b/cockpit/render/repeat-loops/angular/src/environments/environment.development.ts @@ -0,0 +1,5 @@ +export const environment = { + production: false, + langGraphApiUrl: 'http://localhost:4405/api', + streamingAssistantId: 'repeat-loops', +}; diff --git a/cockpit/render/repeat-loops/angular/src/environments/environment.ts b/cockpit/render/repeat-loops/angular/src/environments/environment.ts new file mode 100644 index 000000000..0bf5fa918 --- /dev/null +++ b/cockpit/render/repeat-loops/angular/src/environments/environment.ts @@ -0,0 +1,5 @@ +export const environment = { + production: true, + langGraphApiUrl: '/api', + streamingAssistantId: 'repeat-loops', +}; diff --git a/cockpit/render/repeat-loops/angular/src/index.html b/cockpit/render/repeat-loops/angular/src/index.html new file mode 100644 index 000000000..081ba5944 --- /dev/null +++ b/cockpit/render/repeat-loops/angular/src/index.html @@ -0,0 +1,13 @@ + + + + + Render Repeat Loops — Angular + + + + + + + + diff --git a/cockpit/render/repeat-loops/angular/src/index.ts b/cockpit/render/repeat-loops/angular/src/index.ts new file mode 100644 index 000000000..5e4d442c9 --- /dev/null +++ b/cockpit/render/repeat-loops/angular/src/index.ts @@ -0,0 +1,29 @@ +export interface CockpitCapabilityModule { + id: string; + manifestIdentity: { + product: 'render'; + section: 'core-capabilities'; + topic: 'repeat-loops'; + page: 'overview'; + language: 'angular'; + }; + title: string; + docsPath: string; + promptAssetPaths: string[]; + codeAssetPaths: string[]; +} + +export const renderRepeatLoopsAngularModule: CockpitCapabilityModule = { + id: 'render-repeat-loops-angular', + manifestIdentity: { + product: 'render', + section: 'core-capabilities', + topic: 'repeat-loops', + page: 'overview', + language: 'angular', + }, + title: 'Render Repeat Loops (Angular)', + docsPath: '/docs/render/core-capabilities/repeat-loops/overview/angular', + promptAssetPaths: ['cockpit/render/repeat-loops/angular/prompts/repeat-loops.md'], + codeAssetPaths: ['cockpit/render/repeat-loops/angular/src/app/repeat-loops.component.ts'], +}; diff --git a/cockpit/render/repeat-loops/angular/src/main.ts b/cockpit/render/repeat-loops/angular/src/main.ts new file mode 100644 index 000000000..0d2cac5f6 --- /dev/null +++ b/cockpit/render/repeat-loops/angular/src/main.ts @@ -0,0 +1,6 @@ +// SPDX-License-Identifier: PolyForm-Noncommercial-1.0.0 +import { bootstrapApplication } from '@angular/platform-browser'; +import { appConfig } from './app/app.config'; +import { RepeatLoopsComponent } from './app/repeat-loops.component'; + +bootstrapApplication(RepeatLoopsComponent, appConfig).catch(console.error); diff --git a/cockpit/render/repeat-loops/angular/src/styles.css b/cockpit/render/repeat-loops/angular/src/styles.css new file mode 100644 index 000000000..4b78948f5 --- /dev/null +++ b/cockpit/render/repeat-loops/angular/src/styles.css @@ -0,0 +1 @@ +/* Global styles for the repeat-loops capability demo */ diff --git a/cockpit/render/repeat-loops/angular/tsconfig.app.json b/cockpit/render/repeat-loops/angular/tsconfig.app.json new file mode 100644 index 000000000..72c01e364 --- /dev/null +++ b/cockpit/render/repeat-loops/angular/tsconfig.app.json @@ -0,0 +1,9 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../../../dist/out-tsc", + "types": [] + }, + "files": ["src/main.ts"], + "include": ["src/**/*.d.ts", "src/**/*.ts"] +} diff --git a/cockpit/render/repeat-loops/angular/tsconfig.json b/cockpit/render/repeat-loops/angular/tsconfig.json new file mode 100644 index 000000000..3fd970371 --- /dev/null +++ b/cockpit/render/repeat-loops/angular/tsconfig.json @@ -0,0 +1,24 @@ +{ + "extends": "../../../../tsconfig.base.json", + "compilerOptions": { + "noPropertyAccessFromIndexSignature": false, + "experimentalDecorators": true, + "module": "preserve", + "emitDeclarationOnly": false, + "composite": false, + "lib": ["es2022", "dom"], + "skipLibCheck": true, + "strict": false + }, + "angularCompilerOptions": { + "enableI18nLegacyMessageIdFormat": false, + "strictInjectionParameters": false, + "strictInputAccessModifiers": false, + "strictTemplates": false + }, + "files": [], + "include": [], + "references": [ + { "path": "./tsconfig.app.json" } + ] +} diff --git a/cockpit/render/repeat-loops/python/docs/guide.md b/cockpit/render/repeat-loops/python/docs/guide.md new file mode 100644 index 000000000..ad6833554 --- /dev/null +++ b/cockpit/render/repeat-loops/python/docs/guide.md @@ -0,0 +1,95 @@ +# Repeat Loops with @cacheplane/render + + +Iterate over arrays in the state store using repeat specs. Each iteration +provides RepeatScope context with repeatItem, repeatIndex, and repeatBasePath +for per-item rendering. + + + +Add repeat rendering to this Angular component using repeat specs from +`@cacheplane/render`. Define array state, create a repeat spec template, +access RepeatScope context, and add/remove items dynamically. + + + + + +Create a state store with an array to iterate over: + +```typescript +import { signalStateStore } from '@cacheplane/render'; + +const store = signalStateStore({ + items: [ + { name: 'Item A', done: false }, + { name: 'Item B', done: true }, + { name: 'Item C', done: false }, + ], +}); +``` + + + + +Define a spec with `repeat` pointing to the array path: + +```typescript +const spec = { + type: 'list', + repeat: '/items', + children: [ + { type: 'text', props: { content: { bind: 'name' } } }, + { type: 'checkbox', props: { checked: { bind: 'done' } } }, + ], +}; +``` + + + + +Inside repeated components, inject RepeatScope for iteration context: + +```typescript +const scope = inject(RepeatScope); +const item = scope.repeatItem; // current item +const index = scope.repeatIndex; // zero-based index +const basePath = scope.repeatBasePath; // e.g. '/items/0' +``` + + + + +Modify the array in the store to add or remove items: + +```typescript +// Add an item +store.update((draft) => { + draft.items.push({ name: 'New Item', done: false }); +}); + +// Remove an item by index +store.update((draft) => { + draft.items.splice(index, 1); +}); +``` + + + + +Use `streamResource()` to receive repeat specs from the agent: + +```typescript +protected readonly stream = streamResource({ + apiUrl: environment.langGraphApiUrl, + assistantId: environment.streamingAssistantId, +}); +``` + + + + + +repeatBasePath gives you the JSON Pointer for the current item (e.g. `/items/2`), +so child bindings can use relative paths within each iteration. + diff --git a/cockpit/render/repeat-loops/python/langgraph.json b/cockpit/render/repeat-loops/python/langgraph.json new file mode 100644 index 000000000..52316e099 --- /dev/null +++ b/cockpit/render/repeat-loops/python/langgraph.json @@ -0,0 +1,8 @@ +{ + "graphs": { + "repeat-loops": "./src/graph.py:graph" + }, + "dependencies": ["."], + "python_version": "3.12", + "env": ".env" +} diff --git a/cockpit/render/repeat-loops/python/prompts/repeat-loops.md b/cockpit/render/repeat-loops/python/prompts/repeat-loops.md new file mode 100644 index 000000000..d249e1f75 --- /dev/null +++ b/cockpit/render/repeat-loops/python/prompts/repeat-loops.md @@ -0,0 +1,19 @@ +# Repeat Loops Assistant + +You are an assistant that demonstrates repeat rendering from @cacheplane/render. + +Repeat rendering allows iterating over arrays in the state store to render +a template for each item. It uses: + +- **RepeatScope**: An injection token providing context for each iteration +- **repeatItem**: The current item in the iteration +- **repeatIndex**: The zero-based index of the current item +- **repeatBasePath**: The JSON Pointer base path for the current item + +When the user asks about repeat loops, explain: +- How to define a repeat spec that iterates over an array in the state store +- How RepeatScope provides per-iteration context to child components +- How repeatBasePath enables relative path resolution within each iteration +- How to add and remove items from the array dynamically + +Include examples of repeat specs with array state and item templates. diff --git a/cockpit/render/repeat-loops/python/pyproject.toml b/cockpit/render/repeat-loops/python/pyproject.toml new file mode 100644 index 000000000..d22027246 --- /dev/null +++ b/cockpit/render/repeat-loops/python/pyproject.toml @@ -0,0 +1,21 @@ +[project] +name = "cockpit-render-repeat-loops" +version = "0.1.0" +requires-python = ">=3.12" +dependencies = [ + "langgraph>=0.3", + "langchain-openai>=0.3", + "langsmith>=0.2", +] + +[tool.uv] +dev-dependencies = [ + "langgraph-cli[inmem]>=0.1", +] + +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[tool.hatch.build.targets.wheel] +packages = ["src"] diff --git a/cockpit/render/repeat-loops/python/src/graph.py b/cockpit/render/repeat-loops/python/src/graph.py new file mode 100644 index 000000000..73cbaa3ff --- /dev/null +++ b/cockpit/render/repeat-loops/python/src/graph.py @@ -0,0 +1,39 @@ +""" +Render Repeat Loops Graph + +A LangGraph StateGraph that explains repeat/loop rendering with +RepeatScope for iterating arrays in render specs. +""" + +from pathlib import Path +from langgraph.graph import StateGraph, MessagesState, END +from langchain_openai import ChatOpenAI +from langchain_core.messages import SystemMessage + +PROMPTS_DIR = Path(__file__).parent.parent / "prompts" + + +def build_repeat_loops_graph(): + """ + Constructs a graph that explains repeat rendering. + + The agent responds with guidance on using RepeatScope injection token, + repeatItem, repeatIndex, and repeatBasePath for iterating arrays. + """ + llm = ChatOpenAI(model="gpt-5-mini", streaming=True) + + async def generate(state: MessagesState) -> dict: + system_prompt = (PROMPTS_DIR / "repeat-loops.md").read_text() + messages = [SystemMessage(content=system_prompt)] + state["messages"] + response = await llm.ainvoke(messages) + return {"messages": [response]} + + graph = StateGraph(MessagesState) + graph.add_node("generate", generate) + graph.set_entry_point("generate") + graph.add_edge("generate", END) + + return graph.compile() + + +graph = build_repeat_loops_graph() diff --git a/cockpit/render/repeat-loops/python/src/index.ts b/cockpit/render/repeat-loops/python/src/index.ts new file mode 100644 index 000000000..eb5f509be --- /dev/null +++ b/cockpit/render/repeat-loops/python/src/index.ts @@ -0,0 +1,42 @@ +export interface CockpitCapabilityModule { + id: string; + manifestIdentity: { + product: 'render'; + section: 'core-capabilities'; + topic: 'repeat-loops'; + page: 'overview'; + language: 'python'; + }; + title: string; + docsPath: string; + promptAssetPaths: string[]; + codeAssetPaths: string[]; + backendAssetPaths: string[]; + docsAssetPaths: string[]; + runtimeUrl?: string; + devPort?: number; +} + +export const renderRepeatLoopsPythonModule: CockpitCapabilityModule = { + id: 'render-repeat-loops-python', + manifestIdentity: { + product: 'render', + section: 'core-capabilities', + topic: 'repeat-loops', + page: 'overview', + language: 'python', + }, + title: 'Render Repeat Loops (Python)', + docsPath: '/docs/render/core-capabilities/repeat-loops/overview/python', + promptAssetPaths: ['cockpit/render/repeat-loops/python/prompts/repeat-loops.md'], + codeAssetPaths: [ + 'cockpit/render/repeat-loops/angular/src/app/repeat-loops.component.ts', + 'cockpit/render/repeat-loops/angular/src/app/app.config.ts', + ], + backendAssetPaths: [ + 'cockpit/render/repeat-loops/python/src/graph.py', + ], + docsAssetPaths: ['cockpit/render/repeat-loops/python/docs/guide.md'], + runtimeUrl: 'render/repeat-loops', + devPort: 4405, +}; From a9eea544c9e82337645f06762a66295070c0d114 Mon Sep 17 00:00:00 2001 From: Brian Love Date: Wed, 8 Apr 2026 10:08:50 -0700 Subject: [PATCH 06/48] feat(cockpit): add render/computed-functions capability Co-Authored-By: Claude Opus 4.6 (1M context) --- .../angular/e2e/computed-functions.spec.ts | 19 ++++ .../computed-functions/angular/project.json | 61 +++++++++++++ .../angular/proxy.conf.json | 9 ++ .../angular/src/app/app.config.ts | 21 +++++ .../src/app/computed-functions.component.ts | 85 ++++++++++++++++++ .../environments/environment.development.ts | 5 ++ .../angular/src/environments/environment.ts | 5 ++ .../computed-functions/angular/src/index.html | 13 +++ .../computed-functions/angular/src/index.ts | 29 ++++++ .../computed-functions/angular/src/main.ts | 6 ++ .../computed-functions/angular/src/styles.css | 1 + .../angular/tsconfig.app.json | 9 ++ .../computed-functions/angular/tsconfig.json | 24 +++++ .../computed-functions/python/docs/guide.md | 89 +++++++++++++++++++ .../computed-functions/python/langgraph.json | 8 ++ .../python/prompts/computed-functions.md | 17 ++++ .../computed-functions/python/pyproject.toml | 21 +++++ .../computed-functions/python/src/graph.py | 39 ++++++++ .../computed-functions/python/src/index.ts | 42 +++++++++ 19 files changed, 503 insertions(+) create mode 100644 cockpit/render/computed-functions/angular/e2e/computed-functions.spec.ts create mode 100644 cockpit/render/computed-functions/angular/project.json create mode 100644 cockpit/render/computed-functions/angular/proxy.conf.json create mode 100644 cockpit/render/computed-functions/angular/src/app/app.config.ts create mode 100644 cockpit/render/computed-functions/angular/src/app/computed-functions.component.ts create mode 100644 cockpit/render/computed-functions/angular/src/environments/environment.development.ts create mode 100644 cockpit/render/computed-functions/angular/src/environments/environment.ts create mode 100644 cockpit/render/computed-functions/angular/src/index.html create mode 100644 cockpit/render/computed-functions/angular/src/index.ts create mode 100644 cockpit/render/computed-functions/angular/src/main.ts create mode 100644 cockpit/render/computed-functions/angular/src/styles.css create mode 100644 cockpit/render/computed-functions/angular/tsconfig.app.json create mode 100644 cockpit/render/computed-functions/angular/tsconfig.json create mode 100644 cockpit/render/computed-functions/python/docs/guide.md create mode 100644 cockpit/render/computed-functions/python/langgraph.json create mode 100644 cockpit/render/computed-functions/python/prompts/computed-functions.md create mode 100644 cockpit/render/computed-functions/python/pyproject.toml create mode 100644 cockpit/render/computed-functions/python/src/graph.py create mode 100644 cockpit/render/computed-functions/python/src/index.ts diff --git a/cockpit/render/computed-functions/angular/e2e/computed-functions.spec.ts b/cockpit/render/computed-functions/angular/e2e/computed-functions.spec.ts new file mode 100644 index 000000000..4c0a46c55 --- /dev/null +++ b/cockpit/render/computed-functions/angular/e2e/computed-functions.spec.ts @@ -0,0 +1,19 @@ +import { expect, test } from '@playwright/test'; + +test.describe('Render Computed Functions Example', () => { + test.beforeEach(async ({ page }) => { + await page.goto('http://localhost:4406'); + await page.waitForSelector('app-computed-functions', { state: 'attached' }); + }); + + test('renders the sidebar with computed values', async ({ page }) => { + await expect(page.locator('chat')).toBeVisible(); + await expect(page.locator('aside')).toBeVisible(); + await expect(page.locator('.computed-values')).toBeVisible(); + }); + + test('displays computed functions heading', async ({ page }) => { + await expect(page.locator('aside h3')).toHaveText('Computed Values'); + await expect(page.locator('aside pre')).toContainText('formatDate'); + }); +}); diff --git a/cockpit/render/computed-functions/angular/project.json b/cockpit/render/computed-functions/angular/project.json new file mode 100644 index 000000000..bf070de8e --- /dev/null +++ b/cockpit/render/computed-functions/angular/project.json @@ -0,0 +1,61 @@ +{ + "name": "cockpit-render-computed-functions-angular", + "$schema": "../../../../node_modules/nx/schemas/project-schema.json", + "sourceRoot": "cockpit/render/computed-functions/angular/src", + "projectType": "application", + "targets": { + "build": { + "executor": "@angular/build:application", + "outputs": ["{options.outputPath.base}"], + "options": { + "outputPath": { + "base": "dist/cockpit/render/computed-functions/angular", + "browser": "" + }, + "browser": "cockpit/render/computed-functions/angular/src/main.ts", + "tsConfig": "cockpit/render/computed-functions/angular/tsconfig.app.json", + "styles": ["cockpit/render/computed-functions/angular/src/styles.css"] + }, + "configurations": { + "production": { + "budgets": [ + { "type": "initial", "maximumWarning": "500kb", "maximumError": "1mb" }, + { "type": "anyComponentStyle", "maximumWarning": "4kb", "maximumError": "8kb" } + ], + "outputHashing": "none" + }, + "development": { + "optimization": false, + "extractLicenses": false, + "sourceMap": true, + "fileReplacements": [ + { + "replace": "cockpit/render/computed-functions/angular/src/environments/environment.ts", + "with": "cockpit/render/computed-functions/angular/src/environments/environment.development.ts" + } + ] + } + }, + "defaultConfiguration": "production" + }, + "serve": { + "continuous": true, + "executor": "@angular/build:dev-server", + "configurations": { + "production": { "buildTarget": "cockpit-render-computed-functions-angular:build:production" }, + "development": { "buildTarget": "cockpit-render-computed-functions-angular:build:development" } + }, + "defaultConfiguration": "development", + "options": { + "proxyConfig": "cockpit/render/computed-functions/angular/proxy.conf.json" + } + }, + "smoke": { + "executor": "nx:run-commands", + "options": { + "cwd": "cockpit/render/computed-functions/angular", + "command": "npx tsx -e \"import { renderComputedFunctionsAngularModule } from './src/index.ts'; const module = renderComputedFunctionsAngularModule; if (module.id !== 'render-computed-functions-angular' || module.title !== 'Render Computed Functions (Angular)') { throw new Error('Unexpected module shape for ' + module.id); }\"" + } + } + } +} diff --git a/cockpit/render/computed-functions/angular/proxy.conf.json b/cockpit/render/computed-functions/angular/proxy.conf.json new file mode 100644 index 000000000..8523362d7 --- /dev/null +++ b/cockpit/render/computed-functions/angular/proxy.conf.json @@ -0,0 +1,9 @@ +{ + "/api": { + "target": "http://localhost:8123", + "secure": false, + "changeOrigin": true, + "pathRewrite": { "^/api": "" }, + "ws": true + } +} diff --git a/cockpit/render/computed-functions/angular/src/app/app.config.ts b/cockpit/render/computed-functions/angular/src/app/app.config.ts new file mode 100644 index 000000000..eb89fe1df --- /dev/null +++ b/cockpit/render/computed-functions/angular/src/app/app.config.ts @@ -0,0 +1,21 @@ +// SPDX-License-Identifier: PolyForm-Noncommercial-1.0.0 +import { ApplicationConfig } from '@angular/core'; +import { provideStreamResource } from '@cacheplane/stream-resource'; +import { provideChat } from '@cacheplane/chat'; +import { provideRender } from '@cacheplane/render'; +import { environment } from '../environments/environment'; + +export const appConfig: ApplicationConfig = { + providers: [ + provideStreamResource({ apiUrl: environment.langGraphApiUrl }), + provideChat({}), + provideRender({ + functions: { + formatDate: (value: string) => new Date(value).toLocaleDateString(), + uppercase: (value: string) => value.toUpperCase(), + multiply: (a: number, b: number) => a * b, + reverse: (value: string) => value.split('').reverse().join(''), + }, + }), + ], +}; diff --git a/cockpit/render/computed-functions/angular/src/app/computed-functions.component.ts b/cockpit/render/computed-functions/angular/src/app/computed-functions.component.ts new file mode 100644 index 000000000..34f3078eb --- /dev/null +++ b/cockpit/render/computed-functions/angular/src/app/computed-functions.component.ts @@ -0,0 +1,85 @@ +// SPDX-License-Identifier: PolyForm-Noncommercial-1.0.0 +import { Component, computed, signal } from '@angular/core'; +import { JsonPipe, DatePipe } from '@angular/common'; +import { ChatComponent } from '@cacheplane/chat'; +import { RenderSpecComponent } from '@cacheplane/render'; +import { streamResource } from '@cacheplane/stream-resource'; +import { environment } from '../environments/environment'; + +/** + * ComputedFunctionsComponent demonstrates computed functions from @cacheplane/render. + * + * Shows how custom functions transform data for prop resolution in render specs. + * The sidebar displays computed values including formatted dates, string transforms, + * and math operations. + */ +@Component({ + selector: 'app-computed-functions', + standalone: true, + imports: [ChatComponent, RenderSpecComponent, JsonPipe, DatePipe], + template: ` +
+ + +
+ `, +}) +export class ComputedFunctionsComponent { + protected readonly stream = streamResource({ + apiUrl: environment.langGraphApiUrl, + assistantId: environment.streamingAssistantId, + }); + + protected readonly inputValue = signal('hello world'); + + protected readonly formattedDate = computed(() => + new Date('2024-06-15T12:00:00Z').toLocaleDateString('en-US', { + weekday: 'long', year: 'numeric', month: 'long', day: 'numeric', + }) + ); + + protected readonly uppercased = computed(() => 'render specs are powerful'.toUpperCase()); + + protected readonly multiplied = computed(() => 7 * 6); + + protected readonly reversed = computed(() => + this.inputValue().split('').reverse().join('') + ); + + protected readonly functionNames = ['formatDate', 'uppercase', 'multiply', 'reverse']; +} diff --git a/cockpit/render/computed-functions/angular/src/environments/environment.development.ts b/cockpit/render/computed-functions/angular/src/environments/environment.development.ts new file mode 100644 index 000000000..e03355f89 --- /dev/null +++ b/cockpit/render/computed-functions/angular/src/environments/environment.development.ts @@ -0,0 +1,5 @@ +export const environment = { + production: false, + langGraphApiUrl: 'http://localhost:4406/api', + streamingAssistantId: 'computed-functions', +}; diff --git a/cockpit/render/computed-functions/angular/src/environments/environment.ts b/cockpit/render/computed-functions/angular/src/environments/environment.ts new file mode 100644 index 000000000..ecc47b9ee --- /dev/null +++ b/cockpit/render/computed-functions/angular/src/environments/environment.ts @@ -0,0 +1,5 @@ +export const environment = { + production: true, + langGraphApiUrl: '/api', + streamingAssistantId: 'computed-functions', +}; diff --git a/cockpit/render/computed-functions/angular/src/index.html b/cockpit/render/computed-functions/angular/src/index.html new file mode 100644 index 000000000..1718db452 --- /dev/null +++ b/cockpit/render/computed-functions/angular/src/index.html @@ -0,0 +1,13 @@ + + + + + Render Computed Functions — Angular + + + + + + + + diff --git a/cockpit/render/computed-functions/angular/src/index.ts b/cockpit/render/computed-functions/angular/src/index.ts new file mode 100644 index 000000000..ec12f09aa --- /dev/null +++ b/cockpit/render/computed-functions/angular/src/index.ts @@ -0,0 +1,29 @@ +export interface CockpitCapabilityModule { + id: string; + manifestIdentity: { + product: 'render'; + section: 'core-capabilities'; + topic: 'computed-functions'; + page: 'overview'; + language: 'angular'; + }; + title: string; + docsPath: string; + promptAssetPaths: string[]; + codeAssetPaths: string[]; +} + +export const renderComputedFunctionsAngularModule: CockpitCapabilityModule = { + id: 'render-computed-functions-angular', + manifestIdentity: { + product: 'render', + section: 'core-capabilities', + topic: 'computed-functions', + page: 'overview', + language: 'angular', + }, + title: 'Render Computed Functions (Angular)', + docsPath: '/docs/render/core-capabilities/computed-functions/overview/angular', + promptAssetPaths: ['cockpit/render/computed-functions/angular/prompts/computed-functions.md'], + codeAssetPaths: ['cockpit/render/computed-functions/angular/src/app/computed-functions.component.ts'], +}; diff --git a/cockpit/render/computed-functions/angular/src/main.ts b/cockpit/render/computed-functions/angular/src/main.ts new file mode 100644 index 000000000..80c2386ae --- /dev/null +++ b/cockpit/render/computed-functions/angular/src/main.ts @@ -0,0 +1,6 @@ +// SPDX-License-Identifier: PolyForm-Noncommercial-1.0.0 +import { bootstrapApplication } from '@angular/platform-browser'; +import { appConfig } from './app/app.config'; +import { ComputedFunctionsComponent } from './app/computed-functions.component'; + +bootstrapApplication(ComputedFunctionsComponent, appConfig).catch(console.error); diff --git a/cockpit/render/computed-functions/angular/src/styles.css b/cockpit/render/computed-functions/angular/src/styles.css new file mode 100644 index 000000000..8399d0316 --- /dev/null +++ b/cockpit/render/computed-functions/angular/src/styles.css @@ -0,0 +1 @@ +/* Global styles for the computed-functions capability demo */ diff --git a/cockpit/render/computed-functions/angular/tsconfig.app.json b/cockpit/render/computed-functions/angular/tsconfig.app.json new file mode 100644 index 000000000..72c01e364 --- /dev/null +++ b/cockpit/render/computed-functions/angular/tsconfig.app.json @@ -0,0 +1,9 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../../../dist/out-tsc", + "types": [] + }, + "files": ["src/main.ts"], + "include": ["src/**/*.d.ts", "src/**/*.ts"] +} diff --git a/cockpit/render/computed-functions/angular/tsconfig.json b/cockpit/render/computed-functions/angular/tsconfig.json new file mode 100644 index 000000000..3fd970371 --- /dev/null +++ b/cockpit/render/computed-functions/angular/tsconfig.json @@ -0,0 +1,24 @@ +{ + "extends": "../../../../tsconfig.base.json", + "compilerOptions": { + "noPropertyAccessFromIndexSignature": false, + "experimentalDecorators": true, + "module": "preserve", + "emitDeclarationOnly": false, + "composite": false, + "lib": ["es2022", "dom"], + "skipLibCheck": true, + "strict": false + }, + "angularCompilerOptions": { + "enableI18nLegacyMessageIdFormat": false, + "strictInjectionParameters": false, + "strictInputAccessModifiers": false, + "strictTemplates": false + }, + "files": [], + "include": [], + "references": [ + { "path": "./tsconfig.app.json" } + ] +} diff --git a/cockpit/render/computed-functions/python/docs/guide.md b/cockpit/render/computed-functions/python/docs/guide.md new file mode 100644 index 000000000..e8299bd13 --- /dev/null +++ b/cockpit/render/computed-functions/python/docs/guide.md @@ -0,0 +1,89 @@ +# Computed Functions with @cacheplane/render + + +Define custom functions for prop resolution and data transformation in +render specs. Register functions with provideRender() and reference them +in spec prop expressions for dynamic computed values. + + + +Add computed functions to this Angular application using `provideRender()` +from `@cacheplane/render`. Define custom functions for data formatting, +register them in the render config, and use them in spec props. + + + + + +Create functions for data transformation: + +```typescript +const functions = { + formatDate: (value: string) => new Date(value).toLocaleDateString(), + uppercase: (value: string) => value.toUpperCase(), + multiply: (a: number, b: number) => a * b, +}; +``` + + + + +Pass functions to the provideRender configuration: + +```typescript +export const appConfig: ApplicationConfig = { + providers: [ + provideRender({ + functions, + }), + ], +}; +``` + + + + +Reference computed functions in render spec prop expressions: + +```typescript +const spec = { + type: 'text', + props: { + content: { compute: 'formatDate', args: ['2024-01-15'] }, + }, +}; +``` + + + + +Computed functions can read from the state store: + +```typescript +const spec = { + type: 'text', + props: { + content: { compute: 'uppercase', args: [{ bind: '/user/name' }] }, + }, +}; +``` + + + + +Use `streamResource()` to receive specs with computed props from the agent: + +```typescript +protected readonly stream = streamResource({ + apiUrl: environment.langGraphApiUrl, + assistantId: environment.streamingAssistantId, +}); +``` + + + + + +Keep computed functions pure and side-effect-free. They run during every +change detection cycle, so expensive operations should be memoized. + diff --git a/cockpit/render/computed-functions/python/langgraph.json b/cockpit/render/computed-functions/python/langgraph.json new file mode 100644 index 000000000..050a72f78 --- /dev/null +++ b/cockpit/render/computed-functions/python/langgraph.json @@ -0,0 +1,8 @@ +{ + "graphs": { + "computed-functions": "./src/graph.py:graph" + }, + "dependencies": ["."], + "python_version": "3.12", + "env": ".env" +} diff --git a/cockpit/render/computed-functions/python/prompts/computed-functions.md b/cockpit/render/computed-functions/python/prompts/computed-functions.md new file mode 100644 index 000000000..8e0d90d1e --- /dev/null +++ b/cockpit/render/computed-functions/python/prompts/computed-functions.md @@ -0,0 +1,17 @@ +# Computed Functions Assistant + +You are an assistant that demonstrates computed functions from @cacheplane/render. + +Computed functions are custom functions registered with provideRender() that +can be used for prop resolution, expressions, and data transformation in +render specs. They allow render specs to reference dynamic computations +without hardcoding values. + +When the user asks about computed functions, explain: +- How to define custom functions that transform data for prop resolution +- How to register functions via the provideRender() configuration +- How render specs reference computed functions in prop expressions +- How computed functions combine with state store values for dynamic UIs + +Include examples of formatting dates, string transforms, math operations, +and combining computed values with state bindings. diff --git a/cockpit/render/computed-functions/python/pyproject.toml b/cockpit/render/computed-functions/python/pyproject.toml new file mode 100644 index 000000000..8d9001737 --- /dev/null +++ b/cockpit/render/computed-functions/python/pyproject.toml @@ -0,0 +1,21 @@ +[project] +name = "cockpit-render-computed-functions" +version = "0.1.0" +requires-python = ">=3.12" +dependencies = [ + "langgraph>=0.3", + "langchain-openai>=0.3", + "langsmith>=0.2", +] + +[tool.uv] +dev-dependencies = [ + "langgraph-cli[inmem]>=0.1", +] + +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[tool.hatch.build.targets.wheel] +packages = ["src"] diff --git a/cockpit/render/computed-functions/python/src/graph.py b/cockpit/render/computed-functions/python/src/graph.py new file mode 100644 index 000000000..b74b7fa20 --- /dev/null +++ b/cockpit/render/computed-functions/python/src/graph.py @@ -0,0 +1,39 @@ +""" +Render Computed Functions Graph + +A LangGraph StateGraph that explains computed functions and prop resolution +for data transformation in render specs. +""" + +from pathlib import Path +from langgraph.graph import StateGraph, MessagesState, END +from langchain_openai import ChatOpenAI +from langchain_core.messages import SystemMessage + +PROMPTS_DIR = Path(__file__).parent.parent / "prompts" + + +def build_computed_functions_graph(): + """ + Constructs a graph that explains computed functions. + + The agent responds with guidance on defining custom functions for + prop resolution, expressions, and data transformation in render specs. + """ + llm = ChatOpenAI(model="gpt-5-mini", streaming=True) + + async def generate(state: MessagesState) -> dict: + system_prompt = (PROMPTS_DIR / "computed-functions.md").read_text() + messages = [SystemMessage(content=system_prompt)] + state["messages"] + response = await llm.ainvoke(messages) + return {"messages": [response]} + + graph = StateGraph(MessagesState) + graph.add_node("generate", generate) + graph.set_entry_point("generate") + graph.add_edge("generate", END) + + return graph.compile() + + +graph = build_computed_functions_graph() diff --git a/cockpit/render/computed-functions/python/src/index.ts b/cockpit/render/computed-functions/python/src/index.ts new file mode 100644 index 000000000..79eef758c --- /dev/null +++ b/cockpit/render/computed-functions/python/src/index.ts @@ -0,0 +1,42 @@ +export interface CockpitCapabilityModule { + id: string; + manifestIdentity: { + product: 'render'; + section: 'core-capabilities'; + topic: 'computed-functions'; + page: 'overview'; + language: 'python'; + }; + title: string; + docsPath: string; + promptAssetPaths: string[]; + codeAssetPaths: string[]; + backendAssetPaths: string[]; + docsAssetPaths: string[]; + runtimeUrl?: string; + devPort?: number; +} + +export const renderComputedFunctionsPythonModule: CockpitCapabilityModule = { + id: 'render-computed-functions-python', + manifestIdentity: { + product: 'render', + section: 'core-capabilities', + topic: 'computed-functions', + page: 'overview', + language: 'python', + }, + title: 'Render Computed Functions (Python)', + docsPath: '/docs/render/core-capabilities/computed-functions/overview/python', + promptAssetPaths: ['cockpit/render/computed-functions/python/prompts/computed-functions.md'], + codeAssetPaths: [ + 'cockpit/render/computed-functions/angular/src/app/computed-functions.component.ts', + 'cockpit/render/computed-functions/angular/src/app/app.config.ts', + ], + backendAssetPaths: [ + 'cockpit/render/computed-functions/python/src/graph.py', + ], + docsAssetPaths: ['cockpit/render/computed-functions/python/docs/guide.md'], + runtimeUrl: 'render/computed-functions', + devPort: 4406, +}; From df72c8608ea823708a838c838492bb911b45b2a7 Mon Sep 17 00:00:00 2001 From: Brian Love Date: Wed, 8 Apr 2026 10:12:46 -0700 Subject: [PATCH 07/48] feat(cockpit): add chat/messages capability Adds the messages capability demonstrating ChatMessagesComponent, ChatInputComponent, and ChatTypingIndicatorComponent primitives. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../messages/angular/e2e/messages.spec.ts | 19 +++++ cockpit/chat/messages/angular/project.json | 61 +++++++++++++++ cockpit/chat/messages/angular/proxy.conf.json | 9 +++ .../messages/angular/src/app/app.config.ts | 12 +++ .../angular/src/app/messages.component.ts | 58 ++++++++++++++ .../environments/environment.development.ts | 5 ++ .../angular/src/environments/environment.ts | 5 ++ cockpit/chat/messages/angular/src/index.html | 13 ++++ cockpit/chat/messages/angular/src/index.ts | 29 +++++++ cockpit/chat/messages/angular/src/main.ts | 6 ++ cockpit/chat/messages/angular/src/styles.css | 1 + .../chat/messages/angular/tsconfig.app.json | 9 +++ cockpit/chat/messages/angular/tsconfig.json | 24 ++++++ cockpit/chat/messages/python/docs/guide.md | 75 +++++++++++++++++++ cockpit/chat/messages/python/langgraph.json | 8 ++ .../chat/messages/python/prompts/messages.md | 12 +++ cockpit/chat/messages/python/pyproject.toml | 21 ++++++ cockpit/chat/messages/python/src/graph.py | 39 ++++++++++ cockpit/chat/messages/python/src/index.ts | 42 +++++++++++ 19 files changed, 448 insertions(+) create mode 100644 cockpit/chat/messages/angular/e2e/messages.spec.ts create mode 100644 cockpit/chat/messages/angular/project.json create mode 100644 cockpit/chat/messages/angular/proxy.conf.json create mode 100644 cockpit/chat/messages/angular/src/app/app.config.ts create mode 100644 cockpit/chat/messages/angular/src/app/messages.component.ts create mode 100644 cockpit/chat/messages/angular/src/environments/environment.development.ts create mode 100644 cockpit/chat/messages/angular/src/environments/environment.ts create mode 100644 cockpit/chat/messages/angular/src/index.html create mode 100644 cockpit/chat/messages/angular/src/index.ts create mode 100644 cockpit/chat/messages/angular/src/main.ts create mode 100644 cockpit/chat/messages/angular/src/styles.css create mode 100644 cockpit/chat/messages/angular/tsconfig.app.json create mode 100644 cockpit/chat/messages/angular/tsconfig.json create mode 100644 cockpit/chat/messages/python/docs/guide.md create mode 100644 cockpit/chat/messages/python/langgraph.json create mode 100644 cockpit/chat/messages/python/prompts/messages.md create mode 100644 cockpit/chat/messages/python/pyproject.toml create mode 100644 cockpit/chat/messages/python/src/graph.py create mode 100644 cockpit/chat/messages/python/src/index.ts diff --git a/cockpit/chat/messages/angular/e2e/messages.spec.ts b/cockpit/chat/messages/angular/e2e/messages.spec.ts new file mode 100644 index 000000000..f535a1d32 --- /dev/null +++ b/cockpit/chat/messages/angular/e2e/messages.spec.ts @@ -0,0 +1,19 @@ +import { expect, test } from '@playwright/test'; + +test.describe('Chat Messages Example', () => { + test.beforeEach(async ({ page }) => { + await page.goto('http://localhost:4501'); + await page.waitForSelector('app-messages', { state: 'attached' }); + }); + + test('renders the chat messages interface with primitives sidebar', async ({ page }) => { + await expect(page.locator('chat-messages')).toBeVisible(); + await expect(page.locator('chat-input')).toBeVisible(); + await expect(page.locator('aside')).toBeVisible(); + await expect(page.locator('aside h3')).toHaveText('Primitives Used'); + }); + + test('displays the input component', async ({ page }) => { + await expect(page.locator('chat-input')).toBeVisible(); + }); +}); diff --git a/cockpit/chat/messages/angular/project.json b/cockpit/chat/messages/angular/project.json new file mode 100644 index 000000000..405207583 --- /dev/null +++ b/cockpit/chat/messages/angular/project.json @@ -0,0 +1,61 @@ +{ + "name": "cockpit-chat-messages-angular", + "$schema": "../../../../node_modules/nx/schemas/project-schema.json", + "sourceRoot": "cockpit/chat/messages/angular/src", + "projectType": "application", + "targets": { + "build": { + "executor": "@angular/build:application", + "outputs": ["{options.outputPath.base}"], + "options": { + "outputPath": { + "base": "dist/cockpit/chat/messages/angular", + "browser": "" + }, + "browser": "cockpit/chat/messages/angular/src/main.ts", + "tsConfig": "cockpit/chat/messages/angular/tsconfig.app.json", + "styles": ["cockpit/chat/messages/angular/src/styles.css"] + }, + "configurations": { + "production": { + "budgets": [ + { "type": "initial", "maximumWarning": "500kb", "maximumError": "1mb" }, + { "type": "anyComponentStyle", "maximumWarning": "4kb", "maximumError": "8kb" } + ], + "outputHashing": "none" + }, + "development": { + "optimization": false, + "extractLicenses": false, + "sourceMap": true, + "fileReplacements": [ + { + "replace": "cockpit/chat/messages/angular/src/environments/environment.ts", + "with": "cockpit/chat/messages/angular/src/environments/environment.development.ts" + } + ] + } + }, + "defaultConfiguration": "production" + }, + "serve": { + "continuous": true, + "executor": "@angular/build:dev-server", + "configurations": { + "production": { "buildTarget": "cockpit-chat-messages-angular:build:production" }, + "development": { "buildTarget": "cockpit-chat-messages-angular:build:development" } + }, + "defaultConfiguration": "development", + "options": { + "proxyConfig": "cockpit/chat/messages/angular/proxy.conf.json" + } + }, + "smoke": { + "executor": "nx:run-commands", + "options": { + "cwd": "cockpit/chat/messages/angular", + "command": "npx tsx -e \"import { chatMessagesAngularModule } from './src/index.ts'; const module = chatMessagesAngularModule; if (module.id !== 'chat-messages-angular' || module.title !== 'Chat Messages (Angular)') { throw new Error('Unexpected module shape for ' + module.id); }\"" + } + } + } +} diff --git a/cockpit/chat/messages/angular/proxy.conf.json b/cockpit/chat/messages/angular/proxy.conf.json new file mode 100644 index 000000000..8523362d7 --- /dev/null +++ b/cockpit/chat/messages/angular/proxy.conf.json @@ -0,0 +1,9 @@ +{ + "/api": { + "target": "http://localhost:8123", + "secure": false, + "changeOrigin": true, + "pathRewrite": { "^/api": "" }, + "ws": true + } +} diff --git a/cockpit/chat/messages/angular/src/app/app.config.ts b/cockpit/chat/messages/angular/src/app/app.config.ts new file mode 100644 index 000000000..b52a916ac --- /dev/null +++ b/cockpit/chat/messages/angular/src/app/app.config.ts @@ -0,0 +1,12 @@ +// SPDX-License-Identifier: PolyForm-Noncommercial-1.0.0 +import { ApplicationConfig } from '@angular/core'; +import { provideStreamResource } from '@cacheplane/stream-resource'; +import { provideChat } from '@cacheplane/chat'; +import { environment } from '../environments/environment'; + +export const appConfig: ApplicationConfig = { + providers: [ + provideStreamResource({ apiUrl: environment.langGraphApiUrl }), + provideChat({}), + ], +}; diff --git a/cockpit/chat/messages/angular/src/app/messages.component.ts b/cockpit/chat/messages/angular/src/app/messages.component.ts new file mode 100644 index 000000000..b96f8da4f --- /dev/null +++ b/cockpit/chat/messages/angular/src/app/messages.component.ts @@ -0,0 +1,58 @@ +// SPDX-License-Identifier: PolyForm-Noncommercial-1.0.0 +import { Component } from '@angular/core'; +import { + ChatMessagesComponent, + ChatInputComponent, + ChatTypingIndicatorComponent, +} from '@cacheplane/chat'; +import { streamResource } from '@cacheplane/stream-resource'; +import { environment } from '../environments/environment'; + +/** + * MessagesComponent demonstrates the chat message primitives from @cacheplane/chat. + * + * Uses ChatMessagesComponent, ChatInputComponent, and ChatTypingIndicatorComponent + * individually rather than the composed ChatComponent, giving full control + * over layout and message rendering. + */ +@Component({ + selector: 'app-messages', + standalone: true, + imports: [ChatMessagesComponent, ChatInputComponent, ChatTypingIndicatorComponent], + template: ` +
+
+
+

Chat Messages Primitives

+
+
+ +
+
+ + +
+
+ +
+ `, +}) +export class MessagesComponent { + protected readonly stream = streamResource({ + apiUrl: environment.langGraphApiUrl, + assistantId: environment.streamingAssistantId, + }); + + submitMessage(content: string) { + this.stream.submit([{ role: 'human', content }]); + } +} diff --git a/cockpit/chat/messages/angular/src/environments/environment.development.ts b/cockpit/chat/messages/angular/src/environments/environment.development.ts new file mode 100644 index 000000000..c9660e604 --- /dev/null +++ b/cockpit/chat/messages/angular/src/environments/environment.development.ts @@ -0,0 +1,5 @@ +export const environment = { + production: false, + langGraphApiUrl: 'http://localhost:4501/api', + streamingAssistantId: 'c-messages', +}; diff --git a/cockpit/chat/messages/angular/src/environments/environment.ts b/cockpit/chat/messages/angular/src/environments/environment.ts new file mode 100644 index 000000000..1d22185b8 --- /dev/null +++ b/cockpit/chat/messages/angular/src/environments/environment.ts @@ -0,0 +1,5 @@ +export const environment = { + production: true, + langGraphApiUrl: '/api', + streamingAssistantId: 'c-messages', +}; diff --git a/cockpit/chat/messages/angular/src/index.html b/cockpit/chat/messages/angular/src/index.html new file mode 100644 index 000000000..96fef9bbe --- /dev/null +++ b/cockpit/chat/messages/angular/src/index.html @@ -0,0 +1,13 @@ + + + + + Chat Messages — Angular + + + + + + + + diff --git a/cockpit/chat/messages/angular/src/index.ts b/cockpit/chat/messages/angular/src/index.ts new file mode 100644 index 000000000..0964e1b8a --- /dev/null +++ b/cockpit/chat/messages/angular/src/index.ts @@ -0,0 +1,29 @@ +export interface CockpitCapabilityModule { + id: string; + manifestIdentity: { + product: 'chat'; + section: 'core-capabilities'; + topic: 'messages'; + page: 'overview'; + language: 'angular'; + }; + title: string; + docsPath: string; + promptAssetPaths: string[]; + codeAssetPaths: string[]; +} + +export const chatMessagesAngularModule: CockpitCapabilityModule = { + id: 'chat-messages-angular', + manifestIdentity: { + product: 'chat', + section: 'core-capabilities', + topic: 'messages', + page: 'overview', + language: 'angular', + }, + title: 'Chat Messages (Angular)', + docsPath: '/docs/chat/core-capabilities/messages/overview/angular', + promptAssetPaths: ['cockpit/chat/messages/python/prompts/messages.md'], + codeAssetPaths: ['cockpit/chat/messages/angular/src/app/messages.component.ts'], +}; diff --git a/cockpit/chat/messages/angular/src/main.ts b/cockpit/chat/messages/angular/src/main.ts new file mode 100644 index 000000000..8c812b201 --- /dev/null +++ b/cockpit/chat/messages/angular/src/main.ts @@ -0,0 +1,6 @@ +// SPDX-License-Identifier: PolyForm-Noncommercial-1.0.0 +import { bootstrapApplication } from '@angular/platform-browser'; +import { appConfig } from './app/app.config'; +import { MessagesComponent } from './app/messages.component'; + +bootstrapApplication(MessagesComponent, appConfig).catch(console.error); diff --git a/cockpit/chat/messages/angular/src/styles.css b/cockpit/chat/messages/angular/src/styles.css new file mode 100644 index 000000000..e80c54b41 --- /dev/null +++ b/cockpit/chat/messages/angular/src/styles.css @@ -0,0 +1 @@ +/* Global styles for the messages capability demo */ diff --git a/cockpit/chat/messages/angular/tsconfig.app.json b/cockpit/chat/messages/angular/tsconfig.app.json new file mode 100644 index 000000000..72c01e364 --- /dev/null +++ b/cockpit/chat/messages/angular/tsconfig.app.json @@ -0,0 +1,9 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../../../dist/out-tsc", + "types": [] + }, + "files": ["src/main.ts"], + "include": ["src/**/*.d.ts", "src/**/*.ts"] +} diff --git a/cockpit/chat/messages/angular/tsconfig.json b/cockpit/chat/messages/angular/tsconfig.json new file mode 100644 index 000000000..3fd970371 --- /dev/null +++ b/cockpit/chat/messages/angular/tsconfig.json @@ -0,0 +1,24 @@ +{ + "extends": "../../../../tsconfig.base.json", + "compilerOptions": { + "noPropertyAccessFromIndexSignature": false, + "experimentalDecorators": true, + "module": "preserve", + "emitDeclarationOnly": false, + "composite": false, + "lib": ["es2022", "dom"], + "skipLibCheck": true, + "strict": false + }, + "angularCompilerOptions": { + "enableI18nLegacyMessageIdFormat": false, + "strictInjectionParameters": false, + "strictInputAccessModifiers": false, + "strictTemplates": false + }, + "files": [], + "include": [], + "references": [ + { "path": "./tsconfig.app.json" } + ] +} diff --git a/cockpit/chat/messages/python/docs/guide.md b/cockpit/chat/messages/python/docs/guide.md new file mode 100644 index 000000000..a9211afea --- /dev/null +++ b/cockpit/chat/messages/python/docs/guide.md @@ -0,0 +1,75 @@ +# Chat Messages with @cacheplane/chat + + +Render chat messages using the primitive components ChatMessagesComponent, +ChatInputComponent, and ChatTypingIndicatorComponent. These building blocks +give full control over message layout, input handling, and loading states. + + + +Build a chat interface using the individual message primitives from +`@cacheplane/chat`. Import `ChatMessagesComponent`, `ChatInputComponent`, +and `ChatTypingIndicatorComponent` separately instead of the composed +`ChatComponent`. + + + + + +Import the individual chat primitives instead of the composed `ChatComponent`: + +```typescript +import { + ChatMessagesComponent, + ChatInputComponent, + ChatTypingIndicatorComponent, +} from '@cacheplane/chat'; +``` + + + + +Use `ChatMessagesComponent` to display the conversation history: + +```html + +``` + +The component renders human and AI messages with appropriate styling +and supports streaming token display. + + + + +Place `ChatInputComponent` and `ChatTypingIndicatorComponent` below +the messages: + +```html + + +``` + + + + +Create a `submitMessage()` method that sends user input to the stream: + +```typescript +submitMessage(content: string) { + this.stream.submit([{ role: 'human', content }]); +} +``` + + + + +Override default message templates using content projection or custom +renderers for specialized message displays like cards or rich media. + + + + + +Using primitives instead of the composed ChatComponent gives you full +control over layout, spacing, and intermediate UI between messages. + diff --git a/cockpit/chat/messages/python/langgraph.json b/cockpit/chat/messages/python/langgraph.json new file mode 100644 index 000000000..52012ae73 --- /dev/null +++ b/cockpit/chat/messages/python/langgraph.json @@ -0,0 +1,8 @@ +{ + "graphs": { + "c-messages": "./src/graph.py:graph" + }, + "dependencies": ["."], + "python_version": "3.12", + "env": ".env" +} diff --git a/cockpit/chat/messages/python/prompts/messages.md b/cockpit/chat/messages/python/prompts/messages.md new file mode 100644 index 000000000..5553c7818 --- /dev/null +++ b/cockpit/chat/messages/python/prompts/messages.md @@ -0,0 +1,12 @@ +# Chat Messages Assistant + +You are an assistant that demonstrates the chat message primitives from @cacheplane/chat. + +Your role is to showcase different message types and rendering styles. +Use varied response formats including short answers, longer explanations, +bulleted lists, and code snippets to demonstrate how ChatMessagesComponent +renders different content. + +When greeting the user, explain that this demo showcases ChatMessagesComponent, +ChatInputComponent, and ChatTypingIndicatorComponent working together as +individual primitives rather than the composed ChatComponent. diff --git a/cockpit/chat/messages/python/pyproject.toml b/cockpit/chat/messages/python/pyproject.toml new file mode 100644 index 000000000..5b9755900 --- /dev/null +++ b/cockpit/chat/messages/python/pyproject.toml @@ -0,0 +1,21 @@ +[project] +name = "cockpit-chat-messages" +version = "0.1.0" +requires-python = ">=3.12" +dependencies = [ + "langgraph>=0.3", + "langchain-openai>=0.3", + "langsmith>=0.2", +] + +[tool.uv] +dev-dependencies = [ + "langgraph-cli[inmem]>=0.1", +] + +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[tool.hatch.build.targets.wheel] +packages = ["src"] diff --git a/cockpit/chat/messages/python/src/graph.py b/cockpit/chat/messages/python/src/graph.py new file mode 100644 index 000000000..0b9f3df25 --- /dev/null +++ b/cockpit/chat/messages/python/src/graph.py @@ -0,0 +1,39 @@ +""" +Chat Messages Graph + +A simple conversational agent that demonstrates message rendering +with different message types (human, AI, system). +""" + +from pathlib import Path +from langgraph.graph import StateGraph, MessagesState, END +from langchain_openai import ChatOpenAI +from langchain_core.messages import SystemMessage + +PROMPTS_DIR = Path(__file__).parent.parent / "prompts" + + +def build_messages_graph(): + """ + Constructs a conversational graph that demonstrates message rendering. + + The agent responds with various message styles to showcase + ChatMessagesComponent, ChatInputComponent, and ChatTypingIndicatorComponent. + """ + llm = ChatOpenAI(model="gpt-5-mini", streaming=True) + + async def generate(state: MessagesState) -> dict: + system_prompt = (PROMPTS_DIR / "messages.md").read_text() + messages = [SystemMessage(content=system_prompt)] + state["messages"] + response = await llm.ainvoke(messages) + return {"messages": [response]} + + graph = StateGraph(MessagesState) + graph.add_node("generate", generate) + graph.set_entry_point("generate") + graph.add_edge("generate", END) + + return graph.compile() + + +graph = build_messages_graph() diff --git a/cockpit/chat/messages/python/src/index.ts b/cockpit/chat/messages/python/src/index.ts new file mode 100644 index 000000000..6336aeb91 --- /dev/null +++ b/cockpit/chat/messages/python/src/index.ts @@ -0,0 +1,42 @@ +export interface CockpitCapabilityModule { + id: string; + manifestIdentity: { + product: 'chat'; + section: 'core-capabilities'; + topic: 'messages'; + page: 'overview'; + language: 'python'; + }; + title: string; + docsPath: string; + promptAssetPaths: string[]; + codeAssetPaths: string[]; + backendAssetPaths: string[]; + docsAssetPaths: string[]; + runtimeUrl?: string; + devPort?: number; +} + +export const chatMessagesPythonModule: CockpitCapabilityModule = { + id: 'chat-messages-python', + manifestIdentity: { + product: 'chat', + section: 'core-capabilities', + topic: 'messages', + page: 'overview', + language: 'python', + }, + title: 'Chat Messages (Python)', + docsPath: '/docs/chat/core-capabilities/messages/overview/python', + promptAssetPaths: ['cockpit/chat/messages/python/prompts/messages.md'], + codeAssetPaths: [ + 'cockpit/chat/messages/angular/src/app/messages.component.ts', + 'cockpit/chat/messages/angular/src/app/app.config.ts', + ], + backendAssetPaths: [ + 'cockpit/chat/messages/python/src/graph.py', + ], + docsAssetPaths: ['cockpit/chat/messages/python/docs/guide.md'], + runtimeUrl: 'chat/messages', + devPort: 4501, +}; From 5f7c46277378732b618fbbbac01dd7c99a1a4417 Mon Sep 17 00:00:00 2001 From: Brian Love Date: Wed, 8 Apr 2026 10:14:22 -0700 Subject: [PATCH 08/48] feat(cockpit): add chat/input capability Adds the input capability showcasing ChatInputComponent with keyboard handling, disabled state, custom placeholder, and loading indicators. Co-Authored-By: Claude Opus 4.6 (1M context) --- cockpit/chat/input/angular/e2e/input.spec.ts | 19 +++++ cockpit/chat/input/angular/project.json | 61 ++++++++++++++++ cockpit/chat/input/angular/proxy.conf.json | 9 +++ .../chat/input/angular/src/app/app.config.ts | 12 ++++ .../input/angular/src/app/input.component.ts | 66 +++++++++++++++++ .../environments/environment.development.ts | 5 ++ .../angular/src/environments/environment.ts | 5 ++ cockpit/chat/input/angular/src/index.html | 13 ++++ cockpit/chat/input/angular/src/index.ts | 29 ++++++++ cockpit/chat/input/angular/src/main.ts | 6 ++ cockpit/chat/input/angular/src/styles.css | 1 + cockpit/chat/input/angular/tsconfig.app.json | 9 +++ cockpit/chat/input/angular/tsconfig.json | 24 +++++++ cockpit/chat/input/python/docs/guide.md | 72 +++++++++++++++++++ cockpit/chat/input/python/langgraph.json | 8 +++ cockpit/chat/input/python/prompts/input.md | 11 +++ cockpit/chat/input/python/pyproject.toml | 21 ++++++ cockpit/chat/input/python/src/graph.py | 37 ++++++++++ cockpit/chat/input/python/src/index.ts | 42 +++++++++++ 19 files changed, 450 insertions(+) create mode 100644 cockpit/chat/input/angular/e2e/input.spec.ts create mode 100644 cockpit/chat/input/angular/project.json create mode 100644 cockpit/chat/input/angular/proxy.conf.json create mode 100644 cockpit/chat/input/angular/src/app/app.config.ts create mode 100644 cockpit/chat/input/angular/src/app/input.component.ts create mode 100644 cockpit/chat/input/angular/src/environments/environment.development.ts create mode 100644 cockpit/chat/input/angular/src/environments/environment.ts create mode 100644 cockpit/chat/input/angular/src/index.html create mode 100644 cockpit/chat/input/angular/src/index.ts create mode 100644 cockpit/chat/input/angular/src/main.ts create mode 100644 cockpit/chat/input/angular/src/styles.css create mode 100644 cockpit/chat/input/angular/tsconfig.app.json create mode 100644 cockpit/chat/input/angular/tsconfig.json create mode 100644 cockpit/chat/input/python/docs/guide.md create mode 100644 cockpit/chat/input/python/langgraph.json create mode 100644 cockpit/chat/input/python/prompts/input.md create mode 100644 cockpit/chat/input/python/pyproject.toml create mode 100644 cockpit/chat/input/python/src/graph.py create mode 100644 cockpit/chat/input/python/src/index.ts diff --git a/cockpit/chat/input/angular/e2e/input.spec.ts b/cockpit/chat/input/angular/e2e/input.spec.ts new file mode 100644 index 000000000..6d4430ca9 --- /dev/null +++ b/cockpit/chat/input/angular/e2e/input.spec.ts @@ -0,0 +1,19 @@ +import { expect, test } from '@playwright/test'; + +test.describe('Chat Input Example', () => { + test.beforeEach(async ({ page }) => { + await page.goto('http://localhost:4502'); + await page.waitForSelector('app-input', { state: 'attached' }); + }); + + test('renders the chat input interface with state sidebar', async ({ page }) => { + await expect(page.locator('chat-input')).toBeVisible(); + await expect(page.locator('aside')).toBeVisible(); + await expect(page.locator('aside h3')).toHaveText('Input State'); + }); + + test('displays the features list', async ({ page }) => { + await expect(page.locator('aside')).toContainText('Custom placeholder text'); + await expect(page.locator('aside')).toContainText('Enter to send'); + }); +}); diff --git a/cockpit/chat/input/angular/project.json b/cockpit/chat/input/angular/project.json new file mode 100644 index 000000000..2d77364dc --- /dev/null +++ b/cockpit/chat/input/angular/project.json @@ -0,0 +1,61 @@ +{ + "name": "cockpit-chat-input-angular", + "$schema": "../../../../node_modules/nx/schemas/project-schema.json", + "sourceRoot": "cockpit/chat/input/angular/src", + "projectType": "application", + "targets": { + "build": { + "executor": "@angular/build:application", + "outputs": ["{options.outputPath.base}"], + "options": { + "outputPath": { + "base": "dist/cockpit/chat/input/angular", + "browser": "" + }, + "browser": "cockpit/chat/input/angular/src/main.ts", + "tsConfig": "cockpit/chat/input/angular/tsconfig.app.json", + "styles": ["cockpit/chat/input/angular/src/styles.css"] + }, + "configurations": { + "production": { + "budgets": [ + { "type": "initial", "maximumWarning": "500kb", "maximumError": "1mb" }, + { "type": "anyComponentStyle", "maximumWarning": "4kb", "maximumError": "8kb" } + ], + "outputHashing": "none" + }, + "development": { + "optimization": false, + "extractLicenses": false, + "sourceMap": true, + "fileReplacements": [ + { + "replace": "cockpit/chat/input/angular/src/environments/environment.ts", + "with": "cockpit/chat/input/angular/src/environments/environment.development.ts" + } + ] + } + }, + "defaultConfiguration": "production" + }, + "serve": { + "continuous": true, + "executor": "@angular/build:dev-server", + "configurations": { + "production": { "buildTarget": "cockpit-chat-input-angular:build:production" }, + "development": { "buildTarget": "cockpit-chat-input-angular:build:development" } + }, + "defaultConfiguration": "development", + "options": { + "proxyConfig": "cockpit/chat/input/angular/proxy.conf.json" + } + }, + "smoke": { + "executor": "nx:run-commands", + "options": { + "cwd": "cockpit/chat/input/angular", + "command": "npx tsx -e \"import { chatInputAngularModule } from './src/index.ts'; const module = chatInputAngularModule; if (module.id !== 'chat-input-angular' || module.title !== 'Chat Input (Angular)') { throw new Error('Unexpected module shape for ' + module.id); }\"" + } + } + } +} diff --git a/cockpit/chat/input/angular/proxy.conf.json b/cockpit/chat/input/angular/proxy.conf.json new file mode 100644 index 000000000..8523362d7 --- /dev/null +++ b/cockpit/chat/input/angular/proxy.conf.json @@ -0,0 +1,9 @@ +{ + "/api": { + "target": "http://localhost:8123", + "secure": false, + "changeOrigin": true, + "pathRewrite": { "^/api": "" }, + "ws": true + } +} diff --git a/cockpit/chat/input/angular/src/app/app.config.ts b/cockpit/chat/input/angular/src/app/app.config.ts new file mode 100644 index 000000000..b52a916ac --- /dev/null +++ b/cockpit/chat/input/angular/src/app/app.config.ts @@ -0,0 +1,12 @@ +// SPDX-License-Identifier: PolyForm-Noncommercial-1.0.0 +import { ApplicationConfig } from '@angular/core'; +import { provideStreamResource } from '@cacheplane/stream-resource'; +import { provideChat } from '@cacheplane/chat'; +import { environment } from '../environments/environment'; + +export const appConfig: ApplicationConfig = { + providers: [ + provideStreamResource({ apiUrl: environment.langGraphApiUrl }), + provideChat({}), + ], +}; diff --git a/cockpit/chat/input/angular/src/app/input.component.ts b/cockpit/chat/input/angular/src/app/input.component.ts new file mode 100644 index 000000000..a1de6fe0d --- /dev/null +++ b/cockpit/chat/input/angular/src/app/input.component.ts @@ -0,0 +1,66 @@ +// SPDX-License-Identifier: PolyForm-Noncommercial-1.0.0 +import { Component, computed } from '@angular/core'; +import { ChatInputComponent as ChatInputPrimitive } from '@cacheplane/chat'; +import { ChatMessagesComponent } from '@cacheplane/chat'; +import { streamResource } from '@cacheplane/stream-resource'; +import { environment } from '../environments/environment'; + +/** + * InputComponent showcases ChatInputComponent features including + * keyboard handling, disabled state, and custom placeholder. + * A sidebar displays the current input state. + */ +@Component({ + selector: 'app-input', + standalone: true, + imports: [ChatInputPrimitive, ChatMessagesComponent], + template: ` +
+
+
+

Chat Input Demo

+
+
+ +
+
+ +
+
+ +
+ `, +}) +export class InputComponent { + protected readonly stream = streamResource({ + apiUrl: environment.langGraphApiUrl, + assistantId: environment.streamingAssistantId, + }); + + protected readonly streamStatus = computed(() => this.stream.status()); + protected readonly isLoading = computed(() => this.stream.status() === 'streaming'); + + submitMessage(content: string) { + this.stream.submit([{ role: 'human', content }]); + } +} diff --git a/cockpit/chat/input/angular/src/environments/environment.development.ts b/cockpit/chat/input/angular/src/environments/environment.development.ts new file mode 100644 index 000000000..05aa413c0 --- /dev/null +++ b/cockpit/chat/input/angular/src/environments/environment.development.ts @@ -0,0 +1,5 @@ +export const environment = { + production: false, + langGraphApiUrl: 'http://localhost:4502/api', + streamingAssistantId: 'c-input', +}; diff --git a/cockpit/chat/input/angular/src/environments/environment.ts b/cockpit/chat/input/angular/src/environments/environment.ts new file mode 100644 index 000000000..67680b9f1 --- /dev/null +++ b/cockpit/chat/input/angular/src/environments/environment.ts @@ -0,0 +1,5 @@ +export const environment = { + production: true, + langGraphApiUrl: '/api', + streamingAssistantId: 'c-input', +}; diff --git a/cockpit/chat/input/angular/src/index.html b/cockpit/chat/input/angular/src/index.html new file mode 100644 index 000000000..a09f29255 --- /dev/null +++ b/cockpit/chat/input/angular/src/index.html @@ -0,0 +1,13 @@ + + + + + Chat Input — Angular + + + + + + + + diff --git a/cockpit/chat/input/angular/src/index.ts b/cockpit/chat/input/angular/src/index.ts new file mode 100644 index 000000000..9ae28ba28 --- /dev/null +++ b/cockpit/chat/input/angular/src/index.ts @@ -0,0 +1,29 @@ +export interface CockpitCapabilityModule { + id: string; + manifestIdentity: { + product: 'chat'; + section: 'core-capabilities'; + topic: 'input'; + page: 'overview'; + language: 'angular'; + }; + title: string; + docsPath: string; + promptAssetPaths: string[]; + codeAssetPaths: string[]; +} + +export const chatInputAngularModule: CockpitCapabilityModule = { + id: 'chat-input-angular', + manifestIdentity: { + product: 'chat', + section: 'core-capabilities', + topic: 'input', + page: 'overview', + language: 'angular', + }, + title: 'Chat Input (Angular)', + docsPath: '/docs/chat/core-capabilities/input/overview/angular', + promptAssetPaths: ['cockpit/chat/input/python/prompts/input.md'], + codeAssetPaths: ['cockpit/chat/input/angular/src/app/input.component.ts'], +}; diff --git a/cockpit/chat/input/angular/src/main.ts b/cockpit/chat/input/angular/src/main.ts new file mode 100644 index 000000000..c923d9a0a --- /dev/null +++ b/cockpit/chat/input/angular/src/main.ts @@ -0,0 +1,6 @@ +// SPDX-License-Identifier: PolyForm-Noncommercial-1.0.0 +import { bootstrapApplication } from '@angular/platform-browser'; +import { appConfig } from './app/app.config'; +import { InputComponent } from './app/input.component'; + +bootstrapApplication(InputComponent, appConfig).catch(console.error); diff --git a/cockpit/chat/input/angular/src/styles.css b/cockpit/chat/input/angular/src/styles.css new file mode 100644 index 000000000..d4e83d3e7 --- /dev/null +++ b/cockpit/chat/input/angular/src/styles.css @@ -0,0 +1 @@ +/* Global styles for the input capability demo */ diff --git a/cockpit/chat/input/angular/tsconfig.app.json b/cockpit/chat/input/angular/tsconfig.app.json new file mode 100644 index 000000000..72c01e364 --- /dev/null +++ b/cockpit/chat/input/angular/tsconfig.app.json @@ -0,0 +1,9 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../../../dist/out-tsc", + "types": [] + }, + "files": ["src/main.ts"], + "include": ["src/**/*.d.ts", "src/**/*.ts"] +} diff --git a/cockpit/chat/input/angular/tsconfig.json b/cockpit/chat/input/angular/tsconfig.json new file mode 100644 index 000000000..3fd970371 --- /dev/null +++ b/cockpit/chat/input/angular/tsconfig.json @@ -0,0 +1,24 @@ +{ + "extends": "../../../../tsconfig.base.json", + "compilerOptions": { + "noPropertyAccessFromIndexSignature": false, + "experimentalDecorators": true, + "module": "preserve", + "emitDeclarationOnly": false, + "composite": false, + "lib": ["es2022", "dom"], + "skipLibCheck": true, + "strict": false + }, + "angularCompilerOptions": { + "enableI18nLegacyMessageIdFormat": false, + "strictInjectionParameters": false, + "strictInputAccessModifiers": false, + "strictTemplates": false + }, + "files": [], + "include": [], + "references": [ + { "path": "./tsconfig.app.json" } + ] +} diff --git a/cockpit/chat/input/python/docs/guide.md b/cockpit/chat/input/python/docs/guide.md new file mode 100644 index 000000000..66358d6c4 --- /dev/null +++ b/cockpit/chat/input/python/docs/guide.md @@ -0,0 +1,72 @@ +# Chat Input with @cacheplane/chat + + +Configure and customize the ChatInputComponent for handling user input +in chat interfaces. Supports keyboard shortcuts, placeholder text, +disabled states, and loading indicators. + + + +Add a customized chat input to your Angular component using `ChatInputComponent` +from `@cacheplane/chat`. Configure placeholder text, keyboard handling, +and loading state integration. + + + + + +Import the input component from the chat library: + +```typescript +import { ChatInputComponent } from '@cacheplane/chat'; +``` + + + + +Set a custom placeholder via the component input: + +```html + +``` + + + + +ChatInputComponent supports Enter to send and Shift+Enter for newlines +out of the box. Listen for the `send` event: + +```html + +``` + + + + +The input automatically disables while the stream is active. Access +loading state via the stream resource: + +```typescript +protected readonly isLoading = computed(() => this.stream.status() === 'streaming'); +``` + + + + +Customize input appearance using CSS custom properties: + +```css +chat-input { + --chat-input-bg: #1a1a2e; + --chat-input-border: #333; + --chat-input-text: #e0e0e0; +} +``` + + + + + +The input component auto-focuses when the stream completes, keeping the +conversation flow smooth for the user. + diff --git a/cockpit/chat/input/python/langgraph.json b/cockpit/chat/input/python/langgraph.json new file mode 100644 index 000000000..60f9e23dc --- /dev/null +++ b/cockpit/chat/input/python/langgraph.json @@ -0,0 +1,8 @@ +{ + "graphs": { + "c-input": "./src/graph.py:graph" + }, + "dependencies": ["."], + "python_version": "3.12", + "env": ".env" +} diff --git a/cockpit/chat/input/python/prompts/input.md b/cockpit/chat/input/python/prompts/input.md new file mode 100644 index 000000000..ed2b23926 --- /dev/null +++ b/cockpit/chat/input/python/prompts/input.md @@ -0,0 +1,11 @@ +# Chat Input Assistant + +You are an assistant that demonstrates the ChatInputComponent from @cacheplane/chat. + +Echo back what the user says, and explain the input features being demonstrated: +- Custom placeholder text +- Keyboard handling (Enter to send, Shift+Enter for newline) +- Disabled state while the agent is responding +- Loading indicator integration + +Keep responses concise to showcase the streaming and input state transitions. diff --git a/cockpit/chat/input/python/pyproject.toml b/cockpit/chat/input/python/pyproject.toml new file mode 100644 index 000000000..462382334 --- /dev/null +++ b/cockpit/chat/input/python/pyproject.toml @@ -0,0 +1,21 @@ +[project] +name = "cockpit-chat-input" +version = "0.1.0" +requires-python = ">=3.12" +dependencies = [ + "langgraph>=0.3", + "langchain-openai>=0.3", + "langsmith>=0.2", +] + +[tool.uv] +dev-dependencies = [ + "langgraph-cli[inmem]>=0.1", +] + +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[tool.hatch.build.targets.wheel] +packages = ["src"] diff --git a/cockpit/chat/input/python/src/graph.py b/cockpit/chat/input/python/src/graph.py new file mode 100644 index 000000000..27bbbb21f --- /dev/null +++ b/cockpit/chat/input/python/src/graph.py @@ -0,0 +1,37 @@ +""" +Chat Input Graph + +An echo agent with streaming that demonstrates ChatInputComponent features +including keyboard handling, disabled state, and custom placeholder. +""" + +from pathlib import Path +from langgraph.graph import StateGraph, MessagesState, END +from langchain_openai import ChatOpenAI +from langchain_core.messages import SystemMessage + +PROMPTS_DIR = Path(__file__).parent.parent / "prompts" + + +def build_input_graph(): + """ + Constructs an echo agent that streams responses back, + demonstrating ChatInputComponent features. + """ + llm = ChatOpenAI(model="gpt-5-mini", streaming=True) + + async def generate(state: MessagesState) -> dict: + system_prompt = (PROMPTS_DIR / "input.md").read_text() + messages = [SystemMessage(content=system_prompt)] + state["messages"] + response = await llm.ainvoke(messages) + return {"messages": [response]} + + graph = StateGraph(MessagesState) + graph.add_node("generate", generate) + graph.set_entry_point("generate") + graph.add_edge("generate", END) + + return graph.compile() + + +graph = build_input_graph() diff --git a/cockpit/chat/input/python/src/index.ts b/cockpit/chat/input/python/src/index.ts new file mode 100644 index 000000000..1dc6b7873 --- /dev/null +++ b/cockpit/chat/input/python/src/index.ts @@ -0,0 +1,42 @@ +export interface CockpitCapabilityModule { + id: string; + manifestIdentity: { + product: 'chat'; + section: 'core-capabilities'; + topic: 'input'; + page: 'overview'; + language: 'python'; + }; + title: string; + docsPath: string; + promptAssetPaths: string[]; + codeAssetPaths: string[]; + backendAssetPaths: string[]; + docsAssetPaths: string[]; + runtimeUrl?: string; + devPort?: number; +} + +export const chatInputPythonModule: CockpitCapabilityModule = { + id: 'chat-input-python', + manifestIdentity: { + product: 'chat', + section: 'core-capabilities', + topic: 'input', + page: 'overview', + language: 'python', + }, + title: 'Chat Input (Python)', + docsPath: '/docs/chat/core-capabilities/input/overview/python', + promptAssetPaths: ['cockpit/chat/input/python/prompts/input.md'], + codeAssetPaths: [ + 'cockpit/chat/input/angular/src/app/input.component.ts', + 'cockpit/chat/input/angular/src/app/app.config.ts', + ], + backendAssetPaths: [ + 'cockpit/chat/input/python/src/graph.py', + ], + docsAssetPaths: ['cockpit/chat/input/python/docs/guide.md'], + runtimeUrl: 'chat/input', + devPort: 4502, +}; From 23109f267030c5a32918ac39f80d89a22d127c3c Mon Sep 17 00:00:00 2001 From: Brian Love Date: Wed, 8 Apr 2026 10:15:58 -0700 Subject: [PATCH 09/48] feat(cockpit): add chat/interrupts capability Adds the interrupts capability demonstrating human-in-the-loop approval gates using LangGraph interrupt() and ChatInterruptPanelComponent. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../interrupts/angular/e2e/interrupts.spec.ts | 18 +++++ cockpit/chat/interrupts/angular/project.json | 61 ++++++++++++++ .../chat/interrupts/angular/proxy.conf.json | 9 +++ .../interrupts/angular/src/app/app.config.ts | 12 +++ .../angular/src/app/interrupts.component.ts | 42 ++++++++++ .../environments/environment.development.ts | 5 ++ .../angular/src/environments/environment.ts | 5 ++ .../chat/interrupts/angular/src/index.html | 13 +++ cockpit/chat/interrupts/angular/src/index.ts | 29 +++++++ cockpit/chat/interrupts/angular/src/main.ts | 6 ++ .../chat/interrupts/angular/src/styles.css | 1 + .../chat/interrupts/angular/tsconfig.app.json | 9 +++ cockpit/chat/interrupts/angular/tsconfig.json | 24 ++++++ cockpit/chat/interrupts/python/docs/guide.md | 81 +++++++++++++++++++ cockpit/chat/interrupts/python/langgraph.json | 8 ++ .../interrupts/python/prompts/interrupts.md | 12 +++ cockpit/chat/interrupts/python/pyproject.toml | 21 +++++ cockpit/chat/interrupts/python/src/graph.py | 50 ++++++++++++ cockpit/chat/interrupts/python/src/index.ts | 42 ++++++++++ 19 files changed, 448 insertions(+) create mode 100644 cockpit/chat/interrupts/angular/e2e/interrupts.spec.ts create mode 100644 cockpit/chat/interrupts/angular/project.json create mode 100644 cockpit/chat/interrupts/angular/proxy.conf.json create mode 100644 cockpit/chat/interrupts/angular/src/app/app.config.ts create mode 100644 cockpit/chat/interrupts/angular/src/app/interrupts.component.ts create mode 100644 cockpit/chat/interrupts/angular/src/environments/environment.development.ts create mode 100644 cockpit/chat/interrupts/angular/src/environments/environment.ts create mode 100644 cockpit/chat/interrupts/angular/src/index.html create mode 100644 cockpit/chat/interrupts/angular/src/index.ts create mode 100644 cockpit/chat/interrupts/angular/src/main.ts create mode 100644 cockpit/chat/interrupts/angular/src/styles.css create mode 100644 cockpit/chat/interrupts/angular/tsconfig.app.json create mode 100644 cockpit/chat/interrupts/angular/tsconfig.json create mode 100644 cockpit/chat/interrupts/python/docs/guide.md create mode 100644 cockpit/chat/interrupts/python/langgraph.json create mode 100644 cockpit/chat/interrupts/python/prompts/interrupts.md create mode 100644 cockpit/chat/interrupts/python/pyproject.toml create mode 100644 cockpit/chat/interrupts/python/src/graph.py create mode 100644 cockpit/chat/interrupts/python/src/index.ts diff --git a/cockpit/chat/interrupts/angular/e2e/interrupts.spec.ts b/cockpit/chat/interrupts/angular/e2e/interrupts.spec.ts new file mode 100644 index 000000000..4f84d0472 --- /dev/null +++ b/cockpit/chat/interrupts/angular/e2e/interrupts.spec.ts @@ -0,0 +1,18 @@ +import { expect, test } from '@playwright/test'; + +test.describe('Chat Interrupts Example', () => { + test.beforeEach(async ({ page }) => { + await page.goto('http://localhost:4503'); + await page.waitForSelector('app-interrupts', { state: 'attached' }); + }); + + test('renders the chat interface with interrupt panel sidebar', async ({ page }) => { + await expect(page.locator('chat')).toBeVisible(); + await expect(page.locator('aside')).toBeVisible(); + await expect(page.locator('aside h3')).toHaveText('Interrupt Panel'); + }); + + test('displays the stream status', async ({ page }) => { + await expect(page.locator('aside')).toContainText('Stream Status'); + }); +}); diff --git a/cockpit/chat/interrupts/angular/project.json b/cockpit/chat/interrupts/angular/project.json new file mode 100644 index 000000000..4393871d4 --- /dev/null +++ b/cockpit/chat/interrupts/angular/project.json @@ -0,0 +1,61 @@ +{ + "name": "cockpit-chat-interrupts-angular", + "$schema": "../../../../node_modules/nx/schemas/project-schema.json", + "sourceRoot": "cockpit/chat/interrupts/angular/src", + "projectType": "application", + "targets": { + "build": { + "executor": "@angular/build:application", + "outputs": ["{options.outputPath.base}"], + "options": { + "outputPath": { + "base": "dist/cockpit/chat/interrupts/angular", + "browser": "" + }, + "browser": "cockpit/chat/interrupts/angular/src/main.ts", + "tsConfig": "cockpit/chat/interrupts/angular/tsconfig.app.json", + "styles": ["cockpit/chat/interrupts/angular/src/styles.css"] + }, + "configurations": { + "production": { + "budgets": [ + { "type": "initial", "maximumWarning": "500kb", "maximumError": "1mb" }, + { "type": "anyComponentStyle", "maximumWarning": "4kb", "maximumError": "8kb" } + ], + "outputHashing": "none" + }, + "development": { + "optimization": false, + "extractLicenses": false, + "sourceMap": true, + "fileReplacements": [ + { + "replace": "cockpit/chat/interrupts/angular/src/environments/environment.ts", + "with": "cockpit/chat/interrupts/angular/src/environments/environment.development.ts" + } + ] + } + }, + "defaultConfiguration": "production" + }, + "serve": { + "continuous": true, + "executor": "@angular/build:dev-server", + "configurations": { + "production": { "buildTarget": "cockpit-chat-interrupts-angular:build:production" }, + "development": { "buildTarget": "cockpit-chat-interrupts-angular:build:development" } + }, + "defaultConfiguration": "development", + "options": { + "proxyConfig": "cockpit/chat/interrupts/angular/proxy.conf.json" + } + }, + "smoke": { + "executor": "nx:run-commands", + "options": { + "cwd": "cockpit/chat/interrupts/angular", + "command": "npx tsx -e \"import { chatInterruptsAngularModule } from './src/index.ts'; const module = chatInterruptsAngularModule; if (module.id !== 'chat-interrupts-angular' || module.title !== 'Chat Interrupts (Angular)') { throw new Error('Unexpected module shape for ' + module.id); }\"" + } + } + } +} diff --git a/cockpit/chat/interrupts/angular/proxy.conf.json b/cockpit/chat/interrupts/angular/proxy.conf.json new file mode 100644 index 000000000..8523362d7 --- /dev/null +++ b/cockpit/chat/interrupts/angular/proxy.conf.json @@ -0,0 +1,9 @@ +{ + "/api": { + "target": "http://localhost:8123", + "secure": false, + "changeOrigin": true, + "pathRewrite": { "^/api": "" }, + "ws": true + } +} diff --git a/cockpit/chat/interrupts/angular/src/app/app.config.ts b/cockpit/chat/interrupts/angular/src/app/app.config.ts new file mode 100644 index 000000000..b52a916ac --- /dev/null +++ b/cockpit/chat/interrupts/angular/src/app/app.config.ts @@ -0,0 +1,12 @@ +// SPDX-License-Identifier: PolyForm-Noncommercial-1.0.0 +import { ApplicationConfig } from '@angular/core'; +import { provideStreamResource } from '@cacheplane/stream-resource'; +import { provideChat } from '@cacheplane/chat'; +import { environment } from '../environments/environment'; + +export const appConfig: ApplicationConfig = { + providers: [ + provideStreamResource({ apiUrl: environment.langGraphApiUrl }), + provideChat({}), + ], +}; diff --git a/cockpit/chat/interrupts/angular/src/app/interrupts.component.ts b/cockpit/chat/interrupts/angular/src/app/interrupts.component.ts new file mode 100644 index 000000000..93e3f3b94 --- /dev/null +++ b/cockpit/chat/interrupts/angular/src/app/interrupts.component.ts @@ -0,0 +1,42 @@ +// SPDX-License-Identifier: PolyForm-Noncommercial-1.0.0 +import { Component, computed } from '@angular/core'; +import { JsonPipe } from '@angular/common'; +import { ChatComponent, ChatInterruptPanelComponent } from '@cacheplane/chat'; +import { streamResource } from '@cacheplane/stream-resource'; +import { environment } from '../environments/environment'; + +/** + * InterruptsComponent demonstrates human-in-the-loop approval gates + * using ChatComponent and ChatInterruptPanelComponent. + * + * Shows interrupt payload and action buttons in a sidebar panel. + */ +@Component({ + selector: 'app-interrupts', + standalone: true, + imports: [ChatComponent, ChatInterruptPanelComponent, JsonPipe], + template: ` +
+ + +
+ `, +}) +export class InterruptsComponent { + protected readonly stream = streamResource({ + apiUrl: environment.langGraphApiUrl, + assistantId: environment.streamingAssistantId, + }); + + protected readonly streamStatus = computed(() => this.stream.status()); +} diff --git a/cockpit/chat/interrupts/angular/src/environments/environment.development.ts b/cockpit/chat/interrupts/angular/src/environments/environment.development.ts new file mode 100644 index 000000000..78c1baa4c --- /dev/null +++ b/cockpit/chat/interrupts/angular/src/environments/environment.development.ts @@ -0,0 +1,5 @@ +export const environment = { + production: false, + langGraphApiUrl: 'http://localhost:4503/api', + streamingAssistantId: 'c-interrupts', +}; diff --git a/cockpit/chat/interrupts/angular/src/environments/environment.ts b/cockpit/chat/interrupts/angular/src/environments/environment.ts new file mode 100644 index 000000000..c50a75ab9 --- /dev/null +++ b/cockpit/chat/interrupts/angular/src/environments/environment.ts @@ -0,0 +1,5 @@ +export const environment = { + production: true, + langGraphApiUrl: '/api', + streamingAssistantId: 'c-interrupts', +}; diff --git a/cockpit/chat/interrupts/angular/src/index.html b/cockpit/chat/interrupts/angular/src/index.html new file mode 100644 index 000000000..d4e08882c --- /dev/null +++ b/cockpit/chat/interrupts/angular/src/index.html @@ -0,0 +1,13 @@ + + + + + Chat Interrupts — Angular + + + + + + + + diff --git a/cockpit/chat/interrupts/angular/src/index.ts b/cockpit/chat/interrupts/angular/src/index.ts new file mode 100644 index 000000000..61561953d --- /dev/null +++ b/cockpit/chat/interrupts/angular/src/index.ts @@ -0,0 +1,29 @@ +export interface CockpitCapabilityModule { + id: string; + manifestIdentity: { + product: 'chat'; + section: 'core-capabilities'; + topic: 'interrupts'; + page: 'overview'; + language: 'angular'; + }; + title: string; + docsPath: string; + promptAssetPaths: string[]; + codeAssetPaths: string[]; +} + +export const chatInterruptsAngularModule: CockpitCapabilityModule = { + id: 'chat-interrupts-angular', + manifestIdentity: { + product: 'chat', + section: 'core-capabilities', + topic: 'interrupts', + page: 'overview', + language: 'angular', + }, + title: 'Chat Interrupts (Angular)', + docsPath: '/docs/chat/core-capabilities/interrupts/overview/angular', + promptAssetPaths: ['cockpit/chat/interrupts/python/prompts/interrupts.md'], + codeAssetPaths: ['cockpit/chat/interrupts/angular/src/app/interrupts.component.ts'], +}; diff --git a/cockpit/chat/interrupts/angular/src/main.ts b/cockpit/chat/interrupts/angular/src/main.ts new file mode 100644 index 000000000..16589a6b1 --- /dev/null +++ b/cockpit/chat/interrupts/angular/src/main.ts @@ -0,0 +1,6 @@ +// SPDX-License-Identifier: PolyForm-Noncommercial-1.0.0 +import { bootstrapApplication } from '@angular/platform-browser'; +import { appConfig } from './app/app.config'; +import { InterruptsComponent } from './app/interrupts.component'; + +bootstrapApplication(InterruptsComponent, appConfig).catch(console.error); diff --git a/cockpit/chat/interrupts/angular/src/styles.css b/cockpit/chat/interrupts/angular/src/styles.css new file mode 100644 index 000000000..0275e37b1 --- /dev/null +++ b/cockpit/chat/interrupts/angular/src/styles.css @@ -0,0 +1 @@ +/* Global styles for the interrupts capability demo */ diff --git a/cockpit/chat/interrupts/angular/tsconfig.app.json b/cockpit/chat/interrupts/angular/tsconfig.app.json new file mode 100644 index 000000000..72c01e364 --- /dev/null +++ b/cockpit/chat/interrupts/angular/tsconfig.app.json @@ -0,0 +1,9 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../../../dist/out-tsc", + "types": [] + }, + "files": ["src/main.ts"], + "include": ["src/**/*.d.ts", "src/**/*.ts"] +} diff --git a/cockpit/chat/interrupts/angular/tsconfig.json b/cockpit/chat/interrupts/angular/tsconfig.json new file mode 100644 index 000000000..3fd970371 --- /dev/null +++ b/cockpit/chat/interrupts/angular/tsconfig.json @@ -0,0 +1,24 @@ +{ + "extends": "../../../../tsconfig.base.json", + "compilerOptions": { + "noPropertyAccessFromIndexSignature": false, + "experimentalDecorators": true, + "module": "preserve", + "emitDeclarationOnly": false, + "composite": false, + "lib": ["es2022", "dom"], + "skipLibCheck": true, + "strict": false + }, + "angularCompilerOptions": { + "enableI18nLegacyMessageIdFormat": false, + "strictInjectionParameters": false, + "strictInputAccessModifiers": false, + "strictTemplates": false + }, + "files": [], + "include": [], + "references": [ + { "path": "./tsconfig.app.json" } + ] +} diff --git a/cockpit/chat/interrupts/python/docs/guide.md b/cockpit/chat/interrupts/python/docs/guide.md new file mode 100644 index 000000000..0323e3201 --- /dev/null +++ b/cockpit/chat/interrupts/python/docs/guide.md @@ -0,0 +1,81 @@ +# Chat Interrupts with @cacheplane/chat + + +Implement human-in-the-loop approval gates using LangGraph interrupts +and ChatInterruptPanelComponent. The graph pauses execution and presents +an approval UI before proceeding. + + + +Add interrupt handling to your chat interface using `ChatInterruptPanelComponent` +from `@cacheplane/chat`. Detect when the stream enters an interrupted state +and render approval/rejection controls. + + + + + +Set up `streamResource()` which automatically detects interrupt states +from the LangGraph backend: + +```typescript +protected readonly stream = streamResource({ + apiUrl: environment.langGraphApiUrl, + assistantId: environment.streamingAssistantId, +}); +``` + + + + +Check the stream status for interrupt events. The stream resource exposes +interrupt data when the graph pauses: + +```typescript +protected readonly isInterrupted = computed( + () => this.stream.status() === 'interrupted' +); +``` + + + + +Use `ChatInterruptPanelComponent` to display the approval UI: + +```html + +``` + +The panel shows the interrupt payload, draft content, and action buttons. + + + + +The interrupt panel emits `approve` and `reject` events. Handle them +to resume or cancel the graph execution: + +```html + +``` + + + + +After approval, resume the graph to continue from the interrupt point: + +```typescript +onApprove() { + this.stream.resume({ action: 'approve' }); +} +``` + + + + + +Interrupts are ideal for sensitive actions like sending emails, making +purchases, or modifying data where human oversight is required. + diff --git a/cockpit/chat/interrupts/python/langgraph.json b/cockpit/chat/interrupts/python/langgraph.json new file mode 100644 index 000000000..59ad81b5c --- /dev/null +++ b/cockpit/chat/interrupts/python/langgraph.json @@ -0,0 +1,8 @@ +{ + "graphs": { + "c-interrupts": "./src/graph.py:graph" + }, + "dependencies": ["."], + "python_version": "3.12", + "env": ".env" +} diff --git a/cockpit/chat/interrupts/python/prompts/interrupts.md b/cockpit/chat/interrupts/python/prompts/interrupts.md new file mode 100644 index 000000000..aa92596a4 --- /dev/null +++ b/cockpit/chat/interrupts/python/prompts/interrupts.md @@ -0,0 +1,12 @@ +# Chat Interrupts Assistant + +You are an assistant that demonstrates human-in-the-loop approval gates +using LangGraph interrupts. + +Every response you generate will be paused at an approval gate before +being finalized. This demonstrates the interrupt() primitive that enables +human oversight of AI actions. + +Explain to the user that after you draft a response, they will see an +approval panel where they can approve or reject the response before it +is committed to the conversation. diff --git a/cockpit/chat/interrupts/python/pyproject.toml b/cockpit/chat/interrupts/python/pyproject.toml new file mode 100644 index 000000000..fd3112de7 --- /dev/null +++ b/cockpit/chat/interrupts/python/pyproject.toml @@ -0,0 +1,21 @@ +[project] +name = "cockpit-chat-interrupts" +version = "0.1.0" +requires-python = ">=3.12" +dependencies = [ + "langgraph>=0.3", + "langchain-openai>=0.3", + "langsmith>=0.2", +] + +[tool.uv] +dev-dependencies = [ + "langgraph-cli[inmem]>=0.1", +] + +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[tool.hatch.build.targets.wheel] +packages = ["src"] diff --git a/cockpit/chat/interrupts/python/src/graph.py b/cockpit/chat/interrupts/python/src/graph.py new file mode 100644 index 000000000..fd3c7d9bc --- /dev/null +++ b/cockpit/chat/interrupts/python/src/graph.py @@ -0,0 +1,50 @@ +""" +Chat Interrupts Graph + +A LangGraph StateGraph that demonstrates human-in-the-loop approval gates +using the interrupt() primitive. The graph generates a response, then pauses +for user approval before proceeding. +""" + +from pathlib import Path +from langgraph.graph import StateGraph, MessagesState, END +from langgraph.types import interrupt +from langchain_openai import ChatOpenAI +from langchain_core.messages import SystemMessage, AIMessage + +PROMPTS_DIR = Path(__file__).parent.parent / "prompts" + + +def build_interrupts_graph(): + """ + Constructs a graph with an approval gate that interrupts execution + and waits for human approval before continuing. + """ + llm = ChatOpenAI(model="gpt-5-mini", streaming=True) + + async def generate(state: MessagesState) -> dict: + system_prompt = (PROMPTS_DIR / "interrupts.md").read_text() + messages = [SystemMessage(content=system_prompt)] + state["messages"] + response = await llm.ainvoke(messages) + return {"messages": [response]} + + async def approval_gate(state: MessagesState) -> dict: + last_message = state["messages"][-1] + result = interrupt({ + "type": "approval", + "message": "The assistant wants to proceed. Do you approve?", + "draft": last_message.content, + }) + return {"messages": [AIMessage(content=f"Approved. Proceeding with: {last_message.content}")]} + + graph = StateGraph(MessagesState) + graph.add_node("generate", generate) + graph.add_node("approval_gate", approval_gate) + graph.set_entry_point("generate") + graph.add_edge("generate", "approval_gate") + graph.add_edge("approval_gate", END) + + return graph.compile() + + +graph = build_interrupts_graph() diff --git a/cockpit/chat/interrupts/python/src/index.ts b/cockpit/chat/interrupts/python/src/index.ts new file mode 100644 index 000000000..52626e00f --- /dev/null +++ b/cockpit/chat/interrupts/python/src/index.ts @@ -0,0 +1,42 @@ +export interface CockpitCapabilityModule { + id: string; + manifestIdentity: { + product: 'chat'; + section: 'core-capabilities'; + topic: 'interrupts'; + page: 'overview'; + language: 'python'; + }; + title: string; + docsPath: string; + promptAssetPaths: string[]; + codeAssetPaths: string[]; + backendAssetPaths: string[]; + docsAssetPaths: string[]; + runtimeUrl?: string; + devPort?: number; +} + +export const chatInterruptsPythonModule: CockpitCapabilityModule = { + id: 'chat-interrupts-python', + manifestIdentity: { + product: 'chat', + section: 'core-capabilities', + topic: 'interrupts', + page: 'overview', + language: 'python', + }, + title: 'Chat Interrupts (Python)', + docsPath: '/docs/chat/core-capabilities/interrupts/overview/python', + promptAssetPaths: ['cockpit/chat/interrupts/python/prompts/interrupts.md'], + codeAssetPaths: [ + 'cockpit/chat/interrupts/angular/src/app/interrupts.component.ts', + 'cockpit/chat/interrupts/angular/src/app/app.config.ts', + ], + backendAssetPaths: [ + 'cockpit/chat/interrupts/python/src/graph.py', + ], + docsAssetPaths: ['cockpit/chat/interrupts/python/docs/guide.md'], + runtimeUrl: 'chat/interrupts', + devPort: 4503, +}; From 3f24731c5d56fd4c051b44c9f2fdeff2b47214f3 Mon Sep 17 00:00:00 2001 From: Brian Love Date: Wed, 8 Apr 2026 10:17:31 -0700 Subject: [PATCH 10/48] feat(cockpit): add chat/tool-calls capability Adds the tool-calls capability with search, calculator, and weather tools demonstrating ChatToolCallsComponent and ChatToolCallCardComponent. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../tool-calls/angular/e2e/tool-calls.spec.ts | 20 +++++ cockpit/chat/tool-calls/angular/project.json | 61 +++++++++++++++ .../chat/tool-calls/angular/proxy.conf.json | 9 +++ .../tool-calls/angular/src/app/app.config.ts | 12 +++ .../angular/src/app/tool-calls.component.ts | 45 +++++++++++ .../environments/environment.development.ts | 5 ++ .../angular/src/environments/environment.ts | 5 ++ .../chat/tool-calls/angular/src/index.html | 13 ++++ cockpit/chat/tool-calls/angular/src/index.ts | 29 +++++++ cockpit/chat/tool-calls/angular/src/main.ts | 6 ++ .../chat/tool-calls/angular/src/styles.css | 1 + .../chat/tool-calls/angular/tsconfig.app.json | 9 +++ cockpit/chat/tool-calls/angular/tsconfig.json | 24 ++++++ cockpit/chat/tool-calls/python/docs/guide.md | 73 ++++++++++++++++++ cockpit/chat/tool-calls/python/langgraph.json | 8 ++ .../tool-calls/python/prompts/tool-calls.md | 13 ++++ cockpit/chat/tool-calls/python/pyproject.toml | 21 ++++++ cockpit/chat/tool-calls/python/src/graph.py | 75 +++++++++++++++++++ cockpit/chat/tool-calls/python/src/index.ts | 42 +++++++++++ 19 files changed, 471 insertions(+) create mode 100644 cockpit/chat/tool-calls/angular/e2e/tool-calls.spec.ts create mode 100644 cockpit/chat/tool-calls/angular/project.json create mode 100644 cockpit/chat/tool-calls/angular/proxy.conf.json create mode 100644 cockpit/chat/tool-calls/angular/src/app/app.config.ts create mode 100644 cockpit/chat/tool-calls/angular/src/app/tool-calls.component.ts create mode 100644 cockpit/chat/tool-calls/angular/src/environments/environment.development.ts create mode 100644 cockpit/chat/tool-calls/angular/src/environments/environment.ts create mode 100644 cockpit/chat/tool-calls/angular/src/index.html create mode 100644 cockpit/chat/tool-calls/angular/src/index.ts create mode 100644 cockpit/chat/tool-calls/angular/src/main.ts create mode 100644 cockpit/chat/tool-calls/angular/src/styles.css create mode 100644 cockpit/chat/tool-calls/angular/tsconfig.app.json create mode 100644 cockpit/chat/tool-calls/angular/tsconfig.json create mode 100644 cockpit/chat/tool-calls/python/docs/guide.md create mode 100644 cockpit/chat/tool-calls/python/langgraph.json create mode 100644 cockpit/chat/tool-calls/python/prompts/tool-calls.md create mode 100644 cockpit/chat/tool-calls/python/pyproject.toml create mode 100644 cockpit/chat/tool-calls/python/src/graph.py create mode 100644 cockpit/chat/tool-calls/python/src/index.ts diff --git a/cockpit/chat/tool-calls/angular/e2e/tool-calls.spec.ts b/cockpit/chat/tool-calls/angular/e2e/tool-calls.spec.ts new file mode 100644 index 000000000..5f33c76f6 --- /dev/null +++ b/cockpit/chat/tool-calls/angular/e2e/tool-calls.spec.ts @@ -0,0 +1,20 @@ +import { expect, test } from '@playwright/test'; + +test.describe('Chat Tool Calls Example', () => { + test.beforeEach(async ({ page }) => { + await page.goto('http://localhost:4504'); + await page.waitForSelector('app-tool-calls', { state: 'attached' }); + }); + + test('renders the chat interface with tool calls sidebar', async ({ page }) => { + await expect(page.locator('chat')).toBeVisible(); + await expect(page.locator('aside')).toBeVisible(); + await expect(page.locator('aside h3')).toHaveText('Tool Calls'); + }); + + test('displays the available tools list', async ({ page }) => { + await expect(page.locator('aside')).toContainText('search'); + await expect(page.locator('aside')).toContainText('calculator'); + await expect(page.locator('aside')).toContainText('weather'); + }); +}); diff --git a/cockpit/chat/tool-calls/angular/project.json b/cockpit/chat/tool-calls/angular/project.json new file mode 100644 index 000000000..533b9f573 --- /dev/null +++ b/cockpit/chat/tool-calls/angular/project.json @@ -0,0 +1,61 @@ +{ + "name": "cockpit-chat-tool-calls-angular", + "$schema": "../../../../node_modules/nx/schemas/project-schema.json", + "sourceRoot": "cockpit/chat/tool-calls/angular/src", + "projectType": "application", + "targets": { + "build": { + "executor": "@angular/build:application", + "outputs": ["{options.outputPath.base}"], + "options": { + "outputPath": { + "base": "dist/cockpit/chat/tool-calls/angular", + "browser": "" + }, + "browser": "cockpit/chat/tool-calls/angular/src/main.ts", + "tsConfig": "cockpit/chat/tool-calls/angular/tsconfig.app.json", + "styles": ["cockpit/chat/tool-calls/angular/src/styles.css"] + }, + "configurations": { + "production": { + "budgets": [ + { "type": "initial", "maximumWarning": "500kb", "maximumError": "1mb" }, + { "type": "anyComponentStyle", "maximumWarning": "4kb", "maximumError": "8kb" } + ], + "outputHashing": "none" + }, + "development": { + "optimization": false, + "extractLicenses": false, + "sourceMap": true, + "fileReplacements": [ + { + "replace": "cockpit/chat/tool-calls/angular/src/environments/environment.ts", + "with": "cockpit/chat/tool-calls/angular/src/environments/environment.development.ts" + } + ] + } + }, + "defaultConfiguration": "production" + }, + "serve": { + "continuous": true, + "executor": "@angular/build:dev-server", + "configurations": { + "production": { "buildTarget": "cockpit-chat-tool-calls-angular:build:production" }, + "development": { "buildTarget": "cockpit-chat-tool-calls-angular:build:development" } + }, + "defaultConfiguration": "development", + "options": { + "proxyConfig": "cockpit/chat/tool-calls/angular/proxy.conf.json" + } + }, + "smoke": { + "executor": "nx:run-commands", + "options": { + "cwd": "cockpit/chat/tool-calls/angular", + "command": "npx tsx -e \"import { chatToolCallsAngularModule } from './src/index.ts'; const module = chatToolCallsAngularModule; if (module.id !== 'chat-tool-calls-angular' || module.title !== 'Chat Tool Calls (Angular)') { throw new Error('Unexpected module shape for ' + module.id); }\"" + } + } + } +} diff --git a/cockpit/chat/tool-calls/angular/proxy.conf.json b/cockpit/chat/tool-calls/angular/proxy.conf.json new file mode 100644 index 000000000..8523362d7 --- /dev/null +++ b/cockpit/chat/tool-calls/angular/proxy.conf.json @@ -0,0 +1,9 @@ +{ + "/api": { + "target": "http://localhost:8123", + "secure": false, + "changeOrigin": true, + "pathRewrite": { "^/api": "" }, + "ws": true + } +} diff --git a/cockpit/chat/tool-calls/angular/src/app/app.config.ts b/cockpit/chat/tool-calls/angular/src/app/app.config.ts new file mode 100644 index 000000000..b52a916ac --- /dev/null +++ b/cockpit/chat/tool-calls/angular/src/app/app.config.ts @@ -0,0 +1,12 @@ +// SPDX-License-Identifier: PolyForm-Noncommercial-1.0.0 +import { ApplicationConfig } from '@angular/core'; +import { provideStreamResource } from '@cacheplane/stream-resource'; +import { provideChat } from '@cacheplane/chat'; +import { environment } from '../environments/environment'; + +export const appConfig: ApplicationConfig = { + providers: [ + provideStreamResource({ apiUrl: environment.langGraphApiUrl }), + provideChat({}), + ], +}; diff --git a/cockpit/chat/tool-calls/angular/src/app/tool-calls.component.ts b/cockpit/chat/tool-calls/angular/src/app/tool-calls.component.ts new file mode 100644 index 000000000..ee94fc5b9 --- /dev/null +++ b/cockpit/chat/tool-calls/angular/src/app/tool-calls.component.ts @@ -0,0 +1,45 @@ +// SPDX-License-Identifier: PolyForm-Noncommercial-1.0.0 +import { Component } from '@angular/core'; +import { + ChatComponent, + ChatToolCallsComponent, + ChatToolCallCardComponent, +} from '@cacheplane/chat'; +import { streamResource } from '@cacheplane/stream-resource'; +import { environment } from '../environments/environment'; + +/** + * ToolCallsComponent demonstrates tool calling with ChatComponent + * and a sidebar showing ChatToolCallsComponent / ChatToolCallCardComponent. + */ +@Component({ + selector: 'app-tool-calls', + standalone: true, + imports: [ChatComponent, ChatToolCallsComponent, ChatToolCallCardComponent], + template: ` +
+ + +
+ `, +}) +export class ToolCallsComponent { + protected readonly stream = streamResource({ + apiUrl: environment.langGraphApiUrl, + assistantId: environment.streamingAssistantId, + }); +} diff --git a/cockpit/chat/tool-calls/angular/src/environments/environment.development.ts b/cockpit/chat/tool-calls/angular/src/environments/environment.development.ts new file mode 100644 index 000000000..7fc09b8c9 --- /dev/null +++ b/cockpit/chat/tool-calls/angular/src/environments/environment.development.ts @@ -0,0 +1,5 @@ +export const environment = { + production: false, + langGraphApiUrl: 'http://localhost:4504/api', + streamingAssistantId: 'c-tool-calls', +}; diff --git a/cockpit/chat/tool-calls/angular/src/environments/environment.ts b/cockpit/chat/tool-calls/angular/src/environments/environment.ts new file mode 100644 index 000000000..6e9080d59 --- /dev/null +++ b/cockpit/chat/tool-calls/angular/src/environments/environment.ts @@ -0,0 +1,5 @@ +export const environment = { + production: true, + langGraphApiUrl: '/api', + streamingAssistantId: 'c-tool-calls', +}; diff --git a/cockpit/chat/tool-calls/angular/src/index.html b/cockpit/chat/tool-calls/angular/src/index.html new file mode 100644 index 000000000..d35828138 --- /dev/null +++ b/cockpit/chat/tool-calls/angular/src/index.html @@ -0,0 +1,13 @@ + + + + + Chat Tool Calls — Angular + + + + + + + + diff --git a/cockpit/chat/tool-calls/angular/src/index.ts b/cockpit/chat/tool-calls/angular/src/index.ts new file mode 100644 index 000000000..4e8cf3185 --- /dev/null +++ b/cockpit/chat/tool-calls/angular/src/index.ts @@ -0,0 +1,29 @@ +export interface CockpitCapabilityModule { + id: string; + manifestIdentity: { + product: 'chat'; + section: 'core-capabilities'; + topic: 'tool-calls'; + page: 'overview'; + language: 'angular'; + }; + title: string; + docsPath: string; + promptAssetPaths: string[]; + codeAssetPaths: string[]; +} + +export const chatToolCallsAngularModule: CockpitCapabilityModule = { + id: 'chat-tool-calls-angular', + manifestIdentity: { + product: 'chat', + section: 'core-capabilities', + topic: 'tool-calls', + page: 'overview', + language: 'angular', + }, + title: 'Chat Tool Calls (Angular)', + docsPath: '/docs/chat/core-capabilities/tool-calls/overview/angular', + promptAssetPaths: ['cockpit/chat/tool-calls/python/prompts/tool-calls.md'], + codeAssetPaths: ['cockpit/chat/tool-calls/angular/src/app/tool-calls.component.ts'], +}; diff --git a/cockpit/chat/tool-calls/angular/src/main.ts b/cockpit/chat/tool-calls/angular/src/main.ts new file mode 100644 index 000000000..ecb623177 --- /dev/null +++ b/cockpit/chat/tool-calls/angular/src/main.ts @@ -0,0 +1,6 @@ +// SPDX-License-Identifier: PolyForm-Noncommercial-1.0.0 +import { bootstrapApplication } from '@angular/platform-browser'; +import { appConfig } from './app/app.config'; +import { ToolCallsComponent } from './app/tool-calls.component'; + +bootstrapApplication(ToolCallsComponent, appConfig).catch(console.error); diff --git a/cockpit/chat/tool-calls/angular/src/styles.css b/cockpit/chat/tool-calls/angular/src/styles.css new file mode 100644 index 000000000..79c4e28fd --- /dev/null +++ b/cockpit/chat/tool-calls/angular/src/styles.css @@ -0,0 +1 @@ +/* Global styles for the tool-calls capability demo */ diff --git a/cockpit/chat/tool-calls/angular/tsconfig.app.json b/cockpit/chat/tool-calls/angular/tsconfig.app.json new file mode 100644 index 000000000..72c01e364 --- /dev/null +++ b/cockpit/chat/tool-calls/angular/tsconfig.app.json @@ -0,0 +1,9 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../../../dist/out-tsc", + "types": [] + }, + "files": ["src/main.ts"], + "include": ["src/**/*.d.ts", "src/**/*.ts"] +} diff --git a/cockpit/chat/tool-calls/angular/tsconfig.json b/cockpit/chat/tool-calls/angular/tsconfig.json new file mode 100644 index 000000000..3fd970371 --- /dev/null +++ b/cockpit/chat/tool-calls/angular/tsconfig.json @@ -0,0 +1,24 @@ +{ + "extends": "../../../../tsconfig.base.json", + "compilerOptions": { + "noPropertyAccessFromIndexSignature": false, + "experimentalDecorators": true, + "module": "preserve", + "emitDeclarationOnly": false, + "composite": false, + "lib": ["es2022", "dom"], + "skipLibCheck": true, + "strict": false + }, + "angularCompilerOptions": { + "enableI18nLegacyMessageIdFormat": false, + "strictInjectionParameters": false, + "strictInputAccessModifiers": false, + "strictTemplates": false + }, + "files": [], + "include": [], + "references": [ + { "path": "./tsconfig.app.json" } + ] +} diff --git a/cockpit/chat/tool-calls/python/docs/guide.md b/cockpit/chat/tool-calls/python/docs/guide.md new file mode 100644 index 000000000..42b1b0110 --- /dev/null +++ b/cockpit/chat/tool-calls/python/docs/guide.md @@ -0,0 +1,73 @@ +# Chat Tool Calls with @cacheplane/chat + + +Display tool call execution and results using ChatToolCallsComponent +and ChatToolCallCardComponent. The agent invokes tools and the frontend +renders each call with its arguments and response. + + + +Add tool call visualization to your chat interface using +`ChatToolCallsComponent` and `ChatToolCallCardComponent` from +`@cacheplane/chat`. Display active tool calls in a sidebar. + + + + + +Create LangChain tools using the `@tool` decorator: + +```python +from langchain_core.tools import tool + +@tool +def search(query: str) -> str: + """Search the web for information.""" + return f"Results for '{query}'" +``` + + + + +Bind the tools to the ChatOpenAI model so it can invoke them: + +```python +llm = ChatOpenAI(model="gpt-5-mini", streaming=True).bind_tools(tools) +``` + + + + +Use LangGraph's `ToolNode` to execute tool calls: + +```python +from langgraph.prebuilt import ToolNode +tool_node = ToolNode(tools) +``` + + + + +Use `ChatToolCallsComponent` to display active tool calls: + +```html + +``` + + + + +Use `ChatToolCallCardComponent` for detailed tool call views +showing the tool name, arguments, and result: + +```html + +``` + + + + + +Tool calls execute in a loop — the agent generates a tool call, the tool +node executes it, and the result feeds back into the agent for the next step. + diff --git a/cockpit/chat/tool-calls/python/langgraph.json b/cockpit/chat/tool-calls/python/langgraph.json new file mode 100644 index 000000000..f11bbfefa --- /dev/null +++ b/cockpit/chat/tool-calls/python/langgraph.json @@ -0,0 +1,8 @@ +{ + "graphs": { + "c-tool-calls": "./src/graph.py:graph" + }, + "dependencies": ["."], + "python_version": "3.12", + "env": ".env" +} diff --git a/cockpit/chat/tool-calls/python/prompts/tool-calls.md b/cockpit/chat/tool-calls/python/prompts/tool-calls.md new file mode 100644 index 000000000..bd84e06d7 --- /dev/null +++ b/cockpit/chat/tool-calls/python/prompts/tool-calls.md @@ -0,0 +1,13 @@ +# Chat Tool Calls Assistant + +You are an assistant with access to search, calculator, and weather tools. +Use these tools proactively to answer user questions. + +Available tools: +- **search**: Search the web for information on any topic +- **calculator**: Evaluate mathematical expressions +- **weather**: Get current weather for any city + +When the user asks a question, use the appropriate tool(s) to gather +information before responding. Combine results from multiple tools +when needed. Always explain which tools you used and why. diff --git a/cockpit/chat/tool-calls/python/pyproject.toml b/cockpit/chat/tool-calls/python/pyproject.toml new file mode 100644 index 000000000..ca0f95f2b --- /dev/null +++ b/cockpit/chat/tool-calls/python/pyproject.toml @@ -0,0 +1,21 @@ +[project] +name = "cockpit-chat-tool-calls" +version = "0.1.0" +requires-python = ">=3.12" +dependencies = [ + "langgraph>=0.3", + "langchain-openai>=0.3", + "langsmith>=0.2", +] + +[tool.uv] +dev-dependencies = [ + "langgraph-cli[inmem]>=0.1", +] + +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[tool.hatch.build.targets.wheel] +packages = ["src"] diff --git a/cockpit/chat/tool-calls/python/src/graph.py b/cockpit/chat/tool-calls/python/src/graph.py new file mode 100644 index 000000000..ef3feeaf5 --- /dev/null +++ b/cockpit/chat/tool-calls/python/src/graph.py @@ -0,0 +1,75 @@ +""" +Chat Tool Calls Graph + +A LangGraph StateGraph that demonstrates tool calling with search, +calculator, and weather tools. The agent uses tools proactively +and the frontend renders tool call cards. +""" + +from pathlib import Path +from langgraph.graph import StateGraph, MessagesState, END +from langgraph.prebuilt import ToolNode +from langchain_openai import ChatOpenAI +from langchain_core.messages import SystemMessage +from langchain_core.tools import tool + +PROMPTS_DIR = Path(__file__).parent.parent / "prompts" + + +@tool +def search(query: str) -> str: + """Search the web for information.""" + return f"Search results for '{query}': Found 3 relevant articles about {query}." + + +@tool +def calculator(expression: str) -> str: + """Evaluate a mathematical expression.""" + try: + result = eval(expression) + return f"Result: {result}" + except Exception as e: + return f"Error: {str(e)}" + + +@tool +def weather(city: str) -> str: + """Get current weather for a city.""" + return f"Weather in {city}: 72F, partly cloudy, humidity 45%." + + +tools = [search, calculator, weather] + + +def build_tool_calls_graph(): + """ + Constructs a tool-calling agent with search, calculator, and weather tools. + Uses conditional edges to route between generation and tool execution. + """ + llm = ChatOpenAI(model="gpt-5-mini", streaming=True).bind_tools(tools) + + async def generate(state: MessagesState) -> dict: + system_prompt = (PROMPTS_DIR / "tool-calls.md").read_text() + messages = [SystemMessage(content=system_prompt)] + state["messages"] + response = await llm.ainvoke(messages) + return {"messages": [response]} + + def should_continue(state: MessagesState) -> str: + last_message = state["messages"][-1] + if hasattr(last_message, "tool_calls") and last_message.tool_calls: + return "tools" + return END + + tool_node = ToolNode(tools) + + graph = StateGraph(MessagesState) + graph.add_node("generate", generate) + graph.add_node("tools", tool_node) + graph.set_entry_point("generate") + graph.add_conditional_edges("generate", should_continue) + graph.add_edge("tools", "generate") + + return graph.compile() + + +graph = build_tool_calls_graph() diff --git a/cockpit/chat/tool-calls/python/src/index.ts b/cockpit/chat/tool-calls/python/src/index.ts new file mode 100644 index 000000000..08ea6075f --- /dev/null +++ b/cockpit/chat/tool-calls/python/src/index.ts @@ -0,0 +1,42 @@ +export interface CockpitCapabilityModule { + id: string; + manifestIdentity: { + product: 'chat'; + section: 'core-capabilities'; + topic: 'tool-calls'; + page: 'overview'; + language: 'python'; + }; + title: string; + docsPath: string; + promptAssetPaths: string[]; + codeAssetPaths: string[]; + backendAssetPaths: string[]; + docsAssetPaths: string[]; + runtimeUrl?: string; + devPort?: number; +} + +export const chatToolCallsPythonModule: CockpitCapabilityModule = { + id: 'chat-tool-calls-python', + manifestIdentity: { + product: 'chat', + section: 'core-capabilities', + topic: 'tool-calls', + page: 'overview', + language: 'python', + }, + title: 'Chat Tool Calls (Python)', + docsPath: '/docs/chat/core-capabilities/tool-calls/overview/python', + promptAssetPaths: ['cockpit/chat/tool-calls/python/prompts/tool-calls.md'], + codeAssetPaths: [ + 'cockpit/chat/tool-calls/angular/src/app/tool-calls.component.ts', + 'cockpit/chat/tool-calls/angular/src/app/app.config.ts', + ], + backendAssetPaths: [ + 'cockpit/chat/tool-calls/python/src/graph.py', + ], + docsAssetPaths: ['cockpit/chat/tool-calls/python/docs/guide.md'], + runtimeUrl: 'chat/tool-calls', + devPort: 4504, +}; From 0a20bf12bb1d78c823c23180c7b4e84509ded37e Mon Sep 17 00:00:00 2001 From: Brian Love Date: Wed, 8 Apr 2026 10:19:02 -0700 Subject: [PATCH 11/48] feat(cockpit): add chat/subagents capability Adds the subagents capability demonstrating orchestrator pattern with ChatSubagentsComponent and ChatSubagentCardComponent for tracking research, analysis, and summary subagents. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../subagents/angular/e2e/subagents.spec.ts | 21 ++++++ cockpit/chat/subagents/angular/project.json | 61 ++++++++++++++++ .../chat/subagents/angular/proxy.conf.json | 9 +++ .../subagents/angular/src/app/app.config.ts | 12 ++++ .../angular/src/app/subagents.component.ts | 47 +++++++++++++ .../environments/environment.development.ts | 5 ++ .../angular/src/environments/environment.ts | 5 ++ cockpit/chat/subagents/angular/src/index.html | 13 ++++ cockpit/chat/subagents/angular/src/index.ts | 29 ++++++++ cockpit/chat/subagents/angular/src/main.ts | 6 ++ cockpit/chat/subagents/angular/src/styles.css | 1 + .../chat/subagents/angular/tsconfig.app.json | 9 +++ cockpit/chat/subagents/angular/tsconfig.json | 24 +++++++ cockpit/chat/subagents/python/docs/guide.md | 70 +++++++++++++++++++ cockpit/chat/subagents/python/langgraph.json | 8 +++ .../subagents/python/prompts/subagents.md | 12 ++++ cockpit/chat/subagents/python/pyproject.toml | 21 ++++++ cockpit/chat/subagents/python/src/graph.py | 63 +++++++++++++++++ cockpit/chat/subagents/python/src/index.ts | 42 +++++++++++ 19 files changed, 458 insertions(+) create mode 100644 cockpit/chat/subagents/angular/e2e/subagents.spec.ts create mode 100644 cockpit/chat/subagents/angular/project.json create mode 100644 cockpit/chat/subagents/angular/proxy.conf.json create mode 100644 cockpit/chat/subagents/angular/src/app/app.config.ts create mode 100644 cockpit/chat/subagents/angular/src/app/subagents.component.ts create mode 100644 cockpit/chat/subagents/angular/src/environments/environment.development.ts create mode 100644 cockpit/chat/subagents/angular/src/environments/environment.ts create mode 100644 cockpit/chat/subagents/angular/src/index.html create mode 100644 cockpit/chat/subagents/angular/src/index.ts create mode 100644 cockpit/chat/subagents/angular/src/main.ts create mode 100644 cockpit/chat/subagents/angular/src/styles.css create mode 100644 cockpit/chat/subagents/angular/tsconfig.app.json create mode 100644 cockpit/chat/subagents/angular/tsconfig.json create mode 100644 cockpit/chat/subagents/python/docs/guide.md create mode 100644 cockpit/chat/subagents/python/langgraph.json create mode 100644 cockpit/chat/subagents/python/prompts/subagents.md create mode 100644 cockpit/chat/subagents/python/pyproject.toml create mode 100644 cockpit/chat/subagents/python/src/graph.py create mode 100644 cockpit/chat/subagents/python/src/index.ts diff --git a/cockpit/chat/subagents/angular/e2e/subagents.spec.ts b/cockpit/chat/subagents/angular/e2e/subagents.spec.ts new file mode 100644 index 000000000..5599ffd9e --- /dev/null +++ b/cockpit/chat/subagents/angular/e2e/subagents.spec.ts @@ -0,0 +1,21 @@ +import { expect, test } from '@playwright/test'; + +test.describe('Chat Subagents Example', () => { + test.beforeEach(async ({ page }) => { + await page.goto('http://localhost:4505'); + await page.waitForSelector('app-subagents', { state: 'attached' }); + }); + + test('renders the chat interface with subagents sidebar', async ({ page }) => { + await expect(page.locator('chat')).toBeVisible(); + await expect(page.locator('aside')).toBeVisible(); + await expect(page.locator('aside h3')).toHaveText('Active Subagents'); + }); + + test('displays the agent pipeline', async ({ page }) => { + await expect(page.locator('aside')).toContainText('Orchestrator'); + await expect(page.locator('aside')).toContainText('Research Agent'); + await expect(page.locator('aside')).toContainText('Analysis Agent'); + await expect(page.locator('aside')).toContainText('Summary Agent'); + }); +}); diff --git a/cockpit/chat/subagents/angular/project.json b/cockpit/chat/subagents/angular/project.json new file mode 100644 index 000000000..89ed44c2f --- /dev/null +++ b/cockpit/chat/subagents/angular/project.json @@ -0,0 +1,61 @@ +{ + "name": "cockpit-chat-subagents-angular", + "$schema": "../../../../node_modules/nx/schemas/project-schema.json", + "sourceRoot": "cockpit/chat/subagents/angular/src", + "projectType": "application", + "targets": { + "build": { + "executor": "@angular/build:application", + "outputs": ["{options.outputPath.base}"], + "options": { + "outputPath": { + "base": "dist/cockpit/chat/subagents/angular", + "browser": "" + }, + "browser": "cockpit/chat/subagents/angular/src/main.ts", + "tsConfig": "cockpit/chat/subagents/angular/tsconfig.app.json", + "styles": ["cockpit/chat/subagents/angular/src/styles.css"] + }, + "configurations": { + "production": { + "budgets": [ + { "type": "initial", "maximumWarning": "500kb", "maximumError": "1mb" }, + { "type": "anyComponentStyle", "maximumWarning": "4kb", "maximumError": "8kb" } + ], + "outputHashing": "none" + }, + "development": { + "optimization": false, + "extractLicenses": false, + "sourceMap": true, + "fileReplacements": [ + { + "replace": "cockpit/chat/subagents/angular/src/environments/environment.ts", + "with": "cockpit/chat/subagents/angular/src/environments/environment.development.ts" + } + ] + } + }, + "defaultConfiguration": "production" + }, + "serve": { + "continuous": true, + "executor": "@angular/build:dev-server", + "configurations": { + "production": { "buildTarget": "cockpit-chat-subagents-angular:build:production" }, + "development": { "buildTarget": "cockpit-chat-subagents-angular:build:development" } + }, + "defaultConfiguration": "development", + "options": { + "proxyConfig": "cockpit/chat/subagents/angular/proxy.conf.json" + } + }, + "smoke": { + "executor": "nx:run-commands", + "options": { + "cwd": "cockpit/chat/subagents/angular", + "command": "npx tsx -e \"import { chatSubagentsAngularModule } from './src/index.ts'; const module = chatSubagentsAngularModule; if (module.id !== 'chat-subagents-angular' || module.title !== 'Chat Subagents (Angular)') { throw new Error('Unexpected module shape for ' + module.id); }\"" + } + } + } +} diff --git a/cockpit/chat/subagents/angular/proxy.conf.json b/cockpit/chat/subagents/angular/proxy.conf.json new file mode 100644 index 000000000..8523362d7 --- /dev/null +++ b/cockpit/chat/subagents/angular/proxy.conf.json @@ -0,0 +1,9 @@ +{ + "/api": { + "target": "http://localhost:8123", + "secure": false, + "changeOrigin": true, + "pathRewrite": { "^/api": "" }, + "ws": true + } +} diff --git a/cockpit/chat/subagents/angular/src/app/app.config.ts b/cockpit/chat/subagents/angular/src/app/app.config.ts new file mode 100644 index 000000000..b52a916ac --- /dev/null +++ b/cockpit/chat/subagents/angular/src/app/app.config.ts @@ -0,0 +1,12 @@ +// SPDX-License-Identifier: PolyForm-Noncommercial-1.0.0 +import { ApplicationConfig } from '@angular/core'; +import { provideStreamResource } from '@cacheplane/stream-resource'; +import { provideChat } from '@cacheplane/chat'; +import { environment } from '../environments/environment'; + +export const appConfig: ApplicationConfig = { + providers: [ + provideStreamResource({ apiUrl: environment.langGraphApiUrl }), + provideChat({}), + ], +}; diff --git a/cockpit/chat/subagents/angular/src/app/subagents.component.ts b/cockpit/chat/subagents/angular/src/app/subagents.component.ts new file mode 100644 index 000000000..4558fe206 --- /dev/null +++ b/cockpit/chat/subagents/angular/src/app/subagents.component.ts @@ -0,0 +1,47 @@ +// SPDX-License-Identifier: PolyForm-Noncommercial-1.0.0 +import { Component } from '@angular/core'; +import { + ChatComponent, + ChatSubagentsComponent, + ChatSubagentCardComponent, +} from '@cacheplane/chat'; +import { streamResource } from '@cacheplane/stream-resource'; +import { environment } from '../environments/environment'; + +/** + * SubagentsComponent demonstrates subagent orchestration with + * ChatComponent and a sidebar showing ChatSubagentsComponent / + * ChatSubagentCardComponent for tracking active subagents. + */ +@Component({ + selector: 'app-subagents', + standalone: true, + imports: [ChatComponent, ChatSubagentsComponent, ChatSubagentCardComponent], + template: ` +
+ + +
+ `, +}) +export class SubagentsComponent { + protected readonly stream = streamResource({ + apiUrl: environment.langGraphApiUrl, + assistantId: environment.streamingAssistantId, + }); +} diff --git a/cockpit/chat/subagents/angular/src/environments/environment.development.ts b/cockpit/chat/subagents/angular/src/environments/environment.development.ts new file mode 100644 index 000000000..33f6dc132 --- /dev/null +++ b/cockpit/chat/subagents/angular/src/environments/environment.development.ts @@ -0,0 +1,5 @@ +export const environment = { + production: false, + langGraphApiUrl: 'http://localhost:4505/api', + streamingAssistantId: 'c-subagents', +}; diff --git a/cockpit/chat/subagents/angular/src/environments/environment.ts b/cockpit/chat/subagents/angular/src/environments/environment.ts new file mode 100644 index 000000000..f0a9585e7 --- /dev/null +++ b/cockpit/chat/subagents/angular/src/environments/environment.ts @@ -0,0 +1,5 @@ +export const environment = { + production: true, + langGraphApiUrl: '/api', + streamingAssistantId: 'c-subagents', +}; diff --git a/cockpit/chat/subagents/angular/src/index.html b/cockpit/chat/subagents/angular/src/index.html new file mode 100644 index 000000000..0e43c8531 --- /dev/null +++ b/cockpit/chat/subagents/angular/src/index.html @@ -0,0 +1,13 @@ + + + + + Chat Subagents — Angular + + + + + + + + diff --git a/cockpit/chat/subagents/angular/src/index.ts b/cockpit/chat/subagents/angular/src/index.ts new file mode 100644 index 000000000..f982a4dd0 --- /dev/null +++ b/cockpit/chat/subagents/angular/src/index.ts @@ -0,0 +1,29 @@ +export interface CockpitCapabilityModule { + id: string; + manifestIdentity: { + product: 'chat'; + section: 'core-capabilities'; + topic: 'subagents'; + page: 'overview'; + language: 'angular'; + }; + title: string; + docsPath: string; + promptAssetPaths: string[]; + codeAssetPaths: string[]; +} + +export const chatSubagentsAngularModule: CockpitCapabilityModule = { + id: 'chat-subagents-angular', + manifestIdentity: { + product: 'chat', + section: 'core-capabilities', + topic: 'subagents', + page: 'overview', + language: 'angular', + }, + title: 'Chat Subagents (Angular)', + docsPath: '/docs/chat/core-capabilities/subagents/overview/angular', + promptAssetPaths: ['cockpit/chat/subagents/python/prompts/subagents.md'], + codeAssetPaths: ['cockpit/chat/subagents/angular/src/app/subagents.component.ts'], +}; diff --git a/cockpit/chat/subagents/angular/src/main.ts b/cockpit/chat/subagents/angular/src/main.ts new file mode 100644 index 000000000..869cb2212 --- /dev/null +++ b/cockpit/chat/subagents/angular/src/main.ts @@ -0,0 +1,6 @@ +// SPDX-License-Identifier: PolyForm-Noncommercial-1.0.0 +import { bootstrapApplication } from '@angular/platform-browser'; +import { appConfig } from './app/app.config'; +import { SubagentsComponent } from './app/subagents.component'; + +bootstrapApplication(SubagentsComponent, appConfig).catch(console.error); diff --git a/cockpit/chat/subagents/angular/src/styles.css b/cockpit/chat/subagents/angular/src/styles.css new file mode 100644 index 000000000..315040f3b --- /dev/null +++ b/cockpit/chat/subagents/angular/src/styles.css @@ -0,0 +1 @@ +/* Global styles for the subagents capability demo */ diff --git a/cockpit/chat/subagents/angular/tsconfig.app.json b/cockpit/chat/subagents/angular/tsconfig.app.json new file mode 100644 index 000000000..72c01e364 --- /dev/null +++ b/cockpit/chat/subagents/angular/tsconfig.app.json @@ -0,0 +1,9 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../../../dist/out-tsc", + "types": [] + }, + "files": ["src/main.ts"], + "include": ["src/**/*.d.ts", "src/**/*.ts"] +} diff --git a/cockpit/chat/subagents/angular/tsconfig.json b/cockpit/chat/subagents/angular/tsconfig.json new file mode 100644 index 000000000..3fd970371 --- /dev/null +++ b/cockpit/chat/subagents/angular/tsconfig.json @@ -0,0 +1,24 @@ +{ + "extends": "../../../../tsconfig.base.json", + "compilerOptions": { + "noPropertyAccessFromIndexSignature": false, + "experimentalDecorators": true, + "module": "preserve", + "emitDeclarationOnly": false, + "composite": false, + "lib": ["es2022", "dom"], + "skipLibCheck": true, + "strict": false + }, + "angularCompilerOptions": { + "enableI18nLegacyMessageIdFormat": false, + "strictInjectionParameters": false, + "strictInputAccessModifiers": false, + "strictTemplates": false + }, + "files": [], + "include": [], + "references": [ + { "path": "./tsconfig.app.json" } + ] +} diff --git a/cockpit/chat/subagents/python/docs/guide.md b/cockpit/chat/subagents/python/docs/guide.md new file mode 100644 index 000000000..417a25456 --- /dev/null +++ b/cockpit/chat/subagents/python/docs/guide.md @@ -0,0 +1,70 @@ +# Chat Subagents with @cacheplane/chat + + +Track and display subagent orchestration using ChatSubagentsComponent +and ChatSubagentCardComponent. Shows active subagents with their status +as the orchestrator delegates work. + + + +Add subagent tracking to your chat interface using `ChatSubagentsComponent` +and `ChatSubagentCardComponent` from `@cacheplane/chat`. Display active +subagents with real-time status in a sidebar. + + + + + +Build a LangGraph with an orchestrator node that delegates to subagent nodes: + +```python +graph = StateGraph(MessagesState) +graph.add_node("orchestrator", orchestrator) +graph.add_node("research_agent", research_agent) +graph.add_node("analysis_agent", analysis_agent) +``` + + + + +Each subagent node emits status updates that the frontend tracks. +The stream resource automatically detects node transitions: + +```typescript +protected readonly stream = streamResource({ + apiUrl: environment.langGraphApiUrl, + assistantId: environment.streamingAssistantId, +}); +``` + + + + +Use `ChatSubagentsComponent` to display all active subagents: + +```html + +``` + + + + +Use `ChatSubagentCardComponent` for detailed views of each subagent: + +```html + +``` + + + + +Monitor when all subagents complete and the orchestrator produces +the final response. The UI updates automatically as nodes finish. + + + + + +The orchestrator pattern works well for complex tasks that benefit from +specialized processing by domain-specific agents. + diff --git a/cockpit/chat/subagents/python/langgraph.json b/cockpit/chat/subagents/python/langgraph.json new file mode 100644 index 000000000..f0ccfcd95 --- /dev/null +++ b/cockpit/chat/subagents/python/langgraph.json @@ -0,0 +1,8 @@ +{ + "graphs": { + "c-subagents": "./src/graph.py:graph" + }, + "dependencies": ["."], + "python_version": "3.12", + "env": ".env" +} diff --git a/cockpit/chat/subagents/python/prompts/subagents.md b/cockpit/chat/subagents/python/prompts/subagents.md new file mode 100644 index 000000000..328d196ef --- /dev/null +++ b/cockpit/chat/subagents/python/prompts/subagents.md @@ -0,0 +1,12 @@ +# Chat Subagents Orchestrator + +You are the orchestrator in a multi-agent system. You coordinate specialized +subagents to handle user requests: + +- **Research Agent**: Gathers background information and context +- **Analysis Agent**: Analyzes findings and identifies patterns +- **Summary Agent**: Produces a concise summary of results + +When the user asks a question, acknowledge their request and explain that +you are delegating work to your specialized subagents. Each subagent will +process the task in sequence and their progress will be visible in the UI. diff --git a/cockpit/chat/subagents/python/pyproject.toml b/cockpit/chat/subagents/python/pyproject.toml new file mode 100644 index 000000000..a6d03938e --- /dev/null +++ b/cockpit/chat/subagents/python/pyproject.toml @@ -0,0 +1,21 @@ +[project] +name = "cockpit-chat-subagents" +version = "0.1.0" +requires-python = ">=3.12" +dependencies = [ + "langgraph>=0.3", + "langchain-openai>=0.3", + "langsmith>=0.2", +] + +[tool.uv] +dev-dependencies = [ + "langgraph-cli[inmem]>=0.1", +] + +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[tool.hatch.build.targets.wheel] +packages = ["src"] diff --git a/cockpit/chat/subagents/python/src/graph.py b/cockpit/chat/subagents/python/src/graph.py new file mode 100644 index 000000000..d03984b31 --- /dev/null +++ b/cockpit/chat/subagents/python/src/graph.py @@ -0,0 +1,63 @@ +""" +Chat Subagents Graph + +A LangGraph StateGraph demonstrating an orchestrator pattern with +subagent delegation. The orchestrator routes tasks to specialized +subagent nodes that simulate domain-specific processing. +""" + +from pathlib import Path +from langgraph.graph import StateGraph, MessagesState, END +from langchain_openai import ChatOpenAI +from langchain_core.messages import SystemMessage, AIMessage + +PROMPTS_DIR = Path(__file__).parent.parent / "prompts" + + +def build_subagents_graph(): + """ + Constructs an orchestrator graph that delegates to specialized subagents. + Demonstrates the subagent pattern with research, analysis, and summary nodes. + """ + llm = ChatOpenAI(model="gpt-5-mini", streaming=True) + + async def orchestrator(state: MessagesState) -> dict: + system_prompt = (PROMPTS_DIR / "subagents.md").read_text() + messages = [SystemMessage(content=system_prompt)] + state["messages"] + response = await llm.ainvoke(messages) + return {"messages": [response]} + + async def research_agent(state: MessagesState) -> dict: + last = state["messages"][-1].content + response = AIMessage( + content=f"[Research Agent] Gathered background information on: {last[:100]}" + ) + return {"messages": [response]} + + async def analysis_agent(state: MessagesState) -> dict: + last = state["messages"][-1].content + response = AIMessage( + content=f"[Analysis Agent] Analyzed findings and identified key patterns." + ) + return {"messages": [response]} + + async def summary_agent(state: MessagesState) -> dict: + messages = [SystemMessage(content="Summarize the conversation so far in a concise paragraph.")] + state["messages"] + response = await llm.ainvoke(messages) + return {"messages": [response]} + + graph = StateGraph(MessagesState) + graph.add_node("orchestrator", orchestrator) + graph.add_node("research_agent", research_agent) + graph.add_node("analysis_agent", analysis_agent) + graph.add_node("summary_agent", summary_agent) + graph.set_entry_point("orchestrator") + graph.add_edge("orchestrator", "research_agent") + graph.add_edge("research_agent", "analysis_agent") + graph.add_edge("analysis_agent", "summary_agent") + graph.add_edge("summary_agent", END) + + return graph.compile() + + +graph = build_subagents_graph() diff --git a/cockpit/chat/subagents/python/src/index.ts b/cockpit/chat/subagents/python/src/index.ts new file mode 100644 index 000000000..569001e33 --- /dev/null +++ b/cockpit/chat/subagents/python/src/index.ts @@ -0,0 +1,42 @@ +export interface CockpitCapabilityModule { + id: string; + manifestIdentity: { + product: 'chat'; + section: 'core-capabilities'; + topic: 'subagents'; + page: 'overview'; + language: 'python'; + }; + title: string; + docsPath: string; + promptAssetPaths: string[]; + codeAssetPaths: string[]; + backendAssetPaths: string[]; + docsAssetPaths: string[]; + runtimeUrl?: string; + devPort?: number; +} + +export const chatSubagentsPythonModule: CockpitCapabilityModule = { + id: 'chat-subagents-python', + manifestIdentity: { + product: 'chat', + section: 'core-capabilities', + topic: 'subagents', + page: 'overview', + language: 'python', + }, + title: 'Chat Subagents (Python)', + docsPath: '/docs/chat/core-capabilities/subagents/overview/python', + promptAssetPaths: ['cockpit/chat/subagents/python/prompts/subagents.md'], + codeAssetPaths: [ + 'cockpit/chat/subagents/angular/src/app/subagents.component.ts', + 'cockpit/chat/subagents/angular/src/app/app.config.ts', + ], + backendAssetPaths: [ + 'cockpit/chat/subagents/python/src/graph.py', + ], + docsAssetPaths: ['cockpit/chat/subagents/python/docs/guide.md'], + runtimeUrl: 'chat/subagents', + devPort: 4505, +}; From 1de2146c6983a5c096db7652fb3cf349a89ffbc0 Mon Sep 17 00:00:00 2001 From: Brian Love Date: Wed, 8 Apr 2026 10:20:36 -0700 Subject: [PATCH 12/48] feat(cockpit): add chat/threads capability Adds the threads capability demonstrating multi-thread conversation management with ChatThreadListComponent for creating and switching threads. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../chat/threads/angular/e2e/threads.spec.ts | 18 +++++ cockpit/chat/threads/angular/project.json | 61 ++++++++++++++++ cockpit/chat/threads/angular/proxy.conf.json | 9 +++ .../threads/angular/src/app/app.config.ts | 12 +++ .../angular/src/app/threads.component.ts | 32 ++++++++ .../environments/environment.development.ts | 5 ++ .../angular/src/environments/environment.ts | 5 ++ cockpit/chat/threads/angular/src/index.html | 13 ++++ cockpit/chat/threads/angular/src/index.ts | 29 ++++++++ cockpit/chat/threads/angular/src/main.ts | 6 ++ cockpit/chat/threads/angular/src/styles.css | 1 + .../chat/threads/angular/tsconfig.app.json | 9 +++ cockpit/chat/threads/angular/tsconfig.json | 24 ++++++ cockpit/chat/threads/python/docs/guide.md | 73 +++++++++++++++++++ cockpit/chat/threads/python/langgraph.json | 8 ++ .../chat/threads/python/prompts/threads.md | 11 +++ cockpit/chat/threads/python/pyproject.toml | 21 ++++++ cockpit/chat/threads/python/src/graph.py | 37 ++++++++++ cockpit/chat/threads/python/src/index.ts | 42 +++++++++++ 19 files changed, 416 insertions(+) create mode 100644 cockpit/chat/threads/angular/e2e/threads.spec.ts create mode 100644 cockpit/chat/threads/angular/project.json create mode 100644 cockpit/chat/threads/angular/proxy.conf.json create mode 100644 cockpit/chat/threads/angular/src/app/app.config.ts create mode 100644 cockpit/chat/threads/angular/src/app/threads.component.ts create mode 100644 cockpit/chat/threads/angular/src/environments/environment.development.ts create mode 100644 cockpit/chat/threads/angular/src/environments/environment.ts create mode 100644 cockpit/chat/threads/angular/src/index.html create mode 100644 cockpit/chat/threads/angular/src/index.ts create mode 100644 cockpit/chat/threads/angular/src/main.ts create mode 100644 cockpit/chat/threads/angular/src/styles.css create mode 100644 cockpit/chat/threads/angular/tsconfig.app.json create mode 100644 cockpit/chat/threads/angular/tsconfig.json create mode 100644 cockpit/chat/threads/python/docs/guide.md create mode 100644 cockpit/chat/threads/python/langgraph.json create mode 100644 cockpit/chat/threads/python/prompts/threads.md create mode 100644 cockpit/chat/threads/python/pyproject.toml create mode 100644 cockpit/chat/threads/python/src/graph.py create mode 100644 cockpit/chat/threads/python/src/index.ts diff --git a/cockpit/chat/threads/angular/e2e/threads.spec.ts b/cockpit/chat/threads/angular/e2e/threads.spec.ts new file mode 100644 index 000000000..c9acaf482 --- /dev/null +++ b/cockpit/chat/threads/angular/e2e/threads.spec.ts @@ -0,0 +1,18 @@ +import { expect, test } from '@playwright/test'; + +test.describe('Chat Threads Example', () => { + test.beforeEach(async ({ page }) => { + await page.goto('http://localhost:4506'); + await page.waitForSelector('app-threads', { state: 'attached' }); + }); + + test('renders the chat interface with thread list sidebar', async ({ page }) => { + await expect(page.locator('chat')).toBeVisible(); + await expect(page.locator('aside')).toBeVisible(); + await expect(page.locator('aside h3')).toHaveText('Threads'); + }); + + test('displays the thread list component', async ({ page }) => { + await expect(page.locator('chat-thread-list')).toBeVisible(); + }); +}); diff --git a/cockpit/chat/threads/angular/project.json b/cockpit/chat/threads/angular/project.json new file mode 100644 index 000000000..5dbf5e131 --- /dev/null +++ b/cockpit/chat/threads/angular/project.json @@ -0,0 +1,61 @@ +{ + "name": "cockpit-chat-threads-angular", + "$schema": "../../../../node_modules/nx/schemas/project-schema.json", + "sourceRoot": "cockpit/chat/threads/angular/src", + "projectType": "application", + "targets": { + "build": { + "executor": "@angular/build:application", + "outputs": ["{options.outputPath.base}"], + "options": { + "outputPath": { + "base": "dist/cockpit/chat/threads/angular", + "browser": "" + }, + "browser": "cockpit/chat/threads/angular/src/main.ts", + "tsConfig": "cockpit/chat/threads/angular/tsconfig.app.json", + "styles": ["cockpit/chat/threads/angular/src/styles.css"] + }, + "configurations": { + "production": { + "budgets": [ + { "type": "initial", "maximumWarning": "500kb", "maximumError": "1mb" }, + { "type": "anyComponentStyle", "maximumWarning": "4kb", "maximumError": "8kb" } + ], + "outputHashing": "none" + }, + "development": { + "optimization": false, + "extractLicenses": false, + "sourceMap": true, + "fileReplacements": [ + { + "replace": "cockpit/chat/threads/angular/src/environments/environment.ts", + "with": "cockpit/chat/threads/angular/src/environments/environment.development.ts" + } + ] + } + }, + "defaultConfiguration": "production" + }, + "serve": { + "continuous": true, + "executor": "@angular/build:dev-server", + "configurations": { + "production": { "buildTarget": "cockpit-chat-threads-angular:build:production" }, + "development": { "buildTarget": "cockpit-chat-threads-angular:build:development" } + }, + "defaultConfiguration": "development", + "options": { + "proxyConfig": "cockpit/chat/threads/angular/proxy.conf.json" + } + }, + "smoke": { + "executor": "nx:run-commands", + "options": { + "cwd": "cockpit/chat/threads/angular", + "command": "npx tsx -e \"import { chatThreadsAngularModule } from './src/index.ts'; const module = chatThreadsAngularModule; if (module.id !== 'chat-threads-angular' || module.title !== 'Chat Threads (Angular)') { throw new Error('Unexpected module shape for ' + module.id); }\"" + } + } + } +} diff --git a/cockpit/chat/threads/angular/proxy.conf.json b/cockpit/chat/threads/angular/proxy.conf.json new file mode 100644 index 000000000..8523362d7 --- /dev/null +++ b/cockpit/chat/threads/angular/proxy.conf.json @@ -0,0 +1,9 @@ +{ + "/api": { + "target": "http://localhost:8123", + "secure": false, + "changeOrigin": true, + "pathRewrite": { "^/api": "" }, + "ws": true + } +} diff --git a/cockpit/chat/threads/angular/src/app/app.config.ts b/cockpit/chat/threads/angular/src/app/app.config.ts new file mode 100644 index 000000000..b52a916ac --- /dev/null +++ b/cockpit/chat/threads/angular/src/app/app.config.ts @@ -0,0 +1,12 @@ +// SPDX-License-Identifier: PolyForm-Noncommercial-1.0.0 +import { ApplicationConfig } from '@angular/core'; +import { provideStreamResource } from '@cacheplane/stream-resource'; +import { provideChat } from '@cacheplane/chat'; +import { environment } from '../environments/environment'; + +export const appConfig: ApplicationConfig = { + providers: [ + provideStreamResource({ apiUrl: environment.langGraphApiUrl }), + provideChat({}), + ], +}; diff --git a/cockpit/chat/threads/angular/src/app/threads.component.ts b/cockpit/chat/threads/angular/src/app/threads.component.ts new file mode 100644 index 000000000..acf06401f --- /dev/null +++ b/cockpit/chat/threads/angular/src/app/threads.component.ts @@ -0,0 +1,32 @@ +// SPDX-License-Identifier: PolyForm-Noncommercial-1.0.0 +import { Component } from '@angular/core'; +import { ChatComponent, ChatThreadListComponent } from '@cacheplane/chat'; +import { streamResource } from '@cacheplane/stream-resource'; +import { environment } from '../environments/environment'; + +/** + * ThreadsComponent demonstrates multi-thread conversation management + * with ChatComponent and ChatThreadListComponent in a sidebar. + */ +@Component({ + selector: 'app-threads', + standalone: true, + imports: [ChatComponent, ChatThreadListComponent], + template: ` +
+ + +
+ `, +}) +export class ThreadsComponent { + protected readonly stream = streamResource({ + apiUrl: environment.langGraphApiUrl, + assistantId: environment.streamingAssistantId, + }); +} diff --git a/cockpit/chat/threads/angular/src/environments/environment.development.ts b/cockpit/chat/threads/angular/src/environments/environment.development.ts new file mode 100644 index 000000000..d25c91e6a --- /dev/null +++ b/cockpit/chat/threads/angular/src/environments/environment.development.ts @@ -0,0 +1,5 @@ +export const environment = { + production: false, + langGraphApiUrl: 'http://localhost:4506/api', + streamingAssistantId: 'c-threads', +}; diff --git a/cockpit/chat/threads/angular/src/environments/environment.ts b/cockpit/chat/threads/angular/src/environments/environment.ts new file mode 100644 index 000000000..70af086a0 --- /dev/null +++ b/cockpit/chat/threads/angular/src/environments/environment.ts @@ -0,0 +1,5 @@ +export const environment = { + production: true, + langGraphApiUrl: '/api', + streamingAssistantId: 'c-threads', +}; diff --git a/cockpit/chat/threads/angular/src/index.html b/cockpit/chat/threads/angular/src/index.html new file mode 100644 index 000000000..6eb7f52a8 --- /dev/null +++ b/cockpit/chat/threads/angular/src/index.html @@ -0,0 +1,13 @@ + + + + + Chat Threads — Angular + + + + + + + + diff --git a/cockpit/chat/threads/angular/src/index.ts b/cockpit/chat/threads/angular/src/index.ts new file mode 100644 index 000000000..6a16f54b0 --- /dev/null +++ b/cockpit/chat/threads/angular/src/index.ts @@ -0,0 +1,29 @@ +export interface CockpitCapabilityModule { + id: string; + manifestIdentity: { + product: 'chat'; + section: 'core-capabilities'; + topic: 'threads'; + page: 'overview'; + language: 'angular'; + }; + title: string; + docsPath: string; + promptAssetPaths: string[]; + codeAssetPaths: string[]; +} + +export const chatThreadsAngularModule: CockpitCapabilityModule = { + id: 'chat-threads-angular', + manifestIdentity: { + product: 'chat', + section: 'core-capabilities', + topic: 'threads', + page: 'overview', + language: 'angular', + }, + title: 'Chat Threads (Angular)', + docsPath: '/docs/chat/core-capabilities/threads/overview/angular', + promptAssetPaths: ['cockpit/chat/threads/python/prompts/threads.md'], + codeAssetPaths: ['cockpit/chat/threads/angular/src/app/threads.component.ts'], +}; diff --git a/cockpit/chat/threads/angular/src/main.ts b/cockpit/chat/threads/angular/src/main.ts new file mode 100644 index 000000000..e24b4705e --- /dev/null +++ b/cockpit/chat/threads/angular/src/main.ts @@ -0,0 +1,6 @@ +// SPDX-License-Identifier: PolyForm-Noncommercial-1.0.0 +import { bootstrapApplication } from '@angular/platform-browser'; +import { appConfig } from './app/app.config'; +import { ThreadsComponent } from './app/threads.component'; + +bootstrapApplication(ThreadsComponent, appConfig).catch(console.error); diff --git a/cockpit/chat/threads/angular/src/styles.css b/cockpit/chat/threads/angular/src/styles.css new file mode 100644 index 000000000..7f6c26701 --- /dev/null +++ b/cockpit/chat/threads/angular/src/styles.css @@ -0,0 +1 @@ +/* Global styles for the threads capability demo */ diff --git a/cockpit/chat/threads/angular/tsconfig.app.json b/cockpit/chat/threads/angular/tsconfig.app.json new file mode 100644 index 000000000..72c01e364 --- /dev/null +++ b/cockpit/chat/threads/angular/tsconfig.app.json @@ -0,0 +1,9 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../../../dist/out-tsc", + "types": [] + }, + "files": ["src/main.ts"], + "include": ["src/**/*.d.ts", "src/**/*.ts"] +} diff --git a/cockpit/chat/threads/angular/tsconfig.json b/cockpit/chat/threads/angular/tsconfig.json new file mode 100644 index 000000000..3fd970371 --- /dev/null +++ b/cockpit/chat/threads/angular/tsconfig.json @@ -0,0 +1,24 @@ +{ + "extends": "../../../../tsconfig.base.json", + "compilerOptions": { + "noPropertyAccessFromIndexSignature": false, + "experimentalDecorators": true, + "module": "preserve", + "emitDeclarationOnly": false, + "composite": false, + "lib": ["es2022", "dom"], + "skipLibCheck": true, + "strict": false + }, + "angularCompilerOptions": { + "enableI18nLegacyMessageIdFormat": false, + "strictInjectionParameters": false, + "strictInputAccessModifiers": false, + "strictTemplates": false + }, + "files": [], + "include": [], + "references": [ + { "path": "./tsconfig.app.json" } + ] +} diff --git a/cockpit/chat/threads/python/docs/guide.md b/cockpit/chat/threads/python/docs/guide.md new file mode 100644 index 000000000..9e654b849 --- /dev/null +++ b/cockpit/chat/threads/python/docs/guide.md @@ -0,0 +1,73 @@ +# Chat Threads with @cacheplane/chat + + +Manage multiple conversation threads using ChatThreadListComponent. +Each thread maintains isolated message history, enabling users to +run parallel conversations with the same agent. + + + +Add multi-thread support to your chat interface using `ChatThreadListComponent` +from `@cacheplane/chat`. Display a thread list sidebar for creating and +switching between conversations. + + + + + +Thread management is built into `streamResource()`. Each thread gets +a unique ID that persists its conversation state: + +```typescript +protected readonly stream = streamResource({ + apiUrl: environment.langGraphApiUrl, + assistantId: environment.streamingAssistantId, +}); +``` + + + + +Use `ChatThreadListComponent` in a sidebar to show all threads: + +```html + +``` + + + + +The thread list component includes a button to create new threads. +Each new thread starts with an empty conversation: + +```typescript +createThread() { + this.stream.createThread(); +} +``` + + + + +Click a thread in the list to switch to it. The chat area updates +to show that thread's message history: + +```typescript +switchThread(threadId: string) { + this.stream.switchThread(threadId); +} +``` + + + + +Threads are persisted by the LangGraph backend. Reloading the page +restores the thread list and conversation history. + + + + + +Threads are ideal for keeping separate contexts — e.g., one thread +for code review and another for brainstorming. + diff --git a/cockpit/chat/threads/python/langgraph.json b/cockpit/chat/threads/python/langgraph.json new file mode 100644 index 000000000..8bf246711 --- /dev/null +++ b/cockpit/chat/threads/python/langgraph.json @@ -0,0 +1,8 @@ +{ + "graphs": { + "c-threads": "./src/graph.py:graph" + }, + "dependencies": ["."], + "python_version": "3.12", + "env": ".env" +} diff --git a/cockpit/chat/threads/python/prompts/threads.md b/cockpit/chat/threads/python/prompts/threads.md new file mode 100644 index 000000000..678d2ad5e --- /dev/null +++ b/cockpit/chat/threads/python/prompts/threads.md @@ -0,0 +1,11 @@ +# Chat Threads Assistant + +You are an assistant that demonstrates multi-thread conversation management. + +Each conversation thread maintains its own isolated message history. +Users can create new threads, switch between existing threads, and +each thread preserves its full conversation context independently. + +When the user starts a conversation, acknowledge the current thread +and explain that they can create new threads or switch between them +using the thread list in the sidebar. diff --git a/cockpit/chat/threads/python/pyproject.toml b/cockpit/chat/threads/python/pyproject.toml new file mode 100644 index 000000000..3323111bf --- /dev/null +++ b/cockpit/chat/threads/python/pyproject.toml @@ -0,0 +1,21 @@ +[project] +name = "cockpit-chat-threads" +version = "0.1.0" +requires-python = ">=3.12" +dependencies = [ + "langgraph>=0.3", + "langchain-openai>=0.3", + "langsmith>=0.2", +] + +[tool.uv] +dev-dependencies = [ + "langgraph-cli[inmem]>=0.1", +] + +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[tool.hatch.build.targets.wheel] +packages = ["src"] diff --git a/cockpit/chat/threads/python/src/graph.py b/cockpit/chat/threads/python/src/graph.py new file mode 100644 index 000000000..0d0b03c2d --- /dev/null +++ b/cockpit/chat/threads/python/src/graph.py @@ -0,0 +1,37 @@ +""" +Chat Threads Graph + +A standard conversational agent. Thread management (creating, switching, +persisting) is handled by the frontend and LangGraph SDK, not the graph itself. +""" + +from pathlib import Path +from langgraph.graph import StateGraph, MessagesState, END +from langchain_openai import ChatOpenAI +from langchain_core.messages import SystemMessage + +PROMPTS_DIR = Path(__file__).parent.parent / "prompts" + + +def build_threads_graph(): + """ + Constructs a standard conversational agent. + Threads are managed by the LangGraph SDK on the frontend side. + """ + llm = ChatOpenAI(model="gpt-5-mini", streaming=True) + + async def generate(state: MessagesState) -> dict: + system_prompt = (PROMPTS_DIR / "threads.md").read_text() + messages = [SystemMessage(content=system_prompt)] + state["messages"] + response = await llm.ainvoke(messages) + return {"messages": [response]} + + graph = StateGraph(MessagesState) + graph.add_node("generate", generate) + graph.set_entry_point("generate") + graph.add_edge("generate", END) + + return graph.compile() + + +graph = build_threads_graph() diff --git a/cockpit/chat/threads/python/src/index.ts b/cockpit/chat/threads/python/src/index.ts new file mode 100644 index 000000000..9ef3f113a --- /dev/null +++ b/cockpit/chat/threads/python/src/index.ts @@ -0,0 +1,42 @@ +export interface CockpitCapabilityModule { + id: string; + manifestIdentity: { + product: 'chat'; + section: 'core-capabilities'; + topic: 'threads'; + page: 'overview'; + language: 'python'; + }; + title: string; + docsPath: string; + promptAssetPaths: string[]; + codeAssetPaths: string[]; + backendAssetPaths: string[]; + docsAssetPaths: string[]; + runtimeUrl?: string; + devPort?: number; +} + +export const chatThreadsPythonModule: CockpitCapabilityModule = { + id: 'chat-threads-python', + manifestIdentity: { + product: 'chat', + section: 'core-capabilities', + topic: 'threads', + page: 'overview', + language: 'python', + }, + title: 'Chat Threads (Python)', + docsPath: '/docs/chat/core-capabilities/threads/overview/python', + promptAssetPaths: ['cockpit/chat/threads/python/prompts/threads.md'], + codeAssetPaths: [ + 'cockpit/chat/threads/angular/src/app/threads.component.ts', + 'cockpit/chat/threads/angular/src/app/app.config.ts', + ], + backendAssetPaths: [ + 'cockpit/chat/threads/python/src/graph.py', + ], + docsAssetPaths: ['cockpit/chat/threads/python/docs/guide.md'], + runtimeUrl: 'chat/threads', + devPort: 4506, +}; From 653494d72bb63c550a1eae6346f4d846095b71e7 Mon Sep 17 00:00:00 2001 From: Brian Love Date: Wed, 8 Apr 2026 10:22:09 -0700 Subject: [PATCH 13/48] feat(cockpit): add chat/timeline capability Adds the timeline capability demonstrating conversation checkpoint navigation with ChatTimelineSliderComponent for time-travel debugging. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../timeline/angular/e2e/timeline.spec.ts | 18 +++++ cockpit/chat/timeline/angular/project.json | 61 +++++++++++++++++ cockpit/chat/timeline/angular/proxy.conf.json | 9 +++ .../timeline/angular/src/app/app.config.ts | 12 ++++ .../angular/src/app/timeline.component.ts | 41 ++++++++++++ .../environments/environment.development.ts | 5 ++ .../angular/src/environments/environment.ts | 5 ++ cockpit/chat/timeline/angular/src/index.html | 13 ++++ cockpit/chat/timeline/angular/src/index.ts | 29 ++++++++ cockpit/chat/timeline/angular/src/main.ts | 6 ++ cockpit/chat/timeline/angular/src/styles.css | 1 + .../chat/timeline/angular/tsconfig.app.json | 9 +++ cockpit/chat/timeline/angular/tsconfig.json | 24 +++++++ cockpit/chat/timeline/python/docs/guide.md | 67 +++++++++++++++++++ cockpit/chat/timeline/python/langgraph.json | 8 +++ .../chat/timeline/python/prompts/timeline.md | 11 +++ cockpit/chat/timeline/python/pyproject.toml | 21 ++++++ cockpit/chat/timeline/python/src/graph.py | 37 ++++++++++ cockpit/chat/timeline/python/src/index.ts | 42 ++++++++++++ 19 files changed, 419 insertions(+) create mode 100644 cockpit/chat/timeline/angular/e2e/timeline.spec.ts create mode 100644 cockpit/chat/timeline/angular/project.json create mode 100644 cockpit/chat/timeline/angular/proxy.conf.json create mode 100644 cockpit/chat/timeline/angular/src/app/app.config.ts create mode 100644 cockpit/chat/timeline/angular/src/app/timeline.component.ts create mode 100644 cockpit/chat/timeline/angular/src/environments/environment.development.ts create mode 100644 cockpit/chat/timeline/angular/src/environments/environment.ts create mode 100644 cockpit/chat/timeline/angular/src/index.html create mode 100644 cockpit/chat/timeline/angular/src/index.ts create mode 100644 cockpit/chat/timeline/angular/src/main.ts create mode 100644 cockpit/chat/timeline/angular/src/styles.css create mode 100644 cockpit/chat/timeline/angular/tsconfig.app.json create mode 100644 cockpit/chat/timeline/angular/tsconfig.json create mode 100644 cockpit/chat/timeline/python/docs/guide.md create mode 100644 cockpit/chat/timeline/python/langgraph.json create mode 100644 cockpit/chat/timeline/python/prompts/timeline.md create mode 100644 cockpit/chat/timeline/python/pyproject.toml create mode 100644 cockpit/chat/timeline/python/src/graph.py create mode 100644 cockpit/chat/timeline/python/src/index.ts diff --git a/cockpit/chat/timeline/angular/e2e/timeline.spec.ts b/cockpit/chat/timeline/angular/e2e/timeline.spec.ts new file mode 100644 index 000000000..2e5a2f6e8 --- /dev/null +++ b/cockpit/chat/timeline/angular/e2e/timeline.spec.ts @@ -0,0 +1,18 @@ +import { expect, test } from '@playwright/test'; + +test.describe('Chat Timeline Example', () => { + test.beforeEach(async ({ page }) => { + await page.goto('http://localhost:4507'); + await page.waitForSelector('app-timeline', { state: 'attached' }); + }); + + test('renders the chat interface with timeline sidebar', async ({ page }) => { + await expect(page.locator('chat')).toBeVisible(); + await expect(page.locator('aside')).toBeVisible(); + await expect(page.locator('aside h3')).toHaveText('Timeline'); + }); + + test('displays the timeline slider', async ({ page }) => { + await expect(page.locator('chat-timeline-slider')).toBeVisible(); + }); +}); diff --git a/cockpit/chat/timeline/angular/project.json b/cockpit/chat/timeline/angular/project.json new file mode 100644 index 000000000..f3705700d --- /dev/null +++ b/cockpit/chat/timeline/angular/project.json @@ -0,0 +1,61 @@ +{ + "name": "cockpit-chat-timeline-angular", + "$schema": "../../../../node_modules/nx/schemas/project-schema.json", + "sourceRoot": "cockpit/chat/timeline/angular/src", + "projectType": "application", + "targets": { + "build": { + "executor": "@angular/build:application", + "outputs": ["{options.outputPath.base}"], + "options": { + "outputPath": { + "base": "dist/cockpit/chat/timeline/angular", + "browser": "" + }, + "browser": "cockpit/chat/timeline/angular/src/main.ts", + "tsConfig": "cockpit/chat/timeline/angular/tsconfig.app.json", + "styles": ["cockpit/chat/timeline/angular/src/styles.css"] + }, + "configurations": { + "production": { + "budgets": [ + { "type": "initial", "maximumWarning": "500kb", "maximumError": "1mb" }, + { "type": "anyComponentStyle", "maximumWarning": "4kb", "maximumError": "8kb" } + ], + "outputHashing": "none" + }, + "development": { + "optimization": false, + "extractLicenses": false, + "sourceMap": true, + "fileReplacements": [ + { + "replace": "cockpit/chat/timeline/angular/src/environments/environment.ts", + "with": "cockpit/chat/timeline/angular/src/environments/environment.development.ts" + } + ] + } + }, + "defaultConfiguration": "production" + }, + "serve": { + "continuous": true, + "executor": "@angular/build:dev-server", + "configurations": { + "production": { "buildTarget": "cockpit-chat-timeline-angular:build:production" }, + "development": { "buildTarget": "cockpit-chat-timeline-angular:build:development" } + }, + "defaultConfiguration": "development", + "options": { + "proxyConfig": "cockpit/chat/timeline/angular/proxy.conf.json" + } + }, + "smoke": { + "executor": "nx:run-commands", + "options": { + "cwd": "cockpit/chat/timeline/angular", + "command": "npx tsx -e \"import { chatTimelineAngularModule } from './src/index.ts'; const module = chatTimelineAngularModule; if (module.id !== 'chat-timeline-angular' || module.title !== 'Chat Timeline (Angular)') { throw new Error('Unexpected module shape for ' + module.id); }\"" + } + } + } +} diff --git a/cockpit/chat/timeline/angular/proxy.conf.json b/cockpit/chat/timeline/angular/proxy.conf.json new file mode 100644 index 000000000..8523362d7 --- /dev/null +++ b/cockpit/chat/timeline/angular/proxy.conf.json @@ -0,0 +1,9 @@ +{ + "/api": { + "target": "http://localhost:8123", + "secure": false, + "changeOrigin": true, + "pathRewrite": { "^/api": "" }, + "ws": true + } +} diff --git a/cockpit/chat/timeline/angular/src/app/app.config.ts b/cockpit/chat/timeline/angular/src/app/app.config.ts new file mode 100644 index 000000000..b52a916ac --- /dev/null +++ b/cockpit/chat/timeline/angular/src/app/app.config.ts @@ -0,0 +1,12 @@ +// SPDX-License-Identifier: PolyForm-Noncommercial-1.0.0 +import { ApplicationConfig } from '@angular/core'; +import { provideStreamResource } from '@cacheplane/stream-resource'; +import { provideChat } from '@cacheplane/chat'; +import { environment } from '../environments/environment'; + +export const appConfig: ApplicationConfig = { + providers: [ + provideStreamResource({ apiUrl: environment.langGraphApiUrl }), + provideChat({}), + ], +}; diff --git a/cockpit/chat/timeline/angular/src/app/timeline.component.ts b/cockpit/chat/timeline/angular/src/app/timeline.component.ts new file mode 100644 index 000000000..47de8c02f --- /dev/null +++ b/cockpit/chat/timeline/angular/src/app/timeline.component.ts @@ -0,0 +1,41 @@ +// SPDX-License-Identifier: PolyForm-Noncommercial-1.0.0 +import { Component } from '@angular/core'; +import { ChatComponent, ChatTimelineSliderComponent } from '@cacheplane/chat'; +import { streamResource } from '@cacheplane/stream-resource'; +import { environment } from '../environments/environment'; + +/** + * TimelineComponent demonstrates conversation timeline navigation + * with ChatComponent and ChatTimelineSliderComponent for scrubbing + * through conversation checkpoints. + */ +@Component({ + selector: 'app-timeline', + standalone: true, + imports: [ChatComponent, ChatTimelineSliderComponent], + template: ` +
+ + +
+ `, +}) +export class TimelineComponent { + protected readonly stream = streamResource({ + apiUrl: environment.langGraphApiUrl, + assistantId: environment.streamingAssistantId, + }); +} diff --git a/cockpit/chat/timeline/angular/src/environments/environment.development.ts b/cockpit/chat/timeline/angular/src/environments/environment.development.ts new file mode 100644 index 000000000..1ad681989 --- /dev/null +++ b/cockpit/chat/timeline/angular/src/environments/environment.development.ts @@ -0,0 +1,5 @@ +export const environment = { + production: false, + langGraphApiUrl: 'http://localhost:4507/api', + streamingAssistantId: 'c-timeline', +}; diff --git a/cockpit/chat/timeline/angular/src/environments/environment.ts b/cockpit/chat/timeline/angular/src/environments/environment.ts new file mode 100644 index 000000000..ec0bb715d --- /dev/null +++ b/cockpit/chat/timeline/angular/src/environments/environment.ts @@ -0,0 +1,5 @@ +export const environment = { + production: true, + langGraphApiUrl: '/api', + streamingAssistantId: 'c-timeline', +}; diff --git a/cockpit/chat/timeline/angular/src/index.html b/cockpit/chat/timeline/angular/src/index.html new file mode 100644 index 000000000..860eb68eb --- /dev/null +++ b/cockpit/chat/timeline/angular/src/index.html @@ -0,0 +1,13 @@ + + + + + Chat Timeline — Angular + + + + + + + + diff --git a/cockpit/chat/timeline/angular/src/index.ts b/cockpit/chat/timeline/angular/src/index.ts new file mode 100644 index 000000000..bafe0e47e --- /dev/null +++ b/cockpit/chat/timeline/angular/src/index.ts @@ -0,0 +1,29 @@ +export interface CockpitCapabilityModule { + id: string; + manifestIdentity: { + product: 'chat'; + section: 'core-capabilities'; + topic: 'timeline'; + page: 'overview'; + language: 'angular'; + }; + title: string; + docsPath: string; + promptAssetPaths: string[]; + codeAssetPaths: string[]; +} + +export const chatTimelineAngularModule: CockpitCapabilityModule = { + id: 'chat-timeline-angular', + manifestIdentity: { + product: 'chat', + section: 'core-capabilities', + topic: 'timeline', + page: 'overview', + language: 'angular', + }, + title: 'Chat Timeline (Angular)', + docsPath: '/docs/chat/core-capabilities/timeline/overview/angular', + promptAssetPaths: ['cockpit/chat/timeline/python/prompts/timeline.md'], + codeAssetPaths: ['cockpit/chat/timeline/angular/src/app/timeline.component.ts'], +}; diff --git a/cockpit/chat/timeline/angular/src/main.ts b/cockpit/chat/timeline/angular/src/main.ts new file mode 100644 index 000000000..c454999eb --- /dev/null +++ b/cockpit/chat/timeline/angular/src/main.ts @@ -0,0 +1,6 @@ +// SPDX-License-Identifier: PolyForm-Noncommercial-1.0.0 +import { bootstrapApplication } from '@angular/platform-browser'; +import { appConfig } from './app/app.config'; +import { TimelineComponent } from './app/timeline.component'; + +bootstrapApplication(TimelineComponent, appConfig).catch(console.error); diff --git a/cockpit/chat/timeline/angular/src/styles.css b/cockpit/chat/timeline/angular/src/styles.css new file mode 100644 index 000000000..bd63176c6 --- /dev/null +++ b/cockpit/chat/timeline/angular/src/styles.css @@ -0,0 +1 @@ +/* Global styles for the timeline capability demo */ diff --git a/cockpit/chat/timeline/angular/tsconfig.app.json b/cockpit/chat/timeline/angular/tsconfig.app.json new file mode 100644 index 000000000..72c01e364 --- /dev/null +++ b/cockpit/chat/timeline/angular/tsconfig.app.json @@ -0,0 +1,9 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../../../dist/out-tsc", + "types": [] + }, + "files": ["src/main.ts"], + "include": ["src/**/*.d.ts", "src/**/*.ts"] +} diff --git a/cockpit/chat/timeline/angular/tsconfig.json b/cockpit/chat/timeline/angular/tsconfig.json new file mode 100644 index 000000000..3fd970371 --- /dev/null +++ b/cockpit/chat/timeline/angular/tsconfig.json @@ -0,0 +1,24 @@ +{ + "extends": "../../../../tsconfig.base.json", + "compilerOptions": { + "noPropertyAccessFromIndexSignature": false, + "experimentalDecorators": true, + "module": "preserve", + "emitDeclarationOnly": false, + "composite": false, + "lib": ["es2022", "dom"], + "skipLibCheck": true, + "strict": false + }, + "angularCompilerOptions": { + "enableI18nLegacyMessageIdFormat": false, + "strictInjectionParameters": false, + "strictInputAccessModifiers": false, + "strictTemplates": false + }, + "files": [], + "include": [], + "references": [ + { "path": "./tsconfig.app.json" } + ] +} diff --git a/cockpit/chat/timeline/python/docs/guide.md b/cockpit/chat/timeline/python/docs/guide.md new file mode 100644 index 000000000..40c62d4b9 --- /dev/null +++ b/cockpit/chat/timeline/python/docs/guide.md @@ -0,0 +1,67 @@ +# Chat Timeline with @cacheplane/chat + + +Navigate conversation history using ChatTimelineSliderComponent. +Each exchange creates a checkpoint that users can scrub through, +enabling time-travel debugging and conversation branching. + + + +Add timeline navigation to your chat interface using +`ChatTimelineSliderComponent` from `@cacheplane/chat`. Enable users +to navigate checkpoints and branch from previous conversation states. + + + + + +History tracking is built into `streamResource()`. Each message exchange +creates a checkpoint automatically: + +```typescript +protected readonly stream = streamResource({ + apiUrl: environment.langGraphApiUrl, + assistantId: environment.streamingAssistantId, +}); +``` + + + + +Use `ChatTimelineSliderComponent` to display a scrubber for +navigating conversation checkpoints: + +```html + +``` + + + + +The slider allows users to scrub through conversation history. +Each position corresponds to a graph checkpoint with its full state. + + + + +Position the slider below the chat or in a sidebar for easy access: + +```html + +``` + + + + +Users can branch from any checkpoint to explore alternative +conversation paths. The timeline tracks all branches. + + + + + +Timeline navigation is especially useful for debugging agent behavior +and understanding how the conversation state evolved over time. + diff --git a/cockpit/chat/timeline/python/langgraph.json b/cockpit/chat/timeline/python/langgraph.json new file mode 100644 index 000000000..330a954a2 --- /dev/null +++ b/cockpit/chat/timeline/python/langgraph.json @@ -0,0 +1,8 @@ +{ + "graphs": { + "c-timeline": "./src/graph.py:graph" + }, + "dependencies": ["."], + "python_version": "3.12", + "env": ".env" +} diff --git a/cockpit/chat/timeline/python/prompts/timeline.md b/cockpit/chat/timeline/python/prompts/timeline.md new file mode 100644 index 000000000..2eaaaca16 --- /dev/null +++ b/cockpit/chat/timeline/python/prompts/timeline.md @@ -0,0 +1,11 @@ +# Chat Timeline Assistant + +You are an assistant that demonstrates conversation timeline and +checkpoint navigation using stream-resource. + +Each message exchange creates a checkpoint in the conversation timeline. +Users can navigate backward and forward through these checkpoints using +the timeline slider, and even branch from a previous checkpoint to +explore alternative conversation paths. + +Respond helpfully to demonstrate how checkpoints accumulate over time. diff --git a/cockpit/chat/timeline/python/pyproject.toml b/cockpit/chat/timeline/python/pyproject.toml new file mode 100644 index 000000000..30e1d5b63 --- /dev/null +++ b/cockpit/chat/timeline/python/pyproject.toml @@ -0,0 +1,21 @@ +[project] +name = "cockpit-chat-timeline" +version = "0.1.0" +requires-python = ">=3.12" +dependencies = [ + "langgraph>=0.3", + "langchain-openai>=0.3", + "langsmith>=0.2", +] + +[tool.uv] +dev-dependencies = [ + "langgraph-cli[inmem]>=0.1", +] + +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[tool.hatch.build.targets.wheel] +packages = ["src"] diff --git a/cockpit/chat/timeline/python/src/graph.py b/cockpit/chat/timeline/python/src/graph.py new file mode 100644 index 000000000..40d552d27 --- /dev/null +++ b/cockpit/chat/timeline/python/src/graph.py @@ -0,0 +1,37 @@ +""" +Chat Timeline Graph + +A standard conversational agent. Timeline and checkpoint navigation +is managed by stream-resource on the frontend side. +""" + +from pathlib import Path +from langgraph.graph import StateGraph, MessagesState, END +from langchain_openai import ChatOpenAI +from langchain_core.messages import SystemMessage + +PROMPTS_DIR = Path(__file__).parent.parent / "prompts" + + +def build_timeline_graph(): + """ + Constructs a standard conversational agent. + Timeline/history navigation is handled by the stream-resource frontend. + """ + llm = ChatOpenAI(model="gpt-5-mini", streaming=True) + + async def generate(state: MessagesState) -> dict: + system_prompt = (PROMPTS_DIR / "timeline.md").read_text() + messages = [SystemMessage(content=system_prompt)] + state["messages"] + response = await llm.ainvoke(messages) + return {"messages": [response]} + + graph = StateGraph(MessagesState) + graph.add_node("generate", generate) + graph.set_entry_point("generate") + graph.add_edge("generate", END) + + return graph.compile() + + +graph = build_timeline_graph() diff --git a/cockpit/chat/timeline/python/src/index.ts b/cockpit/chat/timeline/python/src/index.ts new file mode 100644 index 000000000..8c5a8825a --- /dev/null +++ b/cockpit/chat/timeline/python/src/index.ts @@ -0,0 +1,42 @@ +export interface CockpitCapabilityModule { + id: string; + manifestIdentity: { + product: 'chat'; + section: 'core-capabilities'; + topic: 'timeline'; + page: 'overview'; + language: 'python'; + }; + title: string; + docsPath: string; + promptAssetPaths: string[]; + codeAssetPaths: string[]; + backendAssetPaths: string[]; + docsAssetPaths: string[]; + runtimeUrl?: string; + devPort?: number; +} + +export const chatTimelinePythonModule: CockpitCapabilityModule = { + id: 'chat-timeline-python', + manifestIdentity: { + product: 'chat', + section: 'core-capabilities', + topic: 'timeline', + page: 'overview', + language: 'python', + }, + title: 'Chat Timeline (Python)', + docsPath: '/docs/chat/core-capabilities/timeline/overview/python', + promptAssetPaths: ['cockpit/chat/timeline/python/prompts/timeline.md'], + codeAssetPaths: [ + 'cockpit/chat/timeline/angular/src/app/timeline.component.ts', + 'cockpit/chat/timeline/angular/src/app/app.config.ts', + ], + backendAssetPaths: [ + 'cockpit/chat/timeline/python/src/graph.py', + ], + docsAssetPaths: ['cockpit/chat/timeline/python/docs/guide.md'], + runtimeUrl: 'chat/timeline', + devPort: 4507, +}; From 95137d90bc6e0743c2f16d290cc154742ca211ba Mon Sep 17 00:00:00 2001 From: Brian Love Date: Wed, 8 Apr 2026 10:23:43 -0700 Subject: [PATCH 14/48] feat(cockpit): add chat/generative-ui capability Adds the generative-ui capability demonstrating dynamic UI generation within chat messages using ChatGenerativeUiComponent and provideRender. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../angular/e2e/generative-ui.spec.ts | 18 +++++ .../chat/generative-ui/angular/project.json | 61 +++++++++++++++ .../generative-ui/angular/proxy.conf.json | 9 +++ .../angular/src/app/app.config.ts | 14 ++++ .../src/app/generative-ui.component.ts | 42 ++++++++++ .../environments/environment.development.ts | 5 ++ .../angular/src/environments/environment.ts | 5 ++ .../chat/generative-ui/angular/src/index.html | 13 ++++ .../chat/generative-ui/angular/src/index.ts | 29 +++++++ .../chat/generative-ui/angular/src/main.ts | 6 ++ .../chat/generative-ui/angular/src/styles.css | 1 + .../generative-ui/angular/tsconfig.app.json | 9 +++ .../chat/generative-ui/angular/tsconfig.json | 24 ++++++ .../chat/generative-ui/python/docs/guide.md | 76 +++++++++++++++++++ .../chat/generative-ui/python/langgraph.json | 8 ++ .../python/prompts/generative-ui.md | 22 ++++++ .../chat/generative-ui/python/pyproject.toml | 21 +++++ .../chat/generative-ui/python/src/graph.py | 38 ++++++++++ .../chat/generative-ui/python/src/index.ts | 42 ++++++++++ 19 files changed, 443 insertions(+) create mode 100644 cockpit/chat/generative-ui/angular/e2e/generative-ui.spec.ts create mode 100644 cockpit/chat/generative-ui/angular/project.json create mode 100644 cockpit/chat/generative-ui/angular/proxy.conf.json create mode 100644 cockpit/chat/generative-ui/angular/src/app/app.config.ts create mode 100644 cockpit/chat/generative-ui/angular/src/app/generative-ui.component.ts create mode 100644 cockpit/chat/generative-ui/angular/src/environments/environment.development.ts create mode 100644 cockpit/chat/generative-ui/angular/src/environments/environment.ts create mode 100644 cockpit/chat/generative-ui/angular/src/index.html create mode 100644 cockpit/chat/generative-ui/angular/src/index.ts create mode 100644 cockpit/chat/generative-ui/angular/src/main.ts create mode 100644 cockpit/chat/generative-ui/angular/src/styles.css create mode 100644 cockpit/chat/generative-ui/angular/tsconfig.app.json create mode 100644 cockpit/chat/generative-ui/angular/tsconfig.json create mode 100644 cockpit/chat/generative-ui/python/docs/guide.md create mode 100644 cockpit/chat/generative-ui/python/langgraph.json create mode 100644 cockpit/chat/generative-ui/python/prompts/generative-ui.md create mode 100644 cockpit/chat/generative-ui/python/pyproject.toml create mode 100644 cockpit/chat/generative-ui/python/src/graph.py create mode 100644 cockpit/chat/generative-ui/python/src/index.ts diff --git a/cockpit/chat/generative-ui/angular/e2e/generative-ui.spec.ts b/cockpit/chat/generative-ui/angular/e2e/generative-ui.spec.ts new file mode 100644 index 000000000..68302174b --- /dev/null +++ b/cockpit/chat/generative-ui/angular/e2e/generative-ui.spec.ts @@ -0,0 +1,18 @@ +import { expect, test } from '@playwright/test'; + +test.describe('Chat Generative UI Example', () => { + test.beforeEach(async ({ page }) => { + await page.goto('http://localhost:4508'); + await page.waitForSelector('app-generative-ui', { state: 'attached' }); + }); + + test('renders the chat interface with generative UI sidebar', async ({ page }) => { + await expect(page.locator('chat')).toBeVisible(); + await expect(page.locator('aside')).toBeVisible(); + await expect(page.locator('aside h3')).toHaveText('Generative UI'); + }); + + test('displays how it works description', async ({ page }) => { + await expect(page.locator('aside')).toContainText('render specs'); + }); +}); diff --git a/cockpit/chat/generative-ui/angular/project.json b/cockpit/chat/generative-ui/angular/project.json new file mode 100644 index 000000000..fc7a87a51 --- /dev/null +++ b/cockpit/chat/generative-ui/angular/project.json @@ -0,0 +1,61 @@ +{ + "name": "cockpit-chat-generative-ui-angular", + "$schema": "../../../../node_modules/nx/schemas/project-schema.json", + "sourceRoot": "cockpit/chat/generative-ui/angular/src", + "projectType": "application", + "targets": { + "build": { + "executor": "@angular/build:application", + "outputs": ["{options.outputPath.base}"], + "options": { + "outputPath": { + "base": "dist/cockpit/chat/generative-ui/angular", + "browser": "" + }, + "browser": "cockpit/chat/generative-ui/angular/src/main.ts", + "tsConfig": "cockpit/chat/generative-ui/angular/tsconfig.app.json", + "styles": ["cockpit/chat/generative-ui/angular/src/styles.css"] + }, + "configurations": { + "production": { + "budgets": [ + { "type": "initial", "maximumWarning": "500kb", "maximumError": "1mb" }, + { "type": "anyComponentStyle", "maximumWarning": "4kb", "maximumError": "8kb" } + ], + "outputHashing": "none" + }, + "development": { + "optimization": false, + "extractLicenses": false, + "sourceMap": true, + "fileReplacements": [ + { + "replace": "cockpit/chat/generative-ui/angular/src/environments/environment.ts", + "with": "cockpit/chat/generative-ui/angular/src/environments/environment.development.ts" + } + ] + } + }, + "defaultConfiguration": "production" + }, + "serve": { + "continuous": true, + "executor": "@angular/build:dev-server", + "configurations": { + "production": { "buildTarget": "cockpit-chat-generative-ui-angular:build:production" }, + "development": { "buildTarget": "cockpit-chat-generative-ui-angular:build:development" } + }, + "defaultConfiguration": "development", + "options": { + "proxyConfig": "cockpit/chat/generative-ui/angular/proxy.conf.json" + } + }, + "smoke": { + "executor": "nx:run-commands", + "options": { + "cwd": "cockpit/chat/generative-ui/angular", + "command": "npx tsx -e \"import { chatGenerativeUiAngularModule } from './src/index.ts'; const module = chatGenerativeUiAngularModule; if (module.id !== 'chat-generative-ui-angular' || module.title !== 'Chat Generative UI (Angular)') { throw new Error('Unexpected module shape for ' + module.id); }\"" + } + } + } +} diff --git a/cockpit/chat/generative-ui/angular/proxy.conf.json b/cockpit/chat/generative-ui/angular/proxy.conf.json new file mode 100644 index 000000000..8523362d7 --- /dev/null +++ b/cockpit/chat/generative-ui/angular/proxy.conf.json @@ -0,0 +1,9 @@ +{ + "/api": { + "target": "http://localhost:8123", + "secure": false, + "changeOrigin": true, + "pathRewrite": { "^/api": "" }, + "ws": true + } +} diff --git a/cockpit/chat/generative-ui/angular/src/app/app.config.ts b/cockpit/chat/generative-ui/angular/src/app/app.config.ts new file mode 100644 index 000000000..08e9180d0 --- /dev/null +++ b/cockpit/chat/generative-ui/angular/src/app/app.config.ts @@ -0,0 +1,14 @@ +// SPDX-License-Identifier: PolyForm-Noncommercial-1.0.0 +import { ApplicationConfig } from '@angular/core'; +import { provideStreamResource } from '@cacheplane/stream-resource'; +import { provideChat } from '@cacheplane/chat'; +import { provideRender } from '@cacheplane/render'; +import { environment } from '../environments/environment'; + +export const appConfig: ApplicationConfig = { + providers: [ + provideStreamResource({ apiUrl: environment.langGraphApiUrl }), + provideChat({}), + provideRender({}), + ], +}; diff --git a/cockpit/chat/generative-ui/angular/src/app/generative-ui.component.ts b/cockpit/chat/generative-ui/angular/src/app/generative-ui.component.ts new file mode 100644 index 000000000..c5355687c --- /dev/null +++ b/cockpit/chat/generative-ui/angular/src/app/generative-ui.component.ts @@ -0,0 +1,42 @@ +// SPDX-License-Identifier: PolyForm-Noncommercial-1.0.0 +import { Component } from '@angular/core'; +import { ChatComponent, ChatGenerativeUiComponent } from '@cacheplane/chat'; +import { streamResource } from '@cacheplane/stream-resource'; +import { environment } from '../environments/environment'; + +/** + * GenerativeUiComponent demonstrates dynamic UI generation within + * chat messages using ChatComponent and ChatGenerativeUiComponent. + * The agent embeds render specs that are rendered as live components. + */ +@Component({ + selector: 'app-generative-ui', + standalone: true, + imports: [ChatComponent, ChatGenerativeUiComponent], + template: ` +
+ + +
+ `, +}) +export class GenerativeUiComponent { + protected readonly stream = streamResource({ + apiUrl: environment.langGraphApiUrl, + assistantId: environment.streamingAssistantId, + }); +} diff --git a/cockpit/chat/generative-ui/angular/src/environments/environment.development.ts b/cockpit/chat/generative-ui/angular/src/environments/environment.development.ts new file mode 100644 index 000000000..18c14185d --- /dev/null +++ b/cockpit/chat/generative-ui/angular/src/environments/environment.development.ts @@ -0,0 +1,5 @@ +export const environment = { + production: false, + langGraphApiUrl: 'http://localhost:4508/api', + streamingAssistantId: 'c-generative-ui', +}; diff --git a/cockpit/chat/generative-ui/angular/src/environments/environment.ts b/cockpit/chat/generative-ui/angular/src/environments/environment.ts new file mode 100644 index 000000000..2c727e253 --- /dev/null +++ b/cockpit/chat/generative-ui/angular/src/environments/environment.ts @@ -0,0 +1,5 @@ +export const environment = { + production: true, + langGraphApiUrl: '/api', + streamingAssistantId: 'c-generative-ui', +}; diff --git a/cockpit/chat/generative-ui/angular/src/index.html b/cockpit/chat/generative-ui/angular/src/index.html new file mode 100644 index 000000000..8123c3b27 --- /dev/null +++ b/cockpit/chat/generative-ui/angular/src/index.html @@ -0,0 +1,13 @@ + + + + + Chat Generative UI — Angular + + + + + + + + diff --git a/cockpit/chat/generative-ui/angular/src/index.ts b/cockpit/chat/generative-ui/angular/src/index.ts new file mode 100644 index 000000000..a297928bd --- /dev/null +++ b/cockpit/chat/generative-ui/angular/src/index.ts @@ -0,0 +1,29 @@ +export interface CockpitCapabilityModule { + id: string; + manifestIdentity: { + product: 'chat'; + section: 'core-capabilities'; + topic: 'generative-ui'; + page: 'overview'; + language: 'angular'; + }; + title: string; + docsPath: string; + promptAssetPaths: string[]; + codeAssetPaths: string[]; +} + +export const chatGenerativeUiAngularModule: CockpitCapabilityModule = { + id: 'chat-generative-ui-angular', + manifestIdentity: { + product: 'chat', + section: 'core-capabilities', + topic: 'generative-ui', + page: 'overview', + language: 'angular', + }, + title: 'Chat Generative UI (Angular)', + docsPath: '/docs/chat/core-capabilities/generative-ui/overview/angular', + promptAssetPaths: ['cockpit/chat/generative-ui/python/prompts/generative-ui.md'], + codeAssetPaths: ['cockpit/chat/generative-ui/angular/src/app/generative-ui.component.ts'], +}; diff --git a/cockpit/chat/generative-ui/angular/src/main.ts b/cockpit/chat/generative-ui/angular/src/main.ts new file mode 100644 index 000000000..862ebbc69 --- /dev/null +++ b/cockpit/chat/generative-ui/angular/src/main.ts @@ -0,0 +1,6 @@ +// SPDX-License-Identifier: PolyForm-Noncommercial-1.0.0 +import { bootstrapApplication } from '@angular/platform-browser'; +import { appConfig } from './app/app.config'; +import { GenerativeUiComponent } from './app/generative-ui.component'; + +bootstrapApplication(GenerativeUiComponent, appConfig).catch(console.error); diff --git a/cockpit/chat/generative-ui/angular/src/styles.css b/cockpit/chat/generative-ui/angular/src/styles.css new file mode 100644 index 000000000..8b4b3f531 --- /dev/null +++ b/cockpit/chat/generative-ui/angular/src/styles.css @@ -0,0 +1 @@ +/* Global styles for the generative-ui capability demo */ diff --git a/cockpit/chat/generative-ui/angular/tsconfig.app.json b/cockpit/chat/generative-ui/angular/tsconfig.app.json new file mode 100644 index 000000000..72c01e364 --- /dev/null +++ b/cockpit/chat/generative-ui/angular/tsconfig.app.json @@ -0,0 +1,9 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../../../dist/out-tsc", + "types": [] + }, + "files": ["src/main.ts"], + "include": ["src/**/*.d.ts", "src/**/*.ts"] +} diff --git a/cockpit/chat/generative-ui/angular/tsconfig.json b/cockpit/chat/generative-ui/angular/tsconfig.json new file mode 100644 index 000000000..3fd970371 --- /dev/null +++ b/cockpit/chat/generative-ui/angular/tsconfig.json @@ -0,0 +1,24 @@ +{ + "extends": "../../../../tsconfig.base.json", + "compilerOptions": { + "noPropertyAccessFromIndexSignature": false, + "experimentalDecorators": true, + "module": "preserve", + "emitDeclarationOnly": false, + "composite": false, + "lib": ["es2022", "dom"], + "skipLibCheck": true, + "strict": false + }, + "angularCompilerOptions": { + "enableI18nLegacyMessageIdFormat": false, + "strictInjectionParameters": false, + "strictInputAccessModifiers": false, + "strictTemplates": false + }, + "files": [], + "include": [], + "references": [ + { "path": "./tsconfig.app.json" } + ] +} diff --git a/cockpit/chat/generative-ui/python/docs/guide.md b/cockpit/chat/generative-ui/python/docs/guide.md new file mode 100644 index 000000000..92f2ba78c --- /dev/null +++ b/cockpit/chat/generative-ui/python/docs/guide.md @@ -0,0 +1,76 @@ +# Chat Generative UI with @cacheplane/chat + + +Render dynamic UI components within chat messages using +ChatGenerativeUiComponent. The agent embeds JSON render specs +in responses that are rendered as live Angular components. + + + +Add generative UI to your chat interface using `ChatGenerativeUiComponent` +from `@cacheplane/chat` and `provideRender()` from `@cacheplane/render`. +Configure both providers to enable spec detection and rendering. + + + + + +Generative UI requires both `provideChat()` and `provideRender()`: + +```typescript +import { provideRender } from '@cacheplane/render'; +import { provideChat } from '@cacheplane/chat'; + +export const appConfig: ApplicationConfig = { + providers: [ + provideStreamResource({ apiUrl: environment.langGraphApiUrl }), + provideChat({}), + provideRender({}), + ], +}; +``` + + + + +Configure the backend agent to include JSON render specs in its +responses using fenced code blocks with the `render-spec` tag. + + + + +ChatGenerativeUiComponent automatically scans messages for render +spec code blocks and extracts them for rendering. + + + + +Use the component in your template alongside ChatComponent: + +```html + + +``` + + + + +Register custom Angular components to handle specific spec types: + +```typescript +provideRender({ + registry: defineAngularRegistry({ + card: MyCardComponent, + chart: MyChartComponent, + }), +}) +``` + + + + + +Generative UI bridges the gap between conversational AI and rich +interactive interfaces — the agent can create forms, dashboards, +and visualizations on the fly. + diff --git a/cockpit/chat/generative-ui/python/langgraph.json b/cockpit/chat/generative-ui/python/langgraph.json new file mode 100644 index 000000000..583df221b --- /dev/null +++ b/cockpit/chat/generative-ui/python/langgraph.json @@ -0,0 +1,8 @@ +{ + "graphs": { + "c-generative-ui": "./src/graph.py:graph" + }, + "dependencies": ["."], + "python_version": "3.12", + "env": ".env" +} diff --git a/cockpit/chat/generative-ui/python/prompts/generative-ui.md b/cockpit/chat/generative-ui/python/prompts/generative-ui.md new file mode 100644 index 000000000..a1991a980 --- /dev/null +++ b/cockpit/chat/generative-ui/python/prompts/generative-ui.md @@ -0,0 +1,22 @@ +# Chat Generative UI Assistant + +You are an assistant that demonstrates dynamic UI generation within +chat responses using render specs. + +When the user asks you to create a UI element, include a JSON render spec +in your response using a fenced code block with the `render-spec` language tag. +For example: + +```render-spec +{ + "type": "card", + "props": { "title": "Generated Card" }, + "children": [ + { "type": "text", "props": { "content": "This card was generated by the AI" } } + ] +} +``` + +The frontend will detect these specs and render them as live Angular +components inline within the chat message. Explain what you are generating +and why in the surrounding text. diff --git a/cockpit/chat/generative-ui/python/pyproject.toml b/cockpit/chat/generative-ui/python/pyproject.toml new file mode 100644 index 000000000..1f934f820 --- /dev/null +++ b/cockpit/chat/generative-ui/python/pyproject.toml @@ -0,0 +1,21 @@ +[project] +name = "cockpit-chat-generative-ui" +version = "0.1.0" +requires-python = ">=3.12" +dependencies = [ + "langgraph>=0.3", + "langchain-openai>=0.3", + "langsmith>=0.2", +] + +[tool.uv] +dev-dependencies = [ + "langgraph-cli[inmem]>=0.1", +] + +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[tool.hatch.build.targets.wheel] +packages = ["src"] diff --git a/cockpit/chat/generative-ui/python/src/graph.py b/cockpit/chat/generative-ui/python/src/graph.py new file mode 100644 index 000000000..bdeedce3d --- /dev/null +++ b/cockpit/chat/generative-ui/python/src/graph.py @@ -0,0 +1,38 @@ +""" +Chat Generative UI Graph + +A LangGraph StateGraph that generates responses containing JSON render +spec objects. The Angular frontend detects these specs in chat messages +and renders them as live UI components using ChatGenerativeUiComponent. +""" + +from pathlib import Path +from langgraph.graph import StateGraph, MessagesState, END +from langchain_openai import ChatOpenAI +from langchain_core.messages import SystemMessage + +PROMPTS_DIR = Path(__file__).parent.parent / "prompts" + + +def build_generative_ui_graph(): + """ + Constructs an agent that includes JSON render specs in its responses, + enabling dynamic UI generation within chat messages. + """ + llm = ChatOpenAI(model="gpt-5-mini", streaming=True) + + async def generate(state: MessagesState) -> dict: + system_prompt = (PROMPTS_DIR / "generative-ui.md").read_text() + messages = [SystemMessage(content=system_prompt)] + state["messages"] + response = await llm.ainvoke(messages) + return {"messages": [response]} + + graph = StateGraph(MessagesState) + graph.add_node("generate", generate) + graph.set_entry_point("generate") + graph.add_edge("generate", END) + + return graph.compile() + + +graph = build_generative_ui_graph() diff --git a/cockpit/chat/generative-ui/python/src/index.ts b/cockpit/chat/generative-ui/python/src/index.ts new file mode 100644 index 000000000..9e9a2fd3b --- /dev/null +++ b/cockpit/chat/generative-ui/python/src/index.ts @@ -0,0 +1,42 @@ +export interface CockpitCapabilityModule { + id: string; + manifestIdentity: { + product: 'chat'; + section: 'core-capabilities'; + topic: 'generative-ui'; + page: 'overview'; + language: 'python'; + }; + title: string; + docsPath: string; + promptAssetPaths: string[]; + codeAssetPaths: string[]; + backendAssetPaths: string[]; + docsAssetPaths: string[]; + runtimeUrl?: string; + devPort?: number; +} + +export const chatGenerativeUiPythonModule: CockpitCapabilityModule = { + id: 'chat-generative-ui-python', + manifestIdentity: { + product: 'chat', + section: 'core-capabilities', + topic: 'generative-ui', + page: 'overview', + language: 'python', + }, + title: 'Chat Generative UI (Python)', + docsPath: '/docs/chat/core-capabilities/generative-ui/overview/python', + promptAssetPaths: ['cockpit/chat/generative-ui/python/prompts/generative-ui.md'], + codeAssetPaths: [ + 'cockpit/chat/generative-ui/angular/src/app/generative-ui.component.ts', + 'cockpit/chat/generative-ui/angular/src/app/app.config.ts', + ], + backendAssetPaths: [ + 'cockpit/chat/generative-ui/python/src/graph.py', + ], + docsAssetPaths: ['cockpit/chat/generative-ui/python/docs/guide.md'], + runtimeUrl: 'chat/generative-ui', + devPort: 4508, +}; From a522ece0a8f0eef9bb287575945ae7ff215c8be6 Mon Sep 17 00:00:00 2001 From: Brian Love Date: Wed, 8 Apr 2026 10:25:11 -0700 Subject: [PATCH 15/48] feat(cockpit): add chat/debug capability Adds the debug capability using ChatDebugComponent with a multi-step graph (generate/process/summarize) for rich state inspection, diffs, and timeline debugging. Co-Authored-By: Claude Opus 4.6 (1M context) --- cockpit/chat/debug/angular/e2e/debug.spec.ts | 12 ++++ cockpit/chat/debug/angular/project.json | 61 ++++++++++++++++++ cockpit/chat/debug/angular/proxy.conf.json | 9 +++ .../chat/debug/angular/src/app/app.config.ts | 12 ++++ .../debug/angular/src/app/debug.component.ts | 27 ++++++++ .../environments/environment.development.ts | 5 ++ .../angular/src/environments/environment.ts | 5 ++ cockpit/chat/debug/angular/src/index.html | 13 ++++ cockpit/chat/debug/angular/src/index.ts | 29 +++++++++ cockpit/chat/debug/angular/src/main.ts | 6 ++ cockpit/chat/debug/angular/src/styles.css | 1 + cockpit/chat/debug/angular/tsconfig.app.json | 9 +++ cockpit/chat/debug/angular/tsconfig.json | 24 +++++++ cockpit/chat/debug/python/docs/guide.md | 63 +++++++++++++++++++ cockpit/chat/debug/python/langgraph.json | 8 +++ cockpit/chat/debug/python/prompts/debug.md | 13 ++++ cockpit/chat/debug/python/pyproject.toml | 21 +++++++ cockpit/chat/debug/python/src/graph.py | 57 +++++++++++++++++ cockpit/chat/debug/python/src/index.ts | 42 +++++++++++++ 19 files changed, 417 insertions(+) create mode 100644 cockpit/chat/debug/angular/e2e/debug.spec.ts create mode 100644 cockpit/chat/debug/angular/project.json create mode 100644 cockpit/chat/debug/angular/proxy.conf.json create mode 100644 cockpit/chat/debug/angular/src/app/app.config.ts create mode 100644 cockpit/chat/debug/angular/src/app/debug.component.ts create mode 100644 cockpit/chat/debug/angular/src/environments/environment.development.ts create mode 100644 cockpit/chat/debug/angular/src/environments/environment.ts create mode 100644 cockpit/chat/debug/angular/src/index.html create mode 100644 cockpit/chat/debug/angular/src/index.ts create mode 100644 cockpit/chat/debug/angular/src/main.ts create mode 100644 cockpit/chat/debug/angular/src/styles.css create mode 100644 cockpit/chat/debug/angular/tsconfig.app.json create mode 100644 cockpit/chat/debug/angular/tsconfig.json create mode 100644 cockpit/chat/debug/python/docs/guide.md create mode 100644 cockpit/chat/debug/python/langgraph.json create mode 100644 cockpit/chat/debug/python/prompts/debug.md create mode 100644 cockpit/chat/debug/python/pyproject.toml create mode 100644 cockpit/chat/debug/python/src/graph.py create mode 100644 cockpit/chat/debug/python/src/index.ts diff --git a/cockpit/chat/debug/angular/e2e/debug.spec.ts b/cockpit/chat/debug/angular/e2e/debug.spec.ts new file mode 100644 index 000000000..b1412f87f --- /dev/null +++ b/cockpit/chat/debug/angular/e2e/debug.spec.ts @@ -0,0 +1,12 @@ +import { expect, test } from '@playwright/test'; + +test.describe('Chat Debug Example', () => { + test.beforeEach(async ({ page }) => { + await page.goto('http://localhost:4509'); + await page.waitForSelector('app-debug', { state: 'attached' }); + }); + + test('renders the debug panel', async ({ page }) => { + await expect(page.locator('chat-debug')).toBeVisible(); + }); +}); diff --git a/cockpit/chat/debug/angular/project.json b/cockpit/chat/debug/angular/project.json new file mode 100644 index 000000000..ee6aa37c0 --- /dev/null +++ b/cockpit/chat/debug/angular/project.json @@ -0,0 +1,61 @@ +{ + "name": "cockpit-chat-debug-angular", + "$schema": "../../../../node_modules/nx/schemas/project-schema.json", + "sourceRoot": "cockpit/chat/debug/angular/src", + "projectType": "application", + "targets": { + "build": { + "executor": "@angular/build:application", + "outputs": ["{options.outputPath.base}"], + "options": { + "outputPath": { + "base": "dist/cockpit/chat/debug/angular", + "browser": "" + }, + "browser": "cockpit/chat/debug/angular/src/main.ts", + "tsConfig": "cockpit/chat/debug/angular/tsconfig.app.json", + "styles": ["cockpit/chat/debug/angular/src/styles.css"] + }, + "configurations": { + "production": { + "budgets": [ + { "type": "initial", "maximumWarning": "500kb", "maximumError": "1mb" }, + { "type": "anyComponentStyle", "maximumWarning": "4kb", "maximumError": "8kb" } + ], + "outputHashing": "none" + }, + "development": { + "optimization": false, + "extractLicenses": false, + "sourceMap": true, + "fileReplacements": [ + { + "replace": "cockpit/chat/debug/angular/src/environments/environment.ts", + "with": "cockpit/chat/debug/angular/src/environments/environment.development.ts" + } + ] + } + }, + "defaultConfiguration": "production" + }, + "serve": { + "continuous": true, + "executor": "@angular/build:dev-server", + "configurations": { + "production": { "buildTarget": "cockpit-chat-debug-angular:build:production" }, + "development": { "buildTarget": "cockpit-chat-debug-angular:build:development" } + }, + "defaultConfiguration": "development", + "options": { + "proxyConfig": "cockpit/chat/debug/angular/proxy.conf.json" + } + }, + "smoke": { + "executor": "nx:run-commands", + "options": { + "cwd": "cockpit/chat/debug/angular", + "command": "npx tsx -e \"import { chatDebugAngularModule } from './src/index.ts'; const module = chatDebugAngularModule; if (module.id !== 'chat-debug-angular' || module.title !== 'Chat Debug (Angular)') { throw new Error('Unexpected module shape for ' + module.id); }\"" + } + } + } +} diff --git a/cockpit/chat/debug/angular/proxy.conf.json b/cockpit/chat/debug/angular/proxy.conf.json new file mode 100644 index 000000000..8523362d7 --- /dev/null +++ b/cockpit/chat/debug/angular/proxy.conf.json @@ -0,0 +1,9 @@ +{ + "/api": { + "target": "http://localhost:8123", + "secure": false, + "changeOrigin": true, + "pathRewrite": { "^/api": "" }, + "ws": true + } +} diff --git a/cockpit/chat/debug/angular/src/app/app.config.ts b/cockpit/chat/debug/angular/src/app/app.config.ts new file mode 100644 index 000000000..b52a916ac --- /dev/null +++ b/cockpit/chat/debug/angular/src/app/app.config.ts @@ -0,0 +1,12 @@ +// SPDX-License-Identifier: PolyForm-Noncommercial-1.0.0 +import { ApplicationConfig } from '@angular/core'; +import { provideStreamResource } from '@cacheplane/stream-resource'; +import { provideChat } from '@cacheplane/chat'; +import { environment } from '../environments/environment'; + +export const appConfig: ApplicationConfig = { + providers: [ + provideStreamResource({ apiUrl: environment.langGraphApiUrl }), + provideChat({}), + ], +}; diff --git a/cockpit/chat/debug/angular/src/app/debug.component.ts b/cockpit/chat/debug/angular/src/app/debug.component.ts new file mode 100644 index 000000000..ca0813a3d --- /dev/null +++ b/cockpit/chat/debug/angular/src/app/debug.component.ts @@ -0,0 +1,27 @@ +// SPDX-License-Identifier: PolyForm-Noncommercial-1.0.0 +import { Component } from '@angular/core'; +import { ChatDebugComponent } from '@cacheplane/chat'; +import { streamResource } from '@cacheplane/stream-resource'; +import { environment } from '../environments/environment'; + +/** + * DebugComponent demonstrates the ChatDebugComponent which provides + * a full debug panel with timeline, state inspector, and diff viewer. + * Uses ChatDebugComponent instead of the standard ChatComponent. + */ +@Component({ + selector: 'app-debug', + standalone: true, + imports: [ChatDebugComponent], + template: ` +
+ +
+ `, +}) +export class DebugPageComponent { + protected readonly stream = streamResource({ + apiUrl: environment.langGraphApiUrl, + assistantId: environment.streamingAssistantId, + }); +} diff --git a/cockpit/chat/debug/angular/src/environments/environment.development.ts b/cockpit/chat/debug/angular/src/environments/environment.development.ts new file mode 100644 index 000000000..c419cc5d4 --- /dev/null +++ b/cockpit/chat/debug/angular/src/environments/environment.development.ts @@ -0,0 +1,5 @@ +export const environment = { + production: false, + langGraphApiUrl: 'http://localhost:4509/api', + streamingAssistantId: 'c-debug', +}; diff --git a/cockpit/chat/debug/angular/src/environments/environment.ts b/cockpit/chat/debug/angular/src/environments/environment.ts new file mode 100644 index 000000000..b21a057c8 --- /dev/null +++ b/cockpit/chat/debug/angular/src/environments/environment.ts @@ -0,0 +1,5 @@ +export const environment = { + production: true, + langGraphApiUrl: '/api', + streamingAssistantId: 'c-debug', +}; diff --git a/cockpit/chat/debug/angular/src/index.html b/cockpit/chat/debug/angular/src/index.html new file mode 100644 index 000000000..996c3026f --- /dev/null +++ b/cockpit/chat/debug/angular/src/index.html @@ -0,0 +1,13 @@ + + + + + Chat Debug — Angular + + + + + + + + diff --git a/cockpit/chat/debug/angular/src/index.ts b/cockpit/chat/debug/angular/src/index.ts new file mode 100644 index 000000000..a96761929 --- /dev/null +++ b/cockpit/chat/debug/angular/src/index.ts @@ -0,0 +1,29 @@ +export interface CockpitCapabilityModule { + id: string; + manifestIdentity: { + product: 'chat'; + section: 'core-capabilities'; + topic: 'debug'; + page: 'overview'; + language: 'angular'; + }; + title: string; + docsPath: string; + promptAssetPaths: string[]; + codeAssetPaths: string[]; +} + +export const chatDebugAngularModule: CockpitCapabilityModule = { + id: 'chat-debug-angular', + manifestIdentity: { + product: 'chat', + section: 'core-capabilities', + topic: 'debug', + page: 'overview', + language: 'angular', + }, + title: 'Chat Debug (Angular)', + docsPath: '/docs/chat/core-capabilities/debug/overview/angular', + promptAssetPaths: ['cockpit/chat/debug/python/prompts/debug.md'], + codeAssetPaths: ['cockpit/chat/debug/angular/src/app/debug.component.ts'], +}; diff --git a/cockpit/chat/debug/angular/src/main.ts b/cockpit/chat/debug/angular/src/main.ts new file mode 100644 index 000000000..a3f444612 --- /dev/null +++ b/cockpit/chat/debug/angular/src/main.ts @@ -0,0 +1,6 @@ +// SPDX-License-Identifier: PolyForm-Noncommercial-1.0.0 +import { bootstrapApplication } from '@angular/platform-browser'; +import { appConfig } from './app/app.config'; +import { DebugPageComponent } from './app/debug.component'; + +bootstrapApplication(DebugPageComponent, appConfig).catch(console.error); diff --git a/cockpit/chat/debug/angular/src/styles.css b/cockpit/chat/debug/angular/src/styles.css new file mode 100644 index 000000000..7260a5b65 --- /dev/null +++ b/cockpit/chat/debug/angular/src/styles.css @@ -0,0 +1 @@ +/* Global styles for the debug capability demo */ diff --git a/cockpit/chat/debug/angular/tsconfig.app.json b/cockpit/chat/debug/angular/tsconfig.app.json new file mode 100644 index 000000000..72c01e364 --- /dev/null +++ b/cockpit/chat/debug/angular/tsconfig.app.json @@ -0,0 +1,9 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../../../dist/out-tsc", + "types": [] + }, + "files": ["src/main.ts"], + "include": ["src/**/*.d.ts", "src/**/*.ts"] +} diff --git a/cockpit/chat/debug/angular/tsconfig.json b/cockpit/chat/debug/angular/tsconfig.json new file mode 100644 index 000000000..3fd970371 --- /dev/null +++ b/cockpit/chat/debug/angular/tsconfig.json @@ -0,0 +1,24 @@ +{ + "extends": "../../../../tsconfig.base.json", + "compilerOptions": { + "noPropertyAccessFromIndexSignature": false, + "experimentalDecorators": true, + "module": "preserve", + "emitDeclarationOnly": false, + "composite": false, + "lib": ["es2022", "dom"], + "skipLibCheck": true, + "strict": false + }, + "angularCompilerOptions": { + "enableI18nLegacyMessageIdFormat": false, + "strictInjectionParameters": false, + "strictInputAccessModifiers": false, + "strictTemplates": false + }, + "files": [], + "include": [], + "references": [ + { "path": "./tsconfig.app.json" } + ] +} diff --git a/cockpit/chat/debug/python/docs/guide.md b/cockpit/chat/debug/python/docs/guide.md new file mode 100644 index 000000000..b31d45a86 --- /dev/null +++ b/cockpit/chat/debug/python/docs/guide.md @@ -0,0 +1,63 @@ +# Chat Debug with @cacheplane/chat + + +Inspect conversation state, diffs, and graph execution using the +ChatDebugComponent. Provides a full debug panel with timeline, +state inspector, and diff viewer for development. + + + +Add a debug panel to your chat interface using `ChatDebugComponent` +from `@cacheplane/chat`. This replaces `ChatComponent` and provides +full development inspection capabilities. + + + + + +Use `ChatDebugComponent` instead of `ChatComponent` for the full +debug experience: + +```typescript +import { ChatDebugComponent } from '@cacheplane/chat'; +``` + + + + +Place the debug component in your template: + +```html + +``` + +This renders the full debug panel with timeline, state inspector, +and diff viewer. + + + + +The debug panel shows the current graph state at each checkpoint. +Click on any checkpoint to see the full state object and how it +changed from the previous step. + + + + +The diff viewer highlights what changed between consecutive +checkpoints, making it easy to understand how each node modifies +the conversation state. + + + + +The debug panel provides controls for stepping through execution, +replaying from checkpoints, and inspecting intermediate values. + + + + + +ChatDebugComponent is designed for development only. Use ChatComponent +in production for a polished end-user experience. + diff --git a/cockpit/chat/debug/python/langgraph.json b/cockpit/chat/debug/python/langgraph.json new file mode 100644 index 000000000..06efbbfd0 --- /dev/null +++ b/cockpit/chat/debug/python/langgraph.json @@ -0,0 +1,8 @@ +{ + "graphs": { + "c-debug": "./src/graph.py:graph" + }, + "dependencies": ["."], + "python_version": "3.12", + "env": ".env" +} diff --git a/cockpit/chat/debug/python/prompts/debug.md b/cockpit/chat/debug/python/prompts/debug.md new file mode 100644 index 000000000..368a5d96b --- /dev/null +++ b/cockpit/chat/debug/python/prompts/debug.md @@ -0,0 +1,13 @@ +# Chat Debug Assistant + +You are an assistant that demonstrates the ChatDebugComponent for +development inspection. + +Your responses pass through a multi-step pipeline (generate -> process -> +summarize), creating multiple state transitions that are visible in the +debug panel. Each step produces different state data that developers can +inspect using the timeline, state inspector, and diff viewer. + +Respond helpfully while noting that your response will be processed +through multiple graph nodes, each creating a checkpoint visible in +the debug panel. diff --git a/cockpit/chat/debug/python/pyproject.toml b/cockpit/chat/debug/python/pyproject.toml new file mode 100644 index 000000000..7caede06e --- /dev/null +++ b/cockpit/chat/debug/python/pyproject.toml @@ -0,0 +1,21 @@ +[project] +name = "cockpit-chat-debug" +version = "0.1.0" +requires-python = ">=3.12" +dependencies = [ + "langgraph>=0.3", + "langchain-openai>=0.3", + "langsmith>=0.2", +] + +[tool.uv] +dev-dependencies = [ + "langgraph-cli[inmem]>=0.1", +] + +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[tool.hatch.build.targets.wheel] +packages = ["src"] diff --git a/cockpit/chat/debug/python/src/graph.py b/cockpit/chat/debug/python/src/graph.py new file mode 100644 index 000000000..a67620c32 --- /dev/null +++ b/cockpit/chat/debug/python/src/graph.py @@ -0,0 +1,57 @@ +""" +Chat Debug Graph + +A multi-step agent (generate -> process -> summarize) that produces +interesting debug data for inspecting with the ChatDebugComponent. +Multiple nodes create rich state transitions for the debug panel. +""" + +from pathlib import Path +from langgraph.graph import StateGraph, MessagesState, END +from langchain_openai import ChatOpenAI +from langchain_core.messages import SystemMessage, AIMessage + +PROMPTS_DIR = Path(__file__).parent.parent / "prompts" + + +def build_debug_graph(): + """ + Constructs a multi-step graph with generate, process, and summarize + nodes to produce rich state transitions for debug inspection. + """ + llm = ChatOpenAI(model="gpt-5-mini", streaming=True) + + async def generate(state: MessagesState) -> dict: + system_prompt = (PROMPTS_DIR / "debug.md").read_text() + messages = [SystemMessage(content=system_prompt)] + state["messages"] + response = await llm.ainvoke(messages) + return {"messages": [response]} + + async def process(state: MessagesState) -> dict: + last = state["messages"][-1].content + processed = AIMessage( + content=f"[Processing] Analyzed {len(last)} characters. " + f"Found {last.count(' ') + 1} words. Processing complete." + ) + return {"messages": [processed]} + + async def summarize(state: MessagesState) -> dict: + messages = [ + SystemMessage(content="Provide a brief one-sentence summary of the conversation so far.") + ] + state["messages"] + response = await llm.ainvoke(messages) + return {"messages": [response]} + + graph = StateGraph(MessagesState) + graph.add_node("generate", generate) + graph.add_node("process", process) + graph.add_node("summarize", summarize) + graph.set_entry_point("generate") + graph.add_edge("generate", "process") + graph.add_edge("process", "summarize") + graph.add_edge("summarize", END) + + return graph.compile() + + +graph = build_debug_graph() diff --git a/cockpit/chat/debug/python/src/index.ts b/cockpit/chat/debug/python/src/index.ts new file mode 100644 index 000000000..f3d48c561 --- /dev/null +++ b/cockpit/chat/debug/python/src/index.ts @@ -0,0 +1,42 @@ +export interface CockpitCapabilityModule { + id: string; + manifestIdentity: { + product: 'chat'; + section: 'core-capabilities'; + topic: 'debug'; + page: 'overview'; + language: 'python'; + }; + title: string; + docsPath: string; + promptAssetPaths: string[]; + codeAssetPaths: string[]; + backendAssetPaths: string[]; + docsAssetPaths: string[]; + runtimeUrl?: string; + devPort?: number; +} + +export const chatDebugPythonModule: CockpitCapabilityModule = { + id: 'chat-debug-python', + manifestIdentity: { + product: 'chat', + section: 'core-capabilities', + topic: 'debug', + page: 'overview', + language: 'python', + }, + title: 'Chat Debug (Python)', + docsPath: '/docs/chat/core-capabilities/debug/overview/python', + promptAssetPaths: ['cockpit/chat/debug/python/prompts/debug.md'], + codeAssetPaths: [ + 'cockpit/chat/debug/angular/src/app/debug.component.ts', + 'cockpit/chat/debug/angular/src/app/app.config.ts', + ], + backendAssetPaths: [ + 'cockpit/chat/debug/python/src/graph.py', + ], + docsAssetPaths: ['cockpit/chat/debug/python/docs/guide.md'], + runtimeUrl: 'chat/debug', + devPort: 4509, +}; From fb57bcbea0a78013b7a9e3c56f7c47b611bf3a1b Mon Sep 17 00:00:00 2001 From: Brian Love Date: Wed, 8 Apr 2026 10:26:49 -0700 Subject: [PATCH 16/48] feat(cockpit): add chat/theming capability Adds the theming capability demonstrating CSS custom property customization with CHAT_THEME_STYLES and a runtime theme picker supporting dark, light, ocean, and forest presets. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../chat/theming/angular/e2e/theming.spec.ts | 27 +++++ cockpit/chat/theming/angular/project.json | 61 +++++++++++ cockpit/chat/theming/angular/proxy.conf.json | 9 ++ .../theming/angular/src/app/app.config.ts | 12 +++ .../angular/src/app/theming.component.ts | 102 ++++++++++++++++++ .../environments/environment.development.ts | 5 + .../angular/src/environments/environment.ts | 5 + cockpit/chat/theming/angular/src/index.html | 13 +++ cockpit/chat/theming/angular/src/index.ts | 29 +++++ cockpit/chat/theming/angular/src/main.ts | 6 ++ cockpit/chat/theming/angular/src/styles.css | 1 + .../chat/theming/angular/tsconfig.app.json | 9 ++ cockpit/chat/theming/angular/tsconfig.json | 24 +++++ cockpit/chat/theming/python/docs/guide.md | 82 ++++++++++++++ cockpit/chat/theming/python/langgraph.json | 8 ++ .../chat/theming/python/prompts/theming.md | 13 +++ cockpit/chat/theming/python/pyproject.toml | 21 ++++ cockpit/chat/theming/python/src/graph.py | 37 +++++++ cockpit/chat/theming/python/src/index.ts | 42 ++++++++ 19 files changed, 506 insertions(+) create mode 100644 cockpit/chat/theming/angular/e2e/theming.spec.ts create mode 100644 cockpit/chat/theming/angular/project.json create mode 100644 cockpit/chat/theming/angular/proxy.conf.json create mode 100644 cockpit/chat/theming/angular/src/app/app.config.ts create mode 100644 cockpit/chat/theming/angular/src/app/theming.component.ts create mode 100644 cockpit/chat/theming/angular/src/environments/environment.development.ts create mode 100644 cockpit/chat/theming/angular/src/environments/environment.ts create mode 100644 cockpit/chat/theming/angular/src/index.html create mode 100644 cockpit/chat/theming/angular/src/index.ts create mode 100644 cockpit/chat/theming/angular/src/main.ts create mode 100644 cockpit/chat/theming/angular/src/styles.css create mode 100644 cockpit/chat/theming/angular/tsconfig.app.json create mode 100644 cockpit/chat/theming/angular/tsconfig.json create mode 100644 cockpit/chat/theming/python/docs/guide.md create mode 100644 cockpit/chat/theming/python/langgraph.json create mode 100644 cockpit/chat/theming/python/prompts/theming.md create mode 100644 cockpit/chat/theming/python/pyproject.toml create mode 100644 cockpit/chat/theming/python/src/graph.py create mode 100644 cockpit/chat/theming/python/src/index.ts diff --git a/cockpit/chat/theming/angular/e2e/theming.spec.ts b/cockpit/chat/theming/angular/e2e/theming.spec.ts new file mode 100644 index 000000000..8e0cc5ab8 --- /dev/null +++ b/cockpit/chat/theming/angular/e2e/theming.spec.ts @@ -0,0 +1,27 @@ +import { expect, test } from '@playwright/test'; + +test.describe('Chat Theming Example', () => { + test.beforeEach(async ({ page }) => { + await page.goto('http://localhost:4510'); + await page.waitForSelector('app-theming', { state: 'attached' }); + }); + + test('renders the chat interface with theme picker sidebar', async ({ page }) => { + await expect(page.locator('chat')).toBeVisible(); + await expect(page.locator('aside')).toBeVisible(); + await expect(page.locator('aside h3')).toHaveText('Theme Picker'); + }); + + test('displays theme buttons', async ({ page }) => { + await expect(page.locator('aside button')).toHaveCount(4); + await expect(page.locator('aside')).toContainText('Dark'); + await expect(page.locator('aside')).toContainText('Light'); + await expect(page.locator('aside')).toContainText('Ocean'); + await expect(page.locator('aside')).toContainText('Forest'); + }); + + test('displays CSS variable list', async ({ page }) => { + await expect(page.locator('aside')).toContainText('--chat-bg'); + await expect(page.locator('aside')).toContainText('--chat-accent'); + }); +}); diff --git a/cockpit/chat/theming/angular/project.json b/cockpit/chat/theming/angular/project.json new file mode 100644 index 000000000..d0a34587e --- /dev/null +++ b/cockpit/chat/theming/angular/project.json @@ -0,0 +1,61 @@ +{ + "name": "cockpit-chat-theming-angular", + "$schema": "../../../../node_modules/nx/schemas/project-schema.json", + "sourceRoot": "cockpit/chat/theming/angular/src", + "projectType": "application", + "targets": { + "build": { + "executor": "@angular/build:application", + "outputs": ["{options.outputPath.base}"], + "options": { + "outputPath": { + "base": "dist/cockpit/chat/theming/angular", + "browser": "" + }, + "browser": "cockpit/chat/theming/angular/src/main.ts", + "tsConfig": "cockpit/chat/theming/angular/tsconfig.app.json", + "styles": ["cockpit/chat/theming/angular/src/styles.css"] + }, + "configurations": { + "production": { + "budgets": [ + { "type": "initial", "maximumWarning": "500kb", "maximumError": "1mb" }, + { "type": "anyComponentStyle", "maximumWarning": "4kb", "maximumError": "8kb" } + ], + "outputHashing": "none" + }, + "development": { + "optimization": false, + "extractLicenses": false, + "sourceMap": true, + "fileReplacements": [ + { + "replace": "cockpit/chat/theming/angular/src/environments/environment.ts", + "with": "cockpit/chat/theming/angular/src/environments/environment.development.ts" + } + ] + } + }, + "defaultConfiguration": "production" + }, + "serve": { + "continuous": true, + "executor": "@angular/build:dev-server", + "configurations": { + "production": { "buildTarget": "cockpit-chat-theming-angular:build:production" }, + "development": { "buildTarget": "cockpit-chat-theming-angular:build:development" } + }, + "defaultConfiguration": "development", + "options": { + "proxyConfig": "cockpit/chat/theming/angular/proxy.conf.json" + } + }, + "smoke": { + "executor": "nx:run-commands", + "options": { + "cwd": "cockpit/chat/theming/angular", + "command": "npx tsx -e \"import { chatThemingAngularModule } from './src/index.ts'; const module = chatThemingAngularModule; if (module.id !== 'chat-theming-angular' || module.title !== 'Chat Theming (Angular)') { throw new Error('Unexpected module shape for ' + module.id); }\"" + } + } + } +} diff --git a/cockpit/chat/theming/angular/proxy.conf.json b/cockpit/chat/theming/angular/proxy.conf.json new file mode 100644 index 000000000..8523362d7 --- /dev/null +++ b/cockpit/chat/theming/angular/proxy.conf.json @@ -0,0 +1,9 @@ +{ + "/api": { + "target": "http://localhost:8123", + "secure": false, + "changeOrigin": true, + "pathRewrite": { "^/api": "" }, + "ws": true + } +} diff --git a/cockpit/chat/theming/angular/src/app/app.config.ts b/cockpit/chat/theming/angular/src/app/app.config.ts new file mode 100644 index 000000000..b52a916ac --- /dev/null +++ b/cockpit/chat/theming/angular/src/app/app.config.ts @@ -0,0 +1,12 @@ +// SPDX-License-Identifier: PolyForm-Noncommercial-1.0.0 +import { ApplicationConfig } from '@angular/core'; +import { provideStreamResource } from '@cacheplane/stream-resource'; +import { provideChat } from '@cacheplane/chat'; +import { environment } from '../environments/environment'; + +export const appConfig: ApplicationConfig = { + providers: [ + provideStreamResource({ apiUrl: environment.langGraphApiUrl }), + provideChat({}), + ], +}; diff --git a/cockpit/chat/theming/angular/src/app/theming.component.ts b/cockpit/chat/theming/angular/src/app/theming.component.ts new file mode 100644 index 000000000..543882414 --- /dev/null +++ b/cockpit/chat/theming/angular/src/app/theming.component.ts @@ -0,0 +1,102 @@ +// SPDX-License-Identifier: PolyForm-Noncommercial-1.0.0 +import { Component, signal } from '@angular/core'; +import { ChatComponent, CHAT_THEME_STYLES } from '@cacheplane/chat'; +import { streamResource } from '@cacheplane/stream-resource'; +import { environment } from '../environments/environment'; + +const THEMES: Record> = { + dark: { + '--chat-bg': '#171717', + '--chat-text': '#e0e0e0', + '--chat-accent': '#3b82f6', + '--chat-surface': '#222', + '--chat-border': '#333', + '--chat-text-muted': '#777', + }, + light: { + '--chat-bg': '#ffffff', + '--chat-text': '#1a1a1a', + '--chat-accent': '#2563eb', + '--chat-surface': '#f3f4f6', + '--chat-border': '#d1d5db', + '--chat-text-muted': '#6b7280', + }, + ocean: { + '--chat-bg': '#0c1426', + '--chat-text': '#c8d6e5', + '--chat-accent': '#0abde3', + '--chat-surface': '#152238', + '--chat-border': '#1e3a5f', + '--chat-text-muted': '#576574', + }, + forest: { + '--chat-bg': '#1a2e1a', + '--chat-text': '#d4e6d4', + '--chat-accent': '#4ade80', + '--chat-surface': '#243524', + '--chat-border': '#2d4a2d', + '--chat-text-muted': '#6b8f6b', + }, +}; + +/** + * ThemingComponent demonstrates chat theming with CSS custom properties. + * A sidebar with theme picker buttons swaps CSS variables at runtime, + * showcasing CHAT_THEME_STYLES and custom theme presets. + */ +@Component({ + selector: 'app-theming', + standalone: true, + imports: [ChatComponent], + template: ` +
+ + +
+ `, +}) +export class ThemingComponent { + protected readonly stream = streamResource({ + apiUrl: environment.langGraphApiUrl, + assistantId: environment.streamingAssistantId, + }); + + protected readonly themeNames = Object.keys(THEMES); + protected readonly activeTheme = signal('dark'); + + setTheme(name: string) { + const theme = THEMES[name]; + if (!theme) return; + this.activeTheme.set(name); + Object.entries(theme).forEach(([key, value]) => { + document.documentElement.style.setProperty(key, value); + }); + } +} diff --git a/cockpit/chat/theming/angular/src/environments/environment.development.ts b/cockpit/chat/theming/angular/src/environments/environment.development.ts new file mode 100644 index 000000000..118036d5d --- /dev/null +++ b/cockpit/chat/theming/angular/src/environments/environment.development.ts @@ -0,0 +1,5 @@ +export const environment = { + production: false, + langGraphApiUrl: 'http://localhost:4510/api', + streamingAssistantId: 'c-theming', +}; diff --git a/cockpit/chat/theming/angular/src/environments/environment.ts b/cockpit/chat/theming/angular/src/environments/environment.ts new file mode 100644 index 000000000..fca3eed5b --- /dev/null +++ b/cockpit/chat/theming/angular/src/environments/environment.ts @@ -0,0 +1,5 @@ +export const environment = { + production: true, + langGraphApiUrl: '/api', + streamingAssistantId: 'c-theming', +}; diff --git a/cockpit/chat/theming/angular/src/index.html b/cockpit/chat/theming/angular/src/index.html new file mode 100644 index 000000000..cb6f574f4 --- /dev/null +++ b/cockpit/chat/theming/angular/src/index.html @@ -0,0 +1,13 @@ + + + + + Chat Theming — Angular + + + + + + + + diff --git a/cockpit/chat/theming/angular/src/index.ts b/cockpit/chat/theming/angular/src/index.ts new file mode 100644 index 000000000..66ae2b164 --- /dev/null +++ b/cockpit/chat/theming/angular/src/index.ts @@ -0,0 +1,29 @@ +export interface CockpitCapabilityModule { + id: string; + manifestIdentity: { + product: 'chat'; + section: 'core-capabilities'; + topic: 'theming'; + page: 'overview'; + language: 'angular'; + }; + title: string; + docsPath: string; + promptAssetPaths: string[]; + codeAssetPaths: string[]; +} + +export const chatThemingAngularModule: CockpitCapabilityModule = { + id: 'chat-theming-angular', + manifestIdentity: { + product: 'chat', + section: 'core-capabilities', + topic: 'theming', + page: 'overview', + language: 'angular', + }, + title: 'Chat Theming (Angular)', + docsPath: '/docs/chat/core-capabilities/theming/overview/angular', + promptAssetPaths: ['cockpit/chat/theming/python/prompts/theming.md'], + codeAssetPaths: ['cockpit/chat/theming/angular/src/app/theming.component.ts'], +}; diff --git a/cockpit/chat/theming/angular/src/main.ts b/cockpit/chat/theming/angular/src/main.ts new file mode 100644 index 000000000..0669011b9 --- /dev/null +++ b/cockpit/chat/theming/angular/src/main.ts @@ -0,0 +1,6 @@ +// SPDX-License-Identifier: PolyForm-Noncommercial-1.0.0 +import { bootstrapApplication } from '@angular/platform-browser'; +import { appConfig } from './app/app.config'; +import { ThemingComponent } from './app/theming.component'; + +bootstrapApplication(ThemingComponent, appConfig).catch(console.error); diff --git a/cockpit/chat/theming/angular/src/styles.css b/cockpit/chat/theming/angular/src/styles.css new file mode 100644 index 000000000..e31c63fdd --- /dev/null +++ b/cockpit/chat/theming/angular/src/styles.css @@ -0,0 +1 @@ +/* Global styles for the theming capability demo */ diff --git a/cockpit/chat/theming/angular/tsconfig.app.json b/cockpit/chat/theming/angular/tsconfig.app.json new file mode 100644 index 000000000..72c01e364 --- /dev/null +++ b/cockpit/chat/theming/angular/tsconfig.app.json @@ -0,0 +1,9 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../../../dist/out-tsc", + "types": [] + }, + "files": ["src/main.ts"], + "include": ["src/**/*.d.ts", "src/**/*.ts"] +} diff --git a/cockpit/chat/theming/angular/tsconfig.json b/cockpit/chat/theming/angular/tsconfig.json new file mode 100644 index 000000000..3fd970371 --- /dev/null +++ b/cockpit/chat/theming/angular/tsconfig.json @@ -0,0 +1,24 @@ +{ + "extends": "../../../../tsconfig.base.json", + "compilerOptions": { + "noPropertyAccessFromIndexSignature": false, + "experimentalDecorators": true, + "module": "preserve", + "emitDeclarationOnly": false, + "composite": false, + "lib": ["es2022", "dom"], + "skipLibCheck": true, + "strict": false + }, + "angularCompilerOptions": { + "enableI18nLegacyMessageIdFormat": false, + "strictInjectionParameters": false, + "strictInputAccessModifiers": false, + "strictTemplates": false + }, + "files": [], + "include": [], + "references": [ + { "path": "./tsconfig.app.json" } + ] +} diff --git a/cockpit/chat/theming/python/docs/guide.md b/cockpit/chat/theming/python/docs/guide.md new file mode 100644 index 000000000..f16dc371e --- /dev/null +++ b/cockpit/chat/theming/python/docs/guide.md @@ -0,0 +1,82 @@ +# Chat Theming with @cacheplane/chat + + +Customize chat appearance using CSS custom properties and +CHAT_THEME_STYLES. Create theme presets and build a theme picker +for runtime theme switching. + + + +Add theming to your chat interface using CSS custom properties and +`CHAT_THEME_STYLES` from `@cacheplane/chat`. Create theme presets +and a theme picker for switching themes at runtime. + + + + + +Chat components use CSS custom properties for all visual styling: + +```css +--chat-bg: #171717; +--chat-text: #e0e0e0; +--chat-accent: #3b82f6; +--chat-surface: #222; +--chat-border: #333; +--chat-text-muted: #777; +``` + + + + +Use `CHAT_THEME_STYLES` to apply a complete theme: + +```typescript +import { CHAT_THEME_STYLES } from '@cacheplane/chat'; +``` + + + + +Define theme presets as objects mapping CSS custom properties: + +```typescript +const themes = { + dark: { '--chat-bg': '#171717', '--chat-text': '#e0e0e0' }, + light: { '--chat-bg': '#ffffff', '--chat-text': '#1a1a1a' }, + ocean: { '--chat-bg': '#0c1426', '--chat-text': '#c8d6e5' }, +}; +``` + + + + +Create buttons that swap CSS variables on the host element: + +```typescript +setTheme(name: string) { + const theme = this.themes[name]; + Object.entries(theme).forEach(([key, value]) => { + document.documentElement.style.setProperty(key, value); + }); +} +``` + + + + +Override specific component styles without affecting the global theme: + +```css +chat-input { + --chat-input-bg: #1a1a2e; +} +``` + + + + + +CHAT_THEME_STYLES provides sensible defaults. Override only the +properties you need to change for your brand. + diff --git a/cockpit/chat/theming/python/langgraph.json b/cockpit/chat/theming/python/langgraph.json new file mode 100644 index 000000000..35924baa9 --- /dev/null +++ b/cockpit/chat/theming/python/langgraph.json @@ -0,0 +1,8 @@ +{ + "graphs": { + "c-theming": "./src/graph.py:graph" + }, + "dependencies": ["."], + "python_version": "3.12", + "env": ".env" +} diff --git a/cockpit/chat/theming/python/prompts/theming.md b/cockpit/chat/theming/python/prompts/theming.md new file mode 100644 index 000000000..88613665f --- /dev/null +++ b/cockpit/chat/theming/python/prompts/theming.md @@ -0,0 +1,13 @@ +# Chat Theming Assistant + +You are an assistant that demonstrates chat theming and CSS custom +property customization in @cacheplane/chat. + +The chat UI supports extensive theming via CSS custom properties like +`--chat-bg`, `--chat-text`, `--chat-accent`, `--chat-surface`, and more. +These can be swapped at runtime using CHAT_THEME_STYLES or by setting +CSS variables on a parent element. + +Explain the theming system when asked, and demonstrate how different +themes change the appearance of the chat interface. The sidebar contains +theme picker buttons that swap themes in real time. diff --git a/cockpit/chat/theming/python/pyproject.toml b/cockpit/chat/theming/python/pyproject.toml new file mode 100644 index 000000000..919cc8ac6 --- /dev/null +++ b/cockpit/chat/theming/python/pyproject.toml @@ -0,0 +1,21 @@ +[project] +name = "cockpit-chat-theming" +version = "0.1.0" +requires-python = ">=3.12" +dependencies = [ + "langgraph>=0.3", + "langchain-openai>=0.3", + "langsmith>=0.2", +] + +[tool.uv] +dev-dependencies = [ + "langgraph-cli[inmem]>=0.1", +] + +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[tool.hatch.build.targets.wheel] +packages = ["src"] diff --git a/cockpit/chat/theming/python/src/graph.py b/cockpit/chat/theming/python/src/graph.py new file mode 100644 index 000000000..c88e8ef95 --- /dev/null +++ b/cockpit/chat/theming/python/src/graph.py @@ -0,0 +1,37 @@ +""" +Chat Theming Graph + +A simple conversational agent. The focus of this capability is on +frontend theming with CSS custom properties, not graph complexity. +""" + +from pathlib import Path +from langgraph.graph import StateGraph, MessagesState, END +from langchain_openai import ChatOpenAI +from langchain_core.messages import SystemMessage + +PROMPTS_DIR = Path(__file__).parent.parent / "prompts" + + +def build_theming_graph(): + """ + Constructs a simple conversational agent for demonstrating + chat theming and CSS custom property customization. + """ + llm = ChatOpenAI(model="gpt-5-mini", streaming=True) + + async def generate(state: MessagesState) -> dict: + system_prompt = (PROMPTS_DIR / "theming.md").read_text() + messages = [SystemMessage(content=system_prompt)] + state["messages"] + response = await llm.ainvoke(messages) + return {"messages": [response]} + + graph = StateGraph(MessagesState) + graph.add_node("generate", generate) + graph.set_entry_point("generate") + graph.add_edge("generate", END) + + return graph.compile() + + +graph = build_theming_graph() diff --git a/cockpit/chat/theming/python/src/index.ts b/cockpit/chat/theming/python/src/index.ts new file mode 100644 index 000000000..baf29ce75 --- /dev/null +++ b/cockpit/chat/theming/python/src/index.ts @@ -0,0 +1,42 @@ +export interface CockpitCapabilityModule { + id: string; + manifestIdentity: { + product: 'chat'; + section: 'core-capabilities'; + topic: 'theming'; + page: 'overview'; + language: 'python'; + }; + title: string; + docsPath: string; + promptAssetPaths: string[]; + codeAssetPaths: string[]; + backendAssetPaths: string[]; + docsAssetPaths: string[]; + runtimeUrl?: string; + devPort?: number; +} + +export const chatThemingPythonModule: CockpitCapabilityModule = { + id: 'chat-theming-python', + manifestIdentity: { + product: 'chat', + section: 'core-capabilities', + topic: 'theming', + page: 'overview', + language: 'python', + }, + title: 'Chat Theming (Python)', + docsPath: '/docs/chat/core-capabilities/theming/overview/python', + promptAssetPaths: ['cockpit/chat/theming/python/prompts/theming.md'], + codeAssetPaths: [ + 'cockpit/chat/theming/angular/src/app/theming.component.ts', + 'cockpit/chat/theming/angular/src/app/app.config.ts', + ], + backendAssetPaths: [ + 'cockpit/chat/theming/python/src/graph.py', + ], + docsAssetPaths: ['cockpit/chat/theming/python/docs/guide.md'], + runtimeUrl: 'chat/theming', + devPort: 4510, +}; From 409b03c18bd4639509099734a7c1330423414f41 Mon Sep 17 00:00:00 2001 From: Brian Love Date: Wed, 8 Apr 2026 10:29:01 -0700 Subject: [PATCH 17/48] test(cockpit): add render and chat matrix tests Co-Authored-By: Claude Opus 4.6 (1M context) --- cockpit/chat/matrix.spec.ts | 56 +++++++++++++++++++++++++++++++++++ cockpit/render/matrix.spec.ts | 44 +++++++++++++++++++++++++++ 2 files changed, 100 insertions(+) create mode 100644 cockpit/chat/matrix.spec.ts create mode 100644 cockpit/render/matrix.spec.ts diff --git a/cockpit/chat/matrix.spec.ts b/cockpit/chat/matrix.spec.ts new file mode 100644 index 000000000..08c5b0a17 --- /dev/null +++ b/cockpit/chat/matrix.spec.ts @@ -0,0 +1,56 @@ +import { describe, expect, it } from 'vitest'; +import { chatMessagesPythonModule } from './messages/python/src/index'; +import { chatInputPythonModule } from './input/python/src/index'; +import { chatInterruptsPythonModule } from './interrupts/python/src/index'; +import { chatToolCallsPythonModule } from './tool-calls/python/src/index'; +import { chatSubagentsPythonModule } from './subagents/python/src/index'; +import { chatThreadsPythonModule } from './threads/python/src/index'; +import { chatTimelinePythonModule } from './timeline/python/src/index'; +import { chatGenerativeUiPythonModule } from './generative-ui/python/src/index'; +import { chatDebugPythonModule } from './debug/python/src/index'; +import { chatThemingPythonModule } from './theming/python/src/index'; + +describe('Chat matrix slice', () => { + it('exposes canonical python modules for the approved core capability topics', () => { + const modules = [ + chatMessagesPythonModule, + chatInputPythonModule, + chatInterruptsPythonModule, + chatToolCallsPythonModule, + chatSubagentsPythonModule, + chatThreadsPythonModule, + chatTimelinePythonModule, + chatGenerativeUiPythonModule, + chatDebugPythonModule, + chatThemingPythonModule, + ]; + + expect(modules).toHaveLength(10); + expect(modules.map((module) => module.manifestIdentity.topic)).toEqual([ + 'messages', + 'input', + 'interrupts', + 'tool-calls', + 'subagents', + 'threads', + 'timeline', + 'generative-ui', + 'debug', + 'theming', + ]); + + for (const module of modules) { + expect(module.manifestIdentity).toMatchObject({ + product: 'chat', + section: 'core-capabilities', + page: 'overview', + language: 'python', + }); + expect(module.docsPath).toBe( + `/docs/chat/core-capabilities/${module.manifestIdentity.topic}/overview/python` + ); + expect(module.promptAssetPaths.length).toBe(1); + expect(module.codeAssetPaths.length).toBeGreaterThanOrEqual(1); + } + }); +}); diff --git a/cockpit/render/matrix.spec.ts b/cockpit/render/matrix.spec.ts new file mode 100644 index 000000000..0a4e37637 --- /dev/null +++ b/cockpit/render/matrix.spec.ts @@ -0,0 +1,44 @@ +import { describe, expect, it } from 'vitest'; +import { renderSpecRenderingPythonModule } from './spec-rendering/python/src/index'; +import { renderElementRenderingPythonModule } from './element-rendering/python/src/index'; +import { renderStateManagementPythonModule } from './state-management/python/src/index'; +import { renderRegistryPythonModule } from './registry/python/src/index'; +import { renderRepeatLoopsPythonModule } from './repeat-loops/python/src/index'; +import { renderComputedFunctionsPythonModule } from './computed-functions/python/src/index'; + +describe('Render matrix slice', () => { + it('exposes canonical python modules for the approved core capability topics', () => { + const modules = [ + renderSpecRenderingPythonModule, + renderElementRenderingPythonModule, + renderStateManagementPythonModule, + renderRegistryPythonModule, + renderRepeatLoopsPythonModule, + renderComputedFunctionsPythonModule, + ]; + + expect(modules).toHaveLength(6); + expect(modules.map((module) => module.manifestIdentity.topic)).toEqual([ + 'spec-rendering', + 'element-rendering', + 'state-management', + 'registry', + 'repeat-loops', + 'computed-functions', + ]); + + for (const module of modules) { + expect(module.manifestIdentity).toMatchObject({ + product: 'render', + section: 'core-capabilities', + page: 'overview', + language: 'python', + }); + expect(module.docsPath).toBe( + `/docs/render/core-capabilities/${module.manifestIdentity.topic}/overview/python` + ); + expect(module.promptAssetPaths.length).toBe(1); + expect(module.codeAssetPaths.length).toBeGreaterThanOrEqual(1); + } + }); +}); From 3a1c24aa3226c30c7714113c4f8288c691413179 Mon Sep 17 00:00:00 2001 From: Brian Love Date: Wed, 8 Apr 2026 10:32:35 -0700 Subject: [PATCH 18/48] test(cockpit): add render and chat footprint tests Co-Authored-By: Claude Opus 4.6 (1M context) --- cockpit/chat/footprint.spec.ts | 55 ++++++++++++++++++++++++++++++++ cockpit/render/footprint.spec.ts | 51 +++++++++++++++++++++++++++++ 2 files changed, 106 insertions(+) create mode 100644 cockpit/chat/footprint.spec.ts create mode 100644 cockpit/render/footprint.spec.ts diff --git a/cockpit/chat/footprint.spec.ts b/cockpit/chat/footprint.spec.ts new file mode 100644 index 000000000..958e00a41 --- /dev/null +++ b/cockpit/chat/footprint.spec.ts @@ -0,0 +1,55 @@ +import fs from 'node:fs'; +import path from 'node:path'; +import { describe, expect, it } from 'vitest'; + +const topicNames = [ + 'messages', + 'input', + 'interrupts', + 'tool-calls', + 'subagents', + 'threads', + 'timeline', + 'generative-ui', + 'debug', + 'theming', +] as const; + +const pageNames = ['overview', 'build', 'prompts', 'code', 'testing'] as const; + +const chatRoot = path.join(process.cwd(), 'cockpit', 'chat'); +const websiteDocsRoot = path.join( + process.cwd(), 'apps', 'website', 'content', 'docs', 'chat' +); + +describe('Chat footprint', () => { + it('keeps the getting-started overview in place', () => { + expect( + fs.existsSync( + path.join(websiteDocsRoot, 'getting-started', 'overview', 'python', 'overview.mdx') + ) + ).toBe(true); + }); + + it('creates the approved topic modules and docs pages', () => { + for (const topic of topicNames) { + const moduleRoot = path.join(chatRoot, topic, 'python'); + const projectJsonPath = path.join(chatRoot, topic, 'angular', 'project.json'); + + expect(fs.existsSync(path.join(moduleRoot, 'src', 'index.ts'))).toBe(true); + expect(fs.existsSync(path.join(moduleRoot, 'prompts', `${topic}.md`))).toBe(true); + expect(fs.existsSync(projectJsonPath)).toBe(true); + + const projectJson = JSON.parse(fs.readFileSync(projectJsonPath, 'utf8')); + expect(projectJson.targets?.smoke?.executor).toBe('nx:run-commands'); + + for (const page of pageNames) { + expect( + fs.existsSync( + path.join(websiteDocsRoot, 'core-capabilities', topic, 'python', `${page}.mdx`) + ) + ).toBe(true); + } + } + }); +}); diff --git a/cockpit/render/footprint.spec.ts b/cockpit/render/footprint.spec.ts new file mode 100644 index 000000000..5a7bcac97 --- /dev/null +++ b/cockpit/render/footprint.spec.ts @@ -0,0 +1,51 @@ +import fs from 'node:fs'; +import path from 'node:path'; +import { describe, expect, it } from 'vitest'; + +const topicNames = [ + 'spec-rendering', + 'element-rendering', + 'state-management', + 'registry', + 'repeat-loops', + 'computed-functions', +] as const; + +const pageNames = ['overview', 'build', 'prompts', 'code', 'testing'] as const; + +const renderRoot = path.join(process.cwd(), 'cockpit', 'render'); +const websiteDocsRoot = path.join( + process.cwd(), 'apps', 'website', 'content', 'docs', 'render' +); + +describe('Render footprint', () => { + it('keeps the getting-started overview in place', () => { + expect( + fs.existsSync( + path.join(websiteDocsRoot, 'getting-started', 'overview', 'python', 'overview.mdx') + ) + ).toBe(true); + }); + + it('creates the approved topic modules and docs pages', () => { + for (const topic of topicNames) { + const moduleRoot = path.join(renderRoot, topic, 'python'); + const projectJsonPath = path.join(renderRoot, topic, 'angular', 'project.json'); + + expect(fs.existsSync(path.join(moduleRoot, 'src', 'index.ts'))).toBe(true); + expect(fs.existsSync(path.join(moduleRoot, 'prompts', `${topic}.md`))).toBe(true); + expect(fs.existsSync(projectJsonPath)).toBe(true); + + const projectJson = JSON.parse(fs.readFileSync(projectJsonPath, 'utf8')); + expect(projectJson.targets?.smoke?.executor).toBe('nx:run-commands'); + + for (const page of pageNames) { + expect( + fs.existsSync( + path.join(websiteDocsRoot, 'core-capabilities', topic, 'python', `${page}.mdx`) + ) + ).toBe(true); + } + } + }); +}); From d9299e71d49c4831d7ff10dbf6581ff27bcecf7e Mon Sep 17 00:00:00 2001 From: Brian Love Date: Wed, 8 Apr 2026 10:33:05 -0700 Subject: [PATCH 19/48] docs(cockpit): add render product docs pages Co-Authored-By: Claude Opus 4.6 (1M context) --- .../core-capabilities/computed-functions/python/build.mdx | 3 +++ .../core-capabilities/computed-functions/python/code.mdx | 3 +++ .../core-capabilities/computed-functions/python/overview.mdx | 5 +++++ .../core-capabilities/computed-functions/python/prompts.mdx | 3 +++ .../core-capabilities/computed-functions/python/testing.mdx | 3 +++ .../core-capabilities/element-rendering/python/build.mdx | 3 +++ .../core-capabilities/element-rendering/python/code.mdx | 3 +++ .../core-capabilities/element-rendering/python/overview.mdx | 5 +++++ .../core-capabilities/element-rendering/python/prompts.mdx | 3 +++ .../core-capabilities/element-rendering/python/testing.mdx | 3 +++ .../docs/render/core-capabilities/registry/python/build.mdx | 3 +++ .../docs/render/core-capabilities/registry/python/code.mdx | 3 +++ .../render/core-capabilities/registry/python/overview.mdx | 5 +++++ .../render/core-capabilities/registry/python/prompts.mdx | 3 +++ .../render/core-capabilities/registry/python/testing.mdx | 3 +++ .../render/core-capabilities/repeat-loops/python/build.mdx | 3 +++ .../render/core-capabilities/repeat-loops/python/code.mdx | 3 +++ .../core-capabilities/repeat-loops/python/overview.mdx | 5 +++++ .../render/core-capabilities/repeat-loops/python/prompts.mdx | 3 +++ .../render/core-capabilities/repeat-loops/python/testing.mdx | 3 +++ .../render/core-capabilities/spec-rendering/python/build.mdx | 3 +++ .../render/core-capabilities/spec-rendering/python/code.mdx | 3 +++ .../core-capabilities/spec-rendering/python/overview.mdx | 5 +++++ .../core-capabilities/spec-rendering/python/prompts.mdx | 3 +++ .../core-capabilities/spec-rendering/python/testing.mdx | 3 +++ .../core-capabilities/state-management/python/build.mdx | 3 +++ .../core-capabilities/state-management/python/code.mdx | 3 +++ .../core-capabilities/state-management/python/overview.mdx | 5 +++++ .../core-capabilities/state-management/python/prompts.mdx | 3 +++ .../core-capabilities/state-management/python/testing.mdx | 3 +++ .../docs/render/getting-started/overview/python/overview.mdx | 5 +++++ 31 files changed, 107 insertions(+) create mode 100644 apps/website/content/docs/render/core-capabilities/computed-functions/python/build.mdx create mode 100644 apps/website/content/docs/render/core-capabilities/computed-functions/python/code.mdx create mode 100644 apps/website/content/docs/render/core-capabilities/computed-functions/python/overview.mdx create mode 100644 apps/website/content/docs/render/core-capabilities/computed-functions/python/prompts.mdx create mode 100644 apps/website/content/docs/render/core-capabilities/computed-functions/python/testing.mdx create mode 100644 apps/website/content/docs/render/core-capabilities/element-rendering/python/build.mdx create mode 100644 apps/website/content/docs/render/core-capabilities/element-rendering/python/code.mdx create mode 100644 apps/website/content/docs/render/core-capabilities/element-rendering/python/overview.mdx create mode 100644 apps/website/content/docs/render/core-capabilities/element-rendering/python/prompts.mdx create mode 100644 apps/website/content/docs/render/core-capabilities/element-rendering/python/testing.mdx create mode 100644 apps/website/content/docs/render/core-capabilities/registry/python/build.mdx create mode 100644 apps/website/content/docs/render/core-capabilities/registry/python/code.mdx create mode 100644 apps/website/content/docs/render/core-capabilities/registry/python/overview.mdx create mode 100644 apps/website/content/docs/render/core-capabilities/registry/python/prompts.mdx create mode 100644 apps/website/content/docs/render/core-capabilities/registry/python/testing.mdx create mode 100644 apps/website/content/docs/render/core-capabilities/repeat-loops/python/build.mdx create mode 100644 apps/website/content/docs/render/core-capabilities/repeat-loops/python/code.mdx create mode 100644 apps/website/content/docs/render/core-capabilities/repeat-loops/python/overview.mdx create mode 100644 apps/website/content/docs/render/core-capabilities/repeat-loops/python/prompts.mdx create mode 100644 apps/website/content/docs/render/core-capabilities/repeat-loops/python/testing.mdx create mode 100644 apps/website/content/docs/render/core-capabilities/spec-rendering/python/build.mdx create mode 100644 apps/website/content/docs/render/core-capabilities/spec-rendering/python/code.mdx create mode 100644 apps/website/content/docs/render/core-capabilities/spec-rendering/python/overview.mdx create mode 100644 apps/website/content/docs/render/core-capabilities/spec-rendering/python/prompts.mdx create mode 100644 apps/website/content/docs/render/core-capabilities/spec-rendering/python/testing.mdx create mode 100644 apps/website/content/docs/render/core-capabilities/state-management/python/build.mdx create mode 100644 apps/website/content/docs/render/core-capabilities/state-management/python/code.mdx create mode 100644 apps/website/content/docs/render/core-capabilities/state-management/python/overview.mdx create mode 100644 apps/website/content/docs/render/core-capabilities/state-management/python/prompts.mdx create mode 100644 apps/website/content/docs/render/core-capabilities/state-management/python/testing.mdx create mode 100644 apps/website/content/docs/render/getting-started/overview/python/overview.mdx diff --git a/apps/website/content/docs/render/core-capabilities/computed-functions/python/build.mdx b/apps/website/content/docs/render/core-capabilities/computed-functions/python/build.mdx new file mode 100644 index 000000000..e744f56c4 --- /dev/null +++ b/apps/website/content/docs/render/core-capabilities/computed-functions/python/build.mdx @@ -0,0 +1,3 @@ +# Build Computed Function Workflows + +Design computed functions as pure derivations that take store values as inputs and return a single result for spec binding. diff --git a/apps/website/content/docs/render/core-capabilities/computed-functions/python/code.mdx b/apps/website/content/docs/render/core-capabilities/computed-functions/python/code.mdx new file mode 100644 index 000000000..7084ae288 --- /dev/null +++ b/apps/website/content/docs/render/core-capabilities/computed-functions/python/code.mdx @@ -0,0 +1,3 @@ +# Computed Function Code + +Code examples for computed functions show how to register derivations that combine or transform store values for use in spec bindings. diff --git a/apps/website/content/docs/render/core-capabilities/computed-functions/python/overview.mdx b/apps/website/content/docs/render/core-capabilities/computed-functions/python/overview.mdx new file mode 100644 index 000000000..c40be5d97 --- /dev/null +++ b/apps/website/content/docs/render/core-capabilities/computed-functions/python/overview.mdx @@ -0,0 +1,5 @@ +# Computed Functions Overview + +Computed functions derive new values from store state so specs can bind to calculated results instead of raw data. + +Use this page when you need to transform or combine state values before they reach a component. diff --git a/apps/website/content/docs/render/core-capabilities/computed-functions/python/prompts.mdx b/apps/website/content/docs/render/core-capabilities/computed-functions/python/prompts.mdx new file mode 100644 index 000000000..b9590eb78 --- /dev/null +++ b/apps/website/content/docs/render/core-capabilities/computed-functions/python/prompts.mdx @@ -0,0 +1,3 @@ +# Computed Function Prompts + +Prompts for computed functions should name the input store keys, the transformation logic, and the expected output type. diff --git a/apps/website/content/docs/render/core-capabilities/computed-functions/python/testing.mdx b/apps/website/content/docs/render/core-capabilities/computed-functions/python/testing.mdx new file mode 100644 index 000000000..65d4bbddf --- /dev/null +++ b/apps/website/content/docs/render/core-capabilities/computed-functions/python/testing.mdx @@ -0,0 +1,3 @@ +# Computed Function Testing + +Test computed functions by setting input store values and verifying that the derived output matches the expected result. diff --git a/apps/website/content/docs/render/core-capabilities/element-rendering/python/build.mdx b/apps/website/content/docs/render/core-capabilities/element-rendering/python/build.mdx new file mode 100644 index 000000000..7a648942f --- /dev/null +++ b/apps/website/content/docs/render/core-capabilities/element-rendering/python/build.mdx @@ -0,0 +1,3 @@ +# Build Element Rendering Workflows + +Design element rendering steps around clear input bindings and predictable output events for each component. diff --git a/apps/website/content/docs/render/core-capabilities/element-rendering/python/code.mdx b/apps/website/content/docs/render/core-capabilities/element-rendering/python/code.mdx new file mode 100644 index 000000000..8cc45c963 --- /dev/null +++ b/apps/website/content/docs/render/core-capabilities/element-rendering/python/code.mdx @@ -0,0 +1,3 @@ +# Element Rendering Code + +Code examples for element rendering show how individual spec nodes resolve to Angular components with bound inputs and outputs. diff --git a/apps/website/content/docs/render/core-capabilities/element-rendering/python/overview.mdx b/apps/website/content/docs/render/core-capabilities/element-rendering/python/overview.mdx new file mode 100644 index 000000000..32c8fb73c --- /dev/null +++ b/apps/website/content/docs/render/core-capabilities/element-rendering/python/overview.mdx @@ -0,0 +1,5 @@ +# Element Rendering Overview + +Element rendering resolves individual spec nodes into their matching Angular components and binds inputs and outputs. + +Use this page when you need to understand how a single spec element becomes a rendered component instance. diff --git a/apps/website/content/docs/render/core-capabilities/element-rendering/python/prompts.mdx b/apps/website/content/docs/render/core-capabilities/element-rendering/python/prompts.mdx new file mode 100644 index 000000000..97c61e81b --- /dev/null +++ b/apps/website/content/docs/render/core-capabilities/element-rendering/python/prompts.mdx @@ -0,0 +1,3 @@ +# Element Rendering Prompts + +Prompts for element rendering should specify the component type, expected inputs, and any output event handlers. diff --git a/apps/website/content/docs/render/core-capabilities/element-rendering/python/testing.mdx b/apps/website/content/docs/render/core-capabilities/element-rendering/python/testing.mdx new file mode 100644 index 000000000..7a7b742b2 --- /dev/null +++ b/apps/website/content/docs/render/core-capabilities/element-rendering/python/testing.mdx @@ -0,0 +1,3 @@ +# Element Rendering Testing + +Test element rendering by asserting that each spec node produces the correct component with the expected input values and event bindings. diff --git a/apps/website/content/docs/render/core-capabilities/registry/python/build.mdx b/apps/website/content/docs/render/core-capabilities/registry/python/build.mdx new file mode 100644 index 000000000..fa0d90d06 --- /dev/null +++ b/apps/website/content/docs/render/core-capabilities/registry/python/build.mdx @@ -0,0 +1,3 @@ +# Build Registry Workflows + +Design registry workflows around a single registration step at bootstrap that maps every spec type to its component class. diff --git a/apps/website/content/docs/render/core-capabilities/registry/python/code.mdx b/apps/website/content/docs/render/core-capabilities/registry/python/code.mdx new file mode 100644 index 000000000..d8e99db78 --- /dev/null +++ b/apps/website/content/docs/render/core-capabilities/registry/python/code.mdx @@ -0,0 +1,3 @@ +# Registry Code + +Code examples for the registry show how to register components, override defaults, and look up types at runtime. diff --git a/apps/website/content/docs/render/core-capabilities/registry/python/overview.mdx b/apps/website/content/docs/render/core-capabilities/registry/python/overview.mdx new file mode 100644 index 000000000..f9a787e5d --- /dev/null +++ b/apps/website/content/docs/render/core-capabilities/registry/python/overview.mdx @@ -0,0 +1,5 @@ +# Registry Overview + +The component registry maps spec type strings to Angular component classes so the renderer knows which component to instantiate. + +Use this page when you need to register custom components or understand how type resolution works. diff --git a/apps/website/content/docs/render/core-capabilities/registry/python/prompts.mdx b/apps/website/content/docs/render/core-capabilities/registry/python/prompts.mdx new file mode 100644 index 000000000..1a6ff5ad7 --- /dev/null +++ b/apps/website/content/docs/render/core-capabilities/registry/python/prompts.mdx @@ -0,0 +1,3 @@ +# Registry Prompts + +Prompts for registry configuration should list the spec type strings and the Angular components they resolve to. diff --git a/apps/website/content/docs/render/core-capabilities/registry/python/testing.mdx b/apps/website/content/docs/render/core-capabilities/registry/python/testing.mdx new file mode 100644 index 000000000..5e0b1302f --- /dev/null +++ b/apps/website/content/docs/render/core-capabilities/registry/python/testing.mdx @@ -0,0 +1,3 @@ +# Registry Testing + +Test registry configuration by verifying that each registered type string resolves to the correct Angular component class. diff --git a/apps/website/content/docs/render/core-capabilities/repeat-loops/python/build.mdx b/apps/website/content/docs/render/core-capabilities/repeat-loops/python/build.mdx new file mode 100644 index 000000000..0df1d509b --- /dev/null +++ b/apps/website/content/docs/render/core-capabilities/repeat-loops/python/build.mdx @@ -0,0 +1,3 @@ +# Build Repeat Loop Workflows + +Design repeat loops around a clear data source key and a template spec that defines how each item renders. diff --git a/apps/website/content/docs/render/core-capabilities/repeat-loops/python/code.mdx b/apps/website/content/docs/render/core-capabilities/repeat-loops/python/code.mdx new file mode 100644 index 000000000..5c376a7a7 --- /dev/null +++ b/apps/website/content/docs/render/core-capabilities/repeat-loops/python/code.mdx @@ -0,0 +1,3 @@ +# Repeat Loop Code + +Code examples for repeat loops show how to bind an array state key to a template spec and render one component per item. diff --git a/apps/website/content/docs/render/core-capabilities/repeat-loops/python/overview.mdx b/apps/website/content/docs/render/core-capabilities/repeat-loops/python/overview.mdx new file mode 100644 index 000000000..ffd52a4a6 --- /dev/null +++ b/apps/website/content/docs/render/core-capabilities/repeat-loops/python/overview.mdx @@ -0,0 +1,5 @@ +# Repeat Loops Overview + +Repeat loops let a single spec node expand into multiple rendered elements by iterating over an array in the state store. + +Use this page when you need to render lists or collections driven by dynamic data. diff --git a/apps/website/content/docs/render/core-capabilities/repeat-loops/python/prompts.mdx b/apps/website/content/docs/render/core-capabilities/repeat-loops/python/prompts.mdx new file mode 100644 index 000000000..4ba3e9388 --- /dev/null +++ b/apps/website/content/docs/render/core-capabilities/repeat-loops/python/prompts.mdx @@ -0,0 +1,3 @@ +# Repeat Loop Prompts + +Prompts for repeat loops should specify the array key, the item template spec, and any index or tracking behavior. diff --git a/apps/website/content/docs/render/core-capabilities/repeat-loops/python/testing.mdx b/apps/website/content/docs/render/core-capabilities/repeat-loops/python/testing.mdx new file mode 100644 index 000000000..404d9d116 --- /dev/null +++ b/apps/website/content/docs/render/core-capabilities/repeat-loops/python/testing.mdx @@ -0,0 +1,3 @@ +# Repeat Loop Testing + +Test repeat loops by setting an array value in the store and verifying that the correct number of child components render with the right data. diff --git a/apps/website/content/docs/render/core-capabilities/spec-rendering/python/build.mdx b/apps/website/content/docs/render/core-capabilities/spec-rendering/python/build.mdx new file mode 100644 index 000000000..8ccdf6661 --- /dev/null +++ b/apps/website/content/docs/render/core-capabilities/spec-rendering/python/build.mdx @@ -0,0 +1,3 @@ +# Build Spec Rendering Workflows + +Design spec rendering around small, composable spec objects that each describe one visual unit. diff --git a/apps/website/content/docs/render/core-capabilities/spec-rendering/python/code.mdx b/apps/website/content/docs/render/core-capabilities/spec-rendering/python/code.mdx new file mode 100644 index 000000000..eab4678ce --- /dev/null +++ b/apps/website/content/docs/render/core-capabilities/spec-rendering/python/code.mdx @@ -0,0 +1,3 @@ +# Spec Rendering Code + +Code examples for spec rendering show how to construct spec objects and pass them to RenderSpecComponent for dynamic rendering. diff --git a/apps/website/content/docs/render/core-capabilities/spec-rendering/python/overview.mdx b/apps/website/content/docs/render/core-capabilities/spec-rendering/python/overview.mdx new file mode 100644 index 000000000..5ff7f5882 --- /dev/null +++ b/apps/website/content/docs/render/core-capabilities/spec-rendering/python/overview.mdx @@ -0,0 +1,5 @@ +# Spec Rendering Overview + +Spec rendering converts JSON specification objects into live Angular component trees at runtime. + +Use this page when you need to understand how render specs map to components and how the rendering pipeline processes them. diff --git a/apps/website/content/docs/render/core-capabilities/spec-rendering/python/prompts.mdx b/apps/website/content/docs/render/core-capabilities/spec-rendering/python/prompts.mdx new file mode 100644 index 000000000..1774c87fa --- /dev/null +++ b/apps/website/content/docs/render/core-capabilities/spec-rendering/python/prompts.mdx @@ -0,0 +1,3 @@ +# Spec Rendering Prompts + +Prompts for spec rendering should describe the desired component tree shape, the spec types involved, and the expected visual outcome. diff --git a/apps/website/content/docs/render/core-capabilities/spec-rendering/python/testing.mdx b/apps/website/content/docs/render/core-capabilities/spec-rendering/python/testing.mdx new file mode 100644 index 000000000..90fcffb34 --- /dev/null +++ b/apps/website/content/docs/render/core-capabilities/spec-rendering/python/testing.mdx @@ -0,0 +1,3 @@ +# Spec Rendering Testing + +Test spec rendering by verifying that a given spec object produces the correct component tree and DOM output. diff --git a/apps/website/content/docs/render/core-capabilities/state-management/python/build.mdx b/apps/website/content/docs/render/core-capabilities/state-management/python/build.mdx new file mode 100644 index 000000000..96ae2c13e --- /dev/null +++ b/apps/website/content/docs/render/core-capabilities/state-management/python/build.mdx @@ -0,0 +1,3 @@ +# Build State Management Workflows + +Design state management around a flat signal store where each key maps to a single reactive value consumed by specs. diff --git a/apps/website/content/docs/render/core-capabilities/state-management/python/code.mdx b/apps/website/content/docs/render/core-capabilities/state-management/python/code.mdx new file mode 100644 index 000000000..bf794824c --- /dev/null +++ b/apps/website/content/docs/render/core-capabilities/state-management/python/code.mdx @@ -0,0 +1,3 @@ +# State Management Code + +Code examples for state management show how to define store keys, update values through signals, and bind them to spec properties. diff --git a/apps/website/content/docs/render/core-capabilities/state-management/python/overview.mdx b/apps/website/content/docs/render/core-capabilities/state-management/python/overview.mdx new file mode 100644 index 000000000..04836c706 --- /dev/null +++ b/apps/website/content/docs/render/core-capabilities/state-management/python/overview.mdx @@ -0,0 +1,5 @@ +# State Management Overview + +State management in Render uses a signal-based store that feeds reactive values into spec bindings at runtime. + +Use this page when you need to understand how state flows from the store into rendered components. diff --git a/apps/website/content/docs/render/core-capabilities/state-management/python/prompts.mdx b/apps/website/content/docs/render/core-capabilities/state-management/python/prompts.mdx new file mode 100644 index 000000000..c181f2aae --- /dev/null +++ b/apps/website/content/docs/render/core-capabilities/state-management/python/prompts.mdx @@ -0,0 +1,3 @@ +# State Management Prompts + +Prompts for state management should name the store keys, their types, and how spec bindings reference them. diff --git a/apps/website/content/docs/render/core-capabilities/state-management/python/testing.mdx b/apps/website/content/docs/render/core-capabilities/state-management/python/testing.mdx new file mode 100644 index 000000000..28dc2978c --- /dev/null +++ b/apps/website/content/docs/render/core-capabilities/state-management/python/testing.mdx @@ -0,0 +1,3 @@ +# State Management Testing + +Test state management by updating store values and verifying that bound component inputs reflect the new state. diff --git a/apps/website/content/docs/render/getting-started/overview/python/overview.mdx b/apps/website/content/docs/render/getting-started/overview/python/overview.mdx new file mode 100644 index 000000000..105364458 --- /dev/null +++ b/apps/website/content/docs/render/getting-started/overview/python/overview.mdx @@ -0,0 +1,5 @@ +# Render Overview + +Render lets you drive Angular component trees from JSON specifications, so layouts and content can change without redeploying code. + +Start here when you want the fastest path to a working Render app in Angular. The product-first guides focus on the core workflow, then connect you to specs, registries, state, and testing as you grow. From 753a556fdfb4d24414cf9aaba45646a65e9e886c Mon Sep 17 00:00:00 2001 From: Brian Love Date: Wed, 8 Apr 2026 10:34:24 -0700 Subject: [PATCH 20/48] docs(cockpit): add chat product docs pages Co-Authored-By: Claude Opus 4.6 (1M context) --- .../docs/chat/core-capabilities/debug/python/build.mdx | 3 +++ .../docs/chat/core-capabilities/debug/python/code.mdx | 3 +++ .../docs/chat/core-capabilities/debug/python/overview.mdx | 5 +++++ .../docs/chat/core-capabilities/debug/python/prompts.mdx | 3 +++ .../docs/chat/core-capabilities/debug/python/testing.mdx | 3 +++ .../chat/core-capabilities/generative-ui/python/build.mdx | 3 +++ .../chat/core-capabilities/generative-ui/python/code.mdx | 3 +++ .../chat/core-capabilities/generative-ui/python/overview.mdx | 5 +++++ .../chat/core-capabilities/generative-ui/python/prompts.mdx | 3 +++ .../chat/core-capabilities/generative-ui/python/testing.mdx | 3 +++ .../docs/chat/core-capabilities/input/python/build.mdx | 3 +++ .../docs/chat/core-capabilities/input/python/code.mdx | 3 +++ .../docs/chat/core-capabilities/input/python/overview.mdx | 5 +++++ .../docs/chat/core-capabilities/input/python/prompts.mdx | 3 +++ .../docs/chat/core-capabilities/input/python/testing.mdx | 3 +++ .../docs/chat/core-capabilities/interrupts/python/build.mdx | 3 +++ .../docs/chat/core-capabilities/interrupts/python/code.mdx | 3 +++ .../chat/core-capabilities/interrupts/python/overview.mdx | 5 +++++ .../chat/core-capabilities/interrupts/python/prompts.mdx | 3 +++ .../chat/core-capabilities/interrupts/python/testing.mdx | 3 +++ .../docs/chat/core-capabilities/messages/python/build.mdx | 3 +++ .../docs/chat/core-capabilities/messages/python/code.mdx | 3 +++ .../docs/chat/core-capabilities/messages/python/overview.mdx | 5 +++++ .../docs/chat/core-capabilities/messages/python/prompts.mdx | 3 +++ .../docs/chat/core-capabilities/messages/python/testing.mdx | 3 +++ .../docs/chat/core-capabilities/subagents/python/build.mdx | 3 +++ .../docs/chat/core-capabilities/subagents/python/code.mdx | 3 +++ .../chat/core-capabilities/subagents/python/overview.mdx | 5 +++++ .../docs/chat/core-capabilities/subagents/python/prompts.mdx | 3 +++ .../docs/chat/core-capabilities/subagents/python/testing.mdx | 3 +++ .../docs/chat/core-capabilities/theming/python/build.mdx | 3 +++ .../docs/chat/core-capabilities/theming/python/code.mdx | 3 +++ .../docs/chat/core-capabilities/theming/python/overview.mdx | 5 +++++ .../docs/chat/core-capabilities/theming/python/prompts.mdx | 3 +++ .../docs/chat/core-capabilities/theming/python/testing.mdx | 3 +++ .../docs/chat/core-capabilities/threads/python/build.mdx | 3 +++ .../docs/chat/core-capabilities/threads/python/code.mdx | 3 +++ .../docs/chat/core-capabilities/threads/python/overview.mdx | 5 +++++ .../docs/chat/core-capabilities/threads/python/prompts.mdx | 3 +++ .../docs/chat/core-capabilities/threads/python/testing.mdx | 3 +++ .../docs/chat/core-capabilities/timeline/python/build.mdx | 3 +++ .../docs/chat/core-capabilities/timeline/python/code.mdx | 3 +++ .../docs/chat/core-capabilities/timeline/python/overview.mdx | 5 +++++ .../docs/chat/core-capabilities/timeline/python/prompts.mdx | 3 +++ .../docs/chat/core-capabilities/timeline/python/testing.mdx | 3 +++ .../docs/chat/core-capabilities/tool-calls/python/build.mdx | 3 +++ .../docs/chat/core-capabilities/tool-calls/python/code.mdx | 3 +++ .../chat/core-capabilities/tool-calls/python/overview.mdx | 5 +++++ .../chat/core-capabilities/tool-calls/python/prompts.mdx | 3 +++ .../chat/core-capabilities/tool-calls/python/testing.mdx | 3 +++ .../docs/chat/getting-started/overview/python/overview.mdx | 5 +++++ 51 files changed, 175 insertions(+) create mode 100644 apps/website/content/docs/chat/core-capabilities/debug/python/build.mdx create mode 100644 apps/website/content/docs/chat/core-capabilities/debug/python/code.mdx create mode 100644 apps/website/content/docs/chat/core-capabilities/debug/python/overview.mdx create mode 100644 apps/website/content/docs/chat/core-capabilities/debug/python/prompts.mdx create mode 100644 apps/website/content/docs/chat/core-capabilities/debug/python/testing.mdx create mode 100644 apps/website/content/docs/chat/core-capabilities/generative-ui/python/build.mdx create mode 100644 apps/website/content/docs/chat/core-capabilities/generative-ui/python/code.mdx create mode 100644 apps/website/content/docs/chat/core-capabilities/generative-ui/python/overview.mdx create mode 100644 apps/website/content/docs/chat/core-capabilities/generative-ui/python/prompts.mdx create mode 100644 apps/website/content/docs/chat/core-capabilities/generative-ui/python/testing.mdx create mode 100644 apps/website/content/docs/chat/core-capabilities/input/python/build.mdx create mode 100644 apps/website/content/docs/chat/core-capabilities/input/python/code.mdx create mode 100644 apps/website/content/docs/chat/core-capabilities/input/python/overview.mdx create mode 100644 apps/website/content/docs/chat/core-capabilities/input/python/prompts.mdx create mode 100644 apps/website/content/docs/chat/core-capabilities/input/python/testing.mdx create mode 100644 apps/website/content/docs/chat/core-capabilities/interrupts/python/build.mdx create mode 100644 apps/website/content/docs/chat/core-capabilities/interrupts/python/code.mdx create mode 100644 apps/website/content/docs/chat/core-capabilities/interrupts/python/overview.mdx create mode 100644 apps/website/content/docs/chat/core-capabilities/interrupts/python/prompts.mdx create mode 100644 apps/website/content/docs/chat/core-capabilities/interrupts/python/testing.mdx create mode 100644 apps/website/content/docs/chat/core-capabilities/messages/python/build.mdx create mode 100644 apps/website/content/docs/chat/core-capabilities/messages/python/code.mdx create mode 100644 apps/website/content/docs/chat/core-capabilities/messages/python/overview.mdx create mode 100644 apps/website/content/docs/chat/core-capabilities/messages/python/prompts.mdx create mode 100644 apps/website/content/docs/chat/core-capabilities/messages/python/testing.mdx create mode 100644 apps/website/content/docs/chat/core-capabilities/subagents/python/build.mdx create mode 100644 apps/website/content/docs/chat/core-capabilities/subagents/python/code.mdx create mode 100644 apps/website/content/docs/chat/core-capabilities/subagents/python/overview.mdx create mode 100644 apps/website/content/docs/chat/core-capabilities/subagents/python/prompts.mdx create mode 100644 apps/website/content/docs/chat/core-capabilities/subagents/python/testing.mdx create mode 100644 apps/website/content/docs/chat/core-capabilities/theming/python/build.mdx create mode 100644 apps/website/content/docs/chat/core-capabilities/theming/python/code.mdx create mode 100644 apps/website/content/docs/chat/core-capabilities/theming/python/overview.mdx create mode 100644 apps/website/content/docs/chat/core-capabilities/theming/python/prompts.mdx create mode 100644 apps/website/content/docs/chat/core-capabilities/theming/python/testing.mdx create mode 100644 apps/website/content/docs/chat/core-capabilities/threads/python/build.mdx create mode 100644 apps/website/content/docs/chat/core-capabilities/threads/python/code.mdx create mode 100644 apps/website/content/docs/chat/core-capabilities/threads/python/overview.mdx create mode 100644 apps/website/content/docs/chat/core-capabilities/threads/python/prompts.mdx create mode 100644 apps/website/content/docs/chat/core-capabilities/threads/python/testing.mdx create mode 100644 apps/website/content/docs/chat/core-capabilities/timeline/python/build.mdx create mode 100644 apps/website/content/docs/chat/core-capabilities/timeline/python/code.mdx create mode 100644 apps/website/content/docs/chat/core-capabilities/timeline/python/overview.mdx create mode 100644 apps/website/content/docs/chat/core-capabilities/timeline/python/prompts.mdx create mode 100644 apps/website/content/docs/chat/core-capabilities/timeline/python/testing.mdx create mode 100644 apps/website/content/docs/chat/core-capabilities/tool-calls/python/build.mdx create mode 100644 apps/website/content/docs/chat/core-capabilities/tool-calls/python/code.mdx create mode 100644 apps/website/content/docs/chat/core-capabilities/tool-calls/python/overview.mdx create mode 100644 apps/website/content/docs/chat/core-capabilities/tool-calls/python/prompts.mdx create mode 100644 apps/website/content/docs/chat/core-capabilities/tool-calls/python/testing.mdx create mode 100644 apps/website/content/docs/chat/getting-started/overview/python/overview.mdx diff --git a/apps/website/content/docs/chat/core-capabilities/debug/python/build.mdx b/apps/website/content/docs/chat/core-capabilities/debug/python/build.mdx new file mode 100644 index 000000000..29c663c3d --- /dev/null +++ b/apps/website/content/docs/chat/core-capabilities/debug/python/build.mdx @@ -0,0 +1,3 @@ +# Build Debug Workflows + +Design debug workflows around event logging and inspector panels that surface stream events, state changes, and rendering decisions. diff --git a/apps/website/content/docs/chat/core-capabilities/debug/python/code.mdx b/apps/website/content/docs/chat/core-capabilities/debug/python/code.mdx new file mode 100644 index 000000000..4acf906b8 --- /dev/null +++ b/apps/website/content/docs/chat/core-capabilities/debug/python/code.mdx @@ -0,0 +1,3 @@ +# Debug Code + +Code examples for debug tools show how to enable event logging, attach inspector panels, and filter debug output by event type. diff --git a/apps/website/content/docs/chat/core-capabilities/debug/python/overview.mdx b/apps/website/content/docs/chat/core-capabilities/debug/python/overview.mdx new file mode 100644 index 000000000..b5c1af3c8 --- /dev/null +++ b/apps/website/content/docs/chat/core-capabilities/debug/python/overview.mdx @@ -0,0 +1,5 @@ +# Debug Overview + +Debug tools provide visibility into message flow, stream events, tool call execution, and component rendering during development. + +Use this page when you need to diagnose issues in the chat pipeline or inspect runtime behavior. diff --git a/apps/website/content/docs/chat/core-capabilities/debug/python/prompts.mdx b/apps/website/content/docs/chat/core-capabilities/debug/python/prompts.mdx new file mode 100644 index 000000000..1e7898270 --- /dev/null +++ b/apps/website/content/docs/chat/core-capabilities/debug/python/prompts.mdx @@ -0,0 +1,3 @@ +# Debug Prompts + +Prompts for debug configuration should specify which event types to log, the verbosity level, and where debug output should appear. diff --git a/apps/website/content/docs/chat/core-capabilities/debug/python/testing.mdx b/apps/website/content/docs/chat/core-capabilities/debug/python/testing.mdx new file mode 100644 index 000000000..c770320cd --- /dev/null +++ b/apps/website/content/docs/chat/core-capabilities/debug/python/testing.mdx @@ -0,0 +1,3 @@ +# Debug Testing + +Test debug tooling by triggering known events and verifying that the logger captures them with the correct type, payload, and timestamp. diff --git a/apps/website/content/docs/chat/core-capabilities/generative-ui/python/build.mdx b/apps/website/content/docs/chat/core-capabilities/generative-ui/python/build.mdx new file mode 100644 index 000000000..66c685433 --- /dev/null +++ b/apps/website/content/docs/chat/core-capabilities/generative-ui/python/build.mdx @@ -0,0 +1,3 @@ +# Build Generative UI Workflows + +Design generative UI workflows around a component registry that maps response data types to Angular components rendered inline. diff --git a/apps/website/content/docs/chat/core-capabilities/generative-ui/python/code.mdx b/apps/website/content/docs/chat/core-capabilities/generative-ui/python/code.mdx new file mode 100644 index 000000000..3add8914c --- /dev/null +++ b/apps/website/content/docs/chat/core-capabilities/generative-ui/python/code.mdx @@ -0,0 +1,3 @@ +# Generative UI Code + +Code examples for generative UI show how to register inline components, parse structured response data, and render widgets within messages. diff --git a/apps/website/content/docs/chat/core-capabilities/generative-ui/python/overview.mdx b/apps/website/content/docs/chat/core-capabilities/generative-ui/python/overview.mdx new file mode 100644 index 000000000..f1d2add42 --- /dev/null +++ b/apps/website/content/docs/chat/core-capabilities/generative-ui/python/overview.mdx @@ -0,0 +1,5 @@ +# Generative UI Overview + +Generative UI renders dynamic Angular components inline within assistant messages based on structured data from the response stream. + +Use this page when you need to embed interactive widgets, charts, or forms directly in chat responses. diff --git a/apps/website/content/docs/chat/core-capabilities/generative-ui/python/prompts.mdx b/apps/website/content/docs/chat/core-capabilities/generative-ui/python/prompts.mdx new file mode 100644 index 000000000..2c6408383 --- /dev/null +++ b/apps/website/content/docs/chat/core-capabilities/generative-ui/python/prompts.mdx @@ -0,0 +1,3 @@ +# Generative UI Prompts + +Prompts for generative UI should define the data shapes that trigger component rendering and the expected visual output for each type. diff --git a/apps/website/content/docs/chat/core-capabilities/generative-ui/python/testing.mdx b/apps/website/content/docs/chat/core-capabilities/generative-ui/python/testing.mdx new file mode 100644 index 000000000..cfa571e71 --- /dev/null +++ b/apps/website/content/docs/chat/core-capabilities/generative-ui/python/testing.mdx @@ -0,0 +1,3 @@ +# Generative UI Testing + +Test generative UI by sending structured response data and verifying that the correct inline component renders with the expected inputs. diff --git a/apps/website/content/docs/chat/core-capabilities/input/python/build.mdx b/apps/website/content/docs/chat/core-capabilities/input/python/build.mdx new file mode 100644 index 000000000..66a31d77c --- /dev/null +++ b/apps/website/content/docs/chat/core-capabilities/input/python/build.mdx @@ -0,0 +1,3 @@ +# Build Input Workflows + +Design input workflows around a single input component that handles text capture, validation, and submission events. diff --git a/apps/website/content/docs/chat/core-capabilities/input/python/code.mdx b/apps/website/content/docs/chat/core-capabilities/input/python/code.mdx new file mode 100644 index 000000000..f0b93dc3b --- /dev/null +++ b/apps/website/content/docs/chat/core-capabilities/input/python/code.mdx @@ -0,0 +1,3 @@ +# Input Code + +Code examples for input show how to configure the input component, handle submission events, and manage loading states. diff --git a/apps/website/content/docs/chat/core-capabilities/input/python/overview.mdx b/apps/website/content/docs/chat/core-capabilities/input/python/overview.mdx new file mode 100644 index 000000000..2866b2728 --- /dev/null +++ b/apps/website/content/docs/chat/core-capabilities/input/python/overview.mdx @@ -0,0 +1,5 @@ +# Input Overview + +The input system captures user text, handles submission, and manages input state like loading indicators and disabled states. + +Use this page when you need to customize input behavior or add features like file attachments or command detection. diff --git a/apps/website/content/docs/chat/core-capabilities/input/python/prompts.mdx b/apps/website/content/docs/chat/core-capabilities/input/python/prompts.mdx new file mode 100644 index 000000000..2525327a8 --- /dev/null +++ b/apps/website/content/docs/chat/core-capabilities/input/python/prompts.mdx @@ -0,0 +1,3 @@ +# Input Prompts + +Prompts for input configuration should specify placeholder text, validation rules, and any pre-processing before submission. diff --git a/apps/website/content/docs/chat/core-capabilities/input/python/testing.mdx b/apps/website/content/docs/chat/core-capabilities/input/python/testing.mdx new file mode 100644 index 000000000..2a97d31e4 --- /dev/null +++ b/apps/website/content/docs/chat/core-capabilities/input/python/testing.mdx @@ -0,0 +1,3 @@ +# Input Testing + +Test input handling by simulating user keystrokes and submissions, then verifying the emitted message content and input state transitions. diff --git a/apps/website/content/docs/chat/core-capabilities/interrupts/python/build.mdx b/apps/website/content/docs/chat/core-capabilities/interrupts/python/build.mdx new file mode 100644 index 000000000..7ced051de --- /dev/null +++ b/apps/website/content/docs/chat/core-capabilities/interrupts/python/build.mdx @@ -0,0 +1,3 @@ +# Build Interrupt Workflows + +Design interrupt workflows around explicit pause points that present the user with a clear action and resume the stream on response. diff --git a/apps/website/content/docs/chat/core-capabilities/interrupts/python/code.mdx b/apps/website/content/docs/chat/core-capabilities/interrupts/python/code.mdx new file mode 100644 index 000000000..0a486e1cb --- /dev/null +++ b/apps/website/content/docs/chat/core-capabilities/interrupts/python/code.mdx @@ -0,0 +1,3 @@ +# Interrupts Code + +Code examples for interrupts show how to emit an interrupt event, render a confirmation UI, and resume the response stream. diff --git a/apps/website/content/docs/chat/core-capabilities/interrupts/python/overview.mdx b/apps/website/content/docs/chat/core-capabilities/interrupts/python/overview.mdx new file mode 100644 index 000000000..a27649cac --- /dev/null +++ b/apps/website/content/docs/chat/core-capabilities/interrupts/python/overview.mdx @@ -0,0 +1,5 @@ +# Interrupts Overview + +Interrupts pause the assistant response stream to collect user confirmation or additional input before continuing. + +Use this page when you need to add human-in-the-loop checkpoints to your chat workflows. diff --git a/apps/website/content/docs/chat/core-capabilities/interrupts/python/prompts.mdx b/apps/website/content/docs/chat/core-capabilities/interrupts/python/prompts.mdx new file mode 100644 index 000000000..8eda4fe43 --- /dev/null +++ b/apps/website/content/docs/chat/core-capabilities/interrupts/python/prompts.mdx @@ -0,0 +1,3 @@ +# Interrupts Prompts + +Prompts for interrupts should describe the pause condition, the user-facing question, and the expected response format. diff --git a/apps/website/content/docs/chat/core-capabilities/interrupts/python/testing.mdx b/apps/website/content/docs/chat/core-capabilities/interrupts/python/testing.mdx new file mode 100644 index 000000000..663bea01e --- /dev/null +++ b/apps/website/content/docs/chat/core-capabilities/interrupts/python/testing.mdx @@ -0,0 +1,3 @@ +# Interrupts Testing + +Test interrupts by triggering a pause condition and verifying that the stream stops, the prompt renders, and resumption produces the correct continuation. diff --git a/apps/website/content/docs/chat/core-capabilities/messages/python/build.mdx b/apps/website/content/docs/chat/core-capabilities/messages/python/build.mdx new file mode 100644 index 000000000..2c2da917d --- /dev/null +++ b/apps/website/content/docs/chat/core-capabilities/messages/python/build.mdx @@ -0,0 +1,3 @@ +# Build Message Workflows + +Design message workflows around a clear message type hierarchy and consistent serialization for transport and storage. diff --git a/apps/website/content/docs/chat/core-capabilities/messages/python/code.mdx b/apps/website/content/docs/chat/core-capabilities/messages/python/code.mdx new file mode 100644 index 000000000..92fc3ca80 --- /dev/null +++ b/apps/website/content/docs/chat/core-capabilities/messages/python/code.mdx @@ -0,0 +1,3 @@ +# Messages Code + +Code examples for messages show how to create, send, receive, and render message objects in the chat component. diff --git a/apps/website/content/docs/chat/core-capabilities/messages/python/overview.mdx b/apps/website/content/docs/chat/core-capabilities/messages/python/overview.mdx new file mode 100644 index 000000000..db48b7198 --- /dev/null +++ b/apps/website/content/docs/chat/core-capabilities/messages/python/overview.mdx @@ -0,0 +1,5 @@ +# Messages Overview + +Messages are the core data unit in Chat, representing user inputs, assistant responses, and system instructions in a conversation. + +Use this page when you need to understand how messages flow through the chat pipeline and how they are stored and displayed. diff --git a/apps/website/content/docs/chat/core-capabilities/messages/python/prompts.mdx b/apps/website/content/docs/chat/core-capabilities/messages/python/prompts.mdx new file mode 100644 index 000000000..f6f31733c --- /dev/null +++ b/apps/website/content/docs/chat/core-capabilities/messages/python/prompts.mdx @@ -0,0 +1,3 @@ +# Messages Prompts + +Prompts for message handling should define the expected message roles, content formats, and any metadata attached to each message. diff --git a/apps/website/content/docs/chat/core-capabilities/messages/python/testing.mdx b/apps/website/content/docs/chat/core-capabilities/messages/python/testing.mdx new file mode 100644 index 000000000..660f9bbd0 --- /dev/null +++ b/apps/website/content/docs/chat/core-capabilities/messages/python/testing.mdx @@ -0,0 +1,3 @@ +# Messages Testing + +Test message handling by verifying that sent messages appear in the conversation thread with the correct role, content, and ordering. diff --git a/apps/website/content/docs/chat/core-capabilities/subagents/python/build.mdx b/apps/website/content/docs/chat/core-capabilities/subagents/python/build.mdx new file mode 100644 index 000000000..b2c73a0c4 --- /dev/null +++ b/apps/website/content/docs/chat/core-capabilities/subagents/python/build.mdx @@ -0,0 +1,3 @@ +# Build Subagent Workflows + +Design subagent workflows around clear delegation boundaries where the parent agent routes to a child and merges the result. diff --git a/apps/website/content/docs/chat/core-capabilities/subagents/python/code.mdx b/apps/website/content/docs/chat/core-capabilities/subagents/python/code.mdx new file mode 100644 index 000000000..48f02f8f9 --- /dev/null +++ b/apps/website/content/docs/chat/core-capabilities/subagents/python/code.mdx @@ -0,0 +1,3 @@ +# Subagent Code + +Code examples for subagents show how to register child agents, route messages to them, and merge their responses into the main thread. diff --git a/apps/website/content/docs/chat/core-capabilities/subagents/python/overview.mdx b/apps/website/content/docs/chat/core-capabilities/subagents/python/overview.mdx new file mode 100644 index 000000000..b13e78b99 --- /dev/null +++ b/apps/website/content/docs/chat/core-capabilities/subagents/python/overview.mdx @@ -0,0 +1,5 @@ +# Subagents Overview + +Subagents delegate parts of a conversation to specialized agents that handle domain-specific tasks and return results to the main thread. + +Use this page when you need to compose multiple agents within a single chat experience. diff --git a/apps/website/content/docs/chat/core-capabilities/subagents/python/prompts.mdx b/apps/website/content/docs/chat/core-capabilities/subagents/python/prompts.mdx new file mode 100644 index 000000000..08c8efd74 --- /dev/null +++ b/apps/website/content/docs/chat/core-capabilities/subagents/python/prompts.mdx @@ -0,0 +1,3 @@ +# Subagent Prompts + +Prompts for subagents should define the delegation trigger, the child agent's role, and how its response integrates into the parent conversation. diff --git a/apps/website/content/docs/chat/core-capabilities/subagents/python/testing.mdx b/apps/website/content/docs/chat/core-capabilities/subagents/python/testing.mdx new file mode 100644 index 000000000..03dbc210e --- /dev/null +++ b/apps/website/content/docs/chat/core-capabilities/subagents/python/testing.mdx @@ -0,0 +1,3 @@ +# Subagent Testing + +Test subagent delegation by triggering a routing condition and verifying that the child agent handles the task and returns the expected result. diff --git a/apps/website/content/docs/chat/core-capabilities/theming/python/build.mdx b/apps/website/content/docs/chat/core-capabilities/theming/python/build.mdx new file mode 100644 index 000000000..ae3054c9d --- /dev/null +++ b/apps/website/content/docs/chat/core-capabilities/theming/python/build.mdx @@ -0,0 +1,3 @@ +# Build Theming Workflows + +Design theming workflows around a set of CSS custom properties that cascade through all chat components for consistent styling. diff --git a/apps/website/content/docs/chat/core-capabilities/theming/python/code.mdx b/apps/website/content/docs/chat/core-capabilities/theming/python/code.mdx new file mode 100644 index 000000000..96aae2651 --- /dev/null +++ b/apps/website/content/docs/chat/core-capabilities/theming/python/code.mdx @@ -0,0 +1,3 @@ +# Theming Code + +Code examples for theming show how to set CSS custom properties, override component styles, and switch between light and dark modes. diff --git a/apps/website/content/docs/chat/core-capabilities/theming/python/overview.mdx b/apps/website/content/docs/chat/core-capabilities/theming/python/overview.mdx new file mode 100644 index 000000000..cdd44356d --- /dev/null +++ b/apps/website/content/docs/chat/core-capabilities/theming/python/overview.mdx @@ -0,0 +1,5 @@ +# Theming Overview + +Theming controls the visual appearance of the chat interface through CSS custom properties and component-level style overrides. + +Use this page when you need to match the chat UI to your application's design system or brand. diff --git a/apps/website/content/docs/chat/core-capabilities/theming/python/prompts.mdx b/apps/website/content/docs/chat/core-capabilities/theming/python/prompts.mdx new file mode 100644 index 000000000..10bede6bb --- /dev/null +++ b/apps/website/content/docs/chat/core-capabilities/theming/python/prompts.mdx @@ -0,0 +1,3 @@ +# Theming Prompts + +Prompts for theming should specify the design tokens, color palette, typography scale, and spacing values used across the chat UI. diff --git a/apps/website/content/docs/chat/core-capabilities/theming/python/testing.mdx b/apps/website/content/docs/chat/core-capabilities/theming/python/testing.mdx new file mode 100644 index 000000000..ce4731bfa --- /dev/null +++ b/apps/website/content/docs/chat/core-capabilities/theming/python/testing.mdx @@ -0,0 +1,3 @@ +# Theming Testing + +Test theming by applying a custom theme and verifying that CSS custom property values propagate correctly to all chat components. diff --git a/apps/website/content/docs/chat/core-capabilities/threads/python/build.mdx b/apps/website/content/docs/chat/core-capabilities/threads/python/build.mdx new file mode 100644 index 000000000..e2a239e72 --- /dev/null +++ b/apps/website/content/docs/chat/core-capabilities/threads/python/build.mdx @@ -0,0 +1,3 @@ +# Build Thread Workflows + +Design thread workflows around a thread store that persists messages and supports operations like resume, branch, and delete. diff --git a/apps/website/content/docs/chat/core-capabilities/threads/python/code.mdx b/apps/website/content/docs/chat/core-capabilities/threads/python/code.mdx new file mode 100644 index 000000000..c81cda64c --- /dev/null +++ b/apps/website/content/docs/chat/core-capabilities/threads/python/code.mdx @@ -0,0 +1,3 @@ +# Thread Code + +Code examples for threads show how to create, load, resume, and branch conversation threads using the thread store API. diff --git a/apps/website/content/docs/chat/core-capabilities/threads/python/overview.mdx b/apps/website/content/docs/chat/core-capabilities/threads/python/overview.mdx new file mode 100644 index 000000000..59d114293 --- /dev/null +++ b/apps/website/content/docs/chat/core-capabilities/threads/python/overview.mdx @@ -0,0 +1,5 @@ +# Threads Overview + +Threads organize conversations into persistent sessions that can be resumed, branched, or archived. + +Use this page when you need to manage conversation history and multi-session continuity. diff --git a/apps/website/content/docs/chat/core-capabilities/threads/python/prompts.mdx b/apps/website/content/docs/chat/core-capabilities/threads/python/prompts.mdx new file mode 100644 index 000000000..1a400c1be --- /dev/null +++ b/apps/website/content/docs/chat/core-capabilities/threads/python/prompts.mdx @@ -0,0 +1,3 @@ +# Thread Prompts + +Prompts for thread management should specify how threads are created, resumed, and how context is carried across sessions. diff --git a/apps/website/content/docs/chat/core-capabilities/threads/python/testing.mdx b/apps/website/content/docs/chat/core-capabilities/threads/python/testing.mdx new file mode 100644 index 000000000..abb10a8c7 --- /dev/null +++ b/apps/website/content/docs/chat/core-capabilities/threads/python/testing.mdx @@ -0,0 +1,3 @@ +# Thread Testing + +Test thread management by creating threads, adding messages, resuming sessions, and verifying that conversation history is preserved correctly. diff --git a/apps/website/content/docs/chat/core-capabilities/timeline/python/build.mdx b/apps/website/content/docs/chat/core-capabilities/timeline/python/build.mdx new file mode 100644 index 000000000..547c3a45b --- /dev/null +++ b/apps/website/content/docs/chat/core-capabilities/timeline/python/build.mdx @@ -0,0 +1,3 @@ +# Build Timeline Workflows + +Design timeline workflows around a virtualized message list that handles large histories and smooth auto-scrolling during streaming. diff --git a/apps/website/content/docs/chat/core-capabilities/timeline/python/code.mdx b/apps/website/content/docs/chat/core-capabilities/timeline/python/code.mdx new file mode 100644 index 000000000..f6fdc59df --- /dev/null +++ b/apps/website/content/docs/chat/core-capabilities/timeline/python/code.mdx @@ -0,0 +1,3 @@ +# Timeline Code + +Code examples for the timeline show how to configure the message list, add custom message templates, and control scroll behavior. diff --git a/apps/website/content/docs/chat/core-capabilities/timeline/python/overview.mdx b/apps/website/content/docs/chat/core-capabilities/timeline/python/overview.mdx new file mode 100644 index 000000000..ede17ff4c --- /dev/null +++ b/apps/website/content/docs/chat/core-capabilities/timeline/python/overview.mdx @@ -0,0 +1,5 @@ +# Timeline Overview + +The timeline renders the conversation as a scrollable sequence of message bubbles with automatic scroll-to-bottom and streaming indicators. + +Use this page when you need to customize how messages are displayed or add features like timestamps and read receipts. diff --git a/apps/website/content/docs/chat/core-capabilities/timeline/python/prompts.mdx b/apps/website/content/docs/chat/core-capabilities/timeline/python/prompts.mdx new file mode 100644 index 000000000..86db8de0a --- /dev/null +++ b/apps/website/content/docs/chat/core-capabilities/timeline/python/prompts.mdx @@ -0,0 +1,3 @@ +# Timeline Prompts + +Prompts for timeline configuration should describe the desired message layout, grouping rules, and any custom rendering for specific message types. diff --git a/apps/website/content/docs/chat/core-capabilities/timeline/python/testing.mdx b/apps/website/content/docs/chat/core-capabilities/timeline/python/testing.mdx new file mode 100644 index 000000000..5b798d211 --- /dev/null +++ b/apps/website/content/docs/chat/core-capabilities/timeline/python/testing.mdx @@ -0,0 +1,3 @@ +# Timeline Testing + +Test the timeline by rendering a message sequence and verifying scroll position, message ordering, and streaming indicator behavior. diff --git a/apps/website/content/docs/chat/core-capabilities/tool-calls/python/build.mdx b/apps/website/content/docs/chat/core-capabilities/tool-calls/python/build.mdx new file mode 100644 index 000000000..efcdb65be --- /dev/null +++ b/apps/website/content/docs/chat/core-capabilities/tool-calls/python/build.mdx @@ -0,0 +1,3 @@ +# Build Tool Call Workflows + +Design tool call workflows around a registry of available tools, each with a clear schema and a handler that returns structured results. diff --git a/apps/website/content/docs/chat/core-capabilities/tool-calls/python/code.mdx b/apps/website/content/docs/chat/core-capabilities/tool-calls/python/code.mdx new file mode 100644 index 000000000..9b6a4f193 --- /dev/null +++ b/apps/website/content/docs/chat/core-capabilities/tool-calls/python/code.mdx @@ -0,0 +1,3 @@ +# Tool Call Code + +Code examples for tool calls show how to register tools, handle invocation events, execute functions, and return results to the stream. diff --git a/apps/website/content/docs/chat/core-capabilities/tool-calls/python/overview.mdx b/apps/website/content/docs/chat/core-capabilities/tool-calls/python/overview.mdx new file mode 100644 index 000000000..ee522ad9b --- /dev/null +++ b/apps/website/content/docs/chat/core-capabilities/tool-calls/python/overview.mdx @@ -0,0 +1,5 @@ +# Tool Calls Overview + +Tool calls let the assistant invoke external functions during a conversation and incorporate the results into its response. + +Use this page when you need to understand how tool call requests and results flow through the chat pipeline. diff --git a/apps/website/content/docs/chat/core-capabilities/tool-calls/python/prompts.mdx b/apps/website/content/docs/chat/core-capabilities/tool-calls/python/prompts.mdx new file mode 100644 index 000000000..42d9689d2 --- /dev/null +++ b/apps/website/content/docs/chat/core-capabilities/tool-calls/python/prompts.mdx @@ -0,0 +1,3 @@ +# Tool Call Prompts + +Prompts for tool calls should list the available tools, their parameter schemas, and guidelines for when the assistant should invoke them. diff --git a/apps/website/content/docs/chat/core-capabilities/tool-calls/python/testing.mdx b/apps/website/content/docs/chat/core-capabilities/tool-calls/python/testing.mdx new file mode 100644 index 000000000..87938d8b4 --- /dev/null +++ b/apps/website/content/docs/chat/core-capabilities/tool-calls/python/testing.mdx @@ -0,0 +1,3 @@ +# Tool Call Testing + +Test tool calls by providing a tool registry, triggering an invocation, and verifying the result is incorporated into the assistant response. diff --git a/apps/website/content/docs/chat/getting-started/overview/python/overview.mdx b/apps/website/content/docs/chat/getting-started/overview/python/overview.mdx new file mode 100644 index 000000000..cd46bcdaa --- /dev/null +++ b/apps/website/content/docs/chat/getting-started/overview/python/overview.mdx @@ -0,0 +1,5 @@ +# Chat Overview + +Chat provides a full-featured Angular chat interface for conversational AI applications with streaming, tool calls, and generative UI. + +Start here when you want the fastest path to a working Chat app in Angular. The product-first guides focus on the core workflow, then connect you to messages, threads, interrupts, and testing as you grow. From 4567099b1552523f308d6eb8634d615b88078c24 Mon Sep 17 00:00:00 2001 From: Brian Love Date: Wed, 8 Apr 2026 12:07:03 -0700 Subject: [PATCH 21/48] fix(cockpit): rewrite render examples as standalone demos without chat dependency Co-Authored-By: Claude Opus 4.6 (1M context) --- .../angular/src/app/app.config.ts | 13 +- .../src/app/computed-functions.component.ts | 146 ++++++++++---- .../environments/environment.development.ts | 7 +- .../angular/src/environments/environment.ts | 7 +- .../angular/src/app/app.config.ts | 5 - .../src/app/element-rendering.component.ts | 145 +++++++++----- .../environments/environment.development.ts | 7 +- .../angular/src/environments/environment.ts | 7 +- .../registry/angular/src/app/app.config.ts | 5 - .../angular/src/app/registry.component.ts | 163 ++++++++++++---- .../environments/environment.development.ts | 7 +- .../angular/src/environments/environment.ts | 7 +- .../angular/src/app/app.config.ts | 5 - .../angular/src/app/repeat-loops.component.ts | 128 ++++++++---- .../environments/environment.development.ts | 7 +- .../angular/src/environments/environment.ts | 7 +- .../angular/src/app/app.config.ts | 5 - .../src/app/spec-rendering.component.ts | 158 ++++++++++++--- .../environments/environment.development.ts | 7 +- .../angular/src/environments/environment.ts | 7 +- .../angular/src/app/app.config.ts | 5 - .../src/app/state-management.component.ts | 182 +++++++++++++----- .../environments/environment.development.ts | 7 +- .../angular/src/environments/environment.ts | 7 +- 24 files changed, 712 insertions(+), 332 deletions(-) diff --git a/cockpit/render/computed-functions/angular/src/app/app.config.ts b/cockpit/render/computed-functions/angular/src/app/app.config.ts index eb89fe1df..fa452f8d8 100644 --- a/cockpit/render/computed-functions/angular/src/app/app.config.ts +++ b/cockpit/render/computed-functions/angular/src/app/app.config.ts @@ -1,20 +1,15 @@ // SPDX-License-Identifier: PolyForm-Noncommercial-1.0.0 import { ApplicationConfig } from '@angular/core'; -import { provideStreamResource } from '@cacheplane/stream-resource'; -import { provideChat } from '@cacheplane/chat'; import { provideRender } from '@cacheplane/render'; -import { environment } from '../environments/environment'; export const appConfig: ApplicationConfig = { providers: [ - provideStreamResource({ apiUrl: environment.langGraphApiUrl }), - provideChat({}), provideRender({ functions: { - formatDate: (value: string) => new Date(value).toLocaleDateString(), - uppercase: (value: string) => value.toUpperCase(), - multiply: (a: number, b: number) => a * b, - reverse: (value: string) => value.split('').reverse().join(''), + formatDate: (args: Record) => new Date(args['value'] as string).toLocaleDateString(), + uppercase: (args: Record) => (args['value'] as string).toUpperCase(), + multiply: (args: Record) => (args['a'] as number) * (args['b'] as number), + reverse: (args: Record) => (args['value'] as string).split('').reverse().join(''), }, }), ], diff --git a/cockpit/render/computed-functions/angular/src/app/computed-functions.component.ts b/cockpit/render/computed-functions/angular/src/app/computed-functions.component.ts index 34f3078eb..6cb938e33 100644 --- a/cockpit/render/computed-functions/angular/src/app/computed-functions.component.ts +++ b/cockpit/render/computed-functions/angular/src/app/computed-functions.component.ts @@ -1,70 +1,142 @@ // SPDX-License-Identifier: PolyForm-Noncommercial-1.0.0 -import { Component, computed, signal } from '@angular/core'; -import { JsonPipe, DatePipe } from '@angular/common'; -import { ChatComponent } from '@cacheplane/chat'; -import { RenderSpecComponent } from '@cacheplane/render'; -import { streamResource } from '@cacheplane/stream-resource'; -import { environment } from '../environments/environment'; +import { Component, input, computed, signal } from '@angular/core'; +import { JsonPipe } from '@angular/common'; +import { + RenderSpecComponent, + defineAngularRegistry, + signalStateStore, +} from '@cacheplane/render'; +import type { Spec } from '@json-render/core'; + +// --- Inline view component --- + +@Component({ + selector: 'demo-value', + standalone: true, + template: ` +
+ {{ label() }}: + {{ value() }} +
+ `, +}) +class DemoValueComponent { + readonly label = input(''); + readonly value = input(''); + readonly childKeys = input([]); + readonly spec = input(null); +} /** * ComputedFunctionsComponent demonstrates computed functions from @cacheplane/render. * - * Shows how custom functions transform data for prop resolution in render specs. - * The sidebar displays computed values including formatted dates, string transforms, - * and math operations. + * Shows how custom functions registered via provideRender transform data + * for prop resolution in render specs. The main area renders computed values. + * The sidebar shows the function registry, live computed outputs, and an + * editable input. */ @Component({ selector: 'app-computed-functions', standalone: true, - imports: [ChatComponent, RenderSpecComponent, JsonPipe, DatePipe], + imports: [RenderSpecComponent, JsonPipe], template: ` -
- -