@youtyan/code-viewer 0.8.1 → 0.8.3

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
@@ -10480,6 +10480,80 @@ ${frontmatter.yaml}
10480
10480
  return true;
10481
10481
  }
10482
10482
 
10483
+ // web-src/views/database/detail-table.ts
10484
+ function createDetailTable(headers, rows, emptyText) {
10485
+ const table2 = document.createElement("table");
10486
+ table2.className = "db-detail-table";
10487
+ const thead = document.createElement("thead");
10488
+ const headRow = document.createElement("tr");
10489
+ for (const label of headers) {
10490
+ const th = document.createElement("th");
10491
+ th.textContent = label;
10492
+ headRow.appendChild(th);
10493
+ }
10494
+ thead.appendChild(headRow);
10495
+ table2.appendChild(thead);
10496
+ const tbody = document.createElement("tbody");
10497
+ if (rows.length === 0) {
10498
+ const row = document.createElement("tr");
10499
+ const td = document.createElement("td");
10500
+ td.colSpan = headers.length;
10501
+ td.className = "db-value-empty";
10502
+ td.textContent = emptyText;
10503
+ row.appendChild(td);
10504
+ tbody.appendChild(row);
10505
+ }
10506
+ for (const cells of rows) {
10507
+ const row = document.createElement("tr");
10508
+ cells.forEach((cellText, index) => {
10509
+ const td = document.createElement("td");
10510
+ td.textContent = cellText;
10511
+ td.className = index === 0 ? "db-detail-table-primary" : "db-detail-table-muted";
10512
+ row.appendChild(td);
10513
+ });
10514
+ tbody.appendChild(row);
10515
+ }
10516
+ table2.appendChild(tbody);
10517
+ return table2;
10518
+ }
10519
+
10520
+ // web-src/views/database/detail-tabs.ts
10521
+ function createDetailTabs(specs, initial, onSelect) {
10522
+ const tabsEl = document.createElement("div");
10523
+ tabsEl.className = "db-detail-tabs";
10524
+ const buttons = {};
10525
+ const bodies = {};
10526
+ let active = initial;
10527
+ function setActive(tab) {
10528
+ active = tab;
10529
+ for (const spec of specs) {
10530
+ buttons[spec.id].classList.toggle("active", spec.id === tab);
10531
+ bodies[spec.id].hidden = spec.id !== tab;
10532
+ }
10533
+ }
10534
+ for (const spec of specs) {
10535
+ const btn = document.createElement("button");
10536
+ btn.type = "button";
10537
+ btn.className = "db-detail-tab";
10538
+ btn.textContent = spec.label;
10539
+ btn.addEventListener("click", () => {
10540
+ setActive(spec.id);
10541
+ onSelect(spec.id);
10542
+ });
10543
+ buttons[spec.id] = btn;
10544
+ tabsEl.appendChild(btn);
10545
+ const body = document.createElement("div");
10546
+ body.className = "db-detail-tab-body";
10547
+ bodies[spec.id] = body;
10548
+ }
10549
+ setActive(initial);
10550
+ function setLabels(labels) {
10551
+ for (const spec of specs)
10552
+ buttons[spec.id].textContent = labels[spec.id];
10553
+ }
10554
+ return { tabsEl, bodies, getActive: () => active, setActive, setLabels };
10555
+ }
10556
+
10483
10557
  // web-src/views/database/i18n.ts
10484
10558
  var EN = {
10485
10559
  nav: {
@@ -10820,7 +10894,21 @@ ${frontmatter.yaml}
10820
10894
  copied: "Copied",
10821
10895
  copyFailed: "Copy failed",
10822
10896
  invalidAttributeValues: "Invalid attribute values JSON",
10823
- runQuery: "Run"
10897
+ runQuery: "Run",
10898
+ structureTab: "Structure",
10899
+ itemTab: "Item",
10900
+ selectTable: "Select a table to view its structure.",
10901
+ attributeHeader: "Attribute",
10902
+ typeHeader: "Type",
10903
+ keyRoleHeader: "Key",
10904
+ noAttributes: "(no attributes)",
10905
+ globalSecondaryIndexes: "Global secondary indexes",
10906
+ localSecondaryIndexes: "Local secondary indexes",
10907
+ projectionAll: "ALL",
10908
+ projectionKeysOnly: "KEYS_ONLY",
10909
+ projectionInclude: (attrs) => `INCLUDE (${attrs})`,
10910
+ keySchemaOnlyHint: "DynamoDB only enforces types for key attributes. Additional attributes will appear here once items are loaded.",
10911
+ inferredAttributesNote: (count) => `Attributes beyond the key schema are inferred from ${count.toLocaleString()} loaded item${count === 1 ? "" : "s"} and may not reflect every item.`
10824
10912
  }
10825
10913
  }
10826
10914
  };
@@ -11163,7 +11251,21 @@ ${frontmatter.yaml}
11163
11251
  copied: "コピーしました",
11164
11252
  copyFailed: "コピーに失敗しました",
11165
11253
  invalidAttributeValues: "属性値の JSON が不正です",
11166
- runQuery: "実行"
11254
+ runQuery: "実行",
11255
+ structureTab: "構造",
11256
+ itemTab: "アイテム",
11257
+ selectTable: "テーブルを選択すると構造が表示されます。",
11258
+ attributeHeader: "属性",
11259
+ typeHeader: "型",
11260
+ keyRoleHeader: "キー",
11261
+ noAttributes: "(属性がありません)",
11262
+ globalSecondaryIndexes: "グローバルセカンダリインデックス",
11263
+ localSecondaryIndexes: "ローカルセカンダリインデックス",
11264
+ projectionAll: "ALL",
11265
+ projectionKeysOnly: "KEYS_ONLY",
11266
+ projectionInclude: (attrs) => `INCLUDE (${attrs})`,
11267
+ keySchemaOnlyHint: "DynamoDBがスキーマとして強制するのはキー属性のみです。アイテムを読み込むと追加の属性がここに表示されます。",
11268
+ inferredAttributesNote: (count) => `キー以外の属性は、読み込み済みの${count.toLocaleString()}件のアイテムから検出したものです (全アイテムを網羅するとは限りません)。`
11167
11269
  }
11168
11270
  }
11169
11271
  };
@@ -11237,6 +11339,25 @@ ${frontmatter.yaml}
11237
11339
  out[k] = unwrapAttributeValue(v);
11238
11340
  return out;
11239
11341
  }
