Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
51 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
1db8896
Merge remote-tracking branch 'origin/main' into worktree-cryptic-sing…
AlemTuzlak May 15, 2026
af87834
feat(ai-orchestration): accept streaming structured output, surface l…
AlemTuzlak May 15, 2026
efe80c1
feat(ts-react-chat): streaming structured output demos + terminal orc…
AlemTuzlak May 15, 2026
f6c67ea
ci: apply automated fixes
autofix-ci[bot] May 15, 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
2 changes: 2 additions & 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 All @@ -41,6 +42,7 @@
"rehype-raw": "^7.0.0",
"rehype-sanitize": "^6.0.0",
"remark-gfm": "^4.0.1",
"shiki": "^4.0.2",
"tailwindcss": "^4.1.18",
"vite-tsconfig-paths": "^5.1.4",
"zod": "^4.2.0"
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>
)
}
104 changes: 104 additions & 0 deletions examples/ts-react-chat/src/components/CodeBlock.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import { useEffect, useState } from 'react'
import {
getHighlighter,
inferLangFromFilename,
normalizeLang,
} from '@/lib/shiki/highlighter'

interface CodeBlockProps {
code: string
/** Explicit language override; takes precedence over `filename`. */
lang?: string
/** Used to infer language when `lang` is omitted (e.g. `src/server.ts`). */
filename?: string
/** Optional cap; longer code renders a scrollable region. */
maxHeight?: string
/** Append a blinking caret while content is still streaming. */
streaming?: boolean
className?: string
}

/**
* Async-highlighted code block. Renders raw pre/code first (so the streaming
* patch text shows up immediately) then swaps to shiki's HTML output once the
* highlighter is ready and the language is loaded. Subsequent updates to
* `code` re-highlight without re-loading the highlighter.
*
* XSS note: the inner HTML below is the return value of shiki's `codeToHtml`,
* which runs the input through a textmate grammar and emits HTML-escaped
* tokens. The only attack surface would be a bug in shiki itself; the model-
* generated `code` string is otherwise opaque (no React render of raw user
* HTML happens).
*/
export function CodeBlock(props: CodeBlockProps) {
const lang = normalizeLang(
props.lang ?? inferLangFromFilename(props.filename),
)
const [html, setHtml] = useState<string | null>(null)
const [errored, setErrored] = useState(false)

useEffect(() => {
// Object wrapper so the cleanup closure can mutate without ESLint's
// no-unnecessary-condition narrowing the bool to `false` at the check
// sites (it doesn't see the deferred cleanup mutation).
const ctl = { cancelled: false }
void (async () => {
try {
const highlighter = await getHighlighter()
if (!highlighter.getLoadedLanguages().includes(lang)) {
await highlighter.loadLanguage(
lang as Parameters<typeof highlighter.loadLanguage>[0],
)
}
if (ctl.cancelled) return
const out = highlighter.codeToHtml(props.code, {
lang,
theme: 'tanstack-ink',
})
setHtml(out)
} catch {
if (!ctl.cancelled) setErrored(true)
}
})()
return () => {
ctl.cancelled = true
}
}, [props.code, lang])

const containerStyle: React.CSSProperties = {
maxHeight: props.maxHeight,
}

if (!html || errored) {
return (
<pre
className={
'font-mono text-[12.5px] leading-relaxed text-bone whitespace-pre-wrap bg-ink-soft/60 border-l-2 border-citron px-3 py-2 overflow-auto ' +
(props.className ?? '')
}
style={containerStyle}
>
{props.code}
{props.streaming && <span className="anim-blink text-citron">▌</span>}
</pre>
)
}

return (
<div
className={
'shiki-wrap text-[12.5px] leading-relaxed border-l-2 border-citron overflow-auto ' +
(props.className ?? '')
}
style={containerStyle}
>
{/* HTML produced by shiki — see XSS note in the component docblock. */}
<div dangerouslySetInnerHTML={{ __html: html }} />
{props.streaming && (
<div className="px-3 pb-2 -mt-2 bg-ink-soft/60">
<span className="anim-blink text-citron">▌</span>
</div>
)}
</div>
)
}
134 changes: 134 additions & 0 deletions examples/ts-react-chat/src/components/DraftPreview.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
import { useEffect, useRef, useState } from 'react'

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

export function DraftPreview(props: {
draft: unknown
phase?: string
/** When true the draft is being assembled from a live structured-output stream. */
streaming?: boolean
}) {
const draft = (
props.draft && typeof props.draft === 'object' ? props.draft : null
) as Draft | null
Comment on lines +14 to +16
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">
<div className="flex items-baseline gap-3">
<span className="label-mono text-bone">Draft Preview</span>
{props.streaming && (
<span className="label-mono text-citron anim-citron-pulse">
◉ streaming
</span>
)}
</div>
<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) => {
const isLast = i === (draft.paragraphs?.length ?? 0) - 1
return (
<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}
{props.streaming && isLast && (
<span className="anim-blink text-rust ml-0.5">▌</span>
)}
</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>
)
}
Loading
Loading