@youtyan/code-viewer 0.1.13 → 0.1.14

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@youtyan/code-viewer",
3
- "version": "0.1.13",
3
+ "version": "0.1.14",
4
4
  "description": "Local browser-based code and git diff viewer",
5
5
  "type": "module",
6
6
  "bin": {
package/web/app.js CHANGED
@@ -6764,6 +6764,8 @@
6764
6764
  let REPO_SIDEBAR_REF = null;
6765
6765
  let REPO_SIDEBAR_LOAD_REF = null;
6766
6766
  let REPO_SIDEBAR_LOAD = null;
6767
+ let SIDEBAR_FILES = [];
6768
+ let SIDEBAR_ON_FILE_CLICK;
6767
6769
  let PENDING_G_SCOPE = null;
6768
6770
  let PENDING_G_UNTIL = 0;
6769
6771
  let SOURCE_CURSOR = null;
@@ -7239,8 +7241,10 @@
7239
7241
  }
7240
7242
  if (f2.type === "tree") {
7241
7243
  node.explicit = true;
7242
- if (f2.children_omitted === true)
7244
+ if (f2.children_omitted === true) {
7243
7245
  node.children_omitted = true;
7246
+ node.children_omitted_reason = f2.children_omitted_reason;
7247
+ }
7244
7248
  continue;
7245
7249
  }
7246
7250
  node.files.push(f2);
@@ -7283,12 +7287,18 @@
7283
7287
  li.dataset.explicit = "true";
7284
7288
  if (dir.children_omitted) {
7285
7289
  li.classList.add("children-omitted");
7286
- li.title = "Directory contents are intentionally not listed";
7290
+ li.classList.add(dir.children_omitted_reason === "ignored" ? "children-omitted-ignored" : "children-omitted-internal");
7291
+ li.title = dir.children_omitted_reason === "ignored" ? "Ignored directory: open the detail pane to browse its contents" : "Internal Git metadata is not browsed";
7287
7292
  }
7288
7293
  li.style.setProperty("--lvl-pad", 12 + depth * 14 + "px");
7289
7294
  const chev = document.createElement("span");
7290
- chev.className = "chev";
7291
- setChevronIcon(chev);
7295
+ if (dir.children_omitted) {
7296
+ chev.className = "chev-spacer";
7297
+ chev.setAttribute("aria-hidden", "true");
7298
+ } else {
7299
+ chev.className = "chev";
7300
+ setChevronIcon(chev);
7301
+ }
7292
7302
  li.appendChild(chev);
7293
7303
  const dirIcon = document.createElement("span");
7294
7304
  dirIcon.className = "dir-icon";
@@ -7302,9 +7312,9 @@
7302
7312
  label.appendChild(dn);
7303
7313
  if (dir.children_omitted) {
7304
7314
  const omitted = document.createElement("span");
7305
- omitted.className = "dir-omitted";
7306
- omitted.textContent = "skipped";
7307
- omitted.title = "Directory contents are intentionally not listed";
7315
+ omitted.className = "dir-omitted " + (dir.children_omitted_reason === "ignored" ? "dir-omitted-ignored" : "dir-omitted-internal");
7316
+ omitted.textContent = dir.children_omitted_reason === "ignored" ? "ignored" : "private";
7317
+ omitted.title = dir.children_omitted_reason === "ignored" ? "Tree expansion is skipped, but the directory detail can be opened" : "This directory cannot be opened from the browser";
7308
7318
  label.appendChild(omitted);
7309
7319
  }
7310
7320
  li.appendChild(label);
@@ -7329,12 +7339,24 @@
7329
7339
  STATE.collapsedDirs.delete(dir.path);
7330
7340
  localStorage.setItem("gdp:collapsed-dirs", JSON.stringify([...STATE.collapsedDirs]));
7331
7341
  };
