create-theokit 1.2.6 → 1.2.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-theokit",
3
- "version": "1.2.6",
3
+ "version": "1.2.8",
4
4
  "type": "module",
5
5
  "description": "Scaffold a new TheoKit project",
6
6
  "license": "Apache-2.0",
@@ -9,9 +9,12 @@ import Page from './page'
9
9
  * must both type-check AND render. Renders the real React tree in jsdom.
10
10
  */
11
11
  describe('default chat page', () => {
12
- it('renders the empty state + quick actions + composer on first load', () => {
13
- render(<Page />)
14
- expect(screen.getByText('What should we build today?')).toBeDefined()
12
+ it('opens with the agent greeting + quick actions + composer on first load', () => {
13
+ const { container } = render(<Page />)
14
+ // The agent greets first — the transcript starts warm (like the terminal surface). ChatMessage renders
15
+ // the text across markdown spans, so assert the assistant message container structurally, not by text.
16
+ expect(container.querySelector('[data-theo-chat-message="assistant"]')).not.toBeNull()
17
+ // … with the quick-action suggestions and the composer.
15
18
  expect(screen.getByText('Summarize this page')).toBeDefined()
16
19
  expect(screen.getByText('Show available tools')).toBeDefined()
17
20
  expect(screen.getByText('Start a new conversation')).toBeDefined()
@@ -1,6 +1,6 @@
1
1
  'use client'
2
2
 
3
- import { useEffect, useMemo, useState } from 'react'
3
+ import { useEffect, useState } from 'react'
4
4
  import {
5
5
  ChatThread,
6
6
  ChatMessage,
@@ -12,15 +12,7 @@ import {
12
12
  type UIMessage,
13
13
  type QuickAction,
14
14
  } from '@theokit/ui'
15
- import {
16
- EmptyState,
17
- CommandPalette,
18
- Avatar,
19
- Tooltip,
20
- Button,
21
- ScrollArea,
22
- type CommandItem,
23
- } from '@usetheo/ui'
15
+ import { CommandPalette, Avatar, Tooltip, Button, ScrollArea, type CommandItem } from '@usetheo/ui'
24
16
  import { Sparkles, Wrench, RotateCcw, Command } from 'lucide-react'
25
17
  import { useAgent } from 'theokit/client'
26
18
 
@@ -32,16 +24,16 @@ import { useAgent } from 'theokit/client'
32
24
  * AgentStreaming → streaming indicator
33
25
  * AgentErrorCard → error display
34
26
  * ChatComposer → bottom input bar
35
- * EmptyState → first-load screen
27
+ * QuickActionChips → first-load suggestions
36
28
  * ContextWindowBar → context usage at top
37
29
  * CommandPalette → ⌘K quick actions
38
- * Avatar assistant/user face in messages
39
- * Tooltip → hints on icons
30
+ * Avatar / Tooltip → message faces + icon hints
40
31
  *
41
- * `useAgent` binds to the `agents/chat.ts` endpoint, consumes the ai-sdk
42
- * `UIMessageStream`, and handles AbortController cleanup + StrictMode safety.
43
- * `messages` are the reconstructed ASSISTANT `UIMessage[]`; user turns are tracked
44
- * locally and interleaved. Edit `agents/chat.ts` to pick your model / add tools.
32
+ * `useAgent` binds to the `agents/chat.ts` endpoint and consumes the ai-sdk `UIMessageStream`. It opens a
33
+ * FRESH stream per send, so `messages` hold only the CURRENT turn (and carry no stable id) we OWN the
34
+ * transcript: `history` accumulates every finished turn with our own unique ids (`u-N` / `a-N` / `greeting`),
35
+ * and the in-flight reply is shown live until it commits. This keeps the order correct, the history complete,
36
+ * and every id unique. Edit `agents/chat.ts` to pick your model / add tools.
45
37
  */
46
38
 
47
39
  const QUICK_ACTIONS: QuickAction[] = [
@@ -63,6 +55,18 @@ const CONTEXT_USED = 4_200
63
55
  const CONTEXT_TOTAL = 200_000
64
56
  const MODEL_NAME = 'gpt-4o-mini'
65
57
 
58
+ /** The agent's opening line — so the conversation starts warm instead of empty. */
59
+ const GREETING: UIMessage = {
60
+ id: 'greeting',
61
+ role: 'assistant',
62
+ parts: [
63
+ {
64
+ type: 'text',
65
+ text: "Hi — I'm your TheoKit agent. Ask me anything and I'll stream a reply. Try a quick action below or type your own.",
66
+ },
67
+ ],
68
+ }
69
+
66
70
  const ASSISTANT_AVATAR = (
67
71
  <Avatar size="sm" tone="primary">
68
72
  <Avatar.Fallback>TH</Avatar.Fallback>
@@ -76,8 +80,8 @@ const USER_AVATAR = (
76
80
 
77
81
  export default function Page() {
78
82
  const [composerValue, setComposerValue] = useState('')
79
- const [userMessages, setUserMessages] = useState<UIMessage[]>([])
80
83
  const [paletteOpen, setPaletteOpen] = useState(false)
84
+ const [history, setHistory] = useState<UIMessage[]>([GREETING])
81
85
  const { messages, send, status, reset } = useAgent<{ message: string }>('/api/agents/chat')
82
86
 
83
87
  // ⌘K / Ctrl+K opens the CommandPalette.
@@ -92,29 +96,39 @@ export default function Page() {
92
96
  return () => window.removeEventListener('keydown', onKey)
93
97
  }, [])
94
98
 
95
- // Interleave local user turns with the reconstructed assistant UIMessages,
96
- // turn by turn. `ChatMessage` renders each message's parts (text/tool/reasoning).
97
- const thread = useMemo<UIMessage[]>(() => {
98
- const out: UIMessage[] = []
99
- const turns = Math.max(userMessages.length, messages.length)
100
- for (let i = 0; i < turns; i++) {
101
- const user = userMessages[i]
102
- const assistant = messages[i]
103
- if (user) out.push(user)
104
- if (assistant) out.push(assistant)
99
+ const isStreaming = status === 'streaming'
100
+ const users = history.filter((m) => m.role === 'user').length
101
+ const replies = history.filter((m) => m.role === 'assistant' && m.id !== 'greeting').length
102
+ // A sent prompt is still awaiting its committed reply — the current turn is "in flight".
103
+ const pending = users > replies
104
+
105
+ // Merge the in-flight turn's parts into ONE assistant message with our own unique id (the SDK's ids are
106
+ // empty, which would collide in the thread). `suffix` distinguishes the live copy from the committed one.
107
+ const inflightReply = (suffix: string): UIMessage => ({
108
+ id: `a-${String(replies)}${suffix}`,
109
+ role: 'assistant',
110
+ parts: messages.flatMap((m) => m.parts),
111
+ })
112
+
113
+ // Commit the finished reply into history exactly once (the next send resets `messages`).
114
+ useEffect(() => {
115
+ if (!isStreaming && pending && messages.length > 0) {
116
+ setHistory((h) => [...h, inflightReply('')])
105
117
  }
106
- return out
107
- }, [userMessages, messages])
118
+ // eslint-disable-next-line react-hooks/exhaustive-deps
119
+ }, [isStreaming, pending, messages, replies])
120
+
121
+ // Transcript = committed history + the in-flight reply (shown until it commits — no flicker, no double).
122
+ const thread = pending && messages.length > 0 ? [...history, inflightReply('-live')] : history
123
+ const onlyGreeting = history.length === 1 && !isStreaming
108
124
 
109
125
  function handleSubmit(value: string) {
110
126
  const trimmed = value.trim()
111
127
  if (!trimmed) return
112
- const userMessage: UIMessage = {
113
- id: `u-${String(userMessages.length)}`,
114
- role: 'user',
115
- parts: [{ type: 'text', text: trimmed }],
116
- }
117
- setUserMessages((prev) => [...prev, userMessage])
128
+ setHistory((h) => [
129
+ ...h,
130
+ { id: `u-${String(h.length)}`, role: 'user', parts: [{ type: 'text', text: trimmed }] },
131
+ ])
118
132
  send({ message: trimmed })
119
133
  setComposerValue('')
120
134
  }
@@ -122,7 +136,7 @@ export default function Page() {
122
136
  function handleQuickAction(id: string) {
123
137
  setPaletteOpen(false)
124
138
  if (id === 'reset') {
125
- setUserMessages([])
139
+ setHistory([GREETING])
126
140
  reset()
127
141
  return
128
142
  }
@@ -131,8 +145,6 @@ export default function Page() {
131
145
  if (action && typeof action.label === 'string') handleSubmit(action.label)
132
146
  }
133
147
 
134
- const isStreaming = status === 'streaming'
135
- const isEmpty = thread.length === 0 && !isStreaming
136
148
  const hasError = status === 'error'
137
149
 
138
150
  return (
@@ -148,25 +160,18 @@ export default function Page() {
148
160
 
149
161
  <ScrollArea className="flex-1">
150
162
  <div className="mx-auto flex w-full max-w-3xl flex-col gap-4 px-6 py-6">
151
- {isEmpty ? (
152
- <EmptyState
153
- eyebrow="Theo Agent"
154
- icon={Sparkles}
155
- title="What should we build today?"
156
- description="Ask anything. This scaffold ships an agent at agents/chat.ts — edit it to pick your model or add tools."
157
- action={<QuickActionChips actions={QUICK_ACTIONS} onSelect={handleQuickAction} />}
158
- />
159
- ) : (
160
- <ChatThread>
161
- {thread.map((message) => (
162
- <ChatMessage
163
- key={message.id}
164
- message={message}
165
- avatar={message.role === 'assistant' ? ASSISTANT_AVATAR : USER_AVATAR}
166
- />
167
- ))}
168
- {isStreaming && <AgentStreaming model={MODEL_NAME} />}
169
- </ChatThread>
163
+ <ChatThread>
164
+ {thread.map((message) => (
165
+ <ChatMessage
166
+ key={message.id}
167
+ message={message}
168
+ avatar={message.role === 'assistant' ? ASSISTANT_AVATAR : USER_AVATAR}
169
+ />
170
+ ))}
171
+ {isStreaming && <AgentStreaming model={MODEL_NAME} />}
172
+ </ChatThread>
173
+ {onlyGreeting && (
174
+ <QuickActionChips actions={QUICK_ACTIONS} onSelect={handleQuickAction} />
170
175
  )}
171
176
  </div>
172
177
  </ScrollArea>
@@ -20,6 +20,7 @@
20
20
  "@theokit/sdk": "^2.13.0",
21
21
  "@theokit/ui": "^1.0.0",
22
22
  "@usetheo/ui": "^0.14.0",
23
+ "ai": "^7.0.0",
23
24
  "lucide-react": "^0.469.0",
24
25
  "react": "^19.0.0",
25
26
  "react-dom": "^19.0.0",