teapot-coding-agent 0.26.14 → 0.27.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -6,8 +6,6 @@
6
6
 
7
7
  [![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/akku1139/teapot)
8
8
 
9
- [日本語版はこちら](README.ja.md)
10
-
11
9
  ---
12
10
 
13
11
  ## 🚀 Quick start
@@ -167,7 +165,7 @@ Agents can run `bash` and rewrite files — including `rm` and `npm install`.
167
165
 
168
166
  - Designed to run under a **dedicated Linux user**
169
167
  - `git commit` before handing over a repo you care about
170
- - File operations outside the workspace are rejected (`safeJoin`)
168
+ - File operations outside the workspace are rejected (`safeJoin`), but agents can use `bash` outside of the worktree
171
169
  - teapot listens on localhost only by default. If you expose it, set **both** `--host 0.0.0.0` and an API token (`TEAPOT_API_TOKEN` env or config `password`):
172
170
 
173
171
  ```sh
@@ -1227,23 +1227,48 @@ export class Agent {
1227
1227
  }
1228
1228
  if (p.source === "user") {
1229
1229
  const id = p.id ?? `u${Date.now().toString(36)}${Math.random().toString(36).slice(2, 6)}`;
1230
- this.deliveredPrompts.push(id);
1231
- // cap: UI only needs recent confirmations
1232
- if (this.deliveredPrompts.length > 64)
1233
- this.deliveredPrompts.shift();
1234
- void this.log
1235
- .append("system_note", this.currentSession, this.currentBranch, {
1236
- event: "prompt-delivered",
1237
- promptId: id,
1238
- preview: p.text.slice(0, 80),
1239
- })
1240
- .then(() => bus.emit("update", { kind: "agent-update", agentId: this.opts.id }))
1241
- .catch(() => { });
1230
+ // #37: queued here, MARKED at the request boundary instead — see
1231
+ // markDeliveredPrompts(). Text is kept so the marker can carry the same
1232
+ // preview the UI shows.
1233
+ this.undeliveredPrompts.push({ id, preview: p.text.slice(0, 80) });
1242
1234
  }
1243
1235
  }
1244
1236
  }
1245
1237
  /** ids of user prompts that have entered an LLM call payload (recent first-capped) */
1246
1238
  deliveredPrompts = [];
1239
+ /**
1240
+ * User prompts consumed from the queue but not yet MARKED as delivered (#37).
1241
+ *
1242
+ * They are drained at a turn boundary, but the `prompt-delivered` marker must
1243
+ * wait until the request that carries them actually starts — which is later,
1244
+ * and after `maybeCompact()`, which can itself call the LLM.
1245
+ */
1246
+ undeliveredPrompts = [];
1247
+ /**
1248
+ * Log `prompt-delivered` for every prompt whose outbound request is starting.
1249
+ *
1250
+ * #37: the marker's meaning is "the request carrying this prompt has begun".
1251
+ * It used to fire in drainPendingPrompts, i.e. before the request, so the
1252
+ * timeline reordered by a moment the API never saw.
1253
+ */
1254
+ async markDeliveredPrompts() {
1255
+ if (this.undeliveredPrompts.length === 0)
1256
+ return;
1257
+ const pending = this.undeliveredPrompts;
1258
+ this.undeliveredPrompts = [];
1259
+ for (const { id, preview } of pending) {
1260
+ this.deliveredPrompts.push(id);
1261
+ // cap: the UI only needs recent confirmations
1262
+ if (this.deliveredPrompts.length > 64)
1263
+ this.deliveredPrompts.shift();
1264
+ await this.log.append("system_note", this.currentSession, this.currentBranch, {
1265
+ event: "prompt-delivered",
1266
+ promptId: id,
1267
+ preview,
1268
+ });
1269
+ }
1270
+ bus.emit("update", { kind: "agent-update", agentId: this.opts.id });
1271
+ }
1247
1272
  /**
1248
1273
  * Background jobs that exited since the last LLM call. Their outcomes are
1249
1274
  * folded into ONE harness message at the turn boundary — the agent learns
@@ -1366,9 +1391,25 @@ export class Agent {
1366
1391
  stopSeq = 0;
1367
1392
  /** #95: set when this round already injected a harness progress request */
1368
1393
  progressRequestedThisRound = false;
1394
+ /**
1395
+ * CONTROL PLANE: fired on every status change, immediately, independent of the
1396
+ * log.
1397
+ *
1398
+ * #126: `wait_children` wakeups used to ride `EventLog.onEvent`, so a status
1399
+ * change reached the parent only AFTER the write completed. After the
1400
+ * write-before-notify fix that meant a FAILED or slow write left a parked parent
1401
+ * waiting forever — persistence latency became control flow. Measured with every
1402
+ * write failing: `onEvent fired: 0`, so no wakeup at all.
1403
+ *
1404
+ * Control flow must not depend on durability, so this is separate from the
1405
+ * event pipeline. The log's `onEvent` now means "persisted, tell the world";
1406
+ * this means "my state changed, react now".
1407
+ */
1408
+ onControlStatusChange = null;
1369
1409
  setStatus(s, reason = "") {
1370
- // Update the field FIRST. log.append fires onEvent synchronously, and the
1371
- // master's child-event hook wakes parked wait_children callers — those
1410
+ // Update the field FIRST. The control-plane hook fires synchronously below
1411
+ // (see onControlStatusChange), and it is what wakes parked wait_children
1412
+ // callers — those
1372
1413
  // re-check agent.status at that instant. Updating after the append made
1373
1414
  // every waiter observe the OLD status ("running"), conclude "someone is
1374
1415
  // still working", and stay parked until timeout even though the child had
@@ -1377,6 +1418,15 @@ export class Agent {
1377
1418
  this.status = s;
1378
1419
  this.statusReason = reason;
1379
1420
  if (prev !== s) {
1421
+ // #126: control plane FIRST, and not via the log. A parent's wait_children
1422
+ // must wake on the status change itself, not on the durability of the write
1423
+ // that records it.
1424
+ try {
1425
+ this.onControlStatusChange?.(this.opts.id, s);
1426
+ }
1427
+ catch {
1428
+ /* a control-plane listener must never break a status change */
1429
+ }
1380
1430
  void this.log.append("state", this.currentSession, this.currentBranch, {
1381
1431
  from: prev,
1382
1432
  to: s,
@@ -1591,6 +1641,27 @@ export class Agent {
1591
1641
  text: "",
1592
1642
  reasoning: "",
1593
1643
  });
1644
+ // #37: `prompt-delivered` must mean "the outbound request CARRYING this
1645
+ // prompt has started", not "the prompt moved from the queue into
1646
+ // `this.messages`".
1647
+ //
1648
+ // It used to be logged in drainPendingPrompts, which runs BEFORE
1649
+ // refreshSkills, maybeRequestProgress and — critically — maybeCompact. So
1650
+ // the marker preceded the request, and `maybeCompact()` can itself make an
1651
+ // LLM call, which meant the timeline claimed a delivery order the API never
1652
+ // saw. Measured before the fix:
1653
+ //
1654
+ // LLM CALL #1
1655
+ // seq=5 prompt-delivered("first prompt")
1656
+ // seq=6 llm turn start
1657
+ //
1658
+ // The UI reorders by this marker (resequenceToDelivery), so the semantics
1659
+ // were the bug — not timeline-order.ts.
1660
+ //
1661
+ // Emitted ONCE per request, not per attempt: a retry sends the same payload,
1662
+ // so re-emitting would move the marker after a later event on each try.
1663
+ if (attempt === 1)
1664
+ await this.markDeliveredPrompts();
1594
1665
  try {
1595
1666
  return await this.callLlm(messages, tools, onDelta);
1596
1667
  }
@@ -1192,8 +1192,18 @@ export const TOOLS = [
1192
1192
  return stale;
1193
1193
  let oldText = str(args.old_text);
1194
1194
  const newText = str(args.new_text);
1195
+ // #126: the single most common edit_file failure in the real logs (22
1196
+ // occurrences) said only "old_text is required", while the tool's own
1197
+ // schema already documents the better form. Name it and say what to do —
1198
+ // the pattern master.ts uses for "not your sub-agent" (master.ts:979),
1199
+ // which turned a confusing refusal into a re-addressable one.
1195
1200
  if (!oldText)
1196
- return { ok: false, result: "old_text is required" };
1201
+ return {
1202
+ ok: false,
1203
+ result: "old_text is required — pass the exact text to replace, or read with " +
1204
+ '`read_file({line_ids:"hash"})` and pass that block VERBATIM as old_text. ' +
1205
+ "Do NOT copy read_file's `N| ` line-number prefixes.",
1206
+ };
1197
1207
  const replaceAll = args.replace_all === true;
1198
1208
  // #7/#4: accept a block copied straight out of read_file's hashline
1199
1209
  // output. When the anchors do not verify we FAIL with the specific reason
@@ -89,12 +89,25 @@ export class EventLog {
89
89
  data,
90
90
  };
91
91
  this.lastByBranch.set(branch, evt.id);
92
- try {
93
- this.onEvent?.(evt);
94
- }
95
- catch {
96
- /* observer must never break the log */
97
- }
92
+ // #54: WRITE FIRST, then notify.
93
+ //
94
+ // The observer used to fire BEFORE the write. The frontend receives a
95
+ // `kind:"event"` frame and — by design — treats it as "something changed"
96
+ // and re-reads `/events`. So the reload could read the log BEFORE this
97
+ // event reached disk, and the reload would simply not contain it.
98
+ //
99
+ // Measured against a real EventLog, reloading on every notification:
100
+ //
101
+ // appends: 30 WS-triggered reloads that MISSED the event: 30
102
+ //
103
+ // The 120ms debounce usually hides this, which is exactly why #54 felt
104
+ // random: a slow write misses, a fast one lands. Notifying after the write
105
+ // makes the event visible to any reader the observer triggers.
106
+ // `written` distinguishes a REAL write from the post-close drop path, which
107
+ // also resolves. #54: announcing a dropped event would tell the frontend to
108
+ // reload for something that will never appear — the row would flicker in and
109
+ // straight back out.
110
+ let written = false;
98
111
  const p = new Promise((resolve, reject) => {
99
112
  this.chain = this.chain.then(() => {
100
113
  if (!this.stream) {
@@ -103,10 +116,31 @@ export class EventLog {
103
116
  console.error(`[teapot] event dropped after log close (${this.agentId}): ${type}`);
104
117
  return resolve(evt);
105
118
  }
106
- this.stream.write(JSON.stringify(evt) + "\n", "utf8", (err) => err ? reject(err) : resolve(evt));
119
+ this.stream.write(JSON.stringify(evt) + "\n", "utf8", (err) => {
120
+ if (err)
121
+ return reject(err);
122
+ written = true;
123
+ resolve(evt);
124
+ });
107
125
  }, () => resolve(evt));
108
126
  });
109
127
  this.chain = this.chain.then(() => { }, () => { });
128
+ // notify once the write is handed to the OS, so a reload the observer
129
+ // triggers can actually see this event
130
+ void p
131
+ .then(() => {
132
+ if (!written)
133
+ return; // dropped after close: nothing on disk to announce
134
+ try {
135
+ this.onEvent?.(evt);
136
+ }
137
+ catch {
138
+ /* observer must never break the log */
139
+ }
140
+ })
141
+ .catch(() => {
142
+ /* a failed write must not notify — the event is not on disk */
143
+ });
110
144
  return p;
111
145
  }
112
146
  lastEventId(branch) {
package/dist/master.js CHANGED
@@ -524,6 +524,28 @@ export class Master {
524
524
  bus.emit("update", { kind: "event", agentId: e.agent, event: e });
525
525
  this.onChildEvent(ac, e);
526
526
  };
527
+ // #126: the CONTROL PLANE, separate from the event pipeline above.
528
+ //
529
+ // `onChildEvent` used to be what woke a parent's `wait_children`, and it runs
530
+ // after the event is persisted. So a failed or slow write left the parent
531
+ // parked forever — measured with every write failing, the wake never fired.
532
+ //
533
+ // A status change is CONTROL FLOW, not an observation, so it gets its own
534
+ // immediate path. `onChildEvent` keeps its other job (mirroring the child's
535
+ // activity into the parent's timeline, forwarding its final report), which
536
+ // legitimately belongs to the event stream.
537
+ agent.onControlStatusChange = (agentId, status) => {
538
+ if (!ac.parent)
539
+ return;
540
+ const parent = this.agents.get(ac.parent);
541
+ if (!parent)
542
+ return;
543
+ // the waiter re-checks whether ITS targets are all settled, so waking on
544
+ // every movement is correct and cheap
545
+ this.wakeParentWaiters(ac.parent);
546
+ if (status === "idle" || status === "stopped" || status === "waiting" || status === "error")
547
+ parent.enqueuePrompt(`[harness] @${agentId} is now ${status}. Continue your task, or finish() if the work is done.`, "harness");
548
+ };
527
549
  // sub-agent management hooks — only when this agent can legally spawn
528
550
  if (myDepth < this.maxSpawnDepth()) {
529
551
  const self = agent;
@@ -842,9 +864,18 @@ When this task is done (or truly blocked), call finish() with a summary for @${p
842
864
  // silently dropped exactly the details the operator spawned the sub
843
865
  // to gather. Context management is compaction's job, not the pipe's.
844
866
  `[harness] Sub-agent ${ac.id} finished. Final report:\n${report || "(no summary)"}`, "harness");
867
+ // #123: a parent's report is delivered INTO a live agent — queueing it is
868
+ // not enough. If the parent is idle (finished, or parked) the report sat in
869
+ // the queue until something else started the loop, so the operator saw a
870
+ // finished sub-agent and a parent that never reacted.
871
+ if (parent.status !== "running")
872
+ parent.start(`sub-agent ${ac.id} finished`);
845
873
  }
846
874
  else if (e.type === "error") {
847
875
  parent.enqueuePrompt(`[harness] Sub-agent ${ac.id} hit an error: ${String(e.data.message ?? "").slice(0, 500)}`, "harness");
876
+ // #123: same for an error report
877
+ if (parent.status !== "running")
878
+ parent.start(`sub-agent ${ac.id} error`);
848
879
  }
849
880
  // #63: a child's activity is NOT mirrored into the parent's log.
850
881
  //
@@ -525,14 +525,18 @@ export function buildApp(master) {
525
525
  app.get("/api/agents/:id/tree", async (c) => {
526
526
  const a = master.agents.get(c.req.param("id"));
527
527
  if (!a)
528
- return c.json({ error: "not found" }, 404);
528
+ return notFound(c, "agent", c.req.param("id"));
529
529
  const rel = c.req.query("path") || ".";
530
530
  let abs;
531
531
  try {
532
532
  abs = safeJoin(a.workspace, rel);
533
533
  }
534
534
  catch (err) {
535
- return c.json({ error: err.message }, 400);
535
+ // #126: safeJoin's message embeds the RESOLVED absolute path
536
+ // ("path escapes workspace: /home/user/secret/project/…"), so returning it
537
+ // verbatim hands the caller the server's directory layout. The rejection is
538
+ // correct; the disclosure was not. Echo back the path AS GIVEN.
539
+ return c.json({ error: `path is outside the workspace: ${rel}` }, 400);
536
540
  }
537
541
  let dirents;
538
542
  try {
@@ -580,7 +584,7 @@ export function buildApp(master) {
580
584
  app.get("/api/agents/:id/file", async (c) => {
581
585
  const a = master.agents.get(c.req.param("id"));
582
586
  if (!a)
583
- return c.json({ error: "not found" }, 404);
587
+ return notFound(c, "agent", c.req.param("id"));
584
588
  const rel = c.req.query("path") ?? "";
585
589
  if (!rel.trim())
586
590
  return c.json({ error: "path required" }, 400);
@@ -611,7 +615,7 @@ export function buildApp(master) {
611
615
  app.get("/api/agents/:id/raw", async (c) => {
612
616
  const a = master.agents.get(c.req.param("id"));
613
617
  if (!a)
614
- return c.json({ error: "not found" }, 404);
618
+ return notFound(c, "agent", c.req.param("id"));
615
619
  const rel = c.req.query("path") ?? "";
616
620
  if (!rel.trim())
617
621
  return c.json({ error: "path required" }, 400);
@@ -649,7 +653,7 @@ export function buildApp(master) {
649
653
  app.put("/api/agents/:id/file", async (c) => {
650
654
  const a = master.agents.get(c.req.param("id"));
651
655
  if (!a)
652
- return c.json({ error: "not found" }, 404);
656
+ return notFound(c, "agent", c.req.param("id"));
653
657
  const rel = c.req.query("path") ?? "";
654
658
  if (!rel.trim())
655
659
  return c.json({ error: "path required" }, 400);
@@ -755,20 +759,20 @@ export function buildApp(master) {
755
759
  });
756
760
  app.get("/api/agents/:id", (c) => {
757
761
  const a = master.agents.get(c.req.param("id"));
758
- return a ? c.json(a.snapshot()) : c.json({ error: "not found" }, 404);
762
+ return a ? c.json(a.snapshot()) : notFound(c, "agent", c.req.param("id"));
759
763
  });
760
764
  // lazy session load: stopped (not restored) → idle, history rebuilt from the log
761
765
  app.post("/api/agents/:id/load", async (c) => {
762
766
  const a = master.agents.get(c.req.param("id"));
763
767
  if (!a)
764
- return c.json({ error: "not found" }, 404);
768
+ return notFound(c, "agent", c.req.param("id"));
765
769
  await a.load();
766
770
  return c.json({ ok: true, agent: a.snapshot() });
767
771
  });
768
772
  app.post("/api/agents/:id/prompt", async (c) => {
769
773
  const a = master.agents.get(c.req.param("id"));
770
774
  if (!a)
771
- return c.json({ error: "not found" }, 404);
775
+ return notFound(c, "agent", c.req.param("id"));
772
776
  const body = await c.req
773
777
  .json()
774
778
  .catch(() => null); // #110
@@ -797,7 +801,7 @@ export function buildApp(master) {
797
801
  app.post("/api/agents/:id/prompt/cancel", async (c) => {
798
802
  const a = master.agents.get(c.req.param("id"));
799
803
  if (!a)
800
- return c.json({ error: "not found" }, 404);
804
+ return notFound(c, "agent", c.req.param("id"));
801
805
  const body = await c.req.json().catch(() => null); // #110
802
806
  if (!body)
803
807
  return c.json({ error: "invalid JSON" }, 400);
@@ -811,7 +815,7 @@ export function buildApp(master) {
811
815
  app.post("/api/agents/:id/start", (c) => {
812
816
  const a = master.agents.get(c.req.param("id"));
813
817
  if (!a)
814
- return c.json({ error: "not found" }, 404);
818
+ return notFound(c, "agent", c.req.param("id"));
815
819
  a.start("api start");
816
820
  return c.json({ ok: true });
817
821
  });
@@ -874,14 +878,14 @@ export function buildApp(master) {
874
878
  app.post("/api/agents/:id/stop", (c) => {
875
879
  const a = master.agents.get(c.req.param("id"));
876
880
  if (!a)
877
- return c.json({ error: "not found" }, 404);
881
+ return notFound(c, "agent", c.req.param("id"));
878
882
  a.stop("stopped via api");
879
883
  return c.json({ ok: true });
880
884
  });
881
885
  app.post("/api/agents/:id/goal", async (c) => {
882
886
  const a = master.agents.get(c.req.param("id"));
883
887
  if (!a)
884
- return c.json({ error: "not found" }, 404);
888
+ return notFound(c, "agent", c.req.param("id"));
885
889
  const body = await c.req.json()
886
890
  .catch(() => null); // #110
887
891
  if (!body)
@@ -911,7 +915,7 @@ export function buildApp(master) {
911
915
  app.get("/api/agents/:id/skills", async (c) => {
912
916
  const a = master.agents.get(c.req.param("id"));
913
917
  if (!a)
914
- return c.json({ error: "not found" }, 404);
918
+ return notFound(c, "agent", c.req.param("id"));
915
919
  try {
916
920
  const list = await currentSkills(a.toolCtx);
917
921
  return c.json({
@@ -926,7 +930,7 @@ export function buildApp(master) {
926
930
  app.post("/api/agents/:id/compact", async (c) => {
927
931
  const a = master.agents.get(c.req.param("id"));
928
932
  if (!a)
929
- return c.json({ error: "not found" }, 404);
933
+ return notFound(c, "agent", c.req.param("id"));
930
934
  try {
931
935
  return c.json({ ok: true, ...(await a.compactNow()) });
932
936
  }
@@ -954,7 +958,7 @@ export function buildApp(master) {
954
958
  app.post("/api/agents/:id/auto-continue", async (c) => {
955
959
  const a = master.agents.get(c.req.param("id"));
956
960
  if (!a)
957
- return c.json({ error: "not found" }, 404);
961
+ return notFound(c, "agent", c.req.param("id"));
958
962
  const body = await c.req.json().catch(() => null);
959
963
  if (!body || typeof body.value !== "boolean")
960
964
  return c.json({ error: "boolean value required" }, 400);
@@ -970,7 +974,7 @@ export function buildApp(master) {
970
974
  app.post("/api/agents/:id/auto-compact", async (c) => {
971
975
  const a = master.agents.get(c.req.param("id"));
972
976
  if (!a)
973
- return c.json({ error: "not found" }, 404);
977
+ return notFound(c, "agent", c.req.param("id"));
974
978
  const body = await c.req.json().catch(() => null);
975
979
  if (!body || typeof body.value !== "boolean")
976
980
  return c.json({ error: "boolean value required" }, 400);
@@ -990,7 +994,7 @@ export function buildApp(master) {
990
994
  app.post("/api/agents/:id/spawn", async (c) => {
991
995
  const a = master.agents.get(c.req.param("id"));
992
996
  if (!a)
993
- return c.json({ error: "not found" }, 404);
997
+ return notFound(c, "agent", c.req.param("id"));
994
998
  const body = await c.req
995
999
  .json()
996
1000
  .catch(() => null);
@@ -1013,7 +1017,7 @@ export function buildApp(master) {
1013
1017
  app.post("/api/agents/:id/stop-children", async (c) => {
1014
1018
  const a = master.agents.get(c.req.param("id"));
1015
1019
  if (!a)
1016
- return c.json({ error: "not found" }, 404);
1020
+ return notFound(c, "agent", c.req.param("id"));
1017
1021
  const body = await c.req.json().catch(() => ({ ids: undefined }));
1018
1022
  try {
1019
1023
  const r = await master.stopChildrenFor(c.req.param("id"), body.ids);
@@ -1027,20 +1031,26 @@ export function buildApp(master) {
1027
1031
  app.post("/api/agents/:id/todo", async (c) => {
1028
1032
  const a = master.agents.get(c.req.param("id"));
1029
1033
  if (!a)
1030
- return c.json({ error: "not found" }, 404);
1034
+ return notFound(c, "agent", c.req.param("id"));
1031
1035
  const body = await c.req.json().catch(() => null);
1032
1036
  if (!body)
1033
1037
  return c.json({ error: "invalid JSON" }, 400);
1034
1038
  await a.setTodo(body.text ?? "");
1035
1039
  if (body.notify !== false && body.text?.trim())
1036
1040
  a.enqueuePrompt(`[harness] The operator updated the task list:\n\n${body.text}\n\nWork through it (get_todo() always has the latest).`, "harness");
1041
+ // #123: an idle agent must actually START on the queued prompt. The goal
1042
+ // route above learned this (the queued prompt sat forever otherwise); the
1043
+ // todo route did not, so saving a task list on an idle session looked like
1044
+ // it had been ignored.
1045
+ if (a.status !== "running")
1046
+ a.start("todo set");
1037
1047
  return c.json({ ok: true });
1038
1048
  });
1039
1049
  // edit a previously-sent prompt: forks there, optionally summarizes the tail
1040
1050
  app.post("/api/agents/:id/edit-prompt", async (c) => {
1041
1051
  const a = master.agents.get(c.req.param("id"));
1042
1052
  if (!a)
1043
- return c.json({ error: "not found" }, 404);
1053
+ return notFound(c, "agent", c.req.param("id"));
1044
1054
  const body = await c.req
1045
1055
  .json()
1046
1056
  .catch(() => null);
@@ -1066,7 +1076,7 @@ export function buildApp(master) {
1066
1076
  app.post("/api/agents/:id/fork", async (c) => {
1067
1077
  const a = master.agents.get(c.req.param("id"));
1068
1078
  if (!a)
1069
- return c.json({ error: "not found" }, 404);
1079
+ return notFound(c, "agent", c.req.param("id"));
1070
1080
  const body = await c.req.json().catch(() => ({ fromEvent: null }));
1071
1081
  const r = await a.fork(body.fromEvent ?? null);
1072
1082
  return c.json({ ok: true, ...r });
@@ -1096,7 +1106,7 @@ export function buildApp(master) {
1096
1106
  app.get("/api/agents/:id/events", async (c) => {
1097
1107
  const a = master.agents.get(c.req.param("id"));
1098
1108
  if (!a)
1099
- return c.json({ error: "not found" }, 404);
1109
+ return notFound(c, "agent", c.req.param("id"));
1100
1110
  // #54: the frontend pairs a tool_call with its tool_result by walking the
1101
1111
  // events it holds, so a window that can contain a result WITHOUT its call
1102
1112
  // strands the bash row at "waiting for output…" forever — the call scrolled
@@ -1113,7 +1123,7 @@ export function buildApp(master) {
1113
1123
  if (sessionId) {
1114
1124
  const rec = master.sessionById(sessionId);
1115
1125
  if (!rec || rec.agentId !== c.req.param("id"))
1116
- return c.json({ error: "not found" }, 404);
1126
+ return notFound(c, "session (or it belongs to another agent)", sessionId);
1117
1127
  filePath = path.join(rec.dir, "chat.jsonl");
1118
1128
  }
1119
1129
  // #59: the plain "latest N" request is what the UI polls every ~120 ms for
@@ -1152,13 +1162,13 @@ export function buildApp(master) {
1152
1162
  const known = master.agents.has(id) || master.config.agents.some((a) => a.id === id);
1153
1163
  const owned = master.sessionsOf(id);
1154
1164
  if (!known && owned.length === 0)
1155
- return c.json({ error: "not found" }, 404);
1165
+ return notFound(c, "agent", id);
1156
1166
  return c.json({ sessions: owned });
1157
1167
  });
1158
1168
  app.get("/api/agents/:id/branches", async (c) => {
1159
1169
  const a = master.agents.get(c.req.param("id"));
1160
1170
  if (!a)
1161
- return c.json({ error: "not found" }, 404);
1171
+ return notFound(c, "agent", c.req.param("id"));
1162
1172
  // cached like /events — this used to bypass it and re-read the whole log
1163
1173
  const events = await readEventsCached(a.log.filePath);
1164
1174
  const branches = new Map();
@@ -1190,6 +1200,16 @@ export function buildApp(master) {
1190
1200
  // ---- metrics / SSE ----
1191
1201
  app.get("/api/metrics", (c) => c.json(master.metrics()));
1192
1202
  // current server version (for live-update polling)
1203
+ /**
1204
+ * A 404 that says WHICH thing was missing.
1205
+ *
1206
+ * #126: there were 24 hand-copied `c.json({ error: "not found" }, 404)` lines.
1207
+ * From a client they are indistinguishable — a typo in an agent id, a deleted
1208
+ * session, or a bad route all produce the same three words, and none carries
1209
+ * the id that was looked up. The id is the first thing anyone debugging this
1210
+ * needs, and it is already in hand.
1211
+ */
1212
+ const notFound = (c, what, id) => c.json({ error: `${what} not found: ${id || "(no id given)"}` }, 404);
1193
1213
  app.get("/api/version", (c) => c.json({ version: master.getVersion() }));
1194
1214
  // trigger a full server restart (agents stopped gracefully, new process spawned)
1195
1215
  app.post("/api/update/restart", async (c) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "teapot-coding-agent",
3
- "version": "0.26.14",
3
+ "version": "0.27.0",
4
4
  "description": "A lightweight, always-on multi-agent harness for AI coding agents",
5
5
  "type": "module",
6
6
  "license": "AGPL-3.0-or-later",
@@ -0,0 +1 @@
1
+ :root{--bg-darkest:#1a1c22;--bg-dark:#232630;--bg-mid:#2b2e39;--bg-light:#343845;--fg:#dcdee4;--dim:#9298a5;--line:#1d2027;--acc:#5865f2;--ok:#3ba55d;--warn:#faa81a;--err:#ed4245;--tool:#3ba0c9;--term-bg:#0d0e12;--term-fg:#dcdee4;font-family:gg sans,ui-sans-serif,system-ui,-apple-system,Segoe UI,sans-serif}[data-theme=midnight]{--bg-darkest:#0d0f18;--bg-dark:#12141f;--bg-mid:#1b1f31;--bg-light:#252a42;--fg:#dfe2f2;--dim:#8a90ad;--line:#0a0c14;--acc:#8b7cf7;--ok:#54c08a;--warn:#e5b567;--err:#ef6a6a;--tool:#6aa9e9;--term-bg:#0a0c13;--term-fg:#dfe2f2}[data-theme=tea]{--bg-darkest:#241c16;--bg-dark:#2e241c;--bg-mid:#3b2f24;--bg-light:#493b2d;--fg:#f0e4d2;--dim:#b39d82;--line:#1b140f;--acc:#d98e32;--ok:#7fa653;--warn:#e0b058;--err:#cf5b47;--tool:#c9975f;--term-bg:#1d1712;--term-fg:#ecdcc3}[data-theme=coffee]{--bg-darkest:#191410;--bg-dark:#211a14;--bg-mid:#30271e;--bg-light:#403527;--fg:#ede3d0;--dim:#a89478;--line:#120d07;--acc:#c68a4b;--ok:#8aa15c;--warn:#d9a441;--err:#c25450;--tool:#a67c52;--term-bg:#15100c;--term-fg:#e8dcc5}[data-theme=matcha]{--bg-darkest:#1b2217;--bg-dark:#232b1e;--bg-mid:#2f3a28;--bg-light:#3c4a33;--fg:#e8eeda;--dim:#9aab84;--line:#131a10;--acc:#8db64e;--ok:#6fae4e;--warn:#d8b13c;--err:#cd5c50;--tool:#5da88f;--term-bg:#151b11;--term-fg:#dfe8cb}[data-theme=matrix]{--bg-darkest:#060a06;--bg-dark:#0a0f0a;--bg-mid:#122116;--bg-light:#18301d;--fg:#c9f2cf;--dim:#6fa075;--line:#030603;--acc:#2fd558;--ok:#39c95f;--warn:#b8d977;--err:#ff6b6b;--tool:#57c99b;--term-bg:#030503;--term-fg:#3f7}[data-theme=sunset]{--bg-darkest:#1c1422;--bg-dark:#241b2b;--bg-mid:#33253e;--bg-light:#452f52;--fg:#f2e6ea;--dim:#ab93a8;--line:#150e1b;--acc:#f2784b;--ok:#63b46f;--warn:#eda63d;--err:#e25563;--tool:#b06ac9;--term-bg:#170f1d;--term-fg:#f0dfe4}[data-theme=light]{--bg-darkest:#eceef4;--bg-dark:#f6f7fa;--bg-mid:#e2e5ee;--bg-light:#fff;--fg:#24272f;--dim:#5d6470;--line:#d8dbe4;--acc:#4757d8;--ok:#2b8a4b;--warn:#b97a0f;--err:#d83c3f;--tool:#2278a8;--term-bg:#fff;--term-fg:#24272f}[data-theme=strawberry]{--bg-darkest:#fbe4ea;--bg-dark:#fdf0f3;--bg-mid:#f7d4de;--bg-light:#fffafb;--fg:#4a2733;--dim:#96707e;--line:#f0ccd6;--acc:#e05575;--ok:#4d9e63;--warn:#c98a2d;--err:#d43d51;--tool:#b05687;--term-bg:#fff5f8;--term-fg:#4a2733}[data-theme=ramune]{--bg-darkest:#ddeff7;--bg-dark:#ecf7fb;--bg-mid:#c9e7f4;--bg-light:#fbfeff;--fg:#173342;--dim:#5b7d90;--line:#c2e2ef;--acc:#1d86ae;--ok:#3a9e6e;--warn:#cf9a2a;--err:#d64f5c;--tool:#3a9db8;--term-bg:#f4fbfd;--term-fg:#173342}*{box-sizing:border-box}html,body,#root{height:100%;margin:0}body{background:var(--bg-dark);color:var(--fg);font-size:15px}.layout{grid-template-columns:250px 1fr 370px;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,.layout .rightbarbackdrop{display:block}.rightbar .rightbarhead{z-index:1;display:flex;position:sticky;top:0}.rightbar{z-index:5;width:min(420px,92vw);box-shadow:none;transition:transform .18s,box-shadow .18s;position:fixed;top:0;bottom:0;right:0;transform:translate(100%)}.rightbar.open{transform:none;box-shadow:-8px 0 24px #0007}}.sidebar{background:var(--bg-darkest);flex-direction:column;padding:10px 8px;display:flex;overflow:hidden auto}.themepop{background:var(--bg-mid);border:1px solid var(--bg-light);border-radius:8px;flex-direction:column;gap:8px;padding:10px;display:flex;position:static}.trow{color:var(--fg);cursor:pointer;align-items:center;gap:6px;font-size:12.5px;display:flex}.trow select{background:var(--bg-darkest);color:var(--fg);border:none;border-radius:5px;outline:none;padding:3px 6px}.themegrid{grid-template-columns:1fr 1fr;gap:4px;display:grid}.themebtn{background:var(--bg-light);color:var(--fg);cursor:pointer;text-align:left;border:1px solid #0000;border-radius:6px;align-items:center;gap:6px;min-width:0;padding:5px 8px;font-size:12px;display:flex;overflow:hidden}.themebtn>:last-child{text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.themebtn:hover{filter:brightness(1.15)}.themebtn.on{border-color:var(--acc)}.swdots{flex-shrink:0;gap:2px;display:inline-flex}.swdots i{border-radius:50%;width:9px;height:9px;display:inline-block;box-shadow:inset 0 0 0 1px #0004}.agent-list{flex:1;min-width:0;min-height:0;overflow-y:auto}.wsheader{letter-spacing:.04em;text-transform:uppercase;color:var(--dim);cursor:pointer;-webkit-user-select:none;user-select:none;border-top:1px solid var(--line);align-items:center;gap:6px;margin-top:6px;padding:10px 10px 4px;font-size:11px;font-weight:600;display:flex}.wsheader:first-child{border-top:none;margin-top:0}.wsheader:hover{color:var(--fg)}.wsheader .wscaret{flex-shrink:0;width:12px;font-size:9px}.wsheader .wscount{background:var(--bg-light);color:var(--dim);letter-spacing:0;border-radius:999px;flex-shrink:0;margin-left:auto;padding:0 6px;font-size:10px;font-weight:600;line-height:15px}.agent-item{cursor:pointer;color:var(--fg);border-radius:6px;align-items:center;gap:8px;min-width:0;margin-bottom:2px;padding:7px 10px;display:flex}.agent-item>span:not(.dot):not(.caret):not(.caret-spacer):not(.notifbadge):not(.subcount):not(.subtag):not(.ghosttag):not(.mini-cron):not(.goaldone){text-overflow:ellipsis;white-space:nowrap;min-width:0;overflow:hidden}.agent-item .subtag{color:var(--dim);font-size:10px}.agent-item .goaldone{flex-shrink:0}.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.collapsed{cursor:pointer}.agent-item.sel{background:var(--bg-mid)}.agent-item.sub-row{color:var(--dim)}.agent-item.sub-row.sel{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}.sidebar-footer{border-top:1px solid var(--line);flex-direction:column;gap:2px;margin-top:auto;padding-top:8px;display:flex}.footer-row{align-items:center;gap:8px;display:flex}.footer-row .conn{margin-left:0}.sidebar-footer .metrics{border-top:none;margin-top:4px;padding:0}.brand{color:var(--fg);align-items:center;gap:8px;font-size:14px;display:flex}.version{color:var(--dim);font-family:ui-monospace,Menlo,monospace;font-size:11px}.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:.75}.msg.pending .ts{color:var(--warn);font-weight:600}.msg.pending .avatar{animation:1.6s ease-in-out infinite pulse}.msg.pending .editbtn.cancel{color:var(--err)}.msg.withdrawn{opacity:.55}.msg.withdrawn .msg-body{text-decoration:line-through;-webkit-text-decoration-color:var(--muted);text-decoration-color:var(--muted)}.msg.withdrawn .msg-head{text-decoration:none}.msg.withdrawn .ts{color:var(--muted);font-weight:600}.msg.grouped{padding-top:0}.avatar{border-radius:50%;flex-shrink:0;justify-content:center;align-items:center;width:38px;height:38px;margin-top:2px;font-size:17px;display:flex}.msg-body{flex:1;min-width:0}.msg-head{align-items:baseline;gap:8px;display:flex}.actor{color:var(--warn);white-space:nowrap;font-size:11px}.runcard{flex-direction:column;gap:8px;display:flex}.statgrid{grid-template-columns:repeat(3,1fr);gap:6px;display:grid}.stat{background:var(--bg-mid);border-radius:6px;flex-direction:column;gap:1px;min-width:0;padding:5px 8px;display:flex}.stat span{color:var(--dim);text-transform:uppercase;letter-spacing:.05em;font-size:10px}.stat b{text-overflow:ellipsis;font-size:15px;overflow:hidden}.statrow{align-items:center;gap:8px;min-width:0;font-size:12.5px;display:flex}.statrow .k{color:var(--dim);text-transform:uppercase;letter-spacing:.05em;white-space:nowrap;flex-shrink:0;width:auto;min-width:58px;font-size:10px}.pill{background:var(--bg-mid);white-space:nowrap;border-radius:999px;margin-left:auto;padding:1px 8px;font-size:11px}.pill.ok{color:var(--ok)}.pill.warn{color:var(--warn)}.pill.crit{color:var(--err);background:#ed42451a}.bartrack{background:var(--bg-light);border-radius:3px;height:6px;margin-top:2px;overflow:hidden}.barfill{background:var(--ok);border-radius:3px;min-width:2px;height:100%;transition:width .3s}.barfill.warn{background:var(--warn)}.barfill.crit{background:var(--err)}.author{font-size:14.5px;font-weight:600}.ts{color:var(--dim);font-size:11px}.content{white-space:pre-wrap;word-break:break-word;line-height:1.45}.content p{margin:2px 0}.content pre{background:var(--bg-darkest);border-radius:6px;padding:8px;overflow-x:auto}.content pre>code{white-space:pre-wrap;overflow-wrap:anywhere}.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}input[type=checkbox]{accent-color:var(--acc)}.content a{color:var(--tool);text-decoration:underline;-webkit-text-decoration-color:color-mix(in srgb, var(--tool) 45%, transparent);text-decoration-color:color-mix(in srgb, var(--tool) 45%, transparent);text-underline-offset:2px}.content a:hover{filter:brightness(1.15)}.themebtn.on{background:var(--acc);color:#fff}.trow select:focus{outline:1px solid var(--acc)}.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.warn-embed{border-color:var(--warn)}.embed.warn-embed .mono{color:var(--warn)}.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)}.qopt:disabled{opacity:.45;cursor:default;filter:none}.embed.question.answered{opacity:.75}.qfree{gap:6px;margin-top:8px;display:flex}.qfree input{background:var(--bg-mid);border:1px solid var(--bg-light);color:var(--fg);border-radius:6px;outline:none;flex:1;padding:4px 10px;font-size:13px}.qfree input:focus{border-color:var(--acc)}.qfree button{background:var(--bg-light);color:var(--fg);cursor:pointer;border:none;border-radius:6px;padding:4px 12px;font-size:12.5px}.qfree button:disabled{opacity:.45;cursor:default}.bashelapsed{color:var(--warn);margin-left:8px;font-size:11px}.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%;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}.embed summary .iconmeta{flex-shrink:0;font-size:11px}.toolcopy{flex-shrink:0;gap:4px;margin-left:auto;display:flex}.msgfoot{color:var(--dim);align-items:center;gap:6px;margin-top:4px;font-size:11px;display:flex}.imgstage{flex-wrap:wrap;gap:6px;margin-bottom:6px;display:flex}.imgchip{display:inline-flex;position:relative}.imgchip img{object-fit:cover;border:1px solid var(--line);border-radius:6px;width:64px;height:64px}.imgchip .imgx{background:var(--err);color:#fff;cursor:pointer;border:none;border-radius:50%;justify-content:center;align-items:center;width:16px;height:16px;padding:0;font-size:9px;line-height:1;display:inline-flex;position:absolute;top:-5px;right:-5px}.promptimgs{flex-wrap:wrap;gap:6px;margin-top:4px;display:flex}img.promptimg{border:1px solid var(--line);border-radius:6px;max-width:180px;max-height:120px}.bgjob-note{color:var(--warn)}.bgjob-note b{color:var(--fg);font-family:ui-monospace,Menlo,monospace}details.reasoning .thinkscroll{white-space:pre-wrap;word-break:break-word;scrollbar-gutter:stable;max-height:220px;overflow-y:auto}.thinktimer{color:var(--dim);margin-left:6px;font-size:11px;font-weight:400}.termdrawer{background:var(--term-bg);border-top:2px solid var(--line);flex-direction:column;display:flex}.termgrip{cursor:ns-resize;background:var(--bg-light);opacity:.6;border-bottom:1px solid var(--line);height:6px}.termgrip:hover{opacity:1}.termbar{color:var(--dim);border-bottom:1px solid var(--line);background:var(--bg-darkest);justify-content:space-between;align-items:center;padding:4px 10px;font-size:11.5px;display:flex}.termtabs{flex:1;align-items:center;gap:2px;min-width:0;display:flex;overflow-x:auto}.termtab{cursor:pointer;color:var(--dim);white-space:nowrap;background:var(--bg-mid);border-radius:5px;align-items:center;gap:6px;padding:2px 8px;font-size:11.5px;display:inline-flex}.termtab.active{color:var(--fg);outline:1px solid var(--acc)}.termtab:hover{filter:brightness(1.15)}.tabx{color:inherit;opacity:.5;cursor:pointer;background:0 0;border:none;padding:0;font-size:10px}.tabx:hover{opacity:1;color:var(--err)}.termbody{background:var(--term-bg);flex:1;gap:4px;min-height:0;padding:4px;display:flex}.termbody.split .termpane{width:50%}.termpane{background:var(--term-bg);border-radius:6px;flex:1;min-width:0;position:relative;overflow:hidden}.termpane:not(.focused){opacity:.9}.termpane.focused{box-shadow:inset 0 0 0 2px var(--acc)}.termhost{position:absolute;inset:0}.termsession{padding:4px 6px;position:absolute;inset:0}.termsplit{background:var(--line);cursor:col-resize;border-radius:2px;flex-shrink:0;width:3px}.termempty{color:var(--dim);place-items:center;font-size:12px;display:grid;position:absolute;inset:0}.jump{background:var(--acc);color:#fff;cursor:pointer;z-index:2;border:none;border-radius:999px;padding:6px 14px;font-size:12.5px;font-weight:600;position:absolute;top:-34px;left:50%;transform:translate(-50%);box-shadow:0 4px 14px #0008}body:has(.termdrawer) .jump{top:auto;bottom:calc(var(--termh,300px) + 56px);left:50%}.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)}.divider-msg.compacting{color:var(--acc);animation:1.6s ease-in-out infinite pulse}.goalline{color:var(--dim);padding:4px 18px;font-size:11.5px}.goalline-label{color:var(--dim);display:block}.goalline.warn .goalline-label{color:var(--warn)}.goalline.err .goalline-label{color:var(--err)}.goalline .card{text-align:left;margin-top:6px}.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}.rightbarbackdrop{z-index:4;background:#0004;display:none;position:fixed;inset:0}.rightbarhead{background:var(--bg-dark);border-bottom:1px solid var(--line);justify-content:space-between;align-items:center;gap:8px;margin:-14px -14px 0;padding:10px 14px;display:none}.rightbarhead .muted{text-transform:uppercase;letter-spacing:.04em;font-size:11px}.rightbar{background:var(--bg-dark);border-left:2px solid var(--line);scrollbar-gutter:stable;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-of-type{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}.card.runcard{max-height:none;overflow-y:visible}.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}.embed .progrow{gap:8px;margin-bottom:4px;display:flex}.embed .progrow b{color:var(--dim);text-transform:uppercase;letter-spacing:.05em;flex-shrink:0;min-width:62px;padding-top:2px;font-size:10px}.embed .progrow span{min-width:0}.inline-md{display:inline}.inline-md>:first-child{margin-top:0}.inline-md>:last-child{margin-bottom:0}.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,.msg.pending .msg-head .editbtn{opacity:1}.msg-head .editbtn:focus-visible{opacity:1;outline:2px solid var(--acc);outline-offset:1px}.msg-head .editbtn:hover{color:var(--acc)}.branch-row{color:var(--dim);cursor:pointer;justify-content:space-between;padding:3px 0;font-size:12px;display:flex}.branch-row:hover,.branch-row.cur{color:var(--fg)}.mini-cron{opacity:.75;margin-left:auto;font-size:10px}.sched-row{cursor:pointer;border-radius:6px;margin-bottom:2px;padding:6px 8px}.sched-row:hover,.sched-row.cur{background:var(--bg-mid)}.sched-top{align-items:baseline;gap:6px;font-size:12.5px;display:flex}.sched-row .sched-meta{color:var(--dim);margin-top:2px;font-size:11px}.sched-row .sched-prompt{color:var(--dim);text-overflow:ellipsis;white-space:nowrap;margin-top:2px;font-size:11.5px;overflow:hidden}.badge.cron{color:var(--tool);white-space:nowrap;background:#3ba0c91a;border:1px solid #3ba0c944}.sesscard{flex-direction:column;gap:6px;font-size:12.5px;display:flex}.sessrow{align-items:center;gap:8px;display:flex}.filebox{background:var(--bg-darkest);border-radius:8px;height:260px;padding:4px;overflow-y:auto}.filerow{cursor:pointer;color:var(--fg);border-radius:4px;align-items:center;gap:6px;padding:3px 8px;font-size:12.5px;display:flex}.filerow:hover{background:var(--bg-mid)}.filerow .ficon{width:10px;color:var(--dim);text-align:center;flex-shrink:0;font-size:9px}.filerow.isdir .fname{font-weight:600}.fname{text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.fsize{color:var(--dim);flex-shrink:0;margin-left:auto;font-size:10px}.sessrow .k{color:var(--dim);text-transform:uppercase;letter-spacing:.05em;flex-shrink:0;width:74px;font-size:10px}.ellip{text-overflow:ellipsis;white-space:nowrap;text-align:left;direction:rtl;overflow:hidden}.modelbox{background:var(--bg-darkest);border-radius:8px;flex-direction:column;gap:6px;padding:10px;display:flex}.modelbox select,.modelbox input[type=text]{background:var(--bg-mid);color:var(--fg);font:inherit;border:none;border-radius:6px;outline:none;min-width:0;padding:6px 8px}.modelbox button{background:var(--acc);color:#fff;cursor:pointer;white-space:nowrap;border:none;border-radius:6px;padding:6px 10px;font-size:12.5px;font-weight:600}.modelbox button:hover{opacity:.9}.modelbox .meta{color:var(--dim);font-size:11px}.btnrow{gap:6px;margin:8px 0;display:flex}.btnrow button{background:var(--bg-light);color:var(--fg);cursor:pointer;border:none;border-radius:6px;padding:6px 10px;font-size:13px}.btnrow button:hover{filter:brightness(1.2)}.btnrow .runbtn{background:var(--ok);color:#fff;font-weight:600}.btnrow .runbtn,.btnrow .danger{min-width:74px}.btnrow .danger{background:var(--err);color:#fff;font-weight:600}.ctrlrow{align-items:center;gap:8px;margin-top:6px;font-size:12.5px;display:flex}.ctrlrow label{cursor:pointer;color:var(--fg);white-space:nowrap;align-items:center;gap:5px;display:flex}.ctrlrow .muted{font-size:11.5px}.goalstate{background:var(--bg-light);color:var(--fg);cursor:pointer;border:none;border-radius:6px;padding:3px 10px;font-size:12px}.goalstate.on.active{background:var(--ok);color:#fff;font-weight:600}.goalstate.on.done{background:var(--acc);color:#fff;font-weight:600}.goalstate.on.paused{background:var(--warn);color:#fff;font-weight:600}.goalstate:disabled{cursor:default;filter:none}.verifycard{border-left:3px solid var(--tool);margin-top:6px}.auditcard{border-left:3px solid var(--warn);margin-top:6px}.auditcard.ok{border-left-color:var(--ok)}.vtitle{color:var(--dim);text-transform:uppercase;letter-spacing:.04em;align-items:center;gap:6px;margin-bottom:4px;font-size:12px;font-weight:600;display:flex}.iconbtn{background:var(--bg-light);color:var(--fg);cursor:pointer;border:none;border-radius:6px;flex:none;justify-content:center;align-self:flex-end;align-items:center;width:26px;height:24px;padding:0;font-size:13px;display:inline-flex}.iconbtn-glyph{justify-content:center;align-items:center;width:100%;height:100%;line-height:1;display:inline-flex}.iconbtn:hover{filter:brightness(1.3)}.iconbtn.active{background:var(--acc);color:#fff}.overlay{z-index:10;background:#0009;place-items:center;display:grid;position:fixed;inset:0}.modal{background:var(--bg-mid);border-radius:10px;width:min(620px,92vw);max-height:88vh;padding:16px 18px;overflow-y:auto}.modal-head{justify-content:space-between;align-items:center;margin-bottom:12px;font-size:16px;display:flex}.modal label{color:var(--dim);flex-direction:column;gap:4px;font-size:12.5px;display:flex}.modal input[type=text],.modal input[type=number],.modal input[type=password],.modal input[type=url],.modal input[type=email],.modal select,.modal textarea{background:var(--bg-darkest);color:var(--fg);font:inherit;border:1px solid #0000;border-radius:6px;outline:none;min-width:0;padding:7px 9px}.modal input:focus,.modal select:focus,.modal textarea:focus{border-color:var(--acc)}.modal ::placeholder{color:#7c818f;opacity:1}.modal select option{background:var(--bg-darkest);color:var(--fg)}.modal input.compact{padding:5px 8px;font-size:12.5px}.modal textarea{resize:vertical}.w100{width:100%}.mono{font-family:ui-monospace,Menlo,monospace;font-size:13px}.modal fieldset{border:1px solid var(--bg-light);border-radius:8px;margin:0 0 4px;padding:10px}.modal fieldset legend{color:var(--dim);text-transform:uppercase;letter-spacing:.05em;padding:0 6px;font-size:11px}.cfgrow{flex-wrap:wrap;align-items:center;gap:6px;margin-bottom:6px;display:flex}.cfgrow input{background:var(--bg-darkest);border:1px solid var(--bg-light);color:var(--fg);border-radius:5px;flex:1;min-width:0;padding:5px 8px;font-size:12.5px}.cfgrow select{background:var(--bg-darkest);border:1px solid var(--bg-light);color:var(--fg);border-radius:5px;padding:5px 8px;font-size:12.5px}.cfgrow .cfgname{width:90px;color:var(--warn);flex:none;font-weight:600}.cfgrow button{background:var(--bg-mid);border:1px solid var(--bg-light);color:var(--fg);cursor:pointer;border-radius:5px;padding:4px 8px;font-size:12px}.cfgrow button:hover{background:var(--bg-light)}.cfgrow button.danger{background:var(--err);border-color:var(--err);color:#fff}.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);border:1px solid var(--bg-light);color:var(--fg);border-radius:5px;width:100%;padding:5px 8px}.cfgcol{margin-bottom:8px}.cfgcol textarea{background:var(--bg-darkest);border:1px solid var(--bg-light);color:var(--fg);font:inherit;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)}.filepreview{border-radius:8px;max-height:62vh;overflow:auto}.filetoolbar{gap:6px;margin-bottom:8px;display:flex}.filetoolbar button{background:var(--bg-light);color:var(--dim);cursor:pointer;border:none;border-radius:6px;padding:4px 12px;font-size:12.5px}.filetoolbar button:hover{color:var(--fg);filter:brightness(1.15)}.filetoolbar button.on{background:var(--acc);color:#fff;font-weight:600}.fileeditor{flex-direction:column;gap:8px;display:flex}.fileeditor textarea{background:var(--bg-darkest);min-height:55vh;max-height:65vh;color:var(--fg);border:1px solid var(--bg-light);white-space:pre;border-radius:8px;padding:10px;font-size:12.5px;line-height:1.45;overflow:auto}.filerow-actions{justify-content:flex-end;align-items:center;gap:10px;display:flex}.savebtn{background:var(--ok);color:#fff;cursor:pointer;border:none;border-radius:6px;padding:5px 14px;font-size:13px;font-weight:600}.savebtn:disabled{opacity:.4;cursor:default}.mdpreview{background:var(--bg-darkest);border-radius:8px;max-height:62vh;padding:14px 16px;overflow:auto}.filepreview .shiki,.filepreview .shiki span{color:var(--shiki-light);font-family:ui-monospace,Menlo,monospace;font-size:12.5px}html[data-appearance=dark] .filepreview .shiki,html[data-appearance=dark] .filepreview .shiki span{color:var(--shiki-dark)}.filepreview .shiki{background-color:var(--shiki-dark-bg);white-space:pre-wrap;word-break:break-word;overflow-wrap:anywhere;margin:0;padding:10px 12px;font-size:12.5px}html[data-appearance=light] .filepreview .shiki{background-color:var(--shiki-light-bg)}.notifbtn{position:relative}.notifbtn.has-unread{color:var(--acc)}.notifdot{background:var(--err);color:#fff;border-radius:999px;padding:2px 4px;font-size:9px;font-style:normal;font-weight:700;line-height:1;position:absolute;top:-4px;right:-4px}.brand-row{align-items:center;margin-top:2px;display:flex}.notifbackdrop{z-index:7;position:fixed;inset:0}.notifpanel{z-index:8;background:var(--bg-mid);border:1px solid var(--bg-light);border-radius:10px;width:min(360px,88vw);position:absolute;bottom:8px;left:8px;overflow:hidden;box-shadow:0 8px 30px #000a}.notifhead{border-bottom:1px solid var(--line);text-transform:uppercase;letter-spacing:.04em;color:var(--dim);justify-content:space-between;align-items:center;padding:8px 12px;font-size:11px;display:flex}.notiflist{max-height:46vh;overflow-y:auto}.notifitem{border-bottom:1px solid var(--line);cursor:pointer;padding:8px 12px}.notifitem:last-child{border-bottom:none}.notifitem:hover{background:var(--bg-light)}.notifitem .nhead{align-items:baseline;gap:6px;min-width:0;display:flex}.notifitem .ntitle{text-overflow:ellipsis;white-space:nowrap;flex:1;min-width:0;font-size:13px;font-weight:600;overflow:hidden}.notifitem.error .ntitle{color:var(--err)}.notifitem.finish .ntitle{color:var(--ok)}.notifitem.question .ntitle{color:var(--acc)}.notifitem .nbody{color:var(--dim);word-break:break-word;margin-top:3px;font-size:12px}.subcount{color:var(--dim);background:var(--bg-light);white-space:nowrap;border-radius:999px;flex-shrink:0;padding:1px 7px;font-size:10px}.subcount.live{color:var(--ok)}.mediapreview{flex-direction:column;align-items:center;gap:8px;display:flex}.mediapreview img{background:var(--bg-darkest);border-radius:8px;max-width:100%;max-height:65vh}.mediapreview video{background:#000;border-radius:8px;max-width:100%;max-height:65vh}.mediapreview audio{width:100%}.composer{flex-direction:column;display:flex}.composer .hint{flex-shrink:0}.composer.maximized{z-index:5;background:var(--bg-dark);padding:16px 16px 18px;position:absolute;inset:0}.composer.maximized:before{content:"";background:var(--bg-dark);z-index:-1;position:absolute;inset:0}.composer.maximized form{flex:1;align-items:stretch;min-height:0}.composer.maximized textarea{resize:none;flex:1;max-height:none;height:auto!important}.composer.maximized .maxbtn,.composer.maximized form>button[type=submit]{align-self:flex-end}.maxbtn{justify-content:center;align-items:center;font-size:12px;line-height:1;display:inline-flex;padding:0!important}.filerow.gitignored .fname,.filerow.gitignored .ficon,.filerow.gitignored .fsize{opacity:.38}.embed.compaction{border-left-color:var(--tool)}.embed.compaction summary .meta{color:var(--tool)}.avatar.ghosted{opacity:.35;transition:opacity .15s}.msg:hover .avatar.ghosted{opacity:1}.grouptime{display:none}.notifitem{opacity:1}.notifitem.read{opacity:.55}.notifitem.read .ntitle{font-weight:400}.nreadbtn{flex-shrink:0;width:auto;padding:0 6px;font-size:11px}.flash-target{outline:2px solid var(--acc);border-radius:8px;transition:outline-color .6s}.msg.flash-target{background:color-mix(in srgb, var(--acc) 12%, transparent)}.agent-item .ghost{opacity:.55}.ghosttag{flex-shrink:0;font-size:10px}.filetoggle{text-align:center;flex-shrink:0;width:26px;padding:0}h3 .filetoggle{margin-left:0}.notifbadge{color:var(--acc);background:var(--bg-light);white-space:nowrap;border-radius:999px;flex-shrink:0;margin-left:auto;padding:1px 6px;font-size:10px}.agent-item.sel .notifbadge{background:var(--bg-darkest)}input[type=checkbox]{background-color:var(--bg-darkest);border:1px solid var(--dim);border-radius:3px}input[type=number]{appearance:textfield}input[type=number]::-webkit-outer-spin-button{-webkit-appearance:none;margin:0}input[type=number]::-webkit-inner-spin-button{-webkit-appearance:none;margin:0}input[type=number]{-moz-appearance:textfield}.modal>button,.modal button:not(.iconbtn):not([class]){background:var(--bg-light);color:var(--fg);border:1px solid var(--dim);font:inherit;cursor:pointer;border-radius:6px;padding:6px 10px}.modal>button:hover,.modal button:not(.iconbtn):not([class]):hover{background:var(--bg-mid);border-color:var(--acc)}.modal>button:focus-visible,.modal button:not(.iconbtn):not([class]):focus-visible{outline:2px solid var(--acc);outline-offset:1px}.todopane,.todopane-edit{box-sizing:border-box;background:var(--bg-darkest);width:100%;height:11.5em;min-height:7em;max-height:40vh;color:var(--fg);resize:vertical;border:none;border-radius:6px;padding:6px 8px;font-family:ui-monospace,Menlo,monospace;font-size:12.5px;line-height:1.45;display:block;overflow-y:auto}.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}