11342
+ var ATTRIBUTE_VALUE_TAG_ORDER = [
11343
+ "S",
11344
+ "N",
11345
+ "B",
11346
+ "BOOL",
11347
+ "NULL",
11348
+ "M",
11349
+ "L",
11350
+ "SS",
11351
+ "NS",
11352
+ "BS"
11353
+ ];
11354
+ function attributeValueTag(av) {
11355
+ for (const tag of ATTRIBUTE_VALUE_TAG_ORDER) {
11356
+ if (tag in av)
11357
+ return tag;
11358
+ }
11359
+ return "?";
11360
+ }
11240
11361
  function previewItem(item) {
11241
11362
  const entries = Object.entries(item).slice(0, 6);
11242
11363
  const parts = entries.map(([key, rawValue]) => {
@@ -11347,8 +11468,17 @@ ${frontmatter.yaml}
11347
11468
  moreBtn.hidden = true;
11348
11469
  itemListPane.appendChild(moreBtn);
11349
11470
  const detailPane = document.createElement("div");
11350
- detailPane.className = "dynamodb-detail-pane";
11351
- setPaneEmpty(detailPane, text3().dynamodb.selectItem);
11471
+ detailPane.className = "db-detail-pane";
11472
+ const detailTabs = createDetailTabs([
11473
+ { id: "structure", label: text3().dynamodb.structureTab },
11474
+ { id: "item", label: text3().dynamodb.itemTab }
11475
+ ], "structure", () => notifySelectionChange());
11476
+ detailPane.appendChild(detailTabs.tabsEl);
11477
+ const structureBody = detailTabs.bodies.structure;
11478
+ setPaneEmpty(structureBody, text3().dynamodb.selectTable);
11479
+ const itemBody = detailTabs.bodies.item;
11480
+ setPaneEmpty(itemBody, text3().dynamodb.selectItem);
11481
+ detailPane.append(structureBody, itemBody);
11352
11482
  container.append(itemListPane, detailPane);
11353
11483
  let currentDbId = null;
11354
11484
  let currentTable = null;
@@ -11360,6 +11490,7 @@ ${frontmatter.yaml}
11360
11490
  let currentItemKeyToken = null;
11361
11491
  let cumulativeShownCount = 0;
11362
11492
  let cumulativeScannedCount = 0;
11493
+ let lastRenderedItem = null;
11363
11494
  let disposed = false;
11364
11495
  let loadRunId = 0;
11365
11496
  let itemRunId = 0;
@@ -11384,6 +11515,9 @@ ${frontmatter.yaml}
11384
11515
  queryModeBtn.classList.toggle("active", mode === "query");
11385
11516
  keyConditionInput.hidden = mode !== "query";
11386
11517
  }
11518
+ function setDetailTab(tab) {
11519
+ detailTabs.setActive(tab);
11520
+ }
11387
11521
  function renderTables(tableNames, append = false) {
11388
11522
  if (!append) {
11389
11523
  tableList.innerHTML = "";
@@ -11436,10 +11570,51 @@ ${frontmatter.yaml}
11436
11570
  }
11437
11571
  itemList.appendChild(fragment);
11438
11572
  }
11439
- function renderTableInfo() {
11440
- detailPane.innerHTML = "";
11441
- if (!currentTableInfo)
11573
+ function projectionText(projection) {
11574
+ const t2 = text3().dynamodb;
11575
+ if (!projection?.ProjectionType)
11576
+ return "";
11577
+ if (projection.ProjectionType === "ALL")
11578
+ return t2.projectionAll;
11579
+ if (projection.ProjectionType === "KEYS_ONLY")
11580
+ return t2.projectionKeysOnly;
11581
+ return t2.projectionInclude((projection.NonKeyAttributes ?? []).join(", "));
11582
+ }
11583
+ function renderSecondaryIndexes(label, indexes) {
11584
+ if (!indexes || indexes.length === 0)
11442
11585
  return;
11586
+ const section = document.createElement("div");
11587
+ section.className = "dynamodb-index-section";
11588
+ const heading2 = document.createElement("div");
11589
+ heading2.className = "dynamodb-index-heading";
11590
+ heading2.textContent = label;
11591
+ section.appendChild(heading2);
11592
+ for (const ix of indexes) {
11593
+ const row = document.createElement("div");
11594
+ row.className = "dynamodb-index-row";
11595
+ const name = document.createElement("div");
11596
+ name.className = "dynamodb-index-name";
11597
+ name.textContent = ix.IndexName ?? "";
11598
+ const detail = document.createElement("div");
11599
+ detail.className = "dynamodb-index-detail";
11600
+ const keyPart = (ix.KeySchema ?? []).map((k) => `${k.AttributeName} (${k.KeyType})`).join(", ");
11601
+ detail.textContent = [
11602
+ keyPart,
11603
+ projectionText(ix.Projection),
11604
+ ix.ItemCount !== undefined ? `${ix.ItemCount.toLocaleString()} items` : undefined
11605
+ ].filter(Boolean).join(" / ");
11606
+ row.append(name, detail);
11607
+ section.appendChild(row);
11608
+ }
11609
+ structureBody.appendChild(section);
11610
+ }
11611
+ function renderTableStructure() {
11612
+ structureBody.innerHTML = "";
11613
+ if (!currentTableInfo) {
11614
+ setPaneEmpty(structureBody, text3().dynamodb.selectTable);
11615
+ return;
11616
+ }
11617
+ const t2 = text3().dynamodb;
11443
11618
  const header = document.createElement("div");
11444
11619
  header.className = "dynamodb-table-info-header";
11445
11620
  header.textContent = currentTableInfo.TableName ?? currentTable ?? "";
@@ -11447,26 +11622,54 @@ ${frontmatter.yaml}
11447
11622
  meta.className = "dynamodb-table-info-meta";
11448
11623
  meta.textContent = [
11449
11624
  currentTableInfo.TableStatus,
11450
- currentTableInfo.ItemCount !== undefined ? `${currentTableInfo.ItemCount.toLocaleString()} items` : undefined
11625
+ currentTableInfo.ItemCount !== undefined ? `${currentTableInfo.ItemCount.toLocaleString()} items` : undefined,
11626
+ currentTableInfo.TableSizeBytes !== undefined ? formatBytes(currentTableInfo.TableSizeBytes) : undefined,
11627
+ currentTableInfo.BillingModeSummary?.BillingMode
11451
11628
  ].filter(Boolean).join(" / ");
11452
- detailPane.append(header, meta);
11453
- const keySchema = currentTableInfo.KeySchema ?? [];
11454
- if (keySchema.length > 0) {
11455
- const keyList = document.createElement("div");
11456
- keyList.className = "dynamodb-table-info-keys";
11457
- keyList.textContent = keySchema.map((k) => `${k.AttributeName} (${k.KeyType})`).join(", ");
11458
- detailPane.appendChild(keyList);
11459
- }
11460
- const empty = document.createElement("div");
11461
- empty.className = "db-pane-empty dynamodb-table-info-empty";
11462
- const emptyTitle = document.createElement("div");
11463
- emptyTitle.className = "db-pane-empty-title";
11464
- emptyTitle.textContent = text3().dynamodb.selectItem;
11465
- empty.appendChild(emptyTitle);
11466
- detailPane.appendChild(empty);
11629
+ structureBody.append(header, meta);
11630
+ const attributeTypeByName = new Map((currentTableInfo.AttributeDefinitions ?? []).map((a2) => [
11631
+ a2.AttributeName,
11632
+ a2.AttributeType
11633
+ ]));
11634
+ const keyRoleByName = new Map((currentTableInfo.KeySchema ?? []).map((k) => [
11635
+ k.AttributeName,
11636
+ k.KeyType
11637
+ ]));
11638
+ const attrNames = [...attributeTypeByName.keys()];
11639
+ const inferredTypesByName = new Map;
11640
+ for (const item of itemsByKeyToken.values()) {
11641
+ for (const [attrName, av] of Object.entries(item)) {
11642
+ if (attributeTypeByName.has(attrName))
11643
+ continue;
11644
+ const set2 = inferredTypesByName.get(attrName) ?? new Set;
11645
+ set2.add(attributeValueTag(av));
11646
+ inferredTypesByName.set(attrName, set2);
11647
+ }
11648
+ }
11649
+ const inferredNames = [...inferredTypesByName.keys()].sort();
11650
+ const rows = [
11651
+ ...attrNames.map((name) => [
11652
+ name,
11653
+ attributeTypeByName.get(name) ?? "",
11654
+ keyRoleByName.get(name) ?? ""
11655
+ ]),
11656
+ ...inferredNames.map((name) => [
11657
+ name,
11658
+ [...inferredTypesByName.get(name) ?? []].join(", "),
11659
+ ""
11660
+ ])
11661
+ ];
11662
+ structureBody.appendChild(createDetailTable([t2.attributeHeader, t2.typeHeader, t2.keyRoleHeader], rows, t2.noAttributes));
11663
+ const note = document.createElement("div");
11664
+ note.className = "dynamodb-attr-note";
11665
+ note.textContent = inferredNames.length === 0 ? t2.keySchemaOnlyHint : t2.inferredAttributesNote(itemsByKeyToken.size);
11666
+ structureBody.appendChild(note);
11667
+ renderSecondaryIndexes(t2.globalSecondaryIndexes, currentTableInfo.GlobalSecondaryIndexes);
11668
+ renderSecondaryIndexes(t2.localSecondaryIndexes, currentTableInfo.LocalSecondaryIndexes);
11467
11669
  }
11468
11670
  function renderItemDetail(item) {
11469
- detailPane.innerHTML = "";
11671
+ lastRenderedItem = item;
11672
+ itemBody.innerHTML = "";
11470
11673
  const header = document.createElement("div");
11471
11674
  header.className = "dynamodb-item-detail-header";
11472
11675
  const title = document.createElement("span");
@@ -11489,7 +11692,7 @@ ${frontmatter.yaml}
11489
11692
  }
11490
11693
  });
11491
11694
  header.append(title, copyBtn, copyStatus);
11492
- detailPane.appendChild(header);
11695
+ itemBody.appendChild(header);
11493
11696
  const pre = document.createElement("pre");
11494
11697
  pre.className = "dynamodb-item-source";
11495
11698
  try {
@@ -11497,12 +11700,13 @@ ${frontmatter.yaml}
11497
11700
  } catch {
11498
11701
  pre.textContent = String(item);
11499
11702
  }
11500
- detailPane.appendChild(pre);
11703
+ itemBody.appendChild(pre);
11501
11704
  }
