create-theokit 0.7.0 → 0.9.0

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.
@@ -0,0 +1,206 @@
1
+ /* TheoKit — global design tokens + reset */
2
+
3
+ :root {
4
+ --font-sans: -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif;
5
+ --font-mono: 'SF Mono', SFMono-Regular, ui-monospace, 'DejaVu Sans Mono', Menlo, Consolas, monospace;
6
+ --bg: #ffffff;
7
+ --bg-secondary: #fafafa;
8
+ --card: #ffffff;
9
+ --border: #e5e5e5;
10
+ --text: #171717;
11
+ --text-secondary: #666666;
12
+ --text-muted: #999999;
13
+ --accent: #6366f1;
14
+ --accent-hover: #4f46e5;
15
+ --green: #22c55e;
16
+ --red: #ef4444;
17
+ --yellow: #eab308;
18
+ }
19
+
20
+ @media (prefers-color-scheme: dark) {
21
+ :root {
22
+ --bg: #0a0a0a;
23
+ --bg-secondary: #111111;
24
+ --card: #141414;
25
+ --border: #2a2a2a;
26
+ --text: #ededed;
27
+ --text-secondary: #999999;
28
+ --text-muted: #666666;
29
+ --accent: #818cf8;
30
+ --accent-hover: #6366f1;
31
+ --green: #22c55e;
32
+ --red: #ef4444;
33
+ --yellow: #eab308;
34
+ }
35
+
36
+ html { color-scheme: dark; }
37
+ }
38
+
39
+ * { margin: 0; padding: 0; box-sizing: border-box; }
40
+
41
+ html, body {
42
+ max-width: 100vw;
43
+ overflow-x: hidden;
44
+ }
45
+
46
+ body {
47
+ font-family: var(--font-sans);
48
+ background: var(--bg);
49
+ color: var(--text);
50
+ min-height: 100vh;
51
+ display: flex;
52
+ flex-direction: column;
53
+ -webkit-font-smoothing: antialiased;
54
+ -moz-osx-font-smoothing: grayscale;
55
+ }
56
+
57
+ code, pre { font-family: var(--font-mono); }
58
+
59
+ a { color: inherit; text-decoration: none; }
60
+
61
+ /* ─── Layout ────────────────────────────────────────── */
62
+
63
+ .container {
64
+ max-width: 1200px;
65
+ margin: 0 auto;
66
+ padding: 24px;
67
+ flex: 1;
68
+ width: 100%;
69
+ }
70
+
71
+ /* ─── Cards ─────────────────────────────────────────── */
72
+
73
+ .card {
74
+ background: var(--card);
75
+ border: 1px solid var(--border);
76
+ border-radius: 12px;
77
+ padding: 24px;
78
+ }
79
+
80
+ /* ─── Grid ──────────────────────────────────────────── */
81
+
82
+ .grid {
83
+ display: grid;
84
+ grid-template-columns: 1fr 1fr;
85
+ gap: 24px;
86
+ }
87
+
88
+ @media (max-width: 768px) {
89
+ .grid { grid-template-columns: 1fr; }
90
+ }
91
+
92
+ /* ─── Typography ────────────────────────────────────── */
93
+
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; }
96
+
97
+ .accent { color: var(--accent); }
98
+ .subtitle { color: var(--text-secondary); font-size: 0.9rem; margin-top: 4px; line-height: 1.5; }
99
+
100
+ /* ─── Badges ────────────────────────────────────────── */
101
+
102
+ .badge {
103
+ font-size: 0.65rem;
104
+ padding: 2px 10px;
105
+ border-radius: 99px;
106
+ font-weight: 500;
107
+ background: color-mix(in srgb, var(--accent) 12%, transparent);
108
+ color: var(--accent);
109
+ }
110
+
111
+ .badge-ai {
112
+ background: color-mix(in srgb, var(--yellow) 12%, transparent);
113
+ color: var(--yellow);
114
+ }
115
+
116
+ /* ─── Table ─────────────────────────────────────────── */
117
+
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; }
122
+
123
+ .prio { font-size: 0.7rem; padding: 2px 10px; border-radius: 99px; font-weight: 500; }
124
+ .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); }
126
+ .prio-low { background: color-mix(in srgb, var(--green) 12%, transparent); color: var(--green); }
127
+
128
+ /* ─── Forms ─────────────────────────────────────────── */
129
+
130
+ .create-bar { display: flex; gap: 8px; margin-top: 16px; }
131
+ .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;
135
+ outline: none; transition: border-color 0.2s;
136
+ }
137
+ .create-bar input:focus { border-color: var(--accent); }
138
+ .create-bar select {
139
+ padding: 10px 12px; background: var(--bg-secondary);
140
+ border: 1px solid var(--border); border-radius: 8px;
141
+ color: var(--text); font-size: 0.8rem;
142
+ }
143
+ .create-bar button, .chat-bar button {
144
+ padding: 10px 20px; background: var(--accent); color: white;
145
+ border: none; border-radius: 8px; cursor: pointer;
146
+ font-weight: 600; font-size: 0.85rem; transition: background 0.2s;
147
+ }
148
+
149
+ @media (hover: hover) and (pointer: fine) {
150
+ .create-bar button:hover, .chat-bar button:hover { background: var(--accent-hover); }
151
+ }
152
+
153
+ .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
+ }
165
+
166
+ /* ─── Chat ──────────────────────────────────────────── */
167
+
168
+ .chat-box {
169
+ height: 420px; overflow-y: auto; padding: 16px;
170
+ background: var(--bg-secondary); border: 1px solid var(--border);
171
+ border-radius: 10px; margin-bottom: 12px;
172
+ font-size: 0.85rem; line-height: 1.7;
173
+ }
174
+
175
+ .msg { margin-bottom: 10px; padding: 10px 14px; border-radius: 10px; }
176
+ .msg.user { background: color-mix(in srgb, var(--accent) 10%, transparent); color: var(--accent); }
177
+ .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; }
181
+
182
+ .chat-bar { display: flex; gap: 8px; }
183
+ .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;
187
+ outline: none; transition: border-color 0.2s;
188
+ }
189
+ .chat-bar input:focus { border-color: var(--accent); }
190
+
191
+ .cost { color: var(--text-muted); font-size: 0.75rem; margin-top: 8px; text-align: right; }
192
+
193
+ /* ─── Loading ───────────────────────────────────────── */
194
+
195
+ .loading-spinner {
196
+ width: 40px;
197
+ height: 40px;
198
+ border: 3px solid var(--border);
199
+ border-top-color: var(--accent);
200
+ border-radius: 50%;
201
+ animation: spin 0.8s linear infinite;
202
+ }
203
+
204
+ @keyframes spin {
205
+ to { transform: rotate(360deg); }
206
+ }
@@ -0,0 +1,2 @@
1
+ User-agent: *
2
+ Allow: /
@@ -9,20 +9,17 @@ import {
9
9
  Agent, MainLoop, Mixin,
10
10
  Memory, Budget, Hook,
11
11
  } from '@theokit/agents'
