@youtyan/code-viewer 0.11.2 → 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
 
@@ -5051,31 +5264,31 @@ function extractGithubIssueLabels(raw) {
5051
5264
  }
5052
5265
  function normalizeGithubIssueListItem(raw) {
5053
5266
  if (!raw || typeof raw !== "object") return null;
5054
- const issue = raw;
5055
- const number = issue.number;
5056
- const title = issue.title;
5267
+ const issue2 = raw;
5268
+ const number = issue2.number;
5269
+ const title = issue2.title;
5057
5270
  if (typeof number !== "number" || !Number.isInteger(number) || number <= 0 || typeof title !== "string" || !title.trim()) {
5058
5271
  return null;
5059
5272
  }
5060
- const url = singleLineGithubOption(issue.url);
5061
- 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";
5062
5275
  return {
5063
5276
  number,
5064
5277
  title: title.trim().slice(0, 200),
5065
5278
  state,
5066
5279
  ...url ? { url } : {},
5067
- labels: extractGithubIssueLabels(issue.labels)
5280
+ labels: extractGithubIssueLabels(issue2.labels)
5068
5281
  };
5069
5282
  }
5070
5283
  function parseGithubIssueListOutput(stdout) {
5071
5284
  const parsed = JSON.parse(stdout);
5072
5285
  if (!Array.isArray(parsed)) return [];
5073
- return parsed.map(normalizeGithubIssueListItem).filter((issue) => issue !== null);
5286
+ return parsed.map(normalizeGithubIssueListItem).filter((issue2) => issue2 !== null);
5074
5287
  }
5075
5288
  function parseGithubIssueViewOutput(stdout) {
5076
- const issue = normalizeGithubIssueListItem(JSON.parse(stdout));
5077
- if (!issue) throw new GithubIssueListError("failed to parse gh issue output");
5078
- return issue;
5289
+ const issue2 = normalizeGithubIssueListItem(JSON.parse(stdout));
5290
+ if (!issue2) throw new GithubIssueListError("failed to parse gh issue output");
5291
+ return issue2;
5079
5292
  }
5080
5293
  function buildGithubIssueListArgs(options) {
5081
5294
  const search = singleLineGithubOption(options.search);
@@ -5640,21 +5853,21 @@ function printGithubIssues(issues) {
5640
5853
  console.log("no GitHub issues");
5641
5854
  return;
5642
5855
  }
5643
- for (const issue of issues) {
5644
- const labels = issue.labels.length ? ` #${issue.labels.join(" #")}` : "";
5645
- 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}` : "";
5646
5859
  console.log(
5647
- `#${issue.number} ${issue.state} ${issue.title}${labels}${url}`
5860
+ `#${issue2.number} ${issue2.state} ${issue2.title}${labels}${url}`
5648
5861
  );
5649
5862
  }
5650
5863
  }