11502
11705
  function selectItem(item, token) {
11503
11706
  currentItemKeyToken = token ?? itemKeyToken(extractItemKey(item, currentTableInfo?.KeySchema));
11504
11707
  highlightActiveItem(currentItemKeyToken);
11505
11708
  renderItemDetail(item);
11709
+ setDetailTab("item");
11506
11710
  notifySelectionChange();
11507
11711
  }
11508
11712
  async function loadItems(append) {
@@ -11538,8 +11742,6 @@ ${frontmatter.yaml}
11538
11742
  cumulativeShownCount = 0;
11539
11743
  cumulativeScannedCount = 0;
11540
11744
  setPaneStatus(itemList, "Loading items...");
11541
- if (!currentTableInfo)
11542
- setPaneEmpty(detailPane, text3().dynamodb.selectItem);
11543
11745
  }
11544
11746
  try {
11545
11747
  const params = new URLSearchParams({
@@ -11581,6 +11783,8 @@ ${frontmatter.yaml}
11581
11783
  } else {
11582
11784
  appendItems(data.items);
11583
11785
  }
11786
+ if (currentTableInfo)
11787
+ renderTableStructure();
11584
11788
  currentNextToken = data.lastEvaluatedKey;
11585
11789
  moreBtn.hidden = !data.lastEvaluatedKey;
11586
11790
  cumulativeShownCount += data.items.length;
@@ -11602,21 +11806,29 @@ ${frontmatter.yaml}
11602
11806
  return;
11603
11807
  const slot = tableInfoGuard.start();
11604
11808
  const requestDbId = currentDbId;
11809
+ const isStaleRequest = () => disposed || slot.isStale() || requestDbId !== currentDbId || currentTable !== table2;
11605
11810
  try {
11606
11811
  const params = new URLSearchParams({ db: requestDbId, table: table2 });
11607
11812
  const res = await trackLoad(fetch(`/_db/dynamodb/table?${params}`, { signal: slot.signal }));
11608
- if (disposed || slot.isStale())
11813
+ if (isStaleRequest())
11609
11814
  return;
11610
- if (!res.ok)
11815
+ if (!res.ok) {
11816
+ const errText = await res.text();
11817
+ setPaneStatus(structureBody, `Error: ${errText || res.statusText}`, {
11818
+ error: true
11819
+ });
11611
11820
  return;
11821
+ }
11612
11822
  const data = await res.json();
11613
- if (disposed || slot.isStale() || requestDbId !== currentDbId || currentTable !== table2) {
11823
+ if (isStaleRequest())
11614
11824
  return;
11615
- }
11616
11825
  currentTableInfo = data.table;
11617
- if (!currentItemKeyToken)
11618
- renderTableInfo();
11619
- } catch {} finally {
11826
+ renderTableStructure();
11827
+ } catch (err) {
11828
+ if (isStaleRequest())
11829
+ return;
11830
+ setPaneStatus(structureBody, `Error: ${err instanceof Error ? err.message : String(err)}`, { error: true });
11831
+ } finally {
11620
11832
  slot.finish();
11621
11833
  }
11622
11834
  }
@@ -11656,7 +11868,14 @@ ${frontmatter.yaml}
11656
11868
  currentNextToken = undefined;
11657
11869
  highlightActiveTable(name);
11658
11870
  notifySelectionChange();
11659
- setPaneEmpty(detailPane, text3().dynamodb.selectItem);
11871
+ lastRenderedItem = null;
11872
+ itemsByKeyToken.clear();
11873
+ itemRowsByKeyToken.clear();
11874
+ structureBody.innerHTML = "";
11875
+ setPaneStatus(structureBody, "Loading table...");
11876
+ itemBody.innerHTML = "";
11877
+ setPaneEmpty(itemBody, text3().dynamodb.selectItem);
11878
+ setDetailTab("structure");
11660
11879
  await fetchTableInfo(name);
11661
11880
  await loadItems(false);
11662
11881
  }
