@youtyan/code-viewer 0.1.12 → 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/web/index.html CHANGED
@@ -26,6 +26,7 @@
26
26
  <nav class="app-menu" aria-label="Views">
27
27
  <a class="app-menu-item active" data-route="repo" href="/">Repository</a>
28
28
  <a class="app-menu-item active" data-route="diff" href="/todif?from=HEAD&to=worktree">Diff Viewer</a>
29
+ <a class="app-menu-item" data-route="help" href="/help">Help</a>
29
30
  </nav>
30
31
  <div class="global-actions">
31
32
  <button id="theme" title="toggle theme">🌗</button>
package/web/style.css CHANGED
@@ -22,6 +22,7 @@
22
22
  --accent-emph: #0550ae;
23
23
  --accent-subtle: #ddf4ff;
24
24
  --accent-muted: #54aeff66;
25
+ --scope-accent: #bf8700;
25
26
  /* success / danger / attention / done */
26
27
  --success: #1a7f37;
27
28
  --success-emph: #1f883d;
@@ -70,6 +71,7 @@
70
71
  --accent-emph: #1f6feb;
71
72
  --accent-subtle: rgba(56,139,253,0.10);
72
73
  --accent-muted: rgba(56,139,253,0.40);
74
+ --scope-accent: #d29922;
73
75
  --success: #3fb950;
74
76
  --success-emph: #238636;
75
77
  --success-subtle: rgba(46,160,67,0.15);
@@ -804,6 +806,16 @@ html, body {
804
806
  padding: 0 0 32px;
805
807
  z-index: 30;
806
808
  }
