@agimon-ai/po-git 0.0.0-alpha.34 → 0.0.0-alpha.35

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.
@@ -1,12 +1,399 @@
1
1
  import { createApiClient, defineActivityGroup, defineChannelFile, defineFill, defineGlobalStore, defineNativePlugin, defineSessionStore, defineWebLifecycle, nativeApiTransport, sessionApiAddress } from "@agimon-ai/po-core/native";
2
- import { AlertIcon, Badge, BranchIcon, Button, ChevronRightIcon, Dot, FileIcon, Input, MessageIcon, Sheet, SheetBody, SheetContent, SheetHeader, SheetTitle, Spinner, SyntaxLine, Textarea, fontOf } from "@agimon-ai/po-native-components";
3
- import { createElement, useCallback, useEffect, useRef, useState, useSyncExternalStore } from "react";
4
2
  import { View } from "react-native-unistyles/components/native/View";
5
3
  import { Text } from "react-native-unistyles/components/native/Text";
6
- import { ScrollView } from "react-native-unistyles/components/native/ScrollView";
7
- import { Pressable } from "react-native-unistyles/components/native/Pressable";
4
+ import { AlertIcon, Badge, BranchIcon, Button, ChevronRightIcon, Dot, FileIcon, Input, Label, ListRow, MessageIcon, SegmentedControl, Sheet, SheetBody, SheetContent, SheetHeader, SheetTitle, Spinner, SyntaxLine, Textarea, fontOf } from "@agimon-ai/po-native-components";
5
+ import { createElement, memo, useCallback, useEffect, useMemo, useRef, useState, useSyncExternalStore } from "react";
8
6
  import { StyleSheet, useUnistyles } from "react-native-unistyles";
