create-theokit 1.0.16 → 1.0.17

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.0.16",
3
+ "version": "1.0.17",
4
4
  "type": "module",
5
5
  "description": "Scaffold a new TheoKit project",
6
6
  "license": "Apache-2.0",
@@ -0,0 +1,36 @@
1
+ // @vitest-environment jsdom
2
+ import { render, screen } from '@testing-library/react'
3
+ import { describe, it, expect } from 'vitest'
4
+ import { ChatThread, ChatMessage, type UIMessage } from '@theokit/ui'
5
+ import Page from './page'
6
+
7
+ /**
8
+ * Guards the chat page against `@theokit/ui` API drift (issue #80): a fresh scaffold
9
+ * must both type-check AND render. Renders the real React tree in jsdom.
10
+ */
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()
15
+ expect(screen.getByText('Summarize this page')).toBeDefined()
16
+ expect(screen.getByText('Show available tools')).toBeDefined()
17
+ expect(screen.getByText('Start a new conversation')).toBeDefined()
18
+ expect(screen.getByPlaceholderText('Ask the agent…')).toBeDefined()
19
+ expect(screen.getByLabelText('Open command palette')).toBeDefined()
20
+ })
21
+
22
+ it('ChatMessage accepts a UIMessage and renders its message container (auto-dispatch)', () => {
23
+ const assistant: UIMessage = {
24
+ id: 'a-0',
25
+ role: 'assistant',
26
+ parts: [{ type: 'text', text: 'Reading src/index.ts', state: 'done' }],
27
+ }
28
+ const { container } = render(
29
+ <ChatThread>
30
+ <ChatMessage message={assistant} />
31
+ </ChatThread>,
32
+ )
33
+ expect(container.querySelector('[data-slot="chat-message"]')).not.toBeNull()
34
+ expect(container.querySelector('[data-theo-chat-message="assistant"]')).not.toBeNull()
35
+ })
36
+ })
@@ -5,14 +5,12 @@ import {
5
5
  ChatThread,
6
6
  ChatMessage,
7
7
  ChatComposer,
8
- ToolCallCard,
9
8
  AgentStreaming,
10
9
  AgentErrorCard,
11
10
  QuickActionChips,
12
11
  ContextWindowBar,
13
- type Message,
12
+ type UIMessage,
14
13
  type QuickAction,
15
- type ToolCallStatus,
16
14
  } from '@theokit/ui'