12
- import { UseGuards, UseInterceptors } from '@theokit/http-decorators'
12
+ import { UseGuards, UseInterceptors } from '@theokit/http'
13
13
  import { RolesGuard, Roles, Role } from '../guards/auth.guard.js'
14
14
  import { TimingInterceptor } from '../interceptors/timing.interceptor.js'
15
15
  import { TaskTools } from '../toolboxes/task.tools.js'
16
16
 
17
17
  @Agent({
18
- name: 'assistant',
19
- route: '/api/agents/assistant',
18
+ // Convention: AssistantAgent → name: 'assistant', route: /api/agents/assistant
20
19
  model: 'openai/gpt-4o-mini',
21
20
  systemPrompt: `You are a helpful task management assistant.
22
21
  Use the tasks.* tools to list, search, create, and complete tasks.
23
22
  Be concise and actionable.`,
24
- stream: true,
25
- maxIterations: 5,
26
23
  })
27
24
  @UseGuards(RolesGuard)
28
25
  @UseInterceptors(TimingInterceptor)
@@ -12,7 +12,7 @@ import {
12
12
  Body, Param, Query, HttpCode,
13
13
  UseGuards, UseInterceptors, UseFilters,
14
14
  NotFoundException,
15
- } from '@theokit/http-decorators'
15
+ } from '@theokit/http'
16
16
  import { RolesGuard, Roles, Role, IsPublic } from '../guards/auth.guard.js'