@@ -11773,7 +11992,12 @@ ${frontmatter.yaml}
11773
11992
  activeItemRow = null;
11774
11993
  moreBtn.hidden = true;
11775
11994
  tableMoreBtn.hidden = true;
11776
- setPaneEmpty(detailPane, text3().dynamodb.selectItem);
11995
+ lastRenderedItem = null;
11996
+ structureBody.innerHTML = "";
11997
+ setPaneEmpty(structureBody, text3().dynamodb.selectTable);
11998
+ itemBody.innerHTML = "";
11999
+ setPaneEmpty(itemBody, text3().dynamodb.selectItem);
12000
+ setDetailTab("structure");
11777
12001
  setPaneStatus(tableList, "Loading tables...");
11778
12002
  try {
11779
12003
  const res = await trackLoad(fetch(`/_db/dynamodb/tables?db=${encodeURIComponent(dbId)}`, {
@@ -11809,6 +12033,8 @@ ${frontmatter.yaml}
11809
12033
  await selectItemByKey(key);
11810
12034
  } catch {}
11811
12035
  }
12036
+ if (initial?.detailTab)
12037
+ setDetailTab(initial.detailTab);
11812
12038
  } finally {
11813
12039
  suppressNotify = false;
11814
12040
  }
@@ -11854,7 +12080,12 @@ ${frontmatter.yaml}
11854
12080
  itemRowsByKeyToken.clear();
11855
12081
  activeItemRow = null;
11856
12082
  moreBtn.hidden = true;
11857
- setPaneEmpty(detailPane, text3().dynamodb.selectItem);
12083
+ lastRenderedItem = null;
12084
+ structureBody.innerHTML = "";
12085
+ setPaneEmpty(structureBody, text3().dynamodb.selectTable);
12086
+ itemBody.innerHTML = "";
12087
+ setPaneEmpty(itemBody, text3().dynamodb.selectItem);
12088
+ setDetailTab("structure");
11858
12089
  }
11859
12090
  setMode("scan");
11860
12091
  function getSelection() {
@@ -11865,7 +12096,8 @@ ${frontmatter.yaml}
11865
12096
  filterExpression: filterInput.value.trim() || undefined,
11866
12097
  expressionAttributeValues: attributeValuesInput.value.trim() || undefined,
11867
12098
  scanIndexForward: currentMode === "query" ? currentScanIndexForward : undefined,
11868
- itemKey: currentItemKeyToken ?? undefined
12099
+ itemKey: currentItemKeyToken ?? undefined,
12100
+ detailTab: detailTabs.getActive()
11869
12101
  };
11870
12102
  }
11871
12103
  function dispose() {
@@ -11883,12 +12115,18 @@ ${frontmatter.yaml}
11883
12115
  runBtn.textContent = t2.dynamodb.runQuery;
11884
12116
  tableMoreBtn.textContent = t2.common.loadMore;
11885
12117
  moreBtn.textContent = t2.common.loadMore;
11886
- if (!activeItemRow) {
11887
- if (currentTableInfo)
11888
- renderTableInfo();
11889
- else
11890
- setPaneEmpty(detailPane, t2.dynamodb.selectItem);
11891
- }
12118
+ detailTabs.setLabels({
12119
+ structure: t2.dynamodb.structureTab,
12120
+ item: t2.dynamodb.itemTab
12121
+ });
12122
+ if (currentTableInfo)
12123
+ renderTableStructure();
12124
+ else
12125
+ setPaneEmpty(structureBody, t2.dynamodb.selectTable);
12126
+ if (lastRenderedItem)
12127
+ renderItemDetail(lastRenderedItem);
12128
+ else
12129
+ setPaneEmpty(itemBody, t2.dynamodb.selectItem);
11892
12130
  }
11893
12131
  return {
11894
12132
  el: container,
@@ -11951,25 +12189,15 @@ ${frontmatter.yaml}
11951
12189
  docMoreBtn.hidden = true;
11952
12190
  docListPane.appendChild(docMoreBtn);
11953
12191
  const detailPane = document.createElement("div");
11954
- detailPane.className = "es-detail-pane";
11955
- const detailTabs = document.createElement("div");
11956
- detailTabs.className = "es-detail-tabs";
11957
- const tabMapping = document.createElement("button");
11958
- tabMapping.type = "button";
11959
- tabMapping.className = "es-detail-tab active";
11960
- tabMapping.textContent = text3().es.mapping;
11961
- const tabDoc = document.createElement("button");
11962
- tabDoc.type = "button";
11963
- tabDoc.className = "es-detail-tab";
11964
- tabDoc.textContent = text3().es.doc;
11965
- detailTabs.append(tabMapping, tabDoc);
11966
- detailPane.appendChild(detailTabs);
11967
- const mappingBody = document.createElement("div");
11968
- mappingBody.className = "es-mapping-body";
12192
+ detailPane.className = "db-detail-pane";
12193
+ const detailTabs = createDetailTabs([
12194
+ { id: "mapping", label: text3().es.mapping },
12195
+ { id: "doc", label: text3().es.doc }
12196
+ ], "mapping", () => {});
12197
+ detailPane.appendChild(detailTabs.tabsEl);
12198
+ const mappingBody = detailTabs.bodies.mapping;
11969
12199
  setPaneEmpty(mappingBody, text3().es.selectIndex);
11970
- const docBody = document.createElement("div");
11971
- docBody.className = "es-doc-body";
11972
- docBody.hidden = true;
12200
+ const docBody = detailTabs.bodies.doc;
11973
12201
  setPaneEmpty(docBody, text3().es.selectDoc);
11974
12202
  detailPane.append(mappingBody, docBody);
11975
12203
  container.append(docListPane, detailPane);
@@ -12094,13 +12322,8 @@ ${frontmatter.yaml}
12094
12322
  }
12095
12323
  function setDetailTab(tab) {
12096
12324
  detailTab = tab;
12097
- tabMapping.classList.toggle("active", tab === "mapping");
12098
- tabDoc.classList.toggle("active", tab === "doc");
12099
- mappingBody.hidden = tab !== "mapping";
12100
- docBody.hidden = tab !== "doc";
12325
+ detailTabs.setActive(tab);
12101
12326
  }