809
+ body[data-focus-scope="sidebar"] #sidebar {
810
+ background: var(--bg-soft);
811
+ outline: none;
812
+ box-shadow: none;
813
+ }
814
+ body[data-focus-scope="main"] #content {
815
+ background: var(--bg);
816
+ outline: none;
817
+ box-shadow: none;
818
+ }
807
819
  .sb-head {
808
820
  display: flex; justify-content: space-between; align-items: center;
809
821
  padding: 14px 16px 10px;
@@ -926,6 +938,140 @@ body.gdp-resizing #sidebar-resizer {
926
938
  body.gdp-resizing { cursor: col-resize !important; user-select: none; }
927
939
  body.gdp-resizing * { user-select: none !important; }
928
940
 
941
+ body.gdp-help-page #topbar,
942
+ body.gdp-help-page #sidebar,
943
+ body.gdp-help-page #sidebar-resizer {
944
+ display: none;
945
+ }
946
+ body.gdp-help-page #content {
947
+ margin-left: 0;
948
+ padding-top: calc(var(--global-header-h) + 28px);
949
+ }
950
+ .gdp-help-shell {
951
+ width: 100%;
952
+ max-width: none;
953
+ margin: 0;
954
+ }
955
+ .gdp-help-header {
956
+ display: flex;
957
+ align-items: center;
958
+ justify-content: space-between;
959
+ gap: 16px;
960
+ margin-bottom: 22px;
961
+ }
962
+ .gdp-help-header h1 {
963
+ margin: 0;
964
+ font-size: 28px;
965
+ }
966
+ .gdp-help-language {
967
+ appearance: none;
968
+ min-width: 92px;
969
+ height: 34px;
970
+ border: 1px solid var(--border);
971
+ border-radius: 6px;
972
+ background:
973
+ linear-gradient(45deg, transparent 50%, var(--fg-muted) 50%) right 15px center / 5px 5px no-repeat,
974
+ linear-gradient(135deg, var(--fg-muted) 50%, transparent 50%) right 10px center / 5px 5px no-repeat,
975
+ var(--bg);
976
+ color: var(--fg);
977
+ padding: 0 34px 0 12px;
978
+ font: inherit;
979
+ font-size: 13px;
980
+ line-height: 32px;
981
+ cursor: pointer;
982
+ }
983
+ .gdp-help-language:hover {
984
+ border-color: var(--border-strong);
985
+ }
986
+ .gdp-help-language:focus {
987
+ border-color: var(--accent);
988
+ outline: none;
989
+ box-shadow: 0 0 0 3px var(--accent-muted);
990
+ }
991
+ .gdp-help-layout {
992
+ display: grid;
993
+ grid-template-columns: 220px minmax(0, 1fr);
994
+ gap: 28px;
995
+ align-items: start;
996
+ }
997
+ .gdp-help-nav {
998
+ position: sticky;
999
+ top: calc(var(--global-header-h) + 20px);
1000
+ display: grid;
1001
+ gap: 4px;
1002
+ padding-right: 16px;
1003
+ border-right: 1px solid var(--border-muted);
1004
+ }
1005
+ .gdp-help-nav button {
1006
+ appearance: none;
1007
+ border: 0;
1008
+ border-left: 3px solid transparent;
1009
+ border-radius: 0 6px 6px 0;
1010
+ background: transparent;
1011
+ color: var(--fg-muted);
1012
+ padding: 8px 10px;
1013
+ text-align: left;
1014
+ font: inherit;
1015
+ cursor: pointer;
1016
+ }
1017
+ .gdp-help-nav button:hover {
1018
+ background: var(--bg-mute);
1019
+ color: var(--fg);
1020
+ }
1021
+ .gdp-help-nav button.active {
1022
+ border-left-color: var(--accent);
1023
+ background: var(--accent-subtle);
1024
+ color: var(--accent-emph);
1025
+ font-weight: 700;
1026
+ }
1027
+ .gdp-help-content h2 {
1028
+ margin: 0 0 8px;
1029
+ font-size: 24px;
1030
+ }
1031
+ .gdp-help-content > p {
1032
+ margin: 0 0 24px;
1033
+ color: var(--fg-muted);
1034
+ }
1035
+ .gdp-help-group {
1036
+ margin: 0 0 28px;
1037
+ }
1038
+ .gdp-help-group h3 {
1039
+ margin: 0 0 10px;
1040
+ font-size: 16px;
1041
+ }
1042
+ .gdp-help-group table {
1043
+ width: 100%;
1044
+ border-collapse: collapse;
1045
+ border-top: 1px solid var(--border-muted);
1046
+ }
1047
+ .gdp-help-group th,
1048
+ .gdp-help-group td {
1049
+ padding: 11px 8px;
1050
+ border-bottom: 1px solid var(--border-muted);
1051
+ vertical-align: top;
1052
+ }
1053
+ .gdp-help-group th {
1054
+ width: 230px;
1055
+ color: var(--fg);
1056
+ font-weight: 600;
1057
+ text-align: left;
1058
+ }
1059
+ .gdp-help-group td {
1060
+ color: var(--fg-muted);
1061
+ }
1062
+ .gdp-help-group kbd {
1063
+ display: inline-block;
1064
+ min-width: 24px;
1065
+ padding: 2px 6px;
1066
+ border: 1px solid var(--border);
1067
+ border-bottom-color: var(--border-strong);
1068
+ border-radius: 5px;
1069
+ background: var(--bg-soft);
1070
+ color: var(--fg);
1071
+ font: 12px "Monaspace Neon", ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;
1072
+ text-align: center;
1073
+ }
1074
+
929
1075
  /* ===== Tree view ===== */
930
1076
  #filelist.tree {
931
1077
  padding: 4px 0 0;
@@ -954,6 +1100,12 @@ body.gdp-resizing * { user-select: none !important; }
954
1100
  #filelist.tree .tree-dir.children-omitted .dir-label {
955
1101
  border-bottom: 1px dashed var(--border-strong);
956
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
+ }
957
1109
  #filelist.tree .tree-dir .chev {
958
1110
  display: flex;
959
1111
  width: 16px;
@@ -972,6 +1124,11 @@ body.gdp-resizing * { user-select: none !important; }
972
1124
  width: 12px;
973
1125
  height: 12px;
974
1126
  }
1127
+ #filelist.tree .tree-dir .chev-spacer {
1128
+ width: 16px;
1129
+ height: 16px;
1130
+ display: inline-block;
1131
+ }
975
1132
  #filelist.tree .tree-dir .dir-label {
976
1133
  min-width: 0;
977
1134
  display: flex;
@@ -992,7 +1149,7 @@ body.gdp-resizing * { user-select: none !important; }
992
1149
  }
993
1150
  #filelist.tree .tree-dir .dir-omitted {
994
1151
  flex: 0 0 auto;
995
- max-width: 54px;
1152
+ max-width: 62px;
996
1153
  overflow: hidden;