9
7
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
8
+ import { ScrollView } from "react-native-unistyles/components/native/ScrollView";
9
+ import { Pressable } from "react-native-unistyles/components/native/Pressable";
10
+ const api = createApiClient({
11
+ /** Workspace: the new-session dialog's branch list. */
12
+ branches: {
13
+ method: "GET",
14
+ path: "/branches",
15
+ response: void 0
16
+ },
17
+ /** Workspace: a top-level worktree session on an existing or new branch. */
18
+ createSession: {
19
+ method: "POST",
20
+ path: "/sessions",
21
+ response: void 0
22
+ },
23
+ /** Workspace: that workspace's saved remote auth, never the token itself. */
24
+ auth: {
25
+ method: "GET",
26
+ path: "/auth",
27
+ query: ["repositoryId"],
28
+ response: void 0
29
+ },
30
+ /** Workspace: replaces that workspace's remote auth. Passkey step-up gated for remote devices. */
31
+ saveAuth: {
32
+ method: "PUT",
33
+ path: "/auth",
34
+ query: ["repositoryId"],
35
+ response: void 0
36
+ },
37
+ /** Session: the files this session's checkout changed against its base. */
38
+ review: {
39
+ method: "GET",
40
+ path: "/review",
41
+ response: void 0
42
+ },
43
+ /** Session: one changed file's hunks. */
44
+ reviewFile: {
45
+ method: "GET",
46
+ path: "/review/file",
47
+ query: ["path"],
48
+ response: void 0
49
+ }
50
+ }, {
51
+ scopes: [
52
+ "global",
53
+ "workspace",
54
+ "session"
55
+ ],
56
+ basePath: "git",
57
+ transport: nativeApiTransport,
58
+ sessionAddress: sessionApiAddress
59
+ });
60
+ //#endregion
61
+ //#region src/extensions/workspaces/(frontend)/_lib/newSessionForm.ts
62
+ const MAX_LISTED = 200;
63
+ /** Local branches first, then remote-only ones, filtered by a case-insensitive substring. */
64
+ function branchOptions(branches, query) {
65
+ const needle = query.trim().toLowerCase();
66
+ const matches = (text) => needle === "" || text.toLowerCase().includes(needle);
67
+ const local = branches.local.filter((branch) => matches(branch.name)).map((branch) => ({
68
+ key: `local:${branch.name}`,
69
+ name: branch.name,
70
+ current: branch.name === branches.current,
71
+ ...branch.checkedOutAt === void 0 ? {} : { busyAt: branch.checkedOutAt }
72
+ }));
73
+ const remote = branches.remote.filter((branch) => matches(`${branch.remote}/${branch.name}`)).map((branch) => ({
74
+ key: `remote:${branch.remote}/${branch.name}`,
75
+ name: branch.name,
76
+ remote: branch.remote,
77
+ current: false
78
+ }));
79
+ return [...local, ...remote].slice(0, MAX_LISTED);
80
+ }
81
+ /** Why the form cannot be submitted yet, or undefined when it can. */
82
+ function formProblem(state) {
83
+ if (state.mode === "existing") {
84
+ if (state.selected === void 0) return "Pick a branch.";
85
+ if (state.selected.busyAt !== void 0) return `That branch is checked out at ${state.selected.busyAt}.`;
86
+ }
87
+ if (state.mode === "new" && state.newBranch.trim() === "") return "Name the new branch.";
88
+ }
89
+ /** The worktree create request for the form, or undefined in plain mode. */
90
+ function createRequest(state) {
91
+ const name = state.name.trim();
92
+ const named = name === "" ? {} : { name };
93
+ if (state.mode === "existing" && state.selected !== void 0) return {
94
+ mode: "existing-branch",
95
+ branch: state.selected.name,
96
+ ...state.selected.remote === void 0 ? {} : { remote: state.selected.remote },
97
+ ...named
98
+ };
99
+ if (state.mode === "new") {
100
+ const baseRef = state.baseRef.trim();
101
+ return {
102
+ mode: "new-branch",
103
+ branch: state.newBranch.trim(),
104
+ ...baseRef === "" ? {} : { baseRef },
105
+ ...named
106
+ };
107
+ }
108
+ }
109
+ //#endregion
110
+ //#region src/extensions/workspaces/(frontend)/fill/_components/GitNewSessionSheet.native.tsx
111
+ const MODES = [
112
+ {
113
+ value: "existing",
114
+ label: "Existing"
115
+ },
116
+ {
117
+ value: "new",
118
+ label: "New branch"
119
+ },
120
+ {
121
+ value: "plain",
122
+ label: "No branch"
123
+ }
124
+ ];
125
+ /** The native twin of GitNewSessionDialogView: the same form state and checks, in sheet parts. */
126
+ function GitNewSessionSheetView({ workspaceRoot, branches, loadError = "", initialForm, busy = false, error = "", onCreate }) {
127
+ const [form, setForm] = useState({
128
+ mode: "existing",
129
+ newBranch: "",
130
+ baseRef: "",
131
+ name: "",
132
+ ...initialForm
133
+ });
134
+ const [query, setQuery] = useState("");
135
+ const ready = branches?.repository === true && loadError === "";
136
+ const loading = branches === null && loadError === "";
137
+ const mode = ready || loading ? form.mode : "plain";
138
+ const state = {
139
+ ...form,
140
+ mode,
141
+ baseRef: form.baseRef || branches?.defaultBase || ""
142
+ };
143
+ const problem = formProblem(state);
144
+ const update = (patch) => setForm((current) => ({
145
+ ...current,
146
+ ...patch
147
+ }));
148
+ const options = branches === null ? [] : branchOptions(branches, query);
149
+ return /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(SheetHeader, { children: /* @__PURE__ */ jsx(SheetTitle, { children: "New session" }) }), /* @__PURE__ */ jsxs(SheetBody, {
150
+ style: styles$3.body,
151
+ children: [
152
+ /* @__PURE__ */ jsx(Text, {
153
+ style: styles$3.root,
154
+ numberOfLines: 1,
155
+ ellipsizeMode: "middle",
156
+ children: workspaceRoot
157
+ }),
158
+ !ready ? /* @__PURE__ */ jsx(Text, {
159
+ testID: "git-new-session-plain-note",
160
+ style: styles$3.dim,
161
+ children: loading ? "Reading branches..." : loadError || "This workspace is not a git checkout. Start a plain session in this folder."
162
+ }) : /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(SegmentedControl, {
163
+ accessibilityLabel: "Session kind",
164
+ options: MODES,
165
+ value: mode,
166
+ disabled: busy,
167
+ onValueChange: (value) => update({ mode: value })
168
+ }), mode === "existing" ? /* @__PURE__ */ jsxs(View, {
169
+ style: styles$3.field,
170
+ children: [
171
+ /* @__PURE__ */ jsx(Label, { children: "Branch" }),
172
+ /* @__PURE__ */ jsx(Input, {
173
+ testID: "git-new-session-branch-search",
174
+ accessibilityLabel: "Find a branch",
175
+ placeholder: "Find a branch",
176
+ autoCapitalize: "none",
177
+ autoCorrect: false,
178
+ value: query,
179
+ onChangeText: setQuery,
180
+ disabled: busy
181
+ }),
182
+ /* @__PURE__ */ jsxs(View, {
183
+ style: styles$3.list,
184
+ accessibilityLabel: "Branches",
185
+ children: [options.length === 0 ? /* @__PURE__ */ jsx(Text, {
186
+ style: [styles$3.dim, styles$3.pad],
187
+ children: "No matching branches"
188
+ }) : null, options.map((branch, index) => /* @__PURE__ */ jsxs(View, { children: [branch.remote !== void 0 && (index === 0 || options[index - 1]?.remote === void 0) ? /* @__PURE__ */ jsx(Text, {
189
+ style: [
190
+ styles$3.dim,
191
+ styles$3.pad,
192
+ styles$3.divider
193
+ ],
194
+ children: "Remote branches"
195
+ }) : null, /* @__PURE__ */ jsx(ListRow, {
196
+ title: /* @__PURE__ */ jsx(Text, {
197
+ style: styles$3.mono,
198
+ children: branch.remote === void 0 ? branch.name : `${branch.remote}/${branch.name}`
199
+ }),
200
+ description: branch.busyAt !== void 0 ? `Checked out in ${branch.busyAt}` : branch.current ? "Current" : void 0,
201
+ selected: form.selected?.key === branch.key,
202
+ disabled: branch.busyAt !== void 0 || busy,
203
+ disclosure: false,
204
+ onPress: () => update({ selected: branch })
205
+ })] }, branch.key))]
206
+ })
207
+ ]
208
+ }) : mode === "new" ? /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsxs(View, {
209
+ style: styles$3.field,
210
+ children: [/* @__PURE__ */ jsx(Label, { children: "Branch name" }), /* @__PURE__ */ jsx(Input, {
211
+ testID: "git-new-session-new-branch",
212
+ accessibilityLabel: "Branch name",
213
+ placeholder: "feature/my-change",
214
+ autoCapitalize: "none",
215
+ autoCorrect: false,
216
+ style: styles$3.mono,
217
+ value: form.newBranch,
218
+ onChangeText: (newBranch) => update({ newBranch }),
219
+ disabled: busy
220
+ })]
221
+ }), /* @__PURE__ */ jsxs(View, {
222
+ style: styles$3.field,
223
+ children: [/* @__PURE__ */ jsx(Label, { children: "From" }), /* @__PURE__ */ jsx(Input, {
224
+ testID: "git-new-session-base",
225
+ accessibilityLabel: "From",
226
+ placeholder: branches?.defaultBase ?? "Current branch",
227
+ autoCapitalize: "none",
228
+ autoCorrect: false,
229
+ style: styles$3.mono,
230
+ value: form.baseRef,
231
+ onChangeText: (baseRef) => update({ baseRef }),
232
+ disabled: busy
233
+ })]
234
+ })] }) : /* @__PURE__ */ jsx(Text, {
235
+ style: styles$3.dim,
236
+ children: "Start in this workspace folder, without creating a worktree."
237
+ })] }),
238
+ /* @__PURE__ */ jsxs(View, {
239
+ style: styles$3.field,
240
+ children: [/* @__PURE__ */ jsx(Label, { children: "Name" }), /* @__PURE__ */ jsx(Input, {
241
+ testID: "new-session-name",
242
+ accessibilityLabel: "Session name",
243
+ placeholder: mode === "plain" ? "Optional" : "Optional, defaults to the branch",
244
+ value: form.name,
245
+ onChangeText: (name) => update({ name }),
246
+ disabled: busy
247
+ })]
248
+ }),
249
+ mode !== "plain" ? /* @__PURE__ */ jsx(Text, {
250
+ style: styles$3.dim,
251
+ children: "Runs in its own worktree, so this checkout stays as it is."
252
+ }) : null,
253
+ error ? /* @__PURE__ */ jsx(Text, {
254
+ testID: "new-session-error",
255
+ style: styles$3.error,
256
+ accessibilityRole: "alert",
257
+ children: error
258
+ }) : null,
259
+ /* @__PURE__ */ jsx(Button, {
260
+ testID: "new-session-create",
261
+ variant: "primary",
262
+ size: "lg",
263
+ loading: busy,
264
+ loadingLabel: mode === "plain" ? "Creating session" : "Creating worktree",
265
+ disabled: loading || problem !== void 0,
266
+ accessibilityHint: problem,
267
+ onPress: () => onCreate(state),
268
+ children: "Create session"
269
+ })
270
+ ]
271
+ })] });
272
+ }
273
+ function errorOf(body, status) {
274
+ return typeof body === "object" && body !== null && typeof body.error === "string" ? body.error : `The cockpit answered ${String(status)}.`;
275
+ }
276
+ /** The native new-session sheet when git is installed; mirrors GitNewSessionDialog's calls. */
277
+ function GitNewSessionSheet({ newSession }) {
278
+ const [branches, setBranches] = useState(null);
279
+ const [loadError, setLoadError] = useState("");
280
+ const [error, setError] = useState("");
281
+ const [busy, setBusy] = useState(false);
282
+ const workspaceId = newSession?.workspaceId;
283
+ const request = newSession?.requestWithStepUp;
284
+ useEffect(() => {
285
+ if (workspaceId === void 0 || request === void 0) return;
286
+ let stale = false;
287
+ request(api.workspace(workspaceId).branches.url()).then(async (response) => {
288
+ const body = await response.json().catch(() => void 0);
289
+ if (stale) return;
290
+ if (!response.ok) setLoadError(errorOf(body, response.status));
291
+ else setBranches(body);
292
+ }).catch(() => {
293
+ if (!stale) setLoadError("The branches could not be loaded.");
294
+ });
295
+ return () => {
296
+ stale = true;
297
+ };
298
+ }, [workspaceId, request]);
299
+ if (newSession === void 0) return null;
300
+ const submit = async (state) => {
301
+ if (busy || formProblem(state) !== void 0) return;
302
+ setBusy(true);
303
+ setError("");
304
+ const body = createRequest(state);
305
+ let failure;
306
+ try {
307
+ if (body === void 0) failure = await newSession.createPlain(state.name);
308
+ else {
309
+ const response = await newSession.requestWithStepUp(api.workspace(newSession.workspaceId).createSession.url(), {
310
+ method: "POST",
311
+ headers: { "Content-Type": "application/json" },
312
+ body: JSON.stringify(body)
313
+ });
314
+ const answer = await response.json().catch(() => void 0);
315
+ failure = response.ok ? await newSession.openCreated(answer.sessionId) : errorOf(answer, response.status);
316
+ }
317
+ } catch {
318
+ failure = "The cockpit is unreachable.";
319
+ }
320
+ if (failure === void 0) return;
321
+ setError(failure);
322
+ setBusy(false);
323
+ };
324
+ return /* @__PURE__ */ jsx(GitNewSessionSheetView, {
325
+ workspaceRoot: newSession.workspaceRoot,
326
+ branches,
327
+ loadError,
328
+ busy,
329
+ error,
330
+ onCreate: (state) => void submit(state)
331
+ });
332
+ }
333
+ const styles$3 = StyleSheet.create((theme) => {
334
+ const sans = fontOf(theme.fontFamily.sans);
335
+ const mono = fontOf(theme.fontFamily.mono);
336
+ return {
337
+ body: {
338
+ gap: theme.spacing(4),
339
+ uni__dependencies: [0]
340
+ },
341
+ root: {
342
+ fontFamily: mono,
343
+ fontSize: theme.fontSize.sm,
344
+ lineHeight: theme.lineHeight.sm,
345
+ color: theme.colors.dim,
346
+ textAlign: "center",
347
+ uni__dependencies: [0]
348
+ },
349
+ field: {
350
+ gap: theme.spacing(1.5),
351
+ uni__dependencies: [0]
352
+ },
353
+ dim: {
354
+ fontFamily: sans,
355
+ fontSize: theme.fontSize.sm,
356
+ lineHeight: theme.lineHeightRelaxed.sm,
357
+ color: theme.colors.dim,
358
+ uni__dependencies: [0]
359
+ },
360
+ error: {
361
+ fontFamily: sans,
362
+ fontSize: theme.fontSize.sm,
363
+ lineHeight: theme.lineHeightRelaxed.sm,
364
+ color: theme.colors.red,
365
+ uni__dependencies: [0]
366
+ },
367
+ mono: {
368
+ fontFamily: mono,
369
+ fontSize: theme.fontSize.base,
370
+ lineHeight: theme.lineHeight.base,
371
+ color: theme.colors.hi,
372
+ uni__dependencies: [0]
373
+ },
374
+ list: {
375
+ borderRadius: theme.borderRadius.lg,
376
+ borderWidth: 1,
377
+ borderColor: theme.colors["border-soft"],
378
+ overflow: "hidden",
379
+ uni__dependencies: [0]
380
+ },
381
+ pad: {
382
+ paddingHorizontal: theme.spacing(4),
383
+ paddingVertical: theme.spacing(2),
384
+ uni__dependencies: [0]
385
+ },
386
+ divider: {
387
+ borderTopWidth: StyleSheet.hairlineWidth,
388
+ borderTopColor: theme.colors["border-soft"],
389
+ uni__dependencies: [0]
390
+ }
391
+ };
392
+ });
393
+ //#endregion
394
+ //#region src/extensions/workspaces/(frontend)/fill/GitNewSession.new-session.native.tsx
395
+ var GitNewSession_new_session_native_default = defineFill({ component: GitNewSessionSheet });
396
+ //#endregion
10
397
  //#region src/types/webWorktrees.ts