12102
- tabMapping.addEventListener("click", () => setDetailTab("mapping"));
12103
- tabDoc.addEventListener("click", () => setDetailTab("doc"));
12104
12327
  function renderMapping(resp) {
12105
12328
  lastMapping = resp;
12106
12329
  mappingBody.innerHTML = "";
@@ -12108,43 +12331,13 @@ ${frontmatter.yaml}
12108
12331
  header.className = "es-mapping-header";
12109
12332
  header.textContent = resp.mapping.index;
12110
12333
  mappingBody.appendChild(header);
12111
- const table2 = document.createElement("table");
12112
- table2.className = "es-mapping-table";
12113
- const thead = document.createElement("thead");
12114
- const headRow = document.createElement("tr");
12115
- for (const label of [text3().es.fieldHeader, text3().es.typeHeader]) {
12116
- const th = document.createElement("th");
12117
- th.textContent = label;
12118
- headRow.appendChild(th);
12119
- }
12120
- thead.appendChild(headRow);
12121
- table2.appendChild(thead);
12122
- const tbody = document.createElement("tbody");
12123
12334
  const props = resp.mapping.properties;
12124
12335
  const keys = Object.keys(props).sort();
12125
- if (keys.length === 0) {
12126
- const row = document.createElement("tr");
12127
- const td = document.createElement("td");
12128
- td.colSpan = 2;
12129
- td.className = "es-value-empty";
12130
- td.textContent = text3().es.noMappedFields;
12131
- row.appendChild(td);
12132
- tbody.appendChild(row);
12133
- }
12134
- for (const key of keys) {
12135
- const row = document.createElement("tr");
12136
- const fieldTd = document.createElement("td");
12137
- fieldTd.className = "es-mapping-field";
12138
- fieldTd.textContent = key;
12139
- const typeTd = document.createElement("td");
12140
- typeTd.className = "es-mapping-type";
12336
+ const rows = keys.map((key) => {
12141
12337
  const p2 = props[key];
12142
- typeTd.textContent = p2.type ?? (p2.properties ? "object" : "(unknown)");
12143
- row.append(fieldTd, typeTd);
12144
- tbody.appendChild(row);
12145
- }
12146
- table2.appendChild(tbody);
12147
- mappingBody.appendChild(table2);
12338
+ return [key, p2.type ?? (p2.properties ? "object" : "(unknown)")];
12339
+ });
12340
+ mappingBody.appendChild(createDetailTable([text3().es.fieldHeader, text3().es.typeHeader], rows, text3().es.noMappedFields));
12148
12341
  }
12149
12342
  function mkBtn(label, cls) {
12150
12343
  const b2 = document.createElement("button");
@@ -12649,11 +12842,10 @@ ${frontmatter.yaml}
12649
12842
  searchInput.placeholder = t2.es.queryPlaceholder;
12650
12843
  searchBtn.textContent = t2.common.search;
12651
12844
  docMoreBtn.textContent = t2.common.loadMore;
12652
- tabMapping.textContent = t2.es.mapping;
12653
- tabDoc.textContent = t2.es.doc;
12845
+ detailTabs.setLabels({ mapping: t2.es.mapping, doc: t2.es.doc });
12654
12846
  if (!currentIndex) {
12655
12847
  setPaneEmpty(mappingBody, t2.es.selectIndex);
12656
- } else if (lastMapping && mappingBody.querySelector(".es-mapping-table")) {
12848
+ } else if (lastMapping && mappingBody.querySelector(".db-detail-table")) {
12657
12849
  renderMapping(lastMapping);
12658
12850
  }
12659
12851
  if (!activeDocRow) {
@@ -21430,11 +21622,12 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
21430
21622
  dbSelect.value = target;
21431
21623
  currentDbInfo = files.find((f2) => f2.id === target) || null;
21432
21624
  syncConnectionActions();
21625
+ const forceReload = options.forceReload ?? false;
21433
21626
  const explorerInitial = {
21434
- redis: pendingRedisInitial,
21435
- es: pendingEsInitial,
21436
- s3: pendingS3Initial,
21437
- dynamodb: pendingDynamodbInitial
21627
+ redis: pendingRedisInitial ?? (forceReload && currentDbInfo?.kind === "redis" ? redisExplorer.getSelection() : undefined),
21628
+ es: pendingEsInitial ?? (forceReload && currentDbInfo?.kind === "elasticsearch" ? esExplorer.getSelection() : undefined),
21629
+ s3: pendingS3Initial ?? (forceReload && currentDbInfo?.kind === "s3" ? s3Explorer.getSelection() : undefined),
21630
+ dynamodb: pendingDynamodbInitial ?? (forceReload && currentDbInfo?.kind === "dynamodb" ? dynamodbExplorer.getSelection() : undefined)
21438
21631
  };
21439
21632
  pendingRedisInitial = undefined;
21440
21633
  pendingEsInitial = undefined;
@@ -21567,7 +21760,7 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
21567
21760
  }
21568
21761
  } else if (currentDbInfo?.kind === "dynamodb") {
21569
21762
  const sel = dynamodbExplorer.getSelection();
21570
- if (sel.table !== undefined || sel.mode !== "scan" || sel.keyConditionExpression !== undefined || sel.filterExpression !== undefined || sel.expressionAttributeValues !== undefined || sel.itemKey !== undefined) {
21763
+ if (sel.table !== undefined || sel.mode !== "scan" || sel.keyConditionExpression !== undefined || sel.filterExpression !== undefined || sel.expressionAttributeValues !== undefined || sel.itemKey !== undefined || sel.detailTab === "item") {
21571
21764
  state.dynamodb = sel;
21572
21765
  }
21573
21766
  }
@@ -22632,7 +22825,7 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
22632
22825
  if (!mounted || activeTabId !== id)
22633
22826
  return;
22634
22827
  const state = entry.pane.getState();
22635
- await entry.pane.enter(state.dbId ?? undefined, state.schema ?? undefined, state.table ?? undefined, state.view, { autoSelectFirst: state.dbId !== null });
22828
+ await entry.pane.enter(state.dbId ?? undefined, state.schema ?? undefined, state.table ?? undefined, state.view, { autoSelectFirst: state.dbId !== null, forceReload: true });
22636
22829
  if (!mounted || activeTabId !== id)
22637
22830
  return;
22638
22831
  refreshChipLabel(id);
@@ -25862,6 +26055,10 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
25862
26055
  {
25863
26056
  kind: "paragraph",
25864
26057
  text: "In large repositories the sidebar loads folder children on demand. Folders you open are remembered and automatically re-expanded after a reload."
26058
+ },
26059
+ {
26060
+ kind: "paragraph",
26061
+ text: 'Symlinks show a distinct icon and a "→ target" label so they are never mistaken for a regular file or folder, and clicking one navigates to its resolved target. A broken symlink is visually flagged and disabled. Files with pending git changes (new, modified, renamed, deleted) show a status badge in the tree instead of the regular type icon.'
25865
26062
  }
25866
26063
  ]
25867
26064
  },
@@ -26128,7 +26325,7 @@ code-viewer annotate add-db --db app.db --tab query \\
26128
26325
  ],
26129
26326
  [
26130
26327
  "DynamoDB / LocalStack",
26131
- "Detected when DynamoDB is enabled on a LocalStack compose service. List tables, inspect key schemas, scan or query items, follow pagination tokens, and open item details with a copyable key. Browsing is read-only."
26328
+ "Detected when DynamoDB is enabled on a LocalStack compose service. List tables, browse a Structure tab (key schema, GSI/LSI, and non-key attribute types inferred from loaded items), scan or query items, follow pagination tokens, and open item details with a copyable key. Browsing is read-only."
26132
26329
  ],