17
15
  import {
18
16
  EmptyState,
@@ -29,35 +27,23 @@ import { useAgent } from 'theokit/client'
29
27
  /**
30
28
  * Default scaffold — an Agent Surface, composed entirely from TheoUI.
31
29
  *
32
- * ChatThread / ChatMessage → conversation
33
- * ToolCallCard → expandable tool invocations
30
+ * ChatThread / ChatMessage → conversation (ChatMessage auto-dispatches text,
31
+ * tool-call, and reasoning parts of each UIMessage)
34
32
  * AgentStreaming → streaming indicator
35
33
  * AgentErrorCard → error display
36
34
  * ChatComposer → bottom input bar
37
35
  * EmptyState → first-load screen
38
36
  * ContextWindowBar → context usage at top
39
37
  * CommandPalette → ⌘K quick actions
40
- * Avatar → assistant face in messages
38
+ * Avatar → assistant/user face in messages
41
39
  * Tooltip → hints on icons
42
40
  *
43
41
  * `useAgent` binds to the `agents/chat.ts` endpoint, consumes the ai-sdk
44
42
  * `UIMessageStream`, and handles AbortController cleanup + StrictMode safety.
45
- * Edit `agents/chat.ts` to pick your model / add tools.
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.
46
45
  */
47
46
 
48
- type ConversationItem =
49
- | { kind: 'message'; id: string; role: 'user' | 'assistant'; content: string; timestamp: string }
50
- | {
51
- kind: 'tool'
52
- id: string
53
- tool: string
54
- target?: string
55
- status: ToolCallStatus
56
- output?: string
57
- timestamp: string
58
- }
59
- | { kind: 'error'; id: string; message: string; timestamp: string }
60
-
61
47
  const QUICK_ACTIONS: QuickAction[] = [
62
48
  { id: 'summarize', label: 'Summarize this page', icon: Sparkles },
63
49
  { id: 'tools', label: 'Show available tools', icon: Wrench },
@@ -90,7 +76,7 @@ const USER_AVATAR = (
90
76
 
91
77
  export default function Page() {
92
78
  const [composerValue, setComposerValue] = useState('')
93
- const [userMessages, setUserMessages] = useState<ConversationItem[]>([])
79
+ const [userMessages, setUserMessages] = useState<UIMessage[]>([])
94
80
  const [paletteOpen, setPaletteOpen] = useState(false)
95
81
  const { messages, send, status, reset } = useAgent<{ message: string }>('/api/agents/chat')
96
82
 
@@ -106,64 +92,29 @@ export default function Page() {
106
92
  return () => window.removeEventListener('keydown', onKey)
107
93
  }, [])
108
94
 
109
- // Derive the conversation view from the reconstructed assistant UIMessages
110
- // (ai-sdk `UIMessageStream`): text parts → messages, tool parts → tool cards.
111
- const items = useMemo<ConversationItem[]>(() => {
112
- const ts = new Date().toISOString()
113
- const agentItems: ConversationItem[] = []
114
- for (const message of messages) {
115
- if (message.role !== 'assistant') continue
116
- message.parts.forEach((part, i) => {
117
- const id = `${message.id}-${i}`
118
- if (part.type === 'text') {
119
- agentItems.push({
120
- kind: 'message',
121
- id,
122
- role: 'assistant',
123
- content: part.text,
124
- timestamp: ts,
125
- })
126
- } else if (part.type === 'dynamic-tool') {
127
- agentItems.push({
128
- kind: 'tool',
129
- id,
130
- tool: part.toolName,
131
- target:
132
- part.input && typeof part.input === 'object'
133
- ? Object.entries(part.input as Record<string, unknown>)
134
- .map(([k, v]) => `${k}=${JSON.stringify(v)}`)
135
- .join(' ')
136
- : undefined,
137
- status:
138
- part.state === 'output-available'
139
- ? 'success'
140
- : part.state === 'output-error'
141
- ? 'error'
142
- : 'running',
143
- output:
144
- part.state === 'output-available'
145
- ? typeof part.output === 'string'
146
- ? part.output
147
- : JSON.stringify(part.output, null, 2)
148
- : part.state === 'output-error'
149
- ? part.errorText
150
- : undefined,
151
- timestamp: ts,
152
- })
153
- }
154
- })
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)
155
105
  }
156
- return [...userMessages, ...agentItems]
106
+ return out
157
107
  }, [userMessages, messages])
158
108
 
159
109
  function handleSubmit(value: string) {
160
110
  const trimmed = value.trim()
161
111
  if (!trimmed) return
162
- const id = `u-${userMessages.length}`
163
- setUserMessages((prev) => [
164
- ...prev,
165
- { kind: 'message', id, role: 'user', content: trimmed, timestamp: new Date().toISOString() },
166
- ])
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])
167
118
  send({ message: trimmed })
168
119
  setComposerValue('')
169
120
  }
@@ -176,11 +127,12 @@ export default function Page() {
176
127
  return
177
128
  }
178
129
  const action = QUICK_ACTIONS.find((a) => a.id === id)
179
- if (action) handleSubmit(action.label)
130
+ // Quick-action labels are strings; only a string can be sent as a prompt.
131
+ if (action && typeof action.label === 'string') handleSubmit(action.label)
180
132
  }
181
133
 
182
134
  const isStreaming = status === 'streaming'
183
- const isEmpty = items.length === 0 && !isStreaming
135
+ const isEmpty = thread.length === 0 && !isStreaming
184
136
  const hasError = status === 'error'
185
137
 
186
138
  return (
@@ -206,45 +158,13 @@ export default function Page() {
206
158
  />
207
159
  ) : (
208
160
  <ChatThread>
209
- {items.map((item) => {
210
- if (item.kind === 'message') {
211
- const message: Message = {
212
- id: item.id,
213
- role: item.role,
214
- content: item.content,
215
- timestamp: item.timestamp,
216
- model: item.role === 'assistant' ? MODEL_NAME : undefined,
217
- }
218
- return (
219
- <ChatMessage
220
- key={item.id}
221
- message={message}
222
- avatar={item.role === 'assistant' ? ASSISTANT_AVATAR : USER_AVATAR}
223
- />
224
- )
225
- }
226
- if (item.kind === 'tool') {
227
- return (
228
- <ToolCallCard
229
- key={item.id}
230
- tool={item.tool}
231
- icon={Wrench}
232
- target={item.target}
233
- status={item.status}
234
- output={item.output}
235
- timestamp={item.timestamp}
236
- />
237
- )
238
- }
239
- return (
240
- <AgentErrorCard
241
- key={item.id}
242
- kind="model"
243
- title="Agent error"
244
- description={item.message}
245
- />
246
- )
247
- })}
161
+ {thread.map((message) => (
162
+ <ChatMessage
163
+ key={message.id}
164
+ message={message}
165
+ avatar={message.role === 'assistant' ? ASSISTANT_AVATAR : USER_AVATAR}
166
+ />
167
+ ))}
248
168
  {isStreaming && <AgentStreaming model={MODEL_NAME} />}
249
169
  </ChatThread>
250
170
  )}
