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 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**, minimal dependencies (`hono`, `openai`, `@hono/node-server`)
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 # run without installing
45
- teapot ~/my-config.json # explicit config path
46
- TEAPOT_PORT=8080 teapot # env overrides work as usual
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. Stop/resume at any time via API/UI — stop aborts an
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 compaction**: message history is token-estimated each turn; past a
102
- budget (`contextTokenBudget`, default ~96k) older turns are summarized by
103
- the LLM into dense continuation notes (fallback: safe truncation). Cut
104
- points never split a tool_call/tool_result pair.
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
- restart (`restoreSession`), so agents resume mid-task across master
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` / `read_memory` / `set_memory` /
120
- `list_skills`. Paths are confined to the workspace; bash runs detached in its
121
- own process group and the whole group is SIGKILLed on timeout or shutdown.
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`, `memory.md`), so agent
129
- workspaces stay clean. Each incarnation gets a fresh `<agentId>-<uuid>`
130
- directory (no history leaks across projects); restarts reuse the latest one.
131
- Legacy layouts are migrated automatically.
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
@@ -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;
@@ -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} — it works in parallel; use list_children / message_agent to steer it, stop_children to halt` };
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
- ? { contextWindowTokens: (ac.contextWindowTokens ?? this.config.contextWindowTokens) }
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
+ }
@@ -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.json().catch(() => null);
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "teapot-coding-agent",
3
- "version": "0.12.1",
3
+ "version": "0.13.1",
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",
@@ -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}