create-theokit 1.0.3 → 1.0.5

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.3",
3
+ "version": "1.0.5",
4
4
  "type": "module",
5
5
  "description": "Scaffold a new TheoKit project",
6
6
  "license": "Apache-2.0",
@@ -1,10 +1,11 @@
1
- /* TheoKit — global design tokens + reset */
1
+ /* TheoKit — design system */
2
2
 
3
3
  :root {
4
4
  --font-sans: -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif;
5
5
  --font-mono: 'SF Mono', SFMono-Regular, ui-monospace, 'DejaVu Sans Mono', Menlo, Consolas, monospace;
6
- --bg: #ffffff;
7
- --bg-secondary: #fafafa;
6
+
7
+ --bg: #fafafa;
8
+ --bg-content: #ffffff;
8
9
  --card: #ffffff;
9
10
  --border: #e5e5e5;
10
11
  --text: #171717;
@@ -12,6 +13,10 @@
12
13
  --text-muted: #999999;
13
14
  --accent: #6366f1;
14
15
  --accent-hover: #4f46e5;
16
+ --btn-primary-bg: #171717;
17
+ --btn-primary-hover: #383838;
18
+ --btn-secondary-hover: #f2f2f2;
19
+ --btn-secondary-border: #ebebeb;
15
20
  --green: #22c55e;
16
21
  --red: #ef4444;
17
22
  --yellow: #eab308;
@@ -20,7 +25,7 @@
20
25
  @media (prefers-color-scheme: dark) {
21
26
  :root {
22
27
  --bg: #0a0a0a;
23
- --bg-secondary: #111111;
28
+ --bg-content: #0a0a0a;
24
29
  --card: #141414;
25
30
  --border: #2a2a2a;
26
31
  --text: #ededed;
@@ -28,11 +33,14 @@
28
33
  --text-muted: #666666;
29
34
  --accent: #818cf8;
30
35
  --accent-hover: #6366f1;
36
+ --btn-primary-bg: #ededed;
37
+ --btn-primary-hover: #cccccc;
38
+ --btn-secondary-hover: #1a1a1a;
39
+ --btn-secondary-border: #2a2a2a;
31
40
  --green: #22c55e;
32
41
  --red: #ef4444;
33
42
  --yellow: #eab308;
34
43
  }
35
-
36
44
  html { color-scheme: dark; }
37
45
  }
38
46
 
@@ -50,31 +58,146 @@ body {
50
58
  min-height: 100vh;
51
59
  display: flex;
52
60
  flex-direction: column;
61
+ align-items: center;
53
62
  -webkit-font-smoothing: antialiased;
54
63
  -moz-osx-font-smoothing: grayscale;
55
64
  }
56
65
 
66
+ a { color: inherit; text-decoration: none; }
57
67
  code, pre { font-family: var(--font-mono); }
58
68
 
59
- a { color: inherit; text-decoration: none; }
69
+ /* ─── Page container ────────────────────────────────── */
60
70
 
61
- /* ─── Layout ────────────────────────────────────────── */
71
+ .page {
72
+ display: flex;
73
+ flex: 1;
74
+ flex-direction: column;
75
+ align-items: center;
76
+ width: 100%;
77
+ }
62
78
 
63
- .container {
64
- max-width: 1200px;
65
- margin: 0 auto;
66
- padding: 24px;
79
+ .main {
80
+ display: flex;
67
81
  flex: 1;
82
+ flex-direction: column;
68
83
  width: 100%;
84
+ max-width: 900px;
85
+ background: var(--bg-content);
86
+ padding: 80px 48px 48px;
69
87
  }
70
88
 
71
- /* ─── Cards ─────────────────────────────────────────── */
89
+ @media (max-width: 768px) {
90
+ .main { padding: 48px 20px 32px; }
91
+ }
72
92
 
73
- .card {
93
+ /* ─── Hero ──────────────────────────────────────────── */
94
+
95
+ .hero {
96
+ display: flex;
97
+ flex-direction: column;
98
+ align-items: center;
99
+ text-align: center;
100
+ gap: 16px;
101
+ margin-bottom: 48px;
102
+ }
103
+
104
+ .hero-logo {
105
+ border-radius: 16px;
106
+ margin-bottom: 8px;
107
+ }
108
+
109
+ .hero h1 {
110
+ font-size: 40px;
111
+ font-weight: 700;
112
+ letter-spacing: -2.4px;
113
+ line-height: 48px;
114
+ text-wrap: balance;
115
+ }
116
+
117
+ .hero .tagline {
118
+ font-size: 18px;
119
+ line-height: 28px;
120
+ color: var(--text-secondary);
121
+ max-width: 440px;
122
+ text-wrap: balance;
123
+ }
124
+
125
+ .hero .hint {
126
+ font-size: 14px;
127
+ color: var(--text-muted);
128
+ margin-top: 8px;
129
+ }
130
+
131
+ .hero .hint code {
74
132
  background: var(--card);
75
133
  border: 1px solid var(--border);
76
- border-radius: 12px;
77
- padding: 24px;
134
+ padding: 2px 8px;
135
+ border-radius: 6px;
136
+ font-size: 13px;
137
+ }
138
+
139
+ @media (max-width: 768px) {
140
+ .hero h1 { font-size: 32px; line-height: 40px; letter-spacing: -1.92px; }
141
+ .hero .tagline { font-size: 16px; }
142
+ }
143
+
144
+ /* ─── CTA Buttons ───────────────────────────────────── */
145
+
146
+ .ctas {
147
+ display: flex;
148
+ gap: 12px;
149
+ margin-top: 8px;
150
+ }
151
+
152
+ .btn {
153
+ display: inline-flex;
154
+ justify-content: center;
155
+ align-items: center;
156
+ height: 40px;
157
+ padding: 0 20px;
158
+ border-radius: 128px;
159
+ border: 1px solid transparent;
160
+ font-size: 14px;
161
+ font-weight: 500;
162
+ cursor: pointer;
163
+ transition: background 0.2s, border-color 0.2s;
164
+ text-decoration: none;
165
+ }
166
+
167
+ .btn.primary {
168
+ background: var(--btn-primary-bg);
169
+ color: var(--bg);
170
+ }
171
+
172
+ .btn.secondary {
173
+ border-color: var(--btn-secondary-border);
174
+ color: var(--text);
175
+ }
176
+
177
+ @media (hover: hover) and (pointer: fine) {
178
+ .btn.primary:hover { background: var(--btn-primary-hover); }
179
+ .btn.secondary:hover { background: var(--btn-secondary-hover); border-color: transparent; }
180
+ }
181
+
182
+ /* ─── Role selector ─────────────────────────────────── */
183
+
184
+ .role-bar {
185
+ display: flex;
186
+ justify-content: center;
187
+ align-items: center;
188
+ gap: 8px;
189
+ margin-bottom: 32px;
190
+ font-size: 14px;
191
+ color: var(--text-muted);
192
+ }
193
+
194
+ .role-bar select {
195
+ padding: 6px 12px;
196
+ background: var(--card);
197
+ border: 1px solid var(--border);
198
+ border-radius: 6px;
199
+ color: var(--text);
200
+ font-size: 13px;
78
201
  }
79
202
 
80
203
  /* ─── Grid ──────────────────────────────────────────── */
@@ -83,24 +206,35 @@ a { color: inherit; text-decoration: none; }
83
206
  display: grid;
84
207
  grid-template-columns: 1fr 1fr;
85
208
  gap: 24px;
209
+ width: 100%;
86
210
  }
87
211
 
88
212
  @media (max-width: 768px) {
89
213
  .grid { grid-template-columns: 1fr; }
90
214
  }
91
215
 
92
- /* ─── Typography ────────────────────────────────────── */
216
+ /* ─── Cards ─────────────────────────────────────────── */
93
217
 
94
- h1 { font-size: 1.75rem; font-weight: 700; letter-spacing: -0.02em; line-height: 1.2; }
95
- h2 { font-size: 1rem; font-weight: 600; margin-bottom: 16px; display: flex; align-items: center; gap: 8px; }
218
+ .card {
219
+ background: var(--card);
220
+ border: 1px solid var(--border);
221
+ border-radius: 12px;
222
+ padding: 24px;
223
+ }
96
224
 
97
- .accent { color: var(--accent); }
98
- .subtitle { color: var(--text-secondary); font-size: 0.9rem; margin-top: 4px; line-height: 1.5; }
225
+ .card h2 {
226
+ font-size: 15px;
227
+ font-weight: 600;
228
+ margin-bottom: 16px;
229
+ display: flex;
230
+ align-items: center;
231
+ gap: 8px;
232
+ }
99
233
 
100
234
  /* ─── Badges ────────────────────────────────────────── */
101
235
 
102
236
  .badge {
103
- font-size: 0.65rem;
237
+ font-size: 11px;
104
238
  padding: 2px 10px;
105
239
  border-radius: 99px;
106
240
  font-weight: 500;
@@ -115,87 +249,88 @@ h2 { font-size: 1rem; font-weight: 600; margin-bottom: 16px; display: flex; alig
115
249
 
116
250
  /* ─── Table ─────────────────────────────────────────── */
117
251
 
118
- table { width: 100%; border-collapse: collapse; font-size: 0.85rem; }
119
- th { text-align: left; padding: 10px 8px; color: var(--text-muted); border-bottom: 1px solid var(--border); font-weight: 500; font-size: 0.75rem; text-transform: uppercase; letter-spacing: 0.05em; }
120
- td { padding: 10px 8px; border-bottom: 1px solid var(--border); }
121
- tr.done td { opacity: 0.45; text-decoration: line-through; }
252
+ table { width: 100%; border-collapse: collapse; font-size: 13px; }
253
+ th {
254
+ text-align: left; padding: 8px 6px;
255
+ color: var(--text-muted); border-bottom: 1px solid var(--border);
256
+ font-weight: 500; font-size: 11px; text-transform: uppercase; letter-spacing: 0.05em;
257
+ }
258
+ td { padding: 8px 6px; border-bottom: 1px solid var(--border); }
259
+ tr.done td { opacity: 0.4; text-decoration: line-through; }
122
260
 
123
- .prio { font-size: 0.7rem; padding: 2px 10px; border-radius: 99px; font-weight: 500; }
261
+ .prio { font-size: 11px; padding: 2px 8px; border-radius: 99px; font-weight: 500; }
124
262
  .prio-high { background: color-mix(in srgb, var(--red) 12%, transparent); color: var(--red); }
125
- .prio-med { background: color-mix(in srgb, var(--yellow) 12%, transparent); color: var(--yellow); }
263
+ .prio-medium { background: color-mix(in srgb, var(--yellow) 12%, transparent); color: var(--yellow); }
126
264
  .prio-low { background: color-mix(in srgb, var(--green) 12%, transparent); color: var(--green); }
127
265
 
128
266
  /* ─── Forms ─────────────────────────────────────────── */
129
267
 
130
268
  .create-bar { display: flex; gap: 8px; margin-top: 16px; }
131
269
  .create-bar input {
132
- flex: 1; padding: 10px 14px;
133
- background: var(--bg-secondary); border: 1px solid var(--border);
134
- border-radius: 8px; color: var(--text); font-size: 0.85rem;
270
+ flex: 1; padding: 8px 12px;
271
+ background: var(--bg); border: 1px solid var(--border);
272
+ border-radius: 8px; color: var(--text); font-size: 13px;
135
273
  outline: none; transition: border-color 0.2s;
136
274
  }
137
275
  .create-bar input:focus { border-color: var(--accent); }
138
276
  .create-bar select {
139
- padding: 10px 12px; background: var(--bg-secondary);
277
+ padding: 8px 10px; background: var(--bg);
140
278
  border: 1px solid var(--border); border-radius: 8px;
141
- color: var(--text); font-size: 0.8rem;
279
+ color: var(--text); font-size: 13px;
142
280
  }
143
281
  .create-bar button, .chat-bar button {
144
- padding: 10px 20px; background: var(--accent); color: white;
282
+ padding: 8px 16px; background: var(--accent); color: white;
145
283
  border: none; border-radius: 8px; cursor: pointer;
146
- font-weight: 600; font-size: 0.85rem; transition: background 0.2s;
284
+ font-weight: 600; font-size: 13px; transition: background 0.2s;
147
285
  }
148
-
149
286
  @media (hover: hover) and (pointer: fine) {
150
287
  .create-bar button:hover, .chat-bar button:hover { background: var(--accent-hover); }
151
288
  }
152
-
153
289
  .create-bar button:disabled, .chat-bar button:disabled { opacity: 0.4; cursor: not-allowed; }
154
- .error { color: var(--red); font-size: 0.8rem; margin-top: 4px; }
155
-
156
- /* ─── Role selector ─────────────────────────────────── */
157
-
158
- .role-bar { margin-top: 14px; display: flex; align-items: center; gap: 8px; }
159
- .role-bar label { font-size: 0.8rem; color: var(--text-muted); }
160
- .role-bar select {
161
- padding: 6px 12px; background: var(--bg-secondary);
162
- border: 1px solid var(--border); border-radius: 6px;
163
- color: var(--text); font-size: 0.8rem;
164
- }
290
+ .error { color: var(--red); font-size: 13px; margin-top: 6px; }
165
291
 
166
292
  /* ─── Chat ──────────────────────────────────────────── */
167
293
 
168
294
  .chat-box {
169
- height: 420px; overflow-y: auto; padding: 16px;
170
- background: var(--bg-secondary); border: 1px solid var(--border);
295
+ height: 320px; overflow-y: auto; padding: 14px;
296
+ background: var(--bg); border: 1px solid var(--border);
171
297
  border-radius: 10px; margin-bottom: 12px;
172
- font-size: 0.85rem; line-height: 1.7;
298
+ font-size: 13px; line-height: 1.6;
173
299
  }
174
300
 
175
- .msg { margin-bottom: 10px; padding: 10px 14px; border-radius: 10px; }
301
+ .msg { margin-bottom: 8px; padding: 8px 12px; border-radius: 8px; }
176
302
  .msg.user { background: color-mix(in srgb, var(--accent) 10%, transparent); color: var(--accent); }
177
303
  .msg.agent { background: var(--card); border: 1px solid var(--border); }
178
- .msg.tool { background: color-mix(in srgb, var(--yellow) 8%, transparent); color: var(--yellow); font-size: 0.78rem; font-family: var(--font-mono); }
179
- .msg.system { color: var(--text-muted); font-size: 0.78rem; font-style: italic; }
180
- .msg.error { color: var(--red); font-size: 0.8rem; }
304
+ .msg.tool { background: color-mix(in srgb, var(--yellow) 8%, transparent); color: var(--yellow); font-size: 12px; font-family: var(--font-mono); }
305
+ .msg.system { color: var(--text-muted); font-size: 12px; font-style: italic; }
306
+ .msg.error { color: var(--red); font-size: 13px; }
181
307
 
182
308
  .chat-bar { display: flex; gap: 8px; }
183
309
  .chat-bar input {
184
- flex: 1; padding: 12px 16px;
185
- background: var(--bg-secondary); border: 1px solid var(--border);
186
- border-radius: 10px; color: var(--text); font-size: 0.9rem;
310
+ flex: 1; padding: 10px 14px;
311
+ background: var(--bg); border: 1px solid var(--border);
312
+ border-radius: 10px; color: var(--text); font-size: 14px;
187
313
  outline: none; transition: border-color 0.2s;
188
314
  }
189
315
  .chat-bar input:focus { border-color: var(--accent); }
190
316
 
191
- .cost { color: var(--text-muted); font-size: 0.75rem; margin-top: 8px; text-align: right; }
317
+ /* ─── Footer ────────────────────────────────────────── */
318
+
319
+ .footer {
320
+ text-align: center;
321
+ padding: 40px 24px 32px;
322
+ font-size: 13px;
323
+ color: var(--text-muted);
324
+ }
325
+
326
+ .footer a { color: var(--text-secondary); transition: color 0.2s; }
327
+ .footer a:hover { color: var(--accent); }
192
328
 
193
329
  /* ─── Loading ───────────────────────────────────────── */
194
330
 
195
331
  .loading-spinner {
196
- width: 40px;
197
- height: 40px;
198
- border: 3px solid var(--border);
332
+ width: 24px; height: 24px;
333
+ border: 2px solid var(--border);
199
334
  border-top-color: var(--accent);
200
335
  border-radius: 50%;
201
336
  animation: spin 0.8s linear infinite;
@@ -2,307 +2,168 @@
2
2
 
3
3
  import { useState, useEffect, useCallback, useRef, type FormEvent } from 'react'
4
4
 
5
- interface Task {
6
- id: string
7
- title: string
8
- priority: 'high' | 'medium' | 'low'
9
- done: boolean
10
- }
11
-
5
+ interface Task { id: number; title: string; priority: 'high' | 'medium' | 'low'; done: boolean }
12
6
  type Role = '' | 'user' | 'admin'
13
-
14
- interface ChatMessage {
15
- type: 'user' | 'agent' | 'tool' | 'system' | 'error'
16
- content: string
17
- }
18
-
19
- function escapeHtml(str: string): string {
20
- return str.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
21
- }
7
+ interface ChatMsg { role: 'user' | 'agent' | 'tool' | 'system' | 'error'; text: string }
22
8
 
23
9
  export default function Page() {
24
10
  const [tasks, setTasks] = useState<Task[]>([])
25
11
  const [role, setRole] = useState<Role>('user')
26
- const [newTitle, setNewTitle] = useState('')
27
- const [newPriority, setNewPriority] = useState<'high' | 'medium' | 'low'>('medium')
12
+ const [title, setTitle] = useState('')
13
+ const [priority, setPriority] = useState<Task['priority']>('medium')
28
14
  const [formError, setFormError] = useState('')
29
-
30
- const [messages, setMessages] = useState<ChatMessage[]>([
31
- { type: 'system', content: 'Ask me to list, create, or complete tasks...' },
32
- ])
15
+ const [chat, setChat] = useState<ChatMsg[]>([{ role: 'system', text: 'Ask me to list, create, or complete tasks...' }])
33
16
  const [chatInput, setChatInput] = useState('')
34
- const [chatSending, setChatSending] = useState(false)
35
- const [chatCost, setChatCost] = useState('')
36
- const chatBoxRef = useRef<HTMLDivElement>(null)
37
- const sessionId = useRef(`session-${Date.now()}`)
17
+ const [chatBusy, setChatBusy] = useState(false)
18
+ const chatRef = useRef<HTMLDivElement>(null)
38
19
 
39
- const headers = useCallback(() => {
20
+ const hdrs = useCallback((): Record<string, string> => {
40
21
  const h: Record<string, string> = { 'Content-Type': 'application/json' }
41
22
  if (role) h['x-role'] = role
42
23
  return h
43
24
  }, [role])
44
25
 
45
26
  const loadTasks = useCallback(async () => {
46
- try {
47
- const res = await fetch('/api/tasks')
48
- if (res.ok) {
49
- const data = await res.json()
50
- setTasks(data)
51
- }
52
- } catch {
53
- // Network error — silently ignore on initial load
54
- }
27
+ const res = await fetch('/api/tasks')
28
+ if (res.ok) setTasks(await res.json())
55
29
  }, [])
56
30
 
57
- useEffect(() => {
58
- loadTasks()
59
- }, [loadTasks])
60
-
61
- useEffect(() => {
62
- if (chatBoxRef.current) {
63
- chatBoxRef.current.scrollTop = chatBoxRef.current.scrollHeight
64
- }
65
- }, [messages])
31
+ useEffect(() => { loadTasks() }, [loadTasks])
66
32
 
67
- const handleCreateTask = async (e: FormEvent) => {
33
+ const createTask = async (e: FormEvent) => {
68
34
  e.preventDefault()
69
35
  setFormError('')
70
- const title = newTitle.trim()
71
- if (!title) return
72
-
73
- const res = await fetch('/api/tasks', {
74
- method: 'POST',
75
- headers: headers(),
76
- body: JSON.stringify({ title, priority: newPriority }),
77
- })
78
-
79
- if (res.status === 403) {
80
- setFormError('403 — Need User role')
81
- return
82
- }
83
-
84
- if (!res.ok) {
85
- const data = await res.json()
86
- setFormError(data.error?.issues?.[0]?.message ?? `Error ${res.status}`)
87
- return
88
- }
89
-
90
- setNewTitle('')
36
+ if (!title.trim()) return
37
+ const res = await fetch('/api/tasks', { method: 'POST', headers: hdrs(), body: JSON.stringify({ title, priority }) })
38
+ if (res.status === 403) { setFormError('403 — Need User role'); return }
39
+ if (!res.ok) { const b = await res.json(); setFormError(b.error?.issues?.[0]?.message ?? `Error ${res.status}`); return }
40
+ setTitle('')
91
41
  loadTasks()
92
42
  }
93
43
 
94
- const handleSendChat = async () => {
44
+ const sendChat = async () => {
95
45
  const msg = chatInput.trim()
96
- if (!msg || chatSending) return
46
+ if (!msg || chatBusy) return
97
47
  setChatInput('')
98
- setChatSending(true)
99
-
100
- setMessages((prev) => [...prev, { type: 'user', content: `You: ${msg}` }])
101
-
48
+ setChat(c => [...c, { role: 'user', text: msg }])
49
+ setChatBusy(true)
102
50
  try {
103
51
  const res = await fetch('/api/agents/assistant/chat', {
104
- method: 'POST',
105
- headers: headers(),
106
- body: JSON.stringify({ message: msg, sessionId: sessionId.current }),
52
+ method: 'POST', headers: hdrs(),
53
+ body: JSON.stringify({ message: msg, sessionId: 'session-' + Date.now() }),
107
54
  })
108
-
109
- if (res.status === 403) {
110
- setMessages((prev) => [
111
- ...prev,
112
- { type: 'system', content: '403 — Need User role to chat' },
113
- ])
114
- setChatSending(false)
115
- return
116
- }
117
-
55
+ if (res.status === 403) { setChat(c => [...c, { role: 'error', text: '403 — Need User role' }]); return }
118
56
  const reader = res.body?.getReader()
119
- if (!reader) {
120
- setChatSending(false)
121
- return
122
- }
123
-
57
+ if (!reader) return
124
58
  const decoder = new TextDecoder()
125
- let buf = ''
126
- let agentText = ''
127
-
128
- setMessages((prev) => [...prev, { type: 'agent', content: '' }])
129
-
59
+ let buf = '', agentText = ''
130
60
  while (true) {
131
61
  const { done, value } = await reader.read()
132
62
  if (done) break
133
-
134
63
  buf += decoder.decode(value, { stream: true })
135
- const lines = buf.split('\n')
136
- buf = lines.pop() ?? ''
137
-
64
+ const lines = buf.split('\n'); buf = lines.pop() ?? ''
138
65
  for (const line of lines) {
139
66
  if (!line.startsWith('data: ')) continue
140
67
  try {
141
68
  const ev = JSON.parse(line.slice(6))
142
- if (ev.type === 'text_delta') {
143
- agentText += ev.content
144
- setMessages((prev) => {
145
- const updated = [...prev]
146
- updated[updated.length - 1] = { type: 'agent', content: agentText }
147
- return updated
148
- })
149
- } else if (ev.type === 'tool_call') {
150
- setMessages((prev) => {
151
- const inserted = [...prev]
152
- inserted.splice(inserted.length - 1, 0, {
153
- type: 'tool',
154
- content: `\uD83D\uDD27 ${ev.toolName}`,
155
- })
156
- return inserted
157
- })
158
- } else if (ev.type === 'tool_result') {
159
- setMessages((prev) => {
160
- const inserted = [...prev]
161
- inserted.splice(inserted.length - 1, 0, {
162
- type: 'tool',
163
- content: `\u2705 ${(ev.output ?? '').substring(0, 80)}`,
164
- })
165
- return inserted
166
- })
167
- } else if (ev.type === 'thinking') {
168
- setMessages((prev) => {
169
- const inserted = [...prev]
170
- inserted.splice(inserted.length - 1, 0, {
171
- type: 'system',
172
- content: `\uD83D\uDCAD ${ev.content}`,
173
- })
174
- return inserted
175
- })
176
- } else if (ev.type === 'done') {
177
- const tokens = ev.usage?.totalTokens ?? 0
178
- const costStr = ev.cost ? ` \u00B7 $${ev.cost.toFixed(6)}` : ''
179
- setChatCost(`${tokens} tokens \u00B7 ${ev.durationMs}ms${costStr}`)
180
- } else if (ev.type === 'error') {
181
- setMessages((prev) => [...prev, { type: 'error', content: ev.message }])
182
- }
183
- } catch {
184
- /* partial JSON — skip */
185
- }
69
+ if (ev.type === 'text_delta') agentText += ev.content
70
+ else if (ev.type === 'tool_call') setChat(c => [...c, { role: 'tool', text: `🔧 ${ev.toolName}` }])
71
+ else if (ev.type === 'tool_result') setChat(c => [...c, { role: 'tool', text: `✅ ${(ev.output ?? '').slice(0, 80)}` }])
72
+ else if (ev.type === 'error') setChat(c => [...c, { role: 'error', text: ev.message }])
73
+ } catch { /* partial */ }
186
74
  }
187
75
  }
188
-
76
+ if (agentText) setChat(c => [...c, { role: 'agent', text: agentText }])
189
77
  loadTasks()
190
78
  } catch (err) {
191
- const message = err instanceof Error ? err.message : String(err)
192
- setMessages((prev) => [...prev, { type: 'error', content: `Error: ${message}` }])
193
- }
194
-
195
- setChatSending(false)
79
+ setChat(c => [...c, { role: 'error', text: `Error: ${err instanceof Error ? err.message : String(err)}` }])
80
+ } finally { setChatBusy(false) }
196
81
  }
197
82
 
83
+ useEffect(() => { chatRef.current?.scrollTo(0, chatRef.current.scrollHeight) }, [chat])
84
+
198
85
  return (
199
- <div className="container">
200
- <header>
201
- <h1>
202
- <span className="accent">TheoKit</span> Task Manager
203
- </h1>
204
- <p className="subtitle">
205
- HTTP Controllers + AI Agent same guards, distinct pipelines. Edit <code>server/</code>{' '}
206
- to get started.
207
- </p>
86
+ <div className="page">
87
+ <div className="main">
88
+ {/* Hero */}
89
+ <header className="hero">
90
+ <img src="/logo.png" alt="TheoKit" width={72} height={72} className="hero-logo" />
91
+ <h1>TheoKit</h1>
92
+ <p className="tagline">Build the app your agent lives in.</p>
93
+ <nav className="ctas">
94
+ <a href="https://usetheo.dev" target="_blank" rel="noopener noreferrer" className="btn primary">
95
+ Get Started
96
+ </a>
97
+ <a href="https://github.com/usetheodev/theokit" target="_blank" rel="noopener noreferrer" className="btn secondary">
98
+ Documentation
99
+ </a>
100
+ </nav>
101
+ <p className="hint">
102
+ Edit <code>app/page.tsx</code> to get started.
103
+ </p>
104
+ </header>
105
+
106
+ {/* Role */}
208
107
  <div className="role-bar">
209
108
  <label htmlFor="role">Role:</label>
210
- <select id="role" value={role} onChange={(e) => setRole(e.target.value as Role)}>
211
- <option value="">None (public only)</option>
109
+ <select id="role" value={role} onChange={e => setRole(e.target.value as Role)}>
110
+ <option value="">None (public)</option>
212
111
  <option value="user">User</option>
213
112
  <option value="admin">Admin</option>
214
113
  </select>
215
114
  </div>
216
- </header>
217
115
 
218
- <main className="grid">
219
- {/* Left: CRUD Panel */}
220
- <section className="card">
221
- <h2>
222
- Tasks <span className="badge">@Controller</span>
223
- </h2>
224
- <table>
225
- <thead>
226
- <tr>
227
- <th>Task</th>
228
- <th>Priority</th>
229
- <th>Status</th>
230
- </tr>
231
- </thead>
232
- <tbody>
233
- {tasks.map((task) => (
234
- <tr key={task.id} className={task.done ? 'done' : ''}>
235
- <td>
236
- {task.done ? '\u2705 ' : '\u25CB '}
237
- {escapeHtml(task.title)}
238
- </td>
239
- <td>
240
- <span
241
- className={`prio ${
242
- task.priority === 'high'
243
- ? 'prio-high'
244
- : task.priority === 'low'
245
- ? 'prio-low'
246
- : 'prio-med'
247
- }`}
248
- >
249
- {task.priority}
250
- </span>
251
- </td>
252
- <td>{task.done ? 'Done' : 'To do'}</td>
253
- </tr>
116
+ {/* Content */}
117
+ <div className="grid">
118
+ <section className="card">
119
+ <h2>Tasks <span className="badge">@Controller</span></h2>
120
+ <table>
121
+ <thead><tr><th>Task</th><th>Priority</th><th>Status</th></tr></thead>
122
+ <tbody>
123
+ {tasks.map(t => (
124
+ <tr key={t.id} className={t.done ? 'done' : ''}>
125
+ <td>{t.done ? '✅ ' : '○ '}{t.title}</td>
126
+ <td><span className={`prio prio-${t.priority}`}>{t.priority}</span></td>
127
+ <td>{t.done ? 'Done' : 'To do'}</td>
128
+ </tr>
129
+ ))}
130
+ </tbody>
131
+ </table>
132
+ <form onSubmit={createTask} className="create-bar">
133
+ <input value={title} onChange={e => setTitle(e.target.value)} placeholder="New task..." required minLength={3} />
134
+ <select value={priority} onChange={e => setPriority(e.target.value as Task['priority'])}>
135
+ <option value="medium">Medium</option>
136
+ <option value="high">High</option>
137
+ <option value="low">Low</option>
138
+ </select>
139
+ <button type="submit">Add</button>
140
+ </form>
141
+ {formError && <p className="error">{formError}</p>}
142
+ </section>
143
+
144
+ <section className="card">
145
+ <h2>AI Assistant <span className="badge badge-ai">@Agent + SSE</span></h2>
146
+ <div ref={chatRef} className="chat-box">
147
+ {chat.map((m, i) => (
148
+ <div key={i} className={`msg ${m.role}`}>{m.role === 'user' ? `You: ${m.text}` : m.text}</div>
254
149
  ))}
255
- </tbody>
256
- </table>
257
- <form onSubmit={handleCreateTask} className="create-bar">
258
- <input
259
- placeholder="New task..."
260
- required
261
- minLength={3}
262
- value={newTitle}
263
- onChange={(e) => setNewTitle(e.target.value)}
264
- />
265
- <select
266
- value={newPriority}
267
- onChange={(e) => setNewPriority(e.target.value as 'high' | 'medium' | 'low')}
268
- >
269
- <option value="medium">Medium</option>
270
- <option value="high">High</option>
271
- <option value="low">Low</option>
272
- </select>
273
- <button type="submit">Add</button>
274
- </form>
275
- {formError && <p className="error">{formError}</p>}
276
- </section>
150
+ </div>
151
+ <div className="chat-bar">
152
+ <input value={chatInput} onChange={e => setChatInput(e.target.value)} onKeyDown={e => e.key === 'Enter' && sendChat()} placeholder="Message the AI assistant..." disabled={chatBusy} />
153
+ <button type="button" onClick={sendChat} disabled={chatBusy}>Send</button>
154
+ </div>
155
+ </section>
156
+ </div>
277
157
 
278
- {/* Right: AI Chat */}
279
- <section className="card">
280
- <h2>
281
- AI Assistant <span className="badge badge-ai">@Agent + SSE</span>
282
- </h2>
283
- <div ref={chatBoxRef} className="chat-box">
284
- {messages.map((msg, i) => (
285
- <div key={i} className={`msg ${msg.type}`}>
286
- {msg.content}
287
- </div>
288
- ))}
289
- </div>
290
- <div className="chat-bar">
291
- <input
292
- placeholder="Message the AI assistant..."
293
- value={chatInput}
294
- onChange={(e) => setChatInput(e.target.value)}
295
- onKeyDown={(e) => {
296
- if (e.key === 'Enter') handleSendChat()
297
- }}
298
- />
299
- <button type="button" onClick={handleSendChat} disabled={chatSending}>
300
- Send
301
- </button>
302
- </div>
303
- {chatCost && <p className="cost">{chatCost}</p>}
304
- </section>
305
- </main>
158
+ {/* Footer */}
159
+ <footer className="footer">
160
+ Powered by <a href="https://usetheo.dev" target="_blank" rel="noopener noreferrer">TheoKit</a>
161
+ {' · '}
162
+ <a href="https://github.com/usetheodev/theokit" target="_blank" rel="noopener noreferrer">GitHub</a>
163
+ {' · '}
164
+ <a href="https://discord.usetheo.dev" target="_blank" rel="noopener noreferrer">Discord</a>
165
+ </footer>
166
+ </div>
306
167
  </div>
307
168
  )
308
169
  }
@@ -14,7 +14,7 @@
14
14
  "typecheck": "tsc --noEmit"
15
15
  },
16
16
  "dependencies": {
17
- "theokit": "^0.5.1",
17
+ "theokit": "^0.5.3",
18
18
  "react": "^19.0.0",
19
19
  "react-dom": "^19.0.0",
20
20
  "react-router": "^7.0.0",