@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.
package/web/app.js CHANGED
@@ -5,6 +5,311 @@
5
5
  __defProp(target, name, { get: all[name], enumerable: true });
6
6
  };
7
7
 
8
+ // web-src/core/terminal-paste.ts
9
+ var PASTE_IMAGE_TYPES = {
10
+ "image/png": "png",
11
+ "image/jpeg": "jpg",
12
+ "image/gif": "gif",
13
+ "image/webp": "webp"
14
+ };
15
+ var MAX_PASTE_IMAGE_BYTES = 8 * 1024 * 1024;
16
+ var MAX_PASTE_BODY_BYTES = Math.ceil(MAX_PASTE_IMAGE_BYTES * 1.4);
17
+ var SHIFT_ENTER_SEQUENCE = `${String.fromCharCode(27)}[200~${String.fromCharCode(10)}${String.fromCharCode(27)}[201~`;
18
+ function isShiftEnter(event) {
19
+ return event.key === "Enter" && event.shiftKey && !event.ctrlKey && !event.metaKey && !event.altKey;
20
+ }
21
+
22
+ // web-src/core/terminal-images.ts
23
+ var TERMINAL_IMAGE_EXTENSIONS = [
24
+ ...new Set(Object.values(PASTE_IMAGE_TYPES)),
25
+ "jpeg"
26
+ ];
27
+ var MAX_TERMINAL_IMAGE_PATHS = 64;
28
+ var MAX_TERMINAL_IMAGE_QUERY = 16;
29
+ var MAX_IMAGE_PATH_LENGTH = 1024;
30
+ var ESC = String.fromCharCode(27);
31
+ var BEL = String.fromCharCode(7);
32
+ var ANSI_RE = new RegExp(
33
+ `${ESC}\\[[0-9;?]*[ -/]*[@-~]|${ESC}\\][^${BEL}${ESC}]*(?:${BEL}|${ESC}\\\\)|${ESC}[@-Z\\\\-_]`,
34
+ "g"
35
+ );
36
+ function stripAnsi(text3) {
37
+ return text3.replace(ANSI_RE, "");
38
+ }
39
+ var PATH_CHAR = "[\\p{L}\\p{N}._~+@%/-]";
40
+ var NAME_CHAR = "[\\p{L}\\p{N}_~+@%-]";
41
+ var IMAGE_PATH_RE = new RegExp(
42
+ `${PATH_CHAR}*${NAME_CHAR}\\.(?:${TERMINAL_IMAGE_EXTENSIONS.join("|")})(?![\\p{L}\\p{N}])`,
43
+ "giu"
44
+ );
45
+ function looksLikeUrl(value) {
46
+ return value.startsWith("//") || value.includes("://");
47
+ }
48
+ function findImagePaths(text3, limit = MAX_TERMINAL_IMAGE_PATHS) {
49
+ const found = [];
50
+ const seen = /* @__PURE__ */ new Set();
51
+ for (const match2 of text3.matchAll(IMAGE_PATH_RE)) {
52
+ const path = match2[0];
53
+ if (path.length > MAX_IMAGE_PATH_LENGTH) continue;
54
+ if (looksLikeUrl(path)) continue;
55
+ if (seen.has(path)) continue;
56
+ seen.add(path);
57
+ found.push(path);
58
+ if (found.length >= limit) break;
59
+ }
60
+ return found;
61
+ }
62
+ function joinWrappedLines(text3, width) {
63
+ if (!Number.isFinite(width) || width <= 0) return text3;
64
+ const lines = text3.split("\n");
65
+ const joined = [];
66
+ let current = null;
67
+ let previousLength = 0;
68
+ for (const line of lines) {
69
+ if (current === null) {
70
+ current = line;
71
+ } else if (previousLength >= width) {
72
+ current += line;
73
+ } else {
74
+ joined.push(current);
75
+ current = line;
76
+ }
77
+ previousLength = line.length;
78
+ }
79
+ if (current !== null) joined.push(current);
80
+ return joined.join("\n");
81
+ }
82
+ function joinBrokenPathLines(text3) {
83
+ const lines = text3.split("\n");
84
+ const candidates = [];
85
+ for (let i2 = 0; i2 + 1 < lines.length; i2 += 1) {
86
+ const head = lastPathFragment(lines[i2] ?? "");
87
+ const tail = firstWord(lines[i2 + 1] ?? "");
88
+ if (!head || !tail) continue;
89
+ candidates.push(head.text + tail);
90
+ }
91
+ return candidates.join("\n");
92
+ }
93
+ function lastPathFragment(line) {
94
+ let found = null;
95
+ const words = /\S+/g;
96
+ for (const match2 of line.matchAll(words)) {
97
+ if (match2[0].includes("/")) {
98
+ found = { text: match2[0], index: match2.index ?? 0 };
99
+ }
100
+ }
101
+ return found;
102
+ }
103
+ function firstWord(line) {
104
+ return /\S+/.exec(line)?.[0] ?? "";
105
+ }
106
+ function findImagePathsInText(plain, width = 0, limit = MAX_TERMINAL_IMAGE_PATHS) {
107
+ const merged = [];
108
+ const seen = /* @__PURE__ */ new Set();
109
+ const sources = [
110
+ plain,
111
+ width > 0 ? joinWrappedLines(plain, width) : "",
112
+ joinBrokenPathLines(plain)
113
+ ];
114
+ for (const source of sources) {
115
+ if (!source) continue;
116
+ for (const path of findImagePaths(source, limit)) {
117
+ if (seen.has(path)) continue;
118
+ seen.add(path);
119
+ merged.push(path);
120
+ if (merged.length >= limit) return merged;
121
+ }
122
+ }
123
+ return merged;
124
+ }
125
+ function findPathAnchors(lines, candidates) {
126
+ const anchors = [];
127
+ for (const candidate of candidates) {
128
+ if (!candidate) continue;
129
+ const anchor = firstAnchor(lines, candidate);
130
+ if (anchor) anchors.push(anchor);
131
+ }
132
+ return anchors;
133
+ }
134
+ function firstAnchor(lines, candidate) {
135
+ for (let row = 0; row < lines.length; row += 1) {
136
+ const line = lines[row] ?? "";
137
+ const direct = line.indexOf(candidate);
138
+ if (direct >= 0) return { candidate, row, col: direct, span: 1 };
139
+ const head = lastPathFragment(line);
140
+ if (!head) continue;
141
+ const tail = firstWord(lines[row + 1] ?? "");
142
+ if (tail && head.text + tail === candidate) {
143
+ return { candidate, row, col: head.index, span: 2 };
144
+ }
145
+ }
146
+ return null;
147
+ }
148
+
149
+ // web-src/core/agent-screen.ts
150
+ var AGENT_SCREEN_RULE_SET_VERSION = 1;
151
+ var ESC2 = String.fromCharCode(27);
152
+ var BEL2 = String.fromCharCode(7);
153
+ var OSC_TITLE_RE = new RegExp(
154
+ `${ESC2}\\](?:0|2);([^${BEL2}${ESC2}]*)(?:${BEL2}|${ESC2}\\\\)`,
155
+ "g"
156
+ );
157
+ var BLOCKING_HINTS = [
158
+ { contains: ["enter to confirm"] },
159
+ { contains: ["enter to select"] },
160
+ { contains: ["enter to submit"] },
161
+ { contains: ["allow command?"] },
162
+ { contains: ["[y/n]"] },
163
+ { contains: ["yes (y)"] },
164
+ { contains: ["do you want to proceed?"] }
165
+ ];
166
+ var DEFAULT_AGENT_SCREEN_RULES = {
167
+ version: AGENT_SCREEN_RULE_SET_VERSION,
168
+ rules: [
169
+ {
170
+ id: "title_requires_input",
171
+ state: "waiting",
172
+ priority: 1100,
173
+ region: "osc_title",
174
+ contains: ["action required"]
175
+ },
176
+ {
177
+ id: "title_spinner",
178
+ state: "working",
179
+ priority: 1050,
180
+ region: "osc_title",
181
+ regex: ["^[⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏]"]
182
+ },
183
+ {
184
+ id: "transcript_view",
185
+ state: "skip",
186
+ priority: 1e3,
187
+ region: "bottom_non_empty",
188
+ lines: 8,
189
+ any: [
190
+ { contains: ["showing detailed transcript"] },
191
+ { contains: ["pgup/pgdn", "home/end to jump", "q to quit"] }
192
+ ]
193
+ },
194
+ {
195
+ id: "interactive_form",
196
+ state: "waiting",
197
+ priority: 980,
198
+ region: "bottom_non_empty",
199
+ lines: 14,
200
+ contains: ["esc to cancel"],
201
+ any: [
202
+ { contains: ["enter to confirm"] },
203
+ { contains: ["enter to select"] },
204
+ { contains: ["enter to submit"] }
205
+ ]
206
+ },
207
+ {
208
+ id: "live_reasoning",
209
+ state: "working",
210
+ priority: 970,
211
+ region: "bottom_non_empty",
212
+ lines: 8,
213
+ contains: ["tokens", "thinking"],
214
+ any: [{ lineRegex: ["^\\s*[✢✻✽✶✳]"] }, { lineRegex: ["(?i)^\\s*·"] }]
215
+ },
216
+ {
217
+ id: "prompt_box",
218
+ state: "idle",
219
+ priority: 950,
220
+ region: "bottom_non_empty",
221
+ lines: 8,
222
+ lineRegex: ["^\\s*❯"],
223
+ not: [
224
+ ...BLOCKING_HINTS,
225
+ { contains: ["esc to cancel"] },
226
+ { contains: ["arrow keys"] },
227
+ { contains: ["↑/↓ to navigate"] }
228
+ ]
229
+ },
230
+ {
231
+ id: "strong_input_request",
232
+ state: "waiting",
233
+ priority: 900,
234
+ region: "bottom_non_empty",
235
+ lines: 12,
236
+ any: [
237
+ { contains: ["press enter to confirm or esc to cancel"] },
238
+ { contains: ["enter to submit answer"] },
239
+ { contains: ["enter to submit all"] },
240
+ { contains: ["allow command?"] }
241
+ ]
242
+ },
243
+ {
244
+ id: "permission_request",
245
+ state: "waiting",
246
+ priority: 850,
247
+ region: "bottom_non_empty",
248
+ lines: 14,
249
+ contains: ["do you want to proceed?"],
250
+ any: [
251
+ { lineRegex: ["(?i)^\\P{L}*yes\\b"] },
252
+ { lineRegex: ["(?i)^\\P{L}*no\\b"] }
253
+ ]
254
+ },
255
+ {
256
+ id: "weak_input_request",
257
+ state: "waiting",
258
+ priority: 600,
259
+ region: "bottom_non_empty",
260
+ lines: 8,
261
+ any: [
262
+ { contains: ["[y/n]"] },
263
+ { contains: ["yes (y)"] },
264
+ {
265
+ any: [
266
+ { contains: ["do you want to"] },
267
+ { contains: ["would you like to"] }
268
+ ],
269
+ all: [{ any: [{ contains: ["yes"] }, { contains: ["❯"] }] }]
270
+ }
271
+ ]
272
+ },
273
+ {
274
+ id: "live_working_status",
275
+ state: "working",
276
+ priority: 500,
277
+ region: "bottom_non_empty",
278
+ lines: 3,
279
+ lineRegex: ["^[•◦]\\s+Working"],
280
+ not: [{ contains: ["conversation interrupted"] }]
281
+ },
282
+ {
283
+ id: "last_prompt",
284
+ state: "idle",
285
+ priority: 400,
286
+ region: "last_non_empty",
287
+ lineRegex: ["^\\s*[›❯]"],
288
+ not: BLOCKING_HINTS
289
+ }
290
+ ]
291
+ };
292
+ var MATCHER_KEYS = /* @__PURE__ */ new Set([
293
+ "contains",
294
+ "regex",
295
+ "lineRegex",
296
+ "all",
297
+ "any",
298
+ "not"
299
+ ]);
300
+ var RULE_KEYS = /* @__PURE__ */ new Set([
301
+ ...MATCHER_KEYS,
302
+ "id",
303
+ "state",
304
+ "priority",
305
+ "region",
306
+ "lines"
307
+ ]);
308
+ function formatAgentScreenRuleSet(rules) {
309
+ return `${JSON.stringify(rules, null, 2)}
310
+ `;
311
+ }
312
+
8
313
  // web-src/core/diff-file-kinds.ts
9
314
  var HEAVY_SIZE_CLASSES = /* @__PURE__ */ new Set(["medium", "large", "huge"]);
10
315
  function classifyDiffFileKind(file) {
@@ -277,6 +582,191 @@ ${lines.join("\n")}
277
582
  };
278
583
  }
279
584
 
