@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/LICENSE +21 -0
- package/README.md +137 -2
- package/djinn-completion.bash +9 -0
- package/djinn.sh +246 -0
- package/index.js +88 -0
- package/package.json +65 -4
- package/server.js +350 -0
- package/web/dist/assets/index-DRlYcuql.css +1 -0
- package/web/dist/assets/index-fLa0hqqj.js +112 -0
- package/web/dist/index.html +13 -0
- package/window.cjs +20 -0
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}
|