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,96 @@
1
+ # AGENTS.md — TheoKit App
2
+
3
+ Guide for coding agents (Claude, Copilot, Cursor) working on this TheoKit project.
4
+
5
+ ## Architecture
6
+
7
+ This is a **full-stack TypeScript app** built with TheoKit — a framework for AI agent apps.
8
+
9
+ ```
10
+ app.ts → Entry point: TheoApp.create({ controllers, agents, providers })
11
+ server/
12
+ controllers/ → HTTP endpoints (@Controller + @Get/@Post/@Delete)
13
+ agents/ → AI agents (@Agent + @MainLoop + @Tool)
14
+ toolboxes/ → Agent tools (@Toolbox + @Tool with Zod schemas)
15
+ guards/ → Auth/RBAC (@UseGuards + canActivate)
16
+ interceptors/ → Request/response transforms (@UseInterceptors)
17
+ filters/ → Error formatting (@UseFilters + @Catch)
18
+ middleware/ → Request pipeline (NestMiddleware)
19
+ store.ts → In-memory data store
20
+ app/
21
+ page.tsx → React frontend
22
+ layout.tsx → Root layout
23
+ public/
24
+ index.html → Static HTML frontend with chat UI
25
+ ```
26
+
27
+ ## Key Patterns
28
+
29
+ ### Controllers (HTTP API)
30
+ ```typescript
31
+ import { Controller, Get, Post, Body, Param } from '@theokit/http'
32
+ import { z } from 'zod'
33
+
34
+ const zCreate = z.object({ title: z.string().min(3) })
35
+
36
+ @Controller('api/tasks')
37
+ class TasksController {
38
+ @Get()
39
+ list() { return tasks }
40
+
41
+ @Post()
42
+ create(@Body(zCreate) body: z.infer<typeof zCreate>) {
43
+ return store.create(body) // 201 automatic for POST
44
+ }
45
+ }
46
+ ```
47
+
48
+ ### Agents (AI with tools)
49
+ ```typescript
50
+ import { Agent, MainLoop, Toolbox, Tool, Mixin } from '@theokit/agents'
51
+
52
+ @Agent({ name: 'assistant', route: '/api/agents/assistant', model: 'openai/gpt-4o-mini' })
53
+ @Mixin(TaskTools)
54
+ class AssistantAgent {
55
+ @MainLoop({ strategy: 'react' })
56
+ async run() {}
57
+ }
58
+ ```
59
+
60
+ ### Validation
61
+ - **Zod is the single source of truth** — define schema once, get types + validation + OpenAPI
62
+ - `@Body(zodSchema)` validates automatically, returns 422 on failure
63
+ - Use `z.infer<typeof schema>` for TypeScript types
64
+
65
+ ### Error Handling
66
+ - `throw new NotFoundException('...')` → 404
67
+ - `throw new BadRequestException('...')` → 400
68
+ - `@UseFilters(MyFilter)` for custom error format
69
+ - 500 errors are scrubbed — raw messages never reach clients
70
+
71
+ ### Guards (Auth/RBAC)
72
+ - `@UseGuards(AuthGuard)` on controller or agent
73
+ - Guards apply to BOTH HTTP and agent routes (shared pipeline)
74
+ - `canActivate(ctx: ExecutionContext): boolean`
75
+
76
+ ### Path Aliases
77
+ - `@/*` → project root (configured in tsconfig.json)
78
+ - `@/server/*` → `./server/*`
79
+
80
+ ## Commands
81
+
82
+ ```bash
83
+ npm run dev # Start dev server (tsx --watch)
84
+ npm run build # Build for production (tsup)
85
+ npm run start # Run production build
86
+ npm run lint # ESLint check
87
+ npm run format # Prettier format
88
+ npm run typecheck # TypeScript type check
89
+ ```
90
+
91
+ ## Don't
92
+
93
+ - Don't use `any` — use Zod schemas + `z.infer<>`
94
+ - Don't write raw `res.status().json()` — use decorators (`@HttpCode`, `@Header`)
95
+ - Don't create separate auth middleware for agents — use `@UseGuards` (same as controllers)
96
+ - Don't parse request body manually — use `@Body(zodSchema)`
@@ -0,0 +1,11 @@
1
+ export default function ErrorPage() {
2
+ return (
3
+ <div className="container" style={{ textAlign: 'center', paddingTop: '20vh' }}>
4
+ <h1 style={{ fontSize: '2rem', marginBottom: '12px' }}>Something went wrong</h1>
5
+ <p className="subtitle">An unexpected error occurred.</p>
6
+ <a href="/" style={{ color: 'var(--accent)', marginTop: '16px', display: 'inline-block' }}>
7
+ Go back home
8
+ </a>
9
+ </div>
10
+ )
11
+ }
@@ -5,50 +5,11 @@ export default function Layout({ children }: { children: React.ReactNode }) {
5
5
  <meta charSet="utf-8" />
6
6
  <meta name="viewport" content="width=device-width, initial-scale=1" />
7
7
  <title>TheoKit App</title>
8
- <style dangerouslySetInnerHTML={{ __html: STYLES }} />
8
+ <meta name="description" content="Built with TheoKit — the app your agent lives in" />
9
+ <link rel="stylesheet" href="/globals.css" />
10
+ <link rel="icon" href="/favicon.svg" />
9
11
  </head>
10
12
  <body>{children}</body>
11
13
  </html>
12
14
  )