26133
26330
  [
26134
26331
  "S3 / MinIO / LocalStack",
@@ -26529,6 +26726,10 @@ code-viewer query agent-help`
26529
26726
  {
26530
26727
  kind: "paragraph",
26531
26728
  text: "大きいリポジトリではサイドバーがフォルダの中身を必要に応じて読み込みます。開いたフォルダは記憶され、次回のリロード時に同じ状態で展開し直されます。"
26729
+ },
26730
+ {
26731
+ kind: "paragraph",
26732
+ text: "シンボリックリンクは専用アイコンと「→ リンク先」ラベルで表示されるため通常のファイル/フォルダと区別でき、クリックするとリンク先に遷移します。リンク切れのシンボリックリンクは無効化されたことが分かる表示になります。未コミットの git 変更(新規・変更・リネーム・削除)があるファイルは、通常の種類アイコンの代わりにステータスバッジがツリーに表示されます。"
26532
26733
  }
26533
26734
  ]
26534
26735
  },
@@ -26795,7 +26996,7 @@ code-viewer annotate add-db --db app.db --tab query \\
26795
26996
  ],
26796
26997
  [
26797
26998
  "DynamoDB / LocalStack",
26798
- "LocalStack の compose サービスで DynamoDB が有効な場合に検出。テーブル一覧、キースキーマ、scan / query、継続トークンによるページング、コピー可能なキー付きのアイテム詳細を表示します。閲覧専用です。"
26999
+ "LocalStack の compose サービスで DynamoDB が有効な場合に検出。テーブル一覧、構造タブ(キースキーマ・GSI/LSI・読み込み済みアイテムから推測した非キー属性の型)、scan / query、継続トークンによるページング、コピー可能なキー付きのアイテム詳細を表示します。閲覧専用です。"
26799
27000
  ],
26800
27001
  [
26801
27002
  "S3 / MinIO / LocalStack",
@@ -30526,6 +30727,11 @@ code-viewer query agent-help`
30526
30727
  node.children_omitted = true;
30527
30728
  node.children_omitted_reason = f2.children_omitted_reason;
30528
30729
  }
30730
+ if (f2.is_symlink) {
30731
+ node.is_symlink = true;
30732
+ node.symlink_target = f2.symlink_target;
30733
+ node.resolved_path = f2.resolved_path;
30734
+ }
30529
30735
  continue;
30530
30736
  }
30531
30737
  node.files.push(f2);
@@ -30575,6 +30781,8 @@ code-viewer query agent-help`
30575
30781
  li.dataset.childrenOmittedReason = dir.children_omitted_reason;
30576
30782
  if (dir.explicit)
30577
30783
  li.dataset.explicit = "true";
30784
+ if (dir.is_symlink)
30785
+ li.classList.add("symlink-row");
30578
30786
  if (dir.children_omitted) {
30579
30787
  li.classList.add("children-omitted");
30580
30788
  li.classList.add(dir.children_omitted_reason === "heavy" ? "children-omitted-heavy" : "children-omitted-internal");
@@ -30608,6 +30816,9 @@ code-viewer query agent-help`
30608
30816
  omitted.title = badge.title;
30609
30817
  label.appendChild(omitted);
30610
30818
  }
30819
+ const dirSymlinkLabel = symlinkTargetLabel(dir);
30820
+ if (dirSymlinkLabel)
30821
+ label.appendChild(dirSymlinkLabel);
30611
30822
  li.appendChild(label);
30612
30823
  li.appendChild(createOpenPathButton(dir.path, "directory", openDirectoryInOsTitle()));
30613
30824
  const collapsed = STATE.collapsedDirs.has(dir.path);
@@ -30646,7 +30857,8 @@ code-viewer query agent-help`
30646
30857
  display_path: dir.path,
30647
30858
  type: "tree",
30648
30859
  children_omitted: dir.children_omitted,
30649
- children_omitted_reason: dir.children_omitted_reason
30860
+ children_omitted_reason: dir.children_omitted_reason,
30861
+ resolved_path: dir.resolved_path
30650
30862
  });
30651
30863
  scheduleMainSurfaceFocus();
30652
30864
  });
@@ -30709,6 +30921,11 @@ code-viewer query agent-help`
30709
30921
  node.children_omitted = true;
30710
30922
  node.children_omitted_reason = entry.children_omitted_reason;
30711
30923
  }
30924
+ if (entry.is_symlink) {
30925
+ node.is_symlink = true;
30926
+ node.symlink_target = entry.symlink_target;
30927
+ node.resolved_path = entry.resolved_path;
30928
+ }
30712
30929
  return;
30713
30930
  }
30714
30931
  if (!node.files.some((file) => file.path === entry.path))
@@ -30744,7 +30961,12 @@ code-viewer query agent-help`
30744
30961
  type: meta.ref === "worktree" && entry.type === "commit" && !entry.submodule ? "tree" : entry.type,
30745
30962
  submodule: entry.submodule,
30746
30963
  children_omitted: entry.children_omitted,
30747
- children_omitted_reason: entry.children_omitted_reason
30964
+ children_omitted_reason: entry.children_omitted_reason,
30965
+ is_symlink: entry.is_symlink,
30966
+ symlink_target: entry.symlink_target,
30967
+ symlink_target_type: entry.symlink_target_type,
30968
+ resolved_path: entry.resolved_path,
30969
+ status: entry.status
30748
30970
  }));
30749
30971
  mergeSidebarTreeEntries(entries);
30750
30972
  SIDEBAR_LAZY_LOADED_DIRS.add(dir.path);
@@ -30764,6 +30986,8 @@ code-viewer query agent-help`
30764
30986
  li.dataset.childrenOmittedReason = dir.children_omitted_reason;
30765
30987
  if (dir.explicit)
30766
30988
  li.dataset.explicit = "true";
30989
+ if (dir.is_symlink)
30990
+ li.classList.add("symlink-row");
30767
30991
  if (dir.children_omitted) {
30768
30992
  li.classList.add("children-omitted");
30769
30993
  li.classList.add(dir.children_omitted_reason === "heavy" ? "children-omitted-heavy" : "children-omitted-internal");
@@ -30797,6 +31021,9 @@ code-viewer query agent-help`
30797
31021
  omitted.title = badge.title;
30798
31022
  label.appendChild(omitted);
30799
31023
  }
31024
+ const dirSymlinkLabel = symlinkTargetLabel(dir);
31025
+ if (dirSymlinkLabel)
31026
+ label.appendChild(dirSymlinkLabel);
30800
31027
  li.appendChild(label);
30801
31028
  li.appendChild(createOpenPathButton(dir.path, "directory", openDirectoryInOsTitle()));
30802
31029
  const updateIcon = () => {
@@ -30884,16 +31111,38 @@ code-viewer query agent-help`
30884
31111
  return tag;
30885
31112
  }
