@orion-agents/orion-code 0.3.24 → 0.3.25

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/CHANGELOG.md CHANGED
@@ -22,11 +22,42 @@ which is **not** a pass.
22
22
 
23
23
  ## [Unreleased]
24
24
 
25
- ## [0.3.24]
25
+ ## [0.3.25] — UNRELEASED
26
26
 
27
- > **Status: candidate.** Implementation on `codex/v0.3.24`, based on `main@6ed47fd`.
28
- > Plan: `docs/plan/v0.3.24-plan.md`. Registry and release status must be
29
- > verified from their own immutable receipts, not inferred from this changelog.
27
+ > **Status: candidate.** Open. Four open-issue fixes: the Git conflict view reads the
28
+ > edited working file from the filesystem instead of an unstored object (#269), file
29
+ > history walks the requested revision instead of always HEAD (#270), model switching
30
+ > gates the compact preflight on effective input budgets instead of raw context windows
31
+ > (#271), and the left project navigation keeps a stable registration order when
32
+ > sessions are activated (#272). Plan: `docs/plan/v0.3.25-plan.md`. Not merged, tagged
33
+ > or published.
34
+
35
+ ### Fixed
36
+
37
+ - #269: `GET /git/conflict` no longer fails with `git cat-file: could not get object
38
+ info` after editing a conflicted file before staging — the `worktree` version is read
39
+ from the filesystem (contained within the repository root) instead of an object
40
+ `hash-object` never wrote.
41
+ - #270: `GET /git/file-history` with a `rev` now walks that revision; previously the
42
+ validated revision never reached the `git log` command and every query walked HEAD.
43
+ - #271: switching to a model with the same or larger raw context window but a smaller
44
+ effective input budget (larger output reserve) now reports `compactRequired` and runs
45
+ the compact preflight instead of silently persisting a model the history cannot fit.
46
+
47
+ ### Changed
48
+
49
+ - #272: the left project navigation keeps a stable registration order for unpinned
50
+ projects (`registeredAt`, persisted by the workspace registry and exposed additively
51
+ on `WebWorkspaceSummaryV1`); activating sessions no longer reshuffles the tree.
52
+ Pinned projects keep `pinnedOrder` first.
53
+
54
+ ## [0.3.24] — 2026-10-13
55
+
56
+ > **Status: published.** Released as `@orion-agents/orion-code@0.3.24` from
57
+ > `refs/tags/v0.3.24`, and merged to `main` as `85fadb4` (#268). Session
58
+ > navigation & responsiveness for the Web Workbench. Plan:
59
+ > `docs/plan/v0.3.24-plan.md`. GitHub Release:
60
+ > https://github.com/orion-agents/orion-code/releases/tag/v0.3.24
30
61
 
31
62
  ### Fixed
32
63
 
package/README.md CHANGED
@@ -2,9 +2,12 @@
2
2
 
3
3
  Local-first, goal-driven coding agent for the terminal and browser.
4
4
 
5
- > v0.3.24 development focus — Web session responsiveness: Session-scoped request admission,
6
- > cached foreground switching, retained live views, and bounded Git probes during creation.
7
- > Built on the merged v0.3.23 source (`6ed47fd`, #266).
5
+ > v0.3.25 candidate — open. Four open-issue fixes: the Git conflict view reads the edited
6
+ > working file from the filesystem (#269), file history walks the requested revision
7
+ > (#270), model switching gates the compact preflight on effective input budgets (#271),
8
+ > and the left project navigation keeps a stable registration order (#272). The previous
9
+ > release 0.3.24 is on npm (registry `latest`), tagged `v0.3.24` (`85fadb4`, #268) and
10
+ > released on GitHub.
8
11
 
9
12
  [中文说明](README.zh-CN.md) ·
10
13
  [v0.3.24 plan](docs/plan/v0.3.24-plan.md) ·
@@ -167,10 +170,10 @@ user extension boundaries.
167
170
  Node.js 22.12+, 24, and 26 are supported. Use Node 24 LTS for production or Node 26 Current for
168
171
  current development environments. Node 20 is upstream EOL and is no longer a v0.3 runtime.
169
172
 
170
- To install the pinned v0.3.24 package when it is available on npm:
173
+ To install the pinned v0.3.25 package when it is available on npm:
171
174
 
172
175
  ```bash
173
- npm install -g @orion-agents/orion-code@0.3.24
176
+ npm install -g @orion-agents/orion-code@0.3.25
174
177
  orion --version
175
178
  orion doctor
176
179
  ```
package/README.zh-CN.md CHANGED
@@ -2,8 +2,10 @@
2
2
 
3
3
  面向终端与浏览器、本地优先的目标驱动 Coding Agent。
4
4
 
5
- > v0.3.24 开发重点:Web 会话响应性。发送按会话等待、有效缓存即时切换、保留实时显示内容,
6
- > 新建使用有界 Git 探测。基于已合并的 v0.3.23 源码(`6ed47fd`,#266)。
5
+ > v0.3.25 候选版本:进行中。四个开放 issue 修复:Git 冲突视图从文件系统读取已编辑的
6
+ > 工作区文件(#269)、文件历史按请求的 revision 遍历(#270)、模型切换按有效输入预算
7
+ > 触发压缩预检(#271)、左侧项目导航保持稳定注册序(#272)。上一版 0.3.24 已发布 npm
8
+ > (registry `latest`)、打 tag `v0.3.24`(`85fadb4`,#268)并发布 GitHub Release。
7
9
 
8
10
  [English](README.md) ·
9
11
  [v0.3.24 方案](docs/plan/v0.3.24-plan.md) ·
@@ -98,10 +100,10 @@ MCP 仍是用户可见的扩展边界。
98
100
  支持 Node.js 22.12+、24 和 26。生产环境建议使用 Node 24 LTS,当前开发环境也支持 Node 26
99
101
  Current。Node 20 已结束上游维护,不再属于 v0.3 Runtime 合同。
100
102
 
101
- v0.3.24 在 npm 可用后,可安装固定版本:
103
+ v0.3.25 在 npm 可用后,可安装固定版本:
102
104
 
103
105
  ```bash
104
- npm install -g @orion-agents/orion-code@0.3.24
106
+ npm install -g @orion-agents/orion-code@0.3.25
105
107
  orion --version
106
108
  orion doctor
107
109
  ```
@@ -94,4 +94,5 @@ export declare class ModelCoordinator extends EventEmitter {
94
94
  private validateTargetClient;
95
95
  private commitResolvedTarget;
96
96
  }
97
+ export declare function resolveProfileSafeInputBudget(profile: ResolvedModelProfile): number;
97
98
  //# sourceMappingURL=model-coordinator.d.ts.map
@@ -11,6 +11,7 @@
11
11
  Object.defineProperty(exports, "__esModule", { value: true });
12
12
  exports.ModelCoordinator = void 0;
13
13
  exports.validateModelSwitchCompactReceipt = validateModelSwitchCompactReceipt;
14
+ exports.resolveProfileSafeInputBudget = resolveProfileSafeInputBudget;
14
15
  const events_1 = require("events");
15
16
  function validateModelSwitchCompactReceipt(receipt, targetTokens, safeInputBudget = targetTokens) {
16
17
  if (receipt.status === 'rejected')
@@ -114,7 +115,9 @@ class ModelCoordinator extends events_1.EventEmitter {
114
115
  if (clientError)
115
116
  return { success: false, error: clientError };
116
117
  const prev = this.current;
117
- if (!prev || target.resolvedContextWindow >= prev.resolvedContextWindow) {
118
+ // v0.3.25 #271 — same budget gate as commitSwitch: the effective input
119
+ // budgets decide, not raw context windows.
120
+ if (!prev || resolveProfileSafeInputBudget(target) >= resolveProfileSafeInputBudget(prev)) {
118
121
  this.commitResolvedTarget(target, prev);
119
122
  return {
120
123
  success: true,
@@ -190,7 +193,11 @@ class ModelCoordinator extends events_1.EventEmitter {
190
193
  let compacted = false;
191
194
  let compactRequired = false;
192
195
  let compactPreflight = 'not_required';
193
- if (prev && target.resolvedContextWindow < prev.resolvedContextWindow) {
196
+ // v0.3.25 #271 — gate the preflight on the effective input budgets, not raw
197
+ // context windows: a target with the same (or larger) window but a larger
198
+ // output reserve can still have a smaller safe input budget, and the
199
+ // unchanged history would exceed it without any compact.
200
+ if (prev && resolveProfileSafeInputBudget(target) < resolveProfileSafeInputBudget(prev)) {
194
201
  compactRequired = true;
195
202
  const safeInputBudget = resolveProfileSafeInputBudget(target);
196
203
  const targetTokens = Math.max(1, Math.floor(safeInputBudget * 0.65));
@@ -9,6 +9,7 @@ const effort_1 = require("../services/effort");
9
9
  const global_config_1 = require("../services/global-config");
10
10
  const session_storage_1 = require("../services/session-storage");
11
11
  const token_estimate_1 = require("../utils/token-estimate");
12
+ const model_coordinator_1 = require("./model-coordinator");
12
13
  const canonical_1 = require("./protocol/canonical");
13
14
  class SessionComposerControlError extends Error {
14
15
  constructor(code, message) {
@@ -96,7 +97,10 @@ class SessionComposerControlServiceV1 {
96
97
  toModelId: profile.id,
97
98
  requestedEffort,
98
99
  compacted: false,
99
- compactRequired: profile.resolvedContextWindow < this.project(session, true).model.contextWindow,
100
+ // v0.3.25 #271 — same gate as the live path: the effective input
101
+ // budgets decide, not raw context windows.
102
+ compactRequired: (0, model_coordinator_1.resolveProfileSafeInputBudget)(profile) <
103
+ (0, model_coordinator_1.resolveProfileSafeInputBudget)(this.resolveProfile(session.model)),
100
104
  compactPreflight: 'deferred',
101
105
  appliesFrom: 'next-logical-request',
102
106
  sessionPreferencesDigest: (0, canonical_1.digestRuntimeValue)({
@@ -3,6 +3,15 @@ export interface WorkspaceRegistryEntryV1 {
3
3
  readonly canonicalPath: string;
4
4
  readonly label: string;
5
5
  readonly lastActivatedAt: string;
6
+ /**
7
+ * v0.3.25 #272 — when the workspace was first registered. Navigation order is
8
+ * derived from this stable timestamp; `lastActivatedAt` no longer reorders
9
+ * the tree when sessions are activated. Documents persisted before this field
10
+ * existed are backfilled from `lastActivatedAt` on parse (and persisted with
11
+ * the backfilled value on the next write), which keeps the migration
12
+ * deterministic.
13
+ */
14
+ readonly registeredAt: string;
6
15
  readonly pinnedOrder?: number;
7
16
  }
8
17
  export interface WorkspaceRegistryOptions {
@@ -47,6 +47,7 @@ class WorkspaceRegistryV1 {
47
47
  canonicalPath,
48
48
  label: workspaceLabel(canonicalPath),
49
49
  lastActivatedAt: options.activated ? now : (existing?.lastActivatedAt ?? now),
50
+ registeredAt: existing?.registeredAt ?? now,
50
51
  ...(existing?.pinnedOrder !== undefined ? { pinnedOrder: existing.pinnedOrder } : {}),
51
52
  });
52
53
  const entries = existing
@@ -74,6 +75,7 @@ class WorkspaceRegistryV1 {
74
75
  canonicalPath: path,
75
76
  label: workspaceLabel(path),
76
77
  lastActivatedAt: path === active ? now : (existing?.lastActivatedAt ?? now),
78
+ registeredAt: existing?.registeredAt ?? now,
77
79
  ...(existing?.pinnedOrder !== undefined ? { pinnedOrder: existing.pinnedOrder } : {}),
78
80
  }));
79
81
  }
@@ -102,6 +104,7 @@ class WorkspaceRegistryV1 {
102
104
  canonicalPath: target.canonicalPath,
103
105
  label: target.label,
104
106
  lastActivatedAt: target.lastActivatedAt,
107
+ registeredAt: target.registeredAt,
105
108
  ...(pinned ? { pinnedOrder: target.pinnedOrder ?? currentMax + 1 } : {}),
106
109
  });
107
110
  this.writeDocument(document.entries.map(entry => (entry.id === id ? updated : entry)));
@@ -189,6 +192,11 @@ function parseEntry(value) {
189
192
  !row.label.trim() ||
190
193
  typeof row.lastActivatedAt !== 'string' ||
191
194
  !Number.isFinite(Date.parse(row.lastActivatedAt)) ||
195
+ // v0.3.25 #272 — older documents have no registeredAt; backfill from
196
+ // lastActivatedAt so the migration is deterministic and the write-back
197
+ // persists the stable value.
198
+ (row.registeredAt !== undefined &&
199
+ (typeof row.registeredAt !== 'string' || !Number.isFinite(Date.parse(row.registeredAt)))) ||
192
200
  (row.pinnedOrder !== undefined &&
193
201
  (!Number.isSafeInteger(row.pinnedOrder) || Number(row.pinnedOrder) < 1))) {
194
202
  return invalidDocument();
@@ -198,6 +206,7 @@ function parseEntry(value) {
198
206
  canonicalPath: row.canonicalPath,
199
207
  label: row.label,
200
208
  lastActivatedAt: row.lastActivatedAt,
209
+ registeredAt: typeof row.registeredAt === 'string' ? row.registeredAt : row.lastActivatedAt,
201
210
  ...(row.pinnedOrder !== undefined ? { pinnedOrder: Number(row.pinnedOrder) } : {}),
202
211
  });
203
212
  }
@@ -209,8 +218,10 @@ function compareEntries(left, right) {
209
218
  const rightPinned = right.pinnedOrder ?? Number.MAX_SAFE_INTEGER;
210
219
  if (leftPinned !== rightPinned)
211
220
  return leftPinned - rightPinned;
212
- const recent = Date.parse(right.lastActivatedAt) - Date.parse(left.lastActivatedAt);
213
- return recent || left.label.localeCompare(right.label) || left.id.localeCompare(right.id);
221
+ // v0.3.25 #272 — stable registration order: activating a session must not
222
+ // reorder the navigation tree, so lastActivatedAt no longer participates.
223
+ const registration = Date.parse(left.registeredAt) - Date.parse(right.registeredAt);
224
+ return registration || left.label.localeCompare(right.label) || left.id.localeCompare(right.id);
214
225
  }
215
226
  function freezeEntries(entries) {
216
227
  return Object.freeze([...entries].sort(compareEntries).map(entry => Object.freeze({ ...entry })));
@@ -572,11 +572,14 @@ function createGitHistoryReader(options) {
572
572
  // `--follow` only accepts a single path, which is exactly this case. It explains its own
573
573
  // limits for copies and complex merges (plan G6) — we surface `followed` rather than
574
574
  // pretending tracking is complete.
575
+ // v0.3.25 #270 — the validated revision must reach the log command: it was
576
+ // previously used only for the existence check, so every query walked HEAD.
575
577
  const raw = await runGit([
576
578
  'log',
577
579
  `--max-count=${limit + 1}`,
578
580
  `--format=${LIST_FORMAT}${RECORD_SEPARATOR}`,
579
581
  '--follow',
582
+ target,
580
583
  '--',
581
584
  path,
582
585
  ]);
@@ -207,6 +207,15 @@ export declare class GitReadModelServiceV1 {
207
207
  }): Promise<GitDiffDocumentV2>;
208
208
  private versionReaderInstance?;
209
209
  private versionReader;
210
+ /**
211
+ * v0.3.25 #269 — reads one working-tree file for the conflict view, refusing
212
+ * to leave the repository: the requested relative path (already guarded as
213
+ * relative and non-sensitive by the version reader) is resolved against the
214
+ * repository root and both the file's real path and its parent's real path
215
+ * must stay inside the root's real path. Returns null instead of throwing so
216
+ * a missing or unreadable file reads as an absent worktree version.
217
+ */
218
+ private readContainedWorktreeFile;
210
219
  /**
211
220
  * v0.3.17 S5 — base / ours / theirs / worktree for a conflicted file.
212
221
  *
@@ -6,6 +6,7 @@ exports.assertRelativeGitPath = assertRelativeGitPath;
6
6
  const child_process_1 = require("child_process");
7
7
  const crypto_1 = require("crypto");
8
8
  const fs_1 = require("fs");
9
+ const promises_1 = require("fs/promises");
9
10
  const path_1 = require("path");
10
11
  const redaction_1 = require("../services/redaction");
11
12
  const errors_1 = require("./errors");
@@ -349,9 +350,43 @@ class GitReadModelServiceV1 {
349
350
  this.versionReaderInstance ?? (this.versionReaderInstance = (0, git_version_service_1.createGitVersionReader)({
350
351
  runGit: async (args) => this.runGit(args, await this.requireRepositoryRoot()),
351
352
  runGitBytes: async (args) => this.runGitBytes(args, await this.requireRepositoryRoot()),
353
+ readWorktreeFile: async (relativePath) => this.readContainedWorktreeFile(relativePath),
352
354
  }));
353
355
  return this.versionReaderInstance;
354
356
  }
357
+ /**
358
+ * v0.3.25 #269 — reads one working-tree file for the conflict view, refusing
359
+ * to leave the repository: the requested relative path (already guarded as
360
+ * relative and non-sensitive by the version reader) is resolved against the
361
+ * repository root and both the file's real path and its parent's real path
362
+ * must stay inside the root's real path. Returns null instead of throwing so
363
+ * a missing or unreadable file reads as an absent worktree version.
364
+ */
365
+ async readContainedWorktreeFile(relativePath) {
366
+ let root;
367
+ try {
368
+ root = (0, fs_1.realpathSync)(await this.requireRepositoryRoot());
369
+ }
370
+ catch {
371
+ return null;
372
+ }
373
+ try {
374
+ const absolute = (0, path_1.resolve)(root, relativePath);
375
+ const fileReal = (0, fs_1.realpathSync)(absolute);
376
+ const parentReal = (0, fs_1.realpathSync)((0, path_1.resolve)(fileReal, '..'));
377
+ if (fileReal !== root && !fileReal.startsWith(root + path_1.sep))
378
+ return null;
379
+ if (parentReal !== root && !parentReal.startsWith(root + path_1.sep))
380
+ return null;
381
+ const stat = (0, fs_1.statSync)(fileReal);
382
+ if (!stat.isFile())
383
+ return null;
384
+ return await (0, promises_1.readFile)(fileReal);
385
+ }
386
+ catch {
387
+ return null;
388
+ }
389
+ }
355
390
  /**
356
391
  * v0.3.17 S5 — base / ours / theirs / worktree for a conflicted file.
357
392
  *
@@ -66,6 +66,14 @@ export interface GitVersionReaderOptions {
66
66
  readonly runGit: (args: readonly string[]) => Promise<string>;
67
67
  /** Runs git and returns raw bytes, for content that may not be valid UTF-8. */
68
68
  readonly runGitBytes: (args: readonly string[]) => Promise<Buffer>;
69
+ /**
70
+ * v0.3.25 #269 — reads one working-tree file relative to the repository root,
71
+ * returning null when it does not exist or cannot be read safely. The
72
+ * `worktree` conflict version must come from the filesystem: `hash-object`
73
+ * without `-w` computes an OID that is not in the object database, so any
74
+ * edited working file made `cat-file` fail the whole conflict response.
75
+ */
76
+ readonly readWorktreeFile?: (relativePath: string) => Promise<Buffer | null>;
69
77
  }
70
78
  export declare function createGitVersionReader(options: GitVersionReaderOptions): GitVersionReader;
71
79
  //# sourceMappingURL=git-version-service.d.ts.map
@@ -85,28 +85,25 @@ function parseLfsPointer(head) {
85
85
  });
86
86
  }
87
87
  function createGitVersionReader(options) {
88
- const { runGit, runGitBytes } = options;
88
+ const { runGit, runGitBytes, readWorktreeFile } = options;
89
+ /** Absent version — the label legitimately has no content for this shape. */
90
+ const absentVersion = (label) => Object.freeze({
91
+ label,
92
+ oid: null,
93
+ exists: false,
94
+ binary: false,
95
+ byteSize: 0,
96
+ truncated: false,
97
+ content: null,
98
+ lfs: null,
99
+ });
89
100
  /**
90
- * Reads one blob by OID, bounded, with binary and LFS detection.
91
- *
92
- * The size is read first so an enormous historical file is reported by its size instead of
93
- * being pulled into memory just to be truncated.
101
+ * v0.3.25 #269 — decodes already-fetched bytes into a version, shared by the
102
+ * object-database reader and the working-tree reader so both apply the same
103
+ * size bound, binary probe and LFS-pointer detection.
94
104
  */
95
- const readVersion = async (label, oid) => {
96
- if (oid === null) {
97
- return Object.freeze({
98
- label,
99
- oid: null,
100
- exists: false,
101
- binary: false,
102
- byteSize: 0,
103
- truncated: false,
104
- content: null,
105
- lfs: null,
106
- });
107
- }
108
- const sizeRaw = (await runGit(['cat-file', '-s', oid])).trim();
109
- const byteSize = Number(sizeRaw) || 0;
105
+ const versionFromBytes = (label, oid, bytes) => {
106
+ const byteSize = bytes.length;
110
107
  if (byteSize > MAX_VERSION_BYTES) {
111
108
  return Object.freeze({
112
109
  label,
@@ -119,7 +116,6 @@ function createGitVersionReader(options) {
119
116
  lfs: null,
120
117
  });
121
118
  }
122
- const bytes = await runGitBytes(['cat-file', 'blob', oid]);
123
119
  const probe = bytes.subarray(0, LFS_PROBE_BYTES).toString('utf8');
124
120
  const lfs = parseLfsPointer(probe);
125
121
  if (lfs) {
@@ -149,6 +145,31 @@ function createGitVersionReader(options) {
149
145
  dataUrl,
150
146
  });
151
147
  };
148
+ /**
149
+ * Reads one blob by OID, bounded, with binary and LFS detection.
150
+ *
151
+ * The size is read first so an enormous historical file is reported by its size instead of
152
+ * being pulled into memory just to be truncated.
153
+ */
154
+ const readVersion = async (label, oid) => {
155
+ if (oid === null)
156
+ return absentVersion(label);
157
+ const sizeRaw = (await runGit(['cat-file', '-s', oid])).trim();
158
+ const byteSize = Number(sizeRaw) || 0;
159
+ if (byteSize > MAX_VERSION_BYTES) {
160
+ return Object.freeze({
161
+ label,
162
+ oid,
163
+ exists: true,
164
+ binary: false,
165
+ byteSize,
166
+ truncated: true,
167
+ content: null,
168
+ lfs: null,
169
+ });
170
+ }
171
+ return versionFromBytes(label, oid, await runGitBytes(['cat-file', 'blob', oid]));
172
+ };
152
173
  return {
153
174
  async conflict(pathInput) {
154
175
  const path = assertRelativePath(pathInput);
@@ -168,15 +189,18 @@ function createGitVersionReader(options) {
168
189
  continue;
169
190
  stageOids.set(Number(stage), oid);
170
191
  }
171
- const worktreeOid = (await runGit(['hash-object', '--path=' + path, '--', path]).catch(() => ''))
172
- .toString()
173
- .trim();
174
- const worktreeExists = /^[0-9a-f]{7,64}$/iu.test(worktreeOid);
192
+ // v0.3.25 #269 — the working file is read from the filesystem. The previous
193
+ // `hash-object` (without -w) → `cat-file` chain only worked while the edited
194
+ // content happened to match an object git already stored; any real edit made
195
+ // `cat-file` fail the entire conflict response.
196
+ const worktreeBytes = await readWorktreeFile?.(path);
175
197
  const [base, ours, theirs, worktree] = await Promise.all([
176
198
  readVersion('base', stageOids.get(1) ?? null),
177
199
  readVersion('ours', stageOids.get(2) ?? null),
178
200
  readVersion('theirs', stageOids.get(3) ?? null),
179
- worktreeExists ? readVersion('worktree', worktreeOid) : readVersion('worktree', null),
201
+ worktreeBytes
202
+ ? Promise.resolve(versionFromBytes('worktree', null, worktreeBytes))
203
+ : Promise.resolve(absentVersion('worktree')),
180
204
  ]);
181
205
  const hasBase = stageOids.has(1);
182
206
  const hasOurs = stageOids.has(2);
@@ -361,6 +361,11 @@ export interface WebWorkspaceSummaryV1 {
361
361
  readonly available: boolean;
362
362
  readonly sessionCount: number;
363
363
  readonly lastActivatedAt: string;
364
+ /**
365
+ * v0.3.25 #272 — when the workspace was first registered. The navigation's
366
+ * stable default order is derived from this; optional for older Hosts.
367
+ */
368
+ readonly registeredAt?: string;
364
369
  readonly pinnedOrder?: number;
365
370
  }
366
371
  export interface WebWorkspaceProjectSummaryV1 {
@@ -238,6 +238,7 @@ class WebWorkbenchController {
238
238
  available,
239
239
  sessionCount: counts.get(entry.canonicalPath) ?? 0,
240
240
  lastActivatedAt: entry.lastActivatedAt,
241
+ registeredAt: entry.registeredAt,
241
242
  ...(entry.pinnedOrder !== undefined ? { pinnedOrder: entry.pinnedOrder } : {}),
242
243
  });
243
244
  }));
@@ -1 +1 @@
1
- import{r as a,j as e,I as D,s as be,C as ye,W as ge}from"./index-DZOlUgt6.js";import{u as Ce,R as Ne}from"./useAutoNotice-Ylj5bfSH.js";function Le({workspaceId:s,refreshEpoch:o,actions:l,navigatorWidthPx:r,onNavigatorWidthCommit:d,revealRequest:c}){const[f,m]=a.useState({}),[x,j]=a.useState(new Set(["workspace-root"])),[i,N]=a.useState(null),[F,B]=a.useState(""),[M,G]=a.useState(null),[Y,P]=a.useState(!1),[H,K]=a.useState(""),{notice:J,showNotice:$,clearNotice:V}=Ce(),[T,ue]=a.useState(""),[Z,_]=a.useState({}),[I,A]=a.useState(""),[O,R]=a.useState("view"),[L,w]=a.useState(""),[k,q]=a.useState(!1),[de,S]=a.useState(!1),[fe,me]=a.useState(""),[he,W]=a.useState(!1),h=a.useRef(0),C=a.useRef(0),ee=a.useRef(null),E=async(t,n=!1,y=h.current)=>{if(y!==h.current)return;const p=f[t],g=n?p?.nextCursor:void 0;if(!(n&&!g)){m(u=>({...u,[t]:{items:p?.items??[],nextCursor:p?.nextCursor??null,revision:p?.revision??"",loading:!0}}));try{const u=await l.listFiles(t,g??void 0);if(y!==h.current)return;m(v=>({...v,[t]:{items:n?ke(v[t]?.items??[],u.items):u.items,nextCursor:u.nextCursor,revision:u.revision,loading:!1}}))}catch(u){if(y!==h.current)return;if(n&&U(u)){$("目录已变化,已重新载入第一页。"),E(t,!1,y);return}m(v=>({...v,[t]:{items:v[t]?.items??[],nextCursor:v[t]?.nextCursor??null,revision:v[t]?.revision??"",loading:!1,error:z(u)}}))}}},Q=async(t=h.current)=>{try{const n=await l.gitStatus();if(t!==h.current)return;_(Se(n)),A(n.nextCursor?"Git 装饰仅显示首批 200 个变更;在 Git 面板继续分页查看。":"")}catch(n){if(t!==h.current)return;_({}),A(`Git 状态装饰不可用:${z(n)}`)}},te=a.useRef(s);a.useEffect(()=>{const t=h.current+1;h.current=t,C.current+=1;const n=te.current===s?ee.current:null;te.current=s,m({}),j(new Set(["workspace-root"])),N(n),B(""),G(null),P(!1),K(""),V(),_({}),A(""),s&&(E("workspace-root",!1,t),Q(t),n&&b(n,!1,!1,t))},[o,s]);const se=a.useRef(null),b=async(t,n=!1,y=!1,p=h.current,g=C.current+1)=>{if(p===h.current&&!(!t.readable||t.sensitive||X(t))){C.current=g,N(t),K(""),n||(B(""),G(null),P(!1),R("view"),w(""),S(!1)),y||V();try{const u=await l.readFileContent(t.id,n?M??void 0:void 0);if(p!==h.current||g!==C.current)return;P(u.binary),B(v=>n?`${v}${u.content??""}`:u.content??""),G(u.nextCursor),me(u.revision),y&&$("文件已变化,已从第一页重新载入。")}catch(u){if(p!==h.current||g!==C.current)return;if(n&&U(u)){await b(t,!1,!0,p,g);return}K(z(u))}}},re=De({selected:i,binary:Y,hasMorePages:M!==null}),xe=()=>{w(F),S(!1),R("edit")},ie=()=>{if(L!==F){W(!0);return}ne()},ne=()=>{R("view"),w(""),S(!1),W(!1)},je=async()=>{if(!(!i||k)){q(!0),S(!1);try{await l.writeFileContent(i.id,L,fe),R("view"),w(""),await b(i,!1,!0,h.current,C.current+1),$("已保存文件。","success"),Q()}catch(t){U(t)?S(!0):$(z(t),"error")}finally{q(!1)}}},ae=t=>{const n=new Set(x);n.has(t)?n.delete(t):(n.add(t),f[t]||E(t)),j(n)};a.useEffect(()=>{c&&se.current!==c.id&&(se.current=c.id,b({id:c.fileId,name:c.displayPath.split("/").pop()??c.displayPath,displayPath:c.displayPath,kind:"file",modifiedAt:new Date().toISOString(),sensitive:!1,readable:!0}))},[c?.id]),a.useEffect(()=>{ee.current=i},[i]);const pe=a.useMemo(()=>Object.values(f).flatMap(t=>t.items),[f]),oe=T.trim()?pe.filter(t=>t.name.toLocaleLowerCase().includes(T.toLocaleLowerCase())):null,ve=!!f["workspace-root"]?.loading;return e.jsxs("div",{className:"work-resource-panel files-panel",children:[e.jsxs("div",{className:"resource-toolbar",children:[e.jsxs("label",{className:"resource-search",children:[e.jsx("span",{className:"sr-only",children:"搜索已加载文件"}),e.jsx(D,{name:"search",size:16}),e.jsx("input",{type:"search",value:T,onChange:t=>ue(t.target.value),placeholder:"搜索已加载文件",title:"搜索仅覆盖已加载的目录和文件"})]}),e.jsx("button",{type:"button",className:"icon-button","aria-label":"刷新文件树","aria-busy":ve,onClick:()=>{E("workspace-root"),Q()},children:e.jsx(D,{name:"refresh",size:16})})]}),e.jsxs(Ne,{panelId:"files",navigatorWidthPx:r,onNavigatorWidthCommit:d,contentLabel:"文件预览",navigatorLabel:"工作区文件",handleLabel:"调整文件目录宽度",contentClassName:"file-preview",navigatorClassName:"file-tree",children:[e.jsxs(e.Fragment,{children:[I?e.jsx("p",{className:"resource-hint",role:"status",children:I}):null,oe?e.jsx(e.Fragment,{children:e.jsx("ul",{role:"list",className:"file-node-list search-results",children:oe.map(t=>e.jsx(ce,{node:t,depth:0,expanded:!1,selected:i?.id===t.id,gitLabels:Z[t.displayPath]??[],onToggle:ae,onSelect:n=>{b(n)}},t.id))})}):e.jsx(le,{parentId:"workspace-root",depth:0,directories:f,expanded:x,selectedId:i?.id,gitDecorations:Z,onToggle:ae,onSelect:t=>{b(t)},onLoadMore:t=>{E(t,!0)}})]}),e.jsxs(e.Fragment,{children:[J?e.jsx("p",{className:"resource-notice",role:"status",children:J.text}):null,i?e.jsxs(e.Fragment,{children:[e.jsxs("header",{children:[e.jsx("strong",{children:i.name}),e.jsx("span",{children:Fe(i.sizeBytes??0)})]}),H?e.jsx("p",{className:"resource-error",role:"alert",children:H}):Y?e.jsxs("div",{className:"resource-empty",children:[e.jsx(D,{name:"code",size:16}),e.jsx("strong",{title:"出于安全和性能考虑,只显示元数据。",children:"二进制文件"})]}):e.jsxs(e.Fragment,{children:[e.jsx("div",{className:"file-preview-actions",children:O==="view"?e.jsx("button",{type:"button",className:"text-button",disabled:!re,title:re?void 0:"仅支持编辑 512 KB 内的 UTF-8 文本文件",onClick:xe,children:"编辑"}):e.jsxs(e.Fragment,{children:[e.jsx("button",{type:"button",className:"primary-button",disabled:k||L===F,"aria-busy":k,onClick:()=>{je()},children:k?"保存中…":"保存"}),e.jsx("button",{type:"button",className:"text-button",disabled:k,onClick:ie,children:"取消"})]})}),de?e.jsxs("p",{className:"resource-error",role:"alert",children:["文件已在别处变更,保存被拒绝。",e.jsx("button",{type:"button",className:"text-button",onClick:()=>{b(i,!1,!0)},children:"重新加载"})]}):null,O==="edit"?e.jsx("textarea",{className:"file-editor",value:L,spellCheck:!1,"aria-label":`编辑文件 ${i.name}`,onChange:t=>w(t.target.value),onKeyDown:t=>{t.key==="Escape"&&(t.preventDefault(),ie())}}):e.jsx("pre",{tabIndex:0,className:"file-code-view","aria-label":`文件内容 ${i.name}`,children:be(F)})]}),M&&O==="view"?e.jsx("button",{type:"button",className:"secondary-button",onClick:()=>{b(i,!0)},children:"加载更多内容"}):null]}):e.jsxs("div",{className:"resource-empty",children:[e.jsx(D,{name:"workspace",size:16}),e.jsx("strong",{children:"选择文件预览"}),e.jsx("p",{title:"敏感文件、工作区外链接和二进制正文不会返回浏览器。",children:"受限内容不会返回浏览器。"})]})]})]}),e.jsx(ye,{open:he,onClose:()=>W(!1),title:"放弃未保存的修改?",body:"当前草稿尚未保存,放弃后将回到文件的最新内容。",confirmLabel:"放弃修改",danger:!0,onConfirm:ne})]})}function le({parentId:s,depth:o,directories:l,expanded:r,selectedId:d,gitDecorations:c,onToggle:f,onSelect:m,onLoadMore:x}){const j=l[s];return j?j.error?e.jsx("p",{className:"resource-error",role:"alert",children:j.error}):e.jsxs("ul",{role:"list",className:"file-node-list",children:[j.items.map(i=>{const N=r.has(i.id);return e.jsxs("li",{children:[e.jsx(ce,{node:i,depth:o,expanded:N,selected:d===i.id,gitLabels:c[i.displayPath]??[],onToggle:f,onSelect:m}),X(i)&&N?e.jsx(le,{parentId:i.id,depth:o+1,directories:l,expanded:r,selectedId:d,gitDecorations:c,onToggle:f,onSelect:m,onLoadMore:x}):null]},i.id)}),j.loading?e.jsx("li",{className:"resource-loading",children:"正在读取…"}):null,j.nextCursor?e.jsx("li",{children:e.jsx("button",{type:"button",className:"text-button",onClick:()=>x(s),children:"加载更多"})}):null]}):e.jsx("p",{className:"resource-loading",children:"正在读取…"})}function ce({node:s,depth:o,expanded:l,selected:r,gitLabels:d,onToggle:c,onSelect:f}){const m=s.sensitive||!s.readable,x=X(s);return e.jsxs("button",{type:"button",className:`file-node ${r?"selected":""} ${m?"blocked":""}`,style:{paddingInlineStart:`${8+o*15}px`},"aria-expanded":x?l:void 0,"aria-label":`${we(s)} ${s.name}${m?",不可读取":""}${d.length?`,Git ${d.join("、")}`:""}`,onClick:()=>x?c(s.id):f(s),disabled:m,children:[e.jsx(D,{name:x?"workspace":"code",size:16}),e.jsx("span",{children:s.name}),s.kind==="symlink"?e.jsx("small",{children:"链接"}):null,s.sensitive?e.jsx("small",{children:"敏感"}):null,d.length?e.jsx("small",{className:"file-git-status",title:`Git ${d.join("、")}`,children:d.join(" · ")}):null]})}function we(s){return s.kind==="directory"?"目录":s.kind==="symlink"?s.targetKind==="directory"?"符号链接目录":"符号链接":"文件"}function X(s){return s.kind==="directory"||s.kind==="symlink"&&s.targetKind==="directory"}function ke(s,o){const l=new Map(s.map(r=>[r.id,r]));for(const r of o)l.set(r.id,r);return[...l.values()]}function Se(s){if(!s.hasWorktree)return{};const o=new Map,l=(r,d)=>{const c=r.replace(/\\/gu,"/").replace(/^\.\//u,"");if(!c)return;const f=o.get(c)??new Set;f.add(d),o.set(c,f);const m=c.split("/");for(let x=1;x<m.length;x+=1){const j=m.slice(0,x).join("/"),i=o.get(j)??new Set;i.add("含变更"),o.set(j,i)}};return s.conflicted.forEach(r=>l(r.path,"冲突")),s.staged.forEach(r=>l(r.path,"已暂存")),s.unstaged.forEach(r=>l(r.path,"未暂存")),s.untracked.forEach(r=>l(r.path,"未跟踪")),Object.fromEntries([...o].map(([r,d])=>[r,Object.freeze([...d])]))}const Ee=512*1024;function De(s){const{selected:o,binary:l,hasMorePages:r}=s;return!o||l||o.sensitive||!o.readable||r?!1:(o.sizeBytes??0)<=Ee}function Fe(s){return s<1024?`${s} B`:s<1048576?`${(s/1024).toFixed(1)} KB`:`${(s/1048576).toFixed(1)} MB`}function U(s){return s instanceof ge&&s.code==="file_revision_conflict"}function z(s){return s instanceof Error?s.message:"文件请求失败。"}export{Ee as FILE_EDIT_MAX_BYTES,Le as FilesPanel,De as canEditFileContent};
1
+ import{r as a,j as e,I as D,s as be,C as ye,W as ge}from"./index-kQqA3li6.js";import{u as Ce,R as Ne}from"./useAutoNotice-DPXFWIwK.js";function Le({workspaceId:s,refreshEpoch:o,actions:l,navigatorWidthPx:r,onNavigatorWidthCommit:d,revealRequest:c}){const[f,m]=a.useState({}),[x,j]=a.useState(new Set(["workspace-root"])),[i,N]=a.useState(null),[F,B]=a.useState(""),[M,G]=a.useState(null),[Y,P]=a.useState(!1),[H,K]=a.useState(""),{notice:J,showNotice:$,clearNotice:V}=Ce(),[T,ue]=a.useState(""),[Z,_]=a.useState({}),[I,A]=a.useState(""),[O,R]=a.useState("view"),[L,w]=a.useState(""),[k,q]=a.useState(!1),[de,S]=a.useState(!1),[fe,me]=a.useState(""),[he,W]=a.useState(!1),h=a.useRef(0),C=a.useRef(0),ee=a.useRef(null),E=async(t,n=!1,y=h.current)=>{if(y!==h.current)return;const p=f[t],g=n?p?.nextCursor:void 0;if(!(n&&!g)){m(u=>({...u,[t]:{items:p?.items??[],nextCursor:p?.nextCursor??null,revision:p?.revision??"",loading:!0}}));try{const u=await l.listFiles(t,g??void 0);if(y!==h.current)return;m(v=>({...v,[t]:{items:n?ke(v[t]?.items??[],u.items):u.items,nextCursor:u.nextCursor,revision:u.revision,loading:!1}}))}catch(u){if(y!==h.current)return;if(n&&U(u)){$("目录已变化,已重新载入第一页。"),E(t,!1,y);return}m(v=>({...v,[t]:{items:v[t]?.items??[],nextCursor:v[t]?.nextCursor??null,revision:v[t]?.revision??"",loading:!1,error:z(u)}}))}}},Q=async(t=h.current)=>{try{const n=await l.gitStatus();if(t!==h.current)return;_(Se(n)),A(n.nextCursor?"Git 装饰仅显示首批 200 个变更;在 Git 面板继续分页查看。":"")}catch(n){if(t!==h.current)return;_({}),A(`Git 状态装饰不可用:${z(n)}`)}},te=a.useRef(s);a.useEffect(()=>{const t=h.current+1;h.current=t,C.current+=1;const n=te.current===s?ee.current:null;te.current=s,m({}),j(new Set(["workspace-root"])),N(n),B(""),G(null),P(!1),K(""),V(),_({}),A(""),s&&(E("workspace-root",!1,t),Q(t),n&&b(n,!1,!1,t))},[o,s]);const se=a.useRef(null),b=async(t,n=!1,y=!1,p=h.current,g=C.current+1)=>{if(p===h.current&&!(!t.readable||t.sensitive||X(t))){C.current=g,N(t),K(""),n||(B(""),G(null),P(!1),R("view"),w(""),S(!1)),y||V();try{const u=await l.readFileContent(t.id,n?M??void 0:void 0);if(p!==h.current||g!==C.current)return;P(u.binary),B(v=>n?`${v}${u.content??""}`:u.content??""),G(u.nextCursor),me(u.revision),y&&$("文件已变化,已从第一页重新载入。")}catch(u){if(p!==h.current||g!==C.current)return;if(n&&U(u)){await b(t,!1,!0,p,g);return}K(z(u))}}},re=De({selected:i,binary:Y,hasMorePages:M!==null}),xe=()=>{w(F),S(!1),R("edit")},ie=()=>{if(L!==F){W(!0);return}ne()},ne=()=>{R("view"),w(""),S(!1),W(!1)},je=async()=>{if(!(!i||k)){q(!0),S(!1);try{await l.writeFileContent(i.id,L,fe),R("view"),w(""),await b(i,!1,!0,h.current,C.current+1),$("已保存文件。","success"),Q()}catch(t){U(t)?S(!0):$(z(t),"error")}finally{q(!1)}}},ae=t=>{const n=new Set(x);n.has(t)?n.delete(t):(n.add(t),f[t]||E(t)),j(n)};a.useEffect(()=>{c&&se.current!==c.id&&(se.current=c.id,b({id:c.fileId,name:c.displayPath.split("/").pop()??c.displayPath,displayPath:c.displayPath,kind:"file",modifiedAt:new Date().toISOString(),sensitive:!1,readable:!0}))},[c?.id]),a.useEffect(()=>{ee.current=i},[i]);const pe=a.useMemo(()=>Object.values(f).flatMap(t=>t.items),[f]),oe=T.trim()?pe.filter(t=>t.name.toLocaleLowerCase().includes(T.toLocaleLowerCase())):null,ve=!!f["workspace-root"]?.loading;return e.jsxs("div",{className:"work-resource-panel files-panel",children:[e.jsxs("div",{className:"resource-toolbar",children:[e.jsxs("label",{className:"resource-search",children:[e.jsx("span",{className:"sr-only",children:"搜索已加载文件"}),e.jsx(D,{name:"search",size:16}),e.jsx("input",{type:"search",value:T,onChange:t=>ue(t.target.value),placeholder:"搜索已加载文件",title:"搜索仅覆盖已加载的目录和文件"})]}),e.jsx("button",{type:"button",className:"icon-button","aria-label":"刷新文件树","aria-busy":ve,onClick:()=>{E("workspace-root"),Q()},children:e.jsx(D,{name:"refresh",size:16})})]}),e.jsxs(Ne,{panelId:"files",navigatorWidthPx:r,onNavigatorWidthCommit:d,contentLabel:"文件预览",navigatorLabel:"工作区文件",handleLabel:"调整文件目录宽度",contentClassName:"file-preview",navigatorClassName:"file-tree",children:[e.jsxs(e.Fragment,{children:[I?e.jsx("p",{className:"resource-hint",role:"status",children:I}):null,oe?e.jsx(e.Fragment,{children:e.jsx("ul",{role:"list",className:"file-node-list search-results",children:oe.map(t=>e.jsx(ce,{node:t,depth:0,expanded:!1,selected:i?.id===t.id,gitLabels:Z[t.displayPath]??[],onToggle:ae,onSelect:n=>{b(n)}},t.id))})}):e.jsx(le,{parentId:"workspace-root",depth:0,directories:f,expanded:x,selectedId:i?.id,gitDecorations:Z,onToggle:ae,onSelect:t=>{b(t)},onLoadMore:t=>{E(t,!0)}})]}),e.jsxs(e.Fragment,{children:[J?e.jsx("p",{className:"resource-notice",role:"status",children:J.text}):null,i?e.jsxs(e.Fragment,{children:[e.jsxs("header",{children:[e.jsx("strong",{children:i.name}),e.jsx("span",{children:Fe(i.sizeBytes??0)})]}),H?e.jsx("p",{className:"resource-error",role:"alert",children:H}):Y?e.jsxs("div",{className:"resource-empty",children:[e.jsx(D,{name:"code",size:16}),e.jsx("strong",{title:"出于安全和性能考虑,只显示元数据。",children:"二进制文件"})]}):e.jsxs(e.Fragment,{children:[e.jsx("div",{className:"file-preview-actions",children:O==="view"?e.jsx("button",{type:"button",className:"text-button",disabled:!re,title:re?void 0:"仅支持编辑 512 KB 内的 UTF-8 文本文件",onClick:xe,children:"编辑"}):e.jsxs(e.Fragment,{children:[e.jsx("button",{type:"button",className:"primary-button",disabled:k||L===F,"aria-busy":k,onClick:()=>{je()},children:k?"保存中…":"保存"}),e.jsx("button",{type:"button",className:"text-button",disabled:k,onClick:ie,children:"取消"})]})}),de?e.jsxs("p",{className:"resource-error",role:"alert",children:["文件已在别处变更,保存被拒绝。",e.jsx("button",{type:"button",className:"text-button",onClick:()=>{b(i,!1,!0)},children:"重新加载"})]}):null,O==="edit"?e.jsx("textarea",{className:"file-editor",value:L,spellCheck:!1,"aria-label":`编辑文件 ${i.name}`,onChange:t=>w(t.target.value),onKeyDown:t=>{t.key==="Escape"&&(t.preventDefault(),ie())}}):e.jsx("pre",{tabIndex:0,className:"file-code-view","aria-label":`文件内容 ${i.name}`,children:be(F)})]}),M&&O==="view"?e.jsx("button",{type:"button",className:"secondary-button",onClick:()=>{b(i,!0)},children:"加载更多内容"}):null]}):e.jsxs("div",{className:"resource-empty",children:[e.jsx(D,{name:"workspace",size:16}),e.jsx("strong",{children:"选择文件预览"}),e.jsx("p",{title:"敏感文件、工作区外链接和二进制正文不会返回浏览器。",children:"受限内容不会返回浏览器。"})]})]})]}),e.jsx(ye,{open:he,onClose:()=>W(!1),title:"放弃未保存的修改?",body:"当前草稿尚未保存,放弃后将回到文件的最新内容。",confirmLabel:"放弃修改",danger:!0,onConfirm:ne})]})}function le({parentId:s,depth:o,directories:l,expanded:r,selectedId:d,gitDecorations:c,onToggle:f,onSelect:m,onLoadMore:x}){const j=l[s];return j?j.error?e.jsx("p",{className:"resource-error",role:"alert",children:j.error}):e.jsxs("ul",{role:"list",className:"file-node-list",children:[j.items.map(i=>{const N=r.has(i.id);return e.jsxs("li",{children:[e.jsx(ce,{node:i,depth:o,expanded:N,selected:d===i.id,gitLabels:c[i.displayPath]??[],onToggle:f,onSelect:m}),X(i)&&N?e.jsx(le,{parentId:i.id,depth:o+1,directories:l,expanded:r,selectedId:d,gitDecorations:c,onToggle:f,onSelect:m,onLoadMore:x}):null]},i.id)}),j.loading?e.jsx("li",{className:"resource-loading",children:"正在读取…"}):null,j.nextCursor?e.jsx("li",{children:e.jsx("button",{type:"button",className:"text-button",onClick:()=>x(s),children:"加载更多"})}):null]}):e.jsx("p",{className:"resource-loading",children:"正在读取…"})}function ce({node:s,depth:o,expanded:l,selected:r,gitLabels:d,onToggle:c,onSelect:f}){const m=s.sensitive||!s.readable,x=X(s);return e.jsxs("button",{type:"button",className:`file-node ${r?"selected":""} ${m?"blocked":""}`,style:{paddingInlineStart:`${8+o*15}px`},"aria-expanded":x?l:void 0,"aria-label":`${we(s)} ${s.name}${m?",不可读取":""}${d.length?`,Git ${d.join("、")}`:""}`,onClick:()=>x?c(s.id):f(s),disabled:m,children:[e.jsx(D,{name:x?"workspace":"code",size:16}),e.jsx("span",{children:s.name}),s.kind==="symlink"?e.jsx("small",{children:"链接"}):null,s.sensitive?e.jsx("small",{children:"敏感"}):null,d.length?e.jsx("small",{className:"file-git-status",title:`Git ${d.join("、")}`,children:d.join(" · ")}):null]})}function we(s){return s.kind==="directory"?"目录":s.kind==="symlink"?s.targetKind==="directory"?"符号链接目录":"符号链接":"文件"}function X(s){return s.kind==="directory"||s.kind==="symlink"&&s.targetKind==="directory"}function ke(s,o){const l=new Map(s.map(r=>[r.id,r]));for(const r of o)l.set(r.id,r);return[...l.values()]}function Se(s){if(!s.hasWorktree)return{};const o=new Map,l=(r,d)=>{const c=r.replace(/\\/gu,"/").replace(/^\.\//u,"");if(!c)return;const f=o.get(c)??new Set;f.add(d),o.set(c,f);const m=c.split("/");for(let x=1;x<m.length;x+=1){const j=m.slice(0,x).join("/"),i=o.get(j)??new Set;i.add("含变更"),o.set(j,i)}};return s.conflicted.forEach(r=>l(r.path,"冲突")),s.staged.forEach(r=>l(r.path,"已暂存")),s.unstaged.forEach(r=>l(r.path,"未暂存")),s.untracked.forEach(r=>l(r.path,"未跟踪")),Object.fromEntries([...o].map(([r,d])=>[r,Object.freeze([...d])]))}const Ee=512*1024;function De(s){const{selected:o,binary:l,hasMorePages:r}=s;return!o||l||o.sensitive||!o.readable||r?!1:(o.sizeBytes??0)<=Ee}function Fe(s){return s<1024?`${s} B`:s<1048576?`${(s/1024).toFixed(1)} KB`:`${(s/1048576).toFixed(1)} MB`}function U(s){return s instanceof ge&&s.code==="file_revision_conflict"}function z(s){return s instanceof Error?s.message:"文件请求失败。"}export{Ee as FILE_EDIT_MAX_BYTES,Le as FilesPanel,De as canEditFileContent};