farming-code 2.2.11 → 2.2.13

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.
Files changed (100) hide show
  1. package/README.md +9 -8
  2. package/README.zh_cn.md +9 -8
  3. package/backend/agent-manager.js +108 -5
  4. package/backend/codex-session-archive.js +13 -4
  5. package/backend/config-manager.js +5 -0
  6. package/backend/executable-discovery.js +2 -0
  7. package/backend/server.js +72 -0
  8. package/backend/usage-monitor.js +97 -15
  9. package/backend/workspace-directory.js +80 -0
  10. package/dist/assets/App-xASd_Y0F.js +206 -0
  11. package/dist/assets/{FileEditorMarkdownPreview--Cri80nQ.js → FileEditorMarkdownPreview-UlVd0O4D.js} +1 -1
  12. package/dist/assets/FileEditorPane-Dxx4rKOg.js +2 -0
  13. package/dist/assets/IconGlyphs-AU22gPDY.js +1 -0
  14. package/dist/assets/{ProjectFilesSection-Cz1wG4NT.js → ProjectFilesSection-DE7Nb9Jk.js} +3 -3
  15. package/dist/assets/ReviewPage-CeDKDyZh.js +3 -0
  16. package/dist/assets/code-dark-DPiuyeSp.css +1 -0
  17. package/dist/assets/{index-miO2r9IZ.js → index-CYZKdoqi.js} +2 -2
  18. package/dist/assets/main-Bqtb5kuz.css +1 -0
  19. package/dist/assets/{monaco.contribution-CyLosfZI.js → monaco.contribution-BXz-lfFB.js} +2 -2
  20. package/dist/assets/{tsMode-DGZTlTj8.js → tsMode-B0oUTcXQ.js} +1 -1
  21. package/dist/assets/workspace-editor-monaco-BICJESG0.js +4 -0
  22. package/dist/assets/workspace-editor-monaco-DRJ7IaXk.js +1 -0
  23. package/dist/crt-markdown-assets/KaTeX_AMS-Regular-CYEKBG2K.woff +0 -0
  24. package/dist/crt-markdown-assets/KaTeX_AMS-Regular-JKX5W2C4.ttf +0 -0
  25. package/dist/crt-markdown-assets/KaTeX_AMS-Regular-U6PRYMIZ.woff2 +0 -0
  26. package/dist/crt-markdown-assets/KaTeX_Caligraphic-Bold-5QL5CMTE.woff2 +0 -0
  27. package/dist/crt-markdown-assets/KaTeX_Caligraphic-Bold-WZ3QSGD3.woff +0 -0
  28. package/dist/crt-markdown-assets/KaTeX_Caligraphic-Bold-ZTS3R3HK.ttf +0 -0
  29. package/dist/crt-markdown-assets/KaTeX_Caligraphic-Regular-3LKEU76G.woff +0 -0
  30. package/dist/crt-markdown-assets/KaTeX_Caligraphic-Regular-A7XRTZ5Q.ttf +0 -0
  31. package/dist/crt-markdown-assets/KaTeX_Caligraphic-Regular-KX5MEWCF.woff2 +0 -0
  32. package/dist/crt-markdown-assets/KaTeX_Fraktur-Bold-2QVFK6NQ.woff2 +0 -0
  33. package/dist/crt-markdown-assets/KaTeX_Fraktur-Bold-T4SWXBMT.woff +0 -0
  34. package/dist/crt-markdown-assets/KaTeX_Fraktur-Bold-WGHVTYOR.ttf +0 -0
  35. package/dist/crt-markdown-assets/KaTeX_Fraktur-Regular-2PEIFJSJ.woff2 +0 -0
  36. package/dist/crt-markdown-assets/KaTeX_Fraktur-Regular-5U4OPH2X.ttf +0 -0
  37. package/dist/crt-markdown-assets/KaTeX_Fraktur-Regular-PQMHCIK6.woff +0 -0
  38. package/dist/crt-markdown-assets/KaTeX_Main-Bold-2GA4IZIN.woff +0 -0
  39. package/dist/crt-markdown-assets/KaTeX_Main-Bold-W5FBVCZM.ttf +0 -0
  40. package/dist/crt-markdown-assets/KaTeX_Main-Bold-YP5VVQRP.woff2 +0 -0
  41. package/dist/crt-markdown-assets/KaTeX_Main-BoldItalic-4P4C7HJH.woff +0 -0
  42. package/dist/crt-markdown-assets/KaTeX_Main-BoldItalic-N4V3DX7S.woff2 +0 -0
  43. package/dist/crt-markdown-assets/KaTeX_Main-BoldItalic-ODMLBJJQ.ttf +0 -0
  44. package/dist/crt-markdown-assets/KaTeX_Main-Italic-I43T2HSR.ttf +0 -0
  45. package/dist/crt-markdown-assets/KaTeX_Main-Italic-RELBIK7M.woff2 +0 -0
  46. package/dist/crt-markdown-assets/KaTeX_Main-Italic-SASNQFN2.woff +0 -0
  47. package/dist/crt-markdown-assets/KaTeX_Main-Regular-ARRPAO67.woff2 +0 -0
  48. package/dist/crt-markdown-assets/KaTeX_Main-Regular-P5I74A2A.woff +0 -0
  49. package/dist/crt-markdown-assets/KaTeX_Main-Regular-W74P5G27.ttf +0 -0
  50. package/dist/crt-markdown-assets/KaTeX_Math-BoldItalic-6EBV3DK5.woff +0 -0
  51. package/dist/crt-markdown-assets/KaTeX_Math-BoldItalic-K4WTGH3J.woff2 +0 -0
  52. package/dist/crt-markdown-assets/KaTeX_Math-BoldItalic-VB447A4D.ttf +0 -0
  53. package/dist/crt-markdown-assets/KaTeX_Math-Italic-6KGCHLFN.woff2 +0 -0
  54. package/dist/crt-markdown-assets/KaTeX_Math-Italic-KKK3USB2.woff +0 -0
  55. package/dist/crt-markdown-assets/KaTeX_Math-Italic-SON4MRCA.ttf +0 -0
  56. package/dist/crt-markdown-assets/KaTeX_SansSerif-Bold-RRNVJFFW.woff2 +0 -0
  57. package/dist/crt-markdown-assets/KaTeX_SansSerif-Bold-STQ6RXC7.ttf +0 -0
  58. package/dist/crt-markdown-assets/KaTeX_SansSerif-Bold-X5M5EMOD.woff +0 -0
  59. package/dist/crt-markdown-assets/KaTeX_SansSerif-Italic-HMPFTM52.woff2 +0 -0
  60. package/dist/crt-markdown-assets/KaTeX_SansSerif-Italic-PSN4QKYX.woff +0 -0
  61. package/dist/crt-markdown-assets/KaTeX_SansSerif-Italic-WTBAZBGY.ttf +0 -0
  62. package/dist/crt-markdown-assets/KaTeX_SansSerif-Regular-2TL3USAE.ttf +0 -0
  63. package/dist/crt-markdown-assets/KaTeX_SansSerif-Regular-OQCII6EP.woff +0 -0
  64. package/dist/crt-markdown-assets/KaTeX_SansSerif-Regular-XIQ62X4E.woff2 +0 -0
  65. package/dist/crt-markdown-assets/KaTeX_Script-Regular-72OLXYNA.ttf +0 -0
  66. package/dist/crt-markdown-assets/KaTeX_Script-Regular-A5IFOEBS.woff +0 -0
  67. package/dist/crt-markdown-assets/KaTeX_Script-Regular-APUWIHLP.woff2 +0 -0
  68. package/dist/crt-markdown-assets/KaTeX_Size1-Regular-4HRHTS65.woff +0 -0
  69. package/dist/crt-markdown-assets/KaTeX_Size1-Regular-5LRUTBFT.woff2 +0 -0
  70. package/dist/crt-markdown-assets/KaTeX_Size1-Regular-7K6AASVL.ttf +0 -0
  71. package/dist/crt-markdown-assets/KaTeX_Size2-Regular-222HN3GT.ttf +0 -0
  72. package/dist/crt-markdown-assets/KaTeX_Size2-Regular-K5ZHAIS6.woff +0 -0
  73. package/dist/crt-markdown-assets/KaTeX_Size2-Regular-LELKET5D.woff2 +0 -0
  74. package/dist/crt-markdown-assets/KaTeX_Size3-Regular-TLFPAHDE.woff +0 -0
  75. package/dist/crt-markdown-assets/KaTeX_Size3-Regular-UFCO6WCA.ttf +0 -0
  76. package/dist/crt-markdown-assets/KaTeX_Size3-Regular-WQRQ47UD.woff2 +0 -0
  77. package/dist/crt-markdown-assets/KaTeX_Size4-Regular-7PGNVPQK.ttf +0 -0
  78. package/dist/crt-markdown-assets/KaTeX_Size4-Regular-CDMV7U5C.woff2 +0 -0
  79. package/dist/crt-markdown-assets/KaTeX_Size4-Regular-PKMWZHNC.woff +0 -0
  80. package/dist/crt-markdown-assets/KaTeX_Typewriter-Regular-3F5K6SQ6.ttf +0 -0
  81. package/dist/crt-markdown-assets/KaTeX_Typewriter-Regular-MJMFSK64.woff +0 -0
  82. package/dist/crt-markdown-assets/KaTeX_Typewriter-Regular-VBYJ4NRC.woff2 +0 -0
  83. package/dist/crt-markdown-renderer.css +1 -0
  84. package/dist/crt-markdown-renderer.js +361 -0
  85. package/dist/crt-mermaid-renderer.js +3595 -0
  86. package/dist/farming-2/site.webmanifest +2 -0
  87. package/dist/index.html +1 -1
  88. package/frontend/skins/crt/app.js +55 -32
  89. package/frontend/skins/crt/index.html +47 -31
  90. package/frontend/skins/crt/styles/markdown.css +251 -0
  91. package/frontend/skins/crt/styles/monochrome-green.css +14 -3
  92. package/package.json +5 -2
  93. package/dist/assets/App-CHYpgECZ.js +0 -208
  94. package/dist/assets/FileEditorPane-DGJlEKdf.js +0 -2
  95. package/dist/assets/IconGlyphs-Cc14sepw.js +0 -1
  96. package/dist/assets/ReviewPage-LonRc6Fz.js +0 -3
  97. package/dist/assets/code-dark-B1pAtO_P.css +0 -1
  98. package/dist/assets/main-DNe0jPw-.css +0 -1
  99. package/dist/assets/workspace-editor-monaco-5gFbeDPF.js +0 -1
  100. package/dist/assets/workspace-editor-monaco-xPv8JzHH.js +0 -4