7332
- chev.addEventListener("click", toggleDir);
7333
- dirIcon.addEventListener("click", toggleDir);
7342
+ if (!dir.children_omitted) {
7343
+ chev.addEventListener("click", toggleDir);
7344
+ dirIcon.addEventListener("click", toggleDir);
7345
+ }
7334
7346
  if (onFileClick) {
7335
7347
  li.addEventListener("click", (e2) => {
7336
7348
  e2.stopPropagation();
7337
- onFileClick({ path: dir.path, display_path: dir.path, type: "tree", children_omitted: dir.children_omitted });
7349
+ if (dir.children_omitted_reason === "internal")
7350
+ return;
7351
+ if (dir.children_omitted_reason !== "internal") {
7352
+ onFileClick({
7353
+ path: dir.path,
7354
+ display_path: dir.path,
7355
+ type: "tree",
7356
+ children_omitted: dir.children_omitted,
7357
+ children_omitted_reason: dir.children_omitted_reason
7358
+ });
7359
+ }
7338
7360
  focusSidebarPanel();
7339
7361
  });
7340
7362
  } else {
@@ -7416,6 +7438,8 @@
7416
7438
  ul.innerHTML = "";
7417
7439
  ul.classList.toggle("tree", STATE.sbView === "tree");
7418
7440
  STATE.files = files;
7441
+ SIDEBAR_FILES = files;
7442
+ SIDEBAR_ON_FILE_CLICK = onFileClick;
7419
7443
  if (!onFileClick)
7420
7444
  REPO_SIDEBAR_REF = null;
7421
7445
  if (STATE.sbView === "tree") {
@@ -7550,13 +7574,43 @@
7550
7574
  card.scrollIntoView({ behavior: "smooth", block: "start" });
7551
7575
  }
7552
7576
  }
