@youtyan/code-viewer 0.9.1 → 0.10.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
@@ -247,6 +247,22 @@ ${lines.join(`
247
247
  };
248
248
  }
249
249
 
250
+ // web-src/core/changed-paths.ts
251
+ function changedPathCoversPath(changed, path) {
252
+ return path === changed || path.startsWith(`${changed}/`);
253
+ }
254
+ function changedPathsCoverPath(changedPaths, path) {
255
+ if (!changedPaths)
256
+ return true;
257
+ if (changedPaths.has(path))
258
+ return true;
259
+ for (const changed of changedPaths) {
260
+ if (changedPathCoversPath(changed, path))
261
+ return true;
262
+ }
263
+ return false;
264
+ }
265
+
250
266
  // web-src/core/expand-logic.ts
251
267
  function initExpandState(prevHunkEndNew, hunkNewStart) {
252
268
  return {
@@ -558,7 +574,6 @@ ${lines.join(`
558
574
  var MOON_16_PATH = "M9.598 1.591a.749.749 0 0 1 .785-.175 7.001 7.001 0 1 1-8.967 8.967.75.75 0 0 1 .961-.96 5.5 5.5 0 0 0 7.046-7.046.75.75 0 0 1 .175-.786Zm1.616 1.945a7 7 0 0 1-7.678 7.678 5.499 5.499 0 1 0 7.678-7.678Z";
559
575
  var QUESTION_16_PATH = "M0 8a8 8 0 1 1 16 0A8 8 0 0 1 0 8Zm8-6.5a6.5 6.5 0 1 0 0 13 6.5 6.5 0 0 0 0-13ZM6.92 6.085h.001a.749.749 0 1 1-1.342-.67c.169-.339.436-.701.849-.977C6.845 4.16 7.369 4 8 4a2.756 2.756 0 0 1 1.637.525c.503.377.863.965.863 1.725 0 .448-.115.83-.329 1.15-.205.307-.47.513-.692.662-.109.072-.22.138-.313.195l-.006.004a6.24 6.24 0 0 0-.26.16.952.952 0 0 0-.276.245.75.75 0 0 1-1.248-.832c.184-.264.42-.489.692-.661.103-.067.207-.132.313-.195l.007-.004c.1-.061.182-.11.258-.161a.969.969 0 0 0 .277-.245C8.96 6.514 9 6.427 9 6.25a.612.612 0 0 0-.262-.525A1.27 1.27 0 0 0 8 5.5c-.369 0-.595.09-.74.187a1.01 1.01 0 0 0-.34.398ZM9 11a1 1 0 1 1-2 0 1 1 0 0 1 2 0Z";
560
576
  var MARK_GITHUB_16_PATH = "M6.766 11.328c-2.063-.25-3.516-1.734-3.516-3.656 0-.781.281-1.625.75-2.188-.203-.515-.172-1.609.063-2.062.625-.078 1.468.25 1.968.703.594-.187 1.219-.281 1.985-.281.765 0 1.39.094 1.953.265.484-.437 1.344-.765 1.969-.687.218.422.25 1.515.046 2.047.5.593.766 1.39.766 2.203 0 1.922-1.453 3.375-3.547 3.64.531.344.89 1.094.89 1.954v1.625c0 .468.391.734.86.547C13.781 14.359 16 11.53 16 8.03 16 3.61 12.406 0 7.984 0 3.563 0 0 3.61 0 8.031a7.88 7.88 0 0 0 5.172 7.422c.422.156.828-.125.828-.547v-1.25c-.219.094-.5.156-.75.156-1.031 0-1.64-.562-2.078-1.609-.172-.422-.36-.672-.719-.719-.187-.015-.25-.093-.25-.187 0-.188.313-.328.625-.328.453 0 .844.281 1.25.86.313.452.64.655 1.031.655s.641-.14 1-.5c.266-.265.47-.5.657-.656";
561
- var BOOK_16_PATH = "M0 1.75A.75.75 0 0 1 .75 1h4.253c1.227 0 2.317.59 3 1.501A3.743 3.743 0 0 1 11.006 1h4.245a.75.75 0 0 1 .75.75v10.5a.75.75 0 0 1-.75.75h-4.507a2.25 2.25 0 0 0-1.591.659l-.622.621a.75.75 0 0 1-1.06 0l-.622-.621A2.25 2.25 0 0 0 5.258 13H.75a.75.75 0 0 1-.75-.75Zm7.251 10.324.004-5.073-.002-2.253A2.25 2.25 0 0 0 5.003 2.5H1.5v9h3.757a3.75 3.75 0 0 1 1.994.574ZM8.755 4.75l-.004 7.322a3.752 3.752 0 0 1 1.992-.572H14.5v-9h-3.495a2.25 2.25 0 0 0-2.25 2.25Z";
562
577
  var SEARCH_16_PATH = "M10.68 11.74a6 6 0 0 1-7.917-8.984 6 6 0 0 1 8.997 7.905l3.05 3.05a.75.75 0 1 1-1.06 1.06l-3.07-3.03ZM11.5 7a4.499 4.499 0 1 1-8.997 0A4.499 4.499 0 0 1 11.5 7Z";
563
578
  var DOWNLOAD_16_PATHS = [
564
579
  "M2.75 14A1.75 1.75 0 0 1 1 12.25v-2.5a.75.75 0 0 1 1.5 0v2.5c0 .138.112.25.25.25h10.5a.25.25 0 0 0 .25-.25v-2.5a.75.75 0 0 1 1.5 0v2.5A1.75 1.75 0 0 1 13.25 14Z",
@@ -859,6 +874,19 @@ ${lines.join(`
859
874
  return { url: `${target}${lineHash}`, provider: "github" };
860
875
  }
861
876
 
877
+ // web-src/core/tools.ts
878
+ var TOOL_IDS = [
879
+ "markdown",
880
+ "mermaid",
881
+ "json"
882
+ ];
883
+ var DEFAULT_TOOL_ID = "markdown";
884
+ var MIN_TOOLS_SHEET_WIDTH = 480;
885
+ var MAX_TOOLS_SHEET_WIDTH = 4000;
886
+ function isToolId(value) {
887
+ return TOOL_IDS.includes(value);
888
+ }
889
+
862
890
  // web-src/core/routes.ts
863
891
  function assertNever(value) {
864
892
  throw new Error(`unhandled route: ${JSON.stringify(value)}`);
@@ -1129,18 +1157,28 @@ ${lines.join(`
1129
1157
  const params = new URLSearchParams(search);
1130
1158
  return params.get("doctor") === "open";
1131
1159
  }
1132
- function withDoctorOverlay(url, open) {
1160
+ function withQueryParam(url, key, value) {
1133
1161
  const queryIdx = url.indexOf("?");
1134
1162
  const base = queryIdx >= 0 ? url.slice(0, queryIdx) : url;
1135
1163
  const query = queryIdx >= 0 ? url.slice(queryIdx + 1) : "";
1136
1164
  const params = new URLSearchParams(query);
1137
- if (open)
1138
- params.set("doctor", "open");
1165
+ if (value === null)
1166
+ params.delete(key);
1139
1167
  else
1140
- params.delete("doctor");
1168
+ params.set(key, value);
1141
1169
  const qs = params.toString();
1142
1170
  return qs ? `${base}?${qs}` : base;
1143
1171
  }
1172
+ function withDoctorOverlay(url, open) {
1173
+ return withQueryParam(url, "doctor", open ? "open" : null);
1174
+ }
1175
+ function parseToolsOverlay(search) {
1176
+ const raw = new URLSearchParams(search).get("tools");
1177
+ return isToolId(raw) ? raw : null;
1178
+ }
1179
+ function withToolsOverlay(url, tool) {
1180
+ return withQueryParam(url, "tools", tool);
1181
+ }
1144
1182
 
1145
1183
  // web-src/views/media-embed.ts
1146
1184
  var MEDIA_RE = /\.(png|jpe?g|gif|webp|svg|avif|bmp|ico|mp4|webm|mov|mp3|wav|ogg|flac|m4a|aac|opus)(\?.*)?$/i;
@@ -7590,6 +7628,19 @@ ${lines.join(`
7590
7628
  }
7591
7629
 
7592
7630
  // web-src/core/shiki-loader.ts
7631
+ function highlightToInnerHtml(code2, lang, highlighter) {
7632
+ if (!highlighter || !code2)
7633
+ return "";
7634
+ const html = highlighter.codeToHtml(code2, {
7635
+ lang,
7636
+ themes: { light: "github-light", dark: "github-dark" },
7637
+ defaultColor: false
7638
+ });
7639
+ const template = document.createElement("template");
7640
+ template.innerHTML = html;
7641
+ const pre = template.content.querySelector("pre");
7642
+ return pre ? pre.innerHTML : "";
7643
+ }
7593
7644
  var cache = new Map;
7594
7645
  function loadShikiHighlighter(options) {
7595
7646
  const key = JSON.stringify({
@@ -7976,7 +8027,9 @@ ${frontmatter.yaml}
7976
8027
  else
7977
8028
  break;
7978
8029
  }
7979
- if (window.innerHeight + scrollRoot.scrollTop >= scrollRoot.scrollHeight - 4) {
8030
+ const container = scrollableAncestor(root);
8031
+ const atBottom = container ? container.scrollTop + container.clientHeight >= container.scrollHeight - 4 : window.innerHeight + scrollRoot.scrollTop >= scrollRoot.scrollHeight - 4;
8032
+ if (atBottom) {
7980
8033
  active = entries[entries.length - 1];
7981
8034
  }
7982
8035
  entries.forEach((entry) => {
@@ -7988,8 +8041,9 @@ ${frontmatter.yaml}
7988
8041
  if (!raf)
7989
8042
  raf = requestAnimationFrame(update);
7990
8043
  };
7991
- window.addEventListener("scroll", schedule, {
8044
+ document.addEventListener("scroll", schedule, {
7992
8045
  passive: true,
8046
+ capture: true,
7993
8047
  signal: controller.signal
7994
8048
  });
7995
8049
  window.addEventListener("resize", schedule, { signal: controller.signal });
@@ -8012,7 +8066,21 @@ ${frontmatter.yaml}
8012
8066
  function decodeHashFragment(hash) {
8013
8067
  return decodeUriComponentSafe(hash.startsWith("#") ? hash.slice(1) : hash);
8014
8068
  }
8069
+ function scrollableAncestor(element) {
8070
+ for (let node = element.parentElement;node; node = node.parentElement) {
8071
+ const style = getComputedStyle(node);
8072
+ if (/(auto|scroll)/.test(style.overflowY) && node.scrollHeight > node.clientHeight)
8073
+ return node;
8074
+ }
8075
+ return null;
8076
+ }
8015
8077
  function scrollMarkdownSectionIntoView(section, behavior) {
8078
+ const container = scrollableAncestor(section);
8079
+ if (container) {
8080
+ const top2 = section.getBoundingClientRect().top - container.getBoundingClientRect().top + container.scrollTop - 12;
8081
+ container.scrollTo({ top: Math.max(0, top2), behavior });
8082
+ return;
8083
+ }
8016
8084
  const top = section.getBoundingClientRect().top + window.scrollY - markdownAnchorOffset() - 12;
8017
8085
  window.scrollTo({ top: Math.max(0, top), behavior });
8018
8086
  }
@@ -13149,6 +13217,75 @@ ${frontmatter.yaml}
13149
13217
  };
13150
13218
  }
13151
13219
 
13220
+ // web-src/core/diagram-viewport.ts
13221
+ var MIN_SCALE = 0.2;
13222
+ var MAX_SCALE = 3;
13223
+ var BUTTON_STEP = 0.2;
13224
+ var WHEEL_STEP = 0.1;
13225
+ function clampScale(value) {
13226
+ return Math.max(MIN_SCALE, Math.min(MAX_SCALE, value));
13227
+ }
13228
+ function createDiagramViewport(options) {
13229
+ const container = document.createElement("div");
13230
+ container.className = options.containerClassName;
13231
+ const content = document.createElement("div");
13232
+ content.className = options.contentClassName;
13233
+ container.appendChild(content);
13234
+ let scale = 1;
13235
+ function applyZoom(next) {
13236
+ scale = clampScale(next);
13237
+ content.style.transform = `scale(${scale})`;
13238
+ content.style.transformOrigin = "top left";
13239
+ }
13240
+ let dragState = null;
13241
+ container.addEventListener("mousedown", (event) => {
13242
+ if (event.button !== 0)
13243
+ return;
13244
+ dragState = {
13245
+ x: event.clientX,
13246
+ y: event.clientY,
13247
+ sl: container.scrollLeft,
13248
+ st: container.scrollTop
13249
+ };
13250
+ container.style.cursor = "grabbing";
13251
+ event.preventDefault();
13252
+ });
13253
+ const onWindowMouseMove = (event) => {
13254
+ if (!dragState)
13255
+ return;
13256
+ container.scrollLeft = dragState.sl - (event.clientX - dragState.x);
13257
+ container.scrollTop = dragState.st - (event.clientY - dragState.y);
13258
+ };
13259
+ const onWindowMouseUp = () => {
13260
+ if (!dragState)
13261
+ return;
13262
+ dragState = null;
13263
+ container.style.cursor = "";
13264
+ };
13265
+ window.addEventListener("mousemove", onWindowMouseMove);
13266
+ window.addEventListener("mouseup", onWindowMouseUp);
13267
+ container.addEventListener("wheel", (event) => {
13268
+ if (!event.ctrlKey && !event.metaKey)
13269
+ return;
13270
+ event.preventDefault();
13271
+ applyZoom(scale + (event.deltaY > 0 ? -WHEEL_STEP : WHEEL_STEP));
13272
+ }, { passive: false });
13273
+ applyZoom(1);
13274
+ return {
13275
+ container,
13276
+ content,
13277
+ zoomIn: () => applyZoom(scale + BUTTON_STEP),
13278
+ zoomOut: () => applyZoom(scale - BUTTON_STEP),
13279
+ reset: () => applyZoom(1),
13280
+ dispose() {
13281
+ dragState = null;
13282
+ container.style.cursor = "";
13283
+ window.removeEventListener("mousemove", onWindowMouseMove);
13284
+ window.removeEventListener("mouseup", onWindowMouseUp);
13285
+ }
13286
+ };
13287
+ }
13288
+
13152
13289
  // web-src/views/database/er-diagram.ts
13153
13290
  function mermaidType(sqlType) {
13154
13291
  const upper = sqlType.toUpperCase();
@@ -13239,30 +13376,17 @@ ${frontmatter.yaml}
13239
13376
  copyBtn.textContent = text3().er.copyMermaid;
13240
13377
  copyBtn.title = text3().er.copyMermaidTitle;
13241
13378
  toolbar.append(zoomIn, zoomOut, zoomReset, copyBtn);
13242
- const container = document.createElement("div");
13243
- container.className = "db-er-container";
13244
- const svgWrap = document.createElement("div");
13245
- svgWrap.className = "db-er-svg-wrap";
13246
- container.appendChild(svgWrap);
13379
+ const viewport = createDiagramViewport({
13380
+ containerClassName: "db-er-container",
13381
+ contentClassName: "db-er-svg-wrap"
13382
+ });
13383
+ const container = viewport.container;
13384
+ const svgWrap = viewport.content;
13247
13385
  el.append(toolbar, container);
13248
- let scale = 1;
13249
13386
  let lastMarkup = "";
13250
- function applyZoom() {
13251
- svgWrap.style.transform = `scale(${scale})`;
13252
- svgWrap.style.transformOrigin = "top left";
13253
- }
13254
- zoomIn.addEventListener("click", () => {
13255
- scale = Math.min(3, scale + 0.2);
13256
- applyZoom();
13257
- });
13258
- zoomOut.addEventListener("click", () => {
13259
- scale = Math.max(0.2, scale - 0.2);
13260
- applyZoom();
13261
- });
13262
- zoomReset.addEventListener("click", () => {
13263
- scale = 1;
13264
- applyZoom();
13265
- });
13387
+ zoomIn.addEventListener("click", () => viewport.zoomIn());
13388
+ zoomOut.addEventListener("click", () => viewport.zoomOut());
13389
+ zoomReset.addEventListener("click", () => viewport.reset());
13266
13390
  copyBtn.addEventListener("click", () => {
13267
13391
  if (lastMarkup) {
13268
13392
  navigator.clipboard.writeText(lastMarkup).then(() => {
@@ -13275,46 +13399,10 @@ ${frontmatter.yaml}
13275
13399
  });
13276
13400
  }
13277
13401
  });
13278
- let dragState = null;
13279
- container.addEventListener("mousedown", (e2) => {
13280
- if (e2.button !== 0)
13281
- return;
13282
- dragState = {
13283
- x: e2.clientX,
13284
- y: e2.clientY,
13285
- sl: container.scrollLeft,
13286
- st: container.scrollTop
13287
- };
13288
- container.style.cursor = "grabbing";
13289
- e2.preventDefault();
13290
- });
13291
- const onWindowMouseMove = (e2) => {
13292
- if (!dragState)
13293
- return;
13294
- container.scrollLeft = dragState.sl - (e2.clientX - dragState.x);
13295
- container.scrollTop = dragState.st - (e2.clientY - dragState.y);
13296
- };
13297
- const onWindowMouseUp = () => {
13298
- if (dragState) {
13299
- dragState = null;
13300
- container.style.cursor = "";
13301
- }
13302
- };
13303
- window.addEventListener("mousemove", onWindowMouseMove);
13304
- window.addEventListener("mouseup", onWindowMouseUp);
13305
- container.addEventListener("wheel", (e2) => {
13306
- if (e2.ctrlKey || e2.metaKey) {
13307
- e2.preventDefault();
13308
- const delta = e2.deltaY > 0 ? -0.1 : 0.1;
13309
- scale = Math.max(0.2, Math.min(3, scale + delta));
13310
- applyZoom();
13311
- }
13312
- }, { passive: false });
13313
13402
  async function render(schema, columnsMap) {
13314
13403
  el.hidden = false;
13315
13404
  svgWrap.innerHTML = "";
13316
- scale = 1;
13317
- applyZoom();
13405
+ viewport.reset();
13318
13406
  const tables = schema.tables.filter((t2) => t2.type === "table");
13319
13407
  if (tables.length === 0) {
13320
13408
  svgWrap.textContent = text3().er.noTables;
@@ -13341,13 +13429,10 @@ ${frontmatter.yaml}
13341
13429
  el.hidden = true;
13342
13430
  svgWrap.innerHTML = "";
13343
13431
  lastMarkup = "";
13344
- dragState = null;
13345
- container.style.cursor = "";
13346
13432
  }
13347
13433
  function dispose() {
13348
13434
  clear();
13349
- window.removeEventListener("mousemove", onWindowMouseMove);
13350
- window.removeEventListener("mouseup", onWindowMouseUp);
13435
+ viewport.dispose();
13351
13436
  }
13352
13437
  function localize() {
13353
13438
  const t2 = text3().er;
@@ -13635,17 +13720,7 @@ ${frontmatter.yaml}
13635
13720
 
13636
13721
  // web-src/views/database/shiki-sql.ts
13637
13722
  function highlightSqlToInnerHtml(code2, highlighter) {
13638
- if (!highlighter || !code2)
13639
- return "";
13640
- const html = highlighter.codeToHtml(code2, {
13641
- lang: "sql",
13642
- themes: { light: "github-light", dark: "github-dark" },
13643
- defaultColor: false
13644
- });
13645
- const template = document.createElement("template");
13646
- template.innerHTML = html;
13647
- const pre = template.content.querySelector("pre");
13648
- return pre ? pre.innerHTML : "";
13723
+ return highlightToInnerHtml(code2, "sql", highlighter);
13649
13724
  }
13650
13725
 
13651
13726
  // web-src/views/database/query-editor.ts
@@ -23697,7 +23772,7 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
23697
23772
  function invalidateLoadedCard(card, file, changedPaths, measuredHeight) {
23698
23773
  if (!card.classList.contains("loaded") && !card.classList.contains("loading"))
23699
23774
  return false;
23700
- if (changedPaths && !changedPaths.has(file.path))
23775
+ if (!changedPathsCoverPath(changedPaths, file.path))
23701
23776
  return false;
23702
23777
  card.dataset.reqId = String(++CLIENT_REQ_SEQ);
23703
23778
  card.style.minHeight = `${measuredHeight ?? card.offsetHeight}px`;
@@ -23787,13 +23862,12 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
23787
23862
  const pathsUnknown = !changedPaths;
23788
23863
  const contentMayNeedReload = !listSame || pathsUnknown || (changedPaths ? newFiles.some((file) => {
23789
23864
  const key = fileKey(file);
23790
- return prevCardSignatures.get(key) !== newCardSigs.get(key) || changedPaths.has(file.path);
23865
+ return prevCardSignatures.get(key) !== newCardSigs.get(key) || changedPathsCoverPath(changedPaths, file.path);
23791
23866
  }) : false);
23792
23867
  if (contentMayNeedReload)
23793
23868
  resetLoadScheduling();
23794
23869
  let invalidatedCards = 0;
23795
23870
  if (listSame && domIntact) {
23796
- const pathsUnknown2 = !changedPaths;
23797
23871
  const cardsByKey = collectDiffCardsByKey(target);
23798
23872
  const measuredHeights = new Map;
23799
23873
  for (const f2 of newFiles) {
@@ -23808,7 +23882,7 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
23808
23882
  const oldSig = prevCardSignatures.get(key);
23809
23883
  const newSig = newCardSigs.get(key) ?? computeCardSignature(f2);
23810
23884
  const sigChanged = oldSig !== newSig;
23811
- const pathHint = pathsUnknown2 || changedPaths.has(f2.path);
23885
+ const pathHint = changedPathsCoverPath(changedPaths, f2.path);
23812
23886
  if (!sigChanged && !pathHint) {
23813
23887
  continue;
23814
23888
  }
@@ -26440,6 +26514,19 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
26440
26514
  }
26441
26515
  ]
26442
26516
  },
26517
+ {
26518
+ title: "Scratch on pasted text",
26519
+ blocks: [
26520
+ {
26521
+ kind: "paragraph",
26522
+ text: "The Tools item in the header menu opens a drawer for text you paste, without leaving the screen you are on. It holds a Markdown preview (the same renderer as the file preview, so the table of contents, task lists, frontmatter, code highlighting and ```mermaid fences all work), a Mermaid preview with zoom and drag-pan, and a JSON / YAML tool that reads either format and re-emits it as formatted JSON or YAML — a validator and a converter in one."
26523
+ },
26524
+ {
26525
+ kind: "paragraph",
26526
+ text: "Each tool keeps its own draft in .code-viewer/tools.json, so the drawer reopens where you left it. The open tool is part of the URL (?tools=markdown), which makes it shareable and survives a reload, and the drawer width is draggable from the grip on its left edge."
26527
+ }
26528
+ ]
26529
+ },
26443
26530
  {
26444
26531
  title: "Environment doctor",
26445
26532
  blocks: [
@@ -26498,6 +26585,10 @@ code-viewer doctor --bin git=/opt/bin/git --bin docker=/opt/bin/docker --bin gh=
26498
26585
  "db-ui.json",
26499
26586
  "Datastore UI preferences — column widths per (DB, table, column) and toggle states such as Rails FK inference and the S3 tooltip."
26500
26587
  ],
26588
+ [
26589
+ "tools.json",
26590
+ "Tools drawer state — the pasted draft for each tool (Markdown, Mermaid, JSON / YAML), the tool you used last, and the drawer width."
26591
+ ],
26501
26592
  [
26502
26593
  "annotations.json",
26503
26594
  "AI annotation walkthroughs — sessions with their ordered steps (file, line range, title, body), used by the annotation panel and synced live to open tabs over SSE."
@@ -27135,6 +27226,19 @@ code-viewer query agent-help`
27135
27226
  }
27136
27227
  ]
27137
27228
  },
27229
+ {
27230
+ title: "貼り付けたテキストを扱う",
27231
+ blocks: [
27232
+ {
27233
+ kind: "paragraph",
27234
+ text: "ヘッダメニューの Tools は、いま見ている画面を離れずに使える貼り付け用のドロワーを開きます。Markdown プレビュー(ファイルプレビューと同じ描画なので、目次・タスクリスト・frontmatter・コードハイライト・```mermaid フェンスがそのまま効きます)、ズームとドラッグ移動ができる Mermaid プレビュー、JSON と YAML のどちらでも読み取って整形し直す JSON / YAML ツール(検証と相互変換を兼ねます)が入っています。"
27235
+ },
27236
+ {
27237
+ kind: "paragraph",
27238
+ text: "各ツールの入力は .code-viewer/tools.json に保存されるので、開き直すと続きから使えます。開いているツールは URL(?tools=markdown)に載るので共有もリロードもでき、ドロワーの幅は左端のグリップをドラッグして変えられます。"
27239
+ }
27240
+ ]
27241
+ },
27138
27242
  {
27139
27243
  title: "環境ドクター",
27140
27244
  blocks: [
@@ -27193,6 +27297,10 @@ code-viewer doctor --bin git=/opt/bin/git --bin docker=/opt/bin/docker --bin gh=
27193
27297
  "db-ui.json",
27194
27298
  "データストア UI の設定 — (DB, テーブル, カラム) ごとの列幅、Rails FK 推測や S3 ツールチップなどのトグル状態。"
27195
27299
  ],
27300
+ [
27301
+ "tools.json",
27302
+ "ツールドロワーの状態 — 各ツール(Markdown / Mermaid / JSON・YAML)に貼り付けた下書き、最後に使ったツール、ドロワーの幅。"
27303
+ ],
27196
27304
  [
27197
27305
  "annotations.json",
27198
27306
  "AI 注釈のウォークスルー — セッションと順序付きステップ(ファイル、行範囲、タイトル、本文)。注釈パネルが読み、SSE で開いているタブへもライブ同期されます。"
@@ -36180,6 +36288,1017 @@ code-viewer query agent-help`
36180
36288
  };
36181
36289
  }
36182
36290
 
36291
+ // web-src/views/tools/i18n.ts
36292
+ var TEXT2 = {
36293
+ en: {
36294
+ title: "Tools",
36295
+ open: "tools (Markdown / Mermaid / JSON)",
36296
+ close: "Close",
36297
+ resizeSheet: "Resize the tools drawer",
36298
+ saveFailed: "could not save this draft",
36299
+ loadFailed: "could not load saved drafts",
36300
+ tabs: {
36301
+ markdown: "Markdown",
36302
+ mermaid: "Mermaid",
36303
+ json: "JSON / YAML"
36304
+ },
36305
+ pane: {
36306
+ input: "Input",
36307
+ clear: "clear",
36308
+ clearTitle: "Clear the input",
36309
+ copy: "copy",
36310
+ copyTitle: "Copy the output",
36311
+ copied: "copied",
36312
+ copyFailed: "copy failed",
36313
+ resize: "Resize input and output panes",
36314
+ emptyInput: "Paste something on the left to see it rendered here."
36315
+ },
36316
+ markdown: {
36317
+ output: "Preview",
36318
+ placeholder: "Paste Markdown here…"
36319
+ },
36320
+ mermaid: {
36321
+ output: "Diagram",
36322
+ placeholder: `Paste Mermaid source here, for example:
36323
+ graph TD
36324
+ A --> B`,
36325
+ zoomIn: "Zoom in",
36326
+ zoomOut: "Zoom out",
36327
+ zoomReset: "Reset zoom",
36328
+ loadError: "Failed to load the Mermaid renderer.",
36329
+ renderError: "Could not render this diagram. Check the syntax."
36330
+ },
36331
+ json: {
36332
+ output: "Output",
36333
+ placeholder: "Paste JSON or YAML here…",
36334
+ outputFormat: "output format",
36335
+ asJson: "JSON",
36336
+ asYaml: "YAML",
36337
+ minify: "minify",
36338
+ minifyTitle: "Emit JSON on a single line",
36339
+ minifyYamlTitle: "Minify applies to JSON output only",
36340
+ parsedAsJson: "read as JSON",
36341
+ parsedAsYaml: "read as YAML",
36342
+ invalid: (message) => `Invalid input: ${message}`,
36343
+ loadError: "Failed to load the YAML parser."
36344
+ }
36345
+ },
36346
+ ja: {
36347
+ title: "ツール",
36348
+ open: "ツール (Markdown / Mermaid / JSON)",
36349
+ close: "閉じる",
36350
+ resizeSheet: "ツールの幅を変える",
36351
+ saveFailed: "この下書きを保存できません",
36352
+ loadFailed: "保存済みの下書きを読み出せません",
36353
+ tabs: {
36354
+ markdown: "Markdown",
36355
+ mermaid: "Mermaid",
36356
+ json: "JSON / YAML"
36357
+ },
36358
+ pane: {
36359
+ input: "入力",
36360
+ clear: "クリア",
36361
+ clearTitle: "入力を空にする",
36362
+ copy: "コピー",
36363
+ copyTitle: "出力をコピーする",
36364
+ copied: "コピーしました",
36365
+ copyFailed: "コピーできません",
36366
+ resize: "入力と出力の幅を変える",
36367
+ emptyInput: "左に貼り付けると、ここに結果が出ます。"
36368
+ },
36369
+ markdown: {
36370
+ output: "プレビュー",
36371
+ placeholder: "Markdown を貼り付け…"
36372
+ },
36373
+ mermaid: {
36374
+ output: "図",
36375
+ placeholder: `Mermaid の記法を貼り付け。例:
36376
+ graph TD
36377
+ A --> B`,
36378
+ zoomIn: "拡大",
36379
+ zoomOut: "縮小",
36380
+ zoomReset: "等倍に戻す",
36381
+ loadError: "Mermaid の読み込みに失敗しました。",
36382
+ renderError: "この図を描けませんでした。記法を確認してください。"
36383
+ },
36384
+ json: {
36385
+ output: "出力",
36386
+ placeholder: "JSON か YAML を貼り付け…",
36387
+ outputFormat: "出力形式",
36388
+ asJson: "JSON",
36389
+ asYaml: "YAML",
36390
+ minify: "1行化",
36391
+ minifyTitle: "JSON を1行で出力する",
36392
+ minifyYamlTitle: "1行化は JSON 出力のときだけ効きます",
36393
+ parsedAsJson: "JSON として解釈",
36394
+ parsedAsYaml: "YAML として解釈",
36395
+ invalid: (message) => `解釈できません: ${message}`,
36396
+ loadError: "YAML パーサの読み込みに失敗しました。"
36397
+ }
36398
+ }
36399
+ };
36400
+ function toolsText(lang) {
36401
+ return TEXT2[lang];
36402
+ }
36403
+
36404
+ // web-src/core/drag-resizer.ts
36405
+ var DEFAULT_KEYBOARD_STEP = 16;
36406
+ function attachDragResizer(options) {
36407
+ const { handle } = options;
36408
+ const step = options.keyboardStep ?? DEFAULT_KEYBOARD_STEP;
36409
+ let pointerId = null;
36410
+ let startX = 0;
36411
+ let startSize = 0;
36412
+ function setActive(active) {
36413
+ if (!options.activeClassTarget || !options.activeClassName)
36414
+ return;
36415
+ options.activeClassTarget.classList.toggle(options.activeClassName, active);
36416
+ }
36417
+ const onPointerDown = (event) => {
36418
+ if (event.button !== 0)
36419
+ return;
36420
+ pointerId = event.pointerId;
36421
+ startX = event.clientX;
36422
+ startSize = options.getSize();
36423
+ try {
36424
+ handle.setPointerCapture?.(event.pointerId);
36425
+ } catch {}
36426
+ setActive(true);
36427
+ event.preventDefault();
36428
+ };
36429
+ const onPointerMove = (event) => {
36430
+ if (pointerId !== event.pointerId)
36431
+ return;
36432
+ options.applySize(startSize + (event.clientX - startX) * options.direction);
36433
+ };
36434
+ const onPointerEnd = (event) => {
36435
+ if (pointerId !== event.pointerId)
36436
+ return;
36437
+ pointerId = null;
36438
+ setActive(false);
36439
+ options.onEnd?.();
36440
+ };
36441
+ const onKeyDown = (event) => {
36442
+ if (event.key !== "ArrowLeft" && event.key !== "ArrowRight")
36443
+ return;
36444
+ const delta = (event.key === "ArrowRight" ? 1 : -1) * options.direction;
36445
+ options.applySize(options.getSize() + delta * step * (event.shiftKey ? 4 : 1));
36446
+ event.preventDefault();
36447
+ options.onEnd?.();
36448
+ };
36449
+ handle.addEventListener("pointerdown", onPointerDown);
36450
+ handle.addEventListener("pointermove", onPointerMove);
36451
+ handle.addEventListener("pointerup", onPointerEnd);
36452
+ handle.addEventListener("pointercancel", onPointerEnd);
36453
+ handle.addEventListener("keydown", onKeyDown);
36454
+ return () => {
36455
+ handle.removeEventListener("pointerdown", onPointerDown);
36456
+ handle.removeEventListener("pointermove", onPointerMove);
36457
+ handle.removeEventListener("pointerup", onPointerEnd);
36458
+ handle.removeEventListener("pointercancel", onPointerEnd);
36459
+ handle.removeEventListener("keydown", onKeyDown);
36460
+ pointerId = null;
36461
+ setActive(false);
36462
+ };
36463
+ }
36464
+
36465
+ // web-src/core/yaml-loader.ts
36466
+ var yamlPromise = null;
36467
+ function loadYaml() {
36468
+ if (!yamlPromise) {
36469
+ yamlPromise = import("/yaml.js").then((mod) => mod).catch(() => null);
36470
+ }
36471
+ return yamlPromise;
36472
+ }
36473
+
36474
+ // web-src/views/tools/scratchpad-pane.ts
36475
+ var RENDER_DEBOUNCE_MS = 200;
36476
+ var STATUS_CLEAR_MS = 1800;
36477
+ function createPaneAction(label, title) {
36478
+ const button = document.createElement("button");
36479
+ button.type = "button";
36480
+ button.className = "tools-pane-action";
36481
+ button.textContent = label;
36482
+ button.title = title;
36483
+ return button;
36484
+ }
36485
+ function createHead(title) {
36486
+ const head = document.createElement("header");
36487
+ head.className = "tools-pane-head";
36488
+ const titleEl = document.createElement("span");
36489
+ titleEl.className = "tools-pane-title";
36490
+ titleEl.textContent = title;
36491
+ head.appendChild(titleEl);
36492
+ return { head, titleEl };
36493
+ }
36494
+ function createScratchpadPane(text3, outputTitle, placeholder, options) {
36495
+ let currentText = text3;
36496
+ const el = document.createElement("div");
36497
+ el.className = `tools-pane ${options.toolClassName}`;
36498
+ const inputSide = document.createElement("section");
36499
+ inputSide.className = "tools-pane-side tools-pane-input";
36500
+ const { head: inputHead, titleEl: inputTitle } = createHead(text3.pane.input);
36501
+ const clearBtn = document.createElement("button");
36502
+ clearBtn.type = "button";
36503
+ clearBtn.className = "tools-pane-action";
36504
+ clearBtn.textContent = text3.pane.clear;
36505
+ clearBtn.title = text3.pane.clearTitle;
36506
+ inputHead.appendChild(clearBtn);
36507
+ const input2 = document.createElement("textarea");
36508
+ input2.className = "tools-textarea";
36509
+ input2.spellcheck = false;
36510
+ input2.placeholder = placeholder;
36511
+ input2.value = options.initialText;
36512
+ inputSide.append(inputHead, input2);
36513
+ const resizer = document.createElement("div");
36514
+ resizer.className = "tools-pane-resizer";
36515
+ resizer.role = "separator";
36516
+ resizer.tabIndex = 0;
36517
+ resizer.setAttribute("aria-orientation", "vertical");
36518
+ resizer.setAttribute("aria-label", text3.pane.resize);
36519
+ const outputSide = document.createElement("section");
36520
+ outputSide.className = "tools-pane-side tools-pane-output";
36521
+ const { head: outputHead, titleEl: outputTitleEl } = createHead(outputTitle);
36522
+ const actions = document.createElement("div");
36523
+ actions.className = "tools-pane-actions";
36524
+ for (const action of options.outputActions ?? [])
36525
+ actions.appendChild(action);
36526
+ const status = document.createElement("span");
36527
+ status.className = "tools-pane-status";
36528
+ status.role = "status";
36529
+ outputHead.append(actions, status);
36530
+ const output = document.createElement("div");
36531
+ output.className = "tools-pane-body";
36532
+ outputSide.append(outputHead, output);
36533
+ el.append(inputSide, resizer, outputSide);
36534
+ let statusTimer = null;
36535
+ let pinnedStatus = null;
36536
+ let normalStatus = { message: "", tone: "info" };
36537
+ function applyStatus(state) {
36538
+ status.textContent = state.message;
36539
+ status.classList.toggle("tools-pane-status-error", state.tone === "error");
36540
+ }
36541
+ function clearStatusTimer() {
36542
+ if (!statusTimer)
36543
+ return;
36544
+ clearTimeout(statusTimer);
36545
+ statusTimer = null;
36546
+ }
36547
+ function setStatus(message, tone = "info") {
36548
+ clearStatusTimer();
36549
+ normalStatus = { message, tone };
36550
+ if (pinnedStatus)
36551
+ return;
36552
+ applyStatus(normalStatus);
36553
+ }
36554
+ function flashStatus(message, tone = "info") {
36555
+ clearStatusTimer();
36556
+ if (pinnedStatus)
36557
+ return;
36558
+ applyStatus({ message, tone });
36559
+ statusTimer = setTimeout(() => {
36560
+ statusTimer = null;
36561
+ applyStatus(pinnedStatus ?? normalStatus);
36562
+ }, STATUS_CLEAR_MS);
36563
+ }
36564
+ function setPinnedStatus(message, tone = "error") {
36565
+ clearStatusTimer();
36566
+ pinnedStatus = message ? { message, tone } : null;
36567
+ applyStatus(pinnedStatus ?? normalStatus);
36568
+ }
36569
+ let renderController = null;
36570
+ let debounceTimer = null;
36571
+ function renderNow() {
36572
+ renderController?.abort();
36573
+ const controller = new AbortController;
36574
+ renderController = controller;
36575
+ const value = input2.value;
36576
+ if (!value.trim()) {
36577
+ const empty = document.createElement("p");
36578
+ empty.className = "tools-pane-empty";
36579
+ empty.textContent = currentText.pane.emptyInput;
36580
+ output.replaceChildren(empty);
36581
+ setStatus("");
36582
+ return;
36583
+ }
36584
+ (async () => {
36585
+ try {
36586
+ await options.render(value, output, controller.signal);
36587
+ } catch (err) {
36588
+ if (controller.signal.aborted)
36589
+ return;
36590
+ const message = err instanceof Error ? err.message : String(err);
36591
+ setStatus(message, "error");
36592
+ }
36593
+ })();
36594
+ }
36595
+ function scheduleRender() {
36596
+ if (debounceTimer)
36597
+ clearTimeout(debounceTimer);
36598
+ debounceTimer = setTimeout(() => {
36599
+ debounceTimer = null;
36600
+ renderNow();
36601
+ }, RENDER_DEBOUNCE_MS);
36602
+ }
36603
+ input2.addEventListener("input", () => {
36604
+ options.onInput(input2.value);
36605
+ renderController?.abort();
36606
+ renderController = null;
36607
+ scheduleRender();
36608
+ });
36609
+ clearBtn.addEventListener("click", () => {
36610
+ if (!input2.value)
36611
+ return;
36612
+ input2.value = "";
36613
+ options.onInput("");
36614
+ input2.focus();
36615
+ renderNow();
36616
+ });
36617
+ const MIN_SIDE_WIDTH = 220;
36618
+ function applyInputWidth(width) {
36619
+ const total = el.getBoundingClientRect().width;
36620
+ const max = Math.max(MIN_SIDE_WIDTH, total - MIN_SIDE_WIDTH);
36621
+ const clamped = Math.max(MIN_SIDE_WIDTH, Math.min(max, width));
36622
+ el.style.setProperty("--tools-input-width", `${Math.round(clamped)}px`);
36623
+ }
36624
+ const detachResizer = attachDragResizer({
36625
+ handle: resizer,
36626
+ getSize: () => inputSide.getBoundingClientRect().width,
36627
+ applySize: applyInputWidth,
36628
+ direction: 1,
36629
+ activeClassTarget: el,
36630
+ activeClassName: "tools-pane-resizing"
36631
+ });
36632
+ return {
36633
+ el,
36634
+ input: input2,
36635
+ output,
36636
+ setStatus,
36637
+ flashStatus,
36638
+ setPinnedStatus,
36639
+ refresh: renderNow,
36640
+ setText(next) {
36641
+ input2.value = next;
36642
+ },
36643
+ getText: () => input2.value,
36644
+ localize(nextText, nextOutputTitle, nextPlaceholder) {
36645
+ currentText = nextText;
36646
+ inputTitle.textContent = nextText.pane.input;
36647
+ clearBtn.textContent = nextText.pane.clear;
36648
+ clearBtn.title = nextText.pane.clearTitle;
36649
+ resizer.setAttribute("aria-label", nextText.pane.resize);
36650
+ outputTitleEl.textContent = nextOutputTitle;
36651
+ input2.placeholder = nextPlaceholder;
36652
+ },
36653
+ focus: () => input2.focus(),
36654
+ dispose() {
36655
+ detachResizer();
36656
+ renderController?.abort();
36657
+ renderController = null;
36658
+ if (debounceTimer)
36659
+ clearTimeout(debounceTimer);
36660
+ debounceTimer = null;
36661
+ if (statusTimer)
36662
+ clearTimeout(statusTimer);
36663
+ statusTimer = null;
36664
+ }
36665
+ };
36666
+ }
36667
+
36668
+ // web-src/views/tools/structured-text.ts
36669
+ var JSON_INDENT = 2;
36670
+ function parseStructuredText(text3, yaml) {
36671
+ const trimmed = text3.trim();
36672
+ if (!trimmed)
36673
+ return { status: "error", message: "empty input" };
36674
+ let jsonMessage = "";
36675
+ try {
36676
+ return { status: "ok", source: "json", value: JSON.parse(trimmed) };
36677
+ } catch (err) {
36678
+ jsonMessage = err instanceof Error ? err.message : String(err);
36679
+ }
36680
+ if (!yaml)
36681
+ return { status: "error", message: jsonMessage };
36682
+ try {
36683
+ const doc = yaml.parseDocument(text3);
36684
+ if (doc.errors.length > 0)
36685
+ return {
36686
+ status: "error",
36687
+ message: doc.errors.map((issue) => issue.message).join(`
36688
+
36689
+ `)
36690
+ };
36691
+ const warnings = doc.warnings.map((issue) => issue.message);
36692
+ return {
36693
+ status: "ok",
36694
+ source: "yaml",
36695
+ value: doc.toJS(),
36696
+ ...warnings.length ? { warnings } : {}
36697
+ };
36698
+ } catch (err) {
36699
+ return {
36700
+ status: "error",
36701
+ message: err instanceof Error ? err.message : String(err)
36702
+ };
36703
+ }
36704
+ }
36705
+ function formatStructured(value, format2, minify, yaml) {
36706
+ if (format2 === "yaml") {
36707
+ if (!yaml)
36708
+ return null;
36709
+ return yaml.stringify(value, { indent: JSON_INDENT });
36710
+ }
36711
+ return JSON.stringify(value, null, minify ? 0 : JSON_INDENT) ?? "";
36712
+ }
36713
+
36714
+ // web-src/views/tools/json-tool.ts
36715
+ async function resolveInput(value, format2, signal) {
36716
+ const direct = parseStructuredText(value, null);
36717
+ if (direct.status === "ok" && format2 === "json")
36718
+ return { parsed: direct, yaml: null };
36719
+ const yaml = await loadYaml();
36720
+ if (signal.aborted)
36721
+ return null;
36722
+ return {
36723
+ parsed: direct.status === "ok" ? direct : parseStructuredText(value, yaml),
36724
+ yaml
36725
+ };
36726
+ }
36727
+ function createJsonTool(text3, initialText, onInput) {
36728
+ let currentText = text3;
36729
+ let format2 = "json";
36730
+ let minify = false;
36731
+ let lastOutput = "";
36732
+ const seg = document.createElement("div");
36733
+ seg.className = "seg";
36734
+ seg.role = "group";
36735
+ seg.setAttribute("aria-label", text3.json.outputFormat);
36736
+ const jsonBtn = document.createElement("button");
36737
+ jsonBtn.type = "button";
36738
+ jsonBtn.textContent = text3.json.asJson;
36739
+ jsonBtn.classList.add("active");
36740
+ const yamlBtn = document.createElement("button");
36741
+ yamlBtn.type = "button";
36742
+ yamlBtn.textContent = text3.json.asYaml;
36743
+ seg.append(jsonBtn, yamlBtn);
36744
+ const minifyBtn = createPaneAction(text3.json.minify, text3.json.minifyTitle);
36745
+ const copyBtn = createPaneAction(text3.pane.copy, text3.pane.copyTitle);
36746
+ function syncActionState() {
36747
+ jsonBtn.classList.toggle("active", format2 === "json");
36748
+ yamlBtn.classList.toggle("active", format2 === "yaml");
36749
+ minifyBtn.classList.toggle("active", minify);
36750
+ minifyBtn.setAttribute("aria-pressed", String(minify));
36751
+ minifyBtn.disabled = format2 === "yaml";
36752
+ minifyBtn.title = format2 === "yaml" ? currentText.json.minifyYamlTitle : currentText.json.minifyTitle;
36753
+ }
36754
+ const pane = createScratchpadPane(text3, text3.json.output, text3.json.placeholder, {
36755
+ toolClassName: "tools-pane-json",
36756
+ initialText,
36757
+ onInput,
36758
+ outputActions: [seg, minifyBtn, copyBtn],
36759
+ render: async (value, output, signal) => {
36760
+ const resolved = await resolveInput(value, format2, signal);
36761
+ if (!resolved)
36762
+ return;
36763
+ const { parsed, yaml } = resolved;
36764
+ if (parsed.status === "error") {
36765
+ lastOutput = "";
36766
+ const error2 = document.createElement("p");
36767
+ error2.className = "tools-pane-error";
36768
+ error2.textContent = currentText.json.invalid(parsed.message);
36769
+ output.replaceChildren(error2);
36770
+ pane.setStatus("");
36771
+ return;
36772
+ }
36773
+ const formatted = formatStructured(parsed.value, format2, minify, yaml);
36774
+ if (formatted === null) {
36775
+ lastOutput = "";
36776
+ const error2 = document.createElement("p");
36777
+ error2.className = "tools-pane-error";
36778
+ error2.textContent = currentText.json.loadError;
36779
+ output.replaceChildren(error2);
36780
+ pane.setStatus("");
36781
+ return;
36782
+ }
36783
+ lastOutput = formatted;
36784
+ const highlighter = await loadShikiHighlighter({
36785
+ themes: ["github-light", "github-dark"],
36786
+ langs: ["json", "yaml"]
36787
+ });
36788
+ if (signal.aborted)
36789
+ return;
36790
+ const pre = document.createElement("pre");
36791
+ pre.className = "tools-code";
36792
+ const html = highlightToInnerHtml(formatted, format2, highlighter);
36793
+ if (html)
36794
+ pre.innerHTML = html;
36795
+ else
36796
+ pre.textContent = formatted;
36797
+ if (parsed.warnings?.length) {
36798
+ const warning = document.createElement("p");
36799
+ warning.className = "tools-pane-warning";
36800
+ warning.textContent = parsed.warnings.join(`
36801
+
36802
+ `);
36803
+ output.replaceChildren(warning, pre);
36804
+ } else {
36805
+ output.replaceChildren(pre);
36806
+ }
36807
+ pane.setStatus(parsed.source === "json" ? currentText.json.parsedAsJson : currentText.json.parsedAsYaml);
36808
+ }
36809
+ });
36810
+ jsonBtn.addEventListener("click", () => {
36811
+ if (format2 === "json")
36812
+ return;
36813
+ format2 = "json";
36814
+ syncActionState();
36815
+ pane.refresh();
36816
+ });
36817
+ yamlBtn.addEventListener("click", () => {
36818
+ if (format2 === "yaml")
36819
+ return;
36820
+ format2 = "yaml";
36821
+ syncActionState();
36822
+ pane.refresh();
36823
+ });
36824
+ minifyBtn.addEventListener("click", () => {
36825
+ minify = !minify;
36826
+ syncActionState();
36827
+ pane.refresh();
36828
+ });
36829
+ copyBtn.addEventListener("click", () => {
36830
+ if (!lastOutput)
36831
+ return;
36832
+ navigator.clipboard.writeText(lastOutput).then(() => pane.flashStatus(currentText.pane.copied), () => pane.flashStatus(currentText.pane.copyFailed, "error"));
36833
+ });
36834
+ syncActionState();
36835
+ return {
36836
+ ...pane,
36837
+ localizeActions(nextText) {
36838
+ currentText = nextText;
36839
+ seg.setAttribute("aria-label", nextText.json.outputFormat);
36840
+ jsonBtn.textContent = nextText.json.asJson;
36841
+ yamlBtn.textContent = nextText.json.asYaml;
36842
+ minifyBtn.textContent = nextText.json.minify;
36843
+ copyBtn.textContent = nextText.pane.copy;
36844
+ copyBtn.title = nextText.pane.copyTitle;
36845
+ syncActionState();
36846
+ }
36847
+ };
36848
+ }
36849
+
36850
+ // web-src/views/tools/markdown-tool.ts
36851
+ var SCRATCHPAD_TARGET = { path: "scratchpad.md", ref: "worktree" };
36852
+ function createMarkdownTool(text3, initialText, onInput) {
36853
+ return createScratchpadPane(text3, text3.markdown.output, text3.markdown.placeholder, {
36854
+ toolClassName: "tools-pane-markdown",
36855
+ initialText,
36856
+ onInput,
36857
+ render: async (value, output, signal) => {
36858
+ const rendered = await renderMarkdownPreview(value, SCRATCHPAD_TARGET, {
36859
+ syntaxHighlight: true,
36860
+ signal
36861
+ });
36862
+ if (signal.aborted)
36863
+ return;
36864
+ output.replaceChildren(rendered);
36865
+ }
36866
+ });
36867
+ }
36868
+
36869
+ // web-src/views/tools/mermaid-tool.ts
36870
+ function createMermaidTool(text3, initialText, onInput) {
36871
+ let currentText = text3;
36872
+ const viewport = createDiagramViewport({
36873
+ containerClassName: "tools-mermaid-container",
36874
+ contentClassName: "tools-mermaid-canvas"
36875
+ });
36876
+ const zoomIn = createPaneAction("+", text3.mermaid.zoomIn);
36877
+ const zoomOut = createPaneAction("−", text3.mermaid.zoomOut);
36878
+ const zoomReset = createPaneAction("1:1", text3.mermaid.zoomReset);
36879
+ zoomIn.addEventListener("click", () => viewport.zoomIn());
36880
+ zoomOut.addEventListener("click", () => viewport.zoomOut());
36881
+ zoomReset.addEventListener("click", () => viewport.reset());
36882
+ const pane = createScratchpadPane(text3, text3.mermaid.output, text3.mermaid.placeholder, {
36883
+ toolClassName: "tools-pane-mermaid",
36884
+ initialText,
36885
+ onInput,
36886
+ outputActions: [zoomIn, zoomOut, zoomReset],
36887
+ render: async (value, output, signal) => {
36888
+ const mermaid = await loadMermaid();
36889
+ if (signal.aborted)
36890
+ return;
36891
+ if (!mermaid) {
36892
+ pane.setStatus(currentText.mermaid.loadError, "error");
36893
+ return;
36894
+ }
36895
+ const node = document.createElement("div");
36896
+ node.className = "mermaid";
36897
+ node.textContent = value;
36898
+ viewport.content.replaceChildren(node);
36899
+ viewport.reset();
36900
+ output.replaceChildren(viewport.container);
36901
+ try {
36902
+ await mermaid.run({ nodes: [node], suppressErrors: true });
36903
+ } catch {
36904
+ if (signal.aborted)
36905
+ return;
36906
+ pane.setStatus(currentText.mermaid.renderError, "error");
36907
+ return;
36908
+ }
36909
+ if (signal.aborted)
36910
+ return;
36911
+ if (node.querySelector("svg"))
36912
+ pane.setStatus("");
36913
+ else
36914
+ pane.setStatus(currentText.mermaid.renderError, "error");
36915
+ }
36916
+ });
36917
+ const disposePane = pane.dispose;
36918
+ return {
36919
+ ...pane,
36920
+ localizeActions(nextText) {
36921
+ currentText = nextText;
36922
+ zoomIn.title = nextText.mermaid.zoomIn;
36923
+ zoomOut.title = nextText.mermaid.zoomOut;
36924
+ zoomReset.title = nextText.mermaid.zoomReset;
36925
+ },
36926
+ dispose() {
36927
+ disposePane();
36928
+ viewport.dispose();
36929
+ }
36930
+ };
36931
+ }
36932
+
36933
+ // web-src/views/tools/tools-view.ts
36934
+ var SAVE_DEBOUNCE_MS = 800;
36935
+ var SAVE_RETRY_MS = 1500;
36936
+ var SAVE_RETRY_LIMIT = 3;
36937
+ var KEEPALIVE_BODY_LIMIT_BYTES = 64 * 1024;
36938
+ function createToolsView(deps) {
36939
+ let panes = null;
36940
+ let tabs = null;
36941
+ let titleEl = null;
36942
+ let closeBtn = null;
36943
+ let activeTool = DEFAULT_TOOL_ID;
36944
+ let stateLoaded = false;
36945
+ let savedActiveTool = null;
36946
+ const drafts = {};
36947
+ let saveTimer = null;
36948
+ let saveRetryTimer = null;
36949
+ let dirty = false;
36950
+ let saveInFlight = false;
36951
+ let saveController = null;
36952
+ let saveFailed = false;
36953
+ let saveRetries = 0;
36954
+ let loadFailed = false;
36955
+ let disposed = false;
36956
+ let sheetWidth = null;
36957
+ let toolTouched = false;
36958
+ let widthTouched = false;
36959
+ let generation = 0;
36960
+ function text3() {
36961
+ return toolsText(deps.getLanguage());
36962
+ }
36963
+ function getMount() {
36964
+ return deps.$("#tools-sheet");
36965
+ }
36966
+ function getOverlay() {
36967
+ return deps.$("#tools-sheet-overlay");
36968
+ }
36969
+ function isPermanentSaveFailure(status) {
36970
+ return status >= 400 && status < 500;
36971
+ }
36972
+ function syncFailureStatus() {
36973
+ if (!panes)
36974
+ return;
36975
+ const current = text3();
36976
+ const message = loadFailed ? current.loadFailed : saveFailed ? current.saveFailed : null;
36977
+ for (const id of TOOL_IDS)
36978
+ panes[id].setPinnedStatus(message);
36979
+ }
36980
+ function reportSaveFailure() {
36981
+ saveFailed = true;
36982
+ syncFailureStatus();
36983
+ }
36984
+ function clearSaveFailure() {
36985
+ saveRetries = 0;
36986
+ if (!saveFailed)
36987
+ return;
36988
+ saveFailed = false;
36989
+ syncFailureStatus();
36990
+ }
36991
+ function reportLoadFailure() {
36992
+ loadFailed = true;
36993
+ syncFailureStatus();
36994
+ }
36995
+ function save(options = {}) {
36996
+ if (saveTimer) {
36997
+ clearTimeout(saveTimer);
36998
+ saveTimer = null;
36999
+ }
37000
+ if (disposed)
37001
+ return;
37002
+ if (!options.keepalive && saveRetries > SAVE_RETRY_LIMIT)
37003
+ return;
37004
+ const pending = dirty || options.keepalive === true && saveInFlight;
37005
+ if (!pending)
37006
+ return;
37007
+ if (saveInFlight && !options.keepalive)
37008
+ return;
37009
+ if (options.keepalive)
37010
+ saveController?.abort();
37011
+ dirty = false;
37012
+ const body = JSON.stringify({
37013
+ activeTool,
37014
+ drafts,
37015
+ ...sheetWidth === null ? {} : { width: sheetWidth }
37016
+ });
37017
+ const keepalive = options.keepalive === true && new TextEncoder().encode(body).length <= KEEPALIVE_BODY_LIMIT_BYTES;
37018
+ const controller = new AbortController;
37019
+ if (!options.keepalive) {
37020
+ saveInFlight = true;
37021
+ saveController = controller;
37022
+ }
37023
+ deps.trackLoad(fetch("/_state/tools", {
37024
+ method: "PATCH",
37025
+ headers: deps.actionHeaders(),
37026
+ body,
37027
+ keepalive,
37028
+ signal: controller.signal
37029
+ })).then((res) => {
37030
+ if (res.ok) {
37031
+ clearSaveFailure();
37032
+ return;
37033
+ }
37034
+ if (isPermanentSaveFailure(res.status)) {
37035
+ reportSaveFailure();
37036
+ return;
37037
+ }
37038
+ throw new Error(`HTTP ${res.status}`);
37039
+ }).catch(() => {
37040
+ if (disposed)
37041
+ return;
37042
+ dirty = true;
37043
+ saveRetries += 1;
37044
+ if (saveRetries > SAVE_RETRY_LIMIT) {
37045
+ reportSaveFailure();
37046
+ return;
37047
+ }
37048
+ if (options.keepalive || saveRetryTimer)
37049
+ return;
37050
+ saveRetryTimer = setTimeout(() => {
37051
+ saveRetryTimer = null;
37052
+ save();
37053
+ }, SAVE_RETRY_MS);
37054
+ }).finally(() => {
37055
+ if (options.keepalive)
37056
+ return;
37057
+ saveInFlight = false;
37058
+ if (saveController === controller)
37059
+ saveController = null;
37060
+ if (disposed)
37061
+ return;
37062
+ if (dirty && !saveRetryTimer)
37063
+ save();
37064
+ });
37065
+ }
37066
+ function scheduleSave() {
37067
+ dirty = true;
37068
+ saveRetries = 0;
37069
+ if (saveTimer)
37070
+ clearTimeout(saveTimer);
37071
+ saveTimer = setTimeout(() => {
37072
+ saveTimer = null;
37073
+ save();
37074
+ }, SAVE_DEBOUNCE_MS);
37075
+ }
37076
+ function onDraftInput(tool, value) {
37077
+ drafts[tool] = value;
37078
+ toolTouched = true;
37079
+ scheduleSave();
37080
+ }
37081
+ function createPanes(current) {
37082
+ return {
37083
+ markdown: createMarkdownTool(current, drafts.markdown ?? "", (value) => onDraftInput("markdown", value)),
37084
+ mermaid: createMermaidTool(current, drafts.mermaid ?? "", (value) => onDraftInput("mermaid", value)),
37085
+ json: createJsonTool(current, drafts.json ?? "", (value) => onDraftInput("json", value))
37086
+ };
37087
+ }
37088
+ function setActiveTool(tool, notify) {
37089
+ activeTool = tool;
37090
+ if (!panes || !tabs)
37091
+ return;
37092
+ for (const id of TOOL_IDS) {
37093
+ tabs[id].classList.toggle("active", id === tool);
37094
+ tabs[id].setAttribute("aria-pressed", String(id === tool));
37095
+ panes[id].el.hidden = id !== tool;
37096
+ }
37097
+ panes[tool].refresh();
37098
+ panes[tool].focus();
37099
+ if (notify)
37100
+ deps.onToolChange?.(tool);
37101
+ if (savedActiveTool === tool)
37102
+ return;
37103
+ savedActiveTool = tool;
37104
+ scheduleSave();
37105
+ }
37106
+ function applySheetWidth(host, width) {
37107
+ const clamped = Math.round(Math.max(MIN_TOOLS_SHEET_WIDTH, Math.min(MAX_TOOLS_SHEET_WIDTH, width)));
37108
+ sheetWidth = clamped;
37109
+ host.style.setProperty("--tools-sheet-width", `${clamped}px`);
37110
+ }
37111
+ function mount(host) {
37112
+ if (panes)
37113
+ return;
37114
+ const current = text3();
37115
+ host.replaceChildren();
37116
+ const sheetResizer = document.createElement("div");
37117
+ sheetResizer.className = "tools-sheet-resizer";
37118
+ sheetResizer.role = "separator";
37119
+ sheetResizer.tabIndex = 0;
37120
+ sheetResizer.setAttribute("aria-orientation", "vertical");
37121
+ sheetResizer.setAttribute("aria-label", current.resizeSheet);
37122
+ attachDragResizer({
37123
+ handle: sheetResizer,
37124
+ getSize: () => host.getBoundingClientRect().width,
37125
+ applySize: (width) => {
37126
+ widthTouched = true;
37127
+ applySheetWidth(host, width);
37128
+ },
37129
+ direction: -1,
37130
+ onEnd: scheduleSave,
37131
+ activeClassTarget: host,
37132
+ activeClassName: "tools-sheet-resizing"
37133
+ });
37134
+ const header = document.createElement("header");
37135
+ header.className = "tools-header";
37136
+ titleEl = document.createElement("h1");
37137
+ titleEl.textContent = current.title;
37138
+ const tabList = document.createElement("div");
37139
+ tabList.className = "seg tools-tabs";
37140
+ tabList.role = "group";
37141
+ tabList.setAttribute("aria-label", current.title);
37142
+ closeBtn = document.createElement("button");
37143
+ closeBtn.type = "button";
37144
+ closeBtn.className = "tools-close";
37145
+ closeBtn.textContent = "×";
37146
+ closeBtn.title = current.close;
37147
+ closeBtn.setAttribute("aria-label", current.close);
37148
+ closeBtn.addEventListener("click", (event) => {
37149
+ event.preventDefault();
37150
+ deps.onCloseRequest?.();
37151
+ });
37152
+ header.append(titleEl, tabList, closeBtn);
37153
+ panes = createPanes(current);
37154
+ tabs = {};
37155
+ const body = document.createElement("div");
37156
+ body.className = "tools-body";
37157
+ for (const id of TOOL_IDS) {
37158
+ const tab = document.createElement("button");
37159
+ tab.type = "button";
37160
+ tab.textContent = current.tabs[id];
37161
+ tab.addEventListener("click", () => {
37162
+ toolTouched = true;
37163
+ setActiveTool(id, true);
37164
+ });
37165
+ tabs[id] = tab;
37166
+ tabList.appendChild(tab);
37167
+ panes[id].el.hidden = id !== activeTool;
37168
+ tab.classList.toggle("active", id === activeTool);
37169
+ body.appendChild(panes[id].el);
37170
+ }
37171
+ host.append(sheetResizer, header, body);
37172
+ }
37173
+ async function loadState(myGen) {
37174
+ if (stateLoaded)
37175
+ return;
37176
+ try {
37177
+ const res = await deps.trackLoad(fetch("/_state/tools"));
37178
+ if (myGen !== generation)
37179
+ return;
37180
+ if (!res.ok) {
37181
+ reportLoadFailure();
37182
+ return;
37183
+ }
37184
+ const data = await res.json();
37185
+ if (myGen !== generation)
37186
+ return;
37187
+ stateLoaded = true;
37188
+ if (loadFailed) {
37189
+ loadFailed = false;
37190
+ syncFailureStatus();
37191
+ }
37192
+ if (!toolTouched)
37193
+ savedActiveTool = data.activeTool ?? null;
37194
+ const host = getMount();
37195
+ if (host && !widthTouched && typeof data.width === "number")
37196
+ applySheetWidth(host, data.width);
37197
+ for (const id of TOOL_IDS) {
37198
+ const draft = data.drafts?.[id];
37199
+ if (typeof draft !== "string")
37200
+ continue;
37201
+ if (id in drafts)
37202
+ continue;
37203
+ drafts[id] = draft;
37204
+ panes?.[id].setText(draft);
37205
+ }
37206
+ } catch {
37207
+ if (myGen === generation)
37208
+ reportLoadFailure();
37209
+ }
37210
+ }
37211
+ function isOpen() {
37212
+ const host = getMount();
37213
+ return host ? !host.hidden : false;
37214
+ }
37215
+ async function open(tool) {
37216
+ const host = getMount();
37217
+ if (!host)
37218
+ return;
37219
+ const myGen = ++generation;
37220
+ mount(host);
37221
+ host.hidden = false;
37222
+ host.setAttribute("aria-hidden", "false");
37223
+ host.removeAttribute("inert");
37224
+ const overlay = getOverlay();
37225
+ if (overlay) {
37226
+ overlay.hidden = false;
37227
+ overlay.setAttribute("aria-hidden", "false");
37228
+ }
37229
+ document.body.classList.add("tools-sheet-open");
37230
+ await loadState(myGen);
37231
+ if (myGen !== generation || !isOpen())
37232
+ return;
37233
+ setActiveTool(tool ?? savedActiveTool ?? DEFAULT_TOOL_ID, !tool);
37234
+ }
37235
+ function close() {
37236
+ const host = getMount();
37237
+ if (!host)
37238
+ return;
37239
+ host.hidden = true;
37240
+ host.setAttribute("aria-hidden", "true");
37241
+ host.setAttribute("inert", "");
37242
+ const overlay = getOverlay();
37243
+ if (overlay) {
37244
+ overlay.hidden = true;
37245
+ overlay.setAttribute("aria-hidden", "true");
37246
+ }
37247
+ document.body.classList.remove("tools-sheet-open");
37248
+ generation += 1;
37249
+ save();
37250
+ }
37251
+ function localize() {
37252
+ if (!panes || !tabs)
37253
+ return;
37254
+ const current = text3();
37255
+ if (titleEl)
37256
+ titleEl.textContent = current.title;
37257
+ if (closeBtn) {
37258
+ closeBtn.title = current.close;
37259
+ closeBtn.setAttribute("aria-label", current.close);
37260
+ }
37261
+ for (const id of TOOL_IDS)
37262
+ tabs[id].textContent = current.tabs[id];
37263
+ panes.markdown.localize(current, current.markdown.output, current.markdown.placeholder);
37264
+ panes.mermaid.localize(current, current.mermaid.output, current.mermaid.placeholder);
37265
+ panes.json.localize(current, current.json.output, current.json.placeholder);
37266
+ for (const id of TOOL_IDS)
37267
+ panes[id].localizeActions?.(current);
37268
+ panes[activeTool].refresh();
37269
+ syncFailureStatus();
37270
+ }
37271
+ const onBeforeUnload = () => {
37272
+ if (dirty || saveInFlight)
37273
+ save({ keepalive: true });
37274
+ };
37275
+ window.addEventListener("beforeunload", onBeforeUnload);
37276
+ return {
37277
+ open,
37278
+ close,
37279
+ isOpen,
37280
+ getActiveTool: () => activeTool,
37281
+ localize,
37282
+ dispose() {
37283
+ disposed = true;
37284
+ generation += 1;
37285
+ saveController?.abort();
37286
+ saveController = null;
37287
+ window.removeEventListener("beforeunload", onBeforeUnload);
37288
+ if (saveTimer)
37289
+ clearTimeout(saveTimer);
37290
+ saveTimer = null;
37291
+ if (saveRetryTimer)
37292
+ clearTimeout(saveRetryTimer);
37293
+ saveRetryTimer = null;
37294
+ if (!panes)
37295
+ return;
37296
+ for (const id of TOOL_IDS)
37297
+ panes[id].dispose();
37298
+ }
37299
+ };
37300
+ }
37301
+
36183
37302
  // web-src/app.ts
36184
37303
  window.GdpExpandLogic = GdpExpandLogic;
36185
37304
  (() => {
@@ -36593,6 +37712,11 @@ code-viewer query agent-help`
36593
37712
  SERVER_SCOPE_WATCH_LIMIT_MIN = settings.scope.watch_limit_min;
36594
37713
  if (typeof settings.scope.watch_limit_max === "number")
36595
37714
  SERVER_SCOPE_WATCH_LIMIT_MAX = settings.scope.watch_limit_max;
37715
+ if (typeof settings.scope.watch_recursive === "boolean") {
37716
+ const watchSection = document.querySelector("#watch-settings-section");
37717
+ if (watchSection)
37718
+ watchSection.hidden = settings.scope.watch_recursive;
37719
+ }
36596
37720
  return settings;
36597
37721
  } catch {
36598
37722
  return null;
@@ -37009,6 +38133,7 @@ code-viewer query agent-help`
37009
38133
  history: "History",
37010
38134
  journal: "Work Log",
37011
38135
  database: "Datastores",
38136
+ tools: "Tools",
37012
38137
  help: "Help"
37013
38138
  },
37014
38139
  global: {
@@ -37302,6 +38427,7 @@ code-viewer query agent-help`
37302
38427
  history: "履歴",
37303
38428
  journal: "ワークログ",
37304
38429
  database: "データストア",
38430
+ tools: "ツール",
37305
38431
  help: "ヘルプ"
37306
38432
  },
37307
38433
  global: {
@@ -37616,11 +38742,6 @@ code-viewer query agent-help`
37616
38742
  if (route && text3.nav[route])
37617
38743
  link2.textContent = text3.nav[route];
37618
38744
  });
37619
- const helpLink = document.querySelector(".global-icon-link[data-route='help']");
37620
- if (helpLink) {
37621
- helpLink.title = text3.nav.help;
37622
- helpLink.setAttribute("aria-label", text3.nav.help);
37623
- }
37624
38745
  const repoWebLink = document.querySelector("#repo-web-link");
37625
38746
  if (repoWebLink) {
37626
38747
  repoWebLink.title = text3.global.repoWebLink;
@@ -37656,6 +38777,12 @@ code-viewer query agent-help`
37656
38777
  doctorBtn.setAttribute("aria-label", doctorTitle);
37657
38778
  }
37658
38779
  document.querySelector("#doctor-sheet")?.setAttribute("aria-label", doctorTitle);
38780
+ const toolsChrome = toolsText(STATE.language);
38781
+ const toolsBtn = document.querySelector("#tools-btn");
38782
+ if (toolsBtn)
38783
+ toolsBtn.title = toolsChrome.open;
38784
+ document.querySelector("#tools-sheet")?.setAttribute("aria-label", toolsChrome.title);
38785
+ relocalizeTools?.();
37659
38786
  const copyAiContext = document.querySelector("#copy-ai-context");
37660
38787
  if (copyAiContext) {
37661
38788
  copyAiContext.title = text3.global.copyAiContext;
@@ -37817,6 +38944,7 @@ code-viewer query agent-help`
37817
38944
  }
37818
38945
  let relocalizeHistory = null;
37819
38946
  let relocalizeJournal = null;
38947
+ let relocalizeTools = null;
37820
38948
  let relocalizeDatabase = null;
37821
38949
  let QUICK_HELP = null;
37822
38950
  function setViewerLanguage(language, persist = true) {
@@ -38277,8 +39405,11 @@ code-viewer query agent-help`
38277
39405
  function withAnnotationSessionParam(rawUrl) {
38278
39406
  return ANNOTATIONS_UI ? ANNOTATIONS_UI.withSessionParam(rawUrl) : rawUrl;
38279
39407
  }
39408
+ function withOverlayState(url) {
39409
+ return withToolsOverlay(withDoctorOverlay(url, parseDoctorOverlay(window.location.pathname, window.location.search)), parseToolsOverlay(window.location.search));
39410
+ }
38280
39411
  function urlForRoute(route) {
38281
- return withDoctorOverlay(withAnnotationSessionParam(buildRoute(route)), parseDoctorOverlay(window.location.pathname, window.location.search));
39412
+ return withOverlayState(withAnnotationSessionParam(buildRoute(route)));
38282
39413
  }
38283
39414
  function historyStateForRoute(route) {
38284
39415
  return route.screen === "file" ? {
@@ -38289,8 +39420,7 @@ code-viewer query agent-help`
38289
39420
  } : { view: route.screen };
38290
39421
  }
38291
39422
  function replaceUrlWithCurrentRoute() {
38292
- const base2 = withAnnotationSessionParam(buildRoute(STATE.route));
38293
- const url = withDoctorOverlay(base2, parseDoctorOverlay(window.location.pathname, window.location.search));
39423
+ const url = urlForRoute(STATE.route);
38294
39424
  const current = window.location.pathname + window.location.search;
38295
39425
  if (url !== current) {
38296
39426
  history.replaceState(historyStateForRoute(STATE.route), "", url + window.location.hash);
@@ -38438,51 +39568,57 @@ code-viewer query agent-help`
38438
39568
  applyHideTests();
38439
39569
  }
38440
39570
  function syncHeaderMenu() {
38441
- document.querySelectorAll(".app-menu-item, .global-icon-link").forEach((link2) => {
39571
+ const toolsBtn = document.querySelector("#tools-btn");
39572
+ if (toolsBtn) {
39573
+ const open = parseToolsOverlay(window.location.search) !== null;
39574
+ toolsBtn.classList.toggle("active", open);
39575
+ toolsBtn.setAttribute("aria-pressed", String(open));
39576
+ }
39577
+ document.querySelectorAll("a.app-menu-item, a.global-icon-link").forEach((link2) => {
38442
39578
  const fileRouteOwner = STATE.route.screen === "file" && (STATE.route.view === "blob" || STATE.route.view === "blame" || STATE.route.view === "history") ? "repo" : "diff";
38443
39579
  const active = link2.dataset.route === STATE.route.screen || STATE.route.screen === "file" && link2.dataset.route === fileRouteOwner;
38444
39580
  link2.classList.toggle("active", active);
38445
39581
  link2.setAttribute("aria-current", active ? "page" : "false");
38446
39582
  if (link2.dataset.route === "repo") {
38447
- link2.href = buildRoute({
39583
+ link2.href = withOverlayState(buildRoute({
38448
39584
  screen: "repo",
38449
39585
  ref: STATE.repoRef || "worktree",
38450
39586
  path: "",
38451
39587
  range: currentRange()
38452
- });
39588
+ }));
38453
39589
  }
38454
39590
  if (link2.dataset.route === "diff") {
38455
- link2.href = buildRoute({
39591
+ link2.href = withOverlayState(buildRoute({
38456
39592
  screen: "diff",
38457
39593
  range: preHistoryRange ?? currentRange()
38458
- });
39594
+ }));
38459
39595
  }
38460
39596
  if (link2.dataset.route === "history") {
38461
- link2.href = buildRoute({
39597
+ link2.href = withOverlayState(buildRoute({
38462
39598
  screen: "history",
38463
39599
  ref: "HEAD",
38464
39600
  range: currentRange()
38465
- });
39601
+ }));
38466
39602
  }
38467
39603
  if (link2.dataset.route === "journal") {
38468
- link2.href = buildRoute({
39604
+ link2.href = withOverlayState(buildRoute({
38469
39605
  screen: "journal",
38470
39606
  range: currentRange()
38471
- });
39607
+ }));
38472
39608
  }
38473
39609
  if (link2.dataset.route === "database") {
38474
- link2.href = buildRoute({
39610
+ link2.href = withOverlayState(buildRoute({
38475
39611
  screen: "database",
38476
39612
  range: currentRange()
38477
- });
39613
+ }));
38478
39614
  }
38479
39615
  if (link2.dataset.route === "help") {
38480
- link2.href = buildRoute({
39616
+ link2.href = withOverlayState(buildRoute({
38481
39617
  screen: "help",
38482
39618
  lang: STATE.route.screen === "help" ? helpLanguageFromRoute(STATE.route) : STATE.language,
38483
39619
  section: helpSectionFromRoute(STATE.route),
38484
39620
  range: currentRange()
38485
- });
39621
+ }));
38486
39622
  }
38487
39623
  });
38488
39624
  }
@@ -38692,10 +39828,6 @@ code-viewer query agent-help`
38692
39828
  if (quickHelpIcon) {
38693
39829
  quickHelpIcon.innerHTML = iconSvg("octicon-question", QUESTION_16_PATH);
38694
39830
  }
38695
- const helpLinkIcon = document.querySelector(".global-icon-link[data-route='help'] .goi-icon");
38696
- if (helpLinkIcon) {
38697
- helpLinkIcon.innerHTML = iconSvg("octicon-book", BOOK_16_PATH);
38698
- }
38699
39831
  const branchIcon = document.querySelector("#project-branch .goi-icon");
38700
39832
  if (branchIcon) {
38701
39833
  branchIcon.innerHTML = iconSvg("octicon-git-branch", GIT_BRANCH_16_PATH);
@@ -38737,6 +39869,10 @@ code-viewer query agent-help`
38737
39869
  event.preventDefault();
38738
39870
  toggleDoctorSheet();
38739
39871
  });
39872
+ $("#tools-btn")?.addEventListener("click", (event) => {
39873
+ event.preventDefault();
39874
+ toggleToolsSheet();
39875
+ });
38740
39876
  let copyAiContextFeedbackTimer = null;
38741
39877
  $("#copy-ai-context")?.addEventListener("click", async (event) => {
38742
39878
  const button = event.currentTarget;
@@ -39420,6 +40556,7 @@ code-viewer query agent-help`
39420
40556
  load();
39421
40557
  syncLineRefPill();
39422
40558
  syncDoctorSheetFromUrl();
40559
+ syncToolsSheetFromUrl();
39423
40560
  });
39424
40561
  function syncRefInputs() {
39425
40562
  const fi = $("#ref-from"), ti = $("#ref-to");
@@ -39595,6 +40732,57 @@ code-viewer query agent-help`
39595
40732
  event.preventDefault();
39596
40733
  closeDoctorSheet();
39597
40734
  });
40735
+ const TOOLS_VIEW = createToolsView({
40736
+ $: (sel) => document.querySelector(sel),
40737
+ trackLoad,
40738
+ getLanguage: () => STATE.language,
40739
+ actionHeaders,
40740
+ onCloseRequest: () => closeToolsSheet(),
40741
+ onToolChange: (tool) => updateUrlForToolsOverlay(tool)
40742
+ });
40743
+ relocalizeTools = () => TOOLS_VIEW.localize();
40744
+ function openToolsOverlay() {
40745
+ return parseToolsOverlay(window.location.search);
40746
+ }
40747
+ function updateUrlForToolsOverlay(tool) {
40748
+ const current = window.location.pathname + window.location.search;
40749
+ const next = withToolsOverlay(current, tool);
40750
+ if (next !== current) {
40751
+ history.replaceState(history.state, "", next + window.location.hash);
40752
+ }
40753
+ syncHeaderMenu();
40754
+ }
40755
+ function openToolsSheet(tool) {
40756
+ updateUrlForToolsOverlay(tool ?? TOOLS_VIEW.getActiveTool());
40757
+ TOOLS_VIEW.open(tool);
40758
+ }
40759
+ function closeToolsSheet() {
40760
+ TOOLS_VIEW.close();
40761
+ updateUrlForToolsOverlay(null);
40762
+ }
40763
+ function toggleToolsSheet() {
40764
+ if (openToolsOverlay() || TOOLS_VIEW.isOpen())
40765
+ closeToolsSheet();
40766
+ else
40767
+ openToolsSheet();
40768
+ }
40769
+ function syncToolsSheetFromUrl() {
40770
+ const tool = openToolsOverlay();
40771
+ const open = TOOLS_VIEW.isOpen();
40772
+ if (tool && (!open || TOOLS_VIEW.getActiveTool() !== tool))
40773
+ TOOLS_VIEW.open(tool);
40774
+ else if (!tool && open)
40775
+ TOOLS_VIEW.close();
40776
+ }
40777
+ document.getElementById("tools-sheet-overlay")?.addEventListener("click", () => closeToolsSheet());
40778
+ document.addEventListener("keydown", (event) => {
40779
+ if (event.key !== "Escape")
40780
+ return;
40781
+ if (!TOOLS_VIEW.isOpen())
40782
+ return;
40783
+ event.preventDefault();
40784
+ closeToolsSheet();
40785
+ });
39598
40786
  JOURNAL_VIEW = createJournalView({
39599
40787
  getRoute: () => STATE.route,
39600
40788
  setRoute,
@@ -39672,6 +40860,7 @@ code-viewer query agent-help`
39672
40860
  syncHeaderMenu();
39673
40861
  syncLineRefPill();
39674
40862
  syncDoctorSheetFromUrl();
40863
+ syncToolsSheetFromUrl();
39675
40864
  if (isSameBlobFileRoute(previousRoute, STATE.route) && routeBlobPreview(previousRoute) !== routeBlobPreview(STATE.route) && switchSourceTab(routeBlobPreview(STATE.route) ? "preview" : "code", {
39676
40865
  updateRoute: false
39677
40866
  })) {
@@ -39732,7 +40921,7 @@ code-viewer query agent-help`
39732
40921
  }
39733
40922
  window.addEventListener("popstate", applyRouteFromLocation);
39734
40923
  window.addEventListener("pagehide", () => flushViewStatePatch(true));
39735
- document.querySelectorAll(".app-menu-item, .global-icon-link").forEach((link2) => {
40924
+ document.querySelectorAll("a.app-menu-item, a.global-icon-link").forEach((link2) => {
39736
40925
  if (link2.target === "_blank")
39737
40926
  return;
39738
40927
  link2.addEventListener("click", (e2) => {
@@ -40079,7 +41268,7 @@ code-viewer query agent-help`
40079
41268
  return;
40080
41269
  if (isBlobOrBlameFileRoute(route)) {
40081
41270
  const viewingPath = route.path;
40082
- if (paths && viewingPath && !paths.has(viewingPath))
41271
+ if (viewingPath && !changedPathsCoverPath(paths, viewingPath))
40083
41272
  return;
40084
41273
  dispatchFileRoute(route, { refresh: true });
40085
41274
  return;
@@ -40127,7 +41316,7 @@ code-viewer query agent-help`
40127
41316
  const route = STATE.route;
40128
41317
  if (isBlobOrBlameFileRoute(route)) {
40129
41318
  const viewingPath = route.path;
40130
- if (paths && viewingPath && !paths.has(viewingPath))
41319
+ if (viewingPath && !changedPathsCoverPath(paths, viewingPath))
40131
41320
  return;
40132
41321
  }
40133
41322
  if (STATE.autoUpdate) {