11
398
  /**
12
399
  * The cockpit's view of this session's worktrees.
@@ -858,56 +1245,6 @@ var git_changes_native_default = defineChannelFile(gitChangesChannel);
858
1245
  //#endregion
859
1246
  //#region src/extensions/workspaces/sessions/(frontend)/channel/git-worktrees.native.ts
860
1247
  var git_worktrees_native_default = defineChannelFile(worktreesChannel);
861
- const api = createApiClient({
862
- /** Workspace: the new-session dialog's branch list. */
863
- branches: {
864
- method: "GET",
865
- path: "/branches",
866
- response: void 0
867
- },
868
- /** Workspace: a top-level worktree session on an existing or new branch. */
869
- createSession: {
870
- method: "POST",
871
- path: "/sessions",
872
- response: void 0
873
- },
874
- /** Workspace: that workspace's saved remote auth, never the token itself. */
875
- auth: {
876
- method: "GET",
877
- path: "/auth",
878
- query: ["repositoryId"],
879
- response: void 0
880
- },
881
- /** Workspace: replaces that workspace's remote auth. Passkey step-up gated for remote devices. */
882
- saveAuth: {
883
- method: "PUT",
884
- path: "/auth",
885
- query: ["repositoryId"],
886
- response: void 0
887
- },
888
- /** Session: the files this session's checkout changed against its base. */
889
- review: {
890
- method: "GET",
891
- path: "/review",
892
- response: void 0
893
- },
894
- /** Session: one changed file's hunks. */
895
- reviewFile: {
896
- method: "GET",
897
- path: "/review/file",
898
- query: ["path"],
899
- response: void 0
900
- }
901
- }, {
902
- scopes: [
903
- "global",
904
- "workspace",
905
- "session"
906
- ],
907
- basePath: "git",
908
- transport: nativeApiTransport,
909
- sessionAddress: sessionApiAddress
910
- });
911
1248
  //#endregion
