From 5046780af5806bb69cacd3d0bfdfee7bda3782e6 Mon Sep 17 00:00:00 2001 From: Brian Love Date: Sun, 5 Apr 2026 17:46:21 -0700 Subject: [PATCH 1/2] =?UTF-8?q?feat(cockpit):=20Tier=202=20=E2=80=94=20mem?= =?UTF-8?q?ory=20sidebar,=20subagent=20tracking,=20interrupt=20panel?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Rewrites the three LangGraph cockpit examples to use alongside capability-specific sidebars/panels, replacing LegacyChatComponent. - memory: derives memoryEntries from stream.value().memory (confirmed key from Python MemoryState) - subgraphs: derives subagentEntries from stream.subagents() Map with status + message count - interrupts: wires ChatInterruptPanelComponent (action) output to stream.submit(null) resume Co-Authored-By: Claude Sonnet 4.6 --- .../angular/src/app/interrupts.component.ts | 82 ++++++++----------- .../angular/src/app/memory.component.ts | 61 ++++++-------- .../angular/src/app/subgraphs.component.ts | 62 +++++++------- 3 files changed, 88 insertions(+), 117 deletions(-) diff --git a/cockpit/langgraph/interrupts/angular/src/app/interrupts.component.ts b/cockpit/langgraph/interrupts/angular/src/app/interrupts.component.ts index 57b9428b1..0c41a4b08 100644 --- a/cockpit/langgraph/interrupts/angular/src/app/interrupts.component.ts +++ b/cockpit/langgraph/interrupts/angular/src/app/interrupts.component.ts @@ -1,5 +1,6 @@ +// SPDX-License-Identifier: PolyForm-Noncommercial-1.0.0 import { Component } from '@angular/core'; -import { LegacyChatComponent } from '@cacheplane/chat'; +import { ChatComponent, ChatInterruptPanelComponent, type InterruptAction } from '@cacheplane/chat'; import { streamResource } from '@cacheplane/stream-resource'; import { environment } from '../environments/environment'; @@ -8,42 +9,26 @@ import { environment } from '../environments/environment'; * * The LangGraph backend pauses execution when it needs human approval. * The `stream.interrupt()` signal provides the interrupt data, and - * `stream.submit()` resumes execution with the human's decision. + * `stream.submit(null)` resumes execution with the human's decision. * * Key integration points: - * - `stream.interrupt()` — current pause data - * - `stream.submit({ resume: true })` — resume after approval - * - The graph uses LangGraph's `interrupt()` function to pause + * - `stream.interrupt()` — current pause data (undefined when not interrupted) + * - `ChatInterruptPanelComponent` — renders the approval UI with action buttons + * - `stream.submit(null)` — resumes the graph (LangGraph convention) */ @Component({ selector: 'app-interrupts', standalone: true, - imports: [LegacyChatComponent], + imports: [ChatComponent, ChatInterruptPanelComponent], template: ` - - -

Approvals

- @if (stream.interrupt()) { -
-

{{ stream.interrupt() }}

- - -
- } @else { -

No pending approvals

- } -
-
+
+ + @if (stream.interrupt()) { +
+ +
+ } +
`, }) export class InterruptsComponent { @@ -51,7 +36,7 @@ export class InterruptsComponent { * The streaming resource with interrupt support. * * When the LangGraph backend calls `interrupt()`, the `stream.interrupt()` - * signal emits the interrupt payload for display in the sidebar. + * signal emits the interrupt payload for display via ChatInterruptPanelComponent. */ protected readonly stream = streamResource({ apiUrl: environment.langGraphApiUrl, @@ -59,25 +44,22 @@ export class InterruptsComponent { }); /** - * Submit a message to the assistant. - */ - send(text: string): void { - this.stream.submit({ messages: [{ role: 'human', content: text }] }); - } - - /** - * Approve the pending action and resume execution. - * Submitting null continues the graph (LangGraph convention). - */ - approve(): void { - this.stream.submit(null); - } - - /** - * Reject the pending action. Sends a resume value of false - * so the graph can handle rejection logic. + * Handle an interrupt action from the panel. + * + * Submitting null resumes the graph unconditionally (LangGraph convention). + * Tier 3 will add edit/respond flows with richer resume payloads. */ - reject(): void { - this.stream.submit({ resume: false }); + protected onInterruptAction(action: InterruptAction): void { + switch (action) { + case 'accept': + this.stream.submit(null); // Resume with approval + break; + case 'ignore': + case 'respond': + case 'edit': + // For now, just resume — Tier 3 will add edit/respond flows + this.stream.submit(null); + break; + } } } diff --git a/cockpit/langgraph/memory/angular/src/app/memory.component.ts b/cockpit/langgraph/memory/angular/src/app/memory.component.ts index c6ca4fffd..cfd879daf 100644 --- a/cockpit/langgraph/memory/angular/src/app/memory.component.ts +++ b/cockpit/langgraph/memory/angular/src/app/memory.component.ts @@ -1,5 +1,6 @@ +// SPDX-License-Identifier: PolyForm-Noncommercial-1.0.0 import { Component, computed } from '@angular/core'; -import { LegacyChatComponent } from '@cacheplane/chat'; +import { ChatComponent } from '@cacheplane/chat'; import { streamResource } from '@cacheplane/stream-resource'; import { environment } from '../environments/environment'; @@ -12,38 +13,31 @@ import { environment } from '../environments/environment'; * * Key integration points: * - `stream.value()` exposes the full graph state, including the `memory` field - * - `memory()` signal is derived from `stream.value()` for reactive sidebar rendering + * - `memoryEntries` is derived from `stream.value()` for reactive sidebar rendering * - Facts appear in the sidebar as the agent learns them during conversation */ @Component({ selector: 'app-memory', standalone: true, - imports: [LegacyChatComponent], + imports: [ChatComponent], template: ` - - -