585
+ // web-src/core/error-detail.ts
586
+ function errorWithCause(message, cause) {
587
+ return Object.assign(new Error(message), { cause });
588
+ }
589
+ function errorWithCauses(message, errors2) {
590
+ return Object.assign(new Error(message), { errors: [...errors2] });
591
+ }
592
+ var OMIT_VALUE = /* @__PURE__ */ Symbol("omit-sensitive-error-field");
593
+ function isSensitiveFieldName(key) {
594
+ const normalized = key.toLowerCase().replace(/[^a-z0-9]/g, "");
595
+ 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");
596
+ }
597
+ function errorName(error2) {
598
+ try {
599
+ return typeof error2.name === "string" ? error2.name : "Error";
600
+ } catch {
601
+ return "Error";
602
+ }
603
+ }
604
+ function errorMessage(error2) {
605
+ try {
606
+ return typeof error2.message === "string" ? error2.message : "unable to read error message";
607
+ } catch {
608
+ return "unable to read error message";
609
+ }
610
+ }
611
+ function sanitizeObjectFields(value, ancestors, excludedKeys = /* @__PURE__ */ new Set()) {
612
+ let keys;
613
+ try {
614
+ keys = Object.getOwnPropertyNames(value);
615
+ } catch {
616
+ return { value: "[Unserializable object]", removedSensitive: false };
617
+ }
618
+ const output = /* @__PURE__ */ Object.create(null);
619
+ let removedSensitive = false;
620
+ for (const key of keys) {
621
+ if (excludedKeys.has(key)) continue;
622
+ if (isSensitiveFieldName(key)) {
623
+ removedSensitive = true;
624
+ continue;
625
+ }
626
+ let descriptor;
627
+ try {
628
+ descriptor = Object.getOwnPropertyDescriptor(value, key);
629
+ } catch {
630
+ output[key] = "[Unserializable field]";
631
+ continue;
632
+ }
633
+ if (!descriptor) continue;
634
+ if (!("value" in descriptor)) {
635
+ output[key] = "[Accessor]";
636
+ continue;
637
+ }
638
+ const sanitized = sanitizeValue(descriptor.value, ancestors);
639
+ if (sanitized === OMIT_VALUE) {
640
+ removedSensitive = true;
641
+ continue;
642
+ }
643
+ output[key] = sanitized.value;
644
+ removedSensitive ||= sanitized.removedSensitive;
645
+ }
646
+ if (Object.keys(output).length === 0 && removedSensitive) return OMIT_VALUE;
647
+ return { value: output, removedSensitive };
648
+ }
649
+ function sanitizeError(error2, ancestors) {
650
+ const output = /* @__PURE__ */ Object.create(null);
651
+ output.name = errorName(error2);
652
+ output.message = errorMessage(error2);
653
+ const fields = sanitizeObjectFields(
654
+ error2,
655
+ ancestors,
656
+ /* @__PURE__ */ new Set(["name", "message", "stack"])
657
+ );
658
+ if (fields !== OMIT_VALUE) Object.assign(output, fields.value);
659
+ return {
660
+ value: output,
661
+ removedSensitive: fields === OMIT_VALUE ? true : fields.removedSensitive
662
+ };
663
+ }
664
+ function sanitizeValue(value, ancestors) {
665
+ if (value === null || typeof value === "string" || typeof value === "boolean")
666
+ return { value, removedSensitive: false };
667
+ if (typeof value === "number") {
668
+ return {
669
+ value: Number.isFinite(value) ? value : String(value),
670
+ removedSensitive: false
671
+ };
672
+ }
673
+ if (typeof value === "bigint") {
674
+ return { value: `${value}n`, removedSensitive: false };
675
+ }
676
+ if (typeof value === "undefined") {
677
+ return { value: "[undefined]", removedSensitive: false };
678
+ }
679
+ if (typeof value === "symbol") {
680
+ return { value: "[symbol]", removedSensitive: false };
681
+ }
682
+ if (typeof value === "function") {
683
+ return { value: "[function]", removedSensitive: false };
684
+ }
685
+ const objectValue = value;
686
+ if (ancestors.has(objectValue)) {
687
+ return { value: "[Circular]", removedSensitive: false };
688
+ }
689
+ ancestors.add(objectValue);
690
+ try {
691
+ if (Array.isArray(objectValue)) {
692
+ const output = [];
693
+ let removedSensitive = false;
694
+ for (const item of objectValue) {
695
+ const sanitized = sanitizeValue(item, ancestors);
696
+ if (sanitized === OMIT_VALUE) {
697
+ removedSensitive = true;
698
+ continue;
699
+ }
700
+ output.push(sanitized.value);
701
+ removedSensitive ||= sanitized.removedSensitive;
702
+ }
703
+ if (output.length === 0 && removedSensitive) return OMIT_VALUE;
704
+ return { value: output, removedSensitive };
705
+ }
706
+ if (objectValue instanceof Error)
707
+ return sanitizeError(objectValue, ancestors);
708
+ return sanitizeObjectFields(objectValue, ancestors);
709
+ } catch {
710
+ return { value: "[Unserializable object]", removedSensitive: false };
711
+ } finally {
712
+ ancestors.delete(objectValue);
713
+ }
714
+ }
715
+ function formatNonError(value) {
716
+ if (typeof value === "string") return value;
717
+ try {
718
+ const sanitized = sanitizeValue(value, /* @__PURE__ */ new Set());
719
+ const serializable = sanitized === OMIT_VALUE ? {} : sanitized.value;
720
+ return JSON.stringify(serializable);
721
+ } catch {
722
+ return "[Unserializable value]";
723
+ }
724
+ }
725
+ function formatErrorFields(error2) {
726
+ const fields = sanitizeObjectFields(
727
+ error2,
728
+ /* @__PURE__ */ new Set([error2]),
729
+ /* @__PURE__ */ new Set(["name", "message", "stack", "cause"])
730
+ );
731
+ if (fields === OMIT_VALUE) return "";
732
+ const output = fields.value;
733
+ return Object.keys(output).length > 0 ? `
734
+ Details: ${JSON.stringify(output)}` : "";
735
+ }
736
+ function formatErrorDetail(error2) {
737
+ const parts = [];
738
+ const seen = /* @__PURE__ */ new Set();
739
+ let current = error2;
740
+ while (current instanceof Error && !seen.has(current)) {
741
+ seen.add(current);
742
+ parts.push(
743
+ `${errorName(current)}: ${errorMessage(current)}${formatErrorFields(current)}`
744
+ );
745
+ try {
746
+ current = current.cause;
747
+ } catch {
748
+ current = "[Unserializable error cause]";
749
+ }
750
+ }
751
+ if (current !== void 0) {
752
+ parts.push(
753
+ seen.has(current) ? "Error cause cycle detected" : formatNonError(current)
754
+ );
755
+ }
756
+ return parts.join("\nCaused by: ") || formatNonError(error2);
757
+ }
758
+ async function responseErrorMessage(response, operation) {
759
+ const statusText = response.statusText ? ` ${response.statusText}` : "";
760
+ const prefix = `${operation} (HTTP ${response.status}${statusText})`;
761
+ let body;
762
+ try {
763
+ body = await response.text();
764
+ } catch (error2) {
765
+ throw errorWithCause(`${prefix}: failed to read response body`, error2);
766
+ }
767
+ return body ? `${prefix}: ${body}` : prefix;
768
+ }
769
+
280
770
  // web-src/core/expand-logic.ts