13
15
  }
14
-
15
- const STYLES = `
16
- :root { --bg: #0a0a0a; --card: #141414; --border: #2a2a2a; --text: #e0e0e0; --muted: #888; --accent: #6366f1; --green: #22c55e; --red: #ef4444; --yellow: #eab308; }
17
- * { margin: 0; padding: 0; box-sizing: border-box; }
18
- body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; background: var(--bg); color: var(--text); min-height: 100vh; }
19
- #app { max-width: 1200px; margin: 0 auto; padding: 20px; }
20
- header { margin-bottom: 24px; }
21
- h1 { font-size: 1.6rem; } .accent { color: var(--accent); }
22
- .subtitle { color: var(--muted); font-size: 0.85rem; margin-top: 2px; }
23
- .role-bar { margin-top: 12px; }
24
- .role-bar select { padding: 6px 12px; background: #1a1a1a; border: 1px solid var(--border); border-radius: 6px; color: var(--text); font-size: 0.8rem; }
25
- .grid { display: grid; grid-template-columns: 1fr 1fr; gap: 20px; } @media(max-width:768px){.grid{grid-template-columns:1fr;}}
26
- .card { background: var(--card); border: 1px solid var(--border); border-radius: 12px; padding: 20px; }
27
- h2 { font-size: 1rem; margin-bottom: 14px; display: flex; align-items: center; gap: 8px; }
28
- .badge { font-size: 0.65rem; padding: 2px 8px; border-radius: 99px; background: #6366f122; color: var(--accent); }
29
- .badge-ai { background: #eab30822; color: var(--yellow); }
30
- table { width: 100%; border-collapse: collapse; font-size: 0.85rem; }
31
- th { text-align: left; padding: 8px 4px; color: var(--muted); border-bottom: 1px solid var(--border); font-weight: 500; }
32
- td { padding: 8px 4px; border-bottom: 1px solid var(--border); }
33
- tr.done td { opacity: 0.5; text-decoration: line-through; }
34
- .prio { font-size: 0.7rem; padding: 2px 8px; border-radius: 99px; }
35
- .prio-high { background: #ef444422; color: var(--red); } .prio-med { background: #eab30822; color: var(--yellow); } .prio-low { background: #22c55e22; color: var(--green); }
36
- .create-bar { display: flex; gap: 8px; margin-top: 14px; }
37
- .create-bar input { flex: 1; padding: 8px 12px; background: #1a1a1a; border: 1px solid var(--border); border-radius: 6px; color: var(--text); font-size: 0.85rem; }
38
- .create-bar select { padding: 8px; background: #1a1a1a; border: 1px solid var(--border); border-radius: 6px; color: var(--text); font-size: 0.8rem; }
39
- .create-bar button { padding: 8px 16px; background: var(--accent); color: white; border: none; border-radius: 6px; cursor: pointer; font-weight: 600; }
40
- .error { color: var(--red); font-size: 0.8rem; margin-top: 4px; }
41
- .chat-box { height: 400px; overflow-y: auto; padding: 12px; background: #0d0d0d; border-radius: 8px; margin-bottom: 10px; font-size: 0.85rem; line-height: 1.6; }
42
- .msg { margin-bottom: 10px; padding: 8px 12px; border-radius: 8px; }
43
- .msg.user { background: #6366f118; color: var(--accent); }
44
- .msg.agent { background: #1a1a1a; }
45
- .msg.tool { background: #eab30810; color: var(--yellow); font-size: 0.78rem; font-family: monospace; }
46
- .msg.system { color: var(--muted); font-size: 0.78rem; font-style: italic; }
47
- .msg.error { color: var(--red); font-size: 0.8rem; }
48
- .chat-bar { display: flex; gap: 8px; }
49
- .chat-bar input { flex: 1; padding: 10px 14px; background: #1a1a1a; border: 1px solid var(--border); border-radius: 8px; color: var(--text); font-size: 0.9rem; outline: none; }
50
- .chat-bar input:focus { border-color: var(--accent); }
51
- .chat-bar button { padding: 10px 20px; background: var(--accent); color: white; border: none; border-radius: 8px; cursor: pointer; font-weight: 600; }
52
- .chat-bar button:disabled { opacity: 0.4; cursor: not-allowed; }
53
- .cost { color: var(--muted); font-size: 0.75rem; margin-top: 6px; text-align: right; }
54
- `
@@ -0,0 +1,10 @@
1
+ export default function Loading() {
2
+ return (
3
+ <div
4
+ className="container"
5
+ style={{ display: 'flex', justifyContent: 'center', alignItems: 'center', minHeight: '60vh' }}
6
+ >
7
+ <div className="loading-spinner" />
8
+ </div>
9
+ )
10
+ }
@@ -0,0 +1,13 @@
1
+ export default function NotFound() {
2
+ return (
3
+ <div className="container" style={{ textAlign: 'center', paddingTop: '20vh' }}>
4
+ <h1 style={{ fontSize: '4rem', fontWeight: 700, color: 'var(--text-muted)' }}>404</h1>
5
+ <p className="subtitle" style={{ fontSize: '1.1rem' }}>
6
+ Page not found
7
+ </p>
8
+ <a href="/" style={{ color: 'var(--accent)', marginTop: '16px', display: 'inline-block' }}>
9
+ Go back home
10
+ </a>
11
+ </div>
12
+ )
13
+ }
@@ -1,20 +1,19 @@
1
- /**
2
- * Main page — Task Manager + AI Chat.
3
- *
4
- * Split layout: left side CRUD, right side AI agent chat.
5
- * SSE streaming renders token-by-token with tool call visualization.
6
- */
7
1
  export default function Page() {
8
2
  return (
9
- <div id="app">
3
+ <div className="container">
10
4
  <header>
11
- <h1><span className="accent">TheoKit</span> Task Manager</h1>
12
- <p className="subtitle">HTTP Controllers + AI Agent — same pipeline, same guards</p>
5
+ <h1>
6
+ <span className="accent">TheoKit</span> Task Manager
7
+ </h1>
8
+ <p className="subtitle">
9
+ HTTP Controllers + AI Agent — same guards, distinct pipelines. Edit <code>server/</code>{' '}
10
+ to get started.
11
+ </p>
13
12
  <div className="role-bar">
14
- <label>Role: </label>
15
- <select id="role">
13
+ <label htmlFor="role">Role:</label>
14
+ <select id="role" defaultValue="user">
16
15
  <option value="">None (public only)</option>
17
- <option value="user" selected>User</option>
16
+ <option value="user">User</option>
18
17
  <option value="admin">Admin</option>
19
18
  </select>
20
19
  </div>
@@ -23,10 +22,18 @@ export default function Page() {
23
22
  <main className="grid">
24
23
  {/* Left: CRUD Panel */}
25
24
  <section className="card">
26
- <h2>📋 Tasks <span className="badge">@Controller</span></h2>
25
+ <h2>
26
+ Tasks <span className="badge">@Controller</span>
27
+ </h2>
27
28
  <table>
28
- <thead><tr><th>Task</th><th>Priority</th><th>Status</th></tr></thead>
29
- <tbody id="task-list"></tbody>
29
+ <thead>
30
+ <tr>
31
+ <th>Task</th>
32
+ <th>Priority</th>
33
+ <th>Status</th>
34
+ </tr>
35
+ </thead>
36
+ <tbody id="task-list" />
30
37
  </table>
31
38
  <form id="create-form" className="create-bar">
32
39
  <input id="new-title" placeholder="New task..." required minLength={3} />
@@ -37,147 +44,28 @@ export default function Page() {
37
44
  </select>
38
45
  <button type="submit">Add</button>
39
46
  </form>
40
- <p id="form-error" className="error"></p>
47
+ <p id="form-error" className="error" />
41
48
  </section>
42
49
 
43
50
  {/* Right: AI Chat */}
44
51
  <section className="card">
45
- <h2>🤖 AI Assistant <span className="badge badge-ai">@Agent + SSE</span></h2>
52
+ <h2>
53
+ AI Assistant <span className="badge badge-ai">@Agent + SSE</span>
54
+ </h2>
46
55
  <div id="chat" className="chat-box">
47
56
  <div className="msg system">Ask me to list, create, or complete tasks...</div>
48
57
  </div>
49
58
  <div className="chat-bar">
50
59
  <input id="chat-input" placeholder="Message the AI assistant..." />
51
- <button id="chat-send" onClick={() => {}}>Send</button>
60
+ <button id="chat-send" type="button">
61
+ Send
62
+ </button>
52
63
  </div>
53
- <p id="chat-cost" className="cost"></p>
64
+ <p id="chat-cost" className="cost" />
54
65
  </section>
55
66
  </main>
56
67
 
57
- <ClientScript />
68
+ <script src="/client.js" defer />
58
69
  </div>
59
70
  )
60
71
  }
61
-
62
- function ClientScript() {
63
- return (
64
- <script dangerouslySetInnerHTML={{ __html: CLIENT_JS }} />
65
- )
66
- }
67
-
68
- const CLIENT_JS = `
69
- const API = '';
70
- let sessionId = 'session-' + Date.now();
71
- const getRole = () => document.getElementById('role').value;
72
- const headers = () => {
73
- const h = { 'Content-Type': 'application/json' };
74
- const r = getRole();
75
- if (r) h['x-role'] = r;
76
- return h;
77
- };
78
-
79
- // ─── Tasks CRUD ─────────────────────────────────────
80
-
81
- async function loadTasks() {
82
- const res = await fetch(API + '/api/tasks');
83
- const tasks = await res.json();
84
- document.getElementById('task-list').innerHTML = tasks.map(t => {
85
- const statusClass = t.done ? 'done' : 'pending';
86
- const prioClass = t.priority === 'high' ? 'prio-high' : t.priority === 'low' ? 'prio-low' : 'prio-med';
87
- return '<tr class="' + statusClass + '"><td>' + (t.done ? '✅ ' : '○ ') + t.title + '</td><td><span class="prio ' + prioClass + '">' + t.priority + '</span></td><td>' + (t.done ? 'Done' : 'To do') + '</td></tr>';
88
- }).join('');
89
- }
90
-
91
- document.getElementById('create-form').addEventListener('submit', async (e) => {
92
- e.preventDefault();
93
- const title = document.getElementById('new-title').value.trim();
94
- const priority = document.getElementById('new-priority').value;
95
- const err = document.getElementById('form-error');
96
- err.textContent = '';
97
- if (!title) return;
98
- const res = await fetch(API + '/api/tasks', { method: 'POST', headers: headers(), body: JSON.stringify({ title, priority }) });
99
- if (res.status === 403) { err.textContent = '403 — Need User role'; return; }
100
- if (res.status === 422) { const e = await res.json(); err.textContent = e.error?.issues?.[0]?.message || 'Validation error'; return; }
101
- if (!res.ok) { err.textContent = 'Error ' + res.status; return; }
102
- document.getElementById('new-title').value = '';
103
- loadTasks();
104
- });
105
-
106
- // ─── AI Chat (SSE) ──────────────────────────────────
107
-
108
- document.getElementById('chat-send').addEventListener('click', sendChat);
109
- document.getElementById('chat-input').addEventListener('keydown', (e) => { if (e.key === 'Enter') sendChat(); });
110
-
111
- async function sendChat() {
112
- const input = document.getElementById('chat-input');
113
- const msg = input.value.trim();
114
- if (!msg) return;
115
- input.value = '';
116
-
117
- const chat = document.getElementById('chat');
118
- chat.innerHTML += '<div class="msg user">You: ' + escapeHtml(msg) + '</div>';
119
- document.getElementById('chat-send').disabled = true;
120
-
121
- try {
122
- const res = await fetch(API + '/api/agents/assistant/chat', {
123
- method: 'POST', headers: headers(),
124
- body: JSON.stringify({ message: msg, sessionId })
125
- });
126
-
127
- if (res.status === 403) {
128
- chat.innerHTML += '<div class="msg system">403 — Need User role to chat</div>';
129
- document.getElementById('chat-send').disabled = false;
130
- return;
131
- }
132
-
133
- const reader = res.body.getReader();
134
- const decoder = new TextDecoder();
135
- let agentDiv = document.createElement('div');
136
- agentDiv.className = 'msg agent';
137
- agentDiv.textContent = '';
138
- chat.appendChild(agentDiv);
139
-
140
- let buffer = '';
141
- while (true) {
142
- const { done, value } = await reader.read();
143
- if (done) break;
144
- buffer += decoder.decode(value, { stream: true });
145
- const lines = buffer.split('\\n');
146
- buffer = lines.pop() || '';
147
- for (const line of lines) {
148
- if (!line.startsWith('data: ')) continue;
149
- try {
150
- const ev = JSON.parse(line.slice(6));
151
- if (ev.type === 'text_delta') {
152
- agentDiv.innerHTML += formatMarkdown(ev.content);
153
- } else if (ev.type === 'tool_call') {
154
- chat.insertBefore(toolMsg('🔧 Calling: ' + ev.toolName), agentDiv);
155
- } else if (ev.type === 'tool_result') {
156
- chat.insertBefore(toolMsg('✅ ' + (ev.output || '').substring(0, 80)), agentDiv);
157
- } else if (ev.type === 'thinking') {
158
- chat.insertBefore(sysMsg('💭 ' + ev.content), agentDiv);
159
- } else if (ev.type === 'done') {
160
- const cost = ev.cost ? ' · $' + ev.cost.toFixed(6) : '';
161
- document.getElementById('chat-cost').textContent = ev.usage.totalTokens + ' tokens · ' + ev.durationMs + 'ms' + cost;
162
- } else if (ev.type === 'error') {
163
- chat.innerHTML += '<div class="msg error">' + ev.message + '</div>';
164
- }
165
- } catch {}
166
- }
167
- chat.scrollTop = chat.scrollHeight;
168
- }
169
- loadTasks(); // refresh after agent actions
170
- } catch (e) {
171
- chat.innerHTML += '<div class="msg error">Error: ' + e.message + '</div>';
172
- }
173
- document.getElementById('chat-send').disabled = false;
174
- chat.scrollTop = chat.scrollHeight;
175
- }
176
-
177
- function toolMsg(text) { const d = document.createElement('div'); d.className = 'msg tool'; d.textContent = text; return d; }
178
- function sysMsg(text) { const d = document.createElement('div'); d.className = 'msg system'; d.textContent = text; return d; }
179
- function escapeHtml(s) { return s.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;'); }
180
- function formatMarkdown(s) { return escapeHtml(s).replace(/\\*\\*(.+?)\\*\\*/g, '<strong>$1</strong>').replace(/\\n/g, '<br>'); }
181
-
182
- loadTasks();
183
- `
@@ -0,0 +1,30 @@
1
+ /**
2
+ * TheoKit App — opinionated, React-first.
3
+ *
4
+ * React SSR renders app/page.tsx inside app/layout.tsx.
5
+ * Backend classes registered in server/index.ts.
6
+ * Routes inferred from class names. Zero manual wiring.
7
+ */
8
+ import 'reflect-metadata'
9
+ import { renderToString } from 'react-dom/server'
10
+ import { TheoApp } from '@theokit/http/app'
11
+ import { TasksController, AssistantAgent, TaskTools } from './server/index.js'
12
+ import Layout from './app/layout.js'
13
+ import Page from './app/page.js'
14
+
15
+ const html =
16
+ '<!DOCTYPE html>' +
17
+ renderToString(
18
+ <Layout>
19
+ <Page />
20
+ </Layout>,
21
+ )
22
+
23
+ const app = await TheoApp.create({
24
+ controllers: [TasksController],
25
+ agents: [AssistantAgent],
26
+ providers: [TaskTools],
27
+ html,
28
+ })
29
+
30
+ await app.listen(3000)
@@ -0,0 +1,14 @@
1
+ import tseslint from 'typescript-eslint'
2
+ import prettierConfig from 'eslint-config-prettier'
3
+
4
+ export default tseslint.config(
5
+ { ignores: ['dist/', 'node_modules/'] },
6
+ ...tseslint.configs.recommended,
7
+ prettierConfig,
8
+ {
9
+ rules: {
10
+ '@typescript-eslint/no-unused-vars': ['warn', { argsIgnorePattern: '^_' }],
11
+ '@typescript-eslint/no-explicit-any': 'warn',
12
+ },
13
+ },
14
+ )
@@ -4,18 +4,34 @@
4
4
  "private": true,
5
5
  "type": "module",
6
6
  "scripts": {
7
- "dev": "bun app.ts",
8
- "dev:node": "npx tsx app.ts",
9
- "test": "bun test"
7
+ "dev": "npx tsx --watch app.tsx",
8
+ "dev:bun": "bun --watch app.tsx",
9
+ "build": "npx tsup app.tsx --format esm --out-dir dist",
10
+ "start": "node dist/app.js",
11
+ "lint": "eslint .",
12
+ "lint:fix": "eslint . --fix",
13
+ "format": "prettier --write .",
14
+ "format:check": "prettier --check .",
15
+ "typecheck": "tsc --noEmit"
10
16
  },
11
17
  "dependencies": {
12
- "@theokit/http-decorators": "^0.1.0",
13
- "@theokit/agents": "^0.1.0",
18
+ "@theokit/http": "^0.5.0",
19
+ "@theokit/agents": "^0.4.0",
20
+ "react": "^19.0.0",
21
+ "react-dom": "^19.0.0",
14
22
  "reflect-metadata": "^0.2.0",
15
23
  "zod": "^4.0.0"
16
24
  },
17
25
  "devDependencies": {
26
+ "@swc/core": "^1.3.0",
27
+ "@types/react": "^19.0.0",
28
+ "@types/react-dom": "^19.0.0",
29
+ "eslint": "^9.0.0",
30
+ "eslint-config-prettier": "^10.0.0",
31
+ "prettier": "^3.0.0",
32
+ "tsup": "^8.0.0",
33
+ "tsx": "^4.19.0",
18
34
  "typescript": "^5.5.0",
19
- "@swc/core": "^1.3.0"
35
+ "typescript-eslint": "^8.0.0"
20
36
  }
21
37
  }
@@ -0,0 +1,173 @@
1
+ /**
2
+ * Client-side interactivity — loaded via <script src="/client.js" defer> in page.tsx.
3
+ *
4
+ * Handles: task CRUD via fetch, AI chat via SSE streaming.
5
+ * No build step required — runs directly in the browser.
6
+ */
7
+
8
+ var getRole = function () {
9
+ return document.getElementById('role').value
10
+ }
11
+ var headers = function () {
12
+ var h = { 'Content-Type': 'application/json' }
13
+ var r = getRole()
14
+ if (r) h['x-role'] = r
15
+ return h
16
+ }
17
+
18
+ // ─── Tasks CRUD ─────────────────────────────────────
19
+
20
+ async function loadTasks() {
21
+ var res = await fetch('/api/tasks')
22
+ var tasks = await res.json()
23
+ var tbody = document.getElementById('task-list')
24
+ tbody.innerHTML = tasks
25
+ .map(function (t) {
26
+ var cls = t.done ? 'done' : ''
27
+ var prio =
28
+ t.priority === 'high' ? 'prio-high' : t.priority === 'low' ? 'prio-low' : 'prio-med'
29
+ var icon = t.done ? '\u2705 ' : '\u25CB '
30
+ return (
31
+ '<tr class="' +
32
+ cls +
33
+ '"><td>' +
34
+ icon +
35
+ esc(t.title) +
36
+ '</td><td><span class="prio ' +
37
+ prio +
38
+ '">' +
39
+ t.priority +
40
+ '</span></td><td>' +
41
+ (t.done ? 'Done' : 'To do') +
42
+ '</td></tr>'
43
+ )
44
+ })
45
+ .join('')
46
+ }
47
+
48
+ document.getElementById('create-form').addEventListener('submit', async function (e) {
49
+ e.preventDefault()
50
+ var input = document.getElementById('new-title')
51
+ var select = document.getElementById('new-priority')
52
+ var err = document.getElementById('form-error')
53
+ err.textContent = ''
54
+ var title = input.value.trim()
55
+ if (!title) return
56
+ var res = await fetch('/api/tasks', {
57
+ method: 'POST',
58
+ headers: headers(),
59
+ body: JSON.stringify({ title: title, priority: select.value }),
60
+ })
61
+ if (res.status === 403) {
62
+ err.textContent = '403 \u2014 Need User role'
63
+ return
64
+ }
65
+ if (!res.ok) {
66
+ var data = await res.json()
67
+ err.textContent =
68
+ (data.error && data.error.issues && data.error.issues[0] && data.error.issues[0].message) ||
69
+ 'Error ' + res.status
70
+ return
71
+ }
72
+ input.value = ''
73
+ loadTasks()
74
+ })
75
+
76
+ // ─── AI Chat (SSE) ──────────────────────────────────
77
+
78
+ var sessionId = 'session-' + Date.now()
79
+ var chatEl = document.getElementById('chat')
80
+ var chatInput = document.getElementById('chat-input')
81
+ var chatBtn = document.getElementById('chat-send')
82
+
83
+ chatBtn.addEventListener('click', sendChat)
84
+ chatInput.addEventListener('keydown', function (e) {
85
+ if (e.key === 'Enter') sendChat()
86
+ })
87
+
88
+ async function sendChat() {
89
+ var msg = chatInput.value.trim()
90
+ if (!msg) return
91
+ chatInput.value = ''
92
+ chatEl.innerHTML += '<div class="msg user">You: ' + esc(msg) + '</div>'
93
+ chatBtn.disabled = true
94
+
95
+ try {
96
+ var res = await fetch('/api/agents/assistant/chat', {
97
+ method: 'POST',
98
+ headers: headers(),
99
+ body: JSON.stringify({ message: msg, sessionId: sessionId }),
100
+ })
101
+ if (res.status === 403) {
102
+ chatEl.innerHTML += '<div class="msg system">403 \u2014 Need User role to chat</div>'
103
+ chatBtn.disabled = false
104
+ return
105
+ }
106
+
107
+ var reader = res.body.getReader()
108
+ var decoder = new TextDecoder()
109
+ var agentDiv = document.createElement('div')
110
+ agentDiv.className = 'msg agent'
111
+ chatEl.appendChild(agentDiv)
112
+
113
+ var buf = ''
114
+ while (true) {
115
+ var chunk = await reader.read()
116
+ if (chunk.done) break
117
+ buf += decoder.decode(chunk.value, { stream: true })
118
+ var lines = buf.split('\n')
119
+ buf = lines.pop() || ''
120
+ for (var i = 0; i < lines.length; i++) {
121
+ var line = lines[i]
122
+ if (!line.startsWith('data: ')) continue
123
+ try {
124
+ var ev = JSON.parse(line.slice(6))
125
+ if (ev.type === 'text_delta') agentDiv.innerHTML += fmtMd(ev.content)
126
+ else if (ev.type === 'tool_call')
127
+ chatEl.insertBefore(mkMsg('tool', '\uD83D\uDD27 ' + ev.toolName), agentDiv)
128
+ else if (ev.type === 'tool_result')
129
+ chatEl.insertBefore(
130
+ mkMsg('tool', '\u2705 ' + (ev.output || '').substring(0, 80)),
131
+ agentDiv,
132
+ )
133
+ else if (ev.type === 'thinking')
134
+ chatEl.insertBefore(mkMsg('system', '\uD83D\uDCAD ' + ev.content), agentDiv)
135
+ else if (ev.type === 'done')
136
+ document.getElementById('chat-cost').textContent =
137
+ ((ev.usage && ev.usage.totalTokens) || 0) +
138
+ ' tokens \u00B7 ' +
139
+ ev.durationMs +
140
+ 'ms' +
141
+ (ev.cost ? ' \u00B7 $' + ev.cost.toFixed(6) : '')
142
+ else if (ev.type === 'error')
143
+ chatEl.innerHTML += '<div class="msg error">' + esc(ev.message) + '</div>'
144
+ } catch (_) {
145
+ /* partial JSON */
146
+ }
147
+ }
148
+ chatEl.scrollTop = chatEl.scrollHeight
149
+ }
150
+ loadTasks()
151
+ } catch (e) {
152
+ chatEl.innerHTML += '<div class="msg error">Error: ' + esc(e.message) + '</div>'
153
+ }
154
+ chatBtn.disabled = false
155
+ chatEl.scrollTop = chatEl.scrollHeight
156
+ }
157
+
158
+ function mkMsg(cls, text) {
159
+ var d = document.createElement('div')
160
+ d.className = 'msg ' + cls
161
+ d.textContent = text
162
+ return d
163
+ }
164
+ function esc(s) {
165
+ return s.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
166
+ }
167
+ function fmtMd(s) {
168
+ return esc(s)
169
+ .replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>')
170
+ .replace(/\n/g, '<br>')
171
+ }
172
+
173
+ loadTasks()
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32"><rect width="32" height="32" rx="6" fill="#6366f1"/><text x="50%" y="55%" font-family="system-ui" font-size="20" font-weight="bold" fill="white" text-anchor="middle" dominant-baseline="middle">T</text></svg>