@@ -9,8 +9,8 @@ const { attachQuotaForecasts } = require('./usage-forecast');
9
9
  const execFileAsync = promisify(execFile);
10
10
 
11
11
  const USAGE_WINDOW_MS = 5 * 60 * 1000;
12
- const USAGE_TIMELINE_WINDOW_MS = 60 * 60 * 1000;
13
- const USAGE_TIMELINE_BUCKET_COUNT = 30;
12
+ const USAGE_TIMELINE_WINDOW_MS = 24 * 60 * 60 * 1000;
13
+ const USAGE_TIMELINE_BUCKET_COUNT = 24;
14
14
  const USAGE_DAILY_DAYS = 52 * 7;
15
15
  const USAGE_DAILY_CACHE_MS = 5 * 60 * 1000;
16
16
  const USAGE_LIVE_DAY_CACHE_MS = 5 * 1000;
@@ -152,6 +152,32 @@ function addTokenBreakdown(target, source) {
152
152
  return target;
153
153
  }
154
154
 
155
+ function usageAgentIdFromFilePath(filePath) {
156
+ const basename = path.basename(String(filePath || ''), path.extname(String(filePath || '')));
157
+ const uuid = basename.match(/([0-9a-f]{8}-[0-9a-f-]{27})$/i);
158
+ return uuid?.[1] || basename || 'unattributed';
159
+ }
160
+
161
+ function usageAgentLabel(provider, agentId) {
162
+ const providerName = {
163
+ codex: 'Codex',
164
+ claude: 'Claude',
165
+ opencode: 'OpenCode',
166
+ qoder: 'Qoder',
167
+ }[provider] || provider;
168
+ if (!agentId || agentId === 'unattributed') return providerName;
169
+ const shortId = agentId.length > 12 ? `…${agentId.slice(-6)}` : agentId;
170
+ return `${providerName} · ${shortId}`;
171
+ }
172
+
173
+ function attributeUsageEvent(event, provider, agentId) {
174
+ return {
175
+ ...event,
176
+ agentId,
177
+ agentLabel: usageAgentLabel(provider, agentId),
178
+ };
179
+ }
180
+
155
181
  function subtractTokenBreakdown(current, previous) {
156
182
  const result = emptyTokenBreakdown();
157
183
  for (const field of Object.keys(result)) {
@@ -268,7 +294,9 @@ function buildUsageTimeline(providerEvents, options = {}) {
268
294
  const windowMs = Math.max(60_000, options.windowMs ?? USAGE_TIMELINE_WINDOW_MS);
269
295
  const bucketCount = Math.max(1, Math.floor(options.bucketCount ?? USAGE_TIMELINE_BUCKET_COUNT));
270
296
  const bucketMs = windowMs / bucketCount;
271
- const endAt = now;
297
+ const endAt = options.alignToBucket
298
+ ? Math.ceil(now / bucketMs) * bucketMs
299
+ : now;
272
300
  const startAt = endAt - windowMs;
273
301
  const providerNames = Object.keys(providerEvents || {});
274
302
  const points = Array.from({ length: bucketCount }, (_, index) => ({
@@ -652,11 +680,14 @@ function buildUsageDayDetail(providerEvents, options = {}) {
652
680
  hour,
653
681
  label: String(hour).padStart(2, '0'),
654
682
  ...emptyTokenBreakdown(),
683
+ agents: {},
655
684
  }));
656
685
  const providers = Object.fromEntries(
657
686
  providerNames.map(provider => [provider, emptyTokenBreakdown()]),
658
687
  );
659
688
  const total = emptyTokenBreakdown();
689
+ const agents = new Map();
690
+ const agentLabels = options.agentLabels instanceof Map ? options.agentLabels : new Map();
660
691
 
661
692
  for (const provider of providerNames) {
662
693
  for (const event of providerEvents[provider] || []) {
@@ -664,8 +695,24 @@ function buildUsageDayDetail(providerEvents, options = {}) {
664
695
  if (localDateKey(timestamp) !== date) continue;
665
696
  const hour = new Date(timestamp).getHours();
666
697
  if (!Number.isInteger(hour) || hour < 0 || hour > 23) continue;
698
+ const agentId = String(event?.agentId || 'unattributed');
699
+ const agentKey = `${provider}:${agentId}`;
700
+ let agent = agents.get(agentKey);
701
+ if (!agent) {
702
+ agent = {
703
+ key: agentKey,
704
+ provider,
705
+ sessionId: agentId === 'unattributed' ? '' : agentId,
706
+ label: agentLabels.get(agentKey) || event?.agentLabel || usageAgentLabel(provider, agentId),
707
+ ...emptyTokenBreakdown(),
708
+ };
709
+ agents.set(agentKey, agent);
710
+ }
711
+ if (!hours[hour].agents[agentKey]) hours[hour].agents[agentKey] = emptyTokenBreakdown();
667
712
  addTokenBreakdown(hours[hour], event);
713
+ addTokenBreakdown(hours[hour].agents[agentKey], event);
668
714
  addTokenBreakdown(providers[provider], event);
715
+ addTokenBreakdown(agent, event);
669
716
  addTokenBreakdown(total, event);
670
717
  }
671
718
  }
@@ -677,6 +724,9 @@ function buildUsageDayDetail(providerEvents, options = {}) {
677
724
  total,
678
725
  hours,
679
726
  providers,
727
+ agents: Array.from(agents.values()).sort((left, right) => (
728
+ right.totalTokens - left.totalTokens || left.label.localeCompare(right.label)
729
+ )),
680
730
  };
681
731
  }
682
732
 
@@ -709,9 +759,11 @@ async function readDailyFileEvents(filePath, provider, minimumMtimeMs = 0) {
709
759
  }
710
760
  }
711
761
 
762
+ const agentId = usageAgentIdFromFilePath(filePath);
763
+ const attributedEvents = events.map(event => attributeUsageEvent(event, provider, agentId));
712
764
  const truncated = stat.size > JSONL_TAIL_BYTES;
713
- dailyFileEventCache.set(cacheKey, { signature, events, truncated });
714
- return { events, mtimeMs: stat.mtimeMs, truncated };
765
+ dailyFileEventCache.set(cacheKey, { signature, events: attributedEvents, truncated });
766
+ return { events: attributedEvents, mtimeMs: stat.mtimeMs, truncated };
715
767
  }
716
768
 
717
769
  async function collectCodexDailyEventsWithRipgrep(roots, options = {}) {
@@ -765,7 +817,9 @@ async function collectCodexDailyEventsWithRipgrep(roots, options = {}) {
765
817
  states.set(filePath, state);
766
818
  }
767
819
  const event = codexTokenEventFromRecord(record, state);
768
- if (event && event.timestamp >= cutoffMs && event.timestamp <= now + 60_000) events.push(event);
820
+ if (event && event.timestamp >= cutoffMs && event.timestamp <= now + 60_000) {
821
+ events.push(attributeUsageEvent(event, 'codex', usageAgentIdFromFilePath(filePath)));
822
+ }
769
823
  });
770
824
  child.once('close', code => {
771
825
  if (unavailable || (code !== 0 && code !== 1)) {
@@ -892,7 +946,8 @@ async function collectOpenCodeDailyEvents(homePaths, options = {}) {
892
946
  { openCodeHome: session.openCodeHome, timeoutMs: OPENCODE_COMMAND_TIMEOUT_MS },
893
947
  );
894
948
  const exported = JSON.parse(String(result?.stdout || '{}'));
895
- const sessionEvents = openCodeTokenEventsFromExport(exported, { cutoffMs, now });
949
+ const sessionEvents = openCodeTokenEventsFromExport(exported, { cutoffMs, now })
950
+ .map(event => attributeUsageEvent(event, 'opencode', session.id));
896
951
  successfulExports += 1;
897
952
  openCodeSessionEventCache.set(cacheKey, { updatedAt: session.updatedAt, events: sessionEvents });
898
953
  events.push(...sessionEvents);
@@ -1212,6 +1267,33 @@ class UsageMonitor {
1212
1267
  this.liveDayCache.fetchedAt = 0;
1213
1268
  }
1214
1269
 
1270
+ usageAgentLabels() {
1271
+ const labels = new Map();
1272
+ const agents = this.agentManager?.getState?.().agents;
1273
+ if (!Array.isArray(agents)) return labels;
1274
+ for (const agent of agents) {
1275
+ const provider = String(agent?.providerSessionProvider || '').trim();
1276
+ const sessionId = String(agent?.providerSessionId || '').trim();
1277
+ if (!provider || !sessionId) continue;
1278
+ const label = String(
1279
+ agent.customTitle
1280
+ || agent.task
1281
+ || agent.sessionTitle
1282
+ || agent.providerSessionTitle
1283
+ || '',
1284
+ ).trim();
1285
+ if (label) labels.set(`${provider}:${sessionId}`, label);
1286
+ }
1287
+ return labels;
1288
+ }
1289
+
1290
+ buildUsageDay(providerEvents, date) {
1291
+ return buildUsageDayDetail(providerEvents, {
1292
+ date,
1293
+ agentLabels: this.usageAgentLabels(),
1294
+ });
1295
+ }
1296
+
1215
1297
  getDailyUsage(options = {}) {
1216
1298
  const now = options.now ?? Date.now();
1217
1299
  if (
@@ -1247,7 +1329,7 @@ class UsageMonitor {
1247
1329
  if (options.live === true && String(date || '').trim() === localDateKey(now)) {
1248
1330
  const liveDate = String(date).trim();
1249
1331
  const dailyFallback = this.dailyCache.value?.providerEvents
1250
- ? buildUsageDayDetail(this.dailyCache.value.providerEvents, { date: liveDate })
1332
+ ? this.buildUsageDay(this.dailyCache.value.providerEvents, liveDate)
1251
1333
  : null;
1252
1334
  const cachedFallback = this.liveDayCache.date === liveDate
1253
1335
  ? this.liveDayCache.value
@@ -1283,7 +1365,7 @@ class UsageMonitor {
1283
1365
  now,
1284
1366
  days: 1,
1285
1367
  }).then(history => {
1286
- const detail = buildUsageDayDetail(history.providerEvents, { date: liveDate });
1368
+ const detail = this.buildUsageDay(history.providerEvents, liveDate);
1287
1369
  if (this.liveDayCache.date === liveDate) {
1288
1370
  this.liveDayCache.value = detail;
1289
1371
  this.liveDayCache.fetchedAt = now;
@@ -1299,7 +1381,7 @@ class UsageMonitor {
1299
1381
  now,
1300
1382
  force: options.fresh === true,
1301
1383
  });
1302
- return buildUsageDayDetail(history.providerEvents, { date });
1384
+ return this.buildUsageDay(history.providerEvents, date);
1303
1385
  }
1304
1386
 
1305
1387
  async getUsageSummary(options = {}) {
@@ -1330,11 +1412,11 @@ class UsageMonitor {
1330
1412
  historyWindowMs,
1331
1413
  source: 'opencode session export',
1332
1414
  });
1333
- const timeline = buildUsageTimeline({
1334
- codex: codexUsage.tokenEvents,
1335
- claude: claudeUsage.tokenEvents,
1336
- opencode: openCodeUsage.tokenEvents,
1337
- }, { now, windowMs: historyWindowMs });
1415
+ const timeline = buildUsageTimeline(history.providerEvents, {
1416
+ now,
1417
+ windowMs: historyWindowMs,
1418
+ alignToBucket: true,
1419
+ });
1338
1420
  const openCodeCoverage = history.coverage.find(entry => entry.provider === 'opencode');
1339
1421
  const qoderCoverage = history.coverage.find(entry => entry.provider === 'qoder');
1340
1422
 
@@ -5,6 +5,7 @@ const path = require('path');
5
5
 
6
6
  const CREATE_FORBIDDEN_CODES = new Set(['EACCES', 'EPERM', 'EROFS']);
7
7
  const INVALID_PATH_CODES = new Set(['EINVAL', 'ENAMETOOLONG']);
8
+ const DEFAULT_BROWSE_LIMIT = 500;
8
9
 
9
10
  function resolveWorkspaceDirectory(value, homeDir = process.env.HOME || os.homedir()) {
10
11
  const input = typeof value === 'string' ? value.trim() : '';
@@ -131,9 +132,87 @@ async function prepareWorkspaceDirectory(value, options = {}) {
131
132
  }
132
133
  }
133
134
 
135
+ async function browseWorkspaceDirectory(value, options = {}) {
136
+ const workspace = resolveWorkspaceDirectory(value || '~', options.homeDir);
137
+ const fileSystem = options.fileSystem || fs.promises;
138
+ let entries;
139
+ try {
140
+ const stat = await fileSystem.stat(workspace);
141
+ if (!stat.isDirectory()) {
142
+ return {
143
+ status: 409,
144
+ body: {
145
+ status: 'rejected',
146
+ code: 'workspace-not-directory',
147
+ workspace,
148
+ message: `Workspace path is not a directory: ${workspace}`,
149
+ },
150
+ };
151
+ }
152
+ entries = await fileSystem.readdir(workspace, { withFileTypes: true });
153
+ } catch (error) {
154
+ const forbidden = CREATE_FORBIDDEN_CODES.has(error?.code);
155
+ const missing = error?.code === 'ENOENT';
156
+ return {
157
+ status: forbidden ? 403 : missing ? 404 : 500,
158
+ body: {
159
+ status: 'rejected',
160
+ code: forbidden
161
+ ? 'workspace-browse-forbidden'
162
+ : missing
163
+ ? 'workspace-not-found'
164
+ : 'workspace-browse-failed',
165
+ workspace,
166
+ message: forbidden
167
+ ? `Farming does not have permission to read this directory: ${workspace}`
168
+ : missing
169
+ ? `Workspace directory does not exist: ${workspace}`
170
+ : `Failed to read workspace directory: ${workspace}`,
171
+ },
172
+ };
173
+ }
174
+
175
+ const directories = (await Promise.all(entries.map(async (entry) => {
176
+ if (!entry.isDirectory() && !entry.isSymbolicLink()) return null;
177
+ const childPath = path.join(workspace, entry.name);
178
+ if (entry.isSymbolicLink()) {
179
+ try {
180
+ const stat = await fileSystem.stat(childPath);
181
+ if (!stat.isDirectory()) return null;
182
+ } catch {
183
+ return null;
184
+ }
185
+ }
186
+ return { name: entry.name, path: childPath };
187
+ })))
188
+ .filter(Boolean)
189
+ .sort((left, right) => left.name.localeCompare(right.name));
190
+ const limit = Math.max(1, Math.min(Number(options.limit) || DEFAULT_BROWSE_LIMIT, DEFAULT_BROWSE_LIMIT));
191
+ const parentPath = path.dirname(workspace);
192
+
193
+ return {
194
+ status: 200,
195
+ body: {
196
+ status: 'ready',
197
+ workspace,
198
+ parent: parentPath === workspace ? null : parentPath,
199
+ directories: directories.slice(0, limit),
200
+ truncated: directories.length > limit,
201
+ },
202
+ };
203
+ }
204
+
134
205
  function createWorkspaceDirectoryRouter(options = {}) {
135
206
  const router = express.Router();
136
207
  router.use(express.json({ limit: '8kb' }));
208
+ router.get('/browse', async (req, res) => {
209
+ const result = await browseWorkspaceDirectory(req.query?.path, {
210
+ fileSystem: options.fileSystem,
211
+ homeDir: options.homeDir,
212
+ limit: req.query?.limit,
213
+ });
214
+ res.status(result.status).json(result.body);
215
+ });
137
216
  router.post('/prepare', async (req, res) => {
138
217
  const result = await prepareWorkspaceDirectory(req.body?.workspace, {
139
218
  create: req.body?.create === true,
@@ -146,6 +225,7 @@ function createWorkspaceDirectoryRouter(options = {}) {
146
225
  }
147
226
 
148
227
  module.exports = {
228
+ browseWorkspaceDirectory,
149
229
  createWorkspaceDirectoryRouter,
150
230
  prepareWorkspaceDirectory,
151
231
  resolveWorkspaceDirectory,