Agent Memory

+
+ + +
`, }) export class MemoryComponent { @@ -59,24 +53,15 @@ export class MemoryComponent { }); /** - * Reactive list of key-value memory entries derived from the graph state. + * Reactive list of [key, value] memory entries derived from the graph state. * - * The graph updates `memory` as it learns facts from the conversation. + * The Python graph stores learned facts in `state.memory` as a plain dict. * This signal re-computes whenever the stream state changes. */ protected readonly memoryEntries = computed(() => { - const state = this.stream.value() as { memory?: Record } | null; - const memory = state?.memory ?? {}; - return Object.entries(memory).map(([key, value]) => ({ - key, - value: typeof value === 'string' ? value : JSON.stringify(value), - })); + const val = this.stream.value() as Record; + const mem = val?.['memory']; + if (!mem || typeof mem !== 'object') return []; + return Object.entries(mem as Record); }); - - /** - * Submit a message to the agent. - */ - send(text: string): void { - this.stream.submit({ messages: [{ role: 'human', content: text }] }); - } } diff --git a/cockpit/langgraph/subgraphs/angular/src/app/subgraphs.component.ts b/cockpit/langgraph/subgraphs/angular/src/app/subgraphs.component.ts index 6bf543ed7..48a411ab4 100644 --- a/cockpit/langgraph/subgraphs/angular/src/app/subgraphs.component.ts +++ b/cockpit/langgraph/subgraphs/angular/src/app/subgraphs.component.ts @@ -1,5 +1,6 @@ +// SPDX-License-Identifier: PolyForm-Noncommercial-1.0.0 import { Component, computed } from '@angular/core'; -import { LegacyChatComponent } from '@cacheplane/chat'; +import { ChatComponent } from '@cacheplane/chat'; import { streamResource } from '@cacheplane/stream-resource'; import { environment } from '../environments/environment'; @@ -8,35 +9,38 @@ import { environment } from '../environments/environment'; * * This example shows how a parent orchestrator delegates tasks to child subgraphs. * The sidebar tracks active subagents in real time using `stream.subagents()`, - * a Map of running child graph executions and their current status. + * a Signal> of running child graph executions. * * Key integration points: * - `stream.subagents()` returns a Map of active subagents - * - Each entry has a unique run ID (key) and a `status()` signal ('running' | 'done' | 'error') - * - `subagentEntries` is a `computed()` signal derived from the map for iteration in the template + * - Each entry has a unique tool call ID (key) and a `status()` signal + * - `subagentEntries` is a `computed()` signal derived from the map for template iteration */ @Component({ selector: 'app-subgraphs', standalone: true, - imports: [LegacyChatComponent], + imports: [ChatComponent], template: ` - - -

Subagents