281
771
  function initExpandState(prevHunkEndNew, hunkNewStart) {
282
772
  return {
@@ -8439,16 +8929,18 @@ ${lines.join("\n")}
8439
8929
  function loadShikiHighlighter(options) {
8440
8930
  const key = JSON.stringify({
8441
8931
  themes: [...options.themes].sort(),
8442
- langs: [...options.langs].sort()
8932
+ langs: [...options.langs].sort(),
8933
+ failureMode: options.failureMode ?? "fallback"
8443
8934
  });
8444
8935
  const cached = cache.get(key);
8445
8936
  if (cached) return cached;
8446
- const promise = loadShikiModule().then(
8937
+ const load = loadShikiModule().then(
8447
8938
  (mod) => mod.createHighlighter({
8448
8939
  themes: options.themes,
8449
8940
  langs: options.langs
8450
8941
  })
8451
8942
  );
8943
+ const promise = options.failureMode === "throw" ? load : load.catch(() => null);
8452
8944
  cache.set(key, promise);
8453
8945
  return promise;
8454
8946
  }
@@ -11323,188 +11815,6 @@ ${frontmatter.yaml}
11323
11815
  return inferred;
11324
11816
  }
11325
11817
 
11326
- // web-src/core/error-detail.ts
11327
- function errorWithCause(message, cause) {
11328
- return Object.assign(new Error(message), { cause });
11329
- }
11330
- var OMIT_VALUE = /* @__PURE__ */ Symbol("omit-sensitive-error-field");
11331
- function isSensitiveFieldName(key) {
11332
- const normalized = key.toLowerCase().replace(/[^a-z0-9]/g, "");
11333
- 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");
11334
- }
11335
- function errorName(error2) {
11336
- try {
11337
- return typeof error2.name === "string" ? error2.name : "Error";
11338
- } catch {
11339
- return "Error";
11340
- }
11341
- }
11342
- function errorMessage(error2) {
11343
- try {
11344
- return typeof error2.message === "string" ? error2.message : "unable to read error message";
11345
- } catch {
11346
- return "unable to read error message";
11347
- }
11348
- }
11349
- function sanitizeObjectFields(value, ancestors, excludedKeys = /* @__PURE__ */ new Set()) {
11350
- let keys;
11351
- try {
11352
- keys = Object.getOwnPropertyNames(value);
11353
- } catch {
11354
- return { value: "[Unserializable object]", removedSensitive: false };
11355
- }
11356
- const output = /* @__PURE__ */ Object.create(null);
11357
- let removedSensitive = false;
11358
- for (const key of keys) {
11359
- if (excludedKeys.has(key)) continue;
11360
- if (isSensitiveFieldName(key)) {
11361
- removedSensitive = true;
11362
- continue;
11363
- }
11364
- let descriptor;
11365
- try {
11366
- descriptor = Object.getOwnPropertyDescriptor(value, key);
11367
- } catch {
11368
- output[key] = "[Unserializable field]";
11369
- continue;
11370
- }
11371
- if (!descriptor) continue;
11372
- if (!("value" in descriptor)) {
11373
- output[key] = "[Accessor]";
11374
- continue;
11375
- }
11376
- const sanitized = sanitizeValue(descriptor.value, ancestors);
11377
- if (sanitized === OMIT_VALUE) {
11378
- removedSensitive = true;
11379
- continue;
11380
- }
11381
- output[key] = sanitized.value;
11382
- removedSensitive ||= sanitized.removedSensitive;
11383
- }
11384
- if (Object.keys(output).length === 0 && removedSensitive) return OMIT_VALUE;
11385
- return { value: output, removedSensitive };
11386
- }
11387
- function sanitizeError(error2, ancestors) {
11388
- const output = /* @__PURE__ */ Object.create(null);
11389
- output.name = errorName(error2);
11390
- output.message = errorMessage(error2);
11391
- const fields = sanitizeObjectFields(
11392
- error2,
11393
- ancestors,
11394
- /* @__PURE__ */ new Set(["name", "message", "stack"])
11395
- );
11396
- if (fields !== OMIT_VALUE) Object.assign(output, fields.value);
11397
- return {
11398
- value: output,
11399
- removedSensitive: fields === OMIT_VALUE ? true : fields.removedSensitive
11400
- };
11401
- }
11402
- function sanitizeValue(value, ancestors) {
11403
- if (value === null || typeof value === "string" || typeof value === "boolean")
11404
- return { value, removedSensitive: false };
11405
- if (typeof value === "number") {
11406
- return {
11407
- value: Number.isFinite(value) ? value : String(value),
11408
- removedSensitive: false
11409
- };
11410
- }
11411
- if (typeof value === "bigint") {
11412
- return { value: `${value}n`, removedSensitive: false };
11413
- }
11414
- if (typeof value === "undefined") {
11415
- return { value: "[undefined]", removedSensitive: false };
11416
- }
11417
- if (typeof value === "symbol") {
11418
- return { value: "[symbol]", removedSensitive: false };
11419
- }
11420
- if (typeof value === "function") {
11421
- return { value: "[function]", removedSensitive: false };
11422
- }
11423
- const objectValue = value;
11424
- if (ancestors.has(objectValue)) {
11425
- return { value: "[Circular]", removedSensitive: false };
11426
- }
11427
- ancestors.add(objectValue);
11428
- try {
11429
- if (Array.isArray(objectValue)) {
11430
- const output = [];
11431
- let removedSensitive = false;
11432
- for (const item of objectValue) {
11433
- const sanitized = sanitizeValue(item, ancestors);
11434
- if (sanitized === OMIT_VALUE) {
11435
- removedSensitive = true;
11436
- continue;
11437
- }
11438
- output.push(sanitized.value);
11439
- removedSensitive ||= sanitized.removedSensitive;
11440
- }
11441
- if (output.length === 0 && removedSensitive) return OMIT_VALUE;
11442
- return { value: output, removedSensitive };
11443
- }
11444
- if (objectValue instanceof Error)
11445
- return sanitizeError(objectValue, ancestors);
11446
- return sanitizeObjectFields(objectValue, ancestors);
11447
- } catch {
11448
- return { value: "[Unserializable object]", removedSensitive: false };
11449
- } finally {
11450
- ancestors.delete(objectValue);
11451
- }
11452
- }
11453
- function formatNonError(value) {
11454
- if (typeof value === "string") return value;
11455
- try {
11456
- const sanitized = sanitizeValue(value, /* @__PURE__ */ new Set());
11457
- const serializable = sanitized === OMIT_VALUE ? {} : sanitized.value;
11458
- return JSON.stringify(serializable);
11459
- } catch {
11460
- return "[Unserializable value]";
11461
- }
11462
- }
11463
- function formatErrorFields(error2) {
11464
- const fields = sanitizeObjectFields(
11465
- error2,
11466
- /* @__PURE__ */ new Set([error2]),
11467
- /* @__PURE__ */ new Set(["name", "message", "stack", "cause"])
11468
- );
11469
- if (fields === OMIT_VALUE) return "";
11470
- const output = fields.value;
11471
- return Object.keys(output).length > 0 ? `
11472
- Details: ${JSON.stringify(output)}` : "";
11473
- }
11474
- function formatErrorDetail(error2) {
11475
- const parts = [];
11476
- const seen = /* @__PURE__ */ new Set();
11477
- let current = error2;
11478
- while (current instanceof Error && !seen.has(current)) {
11479
- seen.add(current);
11480
- parts.push(
11481
- `${errorName(current)}: ${errorMessage(current)}${formatErrorFields(current)}`
11482
- );
11483
- try {
11484
- current = current.cause;
11485
- } catch {
11486
- current = "[Unserializable error cause]";
11487
- }
11488
- }
11489
- if (current !== void 0) {
11490
- parts.push(
11491
- seen.has(current) ? "Error cause cycle detected" : formatNonError(current)
11492
- );
11493
- }
11494
- return parts.join("\nCaused by: ") || formatNonError(error2);
11495
- }
11496
- async function responseErrorMessage(response, operation) {
11497
- const statusText = response.statusText ? ` ${response.statusText}` : "";
11498
- const prefix = `${operation} (HTTP ${response.status}${statusText})`;
11499
- let body;
11500
- try {
11501
- body = await response.text();
11502
- } catch (error2) {
11503
- throw errorWithCause(`${prefix}: failed to read response body`, error2);
11504
- }
11505
- return body ? `${prefix}: ${body}` : prefix;
11506
- }
11507
-
11508
11818
  // web-src/core/id.ts
11509
11819
  function bytesToHex(bytes) {
11510
11820
  return Array.from(bytes, (b2) => b2.toString(16).padStart(2, "0")).join("");
@@ -23947,11 +24257,31 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
23947
24257
  }
23948
24258
  async function ensureDbUiState() {
23949
24259
  if (dbUiLoadPromise) return dbUiLoadPromise;
23950
- dbUiLoadPromise = fetch("/_db/ui").then(async (res) => {
23951
- if (!res.ok) return;
23952
- applyDbUiState(await res.json());
23953
- }).catch(() => void 0);
23954
- return dbUiLoadPromise;
24260
+ const operation = (async () => {
24261
+ const response = await deps.trackLoad(fetch("/_db/ui"));
24262
+ if (!response.ok) {
24263
+ throw new Error(
24264
+ await responseErrorMessage(response, "load database UI settings")
24265
+ );
24266
+ }
24267
+ let state;
24268
+ try {
24269
+ state = await response.json();
24270
+ } catch (error2) {
24271
+ throw errorWithCause(
24272
+ "load database UI settings: response is not valid JSON",
24273
+ error2
24274
+ );
24275
+ }
24276
+ applyDbUiState(state);
24277
+ })();
24278
+ dbUiLoadPromise = operation;
24279
+ try {
24280
+ await operation;
24281
+ } catch (error2) {
24282
+ if (dbUiLoadPromise === operation) dbUiLoadPromise = null;
24283
+ throw error2;
24284
+ }
23955
24285
  }
23956
24286
  function getColumnWidths(dbId, table2) {
23957
24287
  return { ...dbUiState.columnWidths[dbId]?.[table2] || {} };
@@ -24019,14 +24349,29 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
24019
24349
  const v = dbUiState.prefs?.[key];
24020
24350
  return v === void 0 ? fallback : v;
24021
24351
  }
24022
- function setDbUiPref(key, value) {
24023
- const nextPrefs = { ...dbUiState.prefs ?? {}, [key]: value };
24024
- applyDbUiState({ ...dbUiState, prefs: nextPrefs });
24025
- void fetch("/_db/ui", {
24026
- method: "PATCH",
24027
- headers: actionHeaders(),
24028
- body: JSON.stringify({ prefs: { [key]: value } })
24029
- }).catch(() => void 0);
24352
+ async function saveDbUiPrefs(patch) {
24353
+ const response = await deps.trackLoad(
24354
+ fetch("/_db/ui", {
24355
+ method: "PATCH",
24356
+ headers: actionHeaders(),
24357
+ body: JSON.stringify({ prefs: patch })
24358
+ })
24359
+ );
24360
+ if (!response.ok) {
24361
+ throw new Error(
24362
+ await responseErrorMessage(response, "save database UI settings")
24363
+ );
24364
+ }
24365
+ let saved;
24366
+ try {
24367
+ saved = await response.json();
24368
+ } catch (error2) {
24369
+ throw errorWithCause(
24370
+ "save database UI settings: response is not valid JSON",
24371
+ error2
24372
+ );
24373
+ }
24374
+ applyDbUiState({ ...dbUiState, prefs: saved.prefs });
24030
24375
  }
24031
24376
  function onDbUiPrefChange(listener) {
24032
24377
  dbUiPrefListeners.add(listener);
@@ -24455,7 +24800,6 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
24455
24800
  getSnapshotSelectedTables,
24456
24801
  setSnapshotSelectedTables,
24457
24802
  getDbUiPref,
24458
- setDbUiPref,
24459
24803
  onDbUiPrefChange,
24460
24804
  loadSqlHistory,
24461
24805
  refreshDatastores,
@@ -24872,7 +25216,8 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
24872
25216
  localize,
24873
25217
  refresh: refreshDatastores,
24874
25218
  getDbUiPref,
24875
- setDbUiPref,
25219
+ loadDbUiPrefs: ensureDbUiState,
25220
+ saveDbUiPrefs,
24876
25221
  onDbUiPrefChange
24877
25222
  };
24878
25223
  }
@@ -28421,7 +28766,7 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
28421
28766
  settings: {
28422
28767
  nav: "Settings",
28423
28768
  title: "Settings",
28424
- intro: "Viewer preferences for this project and this browser. Changes are saved as you make them.",
28769
+ intro: "Viewer preferences for this project and this browser. Edits remain a draft until you select Save changes.",
28425
28770
  groups: []
28426
28771
  },
28427
28772
  overview: {
@@ -28542,6 +28887,10 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
28542
28887
  kind: "paragraph",
28543
28888
  text: "The left side starts with a Your turn section for terminals that are waiting for input or finished but unread. Below it, you can scope the session list to this repository or all tmux sessions, filter by state, and search by task, place, or id. Its two-tier tree puts shells opened by this panel at the top: a shell running tmux carries a terminal icon and its session name, and that session's windows and panes hang underneath it. The bottom tier is the tmux sessions no shell has opened yet. Each pane is labelled with the title tmux shows for it — a coding agent running in a pane usually puts what it is doing there, so the tree alone tells you which pane is busy."
28544
28889
  },
28890
+ {
28891
+ kind: "paragraph",
28892
+ text: "Status first uses lifecycle reports when they are available. Otherwise it evaluates every enabled screen rule against the live terminal title and recent visible lines, then uses the highest-priority match. A terminal is tracked only after a report or a visible rule identifies it; screen motion then provides the working/idle fallback. Working matches expire when the title and screen stop changing, so stale status text does not stay active. Settings & Help → Settings contains the full JSON rule set, including regions, priorities, contains checks, regular expressions, and nested all/any/not conditions. Regular expressions use a bounded safe subset: groups, alternation, and backreferences are rejected, and AND/OR belongs in all/any. Saving validates the whole set and shows every error without replacing the active rules. Restoring the built-in rules removes the saved override so updated defaults can arrive with later releases."
28893
+ },
28545
28894
  {
28546
28895
  kind: "paragraph",
28547
28896
  text: "Click a pane and the panel takes you to it. If a shell already has that session open, it switches to that shell and makes the pane current; otherwise a shell is opened and attached for you. A session moves up to the top tier the moment a shell opens it and drops back down when that shell closes, so you end up with one shell per tmux session rather than one per pane. Powerline separators and file icons render when a Nerd Font is installed on the machine running the browser; no font ships with the package. The tree needs tmux on PATH — without it the panel says so, and shells still work. Opening shells needs the optional @lydell/node-pty package."
@@ -28596,6 +28945,10 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
28596
28945
  "settings.json",
28597
28946
  "Settings — diff layout, theme, language, sidebar/history widths, font sizes, syntax highlight, ignore-whitespace, hide-tests, scope overrides (omitted dirs / excluded names), upload toggle, annotation panel open/width/follow/mute/rate, and the last viewed diff range."
28598
28947
  ],
28948
+ [
28949
+ "agent-screen-rules.json",
28950
+ "Terminal status screen rules saved from Settings. Removing the override restores the built-in rules."
28951
+ ],
28599
28952
  [
28600
28953
  "view-state.json",
28601
28954
  "Sidebar tree state — collapsed directories, lazy-expanded directories (folders opened on demand in large repos), and the viewed-file list used to dim already-read entries."
@@ -29155,7 +29508,7 @@ code-viewer annotate add-db --db app.db --tab query \\
29155
29508
  settings: {
29156
29509
  nav: "設定",
29157
29510
  title: "設定",
29158
- intro: "このプロジェクトとこのブラウザのビューア設定です。変更は自動で保存されます。",
29511
+ intro: "このプロジェクトとこのブラウザのビューア設定です。「変更を保存」を押すまで編集内容は下書きのままです。",
29159
29512
  groups: []
29160
29513
  },
29161
29514
  overview: {
@@ -29276,6 +29629,10 @@ code-viewer annotate add-db --db app.db --tab query \\
29276
29629
  kind: "paragraph",
29277
29630
  text: "左側の先頭には、入力待ちと、終わったのにまだ見ていないターミナルを集める「あなたの番」があります。その下の一覧は、このリポジトリだけ、またはすべての tmux セッションに範囲を切り替え、状態で絞り込み、作業内容・場所・宛先で検索できます。2 段のツリーの上段はこのパネルが開いたシェルで、中で tmux が動いていれば端末の印とセッション名が付き、そのセッションのウィンドウとペインがその下にぶら下がります。下段は、まだどのシェルも開いていない tmux セッションです。ペインには tmux 側のタイトルが付きます。ペインで動いているコーディングエージェントは作業内容をタイトルに出すので、ツリーを見るだけでどのペインが動いているか分かります。"
29278
29631
  },
29632
+ {
29633
+ kind: "paragraph",
29634
+ text: "状態変更の申告がある場合はそれを先に使います。申告が無い場合は、現在のターミナルタイトルと画面下端の表示に対して全ルールを評価し、優先度が最大の一致から「作業中」「入力待ち」「待機中」「直前の状態を維持」を決めます。申告か見えているルールで対象を識別した後だけ、画面の変化量を作業中・待機中の補助判定に使います。作業中ルールの文字が残っていても、タイトルと画面が変化しなくなれば待機中へ移ります。設定・ヘルプ → 設定では、見る範囲、優先度、contains、正規表現、入れ子の all/any/not を含むJSONルール集を編集できます。正規表現は処理時間を抑えた範囲だけを許可し、グループ・選択・後方参照は使えません。AND/OR は all/any で表します。保存時は全ルールを検証し、エラーはすべて表示して適用中のルールを置き換えません。組み込みルールへ戻すと保存済みの上書きを削除するため、以後の更新で新しい既定ルールを受け取れます。"
29635
+ },
29279
29636
  {
29280
29637
  kind: "paragraph",
29281
29638
  text: "ペインを押すと、そこまで連れて行きます。そのセッションを既に開いているシェルがあれば、そのシェルに切り替えてペインをカレントにします。無ければ、こちらでシェルを開いて attach します。シェルで開いた瞬間にセッションは下段から上段へ移り、そのシェルを閉じると下段へ戻ります。つまりペインごとではなく、tmux のセッション 1 つにつきシェル 1 本になります。powerline のセパレータやファイルアイコンは、ブラウザを動かしている環境に Nerd Font が入っていれば表示されます(フォントはパッケージに同梱していません)。ツリーには tmux が PATH にあることが必要で、無い場合はその旨を表示します(シェルはそのまま使えます)。シェルを開くには任意依存の @lydell/node-pty が必要です。"
@@ -29330,6 +29687,10 @@ code-viewer annotate add-db --db app.db --tab query \\
29330
29687
  "settings.json",
29331
29688
  "設定 — diff レイアウト、テーマ、言語、サイドバー/履歴幅、フォントサイズ、シンタックスハイライト、whitespace 無視、テスト非表示、scope 上書き(除外ディレクトリ / 除外名)、アップロード許可、注釈パネルの開閉/幅/follow/ミュート/再生速度、最後に表示した diff 範囲を保存します。"
29332
29689
  ],
29690
+ [
29691
+ "agent-screen-rules.json",
29692
+ "設定画面で保存したターミナル状態の画面判定ルール。上書きを削除すると組み込みルールへ戻ります。"
29693
+ ],
29333
29694
  [
29334
29695
  "view-state.json",
29335
29696
  "サイドバーツリーの状態 — 折りたたみ済みディレクトリ、遅延展開済みディレクトリ(大規模リポジトリで必要に応じて開かれたフォルダ)、既読扱いするための表示済みファイル一覧。"
@@ -39320,7 +39681,7 @@ code-viewer annotate add-db --db app.db --tab query \\
39320
39681
  stateWaiting: "waiting",
39321
39682
  stateDone: "unread",
39322
39683
  stateIdle: "idle",
39323
- guessed: "guessed from screen activity",
39684
+ guessed: "detected from visible terminal UI or screen activity",
39324
39685
  filterPlaceholder: "task, place, or id",
39325
39686
  filterAll: "all",
39326
39687
  noMatches: "Nothing matches this filter.",
@@ -39391,11 +39752,11 @@ code-viewer annotate add-db --db app.db --tab query \\
39391
39752
  yourTurnHint: "入力待ちと、終わったのにまだ見ていないもの",
39392
39753
  yourTurnEmpty: "いま手を入れるものはありません。",
39393
39754
  sessions: "ターミナル",
39394
- stateWorking: "稼働",
39395
- stateWaiting: "待ち",
39755
+ stateWorking: "作業中",
39756
+ stateWaiting: "入力待ち",
39396
39757
  stateDone: "未読",
39397
- stateIdle: "停止",
39398
- guessed: "画面の動きからの推測",
39758
+ stateIdle: "待機中",
39759
+ guessed: "画面表示または画面の動きからの判定",
39399
39760
  filterPlaceholder: "作業内容・場所・宛先",
39400
39761
  filterAll: "すべて",
39401
39762
  noMatches: "この条件に合うものはありません。",
@@ -39660,6 +40021,10 @@ code-viewer annotate add-db --db app.db --tab query \\
39660
40021
  const attentionEmpty = document.createElement("p");
39661
40022
  attentionEmpty.className = "terminal-empty";
39662
40023
  attentionSection.append(attentionHead, attentionCards, attentionEmpty);
40024
+ const observationErrors = document.createElement("pre");
40025
+ observationErrors.className = "terminal-observation-errors";
40026
+ observationErrors.setAttribute("role", "alert");
40027
+ observationErrors.hidden = true;
39663
40028
  const filters = document.createElement("div");
39664
40029
  filters.className = "terminal-filters";
39665
40030
  const stateSelect = document.createElement("select");
@@ -39678,14 +40043,15 @@ code-viewer annotate add-db --db app.db --tab query \\
39678
40043
  tree.role = "tree";
39679
40044
  const treeEmpty = document.createElement("p");
39680
40045
  treeEmpty.className = "terminal-empty";
39681
- el.append(scopeBar, attentionSection, filters, tree);
40046
+ el.append(scopeBar, observationErrors, attentionSection, filters, tree);
39682
40047
  let data = {
39683
40048
  panes: null,
39684
40049
  shells: [],
39685
40050
  clients: [],
39686
40051
  shellAvailable: true,
39687
40052
  shellUnavailableReason: "",
39688
- states: []
40053
+ states: [],
40054
+ stateErrors: []
39689
40055
  };
39690
40056
  let selected = null;
39691
40057
  let stateFilter = null;
@@ -39825,7 +40191,7 @@ code-viewer annotate add-db --db app.db --tab query \\
39825
40191
  button.title = [
39826
40192
  row.task,
39827
40193
  row.kind === "tmux" ? `${row.locator} · ${row.place}` : row.place,
39828
- row.source === "activity" ? text3.guessed : "",
40194
+ row.source && row.source !== "hook" ? text3.guessed : "",
39829
40195
  row.lastPrompt ? `${text3.lastPrompt}: ${row.lastPrompt}` : ""
39830
40196
  ].filter(Boolean).join("\n");
39831
40197
  button.addEventListener("click", () => activate(row));
@@ -40028,6 +40394,13 @@ code-viewer annotate add-db --db app.db --tab query \\
40028
40394
  data.clients
40029
40395
  );
40030
40396
  const scoped = filterByScope(all, scope);
40397
+ observationErrors.textContent = data.stateErrors.map((error2) => {
40398
+ const target = error2.target ? ` ${error2.target}` : "";
40399
+ return `[${error2.operation}${target}]
40400
+ ${error2.detail}${error2.stack ? `
40401
+ ${error2.stack}` : ""}`;
40402
+ }).join("\n\n");
40403
+ observationErrors.hidden = data.stateErrors.length === 0;
40031
40404
  renderScope(all, scoped);
40032
40405
  const attention = attentionRows(scoped);
40033
40406
  attentionTitle.textContent = text3.yourTurn;
@@ -40057,147 +40430,6 @@ code-viewer annotate add-db --db app.db --tab query \\
40057
40430
  };
40058
40431
  }
40059
40432
 
40060
- // web-src/core/terminal-paste.ts
40061
- var PASTE_IMAGE_TYPES = {
40062
- "image/png": "png",
40063
- "image/jpeg": "jpg",
40064
- "image/gif": "gif",
40065
- "image/webp": "webp"
40066
- };
40067
- var MAX_PASTE_IMAGE_BYTES = 8 * 1024 * 1024;
40068
- var MAX_PASTE_BODY_BYTES = Math.ceil(MAX_PASTE_IMAGE_BYTES * 1.4);
40069
- var SHIFT_ENTER_SEQUENCE = `${String.fromCharCode(27)}[200~${String.fromCharCode(10)}${String.fromCharCode(27)}[201~`;
40070
- function isShiftEnter(event) {
40071
- return event.key === "Enter" && event.shiftKey && !event.ctrlKey && !event.metaKey && !event.altKey;
40072
- }
40073
-
40074
- // web-src/core/terminal-images.ts
40075
- var TERMINAL_IMAGE_EXTENSIONS = [
40076
- ...new Set(Object.values(PASTE_IMAGE_TYPES)),
40077
- "jpeg"
40078
- ];
40079
- var MAX_TERMINAL_IMAGE_PATHS = 64;
40080
- var MAX_TERMINAL_IMAGE_QUERY = 16;
40081
- var MAX_IMAGE_PATH_LENGTH = 1024;
40082
- var ESC = String.fromCharCode(27);
40083
- var BEL = String.fromCharCode(7);
40084
- var ANSI_RE = new RegExp(
40085
- `${ESC}\\[[0-9;?]*[ -/]*[@-~]|${ESC}\\][^${BEL}${ESC}]*(?:${BEL}|${ESC}\\\\)|${ESC}[@-Z\\\\-_]`,
40086
- "g"
40087
- );
40088
- function stripAnsi(text3) {
40089
- return text3.replace(ANSI_RE, "");
40090
- }
40091
- var PATH_CHAR = "[\\p{L}\\p{N}._~+@%/-]";
40092
- var NAME_CHAR = "[\\p{L}\\p{N}_~+@%-]";
40093
- var IMAGE_PATH_RE = new RegExp(
40094
- `${PATH_CHAR}*${NAME_CHAR}\\.(?:${TERMINAL_IMAGE_EXTENSIONS.join("|")})(?![\\p{L}\\p{N}])`,
40095
- "giu"
40096
- );
40097
- function looksLikeUrl(value) {
40098
- return value.startsWith("//") || value.includes("://");
40099
- }
40100
- function findImagePaths(text3, limit = MAX_TERMINAL_IMAGE_PATHS) {
40101
- const found = [];
40102
- const seen = /* @__PURE__ */ new Set();
40103
- for (const match2 of text3.matchAll(IMAGE_PATH_RE)) {
40104
- const path = match2[0];
40105
- if (path.length > MAX_IMAGE_PATH_LENGTH) continue;
40106
- if (looksLikeUrl(path)) continue;
40107
- if (seen.has(path)) continue;
40108
- seen.add(path);
40109
- found.push(path);
40110
- if (found.length >= limit) break;
40111
- }
40112
- return found;
40113
- }
40114
- function joinWrappedLines(text3, width) {
40115
- if (!Number.isFinite(width) || width <= 0) return text3;
40116
- const lines = text3.split("\n");
40117
- const joined = [];
40118
- let current = null;
40119
- let previousLength = 0;
40120
- for (const line of lines) {
40121
- if (current === null) {
40122
- current = line;
40123
- } else if (previousLength >= width) {
40124
- current += line;
40125
- } else {
40126
- joined.push(current);
40127
- current = line;
40128
- }
40129
- previousLength = line.length;
40130
- }
40131
- if (current !== null) joined.push(current);
40132
- return joined.join("\n");
40133
- }
40134
- function joinBrokenPathLines(text3) {
40135
- const lines = text3.split("\n");
40136
- const candidates = [];
40137
- for (let i2 = 0; i2 + 1 < lines.length; i2 += 1) {
40138
- const head = lastPathFragment(lines[i2] ?? "");
40139
- const tail = firstWord(lines[i2 + 1] ?? "");
40140
- if (!head || !tail) continue;
40141
- candidates.push(head.text + tail);
40142
- }
40143
- return candidates.join("\n");
40144
- }
40145
- function lastPathFragment(line) {
40146
- let found = null;
40147
- const words = /\S+/g;
40148
- for (const match2 of line.matchAll(words)) {
40149
- if (match2[0].includes("/")) {
40150
- found = { text: match2[0], index: match2.index ?? 0 };
40151
- }
40152
- }
40153
- return found;
40154
- }
40155
- function firstWord(line) {
40156
- return /\S+/.exec(line)?.[0] ?? "";
40157
- }
40158
- function findImagePathsInText(plain, width = 0, limit = MAX_TERMINAL_IMAGE_PATHS) {
40159
- const merged = [];
40160
- const seen = /* @__PURE__ */ new Set();
40161
- const sources = [
40162
- plain,
40163
- width > 0 ? joinWrappedLines(plain, width) : "",
40164
- joinBrokenPathLines(plain)
40165
- ];
40166
- for (const source of sources) {
40167
- if (!source) continue;
40168
- for (const path of findImagePaths(source, limit)) {
40169
- if (seen.has(path)) continue;
40170
- seen.add(path);
40171
- merged.push(path);
40172
- if (merged.length >= limit) return merged;
40173
- }
40174
- }
40175
- return merged;
40176
- }
40177
- function findPathAnchors(lines, candidates) {
40178
- const anchors = [];
40179
- for (const candidate of candidates) {
40180
- if (!candidate) continue;
40181
- const anchor = firstAnchor(lines, candidate);
40182
- if (anchor) anchors.push(anchor);
40183
- }
40184
- return anchors;
40185
- }
40186
- function firstAnchor(lines, candidate) {
40187
- for (let row = 0; row < lines.length; row += 1) {
40188
- const line = lines[row] ?? "";
40189
- const direct = line.indexOf(candidate);
40190
- if (direct >= 0) return { candidate, row, col: direct, span: 1 };
40191
- const head = lastPathFragment(line);
40192
- if (!head) continue;
40193
- const tail = firstWord(lines[row + 1] ?? "");
40194
- if (tail && head.text + tail === candidate) {
40195
- return { candidate, row, col: head.index, span: 2 };
40196
- }
40197
- }
40198
- return null;
40199
- }
40200
-
40201
40433
  // web-src/core/xterm-loader.ts
40202
40434
  var loadXterm = createBundleLoader("xterm.js");
40203
40435
 
@@ -40894,6 +41126,7 @@ ${formatErrorDetail(error2)}`
40894
41126
  let board = null;
40895
41127
  let lastTargetId = null;
40896
41128
  let states = [];
41129
+ let stateErrors = [];
40897
41130
  let screen = null;
40898
41131
  let reloadBtn = null;
40899
41132
  let fontSmaller = null;
@@ -41003,7 +41236,8 @@ ${formatErrorDetail(error2)}`);
41003
41236
  clients: clients?.clients ?? [],
