teapot-coding-agent 0.12.1 → 0.13.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/README.md +50 -23
- package/dist/agent/agent.js +12 -0
- package/dist/agent/tools.js +28 -1
- package/dist/master.js +115 -4
- package/dist/model-meta.js +43 -0
- package/dist/server/api.js +43 -2
- package/package.json +1 -1
- package/public/assets/index-CO9AhxUA.css +1 -0
- package/public/assets/index-_IZWOALZ.js +15 -0
- package/public/assets/index-_IZWOALZ.js.map +1 -0
- package/public/index.html +2 -2
- package/public/assets/index-4DOCB8Mz.js +0 -15
- package/public/assets/index-4DOCB8Mz.js.map +0 -1
- package/public/assets/index-Q714iow7.css +0 -1
package/README.md
CHANGED
|
@@ -6,7 +6,9 @@ tab, and any number of long-running agents.
|
|
|
6
6
|
|
|
7
7
|
## Design principles
|
|
8
8
|
|
|
9
|
-
- **TypeScript + Node.js**,
|
|
9
|
+
- **TypeScript + Node.js**, small dependency set (`hono`, `openai`,
|
|
10
|
+
`@hono/node-server`, `ws`, `zod`, `@mozilla/readability` + `happy-dom` for
|
|
11
|
+
`read_url`)
|
|
10
12
|
- **Idle cost ≈ 0**: no polling loops; everything is event-driven or driven by
|
|
11
13
|
one 15-second scheduler tick. Verified: master sits at ~0% CPU when idle.
|
|
12
14
|
- **No TUI** — Discord-style chat UI built with **SolidJS + Vite**
|
|
@@ -29,21 +31,22 @@ Requires Node.js **>= 24** (TypeScript is executed natively in dev mode).
|
|
|
29
31
|
|
|
30
32
|
```sh
|
|
31
33
|
npm install -g teapot-coding-agent
|
|
32
|
-
mkdir -p ~/.config/teapot-coding-agent
|
|
33
|
-
curl -o ~/.config/teapot-coding-agent/config.json \
|
|
34
|
-
https://raw.githubusercontent.com/akku1139/teapot/main/teapot.config.example.json # then edit it
|
|
35
34
|
teapot
|
|
36
|
-
# open http://localhost:7788
|
|
35
|
+
# open http://localhost:7788 — the first-run wizard walks you through
|
|
36
|
+
# provider → API key → model → first agent (writes config.json for you)
|
|
37
37
|
```
|
|
38
38
|
|
|
39
39
|
The global install puts a `teapot` binary on your PATH (it serves the compiled
|
|
40
40
|
server plus the pre-built web UI — no build step, no repo checkout needed).
|
|
41
|
+
Prefer hand-writing the config? Skip the wizard and drop an edited copy of
|
|
42
|
+
[`teapot.config.example.json`](teapot.config.example.json) at the path below.
|
|
41
43
|
Alternatives:
|
|
42
44
|
|
|
43
45
|
```sh
|
|
44
|
-
npx teapot-coding-agent
|
|
45
|
-
teapot
|
|
46
|
-
|
|
46
|
+
npx teapot-coding-agent # run without installing
|
|
47
|
+
teapot --port 8080 # CLI flags: --port/-p, --config/-c
|
|
48
|
+
teapot ~/my-config.json # explicit config path
|
|
49
|
+
TEAPOT_PORT=8080 teapot # env overrides work as usual
|
|
47
50
|
```
|
|
48
51
|
|
|
49
52
|
### Run from a checkout (development)
|
|
@@ -95,16 +98,21 @@ master (Hono server, src/master.ts + src/server/api.ts)
|
|
|
95
98
|
|
|
96
99
|
- **Agent** (`src/agent/agent.ts`): an async loop that alternates LLM turns and
|
|
97
100
|
tool executions until the model stops calling tools, then auto-continues
|
|
98
|
-
toward its goal.
|
|
101
|
+
toward its goal. `ask_user` parks the loop in a `waiting` status until the
|
|
102
|
+
operator replies. Stop/resume at any time via API/UI — stop aborts an
|
|
99
103
|
in-flight LLM call immediately (AbortController). Runaway guards: turn cap
|
|
100
104
|
per round, consecutive-tool-error cap, per-command timeouts.
|
|
101
|
-
- **Context
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
+
- **Context management (staged)**: each turn prefers the provider's real
|
|
106
|
+
`prompt_tokens`; past ~60% of budget, old oversized tool outputs are pruned
|
|
107
|
+
(recent window protected); past the budget (`contextTokenBudget`, default
|
|
108
|
+
~96k — auto-derived at 75% when a model's `contextWindowTokens` is known)
|
|
109
|
+
older turns are summarized into dense continuation notes plus a
|
|
110
|
+
"## Durable lessons" section harvested into memory.md. Cut points never
|
|
111
|
+
split a tool_call/tool_result pair.
|
|
105
112
|
- **Session persistence**: conversations are rebuilt from the JSONL log on
|
|
106
|
-
|
|
107
|
-
restarts instead of starting blank.
|
|
113
|
+
first interaction (lazy — boot cost stays O(agents)), so agents resume
|
|
114
|
+
mid-task across master restarts instead of starting blank. Restores replay
|
|
115
|
+
byte-identical request payloads, keeping provider prefix caches warm.
|
|
108
116
|
- **LLM** (`src/agent/llm.ts`): official `openai` npm client against any
|
|
109
117
|
OpenAI-compatible endpoint (OpenRouter, vLLM, Ollama...). Model is config,
|
|
110
118
|
never hard-coded.
|
|
@@ -115,24 +123,43 @@ master (Hono server, src/master.ts + src/server/api.ts)
|
|
|
115
123
|
(Codex-style multi-file add/update/rename/delete patches, validated
|
|
116
124
|
atomically before writing) and `list_dir`; `bash` for git/builds/tests and
|
|
117
125
|
quick bulk transforms; `read_url` fetches a web page's readable content
|
|
118
|
-
(Mozilla Readability + happy-dom); plus meta tools `finish` /
|
|
119
|
-
`report_progress` / `get_goal` / `set_goal` / `
|
|
120
|
-
`
|
|
121
|
-
|
|
126
|
+
(Mozilla Readability + happy-dom); plus meta tools `finish` / `ask_user` /
|
|
127
|
+
`report_progress` / `get_goal` / `set_goal` / `get_todo` / `set_todo` /
|
|
128
|
+
`read_memory` / `set_memory` / `get_feedback` / `add_feedback` /
|
|
129
|
+
`record_decision` / `get_decisions` / `list_skills`. Paths are confined to
|
|
130
|
+
the workspace (symlink-aware); bash runs detached in its own process group
|
|
131
|
+
and the whole group is SIGKILLed on timeout or shutdown.
|
|
132
|
+
- **Sub-agents** — an agent can `spawn_agent({task, context, persona?})` to
|
|
133
|
+
delegate work: `context:"fork"` starts the child with a byte-exact copy of
|
|
134
|
+
the parent's conversation prefix (provider prefix caches stay warm; stored
|
|
135
|
+
as a reference header in the child's log — parent history is never copied),
|
|
136
|
+
`"none"` starts fresh. Parents steer children via `message_agent`, check on
|
|
137
|
+
them with `list_children`, and can bulk-stop them (`stop_children`,
|
|
138
|
+
descendants included). Children mirror their activity into the parent's
|
|
139
|
+
timeline tagged `@<id>`, and their finish/error reports are delivered back
|
|
140
|
+
automatically. Nesting depth is capped by `maxSpawnDepth` (default 3).
|
|
141
|
+
Default personas: reviewer/tester/researcher/implementer (+ read-only
|
|
142
|
+
enforcement for researcher/reviewer), plus `@mention` spawning from the
|
|
143
|
+
composer.
|
|
122
144
|
- **Cache-friendly prompt design** — the system prompt is byte-identical on
|
|
123
145
|
every turn; session state (goal, memory, skills) is fetched via tools, never
|
|
124
146
|
injected. Combined with the append-only message history this keeps provider
|
|
125
147
|
prefix caches hot, so long sessions pay incremental input prices instead of
|
|
126
148
|
re-sending full context every turn.
|
|
127
149
|
- **Session storage** — everything teapot manages lives under
|
|
128
|
-
`<dataDir>/sessions/<sid>/` (`chat.jsonl`, `goal.md`, `
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
150
|
+
`<dataDir>/sessions/<sid>/` (`chat.jsonl`, `goal.md`, `todo.md`,
|
|
151
|
+
`memory.md`, `feedback.md`, `decisions.md`), so agent workspaces stay
|
|
152
|
+
clean. Each incarnation gets a fresh `<agentId>-<uuid>` directory (no
|
|
153
|
+
history leaks across projects); restarts reuse the latest one. Legacy
|
|
154
|
+
layouts are migrated automatically.
|
|
132
155
|
- **Goal / knowledge** — goal + memory are harness-managed and read/written
|
|
133
156
|
through tools (`get_goal` / `set_goal` / `read_memory` / `set_memory`);
|
|
134
157
|
`AGENTS.md` is optional project knowledge in the workspace root that agents
|
|
135
158
|
are told to read at session start. Nothing is seeded into your project.
|
|
159
|
+
- **Decisions & feedback** — `record_decision(decision, rationale,
|
|
160
|
+
alternatives?)` keeps the *why* behind choices in `decisions.md`
|
|
161
|
+
(compaction forgets reasoning, this file doesn't); corrections become
|
|
162
|
+
durable rules via `add_feedback(rule)` with repetition counts.
|
|
136
163
|
- **Task list** — `todo.md` in the session dir, editable by BOTH sides:
|
|
137
164
|
humans write it from the web UI (✅ tasks panel) or
|
|
138
165
|
`POST /api/agents/:id/todo {text, notify?}`, the agent reads and updates it
|
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/tools.js
CHANGED
|
@@ -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
|
@@ -11,6 +11,7 @@ import { fileURLToPath } from "node:url";
|
|
|
11
11
|
import { Agent } from "./agent/agent.js";
|
|
12
12
|
import { parseSchedule, matches, nextFireAt } from "./scheduler/cron.js";
|
|
13
13
|
import { bus } from "./bus.js";
|
|
14
|
+
import { fetchModelList, contextLengthFor } from "./model-meta.js";
|
|
14
15
|
/** how deep sub-agent spawning may nest (parent=0, its subs=1, …) */
|
|
15
16
|
const MAX_SPAWN_DEPTH = 3;
|
|
16
17
|
/**
|
|
@@ -175,6 +176,7 @@ function printAgentEvent(e) {
|
|
|
175
176
|
export class Master {
|
|
176
177
|
agents = new Map();
|
|
177
178
|
tasks = [];
|
|
179
|
+
childWaiters = new Map();
|
|
178
180
|
startedAt = Date.now();
|
|
179
181
|
config;
|
|
180
182
|
configPath;
|
|
@@ -282,6 +284,20 @@ export class Master {
|
|
|
282
284
|
this.raw.llm = llm;
|
|
283
285
|
if (body.password)
|
|
284
286
|
this.raw.password = body.password;
|
|
287
|
+
// learn the window size up front so compaction derives 75% of it
|
|
288
|
+
try {
|
|
289
|
+
const list = await fetchModelList(llm.baseUrl, body.apiKey);
|
|
290
|
+
const cl = contextLengthFor(list, llm.model);
|
|
291
|
+
if (cl) {
|
|
292
|
+
this.config.contextWindowTokens = cl;
|
|
293
|
+
this.raw.contextWindowTokens = cl;
|
|
294
|
+
raw.contextWindowTokens = cl; // also persist into the fresh config file
|
|
295
|
+
writeFileSync(this.configPath, JSON.stringify(raw, null, 2) + "\n");
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
catch {
|
|
299
|
+
/* metadata is best-effort */
|
|
300
|
+
}
|
|
285
301
|
let agentId;
|
|
286
302
|
if (body.workspace?.trim()) {
|
|
287
303
|
const ws = path.resolve(body.workspace.replace(/^~/, process.env.HOME ?? "~"));
|
|
@@ -326,6 +342,13 @@ export class Master {
|
|
|
326
342
|
// spawn-tree depth: computed from the config chain (ac may not be
|
|
327
343
|
// registered yet when this runs — spawnChildFor persists AFTER creation)
|
|
328
344
|
const myDepth = ac.parent ? this.depthOf(ac.parent) + 1 : 0;
|
|
345
|
+
// learn the model's real context window from provider metadata (once per
|
|
346
|
+
// endpoint, cached) unless the config already pins one explicitly
|
|
347
|
+
let inferredWindow;
|
|
348
|
+
if (!ac.contextWindowTokens && !this.config.contextWindowTokens) {
|
|
349
|
+
const list = await fetchModelList(llm.baseUrl, llm.apiKey).catch(() => []);
|
|
350
|
+
inferredWindow = contextLengthFor(list, llm.model);
|
|
351
|
+
}
|
|
329
352
|
const sessionDir = this.resolveSessionDir(ac.id, opts.fresh === true);
|
|
330
353
|
await mkdirSync(sessionDir, { recursive: true });
|
|
331
354
|
const agent = new Agent({
|
|
@@ -335,10 +358,14 @@ export class Master {
|
|
|
335
358
|
sessionDir,
|
|
336
359
|
progressIntervalMs: this.config.progressIntervalMs,
|
|
337
360
|
...(this.config.progressMinChars ? { progressMinChars: this.config.progressMinChars } : {}),
|
|
338
|
-
autoContinue: true,
|
|
361
|
+
autoContinue: ac.autoContinue ?? true,
|
|
339
362
|
...(this.config.contextTokenBudget ? { contextTokenBudget: this.config.contextTokenBudget } : {}),
|
|
340
|
-
...(ac.contextWindowTokens || this.config.contextWindowTokens
|
|
341
|
-
? {
|
|
363
|
+
...(ac.contextWindowTokens || this.config.contextWindowTokens || inferredWindow
|
|
364
|
+
? {
|
|
365
|
+
contextWindowTokens: (ac.contextWindowTokens ??
|
|
366
|
+
this.config.contextWindowTokens ??
|
|
367
|
+
inferredWindow),
|
|
368
|
+
}
|
|
342
369
|
: {}),
|
|
343
370
|
globalSkillsDir: path.join(CONFIG_DIR, "skills"),
|
|
344
371
|
bundledSkillsDir: BUNDLED_SKILLS_DIR,
|
|
@@ -369,6 +396,7 @@ export class Master {
|
|
|
369
396
|
})),
|
|
370
397
|
stop: (ids) => this.stopChildrenFor(ac.id, ids),
|
|
371
398
|
message: (id, text) => this.messageChild(ac.id, id, text),
|
|
399
|
+
wait: (ids, ms) => this.waitChildren(ac.id, ids, ms),
|
|
372
400
|
};
|
|
373
401
|
agent.toolCtx.subAgents = hooks;
|
|
374
402
|
}
|
|
@@ -491,6 +519,69 @@ export class Master {
|
|
|
491
519
|
return { stopped };
|
|
492
520
|
}
|
|
493
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
|
+
}
|
|
494
585
|
async messageChild(parentId, childId, text) {
|
|
495
586
|
const cfg = this.config.agents.find((a) => a.id === childId);
|
|
496
587
|
if (!cfg || cfg.parent !== parentId)
|
|
@@ -510,6 +601,10 @@ export class Master {
|
|
|
510
601
|
const parentId = ac.parent;
|
|
511
602
|
if (!parentId)
|
|
512
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);
|
|
513
608
|
const parent = this.agents.get(parentId);
|
|
514
609
|
if (!parent)
|
|
515
610
|
return;
|
|
@@ -601,7 +696,7 @@ export class Master {
|
|
|
601
696
|
* Without: keep the current endpoint, swap only the model name.
|
|
602
697
|
* The choice is persisted on the agent config so it survives restarts.
|
|
603
698
|
*/
|
|
604
|
-
setAgentModel(id, providerName, model) {
|
|
699
|
+
async setAgentModel(id, providerName, model, contextWindowTokens) {
|
|
605
700
|
const agent = this.agents.get(id);
|
|
606
701
|
if (!agent)
|
|
607
702
|
throw new Error(`no such agent: ${id}`);
|
|
@@ -635,6 +730,22 @@ export class Master {
|
|
|
635
730
|
ac.provider = providerName;
|
|
636
731
|
if (ac)
|
|
637
732
|
ac.model = llm.model;
|
|
733
|
+
// learn the new model's window (metadata lookup) unless explicitly given
|
|
734
|
+
let win = contextWindowTokens;
|
|
735
|
+
if (win === undefined && ac && !ac.contextWindowTokens && !this.config.contextWindowTokens) {
|
|
736
|
+
try {
|
|
737
|
+
const list = await fetchModelList(llm.baseUrl, llm.apiKey);
|
|
738
|
+
win = contextLengthFor(list, llm.model);
|
|
739
|
+
}
|
|
740
|
+
catch {
|
|
741
|
+
/* metadata is best-effort */
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
if (win !== undefined) {
|
|
745
|
+
if (ac)
|
|
746
|
+
ac.contextWindowTokens = win;
|
|
747
|
+
agent.opts.contextWindowTokens = win;
|
|
748
|
+
}
|
|
638
749
|
this.saveConfig();
|
|
639
750
|
void agent.log.append("system_note", agent.currentSession, agent.currentBranch, {
|
|
640
751
|
event: "model-changed",
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Provider model metadata (context window sizes) with a small TTL cache.
|
|
3
|
+
* Used to auto-populate contextWindowTokens so compaction budgets derive
|
|
4
|
+
* from the real window instead of the safe default.
|
|
5
|
+
*/
|
|
6
|
+
import fs from "node:fs";
|
|
7
|
+
const cache = new Map();
|
|
8
|
+
const TTL = 10 * 60_000;
|
|
9
|
+
/** Fetch GET /models from an OpenAI-compatible endpoint (cached ~10 min). */
|
|
10
|
+
export async function fetchModelList(baseUrl, apiKey) {
|
|
11
|
+
const root = baseUrl.replace(/\/+$/, "");
|
|
12
|
+
const hit = cache.get(root);
|
|
13
|
+
if (hit && Date.now() - hit.at < TTL)
|
|
14
|
+
return hit.list;
|
|
15
|
+
try {
|
|
16
|
+
const res = await fetch(`${root}/models`, {
|
|
17
|
+
headers: apiKey ? { authorization: `Bearer ${apiKey}` } : {},
|
|
18
|
+
signal: AbortSignal.timeout(15_000),
|
|
19
|
+
});
|
|
20
|
+
if (!res.ok)
|
|
21
|
+
throw new Error(`HTTP ${res.status}`);
|
|
22
|
+
const j = (await res.json());
|
|
23
|
+
const list = (j.data ?? [])
|
|
24
|
+
.map((m) => ({
|
|
25
|
+
id: typeof m.id === "string" ? m.id : "",
|
|
26
|
+
contextLength: typeof m.context_length === "number" ? m.context_length : undefined,
|
|
27
|
+
}))
|
|
28
|
+
.filter((m) => m.id);
|
|
29
|
+
cache.set(root, { at: Date.now(), list });
|
|
30
|
+
return list;
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
return []; // unreachable provider — caller treats as "unknown"
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
export function contextLengthFor(list, model) {
|
|
37
|
+
return list.find((m) => m.id === model)?.contextLength;
|
|
38
|
+
}
|
|
39
|
+
/** test hook */
|
|
40
|
+
export function clearModelListCache() {
|
|
41
|
+
cache.clear();
|
|
42
|
+
void fs;
|
|
43
|
+
}
|
package/dist/server/api.js
CHANGED
|
@@ -5,6 +5,7 @@ import { Hono } from "hono";
|
|
|
5
5
|
import { serve, upgradeWebSocket } from "@hono/node-server";
|
|
6
6
|
import { WebSocketServer } from "ws";
|
|
7
7
|
import { readFileSync, existsSync } from "node:fs";
|
|
8
|
+
import { currentSkills } from "../agent/tools.js";
|
|
8
9
|
import { spawn } from "node:child_process";
|
|
9
10
|
import { promises as fs } from "node:fs";
|
|
10
11
|
import { fileURLToPath } from "node:url";
|
|
@@ -354,11 +355,13 @@ export function buildApp(master) {
|
|
|
354
355
|
});
|
|
355
356
|
// switch a running session's model/provider
|
|
356
357
|
app.post("/api/agents/:id/model", async (c) => {
|
|
357
|
-
const body = await c.req
|
|
358
|
+
const body = await c.req
|
|
359
|
+
.json()
|
|
360
|
+
.catch(() => null);
|
|
358
361
|
if (!body)
|
|
359
362
|
return c.json({ error: "invalid JSON" }, 400);
|
|
360
363
|
try {
|
|
361
|
-
const r = master.setAgentModel(c.req.param("id"), body.provider, body.model);
|
|
364
|
+
const r = await master.setAgentModel(c.req.param("id"), body.provider, body.model, body.contextWindowTokens);
|
|
362
365
|
return c.json({ ok: true, ...r });
|
|
363
366
|
}
|
|
364
367
|
catch (err) {
|
|
@@ -390,6 +393,21 @@ export function buildApp(master) {
|
|
|
390
393
|
return c.json({ error: "text or status required" }, 400);
|
|
391
394
|
return c.json({ ok: true });
|
|
392
395
|
});
|
|
396
|
+
// skills visible to an agent (workspace + global + bundled roots)
|
|
397
|
+
app.get("/api/agents/:id/skills", async (c) => {
|
|
398
|
+
const a = master.agents.get(c.req.param("id"));
|
|
399
|
+
if (!a)
|
|
400
|
+
return c.json({ error: "not found" }, 404);
|
|
401
|
+
try {
|
|
402
|
+
const list = await currentSkills(a.toolCtx);
|
|
403
|
+
return c.json({
|
|
404
|
+
skills: list.map((s) => ({ name: s.name, description: s.description, source: s.source })),
|
|
405
|
+
});
|
|
406
|
+
}
|
|
407
|
+
catch (err) {
|
|
408
|
+
return c.json({ error: err.message }, 400);
|
|
409
|
+
}
|
|
410
|
+
});
|
|
393
411
|
// force a context compaction pass (slash command /compact)
|
|
394
412
|
app.post("/api/agents/:id/compact", async (c) => {
|
|
395
413
|
const a = master.agents.get(c.req.param("id"));
|
|
@@ -418,6 +436,22 @@ export function buildApp(master) {
|
|
|
418
436
|
return c.json({ error: err.message }, 400);
|
|
419
437
|
}
|
|
420
438
|
});
|
|
439
|
+
// per-agent auto-continue toggle (loops toward an active goal)
|
|
440
|
+
app.post("/api/agents/:id/auto-continue", async (c) => {
|
|
441
|
+
const a = master.agents.get(c.req.param("id"));
|
|
442
|
+
if (!a)
|
|
443
|
+
return c.json({ error: "not found" }, 404);
|
|
444
|
+
const body = await c.req.json().catch(() => null);
|
|
445
|
+
if (!body || typeof body.value !== "boolean")
|
|
446
|
+
return c.json({ error: "boolean value required" }, 400);
|
|
447
|
+
const ac = master.config.agents.find((x) => x.id === c.req.param("id"));
|
|
448
|
+
if (ac)
|
|
449
|
+
ac.autoContinue = body.value;
|
|
450
|
+
a.opts.autoContinue = body.value;
|
|
451
|
+
master.saveConfig();
|
|
452
|
+
bus.emit("update", { kind: "agent-update", agentId: c.req.param("id") });
|
|
453
|
+
return c.json({ ok: true, value: body.value });
|
|
454
|
+
});
|
|
421
455
|
// default sub-agent personas for @mentions and spawn_agent
|
|
422
456
|
app.get("/api/personas", (c) => c.json({
|
|
423
457
|
personas: Object.entries(SUB_PERSONAS).map(([key, p]) => ({ key, label: p.label, directive: p.directive })),
|
|
@@ -511,6 +545,13 @@ export function buildApp(master) {
|
|
|
511
545
|
events = events.filter((e) => e.branch === branch);
|
|
512
546
|
if (session)
|
|
513
547
|
events = events.filter((e) => e.session === session);
|
|
548
|
+
// cursor pagination for older pages: everything strictly BEFORE this id
|
|
549
|
+
const before = c.req.query("before");
|
|
550
|
+
if (before) {
|
|
551
|
+
const idx = events.findIndex((e) => e.id === before);
|
|
552
|
+
events = idx === -1 ? [] : events.slice(0, idx);
|
|
553
|
+
return c.json({ events: events.slice(-limit), total: events.length });
|
|
554
|
+
}
|
|
514
555
|
return c.json({ events: events.slice(-limit), total: events.length });
|
|
515
556
|
});
|
|
516
557
|
app.get("/api/agents/:id/branches", async (c) => {
|
package/package.json
CHANGED
|
@@ -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;font-family:gg sans,ui-sans-serif,system-ui,-apple-system,Segoe UI,sans-serif}*{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}.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}.ctxwarn{color:var(--warn);font-weight:600}.ctxcrit{color:var(--err);font-weight:700}.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{border-top:2px solid var(--line);background:#0d0e12;flex-direction:column;flex-shrink:0;height:38vh;min-height:220px;display:flex}.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}.termhost{flex:1;min-height:0;padding:6px 8px;overflow:hidden}.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}.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}.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 select,.modal textarea{background:var(--bg-darkest);color:var(--fg);font:inherit;border:none;border-radius:6px;outline:none;padding:7px 9px}.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}
|