@youtyan/code-viewer 0.11.1 → 0.12.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.
@@ -273,6 +273,185 @@ var init_routes = __esm({
273
273
  }
274
274
  });
275
275
 
276
+ // web-src/core/error-detail.ts
277
+ function errorWithCause(message, cause) {
278
+ return Object.assign(new Error(message), { cause });
279
+ }
280
+ function errorWithCauses(message, errors) {
281
+ return Object.assign(new Error(message), { errors: [...errors] });
282
+ }
283
+ function isSensitiveFieldName(key) {
284
+ const normalized = key.toLowerCase().replace(/[^a-z0-9]/g, "");
285
+ return normalized === "auth" || normalized.endsWith("auth") || normalized.startsWith("auth") && !normalized.startsWith("author") || normalized.includes("authorization") || normalized.includes("cookie") || normalized.includes("token") || normalized.includes("password") || normalized.includes("passwd") || normalized.includes("secret") || normalized.includes("credential") || normalized.includes("apikey") || normalized.includes("privatekey");
286
+ }
287
+ function errorName(error) {
288
+ try {
289
+ return typeof error.name === "string" ? error.name : "Error";
290
+ } catch {
291
+ return "Error";
292
+ }
293
+ }
294
+ function errorMessage(error) {
295
+ try {
296
+ return typeof error.message === "string" ? error.message : "unable to read error message";
297
+ } catch {
298
+ return "unable to read error message";
299
+ }
300
+ }
301
+ function sanitizeObjectFields(value, ancestors, excludedKeys = /* @__PURE__ */ new Set()) {
302
+ let keys;
303
+ try {
304
+ keys = Object.getOwnPropertyNames(value);
305
+ } catch {
306
+ return { value: "[Unserializable object]", removedSensitive: false };
307
+ }
308
+ const output = /* @__PURE__ */ Object.create(null);
309
+ let removedSensitive = false;
310
+ for (const key of keys) {
311
+ if (excludedKeys.has(key)) continue;
312
+ if (isSensitiveFieldName(key)) {
313
+ removedSensitive = true;
314
+ continue;
315
+ }
316
+ let descriptor;
317
+ try {
318
+ descriptor = Object.getOwnPropertyDescriptor(value, key);
319
+ } catch {
320
+ output[key] = "[Unserializable field]";
321
+ continue;
322
+ }
323
+ if (!descriptor) continue;
324
+ if (!("value" in descriptor)) {
325
+ output[key] = "[Accessor]";
326
+ continue;
327
+ }
328
+ const sanitized = sanitizeValue(descriptor.value, ancestors);
329
+ if (sanitized === OMIT_VALUE) {
330
+ removedSensitive = true;
331
+ continue;
332
+ }
333
+ output[key] = sanitized.value;
334
+ removedSensitive ||= sanitized.removedSensitive;
335
+ }
336
+ if (Object.keys(output).length === 0 && removedSensitive) return OMIT_VALUE;
337
+ return { value: output, removedSensitive };
338
+ }
339
+ function sanitizeError(error, ancestors) {
340
+ const output = /* @__PURE__ */ Object.create(null);
341
+ output.name = errorName(error);
342
+ output.message = errorMessage(error);
343
+ const fields = sanitizeObjectFields(
344
+ error,
345
+ ancestors,
346
+ /* @__PURE__ */ new Set(["name", "message", "stack"])
347
+ );
348
+ if (fields !== OMIT_VALUE) Object.assign(output, fields.value);
349
+ return {
350
+ value: output,
351
+ removedSensitive: fields === OMIT_VALUE ? true : fields.removedSensitive
352
+ };
353
+ }
354
+ function sanitizeValue(value, ancestors) {
355
+ if (value === null || typeof value === "string" || typeof value === "boolean")
356
+ return { value, removedSensitive: false };
357
+ if (typeof value === "number") {
358
+ return {
359
+ value: Number.isFinite(value) ? value : String(value),
360
+ removedSensitive: false
361
+ };
362
+ }
363
+ if (typeof value === "bigint") {
364
+ return { value: `${value}n`, removedSensitive: false };
365
+ }
366
+ if (typeof value === "undefined") {
367
+ return { value: "[undefined]", removedSensitive: false };
368
+ }
369
+ if (typeof value === "symbol") {
370
+ return { value: "[symbol]", removedSensitive: false };
371
+ }
372
+ if (typeof value === "function") {
373
+ return { value: "[function]", removedSensitive: false };
374
+ }
375
+ const objectValue = value;
376
+ if (ancestors.has(objectValue)) {
377
+ return { value: "[Circular]", removedSensitive: false };
378
+ }
379
+ ancestors.add(objectValue);
380
+ try {
381
+ if (Array.isArray(objectValue)) {
382
+ const output = [];
383
+ let removedSensitive = false;
384
+ for (const item of objectValue) {
385
+ const sanitized = sanitizeValue(item, ancestors);
386
+ if (sanitized === OMIT_VALUE) {
387
+ removedSensitive = true;
388
+ continue;
389
+ }
390
+ output.push(sanitized.value);
391
+ removedSensitive ||= sanitized.removedSensitive;
392
+ }
393
+ if (output.length === 0 && removedSensitive) return OMIT_VALUE;
394
+ return { value: output, removedSensitive };
395
+ }
396
+ if (objectValue instanceof Error)
397
+ return sanitizeError(objectValue, ancestors);
398
+ return sanitizeObjectFields(objectValue, ancestors);
399
+ } catch {
400
+ return { value: "[Unserializable object]", removedSensitive: false };
401
+ } finally {
402
+ ancestors.delete(objectValue);
403
+ }
404
+ }
405
+ function formatNonError(value) {
406
+ if (typeof value === "string") return value;
407
+ try {
408
+ const sanitized = sanitizeValue(value, /* @__PURE__ */ new Set());
409
+ const serializable = sanitized === OMIT_VALUE ? {} : sanitized.value;
410
+ return JSON.stringify(serializable);
411
+ } catch {
412
+ return "[Unserializable value]";
413
+ }
414
+ }
415
+ function formatErrorFields(error) {
416
+ const fields = sanitizeObjectFields(
417
+ error,
418
+ /* @__PURE__ */ new Set([error]),
419
+ /* @__PURE__ */ new Set(["name", "message", "stack", "cause"])
420
+ );
421
+ if (fields === OMIT_VALUE) return "";
422
+ const output = fields.value;
423
+ return Object.keys(output).length > 0 ? `
424
+ Details: ${JSON.stringify(output)}` : "";
425
+ }
426
+ function formatErrorDetail(error) {
427
+ const parts = [];
428
+ const seen2 = /* @__PURE__ */ new Set();
429
+ let current = error;
430
+ while (current instanceof Error && !seen2.has(current)) {
431
+ seen2.add(current);
432
+ parts.push(
433
+ `${errorName(current)}: ${errorMessage(current)}${formatErrorFields(current)}`
434
+ );
435
+ try {
436
+ current = current.cause;
437
+ } catch {
438
+ current = "[Unserializable error cause]";
439
+ }
440
+ }
441
+ if (current !== void 0) {
442
+ parts.push(
443
+ seen2.has(current) ? "Error cause cycle detected" : formatNonError(current)
444
+ );
445
+ }
446
+ return parts.join("\nCaused by: ") || formatNonError(error);
447
+ }
448
+ var OMIT_VALUE;
449
+ var init_error_detail = __esm({
450
+ "web-src/core/error-detail.ts"() {
451
+ OMIT_VALUE = /* @__PURE__ */ Symbol("omit-sensitive-error-field");
452
+ }
453
+ });
454
+
276
455
  // web-src/server/json-store.ts
277
456
  import { randomBytes } from "node:crypto";
278
457
  import { mkdir, readFile, rename, unlink, writeFile } from "node:fs/promises";
@@ -284,14 +463,14 @@ function tmpPath(file) {
284
463
  return `${file}.tmp-${process.pid}-${Date.now()}-${randomBytes(4).toString("hex")}`;
285
464
  }
286
465
  async function backupInvalidFile(file, suffix) {
287
- try {
288
- await rename(file, `${file}.${suffix}-${Date.now()}`);
289
- } catch {
290
- }
466
+ const backup = `${file}.${suffix}-${Date.now()}`;
467
+ await rename(file, backup);
468
+ return backup;
291
469
  }
