@pasko70/pibo 1.10.1 → 1.11.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.
Files changed (39) hide show
  1. package/dist/apps/chat-ui/assets/{dist-4NlLjLs7.js → dist-0E9FVJ6k.js} +1 -1
  2. package/dist/apps/chat-ui/assets/{dist-BpfBSMzK.js → dist-BvqC0hRM.js} +1 -1
  3. package/dist/apps/chat-ui/assets/{dist-hU-2oAb6.js → dist-C2BRHNXr.js} +1 -1
  4. package/dist/apps/chat-ui/assets/{dist-DnXWNQRm.js → dist-C57jNmjf.js} +1 -1
  5. package/dist/apps/chat-ui/assets/{dist-BPl-fzRB.js → dist-CBL8UXjd.js} +1 -1
  6. package/dist/apps/chat-ui/assets/{dist-uF6UXzI7.js → dist-CI-LPHjw.js} +1 -1
  7. package/dist/apps/chat-ui/assets/{dist-3Sts0Afa.js → dist-CMRFkfl5.js} +1 -1
  8. package/dist/apps/chat-ui/assets/{dist-Cvx5M97R.js → dist-CpZPhD2y.js} +1 -1
  9. package/dist/apps/chat-ui/assets/{dist-BA6mhAec.js → dist-CuCZ-3_K.js} +1 -1
  10. package/dist/apps/chat-ui/assets/{dist-BPnn9e2B.js → dist-CzC5kPWJ.js} +1 -1
  11. package/dist/apps/chat-ui/assets/{dist-r31x5Fcc.js → dist-D9hLnn1T.js} +1 -1
  12. package/dist/apps/chat-ui/assets/{index-CaTGYBOS.js → index-BEvjPUor.js} +21 -21
  13. package/dist/apps/chat-ui/assets/{index-al8DeEjA.css → index-DNeE4HrG.css} +1 -1
  14. package/dist/apps/chat-ui/index.html +2 -2
  15. package/dist/apps/chat-vscode-web/assets/{index-CK4SMZuu.js → index-BLZRi4Ka.js} +3 -3
  16. package/dist/apps/chat-vscode-web/assets/index-Bf2JvJ9z.css +2 -0
  17. package/dist/apps/chat-vscode-web/index.html +2 -2
  18. package/dist/apps/vscode-artifacts/latest.vsix +0 -0
  19. package/dist/apps/vscode-artifacts/pibo-vscode-ext-1.11.0.vsix +0 -0
  20. package/dist/auth/better-auth.js +49 -1
  21. package/dist/auth/cli.js +220 -0
  22. package/dist/auth/machine-keys.js +258 -0
  23. package/dist/auth/machine-session.js +123 -0
  24. package/dist/bin/pibo.js +0 -0
  25. package/dist/bin/rg.js +0 -0
  26. package/dist/cli.js +6 -0
  27. package/dist/compute/cli.js +7 -0
  28. package/dist/compute/resource-health.js +27 -3
  29. package/dist/config/config.js +5 -0
  30. package/dist/core/session-router.js +17 -0
  31. package/dist/data/schema.js +4 -0
  32. package/dist/debug/trace-status.js +25 -0
  33. package/dist/debug/trace.js +9 -10
  34. package/dist/resources/cli.js +15 -0
  35. package/dist/resources/lifecycle.js +28 -4
  36. package/dist/resources/reaper.js +1 -0
  37. package/dist/signals/projector.js +10 -1
  38. package/package.json +1 -1
  39. package/dist/apps/chat-vscode-web/assets/index-B5QK07zO.css +0 -2
@@ -0,0 +1,123 @@
1
+ import { createHmac, timingSafeEqual } from "node:crypto";
2
+ export const PIBO_MACHINE_SESSION_COOKIE = "pibo_machine_session";
3
+ export const DEFAULT_MACHINE_SESSION_TTL_SECONDS = 8 * 60 * 60;
4
+ const MACHINE_SESSION_VERSION = 1;
5
+ const MAX_COOKIE_VALUE_LENGTH = 1024;
6
+ function signPayload(secret, encodedPayload) {
7
+ return createHmac("sha256", secret).update(encodedPayload, "utf8").digest();
8
+ }
9
+ function encodePayload(secret, payload) {
10
+ const encodedPayload = Buffer.from(JSON.stringify(payload), "utf8").toString("base64url");
11
+ const signature = signPayload(secret, encodedPayload).toString("base64url");
12
+ return `${encodedPayload}.${signature}`;
13
+ }
14
+ function decodePayload(secret, value) {
15
+ if (value.length === 0 || value.length > MAX_COOKIE_VALUE_LENGTH)
16
+ return undefined;
17
+ const parts = value.split(".");
18
+ if (parts.length !== 2 || !parts[0] || !parts[1])
19
+ return undefined;
20
+ let providedSignature;
21
+ try {
22
+ providedSignature = Buffer.from(parts[1], "base64url");
23
+ }
24
+ catch {
25
+ return undefined;
26
+ }
27
+ const expectedSignature = signPayload(secret, parts[0]);
28
+ if (providedSignature.length !== expectedSignature.length ||
29
+ !timingSafeEqual(providedSignature, expectedSignature)) {
30
+ return undefined;
31
+ }
32
+ try {
33
+ const parsed = JSON.parse(Buffer.from(parts[0], "base64url").toString("utf8"));
34
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
35
+ return undefined;
36
+ const payload = parsed;
37
+ if (payload.v !== MACHINE_SESSION_VERSION ||
38
+ typeof payload.keyId !== "string" ||
39
+ !/^[a-f0-9]{16}$/.test(payload.keyId) ||
40
+ typeof payload.expiresAt !== "number" ||
41
+ !Number.isSafeInteger(payload.expiresAt)) {
42
+ return undefined;
43
+ }
44
+ return { v: MACHINE_SESSION_VERSION, keyId: payload.keyId, expiresAt: payload.expiresAt };
45
+ }
46
+ catch {
47
+ return undefined;
48
+ }
49
+ }
50
+ function cookieValue(headers) {
51
+ const cookieHeader = headers.get("cookie");
52
+ if (!cookieHeader)
53
+ return undefined;
54
+ for (const part of cookieHeader.split(";")) {
55
+ const separator = part.indexOf("=");
56
+ if (separator < 0)
57
+ continue;
58
+ const name = part.slice(0, separator).trim();
59
+ if (name !== PIBO_MACHINE_SESSION_COOKIE)
60
+ continue;
61
+ return part.slice(separator + 1).trim();
62
+ }
63
+ return undefined;
64
+ }
65
+ function sessionCookieHeader(value, expiresAt, maxAgeSeconds) {
66
+ return [
67
+ `${PIBO_MACHINE_SESSION_COOKIE}=${value}`,
68
+ "Path=/",
69
+ "HttpOnly",
70
+ "Secure",
71
+ "SameSite=Strict",
72
+ `Max-Age=${maxAgeSeconds}`,
73
+ `Expires=${expiresAt.toUTCString()}`,
74
+ ].join("; ");
75
+ }
76
+ export function createMachineSessionManager(options) {
77
+ const ttlSeconds = options.ttlSeconds ?? DEFAULT_MACHINE_SESSION_TTL_SECONDS;
78
+ if (!Number.isInteger(ttlSeconds) || ttlSeconds < 1) {
79
+ throw new Error("Machine session TTL must be a positive integer");
80
+ }
81
+ const now = options.now ?? (() => new Date());
82
+ return {
83
+ create(authentication) {
84
+ const createdAt = now();
85
+ const ttlExpiry = createdAt.getTime() + ttlSeconds * 1000;
86
+ const keyExpiry = authentication.session.expiresAt?.getTime();
87
+ const expiryMs = keyExpiry === undefined ? ttlExpiry : Math.min(ttlExpiry, keyExpiry);
88
+ if (expiryMs <= createdAt.getTime())
89
+ throw new Error("Machine key is expired");
90
+ const expiresAt = new Date(expiryMs);
91
+ const value = encodePayload(options.secret, {
92
+ v: MACHINE_SESSION_VERSION,
93
+ keyId: authentication.id,
94
+ expiresAt: expiryMs,
95
+ });
96
+ return {
97
+ header: sessionCookieHeader(value, expiresAt, Math.max(1, Math.floor((expiryMs - createdAt.getTime()) / 1000))),
98
+ expiresAt,
99
+ session: { ...authentication.session, expiresAt },
100
+ };
101
+ },
102
+ getSession(headers) {
103
+ const value = cookieValue(headers);
104
+ if (!value)
105
+ return undefined;
106
+ const payload = decodePayload(options.secret, value);
107
+ const currentTime = now().getTime();
108
+ if (!payload || payload.expiresAt <= currentTime)
109
+ return undefined;
110
+ const keySession = options.machineKeys.getSessionById(payload.keyId);
111
+ if (!keySession)
112
+ return undefined;
113
+ const keyExpiry = keySession.expiresAt?.getTime();
114
+ const effectiveExpiry = keyExpiry === undefined ? payload.expiresAt : Math.min(payload.expiresAt, keyExpiry);
115
+ if (effectiveExpiry <= currentTime)
116
+ return undefined;
117
+ return { ...keySession, expiresAt: new Date(effectiveExpiry) };
118
+ },
119
+ clearHeader() {
120
+ return sessionCookieHeader("", new Date(0), 0);
121
+ },
122
+ };
123
+ }
package/dist/bin/pibo.js CHANGED
File without changes
package/dist/bin/rg.js CHANGED
File without changes
package/dist/cli.js CHANGED
@@ -80,6 +80,11 @@ export async function runPiboCli(argv = process.argv) {
80
80
  printPiboVersion();
81
81
  return;
82
82
  }
