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
|
@@ -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
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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<
|
|
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
|
-
//
|
|
110
|
-
//
|
|
111
|
-
const
|
|
112
|
-
const
|
|
113
|
-
const
|
|
114
|
-
for (
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
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
|
|
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
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
-
{
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
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
|
-
|
|
262
|
-
|
|
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
|
-
|
|
76
|
-
|
|
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`, `
|
|
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.
|
|
19
|
-
"@theokit/agents": "^0.30.
|
|
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
|
}
|