997
1154
  text-overflow: ellipsis;
998
1155
  border: 1px solid var(--border-muted);
@@ -1003,6 +1160,16 @@ body.gdp-resizing * { user-select: none !important; }
1003
1160
  line-height: 15px;
1004
1161
  text-transform: uppercase;
1005
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
+ }
1006
1173
  #filelist.tree .tree-dir .dir-icon {
1007
1174
  color: #54aeff;
1008
1175
  flex-shrink: 0;
@@ -1519,6 +1686,8 @@ table.d2h-diff-table tr.gdp-diff-line-target > td:first-child {
1519
1686
  }
1520
1687
  .gdp-copy-path.copied { color: var(--success); }
1521
1688
  .gdp-copy-path.failed { color: var(--danger); }
1689
+ .gdp-copy-source.copied { color: var(--success); }
1690
+ .gdp-copy-source.failed { color: var(--danger); }
1522
1691
  .gdp-open-path.opened { color: var(--success); }
1523
1692
  .gdp-open-path.failed { color: var(--danger); }
1524
1693
  .gdp-file-unfold[aria-pressed="true"] {
@@ -1738,6 +1907,23 @@ table.d2h-diff-table tr.gdp-diff-line-target > td:first-child {
1738
1907
  color: var(--fg);
1739
1908
  border-bottom-color: var(--blob-tab-active);
1740
1909
  }
1910
+ .gdp-source-tabs .gdp-copy-source {
1911
+ margin-left: auto;
1912
+ align-self: center;
1913
+ width: 28px;
1914
+ height: 28px;
1915
+ padding: 0;
1916
+ border: 1px solid transparent;
1917
+ border-radius: 6px;
1918
+ }
1919
+ .gdp-source-virtual-copy {
1920
+ flex: 0 0 auto;
1921
+ width: 28px;
1922
+ height: 28px;
1923
+ padding: 0;
1924
+ border: 1px solid transparent;
1925
+ border-radius: 6px;
1926
+ }
1741
1927
  .gdp-source-viewer > [hidden],
1742
1928
  .gdp-markdown-layout[hidden],
1743
1929
  .gdp-source-table[hidden] {
@@ -1754,6 +1940,25 @@ table.d2h-diff-table tr.gdp-diff-line-target > td:first-child {
1754
1940
  .gdp-source-table tr.gdp-source-line-target {
1755
1941
  background: var(--line-hit-bg);
1756
1942
  }
1943
+ .gdp-source-table tr.gdp-source-cursor .gdp-source-line-number,
1944
+ .gdp-source-virtual-row.gdp-source-cursor .gdp-source-virtual-line-number {
1945
+ color: var(--fg);
1946
+ box-shadow: inset 3px 0 0 var(--scope-accent), inset -1px 0 0 var(--border-muted);
1947
+ }
1948
+ .gdp-source-table tr.gdp-source-cursor .gdp-source-line-code,
1949
+ .gdp-source-virtual-row.gdp-source-cursor .gdp-source-virtual-line-code {
1950
+ background: color-mix(in oklab, var(--bg) 90%, var(--scope-accent) 10%) !important;
1951
+ }
1952
+ .gdp-source-table tr.gdp-source-cursor .gdp-source-line-code::before,
1953
+ .gdp-source-virtual-row.gdp-source-cursor .gdp-source-virtual-line-code::before {
1954
+ content: "";
1955
+ display: inline-block;
1956
+ width: 2px;
1957
+ height: 1.05em;
1958
+ margin: 0 8px 0 -8px;
1959
+ vertical-align: -0.16em;
1960
+ background: var(--scope-accent);
1961
+ }
1757
1962
  .gdp-source-table tr.gdp-source-line-target .gdp-source-line-number,
1758
1963
  .gdp-source-table tr.gdp-source-line-target .gdp-source-line-code,
1759
1964
  .gdp-source-virtual-row.gdp-source-line-target {
@@ -1907,6 +2112,9 @@ table.d2h-diff-table tr.gdp-diff-line-target > td:first-child {
1907
2112
  border-color: var(--accent);
1908
2113
  color: var(--accent);
1909
2114
  }
2115
+ .gdp-source-virtual-copy:hover {
2116
+ border-color: var(--border);
2117
+ }
1910
2118
  .gdp-source-virtual-scroller {
1911
2119
  position: relative;
1912
2120
  overflow: auto;
@@ -3055,7 +3263,7 @@ body.gdp-file-detail-page #empty {
3055
3263
  font-style: italic;
3056
3264
  }
3057
3265
 
3058
- /* ===== Media (binary image/video) ===== */
3266
+ /* ===== Media (binary image/video/audio) ===== */
3059
3267
  .gdp-media {
3060
3268
  display: flex;
3061
3269
  gap: 16px;
@@ -3091,7 +3299,8 @@ body.gdp-file-detail-page #empty {
3091
3299
  color: var(--danger);
3092
3300
  }
3093
3301
  .gdp-media img,
3094
- .gdp-media video {
3302
+ .gdp-media video,
3303
+ .gdp-media audio {
3095
3304
  max-width: 100%;
3096
3305
  max-height: 70vh;
3097
3306
  border: 1px solid var(--border);
@@ -3106,6 +3315,12 @@ body.gdp-file-detail-page #empty {
3106
3315
  background-position: 0 0, 0 8px, 8px -8px, -8px 0;
3107
3316
  object-fit: contain;
3108
3317
  }
3318
+ .gdp-media audio {
3319
+ width: min(100%, 480px);
3320
+ max-height: none;
3321
+ padding: 8px;
3322
+ background-image: none;
3323
+ }
3109
3324
  .gdp-media .media-empty {
3110
3325
  width: 100%;
3111
3326
  min-height: 120px;
package/web-src/routes.ts CHANGED
@@ -19,9 +19,10 @@ export type AppRoute =
19
19
  | { screen: 'repo'; ref: string; path: string; range: DiffRange }
20
20
  | { screen: 'diff'; range: DiffRange; path?: string; line?: SourceLineTarget }
21
21
  | { screen: 'file'; path: string; ref: string; range: DiffRange; view?: 'blob' | 'detail'; line?: SourceLineTarget }
22
+ | { screen: 'help'; range: DiffRange; lang: string; section: string }
22
23
  | { screen: 'unknown'; reason: 'unknown-pathname' | 'missing-path'; rawPathname: string; rawSearch: string; range: DiffRange };
23
24
 
24
- export const SPA_PATHS = ['/todif', '/todiff', '/file'] as const;
25
+ export const SPA_PATHS = ['/todif', '/todiff', '/file', '/help'] as const;
25
26
  export const APP_ENTRY_PATHS = ['/', '/index.html'] as const;
26
27
 
27
28
  export function assertNever(value: never): never {
@@ -89,6 +90,13 @@ export function parseRoute(pathname: string, search: string, fallbackRange: Diff
89
90
  if (!path) return { screen: 'unknown', reason: 'missing-path', rawPathname: pathname, rawSearch: search, range };
90
91
  return { screen: 'file', path, ref, range, view: target ? 'blob' : 'detail', ...(line ? { line } : {}) };
91
92
  }
93
+ case '/help':
94
+ return {
95
+ screen: 'help',
96
+ range,
97
+ lang: params.get('lang') || 'en',
98
+ section: params.get('section') || 'keybindings',
99
+ };
92
100
  default:
93
101
  return { screen: 'unknown', reason: 'unknown-pathname', rawPathname: pathname, rawSearch: search, range };
94
102
  }
@@ -119,6 +127,13 @@ export function buildRoute(route: AppRoute): string {
119
127
  '&to=' + encodeURIComponent(route.range.to || 'worktree') +
120
128
  (route.path ? '&path=' + encodeURIComponent(route.path) : '') +
121
129
  (route.line ? '&line=' + encodeURIComponent(formatLineTarget(route.line)) : '');
130
+ case 'help': {
131
+ const params = new URLSearchParams();
132
+ if (route.lang && route.lang !== 'en') params.set('lang', route.lang);
133
+ if (route.section && route.section !== 'keybindings') params.set('section', route.section);
134
+ const qs = params.toString();
135
+ return '/help' + (qs ? '?' + qs : '');
136
+ }
122
137
  case 'unknown':
123
138
  return '/todif?from=' + encodeURIComponent(route.range.from || '') +
124
139
  '&to=' + encodeURIComponent(route.range.to || 'worktree');
@@ -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 = {