- @for (entry of subagentEntries(); track entry[0]) { -
- {{ entry[0].substring(0, 8) }}: {{ entry[1].status() }} -
+
+ + +
`, }) export class SubgraphsComponent { @@ -52,15 +56,15 @@ export class SubgraphsComponent { }); /** - * Derived signal: converts the subagents Map to an array of entries for template iteration. + * Derived signal: converts the subagents Map to an array for template iteration. * Using `computed()` ensures the template re-renders whenever the Map changes. */ - subagentEntries = computed(() => Array.from(this.stream.subagents().entries())); - - /** - * Submit a message to the orchestrator graph. - */ - send(text: string): void { - this.stream.submit({ messages: [{ role: 'human', content: text }] }); - } + protected readonly subagentEntries = computed(() => { + const map = this.stream.subagents(); + return Array.from(map.entries()).map(([id, ref]) => ({ + id, + status: ref.status(), + msgCount: ref.messages().length, + })); + }); } From d53ef736131e81684b5f48262e19a5cb3e0b81f9 Mon Sep 17 00:00:00 2001 From: Brian Love Date: Sun, 5 Apr 2026 17:47:44 -0700 Subject: [PATCH 2/2] feat: add ProblemSection with animated gap progress bar Co-Authored-By: Claude Sonnet 4.6 --- .../src/components/landing/ProblemSection.tsx | 322 ++++++++++++++++++ 1 file changed, 322 insertions(+) create mode 100644 apps/website/src/components/landing/ProblemSection.tsx diff --git a/apps/website/src/components/landing/ProblemSection.tsx b/apps/website/src/components/landing/ProblemSection.tsx new file mode 100644 index 000000000..283232b3a --- /dev/null +++ b/apps/website/src/components/landing/ProblemSection.tsx @@ -0,0 +1,322 @@ +'use client'; +import { useRef, useEffect, useState, useCallback } from 'react'; +import { motion, useInView } from 'framer-motion'; +import { tokens } from '@cacheplane/design-tokens'; + +const STATS = [ + { num: '66%', label: 'of AI solutions are almost right — not quite production-ready' }, + { num: '31%', label: 'of prioritized AI use cases actually reach production' }, + { num: '75%', label: 'of developers still want a human in the loop when trust breaks down' }, +]; + +function useCounter(target: number, duration: number, running: boolean) { + const [value, setValue] = useState(0); + useEffect(() => { + if (!running) return; + const start = performance.now(); + let raf: number; + function tick(now: number) { + const t = Math.min((now - start) / duration, 1); + const eased = t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t; + setValue(Math.round(eased * target)); + if (t < 1) raf = requestAnimationFrame(tick); + } + raf = requestAnimationFrame(tick); + return () => cancelAnimationFrame(raf); + }, [running, target, duration]); + return value; +} + +type Phase = 'idle' | 'filling' | 'stall' | 'closing' | 'done'; + +export function ProblemSection() { + const triggerRef = useRef(null); + const inView = useInView(triggerRef, { once: true, amount: 0.3 }); + const [phase, setPhase] = useState('idle'); + const [fillWidth, setFillWidth] = useState('0%'); + const [fillGradient, setFillGradient] = useState( + `linear-gradient(90deg, rgba(221,0,49,.6), rgba(221,0,49,.4))` + ); + const [fillTransition, setFillTransition] = useState('none'); + const counterRunning77 = phase === 'filling'; + const counterRunning100 = phase === 'closing' || phase === 'done'; + const count77 = useCounter(77, 1700, counterRunning77); + const count100 = useCounter(23, 1000, counterRunning100); + const displayCount = phase === 'done' ? 100 : phase === 'closing' ? 77 + count100 : count77; + + const runAnimation = useCallback(() => { + if (phase !== 'idle') return; + // Phase 1: fill to 77% + setTimeout(() => { + setFillTransition('width 1.7s cubic-bezier(.4,0,.2,1)'); + setFillWidth('77%'); + setPhase('filling'); + }, 150); + // Phase 2: stall + setTimeout(() => setPhase('stall'), 2100); + // Phase 3: close gap + setTimeout(() => { + setFillTransition('width 1s cubic-bezier(.4,0,.2,1)'); + setFillGradient( + 'linear-gradient(90deg, rgba(221,0,49,.5) 0%, rgba(221,0,49,.38) 70%, rgba(0,64,144,.8) 82%, #004090 100%)' + ); + setFillWidth('100%'); + setPhase('closing'); + }, 3200); + // Phase 4: done + setTimeout(() => setPhase('done'), 4400); + }, [phase]); + + useEffect(() => { + if (inView) runAnimation(); + }, [inView, runAnimation]); + + const showStall = phase === 'stall'; + const showBadge = phase === 'closing' || phase === 'done'; + const showEnd = phase === 'done'; + + return ( +
+ {/* Eyebrow + headline */} + +

+ The Last Mile Problem +

+

+ Most AI projects get close.
+ Almost none ship. +

+

+ The issue is not generating a demo. It is shipping a trustworthy product. +

+
+ + {/* Stat cards */} +
+ {STATS.map((s, i) => ( + +
{s.num}
+

+ {s.label} +

+
+ ))} +
+ + {/* Gap animation */} + + {/* Labels row */} +
+ + Project kickoff + + + ⚠ Teams stall here + + + ✓ Production + +
+ + {/* Track — overflow:hidden clips fill at container boundary, no border-radius artifact */} +
+
+
+ {/* Hatch overlay (gap zone) */} +
+ + + + + + + + +
+
+ {/* Stall pin — outside the overflow:hidden track */} +
+
+
+ 77% +
+
+
+ + {/* Counter row */} +
+ + {displayCount}% + +
+ + StreamResource closes the gap +
+ + 100% + +
+ + {/* Tagline */} +

+ Your backend agent may already work. The frontend and production path is what slips the schedule. +

+ + + +
+ ); +}