912
1249
  //#region src/extensions/workspaces/sessions/(frontend)/_lib/gitReviewApi.native.ts
913
1250
  /**
@@ -1318,10 +1655,21 @@ function FileHeader({ entry, index, total, onPrevious, onNext, onChanges }) {
1318
1655
  ]
1319
1656
  });
1320
1657
  }
1321
- function DiffRows({ rows, comments = [], selection, onLine, showInlineComments = true }) {
1658
+ const DiffRows = memo(function DiffRows({ rows, comments = [], selection, onLine, showInlineComments = true, gutterDigits }) {
1659
+ const anchoredComments = useMemo(() => {
1660
+ const index = /* @__PURE__ */ new Map();
1661
+ for (const comment of comments) {
1662
+ const key = `${comment.side}:${comment.endLine ?? comment.startLine}`;
1663
+ const entries = index.get(key);
1664
+ if (entries) entries.push(comment);
1665
+ else index.set(key, [comment]);
1666
+ }
1667
+ return index;
1668
+ }, [comments]);
1669
+ const digits = gutterDigits ?? rows.reduce((max, row) => Math.max(max, String(row.line).length), 1);
1322
1670
  return /* @__PURE__ */ jsx(View, { children: rows.map((row, i) => {
1323
1671
  const side = row.marker === "-" ? "old" : "new";
1324
- const anchored = comments.filter((comment) => comment.side === side && (comment.endLine ?? comment.startLine) === row.line);
1672
+ const anchored = anchoredComments.get(`${side}:${row.line}`) ?? [];
1325
1673
  return /* @__PURE__ */ jsxs(View, { children: [/* @__PURE__ */ jsxs(Pressable, {
1326
1674
  accessibilityRole: "button",
1327
1675
  accessibilityLabel: `Select ${side} line ${row.line}`,
@@ -1334,8 +1682,9 @@ function DiffRows({ rows, comments = [], selection, onLine, showInlineComments =
1334
1682
  ],
1335
1683
  children: [
1336
1684
  /* @__PURE__ */ jsx(Text, {
1685
+ numberOfLines: 1,
1337
1686
  style: [styles$1.mono, styles$1.gutter],
1338
- children: row.line
1687
+ children: String(row.line).padStart(digits, "\xA0")
1339
1688
  }),
1340
1689
  /* @__PURE__ */ jsx(Text, {
1341
1690
  style: [styles$1.dim, styles$1.mono],
@@ -1361,7 +1710,7 @@ function DiffRows({ rows, comments = [], selection, onLine, showInlineComments =
1361
1710
  })]
1362
1711
  }, comment.id))] }, i);
1363
1712
  }) });
1364
- }
1713
+ });
1365
1714
  function lineLabel(comment) {
1366
1715
  return comment.startLine === void 0 ? "File comment" : comment.endLine === comment.startLine || comment.endLine === void 0 ? `Line ${comment.startLine}` : `Lines ${comment.startLine} to ${comment.endLine}`;
1367
1716
  }