7553
- function markActive(path) {
7577
+ function sidebarAncestorDirs(path) {
7578
+ const parts = path.split("/").filter(Boolean);
7579
+ const dirs = [];
7580
+ for (let i2 = 1;i2 < parts.length; i2++)
7581
+ dirs.push(parts.slice(0, i2).join("/"));
7582
+ return dirs;
7583
+ }
7584
+ function expandSidebarAncestors(path) {
7585
+ if (STATE.sbView !== "tree")
7586
+ return;
7587
+ let changed = false;
7588
+ for (const dir of sidebarAncestorDirs(path)) {
7589
+ if (STATE.collapsedDirs.delete(dir))
7590
+ changed = true;
7591
+ const row = document.querySelector('#filelist .tree-dir[data-dirpath="' + CSS.escape(dir) + '"]');
7592
+ row?.classList.remove("collapsed");
7593
+ const icon = row?.querySelector(".dir-icon");
7594
+ if (icon)
7595
+ setFolderIcon(icon, false);
7596
+ }
7597
+ if (changed)
7598
+ localStorage.setItem("gdp:collapsed-dirs", JSON.stringify([...STATE.collapsedDirs]));
7599
+ }
7600
+ function markActive(path, options = {}) {
7554
7601
  STATE.activeFile = path;
7602
+ if (options.reveal && STATE.sbView === "tree")
7603
+ expandSidebarAncestors(path);
7555
7604
  $$("#filelist li").forEach((li) => {
7556
7605
  const itemPath = li.dataset.path || li.dataset.dirpath;
7557
7606
  if (itemPath)
7558
7607
  li.classList.toggle("active", itemPath === path);
7559
7608
  });
7609
+ if (options.reveal) {
7610
+ const active = document.querySelector("#filelist li.active[data-path], #filelist .tree-dir.active[data-dirpath]");
7611
+ if (active)
7612
+ requestAnimationFrame(() => scrollSidebarItemIntoView(active));
7613
+ }
7560
7614
  }
7561
7615
  function applyViewedState() {
7562
7616
  $$("#filelist li[data-path]").forEach((li) => {
@@ -8239,7 +8293,8 @@
8239
8293
  path: entry.path,
8240
8294
  display_path: entry.path,
8241
8295
  type: entry.type,
8242
- children_omitted: entry.children_omitted
8296
+ children_omitted: entry.children_omitted,
8297
+ children_omitted_reason: entry.children_omitted_reason
8243
8298
  }));
8244
8299
  renderSidebar(files, (file) => {
8245
8300
  if (file.type === "tree") {
@@ -8266,7 +8321,7 @@
8266
8321
  return load2;
8267
8322
  }
8268
8323
  function activateRepoSidebarPath(currentPath) {
8269
- markActive(currentPath);
8324
+ markActive(currentPath, { reveal: true });
8270
8325
  applyFilter();
8271
8326
  }
8272
8327
  function createPlaceholder(f2) {
@@ -9213,6 +9268,8 @@
9213
9268
  function sourceDisplayKind(path) {
9214
9269
  if (isVideo(path))
9215
9270
  return "video";
9271
+ if (isAudio(path))
9272
+ return "audio";
9216
9273
  if (isImage(path))
9217
9274
  return "image";
9218
9275
  if (/\.pdf$/i.test(path))
@@ -9259,10 +9316,28 @@
9259
9316
  return "MP4 video";
9260
9317
  if (ext === "webm")
9261
9318
  return "WebM video";
9319
+ if (ext === "mp3")
9320
+ return "MP3 audio";
9321
+ if (ext === "wav")
9322
+ return "WAV audio";
9323
+ if (ext === "ogg")
9324
+ return "Ogg audio";
9325
+ if (ext === "flac")
9326
+ return "FLAC audio";
9327
+ if (ext === "m4a")
9328
+ return "M4A audio";
9329
+ if (ext === "aac")
9330
+ return "AAC audio";
9331
+ if (ext === "opus")
9332
+ return "Opus audio";
9333
+ if (ext === "mid" || ext === "midi")
9334
+ return "MIDI file";
9262
9335
  if (mime?.startsWith("image/"))
9263
9336
  return "Image";
9264
9337
  if (mime?.startsWith("video/"))
9265
9338
  return "Video";
9339
+ if (mime?.startsWith("audio/"))
9340
+ return "Audio";
9266
9341
  if (mime === "application/pdf")
9267
9342
  return "PDF document";
9268
9343
  if (fallback === "unsupported file")
@@ -9734,6 +9809,12 @@
9734
9809
  video.controls = true;
9735
9810
  video.preload = "metadata";
9736
9811
  view.appendChild(video);
9812
+ } else if (mediaKind === "audio") {
9813
+ const audio = document.createElement("audio");
9814
+ audio.src = url;
9815
+ audio.controls = true;
9816
+ audio.preload = "metadata";
9817
+ view.appendChild(audio);
9737
9818
  } else if (mediaKind === "pdf") {
9738
9819
  const frame = document.createElement("iframe");
9739
9820
  frame.src = url;
@@ -9900,7 +9981,7 @@
9900
9981
  renderSourceUnsupported(card, target);
9901
9982
  return;
9902
9983
  }
9903
- if (displayKind === "image" || displayKind === "video" || displayKind === "pdf") {
9984
+ if (displayKind === "image" || displayKind === "video" || displayKind === "audio" || displayKind === "pdf") {
9904
9985
  if (req !== SOURCE_REQ_SEQ || !sourceTargetsEqual(sourceTargetFromRoute(), target))
9905
9986
  return;
9906
9987
  finishSourceLoad(req);
@@ -10296,9 +10377,10 @@
10296
10377
  b2.addEventListener("scroll", () => mirror(b2, a2), { passive: true });
10297
10378
  });
10298
10379
  }
10299
- const MEDIA_RE = /\.(png|jpe?g|gif|webp|svg|avif|bmp|ico|mp4|webm|mov)(\?.*)?$/i;
10380
+ const MEDIA_RE = /\.(png|jpe?g|gif|webp|svg|avif|bmp|ico|mp4|webm|mov|mp3|wav|ogg|flac|m4a|aac|opus)(\?.*)?$/i;
10300
10381
  const IMAGE_RE = /\.(png|jpe?g|gif|webp|svg|avif|bmp|ico)(\?.*)?$/i;
10301
10382
  const VIDEO_RE = /\.(mp4|webm|mov)$/i;
10383
+ const AUDIO_RE = /\.(mp3|wav|ogg|flac|m4a|aac|opus)$/i;
10302
10384
  function isMedia(p2) {
10303
10385
  return MEDIA_RE.test(p2);
10304
10386
  }
@@ -10308,6 +10390,9 @@
10308
10390
  function isVideo(p2) {
10309
10391
  return VIDEO_RE.test(p2);
10310
10392
  }
10393
+ function isAudio(p2) {
10394
+ return AUDIO_RE.test(p2);
10395
+ }
10311
10396
  function fileURL(path, ref) {
10312
10397
  return "/_file?path=" + encodeURIComponent(path) + "&ref=" + ref;
10313
10398
  }
@@ -10316,6 +10401,9 @@
10316
10401
  if (isVideo(path)) {
10317
10402
  return '<video src="' + url + '" controls preload="metadata"></video>';
10318
10403
  }
10404
+ if (isAudio(path)) {
10405
+ return '<audio src="' + url + '" controls preload="metadata"></audio>';
10406
+ }
10319
10407
  return '<img src="' + url + '" alt="" loading="lazy">';
10320
10408
  }
10321
10409
  function enhanceMediaCard(file, card) {
@@ -10409,8 +10497,8 @@
10409
10497
  b2.addEventListener("click", () => {
10410
10498
  STATE.sbView = b2.dataset.view || "tree";
10411
10499
  localStorage.setItem("gdp:sbview", STATE.sbView);
10412
- if (STATE.files && STATE.files.length)
10413
- renderSidebar(STATE.files);
10500
+ if (SIDEBAR_FILES.length)
10501
+ renderSidebar(SIDEBAR_FILES, SIDEBAR_ON_FILE_CLICK);
10414
10502
  });
10415
10503
  });
10416
10504
  $("#sb-expand-all").addEventListener("click", () => setAllSidebarDirsCollapsed(false));
package/web/style.css CHANGED
@@ -807,16 +807,14 @@ html, body {
807
807
  z-index: 30;
808
808
  }
809
809
  body[data-focus-scope="sidebar"] #sidebar {
810
- background: color-mix(in oklab, var(--bg-soft) 97%, var(--scope-accent) 3%);
811
- outline: 1px solid color-mix(in oklab, var(--scope-accent) 58%, transparent);
812
- outline-offset: -1px;
813
- box-shadow: inset -3px 0 0 var(--scope-accent);
810
+ background: var(--bg-soft);
811
+ outline: none;
812
+ box-shadow: none;
814
813
  }