41004
41237
  shellAvailable: shells?.available ?? true,
41005
41238
  shellUnavailableReason: shells?.reason ?? "",
41006
- states
41239
+ states,
41240
+ stateErrors
41007
41241
  });
41008
41242
  board?.setSelected(attached?.id ?? null);
41009
41243
  }
@@ -41036,12 +41270,13 @@ ${formatErrorDetail(error2)}`);
41036
41270
  }
41037
41271
  const nextPanes = await paneRes.json();
41038
41272
  const nextShells = await shellRes.json();
41039
- const nextStates = (await stateRes.json()).states;
41273
+ const nextStateResponse = await stateRes.json();
41040
41274
  const nextClients = await clientRes.json();
41041
41275
  if (stale()) return;
41042
41276
  panes = nextPanes;
41043
41277
  shells = nextShells;
41044
- states = nextStates ?? [];
41278
+ states = nextStateResponse.states ?? [];
41279
+ stateErrors = nextStateResponse.errors ?? [];
41045
41280
  clients = nextClients;
41046
41281
  renderLists();
41047
41282
  if (attached && !findShell(attached.id)) {
@@ -42275,6 +42510,17 @@ ${formatErrorDetail(error2)}`);
42275
42510
  { value: "en", label: "English" },
42276
42511
  { value: "ja", label: "日本語" }
42277
42512
  ];
42513
+ var GENERAL_SETTING_FIELDS = [
42514
+ "language",
42515
+ "sidebarFontSize",
42516
+ "codeFontSize",
42517
+ "omitDirs",
42518
+ "excludeNames",
42519
+ "watchLimit",
42520
+ "uploadEnabled",
42521
+ "inferFkRails",
42522
+ "s3TooltipEnabled"
42523
+ ];
42278
42524
  function section() {
42279
42525
  const div = document.createElement("div");
42280
42526
  div.className = "scope-settings-section";
@@ -42316,8 +42562,8 @@ ${formatErrorDetail(error2)}`);
42316
42562
  wrap.append(input2, text3);
42317
42563
  return { wrap, input: input2, text: text3 };
42318
42564
  }
42319
- function setFieldValue(field2, value) {
42320
- if (field2 === document.activeElement) return;
42565
+ function setFieldValue(field2, value, force = false) {
42566
+ if (!force && field2 === document.activeElement) return;
42321
42567
  if (field2.value !== value) field2.value = value;
42322
42568
  }
42323
42569
  function createViewerSettings(deps) {
@@ -42341,22 +42587,66 @@ ${formatErrorDetail(error2)}`);
42341
42587
  const watchLimitNumber = document.createElement("input");
42342
42588
  const watchLimitRange = document.createElement("input");
42343
42589
  const watchLimitHelp = helpText("scope-watch-limit-help");
42344
- const autosaveNote = helpText("scope-settings-autosave-note");
42590
+ const agentRules = document.createElement("textarea");
42591
+ const agentRulesHelp = helpText("agent-screen-rules-help");
42592
+ const agentRulesSource = helpText("agent-screen-rules-source");
42593
+ const agentRulesError = helpText("agent-screen-rules-error");
42594
+ agentRulesError.classList.add("scope-settings-refresh-error");
42595
+ agentRulesError.hidden = true;
42596
+ const agentRulesHighlightError = helpText(
42597
+ "agent-screen-rules-highlight-error"
42598
+ );
42599
+ agentRulesHighlightError.classList.add("scope-settings-refresh-error");
42600
+ agentRulesHighlightError.hidden = true;
42601
+ const agentRulesHighlight = document.createElement("pre");
42602
+ agentRulesHighlight.className = "agent-screen-rules-highlight";
42603
+ agentRulesHighlight.setAttribute("aria-hidden", "true");
42604
+ const agentRulesGuide = document.createElement("details");
42605
+ agentRulesGuide.id = "agent-screen-rules-guide";
42606
+ agentRulesGuide.className = "agent-screen-rules-guide";
42607
+ agentRulesGuide.open = true;
42608
+ const agentRulesGuideTitle = document.createElement("summary");
42609
+ const agentRulesGuideIntro = helpText();
42610
+ const agentRulesGuideFields = helpText();
42611
+ const agentRulesGuideMatchers = helpText();
42612
+ const agentRulesGuideRegions = helpText();
42613
+ const agentRulesGuideExample = document.createElement("pre");
42614
+ agentRulesGuideExample.className = "agent-screen-rules-guide-example";
42615
+ const agentRulesSave = document.createElement("button");
42616
+ const agentRulesReset = document.createElement("button");
42617
+ const saveNote = helpText("scope-settings-save-note");
42618
+ const saveStatus = helpText("scope-settings-save-status");
42619
+ saveStatus.setAttribute("aria-live", "polite");
42620
+ const saveError = helpText("scope-settings-save-error");
42621
+ saveError.classList.add("scope-settings-refresh-error");
42622
+ saveError.hidden = true;
42345
42623
  const refreshError = helpText("scope-settings-refresh-error");
42346
42624
  refreshError.classList.add("scope-settings-refresh-error");
42347
42625
  refreshError.hidden = true;
42348
42626
  const resetButton = document.createElement("button");
42627
+ const saveButton = document.createElement("button");
42349
42628
  const displayTitle = sectionTitle();
42350
42629
  const uploadsTitle = sectionTitle();
42351
42630
  const excludedTitle = sectionTitle();
42352
42631
  const datastoreTitle = sectionTitle();
42353
42632
  const watchTitle = sectionTitle();
42633
+ const agentRulesTitle = sectionTitle();
42354
42634
  const languageLabel = fieldLabel("viewer-language");
42355
42635
  const sidebarFontSizeLabel = fieldLabel("sidebar-font-size");
42356
42636
  const codeFontSizeLabel = fieldLabel("code-font-size");
42357
42637
  const omitDirsLabel = fieldLabel("scope-omit-dirs");
42358
42638
  const excludeNamesLabel = fieldLabel("scope-exclude-names");
42359
42639
  const watchLimitLabel = fieldLabel("scope-watch-limit");