30886
31113
  function sidebarEntryIcon(f2) {
31114
+ if (f2.is_symlink)
31115
+ return iconSvg("octicon-link", LINK_16_PATH);
30887
31116
  return f2.type === "commit" ? iconSvg("octicon-git-branch", GIT_BRANCH_16_PATH) : fileEntryIcon();
30888
31117
  }
31118
+ function symlinkTargetLabel(f2) {
31119
+ if (!f2.is_symlink)
31120
+ return null;
31121
+ const broken = f2.symlink_target_type === "missing";
31122
+ const label = document.createElement("span");
31123
+ label.className = broken ? "symlink-target broken" : "symlink-target";
31124
+ label.textContent = `→ ${f2.symlink_target || "?"}`;
31125
+ label.title = broken ? `Broken symlink → ${f2.symlink_target || ""}` : `Symlink → ${f2.symlink_target || ""}`;
31126
+ return label;
31127
+ }
30889
31128
  function createTreeFileRow(f2, depth, onFileClick) {
30890
31129
  const li = document.createElement("li");
30891
31130
  li.className = "tree-file";
30892
31131
  li.tabIndex = -1;
30893
31132
  li.dataset.path = f2.path;
30894
31133
  li.dataset.type = f2.type || "blob";
31134
+ const brokenSymlink = f2.is_symlink && f2.symlink_target_type === "missing";
31135
+ const deletedEntry = !!onFileClick && f2.status === "D";
31136
+ if (f2.is_symlink)
31137
+ li.classList.add("symlink-row");
30895
31138
  if (f2.type === "commit") {
30896
31139
  li.title = commitEntryBadge(f2.submodule).title;
31140
+ } else if (brokenSymlink) {
31141
+ li.classList.add("symlink-broken-row", "gdp-row-disabled");
31142
+ li.setAttribute("aria-disabled", "true");
31143
+ } else if (deletedEntry) {
31144
+ li.classList.add("gdp-row-disabled");
31145
+ li.setAttribute("aria-disabled", "true");
30897
31146
  }
30898
31147
  li.classList.toggle("viewed", !onFileClick && STATE.viewedFiles.has(f2.path));
30899
31148
  li.classList.toggle("hidden-by-tests", STATE.hideTests && !isRepositorySidebarMode() && isTestPath(f2.path || ""));
@@ -30914,17 +31163,22 @@ code-viewer query agent-help`
30914
31163
  name.textContent = f2.path.split("/").pop();
30915
31164
  name.title = f2.path;
30916
31165
  li.appendChild(name);
31166
+ const symlinkLabel = symlinkTargetLabel(f2);
31167
+ if (symlinkLabel)
31168
+ li.appendChild(symlinkLabel);
30917
31169
  const kindTag = fileKindTag(f2);
30918
31170
  if (kindTag)
30919
31171
  li.appendChild(kindTag);
30920
31172
  li.addEventListener("click", () => {
31173
+ if (brokenSymlink || deletedEntry)
31174
+ return;
30921
31175
  if (onFileClick)
30922
31176
  onFileClick(f2);
30923
31177
  else
30924
31178
  scrollToFile(f2.path);
30925
31179
  scheduleMainSurfaceFocus();
30926
31180
  });
30927
- if (!onFileClick)
31181
+ if (!onFileClick && !brokenSymlink)
30928
31182
  li.addEventListener("mouseenter", () => prefetchByPath(f2.path), {
30929
31183
  passive: true
30930
31184
  });
@@ -31829,7 +32083,8 @@ code-viewer query agent-help`
31829
32083
  sortColumnLabels,
31830
32084
  repositoryFallback,
31831
32085
  repositoryRootFallback,
31832
- commitEntryMeta
32086
+ commitEntryMeta,
32087
+ fileBadge
31833
32088
  } = deps;
