teapot-coding-agent 0.13.0 → 0.14.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.
- package/README.md +12 -3
- package/dist/agent/agent.js +12 -0
- package/dist/agent/llm.js +22 -0
- package/dist/agent/tools.js +30 -3
- package/dist/master.js +70 -1
- package/dist/model-meta.js +5 -1
- package/dist/server/api.js +143 -22
- package/package.json +6 -2
- package/public/assets/index-Bf0jTq07.css +1 -0
- package/public/assets/index-C8JLJ99u.js +30 -0
- package/public/assets/index-C8JLJ99u.js.map +1 -0
- package/public/index.html +19 -3
- package/public/assets/addon-fit-DIOBYJe3.js +0 -2
- package/public/assets/addon-fit-DIOBYJe3.js.map +0 -1
- package/public/assets/index-DC44teo7.js +0 -15
- package/public/assets/index-DC44teo7.js.map +0 -1
- package/public/assets/index-DN50nK9M.css +0 -1
- package/public/assets/xterm-C3BHN0de.js +0 -17
- package/public/assets/xterm-C3BHN0de.js.map +0 -1
package/README.md
CHANGED
|
@@ -73,7 +73,8 @@ Agents are matched to named OpenAI-compatible providers:
|
|
|
73
73
|
{
|
|
74
74
|
"providers": {
|
|
75
75
|
"openrouter": { "baseUrl": "https://openrouter.ai/api/v1", "apiKey": "sk-or-..." },
|
|
76
|
-
"
|
|
76
|
+
"orcarouter": { "baseUrl": "https://api.orcarouter.ai/v1", "apiKey": "sk-orca-...", "model": "orcarouter/auto" },
|
|
77
|
+
"local": { "baseUrl": "http://localhost:8080/v1", "apiKey": "llama.cpp", "model": "local" }
|
|
77
78
|
},
|
|
78
79
|
"defaultProvider": "openrouter",
|
|
79
80
|
"agents": [
|
|
@@ -85,7 +86,15 @@ Agents are matched to named OpenAI-compatible providers:
|
|
|
85
86
|
}
|
|
86
87
|
```
|
|
87
88
|
|
|
88
|
-
Per-agent inline `baseUrl`/`apiKey`/`model` override the provider entry.
|
|
89
|
+
Per-agent inline `baseUrl`/`apiKey`/`model` override the provider entry. The
|
|
90
|
+
first-run wizard and the settings modal ship quick-add presets for OpenRouter,
|
|
91
|
+
OrcaRouter, OpenAI and a local llama.cpp server.
|
|
92
|
+
|
|
93
|
+
**OpenRouter app attribution**: requests to `openrouter.ai` endpoints
|
|
94
|
+
automatically carry `HTTP-Referer` / `X-OpenRouter-Title` /
|
|
95
|
+
`X-OpenRouter-Categories: programming-app`, so teapot's usage shows up in
|
|
96
|
+
OpenRouter's public rankings & analytics — no config needed. Other providers
|
|
97
|
+
receive no extra headers.
|
|
89
98
|
|
|
90
99
|
## Architecture
|
|
91
100
|
|
|
@@ -114,7 +123,7 @@ master (Hono server, src/master.ts + src/server/api.ts)
|
|
|
114
123
|
mid-task across master restarts instead of starting blank. Restores replay
|
|
115
124
|
byte-identical request payloads, keeping provider prefix caches warm.
|
|
116
125
|
- **LLM** (`src/agent/llm.ts`): official `openai` npm client against any
|
|
117
|
-
OpenAI-compatible endpoint (OpenRouter, vLLM,
|
|
126
|
+
OpenAI-compatible endpoint (OpenRouter, OrcaRouter, vLLM, llama.cpp server...). Model is config,
|
|
118
127
|
never hard-coded.
|
|
119
128
|
- **Tools** (`src/agent/tools.ts`): provider-agnostic JSON-schema function specs —
|
|
120
129
|
file work via `read_file` (numbered lines, grep-style `pattern` mode, negative
|
package/dist/agent/agent.js
CHANGED
|
@@ -476,6 +476,7 @@ export class Agent {
|
|
|
476
476
|
todo: this.todo.slice(0, 32_000), // match set_todo's cap — no silent truncation
|
|
477
477
|
parent: this.opts.parent,
|
|
478
478
|
awaiting: this.awaitingUser,
|
|
479
|
+
autoContinue: this.opts.autoContinue,
|
|
479
480
|
};
|
|
480
481
|
}
|
|
481
482
|
/**
|
|
@@ -723,6 +724,17 @@ export class Agent {
|
|
|
723
724
|
return finished;
|
|
724
725
|
if (call.function.name === "finish") {
|
|
725
726
|
await this.handleFinish(call.function.arguments);
|
|
727
|
+
// surface still-running children so the operator knows work may
|
|
728
|
+
// continue after this agent goes idle
|
|
729
|
+
try {
|
|
730
|
+
const running = (this.toolCtx.subAgents?.list?.() ?? []).filter((k) => k.status === "running");
|
|
731
|
+
if (running.length)
|
|
732
|
+
await this.log.append("system_note", this.currentSession, this.currentBranch, {
|
|
733
|
+
event: "subs-still-running",
|
|
734
|
+
detail: running.map((k) => `${k.id} (${k.status})`).join(", "),
|
|
735
|
+
});
|
|
736
|
+
}
|
|
737
|
+
catch { /* best-effort notice */ }
|
|
726
738
|
// answer the tool_call so a follow-up round stays API-valid
|
|
727
739
|
await this.answerMeta(call, this.goal.status === "done" ? "(goal complete)" : "(round ended)");
|
|
728
740
|
finished = true;
|
package/dist/agent/llm.js
CHANGED
|
@@ -7,6 +7,27 @@
|
|
|
7
7
|
*/
|
|
8
8
|
import OpenAI from "openai";
|
|
9
9
|
const clients = new WeakMap();
|
|
10
|
+
/**
|
|
11
|
+
* OpenRouter app attribution (https://openrouter.ai/docs/app-attribution):
|
|
12
|
+
* `HTTP-Referer` is the required identifier that puts teapot's usage on the
|
|
13
|
+
* public rankings/analytics; the title names it; `programming-app` files it
|
|
14
|
+
* under coding tools. Empty (harmless no-op) for every other provider.
|
|
15
|
+
*/
|
|
16
|
+
export function providerHeaders(baseUrl) {
|
|
17
|
+
try {
|
|
18
|
+
const host = new URL(baseUrl).host;
|
|
19
|
+
if (!/(^|\.)openrouter\.ai$/i.test(host))
|
|
20
|
+
return {};
|
|
21
|
+
return {
|
|
22
|
+
"HTTP-Referer": "https://github.com/akku1139/teapot",
|
|
23
|
+
"X-OpenRouter-Title": "teapot",
|
|
24
|
+
"X-OpenRouter-Categories": "programming-app",
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
return {}; // unparseable baseUrl — never block a request over metadata
|
|
29
|
+
}
|
|
30
|
+
}
|
|
10
31
|
function client(cfg) {
|
|
11
32
|
let c = clients.get(cfg);
|
|
12
33
|
if (!c) {
|
|
@@ -15,6 +36,7 @@ function client(cfg) {
|
|
|
15
36
|
apiKey: cfg.apiKey,
|
|
16
37
|
timeout: cfg.timeoutMs ?? 120_000,
|
|
17
38
|
maxRetries: 4, // SDK handles backoff for 429/5xx/network errors
|
|
39
|
+
defaultHeaders: providerHeaders(cfg.baseUrl),
|
|
18
40
|
});
|
|
19
41
|
clients.set(cfg, c);
|
|
20
42
|
}
|
package/dist/agent/tools.js
CHANGED
|
@@ -16,8 +16,8 @@ function clip(s, max) {
|
|
|
16
16
|
async function readText(p) {
|
|
17
17
|
return fs.readFile(p, "utf8");
|
|
18
18
|
}
|
|
19
|
-
/** Resolve a path inside the workspace; reject escapes. */
|
|
20
|
-
function safeJoin(cwd, p) {
|
|
19
|
+
/** Resolve a path inside the workspace; reject escapes (incl. symlink targets). */
|
|
20
|
+
export function safeJoin(cwd, p) {
|
|
21
21
|
const abs = path.resolve(cwd, p);
|
|
22
22
|
const rel = path.relative(cwd, abs);
|
|
23
23
|
if (rel.startsWith("..") || path.isAbsolute(rel)) {
|
|
@@ -741,7 +741,7 @@ export const TOOLS = [
|
|
|
741
741
|
context,
|
|
742
742
|
name: str(args.name).trim() || undefined,
|
|
743
743
|
});
|
|
744
|
-
return { ok: true, result: `spawned sub-agent ${r.id} —
|
|
744
|
+
return { ok: true, result: `spawned sub-agent ${r.id} — park with wait_children() until it reports (never bash sleep), steer via message_agent, halt via stop_children` };
|
|
745
745
|
}
|
|
746
746
|
catch (e) {
|
|
747
747
|
return { ok: false, result: `spawn failed: ${e.message}` };
|
|
@@ -788,6 +788,33 @@ export const TOOLS = [
|
|
|
788
788
|
};
|
|
789
789
|
},
|
|
790
790
|
},
|
|
791
|
+
{
|
|
792
|
+
name: "wait_children",
|
|
793
|
+
description: "Park until at least one sub-agent settles (finished, errored, stopped, or asks you a question) — " +
|
|
794
|
+
"or until the timeout lapses. Costs zero tokens while parked: prefer this over bash sleep when " +
|
|
795
|
+
"waiting on spawned work. The settling child's report is delivered to you afterwards.",
|
|
796
|
+
parameters: {
|
|
797
|
+
type: "object",
|
|
798
|
+
properties: {
|
|
799
|
+
ids: { type: "array", items: { type: "string" }, description: "sub-agent ids; omit for all" },
|
|
800
|
+
timeout_ms: { type: "number", description: "default 300000 (5 min), max 3600000" },
|
|
801
|
+
},
|
|
802
|
+
},
|
|
803
|
+
async run(args, ctx) {
|
|
804
|
+
const sa = ctx.subAgents;
|
|
805
|
+
if (!sa)
|
|
806
|
+
return { ok: false, result: "sub-agents are not available here" };
|
|
807
|
+
const ids = Array.isArray(args.ids) ? args.ids.map(String) : undefined;
|
|
808
|
+
const ms = Math.min(Math.max(num(args.timeout_ms, 300_000), 1_000), 3_600_000);
|
|
809
|
+
try {
|
|
810
|
+
const r = await sa.wait(ids, ms);
|
|
811
|
+
return { ok: true, result: r.note };
|
|
812
|
+
}
|
|
813
|
+
catch (e) {
|
|
814
|
+
return { ok: false, result: `wait failed: ${e.message}` };
|
|
815
|
+
}
|
|
816
|
+
},
|
|
817
|
+
},
|
|
791
818
|
{
|
|
792
819
|
name: "message_agent",
|
|
793
820
|
description: "Send a message to a specific sub-agent (steer it mid-flight or answer its ask_user question).",
|
package/dist/master.js
CHANGED
|
@@ -176,6 +176,7 @@ function printAgentEvent(e) {
|
|
|
176
176
|
export class Master {
|
|
177
177
|
agents = new Map();
|
|
178
178
|
tasks = [];
|
|
179
|
+
childWaiters = new Map();
|
|
179
180
|
startedAt = Date.now();
|
|
180
181
|
config;
|
|
181
182
|
configPath;
|
|
@@ -357,7 +358,7 @@ export class Master {
|
|
|
357
358
|
sessionDir,
|
|
358
359
|
progressIntervalMs: this.config.progressIntervalMs,
|
|
359
360
|
...(this.config.progressMinChars ? { progressMinChars: this.config.progressMinChars } : {}),
|
|
360
|
-
autoContinue: true,
|
|
361
|
+
autoContinue: ac.autoContinue ?? true,
|
|
361
362
|
...(this.config.contextTokenBudget ? { contextTokenBudget: this.config.contextTokenBudget } : {}),
|
|
362
363
|
...(ac.contextWindowTokens || this.config.contextWindowTokens || inferredWindow
|
|
363
364
|
? {
|
|
@@ -395,6 +396,7 @@ export class Master {
|
|
|
395
396
|
})),
|
|
396
397
|
stop: (ids) => this.stopChildrenFor(ac.id, ids),
|
|
397
398
|
message: (id, text) => this.messageChild(ac.id, id, text),
|
|
399
|
+
wait: (ids, ms) => this.waitChildren(ac.id, ids, ms),
|
|
398
400
|
};
|
|
399
401
|
agent.toolCtx.subAgents = hooks;
|
|
400
402
|
}
|
|
@@ -517,6 +519,69 @@ export class Master {
|
|
|
517
519
|
return { stopped };
|
|
518
520
|
}
|
|
519
521
|
/** Deliver a parent's message into a child's mailbox (and wake it). */
|
|
522
|
+
/**
|
|
523
|
+
* Event-driven parking for a parent waiting on sub-agents: resolves as soon
|
|
524
|
+
* as ANY listed child settles (finish/error/stop/waiting) — zero API cost
|
|
525
|
+
* while parked. Falls back to a timeout so waits can never hang forever.
|
|
526
|
+
*/
|
|
527
|
+
waitChildren(parentId, ids, timeoutMs, signal) {
|
|
528
|
+
const targets = (ids && ids.length
|
|
529
|
+
? ids
|
|
530
|
+
: this.childrenOf(parentId).map((c) => c.id)).filter((id) => this.agents.has(id));
|
|
531
|
+
if (targets.length === 0)
|
|
532
|
+
return Promise.resolve({ note: "no live sub-agents to wait for" });
|
|
533
|
+
const active = () => targets.filter((id) => {
|
|
534
|
+
const s = this.agents.get(id)?.status;
|
|
535
|
+
return s === "running" || s === "waiting";
|
|
536
|
+
});
|
|
537
|
+
const first = active();
|
|
538
|
+
if (first.length === 0)
|
|
539
|
+
return Promise.resolve({ note: `all sub-agents already settled: ${targets.join(", ")}` });
|
|
540
|
+
return new Promise((resolve) => {
|
|
541
|
+
let done = false;
|
|
542
|
+
const finish = (note) => {
|
|
543
|
+
if (done)
|
|
544
|
+
return;
|
|
545
|
+
done = true;
|
|
546
|
+
clearTimeout(timer);
|
|
547
|
+
set.delete(wake);
|
|
548
|
+
signal?.removeEventListener("abort", onAbort);
|
|
549
|
+
resolve({ note });
|
|
550
|
+
};
|
|
551
|
+
const onAbort = () => finish("aborted while waiting");
|
|
552
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
553
|
+
const timer = setTimeout(() => {
|
|
554
|
+
const left = active();
|
|
555
|
+
finish(`timeout after ${Math.round(timeoutMs / 1000)}s — still running: ${left.join(", ") || "(none)"}`);
|
|
556
|
+
}, timeoutMs);
|
|
557
|
+
const wake = (_note) => {
|
|
558
|
+
const left = active();
|
|
559
|
+
if (left.length > 0)
|
|
560
|
+
return; // something is still working — keep parked
|
|
561
|
+
clearTimeout(timer);
|
|
562
|
+
finish(`all sub-agents settled: ${targets.join(", ")}`);
|
|
563
|
+
};
|
|
564
|
+
const set = this.childWaiters.get(parentId) ?? new Set();
|
|
565
|
+
if (!this.childWaiters.has(parentId))
|
|
566
|
+
this.childWaiters.set(parentId, set);
|
|
567
|
+
set.add(wake);
|
|
568
|
+
// re-check immediately: children may have settled between snapshot & register
|
|
569
|
+
if (active().length === 0)
|
|
570
|
+
wake();
|
|
571
|
+
});
|
|
572
|
+
}
|
|
573
|
+
/** wake every waiter of a parent (child lifecycle event happened) */
|
|
574
|
+
wakeParentWaiters(parentId) {
|
|
575
|
+
const set = this.childWaiters.get(parentId);
|
|
576
|
+
if (!set)
|
|
577
|
+
return;
|
|
578
|
+
for (const fn of set) {
|
|
579
|
+
try {
|
|
580
|
+
fn("child event");
|
|
581
|
+
}
|
|
582
|
+
catch { /* ignore */ }
|
|
583
|
+
}
|
|
584
|
+
}
|
|
520
585
|
async messageChild(parentId, childId, text) {
|
|
521
586
|
const cfg = this.config.agents.find((a) => a.id === childId);
|
|
522
587
|
if (!cfg || cfg.parent !== parentId)
|
|
@@ -536,6 +601,10 @@ export class Master {
|
|
|
536
601
|
const parentId = ac.parent;
|
|
537
602
|
if (!parentId)
|
|
538
603
|
return;
|
|
604
|
+
// any child lifecycle movement wakes a parked wait_children (the waiter
|
|
605
|
+
// re-checks whether its targets are all settled)
|
|
606
|
+
if (parentId)
|
|
607
|
+
this.wakeParentWaiters(parentId);
|
|
539
608
|
const parent = this.agents.get(parentId);
|
|
540
609
|
if (!parent)
|
|
541
610
|
return;
|
package/dist/model-meta.js
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
* from the real window instead of the safe default.
|
|
5
5
|
*/
|
|
6
6
|
import fs from "node:fs";
|
|
7
|
+
import { providerHeaders } from "./agent/llm.js";
|
|
7
8
|
const cache = new Map();
|
|
8
9
|
const TTL = 10 * 60_000;
|
|
9
10
|
/** Fetch GET /models from an OpenAI-compatible endpoint (cached ~10 min). */
|
|
@@ -14,7 +15,10 @@ export async function fetchModelList(baseUrl, apiKey) {
|
|
|
14
15
|
return hit.list;
|
|
15
16
|
try {
|
|
16
17
|
const res = await fetch(`${root}/models`, {
|
|
17
|
-
headers:
|
|
18
|
+
headers: {
|
|
19
|
+
...(apiKey ? { authorization: `Bearer ${apiKey}` } : {}),
|
|
20
|
+
...providerHeaders(baseUrl), // OpenRouter app attribution
|
|
21
|
+
},
|
|
18
22
|
signal: AbortSignal.timeout(15_000),
|
|
19
23
|
});
|
|
20
24
|
if (!res.ok)
|
package/dist/server/api.js
CHANGED
|
@@ -12,6 +12,36 @@ import { fileURLToPath } from "node:url";
|
|
|
12
12
|
import { parseSchedule } from "../scheduler/cron.js";
|
|
13
13
|
import { SUB_PERSONAS } from "../master.js";
|
|
14
14
|
import { ConfigPatchSchema, formatZodError } from "../config-schema.js";
|
|
15
|
+
import { providerHeaders } from "../agent/llm.js";
|
|
16
|
+
import { safeJoin } from "../agent/tools.js";
|
|
17
|
+
/** GET <baseUrl>/models on any OpenAI-compatible endpoint (id + ctx window + pricing). */
|
|
18
|
+
async function listModels(baseUrl, apiKey) {
|
|
19
|
+
const res = await fetch(`${baseUrl.replace(/\/+$/, "")}/models`, {
|
|
20
|
+
headers: {
|
|
21
|
+
...(apiKey ? { authorization: `Bearer ${apiKey}` } : {}),
|
|
22
|
+
...providerHeaders(baseUrl), // OpenRouter app attribution
|
|
23
|
+
},
|
|
24
|
+
signal: AbortSignal.timeout(15_000),
|
|
25
|
+
});
|
|
26
|
+
if (!res.ok)
|
|
27
|
+
throw new Error(`upstream ${res.status}`);
|
|
28
|
+
// OpenRouter-style entries carry context_length + per-token pricing —
|
|
29
|
+
// surface them so the UI can show what each model offers
|
|
30
|
+
const j = (await res.json());
|
|
31
|
+
return (j.data ?? [])
|
|
32
|
+
.map((m) => ({
|
|
33
|
+
id: typeof m.id === "string" ? m.id : "",
|
|
34
|
+
contextLength: typeof m.context_length === "number" ? m.context_length : undefined,
|
|
35
|
+
pricing: m.pricing && (m.pricing.prompt !== undefined || m.pricing.completion !== undefined)
|
|
36
|
+
? {
|
|
37
|
+
prompt: Number(m.pricing.prompt ?? 0),
|
|
38
|
+
completion: Number(m.pricing.completion ?? 0),
|
|
39
|
+
}
|
|
40
|
+
: undefined,
|
|
41
|
+
}))
|
|
42
|
+
.filter((m) => m.id)
|
|
43
|
+
.sort((a, b) => a.id.localeCompare(b.id));
|
|
44
|
+
}
|
|
15
45
|
import path from "node:path";
|
|
16
46
|
import { bus } from "../bus.js";
|
|
17
47
|
import { readEvents } from "../log/events.js";
|
|
@@ -217,6 +247,78 @@ export function buildApp(master) {
|
|
|
217
247
|
return c.json({ error: "cannot read" }, 400);
|
|
218
248
|
}
|
|
219
249
|
});
|
|
250
|
+
// ---- workspace file tree (read-only, powers the 🗂 files panel) ----
|
|
251
|
+
app.get("/api/agents/:id/tree", async (c) => {
|
|
252
|
+
const a = master.agents.get(c.req.param("id"));
|
|
253
|
+
if (!a)
|
|
254
|
+
return c.json({ error: "not found" }, 404);
|
|
255
|
+
const rel = c.req.query("path") || ".";
|
|
256
|
+
let abs;
|
|
257
|
+
try {
|
|
258
|
+
abs = safeJoin(a.workspace, rel);
|
|
259
|
+
}
|
|
260
|
+
catch (err) {
|
|
261
|
+
return c.json({ error: err.message }, 400);
|
|
262
|
+
}
|
|
263
|
+
let dirents;
|
|
264
|
+
try {
|
|
265
|
+
dirents = await fs.readdir(abs, { withFileTypes: true });
|
|
266
|
+
}
|
|
267
|
+
catch {
|
|
268
|
+
return c.json({ error: "cannot read directory" }, 400);
|
|
269
|
+
}
|
|
270
|
+
const entries = [];
|
|
271
|
+
for (const d of dirents) {
|
|
272
|
+
if (d.name.startsWith("."))
|
|
273
|
+
continue; // hidden files stay out of the tree
|
|
274
|
+
const isDir = d.isDirectory();
|
|
275
|
+
let size;
|
|
276
|
+
if (!isDir) {
|
|
277
|
+
try {
|
|
278
|
+
size = (await fs.stat(path.join(abs, d.name))).size;
|
|
279
|
+
}
|
|
280
|
+
catch {
|
|
281
|
+
/* vanished mid-scan — fine */
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
entries.push({ name: d.name, dir: isDir, ...(size !== undefined ? { size } : {}) });
|
|
285
|
+
}
|
|
286
|
+
entries.sort((x, y) => (x.dir !== y.dir ? (x.dir ? -1 : 1) : x.name.localeCompare(y.name)));
|
|
287
|
+
return c.json({
|
|
288
|
+
path: rel === "." ? "" : rel,
|
|
289
|
+
workspace: a.workspace,
|
|
290
|
+
entries,
|
|
291
|
+
});
|
|
292
|
+
});
|
|
293
|
+
// small text-file preview for the tree (binary-safe guard, hard cap)
|
|
294
|
+
app.get("/api/agents/:id/file", async (c) => {
|
|
295
|
+
const a = master.agents.get(c.req.param("id"));
|
|
296
|
+
if (!a)
|
|
297
|
+
return c.json({ error: "not found" }, 404);
|
|
298
|
+
const rel = c.req.query("path") ?? "";
|
|
299
|
+
if (!rel.trim())
|
|
300
|
+
return c.json({ error: "path required" }, 400);
|
|
301
|
+
let abs;
|
|
302
|
+
try {
|
|
303
|
+
abs = safeJoin(a.workspace, rel);
|
|
304
|
+
}
|
|
305
|
+
catch (err) {
|
|
306
|
+
return c.json({ error: err.message }, 400);
|
|
307
|
+
}
|
|
308
|
+
try {
|
|
309
|
+
const buf = await fs.readFile(abs);
|
|
310
|
+
if (buf.subarray(0, 8192).includes(0))
|
|
311
|
+
return c.json({ path: rel, binary: true, content: "" });
|
|
312
|
+
return c.json({
|
|
313
|
+
path: rel,
|
|
314
|
+
truncated: buf.length > 100_000,
|
|
315
|
+
content: buf.subarray(0, 100_000).toString("utf8"),
|
|
316
|
+
});
|
|
317
|
+
}
|
|
318
|
+
catch (err) {
|
|
319
|
+
return c.json({ error: err.message }, 400);
|
|
320
|
+
}
|
|
321
|
+
});
|
|
220
322
|
// ---- config (view/edit from the web UI) ----
|
|
221
323
|
app.get("/api/config", (c) => {
|
|
222
324
|
const mask = (p) => Object.fromEntries(Object.entries(p ?? {}).map(([k, v]) => [
|
|
@@ -325,29 +427,25 @@ export function buildApp(master) {
|
|
|
325
427
|
if (!prov?.baseUrl)
|
|
326
428
|
return c.json({ error: `unknown provider: ${provName}` }, 400);
|
|
327
429
|
try {
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
430
|
+
return c.json({ provider: provName, models: await listModels(prov.baseUrl, prov.apiKey) });
|
|
431
|
+
}
|
|
432
|
+
catch (err) {
|
|
433
|
+
return c.json({ error: `model list failed: ${err.message}` }, 502);
|
|
434
|
+
}
|
|
435
|
+
});
|
|
436
|
+
// Model discovery DURING first-run setup — the config file doesn't exist
|
|
437
|
+
// yet, so there are no named providers to ask for. Takes the raw endpoint
|
|
438
|
+
// + key the operator is typing in the wizard and proxies GET /models.
|
|
439
|
+
app.get("/api/setup/models", async (c) => {
|
|
440
|
+
if (master.configFileExists)
|
|
441
|
+
return c.json({ error: "setup already completed" }, 409);
|
|
442
|
+
const baseUrl = c.req.query("baseUrl") ?? "";
|
|
443
|
+
if (!/^https?:\/\//.test(baseUrl))
|
|
444
|
+
return c.json({ error: "valid baseUrl required" }, 400);
|
|
445
|
+
try {
|
|
446
|
+
return c.json({
|
|
447
|
+
models: await listModels(baseUrl, c.req.query("apiKey") || undefined),
|
|
331
448
|
});
|
|
332
|
-
if (!res.ok)
|
|
333
|
-
return c.json({ error: `upstream ${res.status}` }, 502);
|
|
334
|
-
// OpenRouter-style entries carry context_length + per-token pricing —
|
|
335
|
-
// surface them so the model switcher can show what each model offers
|
|
336
|
-
const j = (await res.json());
|
|
337
|
-
const models = (j.data ?? [])
|
|
338
|
-
.map((m) => ({
|
|
339
|
-
id: typeof m.id === "string" ? m.id : "",
|
|
340
|
-
contextLength: typeof m.context_length === "number" ? m.context_length : undefined,
|
|
341
|
-
pricing: m.pricing && (m.pricing.prompt !== undefined || m.pricing.completion !== undefined)
|
|
342
|
-
? {
|
|
343
|
-
prompt: Number(m.pricing.prompt ?? 0),
|
|
344
|
-
completion: Number(m.pricing.completion ?? 0),
|
|
345
|
-
}
|
|
346
|
-
: undefined,
|
|
347
|
-
}))
|
|
348
|
-
.filter((m) => m.id)
|
|
349
|
-
.sort((a, b) => a.id.localeCompare(b.id));
|
|
350
|
-
return c.json({ provider: provName, models });
|
|
351
449
|
}
|
|
352
450
|
catch (err) {
|
|
353
451
|
return c.json({ error: `model list failed: ${err.message}` }, 502);
|
|
@@ -436,6 +534,22 @@ export function buildApp(master) {
|
|
|
436
534
|
return c.json({ error: err.message }, 400);
|
|
437
535
|
}
|
|
438
536
|
});
|
|
537
|
+
// per-agent auto-continue toggle (loops toward an active goal)
|
|
538
|
+
app.post("/api/agents/:id/auto-continue", async (c) => {
|
|
539
|
+
const a = master.agents.get(c.req.param("id"));
|
|
540
|
+
if (!a)
|
|
541
|
+
return c.json({ error: "not found" }, 404);
|
|
542
|
+
const body = await c.req.json().catch(() => null);
|
|
543
|
+
if (!body || typeof body.value !== "boolean")
|
|
544
|
+
return c.json({ error: "boolean value required" }, 400);
|
|
545
|
+
const ac = master.config.agents.find((x) => x.id === c.req.param("id"));
|
|
546
|
+
if (ac)
|
|
547
|
+
ac.autoContinue = body.value;
|
|
548
|
+
a.opts.autoContinue = body.value;
|
|
549
|
+
master.saveConfig();
|
|
550
|
+
bus.emit("update", { kind: "agent-update", agentId: c.req.param("id") });
|
|
551
|
+
return c.json({ ok: true, value: body.value });
|
|
552
|
+
});
|
|
439
553
|
// default sub-agent personas for @mentions and spawn_agent
|
|
440
554
|
app.get("/api/personas", (c) => c.json({
|
|
441
555
|
personas: Object.entries(SUB_PERSONAS).map(([key, p]) => ({ key, label: p.label, directive: p.directive })),
|
|
@@ -529,6 +643,13 @@ export function buildApp(master) {
|
|
|
529
643
|
events = events.filter((e) => e.branch === branch);
|
|
530
644
|
if (session)
|
|
531
645
|
events = events.filter((e) => e.session === session);
|
|
646
|
+
// cursor pagination for older pages: everything strictly BEFORE this id
|
|
647
|
+
const before = c.req.query("before");
|
|
648
|
+
if (before) {
|
|
649
|
+
const idx = events.findIndex((e) => e.id === before);
|
|
650
|
+
events = idx === -1 ? [] : events.slice(0, idx);
|
|
651
|
+
return c.json({ events: events.slice(-limit), total: events.length });
|
|
652
|
+
}
|
|
532
653
|
return c.json({ events: events.slice(-limit), total: events.length });
|
|
533
654
|
});
|
|
534
655
|
app.get("/api/agents/:id/branches", async (c) => {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "teapot-coding-agent",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.14.0",
|
|
4
4
|
"description": "A lightweight, always-on multi-agent harness for AI coding agents",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "AGPL-3.0-or-later",
|
|
@@ -30,9 +30,12 @@
|
|
|
30
30
|
"README.md"
|
|
31
31
|
],
|
|
32
32
|
"scripts": {
|
|
33
|
-
"build": "tsc -p tsconfig.json && vite build",
|
|
33
|
+
"build": "tsc -p tsconfig.json && tsc -p tsconfig.frontend.json && vite build",
|
|
34
34
|
"build-server": "tsc -p tsconfig.json",
|
|
35
35
|
"build-web": "vite build",
|
|
36
|
+
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
37
|
+
"typecheck-web": "tsc -p tsconfig.frontend.json",
|
|
38
|
+
"lint": "oxlint src frontend test scripts",
|
|
36
39
|
"start": "node dist/index.js",
|
|
37
40
|
"dev": "node src/index.ts",
|
|
38
41
|
"dev-web": "vite",
|
|
@@ -52,6 +55,7 @@
|
|
|
52
55
|
"@types/ws": "^8.18.1",
|
|
53
56
|
"@xterm/addon-fit": "^0.11.0",
|
|
54
57
|
"@xterm/xterm": "^6.0.0",
|
|
58
|
+
"oxlint": "^1.79.0",
|
|
55
59
|
"solid-js": "^1.9.15",
|
|
56
60
|
"typescript": "^5.8.0",
|
|
57
61
|
"vite": "^8.2.2",
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
:root{--bg-darkest:#1a1c22;--bg-dark:#232630;--bg-mid:#2b2e39;--bg-light:#343845;--fg:#dcdee4;--dim:#9298a5;--line:#1d2027;--acc:#5865f2;--ok:#3ba55d;--warn:#faa81a;--err:#ed4245;--tool:#3ba0c9;--term-bg:#0d0e12;--term-fg:#dcdee4;font-family:gg sans,ui-sans-serif,system-ui,-apple-system,Segoe UI,sans-serif}[data-theme=midnight]{--bg-darkest:#0d0f18;--bg-dark:#12141f;--bg-mid:#1b1f31;--bg-light:#252a42;--fg:#dfe2f2;--dim:#8a90ad;--line:#0a0c14;--acc:#8b7cf7;--ok:#54c08a;--warn:#e5b567;--err:#ef6a6a;--tool:#6aa9e9;--term-bg:#0a0c13;--term-fg:#dfe2f2}[data-theme=tea]{--bg-darkest:#241c16;--bg-dark:#2e241c;--bg-mid:#3b2f24;--bg-light:#493b2d;--fg:#f0e4d2;--dim:#b39d82;--line:#1b140f;--acc:#d98e32;--ok:#7fa653;--warn:#e0b058;--err:#cf5b47;--tool:#c9975f;--term-bg:#1d1712;--term-fg:#ecdcc3}[data-theme=coffee]{--bg-darkest:#191410;--bg-dark:#211a14;--bg-mid:#30271e;--bg-light:#403527;--fg:#ede3d0;--dim:#a89478;--line:#120d07;--acc:#c68a4b;--ok:#8aa15c;--warn:#d9a441;--err:#c25450;--tool:#a67c52;--term-bg:#15100c;--term-fg:#e8dcc5}[data-theme=matcha]{--bg-darkest:#1b2217;--bg-dark:#232b1e;--bg-mid:#2f3a28;--bg-light:#3c4a33;--fg:#e8eeda;--dim:#9aab84;--line:#131a10;--acc:#8db64e;--ok:#6fae4e;--warn:#d8b13c;--err:#cd5c50;--tool:#5da88f;--term-bg:#151b11;--term-fg:#dfe8cb}[data-theme=matrix]{--bg-darkest:#060a06;--bg-dark:#0a0f0a;--bg-mid:#122116;--bg-light:#18301d;--fg:#c9f2cf;--dim:#6fa075;--line:#030603;--acc:#2fd558;--ok:#39c95f;--warn:#b8d977;--err:#ff6b6b;--tool:#57c99b;--term-bg:#030503;--term-fg:#3f7}[data-theme=sunset]{--bg-darkest:#1c1422;--bg-dark:#241b2b;--bg-mid:#33253e;--bg-light:#452f52;--fg:#f2e6ea;--dim:#ab93a8;--line:#150e1b;--acc:#f2784b;--ok:#63b46f;--warn:#eda63d;--err:#e25563;--tool:#b06ac9;--term-bg:#170f1d;--term-fg:#f0dfe4}[data-theme=light]{--bg-darkest:#eceef4;--bg-dark:#f6f7fa;--bg-mid:#e2e5ee;--bg-light:#fff;--fg:#24272f;--dim:#5d6470;--line:#d8dbe4;--acc:#4757d8;--ok:#2b8a4b;--warn:#b97a0f;--err:#d83c3f;--tool:#2278a8;--term-bg:#fff;--term-fg:#24272f}[data-theme=strawberry]{--bg-darkest:#fbe4ea;--bg-dark:#fdf0f3;--bg-mid:#f7d4de;--bg-light:#fffafb;--fg:#4a2733;--dim:#96707e;--line:#f0ccd6;--acc:#e05575;--ok:#4d9e63;--warn:#c98a2d;--err:#d43d51;--tool:#b05687;--term-bg:#fff5f8;--term-fg:#4a2733}[data-theme=ramune]{--bg-darkest:#ddeff7;--bg-dark:#ecf7fb;--bg-mid:#c9e7f4;--bg-light:#fbfeff;--fg:#173342;--dim:#5b7d90;--line:#c2e2ef;--acc:#1d86ae;--ok:#3a9e6e;--warn:#cf9a2a;--err:#d64f5c;--tool:#3a9db8;--term-bg:#f4fbfd;--term-fg:#173342}*{box-sizing:border-box}html,body,#root{height:100%;margin:0}body{background:var(--bg-dark);color:var(--fg);font-size:15px}.layout{grid-template-columns:250px 1fr 300px;height:100dvh;display:grid;overflow:hidden}.layout.right-hidden{grid-template-columns:250px 1fr}.layout.right-hidden .rightbar{display:none}@media (width<=1100px){.layout,.layout.right-hidden{grid-template-columns:220px 1fr}.layout.right-hidden .rightbar{display:block}.rightbar{z-index:5;width:min(320px,88vw);transition:transform .18s;position:fixed;top:0;bottom:0;right:0;transform:translate(100%);box-shadow:-8px 0 24px #0007}.rightbar.open{transform:none}}.sidebar{background:var(--bg-darkest);flex-direction:column;padding:10px 8px;display:flex;overflow-y:auto}.sidebar h1{color:var(--fg);flex-shrink:0;margin:0;padding:4px 8px 10px;font-size:14px;position:relative}.themepop{background:var(--bg-mid);border:1px solid var(--bg-light);z-index:6;border-radius:8px;flex-direction:column;gap:8px;padding:10px;display:flex;position:absolute;top:calc(100% - 2px);left:8px;right:8px;box-shadow:0 8px 24px #0008}.trow{color:var(--fg);cursor:pointer;align-items:center;gap:6px;font-size:12.5px;display:flex}.trow select{background:var(--bg-darkest);color:var(--fg);border:none;border-radius:5px;outline:none;padding:3px 6px}.themegrid{grid-template-columns:1fr 1fr;gap:4px;display:grid}.themebtn{background:var(--bg-light);color:var(--fg);cursor:pointer;text-align:left;border:1px solid #0000;border-radius:6px;align-items:center;gap:6px;padding:5px 8px;font-size:12px;display:flex}.themebtn:hover{filter:brightness(1.15)}.themebtn.on{border-color:var(--acc)}.swdots{flex-shrink:0;gap:2px;display:inline-flex}.swdots i{border-radius:50%;width:9px;height:9px;display:inline-block;box-shadow:inset 0 0 0 1px #0004}.agent-list{flex:1;min-height:0;overflow-y:auto}.agent-item{cursor:pointer;color:var(--dim);border-radius:6px;align-items:center;gap:8px;margin-bottom:2px;padding:7px 10px;display:flex}.agent-item .subtag{color:var(--dim);font-size:10px}.caret{cursor:pointer;width:12px;color:var(--dim);text-align:center;-webkit-user-select:none;user-select:none;flex-shrink:0;font-size:9px}.caret:hover{color:var(--fg)}.caret-spacer{flex-shrink:0;width:12px}.agent-item:hover{background:var(--bg-mid)}.agent-item.sel{background:var(--bg-mid);color:var(--fg)}.dot{border-radius:50%;flex-shrink:0;width:9px;height:9px}.dot.running{background:var(--ok);box-shadow:0 0 6px var(--ok)}.dot.waiting{background:var(--acc);box-shadow:0 0 6px var(--acc);animation:1.6s ease-in-out infinite pulse}.badge.waiting{color:var(--acc)}.dot.idle{background:var(--warn)}.dot.stopped{background:var(--dim)}.dot.error{background:var(--err);box-shadow:0 0 6px var(--err)}.sidebar .metrics{color:var(--dim);border-top:1px solid var(--line);margin-top:12px;padding:8px 10px;font-size:11px;line-height:1.7}.channel{flex-direction:column;min-width:0;min-height:0;display:flex;position:relative;overflow:hidden}.chan-head{border-bottom:2px solid var(--line);background:var(--bg-dark);flex-shrink:0;align-items:center;gap:10px;padding:10px 16px;display:flex}.chan-head .hash{color:var(--dim);font-size:20px}.chan-head .title{font-weight:700}.chan-head .sub{color:var(--dim);text-overflow:ellipsis;white-space:nowrap;margin-left:8px;font-size:12px;overflow:hidden}.badge{background:var(--bg-light);vertical-align:middle;border-radius:10px;padding:1px 8px;font-size:11px}.badge.running{color:var(--ok)}.badge.error{color:var(--err)}.badge.idle{color:var(--warn)}.badge.done{color:var(--acc)}.badge.queued{color:var(--warn);white-space:nowrap;background:#faa81a1a;border:1px solid #faa81a44}.feed{overscroll-behavior:contain;flex:1;min-height:0;padding:14px 0 8px;overflow-y:auto}.msg{gap:14px;padding:3px 18px;display:flex}.msg:hover{background:#ffffff08}.msg.pending{opacity:.55}.msg.pending .ts{color:var(--warn)}.msg.grouped{padding-top:0}.avatar{border-radius:50%;flex-shrink:0;justify-content:center;align-items:center;width:38px;height:38px;margin-top:2px;font-size:17px;display:flex}.msg-body{flex:1;min-width:0}.msg-head{align-items:baseline;gap:8px;display:flex}.actor{color:var(--warn);white-space:nowrap;font-size:11px}.runcard{flex-direction:column;gap:8px;display:flex}.statgrid{grid-template-columns:repeat(3,1fr);gap:6px;display:grid}.stat{background:var(--bg-mid);border-radius:6px;flex-direction:column;gap:1px;min-width:0;padding:5px 8px;display:flex}.stat span{color:var(--dim);text-transform:uppercase;letter-spacing:.05em;font-size:10px}.stat b{text-overflow:ellipsis;font-size:15px;overflow:hidden}.statrow{align-items:center;gap:8px;min-width:0;font-size:12.5px;display:flex}.statrow .k{color:var(--dim);text-transform:uppercase;letter-spacing:.05em;flex-shrink:0;width:58px;font-size:10px}.pill{background:var(--bg-mid);white-space:nowrap;border-radius:999px;margin-left:auto;padding:1px 8px;font-size:11px}.pill.ok{color:var(--ok)}.pill.warn{color:var(--warn)}.pill.crit{color:var(--err);background:#ed42451a}.bartrack{background:var(--bg-light);border-radius:3px;height:6px;margin-top:2px;overflow:hidden}.barfill{background:var(--ok);border-radius:3px;min-width:2px;height:100%;transition:width .3s}.barfill.warn{background:var(--warn)}.barfill.crit{background:var(--err)}.author{font-size:14.5px;font-weight:600}.ts{color:var(--dim);font-size:11px}.content{white-space:pre-wrap;word-break:break-word;line-height:1.45}.content p{margin:2px 0}.content pre{background:var(--bg-darkest);border-radius:6px;padding:8px;overflow-x:auto}.content code{background:var(--bg-darkest);border-radius:4px;padding:1px 4px;font-size:13px}.content pre code{background:0 0;padding:0}.content h1,.content h2,.content h3,.content h4,.content h5,.content h6{margin:8px 0 2px;font-size:15px}.content h1{font-size:16.5px}.content h2{font-size:15.5px}.content blockquote{border-left:3px solid var(--bg-light);color:var(--dim);background:#ffffff05;border-radius:0 4px 4px 0;margin:4px 0;padding:2px 10px}.content blockquote p{margin:2px 0}.content hr{border:none;border-top:1px solid var(--bg-light);margin:8px 0}.content del{color:var(--dim)}.content img{border-radius:6px;max-width:100%}.content li.task{margin-left:-18px;list-style:none}.content li:has(>input[type=checkbox]){margin-left:-18px;list-style:none}.content li input[type=checkbox]{accent-color:var(--ok);margin-right:6px}.tbl{margin:6px 0;overflow-x:auto}.tbl table{border-collapse:collapse;font-size:13px}.tbl th,.tbl td{border:1px solid var(--bg-light);text-align:left;padding:3px 9px}.tbl th{background:var(--bg-darkest);font-weight:600}.embed{border-left:3px solid var(--tool);background:var(--bg-darkest);border-radius:4px;margin-top:3px;padding:6px 10px;font-size:13.5px}.embed.fail{border-color:var(--err)}.embed.done{border-color:var(--ok)}.embed.running{border-color:var(--warn)}.embed.running summary .meta:after{content:"⋯";color:var(--warn);margin-left:6px;animation:1s step-end infinite blink;display:inline-block}.embed summary{gap:8px}.embed summary .mono{font-family:ui-monospace,Menlo,monospace}.toolbody{background:var(--bg-darkest);white-space:pre-wrap;word-break:break-word;border-radius:4px;max-height:340px;margin:4px 0;padding:6px 8px;font-size:12.5px;overflow-y:auto}.toolbody.del{border-left:2px solid var(--err);color:#f0a5a6;background:#ed424514}.toolbody.add{border-left:2px solid var(--ok);color:#9fd8ae;background:#3ba55d14}.toolbody.patch{padding:4px 8px}.toolbody.patch .pline{white-space:pre-wrap;word-break:break-word}.toolbody.patch .pline.add{color:#9fd8ae;border-left:2px solid var(--ok);background:#3ba55d14;padding-left:4px}.toolbody.patch .pline.del{color:#f0a5a6;border-left:2px solid var(--err);background:#ed424514;padding-left:4px}.toolbody.patch .pline.meta{color:var(--dim)}.embed.question{border-color:var(--acc);background:#5865f20f}.qopts{flex-wrap:wrap;gap:6px;margin-top:6px;display:flex}.qopt{background:var(--acc);color:#fff;cursor:pointer;border:none;border-radius:999px;padding:3px 12px;font-size:12.5px}.qopt:hover{filter:brightness(1.15)}.embed.question .meta{margin-top:6px}.embed.decision{border-color:var(--acc);background:#5865f20f}.embed.decision summary .meta{margin-left:6px}.embed.decision .content{margin-top:3px;font-size:13px}.embed summary{cursor:pointer;-webkit-user-select:none;user-select:none;list-style-position:outside}.embed summary::marker{color:var(--dim)}.embed[open] summary{margin-bottom:4px}.embed .mono{white-space:pre-wrap;word-break:break-word;max-height:340px;font-family:ui-monospace,Menlo,monospace;font-size:12.5px;overflow-y:auto}.embed .meta{color:var(--dim);margin-top:3px;font-size:11px}.reasoning{border-left:3px dashed var(--bg-light);color:var(--dim);background:#ffffff05;border-radius:4px;margin:2px 0;padding:2px 10px;font-size:12.5px}.reasoning summary{cursor:pointer;-webkit-user-select:none;user-select:none;opacity:.75}.reasoning summary:hover{opacity:1;color:var(--fg)}.reasoning[open] summary{margin-bottom:4px}.reasoning .mono{white-space:pre-wrap;word-break:break-word;max-height:260px;overflow-y:auto}.msg.live .avatar{animation:1.6s ease-in-out infinite pulse}.cursor{color:var(--acc);animation:1s step-end infinite blink}.interrupted{color:var(--warn);margin-top:3px;font-size:11.5px}@keyframes blink{50%{opacity:0}}@keyframes pulse{50%{opacity:.55}}.conn{background:var(--err);vertical-align:middle;width:8px;height:8px;box-shadow:0 0 6px var(--err);border-radius:50%;margin-left:8px;display:inline-block}.conn.ok{background:var(--ok);box-shadow:0 0 6px var(--ok)}.copybtn{border:1px solid var(--bg-light);color:var(--dim);cursor:pointer;background:0 0;border-radius:4px;flex-shrink:0;padding:0 5px;font-size:11px;line-height:16px}.copybtn:hover{color:var(--fg);filter:brightness(1.3)}.embed summary{align-items:center;gap:6px;display:flex}.msgfoot{color:var(--dim);align-items:center;gap:6px;margin-top:4px;font-size:11px;display:flex}.termdrawer{background:var(--term-bg);border-top:2px solid var(--line);flex-direction:column;display:flex}.termgrip{cursor:ns-resize;background:var(--bg-light);opacity:.6;border-bottom:1px solid var(--line);height:6px}.termgrip:hover{opacity:1}.termbar{color:var(--dim);border-bottom:1px solid var(--line);background:var(--bg-darkest);justify-content:space-between;align-items:center;padding:4px 10px;font-size:11.5px;display:flex}.termtabs{flex:1;align-items:center;gap:2px;min-width:0;display:flex;overflow-x:auto}.termtab{cursor:pointer;color:var(--dim);white-space:nowrap;background:var(--bg-mid);border-radius:5px;align-items:center;gap:6px;padding:2px 8px;font-size:11.5px;display:inline-flex}.termtab.active{color:var(--fg);outline:1px solid var(--acc)}.termtab:hover{filter:brightness(1.15)}.tabx{color:inherit;opacity:.5;cursor:pointer;background:0 0;border:none;padding:0;font-size:10px}.tabx:hover{opacity:1;color:var(--err)}.termbody{background:var(--term-bg);flex:1;gap:4px;min-height:0;padding:4px;display:flex}.termbody.split .termpane{width:50%}.termpane{background:var(--term-bg);border-radius:6px;flex:1;min-width:0;position:relative;overflow:hidden}.termpane:not(.focused){opacity:.9}.termpane.focused{box-shadow:inset 0 0 0 2px var(--acc)}.termhost{position:absolute;inset:0}.termsession{padding:4px 6px;position:absolute;inset:0}.termsplit{background:var(--line);cursor:col-resize;border-radius:2px;flex-shrink:0;width:3px}.termempty{color:var(--dim);place-items:center;font-size:12px;display:grid;position:absolute;inset:0}.jump{background:var(--acc);color:#fff;cursor:pointer;z-index:2;border:none;border-radius:999px;padding:6px 14px;font-size:12.5px;font-weight:600;position:absolute;bottom:86px;left:50%;transform:translate(-50%);box-shadow:0 4px 14px #0008}.divider-msg{color:var(--dim);align-items:center;gap:10px;padding:4px 18px;font-size:11.5px;display:flex}.divider-msg:before,.divider-msg:after{content:"";background:var(--line);flex:1;height:1px}.divider-msg.err{color:var(--err)}.day-divider{align-items:center;gap:10px;padding:14px 18px 6px;display:flex}.day-divider:before,.day-divider:after{content:"";background:var(--bg-light);flex:1;height:1px}.day-divider span{color:var(--dim);font-size:11px}.composer{padding:0 16px 18px;position:relative}.composer form{background:var(--bg-light);border-radius:10px;align-items:flex-end;gap:8px;padding:10px 12px;display:flex}.composer textarea{color:var(--fg);font:inherit;resize:none;background:0 0;border:none;outline:none;flex:1;max-height:160px;line-height:1.4;overflow-y:auto}.cmds{background:var(--bg-mid);border:1px solid var(--bg-light);z-index:3;border-radius:8px;position:absolute;bottom:calc(100% - 8px);left:16px;right:16px;overflow:hidden;box-shadow:0 -6px 20px #0007}.cmdrow{cursor:pointer;align-items:baseline;gap:10px;padding:7px 12px;font-size:13.5px;display:flex}.cmdrow:hover{background:var(--bg-light)}.cmdrow.active{background:var(--acc);color:#fff}.cmdrow.active .muted{color:#e6e9ff}.cmdrow .muted{font-size:12px}.composer button{background:var(--acc);color:#fff;cursor:pointer;border:none;border-radius:8px;padding:7px 14px;font-weight:600}.composer button:hover{opacity:.9}.composer label{color:var(--dim);white-space:nowrap;align-items:center;gap:4px;font-size:12px;display:flex}.hint{color:var(--dim);margin-top:5px;font-size:11px}.rightbar{background:var(--bg-dark);border-left:2px solid var(--line);flex-direction:column;min-height:0;padding:14px;font-size:13px;display:flex;overflow-y:auto}.rightbar>*{flex-shrink:0}.rightbar h3{text-transform:uppercase;letter-spacing:.04em;color:var(--dim);margin:14px 0 6px;font-size:11px}.rightbar h3:first-child{margin-top:0}.card{background:var(--bg-darkest);white-space:pre-wrap;word-break:break-word;border-radius:8px;max-height:200px;padding:10px;overflow-y:auto}.muted{color:var(--dim)}.prog .progrow{gap:8px;margin-bottom:4px;display:flex}.prog .progrow b{color:var(--dim);text-transform:uppercase;letter-spacing:.05em;flex-shrink:0;min-width:62px;padding-top:2px;font-size:10px}.prog .progrow.warn span{color:var(--err)}.msg-head .editbtn{color:var(--dim);cursor:pointer;opacity:0;background:0 0;border:none;padding:0 2px;font-size:11px;transition:opacity .12s}.msg:hover .msg-head .editbtn{opacity:1}.msg-head .editbtn:hover{color:var(--acc)}.branch-row{color:var(--dim);cursor:pointer;justify-content:space-between;padding:3px 0;font-size:12px;display:flex}.branch-row:hover,.branch-row.cur{color:var(--fg)}.mini-cron{opacity:.75;margin-left:auto;font-size:10px}.sched-row{cursor:pointer;border-radius:6px;margin-bottom:2px;padding:6px 8px}.sched-row:hover,.sched-row.cur{background:var(--bg-mid)}.sched-top{align-items:baseline;gap:6px;font-size:12.5px;display:flex}.sched-row .sched-meta{color:var(--dim);margin-top:2px;font-size:11px}.sched-row .sched-prompt{color:var(--dim);text-overflow:ellipsis;white-space:nowrap;margin-top:2px;font-size:11.5px;overflow:hidden}.badge.cron{color:var(--tool);white-space:nowrap;background:#3ba0c91a;border:1px solid #3ba0c944}.sesscard{flex-direction:column;gap:6px;font-size:12.5px;display:flex}.sessrow{align-items:center;gap:8px;display:flex}.filebox{background:var(--bg-darkest);border-radius:8px;max-height:260px;padding:4px;overflow-y:auto}.filerow{cursor:pointer;color:var(--fg);border-radius:4px;align-items:center;gap:6px;padding:3px 8px;font-size:12.5px;display:flex}.filerow:hover{background:var(--bg-mid)}.filerow .ficon{width:10px;color:var(--dim);text-align:center;flex-shrink:0;font-size:9px}.filerow.isdir .fname{font-weight:600}.fname{text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.fsize{color:var(--dim);flex-shrink:0;margin-left:auto;font-size:10px}.sessrow .k{color:var(--dim);text-transform:uppercase;letter-spacing:.05em;flex-shrink:0;width:74px;font-size:10px}.ellip{text-overflow:ellipsis;white-space:nowrap;text-align:left;direction:rtl;overflow:hidden}.modelbox{background:var(--bg-darkest);border-radius:8px;flex-direction:column;gap:6px;padding:10px;display:flex}.modelbox select,.modelbox input[type=text]{background:var(--bg-mid);color:var(--fg);font:inherit;border:none;border-radius:6px;outline:none;min-width:0;padding:6px 8px}.modelbox button{background:var(--acc);color:#fff;cursor:pointer;white-space:nowrap;border:none;border-radius:6px;padding:6px 10px;font-size:12.5px;font-weight:600}.modelbox button:hover{opacity:.9}.modelbox .meta{color:var(--dim);font-size:11px}.btnrow{gap:6px;margin:8px 0;display:flex}.btnrow button{background:var(--bg-light);color:var(--fg);cursor:pointer;border:none;border-radius:6px;padding:6px 10px;font-size:13px}.btnrow button:hover{filter:brightness(1.2)}.btnrow .runbtn{background:var(--ok);color:#fff;font-weight:600}.btnrow .danger{background:var(--err);color:#fff;font-weight:600}.ctrlrow{align-items:center;gap:8px;margin-top:6px;font-size:12.5px;display:flex}.ctrlrow label{cursor:pointer;color:var(--fg);white-space:nowrap;align-items:center;gap:5px;display:flex}.ctrlrow .muted{font-size:11.5px}.goalstate{background:var(--bg-light);color:var(--fg);cursor:pointer;border:none;border-radius:6px;padding:3px 10px;font-size:12px}.goalstate.on.active{background:var(--ok);color:#fff;font-weight:600}.goalstate.on.done{background:var(--acc);color:#fff;font-weight:600}.goalstate.on.paused{background:var(--warn);color:#fff;font-weight:600}.goalstate:disabled{cursor:default;filter:none}.iconbtn{background:var(--bg-light);color:var(--fg);cursor:pointer;border:none;border-radius:6px;width:26px;height:24px;font-size:13px}.iconbtn:hover{filter:brightness(1.3)}.overlay{z-index:10;background:#0009;place-items:center;display:grid;position:fixed;inset:0}.modal{background:var(--bg-mid);border-radius:10px;width:min(620px,92vw);max-height:88vh;padding:16px 18px;overflow-y:auto}.modal-head{justify-content:space-between;align-items:center;margin-bottom:12px;font-size:16px;display:flex}.modal label{color:var(--dim);flex-direction:column;gap:4px;font-size:12.5px;display:flex}.modal input[type=text],.modal input[type=number],.modal input[type=password],.modal input[type=url],.modal input[type=email],.modal select,.modal textarea{background:var(--bg-darkest);color:var(--fg);font:inherit;border:1px solid #0000;border-radius:6px;outline:none;padding:7px 9px}.modal input:focus,.modal select:focus,.modal textarea:focus{border-color:var(--acc)}.modal ::placeholder{color:#7c818f;opacity:1}.modal select option{background:var(--bg-darkest);color:var(--fg)}.modal input.compact{padding:5px 8px;font-size:12.5px}.modal textarea{resize:vertical}.w100{width:100%}.mono{font-family:ui-monospace,Menlo,monospace;font-size:13px}.modal fieldset{border:1px solid var(--bg-light);border-radius:8px;margin:0 0 4px;padding:10px}.modal fieldset legend{color:var(--dim);text-transform:uppercase;letter-spacing:.05em;padding:0 6px;font-size:11px}.cfgrow{flex-wrap:wrap;align-items:center;gap:6px;margin-bottom:6px;display:flex}.cfgrow input{background:var(--bg-darkest);color:var(--fg);border:none;border-radius:5px;flex:1;min-width:0;padding:5px 8px;font-size:12.5px}.cfgrow .cfgname{width:90px;color:var(--warn);flex:none;font-weight:600}.cfggrid{grid-template-columns:repeat(auto-fit,minmax(150px,1fr));gap:8px;display:grid}.cfggrid label,.cfgcol label{color:var(--dim);flex-direction:column;gap:3px;font-size:11px;display:flex}.cfggrid input{background:var(--bg-darkest);color:var(--fg);border:none;border-radius:5px;width:100%;padding:5px 8px}.cfgcol{margin-bottom:8px}.cfgcol textarea{background:var(--bg-darkest);color:var(--fg);font:inherit;border:none;border-radius:5px;width:100%;margin-top:4px;padding:5px 8px;font-size:12.5px}.presetbtn{background:var(--bg-light);color:var(--fg);cursor:pointer;border:1px solid #0000;border-radius:6px;flex:1;padding:7px 10px;font-size:13px}.presetbtn.active{background:var(--acc);color:#fff}.forkchip{white-space:nowrap;color:var(--dim);cursor:pointer;align-items:center;gap:4px;font-size:11.5px;display:flex}.dirlist{background:var(--bg-darkest);border-radius:6px;max-height:160px;padding:4px;overflow-y:auto}.direntry{cursor:pointer;border-radius:4px;padding:4px 8px;font-size:14px}.direntry:hover{background:var(--bg-light)}.xterm{cursor:text;-webkit-user-select:none;user-select:none;position:relative}.xterm.focus,.xterm:focus{outline:none}.xterm .xterm-helpers{z-index:5;position:absolute;top:0}.xterm .xterm-helper-textarea{opacity:0;z-index:-5;white-space:nowrap;resize:none;border:0;width:0;height:0;margin:0;padding:0;position:absolute;top:0;left:-9999em;overflow:hidden}.xterm .composition-view{color:#fff;white-space:nowrap;z-index:1;background:#000;display:none;position:absolute}.xterm .composition-view.active{display:block}.xterm .xterm-viewport{cursor:default;background-color:#000;position:absolute;inset:0;overflow-y:scroll}.xterm .xterm-screen{position:relative}.xterm .xterm-screen canvas{position:absolute;top:0;left:0}.xterm-char-measure-element{visibility:hidden;line-height:normal;display:inline-block;position:absolute;top:0;left:-9999em}.xterm.enable-mouse-events{cursor:default}.xterm.xterm-cursor-pointer,.xterm .xterm-cursor-pointer{cursor:pointer}.xterm.column-select.focus{cursor:crosshair}.xterm .xterm-accessibility:not(.debug),.xterm .xterm-message{z-index:10;color:#0000;pointer-events:none;position:absolute;inset:0}.xterm .xterm-accessibility-tree:not(.debug) ::selection{color:#0000}.xterm .xterm-accessibility-tree{-webkit-user-select:text;user-select:text;white-space:pre;font-family:monospace}.xterm .xterm-accessibility-tree>div{transform-origin:0;width:fit-content}.xterm .live-region{width:1px;height:1px;position:absolute;left:-9999px;overflow:hidden}.xterm-dim{opacity:1!important}.xterm-underline-1{text-decoration:underline}.xterm-underline-2{-webkit-text-decoration:underline double;text-decoration:underline double}.xterm-underline-3{-webkit-text-decoration:underline wavy;text-decoration:underline wavy}.xterm-underline-4{-webkit-text-decoration:underline dotted;text-decoration:underline dotted}.xterm-underline-5{-webkit-text-decoration:underline dashed;text-decoration:underline dashed}.xterm-overline{text-decoration:overline}.xterm-overline.xterm-underline-1{text-decoration:underline overline}.xterm-overline.xterm-underline-2{-webkit-text-decoration:overline double underline;text-decoration:overline double underline}.xterm-overline.xterm-underline-3{-webkit-text-decoration:overline wavy underline;text-decoration:overline wavy underline}.xterm-overline.xterm-underline-4{-webkit-text-decoration:overline dotted underline;text-decoration:overline dotted underline}.xterm-overline.xterm-underline-5{-webkit-text-decoration:overline dashed underline;text-decoration:overline dashed underline}.xterm-strikethrough{text-decoration:line-through}.xterm-screen .xterm-decoration-container .xterm-decoration{z-index:6;position:absolute}.xterm-screen .xterm-decoration-container .xterm-decoration.xterm-decoration-top-layer{z-index:7}.xterm-decoration-overview-ruler{z-index:8;pointer-events:none;position:absolute;top:0;right:0}.xterm-decoration-top{z-index:2;position:relative}.xterm .xterm-scrollable-element>.scrollbar{cursor:default}.xterm .xterm-scrollable-element>.scrollbar>.scra{cursor:pointer;font-size:11px!important}.xterm .xterm-scrollable-element>.visible{opacity:1;z-index:11;background:0 0;transition:opacity .1s linear}.xterm .xterm-scrollable-element>.invisible{opacity:0;pointer-events:none}.xterm .xterm-scrollable-element>.invisible.fade{transition:opacity .8s linear}.xterm .xterm-scrollable-element>.shadow{display:none;position:absolute}.xterm .xterm-scrollable-element>.shadow.top{width:100%;height:3px;box-shadow:var(--vscode-scrollbar-shadow,#000) 0 6px 6px -6px inset;display:block;top:0;left:3px}.xterm .xterm-scrollable-element>.shadow.left{width:3px;height:100%;box-shadow:var(--vscode-scrollbar-shadow,#000) 6px 0 6px -6px inset;display:block;top:3px;left:0}.xterm .xterm-scrollable-element>.shadow.top-left-corner{width:3px;height:3px;display:block;top:0;left:0}.xterm .xterm-scrollable-element>.shadow.top.left{box-shadow:var(--vscode-scrollbar-shadow,#000) 6px 0 6px -6px inset}
|