42640
+ const agentRulesLabel = fieldLabel("agent-screen-rules");
42641
+ let generalDirty = false;
42642
+ let generalSavePending = false;
42643
+ let restoreDefaults = false;
42644
+ const changedGeneralFields = /* @__PURE__ */ new Set();
42645
+ let generalStatus = "idle";
42646
+ let agentRulesPending = false;
42647
+ let agentRulesDirty = false;
42648
+ let jsonHighlighter = null;
42649
+ let refreshGeneration = 0;
42360
42650
  function build() {
42361
42651
  const wrap = document.createElement("div");
42362
42652
  wrap.className = "scope-settings";
@@ -42424,63 +42714,318 @@ ${formatErrorDetail(error2)}`);
42424
42714
  const watch = section();
42425
42715
  watch.id = "watch-settings-section";
42426
42716
  watch.append(watchTitle, watchLimitLabel, watchRow, watchLimitHelp);
42717
+ agentRulesTitle.id = "agent-screen-rules-title";
42718
+ agentRules.id = "agent-screen-rules";
42719
+ agentRules.rows = 24;
42720
+ agentRules.spellcheck = false;
42721
+ agentRules.setAttribute(
42722
+ "aria-describedby",
42723
+ [
42724
+ "agent-screen-rules-help",
42725
+ "agent-screen-rules-source",
42726
+ "agent-screen-rules-error",
42727
+ "agent-screen-rules-highlight-error"
42728
+ ].join(" ")
42729
+ );
42730
+ const agentRulesEditor = document.createElement("div");
42731
+ agentRulesEditor.className = "agent-screen-rules-editor";
42732
+ agentRulesEditor.append(agentRulesHighlight, agentRules);
42733
+ agentRulesGuide.append(
42734
+ agentRulesGuideTitle,
42735
+ agentRulesGuideIntro,
42736
+ agentRulesGuideFields,
42737
+ agentRulesGuideMatchers,
42738
+ agentRulesGuideRegions,
42739
+ agentRulesGuideExample
42740
+ );
42741
+ agentRulesSave.type = "button";
42742
+ agentRulesSave.id = "agent-screen-rules-save";
42743
+ agentRulesReset.type = "button";
42744
+ agentRulesReset.id = "agent-screen-rules-reset";
42745
+ const agentRuleActions = document.createElement("div");
42746
+ agentRuleActions.className = "scope-settings-actions";
42747
+ agentRuleActions.append(agentRulesReset, agentRulesSave);
42748
+ const ruleSettings = section();
42749
+ ruleSettings.classList.add("agent-screen-rules-section");
42750
+ ruleSettings.append(
42751
+ agentRulesTitle,
42752
+ agentRulesLabel,
42753
+ agentRulesHelp,
42754
+ agentRulesGuide,
42755
+ agentRulesEditor,
42756
+ agentRulesSource,
42757
+ agentRulesError,
42758
+ agentRulesHighlightError,
42759
+ agentRuleActions
42760
+ );
42427
42761
  resetButton.id = "scope-omit-reset";
42428
42762
  resetButton.type = "button";
42763
+ saveButton.id = "scope-settings-save";
42764
+ saveButton.type = "button";
42765
+ saveButton.className = "scope-settings-primary-action";
42766
+ const generalActions = document.createElement("div");
42767
+ generalActions.className = "scope-settings-actions";
42768
+ generalActions.append(resetButton, saveButton);
42429
42769
  const footer = document.createElement("div");
42430
42770
  footer.className = "scope-settings-footer";
42431
- footer.append(autosaveNote, refreshError, resetButton);
42432
- wrap.append(display, uploads, excluded, datastores, watch, footer);
42771
+ footer.append(
42772
+ saveNote,
42773
+ saveStatus,
42774
+ saveError,
42775
+ refreshError,
42776
+ generalActions
42777
+ );
42778
+ wrap.append(
42779
+ display,
42780
+ uploads,
42781
+ excluded,
42782
+ datastores,
42783
+ watch,
42784
+ ruleSettings,
42785
+ footer
42786
+ );
42433
42787
  wire();
42788
+ void initializeJsonHighlighting();
42434
42789
  return wrap;
42435
42790
  }
42436
42791
  function wire() {
42437
- language.addEventListener(
42438
- "change",
42439
- () => deps.onLanguageChange(language.value)
42440
- );
42792
+ language.addEventListener("change", () => markGeneralDirty("language"));
42441
42793
  sidebarFontSize.addEventListener(
42442
42794
  "change",
42443
- () => deps.onSidebarFontSizeChange(sidebarFontSize.value)
42795
+ () => markGeneralDirty("sidebarFontSize")
42444
42796
  );
42445
42797
  codeFontSize.addEventListener(
42446
42798
  "change",
42447
- () => deps.onCodeFontSizeChange(codeFontSize.value)
42799
+ () => markGeneralDirty("codeFontSize")
42448
42800
  );
42449
42801
  upload.input.addEventListener(
42450
42802
  "change",
42451
- () => deps.onUploadEnabledChange(upload.input.checked)
42452
- );
42453
- omitDirs.addEventListener(
42454
- "change",
42455
- () => deps.onOmitDirsChange(omitDirs.value)
42456
- );
42457
- excludeNames.addEventListener(
42458
- "change",
42459
- () => deps.onExcludeNamesChange(excludeNames.value)
42803
+ () => markGeneralDirty("uploadEnabled")
42460
42804
  );
42461
42805
  inferFk.input.addEventListener(
42462
42806
  "change",
42463
- () => deps.onInferFkChange(inferFk.input.checked)
42807
+ () => markGeneralDirty("inferFkRails")
42464
42808
  );
42465
42809
  s3Tooltip.input.addEventListener(
42466
42810
  "change",
42467
- () => deps.onS3TooltipChange(s3Tooltip.input.checked)
42811
+ () => markGeneralDirty("s3TooltipEnabled")
42468
42812
  );
42813
+ omitDirs.addEventListener("input", () => markGeneralDirty("omitDirs"));
42814
+ excludeNames.addEventListener(
42815
+ "input",
42816
+ () => markGeneralDirty("excludeNames")
42817
+ );
42818
+ watchLimitNumber.addEventListener("input", () => {
42819
+ watchLimitRange.value = watchLimitNumber.value;
42820
+ markGeneralDirty("watchLimit");
42821
+ });
42469
42822
  watchLimitNumber.addEventListener("change", () => {
42470
42823
  watchLimitRange.value = watchLimitNumber.value;
42471
- deps.onWatchLimitChange(watchLimitNumber.value);
42824
+ markGeneralDirty("watchLimit");
42472
42825
  });
42473
42826
  watchLimitRange.addEventListener("input", () => {
42474
42827
  watchLimitNumber.value = watchLimitRange.value;
42828
+ markGeneralDirty("watchLimit");
42475
42829
  });
42476
42830
  watchLimitRange.addEventListener(
42477
42831
  "change",
42478
- () => deps.onWatchLimitChange(watchLimitRange.value)
42832
+ () => markGeneralDirty("watchLimit")
42479
42833
  );
42480
- resetButton.addEventListener("click", () => {
42481
- deps.onReset();
42482
- sync();
42834
+ agentRules.addEventListener("input", () => {
42835
+ agentRulesDirty = true;
42836
+ syncAgentRulesHighlight();
42837
+ });
42838
+ agentRules.addEventListener("scroll", syncAgentRulesScroll);
42839
+ agentRulesSave.addEventListener("click", () => {
42840
+ void updateAgentRules(() => deps.onAgentRulesSave(agentRules.value));
42483
42841
  });
42842
+ agentRulesReset.addEventListener("click", () => {
42843
+ void updateAgentRules(() => deps.onAgentRulesReset());
42844
+ });
42845
+ resetButton.addEventListener("click", () => {
42846
+ applyGeneralFields(deps.getDefaultValues(), true);
42847
+ restoreDefaults = true;
42848
+ changedGeneralFields.clear();
42849
+ for (const field2 of GENERAL_SETTING_FIELDS) {
42850
+ changedGeneralFields.add(field2);
42851
+ }
42852
+ generalDirty = true;
42853
+ generalStatus = "dirty";
42854
+ clearGeneralSaveError();
42855
+ renderGeneralSaveState();
42856
+ });
42857
+ saveButton.addEventListener("click", () => {
42858
+ void saveGeneralSettings();
42859
+ });
42860
+ }
42861
+ function markGeneralDirty(field2) {
42862
+ if (generalSavePending) return;
42863
+ changedGeneralFields.add(field2);
42864
+ generalDirty = true;
42865
+ restoreDefaults = false;
42866
+ generalStatus = "dirty";
42867
+ watchLimitNumber.setCustomValidity("");
42868
+ clearGeneralSaveError();
42869
+ renderGeneralSaveState();
42870
+ }
42871
+ function clearGeneralSaveError() {
42872
+ saveError.textContent = "";
42873
+ saveError.hidden = true;
42874
+ }
42875
+ function renderGeneralSaveState() {
42876
+ const text3 = deps.getText();
42877
+ saveButton.textContent = generalStatus === "saving" ? text3.saving : text3.save;
42878
+ saveButton.disabled = generalSavePending || !generalDirty;
42879
+ resetButton.disabled = generalSavePending;
42880
+ saveStatus.textContent = generalStatus === "saving" ? text3.saving : generalStatus === "saved" ? text3.saved : generalDirty ? text3.unsaved : "";
42881
+ }
42882
+ function setGeneralControlsDisabled(disabled) {
42883
+ for (const field2 of [
42884
+ language,
42885
+ sidebarFontSize,
42886
+ codeFontSize,
42887
+ upload.input,
42888
+ omitDirs,
42889
+ excludeNames,
42890
+ inferFk.input,
42891
+ s3Tooltip.input,
42892
+ watchLimitNumber,
42893
+ watchLimitRange
42894
+ ]) {
42895
+ field2.disabled = disabled;
42896
+ }
42897
+ }
42898
+ function applyGeneralFields(values, force = false) {
42899
+ setFieldValue(language, values.language, force);
42900
+ setFieldValue(sidebarFontSize, values.sidebarFontSize, force);
42901
+ setFieldValue(codeFontSize, values.codeFontSize, force);
42902
+ setFieldValue(omitDirs, values.omitDirs, force);
42903
+ setFieldValue(excludeNames, values.excludeNames, force);
42904
+ setFieldValue(watchLimitNumber, String(values.watchLimit), force);
42905
+ setFieldValue(watchLimitRange, String(values.watchLimit), force);
42906
+ upload.input.checked = values.uploadEnabled;
42907
+ inferFk.input.checked = values.inferFkRails;
42908
+ s3Tooltip.input.checked = values.s3TooltipEnabled;
42909
+ }
42910
+ function readGeneralDraft() {
42911
+ const values = deps.getValues();
42912
+ const watchLimit = Number(watchLimitNumber.value);
42913
+ if (!Number.isInteger(watchLimit) || watchLimit < values.watchLimitMin || watchLimit > values.watchLimitMax) {
42914
+ const message = deps.getText().watchLimitInvalid(values.watchLimitMin, values.watchLimitMax);
42915
+ watchLimitNumber.setCustomValidity(message);
42916
+ saveError.textContent = message;
42917
+ saveError.hidden = false;
42918
+ generalStatus = "dirty";
42919
+ renderGeneralSaveState();
42920
+ return null;
42921
+ }
42922
+ watchLimitNumber.setCustomValidity("");
42923
+ return {
42924
+ language: language.value,
42925
+ sidebarFontSize: sidebarFontSize.value,
42926
+ codeFontSize: codeFontSize.value,
42927
+ omitDirs: omitDirs.value,
42928
+ excludeNames: excludeNames.value,
42929
+ watchLimit,
42930
+ uploadEnabled: upload.input.checked,
42931
+ inferFkRails: inferFk.input.checked,
42932
+ s3TooltipEnabled: s3Tooltip.input.checked
42933
+ };
42934
+ }
42935
+ async function saveGeneralSettings() {
42936
+ if (generalSavePending || !generalDirty) return;
42937
+ const draft = readGeneralDraft();
42938
+ if (!draft) return;
42939
+ refreshGeneration += 1;
42940
+ generalSavePending = true;
42941
+ generalStatus = "saving";
42942
+ clearGeneralSaveError();
42943
+ setGeneralControlsDisabled(true);
42944
+ renderGeneralSaveState();
42945
+ let completed = false;
42946
+ try {
42947
+ await deps.onSave(draft, {
42948
+ restoreDefaults,
42949
+ changedFields: [...changedGeneralFields]
42950
+ });
42951
+ generalDirty = false;
42952
+ restoreDefaults = false;
42953
+ changedGeneralFields.clear();
42954
+ generalStatus = "saved";
42955
+ completed = true;
42956
+ } catch (error2) {
42957
+ console.error("[code-viewer] viewer settings save failed", error2);
42958
+ saveError.textContent = formatErrorDetail(error2);
42959
+ saveError.hidden = false;
42960
+ generalStatus = "dirty";
42961
+ } finally {
42962
+ generalSavePending = false;
42963
+ setGeneralControlsDisabled(false);
42964
+ if (completed) sync();
42965
+ else renderGeneralSaveState();
42966
+ }
42967
+ }
42968
+ async function initializeJsonHighlighting() {
42969
+ try {
42970
+ jsonHighlighter = await loadShikiHighlighter({
42971
+ themes: ["github-light", "github-dark"],
42972
+ langs: ["json"],
42973
+ failureMode: "throw"
42974
+ });
42975
+ if (!jsonHighlighter) {
42976
+ throw new Error("JSON syntax highlighting could not be loaded");
42977
+ }
42978
+ syncAgentRulesHighlight();
42979
+ } catch (error2) {
42980
+ console.error("[code-viewer] JSON syntax highlighting failed", error2);
42981
+ agentRulesHighlightError.textContent = formatErrorDetail(error2);
42982
+ agentRulesHighlightError.hidden = false;
42983
+ }
42984
+ }
42985
+ function syncAgentRulesHighlight() {
42986
+ const highlighted = highlightToInnerHtml(
42987
+ agentRules.value,
42988
+ "json",
42989
+ jsonHighlighter
42990
+ );
42991
+ if (highlighted) agentRulesHighlight.innerHTML = highlighted;
42992
+ else agentRulesHighlight.textContent = agentRules.value;
42993
+ syncAgentRulesScroll();
42994
+ }
42995
+ function syncAgentRulesScroll() {
42996
+ agentRulesHighlight.scrollTop = agentRules.scrollTop;
42997
+ agentRulesHighlight.scrollLeft = agentRules.scrollLeft;
42998
+ }
42999
+ async function updateAgentRules(operation) {
43000
+ if (agentRulesPending) return;
43001
+ refreshGeneration += 1;
43002
+ agentRulesPending = true;
43003
+ agentRules.disabled = true;
43004
+ agentRulesSave.disabled = true;
43005
+ agentRulesReset.disabled = true;
43006
+ agentRulesSource.textContent = deps.getText().agentRulesSaving;
43007
+ agentRulesError.hidden = true;
43008
+ agentRulesError.textContent = "";
43009
+ let completed = false;
43010
+ try {
43011
+ await operation();
43012
+ agentRulesDirty = false;
43013
+ completed = true;
43014
+ } catch (error2) {
43015
+ console.error("[code-viewer] terminal rule update failed", error2);
43016
+ agentRulesError.textContent = formatErrorDetail(error2);
43017
+ agentRulesError.hidden = false;
43018
+ } finally {
43019
+ agentRulesPending = false;
43020
+ agentRules.disabled = false;
43021
+ agentRulesSave.disabled = false;
43022
+ agentRulesReset.disabled = false;
43023
+ if (completed) {
43024
+ sync();
43025
+ } else {
43026
+ agentRulesSource.textContent = deps.getValues().agentRulesSource === "saved" ? deps.getText().agentRulesSourceSaved : deps.getText().agentRulesSourceDefault;
43027
+ }
43028
+ }
42484
43029
  }
42485
43030
  function applyText() {
42486
43031
  const text3 = deps.getText();
@@ -42490,12 +43035,14 @@ ${formatErrorDetail(error2)}`);
42490
43035
  excludedTitle.textContent = text3.excludedDirectories;
42491
43036
  datastoreTitle.textContent = text3.datastoreTitle;
42492
43037
  watchTitle.textContent = text3.watchTitle;
43038
+ agentRulesTitle.textContent = text3.agentRulesTitle;
42493
43039
  languageLabel.textContent = text3.language;
42494
43040
  sidebarFontSizeLabel.textContent = text3.fileListFontSize;
42495
43041
  codeFontSizeLabel.textContent = text3.codeFontSize;
42496
43042
  omitDirsLabel.textContent = text3.omitDirs;
42497
43043
  excludeNamesLabel.textContent = text3.excludeNames;
42498
43044
  watchLimitLabel.textContent = text3.watchLimit;
43045
+ agentRulesLabel.textContent = text3.agentRulesLabel;
42499
43046
  uiFontSizeHelp.textContent = text3.fileListFontSizeHelp;
