Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
47 commits
Select commit Hold shift + click to select a range
4aa6eb2
feat(ai-orchestration): scaffold package
AlemTuzlak May 10, 2026
06e4f54
feat(ai-orchestration): add core public types
AlemTuzlak May 10, 2026
826920c
feat(ai-orchestration): add in-memory RunStore
AlemTuzlak May 10, 2026
8b39b2f
fix(ai-orchestration): align with repo lint conventions
AlemTuzlak May 10, 2026
03f95d8
feat(ai-orchestration): add approve, bindAgents, and retry primitives
AlemTuzlak May 10, 2026
fbb1114
feat(ai-orchestration): add state snapshot/diff and AG-UI event emit …
AlemTuzlak May 10, 2026
24702c6
feat(ai-orchestration): agent invocation with three return shapes
AlemTuzlak May 10, 2026
f2fcaaa
feat(ai-orchestration): workflow engine drive loop
AlemTuzlak May 10, 2026
916055a
fix(ai-orchestration): persist resumed state, share pendingEvents que…
AlemTuzlak May 10, 2026
2592f84
feat(ai-orchestration): public API helpers, SSE response, and index e…
AlemTuzlak May 10, 2026
8b3423a
feat(ai-client): WorkflowClient
AlemTuzlak May 10, 2026
018763d
feat(ai-react): useWorkflow + useOrchestration
AlemTuzlak May 10, 2026
c1334db
feat(ts-react-chat): article workflow demo
AlemTuzlak May 10, 2026
f8ce39d
feat(ts-react-chat): feature orchestrator demo
AlemTuzlak May 10, 2026
87d7129
feat(ts-react-chat): workflow & orchestration API routes
AlemTuzlak May 10, 2026
cf11d43
feat(ts-react-chat): workflow + orchestration demo pages
AlemTuzlak May 10, 2026
7b07432
test(ai-orchestration): engine smoke tests
AlemTuzlak May 10, 2026
de7f442
refactor(ai-orchestration): collapse runWorkflow/resumeWorkflow into …
AlemTuzlak May 10, 2026
2d273ac
refactor(ai-orchestration): drop toWorkflowSSEResponse wrapper
AlemTuzlak May 10, 2026
802548d
fix(ai-orchestration): make StepGenerator TNext=any so heterogeneous …
AlemTuzlak May 10, 2026
0c30b69
feat(ai-orchestration): pass agents map to orchestrator router for ty…
AlemTuzlak May 10, 2026
32a1987
refactor(ts-react-chat): simplify orchestrator router to be cast-free
AlemTuzlak May 10, 2026
a7bac73
chore(ts-react-chat): use toServerSentEventsResponse directly
AlemTuzlak May 10, 2026
5c2c310
feat(ai-orchestration): ok/fail result helpers
AlemTuzlak May 10, 2026
400dc3d
refactor(ai-orchestration): rename ok to succeed
AlemTuzlak May 10, 2026
58be487
feat(ai-orchestration): add defineRouter helper for extracted routers
AlemTuzlak May 10, 2026
aad5d98
refactor(ts-react-chat): drop redundant initialize and use defineRouter
AlemTuzlak May 10, 2026
95f095f
feat(ai-client): add endpoint shortcut for useWorkflow
AlemTuzlak May 10, 2026
247712b
refactor(ts-react-chat): use endpoint shortcut in demo pages
AlemTuzlak May 10, 2026
dd4bb66
feat(ai-orchestration): add handleWorkflowRequest server helper
AlemTuzlak May 10, 2026
5594a91
refactor(ts-react-chat): use handleWorkflowRequest in API routes
AlemTuzlak May 10, 2026
1473ace
refactor(ai-client): drop endpoint shortcut from WorkflowClient
AlemTuzlak May 10, 2026
474b73f
feat(ai-client): add fetchWorkflowEvents adapter helper
AlemTuzlak May 10, 2026
2e743ac
feat(ai-react): re-export fetchWorkflowEvents and FetchWorkflowEvents…
AlemTuzlak May 10, 2026
878dcfe
refactor(ai-orchestration): replace handleWorkflowRequest with parseW…
AlemTuzlak May 10, 2026
6253eee
refactor(ts-react-chat): use fetchWorkflowEvents + parseWorkflowRequest
AlemTuzlak May 10, 2026
9bbbc83
fix(ai-orchestration): default-import fast-json-patch for ESM/CJS int…
AlemTuzlak May 10, 2026
b7e7843
refactor(ai-orchestration): hand-roll JSON Patch differ, drop fast-js…
AlemTuzlak May 10, 2026
cefd387
fix(ai-react): stabilize useWorkflow client identity (mirror useChat …
AlemTuzlak May 10, 2026
6256f20
fix(ai-orchestration): share dispatch loop so resume handles full des…
AlemTuzlak May 10, 2026
37e31f3
feat(ts-react-chat): editorial-brutalist redesign of workflow + orche…
AlemTuzlak May 10, 2026
85224f3
feat(ai-orchestration,ai-client): stream workflow output through RUN_…
AlemTuzlak May 10, 2026
c0202ef
feat(ai-orchestration,ai-react): support free-text feedback on approv…
AlemTuzlak May 10, 2026
6db5d96
feat(ts-react-chat): article revision loop with editor feedback and 3…
AlemTuzlak May 10, 2026
f87140b
feat(ts-react-chat): live DraftPreview in workflow right column
AlemTuzlak May 10, 2026
9880c45
feat: finalize approval step on resume + modal preview of published a…
AlemTuzlak May 10, 2026
16354b7
ci: apply automated fixes
autofix-ci[bot] May 10, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions examples/ts-react-chat/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
"@tanstack/ai-ollama": "workspace:*",
"@tanstack/ai-openai": "workspace:*",
"@tanstack/ai-openrouter": "workspace:*",
"@tanstack/ai-orchestration": "workspace:*",
"@tanstack/ai-react": "workspace:*",
"@tanstack/ai-react-ui": "workspace:*",
"@tanstack/nitro-v2-vite-plugin": "^1.154.7",
Expand Down
120 changes: 120 additions & 0 deletions examples/ts-react-chat/src/components/ArticleModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
import { useEffect } from 'react'

interface Article {
title: string
paragraphs: Array<string>
}

export function ArticleModal(props: { article: Article; onClose: () => void }) {
// Close on Escape, lock body scroll while open.
useEffect(() => {
const onKey = (e: KeyboardEvent) => {
if (e.key === 'Escape') props.onClose()
}
document.addEventListener('keydown', onKey)
const prev = document.body.style.overflow
document.body.style.overflow = 'hidden'
return () => {
document.removeEventListener('keydown', onKey)
document.body.style.overflow = prev
}
}, [props])

const date = new Date().toLocaleDateString('en-US', {
year: 'numeric',
month: 'long',
day: 'numeric',
})

return (
<div
role="dialog"
aria-modal="true"
aria-label="Published article"
className="fixed inset-0 z-50 anim-log-in"
>
{/* backdrop */}
<div
onClick={props.onClose}
className="absolute inset-0 bg-ink/85 backdrop-blur-sm"
/>

{/* page wrapper — scrollable */}
<div className="relative h-full overflow-auto px-4 sm:px-8 py-10 flex justify-center">
<article className="relative max-w-3xl w-full bg-cream text-ink shadow-[16px_16px_0_0_var(--color-citron)] my-4">
{/* paper grain */}
<div
className="absolute inset-0 pointer-events-none opacity-25 mix-blend-multiply"
style={{
backgroundImage:
"url(\"data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='200' height='200'><filter id='n'><feTurbulence type='fractalNoise' baseFrequency='1.2' numOctaves='2' stitchTiles='stitch'/></filter><rect width='100%' height='100%' filter='url(%23n)' opacity='0.6'/></svg>\")",
}}
/>

{/* hazard tape header strip */}
<div className="tape-citron h-2.5" />

{/* close button */}
<button
onClick={props.onClose}
aria-label="Close"
className="absolute top-5 right-5 z-10 w-9 h-9 flex items-center justify-center bg-ink text-cream hover:bg-rust transition-colors label-mono"
>
</button>

<div className="relative px-8 sm:px-14 py-12">
{/* masthead */}
<div className="flex items-baseline justify-between border-b border-ink pb-3 mb-10">
<span className="label-mono text-rust">Published</span>
<span className="label-mono text-taupe-deep tabular">{date}</span>
</div>

<h1
className="text-[clamp(2.25rem,5.5vw,4.25rem)] leading-[0.96] tracking-tight mb-10"
style={{
fontFamily: 'var(--font-display)',
fontVariationSettings: "'opsz' 144, 'SOFT' 30, 'WONK' 1",
}}
>
{props.article.title}
</h1>

{/* article body — column layout for longer pieces */}
<div className="columns-1 md:columns-2 gap-10">
{props.article.paragraphs.map((p, i) => (
<p
key={i}
className={`mb-5 text-ink leading-[1.65] text-[17px] break-inside-avoid ${
i === 0
? 'first-letter:float-left first-letter:text-7xl first-letter:font-bold first-letter:leading-[0.85] first-letter:mr-3 first-letter:text-rust'
: ''
}`}
style={{
fontFamily: 'var(--font-display)',
fontVariationSettings: "'opsz' 17, 'SOFT' 100, 'WONK' 0",
}}
>
{p}
</p>
))}
</div>

{/* colophon */}
<footer className="mt-14 pt-5 border-t border-ink/40 flex items-baseline justify-between label-mono text-taupe-deep">
<span>TanStack AI · Article Pipeline</span>
<span>—fin—</span>
</footer>
</div>

<div className="tape-citron h-2.5" />
</article>
</div>

{/* corner hint */}
<div className="absolute bottom-4 left-1/2 -translate-x-1/2 label-mono text-bone/60">
press esc or click outside to close
</div>
</div>
)
}
116 changes: 116 additions & 0 deletions examples/ts-react-chat/src/components/DraftPreview.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
import { useEffect, useRef, useState } from 'react'

interface Draft {
title?: string
paragraphs?: Array<string>
}

export function DraftPreview(props: { draft: unknown; phase?: string }) {
const draft = (
props.draft && typeof props.draft === 'object' ? props.draft : null
) as Draft | null
Comment on lines +9 to +11
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Harden draft shape validation before using paragraphs.map.

draft is only object-checked/cast, so malformed payloads (e.g. paragraphs: "text" or {}) can reach Line 77 and crash when .map is invoked.

Suggested fix
 interface Draft {
   title?: string
   paragraphs?: Array<string>
 }
 
 export function DraftPreview(props: { draft: unknown; phase?: string }) {
-  const draft = (
-    props.draft && typeof props.draft === 'object' ? props.draft : null
-  ) as Draft | null
+  const raw = props.draft
+  const draft: Draft | null =
+    raw && typeof raw === 'object'
+      ? {
+          title: typeof (raw as { title?: unknown }).title === 'string'
+            ? (raw as { title: string }).title
+            : undefined,
+          paragraphs: Array.isArray((raw as { paragraphs?: unknown }).paragraphs)
+            ? (raw as { paragraphs: unknown[] }).paragraphs.filter(
+                (p): p is string => typeof p === 'string',
+              )
+            : undefined,
+        }
+      : null

Also applies to: 24-26, 77-92

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@examples/ts-react-chat/src/components/DraftPreview.tsx` around lines 9 - 11,
The current cast of props.draft to Draft lets malformed payloads reach code that
calls draft.paragraphs.map and crash; update the DraftPreview component to
perform runtime shape checks instead of blind casting: ensure props.draft is an
object and that draft.paragraphs is an Array (and optionally validate each item
has the expected fields) before using .map or rendering; modify the draft
initialization and any other places that access draft.paragraphs (the draft
variable and the rendering logic that iterates paragraphs) to guard with
Array.isArray(draft.paragraphs) (or fallback to an empty array) so .map is only
called on a real array and malformed inputs are safely handled.


// Pulse highlight when the draft content changes — gives a sense of life.
const [bumpKey, setBumpKey] = useState(0)
const lastSerialized = useRef('')
useEffect(() => {
const next = JSON.stringify(draft ?? {})
if (next !== lastSerialized.current) {
lastSerialized.current = next
setBumpKey((k) => k + 1)
}
}, [draft])

const hasContent =
draft && (draft.title || (draft.paragraphs && draft.paragraphs.length > 0))

return (
<aside className="relative">
<div className="flex items-baseline justify-between border-b border-bone pb-3 mb-4">
<span className="label-mono text-bone">Draft Preview</span>
<span className="label-mono text-taupe tabular">
{hasContent
? `${(draft.paragraphs?.length ?? 0).toString().padStart(2, '0')} ¶`
: '—'}
</span>
</div>

<div className="relative bg-cream text-ink shadow-[8px_8px_0_0_var(--color-ink-soft)] border border-ink overflow-hidden">
{/* phase stamp */}
{props.phase && (
<div className="absolute top-3 right-3 px-2 py-0.5 bg-ink text-cream label-mono">
{props.phase}
</div>
)}

{/* paper grain */}
<div
className="absolute inset-0 pointer-events-none opacity-30 mix-blend-multiply"
style={{
backgroundImage:
"url(\"data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='160' height='160'><filter id='n'><feTurbulence type='fractalNoise' baseFrequency='1.4' numOctaves='2' stitchTiles='stitch'/></filter><rect width='100%' height='100%' filter='url(%23n)' opacity='0.5'/></svg>\")",
}}
/>

<div
key={bumpKey}
className="relative px-6 py-7 max-h-[34rem] overflow-auto anim-log-in"
>
{!hasContent ? (
<Empty />
) : (
<>
<div className="label-mono text-taupe-deep mb-3">
Draft № {String(bumpKey).padStart(2, '0')}
</div>
{draft.title && (
<h2
className="text-[clamp(1.5rem,2.4vw,2rem)] leading-[0.98] tracking-tight mb-5"
style={{
fontFamily: 'var(--font-display)',
fontVariationSettings: "'opsz' 144, 'SOFT' 30, 'WONK' 1",
}}
>
{draft.title}
</h2>
)}
{draft.paragraphs?.map((p, i) => (
<p
key={i}
className={`mb-3.5 text-[14px] leading-[1.55] text-ink ${
i === 0
? 'first-letter:float-left first-letter:text-5xl first-letter:font-bold first-letter:leading-[0.85] first-letter:mr-2 first-letter:text-rust'
: ''
}`}
style={{
fontFamily: 'var(--font-display)',
fontVariationSettings: "'opsz' 14, 'SOFT' 100, 'WONK' 0",
}}
>
{p}
</p>
))}
</>
)}
</div>
</div>
</aside>
)
}

function Empty() {
return (
<div className="py-10 text-center">
<div
className="text-3xl text-taupe-deep italic mb-2"
style={{
fontFamily: 'var(--font-display)',
fontVariationSettings: "'opsz' 96, 'SOFT' 80, 'WONK' 1",
}}
>
no draft yet.
</div>
<div className="label-mono text-taupe">awaiting writer</div>
</div>
)
}
38 changes: 36 additions & 2 deletions examples/ts-react-chat/src/components/Header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,14 @@ import {
Braces,
FileAudio,
FileText,
GitBranch,
Guitar,
Home,
Image,
Menu,
Mic,
Music,
Network,
Video,
X,
} from 'lucide-react'
Expand Down Expand Up @@ -188,15 +190,47 @@ export default function Header() {
<Link
to="/realtime"
onClick={() => setIsOpen(false)}
className="flex items-center gap-3 p-3 rounded-lg hover:bg-gray-800 transition-colors mb-2"
className="flex items-center gap-3 p-3 rounded-lg hover:bg-gray-800 transition-colors mb-1"
activeProps={{
className:
'flex items-center gap-3 p-3 rounded-lg bg-cyan-600 hover:bg-cyan-700 transition-colors mb-2',
'flex items-center gap-3 p-3 rounded-lg bg-cyan-600 hover:bg-cyan-700 transition-colors mb-1',
}}
>
<Mic size={20} />
<span className="font-medium">Voice Chat (Realtime)</span>
</Link>

<hr className="border-gray-700 my-2" />

<p className="text-xs text-gray-500 uppercase tracking-wider px-3 pt-2 pb-1">
Orchestration
</p>

<Link
to="/workflow"
onClick={() => setIsOpen(false)}
className="flex items-center gap-3 p-3 rounded-lg hover:bg-gray-800 transition-colors mb-1"
activeProps={{
className:
'flex items-center gap-3 p-3 rounded-lg bg-cyan-600 hover:bg-cyan-700 transition-colors mb-1',
}}
>
<GitBranch size={20} />
<span className="font-medium">Article Workflow</span>
</Link>

<Link
to="/orchestration"
onClick={() => setIsOpen(false)}
className="flex items-center gap-3 p-3 rounded-lg hover:bg-gray-800 transition-colors mb-2"
activeProps={{
className:
'flex items-center gap-3 p-3 rounded-lg bg-cyan-600 hover:bg-cyan-700 transition-colors mb-2',
}}
>
<Network size={20} />
<span className="font-medium">Feature Orchestrator</span>
</Link>
</nav>
</aside>
</>
Expand Down
Loading
Loading