@@ -1637,10 +1986,20 @@ const styles$1 = StyleSheet.create((theme) => ({
1637
1986
  },
1638
1987
  gutter: {
1639
1988
  color: theme.colors.dim,
1640
- width: 24,
1989
+ width: "auto",
1990
+ minWidth: 24,
1991
+ flexShrink: 0,
1641
1992
  textAlign: "right",
1642
1993
  uni__dependencies: [0]
1643
1994
  },
1995
+ disclosure: {
1996
+ padding: theme.spacing(3),
1997
+ gap: theme.spacing(2),
1998
+ borderTopWidth: 1,
1999
+ borderColor: theme.colors.border,
2000
+ backgroundColor: theme.colors.bg,
2001
+ uni__dependencies: [0]
2002
+ },
1644
2003
  code: {
1645
2004
  flex: 1,
1646
2005
  minWidth: 0,
@@ -1676,8 +2035,25 @@ const styles$1 = StyleSheet.create((theme) => ({
1676
2035
  },
1677
2036
  remove: { alignSelf: "flex-end" }
1678
2037
  }));
2038
+ const DIFF_ROW_STEP = 200;
1679
2039
  function GitReviewView(props) {
1680
2040
  const { screen, activity, summary, file, path, comments } = props;
2041
+ const rows = useMemo(() => file?.hunks.flatMap((hunk) => hunk.rows) ?? [], [file?.hunks]);
2042
+ const gutterDigits = useMemo(() => rows.reduce((max, row) => Math.max(max, String(row.line).length), 1), [rows]);
2043
+ const fileComments = useMemo(() => comments.filter((comment) => comment.path === path), [comments, path]);
2044
+ const initialLimit = props.initialVisibleRows ?? DIFF_ROW_STEP;
2045
+ const [disclosure, setDisclosure] = useState({
2046
+ rows,
2047
+ path,
2048
+ limit: initialLimit
2049
+ });
2050
+ const current = disclosure.rows === rows && disclosure.path === path ? disclosure : {
2051
+ rows,
2052
+ path,
2053
+ limit: initialLimit
2054
+ };
2055
+ if (current !== disclosure) setDisclosure(current);
2056
+ const visibleRows = useMemo(() => rows.slice(0, current.limit), [rows, current.limit]);
1681
2057
  const index = summary?.files.findIndex((entry) => entry.path === path) ?? -1;
1682
2058
  const entry = summary?.files[index];
1683
2059
  return /* @__PURE__ */ jsxs(View, {
@@ -1789,13 +2165,14 @@ function GitReviewView(props) {
1789
2165
  style: styles$1.section,
1790
2166
  children: file.binary ? "Binary file" : "Diff too large to display"
1791
2167
  })
1792
- }) : file.hunks.map((hunk, i) => /* @__PURE__ */ jsx(DiffRows, {
1793
- rows: hunk.rows,
1794
- comments: comments.filter((comment) => comment.path === file.path),
2168
+ }) : /* @__PURE__ */ jsx(DiffRows, {
2169
+ rows: visibleRows,
2170
+ comments: fileComments,
1795
2171
  selection: props.selection,
1796
- onLine: props.onLine
1797
- }, i)),
1798
- comments.filter((comment) => comment.path === path && comment.startLine === void 0).map((comment) => /* @__PURE__ */ jsxs(View, {
2172
+ onLine: props.onLine,
2173
+ gutterDigits
2174
+ }),
2175
+ fileComments.filter((comment) => comment.startLine === void 0).map((comment) => /* @__PURE__ */ jsxs(View, {
1799
2176
  style: styles$1.inlineComment,
1800
2177
  children: [/* @__PURE__ */ jsx(Text, {
1801
2178
  style: styles$1.blue,
@@ -1819,7 +2196,7 @@ function GitReviewView(props) {
1819
2196
  style: [styles$1.red, styles$1.content],
1820
2197
  children: props.error
1821
2198
  }) : null]
1822
- }),
2199
+ }, `${screen}:${path ?? ""}`),
1823
2200
  /* @__PURE__ */ jsx(Footer, {
1824
2201
  count: comments.length,
1825
2202
  confirmation: screen === "changes" && activity.changes?.rebase !== void 0,
@@ -1827,7 +2204,35 @@ function GitReviewView(props) {
1827
2204
  onReview: () => props.onScreen("review"),
1828
2205
  onSend: props.onSend,
1829
2206
  onDiscard: props.onDiscard
1830
- })
2207
+ }),
2208
+ screen === "file" && file && !file.binary && !file.tooLarge ? /* @__PURE__ */ jsxs(View, {
2209
+ style: styles$1.disclosure,
2210
+ children: [/* @__PURE__ */ jsxs(Text, {
2211
+ style: styles$1.dim,
2212
+ accessibilityLiveRegion: "polite",
2213
+ children: [
2214
+ Math.min(current.limit, rows.length),
2215
+ " of ",
2216
+ rows.length,
2217
+ " diff rows shown",
2218
+ props.selection ? ` · ${props.selection.side} line ${props.selection.startLine} selected` : ""
2219
+ ]
2220
+ }), current.limit < rows.length ? /* @__PURE__ */ jsxs(Button, {
2221
+ variant: "outline",
2222
+ onPress: () => setDisclosure({
2223
+ ...current,
2224
+ limit: Math.min(rows.length, current.limit + DIFF_ROW_STEP)
2225
+ }),
2226
+ children: [
2227
+ "Show ",
2228
+ Math.min(DIFF_ROW_STEP, rows.length - current.limit),
2229
+ " more rows"
2230
+ ]
2231
+ }) : /* @__PURE__ */ jsx(Text, {
2232
+ style: styles$1.dim,
2233
+ children: "All diff rows shown"
2234
+ })]
2235
+ }) : null
1831
2236
  ]
1832
2237
  });