42500
43047
  displaySource.textContent = text3.displaySource;
42501
43048
  upload.text.textContent = text3.uploadEnabledLabel;
@@ -42507,37 +43054,49 @@ ${formatErrorDetail(error2)}`);
42507
43054
  s3Tooltip.text.textContent = text3.datastoreS3TooltipLabel;
42508
43055
  s3TooltipHelp.textContent = text3.datastoreS3TooltipHelp;
42509
43056
  watchLimitHelp.textContent = text3.watchLimitHelp(values.watchLimitDefault);
42510
- autosaveNote.textContent = text3.autosaveNote;
43057
+ agentRulesHelp.textContent = text3.agentRulesHelp;
43058
+ agentRulesGuideTitle.textContent = text3.agentRulesGuideTitle;
43059
+ agentRulesGuideIntro.textContent = text3.agentRulesGuideIntro;
43060
+ agentRulesGuideFields.textContent = text3.agentRulesGuideFields;
43061
+ agentRulesGuideMatchers.textContent = text3.agentRulesGuideMatchers;
43062
+ agentRulesGuideRegions.textContent = text3.agentRulesGuideRegions;
43063
+ agentRulesGuideExample.textContent = text3.agentRulesGuideExample;
43064
+ agentRulesSave.textContent = text3.agentRulesSave;
43065
+ agentRulesReset.textContent = text3.agentRulesReset;
43066
+ saveNote.textContent = text3.saveNote;
42511
43067
  resetButton.textContent = text3.reset;
43068
+ renderGeneralSaveState();
42512
43069
  const sizeLabels = {
42513
43070
  compact: text3.sizeSmall,
42514
43071
  regular: text3.sizeRegular,
42515
43072
  large: text3.sizeLarge,
42516
43073
  xlarge: text3.sizeExtraLarge
42517
43074
  };
42518
- for (const select of [sidebarFontSize, codeFontSize])
42519
- for (const option of Array.from(select.options))
43075
+ for (const select of [sidebarFontSize, codeFontSize]) {
43076
+ for (const option of Array.from(select.options)) {
42520
43077
  option.textContent = sizeLabels[option.value] || option.value;
43078
+ }
43079
+ }
42521
43080
  }
42522
43081
  function sync() {
42523
43082
  if (!root) return;
42524
43083
  applyText();
42525
43084
  const values = deps.getValues();
42526
- setFieldValue(language, values.language);
42527
- setFieldValue(sidebarFontSize, values.sidebarFontSize);
42528
- setFieldValue(codeFontSize, values.codeFontSize);
42529
- setFieldValue(omitDirs, values.omitDirs);
42530
- setFieldValue(excludeNames, values.excludeNames);
42531
43085
  watchLimitNumber.min = String(values.watchLimitMin);
42532
43086
  watchLimitNumber.max = String(values.watchLimitMax);
42533
43087
  watchLimitRange.min = String(values.watchLimitMin);
42534
43088
  watchLimitRange.max = String(values.watchLimitMax);
42535
- setFieldValue(watchLimitNumber, String(values.watchLimit));
42536
- setFieldValue(watchLimitRange, String(values.watchLimit));
42537
- upload.input.checked = values.uploadEnabled;
42538
- inferFk.input.checked = values.inferFkRails;
42539
- s3Tooltip.input.checked = values.s3TooltipEnabled;
43089
+ if (!generalDirty && !generalSavePending) applyGeneralFields(values);
42540
43090
  scopeSource.textContent = values.scopeSource;
43091
+ if (!agentRulesDirty && !agentRulesPending) {
43092
+ setFieldValue(agentRules, values.agentRulesJson);
43093
+ syncAgentRulesHighlight();
43094
+ }
43095
+ if (!agentRulesPending) {
43096
+ agentRulesSource.textContent = values.agentRulesSource === "saved" ? deps.getText().agentRulesSourceSaved : deps.getText().agentRulesSourceDefault;
43097
+ agentRulesError.textContent = values.agentRulesErrors;
43098
+ agentRulesError.hidden = !values.agentRulesErrors;
43099
+ }
42541
43100
  }
42542
43101
  function mount(host) {
42543
43102
  if (!root) root = build();
@@ -42557,11 +43116,19 @@ ${formatErrorDetail(error2)}`);
42557
43116
  sync();
42558
43117
  refreshError.hidden = true;
42559
43118
  refreshError.textContent = "";
42560
- void deps.refresh().then(sync, (error2) => {
42561
- console.error("[code-viewer] viewer settings refresh failed", error2);
42562
- refreshError.textContent = formatErrorDetail(error2);
42563
- refreshError.hidden = false;
42564
- });
43119
+ const generation = ++refreshGeneration;
43120
+ void deps.refresh().then(
43121
+ () => {
43122
+ if (generation !== refreshGeneration) return;
43123
+ sync();
43124
+ },
43125
+ (error2) => {
43126
+ if (generation !== refreshGeneration) return;
43127
+ console.error("[code-viewer] viewer settings refresh failed", error2);
43128
+ refreshError.textContent = formatErrorDetail(error2);
43129
+ refreshError.hidden = false;
43130
+ }
43131
+ );
42565
43132
  }
42566
43133
  function localize() {
42567
43134
  if (!root) return;
@@ -42591,6 +43158,11 @@ ${formatErrorDetail(error2)}`);
42591
43158
  let PROJECT_BRANCH = "";
42592
43159
  let REPO_WEB_URL = null;
42593
43160
  let APP_SETTINGS = { version: 1 };
43161
+ let AGENT_SCREEN_RULES = formatAgentScreenRuleSet(DEFAULT_AGENT_SCREEN_RULES);
43162
+ let AGENT_SCREEN_RULES_SOURCE = "default";
43163
+ let AGENT_SCREEN_RULE_ERRORS = [];
43164
+ let AGENT_SCREEN_RULES_GENERATION = 0;
43165
+ let AGENT_SCREEN_RULE_REQUEST_GENERATION = 0;
42594
43166
  let VIEW_STATE = {
42595
43167
  version: 1,
42596
43168
  collapsedDirs: [],
@@ -42759,6 +43331,13 @@ ${formatErrorDetail(error2)}`);
42759
43331
  "X-Code-Viewer-Action": "1"
42760
43332
  };
42761
43333
  }
43334
+ function reportPersistenceError(operation, error2) {
43335
+ const failure = errorWithCause(`${operation} failed`, error2);
43336
+ console.error(failure);
43337
+ setStatus("error");
43338
+ const statusEl = document.querySelector("#status");
43339
+ if (statusEl) statusEl.title = formatErrorDetail(failure);
43340
+ }
42762
43341
  let cachedKeymapOverrides;
42763
43342
  let cachedKeyBindings = DEFAULT_KEY_BINDINGS;
42764
43343
  function activeKeyBindings() {
@@ -42768,29 +43347,95 @@ ${formatErrorDetail(error2)}`);
42768
43347
  }
42769
43348
  return cachedKeyBindings;
42770
43349
  }
42771
- function patchSettings(patch, options = {}) {
42772
- mergeLocalSettings(patch);
42773
- const body = JSON.stringify(patch);
42774
- void fetch("/_state/settings", {
42775
- method: "PATCH",
42776
- headers: actionHeaders(),
42777
- body,
42778
- keepalive: options.keepalive
42779
- }).catch(() => void 0);
43350
+ let pendingSettingsPatch = null;
43351
+ let pendingSettingsKeepalive = false;
43352
+ let settingsPatchInFlight = null;
43353
+ async function flushSettingsPatch() {
43354
+ if (settingsPatchInFlight) {
43355
+ await settingsPatchInFlight;
43356
+ if (pendingSettingsPatch) await flushSettingsPatch();
43357
+ return;
43358
+ }
43359
+ if (!pendingSettingsPatch) return;
43360
+ const patch = pendingSettingsPatch;
43361
+ const keepalive = pendingSettingsKeepalive;
43362
+ pendingSettingsPatch = null;
43363
+ pendingSettingsKeepalive = false;
43364
+ const operation = sendSettingsPatch(patch, keepalive).then(() => void 0);
43365
+ settingsPatchInFlight = operation;
43366
+ try {
43367
+ await operation;
43368
+ } catch (error2) {
43369
+ pendingSettingsPatch = {
43370
+ ...patch,
43371
+ ...pendingSettingsPatch || {}
43372
+ };
43373
+ pendingSettingsKeepalive ||= keepalive;
43374
+ throw error2;
43375
+ } finally {
43376
+ if (settingsPatchInFlight === operation) settingsPatchInFlight = null;
43377
+ }
43378
+ if (pendingSettingsPatch) await flushSettingsPatch();
42780
43379
  }
42781
- async function persistGrepSettings(patch) {
43380
+ async function sendSettingsPatch(patch, keepalive = false) {
42782
43381
  const response = await trackLoad(
42783
43382
  fetch("/_state/settings", {
42784
43383
  method: "PATCH",
42785
43384
  headers: actionHeaders(),
42786
- body: JSON.stringify(patch)
43385
+ body: JSON.stringify(patch),
43386
+ keepalive
42787
43387
  })
42788
43388
  );
42789
- if (!response.ok) throw new Error(await response.text());
42790
- APP_SETTINGS = await response.json();
43389
+ if (!response.ok) {
43390
+ throw new Error(
43391
+ await responseErrorMessage(response, "save viewer settings")
43392
+ );
43393
+ }
43394
+ try {
43395
+ return await response.json();
43396
+ } catch (error2) {
43397
+ throw errorWithCause(
43398
+ "save viewer settings: response is not valid JSON",
43399
+ error2
43400
+ );
43401
+ }
43402
+ }
43403
+ function patchSettings(patch, options = {}) {
43404
+ mergeLocalSettings(patch);
43405
+ pendingSettingsPatch = {
43406
+ ...pendingSettingsPatch || {},
43407
+ ...patch
43408
+ };
43409
+ pendingSettingsKeepalive ||= options.keepalive === true;
43410
+ if (!settingsPatchInFlight) {
43411
+ void flushSettingsPatch().catch((error2) => {
43412
+ reportPersistenceError("save viewer settings", error2);
43413
+ });
43414
+ }
43415
+ }
43416
+ async function persistSettingsPatch(patch) {
43417
+ await flushSettingsPatch();
43418
+ while (settingsPatchInFlight) await settingsPatchInFlight;
43419
+ const operation = sendSettingsPatch(patch).then((state) => {
43420
+ APP_SETTINGS = state;
43421
+ if (pendingSettingsPatch) mergeLocalSettings(pendingSettingsPatch);
43422
+ });
43423
+ settingsPatchInFlight = operation;
43424
+ try {
43425
+ await operation;
43426
+ } finally {
43427
+ if (settingsPatchInFlight === operation) settingsPatchInFlight = null;
43428
+ if (pendingSettingsPatch) {
43429
+ void flushSettingsPatch().catch((error2) => {
43430
+ reportPersistenceError("save viewer settings", error2);
43431
+ });
43432
+ }
43433
+ }
42791
43434
  }
42792
43435
  let pendingViewPatch = null;
42793
43436
  let pendingViewTimer = null;
43437
+ let pendingViewKeepalive = false;
43438
+ let viewPatchInFlight = false;
42794
43439
  function mergePathDelta(next, base2, patch, addKey, removeKey) {
42795
43440
  const added = new Set(base2?.[addKey] || []);
42796
43441
  const removed = new Set(base2?.[removeKey] || []);
@@ -42852,19 +43497,44 @@ ${formatErrorDetail(error2)}`);
42852
43497
  viewedFiles: [...viewedFiles]
42853
43498
  };
42854
43499
  }
43500
+ async function sendPendingViewPatch() {
43501
+ if (viewPatchInFlight || !pendingViewPatch) return;
43502
+ const patch = pendingViewPatch;
43503
+ const keepalive = pendingViewKeepalive;
43504
+ pendingViewPatch = null;
43505
+ pendingViewKeepalive = false;
43506
+ viewPatchInFlight = true;
43507
+ let saved = false;
43508
+ try {
43509
+ const response = await trackLoad(
43510
+ fetch("/_state/view", {
43511
+ method: "PATCH",
43512
+ headers: actionHeaders(),
43513
+ body: JSON.stringify(patch),
43514
+ keepalive
43515
+ })
43516
+ );
43517
+ if (!response.ok) {
43518
+ throw new Error(
43519
+ await responseErrorMessage(response, "save viewer state")
43520
+ );
43521
+ }
43522
+ saved = true;
43523
+ } catch (error2) {
43524
+ pendingViewPatch = mergeViewPatch(patch, pendingViewPatch || {});
43525
+ reportPersistenceError("save viewer state", error2);
43526
+ } finally {
43527
+ viewPatchInFlight = false;
43528
+ }
43529
+ if (saved && pendingViewPatch) void sendPendingViewPatch();
43530
+ }
42855
43531
  function patchViewState(patch, options = {}) {
42856
43532
  VIEW_STATE = mergeLocalViewState(VIEW_STATE, patch);
42857
43533
  pendingViewPatch = mergeViewPatch(pendingViewPatch, patch);
42858
43534
  const send = (keepalive = false) => {
42859
43535
  if (!pendingViewPatch) return;
42860
- const body = JSON.stringify(pendingViewPatch);
42861
- pendingViewPatch = null;
42862
- void fetch("/_state/view", {
42863
- method: "PATCH",
42864
- headers: actionHeaders(),
42865
- body,
42866
- keepalive
42867
- }).catch(() => void 0);
43536
+ pendingViewKeepalive ||= keepalive;
43537
+ void sendPendingViewPatch();
42868
43538
  };
42869
43539
  if (options.keepalive) {
42870
43540
  if (pendingViewTimer !== null) clearTimeout(pendingViewTimer);
@@ -42888,14 +43558,8 @@ ${formatErrorDetail(error2)}`);
42888
43558
  if (!pendingViewPatch) return;
42889
43559
  if (pendingViewTimer !== null) clearTimeout(pendingViewTimer);
42890
43560
  pendingViewTimer = null;
42891
- const body = JSON.stringify(pendingViewPatch);
42892
- pendingViewPatch = null;
42893
- void fetch("/_state/view", {
42894
- method: "PATCH",
42895
- headers: actionHeaders(),
42896
- body,
42897
- keepalive
42898
- }).catch(() => void 0);
43561
+ pendingViewKeepalive ||= keepalive;
43562
+ void sendPendingViewPatch();
42899
43563
  }
42900
43564
  function savedScopeOmitDirs() {
42901
43565
  return APP_SETTINGS.scopeOmitDirs ? normalizeScopeOmitDirs(APP_SETTINGS.scopeOmitDirs) : null;
@@ -42975,50 +43639,152 @@ ${formatErrorDetail(error2)}`);
42975
43639
  return `${ref}\0${omit ? omit.join("\0") : "server"}\0${exclude ? exclude.join("\0") : "server"}`;
42976
43640
  }
