create-theokit 1.2.7 → 1.2.9
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,73 +1,30 @@
|
|
|
1
1
|
import '@theokit/ui/styles.css'
|
|
2
2
|
|
|
3
3
|
import { Outlet } from 'react-router'
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import { Bot, MessageSquare, History, Settings } from 'lucide-react'
|
|
7
|
-
|
|
8
|
-
const AGENTS: AgentProfileDescriptor[] = [
|
|
9
|
-
{
|
|
10
|
-
id: 'theo',
|
|
11
|
-
name: 'Theo',
|
|
12
|
-
description: 'Edit the agent at agents/chat.ts.',
|
|
13
|
-
tone: 'primary',
|
|
14
|
-
initials: 'TH',
|
|
15
|
-
badge: 'Online',
|
|
16
|
-
},
|
|
17
|
-
]
|
|
4
|
+
import { ThemeSwitcher } from '@theokit/ui'
|
|
5
|
+
import { Bot } from 'lucide-react'
|
|
18
6
|
|
|
7
|
+
/**
|
|
8
|
+
* App shell — a slim top bar (product name + theme toggle) over the chat surface.
|
|
9
|
+
*
|
|
10
|
+
* Deliberately minimal: everything shown WORKS. A scaffold is an honest starting point, not a demo
|
|
11
|
+
* dashboard — so there is no fake cost meter, no fake token counter, and no dead History/Settings buttons.
|
|
12
|
+
* Add real chrome (a conversation list, usage from the stream, a settings route) as you build it.
|
|
13
|
+
*/
|
|
19
14
|
export default function RootLayout() {
|
|
20
15
|
return (
|
|
21
16
|
<div className="grid h-screen w-screen grid-rows-[auto_1fr] bg-background text-foreground">
|
|
22
|
-
<
|
|
23
|
-
<
|
|
24
|
-
<
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
v0.1
|
|
30
|
-
</Badge>
|
|
31
|
-
</span>
|
|
32
|
-
</Tooltip>
|
|
33
|
-
</TopNav.Left>
|
|
34
|
-
<TopNav.Right>
|
|
35
|
-
<Tooltip label="Toggle theme" side="bottom" align="end">
|
|
36
|
-
<span>
|
|
37
|
-
<ThemeSwitcher />
|
|
38
|
-
</span>
|
|
39
|
-
</Tooltip>
|
|
40
|
-
</TopNav.Right>
|
|
41
|
-
</TopNav>
|
|
42
|
-
|
|
43
|
-
<div className="grid h-full grid-cols-[260px_1fr] overflow-hidden">
|
|
44
|
-
<Sidebar className="flex flex-col border-border/60 border-r p-3">
|
|
45
|
-
<Sidebar.Header>
|
|
46
|
-
<AgentProfile agents={AGENTS} activeId="theo" />
|
|
47
|
-
</Sidebar.Header>
|
|
48
|
-
|
|
49
|
-
<Sidebar.Section title="Workspace">
|
|
50
|
-
<Sidebar.Item icon={MessageSquare} active>
|
|
51
|
-
New conversation
|
|
52
|
-
</Sidebar.Item>
|
|
53
|
-
<Sidebar.Item icon={History}>History</Sidebar.Item>
|
|
54
|
-
<Sidebar.Item icon={Settings}>Settings</Sidebar.Item>
|
|
55
|
-
</Sidebar.Section>
|
|
56
|
-
|
|
57
|
-
<Sidebar.Footer className="mt-auto">
|
|
58
|
-
<CostMeter
|
|
59
|
-
compact
|
|
60
|
-
title="This session"
|
|
61
|
-
cost={0.0023}
|
|
62
|
-
delta={{ value: 0.0023, period: 'now' }}
|
|
63
|
-
/>
|
|
64
|
-
</Sidebar.Footer>
|
|
65
|
-
</Sidebar>
|
|
17
|
+
<header className="flex items-center justify-between border-border/60 border-b px-4 py-2">
|
|
18
|
+
<span className="inline-flex items-center gap-2">
|
|
19
|
+
<Bot className="size-5 text-primary" aria-hidden />
|
|
20
|
+
<span className="font-semibold text-sm tracking-tight">Theo Agent</span>
|
|
21
|
+
</span>
|
|
22
|
+
<ThemeSwitcher />
|
|
23
|
+
</header>
|
|
66
24
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
</div>
|
|
25
|
+
<main className="flex h-full flex-col overflow-hidden">
|
|
26
|
+
<Outlet />
|
|
27
|
+
</main>
|
|
71
28
|
</div>
|
|
72
29
|
)
|
|
73
30
|
}
|
|
@@ -9,17 +9,17 @@ 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('opens with the agent greeting +
|
|
12
|
+
it('opens with the agent greeting + honest starter prompts + composer + a working New chat', () => {
|
|
13
13
|
const { container } = render(<Page />)
|
|
14
|
-
// The agent greets first — the transcript starts warm
|
|
15
|
-
//
|
|
14
|
+
// The agent greets first — the transcript starts warm. ChatMessage renders the text across markdown
|
|
15
|
+
// spans, so assert the assistant message container structurally, not by text.
|
|
16
16
|
expect(container.querySelector('[data-theo-chat-message="assistant"]')).not.toBeNull()
|
|
17
|
-
//
|
|
18
|
-
expect(screen.getByText('
|
|
19
|
-
expect(screen.getByText('
|
|
20
|
-
expect(screen.
|
|
21
|
-
|
|
22
|
-
expect(screen.getByLabelText('
|
|
17
|
+
// Honest starter prompts (each sends a real message the agent can answer) + the composer.
|
|
18
|
+
expect(screen.getByText('What can you help me with?')).toBeDefined()
|
|
19
|
+
expect(screen.getByText('Write a haiku about TypeScript')).toBeDefined()
|
|
20
|
+
expect(screen.getByPlaceholderText('Message the agent…')).toBeDefined()
|
|
21
|
+
// `New chat` (reset) is real — no fake cost/token meters or dead History/Settings buttons here.
|
|
22
|
+
expect(screen.getByLabelText('New chat')).toBeDefined()
|
|
23
23
|
})
|
|
24
24
|
|
|
25
25
|
it('ChatMessage accepts a UIMessage and renders its message container (auto-dispatch)', () => {
|
|
@@ -8,102 +8,55 @@ import {
|
|
|
8
8
|
AgentStreaming,
|
|
9
9
|
AgentErrorCard,
|
|
10
10
|
QuickActionChips,
|
|
11
|
-
ContextWindowBar,
|
|
12
11
|
type UIMessage,
|
|
13
12
|
type QuickAction,
|
|
14
13
|
} from '@theokit/ui'
|
|
15
|
-
import {
|
|
16
|
-
import {
|
|
14
|
+
import { Button, ScrollArea } from '@usetheo/ui'
|
|
15
|
+
import { Plus, Sparkles } from 'lucide-react'
|
|
17
16
|
import { useAgent } from 'theokit/client'
|
|
18
17
|
|
|
19
18
|
/**
|
|
20
|
-
* Default scaffold —
|
|
19
|
+
* Default scaffold — a working agent chat, composed from TheoUI. Everything here FUNCTIONS: the thread
|
|
20
|
+
* streams real replies, `New chat` resets, the starter prompts send real messages, and the error card shows
|
|
21
|
+
* the real error. No fake cost/token meters.
|
|
21
22
|
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
* ChatComposer → bottom input bar
|
|
27
|
-
* QuickActionChips → first-load suggestions
|
|
28
|
-
* ContextWindowBar → context usage at top
|
|
29
|
-
* CommandPalette → ⌘K quick actions
|
|
30
|
-
* Avatar / Tooltip → message faces + icon hints
|
|
31
|
-
*
|
|
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.
|
|
23
|
+
* `useAgent` opens a FRESH stream per send — its `messages` hold only the CURRENT turn (with no stable id),
|
|
24
|
+
* so we OWN the transcript: `history` accumulates each finished turn with our own unique ids, and the
|
|
25
|
+
* in-flight reply is shown live until it commits (correct order, complete history, unique ids). Edit
|
|
26
|
+
* `agents/chat.ts` to pick your model / add tools.
|
|
37
27
|
*/
|
|
38
28
|
|
|
39
|
-
const QUICK_ACTIONS: QuickAction[] = [
|
|
40
|
-
{ id: 'summarize', label: 'Summarize this page', icon: Sparkles },
|
|
41
|
-
{ id: 'tools', label: 'Show available tools', icon: Wrench },
|
|
42
|
-
{ id: 'reset', label: 'Start a new conversation', icon: RotateCcw },
|
|
43
|
-
]
|
|
44
|
-
|
|
45
|
-
const COMMAND_ITEMS: CommandItem[] = QUICK_ACTIONS.map((a) => ({
|
|
46
|
-
id: a.id,
|
|
47
|
-
label: a.label,
|
|
48
|
-
icon: a.icon,
|
|
49
|
-
group: 'Quick actions',
|
|
50
|
-
}))
|
|
51
|
-
|
|
52
|
-
// Display-only context-window hint. The agent's real model lives in `agents/chat.ts`
|
|
53
|
-
// (`model: 'openai/gpt-4o-mini'`); wire real token counts from the stream when you need them.
|
|
54
|
-
const CONTEXT_USED = 4_200
|
|
55
|
-
const CONTEXT_TOTAL = 200_000
|
|
56
29
|
const MODEL_NAME = 'gpt-4o-mini'
|
|
57
30
|
|
|
58
31
|
/** The agent's opening line — so the conversation starts warm instead of empty. */
|
|
59
32
|
const GREETING: UIMessage = {
|
|
60
33
|
id: 'greeting',
|
|
61
34
|
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
|
-
],
|
|
35
|
+
parts: [{ type: 'text', text: "Hi — I'm your TheoKit agent. Ask me anything and I'll stream a reply." }],
|
|
68
36
|
}
|
|
69
37
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
<Avatar size="sm" tone="muted">
|
|
77
|
-
<Avatar.Fallback>YOU</Avatar.Fallback>
|
|
78
|
-
</Avatar>
|
|
79
|
-
)
|
|
38
|
+
/** Honest starter prompts — each sends a real message the scaffold agent can actually answer. */
|
|
39
|
+
const STARTERS: QuickAction[] = [
|
|
40
|
+
{ id: 'help', label: 'What can you help me with?', icon: Sparkles },
|
|
41
|
+
{ id: 'haiku', label: 'Write a haiku about TypeScript', icon: Sparkles },
|
|
42
|
+
{ id: 'async', label: 'Explain async/await in one paragraph', icon: Sparkles },
|
|
43
|
+
]
|
|
80
44
|
|
|
81
45
|
export default function Page() {
|
|
82
46
|
const [composerValue, setComposerValue] = useState('')
|
|
83
|
-
const [paletteOpen, setPaletteOpen] = useState(false)
|
|
84
47
|
const [history, setHistory] = useState<UIMessage[]>([GREETING])
|
|
85
|
-
const { messages, send, status, reset } = useAgent<{ message: string }>('/api/agents/chat')
|
|
86
|
-
|
|
87
|
-
// ⌘K / Ctrl+K opens the CommandPalette.
|
|
88
|
-
useEffect(() => {
|
|
89
|
-
function onKey(e: KeyboardEvent) {
|
|
90
|
-
if ((e.metaKey || e.ctrlKey) && e.key === 'k') {
|
|
91
|
-
e.preventDefault()
|
|
92
|
-
setPaletteOpen((v) => !v)
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
window.addEventListener('keydown', onKey)
|
|
96
|
-
return () => window.removeEventListener('keydown', onKey)
|
|
97
|
-
}, [])
|
|
48
|
+
const { messages, send, status, reset, error } = useAgent<{ message: string }>('/api/agents/chat')
|
|
98
49
|
|
|
99
50
|
const isStreaming = status === 'streaming'
|
|
51
|
+
const hasError = status === 'error'
|
|
52
|
+
|
|
100
53
|
const users = history.filter((m) => m.role === 'user').length
|
|
101
54
|
const replies = history.filter((m) => m.role === 'assistant' && m.id !== 'greeting').length
|
|
102
55
|
// A sent prompt is still awaiting its committed reply — the current turn is "in flight".
|
|
103
56
|
const pending = users > replies
|
|
104
57
|
|
|
105
58
|
// 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
|
|
59
|
+
// empty, which would collide). `suffix` distinguishes the live copy from the committed one.
|
|
107
60
|
const inflightReply = (suffix: string): UIMessage => ({
|
|
108
61
|
id: `a-${String(replies)}${suffix}`,
|
|
109
62
|
role: 'assistant',
|
|
@@ -133,45 +86,30 @@ export default function Page() {
|
|
|
133
86
|
setComposerValue('')
|
|
134
87
|
}
|
|
135
88
|
|
|
136
|
-
function
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
reset()
|
|
141
|
-
return
|
|
142
|
-
}
|
|
143
|
-
const action = QUICK_ACTIONS.find((a) => a.id === id)
|
|
144
|
-
// Quick-action labels are strings; only a string can be sent as a prompt.
|
|
145
|
-
if (action && typeof action.label === 'string') handleSubmit(action.label)
|
|
89
|
+
function newChat() {
|
|
90
|
+
setHistory([GREETING])
|
|
91
|
+
reset()
|
|
92
|
+
setComposerValue('')
|
|
146
93
|
}
|
|
147
94
|
|
|
148
|
-
const hasError = status === 'error'
|
|
149
|
-
|
|
150
95
|
return (
|
|
151
96
|
<>
|
|
152
|
-
<ContextWindowBar
|
|
153
|
-
used={CONTEXT_USED}
|
|
154
|
-
total={CONTEXT_TOTAL}
|
|
155
|
-
trailing={MODEL_NAME}
|
|
156
|
-
label="Context window"
|
|
157
|
-
compact
|
|
158
|
-
className="border-border/60 border-b px-6 py-2"
|
|
159
|
-
/>
|
|
160
|
-
|
|
161
97
|
<ScrollArea className="flex-1">
|
|
162
98
|
<div className="mx-auto flex w-full max-w-3xl flex-col gap-4 px-6 py-6">
|
|
163
99
|
<ChatThread>
|
|
164
100
|
{thread.map((message) => (
|
|
165
|
-
<ChatMessage
|
|
166
|
-
key={message.id}
|
|
167
|
-
message={message}
|
|
168
|
-
avatar={message.role === 'assistant' ? ASSISTANT_AVATAR : USER_AVATAR}
|
|
169
|
-
/>
|
|
101
|
+
<ChatMessage key={message.id} message={message} />
|
|
170
102
|
))}
|
|
171
103
|
{isStreaming && <AgentStreaming model={MODEL_NAME} />}
|
|
172
104
|
</ChatThread>
|
|
173
105
|
{onlyGreeting && (
|
|
174
|
-
<QuickActionChips
|
|
106
|
+
<QuickActionChips
|
|
107
|
+
actions={STARTERS}
|
|
108
|
+
onSelect={(id) => {
|
|
109
|
+
const action = STARTERS.find((s) => s.id === id)
|
|
110
|
+
if (action && typeof action.label === 'string') handleSubmit(action.label)
|
|
111
|
+
}}
|
|
112
|
+
/>
|
|
175
113
|
)}
|
|
176
114
|
</div>
|
|
177
115
|
</ScrollArea>
|
|
@@ -182,11 +120,11 @@ export default function Page() {
|
|
|
182
120
|
<div className="mb-3">
|
|
183
121
|
<AgentErrorCard
|
|
184
122
|
kind="network"
|
|
185
|
-
title="
|
|
186
|
-
detail=
|
|
123
|
+
title="The agent stream ended with an error"
|
|
124
|
+
detail={error?.message ?? 'Something went wrong. Start a new chat to try again.'}
|
|
187
125
|
actions={
|
|
188
|
-
<Button variant="ghost" size="sm" onClick={
|
|
189
|
-
|
|
126
|
+
<Button variant="ghost" size="sm" onClick={newChat}>
|
|
127
|
+
New chat
|
|
190
128
|
</Button>
|
|
191
129
|
}
|
|
192
130
|
/>
|
|
@@ -197,32 +135,22 @@ export default function Page() {
|
|
|
197
135
|
onValueChange={setComposerValue}
|
|
198
136
|
onSubmit={handleSubmit}
|
|
199
137
|
running={isStreaming}
|
|
200
|
-
placeholder="
|
|
138
|
+
placeholder="Message the agent…"
|
|
201
139
|
leadingActions={
|
|
202
|
-
<
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
</Tooltip>
|
|
140
|
+
<Button
|
|
141
|
+
type="button"
|
|
142
|
+
variant="ghost"
|
|
143
|
+
size="icon"
|
|
144
|
+
onClick={newChat}
|
|
145
|
+
aria-label="New chat"
|
|
146
|
+
title="New chat"
|
|
147
|
+
>
|
|
148
|
+
<Plus className="size-4" />
|
|
149
|
+
</Button>
|
|
213
150
|
}
|
|
214
151
|
/>
|
|
215
152
|
</div>
|
|
216
153
|
</div>
|
|
217
|
-
|
|
218
|
-
<CommandPalette
|
|
219
|
-
open={paletteOpen}
|
|
220
|
-
onOpenChange={setPaletteOpen}
|
|
221
|
-
items={COMMAND_ITEMS}
|
|
222
|
-
onSelect={handleQuickAction}
|
|
223
|
-
placeholder="Run a command…"
|
|
224
|
-
emptyMessage="No matching commands."
|
|
225
|
-
/>
|
|
226
154
|
</>
|
|
227
155
|
)
|
|
228
156
|
}
|
|
@@ -15,11 +15,12 @@
|
|
|
15
15
|
"typecheck": "tsc --noEmit"
|
|
16
16
|
},
|
|
17
17
|
"dependencies": {
|
|
18
|
-
"theokit": "^0.30.
|
|
18
|
+
"theokit": "^0.30.3",
|
|
19
19
|
"@theokit/agents": "^0.35.2",
|
|
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",
|