@vitortrack777/djinnai 0.0.0-stage → 0.6.1

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/server.js ADDED
@@ -0,0 +1,350 @@
1
+ import { execFile, spawn } from 'node:child_process';
2
+ import { randomBytes } from 'node:crypto';
3
+ import { createReadStream, createWriteStream, existsSync, mkdirSync } from 'node:fs';
4
+ import { mkdir, readFile, writeFile } from 'node:fs/promises';
5
+ import { createServer } from 'node:http';
6
+ import path from 'node:path';
7
+ import { fileURLToPath } from 'node:url';
8
+ import { promisify } from 'node:util';
9
+
10
+ const DIST = path.join(path.dirname(fileURLToPath(import.meta.url)), 'web', 'dist');
11
+ const MIME = { '.html': 'text/html', '.js': 'text/javascript', '.css': 'text/css', '.svg': 'image/svg+xml' };
12
+ const OLLAMA_URL = 'http://127.0.0.1:11434';
13
+ const MAX_FOLDERS = 150;
14
+ const INIT_TIMEOUT_MS = 20000;
15
+
16
+ const APP_COMMANDS = [
17
+ ['start', 'Sobe o stack e builda só o que mudou'],
18
+ ['restart', 'Reinicia só os serviços alterados (ex.: /restart api)'],
19
+ ['rebuild', 'Build sem cache e recriação forçada'],
20
+ ['logs', 'Logs dos containers (ex.: /logs --tail=100 api)'],
21
+ ['status', 'Containers e uso do Docker'],
22
+ ['stop', 'Para e remove os containers'],
23
+ ['clean', 'Remove imagens órfãs e limita o cache'],
24
+ ['hot-reload', 'Atualiza só o frontend'],
25
+ ['worker', 'Builda o front atualizando a pasta dist'],
26
+ ];
27
+
28
+ const FALLBACK_MODELS = [{ key: 'claude:default', name: 'Padrão', tag: 'Claude' }];
29
+
30
+ const LEGACY_DIR = '.detro';
31
+ const DATA_DIR = '.djinn';
32
+
33
+ const dataDir = (projectDir) => path.join(projectDir, existsSync(path.join(projectDir, LEGACY_DIR)) && !existsSync(path.join(projectDir, DATA_DIR)) ? LEGACY_DIR : DATA_DIR);
34
+ const configPath = (projectDir) => path.join(dataDir(projectDir), 'config.json');
35
+
36
+ async function loadConfig(projectDir) {
37
+ try {
38
+ return JSON.parse(await readFile(configPath(projectDir), 'utf8'));
39
+ } catch (error) {
40
+ if (error.code === 'ENOENT') return {};
41
+ throw new Error(`${path.relative(projectDir, configPath(projectDir))} não é um JSON válido: ${error.message}`);
42
+ }
43
+ }
44
+
45
+ async function saveSettings(projectDir, settings) {
46
+ const config = { ...(await loadConfig(projectDir)), ...Object.fromEntries(Object.entries(settings).filter(([, value]) => value !== undefined)) };
47
+ await mkdir(path.dirname(configPath(projectDir)), { recursive: true });
48
+ await writeFile(configPath(projectDir), `${JSON.stringify(config, null, 2)}\n`);
49
+ }
50
+
51
+ const ollamaUrlOf = (config) => (config.ollamaUrl ?? config.ai?.ollama?.baseUrl ?? OLLAMA_URL).replace(/\/$/, '');
52
+
53
+ async function settingsOf(projectDir) {
54
+ const config = await loadConfig(projectDir);
55
+ return {
56
+ model: !config.model || config.model === 'claude:' ? 'claude:default' : config.model,
57
+ effort: config.effort ?? '',
58
+ mode: config.mode ?? 'acceptEdits',
59
+ ollamaUrl: ollamaUrlOf(config),
60
+ };
61
+ }
62
+
63
+ async function ollamaModels(url) {
64
+ try {
65
+ const response = await fetch(`${url}/api/tags`, { signal: AbortSignal.timeout(2000) });
66
+ const { models = [] } = await response.json();
67
+ return models.map(({ name }) => ({ key: `ollama:${name}`, name, tag: 'Ollama' }));
68
+ } catch {
69
+ return [];
70
+ }
71
+ }
72
+
73
+ async function sessionContext(projectDir) {
74
+ const git = (args) => promisify(execFile)('git', args, { cwd: projectDir, maxBuffer: 5e7 }).then(({ stdout }) => stdout.trim(), () => '');
75
+ const [files, notes] = await Promise.all([
76
+ git(['ls-files']),
77
+ readFile(path.join(dataDir(projectDir), 'contexto.md'), 'utf8').then((text) => text.trim(), () => ''),
78
+ ]);
79
+ const folders = [...new Set(files.split('\n').map((file) => file.split('/').slice(0, -1).slice(0, 4).join('/')).filter(Boolean))].sort();
80
+ return [
81
+ folders.length > 0 && `Pastas do projeto (até 4 níveis):\n${folders.slice(0, MAX_FOLDERS).join('\n')}${folders.length > MAX_FOLDERS ? `\n(+${folders.length - MAX_FOLDERS} pastas)` : ''}`,
82
+ notes && `NOTAS DO PROJETO (${path.basename(dataDir(projectDir))}/contexto.md), siga como instrução:\n${notes}`,
83
+ ].filter(Boolean).join('\n\n');
84
+ }
85
+
86
+ function eachLine(stream, onLine) {
87
+ let pending = '';
88
+ stream.on('data', (chunk) => {
89
+ const lines = (pending + chunk).split('\n');
90
+ pending = lines.pop();
91
+ for (const line of lines) if (line.trim()) onLine(line);
92
+ });
93
+ stream.on('end', () => pending.trim() && onLine(pending));
94
+ }
95
+
96
+ const killGroup = (child, signal = 'SIGTERM') => {
97
+ try {
98
+ process.kill(-child.pid, signal);
99
+ } catch {}
100
+ };
101
+
102
+ const readBody = async (request) => {
103
+ let body = '';
104
+ for await (const chunk of request) body += chunk;
105
+ return JSON.parse(body || '{}');
106
+ };
107
+
108
+ export function startServer({ projectDir, appScript, version, port = 0 }) {
109
+ const token = randomBytes(16).toString('hex');
110
+ mkdirSync(dataDir(projectDir), { recursive: true });
111
+ const clients = new Set();
112
+ let history = [];
113
+ let session = null;
114
+ let sessionId = null;
115
+ let busy = false;
116
+ let app = null;
117
+ let record = null;
118
+
119
+ const broadcast = (event, keep = true) => {
120
+ if (keep) history.push(event);
121
+ for (const client of clients) client.write(`data: ${JSON.stringify(event)}\n\n`);
122
+ };
123
+
124
+ const write = (target, message) => target.child.stdin.write(`${JSON.stringify(message)}\n`);
125
+ const control = (target, request) => write(target, { type: 'control_request', request_id: randomBytes(6).toString('hex'), request });
126
+
127
+ async function openSession(settings) {
128
+ const [agent, name] = settings.model.split(/:(.*)/s);
129
+ const args = ['-p', '--input-format', 'stream-json', '--output-format', 'stream-json', '--verbose', '--include-partial-messages', '--permission-prompt-tool', 'stdio', '--permission-mode', settings.mode];
130
+ const context = await sessionContext(projectDir);
131
+ if (context) args.push('--append-system-prompt', context);
132
+ if (name !== 'default') args.push('--model', name);
133
+ if (settings.effort) args.push('--effort', settings.effort);
134
+ if (sessionId) args.push('--resume', sessionId);
135
+
136
+ const env = { ...process.env, CLAUDE_CODE_EMIT_SESSION_STATE_EVENTS: '1' };
137
+ delete env.ANTHROPIC_API_KEY;
138
+ if (agent === 'ollama') Object.assign(env, { ANTHROPIC_BASE_URL: settings.ollamaUrl, ANTHROPIC_AUTH_TOKEN: 'ollama', ANTHROPIC_API_KEY: '' });
139
+
140
+ const child = spawn('claude', args, { cwd: projectDir, env, detached: true });
141
+ const current = { child, agent, effort: settings.effort, model: settings.model, mode: settings.mode, closing: false };
142
+ let resolveInit;
143
+ current.init = new Promise((resolve) => { resolveInit = resolve; });
144
+ const initId = randomBytes(6).toString('hex');
145
+ let errors = '';
146
+ let spawnError = '';
147
+
148
+ eachLine(child.stdout, (line) => {
149
+ record?.write(`${line}\n`);
150
+ let event;
151
+ try {
152
+ event = JSON.parse(line);
153
+ } catch {
154
+ return;
155
+ }
156
+ if (event.session_id) sessionId = event.session_id;
157
+ if (event.type === 'control_response') {
158
+ if (event.response?.request_id === initId) resolveInit(event.response.response ?? null);
159
+ return;
160
+ }
161
+ if (event.type === 'control_request' && event.request?.subtype !== 'can_use_tool') {
162
+ write(current, { type: 'control_response', response: { subtype: 'error', request_id: event.request_id, error: 'O DjinnAI não atende este pedido.' } });
163
+ return;
164
+ }
165
+ if (event.subtype === 'session_state_changed') busy = event.state !== 'idle';
166
+ broadcast(event, event.type !== 'stream_event');
167
+ });
168
+ child.stderr.on('data', (chunk) => { errors = (errors + chunk).slice(-2000); });
169
+ child.stdin.on('error', () => {});
170
+ child.on('error', (error) => {
171
+ spawnError = `Não foi possível executar o claude (${error.message}). Confira se o Claude Code está instalado e logado.`;
172
+ });
173
+ child.on('close', (code) => {
174
+ resolveInit(null);
175
+ if (session === current) session = null;
176
+ if (current.closing) return;
177
+ busy = false;
178
+ broadcast({ type: 'session_end', error: spawnError || errors.trim().slice(-600) || `O agente encerrou sozinho (código ${code}). Mande a mensagem de novo para retomar a conversa.` });
179
+ });
180
+
181
+ write(current, { type: 'control_request', request_id: initId, request: { subtype: 'initialize' } });
182
+ return current;
183
+ }
184
+
185
+ let opening = null;
186
+
187
+ async function ensureSession(settings) {
188
+ while (opening) await opening;
189
+ const [agent] = settings.model.split(':');
190
+ if (session && session.agent === agent && session.effort === settings.effort) {
191
+ if (session.mode !== settings.mode) control(session, { subtype: 'set_permission_mode', mode: settings.mode });
192
+ if (session.model !== settings.model) {
193
+ const name = settings.model.split(/:(.*)/s)[1];
194
+ control(session, { subtype: 'set_model', ...(name !== 'default' && { model: name }) });
195
+ }
196
+ Object.assign(session, { mode: settings.mode, model: settings.model });
197
+ return session;
198
+ }
199
+ if (session && busy) return session;
200
+ closeSession();
201
+ opening = openSession(settings);
202
+ session = await opening;
203
+ opening = null;
204
+ return session;
205
+ }
206
+
207
+ function closeSession() {
208
+ if (!session) return;
209
+ session.closing = true;
210
+ killGroup(session.child);
211
+ session = null;
212
+ }
213
+
214
+ const routes = {
215
+ 'GET /api/state': async () => {
216
+ const settings = await settingsOf(projectDir);
217
+ const current = await ensureSession(settings);
218
+ const init = await Promise.race([current.init, new Promise((resolve) => setTimeout(resolve, INIT_TIMEOUT_MS, null))]);
219
+ const branch = await promisify(execFile)('git', ['branch', '--show-current'], { cwd: projectDir }).then(({ stdout }) => stdout.trim(), () => '');
220
+ const appNames = new Set(['novo', ...APP_COMMANDS.map(([name]) => name)]);
221
+ const claudeModels = (init?.models ?? []).map((m) => ({ key: `claude:${m.value}`, name: m.value === 'default' ? 'Padrão' : m.displayName, tag: m.description?.split(' · ')[0] ?? 'Claude' }));
222
+ return {
223
+ project: path.basename(projectDir),
224
+ projectDir,
225
+ branch,
226
+ version,
227
+ agentError: init ? '' : 'O Claude Code não respondeu. Confira se o comando claude está instalado e logado e rode djinn de novo.',
228
+ models: [...(claudeModels.length ? claudeModels : FALLBACK_MODELS), ...(await ollamaModels(settings.ollamaUrl))],
229
+ appCommands: [...appNames],
230
+ commands: [
231
+ { key: 'novo', name: '/novo', description: 'Começa uma conversa nova' },
232
+ ...APP_COMMANDS.map(([key, description]) => ({ key, name: `/${key}`, description })),
233
+ ...(init?.commands ?? []).filter((c) => !appNames.has(c.name)).map((c) => ({ key: `claude-${c.name}`, name: `/${c.name}`, description: c.description.split(/(?<=\.)\s/)[0].slice(0, 110) })),
234
+ ],
235
+ settings: { model: settings.model, effort: settings.effort, mode: settings.mode },
236
+ };
237
+ },
238
+ 'POST /api/settings': async (request) => {
239
+ const { model, effort, mode } = await readBody(request);
240
+ await saveSettings(projectDir, { model, effort, mode });
241
+ await ensureSession(await settingsOf(projectDir));
242
+ return {};
243
+ },
244
+ 'POST /api/send': async (request) => {
245
+ const { text, images = [] } = await readBody(request);
246
+ const current = await ensureSession(await settingsOf(projectDir));
247
+ if (!busy) {
248
+ record?.end();
249
+ record = createWriteStream(path.join(dataDir(projectDir), 'last-turn.ndjson')).on('error', () => {});
250
+ }
251
+ const content = images.length
252
+ ? [...images.map(({ mediaType, data }) => ({ type: 'image', source: { type: 'base64', media_type: mediaType, data } })), { type: 'text', text }]
253
+ : text;
254
+ write(current, { type: 'user', message: { role: 'user', content }, parent_tool_use_id: null });
255
+ busy = true;
256
+ broadcast({ type: 'user_prompt', text, images: images.map(({ mediaType, data }) => `data:${mediaType};base64,${data}`) });
257
+ return {};
258
+ },
259
+ 'POST /api/permission': async (request) => {
260
+ const { requestId, behavior, updatedInput, updatedPermissions, message } = await readBody(request);
261
+ if (!session) throw new Error('A conversa não está mais aberta. Mande a mensagem de novo.');
262
+ const response = behavior === 'allow'
263
+ ? { behavior, updatedInput, ...(updatedPermissions && { updatedPermissions }) }
264
+ : { behavior, message: message || 'O usuário recusou.' };
265
+ write(session, { type: 'control_response', response: { subtype: 'success', request_id: requestId, response } });
266
+ broadcast({ type: 'permission_answer', requestId, behavior });
267
+ return {};
268
+ },
269
+ 'POST /api/interrupt': async () => {
270
+ if (session && busy) {
271
+ control(session, { subtype: 'interrupt' });
272
+ broadcast({ type: 'interrupted' });
273
+ }
274
+ return {};
275
+ },
276
+ 'POST /api/new': async () => {
277
+ closeSession();
278
+ sessionId = null;
279
+ busy = false;
280
+ history = [];
281
+ broadcast({ type: 'reset' }, false);
282
+ settingsOf(projectDir).then(ensureSession).catch(() => {});
283
+ return {};
284
+ },
285
+ 'POST /api/app': async (request) => {
286
+ const { args } = await readBody(request);
287
+ if (!APP_COMMANDS.some(([name]) => name === args[0])) throw new Error(`Comando desconhecido: ${args[0]}`);
288
+ if (app) throw new Error('Já tem um comando do ambiente rodando. Pare ele antes de mandar outro.');
289
+ const child = spawn(appScript, args, { cwd: projectDir, env: { ...process.env, FORCE_COLOR: '0' }, detached: true });
290
+ app = { child, stopped: false, error: '' };
291
+ const current = app;
292
+ broadcast({ type: 'app_start', command: `/${args.join(' ')}` });
293
+ const out = (line) => broadcast({ type: 'app_out', text: `${line}\n` });
294
+ eachLine(child.stdout, out);
295
+ eachLine(child.stderr, out);
296
+ child.on('error', (error) => {
297
+ current.error = `Não foi possível executar ${appScript} (${error.message}). Confira se ele existe e é executável.`;
298
+ });
299
+ child.on('close', (code, signal) => {
300
+ app = null;
301
+ broadcast({ type: 'app_exit', code: code ?? 1, stopped: current.stopped || Boolean(signal), error: current.error });
302
+ });
303
+ return {};
304
+ },
305
+ 'POST /api/stop': async () => {
306
+ if (app) {
307
+ app.stopped = true;
308
+ killGroup(app.child, 'SIGINT');
309
+ }
310
+ return {};
311
+ },
312
+ };
313
+
314
+ const server = createServer(async (request, response) => {
315
+ const url = new URL(request.url, 'http://localhost');
316
+ if (url.pathname === '/api/events') {
317
+ if (url.searchParams.get('t') !== token) return response.writeHead(403).end();
318
+ response.writeHead(200, { 'content-type': 'text/event-stream', 'cache-control': 'no-store', connection: 'keep-alive' });
319
+ for (const event of [{ type: 'reset' }, ...history]) response.write(`data: ${JSON.stringify(event)}\n\n`);
320
+ clients.add(response);
321
+ request.on('close', () => clients.delete(response));
322
+ return;
323
+ }
324
+ const route = routes[`${request.method} ${url.pathname}`];
325
+ if (!route) {
326
+ const file = path.join(DIST, path.normalize(url.pathname === '/' ? '/index.html' : url.pathname));
327
+ if (!file.startsWith(DIST) || !existsSync(file)) return response.writeHead(404).end();
328
+ response.writeHead(200, { 'content-type': MIME[path.extname(file)] ?? 'application/octet-stream' });
329
+ return createReadStream(file).pipe(response);
330
+ }
331
+ if (request.headers['x-djinn-token'] !== token) return response.writeHead(403).end();
332
+ try {
333
+ response.writeHead(200, { 'content-type': 'application/json' }).end(JSON.stringify(await route(request)));
334
+ } catch (error) {
335
+ response.writeHead(400, { 'content-type': 'application/json' }).end(JSON.stringify({ error: error.message }));
336
+ }
337
+ });
338
+
339
+ return new Promise((resolve, reject) => {
340
+ server.on('error', (error) => reject(new Error(error.code === 'EADDRINUSE' ? `A porta ${port} já está em uso. Feche o outro djinn ou rode sem DJINN_PORT.` : error.message)));
341
+ server.listen(port, '127.0.0.1', () => resolve({
342
+ url: `http://127.0.0.1:${server.address().port}/?t=${token}`,
343
+ close: () => {
344
+ closeSession();
345
+ if (app) killGroup(app.child);
346
+ server.close();
347
+ },
348
+ }));
349
+ });
350
+ }
@@ -0,0 +1 @@
1
+ .lattice-loader{--ll-n: 3;--ll-cell: 6px;--ll-gap: 2px;--ll-font: 14px;--ll-color: currentColor;--ll-mark: #22c55e;--ll-idle: .15;--ll-glow: currentColor;--ll-mark-glow: #22c55e;--ll-peak: 1;--ll-cycle: 864ms;--ll-ease-out: cubic-bezier(.23, 1, .32, 1);--ll-ease-in-out: cubic-bezier(.77, 0, .175, 1);position:relative;display:inline-flex;align-items:center;gap:calc(var(--ll-font) * .625);font-family:inherit;font-size:var(--ll-font);line-height:1}.lattice-loader__grid{display:grid;flex:none}.lattice-loader__layer{grid-area:1 / 1;display:grid;grid-template-columns:repeat(var(--ll-n),var(--ll-cell));gap:var(--ll-gap)}.lattice-loader__cell{width:var(--ll-cell);height:var(--ll-cell);border-radius:max(1px,calc(var(--ll-cell) * .25));background:var(--ll-color);opacity:var(--ll-idle)}.lattice-loader[data-shape=round] .lattice-loader__cell{border-radius:50%}.lattice-loader[data-glow] .lattice-loader__run .lattice-loader__cell:not([data-hole]){box-shadow:0 0 calc(var(--ll-cell) * 1.2) calc(var(--ll-cell) * .12) var(--ll-glow)}.lattice-loader[data-glow] .lattice-loader__mark .lattice-loader__cell[data-on]{box-shadow:0 0 calc(var(--ll-cell) * 1.2) calc(var(--ll-cell) * .12) var(--ll-mark-glow)}.lattice-loader__run{transition:opacity .2s ease}.lattice-loader__run .lattice-loader__cell{animation:lattice-on var(--ll-cycle) var(--ll-ease-in-out) infinite}.lattice-loader__run .lattice-loader__cell[data-lit="45"]{animation-name:lattice-on-45}.lattice-loader__run .lattice-loader__cell[data-lit="35"]{animation-name:lattice-on-35}.lattice-loader__run .lattice-loader__cell[data-lit="25"]{animation-name:lattice-on-25}.lattice-loader__run .lattice-loader__cell[data-hole]{animation:none;opacity:calc(var(--ll-idle) * .47)}.lattice-loader__mark{opacity:0;transform:scale(.9);transform-origin:center;transition:opacity .16s var(--ll-ease-out),transform .16s var(--ll-ease-out)}.lattice-loader__mark .lattice-loader__cell{transition:opacity .2s ease,background-color .2s ease}.lattice-loader__mark .lattice-loader__cell[data-on]{background:var(--ll-mark);opacity:var(--ll-peak)}.lattice-loader:not([data-status=working]) .lattice-loader__run{opacity:0}.lattice-loader:not([data-status=working]) .lattice-loader__run .lattice-loader__cell{animation-play-state:paused}.lattice-loader:not([data-status=working]) .lattice-loader__mark{opacity:1;transform:none;transition:opacity .2s ease,transform .2s var(--ll-ease-out)}.lattice-loader__label{position:relative;display:inline-block;font-weight:500}.lattice-loader__text{position:absolute;top:0;left:0;white-space:nowrap;opacity:0;filter:blur(2px);transition:opacity .2s ease,filter .2s ease}.lattice-loader__text[data-active]{position:static;opacity:1;filter:blur(0)}.lattice-loader__timer{font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:calc(var(--ll-font) * .875);font-variant-numeric:tabular-nums;opacity:.6}.lattice-loader__sr{position:absolute;width:1px;height:1px;margin:-1px;padding:0;border:0;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap}@keyframes lattice-on{0%,to{opacity:var(--ll-idle)}18%,42%{opacity:var(--ll-peak)}62%{opacity:var(--ll-idle)}}@keyframes lattice-on-45{0%,to{opacity:var(--ll-idle)}13%,31%{opacity:var(--ll-peak)}45%{opacity:var(--ll-idle)}}@keyframes lattice-on-35{0%,to{opacity:var(--ll-idle)}10%,24%{opacity:var(--ll-peak)}35%{opacity:var(--ll-idle)}}@keyframes lattice-on-25{0%,to{opacity:var(--ll-idle)}7%,17%{opacity:var(--ll-peak)}25%{opacity:var(--ll-idle)}}@media(prefers-reduced-motion:reduce){.lattice-loader__run{--ll-peak: .7}.lattice-loader__run .lattice-loader__cell{animation-delay:0ms!important;animation-duration:1.4s!important}.lattice-loader .lattice-loader__mark{transform:none}.lattice-loader .lattice-loader__text{filter:none}}.prompt-bar{--pb-bg: #27272a;--pb-ink: #f5f5f5;--pb-menu: #323236;--pb-w: 400px;--pb-radius: 16px;--pb-press: .96;--pb-spark: #b39dff;--pb-ease-out: cubic-bezier(.23, 1, .32, 1);--pb-muted: color-mix(in srgb, var(--pb-ink) 55%, transparent);--pb-hover: color-mix(in srgb, var(--pb-ink) 8%, transparent);position:relative;width:min(var(--pb-w),100%);color:var(--pb-ink);font-family:inherit;font-size:14px;line-height:22px}.prompt-bar__menu{max-height:min(360px,55vh);overflow-y:auto;position:absolute;right:0;bottom:calc(100% + 8px);left:0;z-index:2;padding:4px;border-radius:12px;background:var(--pb-menu);box-shadow:0 10px 30px -10px #00000059,0 1px 2px #00000014;transform-origin:bottom center;animation:prompt-bar-pop .18s var(--pb-ease-out) both}.prompt-bar__menu[data-kind=model]{right:auto;width:200px;transform-origin:bottom left}@keyframes prompt-bar-pop{0%{opacity:0;transform:translateY(4px) scale(.98)}}.prompt-bar__glow{position:absolute;right:4px;left:4px;border-radius:8px;background:var(--pb-hover);opacity:0;pointer-events:none;transition:top .22s var(--pb-ease-out),height .22s var(--pb-ease-out),opacity .15s ease}.prompt-bar__row{position:relative;z-index:1;display:flex;align-items:center;gap:10px;width:100%;height:36px;padding:0 8px;border:0;border-radius:8px;background:transparent;color:inherit;font:inherit;text-align:left;cursor:pointer;outline:none;-webkit-tap-highlight-color:transparent}.prompt-bar__row-icon{display:inline-flex;flex:none;justify-content:center;width:20px;color:color-mix(in srgb,var(--pb-ink) 70%,transparent)}.prompt-bar__row-name{flex:none;font-size:13px;font-weight:500}.prompt-bar__row-desc{flex:1 1 auto;min-width:0;overflow:hidden;color:var(--pb-muted);font-size:12px;white-space:nowrap;text-overflow:ellipsis}.prompt-bar__row-tag{flex:none;margin-left:auto;color:var(--pb-muted);font-size:11px}.prompt-bar__row-check{display:inline-flex;flex:none;justify-content:center;width:16px;opacity:0}.prompt-bar__row-check[data-on]{opacity:1}.prompt-bar__empty{display:flex;align-items:center;height:36px;padding:0 8px;color:var(--pb-muted);font-size:12px}.prompt-bar__field{position:relative;isolation:isolate;display:flex;flex-direction:column;gap:8px;padding:12px;border-radius:var(--pb-radius);background:var(--pb-bg);cursor:text}.prompt-bar__field:before{content:"";position:absolute;top:0;right:0;bottom:0;left:0;z-index:-1;border-radius:inherit;background:radial-gradient(140% 120% at 0% 100%,color-mix(in srgb,var(--pb-spark) 26%,transparent),transparent 62%);opacity:0;pointer-events:none;transition:opacity .5s ease}.prompt-bar__field[data-max]:before{opacity:1}.prompt-bar__sparks{position:absolute;top:0;right:0;bottom:0;left:0;z-index:-1;width:100%;height:100%;border-radius:inherit;pointer-events:none}.prompt-bar__chips{display:flex;flex-wrap:wrap;gap:6px}.prompt-bar__chip{display:inline-flex;align-items:center;gap:6px;height:26px;padding:0 4px 0 8px;border-radius:8px;background:var(--pb-hover);font-size:12px;animation:prompt-bar-pop .2s var(--pb-ease-out) both}.prompt-bar__chip-name{max-width:144px;overflow:hidden;white-space:nowrap;text-overflow:ellipsis}.prompt-bar__chip-x{display:inline-grid;place-items:center;width:18px;height:18px;padding:0;border:0;border-radius:5px;background:transparent;color:inherit;opacity:.6;cursor:pointer;outline:none;transition:opacity .12s ease,background-color .12s ease}.prompt-bar__chip-x:hover{background:color-mix(in srgb,var(--pb-ink) 10%,transparent);opacity:1}.prompt-bar__input{display:block;width:100%;padding:0;border:0;background:transparent;color:inherit;font:inherit;font-size:14px;line-height:22px;resize:none;outline:none;overflow-wrap:anywhere}.prompt-bar__input::placeholder{color:color-mix(in srgb,var(--pb-ink) 45%,transparent)}@media(pointer:coarse){.prompt-bar__input{font-size:16px}}.prompt-bar__bar{display:flex;align-items:center;gap:4px}.prompt-bar__spacer{flex:1 1 auto}.prompt-bar__tool,.prompt-bar__pick,.prompt-bar__send{flex:none;border:0;font:inherit;cursor:pointer;outline:none;user-select:none;-webkit-user-select:none;-webkit-tap-highlight-color:transparent;touch-action:manipulation}.prompt-bar__tool{display:inline-grid;place-items:center;width:28px;height:28px;padding:0;border-radius:8px;background:transparent;color:color-mix(in srgb,var(--pb-ink) 60%,transparent);transition:background-color .15s ease,color .15s ease,transform .16s var(--pb-ease-out)}.prompt-bar__tool:active{transform:scale(.94)}.prompt-bar__pick{display:inline-flex;align-items:center;gap:4px;height:28px;padding:0 8px;border-radius:8px;background:transparent;color:color-mix(in srgb,var(--pb-ink) 70%,transparent);font-size:12px;font-weight:500;transition:background-color .15s ease,color .15s ease}.prompt-bar__tool[data-on],.prompt-bar__pick[data-on]{background:var(--pb-hover);color:var(--pb-ink)}@media(hover:hover)and (pointer:fine){.prompt-bar__tool:hover,.prompt-bar__pick:hover{background:var(--pb-hover);color:var(--pb-ink)}}.prompt-bar__pick[data-max]{color:var(--pb-spark)}.prompt-bar__menu[data-kind=effort]{right:auto;width:248px;padding:12px 14px 14px;transform-origin:bottom left}.prompt-bar__effort-head{display:flex;align-items:center;gap:8px;font-size:13px;line-height:18px}.prompt-bar__effort-title{color:var(--pb-muted)}.prompt-bar__effort-level{font-weight:500}.prompt-bar__effort-help{display:inline-flex;margin-left:auto;color:var(--pb-muted)}.prompt-bar__effort-ends{display:flex;justify-content:space-between;margin-top:12px;color:var(--pb-muted);font-size:12px;line-height:16px}.prompt-bar__effort-track{position:relative;height:22px;margin-top:8px;border-radius:11px;background:var(--pb-hover);cursor:pointer;outline:none;touch-action:none;user-select:none;-webkit-user-select:none}.prompt-bar__effort-fill{position:absolute;top:0;bottom:0;left:0;width:var(--pb-effort-fill);border-radius:11px;background:color-mix(in srgb,var(--pb-ink) 18%,transparent);transition:width .22s var(--pb-ease-out),background-color .3s ease}.prompt-bar__effort-dot{position:absolute;top:50%;width:4px;height:4px;margin:-2px 0 0 -2px;border-radius:50%;background:color-mix(in srgb,var(--pb-ink) 30%,transparent)}.prompt-bar__effort-thumb{position:absolute;top:-3px;left:var(--pb-effort-x);width:14px;height:28px;margin-left:-7px;border-radius:7px;background:var(--pb-ink);box-shadow:0 2px 6px #00000040;transition:left .22s var(--pb-ease-out),background-color .3s ease}.prompt-bar[data-max] .prompt-bar__effort-fill{background:color-mix(in srgb,var(--pb-spark) 35%,transparent)}.prompt-bar[data-max] .prompt-bar__effort-thumb{background:var(--pb-spark)}.prompt-bar__eq{display:flex;align-items:center;gap:2.5px;height:14px}.prompt-bar__eq i{display:block;width:2.5px;height:100%;border-radius:999px;background:currentColor;transform-origin:center;animation:prompt-bar-eq .9s ease-in-out infinite}.prompt-bar__eq i:nth-child(2){animation-delay:.15s}.prompt-bar__eq i:nth-child(3){animation-delay:.3s}@keyframes prompt-bar-eq{0%,to{transform:scaleY(.35)}50%{transform:scaleY(1)}}.prompt-bar__send{position:relative;display:inline-grid;place-items:center;width:28px;height:28px;padding:0;border-radius:8px;background:color-mix(in srgb,var(--pb-ink) 12%,var(--pb-bg));color:color-mix(in srgb,var(--pb-ink) 55%,var(--pb-bg));transition:background-color .2s ease,color .2s ease,transform .16s var(--pb-ease-out)}.prompt-bar__send:disabled{cursor:default}.prompt-bar__send[data-armed]{background:var(--pb-ink);color:var(--pb-bg)}.prompt-bar__send[data-pressed]{transform:scale(var(--pb-press))}.prompt-bar__glyph{display:block;width:16px;height:16px;transform-origin:50% 50%}@media(prefers-reduced-motion:reduce){.prompt-bar__menu,.prompt-bar__chip{animation:none}.prompt-bar__glow{transition:opacity .15s ease}.prompt-bar__effort-fill,.prompt-bar__effort-thumb{transition:background-color .3s ease}.prompt-bar__tool:active,.prompt-bar__send[data-pressed]{transform:none}}.rubber-segment{--rs-track: #27272a;--rs-thumb: #fafafa;--rs-ink: #fafafa;--rs-ink-active: #18181b;--rs-radius: 10px;--rs-inset: 3px;--rs-thumb-radius: 7px;--rs-h: 36px;--rs-font: 13px;--rs-pad: 14px;--rs-min: 44px;--rs-idle: .7;--rs-hover: .9;--rs-ease-out: cubic-bezier(.23, 1, .32, 1);position:relative;display:inline-grid;grid-auto-flow:column;grid-auto-columns:auto;vertical-align:middle;padding:var(--rs-inset);border-radius:var(--rs-radius);background:var(--rs-track);font-family:inherit;touch-action:pan-y;user-select:none;-webkit-user-select:none;-webkit-touch-callout:none;-webkit-tap-highlight-color:transparent}.rubber-segment[data-equal]{grid-auto-columns:minmax(0,1fr)}.rubber-segment[aria-disabled=true]{opacity:.5;pointer-events:none}.rubber-segment__item{display:inline-flex;align-items:center;justify-content:center;gap:6px;height:calc(var(--rs-h) - var(--rs-inset) * 2);min-width:var(--rs-min);margin:0;padding:0 var(--rs-pad);border:0;border-radius:var(--rs-thumb-radius);background:none;color:var(--rs-ink);opacity:var(--rs-idle);font:inherit;font-size:var(--rs-font);font-weight:500;line-height:1;white-space:nowrap;cursor:pointer;outline:none;transition:opacity .16s ease,transform .16s var(--rs-ease-out)}.rubber-segment__item[aria-checked=true]{cursor:default}.rubber-segment[data-draggable] .rubber-segment__item[aria-checked=true]{cursor:grab}.rubber-segment[data-held],.rubber-segment[data-held] .rubber-segment__item{cursor:grabbing}.rubber-segment__item[data-pressed]{transform:scale(.96)}.rubber-segment__item:focus-visible{outline:2px solid var(--rs-thumb);outline-offset:3px}@media(hover:hover)and (pointer:fine){.rubber-segment__item[aria-checked=false]:hover{opacity:var(--rs-hover)}}.rubber-segment__thumb{position:absolute;inset:var(--rs-inset);display:grid;grid-auto-flow:column;grid-auto-columns:auto;background:var(--rs-thumb);color:var(--rs-ink-active);pointer-events:none}.rubber-segment[data-equal] .rubber-segment__thumb{grid-auto-columns:minmax(0,1fr)}.rubber-segment__copy{color:inherit;opacity:1;cursor:default}@media(prefers-reduced-motion:reduce){.rubber-segment__item{transition:opacity .16s ease}}.status-mark{--sm-size: 20px;--sm-stroke: 2;--sm-color: currentColor;--sm-done: #22c55e;--sm-error: #ef4444;--sm-fill: .06;--sm-font: 14px;--sm-draw: .24s;--sm-strike-delay: .18s;--sm-muted: .55;--sm-check-delay: .12s;--sm-label-o: .65;--sm-ease-out: cubic-bezier(.23, 1, .32, 1);--sm-ease-in-out: cubic-bezier(.77, 0, .175, 1);position:relative;display:inline-flex;align-items:center;gap:calc(var(--sm-size) * .5);vertical-align:middle;line-height:1}.status-mark__glyph{flex:none;overflow:visible;color:var(--sm-color);transition:color .2s ease}.status-mark[data-status=done] .status-mark__glyph{color:var(--sm-done)}.status-mark[data-status=failed] .status-mark__glyph{color:var(--sm-error)}.status-mark__track{fill:currentColor;stroke:currentColor;stroke-width:var(--sm-stroke);fill-opacity:0;stroke-opacity:0;transition:fill-opacity .18s ease,stroke-opacity .2s ease}.status-mark[data-status=running] .status-mark__track{stroke-opacity:.2}.status-mark[data-status=done] .status-mark__track,.status-mark[data-status=failed] .status-mark__track{fill-opacity:var(--sm-fill)}.status-mark__ring{fill:none;stroke:currentColor;stroke-width:var(--sm-stroke);stroke-linecap:round;opacity:var(--sm-muted);transition:opacity .2s ease}.status-mark[data-status=running] .status-mark__ring,.status-mark[data-status=done] .status-mark__ring,.status-mark[data-status=failed] .status-mark__ring{opacity:1}.status-mark__check,.status-mark__cross{fill:none;stroke:currentColor;stroke-width:var(--sm-stroke);stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:1 2;stroke-dashoffset:1.05;opacity:0;transition:stroke-dashoffset .16s var(--sm-ease-out),opacity 0ms linear .16s}.status-mark[data-status=done] .status-mark__check,.status-mark[data-status=failed] .status-mark__cross,.status-mark[data-status=cancelled] .status-mark__cross{stroke-dashoffset:0;opacity:1;transition:stroke-dashoffset var(--sm-draw) var(--sm-ease-out) var(--sm-check-delay),opacity 0ms linear var(--sm-check-delay)}.status-mark__label{position:relative;color:var(--sm-color);font-size:var(--sm-font);line-height:1.25;opacity:var(--sm-label-o);transition:opacity .2s ease}.status-mark[data-status=running]{--sm-label-o: 1}.status-mark[data-status=done]{--sm-label-o: .6}.status-mark[data-status=failed]{--sm-label-o: 1}.status-mark[data-status=cancelled]{--sm-label-o: .55}.status-mark__strike{position:absolute;top:50%;right:0;left:0;height:max(1px,calc(var(--sm-font) / 14));background:currentColor;transform:scaleX(0);transform-origin:left center;translate:0 -50%;pointer-events:none;transition:transform .16s var(--sm-ease-out)}.status-mark[data-status=done][data-strike] .status-mark__strike{transform:scaleX(1);transition:transform .28s var(--sm-ease-out) var(--sm-strike-delay)}.status-mark__sr{position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap}@keyframes sm-breathe{50%{opacity:.45}}@media(prefers-reduced-motion:reduce){.status-mark[data-indeterminate] .status-mark__ring{animation:sm-breathe 1.4s var(--sm-ease-in-out) infinite}.status-mark__check,.status-mark__cross{stroke-dashoffset:0;transition:opacity .2s ease}.status-mark__strike{opacity:0;transform:scaleX(1)!important;transition:opacity .2s ease}.status-mark[data-status=done][data-strike] .status-mark__strike{opacity:1}}.thought-line{--tl-ease-out: cubic-bezier(.23, 1, .32, 1);--tl-ease-in-out: cubic-bezier(.77, 0, .175, 1);--tl-font: 16px;--tl-color: currentColor;--tl-glyph: currentColor;--tl-settle: .35s;--tl-blur: 2px;--tl-shimmer: 1.8s;--tl-done: .75;--tl-timer: .55;display:inline-flex;flex-direction:column;align-items:flex-start;color:var(--tl-color);font-family:inherit;font-size:var(--tl-font);font-weight:500;line-height:1.2}.thought-line__head{position:relative;display:inline-flex;align-items:center;gap:.3em;margin:0;padding:0;border:0;background:none;color:inherit;font:inherit;text-align:left;white-space:nowrap;outline:none;cursor:default}.thought-line__head[data-toggle]{cursor:pointer;-webkit-tap-highlight-color:transparent}.thought-line__glyph{display:inline-flex;flex:none;width:1.1em;height:1.1em;margin-right:.2em;color:var(--tl-glyph)}.thought-line__glyph svg{display:block;width:100%;height:100%}.thought-line__dot{width:.5em;height:.5em;margin:auto;border-radius:50%;background:currentColor}.thought-line__label{display:inline-grid}.thought-line__text{grid-area:1 / 1;width:max-content;opacity:0;filter:blur(var(--tl-blur));transition:opacity var(--tl-settle) var(--tl-ease-out),filter var(--tl-settle) var(--tl-ease-out)}.thought-line__text[data-active]{opacity:1;filter:blur(0)}.thought-line__text--done[data-active]{opacity:var(--tl-done)}.thought-line__breath{display:inline-block}.thought-line[data-working] .thought-line__breath[data-shimmer]{background:linear-gradient(100deg,color-mix(in srgb,var(--tl-color) 50%,transparent) 30%,var(--tl-color) 50%,color-mix(in srgb,var(--tl-color) 50%,transparent) 70%);background-size:250% 100%;background-position:125% 0;-webkit-background-clip:text;background-clip:text;color:transparent;-webkit-text-fill-color:transparent;animation:thought-line-shimmer var(--tl-shimmer) linear infinite}@keyframes thought-line-shimmer{to{background-position:-125% 0}}.thought-line__timer{font-variant-numeric:tabular-nums;opacity:var(--tl-timer);transition:opacity var(--tl-settle) ease,transform var(--tl-settle) var(--tl-ease-in-out)}.thought-line__timer[data-done]{opacity:var(--tl-done)}.thought-line__chevron{display:inline-flex;margin-left:.1em;opacity:0;transition:opacity .2s ease,transform .2s var(--tl-ease-out)}.thought-line__chevron[data-on]{opacity:.55}.thought-line[data-open] .thought-line__chevron{transform:rotate(180deg)}.thought-line__sr{position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap}.thought-line__trace{display:grid;grid-template-rows:0fr;width:0;min-width:100%;transition:grid-template-rows var(--tl-settle) var(--tl-ease-out)}.thought-line__trace[data-open]{grid-template-rows:1fr}.thought-line__fold{min-height:0;overflow-x:visible;overflow-y:clip}.thought-line__steps{display:flex;flex-direction:column;gap:.45em;width:max-content;padding:.6em 0 .2em 1.6em;font-size:.875em;font-weight:400;white-space:nowrap}.thought-line__step{display:flex;align-items:center;gap:.5em;opacity:1;transform:translateY(0);transition:opacity .2s var(--tl-ease-out),transform .2s var(--tl-ease-out)}@starting-style{.thought-line__step{opacity:0;transform:translateY(-4px)}}.thought-line__mark{display:inline-grid;flex:none;place-items:center;width:1em;height:1em;opacity:.55}.thought-line__pulse{display:block;width:.4em;height:.4em;border-radius:50%;background:currentColor;animation:thought-line-pulse 1.6s var(--tl-ease-in-out) infinite}@keyframes thought-line-pulse{0%,to{opacity:.55}50%{opacity:1}}.thought-line__step-text{transition:opacity var(--tl-settle) ease}.thought-line__step[data-done] .thought-line__step-text{opacity:.55}@media(prefers-reduced-motion:reduce){.thought-line__text{filter:none!important;transition:opacity var(--tl-settle) ease}.thought-line__timer{transition:opacity var(--tl-settle) ease}.thought-line__step{transform:none!important;transition:opacity .2s ease}.thought-line__pulse{animation-duration:2.4s}}:root{color-scheme:dark;--bg: #18181b;--panel: #232327;--line: #2f2f35;--ink: #f4f4f5;--muted: #a1a1aa;--accent: #b39dff;--danger: #f87171;--warn: #fbbf24}*{box-sizing:border-box}html,body,#root{height:100%;margin:0}body{background:var(--bg);color:var(--ink);font:14px/1.55 Inter,ui-sans-serif,system-ui,-apple-system,Segoe UI,sans-serif}.app{display:flex;flex-direction:column;height:100%;max-width:1280px;margin:0 auto;padding:0 clamp(16px,3vw,40px)}.app--empty{align-items:center;justify-content:center;color:var(--muted)}.app__header{display:flex;align-items:center;justify-content:space-between;gap:16px;flex-wrap:wrap;padding:16px 0;border-bottom:1px solid var(--line)}.app__title{font-size:18px;font-weight:700;letter-spacing:.02em;color:var(--accent)}.app__meta{color:var(--muted);font-size:12px}.app__controls{display:flex;align-items:center;gap:10px;flex-wrap:wrap}.app__context{color:var(--muted);font-size:12px;font-variant-numeric:tabular-nums}.app__context[data-warn]{color:var(--warn)}.app__new{font:inherit;font-size:13px;color:var(--ink);background:var(--panel);border:1px solid var(--line);border-radius:8px;padding:5px 10px;cursor:pointer}.app__new:disabled{opacity:.5;cursor:default}.app__turns{flex:1;overflow-y:auto;padding:20px 0;display:flex;flex-direction:column;gap:28px}.app__hint{color:var(--muted);max-width:560px;margin:15vh auto 0;text-align:center}.app__composer{display:flex;flex-direction:column;align-items:center;gap:8px;padding:12px 0 20px}.app__composer>div{width:100%}.app__attachments{display:flex;gap:8px;flex-wrap:wrap}.app__attachment{position:relative;padding:0;border:1px solid var(--line);border-radius:8px;background:none;cursor:pointer}.app__attachment img{display:block;height:64px;border-radius:8px}.app__attachment span{position:absolute;top:2px;right:4px;color:var(--ink);text-shadow:0 0 3px #000}.turn{display:flex;flex-direction:column;gap:10px}.turn__prompt{align-self:flex-end;max-width:85%;background:var(--panel);border:1px solid var(--line);border-radius:14px;padding:8px 14px;white-space:pre-wrap;overflow-wrap:anywhere}.turn__text{overflow-wrap:anywhere}.turn__text>:first-child{margin-top:0}.turn__text>:last-child{margin-bottom:0}.turn__text p,.turn__text ul,.turn__text ol{margin:0 0 8px}.turn__text a{color:var(--accent)}.turn__text code{font:12.5px ui-monospace,JetBrains Mono,monospace;background:var(--panel);border-radius:4px;padding:1px 4px}.turn__text pre{background:#111113;border:1px solid var(--line);border-radius:10px;padding:10px 12px;overflow:auto}.turn__text pre code{background:none;padding:0}.turn__text table{border-collapse:collapse;margin:0 0 8px}.turn__text th,.turn__text td{border:1px solid var(--line);padding:4px 8px;text-align:left}.turn__tool{display:flex;gap:10px;align-items:flex-start;font-size:13px}.turn__target{color:var(--muted);font-family:ui-monospace,JetBrains Mono,monospace;overflow-wrap:anywhere}.turn__detail{color:var(--muted);font-size:12px}.turn__detail--error{color:var(--danger)}.turn__log{margin:0;max-height:420px;overflow:auto;background:#111113;border:1px solid var(--line);border-radius:10px;padding:10px 12px;font:12px/1.5 ui-monospace,JetBrains Mono,monospace;white-space:pre-wrap}.turn__warning,.turn__error{border-radius:10px;padding:8px 12px;font-size:13px}.turn__warning{background:#fbbf2414;color:var(--warn)}.turn__error{background:#f8717114;color:var(--danger);white-space:pre-wrap}.turn__footer{display:flex;align-items:center;gap:14px;flex-wrap:wrap;color:var(--muted)}.turn__usage{font-size:12px}.turn__images{display:flex;gap:6px;flex-wrap:wrap;margin-bottom:6px}.turn__images img{max-height:120px;border-radius:8px}.ask{border:1px solid var(--line);background:var(--panel);border-radius:12px;padding:12px 14px;display:flex;flex-direction:column;gap:10px}.ask--done{padding:6px 12px;color:var(--muted);font-size:12px}.ask__title{font-weight:600}.ask__reason{color:var(--warn);font-size:12px}.ask__code{margin:0;max-height:300px;overflow:auto;background:#111113;border-radius:8px;padding:8px 10px;font:12px/1.5 ui-monospace,JetBrains Mono,monospace;white-space:pre-wrap}.ask__del{color:var(--danger)}.ask__add{color:#4ade80}.ask__plan{max-height:420px;overflow:auto}.ask__actions{display:flex;gap:8px;flex-wrap:wrap}.ask__actions button,.ask__option{font:inherit;color:var(--ink);background:#2c2c31;border:1px solid var(--line);border-radius:8px;padding:6px 12px;cursor:pointer}.ask__actions button:disabled{opacity:.5;cursor:default}.ask__actions .ask__primary{background:var(--ink);color:var(--bg);border-color:var(--ink)}.ask__question,.ask__options{display:flex;flex-direction:column;gap:6px}.ask__option{display:flex;flex-direction:column;align-items:flex-start;text-align:left}.ask__option span{color:var(--muted);font-size:12px}.ask__option[data-on]{border-color:var(--accent);background:#b39dff1f}.ask__other{font:inherit;color:var(--ink);background:#111113;border:1px solid var(--line);border-radius:8px;padding:6px 10px}