teapot-coding-agent 0.24.1 → 0.24.2

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.
@@ -346,10 +346,23 @@ export class Agent {
346
346
  return this.manualCompactBudget;
347
347
  }
348
348
  /**
349
- * Long-parking tools (wait_children) flip the visible status to idle so the
350
- * operator isn't staring at a running spinner for minutes. The run chain is
351
- * still parked inside the tool — but any prompt/stop wakes it immediately.
349
+ * Is this agent doing work that `stop()` would actually stop?
350
+ *
351
+ * `status` cannot answer this on its own. Three states count as live:
352
+ * - `running`: the obvious one;
353
+ * - a PARKED TOOL: `parkForTool()` flips the visible status to "idle" so a
354
+ * multi-minute `wait_children` doesn't look like a running spinner, but
355
+ * the loop is very much alive inside the tool;
356
+ * - QUEUED PROMPTS: prompts handed to a stopped agent are accepted and held,
357
+ * and a later `start()` will consume them.
358
+ *
359
+ * Anything keying "should this button stop or start?" off `status` alone
360
+ * therefore offers **start** on a working agent, and the user cannot stop it
361
+ * (#59) — the same trap #38 documented for editing history.
352
362
  */
363
+ isLive() {
364
+ return this.status === "running" || this.parkedByTool || this.pendingPrompts.length > 0;
365
+ }
353
366
  /**
354
367
  * A background shell (bash background=true) finished. Two channels:
355
368
  * 1. a system_note row in the timeline — the operator sees the outcome
@@ -614,6 +627,15 @@ export class Agent {
614
627
  if (msgs.length > 0) {
615
628
  this.messages = msgs;
616
629
  this.currentBranch = last.branch;
630
+ // #50: re-apply pruning to the restored history. `maybePrune` clips
631
+ // oversized tool output IN PLACE, so a live session keeps its clipped
632
+ // text while the log still holds the full result — a restart therefore
633
+ // handed the provider a strictly LARGER prompt than the one that had just
634
+ // succeeded, which is a size regression that only shows up after a
635
+ // reload. It is a size fix, not a semantic one: the model has already
636
+ // seen and acted on that output, and the pruned form keeps the head plus
637
+ // a marker, exactly as it did live.
638
+ this.maybePrune();
617
639
  await this.log.append("system_note", this.currentSession, this.currentBranch, {
618
640
  event: "session-restored",
619
641
  branch: last.branch,
@@ -876,6 +898,17 @@ export class Agent {
876
898
  id: this.opts.id,
877
899
  status: this.status,
878
900
  statusReason: this.statusReason,
901
+ /**
902
+ * Is there work in flight that `stop` would actually stop?
903
+ *
904
+ * `status` alone is not enough to answer that, and the UI needs it: a
905
+ * parked tool (`wait_children`) rewrites the status to "idle" while the
906
+ * loop is very much alive, so a client keying "stop vs start" off
907
+ * `status === "running"` offered **start** on a running agent and the
908
+ * user could not stop it (#59). This is the same "running is not the
909
+ * only live state" fact #38 had to be taught about editing.
910
+ */
911
+ live: this.isLive(),
879
912
  workspace: this.workspace,
880
913
  workspaceMissing: this.workspaceMissing,
881
914
  session: this.currentSession,
@@ -2171,6 +2204,20 @@ export class Agent {
2171
2204
  phase: "summarizing",
2172
2205
  summarized: oldCount,
2173
2206
  });
2207
+ // #56: the START belongs on the timeline too. Only the right panel showed
2208
+ // compaction, and it reports the *outcome*; a long summarize (hundreds of
2209
+ // thousands of tokens) looks exactly like a hung agent on the timeline,
2210
+ // because the first `context-compacted` event is only appended once the
2211
+ // whole pass is over. Log the announcement as a visible system_note so
2212
+ // the operator sees WHERE the conversation was rewritten, even if the
2213
+ // pass later fails — `before`/`oldCount` are known at this point, and the
2214
+ // completion divider still carries the authoritative after-figures.
2215
+ await this.log.append("system_note", this.currentSession, this.currentBranch, {
2216
+ event: "context-compaction-started",
2217
+ tokensBefore: before,
2218
+ messages: oldCount,
2219
+ reason: force ? "manual" : "auto",
2220
+ });
2174
2221
  this.compactPhase = "summarizing";