5651
- function taskLinkIssuePayload(command, issue) {
5864
+ function taskLinkIssuePayload(command, issue2) {
5652
5865
  return {
5653
5866
  action: "link-github-issue",
5654
- issue_number: issue.number,
5867
+ issue_number: issue2.number,
5655
5868
  repo: command.repo,
5656
- title: issue.title,
5657
- url: issue.url,
5869
+ title: issue2.title,
5870
+ url: issue2.url,
5658
5871
  memo_label: "Memo:",
5659
5872
  status: command.status,
5660
5873
  priority: command.priority,
@@ -5804,13 +6017,13 @@ async function runJournalCli(argv) {
5804
6017
  console.error(commandConfig.error);
5805
6018
  process.exit(1);
5806
6019
  }
5807
- const issue = await readGithubIssueAsync({
6020
+ const issue2 = await readGithubIssueAsync({
5808
6021
  cwd: root,
5809
6022
  number: command.issueNumber,
5810
6023
  repo: command.repo
5811
6024
  });
5812
6025
  if (dryRun) {
5813
- writePayload(taskLinkIssuePayload(command, issue));
6026
+ writePayload(taskLinkIssuePayload(command, issue2));
5814
6027
  return;
5815
6028
  }
5816
6029
  const serverUrl2 = await ensureServerUrl(root, server2, "/_journal");
@@ -5818,13 +6031,13 @@ async function runJournalCli(argv) {
5818
6031
  serverUrl2,
5819
6032
  "POST",
5820
6033
  "journal task-link-issue",
5821
- taskLinkIssuePayload(command, issue)
6034
+ taskLinkIssuePayload(command, issue2)
5822
6035
  );
5823
6036
  if (command.json)
5824
6037
  console.log(
5825
6038
  JSON.stringify(
5826
6039
  {
5827
- issue,
6040
+ issue: issue2,
5828
6041
  task: result2.task,
5829
6042
  action: result2.created ? "created" : result2.moved ? "moved" : "existing"
5830
6043
  },
@@ -5833,11 +6046,11 @@ async function runJournalCli(argv) {
5833
6046
  )
5834
6047
  );
5835
6048
  else if (result2.created)
5836
- console.log(`linked issue #${issue.number} to task ${result2.task.id}`);
6049
+ console.log(`linked issue #${issue2.number} to task ${result2.task.id}`);
5837
6050
  else if (result2.moved)
5838
- console.log(`moved linked issue #${issue.number} task ${result2.task.id}`);
6051
+ console.log(`moved linked issue #${issue2.number} task ${result2.task.id}`);
5839
6052
  else
5840
- 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}`);
5841
6054
  return;
5842
6055
  }
5843
6056
  const serverUrl = await ensureServerUrl(root, server2, "/_journal");
@@ -10255,9 +10468,9 @@ function parseTerminalArgs(argv) {
10255
10468
  function formatStateLine(record) {
10256
10469
  const mark = needsAttention(record.state) ? "*" : " ";
10257
10470
  const state = record.state.padEnd(7, " ");
10258
- const source = record.source === "hook" ? "hook" : "gues";
10471
+ const source = record.source === "hook" ? "hook " : record.source === "screen" ? "screen" : "motion";
10259
10472
  const text2 = record.note || record.lastPrompt || "";
10260
- return `${mark} ${state} ${source} ${record.target.padEnd(16, " ")} ${text2}`;
10473
+ return `${mark} ${state} ${source} ${record.target.padEnd(16, " ")} ${text2}`;
10261
10474
  }
10262
10475
  async function runTerminalCli(argv) {
10263
10476
  const parsed = parseTerminalArgs(argv);
@@ -10305,9 +10518,16 @@ async function runTerminalCli(argv) {
10305
10518
  const all = response2.states ?? [];
10306
10519
  const states2 = command.attentionOnly ? all.filter((record) => needsAttention(record.state)) : all;
10307
10520
  if (command.json) {
10308
- console.log(JSON.stringify({ states: states2 }, null, 2));
10521
+ console.log(
10522
+ JSON.stringify({ states: states2, errors: response2.errors ?? [] }, null, 2)
10523
+ );
10309
10524
  return;
10310
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
+ }
10311
10531
  if (states2.length === 0) {
10312
10532
  console.log("no terminals are reporting a state.");
10313
10533
  return;
@@ -22174,7 +22394,7 @@ function toFileInfo(entry) {
22174
22394
  kind: entry.kind
22175
22395
  };
22176
22396
  }
22177
- function errorMessage(err) {
22397
+ function errorMessage2(err) {
22178
22398
  const raw = err instanceof Error ? err.message : String(err);
22179
22399
  const withoutControl = Array.from(
22180
22400
  raw,
@@ -22202,7 +22422,7 @@ async function expandDockerServicesForFiles(dockerServices, listDockerDatabases,
22202
22422
  if (isAbortLikeError(err, signal)) throw err;
22203
22423
  return {
22204
22424
  entries: [svc],
22205
- errors: [`${svc.serviceName}: ${errorMessage(err)}`]
22425
+ errors: [`${svc.serviceName}: ${errorMessage2(err)}`]
22206
22426
  };
22207
22427
  }
22208
22428
  if (dbs.length <= 1) {
@@ -22250,7 +22470,7 @@ async function createDbFilesResponse(cwd2, omitDirNames, signal, deps = DEFAULT_
22250
22470
  const dockerServices = dockerSettled.status === "fulfilled" ? dockerSettled.value : [];
22251
22471
  const dockerErrors = [];
22252
22472
  if (dockerSettled.status === "rejected") {
22253
- dockerErrors.push(errorMessage(dockerSettled.reason));
22473
+ dockerErrors.push(errorMessage2(dockerSettled.reason));
22254
22474
  }
22255
22475
  const dockerTruncated = dockerServices.truncated === true;
22256
22476
  const { entries: dockerEntries, errors: listingErrors } = await expandDockerServicesForFiles(
@@ -23963,208 +24183,29 @@ var init_handle = __esm({
23963
24183
  }
23964
24184
  });
23965
24185
 
23966
- // web-src/core/error-detail.ts
23967
- function errorWithCause(message, cause) {
23968
- return Object.assign(new Error(message), { cause });
23969
- }
23970
- function errorWithCauses(message, errors) {
23971
- return Object.assign(new Error(message), { errors: [...errors] });
23972
- }
23973
- function isSensitiveFieldName(key) {
23974
- const normalized = key.toLowerCase().replace(/[^a-z0-9]/g, "");
23975
- 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");
23976
- }
23977
- function errorName(error) {
23978
- try {
23979
- return typeof error.name === "string" ? error.name : "Error";
23980
- } catch {
23981
- return "Error";
23982
- }
23983
- }
23984
- function errorMessage2(error) {
23985
- try {
23986
- return typeof error.message === "string" ? error.message : "unable to read error message";
23987
- } catch {
23988
- return "unable to read error message";
23989
- }
23990
- }
23991
- function sanitizeObjectFields(value, ancestors, excludedKeys = /* @__PURE__ */ new Set()) {
23992
- let keys;
23993
- try {
23994
- keys = Object.getOwnPropertyNames(value);
23995
- } catch {
23996
- return { value: "[Unserializable object]", removedSensitive: false };
23997
- }
23998
- const output = /* @__PURE__ */ Object.create(null);
23999
- let removedSensitive = false;
24000
- for (const key of keys) {
24001
- if (excludedKeys.has(key)) continue;
24002
- if (isSensitiveFieldName(key)) {
24003
- removedSensitive = true;
24004
- continue;
24005
- }
24006
- let descriptor;
24007
- try {
24008
- descriptor = Object.getOwnPropertyDescriptor(value, key);
24009
- } catch {
24010
- output[key] = "[Unserializable field]";
24011
- continue;
24012
- }
24013
- if (!descriptor) continue;
24014
- if (!("value" in descriptor)) {
24015
- output[key] = "[Accessor]";
24016
- continue;
24017
- }
24018
- const sanitized = sanitizeValue(descriptor.value, ancestors);
24019
- if (sanitized === OMIT_VALUE) {
24020
- removedSensitive = true;
24021
- continue;
24022
- }
24023
- output[key] = sanitized.value;
24024
- removedSensitive ||= sanitized.removedSensitive;
24025
- }
24026
- if (Object.keys(output).length === 0 && removedSensitive) return OMIT_VALUE;
24027
- return { value: output, removedSensitive };
24028
- }
24029
- function sanitizeError(error, ancestors) {
24030
- const output = /* @__PURE__ */ Object.create(null);
24031
- output.name = errorName(error);
24032
- output.message = errorMessage2(error);
24033
- const fields = sanitizeObjectFields(
24034
- error,
24035
- ancestors,
24036
- /* @__PURE__ */ new Set(["name", "message", "stack"])
24037
- );
24038
- if (fields !== OMIT_VALUE) Object.assign(output, fields.value);
24039
- return {
24040
- value: output,
24041
- removedSensitive: fields === OMIT_VALUE ? true : fields.removedSensitive
24042
- };
24043
- }
24044
- function sanitizeValue(value, ancestors) {
24045
- if (value === null || typeof value === "string" || typeof value === "boolean")
24046
- return { value, removedSensitive: false };
24047
- if (typeof value === "number") {
24048
- return {
24049
- value: Number.isFinite(value) ? value : String(value),
24050
- removedSensitive: false
24051
- };
24052
- }
24053
- if (typeof value === "bigint") {
24054
- return { value: `${value}n`, removedSensitive: false };
24055
- }
24056
- if (typeof value === "undefined") {
24057
- return { value: "[undefined]", removedSensitive: false };
24058
- }
24059
- if (typeof value === "symbol") {
24060
- return { value: "[symbol]", removedSensitive: false };
24061
- }
24062
- if (typeof value === "function") {
24063
- return { value: "[function]", removedSensitive: false };
24064
- }
24065
- const objectValue = value;
24066
- if (ancestors.has(objectValue)) {
24067
- return { value: "[Circular]", removedSensitive: false };
24068
- }
24069
- ancestors.add(objectValue);
24070
- try {
24071
- if (Array.isArray(objectValue)) {
24072
- const output = [];
24073
- let removedSensitive = false;
24074
- for (const item of objectValue) {
24075
- const sanitized = sanitizeValue(item, ancestors);
24076
- if (sanitized === OMIT_VALUE) {
24077
- removedSensitive = true;
24078
- continue;
24079
- }
24080
- output.push(sanitized.value);
24081
- removedSensitive ||= sanitized.removedSensitive;
24082
- }
24083
- if (output.length === 0 && removedSensitive) return OMIT_VALUE;
24084
- return { value: output, removedSensitive };
24085
- }
24086
- if (objectValue instanceof Error)
24087
- return sanitizeError(objectValue, ancestors);
24088
- return sanitizeObjectFields(objectValue, ancestors);
24089
- } catch {
24090
- return { value: "[Unserializable object]", removedSensitive: false };
24091
- } finally {
24092
- ancestors.delete(objectValue);
24093
- }
24094
- }
24095
- function formatNonError(value) {
24096
- if (typeof value === "string") return value;
24097
- try {
24098
- const sanitized = sanitizeValue(value, /* @__PURE__ */ new Set());
24099
- const serializable = sanitized === OMIT_VALUE ? {} : sanitized.value;
24100
- return JSON.stringify(serializable);
24101
- } catch {
24102
- return "[Unserializable value]";
24103
- }
24104
- }
24105
- function formatErrorFields(error) {
24106
- const fields = sanitizeObjectFields(
24107
- error,
24108
- /* @__PURE__ */ new Set([error]),
24109
- /* @__PURE__ */ new Set(["name", "message", "stack", "cause"])
24110
- );
24111
- if (fields === OMIT_VALUE) return "";
24112
- const output = fields.value;
24113
- return Object.keys(output).length > 0 ? `
24114
- Details: ${JSON.stringify(output)}` : "";
24115
- }
24116
- function formatErrorDetail(error) {
24117
- const parts = [];
24118
- const seen2 = /* @__PURE__ */ new Set();
24119
- let current = error;
24120
- while (current instanceof Error && !seen2.has(current)) {
24121
- seen2.add(current);
24122
- parts.push(
24123
- `${errorName(current)}: ${errorMessage2(current)}${formatErrorFields(current)}`
24124
- );
24125
- try {
24126
- current = current.cause;
24127
- } catch {
24128
- current = "[Unserializable error cause]";
24129
- }
24130
- }
24131
- if (current !== void 0) {
24132
- parts.push(
24133
- seen2.has(current) ? "Error cause cycle detected" : formatNonError(current)
24134
- );
24135
- }
24136
- return parts.join("\nCaused by: ") || formatNonError(error);
24137
- }
24138
- var OMIT_VALUE;
24139
- var init_error_detail = __esm({
24140
- "web-src/core/error-detail.ts"() {
24141
- OMIT_VALUE = /* @__PURE__ */ Symbol("omit-sensitive-error-field");
24142
- }
24143
- });
24144
-
24145
- // web-src/server/shell/session.ts
24146
- var session_exports = {};
24147
- __export(session_exports, {
24148
- closeAllShellSessions: () => closeAllShellSessions,
24149
- closeShellSession: () => closeShellSession,
24150
- createShellSession: () => createShellSession,
24151
- describeShellAvailability: () => describeShellAvailability,
24152
- getShellSession: () => getShellSession,
24153
- listShellSessions: () => listShellSessions,
24154
- readShellBuffer: () => readShellBuffer,
24155
- resizeShell: () => resizeShell,
24156
- subscribeShell: () => subscribeShell,
24157
- writeToShell: () => writeToShell,
24158
- writeToShellWhenReady: () => writeToShellWhenReady
24159
- });
24160
- function loadPty() {
24161
- if (!ptyModulePromise) {
24162
- ptyModulePromise = import("@lydell/node-pty").then((mod) => mod).catch((err) => {
24163
- ptyLoadError = errorWithCause("failed to load the PTY module", err);
24164
- return null;
24165
- });
24166
- }
24167
- 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;
24168
24209
  }
24169
24210
  async function describeShellAvailability() {
24170
24211
  const pty = await loadPty();
@@ -26986,76 +27027,676 @@ var init_search_service = __esm({
26986
27027
  }
26987
27028
  });
26988
27029
 
26989
- // web-src/server/terminal/agent-state.ts
26990
- function clip(value) {
26991
- 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;
26992
27035
  }
26993
- function evictOldest2() {
26994
- while (states.size > MAX_TRACKED_TARGETS) {
26995
- let oldestKey = null;
26996
- let oldestAt = Number.POSITIVE_INFINITY;
26997
- for (const [key, record] of states) {
26998
- if (record.updatedAt < oldestAt) {
26999
- oldestAt = record.updatedAt;
27000
- oldestKey = key;
27001
- }
27002
- }
27003
- if (oldestKey === null) return;
27004
- states.delete(oldestKey);
27005
- }
27036
+ function looksLikeBase64(value) {
27037
+ return typeof value === "string" && value.length > 0 && /^[A-Za-z0-9+/]+={0,2}$/.test(value);
27006
27038
  }
27007
- function recordAgentState(input) {
27008
- const previous = states.get(input.target);
27009
- const next = input.state ?? (input.event ? agentStateForEvent(input.event, previous?.state ?? null) : null);
27010
- if (!next) return null;
27011
- if (input.source === "activity" && previous?.source === "hook") {
27012
- const promoting = input.override === true && next === "working";
27013
- if (!promoting) return previous;
27014
- }
27015
- const at = Number.isFinite(input.at) ? input.at : Date.now();
27016
- if (previous && previous.source === "hook" && input.source === "hook" && at < previous.updatedAt) {
27017
- return previous;
27018
- }
27019
- const record = {
27020
- target: input.target,
27021
- state: next,
27022
- source: input.source,
27023
- updatedAt: at,
27024
- // 添え物は送られてこなければ前の値を残す。ターンの途中で毎回指示文を
27025
- // 送り直させないため。
27026
- lastPrompt: clip(input.lastPrompt ?? previous?.lastPrompt ?? ""),
27027
- note: clip(input.note ?? previous?.note ?? "")
27028
- };
27029
- states.set(input.target, record);
27030
- evictOldest2();
27031
- return record;
27039
+ function base64ByteLength(value) {
27040
+ const padding = value.endsWith("==") ? 2 : value.endsWith("=") ? 1 : 0;
27041
+ return Math.floor(value.length * 3 / 4) - padding;
27032
27042
  }
27033
- function getAgentState(target) {
27034
- return states.get(target) ?? null;
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~`;
27055
+ }
27056
+ });
27057
+
27058
+ // web-src/core/terminal-images.ts
27059
+ function stripAnsi(text2) {
27060
+ return text2.replace(ANSI_RE, "");
27035
27061
  }
27036
- function listAgentStates() {
27037
- return [...states.values()].sort((a, b) => {
27038
- const mine = Number(needsAttention(b.state)) - Number(needsAttention(a.state));
27039
- return mine !== 0 ? mine : a.updatedAt - b.updatedAt;
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
+ );
27089
+ }
27090
+ });
27091
+
27092
+ // web-src/core/agent-screen.ts
27093
+ function isRecord2(value) {
27094
+ return !!value && typeof value === "object" && !Array.isArray(value);
27095
+ }
27096
+ function issue(errors, path, code, message) {
27097
+ errors.push({ path, code, message });
27098
+ }
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);
27040
27131
  });
