@youtyan/code-viewer 0.9.0 → 0.9.1

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/README.md CHANGED
@@ -186,8 +186,24 @@ tree state survives navigation and refresh.
186
186
  Symlinks get a distinct icon and a "→ target" label instead of looking like a
187
187
  regular file, and clicking one navigates straight to its resolved target.
188
188
  Broken symlinks are flagged and disabled instead of erroring out. Files with
189
- pending git changes (new, modified, renamed, deleted) show a status badge in
190
- the tree in place of the regular type icon.
189
+ pending git changes show a status badge in the tree in place of the regular
190
+ type icon:
191
+
192
+ | Badge | Meaning |
193
+ |---|---|
194
+ | `M` | Modified |
195
+ | `A` | Added — staged for commit |
196
+ | `D` | Deleted |
197
+ | `R` | Renamed |
198
+ | `U` | Untracked — in the worktree, not under version control yet |
199
+ | `I` | Ignored — excluded by a `.gitignore` rule |
200
+
201
+ `U` and `A` are deliberately separate: a file you have never run `git add` on
202
+ reads differently from one already staged. A directory that is wholly
203
+ untracked or ignored is badged as a whole, so it stays recognizable while
204
+ collapsed; it keeps its folder icon and carries the badge next to its name.
205
+ Inside an untracked directory the badge is inherited by its contents, except
206
+ where an ignore rule names a file specifically.
191
207
 
192
208
  ## Uploads and Scope Settings
193
209
 