@@ -258,8 +178,8 @@ export default function Page() {
258
178
  <AgentErrorCard
259
179
  kind="network"
260
180
  title="Stream ended with an error"
261
- description="The connection to the agent endpoint was interrupted. Reset to try again."
262
- action={
181
+ detail="The connection to the agent endpoint was interrupted. Reset to try again."
182
+ actions={
263
183
  <Button variant="ghost" size="sm" onClick={() => reset()}>
264
184
  Reset
265
185
  </Button>
@@ -72,8 +72,8 @@ import { z } from 'zod'
72
72
  const currentTimeTool = defineAgentTool({
73
73
  name: 'current_time',
74
74
  description: 'Return the current ISO timestamp',
75
- input: z.object({}),
76
- execute: async () => ({ time: new Date().toISOString() }),
75
+ inputSchema: z.object({}),
76
+ handler: async () => new Date().toISOString(),
77
77
  })
78
78
 
79
79
  export default defineAgent({
@@ -157,7 +157,7 @@ Before writing custom tools, check if they already exist:
157
157
  | Package | What it provides | When to use |
158
158
  |---------|-----------------|-------------|
159
159
  | `@theokit/sdk` | `Agent.create()`, `defineTool()` (primitive), `Run.stream()` | Core agent runtime — always installed |
160
- | `@theokit/sdk-tools` | Ready-made tools: `createReadFileTool`, `createWriteFileTool`, `createSearchTool`, etc. | **Check here FIRST** before writing custom tools for coding agents |
160
+ | `@theokit/sdk-tools` | Ready-made tools: `createReadFileTool`, `createWriteFileTool`, `createSearchTextTool`, `createGlobTool`, `createShellTool`, etc. | **Check here FIRST** before writing custom tools for coding agents |
161
161
  | `@theokit/di-agent` | DI-powered agent with decorator injection | When using dependency injection pattern |
162
162
  | `@theokit/di` | Core DI container (`@Injectable`, `@Inject`) | When `@theokit/di-agent` needs explicit bindings |
163
163
 
@@ -15,8 +15,8 @@
15
15
  "typecheck": "tsc --noEmit"
16
16
  },
17
17
  "dependencies": {
18
- "theokit": "^0.15.1",
19
- "@theokit/agents": "^0.30.1",
18
+ "theokit": "^0.15.2",
19
+ "@theokit/agents": "^0.30.2",
20
20
  "@theokit/sdk": "^2.13.0",
21
21
  "@theokit/ui": "^1.0.0",
22
22
  "@usetheo/ui": "^0.14.0",
@@ -27,8 +27,11 @@
27
27
  "zod": "^4.0.0"
28
28
  },
29
29
  "devDependencies": {
30
+ "@testing-library/react": "^16.0.0",
31
+ "@types/node": "^22.0.0",
30
32
  "@types/react": "^19.0.0",
31
33
  "@types/react-dom": "^19.0.0",
34
+ "jsdom": "^25.0.0",
32
35
  "tailwindcss": "^4.0.0",
33
36
  "@tailwindcss/vite": "^4.0.0",
34
37
  "eslint": "^9.0.0",
@@ -9,7 +9,9 @@
9
9
  "skipLibCheck": true,
10
10
  "jsx": "react-jsx",
11
11
  "isolatedModules": true,
12
- "resolveJsonModule": true
12
+ "resolveJsonModule": true,
13
+ "experimentalDecorators": true,
14
+ "emitDecoratorMetadata": true
13
15
  },
14
16
  "include": ["app/**/*.ts", "app/**/*.tsx", "server/**/*.ts", "agents/**/*.ts"]
15
17
  }