83
+ if (argv[2] === "auth") {
84
+ const { runAuthCli } = await import("./auth/cli.js");
85
+ await runAuthCli([argv[0] ?? "node", "pibo auth", ...argv.slice(3)]);
86
+ return;
87
+ }
83
88
  if (argv[2] === "mcp") {
84
89
  const { runMcpCli } = await import("./mcp/index.js");
85
90
  await runMcpCli([argv[0] ?? "node", "pibo mcp", ...argv.slice(3)]);
@@ -465,6 +470,7 @@ function printRootDiscoveryText() {
465
470
 
466
471
  Commands:
467
472
  config Manage local pibo config
473
+ auth Manage Web authentication and machine identities
468
474
  mcp Discover and call configured MCP servers
469
475
  tools Install and inspect curated external CLI tools
470
476
  pi-packages Register Pi Coding Agent packages
@@ -123,6 +123,13 @@ export function renderComputeResourceHealthText(health) {
123
123
  const lines = [`Compute resource health: ${health.severity} (read-only)`];
124
124
  lines.push(`Generated at: ${health.generatedAt}`);
125
125
  lines.push(`Browser processes: ${health.browserProcesses.totalChromiumMainProcesses} main / ${health.browserProcesses.totalChromiumProcesses} total Chromium processes`);
126
+ if (health.browserProcesses.exemptMainProcessDetails.length > 0) {
127
+ lines.push(`Explicitly exempt browser main processes: ${health.browserProcesses.exemptMainProcessDetails.length}`);
128
+ lines.push("PID\tUSER_DATA_DIR\tCOMMAND");
129
+ for (const process of health.browserProcesses.exemptMainProcessDetails.slice(0, 5)) {
130
+ lines.push(`${process.pid}\t${process.userDataDir ?? "-"}\t${process.commandName}`);
131
+ }
132
+ }
126
133
  if (health.browserProcesses.unassignedMainProcessDetails.length > 0) {
127
134
  lines.push(`Unmanaged browser main processes: ${health.browserProcesses.unassignedMainProcessDetails.length}`);
128
135
  lines.push("PID\tWORKER\tUSER_DATA_DIR\tCOMMAND");
@@ -2,7 +2,7 @@ import { execFile } from "node:child_process";
2
2
  import { existsSync } from "node:fs";
3
3
  import { readdir, readFile } from "node:fs/promises";
4
4
  import { homedir } from "node:os";
5
- import { basename, join } from "node:path";
5
+ import { basename, isAbsolute, join, resolve } from "node:path";
6
6
  import { promisify } from "node:util";
7
7
  import { readResourceReaperTimerStatus } from "../resources/reaper-state.js";
8
8
  import { createEmptyBrowserPoolState, normalizeBrowserPoolState } from "../tools/browser-pool.js";
@@ -43,6 +43,7 @@ export function buildComputeResourceHealth(options = {}) {
43
43
  const disk = options.disk;
44
44
  const checks = [];
45
45
  const mainProcesses = processes.filter((process) => process.isChromium && process.isMainProcess);
46
+ const exemptBrowserUserDataDirs = new Set(normalizeBrowserUserDataDirs(options.exemptBrowserUserDataDirs ?? readExemptBrowserUserDataDirs()));
46
47
  const perWorker = browserPools.map(({ state, statePath }) => {
47
48
  const activeLeaseCount = state.activeLeaseId ? Math.max(1, state.activeLeaseCount ?? 1) : state.activeLeaseCount ?? 0;
48
49
  const matched = mainProcesses.filter((process) => browserProcessMatchesPool(process, state));
@@ -76,8 +77,13 @@ export function buildComputeResourceHealth(options = {}) {
76
77
  const activeWorkerOwnedMainPids = new Set(mainProcesses
77
78
  .filter((process) => Boolean(findOwningActiveWorker(process, processes, workers)))
78
79
  .map((process) => process.pid));
79
- const unassignedMainProcessDetails = mainProcesses
80
- .filter((process) => !assignedMainPids.has(process.pid) && !activeWorkerOwnedMainPids.has(process.pid))
80
+ const unmatchedMainProcesses = mainProcesses
81
+ .filter((process) => !assignedMainPids.has(process.pid) && !activeWorkerOwnedMainPids.has(process.pid));
82
+ const exemptMainProcessDetails = unmatchedMainProcesses
83
+ .filter((process) => browserProcessMatchesExemptUserDataDir(process, exemptBrowserUserDataDirs))
84
+ .map((process) => describeUnassignedBrowserProcess(process, workers));
85
+ const unassignedMainProcessDetails = unmatchedMainProcesses
86
+ .filter((process) => !browserProcessMatchesExemptUserDataDir(process, exemptBrowserUserDataDirs))
81
87
  .map((process) => describeUnassignedBrowserProcess(process, workers));
82
88
  const unassignedChromiumMainProcesses = unassignedMainProcessDetails.length;
83
89
  const activePoolIds = perWorker.filter((pool) => pool.activeLeaseCount > 0).map((pool) => `${pool.workerId}/${pool.poolId}`);
@@ -121,6 +127,8 @@ export function buildComputeResourceHealth(options = {}) {
121
127
  totalChromiumMainProcesses: mainProcesses.length,
122
128
  unassignedChromiumMainProcesses,
123
129
  unassignedMainProcessDetails,
130
+ exemptChromiumMainProcesses: exemptMainProcessDetails.length,
131
+ exemptMainProcessDetails,
124
132
  perWorker,
125
133
  },
126
134
  browserLeases: { active: activePoolIds.length, activePoolIds, staleCdpFiles },
@@ -198,6 +206,21 @@ function readChromeArg(args, name) {
198
206
  const pattern = new RegExp(`(?:^|\\s)--${name}=([^\\s]+)`);
199
207
  return args.match(pattern)?.[1];
200
208
  }
209
+ function readExemptBrowserUserDataDirs() {
210
+ return (process.env.PIBO_RESOURCE_REAPER_EXEMPT_BROWSER_USER_DATA_DIRS ?? "").split(",");
211
+ }
212
+ function normalizeBrowserUserDataDirs(values) {
213
+ return [...new Set([...values]
214
+ .map((value) => value.trim())
215
+ .filter((value) => value.length > 0 && isAbsolute(value))
216
+ .map((value) => resolve(value)))];
217
+ }
218
+ function browserProcessMatchesExemptUserDataDir(process, exemptions) {
219
+ const userDataDir = readChromeArg(process.args, "user-data-dir");
220
+ if (!userDataDir || !isAbsolute(userDataDir))
221
+ return false;
222
+ return exemptions.has(resolve(userDataDir));
223
+ }
201
224
  function sanitizeArgsPreview(args) {
202
225
  const redacted = args
203
226
  .replace(/(token|access_token|refresh_token|password|passwd|cookie|secret)=([^\s]+)/gi, "$1=<redacted>")
@@ -224,6 +247,7 @@ export async function getComputeResourceHealth(options = {}) {
224
247
  browserPools,
225
248
  staleCdpFiles,
226
249
  reaperTimers: detectReaperTimerStatus(),
250
+ exemptBrowserUserDataDirs: options.exemptBrowserUserDataDirs,
227
251
  });
228
252
  }
229
253
  export function defaultBrowserUseHome() {
@@ -43,6 +43,11 @@ export const PIBO_CONFIG_KEYS = [
43
43
  type: "string",
44
44
  description: "SQLite path for Better Auth data.",
45
45
  },
46
+ {
47
+ key: "auth.machineKeyStorePath",
48
+ type: "string",
49
+ description: "Optional path for revocable machine-key records. Default $PIBO_HOME/machine-keys.json.",
50
+ },
46
51
  {
47
52
  key: "auth.mode",
48
53
  type: "string",
@@ -244,6 +244,23 @@ export class PiboSessionRouter {
244
244
  }
245
245
  return output;
246
246
  }
247
+ catch (error) {
248
+ if (event.type === "message" && event.id) {
249
+ this.signalRegistry.project({
250
+ type: "message_rejected",
251
+ piboSessionId: event.piboSessionId,
252
+ eventId: event.id,
253
+ });
254
+ const status = session.getStatus();
255
+ this.signalRegistry.project({
256
+ type: "session_processing_changed",
257
+ piboSessionId: event.piboSessionId,
258
+ processing: status.processing,
259
+ queuedMessages: status.queuedMessages,
260
+ });
261
+ }
262
+ throw error;
263
+ }
247
264
  finally {
248
265
  this.scheduleIdleSessionEvictionIfIdle(event.piboSessionId);
249
266
  }
@@ -354,6 +354,10 @@ export function applyPiboDataSchema(db) {
354
354
  ON sessions(channel, kind, updated_at DESC);
355
355
  CREATE INDEX IF NOT EXISTS idx_event_log_session_stream
356
356
  ON event_log(session_id, stream_id);
357
+ CREATE INDEX IF NOT EXISTS idx_event_log_unread_session_stream
358
+ ON event_log(session_id, stream_id)
359
+ WHERE (retention_class = 'chat_message' AND type IN ('user.message.accepted', 'assistant_message'))
360
+ OR type = 'session_error';
357
361
  CREATE INDEX IF NOT EXISTS idx_event_log_session_sequence_stream
358
362
  ON event_log(session_id, session_sequence DESC, stream_id DESC);
359
363
  CREATE INDEX IF NOT EXISTS idx_event_log_session_type_sequence_stream
@@ -0,0 +1,25 @@
1
+ export function resolveDebugTraceSessionStatus(sessionStatus, eventTypes) {
2
+ for (let index = eventTypes.length - 1; index >= 0; index -= 1) {
3
+ switch (eventTypes[index]) {
4
+ case "session_error":
5
+ return { status: "error", source: "event-log" };
6
+ case "message_started":
7
+ return { status: "running", source: "event-log" };
8
+ case "message_finished":
9
+ return { status: "idle", source: "event-log" };
10
+ }
11
+ }
12
+ return {
13
+ status: sessionStatus === "running" || sessionStatus === "error" ? sessionStatus : "idle",
14
+ source: "session-store",
15
+ };
16
+ }
17
+ export function summarizeDebugTraceStatus(sessionStatus, nodeStatuses) {
18
+ const errorNodeCount = nodeStatuses.filter((status) => status === "error").length;
19
+ if (sessionStatus === "error")
20
+ return { status: "error", errorNodeCount };
21
+ if (sessionStatus === "running" || nodeStatuses.some((status) => status === "running")) {
22
+ return { status: "running", errorNodeCount };
23
+ }
24
+ return { status: "done", errorNodeCount };
25
+ }
@@ -3,6 +3,7 @@ import { normalizeSessionErrorDetails } from "../core/session-errors.js";
3
3
  import { compareTraceNodes } from "../shared/trace-nodes.js";
4
4
  import { openReadOnlyDebugDatabase, withStorePath } from "./sql.js";
5
5
  import { formatNextCommands } from "./next-commands.js";
6
+ import { resolveDebugTraceSessionStatus, summarizeDebugTraceStatus } from "./trace-status.js";
6
7
  export async function inspectDebugTrace(piboSessionId, stores, options = {}) {
7
8
  if (!stores.sessions.exists)
8
9
  throw new Error(`Debug store "sessions" not found at ${stores.sessions.path}`);
@@ -22,19 +23,23 @@ export async function inspectDebugTrace(piboSessionId, stores, options = {}) {
22
23
  .prepare("SELECT stream_id, session_id, session_sequence, event_id, type, created_at, preview_text, attributes_json FROM event_log WHERE session_id = ? ORDER BY stream_id ASC")
23
24
  .all(piboSessionId).map((row) => eventFromRow(row, adapterIssues)).filter((event) => event !== undefined)
24
25
  : [];
26
+ const sessionStatus = resolveDebugTraceSessionStatus(sessionRow.status, events.map((event) => event.type));
25
27
  const view = await buildTraceView({
26
28
  session,
27
29
  sessions,
28
30
  events,
29
- status: sessionRow.status === "running" || sessionRow.status === "error" ? sessionRow.status : "idle",
31
+ status: sessionStatus.status,
30
32
  });
31
33
  const rows = flattenTraceNodes(view.nodes);
34
+ const statusSummary = summarizeDebugTraceStatus(sessionStatus.status, rows.map((node) => node.status));
32
35
  const filtered = options.runningOnly ? rows.filter((node) => node.status === "running") : rows;
33
36
  return {
34
37
  piboSessionId: view.piboSessionId,
35
38
  piSessionId: view.piSessionId,
36
39
  title: view.title,
37
- status: traceStatus(view),
40
+ status: statusSummary.status,
41
+ statusSource: sessionStatus.source,
42
+ errorNodeCount: statusSummary.errorNodeCount,
38
43
  nodes: filtered,
39
44
  rawNodeCount: rows.length,
40
45
  ...(options.check ? { checks: checkTraceView(view, adapterIssues) } : {}),
@@ -69,6 +74,8 @@ export function formatDebugTrace(result, options = {}) {
69
74
  `piSessionId: ${result.piSessionId}`,
70
75
  `title: ${result.title}`,
71
76
  `status: ${result.status}`,
77
+ `statusSource: ${result.statusSource}`,
78
+ `nodeErrors: ${result.errorNodeCount}`,
72
79
  "",
73
80
  ];
74
81
  if (result.nodes.length === 0) {
@@ -161,14 +168,6 @@ function flattenTraceNodes(nodes, depth = 0) {
161
168
  ...flattenTraceNodes(node.children, depth + 1),
162
169
  ]);
163
170
  }
164
- function traceStatus(view) {
165
- const rows = flattenTraceNodes(view.nodes);
166
- if (rows.some((node) => node.status === "error"))
167
- return "error";
168
- if (rows.some((node) => node.status === "running"))
169
- return "running";
170
- return "done";
171
- }
172
171
  export function checkTraceView(view, adapterIssues = []) {
173
172
  const issues = [...adapterIssues];
174
173
  const all = flattenPiboTraceNodes(view.nodes);
@@ -1,3 +1,4 @@
1
+ import { isAbsolute, resolve } from "node:path";
1
2
  import { Command } from "commander";
2
3
  import { getComputeResourceHealth } from "../compute/resource-health.js";
3
4
  import { renderComputeResourceHealthText } from "../compute/cli.js";
@@ -17,6 +18,15 @@ function parsePidList(value) {
17
18
  throw new Error("PIDs must be positive integers separated by commas");
18
19
  return [...new Set(pids)];
19
20
  }
21
+ function parseAbsolutePathList(value) {
22
+ const paths = value.split(",").map((item) => item.trim());
23
+ if (paths.some((path) => !path || !isAbsolute(path)))
24
+ throw new Error("Browser user-data directories must be absolute paths separated by commas");
25
+ return [...new Set(paths.map((path) => resolve(path)))];
26
+ }
27
+ function shellQuote(value) {
28
+ return `'${value.replaceAll("'", `'"'"'`)}'`;
29
+ }
20
30
  export function renderResourceLeasesText(leases) {
21
31
  if (leases.length === 0)
22
32
  return "No active managed browser-pool leases.\nNext: pibo resources status";
@@ -53,6 +63,10 @@ export function renderResourceReapText(value) {
53
63
  ];
54
64
  if (plan.options.includeDev)
55
65
  args.push("--include-dev");
66
+ if (plan.options.exemptBrowserPids.length > 0)
67
+ args.push(`--exempt-browser-pids ${plan.options.exemptBrowserPids.join(",")}`);
68
+ if (plan.options.exemptBrowserUserDataDirs.length > 0)
69
+ args.push(`--exempt-browser-user-data-dirs ${shellQuote(plan.options.exemptBrowserUserDataDirs.join(","))}`);
56
70
  if (plan.options.browserPoolRoot)
57
71
  args.push(`--browser-pool-root ${plan.options.browserPoolRoot}`);
58
72
  if (plan.options.browserUseHome)
@@ -118,6 +132,7 @@ export async function runResourcesCli(argv) {
118
132
  .option("--idle-timeout-minutes <n>", "Select browser pools idle for this many minutes", parseNonNegativeNumber, 10)
119
133
  .option("--unmanaged-browser-grace-minutes <n>", "Select unmanaged Chromium older than this many minutes", parseNonNegativeNumber, 10)
120
134
  .option("--exempt-browser-pids <list>", "Comma-separated browser PIDs or process groups to preserve", parsePidList)
135
+ .option("--exempt-browser-user-data-dirs <list>", "Comma-separated absolute browser profile directories to preserve", parseAbsolutePathList)
121
136
  .option("--browser-pool-root <path>", "Browser pool root directory to scan")
122
137
  .option("--browser-use-home <path>", "Browser-use home directory to scan for stale CDP files")
123
138
  .option("--json", "Print machine-readable cleanup plan or result")
@@ -1,7 +1,7 @@
1
1
  import { execFile } from "node:child_process";
2
2
  import { existsSync } from "node:fs";
3
3
  import { readdir, readFile, rm } from "node:fs/promises";
4
- import { dirname, join } from "node:path";
4
+ import { dirname, isAbsolute, join, resolve } from "node:path";
5
5
  import { promisify } from "node:util";
6
6
  import { applyComputeWorkerReapPlan, buildComputeWorkerReapPlan, planReapWorkers, } from "../compute/docker.js";
7
7
  import { defaultBrowserPoolRoot, defaultBrowserUseHome, getComputeResourceHealth, parseProcessList, } from "../compute/resource-health.js";
@@ -60,9 +60,14 @@ export async function planResourceReap(options = {}) {
60
60
  collectManagedBrowserPools(resolved.browserPoolRoot),
61
61
  planStaleCdpFiles(resolved.browserUseHome),
62
62
  planComputeReapSafely({ includeDev: resolved.includeDev, maxAgeMinutes: resolved.maxAgeMinutes, now }),
63
- getComputeResourceHealth({ now, browserPoolRoot: resolved.browserPoolRoot, browserUseHome: resolved.browserUseHome }),
63
+ getComputeResourceHealth({
64
+ now,
65
+ browserPoolRoot: resolved.browserPoolRoot,
66
+ browserUseHome: resolved.browserUseHome,
67
+ exemptBrowserUserDataDirs: [],
68
+ }),
64
69
  ]);
65
- const unmanagedBrowsers = buildUnmanagedBrowserPlanItems(health.browserProcesses.unassignedMainProcessDetails, resolved.unmanagedBrowserGraceMinutes, new Set(resolved.exemptBrowserPids));
70
+ const unmanagedBrowsers = buildUnmanagedBrowserPlanItems(health.browserProcesses.unassignedMainProcessDetails, resolved.unmanagedBrowserGraceMinutes, new Set(resolved.exemptBrowserPids), new Set(resolved.exemptBrowserUserDataDirs));
66
71
  return buildResourceReapPlan({ now, options: resolved, records, staleFiles, unmanagedBrowsers, compute });
67
72
  }
68
73
  export function buildResourceReapPlan(input) {
@@ -126,9 +131,10 @@ function resolveReapOptions(options) {
126
131
  browserPoolRoot: options.browserPoolRoot ?? defaultBrowserPoolRoot(),
127
132
  browserUseHome: options.browserUseHome ?? defaultBrowserUseHome(),
128
133
  exemptBrowserPids: options.exemptBrowserPids ?? readExemptBrowserPids(),
134
+ exemptBrowserUserDataDirs: normalizeBrowserUserDataDirs(options.exemptBrowserUserDataDirs ?? readExemptBrowserUserDataDirs()),
129
135
  };
130
136
  }
131
- export function buildUnmanagedBrowserPlanItems(processes, graceMinutes, exemptPids = new Set()) {
137
+ export function buildUnmanagedBrowserPlanItems(processes, graceMinutes, exemptPids = new Set(), exemptUserDataDirs = new Set()) {
132
138
  const graceSeconds = graceMinutes * 60;
133
139
  return processes.map((process) => {
134
140
  let action = "terminate";
@@ -141,6 +147,10 @@ export function buildUnmanagedBrowserPlanItems(processes, graceMinutes, exemptPi
141
147
  action = "skip";
142
148
  reason = "explicitly exempted pid or process group";
143
149
  }
150
+ else if (browserUserDataDirIsExempt(process.userDataDir, exemptUserDataDirs)) {
151
+ action = "skip";
152
+ reason = "explicitly exempted browser user-data-dir";
153
+ }
144
154
  else if (process.elapsedSeconds !== undefined && process.elapsedSeconds < graceSeconds) {
145
155
  action = "skip";
146
156
  reason = `process age ${process.elapsedSeconds}s is within ${graceSeconds}s grace period`;
@@ -211,6 +221,20 @@ function readExemptBrowserPids() {
211
221
  .map((value) => Number.parseInt(value.trim(), 10))
212
222
  .filter((value) => Number.isInteger(value) && value > 0);
213
223
  }
224
+ function readExemptBrowserUserDataDirs() {
225
+ return (process.env.PIBO_RESOURCE_REAPER_EXEMPT_BROWSER_USER_DATA_DIRS ?? "").split(",");
226
+ }
227
+ function normalizeBrowserUserDataDirs(values) {
228
+ return [...new Set([...values]
229
+ .map((value) => value.trim())
230
+ .filter((value) => value.length > 0 && isAbsolute(value))
231
+ .map((value) => resolve(value)))];
232
+ }
233
+ function browserUserDataDirIsExempt(userDataDir, exemptions) {
234
+ if (!userDataDir || !isAbsolute(userDataDir))
235
+ return false;
236
+ return exemptions.has(resolve(userDataDir));
237
+ }
214
238
  function buildBrowserReapPlanItem(record, now, idleTimeoutMinutes) {
215
239
  const { state } = record;
216
240
  let action = "skip";
@@ -76,6 +76,7 @@ export class ResourceReaperService {
76
76
  browserPoolRoot: this.options.browserPoolRoot,
77
77
  browserUseHome: this.options.browserUseHome,
78
78
  exemptBrowserPids: this.options.exemptBrowserPids,
79
+ exemptBrowserUserDataDirs: this.options.exemptBrowserUserDataDirs,
79
80
  now: runAt,
80
81
  });
81
82
  result = await this.apply(plan);
@@ -28,7 +28,7 @@ function settleActiveSessionNodes(piboSessionId, context, status, terminalSource
28
28
  }
29
29
  export const sessionLifecycleSignalProducer = {
30
30
  name: "session-lifecycle",
31
- accepts: (input) => ["session_created", "session_disposed", "session_processing_changed", "message_accepted", "queue_changed", "recovery", "session_interrupted", "signal_node_pruned"].includes(input.type),
31
+ accepts: (input) => ["session_created", "session_disposed", "session_processing_changed", "message_accepted", "message_rejected", "queue_changed", "recovery", "session_interrupted", "signal_node_pruned"].includes(input.type),
32
32
  project(input, context) {
33
33
  const data = input;
34
34
  if (data.type === "session_created") {
@@ -78,6 +78,15 @@ export const sessionLifecycleSignalProducer = {
78
78
  }
79
79
  return mutations;
80
80
  }
81
+ if (data.type === "message_rejected") {
82
+ const acceptedTurn = context.getNode(`turn:${data.piboSessionId}:${data.eventId}`);
83
+ return [
84
+ { type: "remove_node", nodeId: `message:${data.piboSessionId}:${data.eventId}` },
85
+ ...(acceptedTurn?.metadata?.accepted === true
86
+ ? [{ type: "remove_node", nodeId: acceptedTurn.id }]
87
+ : []),
88
+ ];
89
+ }
81
90
  if (data.type === "session_disposed") {
82
91
  return [
83
92
  ...settleActiveSessionNodes(data.piboSessionId, context, "cancelled", "session_disposed"),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pasko70/pibo",
3
- "version": "1.10.1",
3
+ "version": "1.11.0",
4
4
  "type": "module",
5
5
  "imports": {
6
6
  "vscode": "./src/apps/chat-vscode/extension/src/vscode-shim.js"
@@ -1,2 +0,0 @@
1
- /*! tailwindcss v4.2.4 | MIT License | https://tailwindcss.com */
2
- @layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-border-style:solid;--tw-font-weight:initial;--tw-tracking:initial;--tw-shadow:0 0 #0000;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 #0000;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 #0000}}}@layer theme{:root,:host{--font-sans:ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";--font-mono:ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;--color-emerald-500:oklch(69.6% .17 162.48);--color-cyan-400:oklch(78.9% .154 211.53);--color-rose-200:oklch(89.2% .058 10.001);--color-rose-300:oklch(81% .117 11.638);--color-rose-500:oklch(64.5% .246 16.439);--color-rose-700:oklch(51.4% .222 16.935);--color-rose-900:oklch(41% .159 10.272);--color-slate-200:oklch(92.9% .013 255.508);--color-slate-300:oklch(86.9% .022 252.894);--color-slate-400:oklch(70.4% .04 256.788);--color-slate-500:oklch(55.4% .046 257.417);--color-slate-700:oklch(37.2% .044 257.287);--color-slate-800:oklch(27.9% .041 260.031);--spacing:.25rem;--text-xs:.75rem;--text-xs--line-height:calc(1 / .75);--text-sm:.875rem;--text-sm--line-height:calc(1.25 / .875);--font-weight-semibold:600;--tracking-wider:.05em;--default-font-family:var(--font-sans);--default-mono-font-family:var(--font-mono)}}@layer base{*,:after,:before,::backdrop{box-sizing:border-box;border:0 solid;margin:0;padding:0}::file-selector-button{box-sizing:border-box;border:0 solid;margin:0;padding:0}html,:host{-webkit-text-size-adjust:100%;tab-size:4;line-height:1.5;font-family:var(--default-font-family,ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji");font-feature-settings:var(--default-font-feature-settings,normal);font-variation-settings:var(--default-font-variation-settings,normal);-webkit-tap-highlight-color:transparent}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:var(--default-mono-font-family,ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace);font-feature-settings:var(--default-mono-font-feature-settings,normal);font-variation-settings:var(--default-mono-font-variation-settings,normal);font-size:1em}small{font-size:80%}sub,sup{vertical-align:baseline;font-size:75%;line-height:0;position:relative}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}:-moz-focusring{outline:auto}progress{vertical-align:baseline}summary{display:list-item}ol,ul,menu{list-style:none}img,svg,video,canvas,audio,iframe,embed,object{vertical-align:middle;display:block}img,video{max-width:100%;height:auto}button,input,select,optgroup,textarea{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}::file-selector-button{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}:where(select:is([multiple],[size])) optgroup{font-weight:bolder}:where(select:is([multiple],[size])) optgroup option{padding-inline-start:20px}::file-selector-button{margin-inline-end:4px}::placeholder{opacity:1}@supports (not ((-webkit-appearance:-apple-pay-button))) or (contain-intrinsic-size:1px){::placeholder{color:currentColor}@supports (color:color-mix(in lab, red, red)){::placeholder{color:color-mix(in oklab, currentcolor 50%, transparent)}}}textarea{resize:vertical}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-date-and-time-value{min-height:1lh;text-align:inherit}::-webkit-datetime-edit{display:inline-flex}::-webkit-datetime-edit-fields-wrapper{padding:0}::-webkit-datetime-edit{padding-block:0}::-webkit-datetime-edit-year-field{padding-block:0}::-webkit-datetime-edit-month-field{padding-block:0}::-webkit-datetime-edit-day-field{padding-block:0}::-webkit-datetime-edit-hour-field{padding-block:0}::-webkit-datetime-edit-minute-field{padding-block:0}::-webkit-datetime-edit-second-field{padding-block:0}::-webkit-datetime-edit-millisecond-field{padding-block:0}::-webkit-datetime-edit-meridiem-field{padding-block:0}::-webkit-calendar-picker-indicator{line-height:1}:-moz-ui-invalid{box-shadow:none}button,input:where([type=button],[type=reset],[type=submit]){appearance:button}::file-selector-button{appearance:button}::-webkit-inner-spin-button{height:auto}::-webkit-outer-spin-button{height:auto}[hidden]:where(:not([hidden=until-found])){display:none!important}}@layer components;@layer utilities{.absolute{position:absolute}.start{inset-inline-start:var(--spacing)}.z-10{z-index:10}.container{width:100%}@media (width>=40rem){.container{max-width:40rem}}@media (width>=48rem){.container{max-width:48rem}}@media (width>=64rem){.container{max-width:64rem}}@media (width>=80rem){.container{max-width:80rem}}@media (width>=96rem){.container{max-width:96rem}}.mt-0\.5{margin-top:calc(var(--spacing) * .5)}.mt-1{margin-top:calc(var(--spacing) * 1)}.mt-12{margin-top:calc(var(--spacing) * 12)}.mb-2{margin-bottom:calc(var(--spacing) * 2)}.-ml-20{margin-left:calc(var(--spacing) * -20)}.ml-3{margin-left:calc(var(--spacing) * 3)}.block{display:block}.flex{display:flex}.inline-block{display:inline-block}.inline-flex{display:inline-flex}.h-1\.5{height:calc(var(--spacing) * 1.5)}.h-7{height:calc(var(--spacing) * 7)}.h-full{height:100%}.min-h-0{min-height:calc(var(--spacing) * 0)}.w-1\.5{width:calc(var(--spacing) * 1.5)}.w-7{width:calc(var(--spacing) * 7)}.w-full{width:100%}.max-w-\[180px\]{max-width:180px}.min-w-0{min-width:calc(var(--spacing) * 0)}.flex-1{flex:1}.shrink-0{flex-shrink:0}.flex-col{flex-direction:column}.items-center{align-items:center}.justify-center{justify-content:center}.gap-1{gap:calc(var(--spacing) * 1)}.gap-2{gap:calc(var(--spacing) * 2)}.truncate{text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.overflow-x-auto{overflow-x:auto}.rounded{border-radius:.25rem}.rounded-full{border-radius:3.40282e38px}.border{border-style:var(--tw-border-style);border-width:1px}.border-b{border-bottom-style:var(--tw-border-style);border-bottom-width:1px}.border-\[\#11a4d4\]{border-color:#11a4d4}.border-rose-700{border-color:var(--color-rose-700)}.border-slate-700{border-color:var(--color-slate-700)}.border-slate-800{border-color:var(--color-slate-800)}.bg-\[\#0a1418\]{background-color:#0a1418}.bg-\[\#0c1820\]{background-color:#0c1820}.bg-\[\#102834\]{background-color:#102834}.bg-cyan-400{background-color:var(--color-cyan-400)}.bg-emerald-500{background-color:var(--color-emerald-500)}.bg-rose-500{background-color:var(--color-rose-500)}.bg-rose-900\/40{background-color:#8b083666}@supports (color:color-mix(in lab, red, red)){.bg-rose-900\/40{background-color:color-mix(in oklab, var(--color-rose-900) 40%, transparent)}}.bg-slate-500{background-color:var(--color-slate-500)}.px-1{padding-inline:calc(var(--spacing) * 1)}.px-2{padding-inline:calc(var(--spacing) * 2)}.px-3{padding-inline:calc(var(--spacing) * 3)}.py-1{padding-block:calc(var(--spacing) * 1)}.py-1\.5{padding-block:calc(var(--spacing) * 1.5)}.py-2{padding-block:calc(var(--spacing) * 2)}.py-3{padding-block:calc(var(--spacing) * 3)}.text-left{text-align:left}.font-mono{font-family:var(--font-mono)}.text-sm{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}.text-xs{font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height))}.text-\[10px\]{font-size:10px}.font-semibold{--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold)}.tracking-wider{--tw-tracking:var(--tracking-wider);letter-spacing:var(--tracking-wider)}.text-rose-200{color:var(--color-rose-200)}.text-rose-300{color:var(--color-rose-300)}.text-slate-200{color:var(--color-slate-200)}.text-slate-300{color:var(--color-slate-300)}.text-slate-400{color:var(--color-slate-400)}.text-slate-500{color:var(--color-slate-500)}.uppercase{text-transform:uppercase}.underline{text-decoration-line:underline}.shadow-lg{--tw-shadow:0 10px 15px -3px var(--tw-shadow-color,#0000001a), 0 4px 6px -4px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}@media (hover:hover){.hover\:border-\[\#11a4d4\]:hover{border-color:#11a4d4}.hover\:border-slate-500:hover{border-color:var(--color-slate-500)}.hover\:bg-\[\#102834\]:hover{background-color:#102834}.hover\:bg-rose-900\/30:hover{background-color:#8b08364d}@supports (color:color-mix(in lab, red, red)){.hover\:bg-rose-900\/30:hover{background-color:color-mix(in oklab, var(--color-rose-900) 30%, transparent)}}.hover\:text-\[\#11a4d4\]:hover{color:#11a4d4}.hover\:text-slate-200:hover{color:var(--color-slate-200)}}}:root{--lightningcss-light: ;--lightningcss-dark:initial;color-scheme:dark;scrollbar-color:#334155 #101d22}html,body,#root{height:100%}body{-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;background:#101d22;margin:0;font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Oxygen,Ubuntu,Cantarell,Helvetica Neue,sans-serif}code,pre{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace}.session-signal{display:inline-block;box-shadow:0 0 #64748b00}.session-signal-idle{background:#475569}.session-signal-running{background:#0bda57;animation:1.9s ease-in-out infinite session-signal-slow-pulse}.session-signal-unread{background:#38bdf8;animation:.85s ease-in-out infinite session-signal-fast-pulse}.session-signal-error{background:#ef4444;animation:.55s ease-in-out infinite session-signal-error-blink}@keyframes session-signal-slow-pulse{0%,to{transform:scale(.95);box-shadow:0 0 #0bda5738}50%{transform:scale(1.15);box-shadow:0 0 0 5px #0bda5700}}@keyframes session-signal-fast-pulse{0%,to{transform:scale(.9);box-shadow:0 0 #38bdf861}50%{transform:scale(1.25);box-shadow:0 0 0 6px #38bdf800}}@keyframes session-signal-error-blink{0%,to{opacity:1;transform:scale(1.15);box-shadow:0 0 #ef444473}50%{opacity:.45;transform:scale(.9);box-shadow:0 0 0 6px #ef444400}}.compact-terminal-working-scramble{color:#737373;letter-spacing:-.02em;vertical-align:baseline;width:10ch;font-size:.92em;display:inline-flex}.compact-terminal-working-scramble>span{width:1ch;display:inline-block}.compact-terminal-working-scramble-active{color:#a3a3a3;text-shadow:0 0 4px #a3a3a324}@media (prefers-reduced-motion:reduce){.compact-terminal-working-scramble-active{color:#737373;text-shadow:none}}.model-response-markdown{color:#dbe7e8;overflow-wrap:anywhere}.model-response-markdown>:first-child{margin-top:0}.model-response-markdown>:last-child{margin-bottom:0}.model-response-markdown p{margin:0 0 .85rem}.model-response-markdown h1,.model-response-markdown h2,.model-response-markdown h3,.model-response-markdown h4{color:#f8fafc;letter-spacing:0;margin:1.1rem 0 .45rem;font-weight:700;line-height:1.25}.model-response-markdown h1{font-size:1.15rem}.model-response-markdown h2{font-size:1.05rem}.model-response-markdown h3,.model-response-markdown h4{font-size:.95rem}.model-response-markdown a{color:#42c7ec;text-underline-offset:3px;-webkit-text-decoration:underline #42c7ec73;text-decoration:underline #42c7ec73}.model-response-markdown a:hover{color:#7dd3fc;text-decoration-color:currentColor}.model-response-markdown strong{color:#f8fafc;font-weight:700}.model-response-markdown del{color:#94a3b8}.model-response-markdown ul,.model-response-markdown ol{margin:0 0 .95rem;padding-left:1.25rem;list-style-position:outside}.model-response-markdown ul{list-style-type:disc}.model-response-markdown ol{list-style-type:decimal}.model-response-markdown ul ul,.model-response-markdown ol ul{margin:.25rem 0;list-style-type:circle}.model-response-markdown ul ol,.model-response-markdown ol ol{margin:.25rem 0;list-style-type:lower-alpha}.model-response-markdown li{margin:.25rem 0;padding-left:.15rem}.model-response-markdown li::marker{color:#11a4d4}.model-response-markdown blockquote{color:#cbd5e1;background:#0bda5712;border-left:2px solid #0bda57;margin:.95rem 0;padding:.55rem .85rem}.model-response-markdown hr{border:0;border-top:1px solid #334155cc;margin:1rem 0}.model-response-markdown table{border-collapse:collapse;border:1px solid #334155e6;border-radius:3px;width:max-content;max-width:100%;margin:1rem 0;font-size:.82rem;display:block;overflow-x:auto}.model-response-markdown th,.model-response-markdown td{text-align:left;vertical-align:top;border:1px solid #334155cc;padding:.45rem .6rem}.model-response-markdown th{color:#e2f6fb;background:#11a4d41f;font-weight:700}.model-response-markdown td{color:#dbe7e8;background:#0e11166b}.model-response-markdown :not(pre)>code{color:#11a4d4;background:#111a1f;border:1px solid #e2e8f02e;border-radius:3px;padding:.1rem .3rem;font-size:.86em}.model-response-markdown pre{color:#d1d5db;background:#0e1116;border:1px solid #334155e6;border-left:2px solid #0bda57;border-radius:2px;margin:.95rem 0;padding:.85rem;font-size:.78rem;line-height:1.55;overflow-x:auto}.model-response-markdown pre code{color:inherit;font-size:inherit;background:0 0}.compact-terminal-markdown{color:#d4d4d4;overflow-wrap:anywhere;word-break:break-word;font-size:12px;line-height:1.45}.compact-terminal-markdown>:first-child{margin-top:0}.compact-terminal-markdown>:last-child{margin-bottom:0}.compact-terminal-markdown p{margin:0 0 .55rem}.compact-terminal-reasoning{color:#c7b27a}.compact-terminal-reasoning p{margin-bottom:.35rem}.compact-terminal-reasoning li::marker{color:#f59e0b}.compact-terminal-markdown h1,.compact-terminal-markdown h2,.compact-terminal-markdown h3,.compact-terminal-markdown h4{color:#f3f4f6;margin:.8rem 0 .35rem;font-size:.95rem;line-height:1.3}.compact-terminal-markdown a{color:#38bdf8}.compact-terminal-markdown ul,.compact-terminal-markdown ol{margin:0 0 .55rem;padding-left:1.35rem;list-style-position:outside}.compact-terminal-markdown ul{list-style-type:disc}.compact-terminal-markdown ol{list-style-type:decimal}.compact-terminal-markdown ul ul,.compact-terminal-markdown ol ul{margin:.2rem 0;list-style-type:circle}.compact-terminal-markdown ul ol,.compact-terminal-markdown ol ol{margin:.2rem 0;list-style-type:lower-alpha}.compact-terminal-markdown li{margin:.15rem 0;padding-left:.1rem}.compact-terminal-markdown li::marker{color:#38bdf8}.compact-terminal-markdown li>p{margin:.15rem 0}.compact-terminal-markdown input[type=checkbox]{vertical-align:-.12em;accent-color:#38bdf8;margin:0 .45rem 0 -1.15rem}.compact-terminal-markdown blockquote{color:#d4d4d4;background:#38bdf814;border-left:1px solid #38bdf8;margin:.6rem 0;padding:.45rem .7rem}.compact-terminal-markdown :not(pre)>code{color:#38bdf8;background:#161616;border:1px solid #5a5a5a99;border-radius:2px;padding:.05rem .25rem}.compact-terminal-markdown pre{color:#d4d4d4;background:#111;border:1px solid #2a2a2a;max-width:100%;margin:.65rem 0;padding:.65rem .8rem;font-size:.78rem;line-height:1.5;overflow-x:auto}.compact-terminal-markdown table{border-collapse:collapse;max-width:100%;display:block;overflow-x:auto}.compact-terminal-markdown th,.compact-terminal-markdown td{vertical-align:top;border:1px solid #2a2a2a;padding:.25rem .4rem}.compact-terminal-markdown th{color:#f3f4f6;background:#161616}.compact-terminal-markdown .token.comment,.model-response-markdown .token.comment,.compact-terminal-markdown .token.prolog,.model-response-markdown .token.prolog,.compact-terminal-markdown .token.doctype,.model-response-markdown .token.doctype,.compact-terminal-markdown .token.cdata,.model-response-markdown .token.cdata{color:#737373}.compact-terminal-markdown .token.punctuation,.model-response-markdown .token.punctuation{color:#94a3b8}.compact-terminal-markdown .token.property,.model-response-markdown .token.property,.compact-terminal-markdown .token.tag,.model-response-markdown .token.tag,.compact-terminal-markdown .token.boolean,.model-response-markdown .token.boolean,.compact-terminal-markdown .token.number,.model-response-markdown .token.number,.compact-terminal-markdown .token.constant,.model-response-markdown .token.constant,.compact-terminal-markdown .token.symbol,.model-response-markdown .token.symbol{color:#60a5fa}.compact-terminal-markdown .token.selector,.model-response-markdown .token.selector,.compact-terminal-markdown .token.attr-name,.model-response-markdown .token.attr-name,.compact-terminal-markdown .token.string,.model-response-markdown .token.string,.compact-terminal-markdown .token.char,.model-response-markdown .token.char,.compact-terminal-markdown .token.builtin,.model-response-markdown .token.builtin{color:#22c55e}.compact-terminal-markdown .token.operator,.model-response-markdown .token.operator,.compact-terminal-markdown .token.entity,.model-response-markdown .token.entity,.compact-terminal-markdown .token.url,.model-response-markdown .token.url,.compact-terminal-markdown .token.variable,.model-response-markdown .token.variable{color:#facc15}.compact-terminal-markdown .token.atrule,.model-response-markdown .token.atrule,.compact-terminal-markdown .token.attr-value,.model-response-markdown .token.attr-value,.compact-terminal-markdown .token.function,.model-response-markdown .token.function,.compact-terminal-markdown .token.class-name,.model-response-markdown .token.class-name{color:#38bdf8}.compact-terminal-markdown .token.keyword,.model-response-markdown .token.keyword{color:#d946ef}.compact-terminal-markdown .token.regex,.model-response-markdown .token.regex,.compact-terminal-markdown .token.important,.model-response-markdown .token.important{color:#fb923c}.compact-terminal-markdown .token.deleted,.model-response-markdown .token.deleted{color:#ef4444}.compact-terminal-markdown .token.inserted,.model-response-markdown .token.inserted{color:#22c55e}.compact-terminal-json{--w-rjv-background-color:transparent;--w-rjv-border-left:#2a2a2a;font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace}.compact-terminal-function-call{overflow-wrap:anywhere;word-break:break-word}.compact-terminal-json-toggle{font:inherit;cursor:pointer;background:0 0;border:0;margin:0;padding:0}.compact-terminal-json-toggle:hover{text-underline-offset:2px;-webkit-text-decoration:underline dotted;text-decoration:underline dotted}.compact-terminal-json-caret{color:#a3a3a3;width:.75em;text-decoration:none;display:inline-block}.context-files-view .mdxeditor{--baseBg:#151f24;--baseBorderColor:#334155;--baseTextColor:#dbe7e8;--accentBg:#11a4d42e;--accentTextColor:#f8fafc;--accentBaseColor:#11a4d4;--basePageBg:transparent;color:#dbe7e8;flex-direction:column;min-height:100%;display:flex}.context-files-view .mdxeditor-toolbar{z-index:2;background:#151f24;border-bottom:1px solid #1e293b;position:sticky;top:0;overflow-x:auto}.context-files-view .mdxeditor-toolbar [role=toolbar]{gap:.3rem;padding:.45rem .55rem}.context-files-view .mdxeditor-root-contenteditable,.context-files-mdx-content{color:#dbe7e8;overflow-wrap:anywhere;word-break:break-word;width:100%;min-width:0;min-height:calc(100vh - 14rem);padding:1.15rem 1.25rem 5rem}.context-files-mdx-content h1,.context-files-mdx-content h2,.context-files-mdx-content h3,.context-files-mdx-content h4,.context-files-mdx-content h5,.context-files-mdx-content h6{color:#f8fafc;margin:1.2rem 0 .55rem;line-height:1.2}.context-files-mdx-content h1{margin-top:0;font-size:1.6rem}.context-files-mdx-content h2{font-size:1.25rem}.context-files-mdx-content h3{font-size:1.05rem}.context-files-mdx-content ul,.context-files-mdx-content ol{margin:0 0 1rem;padding-left:1.4rem}.context-files-mdx-content blockquote{color:#cbd5e1;border-left:4px solid #11a4d473;margin-left:0;padding-left:.9rem}.context-files-mdx-content table{border-collapse:collapse;width:100%;display:block;overflow-x:auto}.context-files-mdx-content th,.context-files-mdx-content td{border:1px solid #334155;padding:.5rem .6rem}.context-files-view .cm-editor,.context-files-view .cm-gutters{color:#dbe7e8;background:#0e1116}.context-files-view .cm-scroller,.context-files-view pre{max-width:100%;overflow-x:auto}.context-files-view .cm-cursor,.context-files-view .cm-dropCursor{border-left-color:#11a4d4}.context-files-view [class*=_iconButton_],.context-files-view [class*=_toolbarButton_],.context-files-view [class*=_toolbarToggleItem_],.context-files-view [class*=_selectTrigger_],.context-files-view [class*=_toolbarNodeKindSelectTrigger_],.context-files-view [class*=_toolbarButtonSelectTrigger_]{color:#94a3b8!important;box-shadow:none!important;background:0 0!important;border-color:#2a3943!important;font-weight:400!important}.context-files-view [class*=_iconButton_] svg,.context-files-view [class*=_toolbarButton_] svg,.context-files-view [class*=_toolbarToggleItem_] svg,.context-files-view [class*=_selectTrigger_] svg,.context-files-view [class*=_toolbarNodeKindSelectTrigger_] svg,.context-files-view [class*=_toolbarButtonSelectTrigger_] svg{opacity:.88;color:#94a3b8!important;stroke:currentColor!important;width:18px!important;height:18px!important}.context-files-view [class*=_iconButton_]:hover,.context-files-view [class*=_toolbarButton_]:hover,.context-files-view [class*=_toolbarToggleItem_]:hover,.context-files-view [class*=_selectTrigger_]:hover,.context-files-view [class*=_toolbarNodeKindSelectTrigger_]:hover,.context-files-view [class*=_toolbarButtonSelectTrigger_]:hover{color:#cbd5e1!important;background:#ffffff08!important;border-color:#334155!important}.context-files-view [class*=_iconButton_]:hover svg,.context-files-view [class*=_toolbarButton_]:hover svg,.context-files-view [class*=_toolbarToggleItem_]:hover svg,.context-files-view [class*=_selectTrigger_]:hover svg,.context-files-view [class*=_toolbarNodeKindSelectTrigger_]:hover svg,.context-files-view [class*=_toolbarButtonSelectTrigger_]:hover svg{opacity:.94;color:#cbd5e1!important}.context-files-view [data-state=on],.context-files-view [aria-checked=true]{color:#dbe7e8!important;background:#11a4d41f!important;border-color:#11a4d473!important}.context-files-view [data-state=on] svg,.context-files-view [aria-checked=true] svg{opacity:.94;color:#cbd5e1!important}.context-files-view [class*=_selectContent_],.context-files-view [class*=_dropdownMenuContent_],.context-files-view [class*=_dialogContent_]{color:#dbe7e8!important;background:#111a1f!important;border:1px solid #334155!important}.context-files-view [class*=_toolbarToggleSingleGroup_]{gap:.3rem}.context-files-plain-fallback{flex-direction:column;height:100%;min-height:0;display:flex}.context-files-plain-fallback__notice{color:#fde68a;background:#f59e0b1a;border-bottom:1px solid #1e293b;margin:0;padding:.75rem 1rem;font-size:.84rem}.context-files-plain-fallback__textarea{color:#dbe7e8;min-height:0;font:inherit;resize:none;background:#151f24;border:0;outline:none;flex:1;padding:1rem 1.15rem 4rem;font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;line-height:1.55;overflow:auto}@property --tw-border-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-font-weight{syntax:"*";inherits:false}@property --tw-tracking{syntax:"*";inherits:false}@property --tw-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-shadow-color{syntax:"*";inherits:false}@property --tw-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-inset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-shadow-color{syntax:"*";inherits:false}@property --tw-inset-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-ring-color{syntax:"*";inherits:false}@property --tw-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-ring-color{syntax:"*";inherits:false}@property --tw-inset-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-ring-inset{syntax:"*";inherits:false}@property --tw-ring-offset-width{syntax:"<length>";inherits:false;initial-value:0}@property --tw-ring-offset-color{syntax:"*";inherits:false;initial-value:#fff}@property --tw-ring-offset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}