17
17
  import { TimingInterceptor } from '../interceptors/timing.interceptor.js'
18
18
  import { HttpErrorFilter } from '../filters/http-error.filter.js'
@@ -23,7 +23,7 @@ const zCreateTask = z.object({
23
23
  priority: z.enum(['low', 'medium', 'high']).default('medium'),
24
24
  })
25
25
 
26
- @Controller('api/tasks')
26
+ @Controller() // → /api/tasks (convention: inferred from TasksController)
27
27
  @UseGuards(RolesGuard)
28
28
  @UseInterceptors(TimingInterceptor)
29
29
  @UseFilters(HttpErrorFilter)
@@ -2,7 +2,7 @@
2
2
  * HttpErrorFilter — custom error response format.
3
3
  * Catches HttpException and returns structured JSON.
4
4
  */
5
- import { Catch, HttpException, type ExceptionFilter, type ArgumentsHost } from '@theokit/http-decorators'
5
+ import { Catch, HttpException, type ExceptionFilter, type ArgumentsHost } from '@theokit/http'
6
6
 
7
7
  @Catch(HttpException)
8
8
  export class HttpErrorFilter implements ExceptionFilter {
@@ -7,7 +7,7 @@
7
7
  import {
8
8
  createDecorator, Reflector,
9
9
  type CanActivate, type ExecutionContext,
10
- } from '@theokit/http-decorators'
10
+ } from '@theokit/http'
11
11
 