31834
32089
  let REPO_SORT = {
31835
32090
  key: "name",
@@ -31875,9 +32130,21 @@ code-viewer query agent-help`
31875
32130
  function commitEntryIcon() {
31876
32131
  return iconSvg("octicon-git-branch", GIT_BRANCH_16_PATH);
31877
32132
  }
32133
+ function symlinkEntryIcon() {
32134
+ return iconSvg("octicon-link", LINK_16_PATH);
32135
+ }
31878
32136
  function isWorktreeRef(ref) {
31879
32137
  return canTrashWorktreeRef(ref);
31880
32138
  }
32139
+ function repoEntryTypeIcon(entry, browsable, nonBrowsableCommit) {
32140
+ const icon = document.createElement("span");
32141
+ icon.className = browsable ? "dir-icon" : nonBrowsableCommit ? "d2h-icon-wrapper gdp-repo-row-gitlink-icon" : "d2h-icon-wrapper";
32142
+ if (browsable)
32143
+ setFolderIcon(icon, true);
32144
+ else
32145
+ icon.innerHTML = entry.is_symlink ? symlinkEntryIcon() : entry.type === "commit" ? commitEntryIcon() : fileEntryIcon();
32146
+ return icon;
32147
+ }
31881
32148
  function canBrowseRepoEntry(entry, ref) {
31882
32149
  return entry.type === "tree" || entry.type === "commit" && isWorktreeRef(ref) && !entry.submodule;
31883
32150
  }
@@ -32255,28 +32522,31 @@ code-viewer query agent-help`
32255
32522
  sortedRepoEntries(meta.entries, meta.ref).forEach((entry) => {
32256
32523
  const browsable = canBrowseRepoEntry(entry, meta.ref);
32257
32524
  const nonBrowsableCommit = entry.type === "commit" && !browsable;
32525
+ const brokenSymlink = !!entry.is_symlink && entry.symlink_target_type === "missing";
32526
+ const deletedEntry = entry.status === "D";
32258
32527
  const row = document.createElement("button");
32259
32528
  row.type = "button";
32260
- row.className = nonBrowsableCommit ? `gdp-repo-row ${entry.type} gdp-repo-row-gitlink` : `gdp-repo-row ${entry.type}`;
32261
- const icon = document.createElement("span");
32262
- icon.className = browsable ? "dir-icon" : nonBrowsableCommit ? "d2h-icon-wrapper gdp-repo-row-gitlink-icon" : "d2h-icon-wrapper";
32263
- if (browsable)
32264
- setFolderIcon(icon, true);
32265
- else
32266
- icon.innerHTML = entry.type === "commit" ? commitEntryIcon() : fileEntryIcon();
32529
+ row.className = nonBrowsableCommit ? `gdp-repo-row ${entry.type} gdp-repo-row-gitlink` : entry.is_symlink ? `gdp-repo-row ${entry.type} symlink-row${brokenSymlink ? " symlink-broken-row gdp-row-disabled" : ""}` : `gdp-repo-row ${entry.type}`;
32530
+ if (deletedEntry)
32531
+ row.classList.add("gdp-row-disabled");
32532
+ const icon = entry.status ? fileBadge(entry.status) : repoEntryTypeIcon(entry, browsable, nonBrowsableCommit);
32267
32533
  const name = document.createElement("span");
32268
32534
  name.className = "name";
32269
32535
  name.textContent = entry.name;
32270
32536
  if (nonBrowsableCommit) {
32271
32537
  row.title = commitEntryMeta(entry.submodule).title;
32272
32538
  row.setAttribute("aria-disabled", "true");
32539
+ } else if (brokenSymlink || deletedEntry) {
32540
+ row.setAttribute("aria-disabled", "true");
32273
32541
  }
32274
32542
  const metaBlock = createRepoEntryMeta(entry, browsable);
32275
32543
  const size = createRepoEntrySize(entry);
32276
32544
  row.append(icon, name, metaBlock, size);
32277
32545
  row.addEventListener("click", () => {
32546
+ if (brokenSymlink || deletedEntry)
32547
+ return;
32278
32548
  if (browsable) {
32279
- setRoute(repoRoute(meta.ref, entry.path));
32549
+ setRoute(repoRoute(meta.ref, entry.resolved_path ?? entry.path));
32280
32550
  loadRepo();
32281
32551
  } else if (entry.type === "blob") {
32282
32552
  setRoute({
@@ -32385,12 +32655,17 @@ code-viewer query agent-help`
32385
32655
  type: canBrowseRepoEntry(entry, normalizedRef) ? "tree" : entry.type,
32386
32656
  submodule: entry.submodule,
32387
32657
  children_omitted: entry.children_omitted,
32388
- children_omitted_reason: entry.children_omitted_reason
32658
+ children_omitted_reason: entry.children_omitted_reason,
32659
+ is_symlink: entry.is_symlink,
32660
+ symlink_target: entry.symlink_target,
32661
+ symlink_target_type: entry.symlink_target_type,
32662
+ resolved_path: entry.resolved_path,
32663
+ status: entry.status
32389
32664
  }));
32390
32665
  setRepoSidebarRef(normalizedRef);
32391
32666
  renderSidebar(files, (file) => {
32392
32667
  if (file.type === "tree") {
32393
- setRoute(repoRoute(normalizedRef, file.path));
32668
+ setRoute(repoRoute(normalizedRef, file.resolved_path ?? file.path));
32394
32669
  loadRepo();
32395
32670
  return;
32396
32671
  }
@@ -32456,6 +32731,15 @@ code-viewer query agent-help`
32456
32731
  meta.title = badge.title;
32457
32732
  return meta;
32458
32733
  }
32734
+ if (entry.is_symlink) {
32735
+ const broken = entry.symlink_target_type === "missing";
32736
+ meta.classList.add("symlink-target");
32737
+ if (broken)
32738
+ meta.classList.add("broken");
32739
+ meta.textContent = `→ ${entry.symlink_target || "?"}`;
32740
+ meta.title = broken ? `Broken symlink → ${entry.symlink_target || ""}` : `Symlink → ${entry.symlink_target || ""}`;
32741
+ return meta;
32742
+ }
32459
32743
  const updated = formatFileDate(entry.updated_at || entry.commit_updated_at);
32460
32744
  const created = formatFileDate(entry.created_at);
32461
32745
  if (browsable && updated) {
@@ -35884,7 +36168,8 @@ code-viewer query agent-help`
35884
36168
  commitEntryMeta: (submodule) => {
35885
36169
  const text3 = uiText().repo;
35886
36170
  return submodule ? { label: text3.submoduleLabel, title: text3.submoduleTitle } : { label: text3.gitlinkLabel, title: text3.gitlinkTitle };
35887
- }
36171
+ },
36172
+ fileBadge: (status) => DIFF_VIEW.fileBadge(status)
35888
36173
  });
35889
36174
  const {
35890
36175
  loadRepo,
@@ -36172,9 +36457,9 @@ code-viewer query agent-help`
36172
36457
  displaySource: "Applies to all projects in this browser.",
36173
36458
  excludedDirectories: "Excluded directories",
36174
36459
  omitDirs: "Skip these directory names while browsing and searching",
36175
- omitDirsHelp: "Reads no contents inside these directories. Applies to the sidebar (Files), Ctrl+K (file search), Ctrl+G (grep), Datastores, and the file change watcher.",
36460
+ omitDirsHelp: "Reads no contents inside these directories. Applies to the sidebar (Files), Ctrl+K (file search), Ctrl+G (grep), Datastores, and the file change watcher. Supports gitignore-style wildcards (*, ?, [abc], [!abc]).",
36176
36461
  excludeNames: "Hide these file or directory names completely",
36177
- excludeNamesHelp: "Removes matching files or directories from the sidebar, search, and grep results entirely. Unlike Skip, the names themselves disappear from the UI.",
36462
+ 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]).",
36178
36463
  reset: "Restore defaults",
36179
36464
  autosaveNote: "Changes save automatically.",
36180
36465
  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.`,
@@ -36462,9 +36747,9 @@ code-viewer query agent-help`
36462
36747
  displaySource: "このブラウザのすべてのプロジェクトに適用されます。",
36463
36748
  excludedDirectories: "除外ディレクトリ",
36464
36749
  omitDirs: "閲覧と検索でスキップするディレクトリ名",
36465
- omitDirsHelp: "これらのディレクトリの中身は読み込みません。サイドバー(Files)・Ctrl+K(ファイル検索)・Ctrl+G(grep)・Datastores・File change watcher の5機能すべてに適用されます。",
36750
+ omitDirsHelp: "これらのディレクトリの中身は読み込みません。サイドバー(Files)・Ctrl+K(ファイル検索)・Ctrl+G(grep)・Datastores・File change watcher の5機能すべてに適用されます。gitignore方式のワイルドカード(*, ?, [abc], [!abc])に対応しています。",
36466
36751
  excludeNames: "完全に非表示にするファイル名またはディレクトリ名",
36467
- excludeNamesHelp: "リスト中の名前に一致するファイル/ディレクトリを、サイドバー・検索結果・grep 結果から完全に消します。Skip と違い、名前自体が UI に出なくなります。",
36752
+ excludeNamesHelp: "リスト中の名前に一致するファイル/ディレクトリを、サイドバー・検索結果・grep 結果から完全に消します。Skip と違い、名前自体が UI に出なくなります。gitignore方式のワイルドカード(*, ?, [abc], [!abc])に対応しています。",
36468
36753
  reset: "デフォルトに戻す",
36469
36754
  autosaveNote: "変更は自動で保存されます。",
36470
36755
  scopeSource: (project, source) => `このブラウザのプロジェクト "${project}" に保存されます。ソース: ${source}。サイドバー、Ctrl+K、Ctrl+G、Datastores、File change watcher で使われます。「デフォルトに戻す」でブラウザ側の上書きを削除します。`,