1833
2238
  }
@@ -1866,7 +2271,6 @@ function GitReviewPanelSession({ sessionId, sendSessionFrame }) {
1866
2271
  const [path, setPath] = useState();
1867
2272
  const [selection, setSelection] = useState();
1868
2273
  const [draft, setDraft] = useState();
1869
- const [comment, setComment] = useState("");
1870
2274
  const request = useRef(void 0);
1871
2275
  const fingerprint = JSON.stringify(activity.changes);
1872
2276
  useEffect(() => {
@@ -1976,20 +2380,16 @@ function GitReviewPanelSession({ sessionId, sendSessionFrame }) {
1976
2380
  if (!open) {
1977
2381
  setDraft(void 0);
1978
2382
  setSelection(void 0);
1979
- setComment("");
1980
2383
  }
1981
2384
  },
1982
- children: /* @__PURE__ */ jsx(SheetContent, { children: /* @__PURE__ */ jsx(SheetBody, { children: /* @__PURE__ */ jsx(CommentSheet, {
2385
+ children: /* @__PURE__ */ jsx(SheetContent, { children: /* @__PURE__ */ jsx(SheetBody, { children: /* @__PURE__ */ jsx(CommentEditor, {
1983
2386
  range: draft?.range,
1984
2387
  snippet: draft?.snippet ?? "",
1985
- value: comment,
1986
- onChange: setComment,
1987
2388
  onCancel: () => {
1988
2389
  setDraft(void 0);
1989
2390
  setSelection(void 0);
1990
- setComment("");
1991
2391
  },
1992
- onAdd: () => {
2392
+ onAdd: (comment) => {
1993
2393
  if (!draft || !comment.trim()) return;
1994
2394
  addReviewComment(sessionId, {
1995
2395
  path: draft.path,
@@ -2000,9 +2400,8 @@ function GitReviewPanelSession({ sessionId, sendSessionFrame }) {
2000
2400
  });
2001
2401
  setDraft(void 0);
2002
2402
  setSelection(void 0);
2003
- setComment("");
2004
2403
  }
2005
- }) }) })
2404
+ }, draft ? "open" : "closed") }) })
2006
2405
  }),
2007
2406
  /* @__PURE__ */ jsx(Sheet, {
2008
2407
  open: confirm !== void 0,
@@ -2034,6 +2433,15 @@ function GitReviewPanelSession({ sessionId, sendSessionFrame }) {
2034
2433
  })
2035
2434
  ] });
2036
2435
  }
2436
+ function CommentEditor({ onAdd, ...props }) {
2437
+ const [body, setBody] = useState("");
2438
+ return /* @__PURE__ */ jsx(CommentSheet, {
2439
+ ...props,
2440
+ value: body,
2441
+ onChange: setBody,
2442
+ onAdd: () => onAdd(body)
2443
+ });
2444
+ }
2037
2445
  //#endregion
2038
2446
  //#region src/extensions/workspaces/sessions/(frontend)/fill/_components/WorktreesActivitySection.native.tsx
2039
2447
  function GitChangesRow({ state = "changes", branch = "Detached HEAD", added = 0, removed = 0, onReview }) {
@@ -2241,6 +2649,12 @@ const via = (identity, value) => value === void 0 ? void 0 : {
2241
2649
  };
2242
2650
  const nativePlugin = defineNativePlugin({
2243
2651
  id: "git",
2652
+ workspace: { get fills() {
2653
+ return [via({
2654
+ slot: "new-session",
2655
+ id: "git-new-session"
2656
+ }, GitNewSession_new_session_native_default)];
2657
+ } },
2244
2658
  session: {
2245
2659
  get activityGroups() {
2246
2660
  return [via({ name: "git" }, git_native_default)];