12
12
  export enum Role {
13
13
  User = 'user',
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Server module — auto-exports all controllers, agents, and providers.
3
+ *
4
+ * Convention: add your classes here. TheoApp.create() imports this one file.
5
+ * Like Rails' application.rb — single entry point for the backend.
6
+ *
7
+ * When you run `theokit generate controller/agent/toolbox`, the generated
8
+ * file is auto-added here.
9
+ */
10
+
11
+ // Controllers — HTTP API
12
+ export { TasksController } from './controllers/tasks.controller.js'
13
+
14
+ // Agents — AI endpoints
15
+ export { AssistantAgent } from './agents/assistant.agent.js'
16
+
17
+ // Providers — DI (toolboxes, services)
18
+ export { TaskTools } from './toolboxes/task.tools.js'
@@ -2,7 +2,7 @@
2
2
  * TimingInterceptor — logs request duration.
3
3
  * Applied to both controllers and agents.
4
4
  */
5
- import type { Interceptor } from '@theokit/http-decorators'
5
+ import type { Interceptor } from '@theokit/http'
6
6
 
7
7
  export class TimingInterceptor implements Interceptor {
8
8
  async intercept(_request: Request, next: () => Promise<unknown>): Promise<unknown> {
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * LoggerMiddleware — logs every incoming request.
3
3
  */
4
- import type { NestMiddleware } from '@theokit/http-decorators'
4
+ import type { NestMiddleware } from '@theokit/http'
5
5
 
6
6
  export class LoggerMiddleware implements NestMiddleware {
7
7
  use(request: Request, next: () => Promise<Response | null>): Promise<Response | null> {
@@ -10,7 +10,7 @@ import { z } from 'zod'
10
10
  import { Toolbox, Tool, Trace, Audit } from '@theokit/agents'
11
11
  import { taskStore } from '../store.js'
12
12
 
13
- @Toolbox({ namespace: 'tasks' })
13
+ @Toolbox() // Convention: TaskTools → namespace: 'task'
14
14
  @Trace(true)
15
15
  export class TaskTools {
16
16
  @Tool({
@@ -6,9 +6,16 @@
6
6
  "experimentalDecorators": true,
7
7
  "emitDecoratorMetadata": true,
8
8
  "strict": true,
9
+ "jsx": "react-jsx",
9
10
  "esModuleInterop": true,
10
11
  "skipLibCheck": true,
11
- "outDir": "dist"
12
+ "outDir": "dist",
13
+ "baseUrl": ".",
14
+ "paths": {
15
+ "@/*": ["./*"],
16
+ "@/server/*": ["./server/*"],
17
+ "@/app/*": ["./app/*"]
18
+ }
12
19
  },
13
20
  "include": ["**/*.ts", "**/*.tsx"],
14
21
  "exclude": ["node_modules", "dist"]
@@ -1,29 +0,0 @@
1
- /**
2
- * TheoKit App — entry point.
3
- *
4
- * That's it. No manual wiring. No plumbing.
5
- * TheoApp.create() handles everything:
6
- * - Controller routes (HTTP CRUD)
7
- * - Agent routes (SSE streaming + tool calling)
8
- * - DI (providers injected into controllers + agents)
9
- * - Shared pipeline (guards, interceptors, filters)
10
- */
11
- import 'reflect-metadata'
12
- import { readFileSync } from 'node:fs'
13
- import { TheoApp } from '@theokit/http-decorators/app'
14
- import { TasksController } from './server/controllers/tasks.controller.js'
15
- import { AssistantAgent } from './server/agents/assistant.agent.js'
16
- import { TaskTools } from './server/toolboxes/task.tools.js'
17
-
18
- // Frontend HTML (inline for alpha — upgrade to Vite plugin for React SSR)
19
- let html: string | undefined
20
- try { html = readFileSync(new URL('./public/index.html', import.meta.url), 'utf-8') } catch { /* no frontend */ }
21
-
22
- const app = await TheoApp.create({
23
- controllers: [TasksController],
24
- agents: [AssistantAgent],
25
- providers: [TaskTools],
26
- html,
27
- })
28
-
29
- await app.listen(3000)
@@ -1,70 +0,0 @@
1
- <!DOCTYPE html>
2
- <html lang="en">
3
- <head>
4
- <meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
5
- <title>TheoKit App</title>
6
- <style>
7
- :root{--bg:#0a0a0a;--card:#141414;--border:#2a2a2a;--text:#e0e0e0;--muted:#888;--accent:#6366f1;--green:#22c55e;--red:#ef4444;--yellow:#eab308}
8
- *{margin:0;padding:0;box-sizing:border-box}body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;background:var(--bg);color:var(--text);min-height:100vh}
9
- #app{max-width:1200px;margin:0 auto;padding:20px}h1{font-size:1.6rem}.accent{color:var(--accent)}.subtitle{color:var(--muted);font-size:.85rem;margin-top:2px}
10
- .role-bar{margin:12px 0}.role-bar select{padding:6px 12px;background:#1a1a1a;border:1px solid var(--border);border-radius:6px;color:var(--text);font-size:.8rem}
11
- .grid{display:grid;grid-template-columns:1fr 1fr;gap:20px}@media(max-width:768px){.grid{grid-template-columns:1fr}}
12
- .card{background:var(--card);border:1px solid var(--border);border-radius:12px;padding:20px}
13
- h2{font-size:1rem;margin-bottom:14px;display:flex;align-items:center;gap:8px}.badge{font-size:.65rem;padding:2px 8px;border-radius:99px;background:#6366f122;color:var(--accent)}.badge-ai{background:#eab30822;color:var(--yellow)}
14
- table{width:100%;border-collapse:collapse;font-size:.85rem}th{text-align:left;padding:8px 4px;color:var(--muted);border-bottom:1px solid var(--border);font-weight:500}td{padding:8px 4px;border-bottom:1px solid var(--border)}
15
- tr.done td{opacity:.5;text-decoration:line-through}.prio{font-size:.7rem;padding:2px 8px;border-radius:99px}.prio-high{background:#ef444422;color:var(--red)}.prio-med{background:#eab30822;color:var(--yellow)}.prio-low{background:#22c55e22;color:var(--green)}
16
- .create-bar{display:flex;gap:8px;margin-top:14px}.create-bar input{flex:1;padding:8px 12px;background:#1a1a1a;border:1px solid var(--border);border-radius:6px;color:var(--text);font-size:.85rem}
17
- .create-bar select{padding:8px;background:#1a1a1a;border:1px solid var(--border);border-radius:6px;color:var(--text);font-size:.8rem}.create-bar button{padding:8px 16px;background:var(--accent);color:#fff;border:none;border-radius:6px;cursor:pointer;font-weight:600}
18
- .error{color:var(--red);font-size:.8rem;margin-top:4px}
19
- .chat-box{height:400px;overflow-y:auto;padding:12px;background:#0d0d0d;border-radius:8px;margin-bottom:10px;font-size:.85rem;line-height:1.6}
20
- .msg{margin-bottom:10px;padding:8px 12px;border-radius:8px}.msg.user{background:#6366f118;color:var(--accent)}.msg.agent{background:#1a1a1a}.msg.tool{background:#eab30810;color:var(--yellow);font-size:.78rem;font-family:monospace}.msg.system{color:var(--muted);font-size:.78rem;font-style:italic}.msg.error{color:var(--red)}
21
- .chat-bar{display:flex;gap:8px}.chat-bar input{flex:1;padding:10px 14px;background:#1a1a1a;border:1px solid var(--border);border-radius:8px;color:var(--text);font-size:.9rem;outline:none}.chat-bar input:focus{border-color:var(--accent)}
22
- .chat-bar button{padding:10px 20px;background:var(--accent);color:#fff;border:none;border-radius:8px;cursor:pointer;font-weight:600}.chat-bar button:disabled{opacity:.4;cursor:not-allowed}
23
- .cost{color:var(--muted);font-size:.75rem;margin-top:6px;text-align:right}
24
- </style>
25
- </head>
26
- <body>
27
- <div id="app">
28
- <header>
29
- <h1><span class="accent">TheoKit</span> App</h1>
30
- <p class="subtitle">Controllers + AI Agent — same pipeline</p>
31
- <div class="role-bar"><label>Role: </label><select id="role"><option value="">None</option><option value="user" selected>User</option><option value="admin">Admin</option></select></div>
32
- </header>
33
- <main class="grid">
34
- <section class="card">
35
- <h2>📋 Tasks <span class="badge">@Controller</span></h2>
36
- <table><thead><tr><th>Task</th><th>Priority</th><th>Status</th></tr></thead><tbody id="task-list"></tbody></table>
37
- <form id="create-form" class="create-bar"><input id="new-title" placeholder="New task..." required minlength="3"><select id="new-priority"><option value="medium">Medium</option><option value="high">High</option><option value="low">Low</option></select><button type="submit">Add</button></form>
38
- <p id="form-error" class="error"></p>
39
- </section>
40
- <section class="card">
41
- <h2>🤖 AI Assistant <span class="badge badge-ai">@Agent + SSE</span></h2>
42
- <div id="chat" class="chat-box"><div class="msg system">Ask me to list, create, or complete tasks...</div></div>
43
- <div class="chat-bar"><input id="chat-input" placeholder="Message the AI assistant..."><button id="chat-send">Send</button></div>
44
- <p id="chat-cost" class="cost"></p>
45
- </section>
46
- </main>
47
- </div>
48
- <script>
49
- const getRole=()=>document.getElementById('role').value
50
- const headers=()=>{const h={'Content-Type':'application/json'};const r=getRole();if(r)h['x-role']=r;return h}
51
- let sessionId='s-'+Date.now()
52
-
53
- async function loadTasks(){const r=await fetch('/api/tasks');const t=await r.json();document.getElementById('task-list').innerHTML=t.map(t=>{const s=t.done?'done':'';const p=t.priority==='high'?'prio-high':t.priority==='low'?'prio-low':'prio-med';return'<tr class="'+s+'"><td>'+(t.done?'✅ ':'○ ')+t.title+'</td><td><span class="prio '+p+'">'+t.priority+'</span></td><td>'+(t.done?'Done':'To do')+'</td></tr>'}).join('')}
54
-
55
- document.getElementById('create-form').addEventListener('submit',async e=>{e.preventDefault();const t=document.getElementById('new-title').value.trim();const p=document.getElementById('new-priority').value;const err=document.getElementById('form-error');err.textContent='';if(!t)return;const r=await fetch('/api/tasks',{method:'POST',headers:headers(),body:JSON.stringify({title:t,priority:p})});if(r.status===403){err.textContent='403 — Need User role';return}if(r.status===422){const e=await r.json();err.textContent=e.error?.issues?.[0]?.message||'Validation error';return}if(!r.ok){err.textContent='Error '+r.status;return}document.getElementById('new-title').value='';loadTasks()})
56
-
57
- document.getElementById('chat-send').addEventListener('click',sendChat)
58
- document.getElementById('chat-input').addEventListener('keydown',e=>{if(e.key==='Enter')sendChat()})
59
-
60
- async function sendChat(){const input=document.getElementById('chat-input');const msg=input.value.trim();if(!msg)return;input.value='';const chat=document.getElementById('chat');chat.innerHTML+='<div class="msg user">You: '+msg.replace(/</g,'&lt;')+'</div>';document.getElementById('chat-send').disabled=true
61
- try{const r=await fetch('/api/agents/assistant/chat',{method:'POST',headers:headers(),body:JSON.stringify({message:msg,sessionId})});if(r.status===403){chat.innerHTML+='<div class="msg system">403 — Need User role</div>';document.getElementById('chat-send').disabled=false;return}
62
- const reader=r.body.getReader();const dec=new TextDecoder();let div=document.createElement('div');div.className='msg agent';chat.appendChild(div);let buf=''
63
- while(true){const{done,value}=await reader.read();if(done)break;buf+=dec.decode(value,{stream:true});const lines=buf.split('\n');buf=lines.pop()||''
64
- for(const line of lines){if(!line.startsWith('data: '))continue;try{const ev=JSON.parse(line.slice(6));if(ev.type==='text_delta')div.innerHTML+=ev.content.replace(/</g,'&lt;').replace(/\*\*(.+?)\*\*/g,'<strong>$1</strong>').replace(/\n/g,'<br>');else if(ev.type==='tool_call'){const t=document.createElement('div');t.className='msg tool';t.textContent='🔧 '+ev.toolName;chat.insertBefore(t,div)}else if(ev.type==='tool_result'){const t=document.createElement('div');t.className='msg tool';t.textContent='✅ '+(ev.output||'').substring(0,80);chat.insertBefore(t,div)}else if(ev.type==='done'){document.getElementById('chat-cost').textContent=(ev.usage?.totalTokens||0)+' tokens · '+(ev.durationMs||0)+'ms'+(ev.cost?' · $'+ev.cost.toFixed(6):'')}else if(ev.type==='error'){chat.innerHTML+='<div class="msg error">'+ev.message+'</div>'}}catch{}}
65
- chat.scrollTop=chat.scrollHeight}loadTasks()}catch(e){chat.innerHTML+='<div class="msg error">'+e.message+'</div>'}document.getElementById('chat-send').disabled=false;chat.scrollTop=chat.scrollHeight}
66
-
67
- loadTasks()
68
- </script>
69
- </body>
70
- </html>