@orion-agents/orion-code 0.3.18 → 0.3.19

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,63 @@ which is **not** a pass.
22
22
 
23
23
  ## [Unreleased]
24
24
 
25
+ ## [0.3.19] — 2026-09-19
26
+
27
+ > **Status: published.** Released as `@orion-agents/orion-code@0.3.19` from
28
+ > `refs/tags/v0.3.19`, and merged to `main` as `d17d211` (#259). Closes the five scenarios
29
+ > the v0.3.17 status table left as "feature present, assertion missing"
30
+ > (`docs/plan/evidence/v0.3.17-git/g317-status.md`), and fixes the three defects those
31
+ > assertions found.
32
+
33
+ ### Fixed
34
+
35
+ - **The responsive tiers were inert.** `GitPanel` attached its `ResizeObserver` from an
36
+ effect with an empty dependency list. The panel's first render is the loading placeholder,
37
+ which does not contain the measured node, so the effect read a null ref, returned, and never
38
+ ran again: the width stayed `0`, `data-width` stayed `wide` on a 480px panel, and the narrow
39
+ affordances never activated on any load whose status was not already cached. The observer is
40
+ now attached with a callback ref, so it follows the node in and out of the tree.
41
+ - **A discarded diff read left the pane blank with a file still selected.** Only a newer
42
+ *selection* supersedes a read now. The document states the revision it was rendered against,
43
+ so accepting it and letting `stale` tell the truth is both safer and correct — dropping it
44
+ because the 2s visible poll had bumped the revision abandoned the read with nothing to
45
+ re-issue it.
46
+ - **Returning from the narrow detail pane threw away the reading position.** The back control
47
+ cleared `selectedFileId` to show the list, which also dropped the anchors and the cached
48
+ document. The pane choice is now its own state.
49
+
50
+ ### Added
51
+
52
+ - **A bare repository is no longer reported as "not a repository".** `rev-parse
53
+ --show-toplevel` fails for a bare repository too, and that single failure was read as "no
54
+ repository here". Status now carries `repositoryKind: 'worktree' | 'bare' | 'absent'` and
55
+ `hasWorktree`; every worktree-backed surface gates on `hasWorktree`, which is the only flag
56
+ that cannot be wrong. A bare repository's branch is reported instead of hidden, its change
57
+ lists stay empty, and the history limit names the real reason.
58
+ - **G317 assertion backlog:** an external write no longer moves the reader (the refresh is
59
+ detected and the document is marked stale instead of silently kept); `在 Files 打开` is
60
+ asserted end to end; the theme is measured rather than assumed (system preference, the
61
+ explicit choice through Settings, reduced motion and forced colours); the narrow tier's back
62
+ affordance and overflow are asserted; and repeated open/close plus a hundred consecutive
63
+ diffs are asserted bounded, with S5/S6 samples recorded by
64
+ `scripts/perf/v0319-git-s6-samples.mjs`.
65
+
66
+ ### Known limits
67
+
68
+ - The `compact` and `wide` panel tiers are pinned by `resolveGitPanelLayout` unit tests, not in
69
+ a browser: neither seeding `workPanel.widthPx` nor dragging the splitter reproduced a tier
70
+ change in the E2E harness. `docs/plan/evidence/v0.3.17-git/s6-responsive-keyboard.md` records
71
+ the measurement.
72
+ - The drawer entry point below 1180px is not asserted; those widths assert overflow only.
73
+
25
74
  ## [0.3.18]
26
75
 
27
- > **Status: released.** Closes the assertion and evidence backlog of the Git
28
- > right-hand work panel shipped in 0.3.17, and fixes the CRLF part-staging
29
- > defect found right after that release.
76
+ > **Status: npm-published, not tagged or merged.** The exact contents below are
77
+ > published to the registry as 0.3.18, but `refs/tags/v0.3.18` does not exist and
78
+ > `main` does not carry this work yet, so the release is not complete.
79
+ > Closes the assertion and evidence backlog of the Git right-hand work panel
80
+ > shipped in 0.3.17, and fixes the CRLF part-staging defect found right after
81
+ > that release.
30
82
 
31
83
  ### Fixed
32
84
 
package/README.md CHANGED
@@ -2,11 +2,12 @@
2
2
 
3
3
  Local-first, goal-driven coding agent for the terminal and browser.
4
4
 
5
- > v0.3.15 candidate — Web Workbench visual & interaction convergence: thinned
6
- > explanatory copy across the resource panels, one unified confirm modal
7
- > replacing the last `window.confirm` guards, busy feedback for refresh/save
8
- > actions, and Orion-style visual tightening of Files / Git / Review.
9
- > Candidate source is not an npm publication, Git tag, or merge-ready release.
5
+ > v0.3.19 — closes the v0.3.17 Git work panel's assertion backlog
6
+ > (`docs/plan/evidence/v0.3.17-git/g317-status.md`) and fixes what those
7
+ > assertions found: responsive tiers that never activated, a diff read discarded
8
+ > on a background refresh that left the pane blank, a narrow back control that
9
+ > dropped the reading position, and a bare repository reported as "not a
10
+ > repository". Published to npm as 0.3.19 and tagged `v0.3.19`.
10
11
 
11
12
  [中文说明](README.zh-CN.md) ·
12
13
  [v0.3.7 plan](docs/plan/v0.3.7-left-rail-improvement-plan.md) ·
@@ -171,7 +172,7 @@ current development environments. Node 20 is upstream EOL and is no longer a v0.
171
172
  After the immutable `0.3.5` npm receipt exists:
172
173
 
173
174
  ```bash
174
- npm install -g @orion-agents/orion-code@0.3.15
175
+ npm install -g @orion-agents/orion-code@0.3.19
175
176
  orion --version
176
177
  orion doctor
177
178
  ```
package/README.zh-CN.md CHANGED
@@ -2,9 +2,10 @@
2
2
 
3
3
  面向终端与浏览器、本地优先的目标驱动 Coding Agent。
4
4
 
5
- > v0.3.15 候选版本:Web Workbench 视觉与交互收敛——资源面板说明文案瘦身、统一确认模态
6
- > 取代最后的 `window.confirm`、刷新/保存的进行中反馈,以及 Files / Git / Review 的
7
- > Orion 风格视觉收敛。Candidate 源码不代表已创建 npm 发布、Git tag 或达到可合并状态。
5
+ > v0.3.19:补齐 v0.3.17 Git 工作栏的断言欠账
6
+ > (`docs/plan/evidence/v0.3.17-git/g317-status.md`),并修掉这些断言找出来的问题——
7
+ > 响应式三档从未生效、被后台刷新丢弃的 Diff 读取导致内容区空白、窄屏返回丢失阅读位置、
8
+ > 裸仓库被当成「不是仓库」。已发布 npm 0.3.19 并打 tag `v0.3.19`。
8
9
 
9
10
  [English](README.md) ·
10
11
  [v0.3.7 方案](docs/plan/v0.3.7-left-rail-improvement-plan.md) ·
@@ -101,7 +102,7 @@ Current。Node 20 已结束上游维护,不再属于 v0.3 Runtime 合同。
101
102
  当 npm 已存在不可变的 `0.3.4` 发布凭据后:
102
103
 
103
104
  ```bash
104
- npm install -g @orion-agents/orion-code@0.3.15
105
+ npm install -g @orion-agents/orion-code@0.3.19
105
106
  orion --version
106
107
  orion doctor
107
108
  ```
@@ -21,6 +21,15 @@ export declare const WEB_E2E_WEB38_SCENARIOS_V1: readonly ["WEB38-P0-01", "WEB38
21
21
  * actually verify; the per-stage unit suites are registered separately.
22
22
  */
23
23
  export declare const WEB_E2E_WEB317_SCENARIOS_V1: readonly string[];
24
+ /**
25
+ * v0.3.19 — closing the v0.3.17 G317 assertion backlog.
26
+ *
27
+ * Each id is one of the five scenarios that were left as "feature present, assertion
28
+ * missing": an external (agent) write must not move the reader, `在 Files 打开` must
29
+ * actually reach the Files panel, the theme and narrow tiers must be verified rather than
30
+ * assumed, and repeated open/close plus a hundred diffs must stay bounded.
31
+ */
32
+ export declare const WEB_E2E_WEB34_SCENARIOS_V1: readonly string[];
24
33
  export declare const WEB_E2E_FULL_SCENARIOS_V1: readonly string[];
25
34
  export declare const WEB_E2E_SETTINGS_SCENARIOS_V1: readonly string[];
26
35
  export declare const WEB_E2E_WEB31_CRITICAL_SCENARIOS_V1: readonly ["WEB31-P0-01", "WEB31-P0-02", "WEB31-P0-08", "WEB31-P0-09", "WEB31-P0-10", "WEB31-P0-12"];
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.WEB_E2E_CRITICAL_SCENARIOS_V1 = exports.WEB_E2E_WEB33_CRITICAL_SCENARIOS_V1 = exports.WEB_E2E_WEB32_CRITICAL_SCENARIOS_V1 = exports.WEB_E2E_WEB31_CRITICAL_SCENARIOS_V1 = exports.WEB_E2E_SETTINGS_SCENARIOS_V1 = exports.WEB_E2E_FULL_SCENARIOS_V1 = exports.WEB_E2E_WEB317_SCENARIOS_V1 = exports.WEB_E2E_WEB38_SCENARIOS_V1 = exports.WEB_E2E_WEB33_SCENARIOS_V1 = exports.WEB_E2E_WEB33_SESSION_SCENARIOS_V1 = exports.WEB_E2E_WEB33_THEME_SCENARIOS_V1 = exports.WEB_E2E_WEB32_SCENARIOS_V1 = exports.WEB_E2E_WEB31_SCENARIOS_V1 = exports.WEB_E2E_LEGACY_SCENARIOS_V1 = exports.SUPPORTED_RELEASE_VERSION_LINE_V1 = exports.SUPPORTED_RELEASE_NODE_FLOORS_V1 = exports.SUPPORTED_RELEASE_NODE_MAJORS_V1 = exports.RELEASE_RECEIPT_VERSION_V1 = void 0;
3
+ exports.WEB_E2E_CRITICAL_SCENARIOS_V1 = exports.WEB_E2E_WEB33_CRITICAL_SCENARIOS_V1 = exports.WEB_E2E_WEB32_CRITICAL_SCENARIOS_V1 = exports.WEB_E2E_WEB31_CRITICAL_SCENARIOS_V1 = exports.WEB_E2E_SETTINGS_SCENARIOS_V1 = exports.WEB_E2E_FULL_SCENARIOS_V1 = exports.WEB_E2E_WEB34_SCENARIOS_V1 = exports.WEB_E2E_WEB317_SCENARIOS_V1 = exports.WEB_E2E_WEB38_SCENARIOS_V1 = exports.WEB_E2E_WEB33_SCENARIOS_V1 = exports.WEB_E2E_WEB33_SESSION_SCENARIOS_V1 = exports.WEB_E2E_WEB33_THEME_SCENARIOS_V1 = exports.WEB_E2E_WEB32_SCENARIOS_V1 = exports.WEB_E2E_WEB31_SCENARIOS_V1 = exports.WEB_E2E_LEGACY_SCENARIOS_V1 = exports.SUPPORTED_RELEASE_VERSION_LINE_V1 = exports.SUPPORTED_RELEASE_NODE_FLOORS_V1 = exports.SUPPORTED_RELEASE_NODE_MAJORS_V1 = exports.RELEASE_RECEIPT_VERSION_V1 = void 0;
4
4
  exports.createTarballArtifactReceiptV1 = createTarballArtifactReceiptV1;
5
5
  exports.verifyTarballArtifactReceiptV1 = verifyTarballArtifactReceiptV1;
6
6
  exports.createRuntimeMatrixReceiptV1 = createRuntimeMatrixReceiptV1;
@@ -125,6 +125,21 @@ exports.WEB_E2E_WEB317_SCENARIOS_V1 = Object.freeze([
125
125
  'WEB39-P0-04',
126
126
  'WEB39-P0-05',
127
127
  ]);
128
+ /**
129
+ * v0.3.19 — closing the v0.3.17 G317 assertion backlog.
130
+ *
131
+ * Each id is one of the five scenarios that were left as "feature present, assertion
132
+ * missing": an external (agent) write must not move the reader, `在 Files 打开` must
133
+ * actually reach the Files panel, the theme and narrow tiers must be verified rather than
134
+ * assumed, and repeated open/close plus a hundred diffs must stay bounded.
135
+ */
136
+ exports.WEB_E2E_WEB34_SCENARIOS_V1 = Object.freeze([
137
+ 'WEB34-P0-01',
138
+ 'WEB34-P0-02',
139
+ 'WEB34-P0-03',
140
+ 'WEB34-P0-04',
141
+ 'WEB34-P0-05',
142
+ ]);
128
143
  exports.WEB_E2E_FULL_SCENARIOS_V1 = Object.freeze([
129
144
  ...exports.WEB_E2E_LEGACY_SCENARIOS_V1,
130
145
  ...exports.WEB_E2E_WEB31_SCENARIOS_V1,
@@ -132,6 +147,7 @@ exports.WEB_E2E_FULL_SCENARIOS_V1 = Object.freeze([
132
147
  ...exports.WEB_E2E_WEB33_SCENARIOS_V1,
133
148
  ...exports.WEB_E2E_WEB38_SCENARIOS_V1,
134
149
  ...exports.WEB_E2E_WEB317_SCENARIOS_V1,
150
+ ...exports.WEB_E2E_WEB34_SCENARIOS_V1,
135
151
  ]);
136
152
  exports.WEB_E2E_SETTINGS_SCENARIOS_V1 = Object.freeze(exports.WEB_E2E_FULL_SCENARIOS_V1.filter(id => id.startsWith('SET-P0-')));
137
153
  exports.WEB_E2E_WEB31_CRITICAL_SCENARIOS_V1 = Object.freeze([
@@ -16,6 +16,6 @@ exports.GIT_FILE_SOURCES_V1 = Object.freeze([
16
16
  'commit',
17
17
  ]);
18
18
  function isGitFileSourceV1(value) {
19
- return (typeof value === 'string' && exports.GIT_FILE_SOURCES_V1.includes(value));
19
+ return typeof value === 'string' && exports.GIT_FILE_SOURCES_V1.includes(value);
20
20
  }
21
21
  //# sourceMappingURL=git-file-source.js.map
@@ -191,9 +191,7 @@ function createGitHistoryReader(options) {
191
191
  const chosen = parents[Math.min(parentIndex, parents.length - 1)];
192
192
  return {
193
193
  baseOid: chosen,
194
- baseLabel: parents.length > 1
195
- ? `父提交 ${parentIndex + 1}/${parents.length}`
196
- : '父提交',
194
+ baseLabel: parents.length > 1 ? `父提交 ${parentIndex + 1}/${parents.length}` : '父提交',
197
195
  };
198
196
  };
199
197
  return {
@@ -276,7 +274,12 @@ function createGitHistoryReader(options) {
276
274
  // The body is a separate call: a commit message may contain any byte, including the
277
275
  // separators the list format relies on.
278
276
  const message = await runGit(['log', '-1', '--format=%B', summary.id]);
279
- const committerRaw = (await runGit(['log', '-1', `--format=%cn${FIELD_SEPARATOR}%ce${FIELD_SEPARATOR}%cI`, summary.id])).trim();
277
+ const committerRaw = (await runGit([
278
+ 'log',
279
+ '-1',
280
+ `--format=%cn${FIELD_SEPARATOR}%ce${FIELD_SEPARATOR}%cI`,
281
+ summary.id,
282
+ ])).trim();
280
283
  const [committerName, authorEmail, committedAt] = committerRaw.split(FIELD_SEPARATOR);
281
284
  const base = await resolveBase(summary.id, parentIndex);
282
285
  return Object.freeze({
@@ -475,17 +478,16 @@ function createGitHistoryReader(options) {
475
478
  .toString()
476
479
  .trim();
477
480
  if (!tracked) {
478
- return Object.freeze({ path, rev: rev ?? null, lines: Object.freeze([]), truncated: false });
481
+ return Object.freeze({
482
+ path,
483
+ rev: rev ?? null,
484
+ lines: Object.freeze([]),
485
+ truncated: false,
486
+ });
479
487
  }
480
488
  // `--porcelain` is machine-readable and stable; the date stays ISO so the client never
481
489
  // re-implements time. Uncommitted lines show as boundary/not-committed metadata.
482
- const raw = await runGit([
483
- 'blame',
484
- '--porcelain',
485
- ...(target ? [target] : []),
486
- '--',
487
- path,
488
- ]);
490
+ const raw = await runGit(['blame', '--porcelain', ...(target ? [target] : []), '--', path]);
489
491
  const commits = new Map();
490
492
  const lines = [];
491
493
  let currentOid = null;
@@ -541,7 +543,11 @@ function createGitHistoryReader(options) {
541
543
  else if (key === 'boundary')
542
544
  boundary = true;
543
545
  if (pendingAuthor && pendingTime && pendingSummary && !commits.has(currentOid)) {
544
- commits.set(currentOid, Object.freeze({ author: pendingAuthor, authoredAt: pendingTime, summary: pendingSummary }));
546
+ commits.set(currentOid, Object.freeze({
547
+ author: pendingAuthor,
548
+ authoredAt: pendingTime,
549
+ summary: pendingSummary,
550
+ }));
545
551
  }
546
552
  }
547
553
  return Object.freeze({
@@ -28,8 +28,32 @@ export interface WebGitStatusCountsV1 {
28
28
  readonly untracked: number;
29
29
  readonly loaded: number;
30
30
  }
31
+ /**
32
+ * v0.3.19 (G317-22) — what this workspace actually is, stated instead of inferred.
33
+ *
34
+ * `git rev-parse --show-toplevel` fails for a bare repository, and that used to be reported
35
+ * as "not a repository at all". Those are two different facts and the reader is owed the
36
+ * right one:
37
+ *
38
+ * - `worktree` — an ordinary repository with a working tree; the panel reads changes.
39
+ * - `bare` — a repository with **no** working tree. It is a repository, its history
40
+ * exists, but this panel has no changes and no diff to read, and it must
41
+ * never invent any.
42
+ * - `absent` — no repository here at all.
43
+ *
44
+ * `isRepository` stays the coarse question ("is this a repository?"). Anything that needs
45
+ * a working tree must ask `hasWorktree`.
46
+ */
47
+ export type GitRepositoryKindV1 = 'worktree' | 'bare' | 'absent';
31
48
  export interface WebGitStatusV1 {
32
49
  readonly isRepository: boolean;
50
+ /** v0.3.19 (G317-22) — the precise answer to "what is this workspace". */
51
+ readonly repositoryKind: GitRepositoryKindV1;
52
+ /**
53
+ * v0.3.19 (G317-22) — true only for `repositoryKind: 'worktree'`. Every worktree-backed
54
+ * surface (change lists, diffs, staging, commit) is meaningless when this is false.
55
+ */
56
+ readonly hasWorktree: boolean;
33
57
  readonly repositoryRevision: string;
34
58
  readonly rootLabel?: string;
35
59
  readonly branch: string | null;
@@ -84,10 +84,15 @@ class GitReadModelServiceV1 {
84
84
  const nextOffset = offset + page.length;
85
85
  return Object.freeze({
86
86
  isRepository: snapshot.isRepository,
87
+ repositoryKind: snapshot.repositoryKind,
88
+ hasWorktree: snapshot.repositoryKind === 'worktree',
87
89
  repositoryRevision: snapshot.revision,
88
90
  ...(snapshot.root ? { rootLabel: (0, path_1.basename)(snapshot.root) } : {}),
89
91
  branch: snapshot.branch,
90
- detached: snapshot.isRepository && !snapshot.branch,
92
+ // v0.3.19 (G317-22) — only a worktree-backed checkout can be "detached". A bare
93
+ // repository has no branch either, and reporting that as a detached HEAD invented a
94
+ // state the repository is not in.
95
+ detached: snapshot.repositoryKind === 'worktree' && !snapshot.branch,
91
96
  head: snapshot.head ? snapshot.head.slice(0, 12) : null,
92
97
  upstream: snapshot.upstream,
93
98
  ahead: snapshot.ahead,
@@ -454,6 +459,7 @@ class GitReadModelServiceV1 {
454
459
  }
455
460
  async capture() {
456
461
  let root;
462
+ let repositoryKind = 'absent';
457
463
  try {
458
464
  root =
459
465
  this.repositoryRoot ??
@@ -461,11 +467,53 @@ class GitReadModelServiceV1 {
461
467
  if (!isWithinRoot(root, this.workspace))
462
468
  throw new Error('repository root escaped workspace');
463
469
  this.repositoryRoot = root;
470
+ repositoryKind = 'worktree';
464
471
  }
465
472
  catch {
473
+ // v0.3.19 (G317-22) — `--show-toplevel` fails for a bare repository too, so the failure
474
+ // alone cannot say which case this is. Ask directly. Conflating "no repository" with
475
+ // "a repository that has no working tree" told the reader the wrong thing about a
476
+ // directory that is in fact a valid repository.
477
+ const bare = (await this.tryGit(['rev-parse', '--is-bare-repository'], this.workspace))
478
+ ?.trim()
479
+ .toLowerCase();
480
+ repositoryKind = bare === 'true' ? 'bare' : 'absent';
481
+ }
482
+ if (repositoryKind === 'bare') {
483
+ // A bare repository has no index and no working tree, so there is nothing to report as
484
+ // changed — but its HEAD is real and worth stating. `clean` stays true in `status()`
485
+ // because zero files differ from an index that does not exist; `hasWorktree` is what
486
+ // tells the reader not to trust that as "you have nothing to commit".
487
+ const [branchResult, headResult] = await Promise.all([
488
+ this.tryGit(['symbolic-ref', '--quiet', '--short', 'HEAD'], this.workspace),
489
+ this.tryGit(['rev-parse', 'HEAD'], this.workspace),
490
+ ]);
491
+ const branch = branchResult?.trim() || null;
492
+ const head = headResult?.trim() || null;
493
+ const revision = (0, crypto_1.createHash)('sha256')
494
+ .update(JSON.stringify({ bare: this.workspace, branch, head }))
495
+ .digest('hex');
496
+ return Object.freeze({
497
+ isRepository: true,
498
+ repositoryKind: 'bare',
499
+ branch,
500
+ head,
501
+ upstream: null,
502
+ ahead: 0,
503
+ behind: 0,
504
+ records: Object.freeze([]),
505
+ rawStatus: '',
506
+ stagedRecords: 0,
507
+ conflictedRecords: 0,
508
+ indexDigest: (0, crypto_1.createHash)('sha256').update('').digest('hex'),
509
+ revision,
510
+ });
511
+ }
512
+ if (repositoryKind === 'absent') {
466
513
  const revision = (0, crypto_1.createHash)('sha256').update(`not-git:${this.workspace}`).digest('hex');
467
514
  return Object.freeze({
468
515
  isRepository: false,
516
+ repositoryKind: 'absent',
469
517
  branch: null,
470
518
  head: null,
471
519
  upstream: null,
@@ -479,6 +527,11 @@ class GitReadModelServiceV1 {
479
527
  revision,
480
528
  });
481
529
  }
530
+ if (!root) {
531
+ // Only the `worktree` branch assigns a root and both other kinds returned above, so this
532
+ // is unreachable today — it exists so the invariant is enforced, not merely inferred.
533
+ throw new Error('Resolved a non-worktree repository without a root.');
534
+ }
482
535
  const [rawStatus, branchResult, headResult, upstreamResult, indexListing] = await Promise.all([
483
536
  this.runGit(['status', '--porcelain=v1', '-z', '--untracked-files=all'], root),
484
537
  this.tryGit(['symbolic-ref', '--quiet', '--short', 'HEAD'], root),
@@ -532,6 +585,7 @@ class GitReadModelServiceV1 {
532
585
  .digest('hex');
533
586
  return Object.freeze({
534
587
  isRepository: true,
588
+ repositoryKind: 'worktree',
535
589
  root,
536
590
  branch,
537
591
  head,
@@ -941,6 +995,13 @@ class GitReadModelServiceV1 {
941
995
  }
942
996
  async requireRepositoryRoot() {
943
997
  const snapshot = await this.capture();
998
+ if (snapshot.repositoryKind === 'bare') {
999
+ // v0.3.19 (G317-22) — a bare repository *is* a repository, so "unavailable" was the
1000
+ // wrong word for it. What it lacks is a working tree, and this panel is worktree-based:
1001
+ // the limit is stated rather than worked around, and reading from a different working
1002
+ // tree would widen access in a way the workspace boundary does not allow.
1003
+ throw new Error('This workspace is a bare repository. The Git panel reads a working tree, so it has no changes or history to show here.');
1004
+ }
944
1005
  if (!snapshot.isRepository || !snapshot.root) {
945
1006
  throw new Error('Git repository is unavailable.');
946
1007
  }
@@ -1196,7 +1257,16 @@ function diffCommands(path, source, ignoreWhitespace = false, wordDiff = false)
1196
1257
  return Object.freeze([
1197
1258
  {
1198
1259
  title: 'Staged',
1199
- args: ['diff', '--cached', ...modes, '--no-ext-diff', '--no-textconv', '--unified=3', '--', path],
1260
+ args: [
1261
+ 'diff',
1262
+ '--cached',
1263
+ ...modes,
1264
+ '--no-ext-diff',
1265
+ '--no-textconv',
1266
+ '--unified=3',
1267
+ '--',
1268
+ path,
1269
+ ],
1200
1270
  },
1201
1271
  ]);
1202
1272
  }
@@ -8,7 +8,7 @@ import type { ToolDetailPage, ToolDetailSummary } from '../runtime/tool-detail-r
8
8
  import type { SessionMeta } from '../services/session-storage';
9
9
  import type { ToolConfirmationPolicy } from '../services/global-config';
10
10
  import type { WebFileContentPageV1, WebFileNodeV1, WebFileTreePageV1 } from './file-read-service';
11
- import type { GitCommitIdentityV1, GitCommitOutcomeV1, GitCommitPreviewV1, GitIndexEntryV1, GitMutationResultV1, GitWorktreeSourceV1, WebGitCommitV1, WebGitDiffPageV1, WebGitFileSourceV1, WebGitFileV1, WebGitLogPageV1, WebGitStatusCountsV1, WebGitStatusV1 } from './git-read-model-service';
11
+ import type { GitCommitIdentityV1, GitCommitOutcomeV1, GitCommitPreviewV1, GitIndexEntryV1, GitMutationResultV1, GitRepositoryKindV1, GitWorktreeSourceV1, WebGitCommitV1, WebGitDiffPageV1, WebGitFileSourceV1, WebGitFileV1, WebGitLogPageV1, WebGitStatusCountsV1, WebGitStatusV1 } from './git-read-model-service';
12
12
  import type { GitDiffCapabilitiesV2, GitDiffDocumentV2, GitDiffDocumentKindV2, GitDiffLineKindV2, GitDiffLineV2, GitHunkV2 } from './git-diff-document';
13
13
  import type { WebReviewSnapshotV1 } from './review-service';
14
14
  import type { WebSessionRuntimeSummaryV1 } from './session-runtime-registry';
@@ -423,9 +423,9 @@ export type WebWorkspaceInvalidationReasonV1 = 'context-change' | 'filesystem-ch
423
423
  export type WebToolDetailSummaryV1 = ToolDetailSummary;
424
424
  export type WebToolDetailPageV1 = ToolDetailPage;
425
425
  export type { GitBlameLineV1, GitGraphPageV1, GitGraphRowV1, GitBlameResultV1, GitRefKindV1, GitRefV1, GitRefsV1, GitCommitSummaryV1, GitCommitDetailV1, GitCommitFileV1, GitCommitFilesV1, GitHistoryQueryV1, GitHistoryPageV1, GitFileHistoryPageV1, } from './git-history-service';
426
- export type { GitCompareFileV1, GitCompareModeV1, GitCompareResultV1, } from './git-compare-service';
426
+ export type { GitCompareFileV1, GitCompareModeV1, GitCompareResultV1 } from './git-compare-service';
427
427
  export type { GitBlobResultV1, GitConflictShapeV1, GitConflictVersionsV1, GitFileVersionV1, GitSubmoduleEntryV1, GitVersionLabelV1, } from './git-version-service';
428
- export type { WebFileContentPageV1, WebFileNodeV1, WebFileTreePageV1, GitCommitIdentityV1, GitCommitOutcomeV1, GitCommitPreviewV1, GitIndexEntryV1, GitMutationResultV1, GitWorktreeSourceV1, GitDiffCapabilitiesV2, GitDiffDocumentV2, GitDiffDocumentKindV2, GitDiffLineKindV2, GitDiffLineV2, GitHunkV2, WebGitCommitV1, WebGitDiffPageV1, WebGitFileSourceV1, WebGitFileV1, WebGitLogPageV1, WebGitStatusCountsV1, WebGitStatusV1, WebReviewSnapshotV1, WebTerminalCreateResultV1, WebTerminalExitV1, WebTerminalGapV1, WebTerminalMetadataV1, WebTerminalOutputFrameV1, WebTerminalStateV1, };
428
+ export type { WebFileContentPageV1, WebFileNodeV1, WebFileTreePageV1, GitCommitIdentityV1, GitCommitOutcomeV1, GitCommitPreviewV1, GitIndexEntryV1, GitMutationResultV1, GitWorktreeSourceV1, GitDiffCapabilitiesV2, GitDiffDocumentV2, GitDiffDocumentKindV2, GitDiffLineKindV2, GitDiffLineV2, GitHunkV2, GitRepositoryKindV1, WebGitCommitV1, WebGitDiffPageV1, WebGitFileSourceV1, WebGitFileV1, WebGitLogPageV1, WebGitStatusCountsV1, WebGitStatusV1, WebReviewSnapshotV1, WebTerminalCreateResultV1, WebTerminalExitV1, WebTerminalGapV1, WebTerminalMetadataV1, WebTerminalOutputFrameV1, WebTerminalStateV1, };
429
429
  export interface WebBootstrapV1 {
430
430
  readonly apiVersion: 1;
431
431
  readonly productVersion: string;
@@ -607,7 +607,16 @@ async function handleRequest(context) {
607
607
  if (method === 'GET' && path === '/git/history') {
608
608
  const contextGuard = requireContextGuardQuery(url);
609
609
  sendJson(response, 200, await context.workbench.gitHistory(contextGuard, {
610
- ...optionalQueryParams(url, ['message', 'author', 'sha', 'path', 'since', 'until', 'rev', 'cursor']),
610
+ ...optionalQueryParams(url, [
611
+ 'message',
612
+ 'author',
613
+ 'sha',
614
+ 'path',
615
+ 'since',
616
+ 'until',
617
+ 'rev',
618
+ 'cursor',
619
+ ]),
611
620
  pageSize: boundedInteger(url.searchParams.get('pageSize'), 30, 1, 100),
612
621
  }));
613
622
  return;
@@ -1 +1 @@
1
- import{r as a,j as e,I as D,s as ye,C as be,W as ge}from"./index-Drja0_BT.js";import{u as Ce,R as Ne}from"./useAutoNotice-DfQi4G1X.js";function Le({workspaceId:s,refreshEpoch:o,actions:l,navigatorWidthPx:r,onNavigatorWidthCommit:d,revealRequest:c}){const[f,m]=a.useState({}),[h,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(""),[xe,W]=a.useState(!1),x=a.useRef(0),C=a.useRef(0),ee=a.useRef(null),E=async(t,n=!1,b=x.current)=>{if(b!==x.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(b!==x.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(b!==x.current)return;if(n&&U(u)){$("目录已变化,已重新载入第一页。"),E(t,!1,b);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=x.current)=>{try{const n=await l.gitStatus();if(t!==x.current)return;_(Se(n)),A(n.nextCursor?"Git 装饰仅显示首批 200 个变更;在 Git 面板继续分页查看。":"")}catch(n){if(t!==x.current)return;_({}),A(`Git 状态装饰不可用:${z(n)}`)}},te=a.useRef(s);a.useEffect(()=>{const t=x.current+1;x.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&&y(n,!1,!1,t))},[o,s]);const se=a.useRef(null),y=async(t,n=!1,b=!1,p=x.current,g=C.current+1)=>{if(p===x.current&&!(!t.readable||t.sensitive||X(t))){C.current=g,N(t),K(""),n||(B(""),G(null),P(!1),R("view"),w(""),S(!1)),b||V();try{const u=await l.readFileContent(t.id,n?M??void 0:void 0);if(p!==x.current||g!==C.current)return;P(u.binary),B(v=>n?`${v}${u.content??""}`:u.content??""),G(u.nextCursor),me(u.revision),b&&$("文件已变化,已从第一页重新载入。")}catch(u){if(p!==x.current||g!==C.current)return;if(n&&U(u)){await y(t,!1,!0,p,g);return}K(z(u))}}},re=De({selected:i,binary:Y,hasMorePages:M!==null}),he=()=>{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 y(i,!1,!0,x.current,C.current+1),$("已保存文件。","success"),Q()}catch(t){U(t)?S(!0):$(z(t),"error")}finally{q(!1)}}},ae=t=>{const n=new Set(h);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,y({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=>{y(n)}},t.id))})}):e.jsx(le,{parentId:"workspace-root",depth:0,directories:f,expanded:h,selectedId:i?.id,gitDecorations:Z,onToggle:ae,onSelect:t=>{y(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:he,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:()=>{y(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:ye(F)})]}),M&&O==="view"?e.jsx("button",{type:"button",className:"secondary-button",onClick:()=>{y(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(be,{open:xe,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:h}){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:h}):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:()=>h(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,h=X(s);return e.jsxs("button",{type:"button",className:`file-node ${r?"selected":""} ${m?"blocked":""}`,style:{paddingInlineStart:`${8+o*15}px`},"aria-expanded":h?l:void 0,"aria-label":`${we(s)} ${s.name}${m?",不可读取":""}${d.length?`,Git ${d.join("、")}`:""}`,onClick:()=>h?c(s.id):f(s),disabled:m,children:[e.jsx(D,{name:h?"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.isRepository)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 h=1;h<m.length;h+=1){const j=m.slice(0,h).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-DYeu_rs7.js";import{u as Ce,R as Ne}from"./useAutoNotice-h7c0Iz1E.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};