@@ -1167,8 +1167,12 @@ function runBytesAsync(args, cwd, options = {}) {
1167
1167
  return new Promise((resolve) => {
1168
1168
  const proc = spawn(args[0], args.slice(1), {
1169
1169
  cwd,
1170
- stdio: ["ignore", "pipe", "pipe"]
1170
+ stdio: [options.stdin === undefined ? "ignore" : "pipe", "pipe", "pipe"]
1171
1171
  });
1172
+ if (options.stdin !== undefined) {
1173
+ proc.stdin?.on("error", () => {});
1174
+ proc.stdin?.end(options.stdin);
1175
+ }
1172
1176
  const stdoutChunks = [];
1173
1177
  const stderrChunks = [];
1174
1178
  let stdoutBytes = 0;
@@ -1448,9 +1452,10 @@ function run(args, cwd) {
1448
1452
  timeout: GIT_COMMAND_TIMEOUT_MS
1449
1453
  });
1450
1454
  }
1451
- function runGitAsync(args, cwd) {
1455
+ function runGitAsync(args, cwd, options = {}) {
1452
1456
  return runAsync(resolveGitArgs(args), cwd, {
1453
- timeout: GIT_COMMAND_TIMEOUT_MS
1457
+ timeout: GIT_COMMAND_TIMEOUT_MS,
1458
+ ...options
1454
1459
  });
1455
1460
  }
1456
1461
  function resolveGitArgs(args) {
@@ -1530,7 +1535,7 @@ async function repoStatusMapAsync(cwd, now = Date.now()) {
1530
1535
  "status",
1531
1536
  "--porcelain=v1",
1532
1537
  "-z",
1533
- "--untracked-files=all"
1538
+ "--untracked-files=normal"
1534
1539
  ], cwd);
1535
1540
  if (res.code !== 0)
1536
1541
  return map;
@@ -1542,7 +1547,7 @@ async function repoStatusMapAsync(cwd, now = Date.now()) {
1542
1547
  if (!path)
1543
1548
  continue;
1544
1549
  if (xy === "??") {
1545
- map.set(path, "A");
1550
+ map.set(path, "U");
1546
1551
  continue;
1547
1552
  }
1548
1553
  if (xy[0] === "R" || xy[0] === "C" || xy[1] === "R" || xy[1] === "C") {
@@ -1557,6 +1562,27 @@ async function repoStatusMapAsync(cwd, now = Date.now()) {
1557
1562
  setTimedCacheEntry(repoStatusMapCache, cwd, { map }, now);
1558
1563
  return map;
1559
1564
  }
1565
+ function repoStatusForPath(map, path) {
1566
+ const own = map.get(path) ?? map.get(`${path}/`);
1567
+ if (own)
1568
+ return { code: own, inherited: false };
1569
+ for (let slash = path.lastIndexOf("/");slash > 0; slash = path.lastIndexOf("/", slash - 1)) {
1570
+ const ancestor = map.get(`${path.slice(0, slash)}/`);
1571
+ if (ancestor)
1572
+ return { code: ancestor, inherited: true };
1573
+ }
1574
+ return;
1575
+ }
1576
+ async function ignoredPathsAsync(paths, cwd) {
1577
+ if (!paths.length)
1578
+ return new Set;
1579
+ const res = await runGitAsync(["git", "check-ignore", "-z", "--stdin"], cwd, {
1580
+ stdin: paths.join("\x00")
1581
+ });
1582
+ if (res.code !== 0 && res.code !== 1)
1583
+ return new Set;
1584
+ return new Set(res.stdout.split("\x00").filter(Boolean));
1585
+ }
1560
1586
  function showAsync(ref, path, cwd) {
1561
1587
  return runGitAsync(["git", "show", `${ref}:${path}`], cwd);
1562
1588
  }
@@ -25750,9 +25776,14 @@ async function handleTree(url) {
25750
25776
  if (tree.error)
25751
25777
  return text(tree.error, tree.status ?? 500);
25752
25778
  const entries = tree.entries.filter((entry) => !isExcludedScopePath(entry.path, excludeNames));
25753
- const statusMap = target === "worktree" || target === "" ? await repoStatusMapAsync(cwd) : null;
25779
+ const worktreeTarget = target === "worktree" || target === "";
25780
+ const [statusMap, ignoredPaths] = worktreeTarget ? await Promise.all([
25781
+ repoStatusMapAsync(cwd),
25782
+ ignoredPathsAsync(entries.map((entry) => entry.path), cwd)
25783
+ ]) : [null, null];
25754
25784
  const withStatus = (entry) => {
25755
- const status = statusMap?.get(entry.path);
25785
+ const found = statusMap && repoStatusForPath(statusMap, entry.path);
25786
+ const status = ignoredPaths?.has(entry.path) && (!found || found.inherited) ? "I" : found?.code;
25756
25787
  return status ? { ...entry, status } : entry;
25757
25788
  };
25758
25789
  const deletedEntries = !recursive && statusMap ? deletedTreeEntriesForPath(statusMap, path) : [];
@@ -25766,7 +25797,7 @@ async function handleTree(url) {
25766
25797
  ...deletedEntries
25767
25798
  ],
25768
25799
  readme: await readReadme(target, path),
25769
- upload_enabled: uploadEnabled && (target === "worktree" || target === "")
25800
+ upload_enabled: uploadEnabled && worktreeTarget
25770
25801
  });
25771
25802
  }
25772
25803
  async function handleSettings() {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@youtyan/code-viewer",
3
- "version": "0.9.0",
3
+ "version": "0.9.1",
4
4
  "description": "Local browser-based code and git diff viewer",
5
5
  "type": "module",
6
6
  "bin": {
package/web/app.js CHANGED
@@ -23348,7 +23348,14 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
23348
23348
  const span = document.createElement("span");
23349
23349
  span.className = `badge ${ch}`;
23350
23350
  span.textContent = ch;
23351
- span.title = { M: "modified", A: "added", D: "deleted", R: "renamed" }[ch] || ch;
23351
+ span.title = {
23352
+ M: "modified",
23353
+ A: "added",
23354
+ D: "deleted",
23355
+ R: "renamed",
23356
+ U: "untracked",
23357
+ I: "ignored"
23358
+ }[ch] || ch;
23352
23359
  return span;
23353
23360
  }
23354
23361
  function setFileViewed(path, viewed) {
@@ -26425,7 +26432,11 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
26425
26432
  },
26426
26433
  {
26427
26434
  kind: "paragraph",
26428
- 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.'
26435
+ 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.'
26436
+ },
26437
+ {
26438
+ kind: "paragraph",
26439
+ text: "Files with pending git changes show a status badge in the tree instead of the regular type icon: M (modified), A (added — staged for commit), D (deleted), R (renamed), U (untracked — in the worktree but not under version control yet), and I (ignored by a .gitignore rule). U and A stay separate so a file you have never run git add on does not look like one that is already staged. A wholly untracked or ignored directory is badged as a whole and keeps its folder icon, so it stays recognizable while collapsed; its contents inherit the badge unless an ignore rule names a file specifically."
26429
26440
  }
26430
26441
  ]
26431
26442
  },
@@ -27116,7 +27127,11 @@ code-viewer query agent-help`
27116
27127
  },
27117
27128
  {
27118
27129
  kind: "paragraph",
27119
- text: "シンボリックリンクは専用アイコンと「→ リンク先」ラベルで表示されるため通常のファイル/フォルダと区別でき、クリックするとリンク先に遷移します。リンク切れのシンボリックリンクは無効化されたことが分かる表示になります。未コミットの git 変更(新規・変更・リネーム・削除)があるファイルは、通常の種類アイコンの代わりにステータスバッジがツリーに表示されます。"
27130
+ text: "シンボリックリンクは専用アイコンと「→ リンク先」ラベルで表示されるため通常のファイル/フォルダと区別でき、クリックするとリンク先に遷移します。リンク切れのシンボリックリンクは無効化されたことが分かる表示になります。"
27131
+ },
27132
+ {
27133
+ kind: "paragraph",
27134
+ text: "git の状態があるファイルは、通常の種類アイコンの代わりにステータスバッジがツリーに表示されます。M(変更)、A(追加 — コミット予定としてステージ済み)、D(削除)、R(リネーム)、U(未追跡 — ワークツリーにあるがまだバージョン管理下にない)、I(.gitignore の対象) の 6 種類です。U と A を分けているのは、git add をまだ一度もしていないファイルが、既にステージ済みのファイルと同じ見た目にならないようにするためです。丸ごと未追跡・無視のディレクトリにはディレクトリ単位でバッジが付き、フォルダアイコンは残るので折りたたんだままでも判別できます。配下のファイルはそのバッジを引き継ぎますが、無視ルールが個別に名指ししているファイルはそちらが優先されます。"
27120
27135
  }
27121
27136
  ]
27122
27137
  },
@@ -31235,6 +31250,43 @@ code-viewer query agent-help`
31235
31250
  function toggleSidebarHidden() {
31236
31251
  applySidebarHidden(!STATE.sidebarHidden);
31237
31252
  }
31253
+ function applyDirEntryToNode(node, entry) {
31254
+ node.explicit = true;
31255
+ if (entry.children_omitted === true) {
31256
+ node.children_omitted = true;
31257
+ node.children_omitted_reason = entry.children_omitted_reason;
31258
+ }
31259
+ if (entry.is_symlink) {
31260
+ node.is_symlink = true;
31261
+ node.symlink_target = entry.symlink_target;
31262
+ node.resolved_path = entry.resolved_path;
31263
+ }
31264
+ if (entry.status)
31265
+ node.status = entry.status;
31266
+ }
31267
+ function createTreeDirLabel(dir) {
31268
+ const label = document.createElement("span");
31269
+ label.className = "dir-label";
31270
+ const dn = document.createElement("span");
31271
+ dn.className = "dir-name";
31272
+ dn.textContent = dir.name;
31273
+ dn.title = dir.path;
31274
+ label.appendChild(dn);
31275
+ if (dir.status)
31276
+ label.appendChild(fileBadge(dir.status));
31277
+ if (dir.children_omitted) {
31278
+ const omitted = document.createElement("span");
31279
+ omitted.className = "dir-omitted " + (dir.children_omitted_reason === "heavy" ? "dir-omitted-heavy" : "dir-omitted-internal");
31280
+ const badge = omittedDirectoryBadge(dir.children_omitted_reason);
31281
+ omitted.textContent = badge.label;
31282
+ omitted.title = badge.title;
31283
+ label.appendChild(omitted);
31284
+ }
31285
+ const dirSymlinkLabel = symlinkTargetLabel(dir);
31286
+ if (dirSymlinkLabel)
31287
+ label.appendChild(dirSymlinkLabel);
31288
+ return label;
31289
+ }
31238
31290
  function buildTree(files) {
31239
31291
  const root = {
31240
31292
  name: "",
@@ -31266,16 +31318,7 @@ code-viewer query agent-help`
31266
31318
  node.minOrder = f2.order;
31267
31319
  }
31268
31320
  if (f2.type === "tree") {
31269
- node.explicit = true;
31270
- if (f2.children_omitted === true) {
31271
- node.children_omitted = true;
31272
- node.children_omitted_reason = f2.children_omitted_reason;
31273
- }
31274
- if (f2.is_symlink) {
31275
- node.is_symlink = true;
31276
- node.symlink_target = f2.symlink_target;
31277
- node.resolved_path = f2.resolved_path;
31278
- }
31321
+ applyDirEntryToNode(node, f2);
31279
31322
  continue;
31280
31323
  }
31281
31324
  node.files.push(f2);
@@ -31345,25 +31388,7 @@ code-viewer query agent-help`
31345
31388
  const dirIcon = document.createElement("span");
31346
31389
  dirIcon.className = "dir-icon";
31347
31390
  li.appendChild(dirIcon);
31348
- const label = document.createElement("span");
31349
- label.className = "dir-label";
31350
- const dn = document.createElement("span");
31351
- dn.className = "dir-name";
31352
- dn.textContent = dir.name;
31353
- dn.title = dir.path;
31354
- label.appendChild(dn);
31355
- if (dir.children_omitted) {
31356
- const omitted = document.createElement("span");
31357
- omitted.className = "dir-omitted " + (dir.children_omitted_reason === "heavy" ? "dir-omitted-heavy" : "dir-omitted-internal");
31358
- const badge = omittedDirectoryBadge(dir.children_omitted_reason);
31359
- omitted.textContent = badge.label;
31360
- omitted.title = badge.title;
31361
- label.appendChild(omitted);
31362
- }
31363
- const dirSymlinkLabel = symlinkTargetLabel(dir);
31364
- if (dirSymlinkLabel)
31365
- label.appendChild(dirSymlinkLabel);
31366
- li.appendChild(label);
31391
+ li.appendChild(createTreeDirLabel(dir));
31367
31392
  li.appendChild(createOpenPathButton(dir.path, "directory", openDirectoryInOsTitle()));
31368
31393
  const collapsed = STATE.collapsedDirs.has(dir.path);
31369
31394
  if (collapsed)
@@ -31460,16 +31485,7 @@ code-viewer query agent-help`
31460
31485
  node.minOrder = Math.min(node.minOrder, order);
31461
31486
  }
31462
31487
  if (entry.type === "tree") {
31463
- node.explicit = true;
31464
- if (entry.children_omitted === true) {
31465
- node.children_omitted = true;
31466
- node.children_omitted_reason = entry.children_omitted_reason;
31467
- }
31468
- if (entry.is_symlink) {
31469
- node.is_symlink = true;
31470
- node.symlink_target = entry.symlink_target;
31471
- node.resolved_path = entry.resolved_path;
31472
- }
31488
+ applyDirEntryToNode(node, entry);
31473
31489
  return;
31474
31490
  }
31475
31491
  if (!node.files.some((file) => file.path === entry.path))
@@ -31553,25 +31569,7 @@ code-viewer query agent-help`
31553
31569
  const dirIcon = document.createElement("span");
31554
31570
  dirIcon.className = "dir-icon";
31555
31571
  li.appendChild(dirIcon);
31556
- const label = document.createElement("span");
31557
- label.className = "dir-label";
31558
- const dn = document.createElement("span");
31559
- dn.className = "dir-name";
31560
- dn.textContent = dir.name;
31561
- dn.title = dir.path;
31562
- label.appendChild(dn);
31563
- if (dir.children_omitted) {
31564
- const omitted = document.createElement("span");
31565
- omitted.className = "dir-omitted " + (dir.children_omitted_reason === "heavy" ? "dir-omitted-heavy" : "dir-omitted-internal");
31566
- const badge = omittedDirectoryBadge(dir.children_omitted_reason);
31567
- omitted.textContent = badge.label;
31568
- omitted.title = badge.title;
31569
- label.appendChild(omitted);
31570
- }
31571
- const dirSymlinkLabel = symlinkTargetLabel(dir);
31572
- if (dirSymlinkLabel)
31573
- label.appendChild(dirSymlinkLabel);
31574
- li.appendChild(label);
31572
+ li.appendChild(createTreeDirLabel(dir));
31575
31573
  li.appendChild(createOpenPathButton(dir.path, "directory", openDirectoryInOsTitle()));
31576
31574
  const updateIcon = () => {
31577
31575
  setFolderIcon(dirIcon, li.classList.contains("collapsed"));
@@ -33209,10 +33207,17 @@ code-viewer query agent-help`
33209
33207
  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}`;
33210
33208
  if (deletedEntry)
33211
33209
  row.classList.add("gdp-row-disabled");
33212
- const icon = entry.status ? fileBadge(entry.status) : repoEntryTypeIcon(entry, browsable, nonBrowsableCommit);
33210
+ const directoryRow = entry.type === "tree";
33211
+ const icon = entry.status && !directoryRow ? fileBadge(entry.status) : repoEntryTypeIcon(entry, browsable, nonBrowsableCommit);
33213
33212
  const name = document.createElement("span");
33214
33213
  name.className = "name";
33215
33214
  name.textContent = entry.name;
33215
+ let nameCell = name;
33216
+ if (directoryRow && entry.status) {
33217
+ nameCell = document.createElement("span");
33218
+ nameCell.className = "name-cell";
33219
+ nameCell.append(name, fileBadge(entry.status));
33220
+ }
33216
33221
  if (nonBrowsableCommit) {
33217
33222
  row.title = commitEntryMeta(entry.submodule).title;
33218
33223
  row.setAttribute("aria-disabled", "true");
@@ -33221,7 +33226,7 @@ code-viewer query agent-help`
33221
33226
  }
33222
33227
  const metaBlock = createRepoEntryMeta(entry, browsable);
33223
33228
  const size = createRepoEntrySize(entry);
33224
- row.append(icon, name, metaBlock, size);
33229
+ row.append(icon, nameCell, metaBlock, size);
33225
33230
  row.addEventListener("click", () => {
33226
33231
  if (brokenSymlink || deletedEntry)
33227
33232
  return;
package/web/style.css CHANGED
@@ -2721,11 +2721,19 @@ body.gdp-help-page #content {
2721
2721
  .badge.A { background: #ccffd866; color: var(--success); }
2722
2722
  .badge.D { background: #ffd7d566; color: var(--danger); }
2723
2723
  .badge.R { background: #d8b9ff66; color: var(--done); }
2724
+ /* Outside version control. U (untracked) takes the accent hue so it reads as
2725
+ "not in git yet" rather than as one more shade of the staged-green A it
2726
+ used to be collapsed into. I (ignored) stays deliberately colorless: an
2727
+ ignored file is present but irrelevant to the repository. */
2728
+ .badge.U { background: var(--accent-subtle); color: var(--accent); }
2729
+ .badge.I { background: var(--bg-emph); color: var(--fg-muted); }
2724
2730
 
2725
2731
  [data-theme="dark"] .badge.M { background: rgba(210,153,34,0.20); color: var(--attn); }
2726
2732
  [data-theme="dark"] .badge.A { background: rgba(46,160,67,0.20); color: var(--success); }
2727
2733
  [data-theme="dark"] .badge.D { background: rgba(248,81,73,0.20); color: var(--danger); }
2728
2734
  [data-theme="dark"] .badge.R { background: rgba(163,113,247,0.20); color: var(--done); }
2735
+ [data-theme="dark"] .badge.U { background: rgba(56,139,253,0.20); color: var(--accent); }
2736
+ [data-theme="dark"] .badge.I { background: rgba(145,152,161,0.15); color: var(--fg-muted); }
2729
2737
 
2730
2738
  /* Sidebar row marker for files worth a second look before diving in:
2731
2739
  heavy diffs, binary/media files, or Git commit entries. Same semantics as
@@ -4792,6 +4800,16 @@ body.gdp-file-detail-page #empty {
4792
4800
  white-space: nowrap;
4793
4801
  font-weight: 500;
4794
4802
  }
4803
+ /* Holds the name plus a directory's status badge inside the single name
4804
+ column of the row grid - same shape as the sidebar's .dir-label, which
4805
+ pairs a directory name with its badges. */
4806
+ .gdp-repo-row .name-cell {
4807
+ min-width: 0;
4808
+ display: flex;
4809
+ align-items: center;
4810
+ gap: 6px;
4811
+ overflow: hidden;
4812
+ }
4795
4813
  .gdp-repo-row .kind {
4796
4814
  justify-self: end;
4797
4815
  color: var(--fg-muted);