815
814
  body[data-focus-scope="main"] #content {
816
- background: color-mix(in oklab, var(--bg) 98%, var(--scope-accent) 2%);
817
- outline: 1px solid color-mix(in oklab, var(--scope-accent) 58%, transparent);
818
- outline-offset: -1px;
819
- box-shadow: inset 3px 0 0 var(--scope-accent);
815
+ background: var(--bg);
816
+ outline: none;
817
+ box-shadow: none;
820
818
  }
821
819
  .sb-head {
822
820
  display: flex; justify-content: space-between; align-items: center;
@@ -1102,6 +1100,12 @@ body.gdp-help-page #content {
1102
1100
  #filelist.tree .tree-dir.children-omitted .dir-label {
1103
1101
  border-bottom: 1px dashed var(--border-strong);
1104
1102
  }
1103
+ #filelist.tree .tree-dir.children-omitted-ignored {
1104
+ cursor: pointer;
1105
+ }
1106
+ #filelist.tree .tree-dir.children-omitted-internal {
1107
+ cursor: default;
1108
+ }
1105
1109
  #filelist.tree .tree-dir .chev {
1106
1110
  display: flex;
1107
1111
  width: 16px;
@@ -1120,6 +1124,11 @@ body.gdp-help-page #content {
1120
1124
  width: 12px;
1121
1125
  height: 12px;
1122
1126
  }
1127
+ #filelist.tree .tree-dir .chev-spacer {
1128
+ width: 16px;
1129
+ height: 16px;
1130
+ display: inline-block;
1131
+ }
1123
1132
  #filelist.tree .tree-dir .dir-label {
1124
1133
  min-width: 0;
1125
1134
  display: flex;
@@ -1140,7 +1149,7 @@ body.gdp-help-page #content {
1140
1149
  }
1141
1150
  #filelist.tree .tree-dir .dir-omitted {
1142
1151
  flex: 0 0 auto;
1143
- max-width: 54px;
1152
+ max-width: 62px;
1144
1153
  overflow: hidden;
1145
1154
  text-overflow: ellipsis;
1146
1155
  border: 1px solid var(--border-muted);
@@ -1151,6 +1160,16 @@ body.gdp-help-page #content {
1151
1160
  line-height: 15px;
1152
1161
  text-transform: uppercase;
1153
1162
  }
1163
+ #filelist.tree .tree-dir .dir-omitted-ignored {
1164
+ color: var(--accent);
1165
+ border-color: var(--accent-muted);
1166
+ background: var(--accent-subtle);
1167
+ }
1168
+ #filelist.tree .tree-dir .dir-omitted-internal {
1169
+ color: var(--fg-subtle);
1170
+ border-color: var(--border-muted);
1171
+ background: var(--bg-soft);
1172
+ }
1154
1173
  #filelist.tree .tree-dir .dir-icon {