27132
+ return out;
27041
27133
  }
27042
- function retainAgentStates(known) {
27043
- let removed = 0;
27044
- for (const target of [...states.keys()]) {
27045
- if (!known.has(target)) {
27046
- states.delete(target);
27047
- 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;
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;
27048
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;
27049
27205
  }
27050
- 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;
27051
27213
  }
27052
- var MAX_TRACKED_TARGETS, MAX_TEXT_LENGTH, states;
27053
- var init_agent_state2 = __esm({
27054
- "web-src/server/terminal/agent-state.ts"() {
27055
- init_agent_state();
27056
- MAX_TRACKED_TARGETS = 200;
27057
- MAX_TEXT_LENGTH = 2e3;
27058
- 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;
27059
27700
  }
27060
27701
  });
27061
27702
 
@@ -27144,80 +27785,583 @@ function sliceShellBuffer(replay, totalChars, cursor) {
27144
27785
  reset: false
27145
27786
  };
27146
27787
  }
27147
- var ANCHOR_LINES, TMUX_CURSOR_RE, SHELL_CURSOR_RE;
27148
- var init_terminal_capture = __esm({
27149
- "web-src/core/terminal-capture.ts"() {
27150
- ANCHOR_LINES = 8;
27151
- TMUX_CURSOR_RE = /^t(\d+)\.([0-9a-z]+)\.([0-9a-z]+)$/;
27152
- SHELL_CURSOR_RE = /^s(\d+)$/;
27153
- }
27154
- });
27155
-
27156
- // web-src/server/tmux/capture.ts
27157
- function closeLineColors(content) {
27158
- if (!content) return content;
27159
- return content.split("\n").map((line) => line ? `${line}${SGR_RESET}` : line).join("\n");
27160
- }
27161
- async function captureTmuxPane(paneId, cwd2, historyLines = 0) {
27162
- const history = Math.min(
27163
- Math.max(Math.trunc(historyLines) || 0, 0),
27164
- MAX_TMUX_HISTORY_LINES
27165
- );
27166
- const result = await runTmux(
27167
- [
27168
- "display-message",
27169
- "-p",
27170
- "-t",
27171
- paneId,
27172
- "-F",
27173
- META_FORMAT,
27174
- ";",
27175
- "capture-pane",
27176
- "-e",
27177
- "-p",
27178
- "-t",
27179
- paneId,
27180
- ...history > 0 ? ["-S", `-${history}`] : []
27181
- ],
27182
- cwd2
27788
+ var ANCHOR_LINES, TMUX_CURSOR_RE, SHELL_CURSOR_RE;
27789
+ var init_terminal_capture = __esm({
27790
+ "web-src/core/terminal-capture.ts"() {
27791
+ ANCHOR_LINES = 8;
27792
+ TMUX_CURSOR_RE = /^t(\d+)\.([0-9a-z]+)\.([0-9a-z]+)$/;
27793
+ SHELL_CURSOR_RE = /^s(\d+)$/;
27794
+ }
27795
+ });
27796
+
27797
+ // web-src/server/tmux/capture.ts
27798
+ function closeLineColors(content) {
27799
+ if (!content) return content;
27800
+ return content.split("\n").map((line) => line ? `${line}${SGR_RESET}` : line).join("\n");
27801
+ }
27802
+ async function captureTmuxPane(paneId, cwd2, historyLines = 0) {
27803
+ const history = Math.min(
27804
+ Math.max(Math.trunc(historyLines) || 0, 0),
27805
+ MAX_TMUX_HISTORY_LINES
27806
+ );
27807
+ const result = await runTmux(
27808
+ [
27809
+ "display-message",
27810
+ "-p",
27811
+ "-t",
27812
+ paneId,
27813
+ "-F",
27814
+ META_FORMAT,
27815
+ ";",
27816
+ "capture-pane",
27817
+ "-e",
27818
+ "-p",
27819
+ "-t",
27820
+ paneId,
27821
+ ...history > 0 ? ["-S", `-${history}`] : []
27822
+ ],
27823
+ cwd2
27824
+ );
27825
+ if (result.status === "missing" || result.status === "no-server") {
27826
+ return { status: "gone" };
27827
+ }
27828
+ if (result.status === "no-target") return { status: "gone" };
27829
+ if (result.status === "error") {
27830
+ return result;
27831
+ }
27832
+ const newline = result.stdout.indexOf("\n");
27833
+ if (newline < 0) {
27834
+ return {
27835
+ status: "error",
27836
+ error: new Error("tmux capture returned no screen")
27837
+ };
27838
+ }
27839
+ const meta = result.stdout.slice(0, newline).split(" ");
27840
+ const toInt2 = (value) => Number.parseInt(value ?? "", 10) || 0;
27841
+ return {
27842
+ status: "ok",
27843
+ screen: {
27844
+ pane: paneId,
27845
+ content: closeLineColors(result.stdout.slice(newline + 1)),
27846
+ width: toInt2(meta[0]),
27847
+ height: toInt2(meta[1]),
27848
+ cursorX: toInt2(meta[2]),
27849
+ cursorY: toInt2(meta[3]),
27850
+ // 頼んだ行数と、実際に在る履歴の短いほう。
27851
+ historyLines: Math.min(history, toInt2(meta[4]))
27852
+ }
27853
+ };
27854
+ }
27855
+ var META_FORMAT, MAX_TMUX_HISTORY_LINES, SGR_RESET;
27856
+ var init_capture = __esm({
27857
+ "web-src/server/tmux/capture.ts"() {
27858
+ init_command();
27859
+ META_FORMAT = "#{pane_width} #{pane_height} #{cursor_x} #{cursor_y} #{history_size}";
27860
+ MAX_TMUX_HISTORY_LINES = 5e3;
27861
+ SGR_RESET = `${String.fromCharCode(27)}[0m`;
27862
+ }
27863
+ });
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
27183
28193
  );
27184
- if (result.status === "missing" || result.status === "no-server") {
27185
- return { status: "gone" };
27186
- }
27187
- if (result.status === "no-target") return { status: "gone" };
27188
- if (result.status === "error") {
27189
- return result;
28194
+ const detected = detectAgentScreen({ screen: content, title }, rules);
28195
+ if (detected.kind === "skip") {
28196
+ return { kind: "skip", seen: activity.seen, ruleId: detected.ruleId };
27190
28197
  }
27191
- const newline = result.stdout.indexOf("\n");
27192
- if (newline < 0) {
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
+ }
27193
28210
  return {
27194
- status: "error",
27195
- error: new Error("tmux capture returned no screen")
28211
+ kind: "record",
28212
+ state: detected.state,
28213
+ seen: activity.seen,
28214
+ override: detected.state === "working" ? activity.override : true,
28215
+ ruleId: detected.ruleId
27196
28216
  };
27197
28217
  }
27198
- const meta = result.stdout.slice(0, newline).split(" ");
27199
- const toInt2 = (value) => Number.parseInt(value ?? "", 10) || 0;
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) {
27200
28245
  return {
27201
- status: "ok",
27202
- screen: {
27203
- pane: paneId,
27204
- content: closeLineColors(result.stdout.slice(newline + 1)),
27205
- width: toInt2(meta[0]),
27206
- height: toInt2(meta[1]),
27207
- cursorX: toInt2(meta[2]),
27208
- cursorY: toInt2(meta[3]),
27209
- // 頼んだ行数と、実際に在る履歴の短いほう。
27210
- historyLines: Math.min(history, toInt2(meta[4]))
27211
- }
28246
+ operation,
28247
+ target,
28248
+ at: Date.now(),
28249
+ detail: formatErrorDetail(error),
28250
+ stack: error instanceof Error ? error.stack ?? "" : ""
27212
28251
  };
27213
28252
  }