292
470
  function createJsonFileStore(options) {
293
471
  const queues = /* @__PURE__ */ new Map();
294
472
  const backupSuffix = options.backupSuffix ?? "corrupt";
473
+ const invalidFileBehavior = options.invalidFileBehavior ?? "empty";
295
474
  const serialize = options.serialize ?? ((state) => `${JSON.stringify(state, null, 2)}
296
475
  `);
297
476
  async function loadUnqueued(root) {
@@ -305,8 +484,25 @@ function createJsonFileStore(options) {
305
484
  }
306
485
  try {
307
486
  return options.sanitize(JSON.parse(raw));
308
- } catch {
309
- await backupInvalidFile(file, backupSuffix);
487
+ } catch (invalidError) {
488
+ let backup;
489
+ try {
490
+ backup = await backupInvalidFile(file, backupSuffix);
491
+ } catch (backupError) {
492
+ throw errorWithCauses("invalid JSON state could not be backed up", [
493
+ invalidError,
494
+ backupError
495
+ ]);
496
+ }
497
+ const recovered = Object.assign(
498
+ errorWithCause("invalid JSON state was moved aside", invalidError),
499
+ { backup }
500
+ );
501
+ if (invalidFileBehavior === "throw") throw recovered;
502
+ console.error(
503
+ "[code-viewer] invalid JSON state was moved aside",
504
+ recovered
505
+ );
310
506
  return options.empty();
311
507
  }
312
508
  }
@@ -322,55 +518,72 @@ function createJsonFileStore(options) {
322
518
  try {
323
519
  await writeFile(tmp, content, "utf8");
324
520
  await rename(tmp, file);
325
- } catch (err) {
326
- await unlink(tmp).catch(() => void 0);
327
- throw err;
521
+ } catch (writeError) {
522
+ try {
523
+ await unlink(tmp);
524
+ } catch (cleanupError) {
525
+ if (!isEnoent(cleanupError)) {
526
+ throw errorWithCauses(
527
+ "failed to save JSON state and remove the temporary file",
528
+ [writeError, cleanupError]
529
+ );
530
+ }
531
+ }
532
+ throw errorWithCause("failed to save JSON state", writeError);
328
533
  }
329
534
  }
330
- async function load(root) {
331
- const pendingWrite = queues.get(options.filePath(root));
332
- if (pendingWrite) await pendingWrite.catch(() => void 0);
333
- return loadUnqueued(root);
334
- }
335
- async function save(root, state) {
535
+ function enqueue(root, operation) {
336
536
  const file = options.filePath(root);
337
537
  const previous = queues.get(file) ?? Promise.resolve();
338
- const run2 = previous.catch(() => void 0).then(() => saveUnqueued(root, state));
339
- const queued = run2.then(
340
- () => void 0,
341
- () => void 0
342
- );
343
- queues.set(file, queued);
344
- try {
345
- await run2;
346
- } finally {
347
- if (queues.get(file) === queued) queues.delete(file);
348
- }
538
+ const gateState = {
539
+ release: () => {
540
+ throw new Error("JSON store queue gate was not initialized");
541
+ }
542
+ };
543
+ const gate = new Promise((resolve4) => {
544
+ gateState.release = resolve4;
545
+ });
546
+ const tail = previous.then(() => gate);
547
+ queues.set(file, tail);
548
+ return previous.then(async () => {
549
+ try {
550
+ return await operation();
551
+ } finally {
552
+ gateState.release();
553
+ if (queues.get(file) === tail) queues.delete(file);
554
+ }
555
+ });
556
+ }
557
+ function load(root) {
558
+ return enqueue(root, () => loadUnqueued(root));
559
+ }
560
+ function save(root, state) {
561
+ return enqueue(root, () => saveUnqueued(root, state));
562
+ }
563
+ function remove(root) {
564
+ return enqueue(root, async () => {
565
+ try {
566
+ await unlink(options.filePath(root));
567
+ return true;
568
+ } catch (error) {
569
+ if (isEnoent(error)) return false;
570
+ throw errorWithCause("failed to remove JSON state", error);
571
+ }
572
+ });
349
573
  }
350
574
  async function update(root, updater) {
351
- const file = options.filePath(root);
352
- const previous = queues.get(file) ?? Promise.resolve();
353
- const run2 = previous.catch(() => void 0).then(async () => {
575
+ return enqueue(root, async () => {
354
576
  const current = await loadUnqueued(root);
355
577
  const updated = await updater(current);
356
578
  await saveUnqueued(root, updated.state);
357
579
  return updated.result;
358
580
  });
359
- const queued = run2.then(
360
- () => void 0,
361
- () => void 0
362
- );
363
- queues.set(file, queued);
364
- try {
365
- return await run2;
366
- } finally {
367
- if (queues.get(file) === queued) queues.delete(file);
368
- }
369
581
  }
370
- return { load, save, update };
582
+ return { load, save, remove, update };
371
583
  }
372
584
  var init_json_store = __esm({
373
585
  "web-src/server/json-store.ts"() {
586
+ init_error_detail();
374
587
  }
375
588
  });
376
589
 
@@ -1173,7 +1386,7 @@ function runBytesSync(args, cwd2, options = {}) {
1173
1386
  }
1174
1387
  function runBytesAsync(args, cwd2, options = {}) {
1175
1388
  const maxBuffer = options.maxBuffer ?? 64 * 1024 * 1024;
1176
- return new Promise((resolve3) => {
1389
+ return new Promise((resolve4) => {
1177
1390
  const proc = spawn(args[0], args.slice(1), {
1178
1391
  cwd: cwd2,
1179
1392
  stdio: [options.stdin === void 0 ? "ignore" : "pipe", "pipe", "pipe"]
@@ -1214,7 +1427,7 @@ function runBytesAsync(args, cwd2, options = {}) {
1214
1427
  } else {
1215
1428
  stderr = appendProcessError(stderr, processError);
1216
1429
  }
1217
- resolve3({
1430
+ resolve4({
1218
1431
  code,
1219
1432
  stdout: concatBytes(stdoutChunks),
1220
1433
  stderr
@@ -1260,41 +1473,28 @@ function concatBytes(chunks) {
1260
1473
  }
1261
1474
  return out;
1262
1475
  }
1263
- function spawnDetached(args) {
1264
- const child = spawn(args[0], args.slice(1), {
1265
- detached: true,
1266
- stdio: "ignore"
1267
- });
1268
- child.on("error", (err) => {
1269
- console.warn(
1270
- "[code-viewer] failed to start detached command:",
1271
- err.message
1272
- );
1273
- });
1274
- child.unref();
1275
- }
1276
1476
  function spawnStream(args, cwd2) {
1277
1477
  const proc = spawn(args[0], args.slice(1), {
1278
1478
  cwd: cwd2,
1279
1479
  stdio: ["ignore", "pipe", "ignore"]
1280
1480
  });
1281
- let errorCode = 0;
1481
+ let errorCode2 = 0;
1282
1482
  proc.on("error", () => {
1283
- errorCode = 1;
1483
+ errorCode2 = 1;
1284
1484
  });
1285
1485
  return {
1286
1486
  stream: Readable.toWeb(
1287
1487
  proc.stdout
1288
1488
  ),
1289
- exited: new Promise((resolve3) => {
1489
+ exited: new Promise((resolve4) => {
1290
1490
  let settled = false;
1291
1491
  const done = (code) => {
1292
1492
  if (settled) return;
1293
1493
  settled = true;
1294
- resolve3(code);
1494
+ resolve4(code);
1295
1495
  };
1296
1496
  proc.on("error", () => done(1));
1297
- proc.on("close", (code) => done(errorCode || (code ?? 1)));
1497
+ proc.on("close", (code) => done(errorCode2 || (code ?? 1)));
1298
1498
  }),
1299
1499
  kill: (signal) => proc.kill(signal)
1300
1500
  };
@@ -1350,7 +1550,7 @@ function startServer(options) {
1350
1550
  res.end("internal server error");
1351
1551
  }
1352
1552
  });
1353
- return new Promise((resolve3, reject) => {
1553
+ return new Promise((resolve4, reject) => {
1354
1554
  server2.once("error", reject);
1355
1555
  server2.listen(options.port, options.hostname, () => {
1356
1556
  server2.off("error", reject);
@@ -1359,7 +1559,7 @@ function startServer(options) {
1359
1559
  });
1360
1560
  const address = server2.address();
1361
1561
  const port = typeof address === "object" && address ? address.port : options.port;
1362
- resolve3({
1562
+ resolve4({
1363
1563
  port,
1364
1564
  close: () => new Promise((resolveClose, rejectClose) => {
1365
1565
  let settled = false;
@@ -1418,7 +1618,7 @@ async function writeWebResponse(res, response) {
1418
1618
  res.end();
1419
1619
  return;
1420
1620
  }
1421
- await new Promise((resolve3, reject) => {
1621
+ await new Promise((resolve4, reject) => {
1422
1622
  const body = Readable.fromWeb(
1423
1623
  response.body
1424
1624
  );
@@ -1435,12 +1635,12 @@ async function writeWebResponse(res, response) {
1435
1635
  reject(error);
1436
1636
  })
1437
1637
  );
1438
- res.on("finish", () => settle(resolve3));
1638
+ res.on("finish", () => settle(resolve4));
1439
1639
  res.on(
1440
1640
  "close",
1441
1641
  () => settle(() => {
1442
1642
  body.destroy();
1443
- resolve3();
1643
+ resolve4();
1444
1644
  })
1445
1645
  );
1446
1646
  body.pipe(res);
@@ -2444,7 +2644,7 @@ async function worktreeFilesystemEntriesAsync(cwd2, path, recursive, omitDirName
2444
2644
  const yieldIfNeeded = async () => {
2445
2645
  visitedDirs++;
2446
2646
  if (visitedDirs % 25 === 0) {
2447
- await new Promise((resolve3) => setTimeout(resolve3, 0));
2647
+ await new Promise((resolve4) => setTimeout(resolve4, 0));
2448
2648
  }
2449
2649
  };
2450
2650
  const walk = async (dir, prefix, depth) => {
@@ -5064,31 +5264,31 @@ function extractGithubIssueLabels(raw) {
5064
5264
  }
5065
5265
  function normalizeGithubIssueListItem(raw) {
5066
5266
  if (!raw || typeof raw !== "object") return null;
5067
- const issue = raw;
5068
- const number = issue.number;
5069
- const title = issue.title;
5267
+ const issue2 = raw;
5268
+ const number = issue2.number;
5269
+ const title = issue2.title;
5070
5270
  if (typeof number !== "number" || !Number.isInteger(number) || number <= 0 || typeof title !== "string" || !title.trim()) {
5071
5271
  return null;
5072
5272
  }
5073
- const url = singleLineGithubOption(issue.url);
5074
- const state = typeof issue.state === "string" && issue.state.trim() ? issue.state.trim().toLowerCase() : "open";
5273
+ const url = singleLineGithubOption(issue2.url);
5274
+ const state = typeof issue2.state === "string" && issue2.state.trim() ? issue2.state.trim().toLowerCase() : "open";
5075
5275
  return {
5076
5276
  number,
5077
5277
  title: title.trim().slice(0, 200),
5078
5278
  state,
5079
5279
  ...url ? { url } : {},
5080
- labels: extractGithubIssueLabels(issue.labels)
5280
+ labels: extractGithubIssueLabels(issue2.labels)
5081
5281
  };
5082
5282
  }
5083
5283
  function parseGithubIssueListOutput(stdout) {
5084
5284
  const parsed = JSON.parse(stdout);
5085
5285
  if (!Array.isArray(parsed)) return [];
5086
- return parsed.map(normalizeGithubIssueListItem).filter((issue) => issue !== null);
5286
+ return parsed.map(normalizeGithubIssueListItem).filter((issue2) => issue2 !== null);
5087
5287
  }
5088
5288
  function parseGithubIssueViewOutput(stdout) {
5089
- const issue = normalizeGithubIssueListItem(JSON.parse(stdout));
5090
- if (!issue) throw new GithubIssueListError("failed to parse gh issue output");
5091
- return issue;
5289
+ const issue2 = normalizeGithubIssueListItem(JSON.parse(stdout));
5290
+ if (!issue2) throw new GithubIssueListError("failed to parse gh issue output");
5291
+ return issue2;
5092
5292
  }
5093
5293
  function buildGithubIssueListArgs(options) {
5094
5294
  const search = singleLineGithubOption(options.search);
@@ -5653,21 +5853,21 @@ function printGithubIssues(issues) {
5653
5853
  console.log("no GitHub issues");
5654
5854
  return;
5655
5855
  }
5656
- for (const issue of issues) {
5657
- const labels = issue.labels.length ? ` #${issue.labels.join(" #")}` : "";
5658
- const url = issue.url ? ` ${issue.url}` : "";
5856
+ for (const issue2 of issues) {
5857
+ const labels = issue2.labels.length ? ` #${issue2.labels.join(" #")}` : "";
5858
+ const url = issue2.url ? ` ${issue2.url}` : "";
5659
5859
  console.log(
5660
- `#${issue.number} ${issue.state} ${issue.title}${labels}${url}`
5860
+ `#${issue2.number} ${issue2.state} ${issue2.title}${labels}${url}`
5661
5861
  );
5662
5862
  }
5663
5863
  }
5664
- function taskLinkIssuePayload(command, issue) {
5864
+ function taskLinkIssuePayload(command, issue2) {
5665
5865
  return {
5666
5866
  action: "link-github-issue",
5667
- issue_number: issue.number,
5867
+ issue_number: issue2.number,
5668
5868
  repo: command.repo,
5669
- title: issue.title,
5670
- url: issue.url,
5869
+ title: issue2.title,
5870
+ url: issue2.url,
5671
5871
  memo_label: "Memo:",
5672
5872
  status: command.status,
5673
5873
  priority: command.priority,
@@ -5817,13 +6017,13 @@ async function runJournalCli(argv) {
5817
6017
  console.error(commandConfig.error);
5818
6018
  process.exit(1);
5819
6019
  }
5820
- const issue = await readGithubIssueAsync({
6020
+ const issue2 = await readGithubIssueAsync({
5821
6021
  cwd: root,
5822
6022
  number: command.issueNumber,
5823
6023
  repo: command.repo
5824
6024
  });
5825
6025
  if (dryRun) {
5826
- writePayload(taskLinkIssuePayload(command, issue));
6026
+ writePayload(taskLinkIssuePayload(command, issue2));
5827
6027
  return;
5828
6028
  }
5829
6029
  const serverUrl2 = await ensureServerUrl(root, server2, "/_journal");
@@ -5831,13 +6031,13 @@ async function runJournalCli(argv) {
5831
6031
  serverUrl2,
5832
6032
  "POST",
5833
6033
  "journal task-link-issue",
5834
- taskLinkIssuePayload(command, issue)
6034
+ taskLinkIssuePayload(command, issue2)
5835
6035
  );
5836
6036
  if (command.json)
5837
6037
  console.log(
5838
6038
  JSON.stringify(
5839
6039
  {
5840
- issue,
6040
+ issue: issue2,
5841
6041
  task: result2.task,
5842
6042
  action: result2.created ? "created" : result2.moved ? "moved" : "existing"
5843
6043
  },
@@ -5846,11 +6046,11 @@ async function runJournalCli(argv) {
5846
6046
  )
5847
6047
  );
5848
6048
  else if (result2.created)
5849
- console.log(`linked issue #${issue.number} to task ${result2.task.id}`);
6049
+ console.log(`linked issue #${issue2.number} to task ${result2.task.id}`);
5850
6050
  else if (result2.moved)
5851
- console.log(`moved linked issue #${issue.number} task ${result2.task.id}`);
6051
+ console.log(`moved linked issue #${issue2.number} task ${result2.task.id}`);
5852
6052
  else
5853
- console.log(`issue #${issue.number} is linked to task ${result2.task.id}`);
6053
+ console.log(`issue #${issue2.number} is linked to task ${result2.task.id}`);
5854
6054
  return;
5855
6055
  }
5856
6056
  const serverUrl = await ensureServerUrl(root, server2, "/_journal");
@@ -7925,7 +8125,7 @@ function searchPollIntervalMs() {
7925
8125
  }
7926
8126
  function sleep(ms) {
7927
8127
  if (ms <= 0) return Promise.resolve();
7928
- return new Promise((resolve3) => setTimeout(resolve3, ms));
8128
+ return new Promise((resolve4) => setTimeout(resolve4, ms));
7929
8129
  }
7930
8130
  async function cancelSearchJobBestEffort(serverUrl, jobId) {
7931
8131
  try {
@@ -8707,8 +8907,8 @@ function computeFuzzyMatch(query, path) {
8707
8907
  const first = indices[0];
8708
8908
  score -= Math.min(first, 40);
8709
8909
  if (indices[0] >= baseStart) score += 20;
8710
- const basename5 = lowerPath.slice(baseStart);
8711
- const tier = pathMatchTier(q, lowerPath, basename5);
8910
+ const basename6 = lowerPath.slice(baseStart);
8911
+ const tier = pathMatchTier(q, lowerPath, basename6);
8712
8912
  const contiguousRange = contiguousPathRange(q, lowerPath, baseStart);
8713
8913
  return {
8714
8914
  score,
@@ -8860,8 +9060,8 @@ function createGlobPathMatcher(query) {
8860
9060
  const suffix = query.replace(/^\*+/, "").toLowerCase();
8861
9061
  return (path) => {
8862
9062
  const baseStart = basenameStart(path);
8863
- const basename5 = path.slice(baseStart);
8864
- if (!regex.test(path) && (query.includes("/") || !regex.test(basename5)))
9063
+ const basename6 = path.slice(baseStart);
9064
+ if (!regex.test(path) && (query.includes("/") || !regex.test(basename6)))
8865
9065
  return null;
8866
9066
  const ranges = [];
8867
9067
  const lowerPath = path.toLowerCase();
@@ -10268,9 +10468,9 @@ function parseTerminalArgs(argv) {
10268
10468
  function formatStateLine(record) {
10269
10469
  const mark = needsAttention(record.state) ? "*" : " ";
10270
10470
  const state = record.state.padEnd(7, " ");
10271
- const source = record.source === "hook" ? "hook" : "gues";
10471
+ const source = record.source === "hook" ? "hook " : record.source === "screen" ? "screen" : "motion";
10272
10472
  const text2 = record.note || record.lastPrompt || "";
10273
- return `${mark} ${state} ${source} ${record.target.padEnd(16, " ")} ${text2}`;
10473
+ return `${mark} ${state} ${source} ${record.target.padEnd(16, " ")} ${text2}`;
10274
10474
  }
10275
10475
  async function runTerminalCli(argv) {
10276
10476
  const parsed = parseTerminalArgs(argv);
@@ -10318,9 +10518,16 @@ async function runTerminalCli(argv) {
10318
10518
  const all = response2.states ?? [];
10319
10519
  const states2 = command.attentionOnly ? all.filter((record) => needsAttention(record.state)) : all;
10320
10520
  if (command.json) {
10321
- console.log(JSON.stringify({ states: states2 }, null, 2));
10521
+ console.log(
10522
+ JSON.stringify({ states: states2, errors: response2.errors ?? [] }, null, 2)
10523
+ );
10322
10524
  return;
10323
10525
  }
10526
+ for (const error of response2.errors ?? []) {
10527
+ const target = error.target ? ` ${error.target}` : "";
10528
+ console.error(`[${error.operation}${target}] ${error.detail}`);
10529
+ if (error.stack) console.error(error.stack);
10530
+ }
10324
10531
  if (states2.length === 0) {
10325
10532
  console.log("no terminals are reporting a state.");
10326
10533
  return;
@@ -10840,7 +11047,7 @@ function send(message) {
10840
11047
  `);
10841
11048
  }
10842
11049
  function readConfigLine() {
10843
- return new Promise((resolve3, reject) => {
11050
+ return new Promise((resolve4, reject) => {
10844
11051
  let buffer = "";
10845
11052
  const stdin = process.stdin;
10846
11053
  stdin.setEncoding("utf8");
@@ -10850,7 +11057,7 @@ function readConfigLine() {
10850
11057
  if (newline === -1) return;
10851
11058
  stdin.off("data", onData);
10852
11059
  stdin.off("end", onEnd);
10853
- resolve3(buffer.slice(0, newline));
11060
+ resolve4(buffer.slice(0, newline));
10854
11061
  };
10855
11062
  const onEnd = () => {
10856
11063
  stdin.off("data", onData);
@@ -10861,12 +11068,12 @@ function readConfigLine() {
10861
11068
  });
10862
11069
  }
10863
11070
  function hashFile(path) {
10864
- return new Promise((resolve3) => {
11071
+ return new Promise((resolve4) => {
10865
11072
  const hash = createHash2("sha256");
10866
11073
  const stream = createReadStream2(path);
10867
11074
  stream.on("data", (chunk) => hash.update(chunk));
10868
- stream.on("end", () => resolve3(hash.digest("hex")));
10869
- stream.on("error", () => resolve3("unreadable"));
11075
+ stream.on("end", () => resolve4(hash.digest("hex")));
11076
+ stream.on("error", () => resolve4("unreadable"));
10870
11077
  });
10871
11078
  }
10872
11079
  function parsePorcelainV2(raw) {
@@ -11391,7 +11598,7 @@ function waitForAbortableResource(promise, signal, dispose, message = "operation
11391
11598
  void promise.then(disposeSafely, () => void 0);
11392
11599
  return Promise.reject(abortError(message));
11393
11600
  }
11394
- return new Promise((resolve3, reject) => {
11601
+ return new Promise((resolve4, reject) => {
11395
11602
  let aborted = false;
11396
11603
  const onAbort = () => {
11397
11604
  aborted = true;
@@ -11406,7 +11613,7 @@ function waitForAbortableResource(promise, signal, dispose, message = "operation
11406
11613
  disposeSafely(resource);
11407
11614
  return;
11408
11615
  }
11409
- resolve3(resource);
11616
+ resolve4(resource);
11410
11617
  },
11411
11618
  (error) => {
11412
11619
  signal.removeEventListener("abort", onAbort);
@@ -11436,7 +11643,7 @@ function appendMessage(buffer, message) {
11436
11643
  function spawnCollectAsync(opts) {
11437
11644
  throwIfAborted(opts.signal, opts.abortMessage);
11438
11645
  const killSignal = opts.killSignal ?? "SIGTERM";
11439
- return new Promise((resolve3, reject) => {
11646
+ return new Promise((resolve4, reject) => {
11440
11647
  const child = spawn2(opts.command, opts.args, {
11441
11648
  cwd: opts.cwd,
11442
11649
  env: opts.env,
@@ -11453,7 +11660,7 @@ function spawnCollectAsync(opts) {
11453
11660
  opts.signal?.removeEventListener("abort", abort);
11454
11661
  let stderr = Buffer.concat(stderrChunks);
11455
11662
  if (fallbackStderr) stderr = appendMessage(stderr, fallbackStderr);
11456
- resolve3({
11663
+ resolve4({
11457
11664
  stdout: Buffer.concat(stdoutChunks),
11458
11665
  stderr,
11459
11666
  code
@@ -15062,7 +15269,7 @@ function guardedS3Transport(signal, operation, deadline) {
15062
15269
  controller.abort(err);
15063
15270
  reject(err);
15064
15271
  };
15065
- const guarded = new Promise((resolve3, reject) => {
15272
+ const guarded = new Promise((resolve4, reject) => {
15066
15273
  const onParentAbort = () => abort(new S3HttpError(503, "S3 HTTP transport aborted"), reject);
15067
15274
  if (signal) {
15068
15275
  signal.addEventListener("abort", onParentAbort, { once: true });
@@ -15076,7 +15283,7 @@ function guardedS3Transport(signal, operation, deadline) {
15076
15283
  (value) => {
15077
15284
  if (settled) return;
15078
15285
  settled = true;
15079
- resolve3(value);
15286
+ resolve4(value);
15080
15287
  },
15081
15288
  (err) => {
15082
15289
  if (settled) return;
@@ -18180,7 +18387,7 @@ function guardedDynamoDbTransport(signal, operation, deadline) {
18180
18387
  controller.abort(err);
18181
18388
  reject(err);
18182
18389
  };
18183
- const guarded = new Promise((resolve3, reject) => {
18390
+ const guarded = new Promise((resolve4, reject) => {
18184
18391
  const onParentAbort = () => abort(
18185
18392
  new DynamoDbHttpError(503, "DynamoDB HTTP transport aborted"),
18186
18393
  reject
@@ -18197,7 +18404,7 @@ function guardedDynamoDbTransport(signal, operation, deadline) {
18197
18404
  (value) => {
18198
18405
  if (settled) return;
18199
18406
  settled = true;
18200
- resolve3(value);
18407
+ resolve4(value);
18201
18408
  },
18202
18409
  (err) => {
18203
18410
  if (settled) return;
@@ -19345,7 +19552,7 @@ async function parseBoundedJsonBody(req, maxBytes, tooLargeMessage) {
19345
19552
  function waitForCallerAbort(promise, signal, message) {
19346
19553
  if (!signal) return promise;
19347
19554
  if (signal.aborted) return Promise.reject(abortError(message));
19348
- return new Promise((resolve3, reject) => {
19555
+ return new Promise((resolve4, reject) => {
19349
19556
  let settled = false;
19350
19557
  const cleanup = () => signal.removeEventListener("abort", onAbort);
19351
19558
  const onAbort = () => {
@@ -19360,7 +19567,7 @@ function waitForCallerAbort(promise, signal, message) {
19360
19567
  if (settled) return;
19361
19568
  settled = true;
19362
19569
  cleanup();
19363
- resolve3(value);
19570
+ resolve4(value);
19364
19571
  },
19365
19572
  (err) => {
19366
19573
  if (settled) return;
@@ -22187,7 +22394,7 @@ function toFileInfo(entry) {
22187
22394
  kind: entry.kind
22188
22395
  };
22189
22396
  }
22190
- function errorMessage(err) {
22397
+ function errorMessage2(err) {
22191
22398
  const raw = err instanceof Error ? err.message : String(err);
22192
22399
  const withoutControl = Array.from(
22193
22400
  raw,
@@ -22215,7 +22422,7 @@ async function expandDockerServicesForFiles(dockerServices, listDockerDatabases,
22215
22422
  if (isAbortLikeError(err, signal)) throw err;
22216
22423
  return {
22217
22424
  entries: [svc],
22218
- errors: [`${svc.serviceName}: ${errorMessage(err)}`]
22425
+ errors: [`${svc.serviceName}: ${errorMessage2(err)}`]
22219
22426
  };
22220
22427
  }
22221
22428
  if (dbs.length <= 1) {
@@ -22263,7 +22470,7 @@ async function createDbFilesResponse(cwd2, omitDirNames, signal, deps = DEFAULT_
22263
22470
  const dockerServices = dockerSettled.status === "fulfilled" ? dockerSettled.value : [];
22264
22471
  const dockerErrors = [];
22265
22472
  if (dockerSettled.status === "rejected") {
22266
- dockerErrors.push(errorMessage(dockerSettled.reason));
22473
+ dockerErrors.push(errorMessage2(dockerSettled.reason));
22267
22474
  }
22268
22475
  const dockerTruncated = dockerServices.truncated === true;
22269
22476
  const { entries: dockerEntries, errors: listingErrors } = await expandDockerServicesForFiles(
@@ -23248,8 +23455,8 @@ async function handleSnapshotCreate(cwd2, req, sendSse2, omitDirNames) {
23248
23455
  let activeSnapshotId;
23249
23456
  let resolveIdAck;
23250
23457
  let rejectIdAck;
23251
- const idAck = new Promise((resolve3, reject) => {
23252
- resolveIdAck = resolve3;
23458
+ const idAck = new Promise((resolve4, reject) => {
23459
+ resolveIdAck = resolve4;
23253
23460
  rejectIdAck = reject;
23254
23461
  });
23255
23462
  (async () => {
@@ -23976,208 +24183,29 @@ var init_handle = __esm({
23976
24183
  }
23977
24184
  });
23978
24185
 
23979
- // web-src/core/error-detail.ts
23980
- function errorWithCause(message, cause) {
23981
- return Object.assign(new Error(message), { cause });
23982
- }
23983
- function errorWithCauses(message, errors) {
23984
- return Object.assign(new Error(message), { errors: [...errors] });
23985
- }
23986
- function isSensitiveFieldName(key) {
23987
- const normalized = key.toLowerCase().replace(/[^a-z0-9]/g, "");
23988
- return normalized === "auth" || normalized.endsWith("auth") || normalized.startsWith("auth") && !normalized.startsWith("author") || normalized.includes("authorization") || normalized.includes("cookie") || normalized.includes("token") || normalized.includes("password") || normalized.includes("passwd") || normalized.includes("secret") || normalized.includes("credential") || normalized.includes("apikey") || normalized.includes("privatekey");
23989
- }
23990
- function errorName(error) {
23991
- try {
23992
- return typeof error.name === "string" ? error.name : "Error";
23993
- } catch {
23994
- return "Error";
23995
- }
23996
- }
23997
- function errorMessage2(error) {
23998
- try {
23999
- return typeof error.message === "string" ? error.message : "unable to read error message";
24000
- } catch {
24001
- return "unable to read error message";
24002
- }
24003
- }
24004
- function sanitizeObjectFields(value, ancestors, excludedKeys = /* @__PURE__ */ new Set()) {
24005
- let keys;
24006
- try {
24007
- keys = Object.getOwnPropertyNames(value);
24008
- } catch {
24009
- return { value: "[Unserializable object]", removedSensitive: false };
24010
- }
24011
- const output = /* @__PURE__ */ Object.create(null);
24012
- let removedSensitive = false;
24013
- for (const key of keys) {
24014
- if (excludedKeys.has(key)) continue;
24015
- if (isSensitiveFieldName(key)) {
24016
- removedSensitive = true;
24017
- continue;
24018
- }
24019
- let descriptor;
24020
- try {
24021
- descriptor = Object.getOwnPropertyDescriptor(value, key);
24022
- } catch {
24023
- output[key] = "[Unserializable field]";
24024
- continue;
24025
- }
24026
- if (!descriptor) continue;
24027
- if (!("value" in descriptor)) {
24028
- output[key] = "[Accessor]";
24029
- continue;
24030
- }
24031
- const sanitized = sanitizeValue(descriptor.value, ancestors);
24032
- if (sanitized === OMIT_VALUE) {
24033
- removedSensitive = true;
24034
- continue;
24035
- }
24036
- output[key] = sanitized.value;
24037
- removedSensitive ||= sanitized.removedSensitive;
24038
- }
24039
- if (Object.keys(output).length === 0 && removedSensitive) return OMIT_VALUE;
24040
- return { value: output, removedSensitive };
24041
- }
24042
- function sanitizeError(error, ancestors) {
24043
- const output = /* @__PURE__ */ Object.create(null);
24044
- output.name = errorName(error);
24045
- output.message = errorMessage2(error);
24046
- const fields = sanitizeObjectFields(
24047
- error,
24048
- ancestors,
24049
- /* @__PURE__ */ new Set(["name", "message", "stack"])
24050
- );
24051
- if (fields !== OMIT_VALUE) Object.assign(output, fields.value);
24052
- return {
24053
- value: output,
24054
- removedSensitive: fields === OMIT_VALUE ? true : fields.removedSensitive
24055
- };
24056
- }
24057
- function sanitizeValue(value, ancestors) {
24058
- if (value === null || typeof value === "string" || typeof value === "boolean")
24059
- return { value, removedSensitive: false };
24060
- if (typeof value === "number") {
24061
- return {
24062
- value: Number.isFinite(value) ? value : String(value),
24063
- removedSensitive: false
24064
- };
24065
- }
24066
- if (typeof value === "bigint") {
24067
- return { value: `${value}n`, removedSensitive: false };
24068
- }
24069
- if (typeof value === "undefined") {
24070
- return { value: "[undefined]", removedSensitive: false };
24071
- }
24072
- if (typeof value === "symbol") {
24073
- return { value: "[symbol]", removedSensitive: false };
24074
- }
24075
- if (typeof value === "function") {
24076
- return { value: "[function]", removedSensitive: false };
24077
- }
24078
- const objectValue = value;
24079
- if (ancestors.has(objectValue)) {
24080
- return { value: "[Circular]", removedSensitive: false };
24081
- }
24082
- ancestors.add(objectValue);
24083
- try {
24084
- if (Array.isArray(objectValue)) {
24085
- const output = [];
24086
- let removedSensitive = false;
24087
- for (const item of objectValue) {
24088
- const sanitized = sanitizeValue(item, ancestors);
24089
- if (sanitized === OMIT_VALUE) {
24090
- removedSensitive = true;
24091
- continue;
24092
- }
24093
- output.push(sanitized.value);
24094
- removedSensitive ||= sanitized.removedSensitive;
24095
- }
24096
- if (output.length === 0 && removedSensitive) return OMIT_VALUE;
24097
- return { value: output, removedSensitive };
24098
- }
24099
- if (objectValue instanceof Error)
24100
- return sanitizeError(objectValue, ancestors);
24101
- return sanitizeObjectFields(objectValue, ancestors);
24102
- } catch {
24103
- return { value: "[Unserializable object]", removedSensitive: false };
24104
- } finally {
24105
- ancestors.delete(objectValue);
24106
- }
24107
- }
24108
- function formatNonError(value) {
24109
- if (typeof value === "string") return value;
24110
- try {
24111
- const sanitized = sanitizeValue(value, /* @__PURE__ */ new Set());
24112
- const serializable = sanitized === OMIT_VALUE ? {} : sanitized.value;
24113
- return JSON.stringify(serializable);
24114
- } catch {
24115
- return "[Unserializable value]";
24116
- }
24117
- }
24118
- function formatErrorFields(error) {
24119
- const fields = sanitizeObjectFields(
24120
- error,
24121
- /* @__PURE__ */ new Set([error]),
24122
- /* @__PURE__ */ new Set(["name", "message", "stack", "cause"])
24123
- );
24124
- if (fields === OMIT_VALUE) return "";
24125
- const output = fields.value;
24126
- return Object.keys(output).length > 0 ? `
24127
- Details: ${JSON.stringify(output)}` : "";
24128
- }
24129
- function formatErrorDetail(error) {
24130
- const parts = [];
24131
- const seen2 = /* @__PURE__ */ new Set();
24132
- let current = error;
24133
- while (current instanceof Error && !seen2.has(current)) {
24134
- seen2.add(current);
24135
- parts.push(
24136
- `${errorName(current)}: ${errorMessage2(current)}${formatErrorFields(current)}`
24137
- );
24138
- try {
24139
- current = current.cause;
24140
- } catch {
24141
- current = "[Unserializable error cause]";
24142
- }
24143
- }
24144
- if (current !== void 0) {
24145
- parts.push(
24146
- seen2.has(current) ? "Error cause cycle detected" : formatNonError(current)
24147
- );
24148
- }
24149
- return parts.join("\nCaused by: ") || formatNonError(error);
24150
- }
24151
- var OMIT_VALUE;
24152
- var init_error_detail = __esm({
24153
- "web-src/core/error-detail.ts"() {
24154
- OMIT_VALUE = /* @__PURE__ */ Symbol("omit-sensitive-error-field");
24155
- }
24156
- });
24157
-
24158
- // web-src/server/shell/session.ts
24159
- var session_exports = {};
24160
- __export(session_exports, {
24161
- closeAllShellSessions: () => closeAllShellSessions,
24162
- closeShellSession: () => closeShellSession,
24163
- createShellSession: () => createShellSession,
24164
- describeShellAvailability: () => describeShellAvailability,
24165
- getShellSession: () => getShellSession,
24166
- listShellSessions: () => listShellSessions,
24167
- readShellBuffer: () => readShellBuffer,
24168
- resizeShell: () => resizeShell,
24169
- subscribeShell: () => subscribeShell,
24170
- writeToShell: () => writeToShell,
24171
- writeToShellWhenReady: () => writeToShellWhenReady
24172
- });
24173
- function loadPty() {
24174
- if (!ptyModulePromise) {
24175
- ptyModulePromise = import("@lydell/node-pty").then((mod) => mod).catch((err) => {
24176
- ptyLoadError = errorWithCause("failed to load the PTY module", err);
24177
- return null;
24178
- });
24179
- }
24180
- return ptyModulePromise;
24186
+ // web-src/server/shell/session.ts
24187
+ var session_exports = {};
24188
+ __export(session_exports, {
24189
+ closeAllShellSessions: () => closeAllShellSessions,
24190
+ closeShellSession: () => closeShellSession,
24191
+ createShellSession: () => createShellSession,
24192
+ describeShellAvailability: () => describeShellAvailability,
24193
+ getShellSession: () => getShellSession,
24194
+ listShellSessions: () => listShellSessions,
24195
+ readShellBuffer: () => readShellBuffer,
24196
+ resizeShell: () => resizeShell,
24197
+ subscribeShell: () => subscribeShell,
24198
+ writeToShell: () => writeToShell,
24199
+ writeToShellWhenReady: () => writeToShellWhenReady
24200
+ });
24201
+ function loadPty() {
24202
+ if (!ptyModulePromise) {
24203
+ ptyModulePromise = import("@lydell/node-pty").then((mod) => mod).catch((err) => {
24204
+ ptyLoadError = errorWithCause("failed to load the PTY module", err);
24205
+ return null;
24206
+ });
24207
+ }
24208
+ return ptyModulePromise;
24181
24209
  }
24182
24210
  async function describeShellAvailability() {
24183
24211
  const pty = await loadPty();
@@ -24361,8 +24389,8 @@ function writeToShellWhenReady(id, data) {
24361
24389
  const entry = sessions.get(id);
24362
24390
  if (!entry || entry.meta.exited) return Promise.resolve({ status: "gone" });
24363
24391
  if (entry.ready) return Promise.resolve(writeToShellEntry(entry, data));
24364
- return new Promise((resolve3) => {
24365
- entry.queued.push({ data, resolve: resolve3 });
24392
+ return new Promise((resolve4) => {
24393
+ entry.queued.push({ data, resolve: resolve4 });
24366
24394
  });
24367
24395
  }
24368
24396
  function writeToShell(id, data) {
@@ -24391,14 +24419,14 @@ function resizeShell(id, cols, rows) {
24391
24419
  }
24392
24420
  function waitForShellExit(entry) {
24393
24421
  if (entry.meta.exited) return Promise.resolve(true);
24394
- return new Promise((resolve3) => {
24422
+ return new Promise((resolve4) => {
24395
24423
  let settled = false;
24396
24424
  const finish = (exited) => {
24397
24425
  if (settled) return;
24398
24426
  settled = true;
24399
24427
  clearTimeout(timer2);
24400
24428
  entry.exitListeners.delete(onExit);
24401
- resolve3(exited);
24429
+ resolve4(exited);
24402
24430
  };
24403
24431
  const onExit = () => finish(true);
24404
24432
  entry.exitListeners.add(onExit);
@@ -25582,10 +25610,10 @@ async function runProbeWithTimeout(probe, file, cwd2, timeoutMs, parentSignal) {
25582
25610
  timedOut: false
25583
25611
  })
25584
25612
  );
25585
- const timeoutPromise = new Promise((resolve3) => {
25613
+ const timeoutPromise = new Promise((resolve4) => {
25586
25614
  timer2 = setTimeout(() => {
25587
25615
  controller.abort();
25588
- resolve3({
25616
+ resolve4({
25589
25617
  kind: "fail",
25590
25618
  reason: `timed out after ${timeoutMs}ms`,
25591
25619
  timedOut: true
@@ -26018,6 +26046,102 @@ var init_dev_assets = __esm({
26018
26046
  }
26019
26047
  });
26020
26048
 
26049
+ // web-src/server/file-upload.ts
26050
+ import {
26051
+ closeSync as closeSync2,
26052
+ constants as constants3,
26053
+ openSync as openSync2,
26054
+ unlinkSync as unlinkSync2,
26055
+ writeFileSync as writeFileSync2
26056
+ } from "node:fs";
26057
+ function errorCode(error) {
26058
+ if (typeof error === "object" && error !== null && "code" in error && typeof error.code === "string") {
26059
+ return error.code;
26060
+ }
26061
+ return void 0;
26062
+ }
26063
+ function withErrorCode(error, code) {
26064
+ return code === void 0 ? error : Object.assign(error, { code });
26065
+ }
26066
+ function writeAndClose(upload, fd, bytes, fileSystem) {
26067
+ let writeError;
26068
+ try {
26069
+ fileSystem.write(fd, bytes);
26070
+ } catch (error) {
26071
+ writeError = error;
26072
+ }
26073
+ try {
26074
+ fileSystem.close(fd);
26075
+ } catch (closeError) {
26076
+ if (writeError !== void 0) {
26077
+ throw withErrorCode(
26078
+ errorWithCauses(`failed to write and close ${upload.target}`, [
26079
+ writeError,
26080
+ closeError
26081
+ ]),
26082
+ errorCode(writeError)
26083
+ );
26084
+ }
26085
+ throw errorWithCause(`failed to close ${upload.target}`, closeError);
26086
+ }
26087
+ if (writeError !== void 0) throw writeError;
26088
+ }
26089
+ async function writeUploadedFiles(uploads, fileSystem = NODE_FILE_SYSTEM) {
26090
+ const created = [];
26091
+ try {
26092
+ for (const upload of uploads) {
26093
+ const bytes = new Uint8Array(await upload.file.arrayBuffer());
26094
+ const fd = fileSystem.open(upload.target);
26095
+ created.push(upload.target);
26096
+ writeAndClose(upload, fd, bytes, fileSystem);
26097
+ }
26098
+ } catch (error) {
26099
+ const cleanupErrors = [];
26100
+ for (const path of created) {
26101
+ try {
26102
+ fileSystem.remove(path);
26103
+ } catch (cleanupError) {
26104
+ cleanupErrors.push(
26105
+ errorWithCause(
26106
+ `failed to remove partial upload ${path}`,
26107
+ cleanupError
26108
+ )
26109
+ );
26110
+ }
26111
+ }
26112
+ const code = errorCode(error);
26113
+ if (cleanupErrors.length > 0) {
26114
+ throw withErrorCode(
26115
+ errorWithCauses(
26116
+ "failed to write uploaded files and clean up partial files",
26117
+ [error, ...cleanupErrors]
26118
+ ),
26119
+ code
26120
+ );
26121
+ }
26122
+ throw withErrorCode(
26123
+ errorWithCause("failed to write uploaded files", error),
26124
+ code
26125
+ );
26126
+ }
26127
+ }
26128
+ var NODE_FILE_SYSTEM;
26129
+ var init_file_upload = __esm({
26130
+ "web-src/server/file-upload.ts"() {
26131
+ init_error_detail();
26132
+ NODE_FILE_SYSTEM = {
26133
+ open: (path) => openSync2(
26134
+ path,
26135
+ constants3.O_WRONLY | constants3.O_CREAT | constants3.O_EXCL | (constants3.O_NOFOLLOW || 0),
26136
+ 420
26137
+ ),
26138
+ write: (fd, bytes) => writeFileSync2(fd, bytes),
26139
+ close: (fd) => closeSync2(fd),
26140
+ remove: (path) => unlinkSync2(path)
26141
+ };
26142
+ }
26143
+ });
26144
+
26021
26145
  // web-src/server/journal.ts
26022
26146
  import { join as join18 } from "node:path";
26023
26147
  function dailyJournalFilePath(root) {
@@ -26903,76 +27027,676 @@ var init_search_service = __esm({
26903
27027
  }
26904
27028
  });
26905
27029
 
26906
- // web-src/server/terminal/agent-state.ts
26907
- function clip(value) {
26908
- return value.length > MAX_TEXT_LENGTH ? value.slice(0, MAX_TEXT_LENGTH) : value;
27030
+ // web-src/core/terminal-paste.ts
27031
+ function pasteImageExtension(mime) {
27032
+ if (typeof mime !== "string") return null;
27033
+ const base = mime.split(";")[0]?.trim().toLowerCase() ?? "";
27034
+ return PASTE_IMAGE_TYPES[base] ?? null;
26909
27035
  }
26910
- function evictOldest2() {
26911
- while (states.size > MAX_TRACKED_TARGETS) {
26912
- let oldestKey = null;
26913
- let oldestAt = Number.POSITIVE_INFINITY;
26914
- for (const [key, record] of states) {
26915
- if (record.updatedAt < oldestAt) {
26916
- oldestAt = record.updatedAt;
26917
- oldestKey = key;
26918
- }
26919
- }
26920
- if (oldestKey === null) return;
26921
- states.delete(oldestKey);
26922
- }
27036
+ function looksLikeBase64(value) {
27037
+ return typeof value === "string" && value.length > 0 && /^[A-Za-z0-9+/]+={0,2}$/.test(value);
26923
27038
  }
26924
- function recordAgentState(input) {
26925
- const previous = states.get(input.target);
26926
- const next = input.state ?? (input.event ? agentStateForEvent(input.event, previous?.state ?? null) : null);
26927
- if (!next) return null;
26928
- if (input.source === "activity" && previous?.source === "hook") {
26929
- const promoting = input.override === true && next === "working";
26930
- if (!promoting) return previous;
27039
+ function base64ByteLength(value) {
27040
+ const padding = value.endsWith("==") ? 2 : value.endsWith("=") ? 1 : 0;
27041
+ return Math.floor(value.length * 3 / 4) - padding;
27042
+ }
27043
+ var PASTE_IMAGE_TYPES, MAX_PASTE_IMAGE_BYTES, MAX_PASTE_BODY_BYTES, SHIFT_ENTER_SEQUENCE;
27044
+ var init_terminal_paste = __esm({
27045
+ "web-src/core/terminal-paste.ts"() {
27046
+ PASTE_IMAGE_TYPES = {
27047
+ "image/png": "png",
27048
+ "image/jpeg": "jpg",
27049
+ "image/gif": "gif",
27050
+ "image/webp": "webp"
27051
+ };
27052
+ MAX_PASTE_IMAGE_BYTES = 8 * 1024 * 1024;
27053
+ MAX_PASTE_BODY_BYTES = Math.ceil(MAX_PASTE_IMAGE_BYTES * 1.4);
27054
+ SHIFT_ENTER_SEQUENCE = `${String.fromCharCode(27)}[200~${String.fromCharCode(10)}${String.fromCharCode(27)}[201~`;
26931
27055
  }
26932
- const at = Number.isFinite(input.at) ? input.at : Date.now();
26933
- if (previous && previous.source === "hook" && input.source === "hook" && at < previous.updatedAt) {
26934
- return previous;
27056
+ });
27057
+
27058
+ // web-src/core/terminal-images.ts
27059
+ function stripAnsi(text2) {
27060
+ return text2.replace(ANSI_RE, "");
27061
+ }
27062
+ function terminalImageExtension(path) {
27063
+ const dot = path.lastIndexOf(".");
27064
+ if (dot < 0) return null;
27065
+ const extension = path.slice(dot + 1).toLowerCase();
27066
+ return TERMINAL_IMAGE_EXTENSIONS.includes(extension) ? extension : null;
27067
+ }
27068
+ var TERMINAL_IMAGE_EXTENSIONS, MAX_TERMINAL_IMAGE_QUERY, ESC, BEL, ANSI_RE, PATH_CHAR, NAME_CHAR, IMAGE_PATH_RE;
27069
+ var init_terminal_images = __esm({
27070
+ "web-src/core/terminal-images.ts"() {
27071
+ init_terminal_paste();
27072
+ TERMINAL_IMAGE_EXTENSIONS = [
27073
+ ...new Set(Object.values(PASTE_IMAGE_TYPES)),
27074
+ "jpeg"
27075
+ ];
27076
+ MAX_TERMINAL_IMAGE_QUERY = 16;
27077
+ ESC = String.fromCharCode(27);
27078
+ BEL = String.fromCharCode(7);
27079
+ ANSI_RE = new RegExp(
27080
+ `${ESC}\\[[0-9;?]*[ -/]*[@-~]|${ESC}\\][^${BEL}${ESC}]*(?:${BEL}|${ESC}\\\\)|${ESC}[@-Z\\\\-_]`,
27081
+ "g"
27082
+ );
27083
+ PATH_CHAR = "[\\p{L}\\p{N}._~+@%/-]";
27084
+ NAME_CHAR = "[\\p{L}\\p{N}_~+@%-]";
27085
+ IMAGE_PATH_RE = new RegExp(
27086
+ `${PATH_CHAR}*${NAME_CHAR}\\.(?:${TERMINAL_IMAGE_EXTENSIONS.join("|")})(?![\\p{L}\\p{N}])`,
27087
+ "giu"
27088
+ );
26935
27089
  }
26936
- const record = {
26937
- target: input.target,
26938
- state: next,
26939
- source: input.source,
26940
- updatedAt: at,
26941
- // 添え物は送られてこなければ前の値を残す。ターンの途中で毎回指示文を
26942
- // 送り直させないため。
26943
- lastPrompt: clip(input.lastPrompt ?? previous?.lastPrompt ?? ""),
26944
- note: clip(input.note ?? previous?.note ?? "")
26945
- };
26946
- states.set(input.target, record);
26947
- evictOldest2();
26948
- return record;
27090
+ });
27091
+
27092
+ // web-src/core/agent-screen.ts
27093
+ function isRecord2(value) {
27094
+ return !!value && typeof value === "object" && !Array.isArray(value);
26949
27095
  }
26950
- function getAgentState(target) {
26951
- return states.get(target) ?? null;
27096
+ function issue(errors, path, code, message) {
27097
+ errors.push({ path, code, message });
26952
27098
  }
26953
- function listAgentStates() {
26954
- return [...states.values()].sort((a, b) => {
26955
- const mine = Number(needsAttention(b.state)) - Number(needsAttention(a.state));
26956
- return mine !== 0 ? mine : a.updatedAt - b.updatedAt;
27099
+ function stringList(value, path, maxLength, errors, validate) {
27100
+ if (value === void 0) return void 0;
27101
+ if (!Array.isArray(value)) {
27102
+ issue(errors, path, "invalid_type", "must be an array of strings");
27103
+ return void 0;
27104
+ }
27105
+ if (value.length === 0 || value.length > MAX_MATCHERS_PER_LIST) {
27106
+ issue(
27107
+ errors,
27108
+ path,
27109
+ "invalid_length",
27110
+ `must contain 1-${MAX_MATCHERS_PER_LIST} items`
27111
+ );
27112
+ }
27113
+ const out = [];
27114
+ value.forEach((item, index) => {
27115
+ const itemPath = `${path}[${index}]`;
27116
+ if (typeof item !== "string") {
27117
+ issue(errors, itemPath, "invalid_type", "must be a string");
27118
+ return;
27119
+ }
27120
+ if (item.length === 0 || item.length > maxLength) {
27121
+ issue(
27122
+ errors,
27123
+ itemPath,
27124
+ "invalid_length",
27125
+ `must contain 1-${maxLength} characters`
27126
+ );
27127
+ return;
27128
+ }
27129
+ validate?.(item, itemPath);
27130
+ out.push(item);
26957
27131
  });
27132
+ return out;
26958
27133
  }
26959
- function retainAgentStates(known) {
26960
- let removed = 0;
26961
- for (const target of [...states.keys()]) {
26962
- if (!known.has(target)) {
26963
- states.delete(target);
26964
- removed += 1;
27134
+ function validateRegex(pattern, path, errors) {
27135
+ try {
27136
+ compileNativeRegex(pattern);
27137
+ } catch (error) {
27138
+ issue(
27139
+ errors,
27140
+ path,
27141
+ "invalid_regex",
27142
+ error instanceof Error ? error.message : String(error)
27143
+ );
27144
+ return;
27145
+ }
27146
+ const unsafeReason = unsafeRegexReason(pattern);
27147
+ if (unsafeReason) {
27148
+ issue(errors, path, "unsafe_regex", unsafeReason);
27149
+ return;
27150
+ }
27151
+ }
27152
+ function unsafeRegexReason(pattern) {
27153
+ const source = pattern.startsWith("(?i)") ? pattern.slice(4) : pattern;
27154
+ let inCharacterClass = false;
27155
+ let quantifiers = 0;
27156
+ let variableRepetitions = 0;
27157
+ for (let index = 0; index < source.length; index += 1) {
27158
+ const char = source[index];
27159
+ if (char === "\\") {
27160
+ const escaped = source[index + 1];
27161
+ if (escaped === void 0) break;
27162
+ if (/^[1-9]$/.test(escaped) || escaped === "k") {
27163
+ return "backreferences are not supported";
27164
+ }
27165
+ if ((escaped === "p" || escaped === "P") && source[index + 2] === "{") {
27166
+ const end = source.indexOf("}", index + 3);
27167
+ if (end < 0) break;
27168
+ index = end;
27169
+ continue;
27170
+ }
27171
+ index += 1;
27172
+ continue;
27173
+ }
27174
+ if (char === "[") {
27175
+ inCharacterClass = true;
27176
+ continue;
27177
+ }
27178
+ if (char === "]" && inCharacterClass) {
27179
+ inCharacterClass = false;
27180
+ continue;
26965
27181
  }
27182
+ if (inCharacterClass) continue;
27183
+ if (char === "(" || char === ")" || char === "|") {
27184
+ return "groups and alternation are not supported; use all or any matchers";
27185
+ }
27186
+ if (char === "*" || char === "+" || char === "?") {
27187
+ quantifiers += 1;
27188
+ variableRepetitions += 1;
27189
+ continue;
27190
+ }
27191
+ if (char !== "{") continue;
27192
+ const match = source.slice(index).match(/^\{(\d+)(?:,(\d*))?\}/);
27193
+ if (!match) continue;
27194
+ quantifiers += 1;
27195
+ const lower = Number(match[1]);
27196
+ const hasComma = match[2] !== void 0;
27197
+ const upper = hasComma && match[2] !== "" ? Number(match[2]) : lower;
27198
+ if (hasComma && (match[2] === "" || upper !== lower)) {
27199
+ variableRepetitions += 1;
27200
+ }
27201
+ if (lower > MAX_REGEX_BOUNDED_REPEAT || upper > MAX_REGEX_BOUNDED_REPEAT) {
27202
+ return `bounded repetitions must not exceed ${MAX_REGEX_BOUNDED_REPEAT}`;
27203
+ }
27204
+ index += match[0].length - 1;
26966
27205
  }
26967
- return removed;
27206
+ if (quantifiers > MAX_REGEX_QUANTIFIERS) {
27207
+ return `must not contain more than ${MAX_REGEX_QUANTIFIERS} quantifiers`;
27208
+ }
27209
+ if (variableRepetitions > MAX_REGEX_VARIABLE_REPETITIONS) {
27210
+ return "must not contain more than one variable-length repetition";
27211
+ }
27212
+ return null;
26968
27213
  }
26969
- var MAX_TRACKED_TARGETS, MAX_TEXT_LENGTH, states;
26970
- var init_agent_state2 = __esm({
26971
- "web-src/server/terminal/agent-state.ts"() {
26972
- init_agent_state();
26973
- MAX_TRACKED_TARGETS = 200;
26974
- MAX_TEXT_LENGTH = 2e3;
26975
- states = /* @__PURE__ */ new Map();
27214
+ function parseMatcher(raw, path, depth, errors) {
27215
+ if (!isRecord2(raw)) {
27216
+ issue(errors, path, "invalid_type", "must be an object");
27217
+ return null;
27218
+ }
27219
+ if (depth > MAX_MATCHER_DEPTH) {
27220
+ issue(
27221
+ errors,
27222
+ path,
27223
+ "too_deep",
27224
+ `matcher nesting must not exceed ${MAX_MATCHER_DEPTH}`
27225
+ );
27226
+ return null;
27227
+ }
27228
+ for (const key of Object.keys(raw)) {
27229
+ if (!MATCHER_KEYS.has(key)) {
27230
+ issue(errors, `${path}.${key}`, "unknown_field", "is not supported");
27231
+ }
27232
+ }
27233
+ const matcher = {};
27234
+ const contains = stringList(
27235
+ raw.contains,
27236
+ `${path}.contains`,
27237
+ MAX_CONTAINS_LENGTH,
27238
+ errors
27239
+ );
27240
+ if (contains) matcher.contains = contains;
27241
+ const regex = stringList(
27242
+ raw.regex,
27243
+ `${path}.regex`,
27244
+ MAX_PATTERN_LENGTH,
27245
+ errors,
27246
+ (pattern, itemPath) => validateRegex(pattern, itemPath, errors)
27247
+ );
27248
+ if (regex) matcher.regex = regex;
27249
+ const lineRegex = stringList(
27250
+ raw.lineRegex,
27251
+ `${path}.lineRegex`,
27252
+ MAX_PATTERN_LENGTH,
27253
+ errors,
27254
+ (pattern, itemPath) => validateRegex(pattern, itemPath, errors)
27255
+ );
27256
+ if (lineRegex) matcher.lineRegex = lineRegex;
27257
+ for (const key of ["all", "any", "not"]) {
27258
+ const value = raw[key];
27259
+ if (value === void 0) continue;
27260
+ if (!Array.isArray(value)) {
27261
+ issue(errors, `${path}.${key}`, "invalid_type", "must be an array");
27262
+ continue;
27263
+ }
27264
+ if (value.length === 0 || value.length > MAX_MATCHERS_PER_LIST) {
27265
+ issue(
27266
+ errors,
27267
+ `${path}.${key}`,
27268
+ "invalid_length",
27269
+ `must contain 1-${MAX_MATCHERS_PER_LIST} matchers`
27270
+ );
27271
+ }
27272
+ const nested = value.flatMap((item, index) => {
27273
+ const parsed = parseMatcher(
27274
+ item,
27275
+ `${path}.${key}[${index}]`,
27276
+ depth + 1,
27277
+ errors
27278
+ );
27279
+ return parsed ? [parsed] : [];
27280
+ });
27281
+ matcher[key] = nested;
27282
+ }
27283
+ if (!Object.keys(matcher).length) {
27284
+ issue(errors, path, "empty_matcher", "must contain a match condition");
27285
+ }
27286
+ return matcher;
27287
+ }
27288
+ function parseRule(raw, index, errors) {
27289
+ const path = `rules[${index}]`;
27290
+ if (!isRecord2(raw)) {
27291
+ issue(errors, path, "invalid_type", "must be an object");
27292
+ return null;
27293
+ }
27294
+ for (const key of Object.keys(raw)) {
27295
+ if (!RULE_KEYS.has(key)) {
27296
+ issue(errors, `${path}.${key}`, "unknown_field", "is not supported");
27297
+ }
27298
+ }
27299
+ const matcher = parseMatcher(
27300
+ Object.fromEntries(
27301
+ Object.entries(raw).filter(([key]) => MATCHER_KEYS.has(key))
27302
+ ),
27303
+ path,
27304
+ 0,
27305
+ errors
27306
+ );
27307
+ const id = raw.id;
27308
+ if (typeof id !== "string" || !RULE_ID_RE.test(id)) {
27309
+ issue(
27310
+ errors,
27311
+ `${path}.id`,
27312
+ "invalid_id",
27313
+ "must use 1-64 lowercase letters, digits, underscores, or hyphens"
27314
+ );
27315
+ }
27316
+ const state = raw.state;
27317
+ if (state !== "working" && state !== "waiting" && state !== "idle" && state !== "skip") {
27318
+ issue(
27319
+ errors,
27320
+ `${path}.state`,
27321
+ "invalid_state",
27322
+ "must be working, waiting, idle, or skip"
27323
+ );
27324
+ }
27325
+ const priority = raw.priority;
27326
+ if (typeof priority !== "number" || !Number.isInteger(priority) || Math.abs(priority) > MAX_PRIORITY) {
27327
+ issue(
27328
+ errors,
27329
+ `${path}.priority`,
27330
+ "invalid_priority",
27331
+ `must be an integer between -${MAX_PRIORITY} and ${MAX_PRIORITY}`
27332
+ );
27333
+ }
27334
+ const region = raw.region;
27335
+ if (typeof region !== "string" || !AGENT_SCREEN_REGIONS.includes(region)) {
27336
+ issue(
27337
+ errors,
27338
+ `${path}.region`,
27339
+ "invalid_region",
27340
+ `must be one of ${AGENT_SCREEN_REGIONS.join(", ")}`
27341
+ );
27342
+ }
27343
+ const lines = raw.lines;
27344
+ if (region === "bottom_non_empty") {
27345
+ if (typeof lines !== "number" || !Number.isInteger(lines) || lines < 1 || lines > MAX_REGION_LINES) {
27346
+ issue(
27347
+ errors,
27348
+ `${path}.lines`,
27349
+ "invalid_lines",
27350
+ `must be an integer between 1 and ${MAX_REGION_LINES}`
27351
+ );
27352
+ }
27353
+ } else if (lines !== void 0) {
27354
+ issue(
27355
+ errors,
27356
+ `${path}.lines`,
27357
+ "unexpected_lines",
27358
+ "is only valid with bottom_non_empty"
27359
+ );
27360
+ }
27361
+ if (!matcher || typeof id !== "string" || !RULE_ID_RE.test(id) || state !== "working" && state !== "waiting" && state !== "idle" && state !== "skip" || typeof priority !== "number" || !Number.isInteger(priority) || Math.abs(priority) > MAX_PRIORITY || typeof region !== "string" || !AGENT_SCREEN_REGIONS.includes(region) || region === "bottom_non_empty" && (typeof lines !== "number" || !Number.isInteger(lines) || lines < 1 || lines > MAX_REGION_LINES) || region !== "bottom_non_empty" && lines !== void 0) {
27362
+ return null;
27363
+ }
27364
+ return {
27365
+ id,
27366
+ state,
27367
+ priority,
27368
+ region,
27369
+ ...typeof lines === "number" ? { lines } : {},
27370
+ ...matcher
27371
+ };
27372
+ }
27373
+ function parseAgentScreenRuleSet(raw) {
27374
+ const errors = [];
27375
+ if (!isRecord2(raw)) {
27376
+ return {
27377
+ ok: false,
27378
+ errors: [
27379
+ { path: "$", code: "invalid_type", message: "must be an object" }
27380
+ ]
27381
+ };
27382
+ }
27383
+ for (const key of Object.keys(raw)) {
27384
+ if (key !== "version" && key !== "rules") {
27385
+ issue(errors, key, "unknown_field", "is not supported");
27386
+ }
27387
+ }
27388
+ if (raw.version !== AGENT_SCREEN_RULE_SET_VERSION) {
27389
+ issue(
27390
+ errors,
27391
+ "version",
27392
+ "unsupported_version",
27393
+ `must be ${AGENT_SCREEN_RULE_SET_VERSION}`
27394
+ );
27395
+ }
27396
+ if (!Array.isArray(raw.rules)) {
27397
+ issue(errors, "rules", "invalid_type", "must be an array");
27398
+ return { ok: false, errors };
27399
+ }
27400
+ if (raw.rules.length > MAX_RULES) {
27401
+ issue(
27402
+ errors,
27403
+ "rules",
27404
+ "too_many_rules",
27405
+ `must contain at most ${MAX_RULES} rules`
27406
+ );
27407
+ }
27408
+ const rules = raw.rules.flatMap((item, index) => {
27409
+ const parsed = parseRule(item, index, errors);
27410
+ return parsed ? [parsed] : [];
27411
+ });
27412
+ const ids = /* @__PURE__ */ new Map();
27413
+ raw.rules.forEach((rule, index) => {
27414
+ if (!isRecord2(rule) || typeof rule.id !== "string" || !RULE_ID_RE.test(rule.id)) {
27415
+ return;
27416
+ }
27417
+ const previous = ids.get(rule.id);
27418
+ if (previous !== void 0) {
27419
+ issue(
27420
+ errors,
27421
+ `rules[${index}].id`,
27422
+ "duplicate_id",
27423
+ `duplicates rules[${previous}].id`
27424
+ );
27425
+ } else {
27426
+ ids.set(rule.id, index);
27427
+ }
27428
+ });
27429
+ if (errors.length) return { ok: false, errors };
27430
+ return {
27431
+ ok: true,
27432
+ value: { version: AGENT_SCREEN_RULE_SET_VERSION, rules }
27433
+ };
27434
+ }
27435
+ function formatAgentScreenRuleSet(rules) {
27436
+ return `${JSON.stringify(rules, null, 2)}
27437
+ `;
27438
+ }
27439
+ function lastOscTitle(raw) {
27440
+ let title = "";
27441
+ for (const match of raw.matchAll(OSC_TITLE_RE)) title = match[1] ?? "";
27442
+ return title;
27443
+ }
27444
+ function recentScreen(raw) {
27445
+ const tail = raw.slice(-MAX_RAW_SCREEN_CHARS);
27446
+ const lines = stripAnsi(tail).split("\r").join("\n").split("\n");
27447
+ return lines.slice(-MAX_RECENT_LINES).join("\n");
27448
+ }
27449
+ function nonEmptyLines(text2) {
27450
+ return text2.split("\n").filter((line) => line.trim() !== "");
27451
+ }
27452
+ function regionText(rule, screen, title) {
27453
+ if (rule.region === "osc_title") return title;
27454
+ if (rule.region === "whole_recent") return screen;
27455
+ const lines = nonEmptyLines(screen);
27456
+ if (rule.region === "last_non_empty") return lines[lines.length - 1] ?? "";
27457
+ return lines.slice(-(rule.lines ?? 1)).join("\n");
27458
+ }
27459
+ function compileNativeRegex(pattern) {
27460
+ const caseInsensitive = pattern.startsWith("(?i)");
27461
+ return new RegExp(
27462
+ caseInsensitive ? pattern.slice(4) : pattern,
27463
+ caseInsensitive ? "iu" : "u"
27464
+ );
27465
+ }
27466
+ function compileRegex(pattern) {
27467
+ const unsafeReason = unsafeRegexReason(pattern);
27468
+ if (unsafeReason) throw new Error(unsafeReason);
27469
+ return compileNativeRegex(pattern);
27470
+ }
27471
+ function regexMatches(pattern, text2) {
27472
+ return compileRegex(pattern).test(text2);
27473
+ }
27474
+ function matcherMatches(matcher, text2) {
27475
+ const lower = text2.toLowerCase();
27476
+ if (!(matcher.contains ?? []).every(
27477
+ (value) => lower.includes(value.toLowerCase())
27478
+ )) {
27479
+ return false;
27480
+ }
27481
+ if (!(matcher.regex ?? []).every((pattern) => regexMatches(pattern, text2))) {
27482
+ return false;
27483
+ }
27484
+ const lines = text2.split("\n");
27485
+ if (!(matcher.lineRegex ?? []).every(
27486
+ (pattern) => lines.some((line) => regexMatches(pattern, line))
27487
+ )) {
27488
+ return false;
27489
+ }
27490
+ if (!(matcher.all ?? []).every((nested) => matcherMatches(nested, text2))) {
27491
+ return false;
27492
+ }
27493
+ if ((matcher.any?.length ?? 0) > 0 && !matcher.any?.some((nested) => matcherMatches(nested, text2))) {
27494
+ return false;
27495
+ }
27496
+ return !(matcher.not ?? []).some((nested) => matcherMatches(nested, text2));
27497
+ }
27498
+ function detectAgentScreen(input, ruleSet = DEFAULT_AGENT_SCREEN_RULES) {
27499
+ const rawTail = input.screen.slice(-MAX_RAW_SCREEN_CHARS);
27500
+ const screen = recentScreen(input.screen);
27501
+ const title = stripAnsi(input.title || lastOscTitle(rawTail));
27502
+ let winner = null;
27503
+ for (const rule of ruleSet.rules) {
27504
+ const text2 = regionText(rule, screen, title);
27505
+ if (!matcherMatches(rule, text2)) continue;
27506
+ if (!winner || rule.priority > winner.priority) winner = rule;
27507
+ }
27508
+ if (!winner) return { kind: "none" };
27509
+ if (winner.state === "skip") {
27510
+ return { kind: "skip", ruleId: winner.id, priority: winner.priority };
27511
+ }
27512
+ return {
27513
+ kind: "state",
27514
+ state: winner.state,
27515
+ ruleId: winner.id,
27516
+ priority: winner.priority
27517
+ };
27518
+ }
27519
+ var AGENT_SCREEN_RULE_SET_VERSION, AGENT_SCREEN_REGIONS, MAX_RAW_SCREEN_CHARS, MAX_RECENT_LINES, MAX_RULES, MAX_MATCHERS_PER_LIST, MAX_MATCHER_DEPTH, MAX_PATTERN_LENGTH, MAX_CONTAINS_LENGTH, MAX_REGION_LINES, MAX_PRIORITY, RULE_ID_RE, ESC2, BEL2, OSC_TITLE_RE, BLOCKING_HINTS, DEFAULT_AGENT_SCREEN_RULES, MATCHER_KEYS, RULE_KEYS, MAX_REGEX_QUANTIFIERS, MAX_REGEX_BOUNDED_REPEAT, MAX_REGEX_VARIABLE_REPETITIONS;
27520
+ var init_agent_screen = __esm({
27521
+ "web-src/core/agent-screen.ts"() {
27522
+ init_terminal_images();
27523
+ AGENT_SCREEN_RULE_SET_VERSION = 1;
27524
+ AGENT_SCREEN_REGIONS = [
27525
+ "osc_title",
27526
+ "whole_recent",
27527
+ "bottom_non_empty",
27528
+ "last_non_empty"
27529
+ ];
27530
+ MAX_RAW_SCREEN_CHARS = 64e3;
27531
+ MAX_RECENT_LINES = 120;
27532
+ MAX_RULES = 100;
27533
+ MAX_MATCHERS_PER_LIST = 20;
27534
+ MAX_MATCHER_DEPTH = 4;
27535
+ MAX_PATTERN_LENGTH = 1e3;
27536
+ MAX_CONTAINS_LENGTH = 200;
27537
+ MAX_REGION_LINES = 120;
27538
+ MAX_PRIORITY = 1e5;
27539
+ RULE_ID_RE = /^[a-z0-9][a-z0-9_-]{0,63}$/;
27540
+ ESC2 = String.fromCharCode(27);
27541
+ BEL2 = String.fromCharCode(7);
27542
+ OSC_TITLE_RE = new RegExp(
27543
+ `${ESC2}\\](?:0|2);([^${BEL2}${ESC2}]*)(?:${BEL2}|${ESC2}\\\\)`,
27544
+ "g"
27545
+ );
27546
+ BLOCKING_HINTS = [
27547
+ { contains: ["enter to confirm"] },
27548
+ { contains: ["enter to select"] },
27549
+ { contains: ["enter to submit"] },
27550
+ { contains: ["allow command?"] },
27551
+ { contains: ["[y/n]"] },
27552
+ { contains: ["yes (y)"] },
27553
+ { contains: ["do you want to proceed?"] }
27554
+ ];
27555
+ DEFAULT_AGENT_SCREEN_RULES = {
27556
+ version: AGENT_SCREEN_RULE_SET_VERSION,
27557
+ rules: [
27558
+ {
27559
+ id: "title_requires_input",
27560
+ state: "waiting",
27561
+ priority: 1100,
27562
+ region: "osc_title",
27563
+ contains: ["action required"]
27564
+ },
27565
+ {
27566
+ id: "title_spinner",
27567
+ state: "working",
27568
+ priority: 1050,
27569
+ region: "osc_title",
27570
+ regex: ["^[⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏]"]
27571
+ },
27572
+ {
27573
+ id: "transcript_view",
27574
+ state: "skip",
27575
+ priority: 1e3,
27576
+ region: "bottom_non_empty",
27577
+ lines: 8,
27578
+ any: [
27579
+ { contains: ["showing detailed transcript"] },
27580
+ { contains: ["pgup/pgdn", "home/end to jump", "q to quit"] }
27581
+ ]
27582
+ },
27583
+ {
27584
+ id: "interactive_form",
27585
+ state: "waiting",
27586
+ priority: 980,
27587
+ region: "bottom_non_empty",
27588
+ lines: 14,
27589
+ contains: ["esc to cancel"],
27590
+ any: [
27591
+ { contains: ["enter to confirm"] },
27592
+ { contains: ["enter to select"] },
27593
+ { contains: ["enter to submit"] }
27594
+ ]
27595
+ },
27596
+ {
27597
+ id: "live_reasoning",
27598
+ state: "working",
27599
+ priority: 970,
27600
+ region: "bottom_non_empty",
27601
+ lines: 8,
27602
+ contains: ["tokens", "thinking"],
27603
+ any: [{ lineRegex: ["^\\s*[✢✻✽✶✳]"] }, { lineRegex: ["(?i)^\\s*·"] }]
27604
+ },
27605
+ {
27606
+ id: "prompt_box",
27607
+ state: "idle",
27608
+ priority: 950,
27609
+ region: "bottom_non_empty",
27610
+ lines: 8,
27611
+ lineRegex: ["^\\s*❯"],
27612
+ not: [
27613
+ ...BLOCKING_HINTS,
27614
+ { contains: ["esc to cancel"] },
27615
+ { contains: ["arrow keys"] },
27616
+ { contains: ["↑/↓ to navigate"] }
27617
+ ]
27618
+ },
27619
+ {
27620
+ id: "strong_input_request",
27621
+ state: "waiting",
27622
+ priority: 900,
27623
+ region: "bottom_non_empty",
27624
+ lines: 12,
27625
+ any: [
27626
+ { contains: ["press enter to confirm or esc to cancel"] },
27627
+ { contains: ["enter to submit answer"] },
27628
+ { contains: ["enter to submit all"] },
27629
+ { contains: ["allow command?"] }
27630
+ ]
27631
+ },
27632
+ {
27633
+ id: "permission_request",
27634
+ state: "waiting",
27635
+ priority: 850,
27636
+ region: "bottom_non_empty",
27637
+ lines: 14,
27638
+ contains: ["do you want to proceed?"],
27639
+ any: [
27640
+ { lineRegex: ["(?i)^\\P{L}*yes\\b"] },
27641
+ { lineRegex: ["(?i)^\\P{L}*no\\b"] }
27642
+ ]
27643
+ },
27644
+ {
27645
+ id: "weak_input_request",
27646
+ state: "waiting",
27647
+ priority: 600,
27648
+ region: "bottom_non_empty",
27649
+ lines: 8,
27650
+ any: [
27651
+ { contains: ["[y/n]"] },
27652
+ { contains: ["yes (y)"] },
27653
+ {
27654
+ any: [
27655
+ { contains: ["do you want to"] },
27656
+ { contains: ["would you like to"] }
27657
+ ],
27658
+ all: [{ any: [{ contains: ["yes"] }, { contains: ["❯"] }] }]
27659
+ }
27660
+ ]
27661
+ },
27662
+ {
27663
+ id: "live_working_status",
27664
+ state: "working",
27665
+ priority: 500,
27666
+ region: "bottom_non_empty",
27667
+ lines: 3,
27668
+ lineRegex: ["^[•◦]\\s+Working"],
27669
+ not: [{ contains: ["conversation interrupted"] }]
27670
+ },
27671
+ {
27672
+ id: "last_prompt",
27673
+ state: "idle",
27674
+ priority: 400,
27675
+ region: "last_non_empty",
27676
+ lineRegex: ["^\\s*[›❯]"],
27677
+ not: BLOCKING_HINTS
27678
+ }
27679
+ ]
27680
+ };
27681
+ MATCHER_KEYS = /* @__PURE__ */ new Set([
27682
+ "contains",
27683
+ "regex",
27684
+ "lineRegex",
27685
+ "all",
27686
+ "any",
27687
+ "not"
27688
+ ]);
27689
+ RULE_KEYS = /* @__PURE__ */ new Set([
27690
+ ...MATCHER_KEYS,
27691
+ "id",
27692
+ "state",
27693
+ "priority",
27694
+ "region",
27695
+ "lines"
27696
+ ]);
27697
+ MAX_REGEX_QUANTIFIERS = 8;
27698
+ MAX_REGEX_BOUNDED_REPEAT = 100;
27699
+ MAX_REGEX_VARIABLE_REPETITIONS = 1;
26976
27700
  }
26977
27701
  });
26978
27702
 
@@ -27138,6 +27862,509 @@ var init_capture = __esm({
27138
27862
  }
27139
27863
  });
27140
27864
 
27865
+ // web-src/server/tmux/panes.ts
27866
+ function toInt(value) {
27867
+ const parsed = Number.parseInt(value ?? "", 10);
27868
+ return Number.isFinite(parsed) ? parsed : 0;
27869
+ }
27870
+ function toFlag(value) {
27871
+ return value === "1";
27872
+ }
27873
+ function parseTmuxPanes(stdout, worktrees = []) {
27874
+ const sessions2 = [];
27875
+ const sessionByName = /* @__PURE__ */ new Map();
27876
+ const windowByKey = /* @__PURE__ */ new Map();
27877
+ for (const line of stdout.split("\n")) {
27878
+ if (!line) continue;
27879
+ const fields = line.split(TMUX_FIELD_SEP);
27880
+ if (fields.length < PANE_FIELDS.length) continue;
27881
+ const paneId = fields[FIELD.paneId];
27882
+ if (!paneId) continue;
27883
+ const sessionName = fields[FIELD.sessionName] ?? "";
27884
+ let session = sessionByName.get(sessionName);
27885
+ if (!session) {
27886
+ session = {
27887
+ name: sessionName,
27888
+ attached: toFlag(fields[FIELD.sessionAttached]),
27889
+ windows: []
27890
+ };
27891
+ sessionByName.set(sessionName, session);
27892
+ sessions2.push(session);
27893
+ }
27894
+ const windowIndex = toInt(fields[FIELD.windowIndex]);
27895
+ const windowKey = `${sessionName}${TMUX_FIELD_SEP}${windowIndex}`;
27896
+ let window = windowByKey.get(windowKey);
27897
+ if (!window) {
27898
+ window = {
27899
+ index: windowIndex,
27900
+ name: fields[FIELD.windowName] ?? "",
27901
+ active: toFlag(fields[FIELD.windowActive]),
27902
+ panes: []
27903
+ };
27904
+ windowByKey.set(windowKey, window);
27905
+ session.windows.push(window);
27906
+ }
27907
+ const paneIndex = toInt(fields[FIELD.paneIndex]);
27908
+ const pane = {
27909
+ id: paneId,
27910
+ label: `${sessionName}:${windowIndex}.${paneIndex}`,
27911
+ paneIndex,
27912
+ title: fields[FIELD.paneTitle] ?? "",
27913
+ command: fields[FIELD.paneCommand] ?? "",
27914
+ path: fields[FIELD.panePath] ?? "",
27915
+ width: toInt(fields[FIELD.paneWidth]),
27916
+ height: toInt(fields[FIELD.paneHeight]),
27917
+ active: toFlag(fields[FIELD.paneActive]),
27918
+ inRepo: worktrees.length === 0 || isPathInsideAny(fields[FIELD.panePath] ?? "", worktrees)
27919
+ };
27920
+ window.panes.push(pane);
27921
+ }
27922
+ return sessions2;
27923
+ }
27924
+ async function listTmuxPanes(cwd2) {
27925
+ const [result, worktrees] = await Promise.all([
27926
+ runTmux(["list-panes", "-a", "-F", PANE_FORMAT], cwd2),
27927
+ worktreePathsAsync(cwd2)
27928
+ ]);
27929
+ if (result.status === "missing") {
27930
+ return { available: false, running: false, sessions: [] };
27931
+ }
27932
+ if (result.status === "no-server" || result.status === "no-target") {
27933
+ return { available: true, running: false, sessions: [] };
27934
+ }
27935
+ if (result.status === "error") {
27936
+ throw errorWithCause("failed to list tmux panes", result.error);
27937
+ }
27938
+ return {
27939
+ available: true,
27940
+ running: true,
27941
+ sessions: parseTmuxPanes(result.stdout, worktrees)
27942
+ };
27943
+ }
27944
+ var PANE_FIELDS, PANE_FORMAT, FIELD;
27945
+ var init_panes = __esm({
27946
+ "web-src/server/tmux/panes.ts"() {
27947
+ init_error_detail();
27948
+ init_tmux();
27949
+ init_git();
27950
+ init_command();
27951
+ PANE_FIELDS = [
27952
+ "#{pane_id}",
27953
+ "#{session_name}",
27954
+ "#{session_attached}",
27955
+ "#{window_index}",
27956
+ "#{window_name}",
27957
+ "#{window_active}",
27958
+ "#{pane_index}",
27959
+ "#{pane_active}",
27960
+ "#{pane_width}",
27961
+ "#{pane_height}",
27962
+ "#{pane_current_command}",
27963
+ "#{pane_current_path}",
27964
+ // タイトルは自由文字列なので必ず最後に置く。
27965
+ "#{pane_title}"
27966
+ ];
27967
+ PANE_FORMAT = PANE_FIELDS.join(TMUX_FIELD_SEP);
27968
+ FIELD = {
27969
+ paneId: 0,
27970
+ sessionName: 1,
27971
+ sessionAttached: 2,
27972
+ windowIndex: 3,
27973
+ windowName: 4,
27974
+ windowActive: 5,
27975
+ paneIndex: 6,
27976
+ paneActive: 7,
27977
+ paneWidth: 8,
27978
+ paneHeight: 9,
27979
+ paneCommand: 10,
27980
+ panePath: 11,
27981
+ paneTitle: 12
27982
+ };
27983
+ }
27984
+ });
27985
+
27986
+ // web-src/server/terminal/agent-state.ts
27987
+ function clip(value) {
27988
+ return value.length > MAX_TEXT_LENGTH ? value.slice(0, MAX_TEXT_LENGTH) : value;
27989
+ }
27990
+ function evictOldest2() {
27991
+ while (states.size > MAX_TRACKED_TARGETS) {
27992
+ let oldestKey = null;
27993
+ let oldestAt = Number.POSITIVE_INFINITY;
27994
+ for (const [key, record] of states) {
27995
+ if (record.updatedAt < oldestAt) {
27996
+ oldestAt = record.updatedAt;
27997
+ oldestKey = key;
27998
+ }
27999
+ }
28000
+ if (oldestKey === null) return;
28001
+ states.delete(oldestKey);
28002
+ }
28003
+ }
28004
+ function recordAgentState(input) {
28005
+ const previous = states.get(input.target);
28006
+ const next = input.state ?? (input.event ? agentStateForEvent(input.event, previous?.state ?? null) : null);
28007
+ if (!next) return null;
28008
+ if (input.source !== "hook" && previous?.source === "hook") {
28009
+ if (previous.state === "done") return previous;
28010
+ const visibleRule = input.source === "screen" && input.override === true;
28011
+ const motionPromoting = input.source === "activity" && input.override === true && next === "working";
28012
+ if (!visibleRule && !motionPromoting) return previous;
28013
+ }
28014
+ const at = Number.isFinite(input.at) ? input.at : Date.now();
28015
+ if (previous && previous.source === "hook" && input.source === "hook" && at < previous.updatedAt) {
28016
+ return previous;
28017
+ }
28018
+ const record = {
28019
+ target: input.target,
28020
+ state: next,
28021
+ source: input.source,
28022
+ updatedAt: input.source !== "hook" && previous?.state === next ? previous.updatedAt : at,
28023
+ // 添え物は送られてこなければ前の値を残す。ターンの途中で毎回指示文を
28024
+ // 送り直させないため。
28025
+ lastPrompt: clip(input.lastPrompt ?? previous?.lastPrompt ?? ""),
28026
+ note: clip(input.note ?? previous?.note ?? "")
28027
+ };
28028
+ states.set(input.target, record);
28029
+ evictOldest2();
28030
+ return record;
28031
+ }
28032
+ function getAgentState(target) {
28033
+ return states.get(target) ?? null;
28034
+ }
28035
+ function listAgentStates() {
28036
+ return [...states.values()].sort((a, b) => {
28037
+ const mine = Number(needsAttention(b.state)) - Number(needsAttention(a.state));
28038
+ return mine !== 0 ? mine : a.updatedAt - b.updatedAt;
28039
+ });
28040
+ }
28041
+ function retainAgentStates(known) {
28042
+ let removed = 0;
28043
+ for (const target of [...states.keys()]) {
28044
+ if (!known.has(target)) {
28045
+ states.delete(target);
28046
+ removed += 1;
28047
+ }
28048
+ }
28049
+ return removed;
28050
+ }
28051
+ var MAX_TRACKED_TARGETS, MAX_TEXT_LENGTH, states;
28052
+ var init_agent_state2 = __esm({
28053
+ "web-src/server/terminal/agent-state.ts"() {
28054
+ init_agent_state();
28055
+ MAX_TRACKED_TARGETS = 200;
28056
+ MAX_TEXT_LENGTH = 2e3;
28057
+ states = /* @__PURE__ */ new Map();
28058
+ }
28059
+ });
28060
+
28061
+ // web-src/server/terminal/rules.ts
28062
+ import { join as join20 } from "node:path";
28063
+ function errorIssue(code, error) {
28064
+ return {
28065
+ path: "$",
28066
+ code,
28067
+ message: formatErrorDetail(error),
28068
+ ...error instanceof Error && error.stack ? { stack: error.stack } : {}
28069
+ };
28070
+ }
28071
+ function defaultResponse(errors = []) {
28072
+ return { rules: DEFAULT_AGENT_SCREEN_RULES, source: "default", errors };
28073
+ }
28074
+ function agentScreenRulesFilePath(root) {
28075
+ return join20(root, ".code-viewer", RULES_FILE_NAME);
28076
+ }
28077
+ function parseStoredRules(raw) {
28078
+ const parsed = parseAgentScreenRuleSet(raw);
28079
+ if ("errors" in parsed) {
28080
+ throw Object.assign(new Error("saved terminal rules are invalid"), {
28081
+ issues: parsed.errors
28082
+ });
28083
+ }
28084
+ return parsed.value;
28085
+ }
28086
+ function getActiveAgentScreenRules() {
28087
+ return activeRules;
28088
+ }
28089
+ function issuesFromLoadError(error) {
28090
+ const seen2 = /* @__PURE__ */ new Set();
28091
+ let current = error;
28092
+ while (current && typeof current === "object" && !seen2.has(current)) {
28093
+ seen2.add(current);
28094
+ const issues = current.issues;
28095
+ if (Array.isArray(issues)) {
28096
+ return issues.filter(
28097
+ (issue2) => !!issue2 && typeof issue2 === "object" && typeof issue2.path === "string" && typeof issue2.code === "string" && typeof issue2.message === "string"
28098
+ );
28099
+ }
28100
+ if (current instanceof SyntaxError)
28101
+ return [errorIssue("invalid_json", current)];
28102
+ current = current.cause;
28103
+ }
28104
+ return [errorIssue("load_failed", error)];
28105
+ }
28106
+ function activate(response) {
28107
+ activeRules = response.rules;
28108
+ activeErrors = response.errors.map((error) => ({ ...error }));
28109
+ activeGeneration += 1;
28110
+ return { ...response, generation: activeGeneration };
28111
+ }
28112
+ async function reloadAgentScreenRules(root) {
28113
+ try {
28114
+ const rules = await rulesStore.load(root);
28115
+ return activate(
28116
+ rules === null ? defaultResponse() : { rules, source: "saved", errors: [] }
28117
+ );
28118
+ } catch (error) {
28119
+ console.error("[code-viewer] terminal rule load failed", error);
28120
+ return activate(defaultResponse(issuesFromLoadError(error)));
28121
+ }
28122
+ }
28123
+ async function saveAgentScreenRules(root, raw) {
28124
+ const parsed = parseAgentScreenRuleSet(raw);
28125
+ if ("errors" in parsed) return { errors: parsed.errors };
28126
+ await rulesStore.save(root, parsed.value);
28127
+ return activate({ rules: parsed.value, source: "saved", errors: [] });
28128
+ }
28129
+ async function resetAgentScreenRules(root) {
28130
+ await rulesStore.remove(root);
28131
+ return activate(defaultResponse());
28132
+ }
28133
+ var MAX_AGENT_SCREEN_RULES_BYTES, RULES_FILE_NAME, activeRules, activeErrors, activeGeneration, rulesStore;
28134
+ var init_rules = __esm({
28135
+ "web-src/server/terminal/rules.ts"() {
28136
+ init_agent_screen();
28137
+ init_error_detail();
28138
+ init_json_store();
28139
+ MAX_AGENT_SCREEN_RULES_BYTES = 2e5;
28140
+ RULES_FILE_NAME = "agent-screen-rules.json";
28141
+ activeRules = DEFAULT_AGENT_SCREEN_RULES;
28142
+ activeErrors = [];
28143
+ activeGeneration = 0;
28144
+ rulesStore = createJsonFileStore({
28145
+ filePath: agentScreenRulesFilePath,
28146
+ empty: () => null,
28147
+ sanitize: (raw) => parseStoredRules(raw),
28148
+ maxBytes: MAX_AGENT_SCREEN_RULES_BYTES,
28149
+ backupSuffix: "corrupt",
28150
+ sizeErrorMessage: `terminal rules must not exceed ${MAX_AGENT_SCREEN_RULES_BYTES} bytes`,
28151
+ serialize: (rules) => {
28152
+ if (rules === null) throw new Error("terminal rules must not be null");
28153
+ return formatAgentScreenRuleSet(rules);
28154
+ },
28155
+ invalidFileBehavior: "throw"
28156
+ });
28157
+ }
28158
+ });
28159
+
28160
+ // web-src/server/terminal/activity.ts
28161
+ var activity_exports = {};
28162
+ __export(activity_exports, {
28163
+ ACTIVITY_IDLE_AFTER_MS: () => ACTIVITY_IDLE_AFTER_MS,
28164
+ ACTIVITY_POLL_INTERVAL_MS: () => ACTIVITY_POLL_INTERVAL_MS,
28165
+ MAX_PANES_PER_SWEEP: () => MAX_PANES_PER_SWEEP,
28166
+ OVERRIDE_CHANGE_STREAK: () => OVERRIDE_CHANGE_STREAK,
28167
+ getAgentActivityErrors: () => getAgentActivityErrors,
28168
+ nextActivityState: () => nextActivityState,
28169
+ nextObservedState: () => nextObservedState,
28170
+ rotateForSweep: () => rotateForSweep,
28171
+ startAgentActivityWatch: () => startAgentActivityWatch,
28172
+ stopAgentActivityWatch: () => stopAgentActivityWatch
28173
+ });
28174
+ function nextActivityState(previous, hash, now) {
28175
+ const changed = previous === void 0 || previous.hash !== hash;
28176
+ const changedAt = changed ? now : previous.changedAt;
28177
+ const changeStreak = changed ? previous === void 0 ? 0 : previous.changeStreak + 1 : 0;
28178
+ return {
28179
+ state: agentStateFromActivity(
28180
+ changed,
28181
+ now - changedAt,
28182
+ ACTIVITY_IDLE_AFTER_MS
28183
+ ),
28184
+ seen: { hash, changedAt, changeStreak },
28185
+ override: changeStreak >= OVERRIDE_CHANGE_STREAK
28186
+ };
28187
+ }
28188
+ function nextObservedState(previous, content, title, now, rules = getActiveAgentScreenRules(), previousState = null) {
28189
+ const activity = nextActivityState(
28190
+ previous,
28191
+ hashLine(`${title ?? ""}\0${content}`),
28192
+ now
28193
+ );
28194
+ const detected = detectAgentScreen({ screen: content, title }, rules);
28195
+ if (detected.kind === "skip") {
28196
+ return { kind: "skip", seen: activity.seen, ruleId: detected.ruleId };
28197
+ }
28198
+ if (detected.kind === "state") {
28199
+ const contentChanged = previous !== void 0 && previous.hash !== activity.seen.hash;
28200
+ if (detected.state === "idle" && previousState === "working" && contentChanged) {
28201
+ return {
28202
+ kind: "hold",
28203
+ seen: activity.seen,
28204
+ ruleId: detected.ruleId
28205
+ };
28206
+ }
28207
+ if (detected.state === "working" && activity.state === "idle") {
28208
+ return { kind: "record", ...activity, ruleId: null };
28209
+ }
28210
+ return {
28211
+ kind: "record",
28212
+ state: detected.state,
28213
+ seen: activity.seen,
28214
+ override: detected.state === "working" ? activity.override : true,
28215
+ ruleId: detected.ruleId
28216
+ };
28217
+ }
28218
+ if (previousState === null) {
28219
+ return { kind: "unidentified", seen: activity.seen };
28220
+ }
28221
+ return { kind: "record", ...activity, ruleId: null };
28222
+ }
28223
+ function observe(target, content, note, title) {
28224
+ const next = nextObservedState(
28225
+ seen.get(target),
28226
+ content,
28227
+ title,
28228
+ Date.now(),
28229
+ getActiveAgentScreenRules(),
28230
+ getAgentState(target)?.state ?? null
28231
+ );
28232
+ seen.set(target, next.seen);
28233
+ if (next.kind === "skip" || next.kind === "hold" || next.kind === "unidentified") {
28234
+ return;
28235
+ }
28236
+ recordAgentState({
28237
+ target,
28238
+ state: next.state,
28239
+ source: next.ruleId ? "screen" : "activity",
28240
+ note,
28241
+ override: next.override
28242
+ });
28243
+ }
28244
+ function observationError(operation, target, error) {
28245
+ return {
28246
+ operation,
28247
+ target,
28248
+ at: Date.now(),
28249
+ detail: formatErrorDetail(error),
28250
+ stack: error instanceof Error ? error.stack ?? "" : ""
28251
+ };
28252
+ }
28253
+ function getAgentActivityErrors() {
28254
+ return [...activityErrors.values()].sort((a, b) => a.at - b.at).map((error) => ({ ...error }));
28255
+ }
28256
+ function activityErrorKey(operation, target) {
28257
+ return `${operation}\0${target}`;
28258
+ }
28259
+ function rotateForSweep(items, offset, limit) {
28260
+ if (items.length === 0) return { batch: [], nextOffset: 0 };
28261
+ const take = Math.min(limit, items.length);
28262
+ const start = (offset % items.length + items.length) % items.length;
28263
+ const batch = [];
28264
+ for (let i = 0; i < take; i += 1) {
28265
+ batch.push(items[(start + i) % items.length]);
28266
+ }
28267
+ return { batch, nextOffset: (start + take) % items.length };
28268
+ }
28269
+ async function sweep(cwd2) {
28270
+ if (inFlight) return;
28271
+ inFlight = true;
28272
+ try {
28273
+ const panes = await listTmuxPanes(cwd2);
28274
+ activityErrors.delete(activityErrorKey("list_terminals", ""));
28275
+ const shells = listShellSessions();
28276
+ const allPanes = panes.running ? flattenTmuxPanes(panes.sessions) : [];
28277
+ if (panes.running) {
28278
+ const known = /* @__PURE__ */ new Set([
28279
+ ...allPanes.map((pane) => pane.id),
28280
+ ...shells.map((session) => session.id)
28281
+ ]);
28282
+ retainAgentStates(known);
28283
+ for (const target of [...seen.keys()]) {
28284
+ if (!known.has(target)) seen.delete(target);
28285
+ }
28286
+ }
28287
+ for (const session of shells) {
28288
+ const buffer = readShellBuffer(session.id);
28289
+ if (!buffer) continue;
28290
+ observe(session.id, buffer.replay, session.command);
28291
+ }
28292
+ const targets = allPanes;
28293
+ const { batch, nextOffset } = rotateForSweep(
28294
+ targets,
28295
+ sweepOffset,
28296
+ MAX_PANES_PER_SWEEP
28297
+ );
28298
+ sweepOffset = nextOffset;
28299
+ for (const pane of batch) {
28300
+ const result = await captureTmuxPane(pane.id, cwd2);
28301
+ if (result.status === "gone") {
28302
+ activityErrors.delete(activityErrorKey("capture_screen", pane.id));
28303
+ seen.delete(pane.id);
28304
+ continue;
28305
+ }
28306
+ if (result.status === "error") {
28307
+ console.error(
28308
+ `[code-viewer] terminal screen capture failed for ${pane.id}`,
28309
+ result.error
28310
+ );
28311
+ activityErrors.set(
28312
+ activityErrorKey("capture_screen", pane.id),
28313
+ observationError("capture_screen", pane.id, result.error)
28314
+ );
28315
+ continue;
28316
+ }
28317
+ activityErrors.delete(activityErrorKey("capture_screen", pane.id));
28318
+ observe(pane.id, result.screen.content, pane.title, pane.title);
28319
+ }
28320
+ } catch (error) {
28321
+ console.error("[code-viewer] terminal state observation failed", error);
28322
+ activityErrors.set(
28323
+ activityErrorKey("list_terminals", ""),
28324
+ observationError("list_terminals", "", error)
28325
+ );
28326
+ } finally {
28327
+ inFlight = false;
28328
+ }
28329
+ }
28330
+ function startAgentActivityWatch(cwd2) {
28331
+ if (timer) return;
28332
+ void reloadAgentScreenRules(cwd2);
28333
+ timer = setInterval(() => void sweep(cwd2), ACTIVITY_POLL_INTERVAL_MS);
28334
+ timer.unref?.();
28335
+ }
28336
+ function stopAgentActivityWatch() {
28337
+ if (timer) clearInterval(timer);
28338
+ timer = null;
28339
+ seen.clear();
28340
+ activityErrors.clear();
28341
+ sweepOffset = 0;
28342
+ }
28343
+ var ACTIVITY_POLL_INTERVAL_MS, ACTIVITY_IDLE_AFTER_MS, OVERRIDE_CHANGE_STREAK, MAX_PANES_PER_SWEEP, seen, timer, inFlight, activityErrors, sweepOffset;
28344
+ var init_activity = __esm({
28345
+ "web-src/server/terminal/activity.ts"() {
28346
+ init_agent_screen();
28347
+ init_agent_state();
28348
+ init_error_detail();
28349
+ init_terminal_capture();
28350
+ init_tmux();
28351
+ init_session();
28352
+ init_capture();
28353
+ init_panes();
28354
+ init_agent_state2();
28355
+ init_rules();
28356
+ ACTIVITY_POLL_INTERVAL_MS = 3e3;
28357
+ ACTIVITY_IDLE_AFTER_MS = 15e3;
28358
+ OVERRIDE_CHANGE_STREAK = 4;
28359
+ MAX_PANES_PER_SWEEP = 12;
28360
+ seen = /* @__PURE__ */ new Map();
28361
+ timer = null;
28362
+ inFlight = false;
28363
+ activityErrors = /* @__PURE__ */ new Map();
28364
+ sweepOffset = 0;
28365
+ }
28366
+ });
28367
+
27141
28368
  // web-src/server/terminal/capture.ts
27142
28369
  function terminalKindOf(target) {
27143
28370
  if (isShellSessionId(target)) return "shell";
@@ -27194,7 +28421,7 @@ var init_capture2 = __esm({
27194
28421
 
27195
28422
  // web-src/server/mcp.ts
27196
28423
  import { readFileSync as readFileSync7 } from "node:fs";
27197
- import { join as join20 } from "node:path";
28424
+ import { join as join21 } from "node:path";
27198
28425
  function defaultMcpTools(options = {}) {
27199
28426
  return [
27200
28427
  {
@@ -27692,7 +28919,7 @@ function defaultMcpTools(options = {}) {
27692
28919
  {
27693
28920
  name: "code_viewer_terminal_list",
27694
28921
  title: "code-viewer terminal list",
27695
- description: "Returns the state of every terminal this server knows about, the same payload `code-viewer terminal list --json` emits: { states: [{ target, state, source, updatedAt, lastPrompt, note }] }. state is working | waiting | done | idle, where done means the turn finished and nobody has read the output yet. source is hook when the agent reported it and activity when it was guessed. Read-only. Call this before asking the human anything — another agent may already be blocking them.",
28922
+ description: "Returns the state of every terminal this server knows about, plus every observation error, using the same payload `code-viewer terminal list --json` emits: { states: [{ target, state, source, updatedAt, lastPrompt, note }], errors: [{ operation, target, at, detail, stack }] }. state is working | waiting | done | idle, where done means the turn finished and nobody has read the output yet. source is hook for a reported event, screen for a visible matched rule, and activity for the motion fallback. Read-only. Call this before asking the human anything — another agent may already be blocking them.",
27696
28923
  inputSchema: {
27697
28924
  type: "object",
27698
28925
  properties: {
@@ -27778,7 +29005,9 @@ function runTerminalListTool(input) {
27778
29005
  }
27779
29006
  const all = listAgentStates();
27780
29007
  const states2 = attentionOnly ? all.filter((record) => needsAttention(record.state)) : all;
27781
- return { text: JSON.stringify({ states: states2 }, null, 2) };
29008
+ return {
29009
+ text: JSON.stringify({ states: states2, errors: getAgentActivityErrors() }, null, 2)
29010
+ };
27782
29011
  }
27783
29012
  async function runTerminalCaptureTool(input, options) {
27784
29013
  const params = isPlainObject(input) ? input : {};
@@ -28886,77 +30115,439 @@ async function dispatchJsonRpc(message, options) {
28886
30115
  };
28887
30116
  }
28888
30117
  }
28889
- async function handleToolsCall(id, rawParams, tools) {
28890
- if (!isPlainObject(rawParams)) {
28891
- return jsonRpcError(
28892
- id,
28893
- JSONRPC_INVALID_PARAMS,
28894
- "Invalid params: tools/call requires an object"
28895
- );
30118
+ async function handleToolsCall(id, rawParams, tools) {
30119
+ if (!isPlainObject(rawParams)) {
30120
+ return jsonRpcError(
30121
+ id,
30122
+ JSONRPC_INVALID_PARAMS,
30123
+ "Invalid params: tools/call requires an object"
30124
+ );
30125
+ }
30126
+ const name = rawParams.name;
30127
+ if (typeof name !== "string" || name.length === 0) {
30128
+ return jsonRpcError(
30129
+ id,
30130
+ JSONRPC_INVALID_PARAMS,
30131
+ "Invalid params: name must be a non-empty string"
30132
+ );
30133
+ }
30134
+ const args = rawParams.arguments;
30135
+ if (args !== void 0 && !isPlainObject(args)) {
30136
+ return jsonRpcError(
30137
+ id,
30138
+ JSONRPC_INVALID_PARAMS,
30139
+ "Invalid params: arguments must be an object"
30140
+ );
30141
+ }
30142
+ const tool = tools.find((t) => t.name === name);
30143
+ if (!tool) {
30144
+ const result2 = {
30145
+ content: [{ type: "text", text: `Unknown tool: ${name}` }],
30146
+ isError: true
30147
+ };
30148
+ return jsonRpcResult(id, result2);
30149
+ }
30150
+ const outcome = await tool.run(args ?? {});
30151
+ const result = {
30152
+ content: [{ type: "text", text: outcome.text }],
30153
+ isError: outcome.isError === true
30154
+ };
30155
+ return jsonRpcResult(id, result);
30156
+ }
30157
+ var MCP_PROTOCOL_VERSION, PACKAGE_VERSION, MCP_SERVER_INFO, JSONRPC_PARSE_ERROR, JSONRPC_INVALID_REQUEST, JSONRPC_METHOD_NOT_FOUND, JSONRPC_INVALID_PARAMS, JSONRPC_INTERNAL_ERROR;
30158
+ var init_mcp = __esm({
30159
+ "web-src/server/mcp.ts"() {
30160
+ init_agent_state();
30161
+ init_error_detail();
30162
+ init_fuzzy_search();
30163
+ init_agent_help();
30164
+ init_cli_helpers();
30165
+ init_handle();
30166
+ init_file_cli();
30167
+ init_root();
30168
+ init_search();
30169
+ init_search_cli();
30170
+ init_search_service();
30171
+ init_status_cli();
30172
+ init_activity();
30173
+ init_agent_state2();
30174
+ init_capture2();
30175
+ init_capture();
30176
+ MCP_PROTOCOL_VERSION = "2025-06-18";
30177
+ PACKAGE_VERSION = JSON.parse(
30178
+ readFileSync7(join21(ROOT, "package.json"), "utf8")
30179
+ ).version;
30180
+ MCP_SERVER_INFO = {
30181
+ name: "code-viewer",
30182
+ title: "code-viewer",
30183
+ version: PACKAGE_VERSION
30184
+ };
30185
+ JSONRPC_PARSE_ERROR = -32700;
30186
+ JSONRPC_INVALID_REQUEST = -32600;
30187
+ JSONRPC_METHOD_NOT_FOUND = -32601;
30188
+ JSONRPC_INVALID_PARAMS = -32602;
30189
+ JSONRPC_INTERNAL_ERROR = -32603;
30190
+ }
30191
+ });
30192
+
30193
+ // web-src/server/os-platform.ts
30194
+ function isWsl(platform, release) {
30195
+ return platform === "linux" && /wsl/i.test(release);
30196
+ }
30197
+ var init_os_platform = __esm({
30198
+ "web-src/server/os-platform.ts"() {
30199
+ }
30200
+ });
30201
+
30202
+ // web-src/server/os-opener.ts
30203
+ import { release as osRelease } from "node:os";
30204
+ function directoryCommands(path, platform) {
30205
+ if (platform === "darwin") {
30206
+ return [{ args: ["open", "--", path], cwd: path }];
30207
+ }
30208
+ if (platform === "win32") {
30209
+ return [{ args: ["explorer.exe", path], cwd: path }];
30210
+ }
30211
+ return [
30212
+ { args: ["xdg-open", path], cwd: path },
30213
+ { args: ["gio", "open", path], cwd: path }
30214
+ ];
30215
+ }
30216
+ function urlCommands(url, cwd2, platform) {
30217
+ if (platform === "darwin") {
30218
+ return [{ args: ["open", url], cwd: cwd2 }];
30219
+ }
30220
+ if (platform === "win32") {
30221
+ return [{ args: ["cmd.exe", "/c", "start", "", url], cwd: cwd2 }];
30222
+ }
30223
+ return [
30224
+ { args: ["xdg-open", url], cwd: cwd2 },
30225
+ { args: ["gio", "open", url], cwd: cwd2 }
30226
+ ];
30227
+ }
30228
+ function commandResultError(command, result) {
30229
+ return Object.assign(
30230
+ new Error(
30231
+ result.code === 0 ? `${command.args[0]} wrote to stderr` : `${command.args[0]} exited with ${result.code}`
30232
+ ),
30233
+ {
30234
+ command: command.args,
30235
+ cwd: command.cwd,
30236
+ result
30237
+ }
30238
+ );
30239
+ }
30240
+ async function executeOpenCommand(command) {
30241
+ try {
30242
+ return await runAsync(command.args, command.cwd, {
30243
+ timeout: OPEN_TIMEOUT_MS
30244
+ });
30245
+ } catch (error) {
30246
+ throw commandExecutionError(command, error);
30247
+ }
30248
+ }
30249
+ function commandSucceeded(result) {
30250
+ return result.code === 0 && result.stderr.trim() === "";
30251
+ }
30252
+ function commandExecutionError(command, cause) {
30253
+ return Object.assign(
30254
+ errorWithCause(`failed to execute ${command.args[0]}`, cause),
30255
+ {
30256
+ command: command.args,
30257
+ cwd: command.cwd
30258
+ }
30259
+ );
30260
+ }
30261
+ async function runOpenCommands(commands, operation, errors = []) {
30262
+ for (const command of commands) {
30263
+ try {
30264
+ const result = await executeOpenCommand(command);
30265
+ if (commandSucceeded(result)) return;
30266
+ errors.push(commandResultError(command, result));
30267
+ } catch (error) {
30268
+ errors.push(
30269
+ error instanceof Error ? error : commandExecutionError(command, error)
30270
+ );
30271
+ }
30272
+ }
30273
+ throw errorWithCauses(`failed to ${operation}`, errors);
30274
+ }
30275
+ async function commandOutput(command) {
30276
+ const result = await executeOpenCommand(command);
30277
+ if (!commandSucceeded(result)) throw commandResultError(command, result);
30278
+ const output = result.stdout.trim();
30279
+ if (!output) {
30280
+ throw Object.assign(new Error(`${command.args[0]} returned no path`), {
30281
+ command: command.args,
30282
+ cwd: command.cwd,
30283
+ result
30284
+ });
30285
+ }
30286
+ return output;
30287
+ }
30288
+ async function wslWindowsCommandCwd(cwd2) {
30289
+ return commandOutput({ args: ["wslpath", "-u", "C:\\"], cwd: cwd2 });
30290
+ }
30291
+ async function openWslDirectory(path) {
30292
+ const errors = [];
30293
+ try {
30294
+ const windowsPath = await commandOutput({
30295
+ args: ["wslpath", "-w", path],
30296
+ cwd: path
30297
+ });
30298
+ const windowsCwd = await wslWindowsCommandCwd(path);
30299
+ const command = {
30300
+ args: ["cmd.exe", "/c", "start", "", windowsPath],
30301
+ cwd: windowsCwd
30302
+ };
30303
+ const result = await executeOpenCommand(command);
30304
+ if (commandSucceeded(result)) return;
30305
+ errors.push(commandResultError(command, result));
30306
+ } catch (error) {
30307
+ errors.push(
30308
+ error instanceof Error ? error : errorWithCause("failed to open directory through WSL", error)
30309
+ );
30310
+ }
30311
+ return runOpenCommands(
30312
+ [
30313
+ { args: ["gio", "open", path], cwd: path },
30314
+ { args: ["xdg-open", path], cwd: path }
30315
+ ],
30316
+ "open directory in OS",
30317
+ errors
30318
+ );
30319
+ }
30320
+ async function openWslUrl(url, cwd2) {
30321
+ const errors = [];
30322
+ try {
30323
+ const windowsCwd = await wslWindowsCommandCwd(cwd2);
30324
+ const command = {
30325
+ args: ["cmd.exe", "/c", "start", "", url],
30326
+ cwd: windowsCwd
30327
+ };
30328
+ const result = await executeOpenCommand(command);
30329
+ if (commandSucceeded(result)) return;
30330
+ errors.push(commandResultError(command, result));
30331
+ } catch (error) {
30332
+ errors.push(
30333
+ error instanceof Error ? error : errorWithCause("failed to open URL through WSL", error)
30334
+ );
30335
+ }
30336
+ return runOpenCommands(
30337
+ [
30338
+ { args: ["gio", "open", url], cwd: cwd2 },
30339
+ { args: ["xdg-open", url], cwd: cwd2 }
30340
+ ],
30341
+ "open URL in OS",
30342
+ errors
30343
+ );
30344
+ }
30345
+ function openDirectoryInOs(path, platform = process.platform, release = osRelease()) {
30346
+ if (isWsl(platform, release)) return openWslDirectory(path);
30347
+ return runOpenCommands(
30348
+ directoryCommands(path, platform),
30349
+ "open directory in OS"
30350
+ );
30351
+ }
30352
+ function openUrlInOs(url, cwd2, platform = process.platform, release = osRelease()) {
30353
+ if (isWsl(platform, release)) return openWslUrl(url, cwd2);
30354
+ return runOpenCommands(urlCommands(url, cwd2, platform), "open URL in OS");
30355
+ }
30356
+ var OPEN_TIMEOUT_MS;
30357
+ var init_os_opener = __esm({
30358
+ "web-src/server/os-opener.ts"() {
30359
+ init_error_detail();
30360
+ init_os_platform();
30361
+ init_runtime();
30362
+ OPEN_TIMEOUT_MS = 15e3;
30363
+ }
30364
+ });
30365
+
30366
+ // web-src/server/os-trash.ts
30367
+ import { randomUUID as randomUUID2 } from "node:crypto";
30368
+ import { existsSync as existsSync8, lstatSync as lstatSync5, mkdirSync as mkdirSync4, renameSync } from "node:fs";
30369
+ import { homedir as homedir3, release as osRelease2 } from "node:os";
30370
+ import { basename as basename3, dirname as dirname6, join as join22, resolve as resolve2 } from "node:path";
30371
+ function windowsTrashScript(path) {
30372
+ const quotedPath = path.replace(/'/g, "''");
30373
+ return [
30374
+ "$ErrorActionPreference = 'Stop';",
30375
+ `$path = '${quotedPath}';`,
30376
+ "Add-Type -TypeDefinition @'",
30377
+ "using System;",
30378
+ "using System.Runtime.InteropServices;",
30379
+ "public static class CodeViewerRecycleBin {",
30380
+ " [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]",
30381
+ " public struct SHFILEOPSTRUCT {",
30382
+ " public IntPtr hwnd;",
30383
+ " public uint wFunc;",
30384
+ " public string pFrom;",
30385
+ " public string pTo;",
30386
+ " public ushort fFlags;",
30387
+ " [MarshalAs(UnmanagedType.Bool)] public bool fAnyOperationsAborted;",
30388
+ " public IntPtr hNameMappings;",
30389
+ " public string lpszProgressTitle;",
30390
+ " }",
30391
+ ' [DllImport("shell32.dll", CharSet = CharSet.Unicode)]',
30392
+ " private static extern int SHFileOperationW(ref SHFILEOPSTRUCT lpFileOp);",
30393
+ " public static void MoveToRecycleBin(string path) {",
30394
+ " const uint FO_DELETE = 0x0003;",
30395
+ " const ushort FOF_SILENT = 0x0004;",
30396
+ " const ushort FOF_NOCONFIRMATION = 0x0010;",
30397
+ " const ushort FOF_ALLOWUNDO = 0x0040;",
30398
+ " const ushort FOF_NOERRORUI = 0x0400;",
30399
+ " var op = new SHFILEOPSTRUCT {",
30400
+ " hwnd = IntPtr.Zero,",
30401
+ " wFunc = FO_DELETE,",
30402
+ ' pFrom = path + "\\0\\0",',
30403
+ " pTo = null,",
30404
+ " fFlags = (ushort)(FOF_ALLOWUNDO | FOF_NOCONFIRMATION | FOF_NOERRORUI | FOF_SILENT),",
30405
+ " fAnyOperationsAborted = false,",
30406
+ " hNameMappings = IntPtr.Zero,",
30407
+ " lpszProgressTitle = null",
30408
+ " };",
30409
+ " int result = SHFileOperationW(ref op);",
30410
+ ' if (result != 0) throw new InvalidOperationException("SHFileOperationW failed: " + result);',
30411
+ ' if (op.fAnyOperationsAborted) throw new OperationCanceledException("SHFileOperationW aborted");',
30412
+ " }",
30413
+ "}",
30414
+ "'@;",
30415
+ "[CodeViewerRecycleBin]::MoveToRecycleBin($path);"
30416
+ ].join(" ");
30417
+ }
30418
+ function windowsRestoreTrashScript(originalPath) {
30419
+ const quotedPath = originalPath.replace(/'/g, "''");
30420
+ return [
30421
+ "$ErrorActionPreference = 'Stop';",
30422
+ `$original = '${quotedPath}';`,
30423
+ "$parent = [System.IO.Path]::GetDirectoryName($original);",
30424
+ "$name = [System.IO.Path]::GetFileName($original);",
30425
+ "$shell = New-Object -ComObject Shell.Application;",
30426
+ "$bin = $shell.Namespace(10);",
30427
+ "$restored = $false;",
30428
+ "foreach ($item in $bin.Items()) {",
30429
+ " $deletedFrom = $item.ExtendedProperty('System.Recycle.DeletedFrom');",
30430
+ " if ($item.Name -eq $name -and $deletedFrom -eq $parent) {",
30431
+ " $item.InvokeVerb('ESTORE');",
30432
+ " $restored = $true;",
30433
+ " break;",
30434
+ " }",
30435
+ "}",
30436
+ "if (-not $restored) { throw 'recycle bin item not found'; }"
30437
+ ].join(" ");
30438
+ }
30439
+ function commandResultError2(operation, command, cwd2, result) {
30440
+ return Object.assign(new Error(`${operation} failed`), {
30441
+ command,
30442
+ cwd: cwd2,
30443
+ result
30444
+ });
30445
+ }
30446
+ async function runRequiredCommand(operation, command, cwd2) {
30447
+ let result;
30448
+ try {
30449
+ result = await runAsync(command, cwd2, { timeout: TRASH_TIMEOUT_MS });
30450
+ } catch (cause) {
30451
+ throw Object.assign(errorWithCause(`${operation} failed`, cause), {
30452
+ command,
30453
+ cwd: cwd2
30454
+ });
30455
+ }
30456
+ if (result.code !== 0) {
30457
+ throw commandResultError2(operation, command, cwd2, result);
30458
+ }
30459
+ }
30460
+ function managedTrashRoot(cwd2) {
30461
+ return join22(cwd2, ".code-viewer", "trash");
30462
+ }
30463
+ function movePathIntoTrashDirectory(path, trashRoot) {
30464
+ mkdirSync4(trashRoot, { recursive: true });
30465
+ const name = basename3(path) || "trash-item";
30466
+ const trashPath = join22(trashRoot, `${name}-${randomUUID2()}`);
30467
+ if (existsSync8(trashPath)) {
30468
+ throw Object.assign(new Error("trash destination already exists"), {
30469
+ trashPath
30470
+ });
30471
+ }
30472
+ renameSync(path, trashPath);
30473
+ return { trashPath };
30474
+ }
30475
+ function trashRootForHandle(cwd2, platform, release) {
30476
+ if (platform === "darwin") return join22(homedir3(), ".Trash");
30477
+ if (isWsl(platform, release)) return managedTrashRoot(cwd2);
30478
+ return null;
30479
+ }
30480
+ function unsupportedTrashError(operation, platform, release) {
30481
+ return Object.assign(new Error(`${operation} unsupported`), {
30482
+ platform,
30483
+ release
30484
+ });
30485
+ }
30486
+ async function movePathToTrash(path, cwd2, platform = process.platform, release = osRelease2()) {
30487
+ lstatSync5(path);
30488
+ if (platform === "darwin") {
30489
+ return movePathIntoTrashDirectory(path, join22(homedir3(), ".Trash"));
30490
+ }
30491
+ if (isWsl(platform, release)) {
30492
+ return movePathIntoTrashDirectory(path, managedTrashRoot(cwd2));
30493
+ }
30494
+ if (platform === "win32") {
30495
+ await runRequiredCommand(
30496
+ "move path to Recycle Bin",
30497
+ [
30498
+ "powershell.exe",
30499
+ "-NoProfile",
30500
+ "-NonInteractive",
30501
+ "-ExecutionPolicy",
30502
+ "Bypass",
30503
+ "-Command",
30504
+ windowsTrashScript(path)
30505
+ ],
30506
+ cwd2
30507
+ );
30508
+ return {};
30509
+ }
30510
+ throw unsupportedTrashError("trash", platform, release);
30511
+ }
30512
+ async function restorePathFromTrash(originalPath, trashPath, cwd2, platform = process.platform, release = osRelease2()) {
30513
+ if (existsSync8(originalPath)) {
30514
+ throw new Error("restore target exists");
28896
30515
  }
28897
- const name = rawParams.name;
28898
- if (typeof name !== "string" || name.length === 0) {
28899
- return jsonRpcError(
28900
- id,
28901
- JSONRPC_INVALID_PARAMS,
28902
- "Invalid params: name must be a non-empty string"
28903
- );
30516
+ if (trashPath) {
30517
+ const trashRoot = trashRootForHandle(cwd2, platform, release);
30518
+ if (!trashRoot || dirname6(resolve2(trashPath)) !== resolve2(trashRoot)) {
30519
+ throw new Error("invalid trash handle");
30520
+ }
30521
+ if (!existsSync8(trashPath)) throw new Error("trash item not found");
30522
+ mkdirSync4(dirname6(originalPath), { recursive: true });
30523
+ renameSync(trashPath, originalPath);
30524
+ return;
28904
30525
  }
28905
- const args = rawParams.arguments;
28906
- if (args !== void 0 && !isPlainObject(args)) {
28907
- return jsonRpcError(
28908
- id,
28909
- JSONRPC_INVALID_PARAMS,
28910
- "Invalid params: arguments must be an object"
30526
+ if (platform === "win32") {
30527
+ await runRequiredCommand(
30528
+ "restore path from Recycle Bin",
30529
+ [
30530
+ "powershell.exe",
30531
+ "-NoProfile",
30532
+ "-NonInteractive",
30533
+ "-ExecutionPolicy",
30534
+ "Bypass",
30535
+ "-Command",
30536
+ windowsRestoreTrashScript(originalPath)
30537
+ ],
30538
+ cwd2
28911
30539
  );
30540
+ return;
28912
30541
  }
28913
- const tool = tools.find((t) => t.name === name);
28914
- if (!tool) {
28915
- const result2 = {
28916
- content: [{ type: "text", text: `Unknown tool: ${name}` }],
28917
- isError: true
28918
- };
28919
- return jsonRpcResult(id, result2);
28920
- }
28921
- const outcome = await tool.run(args ?? {});
28922
- const result = {
28923
- content: [{ type: "text", text: outcome.text }],
28924
- isError: outcome.isError === true
28925
- };
28926
- return jsonRpcResult(id, result);
30542
+ throw unsupportedTrashError("restore from trash", platform, release);
28927
30543
  }
28928
- var MCP_PROTOCOL_VERSION, PACKAGE_VERSION, MCP_SERVER_INFO, JSONRPC_PARSE_ERROR, JSONRPC_INVALID_REQUEST, JSONRPC_METHOD_NOT_FOUND, JSONRPC_INVALID_PARAMS, JSONRPC_INTERNAL_ERROR;
28929
- var init_mcp = __esm({
28930
- "web-src/server/mcp.ts"() {
28931
- init_agent_state();
30544
+ var TRASH_TIMEOUT_MS;
30545
+ var init_os_trash = __esm({
30546
+ "web-src/server/os-trash.ts"() {
28932
30547
  init_error_detail();
28933
- init_fuzzy_search();
28934
- init_agent_help();
28935
- init_cli_helpers();
28936
- init_handle();
28937
- init_file_cli();
28938
- init_root();
28939
- init_search();
28940
- init_search_cli();
28941
- init_search_service();
28942
- init_status_cli();
28943
- init_agent_state2();
28944
- init_capture2();
28945
- init_capture();
28946
- MCP_PROTOCOL_VERSION = "2025-06-18";
28947
- PACKAGE_VERSION = JSON.parse(
28948
- readFileSync7(join20(ROOT, "package.json"), "utf8")
28949
- ).version;
28950
- MCP_SERVER_INFO = {
28951
- name: "code-viewer",
28952
- title: "code-viewer",
28953
- version: PACKAGE_VERSION
28954
- };
28955
- JSONRPC_PARSE_ERROR = -32700;
28956
- JSONRPC_INVALID_REQUEST = -32600;
28957
- JSONRPC_METHOD_NOT_FOUND = -32601;
28958
- JSONRPC_INVALID_PARAMS = -32602;
28959
- JSONRPC_INTERNAL_ERROR = -32603;
30548
+ init_os_platform();
30549
+ init_runtime();
30550
+ TRASH_TIMEOUT_MS = 6e4;
28960
30551
  }
28961
30552
  });
28962
30553
 
@@ -28984,12 +30575,12 @@ var init_request_origin = __esm({
28984
30575
 
28985
30576
  // web-src/server/watch-supervisor.ts
28986
30577
  import { spawn as spawn3 } from "node:child_process";
28987
- import { join as join21 } from "node:path";
30578
+ import { join as join23 } from "node:path";
28988
30579
  import { fileURLToPath as fileURLToPath3 } from "node:url";
28989
30580
  function watchChildCommand() {
28990
30581
  const entry = process.argv[1] ?? "";
28991
30582
  const isTypeScriptEntry = entry.endsWith(".ts");
28992
- const script = isTypeScriptEntry ? join21(fileURLToPath3(new URL(".", import.meta.url)), "cli.ts") : entry;
30583
+ const script = isTypeScriptEntry ? join23(fileURLToPath3(new URL(".", import.meta.url)), "cli.ts") : entry;
28993
30584
  const loaderArgs = isTypeScriptEntry ? process.execArgv : [];
28994
30585
  return [process.argv[0], ...loaderArgs, script, "watch-child"];
28995
30586
  }
@@ -29157,12 +30748,12 @@ function parseTmuxClients(stdout) {
29157
30748
  if (!line) continue;
29158
30749
  const fields = line.split(TMUX_FIELD_SEP);
29159
30750
  if (fields.length < CLIENT_FIELDS.length) continue;
29160
- const tty = fields[FIELD.tty] ?? "";
30751
+ const tty = fields[FIELD2.tty] ?? "";
29161
30752
  if (!tty) continue;
29162
30753
  clients.push({
29163
30754
  tty,
29164
- session: fields[FIELD.session] ?? "",
29165
- pane: fields[FIELD.pane] ?? ""
30755
+ session: fields[FIELD2.session] ?? "",
30756
+ pane: fields[FIELD2.pane] ?? ""
29166
30757
  });
29167
30758
  }
29168
30759
  return clients;
@@ -29177,7 +30768,7 @@ function findClientByTty(clients, tty) {
29177
30768
  if (!tty) return null;
29178
30769
  return clients.find((client) => client.tty === tty) ?? null;
29179
30770
  }
29180
- var CLIENT_FIELDS, CLIENT_FORMAT, FIELD;
30771
+ var CLIENT_FIELDS, CLIENT_FORMAT, FIELD2;
29181
30772
  var init_clients = __esm({
29182
30773
  "web-src/server/tmux/clients.ts"() {
29183
30774
  init_command();
@@ -29189,7 +30780,7 @@ var init_clients = __esm({
29189
30780
  "#{pane_id}"
29190
30781
  ];
29191
30782
  CLIENT_FORMAT = CLIENT_FIELDS.join(TMUX_FIELD_SEP);
29192
- FIELD = {
30783
+ FIELD2 = {
29193
30784
  tty: 0,
29194
30785
  session: 1,
29195
30786
  pane: 2
@@ -29292,127 +30883,6 @@ var init_open = __esm({
29292
30883
  }
29293
30884
  });
29294
30885
 
29295
- // web-src/server/tmux/panes.ts
29296
- function toInt(value) {
29297
- const parsed = Number.parseInt(value ?? "", 10);
29298
- return Number.isFinite(parsed) ? parsed : 0;
29299
- }
29300
- function toFlag(value) {
29301
- return value === "1";
29302
- }
29303
- function parseTmuxPanes(stdout, worktrees = []) {
29304
- const sessions2 = [];
29305
- const sessionByName = /* @__PURE__ */ new Map();
29306
- const windowByKey = /* @__PURE__ */ new Map();
29307
- for (const line of stdout.split("\n")) {
29308
- if (!line) continue;
29309
- const fields = line.split(TMUX_FIELD_SEP);
29310
- if (fields.length < PANE_FIELDS.length) continue;
29311
- const paneId = fields[FIELD2.paneId];
29312
- if (!paneId) continue;
29313
- const sessionName = fields[FIELD2.sessionName] ?? "";
29314
- let session = sessionByName.get(sessionName);
29315
- if (!session) {
29316
- session = {
29317
- name: sessionName,
29318
- attached: toFlag(fields[FIELD2.sessionAttached]),
29319
- windows: []
29320
- };
29321
- sessionByName.set(sessionName, session);
29322
- sessions2.push(session);
29323
- }
29324
- const windowIndex = toInt(fields[FIELD2.windowIndex]);
29325
- const windowKey = `${sessionName}${TMUX_FIELD_SEP}${windowIndex}`;
29326
- let window = windowByKey.get(windowKey);
29327
- if (!window) {
29328
- window = {
29329
- index: windowIndex,
29330
- name: fields[FIELD2.windowName] ?? "",
29331
- active: toFlag(fields[FIELD2.windowActive]),
29332
- panes: []
29333
- };
29334
- windowByKey.set(windowKey, window);
29335
- session.windows.push(window);
29336
- }
29337
- const paneIndex = toInt(fields[FIELD2.paneIndex]);
29338
- const pane = {
29339
- id: paneId,
29340
- label: `${sessionName}:${windowIndex}.${paneIndex}`,
29341
- paneIndex,
29342
- title: fields[FIELD2.paneTitle] ?? "",
29343
- command: fields[FIELD2.paneCommand] ?? "",
29344
- path: fields[FIELD2.panePath] ?? "",
29345
- width: toInt(fields[FIELD2.paneWidth]),
29346
- height: toInt(fields[FIELD2.paneHeight]),
29347
- active: toFlag(fields[FIELD2.paneActive]),
29348
- inRepo: worktrees.length === 0 || isPathInsideAny(fields[FIELD2.panePath] ?? "", worktrees)
29349
- };
29350
- window.panes.push(pane);
29351
- }
29352
- return sessions2;
29353
- }
29354
- async function listTmuxPanes(cwd2) {
29355
- const [result, worktrees] = await Promise.all([
29356
- runTmux(["list-panes", "-a", "-F", PANE_FORMAT], cwd2),
29357
- worktreePathsAsync(cwd2)
29358
- ]);
29359
- if (result.status === "missing") {
29360
- return { available: false, running: false, sessions: [] };
29361
- }
29362
- if (result.status === "no-server" || result.status === "no-target") {
29363
- return { available: true, running: false, sessions: [] };
29364
- }
29365
- if (result.status === "error") {
29366
- throw errorWithCause("failed to list tmux panes", result.error);
29367
- }
29368
- return {
29369
- available: true,
29370
- running: true,
29371
- sessions: parseTmuxPanes(result.stdout, worktrees)
29372
- };
29373
- }
29374
- var PANE_FIELDS, PANE_FORMAT, FIELD2;
29375
- var init_panes = __esm({
29376
- "web-src/server/tmux/panes.ts"() {
29377
- init_error_detail();
29378
- init_tmux();
29379
- init_git();
29380
- init_command();
29381
- PANE_FIELDS = [
29382
- "#{pane_id}",
29383
- "#{session_name}",
29384
- "#{session_attached}",
29385
- "#{window_index}",
29386
- "#{window_name}",
29387
- "#{window_active}",
29388
- "#{pane_index}",
29389
- "#{pane_active}",
29390
- "#{pane_width}",
29391
- "#{pane_height}",
29392
- "#{pane_current_command}",
29393
- "#{pane_current_path}",
29394
- // タイトルは自由文字列なので必ず最後に置く。
29395
- "#{pane_title}"
29396
- ];
29397
- PANE_FORMAT = PANE_FIELDS.join(TMUX_FIELD_SEP);
29398
- FIELD2 = {
29399
- paneId: 0,
29400
- sessionName: 1,
29401
- sessionAttached: 2,
29402
- windowIndex: 3,
29403
- windowName: 4,
29404
- windowActive: 5,
29405
- paneIndex: 6,
29406
- paneActive: 7,
29407
- paneWidth: 8,
29408
- paneHeight: 9,
29409
- paneCommand: 10,
29410
- panePath: 11,
29411
- paneTitle: 12
29412
- };
29413
- }
29414
- });
29415
-
29416
30886
  // web-src/server/tmux/handle.ts
29417
30887
  var handle_exports2 = {};
29418
30888
  __export(handle_exports2, {
@@ -29671,109 +31141,50 @@ function handleShellRoute(req, url, cwd2, sideEffectAllowed) {
29671
31141
  methods: ["POST"],
29672
31142
  sideEffect: true,
29673
31143
  handler: () => handleKeys2(req)
29674
- },
29675
- "/_shell/resize": {
29676
- methods: ["POST"],
29677
- sideEffect: true,
29678
- handler: () => handleResize(req)
29679
- },
29680
- "/_shell/close": {
29681
- methods: ["POST"],
29682
- sideEffect: true,
29683
- handler: () => handleClose2(req)
29684
- }
29685
- },
29686
- sideEffectAllowed,
29687
- (res) => res,
29688
- (err) => handleError("shell", "handle shell request", err)
29689
- );
29690
- }
29691
- var KEEPALIVE_INTERVAL_MS, MAX_KEY_INPUT_LENGTH, SSE_HEADERS, activeStreams;
29692
- var init_handle3 = __esm({
29693
- "web-src/server/shell/handle.ts"() {
29694
- init_error_detail();
29695
- init_shell();
29696
- init_handle_shared();
29697
- init_session();
29698
- KEEPALIVE_INTERVAL_MS = 15e3;
29699
- MAX_KEY_INPUT_LENGTH = 1e5;
29700
- SSE_HEADERS = {
29701
- "Content-Type": "text/event-stream",
29702
- "Cache-Control": "no-cache"
29703
- };
29704
- activeStreams = /* @__PURE__ */ new Set();
29705
- }
29706
- });
29707
-
29708
- // web-src/core/terminal-paste.ts
29709
- function pasteImageExtension(mime) {
29710
- if (typeof mime !== "string") return null;
29711
- const base = mime.split(";")[0]?.trim().toLowerCase() ?? "";
29712
- return PASTE_IMAGE_TYPES[base] ?? null;
29713
- }
29714
- function looksLikeBase64(value) {
29715
- return typeof value === "string" && value.length > 0 && /^[A-Za-z0-9+/]+={0,2}$/.test(value);
29716
- }
29717
- function base64ByteLength(value) {
29718
- const padding = value.endsWith("==") ? 2 : value.endsWith("=") ? 1 : 0;
29719
- return Math.floor(value.length * 3 / 4) - padding;
29720
- }
29721
- var PASTE_IMAGE_TYPES, MAX_PASTE_IMAGE_BYTES, MAX_PASTE_BODY_BYTES, SHIFT_ENTER_SEQUENCE;
29722
- var init_terminal_paste = __esm({
29723
- "web-src/core/terminal-paste.ts"() {
29724
- PASTE_IMAGE_TYPES = {
29725
- "image/png": "png",
29726
- "image/jpeg": "jpg",
29727
- "image/gif": "gif",
29728
- "image/webp": "webp"
29729
- };
29730
- MAX_PASTE_IMAGE_BYTES = 8 * 1024 * 1024;
29731
- MAX_PASTE_BODY_BYTES = Math.ceil(MAX_PASTE_IMAGE_BYTES * 1.4);
29732
- SHIFT_ENTER_SEQUENCE = `${String.fromCharCode(27)}[200~${String.fromCharCode(10)}${String.fromCharCode(27)}[201~`;
29733
- }
29734
- });
29735
-
29736
- // web-src/core/terminal-images.ts
29737
- function terminalImageExtension(path) {
29738
- const dot = path.lastIndexOf(".");
29739
- if (dot < 0) return null;
29740
- const extension = path.slice(dot + 1).toLowerCase();
29741
- return TERMINAL_IMAGE_EXTENSIONS.includes(extension) ? extension : null;
31144
+ },
31145
+ "/_shell/resize": {
31146
+ methods: ["POST"],
31147
+ sideEffect: true,
31148
+ handler: () => handleResize(req)
31149
+ },
31150
+ "/_shell/close": {
31151
+ methods: ["POST"],
31152
+ sideEffect: true,
31153
+ handler: () => handleClose2(req)
31154
+ }
31155
+ },
31156
+ sideEffectAllowed,
31157
+ (res) => res,
31158
+ (err) => handleError("shell", "handle shell request", err)
31159
+ );
29742
31160
  }
29743
- var TERMINAL_IMAGE_EXTENSIONS, MAX_TERMINAL_IMAGE_QUERY, ESC, BEL, ANSI_RE, PATH_CHAR, NAME_CHAR, IMAGE_PATH_RE;
29744
- var init_terminal_images = __esm({
29745
- "web-src/core/terminal-images.ts"() {
29746
- init_terminal_paste();
29747
- TERMINAL_IMAGE_EXTENSIONS = [
29748
- ...new Set(Object.values(PASTE_IMAGE_TYPES)),
29749
- "jpeg"
29750
- ];
29751
- MAX_TERMINAL_IMAGE_QUERY = 16;
29752
- ESC = String.fromCharCode(27);
29753
- BEL = String.fromCharCode(7);
29754
- ANSI_RE = new RegExp(
29755
- `${ESC}\\[[0-9;?]*[ -/]*[@-~]|${ESC}\\][^${BEL}${ESC}]*(?:${BEL}|${ESC}\\\\)|${ESC}[@-Z\\\\-_]`,
29756
- "g"
29757
- );
29758
- PATH_CHAR = "[\\p{L}\\p{N}._~+@%/-]";
29759
- NAME_CHAR = "[\\p{L}\\p{N}_~+@%-]";
29760
- IMAGE_PATH_RE = new RegExp(
29761
- `${PATH_CHAR}*${NAME_CHAR}\\.(?:${TERMINAL_IMAGE_EXTENSIONS.join("|")})(?![\\p{L}\\p{N}])`,
29762
- "giu"
29763
- );
31161
+ var KEEPALIVE_INTERVAL_MS, MAX_KEY_INPUT_LENGTH, SSE_HEADERS, activeStreams;
31162
+ var init_handle3 = __esm({
31163
+ "web-src/server/shell/handle.ts"() {
31164
+ init_error_detail();
31165
+ init_shell();
31166
+ init_handle_shared();
31167
+ init_session();
31168
+ KEEPALIVE_INTERVAL_MS = 15e3;
31169
+ MAX_KEY_INPUT_LENGTH = 1e5;
31170
+ SSE_HEADERS = {
31171
+ "Content-Type": "text/event-stream",
31172
+ "Cache-Control": "no-cache"
31173
+ };
31174
+ activeStreams = /* @__PURE__ */ new Set();
29764
31175
  }
29765
31176
  });
29766
31177
 
29767
31178
  // web-src/server/terminal/images.ts
29768
31179
  import { realpathSync as realpathSync7, statSync as statSync7 } from "node:fs";
29769
- import { homedir as homedir3 } from "node:os";
29770
- import { basename as basename3, isAbsolute as isAbsolute2, resolve as resolve2 } from "node:path";
31180
+ import { homedir as homedir4 } from "node:os";
31181
+ import { basename as basename4, isAbsolute as isAbsolute2, resolve as resolve3 } from "node:path";
29771
31182
  function resolveTerminalImage(cwd2, candidate) {
29772
31183
  if (typeof candidate !== "string" || candidate === "") return null;
29773
31184
  if (candidate.includes("\0")) return null;
29774
31185
  if (!terminalImageExtension(candidate)) return null;
29775
- const expanded = candidate.startsWith("~/") ? resolve2(homedir3(), candidate.slice(2)) : candidate;
29776
- const full = isAbsolute2(expanded) ? expanded : resolve2(cwd2, expanded);
31186
+ const expanded = candidate.startsWith("~/") ? resolve3(homedir4(), candidate.slice(2)) : candidate;
31187
+ const full = isAbsolute2(expanded) ? expanded : resolve3(cwd2, expanded);
29777
31188
  try {
29778
31189
  const real = realpathSync7(full);
29779
31190
  const stat3 = statSync7(real);
@@ -29798,7 +31209,7 @@ function resolveTerminalImages(cwd2, candidates) {
29798
31209
  path: image.path,
29799
31210
  // 画面で探すのは、渡された綴りそのもの。実体のパスとは違うことがある。
29800
31211
  candidate,
29801
- name: basename3(image.path),
31212
+ name: basename4(image.path),
29802
31213
  url: terminalImageUrl(image.path)
29803
31214
  });
29804
31215
  }
@@ -29815,7 +31226,7 @@ var init_images = __esm({
29815
31226
 
29816
31227
  // web-src/server/terminal/paste.ts
29817
31228
  import { mkdir as mkdir2, writeFile as writeFile2 } from "node:fs/promises";
29818
- import { join as join22 } from "node:path";
31229
+ import { join as join24 } from "node:path";
29819
31230
  async function savePastedImage(cwd2, mime, base64) {
29820
31231
  const extension = pasteImageExtension(mime);
29821
31232
  if (!extension) {
@@ -29840,8 +31251,8 @@ async function savePastedImage(cwd2, mime, base64) {
29840
31251
  return { status: "invalid", message: "image too large" };
29841
31252
  }
29842
31253
  const name = `${makeTimedId("paste")}.${extension}`;
29843
- const dir = join22(cwd2, PASTE_DIR);
29844
- const path = join22(dir, name);
31254
+ const dir = join24(cwd2, PASTE_DIR);
31255
+ const path = join24(dir, name);
29845
31256
  try {
29846
31257
  await mkdir2(dir, { recursive: true });
29847
31258
  await writeFile2(path, bytes);
@@ -29858,7 +31269,7 @@ var init_paste = __esm({
29858
31269
  "web-src/server/terminal/paste.ts"() {
29859
31270
  init_id();
29860
31271
  init_terminal_paste();
29861
- PASTE_DIR = join22(".code-viewer", "pasted");
31272
+ PASTE_DIR = join24(".code-viewer", "pasted");
29862
31273
  }
29863
31274
  });
29864
31275
 
@@ -29891,13 +31302,53 @@ async function handleStatePost(req) {
29891
31302
  return json({ ok: true, state: record });
29892
31303
  }
29893
31304
  function handleStatesGet(url) {
31305
+ const errors = getAgentActivityErrors();
29894
31306
  const target = url.searchParams.get("target");
29895
31307
  if (target) {
29896
31308
  const record = getAgentState(target);
29897
31309
  if (!record) return textError("unknown target", 404);
29898
- return json({ states: [record] });
31310
+ return json({ states: [record], errors });
31311
+ }
31312
+ return json({
31313
+ states: listAgentStates(),
31314
+ errors
31315
+ });
31316
+ }
31317
+ function ruleOperationError(code, error) {
31318
+ console.error(`[code-viewer] terminal rule ${code} failed`, error);
31319
+ const errors = [
31320
+ {
31321
+ path: "$",
31322
+ code,
31323
+ message: formatErrorDetail(error),
31324
+ ...error instanceof Error && error.stack ? { stack: error.stack } : {}
31325
+ }
31326
+ ];
31327
+ return json({ errors }, 500);
31328
+ }
31329
+ async function handleRulesGet(cwd2) {
31330
+ return json(await reloadAgentScreenRules(cwd2));
31331
+ }
31332
+ async function handleRulesPut(req, cwd2) {
31333
+ const body = await parseBoundedJsonBody(
31334
+ req,
31335
+ MAX_AGENT_SCREEN_RULES_BYTES,
31336
+ "terminal rules body too large"
31337
+ );
31338
+ if (body instanceof Response) return body;
31339
+ try {
31340
+ const result = await saveAgentScreenRules(cwd2, body);
31341
+ return json(result, "source" in result ? 200 : 400);
31342
+ } catch (error) {
31343
+ return ruleOperationError("save_failed", error);
31344
+ }
31345
+ }
31346
+ async function handleRulesDelete(cwd2) {
31347
+ try {
31348
+ return json(await resetAgentScreenRules(cwd2));
31349
+ } catch (error) {
31350
+ return ruleOperationError("reset_failed", error);
29899
31351
  }
29900
- return json({ states: listAgentStates() });
29901
31352
  }
29902
31353
  async function handleCaptureGet(url, cwd2) {
29903
31354
  const target = url.searchParams.get("target");
@@ -29970,6 +31421,15 @@ function handleAgentRoute(req, url, cwd2, sideEffectAllowed) {
29970
31421
  sideEffect: false,
29971
31422
  handler: () => Promise.resolve(handleStatesGet(url))
29972
31423
  },
31424
+ "/_agent/rules": {
31425
+ methods: ["GET", "PUT", "DELETE"],
31426
+ sideEffect: (method) => method !== "GET",
31427
+ handler: () => {
31428
+ if (req.method === "GET") return handleRulesGet(cwd2);
31429
+ if (req.method === "DELETE") return handleRulesDelete(cwd2);
31430
+ return handleRulesPut(req, cwd2);
31431
+ }
31432
+ },
29973
31433
  "/_agent/capture": {
29974
31434
  methods: ["GET"],
29975
31435
  sideEffect: false,
@@ -30006,10 +31466,12 @@ var init_handle4 = __esm({
30006
31466
  init_handle_shared();
30007
31467
  init_raw_file_headers();
30008
31468
  init_runtime();
31469
+ init_activity();
30009
31470
  init_agent_state2();
30010
31471
  init_capture2();
30011
31472
  init_images();
30012
31473
  init_paste();
31474
+ init_rules();
30013
31475
  MAX_STATE_TEXT = 2e3;
30014
31476
  }
30015
31477
  });
@@ -30035,248 +31497,112 @@ async function handleStatePatch(cwd2, req, patchState, tooLargeMessage, saveFail
30035
31497
  }
30036
31498
  }
30037
31499
  return json(next);
30038
- } catch (err) {
30039
- const message = err instanceof Error ? err.message : String(err);
30040
- if (message === tooLargeMessage) return textError(message, 413);
30041
- console.error("[code-viewer] state error:", err);
30042
- return textError(saveFailedMessage, 500);
30043
- }
30044
- }
30045
- async function handleSettingsGet(cwd2) {
30046
- return jsonLoadResponse(
30047
- () => loadAppSettingsState(cwd2),
30048
- "state",
30049
- "failed to load settings state"
30050
- );
30051
- }
30052
- async function handleSettingsPatch(cwd2, req, onChange) {
30053
- return handleStatePatch(
30054
- cwd2,
30055
- req,
30056
- patchAppSettingsState,
30057
- "settings state too large",
30058
- "failed to save settings state",
30059
- MAX_STATE_PATCH_BODY_BYTES,
30060
- onChange
30061
- );
30062
- }
30063
- async function handleViewGet(cwd2) {
30064
- return jsonLoadResponse(
30065
- () => loadViewState(cwd2),
30066
- "state",
30067
- "failed to load view state"
30068
- );
30069
- }
30070
- async function handleViewPatch(cwd2, req) {
30071
- return handleStatePatch(
30072
- cwd2,
30073
- req,
30074
- patchViewState,
30075
- "view state too large",
30076
- "failed to save view state",
30077
- MAX_STATE_PATCH_BODY_BYTES
30078
- );
30079
- }
30080
- async function handleToolsGet(cwd2) {
30081
- return jsonLoadResponse(
30082
- () => loadToolsState(cwd2),
30083
- "state",
30084
- "failed to load tools state"
30085
- );
30086
- }
30087
- async function handleToolsPatch(cwd2, req) {
30088
- return handleStatePatch(
30089
- cwd2,
30090
- req,
30091
- patchToolsState,
30092
- "tools state too large",
30093
- "failed to save tools state",
30094
- MAX_TOOLS_PATCH_BODY_BYTES
30095
- );
30096
- }
30097
- async function handleStateRoute(req, url, cwd2, sideEffectAllowed, options = {}) {
30098
- return dispatchRoutes(
30099
- req,
30100
- url,
30101
- {
30102
- "/_state/settings": {
30103
- methods: ["GET", "PATCH"],
30104
- sideEffect: (method) => method !== "GET",
30105
- handler: () => req.method === "GET" ? handleSettingsGet(cwd2) : handleSettingsPatch(cwd2, req, options.onSettingsChange)
30106
- },
30107
- "/_state/view": {
30108
- methods: ["GET", "PATCH"],
30109
- sideEffect: (method) => method !== "GET",
30110
- handler: () => req.method === "GET" ? handleViewGet(cwd2) : handleViewPatch(cwd2, req)
30111
- },
30112
- "/_state/tools": {
30113
- methods: ["GET", "PATCH"],
30114
- sideEffect: (method) => method !== "GET",
30115
- handler: () => req.method === "GET" ? handleToolsGet(cwd2) : handleToolsPatch(cwd2, req)
30116
- }
30117
- },
30118
- sideEffectAllowed,
30119
- (res) => res,
30120
- (err) => handleError("state", "handle state request", err)
30121
- );
30122
- }
30123
- var MAX_STATE_PATCH_BODY_BYTES, MAX_TOOLS_PATCH_BODY_BYTES;
30124
- var init_state_route = __esm({
30125
- "web-src/server/state-route.ts"() {
30126
- init_handle_shared();
30127
- init_state_store();
30128
- MAX_STATE_PATCH_BODY_BYTES = 1e6;
30129
- MAX_TOOLS_PATCH_BODY_BYTES = 4e6;
30130
- }
30131
- });
30132
-
30133
- // web-src/server/terminal/activity.ts
30134
- var activity_exports = {};
30135
- __export(activity_exports, {
30136
- ACTIVITY_IDLE_AFTER_MS: () => ACTIVITY_IDLE_AFTER_MS,
30137
- ACTIVITY_POLL_INTERVAL_MS: () => ACTIVITY_POLL_INTERVAL_MS,
30138
- MAX_PANES_PER_SWEEP: () => MAX_PANES_PER_SWEEP,
30139
- OVERRIDE_CHANGE_STREAK: () => OVERRIDE_CHANGE_STREAK,
30140
- nextActivityState: () => nextActivityState,
30141
- rotateForSweep: () => rotateForSweep,
30142
- startAgentActivityWatch: () => startAgentActivityWatch,
30143
- stopAgentActivityWatch: () => stopAgentActivityWatch
30144
- });
30145
- function nextActivityState(previous, hash, now) {
30146
- const changed = previous === void 0 || previous.hash !== hash;
30147
- const changedAt = changed ? now : previous.changedAt;
30148
- const changeStreak = changed ? previous === void 0 ? 0 : previous.changeStreak + 1 : 0;
30149
- return {
30150
- state: agentStateFromActivity(
30151
- changed,
30152
- now - changedAt,
30153
- ACTIVITY_IDLE_AFTER_MS
30154
- ),
30155
- seen: { hash, changedAt, changeStreak },
30156
- override: changeStreak >= OVERRIDE_CHANGE_STREAK
30157
- };
30158
- }
30159
- function observe(target, content, note) {
30160
- const next = nextActivityState(
30161
- seen.get(target),
30162
- hashLine(content),
30163
- Date.now()
30164
- );
30165
- seen.set(target, next.seen);
30166
- recordAgentState({
30167
- target,
30168
- state: next.state,
30169
- source: "activity",
30170
- note,
30171
- override: next.override
30172
- });
30173
- }
30174
- function rotateForSweep(items, offset, limit) {
30175
- if (items.length === 0) return { batch: [], nextOffset: 0 };
30176
- const take = Math.min(limit, items.length);
30177
- const start = (offset % items.length + items.length) % items.length;
30178
- const batch = [];
30179
- for (let i = 0; i < take; i += 1) {
30180
- batch.push(items[(start + i) % items.length]);
30181
- }
30182
- return { batch, nextOffset: (start + take) % items.length };
30183
- }
30184
- async function sweep(cwd2) {
30185
- if (inFlight) return;
30186
- inFlight = true;
30187
- try {
30188
- const panes = await listTmuxPanes(cwd2);
30189
- const shells = listShellSessions();
30190
- const allPanes = panes.running ? flattenTmuxPanes(panes.sessions) : [];
30191
- if (panes.running) {
30192
- const known = /* @__PURE__ */ new Set([
30193
- ...allPanes.map((pane) => pane.id),
30194
- ...shells.map((session) => session.id)
30195
- ]);
30196
- retainAgentStates(known);
30197
- for (const target of [...seen.keys()]) {
30198
- if (!known.has(target)) seen.delete(target);
30199
- }
30200
- }
30201
- for (const session of shells) {
30202
- const buffer = readShellBuffer(session.id);
30203
- if (!buffer) continue;
30204
- observe(session.id, buffer.replay, session.command);
30205
- }
30206
- const targets = allPanes;
30207
- const { batch, nextOffset } = rotateForSweep(
30208
- targets,
30209
- sweepOffset,
30210
- MAX_PANES_PER_SWEEP
30211
- );
30212
- sweepOffset = nextOffset;
30213
- for (const pane of batch) {
30214
- const result = await captureTmuxPane(pane.id, cwd2);
30215
- if (result.status !== "ok") {
30216
- seen.delete(pane.id);
30217
- continue;
30218
- }
30219
- observe(pane.id, result.screen.content, pane.title);
30220
- }
30221
- } catch (error) {
30222
- console.warn(
30223
- `[code-viewer] agent activity sweep skipped: ${String(error)}`
30224
- );
30225
- } finally {
30226
- inFlight = false;
31500
+ } catch (err) {
31501
+ const message = err instanceof Error ? err.message : String(err);
31502
+ if (message === tooLargeMessage) return textError(message, 413);
31503
+ console.error("[code-viewer] state error:", err);
31504
+ return textError(saveFailedMessage, 500);
30227
31505
  }
30228
31506
  }
30229
- function startAgentActivityWatch(cwd2) {
30230
- if (timer) return;
30231
- timer = setInterval(() => void sweep(cwd2), ACTIVITY_POLL_INTERVAL_MS);
30232
- timer.unref?.();
31507
+ async function handleSettingsGet(cwd2) {
31508
+ return jsonLoadResponse(
31509
+ () => loadAppSettingsState(cwd2),
31510
+ "state",
31511
+ "failed to load settings state"
31512
+ );
30233
31513
  }
30234
- function stopAgentActivityWatch() {
30235
- if (timer) clearInterval(timer);
30236
- timer = null;
30237
- seen.clear();
30238
- sweepOffset = 0;
31514
+ async function handleSettingsPatch(cwd2, req, onChange) {
31515
+ return handleStatePatch(
31516
+ cwd2,
31517
+ req,
31518
+ patchAppSettingsState,
31519
+ "settings state too large",
31520
+ "failed to save settings state",
31521
+ MAX_STATE_PATCH_BODY_BYTES,
31522
+ onChange
31523
+ );
30239
31524
  }
30240
- var ACTIVITY_POLL_INTERVAL_MS, ACTIVITY_IDLE_AFTER_MS, OVERRIDE_CHANGE_STREAK, MAX_PANES_PER_SWEEP, seen, timer, inFlight, sweepOffset;
30241
- var init_activity = __esm({
30242
- "web-src/server/terminal/activity.ts"() {
30243
- init_agent_state();
30244
- init_terminal_capture();
30245
- init_tmux();
30246
- init_session();
30247
- init_capture();
30248
- init_panes();
30249
- init_agent_state2();
30250
- ACTIVITY_POLL_INTERVAL_MS = 3e3;
30251
- ACTIVITY_IDLE_AFTER_MS = 15e3;
30252
- OVERRIDE_CHANGE_STREAK = 4;
30253
- MAX_PANES_PER_SWEEP = 12;
30254
- seen = /* @__PURE__ */ new Map();
30255
- timer = null;
30256
- inFlight = false;
30257
- sweepOffset = 0;
31525
+ async function handleViewGet(cwd2) {
31526
+ return jsonLoadResponse(
31527
+ () => loadViewState(cwd2),
31528
+ "state",
31529
+ "failed to load view state"
31530
+ );
31531
+ }
31532
+ async function handleViewPatch(cwd2, req) {
31533
+ return handleStatePatch(
31534
+ cwd2,
31535
+ req,
31536
+ patchViewState,
31537
+ "view state too large",
31538
+ "failed to save view state",
31539
+ MAX_STATE_PATCH_BODY_BYTES
31540
+ );
31541
+ }
31542
+ async function handleToolsGet(cwd2) {
31543
+ return jsonLoadResponse(
31544
+ () => loadToolsState(cwd2),
31545
+ "state",
31546
+ "failed to load tools state"
31547
+ );
31548
+ }
31549
+ async function handleToolsPatch(cwd2, req) {
31550
+ return handleStatePatch(
31551
+ cwd2,
31552
+ req,
31553
+ patchToolsState,
31554
+ "tools state too large",
31555
+ "failed to save tools state",
31556
+ MAX_TOOLS_PATCH_BODY_BYTES
31557
+ );
31558
+ }
31559
+ async function handleStateRoute(req, url, cwd2, sideEffectAllowed, options = {}) {
31560
+ return dispatchRoutes(
31561
+ req,
31562
+ url,
31563
+ {
31564
+ "/_state/settings": {
31565
+ methods: ["GET", "PATCH"],
31566
+ sideEffect: (method) => method !== "GET",
31567
+ handler: () => req.method === "GET" ? handleSettingsGet(cwd2) : handleSettingsPatch(cwd2, req, options.onSettingsChange)
31568
+ },
31569
+ "/_state/view": {
31570
+ methods: ["GET", "PATCH"],
31571
+ sideEffect: (method) => method !== "GET",
31572
+ handler: () => req.method === "GET" ? handleViewGet(cwd2) : handleViewPatch(cwd2, req)
31573
+ },
31574
+ "/_state/tools": {
31575
+ methods: ["GET", "PATCH"],
31576
+ sideEffect: (method) => method !== "GET",
31577
+ handler: () => req.method === "GET" ? handleToolsGet(cwd2) : handleToolsPatch(cwd2, req)
31578
+ }
31579
+ },
31580
+ sideEffectAllowed,
31581
+ (res) => res,
31582
+ (err) => handleError("state", "handle state request", err)
31583
+ );
31584
+ }
31585
+ var MAX_STATE_PATCH_BODY_BYTES, MAX_TOOLS_PATCH_BODY_BYTES;
31586
+ var init_state_route = __esm({
31587
+ "web-src/server/state-route.ts"() {
31588
+ init_handle_shared();
31589
+ init_state_store();
31590
+ MAX_STATE_PATCH_BODY_BYTES = 1e6;
31591
+ MAX_TOOLS_PATCH_BODY_BYTES = 4e6;
30258
31592
  }
30259
31593
  });
30260
31594
 
30261
31595
  // web-src/server/preview.ts
30262
31596
  var preview_exports = {};
30263
31597
  import {
30264
- closeSync as closeSync2,
30265
- constants as constants3,
30266
- existsSync as existsSync8,
30267
- lstatSync as lstatSync5,
30268
- mkdirSync as mkdirSync4,
30269
- openSync as openSync2,
31598
+ existsSync as existsSync9,
31599
+ mkdirSync as mkdirSync5,
30270
31600
  readFileSync as readFileSync8,
30271
31601
  realpathSync as realpathSync8,
30272
- renameSync,
30273
31602
  statSync as statSync8,
30274
- unlinkSync as unlinkSync2,
30275
- watch,
30276
- writeFileSync as writeFileSync2
31603
+ watch
30277
31604
  } from "node:fs";
30278
- import { homedir as homedir4 } from "node:os";
30279
- import { basename as basename4, dirname as dirname6, extname as extname2, join as join23, relative as relative8 } from "node:path";
31605
+ import { basename as basename5, dirname as dirname7, extname as extname2, join as join25, relative as relative8 } from "node:path";
30280
31606
  function parseCli() {
30281
31607
  const rest = [];
30282
31608
  for (let i = 2; i < process.argv.length; i++) {
@@ -30400,7 +31726,7 @@ Examples:
30400
31726
  }
30401
31727
  function warnIfLegacyConfigPresent() {
30402
31728
  try {
30403
- if (existsSync8(join23(cwd, ".code-viewer.json"))) {
31729
+ if (existsSync9(join25(cwd, ".code-viewer.json"))) {
30404
31730
  console.warn(
30405
31731
  "[code-viewer] .code-viewer.json is no longer used; configure scope and upload from Viewer Settings instead. The file can be safely removed."
30406
31732
  );
@@ -30507,8 +31833,8 @@ function staticFile(pathname) {
30507
31833
  }
30508
31834
  const spec = map[pathname];
30509
31835
  if (!spec) return null;
30510
- const full = join23(WEB_ROOT, spec[0]);
30511
- if (!existsSync8(full)) return text("not found", 404);
31836
+ const full = join25(WEB_ROOT, spec[0]);
31837
+ if (!existsSync9(full)) return text("not found", 404);
30512
31838
  return new Response(readFileSync8(full), {
30513
31839
  headers: { "Content-Type": spec[1], "Cache-Control": "no-store" }
30514
31840
  });
@@ -30602,7 +31928,7 @@ async function computePayload(extras, range, pathFilter = "", responseGeneration
30602
31928
  files: [],
30603
31929
  totals: { files: 0, additions: 0, deletions: 0 },
30604
31930
  range: "worktree .. worktree",
30605
- project: basename4(cwd),
31931
+ project: basename5(cwd),
30606
31932
  branch: await currentBranchMetadata(),
30607
31933
  generation: responseGeneration
30608
31934
  };
@@ -30645,7 +31971,7 @@ async function computePayload(extras, range, pathFilter = "", responseGeneration
30645
31971
  files: meta,
30646
31972
  totals,
30647
31973
  range: label || "HEAD",
30648
- project: basename4(cwd),
31974
+ project: basename5(cwd),
30649
31975
  branch: await currentBranchMetadata(),
30650
31976
  generation: responseGeneration,
30651
31977
  ...metaResult.error ? { error: metaResult.error } : {}
@@ -30766,7 +32092,7 @@ function safeWorktreePath2(path) {
30766
32092
  return safeWorktreePath(currentSearchEnv(), path);
30767
32093
  }
30768
32094
  function worktreePath(path) {
30769
- return join23(cwd, path);
32095
+ return join25(cwd, path);
30770
32096
  }
30771
32097
  function safeOpenWorktreePath(path) {
30772
32098
  if (path === "") {
@@ -30781,7 +32107,7 @@ function safeOpenWorktreePath(path) {
30781
32107
  return safeWorktreePath2(path);
30782
32108
  }
30783
32109
  function parentRepoPath(path) {
30784
- const parent = dirname6(path);
32110
+ const parent = dirname7(path);
30785
32111
  return parent === "." ? "" : parent;
30786
32112
  }
30787
32113
  function isoDate(ms) {
@@ -30930,7 +32256,7 @@ async function handleTree(url) {
30930
32256
  return json2({
30931
32257
  ref: target,
30932
32258
  path,
30933
- project: basename4(cwd),
32259
+ project: basename5(cwd),
30934
32260
  branch: await currentBranchMetadata(),
30935
32261
  entries: recursive ? entries.map(withStatus) : [
30936
32262
  ...await Promise.all(
@@ -30946,7 +32272,7 @@ async function handleTree(url) {
30946
32272
  }
30947
32273
  async function handleSettings() {
30948
32274
  return json2({
30949
- project: basename4(cwd),
32275
+ project: basename5(cwd),
30950
32276
  branch: await currentBranchMetadata(),
30951
32277
  repo_web_url: cwdHasGitRepository ? await remoteWebUrlAsync(cwd) : null,
30952
32278
  scope: {
@@ -31103,7 +32429,7 @@ async function handleLog(url) {
31103
32429
  }
31104
32430
  function blamePathKey(p) {
31105
32431
  try {
31106
- const st = statSync8(join23(cwd, p));
32432
+ const st = statSync8(join25(cwd, p));
31107
32433
  return `${st.mtimeMs}:${st.size}`;
31108
32434
  } catch {
31109
32435
  return "missing";
@@ -31591,9 +32917,6 @@ function safeUploadFileName(name) {
31591
32917
  if (!SAFE_UPLOAD_EXTENSIONS.has(extname2(trimmed).toLowerCase())) return null;
31592
32918
  return trimmed;
31593
32919
  }
31594
- function uploadOpenFlags() {
31595
- return constants3.O_WRONLY | constants3.O_CREAT | constants3.O_EXCL | (constants3.O_NOFOLLOW || 0);
31596
- }
31597
32920
  async function handleUploadFiles(req) {
31598
32921
  if (!uploadEnabled) return text("upload disabled by viewer settings", 403);
31599
32922
  if (req.method !== "POST") return text("method not allowed", 405);
@@ -31637,33 +32960,18 @@ async function handleUploadFiles(req) {
31637
32960
  if (file.size > MAX_UPLOAD_FILE_BYTES) return text("file too large", 413);
31638
32961
  total += file.size;
31639
32962
  if (total > MAX_UPLOAD_TOTAL_BYTES) return text("upload too large", 413);
31640
- const target = join23(realDir, safeName);
31641
- if (relative8(realDir, dirname6(target)) !== "")
32963
+ const target = join25(realDir, safeName);
32964
+ if (relative8(realDir, dirname7(target)) !== "")
31642
32965
  return text("invalid filename", 400);
31643
- if (existsSync8(target)) return text("file exists", 409);
32966
+ if (existsSync9(target)) return text("file exists", 409);
31644
32967
  uploads.push({ file, name: safeName, target });
31645
32968
  }
31646
- const written = [];
31647
32969
  try {
31648
- for (const upload of uploads) {
31649
- const fd = openSync2(upload.target, uploadOpenFlags(), 420);
31650
- try {
31651
- writeFileSync2(fd, new Uint8Array(await upload.file.arrayBuffer()));
31652
- } finally {
31653
- closeSync2(fd);
31654
- }
31655
- written.push(upload.target);
31656
- }
32970
+ await writeUploadedFiles(uploads);
31657
32971
  } catch (error) {
31658
- for (const path of written) {
31659
- try {
31660
- unlinkSync2(path);
31661
- } catch {
31662
- }
31663
- }
31664
32972
  if (error.code === "EEXIST")
31665
- return text("file exists", 409);
31666
- return text("upload failed", 500);
32973
+ return text(formatErrorDetail(error), 409);
32974
+ return text(formatErrorDetail(error), 500);
31667
32975
  }
31668
32976
  triggerUpdate(
31669
32977
  uploads.map((upload) => dir ? `${dir}/${upload.name}` : upload.name)
@@ -31674,78 +32982,6 @@ async function handleUploadFiles(req) {
31674
32982
  generation
31675
32983
  });
31676
32984
  }
31677
- function openOsPath(path) {
31678
- const cmd = process.platform === "darwin" ? ["open", "--", path] : process.platform === "win32" ? ["explorer.exe", path] : ["xdg-open", path];
31679
- spawnDetached(cmd);
31680
- }
31681
- function windowsTrashScript(path) {
31682
- const quotedPath = path.replace(/'/g, "''");
31683
- return [
31684
- "$ErrorActionPreference = 'Stop';",
31685
- `$path = '${quotedPath}';`,
31686
- "Add-Type -TypeDefinition @'",
31687
- "using System;",
31688
- "using System.Runtime.InteropServices;",
31689
- "public static class CodeViewerRecycleBin {",
31690
- " [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]",
31691
- " public struct SHFILEOPSTRUCT {",
31692
- " public IntPtr hwnd;",
31693
- " public uint wFunc;",
31694
- " public string pFrom;",
31695
- " public string pTo;",
31696
- " public ushort fFlags;",
31697
- " [MarshalAs(UnmanagedType.Bool)] public bool fAnyOperationsAborted;",
31698
- " public IntPtr hNameMappings;",
31699
- " public string lpszProgressTitle;",
31700
- " }",
31701
- ' [DllImport("shell32.dll", CharSet = CharSet.Unicode)]',
31702
- " private static extern int SHFileOperationW(ref SHFILEOPSTRUCT lpFileOp);",
31703
- " public static void MoveToRecycleBin(string path) {",
31704
- " const uint FO_DELETE = 0x0003;",
31705
- " const ushort FOF_SILENT = 0x0004;",
31706
- " const ushort FOF_NOCONFIRMATION = 0x0010;",
31707
- " const ushort FOF_ALLOWUNDO = 0x0040;",
31708
- " const ushort FOF_NOERRORUI = 0x0400;",
31709
- " var op = new SHFILEOPSTRUCT {",
31710
- " hwnd = IntPtr.Zero,",
31711
- " wFunc = FO_DELETE,",
31712
- ' pFrom = path + "\\0\\0",',
31713
- " pTo = null,",
31714
- " fFlags = (ushort)(FOF_ALLOWUNDO | FOF_NOCONFIRMATION | FOF_NOERRORUI | FOF_SILENT),",
31715
- " fAnyOperationsAborted = false,",
31716
- " hNameMappings = IntPtr.Zero,",
31717
- " lpszProgressTitle = null",
31718
- " };",
31719
- " int result = SHFileOperationW(ref op);",
31720
- ' if (result != 0) throw new InvalidOperationException("SHFileOperationW failed: " + result);',
31721
- ' if (op.fAnyOperationsAborted) throw new OperationCanceledException("SHFileOperationW aborted");',
31722
- " }",
31723
- "}",
31724
- "'@;",
31725
- "[CodeViewerRecycleBin]::MoveToRecycleBin($path);"
31726
- ].join(" ");
31727
- }
31728
- function windowsRestoreTrashScript(originalPath) {
31729
- const quotedPath = originalPath.replace(/'/g, "''");
31730
- return [
31731
- "$ErrorActionPreference = 'Stop';",
31732
- `$original = '${quotedPath}';`,
31733
- "$parent = [System.IO.Path]::GetDirectoryName($original);",
31734
- "$name = [System.IO.Path]::GetFileName($original);",
31735
- "$shell = New-Object -ComObject Shell.Application;",
31736
- "$bin = $shell.Namespace(10);",
31737
- "$restored = $false;",
31738
- "foreach ($item in $bin.Items()) {",
31739
- " $deletedFrom = $item.ExtendedProperty('System.Recycle.DeletedFrom');",
31740
- " if ($item.Name -eq $name -and $deletedFrom -eq $parent) {",
31741
- " $item.InvokeVerb('ESTORE');",
31742
- " $restored = $true;",
31743
- " break;",
31744
- " }",
31745
- "}",
31746
- "if (-not $restored) { throw 'recycle bin item not found'; }"
31747
- ].join(" ");
31748
- }
31749
32985
  function makeUndoId() {
31750
32986
  return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`;
31751
32987
  }
@@ -31760,86 +32996,6 @@ function triggerUpdate(changedPaths) {
31760
32996
  const data = changedPaths?.length && changedPaths.length <= 50 ? JSON.stringify({ generation, paths: changedPaths }) : "tick";
31761
32997
  sendSse("update", data);
31762
32998
  }
31763
- function moveMacPathIntoTrash(path) {
31764
- const trashDir = join23(homedir4(), ".Trash");
31765
- const base = basename4(path) || "code-viewer-trash-item";
31766
- const target = join23(
31767
- trashDir,
31768
- `${base}-${Date.now()}-${process.pid}-${Math.random().toString(36).slice(2, 8)}`
31769
- );
31770
- try {
31771
- mkdirSync4(trashDir, { recursive: true });
31772
- renameSync(path, target);
31773
- return { ok: true, trashPath: target };
31774
- } catch (error) {
31775
- return { ok: false, error: String(error) };
31776
- }
31777
- }
31778
- async function movePathToTrash(path) {
31779
- lstatSync5(path);
31780
- if (process.platform === "darwin") {
31781
- return moveMacPathIntoTrash(path);
31782
- }
31783
- if (process.platform === "win32") {
31784
- const res = await runAsync(
31785
- [
31786
- "powershell.exe",
31787
- "-NoProfile",
31788
- "-NonInteractive",
31789
- "-ExecutionPolicy",
31790
- "Bypass",
31791
- "-Command",
31792
- windowsTrashScript(path)
31793
- ],
31794
- cwd,
31795
- { timeout: 6e4 }
31796
- );
31797
- return res.code === 0 ? { ok: true } : { ok: false, error: res.stderr || res.stdout };
31798
- }
31799
- return { ok: false, error: "trash unsupported" };
31800
- }
31801
- async function restoreTrashPath(originalPath, trashPath) {
31802
- const parent = parentRepoPath(originalPath);
31803
- const parentFullPath = safeOpenWorktreePath(parent);
31804
- if (!parentFullPath) return { ok: false, error: "invalid restore target" };
31805
- const original = worktreePath(originalPath);
31806
- if (existsSync8(original))
31807
- return { ok: false, error: "restore target exists" };
31808
- if (trashPath) {
31809
- if (process.platform !== "darwin")
31810
- return { ok: false, error: "invalid trash handle" };
31811
- if (!existsSync8(trashPath))
31812
- return { ok: false, error: "trash item not found" };
31813
- try {
31814
- const trashRoot = join23(homedir4(), ".Trash");
31815
- const trashRelative = relative8(trashRoot, trashPath);
31816
- if (trashRelative === "" || trashRelative.startsWith("..") || trashRelative.startsWith("/") || trashRelative.startsWith("\\"))
31817
- return { ok: false, error: "invalid trash handle" };
31818
- mkdirSync4(dirname6(original), { recursive: true });
31819
- renameSync(trashPath, original);
31820
- return { ok: true };
31821
- } catch (error) {
31822
- return { ok: false, error: String(error) };
31823
- }
31824
- }
31825
- if (process.platform === "win32") {
31826
- const res = await runAsync(
31827
- [
31828
- "powershell.exe",
31829
- "-NoProfile",
31830
- "-NonInteractive",
31831
- "-ExecutionPolicy",
31832
- "Bypass",
31833
- "-Command",
31834
- windowsRestoreTrashScript(original)
31835
- ],
31836
- cwd,
31837
- { timeout: 6e4 }
31838
- );
31839
- return res.code === 0 ? { ok: true } : { ok: false, error: res.stderr || res.stdout };
31840
- }
31841
- return { ok: false, error: "undo unavailable for this trash operation" };
31842
- }
31843
32999
  async function handleOpenPath(req) {
31844
33000
  if (req.method !== "POST") return text("method not allowed", 405);
31845
33001
  if (!sideEffectRequestAllowed2(req)) return text("forbidden", 403);
@@ -31868,7 +33024,12 @@ async function handleOpenPath(req) {
31868
33024
  if (!target) return text("not found", 404);
31869
33025
  const stats = statSync8(target);
31870
33026
  if (!stats.isDirectory()) return text("not a directory", 400);
31871
- openOsPath(target);
33027
+ try {
33028
+ await openDirectoryInOs(target);
33029
+ } catch (error) {
33030
+ console.error("[code-viewer] failed to open path in OS:", error);
33031
+ return text(formatErrorDetail(error), 500);
33032
+ }
31872
33033
  return json2({ ok: true });
31873
33034
  }
31874
33035
  async function handleTrashPath(req) {
@@ -31890,17 +33051,23 @@ async function handleTrashPath(req) {
31890
33051
  const path = typeof body.path === "string" ? body.path.replace(/^\/+|\/+$/g, "") : "";
31891
33052
  if (!path) return text("invalid path", 400);
31892
33053
  if (!safeRepoPath(path)) return text("invalid path", 400);
31893
- if (isGitInternalPath(path)) return text("forbidden", 403);
33054
+ if (isGitInternalPath(path) || isCodeViewerInternalPath(path))
33055
+ return text("forbidden", 403);
31894
33056
  const originalFullPath = safeWorktreePath2(path);
31895
33057
  if (!originalFullPath) return text("not found", 404);
31896
33058
  let changedPaths;
31897
33059
  try {
31898
33060
  const stats = statSync8(originalFullPath);
31899
33061
  if (!stats.isDirectory()) changedPaths = [path];
31900
- } catch {
33062
+ } catch (error) {
33063
+ return text(formatErrorDetail(error), 500);
33064
+ }
33065
+ let moved;
33066
+ try {
33067
+ moved = await movePathToTrash(worktreePath(path), cwd);
33068
+ } catch (error) {
33069
+ return text(formatErrorDetail(error), 500);
31901
33070
  }
31902
- const moved = await movePathToTrash(worktreePath(path));
31903
- if (!moved.ok) return text(moved.error || "trash failed", 500);
31904
33071
  const undo = {
31905
33072
  id: makeUndoId(),
31906
33073
  type: "trash",
@@ -31944,10 +33111,10 @@ async function handleCreateDirectory(req) {
31944
33111
  const targetPath = dir ? `${dir}/${name}` : name;
31945
33112
  if (!safeRepoPath(targetPath) || isGitInternalPath(targetPath))
31946
33113
  return text("invalid target", 400);
31947
- const target = join23(parent, name);
31948
- if (existsSync8(target)) return text("already exists", 409);
33114
+ const target = join25(parent, name);
33115
+ if (existsSync9(target)) return text("already exists", 409);
31949
33116
  try {
31950
- mkdirSync4(target, { recursive: false });
33117
+ mkdirSync5(target, { recursive: false });
31951
33118
  } catch (error) {
31952
33119
  if (error.code === "EEXIST")
31953
33120
  return text("already exists", 409);
@@ -31976,14 +33143,22 @@ async function handleRestoreTrash(req) {
31976
33143
  const trashPath = typeof body.trashPath === "string" ? body.trashPath : "";
31977
33144
  if (!originalPath || !safeRepoPath(originalPath))
31978
33145
  return text("invalid restore target", 400);
31979
- if (isGitInternalPath(originalPath)) return text("forbidden", 403);
31980
- const restored = await restoreTrashPath(originalPath, trashPath || void 0);
31981
- if (!restored.ok) return text(restored.error || "undo failed", 409);
33146
+ if (isGitInternalPath(originalPath) || isCodeViewerInternalPath(originalPath))
33147
+ return text("forbidden", 403);
33148
+ const parent = parentRepoPath(originalPath);
33149
+ if (!safeOpenWorktreePath(parent)) return text("invalid restore target", 400);
33150
+ const original = worktreePath(originalPath);
33151
+ try {
33152
+ await restorePathFromTrash(original, trashPath || void 0, cwd);
33153
+ } catch (error) {
33154
+ return text(formatErrorDetail(error), 409);
33155
+ }
31982
33156
  let changedPaths;
31983
33157
  try {
31984
- const stats = statSync8(worktreePath(originalPath));
33158
+ const stats = statSync8(original);
31985
33159
  if (!stats.isDirectory()) changedPaths = [originalPath];
31986
- } catch {
33160
+ } catch (error) {
33161
+ return text(formatErrorDetail(error), 500);
31987
33162
  }
31988
33163
  triggerUpdate(changedPaths);
31989
33164
  return json2({ ok: true, generation });
@@ -32480,10 +33655,6 @@ function closeSseClients() {
32480
33655
  }
32481
33656
  }
32482
33657
  }
32483
- function openBrowser(url) {
32484
- const cmd = process.platform === "darwin" ? ["open", url] : process.platform === "win32" ? ["cmd.exe", "/c", "start", "", url] : ["xdg-open", url];
32485
- spawnDetached(cmd);
32486
- }
32487
33658
  async function shutdown(exitCode = 0) {
32488
33659
  if (shuttingDown) {
32489
33660
  process.exit(1);
@@ -32574,10 +33745,13 @@ var init_preview = __esm({
32574
33745
  init_command_resolver();
32575
33746
  init_dev_assets();
32576
33747
  init_doctor();
33748
+ init_file_upload();
32577
33749
  init_git();
32578
33750
  init_github_issues();
32579
33751
  init_journal2();
32580
33752
  init_mcp();
33753
+ init_os_opener();
33754
+ init_os_trash();
32581
33755
  init_range();
32582
33756
  init_raw_file_headers();
32583
33757
  init_request_origin();
@@ -32589,8 +33763,8 @@ var init_preview = __esm({
32589
33763
  init_state_store();
32590
33764
  init_watch_supervisor();
32591
33765
  init_worktree_watcher();
32592
- WEB_ROOT = join23(ROOT, "web");
32593
- VERSION = JSON.parse(readFileSync8(join23(ROOT, "package.json"), "utf8")).version;
33766
+ WEB_ROOT = join25(ROOT, "web");
33767
+ VERSION = JSON.parse(readFileSync8(join25(ROOT, "package.json"), "utf8")).version;
32594
33768
  DEFAULT_ARGS = ["HEAD"];
32595
33769
  PREVIEW_HUNKS_DEFAULT = 3;
32596
33770
  PREVIEW_LINES_DEFAULT = 1200;
@@ -32828,7 +34002,7 @@ data: ${watchLimitReached}
32828
34002
  });
32829
34003
  listenPort = server.port;
32830
34004
  if (openAfterStart) {
32831
- openBrowser(`http://127.0.0.1:${server.port}/`);
34005
+ await openUrlInOs(`http://127.0.0.1:${server.port}/`, cwd);
32832
34006
  }
32833
34007
  writeServerRegistry({
32834
34008
  url: `http://127.0.0.1:${server.port}/`,