42977
43641
  async function loadSettings() {
43642
+ const res = await trackLoad(fetch("/_settings"));
43643
+ if (!res.ok) {
43644
+ throw new Error(
43645
+ await responseErrorMessage(res, "settings request failed")
43646
+ );
43647
+ }
43648
+ let settings;
42978
43649
  try {
42979
- const res = await fetch("/_settings");
42980
- if (!res.ok) return null;
42981
- const settings = await res.json();
42982
- setProjectName(settings.project || "");
42983
- setProjectBranch(settings.branch || "");
42984
- REPO_WEB_URL = settings.repo_web_url;
42985
- const repoLink = document.querySelector("#repo-web-link");
42986
- if (repoLink) {
42987
- repoLink.href = settings.repo_web_url || "#";
42988
- repoLink.hidden = !settings.repo_web_url;
42989
- }
42990
- SERVER_SCOPE_OMIT_DIRS_DEFAULT = normalizeScopeOmitDirs(
42991
- settings.scope.omit_dirs_effective
43650
+ settings = await res.json();
43651
+ } catch (error2) {
43652
+ throw errorWithCause("settings response is not valid JSON", error2);
43653
+ }
43654
+ setProjectName(settings.project || "");
43655
+ setProjectBranch(settings.branch || "");
43656
+ REPO_WEB_URL = settings.repo_web_url;
43657
+ const repoLink = document.querySelector("#repo-web-link");
43658
+ if (repoLink) {
43659
+ repoLink.href = settings.repo_web_url || "#";
43660
+ repoLink.hidden = !settings.repo_web_url;
43661
+ }
43662
+ SERVER_SCOPE_OMIT_DIRS_DEFAULT = normalizeScopeOmitDirs(
43663
+ settings.scope.omit_dirs_effective
43664
+ );
43665
+ SERVER_SCOPE_EXCLUDE_NAMES_DEFAULT = normalizeScopeExcludeNames(
43666
+ settings.scope.exclude_names_effective
43667
+ );
43668
+ if (typeof settings.scope.watch_limit_default === "number")
43669
+ SERVER_SCOPE_WATCH_LIMIT_DEFAULT = settings.scope.watch_limit_default;
43670
+ if (typeof settings.scope.watch_limit_min === "number")
43671
+ SERVER_SCOPE_WATCH_LIMIT_MIN = settings.scope.watch_limit_min;
43672
+ if (typeof settings.scope.watch_limit_max === "number")
43673
+ SERVER_SCOPE_WATCH_LIMIT_MAX = settings.scope.watch_limit_max;
43674
+ if (typeof settings.scope.watch_recursive === "boolean") {
43675
+ const watchSection = document.querySelector(
43676
+ "#watch-settings-section"
42992
43677
  );
42993
- SERVER_SCOPE_EXCLUDE_NAMES_DEFAULT = normalizeScopeExcludeNames(
42994
- settings.scope.exclude_names_effective
43678
+ if (watchSection) watchSection.hidden = settings.scope.watch_recursive;
43679
+ }
43680
+ return settings;
43681
+ }
43682
+ function agentScreenRuleErrorsText(errors2) {
43683
+ return errors2.map(
43684
+ (error2) => `${error2.path} [${error2.code}] ${error2.message}${error2.stack ? `
43685
+ ${error2.stack}` : ""}`
43686
+ ).join("\n\n");
43687
+ }
43688
+ async function agentScreenRuleResponse(response) {
43689
+ let body;
43690
+ try {
43691
+ body = await response.json();
43692
+ } catch (error2) {
43693
+ throw errorWithCause(
43694
+ `terminal rule request returned ${response.status} with invalid JSON`,
43695
+ error2
42995
43696
  );
42996
- if (typeof settings.scope.watch_limit_default === "number")
42997
- SERVER_SCOPE_WATCH_LIMIT_DEFAULT = settings.scope.watch_limit_default;
42998
- if (typeof settings.scope.watch_limit_min === "number")
42999
- SERVER_SCOPE_WATCH_LIMIT_MIN = settings.scope.watch_limit_min;
43000
- if (typeof settings.scope.watch_limit_max === "number")
43001
- SERVER_SCOPE_WATCH_LIMIT_MAX = settings.scope.watch_limit_max;
43002
- if (typeof settings.scope.watch_recursive === "boolean") {
43003
- const watchSection = document.querySelector(
43004
- "#watch-settings-section"
43005
- );
43006
- if (watchSection) watchSection.hidden = settings.scope.watch_recursive;
43007
- }
43008
- return settings;
43009
- } catch {
43010
- return null;
43697
+ }
43698
+ if (!response.ok) {
43699
+ throw Object.assign(
43700
+ new Error(
43701
+ `terminal rule request failed with status ${response.status}`
43702
+ ),
43703
+ { status: response.status, response: body }
43704
+ );
43705
+ }
43706
+ if (!body || typeof body !== "object" || !("rules" in body)) {
43707
+ throw Object.assign(new Error("terminal rule response is incomplete"), {
43708
+ response: body
43709
+ });
43710
+ }
43711
+ if (!("generation" in body) || typeof body.generation !== "number") {
43712
+ throw Object.assign(
43713
+ new Error("terminal rule response has no generation"),
43714
+ { response: body }
43715
+ );
43716
+ }
43717
+ return body;
43718
+ }
43719
+ function applyAgentScreenRuleResponse(response) {
43720
+ if (response.generation < AGENT_SCREEN_RULES_GENERATION) return;
43721
+ AGENT_SCREEN_RULES = formatAgentScreenRuleSet(response.rules);
43722
+ AGENT_SCREEN_RULES_SOURCE = response.source;
43723
+ AGENT_SCREEN_RULE_ERRORS = response.errors;
43724
+ AGENT_SCREEN_RULES_GENERATION = response.generation;
43725
+ }
43726
+ async function loadAgentScreenRules() {
43727
+ const generation = ++AGENT_SCREEN_RULE_REQUEST_GENERATION;
43728
+ const response = await agentScreenRuleResponse(
43729
+ await trackLoad(fetch("/_agent/rules"))
43730
+ );
43731
+ if (generation !== AGENT_SCREEN_RULE_REQUEST_GENERATION) return;
43732
+ applyAgentScreenRuleResponse(response);
43733
+ }
43734
+ async function saveAgentScreenRules(value) {
43735
+ let rules;
43736
+ try {
43737
+ rules = JSON.parse(value);
43738
+ } catch (error2) {
43739
+ throw errorWithCause("terminal rules are not valid JSON", error2);
43740
+ }
43741
+ const generation = ++AGENT_SCREEN_RULE_REQUEST_GENERATION;
43742
+ const response = await agentScreenRuleResponse(
43743
+ await trackLoad(
43744
+ fetch("/_agent/rules", {
43745
+ method: "PUT",
43746
+ headers: actionHeaders(),
43747
+ body: JSON.stringify(rules)
43748
+ })
43749
+ )
43750
+ );
43751
+ if (generation !== AGENT_SCREEN_RULE_REQUEST_GENERATION) return;
43752
+ applyAgentScreenRuleResponse(response);
43753
+ }
43754
+ async function resetAgentScreenRuleSettings() {
43755
+ const generation = ++AGENT_SCREEN_RULE_REQUEST_GENERATION;
43756
+ const response = await agentScreenRuleResponse(
43757
+ await trackLoad(
43758
+ fetch("/_agent/rules", {
43759
+ method: "DELETE",
43760
+ headers: actionHeaders()
43761
+ })
43762
+ )
43763
+ );
43764
+ if (generation !== AGENT_SCREEN_RULE_REQUEST_GENERATION) return;
43765
+ applyAgentScreenRuleResponse(response);
43766
+ }
43767
+ async function loadStateResponse(url, operation) {
43768
+ const response = await trackLoad(fetch(url));
43769
+ if (!response.ok) {
43770
+ throw new Error(await responseErrorMessage(response, operation));
43771
+ }
43772
+ try {
43773
+ return await response.json();
43774
+ } catch (error2) {
43775
+ throw errorWithCause(`${operation}: response is not valid JSON`, error2);
43011
43776
  }
43012
43777
  }
43013
43778
  async function loadPersistedState() {
43014
43779
  const [settings, view] = await Promise.all([
43015
- fetch("/_state/settings").then(
43016
- (res) => res.ok ? res.json() : null
43017
- ).catch(() => null),
43018
- fetch("/_state/view").then((res) => res.ok ? res.json() : null).catch(() => null)
43780
+ loadStateResponse(
43781
+ "/_state/settings",
43782
+ "settings state request failed"
43783
+ ),
43784
+ loadStateResponse("/_state/view", "view state request failed")
43019
43785
  ]);
43020
- if (settings) APP_SETTINGS = settings;
43021
- if (view) VIEW_STATE = view;
43786
+ APP_SETTINGS = settings;
43787
+ VIEW_STATE = view;
43022
43788
  }
43023
43789
  function routeFromLocation() {
43024
43790
  const savedLanguage = viewerLanguageFromSearch(window.location.search) || savedViewerLanguage();
@@ -43428,7 +44194,7 @@ ${formatErrorDetail(error2)}`);
43428
44194
  getGrepGroupByFile: () => APP_SETTINGS.grepGroupByFile === true,
43429
44195
  getGrepPaletteWidth: () => APP_SETTINGS.grepPaletteWidth,
43430
44196
  getGrepPaletteHeight: () => APP_SETTINGS.grepPaletteHeight,
43431
- persistGrepSettings,
44197
+ persistGrepSettings: persistSettingsPatch,
43432
44198
  applyGrepHideTests: (hidden) => {
43433
44199
  STATE.hideTests = hidden;
43434
44200
  applyHideTests();
@@ -43714,7 +44480,12 @@ ${formatErrorDetail(error2)}`);
43714
44480
  excludeNames: "Hide these file or directory names completely",
43715
44481
  excludeNamesHelp: "Removes matching files or directories from the sidebar, search, and grep results entirely. Unlike Skip, the names themselves disappear from the UI. Supports gitignore-style wildcards (*, ?, [abc], [!abc]).",
43716
44482
  reset: "Restore defaults",
43717
- autosaveNote: "Changes save automatically.",
44483
+ save: "Save changes",
44484
+ saving: "Saving…",
44485
+ saved: "Saved.",
44486
+ unsaved: "Unsaved changes.",
44487
+ saveNote: "Edits are not applied until you select Save changes.",
44488
+ watchLimitInvalid: (min, max) => `Enter a whole number from ${min} to ${max}.`,
43718
44489
  scopeSource: (project, source) => `Saved for project "${project}" in this browser. Source: ${source}. Used by the sidebar, Ctrl+K, Ctrl+G, Datastores, and the file change watcher. Restore defaults removes the browser override.`,
43719
44490
  browserOverride: "Browser override",
43720
44491
  serverDefault: "Server default",
@@ -43728,7 +44499,34 @@ ${formatErrorDetail(error2)}`);
43728
44499
  datastoreS3TooltipHelp: "Hovering an S3 object row shows the full key path and a content preview.",
43729
44500
  watchTitle: "File change watcher",
43730
44501
  watchLimit: "Maximum directories to watch",
43731
- watchLimitHelp: (defaultLimit) => `Higher values reduce missed updates in deep trees at the cost of file handles. Combine with the Skip list above to keep heavy folders (node_modules, .git, dist...) out of the watch budget. Default: ${defaultLimit}.`
44502
+ watchLimitHelp: (defaultLimit) => `Higher values reduce missed updates in deep trees at the cost of file handles. Combine with the Skip list above to keep heavy folders (node_modules, .git, dist...) out of the watch budget. Default: ${defaultLimit}.`,
44503
+ agentRulesTitle: "Terminal status detection",
44504
+ agentRulesLabel: "Screen matching rules (JSON)",
44505
+ agentRulesHelp: "Rules can report working, waiting, idle, or skip. Configure priority, region, contains, regex, lineRegex, and nested all/any/not conditions. contains ignores letter case; regex accepts a leading (?i) for case-insensitive matching. To keep matching responsive, regex allows at most one variable-length repetition and rejects groups, alternation, and backreferences; express AND/OR with all/any. The highest-priority match wins; equal priorities keep the earlier rule. Save validates every rule before replacing the active set.",
44506
+ agentRulesGuideTitle: "JSON format and example",
44507
+ agentRulesGuideIntro: "Enter one object with version 1 and a rules array. Each rule needs the required fields listed below plus at least one matcher.",
44508
+ agentRulesGuideFields: "Required fields: id (unique name), state (working, waiting, idle, or skip), priority (higher wins), and region. lines is also required when region is bottom_non_empty.",
44509
+ agentRulesGuideMatchers: "Matchers: contains and regex test the selected region; lineRegex tests each line. Combine matcher objects with all, any, and not.",
44510
+ agentRulesGuideRegions: "Regions: osc_title checks the terminal title, whole_recent checks the recent screen, bottom_non_empty checks the last non-empty lines, and last_non_empty checks only the final non-empty line.",
44511
+ agentRulesGuideExample: `{
44512
+ "version": 1,
44513
+ "rules": [
44514
+ {
44515
+ "id": "waiting_for_confirmation",
44516
+ "state": "waiting",
44517
+ "priority": 900,
44518
+ "region": "bottom_non_empty",
44519
+ "lines": 12,
44520
+ "contains": ["enter to confirm"],
44521
+ "not": [{ "contains": ["finished"] }]
44522
+ }
44523
+ ]
44524
+ }`,
44525
+ agentRulesSave: "Validate and save",
44526
+ agentRulesReset: "Use built-in rules",
44527
+ agentRulesSaving: "Validating and saving…",
44528
+ agentRulesSourceDefault: "Source: built-in rules",
44529
+ agentRulesSourceSaved: "Source: saved rules (active immediately)"
43732
44530
  },
43733
44531
  annotations: {
43734
44532
  title: "Code annotations",
@@ -44017,7 +44815,12 @@ ${formatErrorDetail(error2)}`);
44017
44815
  excludeNames: "完全に非表示にするファイル名またはディレクトリ名",
44018
44816
  excludeNamesHelp: "リスト中の名前に一致するファイル/ディレクトリを、サイドバー・検索結果・grep 結果から完全に消します。Skip と違い、名前自体が UI に出なくなります。gitignore方式のワイルドカード(*, ?, [abc], [!abc])に対応しています。",
44019
44817
  reset: "デフォルトに戻す",
44020
- autosaveNote: "変更は自動で保存されます。",
44818
+ save: "変更を保存",
44819
+ saving: "保存しています…",
44820
+ saved: "保存しました。",
44821
+ unsaved: "未保存の変更があります。",
44822
+ saveNote: "「変更を保存」を押すまで、編集内容は適用されません。",
44823
+ watchLimitInvalid: (min, max) => `${min}〜${max}の整数を入力してください。`,
44021
44824
  scopeSource: (project, source) => `このブラウザのプロジェクト "${project}" に保存されます。ソース: ${source}。サイドバー、Ctrl+K、Ctrl+G、Datastores、File change watcher で使われます。「デフォルトに戻す」でブラウザ側の上書きを削除します。`,
44022
44825
  browserOverride: "ブラウザ側の上書き",
44023
44826
  serverDefault: "サーバ既定値",
@@ -44031,7 +44834,34 @@ ${formatErrorDetail(error2)}`);
44031
44834
  datastoreS3TooltipHelp: "S3 オブジェクト行にホバーすると、完全な key とコンテンツプレビューを表示します。",
44032
44835
  watchTitle: "ファイル変更の監視",
44033
44836
  watchLimit: "監視するディレクトリ数の上限",
44034
- watchLimitHelp: (defaultLimit) => `値を大きくすると深いツリーの変更を取りこぼしにくくなりますが、ファイルハンドル数を消費します。上の Skip リストと併用すると、重いフォルダ(node_modules, .git, dist など)を監視枠から外せます。既定値: ${defaultLimit}。`
44837
+ watchLimitHelp: (defaultLimit) => `値を大きくすると深いツリーの変更を取りこぼしにくくなりますが、ファイルハンドル数を消費します。上の Skip リストと併用すると、重いフォルダ(node_modules, .git, dist など)を監視枠から外せます。既定値: ${defaultLimit}。`,
44838
+ agentRulesTitle: "ターミナルのAI状態判定",
44839
+ agentRulesLabel: "画面の一致ルール(JSON)",
44840
+ agentRulesHelp: "各ルールで working(作業中)・waiting(入力待ち)・idle(待機中)・skip(状態を維持)を指定できます。priority、region、contains、regex、lineRegex、入れ子の all/any/not を編集できます。contains は大文字小文字を区別せず、regex は先頭の (?i) による大小無視に対応します。判定処理を止めないため、regex の可変長の繰返しは1個までで、グループ・選択・後方参照は使えません。AND/OR は all/any で表します。優先度が最大の一致が採用され、同点は上にあるルールが優先されます。保存前に全ルールを検証します。",
44841
+ agentRulesGuideTitle: "JSONの書式と入力例",
44842
+ agentRulesGuideIntro: "version が 1、rules が配列のJSONオブジェクトを入力します。各ルールには下記の必須項目と、1個以上の一致条件が必要です。",
44843
+ agentRulesGuideFields: "必須項目: id(一意の名前)、state(working / waiting / idle / skip)、priority(大きい値を優先)、region。region が bottom_non_empty の場合は lines も必要です。",
44844
+ agentRulesGuideMatchers: "一致条件: contains と regex は選択した領域全体、lineRegex は各行を調べます。一致条件のオブジェクトは all / any / not で組み合わせられます。",
44845
+ agentRulesGuideRegions: "region: osc_title はターミナルタイトル、whole_recent は直近の画面全体、bottom_non_empty は末尾の非空行、last_non_empty は最後の非空行だけを調べます。",
44846
+ agentRulesGuideExample: `{
44847
+ "version": 1,
44848
+ "rules": [
44849
+ {
44850
+ "id": "waiting_for_confirmation",
44851
+ "state": "waiting",
44852
+ "priority": 900,
44853
+ "region": "bottom_non_empty",
44854
+ "lines": 12,
44855
+ "contains": ["enter to confirm"],
44856
+ "not": [{ "contains": ["finished"] }]
44857
+ }
44858
+ ]
44859
+ }`,
44860
+ agentRulesSave: "検証して保存",
44861
+ agentRulesReset: "組み込みルールに戻す",
44862
+ agentRulesSaving: "検証して保存しています…",
44863
+ agentRulesSourceDefault: "適用中: 組み込みルール",
44864
+ agentRulesSourceSaved: "適用中: 保存したルール(即時反映)"
44035
44865
  },
44036
44866
  annotations: {
44037
44867
  title: "コード注釈",
@@ -44410,12 +45240,6 @@ ${formatErrorDetail(error2)}`);
44410
45240
  const target = sourceTargetFromRoute();