2175
2222
  try {
2176
2223
  summary = await this.summarize(old, (text) => {
package/dist/agent/llm.js CHANGED
@@ -128,6 +128,75 @@ function sanitize(messages) {
128
128
  return { ...m };
129
129
  });
130
130
  }
131
+ /**
132
+ * Why did the response carry no usable message?
133
+ *
134
+ * #50 — this used to be a bare "LLM API returned no choices", which is what
135
+ * made the report undiagnosable: an empty `choices` array, a 200 carrying an
136
+ * `error` object, a `delta`-shaped body, and an SSE stream that the SDK
137
+ * handed back as a plain String all produce the identical line, and none of
138
+ * them is a useful thing to read in a log.
139
+ *
140
+ * The leading text is kept stable at "LLM API returned no choices" so any
141
+ * existing matching (and the retry notes operators have learned to read)
142
+ * still works; the diagnosis follows after a colon.
143
+ */
144
+ function noChoicesDetail(res) {
145
+ const base = "LLM API returned no choices";
146
+ // a gateway that answers 200 with an error object instead of failing: the
147
+ // actionable message is right there and used to be thrown away
148
+ const err = res?.error;
149
+ if (err && typeof err === "object") {
150
+ const msg = typeof err.message === "string" ? err.message : JSON.stringify(err);
151
+ const code = err.code ?? err.type;
152
+ return `${base}: provider returned an error in a 200 response${code ? ` (${String(code)})` : ""} — ${String(msg).slice(0, 300)}`;
153
+ }
154
+ // the SDK returned raw text — e.g. an SSE body served to a non-streaming
155
+ // request. JSON.parse succeeds for a real object, so a failure here means we
156
+ // were handed something that is not a completion at all.
157
+ if (typeof res === "string") {
158
+ const head = res.slice(0, 160).replace(/\s+/g, " ").trim();
159
+ return `${base}: response was not a completion object (${res.length} bytes of ${res.trimStart().startsWith("data:") || res.trimStart().startsWith("event:") ? "SSE" : "text"})${head ? ` — ${head}` : ""}`;
160
+ }
161
+ const choices = res?.choices;
162
+ if (Array.isArray(choices)) {
163
+ const first = choices[0];
164
+ if (!first)
165
+ return `${base}: the provider returned an empty choices array (0 candidates)`;
166
+ if (first.delta && !first.message) {
167
+ return `${base}: the provider returned a streaming delta on a non-streaming request (finish_reason=${String(first.finish_reason)})`;
168
+ }
169
+ if (first.message === null) {
170
+ return `${base}: the provider returned a choice with a null message (finish_reason=${String(first.finish_reason)})`;
171
+ }
172
+ return `${base}: the provider returned a choice with no message field (finish_reason=${String(first.finish_reason)})`;
173
+ }
174
+ return `${base}: the response had no choices array (${safeShape(res)})`;
175
+ }
176
+ /** a short type summary of an unexpected response, for the log line */
177
+ function safeShape(res) {
178
+ try {
179
+ const o = res;
180
+ if (!o || typeof o !== "object")
181
+ return `got ${typeof res}`;
182
+ return `keys: ${Object.keys(o).slice(0, 8).join(", ") || "(none)"}`;
183
+ }
184
+ catch {
185
+ return "unreadable response";
186
+ }
187
+ }
188
+ /**
189
+ * HTTP statuses that mean "this request shape is not supported here", so
190
+ * re-issuing it without `stream: true` is a genuinely different request.
191
+ *
192
+ * Deliberately an allow-list rather than the whole 4xx range. 408/425/429 are
193
+ * TRANSIENT: replaying them immediately against an already-saturated provider
194
+ * just doubles the request count without changing anything. 401/403 mean the
195
+ * credentials are wrong, which is not fixed by dropping `stream` either — they
196
+ * are excluded on purpose, since a fallback there would replace one honest
197
+ * auth error with the same one and a wasted round trip.
198
+ */
199
+ const STREAM_REJECTED_STATUS = new Set([400, 404, 405, 406, 415, 422, 501]);
131
200
  export async function chat(cfg, messages, tools, signal, onDelta) {
132
201
  try {
133
202
  const res = await client(cfg).chat.completions.create({
@@ -136,10 +205,14 @@ export async function chat(cfg, messages, tools, signal, onDelta) {
136
205
  ...(tools.length ? { tools } : {}),
137
206
  ...effortForRequest(cfg),
138
207
  }, { signal });
139
- const raw = res.choices?.[0];
208
+ // #50: a gateway may answer with `null` (or any non-object) on some proxy
209
+ // error paths, so the guard is on `rm`, not on `res` being truthy —
210
+ // reading `res.choices` unguarded threw a bare TypeError that named neither
211
+ // the provider nor the shape.
212
+ const raw = res?.choices?.[0];
140
213
  const rm = raw?.message;
141
214
  if (!rm)
142
- throw new Error("LLM API returned no choices");
215
+ throw new Error(noChoicesDetail(res));
143
216
  // normalize: providers attach extra fields (reasoning, refusal, ...) and
144
217
  // nullable content — keep only what our protocol understands
145
218
  const message = {
@@ -198,6 +271,13 @@ export async function chat(cfg, messages, tools, signal, onDelta) {
198
271
  }
199
272
  catch (err) {
200
273
  const e = err;
274
+ // #50: our own shape-diagnosis already reads as a full sentence and names
275
+ // the response it saw. It carries no HTTP status (the request SUCCEEDED —
276
+ // that is the whole problem), so the generic wrapper below would have
277
+ // relabelled it "LLM API error ?: …", hiding both the diagnosis and the
278
+ // fact that no status was ever involved.
279
+ if (String(err.message ?? "").startsWith("LLM API returned no choices"))
280
+ throw err;
201
281
  if (e.status === undefined && !String(err.message).includes("provider returned"))
202
282
  throw err; // not an API error (abort, bug, ...)
203
283
  const detail = e.error?.message ?? e.message ?? "unknown provider error";
@@ -284,14 +364,58 @@ export async function chatStream(cfg, messages, tools, signal, onDelta) {
284
364
  function: { name: c.name, arguments: c.args || "{}" },
285
365
  }];
286
366
  });
287
- if (!message.content && !message.tool_calls)
288
- throw new Error("LLM API error: empty completion");
367
+ if (!message.content && !message.tool_calls) {
368
+ // #50: a stream that opened, produced NOTHING, and closed cleanly. The
369
+ // throw carries a marker so the handler below can tell this apart from a
370
+ // request the provider actively rejected: this one is a broken/flaky
371
+ // proxy, and re-issuing it without `stream` is a genuinely different
372
+ // request that usually succeeds. Throwing a bare Error would make the two
373
+ // indistinguishable and cost us that safety net.
374
+ const e = new Error("LLM API error: empty completion");
375
+ e.emptyStream = true;
376
+ throw e;
377
+ }
289
378
  return { message, reasoning: reasoning || undefined, usage };
290
379
  }
291
380
  catch (err) {
292
- // provider may not support streaming at all — one clean fallback
293
- if (!gotChunk && !signal?.aborted)
381
+ // #50 — a provider that doesn't support streaming at all still gets ONE
382
+ // clean fallback, but ONLY when the streaming attempt produced nothing for
383
+ // a reason that could plausibly be the stream itself.
384
+ //
385
+ // The old condition was `!gotChunk && !signal?.aborted`, which also matched
386
+ // a request that FAILED — a gateway answering 200 with an SSE `error`
387
+ // frame, or a context-length rejection. In those cases the fallback
388
+ // re-issued the same oversized request in a different shape, and the
389
+ // resulting "LLM API returned no choices" replaced the real diagnosis.
390
+ // That is why a restored 1500-message session reported "no choices" three
391
+ // times and then errored: the genuine "context length exceeded" was
392
+ // discarded on the first attempt and could never be recognised by
393
+ // isContextOverflow(), so the compact-and-retry recovery never ran.
394
+ //
395
+ // Two exclusions, and the second one matters as much as the first:
396
+ //
397
+ // 1. A status that means the REQUEST ITSELF was rejected. Only then is
398
+ // re-issuing it without `stream` a different request rather than the
399
+ // same failing one. `400`/`404`/`405`/`422` are the shapes a provider
400
+ // uses to say "no streaming here" — deliberately NOT the whole 4xx
401
+ // range, because 408/425/429 are transient: replaying those against an
402
+ // already-saturated provider just doubles the request count.
403
+ // 2. A THROWN error, as opposed to a request that simply produced no
404
+ // chunks. A 200 that streams zero chunks and closes cleanly is a
405
+ // broken/flaky proxy, and that is precisely the case the non-streaming
406
+ // retry rescues. Requiring a 4xx here would have deleted that safety
407
+ // net — a regression this file's own tests caught.
408
+ //
409
+ // So the fallback runs when the request was rejected outright, OR when it
410
+ // completed without error and without a single chunk. Everything else —
411
+ // 5xx, an error frame, a mid-stream failure — is the ANSWER, rethrown
412
+ // untouched so overflow recovery can see it.
413
+ const status = err?.status;
414
+ const rejectedByProvider = typeof status === "number" && STREAM_REJECTED_STATUS.has(status);
415
+ const emptyStream = err?.emptyStream === true;
416
+ if (!gotChunk && !signal?.aborted && (rejectedByProvider || emptyStream)) {
294
417
  return chat(cfg, messages, tools, signal, onDelta);
418
+ }
295
419
  // user interrupt: hand back whatever streamed so far so the harness can
296
420
  // keep the partial output visible instead of losing it
297
421
  if (signal?.aborted && (text || reasoning)) {
@@ -182,7 +182,13 @@ function startBackgroundShell(cmd, ctx) {
182
182
  * Wording varies by provider (OpenRouter/OpenAI/Anthropic/local servers). */
183
183
  export function isContextOverflow(err) {
184
184
  const msg = String(err?.message ?? "").toLowerCase();
185
- return (/context (length|window|limit)|too many tokens|prompt is too long|max.{0,12}tokens.{0,20}(exceed|surpass)|request too large|payload too large|exceeds.{0,20}limit/.test(msg) || /\b(400|413)\b.*token/.test(msg));
185
+ return (/context (length|window|limit)|too many tokens|prompt is too long|max.{0,12}tokens.{0,20}(exceed|surpass)|request too large|payload too large|exceeds.{0,20}limit/.test(msg) ||
186
+ /\b(400|413)\b.*token/.test(msg) ||
187
+ // #50: some gateways answer an over-long request with HTTP 200 and put the
188
+ // real diagnosis in the body, which llm.ts now surfaces after the "no
189
+ // choices" prefix. Without this the compact-and-retry recovery never
190
+ // recognised it and the turn simply failed.
191
+ /no choices:[\s\S]{0,300}context (length|window|limit)/.test(msg));
186
192
  }
187
193
  /** Any background shell still running for this workspace? The auto-continue
188
194
  * loop consults this before nudging a finished round: an agent that parked
@@ -709,10 +715,43 @@ export function hashLine(content) {
709
715
  }
710
716
  /** `LINE:HASH| content` for each line, 1-indexed */
711
717
  export function renderHashlines(lines, startLine = 1) {
712
- return lines
718
+ // same trailing-newline rule as renderGutter (#7): a file ending in "\n"
719
+ // splits into a final "" that is not a line, and an anchor for it is a lie
720
+ // the model can copy into an edit that then never verifies
721
+ const body = lines.length && lines[lines.length - 1] === "" ? lines.slice(0, -1) : lines;
722
+ return body
713
723
  .map((l, i) => `${startLine + i}:${hashLine(l)}| ${l}`)
714
724
  .join("\n");
715
725
  }
726
+ /**
727
+ * Width of the `N| ` gutter for a page, so every line in it lines up.
728
+ *
729
+ * #7: the prefix had no padding, so the gutter silently widened at line 1000
730
+ * and the whole block jumped sideways mid-file — the exact "unfriendly line
731
+ * numbers" complaint. Padding to the widest number IN THE PAGE is enough: a
732
+ * page is a bounded window, and aligning to the file's total length would
733
+ * push every line of a 20-line read of a 500k-line file 6 characters right.
734
+ */
735
+ export function gutterWidth(startLine, count) {
736
+ return String(Math.max(0, startLine - 1) + count).length;
737
+ }
738
+ /**
739
+ * The default `N| ` display, with a stable gutter width and no phantom line.
740
+ *
741
+ * #7, second half: the prefix was added AFTER `text.split("\n")`, so a file
742
+ * ending in a newline produced a final numbered row for a line that does not
743
+ * exist. A model that copies the tail verbatim then hands `edit_file` an
744
+ * old_text with a phantom line in it, and the edit fails for no visible reason.
745
+ * A trailing empty element is dropped, which is what the reader means.
746
+ */
747
+ export function renderGutter(lines, startLine = 1) {
748
+ // a trailing newline splits into a final "" — not a line of the file
749
+ const body = lines.length && lines[lines.length - 1] === "" ? lines.slice(0, -1) : lines;
750
+ if (!body.length)
751
+ return "";
752
+ const w = gutterWidth(startLine, body.length);
753
+ return body.map((l, i) => `${String(startLine + i).padStart(w)}| ${l}`).join("\n");
754
+ }
716
755
  /** parse a `LINE:HASH` anchor; returns null when the text is not an anchor */
717
756
  export function parseAnchor(anchor) {
718
757
  const m = /^\s*(\d+):([0-9a-z]{4,8}| {4})\s*$/.exec(anchor);
@@ -913,7 +952,7 @@ export const TOOLS = [
913
952
  }
914
953
  const parts = regions.map(([s, e]) => idMode === "hash"
915
954
  ? renderHashlines(lines.slice(s, e + 1), s + 1)
916
- : lines.slice(s, e + 1).map((l, k) => `${s + k + 1}| ${l}`).join("\n"));
955
+ : renderGutter(lines.slice(s, e + 1), s + 1));
917
956
  let result = parts.join("\n--\n");
918
957
  if (idxs.length > page.length || off > 0)
919
958
  result += `\n(${off + 1}–${off + page.length} of ${idxs.length} matches)`;
@@ -924,8 +963,11 @@ export const TOOLS = [
924
963
  off = off < 0 ? Math.max(0, lines.length + off) : Math.max(0, off - 1);
925
964
  const lim = num(args.limit, 2000);
926
965
  const pageLines = lines.slice(off, off + lim);
927
- const slice = idMode === "hash" ? renderHashlines(pageLines, off + 1) : pageLines.map((l, i) => `${off + i + 1}| ${l}`).join("\n");
928
- const more = off + lim < lines.length ? `\n... (${lines.length - off - lim} more lines)` : "";
966
+ const slice = idMode === "hash" ? renderHashlines(pageLines, off + 1) : renderGutter(pageLines, off + 1);
967
+ // the phantom trailing line is dropped for the count too, or a file that
968
+ // ends in a newline always reports one line more than it has
969
+ const total = lines.length && lines[lines.length - 1] === "" ? lines.length - 1 : lines.length;
970
+ const more = off + lim < total ? `\n... (${total - off - lim} more lines)` : "";
929
971
  return { ok: true, result: slice + more };
930
972
  },
931
973
  },
@@ -1090,12 +1132,27 @@ export const TOOLS = [
1090
1132
  // already proved which lines it means, so a block whose content occurs
1091
1133
  // twice must still edit the anchored occurrence rather than fail the
1092
1134
  // uniqueness check (#4).
1135
+ // The hashline path splits on "\n" to index by resolved line number, which
1136
+ // leaves a trailing "\r" on every line of a CRLF file. Rejoining with
1137
+ // "\n" then DROPS all of them, so a single edit silently rewrote a
1138
+ // Windows file's line endings: the replaced line arrived with the model's
1139
+ // "\n" while every untouched line kept its "\r\n". The file still parsed
1140
+ // and the edit "succeeded", so nothing reported it — but the next
1141
+ // checkout diff shows the whole file changed, and tools that normalise on
1142
+ // EOL (git, prettier, formatters) now rewrite it.
1143
+ //
1144
+ // So the EOL is detected ONCE from the untouched text and re-applied to
1145
+ // the replacement, which also keeps a CRLF file CRLF when the model sends
1146
+ // LF-separated new_text (the overwhelmingly common case — models never
1147
+ // emit \r).
1148
+ const eol = text.includes("\r\n") ? "\r\n" : "\n";
1093
1149
  const out = hashSpan
1094
1150
  ? text
1095
1151
  .split("\n")
1096
1152
  .slice(0, hashSpan.start)
1097
- .concat(newText.split("\n"), text.split("\n").slice(hashSpan.end))
1153
+ .concat(newText.replace(/\r\n/g, "\n").split("\n"), text.split("\n").slice(hashSpan.end))
1098
1154
  .join("\n")
1155
+ .replace(/(?<!\r)\n/g, eol)
1099
1156
  : replaceAll
1100
1157
  ? text.split(oldText).join(newText)
1101
1158
  : text.replace(oldText, newText);
@@ -238,3 +238,50 @@ async function readWindow(fh, size, bytes) {
238
238
  }
239
239
  return { events, bytes: buf.length };
240
240
  }
241
+ /**
242
+ * Keep one branch's rows out of a timeline, optionally including the history it
243
+ * inherited before it forked.
244
+ *
245
+ * #38. A fork is a new timeline, but the agent's CONTEXT on it still includes
246
+ * everything before the fork point — `rebuildMessagesFrom` walks the parent
247
+ * chain across branch boundaries precisely so a restarted fork keeps its
248
+ * history. Filtering the view by strict equality therefore showed *less* than
249
+ * the model actually reasons over, which is why a filtered fork looked like
250
+ * data loss even though nothing was missing from the log.
251
+ *
252
+ * `includeInherited` walks the parent links out of the branch's own events and
253
+ * keeps everything they reach, so the view matches the model's context. Strict
254
+ * remains the default: a caller that genuinely wants only a branch's own rows
255
+ * (counting, auditing) should not be handed inherited history silently.
256
+ *
257
+ * Pure and exported so the rule can be tested without an HTTP server.
258
+ */
259
+ export function filterByBranch(events, branch, includeInherited = false) {
260
+ if (!includeInherited)
261
+ return events.filter((e) => e.branch === branch);
262
+ const byId = new Map(events.map((e) => [e.id, e]));
263
+ // every event belonging to the branch, plus everything reachable by `parent`
264
+ const wanted = new Set();
265
+ // A WORKLIST, not a loop over the growing set: iterating `wanted` while
266
+ // adding to it inside the same pass re-visits nodes and, on a parent cycle,
267
+ // never terminates.
268
+ const queue = [];
269
+ for (const e of events) {
270
+ if (e.branch !== branch)
271
+ continue;
272
+ if (wanted.has(e.id))
273
+ continue;
274
+ wanted.add(e.id);
275
+ queue.push(e.id);
276
+ }
277
+ while (queue.length) {
278
+ const cur = byId.get(queue.pop());
279
+ // walk up through the fork boundary — a fork's parent is the last event of
280
+ // the SOURCE branch, and that branch's history is inherited too
281
+ if (cur?.parent && !wanted.has(cur.parent)) {
282
+ wanted.add(cur.parent);
283
+ queue.push(cur.parent);
284
+ }
285
+ }
286
+ return events.filter((e) => wanted.has(e.id));
287
+ }
@@ -51,7 +51,7 @@ async function listModels(baseUrl, apiKey) {
51
51
  }
52
52
  import path from "node:path";
53
53
  import { bus } from "../bus.js";
54
- import { readEvents, readEventsTail } from "../log/events.js";
54
+ import { readEvents, readEventsTail, filterByBranch } from "../log/events.js";
55
55
  /** media categories the file tree can preview inline */
56
56
  const IMAGE_EXT = new Set(["png", "jpg", "jpeg", "gif", "webp", "svg", "bmp", "ico", "avif"]);
57
57
  const VIDEO_EXT = new Set(["mp4", "webm", "mov", "mkv", "avi", "m4v"]);
@@ -976,7 +976,7 @@ export function buildApp(master) {
976
976
  }
977
977
  let events = await readEventsCached(filePath);
978
978
  if (branch)
979
- events = events.filter((e) => e.branch === branch);
979
+ events = filterByBranch(events, branch, c.req.query("lineage") === "true");
980
980
  // cursor pagination for older pages: everything strictly BEFORE this id
981
981
  if (before) {
982
982
  const idx = events.findIndex((e) => e.id === before);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "teapot-coding-agent",
3
- "version": "0.24.1",
3
+ "version": "0.24.2",
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}.chatheader{letter-spacing:.03em;color:var(--dim);cursor:pointer;-webkit-user-select:none;user-select:none;align-items:center;gap:6px;padding:4px 10px 2px 22px;font-size:11px;font-weight:600;display:flex}.chatheader:hover{color:var(--fg)}.chatheader .wscaret{flex-shrink:0;width:12px;font-size:9px}.agent-item{cursor:pointer;color:var(--dim);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.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}.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 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 10px;position:absolute;inset:0}.composer.maximized .hint{white-space:nowrap;text-overflow:ellipsis;flex-shrink:0;margin-top:8px;line-height:1.4;overflow:hidden}.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)}.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}