1155
1174
  color: #54aeff;
1156
1175
  flex-shrink: 0;
@@ -3244,7 +3263,7 @@ body.gdp-file-detail-page #empty {
3244
3263
  font-style: italic;
3245
3264
  }
3246
3265
 
3247
- /* ===== Media (binary image/video) ===== */
3266
+ /* ===== Media (binary image/video/audio) ===== */
3248
3267
  .gdp-media {
3249
3268
  display: flex;
3250
3269
  gap: 16px;
@@ -3280,7 +3299,8 @@ body.gdp-file-detail-page #empty {
3280
3299
  color: var(--danger);
3281
3300
  }
3282
3301
  .gdp-media img,
3283
- .gdp-media video {
3302
+ .gdp-media video,
3303
+ .gdp-media audio {
3284
3304
  max-width: 100%;
3285
3305
  max-height: 70vh;
3286
3306
  border: 1px solid var(--border);
@@ -3295,6 +3315,12 @@ body.gdp-file-detail-page #empty {
3295
3315
  background-position: 0 0, 0 8px, 8px -8px, -8px 0;
3296
3316
  object-fit: contain;
3297
3317
  }
3318
+ .gdp-media audio {
3319
+ width: min(100%, 480px);
3320
+ max-height: none;
3321
+ padding: 8px;
3322
+ background-image: none;
3323
+ }
3298
3324
  .gdp-media .media-empty {
3299
3325
  width: 100%;
3300
3326
  min-height: 120px;
@@ -18,8 +18,11 @@ export type GitTreeEntry = {
18
18
  path: string;
19
19
  type: 'tree' | 'blob' | 'commit';
20
20
  children_omitted?: true;
21
+ children_omitted_reason?: 'ignored' | 'internal';
21
22
  };
22
23
 
24
+ const WORKTREE_RECURSIVE_DEPTH_LIMIT = 32;
25
+
23
26
  function run(args: string[], cwd: string): { code: number; stdout: string; stderr: string } {
24
27
  const proc = Bun.spawnSync(args, { cwd, stdout: 'pipe', stderr: 'pipe' });
25
28
  return {
@@ -156,24 +159,85 @@ function sortTreeEntries(entries: GitTreeEntry[]): GitTreeEntry[] {
156
159
  });
157
160
  }
158
161
 
159
- function worktreeDirectChildren(cwd: string, path: string): GitTreeEntry[] {
162
+ function ignoredWorktreeDirectories(cwd: string, path: string): Set<string> {
160
163
  const base = normalizeTreePath(path);
161
- const dir = join(cwd, base);
162
- try {
163
- return sortTreeEntries(readdirSync(dir, { withFileTypes: true }).map((entry) => {
164
- const entryPath = base ? `${base}/${entry.name}` : entry.name;
165
- const type = entry.isDirectory()
166
- ? hasDotGitEntry(join(dir, entry.name)) ? 'commit' as const : 'tree' as const
167
- : 'blob' as const;
168
- return {
169
- name: entry.name,
164
+ const args = ['git', '-c', 'core.quotepath=false', 'ls-files', '--others', '--ignored', '--exclude-standard', '--directory', '-z'];
165
+ if (base) args.push('--', `${base}/`);
166
+ const proc = Bun.spawnSync(args, {
167
+ cwd,
168
+ stdout: 'pipe',
169
+ stderr: 'ignore',
170
+ });
171
+ if (proc.exitCode !== 0) return new Set();
172
+ return new Set(new TextDecoder().decode(proc.stdout)
173
+ .split('\0')
174
+ .filter(entry => entry.endsWith('/'))
175
+ .map(entry => entry.replace(/\/+$/g, ''))
176
+ .filter(entry => entry && entry !== base));
177
+ }
178
+
179
+ function worktreeEntryFromDirent(base: string, dir: string, name: string, isDirectory: boolean, ignoredDirectories: Set<string>): GitTreeEntry {
180
+ const entryPath = base ? `${base}/${name}` : name;
181
+ const type = isDirectory
182
+ ? hasDotGitEntry(join(dir, name)) ? 'commit' as const : 'tree' as const
183
+ : 'blob' as const;
184
+ return type === 'tree' && (name === '.git' || ignoredDirectories.has(entryPath))
185
+ ? {
186
+ name,
170
187
  path: entryPath,
171
188
  type,
172
- };
173
- }));
189
+ children_omitted: true,
190
+ children_omitted_reason: name === '.git' ? 'internal' : 'ignored',
191
+ }
192
+ : { name, path: entryPath, type };
193
+ }
194
+
195
+ function worktreeFilesystemEntries(cwd: string, path: string, recursive: boolean): GitTreeEntry[] {
196
+ const base = normalizeTreePath(path);
197
+ const root = join(cwd, base);
198
+ const ignoredDirectories = ignoredWorktreeDirectories(cwd, base);
199
+ let directEntries: GitTreeEntry[];
200
+ try {
201
+ const dirents = readdirSync(root, { withFileTypes: true });
202
+ directEntries = sortTreeEntries(dirents
203
+ .map(entry => worktreeEntryFromDirent(base, root, entry.name, entry.isDirectory(), ignoredDirectories)));
174
204
  } catch {
175
205
  return [];
176
206
  }
207
+ if (!recursive) return directEntries;
208
+
209
+ const fileEntries: GitTreeEntry[] = [];
210
+ const walk = (dir: string, prefix: string, depth: number) => {
211
+ if (depth >= WORKTREE_RECURSIVE_DEPTH_LIMIT) return;
212
+ let entries;
213
+ try {
214
+ entries = readdirSync(dir, { withFileTypes: true });
215
+ } catch {
216
+ return;
217
+ }
218
+ for (const entry of entries) {
219
+ const entryPath = prefix ? `${prefix}/${entry.name}` : entry.name;
220
+ const full = join(dir, entry.name);
221
+ if (entry.isDirectory()) {
222
+ if (entry.name === '.git' || ignoredDirectories.has(entryPath)) {
223
+ fileEntries.push({
224
+ name: entry.name,
225
+ path: entryPath,
226
+ type: 'tree',
227
+ children_omitted: true,
228
+ children_omitted_reason: entry.name === '.git' ? 'internal' : 'ignored',
229
+ });
230
+ continue;
231
+ }
232
+ if (hasDotGitEntry(full)) continue;
233
+ walk(full, entryPath, depth + 1);
234
+ } else if (entry.isFile() || entry.isSymbolicLink()) {
235
+ fileEntries.push({ name: entry.name, path: entryPath, type: 'blob' });
236
+ }
237
+ }
238
+ };
239
+ walk(root, base, 0);
240
+ return combineDirectAndRecursiveFiles(directEntries, fileEntries.sort((a, b) => a.path.localeCompare(b.path)));
177
241
  }
178
242
 
179
243
  function hasDotGitEntry(dir: string): boolean {
@@ -185,20 +249,6 @@ function hasDotGitEntry(dir: string): boolean {
185
249
  }
186
250
  }
187
251
 
188
- function worktreeRecursiveFiles(cwd: string, path: string): GitTreeEntry[] {
189
- const base = normalizeTreePath(path);
190
- const trackedArgs = ['git', '-c', 'core.quotepath=false', 'ls-files', '-z'];
191
- if (base) trackedArgs.push('--', `${base}/`);
192
- const tracked = run(trackedArgs, cwd);
193
- const paths = tracked.code === 0 ? tracked.stdout.split('\0').filter(Boolean) : [];
194
- paths.push(...untracked(cwd, base));
195
- return [...new Set(paths)].filter(Boolean).sort().map((entryPath) => ({
196
- name: entryPath.split('/').pop() || entryPath,
197
- path: entryPath,
198
- type: 'blob' as const,
199
- }));
200
- }
201
-
202
252
  function gitTreeEntries(ref: string, path: string, cwd: string, recursive: boolean): { code: number; entries: GitTreeEntry[]; stderr: string } {
203
253
  const base = normalizeTreePath(path);
204
254
  const args = ['git', '-c', 'core.quotepath=false', 'ls-tree'];
@@ -227,31 +277,6 @@ function combineDirectAndRecursiveFiles(directEntries: GitTreeEntry[], fileEntri
227
277
  ];
228
278
  }
229
279
 
230
- function ignoredWorktreePaths(cwd: string, path: string): Set<string> {
231
- const base = normalizeTreePath(path);
232
- const args = ['git', '-c', 'core.quotepath=false', 'status', '--ignored', '--porcelain=v1', '-z', '--'];
233
- if (base) args.push(`${base}/`);
234
- const res = run(args, cwd);
235
- const ignored = new Set<string>();
236
- if (res.code !== 0) return ignored;
237
- const records = res.stdout.split('\0').filter(Boolean);
238
- for (let i = 0; i < records.length; i++) {
239
- const rec = records[i];
240
- if (!rec.startsWith('!! ')) continue;
241
- const path = rec.slice(3).replace(/\/+$/g, '');
242
- if (path) ignored.add(path);
243
- }
244
- return ignored;
245
- }
246
-
247
- function annotateOmittedWorktreeChildren(entries: GitTreeEntry[], ignoredPaths: Set<string>): GitTreeEntry[] {
248
- return entries.map((entry) => {
249
- return entry.type === 'tree' && ignoredPaths.has(entry.path)
250
- ? { ...entry, children_omitted: true }
251
- : entry;
252
- });
253
- }
254
-
255
280
  export function worktreeEntries(cwd: string, path: string): GitTreeEntry[] {
256
281
  return listTree('worktree', path, cwd).entries;
257
282
  }
@@ -276,12 +301,7 @@ export function listTree(
276
301
  ): { code: number; entries: GitTreeEntry[]; stderr: string } {
277
302
  const base = normalizeTreePath(path);
278
303
  if (ref === 'worktree') {
279
- const directEntries = worktreeDirectChildren(cwd, base);
280
- const ignoredPaths = ignoredWorktreePaths(cwd, base);
281
- const annotatedDirectEntries = annotateOmittedWorktreeChildren(directEntries, ignoredPaths);
282
- if (!options.recursive) return { code: 0, entries: annotatedDirectEntries, stderr: '' };
283
- const recursiveEntries = worktreeRecursiveFiles(cwd, base);
284
- return { code: 0, entries: combineDirectAndRecursiveFiles(annotatedDirectEntries, recursiveEntries), stderr: '' };
304
+ return { code: 0, entries: worktreeFilesystemEntries(cwd, base, !!options.recursive), stderr: '' };
285
305
  }
286
306
 
287
307
  const direct = gitTreeEntries(ref, base, cwd, false);
@@ -180,6 +180,7 @@ function guessMediaKind(path: string) {
180
180
  const ext = extname(path).slice(1).toLowerCase();
181
181
  if (['png', 'jpg', 'jpeg', 'gif', 'webp', 'svg', 'avif', 'bmp', 'ico'].includes(ext)) return 'image';
182
182
  if (['mp4', 'webm', 'mov'].includes(ext)) return 'video';
183
+ if (['mp3', 'wav', 'ogg', 'flac', 'm4a', 'aac', 'opus'].includes(ext)) return 'audio';
183
184
  return null;
184
185
  }
185
186
 
@@ -641,7 +642,9 @@ function rawFileHeaders(path: string, size: number | null = null): HeadersInit {
641
642
  const mime: Record<string, string> = {
642
643
  '.png': 'image/png', '.jpg': 'image/jpeg', '.jpeg': 'image/jpeg', '.gif': 'image/gif',
643
644
  '.webp': 'image/webp', '.svg': 'image/svg+xml', '.mp4': 'video/mp4', '.webm': 'video/webm',
644
- '.pdf': 'application/pdf',
645
+ '.mov': 'video/quicktime', '.pdf': 'application/pdf',
646
+ '.mp3': 'audio/mpeg', '.wav': 'audio/wav', '.ogg': 'audio/ogg', '.flac': 'audio/flac',
647
+ '.m4a': 'audio/mp4', '.aac': 'audio/aac', '.opus': 'audio/ogg',
645
648
  };
646
649
  const headers: Record<string, string> = {
647
650
  'Content-Type': mime[extname(path).toLowerCase()] || 'application/octet-stream',
package/web-src/types.ts CHANGED
@@ -38,6 +38,7 @@ export type RepoTreeEntry = {
38
38
  path: string;
39
39
  type: 'tree' | 'blob' | 'commit';
40
40
  children_omitted?: true;
41
+ children_omitted_reason?: 'ignored' | 'internal';
41
42
  };
42
43
 
43
44
  export type RepoTreeResponse = {