44411
45241
  if (target) renderRepoBlobSidebar(target.path, target.ref || "worktree");
44412
45242
  }
44413
- function saveSidebarFontSize(value) {
44414
- const next = normalizeViewerFontSize(value);
44415
- mergeLocalSettings({ sidebarFontSize: next });
44416
- applySidebarFontSize();
44417
- patchSettings({ sidebarFontSize: next });
44418
- }
44419
45243
  const CODE_FONT_STEPS = [
44420
45244
  "compact",
44421
45245
  "regular",
@@ -44444,22 +45268,6 @@ ${formatErrorDetail(error2)}`);
44444
45268
  applyCodeFontSize();
44445
45269
  patchSettings({ codeFontSize: next });
44446
45270
  }
44447
- function saveUploadEnabled(checked) {
44448
- mergeLocalSettings({ uploadEnabled: checked });
44449
- patchSettings({ uploadEnabled: checked });
44450
- }
44451
- function saveScopeOmitDirsField(value) {
44452
- const next = normalizeScopeOmitDirs(value);
44453
- mergeLocalSettings({ scopeOmitDirs: next });
44454
- patchSettings({ scopeOmitDirs: next });
44455
- refreshRepositoryTreeAfterSettings();
44456
- }
44457
- function saveScopeExcludeNamesField(value) {
44458
- const next = normalizeScopeExcludeNames(value);
44459
- mergeLocalSettings({ scopeExcludeNames: next });
44460
- patchSettings({ scopeExcludeNames: next });
44461
- refreshRepositoryTreeAfterSettings();
44462
- }
44463
45271
  function normalizeScopeWatchLimit(value) {
44464
45272
  const parsed = typeof value === "number" ? value : typeof value === "string" ? Number(value) : Number.NaN;
44465
45273
  if (!Number.isFinite(parsed)) return null;
@@ -44474,34 +45282,94 @@ ${formatErrorDetail(error2)}`);
44474
45282
  const saved = normalizeScopeWatchLimit(APP_SETTINGS.scopeWatchLimit);
44475
45283
  return saved ?? SERVER_SCOPE_WATCH_LIMIT_DEFAULT;
44476
45284
  }
44477
- function saveScopeWatchLimitField(value) {
44478
- const next = normalizeScopeWatchLimit(value);
44479
- const resolved = next ?? SERVER_SCOPE_WATCH_LIMIT_DEFAULT;
44480
- mergeLocalSettings({ scopeWatchLimit: resolved });
44481
- patchSettings({ scopeWatchLimit: resolved });
44482
- }
44483
- function resetScopeSettings() {
44484
- setViewerLanguage("en", false);
44485
- mergeLocalSettings({
44486
- sidebarFontSize: null,
44487
- codeFontSize: null,
44488
- scopeOmitDirs: null,
44489
- scopeExcludeNames: null,
44490
- scopeWatchLimit: null,
44491
- uploadEnabled: null
44492
- });
44493
- applySidebarFontSize("regular");
44494
- applyCodeFontSize("regular");
44495
- patchSettings({
44496
- language: STATE.language,
44497
- sidebarFontSize: null,
44498
- codeFontSize: null,
44499
- scopeOmitDirs: null,
44500
- scopeExcludeNames: null,
44501
- scopeWatchLimit: null,
44502
- uploadEnabled: null
44503
- });
44504
- refreshRepositoryTreeAfterSettings();
45285
+ function defaultViewerSettingsDraft() {
45286
+ return {
45287
+ language: "en",
45288
+ sidebarFontSize: "regular",
45289
+ codeFontSize: "regular",
45290
+ omitDirs: serverScopeOmitDirsDefault().join("\n"),
45291
+ excludeNames: serverScopeExcludeNamesDefault().join("\n"),
45292
+ watchLimit: SERVER_SCOPE_WATCH_LIMIT_DEFAULT,
45293
+ uploadEnabled: true,
45294
+ inferFkRails: false,
45295
+ s3TooltipEnabled: true
45296
+ };
45297
+ }
45298
+ async function saveViewerSettings(draft, options) {
45299
+ const normalizedLanguage = normalizeViewerLanguage(draft.language);
45300
+ const normalizedSidebarFontSize = normalizeViewerFontSize(
45301
+ draft.sidebarFontSize
45302
+ );
45303
+ const normalizedCodeFontSize = normalizeViewerFontSize(draft.codeFontSize);
45304
+ const normalizedOmitDirs = normalizeScopeOmitDirs(draft.omitDirs);
45305
+ const normalizedExcludeNames = normalizeScopeExcludeNames(
45306
+ draft.excludeNames
45307
+ );
45308
+ const normalized = {
45309
+ language: normalizedLanguage,
45310
+ sidebarFontSize: normalizedSidebarFontSize,
45311
+ codeFontSize: normalizedCodeFontSize,
45312
+ omitDirs: normalizedOmitDirs.join("\n"),
45313
+ excludeNames: normalizedExcludeNames.join("\n"),
45314
+ watchLimit: normalizeScopeWatchLimit(draft.watchLimit) ?? SERVER_SCOPE_WATCH_LIMIT_DEFAULT,
45315
+ uploadEnabled: draft.uploadEnabled,
45316
+ inferFkRails: draft.inferFkRails,
45317
+ s3TooltipEnabled: draft.s3TooltipEnabled
45318
+ };
45319
+ const changed = new Set(options.changedFields);
45320
+ const appPatch = {};
45321
+ const dbPrefsPatch = {};
45322
+ if (options.restoreDefaults) {
45323
+ Object.assign(appPatch, {
45324
+ language: "en",
45325
+ sidebarFontSize: null,
45326
+ codeFontSize: null,
45327
+ scopeOmitDirs: null,
45328
+ scopeExcludeNames: null,
45329
+ scopeWatchLimit: null,
45330
+ uploadEnabled: null
45331
+ });
45332
+ dbPrefsPatch.inferFkRails = null;
45333
+ dbPrefsPatch.s3TooltipEnabled = null;
45334
+ } else {
45335
+ if (changed.has("language")) appPatch.language = normalizedLanguage;
45336
+ if (changed.has("sidebarFontSize"))
45337
+ appPatch.sidebarFontSize = normalizedSidebarFontSize;
45338
+ if (changed.has("codeFontSize"))
45339
+ appPatch.codeFontSize = normalizedCodeFontSize;
45340
+ if (changed.has("omitDirs")) appPatch.scopeOmitDirs = normalizedOmitDirs;
45341
+ if (changed.has("excludeNames"))
45342
+ appPatch.scopeExcludeNames = normalizedExcludeNames;
45343
+ if (changed.has("watchLimit"))
45344
+ appPatch.scopeWatchLimit = normalized.watchLimit;
45345
+ if (changed.has("uploadEnabled"))
45346
+ appPatch.uploadEnabled = normalized.uploadEnabled;
45347
+ if (changed.has("inferFkRails"))
45348
+ dbPrefsPatch.inferFkRails = normalized.inferFkRails;
45349
+ if (changed.has("s3TooltipEnabled"))
45350
+ dbPrefsPatch.s3TooltipEnabled = normalized.s3TooltipEnabled;
45351
+ }
45352
+ const operations = [];
45353
+ if (Object.keys(appPatch).length > 0)
45354
+ operations.push(persistSettingsPatch(appPatch));
45355
+ if (Object.keys(dbPrefsPatch).length > 0)
45356
+ operations.push(DATABASE_VIEW.saveDbUiPrefs(dbPrefsPatch));
45357
+ const results = await Promise.allSettled(operations);
45358
+ const errors2 = results.flatMap(
45359
+ (result) => result.status === "rejected" ? [result.reason] : []
45360
+ );
45361
+ if (errors2.length > 0) {
45362
+ throw errorWithCauses("save viewer settings failed", errors2);
45363
+ }
45364
+ if (options.restoreDefaults || changed.has("language"))
45365
+ setViewerLanguage(normalizedLanguage, false);
45366
+ if (options.restoreDefaults || changed.has("sidebarFontSize"))
45367
+ applySidebarFontSize();
45368
+ if (options.restoreDefaults || changed.has("codeFontSize"))
45369
+ applyCodeFontSize();
45370
+ if (options.restoreDefaults || changed.has("omitDirs") || changed.has("excludeNames")) {
45371
+ refreshRepositoryTreeAfterSettings();
45372
+ }
44505
45373
  }
44506
45374
  function createRefSelectorInput(options) {
44507
45375
  const wrap = document.createElement("div");
@@ -45013,30 +45881,22 @@ ${formatErrorDetail(error2)}`);
45013
45881
  scopeSource: uiText().settings.scopeSource(
45014
45882
  PROJECT_NAME || "default",
45015
45883
  scopeOmitSourceLabel()
45016
- )
45884
+ ),
45885
+ agentRulesJson: AGENT_SCREEN_RULES,
45886
+ agentRulesSource: AGENT_SCREEN_RULES_SOURCE,
45887
+ agentRulesErrors: agentScreenRuleErrorsText(AGENT_SCREEN_RULE_ERRORS)
45017
45888
  }),
45889
+ getDefaultValues: defaultViewerSettingsDraft,
45018
45890
  refresh: async () => {
45019
- await loadSettings();
45020
- },
45021
- onLanguageChange: (value) => {
45022
- setViewerLanguage(normalizeViewerLanguage(value));
45023
- VIEWER_SETTINGS.sync();
45024
- },
45025
- onSidebarFontSizeChange: saveSidebarFontSize,
45026
- onCodeFontSizeChange: saveCodeFontSize,
45027
- onUploadEnabledChange: saveUploadEnabled,
45028
- onOmitDirsChange: (value) => {
45029
- saveScopeOmitDirsField(value);
45030
- VIEWER_SETTINGS.sync();
45031
- },
45032
- onExcludeNamesChange: (value) => {
45033
- saveScopeExcludeNamesField(value);
45034
- VIEWER_SETTINGS.sync();
45891
+ await Promise.all([
45892
+ loadSettings(),
45893
+ loadAgentScreenRules(),
45894
+ DATABASE_VIEW.loadDbUiPrefs()
45895
+ ]);
45035
45896
  },
45036
- onWatchLimitChange: saveScopeWatchLimitField,
45037
- onInferFkChange: (checked) => DATABASE_VIEW.setDbUiPref("inferFkRails", checked),
45038
- onS3TooltipChange: (checked) => DATABASE_VIEW.setDbUiPref("s3TooltipEnabled", checked),
45039
- onReset: resetScopeSettings
45897
+ onSave: saveViewerSettings,
45898
+ onAgentRulesSave: saveAgentScreenRules,
45899
+ onAgentRulesReset: resetAgentScreenRuleSettings
45040
45900
  });
45041
45901
  relocalizeViewerSettings = () => VIEWER_SETTINGS.localize();
45042
45902
  const KEYBINDING_EDITOR = createHelpKeybindingEditor({