27214
- var META_FORMAT, MAX_TMUX_HISTORY_LINES, SGR_RESET;
27215
- var init_capture = __esm({
27216
- "web-src/server/tmux/capture.ts"() {
27217
- init_command();
27218
- META_FORMAT = "#{pane_width} #{pane_height} #{cursor_x} #{cursor_y} #{history_size}";
27219
- MAX_TMUX_HISTORY_LINES = 5e3;
27220
- SGR_RESET = `${String.fromCharCode(27)}[0m`;
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;
27221
28365
  }
27222
28366
  });
27223
28367
 
@@ -27277,7 +28421,7 @@ var init_capture2 = __esm({
27277
28421
 
27278
28422
  // web-src/server/mcp.ts
27279
28423
  import { readFileSync as readFileSync7 } from "node:fs";
27280
- import { join as join20 } from "node:path";
28424
+ import { join as join21 } from "node:path";
27281
28425
  function defaultMcpTools(options = {}) {
27282
28426
  return [
27283
28427
  {
@@ -27775,7 +28919,7 @@ function defaultMcpTools(options = {}) {
27775
28919
  {
27776
28920
  name: "code_viewer_terminal_list",
27777
28921
  title: "code-viewer terminal list",
27778
- 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.",
27779
28923
  inputSchema: {
27780
28924
  type: "object",
27781
28925
  properties: {
@@ -27861,7 +29005,9 @@ function runTerminalListTool(input) {
27861
29005
  }
27862
29006
  const all = listAgentStates();
27863
29007
  const states2 = attentionOnly ? all.filter((record) => needsAttention(record.state)) : all;
27864
- return { text: JSON.stringify({ states: states2 }, null, 2) };
29008
+ return {
29009
+ text: JSON.stringify({ states: states2, errors: getAgentActivityErrors() }, null, 2)
29010
+ };
27865
29011
  }
27866
29012
  async function runTerminalCaptureTool(input, options) {
27867
29013
  const params = isPlainObject(input) ? input : {};
@@ -29023,12 +30169,13 @@ var init_mcp = __esm({
29023
30169
  init_search_cli();
29024
30170
  init_search_service();
29025
30171
  init_status_cli();
30172
+ init_activity();
29026
30173
  init_agent_state2();
29027
30174
  init_capture2();
29028
30175
  init_capture();
29029
30176
  MCP_PROTOCOL_VERSION = "2025-06-18";
29030
30177
  PACKAGE_VERSION = JSON.parse(
29031
- readFileSync7(join20(ROOT, "package.json"), "utf8")
30178
+ readFileSync7(join21(ROOT, "package.json"), "utf8")
29032
30179
  ).version;
29033
30180
  MCP_SERVER_INFO = {
29034
30181
  name: "code-viewer",
@@ -29220,7 +30367,7 @@ var init_os_opener = __esm({
29220
30367
  import { randomUUID as randomUUID2 } from "node:crypto";
29221
30368
  import { existsSync as existsSync8, lstatSync as lstatSync5, mkdirSync as mkdirSync4, renameSync } from "node:fs";
29222
30369
  import { homedir as homedir3, release as osRelease2 } from "node:os";
29223
- import { basename as basename3, dirname as dirname6, join as join21, resolve as resolve2 } from "node:path";
30370
+ import { basename as basename3, dirname as dirname6, join as join22, resolve as resolve2 } from "node:path";
29224
30371
  function windowsTrashScript(path) {
29225
30372
  const quotedPath = path.replace(/'/g, "''");
29226
30373
  return [
@@ -29311,12 +30458,12 @@ async function runRequiredCommand(operation, command, cwd2) {
29311
30458
  }
29312
30459
  }
29313
30460
  function managedTrashRoot(cwd2) {
29314
- return join21(cwd2, ".code-viewer", "trash");
30461
+ return join22(cwd2, ".code-viewer", "trash");
29315
30462
  }
29316
30463
  function movePathIntoTrashDirectory(path, trashRoot) {
29317
30464
  mkdirSync4(trashRoot, { recursive: true });
29318
30465
  const name = basename3(path) || "trash-item";
29319
- const trashPath = join21(trashRoot, `${name}-${randomUUID2()}`);
30466
+ const trashPath = join22(trashRoot, `${name}-${randomUUID2()}`);
29320
30467
  if (existsSync8(trashPath)) {
29321
30468
  throw Object.assign(new Error("trash destination already exists"), {
29322
30469
  trashPath
@@ -29326,7 +30473,7 @@ function movePathIntoTrashDirectory(path, trashRoot) {
29326
30473
  return { trashPath };
29327
30474
  }
29328
30475
  function trashRootForHandle(cwd2, platform, release) {
29329
- if (platform === "darwin") return join21(homedir3(), ".Trash");
30476
+ if (platform === "darwin") return join22(homedir3(), ".Trash");
29330
30477
  if (isWsl(platform, release)) return managedTrashRoot(cwd2);
29331
30478
  return null;
29332
30479
  }
@@ -29339,7 +30486,7 @@ function unsupportedTrashError(operation, platform, release) {
29339
30486
  async function movePathToTrash(path, cwd2, platform = process.platform, release = osRelease2()) {
29340
30487
  lstatSync5(path);
29341
30488
  if (platform === "darwin") {
29342
- return movePathIntoTrashDirectory(path, join21(homedir3(), ".Trash"));
30489
+ return movePathIntoTrashDirectory(path, join22(homedir3(), ".Trash"));
29343
30490
  }
29344
30491
  if (isWsl(platform, release)) {
29345
30492
  return movePathIntoTrashDirectory(path, managedTrashRoot(cwd2));
@@ -29428,12 +30575,12 @@ var init_request_origin = __esm({
29428
30575
 
29429
30576
  // web-src/server/watch-supervisor.ts
29430
30577
  import { spawn as spawn3 } from "node:child_process";
29431
- import { join as join22 } from "node:path";
30578
+ import { join as join23 } from "node:path";
29432
30579
  import { fileURLToPath as fileURLToPath3 } from "node:url";
29433
30580
  function watchChildCommand() {
29434
30581
  const entry = process.argv[1] ?? "";
29435
30582
  const isTypeScriptEntry = entry.endsWith(".ts");
29436
- const script = isTypeScriptEntry ? join22(fileURLToPath3(new URL(".", import.meta.url)), "cli.ts") : entry;
30583
+ const script = isTypeScriptEntry ? join23(fileURLToPath3(new URL(".", import.meta.url)), "cli.ts") : entry;
29437
30584
  const loaderArgs = isTypeScriptEntry ? process.execArgv : [];
29438
30585
  return [process.argv[0], ...loaderArgs, script, "watch-child"];
29439
30586
  }
@@ -29601,12 +30748,12 @@ function parseTmuxClients(stdout) {
29601
30748
  if (!line) continue;
29602
30749
  const fields = line.split(TMUX_FIELD_SEP);
29603
30750
  if (fields.length < CLIENT_FIELDS.length) continue;
29604
- const tty = fields[FIELD.tty] ?? "";
30751
+ const tty = fields[FIELD2.tty] ?? "";
29605
30752
  if (!tty) continue;
29606
30753
  clients.push({
29607
30754
  tty,
29608
- session: fields[FIELD.session] ?? "",
29609
- pane: fields[FIELD.pane] ?? ""
30755
+ session: fields[FIELD2.session] ?? "",
30756
+ pane: fields[FIELD2.pane] ?? ""
29610
30757
  });
29611
30758
  }
29612
30759
  return clients;
@@ -29621,7 +30768,7 @@ function findClientByTty(clients, tty) {
29621
30768
  if (!tty) return null;
29622
30769
  return clients.find((client) => client.tty === tty) ?? null;
29623
30770
  }
29624
- var CLIENT_FIELDS, CLIENT_FORMAT, FIELD;
30771
+ var CLIENT_FIELDS, CLIENT_FORMAT, FIELD2;
29625
30772
  var init_clients = __esm({
29626
30773
  "web-src/server/tmux/clients.ts"() {
29627
30774
  init_command();
@@ -29633,7 +30780,7 @@ var init_clients = __esm({
29633
30780
  "#{pane_id}"
29634
30781
  ];
29635
30782
  CLIENT_FORMAT = CLIENT_FIELDS.join(TMUX_FIELD_SEP);
29636
- FIELD = {
30783
+ FIELD2 = {
29637
30784
  tty: 0,
29638
30785
  session: 1,
29639
30786
  pane: 2
@@ -29719,141 +30866,20 @@ async function openTmuxPaneInShell(paneId, cwd2, size = {}) {
29719
30866
  status: "error",
29720
30867
  error: errorWithCauses(
29721
30868
  "failed to initialize the shell and close it afterward",
29722
- [writeError, closed.error]
29723
- )
29724
- };
29725
- }
29726
- return written;
29727
- }
29728
- return { status: "ok", session: created.session, action: "attached" };
29729
- }
29730
- var init_open = __esm({
29731
- "web-src/server/terminal/open.ts"() {
29732
- init_error_detail();
29733
- init_session();
29734
- init_clients();
29735
- init_focus();
29736
- }
29737
- });
29738
-
29739
- // web-src/server/tmux/panes.ts
29740
- function toInt(value) {
29741
- const parsed = Number.parseInt(value ?? "", 10);
29742
- return Number.isFinite(parsed) ? parsed : 0;
29743
- }
29744
- function toFlag(value) {
29745
- return value === "1";
29746
- }
29747
- function parseTmuxPanes(stdout, worktrees = []) {
29748
- const sessions2 = [];
29749
- const sessionByName = /* @__PURE__ */ new Map();
29750
- const windowByKey = /* @__PURE__ */ new Map();
29751
- for (const line of stdout.split("\n")) {
29752
- if (!line) continue;
29753
- const fields = line.split(TMUX_FIELD_SEP);
29754
- if (fields.length < PANE_FIELDS.length) continue;
29755
- const paneId = fields[FIELD2.paneId];
29756
- if (!paneId) continue;
29757
- const sessionName = fields[FIELD2.sessionName] ?? "";
29758
- let session = sessionByName.get(sessionName);
29759
- if (!session) {
29760
- session = {
29761
- name: sessionName,
29762
- attached: toFlag(fields[FIELD2.sessionAttached]),
29763
- windows: []
29764
- };
29765
- sessionByName.set(sessionName, session);
29766
- sessions2.push(session);
29767
- }
29768
- const windowIndex = toInt(fields[FIELD2.windowIndex]);
29769
- const windowKey = `${sessionName}${TMUX_FIELD_SEP}${windowIndex}`;
29770
- let window = windowByKey.get(windowKey);
29771
- if (!window) {
29772
- window = {
29773
- index: windowIndex,
29774
- name: fields[FIELD2.windowName] ?? "",
29775
- active: toFlag(fields[FIELD2.windowActive]),
29776
- panes: []
29777
- };
29778
- windowByKey.set(windowKey, window);
29779
- session.windows.push(window);
29780
- }
29781
- const paneIndex = toInt(fields[FIELD2.paneIndex]);
29782
- const pane = {
29783
- id: paneId,
29784
- label: `${sessionName}:${windowIndex}.${paneIndex}`,
29785
- paneIndex,
29786
- title: fields[FIELD2.paneTitle] ?? "",
29787
- command: fields[FIELD2.paneCommand] ?? "",
29788
- path: fields[FIELD2.panePath] ?? "",
29789
- width: toInt(fields[FIELD2.paneWidth]),
29790
- height: toInt(fields[FIELD2.paneHeight]),
29791
- active: toFlag(fields[FIELD2.paneActive]),
29792
- inRepo: worktrees.length === 0 || isPathInsideAny(fields[FIELD2.panePath] ?? "", worktrees)
29793
- };
29794
- window.panes.push(pane);
29795
- }
29796
- return sessions2;
29797
- }
29798
- async function listTmuxPanes(cwd2) {
29799
- const [result, worktrees] = await Promise.all([
29800
- runTmux(["list-panes", "-a", "-F", PANE_FORMAT], cwd2),
29801
- worktreePathsAsync(cwd2)
29802
- ]);
29803
- if (result.status === "missing") {
29804
- return { available: false, running: false, sessions: [] };
29805
- }
29806
- if (result.status === "no-server" || result.status === "no-target") {
29807
- return { available: true, running: false, sessions: [] };
29808
- }
29809
- if (result.status === "error") {
29810
- throw errorWithCause("failed to list tmux panes", result.error);
30869
+ [writeError, closed.error]
30870
+ )
30871
+ };
30872
+ }
30873
+ return written;
29811
30874
  }
29812
- return {
29813
- available: true,
29814
- running: true,
29815
- sessions: parseTmuxPanes(result.stdout, worktrees)
29816
- };
30875
+ return { status: "ok", session: created.session, action: "attached" };
29817
30876
  }
29818
- var PANE_FIELDS, PANE_FORMAT, FIELD2;
29819
- var init_panes = __esm({
29820
- "web-src/server/tmux/panes.ts"() {
30877
+ var init_open = __esm({
30878
+ "web-src/server/terminal/open.ts"() {
29821
30879
  init_error_detail();
29822
- init_tmux();
29823
- init_git();
29824
- init_command();
29825
- PANE_FIELDS = [
29826
- "#{pane_id}",
29827
- "#{session_name}",
29828
- "#{session_attached}",
29829
- "#{window_index}",
29830
- "#{window_name}",
29831
- "#{window_active}",
29832
- "#{pane_index}",
29833
- "#{pane_active}",
29834
- "#{pane_width}",
29835
- "#{pane_height}",
29836
- "#{pane_current_command}",
29837
- "#{pane_current_path}",
29838
- // タイトルは自由文字列なので必ず最後に置く。
29839
- "#{pane_title}"
29840
- ];
29841
- PANE_FORMAT = PANE_FIELDS.join(TMUX_FIELD_SEP);
29842
- FIELD2 = {
29843
- paneId: 0,
29844
- sessionName: 1,
29845
- sessionAttached: 2,
29846
- windowIndex: 3,
29847
- windowName: 4,
29848
- windowActive: 5,
29849
- paneIndex: 6,
29850
- paneActive: 7,
29851
- paneWidth: 8,
29852
- paneHeight: 9,
29853
- paneCommand: 10,
29854
- panePath: 11,
29855
- paneTitle: 12
29856
- };
30880
+ init_session();
30881
+ init_clients();
30882
+ init_focus();
29857
30883
  }
29858
30884
  });
29859
30885
 
@@ -30149,65 +31175,6 @@ var init_handle3 = __esm({
30149
31175
  }
30150
31176
  });
30151
31177
 
30152
- // web-src/core/terminal-paste.ts
30153
- function pasteImageExtension(mime) {
30154
- if (typeof mime !== "string") return null;
30155
- const base = mime.split(";")[0]?.trim().toLowerCase() ?? "";
30156
- return PASTE_IMAGE_TYPES[base] ?? null;
30157
- }
30158
- function looksLikeBase64(value) {
30159
- return typeof value === "string" && value.length > 0 && /^[A-Za-z0-9+/]+={0,2}$/.test(value);
30160
- }
30161
- function base64ByteLength(value) {
30162
- const padding = value.endsWith("==") ? 2 : value.endsWith("=") ? 1 : 0;
30163
- return Math.floor(value.length * 3 / 4) - padding;
30164
- }
30165
- var PASTE_IMAGE_TYPES, MAX_PASTE_IMAGE_BYTES, MAX_PASTE_BODY_BYTES, SHIFT_ENTER_SEQUENCE;
30166
- var init_terminal_paste = __esm({
30167
- "web-src/core/terminal-paste.ts"() {
30168
- PASTE_IMAGE_TYPES = {
30169
- "image/png": "png",
30170
- "image/jpeg": "jpg",
30171
- "image/gif": "gif",
30172
- "image/webp": "webp"
30173
- };
30174
- MAX_PASTE_IMAGE_BYTES = 8 * 1024 * 1024;
30175
- MAX_PASTE_BODY_BYTES = Math.ceil(MAX_PASTE_IMAGE_BYTES * 1.4);
30176
- SHIFT_ENTER_SEQUENCE = `${String.fromCharCode(27)}[200~${String.fromCharCode(10)}${String.fromCharCode(27)}[201~`;
30177
- }
30178
- });
30179
-
30180
- // web-src/core/terminal-images.ts
30181
- function terminalImageExtension(path) {
30182
- const dot = path.lastIndexOf(".");
30183
- if (dot < 0) return null;
30184
- const extension = path.slice(dot + 1).toLowerCase();
30185
- return TERMINAL_IMAGE_EXTENSIONS.includes(extension) ? extension : null;
30186
- }
30187
- var TERMINAL_IMAGE_EXTENSIONS, MAX_TERMINAL_IMAGE_QUERY, ESC, BEL, ANSI_RE, PATH_CHAR, NAME_CHAR, IMAGE_PATH_RE;
30188
- var init_terminal_images = __esm({
30189
- "web-src/core/terminal-images.ts"() {
30190
- init_terminal_paste();
30191
- TERMINAL_IMAGE_EXTENSIONS = [
30192
- ...new Set(Object.values(PASTE_IMAGE_TYPES)),
30193
- "jpeg"
30194
- ];
30195
- MAX_TERMINAL_IMAGE_QUERY = 16;
30196
- ESC = String.fromCharCode(27);
30197
- BEL = String.fromCharCode(7);
30198
- ANSI_RE = new RegExp(
30199
- `${ESC}\\[[0-9;?]*[ -/]*[@-~]|${ESC}\\][^${BEL}${ESC}]*(?:${BEL}|${ESC}\\\\)|${ESC}[@-Z\\\\-_]`,
30200
- "g"
30201
- );
30202
- PATH_CHAR = "[\\p{L}\\p{N}._~+@%/-]";
30203
- NAME_CHAR = "[\\p{L}\\p{N}_~+@%-]";
30204
- IMAGE_PATH_RE = new RegExp(
30205
- `${PATH_CHAR}*${NAME_CHAR}\\.(?:${TERMINAL_IMAGE_EXTENSIONS.join("|")})(?![\\p{L}\\p{N}])`,
30206
- "giu"
30207
- );
30208
- }
30209
- });
30210
-
30211
31178
  // web-src/server/terminal/images.ts
30212
31179
  import { realpathSync as realpathSync7, statSync as statSync7 } from "node:fs";
30213
31180
  import { homedir as homedir4 } from "node:os";
@@ -30259,7 +31226,7 @@ var init_images = __esm({
30259
31226
 
30260
31227
  // web-src/server/terminal/paste.ts
30261
31228
  import { mkdir as mkdir2, writeFile as writeFile2 } from "node:fs/promises";
30262
- import { join as join23 } from "node:path";
31229
+ import { join as join24 } from "node:path";
30263
31230
  async function savePastedImage(cwd2, mime, base64) {
30264
31231
  const extension = pasteImageExtension(mime);
30265
31232
  if (!extension) {
@@ -30284,8 +31251,8 @@ async function savePastedImage(cwd2, mime, base64) {
30284
31251
  return { status: "invalid", message: "image too large" };
30285
31252
  }
30286
31253
  const name = `${makeTimedId("paste")}.${extension}`;
30287
- const dir = join23(cwd2, PASTE_DIR);
30288
- const path = join23(dir, name);
31254
+ const dir = join24(cwd2, PASTE_DIR);
31255
+ const path = join24(dir, name);
30289
31256
  try {
30290
31257
  await mkdir2(dir, { recursive: true });
30291
31258
  await writeFile2(path, bytes);
@@ -30302,7 +31269,7 @@ var init_paste = __esm({
30302
31269
  "web-src/server/terminal/paste.ts"() {
30303
31270
  init_id();
30304
31271
  init_terminal_paste();
30305
- PASTE_DIR = join23(".code-viewer", "pasted");
31272
+ PASTE_DIR = join24(".code-viewer", "pasted");
30306
31273
  }
30307
31274
  });
30308
31275
 
@@ -30335,13 +31302,53 @@ async function handleStatePost(req) {
30335
31302
  return json({ ok: true, state: record });
30336
31303
  }
30337
31304
  function handleStatesGet(url) {
31305
+ const errors = getAgentActivityErrors();
30338
31306
  const target = url.searchParams.get("target");
30339
31307
  if (target) {
30340
31308
  const record = getAgentState(target);
30341
31309
  if (!record) return textError("unknown target", 404);
30342
- 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);
30343
31351
  }
30344
- return json({ states: listAgentStates() });
30345
31352
  }
30346
31353
  async function handleCaptureGet(url, cwd2) {
30347
31354
  const target = url.searchParams.get("target");
@@ -30414,6 +31421,15 @@ function handleAgentRoute(req, url, cwd2, sideEffectAllowed) {
30414
31421
  sideEffect: false,
30415
31422
  handler: () => Promise.resolve(handleStatesGet(url))
30416
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
+ },
30417
31433
  "/_agent/capture": {
30418
31434
  methods: ["GET"],
30419
31435
  sideEffect: false,
@@ -30450,10 +31466,12 @@ var init_handle4 = __esm({
30450
31466
  init_handle_shared();
30451
31467
  init_raw_file_headers();
30452
31468
  init_runtime();
31469
+ init_activity();
30453
31470
  init_agent_state2();
30454
31471
  init_capture2();
30455
31472
  init_images();
30456
31473
  init_paste();
31474
+ init_rules();
30457
31475
  MAX_STATE_TEXT = 2e3;
30458
31476
  }
30459
31477
  });
@@ -30574,134 +31592,6 @@ var init_state_route = __esm({
30574
31592
  }
30575
31593
  });
30576
31594
 
30577
- // web-src/server/terminal/activity.ts
30578
- var activity_exports = {};
30579
- __export(activity_exports, {
30580
- ACTIVITY_IDLE_AFTER_MS: () => ACTIVITY_IDLE_AFTER_MS,
30581
- ACTIVITY_POLL_INTERVAL_MS: () => ACTIVITY_POLL_INTERVAL_MS,
30582
- MAX_PANES_PER_SWEEP: () => MAX_PANES_PER_SWEEP,
30583
- OVERRIDE_CHANGE_STREAK: () => OVERRIDE_CHANGE_STREAK,
30584
- nextActivityState: () => nextActivityState,
30585
- rotateForSweep: () => rotateForSweep,
30586
- startAgentActivityWatch: () => startAgentActivityWatch,
30587
- stopAgentActivityWatch: () => stopAgentActivityWatch
30588
- });
30589
- function nextActivityState(previous, hash, now) {
30590
- const changed = previous === void 0 || previous.hash !== hash;
30591
- const changedAt = changed ? now : previous.changedAt;
30592
- const changeStreak = changed ? previous === void 0 ? 0 : previous.changeStreak + 1 : 0;
30593
- return {
30594
- state: agentStateFromActivity(
30595
- changed,
30596
- now - changedAt,
30597
- ACTIVITY_IDLE_AFTER_MS
30598
- ),
30599
- seen: { hash, changedAt, changeStreak },
30600
- override: changeStreak >= OVERRIDE_CHANGE_STREAK
30601
- };
30602
- }
30603
- function observe(target, content, note) {
30604
- const next = nextActivityState(
30605
- seen.get(target),
30606
- hashLine(content),
30607
- Date.now()
30608
- );
30609
- seen.set(target, next.seen);
30610
- recordAgentState({
30611
- target,
30612
- state: next.state,
30613
- source: "activity",
30614
- note,
30615
- override: next.override
30616
- });
30617
- }
30618
- function rotateForSweep(items, offset, limit) {
30619
- if (items.length === 0) return { batch: [], nextOffset: 0 };
30620
- const take = Math.min(limit, items.length);
30621
- const start = (offset % items.length + items.length) % items.length;
30622
- const batch = [];
30623
- for (let i = 0; i < take; i += 1) {
30624
- batch.push(items[(start + i) % items.length]);
30625
- }
30626
- return { batch, nextOffset: (start + take) % items.length };
30627
- }
30628
- async function sweep(cwd2) {
30629
- if (inFlight) return;
30630
- inFlight = true;
30631
- try {
30632
- const panes = await listTmuxPanes(cwd2);
30633
- const shells = listShellSessions();
30634
- const allPanes = panes.running ? flattenTmuxPanes(panes.sessions) : [];
30635
- if (panes.running) {
30636
- const known = /* @__PURE__ */ new Set([
30637
- ...allPanes.map((pane) => pane.id),
30638
- ...shells.map((session) => session.id)
30639
- ]);
30640
- retainAgentStates(known);
30641
- for (const target of [...seen.keys()]) {
30642
- if (!known.has(target)) seen.delete(target);
30643
- }
30644
- }
30645
- for (const session of shells) {
30646
- const buffer = readShellBuffer(session.id);
30647
- if (!buffer) continue;
30648
- observe(session.id, buffer.replay, session.command);
30649
- }
30650
- const targets = allPanes;
30651
- const { batch, nextOffset } = rotateForSweep(
30652
- targets,
30653
- sweepOffset,
30654
- MAX_PANES_PER_SWEEP
30655
- );
30656
- sweepOffset = nextOffset;
30657
- for (const pane of batch) {
30658
- const result = await captureTmuxPane(pane.id, cwd2);
30659
- if (result.status !== "ok") {
30660
- seen.delete(pane.id);
30661
- continue;
30662
- }
30663
- observe(pane.id, result.screen.content, pane.title);
30664
- }
30665
- } catch (error) {
30666
- console.warn(
30667
- `[code-viewer] agent activity sweep skipped: ${String(error)}`
30668
- );
30669
- } finally {
30670
- inFlight = false;
30671
- }
30672
- }
30673
- function startAgentActivityWatch(cwd2) {
30674
- if (timer) return;
30675
- timer = setInterval(() => void sweep(cwd2), ACTIVITY_POLL_INTERVAL_MS);
30676
- timer.unref?.();
30677
- }
30678
- function stopAgentActivityWatch() {
30679
- if (timer) clearInterval(timer);
30680
- timer = null;
30681
- seen.clear();
30682
- sweepOffset = 0;
30683
- }
30684
- var ACTIVITY_POLL_INTERVAL_MS, ACTIVITY_IDLE_AFTER_MS, OVERRIDE_CHANGE_STREAK, MAX_PANES_PER_SWEEP, seen, timer, inFlight, sweepOffset;
30685
- var init_activity = __esm({
30686
- "web-src/server/terminal/activity.ts"() {
30687
- init_agent_state();
30688
- init_terminal_capture();
30689
- init_tmux();
30690
- init_session();
30691
- init_capture();
30692
- init_panes();
30693
- init_agent_state2();
30694
- ACTIVITY_POLL_INTERVAL_MS = 3e3;
30695
- ACTIVITY_IDLE_AFTER_MS = 15e3;
30696
- OVERRIDE_CHANGE_STREAK = 4;
30697
- MAX_PANES_PER_SWEEP = 12;
30698
- seen = /* @__PURE__ */ new Map();
30699
- timer = null;
30700
- inFlight = false;
30701
- sweepOffset = 0;
30702
- }
30703
- });
30704
-
30705
31595
  // web-src/server/preview.ts
30706
31596
  var preview_exports = {};
30707
31597
  import {
@@ -30712,7 +31602,7 @@ import {
30712
31602
  statSync as statSync8,
30713
31603
  watch
30714
31604
  } from "node:fs";
30715
- import { basename as basename5, dirname as dirname7, extname as extname2, join as join24, 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";
30716
31606
  function parseCli() {
30717
31607
  const rest = [];
30718
31608
  for (let i = 2; i < process.argv.length; i++) {
@@ -30836,7 +31726,7 @@ Examples:
30836
31726
  }
30837
31727
  function warnIfLegacyConfigPresent() {
30838
31728
  try {
30839
- if (existsSync9(join24(cwd, ".code-viewer.json"))) {
31729
+ if (existsSync9(join25(cwd, ".code-viewer.json"))) {
30840
31730
  console.warn(
30841
31731
  "[code-viewer] .code-viewer.json is no longer used; configure scope and upload from Viewer Settings instead. The file can be safely removed."
30842
31732
  );
@@ -30943,7 +31833,7 @@ function staticFile(pathname) {
30943
31833
  }
30944
31834
  const spec = map[pathname];
30945
31835
  if (!spec) return null;
30946
- const full = join24(WEB_ROOT, spec[0]);
31836
+ const full = join25(WEB_ROOT, spec[0]);
30947
31837
  if (!existsSync9(full)) return text("not found", 404);
30948
31838
  return new Response(readFileSync8(full), {
30949
31839
  headers: { "Content-Type": spec[1], "Cache-Control": "no-store" }
@@ -31202,7 +32092,7 @@ function safeWorktreePath2(path) {
31202
32092
  return safeWorktreePath(currentSearchEnv(), path);
31203
32093
  }
31204
32094
  function worktreePath(path) {
31205
- return join24(cwd, path);
32095
+ return join25(cwd, path);
31206
32096
  }
31207
32097
  function safeOpenWorktreePath(path) {
31208
32098
  if (path === "") {
@@ -31539,7 +32429,7 @@ async function handleLog(url) {
31539
32429
  }
31540
32430
  function blamePathKey(p) {
31541
32431
  try {
31542
- const st = statSync8(join24(cwd, p));
32432
+ const st = statSync8(join25(cwd, p));
31543
32433
  return `${st.mtimeMs}:${st.size}`;
31544
32434
  } catch {
31545
32435
  return "missing";
@@ -32070,7 +32960,7 @@ async function handleUploadFiles(req) {
32070
32960
  if (file.size > MAX_UPLOAD_FILE_BYTES) return text("file too large", 413);
32071
32961
  total += file.size;
32072
32962
  if (total > MAX_UPLOAD_TOTAL_BYTES) return text("upload too large", 413);
32073
- const target = join24(realDir, safeName);
32963
+ const target = join25(realDir, safeName);
32074
32964
  if (relative8(realDir, dirname7(target)) !== "")
32075
32965
  return text("invalid filename", 400);
32076
32966
  if (existsSync9(target)) return text("file exists", 409);
@@ -32221,7 +33111,7 @@ async function handleCreateDirectory(req) {
32221
33111
  const targetPath = dir ? `${dir}/${name}` : name;
32222
33112
  if (!safeRepoPath(targetPath) || isGitInternalPath(targetPath))
32223
33113
  return text("invalid target", 400);
32224
- const target = join24(parent, name);
33114
+ const target = join25(parent, name);
32225
33115
  if (existsSync9(target)) return text("already exists", 409);
32226
33116
  try {
32227
33117
  mkdirSync5(target, { recursive: false });
@@ -32873,8 +33763,8 @@ var init_preview = __esm({
32873
33763
  init_state_store();
32874
33764
  init_watch_supervisor();
32875
33765
  init_worktree_watcher();
32876
- WEB_ROOT = join24(ROOT, "web");
32877
- VERSION = JSON.parse(readFileSync8(join24(ROOT, "package.json"), "utf8")).version;
33766
+ WEB_ROOT = join25(ROOT, "web");
33767
+ VERSION = JSON.parse(readFileSync8(join25(ROOT, "package.json"), "utf8")).version;
32878
33768
  DEFAULT_ARGS = ["HEAD"];
32879
33769
  PREVIEW_HUNKS_DEFAULT = 3;
32880
33770
  PREVIEW_LINES_DEFAULT = 1200;